@paged-media/plugin-sdk 0.2.6-canary.0 → 0.2.9-canary.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +504 -32
  2. package/dist/index.js +1338 -39
  3. package/package.json +14 -2
package/dist/index.js CHANGED
@@ -3,13 +3,6 @@ function defineBundle(bundle) {
3
3
  return bundle;
4
4
  }
5
5
 
6
- // src/harness.ts
7
- function createHeadlessHost(_options) {
8
- throw new Error(
9
- "@paged-media/plugin-sdk: the headless harness is not implemented yet (tracked in plugin-draw/BREAKAGE_LOG.md). Run bundles inside the editor app, or unit-test their machines (pure, host-free) instead."
10
- );
11
- }
12
-
13
6
  // src/disposables.ts
14
7
  var DisposableStore = class {
15
8
  items = [];
@@ -49,43 +42,88 @@ function toDisposable(fn) {
49
42
  };
50
43
  }
51
44
 
52
- // src/version.ts
53
- var API_VERSION = "0.2.0";
54
- function parse(v) {
55
- const m = /^(\d+)\.(\d+)(?:\.(\d+))?$/.exec(v.trim());
56
- if (!m) return null;
57
- return [Number(m[1]), Number(m[2]), Number(m[3] ?? "0")];
58
- }
59
- function satisfiesApiVersion(range, version = API_VERSION) {
60
- const r = range.trim();
61
- if (r === "*") return true;
62
- const v = parse(version);
63
- if (!v) return false;
64
- if (r.startsWith("^")) {
65
- const base = parse(r.slice(1));
66
- if (!base) return false;
67
- if (base[0] > 0) {
68
- if (v[0] !== base[0]) return false;
69
- if (v[1] !== base[1]) return v[1] > base[1];
70
- return v[2] >= base[2];
45
+ // src/widgets-fallback.tsx
46
+ import { createElement } from "react";
47
+ function TextareaCodeEditor(props) {
48
+ return createElement("textarea", {
49
+ value: props.value,
50
+ readOnly: props.readOnly,
51
+ spellCheck: false,
52
+ "aria-label": props.ariaLabel,
53
+ "data-code-editor-fallback": props.language ?? "text",
54
+ onChange: (e) => props.onChange(e.target.value),
55
+ style: {
56
+ width: "100%",
57
+ minHeight: props.minHeight ?? 96,
58
+ resize: "vertical",
59
+ font: "12px/1.5 var(--font-mono, monospace)",
60
+ color: "var(--pg-fg)",
61
+ background: "var(--pg-bg)",
62
+ border: "1px solid var(--pg-border)",
63
+ borderRadius: "var(--radius-sm, 4px)",
64
+ padding: "var(--space-2, 8px)",
65
+ boxSizing: "border-box"
71
66
  }
72
- return v[0] === 0 && v[1] === base[1] && v[2] >= base[2];
67
+ });
68
+ }
69
+ var FALLBACK_WIDGETS = {
70
+ CodeEditor: TextareaCodeEditor
71
+ };
72
+
73
+ // src/schema-panel.tsx
74
+ import { createElement as createElement2 } from "react";
75
+ function resolveGate(gate, lookup) {
76
+ if (gate === void 0) return true;
77
+ if (typeof gate === "boolean") return gate;
78
+ const raw = Boolean(lookup(gate.bind));
79
+ return gate.negate ? !raw : raw;
80
+ }
81
+ function makeSchemaPanelComponent(contribution, bindings, renderer) {
82
+ const { schema } = contribution;
83
+ if (renderer) {
84
+ let SchemaPanel2 = function(_props) {
85
+ return createElement2(Renderer, { schema, bindings });
86
+ };
87
+ var SchemaPanel = SchemaPanel2;
88
+ const Renderer = renderer;
89
+ SchemaPanel2.displayName = `SchemaPanel(${schema.id})`;
90
+ return SchemaPanel2;
73
91
  }
74
- const exact = parse(r);
75
- if (!exact) return false;
76
- return v[0] === exact[0] && v[1] === exact[1] && v[2] === exact[2];
92
+ function SchemaPanelSeam(_props) {
93
+ return createElement2(
94
+ "div",
95
+ {
96
+ "data-schema-panel-seam": schema.id,
97
+ style: {
98
+ padding: "var(--space-3, 12px)",
99
+ font: "12px/1.5 var(--font-sans, sans-serif)",
100
+ color: "var(--pg-muted-fg)"
101
+ }
102
+ },
103
+ `Schema panel "${schema.title}" needs a host renderer (supports("schemaPanel.renderer@1") is false).`
104
+ );
105
+ }
106
+ SchemaPanelSeam.displayName = `SchemaPanelSeam(${schema.id})`;
107
+ return SchemaPanelSeam;
77
108
  }
78
109
 
79
110
  // src/host-impl.ts
80
111
  var HOST_FEATURES = [
81
112
  "contribute.tool@1",
82
113
  "contribute.panel@1",
114
+ "contribute.schemaPanel@1",
115
+ "bindings@1",
83
116
  "contribute.command@1",
84
117
  "contribute.keybinding@1",
85
118
  "contribute.overlay@1",
119
+ "contribute.editContext@1",
120
+ "contribute.objectType@1",
121
+ "contribute.importer@1",
122
+ "contribute.exporter@1",
86
123
  "document.mutate@1",
87
124
  "document.undo@1",
88
125
  "document.collection@1",
126
+ "document.frameChain@1",
89
127
  "document.meta@1",
90
128
  "document.pathAnchors@1",
91
129
  "document.hitTest@1",
@@ -98,7 +136,12 @@ var HOST_FEATURES = [
98
136
  "viewport@1",
99
137
  "overlay.toolPreview@1",
100
138
  "storage@1",
101
- "diagnostics@1"
139
+ "diagnostics@1",
140
+ // C-5 / I-04 (core v42): the placed-image bytes read is engine-served
141
+ // through the wire (no injected source), so it is unconditionally
142
+ // implemented at the pinned canvas-wasm — unlike assets.fonts@1, which
143
+ // stays conditional on the editor's injected byte source.
144
+ "assets.images@1"
102
145
  ];
103
146
  var PluginApiNotImplemented = class extends Error {
104
147
  constructor(member, pointer) {
@@ -108,6 +151,21 @@ var PluginApiNotImplemented = class extends Error {
108
151
  this.name = "PluginApiNotImplemented";
109
152
  }
110
153
  };
154
+ var PluginCapabilityError = class extends Error {
155
+ /** The host door that was called (e.g. `"contribute.tool"`). */
156
+ door;
157
+ /** The manifest declaration that would authorize it (e.g.
158
+ * `'contributes.tools[] must include "media.paged.web.tool.pen"'`). */
159
+ missingDeclaration;
160
+ constructor(door, missingDeclaration, pluginId) {
161
+ super(
162
+ `plugin-api: ${pluginId} called ${door} without declaring it \u2014 ${missingDeclaration}. Manifest capabilities are ENFORCED (trust-line W0.11): declare the use or the host refuses it.`
163
+ );
164
+ this.name = "PluginCapabilityError";
165
+ this.door = door;
166
+ this.missingDeclaration = missingDeclaration;
167
+ }
168
+ };
111
169
  function defaultStorageBacking() {
112
170
  const ls = globalThis.localStorage;
113
171
  if (ls) {
@@ -133,6 +191,59 @@ function defaultStorageBacking() {
133
191
  keys: () => Array.from(map.keys())
134
192
  };
135
193
  }
194
+ var ASSET_BUDGETS = {
195
+ /** Largest font face the door will serve, in bytes (8 MiB). */
196
+ maxFontFaceBytes: 8 * 1024 * 1024
197
+ };
198
+ var BLOB_BUDGETS = {
199
+ /** Default per-plugin blob ceiling, in bytes (64 MiB). */
200
+ defaultQuotaBytes: 64 * 1024 * 1024
201
+ };
202
+ function createDataProviderRegistry() {
203
+ const providers = /* @__PURE__ */ new Map();
204
+ const listeners = /* @__PURE__ */ new Map();
205
+ return {
206
+ register(registration) {
207
+ providers.set(registration.id, {
208
+ reg: registration,
209
+ revision: registration.revision
210
+ });
211
+ return {
212
+ update(revision) {
213
+ const entry = providers.get(registration.id);
214
+ if (entry) entry.revision = revision;
215
+ for (const l of listeners.get(registration.id) ?? []) l(revision);
216
+ },
217
+ dispose() {
218
+ providers.delete(registration.id);
219
+ }
220
+ };
221
+ },
222
+ discover(category) {
223
+ return [...providers.values()].filter((e) => category === void 0 || e.reg.category === category).map((e) => ({
224
+ id: e.reg.id,
225
+ category: e.reg.category,
226
+ schema: e.reg.schema,
227
+ revision: e.revision
228
+ }));
229
+ },
230
+ async get(id) {
231
+ const entry = providers.get(id);
232
+ if (!entry) return null;
233
+ const records = await entry.reg.getSnapshot();
234
+ return { id, revision: entry.revision, records };
235
+ },
236
+ onDidChange(id, listener) {
237
+ let set = listeners.get(id);
238
+ if (!set) {
239
+ set = /* @__PURE__ */ new Set();
240
+ listeners.set(id, set);
241
+ }
242
+ set.add(listener);
243
+ return toDisposable(() => set.delete(listener));
244
+ }
245
+ };
246
+ }
136
247
  function createBundleHost(getEditor, manifest, options) {
137
248
  const store = new DisposableStore();
138
249
  const sink = options?.console ?? console;
@@ -152,37 +263,243 @@ function createBundleHost(getEditor, manifest, options) {
152
263
  warn: (m, ...a) => sink.warn(`${tag} ${m}`, ...a),
153
264
  error: (m, ...a) => sink.error(`${tag} ${m}`, ...a)
154
265
  };
266
+ const capabilityMode = options?.capabilityMode ?? "enforce";
267
+ const caps = manifest.capabilities;
268
+ const declared = manifest.contributes;
269
+ const hasDoc = (dir) => caps?.document?.[dir] !== void 0;
270
+ const hasRendering = (s) => caps?.rendering?.includes(s) ?? false;
271
+ const hasAsset = (k) => caps?.assets?.includes(k) ?? false;
272
+ const hasBlobStore = () => caps?.storage?.blob === true;
273
+ const lists = (arr, id) => arr?.includes(id) ?? false;
274
+ const declaresType = (arr, type) => arr?.some((e) => e.type === type) ?? false;
275
+ const requireDeclared = (ok, door, missing) => {
276
+ if (ok) return;
277
+ if (capabilityMode === "warn") {
278
+ log.warn(
279
+ `${door} used without declaring it \u2014 ${missing} (capabilityMode: 'warn'; would refuse in 'enforce')`
280
+ );
281
+ return;
282
+ }
283
+ throw new PluginCapabilityError(door, missing, manifest.id);
284
+ };
285
+ const denyWrite = (ok, door, missing) => {
286
+ if (ok) return null;
287
+ const reason = `${door} requires ${missing} (trust-line W0.11)`;
288
+ if (capabilityMode === "warn") {
289
+ log.warn(`${reason} \u2014 proceeding (capabilityMode: 'warn')`);
290
+ return null;
291
+ }
292
+ return reason;
293
+ };
155
294
  const contribute = {
156
295
  tool(c) {
157
296
  assertNamespaced(c.id, "tool");
297
+ requireDeclared(
298
+ lists(declared?.tools, c.id),
299
+ "contribute.tool",
300
+ `contributes.tools[] must include "${c.id}"`
301
+ );
158
302
  return store.add(getEditor().registries.tools.register(c));
159
303
  },
160
304
  panel(c) {
161
305
  assertNamespaced(c.id, "panel");
306
+ requireDeclared(
307
+ lists(declared?.panels, c.id),
308
+ "contribute.panel",
309
+ `contributes.panels[] must include "${c.id}"`
310
+ );
162
311
  return store.add(getEditor().registries.panels.register(c));
163
312
  },
313
+ schemaPanel(c) {
314
+ assertNamespaced(c.id, "schemaPanel");
315
+ requireDeclared(
316
+ lists(declared?.panels, c.id),
317
+ "contribute.schemaPanel",
318
+ `contributes.panels[] must include "${c.id}"`
319
+ );
320
+ const panel = {
321
+ id: c.id,
322
+ title: c.title,
323
+ icon: c.icon,
324
+ defaultDock: c.defaultDock,
325
+ defaultGroup: c.defaultGroup,
326
+ closable: c.closable,
327
+ movable: c.movable,
328
+ component: makeSchemaPanelComponent(
329
+ c,
330
+ bindings,
331
+ options?.schemaPanelRenderer
332
+ )
333
+ };
334
+ const reg = store.add(getEditor().registries.panels.register(panel));
335
+ const recorded = options?.onSchemaPanelRegistered?.(c);
336
+ if (recorded) {
337
+ const d = store.add(recorded);
338
+ return toDisposable(() => {
339
+ reg.dispose();
340
+ d.dispose();
341
+ });
342
+ }
343
+ return reg;
344
+ },
164
345
  command(c) {
165
346
  assertNamespaced(c.id, "command");
347
+ requireDeclared(
348
+ lists(declared?.commands, c.id),
349
+ "contribute.command",
350
+ `contributes.commands[] must include "${c.id}"`
351
+ );
166
352
  return store.add(getEditor().registries.commands.register(c));
167
353
  },
168
354
  keybinding(c) {
355
+ requireDeclared(
356
+ caps?.keybindings === true,
357
+ "contribute.keybinding",
358
+ "capabilities.keybindings must be true"
359
+ );
169
360
  return store.add(getEditor().registries.keybindings.register(c));
170
361
  },
171
362
  overlay(c) {
172
363
  assertNamespaced(c.id, "overlay");
364
+ requireDeclared(
365
+ hasRendering("overlay"),
366
+ "contribute.overlay",
367
+ 'capabilities.rendering must include "overlay"'
368
+ );
173
369
  return store.add(getEditor().registries.overlays.register(c));
174
370
  },
175
- editContext() {
176
- throw new PluginApiNotImplemented(
371
+ // W3.2 (un-reserved — B-02 / W-03): the last two reserved doors. The
372
+ // capability gate keys off the OBJECT arrays in `contributes`
373
+ // (`editContexts[]` / `objectTypes[]` carry `{type,…}`, not flat
374
+ // ids). The shell owns the stack / chrome / write-scope; the SDK
375
+ // adapter just hands the contribution to the editor's registry (or,
376
+ // when the host hasn't wired one — headless / not-yet-adopted —
377
+ // records it through the harness hook). The `type` is a content-type
378
+ // name, NOT a namespaced id, so the namespace rule does NOT apply
379
+ // (the capability gate is the only gate).
380
+ editContext(c) {
381
+ requireDeclared(
382
+ declaresType(declared?.editContexts, c.type),
177
383
  "contribute.editContext",
178
- "P0 shell work \u2014 plugin-draw/BREAKAGE_LOG.md B-02"
384
+ `contributes.editContexts[] must declare { type: "${c.type}" }`
179
385
  );
386
+ const stamped = {
387
+ ...c,
388
+ metadataKey: metadataKey(manifest)
389
+ };
390
+ const reg = getEditor().registries.editContexts;
391
+ const recorded = options?.onEditContextRegistered?.(stamped);
392
+ if (reg) {
393
+ const d = store.add(reg.register(stamped));
394
+ if (recorded) {
395
+ const r = store.add(recorded);
396
+ return toDisposable(() => {
397
+ d.dispose();
398
+ r.dispose();
399
+ });
400
+ }
401
+ return d;
402
+ }
403
+ if (recorded) return store.add(recorded);
404
+ return store.add(toDisposable(() => {
405
+ }));
180
406
  },
181
- objectType() {
182
- throw new PluginApiNotImplemented(
407
+ objectType(c) {
408
+ requireDeclared(
409
+ declaresType(declared?.objectTypes, c.type),
183
410
  "contribute.objectType",
184
- "paged.web W1 \u2014 base-idea \xA79.1.2"
411
+ `contributes.objectTypes[] must declare { type: "${c.type}" }`
185
412
  );
413
+ const stamped = {
414
+ ...c,
415
+ metadataKey: metadataKey(manifest)
416
+ };
417
+ const reg = getEditor().registries.objectTypes;
418
+ const recorded = options?.onObjectTypeRegistered?.(stamped);
419
+ if (reg) {
420
+ const d = store.add(reg.register(stamped));
421
+ if (recorded) {
422
+ const r = store.add(recorded);
423
+ return toDisposable(() => {
424
+ d.dispose();
425
+ r.dispose();
426
+ });
427
+ }
428
+ return d;
429
+ }
430
+ if (recorded) return store.add(recorded);
431
+ return store.add(toDisposable(() => {
432
+ }));
433
+ },
434
+ // K-2 / S-06 — document IO. Mirrors `command`: namespaced id the
435
+ // manifest must list, routed to the shell registry. The registry is
436
+ // OPTIONAL on the handle (a host that hasn't wired it stays
437
+ // assignable) — when absent the door is a tracked no-op; the headless
438
+ // harness injects a recording registry so conformance can assert it.
439
+ importer(c) {
440
+ assertNamespaced(c.id, "importer");
441
+ requireDeclared(
442
+ lists(declared?.importers, c.id),
443
+ "contribute.importer",
444
+ `contributes.importers[] must include "${c.id}"`
445
+ );
446
+ const reg = getEditor().registries.importers;
447
+ if (reg) return store.add(reg.register(c));
448
+ return store.add(toDisposable(() => {
449
+ }));
450
+ },
451
+ exporter(c) {
452
+ assertNamespaced(c.id, "exporter");
453
+ requireDeclared(
454
+ lists(declared?.exporters, c.id),
455
+ "contribute.exporter",
456
+ `contributes.exporters[] must include "${c.id}"`
457
+ );
458
+ const reg = getEditor().registries.exporters;
459
+ if (reg) return store.add(reg.register(c));
460
+ return store.add(toDisposable(() => {
461
+ }));
462
+ },
463
+ // C-1 — the in-frame scene-layer surface. Capability-gated on
464
+ // `rendering ∋ sceneLayer`; routes submit/clear to the editor's scene
465
+ // channel (`getEditor().sceneLayers` → canvas-wasm submit/clear). When
466
+ // no channel is wired (headless / older editor) the surface warns +
467
+ // no-ops (probe `supports("rendering.sceneLayer@1")`). Disposing the
468
+ // surface clears every layer it submitted (tracked so host.dispose()
469
+ // tears them down).
470
+ sceneLayer() {
471
+ requireDeclared(
472
+ hasRendering("sceneLayer"),
473
+ "contribute.sceneLayer",
474
+ 'capabilities.rendering must include "sceneLayer"'
475
+ );
476
+ const submitted = /* @__PURE__ */ new Set();
477
+ const channel = () => getEditor().sceneLayers;
478
+ const surface = {
479
+ async submit(elementId, layer) {
480
+ const ch = channel();
481
+ if (!ch) {
482
+ log.warn(
483
+ `contribute.sceneLayer().submit("${elementId}") ignored \u2014 the host wired no scene channel (probe supports("rendering.sceneLayer@1"))`
484
+ );
485
+ return;
486
+ }
487
+ submitted.add(elementId);
488
+ await ch.submit(elementId, layer);
489
+ },
490
+ async clear(elementId) {
491
+ submitted.delete(elementId);
492
+ await channel()?.clear(elementId);
493
+ },
494
+ dispose() {
495
+ const ch = channel();
496
+ if (ch) {
497
+ for (const id of submitted) void ch.clear(id);
498
+ }
499
+ submitted.clear();
500
+ }
501
+ };
502
+ return store.add(surface);
186
503
  }
187
504
  };
188
505
  const foreignMetadataKey = (m) => {
@@ -197,8 +514,19 @@ function createBundleHost(getEditor, manifest, options) {
197
514
  }
198
515
  return null;
199
516
  };
517
+ const requireDocRead = (door) => requireDeclared(
518
+ hasDoc("read"),
519
+ door,
520
+ "capabilities.document.read must be declared"
521
+ );
200
522
  const document = {
201
523
  async mutate(mutation) {
524
+ const denied = denyWrite(
525
+ hasDoc("write"),
526
+ "document.mutate",
527
+ "capabilities.document.write"
528
+ );
529
+ if (denied !== null) return { applied: false, error: denied };
202
530
  const foreign = foreignMetadataKey(mutation);
203
531
  if (foreign !== null) {
204
532
  const error = `setPluginMetadata key "${foreign}" is outside this plugin's namespace ("${metadataKey(manifest)}")`;
@@ -223,21 +551,40 @@ function createBundleHost(getEditor, manifest, options) {
223
551
  }
224
552
  },
225
553
  async undo() {
554
+ requireDeclared(
555
+ hasDoc("write"),
556
+ "document.undo",
557
+ "capabilities.document.write"
558
+ );
226
559
  await getEditor().client.undo();
227
560
  },
228
561
  async redo() {
562
+ requireDeclared(
563
+ hasDoc("write"),
564
+ "document.redo",
565
+ "capabilities.document.write"
566
+ );
229
567
  await getEditor().client.redo();
230
568
  },
231
569
  collection(name) {
570
+ requireDocRead("document.collection");
232
571
  return getEditor().client.collection(name);
233
572
  },
234
573
  meta() {
574
+ requireDocRead("document.meta");
235
575
  return getEditor().client.documentMeta();
236
576
  },
237
577
  pathAnchors(id) {
578
+ requireDocRead("document.pathAnchors");
238
579
  return getEditor().client.pathAnchors(id).catch(() => null);
239
580
  },
240
581
  async hitTest(pageId, point, filter = "any") {
582
+ requireDocRead("document.hitTest");
583
+ requireDeclared(
584
+ hasRendering("hitTest"),
585
+ "document.hitTest",
586
+ 'capabilities.rendering must include "hitTest"'
587
+ );
241
588
  try {
242
589
  const reply = await getEditor().client.send({
243
590
  kind: "hitTest",
@@ -249,15 +596,18 @@ function createBundleHost(getEditor, manifest, options) {
249
596
  }
250
597
  },
251
598
  elementGeometry(ids) {
599
+ requireDocRead("document.elementGeometry");
252
600
  return getEditor().client.elementGeometry(ids);
253
601
  },
254
602
  async tree() {
603
+ requireDocRead("document.tree");
255
604
  const reply = await getEditor().client.send({
256
605
  kind: "requestSceneTree"
257
606
  });
258
607
  return reply.kind === "sceneTree" ? reply.payload.roots : [];
259
608
  },
260
609
  async getMetadata(id) {
610
+ requireDocRead("document.getMetadata");
261
611
  const key = metadataKey(manifest);
262
612
  const reply = await getEditor().client.send({
263
613
  kind: "requestElementProperties",
@@ -284,14 +634,29 @@ function createBundleHost(getEditor, manifest, options) {
284
634
  args: {
285
635
  elementId: id,
286
636
  key: metadataKey(manifest),
287
- value: envelope === null ? null : JSON.stringify(envelope)
637
+ value: envelope === null ? null : JSON.stringify(envelope),
638
+ caller: manifest.id
288
639
  }
289
640
  });
290
641
  },
642
+ async frameChain(storyId) {
643
+ requireDocRead("document.frameChain");
644
+ const reply = await getEditor().client.send({
645
+ kind: "requestFrameChain",
646
+ payload: { storyId }
647
+ });
648
+ return reply.kind === "frameChainResult" ? reply.payload.links : [];
649
+ },
291
650
  onDidChange(listener) {
651
+ requireDocRead("document.onDidChange");
292
652
  const off = getEditor().client.subscribe((msg) => {
293
653
  if (msg.kind === "mutationApplied" || msg.kind === "undoApplied" || msg.kind === "redoApplied") {
294
- listener({ kind: msg.kind, pageIds: msg.payload.pageIds });
654
+ const reflow = msg.kind === "mutationApplied" ? msg.payload.reflow : void 0;
655
+ listener({
656
+ kind: msg.kind,
657
+ pageIds: msg.payload.pageIds,
658
+ ...reflow ? { reflow: { frameId: reflow.frameId, contentBox: reflow.contentBox } } : {}
659
+ });
295
660
  }
296
661
  });
297
662
  return store.add(toDisposable(off));
@@ -302,6 +667,11 @@ function createBundleHost(getEditor, manifest, options) {
302
667
  return getEditor().selection.elementSelection;
303
668
  },
304
669
  async set(ids, mode = "replace") {
670
+ requireDeclared(
671
+ hasDoc("write"),
672
+ "selection.set",
673
+ "capabilities.document.write"
674
+ );
305
675
  const editor = getEditor();
306
676
  const applied = await editor.client.setElementSelection(ids, mode);
307
677
  editor.selection.setElementSelection(applied);
@@ -326,8 +696,26 @@ function createBundleHost(getEditor, manifest, options) {
326
696
  return px / (scale > 0 ? scale : 1);
327
697
  }
328
698
  };
699
+ const text = {
700
+ async measureString(family, style, str, sizePt) {
701
+ const editorText = getEditor().text;
702
+ if (editorText) {
703
+ return editorText.measure(family, style, str, sizePt);
704
+ }
705
+ return {
706
+ advance: str.length * sizePt * 0.5,
707
+ ascender: sizePt * 0.8,
708
+ descender: -sizePt * 0.2
709
+ };
710
+ }
711
+ };
329
712
  const overlay = {
330
713
  setToolPreview(shape) {
714
+ requireDeclared(
715
+ hasRendering("overlay"),
716
+ "overlay.setToolPreview",
717
+ 'capabilities.rendering must include "overlay"'
718
+ );
331
719
  getEditor().overlaySignals.setToolPreview(shape);
332
720
  }
333
721
  };
@@ -353,6 +741,98 @@ function createBundleHost(getEditor, manifest, options) {
353
741
  return backing.keys().filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
354
742
  }
355
743
  };
744
+ const declaredNetwork = manifest.capabilities?.network;
745
+ const networkDeclared = declaredNetwork === true || typeof declaredNetwork === "object" && declaredNetwork !== null;
746
+ const mayRequest = (origin) => {
747
+ if (declaredNetwork === true) return true;
748
+ if (typeof declaredNetwork === "object" && declaredNetwork !== null) {
749
+ const o = declaredNetwork.origins;
750
+ return o === "consent" || Array.isArray(o) && o.includes(origin);
751
+ }
752
+ return false;
753
+ };
754
+ const CONSENT_KEY = "network.consentedOrigins";
755
+ const granted = new Set(storage.get(CONSENT_KEY) ?? []);
756
+ const network = {
757
+ async requestConsent(origins, purpose) {
758
+ requireDeclared(
759
+ networkDeclared,
760
+ "network.requestConsent",
761
+ "capabilities.network must declare the network capability (boolean or { origins })"
762
+ );
763
+ const inScope = origins.filter(mayRequest);
764
+ const outOfScope = origins.filter((o) => !mayRequest(o));
765
+ if (outOfScope.length > 0) {
766
+ log.warn(
767
+ `network.requestConsent: ${outOfScope.length} origin(s) outside the declared capabilities.network allow-list \u2014 denied: ${outOfScope.join(", ")}`
768
+ );
769
+ }
770
+ const need = inScope.filter((o) => !granted.has(o));
771
+ let prompted = { granted: [], denied: [], remembered: false };
772
+ if (need.length > 0) {
773
+ if (options?.consent) {
774
+ prompted = await options.consent.request(need, purpose);
775
+ } else {
776
+ log.warn(
777
+ "network.requestConsent: no consent backend wired \u2014 denying (supports('network.consent@1') is false; the editor injects one)"
778
+ );
779
+ prompted = { granted: [], denied: need, remembered: false };
780
+ }
781
+ }
782
+ for (const o of prompted.granted) granted.add(o);
783
+ if (prompted.remembered) storage.set(CONSENT_KEY, [...granted]);
784
+ return {
785
+ granted: inScope.filter((o) => granted.has(o)),
786
+ denied: [...outOfScope, ...inScope.filter((o) => !granted.has(o))],
787
+ remembered: prompted.remembered
788
+ };
789
+ },
790
+ consentedOrigins() {
791
+ return [...granted];
792
+ }
793
+ };
794
+ const dpCap = manifest.capabilities?.dataProviders;
795
+ const mayPublish = (category) => Array.isArray(dpCap?.publish) && dpCap.publish.includes(category);
796
+ const mayConsume = (category) => Array.isArray(dpCap?.consume) && (category === void 0 || dpCap.consume.includes(category));
797
+ const consumeDeclared = Array.isArray(dpCap?.consume) && dpCap.consume.length > 0;
798
+ const dataProviders = {
799
+ register(registration) {
800
+ requireDeclared(
801
+ mayPublish(registration.category),
802
+ "dataProviders.register",
803
+ `capabilities.dataProviders.publish must include "${registration.category}"`
804
+ );
805
+ if (!options?.dataProviders) {
806
+ log.warn(
807
+ `dataProviders.register("${registration.id}") \u2014 no shared registry wired (supports('dataProviders@1') is false; the editor injects one)`
808
+ );
809
+ return { update() {
810
+ }, dispose() {
811
+ } };
812
+ }
813
+ return options.dataProviders.register(registration);
814
+ },
815
+ discover(category) {
816
+ requireDeclared(
817
+ mayConsume(category),
818
+ "dataProviders.discover",
819
+ "capabilities.dataProviders.consume must include the category"
820
+ );
821
+ return options?.dataProviders?.discover(category) ?? [];
822
+ },
823
+ async get(id) {
824
+ requireDeclared(
825
+ consumeDeclared,
826
+ "dataProviders.get",
827
+ "capabilities.dataProviders.consume must be declared"
828
+ );
829
+ return await options?.dataProviders?.get(id) ?? null;
830
+ },
831
+ onDidChange(id, listener) {
832
+ if (!options?.dataProviders) return toDisposable(() => void 0);
833
+ return store.add(options.dataProviders.onDidChange(id, listener));
834
+ }
835
+ };
356
836
  const diagnosticStore = /* @__PURE__ */ new Map();
357
837
  const diagnosticListeners = /* @__PURE__ */ new Set();
358
838
  const emitDiagnostics = (key) => {
@@ -367,11 +847,13 @@ function createBundleHost(getEditor, manifest, options) {
367
847
  else if (d.severity === "warning") sink.warn(line);
368
848
  else sink.info(line);
369
849
  }
850
+ options?.diagnosticsSink?.publish(manifest.id, key, items);
370
851
  emitDiagnostics(key);
371
852
  },
372
853
  clear(key) {
373
854
  if (key !== void 0) diagnosticStore.delete(key);
374
855
  else diagnosticStore.clear();
856
+ options?.diagnosticsSink?.clear(manifest.id, key);
375
857
  emitDiagnostics(key ?? "");
376
858
  },
377
859
  get(key) {
@@ -382,6 +864,27 @@ function createBundleHost(getEditor, manifest, options) {
382
864
  return store.add(toDisposable(() => diagnosticListeners.delete(listener)));
383
865
  }
384
866
  };
867
+ const bindingStore = /* @__PURE__ */ new Map();
868
+ const bindingListeners = /* @__PURE__ */ new Set();
869
+ const emitBinding = (name) => {
870
+ for (const l of bindingListeners) l(name);
871
+ };
872
+ const bindings = {
873
+ publish(name, value) {
874
+ bindingStore.set(name, value);
875
+ emitBinding(name);
876
+ },
877
+ get(name) {
878
+ return bindingStore.get(name);
879
+ },
880
+ delete(name) {
881
+ if (bindingStore.delete(name)) emitBinding(name);
882
+ },
883
+ onDidChange(listener) {
884
+ bindingListeners.add(listener);
885
+ return store.add(toDisposable(() => bindingListeners.delete(listener)));
886
+ }
887
+ };
385
888
  const shell = options?.shell ?? {
386
889
  openPanel(panelId) {
387
890
  log.warn(
@@ -389,11 +892,151 @@ function createBundleHost(getEditor, manifest, options) {
389
892
  );
390
893
  },
391
894
  closePanel() {
895
+ },
896
+ async pickFile() {
897
+ log.warn(
898
+ `shell.pickFile() ignored \u2014 the host app provided no shell actions (probe with supports("shell.pickFile@1"))`
899
+ );
900
+ return [];
901
+ }
902
+ };
903
+ const widgets = options?.widgets ?? FALLBACK_WIDGETS;
904
+ const assetSource = options?.assetSource;
905
+ const assets = {
906
+ async getFontFace(family, style) {
907
+ requireDeclared(
908
+ hasAsset("fonts"),
909
+ "assets.getFontFace",
910
+ 'capabilities.assets must include "fonts"'
911
+ );
912
+ if (!assetSource) return null;
913
+ let face;
914
+ try {
915
+ face = await assetSource.getFontFace(family, style);
916
+ } catch {
917
+ return null;
918
+ }
919
+ if (!face) return null;
920
+ if (face.bytes.byteLength > ASSET_BUDGETS.maxFontFaceBytes) {
921
+ log.warn(
922
+ `assets.getFontFace("${family}"${style ? `, "${style}"` : ""}) served ${face.bytes.byteLength} bytes, over the ${ASSET_BUDGETS.maxFontFaceBytes}-byte per-face cap \u2014 refused`
923
+ );
924
+ return null;
925
+ }
926
+ return face;
927
+ },
928
+ // C-5 / I-04 (core v42): a placed DOCUMENT image's ORIGINAL bytes,
929
+ // straight from the engine's resolver/parse cache through the
930
+ // `requestPlacedAssetBytes` wire query — no injected source needed
931
+ // (the engine IS the byte holder). `found:false` and any channel
932
+ // failure both answer `null`, the door's honest no-bytes mode. No
933
+ // size clamp: document-scale originals (PSDs) are the use case and
934
+ // the engine only serves what the document already holds.
935
+ async getPlacedImage(elementId) {
936
+ requireDeclared(
937
+ hasAsset("images"),
938
+ "assets.getPlacedImage",
939
+ 'capabilities.assets must include "images"'
940
+ );
941
+ try {
942
+ const reply = await getEditor().client.send({
943
+ kind: "requestPlacedAssetBytes",
944
+ payload: { elementId }
945
+ });
946
+ if (reply.kind !== "placedAssetBytes" || !reply.payload.found) {
947
+ return null;
948
+ }
949
+ const p = reply.payload;
950
+ return {
951
+ bytes: Uint8Array.from(p.encoded),
952
+ uri: p.uri,
953
+ width: p.width,
954
+ height: p.height
955
+ };
956
+ } catch {
957
+ return null;
958
+ }
959
+ }
960
+ };
961
+ const blobBackend = options?.blobStore;
962
+ const blobQuota = Math.min(
963
+ BLOB_BUDGETS.defaultQuotaBytes,
964
+ caps?.storage?.quotaBytes ?? BLOB_BUDGETS.defaultQuotaBytes
965
+ );
966
+ const blobGate = (door) => requireDeclared(
967
+ hasBlobStore(),
968
+ door,
969
+ "capabilities.storage must include { blob: true }"
970
+ );
971
+ const blob = {
972
+ async write(key, bytes) {
973
+ blobGate("blob.write");
974
+ if (!blobBackend) {
975
+ throw new Error(
976
+ `host.blob.write("${key}") \u2014 no blob store wired (supports("storage.blob@1") is false; the editor injects one)`
977
+ );
978
+ }
979
+ await blobBackend.delete(manifest.id, key);
980
+ const used = await blobBackend.used(manifest.id);
981
+ if (used + bytes.byteLength > blobQuota) {
982
+ throw new Error(
983
+ `host.blob.write("${key}") \u2014 ${bytes.byteLength} bytes would exceed the ${blobQuota}-byte quota (used ${used}) \u2014 refused`
984
+ );
985
+ }
986
+ await blobBackend.write(manifest.id, key, bytes);
987
+ },
988
+ async read(key) {
989
+ blobGate("blob.read");
990
+ if (!blobBackend) return null;
991
+ return blobBackend.read(manifest.id, key);
992
+ },
993
+ async delete(key) {
994
+ blobGate("blob.delete");
995
+ if (!blobBackend) return;
996
+ await blobBackend.delete(manifest.id, key);
997
+ },
998
+ async keys() {
999
+ blobGate("blob.keys");
1000
+ if (!blobBackend) return [];
1001
+ return blobBackend.keys(manifest.id);
1002
+ },
1003
+ async usage() {
1004
+ blobGate("blob.usage");
1005
+ if (!blobBackend) return { used: 0, quota: 0 };
1006
+ return { used: await blobBackend.used(manifest.id), quota: blobQuota };
392
1007
  }
393
1008
  };
394
1009
  const featureSet = new Set(HOST_FEATURES);
1010
+ if (getEditor().text) {
1011
+ featureSet.add("text.measure@1");
1012
+ }
1013
+ if (getEditor().sceneLayers) {
1014
+ featureSet.add("rendering.sceneLayer@1");
1015
+ }
395
1016
  if (options?.shell) {
396
1017
  featureSet.add("shell.openPanel@1");
1018
+ featureSet.add("shell.pickFile@1");
1019
+ }
1020
+ if (options?.widgets) {
1021
+ featureSet.add("widgets.codeEditor@1");
1022
+ }
1023
+ if (options?.schemaPanelRenderer) {
1024
+ featureSet.add("schemaPanel.renderer@1");
1025
+ }
1026
+ if (options?.diagnosticsSink) {
1027
+ featureSet.add("diagnostics.publish@1");
1028
+ }
1029
+ if (options?.assetSource) {
1030
+ featureSet.add("assets.fonts@1");
1031
+ }
1032
+ if (options?.consent) {
1033
+ featureSet.add("network.consent@1");
1034
+ }
1035
+ if (options?.dataProviders) {
1036
+ featureSet.add("dataProviders@1");
1037
+ }
1038
+ if (options?.blobStore) {
1039
+ featureSet.add("storage.blob@1");
397
1040
  }
398
1041
  const host = {
399
1042
  manifest,
@@ -402,10 +1045,17 @@ function createBundleHost(getEditor, manifest, options) {
402
1045
  document,
403
1046
  selection,
404
1047
  viewport,
1048
+ text,
405
1049
  overlay,
406
1050
  shell,
407
1051
  storage,
1052
+ blob,
1053
+ network,
1054
+ dataProviders,
408
1055
  diagnostics,
1056
+ bindings,
1057
+ widgets,
1058
+ assets,
409
1059
  supports: (feature) => featureSet.has(feature),
410
1060
  get editor() {
411
1061
  return getEditor();
@@ -419,10 +1069,548 @@ function createBundleHost(getEditor, manifest, options) {
419
1069
  };
420
1070
  }
421
1071
 
1072
+ // src/version.ts
1073
+ var API_VERSION = "0.2.0";
1074
+ function parse(v) {
1075
+ const m = /^(\d+)\.(\d+)(?:\.(\d+))?$/.exec(v.trim());
1076
+ if (!m) return null;
1077
+ return [Number(m[1]), Number(m[2]), Number(m[3] ?? "0")];
1078
+ }
1079
+ function satisfiesApiVersion(range, version = API_VERSION) {
1080
+ const r = range.trim();
1081
+ if (r === "*") return true;
1082
+ const v = parse(version);
1083
+ if (!v) return false;
1084
+ if (r.startsWith("^")) {
1085
+ const base = parse(r.slice(1));
1086
+ if (!base) return false;
1087
+ if (base[0] > 0) {
1088
+ if (v[0] !== base[0]) return false;
1089
+ if (v[1] !== base[1]) return v[1] > base[1];
1090
+ return v[2] >= base[2];
1091
+ }
1092
+ return v[0] === 0 && v[1] === base[1] && v[2] >= base[2];
1093
+ }
1094
+ const exact = parse(r);
1095
+ if (!exact) return false;
1096
+ return v[0] === exact[0] && v[1] === exact[1] && v[2] === exact[2];
1097
+ }
1098
+
1099
+ // src/wasm-loader.ts
1100
+ var CANVAS_WASM_PKG = "@paged-media/canvas-wasm";
1101
+ async function nodeBuiltins() {
1102
+ const [{ createRequire }, { readFileSync }, path, url] = await Promise.all([
1103
+ import("module"),
1104
+ import("fs"),
1105
+ import("path"),
1106
+ import("url")
1107
+ ]);
1108
+ return {
1109
+ createRequire,
1110
+ readFileSync,
1111
+ dirname: path.dirname,
1112
+ resolve: path.resolve,
1113
+ fileURLToPath: url.fileURLToPath,
1114
+ pathToFileURL: url.pathToFileURL
1115
+ };
1116
+ }
1117
+ var STAMP_PREFIX = `// Synced from ${CANVAS_WASM_PKG}@`;
1118
+ async function modulePaths() {
1119
+ const { dirname, resolve, fileURLToPath } = await nodeBuiltins();
1120
+ const here = dirname(fileURLToPath(import.meta.url));
1121
+ return {
1122
+ here,
1123
+ wireDts: resolve(here, "../../plugin-api/src/wire.d.ts")
1124
+ };
1125
+ }
1126
+ async function readVendoredWireVersion(wireDtsPath) {
1127
+ const { readFileSync } = await nodeBuiltins();
1128
+ const path = wireDtsPath ?? (await modulePaths()).wireDts;
1129
+ let text;
1130
+ try {
1131
+ text = readFileSync(path, "utf8");
1132
+ } catch {
1133
+ return null;
1134
+ }
1135
+ for (const line of text.split("\n", 8)) {
1136
+ if (line.startsWith(STAMP_PREFIX)) {
1137
+ return line.slice(STAMP_PREFIX.length).trim();
1138
+ }
1139
+ }
1140
+ return null;
1141
+ }
1142
+ function protocolFromVersion(version) {
1143
+ const m = /^\d+\.(\d+)\.\d+/.exec(version.trim());
1144
+ return m ? Number(m[1]) : null;
1145
+ }
1146
+ async function resolveCanvasWasm(resolveFrom) {
1147
+ const { createRequire, readFileSync, dirname, resolve, pathToFileURL } = await nodeBuiltins();
1148
+ const { here } = await modulePaths();
1149
+ const anchors = [
1150
+ resolveFrom,
1151
+ here,
1152
+ resolve(here, "../../../../editor/packages/client"),
1153
+ resolve(here, "../../..")
1154
+ ].filter((a) => Boolean(a));
1155
+ let lastErr;
1156
+ for (const anchor of anchors) {
1157
+ try {
1158
+ const req = createRequire(pathToFileURL(resolve(anchor, "package.json")));
1159
+ const pkgJsonPath = req.resolve(`${CANVAS_WASM_PKG}/package.json`);
1160
+ const dir = dirname(pkgJsonPath);
1161
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
1162
+ const loaderPath = resolve(dir, pkg.main ?? "paged_canvas_wasm.js");
1163
+ const wasmPath = resolve(dir, "paged_canvas_wasm_bg.wasm");
1164
+ return {
1165
+ loaderUrl: pathToFileURL(loaderPath).href,
1166
+ wasmPath,
1167
+ version: pkg.version ?? "unknown",
1168
+ dir
1169
+ };
1170
+ } catch (err) {
1171
+ lastErr = err;
1172
+ }
1173
+ }
1174
+ throw new Error(
1175
+ `plugin-sdk: ${CANVAS_WASM_PKG} is not resolvable for the headless harness (tried ${anchors.length} anchors). Install it as a devDependency, or run inside a workspace where the editor's packages/client provides it. No warn-skip: a headless host with no real engine is the fiction createHeadlessHost exists to prevent (B-13). Last error: ${String(lastErr)}`
1176
+ );
1177
+ }
1178
+ async function loadHeadlessEngine(options = {}) {
1179
+ const { readFileSync } = await nodeBuiltins();
1180
+ const { loaderUrl, wasmPath, version } = await resolveCanvasWasm(
1181
+ options.resolveFrom
1182
+ );
1183
+ const mod = await import(loaderUrl);
1184
+ const bytes = readFileSync(wasmPath);
1185
+ mod.initSync({
1186
+ module: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
1187
+ });
1188
+ const worker = new mod.CanvasWorker();
1189
+ const stampedVersion = await readVendoredWireVersion();
1190
+ const expectedProtocol = options.expectedProtocol ?? (stampedVersion ? protocolFromVersion(stampedVersion) : null);
1191
+ if (expectedProtocol !== null && worker.protocolVersion !== expectedProtocol) {
1192
+ const booted = worker.protocolVersion;
1193
+ try {
1194
+ worker.free();
1195
+ } catch {
1196
+ }
1197
+ throw new Error(
1198
+ `plugin-sdk: headless engine protocol mismatch \u2014 booted ${CANVAS_WASM_PKG}@${version} reports protocol v${booted}, but the vendored wire types are stamped @${stampedVersion ?? "?"} (protocol v${expectedProtocol}). Re-run scripts/sync-wire.mjs and reinstall the matching package.`
1199
+ );
1200
+ }
1201
+ return { worker, version, protocolVersion: worker.protocolVersion };
1202
+ }
1203
+
1204
+ // src/harness.ts
1205
+ function inMemoryBlobStore() {
1206
+ const byPlugin = /* @__PURE__ */ new Map();
1207
+ const dir = (id) => {
1208
+ let d = byPlugin.get(id);
1209
+ if (!d) byPlugin.set(id, d = /* @__PURE__ */ new Map());
1210
+ return d;
1211
+ };
1212
+ return {
1213
+ async write(id, key, bytes) {
1214
+ dir(id).set(key, bytes.slice());
1215
+ },
1216
+ async read(id, key) {
1217
+ const v = dir(id).get(key);
1218
+ return v ? v.slice() : null;
1219
+ },
1220
+ async delete(id, key) {
1221
+ dir(id).delete(key);
1222
+ },
1223
+ async keys(id) {
1224
+ return Array.from(dir(id).keys());
1225
+ },
1226
+ async used(id) {
1227
+ let n = 0;
1228
+ for (const v of dir(id).values()) n += v.byteLength;
1229
+ return n;
1230
+ }
1231
+ };
1232
+ }
1233
+ var seqCounter = 1;
1234
+ function makeEngineEditor(worker, recorder, onToolPreview) {
1235
+ const protocol = worker.protocolVersion;
1236
+ const listeners = /* @__PURE__ */ new Set();
1237
+ const fanOut = (reply) => {
1238
+ for (const l of listeners) l(reply);
1239
+ };
1240
+ const dispatch = (kind, payload) => {
1241
+ const envelope = JSON.stringify(
1242
+ payload === void 0 ? { seq: seqCounter++, protocol, kind } : { seq: seqCounter++, protocol, kind, payload }
1243
+ );
1244
+ const raw = worker.handleMessage(envelope);
1245
+ return JSON.parse(raw);
1246
+ };
1247
+ const recordingRegistry = (kind) => ({
1248
+ register(contribution) {
1249
+ const entry = {
1250
+ kind,
1251
+ id: contribution.id,
1252
+ value: contribution
1253
+ };
1254
+ recorder.push(entry);
1255
+ return {
1256
+ dispose() {
1257
+ const i = recorder.indexOf(entry);
1258
+ if (i >= 0) recorder.splice(i, 1);
1259
+ }
1260
+ };
1261
+ }
1262
+ });
1263
+ let keybindingSeq = 0;
1264
+ const keybindingRegistry = {
1265
+ register(contribution) {
1266
+ const entry = {
1267
+ kind: "keybinding",
1268
+ id: `${contribution.command}#${keybindingSeq++}`,
1269
+ value: contribution
1270
+ };
1271
+ recorder.push(entry);
1272
+ return {
1273
+ dispose() {
1274
+ const i = recorder.indexOf(entry);
1275
+ if (i >= 0) recorder.splice(i, 1);
1276
+ }
1277
+ };
1278
+ }
1279
+ };
1280
+ let elementSelection = [];
1281
+ let toolPreview = null;
1282
+ const client = {
1283
+ async mutate(mutation) {
1284
+ const reply = dispatch("mutate", mutation);
1285
+ fanOut(reply);
1286
+ return reply;
1287
+ },
1288
+ async undo() {
1289
+ const reply = dispatch("undo");
1290
+ fanOut(reply);
1291
+ return reply;
1292
+ },
1293
+ async redo() {
1294
+ const reply = dispatch("redo");
1295
+ fanOut(reply);
1296
+ return reply;
1297
+ },
1298
+ async collection(name) {
1299
+ const reply = dispatch("requestCollection", { name });
1300
+ if (reply.kind === "collectionReply") {
1301
+ const items = reply.payload.items;
1302
+ return Array.isArray(items) ? items : [];
1303
+ }
1304
+ return [];
1305
+ },
1306
+ async documentMeta() {
1307
+ const reply = dispatch("requestDocumentMeta");
1308
+ if (reply.kind === "documentMetaReply") {
1309
+ return reply.payload.meta;
1310
+ }
1311
+ throw new Error(`unexpected reply: ${reply.kind}`);
1312
+ },
1313
+ async pathAnchors(id) {
1314
+ const reply = dispatch("requestPathAnchors", { id });
1315
+ if (reply.kind === "pathAnchors") {
1316
+ return reply.payload.result;
1317
+ }
1318
+ return null;
1319
+ },
1320
+ async elementGeometry(ids) {
1321
+ const reply = dispatch("requestElementGeometry", { ids });
1322
+ if (reply.kind === "elementGeometry") {
1323
+ return reply.payload.items;
1324
+ }
1325
+ return [];
1326
+ },
1327
+ async setElementSelection(ids, mode) {
1328
+ const reply = dispatch("setElementSelection", { ids, mode });
1329
+ if (reply.kind === "elementSelectionApplied") {
1330
+ fanOut(reply);
1331
+ return reply.payload.ids;
1332
+ }
1333
+ throw new Error(`unexpected reply: ${reply.kind}`);
1334
+ },
1335
+ async send(message) {
1336
+ const m = message;
1337
+ const reply = dispatch(m.kind, m.payload);
1338
+ return reply;
1339
+ },
1340
+ subscribe(listener) {
1341
+ listeners.add(listener);
1342
+ return () => listeners.delete(listener);
1343
+ }
1344
+ };
1345
+ const editor = {
1346
+ client,
1347
+ registries: {
1348
+ tools: recordingRegistry("tool"),
1349
+ panels: recordingRegistry("panel"),
1350
+ commands: recordingRegistry("command"),
1351
+ keybindings: keybindingRegistry,
1352
+ overlays: recordingRegistry("overlay"),
1353
+ importers: recordingRegistry("importer"),
1354
+ exporters: recordingRegistry("exporter")
1355
+ },
1356
+ selection: {
1357
+ get elementSelection() {
1358
+ return elementSelection;
1359
+ },
1360
+ setElementSelection(ids) {
1361
+ elementSelection = ids;
1362
+ },
1363
+ setElementGeometry() {
1364
+ }
1365
+ },
1366
+ camera: { camera: { scale: 1, tx: 0, ty: 0 } },
1367
+ overlaySignals: {
1368
+ setToolPreview(value) {
1369
+ toolPreview = value;
1370
+ onToolPreview(toolPreview);
1371
+ }
1372
+ },
1373
+ // No tool spine + no content caret headlessly — both are inert
1374
+ // members of the narrow handle, present so the cast is total.
1375
+ tool: {
1376
+ setBaseTool() {
1377
+ }
1378
+ },
1379
+ contentSelection: { contentSelection: null }
1380
+ };
1381
+ return editor;
1382
+ }
1383
+ async function createHeadlessHost(options = {}) {
1384
+ const engine = await loadHeadlessEngine(options);
1385
+ const worker = engine.worker;
1386
+ const contributions = [];
1387
+ let lastPreview = null;
1388
+ const editor = makeEngineEditor(worker, contributions, (value) => {
1389
+ lastPreview = value;
1390
+ });
1391
+ let active = null;
1392
+ let currentHost = null;
1393
+ let disposed = false;
1394
+ const blobStore = options.blobStore ?? inMemoryBlobStore();
1395
+ const buildHost = (manifest, mode) => createBundleHost(() => editor, manifest, {
1396
+ console: options.console,
1397
+ storage: options.storage,
1398
+ blobStore,
1399
+ capabilityMode: mode,
1400
+ // W-06 — a recordable fake asset source the conformance harness
1401
+ // can pass so a bundle's `@font-face` byte path is exercisable
1402
+ // headlessly (the editor's real adapter currently serves null;
1403
+ // DESIGN.md §13.4). Absent → the no-bytes door.
1404
+ assetSource: options.assetSource,
1405
+ // Record the SCHEMA verbatim at registration — the panel registry
1406
+ // only ever sees the synthesized React panel, so the conformance
1407
+ // log gets the schema through this adapter seam (no host renderer
1408
+ // headlessly; visibility/enabled gates are asserted off `bindings`
1409
+ // directly, not through a mounted UI).
1410
+ onSchemaPanelRegistered: (c) => {
1411
+ const entry = {
1412
+ kind: "schemaPanel",
1413
+ id: c.id,
1414
+ value: c
1415
+ };
1416
+ contributions.push(entry);
1417
+ return {
1418
+ dispose() {
1419
+ const i = contributions.indexOf(entry);
1420
+ if (i >= 0) contributions.splice(i, 1);
1421
+ }
1422
+ };
1423
+ },
1424
+ // W3.2 — the editContext/objectType registries are not wired
1425
+ // headlessly (no shell stack / chrome), so the adapter takes the
1426
+ // recording-stub path and these hooks ARE the registration log.
1427
+ onEditContextRegistered: (c) => {
1428
+ const entry = {
1429
+ kind: "editContext",
1430
+ id: c.type,
1431
+ value: c
1432
+ };
1433
+ contributions.push(entry);
1434
+ return {
1435
+ dispose() {
1436
+ const i = contributions.indexOf(entry);
1437
+ if (i >= 0) contributions.splice(i, 1);
1438
+ }
1439
+ };
1440
+ },
1441
+ onObjectTypeRegistered: (c) => {
1442
+ const entry = {
1443
+ kind: "objectType",
1444
+ id: c.type,
1445
+ value: c
1446
+ };
1447
+ contributions.push(entry);
1448
+ return {
1449
+ dispose() {
1450
+ const i = contributions.indexOf(entry);
1451
+ if (i >= 0) contributions.splice(i, 1);
1452
+ }
1453
+ };
1454
+ }
1455
+ });
1456
+ const NEUTRAL = {
1457
+ id: "media.paged.harness",
1458
+ name: "harness",
1459
+ version: "0.0.0",
1460
+ apiVersion: `^${API_VERSION.slice(0, 3)}`,
1461
+ capabilities: {
1462
+ document: { read: "broad", write: "broad" },
1463
+ rendering: ["overlay", "hitTest", "sceneLayer"],
1464
+ keybindings: true,
1465
+ storage: { blob: true }
1466
+ },
1467
+ // Broad contribution declarations so the neutral DRIVER host (which
1468
+ // registers arbitrary contributions directly in 'warn' mode) never
1469
+ // trips the capability gate. A loaded BUNDLE is the subject — its
1470
+ // OWN manifest is enforced.
1471
+ contributes: {
1472
+ editContexts: [
1473
+ { type: "vectorGraphic", entry: "doubleClick" },
1474
+ { type: "webFrame", entry: "doubleClick" }
1475
+ ],
1476
+ objectTypes: [{ type: "webFrame", bakedFallback: "rectangle" }],
1477
+ importers: ["media.paged.harness.importer.xlsx"],
1478
+ exporters: ["media.paged.harness.exporter.xlsx"]
1479
+ }
1480
+ };
1481
+ let { host, dispose: disposeHostFacades } = buildHost(NEUTRAL, "warn");
1482
+ currentHost = host;
1483
+ const headless = {
1484
+ get host() {
1485
+ return currentHost;
1486
+ },
1487
+ engineVersion: engine.version,
1488
+ protocolVersion: engine.protocolVersion,
1489
+ contributions,
1490
+ toolsContributed() {
1491
+ return contributions.filter((c) => c.kind === "tool").map((c) => c.value);
1492
+ },
1493
+ panelsContributed() {
1494
+ return contributions.filter((c) => c.kind === "panel").map((c) => c.value);
1495
+ },
1496
+ schemaPanelsContributed() {
1497
+ return contributions.filter((c) => c.kind === "schemaPanel").map((c) => c.value);
1498
+ },
1499
+ editContextsContributed() {
1500
+ return contributions.filter((c) => c.kind === "editContext").map((c) => c.value);
1501
+ },
1502
+ objectTypesContributed() {
1503
+ return contributions.filter((c) => c.kind === "objectType").map((c) => c.value);
1504
+ },
1505
+ importersContributed() {
1506
+ return contributions.filter((c) => c.kind === "importer").map((c) => c.value);
1507
+ },
1508
+ exportersContributed() {
1509
+ return contributions.filter((c) => c.kind === "exporter").map((c) => c.value);
1510
+ },
1511
+ lastToolPreview() {
1512
+ return lastPreview;
1513
+ },
1514
+ async load(idml) {
1515
+ const raw = worker.loadDocumentDirect(seqCounter++, idml);
1516
+ const reply = JSON.parse(raw);
1517
+ if (reply.kind === "documentLoaded") {
1518
+ try {
1519
+ worker.runResolveJson();
1520
+ } catch {
1521
+ }
1522
+ return reply.payload.pageIds;
1523
+ }
1524
+ const errKind = reply.kind === "loadFailed" ? reply.payload.error.kind : reply.kind;
1525
+ throw new Error(`headless load failed (${reply.kind}: ${errKind})`);
1526
+ },
1527
+ loadBundle(bundle) {
1528
+ if (active) {
1529
+ throw new Error(
1530
+ "headless host: a bundle is already loaded \u2014 dispose it first (one bundle per headless host in v1)"
1531
+ );
1532
+ }
1533
+ const { manifest } = bundle;
1534
+ if (!satisfiesApiVersion(manifest.apiVersion)) {
1535
+ throw new Error(
1536
+ `headless host: ${manifest.id}@${manifest.version} requires plugin-api "${manifest.apiVersion}", host implements ${API_VERSION}`
1537
+ );
1538
+ }
1539
+ disposeHostFacades();
1540
+ ({ host, dispose: disposeHostFacades } = buildHost(
1541
+ manifest,
1542
+ options.capabilityMode ?? "enforce"
1543
+ ));
1544
+ currentHost = host;
1545
+ const handle = bundle.activate(host);
1546
+ let bundleActive = true;
1547
+ active = {
1548
+ dispose() {
1549
+ if (!bundleActive) return;
1550
+ bundleActive = false;
1551
+ try {
1552
+ handle.dispose();
1553
+ } finally {
1554
+ disposeHostFacades();
1555
+ ({ host, dispose: disposeHostFacades } = buildHost(
1556
+ NEUTRAL,
1557
+ "warn"
1558
+ ));
1559
+ currentHost = host;
1560
+ active = null;
1561
+ }
1562
+ }
1563
+ };
1564
+ return { dispose: () => active?.dispose() };
1565
+ },
1566
+ dispose() {
1567
+ if (disposed) return;
1568
+ disposed = true;
1569
+ try {
1570
+ if (active) active.dispose();
1571
+ else disposeHostFacades();
1572
+ } finally {
1573
+ contributions.length = 0;
1574
+ lastPreview = null;
1575
+ worker.free();
1576
+ }
1577
+ }
1578
+ };
1579
+ return headless;
1580
+ }
1581
+
1582
+ // src/asset-source-fake.ts
1583
+ function createRecordableAssetSource(seeds = []) {
1584
+ const requests = [];
1585
+ return {
1586
+ requests,
1587
+ async getFontFace(family, style) {
1588
+ requests.push(style === void 0 ? { family } : { family, style });
1589
+ const key = family.trim().toLowerCase();
1590
+ for (const seed of seeds) {
1591
+ if (seed.family.trim().toLowerCase() !== key) continue;
1592
+ if (seed.matchStyle !== void 0 && seed.matchStyle !== style) {
1593
+ continue;
1594
+ }
1595
+ const { matchStyle: _unused, ...face } = seed;
1596
+ void _unused;
1597
+ return face;
1598
+ }
1599
+ return null;
1600
+ }
1601
+ };
1602
+ }
1603
+
422
1604
  // src/load.ts
423
1605
  var ID_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9-]*)+$/;
424
1606
  function loadBundle(getEditor, bundle, options) {
425
1607
  const { manifest } = bundle;
1608
+ const trust = options?.trust ?? "first-party";
1609
+ if (trust !== "first-party") {
1610
+ throw new Error(
1611
+ `loadBundle: ${manifest.id} requested trust="${String(trust)}", but same-realm bundle execution is first-party-only during incubation. Loading non-first-party bundles is gated on the isolate/RPC host, enforced capabilities, and package signing (see plugin-trust-line.md "Gate checklist").`
1612
+ );
1613
+ }
426
1614
  if (!ID_PATTERN.test(manifest.id)) {
427
1615
  throw new Error(
428
1616
  `loadBundle: manifest id "${manifest.id}" is not reverse-DNS (expected e.g. "media.paged.draw")`
@@ -457,6 +1645,88 @@ function loadBundle(getEditor, bundle, options) {
457
1645
  };
458
1646
  }
459
1647
 
1648
+ // src/wasm-bundle-loader.ts
1649
+ var WASM_BUDGETS = {
1650
+ /** Hard per-artifact byte ceiling. A release-optimised wasm layout
1651
+ * engine (Blitz-class) lands in the low-single-digit MiB; 8 MiB
1652
+ * rejects an accidentally-bundled debug build while leaving headroom
1653
+ * for one real engine. A manifest `maxBytes` may only TIGHTEN this. */
1654
+ maxArtifactBytes: 8 * 1024 * 1024,
1655
+ /** Total declared wasm across one bundle. Bounds a bundle that ships
1656
+ * several modules (engine + a codec, say). */
1657
+ maxTotalBytes: 16 * 1024 * 1024,
1658
+ /** Wall-clock budget for fetch + compile + instantiate. Protects the
1659
+ * editor's main flow from a pathological module; advisory, the loader
1660
+ * aborts with a clear error when exceeded. */
1661
+ loadTimeBudgetMs: 3e3,
1662
+ /** Linear-memory growth ceiling, in 64 KiB wasm pages (4096 = 256 MiB).
1663
+ * Passed as `WebAssembly.Memory({ maximum })` when the host owns the
1664
+ * memory; a per-page layout pass should sit far under this. */
1665
+ maxMemoryPages: 4096
1666
+ };
1667
+ function findArtifact(bundle, name) {
1668
+ return bundle.manifest.capabilities?.wasm?.find((a) => a.name === name);
1669
+ }
1670
+ function isGranted(grant, name) {
1671
+ if (grant === void 0) return false;
1672
+ if (grant === "*") return true;
1673
+ if (Array.isArray(grant)) return grant.includes(name);
1674
+ return grant.has(name);
1675
+ }
1676
+ async function loadBundleWasm(bundle, name, options) {
1677
+ const id = bundle.manifest.id;
1678
+ const artifact = findArtifact(bundle, name);
1679
+ if (!artifact) {
1680
+ throw new Error(
1681
+ `loadBundleWasm: ${id} has no declared wasm artifact "${name}" \u2014 only artifacts listed in manifest.capabilities.wasm are loadable (declared-only).`
1682
+ );
1683
+ }
1684
+ if (!isGranted(options.grant, name)) {
1685
+ throw new Error(
1686
+ `loadBundleWasm: wasm artifact "${name}" of ${id} is not granted by the host \u2014 wasm carries no ambient authority; the host must grant it explicitly (pass grant: "*" or a name set).`
1687
+ );
1688
+ }
1689
+ const now = options.now ?? (() => Date.now());
1690
+ const budgetMs = options.loadTimeBudgetMs ?? WASM_BUDGETS.loadTimeBudgetMs;
1691
+ const started = now();
1692
+ const overBudget = (stage) => {
1693
+ throw new Error(
1694
+ `loadBundleWasm: "${name}" of ${id} exceeded the ${budgetMs}ms load-time budget at ${stage} (${Math.round(now() - started)}ms).`
1695
+ );
1696
+ };
1697
+ const bytes = await options.assetSource(artifact.path);
1698
+ if (now() - started > budgetMs) overBudget("fetch");
1699
+ const src = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
1700
+ const view = new Uint8Array(src.byteLength);
1701
+ view.set(src);
1702
+ const ceiling = typeof artifact.maxBytes === "number" && artifact.maxBytes > 0 ? Math.min(artifact.maxBytes, WASM_BUDGETS.maxArtifactBytes) : WASM_BUDGETS.maxArtifactBytes;
1703
+ if (view.byteLength > ceiling) {
1704
+ throw new Error(
1705
+ `loadBundleWasm: "${name}" of ${id} is ${view.byteLength} bytes, over its ${ceiling}-byte ceiling.`
1706
+ );
1707
+ }
1708
+ let memory;
1709
+ const imports = { ...options.imports ?? {} };
1710
+ const provideMemory = options.provideMemory ?? true;
1711
+ if (provideMemory) {
1712
+ const env = { ...imports.env };
1713
+ if (!("memory" in env)) {
1714
+ memory = new WebAssembly.Memory({
1715
+ initial: options.initialMemoryPages ?? 16,
1716
+ maximum: WASM_BUDGETS.maxMemoryPages
1717
+ // shared is intentionally absent — non-shared memory only (v1).
1718
+ });
1719
+ env.memory = memory;
1720
+ }
1721
+ imports.env = env;
1722
+ }
1723
+ const module = await WebAssembly.compile(view);
1724
+ if (now() - started > budgetMs) overBudget("compile");
1725
+ const instance = await WebAssembly.instantiate(module, imports);
1726
+ if (now() - started > budgetMs) overBudget("instantiate");
1727
+ return { artifact, module, instance, memory, byteLength: view.byteLength };
1728
+ }
1729
+
460
1730
  // src/gestures.ts
461
1731
  var CLICK_DRAG_THRESHOLD_PX = 4;
462
1732
  function beginPageDrag(e) {
@@ -520,22 +1790,51 @@ function contributeTool(host, tool) {
520
1790
  function contributePanel(host, panel) {
521
1791
  return host.contribute.panel(panel);
522
1792
  }
1793
+ function contributeSchemaPanel(host, panel) {
1794
+ return host.contribute.schemaPanel(panel);
1795
+ }
1796
+
1797
+ // src/edit-context.ts
1798
+ function contributeEditContext(host, contribution) {
1799
+ return host.contribute.editContext(contribution);
1800
+ }
1801
+ function contributeObjectType(host, contribution) {
1802
+ return host.contribute.objectType(contribution);
1803
+ }
523
1804
  export {
524
1805
  API_VERSION,
1806
+ ASSET_BUDGETS,
1807
+ BLOB_BUDGETS,
1808
+ CANVAS_WASM_PKG,
525
1809
  CLICK_DRAG_THRESHOLD_PX,
526
1810
  DisposableStore,
1811
+ FALLBACK_WIDGETS,
527
1812
  HOST_FEATURES,
528
1813
  PluginApiNotImplemented,
1814
+ PluginCapabilityError,
1815
+ WASM_BUDGETS,
529
1816
  beginPageDrag,
530
1817
  commitAndSelect,
1818
+ contributeEditContext,
1819
+ contributeObjectType,
531
1820
  contributePanel,
1821
+ contributeSchemaPanel,
532
1822
  contributeTool,
533
1823
  createBundleHost,
1824
+ createDataProviderRegistry,
534
1825
  createHeadlessHost,
1826
+ createRecordableAssetSource,
535
1827
  defineBundle,
536
1828
  endLocalFor,
537
1829
  loadBundle,
1830
+ loadBundleWasm,
1831
+ loadHeadlessEngine,
1832
+ makeSchemaPanelComponent,
1833
+ protocolFromVersion,
538
1834
  pxToPt,
1835
+ readVendoredWireVersion,
1836
+ resolveCanvasWasm,
1837
+ resolveGate,
539
1838
  satisfiesApiVersion,
540
1839
  toDisposable
541
1840
  };