machine-bridge-mcp 1.1.5 → 1.2.1

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 (63) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CODE_OF_CONDUCT.md +24 -0
  3. package/CONTRIBUTING.md +3 -1
  4. package/GOVERNANCE.md +50 -0
  5. package/README.md +26 -0
  6. package/SUPPORT.md +31 -0
  7. package/browser-extension/browser-operations.js +10 -4
  8. package/browser-extension/devtools-input.js +2 -2
  9. package/browser-extension/manifest.json +2 -2
  10. package/browser-extension/page-automation.js +1 -1
  11. package/docs/ARCHITECTURE.md +14 -17
  12. package/docs/AUDIT.md +28 -0
  13. package/docs/ENGINEERING.md +8 -2
  14. package/docs/PROJECT_STANDARDS.md +4 -2
  15. package/docs/TESTING.md +9 -3
  16. package/docs/UPGRADING.md +32 -0
  17. package/package.json +12 -4
  18. package/scripts/coverage-check.mjs +27 -10
  19. package/src/local/account-access.mjs +7 -5
  20. package/src/local/agent-context.mjs +7 -148
  21. package/src/local/agent-contract.mjs +206 -0
  22. package/src/local/browser-bridge.mjs +30 -281
  23. package/src/local/browser-extension-protocol.mjs +19 -4
  24. package/src/local/browser-operation-service.mjs +325 -0
  25. package/src/local/call-registry.mjs +44 -1
  26. package/src/local/capability-ranking.mjs +13 -0
  27. package/src/local/cli-local-admin.mjs +74 -38
  28. package/src/local/cli-options.mjs +18 -18
  29. package/src/local/cli-policy.mjs +1 -1
  30. package/src/local/cli-service.mjs +132 -0
  31. package/src/local/cli.mjs +27 -109
  32. package/src/local/daemon-process.mjs +1 -1
  33. package/src/local/full-access-test.mjs +1 -1
  34. package/src/local/job-runner.mjs +9 -3
  35. package/src/local/managed-job-plan.mjs +3 -3
  36. package/src/local/monotonic-deadline.mjs +7 -0
  37. package/src/local/numbers.mjs +8 -0
  38. package/src/local/policy.mjs +97 -16
  39. package/src/local/project-metadata.mjs +7 -1
  40. package/src/local/project-package.mjs +1 -1
  41. package/src/local/records.mjs +6 -0
  42. package/src/local/resource-operations.mjs +1 -1
  43. package/src/local/runtime-capabilities.mjs +66 -0
  44. package/src/local/runtime-diagnostics.mjs +91 -0
  45. package/src/local/runtime-reporting.mjs +113 -0
  46. package/src/local/runtime.mjs +54 -191
  47. package/src/local/service-convergence.mjs +1 -1
  48. package/src/local/service.mjs +1 -1
  49. package/src/local/state.mjs +1 -1
  50. package/src/local/windows-service.mjs +1 -1
  51. package/src/local/worker-deployment.mjs +15 -10
  52. package/src/local/worker-health.mjs +8 -4
  53. package/src/worker/access.ts +8 -7
  54. package/src/worker/account-admin.ts +2 -2
  55. package/src/worker/authority.ts +3 -3
  56. package/src/worker/http.ts +2 -2
  57. package/src/worker/index.ts +29 -337
  58. package/src/worker/oauth-controller.ts +343 -0
  59. package/src/worker/oauth-state.ts +1 -1
  60. package/src/worker/oauth-tokens.ts +2 -2
  61. package/src/worker/tool-catalog.ts +1 -1
  62. package/tsconfig.json +2 -1
  63. package/tsconfig.local.json +27 -0
@@ -1,49 +1,33 @@
1
1
  import { DurableObject } from "cloudflare:workers";
2
- import serverMetadata from "../shared/server-metadata.json";
3
- import { PendingCallRegistrationError, PendingCallRegistry } from "./pending-calls";
4
- import { mcpClientRequestKey, resolveMcpSession } from "./mcp-session";
5
- import { daemonToolTimeoutMs } from "./tool-timeout";
6
- import { WorkerObservability } from "./observability";
7
- import { daemonToolError, publicWorkerToolError, WorkerToolError } from "./errors";
8
- import { sanitizeDaemonPolicy, sanitizeDaemonTools, type DaemonPolicy } from "./policy";
9
- import { accountRoleAllowsTool, accountRoleToolNames, type AccountRole } from "./access";
10
- import { accountAuthoritySnapshot, decorateProjectOverview, describeDaemonCeiling } from "./authority";
11
- import { accountAdminAuthorized, handleAccountAdminOperation } from "./account-admin";
12
- import { exchangeOAuthToken } from "./oauth-tokens";
13
- import { serverInfoTool, workspaceTools } from "./tool-catalog";
2
+ import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
3
+ import { PendingCallRegistrationError, PendingCallRegistry } from "./pending-calls.ts";
4
+ import { mcpClientRequestKey, resolveMcpSession } from "./mcp-session.ts";
5
+ import { daemonToolTimeoutMs } from "./tool-timeout.ts";
6
+ import { WorkerObservability } from "./observability.ts";
7
+ import { daemonToolError, publicWorkerToolError, WorkerToolError } from "./errors.ts";
8
+ import { sanitizeDaemonPolicy, sanitizeDaemonTools, type DaemonPolicy } from "./policy.ts";
9
+ import { accountRoleAllowsTool, accountRoleToolNames, type AccountRole } from "./access.ts";
10
+ import { OAuthController, type AuthorizedToken, type OAuthControllerEnv } from "./oauth-controller.ts";
11
+ import { accountAuthoritySnapshot, decorateProjectOverview, describeDaemonCeiling } from "./authority.ts";
12
+ import { serverInfoTool, workspaceTools } from "./tool-catalog.ts";
13
+ import { OFFLINE_ACCESS_SCOPE, randomToken, safeEqual } from "./oauth-state.ts";
14
14
  import {
15
- AUTH_BLOCK_SECONDS, OFFLINE_ACCESS_SCOPE, accountByName, authorizationIdentity, emptyOAuthStore,
16
- isCurrentOAuthStore, pruneAuthFailures, pruneClientRecordByExpiry, pruneRecordByExpiry, randomToken,
17
- recordAuthorizationFailure, safeEqual, sha256Hex, validateAuthorizationRequest, verifyAccountPassword,
18
- type OAuthClient, type OAuthStore, type ValidatedAuthorization,
19
- } from "./oauth-state";
20
- import {
21
- HttpError, applyCors, authorizationRedirectLocation, baseUrl, bearerToken, corsPreflight, escapeHtml,
22
- html, json, methodNotAllowed, normalizeDisplayText, normalizeRedirectUri, oauthRedirect, sanitizeMetadataText,
23
- parseJsonRequest, parseRequestBody, searchParamsEntries, searchParamsObject, workerErrorClass,
24
- } from "./http";
15
+ HttpError, applyCors, baseUrl, bearerToken, corsPreflight, json, methodNotAllowed, sanitizeMetadataText,
16
+ parseJsonRequest, workerErrorClass,
17
+ } from "./http.ts";
25
18
 
26
19
  const SERVER_NAME = String(serverMetadata.name);
27
- const SERVER_VERSION = "1.1.5";
20
+ const SERVER_VERSION = "1.2.1";
28
21
  const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
29
22
  const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
30
23
  const JSONRPC_VERSION = "2.0";
31
24
  const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
32
25
  const MAX_BODY_BYTES = 16 * 1024 * 1024;
33
- const OAUTH_BODY_LIMIT_BYTES = 64 * 1024;
34
26
  const MAX_PENDING_CALLS = 32;
35
27
  const MAX_DAEMON_MESSAGE_BYTES = 8 * 1024 * 1024;
36
28
  const DAEMON_HELLO_TIMEOUT_MS = 10_000;
37
- const OAUTH_UNUSED_CLIENT_TTL_SECONDS = 60 * 60;
38
- const MAX_OAUTH_CLIENTS = 50;
39
- const MAX_OAUTH_CLIENTS_PER_IDENTITY = 5;
40
- const OAUTH_CLIENT_IDLE_TTL_SECONDS = 60 * 60 * 24 * 90;
41
- const MAX_CODES_PER_CLIENT = 10;
42
- const MAX_OAUTH_CODES = 200;
43
- const MAX_AUTH_FAILURE_IDENTITIES = 200;
44
- const AUTHORIZATION_FIELDS = new Set(["response_type", "client_id", "redirect_uri", "code_challenge", "code_challenge_method", "scope", "resource", "state"]);
45
-
46
- interface BridgeEnv {
29
+
30
+ interface BridgeEnv extends OAuthControllerEnv {
47
31
  BRIDGE: DurableObjectNamespace<BridgeRoom>;
48
32
  ACCOUNT_ADMIN_SECRET: string;
49
33
  DAEMON_SHARED_SECRET: string;
@@ -68,23 +52,16 @@ interface DaemonAttachment {
68
52
  tools?: string[];
69
53
  }
70
54
 
71
- interface AuthorizedToken {
72
- tokenKey: string;
73
- clientId: string;
74
- accountId: string;
75
- accountVersion: number;
76
- role: AccountRole;
77
- }
78
-
79
55
  const MCP_INSTRUCTIONS = serverMetadata.instructions.map((value) => String(value)).join("\n");
80
56
 
81
57
  export class BridgeRoom extends DurableObject<BridgeEnv> {
82
58
  private readonly pending = new PendingCallRegistry(MAX_PENDING_CALLS);
83
59
  private readonly observability = new WorkerObservability();
84
- private oauthQueue: Promise<void> = Promise.resolve();
60
+ private readonly oauth: OAuthController;
85
61
 
86
62
  constructor(ctx: DurableObjectState, env: BridgeEnv) {
87
63
  super(ctx, env);
64
+ this.oauth = new OAuthController(ctx, env, SERVER_NAME);
88
65
  }
89
66
 
90
67
  async fetch(request: Request): Promise<Response> {
@@ -125,20 +102,20 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
125
102
  if (request.method !== "GET") return methodNotAllowed("GET");
126
103
  return json(this.protectedResourceMetadata(base));
127
104
  }
128
- if (url.pathname === "/admin/accounts") return await this.handleAccountAdmin(request, "accounts");
129
- if (url.pathname === "/admin/accounts/rotate-password") return await this.handleAccountAdmin(request, "rotate-password");
130
- if (url.pathname === "/oauth/register") {
105
+ if (url.pathname === "/admin/accounts") return await this.oauth.handleAccountAdmin(request, "accounts");
106
+ if (url.pathname === "/admin/accounts/rotate-password") return await this.oauth.handleAccountAdmin(request, "rotate-password");
107
+ if (url.pathname === "/oauth/register") {
131
108
  if (request.method !== "POST") return methodNotAllowed("POST");
132
- return await this.registerClient(request);
109
+ return await this.oauth.registerClient(request);
133
110
  }
134
111
  if (url.pathname === "/oauth/authorize") {
135
- if (request.method === "GET") return await this.authorizeGet(request, base);
136
- if (request.method === "POST") return await this.authorizeSubmit(request, base);
112
+ if (request.method === "GET") return await this.oauth.authorizeGet(request, base);
113
+ if (request.method === "POST") return await this.oauth.authorizeSubmit(request, base);
137
114
  return methodNotAllowed("GET, POST");
138
115
  }
139
116
  if (url.pathname === "/oauth/token") {
140
117
  if (request.method !== "POST") return methodNotAllowed("POST");
141
- return await this.exchangeToken(request, base);
118
+ return await this.oauth.exchangeToken(request, base);
142
119
  }
143
120
  if (url.pathname === "/daemon/ws") {
144
121
  if (request.method !== "GET") return methodNotAllowed("GET");
@@ -270,7 +247,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
270
247
  });
271
248
  }
272
249
 
273
- const authorized = await this.verifyAccessToken(bearerToken(request), base);
250
+ const authorized = await this.oauth.verifyAccessToken(bearerToken(request), base);
274
251
  if (!authorized) {
275
252
  try { await request.arrayBuffer(); } catch { /* The rejected body may already be unavailable; the 401 remains authoritative. */ }
276
253
  return new Response("OAuth bearer token required", {
@@ -290,7 +267,7 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
290
267
  const protocolError = validateProtocolVersionHeader(request, body);
291
268
  if (protocolError) return json(protocolError, 400);
292
269
 
293
- const session = await resolveMcpSession(request, body.method, this.identityKey(), authorized.tokenKey);
270
+ const session = await resolveMcpSession(request, body.method, this.oauth.identityKey(), authorized.tokenKey);
294
271
  if (session.kind === "invalid") return json(rpcError(body.id, -32001, "MCP session not found"), 404);
295
272
  const response = await this.dispatchJsonRpc(body, base, authorized, session.kind === "active" ? session.sessionId : "");
296
273
  if (response === null) return new Response(null, { status: 202 });
@@ -606,287 +583,6 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
606
583
  };
607
584
  }
608
585
 
609
- private async oauthStore(): Promise<OAuthStore> {
610
- const raw = await this.ctx.storage.get<unknown>("oauth");
611
- if (raw !== undefined && !isCurrentOAuthStore(raw)) {
612
- throw new HttpError(503, "oauth_state_schema_mismatch", "OAuth state requires the one-time multi-account upgrade");
613
- }
614
- const store = isCurrentOAuthStore(raw) ? raw : emptyOAuthStore();
615
- let changed = false;
616
- const now = Math.floor(Date.now() / 1000);
617
-
618
- for (const [code, value] of Object.entries(store.codes)) {
619
- const account = store.accounts[value.account_id];
620
- if (value.expires_at <= now || !account || !account.active || account.version !== value.account_version || account.role !== value.role) {
621
- delete store.codes[code];
622
- changed = true;
623
- }
624
- }
625
- for (const [token, value] of Object.entries(store.tokens)) {
626
- const account = store.accounts[value.account_id];
627
- if (value.expires_at <= now || !account || !account.active || account.version !== value.account_version || account.role !== value.role) {
628
- delete store.tokens[token];
629
- changed = true;
630
- }
631
- }
632
- for (const [identity, value] of Object.entries(store.auth_failures)) {
633
- if (!identity.startsWith("hmac-sha256:") || value.last_attempt + AUTH_BLOCK_SECONDS <= now) {
634
- delete store.auth_failures[identity];
635
- changed = true;
636
- }
637
- }
638
- const activeClientIds = new Set([
639
- ...Object.values(store.codes).map((value) => value.client_id),
640
- ...Object.values(store.tokens).map((value) => value.client_id),
641
- ]);
642
- for (const [clientId, client] of Object.entries(store.clients)) {
643
- if (client.registration_identity && !client.registration_identity.startsWith("hmac-sha256:")) {
644
- delete client.registration_identity;
645
- changed = true;
646
- }
647
- const ttl = client.has_been_authorized === false ? OAUTH_UNUSED_CLIENT_TTL_SECONDS : OAUTH_CLIENT_IDLE_TTL_SECONDS;
648
- if (!activeClientIds.has(clientId) && client.last_used_at + ttl <= now) {
649
- delete store.clients[clientId];
650
- changed = true;
651
- }
652
- }
653
- if (changed) await this.ctx.storage.put("oauth", store);
654
- return store;
655
- }
656
-
657
- private async handleAccountAdmin(request: Request, operation: "accounts" | "rotate-password"): Promise<Response> {
658
- if (!(await accountAdminAuthorized(request, this.env.ACCOUNT_ADMIN_SECRET ?? ""))) return json({ error: "unauthorized" }, 401);
659
- return this.withOAuthLock(async () => {
660
- const store = await this.oauthStore();
661
- return handleAccountAdminOperation({
662
- request, operation, store, now: Math.floor(Date.now() / 1000),
663
- save: () => this.ctx.storage.put("oauth", store),
664
- });
665
- });
666
- }
667
-
668
- private async registerClient(request: Request): Promise<Response> {
669
- const body = await parseRequestBody(request, OAUTH_BODY_LIMIT_BYTES);
670
- const redirectUris = body.redirect_uris;
671
- if (!Array.isArray(redirectUris) || redirectUris.length === 0) {
672
- return json({ error: "invalid_client_metadata", error_description: "redirect_uris must be a non-empty array" }, 400);
673
- }
674
- if (redirectUris.length > 5) {
675
- return json({ error: "invalid_client_metadata", error_description: "redirect_uris must contain at most 5 entries" }, 400);
676
- }
677
- const suppliedRedirectUris = redirectUris.map((item) => String(item));
678
- if (suppliedRedirectUris.some((item) => item.length > 1024)) {
679
- return json({ error: "invalid_client_metadata", error_description: "redirect_uri is too long" }, 400);
680
- }
681
- const canonicalRedirectUris = suppliedRedirectUris.map(normalizeRedirectUri);
682
- if (canonicalRedirectUris.some((item) => item === null)) {
683
- return json({ error: "invalid_client_metadata", error_description: "redirect_uris must be canonicalizable https or local http URLs without credentials or fragments" }, 400);
684
- }
685
- const normalized = [...new Set(canonicalRedirectUris as string[])];
686
-
687
- return this.withOAuthLock(async () => {
688
- const store = await this.oauthStore();
689
- const registrationIdentity = await authorizationIdentity(request, this.identityKey());
690
- const pendingIdentityClientCount = Object.values(store.clients).filter((client) => (
691
- client.registration_identity === registrationIdentity && client.has_been_authorized === false
692
- )).length;
693
- if (pendingIdentityClientCount >= MAX_OAUTH_CLIENTS_PER_IDENTITY) {
694
- return json({ error: "too_many_requests", error_description: "pending client registration limit reached for this source" }, 429);
695
- }
696
- if (Object.keys(store.clients).length >= MAX_OAUTH_CLIENTS) {
697
- return json({ error: "temporarily_unavailable", error_description: "client registry is full; remove stale state or retry after inactive clients expire" }, 503);
698
- }
699
- const now = Math.floor(Date.now() / 1000);
700
- const client: OAuthClient = {
701
- client_id: randomToken("mcp_client"),
702
- client_name: normalizeDisplayText(stringOrUndefined(body.client_name) ?? "MCP Client", 128),
703
- redirect_uris: normalized,
704
- created_at: now,
705
- last_used_at: now,
706
- has_been_authorized: false,
707
- registration_identity: registrationIdentity,
708
- };
709
- store.clients[client.client_id] = client;
710
- await this.ctx.storage.put("oauth", store);
711
- return json({
712
- client_id: client.client_id,
713
- client_name: client.client_name,
714
- redirect_uris: client.redirect_uris,
715
- grant_types: ["authorization_code", "refresh_token"],
716
- response_types: ["code"],
717
- token_endpoint_auth_method: "none",
718
- client_id_issued_at: client.created_at,
719
- });
720
- });
721
- }
722
-
723
- private async authorizeGet(request: Request, base: string): Promise<Response> {
724
- const body = searchParamsObject(new URL(request.url).searchParams);
725
- return this.withOAuthLock(async () => {
726
- const store = await this.oauthStore();
727
- const validation = validateAuthorizationRequest(body, base, SERVER_NAME, store);
728
- if ("error" in validation) {
729
- return this.authorizePage(request, base, validation.error, body, validation.status, undefined, false);
730
- }
731
- return this.authorizePage(request, base, "", body, 200, validation.value, true);
732
- });
733
- }
734
-
735
- private authorizePage(
736
- request: Request,
737
- base: string,
738
- error = "",
739
- submitted?: Record<string, unknown>,
740
- status = 200,
741
- authorization?: ValidatedAuthorization,
742
- allowSubmit = true,
743
- ): Response {
744
- const url = new URL(request.url);
745
- const sourceEntries = submitted ? Object.entries(submitted) : searchParamsEntries(url.searchParams);
746
- const hidden = sourceEntries
747
- .filter(([key]) => AUTHORIZATION_FIELDS.has(key))
748
- .map(([key, value]) => `<input type="hidden" name="${escapeHtml(key)}" value="${escapeHtml(String(value))}">`)
749
- .join("\n");
750
- const resource = normalizeDisplayText(
751
- authorization?.requestedResource ?? String(submitted?.resource ?? url.searchParams.get("resource") ?? `${base}/mcp`),
752
- 1024,
753
- `${base}/mcp`,
754
- );
755
- const clientBlock = authorization
756
- ? `<p><strong>Client:</strong> ${escapeHtml(authorization.client.client_name)}</p>
757
- <p><strong>Redirect URI:</strong> <code>${escapeHtml(authorization.redirectUri)}</code></p>`
758
- : "";
759
- const errorBlock = error ? `<p role="alert" aria-live="assertive" style="color:#b91c1c; font-weight:600">${escapeHtml(error)}</p>` : "";
760
- const accountName = normalizeDisplayText(String(submitted?.account_name ?? ""), 64, "");
761
- const form = allowSubmit
762
- ? `<form method="post" action="/oauth/authorize">
763
- ${hidden}
764
- <label>Account name<br><input name="account_name" value="${escapeHtml(accountName)}" autocomplete="username" autofocus required style="width: 100%; box-sizing: border-box; padding: 8px;"></label>
765
- <p><label>Account password<br><input name="account_password" type="password" autocomplete="current-password" required style="width: 100%; box-sizing: border-box; padding: 8px;"></label></p>
766
- <p><button type="submit">Authorize</button></p>
767
- </form>`
768
- : "<p>Authorization cannot continue. Return to the MCP client and start the connection again.</p>";
769
- const redirectOrigin = authorization ? new URL(authorization.redirectUri).origin : "";
770
- return html(`<!doctype html>
771
- <html>
772
- <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Authorize ${SERVER_NAME}</title></head>
773
- <body style="font-family: system-ui, sans-serif; max-width: 640px; margin: 48px auto; line-height: 1.5; padding: 0 16px;">
774
- <h1>Authorize ${SERVER_NAME}</h1>
775
- <p>Only continue if you initiated this MCP connection and recognize the client and redirect URI below.</p>
776
- ${clientBlock}
777
- <p><strong>Resource:</strong> <code>${escapeHtml(resource)}</code></p>
778
- ${errorBlock}
779
- ${form}
780
- </body>
781
- </html>`, status, redirectOrigin);
782
- }
783
-
784
- private async authorizeSubmit(request: Request, base: string): Promise<Response> {
785
- const body = await parseRequestBody(request, OAUTH_BODY_LIMIT_BYTES);
786
- return this.withOAuthLock(async () => {
787
- const store = await this.oauthStore();
788
- const validation = validateAuthorizationRequest(body, base, SERVER_NAME, store);
789
- if ("error" in validation) {
790
- return this.authorizePage(request, base, validation.error, body, validation.status, undefined, false);
791
- }
792
- const { client, clientId, redirectUri, codeChallenge, requestedResource, scope, state } = validation.value;
793
- const now = Math.floor(Date.now() / 1000);
794
- const identity = await authorizationIdentity(request, this.identityKey());
795
- const failure = store.auth_failures[identity];
796
- if (failure?.blocked_until > now) {
797
- return this.authorizePage(request, base, "Too many failed attempts. Try again later.", body, 429, validation.value);
798
- }
799
-
800
- const account = accountByName(store, body.account_name);
801
- const credentialsValid = Boolean(account?.active && await verifyAccountPassword(account, body.account_password));
802
- if (!account || !credentialsValid) {
803
- recordAuthorizationFailure(store, identity, now);
804
- pruneAuthFailures(store, MAX_AUTH_FAILURE_IDENTITIES);
805
- await this.ctx.storage.put("oauth", store);
806
- const status = store.auth_failures[identity]?.blocked_until > now ? 429 : 401;
807
- return this.authorizePage(request, base, "Invalid account credentials.", body, status, validation.value);
808
- }
809
- delete store.auth_failures[identity];
810
- client.last_used_at = now;
811
- client.has_been_authorized = true;
812
-
813
- const code = randomToken("mcp_code");
814
- const redirectLocation = authorizationRedirectLocation(redirectUri, code, state);
815
- store.codes[code] = {
816
- client_id: clientId,
817
- account_id: account.account_id,
818
- account_version: account.version,
819
- role: account.role,
820
- redirect_uri: redirectUri,
821
- code_challenge: codeChallenge,
822
- scope,
823
- resource: requestedResource,
824
- expires_at: now + 300,
825
- };
826
- pruneClientRecordByExpiry(store.codes, clientId, MAX_CODES_PER_CLIENT);
827
- pruneRecordByExpiry(store.codes, MAX_OAUTH_CODES);
828
- await this.ctx.storage.put("oauth", store);
829
-
830
- return oauthRedirect(redirectLocation);
831
- });
832
- }
833
-
834
- private exchangeToken(request: Request, base: string): Promise<Response> {
835
- return exchangeOAuthToken(request, base, {
836
- storage: this.ctx.storage,
837
- tokenVersion: this.env.OAUTH_TOKEN_VERSION ?? "",
838
- serverName: SERVER_NAME,
839
- loadOAuthStore: () => this.oauthStore(),
840
- withLock: (callback) => this.withOAuthLock(callback),
841
- });
842
- }
843
-
844
- private async verifyAccessToken(token: string, base: string): Promise<AuthorizedToken | null> {
845
- return this.withOAuthLock(async () => {
846
- if (!token) return null;
847
- const store = await this.oauthStore();
848
- const key = `sha256:${await sha256Hex(token)}`;
849
- const record = store.tokens[key];
850
- if (!record) return null;
851
- if (record.expires_at <= Math.floor(Date.now() / 1000)) {
852
- delete store.tokens[key];
853
- await this.ctx.storage.put("oauth", store);
854
- return null;
855
- }
856
- const currentVersion = this.env.OAUTH_TOKEN_VERSION ?? "";
857
- if (!record.version || !currentVersion || !(await safeEqual(record.version, currentVersion))) return null;
858
- if (record.resource !== `${base}/mcp`) return null;
859
- const account = store.accounts[record.account_id];
860
- if (!account || !account.active || account.version !== record.account_version || account.role !== record.role) {
861
- delete store.tokens[key];
862
- await this.ctx.storage.put("oauth", store);
863
- return null;
864
- }
865
- return {
866
- tokenKey: key, clientId: record.client_id, accountId: account.account_id,
867
- accountVersion: account.version, role: account.role,
868
- };
869
- });
870
- }
871
-
872
- private async withOAuthLock<T>(callback: () => Promise<T>): Promise<T> {
873
- const previous = this.oauthQueue;
874
- let release = () => {};
875
- this.oauthQueue = new Promise<void>((resolve) => { release = resolve; });
876
- await previous;
877
- try {
878
- return await callback();
879
- } finally {
880
- release();
881
- }
882
- }
883
-
884
- private identityKey(): string {
885
- const key = this.env.OAUTH_TOKEN_VERSION || this.env.DAEMON_SHARED_SECRET || this.env.ACCOUNT_ADMIN_SECRET;
886
- if (!key) throw new HttpError(503, "server_not_configured", "OAuth identity key is not configured");
887
- return key;
888
- }
889
-
890
586
  private bodyLimitBytes(): number {
891
587
  const parsed = Number.parseInt(this.env.MBM_WORKER_MAX_BODY_BYTES ?? "", 10);
892
588
  if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_BODY_BYTES;
@@ -994,10 +690,6 @@ function requiredString(value: Record<string, unknown>, key: string): string {
994
690
  return field.trim();
995
691
  }
996
692
 
997
- function stringOrUndefined(value: unknown): string | undefined {
998
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
999
- }
1000
-
1001
693
  function sessionInstructionText(value: unknown): string {
1002
694
  const object = asObject(value);
1003
695
  const instructions = typeof object.instructions === "string" ? object.instructions : "";