@frockbot/kernel-contracts 0.1.3 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/kernel-contracts",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
package/src/authoring.ts CHANGED
@@ -15,6 +15,8 @@ import type {} from "cordis";
15
15
  export const PACKAGE_BUNDLE_MAX_SOURCE_BYTES = 256 * 1024;
16
16
  /** The only entry a Bot-authored Package may declare in this slice. */
17
17
  export const PACKAGE_BUNDLE_ENTRY = "package.ts";
18
+ /** Raw HTML is content-addressed without transforming it. */
19
+ export const PACKAGE_UI_ARTIFACT_VERSION = "frockbot-inline-html@1";
18
20
 
19
21
  export interface PackageBundleSourceV1 {
20
22
  path: string;
@@ -29,6 +31,8 @@ export interface PackageBundleRequestV1 {
29
31
  compatibilityDate: string;
30
32
  entry: "package.ts";
31
33
  sources: PackageBundleSourceV1[];
34
+ /** Optional immutable inline-only HTML page; never compiled into app code. */
35
+ ui?: { path: "ui.html"; html: string };
32
36
  }
33
37
 
34
38
  export interface PackageBundleArtifactV1 {
@@ -39,12 +43,23 @@ export interface PackageBundleArtifactV1 {
39
43
  bundlerVersion: string;
40
44
  }
41
45
 
46
+ export interface PackageUiArtifactV1 {
47
+ /** sha-256 hex of the exact HTML bytes. */
48
+ contentHash: string;
49
+ size: number;
50
+ mediaType: "text/html";
51
+ bundlerVersion: string;
52
+ }
53
+
42
54
  export type PackageBundleResultV1 =
43
55
  | {
44
56
  schemaVersion: 1;
45
57
  effectId: string;
46
58
  status: "bundled";
47
59
  artifact: PackageBundleArtifactV1;
60
+ uiArtifact?: PackageUiArtifactV1;
61
+ /** Exact HTML bytes; the Durable Object owns the immutable write. */
62
+ uiHtml?: string;
48
63
  /** The module bytes as text; the Durable Object owns the artifact write. */
49
64
  module: string;
50
65
  diagnostics: string[];
@@ -130,6 +145,38 @@ export function decodePackageBundleArtifactV1(
130
145
  };
131
146
  }
132
147
 
148
+ export function decodePackageUiArtifactV1(
149
+ input: unknown,
150
+ label = "package UI artifact",
151
+ ): PackageUiArtifactV1 {
152
+ const value = record(input, label);
153
+ exactKeys(
154
+ value,
155
+ ["contentHash", "size", "mediaType", "bundlerVersion"],
156
+ label,
157
+ );
158
+ if (
159
+ typeof value.contentHash !== "string" ||
160
+ !SHA256_HEX.test(value.contentHash)
161
+ )
162
+ throw new Error(`${label}.contentHash must be a sha-256 hex digest`);
163
+ if (!Number.isSafeInteger(value.size) || (value.size as number) < 0)
164
+ throw new Error(`${label}.size must be a non-negative integer`);
165
+ if (value.mediaType !== "text/html")
166
+ throw new Error(`${label}.mediaType is invalid`);
167
+ const bundlerVersion = boundedString(
168
+ value.bundlerVersion,
169
+ `${label}.bundlerVersion`,
170
+ 128,
171
+ );
172
+ return {
173
+ contentHash: value.contentHash,
174
+ size: value.size as number,
175
+ mediaType: "text/html",
176
+ bundlerVersion,
177
+ };
178
+ }
179
+
133
180
  /**
134
181
  * The exact v1 decoder for what comes back across the bundler binding. The
135
182
  * bundler is a separate Worker, so its answer is an inbound value at a
@@ -144,6 +191,12 @@ export function decodePackageBundleResultV1(
144
191
  throw new Error(`${label}.schemaVersion is unsupported`);
145
192
  const effectId = boundedString(value.effectId, `${label}.effectId`, 200);
146
193
  if (value.status === "bundled") {
194
+ const hasUi = value.uiArtifact !== undefined || value.uiHtml !== undefined;
195
+ if ((value.uiArtifact === undefined) !== (value.uiHtml === undefined)) {
196
+ throw new Error(
197
+ `${label} must return UI artifact metadata and HTML together`,
198
+ );
199
+ }
147
200
  exactKeys(
148
201
  value,
149
202
  [
@@ -153,6 +206,7 @@ export function decodePackageBundleResultV1(
153
206
  "artifact",
154
207
  "module",
155
208
  "diagnostics",
209
+ ...(hasUi ? ["uiArtifact", "uiHtml"] : []),
156
210
  ],
157
211
  label,
158
212
  );
@@ -162,12 +216,22 @@ export function decodePackageBundleResultV1(
162
216
  );
163
217
  if (typeof value.module !== "string" || value.module.length === 0)
164
218
  throw new Error(`${label}.module must be a non-empty string`);
219
+ const uiArtifact = hasUi
220
+ ? decodePackageUiArtifactV1(value.uiArtifact, `${label}.uiArtifact`)
221
+ : undefined;
222
+ if (
223
+ hasUi &&
224
+ (typeof value.uiHtml !== "string" || value.uiHtml.length === 0)
225
+ ) {
226
+ throw new Error(`${label}.uiHtml must be a non-empty string`);
227
+ }
165
228
  return {
166
229
  schemaVersion: 1,
167
230
  effectId,
168
231
  status: "bundled",
169
232
  artifact,
170
233
  module: value.module,
234
+ ...(uiArtifact ? { uiArtifact, uiHtml: value.uiHtml as string } : {}),
171
235
  diagnostics: diagnostics(value.diagnostics, `${label}.diagnostics`),
172
236
  };
173
237
  }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { decodeSessionEvent } from "./types.js";
3
+
4
+ const base = {
5
+ seq: 1,
6
+ timestamp: "2026-09-02T00:00:00.000Z",
7
+ turn: 1,
8
+ step: 1,
9
+ effectId: "catalog-effect-1",
10
+ };
11
+
12
+ describe("Catalog change session events", () => {
13
+ test("decode the exact install intent and outcome", () => {
14
+ expect(
15
+ decodeSessionEvent({
16
+ ...base,
17
+ type: "package/catalog-change-intent",
18
+ action: "install",
19
+ catalogId: "parcel-tracking",
20
+ contentHash: "a".repeat(64),
21
+ }),
22
+ ).toMatchObject({ action: "install", catalogId: "parcel-tracking" });
23
+ expect(
24
+ decodeSessionEvent({
25
+ ...base,
26
+ type: "package/catalog-changed",
27
+ action: "install",
28
+ packageId: "parcel-tracking",
29
+ contentHash: "a".repeat(64),
30
+ generationId: "generation-2",
31
+ }),
32
+ ).toMatchObject({ packageId: "parcel-tracking" });
33
+ });
34
+
35
+ test("refuses mixed install/remove identities and unknown fields", () => {
36
+ expect(() =>
37
+ decodeSessionEvent({
38
+ ...base,
39
+ type: "package/catalog-change-intent",
40
+ action: "remove",
41
+ packageId: "parcel-tracking",
42
+ contentHash: "a".repeat(64),
43
+ }),
44
+ ).toThrow("identity is invalid");
45
+ expect(() =>
46
+ decodeSessionEvent({
47
+ ...base,
48
+ type: "package/catalog-changed",
49
+ action: "remove",
50
+ packageId: "parcel-tracking",
51
+ generationId: "generation-2",
52
+ source: "secret",
53
+ }),
54
+ ).toThrow("invalid fields");
55
+ });
56
+ });
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ decodePackageIframeCatalogV1,
4
+ decodePackageIframePageMessageV1,
5
+ decodePackageIframeToolCommandV1,
6
+ packageIframeToolAllowedV1,
7
+ } from "./iframe-ui.js";
8
+
9
+ describe("Package iframe bridge v1", () => {
10
+ test("exactly decodes the two page-to-host messages", () => {
11
+ expect(
12
+ decodePackageIframePageMessageV1({
13
+ schemaVersion: 1,
14
+ type: "callTool",
15
+ name: "weather_lookup",
16
+ input: { city: "Sydney" },
17
+ }),
18
+ ).toMatchObject({ type: "callTool", name: "weather_lookup" });
19
+ expect(
20
+ decodePackageIframePageMessageV1({
21
+ schemaVersion: 1,
22
+ type: "resize",
23
+ height: 320,
24
+ }),
25
+ ).toEqual({ schemaVersion: 1, type: "resize", height: 320 });
26
+ expect(() =>
27
+ decodePackageIframePageMessageV1({
28
+ schemaVersion: 1,
29
+ type: "resize",
30
+ height: 320,
31
+ token: "secret",
32
+ }),
33
+ ).toThrow("invalid fields");
34
+ });
35
+
36
+ test("refuses a tool the Package did not declare", () => {
37
+ const contribution = { declaredTools: ["weather_lookup"] };
38
+ expect(packageIframeToolAllowedV1(contribution, "weather_lookup")).toBe(
39
+ true,
40
+ );
41
+ expect(packageIframeToolAllowedV1(contribution, "package_author")).toBe(
42
+ false,
43
+ );
44
+ });
45
+
46
+ test("exactly decodes the durable tool command", () => {
47
+ const command = {
48
+ schemaVersion: 1 as const,
49
+ commandId: "command-1",
50
+ generationId: "generation-1",
51
+ packageId: "weather-lookup",
52
+ name: "weather_lookup",
53
+ input: { city: "Sydney" },
54
+ };
55
+ expect(decodePackageIframeToolCommandV1(command)).toEqual(command);
56
+ expect(() =>
57
+ decodePackageIframeToolCommandV1({ ...command, authToken: "nope" }),
58
+ ).toThrow("invalid fields");
59
+ });
60
+
61
+ test("bounds projected HTML artifacts at the catalog seam", () => {
62
+ const catalog = {
63
+ schemaVersion: 1,
64
+ botId: "bot",
65
+ generationId: "generation-1",
66
+ artifactOrigin: "https://ui.bot.frockbot.com",
67
+ contributions: [
68
+ {
69
+ packageId: "weather-page",
70
+ displayName: "Weather page",
71
+ provenance: "Bot-authored",
72
+ artifact: {
73
+ contentHash: "a".repeat(64),
74
+ size: 256 * 1024,
75
+ mediaType: "text/html",
76
+ bundlerVersion: "frockbot-inline-html@1",
77
+ },
78
+ mounts: [{ slot: "frockbot.tool-result:weather_lookup" }],
79
+ declaredTools: ["weather_lookup"],
80
+ },
81
+ ],
82
+ };
83
+ expect(decodePackageIframeCatalogV1(catalog).contributions).toHaveLength(1);
84
+ expect(() =>
85
+ decodePackageIframeCatalogV1({
86
+ ...catalog,
87
+ contributions: [
88
+ {
89
+ ...catalog.contributions[0],
90
+ artifact: {
91
+ ...catalog.contributions[0]!.artifact,
92
+ size: 256 * 1024 + 1,
93
+ },
94
+ },
95
+ ],
96
+ }),
97
+ ).toThrow("metadata is invalid");
98
+ });
99
+ });
@@ -0,0 +1,400 @@
1
+ /** Versioned, deliberately tiny postMessage seam for sandboxed Package pages. */
2
+ export const PACKAGE_IFRAME_BRIDGE_VERSION = 1 as const;
3
+
4
+ export type PackageIframeHostMessageV1 =
5
+ | {
6
+ schemaVersion: 1;
7
+ type: "init";
8
+ themeTokens: Record<string, string>;
9
+ packageId: string;
10
+ botId: string;
11
+ slot: string;
12
+ }
13
+ | {
14
+ schemaVersion: 1;
15
+ type: "state";
16
+ name: string;
17
+ value: unknown;
18
+ };
19
+
20
+ export type PackageIframePageMessageV1 =
21
+ | {
22
+ schemaVersion: 1;
23
+ type: "callTool";
24
+ name: string;
25
+ input: unknown;
26
+ }
27
+ | {
28
+ schemaVersion: 1;
29
+ type: "resize";
30
+ height: number;
31
+ };
32
+
33
+ export interface PackageIframeContributionViewV1 {
34
+ packageId: string;
35
+ displayName: string;
36
+ provenance: "Bot-authored" | "User-installed";
37
+ artifact: {
38
+ contentHash: string;
39
+ size: number;
40
+ mediaType: "text/html";
41
+ bundlerVersion: string;
42
+ };
43
+ mounts: Array<{ slot: string; order?: number }>;
44
+ declaredTools: string[];
45
+ }
46
+
47
+ export interface PackageIframeCompositionV1 {
48
+ schemaVersion: 1;
49
+ botId: string;
50
+ generationId: string;
51
+ contributions: PackageIframeContributionViewV1[];
52
+ }
53
+
54
+ export interface PackageIframeCatalogV1 extends PackageIframeCompositionV1 {
55
+ /** Separate, anonymous serving origin; artifact paths are appended by the host. */
56
+ artifactOrigin: string;
57
+ }
58
+
59
+ export interface PackageIframeToolCommandV1 {
60
+ schemaVersion: 1;
61
+ commandId: string;
62
+ generationId: string;
63
+ packageId: string;
64
+ name: string;
65
+ input: unknown;
66
+ }
67
+
68
+ function record(value: unknown, label: string): Record<string, unknown> {
69
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
70
+ throw new Error(`${label} must be an object`);
71
+ }
72
+ return value as Record<string, unknown>;
73
+ }
74
+
75
+ function exact(
76
+ value: Record<string, unknown>,
77
+ fields: readonly string[],
78
+ label: string,
79
+ ): void {
80
+ if (
81
+ Object.keys(value).length !== fields.length ||
82
+ !fields.every((field) => Object.hasOwn(value, field))
83
+ ) {
84
+ throw new Error(`${label} has invalid fields`);
85
+ }
86
+ }
87
+
88
+ function boundedString(value: unknown, label: string, maximum = 256): string {
89
+ if (
90
+ typeof value !== "string" ||
91
+ value.length === 0 ||
92
+ value.length > maximum
93
+ ) {
94
+ throw new Error(`${label} must be a bounded non-empty string`);
95
+ }
96
+ return value;
97
+ }
98
+
99
+ function json(value: unknown, label: string, depth = 0): void {
100
+ if (depth > 16) throw new Error(`${label} is too deeply nested`);
101
+ if (
102
+ value === null ||
103
+ typeof value === "string" ||
104
+ typeof value === "boolean" ||
105
+ (typeof value === "number" && Number.isFinite(value))
106
+ )
107
+ return;
108
+ if (Array.isArray(value)) {
109
+ if (value.length > 256) throw new Error(`${label} has too many entries`);
110
+ for (const entry of value) json(entry, label, depth + 1);
111
+ return;
112
+ }
113
+ const object = record(value, label);
114
+ if (Object.keys(object).length > 256)
115
+ throw new Error(`${label} has too many fields`);
116
+ for (const entry of Object.values(object)) json(entry, label, depth + 1);
117
+ }
118
+
119
+ function boundedJsonWire(value: unknown, label: string): void {
120
+ let wire: string | undefined;
121
+ try {
122
+ wire = JSON.stringify(value);
123
+ } catch {
124
+ throw new Error(`${label} must contain only acyclic JSON`);
125
+ }
126
+ if (wire === undefined) throw new Error(`${label} must contain JSON`);
127
+ if (new TextEncoder().encode(wire).byteLength > 64 * 1024) {
128
+ throw new Error(`${label} exceeds the wire byte limit`);
129
+ }
130
+ }
131
+
132
+ /** Exact host-side decoder; unknown message types and fields fail closed. */
133
+ export function decodePackageIframePageMessageV1(
134
+ input: unknown,
135
+ ): PackageIframePageMessageV1 {
136
+ const value = record(input, "Package iframe message");
137
+ boundedJsonWire(input, "Package iframe message");
138
+ if (value.schemaVersion !== 1)
139
+ throw new Error("Package iframe schemaVersion is unsupported");
140
+ if (value.type === "callTool") {
141
+ exact(
142
+ value,
143
+ ["schemaVersion", "type", "name", "input"],
144
+ "Package iframe callTool",
145
+ );
146
+ const name = boundedString(value.name, "Package iframe callTool.name", 64);
147
+ if (!/^[a-z][a-z0-9_]{0,63}$/.test(name))
148
+ throw new Error("Package iframe tool name is invalid");
149
+ json(value.input, "Package iframe callTool.input");
150
+ return {
151
+ schemaVersion: 1,
152
+ type: "callTool",
153
+ name,
154
+ input: structuredClone(value.input),
155
+ };
156
+ }
157
+ if (value.type === "resize") {
158
+ exact(value, ["schemaVersion", "type", "height"], "Package iframe resize");
159
+ if (typeof value.height !== "number" || !Number.isFinite(value.height)) {
160
+ throw new Error("Package iframe resize.height must be finite");
161
+ }
162
+ return { schemaVersion: 1, type: "resize", height: value.height };
163
+ }
164
+ throw new Error("Package iframe message type is invalid");
165
+ }
166
+
167
+ export function decodePackageIframeToolCommandV1(
168
+ input: unknown,
169
+ ): PackageIframeToolCommandV1 {
170
+ const value = record(input, "Package iframe tool command");
171
+ boundedJsonWire(input, "Package iframe tool command");
172
+ exact(
173
+ value,
174
+ [
175
+ "schemaVersion",
176
+ "commandId",
177
+ "generationId",
178
+ "packageId",
179
+ "name",
180
+ "input",
181
+ ],
182
+ "Package iframe tool command",
183
+ );
184
+ if (value.schemaVersion !== 1)
185
+ throw new Error("Package iframe tool command version is unsupported");
186
+ const name = boundedString(
187
+ value.name,
188
+ "Package iframe tool command.name",
189
+ 64,
190
+ );
191
+ if (!/^[a-z][a-z0-9_]{0,63}$/.test(name))
192
+ throw new Error("Package iframe tool command name is invalid");
193
+ json(value.input, "Package iframe tool command.input");
194
+ return {
195
+ schemaVersion: 1,
196
+ commandId: boundedString(
197
+ value.commandId,
198
+ "Package iframe tool command.commandId",
199
+ ),
200
+ generationId: boundedString(
201
+ value.generationId,
202
+ "Package iframe tool command.generationId",
203
+ ),
204
+ packageId: boundedString(
205
+ value.packageId,
206
+ "Package iframe tool command.packageId",
207
+ 64,
208
+ ),
209
+ name,
210
+ input: structuredClone(value.input),
211
+ };
212
+ }
213
+
214
+ export function packageIframeToolAllowedV1(
215
+ contribution: Pick<PackageIframeContributionViewV1, "declaredTools">,
216
+ name: string,
217
+ ): boolean {
218
+ return contribution.declaredTools.includes(name);
219
+ }
220
+
221
+ export function decodePackageIframeCatalogV1(
222
+ input: unknown,
223
+ ): PackageIframeCatalogV1 {
224
+ const value = record(input, "Package iframe catalog");
225
+ exact(
226
+ value,
227
+ [
228
+ "schemaVersion",
229
+ "botId",
230
+ "generationId",
231
+ "artifactOrigin",
232
+ "contributions",
233
+ ],
234
+ "Package iframe catalog",
235
+ );
236
+ if (value.schemaVersion !== 1)
237
+ throw new Error("Package iframe catalog version is unsupported");
238
+ const artifactOrigin = boundedString(
239
+ value.artifactOrigin,
240
+ "Package iframe artifactOrigin",
241
+ 2_048,
242
+ );
243
+ const origin = new URL(artifactOrigin);
244
+ if (
245
+ origin.origin !== artifactOrigin ||
246
+ !["http:", "https:"].includes(origin.protocol)
247
+ ) {
248
+ throw new Error("Package iframe artifactOrigin is invalid");
249
+ }
250
+ if (!Array.isArray(value.contributions) || value.contributions.length > 64) {
251
+ throw new Error("Package iframe contributions must be a bounded array");
252
+ }
253
+ const contributions = value.contributions.map((candidate, index) => {
254
+ const label = `Package iframe contributions[${index}]`;
255
+ const contribution = record(candidate, label);
256
+ exact(
257
+ contribution,
258
+ [
259
+ "packageId",
260
+ "displayName",
261
+ "provenance",
262
+ "artifact",
263
+ "mounts",
264
+ "declaredTools",
265
+ ],
266
+ label,
267
+ );
268
+ const artifact = record(contribution.artifact, `${label}.artifact`);
269
+ exact(
270
+ artifact,
271
+ ["contentHash", "size", "mediaType", "bundlerVersion"],
272
+ `${label}.artifact`,
273
+ );
274
+ if (
275
+ typeof artifact.contentHash !== "string" ||
276
+ !/^[0-9a-f]{64}$/.test(artifact.contentHash)
277
+ ) {
278
+ throw new Error(`${label}.artifact.contentHash is invalid`);
279
+ }
280
+ if (
281
+ !Number.isSafeInteger(artifact.size) ||
282
+ (artifact.size as number) < 0 ||
283
+ (artifact.size as number) > 256 * 1024 ||
284
+ artifact.mediaType !== "text/html"
285
+ ) {
286
+ throw new Error(`${label}.artifact metadata is invalid`);
287
+ }
288
+ if (
289
+ !Array.isArray(contribution.mounts) ||
290
+ contribution.mounts.length === 0 ||
291
+ contribution.mounts.length > 64
292
+ ) {
293
+ throw new Error(`${label}.mounts must be a non-empty bounded array`);
294
+ }
295
+ const mounts = contribution.mounts.map((candidateMount, mountIndex) => {
296
+ const mount = record(candidateMount, `${label}.mounts[${mountIndex}]`);
297
+ const hasOrder = mount.order !== undefined;
298
+ exact(
299
+ mount,
300
+ hasOrder ? ["slot", "order"] : ["slot"],
301
+ `${label}.mounts[${mountIndex}]`,
302
+ );
303
+ if (
304
+ hasOrder &&
305
+ (typeof mount.order !== "number" || !Number.isFinite(mount.order))
306
+ ) {
307
+ throw new Error(`${label}.mounts[${mountIndex}].order is invalid`);
308
+ }
309
+ return {
310
+ slot: boundedString(
311
+ mount.slot,
312
+ `${label}.mounts[${mountIndex}].slot`,
313
+ 160,
314
+ ),
315
+ ...(hasOrder ? { order: mount.order as number } : {}),
316
+ };
317
+ });
318
+ if (
319
+ !Array.isArray(contribution.declaredTools) ||
320
+ contribution.declaredTools.length > 64
321
+ ) {
322
+ throw new Error(`${label}.declaredTools must be a bounded array`);
323
+ }
324
+ const declaredTools = contribution.declaredTools.map((tool, toolIndex) =>
325
+ boundedString(tool, `${label}.declaredTools[${toolIndex}]`, 64),
326
+ );
327
+ for (const mount of mounts) {
328
+ const prefix = "frockbot.tool-result:";
329
+ if (mount.slot === "frockbot.bot-settings-sections") continue;
330
+ if (
331
+ !mount.slot.startsWith(prefix) ||
332
+ !declaredTools.includes(mount.slot.slice(prefix.length))
333
+ ) {
334
+ throw new Error(`${label}.mounts contains an unsafe slot`);
335
+ }
336
+ }
337
+ if (
338
+ contribution.provenance !== "Bot-authored" &&
339
+ contribution.provenance !== "User-installed"
340
+ ) {
341
+ throw new Error(`${label}.provenance is invalid`);
342
+ }
343
+ return {
344
+ packageId: boundedString(
345
+ contribution.packageId,
346
+ `${label}.packageId`,
347
+ 64,
348
+ ),
349
+ displayName: boundedString(
350
+ contribution.displayName,
351
+ `${label}.displayName`,
352
+ 128,
353
+ ),
354
+ provenance: contribution.provenance as "Bot-authored" | "User-installed",
355
+ artifact: {
356
+ contentHash: artifact.contentHash,
357
+ size: artifact.size as number,
358
+ mediaType: "text/html" as const,
359
+ bundlerVersion: boundedString(
360
+ artifact.bundlerVersion,
361
+ `${label}.artifact.bundlerVersion`,
362
+ 128,
363
+ ),
364
+ },
365
+ mounts,
366
+ declaredTools,
367
+ };
368
+ });
369
+ if (
370
+ new Set(contributions.map((contribution) => contribution.packageId))
371
+ .size !== contributions.length
372
+ ) {
373
+ throw new Error("Package iframe catalog contains duplicate Packages");
374
+ }
375
+ return {
376
+ schemaVersion: 1,
377
+ botId: boundedString(value.botId, "Package iframe botId"),
378
+ generationId: boundedString(
379
+ value.generationId,
380
+ "Package iframe generationId",
381
+ ),
382
+ artifactOrigin,
383
+ contributions,
384
+ };
385
+ }
386
+
387
+ /** TypeScript contract shown by package_inspect_self. */
388
+ export const PACKAGE_IFRAME_BRIDGE_DTS_V1 = `
389
+ interface FrockBotIframeBridgeV1 {
390
+ readonly ready: Promise<{ themeTokens: Record<string, string>; packageId: string; botId: string; slot: string }>;
391
+ callTool(name: string, input: unknown): void;
392
+ subscribe(name: string, listener: (value: unknown) => void): () => void;
393
+ resize(height?: number): void;
394
+ }
395
+ declare global { interface Window { frockbot: FrockBotIframeBridgeV1 } }
396
+ // Messages are schemaVersion: 1. Results arrive on state name tool:<name>.
397
+ `;
398
+
399
+ /** Tiny inline helper authored pages may paste verbatim. */
400
+ export const PACKAGE_IFRAME_HELPER_JS_V1 = `(()=>{const V=1,L=new Map(),obj=v=>v&&typeof v==='object'&&!Array.isArray(v),exact=(v,ks)=>obj(v)&&Object.keys(v).length===ks.length&&ks.every(k=>Object.prototype.hasOwnProperty.call(v,k)),str=(v,n)=>typeof v==='string'&&v.length>0&&v.length<=n,json=(v,d=0)=>d<=16&&(v===null||typeof v==='string'||typeof v==='boolean'||typeof v==='number'&&Number.isFinite(v)||Array.isArray(v)&&v.length<=256&&v.every(x=>json(x,d+1))||obj(v)&&Object.keys(v).length<=256&&Object.values(v).every(x=>json(x,d+1))),wire=v=>{try{return new TextEncoder().encode(JSON.stringify(v)).byteLength<=65536}catch{return false}};let ok,fail;const ready=new Promise((r,j)=>{ok=r;fail=j});addEventListener('message',e=>{if(e.source!==parent)return;const m=e.data;if(m?.schemaVersion!==V||!wire(m))return;if(m.type==='init'&&exact(m,['schemaVersion','type','themeTokens','packageId','botId','slot'])&&obj(m.themeTokens)&&Object.keys(m.themeTokens).length<=64&&Object.values(m.themeTokens).every(v=>typeof v==='string')&&str(m.packageId,64)&&str(m.botId,256)&&str(m.slot,160)){for(const [k,v] of Object.entries(m.themeTokens))document.documentElement.style.setProperty('--frockbot-'+k,v);ok({themeTokens:m.themeTokens,packageId:m.packageId,botId:m.botId,slot:m.slot});return}if(m.type==='state'&&exact(m,['schemaVersion','type','name','value'])&&str(m.name,256)&&json(m.value))for(const fn of L.get(m.name)||[])fn(m.value)});window.frockbot={ready,callTool(name,input){parent.postMessage({schemaVersion:V,type:'callTool',name,input},'*')},subscribe(name,fn){const s=L.get(name)||new Set();s.add(fn);L.set(name,s);return()=>s.delete(fn)},resize(height=document.documentElement.scrollHeight){parent.postMessage({schemaVersion:V,type:'resize',height},'*')}};setTimeout(()=>fail(new Error('FrockBot iframe init timed out')),10000)})();`;
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export * from "./authoring.js";
2
+ export * from "./iframe-ui.js";
2
3
  export * from "./isolate.js";
4
+ export * from "./isolate-context-catalog.generated.js";
5
+ export * from "./loop-events.js";
3
6
  export * from "./model-invocation.js";
4
7
  export * from "./prompt-assembly.js";
5
8
  export * from "./send-to-user.js";