nanocodex-vite 0.6.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.
- package/README.md +48 -0
- package/chatgpt-egress.mjs +298 -0
- package/chatgpt-subscription.mjs +451 -0
- package/cloudflare-plugin.mjs +80 -0
- package/cloudflare.d.mts +21 -0
- package/cloudflare.mjs +32 -0
- package/codex-auth-file.mjs +158 -0
- package/index.d.mts +36 -0
- package/index.mjs +6 -0
- package/oauth-relay-server.mjs +125 -0
- package/oauth-relay.d.mts +104 -0
- package/oauth-relay.mjs +313 -0
- package/package.json +65 -0
- package/plugin.mjs +525 -0
- package/scripts/build-js-package.sh +147 -0
- package/scripts/wasm-memory-views.mjs +39 -0
- package/tools.d.mts +8 -0
- package/tools.mjs +38 -0
package/oauth-relay.mjs
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
const PROVIDERS = new Set(["github", "google", "gmail", "gdrive", "slack", "x"]);
|
|
2
|
+
const FLOWS = new Set(["connect", "managed"]);
|
|
3
|
+
const BASE64_URL = /^[A-Za-z0-9_-]+$/;
|
|
4
|
+
const INSTANCE_HOST = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.nanocodex\.localhost$/;
|
|
5
|
+
const SIGNING_CONTEXT = "nanocodex-local-oauth-relay-v1\0";
|
|
6
|
+
const HEALTH_CONTEXT = "nanocodex-local-oauth-relay-health-v1\0";
|
|
7
|
+
const STATE_TTL_SECONDS = 10 * 60;
|
|
8
|
+
const MAX_CLOCK_SKEW_SECONDS = 30;
|
|
9
|
+
const MAX_INNER_STATE_LENGTH = 512;
|
|
10
|
+
const MAX_ENVELOPE_LENGTH = 2_048;
|
|
11
|
+
const CONNECTION_ID = /^[A-Za-z0-9_-]{43}$/;
|
|
12
|
+
const CONNECTOR_CALLBACK = /^\/v1\/connectors\/(github|google|gmail|gdrive|slack|x)\/callback$/;
|
|
13
|
+
const MCP_CALLBACK = /^\/v1\/mcp-connections\/([A-Za-z0-9_-]{43})\/callback$/;
|
|
14
|
+
const SAFE_QUERY = ["code", "error", "error_description"];
|
|
15
|
+
|
|
16
|
+
export const LOCAL_OAUTH_RELAY_HOST = "127.0.0.1";
|
|
17
|
+
export const LOCAL_OAUTH_RELAY_PORT = 47_891;
|
|
18
|
+
export const LOCAL_OAUTH_RELAY_ORIGIN = `http://${LOCAL_OAUTH_RELAY_HOST}:${LOCAL_OAUTH_RELAY_PORT}`;
|
|
19
|
+
export const LOCAL_OAUTH_RELAY_HMAC_KEY = "nanocodex-local-oauth-relay-hmac-v1-only";
|
|
20
|
+
|
|
21
|
+
export function localOAuthRelayCallbackUrl(provider) {
|
|
22
|
+
if (!PROVIDERS.has(provider)) return undefined;
|
|
23
|
+
return `${LOCAL_OAUTH_RELAY_ORIGIN}/v1/connectors/${provider}/callback`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function localMcpOAuthRelayCallbackUrl(connectionId) {
|
|
27
|
+
if (typeof connectionId !== "string" || !CONNECTION_ID.test(connectionId)) return undefined;
|
|
28
|
+
return `${LOCAL_OAUTH_RELAY_ORIGIN}/v1/mcp-connections/${connectionId}/callback`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isLocalNanocodexOrigin(value) {
|
|
32
|
+
let url;
|
|
33
|
+
try { url = new URL(value); } catch { return false; }
|
|
34
|
+
if (url.origin !== value || url.username || url.password) return false;
|
|
35
|
+
const hostname = url.hostname.toLowerCase();
|
|
36
|
+
if (hostname !== "nanocodex.localhost" && !INSTANCE_HOST.test(hostname)) return false;
|
|
37
|
+
if (url.protocol === "https:" && !url.port) return true;
|
|
38
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") || !url.port) return false;
|
|
39
|
+
const port = Number(url.port);
|
|
40
|
+
return Number.isSafeInteger(port) && port >= 1_024 && port <= 65_535
|
|
41
|
+
&& port !== LOCAL_OAUTH_RELAY_PORT;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function localConnectorAuthorization(targetOrigin, connector, flow) {
|
|
45
|
+
if (!PROVIDERS.has(connector) || !isLocalNanocodexOrigin(targetOrigin)) return undefined;
|
|
46
|
+
const redirectUri = localOAuthRelayCallbackUrl(connector);
|
|
47
|
+
if (!redirectUri) return undefined;
|
|
48
|
+
return {
|
|
49
|
+
connector,
|
|
50
|
+
redirectUri,
|
|
51
|
+
targetOrigin: new URL(targetOrigin).origin,
|
|
52
|
+
flow,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function localMcpAuthorization(targetOrigin, connectionId, flow) {
|
|
57
|
+
if (typeof connectionId !== "string" || !CONNECTION_ID.test(connectionId)
|
|
58
|
+
|| !isLocalNanocodexOrigin(targetOrigin)) return undefined;
|
|
59
|
+
const redirectUri = localMcpOAuthRelayCallbackUrl(connectionId);
|
|
60
|
+
if (!redirectUri) return undefined;
|
|
61
|
+
return {
|
|
62
|
+
connectionId,
|
|
63
|
+
redirectUri,
|
|
64
|
+
targetOrigin: new URL(targetOrigin).origin,
|
|
65
|
+
flow,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function wrapLocalConnectorAuthorizationState(authorizationUrl, local, relayKey) {
|
|
70
|
+
const state = authorizationUrl.searchParams.get("state");
|
|
71
|
+
if (!state || state.length > MAX_INNER_STATE_LENGTH) {
|
|
72
|
+
throw new Error("invalid local connector authorization state");
|
|
73
|
+
}
|
|
74
|
+
authorizationUrl.searchParams.set("state", await signLocalOAuthRelayState({
|
|
75
|
+
provider: local.connector,
|
|
76
|
+
targetOrigin: local.targetOrigin,
|
|
77
|
+
flow: local.flow,
|
|
78
|
+
state,
|
|
79
|
+
}, relayKey));
|
|
80
|
+
return authorizationUrl;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function wrapLocalMcpAuthorizationState(authorizationUrl, local, relayKey) {
|
|
84
|
+
const state = authorizationUrl.searchParams.get("state");
|
|
85
|
+
if (!state || state.length > MAX_INNER_STATE_LENGTH) {
|
|
86
|
+
throw new Error("invalid local MCP authorization state");
|
|
87
|
+
}
|
|
88
|
+
authorizationUrl.searchParams.set("state", await signLocalOAuthRelayState({
|
|
89
|
+
connectionId: local.connectionId,
|
|
90
|
+
targetOrigin: local.targetOrigin,
|
|
91
|
+
flow: local.flow,
|
|
92
|
+
state,
|
|
93
|
+
}, relayKey));
|
|
94
|
+
return authorizationUrl;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function signLocalOAuthRelayState(
|
|
98
|
+
{ provider, connectionId, targetOrigin, flow, state },
|
|
99
|
+
secret,
|
|
100
|
+
{ now = Date.now(), nonce } = {},
|
|
101
|
+
) {
|
|
102
|
+
if (!validSecret(secret) || !validRelayTarget(provider, connectionId)
|
|
103
|
+
|| !isLocalNanocodexOrigin(targetOrigin) || !FLOWS.has(flow)
|
|
104
|
+
|| typeof state !== "string" || state.length < 1 || state.length > MAX_INNER_STATE_LENGTH) {
|
|
105
|
+
throw new Error("invalid local OAuth relay state");
|
|
106
|
+
}
|
|
107
|
+
const issuedAt = Math.floor(now / 1_000);
|
|
108
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt < 1) {
|
|
109
|
+
throw new Error("invalid local OAuth relay time");
|
|
110
|
+
}
|
|
111
|
+
const envelope = {
|
|
112
|
+
v: 1,
|
|
113
|
+
...(provider === undefined ? { c: connectionId } : { p: provider }),
|
|
114
|
+
o: targetOrigin,
|
|
115
|
+
f: flow,
|
|
116
|
+
s: state,
|
|
117
|
+
i: issuedAt,
|
|
118
|
+
e: issuedAt + STATE_TTL_SECONDS,
|
|
119
|
+
n: nonce ?? randomNonce(),
|
|
120
|
+
};
|
|
121
|
+
if (!validNonce(envelope.n)) throw new Error("invalid local OAuth relay nonce");
|
|
122
|
+
const payload = encodeBase64Url(new TextEncoder().encode(JSON.stringify(envelope)));
|
|
123
|
+
const signature = await sign(secret, payload);
|
|
124
|
+
return `${payload}.${encodeBase64Url(signature)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function verifyLocalOAuthRelayState(
|
|
128
|
+
value,
|
|
129
|
+
expectedProvider,
|
|
130
|
+
secret,
|
|
131
|
+
{ now = Date.now() } = {},
|
|
132
|
+
) {
|
|
133
|
+
if (!PROVIDERS.has(expectedProvider)) return undefined;
|
|
134
|
+
return verifyRelayState(value, { provider: expectedProvider }, secret, { now });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function verifyLocalMcpOAuthRelayState(
|
|
138
|
+
value,
|
|
139
|
+
expectedConnectionId,
|
|
140
|
+
secret,
|
|
141
|
+
{ now = Date.now() } = {},
|
|
142
|
+
) {
|
|
143
|
+
if (typeof expectedConnectionId !== "string" || !CONNECTION_ID.test(expectedConnectionId)) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
return verifyRelayState(value, { connectionId: expectedConnectionId }, secret, { now });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function verifyRelayState(value, expected, secret, { now = Date.now() } = {}) {
|
|
150
|
+
if (!validSecret(secret)
|
|
151
|
+
|| typeof value !== "string" || value.length > MAX_ENVELOPE_LENGTH) return undefined;
|
|
152
|
+
const parts = value.split(".");
|
|
153
|
+
if (parts.length !== 2) return undefined;
|
|
154
|
+
const [payload, encodedSignature] = parts;
|
|
155
|
+
if (!payload || !BASE64_URL.test(payload) || !encodedSignature || !BASE64_URL.test(encodedSignature)) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const signature = decodeBase64Url(encodedSignature);
|
|
159
|
+
if (!signature || signature.byteLength !== 32) return undefined;
|
|
160
|
+
const key = await hmacKey(secret, ["verify"]);
|
|
161
|
+
const valid = await crypto.subtle.verify(
|
|
162
|
+
"HMAC",
|
|
163
|
+
key,
|
|
164
|
+
signature,
|
|
165
|
+
new TextEncoder().encode(`${SIGNING_CONTEXT}${payload}`),
|
|
166
|
+
);
|
|
167
|
+
if (!valid) return undefined;
|
|
168
|
+
const decoded = decodeBase64Url(payload);
|
|
169
|
+
if (!decoded) return undefined;
|
|
170
|
+
let candidate;
|
|
171
|
+
try { candidate = JSON.parse(new TextDecoder().decode(decoded)); } catch { return undefined; }
|
|
172
|
+
const structurallyValid = expected.provider
|
|
173
|
+
? validEnvelope(candidate, expected.provider)
|
|
174
|
+
: validMcpEnvelope(candidate, expected.connectionId);
|
|
175
|
+
if (!structurallyValid) return undefined;
|
|
176
|
+
const current = Math.floor(now / 1_000);
|
|
177
|
+
if (!Number.isSafeInteger(current)
|
|
178
|
+
|| candidate.i > current + MAX_CLOCK_SKEW_SECONDS
|
|
179
|
+
|| current > candidate.e
|
|
180
|
+
|| candidate.e - candidate.i !== STATE_TTL_SECONDS) return undefined;
|
|
181
|
+
return candidate;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function localOAuthRelayCallbackRedirect(url, secret, options) {
|
|
185
|
+
if (!(url instanceof URL) || url.origin !== LOCAL_OAUTH_RELAY_ORIGIN || url.hash) return undefined;
|
|
186
|
+
const provider = url.pathname.match(CONNECTOR_CALLBACK)?.[1];
|
|
187
|
+
const connectionId = url.pathname.match(MCP_CALLBACK)?.[1];
|
|
188
|
+
if (!provider && !connectionId) return undefined;
|
|
189
|
+
const envelope = await verifyRelayState(url.searchParams.get("state"), {
|
|
190
|
+
...(provider ? { provider } : { connectionId }),
|
|
191
|
+
}, secret, options);
|
|
192
|
+
if (!envelope) return undefined;
|
|
193
|
+
const destination = new URL(
|
|
194
|
+
provider
|
|
195
|
+
? envelope.f === "connect"
|
|
196
|
+
? `/v1/connect/auth/connector-callback/${provider}`
|
|
197
|
+
: `/v1/connectors/${provider}/callback`
|
|
198
|
+
: envelope.f === "connect"
|
|
199
|
+
? `/v1/connect/auth/mcp-connection-callback/${connectionId}`
|
|
200
|
+
: `/v1/connectors/mcp-connections/${connectionId}/callback`,
|
|
201
|
+
envelope.o,
|
|
202
|
+
);
|
|
203
|
+
for (const name of SAFE_QUERY) {
|
|
204
|
+
const value = url.searchParams.get(name);
|
|
205
|
+
if (value !== null) destination.searchParams.set(name, value);
|
|
206
|
+
}
|
|
207
|
+
destination.searchParams.set("state", envelope.s);
|
|
208
|
+
return destination;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function localOAuthRelayChallengeProof(challenge, secret) {
|
|
212
|
+
if (!validSecret(secret) || typeof challenge !== "string"
|
|
213
|
+
|| challenge.length !== 43 || !BASE64_URL.test(challenge)) {
|
|
214
|
+
throw new Error("invalid local OAuth relay challenge");
|
|
215
|
+
}
|
|
216
|
+
const key = await hmacKey(secret, ["sign"]);
|
|
217
|
+
const signature = new Uint8Array(await crypto.subtle.sign(
|
|
218
|
+
"HMAC",
|
|
219
|
+
key,
|
|
220
|
+
new TextEncoder().encode(`${HEALTH_CONTEXT}${challenge}`),
|
|
221
|
+
));
|
|
222
|
+
return encodeBase64Url(signature);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function validEnvelope(value, provider) {
|
|
226
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
227
|
+
const keys = Object.keys(value).sort().join(",");
|
|
228
|
+
return keys === "e,f,i,n,o,p,s,v"
|
|
229
|
+
&& value.v === 1
|
|
230
|
+
&& value.p === provider
|
|
231
|
+
&& PROVIDERS.has(value.p)
|
|
232
|
+
&& isLocalNanocodexOrigin(value.o)
|
|
233
|
+
&& FLOWS.has(value.f)
|
|
234
|
+
&& typeof value.s === "string"
|
|
235
|
+
&& value.s.length > 0
|
|
236
|
+
&& value.s.length <= MAX_INNER_STATE_LENGTH
|
|
237
|
+
&& Number.isSafeInteger(value.i)
|
|
238
|
+
&& Number.isSafeInteger(value.e)
|
|
239
|
+
&& validNonce(value.n);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function validMcpEnvelope(value, connectionId) {
|
|
243
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
244
|
+
const keys = Object.keys(value).sort().join(",");
|
|
245
|
+
return keys === "c,e,f,i,n,o,s,v"
|
|
246
|
+
&& value.v === 1
|
|
247
|
+
&& value.c === connectionId
|
|
248
|
+
&& typeof value.c === "string"
|
|
249
|
+
&& CONNECTION_ID.test(value.c)
|
|
250
|
+
&& isLocalNanocodexOrigin(value.o)
|
|
251
|
+
&& FLOWS.has(value.f)
|
|
252
|
+
&& typeof value.s === "string"
|
|
253
|
+
&& value.s.length > 0
|
|
254
|
+
&& value.s.length <= MAX_INNER_STATE_LENGTH
|
|
255
|
+
&& Number.isSafeInteger(value.i)
|
|
256
|
+
&& Number.isSafeInteger(value.e)
|
|
257
|
+
&& validNonce(value.n);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function validRelayTarget(provider, connectionId) {
|
|
261
|
+
return (PROVIDERS.has(provider) && connectionId === undefined)
|
|
262
|
+
|| (provider === undefined && typeof connectionId === "string" && CONNECTION_ID.test(connectionId));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function validSecret(secret) {
|
|
266
|
+
return typeof secret === "string" && secret.length >= 32 && secret.length <= 1_024;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function validNonce(value) {
|
|
270
|
+
return typeof value === "string" && value.length === 22 && BASE64_URL.test(value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function randomNonce() {
|
|
274
|
+
const bytes = new Uint8Array(16);
|
|
275
|
+
crypto.getRandomValues(bytes);
|
|
276
|
+
return encodeBase64Url(bytes);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function sign(secret, payload) {
|
|
280
|
+
const key = await hmacKey(secret, ["sign"]);
|
|
281
|
+
return new Uint8Array(await crypto.subtle.sign(
|
|
282
|
+
"HMAC",
|
|
283
|
+
key,
|
|
284
|
+
new TextEncoder().encode(`${SIGNING_CONTEXT}${payload}`),
|
|
285
|
+
));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function hmacKey(secret, usages) {
|
|
289
|
+
return crypto.subtle.importKey(
|
|
290
|
+
"raw",
|
|
291
|
+
new TextEncoder().encode(secret),
|
|
292
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
293
|
+
false,
|
|
294
|
+
usages,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function encodeBase64Url(value) {
|
|
299
|
+
let binary = "";
|
|
300
|
+
for (const byte of value) binary += String.fromCharCode(byte);
|
|
301
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function decodeBase64Url(value) {
|
|
305
|
+
if (!value || !BASE64_URL.test(value)) return undefined;
|
|
306
|
+
try {
|
|
307
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
308
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
309
|
+
return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
|
|
310
|
+
} catch {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nanocodex-vite",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Vite, Cloudflare, WASM, ChatGPT development, and OAuth relay integration for Nanocodex",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./index.d.mts",
|
|
9
|
+
"import": "./index.mjs"
|
|
10
|
+
},
|
|
11
|
+
"./cloudflare": {
|
|
12
|
+
"types": "./cloudflare.d.mts",
|
|
13
|
+
"import": "./cloudflare.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./oauth-relay": {
|
|
16
|
+
"types": "./oauth-relay.d.mts",
|
|
17
|
+
"import": "./oauth-relay.mjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"*.mjs",
|
|
22
|
+
"*.mts",
|
|
23
|
+
"scripts"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"provenance": true
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"ws": "^8.18.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@cloudflare/vite-plugin": "1.54.2",
|
|
34
|
+
"@types/node": "^22.20.1",
|
|
35
|
+
"nanocodex": "0.6.0",
|
|
36
|
+
"typescript": "5.9.3",
|
|
37
|
+
"vite": "8.2.1",
|
|
38
|
+
"wrangler": "4.127.1"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@cloudflare/vite-plugin": ">=1.54.2",
|
|
42
|
+
"nanocodex": "^0.6.0",
|
|
43
|
+
"vite": ">=6.1.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@cloudflare/vite-plugin": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22.13.0"
|
|
52
|
+
},
|
|
53
|
+
"license": "MIT OR Apache-2.0",
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "git+https://github.com/gakonst/nanocodex.git",
|
|
57
|
+
"directory": "js/nanocodex-vite"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build:wasm": "./scripts/build-js-package.sh",
|
|
61
|
+
"build:wasm:release": "./scripts/build-js-package.sh --release",
|
|
62
|
+
"test": "node --test test/*.test.mjs && npm run test:typecheck",
|
|
63
|
+
"test:typecheck": "tsc -p test/tsconfig.json"
|
|
64
|
+
}
|
|
65
|
+
}
|