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

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.
@@ -1,6 +1,6 @@
1
- import * as _simplewebauthn_server from '@simplewebauthn/server';
2
- import { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/server';
3
1
  import { S as SessionBindingType, K as KeyAlgorithmType, h as KeyPlatformType, l as SocialProvider } from './types-CTdoTOxM.js';
2
+ import * as _simplewebauthn_server from '@simplewebauthn/server';
3
+ import { RegistrationResponseJSON, AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
4
4
  import * as _spfn_core_route from '@spfn/core/route';
5
5
  import * as _sinclair_typebox from '@sinclair/typebox';
6
6
  import { Static } from '@sinclair/typebox';
@@ -91,288 +91,69 @@ interface UserProfile {
91
91
  }
92
92
 
93
93
  /**
94
- * @spfn/auth - Passkeys Entity
94
+ * @spfn/auth - What a sign-in answers with
95
95
  *
96
- * A WebAuthn credential the account owner enrolled on one of their devices.
97
- * It is a *credential*, not a session: an assertion proves who is asking, and
98
- * the ordinary device key in `user_public_keys` is what the request afterwards
99
- * is signed with. The two tables therefore never stand in for each other.
96
+ * One module for the shape every path that starts a session returns, and for the
97
+ * helper that fills in its binding half. It is here rather than in
98
+ * `auth.service.ts` because three of its readers are upstream of that file —
99
+ * `key.service` decides the second-factor step-up, `mfa.service` resolves it —
100
+ * and a type living with one of its producers would put those services in a
101
+ * cycle with each other.
102
+ */
103
+
104
+ /**
105
+ * The second-factor challenge a stepped-up sign-in hands back (#95).
100
106
  *
101
- * Nothing here is a bearer value, so nothing is hashed. `publicKey` is public by
102
- * construction and `credentialId` is a handle the authenticator hands to any
103
- * origin that asks — storing either in the clear costs nothing, and the lookup
104
- * on `credentialId` has to be a plain equality match on an indexed column.
107
+ * `secret` is the 32 random bytes the challenge was minted from, and it is the
108
+ * only form of it that ever leaves the server — the row is addressed by its
109
+ * hash. It authorizes exactly one thing, `POST /_auth/mfa/verify` for this one
110
+ * registration, and it is not a bearer credential for anything else.
111
+ */
112
+ interface MfaChallengeHandle {
113
+ secret: string;
114
+ /** Epoch milliseconds the challenge stops verifying at. */
115
+ expiresAtMillis: number;
116
+ }
117
+ /**
118
+ * What a sign-in answers with, on every path that starts a session.
105
119
  *
106
- * Revocation is soft, and `credentialId` stays unique across live and revoked
107
- * rows alike: a credential someone cut off must never become enrollable again,
108
- * on this account or on another one.
120
+ * **One type with a required discriminant, not a union.** A sign-in on an
121
+ * account with a second factor and a device it has never seen answers 202 with
122
+ * `mfaRequired: true` and a challenge instead of a session (#95), and the two
123
+ * answers have to be one declared type: `authApi.login` infers its result from
124
+ * this declaration, so a union would make every existing `result.userId` in
125
+ * every consuming app stop compiling, and the mobile contract's grammar has no
126
+ * union type either — `DeviceAuthPollResponse` was flattened the same way and
127
+ * for the same reason. Narrow on `mfaRequired` before reading `userId`.
128
+ *
129
+ * `sessionBinding` and `keyExpiresAtMillis` are the carrier #97 needed. The
130
+ * Next.js proxy generated the device key and sealed the cookie, but only the
131
+ * backend knows whether the account asked for a bound session and when the key
132
+ * it just registered runs out — so the sign-in says it here and the interceptor
133
+ * copies both into `SessionData`. A response without them seals an unbound
134
+ * session, which is what every account that did not opt in gets and what every
135
+ * path predating that change keeps getting.
109
136
  */
137
+ interface LoginResult {
138
+ /** true means no session was started: verify the challenge below first. */
139
+ mfaRequired: boolean;
140
+ /** Present exactly when `mfaRequired` is true. */
141
+ challenge?: MfaChallengeHandle;
142
+ userId?: string;
143
+ publicId?: string;
144
+ email?: string;
145
+ phone?: string;
146
+ passwordChangeRequired?: boolean;
147
+ /** `'passkey'` when the key registered by this sign-in is bound. Absent otherwise. */
148
+ sessionBinding?: SessionBindingType;
149
+ /** Epoch milliseconds that key expires at. Only sent alongside `sessionBinding`. */
150
+ keyExpiresAtMillis?: number;
151
+ }
152
+
110
153
  /**
111
- * Whether the credential can leave the authenticator that minted it.
154
+ * @spfn/auth - Auth Service
112
155
  *
113
- * `multiDevice` is a synced passkey (iCloud Keychain, Google Password Manager);
114
- * `singleDevice` is bound to one authenticator. Reported by the authenticator at
115
- * enrollment and shown in the management list, because "this one is only on that
116
- * phone" is what the owner needs to know before revoking the other entry.
117
- */
118
- declare const PASSKEY_DEVICE_TYPES: readonly ["singleDevice", "multiDevice"];
119
- type PasskeyDeviceType = typeof PASSKEY_DEVICE_TYPES[number];
120
- /** How long a label may be — the key list's `deviceName` bound, for the same reason. */
121
- declare const PASSKEY_LABEL_MAX_LENGTH = 64;
122
- declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
123
- name: "passkeys";
124
- schema: string;
125
- columns: {
126
- createdAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>, {
127
- name: string;
128
- tableName: "passkeys";
129
- dataType: "object date";
130
- data: Date;
131
- driverParam: string;
132
- notNull: true;
133
- hasDefault: true;
134
- isPrimaryKey: false;
135
- isAutoincrement: false;
136
- hasRuntimeDefault: false;
137
- enumValues: undefined;
138
- identity: undefined;
139
- generated: undefined;
140
- }>;
141
- updatedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>>, {
142
- name: string;
143
- tableName: "passkeys";
144
- dataType: "object date";
145
- data: Date;
146
- driverParam: string;
147
- notNull: true;
148
- hasDefault: true;
149
- isPrimaryKey: false;
150
- isAutoincrement: false;
151
- hasRuntimeDefault: false;
152
- enumValues: undefined;
153
- identity: undefined;
154
- generated: undefined;
155
- }>;
156
- id: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
157
- name: string;
158
- tableName: "passkeys";
159
- dataType: "number int53";
160
- data: number;
161
- driverParam: number;
162
- notNull: true;
163
- hasDefault: true;
164
- isPrimaryKey: false;
165
- isAutoincrement: false;
166
- hasRuntimeDefault: false;
167
- enumValues: undefined;
168
- identity: undefined;
169
- generated: undefined;
170
- }>;
171
- userId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
172
- name: string;
173
- tableName: "passkeys";
174
- dataType: "number int53";
175
- data: number;
176
- driverParam: string | number;
177
- notNull: true;
178
- hasDefault: false;
179
- isPrimaryKey: false;
180
- isAutoincrement: false;
181
- hasRuntimeDefault: false;
182
- enumValues: undefined;
183
- identity: undefined;
184
- generated: undefined;
185
- }>;
186
- credentialId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
187
- name: string;
188
- tableName: "passkeys";
189
- dataType: "string";
190
- data: string;
191
- driverParam: string;
192
- notNull: true;
193
- hasDefault: false;
194
- isPrimaryKey: false;
195
- isAutoincrement: false;
196
- hasRuntimeDefault: false;
197
- enumValues: undefined;
198
- identity: undefined;
199
- generated: undefined;
200
- }>;
201
- publicKey: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
202
- name: string;
203
- tableName: "passkeys";
204
- dataType: "string";
205
- data: string;
206
- driverParam: string;
207
- notNull: true;
208
- hasDefault: false;
209
- isPrimaryKey: false;
210
- isAutoincrement: false;
211
- hasRuntimeDefault: false;
212
- enumValues: undefined;
213
- identity: undefined;
214
- generated: undefined;
215
- }>;
216
- counter: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgIntegerBuilder>>, {
217
- name: string;
218
- tableName: "passkeys";
219
- dataType: "number int32";
220
- data: number;
221
- driverParam: string | number;
222
- notNull: true;
223
- hasDefault: true;
224
- isPrimaryKey: false;
225
- isAutoincrement: false;
226
- hasRuntimeDefault: false;
227
- enumValues: undefined;
228
- identity: undefined;
229
- generated: undefined;
230
- }>;
231
- transports: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetDimensions<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, 1>, {
232
- name: string;
233
- tableName: "passkeys";
234
- dataType: "string";
235
- data: string[];
236
- driverParam: string | string[];
237
- notNull: false;
238
- hasDefault: false;
239
- isPrimaryKey: false;
240
- isAutoincrement: false;
241
- hasRuntimeDefault: false;
242
- enumValues: undefined;
243
- identity: undefined;
244
- generated: undefined;
245
- }>;
246
- deviceType: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["singleDevice", "multiDevice"] & [string, ...string[]]>>, {
247
- name: string;
248
- tableName: "passkeys";
249
- dataType: "string enum";
250
- data: "singleDevice" | "multiDevice";
251
- driverParam: string;
252
- notNull: true;
253
- hasDefault: false;
254
- isPrimaryKey: false;
255
- isAutoincrement: false;
256
- hasRuntimeDefault: false;
257
- enumValues: ["singleDevice", "multiDevice"] & [string, ...string[]];
258
- identity: undefined;
259
- generated: undefined;
260
- }>;
261
- backedUp: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
262
- name: string;
263
- tableName: "passkeys";
264
- dataType: "boolean";
265
- data: boolean;
266
- driverParam: boolean;
267
- notNull: true;
268
- hasDefault: true;
269
- isPrimaryKey: false;
270
- isAutoincrement: false;
271
- hasRuntimeDefault: false;
272
- enumValues: undefined;
273
- identity: undefined;
274
- generated: undefined;
275
- }>;
276
- aaguid: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
277
- name: string;
278
- tableName: "passkeys";
279
- dataType: "string";
280
- data: string;
281
- driverParam: string;
282
- notNull: false;
283
- hasDefault: false;
284
- isPrimaryKey: false;
285
- isAutoincrement: false;
286
- hasRuntimeDefault: false;
287
- enumValues: undefined;
288
- identity: undefined;
289
- generated: undefined;
290
- }>;
291
- label: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
292
- name: string;
293
- tableName: "passkeys";
294
- dataType: "string";
295
- data: string;
296
- driverParam: string;
297
- notNull: false;
298
- hasDefault: false;
299
- isPrimaryKey: false;
300
- isAutoincrement: false;
301
- hasRuntimeDefault: false;
302
- enumValues: undefined;
303
- identity: undefined;
304
- generated: undefined;
305
- }>;
306
- secondFactor: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
307
- name: string;
308
- tableName: "passkeys";
309
- dataType: "boolean";
310
- data: boolean;
311
- driverParam: boolean;
312
- notNull: true;
313
- hasDefault: true;
314
- isPrimaryKey: false;
315
- isAutoincrement: false;
316
- hasRuntimeDefault: false;
317
- enumValues: undefined;
318
- identity: undefined;
319
- generated: undefined;
320
- }>;
321
- lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
322
- name: string;
323
- tableName: "passkeys";
324
- dataType: "object date";
325
- data: Date;
326
- driverParam: string;
327
- notNull: false;
328
- hasDefault: false;
329
- isPrimaryKey: false;
330
- isAutoincrement: false;
331
- hasRuntimeDefault: false;
332
- enumValues: undefined;
333
- identity: undefined;
334
- generated: undefined;
335
- }>;
336
- revokedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
337
- name: string;
338
- tableName: "passkeys";
339
- dataType: "object date";
340
- data: Date;
341
- driverParam: string;
342
- notNull: false;
343
- hasDefault: false;
344
- isPrimaryKey: false;
345
- isAutoincrement: false;
346
- hasRuntimeDefault: false;
347
- enumValues: undefined;
348
- identity: undefined;
349
- generated: undefined;
350
- }>;
351
- revokedReason: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
352
- name: string;
353
- tableName: "passkeys";
354
- dataType: "string";
355
- data: string;
356
- driverParam: string;
357
- notNull: false;
358
- hasDefault: false;
359
- isPrimaryKey: false;
360
- isAutoincrement: false;
361
- hasRuntimeDefault: false;
362
- enumValues: undefined;
363
- identity: undefined;
364
- generated: undefined;
365
- }>;
366
- };
367
- dialect: "pg";
368
- }>;
369
- type Passkey = typeof passkeys.$inferSelect;
370
- type NewPasskey = typeof passkeys.$inferInsert;
371
-
372
- /**
373
- * @spfn/auth - Auth Service
374
- *
375
- * Core authentication logic: registration, login, logout, password management
156
+ * Core authentication logic: registration, login, logout, password management
376
157
  */
377
158
 
378
159
  interface RegisterParams {
@@ -422,36 +203,7 @@ interface LoginParams {
422
203
  /** Whether proxy-guard recognised the trusted Next.js proxy, from the same helper. */
423
204
  webProxy?: boolean;
424
205
  }
425
- /**
426
- * What a sign-in answers with, on every path that starts a session.
427
- *
428
- * The last two fields are the carrier #97 needed. The Next.js proxy generated
429
- * the device key and sealed the cookie, but only the backend knows whether the
430
- * account asked for a bound session and when the key it just registered runs
431
- * out — so the sign-in says it here and the interceptor copies both into
432
- * `SessionData`. A response without them seals an unbound session, which is what
433
- * every account that did not opt in gets and what every path predating this
434
- * change keeps getting.
435
- */
436
- interface LoginResult {
437
- userId: string;
438
- publicId: string;
439
- email?: string;
440
- phone?: string;
441
- passwordChangeRequired: boolean;
442
- /** `'passkey'` when the key registered by this sign-in is bound. Absent otherwise. */
443
- sessionBinding?: SessionBindingType;
444
- /** Epoch milliseconds that key expires at. Only sent alongside `sessionBinding`. */
445
- keyExpiresAtMillis?: number;
446
- }
447
- /**
448
- * The binding half of a sign-in answer, as a type.
449
- *
450
- * Named because more than one result carries it: a password reset registers a
451
- * device key exactly as a sign-in does, so its answer has to say so too or the
452
- * proxy seals a cookie that does not know the key it holds is short-lived.
453
- */
454
- type LoginBindingFields = Pick<LoginResult, 'sessionBinding' | 'keyExpiresAtMillis'>;
206
+
455
207
  interface LogoutParams {
456
208
  userId: number;
457
209
  keyId: string;
@@ -492,94 +244,6 @@ declare function logoutService(params: LogoutParams): Promise<void>;
492
244
  */
493
245
  declare function changePasswordService(params: ChangePasswordParams): Promise<void>;
494
246
 
495
- /**
496
- * @spfn/auth - Session Renewal Service
497
- *
498
- * What a bound session does when its key runs out: prove, with a fresh WebAuthn
499
- * assertion, that the person who enrolled the passkey is still at the machine,
500
- * and get a new short-lived key sealed into the cookie.
501
- *
502
- * Neither step is public. The expiring key is named by `expiredKeyId`, and that
503
- * value reaches the service from `authenticateForRenewal` — the `keyId` of a
504
- * bearer JWT this very key signed — rather than from the request body, so a
505
- * caller who does not hold the private half cannot name a key at all. The
506
- * assertion still has to be signed by a passkey that key's owner enrolled: the
507
- * signature proves the cookie, and the cookie is the thing that may have been
508
- * copied.
509
- *
510
- * The admission below is run again here all the same. The middleware and the
511
- * service ask the same four questions of the row, and a service that trusted its
512
- * caller to have asked them would be one refactor away from not being asked at
513
- * all.
514
- *
515
- * Every refusal is the same refusal. A key that never existed, a stranger's key,
516
- * an unbound key, a revoked one, one past its grace, an inactive account, a spent
517
- * challenge, an assertion that did not verify — all `SessionRenewalRefusedError`,
518
- * with the same body, because anything finer would answer "is this key id live"
519
- * to whoever asked.
520
- *
521
- * Renewal announces nothing. No `auth.login`, no `auth.device.registered`, and
522
- * `lastLoginAt` does not move: this is the same person on the same device
523
- * continuing the session they already had, and a subscriber mailing "new sign-in"
524
- * once a day per device would train its reader to ignore the notice that matters.
525
- * A `lastLoginAt` that moved every day would make dormant-account detection
526
- * meaningless for exactly the accounts that turned this protection on.
527
- */
528
-
529
- interface StartSessionRenewParams {
530
- /** The key that ran out, read off the JWT the request was signed with. */
531
- expiredKeyId: string;
532
- }
533
- interface FinishSessionRenewParams extends StartSessionRenewParams {
534
- /** The assertion, from `navigator.credentials.get()`. */
535
- response: AuthenticationResponseJSON;
536
- /**
537
- * The new key pair, in the vocabulary the Next.js login interceptor already
538
- * writes: `renew/verify` is on that interceptor's path list, so these arrive
539
- * exactly as they do on a login.
540
- */
541
- keyId: string;
542
- publicKey: string;
543
- fingerprint: string;
544
- algorithm?: KeyAlgorithmType;
545
- }
546
- /**
547
- * What a completed renewal answers: a sign-in result, plus the new key's id.
548
- *
549
- * The id is the one thing a renewal has that a sign-in does not need to say —
550
- * `renewSession()` promises it to the app, which has no other way to learn it
551
- * (the key pair is minted in the proxy and the private half never leaves the
552
- * cookie). It is not a contract operation, so nothing generated reads it.
553
- */
554
- interface SessionRenewResult extends LoginResult {
555
- /** The key this renewal registered, the one the session now signs with. */
556
- keyId: string;
557
- }
558
- /**
559
- * Step 1 — the challenge the authenticator signs.
560
- *
561
- * `allowCredentials` is empty and the account lives only on the challenge row.
562
- * See `startRenewalCeremonyService`.
563
- *
564
- * @throws SessionRenewalRefusedError 갱신할 수 없는 키·계정일 때 (모든 사유 동일)
565
- */
566
- declare function startSessionRenewService(params: StartSessionRenewParams): Promise<PublicKeyCredentialRequestOptionsJSON>;
567
- /**
568
- * Step 2 — verify the assertion, put a new bound key in place of the old one.
569
- *
570
- * The revocation runs first and its answer is the race winner: two verifies that
571
- * both got past their own challenges meet at the same conditional UPDATE, and
572
- * only the one that actually revoked the key goes on to register a replacement.
573
- *
574
- * The new key inherits the old row's provenance, so the device list keeps saying
575
- * where this device first appeared rather than re-stamping itself every day. Its
576
- * expiry is a fresh window from now — renewal is a renewal, not an extension of
577
- * what the old key had.
578
- *
579
- * @throws SessionRenewalRefusedError 갱신할 수 없을 때 (증명 실패 포함, 모든 사유 동일)
580
- */
581
- declare function finishSessionRenewService(params: FinishSessionRenewParams): Promise<SessionRenewResult>;
582
-
583
247
  declare const EmailSchema: _sinclair_typebox.TString;
584
248
  declare const PhoneSchema: _sinclair_typebox.TString;
585
249
  /**
@@ -652,6 +316,7 @@ declare const DeviceAuthPollResponseSchema: _sinclair_typebox.TUnion<[_sinclair_
652
316
  intervalMillis: _sinclair_typebox.TInteger;
653
317
  }>, _sinclair_typebox.TObject<{
654
318
  status: _sinclair_typebox.TLiteral<"approved">;
319
+ mfaRequired: _sinclair_typebox.TBoolean;
655
320
  userId: _sinclair_typebox.TString;
656
321
  publicId: _sinclair_typebox.TString;
657
322
  email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
@@ -898,7 +563,7 @@ interface CompletePasswordResetParams {
898
563
  * believing the session unbound: no user-agent check for the life of that key,
899
564
  * and a sign-out a day later instead of the renewal prompt.
900
565
  */
901
- declare function completePasswordResetService(params: CompletePasswordResetParams): Promise<RegisterResult & LoginBindingFields>;
566
+ declare function completePasswordResetService(params: CompletePasswordResetParams): Promise<LoginResult>;
902
567
 
903
568
  /**
904
569
  * @spfn/auth - Device Auth Service
@@ -986,10 +651,22 @@ interface DeviceAuthPendingResult {
986
651
  status: 'pending';
987
652
  intervalMillis: number;
988
653
  }
989
- /** Approved and spent: the key is registered and this is the login it produced. */
654
+ /**
655
+ * Approved and spent: the key is registered and this is the login it produced.
656
+ *
657
+ * `LoginResult`'s login fields are optional because a sign-in may answer a
658
+ * second-factor challenge instead of a session (#95). This branch never can —
659
+ * the owner already said yes on a device that is signed in, which is itself the
660
+ * second factor — so the three it always carries are narrowed back to required
661
+ * and a consumer of the approved branch reads exactly what it always read.
662
+ */
990
663
  type DeviceAuthApprovedResult = {
991
664
  status: 'approved';
992
- } & LoginResult;
665
+ } & LoginResult & {
666
+ userId: string;
667
+ publicId: string;
668
+ passwordChangeRequired: boolean;
669
+ };
993
670
  type PollDeviceAuthResult = DeviceAuthPendingResult | DeviceAuthApprovedResult;
994
671
  /**
995
672
  * Park a new device's key and hand back the codes it needs.
@@ -1039,95 +716,374 @@ declare function denyDeviceAuthService(params: DenyDeviceAuthParams): Promise<vo
1039
716
  declare function pollDeviceAuthService(params: PollDeviceAuthParams): Promise<PollDeviceAuthResult>;
1040
717
 
1041
718
  /**
1042
- * @spfn/auth - Passkey Service
1043
- *
1044
- * WebAuthn passkeys as an optional account credential, alongside a password and
1045
- * a linked social account rather than in place of either.
1046
- *
1047
- * enroll -> options on an identified session, then verify the attestation
1048
- * sign in -> options with no identifier at all, then verify the assertion
1049
- * manage -> list, rename, revoke
1050
- *
1051
- * A passkey is not a device key. The assertion proves *who* is asking; the
1052
- * device key registered right after it is what every later request is signed
1053
- * with, exactly as after a password login (D2). Nothing in clientProofV1 or in
1054
- * the JWT path changes because a session started this way.
1055
- *
1056
- * Challenges are rows, spent by one conditional UPDATE (D7). Two verifies
1057
- * arriving with the same challenge therefore produce one winner and one refusal,
1058
- * across instances, rather than both reading it as live.
1059
- *
1060
- * Revoking the last thing an account can sign in with is refused (D6) rather
1061
- * than warned about, because that state has no undo. A verified email address
1062
- * counts as one of those things: the password reset flow can always give such an
1063
- * account a password back.
1064
- */
1065
-
1066
- /** One enrolled credential as the management surface shows it. */
1067
- interface PasskeySummary {
1068
- passkeyId: string;
1069
- label: string | null;
1070
- deviceType: PasskeyDeviceType;
1071
- backedUp: boolean;
1072
- transports: string[];
1073
- createdAt: string;
1074
- lastUsedAt: string | null;
1075
- }
1076
- interface RecentAuthenticationParams {
1077
- userId: number;
1078
- /** The device key this request is signed with — its age is the signal. */
1079
- keyId: string;
1080
- currentPassword?: string;
1081
- }
1082
- /**
1083
- * Refuse a passkey change unless the caller has recently proved themselves (D4).
1084
- *
1085
- * Two ways to satisfy it. The device key this request is signed with was
1086
- * registered within the window — that is when this device last presented a
1087
- * credential, and it needs no new state. Or the body carries the account
1088
- * password.
1089
- *
1090
- * An account with no password stored cannot satisfy it with a password, however
1091
- * plausible the value (E5): the comparison still runs, against a dummy hash, so
1092
- * "no password on file" costs exactly what "wrong password" costs. Skipping it
1093
- * would turn response time into an oracle for which accounts are OAuth-only.
1094
- *
1095
- * @throws RecentAuthenticationRequiredError
1096
- */
1097
- declare function assertRecentAuthentication(params: RecentAuthenticationParams): Promise<void>;
1098
- /**
1099
- * Refuse to remove the only thing an account can sign in with (D6).
719
+ * @spfn/auth - Passkeys Entity
1100
720
  *
1101
- * The recovery paths are: another live passkey, a password, a linked social
1102
- * account, and a verified email address. The last one is new — a password reset
1103
- * now exists in this package, and an account that can be reset by email can
1104
- * always get a password back, so the refusal has nothing left to protect.
721
+ * A WebAuthn credential the account owner enrolled on one of their devices.
722
+ * It is a *credential*, not a session: an assertion proves who is asking, and
723
+ * the ordinary device key in `user_public_keys` is what the request afterwards
724
+ * is signed with. The two tables therefore never stand in for each other.
1105
725
  *
1106
- * What remains refused is the account with none of the four: no other passkey,
1107
- * no password, no social account, and no verified email — a phone-only account
1108
- * among them. Nobody, support included, could undo that state.
726
+ * Nothing here is a bearer value, so nothing is hashed. `publicKey` is public by
727
+ * construction and `credentialId` is a handle the authenticator hands to any
728
+ * origin that asks — storing either in the clear costs nothing, and the lookup
729
+ * on `credentialId` has to be a plain equality match on an indexed column.
1109
730
  *
1110
- * @throws LastRecoveryCredentialError
731
+ * Revocation is soft, and `credentialId` stays unique across live and revoked
732
+ * rows alike: a credential someone cut off must never become enrollable again,
733
+ * on this account or on another one.
1111
734
  */
1112
- declare function assertNotLastRecoveryCredential(userId: number): Promise<void>;
1113
- interface StartPasskeyEnrollmentParams {
1114
- userId: number;
1115
- keyId: string;
1116
- currentPassword?: string;
1117
- }
1118
735
  /**
1119
- * Step 1 of enrollment — options for `navigator.credentials.create()`.
1120
- *
1121
- * An enrolled account steps up first, and every account then meets the
1122
- * recent-authentication rule this route has always had (#95). The order is what
1123
- * makes the two independent: `assertStepUp` is a no-op for an unenrolled
1124
- * account, so the answer such a caller gets is byte-for-byte today's.
736
+ * Whether the credential can leave the authenticator that minted it.
1125
737
  *
1126
- * `excludeCredentials` lists the caller's **live** passkeys only, so the
1127
- * authenticator quietly refuses one already enrolled here. Revoked ones are left
1128
- * out on purpose: they must not be re-enrolled either, and the check that
1129
- * refuses them is the global uniqueness check at verify (E11/M10) — listing them
1130
- * here would hand out credential ids the account no longer uses.
738
+ * `multiDevice` is a synced passkey (iCloud Keychain, Google Password Manager);
739
+ * `singleDevice` is bound to one authenticator. Reported by the authenticator at
740
+ * enrollment and shown in the management list, because "this one is only on that
741
+ * phone" is what the owner needs to know before revoking the other entry.
742
+ */
743
+ declare const PASSKEY_DEVICE_TYPES: readonly ["singleDevice", "multiDevice"];
744
+ type PasskeyDeviceType = typeof PASSKEY_DEVICE_TYPES[number];
745
+ /** How long a label may be — the key list's `deviceName` bound, for the same reason. */
746
+ declare const PASSKEY_LABEL_MAX_LENGTH = 64;
747
+ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
748
+ name: "passkeys";
749
+ schema: string;
750
+ columns: {
751
+ createdAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>, {
752
+ name: string;
753
+ tableName: "passkeys";
754
+ dataType: "object date";
755
+ data: Date;
756
+ driverParam: string;
757
+ notNull: true;
758
+ hasDefault: true;
759
+ isPrimaryKey: false;
760
+ isAutoincrement: false;
761
+ hasRuntimeDefault: false;
762
+ enumValues: undefined;
763
+ identity: undefined;
764
+ generated: undefined;
765
+ }>;
766
+ updatedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>>, {
767
+ name: string;
768
+ tableName: "passkeys";
769
+ dataType: "object date";
770
+ data: Date;
771
+ driverParam: string;
772
+ notNull: true;
773
+ hasDefault: true;
774
+ isPrimaryKey: false;
775
+ isAutoincrement: false;
776
+ hasRuntimeDefault: false;
777
+ enumValues: undefined;
778
+ identity: undefined;
779
+ generated: undefined;
780
+ }>;
781
+ id: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
782
+ name: string;
783
+ tableName: "passkeys";
784
+ dataType: "number int53";
785
+ data: number;
786
+ driverParam: number;
787
+ notNull: true;
788
+ hasDefault: true;
789
+ isPrimaryKey: false;
790
+ isAutoincrement: false;
791
+ hasRuntimeDefault: false;
792
+ enumValues: undefined;
793
+ identity: undefined;
794
+ generated: undefined;
795
+ }>;
796
+ userId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
797
+ name: string;
798
+ tableName: "passkeys";
799
+ dataType: "number int53";
800
+ data: number;
801
+ driverParam: string | number;
802
+ notNull: true;
803
+ hasDefault: false;
804
+ isPrimaryKey: false;
805
+ isAutoincrement: false;
806
+ hasRuntimeDefault: false;
807
+ enumValues: undefined;
808
+ identity: undefined;
809
+ generated: undefined;
810
+ }>;
811
+ credentialId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
812
+ name: string;
813
+ tableName: "passkeys";
814
+ dataType: "string";
815
+ data: string;
816
+ driverParam: string;
817
+ notNull: true;
818
+ hasDefault: false;
819
+ isPrimaryKey: false;
820
+ isAutoincrement: false;
821
+ hasRuntimeDefault: false;
822
+ enumValues: undefined;
823
+ identity: undefined;
824
+ generated: undefined;
825
+ }>;
826
+ publicKey: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
827
+ name: string;
828
+ tableName: "passkeys";
829
+ dataType: "string";
830
+ data: string;
831
+ driverParam: string;
832
+ notNull: true;
833
+ hasDefault: false;
834
+ isPrimaryKey: false;
835
+ isAutoincrement: false;
836
+ hasRuntimeDefault: false;
837
+ enumValues: undefined;
838
+ identity: undefined;
839
+ generated: undefined;
840
+ }>;
841
+ counter: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgIntegerBuilder>>, {
842
+ name: string;
843
+ tableName: "passkeys";
844
+ dataType: "number int32";
845
+ data: number;
846
+ driverParam: string | number;
847
+ notNull: true;
848
+ hasDefault: true;
849
+ isPrimaryKey: false;
850
+ isAutoincrement: false;
851
+ hasRuntimeDefault: false;
852
+ enumValues: undefined;
853
+ identity: undefined;
854
+ generated: undefined;
855
+ }>;
856
+ transports: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetDimensions<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, 1>, {
857
+ name: string;
858
+ tableName: "passkeys";
859
+ dataType: "string";
860
+ data: string[];
861
+ driverParam: string | string[];
862
+ notNull: false;
863
+ hasDefault: false;
864
+ isPrimaryKey: false;
865
+ isAutoincrement: false;
866
+ hasRuntimeDefault: false;
867
+ enumValues: undefined;
868
+ identity: undefined;
869
+ generated: undefined;
870
+ }>;
871
+ deviceType: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["singleDevice", "multiDevice"] & [string, ...string[]]>>, {
872
+ name: string;
873
+ tableName: "passkeys";
874
+ dataType: "string enum";
875
+ data: "singleDevice" | "multiDevice";
876
+ driverParam: string;
877
+ notNull: true;
878
+ hasDefault: false;
879
+ isPrimaryKey: false;
880
+ isAutoincrement: false;
881
+ hasRuntimeDefault: false;
882
+ enumValues: ["singleDevice", "multiDevice"] & [string, ...string[]];
883
+ identity: undefined;
884
+ generated: undefined;
885
+ }>;
886
+ backedUp: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
887
+ name: string;
888
+ tableName: "passkeys";
889
+ dataType: "boolean";
890
+ data: boolean;
891
+ driverParam: boolean;
892
+ notNull: true;
893
+ hasDefault: true;
894
+ isPrimaryKey: false;
895
+ isAutoincrement: false;
896
+ hasRuntimeDefault: false;
897
+ enumValues: undefined;
898
+ identity: undefined;
899
+ generated: undefined;
900
+ }>;
901
+ aaguid: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
902
+ name: string;
903
+ tableName: "passkeys";
904
+ dataType: "string";
905
+ data: string;
906
+ driverParam: string;
907
+ notNull: false;
908
+ hasDefault: false;
909
+ isPrimaryKey: false;
910
+ isAutoincrement: false;
911
+ hasRuntimeDefault: false;
912
+ enumValues: undefined;
913
+ identity: undefined;
914
+ generated: undefined;
915
+ }>;
916
+ label: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
917
+ name: string;
918
+ tableName: "passkeys";
919
+ dataType: "string";
920
+ data: string;
921
+ driverParam: string;
922
+ notNull: false;
923
+ hasDefault: false;
924
+ isPrimaryKey: false;
925
+ isAutoincrement: false;
926
+ hasRuntimeDefault: false;
927
+ enumValues: undefined;
928
+ identity: undefined;
929
+ generated: undefined;
930
+ }>;
931
+ secondFactor: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
932
+ name: string;
933
+ tableName: "passkeys";
934
+ dataType: "boolean";
935
+ data: boolean;
936
+ driverParam: boolean;
937
+ notNull: true;
938
+ hasDefault: true;
939
+ isPrimaryKey: false;
940
+ isAutoincrement: false;
941
+ hasRuntimeDefault: false;
942
+ enumValues: undefined;
943
+ identity: undefined;
944
+ generated: undefined;
945
+ }>;
946
+ lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
947
+ name: string;
948
+ tableName: "passkeys";
949
+ dataType: "object date";
950
+ data: Date;
951
+ driverParam: string;
952
+ notNull: false;
953
+ hasDefault: false;
954
+ isPrimaryKey: false;
955
+ isAutoincrement: false;
956
+ hasRuntimeDefault: false;
957
+ enumValues: undefined;
958
+ identity: undefined;
959
+ generated: undefined;
960
+ }>;
961
+ revokedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
962
+ name: string;
963
+ tableName: "passkeys";
964
+ dataType: "object date";
965
+ data: Date;
966
+ driverParam: string;
967
+ notNull: false;
968
+ hasDefault: false;
969
+ isPrimaryKey: false;
970
+ isAutoincrement: false;
971
+ hasRuntimeDefault: false;
972
+ enumValues: undefined;
973
+ identity: undefined;
974
+ generated: undefined;
975
+ }>;
976
+ revokedReason: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
977
+ name: string;
978
+ tableName: "passkeys";
979
+ dataType: "string";
980
+ data: string;
981
+ driverParam: string;
982
+ notNull: false;
983
+ hasDefault: false;
984
+ isPrimaryKey: false;
985
+ isAutoincrement: false;
986
+ hasRuntimeDefault: false;
987
+ enumValues: undefined;
988
+ identity: undefined;
989
+ generated: undefined;
990
+ }>;
991
+ };
992
+ dialect: "pg";
993
+ }>;
994
+ type Passkey = typeof passkeys.$inferSelect;
995
+ type NewPasskey = typeof passkeys.$inferInsert;
996
+
997
+ /**
998
+ * @spfn/auth - Passkey Service
999
+ *
1000
+ * WebAuthn passkeys as an optional account credential, alongside a password and
1001
+ * a linked social account rather than in place of either.
1002
+ *
1003
+ * enroll -> options on an identified session, then verify the attestation
1004
+ * sign in -> options with no identifier at all, then verify the assertion
1005
+ * manage -> list, rename, revoke
1006
+ *
1007
+ * A passkey is not a device key. The assertion proves *who* is asking; the
1008
+ * device key registered right after it is what every later request is signed
1009
+ * with, exactly as after a password login (D2). Nothing in clientProofV1 or in
1010
+ * the JWT path changes because a session started this way.
1011
+ *
1012
+ * Challenges are rows, spent by one conditional UPDATE (D7). Two verifies
1013
+ * arriving with the same challenge therefore produce one winner and one refusal,
1014
+ * across instances, rather than both reading it as live.
1015
+ *
1016
+ * Revoking the last thing an account can sign in with is refused (D6) rather
1017
+ * than warned about, because that state has no undo. A verified email address
1018
+ * counts as one of those things: the password reset flow can always give such an
1019
+ * account a password back.
1020
+ */
1021
+
1022
+ /** One enrolled credential as the management surface shows it. */
1023
+ interface PasskeySummary {
1024
+ passkeyId: string;
1025
+ label: string | null;
1026
+ deviceType: PasskeyDeviceType;
1027
+ backedUp: boolean;
1028
+ transports: string[];
1029
+ createdAt: string;
1030
+ lastUsedAt: string | null;
1031
+ }
1032
+ interface RecentAuthenticationParams {
1033
+ userId: number;
1034
+ /** The device key this request is signed with — its age is the signal. */
1035
+ keyId: string;
1036
+ currentPassword?: string;
1037
+ }
1038
+ /**
1039
+ * Refuse a passkey change unless the caller has recently proved themselves (D4).
1040
+ *
1041
+ * Two ways to satisfy it. The device key this request is signed with was
1042
+ * registered within the window — that is when this device last presented a
1043
+ * credential, and it needs no new state. Or the body carries the account
1044
+ * password.
1045
+ *
1046
+ * An account with no password stored cannot satisfy it with a password, however
1047
+ * plausible the value (E5): the comparison still runs, against a dummy hash, so
1048
+ * "no password on file" costs exactly what "wrong password" costs. Skipping it
1049
+ * would turn response time into an oracle for which accounts are OAuth-only.
1050
+ *
1051
+ * @throws RecentAuthenticationRequiredError
1052
+ */
1053
+ declare function assertRecentAuthentication(params: RecentAuthenticationParams): Promise<void>;
1054
+ /**
1055
+ * Refuse to remove the only thing an account can sign in with (D6).
1056
+ *
1057
+ * The recovery paths are: another live passkey, a password, a linked social
1058
+ * account, and a verified email address. The last one is new — a password reset
1059
+ * now exists in this package, and an account that can be reset by email can
1060
+ * always get a password back, so the refusal has nothing left to protect.
1061
+ *
1062
+ * What remains refused is the account with none of the four: no other passkey,
1063
+ * no password, no social account, and no verified email — a phone-only account
1064
+ * among them. Nobody, support included, could undo that state.
1065
+ *
1066
+ * @throws LastRecoveryCredentialError
1067
+ */
1068
+ declare function assertNotLastRecoveryCredential(userId: number): Promise<void>;
1069
+ interface StartPasskeyEnrollmentParams {
1070
+ userId: number;
1071
+ keyId: string;
1072
+ currentPassword?: string;
1073
+ }
1074
+ /**
1075
+ * Step 1 of enrollment — options for `navigator.credentials.create()`.
1076
+ *
1077
+ * An enrolled account steps up first, and every account then meets the
1078
+ * recent-authentication rule this route has always had (#95). The order is what
1079
+ * makes the two independent: `assertStepUp` is a no-op for an unenrolled
1080
+ * account, so the answer such a caller gets is byte-for-byte today's.
1081
+ *
1082
+ * `excludeCredentials` lists the caller's **live** passkeys only, so the
1083
+ * authenticator quietly refuses one already enrolled here. Revoked ones are left
1084
+ * out on purpose: they must not be re-enrolled either, and the check that
1085
+ * refuses them is the global uniqueness check at verify (E11/M10) — listing them
1086
+ * here would hand out credential ids the account no longer uses.
1131
1087
  */
1132
1088
  declare function startPasskeyEnrollmentService(params: StartPasskeyEnrollmentParams): Promise<PublicKeyCredentialCreationOptionsJSON>;
1133
1089
  interface FinishPasskeyEnrollmentParams {
@@ -1141,94 +1097,607 @@ interface FinishPasskeyEnrollmentResult {
1141
1097
  createdAt: string;
1142
1098
  }
1143
1099
  /**
1144
- * Step 2 of enrollment — verify the attestation and keep the credential.
1100
+ * Step 2 of enrollment — verify the attestation and keep the credential.
1101
+ *
1102
+ * Runs under `Transactional()`: the challenge is spent and the row written
1103
+ * together, so a failure after the spend leaves the challenge live and the
1104
+ * ceremony retryable, while a success can never be replayed.
1105
+ */
1106
+ declare function finishPasskeyEnrollmentService(params: FinishPasskeyEnrollmentParams): Promise<FinishPasskeyEnrollmentResult>;
1107
+ /**
1108
+ * Step 1 of sign-in — options for `navigator.credentials.get()`.
1109
+ *
1110
+ * Takes nothing and returns the same shape to everyone: `allowCredentials` is
1111
+ * always empty and the challenge row names no account (D3). There is no input
1112
+ * that could make this answer differ by whether an account exists, which is the
1113
+ * point — the discoverable credential on the device is what names the owner.
1114
+ */
1115
+ declare function startPasskeyLoginService(): Promise<PublicKeyCredentialRequestOptionsJSON>;
1116
+ interface FinishPasskeyLoginParams {
1117
+ response: AuthenticationResponseJSON;
1118
+ publicKey: string;
1119
+ keyId: string;
1120
+ fingerprint: string;
1121
+ algorithm?: KeyAlgorithmType;
1122
+ oldKeyId?: string;
1123
+ deviceName?: string;
1124
+ platform?: KeyPlatformType;
1125
+ /** Client address of the request, from `deviceProvenance` at the route. */
1126
+ ip?: string;
1127
+ /** `user-agent` of the request, already truncated at the route. */
1128
+ userAgent?: string;
1129
+ /** Whether proxy-guard recognised the trusted Next.js proxy, from the same helper. */
1130
+ webProxy?: boolean;
1131
+ }
1132
+ /**
1133
+ * Step 2 of sign-in — verify the assertion, then sign in exactly as a password
1134
+ * login does.
1135
+ *
1136
+ * The tail from the active-status check onward is the one every identified
1137
+ * sign-in runs (`loginService`, the OAuth flows): revoke the key being replaced,
1138
+ * register the new device key, stamp the last login, announce it after commit.
1139
+ * Passkeys add a way to prove identity, not a second way to hold a session.
1140
+ */
1141
+ declare function finishPasskeyLoginService(params: FinishPasskeyLoginParams): Promise<LoginResult>;
1142
+ /**
1143
+ * The caller's live passkeys, newest first.
1144
+ */
1145
+ declare function listPasskeysService(userId: number): Promise<PasskeySummary[]>;
1146
+ interface RenamePasskeyParams {
1147
+ userId: number;
1148
+ passkeyId: string;
1149
+ label: string;
1150
+ }
1151
+ /**
1152
+ * Rename a passkey. Owner-scoped, so someone else's id is a 404 and nothing
1153
+ * about it is disclosed.
1154
+ *
1155
+ * No recent-authentication gate: a label is display only, and nothing is
1156
+ * authorized by it.
1157
+ */
1158
+ declare function renamePasskeyService(params: RenamePasskeyParams): Promise<{
1159
+ passkeyId: string;
1160
+ label: string;
1161
+ }>;
1162
+ interface RevokePasskeyParams {
1163
+ userId: number;
1164
+ keyId: string;
1165
+ passkeyId: string;
1166
+ currentPassword?: string;
1167
+ }
1168
+ /**
1169
+ * Retire a passkey.
1170
+ *
1171
+ * Gated on the second factor for an enrolled account, and then — for every
1172
+ * account, enrolled or not — on recent authentication, because someone who
1173
+ * walked up to an unlocked laptop should not be able to strip the account's
1174
+ * credentials; and on the
1175
+ * last-recovery-credential guard, because there is no undo for the state that
1176
+ * would leave.
1177
+ *
1178
+ * The owner row is locked before the guard runs, and the route's
1179
+ * `Transactional()` is what holds that lock to commit. Without it the guard is a
1180
+ * read-modify-write with a gap: an owner with two passkeys and nothing else who
1181
+ * fires two revokes at once has both count two live credentials, both pass, and
1182
+ * both revoke — the exact state the guard exists to refuse. Locking makes the
1183
+ * second revoke count one.
1184
+ */
1185
+ declare function revokePasskeyService(params: RevokePasskeyParams): Promise<{
1186
+ passkeyId: string;
1187
+ }>;
1188
+
1189
+ /**
1190
+ * Auth provider type
1191
+ *
1192
+ * 직접 인증(email/phone) + 등록 가능한 모든 소셜 provider(SOCIAL_PROVIDERS).
1193
+ */
1194
+ declare const AuthProviderSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">, ..._sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]]>;
1195
+ /** The doors `auth.login` can name, as a type. */
1196
+ type AuthLoginProvider = Static<typeof AuthLoginProviderSchema>;
1197
+ /**
1198
+ * Login provider type
1199
+ *
1200
+ * AuthProviderSchema + `'device'` and `'passkey'`.
1201
+ *
1202
+ * `'device'` is how a device-code login names itself: the account was proven on
1203
+ * another device that was already signed in, so no credential was presented here
1204
+ * and none of the values above describes it. `'passkey'` is a WebAuthn assertion
1205
+ * — a credential of the account, but not one of the sign-up channels.
1206
+ *
1207
+ * A separate union rather than a widened AuthProviderSchema. Neither is a way to
1208
+ * register — a device-code request can only ever be approved by an existing
1209
+ * account, and a passkey has to be enrolled from a session that already exists —
1210
+ * and neither is something a provider can unlink, so the two events that mean
1211
+ * those things must not start accepting them.
1212
+ */
1213
+ declare const AuthLoginProviderSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">, _sinclair_typebox.TLiteral<"device">, _sinclair_typebox.TLiteral<"passkey">, ..._sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]]>;
1214
+ /**
1215
+ * auth.login - 로그인 성공 이벤트
1216
+ *
1217
+ * 발행 시점:
1218
+ * - 이메일/전화 로그인 성공 시
1219
+ * - OAuth 기존 사용자 로그인 시
1220
+ * - 기기 코드 승인이 소비되어 새 기기 키가 등록될 때 (provider: 'device')
1221
+ *
1222
+ * `mfaEnrolled` is computed as the event is emitted (#95) and is the whole of
1223
+ * the package's opinion about the second factor: it never blocks an account
1224
+ * that has none, and this is the hook an app uses to offer enrolment at a first
1225
+ * login. It says nothing about *which* factor and carries no secret.
1226
+ *
1227
+ * @example
1228
+ * ```typescript
1229
+ * authLoginEvent.subscribe(async (payload) => {
1230
+ * await analytics.trackLogin(payload.userId, payload.provider);
1231
+ * if (!payload.mfaEnrolled) await suggestSecondFactor(payload.userId);
1232
+ * });
1233
+ * ```
1234
+ */
1235
+ declare const authLoginEvent: _spfn_core_event.EventDef<{
1236
+ email?: string | undefined;
1237
+ phone?: string | undefined;
1238
+ userId: string;
1239
+ provider: "email" | "phone" | "passkey" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "device";
1240
+ mfaEnrolled: boolean;
1241
+ }>;
1242
+ /**
1243
+ * Where a device key was registered — the door the new device came through.
1145
1244
  *
1146
- * Runs under `Transactional()`: the challenge is spent and the row written
1147
- * together, so a failure after the spend leaves the challenge live and the
1148
- * ceremony retryable, while a success can never be replayed.
1245
+ * Required on `RegisterPublicKeyParams` rather than optional with a default: a
1246
+ * new *call site* for key registration must choose one, and a default would let
1247
+ * it inherit somebody else's answer silently. `'register'` and `'signup-link'`
1248
+ * both arrive at `createVerifiedAccount`, so the two name themselves there;
1249
+ * `'invitation'` is the one path that stores a key without the key service.
1149
1250
  */
1150
- declare function finishPasskeyEnrollmentService(params: FinishPasskeyEnrollmentParams): Promise<FinishPasskeyEnrollmentResult>;
1251
+ declare const DeviceRegistrationChannelSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"register">, _sinclair_typebox.TLiteral<"signup-link">, _sinclair_typebox.TLiteral<"invitation">, _sinclair_typebox.TLiteral<"password">, _sinclair_typebox.TLiteral<"oauth">, _sinclair_typebox.TLiteral<"oauth-native">, _sinclair_typebox.TLiteral<"device-code">, _sinclair_typebox.TLiteral<"password-reset">, _sinclair_typebox.TLiteral<"passkey">, _sinclair_typebox.TLiteral<"renewal">]>;
1252
+ /** The ten doors a device key is registered through. */
1253
+ type DeviceRegistrationChannel = Static<typeof DeviceRegistrationChannelSchema>;
1151
1254
  /**
1152
- * Step 1 of sign-in — options for `navigator.credentials.get()`.
1255
+ * auth.device.registered — a new device key was added to an account
1153
1256
  *
1154
- * Takes nothing and returns the same shape to everyone: `allowCredentials` is
1155
- * always empty and the challenge row names no account (D3). There is no input
1156
- * that could make this answer differ by whether an account exists, which is the
1157
- * point — the discoverable credential on the device is what names the owner.
1257
+ * 발행 시점:
1258
+ * - a key row was created for an account and the transaction that created it
1259
+ * committed, on every one of the ten channels above
1260
+ *
1261
+ * This is the notice an account owner needs and could not get before: a stolen
1262
+ * password used to sign in on a new device was silent, because a login event
1263
+ * says a session began and not what it began on. Rotation is deliberately not
1264
+ * announced — replacing the key of a device that is already signed in is not a
1265
+ * new device, and a notice for it would train the owner to ignore the ones that
1266
+ * matter.
1267
+ *
1268
+ * `ip` and `userAgent` are what the registering request said about itself. Both
1269
+ * are unauthenticated display material: nothing is decided by them, and a field
1270
+ * is absent rather than carrying a placeholder when the request resolved none.
1271
+ *
1272
+ * Neither the full fingerprint nor the public key is carried. The prefix is
1273
+ * enough to point at one entry of `listKeys`, which is what a notice needs.
1274
+ *
1275
+ * `mfaEnrolled` is computed as the event is emitted (#95), so a notice about a
1276
+ * new device can also be the moment an app offers a second factor to the
1277
+ * accounts that have none.
1278
+ *
1279
+ * @example
1280
+ * ```typescript
1281
+ * authDeviceRegisteredEvent.subscribe(async ({ userId, deviceName, ip, channel }) => {
1282
+ * await notifyOwner(userId, `A new device signed in (${deviceName ?? channel})`);
1283
+ * });
1284
+ * ```
1158
1285
  */
1159
- declare function startPasskeyLoginService(): Promise<PublicKeyCredentialRequestOptionsJSON>;
1160
- interface FinishPasskeyLoginParams {
1161
- response: AuthenticationResponseJSON;
1162
- publicKey: string;
1286
+ declare const authDeviceRegisteredEvent: _spfn_core_event.EventDef<{
1287
+ deviceName?: string | undefined;
1288
+ platform?: string | undefined;
1289
+ ip?: string | undefined;
1290
+ userAgent?: string | undefined;
1163
1291
  keyId: string;
1164
- fingerprint: string;
1165
- algorithm?: KeyAlgorithmType;
1166
- oldKeyId?: string;
1167
- deviceName?: string;
1168
- platform?: KeyPlatformType;
1169
- /** Client address of the request, from `deviceProvenance` at the route. */
1170
- ip?: string;
1171
- /** `user-agent` of the request, already truncated at the route. */
1172
- userAgent?: string;
1173
- /** Whether proxy-guard recognised the trusted Next.js proxy, from the same helper. */
1174
- webProxy?: boolean;
1175
- }
1292
+ algorithm: string;
1293
+ userId: string;
1294
+ mfaEnrolled: boolean;
1295
+ fingerprintPrefix: string;
1296
+ createdAtMillis: number;
1297
+ channel: "password" | "register" | "passkey" | "oauth-native" | "renewal" | "signup-link" | "invitation" | "oauth" | "device-code" | "password-reset";
1298
+ }>;
1176
1299
  /**
1177
- * Step 2 of sign-in — verify the assertion, then sign in exactly as a password
1178
- * login does.
1300
+ * auth.register - 회원가입 성공 이벤트
1179
1301
  *
1180
- * The tail from the active-status check onward is the one every identified
1181
- * sign-in runs (`loginService`, the OAuth flows): revoke the key being replaced,
1182
- * register the new device key, stamp the last login, announce it after commit.
1183
- * Passkeys add a way to prove identity, not a second way to hold a session.
1302
+ * 발행 시점:
1303
+ * - 이메일/전화 회원가입 성공 시
1304
+ * - OAuth 신규 사용자 가입 시
1305
+ *
1306
+ * @example
1307
+ * ```typescript
1308
+ * authRegisterEvent.subscribe(async (payload) => {
1309
+ * await emailService.sendWelcome(payload.email);
1310
+ * });
1311
+ * ```
1312
+ */
1313
+ declare const authRegisterEvent: _spfn_core_event.EventDef<{
1314
+ email?: string | undefined;
1315
+ phone?: string | undefined;
1316
+ metadata?: {
1317
+ [x: string]: unknown;
1318
+ } | undefined;
1319
+ userId: string;
1320
+ provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
1321
+ }>;
1322
+ /**
1323
+ * auth.invitation.created - 초대 생성 이벤트
1324
+ *
1325
+ * 발행 시점:
1326
+ * - createInvitation() 성공 시
1327
+ * - resendInvitation() 성공 시
1328
+ *
1329
+ * @example
1330
+ * ```typescript
1331
+ * invitationCreatedEvent.subscribe(async (payload) => {
1332
+ * const inviteUrl = `${APP_URL}/invite/${payload.token}`;
1333
+ * await notificationService.send({
1334
+ * channel: 'email',
1335
+ * to: payload.email,
1336
+ * subject: 'You are invited!',
1337
+ * html: renderInviteEmail({ inviteUrl, ...payload.metadata }),
1338
+ * });
1339
+ * });
1340
+ * ```
1341
+ */
1342
+ declare const invitationCreatedEvent: _spfn_core_event.EventDef<{
1343
+ metadata?: {
1344
+ [x: string]: unknown;
1345
+ } | undefined;
1346
+ email: string;
1347
+ token: string;
1348
+ expiresAt: string;
1349
+ roleId: number;
1350
+ invitedBy: string;
1351
+ invitationId: string;
1352
+ isResend: boolean;
1353
+ }>;
1354
+ /**
1355
+ * auth.invitation.accepted - 초대 수락 이벤트
1356
+ *
1357
+ * 발행 시점:
1358
+ * - acceptInvitation() 성공 시
1359
+ *
1360
+ * @example
1361
+ * ```typescript
1362
+ * invitationAcceptedEvent.subscribe(async (payload) => {
1363
+ * await onboardingService.start(payload.userId);
1364
+ * });
1365
+ * ```
1366
+ */
1367
+ declare const invitationAcceptedEvent: _spfn_core_event.EventDef<{
1368
+ metadata?: {
1369
+ [x: string]: unknown;
1370
+ } | undefined;
1371
+ email: string;
1372
+ userId: string;
1373
+ roleId: number;
1374
+ invitedBy: string;
1375
+ invitationId: string;
1376
+ }>;
1377
+ /**
1378
+ * auth.deletion.requested - 계정 탈퇴 요청 이벤트
1379
+ *
1380
+ * 발행 시점:
1381
+ * - requestAccountDeletionService() 성공 시 (self/admin 공통)
1382
+ *
1383
+ * @example
1384
+ * ```typescript
1385
+ * authDeletionRequestedEvent.subscribe(async (payload) => {
1386
+ * await analytics.trackChurnRisk(payload.userId);
1387
+ * });
1388
+ * ```
1389
+ */
1390
+ declare const authDeletionRequestedEvent: _spfn_core_event.EventDef<{
1391
+ userId: string;
1392
+ purgeScheduledAt: string;
1393
+ userPublicId: string;
1394
+ requestedBy: "admin" | "self";
1395
+ }>;
1396
+ /**
1397
+ * auth.deletion.cancelled - 계정 탈퇴 복구 이벤트
1398
+ *
1399
+ * 발행 시점:
1400
+ * - cancelAccountDeletionService() 성공 시 (유예 기간 내 복구)
1401
+ */
1402
+ declare const authDeletionCancelledEvent: _spfn_core_event.EventDef<{
1403
+ userId: string;
1404
+ userPublicId: string;
1405
+ }>;
1406
+ /**
1407
+ * auth.deletion.completed - 계정 파기 완료 이벤트
1408
+ *
1409
+ * 발행 시점:
1410
+ * - purge job(또는 즉시 파기 경로)이 유저를 파기한 직후
1411
+ *
1412
+ * PII를 담지 않는다 — userId(내부 순번)/email/phone 없이 userPublicId만 실어
1413
+ * 파기 완료 이후에도 구독자가 식별 정보를 다시 축적하지 않도록 한다.
1414
+ */
1415
+ declare const authDeletionCompletedEvent: _spfn_core_event.EventDef<{
1416
+ userPublicId: string;
1417
+ purgeStrategy: "anonymize" | "hard-delete";
1418
+ }>;
1419
+ /**
1420
+ * auth.oauth.unlinked - provider발 연동 해제 이벤트
1421
+ *
1422
+ * 발행 시점:
1423
+ * - provider(카카오·네이버 등)가 unlink-notify 웹훅으로 연동 해제를 알려와
1424
+ * 소셜 계정 연결과 저장 토큰이 삭제된 직후
1425
+ *
1426
+ * 연결 삭제까지는 프레임워크가 수행하고, 그 이후(계정 탈퇴로 이어갈지 등)는
1427
+ * 앱 정책이므로 이 이벤트를 구독해 처리한다.
1428
+ *
1429
+ * @example
1430
+ * ```typescript
1431
+ * oauthUnlinkedEvent.subscribe(async (payload) => {
1432
+ * await requestAccountDeletionService({ userId: payload.userId, requestedBy: 'self' });
1433
+ * });
1434
+ * ```
1435
+ */
1436
+ declare const oauthUnlinkedEvent: _spfn_core_event.EventDef<{
1437
+ reason?: string | undefined;
1438
+ userId: string;
1439
+ provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
1440
+ providerUserId: string;
1441
+ }>;
1442
+ /**
1443
+ * auth.password.reset — an account's password was replaced through a reset link
1444
+ *
1445
+ * 발행 시점:
1446
+ * - completePasswordResetService()가 커밋된 직후
1447
+ *
1448
+ * Distinct from a password *change*, which is made from a session that already
1449
+ * proved itself. This one is made by whoever opened a link in a mailbox, so it
1450
+ * is the event an app hangs a "your password was reset" notice on — and the
1451
+ * signal to look at, if the owner says they did not ask for it.
1452
+ *
1453
+ * @example
1454
+ * ```typescript
1455
+ * authPasswordResetEvent.subscribe(async (payload) => {
1456
+ * await notifyOwner(payload.userId, 'Your password was reset');
1457
+ * });
1458
+ * ```
1184
1459
  */
1185
- declare function finishPasskeyLoginService(params: FinishPasskeyLoginParams): Promise<LoginResult>;
1460
+ declare const authPasswordResetEvent: _spfn_core_event.EventDef<{
1461
+ email: string;
1462
+ userId: string;
1463
+ }>;
1186
1464
  /**
1187
- * The caller's live passkeys, newest first.
1465
+ * Auth event payload types
1188
1466
  */
1189
- declare function listPasskeysService(userId: number): Promise<PasskeySummary[]>;
1190
- interface RenamePasskeyParams {
1191
- userId: number;
1192
- passkeyId: string;
1193
- label: string;
1194
- }
1467
+ type AuthLoginPayload = typeof authLoginEvent._payload;
1468
+ type AuthRegisterPayload = typeof authRegisterEvent._payload;
1469
+ type AuthPasswordResetPayload = typeof authPasswordResetEvent._payload;
1470
+ type AuthDeviceRegisteredPayload = typeof authDeviceRegisteredEvent._payload;
1471
+ type InvitationCreatedPayload = typeof invitationCreatedEvent._payload;
1472
+ type InvitationAcceptedPayload = typeof invitationAcceptedEvent._payload;
1473
+ type AuthDeletionRequestedPayload = typeof authDeletionRequestedEvent._payload;
1474
+ type AuthDeletionCancelledPayload = typeof authDeletionCancelledEvent._payload;
1475
+ type AuthDeletionCompletedPayload = typeof authDeletionCompletedEvent._payload;
1476
+ type OAuthUnlinkedPayload = typeof oauthUnlinkedEvent._payload;
1477
+
1195
1478
  /**
1196
- * Rename a passkey. Owner-scoped, so someone else's id is a 404 and nothing
1197
- * about it is disclosed.
1479
+ * The four doors a second factor is asked for at.
1198
1480
  *
1199
- * No recent-authentication gate: a label is display only, and nothing is
1200
- * authorized by it.
1481
+ * A subset of `DeviceRegistrationChannel`, and the subset is the decision: these
1482
+ * are the four where a stolen first credential — a password, a social account, a
1483
+ * mailbox — is enough to reach a new device on its own. `device-code` and
1484
+ * `passkey` are exempt because each already carried a second proof, and
1485
+ * `register`, `signup-link`, `invitation` and `renewal` cannot apply (a
1486
+ * brand-new account has nothing enrolled, and a renewal replaces a key on a
1487
+ * device that is already signed in).
1201
1488
  */
1202
- declare function renamePasskeyService(params: RenamePasskeyParams): Promise<{
1203
- passkeyId: string;
1204
- label: string;
1205
- }>;
1206
- interface RevokePasskeyParams {
1207
- userId: number;
1208
- keyId: string;
1209
- passkeyId: string;
1210
- currentPassword?: string;
1211
- }
1489
+ declare const MFA_CHALLENGE_CHANNELS: readonly ["password", "oauth", "oauth-native", "password-reset"];
1490
+ type MfaChallengeChannel = typeof MFA_CHALLENGE_CHANNELS[number];
1491
+ /** How many wrong proofs a challenge survives before it is spent for good. */
1492
+ declare const MFA_CHALLENGE_ATTEMPT_LIMIT = 5;
1212
1493
  /**
1213
- * Retire a passkey.
1494
+ * The `authLoginEvent` payload this registration would have emitted.
1214
1495
  *
1215
- * Gated on the second factor for an enrolled account, and then — for every
1216
- * account, enrolled or not — on recent authentication, because someone who
1217
- * walked up to an unlocked laptop should not be able to strip the account's
1218
- * credentials; and on the
1219
- * last-recovery-credential guard, because there is no undo for the state that
1220
- * would leave.
1496
+ * Carried rather than recomputed because it cannot be recomputed: the app's own
1497
+ * `metadata` reached the sign-in in a sealed OAuth state or a request body that
1498
+ * is gone by the time `verify` runs, and the provider is the social provider on
1499
+ * the OAuth channels rather than anything the user row says. Null on
1500
+ * `password-reset`, which announces a reset and never a login.
1221
1501
  *
1222
- * The owner row is locked before the guard runs, and the route's
1223
- * `Transactional()` is what holds that lock to commit. Without it the guard is a
1224
- * read-modify-write with a gap: an owner with two passkeys and nothing else who
1225
- * fires two revokes at once has both count two live credentials, both pass, and
1226
- * both revoke — the exact state the guard exists to refuse. Locking makes the
1227
- * second revoke count one.
1502
+ * `mfaEnrolled` is not stored: the account is enrolled by construction — that is
1503
+ * why there is a challenge — and re-reading it at verify keeps the payload
1504
+ * honest if the second factor went away in between.
1228
1505
  */
1229
- declare function revokePasskeyService(params: RevokePasskeyParams): Promise<{
1230
- passkeyId: string;
1506
+ interface DeferredLoginEvent {
1507
+ provider: AuthLoginProvider;
1508
+ email?: string;
1509
+ phone?: string;
1510
+ metadata?: Record<string, unknown>;
1511
+ }
1512
+ declare const mfaChallenges: drizzle_orm_pg_core.PgTableWithColumns<{
1513
+ name: "mfa_challenges";
1514
+ schema: string;
1515
+ columns: {
1516
+ createdAt: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>, {
1517
+ name: string;
1518
+ tableName: "mfa_challenges";
1519
+ dataType: "object date";
1520
+ data: Date;
1521
+ driverParam: string;
1522
+ notNull: true;
1523
+ hasDefault: true;
1524
+ isPrimaryKey: false;
1525
+ isAutoincrement: false;
1526
+ hasRuntimeDefault: false;
1527
+ enumValues: undefined;
1528
+ identity: undefined;
1529
+ generated: undefined;
1530
+ }>;
1531
+ updatedAt: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>>, {
1532
+ name: string;
1533
+ tableName: "mfa_challenges";
1534
+ dataType: "object date";
1535
+ data: Date;
1536
+ driverParam: string;
1537
+ notNull: true;
1538
+ hasDefault: true;
1539
+ isPrimaryKey: false;
1540
+ isAutoincrement: false;
1541
+ hasRuntimeDefault: false;
1542
+ enumValues: undefined;
1543
+ identity: undefined;
1544
+ generated: undefined;
1545
+ }>;
1546
+ id: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
1547
+ name: string;
1548
+ tableName: "mfa_challenges";
1549
+ dataType: "number int53";
1550
+ data: number;
1551
+ driverParam: number;
1552
+ notNull: true;
1553
+ hasDefault: true;
1554
+ isPrimaryKey: false;
1555
+ isAutoincrement: false;
1556
+ hasRuntimeDefault: false;
1557
+ enumValues: undefined;
1558
+ identity: undefined;
1559
+ generated: undefined;
1560
+ }>;
1561
+ userId: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
1562
+ name: string;
1563
+ tableName: "mfa_challenges";
1564
+ dataType: "number int53";
1565
+ data: number;
1566
+ driverParam: string | number;
1567
+ notNull: true;
1568
+ hasDefault: false;
1569
+ isPrimaryKey: false;
1570
+ isAutoincrement: false;
1571
+ hasRuntimeDefault: false;
1572
+ enumValues: undefined;
1573
+ identity: undefined;
1574
+ generated: undefined;
1575
+ }>;
1576
+ challengeHash: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
1577
+ name: string;
1578
+ tableName: "mfa_challenges";
1579
+ dataType: "string";
1580
+ data: string;
1581
+ driverParam: string;
1582
+ notNull: true;
1583
+ hasDefault: false;
1584
+ isPrimaryKey: false;
1585
+ isAutoincrement: false;
1586
+ hasRuntimeDefault: false;
1587
+ enumValues: undefined;
1588
+ identity: undefined;
1589
+ generated: undefined;
1590
+ }>;
1591
+ keyId: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
1592
+ name: string;
1593
+ tableName: "mfa_challenges";
1594
+ dataType: "string";
1595
+ data: string;
1596
+ driverParam: string;
1597
+ notNull: true;
1598
+ hasDefault: false;
1599
+ isPrimaryKey: false;
1600
+ isAutoincrement: false;
1601
+ hasRuntimeDefault: false;
1602
+ enumValues: undefined;
1603
+ identity: undefined;
1604
+ generated: undefined;
1605
+ }>;
1606
+ channel: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["password", "oauth", "oauth-native", "password-reset"] & [string, ...string[]]>>, {
1607
+ name: string;
1608
+ tableName: "mfa_challenges";
1609
+ dataType: "string enum";
1610
+ data: "password" | "oauth-native" | "oauth" | "password-reset";
1611
+ driverParam: string;
1612
+ notNull: true;
1613
+ hasDefault: false;
1614
+ isPrimaryKey: false;
1615
+ isAutoincrement: false;
1616
+ hasRuntimeDefault: false;
1617
+ enumValues: ["password", "oauth", "oauth-native", "password-reset"] & [string, ...string[]];
1618
+ identity: undefined;
1619
+ generated: undefined;
1620
+ }>;
1621
+ keyEpoch: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgIntegerBuilder>, {
1622
+ name: string;
1623
+ tableName: "mfa_challenges";
1624
+ dataType: "number int32";
1625
+ data: number;
1626
+ driverParam: string | number;
1627
+ notNull: true;
1628
+ hasDefault: false;
1629
+ isPrimaryKey: false;
1630
+ isAutoincrement: false;
1631
+ hasRuntimeDefault: false;
1632
+ enumValues: undefined;
1633
+ identity: undefined;
1634
+ generated: undefined;
1635
+ }>;
1636
+ attempts: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgIntegerBuilder>>, {
1637
+ name: string;
1638
+ tableName: "mfa_challenges";
1639
+ dataType: "number int32";
1640
+ data: number;
1641
+ driverParam: string | number;
1642
+ notNull: true;
1643
+ hasDefault: true;
1644
+ isPrimaryKey: false;
1645
+ isAutoincrement: false;
1646
+ hasRuntimeDefault: false;
1647
+ enumValues: undefined;
1648
+ identity: undefined;
1649
+ generated: undefined;
1650
+ }>;
1651
+ expiresAt: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTimestampBuilder>, {
1652
+ name: string;
1653
+ tableName: "mfa_challenges";
1654
+ dataType: "object date";
1655
+ data: Date;
1656
+ driverParam: string;
1657
+ notNull: true;
1658
+ hasDefault: false;
1659
+ isPrimaryKey: false;
1660
+ isAutoincrement: false;
1661
+ hasRuntimeDefault: false;
1662
+ enumValues: undefined;
1663
+ identity: undefined;
1664
+ generated: undefined;
1665
+ }>;
1666
+ verifiedAt: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.PgTimestampBuilder, {
1667
+ name: string;
1668
+ tableName: "mfa_challenges";
1669
+ dataType: "object date";
1670
+ data: Date;
1671
+ driverParam: string;
1672
+ notNull: false;
1673
+ hasDefault: false;
1674
+ isPrimaryKey: false;
1675
+ isAutoincrement: false;
1676
+ hasRuntimeDefault: false;
1677
+ enumValues: undefined;
1678
+ identity: undefined;
1679
+ generated: undefined;
1680
+ }>;
1681
+ loginEvent: drizzle_orm_pg_core.PgBuildColumn<"mfa_challenges", drizzle_orm_pg_core.Set$Type<drizzle_orm_pg_core.PgJsonbBuilder, DeferredLoginEvent>, {
1682
+ name: string;
1683
+ tableName: "mfa_challenges";
1684
+ dataType: "object json";
1685
+ data: DeferredLoginEvent;
1686
+ driverParam: unknown;
1687
+ notNull: false;
1688
+ hasDefault: false;
1689
+ isPrimaryKey: false;
1690
+ isAutoincrement: false;
1691
+ hasRuntimeDefault: false;
1692
+ enumValues: undefined;
1693
+ identity: undefined;
1694
+ generated: undefined;
1695
+ }>;
1696
+ };
1697
+ dialect: "pg";
1231
1698
  }>;
1699
+ type MfaChallenge = typeof mfaChallenges.$inferSelect;
1700
+ type NewMfaChallenge = typeof mfaChallenges.$inferInsert;
1232
1701
 
1233
1702
  /**
1234
1703
  * @spfn/auth - Second-Factor Verification Entity
@@ -1374,22 +1843,131 @@ interface AssertStepUpParams {
1374
1843
  * the passkey routes' `assertRecentAuthentication` — and they still run it
1375
1844
  * afterwards; this adds a rule for enrolled accounts rather than replacing one.
1376
1845
  *
1377
- * The unenrolled path therefore costs one indexed lookup and touches no passkey
1378
- * configuration, which is what lets an app with no passkeys at all call it.
1846
+ * The unenrolled path therefore costs one indexed lookup and touches no passkey
1847
+ * configuration, which is what lets an app with no passkeys at all call it.
1848
+ *
1849
+ * @throws StepUpRequiredError when the account is enrolled and the window has passed
1850
+ */
1851
+ declare function assertStepUp(params: AssertStepUpParams): Promise<void>;
1852
+ /**
1853
+ * Carry a device's verification onto the key that replaces it.
1854
+ *
1855
+ * Called from the two rotation seams — `rotateKeyService`, and the `oldKeyId`
1856
+ * rotation every login path runs through `registerPublicKeyService`. Without
1857
+ * it the window would expire silently on every rotation, which the web proxy
1858
+ * does at each login: a user who stepped up a minute ago would be asked again
1859
+ * with nothing to connect it to.
1860
+ */
1861
+ declare function carryStepUpVerification(userId: number, fromKeyId: string, toKeyId: string): Promise<void>;
1862
+ interface OpenStepUpChallengeParams {
1863
+ userId: number;
1864
+ /** The inactive key this challenge would activate. */
1865
+ keyId: string;
1866
+ channel: MfaChallengeChannel;
1867
+ /** The account's key generation right now — the challenge dies with it. */
1868
+ keyEpoch: number;
1869
+ /** The login announcement the 202 is holding back, when the channel has one. */
1870
+ loginEvent?: DeferredLoginEvent;
1871
+ }
1872
+ /**
1873
+ * Mint the challenge a stopped registration hands back.
1874
+ *
1875
+ * The secret is returned once and never stored: only its hash reaches the row,
1876
+ * so a database dump does not yield a spendable challenge, and `verify` finds a
1877
+ * row only for a caller who already had the secret. The row id is not in the
1878
+ * answer at all — it is a sequence, and a sequence on an unauthenticated route
1879
+ * is a thing an attacker walks.
1880
+ */
1881
+ declare function openStepUpChallengeService(params: OpenStepUpChallengeParams): Promise<MfaChallengeHandle>;
1882
+ /**
1883
+ * The challenge already outstanding for this key, re-secreted, or null.
1884
+ *
1885
+ * Registering the same keyId twice is an ordinary path, not a collision: a
1886
+ * native client reuses its keyId, and an OAuth state replayed from the back
1887
+ * button carries the one it was sealed with. Answering 409 there would refuse a
1888
+ * caller for holding a key that is their own and is waiting on them.
1889
+ *
1890
+ * The row is reused rather than replaced, so the retry inherits the attempts
1891
+ * already spent and the expiry already ticking — retrying is not a way around
1892
+ * either. Only the secret is new, because the first one exists nowhere: the row
1893
+ * holds its hash, and that is the property that keeps a database dump from
1894
+ * yielding a spendable challenge.
1895
+ */
1896
+ declare function resumeStepUpChallengeService(userId: number, keyId: string): Promise<MfaChallengeHandle | null>;
1897
+ interface VerifyMfaChallengeParams {
1898
+ /** The secret from the 202 body, the callback query, or the pending page. */
1899
+ challenge: string;
1900
+ code?: string;
1901
+ recoveryCode?: string;
1902
+ response?: AuthenticationResponseJSON;
1903
+ }
1904
+ /**
1905
+ * The sign-in the challenge was standing in for, plus what the proxy needs.
1906
+ *
1907
+ * `keyId` and `challengeHash` are for the Next.js interceptor and nothing else:
1908
+ * it seals a session only when both match the pending cookie it baked at the
1909
+ * 202, which is what stops a cookie minted for one flow from sealing a session
1910
+ * around another flow's key. Neither is a credential — the hash is what the
1911
+ * server already stores, and the key is inactive to anyone without the private
1912
+ * half the proxy is holding.
1913
+ */
1914
+ interface VerifyMfaChallengeResult extends LoginResult {
1915
+ keyId: string;
1916
+ challengeHash: string;
1917
+ }
1918
+ /**
1919
+ * Spend a new-device challenge, which activates the key and starts the session.
1920
+ *
1921
+ * Unauthenticated by construction: the key this would activate is the only one
1922
+ * the caller has and it cannot sign anything yet. The challenge secret is the
1923
+ * whole credential, and it authorizes exactly this — no other route reads it.
1924
+ *
1925
+ * The verified mark is a conditional UPDATE, so two requests carrying the same
1926
+ * secret produce one session and one 401. The key is activated only after that
1927
+ * mark is won, which is what makes "a pending challenge never yields a usable
1928
+ * key" true even under a race.
1379
1929
  *
1380
- * @throws StepUpRequiredError when the account is enrolled and the window has passed
1930
+ * The binding the answer carries was decided at **registration**, not here, and
1931
+ * is read back off the key row. It has to be: the decision reads the owner's
1932
+ * `session_binding` setting together with whether the request came through the
1933
+ * trusted Next.js proxy, and this route is unauthenticated and may be called
1934
+ * from anywhere — deciding it here would let a direct caller ask for a cookie
1935
+ * that believes a bound key is an ordinary one. A bound account that steps up
1936
+ * therefore gets exactly the binding and the expiry its sign-in registered, and
1937
+ * the ten minutes a challenge may sit for come out of that key's short life.
1938
+ *
1939
+ * @throws ValidationError when the body names none or more than one proof
1940
+ * @throws MfaVerificationFailedError for every other refusal, in one body
1381
1941
  */
1382
- declare function assertStepUp(params: AssertStepUpParams): Promise<void>;
1942
+ declare function verifyMfaChallengeService(params: VerifyMfaChallengeParams): Promise<VerifyMfaChallengeResult>;
1383
1943
  /**
1384
- * Carry a device's verification onto the key that replaces it.
1944
+ * Options for finishing a new-device step-up with a passkey.
1385
1945
  *
1386
- * Called from the two rotation seams — `rotateKeyService`, and the `oldKeyId`
1387
- * rotation every login path runs through `registerPublicKeyService`. Without
1388
- * it the window would expire silently on every rotation, which the web proxy
1389
- * does at each login: a user who stepped up a minute ago would be asked again
1390
- * with nothing to connect it to.
1946
+ * The step-up challenge stands in for the session this caller does not have yet:
1947
+ * it is what names the account, so the WebAuthn challenge can be minted for the
1948
+ * right owner without the request having to say who that is. `allowCredentials`
1949
+ * is empty for the reason it is everywhere else here (D3), and the ceremony kind
1950
+ * is `'mfa'`, so what comes back cannot be spent as a sign-in.
1951
+ *
1952
+ * @throws MfaVerificationFailedError when the challenge cannot be spent
1391
1953
  */
1392
- declare function carryStepUpVerification(userId: number, fromKeyId: string, toKeyId: string): Promise<void>;
1954
+ declare function startMfaChallengeAssertionService(challenge: string): Promise<PublicKeyCredentialRequestOptionsJSON>;
1955
+ /**
1956
+ * Drop expired and spent challenges, and the keys they were holding.
1957
+ *
1958
+ * A pending key is unusable by construction, but it is still a key row on an
1959
+ * account nobody is watching, and its challenge is what the owner would be shown
1960
+ * if anything ever listed it. Both go once the challenge can no longer do
1961
+ * anything. A spent one is kept for the same span as an expired one, so a replay
1962
+ * inside the window is answered "already verified" from a row rather than
1963
+ * "unknown" from an absence — the same 401 either way, but the record survives
1964
+ * long enough to be read in a log.
1965
+ *
1966
+ * @returns number of challenge rows deleted
1967
+ */
1968
+ declare function sweepMfaChallengesService(): Promise<{
1969
+ deleted: number;
1970
+ }>;
1393
1971
  interface TotpEnrolmentResult {
1394
1972
  /** The base32 secret, shown once. Never logged, never returned again. */
1395
1973
  secret: string;
@@ -1484,333 +2062,75 @@ interface MfaStatus {
1484
2062
  * The account's second-factor state.
1485
2063
  *
1486
2064
  * Carries no secret, no otpauth URI and no recovery code — only the counts and
1487
- * names an account screen needs. An unconfirmed enrolment is not a method: it
1488
- * gates nothing, so reporting it would tell the owner they are protected when
1489
- * they are not.
1490
- */
1491
- declare function mfaStatusService(userId: number): Promise<MfaStatus>;
1492
- /**
1493
- * Options for a step-up by passkey assertion.
1494
- *
1495
- * `allowCredentials` is empty, as it is for a sign-in and for the same reason
1496
- * (D3): the discoverable credential on the device names itself, and the owner
1497
- * and the second-factor mark are checked when the assertion comes back. The
1498
- * challenge is minted with kind `'mfa'` and this account's id, so it cannot be
1499
- * presented to `passkeys/login/verify` and a sign-in challenge cannot be
1500
- * presented here.
1501
- */
1502
- declare function startStepUpService(userId: number): Promise<PublicKeyCredentialRequestOptionsJSON>;
1503
- interface StepUpParams {
1504
- userId: number;
1505
- /** The device the verification is recorded against. */
1506
- keyId: string;
1507
- code?: string;
1508
- recoveryCode?: string;
1509
- response?: AuthenticationResponseJSON;
1510
- }
1511
- /**
1512
- * Re-prove the second factor on this device, refreshing its window.
1513
- *
1514
- * The escape hatch the exempt registration channels need: a device-code
1515
- * approval and a passkey sign-in register a key with no verification against
1516
- * it, by design, so the session they create would otherwise fail every
1517
- * sensitive change with no way forward.
1518
- *
1519
- * @throws ValidationError when the body names none or more than one input
1520
- * @throws MfaNotEnrolledError | MfaVerificationFailedError
1521
- */
1522
- declare function stepUpService(params: StepUpParams): Promise<void>;
1523
- /**
1524
- * Check exactly one of the three proofs, and say which one it was.
1525
- *
1526
- * Exactly one: two inputs in a body is a caller trying combinations, and none
1527
- * is a malformed request. Neither is a failed verification, so both are a 400
1528
- * rather than the uniform 401 a wrong proof gets.
1529
- *
1530
- * @throws ValidationError | MfaVerificationFailedError
1531
- */
1532
- declare function verifySecondFactor(params: StepUpParams): Promise<MfaVerificationMethod>;
1533
- /**
1534
- * Drop enrolments nobody ever confirmed.
1535
- *
1536
- * A secret handed out and abandoned is a credential sitting in a table doing
1537
- * nothing; a day is long enough for anyone who meant to finish.
1538
- *
1539
- * @returns number of rows deleted
1540
- */
1541
- declare function sweepUnconfirmedMfaService(): Promise<{
1542
- deleted: number;
1543
- }>;
1544
-
1545
- /**
1546
- * Auth provider type
1547
- *
1548
- * 직접 인증(email/phone) + 등록 가능한 모든 소셜 provider(SOCIAL_PROVIDERS).
1549
- */
1550
- declare const AuthProviderSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">, ..._sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]]>;
1551
- /**
1552
- * auth.login - 로그인 성공 이벤트
1553
- *
1554
- * 발행 시점:
1555
- * - 이메일/전화 로그인 성공 시
1556
- * - OAuth 기존 사용자 로그인 시
1557
- * - 기기 코드 승인이 소비되어 새 기기 키가 등록될 때 (provider: 'device')
1558
- *
1559
- * `mfaEnrolled` is computed as the event is emitted (#95) and is the whole of
1560
- * the package's opinion about the second factor: it never blocks an account
1561
- * that has none, and this is the hook an app uses to offer enrolment at a first
1562
- * login. It says nothing about *which* factor and carries no secret.
1563
- *
1564
- * @example
1565
- * ```typescript
1566
- * authLoginEvent.subscribe(async (payload) => {
1567
- * await analytics.trackLogin(payload.userId, payload.provider);
1568
- * if (!payload.mfaEnrolled) await suggestSecondFactor(payload.userId);
1569
- * });
1570
- * ```
1571
- */
1572
- declare const authLoginEvent: _spfn_core_event.EventDef<{
1573
- email?: string | undefined;
1574
- phone?: string | undefined;
1575
- userId: string;
1576
- provider: "email" | "phone" | "passkey" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "device";
1577
- mfaEnrolled: boolean;
1578
- }>;
1579
- /**
1580
- * Where a device key was registered — the door the new device came through.
1581
- *
1582
- * Required on `RegisterPublicKeyParams` rather than optional with a default: a
1583
- * new *call site* for key registration must choose one, and a default would let
1584
- * it inherit somebody else's answer silently. `'register'` and `'signup-link'`
1585
- * both arrive at `createVerifiedAccount`, so the two name themselves there;
1586
- * `'invitation'` is the one path that stores a key without the key service.
1587
- */
1588
- declare const DeviceRegistrationChannelSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"register">, _sinclair_typebox.TLiteral<"signup-link">, _sinclair_typebox.TLiteral<"invitation">, _sinclair_typebox.TLiteral<"password">, _sinclair_typebox.TLiteral<"oauth">, _sinclair_typebox.TLiteral<"oauth-native">, _sinclair_typebox.TLiteral<"device-code">, _sinclair_typebox.TLiteral<"password-reset">, _sinclair_typebox.TLiteral<"passkey">, _sinclair_typebox.TLiteral<"renewal">]>;
1589
- /** The ten doors a device key is registered through. */
1590
- type DeviceRegistrationChannel = Static<typeof DeviceRegistrationChannelSchema>;
1591
- /**
1592
- * auth.device.registered — a new device key was added to an account
1593
- *
1594
- * 발행 시점:
1595
- * - a key row was created for an account and the transaction that created it
1596
- * committed, on every one of the ten channels above
1597
- *
1598
- * This is the notice an account owner needs and could not get before: a stolen
1599
- * password used to sign in on a new device was silent, because a login event
1600
- * says a session began and not what it began on. Rotation is deliberately not
1601
- * announced — replacing the key of a device that is already signed in is not a
1602
- * new device, and a notice for it would train the owner to ignore the ones that
1603
- * matter.
1604
- *
1605
- * `ip` and `userAgent` are what the registering request said about itself. Both
1606
- * are unauthenticated display material: nothing is decided by them, and a field
1607
- * is absent rather than carrying a placeholder when the request resolved none.
1608
- *
1609
- * Neither the full fingerprint nor the public key is carried. The prefix is
1610
- * enough to point at one entry of `listKeys`, which is what a notice needs.
1611
- *
1612
- * `mfaEnrolled` is computed as the event is emitted (#95), so a notice about a
1613
- * new device can also be the moment an app offers a second factor to the
1614
- * accounts that have none.
1615
- *
1616
- * @example
1617
- * ```typescript
1618
- * authDeviceRegisteredEvent.subscribe(async ({ userId, deviceName, ip, channel }) => {
1619
- * await notifyOwner(userId, `A new device signed in (${deviceName ?? channel})`);
1620
- * });
1621
- * ```
1622
- */
1623
- declare const authDeviceRegisteredEvent: _spfn_core_event.EventDef<{
1624
- deviceName?: string | undefined;
1625
- platform?: string | undefined;
1626
- ip?: string | undefined;
1627
- userAgent?: string | undefined;
1628
- keyId: string;
1629
- algorithm: string;
1630
- userId: string;
1631
- fingerprintPrefix: string;
1632
- createdAtMillis: number;
1633
- mfaEnrolled: boolean;
1634
- channel: "password" | "register" | "passkey" | "oauth-native" | "renewal" | "oauth" | "invitation" | "signup-link" | "password-reset" | "device-code";
1635
- }>;
1636
- /**
1637
- * auth.register - 회원가입 성공 이벤트
1638
- *
1639
- * 발행 시점:
1640
- * - 이메일/전화 회원가입 성공 시
1641
- * - OAuth 신규 사용자 가입 시
1642
- *
1643
- * @example
1644
- * ```typescript
1645
- * authRegisterEvent.subscribe(async (payload) => {
1646
- * await emailService.sendWelcome(payload.email);
1647
- * });
1648
- * ```
1649
- */
1650
- declare const authRegisterEvent: _spfn_core_event.EventDef<{
1651
- email?: string | undefined;
1652
- phone?: string | undefined;
1653
- metadata?: {
1654
- [x: string]: unknown;
1655
- } | undefined;
1656
- userId: string;
1657
- provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
1658
- }>;
1659
- /**
1660
- * auth.invitation.created - 초대 생성 이벤트
1661
- *
1662
- * 발행 시점:
1663
- * - createInvitation() 성공 시
1664
- * - resendInvitation() 성공 시
1665
- *
1666
- * @example
1667
- * ```typescript
1668
- * invitationCreatedEvent.subscribe(async (payload) => {
1669
- * const inviteUrl = `${APP_URL}/invite/${payload.token}`;
1670
- * await notificationService.send({
1671
- * channel: 'email',
1672
- * to: payload.email,
1673
- * subject: 'You are invited!',
1674
- * html: renderInviteEmail({ inviteUrl, ...payload.metadata }),
1675
- * });
1676
- * });
1677
- * ```
1678
- */
1679
- declare const invitationCreatedEvent: _spfn_core_event.EventDef<{
1680
- metadata?: {
1681
- [x: string]: unknown;
1682
- } | undefined;
1683
- email: string;
1684
- token: string;
1685
- expiresAt: string;
1686
- roleId: number;
1687
- invitedBy: string;
1688
- invitationId: string;
1689
- isResend: boolean;
1690
- }>;
1691
- /**
1692
- * auth.invitation.accepted - 초대 수락 이벤트
1693
- *
1694
- * 발행 시점:
1695
- * - acceptInvitation() 성공 시
1696
- *
1697
- * @example
1698
- * ```typescript
1699
- * invitationAcceptedEvent.subscribe(async (payload) => {
1700
- * await onboardingService.start(payload.userId);
1701
- * });
1702
- * ```
1703
- */
1704
- declare const invitationAcceptedEvent: _spfn_core_event.EventDef<{
1705
- metadata?: {
1706
- [x: string]: unknown;
1707
- } | undefined;
1708
- email: string;
1709
- userId: string;
1710
- roleId: number;
1711
- invitedBy: string;
1712
- invitationId: string;
1713
- }>;
1714
- /**
1715
- * auth.deletion.requested - 계정 탈퇴 요청 이벤트
1716
- *
1717
- * 발행 시점:
1718
- * - requestAccountDeletionService() 성공 시 (self/admin 공통)
1719
- *
1720
- * @example
1721
- * ```typescript
1722
- * authDeletionRequestedEvent.subscribe(async (payload) => {
1723
- * await analytics.trackChurnRisk(payload.userId);
1724
- * });
1725
- * ```
2065
+ * names an account screen needs. An unconfirmed enrolment is not a method: it
2066
+ * gates nothing, so reporting it would tell the owner they are protected when
2067
+ * they are not.
1726
2068
  */
1727
- declare const authDeletionRequestedEvent: _spfn_core_event.EventDef<{
1728
- userId: string;
1729
- purgeScheduledAt: string;
1730
- userPublicId: string;
1731
- requestedBy: "admin" | "self";
1732
- }>;
2069
+ declare function mfaStatusService(userId: number): Promise<MfaStatus>;
1733
2070
  /**
1734
- * auth.deletion.cancelled - 계정 탈퇴 복구 이벤트
2071
+ * Options for a step-up by passkey assertion.
1735
2072
  *
1736
- * 발행 시점:
1737
- * - cancelAccountDeletionService() 성공 시 (유예 기간 내 복구)
2073
+ * `allowCredentials` is empty, as it is for a sign-in and for the same reason
2074
+ * (D3): the discoverable credential on the device names itself, and the owner
2075
+ * and the second-factor mark are checked when the assertion comes back. The
2076
+ * challenge is minted with kind `'mfa'` and this account's id, so it cannot be
2077
+ * presented to `passkeys/login/verify` and a sign-in challenge cannot be
2078
+ * presented here.
1738
2079
  */
1739
- declare const authDeletionCancelledEvent: _spfn_core_event.EventDef<{
1740
- userId: string;
1741
- userPublicId: string;
1742
- }>;
2080
+ declare function startStepUpService(userId: number): Promise<PublicKeyCredentialRequestOptionsJSON>;
2081
+ interface StepUpParams {
2082
+ userId: number;
2083
+ /** The device the verification is recorded against. */
2084
+ keyId: string;
2085
+ code?: string;
2086
+ recoveryCode?: string;
2087
+ response?: AuthenticationResponseJSON;
2088
+ }
1743
2089
  /**
1744
- * auth.deletion.completed - 계정 파기 완료 이벤트
2090
+ * Re-prove the second factor on this device, refreshing its window.
1745
2091
  *
1746
- * 발행 시점:
1747
- * - purge job(또는 즉시 파기 경로)이 유저를 파기한 직후
2092
+ * The escape hatch the exempt registration channels need: a device-code
2093
+ * approval and a passkey sign-in register a key with no verification against
2094
+ * it, by design, so the session they create would otherwise fail every
2095
+ * sensitive change with no way forward.
1748
2096
  *
1749
- * PII를 담지 않는다 — userId(내부 순번)/email/phone 없이 userPublicId만 실어
1750
- * 파기 완료 이후에도 구독자가 식별 정보를 다시 축적하지 않도록 한다.
2097
+ * @throws ValidationError when the body names none or more than one input
2098
+ * @throws MfaNotEnrolledError | MfaVerificationFailedError
1751
2099
  */
1752
- declare const authDeletionCompletedEvent: _spfn_core_event.EventDef<{
1753
- userPublicId: string;
1754
- purgeStrategy: "anonymize" | "hard-delete";
1755
- }>;
2100
+ declare function stepUpService(params: StepUpParams): Promise<void>;
1756
2101
  /**
1757
- * auth.oauth.unlinked - provider발 연동 해제 이벤트
2102
+ * Check exactly one of the three proofs, and say which one it was — or null.
1758
2103
  *
1759
- * 발행 시점:
1760
- * - provider(카카오·네이버 등)가 unlink-notify 웹훅으로 연동 해제를 알려와
1761
- * 소셜 계정 연결과 저장 토큰이 삭제된 직후
2104
+ * Exactly one: two inputs in a body is a caller trying combinations, and none
2105
+ * is a malformed request. Neither is a failed verification, so both are a 400
2106
+ * rather than the uniform 401 a wrong proof gets.
1762
2107
  *
1763
- * 연결 삭제까지는 프레임워크가 수행하고, 그 이후(계정 탈퇴로 이어갈지 등)는
1764
- * 앱 정책이므로 이 이벤트를 구독해 처리한다.
2108
+ * Null rather than a throw for the failure itself, because one caller has
2109
+ * bookkeeping to do before it refuses: the new-device challenge counts the
2110
+ * attempt, and counting it inside a `catch` around the thing that threw would be
2111
+ * two control flows for one answer.
1765
2112
  *
1766
- * @example
1767
- * ```typescript
1768
- * oauthUnlinkedEvent.subscribe(async (payload) => {
1769
- * await requestAccountDeletionService({ userId: payload.userId, requestedBy: 'self' });
1770
- * });
1771
- * ```
2113
+ * @throws ValidationError when the body names none or more than one input
1772
2114
  */
1773
- declare const oauthUnlinkedEvent: _spfn_core_event.EventDef<{
1774
- reason?: string | undefined;
1775
- userId: string;
1776
- provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
1777
- providerUserId: string;
1778
- }>;
2115
+ declare function attemptSecondFactor(params: StepUpParams): Promise<MfaVerificationMethod | null>;
1779
2116
  /**
1780
- * auth.password.reset — an account's password was replaced through a reset link
2117
+ * `attemptSecondFactor`, for the callers whose only answer to a wrong proof is
2118
+ * the uniform 401.
1781
2119
  *
1782
- * 발행 시점:
1783
- * - completePasswordResetService()가 커밋된 직후
2120
+ * @throws ValidationError | MfaVerificationFailedError
2121
+ */
2122
+ declare function verifySecondFactor(params: StepUpParams): Promise<MfaVerificationMethod>;
2123
+ /**
2124
+ * Drop enrolments nobody ever confirmed.
1784
2125
  *
1785
- * Distinct from a password *change*, which is made from a session that already
1786
- * proved itself. This one is made by whoever opened a link in a mailbox, so it
1787
- * is the event an app hangs a "your password was reset" notice on — and the
1788
- * signal to look at, if the owner says they did not ask for it.
2126
+ * A secret handed out and abandoned is a credential sitting in a table doing
2127
+ * nothing; a day is long enough for anyone who meant to finish.
1789
2128
  *
1790
- * @example
1791
- * ```typescript
1792
- * authPasswordResetEvent.subscribe(async (payload) => {
1793
- * await notifyOwner(payload.userId, 'Your password was reset');
1794
- * });
1795
- * ```
2129
+ * @returns number of rows deleted
1796
2130
  */
1797
- declare const authPasswordResetEvent: _spfn_core_event.EventDef<{
1798
- email: string;
1799
- userId: string;
2131
+ declare function sweepUnconfirmedMfaService(): Promise<{
2132
+ deleted: number;
1800
2133
  }>;
1801
- /**
1802
- * Auth event payload types
1803
- */
1804
- type AuthLoginPayload = typeof authLoginEvent._payload;
1805
- type AuthRegisterPayload = typeof authRegisterEvent._payload;
1806
- type AuthPasswordResetPayload = typeof authPasswordResetEvent._payload;
1807
- type AuthDeviceRegisteredPayload = typeof authDeviceRegisteredEvent._payload;
1808
- type InvitationCreatedPayload = typeof invitationCreatedEvent._payload;
1809
- type InvitationAcceptedPayload = typeof invitationAcceptedEvent._payload;
1810
- type AuthDeletionRequestedPayload = typeof authDeletionRequestedEvent._payload;
1811
- type AuthDeletionCancelledPayload = typeof authDeletionCancelledEvent._payload;
1812
- type AuthDeletionCompletedPayload = typeof authDeletionCompletedEvent._payload;
1813
- type OAuthUnlinkedPayload = typeof oauthUnlinkedEvent._payload;
1814
2134
 
1815
2135
  /**
1816
2136
  * @spfn/auth - Key Service
@@ -1859,7 +2179,40 @@ interface RegisterPublicKeyParams {
1859
2179
  * register a device with the owner's notice switched off.
1860
2180
  */
1861
2181
  replacesKeyId?: string;
2182
+ /**
2183
+ * What to announce if this registration is stopped and later verified (#95).
2184
+ *
2185
+ * Read only on a channel that can step up, and only when it does. The login
2186
+ * event has to fire once and with the original channel's payload, and the
2187
+ * app's own `metadata` and the social provider that carried it are gone by
2188
+ * the time `POST /_auth/mfa/verify` runs — so the announcement rides the
2189
+ * challenge row rather than being guessed at from the user row.
2190
+ */
2191
+ loginEvent?: DeferredLoginEvent;
1862
2192
  }
2193
+ /**
2194
+ * What a registration settled on: a key, or a second factor still to prove.
2195
+ *
2196
+ * The `pending` branch is the 202 (#95). Its callers answer it to the client and
2197
+ * stop — no session, no event, no `lastLoginAt` — and everything they would have
2198
+ * done happens at `verify` instead.
2199
+ */
2200
+ type RegisterPublicKeyResult = ({
2201
+ pending: false;
2202
+ } & RegisteredKeyBinding) | {
2203
+ pending: true;
2204
+ challenge: MfaChallengeHandle;
2205
+ };
2206
+ /**
2207
+ * The binding half of a registration that could not have been stepped up.
2208
+ *
2209
+ * Only four channels can answer `pending`, so a caller on any other one is
2210
+ * reading a case its channel cannot reach. Throwing rather than defaulting is
2211
+ * the honest answer if that ever stops being true: a silent `{}` would seal an
2212
+ * unbound session for a key nobody had proved, which is the one outcome this
2213
+ * whole feature exists to prevent.
2214
+ */
2215
+ declare function registeredBinding(result: RegisterPublicKeyResult): RegisteredKeyBinding;
1863
2216
  interface RotateKeyParams {
1864
2217
  userId: number;
1865
2218
  oldKeyId: string;
@@ -1988,11 +2341,16 @@ interface RegisteredKeyBinding {
1988
2341
  * index, rolling the whole login transaction back into a 500. Reuse is refused
1989
2342
  * with a domain error instead, telling the client to generate a fresh keyId.
1990
2343
  *
2344
+ * An enrolled account arriving on a new device gets the key written **inactive**
2345
+ * and a challenge back (#95). Nothing else happens: no device event, no login
2346
+ * event, no `lastLoginAt` — those are what the challenge is holding, and they
2347
+ * fire at `POST /_auth/mfa/verify` or not at all.
2348
+ *
1991
2349
  * @throws KeyIdAlreadyRegisteredError keyId가 이미 쓰인 값일 때 (자기 폐기 키 재사용 · 남의 키)
1992
2350
  * @throws InvalidKeyFingerprintError fingerprint가 publicKey와 맞지 않을 때
1993
2351
  * @throws KeyAlgorithmMismatchError 키의 SPKI 타입이 선언된 algorithm과 다를 때
1994
2352
  */
1995
- declare function registerPublicKeyService(params: RegisterPublicKeyParams): Promise<RegisteredKeyBinding>;
2353
+ declare function registerPublicKeyService(params: RegisterPublicKeyParams): Promise<RegisterPublicKeyResult>;
1996
2354
  /**
1997
2355
  * Rotate user's public key (revoke old, register new)
1998
2356
  *
@@ -2358,6 +2716,15 @@ interface OAuthCallbackResult {
2358
2716
  userId: string;
2359
2717
  keyId: string;
2360
2718
  isNewUser: boolean;
2719
+ /**
2720
+ * The step-up challenge this callback is carrying, when it is carrying one.
2721
+ *
2722
+ * Present exactly when the redirect names `mfaChallenge` instead of
2723
+ * `userId`/`keyId` (#95). Returned beside the URL so a caller that does not
2724
+ * parse the query — a test, or an app mounting its own handler — can tell
2725
+ * the two redirects apart.
2726
+ */
2727
+ mfaChallenge?: string;
2361
2728
  }
2362
2729
  /**
2363
2730
  * registry에서 provider를 찾아 사용 가능한지 검증 후 반환
@@ -2466,10 +2833,22 @@ interface OAuthNativeParams {
2466
2833
  };
2467
2834
  metadata?: Record<string, unknown>;
2468
2835
  }
2836
+ /**
2837
+ * What a native social sign-in answers with.
2838
+ *
2839
+ * Shaped like `LoginResult` and for the same reason (#95): this channel steps up
2840
+ * too, so the answer is one type carrying a required discriminant and optional
2841
+ * fields rather than a union the typed client and the mobile contract could not
2842
+ * both express. Narrow on `mfaRequired` before reading `userId`.
2843
+ */
2469
2844
  interface OAuthNativeResult {
2470
- userId: string;
2471
- keyId: string;
2472
- isNewUser: boolean;
2845
+ /** true means no key was activated: verify the challenge below first. */
2846
+ mfaRequired: boolean;
2847
+ /** Present exactly when `mfaRequired` is true. */
2848
+ challenge?: MfaChallengeHandle;
2849
+ userId?: string;
2850
+ keyId?: string;
2851
+ isNewUser?: boolean;
2473
2852
  }
2474
2853
  /**
2475
2854
  * native id_token 로그인 처리
@@ -2715,6 +3094,115 @@ declare function disableSessionBindingService(params: DisableSessionBindingParam
2715
3094
  /** The challenge the disabling ceremony signs. Same ceremony renewal uses. */
2716
3095
  declare function startSessionBindingDisableService(userId: number): Promise<PublicKeyCredentialRequestOptionsJSON>;
2717
3096
 
3097
+ /**
3098
+ * @spfn/auth - Session Renewal Service
3099
+ *
3100
+ * What a bound session does when its key runs out: prove, with a fresh WebAuthn
3101
+ * assertion, that the person who enrolled the passkey is still at the machine,
3102
+ * and get a new short-lived key sealed into the cookie.
3103
+ *
3104
+ * Neither step is public. The expiring key is named by `expiredKeyId`, and that
3105
+ * value reaches the service from `authenticateForRenewal` — the `keyId` of a
3106
+ * bearer JWT this very key signed — rather than from the request body, so a
3107
+ * caller who does not hold the private half cannot name a key at all. The
3108
+ * assertion still has to be signed by a passkey that key's owner enrolled: the
3109
+ * signature proves the cookie, and the cookie is the thing that may have been
3110
+ * copied.
3111
+ *
3112
+ * The admission below is run again here all the same. The middleware and the
3113
+ * service ask the same four questions of the row, and a service that trusted its
3114
+ * caller to have asked them would be one refactor away from not being asked at
3115
+ * all.
3116
+ *
3117
+ * Every refusal is the same refusal. A key that never existed, a stranger's key,
3118
+ * an unbound key, a revoked one, one past its grace, an inactive account, a spent
3119
+ * challenge, an assertion that did not verify — all `SessionRenewalRefusedError`,
3120
+ * with the same body, because anything finer would answer "is this key id live"
3121
+ * to whoever asked.
3122
+ *
3123
+ * Renewal announces nothing. No `auth.login`, no `auth.device.registered`, and
3124
+ * `lastLoginAt` does not move: this is the same person on the same device
3125
+ * continuing the session they already had, and a subscriber mailing "new sign-in"
3126
+ * once a day per device would train its reader to ignore the notice that matters.
3127
+ * A `lastLoginAt` that moved every day would make dormant-account detection
3128
+ * meaningless for exactly the accounts that turned this protection on.
3129
+ */
3130
+
3131
+ interface StartSessionRenewParams {
3132
+ /** The key that ran out, read off the JWT the request was signed with. */
3133
+ expiredKeyId: string;
3134
+ }
3135
+ interface FinishSessionRenewParams extends StartSessionRenewParams {
3136
+ /** The assertion, from `navigator.credentials.get()`. */
3137
+ response: AuthenticationResponseJSON;
3138
+ /**
3139
+ * The new key pair, in the vocabulary the Next.js login interceptor already
3140
+ * writes: `renew/verify` is on that interceptor's path list, so these arrive
3141
+ * exactly as they do on a login.
3142
+ */
3143
+ keyId: string;
3144
+ publicKey: string;
3145
+ fingerprint: string;
3146
+ algorithm?: KeyAlgorithmType;
3147
+ }
3148
+ /**
3149
+ * What a completed renewal answers: a sign-in result, plus the new key's id.
3150
+ *
3151
+ * The id is the one thing a renewal has that a sign-in does not need to say —
3152
+ * `renewSession()` promises it to the app, which has no other way to learn it
3153
+ * (the key pair is minted in the proxy and the private half never leaves the
3154
+ * cookie). It is not a contract operation, so nothing generated reads it.
3155
+ */
3156
+ interface SessionRenewResult extends LoginResult {
3157
+ /** The key this renewal registered, the one the session now signs with. */
3158
+ keyId: string;
3159
+ }
3160
+ /**
3161
+ * Step 1 — the challenge the authenticator signs.
3162
+ *
3163
+ * `allowCredentials` is empty and the account lives only on the challenge row.
3164
+ * See `startRenewalCeremonyService`.
3165
+ *
3166
+ * @throws SessionRenewalRefusedError 갱신할 수 없는 키·계정일 때 (모든 사유 동일)
3167
+ */
3168
+ declare function startSessionRenewService(params: StartSessionRenewParams): Promise<PublicKeyCredentialRequestOptionsJSON>;
3169
+ /**
3170
+ * Step 2 — verify the assertion, put a new bound key in place of the old one.
3171
+ *
3172
+ * The revocation runs first and its answer is the race winner: two verifies that
3173
+ * both got past their own challenges meet at the same conditional UPDATE, and
3174
+ * only the one that actually revoked the key goes on to register a replacement.
3175
+ *
3176
+ * The new key inherits the old row's provenance, so the device list keeps saying
3177
+ * where this device first appeared rather than re-stamping itself every day. Its
3178
+ * expiry is a fresh window from now — renewal is a renewal, not an extension of
3179
+ * what the old key had.
3180
+ *
3181
+ * @throws SessionRenewalRefusedError 갱신할 수 없을 때 (증명 실패 포함, 모든 사유 동일)
3182
+ */
3183
+ declare function finishSessionRenewService(params: FinishSessionRenewParams): Promise<SessionRenewResult>;
3184
+
3185
+ /**
3186
+ * What `POST /_auth/oauth/finalize` answers with, in both of its branches.
3187
+ *
3188
+ * One type with a required discriminant rather than a union, on the same
3189
+ * reasoning as `LoginResult` (#95): `authApi.oauthFinalize` infers its result
3190
+ * from this, and a union would make every `result.userId` in every callback page
3191
+ * stop compiling. `mfaRequired` false is the 200 and carries `userId`/`keyId`;
3192
+ * true is the 202 and carries `challenge`.
3193
+ */
3194
+ interface OAuthFinalizeResponse {
3195
+ success: boolean;
3196
+ mfaRequired: boolean;
3197
+ /** The second-factor challenge, echoed back. Present exactly on the 202. */
3198
+ challenge?: string;
3199
+ userId?: string;
3200
+ keyId?: string;
3201
+ returnUrl: string;
3202
+ sessionBinding?: SessionBindingType;
3203
+ keyExpiresAtMillis?: number;
3204
+ }
3205
+
2718
3206
  /**
2719
3207
  * @spfn/auth - Main Router
2720
3208
  *
@@ -2822,7 +3310,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
2822
3310
  deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2823
3311
  platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2824
3312
  }>;
2825
- }, RegisterResult & LoginBindingFields>;
3313
+ }, LoginResult>;
2826
3314
  login: _spfn_core_route.RouteDef<{
2827
3315
  body: _sinclair_typebox.TObject<{
2828
3316
  email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
@@ -2863,6 +3351,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
2863
3351
  sessionBinding?: "none" | "passkey" | undefined;
2864
3352
  keyExpiresAtMillis?: number | undefined;
2865
3353
  status: "approved";
3354
+ mfaRequired: boolean;
2866
3355
  userId: string;
2867
3356
  publicId: string;
2868
3357
  passwordChangeRequired: boolean;
@@ -2966,6 +3455,19 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
2966
3455
  mfaStepUpOptions: _spfn_core_route.RouteDef<{
2967
3456
  body: _sinclair_typebox.TObject<{}>;
2968
3457
  }, {}, _simplewebauthn_server.PublicKeyCredentialRequestOptionsJSON>;
3458
+ mfaVerify: _spfn_core_route.RouteDef<{
3459
+ body: _sinclair_typebox.TObject<{
3460
+ challenge: _sinclair_typebox.TString;
3461
+ code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3462
+ recoveryCode: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3463
+ response: _sinclair_typebox.TOptional<_sinclair_typebox.TUnknown>;
3464
+ }>;
3465
+ }, {}, VerifyMfaChallengeResult>;
3466
+ mfaVerifyOptions: _spfn_core_route.RouteDef<{
3467
+ body: _sinclair_typebox.TObject<{
3468
+ challenge: _sinclair_typebox.TString;
3469
+ }>;
3470
+ }, {}, _simplewebauthn_server.PublicKeyCredentialRequestOptionsJSON>;
2969
3471
  logout: _spfn_core_route.RouteDef<{}, {}, void>;
2970
3472
  rotateKey: _spfn_core_route.RouteDef<{}, {
2971
3473
  body: _sinclair_typebox.TObject<{
@@ -3121,18 +3623,12 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3121
3623
  }>;
3122
3624
  oauthFinalize: _spfn_core_route.RouteDef<{
3123
3625
  body: _sinclair_typebox.TObject<{
3124
- userId: _sinclair_typebox.TString;
3125
- keyId: _sinclair_typebox.TString;
3626
+ userId: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3627
+ keyId: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3628
+ mfaChallenge: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3126
3629
  returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3127
3630
  }>;
3128
- }, {}, {
3129
- sessionBinding?: SessionBindingType;
3130
- keyExpiresAtMillis?: number;
3131
- success: boolean;
3132
- userId: string;
3133
- keyId: string;
3134
- returnUrl: string;
3135
- }>;
3631
+ }, {}, OAuthFinalizeResponse>;
3136
3632
  oauthProviderStart: _spfn_core_route.RouteDef<{
3137
3633
  params: _sinclair_typebox.TObject<{
3138
3634
  provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
@@ -3830,6 +4326,21 @@ declare const userPublicKeys: drizzle_orm_pg_core.PgTableWithColumns<{
3830
4326
  identity: undefined;
3831
4327
  generated: undefined;
3832
4328
  }>;
4329
+ pendingMfaChallengeId: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgBigInt53Builder, {
4330
+ name: string;
4331
+ tableName: "user_public_keys";
4332
+ dataType: "number int53";
4333
+ data: number;
4334
+ driverParam: string | number;
4335
+ notNull: false;
4336
+ hasDefault: false;
4337
+ isPrimaryKey: false;
4338
+ isAutoincrement: false;
4339
+ hasRuntimeDefault: false;
4340
+ enumValues: undefined;
4341
+ identity: undefined;
4342
+ generated: undefined;
4343
+ }>;
3833
4344
  createdAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTimestampBuilder>>, {
3834
4345
  name: string;
3835
4346
  tableName: "user_public_keys";
@@ -4243,4 +4754,4 @@ declare const machineAuth: _spfn_core_route.NamedMiddleware<"machineAuth">;
4243
4754
  */
4244
4755
  declare const requireMachineScope: _spfn_core_route.NamedMiddlewareFactory<"machineScope", string[]>;
4245
4756
 
4246
- export { type AuthLoginPayload as $, type AuthInitOptions as A, type Passkey as B, type ConfirmSignupLinkResult as C, type DeviceAuthInfoResult as D, type MfaVerification as E, type FinishPasskeyEnrollmentResult as F, type MfaVerificationMethod as G, type UserPublicKey as H, type IssueOneTimeTokenResult as I, type AuthContext as J, type KeySummary as K, type LoginBindingFields as L, type MfaStatus as M, type NewUserPublicKey as N, type OAuthStartResult as O, type PermissionConfig as P, type ApproveDeviceAuthParams as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type TotpEnrolmentResult as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type AssertStepUpParams as W, type AuthDeletionCancelledPayload as X, type AuthDeletionCompletedPayload as Y, type AuthDeletionRequestedPayload as Z, type AuthDeviceRegisteredPayload as _, type RegisterResult as a, type RevokeAllKeysParams as a$, type AuthPasswordResetPayload as a0, type AuthProfileOutcome as a1, type AuthProfileVerifier as a2, AuthProviderSchema as a3, type AuthRegisterPayload as a4, type BearerOutcome as a5, type BearerRefusal as a6, type ChangePasswordParams as a7, type CompletePasswordResetParams as a8, type CompleteSignupParams as a9, type NativeVerifyOptions as aA, type NewMfaVerification as aB, type NormalizedIdentity as aC, type OAuth2AuthorizeParams as aD, type OAuth2ScopeDescription as aE, type OAuthCallbackParams as aF, type OAuthCallbackResult as aG, type OAuthCodeExchangeOptions as aH, type OAuthNativeParams as aI, type OAuthStartParams as aJ, type OAuthTokens as aK, type OAuthUnlinkedPayload as aL, PASSKEY_DEVICE_TYPES as aM, PASSKEY_LABEL_MAX_LENGTH as aN, type PasskeyDeviceType as aO, PasswordSchema as aP, PhoneSchema as aQ, PlatformSchema as aR, type PollDeviceAuthParams as aS, type PollDeviceAuthResult as aT, PublicKeySchema as aU, type RecentAuthenticationParams as aV, type RegisterParams as aW, type RegisterPublicKeyParams as aX, type RenamePasskeyParams as aY, type RequestPasswordResetParams as aZ, type RequestSignupLinkParams as a_, type ConfirmPasswordResetParams as aa, type ConfirmSignupLinkParams as ab, type ConfirmTotpParams as ac, type DenyDeviceAuthParams as ad, type DeviceAuthApprovedResult as ae, type DeviceAuthInfoParams as af, type DeviceAuthPendingResult as ag, DeviceAuthPollResponseSchema as ah, DeviceNameSchema as ai, type DeviceRegistrationChannel as aj, type DisableSessionBindingParams as ak, EmailSchema as al, FingerprintSchema as am, type FinishPasskeyEnrollmentParams as an, type FinishPasskeyLoginParams as ao, type FinishSessionRenewParams as ap, type InvitationAcceptedPayload as aq, type InvitationCreatedPayload as ar, KEY_FINGERPRINT_PREFIX_LENGTH as as, KeyIdSchema as at, type LoginParams as au, type LogoutParams as av, MFA_VERIFICATION_METHODS as aw, type MachinePrincipal as ax, type MachineVerifierRegistration as ay, type MarkPasskeyParams as az, type RequestSignupLinkResult as b, listOAuth2GrantsService as b$, type RevokeKeyParams as b0, type RevokePasskeyParams as b1, type RotateKeyParams as b2, type SendVerificationCodeParams as b3, type SessionBindingParams as b4, type StartDeviceAuthParams as b5, type StartPasskeyEnrollmentParams as b6, type StartSessionRenewParams as b7, type StepUpParams as b8, TargetTypeSchema as b9, completePasswordResetService as bA, completeSignupService as bB, confirmPasswordResetService as bC, confirmSignupLinkService as bD, confirmTotpEnrolmentService as bE, denyDeviceAuthService as bF, denyOAuth2AuthorizeService as bG, describeOAuth2AuthorizeRequestService as bH, disableMfaService as bI, disableSessionBindingService as bJ, enableSessionBindingService as bK, finishPasskeyEnrollmentService as bL, finishPasskeyLoginService as bM, finishSessionRenewService as bN, getDeviceAuthInfoService as bO, getEnabledOAuthProviders as bP, getGoogleAccessToken as bQ, getMachinePrincipal as bR, getOAuthProvider as bS, getRegisteredProviders as bT, getSessionBindingService as bU, invitationAcceptedEvent as bV, invitationCreatedEvent as bW, isOAuthProviderEnabled as bX, issueOneTimeTokenService as bY, keySessionBindingService as bZ, listKeysService as b_, type UnlinkNotification as ba, UnlinkNotifyRejection as bb, type UnlinkNotifyRequest as bc, type UnlinkNotifyResult as bd, UserCodeSchema as be, VerificationPurposeSchema as bf, type VerifyCodeParams as bg, type VerifyCodeResult as bh, admitBearerKey as bi, approveDeviceAuthService as bj, approveOAuth2AuthorizeService as bk, assertNotLastRecoveryCredential as bl, assertRecentAuthentication as bm, assertStepUp as bn, authDeletionCancelledEvent as bo, authDeletionCompletedEvent as bp, authDeletionRequestedEvent as bq, authDeviceRegisteredEvent as br, authLoginEvent as bs, authPasswordResetEvent as bt, authRegisterEvent as bu, authenticate as bv, bearerAuthContext as bw, buildOAuthErrorUrl as bx, carryStepUpVerification as by, changePasswordService as bz, type RequestPasswordResetResult as c, listPasskeysService as c0, loginService as c1, logoutService as c2, machineAuth as c3, markPasskeySecondFactorService as c4, mfaEnrolledForUser as c5, mfaStatusService as c6, mfaVerifications as c7, oauthCallbackService as c8, oauthNativeService as c9, sendVerificationCodeService as cA, startDeviceAuthService as cB, startPasskeyEnrollmentService as cC, startPasskeyLoginService as cD, startSessionBindingDisableService as cE, startSessionRenewService as cF, startStepUpService as cG, startTotpEnrolmentService as cH, stepUpService as cI, sweepUnconfirmedMfaService as cJ, userPublicKeys as cK, verifyCodeService as cL, verifyOneTimeTokenService as cM, verifySecondFactor as cN, oauthStartService as ca, oauthUnlinkNotifyService as cb, oauthUnlinkedEvent as cc, optionalAuth as cd, passkeys as ce, pollDeviceAuthService as cf, regenerateRecoveryCodesService as cg, registerAuthProfile as ch, registerMachineVerifier as ci, registerOAuthProvider as cj, registerPublicKeyService as ck, registerService as cl, renamePasskeyService as cm, requestPasswordResetService as cn, requestSignupLinkService as co, requireEnabledProvider as cp, requireMachineScope as cq, resolveAuthenticatedUser as cr, revokeAllKeysService as cs, revokeAllOAuth2GrantsForUser as ct, revokeKeyService as cu, revokeOAuth2GrantService as cv, revokePasskeyService as cw, rotateKeyService as cx, runAuthProfile as cy, selectAuthProfile as cz, type ConfirmPasswordResetResult as d, type LoginResult as e, type StartDeviceAuthResult as f, type PasskeySummary as g, type ConfirmTotpResult as h, type RotateKeyResult as i, type RevokeAllKeysResult as j, type SessionBindingResult as k, type SessionRenewResult as l, mainAuthRouter as m, type OAuthNativeResult as n, type ProfileInfo as o, type OAuth2ConsentView as p, type OAuth2AuthorizationCodeIssued as q, type OAuth2GrantSummary as r, type AuthSession as s, PERMISSION_CATEGORIES as t, type PermissionCategory as u, VERIFICATION_TARGET_TYPES as v, type VerificationPurpose as w, type VerificationTargetType as x, type OAuthProvider as y, type NewPasskey as z };
4757
+ export { type AuthDeletionCompletedPayload as $, type AuthInitOptions as A, type NewPasskey as B, type ConfirmSignupLinkResult as C, type DeviceAuthInfoResult as D, type Passkey as E, type FinishPasskeyEnrollmentResult as F, type MfaVerification as G, type MfaVerificationMethod as H, type IssueOneTimeTokenResult as I, type NewMfaChallenge as J, type KeySummary as K, type LoginResult as L, type MfaStatus as M, type NewUserPublicKey as N, type OAuthStartResult as O, type PermissionConfig as P, type MfaChallenge as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type TotpEnrolmentResult as T, type UserProfile as U, type VerifyMfaChallengeResult as V, type UserPublicKey as W, type AuthContext as X, type ApproveDeviceAuthParams as Y, type AssertStepUpParams as Z, type AuthDeletionCancelledPayload as _, type RegisterResult as a, type PollDeviceAuthParams as a$, type AuthDeletionRequestedPayload as a0, type AuthDeviceRegisteredPayload as a1, type AuthLoginPayload as a2, type AuthPasswordResetPayload as a3, type AuthProfileOutcome as a4, type AuthProfileVerifier as a5, AuthProviderSchema as a6, type AuthRegisterPayload as a7, type BearerOutcome as a8, type BearerRefusal as a9, MFA_CHALLENGE_ATTEMPT_LIMIT as aA, MFA_CHALLENGE_CHANNELS as aB, MFA_VERIFICATION_METHODS as aC, type MachinePrincipal as aD, type MachineVerifierRegistration as aE, type MarkPasskeyParams as aF, type MfaChallengeChannel as aG, type MfaChallengeHandle as aH, type NativeVerifyOptions as aI, type NewMfaVerification as aJ, type NormalizedIdentity as aK, type OAuth2AuthorizeParams as aL, type OAuth2ScopeDescription as aM, type OAuthCallbackParams as aN, type OAuthCallbackResult as aO, type OAuthCodeExchangeOptions as aP, type OAuthNativeParams as aQ, type OAuthStartParams as aR, type OAuthTokens as aS, type OAuthUnlinkedPayload as aT, type OpenStepUpChallengeParams as aU, PASSKEY_DEVICE_TYPES as aV, PASSKEY_LABEL_MAX_LENGTH as aW, type PasskeyDeviceType as aX, PasswordSchema as aY, PhoneSchema as aZ, PlatformSchema as a_, type ChangePasswordParams as aa, type CompletePasswordResetParams as ab, type CompleteSignupParams as ac, type ConfirmPasswordResetParams as ad, type ConfirmSignupLinkParams as ae, type ConfirmTotpParams as af, type DeferredLoginEvent as ag, type DenyDeviceAuthParams as ah, type DeviceAuthApprovedResult as ai, type DeviceAuthInfoParams as aj, type DeviceAuthPendingResult as ak, DeviceAuthPollResponseSchema as al, DeviceNameSchema as am, type DeviceRegistrationChannel as an, type DisableSessionBindingParams as ao, EmailSchema as ap, FingerprintSchema as aq, type FinishPasskeyEnrollmentParams as ar, type FinishPasskeyLoginParams as as, type FinishSessionRenewParams as at, type InvitationAcceptedPayload as au, type InvitationCreatedPayload as av, KEY_FINGERPRINT_PREFIX_LENGTH as aw, KeyIdSchema as ax, type LoginParams as ay, type LogoutParams as az, type RequestSignupLinkResult as b, getEnabledOAuthProviders as b$, type PollDeviceAuthResult as b0, PublicKeySchema as b1, type RecentAuthenticationParams as b2, type RegisterParams as b3, type RegisterPublicKeyParams as b4, type RegisterPublicKeyResult as b5, type RenamePasskeyParams as b6, type RequestPasswordResetParams as b7, type RequestSignupLinkParams as b8, type RevokeAllKeysParams as b9, authDeletionCancelledEvent as bA, authDeletionCompletedEvent as bB, authDeletionRequestedEvent as bC, authDeviceRegisteredEvent as bD, authLoginEvent as bE, authPasswordResetEvent as bF, authRegisterEvent as bG, authenticate as bH, bearerAuthContext as bI, buildOAuthErrorUrl as bJ, carryStepUpVerification as bK, changePasswordService as bL, completePasswordResetService as bM, completeSignupService as bN, confirmPasswordResetService as bO, confirmSignupLinkService as bP, confirmTotpEnrolmentService as bQ, denyDeviceAuthService as bR, denyOAuth2AuthorizeService as bS, describeOAuth2AuthorizeRequestService as bT, disableMfaService as bU, disableSessionBindingService as bV, enableSessionBindingService as bW, finishPasskeyEnrollmentService as bX, finishPasskeyLoginService as bY, finishSessionRenewService as bZ, getDeviceAuthInfoService as b_, type RevokeKeyParams as ba, type RevokePasskeyParams as bb, type RotateKeyParams as bc, type SendVerificationCodeParams as bd, type SessionBindingParams as be, type StartDeviceAuthParams as bf, type StartPasskeyEnrollmentParams as bg, type StartSessionRenewParams as bh, type StepUpParams as bi, TargetTypeSchema as bj, type UnlinkNotification as bk, UnlinkNotifyRejection as bl, type UnlinkNotifyRequest as bm, type UnlinkNotifyResult as bn, UserCodeSchema as bo, VerificationPurposeSchema as bp, type VerifyCodeParams as bq, type VerifyCodeResult as br, type VerifyMfaChallengeParams as bs, admitBearerKey as bt, approveDeviceAuthService as bu, approveOAuth2AuthorizeService as bv, assertNotLastRecoveryCredential as bw, assertRecentAuthentication as bx, assertStepUp as by, attemptSecondFactor as bz, type RequestPasswordResetResult as c, sweepUnconfirmedMfaService as c$, getGoogleAccessToken as c0, getMachinePrincipal as c1, getOAuthProvider as c2, getRegisteredProviders as c3, getSessionBindingService as c4, invitationAcceptedEvent as c5, invitationCreatedEvent as c6, isOAuthProviderEnabled as c7, issueOneTimeTokenService as c8, keySessionBindingService as c9, registeredBinding as cA, renamePasskeyService as cB, requestPasswordResetService as cC, requestSignupLinkService as cD, requireEnabledProvider as cE, requireMachineScope as cF, resolveAuthenticatedUser as cG, resumeStepUpChallengeService as cH, revokeAllKeysService as cI, revokeAllOAuth2GrantsForUser as cJ, revokeKeyService as cK, revokeOAuth2GrantService as cL, revokePasskeyService as cM, rotateKeyService as cN, runAuthProfile as cO, selectAuthProfile as cP, sendVerificationCodeService as cQ, startDeviceAuthService as cR, startMfaChallengeAssertionService as cS, startPasskeyEnrollmentService as cT, startPasskeyLoginService as cU, startSessionBindingDisableService as cV, startSessionRenewService as cW, startStepUpService as cX, startTotpEnrolmentService as cY, stepUpService as cZ, sweepMfaChallengesService as c_, listKeysService as ca, listOAuth2GrantsService as cb, listPasskeysService as cc, loginService as cd, logoutService as ce, machineAuth as cf, markPasskeySecondFactorService as cg, mfaChallenges as ch, mfaEnrolledForUser as ci, mfaStatusService as cj, mfaVerifications as ck, oauthCallbackService as cl, oauthNativeService as cm, oauthStartService as cn, oauthUnlinkNotifyService as co, oauthUnlinkedEvent as cp, openStepUpChallengeService as cq, optionalAuth as cr, passkeys as cs, pollDeviceAuthService as ct, regenerateRecoveryCodesService as cu, registerAuthProfile as cv, registerMachineVerifier as cw, registerOAuthProvider as cx, registerPublicKeyService as cy, registerService as cz, type ConfirmPasswordResetResult as d, userPublicKeys as d0, verifyCodeService as d1, verifyMfaChallengeService as d2, verifyOneTimeTokenService as d3, verifySecondFactor as d4, type StartDeviceAuthResult as e, type PasskeySummary as f, type ConfirmTotpResult as g, type RotateKeyResult as h, type RevokeAllKeysResult as i, type SessionBindingResult as j, type SessionRenewResult as k, type OAuthFinalizeResponse as l, mainAuthRouter as m, type OAuthNativeResult as n, type ProfileInfo as o, type OAuth2ConsentView as p, type OAuth2AuthorizationCodeIssued as q, type OAuth2GrantSummary as r, type AuthSession as s, PERMISSION_CATEGORIES as t, type PermissionCategory as u, VERIFICATION_PURPOSES as v, VERIFICATION_TARGET_TYPES as w, type VerificationPurpose as x, type VerificationTargetType as y, type OAuthProvider as z };