machine-bridge-mcp 3.0.0-beta.17 → 3.0.0-beta.21

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 (74) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +2 -0
  3. package/SECURITY.md +9 -7
  4. package/browser-extension/browser-error-boundary.js +45 -0
  5. package/browser-extension/browser-operations.js +1 -1
  6. package/browser-extension/manifest.json +1 -1
  7. package/browser-extension/service-worker.js +9 -8
  8. package/docs/ARCHITECTURE.md +17 -15
  9. package/docs/AUDIT.md +30 -0
  10. package/docs/ENGINEERING.md +3 -3
  11. package/docs/GETTING_STARTED.md +1 -1
  12. package/docs/LOCAL_AUTHORIZATION.md +2 -2
  13. package/docs/LOCAL_AUTOMATION.md +1 -1
  14. package/docs/LOGGING.md +3 -1
  15. package/docs/MULTI_ACCOUNT.md +2 -2
  16. package/docs/OPERATIONS.md +13 -5
  17. package/docs/OVERVIEW.md +2 -2
  18. package/docs/PRIVACY.md +1 -1
  19. package/docs/TESTING.md +17 -13
  20. package/docs/THREAT_MODEL.md +13 -8
  21. package/docs/UPGRADING.md +19 -1
  22. package/package.json +1 -1
  23. package/scripts/coverage-check.mjs +12 -0
  24. package/src/local/account-admin.mjs +68 -1
  25. package/src/local/agent-skill-discovery.mjs +16 -5
  26. package/src/local/app-automation.mjs +27 -7
  27. package/src/local/cli-options.mjs +1 -1
  28. package/src/local/path-inspection.mjs +23 -0
  29. package/src/local/process-tree-ownership.mjs +17 -5
  30. package/src/local/process-tree.mjs +6 -2
  31. package/src/local/relay-connection.mjs +24 -11
  32. package/src/local/resource-operations.mjs +37 -8
  33. package/src/local/runtime-activation.mjs +16 -0
  34. package/src/local/runtime-capabilities.mjs +15 -3
  35. package/src/local/runtime.mjs +7 -4
  36. package/src/local/worker-deployment.mjs +16 -6
  37. package/src/local/workspace-file-service.mjs +29 -11
  38. package/src/worker/daemon-socket-attachment.ts +52 -0
  39. package/src/worker/daemon-sockets.ts +17 -48
  40. package/src/worker/durable-stream-calls.ts +129 -0
  41. package/src/worker/durable-stream-result.ts +22 -0
  42. package/src/worker/http.ts +37 -15
  43. package/src/worker/index.ts +143 -146
  44. package/src/worker/mcp-pending-call-expiry.ts +20 -0
  45. package/src/worker/mcp-pending-call-inspection.ts +33 -0
  46. package/src/worker/mcp-pending-call-records.ts +58 -0
  47. package/src/worker/mcp-pending-call-storage.ts +18 -0
  48. package/src/worker/mcp-pending-call-store.ts +285 -0
  49. package/src/worker/mcp-resumption-index.ts +41 -0
  50. package/src/worker/mcp-resumption-records.ts +8 -2
  51. package/src/worker/mcp-resumption.ts +84 -86
  52. package/src/worker/mcp-stream-dispatch.ts +42 -54
  53. package/src/worker/mcp-stream-proxy.ts +7 -69
  54. package/src/worker/mcp-stream-subscription.ts +98 -0
  55. package/src/worker/oauth-controller.ts +11 -2
  56. package/src/worker/oauth-refresh-exchange.ts +147 -0
  57. package/src/worker/oauth-refresh-families.ts +59 -17
  58. package/src/worker/oauth-state.ts +13 -2
  59. package/src/worker/oauth-token-derivation.ts +33 -0
  60. package/src/worker/oauth-token-issuance.ts +106 -0
  61. package/src/worker/oauth-tokens.ts +12 -184
  62. package/src/worker/observability.ts +19 -0
  63. package/src/worker/pending-admission.ts +15 -0
  64. package/src/worker/pending-call-contract.ts +4 -7
  65. package/src/worker/pending-calls.ts +4 -10
  66. package/src/worker/runtime-alarm-storage.ts +35 -0
  67. package/src/worker/runtime-alarm.ts +30 -20
  68. package/src/worker/websocket-protocol.ts +4 -0
  69. package/src/worker/worker-edge-guard.ts +81 -0
  70. package/src/worker/worker-edge-log.ts +63 -0
  71. package/src/worker/worker-entry.ts +50 -0
  72. package/src/worker/worker-metadata.ts +39 -0
  73. package/src/worker/worker-static-routes.ts +38 -50
  74. package/wrangler.jsonc +8 -0
@@ -2,7 +2,9 @@ import {
2
2
  emptyOAuthRefreshStore,
3
3
  isCurrentOAuthRefreshStore,
4
4
  upgradeOAuthRefreshStore,
5
+ type ConsumedOAuthRefreshToken,
5
6
  type OAuthRefreshStore,
7
+ type OAuthRefreshToken,
6
8
  type OAuthStore,
7
9
  } from "./oauth-state.ts";
8
10
  import { HttpError } from "./http.ts";
@@ -10,6 +12,8 @@ import { HttpError } from "./http.ts";
10
12
  export const OAUTH_REFRESH_STORE_KEY = "oauth-refresh";
11
13
  const MAX_CONSUMED_REFRESH_TOKENS = 4096;
12
14
  const MAX_REVOKED_REFRESH_FAMILIES = 1024;
15
+ export const OAUTH_REFRESH_RETRY_GRACE_SECONDS = 30;
16
+ export const MAX_REFRESH_RETRY_ISSUES = 2;
13
17
  const TOKEN_HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
14
18
  const FAMILY_ID_PATTERN = /^mcp_family_[A-Za-z0-9_-]{43}$/;
15
19
 
@@ -64,20 +68,41 @@ export function recordConsumedRefreshToken(
64
68
  oauthStore: OAuthStore,
65
69
  store: OAuthRefreshStore,
66
70
  tokenHash: string,
67
- familyId: string,
71
+ source: OAuthRefreshToken,
68
72
  expiresAt: number,
69
73
  consumedAt = Math.floor(Date.now() / 1000),
70
74
  ): void {
71
- if (!TOKEN_HASH_PATTERN.test(tokenHash) || !FAMILY_ID_PATTERN.test(familyId)) {
75
+ if (!TOKEN_HASH_PATTERN.test(tokenHash) || !FAMILY_ID_PATTERN.test(source.family_id)) {
72
76
  throw new Error("consumed refresh-token identity is invalid");
73
77
  }
74
78
  if (!Number.isSafeInteger(consumedAt) || !Number.isSafeInteger(expiresAt) || consumedAt <= 0 || expiresAt <= consumedAt) {
75
79
  throw new Error("consumed refresh-token lifetime is invalid");
76
80
  }
77
- store.consumed[tokenHash] = { family_id: familyId, consumed_at: consumedAt, expires_at: expiresAt };
81
+ store.consumed[tokenHash] = {
82
+ family_id: source.family_id,
83
+ consumed_at: consumedAt,
84
+ expires_at: expiresAt,
85
+ retry_until: Math.min(expiresAt, consumedAt + OAUTH_REFRESH_RETRY_GRACE_SECONDS),
86
+ retry_issues: 0,
87
+ source: { ...source },
88
+ };
78
89
  pruneOAuthRefreshReplayState(store, oauthStore);
79
90
  }
80
91
 
92
+ export function consumedRefreshRetrySource(
93
+ marker: ConsumedOAuthRefreshToken,
94
+ now = Math.floor(Date.now() / 1000),
95
+ ): OAuthRefreshToken | null {
96
+ if (!marker.source || !Number.isSafeInteger(marker.retry_until) || marker.retry_until! < now) return null;
97
+ if (!Number.isSafeInteger(marker.retry_issues) || marker.retry_issues! < 0 || marker.retry_issues! >= MAX_REFRESH_RETRY_ISSUES) return null;
98
+ return { ...marker.source };
99
+ }
100
+
101
+ export function recordConsumedRefreshRetry(marker: ConsumedOAuthRefreshToken): void {
102
+ const current = Number.isSafeInteger(marker.retry_issues) ? marker.retry_issues! : 0;
103
+ marker.retry_issues = current + 1;
104
+ }
105
+
81
106
  export function pruneOAuthRefreshReplayState(store: OAuthRefreshStore, oauthStore?: OAuthStore): boolean {
82
107
  let changed = false;
83
108
  const consumed = Object.entries(store.consumed).sort((left, right) => (
@@ -102,8 +127,38 @@ export function pruneOAuthRefreshReplayState(store: OAuthRefreshStore, oauthStor
102
127
 
103
128
  function validRefreshStoreRecords(store: OAuthRefreshStore): boolean {
104
129
  return Object.entries(store.tokens).every(([key, token]) => (
130
+ TOKEN_HASH_PATTERN.test(key) && validRefreshTokenRecord(token)
131
+ )) && Object.entries(store.consumed).every(([key, token]) => (
105
132
  TOKEN_HASH_PATTERN.test(key)
106
133
  && plainRecord(token)
134
+ && FAMILY_ID_PATTERN.test(token.family_id)
135
+ && validTimestamp(token.consumed_at)
136
+ && validTimestamp(token.expires_at)
137
+ && token.consumed_at < token.expires_at
138
+ && (token.retry_until === undefined || (
139
+ validTimestamp(token.retry_until)
140
+ && token.retry_until >= token.consumed_at
141
+ && token.retry_until <= token.expires_at
142
+ ))
143
+ && (token.retry_issues === undefined || (
144
+ Number.isSafeInteger(token.retry_issues)
145
+ && token.retry_issues >= 0
146
+ && token.retry_issues <= MAX_REFRESH_RETRY_ISSUES
147
+ ))
148
+ && (token.source === undefined || validRefreshTokenRecord(token.source))
149
+ && ((token.source === undefined && token.retry_until === undefined && token.retry_issues === undefined)
150
+ || (token.source !== undefined && token.retry_until !== undefined && token.retry_issues !== undefined
151
+ && token.source.family_id === token.family_id))
152
+ )) && Object.entries(store.revoked_families).every(([familyId, value]) => (
153
+ FAMILY_ID_PATTERN.test(familyId)
154
+ && plainRecord(value)
155
+ && value.reason === "replay"
156
+ && validTimestamp(value.expires_at)
157
+ ));
158
+ }
159
+
160
+ function validRefreshTokenRecord(token: OAuthRefreshToken): boolean {
161
+ return plainRecord(token)
107
162
  && /^mcp_client_[A-Za-z0-9_-]{43}$/.test(token.client_id)
108
163
  && /^acct_[A-Za-z0-9_-]{20,96}$/.test(token.account_id)
109
164
  && Number.isSafeInteger(token.account_version)
@@ -122,20 +177,7 @@ function validRefreshStoreRecords(store: OAuthRefreshStore): boolean {
122
177
  && validTimestamp(token.expires_at)
123
178
  && validTimestamp(token.family_expires_at)
124
179
  && token.issued_at < token.expires_at
125
- && token.expires_at <= token.family_expires_at
126
- )) && Object.entries(store.consumed).every(([key, token]) => (
127
- TOKEN_HASH_PATTERN.test(key)
128
- && plainRecord(token)
129
- && FAMILY_ID_PATTERN.test(token.family_id)
130
- && validTimestamp(token.consumed_at)
131
- && validTimestamp(token.expires_at)
132
- && token.consumed_at < token.expires_at
133
- )) && Object.entries(store.revoked_families).every(([familyId, value]) => (
134
- FAMILY_ID_PATTERN.test(familyId)
135
- && plainRecord(value)
136
- && value.reason === "replay"
137
- && validTimestamp(value.expires_at)
138
- ));
180
+ && token.expires_at <= token.family_expires_at;
139
181
  }
140
182
 
141
183
  function validTimestamp(value: unknown): value is number {
@@ -1,7 +1,7 @@
1
1
  import { normalizeAccountRole, type AccountRole } from "./access.ts";
2
2
 
3
3
  const OAUTH_STORE_SCHEMA_VERSION = 1;
4
- const OAUTH_REFRESH_STORE_SCHEMA_VERSION = 2;
4
+ const OAUTH_REFRESH_STORE_SCHEMA_VERSION = 3;
5
5
  export const OFFLINE_ACCESS_SCOPE = "offline_access";
6
6
  const PASSWORD_TOKEN_PATTERN = /^[a-z][a-z0-9_]{2,31}_[A-Za-z0-9_-]{43}$/;
7
7
  const ACCOUNT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]{1,62}[a-z0-9]$/;
@@ -70,6 +70,9 @@ export interface ConsumedOAuthRefreshToken {
70
70
  family_id: string;
71
71
  consumed_at: number;
72
72
  expires_at: number;
73
+ retry_until?: number;
74
+ retry_issues?: number;
75
+ source?: OAuthRefreshToken;
73
76
  }
74
77
 
75
78
  export interface RevokedOAuthRefreshFamily {
@@ -157,7 +160,15 @@ export function isCurrentOAuthRefreshStore(value: unknown): value is OAuthRefres
157
160
  export function upgradeOAuthRefreshStore(value: unknown): OAuthRefreshStore | null {
158
161
  if (isCurrentOAuthRefreshStore(value)) return value;
159
162
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
160
- const legacy = value as { schema_version?: unknown; tokens?: unknown };
163
+ const legacy = value as { schema_version?: unknown; tokens?: unknown; consumed?: unknown; revoked_families?: unknown };
164
+ if (legacy.schema_version === 2 && isRecord(legacy.tokens) && isRecord(legacy.consumed) && isRecord(legacy.revoked_families)) {
165
+ return {
166
+ schema_version: OAUTH_REFRESH_STORE_SCHEMA_VERSION,
167
+ tokens: legacy.tokens as OAuthRefreshStore["tokens"],
168
+ consumed: legacy.consumed as OAuthRefreshStore["consumed"],
169
+ revoked_families: legacy.revoked_families as OAuthRefreshStore["revoked_families"],
170
+ };
171
+ }
161
172
  if (legacy.schema_version !== 1 || !isRecord(legacy.tokens)) return null;
162
173
  const upgraded = emptyOAuthRefreshStore();
163
174
  const now = Math.floor(Date.now() / 1000);
@@ -0,0 +1,33 @@
1
+ const DERIVATION_DOMAIN = "machine-bridge-mcp/oauth-refresh-replacement/v1";
2
+
3
+ export async function deriveRefreshReplacementPair(
4
+ keyMaterial: string,
5
+ consumedRefreshToken: string,
6
+ ): Promise<{ accessToken: string; refreshToken: string }> {
7
+ if (!keyMaterial) throw new Error("OAuth token derivation key is not configured");
8
+ if (!/^mcp_rt_[A-Za-z0-9_-]{43}$/.test(consumedRefreshToken)) {
9
+ throw new Error("refresh replacement seed is invalid");
10
+ }
11
+ const encoder = new TextEncoder();
12
+ const key = await crypto.subtle.importKey(
13
+ "raw",
14
+ encoder.encode(keyMaterial),
15
+ { name: "HMAC", hash: "SHA-256" },
16
+ false,
17
+ ["sign"],
18
+ );
19
+ const [accessDigest, refreshDigest] = await Promise.all([
20
+ crypto.subtle.sign("HMAC", key, encoder.encode(`${DERIVATION_DOMAIN}\0access\0${consumedRefreshToken}`)),
21
+ crypto.subtle.sign("HMAC", key, encoder.encode(`${DERIVATION_DOMAIN}\0refresh\0${consumedRefreshToken}`)),
22
+ ]);
23
+ return {
24
+ accessToken: `mcp_at_${base64Url(new Uint8Array(accessDigest))}`,
25
+ refreshToken: `mcp_rt_${base64Url(new Uint8Array(refreshDigest))}`,
26
+ };
27
+ }
28
+
29
+ function base64Url(bytes: Uint8Array): string {
30
+ let binary = "";
31
+ for (const byte of bytes) binary += String.fromCharCode(byte);
32
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
33
+ }
@@ -0,0 +1,106 @@
1
+ import {
2
+ pruneClientRecordByExpiry,
3
+ pruneRecordByExpiry,
4
+ randomToken,
5
+ sha256Hex,
6
+ type OAuthCode,
7
+ type OAuthRefreshStore,
8
+ type OAuthRefreshToken,
9
+ type OAuthStore,
10
+ } from "./oauth-state.ts";
11
+ import { HttpError, json } from "./http.ts";
12
+ import { OAUTH_REFRESH_STORE_KEY } from "./oauth-refresh-families.ts";
13
+ import { deriveRefreshReplacementPair } from "./oauth-token-derivation.ts";
14
+
15
+ const ACCESS_TOKEN_TTL_SECONDS = 15 * 60;
16
+ const REFRESH_TOKEN_IDLE_TTL_SECONDS = 60 * 60 * 24 * 14;
17
+ const REFRESH_TOKEN_FAMILY_TTL_SECONDS = 60 * 60 * 24 * 30;
18
+ const MAX_ACCESS_TOKENS = 500;
19
+ const MAX_ACCESS_TOKENS_PER_CLIENT = 20;
20
+ const MAX_REFRESH_TOKENS = 500;
21
+ const MAX_REFRESH_TOKENS_PER_CLIENT = 20;
22
+
23
+ type OAuthLock = <T>(callback: () => Promise<T>) => Promise<T>;
24
+ export type OAuthRefreshEvent = "rotated" | "retry_issued" | "retry_exhausted" | "family_revoked" | "rejected";
25
+
26
+ export interface OAuthTokenExchangeOptions {
27
+ storage: DurableObjectStorage;
28
+ tokenVersion: string;
29
+ serverName: string;
30
+ loadOAuthStore: () => Promise<OAuthStore>;
31
+ withLock: OAuthLock;
32
+ onRefreshEvent?: (event: OAuthRefreshEvent) => void;
33
+ }
34
+
35
+ export interface IssuedTokenPair {
36
+ accessToken: string;
37
+ refreshToken: string;
38
+ }
39
+
40
+ export async function issueTokenPair(
41
+ oauthStore: OAuthStore,
42
+ refreshStore: OAuthRefreshStore,
43
+ source: OAuthCode | OAuthRefreshToken,
44
+ tokenVersion: string,
45
+ dpopJkt?: string,
46
+ issuance: { derivationSeed?: string; issuedAt?: number } = {},
47
+ ): Promise<IssuedTokenPair> {
48
+ if (!tokenVersion) throw new HttpError(503, "server_error", "OAuth token version is not configured");
49
+ const now = Number.isSafeInteger(issuance.issuedAt) && Number(issuance.issuedAt) > 0
50
+ ? Number(issuance.issuedAt)
51
+ : Math.floor(Date.now() / 1000);
52
+ const familyId = "family_id" in source && source.family_id ? source.family_id : randomToken("mcp_family");
53
+ const familyExpiresAt = "family_expires_at" in source && source.family_expires_at
54
+ ? source.family_expires_at
55
+ : now + REFRESH_TOKEN_FAMILY_TTL_SECONDS;
56
+ if (familyExpiresAt <= now) throw new HttpError(400, "invalid_grant", "refresh-token family expired");
57
+ const issued = issuance.derivationSeed
58
+ ? await deriveRefreshReplacementPair(tokenVersion, issuance.derivationSeed)
59
+ : { accessToken: randomToken("mcp_at"), refreshToken: randomToken("mcp_rt") };
60
+ const { accessToken, refreshToken } = issued;
61
+ const common = {
62
+ client_id: source.client_id,
63
+ account_id: source.account_id,
64
+ account_version: source.account_version,
65
+ role: source.role,
66
+ scope: source.scope,
67
+ resource: source.resource,
68
+ version: tokenVersion,
69
+ family_id: familyId,
70
+ ...(dpopJkt ? { dpop_jkt: dpopJkt } : {}),
71
+ };
72
+ oauthStore.tokens[`sha256:${await sha256Hex(accessToken)}`] = {
73
+ ...common,
74
+ expires_at: now + ACCESS_TOKEN_TTL_SECONDS,
75
+ };
76
+ refreshStore.tokens[`sha256:${await sha256Hex(refreshToken)}`] = {
77
+ ...common,
78
+ family_id: familyId,
79
+ family_expires_at: familyExpiresAt,
80
+ issued_at: now,
81
+ expires_at: Math.min(now + REFRESH_TOKEN_IDLE_TTL_SECONDS, familyExpiresAt),
82
+ };
83
+ pruneClientRecordByExpiry(oauthStore.tokens, source.client_id, MAX_ACCESS_TOKENS_PER_CLIENT);
84
+ pruneRecordByExpiry(oauthStore.tokens, MAX_ACCESS_TOKENS);
85
+ pruneClientRecordByExpiry(refreshStore.tokens, source.client_id, MAX_REFRESH_TOKENS_PER_CLIENT);
86
+ pruneRecordByExpiry(refreshStore.tokens, MAX_REFRESH_TOKENS);
87
+ return { accessToken, refreshToken };
88
+ }
89
+
90
+ export function tokenResponse(issued: IssuedTokenPair, scope: string, dpopJkt?: string): Response {
91
+ return json({
92
+ access_token: issued.accessToken,
93
+ refresh_token: issued.refreshToken,
94
+ token_type: dpopJkt ? "DPoP" : "Bearer",
95
+ expires_in: ACCESS_TOKEN_TTL_SECONDS,
96
+ scope,
97
+ });
98
+ }
99
+
100
+ export async function saveOAuthStores(
101
+ oauthStore: OAuthStore,
102
+ refreshStore: OAuthRefreshStore,
103
+ storage: DurableObjectStorage,
104
+ ): Promise<void> {
105
+ await storage.put({ oauth: oauthStore, [OAUTH_REFRESH_STORE_KEY]: refreshStore });
106
+ }
@@ -1,35 +1,17 @@
1
- import {
2
- normalizeOAuthScope, pkceS256, pruneClientRecordByExpiry, pruneRecordByExpiry,
3
- randomToken, safeEqual, sha256Hex,
4
- type OAuthCode, type OAuthRefreshStore, type OAuthRefreshToken, type OAuthStore,
5
- } from "./oauth-state.ts";
6
- import { HttpError, json, parseRequestBody } from "./http.ts";
7
- import { loadOAuthRefreshStore, OAUTH_REFRESH_STORE_KEY, recordConsumedRefreshToken, revokeOAuthRefreshFamily } from "./oauth-refresh-families.ts";
8
1
  import { consumeDpopProof, verifyDpopProof, type VerifiedDpopProof } from "./dpop.ts";
2
+ import { json, parseRequestBody } from "./http.ts";
3
+ import { loadOAuthRefreshStore } from "./oauth-refresh-families.ts";
4
+ import { exchangeRefreshToken } from "./oauth-refresh-exchange.ts";
5
+ import {
6
+ issueTokenPair,
7
+ saveOAuthStores,
8
+ tokenResponse,
9
+ type OAuthTokenExchangeOptions,
10
+ } from "./oauth-token-issuance.ts";
11
+ import { pkceS256, safeEqual } from "./oauth-state.ts";
9
12
 
10
- const ACCESS_TOKEN_TTL_SECONDS = 15 * 60;
11
- const REFRESH_TOKEN_IDLE_TTL_SECONDS = 60 * 60 * 24 * 14;
12
- const REFRESH_TOKEN_FAMILY_TTL_SECONDS = 60 * 60 * 24 * 30;
13
+ export type { OAuthRefreshEvent } from "./oauth-token-issuance.ts";
13
14
  const OAUTH_BODY_LIMIT_BYTES = 64 * 1024;
14
- const MAX_ACCESS_TOKENS = 500;
15
- const MAX_ACCESS_TOKENS_PER_CLIENT = 20;
16
- const MAX_REFRESH_TOKENS = 500;
17
- const MAX_REFRESH_TOKENS_PER_CLIENT = 20;
18
-
19
- type OAuthLock = <T>(callback: () => Promise<T>) => Promise<T>;
20
-
21
- interface OAuthTokenExchangeOptions {
22
- storage: DurableObjectStorage;
23
- tokenVersion: string;
24
- serverName: string;
25
- loadOAuthStore: () => Promise<OAuthStore>;
26
- withLock: OAuthLock;
27
- }
28
-
29
- interface IssuedTokenPair {
30
- accessToken: string;
31
- refreshToken: string;
32
- }
33
15
 
34
16
  export async function exchangeOAuthToken(
35
17
  request: Request,
@@ -73,10 +55,7 @@ async function exchangeAuthorizationCode(
73
55
  }
74
56
  const client = oauthStore.clients[record.client_id];
75
57
  if (!client) return json({ error: "invalid_grant", error_description: "unknown client" }, 400);
76
-
77
- if (dpop && !(await consumeDpopProof(options.storage, dpop))) {
78
- return json({ error: "invalid_dpop_proof" }, 400);
79
- }
58
+ if (dpop && !(await consumeDpopProof(options.storage, dpop))) return json({ error: "invalid_dpop_proof" }, 400);
80
59
  const issued = await issueTokenPair(oauthStore, refreshStore, record, options.tokenVersion, dpop?.jkt);
81
60
  delete oauthStore.codes[code];
82
61
  client.last_used_at = Math.floor(Date.now() / 1000);
@@ -84,154 +63,3 @@ async function exchangeAuthorizationCode(
84
63
  return tokenResponse(issued, record.scope, dpop?.jkt);
85
64
  });
86
65
  }
87
-
88
- async function exchangeRefreshToken(
89
- body: Record<string, unknown>,
90
- base: string,
91
- options: OAuthTokenExchangeOptions,
92
- dpop?: VerifiedDpopProof,
93
- ): Promise<Response> {
94
- const refreshToken = String(body.refresh_token ?? "");
95
- if (!/^mcp_rt_[A-Za-z0-9_-]{43}$/.test(refreshToken)) return json({ error: "invalid_grant" }, 400);
96
- return options.withLock(async () => {
97
- const oauthStore = await options.loadOAuthStore();
98
- const refreshStore = await loadOAuthRefreshStore(oauthStore, options.storage);
99
- const refreshKey = `sha256:${await sha256Hex(refreshToken)}`;
100
- const record = refreshStore.tokens[refreshKey];
101
- if (!record) {
102
- const consumed = refreshStore.consumed[refreshKey];
103
- if (consumed && consumed.expires_at > Math.floor(Date.now() / 1000)) {
104
- revokeOAuthRefreshFamily(oauthStore, refreshStore, consumed.family_id, consumed.expires_at);
105
- await saveOAuthStores(oauthStore, refreshStore, options.storage);
106
- }
107
- return json({ error: "invalid_grant" }, 400);
108
- }
109
- if (record.dpop_jkt && record.dpop_jkt !== dpop?.jkt) return json({ error: "invalid_dpop_proof" }, 400);
110
- if (!record.dpop_jkt && dpop?.jkt) record.dpop_jkt = dpop.jkt;
111
- if (refreshStore.revoked_families[record.family_id]) {
112
- delete refreshStore.tokens[refreshKey];
113
- await options.storage.put(OAUTH_REFRESH_STORE_KEY, refreshStore);
114
- return json({ error: "invalid_grant" }, 400);
115
- }
116
- if (String(body.client_id ?? "") !== record.client_id) {
117
- return json({ error: "invalid_grant", error_description: "client mismatch" }, 400);
118
- }
119
- if (String(body.resource ?? record.resource) !== record.resource || record.resource !== `${base}/mcp`) {
120
- return json({ error: "invalid_target", error_description: "resource mismatch" }, 400);
121
- }
122
- if (body.scope !== undefined && normalizeOAuthScope(body.scope, options.serverName) !== record.scope) {
123
- return json({ error: "invalid_scope" }, 400);
124
- }
125
- if (!options.tokenVersion) {
126
- throw new HttpError(503, "server_error", "OAuth token version is not configured");
127
- }
128
- const account = oauthStore.accounts[record.account_id];
129
- const client = oauthStore.clients[record.client_id];
130
- if (
131
- !record.version
132
- || !(await safeEqual(record.version, options.tokenVersion))
133
- || !account
134
- || !account.active
135
- || account.version !== record.account_version
136
- || account.role !== record.role
137
- || !client
138
- || client.trusted_account_id !== account.account_id
139
- || client.trusted_account_version !== account.version
140
- || client.trusted_role !== account.role
141
- ) {
142
- delete refreshStore.tokens[refreshKey];
143
- await options.storage.put(OAUTH_REFRESH_STORE_KEY, refreshStore);
144
- return json({ error: "invalid_grant" }, 400);
145
- }
146
-
147
- if (dpop && !(await consumeDpopProof(options.storage, dpop))) {
148
- return json({ error: "invalid_dpop_proof" }, 400);
149
- }
150
- let issued: IssuedTokenPair;
151
- try {
152
- issued = await issueTokenPair(oauthStore, refreshStore, record, options.tokenVersion, dpop?.jkt);
153
- } catch (error) {
154
- if (error instanceof HttpError && error.code === "invalid_grant") {
155
- delete refreshStore.tokens[refreshKey];
156
- await options.storage.put(OAUTH_REFRESH_STORE_KEY, refreshStore);
157
- return json({ error: "invalid_grant" }, 400);
158
- }
159
- throw error;
160
- }
161
- delete refreshStore.tokens[refreshKey];
162
- recordConsumedRefreshToken(
163
- oauthStore,
164
- refreshStore,
165
- refreshKey,
166
- record.family_id,
167
- record.family_expires_at,
168
- Math.floor(Date.now() / 1000),
169
- );
170
- client.last_used_at = Math.floor(Date.now() / 1000);
171
- await saveOAuthStores(oauthStore, refreshStore, options.storage);
172
- return tokenResponse(issued, record.scope, dpop?.jkt || record.dpop_jkt);
173
- });
174
- }
175
-
176
- async function issueTokenPair(
177
- oauthStore: OAuthStore,
178
- refreshStore: OAuthRefreshStore,
179
- source: OAuthCode | OAuthRefreshToken,
180
- tokenVersion: string,
181
- dpopJkt?: string,
182
- ): Promise<IssuedTokenPair> {
183
- if (!tokenVersion) throw new HttpError(503, "server_error", "OAuth token version is not configured");
184
- const now = Math.floor(Date.now() / 1000);
185
- const familyId = "family_id" in source && source.family_id ? source.family_id : randomToken("mcp_family");
186
- const familyExpiresAt = "family_expires_at" in source && source.family_expires_at
187
- ? source.family_expires_at
188
- : now + REFRESH_TOKEN_FAMILY_TTL_SECONDS;
189
- if (familyExpiresAt <= now) throw new HttpError(400, "invalid_grant", "refresh-token family expired");
190
- const accessToken = randomToken("mcp_at");
191
- const refreshToken = randomToken("mcp_rt");
192
- const common = {
193
- client_id: source.client_id,
194
- account_id: source.account_id,
195
- account_version: source.account_version,
196
- role: source.role,
197
- scope: source.scope,
198
- resource: source.resource,
199
- version: tokenVersion,
200
- family_id: familyId,
201
- ...(dpopJkt ? { dpop_jkt: dpopJkt } : {}),
202
- };
203
- oauthStore.tokens[`sha256:${await sha256Hex(accessToken)}`] = {
204
- ...common,
205
- expires_at: now + ACCESS_TOKEN_TTL_SECONDS,
206
- };
207
- refreshStore.tokens[`sha256:${await sha256Hex(refreshToken)}`] = {
208
- ...common,
209
- family_id: familyId,
210
- family_expires_at: familyExpiresAt,
211
- issued_at: now,
212
- expires_at: Math.min(now + REFRESH_TOKEN_IDLE_TTL_SECONDS, familyExpiresAt),
213
- };
214
- pruneClientRecordByExpiry(oauthStore.tokens, source.client_id, MAX_ACCESS_TOKENS_PER_CLIENT);
215
- pruneRecordByExpiry(oauthStore.tokens, MAX_ACCESS_TOKENS);
216
- pruneClientRecordByExpiry(refreshStore.tokens, source.client_id, MAX_REFRESH_TOKENS_PER_CLIENT);
217
- pruneRecordByExpiry(refreshStore.tokens, MAX_REFRESH_TOKENS);
218
- return { accessToken, refreshToken };
219
- }
220
-
221
- function tokenResponse(issued: IssuedTokenPair, scope: string, dpopJkt?: string): Response {
222
- return json({
223
- access_token: issued.accessToken,
224
- refresh_token: issued.refreshToken,
225
- token_type: dpopJkt ? "DPoP" : "Bearer",
226
- expires_in: ACCESS_TOKEN_TTL_SECONDS,
227
- scope,
228
- });
229
- }
230
-
231
- async function saveOAuthStores(
232
- oauthStore: OAuthStore,
233
- refreshStore: OAuthRefreshStore,
234
- storage: DurableObjectStorage,
235
- ): Promise<void> {
236
- await storage.put({ oauth: oauthStore, [OAUTH_REFRESH_STORE_KEY]: refreshStore });
237
- }
@@ -10,6 +10,8 @@ export class WorkerObservability {
10
10
  private readonly calls = { started: 0, completed: 0, failed: 0, cancelled: 0, timed_out: 0, unmatched_results: 0 };
11
11
  private readonly sockets = { candidates: 0, authenticated: 0, ready: 0, disconnected: 0, protocol_errors: 0 };
12
12
  private readonly streamTransport = { subscribers_opened: 0, subscribers_replaced: 0, terminal_pushes: 0, terminal_recipients: 0, protocol_errors: 0 };
13
+ private readonly oauthRefresh = { rotated: 0, retry_issued: 0, retry_exhausted: 0, family_revoked: 0, rejected: 0 };
14
+ private readonly durableBudget = { stream_rows_written_estimate: 0, alarm_sets: 0, alarm_deletes: 0, alarm_noops: 0 };
13
15
  private readonly errors = new Map<string, number>();
14
16
  private readonly tools = new Map<string, { started: number; completed: number; failed: number; active: number }>();
15
17
 
@@ -43,6 +45,7 @@ export class WorkerObservability {
43
45
  }
44
46
 
45
47
  unmatchedResult(): void { this.calls.unmatched_results += 1; }
48
+ recordError(code: string): void { this.incrementError(code); }
46
49
 
47
50
  socketCandidate(): void { this.sockets.candidates += 1; }
48
51
  socketAuthenticated(): void { this.sockets.authenticated += 1; }
@@ -68,6 +71,20 @@ export class WorkerObservability {
68
71
  this.incrementError("stream_subscriber_protocol_error");
69
72
  }
70
73
 
74
+ oauthRefreshEvent(event: "rotated" | "retry_issued" | "retry_exhausted" | "family_revoked" | "rejected"): void {
75
+ this.oauthRefresh[event] += 1;
76
+ }
77
+
78
+ streamStorageRowsWritten(rows: number): void {
79
+ this.durableBudget.stream_rows_written_estimate += Math.max(0, Math.floor(rows));
80
+ }
81
+
82
+ runtimeAlarmMutation(action: "set" | "delete" | "noop"): void {
83
+ if (action === "set") this.durableBudget.alarm_sets += 1;
84
+ else if (action === "delete") this.durableBudget.alarm_deletes += 1;
85
+ else this.durableBudget.alarm_noops += 1;
86
+ }
87
+
71
88
  snapshot(): Record<string, unknown> {
72
89
  return {
73
90
  uptime_ms: Math.max(0, performance.now() - this.startedAt),
@@ -75,6 +92,8 @@ export class WorkerObservability {
75
92
  calls: { ...this.calls },
76
93
  sockets: { ...this.sockets },
77
94
  stream_transport: { ...this.streamTransport },
95
+ oauth_refresh: { ...this.oauthRefresh },
96
+ durable_budget: { ...this.durableBudget },
78
97
  errors: Object.fromEntries([...this.errors.entries()].sort(([left], [right]) => left.localeCompare(right))),
79
98
  tools: Object.fromEntries([...this.tools.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([name, metric]) => [name, { ...metric }])),
80
99
  };
@@ -0,0 +1,15 @@
1
+ export class PendingAdmissionGate {
2
+ private tail: Promise<void> = Promise.resolve();
3
+
4
+ async run<T>(operation: () => T | Promise<T>): Promise<T> {
5
+ let release: () => void = () => {};
6
+ const predecessor = this.tail;
7
+ this.tail = new Promise<void>((resolve) => { release = resolve; });
8
+ await predecessor;
9
+ try {
10
+ return await operation();
11
+ } finally {
12
+ release();
13
+ }
14
+ }
15
+ }
@@ -2,9 +2,10 @@ export type PendingCallOutcome =
2
2
  | { ok: true; value: unknown }
3
3
  | { ok: false; error: Error };
4
4
 
5
- export type PendingCallSettlement =
6
- | { kind: "promise"; resolve: (value: unknown) => void; reject: (error: Error) => void }
7
- | { kind: "event"; settle: (outcome: PendingCallOutcome) => void | Promise<void> };
5
+ export type PendingCallSettlement = {
6
+ resolve: (value: unknown) => void;
7
+ reject: (error: Error) => void;
8
+ };
8
9
 
9
10
  export interface PendingCallRecord {
10
11
  id: string;
@@ -36,7 +37,3 @@ export interface RegisterPendingCall {
36
37
  signal?: AbortSignal;
37
38
  onAbort?: (record: PendingCallRecord) => Error;
38
39
  }
39
-
40
- export interface RegisterEventPendingCall extends RegisterPendingCall {
41
- settle: (outcome: PendingCallOutcome) => void | Promise<void>;
42
- }
@@ -1,4 +1,4 @@
1
- import type { PendingCallOutcome, PendingCallRecord, PendingCallSettlement, RegisterEventPendingCall, RegisterPendingCall } from "./pending-call-contract.ts";
1
+ import type { PendingCallOutcome, PendingCallRecord, PendingCallSettlement, RegisterPendingCall } from "./pending-call-contract.ts";
2
2
  import { PendingCallDeadlines, type PendingCallDeadlineOptions } from "./pending-call-deadlines.ts";
3
3
 
4
4
  export class PendingCallRegistrationError extends Error {
@@ -41,15 +41,10 @@ export class PendingCallRegistry {
41
41
  let resolveResult!: (value: unknown) => void;
42
42
  let rejectResult!: (error: Error) => void;
43
43
  const result = new Promise<unknown>((resolve, reject) => { resolveResult = resolve; rejectResult = reject; });
44
- this.add(input, { kind: "promise", resolve: resolveResult, reject: rejectResult });
44
+ this.add(input, { resolve: resolveResult, reject: rejectResult });
45
45
  return result;
46
46
  }
47
47
 
48
- registerEvent(input: RegisterEventPendingCall): void {
49
- this.assertCanRegister(input);
50
- this.add(input, { kind: "event", settle: input.settle });
51
- }
52
-
53
48
  resolve(id: string, socket: WebSocket, value: unknown): Promise<boolean> {
54
49
  const record = this.byId.get(id);
55
50
  return !record || record.socket !== socket ? Promise.resolve(false) : this.finish(id, { ok: true, value });
@@ -101,7 +96,7 @@ export class PendingCallRegistry {
101
96
 
102
97
  snapshot(): { active: number; detached: number; request_keys: number; maximum: number; oldest_ms: number; by_tool: Record<string, number> } {
103
98
  const now = this.deadlines.now();
104
- const byTool: Record<string, number> = {};
99
+ const byTool = Object.create(null) as Record<string, number>;
105
100
  let detached = 0;
106
101
  let oldestMs = 0;
107
102
  for (const record of this.byId.values()) {
@@ -161,8 +156,7 @@ export class PendingCallRegistry {
161
156
  private async finish(id: string, outcome: PendingCallOutcome): Promise<boolean> {
162
157
  const record = this.take(id);
163
158
  if (!record) return false;
164
- if (record.settlement.kind === "promise") outcome.ok ? record.settlement.resolve(outcome.value) : record.settlement.reject(outcome.error);
165
- else await record.settlement.settle(outcome);
159
+ outcome.ok ? record.settlement.resolve(outcome.value) : record.settlement.reject(outcome.error);
166
160
  return true;
167
161
  }
168
162
 
@@ -0,0 +1,35 @@
1
+ export interface AlarmStorage {
2
+ getAlarm(): Promise<number | null>;
3
+ setAlarm(scheduledTime: number | Date): Promise<void>;
4
+ deleteAlarm(): Promise<void>;
5
+ }
6
+
7
+ export async function writeEarliestRuntimeAlarm(input: {
8
+ storage: AlarmStorage;
9
+ nextDeadline: number;
10
+ now: number;
11
+ onError: (error: unknown) => void;
12
+ onMutation?: (action: "set" | "delete" | "noop") => void;
13
+ }): Promise<void> {
14
+ try {
15
+ const current = await input.storage.getAlarm();
16
+ if (!Number.isFinite(input.nextDeadline)) {
17
+ if (current === null) {
18
+ input.onMutation?.("noop");
19
+ return;
20
+ }
21
+ await input.storage.deleteAlarm();
22
+ input.onMutation?.("delete");
23
+ return;
24
+ }
25
+ const target = Math.max(input.now, input.nextDeadline);
26
+ if (current !== null && current > input.now && current <= target) {
27
+ input.onMutation?.("noop");
28
+ return;
29
+ }
30
+ await input.storage.setAlarm(target);
31
+ input.onMutation?.("set");
32
+ } catch (error) {
33
+ input.onError(error);
34
+ }
35
+ }