@grackle-ai/web-server 0.75.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.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @grackle-ai/web-server
2
+
3
+ HTTP web server for [Grackle](https://github.com/nick-pape/grackle) — static file serving, device pairing, OAuth authorization, and ConnectRPC proxy.
4
+
5
+ ## Overview
6
+
7
+ `createWebServer(options)` returns an `http.Server` that handles:
8
+
9
+ - **Static file serving** — serves the `@grackle-ai/web` SPA with path traversal protection and SPA fallback
10
+ - **Device pairing** — `/pair` endpoint for pairing code entry and session cookie creation
11
+ - **OAuth 2.1** — `/.well-known/oauth-authorization-server`, `/register`, `/authorize`, `/token` endpoints
12
+ - **ConnectRPC proxy** — forwards `/grackle.Grackle/*` requests to injected ConnectRPC routes (Connect protocol over HTTP/1.1)
13
+ - **Session auth gate** — unauthenticated requests redirect to `/pair`
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import { createWebServer } from "@grackle-ai/web-server";
19
+ import { registerGrackleRoutes } from "./grpc-service.js";
20
+
21
+ const webServer = createWebServer({
22
+ apiKey,
23
+ webPort: 3000,
24
+ bindHost: "127.0.0.1",
25
+ connectRoutes: registerGrackleRoutes,
26
+ });
27
+
28
+ webServer.listen(3000, "127.0.0.1");
29
+ ```
30
+
31
+ ## API
32
+
33
+ ### `createWebServer(options: WebServerOptions): http.Server`
34
+
35
+ | Option | Type | Required | Description |
36
+ |--------|------|----------|-------------|
37
+ | `apiKey` | `string` | yes | API key for session/bearer auth |
38
+ | `webPort` | `number` | yes | Port (used for OAuth URL generation) |
39
+ | `bindHost` | `string` | yes | Bind host (`127.0.0.1` or `0.0.0.0`) |
40
+ | `connectRoutes` | `(router) => void` | no | ConnectRPC route registration function |
41
+ | `webDistDir` | `string` | no | Override web UI dist directory |
42
+
43
+ ### `isWildcardAddress(host: string): boolean`
44
+
45
+ Returns `true` if the host is a wildcard bind address (`0.0.0.0`, `::`, etc.).
@@ -0,0 +1,4 @@
1
+ export { createWebServer } from "./web-server.js";
2
+ export type { WebServerOptions } from "./web-server.js";
3
+ export { isWildcardAddress } from "./web-server.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createWebServer } from "./web-server.js";
2
+ export { isWildcardAddress } from "./web-server.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.57.7"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,26 @@
1
+ import http from "node:http";
2
+ import type { ConnectRouter } from "@connectrpc/connect";
3
+ /** Options for creating a Grackle web server. */
4
+ export interface WebServerOptions {
5
+ /** API key for session/bearer auth. */
6
+ apiKey: string;
7
+ /** Port the web server will listen on (used for OAuth URL generation). */
8
+ webPort: number;
9
+ /** Bind host (e.g. "127.0.0.1" or "0.0.0.0"). */
10
+ bindHost: string;
11
+ /** ConnectRPC route registration function (injected from grpc-service). */
12
+ connectRoutes?: (router: ConnectRouter) => void;
13
+ /** Override the web UI dist directory (default: resolve from `grackle-ai/web`). */
14
+ webDistDir?: string;
15
+ }
16
+ /** Whether a bind address is a wildcard (binds all interfaces). */
17
+ export declare function isWildcardAddress(host: string): boolean;
18
+ /**
19
+ * Create an HTTP server that serves the Grackle web UI, pairing flow,
20
+ * OAuth authorization server, and optionally proxies ConnectRPC requests.
21
+ *
22
+ * @param options - Server configuration.
23
+ * @returns An `http.Server` ready to `.listen()`.
24
+ */
25
+ export declare function createWebServer(options: WebServerOptions): http.Server;
26
+ //# sourceMappingURL=web-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-server.d.ts","sourceRoot":"","sources":["../src/web-server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAK7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAazD,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAChD,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA4ND,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAID;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC,MAAM,CAwXtE"}
@@ -0,0 +1,536 @@
1
+ import http from "node:http";
2
+ import { readFileSync, existsSync } from "node:fs";
3
+ import { join, dirname, extname, normalize, resolve, relative } from "node:path";
4
+ import { createRequire } from "node:module";
5
+ import { connectNodeAdapter } from "@connectrpc/connect-node";
6
+ import { setSecurityHeaders, createSession, validateSessionCookie, verifyApiKey, redeemPairingCode, registerClient, getClient, createAuthorizationCode, consumeAuthorizationCode, createRefreshToken, consumeRefreshToken, createOAuthAccessToken, OAUTH_ACCESS_TOKEN_TTL_MS, } from "@grackle-ai/auth";
7
+ // ─── Static File Config ─────────────────────────────────────
8
+ const MIME_TYPES = {
9
+ ".html": "text/html",
10
+ ".js": "application/javascript",
11
+ ".css": "text/css",
12
+ ".json": "application/json",
13
+ ".svg": "image/svg+xml",
14
+ ".png": "image/png",
15
+ ".ico": "image/x-icon",
16
+ };
17
+ /** Resolve the web UI dist directory. */
18
+ function resolveWebDistDir() {
19
+ const esmRequire = createRequire(import.meta.url);
20
+ return resolve(process.env.GRACKLE_WEB_DIR
21
+ || join(dirname(esmRequire.resolve("@grackle-ai/web/package.json")), "dist"));
22
+ }
23
+ // ─── HTML Pages ─────────────────────────────────────────────
24
+ /** Escape HTML special characters for safe embedding in attributes and text content. */
25
+ function escapeHtml(str) {
26
+ return str
27
+ .replace(/&/g, "&")
28
+ .replace(/</g, "&lt;")
29
+ .replace(/>/g, "&gt;")
30
+ .replace(/"/g, "&quot;")
31
+ .replace(/'/g, "&#x27;");
32
+ }
33
+ /** Minimal HTML page shown when the user needs to enter a pairing code. */
34
+ function renderPairingPage(error) {
35
+ const errorHtml = error ? `<p style="color:#e74c3c;margin-bottom:1rem">${escapeHtml(error)}</p>` : "";
36
+ return `<!DOCTYPE html>
37
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
38
+ <title>Grackle — Pair Device</title>
39
+ <style>
40
+ *{box-sizing:border-box;margin:0;padding:0}
41
+ body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#0f172a;color:#e2e8f0}
42
+ .card{background:#1e293b;border-radius:12px;padding:2.5rem;max-width:400px;width:100%;text-align:center;box-shadow:0 4px 24px rgba(0,0,0,.3)}
43
+ h1{font-size:1.5rem;margin-bottom:.5rem}
44
+ p{color:#94a3b8;margin-bottom:1.5rem;font-size:.95rem}
45
+ input{width:100%;padding:.75rem 1rem;font-size:1.25rem;letter-spacing:.3em;text-align:center;border:2px solid #334155;border-radius:8px;background:#0f172a;color:#e2e8f0;text-transform:uppercase;font-family:monospace}
46
+ input:focus{outline:none;border-color:#3b82f6}
47
+ button{margin-top:1rem;width:100%;padding:.75rem;font-size:1rem;border:none;border-radius:8px;background:#3b82f6;color:#fff;cursor:pointer;font-weight:600}
48
+ button:hover{background:#2563eb}
49
+ </style></head><body>
50
+ <div class="card">
51
+ <h1>Grackle</h1>
52
+ <p>Enter the pairing code shown in your terminal.</p>
53
+ ${errorHtml}
54
+ <form method="GET" action="/pair">
55
+ <input name="code" type="text" maxlength="6" pattern="[A-Za-z0-9]{6}" autocomplete="off" autofocus placeholder="ABC123" required>
56
+ <button type="submit">Pair</button>
57
+ </form>
58
+ </div></body></html>`;
59
+ }
60
+ /** Shared card styles used by both pairing and authorize pages. */
61
+ const CARD_STYLES = `*{box-sizing:border-box;margin:0;padding:0}
62
+ body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;background:#0f172a;color:#e2e8f0}
63
+ .card{background:#1e293b;border-radius:12px;padding:2.5rem;max-width:400px;width:100%;text-align:center;box-shadow:0 4px 24px rgba(0,0,0,.3)}
64
+ h1{font-size:1.5rem;margin-bottom:.5rem}
65
+ p{color:#94a3b8;margin-bottom:1.5rem;font-size:.95rem}
66
+ input{width:100%;padding:.75rem 1rem;font-size:1.25rem;letter-spacing:.3em;text-align:center;border:2px solid #334155;border-radius:8px;background:#0f172a;color:#e2e8f0;text-transform:uppercase;font-family:monospace;margin-bottom:.5rem}
67
+ input:focus{outline:none;border-color:#3b82f6}
68
+ button{margin-top:1rem;width:100%;padding:.75rem;font-size:1rem;border:none;border-radius:8px;background:#3b82f6;color:#fff;cursor:pointer;font-weight:600}
69
+ button:hover{background:#2563eb}
70
+ .btn-deny{background:#475569;margin-top:.5rem}
71
+ .btn-deny:hover{background:#334155}
72
+ .client-name{color:#3b82f6;font-weight:600}`;
73
+ /**
74
+ * Render the OAuth authorize page.
75
+ *
76
+ * If the user has a valid session, shows a simple "Authorize" / "Deny" form.
77
+ * If not paired, adds a pairing code input so the user can pair and authorize in one step.
78
+ */
79
+ function renderAuthorizePage(clientName, oauthParams, hasPairedSession, error) {
80
+ const errorHtml = error ? `<p style="color:#e74c3c;margin-bottom:1rem">${escapeHtml(error)}</p>` : "";
81
+ const pairingField = hasPairedSession
82
+ ? ""
83
+ : `<p>Enter the pairing code shown in your terminal to pair and authorize.</p>
84
+ <input name="pairing_code" type="text" maxlength="6" pattern="[A-Za-z0-9]{6}" autocomplete="off" autofocus placeholder="ABC123" required>`;
85
+ const buttonLabel = hasPairedSession ? "Authorize" : "Pair &amp; Authorize";
86
+ return `<!DOCTYPE html>
87
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
88
+ <title>Grackle — Authorize MCP Client</title>
89
+ <style>${CARD_STYLES}</style></head><body>
90
+ <div class="card">
91
+ <h1>Authorize MCP Client</h1>
92
+ <p><span class="client-name">${escapeHtml(clientName)}</span> wants to connect to Grackle.</p>
93
+ ${errorHtml}
94
+ <form method="POST" action="/authorize">
95
+ ${pairingField}
96
+ <input type="hidden" name="oauth_params" value="${escapeHtml(oauthParams)}">
97
+ <button type="submit" name="action" value="approve">${buttonLabel}</button>
98
+ <button type="submit" name="action" value="deny" class="btn-deny">Deny</button>
99
+ </form>
100
+ </div></body></html>`;
101
+ }
102
+ // ─── HTTP Helpers ───────────────────────────────────────────
103
+ /** Maximum size for form/JSON request bodies: 16 KB. */
104
+ const MAX_BODY_SIZE = 16_384;
105
+ /**
106
+ * Read the raw body string from an HTTP request with size limit enforcement.
107
+ *
108
+ * @param req - The incoming HTTP request.
109
+ * @returns The raw body as a UTF-8 string.
110
+ */
111
+ function readBody(req) {
112
+ return new Promise((resolve, reject) => {
113
+ const chunks = [];
114
+ let totalSize = 0;
115
+ req.on("data", (chunk) => {
116
+ totalSize += chunk.length;
117
+ if (totalSize > MAX_BODY_SIZE) {
118
+ req.destroy();
119
+ reject(new Error("Body too large"));
120
+ return;
121
+ }
122
+ chunks.push(chunk);
123
+ });
124
+ req.on("end", () => {
125
+ resolve(Buffer.concat(chunks).toString("utf8"));
126
+ });
127
+ req.on("error", reject);
128
+ });
129
+ }
130
+ /**
131
+ * Parse a URL-encoded form body from an HTTP request.
132
+ *
133
+ * @param req - The incoming HTTP request.
134
+ * @returns Parsed key-value pairs from the form body.
135
+ */
136
+ async function parseFormBody(req) {
137
+ const raw = await readBody(req);
138
+ return new URLSearchParams(raw);
139
+ }
140
+ /**
141
+ * Serve a static file from the web dist directory.
142
+ * Always writes a response (200, 403, 404, or 500).
143
+ */
144
+ function serveStaticFile(res, rawPath, distDir) {
145
+ const isRoot = rawPath === "/" || rawPath === "";
146
+ let filePath = isRoot
147
+ ? join(distDir, "index.html")
148
+ : resolve(distDir, normalize(`.${rawPath}`));
149
+ // Prevent path traversal — resolved path must stay within the dist directory
150
+ const rel = relative(distDir, filePath);
151
+ if (rel.startsWith("..") || resolve(distDir, rel) !== filePath) {
152
+ res.writeHead(403);
153
+ res.end("Forbidden");
154
+ return;
155
+ }
156
+ // After path traversal validation, filePath is safe to use with fs operations.
157
+ // CodeQL flags these as "uncontrolled data in path expression" but the check
158
+ // above guarantees the path is within distDir.
159
+ const safeFilePath = existsSync(filePath) ? filePath : join(distDir, "index.html"); // SPA fallback
160
+ if (!existsSync(safeFilePath)) {
161
+ res.writeHead(404);
162
+ res.end("Not found");
163
+ return;
164
+ }
165
+ const ext = extname(safeFilePath);
166
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
167
+ try {
168
+ const content = readFileSync(safeFilePath);
169
+ res.writeHead(200, { "Content-Type": contentType });
170
+ res.end(content);
171
+ }
172
+ catch {
173
+ res.writeHead(500);
174
+ res.end("Server error");
175
+ }
176
+ }
177
+ /** Extract the remote IP from an incoming request. */
178
+ function getRemoteIp(req) {
179
+ return req.socket.remoteAddress || "unknown";
180
+ }
181
+ /** Static assets served without session authentication (favicons, manifest, logo). */
182
+ const PUBLIC_ASSETS = new Set([
183
+ "/favicon.ico",
184
+ "/favicon-16x16.png",
185
+ "/favicon-32x32.png",
186
+ "/apple-touch-icon.png",
187
+ "/manifest.json",
188
+ "/icon-192x192.png",
189
+ "/icon-512x512.png",
190
+ "/grackle-logo.png",
191
+ ]);
192
+ // ─── Utilities ──────────────────────────────────────────────
193
+ /** Whether a bind address is a wildcard (binds all interfaces). */
194
+ export function isWildcardAddress(host) {
195
+ return host === "0.0.0.0" || host === "::" || host === "0:0:0:0:0:0:0:0";
196
+ }
197
+ // ─── Factory ────────────────────────────────────────────────
198
+ /**
199
+ * Create an HTTP server that serves the Grackle web UI, pairing flow,
200
+ * OAuth authorization server, and optionally proxies ConnectRPC requests.
201
+ *
202
+ * @param options - Server configuration.
203
+ * @returns An `http.Server` ready to `.listen()`.
204
+ */
205
+ export function createWebServer(options) {
206
+ const { apiKey, webPort, bindHost, connectRoutes, webDistDir } = options;
207
+ const distDir = webDistDir ?? resolveWebDistDir();
208
+ const allowNetwork = isWildcardAddress(bindHost);
209
+ const dialableHost = allowNetwork ? "127.0.0.1" : bindHost;
210
+ const urlHost = dialableHost.includes(":") ? `[${dialableHost}]` : dialableHost;
211
+ const webBaseUrl = `http://${urlHost}:${webPort}`;
212
+ /** ConnectRPC handler for browser gRPC calls (Connect protocol over HTTP/1.1). */
213
+ const webConnectHandler = connectRoutes
214
+ ? connectNodeAdapter({ routes: connectRoutes })
215
+ : undefined;
216
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
217
+ const handler = async (req, res) => {
218
+ setSecurityHeaders(res);
219
+ let rawPath;
220
+ let queryString = "";
221
+ try {
222
+ const urlParts = (req.url || "/").split("?");
223
+ rawPath = decodeURIComponent(urlParts[0]);
224
+ queryString = urlParts[1] || "";
225
+ }
226
+ catch {
227
+ res.writeHead(400);
228
+ res.end("Bad Request");
229
+ return;
230
+ }
231
+ // --- OAuth Authorization Server Metadata (no auth) ---
232
+ if (rawPath === "/.well-known/oauth-authorization-server") {
233
+ res.writeHead(200, { "Content-Type": "application/json" });
234
+ res.end(JSON.stringify({
235
+ issuer: webBaseUrl,
236
+ authorization_endpoint: `${webBaseUrl}/authorize`,
237
+ token_endpoint: `${webBaseUrl}/token`,
238
+ registration_endpoint: `${webBaseUrl}/register`,
239
+ response_types_supported: ["code"],
240
+ grant_types_supported: ["authorization_code", "refresh_token"],
241
+ code_challenge_methods_supported: ["S256"],
242
+ token_endpoint_auth_methods_supported: ["none"],
243
+ }));
244
+ return;
245
+ }
246
+ // --- Dynamic Client Registration (no auth, JSON body) ---
247
+ if (rawPath === "/register" && req.method === "POST") {
248
+ try {
249
+ const raw = await readBody(req);
250
+ const parsed = JSON.parse(raw);
251
+ const redirectUris = parsed.redirect_uris;
252
+ const clientName = parsed.client_name;
253
+ if (!Array.isArray(redirectUris) || redirectUris.length === 0) {
254
+ res.writeHead(400, { "Content-Type": "application/json" });
255
+ res.end(JSON.stringify({ error: "invalid_request", error_description: "redirect_uris is required" }));
256
+ return;
257
+ }
258
+ // Validate each redirect URI — only allow http(s) on loopback to prevent open redirects
259
+ for (const uri of redirectUris) {
260
+ try {
261
+ const parsed = new URL(uri);
262
+ const isLoopback = parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost" || parsed.hostname === "::1";
263
+ const isHttpOrHttps = parsed.protocol === "http:" || parsed.protocol === "https:";
264
+ if (!isLoopback || !isHttpOrHttps) {
265
+ res.writeHead(400, { "Content-Type": "application/json" });
266
+ res.end(JSON.stringify({ error: "invalid_client_metadata", error_description: "redirect_uris must use http(s) on loopback (127.0.0.1, localhost, or ::1)" }));
267
+ return;
268
+ }
269
+ }
270
+ catch {
271
+ res.writeHead(400, { "Content-Type": "application/json" });
272
+ res.end(JSON.stringify({ error: "invalid_client_metadata", error_description: "Invalid redirect_uri" }));
273
+ return;
274
+ }
275
+ }
276
+ const client = registerClient(redirectUris, clientName);
277
+ if (!client) {
278
+ res.writeHead(503, { "Content-Type": "application/json" });
279
+ res.end(JSON.stringify({ error: "temporarily_unavailable", error_description: "Too many registered clients" }));
280
+ return;
281
+ }
282
+ res.writeHead(201, { "Content-Type": "application/json" });
283
+ res.end(JSON.stringify({
284
+ client_id: client.clientId,
285
+ redirect_uris: client.redirectUris,
286
+ client_name: client.clientName,
287
+ }));
288
+ }
289
+ catch {
290
+ res.writeHead(400, { "Content-Type": "application/json" });
291
+ res.end(JSON.stringify({ error: "invalid_request" }));
292
+ }
293
+ return;
294
+ }
295
+ // --- OAuth Authorize (GET — render page, no auth required) ---
296
+ if (rawPath === "/authorize" && req.method === "GET") {
297
+ const params = new URLSearchParams(queryString);
298
+ const clientId = params.get("client_id") || "";
299
+ const responseType = params.get("response_type") || "";
300
+ const redirectUri = params.get("redirect_uri") || "";
301
+ const codeChallenge = params.get("code_challenge") || "";
302
+ const codeChallengeMethod = params.get("code_challenge_method") || "";
303
+ const state = params.get("state") || "";
304
+ const resource = params.get("resource") || "";
305
+ // Validate required params
306
+ if (responseType !== "code") {
307
+ res.writeHead(400, { "Content-Type": "application/json" });
308
+ res.end(JSON.stringify({ error: "unsupported_response_type" }));
309
+ return;
310
+ }
311
+ if (!clientId || !redirectUri || !codeChallenge) {
312
+ res.writeHead(400, { "Content-Type": "application/json" });
313
+ res.end(JSON.stringify({ error: "invalid_request", error_description: "Missing required parameters" }));
314
+ return;
315
+ }
316
+ if (codeChallengeMethod && codeChallengeMethod !== "S256") {
317
+ res.writeHead(400, { "Content-Type": "application/json" });
318
+ res.end(JSON.stringify({ error: "invalid_request", error_description: "Only S256 code challenge method is supported" }));
319
+ return;
320
+ }
321
+ const client = getClient(clientId);
322
+ if (!client) {
323
+ res.writeHead(400, { "Content-Type": "application/json" });
324
+ res.end(JSON.stringify({ error: "invalid_request", error_description: "Unknown client_id" }));
325
+ return;
326
+ }
327
+ if (!client.redirectUris.includes(redirectUri)) {
328
+ res.writeHead(400, { "Content-Type": "application/json" });
329
+ res.end(JSON.stringify({ error: "invalid_request", error_description: "redirect_uri not registered" }));
330
+ return;
331
+ }
332
+ // Serialize OAuth params for the hidden form field
333
+ const oauthParams = new URLSearchParams({
334
+ client_id: clientId,
335
+ redirect_uri: redirectUri,
336
+ code_challenge: codeChallenge,
337
+ state,
338
+ resource,
339
+ }).toString();
340
+ const cookieHeader = req.headers.cookie || "";
341
+ const hasPairedSession = validateSessionCookie(cookieHeader, apiKey);
342
+ const html = renderAuthorizePage(client.clientName, oauthParams, hasPairedSession);
343
+ res.writeHead(200, { "Content-Type": "text/html" });
344
+ res.end(html);
345
+ return;
346
+ }
347
+ // --- OAuth Authorize (POST — process approval/denial) ---
348
+ if (rawPath === "/authorize" && req.method === "POST") {
349
+ try {
350
+ const formData = await parseFormBody(req);
351
+ const action = formData.get("action") || "";
352
+ const oauthParamsStr = formData.get("oauth_params") || "";
353
+ const pairingCode = formData.get("pairing_code") || "";
354
+ const oauthParams = new URLSearchParams(oauthParamsStr);
355
+ const clientId = oauthParams.get("client_id") || "";
356
+ const redirectUri = oauthParams.get("redirect_uri") || "";
357
+ const codeChallenge = oauthParams.get("code_challenge") || "";
358
+ const state = oauthParams.get("state") || "";
359
+ const resource = oauthParams.get("resource") || "";
360
+ // Validate client and redirect URI before any redirect to prevent open redirect
361
+ const client = getClient(clientId);
362
+ if (!client?.redirectUris.includes(redirectUri)) {
363
+ res.writeHead(400, { "Content-Type": "application/json" });
364
+ res.end(JSON.stringify({ error: "invalid_request" }));
365
+ return;
366
+ }
367
+ // Build redirect URL using URL API to safely merge query params
368
+ const buildRedirect = (params) => {
369
+ const url = new URL(redirectUri);
370
+ for (const [key, value] of Object.entries(params)) {
371
+ url.searchParams.set(key, value);
372
+ }
373
+ if (state) {
374
+ url.searchParams.set("state", state);
375
+ }
376
+ return url.toString();
377
+ };
378
+ // Deny action
379
+ if (action === "deny") {
380
+ res.writeHead(302, { Location: buildRedirect({ error: "access_denied" }) });
381
+ res.end();
382
+ return;
383
+ }
384
+ // Check session — if no session, require pairing code
385
+ const cookieHeader = req.headers.cookie || "";
386
+ let hasPairedSession = validateSessionCookie(cookieHeader, apiKey);
387
+ const responseHeaders = {};
388
+ if (!hasPairedSession) {
389
+ if (!pairingCode) {
390
+ const html = renderAuthorizePage(client.clientName, oauthParamsStr, false, "Pairing code is required.");
391
+ res.writeHead(200, { "Content-Type": "text/html" });
392
+ res.end(html);
393
+ return;
394
+ }
395
+ const remoteIp = getRemoteIp(req);
396
+ if (!redeemPairingCode(pairingCode, remoteIp)) {
397
+ const html = renderAuthorizePage(client.clientName, oauthParamsStr, false, "Invalid or expired pairing code.");
398
+ res.writeHead(200, { "Content-Type": "text/html" });
399
+ res.end(html);
400
+ return;
401
+ }
402
+ // Pairing succeeded — also create a browser session
403
+ const setCookie = createSession(apiKey, { secure: allowNetwork });
404
+ responseHeaders["Set-Cookie"] = setCookie;
405
+ hasPairedSession = true;
406
+ }
407
+ // Approved — create authorization code
408
+ const authCode = createAuthorizationCode(clientId, redirectUri, codeChallenge, resource);
409
+ const redirectUrl = buildRedirect({ code: authCode });
410
+ res.writeHead(302, {
411
+ ...responseHeaders,
412
+ Location: redirectUrl,
413
+ });
414
+ res.end();
415
+ }
416
+ catch {
417
+ res.writeHead(400);
418
+ res.end("Bad Request");
419
+ }
420
+ return;
421
+ }
422
+ // --- OAuth Token endpoint ---
423
+ if (rawPath === "/token" && req.method === "POST") {
424
+ try {
425
+ const formData = await parseFormBody(req);
426
+ const grantType = formData.get("grant_type") || "";
427
+ if (grantType === "authorization_code") {
428
+ const code = formData.get("code") || "";
429
+ const clientId = formData.get("client_id") || "";
430
+ const redirectUri = formData.get("redirect_uri") || "";
431
+ const codeVerifier = formData.get("code_verifier") || "";
432
+ const resource = formData.get("resource") || "";
433
+ const authCodeRecord = consumeAuthorizationCode(code, clientId, redirectUri, codeVerifier, resource);
434
+ if (!authCodeRecord) {
435
+ res.writeHead(400, { "Content-Type": "application/json" });
436
+ res.end(JSON.stringify({ error: "invalid_grant" }));
437
+ return;
438
+ }
439
+ const accessToken = createOAuthAccessToken(clientId, resource, apiKey);
440
+ const refreshToken = createRefreshToken(clientId, resource);
441
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
442
+ res.end(JSON.stringify({
443
+ access_token: accessToken,
444
+ token_type: "Bearer",
445
+ expires_in: Math.floor(OAUTH_ACCESS_TOKEN_TTL_MS / 1000),
446
+ refresh_token: refreshToken,
447
+ }));
448
+ return;
449
+ }
450
+ if (grantType === "refresh_token") {
451
+ const refreshToken = formData.get("refresh_token") || "";
452
+ const clientId = formData.get("client_id") || "";
453
+ const refreshRecord = consumeRefreshToken(refreshToken, clientId);
454
+ if (!refreshRecord) {
455
+ res.writeHead(400, { "Content-Type": "application/json" });
456
+ res.end(JSON.stringify({ error: "invalid_grant" }));
457
+ return;
458
+ }
459
+ const accessToken = createOAuthAccessToken(clientId, refreshRecord.resource, apiKey);
460
+ const newRefreshToken = createRefreshToken(clientId, refreshRecord.resource);
461
+ res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
462
+ res.end(JSON.stringify({
463
+ access_token: accessToken,
464
+ token_type: "Bearer",
465
+ expires_in: Math.floor(OAUTH_ACCESS_TOKEN_TTL_MS / 1000),
466
+ refresh_token: newRefreshToken,
467
+ }));
468
+ return;
469
+ }
470
+ res.writeHead(400, { "Content-Type": "application/json" });
471
+ res.end(JSON.stringify({ error: "unsupported_grant_type" }));
472
+ }
473
+ catch {
474
+ res.writeHead(400, { "Content-Type": "application/json" });
475
+ res.end(JSON.stringify({ error: "invalid_request" }));
476
+ }
477
+ return;
478
+ }
479
+ // --- Pairing endpoint ---
480
+ if (rawPath === "/pair") {
481
+ const params = new URLSearchParams(queryString);
482
+ const code = params.get("code");
483
+ if (code) {
484
+ const remoteIp = getRemoteIp(req);
485
+ if (redeemPairingCode(code, remoteIp)) {
486
+ const setCookie = createSession(apiKey, { secure: allowNetwork });
487
+ res.writeHead(302, {
488
+ Location: "/",
489
+ "Set-Cookie": setCookie,
490
+ });
491
+ res.end();
492
+ return;
493
+ }
494
+ // Invalid or expired code — show pairing page with error
495
+ const html = renderPairingPage("Invalid or expired pairing code. Try again.");
496
+ res.writeHead(200, { "Content-Type": "text/html" });
497
+ res.end(html);
498
+ return;
499
+ }
500
+ // No code provided — show the pairing form
501
+ const html = renderPairingPage();
502
+ res.writeHead(200, { "Content-Type": "text/html" });
503
+ res.end(html);
504
+ return;
505
+ }
506
+ // --- Public static assets (favicons, manifest) — no session required ---
507
+ if (PUBLIC_ASSETS.has(rawPath)) {
508
+ serveStaticFile(res, rawPath, distDir);
509
+ return;
510
+ }
511
+ // --- All other routes require a valid session cookie or Bearer token ---
512
+ const cookieHeader = req.headers.cookie || "";
513
+ const authHeader = req.headers.authorization || "";
514
+ const bearerToken = authHeader.replace(/^Bearer\s+/i, "");
515
+ const hasValidSession = validateSessionCookie(cookieHeader, apiKey);
516
+ const hasValidBearer = bearerToken.length > 0 && verifyApiKey(bearerToken);
517
+ // --- ConnectRPC routes (Connect protocol over HTTP/1.1) ---
518
+ if (rawPath.startsWith("/grackle.Grackle/") && webConnectHandler) {
519
+ if (!hasValidSession && !hasValidBearer) {
520
+ res.writeHead(401, { "Content-Type": "application/json" });
521
+ res.end(JSON.stringify({ error: "Unauthorized" }));
522
+ return;
523
+ }
524
+ webConnectHandler(req, res);
525
+ return;
526
+ }
527
+ if (!hasValidSession) {
528
+ res.writeHead(302, { Location: "/pair" });
529
+ res.end();
530
+ return;
531
+ }
532
+ serveStaticFile(res, rawPath, distDir);
533
+ };
534
+ return http.createServer(handler);
535
+ }
536
+ //# sourceMappingURL=web-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-server.js","sourceRoot":"","sources":["../src/web-server.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@grackle-ai/web-server",
3
+ "version": "0.75.1",
4
+ "description": "HTTP web server for Grackle — static file serving, pairing, OAuth, and ConnectRPC proxy",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/nick-pape/grackle.git",
9
+ "directory": "packages/web-server"
10
+ },
11
+ "keywords": [
12
+ "grackle",
13
+ "web-server",
14
+ "http",
15
+ "oauth",
16
+ "pairing"
17
+ ],
18
+ "engines": {
19
+ "node": ">=22.0.0"
20
+ },
21
+ "type": "module",
22
+ "main": "dist/index.js",
23
+ "types": "dist/index.d.ts",
24
+ "files": [
25
+ "dist/"
26
+ ],
27
+ "dependencies": {
28
+ "@connectrpc/connect": "^2.0.0",
29
+ "@connectrpc/connect-node": "^2.0.0",
30
+ "@grackle-ai/auth": "0.75.1",
31
+ "@grackle-ai/web": "0.75.1"
32
+ },
33
+ "devDependencies": {
34
+ "@rushstack/heft": "1.2.7",
35
+ "@types/node": "^22.0.0",
36
+ "vitest": "^3.1.1",
37
+ "@grackle-ai/heft-rig": "0.0.1"
38
+ },
39
+ "scripts": {
40
+ "build": "heft build --clean",
41
+ "test": "vitest run",
42
+ "clean": "heft clean",
43
+ "_phase:build": "heft run --only build -- --clean",
44
+ "_phase:test": "vitest run"
45
+ }
46
+ }