@spfn/auth 0.3.0-beta.24 → 0.3.0-beta.26

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/client.d.ts CHANGED
@@ -140,5 +140,54 @@ interface DisableSessionBindingOptions {
140
140
  * the whole reason this helper exists.
141
141
  */
142
142
  declare function disableSessionBinding(api: AuthApi, options?: DisableSessionBindingOptions): Promise<PasskeyResult<DisableSessionBindingValue>>;
143
+ /**
144
+ * What a completed step-up answers with — the sign-in the 202 was standing in for.
145
+ *
146
+ * Deliberately loose about the login fields. Through the Next.js proxy the
147
+ * session is already sealed by the time this resolves and the app reads it from
148
+ * the session cookie; a native client reads `userId` off the body. `keyId` and
149
+ * `challengeHash` are the proxy's business and are not repeated here.
150
+ */
151
+ interface CompleteMfaValue {
152
+ userId?: string;
153
+ keyId: string;
154
+ }
155
+ /**
156
+ * Finish a sign-in that answered `mfaRequired` with a code from the
157
+ * authenticator app.
158
+ *
159
+ * The challenge is the `secret` from that 202 — or, on the web OAuth path, the
160
+ * `?challenge=` the callback handler put on the confirm page's URL. It is single
161
+ * use and lives ten minutes.
162
+ *
163
+ * A wrong code is a rejected promise carrying `MfaVerificationFailedError`, not
164
+ * a result: it is the server declining rather than a ceremony failing, and there
165
+ * is no ceremony here at all. Five wrong ones end the challenge and the person
166
+ * signs in again.
167
+ *
168
+ * @param api - the typed auth client
169
+ * @param challenge - the challenge secret from the 202 or the callback query
170
+ * @param code - the six digits, spaces and dashes and all
171
+ */
172
+ declare function completeMfaWithCode(api: AuthApi, challenge: string, code: string): Promise<CompleteMfaValue>;
173
+ /**
174
+ * Finish the same sign-in with one of the ten written-down recovery codes.
175
+ *
176
+ * Single use, and the count in `mfa/status` drops by one — an app that warns at
177
+ * two remaining reads it from there after this resolves.
178
+ */
179
+ declare function completeMfaWithRecoveryCode(api: AuthApi, challenge: string, recoveryCode: string): Promise<CompleteMfaValue>;
180
+ /**
181
+ * Finish the same sign-in with a passkey the owner marked as a second factor.
182
+ *
183
+ * `options` from the server, `navigator.credentials` in the browser, `verify`
184
+ * back — the shape every ceremony in this file has, and a discriminated union
185
+ * rather than a throw for the same reason: a person who dismisses the system
186
+ * sheet has not hit an application error.
187
+ *
188
+ * The challenge is what names the account, since there is no session yet. A
189
+ * passkey the owner never marked is refused with the same body as a wrong code.
190
+ */
191
+ declare function completeMfaWithPasskey(api: AuthApi, challenge: string): Promise<PasskeyResult<CompleteMfaValue>>;
143
192
 
144
- export { type AuthApi, type DisableSessionBindingOptions, type DisableSessionBindingValue, type EnrollPasskeyOptions, type EnrollPasskeyValue, type PasskeyFailureReason, type PasskeyResult, type RenewSessionValue, type SignInWithPasskeyOptions, type SignInWithPasskeyValue, disableSessionBinding, enrollPasskey, isConditionalMediationAvailable, isPasskeySupported, renewSession, signInWithPasskey };
193
+ export { type AuthApi, type CompleteMfaValue, type DisableSessionBindingOptions, type DisableSessionBindingValue, type EnrollPasskeyOptions, type EnrollPasskeyValue, type PasskeyFailureReason, type PasskeyResult, type RenewSessionValue, type SignInWithPasskeyOptions, type SignInWithPasskeyValue, completeMfaWithCode, completeMfaWithPasskey, completeMfaWithRecoveryCode, disableSessionBinding, enrollPasskey, isConditionalMediationAvailable, isPasskeySupported, renewSession, signInWithPasskey };
package/dist/client.js CHANGED
@@ -88,7 +88,32 @@ async function disableSessionBinding(api, options = {}) {
88
88
  await api.setSessionBinding.call({ body: { mode: "none", response } });
89
89
  return { ok: true, mode: "none" };
90
90
  }
91
+ async function completeMfaWithCode(api, challenge, code) {
92
+ return await api.mfaVerify.call({ body: { challenge, code } });
93
+ }
94
+ async function completeMfaWithRecoveryCode(api, challenge, recoveryCode) {
95
+ return await api.mfaVerify.call({ body: { challenge, recoveryCode } });
96
+ }
97
+ async function completeMfaWithPasskey(api, challenge) {
98
+ if (!isPasskeySupported()) {
99
+ return { ok: false, reason: "unsupported" };
100
+ }
101
+ const optionsJSON = await api.mfaVerifyOptions.call({
102
+ body: { challenge }
103
+ });
104
+ let response;
105
+ try {
106
+ response = await startAuthentication({ optionsJSON });
107
+ } catch (error) {
108
+ return { ok: false, reason: failureReason(error, "no-credential"), error };
109
+ }
110
+ const verified = await api.mfaVerify.call({ body: { challenge, response } });
111
+ return { ok: true, ...verified };
112
+ }
91
113
  export {
114
+ completeMfaWithCode,
115
+ completeMfaWithPasskey,
116
+ completeMfaWithRecoveryCode,
92
117
  disableSessionBinding,
93
118
  enrollPasskey,
94
119
  isConditionalMediationAvailable,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client/passkeys.ts"],"sourcesContent":["/**\n * @spfn/auth/client - Passkeys (WebAuthn)\n *\n * Four browser helpers over the two ceremonies. Each one is `options` from the\n * server, `navigator.credentials` in the browser, `verify` back to the server —\n * and each answers with a discriminated union instead of throwing.\n *\n * That is the whole point of this file. A person closing the system passkey\n * sheet raises `NotAllowedError`, and so does a person whose authenticator has\n * nothing to offer; neither is an application error, and code that has to tell\n * them apart by catching and re-reading `error.name` gets it wrong once and\n * shows a red banner to someone who simply changed their mind.\n *\n * Ships to browsers: no Node built-ins here, `Buffer` included. The base64url\n * helpers come from `@simplewebauthn/browser`, which is bundled into this entry.\n */\n\nimport {\n browserSupportsWebAuthn,\n browserSupportsWebAuthnAutofill,\n startAuthentication,\n startRegistration,\n type AuthenticationResponseJSON,\n type PublicKeyCredentialCreationOptionsJSON,\n type PublicKeyCredentialRequestOptionsJSON,\n type RegistrationResponseJSON,\n} from '@simplewebauthn/browser';\n\nimport type { authApi } from '@spfn/auth';\n\n/** The typed auth client these helpers drive. */\nexport type AuthApi = typeof authApi;\n\n/**\n * Why a ceremony did not produce a session.\n *\n * - `unsupported`: this browser has no WebAuthn at all\n * - `cancelled`: the person dismissed the prompt\n * - `no-credential`: the authenticator had nothing for this relying party\n * - `error`: anything else, with the original error attached\n */\nexport type PasskeyFailureReason = 'unsupported' | 'cancelled' | 'no-credential' | 'error';\n\nexport type PasskeyResult<T> =\n | ({ ok: true } & T)\n | { ok: false; reason: PasskeyFailureReason; error?: unknown };\n\n/** Whether this browser can run a WebAuthn ceremony at all. */\nexport function isPasskeySupported(): boolean\n{\n return browserSupportsWebAuthn();\n}\n\n/**\n * Whether the browser can offer passkeys inside the ordinary autofill dropdown.\n *\n * Worth checking before rendering a sign-in form: conditional mediation is what\n * turns a passkey into \"tap the suggestion above the keyboard\", and where it is\n * missing the form needs a visible \"Sign in with a passkey\" button instead.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean>\n{\n return await browserSupportsWebAuthnAutofill();\n}\n\n/**\n * The reason a ceremony failure should be reported as.\n *\n * `NotAllowedError` is the browser's answer both to \"the person said no\" and to\n * \"nothing here matched\", and the specification deliberately does not\n * distinguish them — telling a caller which applied would say whether a\n * credential for this site exists on the device. So it is `cancelled` in both\n * cases on registration, and `no-credential` on a sign-in that offered no\n * credentials to choose from, which is the reading a UI wants.\n */\nfunction failureReason(error: unknown, whenNotAllowed: PasskeyFailureReason): PasskeyFailureReason\n{\n return (error as { name?: string } | null)?.name === 'NotAllowedError' ? whenNotAllowed : 'error';\n}\n\nexport interface EnrollPasskeyOptions\n{\n /** Owner-facing name for the passkey list, e.g. the device model. */\n label?: string;\n /** Sent when the session proved itself longer ago than the recent-auth window. */\n currentPassword?: string;\n}\n\nexport interface EnrollPasskeyValue\n{\n passkeyId: string;\n label: string | null;\n createdAt: string;\n}\n\n/**\n * Enroll a passkey on the device in front of the user.\n *\n * Requires a signed-in session. A 403 with code `RECENT_AUTH_REQUIRED` from the\n * options call means the caller should prompt for the password and try again\n * with `currentPassword`; that is a rejected promise, not a result here, because\n * it is the server declining rather than the ceremony failing.\n */\nexport async function enrollPasskey(\n api: AuthApi,\n options: EnrollPasskeyOptions = {},\n): Promise<PasskeyResult<EnrollPasskeyValue>>\n{\n if (!isPasskeySupported())\n {\n return { ok: false, reason: 'unsupported' };\n }\n\n const optionsJSON = await api.passkeyRegisterOptions.call({\n body: { currentPassword: options.currentPassword },\n }) as PublicKeyCredentialCreationOptionsJSON;\n\n let response: RegistrationResponseJSON;\n\n try\n {\n response = await startRegistration({ optionsJSON });\n }\n catch (error)\n {\n return { ok: false, reason: failureReason(error, 'cancelled'), error };\n }\n\n const enrolled = await api.passkeyRegisterVerify.call({\n body: { response, label: options.label },\n }) as EnrollPasskeyValue;\n\n return { ok: true, ...enrolled };\n}\n\nexport interface SignInWithPasskeyOptions\n{\n /**\n * Offer the passkey through the browser's autofill dropdown instead of a\n * modal. Needs an `<input autocomplete=\"username webauthn\">` on the page.\n */\n conditional?: boolean;\n deviceName?: string;\n platform?: string;\n}\n\nexport interface SignInWithPasskeyValue\n{\n userId: string;\n publicId: string;\n email?: string;\n phone?: string;\n passwordChangeRequired: boolean;\n}\n\n/**\n * Sign in with a passkey, no identifier asked for.\n *\n * The device key the session runs on is generated and stored by the Next.js\n * proxy interceptor, exactly as on a password login — nothing here handles a\n * private key.\n */\nexport async function signInWithPasskey(\n api: AuthApi,\n options: SignInWithPasskeyOptions = {},\n): Promise<PasskeyResult<SignInWithPasskeyValue>>\n{\n if (!isPasskeySupported())\n {\n return { ok: false, reason: 'unsupported' };\n }\n\n const optionsJSON = await api.passkeyLoginOptions.call({\n body: {},\n }) as PublicKeyCredentialRequestOptionsJSON;\n\n let response: AuthenticationResponseJSON;\n\n try\n {\n response = await startAuthentication({ optionsJSON, useBrowserAutofill: options.conditional === true });\n }\n catch (error)\n {\n return { ok: false, reason: failureReason(error, 'no-credential'), error };\n }\n\n const session = await api.passkeyLoginVerify.call({\n body: { response },\n }) as SignInWithPasskeyValue;\n\n return { ok: true, ...session };\n}\n\nexport interface RenewSessionValue\n{\n /** The new device key the session now runs on. */\n keyId: string;\n}\n\n/**\n * Renew a bound session key with a passkey assertion.\n *\n * What an app calls when a request came back `SessionRenewalRequiredError`: the\n * session's short-lived key has run out and one WebAuthn ceremony puts a new one\n * in the cookie. The person sees the system prompt, not a sign-in form.\n *\n * The body is `{ response }` and nothing else. The expiring key's id lives in an\n * HttpOnly cookie that page script cannot read, and the new key pair is the\n * Next.js proxy's to generate — both are injected there, exactly as they are for\n * `signInWithPasskey`. Nothing here handles a private key.\n *\n * Refusals from the server are rejected promises rather than results, on the same\n * rule the rest of this file follows: a `SessionRenewalRefusedError` means the\n * server declined — the key is past its grace, or was revoked — and the app's\n * answer is to send the person to sign in, which is not the same as the ceremony\n * failing.\n */\nexport async function renewSession(api: AuthApi): Promise<PasskeyResult<RenewSessionValue>>\n{\n if (!isPasskeySupported())\n {\n return { ok: false, reason: 'unsupported' };\n }\n\n const optionsJSON = await api.sessionRenewOptions.call({\n body: {},\n }) as PublicKeyCredentialRequestOptionsJSON;\n\n let response: AuthenticationResponseJSON;\n\n try\n {\n response = await startAuthentication({ optionsJSON });\n }\n catch (error)\n {\n return { ok: false, reason: failureReason(error, 'no-credential'), error };\n }\n\n const renewed = await api.sessionRenewVerify.call({\n body: { response },\n }) as { keyId: string };\n\n return { ok: true, keyId: renewed.keyId };\n}\n\n/** What a successful disable answers with — the mode the account is now in. */\nexport interface DisableSessionBindingValue\n{\n mode: 'none';\n}\n\nexport interface DisableSessionBindingOptions\n{\n /**\n * The account password, for a browser with no passkey to hand.\n *\n * Send it, or let the ceremony run — one of the two is required. Key age is\n * deliberately not accepted: a session cookie copied in the minutes after a\n * sign-in carries exactly that, and it must not be able to switch the\n * protection off.\n */\n currentPassword?: string;\n}\n\n/**\n * Turn session binding off for this account.\n *\n * With `currentPassword` this is one call. Without it, the passkey ceremony runs\n * first and the assertion is what proves ownership — the same ceremony renewal\n * uses, for the same reason.\n *\n * Turning binding *on* needs no ceremony and no helper: it is\n * `api.setSessionBinding.call({ body: { mode: 'passkey' } })`. Leaving is the\n * privileged direction here, which is the reverse of the usual posture and is\n * the whole reason this helper exists.\n */\nexport async function disableSessionBinding(\n api: AuthApi,\n options: DisableSessionBindingOptions = {},\n): Promise<PasskeyResult<DisableSessionBindingValue>>\n{\n if (options.currentPassword)\n {\n await api.setSessionBinding.call({ body: { mode: 'none', currentPassword: options.currentPassword } });\n\n return { ok: true, mode: 'none' };\n }\n\n if (!isPasskeySupported())\n {\n return { ok: false, reason: 'unsupported' };\n }\n\n const optionsJSON = await api.sessionBindingDisableOptions.call({\n body: {},\n }) as PublicKeyCredentialRequestOptionsJSON;\n\n let response: AuthenticationResponseJSON;\n\n try\n {\n response = await startAuthentication({ optionsJSON });\n }\n catch (error)\n {\n return { ok: false, reason: failureReason(error, 'no-credential'), error };\n }\n\n await api.setSessionBinding.call({ body: { mode: 'none', response } });\n\n return { ok: true, mode: 'none' };\n}\n"],"mappings":";AAiBA;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKG;AAsBA,SAAS,qBAChB;AACI,SAAO,wBAAwB;AACnC;AASA,eAAsB,kCACtB;AACI,SAAO,MAAM,gCAAgC;AACjD;AAYA,SAAS,cAAc,OAAgB,gBACvC;AACI,SAAQ,OAAoC,SAAS,oBAAoB,iBAAiB;AAC9F;AAyBA,eAAsB,cAClB,KACA,UAAgC,CAAC,GAErC;AACI,MAAI,CAAC,mBAAmB,GACxB;AACI,WAAO,EAAE,IAAI,OAAO,QAAQ,cAAc;AAAA,EAC9C;AAEA,QAAM,cAAc,MAAM,IAAI,uBAAuB,KAAK;AAAA,IACtD,MAAM,EAAE,iBAAiB,QAAQ,gBAAgB;AAAA,EACrD,CAAC;AAED,MAAI;AAEJ,MACA;AACI,eAAW,MAAM,kBAAkB,EAAE,YAAY,CAAC;AAAA,EACtD,SACO,OACP;AACI,WAAO,EAAE,IAAI,OAAO,QAAQ,cAAc,OAAO,WAAW,GAAG,MAAM;AAAA,EACzE;AAEA,QAAM,WAAW,MAAM,IAAI,sBAAsB,KAAK;AAAA,IAClD,MAAM,EAAE,UAAU,OAAO,QAAQ,MAAM;AAAA,EAC3C,CAAC;AAED,SAAO,EAAE,IAAI,MAAM,GAAG,SAAS;AACnC;AA6BA,eAAsB,kBAClB,KACA,UAAoC,CAAC,GAEzC;AACI,MAAI,CAAC,mBAAmB,GACxB;AACI,WAAO,EAAE,IAAI,OAAO,QAAQ,cAAc;AAAA,EAC9C;AAEA,QAAM,cAAc,MAAM,IAAI,oBAAoB,KAAK;AAAA,IACnD,MAAM,CAAC;AAAA,EACX,CAAC;AAED,MAAI;AAEJ,MACA;AACI,eAAW,MAAM,oBAAoB,EAAE,aAAa,oBAAoB,QAAQ,gBAAgB,KAAK,CAAC;AAAA,EAC1G,SACO,OACP;AACI,WAAO,EAAE,IAAI,OAAO,QAAQ,cAAc,OAAO,eAAe,GAAG,MAAM;AAAA,EAC7E;AAEA,QAAM,UAAU,MAAM,IAAI,mBAAmB,KAAK;AAAA,IAC9C,MAAM,EAAE,SAAS;AAAA,EACrB,CAAC;AAED,SAAO,EAAE,IAAI,MAAM,GAAG,QAAQ;AAClC;AA0BA,eAAsB,aAAa,KACnC;AACI,MAAI,CAAC,mBAAmB,GACxB;AACI,WAAO,EAAE,IAAI,OAAO,QAAQ,cAAc;AAAA,EAC9C;AAEA,QAAM,cAAc,MAAM,IAAI,oBAAoB,KAAK;AAAA,IACnD,MAAM,CAAC;AAAA,EACX,CAAC;AAED,MAAI;AAEJ,MACA;AACI,eAAW,MAAM,oBAAoB,EAAE,YAAY,CAAC;AAAA,EACxD,SACO,OACP;AACI,WAAO,EAAE,IAAI,OAAO,QAAQ,cAAc,OAAO,eAAe,GAAG,MAAM;AAAA,EAC7E;AAEA,QAAM,UAAU,MAAM,IAAI,mBAAmB,KAAK;AAAA,IAC9C,MAAM,EAAE,SAAS;AAAA,EACrB,CAAC;AAED,SAAO,EAAE,IAAI,MAAM,OAAO,QAAQ,MAAM;AAC5C;AAiCA,eAAsB,sBAClB,KACA,UAAwC,CAAC,GAE7C;AACI,MAAI,QAAQ,iBACZ;AACI,UAAM,IAAI,kBAAkB,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,iBAAiB,QAAQ,gBAAgB,EAAE,CAAC;AAErG,WAAO,EAAE,IAAI,MAAM,MAAM,OAAO;AAAA,EACpC;AAEA,MAAI,CAAC,mBAAmB,GACxB;AACI,WAAO,EAAE,IAAI,OAAO,QAAQ,cAAc;AAAA,EAC9C;AAEA,QAAM,cAAc,MAAM,IAAI,6BAA6B,KAAK;AAAA,IAC5D,MAAM,CAAC;AAAA,EACX,CAAC;AAED,MAAI;AAEJ,MACA;AACI,eAAW,MAAM,oBAAoB,EAAE,YAAY,CAAC;AAAA,EACxD,SACO,OACP;AACI,WAAO,EAAE,IAAI,OAAO,QAAQ,cAAc,OAAO,eAAe,GAAG,MAAM;AAAA,EAC7E;AAEA,QAAM,IAAI,kBAAkB,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,SAAS,EAAE,CAAC;AAErE,SAAO,EAAE,IAAI,MAAM,MAAM,OAAO;AACpC;","names":[]}
1
+ {"version":3,"sources":["../src/client/passkeys.ts"],"sourcesContent":["/**\n * @spfn/auth/client - Passkeys (WebAuthn)\n *\n * Four browser helpers over the two ceremonies. Each one is `options` from the\n * server, `navigator.credentials` in the browser, `verify` back to the server —\n * and each answers with a discriminated union instead of throwing.\n *\n * That is the whole point of this file. A person closing the system passkey\n * sheet raises `NotAllowedError`, and so does a person whose authenticator has\n * nothing to offer; neither is an application error, and code that has to tell\n * them apart by catching and re-reading `error.name` gets it wrong once and\n * shows a red banner to someone who simply changed their mind.\n *\n * Ships to browsers: no Node built-ins here, `Buffer` included. The base64url\n * helpers come from `@simplewebauthn/browser`, which is bundled into this entry.\n */\n\nimport {\n browserSupportsWebAuthn,\n browserSupportsWebAuthnAutofill,\n startAuthentication,\n startRegistration,\n type AuthenticationResponseJSON,\n type PublicKeyCredentialCreationOptionsJSON,\n type PublicKeyCredentialRequestOptionsJSON,\n type RegistrationResponseJSON,\n} from '@simplewebauthn/browser';\n\nimport type { authApi } from '@spfn/auth';\n\n/** The typed auth client these helpers drive. */\nexport type AuthApi = typeof authApi;\n\n/**\n * Why a ceremony did not produce a session.\n *\n * - `unsupported`: this browser has no WebAuthn at all\n * - `cancelled`: the person dismissed the prompt\n * - `no-credential`: the authenticator had nothing for this relying party\n * - `error`: anything else, with the original error attached\n */\nexport type PasskeyFailureReason = 'unsupported' | 'cancelled' | 'no-credential' | 'error';\n\nexport type PasskeyResult<T> =\n | ({ ok: true } & T)\n | { ok: false; reason: PasskeyFailureReason; error?: unknown };\n\n/** Whether this browser can run a WebAuthn ceremony at all. */\nexport function isPasskeySupported(): boolean\n{\n return browserSupportsWebAuthn();\n}\n\n/**\n * Whether the browser can offer passkeys inside the ordinary autofill dropdown.\n *\n * Worth checking before rendering a sign-in form: conditional mediation is what\n * turns a passkey into \"tap the suggestion above the keyboard\", and where it is\n * missing the form needs a visible \"Sign in with a passkey\" button instead.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean>\n{\n return await browserSupportsWebAuthnAutofill();\n}\n\n/**\n * The reason a ceremony failure should be reported as.\n *\n * `NotAllowedError` is the browser's answer both to \"the person said no\" and to\n * \"nothing here matched\", and the specification deliberately does not\n * distinguish them — telling a caller which applied would say whether a\n * credential for this site exists on the device. So it is `cancelled` in both\n * cases on registration, and `no-credential` on a sign-in that offered no\n * credentials to choose from, which is the reading a UI wants.\n */\nfunction failureReason(error: unknown, whenNotAllowed: PasskeyFailureReason): PasskeyFailureReason\n{\n return (error as { name?: string } | null)?.name === 'NotAllowedError' ? whenNotAllowed : 'error';\n}\n\nexport interface EnrollPasskeyOptions\n{\n /** Owner-facing name for the passkey list, e.g. the device model. */\n label?: string;\n /** Sent when the session proved itself longer ago than the recent-auth window. */\n currentPassword?: string;\n}\n\nexport interface EnrollPasskeyValue\n{\n passkeyId: string;\n label: string | null;\n createdAt: string;\n}\n\n/**\n * Enroll a passkey on the device in front of the user.\n *\n * Requires a signed-in session. A 403 with code `RECENT_AUTH_REQUIRED` from the\n * options call means the caller should prompt for the password and try again\n * with `currentPassword`; that is a rejected promise, not a result here, because\n * it is the server declining rather than the ceremony failing.\n */\nexport async function enrollPasskey(\n api: AuthApi,\n options: EnrollPasskeyOptions = {},\n): Promise<PasskeyResult<EnrollPasskeyValue>>\n{\n if (!isPasskeySupported())\n {\n return { ok: false, reason: 'unsupported' };\n }\n\n const optionsJSON = await api.passkeyRegisterOptions.call({\n body: { currentPassword: options.currentPassword },\n }) as PublicKeyCredentialCreationOptionsJSON;\n\n let response: RegistrationResponseJSON;\n\n try\n {\n response = await startRegistration({ optionsJSON });\n }\n catch (error)\n {\n return { ok: false, reason: failureReason(error, 'cancelled'), error };\n }\n\n const enrolled = await api.passkeyRegisterVerify.call({\n body: { response, label: options.label },\n }) as EnrollPasskeyValue;\n\n return { ok: true, ...enrolled };\n}\n\nexport interface SignInWithPasskeyOptions\n{\n /**\n * Offer the passkey through the browser's autofill dropdown instead of a\n * modal. Needs an `<input autocomplete=\"username webauthn\">` on the page.\n */\n conditional?: boolean;\n deviceName?: string;\n platform?: string;\n}\n\nexport interface SignInWithPasskeyValue\n{\n userId: string;\n publicId: string;\n email?: string;\n phone?: string;\n passwordChangeRequired: boolean;\n}\n\n/**\n * Sign in with a passkey, no identifier asked for.\n *\n * The device key the session runs on is generated and stored by the Next.js\n * proxy interceptor, exactly as on a password login — nothing here handles a\n * private key.\n */\nexport async function signInWithPasskey(\n api: AuthApi,\n options: SignInWithPasskeyOptions = {},\n): Promise<PasskeyResult<SignInWithPasskeyValue>>\n{\n if (!isPasskeySupported())\n {\n return { ok: false, reason: 'unsupported' };\n }\n\n const optionsJSON = await api.passkeyLoginOptions.call({\n body: {},\n }) as PublicKeyCredentialRequestOptionsJSON;\n\n let response: AuthenticationResponseJSON;\n\n try\n {\n response = await startAuthentication({ optionsJSON, useBrowserAutofill: options.conditional === true });\n }\n catch (error)\n {\n return { ok: false, reason: failureReason(error, 'no-credential'), error };\n }\n\n const session = await api.passkeyLoginVerify.call({\n body: { response },\n }) as SignInWithPasskeyValue;\n\n return { ok: true, ...session };\n}\n\nexport interface RenewSessionValue\n{\n /** The new device key the session now runs on. */\n keyId: string;\n}\n\n/**\n * Renew a bound session key with a passkey assertion.\n *\n * What an app calls when a request came back `SessionRenewalRequiredError`: the\n * session's short-lived key has run out and one WebAuthn ceremony puts a new one\n * in the cookie. The person sees the system prompt, not a sign-in form.\n *\n * The body is `{ response }` and nothing else. The expiring key's id lives in an\n * HttpOnly cookie that page script cannot read, and the new key pair is the\n * Next.js proxy's to generate — both are injected there, exactly as they are for\n * `signInWithPasskey`. Nothing here handles a private key.\n *\n * Refusals from the server are rejected promises rather than results, on the same\n * rule the rest of this file follows: a `SessionRenewalRefusedError` means the\n * server declined — the key is past its grace, or was revoked — and the app's\n * answer is to send the person to sign in, which is not the same as the ceremony\n * failing.\n */\nexport async function renewSession(api: AuthApi): Promise<PasskeyResult<RenewSessionValue>>\n{\n if (!isPasskeySupported())\n {\n return { ok: false, reason: 'unsupported' };\n }\n\n const optionsJSON = await api.sessionRenewOptions.call({\n body: {},\n }) as PublicKeyCredentialRequestOptionsJSON;\n\n let response: AuthenticationResponseJSON;\n\n try\n {\n response = await startAuthentication({ optionsJSON });\n }\n catch (error)\n {\n return { ok: false, reason: failureReason(error, 'no-credential'), error };\n }\n\n const renewed = await api.sessionRenewVerify.call({\n body: { response },\n }) as { keyId: string };\n\n return { ok: true, keyId: renewed.keyId };\n}\n\n/** What a successful disable answers with — the mode the account is now in. */\nexport interface DisableSessionBindingValue\n{\n mode: 'none';\n}\n\nexport interface DisableSessionBindingOptions\n{\n /**\n * The account password, for a browser with no passkey to hand.\n *\n * Send it, or let the ceremony run — one of the two is required. Key age is\n * deliberately not accepted: a session cookie copied in the minutes after a\n * sign-in carries exactly that, and it must not be able to switch the\n * protection off.\n */\n currentPassword?: string;\n}\n\n/**\n * Turn session binding off for this account.\n *\n * With `currentPassword` this is one call. Without it, the passkey ceremony runs\n * first and the assertion is what proves ownership — the same ceremony renewal\n * uses, for the same reason.\n *\n * Turning binding *on* needs no ceremony and no helper: it is\n * `api.setSessionBinding.call({ body: { mode: 'passkey' } })`. Leaving is the\n * privileged direction here, which is the reverse of the usual posture and is\n * the whole reason this helper exists.\n */\nexport async function disableSessionBinding(\n api: AuthApi,\n options: DisableSessionBindingOptions = {},\n): Promise<PasskeyResult<DisableSessionBindingValue>>\n{\n if (options.currentPassword)\n {\n await api.setSessionBinding.call({ body: { mode: 'none', currentPassword: options.currentPassword } });\n\n return { ok: true, mode: 'none' };\n }\n\n if (!isPasskeySupported())\n {\n return { ok: false, reason: 'unsupported' };\n }\n\n const optionsJSON = await api.sessionBindingDisableOptions.call({\n body: {},\n }) as PublicKeyCredentialRequestOptionsJSON;\n\n let response: AuthenticationResponseJSON;\n\n try\n {\n response = await startAuthentication({ optionsJSON });\n }\n catch (error)\n {\n return { ok: false, reason: failureReason(error, 'no-credential'), error };\n }\n\n await api.setSessionBinding.call({ body: { mode: 'none', response } });\n\n return { ok: true, mode: 'none' };\n}\n\n// ============================================================================\n// Second-factor step-up on a new device (#95)\n// ============================================================================\n\n/**\n * What a completed step-up answers with — the sign-in the 202 was standing in for.\n *\n * Deliberately loose about the login fields. Through the Next.js proxy the\n * session is already sealed by the time this resolves and the app reads it from\n * the session cookie; a native client reads `userId` off the body. `keyId` and\n * `challengeHash` are the proxy's business and are not repeated here.\n */\nexport interface CompleteMfaValue\n{\n userId?: string;\n keyId: string;\n}\n\n/**\n * Finish a sign-in that answered `mfaRequired` with a code from the\n * authenticator app.\n *\n * The challenge is the `secret` from that 202 — or, on the web OAuth path, the\n * `?challenge=` the callback handler put on the confirm page's URL. It is single\n * use and lives ten minutes.\n *\n * A wrong code is a rejected promise carrying `MfaVerificationFailedError`, not\n * a result: it is the server declining rather than a ceremony failing, and there\n * is no ceremony here at all. Five wrong ones end the challenge and the person\n * signs in again.\n *\n * @param api - the typed auth client\n * @param challenge - the challenge secret from the 202 or the callback query\n * @param code - the six digits, spaces and dashes and all\n */\nexport async function completeMfaWithCode(\n api: AuthApi,\n challenge: string,\n code: string,\n): Promise<CompleteMfaValue>\n{\n return await api.mfaVerify.call({ body: { challenge, code } }) as CompleteMfaValue;\n}\n\n/**\n * Finish the same sign-in with one of the ten written-down recovery codes.\n *\n * Single use, and the count in `mfa/status` drops by one — an app that warns at\n * two remaining reads it from there after this resolves.\n */\nexport async function completeMfaWithRecoveryCode(\n api: AuthApi,\n challenge: string,\n recoveryCode: string,\n): Promise<CompleteMfaValue>\n{\n return await api.mfaVerify.call({ body: { challenge, recoveryCode } }) as CompleteMfaValue;\n}\n\n/**\n * Finish the same sign-in with a passkey the owner marked as a second factor.\n *\n * `options` from the server, `navigator.credentials` in the browser, `verify`\n * back — the shape every ceremony in this file has, and a discriminated union\n * rather than a throw for the same reason: a person who dismisses the system\n * sheet has not hit an application error.\n *\n * The challenge is what names the account, since there is no session yet. A\n * passkey the owner never marked is refused with the same body as a wrong code.\n */\nexport async function completeMfaWithPasskey(\n api: AuthApi,\n challenge: string,\n): Promise<PasskeyResult<CompleteMfaValue>>\n{\n if (!isPasskeySupported())\n {\n return { ok: false, reason: 'unsupported' };\n }\n\n const optionsJSON = await api.mfaVerifyOptions.call({\n body: { challenge },\n }) as PublicKeyCredentialRequestOptionsJSON;\n\n let response: AuthenticationResponseJSON;\n\n try\n {\n response = await startAuthentication({ optionsJSON });\n }\n catch (error)\n {\n return { ok: false, reason: failureReason(error, 'no-credential'), error };\n }\n\n const verified = await api.mfaVerify.call({ body: { challenge, response } }) as CompleteMfaValue;\n\n return { ok: true, ...verified };\n}\n"],"mappings":"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","names":[]}
package/dist/config.d.ts CHANGED
@@ -376,6 +376,26 @@ declare const authEnvSchema: {
376
376
  } & {
377
377
  key: "SPFN_AUTH_MFA_STEP_UP_MINUTES";
378
378
  };
379
+ SPFN_AUTH_MFA_CONFIRM_PATH: {
380
+ description: string;
381
+ default: string;
382
+ required: boolean;
383
+ examples: string[];
384
+ type: "string";
385
+ validator: (value: string) => string;
386
+ } & {
387
+ key: "SPFN_AUTH_MFA_CONFIRM_PATH";
388
+ };
389
+ SPFN_AUTH_MFA_CHALLENGE_TTL_MINUTES: {
390
+ description: string;
391
+ default: number;
392
+ required: boolean;
393
+ examples: number[];
394
+ type: "number";
395
+ validator: (value: string) => number;
396
+ } & {
397
+ key: "SPFN_AUTH_MFA_CHALLENGE_TTL_MINUTES";
398
+ };
379
399
  SPFN_AUTH_BOUND_KEY_TTL_HOURS: {
380
400
  description: string;
381
401
  default: number;
@@ -1021,6 +1041,26 @@ declare const env: _spfn_core_env.InferEnvType<{
1021
1041
  } & {
1022
1042
  key: "SPFN_AUTH_MFA_STEP_UP_MINUTES";
1023
1043
  };
1044
+ SPFN_AUTH_MFA_CONFIRM_PATH: {
1045
+ description: string;
1046
+ default: string;
1047
+ required: boolean;
1048
+ examples: string[];
1049
+ type: "string";
1050
+ validator: (value: string) => string;
1051
+ } & {
1052
+ key: "SPFN_AUTH_MFA_CONFIRM_PATH";
1053
+ };
1054
+ SPFN_AUTH_MFA_CHALLENGE_TTL_MINUTES: {
1055
+ description: string;
1056
+ default: number;
1057
+ required: boolean;
1058
+ examples: number[];
1059
+ type: "number";
1060
+ validator: (value: string) => number;
1061
+ } & {
1062
+ key: "SPFN_AUTH_MFA_CHALLENGE_TTL_MINUTES";
1063
+ };
1024
1064
  SPFN_AUTH_BOUND_KEY_TTL_HOURS: {
1025
1065
  description: string;
1026
1066
  default: number;
package/dist/config.js CHANGED
@@ -360,6 +360,22 @@ var authEnvSchema = defineEnvSchema({
360
360
  examples: [5, 10, 30]
361
361
  })
362
362
  },
363
+ SPFN_AUTH_MFA_CONFIRM_PATH: {
364
+ ...envString({
365
+ description: "App page that asks for the second factor after a 202 sign-in, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. createOAuthCallbackHandler redirects the browser there with ?challenge= when a social sign-in needs a step-up; the page posts that challenge and a code to /_auth/mfa/verify. It is a page in your app, not an API route.",
366
+ default: "/auth/mfa",
367
+ required: false,
368
+ examples: ["/auth/mfa", "/sign-in/two-factor"]
369
+ })
370
+ },
371
+ SPFN_AUTH_MFA_CHALLENGE_TTL_MINUTES: {
372
+ ...envNumber({
373
+ description: "How long a new-device second-factor challenge stays spendable. A sign-in on an enrolled account from a device it has never seen answers 202 with a challenge instead of a session, and the key it registered is inactive until POST /_auth/mfa/verify spends it. This is how long the person has to reach for their authenticator \u2014 and how long an attacker holding only the password has. The Next.js proxy seals its pending cookie for the same span.",
374
+ default: 10,
375
+ required: false,
376
+ examples: [5, 10, 15]
377
+ })
378
+ },
363
379
  // ============================================================================
364
380
  // Session binding (#97)
365
381
  // ============================================================================
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config/index.ts","../src/config/schema.ts"],"sourcesContent":["/**\n * Core Package Configuration\n *\n * @example\n * ```typescript\n * import { registry } from '@spfn/core/config';\n *\n * const env = registry.validate();\n * console.log(env.DB_POOL_MAX);\n * ```\n *\n * @module config\n */\n\nimport { createEnvRegistry } from '@spfn/core/env';\nimport { authEnvSchema } from './schema';\n\nexport { authEnvSchema as envSchema } from './schema';\n\n/**\n * Environment registry\n */\nconst registry = createEnvRegistry(authEnvSchema);\nexport const env = registry.validate();\n","/**\n * Auth Environment Variable Schema\n *\n * Centralized schema definition for all environment variables used in @spfn/auth.\n * This provides type safety, validation, and documentation for Auth configuration.\n *\n * @module config/schema\n */\n\nimport {\n defineEnvSchema,\n envString,\n envNumber,\n envBoolean,\n envEnum,\n createSecureSecretParser,\n createPasswordParser,\n} from '@spfn/core/env';\n\n/**\n * Auth environment variable schema\n *\n * Defines all Auth environment variables with:\n * - Type information\n * - Default values\n * - Validation rules\n * - Documentation\n *\n * @example\n * ```typescript\n * import { envSchema } from '@spfn/auth/config';\n *\n * // Access schema information\n * console.log(envSchema.SPFN_AUTH_SESSION_SECRET.description);\n * console.log(envSchema.SPFN_AUTH_JWT_EXPIRES_IN.default);\n * ```\n */\nexport const authEnvSchema = defineEnvSchema({\n // ============================================================================\n // Session Configuration\n // ============================================================================\n SPFN_AUTH_SESSION_SECRET: {\n ...envString({\n description: 'Session encryption secret (minimum 32 characters for AES-256)',\n required: true,\n fallbackKeys: ['SESSION_SECRET'],\n validator: createSecureSecretParser({\n minLength: 32,\n minUniqueChars: 16,\n minEntropy: 3.5,\n }),\n sensitive: true,\n nextjs: true, // Required for Next.js RSC session validation\n examples: [\n 'my-super-secret-session-key-at-least-32-chars-long',\n 'use-a-cryptographically-secure-random-string-here',\n ],\n }),\n },\n\n SPFN_AUTH_SESSION_TTL: {\n ...envString({\n description: 'Session TTL (time to live) - supports duration strings like \\'7d\\', \\'12h\\', \\'45m\\'',\n default: '7d',\n required: false,\n nextjs: true, // May be needed for session validation in Next.js RSC\n examples: ['7d', '30d', '12h', '45m', '3600'],\n }),\n },\n\n // ============================================================================\n // JWT Configuration\n // ============================================================================\n SPFN_AUTH_JWT_SECRET: {\n ...envString({\n description: 'JWT signing secret for server-signed tokens (legacy mode)',\n default: 'dev-secret-key-change-in-production',\n required: false,\n examples: [\n 'your-jwt-secret-key-here',\n 'use-different-from-session-secret',\n ],\n }),\n },\n\n SPFN_AUTH_JWT_EXPIRES_IN: {\n ...envString({\n description: 'JWT token expiration time (e.g., \\'7d\\', \\'24h\\', \\'1h\\')',\n default: '7d',\n required: false,\n examples: ['7d', '24h', '1h', '30m'],\n }),\n },\n\n // ============================================================================\n // Security Configuration\n // ============================================================================\n SPFN_AUTH_COOKIE_SECURE: {\n ...envBoolean({\n description: 'Override cookie Secure flag. Defaults to NODE_ENV === \"production\". Set to false for HTTP-only environments (e.g. bastion over plain HTTP).',\n required: false,\n nextjs: true,\n examples: [true, false],\n }),\n },\n\n SPFN_AUTH_CSRF: {\n ...envString({\n description: 'CSRF protection for cookie-session mutations in the Next.js proxy: off | warn | enforce. Unset behaves as \"warn\" (log what would be refused, allow it through). configureAuth({ csrf: { mode } }) takes precedence.',\n required: false,\n nextjs: true, // The check runs in the Next.js proxy\n examples: ['enforce', 'warn', 'off'],\n }),\n },\n\n SPFN_AUTH_BCRYPT_SALT_ROUNDS: {\n ...envNumber({\n description: 'Bcrypt salt rounds (cost factor, higher = more secure but slower)',\n default: 12,\n required: false,\n examples: [10, 12, 14],\n }),\n key: 'SPFN_AUTH_BCRYPT_SALT_ROUNDS',\n },\n\n SPFN_AUTH_VERIFICATION_TOKEN_SECRET: {\n ...envString({\n description: 'Verification token secret for email verification, password reset, etc.',\n required: true,\n examples: [\n 'your-verification-token-secret',\n 'can-be-different-from-jwt-secret',\n ],\n }),\n },\n\n SPFN_AUTH_TOKEN_ENCRYPTION_KEYS: {\n ...envString({\n description: 'Backend-only OAuth token encryption keyring. Comma-separated <keyId>:<base64-encoded 32-byte key> entries; the first key encrypts new values and remaining keys decrypt during rotation.',\n required: false,\n sensitive: true,\n examples: [\n 'v2:<base64-encoded-32-byte-key>,v1:<previous-base64-encoded-32-byte-key>',\n ],\n }),\n },\n\n // ============================================================================\n // Admin Account Configuration\n // ============================================================================\n SPFN_AUTH_ADMIN_ACCOUNTS: {\n ...envString({\n description: 'JSON array of admin accounts (recommended for multiple admins)',\n required: false,\n examples: [\n '[{\"email\":\"admin@example.com\",\"password\":\"secure-pass\",\"role\":\"admin\"}]',\n '[{\"email\":\"super@example.com\",\"password\":\"pass1\",\"role\":\"superadmin\"},{\"email\":\"admin@example.com\",\"password\":\"pass2\",\"role\":\"admin\"}]',\n ],\n }),\n },\n\n SPFN_AUTH_ADMIN_EMAILS: {\n ...envString({\n description: 'Comma-separated list of admin emails (legacy CSV format)',\n required: false,\n examples: [\n 'admin@example.com,user@example.com',\n 'super@example.com,admin@example.com,user@example.com',\n ],\n }),\n },\n\n SPFN_AUTH_ADMIN_PASSWORDS: {\n ...envString({\n description: 'Comma-separated list of admin passwords (legacy CSV format)',\n required: false,\n examples: [\n 'admin-pass,user-pass',\n 'super-pass,admin-pass,user-pass',\n ],\n }),\n },\n\n SPFN_AUTH_ADMIN_ROLES: {\n ...envString({\n description: 'Comma-separated list of admin roles (legacy CSV format)',\n required: false,\n examples: [\n 'admin,user',\n 'superadmin,admin,user',\n ],\n }),\n },\n\n SPFN_AUTH_ADMIN_EMAIL: {\n ...envString({\n description: 'Single admin email (simplest format)',\n required: false,\n examples: ['admin@example.com'],\n }),\n },\n\n SPFN_AUTH_ADMIN_PASSWORD: {\n ...envString({\n description: 'Single admin password (simplest format)',\n required: false,\n validator: createPasswordParser({\n minLength: 8,\n requireUppercase: true,\n requireLowercase: true,\n requireNumber: true,\n requireSpecial: true,\n }),\n sensitive: true,\n examples: ['SecureAdmin123!'],\n }),\n },\n\n // ============================================================================\n // Username Configuration\n // ============================================================================\n SPFN_AUTH_RESERVED_USERNAMES: {\n ...envString({\n description: 'Comma-separated list of reserved usernames that cannot be registered',\n required: false,\n default: 'admin,root,system,support,help,moderator,superadmin',\n examples: [\n 'admin,root,system,support,help',\n 'admin,root,system,support,help,moderator,superadmin,operator',\n ],\n }),\n },\n\n SPFN_AUTH_USERNAME_MIN_LENGTH: {\n ...envNumber({\n description: 'Minimum username length',\n default: 3,\n required: false,\n examples: [2, 3, 4],\n }),\n },\n\n SPFN_AUTH_USERNAME_MAX_LENGTH: {\n ...envNumber({\n description: 'Maximum username length',\n default: 30,\n required: false,\n examples: [20, 30, 50],\n }),\n },\n\n // ============================================================================\n // Verified-email signup\n // ============================================================================\n SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES: {\n ...envNumber({\n description: 'How long an emailed signup confirmation link stays valid. Long enough to survive a mail delay, short enough that a link left in an inbox stops working.',\n default: 30,\n required: false,\n examples: [15, 30, 60],\n }),\n },\n\n SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES: {\n ...envNumber({\n description: 'How long the password-setup session opened by a confirmation link stays valid. Covers one sitting at the password form, not an abandoned tab.',\n default: 15,\n required: false,\n examples: [10, 15, 30],\n }),\n },\n\n SPFN_AUTH_SIGNUP_CONFIRM_PATH: {\n ...envString({\n description: 'App page the emailed confirmation link opens, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. The page reads the token from the query string and posts it to /_auth/signup/email/confirm; it is a page in your app, not an API route.',\n default: '/signup/confirm',\n required: false,\n examples: ['/signup/confirm', '/auth/confirm', '/join/verify'],\n }),\n },\n\n // ============================================================================\n // Password reset (verified email)\n // ============================================================================\n SPFN_AUTH_PASSWORD_RESET_LINK_TTL_MINUTES: {\n ...envNumber({\n description: 'How long an emailed password reset link stays valid. Long enough to survive a mail delay, short enough that a link left in an inbox stops working.',\n default: 30,\n required: false,\n examples: [15, 30, 60],\n }),\n },\n\n SPFN_AUTH_PASSWORD_RESET_SETUP_TTL_MINUTES: {\n ...envNumber({\n description: 'How long the password-setup session opened by a reset link stays valid. Covers one sitting at the new-password form, not an abandoned tab.',\n default: 15,\n required: false,\n examples: [10, 15, 30],\n }),\n },\n\n SPFN_AUTH_PASSWORD_RESET_CONFIRM_PATH: {\n ...envString({\n description: 'App page the emailed reset link opens, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. The page reads the token from the query string and posts it to /_auth/password/reset/confirm; it is a page in your app, not an API route.',\n default: '/password/reset',\n required: false,\n examples: ['/password/reset', '/auth/reset', '/forgot/new-password'],\n }),\n },\n\n // ============================================================================\n // Signed sign-out-everywhere link\n // ============================================================================\n SPFN_AUTH_REVOKE_ALL_LINK_TTL_MINUTES: {\n ...envNumber({\n description: 'How long a signed sign-out-everywhere link stays valid. The same default as the other two link flows: long enough to survive a mail delay, short enough that a link left in an inbox stops working — and this one signs every device out.',\n default: 30,\n required: false,\n examples: [15, 30, 60],\n }),\n },\n\n SPFN_AUTH_REVOKE_ALL_CONFIRM_PATH: {\n ...envString({\n description: 'App page the sign-out-everywhere link opens, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. The page reads the token from the query string and posts it to /_auth/keys/revoke-all/confirm to describe the link, then to /_auth/keys/revoke-all/consume when the owner confirms; it is a page in your app, not an API route.',\n default: '/account/revoke-all',\n required: false,\n examples: ['/account/revoke-all', '/security/sign-out-everywhere'],\n }),\n },\n\n // ============================================================================\n // Link mail delivery\n // ============================================================================\n SPFN_AUTH_LINK_MAIL_DELIVERY: {\n ...envEnum(['auto', 'inline', 'queued'] as const, {\n description: 'Who sends signup-link, password-reset and account-exists mail. \\'auto\\' (default) queues it on auth.link-mail when pg-boss is initialised and sends it on the request path when it is not; \\'queued\\' always queues and surfaces an enqueue failure; \\'inline\\' always sends on the request path, which makes how long the request took reveal whether the address has an account.',\n default: 'auto',\n required: false,\n examples: ['auto', 'inline', 'queued'],\n }),\n },\n\n // ============================================================================\n // Passkeys (WebAuthn)\n // ============================================================================\n SPFN_AUTH_PASSKEY_RP_ID: {\n ...envString({\n description: 'Domain passkeys are bound to — a registrable domain with no protocol and no port. Defaults to the host of {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. Set it explicitly when the app is served from several hosts that share a domain; every origin below must be that host or a subdomain of it. Changing it orphans every passkey already enrolled.',\n required: false,\n examples: ['example.com', 'app.example.com', 'localhost'],\n }),\n },\n\n SPFN_AUTH_PASSKEY_RP_NAME: {\n ...envString({\n description: \"Name the authenticator's own prompt shows the user. Defaults to the relying party ID.\",\n required: false,\n examples: ['Acme', 'Acme Staging'],\n }),\n },\n\n SPFN_AUTH_PASSKEY_ORIGINS: {\n ...envString({\n description: 'Comma-separated full origins allowed to run a passkey ceremony. Defaults to the origin of {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. Each must be https (http only for localhost) and must be the relying party ID or a subdomain of it. Checked at boot: a value that breaks either rule refuses to start, because it would otherwise surface as the browser refusing every ceremony.',\n required: false,\n examples: ['https://app.example.com', 'https://app.example.com,https://admin.example.com', 'http://localhost:3000'],\n }),\n },\n\n SPFN_AUTH_PASSKEY_USER_VERIFICATION: {\n ...envString({\n description: \"How hard the authenticator must prove the person is present: 'preferred' or 'required'. 'discouraged' is refused at boot — a passkey is the whole credential here, so an assertion that skipped user verification would sign someone in on an unlocked device alone.\",\n default: 'preferred',\n required: false,\n examples: ['preferred', 'required'],\n }),\n },\n\n SPFN_AUTH_PASSKEY_CHALLENGE_TTL_SECONDS: {\n ...envNumber({\n description: 'How long the challenge minted by a passkey options call stays presentable. One ceremony at the authenticator, not an abandoned tab.',\n default: 300,\n required: false,\n examples: [120, 300, 600],\n }),\n },\n\n SPFN_AUTH_PASSKEY_RECENT_AUTH_MINUTES: {\n ...envNumber({\n description: 'How recently the calling device key must have been registered for enrolling or revoking a passkey to go through without the current password. Older than this and the request needs `currentPassword`; an account with no password has to sign in again.',\n default: 10,\n required: false,\n examples: [5, 10, 30],\n }),\n },\n\n // ============================================================================\n // Second factor (MFA)\n // ============================================================================\n SPFN_AUTH_MFA_ISSUER: {\n ...envString({\n description: 'Name the authenticator app files this account under, carried in the otpauth:// URI. Defaults to the passkey relying-party name, then to the host of {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. Changing it after people have enrolled only relabels the entry in their app; the codes keep working.',\n required: false,\n examples: ['Acme', 'Acme Staging'],\n }),\n },\n\n SPFN_AUTH_MFA_STEP_UP_MINUTES: {\n ...envNumber({\n description: 'How recently an enrolled account must have proved its second factor on the calling device for a sensitive change (password change, sign out everywhere, disabling MFA, passkey management) to go through. Older than this and the request is 403 STEP_UP_REQUIRED until POST /_auth/mfa/step-up succeeds. Unenrolled accounts are unaffected.',\n default: 10,\n required: false,\n examples: [5, 10, 30],\n }),\n },\n\n // ============================================================================\n // Session binding (#97)\n // ============================================================================\n SPFN_AUTH_BOUND_KEY_TTL_HOURS: {\n ...envNumber({\n description: 'How long a session key bound to a passkey lives. This is the window in which a copied session cookie is still indistinguishable from the original, so it is hours rather than days; past it the browser runs one WebAuthn ceremony and gets a new key. Only applies to accounts that turned session binding on.',\n default: 24,\n required: false,\n examples: [8, 24, 72],\n }),\n },\n\n SPFN_AUTH_BOUND_KEY_RENEW_GRACE_HOURS: {\n ...envNumber({\n description: 'How long after a bound key expires a passkey renewal is still offered. Past it the account signs in again. Open-ended grace would make an expired key a long-lived key with extra steps.',\n default: 168,\n required: false,\n examples: [24, 168, 720],\n }),\n },\n\n SPFN_AUTH_CONCURRENT_USE_WINDOW_MS: {\n ...envNumber({\n description: 'How far apart two sightings of one device key from two client addresses still count as concurrent use, surfaced as `concurrentUseAtMillis` on the key list. A signal for the owner to read, never a refusal — addresses change legitimately. Meaningful only where proxy-guard is configured, since without it every web request carries the Next.js server\\'s address.',\n default: 300000,\n required: false,\n examples: [60000, 300000, 900000],\n }),\n },\n\n SPFN_AUTH_SESSION_RENEW_PATH: {\n ...envString({\n description: 'Page in your app that runs the renewal ceremony. `RequireAuth` redirects a bound session whose key expired here instead of to the sign-in page; the page calls `renewSession(api)` and returns the user to where they were. Override per guard with the `renewalPath` prop.',\n default: '/auth/renew',\n required: false,\n nextjs: true, // Read by RequireAuth, which renders in the Next.js runtime\n examples: ['/auth/renew', '/session/renew'],\n }),\n },\n\n // ============================================================================\n // API Configuration\n // ============================================================================\n SPFN_API_URL: {\n ...envString({\n description: 'Internal API URL for server-to-server communication',\n default: 'http://localhost:8790',\n required: false,\n examples: [\n 'https://api.example.com',\n 'http://localhost:8790',\n ],\n }),\n },\n\n NEXT_PUBLIC_SPFN_API_URL: {\n ...envString({\n description: 'Public-facing API URL used for browser-facing redirects. Falls back to SPFN_API_URL if not set.',\n required: false,\n examples: [\n 'https://api.example.com',\n 'http://localhost:8790',\n ],\n }),\n },\n\n SPFN_APP_URL: {\n ...envString({\n description: 'Next.js application URL (internal). Used for server-to-server communication.',\n default: 'http://localhost:3000',\n required: false,\n examples: [\n 'https://app.example.com',\n 'http://localhost:3000',\n ],\n }),\n },\n\n NEXT_PUBLIC_SPFN_APP_URL: {\n ...envString({\n description: 'Public-facing Next.js app URL for browser redirects (e.g. OAuth redirect). Falls back to SPFN_APP_URL if not set.',\n required: false,\n examples: [\n 'https://app.example.com',\n 'http://localhost:3000',\n ],\n }),\n },\n\n // ============================================================================\n // OAuth Configuration - Google\n // ============================================================================\n SPFN_AUTH_GOOGLE_CLIENT_ID: {\n ...envString({\n description: 'Google OAuth 2.0 Client ID. When set, Google OAuth routes are automatically enabled.',\n required: false,\n examples: ['123456789-abc123.apps.googleusercontent.com'],\n }),\n },\n\n SPFN_AUTH_GOOGLE_CLIENT_SECRET: {\n ...envString({\n description: 'Google OAuth 2.0 Client Secret',\n required: false,\n sensitive: true,\n examples: ['GOCSPX-abcdefghijklmnop'],\n }),\n },\n\n SPFN_AUTH_GOOGLE_SCOPES: {\n ...envString({\n description: 'Comma-separated Google OAuth scopes. Defaults to \"email,profile\" if not set.',\n required: false,\n examples: [\n 'email,profile',\n 'email,profile,https://www.googleapis.com/auth/gmail.readonly',\n 'email,profile,https://www.googleapis.com/auth/calendar.readonly',\n ],\n }),\n },\n\n SPFN_AUTH_GOOGLE_REDIRECT_URI: {\n ...envString({\n description: 'Google OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/google/callback. The override must stay on the web app origin at this exact path — the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.',\n required: false,\n examples: [\n 'https://app.example.com/_auth/oauth/google/callback',\n 'http://localhost:3000/_auth/oauth/google/callback',\n ],\n }),\n },\n\n // ============================================================================\n // OAuth Configuration - Kakao\n // ============================================================================\n SPFN_AUTH_KAKAO_CLIENT_ID: {\n ...envString({\n description: 'Kakao Login REST API key. Used as the OAuth client_id.',\n required: false,\n examples: ['your-kakao-rest-api-key'],\n }),\n },\n\n SPFN_AUTH_KAKAO_CLIENT_SECRET: {\n ...envString({\n description: 'Kakao Login client secret. Required when the Kakao client-secret feature is enabled.',\n required: false,\n sensitive: true,\n examples: ['your-kakao-client-secret'],\n }),\n },\n\n SPFN_AUTH_KAKAO_ADMIN_KEY: {\n ...envString({\n description: 'Kakao app admin key. Required to verify the User Unlinked webhook (Authorization: KakaoAK header).',\n required: false,\n sensitive: true,\n examples: ['your-kakao-admin-key'],\n }),\n },\n\n SPFN_AUTH_KAKAO_SCOPES: {\n ...envString({\n description: 'Comma-separated Kakao consent scopes. Defaults to account_email.',\n required: false,\n examples: ['account_email'],\n }),\n },\n\n SPFN_AUTH_KAKAO_REDIRECT_URI: {\n ...envString({\n description: 'Kakao OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/kakao/callback. The override must stay on the web app origin at this exact path — the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.',\n required: false,\n examples: ['https://app.example.com/_auth/oauth/kakao/callback'],\n }),\n },\n\n // ============================================================================\n // OAuth Configuration - Naver\n // ============================================================================\n SPFN_AUTH_NAVER_CLIENT_ID: {\n ...envString({\n description: 'Naver Login OAuth client ID.',\n required: false,\n examples: ['your-naver-client-id'],\n }),\n },\n\n SPFN_AUTH_NAVER_CLIENT_SECRET: {\n ...envString({\n description: 'Naver Login OAuth client secret.',\n required: false,\n sensitive: true,\n examples: ['your-naver-client-secret'],\n }),\n },\n\n SPFN_AUTH_NAVER_REDIRECT_URI: {\n ...envString({\n description: 'Naver OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/naver/callback. The override must stay on the web app origin at this exact path — the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.',\n required: false,\n examples: ['https://app.example.com/_auth/oauth/naver/callback'],\n }),\n },\n\n // ============================================================================\n // OAuth Configuration - GitHub\n // ============================================================================\n SPFN_AUTH_GITHUB_CLIENT_ID: {\n ...envString({\n description: 'GitHub OAuth app client ID. When set, GitHub OAuth routes are automatically enabled.',\n required: false,\n examples: ['Iv1.abc123def456'],\n }),\n },\n\n SPFN_AUTH_GITHUB_CLIENT_SECRET: {\n ...envString({\n description: 'GitHub OAuth app client secret.',\n required: false,\n sensitive: true,\n examples: ['your-github-client-secret'],\n }),\n },\n\n SPFN_AUTH_GITHUB_SCOPES: {\n ...envString({\n description: 'Comma-separated GitHub OAuth scopes. Defaults to \"read:user,user:email\".',\n required: false,\n examples: ['read:user,user:email'],\n }),\n },\n\n SPFN_AUTH_GITHUB_REDIRECT_URI: {\n ...envString({\n description: 'GitHub OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/github/callback. The override must stay on the web app origin at this exact path — the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.',\n required: false,\n examples: ['https://app.example.com/_auth/oauth/github/callback'],\n }),\n },\n\n SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK: {\n ...envString({\n description: 'Boot-time check of the four SPFN_AUTH_<PROVIDER>_REDIRECT_URI overrides: each one that is set must sit on the web app origin ({NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}) at /_auth/oauth/<provider>/callback, because the callback CSRF cookie is host-only. \"off\" is the only value that disables the check — unset, \"on\" and anything else all run it.',\n required: false,\n examples: ['off'],\n }),\n },\n\n // ============================================================================\n // Native Social Login (mobile/web id_token verification)\n //\n // 네이티브 SDK가 받은 id_token을 서버가 JWKS로 검증하는 경로 전용 설정.\n // authorization code 교환을 하지 않으므로 client secret이 필요 없다.\n // audience(aud)로 허용할 client id 목록만 지정한다.\n // ============================================================================\n SPFN_AUTH_GOOGLE_NATIVE_CLIENT_IDS: {\n ...envString({\n description: 'Comma-separated Google client IDs accepted as id_token audience for native sign-in (iOS, Android, web). When set, Google native sign-in is enabled. SPFN_AUTH_GOOGLE_CLIENT_ID is also accepted automatically.',\n required: false,\n examples: [\n '123-ios.apps.googleusercontent.com,123-android.apps.googleusercontent.com',\n ],\n }),\n },\n\n SPFN_AUTH_APPLE_CLIENT_IDS: {\n ...envString({\n description: 'Comma-separated Apple client IDs accepted as id_token audience for native sign-in (iOS bundle ID, web/Android Services ID). When set, Apple native sign-in is enabled.',\n required: false,\n examples: [\n 'com.example.app,com.example.app.service',\n ],\n }),\n },\n\n SPFN_AUTH_KAKAO_NATIVE_CLIENT_IDS: {\n ...envString({\n description: 'Comma-separated Kakao app keys accepted as id_token audience for native sign-in (native app key). SPFN_AUTH_KAKAO_CLIENT_ID (REST API key) is also accepted automatically, so native sign-in is available when either variable is set. Requires OpenID Connect to be enabled in the Kakao developer console.',\n required: false,\n examples: [\n 'your-kakao-native-app-key',\n ],\n }),\n },\n\n SPFN_AUTH_NAVER_NATIVE_CLIENT_IDS: {\n ...envString({\n description: 'Comma-separated Naver client IDs accepted as id_token audience for native sign-in. SPFN_AUTH_NAVER_CLIENT_ID is also accepted automatically, and one Naver application has a single client ID covering web and app environments — set this only when the app uses a separate application.',\n required: false,\n examples: [\n 'your-naver-app-client-id',\n ],\n }),\n },\n\n SPFN_AUTH_OAUTH_SUCCESS_URL: {\n ...envString({\n description: 'OAuth callback page URL. This page should use OAuthCallback component to finalize session.',\n required: false,\n default: '/auth/callback',\n examples: [\n '/auth/callback',\n 'https://app.example.com/auth/callback',\n ],\n }),\n },\n\n SPFN_AUTH_OAUTH_ERROR_URL: {\n ...envString({\n description: 'URL to redirect after OAuth error. Use {error} placeholder for error message.',\n required: false,\n default: '/auth/error?error={error}',\n examples: [\n 'https://app.example.com/auth/error?error={error}',\n 'http://localhost:3000/auth/error?error={error}',\n ],\n }),\n },\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/config/index.ts","../src/config/schema.ts"],"sourcesContent":["/**\n * Core Package Configuration\n *\n * @example\n * ```typescript\n * import { registry } from '@spfn/core/config';\n *\n * const env = registry.validate();\n * console.log(env.DB_POOL_MAX);\n * ```\n *\n * @module config\n */\n\nimport { createEnvRegistry } from '@spfn/core/env';\nimport { authEnvSchema } from './schema';\n\nexport { authEnvSchema as envSchema } from './schema';\n\n/**\n * Environment registry\n */\nconst registry = createEnvRegistry(authEnvSchema);\nexport const env = registry.validate();\n","/**\n * Auth Environment Variable Schema\n *\n * Centralized schema definition for all environment variables used in @spfn/auth.\n * This provides type safety, validation, and documentation for Auth configuration.\n *\n * @module config/schema\n */\n\nimport {\n defineEnvSchema,\n envString,\n envNumber,\n envBoolean,\n envEnum,\n createSecureSecretParser,\n createPasswordParser,\n} from '@spfn/core/env';\n\n/**\n * Auth environment variable schema\n *\n * Defines all Auth environment variables with:\n * - Type information\n * - Default values\n * - Validation rules\n * - Documentation\n *\n * @example\n * ```typescript\n * import { envSchema } from '@spfn/auth/config';\n *\n * // Access schema information\n * console.log(envSchema.SPFN_AUTH_SESSION_SECRET.description);\n * console.log(envSchema.SPFN_AUTH_JWT_EXPIRES_IN.default);\n * ```\n */\nexport const authEnvSchema = defineEnvSchema({\n // ============================================================================\n // Session Configuration\n // ============================================================================\n SPFN_AUTH_SESSION_SECRET: {\n ...envString({\n description: 'Session encryption secret (minimum 32 characters for AES-256)',\n required: true,\n fallbackKeys: ['SESSION_SECRET'],\n validator: createSecureSecretParser({\n minLength: 32,\n minUniqueChars: 16,\n minEntropy: 3.5,\n }),\n sensitive: true,\n nextjs: true, // Required for Next.js RSC session validation\n examples: [\n 'my-super-secret-session-key-at-least-32-chars-long',\n 'use-a-cryptographically-secure-random-string-here',\n ],\n }),\n },\n\n SPFN_AUTH_SESSION_TTL: {\n ...envString({\n description: 'Session TTL (time to live) - supports duration strings like \\'7d\\', \\'12h\\', \\'45m\\'',\n default: '7d',\n required: false,\n nextjs: true, // May be needed for session validation in Next.js RSC\n examples: ['7d', '30d', '12h', '45m', '3600'],\n }),\n },\n\n // ============================================================================\n // JWT Configuration\n // ============================================================================\n SPFN_AUTH_JWT_SECRET: {\n ...envString({\n description: 'JWT signing secret for server-signed tokens (legacy mode)',\n default: 'dev-secret-key-change-in-production',\n required: false,\n examples: [\n 'your-jwt-secret-key-here',\n 'use-different-from-session-secret',\n ],\n }),\n },\n\n SPFN_AUTH_JWT_EXPIRES_IN: {\n ...envString({\n description: 'JWT token expiration time (e.g., \\'7d\\', \\'24h\\', \\'1h\\')',\n default: '7d',\n required: false,\n examples: ['7d', '24h', '1h', '30m'],\n }),\n },\n\n // ============================================================================\n // Security Configuration\n // ============================================================================\n SPFN_AUTH_COOKIE_SECURE: {\n ...envBoolean({\n description: 'Override cookie Secure flag. Defaults to NODE_ENV === \"production\". Set to false for HTTP-only environments (e.g. bastion over plain HTTP).',\n required: false,\n nextjs: true,\n examples: [true, false],\n }),\n },\n\n SPFN_AUTH_CSRF: {\n ...envString({\n description: 'CSRF protection for cookie-session mutations in the Next.js proxy: off | warn | enforce. Unset behaves as \"warn\" (log what would be refused, allow it through). configureAuth({ csrf: { mode } }) takes precedence.',\n required: false,\n nextjs: true, // The check runs in the Next.js proxy\n examples: ['enforce', 'warn', 'off'],\n }),\n },\n\n SPFN_AUTH_BCRYPT_SALT_ROUNDS: {\n ...envNumber({\n description: 'Bcrypt salt rounds (cost factor, higher = more secure but slower)',\n default: 12,\n required: false,\n examples: [10, 12, 14],\n }),\n key: 'SPFN_AUTH_BCRYPT_SALT_ROUNDS',\n },\n\n SPFN_AUTH_VERIFICATION_TOKEN_SECRET: {\n ...envString({\n description: 'Verification token secret for email verification, password reset, etc.',\n required: true,\n examples: [\n 'your-verification-token-secret',\n 'can-be-different-from-jwt-secret',\n ],\n }),\n },\n\n SPFN_AUTH_TOKEN_ENCRYPTION_KEYS: {\n ...envString({\n description: 'Backend-only OAuth token encryption keyring. Comma-separated <keyId>:<base64-encoded 32-byte key> entries; the first key encrypts new values and remaining keys decrypt during rotation.',\n required: false,\n sensitive: true,\n examples: [\n 'v2:<base64-encoded-32-byte-key>,v1:<previous-base64-encoded-32-byte-key>',\n ],\n }),\n },\n\n // ============================================================================\n // Admin Account Configuration\n // ============================================================================\n SPFN_AUTH_ADMIN_ACCOUNTS: {\n ...envString({\n description: 'JSON array of admin accounts (recommended for multiple admins)',\n required: false,\n examples: [\n '[{\"email\":\"admin@example.com\",\"password\":\"secure-pass\",\"role\":\"admin\"}]',\n '[{\"email\":\"super@example.com\",\"password\":\"pass1\",\"role\":\"superadmin\"},{\"email\":\"admin@example.com\",\"password\":\"pass2\",\"role\":\"admin\"}]',\n ],\n }),\n },\n\n SPFN_AUTH_ADMIN_EMAILS: {\n ...envString({\n description: 'Comma-separated list of admin emails (legacy CSV format)',\n required: false,\n examples: [\n 'admin@example.com,user@example.com',\n 'super@example.com,admin@example.com,user@example.com',\n ],\n }),\n },\n\n SPFN_AUTH_ADMIN_PASSWORDS: {\n ...envString({\n description: 'Comma-separated list of admin passwords (legacy CSV format)',\n required: false,\n examples: [\n 'admin-pass,user-pass',\n 'super-pass,admin-pass,user-pass',\n ],\n }),\n },\n\n SPFN_AUTH_ADMIN_ROLES: {\n ...envString({\n description: 'Comma-separated list of admin roles (legacy CSV format)',\n required: false,\n examples: [\n 'admin,user',\n 'superadmin,admin,user',\n ],\n }),\n },\n\n SPFN_AUTH_ADMIN_EMAIL: {\n ...envString({\n description: 'Single admin email (simplest format)',\n required: false,\n examples: ['admin@example.com'],\n }),\n },\n\n SPFN_AUTH_ADMIN_PASSWORD: {\n ...envString({\n description: 'Single admin password (simplest format)',\n required: false,\n validator: createPasswordParser({\n minLength: 8,\n requireUppercase: true,\n requireLowercase: true,\n requireNumber: true,\n requireSpecial: true,\n }),\n sensitive: true,\n examples: ['SecureAdmin123!'],\n }),\n },\n\n // ============================================================================\n // Username Configuration\n // ============================================================================\n SPFN_AUTH_RESERVED_USERNAMES: {\n ...envString({\n description: 'Comma-separated list of reserved usernames that cannot be registered',\n required: false,\n default: 'admin,root,system,support,help,moderator,superadmin',\n examples: [\n 'admin,root,system,support,help',\n 'admin,root,system,support,help,moderator,superadmin,operator',\n ],\n }),\n },\n\n SPFN_AUTH_USERNAME_MIN_LENGTH: {\n ...envNumber({\n description: 'Minimum username length',\n default: 3,\n required: false,\n examples: [2, 3, 4],\n }),\n },\n\n SPFN_AUTH_USERNAME_MAX_LENGTH: {\n ...envNumber({\n description: 'Maximum username length',\n default: 30,\n required: false,\n examples: [20, 30, 50],\n }),\n },\n\n // ============================================================================\n // Verified-email signup\n // ============================================================================\n SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES: {\n ...envNumber({\n description: 'How long an emailed signup confirmation link stays valid. Long enough to survive a mail delay, short enough that a link left in an inbox stops working.',\n default: 30,\n required: false,\n examples: [15, 30, 60],\n }),\n },\n\n SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES: {\n ...envNumber({\n description: 'How long the password-setup session opened by a confirmation link stays valid. Covers one sitting at the password form, not an abandoned tab.',\n default: 15,\n required: false,\n examples: [10, 15, 30],\n }),\n },\n\n SPFN_AUTH_SIGNUP_CONFIRM_PATH: {\n ...envString({\n description: 'App page the emailed confirmation link opens, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. The page reads the token from the query string and posts it to /_auth/signup/email/confirm; it is a page in your app, not an API route.',\n default: '/signup/confirm',\n required: false,\n examples: ['/signup/confirm', '/auth/confirm', '/join/verify'],\n }),\n },\n\n // ============================================================================\n // Password reset (verified email)\n // ============================================================================\n SPFN_AUTH_PASSWORD_RESET_LINK_TTL_MINUTES: {\n ...envNumber({\n description: 'How long an emailed password reset link stays valid. Long enough to survive a mail delay, short enough that a link left in an inbox stops working.',\n default: 30,\n required: false,\n examples: [15, 30, 60],\n }),\n },\n\n SPFN_AUTH_PASSWORD_RESET_SETUP_TTL_MINUTES: {\n ...envNumber({\n description: 'How long the password-setup session opened by a reset link stays valid. Covers one sitting at the new-password form, not an abandoned tab.',\n default: 15,\n required: false,\n examples: [10, 15, 30],\n }),\n },\n\n SPFN_AUTH_PASSWORD_RESET_CONFIRM_PATH: {\n ...envString({\n description: 'App page the emailed reset link opens, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. The page reads the token from the query string and posts it to /_auth/password/reset/confirm; it is a page in your app, not an API route.',\n default: '/password/reset',\n required: false,\n examples: ['/password/reset', '/auth/reset', '/forgot/new-password'],\n }),\n },\n\n // ============================================================================\n // Signed sign-out-everywhere link\n // ============================================================================\n SPFN_AUTH_REVOKE_ALL_LINK_TTL_MINUTES: {\n ...envNumber({\n description: 'How long a signed sign-out-everywhere link stays valid. The same default as the other two link flows: long enough to survive a mail delay, short enough that a link left in an inbox stops working — and this one signs every device out.',\n default: 30,\n required: false,\n examples: [15, 30, 60],\n }),\n },\n\n SPFN_AUTH_REVOKE_ALL_CONFIRM_PATH: {\n ...envString({\n description: 'App page the sign-out-everywhere link opens, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. The page reads the token from the query string and posts it to /_auth/keys/revoke-all/confirm to describe the link, then to /_auth/keys/revoke-all/consume when the owner confirms; it is a page in your app, not an API route.',\n default: '/account/revoke-all',\n required: false,\n examples: ['/account/revoke-all', '/security/sign-out-everywhere'],\n }),\n },\n\n // ============================================================================\n // Link mail delivery\n // ============================================================================\n SPFN_AUTH_LINK_MAIL_DELIVERY: {\n ...envEnum(['auto', 'inline', 'queued'] as const, {\n description: 'Who sends signup-link, password-reset and account-exists mail. \\'auto\\' (default) queues it on auth.link-mail when pg-boss is initialised and sends it on the request path when it is not; \\'queued\\' always queues and surfaces an enqueue failure; \\'inline\\' always sends on the request path, which makes how long the request took reveal whether the address has an account.',\n default: 'auto',\n required: false,\n examples: ['auto', 'inline', 'queued'],\n }),\n },\n\n // ============================================================================\n // Passkeys (WebAuthn)\n // ============================================================================\n SPFN_AUTH_PASSKEY_RP_ID: {\n ...envString({\n description: 'Domain passkeys are bound to — a registrable domain with no protocol and no port. Defaults to the host of {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. Set it explicitly when the app is served from several hosts that share a domain; every origin below must be that host or a subdomain of it. Changing it orphans every passkey already enrolled.',\n required: false,\n examples: ['example.com', 'app.example.com', 'localhost'],\n }),\n },\n\n SPFN_AUTH_PASSKEY_RP_NAME: {\n ...envString({\n description: \"Name the authenticator's own prompt shows the user. Defaults to the relying party ID.\",\n required: false,\n examples: ['Acme', 'Acme Staging'],\n }),\n },\n\n SPFN_AUTH_PASSKEY_ORIGINS: {\n ...envString({\n description: 'Comma-separated full origins allowed to run a passkey ceremony. Defaults to the origin of {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. Each must be https (http only for localhost) and must be the relying party ID or a subdomain of it. Checked at boot: a value that breaks either rule refuses to start, because it would otherwise surface as the browser refusing every ceremony.',\n required: false,\n examples: ['https://app.example.com', 'https://app.example.com,https://admin.example.com', 'http://localhost:3000'],\n }),\n },\n\n SPFN_AUTH_PASSKEY_USER_VERIFICATION: {\n ...envString({\n description: \"How hard the authenticator must prove the person is present: 'preferred' or 'required'. 'discouraged' is refused at boot — a passkey is the whole credential here, so an assertion that skipped user verification would sign someone in on an unlocked device alone.\",\n default: 'preferred',\n required: false,\n examples: ['preferred', 'required'],\n }),\n },\n\n SPFN_AUTH_PASSKEY_CHALLENGE_TTL_SECONDS: {\n ...envNumber({\n description: 'How long the challenge minted by a passkey options call stays presentable. One ceremony at the authenticator, not an abandoned tab.',\n default: 300,\n required: false,\n examples: [120, 300, 600],\n }),\n },\n\n SPFN_AUTH_PASSKEY_RECENT_AUTH_MINUTES: {\n ...envNumber({\n description: 'How recently the calling device key must have been registered for enrolling or revoking a passkey to go through without the current password. Older than this and the request needs `currentPassword`; an account with no password has to sign in again.',\n default: 10,\n required: false,\n examples: [5, 10, 30],\n }),\n },\n\n // ============================================================================\n // Second factor (MFA)\n // ============================================================================\n SPFN_AUTH_MFA_ISSUER: {\n ...envString({\n description: 'Name the authenticator app files this account under, carried in the otpauth:// URI. Defaults to the passkey relying-party name, then to the host of {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. Changing it after people have enrolled only relabels the entry in their app; the codes keep working.',\n required: false,\n examples: ['Acme', 'Acme Staging'],\n }),\n },\n\n SPFN_AUTH_MFA_STEP_UP_MINUTES: {\n ...envNumber({\n description: 'How recently an enrolled account must have proved its second factor on the calling device for a sensitive change (password change, sign out everywhere, disabling MFA, passkey management) to go through. Older than this and the request is 403 STEP_UP_REQUIRED until POST /_auth/mfa/step-up succeeds. Unenrolled accounts are unaffected.',\n default: 10,\n required: false,\n examples: [5, 10, 30],\n }),\n },\n\n SPFN_AUTH_MFA_CONFIRM_PATH: {\n ...envString({\n description: 'App page that asks for the second factor after a 202 sign-in, as a path on {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}. createOAuthCallbackHandler redirects the browser there with ?challenge= when a social sign-in needs a step-up; the page posts that challenge and a code to /_auth/mfa/verify. It is a page in your app, not an API route.',\n default: '/auth/mfa',\n required: false,\n examples: ['/auth/mfa', '/sign-in/two-factor'],\n }),\n },\n\n SPFN_AUTH_MFA_CHALLENGE_TTL_MINUTES: {\n ...envNumber({\n description: 'How long a new-device second-factor challenge stays spendable. A sign-in on an enrolled account from a device it has never seen answers 202 with a challenge instead of a session, and the key it registered is inactive until POST /_auth/mfa/verify spends it. This is how long the person has to reach for their authenticator — and how long an attacker holding only the password has. The Next.js proxy seals its pending cookie for the same span.',\n default: 10,\n required: false,\n examples: [5, 10, 15],\n }),\n },\n\n // ============================================================================\n // Session binding (#97)\n // ============================================================================\n SPFN_AUTH_BOUND_KEY_TTL_HOURS: {\n ...envNumber({\n description: 'How long a session key bound to a passkey lives. This is the window in which a copied session cookie is still indistinguishable from the original, so it is hours rather than days; past it the browser runs one WebAuthn ceremony and gets a new key. Only applies to accounts that turned session binding on.',\n default: 24,\n required: false,\n examples: [8, 24, 72],\n }),\n },\n\n SPFN_AUTH_BOUND_KEY_RENEW_GRACE_HOURS: {\n ...envNumber({\n description: 'How long after a bound key expires a passkey renewal is still offered. Past it the account signs in again. Open-ended grace would make an expired key a long-lived key with extra steps.',\n default: 168,\n required: false,\n examples: [24, 168, 720],\n }),\n },\n\n SPFN_AUTH_CONCURRENT_USE_WINDOW_MS: {\n ...envNumber({\n description: 'How far apart two sightings of one device key from two client addresses still count as concurrent use, surfaced as `concurrentUseAtMillis` on the key list. A signal for the owner to read, never a refusal — addresses change legitimately. Meaningful only where proxy-guard is configured, since without it every web request carries the Next.js server\\'s address.',\n default: 300000,\n required: false,\n examples: [60000, 300000, 900000],\n }),\n },\n\n SPFN_AUTH_SESSION_RENEW_PATH: {\n ...envString({\n description: 'Page in your app that runs the renewal ceremony. `RequireAuth` redirects a bound session whose key expired here instead of to the sign-in page; the page calls `renewSession(api)` and returns the user to where they were. Override per guard with the `renewalPath` prop.',\n default: '/auth/renew',\n required: false,\n nextjs: true, // Read by RequireAuth, which renders in the Next.js runtime\n examples: ['/auth/renew', '/session/renew'],\n }),\n },\n\n // ============================================================================\n // API Configuration\n // ============================================================================\n SPFN_API_URL: {\n ...envString({\n description: 'Internal API URL for server-to-server communication',\n default: 'http://localhost:8790',\n required: false,\n examples: [\n 'https://api.example.com',\n 'http://localhost:8790',\n ],\n }),\n },\n\n NEXT_PUBLIC_SPFN_API_URL: {\n ...envString({\n description: 'Public-facing API URL used for browser-facing redirects. Falls back to SPFN_API_URL if not set.',\n required: false,\n examples: [\n 'https://api.example.com',\n 'http://localhost:8790',\n ],\n }),\n },\n\n SPFN_APP_URL: {\n ...envString({\n description: 'Next.js application URL (internal). Used for server-to-server communication.',\n default: 'http://localhost:3000',\n required: false,\n examples: [\n 'https://app.example.com',\n 'http://localhost:3000',\n ],\n }),\n },\n\n NEXT_PUBLIC_SPFN_APP_URL: {\n ...envString({\n description: 'Public-facing Next.js app URL for browser redirects (e.g. OAuth redirect). Falls back to SPFN_APP_URL if not set.',\n required: false,\n examples: [\n 'https://app.example.com',\n 'http://localhost:3000',\n ],\n }),\n },\n\n // ============================================================================\n // OAuth Configuration - Google\n // ============================================================================\n SPFN_AUTH_GOOGLE_CLIENT_ID: {\n ...envString({\n description: 'Google OAuth 2.0 Client ID. When set, Google OAuth routes are automatically enabled.',\n required: false,\n examples: ['123456789-abc123.apps.googleusercontent.com'],\n }),\n },\n\n SPFN_AUTH_GOOGLE_CLIENT_SECRET: {\n ...envString({\n description: 'Google OAuth 2.0 Client Secret',\n required: false,\n sensitive: true,\n examples: ['GOCSPX-abcdefghijklmnop'],\n }),\n },\n\n SPFN_AUTH_GOOGLE_SCOPES: {\n ...envString({\n description: 'Comma-separated Google OAuth scopes. Defaults to \"email,profile\" if not set.',\n required: false,\n examples: [\n 'email,profile',\n 'email,profile,https://www.googleapis.com/auth/gmail.readonly',\n 'email,profile,https://www.googleapis.com/auth/calendar.readonly',\n ],\n }),\n },\n\n SPFN_AUTH_GOOGLE_REDIRECT_URI: {\n ...envString({\n description: 'Google OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/google/callback. The override must stay on the web app origin at this exact path — the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.',\n required: false,\n examples: [\n 'https://app.example.com/_auth/oauth/google/callback',\n 'http://localhost:3000/_auth/oauth/google/callback',\n ],\n }),\n },\n\n // ============================================================================\n // OAuth Configuration - Kakao\n // ============================================================================\n SPFN_AUTH_KAKAO_CLIENT_ID: {\n ...envString({\n description: 'Kakao Login REST API key. Used as the OAuth client_id.',\n required: false,\n examples: ['your-kakao-rest-api-key'],\n }),\n },\n\n SPFN_AUTH_KAKAO_CLIENT_SECRET: {\n ...envString({\n description: 'Kakao Login client secret. Required when the Kakao client-secret feature is enabled.',\n required: false,\n sensitive: true,\n examples: ['your-kakao-client-secret'],\n }),\n },\n\n SPFN_AUTH_KAKAO_ADMIN_KEY: {\n ...envString({\n description: 'Kakao app admin key. Required to verify the User Unlinked webhook (Authorization: KakaoAK header).',\n required: false,\n sensitive: true,\n examples: ['your-kakao-admin-key'],\n }),\n },\n\n SPFN_AUTH_KAKAO_SCOPES: {\n ...envString({\n description: 'Comma-separated Kakao consent scopes. Defaults to account_email.',\n required: false,\n examples: ['account_email'],\n }),\n },\n\n SPFN_AUTH_KAKAO_REDIRECT_URI: {\n ...envString({\n description: 'Kakao OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/kakao/callback. The override must stay on the web app origin at this exact path — the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.',\n required: false,\n examples: ['https://app.example.com/_auth/oauth/kakao/callback'],\n }),\n },\n\n // ============================================================================\n // OAuth Configuration - Naver\n // ============================================================================\n SPFN_AUTH_NAVER_CLIENT_ID: {\n ...envString({\n description: 'Naver Login OAuth client ID.',\n required: false,\n examples: ['your-naver-client-id'],\n }),\n },\n\n SPFN_AUTH_NAVER_CLIENT_SECRET: {\n ...envString({\n description: 'Naver Login OAuth client secret.',\n required: false,\n sensitive: true,\n examples: ['your-naver-client-secret'],\n }),\n },\n\n SPFN_AUTH_NAVER_REDIRECT_URI: {\n ...envString({\n description: 'Naver OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/naver/callback. The override must stay on the web app origin at this exact path — the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.',\n required: false,\n examples: ['https://app.example.com/_auth/oauth/naver/callback'],\n }),\n },\n\n // ============================================================================\n // OAuth Configuration - GitHub\n // ============================================================================\n SPFN_AUTH_GITHUB_CLIENT_ID: {\n ...envString({\n description: 'GitHub OAuth app client ID. When set, GitHub OAuth routes are automatically enabled.',\n required: false,\n examples: ['Iv1.abc123def456'],\n }),\n },\n\n SPFN_AUTH_GITHUB_CLIENT_SECRET: {\n ...envString({\n description: 'GitHub OAuth app client secret.',\n required: false,\n sensitive: true,\n examples: ['your-github-client-secret'],\n }),\n },\n\n SPFN_AUTH_GITHUB_SCOPES: {\n ...envString({\n description: 'Comma-separated GitHub OAuth scopes. Defaults to \"read:user,user:email\".',\n required: false,\n examples: ['read:user,user:email'],\n }),\n },\n\n SPFN_AUTH_GITHUB_REDIRECT_URI: {\n ...envString({\n description: 'GitHub OAuth callback URL. Defaults to {NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}/_auth/oauth/github/callback. The override must stay on the web app origin at this exact path — the CSRF cookie for the callback is host-only and the app rewrites /_auth/:path* to the API, so a callback that lands anywhere else is refused for CSRF. Checked at boot: a value off the web app origin or off the callback path refuses to start. The one case for an override elsewhere is the direct POST /_auth/oauth/start flow on a split deployment (no Next.js interceptor, so its CSRF cookie is on the API host), which also needs SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK=off.',\n required: false,\n examples: ['https://app.example.com/_auth/oauth/github/callback'],\n }),\n },\n\n SPFN_AUTH_OAUTH_CALLBACK_ORIGIN_CHECK: {\n ...envString({\n description: 'Boot-time check of the four SPFN_AUTH_<PROVIDER>_REDIRECT_URI overrides: each one that is set must sit on the web app origin ({NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL}) at /_auth/oauth/<provider>/callback, because the callback CSRF cookie is host-only. \"off\" is the only value that disables the check — unset, \"on\" and anything else all run it.',\n required: false,\n examples: ['off'],\n }),\n },\n\n // ============================================================================\n // Native Social Login (mobile/web id_token verification)\n //\n // 네이티브 SDK가 받은 id_token을 서버가 JWKS로 검증하는 경로 전용 설정.\n // authorization code 교환을 하지 않으므로 client secret이 필요 없다.\n // audience(aud)로 허용할 client id 목록만 지정한다.\n // ============================================================================\n SPFN_AUTH_GOOGLE_NATIVE_CLIENT_IDS: {\n ...envString({\n description: 'Comma-separated Google client IDs accepted as id_token audience for native sign-in (iOS, Android, web). When set, Google native sign-in is enabled. SPFN_AUTH_GOOGLE_CLIENT_ID is also accepted automatically.',\n required: false,\n examples: [\n '123-ios.apps.googleusercontent.com,123-android.apps.googleusercontent.com',\n ],\n }),\n },\n\n SPFN_AUTH_APPLE_CLIENT_IDS: {\n ...envString({\n description: 'Comma-separated Apple client IDs accepted as id_token audience for native sign-in (iOS bundle ID, web/Android Services ID). When set, Apple native sign-in is enabled.',\n required: false,\n examples: [\n 'com.example.app,com.example.app.service',\n ],\n }),\n },\n\n SPFN_AUTH_KAKAO_NATIVE_CLIENT_IDS: {\n ...envString({\n description: 'Comma-separated Kakao app keys accepted as id_token audience for native sign-in (native app key). SPFN_AUTH_KAKAO_CLIENT_ID (REST API key) is also accepted automatically, so native sign-in is available when either variable is set. Requires OpenID Connect to be enabled in the Kakao developer console.',\n required: false,\n examples: [\n 'your-kakao-native-app-key',\n ],\n }),\n },\n\n SPFN_AUTH_NAVER_NATIVE_CLIENT_IDS: {\n ...envString({\n description: 'Comma-separated Naver client IDs accepted as id_token audience for native sign-in. SPFN_AUTH_NAVER_CLIENT_ID is also accepted automatically, and one Naver application has a single client ID covering web and app environments — set this only when the app uses a separate application.',\n required: false,\n examples: [\n 'your-naver-app-client-id',\n ],\n }),\n },\n\n SPFN_AUTH_OAUTH_SUCCESS_URL: {\n ...envString({\n description: 'OAuth callback page URL. This page should use OAuthCallback component to finalize session.',\n required: false,\n default: '/auth/callback',\n examples: [\n '/auth/callback',\n 'https://app.example.com/auth/callback',\n ],\n }),\n },\n\n SPFN_AUTH_OAUTH_ERROR_URL: {\n ...envString({\n description: 'URL to redirect after OAuth error. Use {error} placeholder for error message.',\n required: false,\n default: '/auth/error?error={error}',\n examples: [\n 'https://app.example.com/auth/error?error={error}',\n 'http://localhost:3000/auth/error?error={error}',\n ],\n }),\n },\n});\n"],"mappings":"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","names":[]}
package/dist/errors.d.ts CHANGED
@@ -853,6 +853,48 @@ declare class StepUpRequiredError extends ForbiddenError {
853
853
  details?: Record<string, any>;
854
854
  });
855
855
  }
856
+ /**
857
+ * Session Pending Mismatch Error (401)
858
+ *
859
+ * Minted by the Next.js proxy: a second-factor verification succeeded at the
860
+ * backend, but the pending cookie this browser is holding was baked for a
861
+ * different challenge or a different device key.
862
+ *
863
+ * The comparison is the whole reason the cookie exists. Without it the proxy
864
+ * would seal whatever private key it happens to be holding around whatever key
865
+ * the verification activated — a person who starts a social login in one tab
866
+ * while a password step-up is outstanding in another would get a session signed
867
+ * with the wrong key, and `authenticate` would refuse every request it made.
868
+ *
869
+ * The key really is active by the time this is raised: the backend accepted the
870
+ * proof and this browser simply cannot prove it is the one that asked. Signing
871
+ * in again is the remedy, and it is a cheap one.
872
+ */
873
+ declare class SessionPendingMismatchError extends UnauthorizedError {
874
+ readonly code = "SESSION_PENDING_MISMATCH";
875
+ constructor(data?: {
876
+ message?: string;
877
+ details?: Record<string, any>;
878
+ });
879
+ }
880
+ /**
881
+ * Session Pending Expired Error (401)
882
+ *
883
+ * Minted by the Next.js proxy: a second-factor verification succeeded and there
884
+ * is no pending cookie to seal a session from — ten minutes passed, or the
885
+ * browser that finished the step-up is not the browser that started it.
886
+ *
887
+ * Told apart from a mismatch on purpose. This one is the ordinary way a person
888
+ * meets the end of the window, and the message can say so; a mismatch is a
889
+ * cookie that is present and wrong, which is worth a different line in a log.
890
+ */
891
+ declare class SessionPendingExpiredError extends UnauthorizedError {
892
+ readonly code = "SESSION_PENDING_EXPIRED";
893
+ constructor(data?: {
894
+ message?: string;
895
+ details?: Record<string, any>;
896
+ });
897
+ }
856
898
  /**
857
899
  * MFA Config Error (500)
858
900
  *
@@ -968,6 +1010,10 @@ type authErrors_SessionBindingUnavailableError = SessionBindingUnavailableError;
968
1010
  declare const authErrors_SessionBindingUnavailableError: typeof SessionBindingUnavailableError;
969
1011
  type authErrors_SessionContextChangedError = SessionContextChangedError;
970
1012
  declare const authErrors_SessionContextChangedError: typeof SessionContextChangedError;
1013
+ type authErrors_SessionPendingExpiredError = SessionPendingExpiredError;
1014
+ declare const authErrors_SessionPendingExpiredError: typeof SessionPendingExpiredError;
1015
+ type authErrors_SessionPendingMismatchError = SessionPendingMismatchError;
1016
+ declare const authErrors_SessionPendingMismatchError: typeof SessionPendingMismatchError;
971
1017
  type authErrors_SessionRenewalRefusedError = SessionRenewalRefusedError;
972
1018
  declare const authErrors_SessionRenewalRefusedError: typeof SessionRenewalRefusedError;
973
1019
  type authErrors_SessionRenewalRequiredError = SessionRenewalRequiredError;
@@ -987,7 +1033,7 @@ declare const authErrors_VerificationTokenPurposeMismatchError: typeof Verificat
987
1033
  type authErrors_VerificationTokenTargetMismatchError = VerificationTokenTargetMismatchError;
988
1034
  declare const authErrors_VerificationTokenTargetMismatchError: typeof VerificationTokenTargetMismatchError;
989
1035
  declare namespace authErrors {
990
- export { authErrors_AccountAlreadyExistsError as AccountAlreadyExistsError, authErrors_AccountDisabledError as AccountDisabledError, authErrors_AccountPendingDeletionError as AccountPendingDeletionError, authErrors_DeletionAlreadyRequestedError as DeletionAlreadyRequestedError, authErrors_DeletionNotRequestedError as DeletionNotRequestedError, authErrors_DeviceAuthAlreadyHandledError as DeviceAuthAlreadyHandledError, authErrors_DeviceAuthDeniedError as DeviceAuthDeniedError, authErrors_DeviceAuthExpiredError as DeviceAuthExpiredError, authErrors_DeviceAuthNotFoundError as DeviceAuthNotFoundError, authErrors_ImmediateDeletionNotAllowedError as ImmediateDeletionNotAllowedError, authErrors_InsufficientPermissionsError as InsufficientPermissionsError, authErrors_InsufficientRoleError as InsufficientRoleError, authErrors_InvalidCredentialsError as InvalidCredentialsError, authErrors_InvalidKeyFingerprintError as InvalidKeyFingerprintError, authErrors_InvalidSignupLinkError as InvalidSignupLinkError, authErrors_InvalidSignupSetupSessionError as InvalidSignupSetupSessionError, authErrors_InvalidSocialTokenError as InvalidSocialTokenError, authErrors_InvalidTokenError as InvalidTokenError, authErrors_InvalidVerificationCodeError as InvalidVerificationCodeError, authErrors_InvalidVerificationTokenError as InvalidVerificationTokenError, authErrors_KeyAlgorithmMismatchError as KeyAlgorithmMismatchError, authErrors_KeyExpiredError as KeyExpiredError, authErrors_KeyIdAlreadyRegisteredError as KeyIdAlreadyRegisteredError, authErrors_KeyNotFoundError as KeyNotFoundError, authErrors_LastRecoveryCredentialError as LastRecoveryCredentialError, authErrors_MfaAlreadyEnrolledError as MfaAlreadyEnrolledError, authErrors_MfaConfigError as MfaConfigError, authErrors_MfaNotEnrolledError as MfaNotEnrolledError, authErrors_MfaVerificationFailedError as MfaVerificationFailedError, authErrors_NativeSignInUnsupportedError as NativeSignInUnsupportedError, authErrors_NonceKeyBindingError as NonceKeyBindingError, authErrors_OAuth2AuthorizeRedirectError as OAuth2AuthorizeRedirectError, authErrors_OAuth2GrantNotFoundError as OAuth2GrantNotFoundError, authErrors_OAuth2RedirectUriMismatchError as OAuth2RedirectUriMismatchError, authErrors_OAuth2UnknownClientError as OAuth2UnknownClientError, authErrors_PasskeyAlreadyRegisteredError as PasskeyAlreadyRegisteredError, authErrors_PasskeyChallengeError as PasskeyChallengeError, authErrors_PasskeyConfigError as PasskeyConfigError, authErrors_PasskeyNotFoundError as PasskeyNotFoundError, authErrors_PasskeyVerificationError as PasskeyVerificationError, authErrors_PasswordResetLinkError as PasswordResetLinkError, authErrors_PasswordResetSessionError as PasswordResetSessionError, authErrors_RecentAuthenticationRequiredError as RecentAuthenticationRequiredError, authErrors_RegistrationRejectedError as RegistrationRejectedError, authErrors_ReservedUsernameError as ReservedUsernameError, authErrors_RevokeAllLinkError as RevokeAllLinkError, authErrors_SessionBindingUnavailableError as SessionBindingUnavailableError, authErrors_SessionContextChangedError as SessionContextChangedError, authErrors_SessionRenewalRefusedError as SessionRenewalRefusedError, authErrors_SessionRenewalRequiredError as SessionRenewalRequiredError, authErrors_SessionResealFailedError as SessionResealFailedError, authErrors_StepUpRequiredError as StepUpRequiredError, authErrors_TokenExpiredError as TokenExpiredError, authErrors_UnverifiedEmailLinkError as UnverifiedEmailLinkError, authErrors_UsernameAlreadyTakenError as UsernameAlreadyTakenError, authErrors_VerificationTokenPurposeMismatchError as VerificationTokenPurposeMismatchError, authErrors_VerificationTokenTargetMismatchError as VerificationTokenTargetMismatchError };
1036
+ export { authErrors_AccountAlreadyExistsError as AccountAlreadyExistsError, authErrors_AccountDisabledError as AccountDisabledError, authErrors_AccountPendingDeletionError as AccountPendingDeletionError, authErrors_DeletionAlreadyRequestedError as DeletionAlreadyRequestedError, authErrors_DeletionNotRequestedError as DeletionNotRequestedError, authErrors_DeviceAuthAlreadyHandledError as DeviceAuthAlreadyHandledError, authErrors_DeviceAuthDeniedError as DeviceAuthDeniedError, authErrors_DeviceAuthExpiredError as DeviceAuthExpiredError, authErrors_DeviceAuthNotFoundError as DeviceAuthNotFoundError, authErrors_ImmediateDeletionNotAllowedError as ImmediateDeletionNotAllowedError, authErrors_InsufficientPermissionsError as InsufficientPermissionsError, authErrors_InsufficientRoleError as InsufficientRoleError, authErrors_InvalidCredentialsError as InvalidCredentialsError, authErrors_InvalidKeyFingerprintError as InvalidKeyFingerprintError, authErrors_InvalidSignupLinkError as InvalidSignupLinkError, authErrors_InvalidSignupSetupSessionError as InvalidSignupSetupSessionError, authErrors_InvalidSocialTokenError as InvalidSocialTokenError, authErrors_InvalidTokenError as InvalidTokenError, authErrors_InvalidVerificationCodeError as InvalidVerificationCodeError, authErrors_InvalidVerificationTokenError as InvalidVerificationTokenError, authErrors_KeyAlgorithmMismatchError as KeyAlgorithmMismatchError, authErrors_KeyExpiredError as KeyExpiredError, authErrors_KeyIdAlreadyRegisteredError as KeyIdAlreadyRegisteredError, authErrors_KeyNotFoundError as KeyNotFoundError, authErrors_LastRecoveryCredentialError as LastRecoveryCredentialError, authErrors_MfaAlreadyEnrolledError as MfaAlreadyEnrolledError, authErrors_MfaConfigError as MfaConfigError, authErrors_MfaNotEnrolledError as MfaNotEnrolledError, authErrors_MfaVerificationFailedError as MfaVerificationFailedError, authErrors_NativeSignInUnsupportedError as NativeSignInUnsupportedError, authErrors_NonceKeyBindingError as NonceKeyBindingError, authErrors_OAuth2AuthorizeRedirectError as OAuth2AuthorizeRedirectError, authErrors_OAuth2GrantNotFoundError as OAuth2GrantNotFoundError, authErrors_OAuth2RedirectUriMismatchError as OAuth2RedirectUriMismatchError, authErrors_OAuth2UnknownClientError as OAuth2UnknownClientError, authErrors_PasskeyAlreadyRegisteredError as PasskeyAlreadyRegisteredError, authErrors_PasskeyChallengeError as PasskeyChallengeError, authErrors_PasskeyConfigError as PasskeyConfigError, authErrors_PasskeyNotFoundError as PasskeyNotFoundError, authErrors_PasskeyVerificationError as PasskeyVerificationError, authErrors_PasswordResetLinkError as PasswordResetLinkError, authErrors_PasswordResetSessionError as PasswordResetSessionError, authErrors_RecentAuthenticationRequiredError as RecentAuthenticationRequiredError, authErrors_RegistrationRejectedError as RegistrationRejectedError, authErrors_ReservedUsernameError as ReservedUsernameError, authErrors_RevokeAllLinkError as RevokeAllLinkError, authErrors_SessionBindingUnavailableError as SessionBindingUnavailableError, authErrors_SessionContextChangedError as SessionContextChangedError, authErrors_SessionPendingExpiredError as SessionPendingExpiredError, authErrors_SessionPendingMismatchError as SessionPendingMismatchError, authErrors_SessionRenewalRefusedError as SessionRenewalRefusedError, authErrors_SessionRenewalRequiredError as SessionRenewalRequiredError, authErrors_SessionResealFailedError as SessionResealFailedError, authErrors_StepUpRequiredError as StepUpRequiredError, authErrors_TokenExpiredError as TokenExpiredError, authErrors_UnverifiedEmailLinkError as UnverifiedEmailLinkError, authErrors_UsernameAlreadyTakenError as UsernameAlreadyTakenError, authErrors_VerificationTokenPurposeMismatchError as VerificationTokenPurposeMismatchError, authErrors_VerificationTokenTargetMismatchError as VerificationTokenTargetMismatchError };
991
1037
  }
992
1038
 
993
1039
  /**
@@ -996,4 +1042,4 @@ declare namespace authErrors {
996
1042
 
997
1043
  declare const authErrorRegistry: ErrorRegistry;
998
1044
 
999
- export { AccountAlreadyExistsError, AccountDisabledError, AccountPendingDeletionError, authErrors as AuthError, DeletionAlreadyRequestedError, DeletionNotRequestedError, DeviceAuthAlreadyHandledError, DeviceAuthDeniedError, DeviceAuthExpiredError, DeviceAuthNotFoundError, ImmediateDeletionNotAllowedError, InsufficientPermissionsError, InsufficientRoleError, InvalidCredentialsError, InvalidKeyFingerprintError, InvalidSignupLinkError, InvalidSignupSetupSessionError, InvalidSocialTokenError, InvalidTokenError, InvalidVerificationCodeError, InvalidVerificationTokenError, KeyAlgorithmMismatchError, KeyExpiredError, KeyIdAlreadyRegisteredError, KeyNotFoundError, LastRecoveryCredentialError, MfaAlreadyEnrolledError, MfaConfigError, MfaNotEnrolledError, MfaVerificationFailedError, NativeSignInUnsupportedError, NonceKeyBindingError, OAuth2AuthorizeRedirectError, OAuth2GrantNotFoundError, OAuth2RedirectUriMismatchError, OAuth2UnknownClientError, PasskeyAlreadyRegisteredError, PasskeyChallengeError, PasskeyConfigError, PasskeyNotFoundError, PasskeyVerificationError, PasswordResetLinkError, PasswordResetSessionError, RecentAuthenticationRequiredError, RegistrationRejectedError, ReservedUsernameError, RevokeAllLinkError, SessionBindingUnavailableError, SessionContextChangedError, SessionRenewalRefusedError, SessionRenewalRequiredError, SessionResealFailedError, StepUpRequiredError, TokenExpiredError, UnverifiedEmailLinkError, UsernameAlreadyTakenError, VerificationTokenPurposeMismatchError, VerificationTokenTargetMismatchError, authErrorRegistry };
1045
+ export { AccountAlreadyExistsError, AccountDisabledError, AccountPendingDeletionError, authErrors as AuthError, DeletionAlreadyRequestedError, DeletionNotRequestedError, DeviceAuthAlreadyHandledError, DeviceAuthDeniedError, DeviceAuthExpiredError, DeviceAuthNotFoundError, ImmediateDeletionNotAllowedError, InsufficientPermissionsError, InsufficientRoleError, InvalidCredentialsError, InvalidKeyFingerprintError, InvalidSignupLinkError, InvalidSignupSetupSessionError, InvalidSocialTokenError, InvalidTokenError, InvalidVerificationCodeError, InvalidVerificationTokenError, KeyAlgorithmMismatchError, KeyExpiredError, KeyIdAlreadyRegisteredError, KeyNotFoundError, LastRecoveryCredentialError, MfaAlreadyEnrolledError, MfaConfigError, MfaNotEnrolledError, MfaVerificationFailedError, NativeSignInUnsupportedError, NonceKeyBindingError, OAuth2AuthorizeRedirectError, OAuth2GrantNotFoundError, OAuth2RedirectUriMismatchError, OAuth2UnknownClientError, PasskeyAlreadyRegisteredError, PasskeyChallengeError, PasskeyConfigError, PasskeyNotFoundError, PasskeyVerificationError, PasswordResetLinkError, PasswordResetSessionError, RecentAuthenticationRequiredError, RegistrationRejectedError, ReservedUsernameError, RevokeAllLinkError, SessionBindingUnavailableError, SessionContextChangedError, SessionPendingExpiredError, SessionPendingMismatchError, SessionRenewalRefusedError, SessionRenewalRequiredError, SessionResealFailedError, StepUpRequiredError, TokenExpiredError, UnverifiedEmailLinkError, UsernameAlreadyTakenError, VerificationTokenPurposeMismatchError, VerificationTokenTargetMismatchError, authErrorRegistry };
package/dist/errors.js CHANGED
@@ -58,6 +58,8 @@ __export(auth_errors_exports, {
58
58
  RevokeAllLinkError: () => RevokeAllLinkError,
59
59
  SessionBindingUnavailableError: () => SessionBindingUnavailableError,
60
60
  SessionContextChangedError: () => SessionContextChangedError,
61
+ SessionPendingExpiredError: () => SessionPendingExpiredError,
62
+ SessionPendingMismatchError: () => SessionPendingMismatchError,
61
63
  SessionRenewalRefusedError: () => SessionRenewalRefusedError,
62
64
  SessionRenewalRequiredError: () => SessionRenewalRequiredError,
63
65
  SessionResealFailedError: () => SessionResealFailedError,
@@ -560,6 +562,26 @@ var StepUpRequiredError = class extends ForbiddenError {
560
562
  this.name = "StepUpRequiredError";
561
563
  }
562
564
  };
565
+ var SessionPendingMismatchError = class extends UnauthorizedError {
566
+ code = "SESSION_PENDING_MISMATCH";
567
+ constructor(data = {}) {
568
+ super({
569
+ message: data.message || "This browser did not start that sign-in. Sign in again.",
570
+ details: data.details
571
+ });
572
+ this.name = "SessionPendingMismatchError";
573
+ }
574
+ };
575
+ var SessionPendingExpiredError = class extends UnauthorizedError {
576
+ code = "SESSION_PENDING_EXPIRED";
577
+ constructor(data = {}) {
578
+ super({
579
+ message: data.message || "That sign-in took too long. Sign in again.",
580
+ details: data.details
581
+ });
582
+ this.name = "SessionPendingExpiredError";
583
+ }
584
+ };
563
585
  var MfaConfigError = class extends HttpError {
564
586
  constructor(data) {
565
587
  super({
@@ -630,7 +652,9 @@ authErrorRegistry.append([
630
652
  MfaNotEnrolledError,
631
653
  MfaVerificationFailedError,
632
654
  StepUpRequiredError,
633
- MfaConfigError
655
+ MfaConfigError,
656
+ SessionPendingMismatchError,
657
+ SessionPendingExpiredError
634
658
  ]);
635
659
  export {
636
660
  AccountAlreadyExistsError,
@@ -682,6 +706,8 @@ export {
682
706
  RevokeAllLinkError,
683
707
  SessionBindingUnavailableError,
684
708
  SessionContextChangedError,
709
+ SessionPendingExpiredError,
710
+ SessionPendingMismatchError,
685
711
  SessionRenewalRefusedError,
686
712
  SessionRenewalRequiredError,
687
713
  SessionResealFailedError,