@spfn/auth 0.3.0-beta.5 → 0.3.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -154,6 +154,8 @@ real secret values out of band, never commit them.
154
154
  | `SPFN_AUTH_OAUTH_SUCCESS_URL` | `.env.server` | — | default `/auth/callback` |
155
155
  | `SPFN_AUTH_OAUTH_ERROR_URL` | `.env.server` | — | default `/auth/error?error={error}` |
156
156
  | `SPFN_AUTH_RESERVED_USERNAMES` / `_USERNAME_MIN_LENGTH` / `_USERNAME_MAX_LENGTH` | `.env.server` | — | username rules |
157
+ | `SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES` / `_SETUP_TTL_MINUTES` | `.env.server` | — | defaults `30` / `15` — see [Verified-email signup](#verified-email-signup) |
158
+ | `SPFN_AUTH_SIGNUP_CONFIRM_PATH` | `.env.server` | — | default `/signup/confirm`; the page in your app the emailed link opens |
157
159
  | `NEXT_PUBLIC_SPFN_API_URL` / `NEXT_PUBLIC_SPFN_APP_URL` | `.env.local` | — | browser-facing URLs for OAuth redirects |
158
160
 
159
161
  Read validated values via `import { env } from '@spfn/auth/config'` (a proxy validated at
@@ -178,6 +180,9 @@ routes use `.skip(['auth'])`; the rest require `Authorization: Bearer <client-si
178
180
  | `sendVerificationCode` | POST `/_auth/codes` | public | send 6-digit OTP |
179
181
  | `verifyCode` | POST `/_auth/codes/verify` | public | verify OTP → verification token |
180
182
  | `register` | POST `/_auth/register` | public | create user + register public key |
183
+ | `requestSignupLink` | POST `/_auth/signup/email` | public | email a one-time signup confirmation link — see [Verified-email signup](#verified-email-signup) |
184
+ | `confirmSignupLink` | POST `/_auth/signup/email/confirm` | public | exchange the link for a password-setup session |
185
+ | `completeSignup` | POST `/_auth/signup/password` | setup session | set the password, which creates the account and signs in |
181
186
  | `login` | POST `/_auth/login` | public | password login + new session key |
182
187
  | `logout` | POST `/_auth/logout` | yes | revoke current key |
183
188
  | `rotateKey` | POST `/_auth/keys/rotate` | yes | rotate public key before 90-day expiry |
@@ -205,6 +210,83 @@ sends the public key on register/login, signs request JWTs locally, and the serv
205
210
  with the stored public key (`keyId` carried in the JWT). The server never holds a private key.
206
211
  Keys expire after 90 days — rotate with `rotateKey`.
207
212
 
213
+ ### Verified-email signup
214
+
215
+ A second way in, alongside the six-digit code. The address is proven before a password
216
+ exists, so nothing is stored for someone who never confirms.
217
+
218
+ ```
219
+ request → a one-time link is emailed
220
+ confirm → the link becomes a short-lived, HttpOnly password-setup session
221
+ password → the account is created, the device registered, the user signed in
222
+ ```
223
+
224
+ The six-digit-code path (`sendVerificationCode` → `verifyCode` → `register`) is unchanged.
225
+ Offer whichever suits your product, or both.
226
+
227
+ **1 — request the link.** The response is identical whether or not the address already has
228
+ an account, so it cannot be used to probe for accounts. When one exists, the owner gets a
229
+ "you already have an account" notice instead of a usable link.
230
+
231
+ ```typescript
232
+ await authApi.requestSignupLink.call({
233
+ body: { email: 'user@example.com', returnPath: '/welcome' }, // returnPath optional
234
+ });
235
+ // → { success: true, expiresAt }
236
+ ```
237
+
238
+ Calling it again is how a resend works: it invalidates the previous link and any setup
239
+ session opened from it. `returnPath` must be a path inside your app — absolute URLs,
240
+ `//host`, and `..` are refused, so the link cannot become an open redirect.
241
+
242
+ **2 — the page the link opens.** The email points at a page in *your* app
243
+ (`SPFN_AUTH_SIGNUP_CONFIRM_PATH`, default `/signup/confirm`), not at an API route. That page
244
+ reads the token from the query string and posts it:
245
+
246
+ ```typescript
247
+ 'use client';
248
+
249
+ const token = useSearchParams().get('token');
250
+
251
+ const { email, returnPath } = await authApi.confirmSignupLink.call({ body: { token } });
252
+
253
+ // Drop the token from the URL so it does not linger in history or a Referer header.
254
+ window.history.replaceState({}, '', window.location.pathname);
255
+ ```
256
+
257
+ The setup session comes back as an HttpOnly cookie — the proxy interceptor moves it there
258
+ and strips it from the response body, so page script never holds it. Serve this page with
259
+ `Referrer-Policy: no-referrer`.
260
+
261
+ **3 — set the password.** This is the step that creates the account. The setup cookie
262
+ authorizes it; the device keypair is injected by the interceptor exactly as it is for
263
+ `register`.
264
+
265
+ ```typescript
266
+ await authApi.completeSignup.call({ body: { password } });
267
+ // → { userId, publicId, email } + session cookie, same as register
268
+ ```
269
+
270
+ Creating the user, registering the device key, and marking the setup session used all commit
271
+ together. A password that fails the strength policy leaves the session usable, so the user
272
+ retypes rather than requesting a fresh email.
273
+
274
+ **Settings.**
275
+
276
+ | Variable | Default | Meaning |
277
+ |----------|---------|---------|
278
+ | `SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES` | `30` | how long the emailed link works |
279
+ | `SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES` | `15` | how long the password-setup session works |
280
+ | `SPFN_AUTH_SIGNUP_CONFIRM_PATH` | `/signup/confirm` | the page in your app the link opens |
281
+
282
+ The link URL is built on `NEXT_PUBLIC_SPFN_APP_URL || SPFN_APP_URL`, the same resolution the
283
+ OAuth callbacks use. Delivery uses the `signup-link` template in `@spfn/notification` —
284
+ override it there to change the copy.
285
+
286
+ **What is stored.** Only SHA-256 hashes of the link token and the setup secret, in
287
+ `spfn_auth.signup_link_tokens`. Neither credential is recoverable from the database, and
288
+ both are one-time: a link opens one setup session, and a setup session sets one password.
289
+
208
290
  ### Registered devices (key management)
209
291
 
210
292
  Keys are per-device, so a login never revokes the previous key and they accumulate on purpose.
@@ -1096,6 +1178,70 @@ app.post('/v1/echo', createClientProofGuard(state), (c) => { /* handler */ });
1096
1178
  Responses and errors MUST be canonical bytes with the contract envelope — build them with
1097
1179
  `encodeCanonicalJson`/`ClientProofRefusal`, never `c.json()` (key order and int64 differ).
1098
1180
 
1181
+ ## Custom auth profiles (`registerAuthProfile`)
1182
+
1183
+ `clientProofV1` is not a special case in the middleware — it is one entry in a registry
1184
+ `authenticate` and `optionalAuth` dispatch on. An app registers its own scheme the same way,
1185
+ without forking the middleware or wrapping it:
1186
+
1187
+ ```typescript
1188
+ import { registerAuthProfile, type AuthContext } from '@spfn/auth/server';
1189
+ import { UnauthorizedError } from '@spfn/core/errors';
1190
+
1191
+ // At boot — server.config.ts, before the server starts taking requests.
1192
+ registerAuthProfile('serviceTokenV1', {
1193
+ verify: async (c): Promise<AuthContext> =>
1194
+ {
1195
+ const user = await findServiceAccount(c.req.header('x-acme-service-token'));
1196
+ if (user === null)
1197
+ {
1198
+ // A refusal leaves the verifier as a throw. It reaches the app's
1199
+ // error handler exactly as the Bearer path's does.
1200
+ throw new UnauthorizedError({ message: 'Invalid service token' });
1201
+ }
1202
+
1203
+ return {
1204
+ user,
1205
+ userId: String(user.id),
1206
+ keyId: 'service-token',
1207
+ role: null,
1208
+ locale: 'en',
1209
+ scheme: 'serviceTokenV1',
1210
+ };
1211
+ },
1212
+ });
1213
+ ```
1214
+
1215
+ A request naming the profile is then answered by that verifier:
1216
+
1217
+ ```http
1218
+ POST /v1/reports
1219
+ x-spfn-auth-profile: serviceTokenV1
1220
+ x-acme-service-token: <the app's own credential>
1221
+ ```
1222
+
1223
+ - **Register at boot, before the first request.** The registry is read on every dispatch, so a
1224
+ profile registered later is simply a profile the requests before it did not have. Registration
1225
+ is not frozen after startup — it is a contract, not a runtime check.
1226
+ - **A duplicate name throws**, `clientProofV1` included. Replacing a registered verifier silently
1227
+ is how an import order or a copied profile name swaps the code that decides who is admitted, so
1228
+ there is no override — and no unregistration API for the same reason.
1229
+ - **The verifier must expose a callable `verify`**, and what it resolves must carry a `userId` — a
1230
+ verifier that cannot admit anyone is refused at boot, and a resolve without a principal (`null`,
1231
+ the JS idiom for "no user") is refused as a throw rather than routed as authenticated.
1232
+ - **An unknown profile is still refused** (`PROFILE_REJECTED`, 400): registering one name does not
1233
+ open the header to others.
1234
+ - **Mixing is still refused.** A request carrying both `x-spfn-auth-profile` and `Authorization` is
1235
+ rejected before either path runs; a custom verifier never sees it.
1236
+ - **A verifier's throw propagates**, and only the internal clientProofV1 contract refusal is
1237
+ answered with the canonical envelope. Under `optionalAuth` too: credentials that were presented
1238
+ and refused are never downgraded to anonymous passage — only "presented nothing" continues
1239
+ without an auth context.
1240
+ - **`AuthContext.scheme` is an open union** — `'bearer' | 'clientProofV1' | 'oneTimeToken' | (string
1241
+ & {})`. The built-in names keep their autocomplete and a registered profile names its own scheme.
1242
+ The field stays informational: downstream permission and tenant code takes one principal shape and
1243
+ never branches on how it was produced.
1244
+
1099
1245
  ## Account Deletion & Recovery
1100
1246
 
1101
1247
  Grace-period deletion with in-window recovery, an admin/GDPR-response entry point for immediate
@@ -214,6 +214,93 @@ declare function verifyCodeService(params: VerifyCodeParams): Promise<{
214
214
  verificationToken: string;
215
215
  }>;
216
216
 
217
+ /**
218
+ * @spfn/auth - Verified-Email Signup Service
219
+ *
220
+ * A signup where the address is proven before a password exists:
221
+ *
222
+ * request -> a one-time link is emailed
223
+ * confirm -> the link is exchanged for a short-lived password-setup session
224
+ * password -> the account is created, the device registered, the user signed in
225
+ *
226
+ * The link token and the setup secret are bearer credentials, so neither is ever
227
+ * stored. Only their SHA-256 hashes are, and lookup is by hash. A database dump
228
+ * therefore yields nothing that can be presented to either step.
229
+ *
230
+ * The six-digit-code registration path is untouched and remains the default; this
231
+ * is a second entry point to the same account creation, not a replacement.
232
+ */
233
+
234
+ /**
235
+ * Whether a return path can be handed back to the browser.
236
+ *
237
+ * Only a path within the app is allowed. The rejected shapes are the ones that
238
+ * turn a return path into an open redirect: an absolute URL, a protocol-relative
239
+ * `//host` that a browser reads as another origin, a backslash that some
240
+ * browsers normalize into a slash, and any `..` traversal.
241
+ */
242
+ declare function isSafeReturnPath(returnPath: string): boolean;
243
+ interface RequestSignupLinkParams {
244
+ email: string;
245
+ returnPath?: string;
246
+ }
247
+ interface RequestSignupLinkResult {
248
+ success: boolean;
249
+ expiresAt: string;
250
+ }
251
+ /**
252
+ * Step 1 — issue a confirmation link for an address.
253
+ *
254
+ * Answers identically whether or not the address already has an account. When it
255
+ * does, the owner gets a notice instead of a usable link, through the same
256
+ * dedupe window the six-digit-code path uses.
257
+ *
258
+ * Requesting again is how a resend works: every live link for the address is
259
+ * superseded first, so the newest link is the only one that opens, and any setup
260
+ * session already opened from an older link dies with it.
261
+ */
262
+ declare function requestSignupLinkService(params: RequestSignupLinkParams): Promise<RequestSignupLinkResult>;
263
+ interface ConfirmSignupLinkParams {
264
+ token: string;
265
+ }
266
+ interface ConfirmSignupLinkResult {
267
+ email: string;
268
+ returnPath: string | null;
269
+ /** Handed to the proxy interceptor, which moves it into an HttpOnly cookie. */
270
+ setupSecret: string;
271
+ setupExpiresAt: string;
272
+ }
273
+ /**
274
+ * Step 2 — exchange a link for a password-setup session.
275
+ *
276
+ * Nothing binds the row to a device or a browser, which is what lets someone
277
+ * request the link on a laptop and open it on a phone.
278
+ */
279
+ declare function confirmSignupLinkService(params: ConfirmSignupLinkParams): Promise<ConfirmSignupLinkResult>;
280
+ interface CompleteSignupParams {
281
+ setupSecret?: string;
282
+ password: string;
283
+ publicKey: string;
284
+ keyId: string;
285
+ fingerprint: string;
286
+ algorithm?: KeyAlgorithmType;
287
+ deviceName?: string;
288
+ platform?: KeyPlatformType;
289
+ metadata?: Record<string, unknown>;
290
+ }
291
+ /**
292
+ * Step 3 — set the password, which is what creates the account.
293
+ *
294
+ * Run under `Transactional()`: the user row, the device key and the completion
295
+ * mark commit together. A device-key failure must not leave an account nobody
296
+ * can sign into, and a completion mark must not survive a rolled-back account.
297
+ *
298
+ * A refusal that is the user's to fix — a weak password, an app policy that
299
+ * rejects the registration — leaves the setup session usable, so the fix is
300
+ * retyping the password rather than requesting a fresh email.
301
+ */
302
+ declare function completeSignupService(params: CompleteSignupParams): Promise<RegisterResult>;
303
+
217
304
  /**
218
305
  * @spfn/auth - Key Service
219
306
  *
@@ -814,6 +901,33 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
814
901
  platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
815
902
  }>;
816
903
  }, RegisterResult>;
904
+ requestSignupLink: _spfn_core_route.RouteDef<{
905
+ body: _sinclair_typebox.TObject<{
906
+ email: _sinclair_typebox.TString;
907
+ returnPath: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
908
+ }>;
909
+ }, {}, RequestSignupLinkResult>;
910
+ confirmSignupLink: _spfn_core_route.RouteDef<{
911
+ body: _sinclair_typebox.TObject<{
912
+ token: _sinclair_typebox.TString;
913
+ }>;
914
+ }, {}, ConfirmSignupLinkResult>;
915
+ completeSignup: _spfn_core_route.RouteDef<{
916
+ body: _sinclair_typebox.TObject<{
917
+ password: _sinclair_typebox.TString;
918
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
919
+ }>;
920
+ }, {
921
+ body: _sinclair_typebox.TObject<{
922
+ setupSecret: _sinclair_typebox.TString;
923
+ publicKey: _sinclair_typebox.TString;
924
+ keyId: _sinclair_typebox.TString;
925
+ fingerprint: _sinclair_typebox.TString;
926
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
927
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
928
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
929
+ }>;
930
+ }, RegisterResult>;
817
931
  login: _spfn_core_route.RouteDef<{
818
932
  body: _sinclair_typebox.TObject<{
819
933
  email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
@@ -879,7 +993,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
879
993
  id: number;
880
994
  name: string;
881
995
  displayName: string;
882
- category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
996
+ category: "custom" | "user" | "auth" | "rbac" | "system" | undefined;
883
997
  }[];
884
998
  userId: number;
885
999
  publicId: string;
@@ -1179,8 +1293,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
1179
1293
  }, {}, {
1180
1294
  roles: {
1181
1295
  description: string | null;
1182
- name: string;
1183
1296
  id: number;
1297
+ name: string;
1184
1298
  displayName: string;
1185
1299
  isBuiltin: boolean;
1186
1300
  isSystem: boolean;
@@ -1201,8 +1315,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
1201
1315
  }, {}, {
1202
1316
  role: {
1203
1317
  description: string | null;
1204
- name: string;
1205
1318
  id: number;
1319
+ name: string;
1206
1320
  displayName: string;
1207
1321
  isBuiltin: boolean;
1208
1322
  isSystem: boolean;
@@ -1225,8 +1339,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
1225
1339
  }, {}, {
1226
1340
  role: {
1227
1341
  description: string | null;
1228
- name: string;
1229
1342
  id: number;
1343
+ name: string;
1230
1344
  displayName: string;
1231
1345
  isBuiltin: boolean;
1232
1346
  isSystem: boolean;
@@ -1310,6 +1424,11 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
1310
1424
  * downstream permission/tenant code consumes one principal shape and never
1311
1425
  * branches on how it was authenticated.
1312
1426
  *
1427
+ * An app adds its own scheme with `registerAuthProfile` at boot. The dispatch
1428
+ * it joins is the one below, unchanged: a name nobody registered is still
1429
+ * refused, and profile credentials mixed with an Authorization header are
1430
+ * still refused before either path runs.
1431
+ *
1313
1432
  * The clientProofV1 verifier reuses the phase-1 admission pieces (header
1314
1433
  * shape, canonical body, proof-input assembly, ECDSA verification) with two
1315
1434
  * production substitutions: the key directory is `user_public_keys` via
@@ -1330,8 +1449,13 @@ interface AuthContext {
1330
1449
  keyId: string;
1331
1450
  role: string | null;
1332
1451
  locale: string;
1333
- /** How the principal was authenticated. Informational — downstream code never branches on it. */
1334
- scheme: 'bearer' | 'clientProofV1' | 'oneTimeToken';
1452
+ /**
1453
+ * How the principal was authenticated. Informational — downstream code
1454
+ * never branches on it. The union stays open for the profiles an app
1455
+ * registers itself: the built-in names keep their autocomplete, and a
1456
+ * registered profile names its own scheme without editing this file.
1457
+ */
1458
+ scheme: 'bearer' | 'clientProofV1' | 'oneTimeToken' | (string & {});
1335
1459
  }
1336
1460
  /** A profile's verifier: admits the request and returns the principal, or throws. */
1337
1461
  interface AuthProfileVerifier {
@@ -1378,6 +1502,49 @@ type AuthProfileOutcome = {
1378
1502
  * handed to the generic error handler is classified by its class name instead.
1379
1503
  */
1380
1504
  declare function runAuthProfile(c: Context): Promise<AuthProfileOutcome>;
1505
+ /**
1506
+ * Registers an app's own verifier under a profile name.
1507
+ *
1508
+ * Call it at boot, before the first request: the registry is a module-global
1509
+ * read on every dispatch, so a profile registered later is simply a profile
1510
+ * the requests before it did not have. There is no freeze and no
1511
+ * unregistration — an auth surface that can be rearranged at runtime is a
1512
+ * surface an app bug can rearrange.
1513
+ *
1514
+ * A duplicate name throws rather than replacing the verifier that holds it,
1515
+ * `clientProofV1` included. A silent override is how a second import order, or
1516
+ * a copied profile name, quietly swaps the code that decides who is admitted.
1517
+ *
1518
+ * The verifier must expose a callable `verify` — a value that cannot admit
1519
+ * anyone is refused at boot rather than becoming a registry entry the dispatch
1520
+ * reads as "no profile header", which is anonymous passage under
1521
+ * `optionalAuth` for a request that presented profile credentials.
1522
+ *
1523
+ * The verifier returns the same `AuthContext` the Bearer path sets and refuses
1524
+ * by throwing. A resolve that carries no `userId` is refused as a throw too —
1525
+ * "no user" is a refusal, never a principal. A throw is not caught here:
1526
+ * `runAuthProfile` answers the internal clientProofV1 contract refusal and
1527
+ * nothing else, so a verifier's own error reaches the app's generic error
1528
+ * handler exactly as the Bearer path's `UnauthorizedError` does — and never
1529
+ * becomes anonymous passage, not even under `optionalAuth`.
1530
+ *
1531
+ * @example
1532
+ * ```typescript
1533
+ * registerAuthProfile('serviceTokenV1', {
1534
+ * verify: async (c) =>
1535
+ * {
1536
+ * const user = await authenticateServiceToken(c.req.header('x-acme-service-token'));
1537
+ * if (user === null)
1538
+ * {
1539
+ * throw new UnauthorizedError({ message: 'Invalid service token' });
1540
+ * }
1541
+ *
1542
+ * return { user, userId: String(user.id), keyId: 'service', role: null, locale: 'en', scheme: 'serviceTokenV1' };
1543
+ * },
1544
+ * });
1545
+ * ```
1546
+ */
1547
+ declare function registerAuthProfile(profileId: string, verifier: AuthProfileVerifier): void;
1381
1548
 
1382
1549
  declare module 'hono' {
1383
1550
  interface ContextVariableMap {
@@ -1444,4 +1611,4 @@ declare const authenticate: _spfn_core_route.NamedMiddleware<"auth">;
1444
1611
  */
1445
1612
  declare const optionalAuth: _spfn_core_route.NamedMiddleware<"optionalAuth">;
1446
1613
 
1447
- export { type UnlinkNotifyResult as $, type AuthInitOptions as A, PasswordSchema as B, type ChangePasswordParams as C, DeviceNameSchema as D, EmailSchema as E, PhoneSchema as F, PlatformSchema as G, type RegisterParams as H, type IssueOneTimeTokenResult as I, type RegisterPublicKeyParams as J, type KeySummary as K, type LoginResult as L, type RevokeAllKeysParams as M, type NativeVerifyOptions as N, type OAuthStartResult as O, type PermissionConfig as P, type RevokeKeyParams as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type RotateKeyParams as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type SendVerificationCodeParams as W, TargetTypeSchema as X, type UnlinkNotification as Y, UnlinkNotifyRejection as Z, type UnlinkNotifyRequest as _, type RegisterResult as a, VerificationPurposeSchema as a0, type VerifyCodeParams as a1, type VerifyCodeResult as a2, authenticate as a3, buildOAuthErrorUrl as a4, changePasswordService as a5, getEnabledOAuthProviders as a6, getGoogleAccessToken as a7, getOAuthProvider as a8, getRegisteredProviders as a9, isOAuthProviderEnabled as aa, issueOneTimeTokenService as ab, listKeysService as ac, loginService as ad, logoutService as ae, oauthCallbackService as af, oauthNativeService as ag, oauthStartService as ah, oauthUnlinkNotifyService as ai, optionalAuth as aj, registerOAuthProvider as ak, registerPublicKeyService as al, registerService as am, requireEnabledProvider as an, resolveAuthenticatedUser as ao, revokeAllKeysService as ap, revokeKeyService as aq, rotateKeyService as ar, runAuthProfile as as, selectAuthProfile as at, sendVerificationCodeService as au, verifyCodeService as av, verifyOneTimeTokenService as aw, type RotateKeyResult as b, type RevokeAllKeysResult as c, type OAuthNativeResult as d, type ProfileInfo as e, type AuthSession as f, PERMISSION_CATEGORIES as g, type PermissionCategory as h, VERIFICATION_TARGET_TYPES as i, type VerificationPurpose as j, type VerificationTargetType as k, type OAuthProvider as l, mainAuthRouter as m, type AuthContext as n, type AuthProfileOutcome as o, type AuthProfileVerifier as p, KEY_FINGERPRINT_PREFIX_LENGTH as q, type LoginParams as r, type LogoutParams as s, type NormalizedIdentity as t, type OAuthCallbackParams as u, type OAuthCallbackResult as v, type OAuthCodeExchangeOptions as w, type OAuthNativeParams as x, type OAuthStartParams as y, type OAuthTokens as z };
1614
+ export { type SendVerificationCodeParams as $, type AuthInitOptions as A, type OAuthCodeExchangeOptions as B, type ConfirmSignupLinkResult as C, DeviceNameSchema as D, EmailSchema as E, type OAuthNativeParams as F, type OAuthStartParams as G, type OAuthTokens as H, type IssueOneTimeTokenResult as I, PasswordSchema as J, type KeySummary as K, type LoginResult as L, PhoneSchema as M, type NativeVerifyOptions as N, type OAuthStartResult as O, type PermissionConfig as P, PlatformSchema as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type RegisterParams as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type RegisterPublicKeyParams as W, type RequestSignupLinkParams as X, type RevokeAllKeysParams as Y, type RevokeKeyParams as Z, type RotateKeyParams as _, type RegisterResult as a, TargetTypeSchema as a0, type UnlinkNotification as a1, UnlinkNotifyRejection as a2, type UnlinkNotifyRequest as a3, type UnlinkNotifyResult as a4, VerificationPurposeSchema as a5, type VerifyCodeParams as a6, type VerifyCodeResult as a7, authenticate as a8, buildOAuthErrorUrl as a9, revokeKeyService as aA, rotateKeyService as aB, runAuthProfile as aC, selectAuthProfile as aD, sendVerificationCodeService as aE, verifyCodeService as aF, verifyOneTimeTokenService as aG, changePasswordService as aa, completeSignupService as ab, confirmSignupLinkService as ac, getEnabledOAuthProviders as ad, getGoogleAccessToken as ae, getOAuthProvider as af, getRegisteredProviders as ag, isOAuthProviderEnabled as ah, isSafeReturnPath as ai, issueOneTimeTokenService as aj, listKeysService as ak, loginService as al, logoutService as am, oauthCallbackService as an, oauthNativeService as ao, oauthStartService as ap, oauthUnlinkNotifyService as aq, optionalAuth as ar, registerAuthProfile as as, registerOAuthProvider as at, registerPublicKeyService as au, registerService as av, requestSignupLinkService as aw, requireEnabledProvider as ax, resolveAuthenticatedUser as ay, revokeAllKeysService as az, type RequestSignupLinkResult as b, type RotateKeyResult as c, type RevokeAllKeysResult as d, type OAuthNativeResult as e, type ProfileInfo as f, type AuthSession as g, PERMISSION_CATEGORIES as h, type PermissionCategory as i, VERIFICATION_TARGET_TYPES as j, type VerificationPurpose as k, type VerificationTargetType as l, mainAuthRouter as m, type OAuthProvider as n, type AuthContext as o, type AuthProfileOutcome as p, type AuthProfileVerifier as q, type ChangePasswordParams as r, type CompleteSignupParams as s, type ConfirmSignupLinkParams as t, KEY_FINGERPRINT_PREFIX_LENGTH as u, type LoginParams as v, type LogoutParams as w, type NormalizedIdentity as x, type OAuthCallbackParams as y, type OAuthCallbackResult as z };
package/dist/config.d.ts CHANGED
@@ -195,6 +195,36 @@ declare const authEnvSchema: {
195
195
  } & {
196
196
  key: "SPFN_AUTH_USERNAME_MAX_LENGTH";
197
197
  };
198
+ SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES: {
199
+ description: string;
200
+ default: number;
201
+ required: boolean;
202
+ examples: number[];
203
+ type: "number";
204
+ validator: (value: string) => number;
205
+ } & {
206
+ key: "SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES";
207
+ };
208
+ SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES: {
209
+ description: string;
210
+ default: number;
211
+ required: boolean;
212
+ examples: number[];
213
+ type: "number";
214
+ validator: (value: string) => number;
215
+ } & {
216
+ key: "SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES";
217
+ };
218
+ SPFN_AUTH_SIGNUP_CONFIRM_PATH: {
219
+ description: string;
220
+ default: string;
221
+ required: boolean;
222
+ examples: string[];
223
+ type: "string";
224
+ validator: (value: string) => string;
225
+ } & {
226
+ key: "SPFN_AUTH_SIGNUP_CONFIRM_PATH";
227
+ };
198
228
  SPFN_API_URL: {
199
229
  description: string;
200
230
  default: string;
@@ -609,6 +639,36 @@ declare const env: _spfn_core_env.InferEnvType<{
609
639
  } & {
610
640
  key: "SPFN_AUTH_USERNAME_MAX_LENGTH";
611
641
  };
642
+ SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES: {
643
+ description: string;
644
+ default: number;
645
+ required: boolean;
646
+ examples: number[];
647
+ type: "number";
648
+ validator: (value: string) => number;
649
+ } & {
650
+ key: "SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES";
651
+ };
652
+ SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES: {
653
+ description: string;
654
+ default: number;
655
+ required: boolean;
656
+ examples: number[];
657
+ type: "number";
658
+ validator: (value: string) => number;
659
+ } & {
660
+ key: "SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES";
661
+ };
662
+ SPFN_AUTH_SIGNUP_CONFIRM_PATH: {
663
+ description: string;
664
+ default: string;
665
+ required: boolean;
666
+ examples: string[];
667
+ type: "string";
668
+ validator: (value: string) => string;
669
+ } & {
670
+ key: "SPFN_AUTH_SIGNUP_CONFIRM_PATH";
671
+ };
612
672
  SPFN_API_URL: {
613
673
  description: string;
614
674
  default: string;
package/dist/config.js CHANGED
@@ -201,6 +201,33 @@ var authEnvSchema = defineEnvSchema({
201
201
  })
202
202
  },
203
203
  // ============================================================================
204
+ // Verified-email signup
205
+ // ============================================================================
206
+ SPFN_AUTH_SIGNUP_LINK_TTL_MINUTES: {
207
+ ...envNumber({
208
+ 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.",
209
+ default: 30,
210
+ required: false,
211
+ examples: [15, 30, 60]
212
+ })
213
+ },
214
+ SPFN_AUTH_SIGNUP_SETUP_TTL_MINUTES: {
215
+ ...envNumber({
216
+ 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.",
217
+ default: 15,
218
+ required: false,
219
+ examples: [10, 15, 30]
220
+ })
221
+ },
222
+ SPFN_AUTH_SIGNUP_CONFIRM_PATH: {
223
+ ...envString({
224
+ 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.",
225
+ default: "/signup/confirm",
226
+ required: false,
227
+ examples: ["/signup/confirm", "/auth/confirm", "/join/verify"]
228
+ })
229
+ },
230
+ // ============================================================================
204
231
  // API Configuration
205
232
  // ============================================================================
206
233
  SPFN_API_URL: {