@fleetx_io/fleetx-mcp-server 1.1.7 → 2.0.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +44 -12
  2. package/README.md +56 -6
  3. package/dist/auth/fleetx/exchange.d.ts +13 -0
  4. package/dist/auth/fleetx/exchange.d.ts.map +1 -0
  5. package/dist/auth/fleetx/exchange.js +27 -0
  6. package/dist/auth/fleetx/exchange.js.map +1 -0
  7. package/dist/auth/google/callback.d.ts +3 -0
  8. package/dist/auth/google/callback.d.ts.map +1 -0
  9. package/dist/auth/google/callback.js +98 -0
  10. package/dist/auth/google/callback.js.map +1 -0
  11. package/dist/auth/google/client.d.ts +17 -0
  12. package/dist/auth/google/client.d.ts.map +1 -0
  13. package/dist/auth/google/client.js +50 -0
  14. package/dist/auth/google/client.js.map +1 -0
  15. package/dist/auth/oauth/authorize.d.ts +3 -0
  16. package/dist/auth/oauth/authorize.d.ts.map +1 -0
  17. package/dist/auth/oauth/authorize.js +74 -0
  18. package/dist/auth/oauth/authorize.js.map +1 -0
  19. package/dist/auth/oauth/cimd.d.ts +33 -0
  20. package/dist/auth/oauth/cimd.d.ts.map +1 -0
  21. package/dist/auth/oauth/cimd.js +63 -0
  22. package/dist/auth/oauth/cimd.js.map +1 -0
  23. package/dist/auth/oauth/pending-sessions.d.ts +5 -0
  24. package/dist/auth/oauth/pending-sessions.d.ts.map +1 -0
  25. package/dist/auth/oauth/pending-sessions.js +24 -0
  26. package/dist/auth/oauth/pending-sessions.js.map +1 -0
  27. package/dist/auth/oauth/pkce.d.ts +3 -0
  28. package/dist/auth/oauth/pkce.d.ts.map +1 -0
  29. package/dist/auth/oauth/pkce.js +7 -0
  30. package/dist/auth/oauth/pkce.js.map +1 -0
  31. package/dist/auth/oauth/register.d.ts +8 -0
  32. package/dist/auth/oauth/register.d.ts.map +1 -0
  33. package/dist/auth/oauth/register.js +37 -0
  34. package/dist/auth/oauth/register.js.map +1 -0
  35. package/dist/auth/oauth/token.d.ts +3 -0
  36. package/dist/auth/oauth/token.d.ts.map +1 -0
  37. package/dist/auth/oauth/token.js +111 -0
  38. package/dist/auth/oauth/token.js.map +1 -0
  39. package/dist/auth/oauth/types.d.ts +11 -0
  40. package/dist/auth/oauth/types.d.ts.map +1 -0
  41. package/dist/auth/oauth/types.js +2 -0
  42. package/dist/auth/oauth/types.js.map +1 -0
  43. package/dist/auth/oauth/userinfo.d.ts +3 -0
  44. package/dist/auth/oauth/userinfo.d.ts.map +1 -0
  45. package/dist/auth/oauth/userinfo.js +29 -0
  46. package/dist/auth/oauth/userinfo.js.map +1 -0
  47. package/dist/auth/storage/authorization-code.d.ts +19 -0
  48. package/dist/auth/storage/authorization-code.d.ts.map +1 -0
  49. package/dist/auth/storage/authorization-code.js +44 -0
  50. package/dist/auth/storage/authorization-code.js.map +1 -0
  51. package/dist/auth/storage/connected-account.d.ts +24 -0
  52. package/dist/auth/storage/connected-account.d.ts.map +1 -0
  53. package/dist/auth/storage/connected-account.js +60 -0
  54. package/dist/auth/storage/connected-account.js.map +1 -0
  55. package/dist/auth/storage/db.d.ts +3 -0
  56. package/dist/auth/storage/db.d.ts.map +1 -0
  57. package/dist/auth/storage/db.js +110 -0
  58. package/dist/auth/storage/db.js.map +1 -0
  59. package/dist/auth/storage/oauth-client.d.ts +12 -0
  60. package/dist/auth/storage/oauth-client.d.ts.map +1 -0
  61. package/dist/auth/storage/oauth-client.js +28 -0
  62. package/dist/auth/storage/oauth-client.js.map +1 -0
  63. package/dist/auth/storage/refresh-token.d.ts +17 -0
  64. package/dist/auth/storage/refresh-token.d.ts.map +1 -0
  65. package/dist/auth/storage/refresh-token.js +46 -0
  66. package/dist/auth/storage/refresh-token.js.map +1 -0
  67. package/dist/auth/tokens/encryption.d.ts +5 -0
  68. package/dist/auth/tokens/encryption.d.ts.map +1 -0
  69. package/dist/auth/tokens/encryption.js +27 -0
  70. package/dist/auth/tokens/encryption.js.map +1 -0
  71. package/dist/auth/tokens/jwt.d.ts +8 -0
  72. package/dist/auth/tokens/jwt.d.ts.map +1 -0
  73. package/dist/auth/tokens/jwt.js +34 -0
  74. package/dist/auth/tokens/jwt.js.map +1 -0
  75. package/dist/auth.d.ts +1 -1
  76. package/dist/auth.d.ts.map +1 -1
  77. package/dist/auth.js +2 -1
  78. package/dist/auth.js.map +1 -1
  79. package/dist/config/oauth.d.ts +21 -0
  80. package/dist/config/oauth.d.ts.map +1 -0
  81. package/dist/config/oauth.js +41 -0
  82. package/dist/config/oauth.js.map +1 -0
  83. package/dist/index.d.ts +1 -1
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +4 -2
  86. package/dist/index.js.map +1 -1
  87. package/dist/mcp/middleware/auth.d.ts +18 -0
  88. package/dist/mcp/middleware/auth.d.ts.map +1 -0
  89. package/dist/mcp/middleware/auth.js +35 -0
  90. package/dist/mcp/middleware/auth.js.map +1 -0
  91. package/dist/server/createMcpServer.d.ts +8 -3
  92. package/dist/server/createMcpServer.d.ts.map +1 -1
  93. package/dist/server/createMcpServer.js +28 -12
  94. package/dist/server/createMcpServer.js.map +1 -1
  95. package/dist/transports/fastify.d.ts +2 -0
  96. package/dist/transports/fastify.d.ts.map +1 -0
  97. package/dist/transports/fastify.js +223 -0
  98. package/dist/transports/fastify.js.map +1 -0
  99. package/dist/transports/sse.d.ts +1 -1
  100. package/dist/transports/sse.d.ts.map +1 -1
  101. package/dist/transports/sse.js +162 -67
  102. package/dist/transports/sse.js.map +1 -1
  103. package/dist/transports/stdio.d.ts.map +1 -1
  104. package/dist/transports/stdio.js +8 -1
  105. package/dist/transports/stdio.js.map +1 -1
  106. package/dist/utils-http.d.ts +8 -0
  107. package/dist/utils-http.d.ts.map +1 -0
  108. package/dist/utils-http.js +15 -0
  109. package/dist/utils-http.js.map +1 -0
  110. package/dist/utils.d.ts +1 -0
  111. package/dist/utils.d.ts.map +1 -1
  112. package/dist/utils.js +12 -2
  113. package/dist/utils.js.map +1 -1
  114. package/dist/well-known/discovery.d.ts +4 -0
  115. package/dist/well-known/discovery.d.ts.map +1 -0
  116. package/dist/well-known/discovery.js +25 -0
  117. package/dist/well-known/discovery.js.map +1 -0
  118. package/package.json +15 -7
package/CHANGELOG.md CHANGED
@@ -5,19 +5,51 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.0.0] - 2026-06-09
9
+
10
+ ### Added
11
+
12
+ - **Remote MCP Server (Streamable HTTP transport)** — server now supports both `stdio` (local) and `http` (remote) transports, selected via `MCP_TRANSPORT` env var.
13
+ - **OAuth 2.1 Authorization Code Flow with PKCE** — full OAuth server built into the MCP server; Claude authenticates users without ever receiving a FleetX token.
14
+ - **Google OAuth sign-in** — users authenticate with their existing FleetX Google accounts via `GET /google/callback`.
15
+ - **FleetX token exchange** — Google ID token is exchanged for a FleetX access token via `POST /api/v2/login/google-one-tap`.
16
+ - **CIMD support** — advertises `client_id_metadata_document_supported: true` + `token_endpoint_auth_methods_supported: ["none"]` so Claude uses Client ID Metadata Document instead of Dynamic Client Registration.
17
+ - **Dynamic Client Registration (DCR, RFC 7591)** — `POST /oauth/register` endpoint for MCP Inspector and other dev tools that require DCR.
18
+ - **Encrypted token storage** — FleetX access tokens stored AES-256-GCM encrypted at rest in SQLite (`better-sqlite3`, WAL mode).
19
+ - **MCP JWT access tokens** — server issues signed HS256 JWTs (1 hr TTL) as MCP bearer tokens; Claude never sees the underlying FleetX token.
20
+ - **OAuth discovery endpoints** — `GET /.well-known/oauth-authorization-server` and `GET /.well-known/oauth-protected-resource`.
21
+ - **`GET /userinfo` endpoint** — returns `sub`, `email`, `name` from the connected account.
22
+ - **`resolveServerBaseUrl()`** — dynamically resolves the server's public base URL from `MCP_BASE_URL` env var or request `Host` header for local dev.
23
+ - **Automatic SQLite schema migrations** — `runMigrations()` on startup; handles existing databases with breaking schema changes (e.g. dropped `NOT NULL` on `fleetx_refresh_token`).
24
+ - **`dotenv/config` loading** — `.env` file loaded automatically at startup; no shell export needed.
25
+ - **`tsx` dev runner** — replaced `ts-node-dev` with `tsx watch` for native ESM support.
26
+ - **`npm run dev:http`** script — starts the HTTP/OAuth server in dev mode with hot reload.
27
+
28
+ ### Changed
29
+
30
+ - **Fastify replaces raw Node HTTP** for the HTTP transport — all OAuth and MCP routes registered through Fastify with proper middleware.
31
+ - **Body parsing fix** — MCP POST handler now uses Fastify's pre-parsed `request.body` instead of re-reading the consumed raw stream.
32
+ - **All auth modes coexist** — API key (env), Basic, raw Bearer, and OAuth MCP JWT all resolved in `resolveCredentials()` with correct priority order.
33
+ - **Logging to stderr** — all `log()` calls write JSON to stderr so stdout stays clean for the stdio JSON-RPC transport.
34
+
35
+ ### Removed
36
+
37
+ - **Refresh token flow** — FleetX `/login/google-one-tap` does not return a refresh token; re-authentication triggers a fresh Google sign-in.
38
+ - **Legacy SSE transport** — removed in favour of `StreamableHTTPServerTransport`.
39
+
8
40
  ## [1.0.0] - 2026-03-03
9
41
 
10
42
  ### Added
11
43
 
12
- - MCP server with stdio transport for local AI agent integration.
13
- - `login` tool for authenticating with the FleetX API.
14
- - Dynamic tool generation from FleetX API definitions after login.
15
- - Zod-based input validation for all dynamically registered tools.
16
- - Automatic Bearer token injection into downstream API calls.
17
- - Token expiry handling — clears token and prompts re-login on 401/403.
18
- - Support for path, query, and body parameters in proxied API calls.
19
- - `npx` support — run directly with `npx @fleetx_io/fleetx-mcp-server`.
20
- - Global install support via `npm install -g`.
21
- - MCP Inspector compatibility for debugging and testing.
22
- - Configuration via environment variables with sensible defaults.
23
- - Cursor, Claude Desktop, and Windsurf integration examples in README.
44
+ - MCP server with stdio transport for local AI agent integration.
45
+ - `login` tool for authenticating with the FleetX API.
46
+ - Dynamic tool generation from FleetX API definitions after login.
47
+ - Zod-based input validation for all dynamically registered tools.
48
+ - Automatic Bearer token injection into downstream API calls.
49
+ - Token expiry handling — clears token and prompts re-login on 401/403.
50
+ - Support for path, query, and body parameters in proxied API calls.
51
+ - `npx` support — run directly with `npx @fleetx_io/fleetx-mcp-server`.
52
+ - Global install support via `npm install -g`.
53
+ - MCP Inspector compatibility for debugging and testing.
54
+ - Configuration via environment variables with sensible defaults.
55
+ - Cursor, Claude Desktop, and Windsurf integration examples in README.
package/README.md CHANGED
@@ -9,10 +9,11 @@ An MCP (Model Context Protocol) server that gives AI agents access to the FleetX
9
9
 
10
10
  ## How it Works
11
11
 
12
- 1. **Hosted:** Your AI agent connects to `https://mcp.fleetx.io/mcp` via URL. **Local:** Your agent launches this server as a subprocess (npx or global install).
13
- 2. The server authenticates — either automatically via credentials (Basic Auth for hosted, env vars for local), or when the agent calls the `login` tool.
14
- 3. After authentication, all available API definitions are fetched and registered as MCP tools with validated inputs.
15
- 4. The agent can now call any FleetX API the server handles auth, validation, and proxying automatically.
12
+ 1. **Claude Connector (OAuth):** Claude connects to `https://mcp.fleetx.io/mcp`, is redirected to Google sign-in, and receives a secure token no credentials ever shared with Claude.
13
+ 2. **Hosted (Basic Auth):** Your AI agent connects via URL with a `Authorization: Basic` header for auto-login.
14
+ 3. **Local:** Your agent launches this server as a subprocess (npx or global install) with credentials in env vars.
15
+ 4. After authentication, all FleetX API definitions are fetched and registered as MCP tools with validated inputs.
16
+ 5. The agent can call any FleetX API — the server handles auth, validation, and proxying automatically.
16
17
 
17
18
  ---
18
19
 
@@ -52,9 +53,52 @@ fleetx-mcp-server
52
53
 
53
54
  ---
54
55
 
55
- ## Hosted MCP Server
56
+ ## Claude Connector (OAuth — Recommended)
56
57
 
57
- Use the hosted FleetX MCP server no local install required. Connect directly from your AI agent.
58
+ The hosted FleetX MCP server supports **Claude Connector** with full OAuth 2.1 authentication. Sign in once with your FleetX Google account and Claude gets access to all FleetX tools — no tokens or passwords needed.
59
+
60
+ **MCP URL:** `https://mcp.fleetx.io/mcp`
61
+
62
+ ### Connect via claude.ai (Web)
63
+
64
+ 1. Open [claude.ai](https://claude.ai) and go to **Settings → Integrations**
65
+ 2. Click **Add custom integration**
66
+ 3. Enter the URL:
67
+ ```
68
+ https://mcp.fleetx.io/mcp
69
+ ```
70
+ 4. Click **Add** — Claude will automatically open a Google sign-in window
71
+ 5. Sign in with your **FleetX Google account** (the same email you use to log in to FleetX)
72
+ 6. After sign-in, Claude redirects back and the integration is connected
73
+ 7. All FleetX tools appear in Claude immediately — no login tool needed
74
+
75
+ > The OAuth token is valid for **15 days**. If the underlying FleetX session expires earlier, the server automatically clears the stored token and Claude will prompt you to re-authenticate.
76
+
77
+ ### Connect via Claude Desktop
78
+
79
+ Add to your `claude_desktop_config.json`:
80
+
81
+ **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
82
+ **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "fleetx": {
88
+ "url": "https://mcp.fleetx.io/mcp",
89
+ "transport": "streamable-http"
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ Save the file and restart Claude Desktop. On first use, Claude opens a browser window for Google sign-in. After authenticating, all FleetX tools are available.
96
+
97
+ ---
98
+
99
+ ## Hosted MCP Server (Basic Auth)
100
+
101
+ For clients that don't support OAuth, use Basic Auth to connect to the hosted server.
58
102
 
59
103
  **URL:** `https://mcp.fleetx.io/mcp`
60
104
 
@@ -436,3 +480,9 @@ This opens a web UI where you can connect, call `login`, browse all discovered t
436
480
  ## License
437
481
 
438
482
  ISC
483
+
484
+ ---
485
+
486
+ ## Changelog
487
+
488
+ See [CHANGELOG.md](CHANGELOG.md) for release history and notable changes.
@@ -0,0 +1,13 @@
1
+ export interface FleetxAuthResult {
2
+ accessToken: string;
3
+ }
4
+ /**
5
+ * Exchanges a Google ID token for a FleetX access token.
6
+ *
7
+ * FleetX response format:
8
+ * { access_token: "...", token_type: "bearer", scope: "..." }
9
+ *
10
+ * User info is NOT included — callers must supply it from the verified Google token.
11
+ */
12
+ export declare function exchangeGoogleTokenWithFleetx(googleIdToken: string): Promise<FleetxAuthResult>;
13
+ //# sourceMappingURL=exchange.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exchange.d.ts","sourceRoot":"","sources":["../../../src/auth/fleetx/exchange.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;CACrB;AAID;;;;;;;GAOG;AACH,wBAAsB,6BAA6B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsBpG"}
@@ -0,0 +1,27 @@
1
+ import axios from "axios";
2
+ import { log } from "../../utils.js";
3
+ const GOOGLE_ONE_TAP_URL = "https://api.fleetx.io/api/v2/login/google-one-tap";
4
+ /**
5
+ * Exchanges a Google ID token for a FleetX access token.
6
+ *
7
+ * FleetX response format:
8
+ * { access_token: "...", token_type: "bearer", scope: "..." }
9
+ *
10
+ * User info is NOT included — callers must supply it from the verified Google token.
11
+ */
12
+ export async function exchangeGoogleTokenWithFleetx(googleIdToken) {
13
+ const form = new FormData();
14
+ form.append("token", googleIdToken);
15
+ const { data } = await axios.post(GOOGLE_ONE_TAP_URL, form, {
16
+ headers: {
17
+ "Content-Type": "multipart/form-data",
18
+ clientid: "fleetxweb",
19
+ },
20
+ });
21
+ if (!data.access_token) {
22
+ throw new Error(`FleetX google-one-tap returned no access_token. Response: ${JSON.stringify(data)}`);
23
+ }
24
+ log("FleetX exchange succeeded, token:", data.access_token.slice(0, 8) + "…");
25
+ return { accessToken: data.access_token };
26
+ }
27
+ //# sourceMappingURL=exchange.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exchange.js","sourceRoot":"","sources":["../../../src/auth/fleetx/exchange.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAMrC,MAAM,kBAAkB,GAAG,mDAAmD,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAAC,aAAqB;IACvE,MAAM,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAEpC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,KAAK,CAAC,IAAI,CAC/B,kBAAkB,EAClB,IAAI,EACJ;QACE,OAAO,EAAE;YACP,cAAc,EAAE,qBAAqB;YACrC,QAAQ,EAAE,WAAW;SACtB;KACF,CACF,CAAC;IAEF,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,6DAA6D,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvG,CAAC;IAED,GAAG,CAAC,mCAAmC,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;IAE9E,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FastifyRequest, FastifyReply } from "fastify";
2
+ export declare function handleGoogleCallback(request: FastifyRequest, reply: FastifyReply): Promise<void>;
3
+ //# sourceMappingURL=callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../../src/auth/google/callback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAgB5D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoFtG"}
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ import { exchangeGoogleCode, verifyGoogleIdToken } from "./client.js";
3
+ import { exchangeGoogleTokenWithFleetx } from "../fleetx/exchange.js";
4
+ import { upsertConnectedAccount } from "../storage/connected-account.js";
5
+ import { createAuthorizationCode } from "../storage/authorization-code.js";
6
+ import { consumePendingSession } from "../oauth/pending-sessions.js";
7
+ import { log } from "../../utils.js";
8
+ const callbackSchema = z.object({
9
+ code: z.string().optional(),
10
+ state: z.string().min(1),
11
+ error: z.string().optional(),
12
+ error_description: z.string().optional(),
13
+ });
14
+ export async function handleGoogleCallback(request, reply) {
15
+ const parsed = callbackSchema.safeParse(request.query);
16
+ if (!parsed.success) {
17
+ return reply.status(400).send({ error: "invalid_callback" });
18
+ }
19
+ const { code, state: sessionId, error: googleError } = parsed.data;
20
+ const pending = consumePendingSession(sessionId);
21
+ if (!pending) {
22
+ return reply.status(400).send({ error: "invalid_state", error_description: "Session not found or expired" });
23
+ }
24
+ if (googleError || !code) {
25
+ const url = new URL(pending.redirectUri);
26
+ url.searchParams.set("error", "access_denied");
27
+ url.searchParams.set("error_description", googleError ?? "Google login was cancelled");
28
+ url.searchParams.set("state", pending.state);
29
+ return reply.redirect(url.toString(), 302);
30
+ }
31
+ // ── Step 1: Exchange Google code for tokens ────────────────────────────
32
+ let tokens;
33
+ try {
34
+ tokens = await exchangeGoogleCode(code, pending.serverBaseUrl);
35
+ log("Google code exchange succeeded");
36
+ }
37
+ catch (err) {
38
+ log("Google code exchange FAILED:", err);
39
+ return redirectError(reply, pending.redirectUri, pending.state, `Google token exchange failed: ${String(err)}`);
40
+ }
41
+ if (!tokens.id_token) {
42
+ log("Google response missing id_token");
43
+ return redirectError(reply, pending.redirectUri, pending.state, "Google response missing id_token");
44
+ }
45
+ // ── Step 2: Verify Google ID token ────────────────────────────────────
46
+ let googleUser;
47
+ try {
48
+ googleUser = await verifyGoogleIdToken(tokens.id_token);
49
+ log("Google ID token verified for:", googleUser.email);
50
+ }
51
+ catch (err) {
52
+ log("Google ID token verification FAILED:", err);
53
+ return redirectError(reply, pending.redirectUri, pending.state, `Google token verification failed: ${String(err)}`);
54
+ }
55
+ // ── Step 3: Exchange with FleetX ──────────────────────────────────────
56
+ let fleetx;
57
+ try {
58
+ fleetx = await exchangeGoogleTokenWithFleetx(tokens.id_token);
59
+ }
60
+ catch (err) {
61
+ log("FleetX exchange FAILED:", err);
62
+ return redirectError(reply, pending.redirectUri, pending.state, `FleetX authentication failed: ${String(err)}`);
63
+ }
64
+ // ── Step 4: Persist account + issue auth code ─────────────────────────
65
+ // User info comes from the verified Google token — FleetX doesn't return it.
66
+ try {
67
+ const account = upsertConnectedAccount({
68
+ userId: googleUser.sub,
69
+ fleetxAccessToken: fleetx.accessToken,
70
+ email: googleUser.email,
71
+ name: googleUser.name,
72
+ });
73
+ const authCode = createAuthorizationCode({
74
+ userId: account.userId,
75
+ clientId: pending.clientId,
76
+ redirectUri: pending.redirectUri,
77
+ codeChallenge: pending.codeChallenge,
78
+ });
79
+ log(`OAuth code issued for ${account.email}`);
80
+ const callbackUrl = new URL(pending.redirectUri);
81
+ callbackUrl.searchParams.set("code", authCode.code);
82
+ callbackUrl.searchParams.set("state", pending.state);
83
+ return reply.redirect(callbackUrl.toString(), 302);
84
+ }
85
+ catch (err) {
86
+ log("Account/code storage FAILED:", err);
87
+ return redirectError(reply, pending.redirectUri, pending.state, `Storage error: ${String(err)}`);
88
+ }
89
+ }
90
+ function redirectError(reply, redirectUri, state, description) {
91
+ log("Redirecting with error:", description);
92
+ const url = new URL(redirectUri);
93
+ url.searchParams.set("error", "server_error");
94
+ url.searchParams.set("error_description", description);
95
+ url.searchParams.set("state", state);
96
+ reply.redirect(url.toString(), 302);
97
+ }
98
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../../../src/auth/google/callback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ export interface GoogleTokens {
2
+ access_token: string;
3
+ id_token: string;
4
+ token_type: string;
5
+ expires_in: number;
6
+ refresh_token?: string;
7
+ }
8
+ export interface GoogleUser {
9
+ sub: string;
10
+ email: string;
11
+ name: string;
12
+ email_verified: boolean;
13
+ }
14
+ export declare function buildGoogleAuthUrl(state: string, serverBaseUrl: string): string;
15
+ export declare function exchangeGoogleCode(code: string, serverBaseUrl: string): Promise<GoogleTokens>;
16
+ export declare function verifyGoogleIdToken(idToken: string): Promise<GoogleUser>;
17
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/auth/google/client.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,OAAO,CAAC;CACzB;AAID,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAc/E;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAuBnG;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAc9E"}
@@ -0,0 +1,50 @@
1
+ import { createRemoteJWKSet, jwtVerify } from "jose";
2
+ import { getOAuthConfig } from "../../config/oauth.js";
3
+ const GOOGLE_JWKS = createRemoteJWKSet(new URL("https://www.googleapis.com/oauth2/v3/certs"));
4
+ export function buildGoogleAuthUrl(state, serverBaseUrl) {
5
+ const { GOOGLE_CLIENT_ID } = getOAuthConfig();
6
+ const params = new URLSearchParams({
7
+ client_id: GOOGLE_CLIENT_ID,
8
+ redirect_uri: `${serverBaseUrl}/google/callback`,
9
+ response_type: "code",
10
+ scope: "openid email profile",
11
+ state,
12
+ access_type: "offline",
13
+ prompt: "select_account consent",
14
+ });
15
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
16
+ }
17
+ export async function exchangeGoogleCode(code, serverBaseUrl) {
18
+ const { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET } = getOAuthConfig();
19
+ const body = new URLSearchParams({
20
+ code,
21
+ client_id: GOOGLE_CLIENT_ID,
22
+ client_secret: GOOGLE_CLIENT_SECRET,
23
+ redirect_uri: `${serverBaseUrl}/google/callback`,
24
+ grant_type: "authorization_code",
25
+ });
26
+ const resp = await fetch("https://oauth2.googleapis.com/token", {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
29
+ body: body.toString(),
30
+ });
31
+ if (!resp.ok) {
32
+ const text = await resp.text();
33
+ throw new Error(`Google token exchange failed (${resp.status}): ${text}`);
34
+ }
35
+ return resp.json();
36
+ }
37
+ export async function verifyGoogleIdToken(idToken) {
38
+ const { GOOGLE_CLIENT_ID } = getOAuthConfig();
39
+ const { payload } = await jwtVerify(idToken, GOOGLE_JWKS, {
40
+ issuer: ["https://accounts.google.com", "accounts.google.com"],
41
+ audience: GOOGLE_CLIENT_ID,
42
+ });
43
+ return {
44
+ sub: payload.sub,
45
+ email: payload["email"],
46
+ name: payload["name"],
47
+ email_verified: payload["email_verified"],
48
+ };
49
+ }
50
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/auth/google/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAiBvD,MAAM,WAAW,GAAG,kBAAkB,CAAC,IAAI,GAAG,CAAC,4CAA4C,CAAC,CAAC,CAAC;AAE9F,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,aAAqB;IACrE,MAAM,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,CAAC;IAE9C,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;QACjC,SAAS,EAAE,gBAAgB;QAC3B,YAAY,EAAE,GAAG,aAAa,kBAAkB;QAChD,aAAa,EAAE,MAAM;QACrB,KAAK,EAAE,sBAAsB;QAC7B,KAAK;QACL,WAAW,EAAE,SAAS;QACtB,MAAM,EAAE,wBAAwB;KACjC,CAAC,CAAC;IAEH,OAAO,gDAAgD,MAAM,EAAE,CAAC;AAClE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAY,EAAE,aAAqB;IAC1E,MAAM,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,GAAG,cAAc,EAAE,CAAC;IAEpE,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;QAC/B,IAAI;QACJ,SAAS,EAAE,gBAAgB;QAC3B,aAAa,EAAE,oBAAoB;QACnC,YAAY,EAAE,GAAG,aAAa,kBAAkB;QAChD,UAAU,EAAE,oBAAoB;KACjC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,qCAAqC,EAAE;QAC9D,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE;KACtB,CAAC,CAAC;IAEH,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,CAAC,MAAM,MAAM,IAAI,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,OAAO,IAAI,CAAC,IAAI,EAA2B,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAe;IACvD,MAAM,EAAE,gBAAgB,EAAE,GAAG,cAAc,EAAE,CAAC;IAE9C,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE;QACxD,MAAM,EAAE,CAAC,6BAA6B,EAAE,qBAAqB,CAAC;QAC9D,QAAQ,EAAE,gBAAgB;KAC3B,CAAC,CAAC;IAEH,OAAO;QACL,GAAG,EAAE,OAAO,CAAC,GAAa;QAC1B,KAAK,EAAE,OAAO,CAAC,OAAO,CAAW;QACjC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAW;QAC/B,cAAc,EAAE,OAAO,CAAC,gBAAgB,CAAY;KACrD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FastifyRequest, FastifyReply } from "fastify";
2
+ export declare function handleAuthorize(request: FastifyRequest, reply: FastifyReply): Promise<void>;
3
+ //# sourceMappingURL=authorize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../../src/auth/oauth/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAqB5D,wBAAsB,eAAe,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DjG"}
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ import { randomBytes } from "node:crypto";
3
+ import { buildGoogleAuthUrl } from "../google/client.js";
4
+ import { addPendingSession } from "./pending-sessions.js";
5
+ import { fetchClientMetadata, isKnownClient } from "./cimd.js";
6
+ import { findClient } from "../storage/oauth-client.js";
7
+ import { CLAUDE_CLIENT_ID, CLAUDE_REDIRECT_URI } from "../../config/oauth.js";
8
+ import { resolveServerBaseUrl } from "../../utils-http.js";
9
+ import { log } from "../../utils.js";
10
+ const authorizeQuerySchema = z.object({
11
+ client_id: z.string().min(1),
12
+ redirect_uri: z.string().min(1),
13
+ response_type: z.literal("code"),
14
+ state: z.string().min(1),
15
+ code_challenge: z.string().min(43).max(128),
16
+ code_challenge_method: z.literal("S256"),
17
+ scope: z.string().optional(),
18
+ });
19
+ export async function handleAuthorize(request, reply) {
20
+ const parsed = authorizeQuerySchema.safeParse(request.query);
21
+ if (!parsed.success) {
22
+ return reply.status(400).send({
23
+ error: "invalid_request",
24
+ error_description: parsed.error.issues.map((i) => i.message).join("; "),
25
+ });
26
+ }
27
+ const { client_id, redirect_uri, state, code_challenge, code_challenge_method } = parsed.data;
28
+ // ── Client validation ────────────────────────────────────────────────────
29
+ if (!isKnownClient(client_id, [CLAUDE_CLIENT_ID], (id) => !!findClient(id))) {
30
+ return reply.status(400).send({ error: "invalid_client", error_description: "Unknown client_id" });
31
+ }
32
+ // ── Validate redirect_uri ────────────────────────────────────────────────
33
+ let allowedUris = null;
34
+ // 1. CIMD: client_id is a URL → fetch metadata document
35
+ try {
36
+ const u = new URL(client_id);
37
+ if (u.protocol === "http:" || u.protocol === "https:") {
38
+ const meta = await fetchClientMetadata(client_id);
39
+ if (meta)
40
+ allowedUris = meta.redirect_uris;
41
+ }
42
+ }
43
+ catch { /* not a URL */ }
44
+ // 2. DCR: client registered via /oauth/register
45
+ if (!allowedUris) {
46
+ const dcrClient = findClient(client_id);
47
+ if (dcrClient)
48
+ allowedUris = dcrClient.redirectUris;
49
+ }
50
+ // 3. Pre-registered static client
51
+ if (!allowedUris && client_id === CLAUDE_CLIENT_ID) {
52
+ allowedUris = [CLAUDE_REDIRECT_URI];
53
+ }
54
+ if (!allowedUris || !allowedUris.includes(redirect_uri)) {
55
+ return reply.status(400).send({
56
+ error: "invalid_request",
57
+ error_description: "redirect_uri not registered for this client",
58
+ });
59
+ }
60
+ // ── Store pending session and redirect to Google ─────────────────────────
61
+ const serverBaseUrl = resolveServerBaseUrl(request);
62
+ const sessionId = randomBytes(32).toString("hex");
63
+ addPendingSession(sessionId, {
64
+ clientId: client_id,
65
+ redirectUri: redirect_uri,
66
+ state,
67
+ codeChallenge: code_challenge,
68
+ codeChallengeMethod: code_challenge_method,
69
+ serverBaseUrl,
70
+ });
71
+ log(`OAuth authorize: client=${client_id} session=${sessionId.slice(0, 8)}… → Google (base=${serverBaseUrl})`);
72
+ return reply.redirect(buildGoogleAuthUrl(sessionId, serverBaseUrl), 302);
73
+ }
74
+ //# sourceMappingURL=authorize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../../src/auth/oauth/authorize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Client ID Metadata Document (CIMD) support.
3
+ *
4
+ * When the client_id is a URL, we fetch that URL to retrieve the client's
5
+ * published metadata (redirect_uris, client_name, etc.) and validate the
6
+ * request against it instead of a hardcoded registry.
7
+ *
8
+ * Spec: https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml
9
+ * MCP auth spec §2.3
10
+ */
11
+ export interface ClientMetadata {
12
+ client_id: string;
13
+ redirect_uris: string[];
14
+ client_name?: string;
15
+ token_endpoint_auth_method?: string;
16
+ grant_types?: string[];
17
+ response_types?: string[];
18
+ }
19
+ /**
20
+ * Fetches the CIMD for a given client_id URL.
21
+ * Tries the client_id URL directly first; if that returns non-JSON or fails,
22
+ * tries `{origin}/.well-known/oauth-client-metadata` as a fallback.
23
+ * Returns null if the client_id is not a URL or the document cannot be fetched.
24
+ */
25
+ export declare function fetchClientMetadata(clientId: string): Promise<ClientMetadata | null>;
26
+ /**
27
+ * Returns true if the client_id is:
28
+ * - a URL (CIMD client), OR
29
+ * - in the pre-registered allowlist, OR
30
+ * - registered via DCR (checked via the provided lookup function)
31
+ */
32
+ export declare function isKnownClient(clientId: string, preRegistered: string[], dcrLookup?: (id: string) => boolean): boolean;
33
+ //# sourceMappingURL=cimd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cimd.d.ts","sourceRoot":"","sources":["../../../src/auth/oauth/cimd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAWD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA2B1F;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EAAE,EACvB,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAClC,OAAO,CAET"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Client ID Metadata Document (CIMD) support.
3
+ *
4
+ * When the client_id is a URL, we fetch that URL to retrieve the client's
5
+ * published metadata (redirect_uris, client_name, etc.) and validate the
6
+ * request against it instead of a hardcoded registry.
7
+ *
8
+ * Spec: https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml
9
+ * MCP auth spec §2.3
10
+ */
11
+ function isUrl(value) {
12
+ try {
13
+ const u = new URL(value);
14
+ return u.protocol === "http:" || u.protocol === "https:";
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ /**
21
+ * Fetches the CIMD for a given client_id URL.
22
+ * Tries the client_id URL directly first; if that returns non-JSON or fails,
23
+ * tries `{origin}/.well-known/oauth-client-metadata` as a fallback.
24
+ * Returns null if the client_id is not a URL or the document cannot be fetched.
25
+ */
26
+ export async function fetchClientMetadata(clientId) {
27
+ if (!isUrl(clientId))
28
+ return null;
29
+ const candidates = [
30
+ clientId,
31
+ `${new URL(clientId).origin}/.well-known/oauth-client-metadata`,
32
+ ];
33
+ for (const url of candidates) {
34
+ try {
35
+ const resp = await fetch(url, {
36
+ headers: { Accept: "application/json" },
37
+ signal: AbortSignal.timeout(5000),
38
+ });
39
+ if (!resp.ok)
40
+ continue;
41
+ const ct = resp.headers.get("content-type") ?? "";
42
+ if (!ct.includes("json"))
43
+ continue;
44
+ const doc = (await resp.json());
45
+ if (Array.isArray(doc.redirect_uris))
46
+ return doc;
47
+ }
48
+ catch {
49
+ // try next candidate
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ /**
55
+ * Returns true if the client_id is:
56
+ * - a URL (CIMD client), OR
57
+ * - in the pre-registered allowlist, OR
58
+ * - registered via DCR (checked via the provided lookup function)
59
+ */
60
+ export function isKnownClient(clientId, preRegistered, dcrLookup) {
61
+ return isUrl(clientId) || preRegistered.includes(clientId) || (dcrLookup?.(clientId) ?? false);
62
+ }
63
+ //# sourceMappingURL=cimd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cimd.js","sourceRoot":"","sources":["../../../src/auth/oauth/cimd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAWH,SAAS,KAAK,CAAC,KAAa;IAC1B,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QACzB,OAAO,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IACxD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAElC,MAAM,UAAU,GAAG;QACjB,QAAQ;QACR,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,oCAAoC;KAChE,CAAC;IAEF,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC5B,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;gBACvC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC;aAClC,CAAC,CAAC;YACH,IAAI,CAAC,IAAI,CAAC,EAAE;gBAAE,SAAS;YAEvB,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;YAClD,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAAE,SAAS;YAEnC,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAmB,CAAC;YAClD,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;gBAAE,OAAO,GAAG,CAAC;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,qBAAqB;QACvB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAgB,EAChB,aAAuB,EACvB,SAAmC;IAEnC,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,aAAa,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,CAAC;AACjG,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { PendingAuthSession } from "./types.js";
2
+ export declare function addPendingSession(sessionId: string, session: Omit<PendingAuthSession, "expiresAt">): void;
3
+ /** Returns the session and removes it (single-use). Returns null if missing or expired. */
4
+ export declare function consumePendingSession(sessionId: string): PendingAuthSession | null;
5
+ //# sourceMappingURL=pending-sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-sessions.d.ts","sourceRoot":"","sources":["../../../src/auth/oauth/pending-sessions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAarD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG,IAAI,CAEzG;AAED,2FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAMlF"}
@@ -0,0 +1,24 @@
1
+ import { PENDING_SESSION_TTL_MS } from "../../config/oauth.js";
2
+ const pendingSessions = new Map();
3
+ // Purge expired sessions every 5 minutes
4
+ setInterval(() => {
5
+ const now = Date.now();
6
+ for (const [id, session] of pendingSessions) {
7
+ if (session.expiresAt < now)
8
+ pendingSessions.delete(id);
9
+ }
10
+ }, 5 * 60 * 1000).unref();
11
+ export function addPendingSession(sessionId, session) {
12
+ pendingSessions.set(sessionId, { ...session, expiresAt: Date.now() + PENDING_SESSION_TTL_MS });
13
+ }
14
+ /** Returns the session and removes it (single-use). Returns null if missing or expired. */
15
+ export function consumePendingSession(sessionId) {
16
+ const session = pendingSessions.get(sessionId);
17
+ if (!session)
18
+ return null;
19
+ pendingSessions.delete(sessionId);
20
+ if (session.expiresAt < Date.now())
21
+ return null;
22
+ return session;
23
+ }
24
+ //# sourceMappingURL=pending-sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-sessions.js","sourceRoot":"","sources":["../../../src/auth/oauth/pending-sessions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,eAAe,GAAG,IAAI,GAAG,EAA8B,CAAC;AAE9D,yCAAyC;AACzC,WAAW,CAAC,GAAG,EAAE;IACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,KAAK,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,eAAe,EAAE,CAAC;QAC5C,IAAI,OAAO,CAAC,SAAS,GAAG,GAAG;YAAE,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;AAE1B,MAAM,UAAU,iBAAiB,CAAC,SAAiB,EAAE,OAA8C;IACjG,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,sBAAsB,EAAE,CAAC,CAAC;AACjG,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,qBAAqB,CAAC,SAAiB;IACrD,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,eAAe,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAClC,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,IAAI,CAAC;IAChD,OAAO,OAAO,CAAC;AACjB,CAAC"}