@toon-protocol/client 0.13.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.
- package/dist/chunk-QEMD5EAI.js +441 -0
- package/dist/chunk-QEMD5EAI.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -1
- package/dist/render/index.d.ts +807 -0
- package/dist/render/index.js +55 -0
- package/dist/render/index.js.map +1 -0
- package/package.json +13 -2
|
@@ -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 };
|