@toon-protocol/client 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,807 @@
1
+ import { NostrEvent, EventTemplate } from 'nostr-tools/pure';
2
+ import { UiCoordinate, UI_RENDERER_KIND } from '@toon-protocol/core';
3
+ export { UI_RENDERER_KIND, UI_TAG, UiCoordinate, buildUiCoordinate, getUiCoordinate, parseUiCoordinate, selectLatestAddressable } from '@toon-protocol/core';
4
+
5
+ /**
6
+ * Branch 1 — the native-component registry.
7
+ *
8
+ * A `kind → native component` map for the kinds the client knows natively. This
9
+ * is the registry abstraction that {@link renderDispatch} consults first: a hit
10
+ * is branch 1 (full trust), a miss falls through to the unknown-kind branches.
11
+ *
12
+ * The component type `C` is generic so the rendering package
13
+ * (`@toon-protocol/views`) instantiates it with its own component contract (e.g.
14
+ * an `Atom`) — this keeps `@toon-protocol/client` free of any React dependency
15
+ * while still owning the dispatch + registry abstraction (per the epic split:
16
+ * dispatch in `client`, branch-1 components in `views`).
17
+ *
18
+ * Replaces ad-hoc per-kind conditionals with a single register/lookup seam.
19
+ */
20
+ /** A native component registered for one or more event kinds. */
21
+ declare class KindRegistry<C> {
22
+ private readonly byKind;
23
+ /**
24
+ * Register a native `component` as the renderer for one or more event
25
+ * `kinds`. Registering an already-registered kind overwrites it (last write
26
+ * wins) so a host can override a default; pass {@link register} per kind to
27
+ * keep the first registration explicit.
28
+ */
29
+ register(kinds: number | readonly number[], component: C): this;
30
+ /** The native component for `kind`, or `undefined` if the kind is unknown. */
31
+ lookup(kind: number): C | undefined;
32
+ /** Whether a native component is registered for `kind` (branch 1 applies). */
33
+ has(kind: number): boolean;
34
+ /** Every kind with a registered native component. */
35
+ kinds(): number[];
36
+ /** Number of registered kinds. */
37
+ get size(): number;
38
+ }
39
+
40
+ /**
41
+ * Render-dispatch types for the NIP-on-TOON render trust gradient.
42
+ *
43
+ * The client forks on one question — *do I know this kind?* — and the answer
44
+ * selects both a render strategy and a trust level. Trust runs *opposite* to
45
+ * flexibility: the more open-ended the render path, the less it is trusted.
46
+ *
47
+ * | Branch | Condition | Strategy | Trust |
48
+ * |--------|--------------------------|---------------------------|--------|
49
+ * | 1 | known kind | native component | full |
50
+ * | 2 | unknown + A2UI spec | client A2UI catalog | medium |
51
+ * | 3 | unknown + raw widget | sandboxed mcp-ui iframe | low |
52
+ * | 4 | unknown + nothing | generative fallback | low |
53
+ *
54
+ * This module is render-framework-agnostic: it carries the *decision*, not the
55
+ * React tree. The `views` package binds branch 1's resolved native component to
56
+ * an actual component, and the sibling tickets (#89/#90/#92) fill in branches
57
+ * 2/3/4. See `skills/nip-on-toon-discovery/SKILL.md` in toon-meta for the spec.
58
+ */
59
+
60
+ /**
61
+ * The four render branches of the trust gradient. The string values are stable
62
+ * and safe to persist / log.
63
+ */
64
+ type RenderBranch = 'native' | 'a2ui' | 'mcp-ui' | 'generative';
65
+ /** Trust tier associated with a branch. Full > medium > low. */
66
+ type RenderTrust = 'full' | 'medium' | 'low';
67
+ /**
68
+ * Branch 1 — a known kind renders with a fully-trusted native component from the
69
+ * client's own registry. The component type `C` is left generic so the rendering
70
+ * package (`@toon-protocol/views`) can specialise it with its own component
71
+ * contract (e.g. an `Atom`) without this package depending on React.
72
+ */
73
+ interface NativeDecision<C> {
74
+ branch: 'native';
75
+ trust: 'full';
76
+ /** The event to render. */
77
+ event: NostrEvent;
78
+ /** The resolved native component for this kind, from the {@link KindRegistry}. */
79
+ component: C;
80
+ }
81
+ /**
82
+ * Branch 2 — unknown kind, an `application/a2ui+json` renderer is available. The
83
+ * renderer's `surfaceUpdate` is the template; `core.decodeEventFromToon(event)`
84
+ * is fed in as the `dataModelUpdate` (medium trust, standard catalog only).
85
+ *
86
+ * STUB for #88: the dispatch routes here; the A2UI renderer is implemented in
87
+ * toon-protocol/toon-client#89.
88
+ */
89
+ interface A2uiDecision {
90
+ branch: 'a2ui';
91
+ trust: 'medium';
92
+ /** The event to render. */
93
+ event: NostrEvent;
94
+ /** The resolved `kind:31036` renderer event carrying the A2UI `surfaceUpdate`. */
95
+ renderer: NostrEvent;
96
+ }
97
+ /**
98
+ * Branch 3 — unknown kind, a `text/html;profile=mcp-app` raw widget renderer is
99
+ * available. Rendered inside a sandboxed mcp-ui iframe at low trust; the consent
100
+ * invariant (authorization surface drawn by the client outside the iframe,
101
+ * non-themeable) is enforced by the host.
102
+ *
103
+ * STUB for #88: the dispatch routes here; the sandboxed AppRenderer + consent
104
+ * invariant are implemented in toon-protocol/toon-client#90.
105
+ */
106
+ interface McpUiDecision {
107
+ branch: 'mcp-ui';
108
+ trust: 'low';
109
+ /** The event to render. */
110
+ event: NostrEvent;
111
+ /** The resolved `kind:31036` renderer event carrying the raw widget. */
112
+ renderer: NostrEvent;
113
+ }
114
+ /**
115
+ * Branch 4 — unknown kind, no renderer available. The client falls back to a
116
+ * generative rendering at low trust (optionally publishing back a `kind:31036`).
117
+ *
118
+ * STUB for #88: the dispatch routes here; the generative fallback is implemented
119
+ * in toon-protocol/toon-client#92.
120
+ */
121
+ interface GenerativeDecision {
122
+ branch: 'generative';
123
+ trust: 'low';
124
+ /** The event to render. */
125
+ event: NostrEvent;
126
+ }
127
+ /**
128
+ * The outcome of {@link renderDispatch}: a discriminated union over
129
+ * {@link RenderBranch}. Consumers switch on `.branch` to pick a renderer.
130
+ */
131
+ type RenderDecision<C> = NativeDecision<C> | A2uiDecision | McpUiDecision | GenerativeDecision;
132
+
133
+ /**
134
+ * Renderer-swap defense — the security guard layer around render dispatch
135
+ * (toon-protocol/toon-client#91, part of toon-protocol/toon-meta#58).
136
+ *
137
+ * ── THREAT: "renderer swap" ───────────────────────────────────────────────────
138
+ * A `kind:31036` renderer is an *addressable* event: the coordinate
139
+ * `31036:<author-pubkey>:<targetKind>` can later resolve to a *different* event
140
+ * (different `id`, different content) by publishing a newer-`created_at`
141
+ * revision. Because resolving the `ui` tag yields a renderer that selects the
142
+ * render strategy *and* the trust tier, an attacker who gets a malicious 31036
143
+ * selected can attack the user:
144
+ *
145
+ * V1. Cross-author substitution — serve a 31036 authored by someone *other*
146
+ * than the authoritative renderer author, hoping the client renders it.
147
+ * V2. Forged / tampered renderer — serve a 31036 whose signature does not
148
+ * verify (mutated tags/content, or no signature at all).
149
+ * V3. Resolution race / nondeterminism — feed candidate revisions in an order
150
+ * that makes some clients pick the attacker's revision.
151
+ * V4. Silent mid-session swap — after a renderer has been pinned for an event,
152
+ * publish a newer revision (new `id`) to swap the active renderer out from
153
+ * under an already-decided render, especially to *downgrade trust* (e.g.
154
+ * push a benign event into a hostile low-trust widget).
155
+ *
156
+ * Per toon#36 decisions: the renderer-author pubkey is the **event author** (the
157
+ * `pubkey` of the event being rendered), and clients MUST re-verify the 31036
158
+ * signature before it can select a render strategy.
159
+ *
160
+ * ── DEFENSE (this module) ─────────────────────────────────────────────────────
161
+ * {@link verifyRendererTrust} is a guard placed *between* renderer resolution and
162
+ * {@link renderDispatch}. It **fails closed**: on any violation it returns a
163
+ * rejection and the caller drops to the safe branch (native for known kinds,
164
+ * generative for unknown kinds) — it never renders the suspect renderer.
165
+ *
166
+ * - **Author binding (closes V1):** the resolved 31036's `pubkey` MUST equal
167
+ * the authoritative renderer author (the rendered event's `pubkey`). The
168
+ * coordinate's `pubkey` segment is also checked, so a coordinate pointing at
169
+ * a third-party author is refused before any fetch is trusted.
170
+ * - **Signature verification (closes V2):** the 31036 event signature is
171
+ * re-verified with {@link verifyEvent}; a tampered/unsigned renderer is
172
+ * refused.
173
+ * - **Deterministic selection (closes V3):** candidates are collapsed with
174
+ * {@link selectLatestAddressable} (latest `created_at`, lowest-`id`
175
+ * tiebreak), so selection is not attacker-race-controllable.
176
+ * - **Anti-swap pinning + downgrade detection (closes V4):** the chosen
177
+ * renderer `id` and its trust tier are pinned per coordinate in a
178
+ * {@link RendererPinStore}. A later revision with a *different* `id` is a
179
+ * detected swap; if it would *lower* the trust tier the swap is refused
180
+ * (fail closed). High-trust kinds use the issue's stricter rule: any `id`
181
+ * change at all → refuse and fall back to the native component.
182
+ *
183
+ * ── RELATION TO {@link import('./resolveRenderer.js').resolveUiRenderer} ───────
184
+ * `resolveRenderer.ts` (#97) is the plain, stateless `ui`→`kind:31036` resolver:
185
+ * author-bound coordinate, latest-addressable selection, signature re-verify,
186
+ * returning the renderer or `undefined`. This guard shares the SAME core
187
+ * primitives it builds on — `getUiCoordinate` / `selectLatestAddressable` (now
188
+ * from `@toon-protocol/core`) and `verifyEvent` — so the two agree bit-for-bit
189
+ * on which revision a coordinate selects and on signature acceptance. The guard
190
+ * adds what the plain resolver deliberately omits: a stateful anti-swap pin
191
+ * store, trust-downgrade / high-trust id-change detection, and *granular*
192
+ * fail-closed {@link SwapRejectionReason}s (the resolver collapses every failure
193
+ * to `undefined`). It is therefore a strict superset, not a parallel copy.
194
+ */
195
+
196
+ /** Whether trust tier `next` is strictly lower than `prev` (a downgrade). */
197
+ declare function isTrustDowngrade(prev: RenderTrust, next: RenderTrust): boolean;
198
+ /** Why a renderer was refused. Stable string values, safe to log. */
199
+ type SwapRejectionReason =
200
+ /** No `ui` coordinate on the event, or it was malformed. */
201
+ 'no-coordinate'
202
+ /** The coordinate's author segment is not the rendered event's author. */
203
+ | 'coordinate-author-mismatch'
204
+ /** No candidate `kind:31036` renderer resolved for the coordinate. */
205
+ | 'no-renderer'
206
+ /** The resolved event is not a `kind:31036` renderer. */
207
+ | 'not-a-renderer'
208
+ /** The resolved renderer's `pubkey` is not the authoritative author. */
209
+ | 'author-mismatch'
210
+ /** The renderer's `d` tag does not match the coordinate's target kind. */
211
+ | 'target-kind-mismatch'
212
+ /** The renderer signature did not verify (tampered / unsigned). */
213
+ | 'bad-signature'
214
+ /** A high-trust kind's pinned renderer `id` changed (issue rule: refuse). */
215
+ | 'high-trust-id-changed'
216
+ /** The swap would lower the trust tier from the pinned tier. */
217
+ | 'trust-downgrade';
218
+ /** A renderer refused by the guard. The caller must fall back, not render. */
219
+ interface SwapRejection {
220
+ ok: false;
221
+ reason: SwapRejectionReason;
222
+ /** Human-readable detail for logging. */
223
+ detail: string;
224
+ /** The coordinate involved, when one was resolvable. */
225
+ coordinate?: UiCoordinate;
226
+ }
227
+ /** A renderer the guard approved for dispatch. */
228
+ interface SwapApproval {
229
+ ok: true;
230
+ /** The verified, author-bound, deterministically-selected renderer. */
231
+ renderer: NostrEvent;
232
+ /** The coordinate the renderer was selected for. */
233
+ coordinate: UiCoordinate;
234
+ /** Whether this approval newly pinned the coordinate (first sighting). */
235
+ pinned: boolean;
236
+ /** Set when an `id` swap was observed but allowed (non-downgrading). */
237
+ swapObserved?: boolean;
238
+ }
239
+ type SwapDecision = SwapApproval | SwapRejection;
240
+ /**
241
+ * A pinned renderer decision for one coordinate: the `id` we committed to and
242
+ * the trust tier it implied. Used to detect swaps and downgrades.
243
+ */
244
+ interface RendererPin {
245
+ /** The pinned `kind:31036` event id. */
246
+ id: string;
247
+ /** The trust tier the pinned renderer selected. */
248
+ trust: RenderTrust;
249
+ }
250
+ /**
251
+ * Pins the chosen renderer per coordinate so a later replaceable `kind:31036`
252
+ * cannot silently swap the active renderer mid-session. Keyed by the canonical
253
+ * coordinate string `31036:<pubkey>:<targetKind>`.
254
+ *
255
+ * In-memory by default; a host may seed pins from config (the issue's
256
+ * "allowlist high-trust renderers by event id" — pre-populate the expected `id`
257
+ * for a known kind) via {@link pin}.
258
+ */
259
+ declare class RendererPinStore {
260
+ private readonly byCoord;
261
+ private static key;
262
+ /** The pin for `coord`, or `undefined` if not yet pinned. */
263
+ get(coord: UiCoordinate): RendererPin | undefined;
264
+ /** Pin (or overwrite) the renderer decision for `coord`. */
265
+ pin(coord: UiCoordinate, decision: RendererPin): this;
266
+ /** Whether `coord` is pinned. */
267
+ has(coord: UiCoordinate): boolean;
268
+ /** Number of pinned coordinates. */
269
+ get size(): number;
270
+ }
271
+ /** Input to {@link verifyRendererTrust}. */
272
+ interface VerifyRendererInput<C> {
273
+ /** The event whose renderer is being resolved. Its `pubkey` is authoritative. */
274
+ event: NostrEvent;
275
+ /**
276
+ * The candidate `kind:31036` renderer(s) fetched for the event's `ui`
277
+ * coordinate. May contain multiple revisions; the guard picks the winner
278
+ * deterministically. The caller does not pre-select.
279
+ */
280
+ candidates: readonly NostrEvent[];
281
+ /** The branch-1 native registry; used to decide which kinds are "high trust". */
282
+ registry: KindRegistry<C>;
283
+ /** The pin store enforcing stable, anti-swap selection across resolutions. */
284
+ pins: RendererPinStore;
285
+ /**
286
+ * Signature verifier (defaults to nostr-tools `verifyEvent`). Injectable so
287
+ * tests can exercise the fail-closed path deterministically.
288
+ */
289
+ verify?: (event: NostrEvent) => boolean;
290
+ /**
291
+ * Treat the event's kind as a "high-trust" kind subject to the issue's strict
292
+ * id-allowlist rule (any `id` change → refuse, fall back to native). Defaults
293
+ * to "the registry has a native component for this kind", matching the spec:
294
+ * branch-1 known kinds are the high-trust set. A host may override.
295
+ */
296
+ isHighTrustKind?: (kind: number) => boolean;
297
+ }
298
+ /**
299
+ * Guard a renderer before it can select a render strategy. Runs author binding,
300
+ * signature verification, deterministic selection, and anti-swap / downgrade
301
+ * detection. **Fails closed**: any violation returns a {@link SwapRejection} and
302
+ * the caller must drop to the safe branch rather than render.
303
+ *
304
+ * On approval, the chosen renderer is pinned for its coordinate so a subsequent
305
+ * resolution that yields a different `id` is detected (and, if it would downgrade
306
+ * trust, refused).
307
+ */
308
+ declare function verifyRendererTrust<C>(input: VerifyRendererInput<C>): SwapDecision;
309
+
310
+ /**
311
+ * Kind-keyed render dispatch — the skeleton the four render branches plug into.
312
+ *
313
+ * Implements §"Client dispatch algorithm" of the NIP-on-TOON render-side spec
314
+ * (`skills/nip-on-toon-discovery/SKILL.md` in toon-meta):
315
+ *
316
+ * 1. Is this kind known? → **branch 1** (native registry). Done.
317
+ * 2. Otherwise resolve the event's `ui` tag to a `kind:31036` renderer.
318
+ * 3. If a renderer is found, read its `m` (mimeType) tag:
319
+ * - `application/a2ui+json` → **branch 2** (A2UI, medium trust)
320
+ * - `text/html;profile=mcp-app` → **branch 3** (sandboxed mcp-ui, low)
321
+ * 4. If no renderer is found → **branch 4** (generative fallback, low trust).
322
+ *
323
+ * SCOPE (#88 — skeleton + branch 1 only): branch 1 is wired through the
324
+ * {@link KindRegistry}; branches 2/3/4 are returned as clearly-marked decisions
325
+ * for the sibling tickets to consume (#89 A2UI, #90 mcp-ui + consent, #92
326
+ * generative). This module does NOT render — it returns a {@link RenderDecision}.
327
+ *
328
+ * The `ui`-tag → `kind:31036` *resolution* lives outside this function — see
329
+ * {@link resolveUiRenderer} in `./resolveRenderer.js`, which parses the `ui`
330
+ * coordinate (via core's `getUiCoordinate` / `parseUiCoordinate`), picks the
331
+ * latest addressable `kind:31036` (`selectLatestAddressable`), and re-verifies
332
+ * its signature before trusting it. The dispatch takes the already-resolved
333
+ * renderer event via {@link DispatchInput.renderer}.
334
+ */
335
+
336
+ /**
337
+ * The `m` (mimeType) tag value of a resolved `kind:31036` renderer, or
338
+ * `undefined` if the event is not a renderer or carries no `m` tag.
339
+ *
340
+ * The `m` tag is the format selector that picks the branch + trust tier.
341
+ */
342
+ declare function resolveRendererMime(renderer: NostrEvent | undefined): string | undefined;
343
+ /** Input to {@link renderDispatch}. */
344
+ interface DispatchInput {
345
+ /** The decoded event the client wants to render. */
346
+ event: NostrEvent;
347
+ /**
348
+ * The `kind:31036` renderer resolved from the event's `ui` tag, if any.
349
+ *
350
+ * Resolution (parse the `ui` coordinate, fetch + pick the latest addressable
351
+ * `kind:31036`) is performed by the caller — see the toon#36 spike. Only
352
+ * consulted when the event's kind is unknown (branches 2–4).
353
+ */
354
+ renderer?: NostrEvent;
355
+ }
356
+ /**
357
+ * Route an event to one of the four render branches.
358
+ *
359
+ * @param input The event + (optionally) its resolved `kind:31036` renderer.
360
+ * @param registry The branch-1 native-component registry to consult first.
361
+ * @returns A {@link RenderDecision} naming the branch, trust tier, and payload.
362
+ */
363
+ declare function renderDispatch<C>(input: DispatchInput, registry: KindRegistry<C>): RenderDecision<C>;
364
+ /** Input to {@link guardedRenderDispatch}. */
365
+ interface GuardedDispatchInput {
366
+ /** The decoded event the client wants to render. */
367
+ event: NostrEvent;
368
+ /**
369
+ * The candidate `kind:31036` renderer(s) fetched for the event's `ui`
370
+ * coordinate, *unfiltered*. The swap-defense guard selects the winner
371
+ * deterministically and verifies it; the caller does NOT pre-select. Pass an
372
+ * empty array (or omit) when no renderer was resolved.
373
+ */
374
+ candidates?: readonly NostrEvent[];
375
+ }
376
+ /** Why {@link guardedRenderDispatch} fell back to a safe branch. */
377
+ interface DispatchGuardInfo {
378
+ /** A renderer was refused by the swap-defense guard. */
379
+ rejected: SwapRejection;
380
+ }
381
+ /**
382
+ * Dispatch with the **renderer-swap defense** (toon-client#91) interposed.
383
+ *
384
+ * This is the secure entry point: it runs {@link verifyRendererTrust} over the
385
+ * raw candidate renderers *before* {@link renderDispatch} can pick a strategy,
386
+ * and **fails closed** on any violation (wrong-author, bad signature,
387
+ * trust-downgrading swap, high-trust id change):
388
+ *
389
+ * - Known kind → branch 1 (native) regardless of renderers. The guard still
390
+ * runs so a *high-trust* renderer swap is detected, but a known kind always
391
+ * has the native component to fall back to, so the result is branch 1.
392
+ * - Unknown kind, renderer **approved** → normal {@link renderDispatch} with the
393
+ * single verified renderer (branches 2/3).
394
+ * - Unknown kind, renderer **refused** or none → branch 4 (generative). We do
395
+ * NOT pass the suspect renderer through; the user gets the safe fallback.
396
+ *
397
+ * @returns the {@link RenderDecision} plus, when a renderer was refused, the
398
+ * {@link DispatchGuardInfo} describing why (for logging / UX "renderer refused").
399
+ */
400
+ declare function guardedRenderDispatch<C>(input: GuardedDispatchInput, registry: KindRegistry<C>, pins: RendererPinStore): {
401
+ decision: RenderDecision<C>;
402
+ guard?: DispatchGuardInfo;
403
+ };
404
+
405
+ /**
406
+ * The consent invariant — the load-bearing security property of branch 3
407
+ * (sandboxed mcp-ui, low trust) of the NIP-on-TOON render trust gradient
408
+ * (toon-meta#58, toon-client#90; spec §"Branch 3 — sandboxed mcp-ui & the
409
+ * consent invariant").
410
+ *
411
+ * ── The invariant (verbatim from the spec) ──────────────────────────────────
412
+ * A sandboxed widget may only *request* an action. The authorization surface is
413
+ * rendered by the trusted client outside the iframe and is non-themeable. The
414
+ * sandboxed widget can never draw, style, or spoof the consent/authorization UI.
415
+ * A widget that can paint the authorization UI collapses the entire trust
416
+ * gradient to its lowest tier.
417
+ * ────────────────────────────────────────────────────────────────────────────
418
+ *
419
+ * This module is the framework-agnostic half of branch 3. It carries the
420
+ * *decision* and the *policy*, not any React tree — mirroring how `@toon-protocol/client`'s
421
+ * {@link ./dispatch} carries the render decision and `@toon-protocol/views`
422
+ * carries the rendered component. The React side (the sandboxed `AppRenderer`
423
+ * iframe + the host-rendered, non-themeable `ConsentPrompt`) lives in
424
+ * `@toon-protocol/views`; it consumes the types and functions defined here.
425
+ *
426
+ * Why the policy lives here, away from React:
427
+ * - The classification of "is this a state-changing action that needs explicit
428
+ * authorization?" is a pure, auditable function with no DOM dependency.
429
+ * - The widget supplies ZERO inputs to this function that can influence the
430
+ * *appearance* of the prompt: it supplies only the requested tool name and
431
+ * arguments. Everything the prompt renders is derived by the trusted client.
432
+ */
433
+
434
+ /**
435
+ * The widget payload handed to the sandboxed iframe. The `m`-tagged
436
+ * `text/html;profile=mcp-app` renderer ships a raw HTML widget as a UIResource;
437
+ * we pass the HTML through to the iframe untouched, but everything the host
438
+ * renders around it is client-controlled.
439
+ *
440
+ * Deliberately minimal: the host needs only the HTML to feed the iframe and the
441
+ * mimeType to assert the branch. No widget-supplied styling, theme, chrome, or
442
+ * "trusted" hints are carried — by construction the widget cannot pass any.
443
+ */
444
+ interface UiResource {
445
+ /** The raw widget HTML to render inside the sandboxed iframe. */
446
+ html: string;
447
+ /** Always `text/html;profile=mcp-app` for branch 3 (asserted on extract). */
448
+ mimeType: string;
449
+ /** The `ui://…` resource URI, if the renderer declared one (host metadata only). */
450
+ uri?: string;
451
+ }
452
+ /**
453
+ * Extract the branch-3 {@link UiResource} from a resolved `kind:31036` renderer
454
+ * event whose `m` tag is `text/html;profile=mcp-app`.
455
+ *
456
+ * The renderer's `content` is either the raw widget HTML, or a JSON-encoded
457
+ * MCP `UIResource` embedded-resource block (`{ type: 'resource', resource: {
458
+ * uri, mimeType, text } }`) as produced by mcp-ui servers. Both are accepted;
459
+ * the HTML is returned verbatim for iframe passthrough.
460
+ *
461
+ * Returns `undefined` (never throws) when the event is not a usable branch-3
462
+ * renderer, so the caller can fall through to branch 4 rather than render
463
+ * something unexpected.
464
+ */
465
+ declare function extractUiResource(renderer: NostrEvent | undefined): UiResource | undefined;
466
+ /**
467
+ * An action a sandboxed widget *requested* (never performed). This is the only
468
+ * thing that crosses the iframe → host boundary, and it carries no presentation
469
+ * data — only the tool name and arguments the widget wants to invoke.
470
+ */
471
+ interface WidgetIntent {
472
+ /** The tool/action name the widget asked the host to invoke. */
473
+ toolName: string;
474
+ /** The arguments the widget supplied for that tool. */
475
+ arguments: Record<string, unknown>;
476
+ }
477
+ /**
478
+ * The classification of a {@link WidgetIntent}: does it need an explicit, host-
479
+ * rendered authorization decision before the host may act on it?
480
+ *
481
+ * - `requires-consent` — a state-changing / spendy / outbound action. The host
482
+ * MUST render the {@link ConsentRequest} prompt (outside the iframe,
483
+ * non-themeable) and only proceed on an explicit user grant.
484
+ * - `auto` — a read-only / inert request the host may forward without a prompt.
485
+ *
486
+ * Default-deny: anything not provably inert is treated as `requires-consent`.
487
+ */
488
+ type IntentClassification = 'requires-consent' | 'auto';
489
+ /**
490
+ * Classify a widget intent. Pure and default-deny: only an exact match against
491
+ * the trusted read-only allowlist is auto-forwarded; everything else requires a
492
+ * host-rendered consent prompt.
493
+ */
494
+ declare function classifyIntent(intent: WidgetIntent): IntentClassification;
495
+ /**
496
+ * The data the trusted host needs to render an authorization prompt for a
497
+ * widget-requested action. EVERY field here is either a fixed, client-owned
498
+ * constant or a plain machine value copied out of the intent — there is NO
499
+ * styling, theme, color, label-override, HTML, or markup field the widget could
500
+ * supply. This is what makes the prompt non-themeable by construction: the type
501
+ * simply has nowhere to put presentation input.
502
+ */
503
+ interface ConsentRequest {
504
+ /** Stable id for correlating the prompt with its resolution. */
505
+ readonly id: string;
506
+ /** The tool the widget asked to invoke (rendered as plain text by the host). */
507
+ readonly toolName: string;
508
+ /** The arguments the widget supplied (rendered as inspectable data, not HTML). */
509
+ readonly arguments: Record<string, unknown>;
510
+ /**
511
+ * The trust tier of the requesting surface — always `'low'` for a branch-3
512
+ * sandboxed widget. Carried so the host can render the appropriate warning
513
+ * chrome; the widget cannot change it.
514
+ */
515
+ readonly trust: 'low';
516
+ }
517
+ /** The user's decision on a {@link ConsentRequest}. */
518
+ type ConsentDecision = 'grant' | 'deny';
519
+ /**
520
+ * Build a {@link ConsentRequest} from a widget intent. The host calls this when
521
+ * {@link classifyIntent} returns `requires-consent`. It copies ONLY the tool
522
+ * name and arguments out of the widget's request; it fixes `trust: 'low'` and
523
+ * generates the id itself. The widget contributes nothing to how the prompt
524
+ * looks.
525
+ */
526
+ declare function buildConsentRequest(intent: WidgetIntent): ConsentRequest;
527
+
528
+ /**
529
+ * `ui`-tag → `kind:31036` renderer resolution (toon#36).
530
+ *
531
+ * This is the resolution seam the {@link renderDispatch} skeleton (#88)
532
+ * deliberately left out: dispatch consumes an *already-resolved* renderer, and
533
+ * this module produces it. It is split out from dispatch so the relay query +
534
+ * cache (which is IO, and lives in the daemon / {@link import('../ToonClient.js')})
535
+ * stays separate from the pure selection logic — mirroring core's own
536
+ * "helpers are pure, resolution is client-local" split.
537
+ *
538
+ * Algorithm, per the toon#36 decisions:
539
+ *
540
+ * 1. Read the rendered event's `ui` tag and parse it into a target coordinate.
541
+ * The coordinate convention is `31036:<renderer-author-pubkey>:<targetKind>`.
542
+ * Per toon#36 the **renderer-author pubkey is the EVENT AUTHOR**, so the
543
+ * `ui` tag MAY carry just the bare target kind (e.g. `42`); the author is
544
+ * taken from `event.pubkey`. A full `31036:<pubkey>:<kind>` coordinate is
545
+ * also accepted (via core's `getUiCoordinate`), but its pubkey MUST equal
546
+ * the event author — a coordinate naming a different author is rejected, so
547
+ * an event cannot point at a third party's renderer.
548
+ * 2. Filter the caller-supplied `kind:31036` candidates to that coordinate
549
+ * (author === event author, `d` tag === target kind) and pick the latest
550
+ * addressable one (NIP-33 latest-wins) via `selectLatestAddressable`.
551
+ * 3. **Re-verify the signature** of the chosen renderer with `verifyEvent`
552
+ * before trusting it. A renderer that fails verification is dropped (the
553
+ * resolution returns `undefined`) — the client never feeds an unverified
554
+ * renderer to the dispatch.
555
+ *
556
+ * The relay query that produces `candidates` is the caller's responsibility:
557
+ * query `kind:31036`, `authors: [event.pubkey]`, `#d: [String(targetKind)]`.
558
+ */
559
+
560
+ /**
561
+ * The renderer coordinate a rendered event points at: a `kind:31036` event
562
+ * authored by the rendered event's author, targeting `targetKind`.
563
+ */
564
+ interface ResolvedCoordinate {
565
+ /** Always {@link UI_RENDERER_KIND} (31036). */
566
+ kind: typeof UI_RENDERER_KIND;
567
+ /** The renderer author pubkey — per toon#36, the EVENT AUTHOR's pubkey. */
568
+ pubkey: string;
569
+ /** The kind of event the renderer targets (the renderer's `d` value). */
570
+ targetKind: number;
571
+ }
572
+ /**
573
+ * Compute the renderer coordinate a rendered event points at, anchoring the
574
+ * renderer-author pubkey to the **event author** per toon#36.
575
+ *
576
+ * Accepts two `ui` tag shapes:
577
+ * - a bare target kind, e.g. `["ui", "42"]` → author = `event.pubkey`;
578
+ * - a full coordinate, e.g. `["ui", "31036:<pubkey>:42"]` → accepted only if
579
+ * `<pubkey>` equals `event.pubkey` (else `null`).
580
+ *
581
+ * Pure: no IO.
582
+ *
583
+ * @param event - The rendered event that may carry a `ui` tag.
584
+ * @returns The resolved coordinate, or `null` if there is no usable `ui` tag.
585
+ */
586
+ declare function resolveUiCoordinate(event: NostrEvent): ResolvedCoordinate | null;
587
+ /**
588
+ * Resolve a rendered event's `ui` tag to a verified `kind:31036` renderer.
589
+ *
590
+ * Filters `candidates` to the coordinate computed by {@link resolveUiCoordinate},
591
+ * picks the latest addressable match, and **re-verifies its signature** before
592
+ * returning it. The result feeds {@link renderDispatch} as `DispatchInput.renderer`.
593
+ *
594
+ * @param event - The rendered event carrying the `ui` tag.
595
+ * @param candidates - `kind:31036` events the caller fetched for this coordinate
596
+ * (the relay query is the caller's responsibility).
597
+ * @returns The latest verified renderer, or `undefined` if none resolves /
598
+ * verifies.
599
+ */
600
+ declare function resolveUiRenderer(event: NostrEvent, candidates: readonly NostrEvent[]): NostrEvent | undefined;
601
+
602
+ /**
603
+ * Render-side protocol constants for NIP-on-TOON.
604
+ *
605
+ * The canonical homes for the renderer kind, the `ui` tag, and the
606
+ * `UiCoordinate` helpers are `@toon-protocol/core` (published in `1.6.0`):
607
+ * {@link UI_RENDERER_KIND} (31036), {@link UI_TAG}, and `parseUiCoordinate` /
608
+ * `buildUiCoordinate` / `getUiCoordinate` / `selectLatestAddressable`. They are
609
+ * re-exported here so the render module has a single import surface; only the
610
+ * mime-type selectors below are owned locally (core does not export them).
611
+ */
612
+
613
+ /**
614
+ * The `m` (mimeType) tag value selecting **branch 2** — A2UI, medium trust.
615
+ *
616
+ * Owned locally: core does not export the render-branch mime selectors.
617
+ */
618
+ declare const MIME_A2UI = "application/a2ui+json";
619
+ /**
620
+ * The `m` (mimeType) tag value selecting **branch 3** — sandboxed mcp-ui iframe,
621
+ * low trust.
622
+ *
623
+ * Owned locally: core does not export the render-branch mime selectors.
624
+ */
625
+ declare const MIME_MCP_APP = "text/html;profile=mcp-app";
626
+
627
+ /**
628
+ * Branch 4 — generative fallback + optional `kind:31036` publish-back.
629
+ *
630
+ * Implements §"branch 4 — generative fallback" of the NIP-on-TOON render-side
631
+ * spec (`skills/nip-on-toon-discovery/SKILL.md` in toon-meta) and
632
+ * toon-protocol/toon-client#92.
633
+ *
634
+ * Branch 4 is reached by {@link renderDispatch} when a kind is **unknown** *and*
635
+ * no resolvable `kind:31036` renderer exists (no `ui` tag, or nothing resolves,
636
+ * or the resolved renderer carries no recognised `m` tag). With nothing else to
637
+ * go on, the client generates a best-effort rendering for the unknown event's
638
+ * shape at **low trust**.
639
+ *
640
+ * Design seams (per the issue's `needs:human` open questions — the model, the
641
+ * curation policy, and the publish-back opt-in semantics are product decisions
642
+ * the host owns, so this module hardcodes none of them):
643
+ *
644
+ * - **Generator** ({@link RendererGenerator}) — the actual model call is
645
+ * abstracted behind an interface the host injects. The host wires its own
646
+ * provider/keys/prompt; this module never imports an LLM SDK. A deterministic
647
+ * non-LLM generator ({@link deterministicGenerator}) is provided as the
648
+ * default and for tests, so branch 4 always produces *something* renderable
649
+ * even with no model configured.
650
+ * - **Publish-back** — optionally republish the generated renderer as a
651
+ * `kind:31036` addressable event so the next client has a "known" renderer
652
+ * for that kind (branch 4 slowly feeds branch 1). This is a **guarded,
653
+ * off-by-default** capability: it only fires when the host explicitly passes
654
+ * `publish: { enabled: true, ... }`. The published renderer is clearly
655
+ * low-trust / curation-pending; the namespacing & curation policy for
656
+ * community-published renderers is an open question in the epic (toon#58) and
657
+ * is intentionally *not* built here.
658
+ */
659
+
660
+ /**
661
+ * A generated renderer for an unknown event kind: an HTML `UIResource`-style
662
+ * document plus the `m` (mimeType) tag that classifies it.
663
+ *
664
+ * The HTML is the raw widget body; if published back as a `kind:31036` event it
665
+ * is rendered through branch 3 (sandboxed mcp-ui iframe) by a downstream client,
666
+ * which is why {@link mimeType} defaults to the branch-3 selector.
667
+ */
668
+ interface GeneratedRenderer {
669
+ /** The generated HTML document (the `UIResource` body). */
670
+ html: string;
671
+ /**
672
+ * The `m` (mimeType) tag for the generated renderer. Defaults to
673
+ * {@link MIME_MCP_APP} (`text/html;profile=mcp-app`) so a published renderer
674
+ * routes through branch 3 (sandboxed, low trust) on the next client.
675
+ */
676
+ mimeType: string;
677
+ /**
678
+ * Whether this rendering came from a model (`'model'`) or the built-in
679
+ * deterministic fallback (`'deterministic'`). Surfaced so the host can label
680
+ * trust/provenance in the UI.
681
+ */
682
+ source: 'model' | 'deterministic';
683
+ }
684
+ /** Context handed to a {@link RendererGenerator}. */
685
+ interface GenerateContext {
686
+ /** The unknown event the client wants to render. */
687
+ event: NostrEvent;
688
+ }
689
+ /**
690
+ * The pluggable generator seam. A host injects its own implementation (wired to
691
+ * whatever model endpoint, key, and prompt it has chosen — all `needs:human`
692
+ * product decisions this module deliberately does not own).
693
+ *
694
+ * Implementations should be best-effort and resilient: a failed model call
695
+ * should reject so {@link GenerativeFallbackRenderer} can fall back to the
696
+ * deterministic generator rather than render nothing.
697
+ */
698
+ interface RendererGenerator {
699
+ generate(ctx: GenerateContext): Promise<GeneratedRenderer>;
700
+ }
701
+ /**
702
+ * A deterministic, non-LLM generator: renders a best-effort, dependency-free
703
+ * "unknown kind" card from the event's shape (kind, author, tags, content). It
704
+ * never calls a network or a model, so it is the safe default and the basis for
705
+ * tests — given the same event it always produces the same HTML.
706
+ *
707
+ * The output is intentionally generic and clearly marked as a low-trust
708
+ * fallback; it makes no claim to understand the kind's semantics.
709
+ */
710
+ declare const deterministicGenerator: RendererGenerator;
711
+ /**
712
+ * The pure HTML projection used by {@link deterministicGenerator}. Exported so
713
+ * tests (and hosts wanting the fallback body without the Promise wrapper) can
714
+ * assert on it directly.
715
+ */
716
+ declare function renderDeterministicHtml(event: NostrEvent): string;
717
+ /** Host-supplied signer seam used to finalize the publish-back event. */
718
+ interface RendererSigner {
719
+ /** The author pubkey (hex) the renderer is published under (the coordinate author). */
720
+ getPublicKey(): string;
721
+ /** Finalize an unsigned event template into a signed `NostrEvent`. */
722
+ signEvent(template: EventTemplate): NostrEvent;
723
+ }
724
+ /** Host-supplied publisher seam used to broadcast the publish-back event. */
725
+ interface RendererPublisher {
726
+ publishEvent(event: NostrEvent): Promise<unknown>;
727
+ }
728
+ /**
729
+ * Publish-back configuration. **Off by default**: publish-back never happens
730
+ * unless the host passes this object with `enabled: true` *and* supplies a
731
+ * signer and publisher. This is the explicit-enablement gate the issue requires
732
+ * — there is no implicit / always-on publish path.
733
+ */
734
+ interface PublishBackOptions {
735
+ /** Master switch. Must be `true` for any publish to occur. */
736
+ enabled: boolean;
737
+ /** Signs the `kind:31036` event (also supplies the coordinate author pubkey). */
738
+ signer: RendererSigner;
739
+ /** Broadcasts the signed event. */
740
+ publisher: RendererPublisher;
741
+ }
742
+ /** Options for {@link GenerativeFallbackRenderer}. */
743
+ interface GenerativeFallbackOptions {
744
+ /**
745
+ * The generator to use. Defaults to {@link deterministicGenerator}. A host
746
+ * injects its model-backed generator here.
747
+ */
748
+ generator?: RendererGenerator;
749
+ /**
750
+ * Publish-back config. Omit (or pass `enabled: false`) to keep publish-back
751
+ * off — the default. See {@link PublishBackOptions}.
752
+ */
753
+ publish?: PublishBackOptions;
754
+ }
755
+ /** The outcome of {@link GenerativeFallbackRenderer.render}. */
756
+ interface GenerativeFallbackResult {
757
+ /** The generated renderer (model output, or the deterministic fallback). */
758
+ rendered: GeneratedRenderer;
759
+ /** Always `'low'` — branch 4 is a low-trust path. */
760
+ trust: 'low';
761
+ /**
762
+ * The signed `kind:31036` event that was published back, or `undefined` when
763
+ * publish-back was disabled (the default) or could not run.
764
+ */
765
+ published?: NostrEvent;
766
+ }
767
+ /**
768
+ * Build the unsigned `kind:31036` renderer event for a generated renderer. The
769
+ * `d` tag is the target kind; the `m` tag is the renderer's mimeType; the body
770
+ * is the generated HTML. The signed event's coordinate is
771
+ * `31036:<author-pubkey>:<targetKind>` (see {@link buildUiCoordinate}).
772
+ *
773
+ * Exported for tests / hosts that want to inspect the event before signing.
774
+ */
775
+ declare function buildRendererEventTemplate(targetKind: number, rendered: GeneratedRenderer): EventTemplate;
776
+ /**
777
+ * Branch 4 renderer: generate a best-effort rendering for an unknown event and,
778
+ * if explicitly enabled, publish it back as a `kind:31036` renderer.
779
+ *
780
+ * Generation is resilient: if the injected model generator throws, the renderer
781
+ * transparently falls back to {@link deterministicGenerator} so a rendering is
782
+ * always produced.
783
+ */
784
+ declare class GenerativeFallbackRenderer {
785
+ private readonly generator;
786
+ private readonly publish?;
787
+ constructor(options?: GenerativeFallbackOptions);
788
+ /**
789
+ * Generate a fallback rendering for `event` (low trust), optionally publishing
790
+ * the result back as a `kind:31036` renderer when publish-back is enabled.
791
+ */
792
+ render(event: NostrEvent): Promise<GenerativeFallbackResult>;
793
+ /** Whether publish-back is currently enabled (for host introspection/UI). */
794
+ get publishBackEnabled(): boolean;
795
+ }
796
+ /**
797
+ * The coordinate (`31036:<author-pubkey>:<targetKind>`) a publish-back will/did
798
+ * use for `targetKind` under `signer`'s identity. Convenience for hosts that
799
+ * want to show or pre-resolve the coordinate.
800
+ *
801
+ * Throws if the signer's pubkey or `targetKind` is malformed — core's
802
+ * {@link buildUiCoordinate} returns `null` for invalid inputs, which here can
803
+ * only mean the host wired a bad signer.
804
+ */
805
+ declare function publishBackCoordinate(signer: RendererSigner, targetKind: number): string;
806
+
807
+ export { type A2uiDecision, type ConsentDecision, type ConsentRequest, type DispatchGuardInfo, type DispatchInput, type GenerateContext, type GeneratedRenderer, type GenerativeDecision, type GenerativeFallbackOptions, GenerativeFallbackRenderer, type GenerativeFallbackResult, type GuardedDispatchInput, type IntentClassification, KindRegistry, MIME_A2UI, MIME_MCP_APP, type McpUiDecision, type NativeDecision, type PublishBackOptions, type RenderBranch, type RenderDecision, type RenderTrust, type RendererGenerator, type RendererPin, RendererPinStore, type RendererPublisher, type RendererSigner, type ResolvedCoordinate, type SwapApproval, type SwapDecision, type SwapRejection, type SwapRejectionReason, type UiResource, type VerifyRendererInput, type WidgetIntent, buildConsentRequest, buildRendererEventTemplate, classifyIntent, deterministicGenerator, extractUiResource, guardedRenderDispatch, isTrustDowngrade, publishBackCoordinate, renderDeterministicHtml, renderDispatch, resolveRendererMime, resolveUiCoordinate, resolveUiRenderer, verifyRendererTrust };