@shware/security 6.2.2 → 6.3.1
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/README.md +11 -4
- package/dist/auth/index.cjs +22 -6
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +6 -0
- package/dist/auth/index.d.cts.map +1 -1
- package/dist/auth/index.d.mts +6 -0
- package/dist/auth/index.d.mts.map +1 -1
- package/dist/auth/index.mjs +22 -6
- package/dist/auth/index.mjs.map +1 -1
- package/dist/session/db.cjs +1 -0
- package/dist/session/db.cjs.map +1 -1
- package/dist/session/db.d.cts +8 -0
- package/dist/session/db.d.cts.map +1 -1
- package/dist/session/db.d.mts +8 -0
- package/dist/session/db.d.mts.map +1 -1
- package/dist/session/db.mjs +1 -0
- package/dist/session/db.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -226,10 +226,14 @@ type SessionEntity = Parameters<DBAdapter['save']>[0];
|
|
|
226
226
|
|
|
227
227
|
class SessionAdapter implements DBAdapter {
|
|
228
228
|
async save(entity: SessionEntity) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
// `isNew` says whether this is the first write: a session created in this request is
|
|
230
|
+
// inserted, one that was loaded is only ever updated. Updating zero rows means a concurrent
|
|
231
|
+
// logout or kick deleted it, and an upsert there would bring the session back from the dead.
|
|
232
|
+
if (entity.isNew) {
|
|
233
|
+
await db.insert(session).values(toRow(entity));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
await db.update(session).set(toRow(entity)).where(eq(session.sessionId, entity.sessionId));
|
|
233
237
|
}
|
|
234
238
|
async deleteById(sessionId: string) {
|
|
235
239
|
await db.delete(session).where(eq(session.sessionId, sessionId));
|
|
@@ -317,6 +321,9 @@ any app; it is sent with an `appsecret_proof`, so the client needs its `clientSe
|
|
|
317
321
|
so register both). `loginOAuth2Code` checks state, exchanges the code as the `web` client,
|
|
318
322
|
calls `onAuthorized` and redirects to `successUri` with `status=success&provider=<key>`, or to
|
|
319
323
|
`errorUri` with `error` and `error_description`.
|
|
324
|
+
3. On success the user is signed in on a new session id, not the one the flow started on, which
|
|
325
|
+
is deleted (session fixation protection). The attributes the host kept in that session move to
|
|
326
|
+
the new one; the single-use authorization request (state, PKCE verifier) does not.
|
|
320
327
|
|
|
321
328
|
Every failure of the callback lands on `errorUri` — never a raw JSON error in the popup:
|
|
322
329
|
|
package/dist/auth/index.cjs
CHANGED
|
@@ -261,6 +261,7 @@ var Auth = class {
|
|
|
261
261
|
if (provider !== cached.provider) return this.redirect("invalid_request", "redirect provider mismatch");
|
|
262
262
|
mark("validate_session");
|
|
263
263
|
const { code, user } = parsed.data;
|
|
264
|
+
let signedIn;
|
|
264
265
|
try {
|
|
265
266
|
const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
|
|
266
267
|
provider,
|
|
@@ -282,10 +283,9 @@ var Auth = class {
|
|
|
282
283
|
attributes: cached.attributes ?? {}
|
|
283
284
|
});
|
|
284
285
|
mark("save_principal");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
session.
|
|
288
|
-
await this.repository.save(session);
|
|
286
|
+
signedIn = this.signedInSession(session, principal.name);
|
|
287
|
+
await this.repository.save(signedIn);
|
|
288
|
+
await this.repository.deleteById(session.getId());
|
|
289
289
|
mark("update_session");
|
|
290
290
|
} catch (error) {
|
|
291
291
|
if (error instanceof require_oauth2_error.OAuth2Error) {
|
|
@@ -303,14 +303,30 @@ var Auth = class {
|
|
|
303
303
|
status: 302,
|
|
304
304
|
headers: { location: uri.href }
|
|
305
305
|
});
|
|
306
|
-
const maxAge =
|
|
307
|
-
require_utils_http.setCookie(response, this.cookieName,
|
|
306
|
+
const maxAge = signedIn.getMaxInactiveInterval();
|
|
307
|
+
require_utils_http.setCookie(response, this.cookieName, signedIn.getId(), {
|
|
308
308
|
...this.cookieOptions,
|
|
309
309
|
maxAge
|
|
310
310
|
});
|
|
311
311
|
setTiming(response);
|
|
312
312
|
return response;
|
|
313
313
|
};
|
|
314
|
+
/**
|
|
315
|
+
* A fresh session for `name`, carrying over what the host kept in the pre-login one. The
|
|
316
|
+
* authorization request (state, PKCE verifier, flow attributes) is single-use and was consumed
|
|
317
|
+
* by this callback, and a principal left there from an earlier sign-in is replaced.
|
|
318
|
+
*/
|
|
319
|
+
signedInSession(previous, name) {
|
|
320
|
+
const session = this.repository.createSession();
|
|
321
|
+
for (const attribute of previous.getAttributeNames()) {
|
|
322
|
+
if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;
|
|
323
|
+
if (attribute === "org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME") continue;
|
|
324
|
+
const value = previous.getAttribute(attribute);
|
|
325
|
+
if (value !== null) session.setAttribute(attribute, value);
|
|
326
|
+
}
|
|
327
|
+
session.setAttribute(require_session_common.PRINCIPAL_NAME_INDEX_NAME, name);
|
|
328
|
+
return session;
|
|
329
|
+
}
|
|
314
330
|
loginOAuth2Native = async (request, handler) => {
|
|
315
331
|
(0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
|
|
316
332
|
const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
|
package/dist/auth/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["isKnownOAuth2ErrorType","attributesSize","MAX_ATTRIBUTES_BYTES","OAuth2Client","getCookie","randomUUID","PRINCIPAL_NAME_INDEX_NAME","createHash","param","timing","query","oauth2RedirectQuerySchema","OAuth2Error","bodyAttributes","googleOneTapSchema","valid","sendEmailVerificationCodeSchema","verifyTurnstileToken","resourceExhausted","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 { OAuth2Error, type OAuth2ErrorType, isKnownOAuth2ErrorType } from '../oauth2/error';\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, resourceExhausted, valid, verifyTurnstileToken } from '../utils/valid';\nimport { MAX_ATTRIBUTES_BYTES, attributesSize, bodyAttributes } from './attributes';\nimport type {\n AuthConfig,\n AuthService,\n AuthorizedHandler,\n LoggedHandler,\n OAuth2AuthorizationOptions,\n} 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\n// A native app's `code_verifier` goes on to the provider, which checks it against the challenge\n// the app sent: accept the whole RFC 7636 unreserved set here, as the provider does.\nconst CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;\n\nconst INVALID_CODE_VERIFIER = {\n error: 'invalid_request',\n error_description: 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]',\n};\n\n// error_description forwarded to errorUri comes from the provider or an error message: cap it so\n// the redirect URL stays short and an attacker-crafted callback cannot stuff the page with text.\nconst MAX_ERROR_DESCRIPTION_LENGTH = 256;\n\n/** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */\nfunction safeErrorType(error: string, fallback: OAuth2ErrorType): OAuth2ErrorType {\n return isKnownOAuth2ErrorType(error) ? error : fallback;\n}\n\n/**\n * The codes a failure past validation keeps on errorUri, each one the user can act on: the code\n * expired or was used (start again), they declined, or the provider is briefly down. Every other\n * failure there (a rejected client, a user info request that failed) is the app's or the\n * provider's problem, which the user sees as `server_error`.\n */\nconst CALLBACK_ERROR_TYPES: ReadonlySet<OAuth2ErrorType> = new Set([\n 'invalid_grant',\n 'access_denied',\n 'temporarily_unavailable',\n]);\n\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes']\n): Record<string, string> | undefined {\n if (!attributes) return undefined;\n const defined: Record<string, string> = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined) defined[key] = value;\n }\n if (Object.keys(defined).length === 0) return undefined;\n // The host's own values, written with the session: oversized is a bug in the host, not a 400.\n const size = attributesSize(defined);\n invariant(\n size <= MAX_ATTRIBUTES_BYTES,\n `oauth2 authorization attributes are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\nconst INVALID_ATTRIBUTES = {\n error: 'invalid_request',\n error_description: `attributes must map strings to strings, at most ${MAX_ATTRIBUTES_BYTES} bytes`,\n};\n\nfunction isFormPost(request: Request) {\n const contentType = request.headers.get('content-type')?.toLowerCase();\n return (\n request.method === 'POST' && !!contentType?.startsWith('application/x-www-form-urlencoded')\n );\n}\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/:provider',\n OAUTH2_NONCE: '/oauth2/nonce/:provider',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:provider',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:provider',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:provider',\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\n/** Seconds an email verification code, and its attempt counter, stay valid. */\nconst EMAIL_VERIFICATION_CODE_TTL = 10 * 60;\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 maxVerificationAttempts: number;\n private readonly verificationResendCooldown: number;\n private readonly oauth2Client: OAuth2Client | null;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\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 this.maxVerificationAttempts = verificationCode?.maxAttempts ?? 5;\n this.verificationResendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(this.maxVerificationAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(\n this.verificationResendCooldown >= 1,\n 'verificationCode.resendCooldown must be at least 1 second'\n );\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 // A Set-Cookie only replaces the stored cookie when Domain and Path match (RFC 6265),\n // so clear it with the same attributes it was set with.\n deleteCookie(response, this.cookieName, this.cookieOptions);\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 getEmailVerificationAttemptsKey(email: string) {\n return `email:verification-attempts:${email}`;\n }\n\n private getEmailVerificationCooldownKey(email: string) {\n return `email:verification-cooldown:${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 { provider } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), provider, 10 * 60);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, 10 * 60);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = async (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, 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 provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(attributes ? { attributes } : {}),\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) {\n uri.searchParams.set('error_description', description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));\n }\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 { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (isFormPost(request)) {\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 // The provider answered with an error instead of a code (the user pressed Cancel →\n // access_denied, invalid_scope, server_error, ...). Forward it rather than reporting a\n // malformed query. This runs before the state check on purpose: providers echo state on\n // error responses, but not all reliably do, and a forged error callback can only make the\n // victim's popup show an error — nothing is exchanged or signed in. A present state that does\n // not match still wins, since that is the more accurate (and more alarming) diagnosis.\n if (parsed.data.error) {\n if (parsed.data.state && parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n return this.redirect(\n safeErrorType(parsed.data.error, 'server_error'),\n parsed.data.error_description\n );\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 (provider !== cached.provider) {\n return this.redirect('invalid_request', 'redirect provider mismatch');\n }\n mark('validate_session');\n\n // 4. exchange authorization code for token and get user info\n const { code, user } = parsed.data;\n\n // Everything past validation can fail (an expired or reused code, a network error, the host's\n // handler or session store). The callback runs in a popup the user is looking at, so every\n // failure must land on errorUri instead of surfacing as a raw JSON error response.\n try {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code,\n pkce: cached.additionalParameters,\n });\n // Apple posts the user's name alongside the code, once: hand it to the provider with the token.\n const userInfo = await this.oauth2Client.getUserInfo({\n provider,\n clientId,\n token,\n response: user ? { user } : undefined,\n });\n mark('exchange_code');\n\n // 5. create or update principal, with what the host attached when the flow started\n const principal = await handler(request, {\n provider,\n clientId,\n userInfo,\n token,\n attributes: cached.attributes ?? {},\n });\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 } catch (error) {\n // The details stay in the logs: an OAuth2Error message can carry a client id or a\n // provider's response, which has no place in a URL the browser keeps in its history.\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 callback failed', error);\n const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : 'server_error';\n return this.redirect(type);\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error');\n }\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\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 { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const body: unknown = await request.json();\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\n if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });\n // Only the credentials go on to the provider client.\n const { attributes: _, ...credentials } = body as NativeCredential & { attributes?: unknown };\n const { code_verifier } = credentials as { code_verifier?: unknown };\n if (\n code_verifier !== undefined &&\n (typeof code_verifier !== 'string' || !CODE_VERIFIER_FORMAT.test(code_verifier))\n ) {\n return Response.json(INVALID_CODE_VERIFIER, { status: 400 });\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\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 !== provider) {\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, { provider, clientId, userInfo, token, attributes });\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 provider = '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 // Google's own form post carries no attributes; an app posting JSON may add them.\n let body: Record<string, string>;\n let attributes: Record<string, string> | null = {};\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 const json: unknown = await request.json();\n attributes = bodyAttributes(json);\n body = 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 if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });\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 // Both the credential and the code are issued to the provider's web client.\n let principal: Principal;\n if (parsed.credential) {\n const { clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n });\n principal = await handler(request, { provider, clientId, userInfo, token, attributes });\n } else if (parsed.code) {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n principal = await handler(request, { provider, clientId, userInfo, token, attributes });\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 // The cooldown is claimed before sending, so of concurrent requests exactly one sends. A failed\n // delivery releases it again: no code was stored, so an immediate retry gains an attacker\n // nothing, while keeping it would lock the user out of signing in for the whole cooldown.\n const cooldownKey = this.getEmailVerificationCooldownKey(email);\n if ((await this.kv.increment(cooldownKey, this.verificationResendCooldown)) > 1) {\n const error = resourceExhausted(\n 'VERIFICATION_CODE_RESEND_TOO_SOON',\n 'Verification code was sent recently'\n );\n return Response.json(error, { status: error.error.code });\n }\n\n try {\n // Sent before it is stored, so a failed delivery leaves the previous code usable.\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n await sender({ email, verificationCode });\n // The new code gets the full attempt budget; the cooldown bounds how often that happens.\n // Reset before storing, so every comparison against the new code is counted.\n await this.kv.removeItem(this.getEmailVerificationAttemptsKey(email));\n await this.kv.setItem(\n this.getEmailVerificationCodeKey(email),\n verificationCode,\n EMAIL_VERIFICATION_CODE_TTL\n );\n return Response.json({});\n } catch (e) {\n console.error('Failed to send email verification code', e);\n await this.kv.removeItem(cooldownKey);\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, attributes } = data;\n\n const key = this.getEmailVerificationCodeKey(email);\n const attemptsKey = this.getEmailVerificationAttemptsKey(email);\n const stored = await this.kv.getItem(key);\n if (typeof stored !== 'string') {\n return Response.json(invalidArgument('INVALID_VERIFICATION_CODE'), { status: 400 });\n }\n\n // Count the attempt before comparing: the atomic increment hands every request, parallel ones\n // included, a distinct count, so at most maxAttempts comparisons are ever made against a code.\n const attempts = await this.kv.increment(attemptsKey, EMAIL_VERIFICATION_CODE_TTL);\n if (attempts > this.maxVerificationAttempts) {\n // Only the code is discarded. The counter stays until it expires (or a new code is sent):\n // removing it would hand requests that already read the code a fresh budget.\n await this.kv.removeItem(key);\n const error = invalidArgument('VERIFICATION_CODE_ATTEMPTS_EXCEEDED');\n return Response.json(error, { status: error.error.code });\n }\n if (\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 await this.kv.removeItem(attemptsKey);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n const principal = await handler(request, {\n provider: 'EMAIL',\n userInfo: { claims, data: {} },\n attributes: attributes ?? {},\n });\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":["isKnownOAuth2ErrorType","attributesSize","MAX_ATTRIBUTES_BYTES","OAuth2Client","getCookie","randomUUID","PRINCIPAL_NAME_INDEX_NAME","createHash","param","timing","query","oauth2RedirectQuerySchema","OAuth2Error","bodyAttributes","googleOneTapSchema","valid","sendEmailVerificationCodeSchema","verifyTurnstileToken","resourceExhausted","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 { OAuth2Error, type OAuth2ErrorType, isKnownOAuth2ErrorType } from '../oauth2/error';\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, resourceExhausted, valid, verifyTurnstileToken } from '../utils/valid';\nimport { MAX_ATTRIBUTES_BYTES, attributesSize, bodyAttributes } from './attributes';\nimport type {\n AuthConfig,\n AuthService,\n AuthorizedHandler,\n LoggedHandler,\n OAuth2AuthorizationOptions,\n} 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\n// A native app's `code_verifier` goes on to the provider, which checks it against the challenge\n// the app sent: accept the whole RFC 7636 unreserved set here, as the provider does.\nconst CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;\n\nconst INVALID_CODE_VERIFIER = {\n error: 'invalid_request',\n error_description: 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]',\n};\n\n// error_description forwarded to errorUri comes from the provider or an error message: cap it so\n// the redirect URL stays short and an attacker-crafted callback cannot stuff the page with text.\nconst MAX_ERROR_DESCRIPTION_LENGTH = 256;\n\n/** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */\nfunction safeErrorType(error: string, fallback: OAuth2ErrorType): OAuth2ErrorType {\n return isKnownOAuth2ErrorType(error) ? error : fallback;\n}\n\n/**\n * The codes a failure past validation keeps on errorUri, each one the user can act on: the code\n * expired or was used (start again), they declined, or the provider is briefly down. Every other\n * failure there (a rejected client, a user info request that failed) is the app's or the\n * provider's problem, which the user sees as `server_error`.\n */\nconst CALLBACK_ERROR_TYPES: ReadonlySet<OAuth2ErrorType> = new Set([\n 'invalid_grant',\n 'access_denied',\n 'temporarily_unavailable',\n]);\n\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes']\n): Record<string, string> | undefined {\n if (!attributes) return undefined;\n const defined: Record<string, string> = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined) defined[key] = value;\n }\n if (Object.keys(defined).length === 0) return undefined;\n // The host's own values, written with the session: oversized is a bug in the host, not a 400.\n const size = attributesSize(defined);\n invariant(\n size <= MAX_ATTRIBUTES_BYTES,\n `oauth2 authorization attributes are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\nconst INVALID_ATTRIBUTES = {\n error: 'invalid_request',\n error_description: `attributes must map strings to strings, at most ${MAX_ATTRIBUTES_BYTES} bytes`,\n};\n\nfunction isFormPost(request: Request) {\n const contentType = request.headers.get('content-type')?.toLowerCase();\n return (\n request.method === 'POST' && !!contentType?.startsWith('application/x-www-form-urlencoded')\n );\n}\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/:provider',\n OAUTH2_NONCE: '/oauth2/nonce/:provider',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:provider',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:provider',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:provider',\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\n/** Seconds an email verification code, and its attempt counter, stay valid. */\nconst EMAIL_VERIFICATION_CODE_TTL = 10 * 60;\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 maxVerificationAttempts: number;\n private readonly verificationResendCooldown: number;\n private readonly oauth2Client: OAuth2Client | null;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\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 this.maxVerificationAttempts = verificationCode?.maxAttempts ?? 5;\n this.verificationResendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(this.maxVerificationAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(\n this.verificationResendCooldown >= 1,\n 'verificationCode.resendCooldown must be at least 1 second'\n );\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 // A Set-Cookie only replaces the stored cookie when Domain and Path match (RFC 6265),\n // so clear it with the same attributes it was set with.\n deleteCookie(response, this.cookieName, this.cookieOptions);\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 getEmailVerificationAttemptsKey(email: string) {\n return `email:verification-attempts:${email}`;\n }\n\n private getEmailVerificationCooldownKey(email: string) {\n return `email:verification-cooldown:${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 { provider } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), provider, 10 * 60);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, 10 * 60);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = async (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, 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 provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(attributes ? { attributes } : {}),\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) {\n uri.searchParams.set('error_description', description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));\n }\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 { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (isFormPost(request)) {\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 // The provider answered with an error instead of a code (the user pressed Cancel →\n // access_denied, invalid_scope, server_error, ...). Forward it rather than reporting a\n // malformed query. This runs before the state check on purpose: providers echo state on\n // error responses, but not all reliably do, and a forged error callback can only make the\n // victim's popup show an error — nothing is exchanged or signed in. A present state that does\n // not match still wins, since that is the more accurate (and more alarming) diagnosis.\n if (parsed.data.error) {\n if (parsed.data.state && parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n return this.redirect(\n safeErrorType(parsed.data.error, 'server_error'),\n parsed.data.error_description\n );\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 (provider !== cached.provider) {\n return this.redirect('invalid_request', 'redirect provider mismatch');\n }\n mark('validate_session');\n\n // 4. exchange authorization code for token and get user info\n const { code, user } = parsed.data;\n\n // Everything past validation can fail (an expired or reused code, a network error, the host's\n // handler or session store). The callback runs in a popup the user is looking at, so every\n // failure must land on errorUri instead of surfacing as a raw JSON error response.\n let signedIn: Session;\n try {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code,\n pkce: cached.additionalParameters,\n });\n // Apple posts the user's name alongside the code, once: hand it to the provider with the token.\n const userInfo = await this.oauth2Client.getUserInfo({\n provider,\n clientId,\n token,\n response: user ? { user } : undefined,\n });\n mark('exchange_code');\n\n // 5. create or update principal, with what the host attached when the flow started\n const principal = await handler(request, {\n provider,\n clientId,\n userInfo,\n token,\n attributes: cached.attributes ?? {},\n });\n mark('save_principal');\n\n // 6. sign in on a new session: the id the browser brought may have been planted by an\n // attacker who started the flow (session fixation), so it must not become authenticated.\n signedIn = this.signedInSession(session, principal.name);\n await this.repository.save(signedIn);\n await this.repository.deleteById(session.getId());\n mark('update_session');\n } catch (error) {\n // The details stay in the logs: an OAuth2Error message can carry a client id or a\n // provider's response, which has no place in a URL the browser keeps in its history.\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 callback failed', error);\n const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : 'server_error';\n return this.redirect(type);\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error');\n }\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = signedIn.getMaxInactiveInterval();\n setCookie(response, this.cookieName, signedIn.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n /**\n * A fresh session for `name`, carrying over what the host kept in the pre-login one. The\n * authorization request (state, PKCE verifier, flow attributes) is single-use and was consumed\n * by this callback, and a principal left there from an earlier sign-in is replaced.\n */\n private signedInSession(previous: Session, name: string): Session {\n const session = this.repository.createSession();\n for (const attribute of previous.getAttributeNames()) {\n if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;\n if (attribute === PRINCIPAL_NAME_INDEX_NAME) continue;\n const value = previous.getAttribute(attribute);\n if (value !== null) session.setAttribute(attribute, value);\n }\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, name);\n return session;\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 { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const body: unknown = await request.json();\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\n if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });\n // Only the credentials go on to the provider client.\n const { attributes: _, ...credentials } = body as NativeCredential & { attributes?: unknown };\n const { code_verifier } = credentials as { code_verifier?: unknown };\n if (\n code_verifier !== undefined &&\n (typeof code_verifier !== 'string' || !CODE_VERIFIER_FORMAT.test(code_verifier))\n ) {\n return Response.json(INVALID_CODE_VERIFIER, { status: 400 });\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\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 !== provider) {\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, { provider, clientId, userInfo, token, attributes });\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 provider = '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 // Google's own form post carries no attributes; an app posting JSON may add them.\n let body: Record<string, string>;\n let attributes: Record<string, string> | null = {};\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 const json: unknown = await request.json();\n attributes = bodyAttributes(json);\n body = 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 if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });\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 // Both the credential and the code are issued to the provider's web client.\n let principal: Principal;\n if (parsed.credential) {\n const { clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n });\n principal = await handler(request, { provider, clientId, userInfo, token, attributes });\n } else if (parsed.code) {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n principal = await handler(request, { provider, clientId, userInfo, token, attributes });\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 // The cooldown is claimed before sending, so of concurrent requests exactly one sends. A failed\n // delivery releases it again: no code was stored, so an immediate retry gains an attacker\n // nothing, while keeping it would lock the user out of signing in for the whole cooldown.\n const cooldownKey = this.getEmailVerificationCooldownKey(email);\n if ((await this.kv.increment(cooldownKey, this.verificationResendCooldown)) > 1) {\n const error = resourceExhausted(\n 'VERIFICATION_CODE_RESEND_TOO_SOON',\n 'Verification code was sent recently'\n );\n return Response.json(error, { status: error.error.code });\n }\n\n try {\n // Sent before it is stored, so a failed delivery leaves the previous code usable.\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n await sender({ email, verificationCode });\n // The new code gets the full attempt budget; the cooldown bounds how often that happens.\n // Reset before storing, so every comparison against the new code is counted.\n await this.kv.removeItem(this.getEmailVerificationAttemptsKey(email));\n await this.kv.setItem(\n this.getEmailVerificationCodeKey(email),\n verificationCode,\n EMAIL_VERIFICATION_CODE_TTL\n );\n return Response.json({});\n } catch (e) {\n console.error('Failed to send email verification code', e);\n await this.kv.removeItem(cooldownKey);\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, attributes } = data;\n\n const key = this.getEmailVerificationCodeKey(email);\n const attemptsKey = this.getEmailVerificationAttemptsKey(email);\n const stored = await this.kv.getItem(key);\n if (typeof stored !== 'string') {\n return Response.json(invalidArgument('INVALID_VERIFICATION_CODE'), { status: 400 });\n }\n\n // Count the attempt before comparing: the atomic increment hands every request, parallel ones\n // included, a distinct count, so at most maxAttempts comparisons are ever made against a code.\n const attempts = await this.kv.increment(attemptsKey, EMAIL_VERIFICATION_CODE_TTL);\n if (attempts > this.maxVerificationAttempts) {\n // Only the code is discarded. The counter stays until it expires (or a new code is sent):\n // removing it would hand requests that already read the code a fresh budget.\n await this.kv.removeItem(key);\n const error = invalidArgument('VERIFICATION_CODE_ATTEMPTS_EXCEEDED');\n return Response.json(error, { status: error.error.code });\n }\n if (\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 await this.kv.removeItem(attemptsKey);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n const principal = await handler(request, {\n provider: 'EMAIL',\n userInfo: { claims, data: {} },\n attributes: attributes ?? {},\n });\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.d.cts
CHANGED
|
@@ -49,6 +49,12 @@ export declare class Auth implements AuthService {
|
|
|
49
49
|
oauth2Authorization: (request: Request, options?: OAuth2AuthorizationOptions) => Promise<Response>;
|
|
50
50
|
private readonly redirect;
|
|
51
51
|
loginOAuth2Code: (request: Request, handler: AuthorizedHandler) => Promise<Response>;
|
|
52
|
+
/**
|
|
53
|
+
* A fresh session for `name`, carrying over what the host kept in the pre-login one. The
|
|
54
|
+
* authorization request (state, PKCE verifier, flow attributes) is single-use and was consumed
|
|
55
|
+
* by this callback, and a principal left there from an earlier sign-in is replaced.
|
|
56
|
+
*/
|
|
57
|
+
private signedInSession;
|
|
52
58
|
loginOAuth2Native: (request: Request, handler: AuthorizedHandler) => Promise<Response>;
|
|
53
59
|
loginOAuth2Onetap: (request: Request, handler: AuthorizedHandler, provider?: string) => Promise<Response>;
|
|
54
60
|
sendEmailVerificationCode: (request: Request, sender: (data: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/auth/index.ts"],"mappings":";;;;qBA4Ga;WACX;WACA;WACA;WAEA;WACA;WACA;WACA;WACA;WACA;WAEA;WACA;WACA;WACA;WASA;WACA;WAEA;;qBAMW,gBAAgB;mBACV;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;EAEjB,cACE,mBACA,cACA,oBACA,kBACA,QACA,QACA,UACC;UAwBK;EAYR,OAAI,UAAoB,YAAU,QAAQ;EAmB1C,SAAM,SAAmB,YAAU,QAAQ;EAU3C,SAAM,SAAmB,SAAO,WAAa,kBAAgB,QAAQ;UAsB7D;UAUA;UAIA;UAIA;UAIA;UAIA;UAIA;UAIA;EAIR,cAAW,SAAmB,YAAU,QAAQ;EAOhD,cAAW,SAAmB,YAAU,QAAQ;EAOhD,sBAAmB,SACR,SAAO,UACN,+BACT,QAAQ;mBAmCM;EAUjB,kBAAe,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../../src/auth/index.ts"],"mappings":";;;;qBA4Ga;WACX;WACA;WACA;WAEA;WACA;WACA;WACA;WACA;WACA;WAEA;WACA;WACA;WACA;WASA;WACA;WAEA;;qBAMW,gBAAgB;mBACV;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;EAEjB,cACE,mBACA,cACA,oBACA,kBACA,QACA,QACA,UACC;UAwBK;EAYR,OAAI,UAAoB,YAAU,QAAQ;EAmB1C,SAAM,SAAmB,YAAU,QAAQ;EAU3C,SAAM,SAAmB,SAAO,WAAa,kBAAgB,QAAQ;UAsB7D;UAUA;UAIA;UAIA;UAIA;UAIA;UAIA;UAIA;EAIR,cAAW,SAAmB,YAAU,QAAQ;EAOhD,cAAW,SAAmB,YAAU,QAAQ;EAOhD,sBAAmB,SACR,SAAO,UACN,+BACT,QAAQ;mBAmCM;EAUjB,kBAAe,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;;;;;;UA+HxE;EAYR,oBAAiB,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;EAmDlF,oBAAiB,SACN,SAAO,SACP,mBAAiB,sBAEzB,QAAQ;EAkEX,4BAAyB,SACd,SAAO,SACP;IAAQ;IAAe;QAA+B,kBAC9D,QAAQ;EA0CX,aAAU,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;;;;;;;;;;;;;;;;;;;;EAsE3E,mBAAgB,SAAmB,YAAU,QAAQ;;;;;;;;;;;;;;;;;;;EA0DrD,kBAAe,SAAmB,YAAU,QAAQ;EAgEpD,4BAAyB,UACb,SAAO,UACP;IAAQ;IAAe;QAA+B,kBAC/D,QAAQ;EAIX,aAAU,UAAoB,SAAO,UAAY,sBAAoB,QAAQ;EAI7E,OAAI,WAAqB,cAAY;EAOrC,kBAAe,SAAmB,YAAU;EAU5C,aAAoB,mBAAiB,SAC1B,SAAO,QACR,MACP,QAAQ,iBAAiB,UAAU;EAwBtC,gBAAa,sBAA8B;EAI3C,eAAY,SAAmB,YAAU,QAAQ;EAUjD,eAAY,WAAqB,cAAY,QAAQ;EAKrD,yBAAsB,0BAAkC,QAAQ"}
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -49,6 +49,12 @@ export declare class Auth implements AuthService {
|
|
|
49
49
|
oauth2Authorization: (request: Request, options?: OAuth2AuthorizationOptions) => Promise<Response>;
|
|
50
50
|
private readonly redirect;
|
|
51
51
|
loginOAuth2Code: (request: Request, handler: AuthorizedHandler) => Promise<Response>;
|
|
52
|
+
/**
|
|
53
|
+
* A fresh session for `name`, carrying over what the host kept in the pre-login one. The
|
|
54
|
+
* authorization request (state, PKCE verifier, flow attributes) is single-use and was consumed
|
|
55
|
+
* by this callback, and a principal left there from an earlier sign-in is replaced.
|
|
56
|
+
*/
|
|
57
|
+
private signedInSession;
|
|
52
58
|
loginOAuth2Native: (request: Request, handler: AuthorizedHandler) => Promise<Response>;
|
|
53
59
|
loginOAuth2Onetap: (request: Request, handler: AuthorizedHandler, provider?: string) => Promise<Response>;
|
|
54
60
|
sendEmailVerificationCode: (request: Request, sender: (data: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/auth/index.ts"],"mappings":";;;;qBA4Ga;WACX;WACA;WACA;WAEA;WACA;WACA;WACA;WACA;WACA;WAEA;WACA;WACA;WACA;WASA;WACA;WAEA;;qBAMW,gBAAgB;mBACV;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;EAEjB,cACE,mBACA,cACA,oBACA,kBACA,QACA,QACA,UACC;UAwBK;EAYR,OAAI,UAAoB,YAAU,QAAQ;EAmB1C,SAAM,SAAmB,YAAU,QAAQ;EAU3C,SAAM,SAAmB,SAAO,WAAa,kBAAgB,QAAQ;UAsB7D;UAUA;UAIA;UAIA;UAIA;UAIA;UAIA;UAIA;EAIR,cAAW,SAAmB,YAAU,QAAQ;EAOhD,cAAW,SAAmB,YAAU,QAAQ;EAOhD,sBAAmB,SACR,SAAO,UACN,+BACT,QAAQ;mBAmCM;EAUjB,kBAAe,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/auth/index.ts"],"mappings":";;;;qBA4Ga;WACX;WACA;WACA;WAEA;WACA;WACA;WACA;WACA;WACA;WAEA;WACA;WACA;WACA;WASA;WACA;WAEA;;qBAMW,gBAAgB;mBACV;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;EAEjB,cACE,mBACA,cACA,oBACA,kBACA,QACA,QACA,UACC;UAwBK;EAYR,OAAI,UAAoB,YAAU,QAAQ;EAmB1C,SAAM,SAAmB,YAAU,QAAQ;EAU3C,SAAM,SAAmB,SAAO,WAAa,kBAAgB,QAAQ;UAsB7D;UAUA;UAIA;UAIA;UAIA;UAIA;UAIA;UAIA;EAIR,cAAW,SAAmB,YAAU,QAAQ;EAOhD,cAAW,SAAmB,YAAU,QAAQ;EAOhD,sBAAmB,SACR,SAAO,UACN,+BACT,QAAQ;mBAmCM;EAUjB,kBAAe,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;;;;;;UA+HxE;EAYR,oBAAiB,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;EAmDlF,oBAAiB,SACN,SAAO,SACP,mBAAiB,sBAEzB,QAAQ;EAkEX,4BAAyB,SACd,SAAO,SACP;IAAQ;IAAe;QAA+B,kBAC9D,QAAQ;EA0CX,aAAU,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;;;;;;;;;;;;;;;;;;;;EAsE3E,mBAAgB,SAAmB,YAAU,QAAQ;;;;;;;;;;;;;;;;;;;EA0DrD,kBAAe,SAAmB,YAAU,QAAQ;EAgEpD,4BAAyB,UACb,SAAO,UACP;IAAQ;IAAe;QAA+B,kBAC/D,QAAQ;EAIX,aAAU,UAAoB,SAAO,UAAY,sBAAoB,QAAQ;EAI7E,OAAI,WAAqB,cAAY;EAOrC,kBAAe,SAAmB,YAAU;EAU5C,aAAoB,mBAAiB,SAC1B,SAAO,QACR,MACP,QAAQ,iBAAiB,UAAU;EAwBtC,gBAAa,sBAA8B;EAI3C,eAAY,SAAmB,YAAU,QAAQ;EAUjD,eAAY,WAAqB,cAAY,QAAQ;EAKrD,yBAAsB,0BAAkC,QAAQ"}
|
package/dist/auth/index.mjs
CHANGED
|
@@ -260,6 +260,7 @@ var Auth = class {
|
|
|
260
260
|
if (provider !== cached.provider) return this.redirect("invalid_request", "redirect provider mismatch");
|
|
261
261
|
mark("validate_session");
|
|
262
262
|
const { code, user } = parsed.data;
|
|
263
|
+
let signedIn;
|
|
263
264
|
try {
|
|
264
265
|
const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({
|
|
265
266
|
provider,
|
|
@@ -281,10 +282,9 @@ var Auth = class {
|
|
|
281
282
|
attributes: cached.attributes ?? {}
|
|
282
283
|
});
|
|
283
284
|
mark("save_principal");
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
session.
|
|
287
|
-
await this.repository.save(session);
|
|
285
|
+
signedIn = this.signedInSession(session, principal.name);
|
|
286
|
+
await this.repository.save(signedIn);
|
|
287
|
+
await this.repository.deleteById(session.getId());
|
|
288
288
|
mark("update_session");
|
|
289
289
|
} catch (error) {
|
|
290
290
|
if (error instanceof OAuth2Error) {
|
|
@@ -302,14 +302,30 @@ var Auth = class {
|
|
|
302
302
|
status: 302,
|
|
303
303
|
headers: { location: uri.href }
|
|
304
304
|
});
|
|
305
|
-
const maxAge =
|
|
306
|
-
setCookie(response, this.cookieName,
|
|
305
|
+
const maxAge = signedIn.getMaxInactiveInterval();
|
|
306
|
+
setCookie(response, this.cookieName, signedIn.getId(), {
|
|
307
307
|
...this.cookieOptions,
|
|
308
308
|
maxAge
|
|
309
309
|
});
|
|
310
310
|
setTiming(response);
|
|
311
311
|
return response;
|
|
312
312
|
};
|
|
313
|
+
/**
|
|
314
|
+
* A fresh session for `name`, carrying over what the host kept in the pre-login one. The
|
|
315
|
+
* authorization request (state, PKCE verifier, flow attributes) is single-use and was consumed
|
|
316
|
+
* by this callback, and a principal left there from an earlier sign-in is replaced.
|
|
317
|
+
*/
|
|
318
|
+
signedInSession(previous, name) {
|
|
319
|
+
const session = this.repository.createSession();
|
|
320
|
+
for (const attribute of previous.getAttributeNames()) {
|
|
321
|
+
if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;
|
|
322
|
+
if (attribute === "org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME") continue;
|
|
323
|
+
const value = previous.getAttribute(attribute);
|
|
324
|
+
if (value !== null) session.setAttribute(attribute, value);
|
|
325
|
+
}
|
|
326
|
+
session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, name);
|
|
327
|
+
return session;
|
|
328
|
+
}
|
|
313
329
|
loginOAuth2Native = async (request, handler) => {
|
|
314
330
|
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
315
331
|
const { mark, setTiming } = timing({ enabled: this.timing });
|
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 { OAuth2Error, type OAuth2ErrorType, isKnownOAuth2ErrorType } from '../oauth2/error';\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, resourceExhausted, valid, verifyTurnstileToken } from '../utils/valid';\nimport { MAX_ATTRIBUTES_BYTES, attributesSize, bodyAttributes } from './attributes';\nimport type {\n AuthConfig,\n AuthService,\n AuthorizedHandler,\n LoggedHandler,\n OAuth2AuthorizationOptions,\n} 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\n// A native app's `code_verifier` goes on to the provider, which checks it against the challenge\n// the app sent: accept the whole RFC 7636 unreserved set here, as the provider does.\nconst CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;\n\nconst INVALID_CODE_VERIFIER = {\n error: 'invalid_request',\n error_description: 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]',\n};\n\n// error_description forwarded to errorUri comes from the provider or an error message: cap it so\n// the redirect URL stays short and an attacker-crafted callback cannot stuff the page with text.\nconst MAX_ERROR_DESCRIPTION_LENGTH = 256;\n\n/** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */\nfunction safeErrorType(error: string, fallback: OAuth2ErrorType): OAuth2ErrorType {\n return isKnownOAuth2ErrorType(error) ? error : fallback;\n}\n\n/**\n * The codes a failure past validation keeps on errorUri, each one the user can act on: the code\n * expired or was used (start again), they declined, or the provider is briefly down. Every other\n * failure there (a rejected client, a user info request that failed) is the app's or the\n * provider's problem, which the user sees as `server_error`.\n */\nconst CALLBACK_ERROR_TYPES: ReadonlySet<OAuth2ErrorType> = new Set([\n 'invalid_grant',\n 'access_denied',\n 'temporarily_unavailable',\n]);\n\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes']\n): Record<string, string> | undefined {\n if (!attributes) return undefined;\n const defined: Record<string, string> = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined) defined[key] = value;\n }\n if (Object.keys(defined).length === 0) return undefined;\n // The host's own values, written with the session: oversized is a bug in the host, not a 400.\n const size = attributesSize(defined);\n invariant(\n size <= MAX_ATTRIBUTES_BYTES,\n `oauth2 authorization attributes are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\nconst INVALID_ATTRIBUTES = {\n error: 'invalid_request',\n error_description: `attributes must map strings to strings, at most ${MAX_ATTRIBUTES_BYTES} bytes`,\n};\n\nfunction isFormPost(request: Request) {\n const contentType = request.headers.get('content-type')?.toLowerCase();\n return (\n request.method === 'POST' && !!contentType?.startsWith('application/x-www-form-urlencoded')\n );\n}\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/:provider',\n OAUTH2_NONCE: '/oauth2/nonce/:provider',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:provider',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:provider',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:provider',\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\n/** Seconds an email verification code, and its attempt counter, stay valid. */\nconst EMAIL_VERIFICATION_CODE_TTL = 10 * 60;\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 maxVerificationAttempts: number;\n private readonly verificationResendCooldown: number;\n private readonly oauth2Client: OAuth2Client | null;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\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 this.maxVerificationAttempts = verificationCode?.maxAttempts ?? 5;\n this.verificationResendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(this.maxVerificationAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(\n this.verificationResendCooldown >= 1,\n 'verificationCode.resendCooldown must be at least 1 second'\n );\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 // A Set-Cookie only replaces the stored cookie when Domain and Path match (RFC 6265),\n // so clear it with the same attributes it was set with.\n deleteCookie(response, this.cookieName, this.cookieOptions);\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 getEmailVerificationAttemptsKey(email: string) {\n return `email:verification-attempts:${email}`;\n }\n\n private getEmailVerificationCooldownKey(email: string) {\n return `email:verification-cooldown:${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 { provider } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), provider, 10 * 60);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, 10 * 60);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = async (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, 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 provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(attributes ? { attributes } : {}),\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) {\n uri.searchParams.set('error_description', description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));\n }\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 { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (isFormPost(request)) {\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 // The provider answered with an error instead of a code (the user pressed Cancel →\n // access_denied, invalid_scope, server_error, ...). Forward it rather than reporting a\n // malformed query. This runs before the state check on purpose: providers echo state on\n // error responses, but not all reliably do, and a forged error callback can only make the\n // victim's popup show an error — nothing is exchanged or signed in. A present state that does\n // not match still wins, since that is the more accurate (and more alarming) diagnosis.\n if (parsed.data.error) {\n if (parsed.data.state && parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n return this.redirect(\n safeErrorType(parsed.data.error, 'server_error'),\n parsed.data.error_description\n );\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 (provider !== cached.provider) {\n return this.redirect('invalid_request', 'redirect provider mismatch');\n }\n mark('validate_session');\n\n // 4. exchange authorization code for token and get user info\n const { code, user } = parsed.data;\n\n // Everything past validation can fail (an expired or reused code, a network error, the host's\n // handler or session store). The callback runs in a popup the user is looking at, so every\n // failure must land on errorUri instead of surfacing as a raw JSON error response.\n try {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code,\n pkce: cached.additionalParameters,\n });\n // Apple posts the user's name alongside the code, once: hand it to the provider with the token.\n const userInfo = await this.oauth2Client.getUserInfo({\n provider,\n clientId,\n token,\n response: user ? { user } : undefined,\n });\n mark('exchange_code');\n\n // 5. create or update principal, with what the host attached when the flow started\n const principal = await handler(request, {\n provider,\n clientId,\n userInfo,\n token,\n attributes: cached.attributes ?? {},\n });\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 } catch (error) {\n // The details stay in the logs: an OAuth2Error message can carry a client id or a\n // provider's response, which has no place in a URL the browser keeps in its history.\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 callback failed', error);\n const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : 'server_error';\n return this.redirect(type);\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error');\n }\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\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 { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const body: unknown = await request.json();\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\n if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });\n // Only the credentials go on to the provider client.\n const { attributes: _, ...credentials } = body as NativeCredential & { attributes?: unknown };\n const { code_verifier } = credentials as { code_verifier?: unknown };\n if (\n code_verifier !== undefined &&\n (typeof code_verifier !== 'string' || !CODE_VERIFIER_FORMAT.test(code_verifier))\n ) {\n return Response.json(INVALID_CODE_VERIFIER, { status: 400 });\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\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 !== provider) {\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, { provider, clientId, userInfo, token, attributes });\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 provider = '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 // Google's own form post carries no attributes; an app posting JSON may add them.\n let body: Record<string, string>;\n let attributes: Record<string, string> | null = {};\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 const json: unknown = await request.json();\n attributes = bodyAttributes(json);\n body = 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 if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });\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 // Both the credential and the code are issued to the provider's web client.\n let principal: Principal;\n if (parsed.credential) {\n const { clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n });\n principal = await handler(request, { provider, clientId, userInfo, token, attributes });\n } else if (parsed.code) {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n principal = await handler(request, { provider, clientId, userInfo, token, attributes });\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 // The cooldown is claimed before sending, so of concurrent requests exactly one sends. A failed\n // delivery releases it again: no code was stored, so an immediate retry gains an attacker\n // nothing, while keeping it would lock the user out of signing in for the whole cooldown.\n const cooldownKey = this.getEmailVerificationCooldownKey(email);\n if ((await this.kv.increment(cooldownKey, this.verificationResendCooldown)) > 1) {\n const error = resourceExhausted(\n 'VERIFICATION_CODE_RESEND_TOO_SOON',\n 'Verification code was sent recently'\n );\n return Response.json(error, { status: error.error.code });\n }\n\n try {\n // Sent before it is stored, so a failed delivery leaves the previous code usable.\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n await sender({ email, verificationCode });\n // The new code gets the full attempt budget; the cooldown bounds how often that happens.\n // Reset before storing, so every comparison against the new code is counted.\n await this.kv.removeItem(this.getEmailVerificationAttemptsKey(email));\n await this.kv.setItem(\n this.getEmailVerificationCodeKey(email),\n verificationCode,\n EMAIL_VERIFICATION_CODE_TTL\n );\n return Response.json({});\n } catch (e) {\n console.error('Failed to send email verification code', e);\n await this.kv.removeItem(cooldownKey);\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, attributes } = data;\n\n const key = this.getEmailVerificationCodeKey(email);\n const attemptsKey = this.getEmailVerificationAttemptsKey(email);\n const stored = await this.kv.getItem(key);\n if (typeof stored !== 'string') {\n return Response.json(invalidArgument('INVALID_VERIFICATION_CODE'), { status: 400 });\n }\n\n // Count the attempt before comparing: the atomic increment hands every request, parallel ones\n // included, a distinct count, so at most maxAttempts comparisons are ever made against a code.\n const attempts = await this.kv.increment(attemptsKey, EMAIL_VERIFICATION_CODE_TTL);\n if (attempts > this.maxVerificationAttempts) {\n // Only the code is discarded. The counter stays until it expires (or a new code is sent):\n // removing it would hand requests that already read the code a fresh budget.\n await this.kv.removeItem(key);\n const error = invalidArgument('VERIFICATION_CODE_ATTEMPTS_EXCEEDED');\n return Response.json(error, { status: error.error.code });\n }\n if (\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 await this.kv.removeItem(attemptsKey);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n const principal = await handler(request, {\n provider: 'EMAIL',\n userInfo: { claims, data: {} },\n attributes: attributes ?? {},\n });\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 { OAuth2Error, type OAuth2ErrorType, isKnownOAuth2ErrorType } from '../oauth2/error';\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, resourceExhausted, valid, verifyTurnstileToken } from '../utils/valid';\nimport { MAX_ATTRIBUTES_BYTES, attributesSize, bodyAttributes } from './attributes';\nimport type {\n AuthConfig,\n AuthService,\n AuthorizedHandler,\n LoggedHandler,\n OAuth2AuthorizationOptions,\n} 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\n// A native app's `code_verifier` goes on to the provider, which checks it against the challenge\n// the app sent: accept the whole RFC 7636 unreserved set here, as the provider does.\nconst CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;\n\nconst INVALID_CODE_VERIFIER = {\n error: 'invalid_request',\n error_description: 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]',\n};\n\n// error_description forwarded to errorUri comes from the provider or an error message: cap it so\n// the redirect URL stays short and an attacker-crafted callback cannot stuff the page with text.\nconst MAX_ERROR_DESCRIPTION_LENGTH = 256;\n\n/** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */\nfunction safeErrorType(error: string, fallback: OAuth2ErrorType): OAuth2ErrorType {\n return isKnownOAuth2ErrorType(error) ? error : fallback;\n}\n\n/**\n * The codes a failure past validation keeps on errorUri, each one the user can act on: the code\n * expired or was used (start again), they declined, or the provider is briefly down. Every other\n * failure there (a rejected client, a user info request that failed) is the app's or the\n * provider's problem, which the user sees as `server_error`.\n */\nconst CALLBACK_ERROR_TYPES: ReadonlySet<OAuth2ErrorType> = new Set([\n 'invalid_grant',\n 'access_denied',\n 'temporarily_unavailable',\n]);\n\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes']\n): Record<string, string> | undefined {\n if (!attributes) return undefined;\n const defined: Record<string, string> = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined) defined[key] = value;\n }\n if (Object.keys(defined).length === 0) return undefined;\n // The host's own values, written with the session: oversized is a bug in the host, not a 400.\n const size = attributesSize(defined);\n invariant(\n size <= MAX_ATTRIBUTES_BYTES,\n `oauth2 authorization attributes are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\nconst INVALID_ATTRIBUTES = {\n error: 'invalid_request',\n error_description: `attributes must map strings to strings, at most ${MAX_ATTRIBUTES_BYTES} bytes`,\n};\n\nfunction isFormPost(request: Request) {\n const contentType = request.headers.get('content-type')?.toLowerCase();\n return (\n request.method === 'POST' && !!contentType?.startsWith('application/x-www-form-urlencoded')\n );\n}\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/:provider',\n OAUTH2_NONCE: '/oauth2/nonce/:provider',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:provider',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:provider',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:provider',\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\n/** Seconds an email verification code, and its attempt counter, stay valid. */\nconst EMAIL_VERIFICATION_CODE_TTL = 10 * 60;\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 maxVerificationAttempts: number;\n private readonly verificationResendCooldown: number;\n private readonly oauth2Client: OAuth2Client | null;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\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 this.maxVerificationAttempts = verificationCode?.maxAttempts ?? 5;\n this.verificationResendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(this.maxVerificationAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(\n this.verificationResendCooldown >= 1,\n 'verificationCode.resendCooldown must be at least 1 second'\n );\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 // A Set-Cookie only replaces the stored cookie when Domain and Path match (RFC 6265),\n // so clear it with the same attributes it was set with.\n deleteCookie(response, this.cookieName, this.cookieOptions);\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 getEmailVerificationAttemptsKey(email: string) {\n return `email:verification-attempts:${email}`;\n }\n\n private getEmailVerificationCooldownKey(email: string) {\n return `email:verification-cooldown:${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 { provider } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), provider, 10 * 60);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, 10 * 60);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = async (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, 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 provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(attributes ? { attributes } : {}),\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) {\n uri.searchParams.set('error_description', description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));\n }\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 { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (isFormPost(request)) {\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 // The provider answered with an error instead of a code (the user pressed Cancel →\n // access_denied, invalid_scope, server_error, ...). Forward it rather than reporting a\n // malformed query. This runs before the state check on purpose: providers echo state on\n // error responses, but not all reliably do, and a forged error callback can only make the\n // victim's popup show an error — nothing is exchanged or signed in. A present state that does\n // not match still wins, since that is the more accurate (and more alarming) diagnosis.\n if (parsed.data.error) {\n if (parsed.data.state && parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n return this.redirect(\n safeErrorType(parsed.data.error, 'server_error'),\n parsed.data.error_description\n );\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 (provider !== cached.provider) {\n return this.redirect('invalid_request', 'redirect provider mismatch');\n }\n mark('validate_session');\n\n // 4. exchange authorization code for token and get user info\n const { code, user } = parsed.data;\n\n // Everything past validation can fail (an expired or reused code, a network error, the host's\n // handler or session store). The callback runs in a popup the user is looking at, so every\n // failure must land on errorUri instead of surfacing as a raw JSON error response.\n let signedIn: Session;\n try {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code,\n pkce: cached.additionalParameters,\n });\n // Apple posts the user's name alongside the code, once: hand it to the provider with the token.\n const userInfo = await this.oauth2Client.getUserInfo({\n provider,\n clientId,\n token,\n response: user ? { user } : undefined,\n });\n mark('exchange_code');\n\n // 5. create or update principal, with what the host attached when the flow started\n const principal = await handler(request, {\n provider,\n clientId,\n userInfo,\n token,\n attributes: cached.attributes ?? {},\n });\n mark('save_principal');\n\n // 6. sign in on a new session: the id the browser brought may have been planted by an\n // attacker who started the flow (session fixation), so it must not become authenticated.\n signedIn = this.signedInSession(session, principal.name);\n await this.repository.save(signedIn);\n await this.repository.deleteById(session.getId());\n mark('update_session');\n } catch (error) {\n // The details stay in the logs: an OAuth2Error message can carry a client id or a\n // provider's response, which has no place in a URL the browser keeps in its history.\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 callback failed', error);\n const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : 'server_error';\n return this.redirect(type);\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error');\n }\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = signedIn.getMaxInactiveInterval();\n setCookie(response, this.cookieName, signedIn.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n /**\n * A fresh session for `name`, carrying over what the host kept in the pre-login one. The\n * authorization request (state, PKCE verifier, flow attributes) is single-use and was consumed\n * by this callback, and a principal left there from an earlier sign-in is replaced.\n */\n private signedInSession(previous: Session, name: string): Session {\n const session = this.repository.createSession();\n for (const attribute of previous.getAttributeNames()) {\n if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;\n if (attribute === PRINCIPAL_NAME_INDEX_NAME) continue;\n const value = previous.getAttribute(attribute);\n if (value !== null) session.setAttribute(attribute, value);\n }\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, name);\n return session;\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 { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const body: unknown = await request.json();\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\n if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });\n // Only the credentials go on to the provider client.\n const { attributes: _, ...credentials } = body as NativeCredential & { attributes?: unknown };\n const { code_verifier } = credentials as { code_verifier?: unknown };\n if (\n code_verifier !== undefined &&\n (typeof code_verifier !== 'string' || !CODE_VERIFIER_FORMAT.test(code_verifier))\n ) {\n return Response.json(INVALID_CODE_VERIFIER, { status: 400 });\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\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 !== provider) {\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, { provider, clientId, userInfo, token, attributes });\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 provider = '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 // Google's own form post carries no attributes; an app posting JSON may add them.\n let body: Record<string, string>;\n let attributes: Record<string, string> | null = {};\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 const json: unknown = await request.json();\n attributes = bodyAttributes(json);\n body = 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 if (!attributes) return Response.json(INVALID_ATTRIBUTES, { status: 400 });\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 // Both the credential and the code are issued to the provider's web client.\n let principal: Principal;\n if (parsed.credential) {\n const { clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n });\n principal = await handler(request, { provider, clientId, userInfo, token, attributes });\n } else if (parsed.code) {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n principal = await handler(request, { provider, clientId, userInfo, token, attributes });\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 // The cooldown is claimed before sending, so of concurrent requests exactly one sends. A failed\n // delivery releases it again: no code was stored, so an immediate retry gains an attacker\n // nothing, while keeping it would lock the user out of signing in for the whole cooldown.\n const cooldownKey = this.getEmailVerificationCooldownKey(email);\n if ((await this.kv.increment(cooldownKey, this.verificationResendCooldown)) > 1) {\n const error = resourceExhausted(\n 'VERIFICATION_CODE_RESEND_TOO_SOON',\n 'Verification code was sent recently'\n );\n return Response.json(error, { status: error.error.code });\n }\n\n try {\n // Sent before it is stored, so a failed delivery leaves the previous code usable.\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n await sender({ email, verificationCode });\n // The new code gets the full attempt budget; the cooldown bounds how often that happens.\n // Reset before storing, so every comparison against the new code is counted.\n await this.kv.removeItem(this.getEmailVerificationAttemptsKey(email));\n await this.kv.setItem(\n this.getEmailVerificationCodeKey(email),\n verificationCode,\n EMAIL_VERIFICATION_CODE_TTL\n );\n return Response.json({});\n } catch (e) {\n console.error('Failed to send email verification code', e);\n await this.kv.removeItem(cooldownKey);\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, attributes } = data;\n\n const key = this.getEmailVerificationCodeKey(email);\n const attemptsKey = this.getEmailVerificationAttemptsKey(email);\n const stored = await this.kv.getItem(key);\n if (typeof stored !== 'string') {\n return Response.json(invalidArgument('INVALID_VERIFICATION_CODE'), { status: 400 });\n }\n\n // Count the attempt before comparing: the atomic increment hands every request, parallel ones\n // included, a distinct count, so at most maxAttempts comparisons are ever made against a code.\n const attempts = await this.kv.increment(attemptsKey, EMAIL_VERIFICATION_CODE_TTL);\n if (attempts > this.maxVerificationAttempts) {\n // Only the code is discarded. The counter stays until it expires (or a new code is sent):\n // removing it would hand requests that already read the code a fresh budget.\n await this.kv.removeItem(key);\n const error = invalidArgument('VERIFICATION_CODE_ATTEMPTS_EXCEEDED');\n return Response.json(error, { status: error.error.code });\n }\n if (\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 await this.kv.removeItem(attemptsKey);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n const principal = await handler(request, {\n provider: 'EMAIL',\n userInfo: { claims, data: {} },\n attributes: attributes ?? {},\n });\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/session/db.cjs
CHANGED
|
@@ -28,6 +28,7 @@ var DBIndexedSessionRepository = class {
|
|
|
28
28
|
return {
|
|
29
29
|
primaryId: session.primaryId,
|
|
30
30
|
sessionId: session.getId(),
|
|
31
|
+
isNew: session.isNew,
|
|
31
32
|
creationTime: session.getCreationTime(),
|
|
32
33
|
lastAccessTime: session.getLastAccessedTime(),
|
|
33
34
|
maxInactiveInterval: session.getMaxInactiveInterval(),
|
package/dist/session/db.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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":";;;;;
|
|
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 /**\n * Set on the entity `save` hands the adapter: `true` for a session created in this request and\n * never stored, `false` for one the adapter returned from `findById` / `findByPrincipalName`. As\n * in Spring Session's `JdbcIndexedSessionRepository`, it is what decides between an INSERT and\n * an UPDATE — an UPDATE that touches no row then means the session was deleted meanwhile\n * (logout, kick) and must not be written back. Absent on entities the adapter returns.\n */\n isNew?: boolean;\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 isNew: session.isNew,\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":";;;;;AAyBA,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,OAAO,QAAQ;GACf,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,cAAA,0FACF,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,6BAA6BG,uBAAAA,2BAA2B,aAAa;CACnF;CAEA,MAAM,uBAAuB,cAAsC;EACjE,OAAO,KAAK,GAAG,uBAAuB,YAAY;CACpD;AACF"}
|
package/dist/session/db.d.cts
CHANGED
|
@@ -4,6 +4,14 @@ import { MapSession } from "./map-session.cjs";
|
|
|
4
4
|
interface SessionEntity {
|
|
5
5
|
primaryId: string;
|
|
6
6
|
sessionId: string;
|
|
7
|
+
/**
|
|
8
|
+
* Set on the entity `save` hands the adapter: `true` for a session created in this request and
|
|
9
|
+
* never stored, `false` for one the adapter returned from `findById` / `findByPrincipalName`. As
|
|
10
|
+
* in Spring Session's `JdbcIndexedSessionRepository`, it is what decides between an INSERT and
|
|
11
|
+
* an UPDATE — an UPDATE that touches no row then means the session was deleted meanwhile
|
|
12
|
+
* (logout, kick) and must not be written back. Absent on entities the adapter returns.
|
|
13
|
+
*/
|
|
14
|
+
isNew?: boolean;
|
|
7
15
|
creationTime: number;
|
|
8
16
|
lastAccessTime: number;
|
|
9
17
|
maxInactiveInterval: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.cts","names":[],"sources":["../../src/session/db.ts"],"mappings":";;;UAKU;EACR;EACA;EACA;EACA;EACA;EACA;EAEA,YAAY;EACZ;;cAGI,kBAAkB;EACf;EACA;EAEP,YAAY,QAAQ,YAAY;;iBAOjB;EACf,KAAK,SAAS,gBAAgB;EAC9B,WAAW,oBAAoB;EAC/B,SAAS,oBAAoB,QAAQ;EACrC,oBAAoB,wBAAwB,QAAQ;EACpD,uBAAuB,wBAAwB;;qBAGpC,sCAAsC,kBAAkB;UAC3D;mBACS;EAEjB,YAAY,IAAI;EAIhB,8BAA8B;UAItB;
|
|
1
|
+
{"version":3,"file":"db.d.cts","names":[],"sources":["../../src/session/db.ts"],"mappings":";;;UAKU;EACR;EACA;;;;;;;;EAQA;EACA;EACA;EACA;EACA;EAEA,YAAY;EACZ;;cAGI,kBAAkB;EACf;EACA;EAEP,YAAY,QAAQ,YAAY;;iBAOjB;EACf,KAAK,SAAS,gBAAgB;EAC9B,WAAW,oBAAoB;EAC/B,SAAS,oBAAoB,QAAQ;EACrC,oBAAoB,wBAAwB,QAAQ;EACpD,uBAAuB,wBAAwB;;qBAGpC,sCAAsC,kBAAkB;UAC3D;mBACS;EAEjB,YAAY,IAAI;EAIhB,8BAA8B;UAItB;UAoBA;UAeM;EAOd,iBAAa;EAMP,KAAK,SAAS,YAAS;EAMvB,SAAS,oBAAoB,QAAQ;EAIrC,WAAW,oBAAiB;EAI5B,6BACJ,mBACA,qBACC,QAAQ,YAAY;EAkBjB,oBAAoB,wBAAwB,QAAQ,YAAY;EAIhE,uBAAuB,wBAAwB"}
|
package/dist/session/db.d.mts
CHANGED
|
@@ -4,6 +4,14 @@ import { MapSession } from "./map-session.mjs";
|
|
|
4
4
|
interface SessionEntity {
|
|
5
5
|
primaryId: string;
|
|
6
6
|
sessionId: string;
|
|
7
|
+
/**
|
|
8
|
+
* Set on the entity `save` hands the adapter: `true` for a session created in this request and
|
|
9
|
+
* never stored, `false` for one the adapter returned from `findById` / `findByPrincipalName`. As
|
|
10
|
+
* in Spring Session's `JdbcIndexedSessionRepository`, it is what decides between an INSERT and
|
|
11
|
+
* an UPDATE — an UPDATE that touches no row then means the session was deleted meanwhile
|
|
12
|
+
* (logout, kick) and must not be written back. Absent on entities the adapter returns.
|
|
13
|
+
*/
|
|
14
|
+
isNew?: boolean;
|
|
7
15
|
creationTime: number;
|
|
8
16
|
lastAccessTime: number;
|
|
9
17
|
maxInactiveInterval: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.mts","names":[],"sources":["../../src/session/db.ts"],"mappings":";;;UAKU;EACR;EACA;EACA;EACA;EACA;EACA;EAEA,YAAY;EACZ;;cAGI,kBAAkB;EACf;EACA;EAEP,YAAY,QAAQ,YAAY;;iBAOjB;EACf,KAAK,SAAS,gBAAgB;EAC9B,WAAW,oBAAoB;EAC/B,SAAS,oBAAoB,QAAQ;EACrC,oBAAoB,wBAAwB,QAAQ;EACpD,uBAAuB,wBAAwB;;qBAGpC,sCAAsC,kBAAkB;UAC3D;mBACS;EAEjB,YAAY,IAAI;EAIhB,8BAA8B;UAItB;
|
|
1
|
+
{"version":3,"file":"db.d.mts","names":[],"sources":["../../src/session/db.ts"],"mappings":";;;UAKU;EACR;EACA;;;;;;;;EAQA;EACA;EACA;EACA;EACA;EAEA,YAAY;EACZ;;cAGI,kBAAkB;EACf;EACA;EAEP,YAAY,QAAQ,YAAY;;iBAOjB;EACf,KAAK,SAAS,gBAAgB;EAC9B,WAAW,oBAAoB;EAC/B,SAAS,oBAAoB,QAAQ;EACrC,oBAAoB,wBAAwB,QAAQ;EACpD,uBAAuB,wBAAwB;;qBAGpC,sCAAsC,kBAAkB;UAC3D;mBACS;EAEjB,YAAY,IAAI;EAIhB,8BAA8B;UAItB;UAoBA;UAeM;EAOd,iBAAa;EAMP,KAAK,SAAS,YAAS;EAMvB,SAAS,oBAAoB,QAAQ;EAIrC,WAAW,oBAAiB;EAI5B,6BACJ,mBACA,qBACC,QAAQ,YAAY;EAkBjB,oBAAoB,wBAAwB,QAAQ,YAAY;EAIhE,uBAAuB,wBAAwB"}
|
package/dist/session/db.mjs
CHANGED
|
@@ -27,6 +27,7 @@ var DBIndexedSessionRepository = class {
|
|
|
27
27
|
return {
|
|
28
28
|
primaryId: session.primaryId,
|
|
29
29
|
sessionId: session.getId(),
|
|
30
|
+
isNew: session.isNew,
|
|
30
31
|
creationTime: session.getCreationTime(),
|
|
31
32
|
lastAccessTime: session.getLastAccessedTime(),
|
|
32
33
|
maxInactiveInterval: session.getMaxInactiveInterval(),
|
package/dist/session/db.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.mjs","names":[],"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":";;;;
|
|
1
|
+
{"version":3,"file":"db.mjs","names":[],"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 /**\n * Set on the entity `save` hands the adapter: `true` for a session created in this request and\n * never stored, `false` for one the adapter returned from `findById` / `findByPrincipalName`. As\n * in Spring Session's `JdbcIndexedSessionRepository`, it is what decides between an INSERT and\n * an UPDATE — an UPDATE that touches no row then means the session was deleted meanwhile\n * (logout, kick) and must not be written back. Absent on entities the adapter returns.\n */\n isNew?: boolean;\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 isNew: session.isNew,\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":";;;;AAyBA,IAAM,YAAN,cAAwB,WAAW;CACjC;CACA;CAEA,YAAY,QAAoB,OAAgB;EAC9C,MAAM,MAAM;EACZ,KAAK,QAAQ;EACb,KAAK,YAAY,WAAW;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,gBAAgB,kBAAkB,OAAO;EAE/C,OAAO;GACL,WAAW,QAAQ;GACnB,WAAW,QAAQ,MAAM;GACzB,OAAO,QAAQ;GACf,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,IAAI,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,IAAI,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,cAAA,0FACF,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,6BAA6B,2BAA2B,aAAa;CACnF;CAEA,MAAM,uBAAuB,cAAsC;EACjE,OAAO,KAAK,GAAG,uBAAuB,YAAY;CACpD;AACF"}
|