@grackle-ai/auth 0.72.3

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 (47) hide show
  1. package/README.md +26 -0
  2. package/dist/api-key.d.ts +12 -0
  3. package/dist/api-key.d.ts.map +1 -0
  4. package/dist/api-key.js +65 -0
  5. package/dist/api-key.js.map +1 -0
  6. package/dist/auth-context.d.ts +20 -0
  7. package/dist/auth-context.d.ts.map +1 -0
  8. package/dist/auth-context.js +2 -0
  9. package/dist/auth-context.js.map +1 -0
  10. package/dist/auth-logger.d.ts +12 -0
  11. package/dist/auth-logger.d.ts.map +1 -0
  12. package/dist/auth-logger.js +24 -0
  13. package/dist/auth-logger.js.map +1 -0
  14. package/dist/auth-middleware.d.ts +16 -0
  15. package/dist/auth-middleware.d.ts.map +1 -0
  16. package/dist/auth-middleware.js +93 -0
  17. package/dist/auth-middleware.js.map +1 -0
  18. package/dist/index.d.ts +15 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +15 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/oauth-token.d.ts +38 -0
  23. package/dist/oauth-token.d.ts.map +1 -0
  24. package/dist/oauth-token.js +101 -0
  25. package/dist/oauth-token.js.map +1 -0
  26. package/dist/oauth.d.ts +102 -0
  27. package/dist/oauth.d.ts.map +1 -0
  28. package/dist/oauth.js +225 -0
  29. package/dist/oauth.js.map +1 -0
  30. package/dist/pairing.d.ts +22 -0
  31. package/dist/pairing.d.ts.map +1 -0
  32. package/dist/pairing.js +136 -0
  33. package/dist/pairing.js.map +1 -0
  34. package/dist/scoped-token.d.ts +55 -0
  35. package/dist/scoped-token.d.ts.map +1 -0
  36. package/dist/scoped-token.js +131 -0
  37. package/dist/scoped-token.js.map +1 -0
  38. package/dist/security-headers.d.ts +17 -0
  39. package/dist/security-headers.d.ts.map +1 -0
  40. package/dist/security-headers.js +31 -0
  41. package/dist/security-headers.js.map +1 -0
  42. package/dist/session.d.ts +36 -0
  43. package/dist/session.d.ts.map +1 -0
  44. package/dist/session.js +143 -0
  45. package/dist/session.js.map +1 -0
  46. package/dist/tsdoc-metadata.json +11 -0
  47. package/package.json +43 -0
package/dist/oauth.js ADDED
@@ -0,0 +1,225 @@
1
+ import { randomUUID, randomBytes, createHash } from "node:crypto";
2
+ import { getAuthLogger } from "./auth-logger.js";
3
+ /** Time-to-live for authorization codes: 30 seconds. */
4
+ const AUTH_CODE_TTL_MS = 30 * 1000;
5
+ /** Time-to-live for refresh tokens: 30 days. */
6
+ const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000;
7
+ /** Interval at which expired OAuth state is cleaned up. */
8
+ const CLEANUP_INTERVAL_MS = 60 * 1000;
9
+ /** Byte length of generated tokens (authorization codes, refresh tokens). */
10
+ const TOKEN_BYTE_LENGTH = 32;
11
+ /** Maximum number of registered OAuth clients to prevent unbounded growth. */
12
+ const MAX_CLIENTS = 100;
13
+ /** Time-to-live for client registrations: 7 days. */
14
+ const CLIENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
15
+ /** Registered OAuth clients keyed by client ID. */
16
+ const clients = new Map();
17
+ /**
18
+ * Register a new OAuth client with dynamic client registration.
19
+ *
20
+ * @param redirectUris - List of allowed redirect URIs for this client.
21
+ * @param clientName - Human-readable name for the client (optional).
22
+ * @returns The newly registered client record.
23
+ */
24
+ export function registerClient(redirectUris, clientName) {
25
+ // Evict expired clients before checking capacity
26
+ const now = Date.now();
27
+ for (const [id, rec] of clients) {
28
+ if (now - rec.createdAt > CLIENT_TTL_MS) {
29
+ clients.delete(id);
30
+ }
31
+ }
32
+ if (clients.size >= MAX_CLIENTS) {
33
+ getAuthLogger().warn({}, "Maximum registered OAuth clients reached (%d)");
34
+ return undefined;
35
+ }
36
+ const clientId = randomUUID();
37
+ const record = {
38
+ clientId,
39
+ redirectUris,
40
+ clientName: clientName || "Unknown Client",
41
+ createdAt: now,
42
+ };
43
+ clients.set(clientId, record);
44
+ getAuthLogger().info({ clientId, clientName: record.clientName }, "OAuth client registered");
45
+ return record;
46
+ }
47
+ /**
48
+ * Look up a registered client by ID.
49
+ *
50
+ * @param clientId - The client ID to look up.
51
+ * @returns The client record, or undefined if not found.
52
+ */
53
+ export function getClient(clientId) {
54
+ return clients.get(clientId);
55
+ }
56
+ /** Active authorization codes keyed by code string. */
57
+ const authCodes = new Map();
58
+ /**
59
+ * Create a single-use authorization code bound to the given parameters.
60
+ *
61
+ * @param clientId - The client that requested authorization.
62
+ * @param redirectUri - The redirect URI for this authorization.
63
+ * @param codeChallenge - PKCE S256 code challenge.
64
+ * @param resource - The resource URL the client wants to access.
65
+ * @returns The generated authorization code string.
66
+ */
67
+ export function createAuthorizationCode(clientId, redirectUri, codeChallenge, resource) {
68
+ const code = randomBytes(TOKEN_BYTE_LENGTH).toString("base64url");
69
+ const now = Date.now();
70
+ const record = {
71
+ code,
72
+ clientId,
73
+ redirectUri,
74
+ codeChallenge,
75
+ resource,
76
+ createdAt: now,
77
+ expiresAt: now + AUTH_CODE_TTL_MS,
78
+ };
79
+ authCodes.set(code, record);
80
+ getAuthLogger().info({ clientId }, "Authorization code created");
81
+ return code;
82
+ }
83
+ /**
84
+ * Compute the S256 code challenge from a code verifier.
85
+ *
86
+ * @param codeVerifier - The PKCE code verifier string.
87
+ * @returns The base64url-encoded SHA-256 hash.
88
+ */
89
+ export function computeCodeChallenge(codeVerifier) {
90
+ return createHash("sha256").update(codeVerifier).digest("base64url");
91
+ }
92
+ /**
93
+ * Verify that a code verifier matches a code challenge using S256.
94
+ *
95
+ * @param codeVerifier - The PKCE code verifier from the token request.
96
+ * @param codeChallenge - The code challenge stored at authorization time.
97
+ * @returns True if the verifier matches the challenge.
98
+ */
99
+ export function verifyCodeChallenge(codeVerifier, codeChallenge) {
100
+ const computed = computeCodeChallenge(codeVerifier);
101
+ return computed === codeChallenge;
102
+ }
103
+ /**
104
+ * Consume an authorization code, validating all parameters.
105
+ *
106
+ * The code is deleted regardless of whether validation succeeds (single-use).
107
+ *
108
+ * @param code - The authorization code to consume.
109
+ * @param clientId - The client ID making the token request.
110
+ * @param redirectUri - The redirect URI from the token request.
111
+ * @param codeVerifier - The PKCE code verifier.
112
+ * @param resource - The resource URL from the token request.
113
+ * @returns The auth code record if valid, or undefined.
114
+ */
115
+ export function consumeAuthorizationCode(code, clientId, redirectUri, codeVerifier, resource) {
116
+ const record = authCodes.get(code);
117
+ // Always delete to prevent replay — even if validation fails
118
+ authCodes.delete(code);
119
+ if (!record) {
120
+ return undefined;
121
+ }
122
+ const now = Date.now();
123
+ if (now > record.expiresAt) {
124
+ return undefined;
125
+ }
126
+ if (record.clientId !== clientId) {
127
+ return undefined;
128
+ }
129
+ if (record.redirectUri !== redirectUri) {
130
+ return undefined;
131
+ }
132
+ if (record.resource !== resource) {
133
+ return undefined;
134
+ }
135
+ if (!verifyCodeChallenge(codeVerifier, record.codeChallenge)) {
136
+ return undefined;
137
+ }
138
+ return record;
139
+ }
140
+ /** Active refresh tokens keyed by token string. */
141
+ const refreshTokens = new Map();
142
+ /**
143
+ * Create a new refresh token for the given client and resource.
144
+ *
145
+ * @param clientId - The client this refresh token is issued to.
146
+ * @param resource - The resource URL this refresh token is scoped to.
147
+ * @returns The generated refresh token string.
148
+ */
149
+ export function createRefreshToken(clientId, resource) {
150
+ const token = randomBytes(TOKEN_BYTE_LENGTH).toString("base64url");
151
+ const now = Date.now();
152
+ refreshTokens.set(token, {
153
+ token,
154
+ clientId,
155
+ resource,
156
+ createdAt: now,
157
+ expiresAt: now + REFRESH_TOKEN_TTL_MS,
158
+ });
159
+ return token;
160
+ }
161
+ /**
162
+ * Consume a refresh token with rotation — the old token is invalidated and
163
+ * a new one must be issued in its place.
164
+ *
165
+ * @param token - The refresh token to consume.
166
+ * @param clientId - The client ID making the refresh request.
167
+ * @returns The refresh token record if valid, or undefined.
168
+ */
169
+ export function consumeRefreshToken(token, clientId) {
170
+ const record = refreshTokens.get(token);
171
+ // Always delete to enforce rotation
172
+ refreshTokens.delete(token);
173
+ if (!record) {
174
+ return undefined;
175
+ }
176
+ const now = Date.now();
177
+ if (now > record.expiresAt) {
178
+ return undefined;
179
+ }
180
+ if (record.clientId !== clientId) {
181
+ return undefined;
182
+ }
183
+ return record;
184
+ }
185
+ // ─── Cleanup ──────────────────────────────────────────────────────────
186
+ let cleanupTimer;
187
+ /** Start the periodic OAuth state cleanup timer. Call once on server startup. */
188
+ export function startOAuthCleanup() {
189
+ if (cleanupTimer) {
190
+ return;
191
+ }
192
+ cleanupTimer = setInterval(() => {
193
+ const now = Date.now();
194
+ for (const [id, record] of clients) {
195
+ if (now - record.createdAt > CLIENT_TTL_MS) {
196
+ clients.delete(id);
197
+ }
198
+ }
199
+ for (const [code, record] of authCodes) {
200
+ if (now > record.expiresAt) {
201
+ authCodes.delete(code);
202
+ }
203
+ }
204
+ for (const [token, record] of refreshTokens) {
205
+ if (now > record.expiresAt) {
206
+ refreshTokens.delete(token);
207
+ }
208
+ }
209
+ }, CLEANUP_INTERVAL_MS);
210
+ cleanupTimer.unref();
211
+ }
212
+ /** Stop the periodic OAuth state cleanup timer. */
213
+ export function stopOAuthCleanup() {
214
+ if (cleanupTimer) {
215
+ clearInterval(cleanupTimer);
216
+ cleanupTimer = undefined;
217
+ }
218
+ }
219
+ /** Clear all OAuth state. Intended for testing only. */
220
+ export function clearOAuthState() {
221
+ clients.clear();
222
+ authCodes.clear();
223
+ refreshTokens.clear();
224
+ }
225
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /** Start the periodic cleanup timer. Call once on server startup. */
2
+ export declare function startPairingCleanup(): void;
3
+ /** Stop the periodic cleanup timer. */
4
+ export declare function stopPairingCleanup(): void;
5
+ /**
6
+ * Generate a new pairing code.
7
+ *
8
+ * Returns the 6-character uppercase alphanumeric code, or undefined
9
+ * if the maximum number of active codes has been reached.
10
+ */
11
+ export declare function generatePairingCode(): string | undefined;
12
+ /**
13
+ * Attempt to redeem a pairing code. Returns true if the code was valid
14
+ * and has been consumed (burned). Returns false otherwise.
15
+ *
16
+ * @param code - The pairing code to redeem (case-insensitive).
17
+ * @param remoteIp - The remote IP address for rate limiting.
18
+ */
19
+ export declare function redeemPairingCode(code: string, remoteIp: string): boolean;
20
+ /** Clear all pairing codes and rate limits (for testing). */
21
+ export declare function clearPairing(): void;
22
+ //# sourceMappingURL=pairing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"AA4CA,qEAAqE;AACrE,wBAAgB,mBAAmB,IAAI,IAAI,CAmB1C;AAED,uCAAuC;AACvC,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAgCxD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAuCzE;AAED,6DAA6D;AAC7D,wBAAgB,YAAY,IAAI,IAAI,CAGnC"}
@@ -0,0 +1,136 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { getAuthLogger } from "./auth-logger.js";
3
+ /** How long a pairing code is valid after generation. */
4
+ const PAIRING_CODE_TTL_MS = 5 * 60 * 1000;
5
+ /** Maximum number of active (unexpired) pairing codes. */
6
+ const MAX_ACTIVE_CODES = 10;
7
+ /** Number of characters in a pairing code. */
8
+ const PAIRING_CODE_LENGTH = 6;
9
+ /** Maximum failed redemption attempts per IP before blocking. */
10
+ const MAX_FAILED_ATTEMPTS = 5;
11
+ /** Duration to block an IP after exceeding failed attempts. */
12
+ const RATE_LIMIT_WINDOW_MS = 60 * 1000;
13
+ /** Duration to block an IP after exceeding the rate limit. */
14
+ const RATE_LIMIT_BLOCK_MS = 5 * 60 * 1000;
15
+ /** Interval at which expired codes and rate-limit entries are cleaned up. */
16
+ const CLEANUP_INTERVAL_MS = 60 * 1000;
17
+ /** Active pairing codes keyed by code string (uppercase). */
18
+ const activeCodes = new Map();
19
+ /** Rate-limit tracking keyed by remote IP. */
20
+ const rateLimits = new Map();
21
+ let cleanupTimer;
22
+ /** Start the periodic cleanup timer. Call once on server startup. */
23
+ export function startPairingCleanup() {
24
+ if (cleanupTimer) {
25
+ return;
26
+ }
27
+ cleanupTimer = setInterval(() => {
28
+ const now = Date.now();
29
+ for (const [code, record] of activeCodes) {
30
+ if (now > record.expiresAt) {
31
+ activeCodes.delete(code);
32
+ }
33
+ }
34
+ for (const [ip, record] of rateLimits) {
35
+ if (now > record.blockedUntil && now - record.firstAttempt > RATE_LIMIT_WINDOW_MS) {
36
+ rateLimits.delete(ip);
37
+ }
38
+ }
39
+ }, CLEANUP_INTERVAL_MS);
40
+ // Allow the process to exit even if the timer is still running
41
+ cleanupTimer.unref();
42
+ }
43
+ /** Stop the periodic cleanup timer. */
44
+ export function stopPairingCleanup() {
45
+ if (cleanupTimer) {
46
+ clearInterval(cleanupTimer);
47
+ cleanupTimer = undefined;
48
+ }
49
+ }
50
+ /**
51
+ * Generate a new pairing code.
52
+ *
53
+ * Returns the 6-character uppercase alphanumeric code, or undefined
54
+ * if the maximum number of active codes has been reached.
55
+ */
56
+ export function generatePairingCode() {
57
+ // Purge expired codes first
58
+ const now = Date.now();
59
+ for (const [code, record] of activeCodes) {
60
+ if (now > record.expiresAt) {
61
+ activeCodes.delete(code);
62
+ }
63
+ }
64
+ if (activeCodes.size >= MAX_ACTIVE_CODES) {
65
+ getAuthLogger().warn({}, "Maximum active pairing codes reached (%d)");
66
+ return undefined;
67
+ }
68
+ // Generate a code that doesn't collide with existing ones
69
+ let code;
70
+ do {
71
+ code = randomBytes(4)
72
+ .toString("base64url")
73
+ .replace(/[^A-Za-z0-9]/g, "")
74
+ .slice(0, PAIRING_CODE_LENGTH)
75
+ .toUpperCase();
76
+ } while (code.length < PAIRING_CODE_LENGTH || activeCodes.has(code));
77
+ const record = {
78
+ code,
79
+ createdAt: now,
80
+ expiresAt: now + PAIRING_CODE_TTL_MS,
81
+ };
82
+ activeCodes.set(code, record);
83
+ getAuthLogger().info({ expiresIn: PAIRING_CODE_TTL_MS / 1000 }, "Generated pairing code");
84
+ return code;
85
+ }
86
+ /**
87
+ * Attempt to redeem a pairing code. Returns true if the code was valid
88
+ * and has been consumed (burned). Returns false otherwise.
89
+ *
90
+ * @param code - The pairing code to redeem (case-insensitive).
91
+ * @param remoteIp - The remote IP address for rate limiting.
92
+ */
93
+ export function redeemPairingCode(code, remoteIp) {
94
+ const now = Date.now();
95
+ const normalised = code.toUpperCase().trim();
96
+ // Check rate limit
97
+ const limit = rateLimits.get(remoteIp);
98
+ if (limit && now < limit.blockedUntil) {
99
+ getAuthLogger().warn({ remoteIp }, "Pairing attempt blocked by rate limit");
100
+ return false;
101
+ }
102
+ const record = activeCodes.get(normalised);
103
+ if (!record || now > record.expiresAt) {
104
+ // Record failed attempt
105
+ if (limit) {
106
+ if (now - limit.firstAttempt > RATE_LIMIT_WINDOW_MS) {
107
+ // Reset window
108
+ rateLimits.set(remoteIp, { attempts: 1, firstAttempt: now, blockedUntil: 0 });
109
+ }
110
+ else {
111
+ limit.attempts++;
112
+ if (limit.attempts >= MAX_FAILED_ATTEMPTS) {
113
+ limit.blockedUntil = now + RATE_LIMIT_BLOCK_MS;
114
+ getAuthLogger().warn({ remoteIp, attempts: limit.attempts }, "Rate limit triggered for pairing attempts");
115
+ }
116
+ }
117
+ }
118
+ else {
119
+ rateLimits.set(remoteIp, { attempts: 1, firstAttempt: now, blockedUntil: 0 });
120
+ }
121
+ if (record && now > record.expiresAt) {
122
+ activeCodes.delete(normalised);
123
+ }
124
+ return false;
125
+ }
126
+ // Burn the code — single use
127
+ activeCodes.delete(normalised);
128
+ getAuthLogger().info({}, "Pairing code redeemed");
129
+ return true;
130
+ }
131
+ /** Clear all pairing codes and rate limits (for testing). */
132
+ export function clearPairing() {
133
+ activeCodes.clear();
134
+ rateLimits.clear();
135
+ }
136
+ //# sourceMappingURL=pairing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.js","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,yDAAyD;AACzD,MAAM,mBAAmB,GAAW,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAElD,0DAA0D;AAC1D,MAAM,gBAAgB,GAAW,EAAE,CAAC;AAEpC,8CAA8C;AAC9C,MAAM,mBAAmB,GAAW,CAAC,CAAC;AAEtC,iEAAiE;AACjE,MAAM,mBAAmB,GAAW,CAAC,CAAC;AAEtC,+DAA+D;AAC/D,MAAM,oBAAoB,GAAW,EAAE,GAAG,IAAI,CAAC;AAE/C,8DAA8D;AAC9D,MAAM,mBAAmB,GAAW,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAElD,6EAA6E;AAC7E,MAAM,mBAAmB,GAAW,EAAE,GAAG,IAAI,CAAC;AAc9C,6DAA6D;AAC7D,MAAM,WAAW,GAA+B,IAAI,GAAG,EAAyB,CAAC;AAEjF,8CAA8C;AAC9C,MAAM,UAAU,GAAiC,IAAI,GAAG,EAA2B,CAAC;AAEpF,IAAI,YAAwD,CAAC;AAE7D,qEAAqE;AACrE,MAAM,UAAU,mBAAmB;IACjC,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO;IACT,CAAC;IACD,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;YACzC,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;gBAC3B,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;QACD,KAAK,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YACtC,IAAI,GAAG,GAAG,MAAM,CAAC,YAAY,IAAI,GAAG,GAAG,MAAM,CAAC,YAAY,GAAG,oBAAoB,EAAE,CAAC;gBAClF,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC,EAAE,mBAAmB,CAAC,CAAC;IACxB,+DAA+D;IAC/D,YAAY,CAAC,KAAK,EAAE,CAAC;AACvB,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,kBAAkB;IAChC,IAAI,YAAY,EAAE,CAAC;QACjB,aAAa,CAAC,YAAY,CAAC,CAAC;QAC5B,YAAY,GAAG,SAAS,CAAC;IAC3B,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB;IACjC,4BAA4B;IAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;QACzC,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;YAC3B,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,IAAI,WAAW,CAAC,IAAI,IAAI,gBAAgB,EAAE,CAAC;QACzC,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,2CAA2C,CAAC,CAAC;QACtE,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,0DAA0D;IAC1D,IAAI,IAAY,CAAC;IACjB,GAAG,CAAC;QACF,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC;aAClB,QAAQ,CAAC,WAAW,CAAC;aACrB,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;aAC5B,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC;aAC7B,WAAW,EAAE,CAAC;IACnB,CAAC,QAAQ,IAAI,CAAC,MAAM,GAAG,mBAAmB,IAAI,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;IAErE,MAAM,MAAM,GAAkB;QAC5B,IAAI;QACJ,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,GAAG,GAAG,mBAAmB;KACrC,CAAC;IACF,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC9B,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,mBAAmB,GAAG,IAAI,EAAE,EAAE,wBAAwB,CAAC,CAAC;IAC1F,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,QAAgB;IAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IAE7C,mBAAmB;IACnB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC,YAAY,EAAE,CAAC;QACtC,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,uCAAuC,CAAC,CAAC;QAC5E,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;QACtC,wBAAwB;QACxB,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,GAAG,GAAG,KAAK,CAAC,YAAY,GAAG,oBAAoB,EAAE,CAAC;gBACpD,eAAe;gBACf,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;YAChF,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACjB,IAAI,KAAK,CAAC,QAAQ,IAAI,mBAAmB,EAAE,CAAC;oBAC1C,KAAK,CAAC,YAAY,GAAG,GAAG,GAAG,mBAAmB,CAAC;oBAC/C,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,EAAE,2CAA2C,CAAC,CAAC;gBAC5G,CAAC;YACH,CAAC;QACH,CAAC;aAAM,CAAC;YACN,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;QAChF,CAAC;QAED,IAAI,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC;YACrC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,6BAA6B;IAC7B,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC/B,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,uBAAuB,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,YAAY;IAC1B,WAAW,CAAC,KAAK,EAAE,CAAC;IACpB,UAAU,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC"}
@@ -0,0 +1,55 @@
1
+ /** Claims embedded in a scoped token payload. */
2
+ export interface ScopedTokenClaims {
3
+ /** Task ID (subject). */
4
+ sub: string;
5
+ /** Workspace ID. */
6
+ pid: string;
7
+ /** Persona ID. */
8
+ per: string;
9
+ /** Session ID. */
10
+ sid: string;
11
+ /** Issued-at time (epoch seconds). */
12
+ iat: number;
13
+ /** Expiry time (epoch seconds). */
14
+ exp: number;
15
+ }
16
+ /**
17
+ * Create a scoped token with the given claims, signed with the provided secret.
18
+ *
19
+ * @param claims - Token claims (sub, pid, per, sid). iat/exp are set automatically.
20
+ * @param signingSecret - Secret used to HMAC-sign the token (typically the API key).
21
+ * @param ttlMs - Token time-to-live in milliseconds (default: 24 hours).
22
+ * @returns The signed opaque token string.
23
+ */
24
+ export declare function createScopedToken(claims: Pick<ScopedTokenClaims, "sub" | "pid" | "per" | "sid">, signingSecret: string, ttlMs?: number): string;
25
+ /**
26
+ * Verify a scoped token's signature and expiry.
27
+ *
28
+ * Uses constant-time comparison for the HMAC signature.
29
+ *
30
+ * @param token - The token string to verify.
31
+ * @param signingSecret - The secret used to verify the HMAC signature.
32
+ * @returns The decoded claims if valid, or `undefined` if verification fails.
33
+ */
34
+ export declare function verifyScopedToken(token: string, signingSecret: string): ScopedTokenClaims | undefined;
35
+ /**
36
+ * Revoke all tokens for a given task ID.
37
+ * Revoked tokens are rejected by `authenticateMcpRequest` even if not yet expired.
38
+ */
39
+ export declare function revokeTask(taskId: string): void;
40
+ /** Check whether a task ID has been revoked. */
41
+ export declare function isRevokedTask(taskId: string): boolean;
42
+ /**
43
+ * Remove revocation entries older than the given TTL.
44
+ * Since expired tokens are already rejected by the `exp` check,
45
+ * revocation entries only need to live as long as the token TTL.
46
+ *
47
+ * @param ttlMs - Maximum age of revocation entries in milliseconds (default: 24 hours).
48
+ */
49
+ export declare function pruneRevocations(ttlMs?: number): void;
50
+ /**
51
+ * Clear all revocation entries. Intended for testing only.
52
+ * @internal
53
+ */
54
+ export declare function clearRevocations(): void;
55
+ //# sourceMappingURL=scoped-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoped-token.d.ts","sourceRoot":"","sources":["../src/scoped-token.ts"],"names":[],"mappings":"AAKA,iDAAiD;AACjD,MAAM,WAAW,iBAAiB;IAChC,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,kBAAkB;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,kBAAkB;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;CACb;AAoBD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,EAC9D,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,MAAuB,GAC7B,MAAM,CAWR;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CA2DrG;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,MAAuB,GAAG,IAAI,CAOrE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
@@ -0,0 +1,131 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ /** Default token time-to-live: 24 hours in milliseconds. */
3
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
4
+ /** In-memory revocation set: taskId → revocation timestamp (epoch ms). */
5
+ const revokedTasks = new Map();
6
+ /** Encode a buffer as base64url (no padding). */
7
+ function toBase64Url(buf) {
8
+ return buf.toString("base64url");
9
+ }
10
+ /** Decode a base64url string to a Buffer. */
11
+ function fromBase64Url(str) {
12
+ return Buffer.from(str, "base64url");
13
+ }
14
+ /** Compute HMAC-SHA256 signature over a payload string. */
15
+ function sign(payload, secret) {
16
+ return createHmac("sha256", secret).update(payload).digest();
17
+ }
18
+ /**
19
+ * Create a scoped token with the given claims, signed with the provided secret.
20
+ *
21
+ * @param claims - Token claims (sub, pid, per, sid). iat/exp are set automatically.
22
+ * @param signingSecret - Secret used to HMAC-sign the token (typically the API key).
23
+ * @param ttlMs - Token time-to-live in milliseconds (default: 24 hours).
24
+ * @returns The signed opaque token string.
25
+ */
26
+ export function createScopedToken(claims, signingSecret, ttlMs = DEFAULT_TTL_MS) {
27
+ const now = Math.floor(Date.now() / 1000);
28
+ const payload = {
29
+ ...claims,
30
+ iat: now,
31
+ exp: now + Math.floor(ttlMs / 1000),
32
+ };
33
+ const payloadStr = JSON.stringify(payload);
34
+ const payloadEncoded = toBase64Url(Buffer.from(payloadStr, "utf8"));
35
+ const signature = toBase64Url(sign(payloadEncoded, signingSecret));
36
+ return `${payloadEncoded}.${signature}`;
37
+ }
38
+ /**
39
+ * Verify a scoped token's signature and expiry.
40
+ *
41
+ * Uses constant-time comparison for the HMAC signature.
42
+ *
43
+ * @param token - The token string to verify.
44
+ * @param signingSecret - The secret used to verify the HMAC signature.
45
+ * @returns The decoded claims if valid, or `undefined` if verification fails.
46
+ */
47
+ export function verifyScopedToken(token, signingSecret) {
48
+ const dotIndex = token.indexOf(".");
49
+ if (dotIndex === -1 || dotIndex === 0 || dotIndex === token.length - 1) {
50
+ return undefined;
51
+ }
52
+ // Reject tokens with multiple dots
53
+ if (token.indexOf(".", dotIndex + 1) !== -1) {
54
+ return undefined;
55
+ }
56
+ const payloadEncoded = token.slice(0, dotIndex);
57
+ const signatureEncoded = token.slice(dotIndex + 1);
58
+ // Verify signature using constant-time comparison
59
+ const expectedSignature = sign(payloadEncoded, signingSecret);
60
+ let actualSignature;
61
+ try {
62
+ actualSignature = fromBase64Url(signatureEncoded);
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ if (expectedSignature.length !== actualSignature.length) {
68
+ return undefined;
69
+ }
70
+ if (!timingSafeEqual(expectedSignature, actualSignature)) {
71
+ return undefined;
72
+ }
73
+ // Decode and parse payload
74
+ let claims;
75
+ try {
76
+ const payloadStr = fromBase64Url(payloadEncoded).toString("utf8");
77
+ claims = JSON.parse(payloadStr);
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ // Validate claim types to prevent bypass via crafted payloads
83
+ if (typeof claims.sub !== "string" ||
84
+ typeof claims.pid !== "string" ||
85
+ typeof claims.per !== "string" ||
86
+ typeof claims.sid !== "string" ||
87
+ !Number.isFinite(claims.iat) ||
88
+ !Number.isFinite(claims.exp)) {
89
+ return undefined;
90
+ }
91
+ // Check expiry (exp must be strictly greater than both iat and now)
92
+ const now = Math.floor(Date.now() / 1000);
93
+ if (claims.exp <= now || claims.exp <= claims.iat) {
94
+ return undefined;
95
+ }
96
+ return claims;
97
+ }
98
+ /**
99
+ * Revoke all tokens for a given task ID.
100
+ * Revoked tokens are rejected by `authenticateMcpRequest` even if not yet expired.
101
+ */
102
+ export function revokeTask(taskId) {
103
+ revokedTasks.set(taskId, Date.now());
104
+ }
105
+ /** Check whether a task ID has been revoked. */
106
+ export function isRevokedTask(taskId) {
107
+ return revokedTasks.has(taskId);
108
+ }
109
+ /**
110
+ * Remove revocation entries older than the given TTL.
111
+ * Since expired tokens are already rejected by the `exp` check,
112
+ * revocation entries only need to live as long as the token TTL.
113
+ *
114
+ * @param ttlMs - Maximum age of revocation entries in milliseconds (default: 24 hours).
115
+ */
116
+ export function pruneRevocations(ttlMs = DEFAULT_TTL_MS) {
117
+ const cutoff = Date.now() - ttlMs;
118
+ for (const [taskId, revokedAt] of revokedTasks) {
119
+ if (revokedAt < cutoff) {
120
+ revokedTasks.delete(taskId);
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Clear all revocation entries. Intended for testing only.
126
+ * @internal
127
+ */
128
+ export function clearRevocations() {
129
+ revokedTasks.clear();
130
+ }
131
+ //# sourceMappingURL=scoped-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoped-token.js","sourceRoot":"","sources":["../src/scoped-token.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { ServerResponse } from "node:http";
2
+ /**
3
+ * Content Security Policy for the web handler.
4
+ *
5
+ * Covers the React SPA (served from 'self') and server-rendered pages
6
+ * (pairing/authorize) which use inline styles.
7
+ */
8
+ export declare const WEB_CONTENT_SECURITY_POLICY: string;
9
+ /**
10
+ * Set defense-in-depth security headers on every web response.
11
+ *
12
+ * Called at the top of `createWebHandler`'s returned function so that all
13
+ * response paths (static files, HTML pages, JSON APIs, redirects) are covered
14
+ * without modifying each `writeHead` call individually.
15
+ */
16
+ export declare function setSecurityHeaders(res: ServerResponse): void;
17
+ //# sourceMappingURL=security-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,EAAE,MAW9B,CAAC;AAEb;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAI5D"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Content Security Policy for the web handler.
3
+ *
4
+ * Covers the React SPA (served from 'self') and server-rendered pages
5
+ * (pairing/authorize) which use inline styles.
6
+ */
7
+ export const WEB_CONTENT_SECURITY_POLICY = [
8
+ "default-src 'self'",
9
+ "script-src 'self'",
10
+ "style-src 'self' 'unsafe-inline'",
11
+ "img-src 'self' data:",
12
+ "font-src 'self'",
13
+ "connect-src 'self'",
14
+ "object-src 'none'",
15
+ "form-action 'self'",
16
+ "frame-ancestors 'none'",
17
+ "base-uri 'self'",
18
+ ].join("; ");
19
+ /**
20
+ * Set defense-in-depth security headers on every web response.
21
+ *
22
+ * Called at the top of `createWebHandler`'s returned function so that all
23
+ * response paths (static files, HTML pages, JSON APIs, redirects) are covered
24
+ * without modifying each `writeHead` call individually.
25
+ */
26
+ export function setSecurityHeaders(res) {
27
+ res.setHeader("X-Content-Type-Options", "nosniff");
28
+ res.setHeader("X-Frame-Options", "DENY");
29
+ res.setHeader("Content-Security-Policy", WEB_CONTENT_SECURITY_POLICY);
30
+ }
31
+ //# sourceMappingURL=security-headers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAW;IACjD,oBAAoB;IACpB,mBAAmB;IACnB,kCAAkC;IAClC,sBAAsB;IACtB,iBAAiB;IACjB,oBAAoB;IACpB,mBAAmB;IACnB,oBAAoB;IACpB,wBAAwB;IACxB,iBAAiB;CAClB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAmB;IACpD,GAAG,CAAC,SAAS,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;IACnD,GAAG,CAAC,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACzC,GAAG,CAAC,SAAS,CAAC,yBAAyB,EAAE,2BAA2B,CAAC,CAAC;AACxE,CAAC"}