@robelest/convex-auth 0.0.4-preview.21 → 0.0.4-preview.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/index.js.map +1 -1
- package/dist/client/index.d.ts +1 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +36 -39
- package/dist/client/index.js.map +1 -1
- package/dist/component/client/index.d.ts +1 -2
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/model.d.ts +5 -5
- package/dist/component/model.d.ts.map +1 -1
- package/dist/component/public/enterprise/audit.d.ts.map +1 -1
- package/dist/component/public/enterprise/audit.js.map +1 -1
- package/dist/component/public/enterprise/core.d.ts.map +1 -1
- package/dist/component/public/enterprise/core.js.map +1 -1
- package/dist/component/public/enterprise/domains.d.ts.map +1 -1
- package/dist/component/public/enterprise/domains.js.map +1 -1
- package/dist/component/public/enterprise/scim.d.ts.map +1 -1
- package/dist/component/public/enterprise/scim.js.map +1 -1
- package/dist/component/public/enterprise/secrets.d.ts.map +1 -1
- package/dist/component/public/enterprise/secrets.js.map +1 -1
- package/dist/component/public/enterprise/webhooks.d.ts.map +1 -1
- package/dist/component/public/enterprise/webhooks.js.map +1 -1
- package/dist/component/public/factors/devices.d.ts.map +1 -1
- package/dist/component/public/factors/devices.js.map +1 -1
- package/dist/component/public/factors/passkeys.d.ts.map +1 -1
- package/dist/component/public/factors/passkeys.js.map +1 -1
- package/dist/component/public/factors/totp.d.ts.map +1 -1
- package/dist/component/public/factors/totp.js.map +1 -1
- package/dist/component/public/groups/core.js.map +1 -1
- package/dist/component/public/groups/invites.d.ts.map +1 -1
- package/dist/component/public/groups/invites.js.map +1 -1
- package/dist/component/public/groups/members.d.ts.map +1 -1
- package/dist/component/public/groups/members.js.map +1 -1
- package/dist/component/public/identity/accounts.d.ts.map +1 -1
- package/dist/component/public/identity/accounts.js.map +1 -1
- package/dist/component/public/identity/codes.d.ts.map +1 -1
- package/dist/component/public/identity/codes.js.map +1 -1
- package/dist/component/public/identity/sessions.d.ts.map +1 -1
- package/dist/component/public/identity/sessions.js.map +1 -1
- package/dist/component/public/identity/tokens.d.ts.map +1 -1
- package/dist/component/public/identity/tokens.js.map +1 -1
- package/dist/component/public/identity/users.d.ts.map +1 -1
- package/dist/component/public/identity/users.js.map +1 -1
- package/dist/component/public/identity/verifiers.d.ts.map +1 -1
- package/dist/component/public/identity/verifiers.js.map +1 -1
- package/dist/component/public/security/keys.d.ts.map +1 -1
- package/dist/component/public/security/keys.js.map +1 -1
- package/dist/component/public/security/limits.d.ts.map +1 -1
- package/dist/component/public/security/limits.js.map +1 -1
- package/dist/component/schema.d.ts +39 -39
- package/dist/component/server/auth.d.ts +95 -52
- package/dist/component/server/auth.d.ts.map +1 -1
- package/dist/component/server/auth.js +63 -43
- package/dist/component/server/auth.js.map +1 -1
- package/dist/component/server/core.js +116 -235
- package/dist/component/server/core.js.map +1 -1
- package/dist/component/server/crypto.js +25 -7
- package/dist/component/server/crypto.js.map +1 -1
- package/dist/component/server/device.js +58 -15
- package/dist/component/server/device.js.map +1 -1
- package/dist/component/server/enterprise/domain.js +148 -59
- package/dist/component/server/enterprise/domain.js.map +1 -1
- package/dist/component/server/enterprise/http.js +36 -15
- package/dist/component/server/enterprise/http.js.map +1 -1
- package/dist/component/server/enterprise/oidc.js +1 -1
- package/dist/component/server/http.js +26 -21
- package/dist/component/server/http.js.map +1 -1
- package/dist/component/server/identity.js +5 -2
- package/dist/component/server/identity.js.map +1 -1
- package/dist/component/server/limits.js +21 -30
- package/dist/component/server/limits.js.map +1 -1
- package/dist/component/server/mutations/account.js +12 -10
- package/dist/component/server/mutations/account.js.map +1 -1
- package/dist/component/server/mutations/code.js +5 -2
- package/dist/component/server/mutations/code.js.map +1 -1
- package/dist/component/server/mutations/invalidate.js +1 -1
- package/dist/component/server/mutations/invalidate.js.map +1 -1
- package/dist/component/server/mutations/oauth.js +10 -4
- package/dist/component/server/mutations/oauth.js.map +1 -1
- package/dist/component/server/mutations/refresh.js +2 -2
- package/dist/component/server/mutations/refresh.js.map +1 -1
- package/dist/component/server/mutations/register.js +46 -42
- package/dist/component/server/mutations/register.js.map +1 -1
- package/dist/component/server/mutations/retrieve.js +21 -25
- package/dist/component/server/mutations/retrieve.js.map +1 -1
- package/dist/component/server/mutations/signature.js +10 -4
- package/dist/component/server/mutations/signature.js.map +1 -1
- package/dist/component/server/mutations/signout.js.map +1 -1
- package/dist/component/server/mutations/store.js +9 -24
- package/dist/component/server/mutations/store.js.map +1 -1
- package/dist/component/server/mutations/verifier.js.map +1 -1
- package/dist/component/server/mutations/verify.js +1 -1
- package/dist/component/server/mutations/verify.js.map +1 -1
- package/dist/component/server/oauth.js +53 -16
- package/dist/component/server/oauth.js.map +1 -1
- package/dist/component/server/passkey.js +115 -31
- package/dist/component/server/passkey.js.map +1 -1
- package/dist/component/server/redirects.js +9 -3
- package/dist/component/server/redirects.js.map +1 -1
- package/dist/component/server/refresh.js +10 -7
- package/dist/component/server/refresh.js.map +1 -1
- package/dist/component/server/runtime.d.ts +3 -3
- package/dist/component/server/runtime.d.ts.map +1 -1
- package/dist/component/server/runtime.js +62 -20
- package/dist/component/server/runtime.js.map +1 -1
- package/dist/component/server/signin.js +34 -10
- package/dist/component/server/signin.js.map +1 -1
- package/dist/component/server/totp.js +79 -19
- package/dist/component/server/totp.js.map +1 -1
- package/dist/component/server/types.d.ts +12 -20
- package/dist/component/server/types.d.ts.map +1 -1
- package/dist/component/server/types.js.map +1 -1
- package/dist/component/server/users.js +6 -3
- package/dist/component/server/users.js.map +1 -1
- package/dist/component/server/utils.js +10 -4
- package/dist/component/server/utils.js.map +1 -1
- package/dist/core/types.d.ts +14 -22
- package/dist/core/types.d.ts.map +1 -1
- package/dist/factors/device.js +8 -9
- package/dist/factors/device.js.map +1 -1
- package/dist/factors/passkey.js +18 -21
- package/dist/factors/passkey.js.map +1 -1
- package/dist/providers/password.js +66 -81
- package/dist/providers/password.js.map +1 -1
- package/dist/runtime/invite.js +2 -8
- package/dist/runtime/invite.js.map +1 -1
- package/dist/server/auth.d.ts +95 -52
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +63 -43
- package/dist/server/auth.js.map +1 -1
- package/dist/server/core.d.ts +71 -159
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +116 -235
- package/dist/server/core.js.map +1 -1
- package/dist/server/crypto.d.ts.map +1 -1
- package/dist/server/crypto.js +25 -7
- package/dist/server/crypto.js.map +1 -1
- package/dist/server/device.js +58 -15
- package/dist/server/device.js.map +1 -1
- package/dist/server/enterprise/domain.d.ts +0 -8
- package/dist/server/enterprise/domain.d.ts.map +1 -1
- package/dist/server/enterprise/domain.js +148 -59
- package/dist/server/enterprise/domain.js.map +1 -1
- package/dist/server/enterprise/http.d.ts.map +1 -1
- package/dist/server/enterprise/http.js +35 -14
- package/dist/server/enterprise/http.js.map +1 -1
- package/dist/server/http.d.ts +2 -2
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +25 -20
- package/dist/server/http.js.map +1 -1
- package/dist/server/identity.js +5 -2
- package/dist/server/identity.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/limits.js +21 -30
- package/dist/server/limits.js.map +1 -1
- package/dist/server/mounts.d.ts +26 -64
- package/dist/server/mounts.d.ts.map +1 -1
- package/dist/server/mounts.js +45 -106
- package/dist/server/mounts.js.map +1 -1
- package/dist/server/mutations/account.d.ts +8 -9
- package/dist/server/mutations/account.d.ts.map +1 -1
- package/dist/server/mutations/account.js +11 -9
- package/dist/server/mutations/account.js.map +1 -1
- package/dist/server/mutations/code.d.ts +13 -13
- package/dist/server/mutations/code.d.ts.map +1 -1
- package/dist/server/mutations/code.js +5 -2
- package/dist/server/mutations/code.js.map +1 -1
- package/dist/server/mutations/invalidate.d.ts +4 -4
- package/dist/server/mutations/invalidate.d.ts.map +1 -1
- package/dist/server/mutations/invalidate.js.map +1 -1
- package/dist/server/mutations/oauth.d.ts +12 -10
- package/dist/server/mutations/oauth.d.ts.map +1 -1
- package/dist/server/mutations/oauth.js +9 -3
- package/dist/server/mutations/oauth.js.map +1 -1
- package/dist/server/mutations/refresh.d.ts +3 -3
- package/dist/server/mutations/refresh.d.ts.map +1 -1
- package/dist/server/mutations/refresh.js +1 -1
- package/dist/server/mutations/refresh.js.map +1 -1
- package/dist/server/mutations/register.d.ts +11 -11
- package/dist/server/mutations/register.d.ts.map +1 -1
- package/dist/server/mutations/register.js +45 -41
- package/dist/server/mutations/register.js.map +1 -1
- package/dist/server/mutations/retrieve.d.ts +6 -6
- package/dist/server/mutations/retrieve.d.ts.map +1 -1
- package/dist/server/mutations/retrieve.js +20 -24
- package/dist/server/mutations/retrieve.js.map +1 -1
- package/dist/server/mutations/signature.d.ts +6 -7
- package/dist/server/mutations/signature.d.ts.map +1 -1
- package/dist/server/mutations/signature.js +9 -3
- package/dist/server/mutations/signature.js.map +1 -1
- package/dist/server/mutations/signin.d.ts +5 -5
- package/dist/server/mutations/signin.d.ts.map +1 -1
- package/dist/server/mutations/signout.js.map +1 -1
- package/dist/server/mutations/store.d.ts +97 -97
- package/dist/server/mutations/store.d.ts.map +1 -1
- package/dist/server/mutations/store.js +8 -23
- package/dist/server/mutations/store.js.map +1 -1
- package/dist/server/mutations/verifier.js.map +1 -1
- package/dist/server/mutations/verify.d.ts +10 -10
- package/dist/server/mutations/verify.d.ts.map +1 -1
- package/dist/server/mutations/verify.js.map +1 -1
- package/dist/server/oauth.js +53 -16
- package/dist/server/oauth.js.map +1 -1
- package/dist/server/passkey.d.ts +2 -2
- package/dist/server/passkey.d.ts.map +1 -1
- package/dist/server/passkey.js +114 -30
- package/dist/server/passkey.js.map +1 -1
- package/dist/server/redirects.js +9 -3
- package/dist/server/redirects.js.map +1 -1
- package/dist/server/refresh.js +10 -7
- package/dist/server/refresh.js.map +1 -1
- package/dist/server/runtime.d.ts +14 -14
- package/dist/server/runtime.d.ts.map +1 -1
- package/dist/server/runtime.js +61 -19
- package/dist/server/runtime.js.map +1 -1
- package/dist/server/signin.js +34 -10
- package/dist/server/signin.js.map +1 -1
- package/dist/server/ssr.d.ts.map +1 -1
- package/dist/server/ssr.js +175 -184
- package/dist/server/ssr.js.map +1 -1
- package/dist/server/totp.js +78 -18
- package/dist/server/totp.js.map +1 -1
- package/dist/server/types.d.ts +13 -21
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/dist/server/users.js +6 -3
- package/dist/server/users.js.map +1 -1
- package/dist/server/utils.js +10 -4
- package/dist/server/utils.js.map +1 -1
- package/package.json +2 -6
- package/src/authorization/index.ts +1 -1
- package/src/cli/index.ts +1 -1
- package/src/client/core/types.ts +14 -14
- package/src/client/factors/device.ts +10 -12
- package/src/client/factors/passkey.ts +23 -26
- package/src/client/index.ts +54 -64
- package/src/client/runtime/invite.ts +5 -7
- package/src/component/index.ts +1 -0
- package/src/component/public/enterprise/audit.ts +6 -1
- package/src/component/public/enterprise/core.ts +1 -0
- package/src/component/public/enterprise/domains.ts +5 -1
- package/src/component/public/enterprise/scim.ts +1 -0
- package/src/component/public/enterprise/secrets.ts +1 -0
- package/src/component/public/enterprise/webhooks.ts +1 -0
- package/src/component/public/factors/devices.ts +1 -0
- package/src/component/public/factors/passkeys.ts +1 -0
- package/src/component/public/factors/totp.ts +1 -0
- package/src/component/public/groups/core.ts +1 -1
- package/src/component/public/groups/invites.ts +7 -1
- package/src/component/public/groups/members.ts +1 -0
- package/src/component/public/identity/accounts.ts +1 -0
- package/src/component/public/identity/codes.ts +1 -0
- package/src/component/public/identity/sessions.ts +1 -0
- package/src/component/public/identity/tokens.ts +1 -0
- package/src/component/public/identity/users.ts +1 -0
- package/src/component/public/identity/verifiers.ts +1 -0
- package/src/component/public/security/keys.ts +1 -0
- package/src/component/public/security/limits.ts +1 -0
- package/src/providers/password.ts +89 -110
- package/src/server/auth.ts +177 -111
- package/src/server/core.ts +197 -233
- package/src/server/crypto.ts +31 -29
- package/src/server/device.ts +65 -32
- package/src/server/enterprise/domain.ts +158 -170
- package/src/server/enterprise/http.ts +46 -39
- package/src/server/http.ts +36 -30
- package/src/server/identity.ts +5 -5
- package/src/server/index.ts +2 -0
- package/src/server/limits.ts +53 -80
- package/src/server/mounts.ts +47 -74
- package/src/server/mutations/account.ts +22 -36
- package/src/server/mutations/code.ts +6 -6
- package/src/server/mutations/invalidate.ts +1 -1
- package/src/server/mutations/oauth.ts +14 -8
- package/src/server/mutations/refresh.ts +5 -4
- package/src/server/mutations/register.ts +87 -132
- package/src/server/mutations/retrieve.ts +44 -44
- package/src/server/mutations/signature.ts +13 -6
- package/src/server/mutations/signout.ts +1 -1
- package/src/server/mutations/store.ts +16 -31
- package/src/server/mutations/verifier.ts +1 -1
- package/src/server/mutations/verify.ts +3 -5
- package/src/server/oauth.ts +60 -69
- package/src/server/passkey.ts +567 -517
- package/src/server/redirects.ts +10 -6
- package/src/server/refresh.ts +14 -18
- package/src/server/runtime.ts +70 -55
- package/src/server/signin.ts +44 -37
- package/src/server/ssr.ts +390 -407
- package/src/server/totp.ts +85 -35
- package/src/server/types.ts +19 -22
- package/src/server/users.ts +7 -6
- package/src/server/utils.ts +10 -12
- package/dist/component/server/authError.js +0 -34
- package/dist/component/server/authError.js.map +0 -1
- package/dist/component/server/errors.d.ts +0 -1
- package/dist/component/server/errors.js +0 -137
- package/dist/component/server/errors.js.map +0 -1
- package/dist/server/authError.d.ts +0 -46
- package/dist/server/authError.d.ts.map +0 -1
- package/dist/server/authError.js +0 -34
- package/dist/server/authError.js.map +0 -1
- package/dist/server/errors.d.ts +0 -177
- package/dist/server/errors.d.ts.map +0 -1
- package/dist/server/errors.js +0 -212
- package/dist/server/errors.js.map +0 -1
- package/src/server/authError.ts +0 -44
- package/src/server/errors.ts +0 -290
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passkey.js","names":["hasSiteUrl","hasRpId","siteUrl"],"sources":["../../src/server/passkey.ts"],"sourcesContent":["/**\n * Server-side WebAuthn ceremony logic for passkey authentication.\n *\n * Handles the four phases of the WebAuthn flow:\n * 1. registerOptions — generate PublicKeyCredentialCreationOptions\n * 2. registerVerify — verify attestation and store credential\n * 3. authOptions — generate PublicKeyCredentialRequestOptions\n * 4. authVerify — verify assertion signature and sign in\n *\n * Uses `@oslojs/webauthn` for attestation/assertion parsing and\n * `@oslojs/crypto` for signature verification.\n *\n * All functions return `Fx<A, AuthError>` composed via `Fx.chain` pipelines.\n *\n * @module\n */\n\nimport {\n p256,\n verifyECDSASignature,\n decodeSEC1PublicKey,\n decodePKIXECDSASignature,\n} from \"@oslojs/crypto/ecdsa\";\nimport {\n RSAPublicKey,\n decodePKCS1RSAPublicKey,\n sha256ObjectIdentifier,\n verifyRSASSAPKCS1v15Signature,\n} from \"@oslojs/crypto/rsa\";\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport {\n encodeBase64urlNoPadding,\n decodeBase64urlIgnorePadding,\n} from \"@oslojs/encoding\";\nimport {\n parseAttestationObject,\n parseClientDataJSON,\n parseAuthenticatorData,\n createAssertionSignatureMessage,\n ClientDataType,\n coseAlgorithmES256,\n coseAlgorithmRS256,\n COSEKeyType,\n} from \"@oslojs/webauthn\";\nimport type { Fx as FxType } from \"@robelest/fx\";\n\nimport { authDb } from \"./db\";\nimport { Fx } from \"@robelest/fx\";\n\nimport { AuthError } from \"./authError\";\nimport { userIdFromIdentitySubject } from \"./identity\";\nimport { callSignIn, callVerifier } from \"./mutations/index\";\nimport { callVerifierSignature } from \"./mutations/signature\";\nimport { PasskeyProviderConfig, GenericActionCtxWithAuthConfig } from \"./types\";\nimport {\n AuthDataModel,\n SessionInfo,\n queryUserById,\n queryUserByVerifiedEmail,\n queryPasskeysByUserId,\n queryPasskeyByCredentialId,\n queryVerifierById,\n mutatePasskeyInsert,\n mutatePasskeyUpdateCounter,\n mutateVerifierDelete,\n} from \"./types\";\n\ntype EnrichedActionCtx = GenericActionCtxWithAuthConfig<AuthDataModel>;\n\n// ============================================================================\n// Resolve RP options — Fx pipeline with validation\n// ============================================================================\n\n/** Resolved relying party configuration. */\ninterface RpOptions {\n rpName: string;\n rpId: string;\n origin: string | string[];\n attestation: string;\n userVerification: string;\n residentKey: string;\n authenticatorAttachment?: string;\n algorithms: number[];\n challengeExpirationMs: number;\n}\n\n/**\n * Resolve passkey relying party options from provider config and environment.\n *\n * Returns `Fx<RpOptions, AuthError>` — fails if neither SITE_URL nor rpId\n * is configured.\n */\nconst resolveRpOptionsFx = (\n provider: PasskeyProviderConfig,\n): FxType<RpOptions, AuthError> => {\n const siteUrl = process.env.SITE_URL;\n const hasSiteUrl = siteUrl !== undefined && siteUrl !== \"\";\n const hasRpId = provider.options.rpId !== undefined;\n\n return Fx.succeed({ siteUrl, hasSiteUrl, hasRpId }).pipe(\n Fx.chain(({ siteUrl, hasSiteUrl, hasRpId }) =>\n !hasSiteUrl && !hasRpId\n ? Fx.fail(\n new AuthError(\n \"PASSKEY_MISSING_CONFIG\",\n \"Passkey provider requires SITE_URL env var (your frontend URL) \" +\n \"or explicit rpId / origin in the provider config. \" +\n \"CONVEX_SITE_URL cannot be used because WebAuthn RP ID must match the frontend domain.\",\n ),\n )\n : Fx.succeed(siteUrl),\n ),\n Fx.map((siteUrl) => {\n const siteHostname = siteUrl ? new URL(siteUrl).hostname : undefined;\n return {\n rpName: provider.options.rpName ?? siteHostname ?? \"localhost\",\n rpId: provider.options.rpId ?? siteHostname ?? \"localhost\",\n origin: provider.options.origin ?? siteUrl ?? \"http://localhost\",\n attestation: provider.options.attestation ?? \"none\",\n userVerification: provider.options.userVerification ?? \"required\",\n residentKey: provider.options.residentKey ?? \"preferred\",\n authenticatorAttachment: provider.options.authenticatorAttachment,\n algorithms: provider.options.algorithms ?? [\n coseAlgorithmES256,\n coseAlgorithmRS256,\n ],\n challengeExpirationMs:\n provider.options.challengeExpirationMs ?? 300_000,\n };\n }),\n );\n};\n\n// ============================================================================\n// Composable validators — small functions (A) => Fx<B, AuthError>\n// ============================================================================\n\n/** Verify client data type matches expected WebAuthn ceremony type. */\nconst verifyClientDataType =\n <T extends { type: ClientDataType }>(\n expectedType: ClientDataType,\n label: string,\n ) =>\n (clientData: T): FxType<T, AuthError> =>\n clientData.type === expectedType\n ? Fx.succeed(clientData)\n : Fx.fail(\n new AuthError(\n \"PASSKEY_INVALID_CLIENT_DATA\",\n `Invalid client data type: expected ${label}`,\n ),\n );\n\n/** Verify origin is in the allowed list. */\nconst verifyOrigin =\n (rp: RpOptions) =>\n <T extends { origin: string }>(clientData: T): FxType<T, AuthError> => {\n const allowed = Array.isArray(rp.origin) ? rp.origin : [rp.origin];\n return allowed.includes(clientData.origin)\n ? Fx.succeed(clientData)\n : Fx.fail(\n new AuthError(\n \"PASSKEY_INVALID_ORIGIN\",\n `Invalid origin: ${clientData.origin}, expected one of: ${allowed.join(\", \")}`,\n ),\n );\n };\n\n/** Verify the challenge hash matches the stored verifier, then delete verifier. */\nconst verifyAndConsumeChallenge =\n (ctx: EnrichedActionCtx, verifierValue: string) =>\n <T extends { challenge: Uint8Array }>(\n clientData: T,\n ): FxType<T, AuthError> => {\n const challengeHash = encodeBase64urlNoPadding(\n new Uint8Array(sha256(clientData.challenge)),\n );\n return Fx.from({\n ok: () => queryVerifierById(ctx, verifierValue),\n err: () => new AuthError(\"PASSKEY_INVALID_CHALLENGE\"),\n }).pipe(\n Fx.chain((doc) =>\n !doc || doc.signature !== challengeHash\n ? Fx.fail(new AuthError(\"PASSKEY_INVALID_CHALLENGE\"))\n : Fx.succeed(doc),\n ),\n Fx.chain(() =>\n Fx.from({\n ok: () => mutateVerifierDelete(ctx, verifierValue),\n err: () => new AuthError(\"PASSKEY_INVALID_CHALLENGE\"),\n }),\n ),\n Fx.map(() => clientData),\n );\n };\n\n/** Verify RP ID hash matches. */\nconst verifyRpId =\n (rpId: string) =>\n <T extends { verifyRelyingPartyIdHash: (id: string) => boolean }>(\n authData: T,\n ): FxType<T, AuthError> =>\n authData.verifyRelyingPartyIdHash(rpId)\n ? Fx.succeed(authData)\n : Fx.fail(new AuthError(\"PASSKEY_RP_MISMATCH\"));\n\n/** Verify user presence and (optionally) user verification flags. */\nconst verifyUserFlags =\n (rp: RpOptions) =>\n <T extends { userPresent: boolean; userVerified: boolean }>(\n authData: T,\n ): FxType<T, AuthError> =>\n !authData.userPresent\n ? Fx.fail(new AuthError(\"PASSKEY_USER_PRESENCE\"))\n : rp.userVerification === \"required\" && !authData.userVerified\n ? Fx.fail(new AuthError(\"PASSKEY_USER_VERIFICATION\"))\n : Fx.succeed(authData);\n\n// ============================================================================\n// Registration flow\n// ============================================================================\n\n// ============================================================================\n// Authentication flow\n// ============================================================================\n\n// ============================================================================\n// Main dispatch\n// ============================================================================\n\n/** Result type for all passkey flows. */\ntype PasskeyResult =\n | { kind: \"signedIn\"; signedIn: SessionInfo | null }\n | { kind: \"passkeyOptions\"; options: Record<string, any>; verifier: string };\n\nconst PASSKEY_FLOW = {\n registerOptions: \"registerOptions\",\n registerVerify: \"registerVerify\",\n authOptions: \"authOptions\",\n authVerify: \"authVerify\",\n} as const;\n\nconst PASSKEY_FLOWS = [\n PASSKEY_FLOW.registerOptions,\n PASSKEY_FLOW.registerVerify,\n PASSKEY_FLOW.authOptions,\n PASSKEY_FLOW.authVerify,\n] as const;\n\ntype PasskeyDispatch =\n | { flow: typeof PASSKEY_FLOW.registerOptions }\n | { flow: typeof PASSKEY_FLOW.registerVerify }\n | { flow: typeof PASSKEY_FLOW.authOptions }\n | { flow: typeof PASSKEY_FLOW.authVerify };\n\nconst resolvePasskeyDispatchFx = (\n params: Record<string, unknown>,\n): FxType<PasskeyDispatch, AuthError> => {\n const flow = params.flow;\n return typeof flow === \"string\" && PASSKEY_FLOWS.includes(flow as never)\n ? Fx.succeed({ flow: flow as (typeof PASSKEY_FLOWS)[number] })\n : Fx.fail(\n new AuthError(\n \"PASSKEY_MISSING_FLOW\",\n \"Missing `flow` parameter. Expected one of: registerOptions, registerVerify, authOptions, authVerify\",\n ),\n );\n};\n\nconst requirePasskeyVerifierFx = (\n verifier: string | undefined,\n): FxType<string, AuthError> =>\n verifier != null\n ? Fx.succeed(verifier)\n : Fx.fail(new AuthError(\"PASSKEY_MISSING_VERIFIER\"));\n\n/**\n * Main passkey handler dispatched from signIn.ts.\n *\n * Routes to the appropriate phase based on `params.flow` via `dispatchFx`.\n */\nexport function handlePasskeyFx(\n ctx: EnrichedActionCtx,\n provider: PasskeyProviderConfig,\n args: {\n params?: Record<string, any>;\n verifier?: string;\n },\n): FxType<PasskeyResult, AuthError> {\n const params = (args.params ?? {}) as Record<string, any>;\n\n return resolvePasskeyDispatchFx(params).pipe(\n Fx.chain((dispatch) => {\n const flowFx: FxType<PasskeyResult, AuthError> = Fx.match(dispatch).on(\n \"flow\",\n {\n registerOptions: (_) =>\n Fx.zip(\n Fx.from({\n ok: () => ctx.auth.getUserIdentity(),\n err: () => new AuthError(\"PASSKEY_AUTH_REQUIRED\"),\n }).pipe(\n Fx.chain((id) =>\n id === null\n ? Fx.fail(new AuthError(\"PASSKEY_AUTH_REQUIRED\"))\n : Fx.succeed(userIdFromIdentitySubject(id.subject)),\n ),\n ),\n resolveRpOptionsFx(provider),\n ).pipe(\n Fx.chain(([userId, rp]) => {\n const challenge = new Uint8Array(32);\n crypto.getRandomValues(challenge);\n const challengeHash = encodeBase64urlNoPadding(\n new Uint8Array(sha256(challenge)),\n );\n\n return Fx.from({\n ok: async () => {\n const verifier = await callVerifier(ctx);\n await callVerifierSignature(ctx, {\n verifier,\n signature: challengeHash,\n });\n\n const user = await queryUserById(ctx, userId);\n const userName = params.userName ?? user?.email ?? \"user\";\n const userDisplayName =\n params.userDisplayName ?? user?.name ?? userName;\n\n const existing = await queryPasskeysByUserId(ctx, userId);\n const excludeCredentials = existing.map((pk) => ({\n id: pk.credentialId,\n transports: pk.transports,\n }));\n\n const userHandle = encodeBase64urlNoPadding(\n new TextEncoder().encode(userId),\n );\n\n const options = {\n rp: { name: rp.rpName, id: rp.rpId },\n user: {\n id: userHandle,\n name: userName,\n displayName: userDisplayName,\n },\n challenge: encodeBase64urlNoPadding(challenge),\n pubKeyCredParams: rp.algorithms.map((alg) => ({\n type: \"public-key\" as const,\n alg,\n })),\n timeout: rp.challengeExpirationMs,\n attestation: rp.attestation,\n authenticatorSelection: {\n residentKey: rp.residentKey,\n requireResidentKey: rp.residentKey === \"required\",\n userVerification: rp.userVerification,\n ...(rp.authenticatorAttachment\n ? {\n authenticatorAttachment:\n rp.authenticatorAttachment,\n }\n : {}),\n },\n excludeCredentials,\n };\n\n return {\n kind: \"passkeyOptions\" as const,\n options,\n verifier,\n };\n },\n err: () => new AuthError(\"INTERNAL_ERROR\"),\n });\n }),\n ),\n registerVerify: (_) =>\n Fx.zip(\n Fx.from({\n ok: () => ctx.auth.getUserIdentity(),\n err: () => new AuthError(\"PASSKEY_AUTH_REQUIRED\"),\n }).pipe(\n Fx.chain((id) =>\n id === null\n ? Fx.fail(new AuthError(\"PASSKEY_AUTH_REQUIRED\"))\n : Fx.succeed(userIdFromIdentitySubject(id.subject)),\n ),\n ),\n resolveRpOptionsFx(provider),\n ).pipe(\n Fx.chain(([userId, rp]) =>\n requirePasskeyVerifierFx(args.verifier).pipe(\n Fx.chain((verifier) => {\n const clientDataJSON = decodeBase64urlIgnorePadding(\n params.clientDataJSON,\n );\n const clientData = parseClientDataJSON(clientDataJSON);\n\n const verifiedClientDataFx = Fx.succeed(clientData).pipe(\n Fx.chain(\n verifyClientDataType(\n ClientDataType.Create,\n \"webauthn.create\",\n ),\n ),\n Fx.chain(verifyOrigin(rp)),\n Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),\n Fx.map(() => {\n const attestationObjectBytes =\n decodeBase64urlIgnorePadding(\n params.attestationObject,\n );\n const attestation = parseAttestationObject(\n attestationObjectBytes,\n );\n return attestation.authenticatorData;\n }),\n );\n\n return verifiedClientDataFx.pipe(\n Fx.chain(verifyRpId(rp.rpId)),\n Fx.chain(verifyUserFlags(rp)),\n Fx.chain((authData) => {\n if (authData.credential == null) {\n return Fx.fail(\n new AuthError(\"PASSKEY_NO_CREDENTIAL\"),\n );\n }\n return Fx.succeed({\n authData,\n credential: authData.credential,\n });\n }),\n Fx.chain(({ authData, credential }) => {\n const credentialId = encodeBase64urlNoPadding(\n credential.id,\n );\n const publicKey = credential.publicKey;\n\n let algorithm: number;\n if (publicKey.isAlgorithmDefined()) {\n algorithm = publicKey.algorithm();\n } else {\n const keyType = publicKey.type();\n algorithm =\n keyType === COSEKeyType.EC2\n ? coseAlgorithmES256\n : keyType === COSEKeyType.RSA\n ? coseAlgorithmRS256\n : coseAlgorithmES256;\n }\n\n const handlers: Record<\n number,\n (() => FxType<Uint8Array, AuthError>) | undefined\n > = {\n [coseAlgorithmES256]: () => {\n const ec2 = publicKey.ec2();\n const xBytes = new Uint8Array(32);\n let vx = ec2.x;\n for (let i = 31; i >= 0; i--) {\n xBytes[i] = Number(vx & 0xffn);\n vx >>= 8n;\n }\n const yBytes = new Uint8Array(32);\n let vy = ec2.y;\n for (let i = 31; i >= 0; i--) {\n yBytes[i] = Number(vy & 0xffn);\n vy >>= 8n;\n }\n const bytes = new Uint8Array(65);\n bytes[0] = 0x04;\n bytes.set(xBytes, 1);\n bytes.set(yBytes, 33);\n return Fx.succeed(bytes);\n },\n [coseAlgorithmRS256]: () => {\n const rsa = publicKey.rsa();\n const rsaPubKey = new RSAPublicKey(rsa.n, rsa.e);\n return Fx.succeed(rsaPubKey.encodePKCS1());\n },\n };\n\n const handler = handlers[algorithm];\n return (\n handler\n ? handler()\n : Fx.fail(\n new AuthError(\n \"PASSKEY_UNSUPPORTED_ALGORITHM\",\n `Unsupported algorithm: ${algorithm}`,\n ),\n )\n ).pipe(\n Fx.chain((publicKeyBytes) =>\n Fx.from({\n ok: async () => {\n const deviceType =\n params.deviceType ?? \"single-device\";\n const backedUp = params.backedUp ?? false;\n\n const db = authDb(ctx, ctx.auth.config);\n await db.accounts.create({\n userId,\n provider: provider.id,\n providerAccountId: credentialId,\n });\n\n await mutatePasskeyInsert(ctx, {\n userId,\n credentialId,\n publicKey: publicKeyBytes.buffer.slice(\n publicKeyBytes.byteOffset,\n publicKeyBytes.byteOffset +\n publicKeyBytes.byteLength,\n ),\n algorithm,\n counter: authData.signatureCounter,\n transports: params.transports,\n deviceType,\n backedUp,\n name: params.passkeyName,\n createdAt: Date.now(),\n });\n\n const signInResult = await callSignIn(ctx, {\n userId,\n generateTokens: true,\n });\n\n return {\n kind: \"signedIn\" as const,\n signedIn: signInResult,\n };\n },\n err: () => new AuthError(\"INTERNAL_ERROR\"),\n }),\n ),\n );\n }),\n );\n }),\n ),\n ),\n ),\n authOptions: (_) =>\n resolveRpOptionsFx(provider).pipe(\n Fx.chain((rp) => {\n const challenge = new Uint8Array(32);\n crypto.getRandomValues(challenge);\n const challengeHash = encodeBase64urlNoPadding(\n new Uint8Array(sha256(challenge)),\n );\n\n return Fx.from({\n ok: async () => {\n const verifier = await callVerifier(ctx);\n await callVerifierSignature(ctx, {\n verifier,\n signature: challengeHash,\n });\n\n let allowCredentials:\n | Array<{\n type: string;\n id: string;\n transports?: string[];\n }>\n | undefined;\n if (params.email) {\n const user = await queryUserByVerifiedEmail(\n ctx,\n params.email,\n );\n if (user) {\n const passkeys = await queryPasskeysByUserId(\n ctx,\n user._id,\n );\n if (passkeys.length > 0) {\n allowCredentials = passkeys.map((pk) => ({\n type: \"public-key\",\n id: pk.credentialId,\n transports: pk.transports,\n }));\n }\n }\n }\n\n const options: Record<string, any> = {\n challenge: encodeBase64urlNoPadding(challenge),\n timeout: rp.challengeExpirationMs,\n rpId: rp.rpId,\n userVerification: rp.userVerification,\n };\n\n if (allowCredentials) {\n options.allowCredentials = allowCredentials;\n }\n\n return {\n kind: \"passkeyOptions\" as const,\n options,\n verifier,\n };\n },\n err: () => new AuthError(\"INTERNAL_ERROR\"),\n });\n }),\n ),\n authVerify: (_) =>\n Fx.zip(\n resolveRpOptionsFx(provider),\n requirePasskeyVerifierFx(args.verifier),\n ).pipe(\n Fx.chain(([rp, verifier]) => {\n const clientDataJSON = decodeBase64urlIgnorePadding(\n params.clientDataJSON,\n );\n const clientData = parseClientDataJSON(clientDataJSON);\n\n const verifiedClientDataFx = Fx.succeed(clientData).pipe(\n Fx.chain(\n verifyClientDataType(ClientDataType.Get, \"webauthn.get\"),\n ),\n Fx.chain(verifyOrigin(rp)),\n Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),\n Fx.chain(() =>\n params.credentialId != null\n ? Fx.succeed(params.credentialId as string)\n : Fx.fail(\n new AuthError(\n \"PASSKEY_UNKNOWN_CREDENTIAL\",\n \"Missing credential ID\",\n ),\n ),\n ),\n );\n\n return verifiedClientDataFx.pipe(\n Fx.chain((credentialId) =>\n Fx.from({\n ok: () => queryPasskeyByCredentialId(ctx, credentialId),\n err: () => new AuthError(\"PASSKEY_UNKNOWN_CREDENTIAL\"),\n }).pipe(\n Fx.chain((passkey) =>\n passkey\n ? Fx.succeed(passkey)\n : Fx.fail(\n new AuthError(\n \"PASSKEY_UNKNOWN_CREDENTIAL\",\n \"Unknown credential\",\n ),\n ),\n ),\n ),\n ),\n Fx.chain((passkey) => {\n const authenticatorDataBytes = decodeBase64urlIgnorePadding(\n params.authenticatorData,\n );\n const authenticatorData = parseAuthenticatorData(\n authenticatorDataBytes,\n );\n\n const signature = decodeBase64urlIgnorePadding(\n params.signature,\n );\n const signatureMessage = createAssertionSignatureMessage(\n authenticatorDataBytes,\n clientDataJSON,\n );\n const messageHash = sha256(signatureMessage);\n\n const checkedAuthenticatorFx = Fx.succeed(\n authenticatorData,\n ).pipe(\n Fx.chain(verifyRpId(rp.rpId)),\n Fx.chain(verifyUserFlags(rp)),\n );\n\n const signatureVerifiedFx = checkedAuthenticatorFx.pipe(\n Fx.chain(() => {\n const storedPublicKeyBytes = new Uint8Array(\n passkey.publicKey,\n );\n const algorithmHandlers: Record<\n number,\n (() => FxType<void, AuthError>) | undefined\n > = {\n [coseAlgorithmES256]: () => {\n const ecPublicKey = decodeSEC1PublicKey(\n p256,\n storedPublicKeyBytes,\n );\n const ecdsaSignature =\n decodePKIXECDSASignature(signature);\n const valid = verifyECDSASignature(\n ecPublicKey,\n messageHash,\n ecdsaSignature,\n );\n return valid\n ? Fx.succeed(undefined as void)\n : Fx.fail(\n new AuthError(\"PASSKEY_INVALID_SIGNATURE\"),\n );\n },\n [coseAlgorithmRS256]: () => {\n const rsaPublicKey =\n decodePKCS1RSAPublicKey(storedPublicKeyBytes);\n const valid = verifyRSASSAPKCS1v15Signature(\n rsaPublicKey,\n sha256ObjectIdentifier,\n messageHash,\n signature,\n );\n return valid\n ? Fx.succeed(undefined as void)\n : Fx.fail(\n new AuthError(\"PASSKEY_INVALID_SIGNATURE\"),\n );\n },\n };\n\n const handler = algorithmHandlers[passkey.algorithm];\n return handler\n ? handler()\n : Fx.fail(\n new AuthError(\n \"PASSKEY_UNSUPPORTED_ALGORITHM\",\n `Unsupported algorithm: ${passkey.algorithm}`,\n ),\n );\n }),\n );\n\n const counterValidatedFx = signatureVerifiedFx.pipe(\n Fx.chain(() =>\n passkey.counter !== 0 &&\n authenticatorData.signatureCounter !== 0 &&\n authenticatorData.signatureCounter <= passkey.counter\n ? Fx.fail(new AuthError(\"PASSKEY_COUNTER_ERROR\"))\n : Fx.succeed(authenticatorData),\n ),\n );\n\n return counterValidatedFx.pipe(\n Fx.chain(() =>\n Fx.from({\n ok: async () => {\n await mutatePasskeyUpdateCounter(\n ctx,\n passkey._id,\n authenticatorData.signatureCounter,\n Date.now(),\n );\n\n const signInResult = await callSignIn(ctx, {\n userId: passkey.userId,\n generateTokens: true,\n });\n\n return {\n kind: \"signedIn\" as const,\n signedIn: signInResult,\n };\n },\n err: () => new AuthError(\"INTERNAL_ERROR\"),\n }),\n ),\n );\n }),\n );\n }),\n ),\n },\n );\n return flowFx;\n }),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"passkey.js","names":["hasSiteUrl","hasRpId","siteUrl"],"sources":["../../src/server/passkey.ts"],"sourcesContent":["/**\n * Server-side WebAuthn ceremony logic for passkey authentication.\n *\n * Handles the four phases of the WebAuthn flow:\n * 1. registerOptions — generate PublicKeyCredentialCreationOptions\n * 2. registerVerify — verify attestation and store credential\n * 3. authOptions — generate PublicKeyCredentialRequestOptions\n * 4. authVerify — verify assertion signature and sign in\n *\n * Uses `@oslojs/webauthn` for attestation/assertion parsing and\n * `@oslojs/crypto` for signature verification.\n *\n * All functions return `Fx<A, ConvexError<any>>` composed via `Fx.chain` pipelines.\n *\n * @module\n */\n\nimport {\n p256,\n verifyECDSASignature,\n decodeSEC1PublicKey,\n decodePKIXECDSASignature,\n} from \"@oslojs/crypto/ecdsa\";\nimport {\n RSAPublicKey,\n decodePKCS1RSAPublicKey,\n sha256ObjectIdentifier,\n verifyRSASSAPKCS1v15Signature,\n} from \"@oslojs/crypto/rsa\";\nimport { sha256 } from \"@oslojs/crypto/sha2\";\nimport {\n encodeBase64urlNoPadding,\n decodeBase64urlIgnorePadding,\n} from \"@oslojs/encoding\";\nimport {\n parseAttestationObject,\n parseClientDataJSON,\n parseAuthenticatorData,\n createAssertionSignatureMessage,\n ClientDataType,\n coseAlgorithmES256,\n coseAlgorithmRS256,\n COSEKeyType,\n} from \"@oslojs/webauthn\";\nimport type { Fx as FxType } from \"@robelest/fx\";\nimport { Fx } from \"@robelest/fx\";\nimport { Cv } from \"@robelest/fx/convex\";\nimport type { ConvexError } from \"convex/values\";\n\nimport { authDb } from \"./db\";\nimport { userIdFromIdentitySubject } from \"./identity\";\nimport { callSignIn, callVerifier } from \"./mutations/index\";\nimport { callVerifierSignature } from \"./mutations/signature\";\nimport { PasskeyProviderConfig, GenericActionCtxWithAuthConfig } from \"./types\";\nimport {\n AuthDataModel,\n SessionInfo,\n queryUserById,\n queryUserByVerifiedEmail,\n queryPasskeysByUserId,\n queryPasskeyByCredentialId,\n queryVerifierById,\n mutatePasskeyInsert,\n mutatePasskeyUpdateCounter,\n mutateVerifierDelete,\n} from \"./types\";\n\ntype EnrichedActionCtx = GenericActionCtxWithAuthConfig<AuthDataModel>;\n\n// ============================================================================\n// Resolve RP options — Fx pipeline with validation\n// ============================================================================\n\n/** Resolved relying party configuration. */\ninterface RpOptions {\n rpName: string;\n rpId: string;\n origin: string | string[];\n attestation: string;\n userVerification: string;\n residentKey: string;\n authenticatorAttachment?: string;\n algorithms: number[];\n challengeExpirationMs: number;\n}\n\n/**\n * Resolve passkey relying party options from provider config and environment.\n *\n * Returns `Fx<RpOptions, ConvexError<any>>` — fails if neither SITE_URL nor rpId\n * is configured.\n */\nconst resolveRpOptionsFx = (\n provider: PasskeyProviderConfig,\n): FxType<RpOptions, ConvexError<any>> => {\n const siteUrl = process.env.SITE_URL;\n const hasSiteUrl = siteUrl !== undefined && siteUrl !== \"\";\n const hasRpId = provider.options.rpId !== undefined;\n\n return Fx.succeed({ siteUrl, hasSiteUrl, hasRpId }).pipe(\n Fx.chain(({ siteUrl, hasSiteUrl, hasRpId }) =>\n !hasSiteUrl && !hasRpId\n ? Cv.fail({\n code: \"PASSKEY_MISSING_CONFIG\",\n message:\n \"Passkey provider requires SITE_URL env var (your frontend URL) \" +\n \"or explicit rpId / origin in the provider config. \" +\n \"CONVEX_SITE_URL cannot be used because WebAuthn RP ID must match the frontend domain.\",\n })\n : Fx.succeed(siteUrl),\n ),\n Fx.map((siteUrl) => {\n const siteHostname = siteUrl ? new URL(siteUrl).hostname : undefined;\n return {\n rpName: provider.options.rpName ?? siteHostname ?? \"localhost\",\n rpId: provider.options.rpId ?? siteHostname ?? \"localhost\",\n origin: provider.options.origin ?? siteUrl ?? \"http://localhost\",\n attestation: provider.options.attestation ?? \"none\",\n userVerification: provider.options.userVerification ?? \"required\",\n residentKey: provider.options.residentKey ?? \"preferred\",\n authenticatorAttachment: provider.options.authenticatorAttachment,\n algorithms: provider.options.algorithms ?? [\n coseAlgorithmES256,\n coseAlgorithmRS256,\n ],\n challengeExpirationMs:\n provider.options.challengeExpirationMs ?? 300_000,\n };\n }),\n );\n};\n\n// ============================================================================\n// Composable validators — small functions (A) => Fx<B, ConvexError<any>>\n// ============================================================================\n\n/** Verify client data type matches expected WebAuthn ceremony type. */\nconst verifyClientDataType =\n <T extends { type: ClientDataType }>(\n expectedType: ClientDataType,\n label: string,\n ) =>\n (clientData: T): FxType<T, ConvexError<any>> =>\n clientData.type === expectedType\n ? Fx.succeed(clientData)\n : Cv.fail({\n code: \"PASSKEY_INVALID_CLIENT_DATA\",\n message: `Invalid client data type: expected ${label}`,\n });\n\n/** Verify origin is in the allowed list. */\nconst verifyOrigin =\n (rp: RpOptions) =>\n <T extends { origin: string }>(\n clientData: T,\n ): FxType<T, ConvexError<any>> => {\n const allowed = Array.isArray(rp.origin) ? rp.origin : [rp.origin];\n return allowed.includes(clientData.origin)\n ? Fx.succeed(clientData)\n : Cv.fail({\n code: \"PASSKEY_INVALID_ORIGIN\",\n message: `Invalid origin: ${clientData.origin}, expected one of: ${allowed.join(\", \")}`,\n });\n };\n\n/** Verify the challenge hash matches the stored verifier, then delete verifier. */\nconst verifyAndConsumeChallenge =\n (ctx: EnrichedActionCtx, verifierValue: string) =>\n <T extends { challenge: Uint8Array }>(\n clientData: T,\n ): FxType<T, ConvexError<any>> => {\n const challengeHash = encodeBase64urlNoPadding(\n new Uint8Array(sha256(clientData.challenge)),\n );\n return Fx.from({\n ok: () => queryVerifierById(ctx, verifierValue),\n err: () =>\n Cv.error({\n code: \"PASSKEY_INVALID_CHALLENGE\",\n message: \"Invalid or expired passkey challenge.\",\n }),\n }).pipe(\n Fx.chain((doc) =>\n !doc || doc.signature !== challengeHash\n ? Cv.fail({\n code: \"PASSKEY_INVALID_CHALLENGE\",\n message: \"Invalid or expired passkey challenge.\",\n })\n : Fx.succeed(doc),\n ),\n Fx.chain(() =>\n Fx.from({\n ok: () => mutateVerifierDelete(ctx, verifierValue),\n err: () =>\n Cv.error({\n code: \"PASSKEY_INVALID_CHALLENGE\",\n message: \"Invalid or expired passkey challenge.\",\n }),\n }),\n ),\n Fx.map(() => clientData),\n );\n };\n\n/** Verify RP ID hash matches. */\nconst verifyRpId =\n (rpId: string) =>\n <T extends { verifyRelyingPartyIdHash: (id: string) => boolean }>(\n authData: T,\n ): FxType<T, ConvexError<any>> =>\n authData.verifyRelyingPartyIdHash(rpId)\n ? Fx.succeed(authData)\n : Cv.fail({\n code: \"PASSKEY_RP_MISMATCH\",\n message: \"Relying party ID mismatch.\",\n });\n\n/** Verify user presence and (optionally) user verification flags. */\nconst verifyUserFlags =\n (rp: RpOptions) =>\n <T extends { userPresent: boolean; userVerified: boolean }>(\n authData: T,\n ): FxType<T, ConvexError<any>> =>\n !authData.userPresent\n ? Cv.fail({\n code: \"PASSKEY_USER_PRESENCE\",\n message: \"User presence flag not set.\",\n })\n : rp.userVerification === \"required\" && !authData.userVerified\n ? Cv.fail({\n code: \"PASSKEY_USER_VERIFICATION\",\n message: \"User verification required but not performed.\",\n })\n : Fx.succeed(authData);\n\n// ============================================================================\n// Registration flow\n// ============================================================================\n\n// ============================================================================\n// Authentication flow\n// ============================================================================\n\n// ============================================================================\n// Main dispatch\n// ============================================================================\n\n/** Result type for all passkey flows. */\ntype PasskeyResult =\n | { kind: \"signedIn\"; signedIn: SessionInfo | null }\n | { kind: \"passkeyOptions\"; options: Record<string, any>; verifier: string };\n\nconst PASSKEY_FLOW = {\n registerOptions: \"registerOptions\",\n registerVerify: \"registerVerify\",\n authOptions: \"authOptions\",\n authVerify: \"authVerify\",\n} as const;\n\nconst PASSKEY_FLOWS = [\n PASSKEY_FLOW.registerOptions,\n PASSKEY_FLOW.registerVerify,\n PASSKEY_FLOW.authOptions,\n PASSKEY_FLOW.authVerify,\n] as const;\n\ntype PasskeyDispatch =\n | { flow: typeof PASSKEY_FLOW.registerOptions }\n | { flow: typeof PASSKEY_FLOW.registerVerify }\n | { flow: typeof PASSKEY_FLOW.authOptions }\n | { flow: typeof PASSKEY_FLOW.authVerify };\n\nconst resolvePasskeyDispatchFx = (\n params: Record<string, unknown>,\n): FxType<PasskeyDispatch, ConvexError<any>> => {\n const flow = params.flow;\n return typeof flow === \"string\" && PASSKEY_FLOWS.includes(flow as never)\n ? Fx.succeed({ flow: flow as (typeof PASSKEY_FLOWS)[number] })\n : Cv.fail({\n code: \"PASSKEY_MISSING_FLOW\",\n message:\n \"Missing `flow` parameter. Expected one of: registerOptions, registerVerify, authOptions, authVerify\",\n });\n};\n\nconst requirePasskeyVerifierFx = (\n verifier: string | undefined,\n): FxType<string, ConvexError<any>> =>\n verifier != null\n ? Fx.succeed(verifier)\n : Cv.fail({\n code: \"PASSKEY_MISSING_VERIFIER\",\n message: \"Missing verifier for passkey operation.\",\n });\n\n/**\n * Main passkey handler dispatched from signIn.ts.\n *\n * Routes to the appropriate phase based on `params.flow` via `dispatchFx`.\n */\nexport function handlePasskeyFx(\n ctx: EnrichedActionCtx,\n provider: PasskeyProviderConfig,\n args: {\n params?: Record<string, any>;\n verifier?: string;\n },\n): FxType<PasskeyResult, ConvexError<any>> {\n const params = (args.params ?? {}) as Record<string, any>;\n\n return resolvePasskeyDispatchFx(params).pipe(\n Fx.chain((dispatch) => {\n const flowFx: FxType<PasskeyResult, ConvexError<any>> = Fx.match(\n dispatch,\n ).on(\"flow\", {\n registerOptions: (_) =>\n Fx.zip(\n Fx.from({\n ok: () => ctx.auth.getUserIdentity(),\n err: () =>\n Cv.error({\n code: \"PASSKEY_AUTH_REQUIRED\",\n message: \"Sign in first, then add a passkey to your account.\",\n }),\n }).pipe(\n Fx.chain((id) =>\n id === null\n ? Cv.fail({\n code: \"PASSKEY_AUTH_REQUIRED\",\n message:\n \"Sign in first, then add a passkey to your account.\",\n })\n : Fx.succeed(userIdFromIdentitySubject(id.subject)),\n ),\n ),\n resolveRpOptionsFx(provider),\n ).pipe(\n Fx.chain(([userId, rp]) => {\n const challenge = new Uint8Array(32);\n crypto.getRandomValues(challenge);\n const challengeHash = encodeBase64urlNoPadding(\n new Uint8Array(sha256(challenge)),\n );\n\n return Fx.from({\n ok: async () => {\n const verifier = await callVerifier(ctx);\n await callVerifierSignature(ctx, {\n verifier,\n signature: challengeHash,\n });\n\n const user = await queryUserById(ctx, userId);\n const userName = params.userName ?? user?.email ?? \"user\";\n const userDisplayName =\n params.userDisplayName ?? user?.name ?? userName;\n\n const existing = await queryPasskeysByUserId(ctx, userId);\n const excludeCredentials = existing.map((pk) => ({\n id: pk.credentialId,\n transports: pk.transports,\n }));\n\n const userHandle = encodeBase64urlNoPadding(\n new TextEncoder().encode(userId),\n );\n\n const options = {\n rp: { name: rp.rpName, id: rp.rpId },\n user: {\n id: userHandle,\n name: userName,\n displayName: userDisplayName,\n },\n challenge: encodeBase64urlNoPadding(challenge),\n pubKeyCredParams: rp.algorithms.map((alg) => ({\n type: \"public-key\" as const,\n alg,\n })),\n timeout: rp.challengeExpirationMs,\n attestation: rp.attestation,\n authenticatorSelection: {\n residentKey: rp.residentKey,\n requireResidentKey: rp.residentKey === \"required\",\n userVerification: rp.userVerification,\n ...(rp.authenticatorAttachment\n ? {\n authenticatorAttachment: rp.authenticatorAttachment,\n }\n : {}),\n },\n excludeCredentials,\n };\n\n return {\n kind: \"passkeyOptions\" as const,\n options,\n verifier,\n };\n },\n err: () =>\n Cv.error({\n code: \"INTERNAL_ERROR\",\n message: \"An unexpected error occurred.\",\n }),\n });\n }),\n ),\n registerVerify: (_) =>\n Fx.zip(\n Fx.from({\n ok: () => ctx.auth.getUserIdentity(),\n err: () =>\n Cv.error({\n code: \"PASSKEY_AUTH_REQUIRED\",\n message: \"Sign in first, then add a passkey to your account.\",\n }),\n }).pipe(\n Fx.chain((id) =>\n id === null\n ? Cv.fail({\n code: \"PASSKEY_AUTH_REQUIRED\",\n message:\n \"Sign in first, then add a passkey to your account.\",\n })\n : Fx.succeed(userIdFromIdentitySubject(id.subject)),\n ),\n ),\n resolveRpOptionsFx(provider),\n ).pipe(\n Fx.chain(([userId, rp]) =>\n requirePasskeyVerifierFx(args.verifier).pipe(\n Fx.chain((verifier) => {\n const clientDataJSON = decodeBase64urlIgnorePadding(\n params.clientDataJSON,\n );\n const clientData = parseClientDataJSON(clientDataJSON);\n\n const verifiedClientDataFx = Fx.succeed(clientData).pipe(\n Fx.chain(\n verifyClientDataType(\n ClientDataType.Create,\n \"webauthn.create\",\n ),\n ),\n Fx.chain(verifyOrigin(rp)),\n Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),\n Fx.map(() => {\n const attestationObjectBytes =\n decodeBase64urlIgnorePadding(params.attestationObject);\n const attestation = parseAttestationObject(\n attestationObjectBytes,\n );\n return attestation.authenticatorData;\n }),\n );\n\n return verifiedClientDataFx.pipe(\n Fx.chain(verifyRpId(rp.rpId)),\n Fx.chain(verifyUserFlags(rp)),\n Fx.chain((authData) => {\n if (authData.credential == null) {\n return Cv.fail({\n code: \"PASSKEY_NO_CREDENTIAL\",\n message: \"No credential in attestation.\",\n });\n }\n return Fx.succeed({\n authData,\n credential: authData.credential,\n });\n }),\n Fx.chain(({ authData, credential }) => {\n const credentialId = encodeBase64urlNoPadding(\n credential.id,\n );\n const publicKey = credential.publicKey;\n\n let algorithm: number;\n if (publicKey.isAlgorithmDefined()) {\n algorithm = publicKey.algorithm();\n } else {\n const keyType = publicKey.type();\n algorithm =\n keyType === COSEKeyType.EC2\n ? coseAlgorithmES256\n : keyType === COSEKeyType.RSA\n ? coseAlgorithmRS256\n : coseAlgorithmES256;\n }\n\n const handlers: Record<\n number,\n (() => FxType<Uint8Array, ConvexError<any>>) | undefined\n > = {\n [coseAlgorithmES256]: () => {\n const ec2 = publicKey.ec2();\n const xBytes = new Uint8Array(32);\n let vx = ec2.x;\n for (let i = 31; i >= 0; i--) {\n xBytes[i] = Number(vx & 0xffn);\n vx >>= 8n;\n }\n const yBytes = new Uint8Array(32);\n let vy = ec2.y;\n for (let i = 31; i >= 0; i--) {\n yBytes[i] = Number(vy & 0xffn);\n vy >>= 8n;\n }\n const bytes = new Uint8Array(65);\n bytes[0] = 0x04;\n bytes.set(xBytes, 1);\n bytes.set(yBytes, 33);\n return Fx.succeed(bytes);\n },\n [coseAlgorithmRS256]: () => {\n const rsa = publicKey.rsa();\n const rsaPubKey = new RSAPublicKey(rsa.n, rsa.e);\n return Fx.succeed(rsaPubKey.encodePKCS1());\n },\n };\n\n const handler = handlers[algorithm];\n return (\n handler\n ? handler()\n : Cv.fail({\n code: \"PASSKEY_UNSUPPORTED_ALGORITHM\",\n message: `Unsupported algorithm: ${algorithm}`,\n })\n ).pipe(\n Fx.chain((publicKeyBytes) =>\n Fx.from({\n ok: async () => {\n const deviceType =\n params.deviceType ?? \"single-device\";\n const backedUp = params.backedUp ?? false;\n\n const db = authDb(ctx, ctx.auth.config);\n await db.accounts.create({\n userId,\n provider: provider.id,\n providerAccountId: credentialId,\n });\n\n await mutatePasskeyInsert(ctx, {\n userId,\n credentialId,\n publicKey: publicKeyBytes.buffer.slice(\n publicKeyBytes.byteOffset,\n publicKeyBytes.byteOffset +\n publicKeyBytes.byteLength,\n ),\n algorithm,\n counter: authData.signatureCounter,\n transports: params.transports,\n deviceType,\n backedUp,\n name: params.passkeyName,\n createdAt: Date.now(),\n });\n\n const signInResult = await callSignIn(ctx, {\n userId,\n generateTokens: true,\n });\n\n return {\n kind: \"signedIn\" as const,\n signedIn: signInResult,\n };\n },\n err: () =>\n Cv.error({\n code: \"INTERNAL_ERROR\",\n message: \"An unexpected error occurred.\",\n }),\n }),\n ),\n );\n }),\n );\n }),\n ),\n ),\n ),\n authOptions: (_) =>\n resolveRpOptionsFx(provider).pipe(\n Fx.chain((rp) => {\n const challenge = new Uint8Array(32);\n crypto.getRandomValues(challenge);\n const challengeHash = encodeBase64urlNoPadding(\n new Uint8Array(sha256(challenge)),\n );\n\n return Fx.from({\n ok: async () => {\n const verifier = await callVerifier(ctx);\n await callVerifierSignature(ctx, {\n verifier,\n signature: challengeHash,\n });\n\n let allowCredentials:\n | Array<{\n type: string;\n id: string;\n transports?: string[];\n }>\n | undefined;\n if (params.email) {\n const user = await queryUserByVerifiedEmail(\n ctx,\n params.email,\n );\n if (user) {\n const passkeys = await queryPasskeysByUserId(\n ctx,\n user._id,\n );\n if (passkeys.length > 0) {\n allowCredentials = passkeys.map((pk) => ({\n type: \"public-key\",\n id: pk.credentialId,\n transports: pk.transports,\n }));\n }\n }\n }\n\n const options: Record<string, any> = {\n challenge: encodeBase64urlNoPadding(challenge),\n timeout: rp.challengeExpirationMs,\n rpId: rp.rpId,\n userVerification: rp.userVerification,\n };\n\n if (allowCredentials) {\n options.allowCredentials = allowCredentials;\n }\n\n return {\n kind: \"passkeyOptions\" as const,\n options,\n verifier,\n };\n },\n err: () =>\n Cv.error({\n code: \"INTERNAL_ERROR\",\n message: \"An unexpected error occurred.\",\n }),\n });\n }),\n ),\n authVerify: (_) =>\n Fx.zip(\n resolveRpOptionsFx(provider),\n requirePasskeyVerifierFx(args.verifier),\n ).pipe(\n Fx.chain(([rp, verifier]) => {\n const clientDataJSON = decodeBase64urlIgnorePadding(\n params.clientDataJSON,\n );\n const clientData = parseClientDataJSON(clientDataJSON);\n\n const verifiedClientDataFx = Fx.succeed(clientData).pipe(\n Fx.chain(\n verifyClientDataType(ClientDataType.Get, \"webauthn.get\"),\n ),\n Fx.chain(verifyOrigin(rp)),\n Fx.chain(verifyAndConsumeChallenge(ctx, verifier)),\n Fx.chain(() =>\n params.credentialId != null\n ? Fx.succeed(params.credentialId as string)\n : Cv.fail({\n code: \"PASSKEY_UNKNOWN_CREDENTIAL\",\n message: \"Missing credential ID\",\n }),\n ),\n );\n\n return verifiedClientDataFx.pipe(\n Fx.chain((credentialId) =>\n Fx.from({\n ok: () => queryPasskeyByCredentialId(ctx, credentialId),\n err: () =>\n Cv.error({\n code: \"PASSKEY_UNKNOWN_CREDENTIAL\",\n message: \"Unknown passkey credential.\",\n }),\n }).pipe(\n Fx.chain((passkey) =>\n passkey\n ? Fx.succeed(passkey)\n : Cv.fail({\n code: \"PASSKEY_UNKNOWN_CREDENTIAL\",\n message: \"Unknown credential\",\n }),\n ),\n ),\n ),\n Fx.chain((passkey) => {\n const authenticatorDataBytes = decodeBase64urlIgnorePadding(\n params.authenticatorData,\n );\n const authenticatorData = parseAuthenticatorData(\n authenticatorDataBytes,\n );\n\n const signature = decodeBase64urlIgnorePadding(\n params.signature,\n );\n const signatureMessage = createAssertionSignatureMessage(\n authenticatorDataBytes,\n clientDataJSON,\n );\n const messageHash = sha256(signatureMessage);\n\n const checkedAuthenticatorFx = Fx.succeed(\n authenticatorData,\n ).pipe(\n Fx.chain(verifyRpId(rp.rpId)),\n Fx.chain(verifyUserFlags(rp)),\n );\n\n const signatureVerifiedFx = checkedAuthenticatorFx.pipe(\n Fx.chain(() => {\n const storedPublicKeyBytes = new Uint8Array(\n passkey.publicKey,\n );\n const algorithmHandlers: Record<\n number,\n (() => FxType<void, ConvexError<any>>) | undefined\n > = {\n [coseAlgorithmES256]: () => {\n const ecPublicKey = decodeSEC1PublicKey(\n p256,\n storedPublicKeyBytes,\n );\n const ecdsaSignature =\n decodePKIXECDSASignature(signature);\n const valid = verifyECDSASignature(\n ecPublicKey,\n messageHash,\n ecdsaSignature,\n );\n return valid\n ? Fx.succeed(undefined as void)\n : Cv.fail({\n code: \"PASSKEY_INVALID_SIGNATURE\",\n message: \"Invalid passkey signature.\",\n });\n },\n [coseAlgorithmRS256]: () => {\n const rsaPublicKey =\n decodePKCS1RSAPublicKey(storedPublicKeyBytes);\n const valid = verifyRSASSAPKCS1v15Signature(\n rsaPublicKey,\n sha256ObjectIdentifier,\n messageHash,\n signature,\n );\n return valid\n ? Fx.succeed(undefined as void)\n : Cv.fail({\n code: \"PASSKEY_INVALID_SIGNATURE\",\n message: \"Invalid passkey signature.\",\n });\n },\n };\n\n const handler = algorithmHandlers[passkey.algorithm];\n return handler\n ? handler()\n : Cv.fail({\n code: \"PASSKEY_UNSUPPORTED_ALGORITHM\",\n message: `Unsupported algorithm: ${passkey.algorithm}`,\n });\n }),\n );\n\n const counterValidatedFx = signatureVerifiedFx.pipe(\n Fx.chain(() =>\n passkey.counter !== 0 &&\n authenticatorData.signatureCounter !== 0 &&\n authenticatorData.signatureCounter <= passkey.counter\n ? Cv.fail({\n code: \"PASSKEY_COUNTER_ERROR\",\n message:\n \"Authenticator counter did not increase — possible credential cloning detected.\",\n })\n : Fx.succeed(authenticatorData),\n ),\n );\n\n return counterValidatedFx.pipe(\n Fx.chain(() =>\n Fx.from({\n ok: async () => {\n await mutatePasskeyUpdateCounter(\n ctx,\n passkey._id,\n authenticatorData.signatureCounter,\n Date.now(),\n );\n\n const signInResult = await callSignIn(ctx, {\n userId: passkey.userId,\n generateTokens: true,\n });\n\n return {\n kind: \"signedIn\" as const,\n signedIn: signInResult,\n };\n },\n err: () =>\n Cv.error({\n code: \"INTERNAL_ERROR\",\n message: \"An unexpected error occurred.\",\n }),\n }),\n ),\n );\n }),\n );\n }),\n ),\n });\n return flowFx;\n }),\n );\n}\n"],"mappings":"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"}
|
package/dist/server/redirects.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import { AuthError } from "./authError.js";
|
|
2
1
|
import { requireEnv } from "./utils.js";
|
|
2
|
+
import { Cv } from "@robelest/fx/convex";
|
|
3
3
|
|
|
4
4
|
//#region src/server/redirects.ts
|
|
5
5
|
/** @internal */
|
|
6
6
|
async function redirectAbsoluteUrl(config, params) {
|
|
7
7
|
if (params.redirectTo === void 0) return requireEnv("SITE_URL").replace(/\/$/, "");
|
|
8
|
-
if (typeof params.redirectTo !== "string") throw
|
|
8
|
+
if (typeof params.redirectTo !== "string") throw Cv.error({
|
|
9
|
+
code: "INVALID_REDIRECT",
|
|
10
|
+
message: `Expected \`redirectTo\` to be a string, got ${params.redirectTo}`
|
|
11
|
+
});
|
|
9
12
|
const redirectCallback = config.callbacks?.redirect ?? defaultRedirectCallback;
|
|
10
13
|
try {
|
|
11
14
|
return await redirectCallback({ redirectTo: params.redirectTo });
|
|
12
15
|
} catch {
|
|
13
|
-
throw
|
|
16
|
+
throw Cv.error({
|
|
17
|
+
code: "INTERNAL_ERROR",
|
|
18
|
+
message: "An unexpected error occurred."
|
|
19
|
+
});
|
|
14
20
|
}
|
|
15
21
|
}
|
|
16
22
|
async function defaultRedirectCallback({ redirectTo }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirects.js","names":[],"sources":["../../src/server/redirects.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"redirects.js","names":[],"sources":["../../src/server/redirects.ts"],"sourcesContent":["import { Cv } from \"@robelest/fx/convex\";\n\nimport { ConvexAuthMaterializedConfig } from \"./types\";\nimport { requireEnv } from \"./utils\";\n\n/** @internal */\nexport async function redirectAbsoluteUrl(\n config: ConvexAuthMaterializedConfig,\n params: { redirectTo: unknown },\n) {\n if (params.redirectTo === undefined) {\n return requireEnv(\"SITE_URL\").replace(/\\/$/, \"\");\n }\n if (typeof params.redirectTo !== \"string\") {\n throw Cv.error({\n code: \"INVALID_REDIRECT\",\n message: `Expected \\`redirectTo\\` to be a string, got ${params.redirectTo as any}`,\n });\n }\n const redirectCallback =\n config.callbacks?.redirect ?? defaultRedirectCallback;\n try {\n return await redirectCallback({ redirectTo: params.redirectTo });\n } catch {\n throw Cv.error({\n code: \"INTERNAL_ERROR\",\n message: \"An unexpected error occurred.\",\n });\n }\n}\n\nasync function defaultRedirectCallback({ redirectTo }: { redirectTo: string }) {\n // Resolve relative paths against SITE_URL; absolute URLs are passed through\n // as-is. The developer is trusted to provide valid redirect targets.\n if (redirectTo.startsWith(\"?\") || redirectTo.startsWith(\"/\")) {\n return `${requireEnv(\"SITE_URL\").replace(/\\/$/, \"\")}${redirectTo}`;\n }\n return redirectTo;\n}\n\n// Temporary work-around because Convex doesn't support\n// schemes other than http and https.\n/** @internal */\nexport function setURLSearchParam(\n absoluteUrl: string,\n param: string,\n value: string,\n) {\n const pattern = /([^:]+):(.*)/;\n const [, scheme, rest] = absoluteUrl.match(pattern)!;\n const hasNoDomain = /^\\/\\/(?:\\/|$|\\?)/.test(rest);\n const startsWithPath = hasNoDomain && rest.startsWith(\"///\");\n const url = new URL(\n `http:${hasNoDomain ? \"//googblibok\" + rest.slice(2) : rest}`,\n );\n url.searchParams.set(param, value);\n const [, , withParam] = url.toString().match(pattern)!;\n return `${scheme}:${hasNoDomain ? (startsWithPath ? \"/\" : \"\") + \"//\" + withParam.slice(13) : withParam}`;\n}\n"],"mappings":";;;;;AAMA,eAAsB,oBACpB,QACA,QACA;AACA,KAAI,OAAO,eAAe,OACxB,QAAO,WAAW,WAAW,CAAC,QAAQ,OAAO,GAAG;AAElD,KAAI,OAAO,OAAO,eAAe,SAC/B,OAAM,GAAG,MAAM;EACb,MAAM;EACN,SAAS,+CAA+C,OAAO;EAChE,CAAC;CAEJ,MAAM,mBACJ,OAAO,WAAW,YAAY;AAChC,KAAI;AACF,SAAO,MAAM,iBAAiB,EAAE,YAAY,OAAO,YAAY,CAAC;SAC1D;AACN,QAAM,GAAG,MAAM;GACb,MAAM;GACN,SAAS;GACV,CAAC;;;AAIN,eAAe,wBAAwB,EAAE,cAAsC;AAG7E,KAAI,WAAW,WAAW,IAAI,IAAI,WAAW,WAAW,IAAI,CAC1D,QAAO,GAAG,WAAW,WAAW,CAAC,QAAQ,OAAO,GAAG,GAAG;AAExD,QAAO;;;AAMT,SAAgB,kBACd,aACA,OACA,OACA;CACA,MAAM,UAAU;CAChB,MAAM,GAAG,QAAQ,QAAQ,YAAY,MAAM,QAAQ;CACnD,MAAM,cAAc,mBAAmB,KAAK,KAAK;CACjD,MAAM,iBAAiB,eAAe,KAAK,WAAW,MAAM;CAC5D,MAAM,MAAM,IAAI,IACd,QAAQ,cAAc,iBAAiB,KAAK,MAAM,EAAE,GAAG,OACxD;AACD,KAAI,aAAa,IAAI,OAAO,MAAM;CAClC,MAAM,KAAK,aAAa,IAAI,UAAU,CAAC,MAAM,QAAQ;AACrD,QAAO,GAAG,OAAO,GAAG,eAAe,iBAAiB,MAAM,MAAM,OAAO,UAAU,MAAM,GAAG,GAAG"}
|
package/dist/server/refresh.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AuthError } from "./authError.js";
|
|
2
1
|
import { LOG_LEVELS, REFRESH_TOKEN_DIVIDER, logWithLevel, maybeRedact } from "./utils.js";
|
|
3
2
|
import { authDb } from "./db.js";
|
|
4
3
|
import { Fx } from "@robelest/fx";
|
|
4
|
+
import { Cv } from "@robelest/fx/convex";
|
|
5
5
|
|
|
6
6
|
//#region src/server/refresh.ts
|
|
7
7
|
const DEFAULT_SESSION_INACTIVE_DURATION_MS = 1e3 * 60 * 60 * 24 * 30;
|
|
@@ -26,8 +26,14 @@ async function createRefreshToken(ctx, config, sessionId, parentRefreshTokenId)
|
|
|
26
26
|
const parseRefreshToken = (refreshToken) => {
|
|
27
27
|
const [refreshTokenId, sessionId] = refreshToken.split(REFRESH_TOKEN_DIVIDER);
|
|
28
28
|
const msg = `Can't parse refresh token: ${maybeRedact(refreshToken)}`;
|
|
29
|
-
return (refreshTokenId != null ? Fx.succeed(refreshTokenId) :
|
|
30
|
-
|
|
29
|
+
return (refreshTokenId != null ? Fx.succeed(refreshTokenId) : Cv.fail({
|
|
30
|
+
code: "INVALID_REFRESH_TOKEN",
|
|
31
|
+
message: msg
|
|
32
|
+
})).pipe(Fx.chain((rtId) => {
|
|
33
|
+
return (sessionId != null ? Fx.succeed(sessionId) : Cv.fail({
|
|
34
|
+
code: "INVALID_REFRESH_TOKEN",
|
|
35
|
+
message: msg
|
|
36
|
+
})).pipe(Fx.map((sId) => ({
|
|
31
37
|
refreshTokenId: rtId,
|
|
32
38
|
sessionId: sId
|
|
33
39
|
})));
|
|
@@ -56,10 +62,7 @@ async function invalidateRefreshTokensInSubtree(ctx, refreshToken, config) {
|
|
|
56
62
|
}
|
|
57
63
|
frontier = nextFrontier;
|
|
58
64
|
}
|
|
59
|
-
await Fx.run(Fx.each(tokensToInvalidate, (token) => token.firstUsedTime === void 0 || token.firstUsedTime > Date.now() - REFRESH_TOKEN_REUSE_WINDOW_MS ? Fx.
|
|
60
|
-
ok: () => db.refreshTokens.patch(token._id, { firstUsedTime: Date.now() - REFRESH_TOKEN_REUSE_WINDOW_MS }),
|
|
61
|
-
err: (e) => e
|
|
62
|
-
}) : Fx.unit));
|
|
65
|
+
await Fx.run(Fx.each(tokensToInvalidate, (token) => token.firstUsedTime === void 0 || token.firstUsedTime > Date.now() - REFRESH_TOKEN_REUSE_WINDOW_MS ? Fx.promise(() => db.refreshTokens.patch(token._id, { firstUsedTime: Date.now() - REFRESH_TOKEN_REUSE_WINDOW_MS })) : Fx.unit));
|
|
63
66
|
return tokensToInvalidate;
|
|
64
67
|
}
|
|
65
68
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh.js","names":[],"sources":["../../src/server/refresh.ts"],"sourcesContent":["import { Fx } from \"@robelest/fx\";\nimport {
|
|
1
|
+
{"version":3,"file":"refresh.js","names":[],"sources":["../../src/server/refresh.ts"],"sourcesContent":["import { Fx } from \"@robelest/fx\";\nimport { Cv } from \"@robelest/fx/convex\";\nimport { ConvexError, GenericId } from \"convex/values\";\n\nimport { authDb } from \"./db\";\nimport { Doc, MutationCtx } from \"./types\";\nimport { ConvexAuthConfig } from \"./types\";\nimport {\n LOG_LEVELS,\n REFRESH_TOKEN_DIVIDER,\n logWithLevel,\n maybeRedact,\n} from \"./utils\";\n\nconst DEFAULT_SESSION_INACTIVE_DURATION_MS = 1000 * 60 * 60 * 24 * 30; // 30 days\n/** @internal */\nexport const REFRESH_TOKEN_REUSE_WINDOW_MS = 10 * 1000; // 10 seconds\n\n// ---------------------------------------------------------------------------\n// Refresh token CRUD\n// ---------------------------------------------------------------------------\n\n/**\n * Create a new refresh token for the given session.\n */\n/** @internal */\nexport async function createRefreshToken(\n ctx: MutationCtx,\n config: ConvexAuthConfig,\n sessionId: GenericId<\"Session\">,\n parentRefreshTokenId: GenericId<\"RefreshToken\"> | null,\n): Promise<GenericId<\"RefreshToken\">> {\n const expirationTime =\n Date.now() +\n (config.session?.inactiveDurationMs ??\n (process.env.AUTH_SESSION_INACTIVE_DURATION_MS !== undefined\n ? Number(process.env.AUTH_SESSION_INACTIVE_DURATION_MS)\n : undefined) ??\n DEFAULT_SESSION_INACTIVE_DURATION_MS);\n\n return authDb(ctx, config).refreshTokens.create({\n sessionId,\n expirationTime,\n parentRefreshTokenId: parentRefreshTokenId ?? undefined,\n }) as Promise<GenericId<\"RefreshToken\">>;\n}\n\n/**\n * Parse a compound refresh token string into its constituent IDs.\n */\n/** @internal */\nexport const parseRefreshToken = (\n refreshToken: string,\n): Fx<\n {\n refreshTokenId: GenericId<\"RefreshToken\">;\n sessionId: GenericId<\"Session\">;\n },\n ConvexError<any>\n> => {\n const [refreshTokenId, sessionId] = refreshToken.split(REFRESH_TOKEN_DIVIDER);\n const msg = `Can't parse refresh token: ${maybeRedact(refreshToken)}`;\n const refreshTokenIdFx: Fx<string, ConvexError<any>> = refreshTokenId != null\n ? Fx.succeed(refreshTokenId)\n : Cv.fail({ code: \"INVALID_REFRESH_TOKEN\", message: msg });\n\n return refreshTokenIdFx.pipe(\n Fx.chain((rtId) => {\n const sessionIdFx: Fx<string, ConvexError<any>> = sessionId != null\n ? Fx.succeed(sessionId)\n : Cv.fail({ code: \"INVALID_REFRESH_TOKEN\", message: msg });\n return sessionIdFx.pipe(\n Fx.map((sId) => ({\n refreshTokenId: rtId as GenericId<\"RefreshToken\">,\n sessionId: sId as GenericId<\"Session\">,\n })),\n );\n }),\n );\n};\n\n/**\n * Mark all refresh tokens descending from the given refresh token as invalid\n * immediately. Used when we detect token reuse — revoke the entire tree.\n */\n/** @internal */\nexport async function invalidateRefreshTokensInSubtree(\n ctx: MutationCtx,\n refreshToken: Doc<\"RefreshToken\">,\n config: ConvexAuthConfig,\n) {\n const db = authDb(ctx, config);\n const tokensToInvalidate = [refreshToken];\n const visited = new Set<GenericId<\"RefreshToken\">>([refreshToken._id]);\n let frontier: GenericId<\"RefreshToken\">[] = [refreshToken._id];\n while (frontier.length > 0) {\n const nextFrontier: GenericId<\"RefreshToken\">[] = [];\n for (const currentTokenId of frontier) {\n const children = (await db.refreshTokens.getChildren(\n refreshToken.sessionId,\n currentTokenId,\n )) as Doc<\"RefreshToken\">[];\n for (const child of children) {\n if (visited.has(child._id)) continue;\n visited.add(child._id);\n tokensToInvalidate.push(child);\n nextFrontier.push(child._id);\n }\n }\n frontier = nextFrontier;\n }\n await Fx.run(\n Fx.each(tokensToInvalidate, (token) =>\n token.firstUsedTime === undefined ||\n token.firstUsedTime > Date.now() - REFRESH_TOKEN_REUSE_WINDOW_MS\n ? Fx.promise(() =>\n db.refreshTokens.patch(token._id, {\n firstUsedTime: Date.now() - REFRESH_TOKEN_REUSE_WINDOW_MS,\n }),\n )\n : Fx.unit,\n ),\n );\n return tokensToInvalidate;\n}\n\n// ---------------------------------------------------------------------------\n// Validation pipeline — the core of refresh token handling\n// ---------------------------------------------------------------------------\n\n/**\n * Validate a refresh token and its associated session.\n *\n * Returns `null` on any validation failure (matching original semantics).\n * Each validation step is a small composable function chained with `Fx.chain`.\n * On failure, the error message is logged and the pipeline folds to `null`.\n */\n/** @internal */\nexport const refreshTokenIfValid = (\n ctx: MutationCtx,\n refreshTokenId: string,\n tokenSessionId: string,\n config: ConvexAuthConfig,\n): Fx<\n { session: Doc<\"Session\">; refreshTokenDoc: Doc<\"RefreshToken\"> } | null,\n never\n> => {\n const db = authDb(ctx, config);\n\n const fetchDoc = <T>(\n promise: () => Promise<T | null>,\n failMsg: string,\n ): Fx<T | null, never> =>\n Fx.from({ ok: promise, err: () => failMsg }).pipe(\n Fx.recover((msg) => {\n logWithLevel(LOG_LEVELS.ERROR, msg);\n return Fx.succeed(null as T | null);\n }),\n );\n\n // The entire validation is a single pipeline:\n // fetch token → not null → not expired → session matches → fetch session → not null → not expired → combine\n return fetchDoc(\n () =>\n db.refreshTokens.getById(\n refreshTokenId as GenericId<\"RefreshToken\">,\n ) as Promise<Doc<\"RefreshToken\"> | null>,\n \"Invalid refresh token format\",\n )\n .pipe(\n Fx.chain((doc) =>\n doc !== null ? Fx.succeed(doc) : Fx.fail(\"Invalid refresh token\"),\n ),\n Fx.chain((doc) =>\n doc.expirationTime >= Date.now()\n ? Fx.succeed(doc)\n : Fx.fail(\"Expired refresh token\"),\n ),\n Fx.chain((doc) =>\n doc.sessionId === tokenSessionId\n ? Fx.succeed(doc)\n : Fx.fail(\"Invalid refresh token session ID\"),\n ),\n )\n .pipe(\n Fx.chain((doc: Doc<\"RefreshToken\">) =>\n fetchDoc(\n () =>\n db.sessions.getById(\n doc.sessionId,\n ) as Promise<Doc<\"Session\"> | null>,\n \"Invalid refresh token session format\",\n ).pipe(\n Fx.chain((session) =>\n session !== null\n ? Fx.succeed(session)\n : Fx.fail(\"Invalid refresh token session\"),\n ),\n Fx.chain((session) =>\n session.expirationTime >= Date.now()\n ? Fx.succeed(session)\n : Fx.fail(\"Expired refresh token session\"),\n ),\n Fx.map((session) => ({\n session,\n refreshTokenDoc: doc,\n })),\n ),\n ),\n Fx.fold({\n ok: (result) => result,\n err: (msg) => {\n logWithLevel(LOG_LEVELS.ERROR, msg);\n return null;\n },\n }),\n );\n};\n"],"mappings":"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"}
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConvexAuthConfig, Doc, SessionInfo } from "./types.js";
|
|
2
2
|
import * as convex_server63 from "convex/server";
|
|
3
|
-
import * as
|
|
3
|
+
import * as convex_values90 from "convex/values";
|
|
4
4
|
|
|
5
5
|
//#region src/server/runtime.d.ts
|
|
6
6
|
/**
|
|
@@ -33,9 +33,9 @@ declare function Auth(config_: ConvexAuthConfig): {
|
|
|
33
33
|
*/
|
|
34
34
|
signIn: convex_server63.RegisteredAction<"public", {
|
|
35
35
|
provider?: string | undefined;
|
|
36
|
-
refreshToken?: string | undefined;
|
|
37
36
|
params?: any;
|
|
38
37
|
verifier?: string | undefined;
|
|
38
|
+
refreshToken?: string | undefined;
|
|
39
39
|
calledBy?: string | undefined;
|
|
40
40
|
}, Promise<SignInActionResult>>;
|
|
41
41
|
/**
|
|
@@ -49,8 +49,8 @@ declare function Auth(config_: ConvexAuthConfig): {
|
|
|
49
49
|
store: convex_server63.RegisteredMutation<"internal", {
|
|
50
50
|
args: {
|
|
51
51
|
sessionId?: string | undefined;
|
|
52
|
-
userId: string;
|
|
53
52
|
type: "signIn";
|
|
53
|
+
userId: string;
|
|
54
54
|
generateTokens: boolean;
|
|
55
55
|
} | {
|
|
56
56
|
type: "signOut";
|
|
@@ -61,9 +61,9 @@ declare function Auth(config_: ConvexAuthConfig): {
|
|
|
61
61
|
provider?: string | undefined;
|
|
62
62
|
verifier?: string | undefined;
|
|
63
63
|
type: "verifyCodeAndSignIn";
|
|
64
|
-
allowExtraProviders: boolean;
|
|
65
|
-
generateTokens: boolean;
|
|
66
64
|
params: any;
|
|
65
|
+
generateTokens: boolean;
|
|
66
|
+
allowExtraProviders: boolean;
|
|
67
67
|
} | {
|
|
68
68
|
type: "verifier";
|
|
69
69
|
} | {
|
|
@@ -78,24 +78,24 @@ declare function Auth(config_: ConvexAuthConfig): {
|
|
|
78
78
|
providerAccountId: string;
|
|
79
79
|
profile: any;
|
|
80
80
|
} | {
|
|
81
|
-
email?: string | undefined;
|
|
82
|
-
accountId?: string | undefined;
|
|
83
81
|
phone?: string | undefined;
|
|
82
|
+
accountId?: string | undefined;
|
|
83
|
+
email?: string | undefined;
|
|
84
84
|
type: "createVerificationCode";
|
|
85
85
|
provider: string;
|
|
86
|
+
allowExtraProviders: boolean;
|
|
86
87
|
code: string;
|
|
87
88
|
expirationTime: number;
|
|
88
|
-
allowExtraProviders: boolean;
|
|
89
89
|
} | {
|
|
90
90
|
shouldLinkViaEmail?: boolean | undefined;
|
|
91
91
|
shouldLinkViaPhone?: boolean | undefined;
|
|
92
92
|
type: "createAccountFromCredentials";
|
|
93
93
|
provider: string;
|
|
94
|
+
profile: any;
|
|
94
95
|
account: {
|
|
95
96
|
secret?: string | undefined;
|
|
96
97
|
id: string;
|
|
97
98
|
};
|
|
98
|
-
profile: any;
|
|
99
99
|
} | {
|
|
100
100
|
type: "retrieveAccountWithCredentials";
|
|
101
101
|
provider: string;
|
|
@@ -112,15 +112,15 @@ declare function Auth(config_: ConvexAuthConfig): {
|
|
|
112
112
|
};
|
|
113
113
|
} | {
|
|
114
114
|
except?: string[] | undefined;
|
|
115
|
-
userId: string;
|
|
116
115
|
type: "invalidateSessions";
|
|
116
|
+
userId: string;
|
|
117
117
|
};
|
|
118
|
-
}, Promise<string | void |
|
|
119
|
-
userId:
|
|
120
|
-
sessionId:
|
|
118
|
+
}, Promise<string | void | {
|
|
119
|
+
userId: convex_values90.GenericId<"User">;
|
|
120
|
+
sessionId: convex_values90.GenericId<"Session">;
|
|
121
121
|
} | (string & {
|
|
122
122
|
__tableName: "AuthVerifier";
|
|
123
|
-
}) | {
|
|
123
|
+
}) | SessionInfo | {
|
|
124
124
|
token: string;
|
|
125
125
|
refreshToken: string;
|
|
126
126
|
} | {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","names":[],"sources":["../../src/server/runtime.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","names":[],"sources":["../../src/server/runtime.ts"],"mappings":";;;;;;;;;AAoJA;;;;;;;;;;;;;;iBAAgB,IAAA,CAAK,OAAA,EAAS,gBAAA"}
|
package/dist/server/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AuthError } from "./authError.js";
|
|
2
|
-
import { LOG_LEVELS, decryptSecret, encryptSecret, generateRandomString, logError, logWithLevel, requireEnv, sha256 } from "./utils.js";
|
|
3
1
|
import { configDefaults, listAvailableProviders } from "./config.js";
|
|
2
|
+
import { LOG_LEVELS, decryptSecret, encryptSecret, generateRandomString, logError, logWithLevel, requireEnv, sha256 } from "./utils.js";
|
|
3
|
+
import { redirectToParamCookie, useRedirectToParam } from "./cookies.js";
|
|
4
4
|
import { callModifyAccount } from "./mutations/account.js";
|
|
5
5
|
import { callInvalidateSessions } from "./mutations/invalidate.js";
|
|
6
6
|
import { enterpriseOidcProviderId, getEnterpriseOidcUrls, isEnterpriseSamlSourceActive, normalizeDomain } from "./enterprise/shared.js";
|
|
@@ -14,15 +14,15 @@ import { storeArgs, storeImpl } from "./mutations/store.js";
|
|
|
14
14
|
import { redirectAbsoluteUrl, setURLSearchParam } from "./redirects.js";
|
|
15
15
|
import { signInImpl } from "./signin.js";
|
|
16
16
|
import { createCoreDomains } from "./core.js";
|
|
17
|
-
import {
|
|
17
|
+
import { getOidcConfig, getPublicOidcConfig, getSamlConfig, upsertProtocolConfig, withOidcSecretState } from "./enterprise/config.js";
|
|
18
18
|
import { createEnterpriseDomain } from "./enterprise/domain.js";
|
|
19
19
|
import { addAuthRoutes, addOpenIdRoutes, convertErrorsToResponse, createHttpAction, createHttpRoute, getCookies } from "./http.js";
|
|
20
20
|
import { createOAuthAuthorizationURL, handleOAuthCallback } from "./oauth.js";
|
|
21
|
-
import { getOidcConfig, getPublicOidcConfig, getSamlConfig, upsertProtocolConfig, withOidcSecretState } from "./enterprise/config.js";
|
|
22
21
|
import { createServiceProviderMetadata, getSamlServiceProviderOptions, parseSamlIdpMetadata } from "./enterprise/saml.js";
|
|
23
22
|
import { parseScimPath } from "./enterprise/scim.js";
|
|
24
23
|
import { addEnterpriseHttpRuntime } from "./enterprise/http.js";
|
|
25
24
|
import { Fx } from "@robelest/fx";
|
|
25
|
+
import { Cv } from "@robelest/fx/convex";
|
|
26
26
|
import { actionGeneric, internalMutationGeneric } from "convex/server";
|
|
27
27
|
import { v } from "convex/values";
|
|
28
28
|
import { serialize } from "cookie";
|
|
@@ -56,7 +56,11 @@ function Auth(config_) {
|
|
|
56
56
|
if (provider === void 0) {
|
|
57
57
|
const detail = `Provider \`${id}\` is not configured, available providers are ${listAvailableProviders(config, allowExtraProviders)}.`;
|
|
58
58
|
logWithLevel(LOG_LEVELS.ERROR, detail);
|
|
59
|
-
throw
|
|
59
|
+
throw Cv.error({
|
|
60
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
61
|
+
message: detail,
|
|
62
|
+
provider: id
|
|
63
|
+
});
|
|
60
64
|
}
|
|
61
65
|
return provider;
|
|
62
66
|
};
|
|
@@ -81,12 +85,18 @@ function Auth(config_) {
|
|
|
81
85
|
const getPolicyFromEnterprise = (enterprise) => normalizeEnterprisePolicy(enterprise.policy);
|
|
82
86
|
const loadEnterpriseOrThrow = async (ctx, enterpriseId) => {
|
|
83
87
|
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId });
|
|
84
|
-
if (!enterprise) throw
|
|
88
|
+
if (!enterprise) throw Cv.error({
|
|
89
|
+
code: "INVALID_PARAMETERS",
|
|
90
|
+
message: enterpriseNotFoundError
|
|
91
|
+
});
|
|
85
92
|
return enterprise;
|
|
86
93
|
};
|
|
87
94
|
const loadActiveEnterpriseOrThrow = async (ctx, enterpriseId) => {
|
|
88
95
|
const enterprise = await loadEnterpriseOrThrow(ctx, enterpriseId);
|
|
89
|
-
if (enterprise.status !== "active") throw
|
|
96
|
+
if (enterprise.status !== "active") throw Cv.error({
|
|
97
|
+
code: "INVALID_PARAMETERS",
|
|
98
|
+
message: "Enterprise connection is not active."
|
|
99
|
+
});
|
|
90
100
|
return enterprise;
|
|
91
101
|
};
|
|
92
102
|
const loadActiveEnterpriseSamlOrThrow = async (ctx, enterpriseId) => {
|
|
@@ -100,9 +110,15 @@ function Auth(config_) {
|
|
|
100
110
|
status: enterprise.status,
|
|
101
111
|
enterprise
|
|
102
112
|
};
|
|
103
|
-
if (!isEnterpriseSamlSourceActive(loaded)) throw
|
|
113
|
+
if (!isEnterpriseSamlSourceActive(loaded)) throw Cv.error({
|
|
114
|
+
code: "INVALID_PARAMETERS",
|
|
115
|
+
message: "Enterprise connection is not active."
|
|
116
|
+
});
|
|
104
117
|
const saml = getSamlConfig(loaded.config);
|
|
105
|
-
if (!saml.idp?.metadataXml) throw
|
|
118
|
+
if (!saml.idp?.metadataXml) throw Cv.error({
|
|
119
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
120
|
+
message: "SAML is not configured for this enterprise."
|
|
121
|
+
});
|
|
106
122
|
return {
|
|
107
123
|
loaded,
|
|
108
124
|
enterprise,
|
|
@@ -112,7 +128,10 @@ function Auth(config_) {
|
|
|
112
128
|
const loadEnterpriseOidcOrThrow = async (ctx, enterpriseId) => {
|
|
113
129
|
const enterprise = await loadActiveEnterpriseOrThrow(ctx, enterpriseId);
|
|
114
130
|
const oidc = await getEnterpriseOidcConfigWithSecret(ctx, enterprise);
|
|
115
|
-
if (oidc.enabled !== true) throw
|
|
131
|
+
if (oidc.enabled !== true) throw Cv.error({
|
|
132
|
+
code: "PROVIDER_NOT_CONFIGURED",
|
|
133
|
+
message: "OIDC is not configured for this enterprise."
|
|
134
|
+
});
|
|
116
135
|
return {
|
|
117
136
|
enterprise,
|
|
118
137
|
oidc
|
|
@@ -164,14 +183,26 @@ function Auth(config_) {
|
|
|
164
183
|
};
|
|
165
184
|
const getEnterpriseScimContext = async (ctx, request) => {
|
|
166
185
|
const authHeader = request.headers.get("Authorization");
|
|
167
|
-
if (!authHeader?.startsWith("Bearer ")) throw
|
|
186
|
+
if (!authHeader?.startsWith("Bearer ")) throw Cv.error({
|
|
187
|
+
code: "MISSING_BEARER_TOKEN",
|
|
188
|
+
message: "Missing or malformed Authorization: Bearer header."
|
|
189
|
+
});
|
|
168
190
|
const token = authHeader.slice(7);
|
|
169
191
|
const scimConfig = await ctx.runQuery(config.component.public.enterpriseScimConfigGetByTokenHash, { tokenHash: await sha256(token) });
|
|
170
|
-
if (!scimConfig || scimConfig.status !== "active") throw
|
|
192
|
+
if (!scimConfig || scimConfig.status !== "active") throw Cv.error({
|
|
193
|
+
code: "INVALID_API_KEY",
|
|
194
|
+
message: "Invalid SCIM token."
|
|
195
|
+
});
|
|
171
196
|
const parsedPath = parseScimPath(new URL(request.url).pathname);
|
|
172
|
-
if (parsedPath.enterpriseId !== scimConfig.enterpriseId) throw
|
|
197
|
+
if (parsedPath.enterpriseId !== scimConfig.enterpriseId) throw Cv.error({
|
|
198
|
+
code: "INVALID_API_KEY",
|
|
199
|
+
message: "SCIM token/tenant mismatch."
|
|
200
|
+
});
|
|
173
201
|
const enterprise = await ctx.runQuery(config.component.public.enterpriseGet, { enterpriseId: scimConfig.enterpriseId });
|
|
174
|
-
if (enterprise === null) throw
|
|
202
|
+
if (enterprise === null) throw Cv.error({
|
|
203
|
+
code: "INVALID_PARAMETERS",
|
|
204
|
+
message: "Enterprise not found."
|
|
205
|
+
});
|
|
175
206
|
return {
|
|
176
207
|
scimConfig,
|
|
177
208
|
enterprise,
|
|
@@ -248,10 +279,17 @@ function Auth(config_) {
|
|
|
248
279
|
if (hasOAuth) addAuthRoutes(http, {
|
|
249
280
|
handleSignIn: convertErrorsToResponse(400, async (ctx, request) => {
|
|
250
281
|
const url = new URL(request.url);
|
|
251
|
-
const
|
|
252
|
-
|
|
282
|
+
const pathParts = url.pathname.split("/");
|
|
283
|
+
const providerId = pathParts[pathParts.length - 1];
|
|
284
|
+
if (providerId === null) throw Cv.error({
|
|
285
|
+
code: "OAUTH_MISSING_PROVIDER",
|
|
286
|
+
message: "Missing OAuth provider ID."
|
|
287
|
+
});
|
|
253
288
|
const verifier = url.searchParams.get("code");
|
|
254
|
-
if (verifier === null) throw
|
|
289
|
+
if (verifier === null) throw Cv.error({
|
|
290
|
+
code: "OAUTH_MISSING_VERIFIER",
|
|
291
|
+
message: "Missing sign-in verifier."
|
|
292
|
+
});
|
|
255
293
|
const oauthConfig = getProviderOrThrow(providerId);
|
|
256
294
|
const { redirect, cookies, signature } = await createOAuthAuthorizationURL(providerId, oauthConfig.provider, oauthConfig);
|
|
257
295
|
await callVerifierSignature(ctx, {
|
|
@@ -269,8 +307,12 @@ function Auth(config_) {
|
|
|
269
307
|
}),
|
|
270
308
|
handleCallback: async (ctx, request) => {
|
|
271
309
|
const url = new URL(request.url);
|
|
272
|
-
const
|
|
273
|
-
|
|
310
|
+
const callbackPathParts = new URL(request.url).pathname.split("/");
|
|
311
|
+
const providerId = callbackPathParts[callbackPathParts.length - 1];
|
|
312
|
+
if (!providerId) throw Cv.error({
|
|
313
|
+
code: "OAUTH_MISSING_PROVIDER",
|
|
314
|
+
message: "Missing OAuth provider ID."
|
|
315
|
+
});
|
|
274
316
|
logWithLevel(LOG_LEVELS.DEBUG, "Handling OAuth callback for provider:", providerId);
|
|
275
317
|
const provider = getProviderOrThrow(providerId);
|
|
276
318
|
const cookies = getCookies(request);
|