machine-bridge-mcp 1.0.8 → 1.1.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.
@@ -0,0 +1,208 @@
1
+ import {
2
+ emptyOAuthRefreshStore, isCurrentOAuthRefreshStore, normalizeOAuthScope, pkceS256,
3
+ pruneClientRecordByExpiry, pruneRecordByExpiry, randomToken, safeEqual, sha256Hex,
4
+ type OAuthCode, type OAuthRefreshStore, type OAuthRefreshToken, type OAuthStore,
5
+ } from "./oauth-state";
6
+ import { HttpError, json, parseRequestBody } from "./http";
7
+
8
+ const ACCESS_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 30;
9
+ const REFRESH_TOKEN_TTL_SECONDS = 60 * 60 * 24 * 90;
10
+ const OAUTH_BODY_LIMIT_BYTES = 64 * 1024;
11
+ const MAX_ACCESS_TOKENS = 500;
12
+ const MAX_ACCESS_TOKENS_PER_CLIENT = 20;
13
+ const MAX_REFRESH_TOKENS = 500;
14
+ const MAX_REFRESH_TOKENS_PER_CLIENT = 20;
15
+ const OAUTH_REFRESH_STORE_KEY = "oauth-refresh";
16
+
17
+ type OAuthLock = <T>(callback: () => Promise<T>) => Promise<T>;
18
+
19
+ interface OAuthTokenExchangeOptions {
20
+ storage: DurableObjectStorage;
21
+ tokenVersion: string;
22
+ serverName: string;
23
+ loadOAuthStore: () => Promise<OAuthStore>;
24
+ withLock: OAuthLock;
25
+ }
26
+
27
+ interface IssuedTokenPair {
28
+ accessToken: string;
29
+ refreshToken: string;
30
+ }
31
+
32
+ export async function exchangeOAuthToken(
33
+ request: Request,
34
+ base: string,
35
+ options: OAuthTokenExchangeOptions,
36
+ ): Promise<Response> {
37
+ const body = await parseRequestBody(request, OAUTH_BODY_LIMIT_BYTES);
38
+ const grantType = String(body.grant_type ?? "");
39
+ if (grantType === "authorization_code") return exchangeAuthorizationCode(body, base, options);
40
+ if (grantType === "refresh_token") return exchangeRefreshToken(body, base, options);
41
+ return json({ error: "unsupported_grant_type" }, 400);
42
+ }
43
+
44
+ async function exchangeAuthorizationCode(
45
+ body: Record<string, unknown>,
46
+ base: string,
47
+ options: OAuthTokenExchangeOptions,
48
+ ): Promise<Response> {
49
+ const code = String(body.code ?? "");
50
+ const verifier = String(body.code_verifier ?? "");
51
+ if (!/^[A-Za-z0-9._~-]{43,128}$/.test(verifier)) {
52
+ return json({ error: "invalid_grant", error_description: "invalid code_verifier" }, 400);
53
+ }
54
+ return options.withLock(async () => {
55
+ const oauthStore = await options.loadOAuthStore();
56
+ const refreshStore = await loadRefreshStore(oauthStore, options);
57
+ const record = oauthStore.codes[code];
58
+ if (!record) return json({ error: "invalid_grant" }, 400);
59
+ if (String(body.client_id ?? "") !== record.client_id || String(body.redirect_uri ?? "") !== record.redirect_uri) {
60
+ return json({ error: "invalid_grant", error_description: "client or redirect mismatch" }, 400);
61
+ }
62
+ if (String(body.resource ?? record.resource) !== record.resource || record.resource !== `${base}/mcp`) {
63
+ return json({ error: "invalid_target", error_description: "resource mismatch" }, 400);
64
+ }
65
+ if (!(await safeEqual(await pkceS256(verifier), record.code_challenge))) {
66
+ return json({ error: "invalid_grant", error_description: "invalid code_verifier" }, 400);
67
+ }
68
+ const client = oauthStore.clients[record.client_id];
69
+ if (!client) return json({ error: "invalid_grant", error_description: "unknown client" }, 400);
70
+
71
+ const issued = await issueTokenPair(oauthStore, refreshStore, record, options.tokenVersion);
72
+ delete oauthStore.codes[code];
73
+ client.last_used_at = Math.floor(Date.now() / 1000);
74
+ await saveOAuthStores(oauthStore, refreshStore, options.storage);
75
+ return tokenResponse(issued, record.scope);
76
+ });
77
+ }
78
+
79
+ async function exchangeRefreshToken(
80
+ body: Record<string, unknown>,
81
+ base: string,
82
+ options: OAuthTokenExchangeOptions,
83
+ ): Promise<Response> {
84
+ const refreshToken = String(body.refresh_token ?? "");
85
+ if (!/^mcp_rt_[A-Za-z0-9_-]{43}$/.test(refreshToken)) return json({ error: "invalid_grant" }, 400);
86
+ return options.withLock(async () => {
87
+ const oauthStore = await options.loadOAuthStore();
88
+ const refreshStore = await loadRefreshStore(oauthStore, options);
89
+ const refreshKey = `sha256:${await sha256Hex(refreshToken)}`;
90
+ const record = refreshStore.tokens[refreshKey];
91
+ if (!record) return json({ error: "invalid_grant" }, 400);
92
+ if (String(body.client_id ?? "") !== record.client_id) {
93
+ return json({ error: "invalid_grant", error_description: "client mismatch" }, 400);
94
+ }
95
+ if (String(body.resource ?? record.resource) !== record.resource || record.resource !== `${base}/mcp`) {
96
+ return json({ error: "invalid_target", error_description: "resource mismatch" }, 400);
97
+ }
98
+ if (body.scope !== undefined && normalizeOAuthScope(body.scope, options.serverName) !== record.scope) {
99
+ return json({ error: "invalid_scope" }, 400);
100
+ }
101
+ if (!options.tokenVersion) {
102
+ throw new HttpError(503, "server_error", "OAuth token version is not configured");
103
+ }
104
+ const account = oauthStore.accounts[record.account_id];
105
+ const client = oauthStore.clients[record.client_id];
106
+ if (
107
+ !record.version
108
+ || !(await safeEqual(record.version, options.tokenVersion))
109
+ || !account
110
+ || !account.active
111
+ || account.version !== record.account_version
112
+ || account.role !== record.role
113
+ || !client
114
+ ) {
115
+ delete refreshStore.tokens[refreshKey];
116
+ await options.storage.put(OAUTH_REFRESH_STORE_KEY, refreshStore);
117
+ return json({ error: "invalid_grant" }, 400);
118
+ }
119
+
120
+ const issued = await issueTokenPair(oauthStore, refreshStore, record, options.tokenVersion);
121
+ delete refreshStore.tokens[refreshKey];
122
+ client.last_used_at = Math.floor(Date.now() / 1000);
123
+ await saveOAuthStores(oauthStore, refreshStore, options.storage);
124
+ return tokenResponse(issued, record.scope);
125
+ });
126
+ }
127
+
128
+ async function loadRefreshStore(
129
+ oauthStore: OAuthStore,
130
+ options: OAuthTokenExchangeOptions,
131
+ ): Promise<OAuthRefreshStore> {
132
+ const raw = await options.storage.get<unknown>(OAUTH_REFRESH_STORE_KEY);
133
+ if (raw !== undefined && !isCurrentOAuthRefreshStore(raw)) {
134
+ throw new HttpError(503, "oauth_refresh_state_schema_mismatch", "OAuth refresh-token state requires operator repair");
135
+ }
136
+ const store = isCurrentOAuthRefreshStore(raw) ? raw : emptyOAuthRefreshStore();
137
+ let changed = false;
138
+ const now = Math.floor(Date.now() / 1000);
139
+ for (const [token, value] of Object.entries(store.tokens)) {
140
+ const account = oauthStore.accounts[value.account_id];
141
+ const client = oauthStore.clients[value.client_id];
142
+ if (
143
+ value.expires_at <= now
144
+ || !account
145
+ || !account.active
146
+ || account.version !== value.account_version
147
+ || account.role !== value.role
148
+ || !client
149
+ ) {
150
+ delete store.tokens[token];
151
+ changed = true;
152
+ }
153
+ }
154
+ if (changed) await options.storage.put(OAUTH_REFRESH_STORE_KEY, store);
155
+ return store;
156
+ }
157
+
158
+ async function issueTokenPair(
159
+ oauthStore: OAuthStore,
160
+ refreshStore: OAuthRefreshStore,
161
+ source: OAuthCode | OAuthRefreshToken,
162
+ tokenVersion: string,
163
+ ): Promise<IssuedTokenPair> {
164
+ if (!tokenVersion) throw new HttpError(503, "server_error", "OAuth token version is not configured");
165
+ const now = Math.floor(Date.now() / 1000);
166
+ const accessToken = randomToken("mcp_at");
167
+ const refreshToken = randomToken("mcp_rt");
168
+ const common = {
169
+ client_id: source.client_id,
170
+ account_id: source.account_id,
171
+ account_version: source.account_version,
172
+ role: source.role,
173
+ scope: source.scope,
174
+ resource: source.resource,
175
+ version: tokenVersion,
176
+ };
177
+ oauthStore.tokens[`sha256:${await sha256Hex(accessToken)}`] = {
178
+ ...common,
179
+ expires_at: now + ACCESS_TOKEN_TTL_SECONDS,
180
+ };
181
+ refreshStore.tokens[`sha256:${await sha256Hex(refreshToken)}`] = {
182
+ ...common,
183
+ expires_at: now + REFRESH_TOKEN_TTL_SECONDS,
184
+ };
185
+ pruneClientRecordByExpiry(oauthStore.tokens, source.client_id, MAX_ACCESS_TOKENS_PER_CLIENT);
186
+ pruneRecordByExpiry(oauthStore.tokens, MAX_ACCESS_TOKENS);
187
+ pruneClientRecordByExpiry(refreshStore.tokens, source.client_id, MAX_REFRESH_TOKENS_PER_CLIENT);
188
+ pruneRecordByExpiry(refreshStore.tokens, MAX_REFRESH_TOKENS);
189
+ return { accessToken, refreshToken };
190
+ }
191
+
192
+ function tokenResponse(issued: IssuedTokenPair, scope: string): Response {
193
+ return json({
194
+ access_token: issued.accessToken,
195
+ refresh_token: issued.refreshToken,
196
+ token_type: "Bearer",
197
+ expires_in: ACCESS_TOKEN_TTL_SECONDS,
198
+ scope,
199
+ });
200
+ }
201
+
202
+ async function saveOAuthStores(
203
+ oauthStore: OAuthStore,
204
+ refreshStore: OAuthRefreshStore,
205
+ storage: DurableObjectStorage,
206
+ ): Promise<void> {
207
+ await storage.put({ oauth: oauthStore, [OAUTH_REFRESH_STORE_KEY]: refreshStore });
208
+ }