@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.
- package/README.md +2 -2
- package/dist/MockPluginStorage-DF3DL1dI.d.ts +59 -0
- package/dist/MockPluginStorage-QKfz8CMt.d.cts +59 -0
- package/dist/{bridge-client-RJOl8WlU.d.ts → bridge-client-CkxYcZcm.d.ts} +1 -1
- package/dist/{bridge-client-D9EYFq1l.d.cts → bridge-client-DkogtCvn.d.cts} +1 -1
- package/dist/{bridge-envelopes-M5a42rAy.d.cts → bridge-envelopes-BcKu-nQm.d.cts} +72 -1
- package/dist/{bridge-envelopes-M5a42rAy.d.ts → bridge-envelopes-BcKu-nQm.d.ts} +72 -1
- package/dist/host/index.cjs +84 -2
- package/dist/host/index.cjs.map +1 -1
- package/dist/host/index.d.cts +3 -3
- package/dist/host/index.d.ts +3 -3
- package/dist/host/index.js +84 -2
- package/dist/host/index.js.map +1 -1
- package/dist/mock-host/cli.cjs +340 -7
- package/dist/mock-host/cli.cjs.map +1 -1
- package/dist/mock-host/cli.d.cts +10 -3
- package/dist/mock-host/cli.d.ts +10 -3
- package/dist/mock-host/cli.js +339 -8
- package/dist/mock-host/cli.js.map +1 -1
- package/dist/mock-host/index.cjs +276 -1
- package/dist/mock-host/index.cjs.map +1 -1
- package/dist/mock-host/index.d.cts +14 -4
- package/dist/mock-host/index.d.ts +14 -4
- package/dist/mock-host/index.js +276 -2
- package/dist/mock-host/index.js.map +1 -1
- package/dist/plugin/index.cjs +407 -100
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.d.cts +186 -6
- package/dist/plugin/index.d.ts +186 -6
- package/dist/plugin/index.js +395 -101
- package/dist/plugin/index.js.map +1 -1
- package/dist/{transport-UaoDRdm3.d.cts → transport-D1lE-5nt.d.cts} +30 -3
- package/dist/{transport-CFzJQiFG.d.ts → transport-INVtDo2s.d.ts} +30 -3
- 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-
|
|
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-
|
|
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 };
|
package/dist/host/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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() }),
|