@silkweave/auth 1.3.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.
@@ -0,0 +1,444 @@
1
+ import { randomBytes, randomUUID } from 'crypto'
2
+ import { SignJWT, jwtVerify } from 'jose'
3
+ import { createMemoryStore } from '../store/memory-store.js'
4
+ import { AuthInfo } from '../types.js'
5
+ import { OAuthStore } from './store.js'
6
+ import { OAuthProvider, OAuthResponse } from './types.js'
7
+ import { matchRedirectUri } from './uri.js'
8
+
9
+ export interface OAuthProxyConfig {
10
+ authorizeUrl: string
11
+ tokenUrl: string
12
+ userinfoUrl?: string
13
+ clientId: string
14
+ clientSecret: string
15
+ resourceUrl: string
16
+ redirectUris: string[]
17
+ requiredScopes?: string[]
18
+ callbackPath?: string
19
+ signingKey?: string
20
+ tokenTtl?: number
21
+ store?: OAuthStore
22
+ }
23
+
24
+ const CORS_HEADERS: Record<string, string> = {
25
+ 'Access-Control-Allow-Origin': '*',
26
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
27
+ 'Access-Control-Allow-Headers': '*'
28
+ }
29
+
30
+ export function createOAuthProxy(config: OAuthProxyConfig): OAuthProvider {
31
+ const store = config.store ?? createMemoryStore()
32
+ const callbackPath = config.callbackPath ?? '/auth/callback'
33
+ const tokenTtl = config.tokenTtl ?? 3600
34
+ const scopes = config.requiredScopes ?? []
35
+
36
+ let signingKey: Uint8Array | null = null
37
+
38
+ async function getSigningKey(): Promise<Uint8Array> {
39
+ if (signingKey) { return signingKey }
40
+ if (config.signingKey) {
41
+ signingKey = new TextEncoder().encode(config.signingKey)
42
+ } else {
43
+ signingKey = randomBytes(32)
44
+ }
45
+ return signingKey
46
+ }
47
+
48
+ function generateCode(): string {
49
+ return randomBytes(32).toString('base64url')
50
+ }
51
+
52
+ async function generatePkce(): Promise<{ verifier: string; challenge: string }> {
53
+ const verifier = randomBytes(32).toString('base64url')
54
+ const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
55
+ const challenge = Buffer.from(hash).toString('base64url')
56
+ return { verifier, challenge }
57
+ }
58
+
59
+ async function verifyPkce(verifier: string, challenge: string): Promise<boolean> {
60
+ const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
61
+ const computed = Buffer.from(hash).toString('base64url')
62
+ return computed === challenge
63
+ }
64
+
65
+ function jsonResponse(status: number, body: Record<string, unknown>, headers: Record<string, string> = {}): OAuthResponse {
66
+ return {
67
+ status,
68
+ headers: { 'Content-Type': 'application/json', ...CORS_HEADERS, ...headers },
69
+ body
70
+ }
71
+ }
72
+
73
+ function redirectResponse(url: string): OAuthResponse {
74
+ return {
75
+ status: 302,
76
+ headers: { Location: url, ...CORS_HEADERS },
77
+ body: undefined
78
+ }
79
+ }
80
+
81
+ function errorResponse(status: number, error: string, description: string): OAuthResponse {
82
+ return jsonResponse(status, { error, error_description: description })
83
+ }
84
+
85
+ const provider: OAuthProvider = {
86
+ metadata(): OAuthResponse {
87
+ return jsonResponse(200, {
88
+ issuer: config.resourceUrl,
89
+ authorization_endpoint: `${config.resourceUrl}/authorize`,
90
+ token_endpoint: `${config.resourceUrl}/token`,
91
+ registration_endpoint: `${config.resourceUrl}/register`,
92
+ response_types_supported: ['code'],
93
+ grant_types_supported: ['authorization_code', 'refresh_token'],
94
+ code_challenge_methods_supported: ['S256'],
95
+ token_endpoint_auth_methods_supported: ['none', 'client_secret_post'],
96
+ scopes_supported: scopes
97
+ }, { 'Cache-Control': 'max-age=3600' })
98
+ },
99
+
100
+ async register(req): Promise<OAuthResponse> {
101
+ const body = req.body
102
+ if (!body) { return errorResponse(400, 'invalid_request', 'Missing request body') }
103
+
104
+ const redirectUris = body.redirect_uris
105
+ if (!redirectUris) { return errorResponse(400, 'invalid_request', 'redirect_uris is required') }
106
+
107
+ let uris: string[]
108
+ try {
109
+ uris = typeof redirectUris === 'string' ? JSON.parse(redirectUris) : redirectUris
110
+ } catch {
111
+ uris = [redirectUris]
112
+ }
113
+
114
+ if (!Array.isArray(uris) || uris.length === 0) {
115
+ return errorResponse(400, 'invalid_request', 'redirect_uris must be a non-empty array')
116
+ }
117
+
118
+ for (const uri of uris) {
119
+ if (!matchRedirectUri(uri, config.redirectUris)) {
120
+ return errorResponse(400, 'invalid_redirect_uri', `Redirect URI not allowed: ${uri}`)
121
+ }
122
+ }
123
+
124
+ const clientId = randomUUID()
125
+ const clientSecret = randomBytes(32).toString('base64url')
126
+ const registration = {
127
+ clientId,
128
+ clientSecret,
129
+ redirectUris: uris,
130
+ clientName: body.client_name,
131
+ createdAt: Date.now() / 1000
132
+ }
133
+
134
+ await store.saveClient(clientId, registration)
135
+
136
+ return jsonResponse(201, {
137
+ client_id: clientId,
138
+ client_secret: clientSecret,
139
+ redirect_uris: uris,
140
+ client_name: body.client_name,
141
+ token_endpoint_auth_method: 'none'
142
+ })
143
+ },
144
+
145
+ async authorize(req): Promise<OAuthResponse> {
146
+ const params = req.url.searchParams
147
+ const responseType = params.get('response_type')
148
+ const clientId = params.get('client_id')
149
+ const redirectUri = params.get('redirect_uri')
150
+ const scope = params.get('scope') ?? ''
151
+ const clientState = params.get('state') ?? ''
152
+ const codeChallenge = params.get('code_challenge')
153
+ const codeChallengeMethod = params.get('code_challenge_method')
154
+ const resource = params.get('resource')
155
+
156
+ if (responseType !== 'code') {
157
+ return errorResponse(400, 'unsupported_response_type', 'Only response_type=code is supported')
158
+ }
159
+ if (!clientId) { return errorResponse(400, 'invalid_request', 'client_id is required') }
160
+ if (!redirectUri) { return errorResponse(400, 'invalid_request', 'redirect_uri is required') }
161
+ if (!codeChallenge || codeChallengeMethod !== 'S256') {
162
+ return errorResponse(400, 'invalid_request', 'PKCE with S256 is required')
163
+ }
164
+
165
+ let client = await store.getClient(clientId)
166
+
167
+ if (!client && clientId.startsWith('https://')) {
168
+ try {
169
+ const metaRes = await fetch(clientId)
170
+ if (!metaRes.ok) {
171
+ return errorResponse(400, 'invalid_client', 'Failed to fetch client metadata document')
172
+ }
173
+ const meta = await metaRes.json() as Record<string, unknown>
174
+ const metaRedirectUris = meta.redirect_uris as string[] | undefined
175
+ if (!Array.isArray(metaRedirectUris) || metaRedirectUris.length === 0) {
176
+ return errorResponse(400, 'invalid_client', 'Client metadata must include redirect_uris')
177
+ }
178
+ for (const uri of metaRedirectUris) {
179
+ if (!matchRedirectUri(uri, config.redirectUris)) {
180
+ return errorResponse(400, 'invalid_redirect_uri', `Redirect URI not allowed: ${uri}`)
181
+ }
182
+ }
183
+ client = {
184
+ clientId,
185
+ clientSecret: '',
186
+ redirectUris: metaRedirectUris,
187
+ clientName: meta.client_name as string | undefined,
188
+ createdAt: Date.now() / 1000
189
+ }
190
+ await store.saveClient(clientId, client)
191
+ } catch {
192
+ return errorResponse(400, 'invalid_client', 'Failed to fetch client metadata document')
193
+ }
194
+ }
195
+
196
+ if (!client) { return errorResponse(400, 'invalid_client', 'Unknown client_id') }
197
+
198
+ if (!client.redirectUris.includes(redirectUri)) {
199
+ return errorResponse(400, 'invalid_redirect_uri', 'redirect_uri does not match registered URIs')
200
+ }
201
+
202
+ const proxyState = randomUUID()
203
+ const pkce = await generatePkce()
204
+
205
+ await store.savePendingAuth(proxyState, {
206
+ clientId,
207
+ redirectUri,
208
+ codeChallenge,
209
+ codeChallengeMethod,
210
+ scope,
211
+ clientState,
212
+ resource: resource ?? undefined,
213
+ expiresAt: Date.now() / 1000 + 600
214
+ })
215
+
216
+ await store.savePkceVerifier(proxyState, pkce.verifier)
217
+
218
+ const upstreamScopes = scopes.length > 0 ? scopes.join(' ') : scope
219
+ const upstreamUrl = new URL(config.authorizeUrl)
220
+ upstreamUrl.searchParams.set('response_type', 'code')
221
+ upstreamUrl.searchParams.set('client_id', config.clientId)
222
+ upstreamUrl.searchParams.set('redirect_uri', `${config.resourceUrl}${callbackPath}`)
223
+ upstreamUrl.searchParams.set('scope', upstreamScopes)
224
+ upstreamUrl.searchParams.set('state', proxyState)
225
+ upstreamUrl.searchParams.set('code_challenge', pkce.challenge)
226
+ upstreamUrl.searchParams.set('code_challenge_method', 'S256')
227
+ if (params.has('access_type')) {
228
+ upstreamUrl.searchParams.set('access_type', params.get('access_type')!)
229
+ }
230
+
231
+ return redirectResponse(upstreamUrl.toString())
232
+ },
233
+
234
+ async callback(req): Promise<OAuthResponse> {
235
+ const params = req.url.searchParams
236
+ const upstreamCode = params.get('code')
237
+ const proxyState = params.get('state')
238
+ const upstreamError = params.get('error')
239
+
240
+ if (upstreamError) {
241
+ return errorResponse(400, 'upstream_error', params.get('error_description') ?? upstreamError)
242
+ }
243
+ if (!upstreamCode || !proxyState) {
244
+ return errorResponse(400, 'invalid_request', 'Missing code or state')
245
+ }
246
+
247
+ const pending = await store.getPendingAuth(proxyState)
248
+ if (!pending) { return errorResponse(400, 'invalid_request', 'Unknown or expired state') }
249
+
250
+ const pkceVerifier = await store.getPkceVerifier(proxyState)
251
+ if (!pkceVerifier) { return errorResponse(400, 'invalid_request', 'Missing PKCE verifier') }
252
+
253
+ await store.deletePendingAuth(proxyState)
254
+ await store.deletePkceVerifier(proxyState)
255
+
256
+ const tokenBody = new URLSearchParams({
257
+ grant_type: 'authorization_code',
258
+ code: upstreamCode,
259
+ redirect_uri: `${config.resourceUrl}${callbackPath}`,
260
+ client_id: config.clientId,
261
+ client_secret: config.clientSecret,
262
+ code_verifier: pkceVerifier
263
+ })
264
+
265
+ const tokenResponse = await fetch(config.tokenUrl, {
266
+ method: 'POST',
267
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
268
+ body: tokenBody.toString()
269
+ })
270
+
271
+ if (!tokenResponse.ok) {
272
+ const errorBody = await tokenResponse.text()
273
+ console.error('Upstream token exchange failed:', errorBody)
274
+ return errorResponse(502, 'upstream_error', 'Failed to exchange code with upstream provider')
275
+ }
276
+
277
+ const tokens = await tokenResponse.json() as Record<string, unknown>
278
+ const upstreamAccessToken = tokens.access_token as string
279
+ const upstreamIdToken = tokens.id_token as string | undefined
280
+
281
+ let email: string | undefined
282
+ let sub: string | undefined
283
+ if (config.userinfoUrl && upstreamAccessToken) {
284
+ try {
285
+ const userinfoResponse = await fetch(config.userinfoUrl, {
286
+ headers: { Authorization: `Bearer ${upstreamAccessToken}` }
287
+ })
288
+ if (userinfoResponse.ok) {
289
+ const userinfo = await userinfoResponse.json() as Record<string, unknown>
290
+ email = userinfo.email as string | undefined
291
+ sub = userinfo.sub as string | undefined
292
+ }
293
+ } catch {
294
+ // Userinfo fetch is best-effort
295
+ }
296
+ }
297
+
298
+ const mcpCode = generateCode()
299
+ await store.saveAuthCode(mcpCode, {
300
+ clientId: pending.clientId,
301
+ redirectUri: pending.redirectUri,
302
+ codeChallenge: pending.codeChallenge,
303
+ codeChallengeMethod: pending.codeChallengeMethod,
304
+ scopes: pending.scope.split(' ').filter(Boolean),
305
+ upstreamAccessToken,
306
+ upstreamIdToken,
307
+ email,
308
+ sub,
309
+ expiresAt: Date.now() / 1000 + 600
310
+ })
311
+
312
+ const clientRedirect = new URL(pending.redirectUri)
313
+ clientRedirect.searchParams.set('code', mcpCode)
314
+ if (pending.clientState) {
315
+ clientRedirect.searchParams.set('state', pending.clientState)
316
+ }
317
+
318
+ return redirectResponse(clientRedirect.toString())
319
+ },
320
+
321
+ async token(req): Promise<OAuthResponse> {
322
+ const body = req.body
323
+ if (!body) { return errorResponse(400, 'invalid_request', 'Missing request body') }
324
+
325
+ const grantType = body.grant_type
326
+
327
+ if (grantType === 'refresh_token') {
328
+ const refreshToken = body.refresh_token
329
+ if (!refreshToken) {
330
+ return errorResponse(400, 'invalid_request', 'Missing refresh_token')
331
+ }
332
+
333
+ const tokenData = await store.getRefreshToken(refreshToken)
334
+ if (!tokenData) {
335
+ return errorResponse(400, 'invalid_grant', 'Invalid or expired refresh token')
336
+ }
337
+
338
+ const key = await getSigningKey()
339
+ const now = Math.floor(Date.now() / 1000)
340
+ const accessToken = await new SignJWT({
341
+ scope: tokenData.scopes.join(' '),
342
+ email: tokenData.email
343
+ })
344
+ .setProtectedHeader({ alg: 'HS256' })
345
+ .setSubject(tokenData.sub ?? tokenData.email ?? tokenData.clientId)
346
+ .setIssuer(config.resourceUrl)
347
+ .setAudience(config.resourceUrl)
348
+ .setIssuedAt(now)
349
+ .setExpirationTime(now + tokenTtl)
350
+ .sign(key)
351
+
352
+ return jsonResponse(200, {
353
+ access_token: accessToken,
354
+ token_type: 'Bearer',
355
+ expires_in: tokenTtl,
356
+ scope: tokenData.scopes.join(' '),
357
+ refresh_token: refreshToken
358
+ })
359
+ }
360
+
361
+ if (grantType !== 'authorization_code') {
362
+ return errorResponse(400, 'unsupported_grant_type', 'Supported grant types: authorization_code, refresh_token')
363
+ }
364
+
365
+ const code = body.code
366
+ const redirectUri = body.redirect_uri
367
+ const clientId = body.client_id
368
+ const codeVerifier = body.code_verifier
369
+
370
+ if (!code || !codeVerifier || !clientId) {
371
+ return errorResponse(400, 'invalid_request', 'Missing required parameters')
372
+ }
373
+
374
+ const authCode = await store.getAuthCode(code)
375
+ if (!authCode) { return errorResponse(400, 'invalid_grant', 'Invalid or expired authorization code') }
376
+
377
+ await store.deleteAuthCode(code)
378
+
379
+ if (authCode.clientId !== clientId) {
380
+ return errorResponse(400, 'invalid_grant', 'client_id mismatch')
381
+ }
382
+ if (redirectUri && authCode.redirectUri !== redirectUri) {
383
+ return errorResponse(400, 'invalid_grant', 'redirect_uri mismatch')
384
+ }
385
+
386
+ const pkceValid = await verifyPkce(codeVerifier, authCode.codeChallenge)
387
+ if (!pkceValid) {
388
+ return errorResponse(400, 'invalid_grant', 'PKCE verification failed')
389
+ }
390
+
391
+ const key = await getSigningKey()
392
+ const now = Math.floor(Date.now() / 1000)
393
+ const accessToken = await new SignJWT({
394
+ scope: authCode.scopes.join(' '),
395
+ email: authCode.email
396
+ })
397
+ .setProtectedHeader({ alg: 'HS256' })
398
+ .setSubject(authCode.sub ?? authCode.email ?? authCode.clientId)
399
+ .setIssuer(config.resourceUrl)
400
+ .setAudience(config.resourceUrl)
401
+ .setIssuedAt(now)
402
+ .setExpirationTime(now + tokenTtl)
403
+ .sign(key)
404
+
405
+ const refreshToken = randomBytes(32).toString('base64url')
406
+ await store.saveRefreshToken(refreshToken, {
407
+ clientId: authCode.clientId,
408
+ scopes: authCode.scopes,
409
+ email: authCode.email,
410
+ sub: authCode.sub,
411
+ expiresAt: now + 30 * 24 * 3600
412
+ })
413
+
414
+ return jsonResponse(200, {
415
+ access_token: accessToken,
416
+ token_type: 'Bearer',
417
+ expires_in: tokenTtl,
418
+ scope: authCode.scopes.join(' '),
419
+ refresh_token: refreshToken
420
+ })
421
+ },
422
+
423
+ async verifyToken(token): Promise<AuthInfo | undefined> {
424
+ try {
425
+ const key = await getSigningKey()
426
+ const { payload } = await jwtVerify(token, key, {
427
+ issuer: config.resourceUrl,
428
+ audience: config.resourceUrl
429
+ })
430
+ return {
431
+ token,
432
+ clientId: payload.sub ?? undefined,
433
+ scopes: typeof payload.scope === 'string' ? payload.scope.split(' ').filter(Boolean) : [],
434
+ expiresAt: payload.exp,
435
+ email: payload.email as string | undefined
436
+ }
437
+ } catch {
438
+ return undefined
439
+ }
440
+ }
441
+ }
442
+
443
+ return provider
444
+ }
@@ -0,0 +1,60 @@
1
+ export interface AuthCodeData {
2
+ clientId: string
3
+ redirectUri: string
4
+ codeChallenge: string
5
+ codeChallengeMethod: string
6
+ scopes: string[]
7
+ upstreamAccessToken: string
8
+ upstreamIdToken?: string
9
+ email?: string
10
+ sub?: string
11
+ expiresAt: number
12
+ }
13
+
14
+ export interface PendingAuthData {
15
+ clientId: string
16
+ redirectUri: string
17
+ codeChallenge: string
18
+ codeChallengeMethod: string
19
+ scope: string
20
+ clientState: string
21
+ resource?: string
22
+ expiresAt: number
23
+ }
24
+
25
+ export interface ClientRegistration {
26
+ clientId: string
27
+ clientSecret: string
28
+ redirectUris: string[]
29
+ clientName?: string
30
+ createdAt: number
31
+ }
32
+
33
+ export interface RefreshTokenData {
34
+ clientId: string
35
+ scopes: string[]
36
+ email?: string
37
+ sub?: string
38
+ expiresAt: number
39
+ }
40
+
41
+ export interface OAuthStore {
42
+ saveAuthCode(code: string, data: AuthCodeData): Promise<void>
43
+ getAuthCode(code: string): Promise<AuthCodeData | undefined>
44
+ deleteAuthCode(code: string): Promise<void>
45
+
46
+ savePendingAuth(state: string, data: PendingAuthData): Promise<void>
47
+ getPendingAuth(state: string): Promise<PendingAuthData | undefined>
48
+ deletePendingAuth(state: string): Promise<void>
49
+
50
+ savePkceVerifier(state: string, verifier: string): Promise<void>
51
+ getPkceVerifier(state: string): Promise<string | undefined>
52
+ deletePkceVerifier(state: string): Promise<void>
53
+
54
+ saveClient(clientId: string, data: ClientRegistration): Promise<void>
55
+ getClient(clientId: string): Promise<ClientRegistration | undefined>
56
+
57
+ saveRefreshToken(token: string, data: RefreshTokenData): Promise<void>
58
+ getRefreshToken(token: string): Promise<RefreshTokenData | undefined>
59
+ deleteRefreshToken(token: string): Promise<void>
60
+ }
@@ -0,0 +1,23 @@
1
+ import { AuthInfo } from '../types.js'
2
+
3
+ export interface OAuthRequest {
4
+ method: string
5
+ url: URL
6
+ headers: Record<string, string | undefined>
7
+ body?: Record<string, string>
8
+ }
9
+
10
+ export interface OAuthResponse {
11
+ status: number
12
+ headers: Record<string, string>
13
+ body?: string | Record<string, unknown>
14
+ }
15
+
16
+ export interface OAuthProvider {
17
+ authorize(req: OAuthRequest): Promise<OAuthResponse>
18
+ callback(req: OAuthRequest): Promise<OAuthResponse>
19
+ token(req: OAuthRequest): Promise<OAuthResponse>
20
+ register(req: OAuthRequest): Promise<OAuthResponse>
21
+ metadata(): OAuthResponse
22
+ verifyToken(token: string): Promise<AuthInfo | undefined>
23
+ }
@@ -0,0 +1,13 @@
1
+ export function matchRedirectUri(uri: string, patterns: string[]): boolean {
2
+ return patterns.some((pattern) => {
3
+ const regex = patternToRegex(pattern)
4
+ return regex.test(uri)
5
+ })
6
+ }
7
+
8
+ function patternToRegex(pattern: string): RegExp {
9
+ const escaped = pattern
10
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
11
+ .replace(/\*/g, '.*')
12
+ return new RegExp(`^${escaped}$`)
13
+ }
@@ -0,0 +1,97 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
2
+ import { AuthCodeData, ClientRegistration, OAuthStore, PendingAuthData, RefreshTokenData } from '../provider/store.js'
3
+
4
+ function withTtl<T extends { expiresAt: number }>(obj: Record<string, T>, key: string): T | undefined {
5
+ const item = obj[key]
6
+ if (!item) { return undefined }
7
+ if (item.expiresAt < Date.now() / 1000) {
8
+ delete obj[key]
9
+ return undefined
10
+ }
11
+ return item
12
+ }
13
+
14
+ interface JsonStore {
15
+ authCodes: Record<string, AuthCodeData>
16
+ pendingAuths: Record<string, PendingAuthData>
17
+ pkceVerifiers: Record<string, { verifier: string; expiresAt: number }>
18
+ clients: Record<string, ClientRegistration>
19
+ refreshTokens: Record<string, RefreshTokenData>
20
+ }
21
+
22
+ function writeJsonStore(path: string, store: JsonStore) {
23
+ writeFileSync(path, JSON.stringify(store, null, 2), 'utf-8')
24
+ }
25
+
26
+ function readJsonStore(path: string): JsonStore {
27
+ if (!existsSync(path)) {
28
+ return {
29
+ authCodes: {},
30
+ pendingAuths: {},
31
+ pkceVerifiers: {},
32
+ clients: {},
33
+ refreshTokens: {}
34
+ }
35
+ }
36
+ const data = JSON.parse(readFileSync(path, 'utf-8'))
37
+ if (!data.refreshTokens) { data.refreshTokens = {} }
38
+ return data
39
+ }
40
+
41
+ export function createJsonStore(path: string): OAuthStore {
42
+ const store: JsonStore = readJsonStore(path)
43
+
44
+ return {
45
+ async saveAuthCode(code, data) {
46
+ store.authCodes[code] = data
47
+ writeJsonStore(path, store)
48
+ },
49
+ async getAuthCode(code) { return withTtl(store.authCodes, code) },
50
+ async deleteAuthCode(code) {
51
+ delete store.authCodes[code]
52
+ writeJsonStore(path, store)
53
+ },
54
+ async savePendingAuth(state, data) {
55
+ store.pendingAuths[state] = data
56
+ writeJsonStore(path, store)
57
+ },
58
+ async getPendingAuth(state) { return withTtl(store.pendingAuths, state) },
59
+ async deletePendingAuth(state) {
60
+ delete store.pendingAuths[state]
61
+ writeJsonStore(path, store)
62
+ },
63
+ async savePkceVerifier(state, verifier) {
64
+ store.pkceVerifiers[state] = { verifier, expiresAt: Date.now() / 1000 + 600 }
65
+ writeJsonStore(path, store)
66
+ },
67
+ async getPkceVerifier(state) {
68
+ const item = store.pkceVerifiers[state]
69
+ if (!item) { return undefined }
70
+ if (item.expiresAt < Date.now() / 1000) {
71
+ delete store.pkceVerifiers[state]
72
+ writeJsonStore(path, store)
73
+ return undefined
74
+ }
75
+ return item.verifier
76
+ },
77
+ async deletePkceVerifier(state) {
78
+ delete store.pkceVerifiers[state]
79
+ writeJsonStore(path, store)
80
+ },
81
+ async saveClient(clientId, data) {
82
+ store.clients[clientId] = data
83
+ writeJsonStore(path, store)
84
+ },
85
+ async getClient(clientId) { return store.clients[clientId] },
86
+
87
+ async saveRefreshToken(token, data) {
88
+ store.refreshTokens[token] = data
89
+ writeJsonStore(path, store)
90
+ },
91
+ async getRefreshToken(token) { return withTtl(store.refreshTokens, token) },
92
+ async deleteRefreshToken(token) {
93
+ delete store.refreshTokens[token]
94
+ writeJsonStore(path, store)
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,50 @@
1
+ import { AuthCodeData, ClientRegistration, OAuthStore, PendingAuthData, RefreshTokenData } from '../provider/store.js'
2
+
3
+ function withTtl<T extends { expiresAt: number }>(map: Map<string, T>, key: string): T | undefined {
4
+ const item = map.get(key)
5
+ if (!item) { return undefined }
6
+ if (item.expiresAt < Date.now() / 1000) {
7
+ map.delete(key)
8
+ return undefined
9
+ }
10
+ return item
11
+ }
12
+
13
+ export function createMemoryStore(): OAuthStore {
14
+ const authCodes = new Map<string, AuthCodeData>()
15
+ const pendingAuths = new Map<string, PendingAuthData>()
16
+ const pkceVerifiers = new Map<string, { verifier: string; expiresAt: number }>()
17
+ const clients = new Map<string, ClientRegistration>()
18
+ const refreshTokens = new Map<string, RefreshTokenData>()
19
+
20
+ return {
21
+ async saveAuthCode(code, data) { authCodes.set(code, data) },
22
+ async getAuthCode(code) { return withTtl(authCodes, code) },
23
+ async deleteAuthCode(code) { authCodes.delete(code) },
24
+
25
+ async savePendingAuth(state, data) { pendingAuths.set(state, data) },
26
+ async getPendingAuth(state) { return withTtl(pendingAuths, state) },
27
+ async deletePendingAuth(state) { pendingAuths.delete(state) },
28
+
29
+ async savePkceVerifier(state, verifier) {
30
+ pkceVerifiers.set(state, { verifier, expiresAt: Date.now() / 1000 + 600 })
31
+ },
32
+ async getPkceVerifier(state) {
33
+ const item = pkceVerifiers.get(state)
34
+ if (!item) { return undefined }
35
+ if (item.expiresAt < Date.now() / 1000) {
36
+ pkceVerifiers.delete(state)
37
+ return undefined
38
+ }
39
+ return item.verifier
40
+ },
41
+ async deletePkceVerifier(state) { pkceVerifiers.delete(state) },
42
+
43
+ async saveClient(clientId, data) { clients.set(clientId, data) },
44
+ async getClient(clientId) { return clients.get(clientId) },
45
+
46
+ async saveRefreshToken(token, data) { refreshTokens.set(token, data) },
47
+ async getRefreshToken(token) { return withTtl(refreshTokens, token) },
48
+ async deleteRefreshToken(token) { refreshTokens.delete(token) }
49
+ }
50
+ }