your personal website on atproto - mirror blento.app
25
fork

Configure Feed

Select the types of activity you want to include in your feed.

moar updates

+76 -8
+39 -3
docs/embed-sdk/v0.md
··· 44 44 Each origin is added to a hardcoded server-side allowlist with the collection 45 45 NSID prefixes it may write. v0 ships with: 46 46 47 - | Origin | Allowed collection prefixes | 48 - | ------------------- | ----------------------------- | 49 - | `https://atmo.rsvp` | `community.lexicon.calendar.` | 47 + | Origin | Allowed collections | 48 + | ------------------- | ---------------------------------------------------- | 49 + | `https://atmo.rsvp` | `community.lexicon.calendar.*`, `app.bsky.feed.post` | 50 + 51 + Prefix entries ending with `.` match anything under that namespace 52 + (`community.lexicon.calendar.event`, `community.lexicon.calendar.rsvp`, …). 53 + Entries without a trailing dot match the exact NSID only. 50 54 51 55 Adding a new origin or collection requires: 52 56 ··· 196 200 Calling `promptLogin()` while the user is already signed in is a no-op from 197 201 the iframe's perspective; the parent may still display the modal. 198 202 203 + ### `Blento.notify(name: string, payload?: unknown): void` 204 + 205 + Generic iframe → parent signal for app-defined events. Names are not 206 + validated by Blento — they're a contract between your embed and the Blento 207 + surface that hosts it. Fire-and-forget; no response. 208 + 209 + Typical uses: tell the parent to close a modal after a successful create, 210 + nudge the parent to refresh a sibling counter, surface an "edit cancelled" 211 + intent. 212 + 213 + ```js 214 + // in the iframe 215 + await Blento.createRecord({ ... }); 216 + Blento.notify('event-created', { uri }); 217 + 218 + // in Blento, on the host component 219 + <AtmoEmbed 220 + origin="https://atmo.rsvp" 221 + path="/embed/create" 222 + allowedCollectionPrefixes={['community.lexicon.calendar.']} 223 + onnotify={(name, payload) => { 224 + if (name === 'event-created') closeModal(); 225 + if (name === 'cancel') closeModal(); 226 + }} 227 + /> 228 + ``` 229 + 230 + Prefer `notify()` over `notifyNavigate()` when the parent wants to react 231 + locally (close a modal, show a toast, refresh a count) without changing the 232 + top-level URL. 233 + 199 234 ## Errors 200 235 201 236 All write rejections are `BlentoError` instances with a stable `.code`: ··· 237 272 { v: 0, type: 'blento:resize', heightPx } // unsolicited 238 273 { v: 0, type: 'blento:navigate', url } // unsolicited 239 274 { v: 0, type: 'blento:promptLogin' } // unsolicited 275 + { v: 0, type: 'blento:notify', name, payload? } // unsolicited 240 276 ``` 241 277 242 278 `id` is any unique string you generate — the parent echoes it on the response.
+10 -3
src/lib/embed/AtmoEmbed.svelte
··· 21 21 maxHeight?: number; 22 22 title?: string; 23 23 class?: string; 24 + onnotify?: (name: string, payload: unknown) => void; 24 25 }; 25 26 26 27 let { ··· 31 32 minHeight = 80, 32 33 maxHeight = 20000, 33 34 title = 'Embedded content', 34 - class: className = '' 35 + class: className = '', 36 + onnotify 35 37 }: Props = $props(); 36 38 37 39 const PROTOCOL_VERSION = 0; ··· 61 63 function isAllowedCollectionLocal(collection: string): boolean { 62 64 return allowedCollectionPrefixes.some((p) => { 63 65 if (p === '*') return true; 64 - const stripped = p.replace(/\.$/, ''); 65 - return collection === stripped || collection.startsWith(p); 66 + if (p.endsWith('.')) return collection.startsWith(p); 67 + return collection === p; 66 68 }); 67 69 } 68 70 ··· 204 206 205 207 if (data.type === 'blento:promptLogin') { 206 208 atProtoLoginModalState.show(); 209 + return; 210 + } 211 + 212 + if (data.type === 'blento:notify' && typeof data.name === 'string') { 213 + onnotify?.(data.name, data.payload); 207 214 return; 208 215 } 209 216
+3 -2
src/lib/embed/allowlist.ts
··· 7 7 8 8 const PROD_ALLOWLIST: Record<string, AllowlistEntry> = { 9 9 'https://atmo.rsvp': { 10 - collectionPrefixes: ['community.lexicon.calendar.'], 10 + collectionPrefixes: ['community.lexicon.calendar.', 'app.bsky.feed.post'], 11 11 label: 'atmo.rsvp' 12 12 } 13 13 }; ··· 33 33 34 34 function matchesPrefix(collection: string, prefix: string): boolean { 35 35 if (prefix === '*') return true; 36 - return collection === prefix.replace(/\.$/, '') || collection.startsWith(prefix); 36 + if (prefix.endsWith('.')) return collection.startsWith(prefix); 37 + return collection === prefix; 37 38 } 38 39 39 40 export function isAllowedCollection(origin: string, collection: string): boolean {
+10
src/routes/embed-test/+page.svelte
··· 4 4 import { user } from '$lib/atproto'; 5 5 6 6 let origin = $state(''); 7 + let lastNotify = $state<{ name: string; payload: unknown; at: number } | null>(null); 7 8 8 9 onMount(() => { 9 10 origin = window.location.origin; ··· 24 25 <p class="text-sm opacity-70"> 25 26 Logged in as: <strong>{user.profile?.handle ?? user.did ?? 'not signed in'}</strong> 26 27 </p> 28 + {#if lastNotify} 29 + <p class="text-sm opacity-70"> 30 + Last notify: <code>{lastNotify.name}</code> · 31 + <code>{JSON.stringify(lastNotify.payload)}</code> 32 + </p> 33 + {/if} 27 34 </header> 28 35 29 36 {#if origin} ··· 34 41 height={700} 35 42 title="Embed SDK test harness" 36 43 class="w-full rounded-lg border border-black/10 dark:border-white/10" 44 + onnotify={(name, payload) => { 45 + lastNotify = { name, payload, at: Date.now() }; 46 + }} 37 47 /> 38 48 {/if} 39 49 </main>
+7
static/embed/v0/sdk.js
··· 17 17 * { v: 0, type: 'blento:resize', heightPx } 18 18 * { v: 0, type: 'blento:navigate', url } 19 19 * { v: 0, type: 'blento:promptLogin' } 20 + * { v: 0, type: 'blento:notify', name, payload? } 20 21 * 21 22 * ─── Wire protocol (parent → iframe) ───────────────────────────────────────── 22 23 * { v: 0, type: 'ready', session } // sent once after handshake ··· 206 207 }, 207 208 promptLogin: function () { 208 209 sendToParent({ v: PROTOCOL_VERSION, type: 'blento:promptLogin' }); 210 + }, 211 + notify: function (name, payload) { 212 + if (typeof name !== 'string' || !name) { 213 + throw new BlentoError('invalid_request', 'notify(name): name must be a non-empty string'); 214 + } 215 + sendToParent({ v: PROTOCOL_VERSION, type: 'blento:notify', name: name, payload: payload }); 209 216 } 210 217 }; 211 218
+7
static/embed/v0/test.html
··· 131 131 <button id="btn-resize">notifyResize(800)</button> 132 132 <button id="btn-navigate">notifyNavigate('/')</button> 133 133 <button id="btn-prompt-login">promptLogin()</button> 134 + <button id="btn-notify">notify('test-event', {ts})</button> 134 135 </div> 135 136 136 137 <div class="field"> ··· 253 254 $('btn-prompt-login').onclick = () => { 254 255 window.Blento.promptLogin(); 255 256 show('promptLogin() sent', null); 257 + }; 258 + 259 + $('btn-notify').onclick = () => { 260 + const payload = { ts: Date.now() }; 261 + window.Blento.notify('test-event', payload); 262 + show('notify("test-event") sent', payload); 256 263 }; 257 264 258 265 if (window.Blento.getTheme().dark) {