@nodetool-ai/websocket 0.7.0-rc.11
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/dist/agent/agent-runtime.d.ts +47 -0
- package/dist/agent/agent-runtime.d.ts.map +1 -0
- package/dist/agent/agent-runtime.js +577 -0
- package/dist/agent/agent-runtime.js.map +1 -0
- package/dist/agent/codex-agent.d.ts +41 -0
- package/dist/agent/codex-agent.d.ts.map +1 -0
- package/dist/agent/codex-agent.js +334 -0
- package/dist/agent/codex-agent.js.map +1 -0
- package/dist/agent/index.d.ts +6 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +4 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/llm-agent.d.ts +51 -0
- package/dist/agent/llm-agent.d.ts.map +1 -0
- package/dist/agent/llm-agent.js +526 -0
- package/dist/agent/llm-agent.js.map +1 -0
- package/dist/agent/mcp-tool-server.d.ts +33 -0
- package/dist/agent/mcp-tool-server.d.ts.map +1 -0
- package/dist/agent/mcp-tool-server.js +292 -0
- package/dist/agent/mcp-tool-server.js.map +1 -0
- package/dist/agent/opencode-agent.d.ts +32 -0
- package/dist/agent/opencode-agent.d.ts.map +1 -0
- package/dist/agent/opencode-agent.js +332 -0
- package/dist/agent/opencode-agent.js.map +1 -0
- package/dist/agent/pi-agent.d.ts +42 -0
- package/dist/agent/pi-agent.d.ts.map +1 -0
- package/dist/agent/pi-agent.js +429 -0
- package/dist/agent/pi-agent.js.map +1 -0
- package/dist/agent/sdk-provider.d.ts +46 -0
- package/dist/agent/sdk-provider.d.ts.map +1 -0
- package/dist/agent/sdk-provider.js +50 -0
- package/dist/agent/sdk-provider.js.map +1 -0
- package/dist/agent/socket-route.d.ts +11 -0
- package/dist/agent/socket-route.d.ts.map +1 -0
- package/dist/agent/socket-route.js +262 -0
- package/dist/agent/socket-route.js.map +1 -0
- package/dist/agent/transport.d.ts +39 -0
- package/dist/agent/transport.d.ts.map +1 -0
- package/dist/agent/transport.js +17 -0
- package/dist/agent/transport.js.map +1 -0
- package/dist/agent/types.d.ts +11 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +10 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/collection-api.d.ts +14 -0
- package/dist/collection-api.d.ts.map +1 -0
- package/dist/collection-api.js +75 -0
- package/dist/collection-api.js.map +1 -0
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +2 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/file-api.d.ts +12 -0
- package/dist/file-api.d.ts.map +1 -0
- package/dist/file-api.js +76 -0
- package/dist/file-api.js.map +1 -0
- package/dist/http-api.d.ts +94 -0
- package/dist/http-api.d.ts.map +1 -0
- package/dist/http-api.js +1472 -0
- package/dist/http-api.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/asset-paths.d.ts +10 -0
- package/dist/lib/asset-paths.d.ts.map +1 -0
- package/dist/lib/asset-paths.js +45 -0
- package/dist/lib/asset-paths.js.map +1 -0
- package/dist/lib/bridge.d.ts +8 -0
- package/dist/lib/bridge.d.ts.map +1 -0
- package/dist/lib/bridge.js +70 -0
- package/dist/lib/bridge.js.map +1 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +842 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/models-api.d.ts +12 -0
- package/dist/models-api.d.ts.map +1 -0
- package/dist/models-api.js +730 -0
- package/dist/models-api.js.map +1 -0
- package/dist/oauth-api.d.ts +28 -0
- package/dist/oauth-api.d.ts.map +1 -0
- package/dist/oauth-api.js +681 -0
- package/dist/oauth-api.js.map +1 -0
- package/dist/openai-api.d.ts +63 -0
- package/dist/openai-api.d.ts.map +1 -0
- package/dist/openai-api.js +359 -0
- package/dist/openai-api.js.map +1 -0
- package/dist/plugins/websocket.d.ts +14 -0
- package/dist/plugins/websocket.d.ts.map +1 -0
- package/dist/plugins/websocket.js +431 -0
- package/dist/plugins/websocket.js.map +1 -0
- package/dist/resolve-media-urls.d.ts +19 -0
- package/dist/resolve-media-urls.d.ts.map +1 -0
- package/dist/resolve-media-urls.js +109 -0
- package/dist/resolve-media-urls.js.map +1 -0
- package/dist/routes/assets.d.ts +22 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +168 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/collections.d.ts +16 -0
- package/dist/routes/collections.d.ts.map +1 -0
- package/dist/routes/collections.js +32 -0
- package/dist/routes/collections.js.map +1 -0
- package/dist/routes/files.d.ts +12 -0
- package/dist/routes/files.d.ts.map +1 -0
- package/dist/routes/files.js +11 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +41 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/jobs.d.ts +16 -0
- package/dist/routes/jobs.d.ts.map +1 -0
- package/dist/routes/jobs.js +25 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/nodes.d.ts +8 -0
- package/dist/routes/nodes.d.ts.map +1 -0
- package/dist/routes/nodes.js +124 -0
- package/dist/routes/nodes.js.map +1 -0
- package/dist/routes/oauth.d.ts +8 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +19 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/openai.d.ts +8 -0
- package/dist/routes/openai.d.ts.map +1 -0
- package/dist/routes/openai.js +20 -0
- package/dist/routes/openai.js.map +1 -0
- package/dist/routes/storage.d.ts +12 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +13 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/workflows.d.ts +18 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +25 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/routes/workspace.d.ts +15 -0
- package/dist/routes/workspace.d.ts.map +1 -0
- package/dist/routes/workspace.js +24 -0
- package/dist/routes/workspace.js.map +1 -0
- package/dist/screenshot-server.d.ts +21 -0
- package/dist/screenshot-server.d.ts.map +1 -0
- package/dist/screenshot-server.js +222 -0
- package/dist/screenshot-server.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +783 -0
- package/dist/server.js.map +1 -0
- package/dist/settings-registry.d.ts +29 -0
- package/dist/settings-registry.d.ts.map +1 -0
- package/dist/settings-registry.js +117 -0
- package/dist/settings-registry.js.map +1 -0
- package/dist/storage-api.d.ts +5 -0
- package/dist/storage-api.d.ts.map +1 -0
- package/dist/storage-api.js +221 -0
- package/dist/storage-api.js.map +1 -0
- package/dist/test-ui-server.d.ts +19 -0
- package/dist/test-ui-server.d.ts.map +1 -0
- package/dist/test-ui-server.js +1256 -0
- package/dist/test-ui-server.js.map +1 -0
- package/dist/trpc/context.d.ts +21 -0
- package/dist/trpc/context.d.ts.map +1 -0
- package/dist/trpc/context.js +10 -0
- package/dist/trpc/context.js.map +1 -0
- package/dist/trpc/error-formatter.d.ts +18 -0
- package/dist/trpc/error-formatter.d.ts.map +1 -0
- package/dist/trpc/error-formatter.js +41 -0
- package/dist/trpc/error-formatter.js.map +1 -0
- package/dist/trpc/index.d.ts +18 -0
- package/dist/trpc/index.d.ts.map +1 -0
- package/dist/trpc/index.js +13 -0
- package/dist/trpc/index.js.map +1 -0
- package/dist/trpc/middleware.d.ts +8 -0
- package/dist/trpc/middleware.d.ts.map +1 -0
- package/dist/trpc/middleware.js +12 -0
- package/dist/trpc/middleware.js.map +1 -0
- package/dist/trpc/router.d.ts +2543 -0
- package/dist/trpc/router.d.ts.map +1 -0
- package/dist/trpc/router.js +43 -0
- package/dist/trpc/router.js.map +1 -0
- package/dist/trpc/routers/assets.d.ts +229 -0
- package/dist/trpc/routers/assets.d.ts.map +1 -0
- package/dist/trpc/routers/assets.js +263 -0
- package/dist/trpc/routers/assets.js.map +1 -0
- package/dist/trpc/routers/collections.d.ts +88 -0
- package/dist/trpc/routers/collections.d.ts.map +1 -0
- package/dist/trpc/routers/collections.js +166 -0
- package/dist/trpc/routers/collections.js.map +1 -0
- package/dist/trpc/routers/costs.d.ts +129 -0
- package/dist/trpc/routers/costs.d.ts.map +1 -0
- package/dist/trpc/routers/costs.js +95 -0
- package/dist/trpc/routers/costs.js.map +1 -0
- package/dist/trpc/routers/files.d.ts +34 -0
- package/dist/trpc/routers/files.d.ts.map +1 -0
- package/dist/trpc/routers/files.js +92 -0
- package/dist/trpc/routers/files.js.map +1 -0
- package/dist/trpc/routers/jobs.d.ts +92 -0
- package/dist/trpc/routers/jobs.d.ts.map +1 -0
- package/dist/trpc/routers/jobs.js +100 -0
- package/dist/trpc/routers/jobs.js.map +1 -0
- package/dist/trpc/routers/mcp-config.d.ts +69 -0
- package/dist/trpc/routers/mcp-config.d.ts.map +1 -0
- package/dist/trpc/routers/mcp-config.js +290 -0
- package/dist/trpc/routers/mcp-config.js.map +1 -0
- package/dist/trpc/routers/messages.d.ts +121 -0
- package/dist/trpc/routers/messages.d.ts.map +1 -0
- package/dist/trpc/routers/messages.js +113 -0
- package/dist/trpc/routers/messages.js.map +1 -0
- package/dist/trpc/routers/models.d.ts +748 -0
- package/dist/trpc/routers/models.d.ts.map +1 -0
- package/dist/trpc/routers/models.js +1107 -0
- package/dist/trpc/routers/models.js.map +1 -0
- package/dist/trpc/routers/nodes.d.ts +48 -0
- package/dist/trpc/routers/nodes.d.ts.map +1 -0
- package/dist/trpc/routers/nodes.js +57 -0
- package/dist/trpc/routers/nodes.js.map +1 -0
- package/dist/trpc/routers/sandboxes.d.ts +64 -0
- package/dist/trpc/routers/sandboxes.d.ts.map +1 -0
- package/dist/trpc/routers/sandboxes.js +327 -0
- package/dist/trpc/routers/sandboxes.js.map +1 -0
- package/dist/trpc/routers/settings.d.ts +113 -0
- package/dist/trpc/routers/settings.d.ts.map +1 -0
- package/dist/trpc/routers/settings.js +193 -0
- package/dist/trpc/routers/settings.js.map +1 -0
- package/dist/trpc/routers/skills.d.ts +41 -0
- package/dist/trpc/routers/skills.d.ts.map +1 -0
- package/dist/trpc/routers/skills.js +194 -0
- package/dist/trpc/routers/skills.js.map +1 -0
- package/dist/trpc/routers/storage.d.ts +44 -0
- package/dist/trpc/routers/storage.d.ts.map +1 -0
- package/dist/trpc/routers/storage.js +153 -0
- package/dist/trpc/routers/storage.js.map +1 -0
- package/dist/trpc/routers/threads.d.ts +95 -0
- package/dist/trpc/routers/threads.d.ts.map +1 -0
- package/dist/trpc/routers/threads.js +150 -0
- package/dist/trpc/routers/threads.js.map +1 -0
- package/dist/trpc/routers/users.d.ts +79 -0
- package/dist/trpc/routers/users.d.ts.map +1 -0
- package/dist/trpc/routers/users.js +125 -0
- package/dist/trpc/routers/users.js.map +1 -0
- package/dist/trpc/routers/workflows.d.ts +681 -0
- package/dist/trpc/routers/workflows.d.ts.map +1 -0
- package/dist/trpc/routers/workflows.js +725 -0
- package/dist/trpc/routers/workflows.js.map +1 -0
- package/dist/trpc/routers/workspace.d.ts +127 -0
- package/dist/trpc/routers/workspace.d.ts.map +1 -0
- package/dist/trpc/routers/workspace.js +203 -0
- package/dist/trpc/routers/workspace.js.map +1 -0
- package/dist/unified-websocket-runner.d.ts +280 -0
- package/dist/unified-websocket-runner.d.ts.map +1 -0
- package/dist/unified-websocket-runner.js +3840 -0
- package/dist/unified-websocket-runner.js.map +1 -0
- package/dist/workspace-api.d.ts +14 -0
- package/dist/workspace-api.d.ts.map +1 -0
- package/dist/workspace-api.js +104 -0
- package/dist/workspace-api.js.map +1 -0
- package/dist/ws-adapter.d.ts +25 -0
- package/dist/ws-adapter.d.ts.map +1 -0
- package/dist/ws-adapter.js +63 -0
- package/dist/ws-adapter.js.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth API handler – third-party service authentication via OAuth 2.0 PKCE.
|
|
3
|
+
*
|
|
4
|
+
* Port of Python's `nodetool.api.oauth`.
|
|
5
|
+
*
|
|
6
|
+
* Supports HuggingFace and GitHub OAuth flows with PKCE.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
9
|
+
import { OAuthCredential } from "@nodetool-ai/models";
|
|
10
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
11
|
+
const HF_AUTHORIZATION_URL = "https://huggingface.co/oauth/authorize";
|
|
12
|
+
const HF_TOKEN_URL = "https://huggingface.co/oauth/token";
|
|
13
|
+
const HF_WHOAMI_URL = "https://huggingface.co/api/whoami-v2";
|
|
14
|
+
const HF_CLIENT_ID = "54d170bb-b441-445b-a167-56935d718d4e";
|
|
15
|
+
const HF_SCOPES = ["openid", "read-repos", "inference-api"];
|
|
16
|
+
const GITHUB_AUTHORIZATION_URL = "https://github.com/login/oauth/authorize";
|
|
17
|
+
const GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
18
|
+
const GITHUB_USER_URL = "https://api.github.com/user";
|
|
19
|
+
const GITHUB_SCOPES = ["user:email", "read:user"];
|
|
20
|
+
const STATE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
21
|
+
// ── PKCE Utilities ───────────────────────────────────────────────────
|
|
22
|
+
export function generatePkcePair() {
|
|
23
|
+
const codeVerifier = randomBytes(96).toString("base64url");
|
|
24
|
+
const codeChallenge = createHash("sha256")
|
|
25
|
+
.update(codeVerifier)
|
|
26
|
+
.digest("base64url");
|
|
27
|
+
return { codeVerifier, codeChallenge };
|
|
28
|
+
}
|
|
29
|
+
export function generateState() {
|
|
30
|
+
return randomBytes(32).toString("base64url");
|
|
31
|
+
}
|
|
32
|
+
export const oauthStateStore = new Map();
|
|
33
|
+
/** Remove expired entries from the state store. */
|
|
34
|
+
function pruneExpiredStates() {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const [key, data] of oauthStateStore) {
|
|
37
|
+
if (now - data.createdAt > STATE_TTL_MS) {
|
|
38
|
+
oauthStateStore.delete(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ── HTML Sanitization ────────────────────────────────────────────────
|
|
43
|
+
/** Escape HTML special characters to prevent XSS. */
|
|
44
|
+
function escapeHtml(str) {
|
|
45
|
+
return str
|
|
46
|
+
.replace(/&/g, "&")
|
|
47
|
+
.replace(/</g, "<")
|
|
48
|
+
.replace(/>/g, ">")
|
|
49
|
+
.replace(/"/g, """)
|
|
50
|
+
.replace(/'/g, "'");
|
|
51
|
+
}
|
|
52
|
+
// ── Response Helpers ─────────────────────────────────────────────────
|
|
53
|
+
function jsonResponse(data, status = 200) {
|
|
54
|
+
return new Response(JSON.stringify(data), {
|
|
55
|
+
status,
|
|
56
|
+
headers: { "content-type": "application/json" }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function errorResponse(status, detail) {
|
|
60
|
+
return jsonResponse({ detail }, status);
|
|
61
|
+
}
|
|
62
|
+
function htmlResponse(html, status = 200) {
|
|
63
|
+
return new Response(html, {
|
|
64
|
+
status,
|
|
65
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function oauthHtmlResponse(opts) {
|
|
69
|
+
const successColor = "#22C55E";
|
|
70
|
+
const errorColor = "#EF4444";
|
|
71
|
+
const primaryColor = "#FFD21E";
|
|
72
|
+
const icon = opts.success ? "\u2713" : "\u2717";
|
|
73
|
+
const iconColor = opts.success ? successColor : errorColor;
|
|
74
|
+
let heading;
|
|
75
|
+
let message;
|
|
76
|
+
let details;
|
|
77
|
+
if (opts.success) {
|
|
78
|
+
heading = "Authentication Successful";
|
|
79
|
+
message = "Your account has been connected successfully.";
|
|
80
|
+
details = opts.username
|
|
81
|
+
? `<strong>Username:</strong> ${escapeHtml(opts.username)}`
|
|
82
|
+
: "";
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
heading = "Authentication Failed";
|
|
86
|
+
message = escapeHtml(opts.errorDescription || "An error occurred during authentication.");
|
|
87
|
+
details = opts.error
|
|
88
|
+
? `<strong>Error:</strong> ${escapeHtml(opts.error)}`
|
|
89
|
+
: "";
|
|
90
|
+
}
|
|
91
|
+
const autoCloseScript = opts.autoClose
|
|
92
|
+
? `<script>setTimeout(function(){ window.close(); }, 5000);</script>`
|
|
93
|
+
: "";
|
|
94
|
+
const html = `<!DOCTYPE html>
|
|
95
|
+
<html lang="en">
|
|
96
|
+
<head>
|
|
97
|
+
<meta charset="UTF-8">
|
|
98
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
99
|
+
<title>${escapeHtml(opts.title)}</title>
|
|
100
|
+
<style>
|
|
101
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
102
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
|
|
103
|
+
.container { background: white; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); padding: 48px; max-width: 500px; width: 100%; text-align: center; }
|
|
104
|
+
.icon { font-size: 64px; color: ${iconColor}; margin-bottom: 24px; }
|
|
105
|
+
h1 { font-size: 28px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px; }
|
|
106
|
+
.message { font-size: 16px; color: #4a4a4a; margin-bottom: 24px; line-height: 1.5; }
|
|
107
|
+
.details { background: #f5f5f5; padding: 16px; border-radius: 8px; margin: 24px 0; font-size: 14px; color: #666; }
|
|
108
|
+
.close-hint { margin-top: 24px; font-size: 14px; color: #888; }
|
|
109
|
+
.logo { margin-top: 32px; font-size: 14px; color: #999; }
|
|
110
|
+
.close-button { background: ${primaryColor}; color: #1a1a1a; border: none; padding: 12px 24px; border-radius: 8px; font-size: 16px; font-weight: 500; cursor: pointer; margin-top: 24px; }
|
|
111
|
+
</style>
|
|
112
|
+
</head>
|
|
113
|
+
<body>
|
|
114
|
+
<div class="container">
|
|
115
|
+
<div class="icon">${icon}</div>
|
|
116
|
+
<h1>${heading}</h1>
|
|
117
|
+
<p class="message">${message}</p>
|
|
118
|
+
${details ? `<div class="details">${details}</div>` : ""}
|
|
119
|
+
<button class="close-button" onclick="window.close()">Close Window</button>
|
|
120
|
+
<div class="close-hint">You can also close this window manually</div>
|
|
121
|
+
<div class="logo">Powered by NodeTool</div>
|
|
122
|
+
</div>
|
|
123
|
+
${autoCloseScript}
|
|
124
|
+
</body>
|
|
125
|
+
</html>`;
|
|
126
|
+
return htmlResponse(html);
|
|
127
|
+
}
|
|
128
|
+
// ── Token metadata helper ────────────────────────────────────────────
|
|
129
|
+
function toTokenMetadata(cred) {
|
|
130
|
+
return {
|
|
131
|
+
id: cred.id,
|
|
132
|
+
provider: cred.provider,
|
|
133
|
+
account_id: cred.account_id,
|
|
134
|
+
username: cred.username,
|
|
135
|
+
token_type: cred.token_type,
|
|
136
|
+
scope: cred.scope,
|
|
137
|
+
received_at: cred.received_at,
|
|
138
|
+
expires_at: cred.expires_at,
|
|
139
|
+
created_at: cred.created_at,
|
|
140
|
+
updated_at: cred.updated_at
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// ── HuggingFace Endpoints ────────────────────────────────────────────
|
|
144
|
+
async function handleHfStart(request, getUserId) {
|
|
145
|
+
if (request.method !== "GET")
|
|
146
|
+
return errorResponse(405, "Method not allowed");
|
|
147
|
+
const userId = getUserId();
|
|
148
|
+
const { codeVerifier, codeChallenge } = generatePkcePair();
|
|
149
|
+
const state = generateState();
|
|
150
|
+
// Determine redirect URI from request host
|
|
151
|
+
let host = request.headers.get("host") ?? "localhost:7777";
|
|
152
|
+
if (host.includes("://")) {
|
|
153
|
+
host = host.split("://")[1];
|
|
154
|
+
}
|
|
155
|
+
if (host.startsWith("127.0.0.1")) {
|
|
156
|
+
host = host.replace("127.0.0.1", "localhost");
|
|
157
|
+
}
|
|
158
|
+
const scheme = host.includes("localhost") ? "http" : "https";
|
|
159
|
+
const redirectUri = `${scheme}://${host}/api/oauth/hf/callback`;
|
|
160
|
+
// Store state
|
|
161
|
+
pruneExpiredStates();
|
|
162
|
+
oauthStateStore.set(state, {
|
|
163
|
+
userId,
|
|
164
|
+
codeVerifier,
|
|
165
|
+
redirectUri,
|
|
166
|
+
createdAt: Date.now()
|
|
167
|
+
});
|
|
168
|
+
const params = new URLSearchParams({
|
|
169
|
+
client_id: HF_CLIENT_ID,
|
|
170
|
+
redirect_uri: redirectUri,
|
|
171
|
+
response_type: "code",
|
|
172
|
+
scope: HF_SCOPES.join(" "),
|
|
173
|
+
state,
|
|
174
|
+
code_challenge: codeChallenge,
|
|
175
|
+
code_challenge_method: "S256"
|
|
176
|
+
});
|
|
177
|
+
return jsonResponse({ auth_url: `${HF_AUTHORIZATION_URL}?${params}` });
|
|
178
|
+
}
|
|
179
|
+
async function handleHfCallback(request) {
|
|
180
|
+
if (request.method !== "GET")
|
|
181
|
+
return errorResponse(405, "Method not allowed");
|
|
182
|
+
const url = new URL(request.url);
|
|
183
|
+
const code = url.searchParams.get("code");
|
|
184
|
+
const state = url.searchParams.get("state");
|
|
185
|
+
const error = url.searchParams.get("error");
|
|
186
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
187
|
+
if (error) {
|
|
188
|
+
return oauthHtmlResponse({
|
|
189
|
+
title: "OAuth Error",
|
|
190
|
+
success: false,
|
|
191
|
+
error,
|
|
192
|
+
errorDescription: errorDescription || "No description provided"
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (!code || !state) {
|
|
196
|
+
return oauthHtmlResponse({
|
|
197
|
+
title: "OAuth Error",
|
|
198
|
+
success: false,
|
|
199
|
+
error: "invalid_request",
|
|
200
|
+
errorDescription: "Missing required parameters (code or state)."
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const stateData = oauthStateStore.get(state);
|
|
204
|
+
if (!stateData) {
|
|
205
|
+
return oauthHtmlResponse({
|
|
206
|
+
title: "OAuth Error",
|
|
207
|
+
success: false,
|
|
208
|
+
error: "invalid_state",
|
|
209
|
+
errorDescription: "The authentication request has expired or is invalid. Please try again."
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// Check TTL
|
|
213
|
+
if (Date.now() - stateData.createdAt > STATE_TTL_MS) {
|
|
214
|
+
oauthStateStore.delete(state);
|
|
215
|
+
return oauthHtmlResponse({
|
|
216
|
+
title: "OAuth Error",
|
|
217
|
+
success: false,
|
|
218
|
+
error: "invalid_state",
|
|
219
|
+
errorDescription: "The authentication request has expired. Please try again."
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const { userId, codeVerifier, redirectUri } = stateData;
|
|
223
|
+
oauthStateStore.delete(state);
|
|
224
|
+
try {
|
|
225
|
+
// Exchange code for tokens
|
|
226
|
+
const tokenRes = await fetch(HF_TOKEN_URL, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
229
|
+
body: new URLSearchParams({
|
|
230
|
+
grant_type: "authorization_code",
|
|
231
|
+
code,
|
|
232
|
+
redirect_uri: redirectUri,
|
|
233
|
+
client_id: HF_CLIENT_ID,
|
|
234
|
+
code_verifier: codeVerifier
|
|
235
|
+
})
|
|
236
|
+
});
|
|
237
|
+
if (!tokenRes.ok) {
|
|
238
|
+
const text = await tokenRes.text();
|
|
239
|
+
return oauthHtmlResponse({
|
|
240
|
+
title: "OAuth Error",
|
|
241
|
+
success: false,
|
|
242
|
+
error: "token_exchange_failed",
|
|
243
|
+
errorDescription: `Failed to exchange authorization code for tokens: ${text}`
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const tokenData = (await tokenRes.json());
|
|
247
|
+
const accessToken = tokenData.access_token;
|
|
248
|
+
const refreshToken = tokenData.refresh_token ?? null;
|
|
249
|
+
const tokenType = tokenData.token_type ?? "Bearer";
|
|
250
|
+
const scope = tokenData.scope ?? null;
|
|
251
|
+
const expiresIn = tokenData.expires_in;
|
|
252
|
+
if (!accessToken) {
|
|
253
|
+
return oauthHtmlResponse({
|
|
254
|
+
title: "OAuth Error",
|
|
255
|
+
success: false,
|
|
256
|
+
error: "token_exchange_failed",
|
|
257
|
+
errorDescription: "No access token received from Hugging Face."
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// Get user info
|
|
261
|
+
const whoamiRes = await fetch(HF_WHOAMI_URL, {
|
|
262
|
+
headers: { Authorization: `${tokenType} ${accessToken}` }
|
|
263
|
+
});
|
|
264
|
+
let username = null;
|
|
265
|
+
let accountId;
|
|
266
|
+
if (whoamiRes.ok) {
|
|
267
|
+
const userInfo = (await whoamiRes.json());
|
|
268
|
+
username = userInfo.name ?? userInfo.id ?? null;
|
|
269
|
+
accountId = userInfo.id ?? accessToken.slice(0, 16);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
accountId = accessToken.slice(0, 16);
|
|
273
|
+
}
|
|
274
|
+
const now = new Date().toISOString();
|
|
275
|
+
let expiresAt = null;
|
|
276
|
+
if (expiresIn) {
|
|
277
|
+
expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
278
|
+
}
|
|
279
|
+
await OAuthCredential.upsert({
|
|
280
|
+
user_id: userId,
|
|
281
|
+
provider: "huggingface",
|
|
282
|
+
account_id: accountId,
|
|
283
|
+
access_token: accessToken,
|
|
284
|
+
refresh_token: refreshToken,
|
|
285
|
+
username,
|
|
286
|
+
token_type: tokenType,
|
|
287
|
+
scope,
|
|
288
|
+
received_at: now,
|
|
289
|
+
expires_at: expiresAt
|
|
290
|
+
});
|
|
291
|
+
return oauthHtmlResponse({
|
|
292
|
+
title: "OAuth Success",
|
|
293
|
+
success: true,
|
|
294
|
+
username,
|
|
295
|
+
autoClose: true
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
300
|
+
return oauthHtmlResponse({
|
|
301
|
+
title: "OAuth Error",
|
|
302
|
+
success: false,
|
|
303
|
+
error: "internal_error",
|
|
304
|
+
errorDescription: `An unexpected error occurred: ${message}`
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function handleHfTokens(getUserId) {
|
|
309
|
+
const userId = getUserId();
|
|
310
|
+
const credentials = await OAuthCredential.listForUserAndProvider(userId, "huggingface");
|
|
311
|
+
return jsonResponse({
|
|
312
|
+
tokens: credentials.map(toTokenMetadata)
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async function handleHfRefresh(request, getUserId) {
|
|
316
|
+
if (request.method !== "POST")
|
|
317
|
+
return errorResponse(405, "Method not allowed");
|
|
318
|
+
const url = new URL(request.url);
|
|
319
|
+
const accountId = url.searchParams.get("account_id");
|
|
320
|
+
if (!accountId)
|
|
321
|
+
return errorResponse(400, "Missing account_id parameter");
|
|
322
|
+
const userId = getUserId();
|
|
323
|
+
const credential = await OAuthCredential.findByAccount(userId, "huggingface", accountId);
|
|
324
|
+
if (!credential) {
|
|
325
|
+
return errorResponse(404, `No credential found for account_id: ${accountId}`);
|
|
326
|
+
}
|
|
327
|
+
if (!credential.encrypted_refresh_token) {
|
|
328
|
+
return errorResponse(400, "No refresh token available. Please re-authenticate.");
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const tokenRes = await fetch(HF_TOKEN_URL, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
334
|
+
body: new URLSearchParams({
|
|
335
|
+
grant_type: "refresh_token",
|
|
336
|
+
refresh_token: credential.encrypted_refresh_token,
|
|
337
|
+
client_id: HF_CLIENT_ID
|
|
338
|
+
})
|
|
339
|
+
});
|
|
340
|
+
if (!tokenRes.ok) {
|
|
341
|
+
const text = await tokenRes.text();
|
|
342
|
+
return errorResponse(400, `Failed to refresh token: ${text}`);
|
|
343
|
+
}
|
|
344
|
+
const tokenData = (await tokenRes.json());
|
|
345
|
+
const accessToken = tokenData.access_token;
|
|
346
|
+
const newRefreshToken = tokenData.refresh_token ?? credential.encrypted_refresh_token;
|
|
347
|
+
const tokenType = tokenData.token_type ?? credential.token_type;
|
|
348
|
+
const scope = tokenData.scope ?? credential.scope;
|
|
349
|
+
const expiresIn = tokenData.expires_in;
|
|
350
|
+
if (!accessToken) {
|
|
351
|
+
return errorResponse(400, "No access token in refresh response");
|
|
352
|
+
}
|
|
353
|
+
let expiresAt = null;
|
|
354
|
+
if (expiresIn) {
|
|
355
|
+
expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
356
|
+
}
|
|
357
|
+
credential.encrypted_access_token = accessToken;
|
|
358
|
+
credential.encrypted_refresh_token = newRefreshToken;
|
|
359
|
+
credential.token_type = tokenType;
|
|
360
|
+
credential.scope = scope;
|
|
361
|
+
credential.received_at = new Date().toISOString();
|
|
362
|
+
credential.expires_at = expiresAt;
|
|
363
|
+
await credential.save();
|
|
364
|
+
return jsonResponse({
|
|
365
|
+
success: true,
|
|
366
|
+
message: "Token refreshed successfully"
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
371
|
+
return errorResponse(500, `Failed to communicate with Hugging Face: ${message}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function handleHfWhoami(request, getUserId) {
|
|
375
|
+
if (request.method !== "GET")
|
|
376
|
+
return errorResponse(405, "Method not allowed");
|
|
377
|
+
const url = new URL(request.url);
|
|
378
|
+
const accountId = url.searchParams.get("account_id");
|
|
379
|
+
if (!accountId)
|
|
380
|
+
return errorResponse(400, "Missing account_id parameter");
|
|
381
|
+
const userId = getUserId();
|
|
382
|
+
const credential = await OAuthCredential.findByAccount(userId, "huggingface", accountId);
|
|
383
|
+
if (!credential) {
|
|
384
|
+
return errorResponse(404, `No credential found for account_id: ${accountId}`);
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const res = await fetch(HF_WHOAMI_URL, {
|
|
388
|
+
headers: {
|
|
389
|
+
Authorization: `${credential.token_type} ${credential.encrypted_access_token}`
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
if (res.status === 401) {
|
|
393
|
+
return errorResponse(401, "Token expired or invalid. Please refresh or re-authenticate.");
|
|
394
|
+
}
|
|
395
|
+
if (!res.ok) {
|
|
396
|
+
const text = await res.text();
|
|
397
|
+
return errorResponse(res.status, `Hugging Face API error: ${text}`);
|
|
398
|
+
}
|
|
399
|
+
const data = (await res.json());
|
|
400
|
+
return jsonResponse({
|
|
401
|
+
id: data.id ?? "",
|
|
402
|
+
name: data.name ?? null,
|
|
403
|
+
email: data.email ?? null,
|
|
404
|
+
type: data.type ?? null,
|
|
405
|
+
orgs: data.orgs ?? null
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
410
|
+
return errorResponse(500, `Failed to communicate with Hugging Face: ${message}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// ── GitHub Endpoints ─────────────────────────────────────────────────
|
|
414
|
+
async function handleGithubStart(request, getUserId) {
|
|
415
|
+
if (request.method !== "GET")
|
|
416
|
+
return errorResponse(405, "Method not allowed");
|
|
417
|
+
const githubClientId = process.env.GITHUB_CLIENT_ID;
|
|
418
|
+
if (!githubClientId) {
|
|
419
|
+
return errorResponse(500, "GitHub OAuth not configured. Please set GITHUB_CLIENT_ID.");
|
|
420
|
+
}
|
|
421
|
+
const userId = getUserId();
|
|
422
|
+
const { codeVerifier, codeChallenge } = generatePkcePair();
|
|
423
|
+
const state = generateState();
|
|
424
|
+
let host = request.headers.get("host") ?? "localhost:7777";
|
|
425
|
+
if (host.includes("://")) {
|
|
426
|
+
host = host.split("://")[1];
|
|
427
|
+
}
|
|
428
|
+
if (host.startsWith("127.0.0.1")) {
|
|
429
|
+
host = host.replace("127.0.0.1", "localhost");
|
|
430
|
+
}
|
|
431
|
+
const scheme = host.includes("localhost") ? "http" : "https";
|
|
432
|
+
const redirectUri = `${scheme}://${host}/api/oauth/github/callback`;
|
|
433
|
+
pruneExpiredStates();
|
|
434
|
+
oauthStateStore.set(state, {
|
|
435
|
+
userId,
|
|
436
|
+
codeVerifier,
|
|
437
|
+
redirectUri,
|
|
438
|
+
createdAt: Date.now()
|
|
439
|
+
});
|
|
440
|
+
const params = new URLSearchParams({
|
|
441
|
+
client_id: githubClientId,
|
|
442
|
+
redirect_uri: redirectUri,
|
|
443
|
+
response_type: "code",
|
|
444
|
+
scope: GITHUB_SCOPES.join(" "),
|
|
445
|
+
state,
|
|
446
|
+
code_challenge: codeChallenge,
|
|
447
|
+
code_challenge_method: "S256"
|
|
448
|
+
});
|
|
449
|
+
return jsonResponse({
|
|
450
|
+
auth_url: `${GITHUB_AUTHORIZATION_URL}?${params}`
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async function handleGithubCallback(request) {
|
|
454
|
+
if (request.method !== "GET")
|
|
455
|
+
return errorResponse(405, "Method not allowed");
|
|
456
|
+
const url = new URL(request.url);
|
|
457
|
+
const code = url.searchParams.get("code");
|
|
458
|
+
const state = url.searchParams.get("state");
|
|
459
|
+
const error = url.searchParams.get("error");
|
|
460
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
461
|
+
if (error) {
|
|
462
|
+
return oauthHtmlResponse({
|
|
463
|
+
title: "OAuth Error",
|
|
464
|
+
success: false,
|
|
465
|
+
error,
|
|
466
|
+
errorDescription: errorDescription || "No description provided"
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
if (!code || !state) {
|
|
470
|
+
return oauthHtmlResponse({
|
|
471
|
+
title: "OAuth Error",
|
|
472
|
+
success: false,
|
|
473
|
+
error: "invalid_request",
|
|
474
|
+
errorDescription: "Missing required parameters (code or state)."
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
const stateData = oauthStateStore.get(state);
|
|
478
|
+
if (!stateData) {
|
|
479
|
+
return oauthHtmlResponse({
|
|
480
|
+
title: "OAuth Error",
|
|
481
|
+
success: false,
|
|
482
|
+
error: "invalid_state",
|
|
483
|
+
errorDescription: "The authentication request has expired or is invalid. Please try again."
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
if (Date.now() - stateData.createdAt > STATE_TTL_MS) {
|
|
487
|
+
oauthStateStore.delete(state);
|
|
488
|
+
return oauthHtmlResponse({
|
|
489
|
+
title: "OAuth Error",
|
|
490
|
+
success: false,
|
|
491
|
+
error: "invalid_state",
|
|
492
|
+
errorDescription: "The authentication request has expired. Please try again."
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const { userId, codeVerifier, redirectUri } = stateData;
|
|
496
|
+
oauthStateStore.delete(state);
|
|
497
|
+
const githubClientId = process.env.GITHUB_CLIENT_ID;
|
|
498
|
+
const githubClientSecret = process.env.GITHUB_CLIENT_SECRET;
|
|
499
|
+
if (!githubClientId || !githubClientSecret) {
|
|
500
|
+
return oauthHtmlResponse({
|
|
501
|
+
title: "OAuth Error",
|
|
502
|
+
success: false,
|
|
503
|
+
error: "configuration_error",
|
|
504
|
+
errorDescription: "GitHub OAuth is not properly configured."
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const tokenRes = await fetch(GITHUB_TOKEN_URL, {
|
|
509
|
+
method: "POST",
|
|
510
|
+
headers: {
|
|
511
|
+
Accept: "application/json",
|
|
512
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
513
|
+
},
|
|
514
|
+
body: new URLSearchParams({
|
|
515
|
+
client_id: githubClientId,
|
|
516
|
+
client_secret: githubClientSecret,
|
|
517
|
+
code,
|
|
518
|
+
code_verifier: codeVerifier,
|
|
519
|
+
redirect_uri: redirectUri
|
|
520
|
+
})
|
|
521
|
+
});
|
|
522
|
+
if (!tokenRes.ok) {
|
|
523
|
+
const text = await tokenRes.text();
|
|
524
|
+
return oauthHtmlResponse({
|
|
525
|
+
title: "OAuth Error",
|
|
526
|
+
success: false,
|
|
527
|
+
error: "token_exchange_failed",
|
|
528
|
+
errorDescription: `Failed to exchange authorization code for tokens: ${text}`
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const tokenData = (await tokenRes.json());
|
|
532
|
+
const accessToken = tokenData.access_token;
|
|
533
|
+
const tokenType = tokenData.token_type ?? "Bearer";
|
|
534
|
+
const scope = tokenData.scope ?? null;
|
|
535
|
+
if (!accessToken) {
|
|
536
|
+
return oauthHtmlResponse({
|
|
537
|
+
title: "OAuth Error",
|
|
538
|
+
success: false,
|
|
539
|
+
error: "token_exchange_failed",
|
|
540
|
+
errorDescription: "No access token received from GitHub."
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
// Get user info
|
|
544
|
+
const userRes = await fetch(GITHUB_USER_URL, {
|
|
545
|
+
headers: {
|
|
546
|
+
Authorization: `${tokenType} ${accessToken}`,
|
|
547
|
+
Accept: "application/json"
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
let username = null;
|
|
551
|
+
let accountId;
|
|
552
|
+
if (userRes.ok) {
|
|
553
|
+
const userInfo = (await userRes.json());
|
|
554
|
+
username = userInfo.login ?? null;
|
|
555
|
+
accountId = String(userInfo.id);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
accountId = String(Math.abs(hashCode(accessToken.slice(0, 16))));
|
|
559
|
+
}
|
|
560
|
+
const now = new Date().toISOString();
|
|
561
|
+
await OAuthCredential.upsert({
|
|
562
|
+
user_id: userId,
|
|
563
|
+
provider: "github",
|
|
564
|
+
account_id: accountId,
|
|
565
|
+
access_token: accessToken,
|
|
566
|
+
refresh_token: null,
|
|
567
|
+
username,
|
|
568
|
+
token_type: tokenType,
|
|
569
|
+
scope,
|
|
570
|
+
received_at: now,
|
|
571
|
+
expires_at: null
|
|
572
|
+
});
|
|
573
|
+
return oauthHtmlResponse({
|
|
574
|
+
title: "OAuth Success",
|
|
575
|
+
success: true,
|
|
576
|
+
username,
|
|
577
|
+
autoClose: true
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
582
|
+
return oauthHtmlResponse({
|
|
583
|
+
title: "OAuth Error",
|
|
584
|
+
success: false,
|
|
585
|
+
error: "internal_error",
|
|
586
|
+
errorDescription: `An unexpected error occurred: ${message}`
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async function handleGithubTokens(getUserId) {
|
|
591
|
+
const userId = getUserId();
|
|
592
|
+
const credentials = await OAuthCredential.listForUserAndProvider(userId, "github");
|
|
593
|
+
return jsonResponse({
|
|
594
|
+
tokens: credentials.map(toTokenMetadata)
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async function handleGithubUser(request, getUserId) {
|
|
598
|
+
if (request.method !== "GET")
|
|
599
|
+
return errorResponse(405, "Method not allowed");
|
|
600
|
+
const url = new URL(request.url);
|
|
601
|
+
const accountId = url.searchParams.get("account_id");
|
|
602
|
+
if (!accountId)
|
|
603
|
+
return errorResponse(400, "Missing account_id parameter");
|
|
604
|
+
const userId = getUserId();
|
|
605
|
+
const credential = await OAuthCredential.findByAccount(userId, "github", accountId);
|
|
606
|
+
if (!credential) {
|
|
607
|
+
return errorResponse(404, `No credential found for account_id: ${accountId}`);
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
const res = await fetch(GITHUB_USER_URL, {
|
|
611
|
+
headers: {
|
|
612
|
+
Authorization: `${credential.token_type} ${credential.encrypted_access_token}`,
|
|
613
|
+
Accept: "application/json"
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
if (res.status === 401) {
|
|
617
|
+
return errorResponse(401, "Token expired or invalid. Please re-authenticate.");
|
|
618
|
+
}
|
|
619
|
+
if (!res.ok) {
|
|
620
|
+
const text = await res.text();
|
|
621
|
+
return errorResponse(res.status, `GitHub API error: ${text}`);
|
|
622
|
+
}
|
|
623
|
+
const data = await res.json();
|
|
624
|
+
return jsonResponse(data);
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
628
|
+
return errorResponse(500, `Failed to communicate with GitHub: ${message}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// ── Simple hash helper (replicates Python's hash() for fallback) ─────
|
|
632
|
+
function hashCode(str) {
|
|
633
|
+
let hash = 0;
|
|
634
|
+
for (let i = 0; i < str.length; i++) {
|
|
635
|
+
const ch = str.charCodeAt(i);
|
|
636
|
+
hash = ((hash << 5) - hash + ch) | 0;
|
|
637
|
+
}
|
|
638
|
+
return hash;
|
|
639
|
+
}
|
|
640
|
+
// ── Normalise path ───────────────────────────────────────────────────
|
|
641
|
+
function normalizePath(pathname) {
|
|
642
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
643
|
+
return pathname.slice(0, -1);
|
|
644
|
+
}
|
|
645
|
+
return pathname;
|
|
646
|
+
}
|
|
647
|
+
// ── Main Router ──────────────────────────────────────────────────────
|
|
648
|
+
/**
|
|
649
|
+
* Handle an OAuth API request.
|
|
650
|
+
*
|
|
651
|
+
* Returns a Response for recognized routes under /api/oauth/,
|
|
652
|
+
* or null if the path is not an OAuth route.
|
|
653
|
+
*/
|
|
654
|
+
export async function handleOAuthRequest(request, pathname, getUserId) {
|
|
655
|
+
const normalised = normalizePath(pathname);
|
|
656
|
+
switch (normalised) {
|
|
657
|
+
// HuggingFace
|
|
658
|
+
case "/api/oauth/hf/start":
|
|
659
|
+
return handleHfStart(request, getUserId);
|
|
660
|
+
case "/api/oauth/hf/callback":
|
|
661
|
+
return handleHfCallback(request);
|
|
662
|
+
case "/api/oauth/hf/tokens":
|
|
663
|
+
return handleHfTokens(getUserId);
|
|
664
|
+
case "/api/oauth/hf/refresh":
|
|
665
|
+
return handleHfRefresh(request, getUserId);
|
|
666
|
+
case "/api/oauth/hf/whoami":
|
|
667
|
+
return handleHfWhoami(request, getUserId);
|
|
668
|
+
// GitHub
|
|
669
|
+
case "/api/oauth/github/start":
|
|
670
|
+
return handleGithubStart(request, getUserId);
|
|
671
|
+
case "/api/oauth/github/callback":
|
|
672
|
+
return handleGithubCallback(request);
|
|
673
|
+
case "/api/oauth/github/tokens":
|
|
674
|
+
return handleGithubTokens(getUserId);
|
|
675
|
+
case "/api/oauth/github/user":
|
|
676
|
+
return handleGithubUser(request, getUserId);
|
|
677
|
+
default:
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=oauth-api.js.map
|