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