@paged-media/plugin-sdk 0.2.17-canary.0 → 0.2.19-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.
package/dist/index.d.ts CHANGED
@@ -98,6 +98,17 @@ declare const BLOB_BUDGETS: {
98
98
  /** Default per-plugin blob ceiling, in bytes (64 MiB). */
99
99
  readonly defaultQuotaBytes: number;
100
100
  };
101
+ /** Worker spawn + SAB budgets (K-3 / S-07). The host enforces the
102
+ * stricter of these and any manifest tightening. */
103
+ declare const WORKER_BUDGETS: {
104
+ /** Hard worker-count cap per bundle. The grant is `min(declared.max,
105
+ * hardwareConcurrency, maxWorkers)` — a runaway `max` can't exhaust
106
+ * the machine. */
107
+ readonly maxWorkers: 8;
108
+ /** Default per-bundle shared-memory ceiling, in bytes (256 MiB). A
109
+ * manifest's `maxSharedBytes` may only TIGHTEN this. */
110
+ readonly defaultSharedBytes: number;
111
+ };
101
112
  /**
102
113
  * The backend the editor injects to back `host.blob` (K-4 / S-08): a
103
114
  * RAW per-plugin byte store (OPFS in-browser; an in-memory map in the
@@ -113,6 +124,60 @@ interface BlobStore {
113
124
  /** Total bytes this plugin currently stores. */
114
125
  used(pluginId: string): Promise<number>;
115
126
  }
127
+ /**
128
+ * A raw spawned worker the `WorkerBackend` hands back (K-3 / S-07). The
129
+ * SDK adapter owns the capability gate, the count cap, the SAB budget,
130
+ * and teardown tracking; this backend only constructs the realm + does
131
+ * the raw message IO. `post` forwards to the underlying worker (optional
132
+ * transfer); `onMessage` subscribes (the backend fans out); `terminate`
133
+ * destroys the realm.
134
+ */
135
+ interface SpawnedWorker {
136
+ post(message: unknown, transfer?: Transferable[]): void;
137
+ onMessage(handler: (message: unknown) => void): Disposable;
138
+ terminate(): void;
139
+ }
140
+ /**
141
+ * The backend the editor injects to back `host.workers` (K-3 / S-07 /
142
+ * I-02): it resolves a bundle's DECLARED, bundle-relative `module` path
143
+ * through the bundle's own asset base (the `/@fs/`-allowed sibling-plugin
144
+ * path the wasm artifacts use) and constructs an ES-module `Worker`. The
145
+ * SDK adapter passes the plugin id + the declared module path; the
146
+ * backend never invents a URL. Returns a `SpawnedWorker` (or rejects when
147
+ * the module fails to resolve/construct). The SAB itself is allocated by
148
+ * the SDK adapter (a plain `new SharedArrayBuffer` under the host budget)
149
+ * — the backend's job is purely the worker realm. A headless host injects
150
+ * no backend → `spawn` rejects + `supports("workers@1")` is false.
151
+ */
152
+ interface WorkerBackend {
153
+ spawn(pluginId: string, module: string, name?: string): Promise<SpawnedWorker>;
154
+ }
155
+ /**
156
+ * The backend the editor injects to back `host.secrets` (D-11;
157
+ * rfc-credential-store): a REFERENCE-ONLY, host-owned credential store. The
158
+ * SDK adapter owns the capability gate + the namespacing (passes the plugin
159
+ * id); this backend owns the storage tier (WebCrypto-wrapped IndexedDB in
160
+ * the editor, an in-memory map in the headless harness) AND the user PROMPT
161
+ * — the RFC's "via host UI only": `set` resolves only after the host has
162
+ * the material (the editor backing prompts the user; the plugin's supplied
163
+ * `secret` is the value to STORE, never persisted silently by the adapter).
164
+ *
165
+ * The trust line is the ABSENCE of a read: there is `set`/`exists`/`forget`
166
+ * and NO `get` — secret bytes never leave the host realm. A headless host
167
+ * injects no backend → `set`/`forget` reject, `exists` is false,
168
+ * `supports("secrets@1")` is false. The plugin id + the (caller-supplied)
169
+ * `ref` together namespace the stored secret (`paged:<plugin-id>:<ref>`).
170
+ */
171
+ interface SecretStoreBackend {
172
+ /** Store `secret` for `pluginId` under `ref` (the editor backing PROMPTS
173
+ * the user — "via host UI only"). Rejects when the user declines or the
174
+ * store is unavailable. */
175
+ set(pluginId: string, ref: string, secret: string): Promise<void>;
176
+ /** Whether a secret is stored for `pluginId`/`ref`. */
177
+ exists(pluginId: string, ref: string): Promise<boolean>;
178
+ /** Forget the secret for `pluginId`/`ref` (idempotent). */
179
+ forget(pluginId: string, ref: string): Promise<void>;
180
+ }
116
181
  /**
117
182
  * The backend the editor injects to back `host.clipboard` (K-6 / S-14): a
118
183
  * thin read/write pair over the REAL system clipboard (`navigator.clipboard`
@@ -240,6 +305,22 @@ interface CreateBundleHostOptions {
240
305
  * When absent, `read` answers `null` and `write` is a no-op (the honest
241
306
  * no-clipboard door). */
242
307
  clipboard?: ClipboardBackend;
308
+ /** Host-provided WORKER backend (K-3 / S-07 / I-02). When present,
309
+ * `host.workers.spawn` resolves a declared bundle-relative module +
310
+ * constructs a host-owned `Worker` through it (capability-gated,
311
+ * count-capped, SAB-budgeted, teardown-tracked) and
312
+ * `supports("workers@1")` answers true. When absent, `spawn` rejects
313
+ * honestly, `concurrency()` is 0, and the feature flag is false (the
314
+ * honest no-worker door). */
315
+ workers?: WorkerBackend;
316
+ /** Host-provided CREDENTIAL-STORE backend (D-11; rfc-credential-store).
317
+ * When present, `host.secrets.set/exists/forget` go through it
318
+ * (capability-gated on `capabilities.secrets`; `set` prompts the user —
319
+ * "via host UI only") and `supports("secrets@1")` answers true. When
320
+ * absent, `set`/`forget` reject and `exists` is false (the honest
321
+ * no-store door). REFERENCE-ONLY: there is no `get` anywhere — secret
322
+ * bytes never enter the plugin realm. */
323
+ secrets?: SecretStoreBackend;
243
324
  /**
244
325
  * How the host treats a declaration↔use mismatch — a bundle that
245
326
  * USES a door (`contribute.tool`, `document.mutate`, …) it did not
@@ -334,7 +415,7 @@ interface RecordedContribution {
334
415
  id: string;
335
416
  value: ToolContribution | PanelContribution | SchemaPanelContribution | CommandContribution | KeybindingContribution | OverlayContribution | EditContextContribution | ObjectTypeContribution | ImporterContribution | ExporterContribution;
336
417
  }
337
- interface HarnessOptions extends LoadHeadlessEngineOptions, Pick<CreateBundleHostOptions, "console" | "storage" | "capabilityMode" | "assetSource" | "blobStore" | "clipboard"> {
418
+ interface HarnessOptions extends LoadHeadlessEngineOptions, Pick<CreateBundleHostOptions, "console" | "storage" | "capabilityMode" | "assetSource" | "blobStore" | "clipboard" | "secrets"> {
338
419
  }
339
420
  /** What `createHeadlessHost` resolves to: a real engine-backed host plus
340
421
  * the conformance affordances (load an IDML, read the contribution log,
@@ -603,4 +684,4 @@ declare function contributeEditContext(host: BundleHost, contribution: EditConte
603
684
  * descent. Capability-gated on `contributes.objectTypes`. */
604
685
  declare function contributeObjectType(host: BundleHost, contribution: ObjectTypeContribution): Disposable;
605
686
 
606
- export { API_VERSION, ASSET_BUDGETS, BLOB_BUDGETS, type BlobStore, type BundleAssetProvider, type BundleAssetSource, type BundleHostHandle, type BundleTrust, CANVAS_WASM_PKG, CLICK_DRAG_THRESHOLD_PX, type ClipboardBackend, type ConsentBackend, type CreateBundleHostOptions, type DataProviderBackend, type DiagnosticsSink, DisposableStore, FALLBACK_WIDGETS, HOST_FEATURES, type HarnessOptions, type HeadlessCanvasWorker, type HeadlessHost, type HeadlessHostHandle, type LoadBundleWasmOptions, type LoadHeadlessEngineOptions, type LoadedBundle, type LoadedBundleWasm, type LoadedEngine, type PageDrag, PluginApiNotImplemented, PluginCapabilityError, type RecordableAssetSource, type RecordedContribution, type RecordedFontFaceRequest, type SeededFace, type StorageBacking, WASM_BUDGETS, beginPageDrag, commitAndSelect, contributeEditContext, contributeObjectType, contributePanel, contributeSchemaPanel, contributeTool, createBundleHost, createDataProviderRegistry, createHeadlessHost, createRecordableAssetSource, defineBundle, endLocalFor, loadBundle, loadBundleWasm, loadHeadlessEngine, makeSchemaPanelComponent, protocolFromVersion, pxToPt, readVendoredWireVersion, resolveCanvasWasm, resolveGate, satisfiesApiVersion, toDisposable };
687
+ export { API_VERSION, ASSET_BUDGETS, BLOB_BUDGETS, type BlobStore, type BundleAssetProvider, type BundleAssetSource, type BundleHostHandle, type BundleTrust, CANVAS_WASM_PKG, CLICK_DRAG_THRESHOLD_PX, type ClipboardBackend, type ConsentBackend, type CreateBundleHostOptions, type DataProviderBackend, type DiagnosticsSink, DisposableStore, FALLBACK_WIDGETS, HOST_FEATURES, type HarnessOptions, type HeadlessCanvasWorker, type HeadlessHost, type HeadlessHostHandle, type LoadBundleWasmOptions, type LoadHeadlessEngineOptions, type LoadedBundle, type LoadedBundleWasm, type LoadedEngine, type PageDrag, PluginApiNotImplemented, PluginCapabilityError, type RecordableAssetSource, type RecordedContribution, type RecordedFontFaceRequest, type SecretStoreBackend, type SeededFace, type SpawnedWorker, type StorageBacking, WASM_BUDGETS, WORKER_BUDGETS, type WorkerBackend, beginPageDrag, commitAndSelect, contributeEditContext, contributeObjectType, contributePanel, contributeSchemaPanel, contributeTool, createBundleHost, createDataProviderRegistry, createHeadlessHost, createRecordableAssetSource, defineBundle, endLocalFor, loadBundle, loadBundleWasm, loadHeadlessEngine, makeSchemaPanelComponent, protocolFromVersion, pxToPt, readVendoredWireVersion, resolveCanvasWasm, resolveGate, satisfiesApiVersion, toDisposable };
package/dist/index.js CHANGED
@@ -201,6 +201,15 @@ var BLOB_BUDGETS = {
201
201
  /** Default per-plugin blob ceiling, in bytes (64 MiB). */
202
202
  defaultQuotaBytes: 64 * 1024 * 1024
203
203
  };
204
+ var WORKER_BUDGETS = {
205
+ /** Hard worker-count cap per bundle. The grant is `min(declared.max,
206
+ * hardwareConcurrency, maxWorkers)` — a runaway `max` can't exhaust
207
+ * the machine. */
208
+ maxWorkers: 8,
209
+ /** Default per-bundle shared-memory ceiling, in bytes (256 MiB). A
210
+ * manifest's `maxSharedBytes` may only TIGHTEN this. */
211
+ defaultSharedBytes: 256 * 1024 * 1024
212
+ };
204
213
  function createDataProviderRegistry() {
205
214
  const providers = /* @__PURE__ */ new Map();
206
215
  const listeners = /* @__PURE__ */ new Map();
@@ -273,6 +282,8 @@ function createBundleHost(getEditor, manifest, options) {
273
282
  const hasAsset = (k) => caps?.assets?.includes(k) ?? false;
274
283
  const hasBlobStore = () => caps?.storage?.blob === true;
275
284
  const clipboardGrant = () => caps?.clipboard === "full" ? "full" : caps?.clipboard === "vector" ? "vector" : "none";
285
+ const hasWorkers = () => typeof caps?.workers?.max === "number";
286
+ const hasSecrets = () => caps?.secrets?.sources === true;
276
287
  const lists = (arr, id) => arr?.includes(id) ?? false;
277
288
  const declaresType = (arr, type) => arr?.some((e) => e.type === type) ?? false;
278
289
  const requireDeclared = (ok, door, missing) => {
@@ -1158,6 +1169,159 @@ function createBundleHost(getEditor, manifest, options) {
1158
1169
  }
1159
1170
  }
1160
1171
  };
1172
+ const workerBackend = options?.workers;
1173
+ const declaredWorkers = caps?.workers;
1174
+ const hardwareConcurrency = globalThis.navigator?.hardwareConcurrency ?? 1;
1175
+ const workerCap = hasWorkers() && workerBackend ? Math.max(
1176
+ 0,
1177
+ Math.min(
1178
+ declaredWorkers?.max ?? 0,
1179
+ hardwareConcurrency,
1180
+ WORKER_BUDGETS.maxWorkers
1181
+ )
1182
+ ) : 0;
1183
+ const sharedByteBudget = Math.min(
1184
+ WORKER_BUDGETS.defaultSharedBytes,
1185
+ declaredWorkers?.maxSharedBytes ?? WORKER_BUDGETS.defaultSharedBytes
1186
+ );
1187
+ const sharedMemoryDeclared = declaredWorkers?.sharedMemory === true;
1188
+ const crossOriginIsolated = globalThis.crossOriginIsolated === true;
1189
+ let liveWorkerCount = 0;
1190
+ let sharedBytesUsed = 0;
1191
+ const makeBundleWorker = (raw) => {
1192
+ let terminated = false;
1193
+ let mySharedBytes = 0;
1194
+ const subs = new DisposableStore();
1195
+ const worker = {
1196
+ post(message, transfer) {
1197
+ if (terminated) return;
1198
+ raw.post(message, transfer);
1199
+ },
1200
+ onMessage(handler) {
1201
+ if (terminated) return toDisposable(() => {
1202
+ });
1203
+ return subs.add(raw.onMessage(handler));
1204
+ },
1205
+ allocateShared(bytes) {
1206
+ if (terminated) return null;
1207
+ if (!sharedMemoryDeclared) {
1208
+ log.warn(
1209
+ "workers.allocateShared: capabilities.workers.sharedMemory is not declared \u2014 no SharedArrayBuffer (declare it to allocate)"
1210
+ );
1211
+ return null;
1212
+ }
1213
+ if (!crossOriginIsolated) {
1214
+ log.warn(
1215
+ "workers.allocateShared: the environment is not cross-origin isolated \u2014 SharedArrayBuffer is unavailable (the host needs COOP/COEP)"
1216
+ );
1217
+ return null;
1218
+ }
1219
+ if (!Number.isInteger(bytes) || bytes <= 0) return null;
1220
+ if (sharedBytesUsed + bytes > sharedByteBudget) {
1221
+ log.warn(
1222
+ `workers.allocateShared(${bytes}) would exceed the ${sharedByteBudget}-byte per-bundle shared-memory budget (used ${sharedBytesUsed}) \u2014 refused`
1223
+ );
1224
+ return null;
1225
+ }
1226
+ let sab;
1227
+ try {
1228
+ sab = new SharedArrayBuffer(bytes);
1229
+ } catch (err) {
1230
+ log.warn(`workers.allocateShared(${bytes}) failed`, err);
1231
+ return null;
1232
+ }
1233
+ sharedBytesUsed += bytes;
1234
+ mySharedBytes += bytes;
1235
+ return sab;
1236
+ },
1237
+ terminate() {
1238
+ if (terminated) return;
1239
+ terminated = true;
1240
+ subs.dispose();
1241
+ sharedBytesUsed -= mySharedBytes;
1242
+ mySharedBytes = 0;
1243
+ liveWorkerCount = Math.max(0, liveWorkerCount - 1);
1244
+ try {
1245
+ raw.terminate();
1246
+ } catch (err) {
1247
+ log.warn("workers.terminate: backend terminate threw", err);
1248
+ }
1249
+ }
1250
+ };
1251
+ return worker;
1252
+ };
1253
+ const workers = {
1254
+ async spawn(opts) {
1255
+ requireDeclared(
1256
+ hasWorkers(),
1257
+ "workers.spawn",
1258
+ "capabilities.workers must be declared"
1259
+ );
1260
+ if (!workerBackend) {
1261
+ throw new Error(
1262
+ `host.workers.spawn("${opts.module}") \u2014 no worker backend wired (supports("workers@1") is false; the editor injects one)`
1263
+ );
1264
+ }
1265
+ if (liveWorkerCount >= workerCap) {
1266
+ throw new Error(
1267
+ `host.workers.spawn("${opts.module}") \u2014 the ${workerCap}-worker count cap is reached (declared max ${declaredWorkers?.max}, clamped to min(declared, hardwareConcurrency ${hardwareConcurrency}, ${WORKER_BUDGETS.maxWorkers})) \u2014 terminate a worker first`
1268
+ );
1269
+ }
1270
+ liveWorkerCount++;
1271
+ let raw;
1272
+ try {
1273
+ raw = await workerBackend.spawn(manifest.id, opts.module, opts.name);
1274
+ } catch (err) {
1275
+ liveWorkerCount = Math.max(0, liveWorkerCount - 1);
1276
+ throw err instanceof Error ? err : new Error(
1277
+ `host.workers.spawn("${opts.module}") failed: ${String(err)}`
1278
+ );
1279
+ }
1280
+ const worker = makeBundleWorker(raw);
1281
+ store.add(toDisposable(() => worker.terminate()));
1282
+ return worker;
1283
+ },
1284
+ concurrency: () => workerCap
1285
+ };
1286
+ const secretStore = options?.secrets;
1287
+ const secrets = {
1288
+ async set(ref, secret) {
1289
+ requireDeclared(
1290
+ hasSecrets(),
1291
+ "secrets.set",
1292
+ "capabilities.secrets must declare { sources: true }"
1293
+ );
1294
+ if (!secretStore) {
1295
+ throw new Error(
1296
+ `host.secrets.set("${ref}") \u2014 no secret-store backend wired (supports("secrets@1") is false; the editor injects one)`
1297
+ );
1298
+ }
1299
+ await secretStore.set(manifest.id, ref, secret);
1300
+ },
1301
+ async exists(ref) {
1302
+ requireDeclared(
1303
+ hasSecrets(),
1304
+ "secrets.exists",
1305
+ "capabilities.secrets must declare { sources: true }"
1306
+ );
1307
+ if (!secretStore) return false;
1308
+ return secretStore.exists(manifest.id, ref);
1309
+ },
1310
+ async forget(ref) {
1311
+ requireDeclared(
1312
+ hasSecrets(),
1313
+ "secrets.forget",
1314
+ "capabilities.secrets must declare { sources: true }"
1315
+ );
1316
+ if (!secretStore) {
1317
+ log.warn(
1318
+ `host.secrets.forget("${ref}") \u2014 no secret-store backend wired (supports("secrets@1") is false); nothing to forget`
1319
+ );
1320
+ return;
1321
+ }
1322
+ await secretStore.forget(manifest.id, ref);
1323
+ }
1324
+ };
1161
1325
  const featureSet = new Set(HOST_FEATURES);
1162
1326
  if (getEditor().text) {
1163
1327
  featureSet.add("text.measure@1");
@@ -1196,6 +1360,12 @@ function createBundleHost(getEditor, manifest, options) {
1196
1360
  if (options?.clipboard) {
1197
1361
  featureSet.add("clipboard@1");
1198
1362
  }
1363
+ if (options?.workers) {
1364
+ featureSet.add("workers@1");
1365
+ }
1366
+ if (options?.secrets) {
1367
+ featureSet.add("secrets@1");
1368
+ }
1199
1369
  const host = {
1200
1370
  manifest,
1201
1371
  log,
@@ -1215,6 +1385,8 @@ function createBundleHost(getEditor, manifest, options) {
1215
1385
  widgets,
1216
1386
  assets,
1217
1387
  images,
1388
+ workers,
1389
+ secrets,
1218
1390
  clipboard,
1219
1391
  supports: (feature) => featureSet.has(feature),
1220
1392
  get editor() {
@@ -1408,6 +1580,25 @@ function inMemoryClipboard() {
1408
1580
  }
1409
1581
  };
1410
1582
  }
1583
+ function inMemorySecretStore() {
1584
+ const byPlugin = /* @__PURE__ */ new Map();
1585
+ const dir = (id) => {
1586
+ let d = byPlugin.get(id);
1587
+ if (!d) byPlugin.set(id, d = /* @__PURE__ */ new Set());
1588
+ return d;
1589
+ };
1590
+ return {
1591
+ async set(id, ref, _secret) {
1592
+ dir(id).add(ref);
1593
+ },
1594
+ async exists(id, ref) {
1595
+ return dir(id).has(ref);
1596
+ },
1597
+ async forget(id, ref) {
1598
+ dir(id).delete(ref);
1599
+ }
1600
+ };
1601
+ }
1411
1602
  var seqCounter = 1;
1412
1603
  function makeEngineEditor(worker, recorder, onToolPreview) {
1413
1604
  const protocol = worker.protocolVersion;
@@ -1571,11 +1762,13 @@ async function createHeadlessHost(options = {}) {
1571
1762
  let disposed = false;
1572
1763
  const blobStore = options.blobStore ?? inMemoryBlobStore();
1573
1764
  const clipboard = options.clipboard ?? inMemoryClipboard();
1765
+ const secrets = options.secrets ?? inMemorySecretStore();
1574
1766
  const buildHost = (manifest, mode) => createBundleHost(() => editor, manifest, {
1575
1767
  console: options.console,
1576
1768
  storage: options.storage,
1577
1769
  blobStore,
1578
1770
  clipboard,
1771
+ secrets,
1579
1772
  capabilityMode: mode,
1580
1773
  // W-06 — a recordable fake asset source the conformance harness
1581
1774
  // can pass so a bundle's `@font-face` byte path is exercisable
@@ -1642,7 +1835,8 @@ async function createHeadlessHost(options = {}) {
1642
1835
  document: { read: "broad", write: "broad" },
1643
1836
  rendering: ["overlay", "hitTest", "sceneLayer"],
1644
1837
  keybindings: true,
1645
- storage: { blob: true }
1838
+ storage: { blob: true },
1839
+ secrets: { sources: true }
1646
1840
  },
1647
1841
  // Broad contribution declarations so the neutral DRIVER host (which
1648
1842
  // registers arbitrary contributions directly in 'warn' mode) never
@@ -2000,6 +2194,7 @@ export {
2000
2194
  PluginApiNotImplemented,
2001
2195
  PluginCapabilityError,
2002
2196
  WASM_BUDGETS,
2197
+ WORKER_BUDGETS,
2003
2198
  beginPageDrag,
2004
2199
  commitAndSelect,
2005
2200
  contributeEditContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paged-media/plugin-sdk",
3
- "version": "0.2.17-canary.0",
3
+ "version": "0.2.19-canary.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@paged-media/plugin-api": "0.2.17-canary.0"
14
+ "@paged-media/plugin-api": "0.2.19-canary.0"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "react": "^18.3.0"
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "devDependencies": {
25
- "@paged-media/canvas-wasm": "0.44.0",
25
+ "@paged-media/canvas-wasm": "0.44.1",
26
26
  "@types/node": "20.19.39",
27
27
  "@types/react": "^18.3.12",
28
28
  "react": "^18.3.0",