@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.
- package/dist/chunk-QEMD5EAI.js +441 -0
- package/dist/chunk-QEMD5EAI.js.map +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +53 -5
- 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,441 @@
|
|
|
1
|
+
// src/render/constants.ts
|
|
2
|
+
import {
|
|
3
|
+
UI_RENDERER_KIND,
|
|
4
|
+
UI_TAG,
|
|
5
|
+
buildUiCoordinate,
|
|
6
|
+
parseUiCoordinate,
|
|
7
|
+
getUiCoordinate,
|
|
8
|
+
selectLatestAddressable
|
|
9
|
+
} from "@toon-protocol/core";
|
|
10
|
+
var MIME_A2UI = "application/a2ui+json";
|
|
11
|
+
var MIME_MCP_APP = "text/html;profile=mcp-app";
|
|
12
|
+
|
|
13
|
+
// src/render/swap-defense.ts
|
|
14
|
+
import { verifyEvent } from "nostr-tools/pure";
|
|
15
|
+
import {
|
|
16
|
+
UI_RENDERER_KIND as UI_RENDERER_KIND2,
|
|
17
|
+
getUiCoordinate as getUiCoordinate2,
|
|
18
|
+
selectLatestAddressable as selectLatestAddressable2
|
|
19
|
+
} from "@toon-protocol/core";
|
|
20
|
+
var TRUST_RANK = { low: 0, medium: 1, full: 2 };
|
|
21
|
+
function isTrustDowngrade(prev, next) {
|
|
22
|
+
return TRUST_RANK[next] < TRUST_RANK[prev];
|
|
23
|
+
}
|
|
24
|
+
var RendererPinStore = class _RendererPinStore {
|
|
25
|
+
byCoord = /* @__PURE__ */ new Map();
|
|
26
|
+
static key(coord) {
|
|
27
|
+
return `${coord.kind}:${coord.pubkey}:${coord.targetKind}`;
|
|
28
|
+
}
|
|
29
|
+
/** The pin for `coord`, or `undefined` if not yet pinned. */
|
|
30
|
+
get(coord) {
|
|
31
|
+
return this.byCoord.get(_RendererPinStore.key(coord));
|
|
32
|
+
}
|
|
33
|
+
/** Pin (or overwrite) the renderer decision for `coord`. */
|
|
34
|
+
pin(coord, decision) {
|
|
35
|
+
this.byCoord.set(_RendererPinStore.key(coord), { ...decision });
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
/** Whether `coord` is pinned. */
|
|
39
|
+
has(coord) {
|
|
40
|
+
return this.byCoord.has(_RendererPinStore.key(coord));
|
|
41
|
+
}
|
|
42
|
+
/** Number of pinned coordinates. */
|
|
43
|
+
get size() {
|
|
44
|
+
return this.byCoord.size;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function rendererTrust(renderer) {
|
|
48
|
+
const mime = renderer.tags.find((t) => t[0] === "m")?.[1];
|
|
49
|
+
if (mime === "application/a2ui+json") return "medium";
|
|
50
|
+
if (mime === "text/html;profile=mcp-app") return "low";
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
function rendererTargetKind(renderer) {
|
|
54
|
+
const d = renderer.tags.find((t) => t[0] === "d")?.[1];
|
|
55
|
+
if (d === void 0 || !/^\d+$/.test(d)) return void 0;
|
|
56
|
+
return Number(d);
|
|
57
|
+
}
|
|
58
|
+
function verifyRendererTrust(input) {
|
|
59
|
+
const { event, candidates, registry, pins } = input;
|
|
60
|
+
const verify = input.verify ?? verifyEvent;
|
|
61
|
+
const isHighTrustKind = input.isHighTrustKind ?? ((kind) => registry.has(kind));
|
|
62
|
+
const authorPubkey = event.pubkey;
|
|
63
|
+
const coordinate = getUiCoordinate2(event);
|
|
64
|
+
if (coordinate === null) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
reason: "no-coordinate",
|
|
68
|
+
detail: "event has no parseable ui tag"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (coordinate.pubkey !== authorPubkey) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
reason: "coordinate-author-mismatch",
|
|
75
|
+
detail: `ui coordinate author ${coordinate.pubkey} != event author ${authorPubkey}`,
|
|
76
|
+
coordinate
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const matching = candidates.filter(
|
|
80
|
+
(c) => c.kind === UI_RENDERER_KIND2 && c.pubkey === authorPubkey && rendererTargetKind(c) === coordinate.targetKind
|
|
81
|
+
);
|
|
82
|
+
const renderer = selectLatestAddressable2([...matching]);
|
|
83
|
+
if (renderer === void 0) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: "no-renderer",
|
|
87
|
+
detail: "no candidate matched the coordinate",
|
|
88
|
+
coordinate
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (renderer.kind !== UI_RENDERER_KIND2) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
reason: "not-a-renderer",
|
|
95
|
+
detail: `kind ${renderer.kind} != ${UI_RENDERER_KIND2}`,
|
|
96
|
+
coordinate
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (renderer.pubkey !== authorPubkey) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
reason: "author-mismatch",
|
|
103
|
+
detail: `renderer author ${renderer.pubkey} != event author ${authorPubkey}`,
|
|
104
|
+
coordinate
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (rendererTargetKind(renderer) !== coordinate.targetKind) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
reason: "target-kind-mismatch",
|
|
111
|
+
detail: `renderer d != coordinate target kind ${coordinate.targetKind}`,
|
|
112
|
+
coordinate
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
let signatureOk;
|
|
116
|
+
try {
|
|
117
|
+
signatureOk = verify(renderer);
|
|
118
|
+
} catch {
|
|
119
|
+
signatureOk = false;
|
|
120
|
+
}
|
|
121
|
+
if (!signatureOk) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: "bad-signature",
|
|
125
|
+
detail: `renderer ${renderer.id} signature did not verify`,
|
|
126
|
+
coordinate
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const trust = rendererTrust(renderer);
|
|
130
|
+
const effectiveTrust = trust ?? "low";
|
|
131
|
+
const existing = pins.get(coordinate);
|
|
132
|
+
const highTrust = isHighTrustKind(event.kind);
|
|
133
|
+
if (existing === void 0) {
|
|
134
|
+
pins.pin(coordinate, { id: renderer.id, trust: effectiveTrust });
|
|
135
|
+
return { ok: true, renderer, coordinate, pinned: true };
|
|
136
|
+
}
|
|
137
|
+
if (existing.id === renderer.id) {
|
|
138
|
+
return { ok: true, renderer, coordinate, pinned: false };
|
|
139
|
+
}
|
|
140
|
+
if (highTrust) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
reason: "high-trust-id-changed",
|
|
144
|
+
detail: `high-trust kind ${event.kind}: pinned renderer ${existing.id} swapped to ${renderer.id}`,
|
|
145
|
+
coordinate
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (isTrustDowngrade(existing.trust, effectiveTrust)) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
reason: "trust-downgrade",
|
|
152
|
+
detail: `swap would downgrade trust ${existing.trust} \u2192 ${effectiveTrust} for coordinate ${coordinate.pubkey}:${coordinate.targetKind}`,
|
|
153
|
+
coordinate
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
pins.pin(coordinate, { id: renderer.id, trust: effectiveTrust });
|
|
157
|
+
return { ok: true, renderer, coordinate, pinned: false, swapObserved: true };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/render/dispatch.ts
|
|
161
|
+
function tagValue(event, name) {
|
|
162
|
+
const tag = event.tags.find((t) => t[0] === name);
|
|
163
|
+
return tag?.[1];
|
|
164
|
+
}
|
|
165
|
+
function resolveRendererMime(renderer) {
|
|
166
|
+
if (!renderer || renderer.kind !== UI_RENDERER_KIND) return void 0;
|
|
167
|
+
return tagValue(renderer, "m");
|
|
168
|
+
}
|
|
169
|
+
function renderDispatch(input, registry) {
|
|
170
|
+
const { event, renderer } = input;
|
|
171
|
+
const component = registry.lookup(event.kind);
|
|
172
|
+
if (component !== void 0) {
|
|
173
|
+
return { branch: "native", trust: "full", event, component };
|
|
174
|
+
}
|
|
175
|
+
const mime = resolveRendererMime(renderer);
|
|
176
|
+
if (mime === MIME_A2UI && renderer) {
|
|
177
|
+
return { branch: "a2ui", trust: "medium", event, renderer };
|
|
178
|
+
}
|
|
179
|
+
if (mime === MIME_MCP_APP && renderer) {
|
|
180
|
+
return { branch: "mcp-ui", trust: "low", event, renderer };
|
|
181
|
+
}
|
|
182
|
+
return { branch: "generative", trust: "low", event };
|
|
183
|
+
}
|
|
184
|
+
function guardedRenderDispatch(input, registry, pins) {
|
|
185
|
+
const { event } = input;
|
|
186
|
+
const candidates = input.candidates ?? [];
|
|
187
|
+
if (registry.has(event.kind)) {
|
|
188
|
+
const guarded2 = verifyRendererTrust({ event, candidates, registry, pins });
|
|
189
|
+
const decision = renderDispatch({ event }, registry);
|
|
190
|
+
return guarded2.ok ? { decision } : { decision, guard: { rejected: guarded2 } };
|
|
191
|
+
}
|
|
192
|
+
if (candidates.length === 0) {
|
|
193
|
+
return { decision: renderDispatch({ event }, registry) };
|
|
194
|
+
}
|
|
195
|
+
const guarded = verifyRendererTrust({ event, candidates, registry, pins });
|
|
196
|
+
if (!guarded.ok) {
|
|
197
|
+
return {
|
|
198
|
+
decision: renderDispatch({ event }, registry),
|
|
199
|
+
guard: { rejected: guarded }
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
decision: renderDispatch({ event, renderer: guarded.renderer }, registry)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/render/consent.ts
|
|
208
|
+
function extractUiResource(renderer) {
|
|
209
|
+
if (!renderer || renderer.kind !== UI_RENDERER_KIND) return void 0;
|
|
210
|
+
const mime = renderer.tags.find((t) => t[0] === "m")?.[1];
|
|
211
|
+
if (mime !== MIME_MCP_APP) return void 0;
|
|
212
|
+
const raw = renderer.content;
|
|
213
|
+
if (typeof raw !== "string" || raw.length === 0) return void 0;
|
|
214
|
+
const trimmed = raw.trimStart();
|
|
215
|
+
if (trimmed.startsWith("{")) {
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(raw);
|
|
218
|
+
const res = parsed.resource;
|
|
219
|
+
if (parsed.type === "resource" && res && typeof res.text === "string") {
|
|
220
|
+
return {
|
|
221
|
+
html: res.text,
|
|
222
|
+
mimeType: MIME_MCP_APP,
|
|
223
|
+
...typeof res.uri === "string" ? { uri: res.uri } : {}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { html: raw, mimeType: MIME_MCP_APP };
|
|
230
|
+
}
|
|
231
|
+
var AUTO_FORWARD_TOOLS = /* @__PURE__ */ new Set([
|
|
232
|
+
"toon_read",
|
|
233
|
+
"toon_query",
|
|
234
|
+
"toon_status",
|
|
235
|
+
"toon_identity",
|
|
236
|
+
"toon_channels",
|
|
237
|
+
"toon_targets",
|
|
238
|
+
"toon_atoms"
|
|
239
|
+
]);
|
|
240
|
+
function classifyIntent(intent) {
|
|
241
|
+
return AUTO_FORWARD_TOOLS.has(intent.toolName) ? "auto" : "requires-consent";
|
|
242
|
+
}
|
|
243
|
+
var consentSeq = 0;
|
|
244
|
+
function buildConsentRequest(intent) {
|
|
245
|
+
consentSeq += 1;
|
|
246
|
+
return {
|
|
247
|
+
id: `consent-${consentSeq}`,
|
|
248
|
+
toolName: intent.toolName,
|
|
249
|
+
arguments: intent.arguments,
|
|
250
|
+
trust: "low"
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/render/KindRegistry.ts
|
|
255
|
+
var KindRegistry = class {
|
|
256
|
+
byKind = /* @__PURE__ */ new Map();
|
|
257
|
+
/**
|
|
258
|
+
* Register a native `component` as the renderer for one or more event
|
|
259
|
+
* `kinds`. Registering an already-registered kind overwrites it (last write
|
|
260
|
+
* wins) so a host can override a default; pass {@link register} per kind to
|
|
261
|
+
* keep the first registration explicit.
|
|
262
|
+
*/
|
|
263
|
+
register(kinds, component) {
|
|
264
|
+
const list = typeof kinds === "number" ? [kinds] : kinds;
|
|
265
|
+
for (const kind of list) {
|
|
266
|
+
this.byKind.set(kind, component);
|
|
267
|
+
}
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
/** The native component for `kind`, or `undefined` if the kind is unknown. */
|
|
271
|
+
lookup(kind) {
|
|
272
|
+
return this.byKind.get(kind);
|
|
273
|
+
}
|
|
274
|
+
/** Whether a native component is registered for `kind` (branch 1 applies). */
|
|
275
|
+
has(kind) {
|
|
276
|
+
return this.byKind.has(kind);
|
|
277
|
+
}
|
|
278
|
+
/** Every kind with a registered native component. */
|
|
279
|
+
kinds() {
|
|
280
|
+
return [...this.byKind.keys()];
|
|
281
|
+
}
|
|
282
|
+
/** Number of registered kinds. */
|
|
283
|
+
get size() {
|
|
284
|
+
return this.byKind.size;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// src/render/resolveRenderer.ts
|
|
289
|
+
import { verifyEvent as verifyEvent2 } from "nostr-tools/pure";
|
|
290
|
+
import {
|
|
291
|
+
UI_RENDERER_KIND as UI_RENDERER_KIND3,
|
|
292
|
+
getUiCoordinate as getUiCoordinate3,
|
|
293
|
+
parseUiCoordinate as parseUiCoordinate2,
|
|
294
|
+
selectLatestAddressable as selectLatestAddressable3
|
|
295
|
+
} from "@toon-protocol/core";
|
|
296
|
+
function tagValue2(event, name) {
|
|
297
|
+
return event.tags.find((t) => t[0] === name)?.[1];
|
|
298
|
+
}
|
|
299
|
+
function resolveUiCoordinate(event) {
|
|
300
|
+
const raw = tagValue2(event, "ui");
|
|
301
|
+
if (raw === void 0) return null;
|
|
302
|
+
const full = getUiCoordinate3(event) ?? parseUiCoordinate2(raw);
|
|
303
|
+
if (full) {
|
|
304
|
+
if (full.pubkey !== event.pubkey) return null;
|
|
305
|
+
return { kind: UI_RENDERER_KIND3, pubkey: event.pubkey, targetKind: full.targetKind };
|
|
306
|
+
}
|
|
307
|
+
const targetKind = Number(raw);
|
|
308
|
+
if (!Number.isInteger(targetKind) || targetKind < 0) return null;
|
|
309
|
+
return { kind: UI_RENDERER_KIND3, pubkey: event.pubkey, targetKind };
|
|
310
|
+
}
|
|
311
|
+
function resolveUiRenderer(event, candidates) {
|
|
312
|
+
const coord = resolveUiCoordinate(event);
|
|
313
|
+
if (!coord) return void 0;
|
|
314
|
+
const matches = candidates.filter(
|
|
315
|
+
(c) => c.kind === UI_RENDERER_KIND3 && c.pubkey === coord.pubkey && tagValue2(c, "d") === String(coord.targetKind)
|
|
316
|
+
);
|
|
317
|
+
const latest = selectLatestAddressable3(matches);
|
|
318
|
+
if (!latest) return void 0;
|
|
319
|
+
if (!verifyEvent2(latest)) return void 0;
|
|
320
|
+
return latest;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/render/index.ts
|
|
324
|
+
import {
|
|
325
|
+
parseUiCoordinate as parseUiCoordinate3,
|
|
326
|
+
getUiCoordinate as getUiCoordinate4,
|
|
327
|
+
buildUiCoordinate as buildUiCoordinate2,
|
|
328
|
+
selectLatestAddressable as selectLatestAddressable4
|
|
329
|
+
} from "@toon-protocol/core";
|
|
330
|
+
|
|
331
|
+
// src/render/generative.ts
|
|
332
|
+
function escapeHtml(value) {
|
|
333
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
334
|
+
}
|
|
335
|
+
var deterministicGenerator = {
|
|
336
|
+
async generate({ event }) {
|
|
337
|
+
return { html: renderDeterministicHtml(event), mimeType: MIME_MCP_APP, source: "deterministic" };
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
function renderDeterministicHtml(event) {
|
|
341
|
+
const rows = event.tags.filter((t) => t[0] !== void 0).map(
|
|
342
|
+
(t) => `<tr><th>${escapeHtml(String(t[0]))}</th><td>${escapeHtml(t.slice(1).join(" "))}</td></tr>`
|
|
343
|
+
).join("");
|
|
344
|
+
const content = event.content ? escapeHtml(event.content) : "<em>(no content)</em>";
|
|
345
|
+
return [
|
|
346
|
+
"<!doctype html>",
|
|
347
|
+
'<section data-toon-fallback="generative" data-trust="low">',
|
|
348
|
+
`<header><h1>Unknown event</h1><p>kind <code>${event.kind}</code></p></header>`,
|
|
349
|
+
`<p class="toon-fallback-note">No renderer was found for this kind. This is a best-effort, low-trust fallback rendering of the event's raw shape.</p>`,
|
|
350
|
+
`<dl><dt>author</dt><dd><code>${escapeHtml(event.pubkey)}</code></dd>`,
|
|
351
|
+
`<dt>id</dt><dd><code>${escapeHtml(event.id)}</code></dd></dl>`,
|
|
352
|
+
`<div class="toon-fallback-content">${content}</div>`,
|
|
353
|
+
rows ? `<table class="toon-fallback-tags"><tbody>${rows}</tbody></table>` : "",
|
|
354
|
+
"</section>"
|
|
355
|
+
].filter(Boolean).join("\n");
|
|
356
|
+
}
|
|
357
|
+
function buildRendererEventTemplate(targetKind, rendered) {
|
|
358
|
+
return {
|
|
359
|
+
kind: UI_RENDERER_KIND,
|
|
360
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
361
|
+
tags: [
|
|
362
|
+
["d", String(targetKind)],
|
|
363
|
+
["m", rendered.mimeType],
|
|
364
|
+
// Self-describing, curation-pending marker. The full curation/namespacing
|
|
365
|
+
// policy for community-published renderers is an open epic question
|
|
366
|
+
// (toon#58) and is intentionally not modelled here.
|
|
367
|
+
["t", "generative-fallback"]
|
|
368
|
+
],
|
|
369
|
+
content: rendered.html
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
var GenerativeFallbackRenderer = class {
|
|
373
|
+
generator;
|
|
374
|
+
publish;
|
|
375
|
+
constructor(options = {}) {
|
|
376
|
+
this.generator = options.generator ?? deterministicGenerator;
|
|
377
|
+
this.publish = options.publish;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Generate a fallback rendering for `event` (low trust), optionally publishing
|
|
381
|
+
* the result back as a `kind:31036` renderer when publish-back is enabled.
|
|
382
|
+
*/
|
|
383
|
+
async render(event) {
|
|
384
|
+
let rendered;
|
|
385
|
+
try {
|
|
386
|
+
rendered = await this.generator.generate({ event });
|
|
387
|
+
} catch {
|
|
388
|
+
rendered = await deterministicGenerator.generate({ event });
|
|
389
|
+
}
|
|
390
|
+
const result = { rendered, trust: "low" };
|
|
391
|
+
if (this.publish?.enabled && this.publish.signer && this.publish.publisher) {
|
|
392
|
+
const template = buildRendererEventTemplate(event.kind, rendered);
|
|
393
|
+
const signed = this.publish.signer.signEvent(template);
|
|
394
|
+
await this.publish.publisher.publishEvent(signed);
|
|
395
|
+
result.published = signed;
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
/** Whether publish-back is currently enabled (for host introspection/UI). */
|
|
400
|
+
get publishBackEnabled() {
|
|
401
|
+
return this.publish?.enabled === true;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function publishBackCoordinate(signer, targetKind) {
|
|
405
|
+
const coord = buildUiCoordinate({ pubkey: signer.getPublicKey(), targetKind });
|
|
406
|
+
if (coord === null) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`publishBackCoordinate: invalid renderer coordinate for pubkey=${signer.getPublicKey()} targetKind=${targetKind}`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return coord;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export {
|
|
415
|
+
MIME_A2UI,
|
|
416
|
+
MIME_MCP_APP,
|
|
417
|
+
UI_RENDERER_KIND,
|
|
418
|
+
UI_TAG,
|
|
419
|
+
isTrustDowngrade,
|
|
420
|
+
RendererPinStore,
|
|
421
|
+
verifyRendererTrust,
|
|
422
|
+
resolveRendererMime,
|
|
423
|
+
renderDispatch,
|
|
424
|
+
guardedRenderDispatch,
|
|
425
|
+
extractUiResource,
|
|
426
|
+
classifyIntent,
|
|
427
|
+
buildConsentRequest,
|
|
428
|
+
KindRegistry,
|
|
429
|
+
resolveUiCoordinate,
|
|
430
|
+
resolveUiRenderer,
|
|
431
|
+
deterministicGenerator,
|
|
432
|
+
renderDeterministicHtml,
|
|
433
|
+
buildRendererEventTemplate,
|
|
434
|
+
GenerativeFallbackRenderer,
|
|
435
|
+
publishBackCoordinate,
|
|
436
|
+
parseUiCoordinate3 as parseUiCoordinate,
|
|
437
|
+
getUiCoordinate4 as getUiCoordinate,
|
|
438
|
+
buildUiCoordinate2 as buildUiCoordinate,
|
|
439
|
+
selectLatestAddressable4 as selectLatestAddressable
|
|
440
|
+
};
|
|
441
|
+
//# sourceMappingURL=chunk-QEMD5EAI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/render/constants.ts","../src/render/swap-defense.ts","../src/render/dispatch.ts","../src/render/consent.ts","../src/render/KindRegistry.ts","../src/render/resolveRenderer.ts","../src/render/index.ts","../src/render/generative.ts"],"sourcesContent":["/**\n * Render-side protocol constants for NIP-on-TOON.\n *\n * The canonical homes for the renderer kind, the `ui` tag, and the\n * `UiCoordinate` helpers are `@toon-protocol/core` (published in `1.6.0`):\n * {@link UI_RENDERER_KIND} (31036), {@link UI_TAG}, and `parseUiCoordinate` /\n * `buildUiCoordinate` / `getUiCoordinate` / `selectLatestAddressable`. They are\n * re-exported here so the render module has a single import surface; only the\n * mime-type selectors below are owned locally (core does not export them).\n */\n\n// `UI_RENDERER_KIND`, `UI_TAG`, and the `UiCoordinate` helpers now come from\n// core — re-exported, not mirrored.\nexport {\n UI_RENDERER_KIND,\n UI_TAG,\n buildUiCoordinate,\n parseUiCoordinate,\n getUiCoordinate,\n selectLatestAddressable,\n type UiCoordinate,\n} from '@toon-protocol/core';\n\n/**\n * The `m` (mimeType) tag value selecting **branch 2** — A2UI, medium trust.\n *\n * Owned locally: core does not export the render-branch mime selectors.\n */\nexport const MIME_A2UI = 'application/a2ui+json';\n\n/**\n * The `m` (mimeType) tag value selecting **branch 3** — sandboxed mcp-ui iframe,\n * low trust.\n *\n * Owned locally: core does not export the render-branch mime selectors.\n */\nexport const MIME_MCP_APP = 'text/html;profile=mcp-app';\n","/**\n * Renderer-swap defense — the security guard layer around render dispatch\n * (toon-protocol/toon-client#91, part of toon-protocol/toon-meta#58).\n *\n * ── THREAT: \"renderer swap\" ───────────────────────────────────────────────────\n * A `kind:31036` renderer is an *addressable* event: the coordinate\n * `31036:<author-pubkey>:<targetKind>` can later resolve to a *different* event\n * (different `id`, different content) by publishing a newer-`created_at`\n * revision. Because resolving the `ui` tag yields a renderer that selects the\n * render strategy *and* the trust tier, an attacker who gets a malicious 31036\n * selected can attack the user:\n *\n * V1. Cross-author substitution — serve a 31036 authored by someone *other*\n * than the authoritative renderer author, hoping the client renders it.\n * V2. Forged / tampered renderer — serve a 31036 whose signature does not\n * verify (mutated tags/content, or no signature at all).\n * V3. Resolution race / nondeterminism — feed candidate revisions in an order\n * that makes some clients pick the attacker's revision.\n * V4. Silent mid-session swap — after a renderer has been pinned for an event,\n * publish a newer revision (new `id`) to swap the active renderer out from\n * under an already-decided render, especially to *downgrade trust* (e.g.\n * push a benign event into a hostile low-trust widget).\n *\n * Per toon#36 decisions: the renderer-author pubkey is the **event author** (the\n * `pubkey` of the event being rendered), and clients MUST re-verify the 31036\n * signature before it can select a render strategy.\n *\n * ── DEFENSE (this module) ─────────────────────────────────────────────────────\n * {@link verifyRendererTrust} is a guard placed *between* renderer resolution and\n * {@link renderDispatch}. It **fails closed**: on any violation it returns a\n * rejection and the caller drops to the safe branch (native for known kinds,\n * generative for unknown kinds) — it never renders the suspect renderer.\n *\n * - **Author binding (closes V1):** the resolved 31036's `pubkey` MUST equal\n * the authoritative renderer author (the rendered event's `pubkey`). The\n * coordinate's `pubkey` segment is also checked, so a coordinate pointing at\n * a third-party author is refused before any fetch is trusted.\n * - **Signature verification (closes V2):** the 31036 event signature is\n * re-verified with {@link verifyEvent}; a tampered/unsigned renderer is\n * refused.\n * - **Deterministic selection (closes V3):** candidates are collapsed with\n * {@link selectLatestAddressable} (latest `created_at`, lowest-`id`\n * tiebreak), so selection is not attacker-race-controllable.\n * - **Anti-swap pinning + downgrade detection (closes V4):** the chosen\n * renderer `id` and its trust tier are pinned per coordinate in a\n * {@link RendererPinStore}. A later revision with a *different* `id` is a\n * detected swap; if it would *lower* the trust tier the swap is refused\n * (fail closed). High-trust kinds use the issue's stricter rule: any `id`\n * change at all → refuse and fall back to the native component.\n *\n * ── RELATION TO {@link import('./resolveRenderer.js').resolveUiRenderer} ───────\n * `resolveRenderer.ts` (#97) is the plain, stateless `ui`→`kind:31036` resolver:\n * author-bound coordinate, latest-addressable selection, signature re-verify,\n * returning the renderer or `undefined`. This guard shares the SAME core\n * primitives it builds on — `getUiCoordinate` / `selectLatestAddressable` (now\n * from `@toon-protocol/core`) and `verifyEvent` — so the two agree bit-for-bit\n * on which revision a coordinate selects and on signature acceptance. The guard\n * adds what the plain resolver deliberately omits: a stateful anti-swap pin\n * store, trust-downgrade / high-trust id-change detection, and *granular*\n * fail-closed {@link SwapRejectionReason}s (the resolver collapses every failure\n * to `undefined`). It is therefore a strict superset, not a parallel copy.\n */\n\nimport { verifyEvent } from 'nostr-tools/pure';\nimport type { NostrEvent } from 'nostr-tools/pure';\n// The `ui` coordinate primitives now live in `@toon-protocol/core@1.6.0` (#97\n// dropped the local `constants.ts` mirror). The pure resolution/selection\n// helpers (`getUiCoordinate` / `selectLatestAddressable`) are shared verbatim\n// with `./resolveRenderer.js`, so the swap-defense guard and the plain resolver\n// agree bit-for-bit on which `kind:31036` revision a coordinate selects — the\n// guard layers anti-swap pinning + fail-closed *granular* rejection reasons on\n// top of that same selection, rather than re-deriving it.\nimport {\n UI_RENDERER_KIND,\n getUiCoordinate,\n selectLatestAddressable,\n type UiCoordinate,\n} from '@toon-protocol/core';\nimport type { KindRegistry } from './KindRegistry.js';\nimport type { RenderTrust } from './types.js';\n\n/** Ordering of the trust tiers; higher number = more trusted. */\nconst TRUST_RANK: Record<RenderTrust, number> = { low: 0, medium: 1, full: 2 };\n\n/** Whether trust tier `next` is strictly lower than `prev` (a downgrade). */\nexport function isTrustDowngrade(\n prev: RenderTrust,\n next: RenderTrust\n): boolean {\n return TRUST_RANK[next] < TRUST_RANK[prev];\n}\n\n/** Why a renderer was refused. Stable string values, safe to log. */\nexport type SwapRejectionReason =\n /** No `ui` coordinate on the event, or it was malformed. */\n | 'no-coordinate'\n /** The coordinate's author segment is not the rendered event's author. */\n | 'coordinate-author-mismatch'\n /** No candidate `kind:31036` renderer resolved for the coordinate. */\n | 'no-renderer'\n /** The resolved event is not a `kind:31036` renderer. */\n | 'not-a-renderer'\n /** The resolved renderer's `pubkey` is not the authoritative author. */\n | 'author-mismatch'\n /** The renderer's `d` tag does not match the coordinate's target kind. */\n | 'target-kind-mismatch'\n /** The renderer signature did not verify (tampered / unsigned). */\n | 'bad-signature'\n /** A high-trust kind's pinned renderer `id` changed (issue rule: refuse). */\n | 'high-trust-id-changed'\n /** The swap would lower the trust tier from the pinned tier. */\n | 'trust-downgrade';\n\n/** A renderer refused by the guard. The caller must fall back, not render. */\nexport interface SwapRejection {\n ok: false;\n reason: SwapRejectionReason;\n /** Human-readable detail for logging. */\n detail: string;\n /** The coordinate involved, when one was resolvable. */\n coordinate?: UiCoordinate;\n}\n\n/** A renderer the guard approved for dispatch. */\nexport interface SwapApproval {\n ok: true;\n /** The verified, author-bound, deterministically-selected renderer. */\n renderer: NostrEvent;\n /** The coordinate the renderer was selected for. */\n coordinate: UiCoordinate;\n /** Whether this approval newly pinned the coordinate (first sighting). */\n pinned: boolean;\n /** Set when an `id` swap was observed but allowed (non-downgrading). */\n swapObserved?: boolean;\n}\n\nexport type SwapDecision = SwapApproval | SwapRejection;\n\n/**\n * A pinned renderer decision for one coordinate: the `id` we committed to and\n * the trust tier it implied. Used to detect swaps and downgrades.\n */\nexport interface RendererPin {\n /** The pinned `kind:31036` event id. */\n id: string;\n /** The trust tier the pinned renderer selected. */\n trust: RenderTrust;\n}\n\n/**\n * Pins the chosen renderer per coordinate so a later replaceable `kind:31036`\n * cannot silently swap the active renderer mid-session. Keyed by the canonical\n * coordinate string `31036:<pubkey>:<targetKind>`.\n *\n * In-memory by default; a host may seed pins from config (the issue's\n * \"allowlist high-trust renderers by event id\" — pre-populate the expected `id`\n * for a known kind) via {@link pin}.\n */\nexport class RendererPinStore {\n private readonly byCoord = new Map<string, RendererPin>();\n\n private static key(coord: UiCoordinate): string {\n return `${coord.kind}:${coord.pubkey}:${coord.targetKind}`;\n }\n\n /** The pin for `coord`, or `undefined` if not yet pinned. */\n get(coord: UiCoordinate): RendererPin | undefined {\n return this.byCoord.get(RendererPinStore.key(coord));\n }\n\n /** Pin (or overwrite) the renderer decision for `coord`. */\n pin(coord: UiCoordinate, decision: RendererPin): this {\n this.byCoord.set(RendererPinStore.key(coord), { ...decision });\n return this;\n }\n\n /** Whether `coord` is pinned. */\n has(coord: UiCoordinate): boolean {\n return this.byCoord.has(RendererPinStore.key(coord));\n }\n\n /** Number of pinned coordinates. */\n get size(): number {\n return this.byCoord.size;\n }\n}\n\n/** The trust tier a renderer's `m` (mimeType) tag selects, or `undefined`. */\nfunction rendererTrust(renderer: NostrEvent): RenderTrust | undefined {\n const mime = renderer.tags.find((t) => t[0] === 'm')?.[1];\n if (mime === 'application/a2ui+json') return 'medium';\n if (mime === 'text/html;profile=mcp-app') return 'low';\n return undefined;\n}\n\n/** The renderer's `d` (target kind) tag, parsed, or `undefined`. */\nfunction rendererTargetKind(renderer: NostrEvent): number | undefined {\n const d = renderer.tags.find((t) => t[0] === 'd')?.[1];\n if (d === undefined || !/^\\d+$/.test(d)) return undefined;\n return Number(d);\n}\n\n/** Input to {@link verifyRendererTrust}. */\nexport interface VerifyRendererInput<C> {\n /** The event whose renderer is being resolved. Its `pubkey` is authoritative. */\n event: NostrEvent;\n /**\n * The candidate `kind:31036` renderer(s) fetched for the event's `ui`\n * coordinate. May contain multiple revisions; the guard picks the winner\n * deterministically. The caller does not pre-select.\n */\n candidates: readonly NostrEvent[];\n /** The branch-1 native registry; used to decide which kinds are \"high trust\". */\n registry: KindRegistry<C>;\n /** The pin store enforcing stable, anti-swap selection across resolutions. */\n pins: RendererPinStore;\n /**\n * Signature verifier (defaults to nostr-tools `verifyEvent`). Injectable so\n * tests can exercise the fail-closed path deterministically.\n */\n verify?: (event: NostrEvent) => boolean;\n /**\n * Treat the event's kind as a \"high-trust\" kind subject to the issue's strict\n * id-allowlist rule (any `id` change → refuse, fall back to native). Defaults\n * to \"the registry has a native component for this kind\", matching the spec:\n * branch-1 known kinds are the high-trust set. A host may override.\n */\n isHighTrustKind?: (kind: number) => boolean;\n}\n\n/**\n * Guard a renderer before it can select a render strategy. Runs author binding,\n * signature verification, deterministic selection, and anti-swap / downgrade\n * detection. **Fails closed**: any violation returns a {@link SwapRejection} and\n * the caller must drop to the safe branch rather than render.\n *\n * On approval, the chosen renderer is pinned for its coordinate so a subsequent\n * resolution that yields a different `id` is detected (and, if it would downgrade\n * trust, refused).\n */\nexport function verifyRendererTrust<C>(\n input: VerifyRendererInput<C>\n): SwapDecision {\n const { event, candidates, registry, pins } = input;\n const verify = input.verify ?? verifyEvent;\n const isHighTrustKind =\n input.isHighTrustKind ?? ((kind: number) => registry.has(kind));\n\n // The authoritative renderer author is the rendered EVENT's author (toon#36).\n const authorPubkey = event.pubkey;\n\n // 0. The event must carry a well-formed `ui` coordinate.\n const coordinate = getUiCoordinate(event);\n if (coordinate === null) {\n return {\n ok: false,\n reason: 'no-coordinate',\n detail: 'event has no parseable ui tag',\n };\n }\n\n // Author binding (V1), checked at the coordinate level first: a coordinate\n // pointing at a third party is refused before we trust any fetched event.\n if (coordinate.pubkey !== authorPubkey) {\n return {\n ok: false,\n reason: 'coordinate-author-mismatch',\n detail: `ui coordinate author ${coordinate.pubkey} != event author ${authorPubkey}`,\n coordinate,\n };\n }\n\n // 1. Deterministic selection (V3): collapse all candidates to the single\n // winning revision — latest created_at, lowest-id tiebreak — but only among\n // candidates that actually match this coordinate (right kind + author +\n // target kind), so a foreign revision cannot influence the pick.\n const matching = candidates.filter(\n (c) =>\n c.kind === UI_RENDERER_KIND &&\n c.pubkey === authorPubkey &&\n rendererTargetKind(c) === coordinate.targetKind\n );\n const renderer = selectLatestAddressable([...matching]);\n if (renderer === undefined) {\n return {\n ok: false,\n reason: 'no-renderer',\n detail: 'no candidate matched the coordinate',\n coordinate,\n };\n }\n\n // 2. Structural checks on the selected renderer (defence in depth — the filter\n // above already enforces these, but assert them explicitly so a future\n // refactor cannot quietly weaken the guarantee).\n if (renderer.kind !== UI_RENDERER_KIND) {\n return {\n ok: false,\n reason: 'not-a-renderer',\n detail: `kind ${renderer.kind} != ${UI_RENDERER_KIND}`,\n coordinate,\n };\n }\n if (renderer.pubkey !== authorPubkey) {\n return {\n ok: false,\n reason: 'author-mismatch',\n detail: `renderer author ${renderer.pubkey} != event author ${authorPubkey}`,\n coordinate,\n };\n }\n if (rendererTargetKind(renderer) !== coordinate.targetKind) {\n return {\n ok: false,\n reason: 'target-kind-mismatch',\n detail: `renderer d != coordinate target kind ${coordinate.targetKind}`,\n coordinate,\n };\n }\n\n // 3. Signature verification (V2): re-verify before the renderer can select a\n // strategy. A tampered or unsigned renderer fails closed.\n let signatureOk: boolean;\n try {\n signatureOk = verify(renderer);\n } catch {\n signatureOk = false;\n }\n if (!signatureOk) {\n return {\n ok: false,\n reason: 'bad-signature',\n detail: `renderer ${renderer.id} signature did not verify`,\n coordinate,\n };\n }\n\n // 4. Anti-swap pinning + downgrade detection (V4).\n const trust = rendererTrust(renderer);\n // Renderers with an unrecognised `m` tag select no branch; treat as low trust\n // for downgrade math so they can never be used to *raise* a later pin's floor.\n const effectiveTrust: RenderTrust = trust ?? 'low';\n const existing = pins.get(coordinate);\n const highTrust = isHighTrustKind(event.kind);\n\n if (existing === undefined) {\n // First sighting for this coordinate — establish the pin.\n pins.pin(coordinate, { id: renderer.id, trust: effectiveTrust });\n return { ok: true, renderer, coordinate, pinned: true };\n }\n\n if (existing.id === renderer.id) {\n // Same revision as pinned — stable, no swap.\n return { ok: true, renderer, coordinate, pinned: false };\n }\n\n // The `id` changed under a stable coordinate: this is a swap.\n if (highTrust) {\n // Issue rule for high-trust (branch-1 known) kinds: never silently render a\n // new id. Refuse so the caller falls back to the native component.\n return {\n ok: false,\n reason: 'high-trust-id-changed',\n detail: `high-trust kind ${event.kind}: pinned renderer ${existing.id} swapped to ${renderer.id}`,\n coordinate,\n };\n }\n\n if (isTrustDowngrade(existing.trust, effectiveTrust)) {\n // A trust-lowering swap (e.g. medium A2UI → low sandboxed/unknown). Refuse:\n // the user already saw content at the higher tier; do not silently demote.\n return {\n ok: false,\n reason: 'trust-downgrade',\n detail: `swap would downgrade trust ${existing.trust} → ${effectiveTrust} for coordinate ${coordinate.pubkey}:${coordinate.targetKind}`,\n coordinate,\n };\n }\n\n // A non-downgrading swap of a low-trust kind: allowed, but re-pin and flag so\n // the host can surface \"renderer updated\" through the trust gradient again.\n pins.pin(coordinate, { id: renderer.id, trust: effectiveTrust });\n return { ok: true, renderer, coordinate, pinned: false, swapObserved: true };\n}\n","/**\n * Kind-keyed render dispatch — the skeleton the four render branches plug into.\n *\n * Implements §\"Client dispatch algorithm\" of the NIP-on-TOON render-side spec\n * (`skills/nip-on-toon-discovery/SKILL.md` in toon-meta):\n *\n * 1. Is this kind known? → **branch 1** (native registry). Done.\n * 2. Otherwise resolve the event's `ui` tag to a `kind:31036` renderer.\n * 3. If a renderer is found, read its `m` (mimeType) tag:\n * - `application/a2ui+json` → **branch 2** (A2UI, medium trust)\n * - `text/html;profile=mcp-app` → **branch 3** (sandboxed mcp-ui, low)\n * 4. If no renderer is found → **branch 4** (generative fallback, low trust).\n *\n * SCOPE (#88 — skeleton + branch 1 only): branch 1 is wired through the\n * {@link KindRegistry}; branches 2/3/4 are returned as clearly-marked decisions\n * for the sibling tickets to consume (#89 A2UI, #90 mcp-ui + consent, #92\n * generative). This module does NOT render — it returns a {@link RenderDecision}.\n *\n * The `ui`-tag → `kind:31036` *resolution* lives outside this function — see\n * {@link resolveUiRenderer} in `./resolveRenderer.js`, which parses the `ui`\n * coordinate (via core's `getUiCoordinate` / `parseUiCoordinate`), picks the\n * latest addressable `kind:31036` (`selectLatestAddressable`), and re-verifies\n * its signature before trusting it. The dispatch takes the already-resolved\n * renderer event via {@link DispatchInput.renderer}.\n */\n\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport { MIME_A2UI, MIME_MCP_APP, UI_RENDERER_KIND } from './constants.js';\nimport type { KindRegistry } from './KindRegistry.js';\nimport type { RenderDecision } from './types.js';\nimport {\n verifyRendererTrust,\n type RendererPinStore,\n type SwapRejection,\n} from './swap-defense.js';\n\n/** Read the first value of the named tag, or `undefined`. */\nfunction tagValue(event: NostrEvent, name: string): string | undefined {\n const tag = event.tags.find((t) => t[0] === name);\n return tag?.[1];\n}\n\n/**\n * The `m` (mimeType) tag value of a resolved `kind:31036` renderer, or\n * `undefined` if the event is not a renderer or carries no `m` tag.\n *\n * The `m` tag is the format selector that picks the branch + trust tier.\n */\nexport function resolveRendererMime(\n renderer: NostrEvent | undefined\n): string | undefined {\n if (!renderer || renderer.kind !== UI_RENDERER_KIND) return undefined;\n return tagValue(renderer, 'm');\n}\n\n/** Input to {@link renderDispatch}. */\nexport interface DispatchInput {\n /** The decoded event the client wants to render. */\n event: NostrEvent;\n /**\n * The `kind:31036` renderer resolved from the event's `ui` tag, if any.\n *\n * Resolution (parse the `ui` coordinate, fetch + pick the latest addressable\n * `kind:31036`) is performed by the caller — see the toon#36 spike. Only\n * consulted when the event's kind is unknown (branches 2–4).\n */\n renderer?: NostrEvent;\n}\n\n/**\n * Route an event to one of the four render branches.\n *\n * @param input The event + (optionally) its resolved `kind:31036` renderer.\n * @param registry The branch-1 native-component registry to consult first.\n * @returns A {@link RenderDecision} naming the branch, trust tier, and payload.\n */\nexport function renderDispatch<C>(\n input: DispatchInput,\n registry: KindRegistry<C>\n): RenderDecision<C> {\n const { event, renderer } = input;\n\n // Branch 1 — known kind → native component, full trust.\n const component = registry.lookup(event.kind);\n if (component !== undefined) {\n return { branch: 'native', trust: 'full', event, component };\n }\n\n // Unknown kind: the `m` tag of the resolved renderer picks the branch.\n const mime = resolveRendererMime(renderer);\n\n // Branch 2 — A2UI, medium trust. [STUB: renderer in #89]\n if (mime === MIME_A2UI && renderer) {\n return { branch: 'a2ui', trust: 'medium', event, renderer };\n }\n\n // Branch 3 — sandboxed mcp-ui iframe, low trust. [STUB: renderer + consent in #90]\n if (mime === MIME_MCP_APP && renderer) {\n return { branch: 'mcp-ui', trust: 'low', event, renderer };\n }\n\n // Branch 4 — no (recognised) renderer → generative fallback, low trust.\n // [STUB: generative fallback + kind:31036 publish-back in #92]\n return { branch: 'generative', trust: 'low', event };\n}\n\n/** Input to {@link guardedRenderDispatch}. */\nexport interface GuardedDispatchInput {\n /** The decoded event the client wants to render. */\n event: NostrEvent;\n /**\n * The candidate `kind:31036` renderer(s) fetched for the event's `ui`\n * coordinate, *unfiltered*. The swap-defense guard selects the winner\n * deterministically and verifies it; the caller does NOT pre-select. Pass an\n * empty array (or omit) when no renderer was resolved.\n */\n candidates?: readonly NostrEvent[];\n}\n\n/** Why {@link guardedRenderDispatch} fell back to a safe branch. */\nexport interface DispatchGuardInfo {\n /** A renderer was refused by the swap-defense guard. */\n rejected: SwapRejection;\n}\n\n/**\n * Dispatch with the **renderer-swap defense** (toon-client#91) interposed.\n *\n * This is the secure entry point: it runs {@link verifyRendererTrust} over the\n * raw candidate renderers *before* {@link renderDispatch} can pick a strategy,\n * and **fails closed** on any violation (wrong-author, bad signature,\n * trust-downgrading swap, high-trust id change):\n *\n * - Known kind → branch 1 (native) regardless of renderers. The guard still\n * runs so a *high-trust* renderer swap is detected, but a known kind always\n * has the native component to fall back to, so the result is branch 1.\n * - Unknown kind, renderer **approved** → normal {@link renderDispatch} with the\n * single verified renderer (branches 2/3).\n * - Unknown kind, renderer **refused** or none → branch 4 (generative). We do\n * NOT pass the suspect renderer through; the user gets the safe fallback.\n *\n * @returns the {@link RenderDecision} plus, when a renderer was refused, the\n * {@link DispatchGuardInfo} describing why (for logging / UX \"renderer refused\").\n */\nexport function guardedRenderDispatch<C>(\n input: GuardedDispatchInput,\n registry: KindRegistry<C>,\n pins: RendererPinStore\n): { decision: RenderDecision<C>; guard?: DispatchGuardInfo } {\n const { event } = input;\n const candidates = input.candidates ?? [];\n\n // Known kind: branch 1 wins. Still run the guard so a high-trust swap is\n // observed/pinned, but the safe fall-back for a known kind is its native\n // component, so the decision is branch 1 either way.\n if (registry.has(event.kind)) {\n const guarded = verifyRendererTrust({ event, candidates, registry, pins });\n const decision = renderDispatch({ event }, registry);\n return guarded.ok\n ? { decision }\n : { decision, guard: { rejected: guarded } };\n }\n\n // Unknown kind with no candidates: nothing to guard → generative fallback.\n if (candidates.length === 0) {\n return { decision: renderDispatch({ event }, registry) };\n }\n\n // Unknown kind with candidates: the guard must approve a renderer before it\n // can select a strategy. Fail closed to generative on refusal.\n const guarded = verifyRendererTrust({ event, candidates, registry, pins });\n if (!guarded.ok) {\n return {\n decision: renderDispatch({ event }, registry),\n guard: { rejected: guarded },\n };\n }\n\n // Approved: dispatch with ONLY the verified, author-bound renderer.\n return {\n decision: renderDispatch({ event, renderer: guarded.renderer }, registry),\n };\n}\n","/**\n * The consent invariant — the load-bearing security property of branch 3\n * (sandboxed mcp-ui, low trust) of the NIP-on-TOON render trust gradient\n * (toon-meta#58, toon-client#90; spec §\"Branch 3 — sandboxed mcp-ui & the\n * consent invariant\").\n *\n * ── The invariant (verbatim from the spec) ──────────────────────────────────\n * A sandboxed widget may only *request* an action. The authorization surface is\n * rendered by the trusted client outside the iframe and is non-themeable. The\n * sandboxed widget can never draw, style, or spoof the consent/authorization UI.\n * A widget that can paint the authorization UI collapses the entire trust\n * gradient to its lowest tier.\n * ────────────────────────────────────────────────────────────────────────────\n *\n * This module is the framework-agnostic half of branch 3. It carries the\n * *decision* and the *policy*, not any React tree — mirroring how `@toon-protocol/client`'s\n * {@link ./dispatch} carries the render decision and `@toon-protocol/views`\n * carries the rendered component. The React side (the sandboxed `AppRenderer`\n * iframe + the host-rendered, non-themeable `ConsentPrompt`) lives in\n * `@toon-protocol/views`; it consumes the types and functions defined here.\n *\n * Why the policy lives here, away from React:\n * - The classification of \"is this a state-changing action that needs explicit\n * authorization?\" is a pure, auditable function with no DOM dependency.\n * - The widget supplies ZERO inputs to this function that can influence the\n * *appearance* of the prompt: it supplies only the requested tool name and\n * arguments. Everything the prompt renders is derived by the trusted client.\n */\n\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport { MIME_MCP_APP, UI_RENDERER_KIND } from './constants.js';\n\n/**\n * The widget payload handed to the sandboxed iframe. The `m`-tagged\n * `text/html;profile=mcp-app` renderer ships a raw HTML widget as a UIResource;\n * we pass the HTML through to the iframe untouched, but everything the host\n * renders around it is client-controlled.\n *\n * Deliberately minimal: the host needs only the HTML to feed the iframe and the\n * mimeType to assert the branch. No widget-supplied styling, theme, chrome, or\n * \"trusted\" hints are carried — by construction the widget cannot pass any.\n */\nexport interface UiResource {\n /** The raw widget HTML to render inside the sandboxed iframe. */\n html: string;\n /** Always `text/html;profile=mcp-app` for branch 3 (asserted on extract). */\n mimeType: string;\n /** The `ui://…` resource URI, if the renderer declared one (host metadata only). */\n uri?: string;\n}\n\n/**\n * Extract the branch-3 {@link UiResource} from a resolved `kind:31036` renderer\n * event whose `m` tag is `text/html;profile=mcp-app`.\n *\n * The renderer's `content` is either the raw widget HTML, or a JSON-encoded\n * MCP `UIResource` embedded-resource block (`{ type: 'resource', resource: {\n * uri, mimeType, text } }`) as produced by mcp-ui servers. Both are accepted;\n * the HTML is returned verbatim for iframe passthrough.\n *\n * Returns `undefined` (never throws) when the event is not a usable branch-3\n * renderer, so the caller can fall through to branch 4 rather than render\n * something unexpected.\n */\nexport function extractUiResource(renderer: NostrEvent | undefined): UiResource | undefined {\n if (!renderer || renderer.kind !== UI_RENDERER_KIND) return undefined;\n const mime = renderer.tags.find((t) => t[0] === 'm')?.[1];\n if (mime !== MIME_MCP_APP) return undefined;\n\n const raw = renderer.content;\n if (typeof raw !== 'string' || raw.length === 0) return undefined;\n\n // Try the embedded-resource JSON shape first; fall back to raw HTML.\n const trimmed = raw.trimStart();\n if (trimmed.startsWith('{')) {\n try {\n const parsed = JSON.parse(raw) as {\n type?: string;\n resource?: { uri?: unknown; mimeType?: unknown; text?: unknown };\n };\n const res = parsed.resource;\n if (parsed.type === 'resource' && res && typeof res.text === 'string') {\n return {\n html: res.text,\n mimeType: MIME_MCP_APP,\n ...(typeof res.uri === 'string' ? { uri: res.uri } : {}),\n };\n }\n } catch {\n // Not JSON — treat the whole content as raw HTML below.\n }\n }\n return { html: raw, mimeType: MIME_MCP_APP };\n}\n\n/**\n * An action a sandboxed widget *requested* (never performed). This is the only\n * thing that crosses the iframe → host boundary, and it carries no presentation\n * data — only the tool name and arguments the widget wants to invoke.\n */\nexport interface WidgetIntent {\n /** The tool/action name the widget asked the host to invoke. */\n toolName: string;\n /** The arguments the widget supplied for that tool. */\n arguments: Record<string, unknown>;\n}\n\n/**\n * The classification of a {@link WidgetIntent}: does it need an explicit, host-\n * rendered authorization decision before the host may act on it?\n *\n * - `requires-consent` — a state-changing / spendy / outbound action. The host\n * MUST render the {@link ConsentRequest} prompt (outside the iframe,\n * non-themeable) and only proceed on an explicit user grant.\n * - `auto` — a read-only / inert request the host may forward without a prompt.\n *\n * Default-deny: anything not provably inert is treated as `requires-consent`.\n */\nexport type IntentClassification = 'requires-consent' | 'auto';\n\n/**\n * Tool-name prefixes / names that are read-only on the TOON client and therefore\n * safe to forward without an authorization prompt. Everything else (publishing,\n * paying, swapping, channel ops, media upload, link opening, anything unknown)\n * requires explicit, host-rendered consent.\n *\n * This allowlist is intentionally tiny and lives in trusted client code; a\n * widget cannot extend it.\n */\nconst AUTO_FORWARD_TOOLS: ReadonlySet<string> = new Set([\n 'toon_read',\n 'toon_query',\n 'toon_status',\n 'toon_identity',\n 'toon_channels',\n 'toon_targets',\n 'toon_atoms',\n]);\n\n/**\n * Classify a widget intent. Pure and default-deny: only an exact match against\n * the trusted read-only allowlist is auto-forwarded; everything else requires a\n * host-rendered consent prompt.\n */\nexport function classifyIntent(intent: WidgetIntent): IntentClassification {\n return AUTO_FORWARD_TOOLS.has(intent.toolName) ? 'auto' : 'requires-consent';\n}\n\n/**\n * The data the trusted host needs to render an authorization prompt for a\n * widget-requested action. EVERY field here is either a fixed, client-owned\n * constant or a plain machine value copied out of the intent — there is NO\n * styling, theme, color, label-override, HTML, or markup field the widget could\n * supply. This is what makes the prompt non-themeable by construction: the type\n * simply has nowhere to put presentation input.\n */\nexport interface ConsentRequest {\n /** Stable id for correlating the prompt with its resolution. */\n readonly id: string;\n /** The tool the widget asked to invoke (rendered as plain text by the host). */\n readonly toolName: string;\n /** The arguments the widget supplied (rendered as inspectable data, not HTML). */\n readonly arguments: Record<string, unknown>;\n /**\n * The trust tier of the requesting surface — always `'low'` for a branch-3\n * sandboxed widget. Carried so the host can render the appropriate warning\n * chrome; the widget cannot change it.\n */\n readonly trust: 'low';\n}\n\n/** The user's decision on a {@link ConsentRequest}. */\nexport type ConsentDecision = 'grant' | 'deny';\n\n/** Monotonic counter so generated consent ids are unique within a session. */\nlet consentSeq = 0;\n\n/**\n * Build a {@link ConsentRequest} from a widget intent. The host calls this when\n * {@link classifyIntent} returns `requires-consent`. It copies ONLY the tool\n * name and arguments out of the widget's request; it fixes `trust: 'low'` and\n * generates the id itself. The widget contributes nothing to how the prompt\n * looks.\n */\nexport function buildConsentRequest(intent: WidgetIntent): ConsentRequest {\n consentSeq += 1;\n return {\n id: `consent-${consentSeq}`,\n toolName: intent.toolName,\n arguments: intent.arguments,\n trust: 'low',\n };\n}\n","/**\n * Branch 1 — the native-component registry.\n *\n * A `kind → native component` map for the kinds the client knows natively. This\n * is the registry abstraction that {@link renderDispatch} consults first: a hit\n * is branch 1 (full trust), a miss falls through to the unknown-kind branches.\n *\n * The component type `C` is generic so the rendering package\n * (`@toon-protocol/views`) instantiates it with its own component contract (e.g.\n * an `Atom`) — this keeps `@toon-protocol/client` free of any React dependency\n * while still owning the dispatch + registry abstraction (per the epic split:\n * dispatch in `client`, branch-1 components in `views`).\n *\n * Replaces ad-hoc per-kind conditionals with a single register/lookup seam.\n */\n\n/** A native component registered for one or more event kinds. */\nexport class KindRegistry<C> {\n private readonly byKind = new Map<number, C>();\n\n /**\n * Register a native `component` as the renderer for one or more event\n * `kinds`. Registering an already-registered kind overwrites it (last write\n * wins) so a host can override a default; pass {@link register} per kind to\n * keep the first registration explicit.\n */\n register(kinds: number | readonly number[], component: C): this {\n const list = typeof kinds === 'number' ? [kinds] : kinds;\n for (const kind of list) {\n this.byKind.set(kind, component);\n }\n return this;\n }\n\n /** The native component for `kind`, or `undefined` if the kind is unknown. */\n lookup(kind: number): C | undefined {\n return this.byKind.get(kind);\n }\n\n /** Whether a native component is registered for `kind` (branch 1 applies). */\n has(kind: number): boolean {\n return this.byKind.has(kind);\n }\n\n /** Every kind with a registered native component. */\n kinds(): number[] {\n return [...this.byKind.keys()];\n }\n\n /** Number of registered kinds. */\n get size(): number {\n return this.byKind.size;\n }\n}\n","/**\n * `ui`-tag → `kind:31036` renderer resolution (toon#36).\n *\n * This is the resolution seam the {@link renderDispatch} skeleton (#88)\n * deliberately left out: dispatch consumes an *already-resolved* renderer, and\n * this module produces it. It is split out from dispatch so the relay query +\n * cache (which is IO, and lives in the daemon / {@link import('../ToonClient.js')})\n * stays separate from the pure selection logic — mirroring core's own\n * \"helpers are pure, resolution is client-local\" split.\n *\n * Algorithm, per the toon#36 decisions:\n *\n * 1. Read the rendered event's `ui` tag and parse it into a target coordinate.\n * The coordinate convention is `31036:<renderer-author-pubkey>:<targetKind>`.\n * Per toon#36 the **renderer-author pubkey is the EVENT AUTHOR**, so the\n * `ui` tag MAY carry just the bare target kind (e.g. `42`); the author is\n * taken from `event.pubkey`. A full `31036:<pubkey>:<kind>` coordinate is\n * also accepted (via core's `getUiCoordinate`), but its pubkey MUST equal\n * the event author — a coordinate naming a different author is rejected, so\n * an event cannot point at a third party's renderer.\n * 2. Filter the caller-supplied `kind:31036` candidates to that coordinate\n * (author === event author, `d` tag === target kind) and pick the latest\n * addressable one (NIP-33 latest-wins) via `selectLatestAddressable`.\n * 3. **Re-verify the signature** of the chosen renderer with `verifyEvent`\n * before trusting it. A renderer that fails verification is dropped (the\n * resolution returns `undefined`) — the client never feeds an unverified\n * renderer to the dispatch.\n *\n * The relay query that produces `candidates` is the caller's responsibility:\n * query `kind:31036`, `authors: [event.pubkey]`, `#d: [String(targetKind)]`.\n */\n\nimport { verifyEvent } from 'nostr-tools/pure';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport {\n UI_RENDERER_KIND,\n getUiCoordinate,\n parseUiCoordinate,\n selectLatestAddressable,\n} from '@toon-protocol/core';\n\n/** Read the first value of the named tag, or `undefined`. */\nfunction tagValue(event: NostrEvent, name: string): string | undefined {\n return event.tags.find((t) => t[0] === name)?.[1];\n}\n\n/**\n * The renderer coordinate a rendered event points at: a `kind:31036` event\n * authored by the rendered event's author, targeting `targetKind`.\n */\nexport interface ResolvedCoordinate {\n /** Always {@link UI_RENDERER_KIND} (31036). */\n kind: typeof UI_RENDERER_KIND;\n /** The renderer author pubkey — per toon#36, the EVENT AUTHOR's pubkey. */\n pubkey: string;\n /** The kind of event the renderer targets (the renderer's `d` value). */\n targetKind: number;\n}\n\n/**\n * Compute the renderer coordinate a rendered event points at, anchoring the\n * renderer-author pubkey to the **event author** per toon#36.\n *\n * Accepts two `ui` tag shapes:\n * - a bare target kind, e.g. `[\"ui\", \"42\"]` → author = `event.pubkey`;\n * - a full coordinate, e.g. `[\"ui\", \"31036:<pubkey>:42\"]` → accepted only if\n * `<pubkey>` equals `event.pubkey` (else `null`).\n *\n * Pure: no IO.\n *\n * @param event - The rendered event that may carry a `ui` tag.\n * @returns The resolved coordinate, or `null` if there is no usable `ui` tag.\n */\nexport function resolveUiCoordinate(event: NostrEvent): ResolvedCoordinate | null {\n const raw = tagValue(event, 'ui');\n if (raw === undefined) return null;\n\n // Full `31036:<pubkey>:<kind>` coordinate form.\n const full = getUiCoordinate(event) ?? parseUiCoordinate(raw);\n if (full) {\n // The coordinate must name the event author — no third-party renderers.\n if (full.pubkey !== event.pubkey) return null;\n return { kind: UI_RENDERER_KIND, pubkey: event.pubkey, targetKind: full.targetKind };\n }\n\n // Bare target-kind form: the author is the event author.\n const targetKind = Number(raw);\n if (!Number.isInteger(targetKind) || targetKind < 0) return null;\n return { kind: UI_RENDERER_KIND, pubkey: event.pubkey, targetKind };\n}\n\n/**\n * Resolve a rendered event's `ui` tag to a verified `kind:31036` renderer.\n *\n * Filters `candidates` to the coordinate computed by {@link resolveUiCoordinate},\n * picks the latest addressable match, and **re-verifies its signature** before\n * returning it. The result feeds {@link renderDispatch} as `DispatchInput.renderer`.\n *\n * @param event - The rendered event carrying the `ui` tag.\n * @param candidates - `kind:31036` events the caller fetched for this coordinate\n * (the relay query is the caller's responsibility).\n * @returns The latest verified renderer, or `undefined` if none resolves /\n * verifies.\n */\nexport function resolveUiRenderer(\n event: NostrEvent,\n candidates: readonly NostrEvent[]\n): NostrEvent | undefined {\n const coord = resolveUiCoordinate(event);\n if (!coord) return undefined;\n\n const matches = candidates.filter(\n (c) =>\n c.kind === UI_RENDERER_KIND &&\n c.pubkey === coord.pubkey &&\n tagValue(c, 'd') === String(coord.targetKind)\n );\n\n const latest = selectLatestAddressable(matches);\n if (!latest) return undefined;\n\n // Re-verify the resolved renderer's signature before trusting it (toon#36).\n if (!verifyEvent(latest)) return undefined;\n\n return latest;\n}\n","/**\n * NIP-on-TOON render dispatch (toon-protocol/toon-meta#58).\n *\n * The kind-keyed dispatch skeleton + branch-1 native-component registry. Branches\n * 2/4 are routed to as clearly-marked decisions for the sibling tickets to\n * implement (#89 A2UI, #92 generative fallback); branch 3 (#90) adds the\n * framework-agnostic consent invariant consumed by the `@toon-protocol/views`\n * sandboxed renderer.\n */\n\nexport {\n renderDispatch,\n resolveRendererMime,\n guardedRenderDispatch,\n} from './dispatch.js';\nexport type {\n DispatchInput,\n GuardedDispatchInput,\n DispatchGuardInfo,\n} from './dispatch.js';\nexport {\n extractUiResource,\n classifyIntent,\n buildConsentRequest,\n} from './consent.js';\nexport type {\n UiResource,\n WidgetIntent,\n IntentClassification,\n ConsentRequest,\n ConsentDecision,\n} from './consent.js';\nexport { KindRegistry } from './KindRegistry.js';\nexport { resolveUiCoordinate, resolveUiRenderer } from './resolveRenderer.js';\nexport type { ResolvedCoordinate } from './resolveRenderer.js';\nexport { UI_RENDERER_KIND, UI_TAG, MIME_A2UI, MIME_MCP_APP } from './constants.js';\n// The `ui` coordinate helpers + type now live in `@toon-protocol/core@1.6.0`\n// (#97 dropped the local mirror); re-exported here so the render module keeps a\n// single import surface for them.\nexport {\n parseUiCoordinate,\n getUiCoordinate,\n buildUiCoordinate,\n selectLatestAddressable,\n} from '@toon-protocol/core';\nexport type { UiCoordinate } from '@toon-protocol/core';\nexport {\n verifyRendererTrust,\n isTrustDowngrade,\n RendererPinStore,\n} from './swap-defense.js';\nexport type {\n SwapDecision,\n SwapApproval,\n SwapRejection,\n SwapRejectionReason,\n RendererPin,\n VerifyRendererInput,\n} from './swap-defense.js';\nexport type {\n RenderBranch,\n RenderTrust,\n RenderDecision,\n NativeDecision,\n A2uiDecision,\n McpUiDecision,\n GenerativeDecision,\n} from './types.js';\n\n// Branch 4 — generative fallback + optional kind:31036 publish-back (#92).\nexport {\n GenerativeFallbackRenderer,\n deterministicGenerator,\n renderDeterministicHtml,\n buildRendererEventTemplate,\n publishBackCoordinate,\n} from './generative.js';\nexport type {\n GeneratedRenderer,\n GenerateContext,\n RendererGenerator,\n RendererSigner,\n RendererPublisher,\n PublishBackOptions,\n GenerativeFallbackOptions,\n GenerativeFallbackResult,\n} from './generative.js';\n","/**\n * Branch 4 — generative fallback + optional `kind:31036` publish-back.\n *\n * Implements §\"branch 4 — generative fallback\" of the NIP-on-TOON render-side\n * spec (`skills/nip-on-toon-discovery/SKILL.md` in toon-meta) and\n * toon-protocol/toon-client#92.\n *\n * Branch 4 is reached by {@link renderDispatch} when a kind is **unknown** *and*\n * no resolvable `kind:31036` renderer exists (no `ui` tag, or nothing resolves,\n * or the resolved renderer carries no recognised `m` tag). With nothing else to\n * go on, the client generates a best-effort rendering for the unknown event's\n * shape at **low trust**.\n *\n * Design seams (per the issue's `needs:human` open questions — the model, the\n * curation policy, and the publish-back opt-in semantics are product decisions\n * the host owns, so this module hardcodes none of them):\n *\n * - **Generator** ({@link RendererGenerator}) — the actual model call is\n * abstracted behind an interface the host injects. The host wires its own\n * provider/keys/prompt; this module never imports an LLM SDK. A deterministic\n * non-LLM generator ({@link deterministicGenerator}) is provided as the\n * default and for tests, so branch 4 always produces *something* renderable\n * even with no model configured.\n * - **Publish-back** — optionally republish the generated renderer as a\n * `kind:31036` addressable event so the next client has a \"known\" renderer\n * for that kind (branch 4 slowly feeds branch 1). This is a **guarded,\n * off-by-default** capability: it only fires when the host explicitly passes\n * `publish: { enabled: true, ... }`. The published renderer is clearly\n * low-trust / curation-pending; the namespacing & curation policy for\n * community-published renderers is an open question in the epic (toon#58) and\n * is intentionally *not* built here.\n */\n\nimport type { NostrEvent, EventTemplate } from 'nostr-tools/pure';\nimport { MIME_MCP_APP, UI_RENDERER_KIND, buildUiCoordinate } from './constants.js';\n\n/**\n * A generated renderer for an unknown event kind: an HTML `UIResource`-style\n * document plus the `m` (mimeType) tag that classifies it.\n *\n * The HTML is the raw widget body; if published back as a `kind:31036` event it\n * is rendered through branch 3 (sandboxed mcp-ui iframe) by a downstream client,\n * which is why {@link mimeType} defaults to the branch-3 selector.\n */\nexport interface GeneratedRenderer {\n /** The generated HTML document (the `UIResource` body). */\n html: string;\n /**\n * The `m` (mimeType) tag for the generated renderer. Defaults to\n * {@link MIME_MCP_APP} (`text/html;profile=mcp-app`) so a published renderer\n * routes through branch 3 (sandboxed, low trust) on the next client.\n */\n mimeType: string;\n /**\n * Whether this rendering came from a model (`'model'`) or the built-in\n * deterministic fallback (`'deterministic'`). Surfaced so the host can label\n * trust/provenance in the UI.\n */\n source: 'model' | 'deterministic';\n}\n\n/** Context handed to a {@link RendererGenerator}. */\nexport interface GenerateContext {\n /** The unknown event the client wants to render. */\n event: NostrEvent;\n}\n\n/**\n * The pluggable generator seam. A host injects its own implementation (wired to\n * whatever model endpoint, key, and prompt it has chosen — all `needs:human`\n * product decisions this module deliberately does not own).\n *\n * Implementations should be best-effort and resilient: a failed model call\n * should reject so {@link GenerativeFallbackRenderer} can fall back to the\n * deterministic generator rather than render nothing.\n */\nexport interface RendererGenerator {\n generate(ctx: GenerateContext): Promise<GeneratedRenderer>;\n}\n\n/** Minimal HTML-escape for embedding event data in the deterministic template. */\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * A deterministic, non-LLM generator: renders a best-effort, dependency-free\n * \"unknown kind\" card from the event's shape (kind, author, tags, content). It\n * never calls a network or a model, so it is the safe default and the basis for\n * tests — given the same event it always produces the same HTML.\n *\n * The output is intentionally generic and clearly marked as a low-trust\n * fallback; it makes no claim to understand the kind's semantics.\n */\nexport const deterministicGenerator: RendererGenerator = {\n async generate({ event }: GenerateContext): Promise<GeneratedRenderer> {\n return { html: renderDeterministicHtml(event), mimeType: MIME_MCP_APP, source: 'deterministic' };\n },\n};\n\n/**\n * The pure HTML projection used by {@link deterministicGenerator}. Exported so\n * tests (and hosts wanting the fallback body without the Promise wrapper) can\n * assert on it directly.\n */\nexport function renderDeterministicHtml(event: NostrEvent): string {\n const rows = event.tags\n .filter((t) => t[0] !== undefined)\n .map(\n (t) =>\n `<tr><th>${escapeHtml(String(t[0]))}</th><td>${escapeHtml(t.slice(1).join(' '))}</td></tr>`\n )\n .join('');\n const content = event.content ? escapeHtml(event.content) : '<em>(no content)</em>';\n return [\n '<!doctype html>',\n '<section data-toon-fallback=\"generative\" data-trust=\"low\">',\n `<header><h1>Unknown event</h1><p>kind <code>${event.kind}</code></p></header>`,\n `<p class=\"toon-fallback-note\">No renderer was found for this kind. This is a best-effort, low-trust fallback rendering of the event's raw shape.</p>`,\n `<dl><dt>author</dt><dd><code>${escapeHtml(event.pubkey)}</code></dd>`,\n `<dt>id</dt><dd><code>${escapeHtml(event.id)}</code></dd></dl>`,\n `<div class=\"toon-fallback-content\">${content}</div>`,\n rows ? `<table class=\"toon-fallback-tags\"><tbody>${rows}</tbody></table>` : '',\n '</section>',\n ]\n .filter(Boolean)\n .join('\\n');\n}\n\n/** Host-supplied signer seam used to finalize the publish-back event. */\nexport interface RendererSigner {\n /** The author pubkey (hex) the renderer is published under (the coordinate author). */\n getPublicKey(): string;\n /** Finalize an unsigned event template into a signed `NostrEvent`. */\n signEvent(template: EventTemplate): NostrEvent;\n}\n\n/** Host-supplied publisher seam used to broadcast the publish-back event. */\nexport interface RendererPublisher {\n publishEvent(event: NostrEvent): Promise<unknown>;\n}\n\n/**\n * Publish-back configuration. **Off by default**: publish-back never happens\n * unless the host passes this object with `enabled: true` *and* supplies a\n * signer and publisher. This is the explicit-enablement gate the issue requires\n * — there is no implicit / always-on publish path.\n */\nexport interface PublishBackOptions {\n /** Master switch. Must be `true` for any publish to occur. */\n enabled: boolean;\n /** Signs the `kind:31036` event (also supplies the coordinate author pubkey). */\n signer: RendererSigner;\n /** Broadcasts the signed event. */\n publisher: RendererPublisher;\n}\n\n/** Options for {@link GenerativeFallbackRenderer}. */\nexport interface GenerativeFallbackOptions {\n /**\n * The generator to use. Defaults to {@link deterministicGenerator}. A host\n * injects its model-backed generator here.\n */\n generator?: RendererGenerator;\n /**\n * Publish-back config. Omit (or pass `enabled: false`) to keep publish-back\n * off — the default. See {@link PublishBackOptions}.\n */\n publish?: PublishBackOptions;\n}\n\n/** The outcome of {@link GenerativeFallbackRenderer.render}. */\nexport interface GenerativeFallbackResult {\n /** The generated renderer (model output, or the deterministic fallback). */\n rendered: GeneratedRenderer;\n /** Always `'low'` — branch 4 is a low-trust path. */\n trust: 'low';\n /**\n * The signed `kind:31036` event that was published back, or `undefined` when\n * publish-back was disabled (the default) or could not run.\n */\n published?: NostrEvent;\n}\n\n/**\n * Build the unsigned `kind:31036` renderer event for a generated renderer. The\n * `d` tag is the target kind; the `m` tag is the renderer's mimeType; the body\n * is the generated HTML. The signed event's coordinate is\n * `31036:<author-pubkey>:<targetKind>` (see {@link buildUiCoordinate}).\n *\n * Exported for tests / hosts that want to inspect the event before signing.\n */\nexport function buildRendererEventTemplate(\n targetKind: number,\n rendered: GeneratedRenderer\n): EventTemplate {\n return {\n kind: UI_RENDERER_KIND,\n created_at: Math.floor(Date.now() / 1000),\n tags: [\n ['d', String(targetKind)],\n ['m', rendered.mimeType],\n // Self-describing, curation-pending marker. The full curation/namespacing\n // policy for community-published renderers is an open epic question\n // (toon#58) and is intentionally not modelled here.\n ['t', 'generative-fallback'],\n ],\n content: rendered.html,\n };\n}\n\n/**\n * Branch 4 renderer: generate a best-effort rendering for an unknown event and,\n * if explicitly enabled, publish it back as a `kind:31036` renderer.\n *\n * Generation is resilient: if the injected model generator throws, the renderer\n * transparently falls back to {@link deterministicGenerator} so a rendering is\n * always produced.\n */\nexport class GenerativeFallbackRenderer {\n private readonly generator: RendererGenerator;\n private readonly publish?: PublishBackOptions;\n\n constructor(options: GenerativeFallbackOptions = {}) {\n this.generator = options.generator ?? deterministicGenerator;\n this.publish = options.publish;\n }\n\n /**\n * Generate a fallback rendering for `event` (low trust), optionally publishing\n * the result back as a `kind:31036` renderer when publish-back is enabled.\n */\n async render(event: NostrEvent): Promise<GenerativeFallbackResult> {\n let rendered: GeneratedRenderer;\n try {\n rendered = await this.generator.generate({ event });\n } catch {\n // Model call failed → never render nothing; use the deterministic fallback.\n rendered = await deterministicGenerator.generate({ event });\n }\n\n const result: GenerativeFallbackResult = { rendered, trust: 'low' };\n\n // Publish-back gate: only when explicitly enabled with a signer + publisher.\n if (this.publish?.enabled && this.publish.signer && this.publish.publisher) {\n const template = buildRendererEventTemplate(event.kind, rendered);\n const signed = this.publish.signer.signEvent(template);\n await this.publish.publisher.publishEvent(signed);\n result.published = signed;\n }\n\n return result;\n }\n\n /** Whether publish-back is currently enabled (for host introspection/UI). */\n get publishBackEnabled(): boolean {\n return this.publish?.enabled === true;\n }\n}\n\n/**\n * The coordinate (`31036:<author-pubkey>:<targetKind>`) a publish-back will/did\n * use for `targetKind` under `signer`'s identity. Convenience for hosts that\n * want to show or pre-resolve the coordinate.\n *\n * Throws if the signer's pubkey or `targetKind` is malformed — core's\n * {@link buildUiCoordinate} returns `null` for invalid inputs, which here can\n * only mean the host wired a bad signer.\n */\nexport function publishBackCoordinate(signer: RendererSigner, targetKind: number): string {\n const coord = buildUiCoordinate({ pubkey: signer.getPublicKey(), targetKind });\n if (coord === null) {\n throw new Error(\n `publishBackCoordinate: invalid renderer coordinate for pubkey=${signer.getPublicKey()} targetKind=${targetKind}`\n );\n }\n return coord;\n}\n"],"mappings":"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","names":["UI_RENDERER_KIND","getUiCoordinate","selectLatestAddressable","guarded","verifyEvent","UI_RENDERER_KIND","getUiCoordinate","parseUiCoordinate","selectLatestAddressable","tagValue","parseUiCoordinate","getUiCoordinate","buildUiCoordinate","selectLatestAddressable"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as _toon_protocol_core from '@toon-protocol/core';
|
|
2
2
|
import { IlpPeerInfo, IlpClient, IlpSendResult, NetworkFamilyStatus, ConnectorAdminClient, ConnectorChannelClient, OpenChannelParams, OpenChannelResult, ChannelState } from '@toon-protocol/core';
|
|
3
|
+
export { UI_RENDERER_KIND, UI_TAG, UiCoordinate, buildUiCoordinate, getUiCoordinate, parseUiCoordinate, selectLatestAddressable } from '@toon-protocol/core';
|
|
3
4
|
import { NostrEvent, EventTemplate } from 'nostr-tools/pure';
|
|
4
5
|
import { PrivateKeyAccount } from 'viem/accounts';
|
|
6
|
+
export { A2uiDecision, ConsentDecision, ConsentRequest, DispatchGuardInfo, DispatchInput, GenerateContext, GeneratedRenderer, GenerativeDecision, GenerativeFallbackOptions, GenerativeFallbackRenderer, GenerativeFallbackResult, GuardedDispatchInput, IntentClassification, KindRegistry, MIME_A2UI, MIME_MCP_APP, McpUiDecision, NativeDecision, PublishBackOptions, RenderBranch, RenderDecision, RenderTrust, RendererGenerator, RendererPin, RendererPinStore, RendererPublisher, RendererSigner, ResolvedCoordinate, SwapApproval, SwapDecision, SwapRejection, SwapRejectionReason, UiResource, VerifyRendererInput, WidgetIntent, buildConsentRequest, buildRendererEventTemplate, classifyIntent, deterministicGenerator, extractUiResource, guardedRenderDispatch, isTrustDowngrade, publishBackCoordinate, renderDeterministicHtml, renderDispatch, resolveRendererMime, resolveUiCoordinate, resolveUiRenderer, verifyRendererTrust } from './render/index.js';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Solana payment-channel parameters supplied via `ToonClientConfig.solanaChannel`.
|
|
@@ -2882,12 +2884,12 @@ declare function buildPetPurchaseRequest(params: PetPurchaseRequestParams): Unsi
|
|
|
2882
2884
|
*
|
|
2883
2885
|
* EVM `POST {faucetUrl}/api/request` body `{ address }` → 100 ETH + 10k USDC
|
|
2884
2886
|
* Solana `POST {faucetUrl}/api/solana/request` body `{ address }` → SOL + USDC
|
|
2885
|
-
* Mina `POST {faucetUrl}/api/mina/request` body `{ address }` → native MINA
|
|
2887
|
+
* Mina `POST {faucetUrl}/api/mina/request` body `{ address }` → native MINA + USDC
|
|
2886
2888
|
*
|
|
2887
2889
|
* Devnet edge (today): `https://faucet.devnet.toonprotocol.dev`.
|
|
2888
2890
|
*
|
|
2889
|
-
*
|
|
2890
|
-
*
|
|
2891
|
+
* All three chains are live on the deployed faucet — the request shape is
|
|
2892
|
+
* identical (`{ address }`); only the path differs.
|
|
2891
2893
|
*/
|
|
2892
2894
|
/** Supported faucet chains. */
|
|
2893
2895
|
type FaucetChain = 'evm' | 'solana' | 'mina';
|
|
@@ -2913,8 +2915,8 @@ interface FundWalletOptions {
|
|
|
2913
2915
|
* @param faucetUrl - Faucet base URL, e.g. `https://faucet.devnet.toonprotocol.dev`.
|
|
2914
2916
|
* A trailing `/` is tolerated.
|
|
2915
2917
|
* @param address - The chain address to fund (EVM 0x address, Solana base58, etc).
|
|
2916
|
-
* @param chain - `'evm'`
|
|
2917
|
-
* @throws {Error} If `faucetUrl
|
|
2918
|
+
* @param chain - `'evm'` | `'solana'` | `'mina'` (all live on the devnet faucet).
|
|
2919
|
+
* @throws {Error} If `faucetUrl` or `address` is missing.
|
|
2918
2920
|
* @throws {NetworkError} On transport failure or a non-2xx faucet response.
|
|
2919
2921
|
*/
|
|
2920
2922
|
declare function fundWallet(faucetUrl: string, address: string, chain: FaucetChain, options?: FundWalletOptions): Promise<FundWalletResult>;
|