@ethisyscore/extension-runtime 1.137.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.
@@ -7,6 +7,20 @@ interface MockStoredFile {
7
7
  readonly contentType: string;
8
8
  readonly bytes: Uint8Array;
9
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
+ }
10
24
  /**
11
25
  * In-memory stand-in for an app's plugin storage, answering the `uploadToStorage` request kind for
12
26
  * `dev:mock` and tests.
@@ -18,12 +32,22 @@ interface MockStoredFile {
18
32
  * - the path is server-chosen, `{prefix}/{32 hex}{ext}` with the kernel's extension rule, so two
19
33
  * uploads never share a path.
20
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
+ *
21
38
  * It does not model quota (507), the storage lock or upload slots (503), or the deadline (408).
22
39
  */
23
40
  declare class MockPluginStorage {
24
41
  private readonly files;
42
+ private readonly rateLimit?;
43
+ private readonly now;
44
+ private windowStartMs;
45
+ private windowCount;
46
+ constructor(options?: MockPluginStorageOptions);
25
47
  /** Store `buffer` under a new path below `meta.pathPrefix`. */
26
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;
27
51
  /** The stored file at `path`, or `undefined`. */
28
52
  get(path: string): MockStoredFile | undefined;
29
53
  /** Every stored file, in upload order. */
@@ -32,4 +56,4 @@ declare class MockPluginStorage {
32
56
  clear(): void;
33
57
  }
34
58
 
35
- export { MockPluginStorage as M, type MockStoredFile as a };
59
+ export { MockPluginStorage as M, type MockPluginStorageOptions as a, type MockPluginStorageRateLimit as b, type MockStoredFile as c };
@@ -7,6 +7,20 @@ interface MockStoredFile {
7
7
  readonly contentType: string;
8
8
  readonly bytes: Uint8Array;
9
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
+ }
10
24
  /**
11
25
  * In-memory stand-in for an app's plugin storage, answering the `uploadToStorage` request kind for
12
26
  * `dev:mock` and tests.
@@ -18,12 +32,22 @@ interface MockStoredFile {
18
32
  * - the path is server-chosen, `{prefix}/{32 hex}{ext}` with the kernel's extension rule, so two
19
33
  * uploads never share a path.
20
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
+ *
21
38
  * It does not model quota (507), the storage lock or upload slots (503), or the deadline (408).
22
39
  */
23
40
  declare class MockPluginStorage {
24
41
  private readonly files;
42
+ private readonly rateLimit?;
43
+ private readonly now;
44
+ private windowStartMs;
45
+ private windowCount;
46
+ constructor(options?: MockPluginStorageOptions);
25
47
  /** Store `buffer` under a new path below `meta.pathPrefix`. */
26
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;
27
51
  /** The stored file at `path`, or `undefined`. */
28
52
  get(path: string): MockStoredFile | undefined;
29
53
  /** Every stored file, in upload order. */
@@ -32,4 +56,4 @@ declare class MockPluginStorage {
32
56
  clear(): void;
33
57
  }
34
58
 
35
- export { MockPluginStorage as M, type MockStoredFile as a };
59
+ export { MockPluginStorage as M, type MockPluginStorageOptions as a, type MockPluginStorageRateLimit as b, type MockStoredFile as c };
@@ -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";
@@ -965,7 +979,10 @@ var WorkerRemoteDomTransport = class {
965
979
  // The HTTP status, on failure only. A bare number carries no host internals, and it
966
980
  // is the only way the plugin can tell 413 (file too large) from 507 (quota) from 503
967
981
  // (storage lock busy): the closed MCP code folds all three into `internal`/`unavailable`.
968
- ...!result.ok && typeof result.status === "number" ? { status: result.status } : {}
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)
969
986
  });
970
987
  } catch (err) {
971
988
  this.replyError(message.id, resultType, err, true);
@@ -974,16 +991,26 @@ var WorkerRemoteDomTransport = class {
974
991
  replyError(id, type, err, includeStatus = false) {
975
992
  const message = err instanceof Error ? err.message : "MCP request failed";
976
993
  const status = includeStatus ? statusOf(err) : void 0;
994
+ const retryAfterSeconds = includeStatus && err !== null && typeof err === "object" ? parsePluginStorageRetryAfter(err.retryAfterSeconds) : void 0;
977
995
  this.safePostMessage({
978
996
  id,
979
997
  type,
980
998
  ok: false,
981
999
  error: message,
982
1000
  code: classifyHostError(err),
983
- ...status !== void 0 ? { status } : {}
1001
+ ...status !== void 0 ? { status } : {},
1002
+ ...retryAfterSeconds !== void 0 ? { retryAfterSeconds } : {}
984
1003
  });
985
1004
  }
986
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
+ }
987
1014
  function statusOf(err) {
988
1015
  if (err === null || typeof err !== "object") {
989
1016
  return void 0;