@shware/security 9.2.0 → 9.2.2

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.
@@ -12,7 +12,7 @@ import { DesktopCodes } from "./desktop-codes.mjs";
12
12
  import { EmailVerificationCodes } from "./email-codes.mjs";
13
13
  import { createHash, randomBytes, randomUUID } from "node:crypto";
14
14
  import { invariant } from "@shware/utils";
15
- import { extend, literal, object, regex, string } from "zod/mini";
15
+ import { custom, extend, literal, object, optional, regex, string } from "zod/mini";
16
16
  // src/auth/index.ts
17
17
  const PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;
18
18
  const CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;
@@ -20,7 +20,6 @@ const CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;
20
20
  const SESSION_HEADER = "X-Auth-Token";
21
21
  /** Seconds an OAuth2 state or nonce may be used within. */
22
22
  const OAUTH2_PARAMETER_TTL = 600;
23
- const MAX_ERROR_DESCRIPTION_LENGTH = 256;
24
23
  /** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */
25
24
  function safeErrorType(error, fallback) {
26
25
  return isKnownOAuth2ErrorType(error) ? error : fallback;
@@ -57,7 +56,6 @@ const REDIRECT_PARAMS = /* @__PURE__ */ new Set([
57
56
  "status",
58
57
  "provider",
59
58
  "error",
60
- "error_description",
61
59
  "reason"
62
60
  ]);
63
61
  /** The host's `redirectParams` without the names the redirects keep for themselves. */
@@ -77,6 +75,27 @@ function isFormPost(request) {
77
75
  const contentType = request.headers.get("content-type")?.toLowerCase();
78
76
  return request.method === "POST" && !!contentType?.startsWith("application/x-www-form-urlencoded");
79
77
  }
78
+ /**
79
+ * The platform's name object as the app posts it (iOS `PersonNameComponents`): any object, since
80
+ * the provider cleans each part itself and ignores what is not a string.
81
+ */
82
+ const personNameSchema = custom((value) => typeof value === "object" && value !== null && !Array.isArray(value));
83
+ /**
84
+ * A native login body (`NativeCredential`): each protocol parameter a string when present. Which
85
+ * ones a provider needs is its own check (a missing `code` is an `OAuth2Error` `invalid_request`);
86
+ * `code_verifier`'s format has its own reason (`INVALID_CODE_VERIFIER`) and `attributes` their own
87
+ * schema, so neither is checked here.
88
+ */
89
+ const nativeCredentialSchema = object({
90
+ state: optional(string()),
91
+ code: optional(string()),
92
+ client_id: optional(string()),
93
+ redirect_uri: optional(string()),
94
+ code_verifier: optional(string()),
95
+ id_token: optional(string()),
96
+ access_token: optional(string()),
97
+ fullName: optional(personNameSchema)
98
+ });
80
99
  /** Google's own form post carries no attributes; an app posting JSON may add them. */
81
100
  const oneTapBodySchema = extend(googleOneTapSchema, { attributes: attributesSchema });
82
101
  const desktopAuthorizeSchema = object({
@@ -214,16 +233,19 @@ var Auth = class {
214
233
  }
215
234
  /**
216
235
  * The error redirect of the callback flow: `refusal`'s reason and metadata ride along, and the
217
- * flow's `redirectParams` once its authorization request is known.
236
+ * flow's `redirectParams` once its authorization request is known. `detail` (a failed validation,
237
+ * the provider's `error_description`) is logged, not forwarded: the page shows a message per
238
+ * `error` code and never the text, so a URL the browser keeps in its history has no use for it,
239
+ * and a forged callback could set it to anything.
218
240
  */
219
- redirect(error, { description, refusal, params } = {}) {
241
+ redirect(error, { detail, refusal, params } = {}) {
220
242
  invariant(this.oauth2Client, "oauth2Client is not initialized");
243
+ if (detail) console.warn(`OAuth2 callback failed: ${error}`, detail);
221
244
  const uri = withParams(this.oauth2Client.errorUri, {
222
245
  ...params,
223
246
  ...refusal?.metadata
224
247
  });
225
248
  uri.searchParams.set("error", error);
226
- if (description) uri.searchParams.set("error_description", description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));
227
249
  if (refusal) uri.searchParams.set("reason", refusal.reason);
228
250
  return Response.redirect(uri.href, 302);
229
251
  }
@@ -365,40 +387,40 @@ var Auth = class {
365
387
  invariant(this.oauth2Client, "oauth2Client is not initialized");
366
388
  const { mark, setTiming } = timing({ enabled: this.timing });
367
389
  const session = await this.loadSession(request);
368
- if (!session) return this.redirect("invalid_request", { description: "session not found" });
390
+ if (!session) return this.redirect("invalid_request", { detail: "session not found" });
369
391
  mark("load_session");
370
392
  const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
371
- if (!json) return this.redirect("invalid_request", { description: "authorization request not found" });
393
+ if (!json) return this.redirect("invalid_request", { detail: "authorization request not found" });
372
394
  const cached = JSON.parse(json);
373
395
  const params = cached.redirectParams;
374
396
  const { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);
375
397
  const data = isFormPost(request) ? Object.fromEntries(new URLSearchParams(await request.text())) : query(request);
376
398
  const parsed = oauth2RedirectQuerySchema.safeParse(data);
377
399
  if (!parsed.success) return this.redirect("invalid_request", {
378
- description: "invalid redirect query",
400
+ detail: "invalid redirect query",
379
401
  params
380
402
  });
381
403
  if (parsed.data.error) {
382
404
  if (parsed.data.state && parsed.data.state !== cached.state) return this.redirect("invalid_request", {
383
- description: "redirect state mismatch",
405
+ detail: "redirect state mismatch",
384
406
  params
385
407
  });
386
408
  return this.redirect(safeErrorType(parsed.data.error, "server_error"), {
387
- description: parsed.data.error_description,
409
+ detail: parsed.data.error_description,
388
410
  params
389
411
  });
390
412
  }
391
413
  const { code, state, user } = parsed.data;
392
414
  if (!code || !state) return this.redirect("invalid_request", {
393
- description: "invalid redirect query",
415
+ detail: "invalid redirect query",
394
416
  params
395
417
  });
396
418
  if (state !== cached.state) return this.redirect("invalid_request", {
397
- description: "redirect state mismatch",
419
+ detail: "redirect state mismatch",
398
420
  params
399
421
  });
400
422
  if (provider !== cached.provider) return this.redirect("invalid_request", {
401
- description: "redirect provider mismatch",
423
+ detail: "redirect provider mismatch",
402
424
  params
403
425
  });
404
426
  mark("validate_session");
@@ -473,9 +495,9 @@ var Auth = class {
473
495
  const { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);
474
496
  const body = await readJson(request);
475
497
  const attributes = bodyAttributes(body);
476
- const { attributes: _, ...credentials } = body;
498
+ const credentials = parse(nativeCredentialSchema, body);
477
499
  const { code_verifier } = credentials;
478
- if (code_verifier !== void 0 && (typeof code_verifier !== "string" || !CODE_VERIFIER_FORMAT.test(code_verifier))) throw invalidArgument("INVALID_CODE_VERIFIER", "code_verifier must be 43-128 characters from [A-Za-z0-9-._~]");
500
+ if (code_verifier !== void 0 && !CODE_VERIFIER_FORMAT.test(code_verifier)) throw invalidArgument("INVALID_CODE_VERIFIER", "code_verifier must be 43-128 characters from [A-Za-z0-9-._~]");
479
501
  const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({
480
502
  provider,
481
503
  credentials
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomBytes, randomUUID } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport { extend, literal, object, regex, string } from 'zod/mini';\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 { parse, readForm, readJson, verifyTurnstileToken } from '../utils/valid';\nimport {\n MAX_ATTRIBUTES_BYTES,\n attributesSchema,\n attributesSize,\n bodyAttributes,\n} from './attributes';\nimport { DesktopCodes } from './desktop-codes';\nimport { type EmailSender, EmailVerificationCodes } from './email-codes';\nimport { AuthenticationError } from './error';\nimport { errorResponse, invalidArgument, unauthenticated } from './respond';\nimport type {\n AuthConfig,\n AuthService,\n Authorization,\n AuthorizedHandler,\n LoggedHandler,\n LogoutOptions,\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\n/** The header a session id travels in besides the cookie, with `session.header` on. */\nconst SESSION_HEADER = 'X-Auth-Token';\n\n/** Seconds an OAuth2 state or nonce may be used within. */\nconst OAUTH2_PARAMETER_TTL = 10 * 60;\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\n/**\n * The host's values for the flow (`attributes`, `redirectParams`), `undefined` ones dropped; `name`\n * is what the size invariant calls them.\n */\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes'],\n name = '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 ${name} are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\ninterface RedirectDetails {\n description?: string;\n refusal?: AuthenticationError;\n /** The flow's `redirectParams`. */\n params?: Record<string, string>;\n}\n\n/**\n * What the callback's redirects say themselves. A host's `redirectParams` never carry these, even\n * on a redirect that leaves one out (a cancel has no `reason`): the page would read it as the SDK's.\n */\nconst REDIRECT_PARAMS = new Set(['status', 'provider', 'error', 'error_description', 'reason']);\n\n/** The host's `redirectParams` without the names the redirects keep for themselves. */\nfunction redirectParams(params: OAuth2AuthorizationOptions['redirectParams']) {\n if (!params) return undefined;\n const own = Object.entries(params).filter(([key]) => !REDIRECT_PARAMS.has(key));\n return authorizationAttributes(Object.fromEntries(own), 'redirectParams');\n}\n\n/** `base` with `params` added, none replacing one it carries itself. */\nfunction withParams(base: string, params: Record<string, string> = {}) {\n const uri = new URL(base);\n for (const [key, value] of Object.entries(params)) {\n if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);\n }\n return uri;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\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\n/** Google's own form post carries no attributes; an app posting JSON may add them. */\nconst oneTapBodySchema = extend(googleOneTapSchema, { attributes: attributesSchema });\n\nconst desktopAuthorizeSchema = object({\n code_challenge: string().check(regex(PKCE_FORMAT)),\n code_challenge_method: literal('S256'),\n});\n\nconst desktopExchangeSchema = object({\n code: string(),\n code_verifier: string().check(regex(PKCE_FORMAT)),\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 SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\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/**\n * The login routes and session helpers. Every route is one straight line: read and validate the\n * request, check the credential, call the host's `onAuthorized`, sign in. Failures are thrown and\n * answered in one place per kind of route: a JSON route runs under `guard`, which maps a\n * `RequestError`, an `AuthenticationError` or an `OAuth2Error` to its google.rpc body (see\n * `respond.ts`); the redirect callback maps the same errors to `errorUri` parameters (`redirect`).\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 emailCodes: EmailVerificationCodes;\n private readonly desktopCodes: DesktopCodes;\n private readonly oauth2Client: OAuth2Client | null;\n /** Provider keys whose logins may come without a nonce (`oauth2.nonce.optional`). */\n private readonly nonceOptional: ReadonlySet<string>;\n private readonly touchInterval: number;\n private readonly sessionHeader: boolean;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\n oauth2,\n cookie,\n session,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const maxAttempts = verificationCode?.maxAttempts ?? 5;\n const resendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(maxAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(resendCooldown >= 1, 'verificationCode.resendCooldown must be at least 1 second');\n this.emailCodes = new EmailVerificationCodes(kvRepository, { maxAttempts, resendCooldown });\n this.desktopCodes = new DesktopCodes(kvRepository);\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 this.nonceOptional = new Set(oauth2?.nonce?.optional);\n this.touchInterval = (session?.touchInterval ?? 3600) * 1000;\n this.sessionHeader = session?.header ?? false;\n invariant(this.touchInterval >= 0, 'session.touchInterval must not be negative');\n }\n\n // ---- sessions and responses ------------------------------------------------------------------\n\n private getSessionId(request: Request): string | undefined {\n const fromCookie = getCookie(request, this.cookieName);\n if (fromCookie) return fromCookie;\n return (this.sessionHeader && request.headers.get(SESSION_HEADER)) || undefined;\n }\n\n /** The session the request carries, if it exists. */\n private async loadSession(request: Request): Promise<Session | null> {\n const sessionId = this.getSessionId(request);\n return sessionId ? this.repository.findById(sessionId) : null;\n }\n\n /** The signed-in principal of a session, if any. */\n private principalOf(session: Session): Principal | null {\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n return name ? { name: String(name) } : null;\n }\n\n /**\n * A new session signed in as `principal`. With `carryOver`, what the host kept in the pre-login\n * session moves to the new one; the authorization request (state, PKCE verifier, flow\n * attributes) is single-use and was consumed, and a principal left there from an earlier\n * sign-in is replaced. The new id is never the one the browser brought, which may have been\n * planted by an attacker who started the flow (session fixation).\n */\n private async signIn(principal: Principal, carryOver?: Session): Promise<Session> {\n const session = this.repository.createSession();\n for (const attribute of carryOver?.getAttributeNames() ?? []) {\n if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;\n if (attribute === PRINCIPAL_NAME_INDEX_NAME) continue;\n const value = carryOver?.getAttribute(attribute);\n if (value !== null && value !== undefined) session.setAttribute(attribute, value);\n }\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n return session;\n }\n\n /**\n * `response` carrying `session`: its cookie, renewed for the whole inactive interval, and with\n * `session.header` on, the `X-Auth-Token` header a client without a cookie jar reads.\n */\n private withSession(response: Response, session: Session, cookie?: CookieOptions): Response {\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n ...cookie,\n maxAge,\n });\n if (this.sessionHeader) response.headers.set(SESSION_HEADER, session.getId());\n return response;\n }\n\n /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */\n private async guard(run: () => Promise<Response>): Promise<Response> {\n try {\n return await run();\n } catch (error) {\n const response = errorResponse(error);\n if (response) return response;\n throw error;\n }\n }\n\n /**\n * The error redirect of the callback flow: `refusal`'s reason and metadata ride along, and the\n * flow's `redirectParams` once its authorization request is known.\n */\n private redirect(\n error: OAuth2ErrorType,\n { description, refusal, params }: RedirectDetails = {}\n ): Response {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n // A refusal's metadata wins over the host's parameters of the same name: it is about this answer.\n const uri = withParams(this.oauth2Client.errorUri, { ...params, ...refusal?.metadata });\n uri.searchParams.set('error', error);\n if (description) {\n uri.searchParams.set('error_description', description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));\n }\n if (refusal) uri.searchParams.set('reason', refusal.reason);\n return Response.redirect(uri.href, 302);\n }\n\n // ---- basic -----------------------------------------------------------------------------------\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, options?: LogoutOptions): Promise<Response> => {\n const scope = options?.scope ?? 'local';\n const sessionId = this.getSessionId(request);\n // `global` and `others` reach the principal's other sessions through the one the request\n // carries; a request without a signed-in session has no principal to widen to, and ends as\n // `local` would.\n if (scope !== 'local' && sessionId) {\n const session = await this.repository.findById(sessionId);\n const principal = session && this.principalOf(session);\n if (principal) {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const id of sessions.keys()) {\n if (id !== sessionId) await this.repository.deleteById(id);\n }\n }\n }\n const response = Response.json({});\n if (scope === 'others') return response;\n if (sessionId) await this.repository.deleteById(sessionId);\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 const session = await this.loadSession(request);\n if (!session) return Response.json({ data: false });\n\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n const principal = this.principalOf(session);\n if (principal) await onLogged?.(principal);\n return this.withSession(Response.json({ data: principal !== null }), session);\n };\n\n // ---- oauth2 ----------------------------------------------------------------------------------\n\n private createPkceParameters(): PkceParameters {\n // 32 random bytes: a 43-char base64url verifier, the RFC 7636 minimum (a UUID is 36).\n const code_verifier = randomBytes(32).toString('base64url');\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 /**\n * Spends a nonce `oauth2Nonce` issued for `provider`: `NONCE_MISMATCH` for one that was not,\n * or was used already. An id token is replayable until it expires; the nonce it carries makes\n * it sign in once.\n */\n private async consumeNonce(nonce: string, provider: string): Promise<void> {\n const key = this.getOauth2NonceKey(nonce);\n if ((await this.kv.getItem(key)) !== provider) throw new AuthenticationError('NONCE_MISMATCH');\n await this.kv.removeItem(key);\n }\n\n /**\n * The nonce check of the JSON logins (One Tap, native), where the client obtained the id token\n * and the nonce came from `oauth2Nonce`: spends the one the token carries, and refuses a token\n * without one when the provider's flow echoes nonces (`required`) and the host has not listed the\n * provider as optional. An id token is replayable until it expires; the nonce makes it sign in\n * once.\n */\n private async checkIssuedNonce(\n userInfo: Authorization['userInfo'],\n provider: string,\n required: boolean\n ): Promise<boolean> {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce === 'string' && nonce) {\n await this.consumeNonce(nonce, provider);\n return true;\n }\n if (required && !this.nonceOptional.has(provider)) {\n throw new AuthenticationError('NONCE_MISSING');\n }\n return false;\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, OAUTH2_PARAMETER_TTL);\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, OAUTH2_PARAMETER_TTL);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const params = redirectParams(options?.redirectParams);\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 // Sent only to a provider whose id token echoes it: the callback then requires it back, so\n // a code that was swapped in for another sign-in's yields a token the session refuses.\n const nonce = (await this.oauth2Client.nonceSupport(provider)).web ? randomUUID() : undefined;\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, state, pkce, nonce });\n let session = await this.loadSession(request);\n if (!session) {\n // A session made for the flow only has to outlive the state: an abandoned flow must not\n // keep a row for the default interval. Sign-in moves to a new session, with the default.\n session = this.repository.createSession();\n session.setMaxInactiveInterval(OAUTH2_PARAMETER_TTL);\n }\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(nonce ? { nonce } : {}),\n ...(attributes ? { attributes } : {}),\n ...(params ? { redirectParams: params } : {}),\n };\n session.setAttribute(\n this.ATTR_OAUTH2_AUTHORIZATION_REQUEST,\n JSON.stringify(authorizationRequest)\n );\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, session);\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. the session the flow started on, and the authorization request it stored\n const session = await this.loadSession(request);\n if (!session) return this.redirect('invalid_request', { description: 'session not found' });\n mark('load_session');\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', { description: 'authorization request not found' });\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n // From here on the redirect back to the app is this flow's, with the host's parameters.\n const params = cached.redirectParams;\n\n // 2. the provider's answer: query, or a form for Apple's response_mode=form_post\n const { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n const data = isFormPost(request)\n ? Object.fromEntries(new URLSearchParams(await request.text()))\n : query(request);\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', { description: 'invalid redirect query', params });\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', { description: 'redirect state mismatch', params });\n }\n return this.redirect(safeErrorType(parsed.data.error, 'server_error'), {\n description: parsed.data.error_description,\n params,\n });\n }\n const { code, state, user } = parsed.data;\n if (!code || !state) {\n return this.redirect('invalid_request', { description: 'invalid redirect query', params });\n }\n if (state !== cached.state) {\n return this.redirect('invalid_request', { description: 'redirect state mismatch', params });\n }\n if (provider !== cached.provider) {\n return this.redirect('invalid_request', {\n description: 'redirect provider mismatch',\n params,\n });\n }\n mark('validate_session');\n\n // 3. exchange the code, load the user, sign in. Everything here can fail (an expired or reused\n // code, a network error, the host's handler or session store). The callback runs in a popup\n // the user is looking at, so every failure must land on errorUri, never as a JSON error.\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 // The id token must carry the nonce this flow sent (stored with the state, not in KV: it\n // is bound to this session's request). A provider that gave no id token has nothing to\n // carry it, as has a request stored before nonces were sent.\n if (cached.nonce && token.id_token) {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce !== 'string' || !nonce) throw new AuthenticationError('NONCE_MISSING');\n if (nonce !== cached.nonce) throw new AuthenticationError('NONCE_MISMATCH');\n mark('check_nonce');\n }\n\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 signedIn = await this.signIn(principal, session);\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, { params });\n }\n // A refusal is something the user can act on, so its reason and metadata reach the page,\n // as they reach a JSON client through the ErrorInfo.\n if (error instanceof AuthenticationError) {\n console.warn('OAuth2 sign-in refused', error.reason, error.metadata);\n return this.redirect('access_denied', { refusal: error, params });\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error', { params });\n }\n\n const uri = withParams(this.oauth2Client.successUri, params);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, signedIn);\n };\n\n /** The shared tail of the JSON login routes: the host's handler, then a signed-in session. */\n private async authorize(\n request: Request,\n handler: AuthorizedHandler,\n authorization: Authorization,\n setTiming: (response: Response) => void\n ): Promise<Response> {\n const principal = await handler(request, authorization);\n const session = await this.signIn(principal);\n const response = Response.json(principal);\n setTiming(response);\n return this.withSession(response, session);\n }\n\n loginOAuth2Native = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\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 = await readJson(request);\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\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 throw invalidArgument(\n 'INVALID_CODE_VERIFIER',\n 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]'\n );\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\n credentials,\n });\n mark('login_oauth2_native');\n\n // Required when the provider's native id token echoes a nonce and the login produced one\n // (Facebook's access_token login has none to carry it).\n const required = (await this.oauth2Client.nonceSupport(provider)).native && !!token.id_token;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n loginOAuth2Onetap = (\n request: Request,\n handler: AuthorizedHandler,\n provider = 'google'\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // Google's own form post, or an app's JSON.\n const contentType = request.headers.get('content-type') ?? '';\n const body = contentType.includes('application/json')\n ? await readJson(request)\n : await readForm(request);\n const { attributes = {}, ...parsed } = parse(oneTapBodySchema, body);\n\n // Both the credential and the code are issued to the provider's web client.\n let clientId: string;\n let token: Authorization['token'];\n let userInfo: Authorization['userInfo'];\n if (parsed.credential) {\n ({ clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n }));\n mark('verify_credential');\n\n // The credential must carry a single-use nonce from `oauth2Nonce`, which the front end\n // passed to `prompt`. This replaces Google's `g_csrf_token` double-submit cookie, which\n // FedCM mode no longer sets.\n const required = (await this.oauth2Client.nonceSupport(provider)).web;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n } else if (parsed.code) {\n ({ clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n }));\n userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n } else {\n throw invalidArgument('INVALID_ARGUMENT', 'credential or code is required');\n }\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n // ---- email -----------------------------------------------------------------------------------\n\n sendEmailVerificationCode = (request: Request, sender: EmailSender): Promise<Response> =>\n this.guard(async () => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n const { email, turnstileToken } = parse(\n sendEmailVerificationCodeSchema,\n await readJson(request)\n );\n await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n await this.emailCodes.send(email, sender);\n return Response.json({});\n });\n\n verifyEmailCode = (email: string, verificationCode: string): Promise<void> =>\n this.emailCodes.verify(email, verificationCode);\n\n loginEmail = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n const { setTiming } = timing({ enabled: this.timing });\n const {\n email,\n verificationCode,\n attributes = {},\n } = parse(loginEmailSchema, await readJson(request));\n await this.emailCodes.verify(email, verificationCode);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n return this.authorize(\n request,\n handler,\n { provider: 'EMAIL', userInfo: { claims, data: {} }, attributes },\n setTiming\n );\n });\n\n // ---- desktop ---------------------------------------------------------------------------------\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already completed normal\n * sign-in on the web (so a cookie session exists); this issues a 5-minute single-use code that\n * the web page hands off to the desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier` before opening the\n * browser, derives `code_challenge = base64url(sha256(verifier))`, and threads the challenge\n * through the URL → web page → here. The challenge is pinned to the code; `desktopExchange`\n * enforces that the caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet capture on loopback)\n * from exchanging it.\n *\n * Requires an authenticated cookie session: anonymous callers get 401. Throttling / abuse\n * protection on this endpoint is the API layer's job (rate-limit per principal); the SDK does\n * not gate it because the upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const principal = await this.getPrincipal(request);\n if (!principal) throw unauthenticated('Sign in first');\n const { code_challenge } = parse(desktopAuthorizeSchema, await readJson(request));\n const code = await this.desktopCodes.issue(principal, code_challenge);\n return Response.json({ code });\n });\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop client hands back the auth\n * code it received via the loopback callback, and we mint a fresh session for it, independent\n * of the web cookie session that originally authorized the code, so logging out on either side\n * does not cascade to the other.\n *\n * Cookie attributes: `SameSite=None; Secure` so the cookie survives cross-site fetches from the\n * desktop renderer's `file://` / `app://` origin. The configured `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`); the override is targeted at the\n * SameSite/Secure pair only. In a non-HTTPS dev backend this combination is rejected by\n * Chromium, see the desktop sign-in PRD for workarounds.\n *\n * No CSRF check needed: the request carries no cookie, the auth code itself is the credential,\n * and the code's single-use removal blocks replay.\n */\n desktopExchange = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const { code, code_verifier } = parse(desktopExchangeSchema, await readJson(request));\n const principal = await this.desktopCodes.redeem(code, code_verifier);\n const session = await this.signIn(principal);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n return this.withSession(Response.json({ ok: true }), session, {\n sameSite: 'none',\n secure: true,\n });\n });\n\n // ---- session management ----------------------------------------------------------------------\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 /**\n * Sliding expiration: an authenticated request renews the session, as `logged` does, so a user\n * who keeps using the app is never signed out after `maxInactiveInterval` of not calling one\n * particular endpoint. No extra read: the session was just loaded to authenticate, and its\n * `lastAccessedTime` is compared in memory; writes are spaced by `session.touchInterval` (see\n * `AuthConfig`), so a session accessed again within the interval is not rewritten.\n */\n private async touch(session: Session) {\n const now = Date.now();\n if (now - session.getLastAccessedTime() < this.touchInterval) return;\n session.setLastAccessedTime(now);\n await this.repository.save(session);\n }\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const session = await this.loadSession(request);\n const principal = session && this.principalOf(session);\n if (!principal) return null;\n await this.touch(session);\n return principal;\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n return (await this.getPrincipal(request)) !== null;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n type Result = T extends true ? Session : Session | null;\n const session = await this.loadSession(request);\n if (session || !create) return session as Result;\n const created = this.repository.createSession();\n await this.repository.save(created);\n return created;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\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":";;;;;;;;;;;;;;;;AAkDA,MAAM,cAAc;AAIpB,MAAM,uBAAuB;;AAG7B,MAAM,iBAAiB;;AAGvB,MAAM,uBAAuB;AAI7B,MAAM,+BAA+B;;AAGrC,SAAS,cAAc,OAAe,UAA4C;CAChF,OAAO,uBAAuB,KAAK,IAAI,QAAQ;AACjD;;;;;;;AAQA,MAAM,uCAAqD,IAAI,IAAI;CACjE;CACA;CACA;AACF,CAAC;;;;;AAMD,SAAS,wBACP,YACA,OAAO,cAC6B;CACpC,IAAI,CAAC,YAAY,OAAO,KAAA;CACxB,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,GAClD,IAAI,UAAU,KAAA,GAAW,QAAQ,OAAO;CAE1C,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,WAAW,GAAG,OAAO,KAAA;CAE9C,MAAM,OAAO,eAAe,OAAO;CACnC,UACE,QAAQ,sBACR,wBAAwB,KAAK,OAAO,KAAK,kBAAkB,qBAAqB,SAClF;CACA,OAAO;AACT;;;;;AAaA,MAAM,kCAAkB,IAAI,IAAI;CAAC;CAAU;CAAY;CAAS;CAAqB;AAAQ,CAAC;;AAG9F,SAAS,eAAe,QAAsD;CAC5E,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,MAAM,MAAM,OAAO,QAAQ,MAAM,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,gBAAgB,IAAI,GAAG,CAAC;CAC9E,OAAO,wBAAwB,OAAO,YAAY,GAAG,GAAG,gBAAgB;AAC1E;;AAGA,SAAS,WAAW,MAAc,SAAiC,CAAC,GAAG;CACrE,MAAM,MAAM,IAAI,IAAI,IAAI;CACxB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,CAAC,IAAI,aAAa,IAAI,GAAG,GAAG,IAAI,aAAa,IAAI,KAAK,KAAK;CAEjE,OAAO;AACT;;AAGA,SAAS,WAAW,SAAkB;CACpC,MAAM,cAAc,QAAQ,QAAQ,IAAI,cAAc,CAAC,EAAE,YAAY;CACrE,OACE,QAAQ,WAAW,UAAU,CAAC,CAAC,aAAa,WAAW,mCAAmC;AAE9F;;AAGA,MAAM,mBAAmB,OAAO,oBAAoB,EAAE,YAAY,iBAAiB,CAAC;AAEpF,MAAM,yBAAyB,OAAO;CACpC,gBAAgB,OAAO,CAAC,CAAC,MAAM,MAAM,WAAW,CAAC;CACjD,uBAAuB,QAAQ,MAAM;AACvC,CAAC;AAED,MAAM,wBAAwB,OAAO;CACnC,MAAM,OAAO;CACb,eAAe,OAAO,CAAC,CAAC,MAAM,MAAM,WAAW,CAAC;AAClD,CAAC;AAED,MAAa,OAAO;CAClB,MAAM;CACN,QAAQ;CACR,QAAQ;CAER,cAAc;CACd,cAAc;CACd,sBAAsB;CACtB,mBAAmB;CACnB,qBAAqB;CACrB,qBAAqB;CAErB,aAAa;CACb,8BAA8B;CAS9B,mBAAmB;CACnB,kBAAkB;CAElB,0BAA0B;AAC5B;;;;;;;;AASA,IAAa,OAAb,MAAyC;CACvC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;CAEA;CACA;CACA;CACA,oCAAqD;CAErD,YAAY,EACV,mBACA,cACA,oBACA,kBACA,QACA,QACA,SACA,UACa;EACb,KAAK,SAAS,UAAU;EACxB,KAAK,KAAK;EACV,KAAK,aAAa;EAClB,KAAK,qBAAqB;EAC1B,MAAM,cAAc,kBAAkB,eAAe;EACrD,MAAM,iBAAiB,kBAAkB,kBAAkB;EAC3D,UAAU,eAAe,GAAG,iDAAiD;EAC7E,UAAU,kBAAkB,GAAG,2DAA2D;EAC1F,KAAK,aAAa,IAAI,uBAAuB,cAAc;GAAE;GAAa;EAAe,CAAC;EAC1F,KAAK,eAAe,IAAI,aAAa,YAAY;EACjD,MAAM,EAAE,MAAM,GAAG,kBAAkB,UAAU,CAAC;EAC9C,KAAK,aAAa,QAAQ;EAC1B,KAAK,gBAAgB;GACnB,MAAM;GACN,UAAU;GACV,QAAQ;GACR,UAAU;GACV,GAAG;EACL;EACA,KAAK,eAAe,QAAQ,SAAS,IAAI,aAAa,OAAO,MAAM,IAAI;EACvE,KAAK,gBAAgB,IAAI,IAAI,QAAQ,OAAO,QAAQ;EACpD,KAAK,iBAAiB,SAAS,iBAAiB,QAAQ;EACxD,KAAK,gBAAgB,SAAS,UAAU;EACxC,UAAU,KAAK,iBAAiB,GAAG,4CAA4C;CACjF;CAIA,aAAqB,SAAsC;EACzD,MAAM,aAAa,UAAU,SAAS,KAAK,UAAU;EACrD,IAAI,YAAY,OAAO;EACvB,OAAQ,KAAK,iBAAiB,QAAQ,QAAQ,IAAI,cAAc,KAAM,KAAA;CACxE;;CAGA,MAAc,YAAY,SAA2C;EACnE,MAAM,YAAY,KAAK,aAAa,OAAO;EAC3C,OAAO,YAAY,KAAK,WAAW,SAAS,SAAS,IAAI;CAC3D;;CAGA,YAAoB,SAAoC;EACtD,MAAM,OAAO,QAAQ,aAAa,yBAAyB;EAC3D,OAAO,OAAO,EAAE,MAAM,OAAO,IAAI,EAAE,IAAI;CACzC;;;;;;;;CASA,MAAc,OAAO,WAAsB,WAAuC;EAChF,MAAM,UAAU,KAAK,WAAW,cAAc;EAC9C,KAAK,MAAM,aAAa,WAAW,kBAAkB,KAAK,CAAC,GAAG;GAC5D,IAAI,cAAc,KAAK,mCAAmC;GAC1D,IAAI,cAAA,iBAAyC;GAC7C,MAAM,QAAQ,WAAW,aAAa,SAAS;GAC/C,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,QAAQ,aAAa,WAAW,KAAK;EAClF;EACA,QAAQ,aAAa,2BAA2B,UAAU,IAAI;EAC9D,MAAM,KAAK,WAAW,KAAK,OAAO;EAClC,OAAO;CACT;;;;;CAMA,YAAoB,UAAoB,SAAkB,QAAkC;EAC1F,MAAM,SAAS,QAAQ,uBAAuB;EAC9C,UAAU,UAAU,KAAK,YAAY,QAAQ,MAAM,GAAG;GACpD,GAAG,KAAK;GACR,GAAG;GACH;EACF,CAAC;EACD,IAAI,KAAK,eAAe,SAAS,QAAQ,IAAI,gBAAgB,QAAQ,MAAM,CAAC;EAC5E,OAAO;CACT;;CAGA,MAAc,MAAM,KAAiD;EACnE,IAAI;GACF,OAAO,MAAM,IAAI;EACnB,SAAS,OAAO;GACd,MAAM,WAAW,cAAc,KAAK;GACpC,IAAI,UAAU,OAAO;GACrB,MAAM;EACR;CACF;;;;;CAMA,SACE,OACA,EAAE,aAAa,SAAS,WAA4B,CAAC,GAC3C;EACV,UAAU,KAAK,cAAc,iCAAiC;EAE9D,MAAM,MAAM,WAAW,KAAK,aAAa,UAAU;GAAE,GAAG;GAAQ,GAAG,SAAS;EAAS,CAAC;EACtF,IAAI,aAAa,IAAI,SAAS,KAAK;EACnC,IAAI,aACF,IAAI,aAAa,IAAI,qBAAqB,YAAY,MAAM,GAAG,4BAA4B,CAAC;EAE9F,IAAI,SAAS,IAAI,aAAa,IAAI,UAAU,QAAQ,MAAM;EAC1D,OAAO,SAAS,SAAS,IAAI,MAAM,GAAG;CACxC;CAIA,OAAO,OAAO,aAAyC;EACrD,MAAM,QAAQ,WAAW;EACzB,MAAM,WAAW,SAAS,KAAK;GAC7B;GACA,eAAe;GACf,YAAY;EACd,CAAC;EAGD,UAAU,UAAU,cAAc,OAAO;GACvC,MAAM;GACN,QAAQ;GACR,UAAU;GACV,UAAU;GACV,QAAQ,KAAK,cAAc;EAC7B,CAAC;EACD,OAAO;CACT;CAEA,SAAS,OAAO,SAAkB,YAA+C;EAC/E,MAAM,QAAQ,SAAS,SAAS;EAChC,MAAM,YAAY,KAAK,aAAa,OAAO;EAI3C,IAAI,UAAU,WAAW,WAAW;GAClC,MAAM,UAAU,MAAM,KAAK,WAAW,SAAS,SAAS;GACxD,MAAM,YAAY,WAAW,KAAK,YAAY,OAAO;GACrD,IAAI,WAAW;IACb,MAAM,WAAW,MAAM,KAAK,WAAW,oBAAoB,UAAU,IAAI;IACzE,KAAK,MAAM,MAAM,SAAS,KAAK,GAC7B,IAAI,OAAO,WAAW,MAAM,KAAK,WAAW,WAAW,EAAE;GAE7D;EACF;EACA,MAAM,WAAW,SAAS,KAAK,CAAC,CAAC;EACjC,IAAI,UAAU,UAAU,OAAO;EAC/B,IAAI,WAAW,MAAM,KAAK,WAAW,WAAW,SAAS;EAGzD,aAAa,UAAU,KAAK,YAAY,KAAK,aAAa;EAC1D,OAAO;CACT;CAEA,SAAS,OAAO,SAAkB,aAAgD;EAChF,MAAM,UAAU,MAAM,KAAK,YAAY,OAAO;EAC9C,IAAI,CAAC,SAAS,OAAO,SAAS,KAAK,EAAE,MAAM,MAAM,CAAC;EAElD,QAAQ,oBAAoB,KAAK,IAAI,CAAC;EACtC,MAAM,KAAK,WAAW,KAAK,OAAO;EAElC,MAAM,YAAY,KAAK,YAAY,OAAO;EAC1C,IAAI,WAAW,MAAM,WAAW,SAAS;EACzC,OAAO,KAAK,YAAY,SAAS,KAAK,EAAE,MAAM,cAAc,KAAK,CAAC,GAAG,OAAO;CAC9E;CAIA,uBAA+C;EAE7C,MAAM,gBAAgB,YAAY,EAAE,CAAC,CAAC,SAAS,WAAW;EAM1D,OAAO;GAAE;GAAe,gBALD,WAAW,QAAQ,CAAC,CACxC,OAAO,aAAa,CAAC,CACrB,OAAO,CAAC,CACR,SAAS,WAEyB;GAAG,uBAAuB;EAAO;CACxE;CAEA,kBAA0B,OAAe;EACvC,OAAO,gBAAgB;CACzB;CAEA,kBAA0B,OAAe;EACvC,OAAO,gBAAgB;CACzB;;;;;;CAOA,MAAc,aAAa,OAAe,UAAiC;EACzE,MAAM,MAAM,KAAK,kBAAkB,KAAK;EACxC,IAAK,MAAM,KAAK,GAAG,QAAQ,GAAG,MAAO,UAAU,MAAM,IAAI,oBAAoB,gBAAgB;EAC7F,MAAM,KAAK,GAAG,WAAW,GAAG;CAC9B;;;;;;;;CASA,MAAc,iBACZ,UACA,UACA,UACkB;EAClB,MAAM,EAAE,UAAU,SAAS;EAC3B,IAAI,OAAO,UAAU,YAAY,OAAO;GACtC,MAAM,KAAK,aAAa,OAAO,QAAQ;GACvC,OAAO;EACT;EACA,IAAI,YAAY,CAAC,KAAK,cAAc,IAAI,QAAQ,GAC9C,MAAM,IAAI,oBAAoB,eAAe;EAE/C,OAAO;CACT;CAEA,cAAc,OAAO,YAAwC;EAC3D,MAAM,EAAE,aAAa,MAAM,SAAS,KAAK,YAAY;EACrD,MAAM,QAAQ,WAAW;EACzB,MAAM,KAAK,GAAG,QAAQ,KAAK,kBAAkB,KAAK,GAAG,UAAU,oBAAoB;EACnF,OAAO,SAAS,KAAK,EAAE,MAAM,CAAC;CAChC;CAEA,cAAc,OAAO,YAAwC;EAC3D,MAAM,EAAE,aAAa,MAAM,SAAS,KAAK,YAAY;EACrD,MAAM,QAAQ,WAAW;EACzB,MAAM,KAAK,GAAG,QAAQ,KAAK,kBAAkB,KAAK,GAAG,UAAU,oBAAoB;EACnF,OAAO,SAAS,KAAK,EAAE,MAAM,CAAC;CAChC;CAEA,uBACE,SACA,YAEA,KAAK,MAAM,YAAY;EACrB,UAAU,KAAK,cAAc,iCAAiC;EAC9D,MAAM,aAAa,wBAAwB,SAAS,UAAU;EAC9D,MAAM,SAAS,eAAe,SAAS,cAAc;EACrD,MAAM,EAAE,MAAM,cAAc,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC;EAC3D,MAAM,EAAE,aAAa,MAAM,SAAS,KAAK,oBAAoB;EAC7D,MAAM,QAAQ,WAAW;EACzB,MAAM,OAAO,KAAK,qBAAqB;EAGvC,MAAM,SAAS,MAAM,KAAK,aAAa,aAAa,QAAQ,EAAA,CAAG,MAAM,WAAW,IAAI,KAAA;EACpF,MAAM,MAAM,MAAM,KAAK,aAAa,uBAAuB;GAAE;GAAU;GAAO;GAAM;EAAM,CAAC;EAC3F,IAAI,UAAU,MAAM,KAAK,YAAY,OAAO;EAC5C,IAAI,CAAC,SAAS;GAGZ,UAAU,KAAK,WAAW,cAAc;GACxC,QAAQ,uBAAuB,oBAAoB;EACrD;EACA,KAAK,eAAe;EAEpB,MAAM,uBAAmD;GACvD;GACA;GACA,yBAAyB,IAAI;GAC7B,sBAAsB;GACtB,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;GACzB,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;GACnC,GAAI,SAAS,EAAE,gBAAgB,OAAO,IAAI,CAAC;EAC7C;EACA,QAAQ,aACN,KAAK,mCACL,KAAK,UAAU,oBAAoB,CACrC;EACA,MAAM,KAAK,WAAW,KAAK,OAAO;EAClC,KAAK,cAAc;EAEnB,MAAM,WAAW,IAAI,SAAS,MAAM;GAAE,QAAQ;GAAK,SAAS,EAAE,UAAU,IAAI,KAAK;EAAE,CAAC;EACpF,UAAU,QAAQ;EAClB,OAAO,KAAK,YAAY,UAAU,OAAO;CAC3C,CAAC;CAEH,kBAAkB,OAAO,SAAkB,YAAkD;EAC3F,UAAU,KAAK,cAAc,iCAAiC;EAC9D,MAAM,EAAE,MAAM,cAAc,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC;EAG3D,MAAM,UAAU,MAAM,KAAK,YAAY,OAAO;EAC9C,IAAI,CAAC,SAAS,OAAO,KAAK,SAAS,mBAAmB,EAAE,aAAa,oBAAoB,CAAC;EAC1F,KAAK,cAAc;EACnB,MAAM,OAAO,QAAQ,aAAa,KAAK,iCAAiC;EACxE,IAAI,CAAC,MACH,OAAO,KAAK,SAAS,mBAAmB,EAAE,aAAa,kCAAkC,CAAC;EAE5F,MAAM,SAAqC,KAAK,MAAM,IAAI;EAE1D,MAAM,SAAS,OAAO;EAGtB,MAAM,EAAE,aAAa,MAAM,SAAS,KAAK,iBAAiB;EAC1D,MAAM,OAAO,WAAW,OAAO,IAC3B,OAAO,YAAY,IAAI,gBAAgB,MAAM,QAAQ,KAAK,CAAC,CAAC,IAC5D,MAAM,OAAO;EACjB,MAAM,SAAS,0BAA0B,UAAU,IAAI;EACvD,IAAI,CAAC,OAAO,SACV,OAAO,KAAK,SAAS,mBAAmB;GAAE,aAAa;GAA0B;EAAO,CAAC;EAQ3F,IAAI,OAAO,KAAK,OAAO;GACrB,IAAI,OAAO,KAAK,SAAS,OAAO,KAAK,UAAU,OAAO,OACpD,OAAO,KAAK,SAAS,mBAAmB;IAAE,aAAa;IAA2B;GAAO,CAAC;GAE5F,OAAO,KAAK,SAAS,cAAc,OAAO,KAAK,OAAO,cAAc,GAAG;IACrE,aAAa,OAAO,KAAK;IACzB;GACF,CAAC;EACH;EACA,MAAM,EAAE,MAAM,OAAO,SAAS,OAAO;EACrC,IAAI,CAAC,QAAQ,CAAC,OACZ,OAAO,KAAK,SAAS,mBAAmB;GAAE,aAAa;GAA0B;EAAO,CAAC;EAE3F,IAAI,UAAU,OAAO,OACnB,OAAO,KAAK,SAAS,mBAAmB;GAAE,aAAa;GAA2B;EAAO,CAAC;EAE5F,IAAI,aAAa,OAAO,UACtB,OAAO,KAAK,SAAS,mBAAmB;GACtC,aAAa;GACb;EACF,CAAC;EAEH,KAAK,kBAAkB;EAKvB,IAAI;EACJ,IAAI;GACF,MAAM,EAAE,UAAU,UAAU,MAAM,KAAK,aAAa,0BAA0B;IAC5E;IACA;IACA,MAAM,OAAO;GACf,CAAC;GAED,MAAM,WAAW,MAAM,KAAK,aAAa,YAAY;IACnD;IACA;IACA;IACA,UAAU,OAAO,EAAE,KAAK,IAAI,KAAA;GAC9B,CAAC;GACD,KAAK,eAAe;GAIpB,IAAI,OAAO,SAAS,MAAM,UAAU;IAClC,MAAM,EAAE,UAAU,SAAS;IAC3B,IAAI,OAAO,UAAU,YAAY,CAAC,OAAO,MAAM,IAAI,oBAAoB,eAAe;IACtF,IAAI,UAAU,OAAO,OAAO,MAAM,IAAI,oBAAoB,gBAAgB;IAC1E,KAAK,aAAa;GACpB;GAEA,MAAM,YAAY,MAAM,QAAQ,SAAS;IACvC;IACA;IACA;IACA;IACA,YAAY,OAAO,cAAc,CAAC;GACpC,CAAC;GACD,KAAK,gBAAgB;GAErB,WAAW,MAAM,KAAK,OAAO,WAAW,OAAO;GAC/C,MAAM,KAAK,WAAW,WAAW,QAAQ,MAAM,CAAC;GAChD,KAAK,gBAAgB;EACvB,SAAS,OAAO;GAGd,IAAI,iBAAiB,aAAa;IAChC,QAAQ,KAAK,0BAA0B,KAAK;IAC5C,MAAM,OAAO,qBAAqB,IAAI,MAAM,KAAK,IAAI,MAAM,QAAQ;IACnE,OAAO,KAAK,SAAS,MAAM,EAAE,OAAO,CAAC;GACvC;GAGA,IAAI,iBAAiB,qBAAqB;IACxC,QAAQ,KAAK,0BAA0B,MAAM,QAAQ,MAAM,QAAQ;IACnE,OAAO,KAAK,SAAS,iBAAiB;KAAE,SAAS;KAAO;IAAO,CAAC;GAClE;GACA,QAAQ,MAAM,0BAA0B,KAAK;GAC7C,OAAO,KAAK,SAAS,gBAAgB,EAAE,OAAO,CAAC;EACjD;EAEA,MAAM,MAAM,WAAW,KAAK,aAAa,YAAY,MAAM;EAC3D,IAAI,aAAa,IAAI,UAAU,SAAS;EACxC,IAAI,aAAa,IAAI,YAAY,QAAQ;EACzC,MAAM,WAAW,IAAI,SAAS,MAAM;GAAE,QAAQ;GAAK,SAAS,EAAE,UAAU,IAAI,KAAK;EAAE,CAAC;EACpF,UAAU,QAAQ;EAClB,OAAO,KAAK,YAAY,UAAU,QAAQ;CAC5C;;CAGA,MAAc,UACZ,SACA,SACA,eACA,WACmB;EACnB,MAAM,YAAY,MAAM,QAAQ,SAAS,aAAa;EACtD,MAAM,UAAU,MAAM,KAAK,OAAO,SAAS;EAC3C,MAAM,WAAW,SAAS,KAAK,SAAS;EACxC,UAAU,QAAQ;EAClB,OAAO,KAAK,YAAY,UAAU,OAAO;CAC3C;CAEA,qBAAqB,SAAkB,YACrC,KAAK,MAAM,YAAY;EACrB,UAAU,KAAK,cAAc,iCAAiC;EAC9D,MAAM,EAAE,MAAM,cAAc,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC;EAC3D,MAAM,EAAE,aAAa,MAAM,SAAS,KAAK,mBAAmB;EAC5D,MAAM,OAAO,MAAM,SAAS,OAAO;EAEnC,MAAM,aAAa,eAAe,IAAI;EAEtC,MAAM,EAAE,YAAY,GAAG,GAAG,gBAAgB;EAC1C,MAAM,EAAE,kBAAkB;EAC1B,IACE,kBAAkB,KAAA,MACjB,OAAO,kBAAkB,YAAY,CAAC,qBAAqB,KAAK,aAAa,IAE9E,MAAM,gBACJ,yBACA,8DACF;EAEF,MAAM,EAAE,UAAU,UAAU,UAAU,MAAM,KAAK,aAAa,kBAAkB;GAC9E;GACA;EACF,CAAC;EACD,KAAK,qBAAqB;EAI1B,MAAM,YAAY,MAAM,KAAK,aAAa,aAAa,QAAQ,EAAA,CAAG,UAAU,CAAC,CAAC,MAAM;EACpF,IAAI,MAAM,KAAK,iBAAiB,UAAU,UAAU,QAAQ,GAAG,KAAK,aAAa;EAEjF,OAAO,KAAK,UACV,SACA,SACA;GAAE;GAAU;GAAU;GAAU;GAAO;EAAW,GAClD,SACF;CACF,CAAC;CAEH,qBACE,SACA,SACA,WAAW,aAEX,KAAK,MAAM,YAAY;EACrB,UAAU,KAAK,cAAc,iCAAiC;EAC9D,MAAM,EAAE,MAAM,cAAc,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC;EAI3D,MAAM,QADc,QAAQ,QAAQ,IAAI,cAAc,KAAK,GAAA,CAClC,SAAS,kBAAkB,IAChD,MAAM,SAAS,OAAO,IACtB,MAAM,SAAS,OAAO;EAC1B,MAAM,EAAE,aAAa,CAAC,GAAG,GAAG,WAAW,MAAM,kBAAkB,IAAI;EAGnE,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI,OAAO,YAAY;GACrB,CAAC,CAAE,UAAU,OAAO,YAAa,MAAM,KAAK,aAAa,aAAa;IACpE;IACA,YAAY,OAAO;GACrB,CAAC;GACD,KAAK,mBAAmB;GAKxB,MAAM,YAAY,MAAM,KAAK,aAAa,aAAa,QAAQ,EAAA,CAAG;GAClE,IAAI,MAAM,KAAK,iBAAiB,UAAU,UAAU,QAAQ,GAAG,KAAK,aAAa;EACnF,OAAO,IAAI,OAAO,MAAM;GACtB,CAAC,CAAE,UAAU,SAAU,MAAM,KAAK,aAAa,0BAA0B;IACvE;IACA,MAAM,OAAO;GACf,CAAC;GACD,WAAW,MAAM,KAAK,aAAa,YAAY;IAAE;IAAU;IAAU;GAAM,CAAC;EAC9E,OACE,MAAM,gBAAgB,oBAAoB,gCAAgC;EAG5E,OAAO,KAAK,UACV,SACA,SACA;GAAE;GAAU;GAAU;GAAU;GAAO;EAAW,GAClD,SACF;CACF,CAAC;CAIH,6BAA6B,SAAkB,WAC7C,KAAK,MAAM,YAAY;EACrB,UAAU,KAAK,oBAAoB,sCAAsC;EACzE,MAAM,EAAE,OAAO,mBAAmB,MAChC,iCACA,MAAM,SAAS,OAAO,CACxB;EACA,MAAM,qBAAqB,KAAK,oBAAoB,cAAc;EAClE,MAAM,KAAK,WAAW,KAAK,OAAO,MAAM;EACxC,OAAO,SAAS,KAAK,CAAC,CAAC;CACzB,CAAC;CAEH,mBAAmB,OAAe,qBAChC,KAAK,WAAW,OAAO,OAAO,gBAAgB;CAEhD,cAAc,SAAkB,YAC9B,KAAK,MAAM,YAAY;EACrB,MAAM,EAAE,cAAc,OAAO,EAAE,SAAS,KAAK,OAAO,CAAC;EACrD,MAAM,EACJ,OACA,kBACA,aAAa,CAAC,MACZ,MAAM,kBAAkB,MAAM,SAAS,OAAO,CAAC;EACnD,MAAM,KAAK,WAAW,OAAO,OAAO,gBAAgB;EACpD,MAAM,SAAyB;GAAE,KAAK;GAAO;GAAO,gBAAgB;EAAK;EACzE,OAAO,KAAK,UACV,SACA,SACA;GAAE,UAAU;GAAS,UAAU;IAAE;IAAQ,MAAM,CAAC;GAAE;GAAG;EAAW,GAChE,SACF;CACF,CAAC;;;;;;;;;;;;;;;;;CAoBH,oBAAoB,YAClB,KAAK,MAAM,YAAY;EACrB,MAAM,YAAY,MAAM,KAAK,aAAa,OAAO;EACjD,IAAI,CAAC,WAAW,MAAM,gBAAgB,eAAe;EACrD,MAAM,EAAE,mBAAmB,MAAM,wBAAwB,MAAM,SAAS,OAAO,CAAC;EAChF,MAAM,OAAO,MAAM,KAAK,aAAa,MAAM,WAAW,cAAc;EACpE,OAAO,SAAS,KAAK,EAAE,KAAK,CAAC;CAC/B,CAAC;;;;;;;;;;;;;;;;CAiBH,mBAAmB,YACjB,KAAK,MAAM,YAAY;EACrB,MAAM,EAAE,MAAM,kBAAkB,MAAM,uBAAuB,MAAM,SAAS,OAAO,CAAC;EACpF,MAAM,YAAY,MAAM,KAAK,aAAa,OAAO,MAAM,aAAa;EACpE,MAAM,UAAU,MAAM,KAAK,OAAO,SAAS;EAC3C,QAAQ,aAAa,eAAe,SAAS;EAC7C,MAAM,KAAK,WAAW,KAAK,OAAO;EAClC,OAAO,KAAK,YAAY,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC,GAAG,SAAS;GAC5D,UAAU;GACV,QAAQ;EACV,CAAC;CACH,CAAC;CAIH,OAAO,OAAO,cAAwC;EACpD,MAAM,WAAW,MAAM,KAAK,WAAW,oBAAoB,UAAU,IAAI;EACzE,KAAK,MAAM,aAAa,SAAS,KAAK,GACpC,MAAM,KAAK,WAAW,WAAW,SAAS;CAE9C;;;;;;;;CASA,MAAc,MAAM,SAAkB;EACpC,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,MAAM,QAAQ,oBAAoB,IAAI,KAAK,eAAe;EAC9D,QAAQ,oBAAoB,GAAG;EAC/B,MAAM,KAAK,WAAW,KAAK,OAAO;CACpC;CAEA,eAAe,OAAO,YAAgD;EACpE,MAAM,UAAU,MAAM,KAAK,YAAY,OAAO;EAC9C,MAAM,YAAY,WAAW,KAAK,YAAY,OAAO;EACrD,IAAI,CAAC,WAAW,OAAO;EACvB,MAAM,KAAK,MAAM,OAAO;EACxB,OAAO;CACT;CAEA,kBAAkB,OAAO,YAAuC;EAC9D,OAAQ,MAAM,KAAK,aAAa,OAAO,MAAO;CAChD;CAEA,aAAa,OACX,SACA,WACuD;EAEvD,MAAM,UAAU,MAAM,KAAK,YAAY,OAAO;EAC9C,IAAI,WAAW,CAAC,QAAQ,OAAO;EAC/B,MAAM,UAAU,KAAK,WAAW,cAAc;EAC9C,MAAM,KAAK,WAAW,KAAK,OAAO;EAClC,OAAO;CACT;CAEA,gBAAgB,OAAO,cAAqC;EAC1D,MAAM,KAAK,WAAW,WAAW,SAAS;CAC5C;CAEA,eAAe,OAAO,cAA6C;EACjE,MAAM,WAAW,MAAM,KAAK,WAAW,oBAAoB,UAAU,IAAI;EACzE,OAAO,MAAM,KAAK,SAAS,OAAO,CAAC;CACrC;CAEA,yBAAyB,OAAO,iBAA6C;EAC3E,MAAM,KAAK,WAAW,uBAAuB,YAAY;EACzD,OAAO,SAAS,KAAK,CAAC,CAAC;CACzB;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomBytes, randomUUID } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport { custom, extend, literal, object, optional, regex, string } from 'zod/mini';\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 PersonNameComponents,\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 { parse, readForm, readJson, verifyTurnstileToken } from '../utils/valid';\nimport {\n MAX_ATTRIBUTES_BYTES,\n attributesSchema,\n attributesSize,\n bodyAttributes,\n} from './attributes';\nimport { DesktopCodes } from './desktop-codes';\nimport { type EmailSender, EmailVerificationCodes } from './email-codes';\nimport { AuthenticationError } from './error';\nimport { errorResponse, invalidArgument, unauthenticated } from './respond';\nimport type {\n AuthConfig,\n AuthService,\n Authorization,\n AuthorizedHandler,\n LoggedHandler,\n LogoutOptions,\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\n/** The header a session id travels in besides the cookie, with `session.header` on. */\nconst SESSION_HEADER = 'X-Auth-Token';\n\n/** Seconds an OAuth2 state or nonce may be used within. */\nconst OAUTH2_PARAMETER_TTL = 10 * 60;\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\n/**\n * The host's values for the flow (`attributes`, `redirectParams`), `undefined` ones dropped; `name`\n * is what the size invariant calls them.\n */\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes'],\n name = '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 ${name} are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\ninterface RedirectDetails {\n /** What went wrong, for the logs: the page gets the `error` code only. */\n detail?: string;\n refusal?: AuthenticationError;\n /** The flow's `redirectParams`. */\n params?: Record<string, string>;\n}\n\n/**\n * What the callback's redirects say themselves. A host's `redirectParams` never carry these, even\n * on a redirect that leaves one out (a cancel has no `reason`): the page would read it as the SDK's.\n */\nconst REDIRECT_PARAMS = new Set(['status', 'provider', 'error', 'reason']);\n\n/** The host's `redirectParams` without the names the redirects keep for themselves. */\nfunction redirectParams(params: OAuth2AuthorizationOptions['redirectParams']) {\n if (!params) return undefined;\n const own = Object.entries(params).filter(([key]) => !REDIRECT_PARAMS.has(key));\n return authorizationAttributes(Object.fromEntries(own), 'redirectParams');\n}\n\n/** `base` with `params` added, none replacing one it carries itself. */\nfunction withParams(base: string, params: Record<string, string> = {}) {\n const uri = new URL(base);\n for (const [key, value] of Object.entries(params)) {\n if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);\n }\n return uri;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\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\n/**\n * The platform's name object as the app posts it (iOS `PersonNameComponents`): any object, since\n * the provider cleans each part itself and ignores what is not a string.\n */\nconst personNameSchema = custom<PersonNameComponents>(\n (value: unknown) => typeof value === 'object' && value !== null && !Array.isArray(value)\n);\n\n/**\n * A native login body (`NativeCredential`): each protocol parameter a string when present. Which\n * ones a provider needs is its own check (a missing `code` is an `OAuth2Error` `invalid_request`);\n * `code_verifier`'s format has its own reason (`INVALID_CODE_VERIFIER`) and `attributes` their own\n * schema, so neither is checked here.\n */\nconst nativeCredentialSchema = object({\n state: optional(string()),\n code: optional(string()),\n client_id: optional(string()),\n redirect_uri: optional(string()),\n code_verifier: optional(string()),\n id_token: optional(string()),\n access_token: optional(string()),\n fullName: optional(personNameSchema),\n});\n\n/** Google's own form post carries no attributes; an app posting JSON may add them. */\nconst oneTapBodySchema = extend(googleOneTapSchema, { attributes: attributesSchema });\n\nconst desktopAuthorizeSchema = object({\n code_challenge: string().check(regex(PKCE_FORMAT)),\n code_challenge_method: literal('S256'),\n});\n\nconst desktopExchangeSchema = object({\n code: string(),\n code_verifier: string().check(regex(PKCE_FORMAT)),\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 SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\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/**\n * The login routes and session helpers. Every route is one straight line: read and validate the\n * request, check the credential, call the host's `onAuthorized`, sign in. Failures are thrown and\n * answered in one place per kind of route: a JSON route runs under `guard`, which maps a\n * `RequestError`, an `AuthenticationError` or an `OAuth2Error` to its google.rpc body (see\n * `respond.ts`); the redirect callback maps the same errors to `errorUri` parameters (`redirect`).\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 emailCodes: EmailVerificationCodes;\n private readonly desktopCodes: DesktopCodes;\n private readonly oauth2Client: OAuth2Client | null;\n /** Provider keys whose logins may come without a nonce (`oauth2.nonce.optional`). */\n private readonly nonceOptional: ReadonlySet<string>;\n private readonly touchInterval: number;\n private readonly sessionHeader: boolean;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\n oauth2,\n cookie,\n session,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const maxAttempts = verificationCode?.maxAttempts ?? 5;\n const resendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(maxAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(resendCooldown >= 1, 'verificationCode.resendCooldown must be at least 1 second');\n this.emailCodes = new EmailVerificationCodes(kvRepository, { maxAttempts, resendCooldown });\n this.desktopCodes = new DesktopCodes(kvRepository);\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 this.nonceOptional = new Set(oauth2?.nonce?.optional);\n this.touchInterval = (session?.touchInterval ?? 3600) * 1000;\n this.sessionHeader = session?.header ?? false;\n invariant(this.touchInterval >= 0, 'session.touchInterval must not be negative');\n }\n\n // ---- sessions and responses ------------------------------------------------------------------\n\n private getSessionId(request: Request): string | undefined {\n const fromCookie = getCookie(request, this.cookieName);\n if (fromCookie) return fromCookie;\n return (this.sessionHeader && request.headers.get(SESSION_HEADER)) || undefined;\n }\n\n /** The session the request carries, if it exists. */\n private async loadSession(request: Request): Promise<Session | null> {\n const sessionId = this.getSessionId(request);\n return sessionId ? this.repository.findById(sessionId) : null;\n }\n\n /** The signed-in principal of a session, if any. */\n private principalOf(session: Session): Principal | null {\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n return name ? { name: String(name) } : null;\n }\n\n /**\n * A new session signed in as `principal`. With `carryOver`, what the host kept in the pre-login\n * session moves to the new one; the authorization request (state, PKCE verifier, flow\n * attributes) is single-use and was consumed, and a principal left there from an earlier\n * sign-in is replaced. The new id is never the one the browser brought, which may have been\n * planted by an attacker who started the flow (session fixation).\n */\n private async signIn(principal: Principal, carryOver?: Session): Promise<Session> {\n const session = this.repository.createSession();\n for (const attribute of carryOver?.getAttributeNames() ?? []) {\n if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;\n if (attribute === PRINCIPAL_NAME_INDEX_NAME) continue;\n const value = carryOver?.getAttribute(attribute);\n if (value !== null && value !== undefined) session.setAttribute(attribute, value);\n }\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n return session;\n }\n\n /**\n * `response` carrying `session`: its cookie, renewed for the whole inactive interval, and with\n * `session.header` on, the `X-Auth-Token` header a client without a cookie jar reads.\n */\n private withSession(response: Response, session: Session, cookie?: CookieOptions): Response {\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n ...cookie,\n maxAge,\n });\n if (this.sessionHeader) response.headers.set(SESSION_HEADER, session.getId());\n return response;\n }\n\n /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */\n private async guard(run: () => Promise<Response>): Promise<Response> {\n try {\n return await run();\n } catch (error) {\n const response = errorResponse(error);\n if (response) return response;\n throw error;\n }\n }\n\n /**\n * The error redirect of the callback flow: `refusal`'s reason and metadata ride along, and the\n * flow's `redirectParams` once its authorization request is known. `detail` (a failed validation,\n * the provider's `error_description`) is logged, not forwarded: the page shows a message per\n * `error` code and never the text, so a URL the browser keeps in its history has no use for it,\n * and a forged callback could set it to anything.\n */\n private redirect(\n error: OAuth2ErrorType,\n { detail, refusal, params }: RedirectDetails = {}\n ): Response {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n if (detail) console.warn(`OAuth2 callback failed: ${error}`, detail);\n // A refusal's metadata wins over the host's parameters of the same name: it is about this answer.\n const uri = withParams(this.oauth2Client.errorUri, { ...params, ...refusal?.metadata });\n uri.searchParams.set('error', error);\n if (refusal) uri.searchParams.set('reason', refusal.reason);\n return Response.redirect(uri.href, 302);\n }\n\n // ---- basic -----------------------------------------------------------------------------------\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, options?: LogoutOptions): Promise<Response> => {\n const scope = options?.scope ?? 'local';\n const sessionId = this.getSessionId(request);\n // `global` and `others` reach the principal's other sessions through the one the request\n // carries; a request without a signed-in session has no principal to widen to, and ends as\n // `local` would.\n if (scope !== 'local' && sessionId) {\n const session = await this.repository.findById(sessionId);\n const principal = session && this.principalOf(session);\n if (principal) {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const id of sessions.keys()) {\n if (id !== sessionId) await this.repository.deleteById(id);\n }\n }\n }\n const response = Response.json({});\n if (scope === 'others') return response;\n if (sessionId) await this.repository.deleteById(sessionId);\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 const session = await this.loadSession(request);\n if (!session) return Response.json({ data: false });\n\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n const principal = this.principalOf(session);\n if (principal) await onLogged?.(principal);\n return this.withSession(Response.json({ data: principal !== null }), session);\n };\n\n // ---- oauth2 ----------------------------------------------------------------------------------\n\n private createPkceParameters(): PkceParameters {\n // 32 random bytes: a 43-char base64url verifier, the RFC 7636 minimum (a UUID is 36).\n const code_verifier = randomBytes(32).toString('base64url');\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 /**\n * Spends a nonce `oauth2Nonce` issued for `provider`: `NONCE_MISMATCH` for one that was not,\n * or was used already. An id token is replayable until it expires; the nonce it carries makes\n * it sign in once.\n */\n private async consumeNonce(nonce: string, provider: string): Promise<void> {\n const key = this.getOauth2NonceKey(nonce);\n if ((await this.kv.getItem(key)) !== provider) throw new AuthenticationError('NONCE_MISMATCH');\n await this.kv.removeItem(key);\n }\n\n /**\n * The nonce check of the JSON logins (One Tap, native), where the client obtained the id token\n * and the nonce came from `oauth2Nonce`: spends the one the token carries, and refuses a token\n * without one when the provider's flow echoes nonces (`required`) and the host has not listed the\n * provider as optional. An id token is replayable until it expires; the nonce makes it sign in\n * once.\n */\n private async checkIssuedNonce(\n userInfo: Authorization['userInfo'],\n provider: string,\n required: boolean\n ): Promise<boolean> {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce === 'string' && nonce) {\n await this.consumeNonce(nonce, provider);\n return true;\n }\n if (required && !this.nonceOptional.has(provider)) {\n throw new AuthenticationError('NONCE_MISSING');\n }\n return false;\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, OAUTH2_PARAMETER_TTL);\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, OAUTH2_PARAMETER_TTL);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const params = redirectParams(options?.redirectParams);\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 // Sent only to a provider whose id token echoes it: the callback then requires it back, so\n // a code that was swapped in for another sign-in's yields a token the session refuses.\n const nonce = (await this.oauth2Client.nonceSupport(provider)).web ? randomUUID() : undefined;\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, state, pkce, nonce });\n let session = await this.loadSession(request);\n if (!session) {\n // A session made for the flow only has to outlive the state: an abandoned flow must not\n // keep a row for the default interval. Sign-in moves to a new session, with the default.\n session = this.repository.createSession();\n session.setMaxInactiveInterval(OAUTH2_PARAMETER_TTL);\n }\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(nonce ? { nonce } : {}),\n ...(attributes ? { attributes } : {}),\n ...(params ? { redirectParams: params } : {}),\n };\n session.setAttribute(\n this.ATTR_OAUTH2_AUTHORIZATION_REQUEST,\n JSON.stringify(authorizationRequest)\n );\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, session);\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. the session the flow started on, and the authorization request it stored\n const session = await this.loadSession(request);\n if (!session) return this.redirect('invalid_request', { detail: 'session not found' });\n mark('load_session');\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', { detail: 'authorization request not found' });\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n // From here on the redirect back to the app is this flow's, with the host's parameters.\n const params = cached.redirectParams;\n\n // 2. the provider's answer: query, or a form for Apple's response_mode=form_post\n const { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n const data = isFormPost(request)\n ? Object.fromEntries(new URLSearchParams(await request.text()))\n : query(request);\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', { detail: 'invalid redirect query', params });\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', { detail: 'redirect state mismatch', params });\n }\n return this.redirect(safeErrorType(parsed.data.error, 'server_error'), {\n detail: parsed.data.error_description,\n params,\n });\n }\n const { code, state, user } = parsed.data;\n if (!code || !state) {\n return this.redirect('invalid_request', { detail: 'invalid redirect query', params });\n }\n if (state !== cached.state) {\n return this.redirect('invalid_request', { detail: 'redirect state mismatch', params });\n }\n if (provider !== cached.provider) {\n return this.redirect('invalid_request', {\n detail: 'redirect provider mismatch',\n params,\n });\n }\n mark('validate_session');\n\n // 3. exchange the code, load the user, sign in. Everything here can fail (an expired or reused\n // code, a network error, the host's handler or session store). The callback runs in a popup\n // the user is looking at, so every failure must land on errorUri, never as a JSON error.\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 // The id token must carry the nonce this flow sent (stored with the state, not in KV: it\n // is bound to this session's request). A provider that gave no id token has nothing to\n // carry it, as has a request stored before nonces were sent.\n if (cached.nonce && token.id_token) {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce !== 'string' || !nonce) throw new AuthenticationError('NONCE_MISSING');\n if (nonce !== cached.nonce) throw new AuthenticationError('NONCE_MISMATCH');\n mark('check_nonce');\n }\n\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 signedIn = await this.signIn(principal, session);\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, { params });\n }\n // A refusal is something the user can act on, so its reason and metadata reach the page,\n // as they reach a JSON client through the ErrorInfo.\n if (error instanceof AuthenticationError) {\n console.warn('OAuth2 sign-in refused', error.reason, error.metadata);\n return this.redirect('access_denied', { refusal: error, params });\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error', { params });\n }\n\n const uri = withParams(this.oauth2Client.successUri, params);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, signedIn);\n };\n\n /** The shared tail of the JSON login routes: the host's handler, then a signed-in session. */\n private async authorize(\n request: Request,\n handler: AuthorizedHandler,\n authorization: Authorization,\n setTiming: (response: Response) => void\n ): Promise<Response> {\n const principal = await handler(request, authorization);\n const session = await this.signIn(principal);\n const response = Response.json(principal);\n setTiming(response);\n return this.withSession(response, session);\n }\n\n loginOAuth2Native = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\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 = await readJson(request);\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\n // Only the credentials go on to the provider client; anything else in the body is dropped.\n const credentials: NativeCredential = parse(nativeCredentialSchema, body);\n const { code_verifier } = credentials;\n if (code_verifier !== undefined && !CODE_VERIFIER_FORMAT.test(code_verifier)) {\n throw invalidArgument(\n 'INVALID_CODE_VERIFIER',\n 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]'\n );\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\n credentials,\n });\n mark('login_oauth2_native');\n\n // Required when the provider's native id token echoes a nonce and the login produced one\n // (Facebook's access_token login has none to carry it).\n const required = (await this.oauth2Client.nonceSupport(provider)).native && !!token.id_token;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n loginOAuth2Onetap = (\n request: Request,\n handler: AuthorizedHandler,\n provider = 'google'\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // Google's own form post, or an app's JSON.\n const contentType = request.headers.get('content-type') ?? '';\n const body = contentType.includes('application/json')\n ? await readJson(request)\n : await readForm(request);\n const { attributes = {}, ...parsed } = parse(oneTapBodySchema, body);\n\n // Both the credential and the code are issued to the provider's web client.\n let clientId: string;\n let token: Authorization['token'];\n let userInfo: Authorization['userInfo'];\n if (parsed.credential) {\n ({ clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n }));\n mark('verify_credential');\n\n // The credential must carry a single-use nonce from `oauth2Nonce`, which the front end\n // passed to `prompt`. This replaces Google's `g_csrf_token` double-submit cookie, which\n // FedCM mode no longer sets.\n const required = (await this.oauth2Client.nonceSupport(provider)).web;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n } else if (parsed.code) {\n ({ clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n }));\n userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n } else {\n throw invalidArgument('INVALID_ARGUMENT', 'credential or code is required');\n }\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n // ---- email -----------------------------------------------------------------------------------\n\n sendEmailVerificationCode = (request: Request, sender: EmailSender): Promise<Response> =>\n this.guard(async () => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n const { email, turnstileToken } = parse(\n sendEmailVerificationCodeSchema,\n await readJson(request)\n );\n await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n await this.emailCodes.send(email, sender);\n return Response.json({});\n });\n\n verifyEmailCode = (email: string, verificationCode: string): Promise<void> =>\n this.emailCodes.verify(email, verificationCode);\n\n loginEmail = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n const { setTiming } = timing({ enabled: this.timing });\n const {\n email,\n verificationCode,\n attributes = {},\n } = parse(loginEmailSchema, await readJson(request));\n await this.emailCodes.verify(email, verificationCode);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n return this.authorize(\n request,\n handler,\n { provider: 'EMAIL', userInfo: { claims, data: {} }, attributes },\n setTiming\n );\n });\n\n // ---- desktop ---------------------------------------------------------------------------------\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already completed normal\n * sign-in on the web (so a cookie session exists); this issues a 5-minute single-use code that\n * the web page hands off to the desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier` before opening the\n * browser, derives `code_challenge = base64url(sha256(verifier))`, and threads the challenge\n * through the URL → web page → here. The challenge is pinned to the code; `desktopExchange`\n * enforces that the caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet capture on loopback)\n * from exchanging it.\n *\n * Requires an authenticated cookie session: anonymous callers get 401. Throttling / abuse\n * protection on this endpoint is the API layer's job (rate-limit per principal); the SDK does\n * not gate it because the upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const principal = await this.getPrincipal(request);\n if (!principal) throw unauthenticated('Sign in first');\n const { code_challenge } = parse(desktopAuthorizeSchema, await readJson(request));\n const code = await this.desktopCodes.issue(principal, code_challenge);\n return Response.json({ code });\n });\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop client hands back the auth\n * code it received via the loopback callback, and we mint a fresh session for it, independent\n * of the web cookie session that originally authorized the code, so logging out on either side\n * does not cascade to the other.\n *\n * Cookie attributes: `SameSite=None; Secure` so the cookie survives cross-site fetches from the\n * desktop renderer's `file://` / `app://` origin. The configured `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`); the override is targeted at the\n * SameSite/Secure pair only. In a non-HTTPS dev backend this combination is rejected by\n * Chromium, see the desktop sign-in PRD for workarounds.\n *\n * No CSRF check needed: the request carries no cookie, the auth code itself is the credential,\n * and the code's single-use removal blocks replay.\n */\n desktopExchange = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const { code, code_verifier } = parse(desktopExchangeSchema, await readJson(request));\n const principal = await this.desktopCodes.redeem(code, code_verifier);\n const session = await this.signIn(principal);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n return this.withSession(Response.json({ ok: true }), session, {\n sameSite: 'none',\n secure: true,\n });\n });\n\n // ---- session management ----------------------------------------------------------------------\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 /**\n * Sliding expiration: an authenticated request renews the session, as `logged` does, so a user\n * who keeps using the app is never signed out after `maxInactiveInterval` of not calling one\n * particular endpoint. No extra read: the session was just loaded to authenticate, and its\n * `lastAccessedTime` is compared in memory; writes are spaced by `session.touchInterval` (see\n * `AuthConfig`), so a session accessed again within the interval is not rewritten.\n */\n private async touch(session: Session) {\n const now = Date.now();\n if (now - session.getLastAccessedTime() < this.touchInterval) return;\n session.setLastAccessedTime(now);\n await this.repository.save(session);\n }\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const session = await this.loadSession(request);\n const principal = session && this.principalOf(session);\n if (!principal) return null;\n await this.touch(session);\n return principal;\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n return (await this.getPrincipal(request)) !== null;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n type Result = T extends true ? Session : Session | null;\n const session = await this.loadSession(request);\n if (session || !create) return session as Result;\n const created = this.repository.createSession();\n await this.repository.save(created);\n return created;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\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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"}
@@ -30,9 +30,14 @@ var RequestError = class RequestError extends Error {
30
30
  Object.setPrototypeOf(this, RequestError.prototype);
31
31
  }
32
32
  };
33
- function invalidArgument(reason, message, details) {
33
+ /**
34
+ * A malformed request: the `ErrorInfo` with `reason` first, then any further `details` (a
35
+ * `BadRequest` naming the fields a schema refused), so `getErrorInfo` finds the reason whatever
36
+ * else the body carries.
37
+ */
38
+ function invalidArgument(reason, message, details = []) {
34
39
  const body = errorBody(400, "INVALID_ARGUMENT", reason, message);
35
- if (details) body.error.details = details;
40
+ body.error.details.push(...details);
36
41
  return new RequestError(body);
37
42
  }
38
43
  function unauthenticated(message) {
@@ -1 +1 @@
1
- {"version":3,"file":"respond.cjs","names":["AuthenticationError","OAuth2Error"],"sources":["../../src/auth/respond.ts"],"sourcesContent":["import { OAuth2Error, type OAuth2ErrorType } from '../oauth2/error';\nimport { AuthenticationError } from './error';\n\n/**\n * How the JSON routes answer a failure: a google.rpc error body, as `@shware/http` reads it, with\n * one `ErrorInfo` whose `reason` the client branches on. The redirect flow answers differently\n * (`error=…` on `errorUri`, see `Auth.redirect`), since a page, not a client, reads it.\n */\nexport interface ErrorBody {\n error: {\n code: number;\n status: string;\n message: string;\n details: unknown[];\n };\n}\n\nconst ERROR_INFO = 'type.googleapis.com/google.rpc.ErrorInfo';\n\nexport function errorBody(\n code: number,\n status: string,\n reason: string,\n message: string,\n metadata?: Record<string, string>\n): ErrorBody {\n const info = { '@type': ERROR_INFO, reason, ...(metadata ? { metadata } : {}) };\n return { error: { code, status, message, details: [info] } };\n}\n\n/**\n * A request a route cannot act on, answered as it is: malformed, too soon, not signed in. Thrown\n * by the validation helpers so a route reads as one straight line; `errorResponse` turns it into\n * the response. Unlike `AuthenticationError`, which is about who is signing in, this is about\n * the request itself, so it is not the host's to throw.\n */\nexport class RequestError extends Error {\n readonly body: ErrorBody;\n\n constructor(body: ErrorBody) {\n super(body.error.message);\n this.name = 'RequestError';\n this.body = body;\n Object.setPrototypeOf(this, RequestError.prototype);\n }\n}\n\nexport function invalidArgument(reason: string, message: string, details?: unknown[]) {\n const body = errorBody(400, 'INVALID_ARGUMENT', reason, message);\n if (details) body.error.details = details;\n return new RequestError(body);\n}\n\nexport function unauthenticated(message: string) {\n return new RequestError(errorBody(401, 'UNAUTHENTICATED', 'UNAUTHENTICATED', message));\n}\n\nexport function resourceExhausted(reason: string, message: string) {\n return new RequestError(errorBody(429, 'RESOURCE_EXHAUSTED', reason, message));\n}\n\nexport function unavailable(reason: string, message: string) {\n return new RequestError(errorBody(503, 'UNAVAILABLE', reason, message));\n}\n\n/**\n * What an `OAuth2Error` from the provider client means to the caller of a JSON route: the request\n * named something that does not exist (400), the credential it sent was refused (403), or the\n * provider is down (503). Its message stays in the logs: it can name a client id or quote the\n * provider's response.\n */\nconst OAUTH2_STATUS: Record<OAuth2ErrorType, [code: number, status: string]> = {\n invalid_request: [400, 'INVALID_ARGUMENT'],\n invalid_scope: [400, 'INVALID_ARGUMENT'],\n unsupported_grant_type: [400, 'INVALID_ARGUMENT'],\n unsupported_response_type: [400, 'INVALID_ARGUMENT'],\n redirect_uri_mismatch: [400, 'INVALID_ARGUMENT'],\n invalid_client: [403, 'PERMISSION_DENIED'],\n invalid_grant: [403, 'PERMISSION_DENIED'],\n invalid_token: [403, 'PERMISSION_DENIED'],\n unauthorized_client: [403, 'PERMISSION_DENIED'],\n insufficient_scope: [403, 'PERMISSION_DENIED'],\n access_denied: [403, 'PERMISSION_DENIED'],\n server_error: [503, 'UNAVAILABLE'],\n temporarily_unavailable: [503, 'UNAVAILABLE'],\n};\n\n/**\n * The response a JSON route gives for an error it knows, or `null` for one it does not, which is\n * the host's (a 500). Every JSON route runs under this, so the three kinds of failure have one\n * mapping: a `RequestError` as it is, an `AuthenticationError` as `403 PERMISSION_DENIED` with the\n * reason and metadata (the host's refusal or the SDK's own: a wrong code, a replayed nonce), an\n * `OAuth2Error` by what it means (`OAUTH2_<error>` as the reason). Exported for the host's error\n * handler, so a flow of its own that calls `Auth` directly (`verifyEmailCode`) answers the same way:\n * `errorResponse(error) ?? yourHandler(error)`.\n */\nexport function errorResponse(error: unknown): Response | null {\n if (error instanceof RequestError) {\n return Response.json(error.body, { status: error.body.error.code });\n }\n if (error instanceof AuthenticationError) {\n const body = errorBody(403, 'PERMISSION_DENIED', error.reason, error.message, error.metadata);\n return Response.json(body, { status: body.error.code });\n }\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 login failed', error);\n const [code, status] = OAUTH2_STATUS[error.error] ?? [503, 'UNAVAILABLE'];\n const reason = `OAUTH2_${error.error.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;\n const body = errorBody(code, status, reason, `OAuth2 ${error.error}`);\n return Response.json(body, { status: body.error.code });\n }\n return null;\n}\n"],"mappings":";;;;AAiBA,MAAM,aAAa;AAEnB,SAAgB,UACd,MACA,QACA,QACA,SACA,UACW;CAEX,OAAO,EAAE,OAAO;EAAE;EAAM;EAAQ;EAAS,SAAS,CAAC;GADpC,SAAS;GAAY;GAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EACrB,CAAC;CAAE,EAAE;AAC7D;;;;;;;AAQA,IAAa,eAAb,MAAa,qBAAqB,MAAM;CACtC;CAEA,YAAY,MAAiB;EAC3B,MAAM,KAAK,MAAM,OAAO;EACxB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,OAAO,eAAe,MAAM,aAAa,SAAS;CACpD;AACF;AAEA,SAAgB,gBAAgB,QAAgB,SAAiB,SAAqB;CACpF,MAAM,OAAO,UAAU,KAAK,oBAAoB,QAAQ,OAAO;CAC/D,IAAI,SAAS,KAAK,MAAM,UAAU;CAClC,OAAO,IAAI,aAAa,IAAI;AAC9B;AAEA,SAAgB,gBAAgB,SAAiB;CAC/C,OAAO,IAAI,aAAa,UAAU,KAAK,mBAAmB,mBAAmB,OAAO,CAAC;AACvF;AAEA,SAAgB,kBAAkB,QAAgB,SAAiB;CACjE,OAAO,IAAI,aAAa,UAAU,KAAK,sBAAsB,QAAQ,OAAO,CAAC;AAC/E;AAEA,SAAgB,YAAY,QAAgB,SAAiB;CAC3D,OAAO,IAAI,aAAa,UAAU,KAAK,eAAe,QAAQ,OAAO,CAAC;AACxE;;;;;;;AAQA,MAAM,gBAAyE;CAC7E,iBAAiB,CAAC,KAAK,kBAAkB;CACzC,eAAe,CAAC,KAAK,kBAAkB;CACvC,wBAAwB,CAAC,KAAK,kBAAkB;CAChD,2BAA2B,CAAC,KAAK,kBAAkB;CACnD,uBAAuB,CAAC,KAAK,kBAAkB;CAC/C,gBAAgB,CAAC,KAAK,mBAAmB;CACzC,eAAe,CAAC,KAAK,mBAAmB;CACxC,eAAe,CAAC,KAAK,mBAAmB;CACxC,qBAAqB,CAAC,KAAK,mBAAmB;CAC9C,oBAAoB,CAAC,KAAK,mBAAmB;CAC7C,eAAe,CAAC,KAAK,mBAAmB;CACxC,cAAc,CAAC,KAAK,aAAa;CACjC,yBAAyB,CAAC,KAAK,aAAa;AAC9C;;;;;;;;;;AAWA,SAAgB,cAAc,OAAiC;CAC7D,IAAI,iBAAiB,cACnB,OAAO,SAAS,KAAK,MAAM,MAAM,EAAE,QAAQ,MAAM,KAAK,MAAM,KAAK,CAAC;CAEpE,IAAI,iBAAiBA,mBAAAA,qBAAqB;EACxC,MAAM,OAAO,UAAU,KAAK,qBAAqB,MAAM,QAAQ,MAAM,SAAS,MAAM,QAAQ;EAC5F,OAAO,SAAS,KAAK,MAAM,EAAE,QAAQ,KAAK,MAAM,KAAK,CAAC;CACxD;CACA,IAAI,iBAAiBC,qBAAAA,aAAa;EAChC,QAAQ,KAAK,uBAAuB,KAAK;EACzC,MAAM,CAAC,MAAM,UAAU,cAAc,MAAM,UAAU,CAAC,KAAK,aAAa;EAExE,MAAM,OAAO,UAAU,MAAM,QAAQ,UADZ,MAAM,MAAM,YAAY,CAAC,CAAC,QAAQ,cAAc,GAAG,KAC/B,UAAU,MAAM,OAAO;EACpE,OAAO,SAAS,KAAK,MAAM,EAAE,QAAQ,KAAK,MAAM,KAAK,CAAC;CACxD;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"respond.cjs","names":["AuthenticationError","OAuth2Error"],"sources":["../../src/auth/respond.ts"],"sourcesContent":["import { OAuth2Error, type OAuth2ErrorType } from '../oauth2/error';\nimport { AuthenticationError } from './error';\n\n/**\n * How the JSON routes answer a failure: a google.rpc error body, as `@shware/http` reads it, with\n * one `ErrorInfo` whose `reason` the client branches on. The redirect flow answers differently\n * (`error=…` on `errorUri`, see `Auth.redirect`), since a page, not a client, reads it.\n */\nexport interface ErrorBody {\n error: {\n code: number;\n status: string;\n message: string;\n details: unknown[];\n };\n}\n\nconst ERROR_INFO = 'type.googleapis.com/google.rpc.ErrorInfo';\n\nexport function errorBody(\n code: number,\n status: string,\n reason: string,\n message: string,\n metadata?: Record<string, string>\n): ErrorBody {\n const info = { '@type': ERROR_INFO, reason, ...(metadata ? { metadata } : {}) };\n return { error: { code, status, message, details: [info] } };\n}\n\n/**\n * A request a route cannot act on, answered as it is: malformed, too soon, not signed in. Thrown\n * by the validation helpers so a route reads as one straight line; `errorResponse` turns it into\n * the response. Unlike `AuthenticationError`, which is about who is signing in, this is about\n * the request itself, so it is not the host's to throw.\n */\nexport class RequestError extends Error {\n readonly body: ErrorBody;\n\n constructor(body: ErrorBody) {\n super(body.error.message);\n this.name = 'RequestError';\n this.body = body;\n Object.setPrototypeOf(this, RequestError.prototype);\n }\n}\n\n/**\n * A malformed request: the `ErrorInfo` with `reason` first, then any further `details` (a\n * `BadRequest` naming the fields a schema refused), so `getErrorInfo` finds the reason whatever\n * else the body carries.\n */\nexport function invalidArgument(reason: string, message: string, details: unknown[] = []) {\n const body = errorBody(400, 'INVALID_ARGUMENT', reason, message);\n body.error.details.push(...details);\n return new RequestError(body);\n}\n\nexport function unauthenticated(message: string) {\n return new RequestError(errorBody(401, 'UNAUTHENTICATED', 'UNAUTHENTICATED', message));\n}\n\nexport function resourceExhausted(reason: string, message: string) {\n return new RequestError(errorBody(429, 'RESOURCE_EXHAUSTED', reason, message));\n}\n\nexport function unavailable(reason: string, message: string) {\n return new RequestError(errorBody(503, 'UNAVAILABLE', reason, message));\n}\n\n/**\n * What an `OAuth2Error` from the provider client means to the caller of a JSON route: the request\n * named something that does not exist (400), the credential it sent was refused (403), or the\n * provider is down (503). Its message stays in the logs: it can name a client id or quote the\n * provider's response.\n */\nconst OAUTH2_STATUS: Record<OAuth2ErrorType, [code: number, status: string]> = {\n invalid_request: [400, 'INVALID_ARGUMENT'],\n invalid_scope: [400, 'INVALID_ARGUMENT'],\n unsupported_grant_type: [400, 'INVALID_ARGUMENT'],\n unsupported_response_type: [400, 'INVALID_ARGUMENT'],\n redirect_uri_mismatch: [400, 'INVALID_ARGUMENT'],\n invalid_client: [403, 'PERMISSION_DENIED'],\n invalid_grant: [403, 'PERMISSION_DENIED'],\n invalid_token: [403, 'PERMISSION_DENIED'],\n unauthorized_client: [403, 'PERMISSION_DENIED'],\n insufficient_scope: [403, 'PERMISSION_DENIED'],\n access_denied: [403, 'PERMISSION_DENIED'],\n server_error: [503, 'UNAVAILABLE'],\n temporarily_unavailable: [503, 'UNAVAILABLE'],\n};\n\n/**\n * The response a JSON route gives for an error it knows, or `null` for one it does not, which is\n * the host's (a 500). Every JSON route runs under this, so the three kinds of failure have one\n * mapping: a `RequestError` as it is, an `AuthenticationError` as `403 PERMISSION_DENIED` with the\n * reason and metadata (the host's refusal or the SDK's own: a wrong code, a replayed nonce), an\n * `OAuth2Error` by what it means (`OAUTH2_<error>` as the reason). Exported for the host's error\n * handler, so a flow of its own that calls `Auth` directly (`verifyEmailCode`) answers the same way:\n * `errorResponse(error) ?? yourHandler(error)`.\n */\nexport function errorResponse(error: unknown): Response | null {\n if (error instanceof RequestError) {\n return Response.json(error.body, { status: error.body.error.code });\n }\n if (error instanceof AuthenticationError) {\n const body = errorBody(403, 'PERMISSION_DENIED', error.reason, error.message, error.metadata);\n return Response.json(body, { status: body.error.code });\n }\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 login failed', error);\n const [code, status] = OAUTH2_STATUS[error.error] ?? [503, 'UNAVAILABLE'];\n const reason = `OAUTH2_${error.error.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;\n const body = errorBody(code, status, reason, `OAuth2 ${error.error}`);\n return Response.json(body, { status: body.error.code });\n }\n return null;\n}\n"],"mappings":";;;;AAiBA,MAAM,aAAa;AAEnB,SAAgB,UACd,MACA,QACA,QACA,SACA,UACW;CAEX,OAAO,EAAE,OAAO;EAAE;EAAM;EAAQ;EAAS,SAAS,CAAC;GADpC,SAAS;GAAY;GAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EACrB,CAAC;CAAE,EAAE;AAC7D;;;;;;;AAQA,IAAa,eAAb,MAAa,qBAAqB,MAAM;CACtC;CAEA,YAAY,MAAiB;EAC3B,MAAM,KAAK,MAAM,OAAO;EACxB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,OAAO,eAAe,MAAM,aAAa,SAAS;CACpD;AACF;;;;;;AAOA,SAAgB,gBAAgB,QAAgB,SAAiB,UAAqB,CAAC,GAAG;CACxF,MAAM,OAAO,UAAU,KAAK,oBAAoB,QAAQ,OAAO;CAC/D,KAAK,MAAM,QAAQ,KAAK,GAAG,OAAO;CAClC,OAAO,IAAI,aAAa,IAAI;AAC9B;AAEA,SAAgB,gBAAgB,SAAiB;CAC/C,OAAO,IAAI,aAAa,UAAU,KAAK,mBAAmB,mBAAmB,OAAO,CAAC;AACvF;AAEA,SAAgB,kBAAkB,QAAgB,SAAiB;CACjE,OAAO,IAAI,aAAa,UAAU,KAAK,sBAAsB,QAAQ,OAAO,CAAC;AAC/E;AAEA,SAAgB,YAAY,QAAgB,SAAiB;CAC3D,OAAO,IAAI,aAAa,UAAU,KAAK,eAAe,QAAQ,OAAO,CAAC;AACxE;;;;;;;AAQA,MAAM,gBAAyE;CAC7E,iBAAiB,CAAC,KAAK,kBAAkB;CACzC,eAAe,CAAC,KAAK,kBAAkB;CACvC,wBAAwB,CAAC,KAAK,kBAAkB;CAChD,2BAA2B,CAAC,KAAK,kBAAkB;CACnD,uBAAuB,CAAC,KAAK,kBAAkB;CAC/C,gBAAgB,CAAC,KAAK,mBAAmB;CACzC,eAAe,CAAC,KAAK,mBAAmB;CACxC,eAAe,CAAC,KAAK,mBAAmB;CACxC,qBAAqB,CAAC,KAAK,mBAAmB;CAC9C,oBAAoB,CAAC,KAAK,mBAAmB;CAC7C,eAAe,CAAC,KAAK,mBAAmB;CACxC,cAAc,CAAC,KAAK,aAAa;CACjC,yBAAyB,CAAC,KAAK,aAAa;AAC9C;;;;;;;;;;AAWA,SAAgB,cAAc,OAAiC;CAC7D,IAAI,iBAAiB,cACnB,OAAO,SAAS,KAAK,MAAM,MAAM,EAAE,QAAQ,MAAM,KAAK,MAAM,KAAK,CAAC;CAEpE,IAAI,iBAAiBA,mBAAAA,qBAAqB;EACxC,MAAM,OAAO,UAAU,KAAK,qBAAqB,MAAM,QAAQ,MAAM,SAAS,MAAM,QAAQ;EAC5F,OAAO,SAAS,KAAK,MAAM,EAAE,QAAQ,KAAK,MAAM,KAAK,CAAC;CACxD;CACA,IAAI,iBAAiBC,qBAAAA,aAAa;EAChC,QAAQ,KAAK,uBAAuB,KAAK;EACzC,MAAM,CAAC,MAAM,UAAU,cAAc,MAAM,UAAU,CAAC,KAAK,aAAa;EAExE,MAAM,OAAO,UAAU,MAAM,QAAQ,UADZ,MAAM,MAAM,YAAY,CAAC,CAAC,QAAQ,cAAc,GAAG,KAC/B,UAAU,MAAM,OAAO;EACpE,OAAO,SAAS,KAAK,MAAM,EAAE,QAAQ,KAAK,MAAM,KAAK,CAAC;CACxD;CACA,OAAO;AACT"}
@@ -22,6 +22,11 @@ export declare class RequestError extends Error {
22
22
  readonly body: ErrorBody;
23
23
  constructor(body: ErrorBody);
24
24
  }
25
+ /**
26
+ * A malformed request: the `ErrorInfo` with `reason` first, then any further `details` (a
27
+ * `BadRequest` naming the fields a schema refused), so `getErrorInfo` finds the reason whatever
28
+ * else the body carries.
29
+ */
25
30
  export declare function invalidArgument(reason: string, message: string, details?: unknown[]): RequestError;
26
31
  export declare function unauthenticated(message: string): RequestError;
27
32
  export declare function resourceExhausted(reason: string, message: string): RequestError;
@@ -1 +1 @@
1
- {"version":3,"file":"respond.d.cts","names":[],"sources":["../../src/auth/respond.ts"],"mappings":";;;;;iBAQiB;EACf;IACE;IACA;IACA;IACA;;;wBAMY,UACd,cACA,gBACA,gBACA,iBACA,WAAW,yBACV;;;;;;;qBAWU,qBAAqB;WACvB,MAAM;EAEf,YAAY,MAAM;;wBAQJ,gBAAgB,gBAAgB,iBAAiB,sBAAmB;wBAMpE,gBAAgB,kBAAe;wBAI/B,kBAAkB,gBAAgB,kBAAe;wBAIjD,YAAY,gBAAgB,kBAAe;;;;;;;;;;wBAmC3C,cAAc,iBAAiB"}
1
+ {"version":3,"file":"respond.d.cts","names":[],"sources":["../../src/auth/respond.ts"],"mappings":";;;;;iBAQiB;EACf;IACE;IACA;IACA;IACA;;;wBAMY,UACd,cACA,gBACA,gBACA,iBACA,WAAW,yBACV;;;;;;;qBAWU,qBAAqB;WACvB,MAAM;EAEf,YAAY,MAAM;;;;;;;wBAaJ,gBAAgB,gBAAgB,iBAAiB,sBAAuB;wBAMxE,gBAAgB,kBAAe;wBAI/B,kBAAkB,gBAAgB,kBAAe;wBAIjD,YAAY,gBAAgB,kBAAe;;;;;;;;;;wBAmC3C,cAAc,iBAAiB"}
@@ -22,6 +22,11 @@ export declare class RequestError extends Error {
22
22
  readonly body: ErrorBody;
23
23
  constructor(body: ErrorBody);
24
24
  }
25
+ /**
26
+ * A malformed request: the `ErrorInfo` with `reason` first, then any further `details` (a
27
+ * `BadRequest` naming the fields a schema refused), so `getErrorInfo` finds the reason whatever
28
+ * else the body carries.
29
+ */
25
30
  export declare function invalidArgument(reason: string, message: string, details?: unknown[]): RequestError;
26
31
  export declare function unauthenticated(message: string): RequestError;
27
32
  export declare function resourceExhausted(reason: string, message: string): RequestError;
@@ -1 +1 @@
1
- {"version":3,"file":"respond.d.mts","names":[],"sources":["../../src/auth/respond.ts"],"mappings":";;;;;iBAQiB;EACf;IACE;IACA;IACA;IACA;;;wBAMY,UACd,cACA,gBACA,gBACA,iBACA,WAAW,yBACV;;;;;;;qBAWU,qBAAqB;WACvB,MAAM;EAEf,YAAY,MAAM;;wBAQJ,gBAAgB,gBAAgB,iBAAiB,sBAAmB;wBAMpE,gBAAgB,kBAAe;wBAI/B,kBAAkB,gBAAgB,kBAAe;wBAIjD,YAAY,gBAAgB,kBAAe;;;;;;;;;;wBAmC3C,cAAc,iBAAiB"}
1
+ {"version":3,"file":"respond.d.mts","names":[],"sources":["../../src/auth/respond.ts"],"mappings":";;;;;iBAQiB;EACf;IACE;IACA;IACA;IACA;;;wBAMY,UACd,cACA,gBACA,gBACA,iBACA,WAAW,yBACV;;;;;;;qBAWU,qBAAqB;WACvB,MAAM;EAEf,YAAY,MAAM;;;;;;;wBAaJ,gBAAgB,gBAAgB,iBAAiB,sBAAuB;wBAMxE,gBAAgB,kBAAe;wBAI/B,kBAAkB,gBAAgB,kBAAe;wBAIjD,YAAY,gBAAgB,kBAAe;;;;;;;;;;wBAmC3C,cAAc,iBAAiB"}
@@ -29,9 +29,14 @@ var RequestError = class RequestError extends Error {
29
29
  Object.setPrototypeOf(this, RequestError.prototype);
30
30
  }
31
31
  };
32
- function invalidArgument(reason, message, details) {
32
+ /**
33
+ * A malformed request: the `ErrorInfo` with `reason` first, then any further `details` (a
34
+ * `BadRequest` naming the fields a schema refused), so `getErrorInfo` finds the reason whatever
35
+ * else the body carries.
36
+ */
37
+ function invalidArgument(reason, message, details = []) {
33
38
  const body = errorBody(400, "INVALID_ARGUMENT", reason, message);
34
- if (details) body.error.details = details;
39
+ body.error.details.push(...details);
35
40
  return new RequestError(body);
36
41
  }
37
42
  function unauthenticated(message) {
@@ -1 +1 @@
1
- {"version":3,"file":"respond.mjs","names":[],"sources":["../../src/auth/respond.ts"],"sourcesContent":["import { OAuth2Error, type OAuth2ErrorType } from '../oauth2/error';\nimport { AuthenticationError } from './error';\n\n/**\n * How the JSON routes answer a failure: a google.rpc error body, as `@shware/http` reads it, with\n * one `ErrorInfo` whose `reason` the client branches on. The redirect flow answers differently\n * (`error=…` on `errorUri`, see `Auth.redirect`), since a page, not a client, reads it.\n */\nexport interface ErrorBody {\n error: {\n code: number;\n status: string;\n message: string;\n details: unknown[];\n };\n}\n\nconst ERROR_INFO = 'type.googleapis.com/google.rpc.ErrorInfo';\n\nexport function errorBody(\n code: number,\n status: string,\n reason: string,\n message: string,\n metadata?: Record<string, string>\n): ErrorBody {\n const info = { '@type': ERROR_INFO, reason, ...(metadata ? { metadata } : {}) };\n return { error: { code, status, message, details: [info] } };\n}\n\n/**\n * A request a route cannot act on, answered as it is: malformed, too soon, not signed in. Thrown\n * by the validation helpers so a route reads as one straight line; `errorResponse` turns it into\n * the response. Unlike `AuthenticationError`, which is about who is signing in, this is about\n * the request itself, so it is not the host's to throw.\n */\nexport class RequestError extends Error {\n readonly body: ErrorBody;\n\n constructor(body: ErrorBody) {\n super(body.error.message);\n this.name = 'RequestError';\n this.body = body;\n Object.setPrototypeOf(this, RequestError.prototype);\n }\n}\n\nexport function invalidArgument(reason: string, message: string, details?: unknown[]) {\n const body = errorBody(400, 'INVALID_ARGUMENT', reason, message);\n if (details) body.error.details = details;\n return new RequestError(body);\n}\n\nexport function unauthenticated(message: string) {\n return new RequestError(errorBody(401, 'UNAUTHENTICATED', 'UNAUTHENTICATED', message));\n}\n\nexport function resourceExhausted(reason: string, message: string) {\n return new RequestError(errorBody(429, 'RESOURCE_EXHAUSTED', reason, message));\n}\n\nexport function unavailable(reason: string, message: string) {\n return new RequestError(errorBody(503, 'UNAVAILABLE', reason, message));\n}\n\n/**\n * What an `OAuth2Error` from the provider client means to the caller of a JSON route: the request\n * named something that does not exist (400), the credential it sent was refused (403), or the\n * provider is down (503). Its message stays in the logs: it can name a client id or quote the\n * provider's response.\n */\nconst OAUTH2_STATUS: Record<OAuth2ErrorType, [code: number, status: string]> = {\n invalid_request: [400, 'INVALID_ARGUMENT'],\n invalid_scope: [400, 'INVALID_ARGUMENT'],\n unsupported_grant_type: [400, 'INVALID_ARGUMENT'],\n unsupported_response_type: [400, 'INVALID_ARGUMENT'],\n redirect_uri_mismatch: [400, 'INVALID_ARGUMENT'],\n invalid_client: [403, 'PERMISSION_DENIED'],\n invalid_grant: [403, 'PERMISSION_DENIED'],\n invalid_token: [403, 'PERMISSION_DENIED'],\n unauthorized_client: [403, 'PERMISSION_DENIED'],\n insufficient_scope: [403, 'PERMISSION_DENIED'],\n access_denied: [403, 'PERMISSION_DENIED'],\n server_error: [503, 'UNAVAILABLE'],\n temporarily_unavailable: [503, 'UNAVAILABLE'],\n};\n\n/**\n * The response a JSON route gives for an error it knows, or `null` for one it does not, which is\n * the host's (a 500). Every JSON route runs under this, so the three kinds of failure have one\n * mapping: a `RequestError` as it is, an `AuthenticationError` as `403 PERMISSION_DENIED` with the\n * reason and metadata (the host's refusal or the SDK's own: a wrong code, a replayed nonce), an\n * `OAuth2Error` by what it means (`OAUTH2_<error>` as the reason). Exported for the host's error\n * handler, so a flow of its own that calls `Auth` directly (`verifyEmailCode`) answers the same way:\n * `errorResponse(error) ?? yourHandler(error)`.\n */\nexport function errorResponse(error: unknown): Response | null {\n if (error instanceof RequestError) {\n return Response.json(error.body, { status: error.body.error.code });\n }\n if (error instanceof AuthenticationError) {\n const body = errorBody(403, 'PERMISSION_DENIED', error.reason, error.message, error.metadata);\n return Response.json(body, { status: body.error.code });\n }\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 login failed', error);\n const [code, status] = OAUTH2_STATUS[error.error] ?? [503, 'UNAVAILABLE'];\n const reason = `OAUTH2_${error.error.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;\n const body = errorBody(code, status, reason, `OAuth2 ${error.error}`);\n return Response.json(body, { status: body.error.code });\n }\n return null;\n}\n"],"mappings":";;;AAiBA,MAAM,aAAa;AAEnB,SAAgB,UACd,MACA,QACA,QACA,SACA,UACW;CAEX,OAAO,EAAE,OAAO;EAAE;EAAM;EAAQ;EAAS,SAAS,CAAC;GADpC,SAAS;GAAY;GAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EACrB,CAAC;CAAE,EAAE;AAC7D;;;;;;;AAQA,IAAa,eAAb,MAAa,qBAAqB,MAAM;CACtC;CAEA,YAAY,MAAiB;EAC3B,MAAM,KAAK,MAAM,OAAO;EACxB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,OAAO,eAAe,MAAM,aAAa,SAAS;CACpD;AACF;AAEA,SAAgB,gBAAgB,QAAgB,SAAiB,SAAqB;CACpF,MAAM,OAAO,UAAU,KAAK,oBAAoB,QAAQ,OAAO;CAC/D,IAAI,SAAS,KAAK,MAAM,UAAU;CAClC,OAAO,IAAI,aAAa,IAAI;AAC9B;AAEA,SAAgB,gBAAgB,SAAiB;CAC/C,OAAO,IAAI,aAAa,UAAU,KAAK,mBAAmB,mBAAmB,OAAO,CAAC;AACvF;AAEA,SAAgB,kBAAkB,QAAgB,SAAiB;CACjE,OAAO,IAAI,aAAa,UAAU,KAAK,sBAAsB,QAAQ,OAAO,CAAC;AAC/E;AAEA,SAAgB,YAAY,QAAgB,SAAiB;CAC3D,OAAO,IAAI,aAAa,UAAU,KAAK,eAAe,QAAQ,OAAO,CAAC;AACxE;;;;;;;AAQA,MAAM,gBAAyE;CAC7E,iBAAiB,CAAC,KAAK,kBAAkB;CACzC,eAAe,CAAC,KAAK,kBAAkB;CACvC,wBAAwB,CAAC,KAAK,kBAAkB;CAChD,2BAA2B,CAAC,KAAK,kBAAkB;CACnD,uBAAuB,CAAC,KAAK,kBAAkB;CAC/C,gBAAgB,CAAC,KAAK,mBAAmB;CACzC,eAAe,CAAC,KAAK,mBAAmB;CACxC,eAAe,CAAC,KAAK,mBAAmB;CACxC,qBAAqB,CAAC,KAAK,mBAAmB;CAC9C,oBAAoB,CAAC,KAAK,mBAAmB;CAC7C,eAAe,CAAC,KAAK,mBAAmB;CACxC,cAAc,CAAC,KAAK,aAAa;CACjC,yBAAyB,CAAC,KAAK,aAAa;AAC9C;;;;;;;;;;AAWA,SAAgB,cAAc,OAAiC;CAC7D,IAAI,iBAAiB,cACnB,OAAO,SAAS,KAAK,MAAM,MAAM,EAAE,QAAQ,MAAM,KAAK,MAAM,KAAK,CAAC;CAEpE,IAAI,iBAAiB,qBAAqB;EACxC,MAAM,OAAO,UAAU,KAAK,qBAAqB,MAAM,QAAQ,MAAM,SAAS,MAAM,QAAQ;EAC5F,OAAO,SAAS,KAAK,MAAM,EAAE,QAAQ,KAAK,MAAM,KAAK,CAAC;CACxD;CACA,IAAI,iBAAiB,aAAa;EAChC,QAAQ,KAAK,uBAAuB,KAAK;EACzC,MAAM,CAAC,MAAM,UAAU,cAAc,MAAM,UAAU,CAAC,KAAK,aAAa;EAExE,MAAM,OAAO,UAAU,MAAM,QAAQ,UADZ,MAAM,MAAM,YAAY,CAAC,CAAC,QAAQ,cAAc,GAAG,KAC/B,UAAU,MAAM,OAAO;EACpE,OAAO,SAAS,KAAK,MAAM,EAAE,QAAQ,KAAK,MAAM,KAAK,CAAC;CACxD;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"respond.mjs","names":[],"sources":["../../src/auth/respond.ts"],"sourcesContent":["import { OAuth2Error, type OAuth2ErrorType } from '../oauth2/error';\nimport { AuthenticationError } from './error';\n\n/**\n * How the JSON routes answer a failure: a google.rpc error body, as `@shware/http` reads it, with\n * one `ErrorInfo` whose `reason` the client branches on. The redirect flow answers differently\n * (`error=…` on `errorUri`, see `Auth.redirect`), since a page, not a client, reads it.\n */\nexport interface ErrorBody {\n error: {\n code: number;\n status: string;\n message: string;\n details: unknown[];\n };\n}\n\nconst ERROR_INFO = 'type.googleapis.com/google.rpc.ErrorInfo';\n\nexport function errorBody(\n code: number,\n status: string,\n reason: string,\n message: string,\n metadata?: Record<string, string>\n): ErrorBody {\n const info = { '@type': ERROR_INFO, reason, ...(metadata ? { metadata } : {}) };\n return { error: { code, status, message, details: [info] } };\n}\n\n/**\n * A request a route cannot act on, answered as it is: malformed, too soon, not signed in. Thrown\n * by the validation helpers so a route reads as one straight line; `errorResponse` turns it into\n * the response. Unlike `AuthenticationError`, which is about who is signing in, this is about\n * the request itself, so it is not the host's to throw.\n */\nexport class RequestError extends Error {\n readonly body: ErrorBody;\n\n constructor(body: ErrorBody) {\n super(body.error.message);\n this.name = 'RequestError';\n this.body = body;\n Object.setPrototypeOf(this, RequestError.prototype);\n }\n}\n\n/**\n * A malformed request: the `ErrorInfo` with `reason` first, then any further `details` (a\n * `BadRequest` naming the fields a schema refused), so `getErrorInfo` finds the reason whatever\n * else the body carries.\n */\nexport function invalidArgument(reason: string, message: string, details: unknown[] = []) {\n const body = errorBody(400, 'INVALID_ARGUMENT', reason, message);\n body.error.details.push(...details);\n return new RequestError(body);\n}\n\nexport function unauthenticated(message: string) {\n return new RequestError(errorBody(401, 'UNAUTHENTICATED', 'UNAUTHENTICATED', message));\n}\n\nexport function resourceExhausted(reason: string, message: string) {\n return new RequestError(errorBody(429, 'RESOURCE_EXHAUSTED', reason, message));\n}\n\nexport function unavailable(reason: string, message: string) {\n return new RequestError(errorBody(503, 'UNAVAILABLE', reason, message));\n}\n\n/**\n * What an `OAuth2Error` from the provider client means to the caller of a JSON route: the request\n * named something that does not exist (400), the credential it sent was refused (403), or the\n * provider is down (503). Its message stays in the logs: it can name a client id or quote the\n * provider's response.\n */\nconst OAUTH2_STATUS: Record<OAuth2ErrorType, [code: number, status: string]> = {\n invalid_request: [400, 'INVALID_ARGUMENT'],\n invalid_scope: [400, 'INVALID_ARGUMENT'],\n unsupported_grant_type: [400, 'INVALID_ARGUMENT'],\n unsupported_response_type: [400, 'INVALID_ARGUMENT'],\n redirect_uri_mismatch: [400, 'INVALID_ARGUMENT'],\n invalid_client: [403, 'PERMISSION_DENIED'],\n invalid_grant: [403, 'PERMISSION_DENIED'],\n invalid_token: [403, 'PERMISSION_DENIED'],\n unauthorized_client: [403, 'PERMISSION_DENIED'],\n insufficient_scope: [403, 'PERMISSION_DENIED'],\n access_denied: [403, 'PERMISSION_DENIED'],\n server_error: [503, 'UNAVAILABLE'],\n temporarily_unavailable: [503, 'UNAVAILABLE'],\n};\n\n/**\n * The response a JSON route gives for an error it knows, or `null` for one it does not, which is\n * the host's (a 500). Every JSON route runs under this, so the three kinds of failure have one\n * mapping: a `RequestError` as it is, an `AuthenticationError` as `403 PERMISSION_DENIED` with the\n * reason and metadata (the host's refusal or the SDK's own: a wrong code, a replayed nonce), an\n * `OAuth2Error` by what it means (`OAUTH2_<error>` as the reason). Exported for the host's error\n * handler, so a flow of its own that calls `Auth` directly (`verifyEmailCode`) answers the same way:\n * `errorResponse(error) ?? yourHandler(error)`.\n */\nexport function errorResponse(error: unknown): Response | null {\n if (error instanceof RequestError) {\n return Response.json(error.body, { status: error.body.error.code });\n }\n if (error instanceof AuthenticationError) {\n const body = errorBody(403, 'PERMISSION_DENIED', error.reason, error.message, error.metadata);\n return Response.json(body, { status: body.error.code });\n }\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 login failed', error);\n const [code, status] = OAUTH2_STATUS[error.error] ?? [503, 'UNAVAILABLE'];\n const reason = `OAUTH2_${error.error.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;\n const body = errorBody(code, status, reason, `OAuth2 ${error.error}`);\n return Response.json(body, { status: body.error.code });\n }\n return null;\n}\n"],"mappings":";;;AAiBA,MAAM,aAAa;AAEnB,SAAgB,UACd,MACA,QACA,QACA,SACA,UACW;CAEX,OAAO,EAAE,OAAO;EAAE;EAAM;EAAQ;EAAS,SAAS,CAAC;GADpC,SAAS;GAAY;GAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EACrB,CAAC;CAAE,EAAE;AAC7D;;;;;;;AAQA,IAAa,eAAb,MAAa,qBAAqB,MAAM;CACtC;CAEA,YAAY,MAAiB;EAC3B,MAAM,KAAK,MAAM,OAAO;EACxB,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,OAAO,eAAe,MAAM,aAAa,SAAS;CACpD;AACF;;;;;;AAOA,SAAgB,gBAAgB,QAAgB,SAAiB,UAAqB,CAAC,GAAG;CACxF,MAAM,OAAO,UAAU,KAAK,oBAAoB,QAAQ,OAAO;CAC/D,KAAK,MAAM,QAAQ,KAAK,GAAG,OAAO;CAClC,OAAO,IAAI,aAAa,IAAI;AAC9B;AAEA,SAAgB,gBAAgB,SAAiB;CAC/C,OAAO,IAAI,aAAa,UAAU,KAAK,mBAAmB,mBAAmB,OAAO,CAAC;AACvF;AAEA,SAAgB,kBAAkB,QAAgB,SAAiB;CACjE,OAAO,IAAI,aAAa,UAAU,KAAK,sBAAsB,QAAQ,OAAO,CAAC;AAC/E;AAEA,SAAgB,YAAY,QAAgB,SAAiB;CAC3D,OAAO,IAAI,aAAa,UAAU,KAAK,eAAe,QAAQ,OAAO,CAAC;AACxE;;;;;;;AAQA,MAAM,gBAAyE;CAC7E,iBAAiB,CAAC,KAAK,kBAAkB;CACzC,eAAe,CAAC,KAAK,kBAAkB;CACvC,wBAAwB,CAAC,KAAK,kBAAkB;CAChD,2BAA2B,CAAC,KAAK,kBAAkB;CACnD,uBAAuB,CAAC,KAAK,kBAAkB;CAC/C,gBAAgB,CAAC,KAAK,mBAAmB;CACzC,eAAe,CAAC,KAAK,mBAAmB;CACxC,eAAe,CAAC,KAAK,mBAAmB;CACxC,qBAAqB,CAAC,KAAK,mBAAmB;CAC9C,oBAAoB,CAAC,KAAK,mBAAmB;CAC7C,eAAe,CAAC,KAAK,mBAAmB;CACxC,cAAc,CAAC,KAAK,aAAa;CACjC,yBAAyB,CAAC,KAAK,aAAa;AAC9C;;;;;;;;;;AAWA,SAAgB,cAAc,OAAiC;CAC7D,IAAI,iBAAiB,cACnB,OAAO,SAAS,KAAK,MAAM,MAAM,EAAE,QAAQ,MAAM,KAAK,MAAM,KAAK,CAAC;CAEpE,IAAI,iBAAiB,qBAAqB;EACxC,MAAM,OAAO,UAAU,KAAK,qBAAqB,MAAM,QAAQ,MAAM,SAAS,MAAM,QAAQ;EAC5F,OAAO,SAAS,KAAK,MAAM,EAAE,QAAQ,KAAK,MAAM,KAAK,CAAC;CACxD;CACA,IAAI,iBAAiB,aAAa;EAChC,QAAQ,KAAK,uBAAuB,KAAK;EACzC,MAAM,CAAC,MAAM,UAAU,cAAc,MAAM,UAAU,CAAC,KAAK,aAAa;EAExE,MAAM,OAAO,UAAU,MAAM,QAAQ,UADZ,MAAM,MAAM,YAAY,CAAC,CAAC,QAAQ,cAAc,GAAG,KAC/B,UAAU,MAAM,OAAO;EACpE,OAAO,SAAS,KAAK,MAAM,EAAE,QAAQ,KAAK,MAAM,KAAK,CAAC;CACxD;CACA,OAAO;AACT"}
@@ -65,7 +65,7 @@ function createAppleProvider() {
65
65
  };
66
66
  },
67
67
  async loginOAuth2Native({ pkce: _, credentials, ...params }) {
68
- (0, _shware_utils.invariant)(credentials.code, "code is required");
68
+ if (!credentials.code) throw new require_oauth2_error.OAuth2Error(400, "invalid_request", "code is required");
69
69
  const { tokenUri } = this;
70
70
  const { code } = credentials;
71
71
  const response = await require_oauth2_provider_common.exchangeAuthorizationCode({
@@ -1 +1 @@
1
- {"version":3,"file":"apple.cjs","names":["createAuthorizationUri","exchangeAuthorizationCode","OAuth2Error","verifyIdToken","parseAppleUser","nameClaims","cleanPersonName"],"sources":["../../../src/oauth2/provider/apple.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { OAuth2Error } from '../error';\nimport { type AppleUser, cleanPersonName, nameClaims, parseAppleUser } from '../name';\nimport type {\n LoginOAuth2NativeParams,\n NativeCredential,\n OAuth2Token,\n PersonNameComponents,\n Provider,\n UserInfo,\n} from '../types';\nimport { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from './common';\n\n/** `iss` of every Apple id token (https://appleid.apple.com/.well-known/openid-configuration). */\nconst APPLE_ISSUER = 'https://appleid.apple.com';\n\n// ref: https://account.apple.com/.well-known/openid-configuration\nexport function createAppleProvider(): Provider {\n return {\n // important notice: response_mode=form_post is required for apple\n authorizationUri: 'https://appleid.apple.com/auth/authorize?response_mode=form_post',\n tokenUri: 'https://appleid.apple.com/auth/token',\n jwkSetUri: 'https://appleid.apple.com/auth/keys',\n userNameAttribute: 'sub',\n defaultScope: ['openid', 'name', 'email'],\n // The web flow's id token echoes the request's nonce; the app's carries the one bound into\n // ASAuthorizationAppleIDRequest, hashed or not (`nonce_supported`).\n nonce: { web: true, native: true },\n createAuthorizationUri({ pkce: _, ...params }) {\n return createAuthorizationUri({\n ...params,\n scope: params.scope ?? this.defaultScope,\n authorizationUri: this.authorizationUri,\n });\n },\n async exchangeAuthorizationCode({ pkce: _, ...params }) {\n const response = await exchangeAuthorizationCode({ ...params, tokenUri: this.tokenUri });\n if (!response.ok) {\n const { error } = (await response.json()) as AppleErrorResponse;\n throw new OAuth2Error(response.status, error);\n }\n return (await response.json()) as AppleToken;\n },\n async getUserInfo({ id_token }, { clientId, response }) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n // The audience is the client the code was exchanged as: the bundle id for an app, the\n // Services ID for the web.\n const token = await verifyIdToken<AppleIdToken>(id_token, {\n jwkSetUri: this.jwkSetUri,\n issuer: APPLE_ISSUER,\n audience: clientId,\n });\n // Apple's id token never carries a name; on the first authorization of the web flow the\n // name comes in the form_post `user` instead, unsigned, so only its name parts are used\n // and the signed id token's email always wins over the one beside them.\n const user = parseAppleUser(response?.user);\n const data: AppleUserInfo = user ? { ...token, user } : token;\n return {\n data,\n claims: {\n sub: token.sub,\n email: token.email,\n email_verified: true,\n ...nameClaims({\n given_name: user?.name?.firstName,\n family_name: user?.name?.lastName,\n }),\n },\n };\n },\n async loginOAuth2Native({ pkce: _, credentials, ...params }: LoginOAuth2NativeParams) {\n invariant(credentials.code, 'code is required');\n const { tokenUri } = this;\n const { code } = credentials;\n const response = await exchangeAuthorizationCode({ code, tokenUri, ...params });\n if (!response.ok) {\n const { error } = (await response.json()) as AppleErrorResponse;\n throw new OAuth2Error(response.status, error);\n }\n const token = (await response.json()) as AppleToken;\n const { data, claims } = await this.getUserInfo(token, { clientId: params.clientId });\n // The app gets the name from Apple on the first authorization only and passes it along in\n // the login body as the platform's `fullName`.\n const fullName = cleanPersonName(credentials.fullName);\n const name = nameClaims({\n given_name: fullName?.givenName ?? undefined,\n family_name: fullName?.familyName ?? undefined,\n middle_name: fullName?.middleName ?? undefined,\n nickname: fullName?.nickname ?? undefined,\n });\n const userInfo: UserInfo<AppleUserInfo> = {\n data: fullName ? { ...(data as AppleUserInfo), fullName } : (data as AppleUserInfo),\n claims: { ...claims, ...name },\n };\n return { token, userInfo };\n },\n };\n}\n\nexport const apple = createAppleProvider();\n\n// Claims of Apple's id token. It carries no name or picture, not even on the first authorization.\n// https://developer.apple.com/documentation/signinwithapple/authenticating-users-with-sign-in-with-apple\nexport interface AppleIdToken {\n iss: string;\n aud: string;\n exp: number;\n iat: number;\n sub: string;\n email: string;\n email_verified: true | 'true';\n is_private_email: boolean;\n /** Native flow, first authorization only: 0 unsupported, 1 unknown, 2 likely real. */\n real_user_status?: number;\n nonce?: string;\n nonce_supported?: boolean;\n}\n\n/**\n * `userInfo.data` of an Apple login: the id token's claims, plus the unsigned name the client\n * passed on the first authorization, cleaned (see `nameClaims` for the standard claims made of it).\n */\nexport interface AppleUserInfo extends AppleIdToken {\n /** Web flow: Apple's form_post `user`, as Apple names it. */\n user?: AppleUser;\n /** Native flow: the login body's `fullName`, which alone keeps `namePrefix` / `nameSuffix`. */\n fullName?: PersonNameComponents;\n}\n\n// https://developer.apple.com/documentation/devicemanagement/implementing-the-oauth2-authentication-user-enrollment-flow\n// https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens\nexport interface AppleToken extends OAuth2Token {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token: string;\n}\n\nexport interface AppleNativeCredential extends NativeCredential {\n state: string;\n code: string;\n id_token: string;\n}\n\n// https://developer.apple.com/documentation/sign_in_with_apple/errorresponse\nexport interface AppleErrorResponse {\n error:\n | 'invalid_request'\n | 'invalid_client'\n | 'invalid_grant'\n | 'unauthorized_client'\n | 'unsupported_grant_type'\n | 'invalid_scope';\n}\n"],"mappings":";;;;;;;AAcA,MAAM,eAAe;AAGrB,SAAgB,sBAAgC;CAC9C,OAAO;EAEL,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,mBAAmB;EACnB,cAAc;GAAC;GAAU;GAAQ;EAAO;EAGxC,OAAO;GAAE,KAAK;GAAM,QAAQ;EAAK;EACjC,uBAAuB,EAAE,MAAM,GAAG,GAAG,UAAU;GAC7C,OAAOA,+BAAAA,uBAAuB;IAC5B,GAAG;IACH,OAAO,OAAO,SAAS,KAAK;IAC5B,kBAAkB,KAAK;GACzB,CAAC;EACH;EACA,MAAM,0BAA0B,EAAE,MAAM,GAAG,GAAG,UAAU;GACtD,MAAM,WAAW,MAAMC,+BAAAA,0BAA0B;IAAE,GAAG;IAAQ,UAAU,KAAK;GAAS,CAAC;GACvF,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,UAAW,MAAM,SAAS,KAAK;IACvC,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,KAAK;GAC9C;GACA,OAAQ,MAAM,SAAS,KAAK;EAC9B;EACA,MAAM,YAAY,EAAE,YAAY,EAAE,UAAU,YAAY;GACtD,CAAA,GAAA,cAAA,UAAA,CAAU,UAAU,sBAAsB;GAC1C,CAAA,GAAA,cAAA,UAAA,CAAU,KAAK,WAAW,uBAAuB;GAGjD,MAAM,QAAQ,MAAMC,+BAAAA,cAA4B,UAAU;IACxD,WAAW,KAAK;IAChB,QAAQ;IACR,UAAU;GACZ,CAAC;GAID,MAAM,OAAOC,oBAAAA,eAAe,UAAU,IAAI;GAE1C,OAAO;IACL,MAF0B,OAAO;KAAE,GAAG;KAAO;IAAK,IAAI;IAGtD,QAAQ;KACN,KAAK,MAAM;KACX,OAAO,MAAM;KACb,gBAAgB;KAChB,GAAGC,oBAAAA,WAAW;MACZ,YAAY,MAAM,MAAM;MACxB,aAAa,MAAM,MAAM;KAC3B,CAAC;IACH;GACF;EACF;EACA,MAAM,kBAAkB,EAAE,MAAM,GAAG,aAAa,GAAG,UAAmC;GACpF,CAAA,GAAA,cAAA,UAAA,CAAU,YAAY,MAAM,kBAAkB;GAC9C,MAAM,EAAE,aAAa;GACrB,MAAM,EAAE,SAAS;GACjB,MAAM,WAAW,MAAMJ,+BAAAA,0BAA0B;IAAE;IAAM;IAAU,GAAG;GAAO,CAAC;GAC9E,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,UAAW,MAAM,SAAS,KAAK;IACvC,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,KAAK;GAC9C;GACA,MAAM,QAAS,MAAM,SAAS,KAAK;GACnC,MAAM,EAAE,MAAM,WAAW,MAAM,KAAK,YAAY,OAAO,EAAE,UAAU,OAAO,SAAS,CAAC;GAGpF,MAAM,WAAWI,oBAAAA,gBAAgB,YAAY,QAAQ;GACrD,MAAM,OAAOD,oBAAAA,WAAW;IACtB,YAAY,UAAU,aAAa,KAAA;IACnC,aAAa,UAAU,cAAc,KAAA;IACrC,aAAa,UAAU,cAAc,KAAA;IACrC,UAAU,UAAU,YAAY,KAAA;GAClC,CAAC;GAKD,OAAO;IAAE;IAAO,UAAA;KAHd,MAAM,WAAW;MAAE,GAAI;MAAwB;KAAS,IAAK;KAC7D,QAAQ;MAAE,GAAG;MAAQ,GAAG;KAAK;IAER;GAAE;EAC3B;CACF;AACF;AAEA,MAAa,QAAQ,oBAAoB"}
1
+ {"version":3,"file":"apple.cjs","names":["createAuthorizationUri","exchangeAuthorizationCode","OAuth2Error","verifyIdToken","parseAppleUser","nameClaims","cleanPersonName"],"sources":["../../../src/oauth2/provider/apple.ts"],"sourcesContent":["import { invariant } from '@shware/utils';\nimport { OAuth2Error } from '../error';\nimport { type AppleUser, cleanPersonName, nameClaims, parseAppleUser } from '../name';\nimport type {\n LoginOAuth2NativeParams,\n NativeCredential,\n OAuth2Token,\n PersonNameComponents,\n Provider,\n UserInfo,\n} from '../types';\nimport { createAuthorizationUri, exchangeAuthorizationCode, verifyIdToken } from './common';\n\n/** `iss` of every Apple id token (https://appleid.apple.com/.well-known/openid-configuration). */\nconst APPLE_ISSUER = 'https://appleid.apple.com';\n\n// ref: https://account.apple.com/.well-known/openid-configuration\nexport function createAppleProvider(): Provider {\n return {\n // important notice: response_mode=form_post is required for apple\n authorizationUri: 'https://appleid.apple.com/auth/authorize?response_mode=form_post',\n tokenUri: 'https://appleid.apple.com/auth/token',\n jwkSetUri: 'https://appleid.apple.com/auth/keys',\n userNameAttribute: 'sub',\n defaultScope: ['openid', 'name', 'email'],\n // The web flow's id token echoes the request's nonce; the app's carries the one bound into\n // ASAuthorizationAppleIDRequest, hashed or not (`nonce_supported`).\n nonce: { web: true, native: true },\n createAuthorizationUri({ pkce: _, ...params }) {\n return createAuthorizationUri({\n ...params,\n scope: params.scope ?? this.defaultScope,\n authorizationUri: this.authorizationUri,\n });\n },\n async exchangeAuthorizationCode({ pkce: _, ...params }) {\n const response = await exchangeAuthorizationCode({ ...params, tokenUri: this.tokenUri });\n if (!response.ok) {\n const { error } = (await response.json()) as AppleErrorResponse;\n throw new OAuth2Error(response.status, error);\n }\n return (await response.json()) as AppleToken;\n },\n async getUserInfo({ id_token }, { clientId, response }) {\n invariant(id_token, 'id_token is required');\n invariant(this.jwkSetUri, 'jwkSetUri is required');\n // The audience is the client the code was exchanged as: the bundle id for an app, the\n // Services ID for the web.\n const token = await verifyIdToken<AppleIdToken>(id_token, {\n jwkSetUri: this.jwkSetUri,\n issuer: APPLE_ISSUER,\n audience: clientId,\n });\n // Apple's id token never carries a name; on the first authorization of the web flow the\n // name comes in the form_post `user` instead, unsigned, so only its name parts are used\n // and the signed id token's email always wins over the one beside them.\n const user = parseAppleUser(response?.user);\n const data: AppleUserInfo = user ? { ...token, user } : token;\n return {\n data,\n claims: {\n sub: token.sub,\n email: token.email,\n email_verified: true,\n ...nameClaims({\n given_name: user?.name?.firstName,\n family_name: user?.name?.lastName,\n }),\n },\n };\n },\n async loginOAuth2Native({ pkce: _, credentials, ...params }: LoginOAuth2NativeParams) {\n // The app's mistake, a 400 like any other malformed login body, not a server fault.\n if (!credentials.code) throw new OAuth2Error(400, 'invalid_request', 'code is required');\n const { tokenUri } = this;\n const { code } = credentials;\n const response = await exchangeAuthorizationCode({ code, tokenUri, ...params });\n if (!response.ok) {\n const { error } = (await response.json()) as AppleErrorResponse;\n throw new OAuth2Error(response.status, error);\n }\n const token = (await response.json()) as AppleToken;\n const { data, claims } = await this.getUserInfo(token, { clientId: params.clientId });\n // The app gets the name from Apple on the first authorization only and passes it along in\n // the login body as the platform's `fullName`.\n const fullName = cleanPersonName(credentials.fullName);\n const name = nameClaims({\n given_name: fullName?.givenName ?? undefined,\n family_name: fullName?.familyName ?? undefined,\n middle_name: fullName?.middleName ?? undefined,\n nickname: fullName?.nickname ?? undefined,\n });\n const userInfo: UserInfo<AppleUserInfo> = {\n data: fullName ? { ...(data as AppleUserInfo), fullName } : (data as AppleUserInfo),\n claims: { ...claims, ...name },\n };\n return { token, userInfo };\n },\n };\n}\n\nexport const apple = createAppleProvider();\n\n// Claims of Apple's id token. It carries no name or picture, not even on the first authorization.\n// https://developer.apple.com/documentation/signinwithapple/authenticating-users-with-sign-in-with-apple\nexport interface AppleIdToken {\n iss: string;\n aud: string;\n exp: number;\n iat: number;\n sub: string;\n email: string;\n email_verified: true | 'true';\n is_private_email: boolean;\n /** Native flow, first authorization only: 0 unsupported, 1 unknown, 2 likely real. */\n real_user_status?: number;\n nonce?: string;\n nonce_supported?: boolean;\n}\n\n/**\n * `userInfo.data` of an Apple login: the id token's claims, plus the unsigned name the client\n * passed on the first authorization, cleaned (see `nameClaims` for the standard claims made of it).\n */\nexport interface AppleUserInfo extends AppleIdToken {\n /** Web flow: Apple's form_post `user`, as Apple names it. */\n user?: AppleUser;\n /** Native flow: the login body's `fullName`, which alone keeps `namePrefix` / `nameSuffix`. */\n fullName?: PersonNameComponents;\n}\n\n// https://developer.apple.com/documentation/devicemanagement/implementing-the-oauth2-authentication-user-enrollment-flow\n// https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens\nexport interface AppleToken extends OAuth2Token {\n access_token: string;\n token_type: string;\n expires_in: number;\n refresh_token: string;\n id_token: string;\n}\n\nexport interface AppleNativeCredential extends NativeCredential {\n state: string;\n code: string;\n id_token: string;\n}\n\n// https://developer.apple.com/documentation/sign_in_with_apple/errorresponse\nexport interface AppleErrorResponse {\n error:\n | 'invalid_request'\n | 'invalid_client'\n | 'invalid_grant'\n | 'unauthorized_client'\n | 'unsupported_grant_type'\n | 'invalid_scope';\n}\n"],"mappings":";;;;;;;AAcA,MAAM,eAAe;AAGrB,SAAgB,sBAAgC;CAC9C,OAAO;EAEL,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,mBAAmB;EACnB,cAAc;GAAC;GAAU;GAAQ;EAAO;EAGxC,OAAO;GAAE,KAAK;GAAM,QAAQ;EAAK;EACjC,uBAAuB,EAAE,MAAM,GAAG,GAAG,UAAU;GAC7C,OAAOA,+BAAAA,uBAAuB;IAC5B,GAAG;IACH,OAAO,OAAO,SAAS,KAAK;IAC5B,kBAAkB,KAAK;GACzB,CAAC;EACH;EACA,MAAM,0BAA0B,EAAE,MAAM,GAAG,GAAG,UAAU;GACtD,MAAM,WAAW,MAAMC,+BAAAA,0BAA0B;IAAE,GAAG;IAAQ,UAAU,KAAK;GAAS,CAAC;GACvF,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,UAAW,MAAM,SAAS,KAAK;IACvC,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,KAAK;GAC9C;GACA,OAAQ,MAAM,SAAS,KAAK;EAC9B;EACA,MAAM,YAAY,EAAE,YAAY,EAAE,UAAU,YAAY;GACtD,CAAA,GAAA,cAAA,UAAA,CAAU,UAAU,sBAAsB;GAC1C,CAAA,GAAA,cAAA,UAAA,CAAU,KAAK,WAAW,uBAAuB;GAGjD,MAAM,QAAQ,MAAMC,+BAAAA,cAA4B,UAAU;IACxD,WAAW,KAAK;IAChB,QAAQ;IACR,UAAU;GACZ,CAAC;GAID,MAAM,OAAOC,oBAAAA,eAAe,UAAU,IAAI;GAE1C,OAAO;IACL,MAF0B,OAAO;KAAE,GAAG;KAAO;IAAK,IAAI;IAGtD,QAAQ;KACN,KAAK,MAAM;KACX,OAAO,MAAM;KACb,gBAAgB;KAChB,GAAGC,oBAAAA,WAAW;MACZ,YAAY,MAAM,MAAM;MACxB,aAAa,MAAM,MAAM;KAC3B,CAAC;IACH;GACF;EACF;EACA,MAAM,kBAAkB,EAAE,MAAM,GAAG,aAAa,GAAG,UAAmC;GAEpF,IAAI,CAAC,YAAY,MAAM,MAAM,IAAIH,qBAAAA,YAAY,KAAK,mBAAmB,kBAAkB;GACvF,MAAM,EAAE,aAAa;GACrB,MAAM,EAAE,SAAS;GACjB,MAAM,WAAW,MAAMD,+BAAAA,0BAA0B;IAAE;IAAM;IAAU,GAAG;GAAO,CAAC;GAC9E,IAAI,CAAC,SAAS,IAAI;IAChB,MAAM,EAAE,UAAW,MAAM,SAAS,KAAK;IACvC,MAAM,IAAIC,qBAAAA,YAAY,SAAS,QAAQ,KAAK;GAC9C;GACA,MAAM,QAAS,MAAM,SAAS,KAAK;GACnC,MAAM,EAAE,MAAM,WAAW,MAAM,KAAK,YAAY,OAAO,EAAE,UAAU,OAAO,SAAS,CAAC;GAGpF,MAAM,WAAWI,oBAAAA,gBAAgB,YAAY,QAAQ;GACrD,MAAM,OAAOD,oBAAAA,WAAW;IACtB,YAAY,UAAU,aAAa,KAAA;IACnC,aAAa,UAAU,cAAc,KAAA;IACrC,aAAa,UAAU,cAAc,KAAA;IACrC,UAAU,UAAU,YAAY,KAAA;GAClC,CAAC;GAKD,OAAO;IAAE;IAAO,UAAA;KAHd,MAAM,WAAW;MAAE,GAAI;MAAwB;KAAS,IAAK;KAC7D,QAAQ;MAAE,GAAG;MAAQ,GAAG;KAAK;IAER;GAAE;EAC3B;CACF;AACF;AAEA,MAAa,QAAQ,oBAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"apple.d.cts","names":[],"sources":["../../../src/oauth2/provider/apple.ts"],"mappings":";;wBAiBgB,uBAAuB;qBAmF1B,OAAK;iBAID;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;;;;;;iBAOe,sBAAsB;;EAErC,OAAO;;EAEP,WAAW;;iBAKI,mBAAmB;EAClC;EACA;EACA;EACA;EACA;;iBAGe,8BAA8B;EAC7C;EACA;EACA;;iBAIe;EACf"}
1
+ {"version":3,"file":"apple.d.cts","names":[],"sources":["../../../src/oauth2/provider/apple.ts"],"mappings":";;wBAiBgB,uBAAuB;qBAoF1B,OAAK;iBAID;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA;;;;;;iBAOe,sBAAsB;;EAErC,OAAO;;EAEP,WAAW;;iBAKI,mBAAmB;EAClC;EACA;EACA;EACA;EACA;;iBAGe,8BAA8B;EAC7C;EACA;EACA;;iBAIe;EACf"}