@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
@@ -69,6 +69,29 @@ var MCP_ERROR_CODES = [
69
69
  function isMcpErrorCode(value) {
70
70
  return typeof value === "string" && MCP_ERROR_CODES.includes(value);
71
71
  }
72
+ var RETRYABLE = ["rate_limited", "timeout", "unavailable"];
73
+ function isRetryableMcpErrorCode(code) {
74
+ return RETRYABLE.includes(code);
75
+ }
76
+ var McpToolError = class extends Error {
77
+ /** Machine-readable classification. `internal` when the host sent none. */
78
+ code;
79
+ /**
80
+ * Marks the instance for {@link isMcpToolError}. A structural marker rather than a prototype
81
+ * check because the error can be constructed in one bundle and inspected in another, where
82
+ * `instanceof` compares two different class objects and answers false.
83
+ */
84
+ isMcpToolError = true;
85
+ constructor(message, code = "internal") {
86
+ super(message);
87
+ this.name = "McpHostError";
88
+ this.code = code;
89
+ }
90
+ /** True when this failure is worth retrying unchanged. */
91
+ get retryable() {
92
+ return isRetryableMcpErrorCode(this.code);
93
+ }
94
+ };
72
95
  function mcpErrorCodeFromHttpStatus(status) {
73
96
  switch (status) {
74
97
  case 401:
@@ -122,6 +145,157 @@ function classifyHostError(err) {
122
145
  return "internal";
123
146
  }
124
147
 
148
+ // src/plugin/pluginStorageUpload.ts
149
+ var PLUGIN_STORAGE_UPLOAD_MAX_BYTES = 30 * 1024 * 1024;
150
+ var PLUGIN_STORAGE_PREFIX_MAX_LENGTH = 84;
151
+ var PLUGIN_STORAGE_PATH_MAX_LENGTH = 128;
152
+ var PluginStorageUploadError = class extends McpToolError {
153
+ /** What went wrong, in terms a caller branches on. */
154
+ reason;
155
+ /** HTTP status from the server, when the host surfaced one. */
156
+ status;
157
+ /**
158
+ * Seconds the server asked the caller to wait (`Retry-After`), on a `rate_limited` failure
159
+ * when the host forwarded the header. Absent otherwise.
160
+ */
161
+ retryAfterSeconds;
162
+ /** Structural marker for {@link isPluginStorageUploadError}. */
163
+ isPluginStorageUploadError = true;
164
+ constructor(message, reason, options = {}) {
165
+ super(message, options.code ?? codeForReason(reason, options.status));
166
+ this.name = "PluginStorageUploadError";
167
+ this.reason = reason;
168
+ this.status = options.status;
169
+ if (options.retryAfterSeconds !== void 0) {
170
+ this.retryAfterSeconds = options.retryAfterSeconds;
171
+ }
172
+ }
173
+ };
174
+ function parsePluginStorageRetryAfter(raw, nowMs = Date.now()) {
175
+ if (typeof raw === "number") {
176
+ return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : void 0;
177
+ }
178
+ if (typeof raw !== "string" || raw.trim().length === 0) {
179
+ return void 0;
180
+ }
181
+ const text = raw.trim();
182
+ if (/^\d+$/.test(text)) {
183
+ return Number(text);
184
+ }
185
+ const at = /[A-Za-z]/.test(text) ? Date.parse(text) : Number.NaN;
186
+ return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - nowMs) / 1e3));
187
+ }
188
+ function validatePluginStoragePrefix(raw) {
189
+ if (typeof raw !== "string" || raw.trim().length === 0) {
190
+ return { ok: false, error: "A storage path prefix is required." };
191
+ }
192
+ if (raw.length > PLUGIN_STORAGE_PREFIX_MAX_LENGTH) {
193
+ return {
194
+ ok: false,
195
+ 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}.`
196
+ };
197
+ }
198
+ if (!/^[A-Za-z0-9_\-/]+$/.test(raw)) {
199
+ return {
200
+ ok: false,
201
+ error: "The storage path prefix may contain only letters, digits, '-', '_' and '/' separators."
202
+ };
203
+ }
204
+ if (raw.split("/").some((segment) => segment.length === 0)) {
205
+ return {
206
+ ok: false,
207
+ error: "The storage path prefix must not start or end with '/' or contain an empty segment."
208
+ };
209
+ }
210
+ return { ok: true, prefix: raw };
211
+ }
212
+ var PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES = Object.freeze({
213
+ pdf: ["application/pdf"],
214
+ png: ["image/png"],
215
+ jpg: ["image/jpeg", "image/pjpeg", "image/jpg"],
216
+ jpeg: ["image/jpeg", "image/pjpeg", "image/jpg"],
217
+ gif: ["image/gif"],
218
+ webp: ["image/webp"],
219
+ doc: ["application/msword"],
220
+ docx: ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
221
+ xls: ["application/vnd.ms-excel"],
222
+ xlsx: ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"],
223
+ ppt: ["application/vnd.ms-powerpoint"],
224
+ pptx: ["application/vnd.openxmlformats-officedocument.presentationml.presentation"],
225
+ odt: ["application/vnd.oasis.opendocument.text"],
226
+ ods: ["application/vnd.oasis.opendocument.spreadsheet"],
227
+ odp: ["application/vnd.oasis.opendocument.presentation"],
228
+ txt: ["text/plain"],
229
+ csv: ["text/csv", "application/vnd.ms-excel", "application/csv", "text/plain"],
230
+ zip: ["application/zip", "application/x-zip-compressed", "application/x-zip"],
231
+ mp3: ["audio/mpeg", "audio/mp3"],
232
+ m4a: ["audio/mp4", "audio/x-m4a"],
233
+ mp4: ["video/mp4"]
234
+ });
235
+ var OCTET_STREAM = "application/octet-stream";
236
+ function pluginStorageSafeExtension(fileName) {
237
+ if (typeof fileName !== "string" || fileName.trim().length === 0) {
238
+ return "";
239
+ }
240
+ const leaf = fileName.split(/[\\/]/).pop() ?? "";
241
+ const dot = leaf.lastIndexOf(".");
242
+ if (dot < 0) {
243
+ return "";
244
+ }
245
+ const body = leaf.slice(dot + 1);
246
+ return /^[A-Za-z0-9]{1,10}$/.test(body) ? `.${body.toLowerCase()}` : "";
247
+ }
248
+ function resolvePluginStorageContentType(fileName, declaredContentType) {
249
+ const extension = pluginStorageSafeExtension(fileName).slice(1);
250
+ const allowed = extension.length > 0 && Object.prototype.hasOwnProperty.call(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES, extension) ? PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES[extension] : void 0;
251
+ if (allowed === void 0) {
252
+ return {
253
+ ok: false,
254
+ error: "This file type is not allowed. Allowed: " + Object.keys(PLUGIN_STORAGE_UPLOAD_CONTENT_TYPES).sort().join(", ") + "."
255
+ };
256
+ }
257
+ const declared = mediaTypeOf(declaredContentType);
258
+ if (declared === void 0 || declared === OCTET_STREAM || allowed.includes(declared)) {
259
+ return { ok: true, contentType: allowed[0] };
260
+ }
261
+ return {
262
+ ok: false,
263
+ error: `The declared content type '${declared}' does not match the file extension '.${extension}'.`
264
+ };
265
+ }
266
+ function mediaTypeOf(raw) {
267
+ if (typeof raw !== "string") {
268
+ return void 0;
269
+ }
270
+ const mediaType = raw.split(";")[0].trim().toLowerCase();
271
+ return /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mediaType) ? mediaType : void 0;
272
+ }
273
+ function codeForReason(reason, status) {
274
+ if (status !== void 0) {
275
+ return mcpErrorCodeFromHttpStatus(status);
276
+ }
277
+ switch (reason) {
278
+ case "invalid_prefix":
279
+ case "too_large":
280
+ case "unsupported_type":
281
+ return "invalid_request";
282
+ case "timeout":
283
+ return "timeout";
284
+ case "unauthorized":
285
+ return "unauthorized";
286
+ case "forbidden":
287
+ return "forbidden";
288
+ case "busy":
289
+ return "unavailable";
290
+ case "rate_limited":
291
+ return "rate_limited";
292
+ case "aborted":
293
+ return "timeout";
294
+ default:
295
+ return "internal";
296
+ }
297
+ }
298
+
125
299
  // src/host/version-skew.ts
126
300
  var EXTENSION_VERSION_HEADER = "x-extension-version";
127
301
  var EXTENSION_ACTIVE_VERSION_HEADER = "x-extension-active-version";
@@ -435,6 +609,9 @@ var WorkerRemoteDomTransport = class {
435
609
  case "ethisys:mcp:uploadDocument":
436
610
  this.dispatchMcp(message, "ethisys:mcp:uploadDocument:result", (m) => this.handleUploadDocument(m));
437
611
  return;
612
+ case "ethisys:mcp:uploadToStorage":
613
+ this.dispatchMcp(message, "ethisys:mcp:uploadToStorage:result", (m) => this.handleUploadToStorage(m));
614
+ return;
438
615
  case "ethisys:mcp:abort":
439
616
  this.inFlightAborts.get(message.id ?? "")?.abort();
440
617
  return;
@@ -672,11 +849,76 @@ var WorkerRemoteDomTransport = class {
672
849
  this.replyError(message.id, "ethisys:mcp:uploadDocument:result", err);
673
850
  }
674
851
  }
675
- replyError(id, type, err) {
852
+ async handleUploadToStorage(message) {
853
+ const resultType = "ethisys:mcp:uploadToStorage:result";
854
+ let token;
855
+ try {
856
+ token = await this.capabilityTokenProvider();
857
+ } catch (err) {
858
+ this.replyError(message.id, resultType, err, true);
859
+ return;
860
+ }
861
+ if (this.disposed) {
862
+ return;
863
+ }
864
+ try {
865
+ const result = await this.mcpClient.fetch({
866
+ kind: "uploadToStorage",
867
+ meta: message.meta,
868
+ buffer: message.buffer,
869
+ capabilityToken: token,
870
+ signal: this.requestSignal(message.id)
871
+ });
872
+ this.reportVersionSkew(result);
873
+ this.safePostMessage({
874
+ id: message.id,
875
+ type: resultType,
876
+ ok: result.ok,
877
+ data: result.data,
878
+ error: result.error,
879
+ code: classifyHostResponse(result),
880
+ // The HTTP status, on failure only. A bare number carries no host internals, and it
881
+ // is the only way the plugin can tell 413 (file too large) from 507 (quota) from 503
882
+ // (storage lock busy): the closed MCP code folds all three into `internal`/`unavailable`.
883
+ ...!result.ok && typeof result.status === "number" ? { status: result.status } : {},
884
+ // The server's `Retry-After` (429), as whole seconds, so a plugin can show how long
885
+ // to wait. Never acted on here: an upload is not retried automatically.
886
+ ...retryAfterField(result)
887
+ });
888
+ } catch (err) {
889
+ this.replyError(message.id, resultType, err, true);
890
+ }
891
+ }
892
+ replyError(id, type, err, includeStatus = false) {
676
893
  const message = err instanceof Error ? err.message : "MCP request failed";
677
- this.safePostMessage({ id, type, ok: false, error: message, code: classifyHostError(err) });
894
+ const status = includeStatus ? statusOf(err) : void 0;
895
+ const retryAfterSeconds = includeStatus && err !== null && typeof err === "object" ? parsePluginStorageRetryAfter(err.retryAfterSeconds) : void 0;
896
+ this.safePostMessage({
897
+ id,
898
+ type,
899
+ ok: false,
900
+ error: message,
901
+ code: classifyHostError(err),
902
+ ...status !== void 0 ? { status } : {},
903
+ ...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
904
+ });
678
905
  }
679
906
  };
907
+ function retryAfterField(result) {
908
+ if (result.ok || !result.headers) {
909
+ return {};
910
+ }
911
+ const key = Object.keys(result.headers).find((k) => k.toLowerCase() === "retry-after");
912
+ const seconds = key === void 0 ? void 0 : parsePluginStorageRetryAfter(result.headers[key]);
913
+ return seconds === void 0 ? {} : { retryAfterSeconds: seconds };
914
+ }
915
+ function statusOf(err) {
916
+ if (err === null || typeof err !== "object") {
917
+ return void 0;
918
+ }
919
+ const candidate = err;
920
+ return typeof candidate.status === "number" ? candidate.status : typeof candidate.statusCode === "number" ? candidate.statusCode : void 0;
921
+ }
680
922
 
681
923
  // src/host/worker/component-registry.ts
682
924
  var CONTRACT_B_PRIMITIVES = [
@@ -700,6 +942,83 @@ var CONTRACT_B_PRIMITIVES = [
700
942
  ];
701
943
  new Set(CONTRACT_B_PRIMITIVES);
702
944
 
945
+ // src/mock-host/MockPluginStorage.ts
946
+ var MockPluginStorage = class {
947
+ files = /* @__PURE__ */ new Map();
948
+ rateLimit;
949
+ now;
950
+ windowStartMs = 0;
951
+ windowCount = 0;
952
+ constructor(options = {}) {
953
+ this.rateLimit = options.rateLimit;
954
+ this.now = options.now ?? Date.now;
955
+ }
956
+ /** Store `buffer` under a new path below `meta.pathPrefix`. */
957
+ store(meta, buffer) {
958
+ this.enforceRateLimit();
959
+ const prefix = validatePluginStoragePrefix(meta.pathPrefix);
960
+ if (!prefix.ok) {
961
+ throw new PluginStorageUploadError(prefix.error, "invalid_prefix", { status: 400 });
962
+ }
963
+ if (buffer.byteLength > PLUGIN_STORAGE_UPLOAD_MAX_BYTES) {
964
+ throw new PluginStorageUploadError(
965
+ `The upload exceeds the ${PLUGIN_STORAGE_UPLOAD_MAX_BYTES}-byte limit.`,
966
+ "too_large",
967
+ { status: 413 }
968
+ );
969
+ }
970
+ const fileName = meta.fileName && meta.fileName.length > 0 ? meta.fileName : "upload.bin";
971
+ const type = resolvePluginStorageContentType(fileName, meta.contentType);
972
+ if (!type.ok) {
973
+ throw new PluginStorageUploadError(type.error, "unsupported_type", { status: 415 });
974
+ }
975
+ const path2 = `${prefix.prefix}/${randomGuidN()}${pluginStorageSafeExtension(fileName)}`;
976
+ const bytes = new Uint8Array(buffer.slice(0));
977
+ this.files.set(path2, { path: path2, fileName, contentType: type.contentType, bytes });
978
+ return { path: path2, fileName, contentType: type.contentType, sizeBytes: bytes.byteLength };
979
+ }
980
+ /** The kernel refuses before the handler runs, so this is checked before anything else. */
981
+ enforceRateLimit() {
982
+ if (this.rateLimit === void 0) {
983
+ return;
984
+ }
985
+ const windowMs = (this.rateLimit.windowSeconds ?? 60) * 1e3;
986
+ const now = this.now();
987
+ if (this.windowCount === 0 || now - this.windowStartMs >= windowMs) {
988
+ this.windowStartMs = now;
989
+ this.windowCount = 0;
990
+ }
991
+ this.windowCount += 1;
992
+ if (this.windowCount > this.rateLimit.maxRequests) {
993
+ const retryAfterSeconds = Math.max(1, Math.ceil((this.windowStartMs + windowMs - now) / 1e3));
994
+ throw new PluginStorageUploadError(
995
+ "Rate limit exceeded. Please try again later.",
996
+ "rate_limited",
997
+ { status: 429, retryAfterSeconds }
998
+ );
999
+ }
1000
+ }
1001
+ /** The stored file at `path`, or `undefined`. */
1002
+ get(path2) {
1003
+ return this.files.get(path2);
1004
+ }
1005
+ /** Every stored file, in upload order. */
1006
+ list() {
1007
+ return [...this.files.values()];
1008
+ }
1009
+ /** Forget every stored file. */
1010
+ clear() {
1011
+ this.files.clear();
1012
+ }
1013
+ };
1014
+ function randomGuidN() {
1015
+ const bytes = new Uint8Array(16);
1016
+ globalThis.crypto.getRandomValues(bytes);
1017
+ bytes[6] = bytes[6] & 15 | 64;
1018
+ bytes[8] = bytes[8] & 63 | 128;
1019
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1020
+ }
1021
+
703
1022
  // src/mock-host/cli.ts
704
1023
  async function loadResources(dir) {
705
1024
  const absDir = path__namespace.resolve(dir);
@@ -945,6 +1264,7 @@ import {
945
1264
  WorkerRemoteDomTransport,
946
1265
  SemanticComponentRegistry,
947
1266
  } from "@ethisyscore/extension-runtime/host";
1267
+ import { MockPluginStorage } from "@ethisyscore/extension-runtime/mock-host";
948
1268
 
949
1269
  const PRIMITIVES = ${safePrimitives};
950
1270
  const bundleUrl = ${safeUrl};
@@ -960,12 +1280,18 @@ const registry = SemanticComponentRegistry.fromMap(componentMap);
960
1280
 
961
1281
  // Mock-host MCP client: no platform, so every tool/resource call resolves
962
1282
  // to a friendly placeholder. Authors who need real MCP traffic should run
963
- // against the full platform host.
1283
+ // against the full platform host. uploadToStorage writes into an in-memory
1284
+ // plugin storage and returns a server-style {prefix}/{guid}{ext} path; an
1285
+ // invalid prefix or an over-size body throws with status 400 / 413, which the
1286
+ // transport forwards to the plugin as a typed error.
1287
+ const pluginStorage = new MockPluginStorage();
964
1288
  const mcpClient = {
965
- fetch: async (req) => ({
966
- ok: true,
967
- data: { mockHost: true, kind: req.kind, name: req.name ?? req.uri },
968
- }),
1289
+ fetch: async (req) => req.kind === "uploadToStorage"
1290
+ ? { ok: true, data: pluginStorage.store(req.meta, req.buffer) }
1291
+ : {
1292
+ ok: true,
1293
+ data: { mockHost: true, kind: req.kind, name: req.name ?? req.uri },
1294
+ },
969
1295
  };
970
1296
 
971
1297
  // The capability token never crosses the worker boundary \u2014 supply a marker
@@ -988,10 +1314,15 @@ transport.onRemoteDom(() => {
988
1314
  // devtools (e.g., to terminate the worker between iteration cycles).
989
1315
  window.__MOCK_HOST_TRANSPORT__ = transport;
990
1316
  window.__MOCK_HOST_REGISTRY__ = registry;
1317
+ window.__MOCK_HOST_PLUGIN_STORAGE__ = pluginStorage;
991
1318
  `;
992
1319
  }
1320
+ var defaultMockPluginStorage = new MockPluginStorage();
993
1321
  var defaultMockMcpClient = {
994
1322
  fetch: async (req) => {
1323
+ if (req.kind === "uploadToStorage") {
1324
+ return { ok: true, data: defaultMockPluginStorage.store(req.meta, req.buffer) };
1325
+ }
995
1326
  if (req.kind === "uploadDocument") {
996
1327
  return {
997
1328
  ok: true,
@@ -1174,6 +1505,8 @@ async function run(args) {
1174
1505
  }
1175
1506
 
1176
1507
  exports.bootContractBHost = bootContractBHost;
1508
+ exports.defaultMockMcpClient = defaultMockMcpClient;
1509
+ exports.defaultMockPluginStorage = defaultMockPluginStorage;
1177
1510
  exports.loadResources = loadResources;
1178
1511
  exports.loadTools = loadTools;
1179
1512
  exports.normaliseEntry = normaliseEntry;