@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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ReactNode, ReactElement } from 'react';
|
|
2
2
|
import { C as ComponentRegistry } from '../registry-DpCx_LxF.cjs';
|
|
3
3
|
import { SduiNode } from '@ethisyscore/protocol';
|
|
4
|
-
import { M as McpTransport, U as UploadDocumentMeta, f as UploadDocumentResult } from '../bridge-envelopes-
|
|
5
|
-
import {
|
|
4
|
+
import { M as McpTransport, U as UploadDocumentMeta, f as UploadDocumentResult, g as UploadToStorageMeta, h as UploadToStorageResult } from '../bridge-envelopes-BcKu-nQm.cjs';
|
|
5
|
+
import { M as MockPluginStorage } from '../MockPluginStorage-QKfz8CMt.cjs';
|
|
6
|
+
export { a as MockPluginStorageOptions, b as MockPluginStorageRateLimit, c as MockStoredFile } from '../MockPluginStorage-QKfz8CMt.cjs';
|
|
7
|
+
import { P as PortBridgeClient, T as ThemePayload, L as LocalePayload, D as DensityPayload, A as A11yPayload, N as NavPayload, S as SessionTokenPayload } from '../bridge-client-DkogtCvn.cjs';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Handler for a mocked tool invocation. The handler receives the request
|
|
@@ -37,12 +39,20 @@ type MockUploadHandler = (meta: UploadDocumentMeta, buffer: ArrayBuffer, signal?
|
|
|
37
39
|
* contract. `uploadDocument` DOES observe the signal (rejecting with an
|
|
38
40
|
* `AbortError`): its contract mandates it, a mock upload handler may be
|
|
39
41
|
* genuinely async, and dev-host flows need to simulate upload cancellation.
|
|
42
|
+
*
|
|
43
|
+
* `uploadToStorage` writes into {@link pluginStorage}, an in-memory {@link MockPluginStorage}, and
|
|
44
|
+
* returns a server-style `{prefix}/{guid}{ext}` path. It validates the prefix and size with the
|
|
45
|
+
* kernel's rules, rejecting with a typed `PluginStorageUploadError` (status 400 / 413), and
|
|
46
|
+
* honours the signal.
|
|
40
47
|
*/
|
|
41
48
|
declare class InMemoryMcpTransport implements McpTransport {
|
|
42
49
|
private readonly resources;
|
|
43
50
|
private readonly tools;
|
|
44
51
|
private readonly uploadHandler?;
|
|
45
|
-
|
|
52
|
+
/** Where `uploadToStorage` writes. Read it back in a test, or from devtools under `dev:mock`. */
|
|
53
|
+
readonly pluginStorage: MockPluginStorage;
|
|
54
|
+
constructor(resources: Record<string, SduiNode>, tools?: Record<string, MockToolHandler>, uploadHandler?: MockUploadHandler, pluginStorage?: MockPluginStorage);
|
|
55
|
+
uploadToStorage(meta: UploadToStorageMeta, buffer: ArrayBuffer, signal?: AbortSignal): Promise<UploadToStorageResult>;
|
|
46
56
|
getResource<T>(uri: string): Promise<{
|
|
47
57
|
uri: string;
|
|
48
58
|
data: T;
|
|
@@ -196,4 +206,4 @@ interface WorkerMockHostProps extends DeclarativeMockHostProps {
|
|
|
196
206
|
*/
|
|
197
207
|
declare function WorkerMockHost(props: WorkerMockHostProps): ReactElement;
|
|
198
208
|
|
|
199
|
-
export { DeclarativeMockHost, type DeclarativeMockHostProps, InMemoryBridgeTransport, InMemoryMcpTransport, type MockToolHandler, WorkerMockHost, type WorkerMockHostProps };
|
|
209
|
+
export { DeclarativeMockHost, type DeclarativeMockHostProps, InMemoryBridgeTransport, InMemoryMcpTransport, MockPluginStorage, type MockToolHandler, WorkerMockHost, type WorkerMockHostProps };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ReactNode, ReactElement } from 'react';
|
|
2
2
|
import { C as ComponentRegistry } from '../registry-DpCx_LxF.js';
|
|
3
3
|
import { SduiNode } from '@ethisyscore/protocol';
|
|
4
|
-
import { M as McpTransport, U as UploadDocumentMeta, f as UploadDocumentResult } from '../bridge-envelopes-
|
|
5
|
-
import {
|
|
4
|
+
import { M as McpTransport, U as UploadDocumentMeta, f as UploadDocumentResult, g as UploadToStorageMeta, h as UploadToStorageResult } from '../bridge-envelopes-BcKu-nQm.js';
|
|
5
|
+
import { M as MockPluginStorage } from '../MockPluginStorage-DF3DL1dI.js';
|
|
6
|
+
export { a as MockPluginStorageOptions, b as MockPluginStorageRateLimit, c as MockStoredFile } from '../MockPluginStorage-DF3DL1dI.js';
|
|
7
|
+
import { P as PortBridgeClient, T as ThemePayload, L as LocalePayload, D as DensityPayload, A as A11yPayload, N as NavPayload, S as SessionTokenPayload } from '../bridge-client-CkxYcZcm.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Handler for a mocked tool invocation. The handler receives the request
|
|
@@ -37,12 +39,20 @@ type MockUploadHandler = (meta: UploadDocumentMeta, buffer: ArrayBuffer, signal?
|
|
|
37
39
|
* contract. `uploadDocument` DOES observe the signal (rejecting with an
|
|
38
40
|
* `AbortError`): its contract mandates it, a mock upload handler may be
|
|
39
41
|
* genuinely async, and dev-host flows need to simulate upload cancellation.
|
|
42
|
+
*
|
|
43
|
+
* `uploadToStorage` writes into {@link pluginStorage}, an in-memory {@link MockPluginStorage}, and
|
|
44
|
+
* returns a server-style `{prefix}/{guid}{ext}` path. It validates the prefix and size with the
|
|
45
|
+
* kernel's rules, rejecting with a typed `PluginStorageUploadError` (status 400 / 413), and
|
|
46
|
+
* honours the signal.
|
|
40
47
|
*/
|
|
41
48
|
declare class InMemoryMcpTransport implements McpTransport {
|
|
42
49
|
private readonly resources;
|
|
43
50
|
private readonly tools;
|
|
44
51
|
private readonly uploadHandler?;
|
|
45
|
-
|
|
52
|
+
/** Where `uploadToStorage` writes. Read it back in a test, or from devtools under `dev:mock`. */
|
|
53
|
+
readonly pluginStorage: MockPluginStorage;
|
|
54
|
+
constructor(resources: Record<string, SduiNode>, tools?: Record<string, MockToolHandler>, uploadHandler?: MockUploadHandler, pluginStorage?: MockPluginStorage);
|
|
55
|
+
uploadToStorage(meta: UploadToStorageMeta, buffer: ArrayBuffer, signal?: AbortSignal): Promise<UploadToStorageResult>;
|
|
46
56
|
getResource<T>(uri: string): Promise<{
|
|
47
57
|
uri: string;
|
|
48
58
|
data: T;
|
|
@@ -196,4 +206,4 @@ interface WorkerMockHostProps extends DeclarativeMockHostProps {
|
|
|
196
206
|
*/
|
|
197
207
|
declare function WorkerMockHost(props: WorkerMockHostProps): ReactElement;
|
|
198
208
|
|
|
199
|
-
export { DeclarativeMockHost, type DeclarativeMockHostProps, InMemoryBridgeTransport, InMemoryMcpTransport, type MockToolHandler, WorkerMockHost, type WorkerMockHostProps };
|
|
209
|
+
export { DeclarativeMockHost, type DeclarativeMockHostProps, InMemoryBridgeTransport, InMemoryMcpTransport, MockPluginStorage, type MockToolHandler, WorkerMockHost, type WorkerMockHostProps };
|
package/dist/mock-host/index.js
CHANGED
|
@@ -34,15 +34,289 @@ function ExtensionRuntimeProvider({ transport, clientPush = null, identity = nul
|
|
|
34
34
|
return /* @__PURE__ */ jsx(ExtensionRuntimeContext.Provider, { value, children: /* @__PURE__ */ jsx(ClientPushContext.Provider, { value: push, children: /* @__PURE__ */ jsx(HostIdentityContext.Provider, { value: id, children: /* @__PURE__ */ jsx(PluginRealtimeContext.Provider, { value: realtimeValue, children }) }) }) });
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// src/bridge/mcp-error.ts
|
|
38
|
+
var RETRYABLE = ["rate_limited", "timeout", "unavailable"];
|
|
39
|
+
function isRetryableMcpErrorCode(code) {
|
|
40
|
+
return RETRYABLE.includes(code);
|
|
41
|
+
}
|
|
42
|
+
var McpToolError = class extends Error {
|
|
43
|
+
/** Machine-readable classification. `internal` when the host sent none. */
|
|
44
|
+
code;
|
|
45
|
+
/**
|
|
46
|
+
* Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype
|
|
47
|
+
* check because the error can be constructed in one bundle and inspected in another, where
|
|
48
|
+
* `instanceof` compares two different class objects and answers false.
|
|
49
|
+
*/
|
|
50
|
+
isMcpToolError = true;
|
|
51
|
+
constructor(message, code = "internal") {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "McpHostError";
|
|
54
|
+
this.code = code;
|
|
55
|
+
}
|
|
56
|
+
/** True when this failure is worth retrying unchanged. */
|
|
57
|
+
get retryable() {
|
|
58
|
+
return isRetryableMcpErrorCode(this.code);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function mcpErrorCodeFromHttpStatus(status) {
|
|
62
|
+
switch (status) {
|
|
63
|
+
case 401:
|
|
64
|
+
return "unauthorized";
|
|
65
|
+
case 403:
|
|
66
|
+
return "forbidden";
|
|
67
|
+
case 404:
|
|
68
|
+
return "not_found";
|
|
69
|
+
case 400:
|
|
70
|
+
case 422:
|
|
71
|
+
return "invalid_request";
|
|
72
|
+
case 409:
|
|
73
|
+
case 412:
|
|
74
|
+
return "conflict";
|
|
75
|
+
case 429:
|
|
76
|
+
return "rate_limited";
|
|
77
|
+
case 408:
|
|
78
|
+
case 504:
|
|
79
|
+
return "timeout";
|
|
80
|
+
case 502:
|
|
81
|
+
case 503:
|
|
82
|
+
return "unavailable";
|
|
83
|
+
default:
|
|
84
|
+
return "internal";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/plugin/pluginStorageUpload.ts
|
|
89
|
+
var PLUGIN_STORAGE_UPLOAD_MAX_BYTES = 30 * 1024 * 1024;
|
|
90
|
+
var PLUGIN_STORAGE_PREFIX_MAX_LENGTH = 84;
|
|
91
|
+
var PLUGIN_STORAGE_PATH_MAX_LENGTH = 128;
|
|
92
|
+
var PluginStorageUploadError = class extends McpToolError {
|
|
93
|
+
/** What went wrong, in terms a caller branches on. */
|
|
94
|
+
reason;
|
|
95
|
+
/** HTTP status from the server, when the host surfaced one. */
|
|
96
|
+
status;
|
|
97
|
+
/**
|
|
98
|
+
* Seconds the server asked the caller to wait (`Retry-After`), on a `rate_limited` failure
|
|
99
|
+
* when the host forwarded the header. Absent otherwise.
|
|
100
|
+
*/
|
|
101
|
+
retryAfterSeconds;
|
|
102
|
+
/** Structural marker for {@link isPluginStorageUploadError}. */
|
|
103
|
+
isPluginStorageUploadError = true;
|
|
104
|
+
constructor(message, reason, options = {}) {
|
|
105
|
+
super(message, options.code ?? codeForReason(reason, options.status));
|
|
106
|
+
this.name = "PluginStorageUploadError";
|
|
107
|
+
this.reason = reason;
|
|
108
|
+
this.status = options.status;
|
|
109
|
+
if (options.retryAfterSeconds !== void 0) {
|
|
110
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function validatePluginStoragePrefix(raw) {
|
|
115
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
116
|
+
return { ok: false, error: "A storage path prefix is required." };
|
|
117
|
+
}
|
|
118
|
+
if (raw.length > PLUGIN_STORAGE_PREFIX_MAX_LENGTH) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: `The storage path prefix must be at most ${PLUGIN_STORAGE_PREFIX_MAX_LENGTH} characters, so the stored path fits ${PLUGIN_STORAGE_PATH_MAX_LENGTH}.`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!/^[A-Za-z0-9_\-/]+$/.test(raw)) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: "The storage path prefix may contain only letters, digits, '-', '_' and '/' separators."
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (raw.split("/").some((segment) => segment.length === 0)) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
error: "The storage path prefix must not start or end with '/' or contain an empty segment."
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { ok: true, prefix: raw };
|
|
137
|
+
}
|
|
138
|
+
var PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES = Object.freeze({
|
|
139
|
+
pdf: ["application/pdf"],
|
|
140
|
+
png: ["image/png"],
|
|
141
|
+
jpg: ["image/jpeg", "image/pjpeg", "image/jpg"],
|
|
142
|
+
jpeg: ["image/jpeg", "image/pjpeg", "image/jpg"],
|
|
143
|
+
gif: ["image/gif"],
|
|
144
|
+
webp: ["image/webp"],
|
|
145
|
+
doc: ["application/msword"],
|
|
146
|
+
docx: ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
|
|
147
|
+
xls: ["application/vnd.ms-excel"],
|
|
148
|
+
xlsx: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
|
|
149
|
+
ppt: ["application/vnd.ms-powerpoint"],
|
|
150
|
+
pptx: ["application/vnd.openxmlformats-officedocument.presentationml.presentation"],
|
|
151
|
+
odt: ["application/vnd.oasis.opendocument.text"],
|
|
152
|
+
ods: ["application/vnd.oasis.opendocument.spreadsheet"],
|
|
153
|
+
odp: ["application/vnd.oasis.opendocument.presentation"],
|
|
154
|
+
txt: ["text/plain"],
|
|
155
|
+
csv: ["text/csv", "application/vnd.ms-excel", "application/csv", "text/plain"],
|
|
156
|
+
zip: ["application/zip", "application/x-zip-compressed", "application/x-zip"],
|
|
157
|
+
mp3: ["audio/mpeg", "audio/mp3"],
|
|
158
|
+
m4a: ["audio/mp4", "audio/x-m4a"],
|
|
159
|
+
mp4: ["video/mp4"]
|
|
160
|
+
});
|
|
161
|
+
var OCTET_STREAM = "application/octet-stream";
|
|
162
|
+
function pluginStorageSafeExtension(fileName) {
|
|
163
|
+
if (typeof fileName !== "string" || fileName.trim().length === 0) {
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
const leaf = fileName.split(/[\\/]/).pop() ?? "";
|
|
167
|
+
const dot = leaf.lastIndexOf(".");
|
|
168
|
+
if (dot < 0) {
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
const body = leaf.slice(dot + 1);
|
|
172
|
+
return /^[A-Za-z0-9]{1,10}$/.test(body) ? `.${body.toLowerCase()}` : "";
|
|
173
|
+
}
|
|
174
|
+
function resolvePluginStorageContentType(fileName, declaredContentType) {
|
|
175
|
+
const extension = pluginStorageSafeExtension(fileName).slice(1);
|
|
176
|
+
const allowed = extension.length > 0 && Object.prototype.hasOwnProperty.call(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES, extension) ? PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES[extension] : void 0;
|
|
177
|
+
if (allowed === void 0) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
error: "This file type is not allowed. Allowed: " + Object.keys(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES).sort().join(", ") + "."
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const declared = mediaTypeOf(declaredContentType);
|
|
184
|
+
if (declared === void 0 || declared === OCTET_STREAM || allowed.includes(declared)) {
|
|
185
|
+
return { ok: true, contentType: allowed[0] };
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
error: `The declared content type '${declared}' does not match the file extension '.${extension}'.`
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function mediaTypeOf(raw) {
|
|
193
|
+
if (typeof raw !== "string") {
|
|
194
|
+
return void 0;
|
|
195
|
+
}
|
|
196
|
+
const mediaType = raw.split(";")[0].trim().toLowerCase();
|
|
197
|
+
return /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType) ? mediaType : void 0;
|
|
198
|
+
}
|
|
199
|
+
function codeForReason(reason, status) {
|
|
200
|
+
if (status !== void 0) {
|
|
201
|
+
return mcpErrorCodeFromHttpStatus(status);
|
|
202
|
+
}
|
|
203
|
+
switch (reason) {
|
|
204
|
+
case "invalid_prefix":
|
|
205
|
+
case "too_large":
|
|
206
|
+
case "unsupported_type":
|
|
207
|
+
return "invalid_request";
|
|
208
|
+
case "timeout":
|
|
209
|
+
return "timeout";
|
|
210
|
+
case "unauthorized":
|
|
211
|
+
return "unauthorized";
|
|
212
|
+
case "forbidden":
|
|
213
|
+
return "forbidden";
|
|
214
|
+
case "busy":
|
|
215
|
+
return "unavailable";
|
|
216
|
+
case "rate_limited":
|
|
217
|
+
return "rate_limited";
|
|
218
|
+
case "aborted":
|
|
219
|
+
return "timeout";
|
|
220
|
+
default:
|
|
221
|
+
return "internal";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/mock-host/MockPluginStorage.ts
|
|
226
|
+
var MockPluginStorage = class {
|
|
227
|
+
files = /* @__PURE__ */ new Map();
|
|
228
|
+
rateLimit;
|
|
229
|
+
now;
|
|
230
|
+
windowStartMs = 0;
|
|
231
|
+
windowCount = 0;
|
|
232
|
+
constructor(options = {}) {
|
|
233
|
+
this.rateLimit = options.rateLimit;
|
|
234
|
+
this.now = options.now ?? Date.now;
|
|
235
|
+
}
|
|
236
|
+
/** Store `buffer` under a new path below `meta.pathPrefix`. */
|
|
237
|
+
store(meta, buffer) {
|
|
238
|
+
this.enforceRateLimit();
|
|
239
|
+
const prefix = validatePluginStoragePrefix(meta.pathPrefix);
|
|
240
|
+
if (!prefix.ok) {
|
|
241
|
+
throw new PluginStorageUploadError(prefix.error, "invalid_prefix", { status: 400 });
|
|
242
|
+
}
|
|
243
|
+
if (buffer.byteLength > PLUGIN_STORAGE_UPLOAD_MAX_BYTES) {
|
|
244
|
+
throw new PluginStorageUploadError(
|
|
245
|
+
`The upload exceeds the ${PLUGIN_STORAGE_UPLOAD_MAX_BYTES}-byte limit.`,
|
|
246
|
+
"too_large",
|
|
247
|
+
{ status: 413 }
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const fileName = meta.fileName && meta.fileName.length > 0 ? meta.fileName : "upload.bin";
|
|
251
|
+
const type = resolvePluginStorageContentType(fileName, meta.contentType);
|
|
252
|
+
if (!type.ok) {
|
|
253
|
+
throw new PluginStorageUploadError(type.error, "unsupported_type", { status: 415 });
|
|
254
|
+
}
|
|
255
|
+
const path = `${prefix.prefix}/${randomGuidN()}${pluginStorageSafeExtension(fileName)}`;
|
|
256
|
+
const bytes = new Uint8Array(buffer.slice(0));
|
|
257
|
+
this.files.set(path, { path, fileName, contentType: type.contentType, bytes });
|
|
258
|
+
return { path, fileName, contentType: type.contentType, sizeBytes: bytes.byteLength };
|
|
259
|
+
}
|
|
260
|
+
/** The kernel refuses before the handler runs, so this is checked before anything else. */
|
|
261
|
+
enforceRateLimit() {
|
|
262
|
+
if (this.rateLimit === void 0) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const windowMs = (this.rateLimit.windowSeconds ?? 60) * 1e3;
|
|
266
|
+
const now = this.now();
|
|
267
|
+
if (this.windowCount === 0 || now - this.windowStartMs >= windowMs) {
|
|
268
|
+
this.windowStartMs = now;
|
|
269
|
+
this.windowCount = 0;
|
|
270
|
+
}
|
|
271
|
+
this.windowCount += 1;
|
|
272
|
+
if (this.windowCount > this.rateLimit.maxRequests) {
|
|
273
|
+
const retryAfterSeconds = Math.max(1, Math.ceil((this.windowStartMs + windowMs - now) / 1e3));
|
|
274
|
+
throw new PluginStorageUploadError(
|
|
275
|
+
"Rate limit exceeded. Please try again later.",
|
|
276
|
+
"rate_limited",
|
|
277
|
+
{ status: 429, retryAfterSeconds }
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** The stored file at `path`, or `undefined`. */
|
|
282
|
+
get(path) {
|
|
283
|
+
return this.files.get(path);
|
|
284
|
+
}
|
|
285
|
+
/** Every stored file, in upload order. */
|
|
286
|
+
list() {
|
|
287
|
+
return [...this.files.values()];
|
|
288
|
+
}
|
|
289
|
+
/** Forget every stored file. */
|
|
290
|
+
clear() {
|
|
291
|
+
this.files.clear();
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
function randomGuidN() {
|
|
295
|
+
const bytes = new Uint8Array(16);
|
|
296
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
297
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
298
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
299
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
300
|
+
}
|
|
301
|
+
|
|
37
302
|
// src/mock-host/InMemoryMcpTransport.ts
|
|
38
303
|
var InMemoryMcpTransport = class {
|
|
39
304
|
resources;
|
|
40
305
|
tools;
|
|
41
306
|
uploadHandler;
|
|
42
|
-
|
|
307
|
+
/** Where `uploadToStorage` writes. Read it back in a test, or from devtools under `dev:mock`. */
|
|
308
|
+
pluginStorage;
|
|
309
|
+
constructor(resources, tools = {}, uploadHandler, pluginStorage = new MockPluginStorage()) {
|
|
43
310
|
this.resources = resources;
|
|
44
311
|
this.tools = tools;
|
|
45
312
|
this.uploadHandler = uploadHandler;
|
|
313
|
+
this.pluginStorage = pluginStorage;
|
|
314
|
+
}
|
|
315
|
+
async uploadToStorage(meta, buffer, signal) {
|
|
316
|
+
if (signal?.aborted === true) {
|
|
317
|
+
throw abortError();
|
|
318
|
+
}
|
|
319
|
+
return this.pluginStorage.store(meta, buffer);
|
|
46
320
|
}
|
|
47
321
|
async getResource(uri) {
|
|
48
322
|
const data = this.resources[uri];
|
|
@@ -194,6 +468,6 @@ function WorkerMockHost(props) {
|
|
|
194
468
|
return /* @__PURE__ */ jsx(BridgeClientContext.Provider, { value: bridgeTransport, children: /* @__PURE__ */ jsx(DeclarativeMockHost, { ...declarativeProps }) });
|
|
195
469
|
}
|
|
196
470
|
|
|
197
|
-
export { DeclarativeMockHost, InMemoryBridgeTransport, InMemoryMcpTransport, WorkerMockHost };
|
|
471
|
+
export { DeclarativeMockHost, InMemoryBridgeTransport, InMemoryMcpTransport, MockPluginStorage, WorkerMockHost };
|
|
198
472
|
//# sourceMappingURL=index.js.map
|
|
199
473
|
//# sourceMappingURL=index.js.map
|