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:
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";