@ethisyscore/extension-runtime 1.135.0 → 1.137.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 (34) hide show
  1. package/README.md +2 -2
  2. package/dist/MockPluginStorage-BM3bq99B.d.cts +35 -0
  3. package/dist/MockPluginStorage-BndTX2RR.d.ts +35 -0
  4. package/dist/{bridge-client-RJOl8WlU.d.ts → bridge-client-CkxYcZcm.d.ts} +1 -1
  5. package/dist/{bridge-client-D9EYFq1l.d.cts → bridge-client-DkogtCvn.d.cts} +1 -1
  6. package/dist/{bridge-envelopes-M5a42rAy.d.cts → bridge-envelopes-BcKu-nQm.d.cts} +72 -1
  7. package/dist/{bridge-envelopes-M5a42rAy.d.ts → bridge-envelopes-BcKu-nQm.d.ts} +72 -1
  8. package/dist/host/index.cjs +57 -2
  9. package/dist/host/index.cjs.map +1 -1
  10. package/dist/host/index.d.cts +3 -3
  11. package/dist/host/index.d.ts +3 -3
  12. package/dist/host/index.js +57 -2
  13. package/dist/host/index.js.map +1 -1
  14. package/dist/mock-host/cli.cjs +273 -7
  15. package/dist/mock-host/cli.cjs.map +1 -1
  16. package/dist/mock-host/cli.d.cts +10 -3
  17. package/dist/mock-host/cli.d.ts +10 -3
  18. package/dist/mock-host/cli.js +272 -8
  19. package/dist/mock-host/cli.js.map +1 -1
  20. package/dist/mock-host/index.cjs +236 -1
  21. package/dist/mock-host/index.cjs.map +1 -1
  22. package/dist/mock-host/index.d.cts +14 -4
  23. package/dist/mock-host/index.d.ts +14 -4
  24. package/dist/mock-host/index.js +236 -2
  25. package/dist/mock-host/index.js.map +1 -1
  26. package/dist/plugin/index.cjs +378 -100
  27. package/dist/plugin/index.cjs.map +1 -1
  28. package/dist/plugin/index.d.cts +171 -6
  29. package/dist/plugin/index.d.ts +171 -6
  30. package/dist/plugin/index.js +366 -101
  31. package/dist/plugin/index.js.map +1 -1
  32. package/dist/{transport-UaoDRdm3.d.cts → transport-D1lE-5nt.d.cts} +30 -3
  33. package/dist/{transport-CFzJQiFG.d.ts → transport-INVtDo2s.d.ts} +30 -3
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -13,8 +13,8 @@ Built on top of `@ethisyscore/protocol` (canonical bridge / manifest / SDUI / ca
13
13
  | ----- | --- | ------- |
14
14
  | `@ethisyscore/extension-runtime` | Root re-exports | Stable consumer-facing types and helpers. |
15
15
  | `@ethisyscore/extension-runtime/host` | Host integration | `WorkerRemoteDomTransport`, declarative interpreter, semantic component registry, offscreen-canvas helpers. |
16
- | `@ethisyscore/extension-runtime/plugin` | Plugin side | `ExtensionRuntimeProvider`, `useMcpResource`, `useMcpTool`, transport abstraction. |
17
- | `@ethisyscore/extension-runtime/mock-host` | Local dev | `DeclarativeMockHost` and the `mock-host` CLI entry. |
16
+ | `@ethisyscore/extension-runtime/plugin` | Plugin side | `ExtensionRuntimeProvider`, `useMcpResource`, `useMcpTool`, transport abstraction, `uploadFileToPluginStorage` (browser file → the app's own plugin storage via the `uploadToStorage` request kind). |
17
+ | `@ethisyscore/extension-runtime/mock-host` | Local dev | `DeclarativeMockHost`, `InMemoryMcpTransport` (its `uploadToStorage` writes to an in-memory `MockPluginStorage`) and the `mock-host` CLI entry. |
18
18
 
19
19
  The `mock-host` bin (`npx mock-host <dir>`) is shipped — boots a Vite dev page that mounts the declarative interpreter against a directory of SDUI JSON resources. Run `--render-mode remote-runtime <bundle>` for the Contract B variant.
20
20
 
@@ -0,0 +1,35 @@
1
+ import { g as UploadToStorageMeta, h as UploadToStorageResult } from './bridge-envelopes-BcKu-nQm.cjs';
2
+
3
+ /** A file the mock host stored for an `uploadToStorage` call. */
4
+ interface MockStoredFile {
5
+ readonly path: string;
6
+ readonly fileName: string;
7
+ readonly contentType: string;
8
+ readonly bytes: Uint8Array;
9
+ }
10
+ /**
11
+ * In-memory stand-in for an app's plugin storage, answering the `uploadToStorage` request kind for
12
+ * `dev:mock` and tests.
13
+ *
14
+ * It behaves like the kernel route where a plugin can observe the difference:
15
+ * - the prefix is validated with the same rules (an invalid one rejects with status 400);
16
+ * - the file type is checked against the same allow-list (415), and the CANONICAL type is stored;
17
+ * - a body over 30 MB rejects with 413;
18
+ * - the path is server-chosen, `{prefix}/{32 hex}{ext}` with the kernel's extension rule, so two
19
+ * uploads never share a path.
20
+ *
21
+ * It does not model quota (507), the storage lock or upload slots (503), or the deadline (408).
22
+ */
23
+ declare class MockPluginStorage {
24
+ private readonly files;
25
+ /** Store `buffer` under a new path below `meta.pathPrefix`. */
26
+ store(meta: UploadToStorageMeta, buffer: ArrayBuffer): UploadToStorageResult;
27
+ /** The stored file at `path`, or `undefined`. */
28
+ get(path: string): MockStoredFile | undefined;
29
+ /** Every stored file, in upload order. */
30
+ list(): readonly MockStoredFile[];
31
+ /** Forget every stored file. */
32
+ clear(): void;
33
+ }
34
+
35
+ export { MockPluginStorage as M, type MockStoredFile as a };
@@ -0,0 +1,35 @@
1
+ import { g as UploadToStorageMeta, h as UploadToStorageResult } from './bridge-envelopes-BcKu-nQm.js';
2
+
3
+ /** A file the mock host stored for an `uploadToStorage` call. */
4
+ interface MockStoredFile {
5
+ readonly path: string;
6
+ readonly fileName: string;
7
+ readonly contentType: string;
8
+ readonly bytes: Uint8Array;
9
+ }
10
+ /**
11
+ * In-memory stand-in for an app's plugin storage, answering the `uploadToStorage` request kind for
12
+ * `dev:mock` and tests.
13
+ *
14
+ * It behaves like the kernel route where a plugin can observe the difference:
15
+ * - the prefix is validated with the same rules (an invalid one rejects with status 400);
16
+ * - the file type is checked against the same allow-list (415), and the CANONICAL type is stored;
17
+ * - a body over 30 MB rejects with 413;
18
+ * - the path is server-chosen, `{prefix}/{32 hex}{ext}` with the kernel's extension rule, so two
19
+ * uploads never share a path.
20
+ *
21
+ * It does not model quota (507), the storage lock or upload slots (503), or the deadline (408).
22
+ */
23
+ declare class MockPluginStorage {
24
+ private readonly files;
25
+ /** Store `buffer` under a new path below `meta.pathPrefix`. */
26
+ store(meta: UploadToStorageMeta, buffer: ArrayBuffer): UploadToStorageResult;
27
+ /** The stored file at `path`, or `undefined`. */
28
+ get(path: string): MockStoredFile | undefined;
29
+ /** Every stored file, in upload order. */
30
+ list(): readonly MockStoredFile[];
31
+ /** Forget every stored file. */
32
+ clear(): void;
33
+ }
34
+
35
+ export { MockPluginStorage as M, type MockStoredFile as a };
@@ -1,4 +1,4 @@
1
- import { d as BridgePushThemeEnvelope, c as BridgePushLocaleEnvelope, b as BridgePushDensityEnvelope, a as BridgePushA11yEnvelope, B as BridgeNavPushEnvelope, e as BridgeSessionTokenPushEnvelope } from './bridge-envelopes-M5a42rAy.js';
1
+ import { d as BridgePushThemeEnvelope, c as BridgePushLocaleEnvelope, b as BridgePushDensityEnvelope, a as BridgePushA11yEnvelope, B as BridgeNavPushEnvelope, e as BridgeSessionTokenPushEnvelope } from './bridge-envelopes-BcKu-nQm.js';
2
2
 
3
3
  /**
4
4
  * Plugin-side bridge client for the host ↔ plugin platform contract
@@ -1,4 +1,4 @@
1
- import { d as BridgePushThemeEnvelope, c as BridgePushLocaleEnvelope, b as BridgePushDensityEnvelope, a as BridgePushA11yEnvelope, B as BridgeNavPushEnvelope, e as BridgeSessionTokenPushEnvelope } from './bridge-envelopes-M5a42rAy.cjs';
1
+ import { d as BridgePushThemeEnvelope, c as BridgePushLocaleEnvelope, b as BridgePushDensityEnvelope, a as BridgePushA11yEnvelope, B as BridgeNavPushEnvelope, e as BridgeSessionTokenPushEnvelope } from './bridge-envelopes-BcKu-nQm.cjs';
2
2
 
3
3
  /**
4
4
  * Plugin-side bridge client for the host ↔ plugin platform contract
@@ -107,6 +107,77 @@ interface McpTransport {
107
107
  * completion.
108
108
  */
109
109
  uploadDocument?(meta: UploadDocumentMeta, buffer: ArrayBuffer, signal?: AbortSignal): Promise<UploadDocumentResult>;
110
+ /**
111
+ * Stream a browser file into the calling app's OWN plugin storage via the host shell, and
112
+ * resolve the {@link UploadToStorageResult}: the plugin-relative `path` the server chose, which
113
+ * is exactly the string the plugin's backend `IPluginStorage` reads, deletes or links.
114
+ *
115
+ * This is the route for every browser upload that a plugin's backend will later read. Never
116
+ * base64 a file through a tool call: the bytes would cross the MCP JSON pipe, the tool's
117
+ * argument limits and the audit payload. Unlike {@link uploadDocument} (legacy CORE storage,
118
+ * `StoredDocument` ids), the destination is the app's own storage namespace, which the kernel
119
+ * derives from the capability token alone.
120
+ *
121
+ * **Optional capability**, for the same reason as {@link uploadDocument}: a transport that
122
+ * predates it simply omits the member, and `uploadFileToPluginStorage` fails with a typed
123
+ * `unsupported` error rather than a `not a function` TypeError.
124
+ *
125
+ * The bytes are handed off as an {@link ArrayBuffer} the transport moves as a transferable
126
+ * (zero-copy) across the worker↔host port, so the caller's buffer is detached afterwards. The
127
+ * worker never sees the capability token or the kernel URL: the host mints the token, POSTs the
128
+ * octet-stream to `extensions/storage/upload-stream` with `x-cc-storage-prefix`,
129
+ * `x-cc-file-name` and `x-cc-content-type`, and returns only the result.
130
+ *
131
+ * When provided, implementations MUST observe `signal`. A host failure SHOULD reject with an
132
+ * error carrying the HTTP `status` (see `PluginStorageUploadError`), so a plugin can tell an
133
+ * over-size file (413) from an exhausted quota (507).
134
+ */
135
+ uploadToStorage?(meta: UploadToStorageMeta, buffer: ArrayBuffer, signal?: AbortSignal): Promise<UploadToStorageResult>;
136
+ }
137
+ /**
138
+ * Metadata accompanying an {@link McpTransport.uploadToStorage} call. There is no module key and
139
+ * no organisation: the kernel derives the storage namespace from the capability token, so there is
140
+ * nothing for the client to claim.
141
+ */
142
+ interface UploadToStorageMeta {
143
+ /**
144
+ * Folder inside the app's own storage namespace: one or more `/`-separated segments of
145
+ * `[A-Za-z0-9_-]` (HR: `cvs`; Legal: `governance/{documentId}/{versionId}`), at most 84
146
+ * characters, with no leading or trailing `/`. A malformed prefix is refused (400), never
147
+ * repaired. Sent as `x-cc-storage-prefix`. The server re-validates it and is authoritative.
148
+ */
149
+ pathPrefix: string;
150
+ /**
151
+ * Client-supplied file name: display metadata, and its extension decides the stored type. It
152
+ * must end in an allow-listed extension (`PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES`) or the server
153
+ * answers 415. Sent as `x-cc-file-name`.
154
+ */
155
+ fileName: string;
156
+ /**
157
+ * Declared MIME type. It must be the extension's canonical type or a listed alias (415
158
+ * otherwise); empty or `application/octet-stream` means "use the extension's type". Sent as
159
+ * `x-cc-content-type`.
160
+ */
161
+ contentType: string;
162
+ /** Byte length of the buffer (advisory; the server reports what actually reached storage). */
163
+ sizeBytes: number;
164
+ }
165
+ /**
166
+ * Result of a successful {@link McpTransport.uploadToStorage} call.
167
+ */
168
+ interface UploadToStorageResult {
169
+ /**
170
+ * Plugin-relative storage path, `{pathPrefix}/{guid}{ext}`, chosen by the server: exactly the
171
+ * string the plugin backend's `IPluginStorage` accepts. Not a URI. Record it with the app's own
172
+ * command.
173
+ */
174
+ path: string;
175
+ /** The sanitised file name the server recorded. */
176
+ fileName: string;
177
+ /** The content type the server recorded. */
178
+ contentType: string;
179
+ /** Bytes that actually reached storage, counted by the server. */
180
+ sizeBytes: number;
110
181
  }
111
182
  /**
112
183
  * Metadata accompanying an {@link McpTransport.uploadDocument} call. All fields
@@ -205,4 +276,4 @@ interface BridgeSessionTokenPushEnvelope {
205
276
  readonly expiresAtMs: number;
206
277
  }
207
278
 
208
- export type { BridgeNavPushEnvelope as B, McpTransport as M, UploadDocumentMeta as U, BridgePushA11yEnvelope as a, BridgePushDensityEnvelope as b, BridgePushLocaleEnvelope as c, BridgePushThemeEnvelope as d, BridgeSessionTokenPushEnvelope as e, UploadDocumentResult as f };
279
+ export type { BridgeNavPushEnvelope as B, McpTransport as M, UploadDocumentMeta as U, BridgePushA11yEnvelope as a, BridgePushDensityEnvelope as b, BridgePushLocaleEnvelope as c, BridgePushThemeEnvelope as d, BridgeSessionTokenPushEnvelope as e, UploadDocumentResult as f, UploadToStorageMeta as g, UploadToStorageResult as h };
@@ -107,6 +107,77 @@ interface McpTransport {
107
107
  * completion.
108
108
  */
109
109
  uploadDocument?(meta: UploadDocumentMeta, buffer: ArrayBuffer, signal?: AbortSignal): Promise<UploadDocumentResult>;
110
+ /**
111
+ * Stream a browser file into the calling app's OWN plugin storage via the host shell, and
112
+ * resolve the {@link UploadToStorageResult}: the plugin-relative `path` the server chose, which
113
+ * is exactly the string the plugin's backend `IPluginStorage` reads, deletes or links.
114
+ *
115
+ * This is the route for every browser upload that a plugin's backend will later read. Never
116
+ * base64 a file through a tool call: the bytes would cross the MCP JSON pipe, the tool's
117
+ * argument limits and the audit payload. Unlike {@link uploadDocument} (legacy CORE storage,
118
+ * `StoredDocument` ids), the destination is the app's own storage namespace, which the kernel
119
+ * derives from the capability token alone.
120
+ *
121
+ * **Optional capability**, for the same reason as {@link uploadDocument}: a transport that
122
+ * predates it simply omits the member, and `uploadFileToPluginStorage` fails with a typed
123
+ * `unsupported` error rather than a `not a function` TypeError.
124
+ *
125
+ * The bytes are handed off as an {@link ArrayBuffer} the transport moves as a transferable
126
+ * (zero-copy) across the worker↔host port, so the caller's buffer is detached afterwards. The
127
+ * worker never sees the capability token or the kernel URL: the host mints the token, POSTs the
128
+ * octet-stream to `extensions/storage/upload-stream` with `x-cc-storage-prefix`,
129
+ * `x-cc-file-name` and `x-cc-content-type`, and returns only the result.
130
+ *
131
+ * When provided, implementations MUST observe `signal`. A host failure SHOULD reject with an
132
+ * error carrying the HTTP `status` (see `PluginStorageUploadError`), so a plugin can tell an
133
+ * over-size file (413) from an exhausted quota (507).
134
+ */
135
+ uploadToStorage?(meta: UploadToStorageMeta, buffer: ArrayBuffer, signal?: AbortSignal): Promise<UploadToStorageResult>;
136
+ }
137
+ /**
138
+ * Metadata accompanying an {@link McpTransport.uploadToStorage} call. There is no module key and
139
+ * no organisation: the kernel derives the storage namespace from the capability token, so there is
140
+ * nothing for the client to claim.
141
+ */
142
+ interface UploadToStorageMeta {
143
+ /**
144
+ * Folder inside the app's own storage namespace: one or more `/`-separated segments of
145
+ * `[A-Za-z0-9_-]` (HR: `cvs`; Legal: `governance/{documentId}/{versionId}`), at most 84
146
+ * characters, with no leading or trailing `/`. A malformed prefix is refused (400), never
147
+ * repaired. Sent as `x-cc-storage-prefix`. The server re-validates it and is authoritative.
148
+ */
149
+ pathPrefix: string;
150
+ /**
151
+ * Client-supplied file name: display metadata, and its extension decides the stored type. It
152
+ * must end in an allow-listed extension (`PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES`) or the server
153
+ * answers 415. Sent as `x-cc-file-name`.
154
+ */
155
+ fileName: string;
156
+ /**
157
+ * Declared MIME type. It must be the extension's canonical type or a listed alias (415
158
+ * otherwise); empty or `application/octet-stream` means "use the extension's type". Sent as
159
+ * `x-cc-content-type`.
160
+ */
161
+ contentType: string;
162
+ /** Byte length of the buffer (advisory; the server reports what actually reached storage). */
163
+ sizeBytes: number;
164
+ }
165
+ /**
166
+ * Result of a successful {@link McpTransport.uploadToStorage} call.
167
+ */
168
+ interface UploadToStorageResult {
169
+ /**
170
+ * Plugin-relative storage path, `{pathPrefix}/{guid}{ext}`, chosen by the server: exactly the
171
+ * string the plugin backend's `IPluginStorage` accepts. Not a URI. Record it with the app's own
172
+ * command.
173
+ */
174
+ path: string;
175
+ /** The sanitised file name the server recorded. */
176
+ fileName: string;
177
+ /** The content type the server recorded. */
178
+ contentType: string;
179
+ /** Bytes that actually reached storage, counted by the server. */
180
+ sizeBytes: number;
110
181
  }
111
182
  /**
112
183
  * Metadata accompanying an {@link McpTransport.uploadDocument} call. All fields
@@ -205,4 +276,4 @@ interface BridgeSessionTokenPushEnvelope {
205
276
  readonly expiresAtMs: number;
206
277
  }
207
278
 
208
- export type { BridgeNavPushEnvelope as B, McpTransport as M, UploadDocumentMeta as U, BridgePushA11yEnvelope as a, BridgePushDensityEnvelope as b, BridgePushLocaleEnvelope as c, BridgePushThemeEnvelope as d, BridgeSessionTokenPushEnvelope as e, UploadDocumentResult as f };
279
+ export type { BridgeNavPushEnvelope as B, McpTransport as M, UploadDocumentMeta as U, BridgePushA11yEnvelope as a, BridgePushDensityEnvelope as b, BridgePushLocaleEnvelope as c, BridgePushThemeEnvelope as d, BridgeSessionTokenPushEnvelope as e, UploadDocumentResult as f, UploadToStorageMeta as g, UploadToStorageResult as h };
@@ -694,6 +694,9 @@ var WorkerRemoteDomTransport = class {
694
694
  case "ethisys:mcp:uploadDocument":
695
695
  this.dispatchMcp(message, "ethisys:mcp:uploadDocument:result", (m) => this.handleUploadDocument(m));
696
696
  return;
697
+ case "ethisys:mcp:uploadToStorage":
698
+ this.dispatchMcp(message, "ethisys:mcp:uploadToStorage:result", (m) => this.handleUploadToStorage(m));
699
+ return;
697
700
  case "ethisys:mcp:abort":
698
701
  this.inFlightAborts.get(message.id ?? "")?.abort();
699
702
  return;
@@ -931,11 +934,63 @@ var WorkerRemoteDomTransport = class {
931
934
  this.replyError(message.id, "ethisys:mcp:uploadDocument:result", err);
932
935
  }
933
936
  }
934
- replyError(id, type, err) {
937
+ async handleUploadToStorage(message) {
938
+ const resultType = "ethisys:mcp:uploadToStorage:result";
939
+ let token;
940
+ try {
941
+ token = await this.capabilityTokenProvider();
942
+ } catch (err) {
943
+ this.replyError(message.id, resultType, err, true);
944
+ return;
945
+ }
946
+ if (this.disposed) {
947
+ return;
948
+ }
949
+ try {
950
+ const result = await this.mcpClient.fetch({
951
+ kind: "uploadToStorage",
952
+ meta: message.meta,
953
+ buffer: message.buffer,
954
+ capabilityToken: token,
955
+ signal: this.requestSignal(message.id)
956
+ });
957
+ this.reportVersionSkew(result);
958
+ this.safePostMessage({
959
+ id: message.id,
960
+ type: resultType,
961
+ ok: result.ok,
962
+ data: result.data,
963
+ error: result.error,
964
+ code: classifyHostResponse(result),
965
+ // The HTTP status, on failure only. A bare number carries no host internals, and it
966
+ // is the only way the plugin can tell 413 (file too large) from 507 (quota) from 503
967
+ // (storage lock busy): the closed MCP code folds all three into `internal`/`unavailable`.
968
+ ...!result.ok && typeof result.status === "number" ? { status: result.status } : {}
969
+ });
970
+ } catch (err) {
971
+ this.replyError(message.id, resultType, err, true);
972
+ }
973
+ }
974
+ replyError(id, type, err, includeStatus = false) {
935
975
  const message = err instanceof Error ? err.message : "MCP request failed";
936
- this.safePostMessage({ id, type, ok: false, error: message, code: classifyHostError(err) });
976
+ const status = includeStatus ? statusOf(err) : void 0;
977
+ this.safePostMessage({
978
+ id,
979
+ type,
980
+ ok: false,
981
+ error: message,
982
+ code: classifyHostError(err),
983
+ ...status !== void 0 ? { status } : {}
984
+ });
937
985
  }
938
986
  };
987
+ function statusOf(err) {
988
+ if (err === null || typeof err !== "object") {
989
+ return void 0;
990
+ }
991
+ const candidate = err;
992
+ return typeof candidate.status === "number" ? candidate.status : typeof candidate.statusCode === "number" ? candidate.statusCode : void 0;
993
+ }
939
994
  var IFRAME_BRIDGE_PROTOCOL = "ethisys.iframe.bridge.v1";
940
995
  var InboundEnvelope = zod.z.discriminatedUnion("type", [
941
996
  zod.z.object({ type: zod.z.literal("ethisys:mcp:invokeTool"), id: zod.z.string().min(1), name: zod.z.string().min(1), args: zod.z.unknown(), nonce: zod.z.string() }),