@shware/security 3.1.1 → 3.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.
- package/dist/auth/index.cjs +14 -10
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.mjs +14 -10
- package/dist/auth/index.mjs.map +1 -1
- package/dist/email/schema.cjs.map +1 -1
- package/dist/oauth2/client.cjs.map +1 -1
- package/dist/oauth2/provider/apple-client-secret.cjs +46 -0
- package/dist/oauth2/provider/apple-client-secret.cjs.map +1 -0
- package/dist/oauth2/provider/apple-client-secret.d.cts +28 -0
- package/dist/oauth2/provider/apple-client-secret.d.cts.map +1 -0
- package/dist/oauth2/provider/apple-client-secret.d.mts +28 -0
- package/dist/oauth2/provider/apple-client-secret.d.mts.map +1 -0
- package/dist/oauth2/provider/apple-client-secret.mjs +45 -0
- package/dist/oauth2/provider/apple-client-secret.mjs.map +1 -0
- package/dist/oauth2/provider/common.cjs +3 -2
- package/dist/oauth2/provider/common.cjs.map +1 -1
- package/dist/oauth2/provider/common.d.cts.map +1 -1
- package/dist/oauth2/provider/common.d.mts.map +1 -1
- package/dist/oauth2/provider/common.mjs +3 -2
- package/dist/oauth2/provider/common.mjs.map +1 -1
- package/dist/oauth2/provider/google.cjs +9 -1
- package/dist/oauth2/provider/google.cjs.map +1 -1
- package/dist/oauth2/provider/google.d.cts.map +1 -1
- package/dist/oauth2/provider/google.d.mts.map +1 -1
- package/dist/oauth2/provider/google.mjs +9 -1
- package/dist/oauth2/provider/google.mjs.map +1 -1
- package/dist/oauth2/provider/index.cjs +2 -0
- package/dist/oauth2/provider/index.d.cts +2 -1
- package/dist/oauth2/provider/index.d.mts +2 -1
- package/dist/oauth2/provider/index.mjs +2 -1
- package/dist/session/db.cjs.map +1 -1
- package/dist/session/map-session.cjs.map +1 -1
- package/dist/utils/http.cjs.map +1 -1
- package/package.json +1 -1
package/dist/auth/index.cjs
CHANGED
|
@@ -482,17 +482,21 @@ var Auth = class {
|
|
|
482
482
|
};
|
|
483
483
|
getSession = async (request, create) => {
|
|
484
484
|
const sessionId = this.getSessionId(request);
|
|
485
|
-
if (!sessionId)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
485
|
+
if (!sessionId) {
|
|
486
|
+
if (create) {
|
|
487
|
+
const session = this.repository.createSession();
|
|
488
|
+
await this.repository.save(session);
|
|
489
|
+
return session;
|
|
490
|
+
} else return null;
|
|
491
|
+
}
|
|
490
492
|
const session = await this.repository.findById(sessionId);
|
|
491
|
-
if (!session)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
493
|
+
if (!session) {
|
|
494
|
+
if (create) {
|
|
495
|
+
const session = this.repository.createSession();
|
|
496
|
+
await this.repository.save(session);
|
|
497
|
+
return session;
|
|
498
|
+
} else return null;
|
|
499
|
+
}
|
|
496
500
|
return session;
|
|
497
501
|
};
|
|
498
502
|
deleteSession = async (sessionId) => {
|
package/dist/auth/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["OAuth2Client","getCookie","PRINCIPAL_NAME_INDEX_NAME","param","timing","query","oauth2RedirectQuerySchema","googleOneTapSchema","google","valid","sendEmailVerificationCodeSchema","verifyTurnstileToken","invalidArgument","loginEmailSchema"],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomInt, randomUUID, timingSafeEqual } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport type { Principal } from '../core/index';\nimport { loginEmailSchema, sendEmailVerificationCodeSchema } from '../email/schema';\nimport { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport type { OAuth2ErrorType } from '../oauth2/error';\nimport { google } from '../oauth2/provider/index';\nimport type {\n NativeCredential,\n OAuth2AuthorizationRequest,\n PkceParameters,\n StandardClaims,\n} from '../oauth2/types';\nimport { PRINCIPAL_NAME_INDEX_NAME } from '../session/common';\nimport type { KVRepository, Session, SessionRepository } from '../session/types';\nimport {\n type CookieOptions,\n deleteCookie,\n getCookie,\n param,\n query,\n setCookie,\n} from '../utils/http';\nimport { timing } from '../utils/timing';\nimport { invalidArgument, valid, verifyTurnstileToken } from '../utils/valid';\nimport type { AuthConfig, AuthService, AuthorizedHandler, LoggedHandler } from './types';\n\n// PKCE per RFC 7636: code_verifier / code_challenge are 43-128 chars from\n// the unreserved set. We restrict to base64url (drops \".\" and \"~\") since\n// that's what S256 produces and what callers should be sending; rejecting\n// the rest shrinks the accepted input shape without losing legit clients.\n// A 32-byte random verifier base64url-encodes to exactly 43 chars.\nconst PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;\n\ninterface DesktopCodeRecord {\n /** principal.name from the upstream cookie session */\n name: string;\n /** code_challenge (S256, base64url) — verifier must hash to this */\n cc: string;\n}\n\nexport const PATH = {\n CSRF: '/csrf',\n LOGOUT: '/logout',\n LOGGED: '/logged',\n\n OAUTH2_STATE: '/oauth2/state/:registrationId',\n OAUTH2_NONCE: '/oauth2/nonce/:registrationId',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:registrationId',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:registrationId',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:registrationId',\n LOGIN_OAUTH2_ONETAP: '/login/oauth2/onetap/google',\n\n LOGIN_EMAIL: '/login/email',\n LOGIN_PHONE: '/login/phone',\n SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\n SEND_PHONE_VERIFICATION_CODE: '/auth/sendPhoneVerificationCode',\n\n // Desktop / native app sign-in via web loopback (RFC 8252 + OAuth 2.1 Auth Code Flow):\n // 1. user already authenticated with cookie session → POST DESKTOP_AUTHORIZE → { code }\n // 2. web redirects to desktop's loopback callback with the code\n // 3. desktop renderer POSTs DESKTOP_EXCHANGE with the code → server returns\n // Set-Cookie carrying a fresh session, single-use code is invalidated\n // The two-step flow keeps the actual session id off the redirect URL — only a\n // 5-minute single-use code transits via the loopback.\n DESKTOP_AUTHORIZE: '/auth/desktop/authorize',\n DESKTOP_EXCHANGE: '/auth/desktop/exchange',\n\n CLEANUP_EXPIRED_SESSIONS: '/sessions/expired/cleanup',\n} as const;\n\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly kv: KVRepository;\n private readonly repository: SessionRepository;\n private readonly turnstileSecretKey?: string;\n private readonly oauth2Client: OAuth2Client | null;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n oauth2,\n cookie,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n }\n\n private getSessionId(request: Request): string | undefined {\n let sessionId: string | undefined;\n const header = request.headers.get('Authorization');\n if (header?.startsWith('Bearer ')) {\n sessionId = header.replace('Bearer ', '');\n }\n if (!sessionId) {\n sessionId = getCookie(request, this.cookieName);\n }\n return sessionId;\n }\n\n csrf = async (_request: Request): Promise<Response> => {\n const token = randomUUID();\n const response = Response.json({\n token,\n parameterName: '_csrf',\n headerName: 'X-XSRF-TOKEN',\n });\n\n // Double Submit Cookie Protection\n setCookie(response, 'XSRF-TOKEN', token, {\n path: '/',\n secure: true,\n httpOnly: false,\n sameSite: 'none',\n domain: this.cookieOptions.domain,\n });\n return response;\n };\n\n logout = async (request: Request): Promise<Response> => {\n const sessionId = this.getSessionId(request);\n if (sessionId) await this.repository.deleteById(sessionId);\n const response = Response.json({});\n deleteCookie(response, this.cookieName);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n // 1. get session from header or cookie\n const sessionId = this.getSessionId(request);\n if (!sessionId) return Response.json({ data: false });\n const session = await this.repository.findById(sessionId);\n if (!session) return Response.json({ data: false });\n\n // 2. get principal name from session\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n // 3. call onLogged\n if (name !== null) await onLogged?.({ name: String(name) });\n\n // 4. renew cookie\n const response = Response.json({ data: name !== null });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n\n private createPkceParameters(): PkceParameters {\n const code_verifier = randomUUID();\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n private getOauth2NonceKey(nonce: string) {\n return `oauth2:nonce:${nonce}`;\n }\n\n private getEmailVerificationCodeKey(email: string) {\n return `email:verification:${email}`;\n }\n\n private getPhoneVerificationCodeKey(phone: string) {\n return `phone:verification:${phone}`;\n }\n\n private getDesktopCodeKey(code: string) {\n return `desktop:code:${code}`;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), registrationId, 10 * 60);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), registrationId, 10 * 60);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = async (request: Request): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = await this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });\n const sessionId = this.getSessionId(request);\n const session = sessionId\n ? ((await this.repository.findById(sessionId)) ?? this.repository.createSession())\n : this.repository.createSession();\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n registrationId,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n };\n\n const value = JSON.stringify(authorizationRequest);\n session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n private readonly redirect = (error: OAuth2ErrorType, description?: string): Response => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const uri = new URL(this.oauth2Client.errorUri);\n uri.searchParams.set('error', error);\n if (description) uri.searchParams.set('error_description', description);\n return Response.redirect(uri.href, 302);\n };\n\n loginOAuth2Code = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. get session from header or cookie\n const sessionId = this.getSessionId(request);\n const session = sessionId ? await this.repository.findById(sessionId) : null;\n if (!session) {\n return this.redirect('invalid_request', 'session not found');\n }\n mark('load_session');\n\n // 2. get cached authorization request from session\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', 'authorization request not found');\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 3. validate redirect query/formdata\n const { registrationId } = param(request, PATH.LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (\n request.method === 'POST' &&\n request.headers.get('content-type')?.toLowerCase() === 'application/x-www-form-urlencoded'\n ) {\n const params = new URLSearchParams(await request.text());\n data = Object.fromEntries(params.entries());\n } else {\n data = query(request);\n }\n\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (!parsed.data.code || !parsed.data.state) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n if (registrationId !== cached.registrationId) {\n return this.redirect('invalid_request', 'redirect registration mismatch');\n }\n mark('validate_session');\n\n // 4. exchange authorization code for token and get user info\n const { code } = parsed.data;\n\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code,\n pkce: cached.additionalParameters,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n mark('exchange_code');\n\n // 5. create or update principal\n const principal = await handler(request, registrationId, userInfo, token);\n mark('save_principal');\n\n // 6. update session\n session.setLastAccessedTime(Date.now());\n session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n mark('update_session');\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('registrationId', registrationId);\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Native = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const credentials = (await request.json()) as NativeCredential;\n const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n registrationId,\n credentials,\n });\n mark('login_oauth2_native');\n\n const { nonce } = userInfo.data as { nonce?: string };\n if (nonce) {\n const value = await this.kv.getItem(this.getOauth2NonceKey(nonce));\n if (value !== registrationId) {\n const json = { error: 'invalid_request', error_description: 'nonce mismatch' };\n return Response.json(json, { status: 400 });\n }\n await this.kv.removeItem(this.getOauth2NonceKey(nonce));\n mark('check_nonce');\n }\n\n const principal = await handler(request, registrationId, userInfo, token);\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Onetap = async (\n request: Request,\n handler: AuthorizedHandler,\n registrationId = 'google'\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // should support application/x-www-form-urlencoded and application/json\n let body: Record<string, string>;\n if (request.headers.get('Content-Type')?.includes('application/x-www-form-urlencoded')) {\n const text = await request.text();\n body = Object.fromEntries(new URLSearchParams(text));\n } else if (request.headers.get('Content-Type')?.includes('application/json')) {\n body = (await request.json()) as Record<string, string>;\n } else {\n const json = { error: 'invalid_request', error_description: 'invalid content type' };\n return Response.json(json, { status: 400 });\n }\n\n const parsed = googleOneTapSchema.parse(body);\n\n // csrf token is not supported in FedCM mode\n // const csrf_cookie = getCookie(request, 'g_csrf_token');\n // const csrf_request = parsed.g_csrf_token;\n // if (!csrf_request || csrf_cookie !== csrf_request) {\n // const json = { error: 'invalid_request', error_description: 'invalid csrf token' };\n // return Response.json(json, { status: 400 });\n // }\n\n let principal: Principal;\n if (parsed.credential) {\n const { token, userInfo } = await google.getTokenInfo(parsed.credential);\n principal = await handler(request, registrationId, userInfo, token);\n } else if (parsed.code) {\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code: parsed.code,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n principal = await handler(request, registrationId, userInfo, token);\n } else {\n throw new Error('invalid request');\n }\n\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n sendEmailVerificationCode = async (\n request: Request,\n sender: (data: { email: string; verificationCode: string }) => Promise<void>\n ): Promise<Response> => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n\n const { data, error } = valid(sendEmailVerificationCodeSchema, await request.json());\n if (error) return Response.json(error, { status: error.error.code });\n const { email, turnstileToken } = data;\n\n const result = await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n if (result.error) return Response.json(result.error, { status: result.error.error.code });\n\n try {\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n await sender({ email, verificationCode });\n await this.kv.setItem(this.getEmailVerificationCodeKey(email), verificationCode, 10 * 60);\n return Response.json({});\n } catch (e) {\n console.error('Failed to send email verification code', e);\n return Response.json(invalidArgument('EMAIL_DELIVERY_FAILED'), { status: 400 });\n }\n };\n\n loginEmail = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n const { data, error } = valid(loginEmailSchema, await request.json());\n if (error) return Response.json(error, { status: error.error.code });\n const { email, verificationCode } = data;\n\n const key = this.getEmailVerificationCodeKey(email);\n const stored = await this.kv.getItem(key);\n if (\n typeof stored !== 'string' ||\n stored.length !== verificationCode.length ||\n !timingSafeEqual(Buffer.from(stored), Buffer.from(verificationCode))\n ) {\n return Response.json(invalidArgument('INVALID_VERIFICATION_CODE'), { status: 400 });\n }\n await this.kv.removeItem(key);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n const principal = await handler(request, 'EMAIL', { claims, data: {} });\n\n // save session\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n\n return response;\n };\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already\n * completed normal sign-in on the web (so a cookie session exists); this\n * issues a 5-minute single-use code that the web page hands off to the\n * desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier`\n * before opening the browser, derives `code_challenge = base64url(sha256(verifier))`,\n * and threads the challenge through the URL → web page → here. We pin\n * the challenge to the code in KV; `desktopExchange` enforces that the\n * caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet\n * capture on loopback) from exchanging it.\n *\n * Requires an authenticated cookie session — anonymous callers get 401.\n * Throttling / abuse protection on this endpoint is the API layer's job\n * (rate-limit per principal); the SDK doesn't gate it because the\n * upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = async (request: Request): Promise<Response> => {\n const principal = await this.getPrincipal(request);\n if (!principal) {\n const body = {\n error: {\n code: 401,\n status: 'UNAUTHENTICATED',\n message: 'sign in first',\n details: [],\n },\n };\n return Response.json(body, { status: 401 });\n }\n\n let body: { code_challenge?: unknown; code_challenge_method?: unknown };\n try {\n body = (await request.json()) as {\n code_challenge?: unknown;\n code_challenge_method?: unknown;\n };\n } catch {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const codeChallenge = typeof body.code_challenge === 'string' ? body.code_challenge : null;\n if (\n !codeChallenge ||\n body.code_challenge_method !== 'S256' ||\n !PKCE_FORMAT.test(codeChallenge)\n ) {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const code = randomUUID();\n const record: DesktopCodeRecord = { name: principal.name, cc: codeChallenge };\n await this.kv.setItem(this.getDesktopCodeKey(code), JSON.stringify(record), 5 * 60);\n\n return Response.json({ code });\n };\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop\n * client hands back the auth code it received via the loopback callback,\n * and we mint a fresh session for it — independent of the web cookie\n * session that originally `authorize`'d the code, so logging out on\n * either side doesn't cascade to the other.\n *\n * Cookie attributes: defaults to `SameSite=None; Secure` so the cookie\n * survives cross-site fetches from the desktop renderer's `file://` /\n * `app://` origin. The configured base `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`, `maxAge`); the override\n * is targeted at the SameSite/Secure pair only. In a non-HTTPS dev\n * backend this combination is rejected by Chromium — see the desktop\n * sign-in PRD for workarounds.\n *\n * No CSRF check needed — the request carries no cookie, the auth code\n * itself is the credential, and KV's single-use removal blocks replay.\n */\n desktopExchange = async (request: Request): Promise<Response> => {\n let body: { code?: unknown; code_verifier?: unknown };\n try {\n body = (await request.json()) as { code?: unknown; code_verifier?: unknown };\n } catch {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const code = typeof body.code === 'string' ? body.code : null;\n const verifier = typeof body.code_verifier === 'string' ? body.code_verifier : null;\n if (!code || !verifier || !PKCE_FORMAT.test(verifier)) {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const key = this.getDesktopCodeKey(code);\n const raw = await this.kv.getItem(key);\n if (typeof raw !== 'string') {\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n let record: DesktopCodeRecord;\n try {\n record = JSON.parse(raw) as DesktopCodeRecord;\n } catch {\n // Malformed KV record (shouldn't happen with our own writes) — drop it\n // and surface as an invalid code; the caller can retry the whole flow.\n await this.kv.removeItem(key);\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n\n // Verify PKCE: base64url(sha256(verifier)) === stored challenge.\n // Don't burn the code on a verifier mismatch — that would let an\n // attacker who races a network retry brick a legit exchange. The code\n // is single-use on success and 5-min TTL bounded; brute-forcing the\n // 256-bit verifier in that window is infeasible.\n const computed = createHash('sha256').update(verifier).digest();\n const stored = Buffer.from(record.cc, 'base64url');\n if (stored.length !== computed.length || !timingSafeEqual(computed, stored)) {\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n\n // One-time use: remove BEFORE creating the session so a concurrent\n // duplicate exchange (e.g. retry after timeout) sees the code as\n // consumed even if our session save races it.\n await this.kv.removeItem(key);\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, record.name);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n\n const response = Response.json({ ok: true });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n // Override sameSite/secure for desktop: the renderer's origin\n // (file:// / app://) is cross-site to the API, so SameSite=Lax/Strict\n // would block the cookie on subsequent fetches.\n sameSite: 'none',\n secure: true,\n maxAge,\n });\n return response;\n };\n\n sendPhoneVerificationCode = async (\n _request: Request,\n _sender: (data: { phone: string; verificationCode: string }) => Promise<void>\n ): Promise<Response> => {\n return Response.json({ error: 'not_implemented' }, { status: 501 });\n };\n\n loginPhone = async (_request: Request, _handler: AuthorizedHandler): Promise<Response> => {\n return Response.json({ error: 'not_implemented' }, { status: 501 });\n };\n\n kick = async (principal: Principal): Promise<void> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const sessionId of sessions.keys()) {\n await this.repository.deleteById(sessionId);\n }\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) return false;\n const session = await this.repository.findById(sessionId);\n if (!session) return false;\n const principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n if (!principalName) return false;\n return true;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) {\n if (create) {\n const session = this.repository.createSession();\n await this.repository.save(session);\n return session;\n } else {\n return null as T extends true ? Session : Session | null;\n }\n }\n const session = await this.repository.findById(sessionId);\n if (!session) {\n if (create) {\n const session = this.repository.createSession();\n await this.repository.save(session);\n return session;\n } else {\n return null as T extends true ? Session : Session | null;\n }\n }\n return session;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\n };\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) return null;\n const session = await this.repository.findById(sessionId);\n if (!session) return null;\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n if (!name) return null;\n return { name: String(name) };\n };\n\n listSessions = async (principal: Principal): Promise<Session[]> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n return Array.from(sessions.values());\n };\n\n cleanupExpiredSessions = async (cleanupCount?: number): Promise<Response> => {\n await this.repository.cleanupExpiredSessions(cleanupCount);\n return Response.json({});\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["OAuth2Client","getCookie","randomUUID","PRINCIPAL_NAME_INDEX_NAME","createHash","param","timing","query","oauth2RedirectQuerySchema","googleOneTapSchema","google","valid","sendEmailVerificationCodeSchema","verifyTurnstileToken","randomInt","invalidArgument","loginEmailSchema","timingSafeEqual"],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomInt, randomUUID, timingSafeEqual } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport type { Principal } from '../core/index';\nimport { loginEmailSchema, sendEmailVerificationCodeSchema } from '../email/schema';\nimport { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport type { OAuth2ErrorType } from '../oauth2/error';\nimport { google } from '../oauth2/provider/index';\nimport type {\n NativeCredential,\n OAuth2AuthorizationRequest,\n PkceParameters,\n StandardClaims,\n} from '../oauth2/types';\nimport { PRINCIPAL_NAME_INDEX_NAME } from '../session/common';\nimport type { KVRepository, Session, SessionRepository } from '../session/types';\nimport {\n type CookieOptions,\n deleteCookie,\n getCookie,\n param,\n query,\n setCookie,\n} from '../utils/http';\nimport { timing } from '../utils/timing';\nimport { invalidArgument, valid, verifyTurnstileToken } from '../utils/valid';\nimport type { AuthConfig, AuthService, AuthorizedHandler, LoggedHandler } from './types';\n\n// PKCE per RFC 7636: code_verifier / code_challenge are 43-128 chars from\n// the unreserved set. We restrict to base64url (drops \".\" and \"~\") since\n// that's what S256 produces and what callers should be sending; rejecting\n// the rest shrinks the accepted input shape without losing legit clients.\n// A 32-byte random verifier base64url-encodes to exactly 43 chars.\nconst PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;\n\ninterface DesktopCodeRecord {\n /** principal.name from the upstream cookie session */\n name: string;\n /** code_challenge (S256, base64url) — verifier must hash to this */\n cc: string;\n}\n\nexport const PATH = {\n CSRF: '/csrf',\n LOGOUT: '/logout',\n LOGGED: '/logged',\n\n OAUTH2_STATE: '/oauth2/state/:registrationId',\n OAUTH2_NONCE: '/oauth2/nonce/:registrationId',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:registrationId',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:registrationId',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:registrationId',\n LOGIN_OAUTH2_ONETAP: '/login/oauth2/onetap/google',\n\n LOGIN_EMAIL: '/login/email',\n LOGIN_PHONE: '/login/phone',\n SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\n SEND_PHONE_VERIFICATION_CODE: '/auth/sendPhoneVerificationCode',\n\n // Desktop / native app sign-in via web loopback (RFC 8252 + OAuth 2.1 Auth Code Flow):\n // 1. user already authenticated with cookie session → POST DESKTOP_AUTHORIZE → { code }\n // 2. web redirects to desktop's loopback callback with the code\n // 3. desktop renderer POSTs DESKTOP_EXCHANGE with the code → server returns\n // Set-Cookie carrying a fresh session, single-use code is invalidated\n // The two-step flow keeps the actual session id off the redirect URL — only a\n // 5-minute single-use code transits via the loopback.\n DESKTOP_AUTHORIZE: '/auth/desktop/authorize',\n DESKTOP_EXCHANGE: '/auth/desktop/exchange',\n\n CLEANUP_EXPIRED_SESSIONS: '/sessions/expired/cleanup',\n} as const;\n\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly kv: KVRepository;\n private readonly repository: SessionRepository;\n private readonly turnstileSecretKey?: string;\n private readonly oauth2Client: OAuth2Client | null;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n oauth2,\n cookie,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n }\n\n private getSessionId(request: Request): string | undefined {\n let sessionId: string | undefined;\n const header = request.headers.get('Authorization');\n if (header?.startsWith('Bearer ')) {\n sessionId = header.replace('Bearer ', '');\n }\n if (!sessionId) {\n sessionId = getCookie(request, this.cookieName);\n }\n return sessionId;\n }\n\n csrf = async (_request: Request): Promise<Response> => {\n const token = randomUUID();\n const response = Response.json({\n token,\n parameterName: '_csrf',\n headerName: 'X-XSRF-TOKEN',\n });\n\n // Double Submit Cookie Protection\n setCookie(response, 'XSRF-TOKEN', token, {\n path: '/',\n secure: true,\n httpOnly: false,\n sameSite: 'none',\n domain: this.cookieOptions.domain,\n });\n return response;\n };\n\n logout = async (request: Request): Promise<Response> => {\n const sessionId = this.getSessionId(request);\n if (sessionId) await this.repository.deleteById(sessionId);\n const response = Response.json({});\n deleteCookie(response, this.cookieName);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n // 1. get session from header or cookie\n const sessionId = this.getSessionId(request);\n if (!sessionId) return Response.json({ data: false });\n const session = await this.repository.findById(sessionId);\n if (!session) return Response.json({ data: false });\n\n // 2. get principal name from session\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n // 3. call onLogged\n if (name !== null) await onLogged?.({ name: String(name) });\n\n // 4. renew cookie\n const response = Response.json({ data: name !== null });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n\n private createPkceParameters(): PkceParameters {\n const code_verifier = randomUUID();\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n private getOauth2NonceKey(nonce: string) {\n return `oauth2:nonce:${nonce}`;\n }\n\n private getEmailVerificationCodeKey(email: string) {\n return `email:verification:${email}`;\n }\n\n private getPhoneVerificationCodeKey(phone: string) {\n return `phone:verification:${phone}`;\n }\n\n private getDesktopCodeKey(code: string) {\n return `desktop:code:${code}`;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), registrationId, 10 * 60);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), registrationId, 10 * 60);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = async (request: Request): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = await this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });\n const sessionId = this.getSessionId(request);\n const session = sessionId\n ? ((await this.repository.findById(sessionId)) ?? this.repository.createSession())\n : this.repository.createSession();\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n registrationId,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n };\n\n const value = JSON.stringify(authorizationRequest);\n session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n private readonly redirect = (error: OAuth2ErrorType, description?: string): Response => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const uri = new URL(this.oauth2Client.errorUri);\n uri.searchParams.set('error', error);\n if (description) uri.searchParams.set('error_description', description);\n return Response.redirect(uri.href, 302);\n };\n\n loginOAuth2Code = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. get session from header or cookie\n const sessionId = this.getSessionId(request);\n const session = sessionId ? await this.repository.findById(sessionId) : null;\n if (!session) {\n return this.redirect('invalid_request', 'session not found');\n }\n mark('load_session');\n\n // 2. get cached authorization request from session\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', 'authorization request not found');\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 3. validate redirect query/formdata\n const { registrationId } = param(request, PATH.LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (\n request.method === 'POST' &&\n request.headers.get('content-type')?.toLowerCase() === 'application/x-www-form-urlencoded'\n ) {\n const params = new URLSearchParams(await request.text());\n data = Object.fromEntries(params.entries());\n } else {\n data = query(request);\n }\n\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (!parsed.data.code || !parsed.data.state) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n if (registrationId !== cached.registrationId) {\n return this.redirect('invalid_request', 'redirect registration mismatch');\n }\n mark('validate_session');\n\n // 4. exchange authorization code for token and get user info\n const { code } = parsed.data;\n\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code,\n pkce: cached.additionalParameters,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n mark('exchange_code');\n\n // 5. create or update principal\n const principal = await handler(request, registrationId, userInfo, token);\n mark('save_principal');\n\n // 6. update session\n session.setLastAccessedTime(Date.now());\n session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n mark('update_session');\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('registrationId', registrationId);\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Native = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const credentials = (await request.json()) as NativeCredential;\n const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n registrationId,\n credentials,\n });\n mark('login_oauth2_native');\n\n const { nonce } = userInfo.data as { nonce?: string };\n if (nonce) {\n const value = await this.kv.getItem(this.getOauth2NonceKey(nonce));\n if (value !== registrationId) {\n const json = { error: 'invalid_request', error_description: 'nonce mismatch' };\n return Response.json(json, { status: 400 });\n }\n await this.kv.removeItem(this.getOauth2NonceKey(nonce));\n mark('check_nonce');\n }\n\n const principal = await handler(request, registrationId, userInfo, token);\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Onetap = async (\n request: Request,\n handler: AuthorizedHandler,\n registrationId = 'google'\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // should support application/x-www-form-urlencoded and application/json\n let body: Record<string, string>;\n if (request.headers.get('Content-Type')?.includes('application/x-www-form-urlencoded')) {\n const text = await request.text();\n body = Object.fromEntries(new URLSearchParams(text));\n } else if (request.headers.get('Content-Type')?.includes('application/json')) {\n body = (await request.json()) as Record<string, string>;\n } else {\n const json = { error: 'invalid_request', error_description: 'invalid content type' };\n return Response.json(json, { status: 400 });\n }\n\n const parsed = googleOneTapSchema.parse(body);\n\n // csrf token is not supported in FedCM mode\n // const csrf_cookie = getCookie(request, 'g_csrf_token');\n // const csrf_request = parsed.g_csrf_token;\n // if (!csrf_request || csrf_cookie !== csrf_request) {\n // const json = { error: 'invalid_request', error_description: 'invalid csrf token' };\n // return Response.json(json, { status: 400 });\n // }\n\n let principal: Principal;\n if (parsed.credential) {\n const { token, userInfo } = await google.getTokenInfo(parsed.credential);\n principal = await handler(request, registrationId, userInfo, token);\n } else if (parsed.code) {\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code: parsed.code,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n principal = await handler(request, registrationId, userInfo, token);\n } else {\n throw new Error('invalid request');\n }\n\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n sendEmailVerificationCode = async (\n request: Request,\n sender: (data: { email: string; verificationCode: string }) => Promise<void>\n ): Promise<Response> => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n\n const { data, error } = valid(sendEmailVerificationCodeSchema, await request.json());\n if (error) return Response.json(error, { status: error.error.code });\n const { email, turnstileToken } = data;\n\n const result = await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n if (result.error) return Response.json(result.error, { status: result.error.error.code });\n\n try {\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n await sender({ email, verificationCode });\n await this.kv.setItem(this.getEmailVerificationCodeKey(email), verificationCode, 10 * 60);\n return Response.json({});\n } catch (e) {\n console.error('Failed to send email verification code', e);\n return Response.json(invalidArgument('EMAIL_DELIVERY_FAILED'), { status: 400 });\n }\n };\n\n loginEmail = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n const { data, error } = valid(loginEmailSchema, await request.json());\n if (error) return Response.json(error, { status: error.error.code });\n const { email, verificationCode } = data;\n\n const key = this.getEmailVerificationCodeKey(email);\n const stored = await this.kv.getItem(key);\n if (\n typeof stored !== 'string' ||\n stored.length !== verificationCode.length ||\n !timingSafeEqual(Buffer.from(stored), Buffer.from(verificationCode))\n ) {\n return Response.json(invalidArgument('INVALID_VERIFICATION_CODE'), { status: 400 });\n }\n await this.kv.removeItem(key);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n const principal = await handler(request, 'EMAIL', { claims, data: {} });\n\n // save session\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n\n return response;\n };\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already\n * completed normal sign-in on the web (so a cookie session exists); this\n * issues a 5-minute single-use code that the web page hands off to the\n * desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier`\n * before opening the browser, derives `code_challenge = base64url(sha256(verifier))`,\n * and threads the challenge through the URL → web page → here. We pin\n * the challenge to the code in KV; `desktopExchange` enforces that the\n * caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet\n * capture on loopback) from exchanging it.\n *\n * Requires an authenticated cookie session — anonymous callers get 401.\n * Throttling / abuse protection on this endpoint is the API layer's job\n * (rate-limit per principal); the SDK doesn't gate it because the\n * upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = async (request: Request): Promise<Response> => {\n const principal = await this.getPrincipal(request);\n if (!principal) {\n const body = {\n error: {\n code: 401,\n status: 'UNAUTHENTICATED',\n message: 'sign in first',\n details: [],\n },\n };\n return Response.json(body, { status: 401 });\n }\n\n let body: { code_challenge?: unknown; code_challenge_method?: unknown };\n try {\n body = (await request.json()) as {\n code_challenge?: unknown;\n code_challenge_method?: unknown;\n };\n } catch {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const codeChallenge = typeof body.code_challenge === 'string' ? body.code_challenge : null;\n if (\n !codeChallenge ||\n body.code_challenge_method !== 'S256' ||\n !PKCE_FORMAT.test(codeChallenge)\n ) {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const code = randomUUID();\n const record: DesktopCodeRecord = { name: principal.name, cc: codeChallenge };\n await this.kv.setItem(this.getDesktopCodeKey(code), JSON.stringify(record), 5 * 60);\n\n return Response.json({ code });\n };\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop\n * client hands back the auth code it received via the loopback callback,\n * and we mint a fresh session for it — independent of the web cookie\n * session that originally `authorize`'d the code, so logging out on\n * either side doesn't cascade to the other.\n *\n * Cookie attributes: defaults to `SameSite=None; Secure` so the cookie\n * survives cross-site fetches from the desktop renderer's `file://` /\n * `app://` origin. The configured base `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`, `maxAge`); the override\n * is targeted at the SameSite/Secure pair only. In a non-HTTPS dev\n * backend this combination is rejected by Chromium — see the desktop\n * sign-in PRD for workarounds.\n *\n * No CSRF check needed — the request carries no cookie, the auth code\n * itself is the credential, and KV's single-use removal blocks replay.\n */\n desktopExchange = async (request: Request): Promise<Response> => {\n let body: { code?: unknown; code_verifier?: unknown };\n try {\n body = (await request.json()) as { code?: unknown; code_verifier?: unknown };\n } catch {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const code = typeof body.code === 'string' ? body.code : null;\n const verifier = typeof body.code_verifier === 'string' ? body.code_verifier : null;\n if (!code || !verifier || !PKCE_FORMAT.test(verifier)) {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const key = this.getDesktopCodeKey(code);\n const raw = await this.kv.getItem(key);\n if (typeof raw !== 'string') {\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n let record: DesktopCodeRecord;\n try {\n record = JSON.parse(raw) as DesktopCodeRecord;\n } catch {\n // Malformed KV record (shouldn't happen with our own writes) — drop it\n // and surface as an invalid code; the caller can retry the whole flow.\n await this.kv.removeItem(key);\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n\n // Verify PKCE: base64url(sha256(verifier)) === stored challenge.\n // Don't burn the code on a verifier mismatch — that would let an\n // attacker who races a network retry brick a legit exchange. The code\n // is single-use on success and 5-min TTL bounded; brute-forcing the\n // 256-bit verifier in that window is infeasible.\n const computed = createHash('sha256').update(verifier).digest();\n const stored = Buffer.from(record.cc, 'base64url');\n if (stored.length !== computed.length || !timingSafeEqual(computed, stored)) {\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n\n // One-time use: remove BEFORE creating the session so a concurrent\n // duplicate exchange (e.g. retry after timeout) sees the code as\n // consumed even if our session save races it.\n await this.kv.removeItem(key);\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, record.name);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n\n const response = Response.json({ ok: true });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n // Override sameSite/secure for desktop: the renderer's origin\n // (file:// / app://) is cross-site to the API, so SameSite=Lax/Strict\n // would block the cookie on subsequent fetches.\n sameSite: 'none',\n secure: true,\n maxAge,\n });\n return response;\n };\n\n sendPhoneVerificationCode = async (\n _request: Request,\n _sender: (data: { phone: string; verificationCode: string }) => Promise<void>\n ): Promise<Response> => {\n return Response.json({ error: 'not_implemented' }, { status: 501 });\n };\n\n loginPhone = async (_request: Request, _handler: AuthorizedHandler): Promise<Response> => {\n return Response.json({ error: 'not_implemented' }, { status: 501 });\n };\n\n kick = async (principal: Principal): Promise<void> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const sessionId of sessions.keys()) {\n await this.repository.deleteById(sessionId);\n }\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) return false;\n const session = await this.repository.findById(sessionId);\n if (!session) return false;\n const principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n if (!principalName) return false;\n return true;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) {\n if (create) {\n const session = this.repository.createSession();\n await this.repository.save(session);\n return session;\n } else {\n return null as T extends true ? Session : Session | null;\n }\n }\n const session = await this.repository.findById(sessionId);\n if (!session) {\n if (create) {\n const session = this.repository.createSession();\n await this.repository.save(session);\n return session;\n } else {\n return null as T extends true ? Session : Session | null;\n }\n }\n return session;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\n };\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) return null;\n const session = await this.repository.findById(sessionId);\n if (!session) return null;\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n if (!name) return null;\n return { name: String(name) };\n };\n\n listSessions = async (principal: Principal): Promise<Session[]> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n return Array.from(sessions.values());\n };\n\n cleanupExpiredSessions = async (cleanupCount?: number): Promise<Response> => {\n await this.repository.cleanupExpiredSessions(cleanupCount);\n return Response.json({});\n };\n}\n"],"mappings":"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"}
|
package/dist/auth/index.mjs
CHANGED
|
@@ -481,17 +481,21 @@ var Auth = class {
|
|
|
481
481
|
};
|
|
482
482
|
getSession = async (request, create) => {
|
|
483
483
|
const sessionId = this.getSessionId(request);
|
|
484
|
-
if (!sessionId)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
484
|
+
if (!sessionId) {
|
|
485
|
+
if (create) {
|
|
486
|
+
const session = this.repository.createSession();
|
|
487
|
+
await this.repository.save(session);
|
|
488
|
+
return session;
|
|
489
|
+
} else return null;
|
|
490
|
+
}
|
|
489
491
|
const session = await this.repository.findById(sessionId);
|
|
490
|
-
if (!session)
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
492
|
+
if (!session) {
|
|
493
|
+
if (create) {
|
|
494
|
+
const session = this.repository.createSession();
|
|
495
|
+
await this.repository.save(session);
|
|
496
|
+
return session;
|
|
497
|
+
} else return null;
|
|
498
|
+
}
|
|
495
499
|
return session;
|
|
496
500
|
};
|
|
497
501
|
deleteSession = async (sessionId) => {
|
package/dist/auth/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomInt, randomUUID, timingSafeEqual } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport type { Principal } from '../core/index';\nimport { loginEmailSchema, sendEmailVerificationCodeSchema } from '../email/schema';\nimport { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport type { OAuth2ErrorType } from '../oauth2/error';\nimport { google } from '../oauth2/provider/index';\nimport type {\n NativeCredential,\n OAuth2AuthorizationRequest,\n PkceParameters,\n StandardClaims,\n} from '../oauth2/types';\nimport { PRINCIPAL_NAME_INDEX_NAME } from '../session/common';\nimport type { KVRepository, Session, SessionRepository } from '../session/types';\nimport {\n type CookieOptions,\n deleteCookie,\n getCookie,\n param,\n query,\n setCookie,\n} from '../utils/http';\nimport { timing } from '../utils/timing';\nimport { invalidArgument, valid, verifyTurnstileToken } from '../utils/valid';\nimport type { AuthConfig, AuthService, AuthorizedHandler, LoggedHandler } from './types';\n\n// PKCE per RFC 7636: code_verifier / code_challenge are 43-128 chars from\n// the unreserved set. We restrict to base64url (drops \".\" and \"~\") since\n// that's what S256 produces and what callers should be sending; rejecting\n// the rest shrinks the accepted input shape without losing legit clients.\n// A 32-byte random verifier base64url-encodes to exactly 43 chars.\nconst PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;\n\ninterface DesktopCodeRecord {\n /** principal.name from the upstream cookie session */\n name: string;\n /** code_challenge (S256, base64url) — verifier must hash to this */\n cc: string;\n}\n\nexport const PATH = {\n CSRF: '/csrf',\n LOGOUT: '/logout',\n LOGGED: '/logged',\n\n OAUTH2_STATE: '/oauth2/state/:registrationId',\n OAUTH2_NONCE: '/oauth2/nonce/:registrationId',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:registrationId',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:registrationId',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:registrationId',\n LOGIN_OAUTH2_ONETAP: '/login/oauth2/onetap/google',\n\n LOGIN_EMAIL: '/login/email',\n LOGIN_PHONE: '/login/phone',\n SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\n SEND_PHONE_VERIFICATION_CODE: '/auth/sendPhoneVerificationCode',\n\n // Desktop / native app sign-in via web loopback (RFC 8252 + OAuth 2.1 Auth Code Flow):\n // 1. user already authenticated with cookie session → POST DESKTOP_AUTHORIZE → { code }\n // 2. web redirects to desktop's loopback callback with the code\n // 3. desktop renderer POSTs DESKTOP_EXCHANGE with the code → server returns\n // Set-Cookie carrying a fresh session, single-use code is invalidated\n // The two-step flow keeps the actual session id off the redirect URL — only a\n // 5-minute single-use code transits via the loopback.\n DESKTOP_AUTHORIZE: '/auth/desktop/authorize',\n DESKTOP_EXCHANGE: '/auth/desktop/exchange',\n\n CLEANUP_EXPIRED_SESSIONS: '/sessions/expired/cleanup',\n} as const;\n\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly kv: KVRepository;\n private readonly repository: SessionRepository;\n private readonly turnstileSecretKey?: string;\n private readonly oauth2Client: OAuth2Client | null;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n oauth2,\n cookie,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n }\n\n private getSessionId(request: Request): string | undefined {\n let sessionId: string | undefined;\n const header = request.headers.get('Authorization');\n if (header?.startsWith('Bearer ')) {\n sessionId = header.replace('Bearer ', '');\n }\n if (!sessionId) {\n sessionId = getCookie(request, this.cookieName);\n }\n return sessionId;\n }\n\n csrf = async (_request: Request): Promise<Response> => {\n const token = randomUUID();\n const response = Response.json({\n token,\n parameterName: '_csrf',\n headerName: 'X-XSRF-TOKEN',\n });\n\n // Double Submit Cookie Protection\n setCookie(response, 'XSRF-TOKEN', token, {\n path: '/',\n secure: true,\n httpOnly: false,\n sameSite: 'none',\n domain: this.cookieOptions.domain,\n });\n return response;\n };\n\n logout = async (request: Request): Promise<Response> => {\n const sessionId = this.getSessionId(request);\n if (sessionId) await this.repository.deleteById(sessionId);\n const response = Response.json({});\n deleteCookie(response, this.cookieName);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n // 1. get session from header or cookie\n const sessionId = this.getSessionId(request);\n if (!sessionId) return Response.json({ data: false });\n const session = await this.repository.findById(sessionId);\n if (!session) return Response.json({ data: false });\n\n // 2. get principal name from session\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n // 3. call onLogged\n if (name !== null) await onLogged?.({ name: String(name) });\n\n // 4. renew cookie\n const response = Response.json({ data: name !== null });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n\n private createPkceParameters(): PkceParameters {\n const code_verifier = randomUUID();\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n private getOauth2NonceKey(nonce: string) {\n return `oauth2:nonce:${nonce}`;\n }\n\n private getEmailVerificationCodeKey(email: string) {\n return `email:verification:${email}`;\n }\n\n private getPhoneVerificationCodeKey(phone: string) {\n return `phone:verification:${phone}`;\n }\n\n private getDesktopCodeKey(code: string) {\n return `desktop:code:${code}`;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), registrationId, 10 * 60);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), registrationId, 10 * 60);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = async (request: Request): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = await this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });\n const sessionId = this.getSessionId(request);\n const session = sessionId\n ? ((await this.repository.findById(sessionId)) ?? this.repository.createSession())\n : this.repository.createSession();\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n registrationId,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n };\n\n const value = JSON.stringify(authorizationRequest);\n session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n private readonly redirect = (error: OAuth2ErrorType, description?: string): Response => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const uri = new URL(this.oauth2Client.errorUri);\n uri.searchParams.set('error', error);\n if (description) uri.searchParams.set('error_description', description);\n return Response.redirect(uri.href, 302);\n };\n\n loginOAuth2Code = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. get session from header or cookie\n const sessionId = this.getSessionId(request);\n const session = sessionId ? await this.repository.findById(sessionId) : null;\n if (!session) {\n return this.redirect('invalid_request', 'session not found');\n }\n mark('load_session');\n\n // 2. get cached authorization request from session\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', 'authorization request not found');\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 3. validate redirect query/formdata\n const { registrationId } = param(request, PATH.LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (\n request.method === 'POST' &&\n request.headers.get('content-type')?.toLowerCase() === 'application/x-www-form-urlencoded'\n ) {\n const params = new URLSearchParams(await request.text());\n data = Object.fromEntries(params.entries());\n } else {\n data = query(request);\n }\n\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (!parsed.data.code || !parsed.data.state) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n if (registrationId !== cached.registrationId) {\n return this.redirect('invalid_request', 'redirect registration mismatch');\n }\n mark('validate_session');\n\n // 4. exchange authorization code for token and get user info\n const { code } = parsed.data;\n\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code,\n pkce: cached.additionalParameters,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n mark('exchange_code');\n\n // 5. create or update principal\n const principal = await handler(request, registrationId, userInfo, token);\n mark('save_principal');\n\n // 6. update session\n session.setLastAccessedTime(Date.now());\n session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n mark('update_session');\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('registrationId', registrationId);\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Native = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const credentials = (await request.json()) as NativeCredential;\n const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n registrationId,\n credentials,\n });\n mark('login_oauth2_native');\n\n const { nonce } = userInfo.data as { nonce?: string };\n if (nonce) {\n const value = await this.kv.getItem(this.getOauth2NonceKey(nonce));\n if (value !== registrationId) {\n const json = { error: 'invalid_request', error_description: 'nonce mismatch' };\n return Response.json(json, { status: 400 });\n }\n await this.kv.removeItem(this.getOauth2NonceKey(nonce));\n mark('check_nonce');\n }\n\n const principal = await handler(request, registrationId, userInfo, token);\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Onetap = async (\n request: Request,\n handler: AuthorizedHandler,\n registrationId = 'google'\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // should support application/x-www-form-urlencoded and application/json\n let body: Record<string, string>;\n if (request.headers.get('Content-Type')?.includes('application/x-www-form-urlencoded')) {\n const text = await request.text();\n body = Object.fromEntries(new URLSearchParams(text));\n } else if (request.headers.get('Content-Type')?.includes('application/json')) {\n body = (await request.json()) as Record<string, string>;\n } else {\n const json = { error: 'invalid_request', error_description: 'invalid content type' };\n return Response.json(json, { status: 400 });\n }\n\n const parsed = googleOneTapSchema.parse(body);\n\n // csrf token is not supported in FedCM mode\n // const csrf_cookie = getCookie(request, 'g_csrf_token');\n // const csrf_request = parsed.g_csrf_token;\n // if (!csrf_request || csrf_cookie !== csrf_request) {\n // const json = { error: 'invalid_request', error_description: 'invalid csrf token' };\n // return Response.json(json, { status: 400 });\n // }\n\n let principal: Principal;\n if (parsed.credential) {\n const { token, userInfo } = await google.getTokenInfo(parsed.credential);\n principal = await handler(request, registrationId, userInfo, token);\n } else if (parsed.code) {\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code: parsed.code,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n principal = await handler(request, registrationId, userInfo, token);\n } else {\n throw new Error('invalid request');\n }\n\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n sendEmailVerificationCode = async (\n request: Request,\n sender: (data: { email: string; verificationCode: string }) => Promise<void>\n ): Promise<Response> => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n\n const { data, error } = valid(sendEmailVerificationCodeSchema, await request.json());\n if (error) return Response.json(error, { status: error.error.code });\n const { email, turnstileToken } = data;\n\n const result = await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n if (result.error) return Response.json(result.error, { status: result.error.error.code });\n\n try {\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n await sender({ email, verificationCode });\n await this.kv.setItem(this.getEmailVerificationCodeKey(email), verificationCode, 10 * 60);\n return Response.json({});\n } catch (e) {\n console.error('Failed to send email verification code', e);\n return Response.json(invalidArgument('EMAIL_DELIVERY_FAILED'), { status: 400 });\n }\n };\n\n loginEmail = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n const { data, error } = valid(loginEmailSchema, await request.json());\n if (error) return Response.json(error, { status: error.error.code });\n const { email, verificationCode } = data;\n\n const key = this.getEmailVerificationCodeKey(email);\n const stored = await this.kv.getItem(key);\n if (\n typeof stored !== 'string' ||\n stored.length !== verificationCode.length ||\n !timingSafeEqual(Buffer.from(stored), Buffer.from(verificationCode))\n ) {\n return Response.json(invalidArgument('INVALID_VERIFICATION_CODE'), { status: 400 });\n }\n await this.kv.removeItem(key);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n const principal = await handler(request, 'EMAIL', { claims, data: {} });\n\n // save session\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n\n return response;\n };\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already\n * completed normal sign-in on the web (so a cookie session exists); this\n * issues a 5-minute single-use code that the web page hands off to the\n * desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier`\n * before opening the browser, derives `code_challenge = base64url(sha256(verifier))`,\n * and threads the challenge through the URL → web page → here. We pin\n * the challenge to the code in KV; `desktopExchange` enforces that the\n * caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet\n * capture on loopback) from exchanging it.\n *\n * Requires an authenticated cookie session — anonymous callers get 401.\n * Throttling / abuse protection on this endpoint is the API layer's job\n * (rate-limit per principal); the SDK doesn't gate it because the\n * upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = async (request: Request): Promise<Response> => {\n const principal = await this.getPrincipal(request);\n if (!principal) {\n const body = {\n error: {\n code: 401,\n status: 'UNAUTHENTICATED',\n message: 'sign in first',\n details: [],\n },\n };\n return Response.json(body, { status: 401 });\n }\n\n let body: { code_challenge?: unknown; code_challenge_method?: unknown };\n try {\n body = (await request.json()) as {\n code_challenge?: unknown;\n code_challenge_method?: unknown;\n };\n } catch {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const codeChallenge = typeof body.code_challenge === 'string' ? body.code_challenge : null;\n if (\n !codeChallenge ||\n body.code_challenge_method !== 'S256' ||\n !PKCE_FORMAT.test(codeChallenge)\n ) {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const code = randomUUID();\n const record: DesktopCodeRecord = { name: principal.name, cc: codeChallenge };\n await this.kv.setItem(this.getDesktopCodeKey(code), JSON.stringify(record), 5 * 60);\n\n return Response.json({ code });\n };\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop\n * client hands back the auth code it received via the loopback callback,\n * and we mint a fresh session for it — independent of the web cookie\n * session that originally `authorize`'d the code, so logging out on\n * either side doesn't cascade to the other.\n *\n * Cookie attributes: defaults to `SameSite=None; Secure` so the cookie\n * survives cross-site fetches from the desktop renderer's `file://` /\n * `app://` origin. The configured base `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`, `maxAge`); the override\n * is targeted at the SameSite/Secure pair only. In a non-HTTPS dev\n * backend this combination is rejected by Chromium — see the desktop\n * sign-in PRD for workarounds.\n *\n * No CSRF check needed — the request carries no cookie, the auth code\n * itself is the credential, and KV's single-use removal blocks replay.\n */\n desktopExchange = async (request: Request): Promise<Response> => {\n let body: { code?: unknown; code_verifier?: unknown };\n try {\n body = (await request.json()) as { code?: unknown; code_verifier?: unknown };\n } catch {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const code = typeof body.code === 'string' ? body.code : null;\n const verifier = typeof body.code_verifier === 'string' ? body.code_verifier : null;\n if (!code || !verifier || !PKCE_FORMAT.test(verifier)) {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const key = this.getDesktopCodeKey(code);\n const raw = await this.kv.getItem(key);\n if (typeof raw !== 'string') {\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n let record: DesktopCodeRecord;\n try {\n record = JSON.parse(raw) as DesktopCodeRecord;\n } catch {\n // Malformed KV record (shouldn't happen with our own writes) — drop it\n // and surface as an invalid code; the caller can retry the whole flow.\n await this.kv.removeItem(key);\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n\n // Verify PKCE: base64url(sha256(verifier)) === stored challenge.\n // Don't burn the code on a verifier mismatch — that would let an\n // attacker who races a network retry brick a legit exchange. The code\n // is single-use on success and 5-min TTL bounded; brute-forcing the\n // 256-bit verifier in that window is infeasible.\n const computed = createHash('sha256').update(verifier).digest();\n const stored = Buffer.from(record.cc, 'base64url');\n if (stored.length !== computed.length || !timingSafeEqual(computed, stored)) {\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n\n // One-time use: remove BEFORE creating the session so a concurrent\n // duplicate exchange (e.g. retry after timeout) sees the code as\n // consumed even if our session save races it.\n await this.kv.removeItem(key);\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, record.name);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n\n const response = Response.json({ ok: true });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n // Override sameSite/secure for desktop: the renderer's origin\n // (file:// / app://) is cross-site to the API, so SameSite=Lax/Strict\n // would block the cookie on subsequent fetches.\n sameSite: 'none',\n secure: true,\n maxAge,\n });\n return response;\n };\n\n sendPhoneVerificationCode = async (\n _request: Request,\n _sender: (data: { phone: string; verificationCode: string }) => Promise<void>\n ): Promise<Response> => {\n return Response.json({ error: 'not_implemented' }, { status: 501 });\n };\n\n loginPhone = async (_request: Request, _handler: AuthorizedHandler): Promise<Response> => {\n return Response.json({ error: 'not_implemented' }, { status: 501 });\n };\n\n kick = async (principal: Principal): Promise<void> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const sessionId of sessions.keys()) {\n await this.repository.deleteById(sessionId);\n }\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) return false;\n const session = await this.repository.findById(sessionId);\n if (!session) return false;\n const principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n if (!principalName) return false;\n return true;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) {\n if (create) {\n const session = this.repository.createSession();\n await this.repository.save(session);\n return session;\n } else {\n return null as T extends true ? Session : Session | null;\n }\n }\n const session = await this.repository.findById(sessionId);\n if (!session) {\n if (create) {\n const session = this.repository.createSession();\n await this.repository.save(session);\n return session;\n } else {\n return null as T extends true ? Session : Session | null;\n }\n }\n return session;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\n };\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) return null;\n const session = await this.repository.findById(sessionId);\n if (!session) return null;\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n if (!name) return null;\n return { name: String(name) };\n };\n\n listSessions = async (principal: Principal): Promise<Session[]> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n return Array.from(sessions.values());\n };\n\n cleanupExpiredSessions = async (cleanupCount?: number): Promise<Response> => {\n await this.repository.cleanupExpiredSessions(cleanupCount);\n return Response.json({});\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomInt, randomUUID, timingSafeEqual } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport type { Principal } from '../core/index';\nimport { loginEmailSchema, sendEmailVerificationCodeSchema } from '../email/schema';\nimport { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport type { OAuth2ErrorType } from '../oauth2/error';\nimport { google } from '../oauth2/provider/index';\nimport type {\n NativeCredential,\n OAuth2AuthorizationRequest,\n PkceParameters,\n StandardClaims,\n} from '../oauth2/types';\nimport { PRINCIPAL_NAME_INDEX_NAME } from '../session/common';\nimport type { KVRepository, Session, SessionRepository } from '../session/types';\nimport {\n type CookieOptions,\n deleteCookie,\n getCookie,\n param,\n query,\n setCookie,\n} from '../utils/http';\nimport { timing } from '../utils/timing';\nimport { invalidArgument, valid, verifyTurnstileToken } from '../utils/valid';\nimport type { AuthConfig, AuthService, AuthorizedHandler, LoggedHandler } from './types';\n\n// PKCE per RFC 7636: code_verifier / code_challenge are 43-128 chars from\n// the unreserved set. We restrict to base64url (drops \".\" and \"~\") since\n// that's what S256 produces and what callers should be sending; rejecting\n// the rest shrinks the accepted input shape without losing legit clients.\n// A 32-byte random verifier base64url-encodes to exactly 43 chars.\nconst PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;\n\ninterface DesktopCodeRecord {\n /** principal.name from the upstream cookie session */\n name: string;\n /** code_challenge (S256, base64url) — verifier must hash to this */\n cc: string;\n}\n\nexport const PATH = {\n CSRF: '/csrf',\n LOGOUT: '/logout',\n LOGGED: '/logged',\n\n OAUTH2_STATE: '/oauth2/state/:registrationId',\n OAUTH2_NONCE: '/oauth2/nonce/:registrationId',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:registrationId',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:registrationId',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:registrationId',\n LOGIN_OAUTH2_ONETAP: '/login/oauth2/onetap/google',\n\n LOGIN_EMAIL: '/login/email',\n LOGIN_PHONE: '/login/phone',\n SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\n SEND_PHONE_VERIFICATION_CODE: '/auth/sendPhoneVerificationCode',\n\n // Desktop / native app sign-in via web loopback (RFC 8252 + OAuth 2.1 Auth Code Flow):\n // 1. user already authenticated with cookie session → POST DESKTOP_AUTHORIZE → { code }\n // 2. web redirects to desktop's loopback callback with the code\n // 3. desktop renderer POSTs DESKTOP_EXCHANGE with the code → server returns\n // Set-Cookie carrying a fresh session, single-use code is invalidated\n // The two-step flow keeps the actual session id off the redirect URL — only a\n // 5-minute single-use code transits via the loopback.\n DESKTOP_AUTHORIZE: '/auth/desktop/authorize',\n DESKTOP_EXCHANGE: '/auth/desktop/exchange',\n\n CLEANUP_EXPIRED_SESSIONS: '/sessions/expired/cleanup',\n} as const;\n\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly kv: KVRepository;\n private readonly repository: SessionRepository;\n private readonly turnstileSecretKey?: string;\n private readonly oauth2Client: OAuth2Client | null;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n oauth2,\n cookie,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n }\n\n private getSessionId(request: Request): string | undefined {\n let sessionId: string | undefined;\n const header = request.headers.get('Authorization');\n if (header?.startsWith('Bearer ')) {\n sessionId = header.replace('Bearer ', '');\n }\n if (!sessionId) {\n sessionId = getCookie(request, this.cookieName);\n }\n return sessionId;\n }\n\n csrf = async (_request: Request): Promise<Response> => {\n const token = randomUUID();\n const response = Response.json({\n token,\n parameterName: '_csrf',\n headerName: 'X-XSRF-TOKEN',\n });\n\n // Double Submit Cookie Protection\n setCookie(response, 'XSRF-TOKEN', token, {\n path: '/',\n secure: true,\n httpOnly: false,\n sameSite: 'none',\n domain: this.cookieOptions.domain,\n });\n return response;\n };\n\n logout = async (request: Request): Promise<Response> => {\n const sessionId = this.getSessionId(request);\n if (sessionId) await this.repository.deleteById(sessionId);\n const response = Response.json({});\n deleteCookie(response, this.cookieName);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n // 1. get session from header or cookie\n const sessionId = this.getSessionId(request);\n if (!sessionId) return Response.json({ data: false });\n const session = await this.repository.findById(sessionId);\n if (!session) return Response.json({ data: false });\n\n // 2. get principal name from session\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n // 3. call onLogged\n if (name !== null) await onLogged?.({ name: String(name) });\n\n // 4. renew cookie\n const response = Response.json({ data: name !== null });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n\n private createPkceParameters(): PkceParameters {\n const code_verifier = randomUUID();\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n private getOauth2NonceKey(nonce: string) {\n return `oauth2:nonce:${nonce}`;\n }\n\n private getEmailVerificationCodeKey(email: string) {\n return `email:verification:${email}`;\n }\n\n private getPhoneVerificationCodeKey(phone: string) {\n return `phone:verification:${phone}`;\n }\n\n private getDesktopCodeKey(code: string) {\n return `desktop:code:${code}`;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), registrationId, 10 * 60);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), registrationId, 10 * 60);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = async (request: Request): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = await this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });\n const sessionId = this.getSessionId(request);\n const session = sessionId\n ? ((await this.repository.findById(sessionId)) ?? this.repository.createSession())\n : this.repository.createSession();\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n registrationId,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n };\n\n const value = JSON.stringify(authorizationRequest);\n session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n private readonly redirect = (error: OAuth2ErrorType, description?: string): Response => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const uri = new URL(this.oauth2Client.errorUri);\n uri.searchParams.set('error', error);\n if (description) uri.searchParams.set('error_description', description);\n return Response.redirect(uri.href, 302);\n };\n\n loginOAuth2Code = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. get session from header or cookie\n const sessionId = this.getSessionId(request);\n const session = sessionId ? await this.repository.findById(sessionId) : null;\n if (!session) {\n return this.redirect('invalid_request', 'session not found');\n }\n mark('load_session');\n\n // 2. get cached authorization request from session\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', 'authorization request not found');\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 3. validate redirect query/formdata\n const { registrationId } = param(request, PATH.LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (\n request.method === 'POST' &&\n request.headers.get('content-type')?.toLowerCase() === 'application/x-www-form-urlencoded'\n ) {\n const params = new URLSearchParams(await request.text());\n data = Object.fromEntries(params.entries());\n } else {\n data = query(request);\n }\n\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (!parsed.data.code || !parsed.data.state) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n if (registrationId !== cached.registrationId) {\n return this.redirect('invalid_request', 'redirect registration mismatch');\n }\n mark('validate_session');\n\n // 4. exchange authorization code for token and get user info\n const { code } = parsed.data;\n\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code,\n pkce: cached.additionalParameters,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n mark('exchange_code');\n\n // 5. create or update principal\n const principal = await handler(request, registrationId, userInfo, token);\n mark('save_principal');\n\n // 6. update session\n session.setLastAccessedTime(Date.now());\n session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n mark('update_session');\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('registrationId', registrationId);\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Native = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const credentials = (await request.json()) as NativeCredential;\n const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n registrationId,\n credentials,\n });\n mark('login_oauth2_native');\n\n const { nonce } = userInfo.data as { nonce?: string };\n if (nonce) {\n const value = await this.kv.getItem(this.getOauth2NonceKey(nonce));\n if (value !== registrationId) {\n const json = { error: 'invalid_request', error_description: 'nonce mismatch' };\n return Response.json(json, { status: 400 });\n }\n await this.kv.removeItem(this.getOauth2NonceKey(nonce));\n mark('check_nonce');\n }\n\n const principal = await handler(request, registrationId, userInfo, token);\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Onetap = async (\n request: Request,\n handler: AuthorizedHandler,\n registrationId = 'google'\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // should support application/x-www-form-urlencoded and application/json\n let body: Record<string, string>;\n if (request.headers.get('Content-Type')?.includes('application/x-www-form-urlencoded')) {\n const text = await request.text();\n body = Object.fromEntries(new URLSearchParams(text));\n } else if (request.headers.get('Content-Type')?.includes('application/json')) {\n body = (await request.json()) as Record<string, string>;\n } else {\n const json = { error: 'invalid_request', error_description: 'invalid content type' };\n return Response.json(json, { status: 400 });\n }\n\n const parsed = googleOneTapSchema.parse(body);\n\n // csrf token is not supported in FedCM mode\n // const csrf_cookie = getCookie(request, 'g_csrf_token');\n // const csrf_request = parsed.g_csrf_token;\n // if (!csrf_request || csrf_cookie !== csrf_request) {\n // const json = { error: 'invalid_request', error_description: 'invalid csrf token' };\n // return Response.json(json, { status: 400 });\n // }\n\n let principal: Principal;\n if (parsed.credential) {\n const { token, userInfo } = await google.getTokenInfo(parsed.credential);\n principal = await handler(request, registrationId, userInfo, token);\n } else if (parsed.code) {\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code: parsed.code,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n principal = await handler(request, registrationId, userInfo, token);\n } else {\n throw new Error('invalid request');\n }\n\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n sendEmailVerificationCode = async (\n request: Request,\n sender: (data: { email: string; verificationCode: string }) => Promise<void>\n ): Promise<Response> => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n\n const { data, error } = valid(sendEmailVerificationCodeSchema, await request.json());\n if (error) return Response.json(error, { status: error.error.code });\n const { email, turnstileToken } = data;\n\n const result = await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n if (result.error) return Response.json(result.error, { status: result.error.error.code });\n\n try {\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n await sender({ email, verificationCode });\n await this.kv.setItem(this.getEmailVerificationCodeKey(email), verificationCode, 10 * 60);\n return Response.json({});\n } catch (e) {\n console.error('Failed to send email verification code', e);\n return Response.json(invalidArgument('EMAIL_DELIVERY_FAILED'), { status: 400 });\n }\n };\n\n loginEmail = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n const { data, error } = valid(loginEmailSchema, await request.json());\n if (error) return Response.json(error, { status: error.error.code });\n const { email, verificationCode } = data;\n\n const key = this.getEmailVerificationCodeKey(email);\n const stored = await this.kv.getItem(key);\n if (\n typeof stored !== 'string' ||\n stored.length !== verificationCode.length ||\n !timingSafeEqual(Buffer.from(stored), Buffer.from(verificationCode))\n ) {\n return Response.json(invalidArgument('INVALID_VERIFICATION_CODE'), { status: 400 });\n }\n await this.kv.removeItem(key);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n const principal = await handler(request, 'EMAIL', { claims, data: {} });\n\n // save session\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n\n return response;\n };\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already\n * completed normal sign-in on the web (so a cookie session exists); this\n * issues a 5-minute single-use code that the web page hands off to the\n * desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier`\n * before opening the browser, derives `code_challenge = base64url(sha256(verifier))`,\n * and threads the challenge through the URL → web page → here. We pin\n * the challenge to the code in KV; `desktopExchange` enforces that the\n * caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet\n * capture on loopback) from exchanging it.\n *\n * Requires an authenticated cookie session — anonymous callers get 401.\n * Throttling / abuse protection on this endpoint is the API layer's job\n * (rate-limit per principal); the SDK doesn't gate it because the\n * upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = async (request: Request): Promise<Response> => {\n const principal = await this.getPrincipal(request);\n if (!principal) {\n const body = {\n error: {\n code: 401,\n status: 'UNAUTHENTICATED',\n message: 'sign in first',\n details: [],\n },\n };\n return Response.json(body, { status: 401 });\n }\n\n let body: { code_challenge?: unknown; code_challenge_method?: unknown };\n try {\n body = (await request.json()) as {\n code_challenge?: unknown;\n code_challenge_method?: unknown;\n };\n } catch {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const codeChallenge = typeof body.code_challenge === 'string' ? body.code_challenge : null;\n if (\n !codeChallenge ||\n body.code_challenge_method !== 'S256' ||\n !PKCE_FORMAT.test(codeChallenge)\n ) {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const code = randomUUID();\n const record: DesktopCodeRecord = { name: principal.name, cc: codeChallenge };\n await this.kv.setItem(this.getDesktopCodeKey(code), JSON.stringify(record), 5 * 60);\n\n return Response.json({ code });\n };\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop\n * client hands back the auth code it received via the loopback callback,\n * and we mint a fresh session for it — independent of the web cookie\n * session that originally `authorize`'d the code, so logging out on\n * either side doesn't cascade to the other.\n *\n * Cookie attributes: defaults to `SameSite=None; Secure` so the cookie\n * survives cross-site fetches from the desktop renderer's `file://` /\n * `app://` origin. The configured base `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`, `maxAge`); the override\n * is targeted at the SameSite/Secure pair only. In a non-HTTPS dev\n * backend this combination is rejected by Chromium — see the desktop\n * sign-in PRD for workarounds.\n *\n * No CSRF check needed — the request carries no cookie, the auth code\n * itself is the credential, and KV's single-use removal blocks replay.\n */\n desktopExchange = async (request: Request): Promise<Response> => {\n let body: { code?: unknown; code_verifier?: unknown };\n try {\n body = (await request.json()) as { code?: unknown; code_verifier?: unknown };\n } catch {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const code = typeof body.code === 'string' ? body.code : null;\n const verifier = typeof body.code_verifier === 'string' ? body.code_verifier : null;\n if (!code || !verifier || !PKCE_FORMAT.test(verifier)) {\n return Response.json(invalidArgument('INVALID_ARGUMENT'), { status: 400 });\n }\n\n const key = this.getDesktopCodeKey(code);\n const raw = await this.kv.getItem(key);\n if (typeof raw !== 'string') {\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n let record: DesktopCodeRecord;\n try {\n record = JSON.parse(raw) as DesktopCodeRecord;\n } catch {\n // Malformed KV record (shouldn't happen with our own writes) — drop it\n // and surface as an invalid code; the caller can retry the whole flow.\n await this.kv.removeItem(key);\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n\n // Verify PKCE: base64url(sha256(verifier)) === stored challenge.\n // Don't burn the code on a verifier mismatch — that would let an\n // attacker who races a network retry brick a legit exchange. The code\n // is single-use on success and 5-min TTL bounded; brute-forcing the\n // 256-bit verifier in that window is infeasible.\n const computed = createHash('sha256').update(verifier).digest();\n const stored = Buffer.from(record.cc, 'base64url');\n if (stored.length !== computed.length || !timingSafeEqual(computed, stored)) {\n return Response.json(invalidArgument('INVALID_DESKTOP_CODE'), { status: 400 });\n }\n\n // One-time use: remove BEFORE creating the session so a concurrent\n // duplicate exchange (e.g. retry after timeout) sees the code as\n // consumed even if our session save races it.\n await this.kv.removeItem(key);\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, record.name);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n\n const response = Response.json({ ok: true });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n // Override sameSite/secure for desktop: the renderer's origin\n // (file:// / app://) is cross-site to the API, so SameSite=Lax/Strict\n // would block the cookie on subsequent fetches.\n sameSite: 'none',\n secure: true,\n maxAge,\n });\n return response;\n };\n\n sendPhoneVerificationCode = async (\n _request: Request,\n _sender: (data: { phone: string; verificationCode: string }) => Promise<void>\n ): Promise<Response> => {\n return Response.json({ error: 'not_implemented' }, { status: 501 });\n };\n\n loginPhone = async (_request: Request, _handler: AuthorizedHandler): Promise<Response> => {\n return Response.json({ error: 'not_implemented' }, { status: 501 });\n };\n\n kick = async (principal: Principal): Promise<void> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const sessionId of sessions.keys()) {\n await this.repository.deleteById(sessionId);\n }\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) return false;\n const session = await this.repository.findById(sessionId);\n if (!session) return false;\n const principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n if (!principalName) return false;\n return true;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) {\n if (create) {\n const session = this.repository.createSession();\n await this.repository.save(session);\n return session;\n } else {\n return null as T extends true ? Session : Session | null;\n }\n }\n const session = await this.repository.findById(sessionId);\n if (!session) {\n if (create) {\n const session = this.repository.createSession();\n await this.repository.save(session);\n return session;\n } else {\n return null as T extends true ? Session : Session | null;\n }\n }\n return session;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\n };\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const sessionId = this.getSessionId(request);\n if (!sessionId) return null;\n const session = await this.repository.findById(sessionId);\n if (!session) return null;\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n if (!name) return null;\n return { name: String(name) };\n };\n\n listSessions = async (principal: Principal): Promise<Session[]> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n return Array.from(sessions.values());\n };\n\n cleanupExpiredSessions = async (cleanupCount?: number): Promise<Response> => {\n await this.repository.cleanupExpiredSessions(cleanupCount);\n return Response.json({});\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.cjs","names":[],"sources":["../../src/email/schema.ts"],"sourcesContent":["import { email, maxLength, object, regex, string } from 'zod/mini';\n\nexport const sendEmailVerificationCodeSchema = object({\n email: email().check(maxLength(320)),\n turnstileToken: string().check(maxLength(2048)),\n});\n\nexport const loginEmailSchema = object({\n email: email().check(maxLength(320)),\n verificationCode: string().check(regex(/^\\d{6}$/)),\n});\n"],"mappings":";;;AAEA,MAAa,mCAAA,
|
|
1
|
+
{"version":3,"file":"schema.cjs","names":["object","email","maxLength","string","regex"],"sources":["../../src/email/schema.ts"],"sourcesContent":["import { email, maxLength, object, regex, string } from 'zod/mini';\n\nexport const sendEmailVerificationCodeSchema = object({\n email: email().check(maxLength(320)),\n turnstileToken: string().check(maxLength(2048)),\n});\n\nexport const loginEmailSchema = object({\n email: email().check(maxLength(320)),\n verificationCode: string().check(regex(/^\\d{6}$/)),\n});\n"],"mappings":";;;AAEA,MAAa,mCAAA,GAAkCA,SAAAA,OAAAA,CAAO;CACpD,QAAA,GAAOC,SAAAA,MAAAA,CAAM,CAAC,CAAC,OAAA,GAAMC,SAAAA,UAAAA,CAAU,GAAG,CAAC;CACnC,iBAAA,GAAgBC,SAAAA,OAAAA,CAAO,CAAC,CAAC,OAAA,GAAMD,SAAAA,UAAAA,CAAU,IAAI,CAAC;AAChD,CAAC;AAED,MAAa,oBAAA,GAAmBF,SAAAA,OAAAA,CAAO;CACrC,QAAA,GAAOC,SAAAA,MAAAA,CAAM,CAAC,CAAC,OAAA,GAAMC,SAAAA,UAAAA,CAAU,GAAG,CAAC;CACnC,mBAAA,GAAkBC,SAAAA,OAAAA,CAAO,CAAC,CAAC,OAAA,GAAMC,SAAAA,MAAAA,CAAM,SAAS,CAAC;AACnD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.cjs","names":[],"sources":["../../src/oauth2/client.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { object, optional, string } from 'zod/v4-mini';\nimport type {\n NativeCredential,\n OAuth2ClientConfig,\n OAuth2Token,\n OidcToken,\n PkceParameters,\n} from './types';\n\nexport const oauth2RedirectQuerySchema = object({\n code: optional(string()),\n state: optional(string()),\n error: optional(string()),\n error_description: optional(string()),\n error_uri: optional(string()),\n});\n\nexport const googleOneTapSchema = object({\n // login_uri\n code: optional(string()),\n state: optional(string()),\n error: optional(string()),\n error_description: optional(string()),\n error_uri: optional(string()),\n nonce: optional(string()),\n g_csrf_token: optional(string()),\n hd: optional(string()),\n // callback\n credential: optional(string()),\n});\n\nexport class OAuth2Client {\n private readonly config: OAuth2ClientConfig;\n constructor(config: OAuth2ClientConfig) {\n this.config = config;\n }\n\n get baseUri() {\n return this.config.baseUri;\n }\n\n get successUri() {\n return this.config.successUri;\n }\n\n get errorUri() {\n return this.config.errorUri;\n }\n\n private async getClientConfig(registrationId: string) {\n const registrations =\n typeof this.config.registration === 'function'\n ? await this.config.registration()\n : this.config.registration;\n\n const registration = registrations[registrationId];\n invariant(registration, `Registration ${registrationId} not found`);\n\n const provider = this.config.provider[registration.provider ?? registrationId];\n invariant(provider, `Provider ${registration.provider ?? registrationId} not found`);\n return { registration, provider };\n }\n\n async createAuthorizationUri({\n registrationId,\n state,\n pkce,\n }: {\n registrationId: string;\n state: string;\n pkce?: PkceParameters;\n }): Promise<URL> {\n const { provider, registration } = await this.getClientConfig(registrationId);\n\n const { baseUri } = this.config;\n const { scope, clientId, redirectUri } = registration;\n return provider.createAuthorizationUri({\n pkce,\n state,\n scope,\n clientId,\n redirectUri: redirectUri ?? `${baseUri}/login/oauth2/code/${registrationId}`,\n });\n }\n\n async exchangeAuthorizationCode({\n registrationId,\n code,\n pkce,\n }: {\n registrationId: string;\n code: string;\n pkce?: PkceParameters;\n }): Promise<OAuth2Token> {\n const { provider, registration } = await this.getClientConfig(registrationId);\n\n const { baseUri } = this.config;\n const { clientId, clientSecret, redirectUri } = registration;\n return provider.exchangeAuthorizationCode({\n code,\n pkce,\n clientId,\n clientSecret,\n redirectUri: redirectUri ?? `${baseUri}/login/oauth2/code/${registrationId}`,\n });\n }\n\n async getUserInfo({\n registrationId,\n token,\n }: {\n registrationId: string;\n token: OAuth2Token | OidcToken;\n }) {\n const { provider } = await this.getClientConfig(registrationId);\n return provider.getUserInfo(token);\n }\n\n async refreshAccessToken({\n registrationId,\n refreshToken,\n }: {\n registrationId: string;\n refreshToken: string;\n }): Promise<OAuth2Token> {\n const { provider, registration } = await this.getClientConfig(registrationId);\n const { clientId, clientSecret } = registration;\n\n invariant(provider.refreshAccessToken, 'Provider does not support refreshAccessToken');\n return provider.refreshAccessToken({ refreshToken, clientId, clientSecret });\n }\n\n async revokeToken(registrationId: string, token: string) {\n const { provider, registration } = await this.getClientConfig(registrationId);\n const { clientId, clientSecret } = registration;\n\n invariant(provider.revokeToken, 'Provider does not support revokeToken');\n await provider.revokeToken({ token, clientId, clientSecret });\n }\n\n async loginOAuth2Native({\n registrationId,\n credentials,\n pkce,\n }: {\n registrationId: string;\n credentials: NativeCredential;\n pkce?: PkceParameters;\n }) {\n const { provider, registration } = await this.getClientConfig(registrationId);\n invariant(provider.loginOAuth2Native, 'Provider does not support loginOAuth2Native');\n const { baseUri } = this.config;\n const { clientId, clientSecret, redirectUri } = registration;\n\n return provider.loginOAuth2Native({\n clientId,\n clientSecret,\n redirectUri: redirectUri ?? `${baseUri}/login/oauth2/code/${registrationId}`,\n credentials,\n pkce,\n });\n }\n}\n"],"mappings":";;;;AAUA,MAAa,6BAAA,
|
|
1
|
+
{"version":3,"file":"client.cjs","names":["object","optional","string"],"sources":["../../src/oauth2/client.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { object, optional, string } from 'zod/v4-mini';\nimport type {\n NativeCredential,\n OAuth2ClientConfig,\n OAuth2Token,\n OidcToken,\n PkceParameters,\n} from './types';\n\nexport const oauth2RedirectQuerySchema = object({\n code: optional(string()),\n state: optional(string()),\n error: optional(string()),\n error_description: optional(string()),\n error_uri: optional(string()),\n});\n\nexport const googleOneTapSchema = object({\n // login_uri\n code: optional(string()),\n state: optional(string()),\n error: optional(string()),\n error_description: optional(string()),\n error_uri: optional(string()),\n nonce: optional(string()),\n g_csrf_token: optional(string()),\n hd: optional(string()),\n // callback\n credential: optional(string()),\n});\n\nexport class OAuth2Client {\n private readonly config: OAuth2ClientConfig;\n constructor(config: OAuth2ClientConfig) {\n this.config = config;\n }\n\n get baseUri() {\n return this.config.baseUri;\n }\n\n get successUri() {\n return this.config.successUri;\n }\n\n get errorUri() {\n return this.config.errorUri;\n }\n\n private async getClientConfig(registrationId: string) {\n const registrations =\n typeof this.config.registration === 'function'\n ? await this.config.registration()\n : this.config.registration;\n\n const registration = registrations[registrationId];\n invariant(registration, `Registration ${registrationId} not found`);\n\n const provider = this.config.provider[registration.provider ?? registrationId];\n invariant(provider, `Provider ${registration.provider ?? registrationId} not found`);\n return { registration, provider };\n }\n\n async createAuthorizationUri({\n registrationId,\n state,\n pkce,\n }: {\n registrationId: string;\n state: string;\n pkce?: PkceParameters;\n }): Promise<URL> {\n const { provider, registration } = await this.getClientConfig(registrationId);\n\n const { baseUri } = this.config;\n const { scope, clientId, redirectUri } = registration;\n return provider.createAuthorizationUri({\n pkce,\n state,\n scope,\n clientId,\n redirectUri: redirectUri ?? `${baseUri}/login/oauth2/code/${registrationId}`,\n });\n }\n\n async exchangeAuthorizationCode({\n registrationId,\n code,\n pkce,\n }: {\n registrationId: string;\n code: string;\n pkce?: PkceParameters;\n }): Promise<OAuth2Token> {\n const { provider, registration } = await this.getClientConfig(registrationId);\n\n const { baseUri } = this.config;\n const { clientId, clientSecret, redirectUri } = registration;\n return provider.exchangeAuthorizationCode({\n code,\n pkce,\n clientId,\n clientSecret,\n redirectUri: redirectUri ?? `${baseUri}/login/oauth2/code/${registrationId}`,\n });\n }\n\n async getUserInfo({\n registrationId,\n token,\n }: {\n registrationId: string;\n token: OAuth2Token | OidcToken;\n }) {\n const { provider } = await this.getClientConfig(registrationId);\n return provider.getUserInfo(token);\n }\n\n async refreshAccessToken({\n registrationId,\n refreshToken,\n }: {\n registrationId: string;\n refreshToken: string;\n }): Promise<OAuth2Token> {\n const { provider, registration } = await this.getClientConfig(registrationId);\n const { clientId, clientSecret } = registration;\n\n invariant(provider.refreshAccessToken, 'Provider does not support refreshAccessToken');\n return provider.refreshAccessToken({ refreshToken, clientId, clientSecret });\n }\n\n async revokeToken(registrationId: string, token: string) {\n const { provider, registration } = await this.getClientConfig(registrationId);\n const { clientId, clientSecret } = registration;\n\n invariant(provider.revokeToken, 'Provider does not support revokeToken');\n await provider.revokeToken({ token, clientId, clientSecret });\n }\n\n async loginOAuth2Native({\n registrationId,\n credentials,\n pkce,\n }: {\n registrationId: string;\n credentials: NativeCredential;\n pkce?: PkceParameters;\n }) {\n const { provider, registration } = await this.getClientConfig(registrationId);\n invariant(provider.loginOAuth2Native, 'Provider does not support loginOAuth2Native');\n const { baseUri } = this.config;\n const { clientId, clientSecret, redirectUri } = registration;\n\n return provider.loginOAuth2Native({\n clientId,\n clientSecret,\n redirectUri: redirectUri ?? `${baseUri}/login/oauth2/code/${registrationId}`,\n credentials,\n pkce,\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let jose = require("jose");
|
|
3
|
+
//#region src/oauth2/provider/apple-client-secret.ts
|
|
4
|
+
/**
|
|
5
|
+
* Sign in with Apple has no static client secret: the secret is a JWT the server signs with its
|
|
6
|
+
* Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and
|
|
7
|
+
* a team signs one per client id — the app's bundle id and the web Services ID share the key and
|
|
8
|
+
* differ only in `sub`.
|
|
9
|
+
*
|
|
10
|
+
* Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`
|
|
11
|
+
* seconds before expiry, so the secret can be minted by the server at runtime (an
|
|
12
|
+
* `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and
|
|
13
|
+
* rotated, as a static secret.
|
|
14
|
+
*/
|
|
15
|
+
function createAppleClientSecret(options) {
|
|
16
|
+
const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;
|
|
17
|
+
if (renewBefore >= expiresIn) throw new Error("renewBefore must be shorter than expiresIn");
|
|
18
|
+
let key;
|
|
19
|
+
const cache = /* @__PURE__ */ new Map();
|
|
20
|
+
function getKey() {
|
|
21
|
+
key ??= (0, jose.importPKCS8)(privateKey, "ES256").catch((error) => {
|
|
22
|
+
key = void 0;
|
|
23
|
+
throw error;
|
|
24
|
+
});
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
return async function appleClientSecret(clientId) {
|
|
28
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
29
|
+
const cached = cache.get(clientId);
|
|
30
|
+
if (cached && cached.exp - now > renewBefore) return cached.secret;
|
|
31
|
+
const exp = now + expiresIn;
|
|
32
|
+
const secret = await new jose.SignJWT({}).setProtectedHeader({
|
|
33
|
+
alg: "ES256",
|
|
34
|
+
kid: keyId
|
|
35
|
+
}).setIssuer(teamId).setSubject(clientId).setAudience("https://appleid.apple.com").setIssuedAt(now).setExpirationTime(exp).sign(await getKey());
|
|
36
|
+
cache.set(clientId, {
|
|
37
|
+
secret,
|
|
38
|
+
exp
|
|
39
|
+
});
|
|
40
|
+
return secret;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
exports.createAppleClientSecret = createAppleClientSecret;
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=apple-client-secret.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-client-secret.cjs","names":["importPKCS8","SignJWT"],"sources":["../../../src/oauth2/provider/apple-client-secret.ts"],"sourcesContent":["import { SignJWT, importPKCS8 } from 'jose';\n\nexport interface AppleClientSecretOptions {\n /** Apple Developer Team ID (Membership details). */\n teamId: string;\n /** Key ID of a key with Sign in with Apple enabled (Certificates, Identifiers & Profiles → Keys). */\n keyId: string;\n /** Contents of that key's `.p8` file: a PKCS#8 PEM. */\n privateKey: string;\n /** Lifetime of each signed secret, in seconds. Apple caps it at six months. Default: one hour. */\n expiresIn?: number;\n /** Re-sign once fewer than this many seconds remain, so a cached secret never reaches Apple expired. Default: five minutes. */\n renewBefore?: number;\n}\n\n/**\n * Sign in with Apple has no static client secret: the secret is a JWT the server signs with its\n * Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and\n * a team signs one per client id — the app's bundle id and the web Services ID share the key and\n * differ only in `sub`.\n *\n * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`\n * seconds before expiry, so the secret can be minted by the server at runtime (an\n * `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and\n * rotated, as a static secret.\n */\nexport function createAppleClientSecret(options: AppleClientSecretOptions) {\n const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;\n if (renewBefore >= expiresIn) {\n throw new Error('renewBefore must be shorter than expiresIn');\n }\n\n // The import is cached as a promise so concurrent first calls share one; a failed import is\n // dropped so the next call retries instead of failing forever.\n let key: Promise<CryptoKey> | undefined;\n const cache = new Map<string, { secret: string; exp: number }>();\n\n function getKey() {\n key ??= importPKCS8(privateKey, 'ES256').catch((error: unknown) => {\n key = undefined;\n throw error;\n });\n return key;\n }\n\n return async function appleClientSecret(clientId: string): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const cached = cache.get(clientId);\n if (cached && cached.exp - now > renewBefore) return cached.secret;\n\n const exp = now + expiresIn;\n const secret = await new SignJWT({})\n .setProtectedHeader({ alg: 'ES256', kid: keyId })\n .setIssuer(teamId)\n .setSubject(clientId)\n .setAudience('https://appleid.apple.com')\n .setIssuedAt(now)\n .setExpirationTime(exp)\n .sign(await getKey());\n cache.set(clientId, { secret, exp });\n return secret;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AA0BA,SAAgB,wBAAwB,SAAmC;CACzE,MAAM,EAAE,QAAQ,OAAO,YAAY,YAAY,MAAM,cAAc,QAAQ;CAC3E,IAAI,eAAe,WACjB,MAAM,IAAI,MAAM,4CAA4C;CAK9D,IAAI;CACJ,MAAM,wBAAQ,IAAI,IAA6C;CAE/D,SAAS,SAAS;EAChB,SAAA,GAAQA,KAAAA,YAAAA,CAAY,YAAY,OAAO,CAAC,CAAC,OAAO,UAAmB;GACjE,MAAM,KAAA;GACN,MAAM;EACR,CAAC;EACD,OAAO;CACT;CAEA,OAAO,eAAe,kBAAkB,UAAmC;EACzE,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;EACxC,MAAM,SAAS,MAAM,IAAI,QAAQ;EACjC,IAAI,UAAU,OAAO,MAAM,MAAM,aAAa,OAAO,OAAO;EAE5D,MAAM,MAAM,MAAM;EAClB,MAAM,SAAS,MAAM,IAAIC,KAAAA,QAAQ,CAAC,CAAC,CAAC,CACjC,mBAAmB;GAAE,KAAK;GAAS,KAAK;EAAM,CAAC,CAAC,CAChD,UAAU,MAAM,CAAC,CACjB,WAAW,QAAQ,CAAC,CACpB,YAAY,2BAA2B,CAAC,CACxC,YAAY,GAAG,CAAC,CAChB,kBAAkB,GAAG,CAAC,CACtB,KAAK,MAAM,OAAO,CAAC;EACtB,MAAM,IAAI,UAAU;GAAE;GAAQ;EAAI,CAAC;EACnC,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/oauth2/provider/apple-client-secret.d.ts
|
|
2
|
+
interface AppleClientSecretOptions {
|
|
3
|
+
/** Apple Developer Team ID (Membership details). */
|
|
4
|
+
teamId: string;
|
|
5
|
+
/** Key ID of a key with Sign in with Apple enabled (Certificates, Identifiers & Profiles → Keys). */
|
|
6
|
+
keyId: string;
|
|
7
|
+
/** Contents of that key's `.p8` file: a PKCS#8 PEM. */
|
|
8
|
+
privateKey: string;
|
|
9
|
+
/** Lifetime of each signed secret, in seconds. Apple caps it at six months. Default: one hour. */
|
|
10
|
+
expiresIn?: number;
|
|
11
|
+
/** Re-sign once fewer than this many seconds remain, so a cached secret never reaches Apple expired. Default: five minutes. */
|
|
12
|
+
renewBefore?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sign in with Apple has no static client secret: the secret is a JWT the server signs with its
|
|
16
|
+
* Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and
|
|
17
|
+
* a team signs one per client id — the app's bundle id and the web Services ID share the key and
|
|
18
|
+
* differ only in `sub`.
|
|
19
|
+
*
|
|
20
|
+
* Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`
|
|
21
|
+
* seconds before expiry, so the secret can be minted by the server at runtime (an
|
|
22
|
+
* `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and
|
|
23
|
+
* rotated, as a static secret.
|
|
24
|
+
*/
|
|
25
|
+
declare function createAppleClientSecret(options: AppleClientSecretOptions): (clientId: string) => Promise<string>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { AppleClientSecretOptions, createAppleClientSecret };
|
|
28
|
+
//# sourceMappingURL=apple-client-secret.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-client-secret.d.cts","names":[],"sources":["../../../src/oauth2/provider/apple-client-secret.ts"],"mappings":";UAEiB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;iBAcc,wBAAwB,SAAS,4BAAwB,qBAmBZ"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/oauth2/provider/apple-client-secret.d.ts
|
|
2
|
+
interface AppleClientSecretOptions {
|
|
3
|
+
/** Apple Developer Team ID (Membership details). */
|
|
4
|
+
teamId: string;
|
|
5
|
+
/** Key ID of a key with Sign in with Apple enabled (Certificates, Identifiers & Profiles → Keys). */
|
|
6
|
+
keyId: string;
|
|
7
|
+
/** Contents of that key's `.p8` file: a PKCS#8 PEM. */
|
|
8
|
+
privateKey: string;
|
|
9
|
+
/** Lifetime of each signed secret, in seconds. Apple caps it at six months. Default: one hour. */
|
|
10
|
+
expiresIn?: number;
|
|
11
|
+
/** Re-sign once fewer than this many seconds remain, so a cached secret never reaches Apple expired. Default: five minutes. */
|
|
12
|
+
renewBefore?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sign in with Apple has no static client secret: the secret is a JWT the server signs with its
|
|
16
|
+
* Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and
|
|
17
|
+
* a team signs one per client id — the app's bundle id and the web Services ID share the key and
|
|
18
|
+
* differ only in `sub`.
|
|
19
|
+
*
|
|
20
|
+
* Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`
|
|
21
|
+
* seconds before expiry, so the secret can be minted by the server at runtime (an
|
|
22
|
+
* `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and
|
|
23
|
+
* rotated, as a static secret.
|
|
24
|
+
*/
|
|
25
|
+
declare function createAppleClientSecret(options: AppleClientSecretOptions): (clientId: string) => Promise<string>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { AppleClientSecretOptions, createAppleClientSecret };
|
|
28
|
+
//# sourceMappingURL=apple-client-secret.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-client-secret.d.mts","names":[],"sources":["../../../src/oauth2/provider/apple-client-secret.ts"],"mappings":";UAEiB;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;iBAcc,wBAAwB,SAAS,4BAAwB,qBAmBZ"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { SignJWT, importPKCS8 } from "jose";
|
|
2
|
+
//#region src/oauth2/provider/apple-client-secret.ts
|
|
3
|
+
/**
|
|
4
|
+
* Sign in with Apple has no static client secret: the secret is a JWT the server signs with its
|
|
5
|
+
* Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and
|
|
6
|
+
* a team signs one per client id — the app's bundle id and the web Services ID share the key and
|
|
7
|
+
* differ only in `sub`.
|
|
8
|
+
*
|
|
9
|
+
* Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`
|
|
10
|
+
* seconds before expiry, so the secret can be minted by the server at runtime (an
|
|
11
|
+
* `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and
|
|
12
|
+
* rotated, as a static secret.
|
|
13
|
+
*/
|
|
14
|
+
function createAppleClientSecret(options) {
|
|
15
|
+
const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;
|
|
16
|
+
if (renewBefore >= expiresIn) throw new Error("renewBefore must be shorter than expiresIn");
|
|
17
|
+
let key;
|
|
18
|
+
const cache = /* @__PURE__ */ new Map();
|
|
19
|
+
function getKey() {
|
|
20
|
+
key ??= importPKCS8(privateKey, "ES256").catch((error) => {
|
|
21
|
+
key = void 0;
|
|
22
|
+
throw error;
|
|
23
|
+
});
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
26
|
+
return async function appleClientSecret(clientId) {
|
|
27
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
28
|
+
const cached = cache.get(clientId);
|
|
29
|
+
if (cached && cached.exp - now > renewBefore) return cached.secret;
|
|
30
|
+
const exp = now + expiresIn;
|
|
31
|
+
const secret = await new SignJWT({}).setProtectedHeader({
|
|
32
|
+
alg: "ES256",
|
|
33
|
+
kid: keyId
|
|
34
|
+
}).setIssuer(teamId).setSubject(clientId).setAudience("https://appleid.apple.com").setIssuedAt(now).setExpirationTime(exp).sign(await getKey());
|
|
35
|
+
cache.set(clientId, {
|
|
36
|
+
secret,
|
|
37
|
+
exp
|
|
38
|
+
});
|
|
39
|
+
return secret;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { createAppleClientSecret };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=apple-client-secret.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apple-client-secret.mjs","names":[],"sources":["../../../src/oauth2/provider/apple-client-secret.ts"],"sourcesContent":["import { SignJWT, importPKCS8 } from 'jose';\n\nexport interface AppleClientSecretOptions {\n /** Apple Developer Team ID (Membership details). */\n teamId: string;\n /** Key ID of a key with Sign in with Apple enabled (Certificates, Identifiers & Profiles → Keys). */\n keyId: string;\n /** Contents of that key's `.p8` file: a PKCS#8 PEM. */\n privateKey: string;\n /** Lifetime of each signed secret, in seconds. Apple caps it at six months. Default: one hour. */\n expiresIn?: number;\n /** Re-sign once fewer than this many seconds remain, so a cached secret never reaches Apple expired. Default: five minutes. */\n renewBefore?: number;\n}\n\n/**\n * Sign in with Apple has no static client secret: the secret is a JWT the server signs with its\n * Sign in with Apple key (ES256, `iss` = team, `sub` = client id, `aud` = appleid.apple.com), and\n * a team signs one per client id — the app's bundle id and the web Services ID share the key and\n * differ only in `sub`.\n *\n * Returns `clientId => secret`, signing on demand and caching per client id until `renewBefore`\n * seconds before expiry, so the secret can be minted by the server at runtime (an\n * `OAuth2ClientConfig.registration` function) instead of being generated by hand and stored, and\n * rotated, as a static secret.\n */\nexport function createAppleClientSecret(options: AppleClientSecretOptions) {\n const { teamId, keyId, privateKey, expiresIn = 3600, renewBefore = 300 } = options;\n if (renewBefore >= expiresIn) {\n throw new Error('renewBefore must be shorter than expiresIn');\n }\n\n // The import is cached as a promise so concurrent first calls share one; a failed import is\n // dropped so the next call retries instead of failing forever.\n let key: Promise<CryptoKey> | undefined;\n const cache = new Map<string, { secret: string; exp: number }>();\n\n function getKey() {\n key ??= importPKCS8(privateKey, 'ES256').catch((error: unknown) => {\n key = undefined;\n throw error;\n });\n return key;\n }\n\n return async function appleClientSecret(clientId: string): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const cached = cache.get(clientId);\n if (cached && cached.exp - now > renewBefore) return cached.secret;\n\n const exp = now + expiresIn;\n const secret = await new SignJWT({})\n .setProtectedHeader({ alg: 'ES256', kid: keyId })\n .setIssuer(teamId)\n .setSubject(clientId)\n .setAudience('https://appleid.apple.com')\n .setIssuedAt(now)\n .setExpirationTime(exp)\n .sign(await getKey());\n cache.set(clientId, { secret, exp });\n return secret;\n };\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,SAAgB,wBAAwB,SAAmC;CACzE,MAAM,EAAE,QAAQ,OAAO,YAAY,YAAY,MAAM,cAAc,QAAQ;CAC3E,IAAI,eAAe,WACjB,MAAM,IAAI,MAAM,4CAA4C;CAK9D,IAAI;CACJ,MAAM,wBAAQ,IAAI,IAA6C;CAE/D,SAAS,SAAS;EAChB,QAAQ,YAAY,YAAY,OAAO,CAAC,CAAC,OAAO,UAAmB;GACjE,MAAM,KAAA;GACN,MAAM;EACR,CAAC;EACD,OAAO;CACT;CAEA,OAAO,eAAe,kBAAkB,UAAmC;EACzE,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;EACxC,MAAM,SAAS,MAAM,IAAI,QAAQ;EACjC,IAAI,UAAU,OAAO,MAAM,MAAM,aAAa,OAAO,OAAO;EAE5D,MAAM,MAAM,MAAM;EAClB,MAAM,SAAS,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CACjC,mBAAmB;GAAE,KAAK;GAAS,KAAK;EAAM,CAAC,CAAC,CAChD,UAAU,MAAM,CAAC,CACjB,WAAW,QAAQ,CAAC,CACpB,YAAY,2BAA2B,CAAC,CACxC,YAAY,GAAG,CAAC,CAChB,kBAAkB,GAAG,CAAC,CACtB,KAAK,MAAM,OAAO,CAAC;EACtB,MAAM,IAAI,UAAU;GAAE;GAAQ;EAAI,CAAC;EACnC,OAAO;CACT;AACF"}
|
|
@@ -28,7 +28,7 @@ async function exchangeAuthorizationCode(params) {
|
|
|
28
28
|
if (params.authentication === "basic") headers.Authorization = `Basic ${Buffer.from(`${params.clientId}:${params.clientSecret}`).toString("base64")}`;
|
|
29
29
|
else {
|
|
30
30
|
body.append("client_id", params.clientId);
|
|
31
|
-
body.append("client_secret", params.clientSecret);
|
|
31
|
+
if (params.clientSecret) body.append("client_secret", params.clientSecret);
|
|
32
32
|
}
|
|
33
33
|
return fetch(params.tokenUri, {
|
|
34
34
|
method: "POST",
|
|
@@ -80,7 +80,8 @@ async function revokeToken(params) {
|
|
|
80
80
|
}
|
|
81
81
|
async function verifyIdToken(idToken, jwkSetUri) {
|
|
82
82
|
try {
|
|
83
|
-
const
|
|
83
|
+
const jwks = (0, jose.createRemoteJWKSet)(new URL(jwkSetUri));
|
|
84
|
+
const { payload } = await (0, jose.jwtVerify)(idToken, jwks);
|
|
84
85
|
return payload;
|
|
85
86
|
} catch {
|
|
86
87
|
console.error("Failed to verify id_token");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.cjs","names":["OAuth2Error"],"sources":["../../../src/oauth2/provider/common.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from 'jose';\nimport { OAuth2Error } from '../error';\nimport type { ExchangeCodeParams, PkceParameters, RefreshTokenParams } from '../types';\n\nexport function createAuthorizationUri(options: {\n state: string;\n scope: string[];\n clientId: string;\n redirectUri: string;\n authorizationUri: string;\n pkce?: Omit<PkceParameters, 'code_verifier'>;\n}) {\n const url = new URL(options.authorizationUri);\n url.searchParams.append('response_type', 'code');\n url.searchParams.append('client_id', options.clientId);\n url.searchParams.append('state', options.state);\n url.searchParams.append('scope', options.scope.join(' '));\n url.searchParams.append('redirect_uri', options.redirectUri);\n if (options.pkce) {\n url.searchParams.append('code_challenge_method', options.pkce.code_challenge_method);\n url.searchParams.append('code_challenge', options.pkce.code_challenge);\n }\n return url;\n}\n\nexport async function exchangeAuthorizationCode(\n params: ExchangeCodeParams & {\n tokenUri: string;\n authentication?: 'basic' | 'post';\n }\n) {\n const body = new URLSearchParams();\n body.append('code', params.code);\n body.append('redirect_uri', params.redirectUri);\n body.append('grant_type', 'authorization_code');\n if (params.pkce) body.append('code_verifier', params.pkce.code_verifier);\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n return fetch(params.tokenUri, { method: 'POST', headers, body });\n}\n\nexport async function refreshAccessToken(\n params: RefreshTokenParams & {\n tokenUri: string;\n authentication?: 'basic' | 'post';\n }\n) {\n const body = new URLSearchParams();\n body.append('grant_type', 'refresh_token');\n body.append('refresh_token', params.refreshToken);\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n return fetch(params.tokenUri, { method: 'POST', headers, body });\n}\n\ninterface RevokeTokenParams {\n token: string;\n clientId: string;\n clientSecret: string;\n tokenRevokeUri: string;\n authentication?: 'basic' | 'post';\n}\n\nexport async function revokeToken(params: RevokeTokenParams) {\n const body = new URLSearchParams();\n body.append('token', params.token);\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n const response = await fetch(params.tokenRevokeUri, { method: 'POST', headers, body });\n if (!response.ok) {\n const error = await response.json();\n console.error('Refresh token error:', error);\n throw new OAuth2Error(response.status, 'invalid_request', 'Failed to revoke token');\n }\n}\n\nexport async function verifyIdToken<T>(idToken: string, jwkSetUri: string) {\n try {\n const jwks = createRemoteJWKSet(new URL(jwkSetUri));\n const { payload } = await jwtVerify(idToken, jwks);\n return payload as T;\n } catch {\n console.error('Failed to verify id_token');\n throw new OAuth2Error(400, 'invalid_request', 'Failed to verify id_token');\n }\n}\n"],"mappings":";;;;AAIA,SAAgB,uBAAuB,SAOpC;CACD,MAAM,MAAM,IAAI,IAAI,QAAQ,gBAAgB;CAC5C,IAAI,aAAa,OAAO,iBAAiB,MAAM;CAC/C,IAAI,aAAa,OAAO,aAAa,QAAQ,QAAQ;CACrD,IAAI,aAAa,OAAO,SAAS,QAAQ,KAAK;CAC9C,IAAI,aAAa,OAAO,SAAS,QAAQ,MAAM,KAAK,GAAG,CAAC;CACxD,IAAI,aAAa,OAAO,gBAAgB,QAAQ,WAAW;CAC3D,IAAI,QAAQ,MAAM;EAChB,IAAI,aAAa,OAAO,yBAAyB,QAAQ,KAAK,qBAAqB;EACnF,IAAI,aAAa,OAAO,kBAAkB,QAAQ,KAAK,cAAc;CACvE;CACA,OAAO;AACT;AAEA,eAAsB,0BACpB,QAIA;CACA,MAAM,OAAO,IAAI,gBAAgB;CACjC,KAAK,OAAO,QAAQ,OAAO,IAAI;CAC/B,KAAK,OAAO,gBAAgB,OAAO,WAAW;CAC9C,KAAK,OAAO,cAAc,oBAAoB;CAC9C,IAAI,OAAO,MAAM,KAAK,OAAO,iBAAiB,OAAO,KAAK,aAAa;CAEvE,MAAM,UAAkC;EACtC,QAAQ;EACR,gBAAgB;CAClB;CACA,IAAI,OAAO,mBAAmB,SAE5B,QAAQ,gBAAgB,SADV,OAAO,KAAK,GAAG,OAAO,SAAS,GAAG,OAAO,cAAc,CAAC,CAAC,SAAS,QAC3C;MAChC;EACL,KAAK,OAAO,aAAa,OAAO,QAAQ;
|
|
1
|
+
{"version":3,"file":"common.cjs","names":["OAuth2Error","createRemoteJWKSet","jwtVerify"],"sources":["../../../src/oauth2/provider/common.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from 'jose';\nimport { OAuth2Error } from '../error';\nimport type { ExchangeCodeParams, PkceParameters, RefreshTokenParams } from '../types';\n\nexport function createAuthorizationUri(options: {\n state: string;\n scope: string[];\n clientId: string;\n redirectUri: string;\n authorizationUri: string;\n pkce?: Omit<PkceParameters, 'code_verifier'>;\n}) {\n const url = new URL(options.authorizationUri);\n url.searchParams.append('response_type', 'code');\n url.searchParams.append('client_id', options.clientId);\n url.searchParams.append('state', options.state);\n url.searchParams.append('scope', options.scope.join(' '));\n url.searchParams.append('redirect_uri', options.redirectUri);\n if (options.pkce) {\n url.searchParams.append('code_challenge_method', options.pkce.code_challenge_method);\n url.searchParams.append('code_challenge', options.pkce.code_challenge);\n }\n return url;\n}\n\nexport async function exchangeAuthorizationCode(\n params: ExchangeCodeParams & {\n tokenUri: string;\n authentication?: 'basic' | 'post';\n }\n) {\n const body = new URLSearchParams();\n body.append('code', params.code);\n body.append('redirect_uri', params.redirectUri);\n body.append('grant_type', 'authorization_code');\n if (params.pkce) body.append('code_verifier', params.pkce.code_verifier);\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n // Public clients (Google's installed-app clients, PKCE-only clients) have no secret.\n if (params.clientSecret) body.append('client_secret', params.clientSecret);\n }\n\n return fetch(params.tokenUri, { method: 'POST', headers, body });\n}\n\nexport async function refreshAccessToken(\n params: RefreshTokenParams & {\n tokenUri: string;\n authentication?: 'basic' | 'post';\n }\n) {\n const body = new URLSearchParams();\n body.append('grant_type', 'refresh_token');\n body.append('refresh_token', params.refreshToken);\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n return fetch(params.tokenUri, { method: 'POST', headers, body });\n}\n\ninterface RevokeTokenParams {\n token: string;\n clientId: string;\n clientSecret: string;\n tokenRevokeUri: string;\n authentication?: 'basic' | 'post';\n}\n\nexport async function revokeToken(params: RevokeTokenParams) {\n const body = new URLSearchParams();\n body.append('token', params.token);\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n const response = await fetch(params.tokenRevokeUri, { method: 'POST', headers, body });\n if (!response.ok) {\n const error = await response.json();\n console.error('Refresh token error:', error);\n throw new OAuth2Error(response.status, 'invalid_request', 'Failed to revoke token');\n }\n}\n\nexport async function verifyIdToken<T>(idToken: string, jwkSetUri: string) {\n try {\n const jwks = createRemoteJWKSet(new URL(jwkSetUri));\n const { payload } = await jwtVerify(idToken, jwks);\n return payload as T;\n } catch {\n console.error('Failed to verify id_token');\n throw new OAuth2Error(400, 'invalid_request', 'Failed to verify id_token');\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.cts","names":[],"sources":["../../../src/oauth2/provider/common.ts"],"mappings":";;iBAIgB,uBAAuB;EACrC;EACA;EACA;EACA;EACA;EACA,OAAO,KAAK;IACb;iBAcqB,0BACpB,QAAQ;EACN;EACA;IACD,QAAA;
|
|
1
|
+
{"version":3,"file":"common.d.cts","names":[],"sources":["../../../src/oauth2/provider/common.ts"],"mappings":";;iBAIgB,uBAAuB;EACrC;EACA;EACA;EACA;EACA;EACA,OAAO,KAAK;IACb;iBAcqB,0BACpB,QAAQ;EACN;EACA;IACD,QAAA;iBAwBmB,mBACpB,QAAQ;EACN;EACA;IACD,QAAA;UAsBO;EACR;EACA;EACA;EACA;EACA;;iBAGoB,YAAY,QAAQ,oBAAiB;iBAwBrC,cAAc,GAAG,iBAAiB,oBAAiB,QAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.mts","names":[],"sources":["../../../src/oauth2/provider/common.ts"],"mappings":";;iBAIgB,uBAAuB;EACrC;EACA;EACA;EACA;EACA;EACA,OAAO,KAAK;IACb;iBAcqB,0BACpB,QAAQ;EACN;EACA;IACD,QAAA;
|
|
1
|
+
{"version":3,"file":"common.d.mts","names":[],"sources":["../../../src/oauth2/provider/common.ts"],"mappings":";;iBAIgB,uBAAuB;EACrC;EACA;EACA;EACA;EACA;EACA,OAAO,KAAK;IACb;iBAcqB,0BACpB,QAAQ;EACN;EACA;IACD,QAAA;iBAwBmB,mBACpB,QAAQ;EACN;EACA;IACD,QAAA;UAsBO;EACR;EACA;EACA;EACA;EACA;;iBAGoB,YAAY,QAAQ,oBAAiB;iBAwBrC,cAAc,GAAG,iBAAiB,oBAAiB,QAAA"}
|
|
@@ -27,7 +27,7 @@ async function exchangeAuthorizationCode(params) {
|
|
|
27
27
|
if (params.authentication === "basic") headers.Authorization = `Basic ${Buffer.from(`${params.clientId}:${params.clientSecret}`).toString("base64")}`;
|
|
28
28
|
else {
|
|
29
29
|
body.append("client_id", params.clientId);
|
|
30
|
-
body.append("client_secret", params.clientSecret);
|
|
30
|
+
if (params.clientSecret) body.append("client_secret", params.clientSecret);
|
|
31
31
|
}
|
|
32
32
|
return fetch(params.tokenUri, {
|
|
33
33
|
method: "POST",
|
|
@@ -79,7 +79,8 @@ async function revokeToken(params) {
|
|
|
79
79
|
}
|
|
80
80
|
async function verifyIdToken(idToken, jwkSetUri) {
|
|
81
81
|
try {
|
|
82
|
-
const
|
|
82
|
+
const jwks = createRemoteJWKSet(new URL(jwkSetUri));
|
|
83
|
+
const { payload } = await jwtVerify(idToken, jwks);
|
|
83
84
|
return payload;
|
|
84
85
|
} catch {
|
|
85
86
|
console.error("Failed to verify id_token");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.mjs","names":[],"sources":["../../../src/oauth2/provider/common.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from 'jose';\nimport { OAuth2Error } from '../error';\nimport type { ExchangeCodeParams, PkceParameters, RefreshTokenParams } from '../types';\n\nexport function createAuthorizationUri(options: {\n state: string;\n scope: string[];\n clientId: string;\n redirectUri: string;\n authorizationUri: string;\n pkce?: Omit<PkceParameters, 'code_verifier'>;\n}) {\n const url = new URL(options.authorizationUri);\n url.searchParams.append('response_type', 'code');\n url.searchParams.append('client_id', options.clientId);\n url.searchParams.append('state', options.state);\n url.searchParams.append('scope', options.scope.join(' '));\n url.searchParams.append('redirect_uri', options.redirectUri);\n if (options.pkce) {\n url.searchParams.append('code_challenge_method', options.pkce.code_challenge_method);\n url.searchParams.append('code_challenge', options.pkce.code_challenge);\n }\n return url;\n}\n\nexport async function exchangeAuthorizationCode(\n params: ExchangeCodeParams & {\n tokenUri: string;\n authentication?: 'basic' | 'post';\n }\n) {\n const body = new URLSearchParams();\n body.append('code', params.code);\n body.append('redirect_uri', params.redirectUri);\n body.append('grant_type', 'authorization_code');\n if (params.pkce) body.append('code_verifier', params.pkce.code_verifier);\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n return fetch(params.tokenUri, { method: 'POST', headers, body });\n}\n\nexport async function refreshAccessToken(\n params: RefreshTokenParams & {\n tokenUri: string;\n authentication?: 'basic' | 'post';\n }\n) {\n const body = new URLSearchParams();\n body.append('grant_type', 'refresh_token');\n body.append('refresh_token', params.refreshToken);\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n return fetch(params.tokenUri, { method: 'POST', headers, body });\n}\n\ninterface RevokeTokenParams {\n token: string;\n clientId: string;\n clientSecret: string;\n tokenRevokeUri: string;\n authentication?: 'basic' | 'post';\n}\n\nexport async function revokeToken(params: RevokeTokenParams) {\n const body = new URLSearchParams();\n body.append('token', params.token);\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n const response = await fetch(params.tokenRevokeUri, { method: 'POST', headers, body });\n if (!response.ok) {\n const error = await response.json();\n console.error('Refresh token error:', error);\n throw new OAuth2Error(response.status, 'invalid_request', 'Failed to revoke token');\n }\n}\n\nexport async function verifyIdToken<T>(idToken: string, jwkSetUri: string) {\n try {\n const jwks = createRemoteJWKSet(new URL(jwkSetUri));\n const { payload } = await jwtVerify(idToken, jwks);\n return payload as T;\n } catch {\n console.error('Failed to verify id_token');\n throw new OAuth2Error(400, 'invalid_request', 'Failed to verify id_token');\n }\n}\n"],"mappings":";;;AAIA,SAAgB,uBAAuB,SAOpC;CACD,MAAM,MAAM,IAAI,IAAI,QAAQ,gBAAgB;CAC5C,IAAI,aAAa,OAAO,iBAAiB,MAAM;CAC/C,IAAI,aAAa,OAAO,aAAa,QAAQ,QAAQ;CACrD,IAAI,aAAa,OAAO,SAAS,QAAQ,KAAK;CAC9C,IAAI,aAAa,OAAO,SAAS,QAAQ,MAAM,KAAK,GAAG,CAAC;CACxD,IAAI,aAAa,OAAO,gBAAgB,QAAQ,WAAW;CAC3D,IAAI,QAAQ,MAAM;EAChB,IAAI,aAAa,OAAO,yBAAyB,QAAQ,KAAK,qBAAqB;EACnF,IAAI,aAAa,OAAO,kBAAkB,QAAQ,KAAK,cAAc;CACvE;CACA,OAAO;AACT;AAEA,eAAsB,0BACpB,QAIA;CACA,MAAM,OAAO,IAAI,gBAAgB;CACjC,KAAK,OAAO,QAAQ,OAAO,IAAI;CAC/B,KAAK,OAAO,gBAAgB,OAAO,WAAW;CAC9C,KAAK,OAAO,cAAc,oBAAoB;CAC9C,IAAI,OAAO,MAAM,KAAK,OAAO,iBAAiB,OAAO,KAAK,aAAa;CAEvE,MAAM,UAAkC;EACtC,QAAQ;EACR,gBAAgB;CAClB;CACA,IAAI,OAAO,mBAAmB,SAE5B,QAAQ,gBAAgB,SADV,OAAO,KAAK,GAAG,OAAO,SAAS,GAAG,OAAO,cAAc,CAAC,CAAC,SAAS,QAC3C;MAChC;EACL,KAAK,OAAO,aAAa,OAAO,QAAQ;
|
|
1
|
+
{"version":3,"file":"common.mjs","names":[],"sources":["../../../src/oauth2/provider/common.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from 'jose';\nimport { OAuth2Error } from '../error';\nimport type { ExchangeCodeParams, PkceParameters, RefreshTokenParams } from '../types';\n\nexport function createAuthorizationUri(options: {\n state: string;\n scope: string[];\n clientId: string;\n redirectUri: string;\n authorizationUri: string;\n pkce?: Omit<PkceParameters, 'code_verifier'>;\n}) {\n const url = new URL(options.authorizationUri);\n url.searchParams.append('response_type', 'code');\n url.searchParams.append('client_id', options.clientId);\n url.searchParams.append('state', options.state);\n url.searchParams.append('scope', options.scope.join(' '));\n url.searchParams.append('redirect_uri', options.redirectUri);\n if (options.pkce) {\n url.searchParams.append('code_challenge_method', options.pkce.code_challenge_method);\n url.searchParams.append('code_challenge', options.pkce.code_challenge);\n }\n return url;\n}\n\nexport async function exchangeAuthorizationCode(\n params: ExchangeCodeParams & {\n tokenUri: string;\n authentication?: 'basic' | 'post';\n }\n) {\n const body = new URLSearchParams();\n body.append('code', params.code);\n body.append('redirect_uri', params.redirectUri);\n body.append('grant_type', 'authorization_code');\n if (params.pkce) body.append('code_verifier', params.pkce.code_verifier);\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n // Public clients (Google's installed-app clients, PKCE-only clients) have no secret.\n if (params.clientSecret) body.append('client_secret', params.clientSecret);\n }\n\n return fetch(params.tokenUri, { method: 'POST', headers, body });\n}\n\nexport async function refreshAccessToken(\n params: RefreshTokenParams & {\n tokenUri: string;\n authentication?: 'basic' | 'post';\n }\n) {\n const body = new URLSearchParams();\n body.append('grant_type', 'refresh_token');\n body.append('refresh_token', params.refreshToken);\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n return fetch(params.tokenUri, { method: 'POST', headers, body });\n}\n\ninterface RevokeTokenParams {\n token: string;\n clientId: string;\n clientSecret: string;\n tokenRevokeUri: string;\n authentication?: 'basic' | 'post';\n}\n\nexport async function revokeToken(params: RevokeTokenParams) {\n const body = new URLSearchParams();\n body.append('token', params.token);\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/x-www-form-urlencoded',\n };\n\n if (params.authentication === 'basic') {\n const token = Buffer.from(`${params.clientId}:${params.clientSecret}`).toString('base64');\n headers.Authorization = `Basic ${token}`;\n } else {\n body.append('client_id', params.clientId);\n body.append('client_secret', params.clientSecret);\n }\n\n const response = await fetch(params.tokenRevokeUri, { method: 'POST', headers, body });\n if (!response.ok) {\n const error = await response.json();\n console.error('Refresh token error:', error);\n throw new OAuth2Error(response.status, 'invalid_request', 'Failed to revoke token');\n }\n}\n\nexport async function verifyIdToken<T>(idToken: string, jwkSetUri: string) {\n try {\n const jwks = createRemoteJWKSet(new URL(jwkSetUri));\n const { payload } = await jwtVerify(idToken, jwks);\n return payload as T;\n } catch {\n console.error('Failed to verify id_token');\n throw new OAuth2Error(400, 'invalid_request', 'Failed to verify id_token');\n }\n}\n"],"mappings":"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"}
|
|
@@ -3,6 +3,10 @@ const require_oauth2_error = require("../error.cjs");
|
|
|
3
3
|
const require_oauth2_provider_common = require("./common.cjs");
|
|
4
4
|
let _shware_utils = require("@shware/utils");
|
|
5
5
|
//#region src/oauth2/provider/google.ts
|
|
6
|
+
/** The Google Cloud project number every client id of a project starts with: `<number>-<hash>.apps.googleusercontent.com`. */
|
|
7
|
+
function projectNumber(clientId) {
|
|
8
|
+
return clientId.split("-", 1)[0];
|
|
9
|
+
}
|
|
6
10
|
function createGoogleProvider() {
|
|
7
11
|
return {
|
|
8
12
|
authorizationUri: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
@@ -55,10 +59,14 @@ function createGoogleProvider() {
|
|
|
55
59
|
(0, _shware_utils.invariant)(credentials.code, "code is required");
|
|
56
60
|
const { code } = credentials;
|
|
57
61
|
const { tokenUri } = this;
|
|
62
|
+
const { client_id = params.clientId, redirect_uri = params.redirectUri } = credentials;
|
|
63
|
+
if (projectNumber(client_id) !== projectNumber(params.clientId)) throw new require_oauth2_error.OAuth2Error(400, "invalid_client", "client_id belongs to another project");
|
|
58
64
|
const response = await require_oauth2_provider_common.exchangeAuthorizationCode({
|
|
59
65
|
code,
|
|
60
66
|
tokenUri,
|
|
61
|
-
|
|
67
|
+
clientId: client_id,
|
|
68
|
+
clientSecret: client_id === params.clientId ? params.clientSecret : "",
|
|
69
|
+
redirectUri: redirect_uri
|
|
62
70
|
});
|
|
63
71
|
if (!response.ok) {
|
|
64
72
|
const { error, error_description } = await response.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google.cjs","names":["createAuthorizationUri","exchangeAuthorizationCode","OAuth2Error","verifyIdToken"],"sources":["../../../src/oauth2/provider/google.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { OAuth2Error } from '../error';\nimport type {\n LoginOAuth2NativeParams,\n NativeCredential,\n OAuth2Token,\n OneTapProvider,\n} from '../types';\nimport { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from './common';\n\nexport function createGoogleProvider(): OneTapProvider {\n return {\n authorizationUri: 'https://accounts.google.com/o/oauth2/v2/auth',\n tokenUri: 'https://oauth2.googleapis.com/token',\n userInfoUri: 'https://www.googleapis.com/oauth2/v3/userinfo',\n userNameAttribute: 'sub',\n jwkSetUri: 'https://www.googleapis.com/oauth2/v3/certs',\n // https://developers.google.com/identity/protocols/oauth2/scopes#oauth2\n defaultScope: [\n 'https://www.googleapis.com/auth/userinfo.email',\n 'https://www.googleapis.com/auth/userinfo.profile',\n 'openid',\n ],\n createAuthorizationUri(params) {\n return createAuthorizationUri({\n ...params,\n scope: params.scope ?? this.defaultScope,\n authorizationUri: this.authorizationUri,\n });\n },\n async exchangeAuthorizationCode(params) {\n const response = await exchangeAuthorizationCode({ ...params, tokenUri: this.tokenUri });\n if (!response.ok) {\n const { error, error_description } = (await response.json()) as GoogleErrorResponse;\n throw new OAuth2Error(response.status, error, error_description);\n }\n return (await response.json()) as GoogleToken;\n },\n async getUserInfo({ id_token }) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n const data = await verifyIdToken<GoogleUserInfo>(id_token, this.jwkSetUri);\n return {\n data,\n claims: {\n sub: data.sub,\n name: data.name,\n picture: data.picture,\n email: data.email,\n email_verified: data.email_verified,\n given_name: data.given_name,\n family_name: data.family_name,\n locale: data.locale,\n },\n };\n },\n async loginOAuth2Native({ pkce: _, credentials, ...params }: LoginOAuth2NativeParams) {\n invariant(credentials.code, 'code is required');\n const { code } = credentials;\n const { tokenUri } = this;\n const response = await exchangeAuthorizationCode({
|
|
1
|
+
{"version":3,"file":"google.cjs","names":["createAuthorizationUri","exchangeAuthorizationCode","OAuth2Error","verifyIdToken"],"sources":["../../../src/oauth2/provider/google.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { OAuth2Error } from '../error';\nimport type {\n LoginOAuth2NativeParams,\n NativeCredential,\n OAuth2Token,\n OneTapProvider,\n} from '../types';\nimport { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from './common';\n\n/** The Google Cloud project number every client id of a project starts with: `<number>-<hash>.apps.googleusercontent.com`. */\nfunction projectNumber(clientId: string) {\n return clientId.split('-', 1)[0];\n}\n\nexport function createGoogleProvider(): OneTapProvider {\n return {\n authorizationUri: 'https://accounts.google.com/o/oauth2/v2/auth',\n tokenUri: 'https://oauth2.googleapis.com/token',\n userInfoUri: 'https://www.googleapis.com/oauth2/v3/userinfo',\n userNameAttribute: 'sub',\n jwkSetUri: 'https://www.googleapis.com/oauth2/v3/certs',\n // https://developers.google.com/identity/protocols/oauth2/scopes#oauth2\n defaultScope: [\n 'https://www.googleapis.com/auth/userinfo.email',\n 'https://www.googleapis.com/auth/userinfo.profile',\n 'openid',\n ],\n createAuthorizationUri(params) {\n return createAuthorizationUri({\n ...params,\n scope: params.scope ?? this.defaultScope,\n authorizationUri: this.authorizationUri,\n });\n },\n async exchangeAuthorizationCode(params) {\n const response = await exchangeAuthorizationCode({ ...params, tokenUri: this.tokenUri });\n if (!response.ok) {\n const { error, error_description } = (await response.json()) as GoogleErrorResponse;\n throw new OAuth2Error(response.status, error, error_description);\n }\n return (await response.json()) as GoogleToken;\n },\n async getUserInfo({ id_token }) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n const data = await verifyIdToken<GoogleUserInfo>(id_token, this.jwkSetUri);\n return {\n data,\n claims: {\n sub: data.sub,\n name: data.name,\n picture: data.picture,\n email: data.email,\n email_verified: data.email_verified,\n given_name: data.given_name,\n family_name: data.family_name,\n locale: data.locale,\n },\n };\n },\n async loginOAuth2Native({ pkce: _, credentials, ...params }: LoginOAuth2NativeParams) {\n invariant(credentials.code, 'code is required');\n const { code } = credentials;\n const { tokenUri } = this;\n // An app signs in as its own installed-app client (the iOS / Android client id, custom-scheme\n // redirect), and Google only exchanges a code for the client it was issued to, so the exchange\n // uses the client the app names rather than the registration's web client. Installed-app\n // clients have no secret; the registration's is sent only when the exchange is for that very\n // client. Every client id carries its project number as prefix, which pins the app's client to\n // the registration's Google Cloud project — codes from a foreign project's client are refused.\n const { client_id = params.clientId, redirect_uri = params.redirectUri } = credentials;\n if (projectNumber(client_id) !== projectNumber(params.clientId)) {\n throw new OAuth2Error(400, 'invalid_client', 'client_id belongs to another project');\n }\n const response = await exchangeAuthorizationCode({\n code,\n tokenUri,\n clientId: client_id,\n clientSecret: client_id === params.clientId ? params.clientSecret : '',\n redirectUri: redirect_uri,\n });\n if (!response.ok) {\n const { error, error_description } = (await response.json()) as GoogleErrorResponse;\n throw new OAuth2Error(response.status, error, error_description);\n }\n const token = (await response.json()) as GoogleToken;\n const userInfo = await this.getUserInfo(token);\n return { token, userInfo };\n },\n async getTokenInfo(id_token: string) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n const data = await verifyIdToken<GoogleUserInfo>(id_token, this.jwkSetUri);\n return {\n token: {\n id_token,\n access_token: '',\n token_type: '',\n expires_in: 0,\n refresh_token: '',\n scope: '',\n },\n userInfo: {\n data,\n claims: {\n sub: data.sub,\n name: data.name,\n picture: data.picture,\n email: data.email,\n email_verified: data.email_verified,\n given_name: data.given_name,\n family_name: data.family_name,\n locale: data.locale,\n },\n },\n };\n },\n };\n}\n\nexport const google = createGoogleProvider();\n\nexport interface GoogleUserInfo {\n aud: string;\n azp: string;\n email: string;\n email_verified: boolean;\n exp: number;\n family_name: string;\n given_name: string;\n hd?: string;\n iat: number;\n iss: string;\n jti?: string;\n locale?: string;\n name: string;\n nbf?: number;\n picture: string;\n sub: string;\n nonce?: string;\n}\n\nexport interface GoogleToken extends OAuth2Token {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token: string;\n scope: string;\n}\n\n// android,ios has different client_id and redirect_uri\nexport interface GoogleAppCredential extends NativeCredential {\n state: string;\n code: string;\n client_id: string;\n redirect_uri: string;\n}\n\nexport interface GoogleErrorResponse {\n error:\n | 'invalid_request'\n | 'invalid_client'\n | 'invalid_grant'\n | 'invalid_token'\n | 'invalid_scope'\n | 'unauthorized_client'\n | 'unsupported_grant_type'\n | 'access_denied'\n | 'server_error';\n error_description: string;\n}\n"],"mappings":";;;;;;AAWA,SAAS,cAAc,UAAkB;CACvC,OAAO,SAAS,MAAM,KAAK,CAAC,CAAC,CAAC;AAChC;AAEA,SAAgB,uBAAuC;CACrD,OAAO;EACL,kBAAkB;EAClB,UAAU;EACV,aAAa;EACb,mBAAmB;EACnB,WAAW;EAEX,cAAc;GACZ;GACA;GACA;EACF;EACA,uBAAuB,QAAQ;GAC7B,OAAOA,+BAAAA,uBAAuB;IAC5B,GAAG;IACH,OAAO,OAAO,SAAS,KAAK;IAC5B,kBAAkB,KAAK;GACzB,CAAC;EACH;EACA,MAAM,0BAA0B,QAAQ;GACtC,MAAM,WAAW,MAAMC,+BAAAA,0BAA0B;IAAE,GAAG;IAAQ,UAAU,KAAK;GAAS,CAAC;GACvF,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,OAAO,sBAAuB,MAAM,SAAS,KAAK;IAC1D,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,OAAO,iBAAiB;GACjE;GACA,OAAQ,MAAM,SAAS,KAAK;EAC9B;EACA,MAAM,YAAY,EAAE,YAAY;GAC9B,CAAA,GAAA,cAAA,UAAA,CAAU,UAAU,sBAAsB;GAC1C,CAAA,GAAA,cAAA,UAAA,CAAU,KAAK,WAAW,uBAAuB;GACjD,MAAM,OAAO,MAAMC,+BAAAA,cAA8B,UAAU,KAAK,SAAS;GACzE,OAAO;IACL;IACA,QAAQ;KACN,KAAK,KAAK;KACV,MAAM,KAAK;KACX,SAAS,KAAK;KACd,OAAO,KAAK;KACZ,gBAAgB,KAAK;KACrB,YAAY,KAAK;KACjB,aAAa,KAAK;KAClB,QAAQ,KAAK;IACf;GACF;EACF;EACA,MAAM,kBAAkB,EAAE,MAAM,GAAG,aAAa,GAAG,UAAmC;GACpF,CAAA,GAAA,cAAA,UAAA,CAAU,YAAY,MAAM,kBAAkB;GAC9C,MAAM,EAAE,SAAS;GACjB,MAAM,EAAE,aAAa;GAOrB,MAAM,EAAE,YAAY,OAAO,UAAU,eAAe,OAAO,gBAAgB;GAC3E,IAAI,cAAc,SAAS,MAAM,cAAc,OAAO,QAAQ,GAC5D,MAAM,IAAID,qBAAAA,YAAY,KAAK,kBAAkB,sCAAsC;GAErF,MAAM,WAAW,MAAMD,+BAAAA,0BAA0B;IAC/C;IACA;IACA,UAAU;IACV,cAAc,cAAc,OAAO,WAAW,OAAO,eAAe;IACpE,aAAa;GACf,CAAC;GACD,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,OAAO,sBAAuB,MAAM,SAAS,KAAK;IAC1D,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,OAAO,iBAAiB;GACjE;GACA,MAAM,QAAS,MAAM,SAAS,KAAK;GAEnC,OAAO;IAAE;IAAO,UAAA,MADO,KAAK,YAAY,KAAK;GACpB;EAC3B;EACA,MAAM,aAAa,UAAkB;GACnC,CAAA,GAAA,cAAA,UAAA,CAAU,UAAU,sBAAsB;GAC1C,CAAA,GAAA,cAAA,UAAA,CAAU,KAAK,WAAW,uBAAuB;GACjD,MAAM,OAAO,MAAMC,+BAAAA,cAA8B,UAAU,KAAK,SAAS;GACzE,OAAO;IACL,OAAO;KACL;KACA,cAAc;KACd,YAAY;KACZ,YAAY;KACZ,eAAe;KACf,OAAO;IACT;IACA,UAAU;KACR;KACA,QAAQ;MACN,KAAK,KAAK;MACV,MAAM,KAAK;MACX,SAAS,KAAK;MACd,OAAO,KAAK;MACZ,gBAAgB,KAAK;MACrB,YAAY,KAAK;MACjB,aAAa,KAAK;MAClB,QAAQ,KAAK;KACf;IACF;GACF;EACF;CACF;AACF;AAEA,MAAa,SAAS,qBAAqB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google.d.cts","names":[],"sources":["../../../src/oauth2/provider/google.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"google.d.cts","names":[],"sources":["../../../src/oauth2/provider/google.ts"],"mappings":";;iBAegB,wBAAwB;cA0G3B,QAAM;UAEF;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe,oBAAoB;EACnC;EACA;EACA;EACA;EACA;EACA;;UAIe,4BAA4B;EAC3C;EACA;EACA;EACA;;UAGe;EACf;EAUA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google.d.mts","names":[],"sources":["../../../src/oauth2/provider/google.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"google.d.mts","names":[],"sources":["../../../src/oauth2/provider/google.ts"],"mappings":";;iBAegB,wBAAwB;cA0G3B,QAAM;UAEF;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe,oBAAoB;EACnC;EACA;EACA;EACA;EACA;EACA;;UAIe,4BAA4B;EAC3C;EACA;EACA;EACA;;UAGe;EACf;EAUA"}
|
|
@@ -2,6 +2,10 @@ import { OAuth2Error } from "../error.mjs";
|
|
|
2
2
|
import { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from "./common.mjs";
|
|
3
3
|
import { invariant } from "@shware/utils";
|
|
4
4
|
//#region src/oauth2/provider/google.ts
|
|
5
|
+
/** The Google Cloud project number every client id of a project starts with: `<number>-<hash>.apps.googleusercontent.com`. */
|
|
6
|
+
function projectNumber(clientId) {
|
|
7
|
+
return clientId.split("-", 1)[0];
|
|
8
|
+
}
|
|
5
9
|
function createGoogleProvider() {
|
|
6
10
|
return {
|
|
7
11
|
authorizationUri: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
@@ -54,10 +58,14 @@ function createGoogleProvider() {
|
|
|
54
58
|
invariant(credentials.code, "code is required");
|
|
55
59
|
const { code } = credentials;
|
|
56
60
|
const { tokenUri } = this;
|
|
61
|
+
const { client_id = params.clientId, redirect_uri = params.redirectUri } = credentials;
|
|
62
|
+
if (projectNumber(client_id) !== projectNumber(params.clientId)) throw new OAuth2Error(400, "invalid_client", "client_id belongs to another project");
|
|
57
63
|
const response = await exchangeAuthorizationCode({
|
|
58
64
|
code,
|
|
59
65
|
tokenUri,
|
|
60
|
-
|
|
66
|
+
clientId: client_id,
|
|
67
|
+
clientSecret: client_id === params.clientId ? params.clientSecret : "",
|
|
68
|
+
redirectUri: redirect_uri
|
|
61
69
|
});
|
|
62
70
|
if (!response.ok) {
|
|
63
71
|
const { error, error_description } = await response.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google.mjs","names":[],"sources":["../../../src/oauth2/provider/google.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { OAuth2Error } from '../error';\nimport type {\n LoginOAuth2NativeParams,\n NativeCredential,\n OAuth2Token,\n OneTapProvider,\n} from '../types';\nimport { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from './common';\n\nexport function createGoogleProvider(): OneTapProvider {\n return {\n authorizationUri: 'https://accounts.google.com/o/oauth2/v2/auth',\n tokenUri: 'https://oauth2.googleapis.com/token',\n userInfoUri: 'https://www.googleapis.com/oauth2/v3/userinfo',\n userNameAttribute: 'sub',\n jwkSetUri: 'https://www.googleapis.com/oauth2/v3/certs',\n // https://developers.google.com/identity/protocols/oauth2/scopes#oauth2\n defaultScope: [\n 'https://www.googleapis.com/auth/userinfo.email',\n 'https://www.googleapis.com/auth/userinfo.profile',\n 'openid',\n ],\n createAuthorizationUri(params) {\n return createAuthorizationUri({\n ...params,\n scope: params.scope ?? this.defaultScope,\n authorizationUri: this.authorizationUri,\n });\n },\n async exchangeAuthorizationCode(params) {\n const response = await exchangeAuthorizationCode({ ...params, tokenUri: this.tokenUri });\n if (!response.ok) {\n const { error, error_description } = (await response.json()) as GoogleErrorResponse;\n throw new OAuth2Error(response.status, error, error_description);\n }\n return (await response.json()) as GoogleToken;\n },\n async getUserInfo({ id_token }) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n const data = await verifyIdToken<GoogleUserInfo>(id_token, this.jwkSetUri);\n return {\n data,\n claims: {\n sub: data.sub,\n name: data.name,\n picture: data.picture,\n email: data.email,\n email_verified: data.email_verified,\n given_name: data.given_name,\n family_name: data.family_name,\n locale: data.locale,\n },\n };\n },\n async loginOAuth2Native({ pkce: _, credentials, ...params }: LoginOAuth2NativeParams) {\n invariant(credentials.code, 'code is required');\n const { code } = credentials;\n const { tokenUri } = this;\n const response = await exchangeAuthorizationCode({
|
|
1
|
+
{"version":3,"file":"google.mjs","names":[],"sources":["../../../src/oauth2/provider/google.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { OAuth2Error } from '../error';\nimport type {\n LoginOAuth2NativeParams,\n NativeCredential,\n OAuth2Token,\n OneTapProvider,\n} from '../types';\nimport { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from './common';\n\n/** The Google Cloud project number every client id of a project starts with: `<number>-<hash>.apps.googleusercontent.com`. */\nfunction projectNumber(clientId: string) {\n return clientId.split('-', 1)[0];\n}\n\nexport function createGoogleProvider(): OneTapProvider {\n return {\n authorizationUri: 'https://accounts.google.com/o/oauth2/v2/auth',\n tokenUri: 'https://oauth2.googleapis.com/token',\n userInfoUri: 'https://www.googleapis.com/oauth2/v3/userinfo',\n userNameAttribute: 'sub',\n jwkSetUri: 'https://www.googleapis.com/oauth2/v3/certs',\n // https://developers.google.com/identity/protocols/oauth2/scopes#oauth2\n defaultScope: [\n 'https://www.googleapis.com/auth/userinfo.email',\n 'https://www.googleapis.com/auth/userinfo.profile',\n 'openid',\n ],\n createAuthorizationUri(params) {\n return createAuthorizationUri({\n ...params,\n scope: params.scope ?? this.defaultScope,\n authorizationUri: this.authorizationUri,\n });\n },\n async exchangeAuthorizationCode(params) {\n const response = await exchangeAuthorizationCode({ ...params, tokenUri: this.tokenUri });\n if (!response.ok) {\n const { error, error_description } = (await response.json()) as GoogleErrorResponse;\n throw new OAuth2Error(response.status, error, error_description);\n }\n return (await response.json()) as GoogleToken;\n },\n async getUserInfo({ id_token }) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n const data = await verifyIdToken<GoogleUserInfo>(id_token, this.jwkSetUri);\n return {\n data,\n claims: {\n sub: data.sub,\n name: data.name,\n picture: data.picture,\n email: data.email,\n email_verified: data.email_verified,\n given_name: data.given_name,\n family_name: data.family_name,\n locale: data.locale,\n },\n };\n },\n async loginOAuth2Native({ pkce: _, credentials, ...params }: LoginOAuth2NativeParams) {\n invariant(credentials.code, 'code is required');\n const { code } = credentials;\n const { tokenUri } = this;\n // An app signs in as its own installed-app client (the iOS / Android client id, custom-scheme\n // redirect), and Google only exchanges a code for the client it was issued to, so the exchange\n // uses the client the app names rather than the registration's web client. Installed-app\n // clients have no secret; the registration's is sent only when the exchange is for that very\n // client. Every client id carries its project number as prefix, which pins the app's client to\n // the registration's Google Cloud project — codes from a foreign project's client are refused.\n const { client_id = params.clientId, redirect_uri = params.redirectUri } = credentials;\n if (projectNumber(client_id) !== projectNumber(params.clientId)) {\n throw new OAuth2Error(400, 'invalid_client', 'client_id belongs to another project');\n }\n const response = await exchangeAuthorizationCode({\n code,\n tokenUri,\n clientId: client_id,\n clientSecret: client_id === params.clientId ? params.clientSecret : '',\n redirectUri: redirect_uri,\n });\n if (!response.ok) {\n const { error, error_description } = (await response.json()) as GoogleErrorResponse;\n throw new OAuth2Error(response.status, error, error_description);\n }\n const token = (await response.json()) as GoogleToken;\n const userInfo = await this.getUserInfo(token);\n return { token, userInfo };\n },\n async getTokenInfo(id_token: string) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n const data = await verifyIdToken<GoogleUserInfo>(id_token, this.jwkSetUri);\n return {\n token: {\n id_token,\n access_token: '',\n token_type: '',\n expires_in: 0,\n refresh_token: '',\n scope: '',\n },\n userInfo: {\n data,\n claims: {\n sub: data.sub,\n name: data.name,\n picture: data.picture,\n email: data.email,\n email_verified: data.email_verified,\n given_name: data.given_name,\n family_name: data.family_name,\n locale: data.locale,\n },\n },\n };\n },\n };\n}\n\nexport const google = createGoogleProvider();\n\nexport interface GoogleUserInfo {\n aud: string;\n azp: string;\n email: string;\n email_verified: boolean;\n exp: number;\n family_name: string;\n given_name: string;\n hd?: string;\n iat: number;\n iss: string;\n jti?: string;\n locale?: string;\n name: string;\n nbf?: number;\n picture: string;\n sub: string;\n nonce?: string;\n}\n\nexport interface GoogleToken extends OAuth2Token {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token: string;\n scope: string;\n}\n\n// android,ios has different client_id and redirect_uri\nexport interface GoogleAppCredential extends NativeCredential {\n state: string;\n code: string;\n client_id: string;\n redirect_uri: string;\n}\n\nexport interface GoogleErrorResponse {\n error:\n | 'invalid_request'\n | 'invalid_client'\n | 'invalid_grant'\n | 'invalid_token'\n | 'invalid_scope'\n | 'unauthorized_client'\n | 'unsupported_grant_type'\n | 'access_denied'\n | 'server_error';\n error_description: string;\n}\n"],"mappings":";;;;;AAWA,SAAS,cAAc,UAAkB;CACvC,OAAO,SAAS,MAAM,KAAK,CAAC,CAAC,CAAC;AAChC;AAEA,SAAgB,uBAAuC;CACrD,OAAO;EACL,kBAAkB;EAClB,UAAU;EACV,aAAa;EACb,mBAAmB;EACnB,WAAW;EAEX,cAAc;GACZ;GACA;GACA;EACF;EACA,uBAAuB,QAAQ;GAC7B,OAAO,uBAAuB;IAC5B,GAAG;IACH,OAAO,OAAO,SAAS,KAAK;IAC5B,kBAAkB,KAAK;GACzB,CAAC;EACH;EACA,MAAM,0BAA0B,QAAQ;GACtC,MAAM,WAAW,MAAM,0BAA0B;IAAE,GAAG;IAAQ,UAAU,KAAK;GAAS,CAAC;GACvF,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,OAAO,sBAAuB,MAAM,SAAS,KAAK;IAC1D,MAAM,IAAI,YAAY,SAAS,QAAQ,OAAO,iBAAiB;GACjE;GACA,OAAQ,MAAM,SAAS,KAAK;EAC9B;EACA,MAAM,YAAY,EAAE,YAAY;GAC9B,UAAU,UAAU,sBAAsB;GAC1C,UAAU,KAAK,WAAW,uBAAuB;GACjD,MAAM,OAAO,MAAM,cAA8B,UAAU,KAAK,SAAS;GACzE,OAAO;IACL;IACA,QAAQ;KACN,KAAK,KAAK;KACV,MAAM,KAAK;KACX,SAAS,KAAK;KACd,OAAO,KAAK;KACZ,gBAAgB,KAAK;KACrB,YAAY,KAAK;KACjB,aAAa,KAAK;KAClB,QAAQ,KAAK;IACf;GACF;EACF;EACA,MAAM,kBAAkB,EAAE,MAAM,GAAG,aAAa,GAAG,UAAmC;GACpF,UAAU,YAAY,MAAM,kBAAkB;GAC9C,MAAM,EAAE,SAAS;GACjB,MAAM,EAAE,aAAa;GAOrB,MAAM,EAAE,YAAY,OAAO,UAAU,eAAe,OAAO,gBAAgB;GAC3E,IAAI,cAAc,SAAS,MAAM,cAAc,OAAO,QAAQ,GAC5D,MAAM,IAAI,YAAY,KAAK,kBAAkB,sCAAsC;GAErF,MAAM,WAAW,MAAM,0BAA0B;IAC/C;IACA;IACA,UAAU;IACV,cAAc,cAAc,OAAO,WAAW,OAAO,eAAe;IACpE,aAAa;GACf,CAAC;GACD,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,OAAO,sBAAuB,MAAM,SAAS,KAAK;IAC1D,MAAM,IAAI,YAAY,SAAS,QAAQ,OAAO,iBAAiB;GACjE;GACA,MAAM,QAAS,MAAM,SAAS,KAAK;GAEnC,OAAO;IAAE;IAAO,UAAA,MADO,KAAK,YAAY,KAAK;GACpB;EAC3B;EACA,MAAM,aAAa,UAAkB;GACnC,UAAU,UAAU,sBAAsB;GAC1C,UAAU,KAAK,WAAW,uBAAuB;GACjD,MAAM,OAAO,MAAM,cAA8B,UAAU,KAAK,SAAS;GACzE,OAAO;IACL,OAAO;KACL;KACA,cAAc;KACd,YAAY;KACZ,YAAY;KACZ,eAAe;KACf,OAAO;IACT;IACA,UAAU;KACR;KACA,QAAQ;MACN,KAAK,KAAK;MACV,MAAM,KAAK;MACX,SAAS,KAAK;MACd,OAAO,KAAK;MACZ,gBAAgB,KAAK;MACrB,YAAY,KAAK;MACjB,aAAa,KAAK;MAClB,QAAQ,KAAK;KACf;IACF;GACF;EACF;CACF;AACF;AAEA,MAAa,SAAS,qBAAqB"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_oauth2_provider_apple = require("./apple.cjs");
|
|
3
|
+
const require_oauth2_provider_apple_client_secret = require("./apple-client-secret.cjs");
|
|
3
4
|
const require_oauth2_provider_facebook = require("./facebook.cjs");
|
|
4
5
|
const require_oauth2_provider_github = require("./github.cjs");
|
|
5
6
|
const require_oauth2_provider_google = require("./google.cjs");
|
|
6
7
|
const require_oauth2_provider_wechat = require("./wechat.cjs");
|
|
7
8
|
const require_oauth2_provider_x = require("./x.cjs");
|
|
8
9
|
exports.apple = require_oauth2_provider_apple.apple;
|
|
10
|
+
exports.createAppleClientSecret = require_oauth2_provider_apple_client_secret.createAppleClientSecret;
|
|
9
11
|
exports.facebook = require_oauth2_provider_facebook.facebook;
|
|
10
12
|
exports.github = require_oauth2_provider_github.github;
|
|
11
13
|
exports.google = require_oauth2_provider_google.google;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { AppleClientSecretOptions, createAppleClientSecret } from "./apple-client-secret.cjs";
|
|
1
2
|
import { apple } from "./apple.cjs";
|
|
2
3
|
import { facebook } from "./facebook.cjs";
|
|
3
4
|
import { github } from "./github.cjs";
|
|
4
5
|
import { google } from "./google.cjs";
|
|
5
6
|
import { wechat } from "./wechat.cjs";
|
|
6
7
|
import { x } from "./x.cjs";
|
|
7
|
-
export { apple, facebook, github, google, wechat, x };
|
|
8
|
+
export { type AppleClientSecretOptions, apple, createAppleClientSecret, facebook, github, google, wechat, x };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { AppleClientSecretOptions, createAppleClientSecret } from "./apple-client-secret.mjs";
|
|
1
2
|
import { apple } from "./apple.mjs";
|
|
2
3
|
import { facebook } from "./facebook.mjs";
|
|
3
4
|
import { github } from "./github.mjs";
|
|
4
5
|
import { google } from "./google.mjs";
|
|
5
6
|
import { wechat } from "./wechat.mjs";
|
|
6
7
|
import { x } from "./x.mjs";
|
|
7
|
-
export { apple, facebook, github, google, wechat, x };
|
|
8
|
+
export { type AppleClientSecretOptions, apple, createAppleClientSecret, facebook, github, google, wechat, x };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { apple } from "./apple.mjs";
|
|
2
|
+
import { createAppleClientSecret } from "./apple-client-secret.mjs";
|
|
2
3
|
import { facebook } from "./facebook.mjs";
|
|
3
4
|
import { github } from "./github.mjs";
|
|
4
5
|
import { google } from "./google.mjs";
|
|
5
6
|
import { wechat } from "./wechat.mjs";
|
|
6
7
|
import { x } from "./x.mjs";
|
|
7
|
-
export { apple, facebook, github, google, wechat, x };
|
|
8
|
+
export { apple, createAppleClientSecret, facebook, github, google, wechat, x };
|
package/dist/session/db.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.cjs","names":["MapSession","resolveIndexesFor","PRINCIPAL_NAME_INDEX_NAME"],"sources":["../../src/session/db.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { PRINCIPAL_NAME_INDEX_NAME, resolveIndexesFor } from './common';\nimport { MapSession } from './map-session';\nimport type { SessionRepository } from './types';\n\ninterface SessionEntity {\n primaryId: string;\n sessionId: string;\n creationTime: number;\n lastAccessTime: number;\n maxInactiveInterval: number;\n expiryTime: number;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attributes: Record<string, any>;\n principalName?: string | null;\n}\n\nclass DBSession extends MapSession {\n public isNew: boolean;\n public primaryId: string;\n\n constructor(cached: MapSession, isNew: boolean) {\n super(cached);\n this.isNew = isNew;\n this.primaryId = randomUUID();\n }\n}\n\nexport interface DBAdapter {\n save(session: SessionEntity): Promise<void>;\n deleteById(sessionId: string): Promise<void>;\n findById(sessionId: string): Promise<SessionEntity | null>;\n findByPrincipalName(principalName: string): Promise<SessionEntity[]>;\n cleanupExpiredSessions(cleanupCount?: number): Promise<void>;\n}\n\nexport class DBIndexedSessionRepository implements SessionRepository<DBSession> {\n private defaultMaxInactiveInterval = 1800;\n private readonly db: DBAdapter;\n\n constructor(db: DBAdapter) {\n this.db = db;\n }\n\n setDefaultMaxInactiveInterval(interval: number) {\n this.defaultMaxInactiveInterval = interval;\n }\n\n private mapSessionToEntity(session: DBSession): SessionEntity {\n const attributes: Record<string, string | number | null> = {};\n for (const name of session.getAttributeNames()) {\n attributes[name] = session.getAttribute(name);\n }\n const principalName = resolveIndexesFor(session);\n\n return {\n primaryId: session.primaryId,\n sessionId: session.getId(),\n creationTime: session.getCreationTime(),\n lastAccessTime: session.getLastAccessedTime(),\n maxInactiveInterval: session.getMaxInactiveInterval(),\n expiryTime: session.getLastAccessedTime() + session.getMaxInactiveInterval() * 1000,\n attributes,\n principalName,\n };\n }\n\n private mapEntityToSession(entity: SessionEntity, allowExpired: boolean): DBSession | null {\n const loaded = new MapSession(entity.sessionId);\n loaded.setCreationTime(entity.creationTime);\n loaded.setLastAccessedTime(entity.lastAccessTime);\n loaded.setMaxInactiveInterval(entity.maxInactiveInterval);\n for (const [key, value] of Object.entries(entity.attributes)) {\n loaded.setAttribute(key, value);\n }\n\n if (!allowExpired && loaded.isExpired()) return null;\n const session = new DBSession(loaded, false);\n session.primaryId = entity.primaryId;\n return session;\n }\n\n private async getSession(id: string, allowExpired: boolean): Promise<DBSession | null> {\n const entity = await this.db.findById(id);\n if (!entity) return null;\n return this.mapEntityToSession(entity, allowExpired);\n }\n\n // override\n createSession() {\n const cached = new MapSession();\n cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);\n return new DBSession(cached, true);\n }\n\n async save(session: DBSession) {\n const entity = this.mapSessionToEntity(session);\n await this.db.save(entity);\n session.isNew = false;\n }\n\n async findById(sessionId: string): Promise<DBSession | null> {\n return this.getSession(sessionId, false);\n }\n\n async deleteById(sessionId: string) {\n await this.db.deleteById(sessionId);\n }\n\n async findByIndexNameAndIndexValue(\n indexName: string,\n indexValue: string\n ): Promise<Map<string, DBSession>> {\n if (indexName !== PRINCIPAL_NAME_INDEX_NAME) {\n return new Map<string, DBSession>();\n }\n const entities = await this.db.findByPrincipalName(indexValue);\n if (entities.length === 0) {\n return new Map<string, DBSession>();\n }\n const sessions = new Map<string, DBSession>();\n for (const entity of entities) {\n const session = this.mapEntityToSession(entity, false);\n if (session) {\n sessions.set(session.getId(), session);\n }\n }\n return sessions;\n }\n\n async findByPrincipalName(principalName: string): Promise<Map<string, DBSession>> {\n return this.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);\n }\n\n async cleanupExpiredSessions(cleanupCount?: number): Promise<void> {\n return this.db.cleanupExpiredSessions(cleanupCount);\n }\n}\n"],"mappings":";;;;;AAiBA,IAAM,YAAN,cAAwBA,4BAAAA,WAAW;CACjC;CACA;CAEA,YAAY,QAAoB,OAAgB;EAC9C,MAAM,MAAM;EACZ,KAAK,QAAQ;EACb,KAAK,aAAA,
|
|
1
|
+
{"version":3,"file":"db.cjs","names":["MapSession","randomUUID","resolveIndexesFor","PRINCIPAL_NAME_INDEX_NAME"],"sources":["../../src/session/db.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { PRINCIPAL_NAME_INDEX_NAME, resolveIndexesFor } from './common';\nimport { MapSession } from './map-session';\nimport type { SessionRepository } from './types';\n\ninterface SessionEntity {\n primaryId: string;\n sessionId: string;\n creationTime: number;\n lastAccessTime: number;\n maxInactiveInterval: number;\n expiryTime: number;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n attributes: Record<string, any>;\n principalName?: string | null;\n}\n\nclass DBSession extends MapSession {\n public isNew: boolean;\n public primaryId: string;\n\n constructor(cached: MapSession, isNew: boolean) {\n super(cached);\n this.isNew = isNew;\n this.primaryId = randomUUID();\n }\n}\n\nexport interface DBAdapter {\n save(session: SessionEntity): Promise<void>;\n deleteById(sessionId: string): Promise<void>;\n findById(sessionId: string): Promise<SessionEntity | null>;\n findByPrincipalName(principalName: string): Promise<SessionEntity[]>;\n cleanupExpiredSessions(cleanupCount?: number): Promise<void>;\n}\n\nexport class DBIndexedSessionRepository implements SessionRepository<DBSession> {\n private defaultMaxInactiveInterval = 1800;\n private readonly db: DBAdapter;\n\n constructor(db: DBAdapter) {\n this.db = db;\n }\n\n setDefaultMaxInactiveInterval(interval: number) {\n this.defaultMaxInactiveInterval = interval;\n }\n\n private mapSessionToEntity(session: DBSession): SessionEntity {\n const attributes: Record<string, string | number | null> = {};\n for (const name of session.getAttributeNames()) {\n attributes[name] = session.getAttribute(name);\n }\n const principalName = resolveIndexesFor(session);\n\n return {\n primaryId: session.primaryId,\n sessionId: session.getId(),\n creationTime: session.getCreationTime(),\n lastAccessTime: session.getLastAccessedTime(),\n maxInactiveInterval: session.getMaxInactiveInterval(),\n expiryTime: session.getLastAccessedTime() + session.getMaxInactiveInterval() * 1000,\n attributes,\n principalName,\n };\n }\n\n private mapEntityToSession(entity: SessionEntity, allowExpired: boolean): DBSession | null {\n const loaded = new MapSession(entity.sessionId);\n loaded.setCreationTime(entity.creationTime);\n loaded.setLastAccessedTime(entity.lastAccessTime);\n loaded.setMaxInactiveInterval(entity.maxInactiveInterval);\n for (const [key, value] of Object.entries(entity.attributes)) {\n loaded.setAttribute(key, value);\n }\n\n if (!allowExpired && loaded.isExpired()) return null;\n const session = new DBSession(loaded, false);\n session.primaryId = entity.primaryId;\n return session;\n }\n\n private async getSession(id: string, allowExpired: boolean): Promise<DBSession | null> {\n const entity = await this.db.findById(id);\n if (!entity) return null;\n return this.mapEntityToSession(entity, allowExpired);\n }\n\n // override\n createSession() {\n const cached = new MapSession();\n cached.setMaxInactiveInterval(this.defaultMaxInactiveInterval);\n return new DBSession(cached, true);\n }\n\n async save(session: DBSession) {\n const entity = this.mapSessionToEntity(session);\n await this.db.save(entity);\n session.isNew = false;\n }\n\n async findById(sessionId: string): Promise<DBSession | null> {\n return this.getSession(sessionId, false);\n }\n\n async deleteById(sessionId: string) {\n await this.db.deleteById(sessionId);\n }\n\n async findByIndexNameAndIndexValue(\n indexName: string,\n indexValue: string\n ): Promise<Map<string, DBSession>> {\n if (indexName !== PRINCIPAL_NAME_INDEX_NAME) {\n return new Map<string, DBSession>();\n }\n const entities = await this.db.findByPrincipalName(indexValue);\n if (entities.length === 0) {\n return new Map<string, DBSession>();\n }\n const sessions = new Map<string, DBSession>();\n for (const entity of entities) {\n const session = this.mapEntityToSession(entity, false);\n if (session) {\n sessions.set(session.getId(), session);\n }\n }\n return sessions;\n }\n\n async findByPrincipalName(principalName: string): Promise<Map<string, DBSession>> {\n return this.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);\n }\n\n async cleanupExpiredSessions(cleanupCount?: number): Promise<void> {\n return this.db.cleanupExpiredSessions(cleanupCount);\n }\n}\n"],"mappings":";;;;;AAiBA,IAAM,YAAN,cAAwBA,4BAAAA,WAAW;CACjC;CACA;CAEA,YAAY,QAAoB,OAAgB;EAC9C,MAAM,MAAM;EACZ,KAAK,QAAQ;EACb,KAAK,aAAA,GAAYC,YAAAA,WAAAA,CAAW;CAC9B;AACF;AAUA,IAAa,6BAAb,MAAgF;CAC9E,6BAAqC;CACrC;CAEA,YAAY,IAAe;EACzB,KAAK,KAAK;CACZ;CAEA,8BAA8B,UAAkB;EAC9C,KAAK,6BAA6B;CACpC;CAEA,mBAA2B,SAAmC;EAC5D,MAAM,aAAqD,CAAC;EAC5D,KAAK,MAAM,QAAQ,QAAQ,kBAAkB,GAC3C,WAAW,QAAQ,QAAQ,aAAa,IAAI;EAE9C,MAAM,gBAAgBC,uBAAAA,kBAAkB,OAAO;EAE/C,OAAO;GACL,WAAW,QAAQ;GACnB,WAAW,QAAQ,MAAM;GACzB,cAAc,QAAQ,gBAAgB;GACtC,gBAAgB,QAAQ,oBAAoB;GAC5C,qBAAqB,QAAQ,uBAAuB;GACpD,YAAY,QAAQ,oBAAoB,IAAI,QAAQ,uBAAuB,IAAI;GAC/E;GACA;EACF;CACF;CAEA,mBAA2B,QAAuB,cAAyC;EACzF,MAAM,SAAS,IAAIF,4BAAAA,WAAW,OAAO,SAAS;EAC9C,OAAO,gBAAgB,OAAO,YAAY;EAC1C,OAAO,oBAAoB,OAAO,cAAc;EAChD,OAAO,uBAAuB,OAAO,mBAAmB;EACxD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,UAAU,GACzD,OAAO,aAAa,KAAK,KAAK;EAGhC,IAAI,CAAC,gBAAgB,OAAO,UAAU,GAAG,OAAO;EAChD,MAAM,UAAU,IAAI,UAAU,QAAQ,KAAK;EAC3C,QAAQ,YAAY,OAAO;EAC3B,OAAO;CACT;CAEA,MAAc,WAAW,IAAY,cAAkD;EACrF,MAAM,SAAS,MAAM,KAAK,GAAG,SAAS,EAAE;EACxC,IAAI,CAAC,QAAQ,OAAO;EACpB,OAAO,KAAK,mBAAmB,QAAQ,YAAY;CACrD;CAGA,gBAAgB;EACd,MAAM,SAAS,IAAIA,4BAAAA,WAAW;EAC9B,OAAO,uBAAuB,KAAK,0BAA0B;EAC7D,OAAO,IAAI,UAAU,QAAQ,IAAI;CACnC;CAEA,MAAM,KAAK,SAAoB;EAC7B,MAAM,SAAS,KAAK,mBAAmB,OAAO;EAC9C,MAAM,KAAK,GAAG,KAAK,MAAM;EACzB,QAAQ,QAAQ;CAClB;CAEA,MAAM,SAAS,WAA8C;EAC3D,OAAO,KAAK,WAAW,WAAW,KAAK;CACzC;CAEA,MAAM,WAAW,WAAmB;EAClC,MAAM,KAAK,GAAG,WAAW,SAAS;CACpC;CAEA,MAAM,6BACJ,WACA,YACiC;EACjC,IAAI,cAAcG,uBAAAA,2BAChB,uBAAO,IAAI,IAAuB;EAEpC,MAAM,WAAW,MAAM,KAAK,GAAG,oBAAoB,UAAU;EAC7D,IAAI,SAAS,WAAW,GACtB,uBAAO,IAAI,IAAuB;EAEpC,MAAM,2BAAW,IAAI,IAAuB;EAC5C,KAAK,MAAM,UAAU,UAAU;GAC7B,MAAM,UAAU,KAAK,mBAAmB,QAAQ,KAAK;GACrD,IAAI,SACF,SAAS,IAAI,QAAQ,MAAM,GAAG,OAAO;EAEzC;EACA,OAAO;CACT;CAEA,MAAM,oBAAoB,eAAwD;EAChF,OAAO,KAAK,6BAA6BA,uBAAAA,2BAA2B,aAAa;CACnF;CAEA,MAAM,uBAAuB,cAAsC;EACjE,OAAO,KAAK,GAAG,uBAAuB,YAAY;CACpD;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-session.cjs","names":[],"sources":["../../src/session/map-session.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { Session } from './types';\n\nfunction generateId() {\n return randomUUID();\n}\n\nconst DEFAULT_MAX_INACTIVE_INTERVAL = 1800;\n\nexport class MapSession implements Session {\n private id: string;\n private readonly sessionAttrs = new Map<string, string | number>();\n private creationTime = Date.now();\n private lastAccessedTime = this.creationTime;\n private maxInactiveInterval = DEFAULT_MAX_INACTIVE_INTERVAL;\n\n constructor(id?: string);\n constructor(session: Session);\n constructor(input?: string | Session) {\n if (input === undefined) {\n this.id = generateId();\n } else if (typeof input === 'string') {\n this.id = input;\n } else {\n const session = input;\n this.id = session.getId();\n\n for (const attrName of session.getAttributeNames()) {\n const attrValue = session.getAttribute(attrName);\n if (attrValue !== null) {\n this.setAttribute(attrName, attrValue);\n }\n }\n\n this.lastAccessedTime = session.getLastAccessedTime();\n this.creationTime = session.getCreationTime();\n this.maxInactiveInterval = session.getMaxInactiveInterval();\n }\n }\n\n setCreationTime(creationTime: number) {\n this.creationTime = creationTime;\n }\n\n // override\n getId() {\n return this.id;\n }\n\n changeSessionId() {\n const changedId = generateId();\n this.id = changedId;\n return changedId;\n }\n\n getAttribute(name: string) {\n return this.sessionAttrs.get(name) ?? null;\n }\n\n getAttributeNames(): string[] {\n return Array.from(this.sessionAttrs.keys());\n }\n\n setAttribute(name: string, value: string | number | null) {\n if (value === null) {\n this.removeAttribute(name);\n } else {\n this.sessionAttrs.set(name, value);\n }\n }\n\n removeAttribute(name: string) {\n this.sessionAttrs.delete(name);\n }\n\n getCreationTime() {\n return this.creationTime;\n }\n\n getLastAccessedTime() {\n return this.lastAccessedTime;\n }\n\n setLastAccessedTime(lastAccessedTime: number) {\n this.lastAccessedTime = lastAccessedTime;\n }\n\n getMaxInactiveInterval() {\n return this.maxInactiveInterval;\n }\n\n setMaxInactiveInterval(interval: number) {\n this.maxInactiveInterval = interval;\n }\n\n isExpired() {\n if (this.maxInactiveInterval < 0) return false;\n return Date.now() - this.maxInactiveInterval * 1000 - this.lastAccessedTime >= 0;\n }\n}\n"],"mappings":";;;AAGA,SAAS,aAAa;CACpB,QAAA,
|
|
1
|
+
{"version":3,"file":"map-session.cjs","names":["randomUUID"],"sources":["../../src/session/map-session.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { Session } from './types';\n\nfunction generateId() {\n return randomUUID();\n}\n\nconst DEFAULT_MAX_INACTIVE_INTERVAL = 1800;\n\nexport class MapSession implements Session {\n private id: string;\n private readonly sessionAttrs = new Map<string, string | number>();\n private creationTime = Date.now();\n private lastAccessedTime = this.creationTime;\n private maxInactiveInterval = DEFAULT_MAX_INACTIVE_INTERVAL;\n\n constructor(id?: string);\n constructor(session: Session);\n constructor(input?: string | Session) {\n if (input === undefined) {\n this.id = generateId();\n } else if (typeof input === 'string') {\n this.id = input;\n } else {\n const session = input;\n this.id = session.getId();\n\n for (const attrName of session.getAttributeNames()) {\n const attrValue = session.getAttribute(attrName);\n if (attrValue !== null) {\n this.setAttribute(attrName, attrValue);\n }\n }\n\n this.lastAccessedTime = session.getLastAccessedTime();\n this.creationTime = session.getCreationTime();\n this.maxInactiveInterval = session.getMaxInactiveInterval();\n }\n }\n\n setCreationTime(creationTime: number) {\n this.creationTime = creationTime;\n }\n\n // override\n getId() {\n return this.id;\n }\n\n changeSessionId() {\n const changedId = generateId();\n this.id = changedId;\n return changedId;\n }\n\n getAttribute(name: string) {\n return this.sessionAttrs.get(name) ?? null;\n }\n\n getAttributeNames(): string[] {\n return Array.from(this.sessionAttrs.keys());\n }\n\n setAttribute(name: string, value: string | number | null) {\n if (value === null) {\n this.removeAttribute(name);\n } else {\n this.sessionAttrs.set(name, value);\n }\n }\n\n removeAttribute(name: string) {\n this.sessionAttrs.delete(name);\n }\n\n getCreationTime() {\n return this.creationTime;\n }\n\n getLastAccessedTime() {\n return this.lastAccessedTime;\n }\n\n setLastAccessedTime(lastAccessedTime: number) {\n this.lastAccessedTime = lastAccessedTime;\n }\n\n getMaxInactiveInterval() {\n return this.maxInactiveInterval;\n }\n\n setMaxInactiveInterval(interval: number) {\n this.maxInactiveInterval = interval;\n }\n\n isExpired() {\n if (this.maxInactiveInterval < 0) return false;\n return Date.now() - this.maxInactiveInterval * 1000 - this.lastAccessedTime >= 0;\n }\n}\n"],"mappings":";;;AAGA,SAAS,aAAa;CACpB,QAAA,GAAOA,YAAAA,WAAAA,CAAW;AACpB;AAEA,MAAM,gCAAgC;AAEtC,IAAa,aAAb,MAA2C;CACzC;CACA,+BAAgC,IAAI,IAA6B;CACjE,eAAuB,KAAK,IAAI;CAChC,mBAA2B,KAAK;CAChC,sBAA8B;CAI9B,YAAY,OAA0B;EACpC,IAAI,UAAU,KAAA,GACZ,KAAK,KAAK,WAAW;OAChB,IAAI,OAAO,UAAU,UAC1B,KAAK,KAAK;OACL;GACL,MAAM,UAAU;GAChB,KAAK,KAAK,QAAQ,MAAM;GAExB,KAAK,MAAM,YAAY,QAAQ,kBAAkB,GAAG;IAClD,MAAM,YAAY,QAAQ,aAAa,QAAQ;IAC/C,IAAI,cAAc,MAChB,KAAK,aAAa,UAAU,SAAS;GAEzC;GAEA,KAAK,mBAAmB,QAAQ,oBAAoB;GACpD,KAAK,eAAe,QAAQ,gBAAgB;GAC5C,KAAK,sBAAsB,QAAQ,uBAAuB;EAC5D;CACF;CAEA,gBAAgB,cAAsB;EACpC,KAAK,eAAe;CACtB;CAGA,QAAQ;EACN,OAAO,KAAK;CACd;CAEA,kBAAkB;EAChB,MAAM,YAAY,WAAW;EAC7B,KAAK,KAAK;EACV,OAAO;CACT;CAEA,aAAa,MAAc;EACzB,OAAO,KAAK,aAAa,IAAI,IAAI,KAAK;CACxC;CAEA,oBAA8B;EAC5B,OAAO,MAAM,KAAK,KAAK,aAAa,KAAK,CAAC;CAC5C;CAEA,aAAa,MAAc,OAA+B;EACxD,IAAI,UAAU,MACZ,KAAK,gBAAgB,IAAI;OAEzB,KAAK,aAAa,IAAI,MAAM,KAAK;CAErC;CAEA,gBAAgB,MAAc;EAC5B,KAAK,aAAa,OAAO,IAAI;CAC/B;CAEA,kBAAkB;EAChB,OAAO,KAAK;CACd;CAEA,sBAAsB;EACpB,OAAO,KAAK;CACd;CAEA,oBAAoB,kBAA0B;EAC5C,KAAK,mBAAmB;CAC1B;CAEA,yBAAyB;EACvB,OAAO,KAAK;CACd;CAEA,uBAAuB,UAAkB;EACvC,KAAK,sBAAsB;CAC7B;CAEA,YAAY;EACV,IAAI,KAAK,sBAAsB,GAAG,OAAO;EACzC,OAAO,KAAK,IAAI,IAAI,KAAK,sBAAsB,MAAO,KAAK,oBAAoB;CACjF;AACF"}
|
package/dist/utils/http.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.cjs","names":["cookie"],"sources":["../../src/utils/http.ts"],"sourcesContent":["import { type SerializeOptions, parseCookie, stringifySetCookie } from 'cookie';\nimport { match } from 'path-to-regexp';\n\nexport type CookieOptions = Omit<SerializeOptions, 'encode'>;\n\nexport function getCookie(request: Request, name: string): string | undefined {\n const cookie = request.headers.get('Cookie');\n if (!cookie) return undefined;\n const parsed = parseCookie(cookie);\n return parsed[name];\n}\n\nexport function setCookie(\n response: Response,\n name: string,\n value: string,\n options?: SerializeOptions\n) {\n response.headers.append('Set-Cookie', stringifySetCookie({ name, value, ...options }));\n}\n\nexport function deleteCookie(response: Response, name: string, options?: SerializeOptions) {\n response.headers.append(\n 'Set-Cookie',\n stringifySetCookie({ name, value: '', ...options, maxAge: 0 })\n );\n}\n\ntype PathParam<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`\n ? { [K in Param]: string } & PathParam<Rest>\n : T extends `${string}:${infer Param}`\n ? { [K in Param]: string }\n : // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n {};\n\nexport function param<P extends string>(request: Request, path: P): PathParam<P> {\n const url = new URL(request.url);\n const fn = match(path);\n const result = fn(url.pathname);\n if (!result) throw new Error(`Path not matched: ${url.pathname}, path: ${path}`);\n return result.params as PathParam<P>;\n}\n\nexport function query(request: Request) {\n const url = new URL(request.url);\n return Object.fromEntries(url.searchParams.entries());\n}\n"],"mappings":";;;;AAKA,SAAgB,UAAU,SAAkB,MAAkC;CAC5E,MAAMA,WAAS,QAAQ,QAAQ,IAAI,QAAQ;CAC3C,IAAI,CAACA,UAAQ,OAAO,KAAA;CAEpB,QAAA,
|
|
1
|
+
{"version":3,"file":"http.cjs","names":["cookie","parseCookie","stringifySetCookie","match"],"sources":["../../src/utils/http.ts"],"sourcesContent":["import { type SerializeOptions, parseCookie, stringifySetCookie } from 'cookie';\nimport { match } from 'path-to-regexp';\n\nexport type CookieOptions = Omit<SerializeOptions, 'encode'>;\n\nexport function getCookie(request: Request, name: string): string | undefined {\n const cookie = request.headers.get('Cookie');\n if (!cookie) return undefined;\n const parsed = parseCookie(cookie);\n return parsed[name];\n}\n\nexport function setCookie(\n response: Response,\n name: string,\n value: string,\n options?: SerializeOptions\n) {\n response.headers.append('Set-Cookie', stringifySetCookie({ name, value, ...options }));\n}\n\nexport function deleteCookie(response: Response, name: string, options?: SerializeOptions) {\n response.headers.append(\n 'Set-Cookie',\n stringifySetCookie({ name, value: '', ...options, maxAge: 0 })\n );\n}\n\ntype PathParam<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`\n ? { [K in Param]: string } & PathParam<Rest>\n : T extends `${string}:${infer Param}`\n ? { [K in Param]: string }\n : // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n {};\n\nexport function param<P extends string>(request: Request, path: P): PathParam<P> {\n const url = new URL(request.url);\n const fn = match(path);\n const result = fn(url.pathname);\n if (!result) throw new Error(`Path not matched: ${url.pathname}, path: ${path}`);\n return result.params as PathParam<P>;\n}\n\nexport function query(request: Request) {\n const url = new URL(request.url);\n return Object.fromEntries(url.searchParams.entries());\n}\n"],"mappings":";;;;AAKA,SAAgB,UAAU,SAAkB,MAAkC;CAC5E,MAAMA,WAAS,QAAQ,QAAQ,IAAI,QAAQ;CAC3C,IAAI,CAACA,UAAQ,OAAO,KAAA;CAEpB,QAAA,GADeC,OAAAA,YAAAA,CAAYD,QACf,CAAC,CAAC;AAChB;AAEA,SAAgB,UACd,UACA,MACA,OACA,SACA;CACA,SAAS,QAAQ,OAAO,eAAA,GAAcE,OAAAA,mBAAAA,CAAmB;EAAE;EAAM;EAAO,GAAG;CAAQ,CAAC,CAAC;AACvF;AAEA,SAAgB,aAAa,UAAoB,MAAc,SAA4B;CACzF,SAAS,QAAQ,OACf,eAAA,GACAA,OAAAA,mBAAAA,CAAmB;EAAE;EAAM,OAAO;EAAI,GAAG;EAAS,QAAQ;CAAE,CAAC,CAC/D;AACF;AASA,SAAgB,MAAwB,SAAkB,MAAuB;CAC/E,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;CAE/B,MAAM,UAAA,GADKC,eAAAA,MAAAA,CAAM,IACD,CAAC,CAAC,IAAI,QAAQ;CAC9B,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,qBAAqB,IAAI,SAAS,UAAU,MAAM;CAC/E,OAAO,OAAO;AAChB;AAEA,SAAgB,MAAM,SAAkB;CACtC,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;CAC/B,OAAO,OAAO,YAAY,IAAI,aAAa,QAAQ,CAAC;AACtD"}
|