@spfn/auth 0.3.0-beta.27 → 0.3.0-beta.28

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 { S as SessionBindingType, K as KeyAlgorithmType, h as KeyPlatformType, l as SocialProvider } from './types-CTdoTOxM.js';
2
1
  import * as _simplewebauthn_server from '@simplewebauthn/server';
3
- import { RegistrationResponseJSON, AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
2
+ import { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/server';
3
+ import { S as SessionBindingType, K as KeyAlgorithmType, h as KeyPlatformType, l as SocialProvider } from './types-CTdoTOxM.js';
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';
@@ -10,85 +10,283 @@ import { Context } from 'hono';
10
10
  import { User } from '@spfn/auth/server';
11
11
 
12
12
  /**
13
- * Role information for client/API responses
13
+ * @spfn/auth - Passkeys Entity
14
+ *
15
+ * A WebAuthn credential the account owner enrolled on one of their devices.
16
+ * It is a *credential*, not a session: an assertion proves who is asking, and
17
+ * the ordinary device key in `user_public_keys` is what the request afterwards
18
+ * is signed with. The two tables therefore never stand in for each other.
19
+ *
20
+ * Nothing here is a bearer value, so nothing is hashed. `publicKey` is public by
21
+ * construction and `credentialId` is a handle the authenticator hands to any
22
+ * origin that asks — storing either in the clear costs nothing, and the lookup
23
+ * on `credentialId` has to be a plain equality match on an indexed column.
24
+ *
25
+ * Revocation is soft, and `credentialId` stays unique across live and revoked
26
+ * rows alike: a credential someone cut off must never become enrollable again,
27
+ * on this account or on another one.
14
28
  */
15
- interface Role {
16
- id: number;
17
- name: string;
18
- displayName: string;
19
- description: string | null;
20
- isBuiltin: boolean;
21
- isSystem: boolean;
22
- isActive: boolean;
23
- priority: number;
24
- createdAt: Date;
25
- updatedAt: Date;
26
- }
27
29
  /**
28
- * Permission information for client/API responses
29
- */
30
- interface Permission {
31
- id: number;
32
- name: string;
33
- displayName: string;
34
- description: string | null;
35
- category: string | null;
36
- isBuiltin: boolean;
37
- isSystem: boolean;
38
- isActive: boolean;
39
- metadata: Record<string, any> | null;
40
- createdAt: Date;
41
- updatedAt: Date;
42
- }
43
- interface AuthSession {
44
- userId: number;
45
- publicId: string;
46
- email: string | null;
47
- emailVerified: boolean;
48
- phoneVerified: boolean;
49
- hasPassword: boolean;
50
- role: Role;
51
- permissions: Permission[];
52
- }
53
- interface ProfileInfo {
54
- profileId: number;
55
- displayName: string | null;
56
- firstName: string | null;
57
- lastName: string | null;
58
- avatarUrl: string | null;
59
- bio: string | null;
60
- locale: string;
61
- timezone: string;
62
- website: string | null;
63
- location: string | null;
64
- company: string | null;
65
- jobTitle: string | null;
66
- metadata: Record<string, any> | null;
67
- createdAt: Date;
68
- updatedAt: Date;
69
- }
70
- /**
71
- * User Profile Response
72
- *
73
- * Complete user data including:
74
- * - User fields at top level (userId, email, etc.)
75
- * - Profile data as nested field (optional)
30
+ * Whether the credential can leave the authenticator that minted it.
76
31
  *
77
- * Excludes:
78
- * - Role and permissions (use auth session API)
32
+ * `multiDevice` is a synced passkey (iCloud Keychain, Google Password Manager);
33
+ * `singleDevice` is bound to one authenticator. Reported by the authenticator at
34
+ * enrollment and shown in the management list, because "this one is only on that
35
+ * phone" is what the owner needs to know before revoking the other entry.
79
36
  */
80
- interface UserProfile {
81
- userId: number;
82
- publicId: string;
83
- email: string | null;
84
- username: string | null;
85
- emailVerified: boolean;
86
- phoneVerified: boolean;
87
- lastLoginAt: Date | null;
88
- createdAt: Date;
89
- updatedAt: Date;
90
- profile: ProfileInfo | null;
91
- }
37
+ declare const PASSKEY_DEVICE_TYPES: readonly ["singleDevice", "multiDevice"];
38
+ type PasskeyDeviceType = typeof PASSKEY_DEVICE_TYPES[number];
39
+ /** How long a label may be — the key list's `deviceName` bound, for the same reason. */
40
+ declare const PASSKEY_LABEL_MAX_LENGTH = 64;
41
+ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
42
+ name: "passkeys";
43
+ schema: string;
44
+ columns: {
45
+ createdAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>, {
46
+ name: string;
47
+ tableName: "passkeys";
48
+ dataType: "object date";
49
+ data: Date;
50
+ driverParam: string;
51
+ notNull: true;
52
+ hasDefault: true;
53
+ isPrimaryKey: false;
54
+ isAutoincrement: false;
55
+ hasRuntimeDefault: false;
56
+ enumValues: undefined;
57
+ identity: undefined;
58
+ generated: undefined;
59
+ }>;
60
+ 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>>>, {
61
+ name: string;
62
+ tableName: "passkeys";
63
+ dataType: "object date";
64
+ data: Date;
65
+ driverParam: string;
66
+ notNull: true;
67
+ hasDefault: true;
68
+ isPrimaryKey: false;
69
+ isAutoincrement: false;
70
+ hasRuntimeDefault: false;
71
+ enumValues: undefined;
72
+ identity: undefined;
73
+ generated: undefined;
74
+ }>;
75
+ id: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
76
+ name: string;
77
+ tableName: "passkeys";
78
+ dataType: "number int53";
79
+ data: number;
80
+ driverParam: number;
81
+ notNull: true;
82
+ hasDefault: true;
83
+ isPrimaryKey: false;
84
+ isAutoincrement: false;
85
+ hasRuntimeDefault: false;
86
+ enumValues: undefined;
87
+ identity: undefined;
88
+ generated: undefined;
89
+ }>;
90
+ userId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
91
+ name: string;
92
+ tableName: "passkeys";
93
+ dataType: "number int53";
94
+ data: number;
95
+ driverParam: string | number;
96
+ notNull: true;
97
+ hasDefault: false;
98
+ isPrimaryKey: false;
99
+ isAutoincrement: false;
100
+ hasRuntimeDefault: false;
101
+ enumValues: undefined;
102
+ identity: undefined;
103
+ generated: undefined;
104
+ }>;
105
+ credentialId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
106
+ name: string;
107
+ tableName: "passkeys";
108
+ dataType: "string";
109
+ data: string;
110
+ driverParam: string;
111
+ notNull: true;
112
+ hasDefault: false;
113
+ isPrimaryKey: false;
114
+ isAutoincrement: false;
115
+ hasRuntimeDefault: false;
116
+ enumValues: undefined;
117
+ identity: undefined;
118
+ generated: undefined;
119
+ }>;
120
+ publicKey: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
121
+ name: string;
122
+ tableName: "passkeys";
123
+ dataType: "string";
124
+ data: string;
125
+ driverParam: string;
126
+ notNull: true;
127
+ hasDefault: false;
128
+ isPrimaryKey: false;
129
+ isAutoincrement: false;
130
+ hasRuntimeDefault: false;
131
+ enumValues: undefined;
132
+ identity: undefined;
133
+ generated: undefined;
134
+ }>;
135
+ counter: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgIntegerBuilder>>, {
136
+ name: string;
137
+ tableName: "passkeys";
138
+ dataType: "number int32";
139
+ data: number;
140
+ driverParam: string | number;
141
+ notNull: true;
142
+ hasDefault: true;
143
+ isPrimaryKey: false;
144
+ isAutoincrement: false;
145
+ hasRuntimeDefault: false;
146
+ enumValues: undefined;
147
+ identity: undefined;
148
+ generated: undefined;
149
+ }>;
150
+ transports: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetDimensions<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, 1>, {
151
+ name: string;
152
+ tableName: "passkeys";
153
+ dataType: "string";
154
+ data: string[];
155
+ driverParam: string | string[];
156
+ notNull: false;
157
+ hasDefault: false;
158
+ isPrimaryKey: false;
159
+ isAutoincrement: false;
160
+ hasRuntimeDefault: false;
161
+ enumValues: undefined;
162
+ identity: undefined;
163
+ generated: undefined;
164
+ }>;
165
+ deviceType: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["singleDevice", "multiDevice"] & [string, ...string[]]>>, {
166
+ name: string;
167
+ tableName: "passkeys";
168
+ dataType: "string enum";
169
+ data: "singleDevice" | "multiDevice";
170
+ driverParam: string;
171
+ notNull: true;
172
+ hasDefault: false;
173
+ isPrimaryKey: false;
174
+ isAutoincrement: false;
175
+ hasRuntimeDefault: false;
176
+ enumValues: ["singleDevice", "multiDevice"] & [string, ...string[]];
177
+ identity: undefined;
178
+ generated: undefined;
179
+ }>;
180
+ backedUp: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
181
+ name: string;
182
+ tableName: "passkeys";
183
+ dataType: "boolean";
184
+ data: boolean;
185
+ driverParam: boolean;
186
+ notNull: true;
187
+ hasDefault: true;
188
+ isPrimaryKey: false;
189
+ isAutoincrement: false;
190
+ hasRuntimeDefault: false;
191
+ enumValues: undefined;
192
+ identity: undefined;
193
+ generated: undefined;
194
+ }>;
195
+ aaguid: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
196
+ name: string;
197
+ tableName: "passkeys";
198
+ dataType: "string";
199
+ data: string;
200
+ driverParam: string;
201
+ notNull: false;
202
+ hasDefault: false;
203
+ isPrimaryKey: false;
204
+ isAutoincrement: false;
205
+ hasRuntimeDefault: false;
206
+ enumValues: undefined;
207
+ identity: undefined;
208
+ generated: undefined;
209
+ }>;
210
+ label: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
211
+ name: string;
212
+ tableName: "passkeys";
213
+ dataType: "string";
214
+ data: string;
215
+ driverParam: string;
216
+ notNull: false;
217
+ hasDefault: false;
218
+ isPrimaryKey: false;
219
+ isAutoincrement: false;
220
+ hasRuntimeDefault: false;
221
+ enumValues: undefined;
222
+ identity: undefined;
223
+ generated: undefined;
224
+ }>;
225
+ secondFactor: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
226
+ name: string;
227
+ tableName: "passkeys";
228
+ dataType: "boolean";
229
+ data: boolean;
230
+ driverParam: boolean;
231
+ notNull: true;
232
+ hasDefault: true;
233
+ isPrimaryKey: false;
234
+ isAutoincrement: false;
235
+ hasRuntimeDefault: false;
236
+ enumValues: undefined;
237
+ identity: undefined;
238
+ generated: undefined;
239
+ }>;
240
+ lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
241
+ name: string;
242
+ tableName: "passkeys";
243
+ dataType: "object date";
244
+ data: Date;
245
+ driverParam: string;
246
+ notNull: false;
247
+ hasDefault: false;
248
+ isPrimaryKey: false;
249
+ isAutoincrement: false;
250
+ hasRuntimeDefault: false;
251
+ enumValues: undefined;
252
+ identity: undefined;
253
+ generated: undefined;
254
+ }>;
255
+ revokedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
256
+ name: string;
257
+ tableName: "passkeys";
258
+ dataType: "object date";
259
+ data: Date;
260
+ driverParam: string;
261
+ notNull: false;
262
+ hasDefault: false;
263
+ isPrimaryKey: false;
264
+ isAutoincrement: false;
265
+ hasRuntimeDefault: false;
266
+ enumValues: undefined;
267
+ identity: undefined;
268
+ generated: undefined;
269
+ }>;
270
+ revokedReason: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
271
+ name: string;
272
+ tableName: "passkeys";
273
+ dataType: "string";
274
+ data: string;
275
+ driverParam: string;
276
+ notNull: false;
277
+ hasDefault: false;
278
+ isPrimaryKey: false;
279
+ isAutoincrement: false;
280
+ hasRuntimeDefault: false;
281
+ enumValues: undefined;
282
+ identity: undefined;
283
+ generated: undefined;
284
+ }>;
285
+ };
286
+ dialect: "pg";
287
+ }>;
288
+ type Passkey = typeof passkeys.$inferSelect;
289
+ type NewPasskey = typeof passkeys.$inferInsert;
92
290
 
93
291
  /**
94
292
  * @spfn/auth - What a sign-in answers with
@@ -218,31 +416,119 @@ interface ChangePasswordParams {
218
416
  * adds no refusal for anybody who has not opted in.
219
417
  */
220
418
  keyId: string;
221
- currentPassword?: string;
222
- newPassword: string;
223
- passwordHash?: string;
419
+ currentPassword?: string;
420
+ newPassword: string;
421
+ passwordHash?: string;
422
+ }
423
+ /**
424
+ * Register a new user account
425
+ */
426
+ declare function registerService(params: RegisterParams): Promise<RegisterResult>;
427
+ /**
428
+ * Authenticate user and create session
429
+ */
430
+ declare function loginService(params: LoginParams): Promise<LoginResult>;
431
+ /**
432
+ * Logout user (revoke current key)
433
+ */
434
+ declare function logoutService(params: LogoutParams): Promise<void>;
435
+ /**
436
+ * Change user password
437
+ *
438
+ * An enrolled account steps up first (#95): a stolen session must not be able
439
+ * to take the account over by setting a new password. An unenrolled account is
440
+ * unaffected — including the OAuth-only account with no password and a key
441
+ * older than ten minutes, which still sets a first password and gets a 200.
442
+ */
443
+ declare function changePasswordService(params: ChangePasswordParams): Promise<void>;
444
+
445
+ /**
446
+ * @spfn/auth - Session Renewal Service
447
+ *
448
+ * What a bound session does when its key runs out: prove, with a fresh WebAuthn
449
+ * assertion, that the person who enrolled the passkey is still at the machine,
450
+ * and get a new short-lived key sealed into the cookie.
451
+ *
452
+ * Neither step is public. The expiring key is named by `expiredKeyId`, and that
453
+ * value reaches the service from `authenticateForRenewal` — the `keyId` of a
454
+ * bearer JWT this very key signed — rather than from the request body, so a
455
+ * caller who does not hold the private half cannot name a key at all. The
456
+ * assertion still has to be signed by a passkey that key's owner enrolled: the
457
+ * signature proves the cookie, and the cookie is the thing that may have been
458
+ * copied.
459
+ *
460
+ * The admission below is run again here all the same. The middleware and the
461
+ * service ask the same four questions of the row, and a service that trusted its
462
+ * caller to have asked them would be one refactor away from not being asked at
463
+ * all.
464
+ *
465
+ * Every refusal is the same refusal. A key that never existed, a stranger's key,
466
+ * an unbound key, a revoked one, one past its grace, an inactive account, a spent
467
+ * challenge, an assertion that did not verify — all `SessionRenewalRefusedError`,
468
+ * with the same body, because anything finer would answer "is this key id live"
469
+ * to whoever asked.
470
+ *
471
+ * Renewal announces nothing. No `auth.login`, no `auth.device.registered`, and
472
+ * `lastLoginAt` does not move: this is the same person on the same device
473
+ * continuing the session they already had, and a subscriber mailing "new sign-in"
474
+ * once a day per device would train its reader to ignore the notice that matters.
475
+ * A `lastLoginAt` that moved every day would make dormant-account detection
476
+ * meaningless for exactly the accounts that turned this protection on.
477
+ */
478
+
479
+ interface StartSessionRenewParams {
480
+ /** The key that ran out, read off the JWT the request was signed with. */
481
+ expiredKeyId: string;
482
+ }
483
+ interface FinishSessionRenewParams extends StartSessionRenewParams {
484
+ /** The assertion, from `navigator.credentials.get()`. */
485
+ response: AuthenticationResponseJSON;
486
+ /**
487
+ * The new key pair, in the vocabulary the Next.js login interceptor already
488
+ * writes: `renew/verify` is on that interceptor's path list, so these arrive
489
+ * exactly as they do on a login.
490
+ */
491
+ keyId: string;
492
+ publicKey: string;
493
+ fingerprint: string;
494
+ algorithm?: KeyAlgorithmType;
224
495
  }
225
496
  /**
226
- * Register a new user account
227
- */
228
- declare function registerService(params: RegisterParams): Promise<RegisterResult>;
229
- /**
230
- * Authenticate user and create session
497
+ * What a completed renewal answers: a sign-in result, plus the new key's id.
498
+ *
499
+ * The id is the one thing a renewal has that a sign-in does not need to say —
500
+ * `renewSession()` promises it to the app, which has no other way to learn it
501
+ * (the key pair is minted in the proxy and the private half never leaves the
502
+ * cookie). It is not a contract operation, so nothing generated reads it.
231
503
  */
232
- declare function loginService(params: LoginParams): Promise<LoginResult>;
504
+ interface SessionRenewResult extends LoginResult {
505
+ /** The key this renewal registered, the one the session now signs with. */
506
+ keyId: string;
507
+ }
233
508
  /**
234
- * Logout user (revoke current key)
509
+ * Step 1 — the challenge the authenticator signs.
510
+ *
511
+ * `allowCredentials` is empty and the account lives only on the challenge row.
512
+ * See `startRenewalCeremonyService`.
513
+ *
514
+ * @throws SessionRenewalRefusedError 갱신할 수 없는 키·계정일 때 (모든 사유 동일)
235
515
  */
236
- declare function logoutService(params: LogoutParams): Promise<void>;
516
+ declare function startSessionRenewService(params: StartSessionRenewParams): Promise<PublicKeyCredentialRequestOptionsJSON>;
237
517
  /**
238
- * Change user password
518
+ * Step 2 — verify the assertion, put a new bound key in place of the old one.
239
519
  *
240
- * An enrolled account steps up first (#95): a stolen session must not be able
241
- * to take the account over by setting a new password. An unenrolled account is
242
- * unaffected — including the OAuth-only account with no password and a key
243
- * older than ten minutes, which still sets a first password and gets a 200.
520
+ * The revocation runs first and its answer is the race winner: two verifies that
521
+ * both got past their own challenges meet at the same conditional UPDATE, and
522
+ * only the one that actually revoked the key goes on to register a replacement.
523
+ *
524
+ * The new key inherits the old row's provenance, so the device list keeps saying
525
+ * where this device first appeared rather than re-stamping itself every day. Its
526
+ * expiry is a fresh window from now — renewal is a renewal, not an extension of
527
+ * what the old key had.
528
+ *
529
+ * @throws SessionRenewalRefusedError 갱신할 수 없을 때 (증명 실패 포함, 모든 사유 동일)
244
530
  */
245
- declare function changePasswordService(params: ChangePasswordParams): Promise<void>;
531
+ declare function finishSessionRenewService(params: FinishSessionRenewParams): Promise<SessionRenewResult>;
246
532
 
247
533
  declare const EmailSchema: _sinclair_typebox.TString;
248
534
  declare const PhoneSchema: _sinclair_typebox.TString;
@@ -289,6 +575,15 @@ declare const FingerprintSchema: _sinclair_typebox.TString;
289
575
  * out the format — `USER_CODE_ALPHABET` is the only thing that can match a row.
290
576
  */
291
577
  declare const UserCodeSchema: _sinclair_typebox.TString;
578
+ /**
579
+ * The issuer's handle on a device link, as `issue` returned it.
580
+ *
581
+ * A UUID today; bounded rather than patterned so the handle's format stays the
582
+ * server's to change. It authorizes nothing without the issuing key beside it.
583
+ */
584
+ declare const LinkIdSchema: _sinclair_typebox.TString;
585
+ /** A number the issuer picked from the three `status` showed. */
586
+ declare const MatchChoiceSchema: _sinclair_typebox.TInteger;
292
587
  /**
293
588
  * What `POST /_auth/device/poll` answers with.
294
589
  *
@@ -565,6 +860,41 @@ interface CompletePasswordResetParams {
565
860
  */
566
861
  declare function completePasswordResetService(params: CompletePasswordResetParams): Promise<LoginResult>;
567
862
 
863
+ /**
864
+ * @spfn/auth - Device Registration Provenance
865
+ *
866
+ * What a registering request said about where it came from: the client address
867
+ * and the `user-agent` header, read once at the top of a route and carried into
868
+ * the key row so the account owner's device list can say "this one appeared
869
+ * from there".
870
+ *
871
+ * Both values are unauthenticated display material. `getClientIp` is documented
872
+ * as best-effort keying material and is spoofable on any request that is not
873
+ * proxy-verified, and a `user-agent` is whatever the caller typed — so nothing
874
+ * in this package decides anything by either one, and neither is ever compared.
875
+ */
876
+
877
+ /** Where a device key registration came from, as the request stated it. */
878
+ interface DeviceProvenance {
879
+ ip?: string;
880
+ userAgent?: string;
881
+ /**
882
+ * Whether `proxy-guard` recognised this request as the trusted Next.js
883
+ * proxy's — the one fact here that a caller cannot state about itself.
884
+ *
885
+ * The two fields above are what the request claimed; this one is what the
886
+ * signature check concluded, so it is the only part of the provenance
887
+ * anything is allowed to decide by. Session binding decides by it: a key can
888
+ * be bound only on a request that reached the backend through the proxy that
889
+ * holds the session cookie, because nothing else can run the renewal.
890
+ *
891
+ * Optional so that a caller assembling provenance by hand — a test, a job
892
+ * replaying a request — can leave it out. Absent reads as "not the proxy",
893
+ * which is the conservative answer: the key is registered unbound.
894
+ */
895
+ webProxy?: boolean;
896
+ }
897
+
568
898
  /**
569
899
  * @spfn/auth - Device Auth Service
570
900
  *
@@ -733,41 +1063,47 @@ declare function denyDeviceAuthService(params: DenyDeviceAuthParams): Promise<vo
733
1063
  declare function pollDeviceAuthService(params: PollDeviceAuthParams): Promise<PollDeviceAuthResult>;
734
1064
 
735
1065
  /**
736
- * @spfn/auth - Passkeys Entity
1066
+ * @spfn/auth - Device Links Entity
737
1067
  *
738
- * A WebAuthn credential the account owner enrolled on one of their devices.
739
- * It is a *credential*, not a session: an assertion proves who is asking, and
740
- * the ordinary device key in `user_public_keys` is what the request afterwards
741
- * is signed with. The two tables therefore never stand in for each other.
1068
+ * Backs device link: the mirror image of device-code login. A device that is
1069
+ * already signed in (the issuer) asks for a short code and shows it; a new
1070
+ * device with no key on file reads it, parks its public key here, and shows a
1071
+ * two-digit match number; the issuer picks that number out of three, and the
1072
+ * new device's next poll registers the parked key under the issuer's account.
742
1073
  *
743
- * Nothing here is a bearer value, so nothing is hashed. `publicKey` is public by
744
- * construction and `credentialId` is a handle the authenticator hands to any
745
- * origin that asks — storing either in the clear costs nothing, and the lookup
746
- * on `credentialId` has to be a plain equality match on an indexed column.
1074
+ * A separate table from `device_authorizations` rather than a mode of it. The
1075
+ * two records are owned from opposite ends — a device authorization gains an
1076
+ * account only when someone approves it, a link has one from the moment it is
1077
+ * issued, and is bound to the issuing key as well — and folding them together
1078
+ * would leave every column meaning one thing in one flow and something else in
1079
+ * the other.
747
1080
  *
748
- * Revocation is soft, and `credentialId` stays unique across live and revoked
749
- * rows alike: a credential someone cut off must never become enrollable again,
750
- * on this account or on another one.
1081
+ * The parked key material duplicates the `user_public_keys` columns for the
1082
+ * reason `device_authorizations` gives: a row there can sign requests, and
1083
+ * nothing here has been confirmed yet.
1084
+ *
1085
+ * Nothing sweeps this table. Rows are judged by `expiresAt` and by the issuing
1086
+ * key on read, so a stale row authorizes nothing.
751
1087
  */
752
1088
  /**
753
- * Whether the credential can leave the authenticator that minted it.
1089
+ * Lifecycle of one device link.
754
1090
  *
755
- * `multiDevice` is a synced passkey (iCloud Keychain, Google Password Manager);
756
- * `singleDevice` is bound to one authenticator. Reported by the authenticator at
757
- * enrollment and shown in the management list, because "this one is only on that
758
- * phone" is what the owner needs to know before revoking the other entry.
1091
+ * `issued -> redeemed -> approved -> consumed` is the only path to a registered
1092
+ * key. `denied` is a refusal the new device is told about — the issuer said no,
1093
+ * or picked the wrong number. `expired` is a link its issuer abandoned: a newer
1094
+ * one issued from the same key, a cancel, or a global revocation of the account.
1095
+ * A link whose TTL ran out, or whose issuing key was revoked, is judged expired
1096
+ * on read without the status having to say so.
759
1097
  */
760
- declare const PASSKEY_DEVICE_TYPES: readonly ["singleDevice", "multiDevice"];
761
- type PasskeyDeviceType = typeof PASSKEY_DEVICE_TYPES[number];
762
- /** How long a label may be — the key list's `deviceName` bound, for the same reason. */
763
- declare const PASSKEY_LABEL_MAX_LENGTH = 64;
764
- declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
765
- name: "passkeys";
1098
+ declare const DEVICE_LINK_STATUSES: readonly ["issued", "redeemed", "approved", "denied", "consumed", "expired"];
1099
+ type DeviceLinkStatus = typeof DEVICE_LINK_STATUSES[number];
1100
+ declare const deviceLinks: drizzle_orm_pg_core.PgTableWithColumns<{
1101
+ name: "device_links";
766
1102
  schema: string;
767
1103
  columns: {
768
- createdAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>, {
1104
+ createdAt: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>, {
769
1105
  name: string;
770
- tableName: "passkeys";
1106
+ tableName: "device_links";
771
1107
  dataType: "object date";
772
1108
  data: Date;
773
1109
  driverParam: string;
@@ -780,9 +1116,9 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
780
1116
  identity: undefined;
781
1117
  generated: undefined;
782
1118
  }>;
783
- 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>>>, {
1119
+ updatedAt: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>>, {
784
1120
  name: string;
785
- tableName: "passkeys";
1121
+ tableName: "device_links";
786
1122
  dataType: "object date";
787
1123
  data: Date;
788
1124
  driverParam: string;
@@ -795,9 +1131,9 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
795
1131
  identity: undefined;
796
1132
  generated: undefined;
797
1133
  }>;
798
- id: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
1134
+ id: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
799
1135
  name: string;
800
- tableName: "passkeys";
1136
+ tableName: "device_links";
801
1137
  dataType: "number int53";
802
1138
  data: number;
803
1139
  driverParam: number;
@@ -810,9 +1146,39 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
810
1146
  identity: undefined;
811
1147
  generated: undefined;
812
1148
  }>;
813
- userId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
1149
+ linkId: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
814
1150
  name: string;
815
- tableName: "passkeys";
1151
+ tableName: "device_links";
1152
+ dataType: "string";
1153
+ data: string;
1154
+ driverParam: string;
1155
+ notNull: true;
1156
+ hasDefault: false;
1157
+ isPrimaryKey: false;
1158
+ isAutoincrement: false;
1159
+ hasRuntimeDefault: false;
1160
+ enumValues: undefined;
1161
+ identity: undefined;
1162
+ generated: undefined;
1163
+ }>;
1164
+ userCode: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
1165
+ name: string;
1166
+ tableName: "device_links";
1167
+ dataType: "string";
1168
+ data: string;
1169
+ driverParam: string;
1170
+ notNull: true;
1171
+ hasDefault: false;
1172
+ isPrimaryKey: false;
1173
+ isAutoincrement: false;
1174
+ hasRuntimeDefault: false;
1175
+ enumValues: undefined;
1176
+ identity: undefined;
1177
+ generated: undefined;
1178
+ }>;
1179
+ issuerUserId: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
1180
+ name: string;
1181
+ tableName: "device_links";
816
1182
  dataType: "number int53";
817
1183
  data: number;
818
1184
  driverParam: string | number;
@@ -825,9 +1191,9 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
825
1191
  identity: undefined;
826
1192
  generated: undefined;
827
1193
  }>;
828
- credentialId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
1194
+ issuerKeyId: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
829
1195
  name: string;
830
- tableName: "passkeys";
1196
+ tableName: "device_links";
831
1197
  dataType: "string";
832
1198
  data: string;
833
1199
  driverParam: string;
@@ -840,13 +1206,13 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
840
1206
  identity: undefined;
841
1207
  generated: undefined;
842
1208
  }>;
843
- publicKey: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
1209
+ deviceCodeHash: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
844
1210
  name: string;
845
- tableName: "passkeys";
1211
+ tableName: "device_links";
846
1212
  dataType: "string";
847
1213
  data: string;
848
1214
  driverParam: string;
849
- notNull: true;
1215
+ notNull: false;
850
1216
  hasDefault: false;
851
1217
  isPrimaryKey: false;
852
1218
  isAutoincrement: false;
@@ -855,14 +1221,14 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
855
1221
  identity: undefined;
856
1222
  generated: undefined;
857
1223
  }>;
858
- counter: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgIntegerBuilder>>, {
1224
+ publicKey: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
859
1225
  name: string;
860
- tableName: "passkeys";
861
- dataType: "number int32";
862
- data: number;
863
- driverParam: string | number;
864
- notNull: true;
865
- hasDefault: true;
1226
+ tableName: "device_links";
1227
+ dataType: "string";
1228
+ data: string;
1229
+ driverParam: string;
1230
+ notNull: false;
1231
+ hasDefault: false;
866
1232
  isPrimaryKey: false;
867
1233
  isAutoincrement: false;
868
1234
  hasRuntimeDefault: false;
@@ -870,12 +1236,12 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
870
1236
  identity: undefined;
871
1237
  generated: undefined;
872
1238
  }>;
873
- transports: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetDimensions<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, 1>, {
1239
+ keyId: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
874
1240
  name: string;
875
- tableName: "passkeys";
1241
+ tableName: "device_links";
876
1242
  dataType: "string";
877
- data: string[];
878
- driverParam: string | string[];
1243
+ data: string;
1244
+ driverParam: string;
879
1245
  notNull: false;
880
1246
  hasDefault: false;
881
1247
  isPrimaryKey: false;
@@ -885,39 +1251,39 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
885
1251
  identity: undefined;
886
1252
  generated: undefined;
887
1253
  }>;
888
- deviceType: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["singleDevice", "multiDevice"] & [string, ...string[]]>>, {
1254
+ fingerprint: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
889
1255
  name: string;
890
- tableName: "passkeys";
891
- dataType: "string enum";
892
- data: "singleDevice" | "multiDevice";
1256
+ tableName: "device_links";
1257
+ dataType: "string";
1258
+ data: string;
893
1259
  driverParam: string;
894
- notNull: true;
1260
+ notNull: false;
895
1261
  hasDefault: false;
896
1262
  isPrimaryKey: false;
897
1263
  isAutoincrement: false;
898
1264
  hasRuntimeDefault: false;
899
- enumValues: ["singleDevice", "multiDevice"] & [string, ...string[]];
1265
+ enumValues: undefined;
900
1266
  identity: undefined;
901
1267
  generated: undefined;
902
1268
  }>;
903
- backedUp: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
1269
+ algorithm: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTextBuilder<["ES256", "RS256"] & [string, ...string[]]>, {
904
1270
  name: string;
905
- tableName: "passkeys";
906
- dataType: "boolean";
907
- data: boolean;
908
- driverParam: boolean;
909
- notNull: true;
910
- hasDefault: true;
1271
+ tableName: "device_links";
1272
+ dataType: "string enum";
1273
+ data: "ES256" | "RS256";
1274
+ driverParam: string;
1275
+ notNull: false;
1276
+ hasDefault: false;
911
1277
  isPrimaryKey: false;
912
1278
  isAutoincrement: false;
913
1279
  hasRuntimeDefault: false;
914
- enumValues: undefined;
1280
+ enumValues: ["ES256", "RS256"] & [string, ...string[]];
915
1281
  identity: undefined;
916
1282
  generated: undefined;
917
1283
  }>;
918
- aaguid: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
1284
+ deviceName: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
919
1285
  name: string;
920
- tableName: "passkeys";
1286
+ tableName: "device_links";
921
1287
  dataType: "string";
922
1288
  data: string;
923
1289
  driverParam: string;
@@ -930,39 +1296,84 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
930
1296
  identity: undefined;
931
1297
  generated: undefined;
932
1298
  }>;
933
- label: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
1299
+ platform: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTextBuilder<["ios", "android", "web", "desktop"] & [string, ...string[]]>, {
934
1300
  name: string;
935
- tableName: "passkeys";
936
- dataType: "string";
937
- data: string;
1301
+ tableName: "device_links";
1302
+ dataType: "string enum";
1303
+ data: "ios" | "android" | "web" | "desktop";
938
1304
  driverParam: string;
939
1305
  notNull: false;
940
1306
  hasDefault: false;
941
1307
  isPrimaryKey: false;
942
1308
  isAutoincrement: false;
943
1309
  hasRuntimeDefault: false;
1310
+ enumValues: ["ios", "android", "web", "desktop"] & [string, ...string[]];
1311
+ identity: undefined;
1312
+ generated: undefined;
1313
+ }>;
1314
+ matchNumber: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgIntegerBuilder, {
1315
+ name: string;
1316
+ tableName: "device_links";
1317
+ dataType: "number int32";
1318
+ data: number;
1319
+ driverParam: string | number;
1320
+ notNull: false;
1321
+ hasDefault: false;
1322
+ isPrimaryKey: false;
1323
+ isAutoincrement: false;
1324
+ hasRuntimeDefault: false;
944
1325
  enumValues: undefined;
945
1326
  identity: undefined;
946
1327
  generated: undefined;
947
1328
  }>;
948
- secondFactor: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
1329
+ choices: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetDimensions<drizzle_orm_pg_core.PgIntegerBuilder, 1>, {
949
1330
  name: string;
950
- tableName: "passkeys";
951
- dataType: "boolean";
952
- data: boolean;
953
- driverParam: boolean;
1331
+ tableName: "device_links";
1332
+ dataType: "number int32";
1333
+ data: number[];
1334
+ driverParam: string | (string | number)[];
1335
+ notNull: false;
1336
+ hasDefault: false;
1337
+ isPrimaryKey: false;
1338
+ isAutoincrement: false;
1339
+ hasRuntimeDefault: false;
1340
+ enumValues: undefined;
1341
+ identity: undefined;
1342
+ generated: undefined;
1343
+ }>;
1344
+ status: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["issued", "redeemed", "approved", "denied", "consumed", "expired"] & [string, ...string[]]>>>, {
1345
+ name: string;
1346
+ tableName: "device_links";
1347
+ dataType: "string enum";
1348
+ data: "expired" | "approved" | "denied" | "consumed" | "issued" | "redeemed";
1349
+ driverParam: string;
954
1350
  notNull: true;
955
1351
  hasDefault: true;
956
1352
  isPrimaryKey: false;
957
1353
  isAutoincrement: false;
958
1354
  hasRuntimeDefault: false;
1355
+ enumValues: ["issued", "redeemed", "approved", "denied", "consumed", "expired"] & [string, ...string[]];
1356
+ identity: undefined;
1357
+ generated: undefined;
1358
+ }>;
1359
+ expiresAt: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTimestampBuilder>, {
1360
+ name: string;
1361
+ tableName: "device_links";
1362
+ dataType: "object date";
1363
+ data: Date;
1364
+ driverParam: string;
1365
+ notNull: true;
1366
+ hasDefault: false;
1367
+ isPrimaryKey: false;
1368
+ isAutoincrement: false;
1369
+ hasRuntimeDefault: false;
959
1370
  enumValues: undefined;
960
1371
  identity: undefined;
961
1372
  generated: undefined;
962
1373
  }>;
963
- lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
1374
+ redeemedAt: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTimestampBuilder, {
964
1375
  name: string;
965
- tableName: "passkeys";
1376
+ tableName: "device_links";
966
1377
  dataType: "object date";
967
1378
  data: Date;
968
1379
  driverParam: string;
@@ -975,9 +1386,9 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
975
1386
  identity: undefined;
976
1387
  generated: undefined;
977
1388
  }>;
978
- revokedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
1389
+ approvedAt: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTimestampBuilder, {
979
1390
  name: string;
980
- tableName: "passkeys";
1391
+ tableName: "device_links";
981
1392
  dataType: "object date";
982
1393
  data: Date;
983
1394
  driverParam: string;
@@ -990,11 +1401,11 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
990
1401
  identity: undefined;
991
1402
  generated: undefined;
992
1403
  }>;
993
- revokedReason: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
1404
+ consumedAt: drizzle_orm_pg_core.PgBuildColumn<"device_links", drizzle_orm_pg_core.PgTimestampBuilder, {
994
1405
  name: string;
995
- tableName: "passkeys";
996
- dataType: "string";
997
- data: string;
1406
+ tableName: "device_links";
1407
+ dataType: "object date";
1408
+ data: Date;
998
1409
  driverParam: string;
999
1410
  notNull: false;
1000
1411
  hasDefault: false;
@@ -1008,8 +1419,162 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
1008
1419
  };
1009
1420
  dialect: "pg";
1010
1421
  }>;
1011
- type Passkey = typeof passkeys.$inferSelect;
1012
- type NewPasskey = typeof passkeys.$inferInsert;
1422
+ type DeviceLink = typeof deviceLinks.$inferSelect;
1423
+ type NewDeviceLink = typeof deviceLinks.$inferInsert;
1424
+
1425
+ /**
1426
+ * @spfn/auth - Device Link Service
1427
+ *
1428
+ * Device link: the mirror image of device-code login. A device that is already
1429
+ * signed in (the issuer) asks for a short code and shows it; a new device with no
1430
+ * key on file reads it, redeems it with its public key, and shows a two-digit
1431
+ * match number; the issuer is shown the device and three numbers, and picks the
1432
+ * one on the new device's screen. The new device's next poll registers its key
1433
+ * under the issuer's account and answers exactly what `loginService` answers —
1434
+ * the same completion device-code login uses, so the two are indistinguishable.
1435
+ *
1436
+ * | state ↓ op → | redeem | status | confirm | deny | cancel | poll |
1437
+ * | --- | --- | --- | --- | --- | --- | --- |
1438
+ * | issued | device parked, → redeemed | issued | NotRedeemed | NotRedeemed | → expired | — |
1439
+ * | redeemed | NotFound | device + choices | match: → approved; else → denied, WrongMatch | → denied | → expired | pending |
1440
+ * | approved | NotFound | approved | AlreadyHandled | AlreadyHandled | AlreadyHandled | key registered, → consumed |
1441
+ * | denied | NotFound | denied | AlreadyHandled | AlreadyHandled | AlreadyHandled | Denied |
1442
+ * | consumed | NotFound | consumed | AlreadyHandled | AlreadyHandled | AlreadyHandled | NotFound |
1443
+ * | dead | Expired | Expired | Expired | Expired | Expired | Expired |
1444
+ * | unknown | NotFound | NotFound | NotFound | NotFound | NotFound | NotFound |
1445
+ *
1446
+ * "Dead" is a link past its TTL, one its issuer cancelled or replaced or a global
1447
+ * revocation expired (status `expired`), and one whose issuing key has since been
1448
+ * revoked or run out — whatever state it was in. Expiry outranks state, with one
1449
+ * exception on the new device's side, for device-code login's reason: a spent
1450
+ * link answers redeem and poll as unknown even once its TTL has run out.
1451
+ *
1452
+ * Status, confirm, deny and cancel belong to the issuing key alone. A link
1453
+ * another key issued — another account's, or another device of the same
1454
+ * account's — answers NotFound to them before anything else is judged, so a
1455
+ * device that did not issue a link cannot learn it exists.
1456
+ *
1457
+ * Both parties can long-poll: the issuer's `status` while the link waits on the
1458
+ * new device (issued, or approved and not yet collected), the new device's `poll`
1459
+ * while it waits on the issuer (redeemed). The waits run before the routes'
1460
+ * transactions open, as device-code login's does, and every transition wakes
1461
+ * both after commit.
1462
+ *
1463
+ * Nothing here logs a user code, a device code, a match number or a key.
1464
+ */
1465
+
1466
+ /** The signed-in device a link belongs to, read from the request's principal. Never from a body. */
1467
+ interface DeviceLinkIssuer {
1468
+ userId: number;
1469
+ /** The key that signed the request. */
1470
+ keyId: string;
1471
+ }
1472
+ interface IssueDeviceLinkResult {
1473
+ /** The issuer's handle on the link, for status, confirm, deny and cancel. */
1474
+ linkId: string;
1475
+ /** `XXXX-XXXX`, for the issuer's screen — as text and in the QR the client draws. */
1476
+ userCode: string;
1477
+ /** For the countdown on the issuer's screen. Display only; the server decides by its own clock. */
1478
+ expiresAtMillis: number;
1479
+ }
1480
+ interface RedeemDeviceLinkParams {
1481
+ userCode: string;
1482
+ publicKey: string;
1483
+ keyId: string;
1484
+ fingerprint: string;
1485
+ algorithm?: KeyAlgorithmType;
1486
+ /** Device label shown to the issuer. Display only — nothing is authorized by it. */
1487
+ deviceName?: string;
1488
+ platform?: KeyPlatformType;
1489
+ }
1490
+ interface RedeemDeviceLinkResult {
1491
+ /** Returned once. The new device polls with it; the server stores only its hash. */
1492
+ deviceCode: string;
1493
+ /** The number the new device shows, 10–99, for the issuer to pick out of three. */
1494
+ matchNumber: number;
1495
+ expiresAtMillis: number;
1496
+ /** Milliseconds the new device should wait between polls. */
1497
+ intervalMillis: number;
1498
+ }
1499
+ interface DeviceLinkParams {
1500
+ linkId: string;
1501
+ issuer: DeviceLinkIssuer;
1502
+ }
1503
+ interface ConfirmDeviceLinkParams extends DeviceLinkParams {
1504
+ /** The number the issuer picked. */
1505
+ choice: number;
1506
+ }
1507
+ /**
1508
+ * The issuer's view of its link, which every issuer operation answers with.
1509
+ *
1510
+ * The device fields appear once a device has redeemed the code; `choices` only
1511
+ * while the link waits on the issuer's pick (`redeemed`), since that is the only
1512
+ * moment a number means anything. `expired` is only ever cancel's answer: a
1513
+ * link that is dead when it is asked about answers `DeviceLinkExpiredError`.
1514
+ */
1515
+ interface DeviceLinkStatusResult {
1516
+ status: DeviceLinkStatus;
1517
+ expiresAtMillis: number;
1518
+ deviceName?: string;
1519
+ platform?: KeyPlatformType;
1520
+ /** First bytes of the redeeming key's fingerprint, as the device list truncates it. */
1521
+ fingerprintPrefix?: string;
1522
+ redeemedAtMillis?: number;
1523
+ /** The match and two decoys, in the order drawn at redeem. */
1524
+ choices?: number[];
1525
+ }
1526
+ interface PollDeviceLinkParams extends DeviceProvenance {
1527
+ deviceCode: string;
1528
+ /** How long this request already waited on the server, from the long-poll middleware. */
1529
+ waitedMillis?: number;
1530
+ }
1531
+ /**
1532
+ * Issue a link from the signed-in device that asked, replacing any link that
1533
+ * device still has in play — one live link per issuing key, so a screen that was
1534
+ * closed without cancelling leaves nothing behind that a later redeem could use.
1535
+ */
1536
+ declare function issueDeviceLinkService(issuer: DeviceLinkIssuer): Promise<IssueDeviceLinkResult>;
1537
+ /**
1538
+ * Park a new device's key on the link its code names, and hand back what that
1539
+ * device shows and polls with.
1540
+ *
1541
+ * Public by definition: the caller has no key yet. The code is the only thing it
1542
+ * holds, so every refusal that could tell a guesser the code was real — someone
1543
+ * redeemed it first, it was already used — is the same NotFound as a code never
1544
+ * issued. Only a code that died of age answers Expired, as the state table asks.
1545
+ */
1546
+ declare function redeemDeviceLinkService(params: RedeemDeviceLinkParams): Promise<RedeemDeviceLinkResult>;
1547
+ /** The issuer asking where its link stands. */
1548
+ declare function getDeviceLinkStatusService(params: DeviceLinkParams): Promise<DeviceLinkStatusResult>;
1549
+ /**
1550
+ * The issuer picked a number.
1551
+ *
1552
+ * The right one approves the link; any other denies it on the spot, and the
1553
+ * refusal is committed before the error answers — there is no second pick. The
1554
+ * route is not wrapped in `Transactional()` for exactly that reason: a rollback
1555
+ * would undo the denial and hand the issuer another guess.
1556
+ *
1557
+ * The match number is fixed once a link is redeemed, so comparing against the
1558
+ * value read is safe; the transitions still name `redeemed`, so a deny or a
1559
+ * cancel landing in between wins or loses cleanly.
1560
+ */
1561
+ declare function confirmDeviceLinkService(params: ConfirmDeviceLinkParams): Promise<DeviceLinkStatusResult>;
1562
+ /** The issuer refused the device, so it is told no instead of timing out. */
1563
+ declare function denyDeviceLinkService(params: DeviceLinkParams): Promise<DeviceLinkStatusResult>;
1564
+ /**
1565
+ * The issuer closed its screen before letting anyone in. The link is expired, so
1566
+ * a device holding its code — or already showing a match number — is told so.
1567
+ */
1568
+ declare function cancelDeviceLinkService(params: DeviceLinkParams): Promise<DeviceLinkStatusResult>;
1569
+ /**
1570
+ * The new device asking whether the issuer has answered.
1571
+ *
1572
+ * Approved is the one branch with a side effect, and it is device-code login's
1573
+ * one-shot: the link is spent by a conditional update naming `approved` — and the
1574
+ * issuing key, so a link whose issuer signed out after confirming registers
1575
+ * nothing — and of two polls arriving together exactly one registers the key.
1576
+ */
1577
+ declare function pollDeviceLinkService(params: PollDeviceLinkParams): Promise<PollDeviceAuthResult>;
1013
1578
 
1014
1579
  /**
1015
1580
  * @spfn/auth - Passkey Service
@@ -1253,7 +1818,7 @@ declare const authLoginEvent: _spfn_core_event.EventDef<{
1253
1818
  email?: string | undefined;
1254
1819
  phone?: string | undefined;
1255
1820
  userId: string;
1256
- provider: "email" | "phone" | "passkey" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "device";
1821
+ provider: "passkey" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "email" | "phone" | "device";
1257
1822
  mfaEnrolled: boolean;
1258
1823
  }>;
1259
1824
  /**
@@ -1265,15 +1830,15 @@ declare const authLoginEvent: _spfn_core_event.EventDef<{
1265
1830
  * both arrive at `createVerifiedAccount`, so the two name themselves there;
1266
1831
  * `'invitation'` is the one path that stores a key without the key service.
1267
1832
  */
1268
- 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">]>;
1269
- /** The ten doors a device key is registered through. */
1833
+ 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<"device-link">, _sinclair_typebox.TLiteral<"password-reset">, _sinclair_typebox.TLiteral<"passkey">, _sinclair_typebox.TLiteral<"renewal">]>;
1834
+ /** The eleven doors a device key is registered through. */
1270
1835
  type DeviceRegistrationChannel = Static<typeof DeviceRegistrationChannelSchema>;
1271
1836
  /**
1272
1837
  * auth.device.registered — a new device key was added to an account
1273
1838
  *
1274
1839
  * 발행 시점:
1275
1840
  * - a key row was created for an account and the transaction that created it
1276
- * committed, on every one of the ten channels above
1841
+ * committed, on every one of the eleven channels above
1277
1842
  *
1278
1843
  * This is the notice an account owner needs and could not get before: a stolen
1279
1844
  * password used to sign in on a new device was silent, because a login event
@@ -1305,13 +1870,13 @@ declare const authDeviceRegisteredEvent: _spfn_core_event.EventDef<{
1305
1870
  platform?: string | undefined;
1306
1871
  ip?: string | undefined;
1307
1872
  userAgent?: string | undefined;
1873
+ userId: string;
1308
1874
  keyId: string;
1309
1875
  algorithm: string;
1310
- userId: string;
1311
1876
  mfaEnrolled: boolean;
1312
1877
  fingerprintPrefix: string;
1313
1878
  createdAtMillis: number;
1314
- channel: "password" | "register" | "passkey" | "oauth-native" | "renewal" | "signup-link" | "invitation" | "oauth" | "device-code" | "password-reset";
1879
+ channel: "passkey" | "renewal" | "register" | "signup-link" | "invitation" | "password" | "oauth" | "oauth-native" | "device-code" | "device-link" | "password-reset";
1315
1880
  }>;
1316
1881
  /**
1317
1882
  * auth.register - 회원가입 성공 이벤트
@@ -1334,7 +1899,7 @@ declare const authRegisterEvent: _spfn_core_event.EventDef<{
1334
1899
  [x: string]: unknown;
1335
1900
  } | undefined;
1336
1901
  userId: string;
1337
- provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
1902
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "email" | "phone";
1338
1903
  }>;
1339
1904
  /**
1340
1905
  * auth.invitation.created - 초대 생성 이벤트
@@ -1361,11 +1926,11 @@ declare const invitationCreatedEvent: _spfn_core_event.EventDef<{
1361
1926
  [x: string]: unknown;
1362
1927
  } | undefined;
1363
1928
  email: string;
1364
- token: string;
1365
- expiresAt: string;
1366
1929
  roleId: number;
1367
- invitedBy: string;
1930
+ expiresAt: string;
1368
1931
  invitationId: string;
1932
+ token: string;
1933
+ invitedBy: string;
1369
1934
  isResend: boolean;
1370
1935
  }>;
1371
1936
  /**
@@ -1385,11 +1950,11 @@ declare const invitationAcceptedEvent: _spfn_core_event.EventDef<{
1385
1950
  metadata?: {
1386
1951
  [x: string]: unknown;
1387
1952
  } | undefined;
1388
- email: string;
1389
1953
  userId: string;
1954
+ email: string;
1390
1955
  roleId: number;
1391
- invitedBy: string;
1392
1956
  invitationId: string;
1957
+ invitedBy: string;
1393
1958
  }>;
1394
1959
  /**
1395
1960
  * auth.deletion.requested - 계정 탈퇴 요청 이벤트
@@ -1408,7 +1973,7 @@ declare const authDeletionRequestedEvent: _spfn_core_event.EventDef<{
1408
1973
  userId: string;
1409
1974
  purgeScheduledAt: string;
1410
1975
  userPublicId: string;
1411
- requestedBy: "admin" | "self";
1976
+ requestedBy: "self" | "admin";
1412
1977
  }>;
1413
1978
  /**
1414
1979
  * auth.deletion.cancelled - 계정 탈퇴 복구 이벤트
@@ -1453,7 +2018,7 @@ declare const authDeletionCompletedEvent: _spfn_core_event.EventDef<{
1453
2018
  declare const oauthUnlinkedEvent: _spfn_core_event.EventDef<{
1454
2019
  reason?: string | undefined;
1455
2020
  userId: string;
1456
- provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
2021
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "email" | "phone";
1457
2022
  providerUserId: string;
1458
2023
  }>;
1459
2024
  /**
@@ -1475,8 +2040,8 @@ declare const oauthUnlinkedEvent: _spfn_core_event.EventDef<{
1475
2040
  * ```
1476
2041
  */
1477
2042
  declare const authPasswordResetEvent: _spfn_core_event.EventDef<{
1478
- email: string;
1479
2043
  userId: string;
2044
+ email: string;
1480
2045
  }>;
1481
2046
  /**
1482
2047
  * Auth event payload types
@@ -1624,7 +2189,7 @@ declare const mfaChallenges: drizzle_orm_pg_core.PgTableWithColumns<{
1624
2189
  name: string;
1625
2190
  tableName: "mfa_challenges";
1626
2191
  dataType: "string enum";
1627
- data: "password" | "oauth-native" | "oauth" | "password-reset";
2192
+ data: "password" | "oauth" | "oauth-native" | "password-reset";
1628
2193
  driverParam: string;
1629
2194
  notNull: true;
1630
2195
  hasDefault: false;
@@ -3111,94 +3676,6 @@ declare function disableSessionBindingService(params: DisableSessionBindingParam
3111
3676
  /** The challenge the disabling ceremony signs. Same ceremony renewal uses. */
3112
3677
  declare function startSessionBindingDisableService(userId: number): Promise<PublicKeyCredentialRequestOptionsJSON>;
3113
3678
 
3114
- /**
3115
- * @spfn/auth - Session Renewal Service
3116
- *
3117
- * What a bound session does when its key runs out: prove, with a fresh WebAuthn
3118
- * assertion, that the person who enrolled the passkey is still at the machine,
3119
- * and get a new short-lived key sealed into the cookie.
3120
- *
3121
- * Neither step is public. The expiring key is named by `expiredKeyId`, and that
3122
- * value reaches the service from `authenticateForRenewal` — the `keyId` of a
3123
- * bearer JWT this very key signed — rather than from the request body, so a
3124
- * caller who does not hold the private half cannot name a key at all. The
3125
- * assertion still has to be signed by a passkey that key's owner enrolled: the
3126
- * signature proves the cookie, and the cookie is the thing that may have been
3127
- * copied.
3128
- *
3129
- * The admission below is run again here all the same. The middleware and the
3130
- * service ask the same four questions of the row, and a service that trusted its
3131
- * caller to have asked them would be one refactor away from not being asked at
3132
- * all.
3133
- *
3134
- * Every refusal is the same refusal. A key that never existed, a stranger's key,
3135
- * an unbound key, a revoked one, one past its grace, an inactive account, a spent
3136
- * challenge, an assertion that did not verify — all `SessionRenewalRefusedError`,
3137
- * with the same body, because anything finer would answer "is this key id live"
3138
- * to whoever asked.
3139
- *
3140
- * Renewal announces nothing. No `auth.login`, no `auth.device.registered`, and
3141
- * `lastLoginAt` does not move: this is the same person on the same device
3142
- * continuing the session they already had, and a subscriber mailing "new sign-in"
3143
- * once a day per device would train its reader to ignore the notice that matters.
3144
- * A `lastLoginAt` that moved every day would make dormant-account detection
3145
- * meaningless for exactly the accounts that turned this protection on.
3146
- */
3147
-
3148
- interface StartSessionRenewParams {
3149
- /** The key that ran out, read off the JWT the request was signed with. */
3150
- expiredKeyId: string;
3151
- }
3152
- interface FinishSessionRenewParams extends StartSessionRenewParams {
3153
- /** The assertion, from `navigator.credentials.get()`. */
3154
- response: AuthenticationResponseJSON;
3155
- /**
3156
- * The new key pair, in the vocabulary the Next.js login interceptor already
3157
- * writes: `renew/verify` is on that interceptor's path list, so these arrive
3158
- * exactly as they do on a login.
3159
- */
3160
- keyId: string;
3161
- publicKey: string;
3162
- fingerprint: string;
3163
- algorithm?: KeyAlgorithmType;
3164
- }
3165
- /**
3166
- * What a completed renewal answers: a sign-in result, plus the new key's id.
3167
- *
3168
- * The id is the one thing a renewal has that a sign-in does not need to say —
3169
- * `renewSession()` promises it to the app, which has no other way to learn it
3170
- * (the key pair is minted in the proxy and the private half never leaves the
3171
- * cookie). It is not a contract operation, so nothing generated reads it.
3172
- */
3173
- interface SessionRenewResult extends LoginResult {
3174
- /** The key this renewal registered, the one the session now signs with. */
3175
- keyId: string;
3176
- }
3177
- /**
3178
- * Step 1 — the challenge the authenticator signs.
3179
- *
3180
- * `allowCredentials` is empty and the account lives only on the challenge row.
3181
- * See `startRenewalCeremonyService`.
3182
- *
3183
- * @throws SessionRenewalRefusedError 갱신할 수 없는 키·계정일 때 (모든 사유 동일)
3184
- */
3185
- declare function startSessionRenewService(params: StartSessionRenewParams): Promise<PublicKeyCredentialRequestOptionsJSON>;
3186
- /**
3187
- * Step 2 — verify the assertion, put a new bound key in place of the old one.
3188
- *
3189
- * The revocation runs first and its answer is the race winner: two verifies that
3190
- * both got past their own challenges meet at the same conditional UPDATE, and
3191
- * only the one that actually revoked the key goes on to register a replacement.
3192
- *
3193
- * The new key inherits the old row's provenance, so the device list keeps saying
3194
- * where this device first appeared rather than re-stamping itself every day. Its
3195
- * expiry is a fresh window from now — renewal is a renewal, not an extension of
3196
- * what the old key had.
3197
- *
3198
- * @throws SessionRenewalRefusedError 갱신할 수 없을 때 (증명 실패 포함, 모든 사유 동일)
3199
- */
3200
- declare function finishSessionRenewService(params: FinishSessionRenewParams): Promise<SessionRenewResult>;
3201
-
3202
3679
  /**
3203
3680
  * What `POST /_auth/oauth/finalize` answers with, in both of its branches.
3204
3681
  *
@@ -3220,6 +3697,87 @@ interface OAuthFinalizeResponse {
3220
3697
  keyExpiresAtMillis?: number;
3221
3698
  }
3222
3699
 
3700
+ /**
3701
+ * Role information for client/API responses
3702
+ */
3703
+ interface Role {
3704
+ id: number;
3705
+ name: string;
3706
+ displayName: string;
3707
+ description: string | null;
3708
+ isBuiltin: boolean;
3709
+ isSystem: boolean;
3710
+ isActive: boolean;
3711
+ priority: number;
3712
+ createdAt: Date;
3713
+ updatedAt: Date;
3714
+ }
3715
+ /**
3716
+ * Permission information for client/API responses
3717
+ */
3718
+ interface Permission {
3719
+ id: number;
3720
+ name: string;
3721
+ displayName: string;
3722
+ description: string | null;
3723
+ category: string | null;
3724
+ isBuiltin: boolean;
3725
+ isSystem: boolean;
3726
+ isActive: boolean;
3727
+ metadata: Record<string, any> | null;
3728
+ createdAt: Date;
3729
+ updatedAt: Date;
3730
+ }
3731
+ interface AuthSession {
3732
+ userId: number;
3733
+ publicId: string;
3734
+ email: string | null;
3735
+ emailVerified: boolean;
3736
+ phoneVerified: boolean;
3737
+ hasPassword: boolean;
3738
+ role: Role;
3739
+ permissions: Permission[];
3740
+ }
3741
+ interface ProfileInfo {
3742
+ profileId: number;
3743
+ displayName: string | null;
3744
+ firstName: string | null;
3745
+ lastName: string | null;
3746
+ avatarUrl: string | null;
3747
+ bio: string | null;
3748
+ locale: string;
3749
+ timezone: string;
3750
+ website: string | null;
3751
+ location: string | null;
3752
+ company: string | null;
3753
+ jobTitle: string | null;
3754
+ metadata: Record<string, any> | null;
3755
+ createdAt: Date;
3756
+ updatedAt: Date;
3757
+ }
3758
+ /**
3759
+ * User Profile Response
3760
+ *
3761
+ * Complete user data including:
3762
+ * - User fields at top level (userId, email, etc.)
3763
+ * - Profile data as nested field (optional)
3764
+ *
3765
+ * Excludes:
3766
+ * - Role and permissions (use auth session API)
3767
+ */
3768
+ interface UserProfile {
3769
+ userId: number;
3770
+ publicId: string;
3771
+ email: string | null;
3772
+ username: string | null;
3773
+ emailVerified: boolean;
3774
+ phoneVerified: boolean;
3775
+ lastLoginAt: Date | null;
3776
+ createdAt: Date;
3777
+ updatedAt: Date;
3778
+ profile: ProfileInfo | null;
3779
+ }
3780
+
3223
3781
  /**
3224
3782
  * @spfn/auth - Main Router
3225
3783
  *
@@ -3389,6 +3947,59 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3389
3947
  userCode: _sinclair_typebox.TString;
3390
3948
  }>;
3391
3949
  }, {}, void>;
3950
+ issueDeviceLink: _spfn_core_route.RouteDef<{}, {}, IssueDeviceLinkResult>;
3951
+ redeemDeviceLink: _spfn_core_route.RouteDef<{
3952
+ body: _sinclair_typebox.TObject<{
3953
+ userCode: _sinclair_typebox.TString;
3954
+ publicKey: _sinclair_typebox.TString;
3955
+ keyId: _sinclair_typebox.TString;
3956
+ fingerprint: _sinclair_typebox.TString;
3957
+ algorithm: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>>;
3958
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3959
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
3960
+ }>;
3961
+ }, {}, RedeemDeviceLinkResult>;
3962
+ getDeviceLinkStatus: _spfn_core_route.RouteDef<{
3963
+ body: _sinclair_typebox.TObject<{
3964
+ linkId: _sinclair_typebox.TString;
3965
+ waitMillis: _sinclair_typebox.TOptional<_sinclair_typebox.TInteger>;
3966
+ }>;
3967
+ }, {}, DeviceLinkStatusResult>;
3968
+ confirmDeviceLink: _spfn_core_route.RouteDef<{
3969
+ body: _sinclair_typebox.TObject<{
3970
+ linkId: _sinclair_typebox.TString;
3971
+ choice: _sinclair_typebox.TInteger;
3972
+ }>;
3973
+ }, {}, DeviceLinkStatusResult>;
3974
+ denyDeviceLink: _spfn_core_route.RouteDef<{
3975
+ body: _sinclair_typebox.TObject<{
3976
+ linkId: _sinclair_typebox.TString;
3977
+ }>;
3978
+ }, {}, DeviceLinkStatusResult>;
3979
+ cancelDeviceLink: _spfn_core_route.RouteDef<{
3980
+ body: _sinclair_typebox.TObject<{
3981
+ linkId: _sinclair_typebox.TString;
3982
+ }>;
3983
+ }, {}, DeviceLinkStatusResult>;
3984
+ pollDeviceLink: _spfn_core_route.RouteDef<{
3985
+ body: _sinclair_typebox.TObject<{
3986
+ deviceCode: _sinclair_typebox.TString;
3987
+ waitMillis: _sinclair_typebox.TOptional<_sinclair_typebox.TInteger>;
3988
+ }>;
3989
+ }, {}, {
3990
+ status: "pending";
3991
+ intervalMillis: number;
3992
+ } | {
3993
+ email?: string | undefined;
3994
+ phone?: string | undefined;
3995
+ sessionBinding?: "none" | "passkey" | undefined;
3996
+ keyExpiresAtMillis?: number | undefined;
3997
+ status: "approved";
3998
+ mfaRequired: boolean;
3999
+ userId: string;
4000
+ publicId: string;
4001
+ passwordChangeRequired: boolean;
4002
+ }>;
3392
4003
  passkeyRegisterOptions: _spfn_core_route.RouteDef<{
3393
4004
  body: _sinclair_typebox.TObject<{
3394
4005
  currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
@@ -3549,7 +4160,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3549
4160
  id: number;
3550
4161
  name: string;
3551
4162
  displayName: string;
3552
- category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
4163
+ category: "custom" | "user" | "auth" | "rbac" | "system" | undefined;
3553
4164
  }[];
3554
4165
  userId: number;
3555
4166
  publicId: string;
@@ -3874,8 +4485,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3874
4485
  }, {}, {
3875
4486
  roles: {
3876
4487
  description: string | null;
3877
- name: string;
3878
4488
  id: number;
4489
+ name: string;
3879
4490
  displayName: string;
3880
4491
  isBuiltin: boolean;
3881
4492
  isSystem: boolean;
@@ -3896,8 +4507,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3896
4507
  }, {}, {
3897
4508
  role: {
3898
4509
  description: string | null;
3899
- name: string;
3900
4510
  id: number;
4511
+ name: string;
3901
4512
  displayName: string;
3902
4513
  isBuiltin: boolean;
3903
4514
  isSystem: boolean;
@@ -3920,8 +4531,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3920
4531
  }, {}, {
3921
4532
  role: {
3922
4533
  description: string | null;
3923
- name: string;
3924
4534
  id: number;
4535
+ name: string;
3925
4536
  displayName: string;
3926
4537
  isBuiltin: boolean;
3927
4538
  isSystem: boolean;
@@ -4772,4 +5383,4 @@ declare const machineAuth: _spfn_core_route.NamedMiddleware<"machineAuth">;
4772
5383
  */
4773
5384
  declare const requireMachineScope: _spfn_core_route.NamedMiddlewareFactory<"machineScope", string[]>;
4774
5385
 
4775
- 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 };
5386
+ export { type UserPublicKey as $, type AuthInitOptions as A, type VerificationPurpose as B, type ConfirmSignupLinkResult as C, type DeviceAuthInfoResult as D, type VerificationTargetType as E, type FinishPasskeyEnrollmentResult as F, type OAuthProvider as G, type NewPasskey as H, type IssueDeviceLinkResult as I, type Passkey 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 MfaVerification as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type TotpEnrolmentResult as T, type UserProfile as U, type VerifyMfaChallengeResult as V, type MfaVerificationMethod as W, type NewMfaChallenge as X, type MfaChallenge as Y, type DeviceLink as Z, type NewDeviceLink as _, type RegisterResult as a, type OAuthCodeExchangeOptions as a$, type AuthContext as a0, type ApproveDeviceAuthParams as a1, type AssertStepUpParams as a2, type AuthDeletionCancelledPayload as a3, type AuthDeletionCompletedPayload as a4, type AuthDeletionRequestedPayload as a5, type AuthDeviceRegisteredPayload as a6, type AuthLoginPayload as a7, type AuthPasswordResetPayload as a8, type AuthProfileOutcome as a9, FingerprintSchema as aA, type FinishPasskeyEnrollmentParams as aB, type FinishPasskeyLoginParams as aC, type FinishSessionRenewParams as aD, type InvitationAcceptedPayload as aE, type InvitationCreatedPayload as aF, KEY_FINGERPRINT_PREFIX_LENGTH as aG, KeyIdSchema as aH, LinkIdSchema as aI, type LoginParams as aJ, type LogoutParams as aK, MFA_CHALLENGE_ATTEMPT_LIMIT as aL, MFA_CHALLENGE_CHANNELS as aM, MFA_VERIFICATION_METHODS as aN, type MachinePrincipal as aO, type MachineVerifierRegistration as aP, type MarkPasskeyParams as aQ, MatchChoiceSchema as aR, type MfaChallengeChannel as aS, type MfaChallengeHandle as aT, type NativeVerifyOptions as aU, type NewMfaVerification as aV, type NormalizedIdentity as aW, type OAuth2AuthorizeParams as aX, type OAuth2ScopeDescription as aY, type OAuthCallbackParams as aZ, type OAuthCallbackResult as a_, type AuthProfileVerifier as aa, AuthProviderSchema as ab, type AuthRegisterPayload as ac, type BearerOutcome as ad, type BearerRefusal as ae, type ChangePasswordParams as af, type CompletePasswordResetParams as ag, type CompleteSignupParams as ah, type ConfirmDeviceLinkParams as ai, type ConfirmPasswordResetParams as aj, type ConfirmSignupLinkParams as ak, type ConfirmTotpParams as al, DEVICE_LINK_STATUSES as am, type DeferredLoginEvent as an, type DenyDeviceAuthParams as ao, type DeviceAuthApprovedResult as ap, type DeviceAuthInfoParams as aq, type DeviceAuthPendingResult as ar, DeviceAuthPollResponseSchema as as, type DeviceLinkIssuer as at, type DeviceLinkParams as au, type DeviceLinkStatus as av, DeviceNameSchema as aw, type DeviceRegistrationChannel as ax, type DisableSessionBindingParams as ay, EmailSchema as az, type StartDeviceAuthResult as b, completePasswordResetService as b$, type OAuthNativeParams as b0, type OAuthStartParams as b1, type OAuthTokens as b2, type OAuthUnlinkedPayload as b3, type OpenStepUpChallengeParams as b4, PASSKEY_DEVICE_TYPES as b5, PASSKEY_LABEL_MAX_LENGTH as b6, type PasskeyDeviceType as b7, PasswordSchema as b8, PhoneSchema as b9, type UnlinkNotifyRequest as bA, type UnlinkNotifyResult as bB, UserCodeSchema as bC, VerificationPurposeSchema as bD, type VerifyCodeParams as bE, type VerifyCodeResult as bF, type VerifyMfaChallengeParams as bG, admitBearerKey as bH, approveDeviceAuthService as bI, approveOAuth2AuthorizeService as bJ, assertNotLastRecoveryCredential as bK, assertRecentAuthentication as bL, assertStepUp as bM, attemptSecondFactor 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, cancelDeviceLinkService as bY, carryStepUpVerification as bZ, changePasswordService as b_, PlatformSchema as ba, type PollDeviceAuthParams as bb, type PollDeviceAuthResult as bc, type PollDeviceLinkParams as bd, PublicKeySchema as be, type RecentAuthenticationParams as bf, type RedeemDeviceLinkParams as bg, type RegisterParams as bh, type RegisterPublicKeyParams as bi, type RegisterPublicKeyResult as bj, type RenamePasskeyParams as bk, type RequestPasswordResetParams as bl, type RequestSignupLinkParams as bm, type RevokeAllKeysParams as bn, type RevokeKeyParams as bo, type RevokePasskeyParams as bp, type RotateKeyParams as bq, type SendVerificationCodeParams as br, type SessionBindingParams as bs, type StartDeviceAuthParams as bt, type StartPasskeyEnrollmentParams as bu, type StartSessionRenewParams as bv, type StepUpParams as bw, TargetTypeSchema as bx, type UnlinkNotification as by, UnlinkNotifyRejection as bz, type RedeemDeviceLinkResult as c, requireMachineScope as c$, completeSignupService as c0, confirmDeviceLinkService as c1, confirmPasswordResetService as c2, confirmSignupLinkService as c3, confirmTotpEnrolmentService as c4, denyDeviceAuthService as c5, denyDeviceLinkService as c6, denyOAuth2AuthorizeService as c7, describeOAuth2AuthorizeRequestService as c8, deviceLinks as c9, markPasskeySecondFactorService as cA, mfaChallenges as cB, mfaEnrolledForUser as cC, mfaStatusService as cD, mfaVerifications as cE, oauthCallbackService as cF, oauthNativeService as cG, oauthStartService as cH, oauthUnlinkNotifyService as cI, oauthUnlinkedEvent as cJ, openStepUpChallengeService as cK, optionalAuth as cL, passkeys as cM, pollDeviceAuthService as cN, pollDeviceLinkService as cO, redeemDeviceLinkService as cP, regenerateRecoveryCodesService as cQ, registerAuthProfile as cR, registerMachineVerifier as cS, registerOAuthProvider as cT, registerPublicKeyService as cU, registerService as cV, registeredBinding as cW, renamePasskeyService as cX, requestPasswordResetService as cY, requestSignupLinkService as cZ, requireEnabledProvider as c_, disableMfaService as ca, disableSessionBindingService as cb, enableSessionBindingService as cc, finishPasskeyEnrollmentService as cd, finishPasskeyLoginService as ce, finishSessionRenewService as cf, getDeviceAuthInfoService as cg, getDeviceLinkStatusService as ch, getEnabledOAuthProviders as ci, getGoogleAccessToken as cj, getMachinePrincipal as ck, getOAuthProvider as cl, getRegisteredProviders as cm, getSessionBindingService as cn, invitationAcceptedEvent as co, invitationCreatedEvent as cp, isOAuthProviderEnabled as cq, issueDeviceLinkService as cr, issueOneTimeTokenService as cs, keySessionBindingService as ct, listKeysService as cu, listOAuth2GrantsService as cv, listPasskeysService as cw, loginService as cx, logoutService as cy, machineAuth as cz, type DeviceLinkStatusResult as d, resolveAuthenticatedUser as d0, resumeStepUpChallengeService as d1, revokeAllKeysService as d2, revokeAllOAuth2GrantsForUser as d3, revokeKeyService as d4, revokeOAuth2GrantService as d5, revokePasskeyService as d6, rotateKeyService as d7, runAuthProfile as d8, selectAuthProfile as d9, sendVerificationCodeService as da, startDeviceAuthService as db, startMfaChallengeAssertionService as dc, startPasskeyEnrollmentService as dd, startPasskeyLoginService as de, startSessionBindingDisableService as df, startSessionRenewService as dg, startStepUpService as dh, startTotpEnrolmentService as di, stepUpService as dj, sweepMfaChallengesService as dk, sweepUnconfirmedMfaService as dl, userPublicKeys as dm, verifyCodeService as dn, verifyMfaChallengeService as dp, verifyOneTimeTokenService as dq, verifySecondFactor as dr, type RotateKeyResult as e, type RevokeAllKeysResult as f, type IssueOneTimeTokenResult as g, type ProfileInfo as h, type OAuthFinalizeResponse as i, type OAuthNativeResult as j, type RequestSignupLinkResult as k, type RequestPasswordResetResult as l, mainAuthRouter as m, type ConfirmPasswordResetResult as n, type PasskeySummary as o, type ConfirmTotpResult as p, type SessionBindingResult as q, type SessionRenewResult as r, type OAuth2ConsentView as s, type OAuth2AuthorizationCodeIssued as t, type OAuth2GrantSummary as u, type AuthSession as v, PERMISSION_CATEGORIES as w, type PermissionCategory as x, VERIFICATION_PURPOSES as y, VERIFICATION_TARGET_TYPES as z };