web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

commit ff686a496ef787f2735e87049b1729cb02bf6e10
parent 1a55ace5c450be5b2bc9e3b5c9fdeac84dd7d3c7
Author: triesap <triesap@radroots.dev>
Date:   Fri, 26 Dec 2025 21:13:41 +0000

nostr: harden event tagging and context propagation

- Emit/parse profile actor tags and export tag helpers from index
- Add farm ref tag builder, list private entry JSON/parse helpers, and validate plot farm pubkey
- Store carousel context in a writable and sync derived carousel updates
- Add view context fallback store effect and drop view-pane tabindex prop

Diffstat:
Mapps-lib-pwa/src/lib/components/lib/carousel-container.svelte | 24++++++++++++++++++------
Mapps-lib-pwa/src/lib/components/lib/carousel-item.svelte | 13++++++++++---
Mapps-lib/src/lib/components/view-pane.svelte | 26++++++++++++++++++++++----
Mapps-lib/src/lib/components/view-stack.svelte | 4++--
Mnostr/src/events/farm/lib.ts | 12+++++++++++-
Mnostr/src/events/list/lib.ts | 37++++++++++++++++++++++++++++++++++++-
Mnostr/src/events/list_set/lib.ts | 2++
Mnostr/src/events/plot/parse.ts | 3+++
Mnostr/src/events/profile/lib.ts | 11+++++++----
Mnostr/src/events/profile/parse.ts | 11++++++++---
Anostr/src/events/profile/tags.ts | 25+++++++++++++++++++++++++
Mnostr/src/index.ts | 1+
12 files changed, 145 insertions(+), 24 deletions(-)

diff --git a/apps-lib-pwa/src/lib/components/lib/carousel-container.svelte b/apps-lib-pwa/src/lib/components/lib/carousel-container.svelte @@ -6,8 +6,10 @@ carousel_watch, fmt_cl, set_context, + type CarouselStore, } from "@radroots/apps-lib"; import type { Snippet } from "svelte"; + import { writable } from "svelte/store"; let { basis, @@ -17,9 +19,19 @@ children: Snippet; } = $props(); - if (basis.carousel) set_context(CAROUSEL_CONTEXT_KEY, basis.carousel); + const carousel_context = writable<CarouselStore<string> | undefined>( + undefined, + ); + + set_context(CAROUSEL_CONTEXT_KEY, carousel_context); + + const carousel = $derived(basis.carousel ?? undefined); + + $effect(() => { + carousel_context.set(carousel); + }); - const view = $derived(basis.view ?? basis.carousel?.view ?? ``); + const view = $derived(basis.view ?? carousel?.view ?? ``); const classes = $derived( `${fmt_cl(basis.classes)} carousel-container flex h-full w-full`, @@ -28,13 +40,13 @@ let container_el: HTMLDivElement | null = $state(null); $effect(() => { - if (!basis.carousel) return; - return carousel_register_container(basis.carousel, container_el); + if (!carousel) return; + return carousel_register_container(carousel, container_el); }); $effect(() => { - if (!basis.carousel) return; - return carousel_watch(basis.carousel); + if (!carousel) return; + return carousel_watch(carousel); }); </script> diff --git a/apps-lib-pwa/src/lib/components/lib/carousel-item.svelte b/apps-lib-pwa/src/lib/components/lib/carousel-item.svelte @@ -12,6 +12,7 @@ get_context, } from "@radroots/apps-lib"; import type { Snippet } from "svelte"; + import { writable, type Writable } from "svelte/store"; let { basis, @@ -21,11 +22,17 @@ children: Snippet; } = $props(); - const carousel_ctx = get_context<CarouselStore<string> | undefined>( - CAROUSEL_CONTEXT_KEY, + const carousel_context_fallback = writable<CarouselStore<string> | undefined>( + undefined, ); + const carousel_context_store = + get_context<Writable<CarouselStore<string> | undefined> | undefined>( + CAROUSEL_CONTEXT_KEY, + ) ?? carousel_context_fallback; + + const carousel_context_value = $derived($carousel_context_store); - const carousel = $derived(basis.carousel ?? carousel_ctx); + const carousel = $derived(basis.carousel ?? carousel_context_value); const view = $derived(basis.view ?? carousel?.view ?? ``); const classes = $derived( diff --git a/apps-lib/src/lib/components/view-pane.svelte b/apps-lib/src/lib/components/view-pane.svelte @@ -61,7 +61,6 @@ style_active?: string; style_inactive?: string; role?: string; - tabindex?: number; aria_label?: string; }; @@ -73,11 +72,30 @@ children: Snippet; } = $props(); + const view_context_fallback = writable<ViewContext<string>>({ + active_view: "", + mode: "stack", + fade: true, + transition_ms: DEFAULT_TRANSITION_MS, + opacity_inactive: DEFAULT_OPACITY_INACTIVE, + scale_inactive: DEFAULT_SCALE_INACTIVE, + offset_x: DEFAULT_OFFSET_X, + offset_y: DEFAULT_OFFSET_Y, + blur_inactive_px: DEFAULT_BLUR_INACTIVE_PX, + pointer_events_inactive: DEFAULT_POINTER_EVENTS_INACTIVE, + z_index_active: DEFAULT_Z_INDEX_ACTIVE, + z_index_inactive: DEFAULT_Z_INDEX_INACTIVE, + }); + const view_context_store = get_context<Writable<ViewContext<string>> | undefined>( VIEW_CONTEXT_KEY, - ) ?? - writable<ViewContext<string>>({ + ) ?? view_context_fallback; + const use_fallback = view_context_store === view_context_fallback; + + $effect(() => { + if (!use_fallback) return; + view_context_fallback.set({ active_view: basis.active_view ?? basis.view, mode: basis.mode ?? "stack", fade: basis.fade ?? true, @@ -93,6 +111,7 @@ z_index_active: basis.z_index_active ?? DEFAULT_Z_INDEX_ACTIVE, z_index_inactive: basis.z_index_inactive ?? DEFAULT_Z_INDEX_INACTIVE, }); + }); const view_context_value = $derived($view_context_store); @@ -186,7 +205,6 @@ data-view={basis.view} style={style} role={basis.role ?? undefined} - tabindex={basis.tabindex ?? undefined} aria-label={basis.aria_label ?? undefined} aria-hidden={!is_active} > diff --git a/apps-lib/src/lib/components/view-stack.svelte b/apps-lib/src/lib/components/view-stack.svelte @@ -68,7 +68,7 @@ z_index_inactive: DEFAULT_Z_INDEX_INACTIVE, }); - const view_context_value = $derived((): ViewContext<string> => ({ + const view_context_value = $derived({ active_view: basis.active_view, mode: basis.mode ?? "stack", fade: basis.fade ?? true, @@ -82,7 +82,7 @@ basis.pointer_events_inactive ?? DEFAULT_POINTER_EVENTS_INACTIVE, z_index_active: basis.z_index_active ?? DEFAULT_Z_INDEX_ACTIVE, z_index_inactive: basis.z_index_inactive ?? DEFAULT_Z_INDEX_INACTIVE, - })); + }); set_context(VIEW_CONTEXT_KEY, view_context); diff --git a/nostr/src/events/farm/lib.ts b/nostr/src/events/farm/lib.ts @@ -1,4 +1,5 @@ -import type { RadrootsFarm } from "@radroots/events-bindings"; +import type { RadrootsFarm, RadrootsFarmRef } from "@radroots/events-bindings"; +import type { NostrEventTags } from "../../types/lib.js"; import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js"; import { nostr_event_create } from "../lib.js"; import { tags_farm } from "./tags.js"; @@ -6,6 +7,15 @@ import { tags_farm } from "./tags.js"; export const KIND_RADROOTS_FARM = 30340; export type KindRadrootsFarm = typeof KIND_RADROOTS_FARM; +export const nostr_tags_farm_ref = (farm: RadrootsFarmRef): NostrEventTags | undefined => { + if (!farm.pubkey.trim()) return undefined; + if (!farm.d_tag.trim()) return undefined; + return [ + ["p", farm.pubkey], + ["a", `${KIND_RADROOTS_FARM}:${farm.pubkey}:${farm.d_tag}`], + ]; +}; + export const nostr_event_farm = async ( opts: NostrEventFigure<{ data: RadrootsFarm }>, ): Promise<NostrSignedEvent | undefined> => { diff --git a/nostr/src/events/list/lib.ts b/nostr/src/events/list/lib.ts @@ -1,4 +1,4 @@ -import type { RadrootsList } from "@radroots/events-bindings"; +import type { RadrootsList, RadrootsListEntry } from "@radroots/events-bindings"; import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js"; import { nostr_event_create } from "../lib.js"; import { tags_list } from "./tags.js"; @@ -42,3 +42,38 @@ export const nostr_event_list = async ( }, }); }; + +const is_string_array = (value: unknown): value is string[] => + Array.isArray(value) && value.every(item => typeof item === "string"); + +export const list_private_entries_json = (entries: RadrootsListEntry[]): string | undefined => { + const tags: string[][] = []; + for (const entry of entries) { + if (!entry.tag.trim()) return undefined; + const first = entry.values[0]; + if (!first || !first.trim()) return undefined; + tags.push([entry.tag, ...entry.values]); + } + return JSON.stringify(tags); +}; + +export const list_private_entries_parse = (content: string): RadrootsListEntry[] | undefined => { + let parsed: unknown; + try { + parsed = JSON.parse(content); + } catch { + return undefined; + } + if (!Array.isArray(parsed)) return undefined; + const entries: RadrootsListEntry[] = []; + for (const tag of parsed) { + if (!is_string_array(tag)) return undefined; + if (!tag.length) return undefined; + const [name, ...values] = tag; + if (!name.trim()) return undefined; + const first = values[0]; + if (!first || !first.trim()) return undefined; + entries.push({ tag: name, values }); + } + return entries; +}; diff --git a/nostr/src/events/list_set/lib.ts b/nostr/src/events/list_set/lib.ts @@ -41,3 +41,5 @@ export const nostr_event_list_set = async ( }, }); }; + +export { list_private_entries_json, list_private_entries_parse } from "../list/lib.js"; diff --git a/nostr/src/events/plot/parse.ts b/nostr/src/events/plot/parse.ts @@ -33,8 +33,11 @@ export const parse_nostr_plot_event = ( if (!d_tag) return undefined; const farm_addr = get_event_tag(event.tags, "a"); if (!farm_addr) return undefined; + const farm_pubkey = get_event_tag(event.tags, "p"); + if (!farm_pubkey) return undefined; const farm_ref = parse_farm_addr(farm_addr); if (!farm_ref) return undefined; + if (farm_pubkey !== farm_ref.pubkey) return undefined; try { const parsed = JSON.parse(event.content); const plot = radroots_plot_schema.parse(parsed); diff --git a/nostr/src/events/profile/lib.ts b/nostr/src/events/profile/lib.ts @@ -1,19 +1,22 @@ -import type { RadrootsProfile } from "@radroots/events-bindings"; +import type { RadrootsActorType, RadrootsProfile } from "@radroots/events-bindings"; import type { NostrEventFigure, NostrSignedEvent } from "../../types/nostr.js"; import { nostr_event_create } from "../lib.js"; +import { tags_profile_actor } from "./tags.js"; export const KIND_RADROOTS_PROFILE = 0; export type KindRadrootsProfile = typeof KIND_RADROOTS_PROFILE; export const nostr_event_profile = async ( - opts: NostrEventFigure<{ data: RadrootsProfile }>, + opts: NostrEventFigure<{ data: RadrootsProfile; actor?: RadrootsActorType }>, ): Promise<NostrSignedEvent | undefined> => { - const { data } = opts; + const { data, actor, ...event_opts } = opts; + const tags = tags_profile_actor(actor); return nostr_event_create({ - ...opts, + ...event_opts, basis: { kind: KIND_RADROOTS_PROFILE, content: JSON.stringify(data), + tags: tags.length ? tags : undefined, }, }); }; diff --git a/nostr/src/events/profile/parse.ts b/nostr/src/events/profile/parse.ts @@ -1,11 +1,15 @@ -import type { RadrootsProfile } from "@radroots/events-bindings"; +import type { RadrootsActorType, RadrootsProfile } from "@radroots/events-bindings"; import { radroots_profile_schema } from "@radroots/events-bindings"; import type { NostrEvent } from "../../types/nostr.js"; import { parse_nostr_event_basis } from "../lib.js"; import type { NostrEventBasis } from "../subscription.js"; import { KIND_RADROOTS_PROFILE, type KindRadrootsProfile } from "./lib.js"; +import { parse_profile_actor_tag } from "./tags.js"; -export type RadrootsProfileNostrEvent = NostrEventBasis<KindRadrootsProfile> & { profile: RadrootsProfile }; +export type RadrootsProfileNostrEvent = NostrEventBasis<KindRadrootsProfile> & { + profile: RadrootsProfile; + actor_type?: RadrootsActorType; +}; export const parse_nostr_profile_event = ( event: NostrEvent, @@ -15,7 +19,8 @@ export const parse_nostr_profile_event = ( try { const parsed = JSON.parse(event.content); const profile = radroots_profile_schema.parse(parsed); - return { ...ev, profile }; + const actor_type = parse_profile_actor_tag(event.tags); + return actor_type ? { ...ev, profile, actor_type } : { ...ev, profile }; } catch { return undefined; } diff --git a/nostr/src/events/profile/tags.ts b/nostr/src/events/profile/tags.ts @@ -0,0 +1,25 @@ +import type { RadrootsActorType } from "@radroots/events-bindings"; +import type { NostrEventTags } from "../../types/lib.js"; + +const ACTOR_TAG_KEY = "t"; +const ACTOR_TAG_PREFIX = "radroots:actor:"; + +const is_actor_type = (value: string): value is RadrootsActorType => + value === "person" || value === "farm"; + +export const radroots_actor_tag_value = (actor: RadrootsActorType): string => + `${ACTOR_TAG_PREFIX}${actor}`; + +export const tags_profile_actor = (actor?: RadrootsActorType): NostrEventTags => + actor ? [[ACTOR_TAG_KEY, radroots_actor_tag_value(actor)]] : []; + +export const parse_profile_actor_tag = (tags: NostrEventTags): RadrootsActorType | undefined => { + for (const tag of tags) { + if (tag[0] !== ACTOR_TAG_KEY) continue; + const value = tag[1]; + if (!value || !value.startsWith(ACTOR_TAG_PREFIX)) continue; + const actor = value.slice(ACTOR_TAG_PREFIX.length); + if (is_actor_type(actor)) return actor; + } + return undefined; +}; diff --git a/nostr/src/index.ts b/nostr/src/index.ts @@ -35,6 +35,7 @@ export * from "./events/plot/parse.js"; export * from "./events/plot/tags.js"; export * from "./events/profile/lib.js"; export * from "./events/profile/parse.js"; +export * from "./events/profile/tags.js"; export * from "./events/reaction/lib.js"; export * from "./events/reaction/parse.js"; export * from "./events/reaction/tags.js";