diff --git a/docs/embed-sdk/v0.md b/docs/embed-sdk/v0.md index 9b3d2ab..bb50d69 100644 --- a/docs/embed-sdk/v0.md +++ b/docs/embed-sdk/v0.md @@ -44,9 +44,13 @@ the parent. Wait for `Blento.ready` before any write. Each origin is added to a hardcoded server-side allowlist with the collection NSID prefixes it may write. v0 ships with: -| Origin | Allowed collection prefixes | -| ------------------- | ----------------------------- | -| `https://atmo.rsvp` | `community.lexicon.calendar.` | +| Origin | Allowed collections | +| ------------------- | ---------------------------------------------------- | +| `https://atmo.rsvp` | `community.lexicon.calendar.*`, `app.bsky.feed.post` | + +Prefix entries ending with `.` match anything under that namespace +(`community.lexicon.calendar.event`, `community.lexicon.calendar.rsvp`, …). +Entries without a trailing dot match the exact NSID only. Adding a new origin or collection requires: @@ -196,6 +200,37 @@ if (!Blento.getSession()) { Calling `promptLogin()` while the user is already signed in is a no-op from the iframe's perspective; the parent may still display the modal. +### `Blento.notify(name: string, payload?: unknown): void` + +Generic iframe → parent signal for app-defined events. Names are not +validated by Blento — they're a contract between your embed and the Blento +surface that hosts it. Fire-and-forget; no response. + +Typical uses: tell the parent to close a modal after a successful create, +nudge the parent to refresh a sibling counter, surface an "edit cancelled" +intent. + +```js +// in the iframe +await Blento.createRecord({ ... }); +Blento.notify('event-created', { uri }); + +// in Blento, on the host component + { + if (name === 'event-created') closeModal(); + if (name === 'cancel') closeModal(); + }} +/> +``` + +Prefer `notify()` over `notifyNavigate()` when the parent wants to react +locally (close a modal, show a toast, refresh a count) without changing the +top-level URL. + ## Errors All write rejections are `BlentoError` instances with a stable `.code`: @@ -237,6 +272,7 @@ implement directly. All messages include `v: 0`. { v: 0, type: 'blento:resize', heightPx } // unsolicited { v: 0, type: 'blento:navigate', url } // unsolicited { v: 0, type: 'blento:promptLogin' } // unsolicited +{ v: 0, type: 'blento:notify', name, payload? } // unsolicited ``` `id` is any unique string you generate — the parent echoes it on the response. diff --git a/src/lib/embed/AtmoEmbed.svelte b/src/lib/embed/AtmoEmbed.svelte index 7cc3490..a301cc1 100644 --- a/src/lib/embed/AtmoEmbed.svelte +++ b/src/lib/embed/AtmoEmbed.svelte @@ -21,6 +21,7 @@ maxHeight?: number; title?: string; class?: string; + onnotify?: (name: string, payload: unknown) => void; }; let { @@ -31,7 +32,8 @@ minHeight = 80, maxHeight = 20000, title = 'Embedded content', - class: className = '' + class: className = '', + onnotify }: Props = $props(); const PROTOCOL_VERSION = 0; @@ -61,8 +63,8 @@ function isAllowedCollectionLocal(collection: string): boolean { return allowedCollectionPrefixes.some((p) => { if (p === '*') return true; - const stripped = p.replace(/\.$/, ''); - return collection === stripped || collection.startsWith(p); + if (p.endsWith('.')) return collection.startsWith(p); + return collection === p; }); } @@ -207,6 +209,11 @@ return; } + if (data.type === 'blento:notify' && typeof data.name === 'string') { + onnotify?.(data.name, data.payload); + return; + } + if (typeof data.id === 'string' && typeof data.type === 'string') { handleRequest(data.id, data.type, data.payload); } diff --git a/src/lib/embed/allowlist.ts b/src/lib/embed/allowlist.ts index b51fd99..1f1b363 100644 --- a/src/lib/embed/allowlist.ts +++ b/src/lib/embed/allowlist.ts @@ -7,7 +7,7 @@ export type AllowlistEntry = { const PROD_ALLOWLIST: Record = { 'https://atmo.rsvp': { - collectionPrefixes: ['community.lexicon.calendar.'], + collectionPrefixes: ['community.lexicon.calendar.', 'app.bsky.feed.post'], label: 'atmo.rsvp' } }; @@ -33,7 +33,8 @@ export function isAllowedOrigin(origin: string): boolean { function matchesPrefix(collection: string, prefix: string): boolean { if (prefix === '*') return true; - return collection === prefix.replace(/\.$/, '') || collection.startsWith(prefix); + if (prefix.endsWith('.')) return collection.startsWith(prefix); + return collection === prefix; } export function isAllowedCollection(origin: string, collection: string): boolean { diff --git a/src/routes/embed-test/+page.svelte b/src/routes/embed-test/+page.svelte index db0e84b..7d5ee49 100644 --- a/src/routes/embed-test/+page.svelte +++ b/src/routes/embed-test/+page.svelte @@ -4,6 +4,7 @@ import { user } from '$lib/atproto'; let origin = $state(''); + let lastNotify = $state<{ name: string; payload: unknown; at: number } | null>(null); onMount(() => { origin = window.location.origin; @@ -24,6 +25,12 @@

Logged in as: {user.profile?.handle ?? user.did ?? 'not signed in'}

+ {#if lastNotify} +

+ Last notify: {lastNotify.name} · + {JSON.stringify(lastNotify.payload)} +

+ {/if} {#if origin} @@ -34,6 +41,9 @@ height={700} title="Embed SDK test harness" class="w-full rounded-lg border border-black/10 dark:border-white/10" + onnotify={(name, payload) => { + lastNotify = { name, payload, at: Date.now() }; + }} /> {/if} diff --git a/static/embed/v0/sdk.js b/static/embed/v0/sdk.js index 1ab4bd6..8fe7fa6 100644 --- a/static/embed/v0/sdk.js +++ b/static/embed/v0/sdk.js @@ -17,6 +17,7 @@ * { v: 0, type: 'blento:resize', heightPx } * { v: 0, type: 'blento:navigate', url } * { v: 0, type: 'blento:promptLogin' } + * { v: 0, type: 'blento:notify', name, payload? } * * ─── Wire protocol (parent → iframe) ───────────────────────────────────────── * { v: 0, type: 'ready', session } // sent once after handshake @@ -206,6 +207,12 @@ }, promptLogin: function () { sendToParent({ v: PROTOCOL_VERSION, type: 'blento:promptLogin' }); + }, + notify: function (name, payload) { + if (typeof name !== 'string' || !name) { + throw new BlentoError('invalid_request', 'notify(name): name must be a non-empty string'); + } + sendToParent({ v: PROTOCOL_VERSION, type: 'blento:notify', name: name, payload: payload }); } }; diff --git a/static/embed/v0/test.html b/static/embed/v0/test.html index 19b652d..ed81e35 100644 --- a/static/embed/v0/test.html +++ b/static/embed/v0/test.html @@ -131,6 +131,7 @@ +
@@ -255,6 +256,12 @@ show('promptLogin() sent', null); }; + $('btn-notify').onclick = () => { + const payload = { ts: Date.now() }; + window.Blento.notify('test-event', payload); + show('notify("test-event") sent', payload); + }; + if (window.Blento.getTheme().dark) { document.documentElement.classList.add('dark'); }