@paged-media/plugin-sdk 0.2.5-canary.0 → 0.2.8-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 +480 -32
  2. package/dist/index.js +1312 -38
  3. package/package.json +15 -3
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",
@@ -108,6 +146,21 @@ var PluginApiNotImplemented = class extends Error {
108
146
  this.name = "PluginApiNotImplemented";
109
147
  }
110
148
  };
149
+ var PluginCapabilityError = class extends Error {
150
+ /** The host door that was called (e.g. `"contribute.tool"`). */
151
+ door;
152
+ /** The manifest declaration that would authorize it (e.g.
153
+ * `'contributes.tools[] must include "media.paged.web.tool.pen"'`). */
154
+ missingDeclaration;
155
+ constructor(door, missingDeclaration, pluginId) {
156
+ super(
157
+ `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.`
158
+ );
159
+ this.name = "PluginCapabilityError";
160
+ this.door = door;
161
+ this.missingDeclaration = missingDeclaration;
162
+ }
163
+ };
111
164
  function defaultStorageBacking() {
112
165
  const ls = globalThis.localStorage;
113
166
  if (ls) {
@@ -133,6 +186,59 @@ function defaultStorageBacking() {
133
186
  keys: () => Array.from(map.keys())
134
187
  };
135
188
  }
189
+ var ASSET_BUDGETS = {
190
+ /** Largest font face the door will serve, in bytes (8 MiB). */
191
+ maxFontFaceBytes: 8 * 1024 * 1024
192
+ };
193
+ var BLOB_BUDGETS = {
194
+ /** Default per-plugin blob ceiling, in bytes (64 MiB). */
195
+ defaultQuotaBytes: 64 * 1024 * 1024
196
+ };
197
+ function createDataProviderRegistry() {
198
+ const providers = /* @__PURE__ */ new Map();
199
+ const listeners = /* @__PURE__ */ new Map();
200
+ return {
201
+ register(registration) {
202
+ providers.set(registration.id, {
203
+ reg: registration,
204
+ revision: registration.revision
205
+ });
206
+ return {
207
+ update(revision) {
208
+ const entry = providers.get(registration.id);
209
+ if (entry) entry.revision = revision;
210
+ for (const l of listeners.get(registration.id) ?? []) l(revision);
211
+ },
212
+ dispose() {
213
+ providers.delete(registration.id);
214
+ }
215
+ };
216
+ },
217
+ discover(category) {
218
+ return [...providers.values()].filter((e) => category === void 0 || e.reg.category === category).map((e) => ({
219
+ id: e.reg.id,
220
+ category: e.reg.category,
221
+ schema: e.reg.schema,
222
+ revision: e.revision
223
+ }));
224
+ },
225
+ async get(id) {
226
+ const entry = providers.get(id);
227
+ if (!entry) return null;
228
+ const records = await entry.reg.getSnapshot();
229
+ return { id, revision: entry.revision, records };
230
+ },
231
+ onDidChange(id, listener) {
232
+ let set = listeners.get(id);
233
+ if (!set) {
234
+ set = /* @__PURE__ */ new Set();
235
+ listeners.set(id, set);
236
+ }
237
+ set.add(listener);
238
+ return toDisposable(() => set.delete(listener));
239
+ }
240
+ };
241
+ }
136
242
  function createBundleHost(getEditor, manifest, options) {
137
243
  const store = new DisposableStore();
138
244
  const sink = options?.console ?? console;
@@ -152,41 +258,276 @@ function createBundleHost(getEditor, manifest, options) {
152
258
  warn: (m, ...a) => sink.warn(`${tag} ${m}`, ...a),
153
259
  error: (m, ...a) => sink.error(`${tag} ${m}`, ...a)
154
260
  };
261
+ const capabilityMode = options?.capabilityMode ?? "enforce";
262
+ const caps = manifest.capabilities;
263
+ const declared = manifest.contributes;
264
+ const hasDoc = (dir) => caps?.document?.[dir] !== void 0;
265
+ const hasRendering = (s) => caps?.rendering?.includes(s) ?? false;
266
+ const hasAsset = (k) => caps?.assets?.includes(k) ?? false;
267
+ const hasBlobStore = () => caps?.storage?.blob === true;
268
+ const lists = (arr, id) => arr?.includes(id) ?? false;
269
+ const declaresType = (arr, type) => arr?.some((e) => e.type === type) ?? false;
270
+ const requireDeclared = (ok, door, missing) => {
271
+ if (ok) return;
272
+ if (capabilityMode === "warn") {
273
+ log.warn(
274
+ `${door} used without declaring it \u2014 ${missing} (capabilityMode: 'warn'; would refuse in 'enforce')`
275
+ );
276
+ return;
277
+ }
278
+ throw new PluginCapabilityError(door, missing, manifest.id);
279
+ };
280
+ const denyWrite = (ok, door, missing) => {
281
+ if (ok) return null;
282
+ const reason = `${door} requires ${missing} (trust-line W0.11)`;
283
+ if (capabilityMode === "warn") {
284
+ log.warn(`${reason} \u2014 proceeding (capabilityMode: 'warn')`);
285
+ return null;
286
+ }
287
+ return reason;
288
+ };
155
289
  const contribute = {
156
290
  tool(c) {
157
291
  assertNamespaced(c.id, "tool");
292
+ requireDeclared(
293
+ lists(declared?.tools, c.id),
294
+ "contribute.tool",
295
+ `contributes.tools[] must include "${c.id}"`
296
+ );
158
297
  return store.add(getEditor().registries.tools.register(c));
159
298
  },
160
299
  panel(c) {
161
300
  assertNamespaced(c.id, "panel");
301
+ requireDeclared(
302
+ lists(declared?.panels, c.id),
303
+ "contribute.panel",
304
+ `contributes.panels[] must include "${c.id}"`
305
+ );
162
306
  return store.add(getEditor().registries.panels.register(c));
163
307
  },
308
+ schemaPanel(c) {
309
+ assertNamespaced(c.id, "schemaPanel");
310
+ requireDeclared(
311
+ lists(declared?.panels, c.id),
312
+ "contribute.schemaPanel",
313
+ `contributes.panels[] must include "${c.id}"`
314
+ );
315
+ const panel = {
316
+ id: c.id,
317
+ title: c.title,
318
+ icon: c.icon,
319
+ defaultDock: c.defaultDock,
320
+ defaultGroup: c.defaultGroup,
321
+ closable: c.closable,
322
+ movable: c.movable,
323
+ component: makeSchemaPanelComponent(
324
+ c,
325
+ bindings,
326
+ options?.schemaPanelRenderer
327
+ )
328
+ };
329
+ const reg = store.add(getEditor().registries.panels.register(panel));
330
+ const recorded = options?.onSchemaPanelRegistered?.(c);
331
+ if (recorded) {
332
+ const d = store.add(recorded);
333
+ return toDisposable(() => {
334
+ reg.dispose();
335
+ d.dispose();
336
+ });
337
+ }
338
+ return reg;
339
+ },
164
340
  command(c) {
165
341
  assertNamespaced(c.id, "command");
342
+ requireDeclared(
343
+ lists(declared?.commands, c.id),
344
+ "contribute.command",
345
+ `contributes.commands[] must include "${c.id}"`
346
+ );
166
347
  return store.add(getEditor().registries.commands.register(c));
167
348
  },
168
349
  keybinding(c) {
350
+ requireDeclared(
351
+ caps?.keybindings === true,
352
+ "contribute.keybinding",
353
+ "capabilities.keybindings must be true"
354
+ );
169
355
  return store.add(getEditor().registries.keybindings.register(c));
170
356
  },
171
357
  overlay(c) {
172
358
  assertNamespaced(c.id, "overlay");
359
+ requireDeclared(
360
+ hasRendering("overlay"),
361
+ "contribute.overlay",
362
+ 'capabilities.rendering must include "overlay"'
363
+ );
173
364
  return store.add(getEditor().registries.overlays.register(c));
174
365
  },
175
- editContext() {
176
- throw new PluginApiNotImplemented(
366
+ // W3.2 (un-reserved — B-02 / W-03): the last two reserved doors. The
367
+ // capability gate keys off the OBJECT arrays in `contributes`
368
+ // (`editContexts[]` / `objectTypes[]` carry `{type,…}`, not flat
369
+ // ids). The shell owns the stack / chrome / write-scope; the SDK
370
+ // adapter just hands the contribution to the editor's registry (or,
371
+ // when the host hasn't wired one — headless / not-yet-adopted —
372
+ // records it through the harness hook). The `type` is a content-type
373
+ // name, NOT a namespaced id, so the namespace rule does NOT apply
374
+ // (the capability gate is the only gate).
375
+ editContext(c) {
376
+ requireDeclared(
377
+ declaresType(declared?.editContexts, c.type),
177
378
  "contribute.editContext",
178
- "P0 shell work \u2014 plugin-draw/BREAKAGE_LOG.md B-02"
379
+ `contributes.editContexts[] must declare { type: "${c.type}" }`
179
380
  );
381
+ const stamped = {
382
+ ...c,
383
+ metadataKey: metadataKey(manifest)
384
+ };
385
+ const reg = getEditor().registries.editContexts;
386
+ const recorded = options?.onEditContextRegistered?.(stamped);
387
+ if (reg) {
388
+ const d = store.add(reg.register(stamped));
389
+ if (recorded) {
390
+ const r = store.add(recorded);
391
+ return toDisposable(() => {
392
+ d.dispose();
393
+ r.dispose();
394
+ });
395
+ }
396
+ return d;
397
+ }
398
+ if (recorded) return store.add(recorded);
399
+ return store.add(toDisposable(() => {
400
+ }));
180
401
  },
181
- objectType() {
182
- throw new PluginApiNotImplemented(
402
+ objectType(c) {
403
+ requireDeclared(
404
+ declaresType(declared?.objectTypes, c.type),
183
405
  "contribute.objectType",
184
- "paged.web W1 \u2014 base-idea \xA79.1.2"
406
+ `contributes.objectTypes[] must declare { type: "${c.type}" }`
407
+ );
408
+ const stamped = {
409
+ ...c,
410
+ metadataKey: metadataKey(manifest)
411
+ };
412
+ const reg = getEditor().registries.objectTypes;
413
+ const recorded = options?.onObjectTypeRegistered?.(stamped);
414
+ if (reg) {
415
+ const d = store.add(reg.register(stamped));
416
+ if (recorded) {
417
+ const r = store.add(recorded);
418
+ return toDisposable(() => {
419
+ d.dispose();
420
+ r.dispose();
421
+ });
422
+ }
423
+ return d;
424
+ }
425
+ if (recorded) return store.add(recorded);
426
+ return store.add(toDisposable(() => {
427
+ }));
428
+ },
429
+ // K-2 / S-06 — document IO. Mirrors `command`: namespaced id the
430
+ // manifest must list, routed to the shell registry. The registry is
431
+ // OPTIONAL on the handle (a host that hasn't wired it stays
432
+ // assignable) — when absent the door is a tracked no-op; the headless
433
+ // harness injects a recording registry so conformance can assert it.
434
+ importer(c) {
435
+ assertNamespaced(c.id, "importer");
436
+ requireDeclared(
437
+ lists(declared?.importers, c.id),
438
+ "contribute.importer",
439
+ `contributes.importers[] must include "${c.id}"`
440
+ );
441
+ const reg = getEditor().registries.importers;
442
+ if (reg) return store.add(reg.register(c));
443
+ return store.add(toDisposable(() => {
444
+ }));
445
+ },
446
+ exporter(c) {
447
+ assertNamespaced(c.id, "exporter");
448
+ requireDeclared(
449
+ lists(declared?.exporters, c.id),
450
+ "contribute.exporter",
451
+ `contributes.exporters[] must include "${c.id}"`
452
+ );
453
+ const reg = getEditor().registries.exporters;
454
+ if (reg) return store.add(reg.register(c));
455
+ return store.add(toDisposable(() => {
456
+ }));
457
+ },
458
+ // C-1 — the in-frame scene-layer surface. Capability-gated on
459
+ // `rendering ∋ sceneLayer`; routes submit/clear to the editor's scene
460
+ // channel (`getEditor().sceneLayers` → canvas-wasm submit/clear). When
461
+ // no channel is wired (headless / older editor) the surface warns +
462
+ // no-ops (probe `supports("rendering.sceneLayer@1")`). Disposing the
463
+ // surface clears every layer it submitted (tracked so host.dispose()
464
+ // tears them down).
465
+ sceneLayer() {
466
+ requireDeclared(
467
+ hasRendering("sceneLayer"),
468
+ "contribute.sceneLayer",
469
+ 'capabilities.rendering must include "sceneLayer"'
185
470
  );
471
+ const submitted = /* @__PURE__ */ new Set();
472
+ const channel = () => getEditor().sceneLayers;
473
+ const surface = {
474
+ async submit(elementId, layer) {
475
+ const ch = channel();
476
+ if (!ch) {
477
+ log.warn(
478
+ `contribute.sceneLayer().submit("${elementId}") ignored \u2014 the host wired no scene channel (probe supports("rendering.sceneLayer@1"))`
479
+ );
480
+ return;
481
+ }
482
+ submitted.add(elementId);
483
+ await ch.submit(elementId, layer);
484
+ },
485
+ async clear(elementId) {
486
+ submitted.delete(elementId);
487
+ await channel()?.clear(elementId);
488
+ },
489
+ dispose() {
490
+ const ch = channel();
491
+ if (ch) {
492
+ for (const id of submitted) void ch.clear(id);
493
+ }
494
+ submitted.clear();
495
+ }
496
+ };
497
+ return store.add(surface);
186
498
  }
187
499
  };
500
+ const foreignMetadataKey = (m) => {
501
+ if (m.op === "setPluginMetadata") {
502
+ return m.args.key === metadataKey(manifest) ? null : m.args.key;
503
+ }
504
+ if (m.op === "batch") {
505
+ for (const child of m.args.ops) {
506
+ const bad = foreignMetadataKey(child);
507
+ if (bad !== null) return bad;
508
+ }
509
+ }
510
+ return null;
511
+ };
512
+ const requireDocRead = (door) => requireDeclared(
513
+ hasDoc("read"),
514
+ door,
515
+ "capabilities.document.read must be declared"
516
+ );
188
517
  const document = {
189
518
  async mutate(mutation) {
519
+ const denied = denyWrite(
520
+ hasDoc("write"),
521
+ "document.mutate",
522
+ "capabilities.document.write"
523
+ );
524
+ if (denied !== null) return { applied: false, error: denied };
525
+ const foreign = foreignMetadataKey(mutation);
526
+ if (foreign !== null) {
527
+ const error = `setPluginMetadata key "${foreign}" is outside this plugin's namespace ("${metadataKey(manifest)}")`;
528
+ log.warn(error);
529
+ return { applied: false, error };
530
+ }
190
531
  try {
191
532
  const reply = await getEditor().client.mutate(mutation);
192
533
  if (reply.kind === "mutationApplied") {
@@ -205,21 +546,40 @@ function createBundleHost(getEditor, manifest, options) {
205
546
  }
206
547
  },
207
548
  async undo() {
549
+ requireDeclared(
550
+ hasDoc("write"),
551
+ "document.undo",
552
+ "capabilities.document.write"
553
+ );
208
554
  await getEditor().client.undo();
209
555
  },
210
556
  async redo() {
557
+ requireDeclared(
558
+ hasDoc("write"),
559
+ "document.redo",
560
+ "capabilities.document.write"
561
+ );
211
562
  await getEditor().client.redo();
212
563
  },
213
564
  collection(name) {
565
+ requireDocRead("document.collection");
214
566
  return getEditor().client.collection(name);
215
567
  },
216
568
  meta() {
569
+ requireDocRead("document.meta");
217
570
  return getEditor().client.documentMeta();
218
571
  },
219
572
  pathAnchors(id) {
573
+ requireDocRead("document.pathAnchors");
220
574
  return getEditor().client.pathAnchors(id).catch(() => null);
221
575
  },
222
576
  async hitTest(pageId, point, filter = "any") {
577
+ requireDocRead("document.hitTest");
578
+ requireDeclared(
579
+ hasRendering("hitTest"),
580
+ "document.hitTest",
581
+ 'capabilities.rendering must include "hitTest"'
582
+ );
223
583
  try {
224
584
  const reply = await getEditor().client.send({
225
585
  kind: "hitTest",
@@ -231,15 +591,18 @@ function createBundleHost(getEditor, manifest, options) {
231
591
  }
232
592
  },
233
593
  elementGeometry(ids) {
594
+ requireDocRead("document.elementGeometry");
234
595
  return getEditor().client.elementGeometry(ids);
235
596
  },
236
597
  async tree() {
598
+ requireDocRead("document.tree");
237
599
  const reply = await getEditor().client.send({
238
600
  kind: "requestSceneTree"
239
601
  });
240
602
  return reply.kind === "sceneTree" ? reply.payload.roots : [];
241
603
  },
242
604
  async getMetadata(id) {
605
+ requireDocRead("document.getMetadata");
243
606
  const key = metadataKey(manifest);
244
607
  const reply = await getEditor().client.send({
245
608
  kind: "requestElementProperties",
@@ -266,14 +629,29 @@ function createBundleHost(getEditor, manifest, options) {
266
629
  args: {
267
630
  elementId: id,
268
631
  key: metadataKey(manifest),
269
- value: envelope === null ? null : JSON.stringify(envelope)
632
+ value: envelope === null ? null : JSON.stringify(envelope),
633
+ caller: manifest.id
270
634
  }
271
635
  });
272
636
  },
637
+ async frameChain(storyId) {
638
+ requireDocRead("document.frameChain");
639
+ const reply = await getEditor().client.send({
640
+ kind: "requestFrameChain",
641
+ payload: { storyId }
642
+ });
643
+ return reply.kind === "frameChainResult" ? reply.payload.links : [];
644
+ },
273
645
  onDidChange(listener) {
646
+ requireDocRead("document.onDidChange");
274
647
  const off = getEditor().client.subscribe((msg) => {
275
648
  if (msg.kind === "mutationApplied" || msg.kind === "undoApplied" || msg.kind === "redoApplied") {
276
- listener({ kind: msg.kind, pageIds: msg.payload.pageIds });
649
+ const reflow = msg.kind === "mutationApplied" ? msg.payload.reflow : void 0;
650
+ listener({
651
+ kind: msg.kind,
652
+ pageIds: msg.payload.pageIds,
653
+ ...reflow ? { reflow: { frameId: reflow.frameId, contentBox: reflow.contentBox } } : {}
654
+ });
277
655
  }
278
656
  });
279
657
  return store.add(toDisposable(off));
@@ -284,6 +662,11 @@ function createBundleHost(getEditor, manifest, options) {
284
662
  return getEditor().selection.elementSelection;
285
663
  },
286
664
  async set(ids, mode = "replace") {
665
+ requireDeclared(
666
+ hasDoc("write"),
667
+ "selection.set",
668
+ "capabilities.document.write"
669
+ );
287
670
  const editor = getEditor();
288
671
  const applied = await editor.client.setElementSelection(ids, mode);
289
672
  editor.selection.setElementSelection(applied);
@@ -308,8 +691,26 @@ function createBundleHost(getEditor, manifest, options) {
308
691
  return px / (scale > 0 ? scale : 1);
309
692
  }
310
693
  };
694
+ const text = {
695
+ async measureString(family, style, str, sizePt) {
696
+ const editorText = getEditor().text;
697
+ if (editorText) {
698
+ return editorText.measure(family, style, str, sizePt);
699
+ }
700
+ return {
701
+ advance: str.length * sizePt * 0.5,
702
+ ascender: sizePt * 0.8,
703
+ descender: -sizePt * 0.2
704
+ };
705
+ }
706
+ };
311
707
  const overlay = {
312
708
  setToolPreview(shape) {
709
+ requireDeclared(
710
+ hasRendering("overlay"),
711
+ "overlay.setToolPreview",
712
+ 'capabilities.rendering must include "overlay"'
713
+ );
313
714
  getEditor().overlaySignals.setToolPreview(shape);
314
715
  }
315
716
  };
@@ -335,6 +736,98 @@ function createBundleHost(getEditor, manifest, options) {
335
736
  return backing.keys().filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
336
737
  }
337
738
  };
739
+ const declaredNetwork = manifest.capabilities?.network;
740
+ const networkDeclared = declaredNetwork === true || typeof declaredNetwork === "object" && declaredNetwork !== null;
741
+ const mayRequest = (origin) => {
742
+ if (declaredNetwork === true) return true;
743
+ if (typeof declaredNetwork === "object" && declaredNetwork !== null) {
744
+ const o = declaredNetwork.origins;
745
+ return o === "consent" || Array.isArray(o) && o.includes(origin);
746
+ }
747
+ return false;
748
+ };
749
+ const CONSENT_KEY = "network.consentedOrigins";
750
+ const granted = new Set(storage.get(CONSENT_KEY) ?? []);
751
+ const network = {
752
+ async requestConsent(origins, purpose) {
753
+ requireDeclared(
754
+ networkDeclared,
755
+ "network.requestConsent",
756
+ "capabilities.network must declare the network capability (boolean or { origins })"
757
+ );
758
+ const inScope = origins.filter(mayRequest);
759
+ const outOfScope = origins.filter((o) => !mayRequest(o));
760
+ if (outOfScope.length > 0) {
761
+ log.warn(
762
+ `network.requestConsent: ${outOfScope.length} origin(s) outside the declared capabilities.network allow-list \u2014 denied: ${outOfScope.join(", ")}`
763
+ );
764
+ }
765
+ const need = inScope.filter((o) => !granted.has(o));
766
+ let prompted = { granted: [], denied: [], remembered: false };
767
+ if (need.length > 0) {
768
+ if (options?.consent) {
769
+ prompted = await options.consent.request(need, purpose);
770
+ } else {
771
+ log.warn(
772
+ "network.requestConsent: no consent backend wired \u2014 denying (supports('network.consent@1') is false; the editor injects one)"
773
+ );
774
+ prompted = { granted: [], denied: need, remembered: false };
775
+ }
776
+ }
777
+ for (const o of prompted.granted) granted.add(o);
778
+ if (prompted.remembered) storage.set(CONSENT_KEY, [...granted]);
779
+ return {
780
+ granted: inScope.filter((o) => granted.has(o)),
781
+ denied: [...outOfScope, ...inScope.filter((o) => !granted.has(o))],
782
+ remembered: prompted.remembered
783
+ };
784
+ },
785
+ consentedOrigins() {
786
+ return [...granted];
787
+ }
788
+ };
789
+ const dpCap = manifest.capabilities?.dataProviders;
790
+ const mayPublish = (category) => Array.isArray(dpCap?.publish) && dpCap.publish.includes(category);
791
+ const mayConsume = (category) => Array.isArray(dpCap?.consume) && (category === void 0 || dpCap.consume.includes(category));
792
+ const consumeDeclared = Array.isArray(dpCap?.consume) && dpCap.consume.length > 0;
793
+ const dataProviders = {
794
+ register(registration) {
795
+ requireDeclared(
796
+ mayPublish(registration.category),
797
+ "dataProviders.register",
798
+ `capabilities.dataProviders.publish must include "${registration.category}"`
799
+ );
800
+ if (!options?.dataProviders) {
801
+ log.warn(
802
+ `dataProviders.register("${registration.id}") \u2014 no shared registry wired (supports('dataProviders@1') is false; the editor injects one)`
803
+ );
804
+ return { update() {
805
+ }, dispose() {
806
+ } };
807
+ }
808
+ return options.dataProviders.register(registration);
809
+ },
810
+ discover(category) {
811
+ requireDeclared(
812
+ mayConsume(category),
813
+ "dataProviders.discover",
814
+ "capabilities.dataProviders.consume must include the category"
815
+ );
816
+ return options?.dataProviders?.discover(category) ?? [];
817
+ },
818
+ async get(id) {
819
+ requireDeclared(
820
+ consumeDeclared,
821
+ "dataProviders.get",
822
+ "capabilities.dataProviders.consume must be declared"
823
+ );
824
+ return await options?.dataProviders?.get(id) ?? null;
825
+ },
826
+ onDidChange(id, listener) {
827
+ if (!options?.dataProviders) return toDisposable(() => void 0);
828
+ return store.add(options.dataProviders.onDidChange(id, listener));
829
+ }
830
+ };
338
831
  const diagnosticStore = /* @__PURE__ */ new Map();
339
832
  const diagnosticListeners = /* @__PURE__ */ new Set();
340
833
  const emitDiagnostics = (key) => {
@@ -349,11 +842,13 @@ function createBundleHost(getEditor, manifest, options) {
349
842
  else if (d.severity === "warning") sink.warn(line);
350
843
  else sink.info(line);
351
844
  }
845
+ options?.diagnosticsSink?.publish(manifest.id, key, items);
352
846
  emitDiagnostics(key);
353
847
  },
354
848
  clear(key) {
355
849
  if (key !== void 0) diagnosticStore.delete(key);
356
850
  else diagnosticStore.clear();
851
+ options?.diagnosticsSink?.clear(manifest.id, key);
357
852
  emitDiagnostics(key ?? "");
358
853
  },
359
854
  get(key) {
@@ -364,6 +859,27 @@ function createBundleHost(getEditor, manifest, options) {
364
859
  return store.add(toDisposable(() => diagnosticListeners.delete(listener)));
365
860
  }
366
861
  };
862
+ const bindingStore = /* @__PURE__ */ new Map();
863
+ const bindingListeners = /* @__PURE__ */ new Set();
864
+ const emitBinding = (name) => {
865
+ for (const l of bindingListeners) l(name);
866
+ };
867
+ const bindings = {
868
+ publish(name, value) {
869
+ bindingStore.set(name, value);
870
+ emitBinding(name);
871
+ },
872
+ get(name) {
873
+ return bindingStore.get(name);
874
+ },
875
+ delete(name) {
876
+ if (bindingStore.delete(name)) emitBinding(name);
877
+ },
878
+ onDidChange(listener) {
879
+ bindingListeners.add(listener);
880
+ return store.add(toDisposable(() => bindingListeners.delete(listener)));
881
+ }
882
+ };
367
883
  const shell = options?.shell ?? {
368
884
  openPanel(panelId) {
369
885
  log.warn(
@@ -371,11 +887,119 @@ function createBundleHost(getEditor, manifest, options) {
371
887
  );
372
888
  },
373
889
  closePanel() {
890
+ },
891
+ async pickFile() {
892
+ log.warn(
893
+ `shell.pickFile() ignored \u2014 the host app provided no shell actions (probe with supports("shell.pickFile@1"))`
894
+ );
895
+ return [];
896
+ }
897
+ };
898
+ const widgets = options?.widgets ?? FALLBACK_WIDGETS;
899
+ const assetSource = options?.assetSource;
900
+ const assets = {
901
+ async getFontFace(family, style) {
902
+ requireDeclared(
903
+ hasAsset("fonts"),
904
+ "assets.getFontFace",
905
+ 'capabilities.assets must include "fonts"'
906
+ );
907
+ if (!assetSource) return null;
908
+ let face;
909
+ try {
910
+ face = await assetSource.getFontFace(family, style);
911
+ } catch {
912
+ return null;
913
+ }
914
+ if (!face) return null;
915
+ if (face.bytes.byteLength > ASSET_BUDGETS.maxFontFaceBytes) {
916
+ log.warn(
917
+ `assets.getFontFace("${family}"${style ? `, "${style}"` : ""}) served ${face.bytes.byteLength} bytes, over the ${ASSET_BUDGETS.maxFontFaceBytes}-byte per-face cap \u2014 refused`
918
+ );
919
+ return null;
920
+ }
921
+ return face;
922
+ }
923
+ };
924
+ const blobBackend = options?.blobStore;
925
+ const blobQuota = Math.min(
926
+ BLOB_BUDGETS.defaultQuotaBytes,
927
+ caps?.storage?.quotaBytes ?? BLOB_BUDGETS.defaultQuotaBytes
928
+ );
929
+ const blobGate = (door) => requireDeclared(
930
+ hasBlobStore(),
931
+ door,
932
+ "capabilities.storage must include { blob: true }"
933
+ );
934
+ const blob = {
935
+ async write(key, bytes) {
936
+ blobGate("blob.write");
937
+ if (!blobBackend) {
938
+ throw new Error(
939
+ `host.blob.write("${key}") \u2014 no blob store wired (supports("storage.blob@1") is false; the editor injects one)`
940
+ );
941
+ }
942
+ await blobBackend.delete(manifest.id, key);
943
+ const used = await blobBackend.used(manifest.id);
944
+ if (used + bytes.byteLength > blobQuota) {
945
+ throw new Error(
946
+ `host.blob.write("${key}") \u2014 ${bytes.byteLength} bytes would exceed the ${blobQuota}-byte quota (used ${used}) \u2014 refused`
947
+ );
948
+ }
949
+ await blobBackend.write(manifest.id, key, bytes);
950
+ },
951
+ async read(key) {
952
+ blobGate("blob.read");
953
+ if (!blobBackend) return null;
954
+ return blobBackend.read(manifest.id, key);
955
+ },
956
+ async delete(key) {
957
+ blobGate("blob.delete");
958
+ if (!blobBackend) return;
959
+ await blobBackend.delete(manifest.id, key);
960
+ },
961
+ async keys() {
962
+ blobGate("blob.keys");
963
+ if (!blobBackend) return [];
964
+ return blobBackend.keys(manifest.id);
965
+ },
966
+ async usage() {
967
+ blobGate("blob.usage");
968
+ if (!blobBackend) return { used: 0, quota: 0 };
969
+ return { used: await blobBackend.used(manifest.id), quota: blobQuota };
374
970
  }
375
971
  };
376
972
  const featureSet = new Set(HOST_FEATURES);
973
+ if (getEditor().text) {
974
+ featureSet.add("text.measure@1");
975
+ }
976
+ if (getEditor().sceneLayers) {
977
+ featureSet.add("rendering.sceneLayer@1");
978
+ }
377
979
  if (options?.shell) {
378
980
  featureSet.add("shell.openPanel@1");
981
+ featureSet.add("shell.pickFile@1");
982
+ }
983
+ if (options?.widgets) {
984
+ featureSet.add("widgets.codeEditor@1");
985
+ }
986
+ if (options?.schemaPanelRenderer) {
987
+ featureSet.add("schemaPanel.renderer@1");
988
+ }
989
+ if (options?.diagnosticsSink) {
990
+ featureSet.add("diagnostics.publish@1");
991
+ }
992
+ if (options?.assetSource) {
993
+ featureSet.add("assets.fonts@1");
994
+ }
995
+ if (options?.consent) {
996
+ featureSet.add("network.consent@1");
997
+ }
998
+ if (options?.dataProviders) {
999
+ featureSet.add("dataProviders@1");
1000
+ }
1001
+ if (options?.blobStore) {
1002
+ featureSet.add("storage.blob@1");
379
1003
  }
380
1004
  const host = {
381
1005
  manifest,
@@ -384,10 +1008,17 @@ function createBundleHost(getEditor, manifest, options) {
384
1008
  document,
385
1009
  selection,
386
1010
  viewport,
1011
+ text,
387
1012
  overlay,
388
1013
  shell,
389
1014
  storage,
1015
+ blob,
1016
+ network,
1017
+ dataProviders,
390
1018
  diagnostics,
1019
+ bindings,
1020
+ widgets,
1021
+ assets,
391
1022
  supports: (feature) => featureSet.has(feature),
392
1023
  get editor() {
393
1024
  return getEditor();
@@ -401,6 +1032,538 @@ function createBundleHost(getEditor, manifest, options) {
401
1032
  };
402
1033
  }
403
1034
 
1035
+ // src/version.ts
1036
+ var API_VERSION = "0.2.0";
1037
+ function parse(v) {
1038
+ const m = /^(\d+)\.(\d+)(?:\.(\d+))?$/.exec(v.trim());
1039
+ if (!m) return null;
1040
+ return [Number(m[1]), Number(m[2]), Number(m[3] ?? "0")];
1041
+ }
1042
+ function satisfiesApiVersion(range, version = API_VERSION) {
1043
+ const r = range.trim();
1044
+ if (r === "*") return true;
1045
+ const v = parse(version);
1046
+ if (!v) return false;
1047
+ if (r.startsWith("^")) {
1048
+ const base = parse(r.slice(1));
1049
+ if (!base) return false;
1050
+ if (base[0] > 0) {
1051
+ if (v[0] !== base[0]) return false;
1052
+ if (v[1] !== base[1]) return v[1] > base[1];
1053
+ return v[2] >= base[2];
1054
+ }
1055
+ return v[0] === 0 && v[1] === base[1] && v[2] >= base[2];
1056
+ }
1057
+ const exact = parse(r);
1058
+ if (!exact) return false;
1059
+ return v[0] === exact[0] && v[1] === exact[1] && v[2] === exact[2];
1060
+ }
1061
+
1062
+ // src/wasm-loader.ts
1063
+ var CANVAS_WASM_PKG = "@paged-media/canvas-wasm";
1064
+ async function nodeBuiltins() {
1065
+ const [{ createRequire }, { readFileSync }, path, url] = await Promise.all([
1066
+ import("module"),
1067
+ import("fs"),
1068
+ import("path"),
1069
+ import("url")
1070
+ ]);
1071
+ return {
1072
+ createRequire,
1073
+ readFileSync,
1074
+ dirname: path.dirname,
1075
+ resolve: path.resolve,
1076
+ fileURLToPath: url.fileURLToPath,
1077
+ pathToFileURL: url.pathToFileURL
1078
+ };
1079
+ }
1080
+ var STAMP_PREFIX = `// Synced from ${CANVAS_WASM_PKG}@`;
1081
+ async function modulePaths() {
1082
+ const { dirname, resolve, fileURLToPath } = await nodeBuiltins();
1083
+ const here = dirname(fileURLToPath(import.meta.url));
1084
+ return {
1085
+ here,
1086
+ wireDts: resolve(here, "../../plugin-api/src/wire.d.ts")
1087
+ };
1088
+ }
1089
+ async function readVendoredWireVersion(wireDtsPath) {
1090
+ const { readFileSync } = await nodeBuiltins();
1091
+ const path = wireDtsPath ?? (await modulePaths()).wireDts;
1092
+ let text;
1093
+ try {
1094
+ text = readFileSync(path, "utf8");
1095
+ } catch {
1096
+ return null;
1097
+ }
1098
+ for (const line of text.split("\n", 8)) {
1099
+ if (line.startsWith(STAMP_PREFIX)) {
1100
+ return line.slice(STAMP_PREFIX.length).trim();
1101
+ }
1102
+ }
1103
+ return null;
1104
+ }
1105
+ function protocolFromVersion(version) {
1106
+ const m = /^\d+\.(\d+)\.\d+/.exec(version.trim());
1107
+ return m ? Number(m[1]) : null;
1108
+ }
1109
+ async function resolveCanvasWasm(resolveFrom) {
1110
+ const { createRequire, readFileSync, dirname, resolve, pathToFileURL } = await nodeBuiltins();
1111
+ const { here } = await modulePaths();
1112
+ const anchors = [
1113
+ resolveFrom,
1114
+ here,
1115
+ resolve(here, "../../../../editor/packages/client"),
1116
+ resolve(here, "../../..")
1117
+ ].filter((a) => Boolean(a));
1118
+ let lastErr;
1119
+ for (const anchor of anchors) {
1120
+ try {
1121
+ const req = createRequire(pathToFileURL(resolve(anchor, "package.json")));
1122
+ const pkgJsonPath = req.resolve(`${CANVAS_WASM_PKG}/package.json`);
1123
+ const dir = dirname(pkgJsonPath);
1124
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
1125
+ const loaderPath = resolve(dir, pkg.main ?? "paged_canvas_wasm.js");
1126
+ const wasmPath = resolve(dir, "paged_canvas_wasm_bg.wasm");
1127
+ return {
1128
+ loaderUrl: pathToFileURL(loaderPath).href,
1129
+ wasmPath,
1130
+ version: pkg.version ?? "unknown",
1131
+ dir
1132
+ };
1133
+ } catch (err) {
1134
+ lastErr = err;
1135
+ }
1136
+ }
1137
+ throw new Error(
1138
+ `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)}`
1139
+ );
1140
+ }
1141
+ async function loadHeadlessEngine(options = {}) {
1142
+ const { readFileSync } = await nodeBuiltins();
1143
+ const { loaderUrl, wasmPath, version } = await resolveCanvasWasm(
1144
+ options.resolveFrom
1145
+ );
1146
+ const mod = await import(loaderUrl);
1147
+ const bytes = readFileSync(wasmPath);
1148
+ mod.initSync({
1149
+ module: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
1150
+ });
1151
+ const worker = new mod.CanvasWorker();
1152
+ const stampedVersion = await readVendoredWireVersion();
1153
+ const expectedProtocol = options.expectedProtocol ?? (stampedVersion ? protocolFromVersion(stampedVersion) : null);
1154
+ if (expectedProtocol !== null && worker.protocolVersion !== expectedProtocol) {
1155
+ const booted = worker.protocolVersion;
1156
+ try {
1157
+ worker.free();
1158
+ } catch {
1159
+ }
1160
+ throw new Error(
1161
+ `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.`
1162
+ );
1163
+ }
1164
+ return { worker, version, protocolVersion: worker.protocolVersion };
1165
+ }
1166
+
1167
+ // src/harness.ts
1168
+ function inMemoryBlobStore() {
1169
+ const byPlugin = /* @__PURE__ */ new Map();
1170
+ const dir = (id) => {
1171
+ let d = byPlugin.get(id);
1172
+ if (!d) byPlugin.set(id, d = /* @__PURE__ */ new Map());
1173
+ return d;
1174
+ };
1175
+ return {
1176
+ async write(id, key, bytes) {
1177
+ dir(id).set(key, bytes.slice());
1178
+ },
1179
+ async read(id, key) {
1180
+ const v = dir(id).get(key);
1181
+ return v ? v.slice() : null;
1182
+ },
1183
+ async delete(id, key) {
1184
+ dir(id).delete(key);
1185
+ },
1186
+ async keys(id) {
1187
+ return Array.from(dir(id).keys());
1188
+ },
1189
+ async used(id) {
1190
+ let n = 0;
1191
+ for (const v of dir(id).values()) n += v.byteLength;
1192
+ return n;
1193
+ }
1194
+ };
1195
+ }
1196
+ var seqCounter = 1;
1197
+ function makeEngineEditor(worker, recorder, onToolPreview) {
1198
+ const protocol = worker.protocolVersion;
1199
+ const listeners = /* @__PURE__ */ new Set();
1200
+ const fanOut = (reply) => {
1201
+ for (const l of listeners) l(reply);
1202
+ };
1203
+ const dispatch = (kind, payload) => {
1204
+ const envelope = JSON.stringify(
1205
+ payload === void 0 ? { seq: seqCounter++, protocol, kind } : { seq: seqCounter++, protocol, kind, payload }
1206
+ );
1207
+ const raw = worker.handleMessage(envelope);
1208
+ return JSON.parse(raw);
1209
+ };
1210
+ const recordingRegistry = (kind) => ({
1211
+ register(contribution) {
1212
+ const entry = {
1213
+ kind,
1214
+ id: contribution.id,
1215
+ value: contribution
1216
+ };
1217
+ recorder.push(entry);
1218
+ return {
1219
+ dispose() {
1220
+ const i = recorder.indexOf(entry);
1221
+ if (i >= 0) recorder.splice(i, 1);
1222
+ }
1223
+ };
1224
+ }
1225
+ });
1226
+ let keybindingSeq = 0;
1227
+ const keybindingRegistry = {
1228
+ register(contribution) {
1229
+ const entry = {
1230
+ kind: "keybinding",
1231
+ id: `${contribution.command}#${keybindingSeq++}`,
1232
+ value: contribution
1233
+ };
1234
+ recorder.push(entry);
1235
+ return {
1236
+ dispose() {
1237
+ const i = recorder.indexOf(entry);
1238
+ if (i >= 0) recorder.splice(i, 1);
1239
+ }
1240
+ };
1241
+ }
1242
+ };
1243
+ let elementSelection = [];
1244
+ let toolPreview = null;
1245
+ const client = {
1246
+ async mutate(mutation) {
1247
+ const reply = dispatch("mutate", mutation);
1248
+ fanOut(reply);
1249
+ return reply;
1250
+ },
1251
+ async undo() {
1252
+ const reply = dispatch("undo");
1253
+ fanOut(reply);
1254
+ return reply;
1255
+ },
1256
+ async redo() {
1257
+ const reply = dispatch("redo");
1258
+ fanOut(reply);
1259
+ return reply;
1260
+ },
1261
+ async collection(name) {
1262
+ const reply = dispatch("requestCollection", { name });
1263
+ if (reply.kind === "collectionReply") {
1264
+ const items = reply.payload.items;
1265
+ return Array.isArray(items) ? items : [];
1266
+ }
1267
+ return [];
1268
+ },
1269
+ async documentMeta() {
1270
+ const reply = dispatch("requestDocumentMeta");
1271
+ if (reply.kind === "documentMetaReply") {
1272
+ return reply.payload.meta;
1273
+ }
1274
+ throw new Error(`unexpected reply: ${reply.kind}`);
1275
+ },
1276
+ async pathAnchors(id) {
1277
+ const reply = dispatch("requestPathAnchors", { id });
1278
+ if (reply.kind === "pathAnchors") {
1279
+ return reply.payload.result;
1280
+ }
1281
+ return null;
1282
+ },
1283
+ async elementGeometry(ids) {
1284
+ const reply = dispatch("requestElementGeometry", { ids });
1285
+ if (reply.kind === "elementGeometry") {
1286
+ return reply.payload.items;
1287
+ }
1288
+ return [];
1289
+ },
1290
+ async setElementSelection(ids, mode) {
1291
+ const reply = dispatch("setElementSelection", { ids, mode });
1292
+ if (reply.kind === "elementSelectionApplied") {
1293
+ fanOut(reply);
1294
+ return reply.payload.ids;
1295
+ }
1296
+ throw new Error(`unexpected reply: ${reply.kind}`);
1297
+ },
1298
+ async send(message) {
1299
+ const m = message;
1300
+ const reply = dispatch(m.kind, m.payload);
1301
+ return reply;
1302
+ },
1303
+ subscribe(listener) {
1304
+ listeners.add(listener);
1305
+ return () => listeners.delete(listener);
1306
+ }
1307
+ };
1308
+ const editor = {
1309
+ client,
1310
+ registries: {
1311
+ tools: recordingRegistry("tool"),
1312
+ panels: recordingRegistry("panel"),
1313
+ commands: recordingRegistry("command"),
1314
+ keybindings: keybindingRegistry,
1315
+ overlays: recordingRegistry("overlay"),
1316
+ importers: recordingRegistry("importer"),
1317
+ exporters: recordingRegistry("exporter")
1318
+ },
1319
+ selection: {
1320
+ get elementSelection() {
1321
+ return elementSelection;
1322
+ },
1323
+ setElementSelection(ids) {
1324
+ elementSelection = ids;
1325
+ },
1326
+ setElementGeometry() {
1327
+ }
1328
+ },
1329
+ camera: { camera: { scale: 1, tx: 0, ty: 0 } },
1330
+ overlaySignals: {
1331
+ setToolPreview(value) {
1332
+ toolPreview = value;
1333
+ onToolPreview(toolPreview);
1334
+ }
1335
+ },
1336
+ // No tool spine + no content caret headlessly — both are inert
1337
+ // members of the narrow handle, present so the cast is total.
1338
+ tool: {
1339
+ setBaseTool() {
1340
+ }
1341
+ },
1342
+ contentSelection: { contentSelection: null }
1343
+ };
1344
+ return editor;
1345
+ }
1346
+ async function createHeadlessHost(options = {}) {
1347
+ const engine = await loadHeadlessEngine(options);
1348
+ const worker = engine.worker;
1349
+ const contributions = [];
1350
+ let lastPreview = null;
1351
+ const editor = makeEngineEditor(worker, contributions, (value) => {
1352
+ lastPreview = value;
1353
+ });
1354
+ let active = null;
1355
+ let currentHost = null;
1356
+ let disposed = false;
1357
+ const blobStore = options.blobStore ?? inMemoryBlobStore();
1358
+ const buildHost = (manifest, mode) => createBundleHost(() => editor, manifest, {
1359
+ console: options.console,
1360
+ storage: options.storage,
1361
+ blobStore,
1362
+ capabilityMode: mode,
1363
+ // W-06 — a recordable fake asset source the conformance harness
1364
+ // can pass so a bundle's `@font-face` byte path is exercisable
1365
+ // headlessly (the editor's real adapter currently serves null;
1366
+ // DESIGN.md §13.4). Absent → the no-bytes door.
1367
+ assetSource: options.assetSource,
1368
+ // Record the SCHEMA verbatim at registration — the panel registry
1369
+ // only ever sees the synthesized React panel, so the conformance
1370
+ // log gets the schema through this adapter seam (no host renderer
1371
+ // headlessly; visibility/enabled gates are asserted off `bindings`
1372
+ // directly, not through a mounted UI).
1373
+ onSchemaPanelRegistered: (c) => {
1374
+ const entry = {
1375
+ kind: "schemaPanel",
1376
+ id: c.id,
1377
+ value: c
1378
+ };
1379
+ contributions.push(entry);
1380
+ return {
1381
+ dispose() {
1382
+ const i = contributions.indexOf(entry);
1383
+ if (i >= 0) contributions.splice(i, 1);
1384
+ }
1385
+ };
1386
+ },
1387
+ // W3.2 — the editContext/objectType registries are not wired
1388
+ // headlessly (no shell stack / chrome), so the adapter takes the
1389
+ // recording-stub path and these hooks ARE the registration log.
1390
+ onEditContextRegistered: (c) => {
1391
+ const entry = {
1392
+ kind: "editContext",
1393
+ id: c.type,
1394
+ value: c
1395
+ };
1396
+ contributions.push(entry);
1397
+ return {
1398
+ dispose() {
1399
+ const i = contributions.indexOf(entry);
1400
+ if (i >= 0) contributions.splice(i, 1);
1401
+ }
1402
+ };
1403
+ },
1404
+ onObjectTypeRegistered: (c) => {
1405
+ const entry = {
1406
+ kind: "objectType",
1407
+ id: c.type,
1408
+ value: c
1409
+ };
1410
+ contributions.push(entry);
1411
+ return {
1412
+ dispose() {
1413
+ const i = contributions.indexOf(entry);
1414
+ if (i >= 0) contributions.splice(i, 1);
1415
+ }
1416
+ };
1417
+ }
1418
+ });
1419
+ const NEUTRAL = {
1420
+ id: "media.paged.harness",
1421
+ name: "harness",
1422
+ version: "0.0.0",
1423
+ apiVersion: `^${API_VERSION.slice(0, 3)}`,
1424
+ capabilities: {
1425
+ document: { read: "broad", write: "broad" },
1426
+ rendering: ["overlay", "hitTest", "sceneLayer"],
1427
+ keybindings: true,
1428
+ storage: { blob: true }
1429
+ },
1430
+ // Broad contribution declarations so the neutral DRIVER host (which
1431
+ // registers arbitrary contributions directly in 'warn' mode) never
1432
+ // trips the capability gate. A loaded BUNDLE is the subject — its
1433
+ // OWN manifest is enforced.
1434
+ contributes: {
1435
+ editContexts: [
1436
+ { type: "vectorGraphic", entry: "doubleClick" },
1437
+ { type: "webFrame", entry: "doubleClick" }
1438
+ ],
1439
+ objectTypes: [{ type: "webFrame", bakedFallback: "rectangle" }],
1440
+ importers: ["media.paged.harness.importer.xlsx"],
1441
+ exporters: ["media.paged.harness.exporter.xlsx"]
1442
+ }
1443
+ };
1444
+ let { host, dispose: disposeHostFacades } = buildHost(NEUTRAL, "warn");
1445
+ currentHost = host;
1446
+ const headless = {
1447
+ get host() {
1448
+ return currentHost;
1449
+ },
1450
+ engineVersion: engine.version,
1451
+ protocolVersion: engine.protocolVersion,
1452
+ contributions,
1453
+ toolsContributed() {
1454
+ return contributions.filter((c) => c.kind === "tool").map((c) => c.value);
1455
+ },
1456
+ panelsContributed() {
1457
+ return contributions.filter((c) => c.kind === "panel").map((c) => c.value);
1458
+ },
1459
+ schemaPanelsContributed() {
1460
+ return contributions.filter((c) => c.kind === "schemaPanel").map((c) => c.value);
1461
+ },
1462
+ editContextsContributed() {
1463
+ return contributions.filter((c) => c.kind === "editContext").map((c) => c.value);
1464
+ },
1465
+ objectTypesContributed() {
1466
+ return contributions.filter((c) => c.kind === "objectType").map((c) => c.value);
1467
+ },
1468
+ importersContributed() {
1469
+ return contributions.filter((c) => c.kind === "importer").map((c) => c.value);
1470
+ },
1471
+ exportersContributed() {
1472
+ return contributions.filter((c) => c.kind === "exporter").map((c) => c.value);
1473
+ },
1474
+ lastToolPreview() {
1475
+ return lastPreview;
1476
+ },
1477
+ async load(idml) {
1478
+ const raw = worker.loadDocumentDirect(seqCounter++, idml);
1479
+ const reply = JSON.parse(raw);
1480
+ if (reply.kind === "documentLoaded") {
1481
+ try {
1482
+ worker.runResolveJson();
1483
+ } catch {
1484
+ }
1485
+ return reply.payload.pageIds;
1486
+ }
1487
+ const errKind = reply.kind === "loadFailed" ? reply.payload.error.kind : reply.kind;
1488
+ throw new Error(`headless load failed (${reply.kind}: ${errKind})`);
1489
+ },
1490
+ loadBundle(bundle) {
1491
+ if (active) {
1492
+ throw new Error(
1493
+ "headless host: a bundle is already loaded \u2014 dispose it first (one bundle per headless host in v1)"
1494
+ );
1495
+ }
1496
+ const { manifest } = bundle;
1497
+ if (!satisfiesApiVersion(manifest.apiVersion)) {
1498
+ throw new Error(
1499
+ `headless host: ${manifest.id}@${manifest.version} requires plugin-api "${manifest.apiVersion}", host implements ${API_VERSION}`
1500
+ );
1501
+ }
1502
+ disposeHostFacades();
1503
+ ({ host, dispose: disposeHostFacades } = buildHost(
1504
+ manifest,
1505
+ options.capabilityMode ?? "enforce"
1506
+ ));
1507
+ currentHost = host;
1508
+ const handle = bundle.activate(host);
1509
+ let bundleActive = true;
1510
+ active = {
1511
+ dispose() {
1512
+ if (!bundleActive) return;
1513
+ bundleActive = false;
1514
+ try {
1515
+ handle.dispose();
1516
+ } finally {
1517
+ disposeHostFacades();
1518
+ ({ host, dispose: disposeHostFacades } = buildHost(
1519
+ NEUTRAL,
1520
+ "warn"
1521
+ ));
1522
+ currentHost = host;
1523
+ active = null;
1524
+ }
1525
+ }
1526
+ };
1527
+ return { dispose: () => active?.dispose() };
1528
+ },
1529
+ dispose() {
1530
+ if (disposed) return;
1531
+ disposed = true;
1532
+ try {
1533
+ if (active) active.dispose();
1534
+ else disposeHostFacades();
1535
+ } finally {
1536
+ contributions.length = 0;
1537
+ lastPreview = null;
1538
+ worker.free();
1539
+ }
1540
+ }
1541
+ };
1542
+ return headless;
1543
+ }
1544
+
1545
+ // src/asset-source-fake.ts
1546
+ function createRecordableAssetSource(seeds = []) {
1547
+ const requests = [];
1548
+ return {
1549
+ requests,
1550
+ async getFontFace(family, style) {
1551
+ requests.push(style === void 0 ? { family } : { family, style });
1552
+ const key = family.trim().toLowerCase();
1553
+ for (const seed of seeds) {
1554
+ if (seed.family.trim().toLowerCase() !== key) continue;
1555
+ if (seed.matchStyle !== void 0 && seed.matchStyle !== style) {
1556
+ continue;
1557
+ }
1558
+ const { matchStyle: _unused, ...face } = seed;
1559
+ void _unused;
1560
+ return face;
1561
+ }
1562
+ return null;
1563
+ }
1564
+ };
1565
+ }
1566
+
404
1567
  // src/load.ts
405
1568
  var ID_PATTERN = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9-]*)+$/;
406
1569
  function loadBundle(getEditor, bundle, options) {
@@ -439,6 +1602,88 @@ function loadBundle(getEditor, bundle, options) {
439
1602
  };
440
1603
  }
441
1604
 
1605
+ // src/wasm-bundle-loader.ts
1606
+ var WASM_BUDGETS = {
1607
+ /** Hard per-artifact byte ceiling. A release-optimised wasm layout
1608
+ * engine (Blitz-class) lands in the low-single-digit MiB; 8 MiB
1609
+ * rejects an accidentally-bundled debug build while leaving headroom
1610
+ * for one real engine. A manifest `maxBytes` may only TIGHTEN this. */
1611
+ maxArtifactBytes: 8 * 1024 * 1024,
1612
+ /** Total declared wasm across one bundle. Bounds a bundle that ships
1613
+ * several modules (engine + a codec, say). */
1614
+ maxTotalBytes: 16 * 1024 * 1024,
1615
+ /** Wall-clock budget for fetch + compile + instantiate. Protects the
1616
+ * editor's main flow from a pathological module; advisory, the loader
1617
+ * aborts with a clear error when exceeded. */
1618
+ loadTimeBudgetMs: 3e3,
1619
+ /** Linear-memory growth ceiling, in 64 KiB wasm pages (4096 = 256 MiB).
1620
+ * Passed as `WebAssembly.Memory({ maximum })` when the host owns the
1621
+ * memory; a per-page layout pass should sit far under this. */
1622
+ maxMemoryPages: 4096
1623
+ };
1624
+ function findArtifact(bundle, name) {
1625
+ return bundle.manifest.capabilities?.wasm?.find((a) => a.name === name);
1626
+ }
1627
+ function isGranted(grant, name) {
1628
+ if (grant === void 0) return false;
1629
+ if (grant === "*") return true;
1630
+ if (Array.isArray(grant)) return grant.includes(name);
1631
+ return grant.has(name);
1632
+ }
1633
+ async function loadBundleWasm(bundle, name, options) {
1634
+ const id = bundle.manifest.id;
1635
+ const artifact = findArtifact(bundle, name);
1636
+ if (!artifact) {
1637
+ throw new Error(
1638
+ `loadBundleWasm: ${id} has no declared wasm artifact "${name}" \u2014 only artifacts listed in manifest.capabilities.wasm are loadable (declared-only).`
1639
+ );
1640
+ }
1641
+ if (!isGranted(options.grant, name)) {
1642
+ throw new Error(
1643
+ `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).`
1644
+ );
1645
+ }
1646
+ const now = options.now ?? (() => Date.now());
1647
+ const budgetMs = options.loadTimeBudgetMs ?? WASM_BUDGETS.loadTimeBudgetMs;
1648
+ const started = now();
1649
+ const overBudget = (stage) => {
1650
+ throw new Error(
1651
+ `loadBundleWasm: "${name}" of ${id} exceeded the ${budgetMs}ms load-time budget at ${stage} (${Math.round(now() - started)}ms).`
1652
+ );
1653
+ };
1654
+ const bytes = await options.assetSource(artifact.path);
1655
+ if (now() - started > budgetMs) overBudget("fetch");
1656
+ const src = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
1657
+ const view = new Uint8Array(src.byteLength);
1658
+ view.set(src);
1659
+ const ceiling = typeof artifact.maxBytes === "number" && artifact.maxBytes > 0 ? Math.min(artifact.maxBytes, WASM_BUDGETS.maxArtifactBytes) : WASM_BUDGETS.maxArtifactBytes;
1660
+ if (view.byteLength > ceiling) {
1661
+ throw new Error(
1662
+ `loadBundleWasm: "${name}" of ${id} is ${view.byteLength} bytes, over its ${ceiling}-byte ceiling.`
1663
+ );
1664
+ }
1665
+ let memory;
1666
+ const imports = { ...options.imports ?? {} };
1667
+ const provideMemory = options.provideMemory ?? true;
1668
+ if (provideMemory) {
1669
+ const env = { ...imports.env };
1670
+ if (!("memory" in env)) {
1671
+ memory = new WebAssembly.Memory({
1672
+ initial: options.initialMemoryPages ?? 16,
1673
+ maximum: WASM_BUDGETS.maxMemoryPages
1674
+ // shared is intentionally absent — non-shared memory only (v1).
1675
+ });
1676
+ env.memory = memory;
1677
+ }
1678
+ imports.env = env;
1679
+ }
1680
+ const module = await WebAssembly.compile(view);
1681
+ if (now() - started > budgetMs) overBudget("compile");
1682
+ const instance = await WebAssembly.instantiate(module, imports);
1683
+ if (now() - started > budgetMs) overBudget("instantiate");
1684
+ return { artifact, module, instance, memory, byteLength: view.byteLength };
1685
+ }
1686
+
442
1687
  // src/gestures.ts
443
1688
  var CLICK_DRAG_THRESHOLD_PX = 4;
444
1689
  function beginPageDrag(e) {
@@ -502,22 +1747,51 @@ function contributeTool(host, tool) {
502
1747
  function contributePanel(host, panel) {
503
1748
  return host.contribute.panel(panel);
504
1749
  }
1750
+ function contributeSchemaPanel(host, panel) {
1751
+ return host.contribute.schemaPanel(panel);
1752
+ }
1753
+
1754
+ // src/edit-context.ts
1755
+ function contributeEditContext(host, contribution) {
1756
+ return host.contribute.editContext(contribution);
1757
+ }
1758
+ function contributeObjectType(host, contribution) {
1759
+ return host.contribute.objectType(contribution);
1760
+ }
505
1761
  export {
506
1762
  API_VERSION,
1763
+ ASSET_BUDGETS,
1764
+ BLOB_BUDGETS,
1765
+ CANVAS_WASM_PKG,
507
1766
  CLICK_DRAG_THRESHOLD_PX,
508
1767
  DisposableStore,
1768
+ FALLBACK_WIDGETS,
509
1769
  HOST_FEATURES,
510
1770
  PluginApiNotImplemented,
1771
+ PluginCapabilityError,
1772
+ WASM_BUDGETS,
511
1773
  beginPageDrag,
512
1774
  commitAndSelect,
1775
+ contributeEditContext,
1776
+ contributeObjectType,
513
1777
  contributePanel,
1778
+ contributeSchemaPanel,
514
1779
  contributeTool,
515
1780
  createBundleHost,
1781
+ createDataProviderRegistry,
516
1782
  createHeadlessHost,
1783
+ createRecordableAssetSource,
517
1784
  defineBundle,
518
1785
  endLocalFor,
519
1786
  loadBundle,
1787
+ loadBundleWasm,
1788
+ loadHeadlessEngine,
1789
+ makeSchemaPanelComponent,
1790
+ protocolFromVersion,
520
1791
  pxToPt,
1792
+ readVendoredWireVersion,
1793
+ resolveCanvasWasm,
1794
+ resolveGate,
521
1795
  satisfiesApiVersion,
522
1796
  toDisposable
523
1797
  };