@spfn/auth 0.3.0-beta.26 → 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
14
- */
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
- /**
28
- * Permission 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.
29
28
  */
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
29
  /**
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
  *
@@ -591,6 +921,12 @@ declare function completePasswordResetService(params: CompletePasswordResetParam
591
921
  * refuses the account's live records too, as `denied`, so they land in that row
592
922
  * of the table. See `denyAllActiveByUserId`; the three callers are the three
593
923
  * places that revoke every key at once.
924
+ *
925
+ * A poll may ask to wait (`waitMillis`). `waitForDeviceAuthAnswerService` then
926
+ * holds a pending record's request — before the route's transaction opens — until
927
+ * something answers it, the wait runs out, or the device hangs up, and only after
928
+ * that is the table above applied. A long poll answers exactly what a short poll
929
+ * sent at the same moment would, only sooner.
594
930
  */
595
931
 
596
932
  interface StartDeviceAuthParams {
@@ -645,10 +981,21 @@ interface PollDeviceAuthParams {
645
981
  * to run a WebAuthn ceremony in needs.
646
982
  */
647
983
  webProxy?: boolean;
984
+ /**
985
+ * How long this request already waited on the server, from the long-poll
986
+ * middleware. A pending answer subtracts it from the interval: the device
987
+ * has done that much of its waiting here.
988
+ */
989
+ waitedMillis?: number;
648
990
  }
649
991
  /** Nobody has answered yet. Not an error — the waiting device waits. */
650
992
  interface DeviceAuthPendingResult {
651
993
  status: 'pending';
994
+ /**
995
+ * How long to wait before polling again, less what the request already
996
+ * waited on the server — 0 once a long poll has waited at least the interval,
997
+ * so the device asks again at once.
998
+ */
652
999
  intervalMillis: number;
653
1000
  }
654
1001
  /**
@@ -716,41 +1063,47 @@ declare function denyDeviceAuthService(params: DenyDeviceAuthParams): Promise<vo
716
1063
  declare function pollDeviceAuthService(params: PollDeviceAuthParams): Promise<PollDeviceAuthResult>;
717
1064
 
718
1065
  /**
719
- * @spfn/auth - Passkeys Entity
1066
+ * @spfn/auth - Device Links Entity
720
1067
  *
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.
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.
725
1073
  *
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.
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.
730
1080
  *
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.
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.
734
1087
  */
735
1088
  /**
736
- * Whether the credential can leave the authenticator that minted it.
1089
+ * Lifecycle of one device link.
737
1090
  *
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.
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.
742
1097
  */
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";
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";
749
1102
  schema: string;
750
1103
  columns: {
751
- 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>>, {
752
1105
  name: string;
753
- tableName: "passkeys";
1106
+ tableName: "device_links";
754
1107
  dataType: "object date";
755
1108
  data: Date;
756
1109
  driverParam: string;
@@ -763,9 +1116,9 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
763
1116
  identity: undefined;
764
1117
  generated: undefined;
765
1118
  }>;
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>>>, {
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>>>, {
767
1120
  name: string;
768
- tableName: "passkeys";
1121
+ tableName: "device_links";
769
1122
  dataType: "object date";
770
1123
  data: Date;
771
1124
  driverParam: string;
@@ -778,9 +1131,9 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
778
1131
  identity: undefined;
779
1132
  generated: undefined;
780
1133
  }>;
781
- 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>, {
782
1135
  name: string;
783
- tableName: "passkeys";
1136
+ tableName: "device_links";
784
1137
  dataType: "number int53";
785
1138
  data: number;
786
1139
  driverParam: number;
@@ -793,9 +1146,39 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
793
1146
  identity: undefined;
794
1147
  generated: undefined;
795
1148
  }>;
796
- 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[]]>>, {
797
1150
  name: string;
798
- 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";
799
1182
  dataType: "number int53";
800
1183
  data: number;
801
1184
  driverParam: string | number;
@@ -808,9 +1191,9 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
808
1191
  identity: undefined;
809
1192
  generated: undefined;
810
1193
  }>;
811
- 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[]]>>, {
812
1195
  name: string;
813
- tableName: "passkeys";
1196
+ tableName: "device_links";
814
1197
  dataType: "string";
815
1198
  data: string;
816
1199
  driverParam: string;
@@ -823,13 +1206,13 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
823
1206
  identity: undefined;
824
1207
  generated: undefined;
825
1208
  }>;
826
- 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[]]>, {
827
1210
  name: string;
828
- tableName: "passkeys";
1211
+ tableName: "device_links";
829
1212
  dataType: "string";
830
1213
  data: string;
831
1214
  driverParam: string;
832
- notNull: true;
1215
+ notNull: false;
833
1216
  hasDefault: false;
834
1217
  isPrimaryKey: false;
835
1218
  isAutoincrement: false;
@@ -838,14 +1221,14 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
838
1221
  identity: undefined;
839
1222
  generated: undefined;
840
1223
  }>;
841
- 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[]]>, {
842
1225
  name: string;
843
- tableName: "passkeys";
844
- dataType: "number int32";
845
- data: number;
846
- driverParam: string | number;
847
- notNull: true;
848
- hasDefault: true;
1226
+ tableName: "device_links";
1227
+ dataType: "string";
1228
+ data: string;
1229
+ driverParam: string;
1230
+ notNull: false;
1231
+ hasDefault: false;
849
1232
  isPrimaryKey: false;
850
1233
  isAutoincrement: false;
851
1234
  hasRuntimeDefault: false;
@@ -853,12 +1236,12 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
853
1236
  identity: undefined;
854
1237
  generated: undefined;
855
1238
  }>;
856
- 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[]]>, {
857
1240
  name: string;
858
- tableName: "passkeys";
1241
+ tableName: "device_links";
859
1242
  dataType: "string";
860
- data: string[];
861
- driverParam: string | string[];
1243
+ data: string;
1244
+ driverParam: string;
862
1245
  notNull: false;
863
1246
  hasDefault: false;
864
1247
  isPrimaryKey: false;
@@ -868,39 +1251,39 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
868
1251
  identity: undefined;
869
1252
  generated: undefined;
870
1253
  }>;
871
- 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[]]>, {
872
1255
  name: string;
873
- tableName: "passkeys";
874
- dataType: "string enum";
875
- data: "singleDevice" | "multiDevice";
1256
+ tableName: "device_links";
1257
+ dataType: "string";
1258
+ data: string;
876
1259
  driverParam: string;
877
- notNull: true;
1260
+ notNull: false;
878
1261
  hasDefault: false;
879
1262
  isPrimaryKey: false;
880
1263
  isAutoincrement: false;
881
1264
  hasRuntimeDefault: false;
882
- enumValues: ["singleDevice", "multiDevice"] & [string, ...string[]];
1265
+ enumValues: undefined;
883
1266
  identity: undefined;
884
1267
  generated: undefined;
885
1268
  }>;
886
- 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[]]>, {
887
1270
  name: string;
888
- tableName: "passkeys";
889
- dataType: "boolean";
890
- data: boolean;
891
- driverParam: boolean;
892
- notNull: true;
893
- hasDefault: true;
1271
+ tableName: "device_links";
1272
+ dataType: "string enum";
1273
+ data: "ES256" | "RS256";
1274
+ driverParam: string;
1275
+ notNull: false;
1276
+ hasDefault: false;
894
1277
  isPrimaryKey: false;
895
1278
  isAutoincrement: false;
896
1279
  hasRuntimeDefault: false;
897
- enumValues: undefined;
1280
+ enumValues: ["ES256", "RS256"] & [string, ...string[]];
898
1281
  identity: undefined;
899
1282
  generated: undefined;
900
1283
  }>;
901
- 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[]]>, {
902
1285
  name: string;
903
- tableName: "passkeys";
1286
+ tableName: "device_links";
904
1287
  dataType: "string";
905
1288
  data: string;
906
1289
  driverParam: string;
@@ -913,39 +1296,84 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
913
1296
  identity: undefined;
914
1297
  generated: undefined;
915
1298
  }>;
916
- 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[]]>, {
917
1300
  name: string;
918
- tableName: "passkeys";
919
- dataType: "string";
920
- data: string;
1301
+ tableName: "device_links";
1302
+ dataType: "string enum";
1303
+ data: "ios" | "android" | "web" | "desktop";
921
1304
  driverParam: string;
922
1305
  notNull: false;
923
1306
  hasDefault: false;
924
1307
  isPrimaryKey: false;
925
1308
  isAutoincrement: false;
926
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;
927
1325
  enumValues: undefined;
928
1326
  identity: undefined;
929
1327
  generated: undefined;
930
1328
  }>;
931
- 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>, {
932
1330
  name: string;
933
- tableName: "passkeys";
934
- dataType: "boolean";
935
- data: boolean;
936
- 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;
937
1350
  notNull: true;
938
1351
  hasDefault: true;
939
1352
  isPrimaryKey: false;
940
1353
  isAutoincrement: false;
941
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;
942
1370
  enumValues: undefined;
943
1371
  identity: undefined;
944
1372
  generated: undefined;
945
1373
  }>;
946
- 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, {
947
1375
  name: string;
948
- tableName: "passkeys";
1376
+ tableName: "device_links";
949
1377
  dataType: "object date";
950
1378
  data: Date;
951
1379
  driverParam: string;
@@ -958,9 +1386,9 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
958
1386
  identity: undefined;
959
1387
  generated: undefined;
960
1388
  }>;
961
- 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, {
962
1390
  name: string;
963
- tableName: "passkeys";
1391
+ tableName: "device_links";
964
1392
  dataType: "object date";
965
1393
  data: Date;
966
1394
  driverParam: string;
@@ -973,11 +1401,11 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
973
1401
  identity: undefined;
974
1402
  generated: undefined;
975
1403
  }>;
976
- 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, {
977
1405
  name: string;
978
- tableName: "passkeys";
979
- dataType: "string";
980
- data: string;
1406
+ tableName: "device_links";
1407
+ dataType: "object date";
1408
+ data: Date;
981
1409
  driverParam: string;
982
1410
  notNull: false;
983
1411
  hasDefault: false;
@@ -991,8 +1419,162 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
991
1419
  };
992
1420
  dialect: "pg";
993
1421
  }>;
994
- type Passkey = typeof passkeys.$inferSelect;
995
- 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>;
996
1578
 
997
1579
  /**
998
1580
  * @spfn/auth - Passkey Service
@@ -1236,7 +1818,7 @@ declare const authLoginEvent: _spfn_core_event.EventDef<{
1236
1818
  email?: string | undefined;
1237
1819
  phone?: string | undefined;
1238
1820
  userId: string;
1239
- provider: "email" | "phone" | "passkey" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "device";
1821
+ provider: "passkey" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "email" | "phone" | "device";
1240
1822
  mfaEnrolled: boolean;
1241
1823
  }>;
1242
1824
  /**
@@ -1248,15 +1830,15 @@ declare const authLoginEvent: _spfn_core_event.EventDef<{
1248
1830
  * both arrive at `createVerifiedAccount`, so the two name themselves there;
1249
1831
  * `'invitation'` is the one path that stores a key without the key service.
1250
1832
  */
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. */
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. */
1253
1835
  type DeviceRegistrationChannel = Static<typeof DeviceRegistrationChannelSchema>;
1254
1836
  /**
1255
1837
  * auth.device.registered — a new device key was added to an account
1256
1838
  *
1257
1839
  * 발행 시점:
1258
1840
  * - a key row was created for an account and the transaction that created it
1259
- * committed, on every one of the ten channels above
1841
+ * committed, on every one of the eleven channels above
1260
1842
  *
1261
1843
  * This is the notice an account owner needs and could not get before: a stolen
1262
1844
  * password used to sign in on a new device was silent, because a login event
@@ -1288,13 +1870,13 @@ declare const authDeviceRegisteredEvent: _spfn_core_event.EventDef<{
1288
1870
  platform?: string | undefined;
1289
1871
  ip?: string | undefined;
1290
1872
  userAgent?: string | undefined;
1873
+ userId: string;
1291
1874
  keyId: string;
1292
1875
  algorithm: string;
1293
- userId: string;
1294
1876
  mfaEnrolled: boolean;
1295
1877
  fingerprintPrefix: string;
1296
1878
  createdAtMillis: number;
1297
- 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";
1298
1880
  }>;
1299
1881
  /**
1300
1882
  * auth.register - 회원가입 성공 이벤트
@@ -1317,7 +1899,7 @@ declare const authRegisterEvent: _spfn_core_event.EventDef<{
1317
1899
  [x: string]: unknown;
1318
1900
  } | undefined;
1319
1901
  userId: string;
1320
- provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
1902
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "email" | "phone";
1321
1903
  }>;
1322
1904
  /**
1323
1905
  * auth.invitation.created - 초대 생성 이벤트
@@ -1344,11 +1926,11 @@ declare const invitationCreatedEvent: _spfn_core_event.EventDef<{
1344
1926
  [x: string]: unknown;
1345
1927
  } | undefined;
1346
1928
  email: string;
1347
- token: string;
1348
- expiresAt: string;
1349
1929
  roleId: number;
1350
- invitedBy: string;
1930
+ expiresAt: string;
1351
1931
  invitationId: string;
1932
+ token: string;
1933
+ invitedBy: string;
1352
1934
  isResend: boolean;
1353
1935
  }>;
1354
1936
  /**
@@ -1368,11 +1950,11 @@ declare const invitationAcceptedEvent: _spfn_core_event.EventDef<{
1368
1950
  metadata?: {
1369
1951
  [x: string]: unknown;
1370
1952
  } | undefined;
1371
- email: string;
1372
1953
  userId: string;
1954
+ email: string;
1373
1955
  roleId: number;
1374
- invitedBy: string;
1375
1956
  invitationId: string;
1957
+ invitedBy: string;
1376
1958
  }>;
1377
1959
  /**
1378
1960
  * auth.deletion.requested - 계정 탈퇴 요청 이벤트
@@ -1391,7 +1973,7 @@ declare const authDeletionRequestedEvent: _spfn_core_event.EventDef<{
1391
1973
  userId: string;
1392
1974
  purgeScheduledAt: string;
1393
1975
  userPublicId: string;
1394
- requestedBy: "admin" | "self";
1976
+ requestedBy: "self" | "admin";
1395
1977
  }>;
1396
1978
  /**
1397
1979
  * auth.deletion.cancelled - 계정 탈퇴 복구 이벤트
@@ -1436,7 +2018,7 @@ declare const authDeletionCompletedEvent: _spfn_core_event.EventDef<{
1436
2018
  declare const oauthUnlinkedEvent: _spfn_core_event.EventDef<{
1437
2019
  reason?: string | undefined;
1438
2020
  userId: string;
1439
- provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
2021
+ provider: "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "email" | "phone";
1440
2022
  providerUserId: string;
1441
2023
  }>;
1442
2024
  /**
@@ -1458,8 +2040,8 @@ declare const oauthUnlinkedEvent: _spfn_core_event.EventDef<{
1458
2040
  * ```
1459
2041
  */
1460
2042
  declare const authPasswordResetEvent: _spfn_core_event.EventDef<{
1461
- email: string;
1462
2043
  userId: string;
2044
+ email: string;
1463
2045
  }>;
1464
2046
  /**
1465
2047
  * Auth event payload types
@@ -1607,7 +2189,7 @@ declare const mfaChallenges: drizzle_orm_pg_core.PgTableWithColumns<{
1607
2189
  name: string;
1608
2190
  tableName: "mfa_challenges";
1609
2191
  dataType: "string enum";
1610
- data: "password" | "oauth-native" | "oauth" | "password-reset";
2192
+ data: "password" | "oauth" | "oauth-native" | "password-reset";
1611
2193
  driverParam: string;
1612
2194
  notNull: true;
1613
2195
  hasDefault: false;
@@ -3094,94 +3676,6 @@ declare function disableSessionBindingService(params: DisableSessionBindingParam
3094
3676
  /** The challenge the disabling ceremony signs. Same ceremony renewal uses. */
3095
3677
  declare function startSessionBindingDisableService(userId: number): Promise<PublicKeyCredentialRequestOptionsJSON>;
3096
3678
 
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
3679
  /**
3186
3680
  * What `POST /_auth/oauth/finalize` answers with, in both of its branches.
3187
3681
  *
@@ -3203,6 +3697,87 @@ interface OAuthFinalizeResponse {
3203
3697
  keyExpiresAtMillis?: number;
3204
3698
  }
3205
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
+
3206
3781
  /**
3207
3782
  * @spfn/auth - Main Router
3208
3783
  *
@@ -3341,6 +3916,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3341
3916
  pollDeviceAuth: _spfn_core_route.RouteDef<{
3342
3917
  body: _sinclair_typebox.TObject<{
3343
3918
  deviceCode: _sinclair_typebox.TString;
3919
+ waitMillis: _sinclair_typebox.TOptional<_sinclair_typebox.TInteger>;
3344
3920
  }>;
3345
3921
  }, {}, {
3346
3922
  status: "pending";
@@ -3371,6 +3947,59 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3371
3947
  userCode: _sinclair_typebox.TString;
3372
3948
  }>;
3373
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
+ }>;
3374
4003
  passkeyRegisterOptions: _spfn_core_route.RouteDef<{
3375
4004
  body: _sinclair_typebox.TObject<{
3376
4005
  currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
@@ -3531,7 +4160,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3531
4160
  id: number;
3532
4161
  name: string;
3533
4162
  displayName: string;
3534
- category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
4163
+ category: "custom" | "user" | "auth" | "rbac" | "system" | undefined;
3535
4164
  }[];
3536
4165
  userId: number;
3537
4166
  publicId: string;
@@ -3856,8 +4485,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3856
4485
  }, {}, {
3857
4486
  roles: {
3858
4487
  description: string | null;
3859
- name: string;
3860
4488
  id: number;
4489
+ name: string;
3861
4490
  displayName: string;
3862
4491
  isBuiltin: boolean;
3863
4492
  isSystem: boolean;
@@ -3878,8 +4507,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3878
4507
  }, {}, {
3879
4508
  role: {
3880
4509
  description: string | null;
3881
- name: string;
3882
4510
  id: number;
4511
+ name: string;
3883
4512
  displayName: string;
3884
4513
  isBuiltin: boolean;
3885
4514
  isSystem: boolean;
@@ -3902,8 +4531,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
3902
4531
  }, {}, {
3903
4532
  role: {
3904
4533
  description: string | null;
3905
- name: string;
3906
4534
  id: number;
4535
+ name: string;
3907
4536
  displayName: string;
3908
4537
  isBuiltin: boolean;
3909
4538
  isSystem: boolean;
@@ -4754,4 +5383,4 @@ declare const machineAuth: _spfn_core_route.NamedMiddleware<"machineAuth">;
4754
5383
  */
4755
5384
  declare const requireMachineScope: _spfn_core_route.NamedMiddlewareFactory<"machineScope", string[]>;
4756
5385
 
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 };
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 };