@ethisyscore/extension-runtime 1.136.0 → 1.138.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-DF3DL1dI.d.ts +59 -0
  3. package/dist/MockPluginStorage-QKfz8CMt.d.cts +59 -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 +84 -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 +84 -2
  13. package/dist/host/index.js.map +1 -1
  14. package/dist/mock-host/cli.cjs +340 -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 +339 -8
  19. package/dist/mock-host/cli.js.map +1 -1
  20. package/dist/mock-host/index.cjs +276 -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 +276 -2
  25. package/dist/mock-host/index.js.map +1 -1
  26. package/dist/plugin/index.cjs +407 -100
  27. package/dist/plugin/index.cjs.map +1 -1
  28. package/dist/plugin/index.d.cts +186 -6
  29. package/dist/plugin/index.d.ts +186 -6
  30. package/dist/plugin/index.js +395 -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 +2 -2
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,59 @@
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
+ /** Optional per-user request rate limit for {@link MockPluginStorage}, mirroring the kernel route's 429. */
11
+ interface MockPluginStorageRateLimit {
12
+ /** Requests allowed per window. The kernel default is 60. */
13
+ readonly maxRequests: number;
14
+ /** Window length in seconds. Defaults to 60, the kernel default. */
15
+ readonly windowSeconds?: number;
16
+ }
17
+ /** Options for {@link MockPluginStorage}. Everything is off by default. */
18
+ interface MockPluginStorageOptions {
19
+ /** Refuse uploads past this rate with 429 `rate_limited`. Off when omitted. */
20
+ readonly rateLimit?: MockPluginStorageRateLimit;
21
+ /** Clock in epoch milliseconds, for tests. Defaults to `Date.now`. */
22
+ readonly now?: () => number;
23
+ }
24
+ /**
25
+ * In-memory stand-in for an app's plugin storage, answering the `uploadToStorage` request kind for
26
+ * `dev:mock` and tests.
27
+ *
28
+ * It behaves like the kernel route where a plugin can observe the difference:
29
+ * - the prefix is validated with the same rules (an invalid one rejects with status 400);
30
+ * - the file type is checked against the same allow-list (415), and the CANONICAL type is stored;
31
+ * - a body over 30 MB rejects with 413;
32
+ * - the path is server-chosen, `{prefix}/{32 hex}{ext}` with the kernel's extension rule, so two
33
+ * uploads never share a path.
34
+ *
35
+ * - with `rateLimit` set, a request over the rate rejects with 429 and `retryAfterSeconds` (off by
36
+ * default; a fixed window like the kernel's, counting every request, accepted or not).
37
+ *
38
+ * It does not model quota (507), the storage lock or upload slots (503), or the deadline (408).
39
+ */
40
+ declare class MockPluginStorage {
41
+ private readonly files;
42
+ private readonly rateLimit?;
43
+ private readonly now;
44
+ private windowStartMs;
45
+ private windowCount;
46
+ constructor(options?: MockPluginStorageOptions);
47
+ /** Store `buffer` under a new path below `meta.pathPrefix`. */
48
+ store(meta: UploadToStorageMeta, buffer: ArrayBuffer): UploadToStorageResult;
49
+ /** The kernel refuses before the handler runs, so this is checked before anything else. */
50
+ private enforceRateLimit;
51
+ /** The stored file at `path`, or `undefined`. */
52
+ get(path: string): MockStoredFile | undefined;
53
+ /** Every stored file, in upload order. */
54
+ list(): readonly MockStoredFile[];
55
+ /** Forget every stored file. */
56
+ clear(): void;
57
+ }
58
+
59
+ export { MockPluginStorage as M, type MockPluginStorageOptions as a, type MockPluginStorageRateLimit as b, type MockStoredFile as c };
@@ -0,0 +1,59 @@
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
+ /** Optional per-user request rate limit for {@link MockPluginStorage}, mirroring the kernel route's 429. */
11
+ interface MockPluginStorageRateLimit {
12
+ /** Requests allowed per window. The kernel default is 60. */
13
+ readonly maxRequests: number;
14
+ /** Window length in seconds. Defaults to 60, the kernel default. */
15
+ readonly windowSeconds?: number;
16
+ }
17
+ /** Options for {@link MockPluginStorage}. Everything is off by default. */
18
+ interface MockPluginStorageOptions {
19
+ /** Refuse uploads past this rate with 429 `rate_limited`. Off when omitted. */
20
+ readonly rateLimit?: MockPluginStorageRateLimit;
21
+ /** Clock in epoch milliseconds, for tests. Defaults to `Date.now`. */
22
+ readonly now?: () => number;
23
+ }
24
+ /**
25
+ * In-memory stand-in for an app's plugin storage, answering the `uploadToStorage` request kind for
26
+ * `dev:mock` and tests.
27
+ *
28
+ * It behaves like the kernel route where a plugin can observe the difference:
29
+ * - the prefix is validated with the same rules (an invalid one rejects with status 400);
30
+ * - the file type is checked against the same allow-list (415), and the CANONICAL type is stored;
31
+ * - a body over 30 MB rejects with 413;
32
+ * - the path is server-chosen, `{prefix}/{32 hex}{ext}` with the kernel's extension rule, so two
33
+ * uploads never share a path.
34
+ *
35
+ * - with `rateLimit` set, a request over the rate rejects with 429 and `retryAfterSeconds` (off by
36
+ * default; a fixed window like the kernel's, counting every request, accepted or not).
37
+ *
38
+ * It does not model quota (507), the storage lock or upload slots (503), or the deadline (408).
39
+ */
40
+ declare class MockPluginStorage {
41
+ private readonly files;
42
+ private readonly rateLimit?;
43
+ private readonly now;
44
+ private windowStartMs;
45
+ private windowCount;
46
+ constructor(options?: MockPluginStorageOptions);
47
+ /** Store `buffer` under a new path below `meta.pathPrefix`. */
48
+ store(meta: UploadToStorageMeta, buffer: ArrayBuffer): UploadToStorageResult;
49
+ /** The kernel refuses before the handler runs, so this is checked before anything else. */
50
+ private enforceRateLimit;
51
+ /** The stored file at `path`, or `undefined`. */
52
+ get(path: string): MockStoredFile | undefined;
53
+ /** Every stored file, in upload order. */
54
+ list(): readonly MockStoredFile[];
55
+ /** Forget every stored file. */
56
+ clear(): void;
57
+ }
58
+
59
+ export { MockPluginStorage as M, type MockPluginStorageOptions as a, type MockPluginStorageRateLimit as b, type MockStoredFile as c };
@@ -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 };
@@ -380,6 +380,20 @@ function classifyHostError(err) {
380
380
  }
381
381
  return "internal";
382
382
  }
383
+ function parsePluginStorageRetryAfter(raw, nowMs = Date.now()) {
384
+ if (typeof raw === "number") {
385
+ return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : void 0;
386
+ }
387
+ if (typeof raw !== "string" || raw.trim().length === 0) {
388
+ return void 0;
389
+ }
390
+ const text = raw.trim();
391
+ if (/^\d+$/.test(text)) {
392
+ return Number(text);
393
+ }
394
+ const at = /[A-Za-z]/.test(text) ? Date.parse(text) : Number.NaN;
395
+ return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - nowMs) / 1e3));
396
+ }
383
397
 
384
398
  // src/host/version-skew.ts
385
399
  var EXTENSION_VERSION_HEADER = "x-extension-version";
@@ -694,6 +708,9 @@ var WorkerRemoteDomTransport = class {
694
708
  case "ethisys:mcp:uploadDocument":
695
709
  this.dispatchMcp(message, "ethisys:mcp:uploadDocument:result", (m) => this.handleUploadDocument(m));
696
710
  return;
711
+ case "ethisys:mcp:uploadToStorage":
712
+ this.dispatchMcp(message, "ethisys:mcp:uploadToStorage:result", (m) => this.handleUploadToStorage(m));
713
+ return;
697
714
  case "ethisys:mcp:abort":
698
715
  this.inFlightAborts.get(message.id ?? "")?.abort();
699
716
  return;
@@ -931,11 +948,76 @@ var WorkerRemoteDomTransport = class {
931
948
  this.replyError(message.id, "ethisys:mcp:uploadDocument:result", err);
932
949
  }
933
950
  }
934
- replyError(id, type, err) {
951
+ async handleUploadToStorage(message) {
952
+ const resultType = "ethisys:mcp:uploadToStorage:result";
953
+ let token;
954
+ try {
955
+ token = await this.capabilityTokenProvider();
956
+ } catch (err) {
957
+ this.replyError(message.id, resultType, err, true);
958
+ return;
959
+ }
960
+ if (this.disposed) {
961
+ return;
962
+ }
963
+ try {
964
+ const result = await this.mcpClient.fetch({
965
+ kind: "uploadToStorage",
966
+ meta: message.meta,
967
+ buffer: message.buffer,
968
+ capabilityToken: token,
969
+ signal: this.requestSignal(message.id)
970
+ });
971
+ this.reportVersionSkew(result);
972
+ this.safePostMessage({
973
+ id: message.id,
974
+ type: resultType,
975
+ ok: result.ok,
976
+ data: result.data,
977
+ error: result.error,
978
+ code: classifyHostResponse(result),
979
+ // The HTTP status, on failure only. A bare number carries no host internals, and it
980
+ // is the only way the plugin can tell 413 (file too large) from 507 (quota) from 503
981
+ // (storage lock busy): the closed MCP code folds all three into `internal`/`unavailable`.
982
+ ...!result.ok && typeof result.status === "number" ? { status: result.status } : {},
983
+ // The server's `Retry-After` (429), as whole seconds, so a plugin can show how long
984
+ // to wait. Never acted on here: an upload is not retried automatically.
985
+ ...retryAfterField(result)
986
+ });
987
+ } catch (err) {
988
+ this.replyError(message.id, resultType, err, true);
989
+ }
990
+ }
991
+ replyError(id, type, err, includeStatus = false) {
935
992
  const message = err instanceof Error ? err.message : "MCP request failed";
936
- this.safePostMessage({ id, type, ok: false, error: message, code: classifyHostError(err) });
993
+ const status = includeStatus ? statusOf(err) : void 0;
994
+ const retryAfterSeconds = includeStatus && err !== null && typeof err === "object" ? parsePluginStorageRetryAfter(err.retryAfterSeconds) : void 0;
995
+ this.safePostMessage({
996
+ id,
997
+ type,
998
+ ok: false,
999
+ error: message,
1000
+ code: classifyHostError(err),
1001
+ ...status !== void 0 ? { status } : {},
1002
+ ...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
1003
+ });
937
1004
  }
938
1005
  };
1006
+ function retryAfterField(result) {
1007
+ if (result.ok || !result.headers) {
1008
+ return {};
1009
+ }
1010
+ const key = Object.keys(result.headers).find((k) => k.toLowerCase() === "retry-after");
1011
+ const seconds = key === void 0 ? void 0 : parsePluginStorageRetryAfter(result.headers[key]);
1012
+ return seconds === void 0 ? {} : { retryAfterSeconds: seconds };
1013
+ }
1014
+ function statusOf(err) {
1015
+ if (err === null || typeof err !== "object") {
1016
+ return void 0;
1017
+ }
1018
+ const candidate = err;
1019
+ return typeof candidate.status === "number" ? candidate.status : typeof candidate.statusCode === "number" ? candidate.statusCode : void 0;
1020
+ }
939
1021
  var IFRAME_BRIDGE_PROTOCOL = "ethisys.iframe.bridge.v1";
940
1022
  var InboundEnvelope = zod.z.discriminatedUnion("type", [
941
1023
  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() }),