@spfn/auth 0.3.0-beta.2 → 0.3.0-beta.21

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.
Files changed (47) hide show
  1. package/README.md +1488 -25
  2. package/dist/client-proof.d.ts +45 -15
  3. package/dist/client-proof.js +201 -5
  4. package/dist/client-proof.js.map +1 -1
  5. package/dist/client.d.ts +92 -1
  6. package/dist/client.js +58 -0
  7. package/dist/client.js.map +1 -1
  8. package/dist/config.d.ts +342 -0
  9. package/dist/config.js +153 -4
  10. package/dist/config.js.map +1 -1
  11. package/dist/errors.d.ts +396 -3
  12. package/dist/errors.js +257 -2
  13. package/dist/errors.js.map +1 -1
  14. package/dist/index.d.ts +202 -2
  15. package/dist/index.js +277 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/machine-principals-B7N8gux0.d.ts +3087 -0
  18. package/dist/nextjs/api.js +350 -12
  19. package/dist/nextjs/api.js.map +1 -1
  20. package/dist/nextjs/client.d.ts +28 -1
  21. package/dist/nextjs/client.js +24 -3
  22. package/dist/nextjs/client.js.map +1 -1
  23. package/dist/nextjs/server.d.ts +173 -3
  24. package/dist/nextjs/server.js +372 -10
  25. package/dist/nextjs/server.js.map +1 -1
  26. package/dist/server.d.ts +4167 -582
  27. package/dist/server.js +9618 -4341
  28. package/dist/server.js.map +1 -1
  29. package/dist/{session-DTHahDQ9.d.ts → session-Dfwu5g2W.d.ts} +28 -1
  30. package/migrations/20260810112144_colorful_tomorrow_man/migration.sql +18 -0
  31. package/migrations/20260810112144_colorful_tomorrow_man/snapshot.json +3576 -0
  32. package/migrations/20260901091716_fine_arclight/migration.sql +21 -0
  33. package/migrations/20260901091716_fine_arclight/snapshot.json +3849 -0
  34. package/migrations/20260906155957_natural_moonstone/migration.sql +33 -0
  35. package/migrations/20260906155957_natural_moonstone/snapshot.json +4275 -0
  36. package/migrations/20260907020904_giant_eternals/migration.sql +21 -0
  37. package/migrations/20260907020904_giant_eternals/snapshot.json +4561 -0
  38. package/migrations/20260907044807_eminent_angel/migration.sql +2 -0
  39. package/migrations/20260907044807_eminent_angel/snapshot.json +4561 -0
  40. package/migrations/20260918083158_foamy_roughhouse/migration.sql +55 -0
  41. package/migrations/20260918083158_foamy_roughhouse/snapshot.json +5271 -0
  42. package/migrations/20260918142743_smart_cassandra_nova/migration.sql +2 -0
  43. package/migrations/20260918142743_smart_cassandra_nova/snapshot.json +5297 -0
  44. package/migrations/20260918143800_fat_princess_powerful/migration.sql +17 -0
  45. package/migrations/20260918143800_fat_princess_powerful/snapshot.json +5523 -0
  46. package/package.json +9 -6
  47. package/dist/authenticate-55LeXHqZ.d.ts +0 -1447
@@ -0,0 +1,3087 @@
1
+ import * as _simplewebauthn_server from '@simplewebauthn/server';
2
+ import { RegistrationResponseJSON, AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
3
+ import * as _spfn_core_route from '@spfn/core/route';
4
+ import { K as KeyAlgorithmType, h as KeyPlatformType, j as SocialProvider } from './types-DYyhze28.js';
5
+ import * as _sinclair_typebox from '@sinclair/typebox';
6
+ import { Static } from '@sinclair/typebox';
7
+ import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
8
+ import * as _spfn_core_event from '@spfn/core/event';
9
+ import { Context } from 'hono';
10
+ import { User } from '@spfn/auth/server';
11
+
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
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)
76
+ *
77
+ * Excludes:
78
+ * - Role and permissions (use auth session API)
79
+ */
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
+ }
92
+
93
+ /**
94
+ * @spfn/auth - Auth Service
95
+ *
96
+ * Core authentication logic: registration, login, logout, password management
97
+ */
98
+
99
+ interface RegisterParams {
100
+ email?: string;
101
+ phone?: string;
102
+ verificationToken: string;
103
+ password: string;
104
+ publicKey: string;
105
+ keyId: string;
106
+ fingerprint: string;
107
+ algorithm?: KeyAlgorithmType;
108
+ deviceName?: string;
109
+ platform?: KeyPlatformType;
110
+ metadata?: Record<string, unknown>;
111
+ /** Client address of the request, from `deviceProvenance` at the route. */
112
+ ip?: string;
113
+ /** `user-agent` of the request, already truncated at the route. */
114
+ userAgent?: string;
115
+ }
116
+ interface RegisterResult {
117
+ userId: string;
118
+ publicId: string;
119
+ email?: string;
120
+ phone?: string;
121
+ }
122
+ interface LoginParams {
123
+ email?: string;
124
+ phone?: string;
125
+ password: string;
126
+ publicKey: string;
127
+ keyId: string;
128
+ fingerprint: string;
129
+ oldKeyId?: string;
130
+ algorithm?: KeyAlgorithmType;
131
+ deviceName?: string;
132
+ platform?: KeyPlatformType;
133
+ /** Client address of the request, from `deviceProvenance` at the route. */
134
+ ip?: string;
135
+ /** `user-agent` of the request, already truncated at the route. */
136
+ userAgent?: string;
137
+ }
138
+ interface LoginResult {
139
+ userId: string;
140
+ publicId: string;
141
+ email?: string;
142
+ phone?: string;
143
+ passwordChangeRequired: boolean;
144
+ }
145
+ interface LogoutParams {
146
+ userId: number;
147
+ keyId: string;
148
+ }
149
+ interface ChangePasswordParams {
150
+ userId: number;
151
+ currentPassword?: string;
152
+ newPassword: string;
153
+ passwordHash?: string;
154
+ }
155
+ /**
156
+ * Register a new user account
157
+ */
158
+ declare function registerService(params: RegisterParams): Promise<RegisterResult>;
159
+ /**
160
+ * Authenticate user and create session
161
+ */
162
+ declare function loginService(params: LoginParams): Promise<LoginResult>;
163
+ /**
164
+ * Logout user (revoke current key)
165
+ */
166
+ declare function logoutService(params: LogoutParams): Promise<void>;
167
+ /**
168
+ * Change user password
169
+ */
170
+ declare function changePasswordService(params: ChangePasswordParams): Promise<void>;
171
+
172
+ declare const EmailSchema: _sinclair_typebox.TString;
173
+ declare const PhoneSchema: _sinclair_typebox.TString;
174
+ /**
175
+ * Optional device labels a client may send when registering a key.
176
+ *
177
+ * Display only: the key list uses them to tell one device from another, and
178
+ * nothing is authorized or refused by either value, so a client that lies about
179
+ * them gains nothing. Both are omitted by every key registered before they
180
+ * existed, hence optional rather than defaulted.
181
+ */
182
+ declare const DeviceNameSchema: _sinclair_typebox.TString;
183
+ declare const PlatformSchema: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>;
184
+ /**
185
+ * Key material as a device sends it, bounded.
186
+ *
187
+ * The bounds exist for the one route that takes this material from a caller who
188
+ * has not authenticated and cannot: `POST /_auth/device/start` persists what it
189
+ * is given, and a correctly fingerprinted megabyte of base64 would sit in
190
+ * `device_authorizations` until something swept it — and nothing sweeps it.
191
+ *
192
+ * The numbers are what real key material measures, with room to spare. The
193
+ * package's own generators produce SPKI DER in base64: 124 characters for
194
+ * ES256 (P-256), 392 for RS256 (RSA-2048). An RSA-4096 key would be 736, an
195
+ * RSA-8192 key about 1400, and the same 4096-bit key PEM-armoured about 800 —
196
+ * so 2048 admits every shape of key anyone could reasonably present, while a
197
+ * megabyte is refused three orders of magnitude before it reaches a row.
198
+ *
199
+ * `keyId` is a UUID (36) everywhere this package generates one; 64 leaves room
200
+ * for a client that prefixes or namespaces its own. `fingerprint` is SHA-256
201
+ * hex, exactly 64, and nothing else can ever verify against the public key —
202
+ * 128 is the length a longer digest would need, and no more.
203
+ */
204
+ declare const PublicKeySchema: _sinclair_typebox.TString;
205
+ declare const KeyIdSchema: _sinclair_typebox.TString;
206
+ declare const FingerprintSchema: _sinclair_typebox.TString;
207
+ /**
208
+ * The code a person reads off the waiting device and types on their own.
209
+ *
210
+ * Loose on purpose: 8 characters plus an optional dash is what is shown, but the
211
+ * server folds whitespace, dashes and lower case away before looking anything up,
212
+ * so refusing those spellings here would refuse a code that is on screen. The
213
+ * bounds exist to stop an unbounded string reaching the database, not to spell
214
+ * out the format — `USER_CODE_ALPHABET` is the only thing that can match a row.
215
+ */
216
+ declare const UserCodeSchema: _sinclair_typebox.TString;
217
+ /**
218
+ * What `POST /_auth/device/poll` answers with.
219
+ *
220
+ * A union, because the two answers are different kinds of thing rather than one
221
+ * shape with optional fields: pending says "ask again in this long", approved is
222
+ * a completed login carrying exactly what `/_auth/login` returns. `status` is the
223
+ * discriminant, so a generated client narrows on it instead of testing which
224
+ * fields happen to be present.
225
+ *
226
+ * The mobile contract has no union type, so it exports this as one object with
227
+ * `status` required and every branch field optional — see
228
+ * `deviceAuthorization.pollStatusRule` in the bundle. `intervalMillis` is an
229
+ * integer for the same reason: that grammar carries no floating-point scalar,
230
+ * and a count of milliseconds never needed one.
231
+ *
232
+ * That integer is a promise two things keep, because nothing validates a response
233
+ * against this schema on the way out. `configureDeviceAuth` refuses an interval
234
+ * that is not a whole number of milliseconds, so the only value this branch can
235
+ * carry is one; and `contract-export.test.ts` reads this schema to check the
236
+ * exported declaration, so writing `Type.Number` here fails the suite instead of
237
+ * publishing an integer the server does not send.
238
+ */
239
+ declare const DeviceAuthPollResponseSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TObject<{
240
+ status: _sinclair_typebox.TLiteral<"pending">;
241
+ intervalMillis: _sinclair_typebox.TInteger;
242
+ }>, _sinclair_typebox.TObject<{
243
+ status: _sinclair_typebox.TLiteral<"approved">;
244
+ userId: _sinclair_typebox.TString;
245
+ publicId: _sinclair_typebox.TString;
246
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
247
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
248
+ passwordChangeRequired: _sinclair_typebox.TBoolean;
249
+ }>]>;
250
+ declare const PasswordSchema: _sinclair_typebox.TString;
251
+ declare const TargetTypeSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
252
+ type VerificationTargetType = Static<typeof TargetTypeSchema>;
253
+ declare const VERIFICATION_TARGET_TYPES: readonly ["email", "phone"];
254
+ declare const VerificationPurposeSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"registration">, _sinclair_typebox.TLiteral<"login">, _sinclair_typebox.TLiteral<"password_reset">, _sinclair_typebox.TLiteral<"email_change">, _sinclair_typebox.TLiteral<"phone_change">, _sinclair_typebox.TLiteral<"account_deletion">]>;
255
+ type VerificationPurpose = Static<typeof VerificationPurposeSchema>;
256
+ declare const VERIFICATION_PURPOSES: readonly ["registration", "login", "password_reset", "email_change", "phone_change", "account_deletion"];
257
+
258
+ /**
259
+ * @spfn/auth - Verification Service
260
+ *
261
+ * Handles OTP code generation, validation, and delivery
262
+ */
263
+
264
+ interface SendVerificationCodeParams {
265
+ target: string;
266
+ targetType: VerificationTargetType;
267
+ purpose: VerificationPurpose;
268
+ }
269
+ interface SendVerificationCodeResult {
270
+ success: boolean;
271
+ expiresAt: string;
272
+ }
273
+ interface VerifyCodeParams {
274
+ target: string;
275
+ targetType: VerificationTargetType;
276
+ code: string;
277
+ purpose: VerificationPurpose;
278
+ }
279
+ interface VerifyCodeResult {
280
+ valid: boolean;
281
+ verificationToken: string;
282
+ }
283
+ /**
284
+ * Send verification code via email or SMS
285
+ */
286
+ declare function sendVerificationCodeService(params: SendVerificationCodeParams): Promise<SendVerificationCodeResult>;
287
+ /**
288
+ * Verify OTP code and return verification token
289
+ */
290
+ declare function verifyCodeService(params: VerifyCodeParams): Promise<{
291
+ valid: boolean;
292
+ verificationToken: string;
293
+ }>;
294
+
295
+ /**
296
+ * @spfn/auth - Verified-Email Signup Service
297
+ *
298
+ * A signup where the address is proven before a password exists:
299
+ *
300
+ * request -> a one-time link is emailed
301
+ * confirm -> the link is exchanged for a short-lived password-setup session
302
+ * password -> the account is created, the device registered, the user signed in
303
+ *
304
+ * The link token and the setup secret are bearer credentials, so neither is ever
305
+ * stored. Only their SHA-256 hashes are, and lookup is by hash. A database dump
306
+ * therefore yields nothing that can be presented to either step.
307
+ *
308
+ * The six-digit-code registration path is untouched and remains the default; this
309
+ * is a second entry point to the same account creation, not a replacement.
310
+ */
311
+
312
+ interface RequestSignupLinkParams {
313
+ email: string;
314
+ returnPath?: string;
315
+ }
316
+ interface RequestSignupLinkResult {
317
+ success: boolean;
318
+ expiresAt: string;
319
+ }
320
+ /**
321
+ * Step 1 — issue a confirmation link for an address.
322
+ *
323
+ * Answers identically whether or not the address already has an account. When it
324
+ * does, the owner gets a notice instead of a usable link, through the same
325
+ * dedupe window the six-digit-code path uses.
326
+ *
327
+ * Requesting again is how a resend works: every live link for the address is
328
+ * superseded first, so the newest link is the only one that opens, and any setup
329
+ * session already opened from an older link dies with it.
330
+ *
331
+ * Neither branch sends mail: both hand it to `auth.link-mail`, so the answer
332
+ * costs the same database work whichever one ran. With no pg-boss initialised
333
+ * the mail still goes out on this request — see `lib/link-mail-delivery.ts`.
334
+ */
335
+ declare function requestSignupLinkService(params: RequestSignupLinkParams): Promise<RequestSignupLinkResult>;
336
+ interface ConfirmSignupLinkParams {
337
+ token: string;
338
+ }
339
+ interface ConfirmSignupLinkResult {
340
+ email: string;
341
+ returnPath: string | null;
342
+ /** Handed to the proxy interceptor, which moves it into an HttpOnly cookie. */
343
+ setupSecret: string;
344
+ setupExpiresAt: string;
345
+ }
346
+ /**
347
+ * Step 2 — exchange a link for a password-setup session.
348
+ *
349
+ * Nothing binds the row to a device or a browser, which is what lets someone
350
+ * request the link on a laptop and open it on a phone.
351
+ */
352
+ declare function confirmSignupLinkService(params: ConfirmSignupLinkParams): Promise<ConfirmSignupLinkResult>;
353
+ interface CompleteSignupParams {
354
+ setupSecret?: string;
355
+ password: string;
356
+ publicKey: string;
357
+ keyId: string;
358
+ fingerprint: string;
359
+ algorithm?: KeyAlgorithmType;
360
+ deviceName?: string;
361
+ platform?: KeyPlatformType;
362
+ metadata?: Record<string, unknown>;
363
+ /** Client address of the request, from `deviceProvenance` at the route. */
364
+ ip?: string;
365
+ /** `user-agent` of the request, already truncated at the route. */
366
+ userAgent?: string;
367
+ }
368
+ /**
369
+ * Step 3 — set the password, which is what creates the account.
370
+ *
371
+ * Run under `Transactional()`: the user row, the device key and the completion
372
+ * mark commit together. A device-key failure must not leave an account nobody
373
+ * can sign into, and a completion mark must not survive a rolled-back account.
374
+ *
375
+ * A refusal that is the user's to fix — a weak password, an app policy that
376
+ * rejects the registration — leaves the setup session usable, so the fix is
377
+ * retyping the password rather than requesting a fresh email.
378
+ */
379
+ declare function completeSignupService(params: CompleteSignupParams): Promise<RegisterResult>;
380
+
381
+ /**
382
+ * @spfn/auth - Password Reset Service
383
+ *
384
+ * Getting back into an account whose password is gone, using the address the
385
+ * account already proved:
386
+ *
387
+ * request -> a one-time link is emailed
388
+ * confirm -> the link is exchanged for a short-lived password-setup session
389
+ * complete -> the new password is written, everything else is signed out,
390
+ * and the browser that reset is signed in on a fresh device key
391
+ *
392
+ * Mirrors the verified-email signup slice deliberately — same credentials, same
393
+ * hashing, same supersede-on-resend, same interceptor moves — with two
394
+ * differences that matter.
395
+ *
396
+ * First, the request answers identically for *every* input and sends mail only
397
+ * to an account that can be reset. Signup can afford to tell an existing owner
398
+ * "you already have an account"; a reset cannot send anything to a stranger's
399
+ * mailbox, because the mail itself would be the answer to "does this address
400
+ * have an account here".
401
+ *
402
+ * Second, completing it is a credential change on a live account, so it carries
403
+ * the same blast radius as `changePasswordService`: pending device
404
+ * authorizations are denied and every active key is revoked. Whoever was signed
405
+ * in on the old password is signed out, including the attacker the reset was
406
+ * needed for.
407
+ */
408
+
409
+ interface RequestPasswordResetParams {
410
+ email: string;
411
+ returnPath?: string;
412
+ }
413
+ interface RequestPasswordResetResult {
414
+ success: boolean;
415
+ expiresAt: string;
416
+ }
417
+ /**
418
+ * Step 1 — issue a reset link for an address.
419
+ *
420
+ * Answers identically for every input: the same status, the same two fields, and
421
+ * an `expiresAt` computed the same way whether or not a row was written. An
422
+ * address with no account, an account that cannot be reset, and an account that
423
+ * can are indistinguishable to the caller — only the first of the three gets
424
+ * mail, and it goes to the owner.
425
+ *
426
+ * Requesting again is how a resend works: every live link for the account is
427
+ * superseded first, so the newest link is the only one that opens, and any setup
428
+ * session already opened from an older link dies with it.
429
+ *
430
+ * The eligible branch does not send the mail either — it hands it to
431
+ * `auth.link-mail` — so the two branches differ by a few database writes and not
432
+ * by a mail provider's round trip. With no pg-boss initialised the mail still
433
+ * goes out on this request; see `lib/link-mail-delivery.ts`.
434
+ */
435
+ declare function requestPasswordResetService(params: RequestPasswordResetParams): Promise<RequestPasswordResetResult>;
436
+ interface ConfirmPasswordResetParams {
437
+ token: string;
438
+ }
439
+ interface ConfirmPasswordResetResult {
440
+ email: string;
441
+ returnPath: string | null;
442
+ /** Handed to the proxy interceptor, which moves it into an HttpOnly cookie. */
443
+ setupSecret: string;
444
+ setupExpiresAt: string;
445
+ }
446
+ /**
447
+ * Step 2 — exchange a link for a password-setup session.
448
+ *
449
+ * Nothing binds the row to a device or a browser, which is what lets someone ask
450
+ * for the link on a laptop and open it on a phone.
451
+ */
452
+ declare function confirmPasswordResetService(params: ConfirmPasswordResetParams): Promise<ConfirmPasswordResetResult>;
453
+ interface CompletePasswordResetParams {
454
+ setupSecret?: string;
455
+ password: string;
456
+ publicKey: string;
457
+ keyId: string;
458
+ fingerprint: string;
459
+ algorithm?: KeyAlgorithmType;
460
+ deviceName?: string;
461
+ platform?: KeyPlatformType;
462
+ /** Client address of the request, from `deviceProvenance` at the route. */
463
+ ip?: string;
464
+ /** `user-agent` of the request, already truncated at the route. */
465
+ userAgent?: string;
466
+ }
467
+ /**
468
+ * Step 3 — set the new password, which is what completes the reset.
469
+ *
470
+ * Run under `Transactional()`: the password, the revocations, the new device key
471
+ * and the completion mark commit together. A key registration that failed after
472
+ * the revoke-all would otherwise leave an account with a new password and
473
+ * nothing signed in.
474
+ *
475
+ * A refusal that is the user's to fix — a password the policy rejects, a body
476
+ * with no device key — leaves the setup session usable, so the fix is retyping
477
+ * the password rather than asking for a fresh email.
478
+ */
479
+ declare function completePasswordResetService(params: CompletePasswordResetParams): Promise<RegisterResult>;
480
+
481
+ /**
482
+ * @spfn/auth - Device Auth Service
483
+ *
484
+ * Device-code login: a device with no key on file yet shows a short code, the
485
+ * account owner types that code on a device that is already signed in, and the
486
+ * waiting device's key is registered on approval.
487
+ *
488
+ * There is no token to hand over. Every request in this system is signed by the
489
+ * calling device's own key, so "logging a device in" means one thing — getting
490
+ * its public key into `user_public_keys` under the right account. That is what
491
+ * the poll does, and it is why the poll returns exactly what `loginService`
492
+ * returns: from the client's side the two ways in are indistinguishable.
493
+ *
494
+ * | state ↓ op → | info | approve | deny | poll |
495
+ * | --- | --- | --- | --- | --- |
496
+ * | pending | device details | → approved | → denied | pending |
497
+ * | approved | AlreadyHandled | AlreadyHandled | AlreadyHandled | key registered, → consumed |
498
+ * | denied | AlreadyHandled | AlreadyHandled | AlreadyHandled | Denied |
499
+ * | consumed | NotFound | NotFound | NotFound | NotFound |
500
+ * | expired | Expired | Expired | Expired | Expired |
501
+ * | unknown | NotFound | NotFound | NotFound | NotFound |
502
+ *
503
+ * A global revocation — revoke-all, a password change, a deletion request —
504
+ * refuses the account's live records too, as `denied`, so they land in that row
505
+ * of the table. See `denyAllActiveByUserId`; the three callers are the three
506
+ * places that revoke every key at once.
507
+ */
508
+
509
+ interface StartDeviceAuthParams {
510
+ publicKey: string;
511
+ keyId: string;
512
+ fingerprint: string;
513
+ algorithm?: KeyAlgorithmType;
514
+ /** Device label shown to the approver. Display only — nothing is authorized by it. */
515
+ deviceName?: string;
516
+ platform?: KeyPlatformType;
517
+ }
518
+ interface StartDeviceAuthResult {
519
+ /** Returned once. The waiting device polls with it; the server stores only its hash. */
520
+ deviceCode: string;
521
+ /** `XXXX-XXXX`, for the waiting device's screen and nowhere else. */
522
+ userCode: string;
523
+ expiresAtMillis: number;
524
+ /** Milliseconds the waiting device should wait between polls. */
525
+ intervalMillis: number;
526
+ }
527
+ interface DeviceAuthInfoParams {
528
+ userCode: string;
529
+ }
530
+ /** What the approver is shown about the device asking to be let in. */
531
+ interface DeviceAuthInfoResult {
532
+ deviceName?: string;
533
+ /** One of `KEY_PLATFORM`, which is what the route accepts and the column stores. */
534
+ platform?: KeyPlatformType;
535
+ /** First bytes of the pending key's fingerprint, as the device list truncates it. */
536
+ fingerprintPrefix: string;
537
+ requestedAtMillis: number;
538
+ expiresAtMillis: number;
539
+ }
540
+ interface ApproveDeviceAuthParams {
541
+ userCode: string;
542
+ /** The approver, read from their session. Never from a request body. */
543
+ userId: number;
544
+ }
545
+ interface DenyDeviceAuthParams {
546
+ userCode: string;
547
+ }
548
+ interface PollDeviceAuthParams {
549
+ deviceCode: string;
550
+ /** Client address of the request, from `deviceProvenance` at the route. */
551
+ ip?: string;
552
+ /** `user-agent` of the request, already truncated at the route. */
553
+ userAgent?: string;
554
+ }
555
+ /** Nobody has answered yet. Not an error — the waiting device waits. */
556
+ interface DeviceAuthPendingResult {
557
+ status: 'pending';
558
+ intervalMillis: number;
559
+ }
560
+ /** Approved and spent: the key is registered and this is the login it produced. */
561
+ type DeviceAuthApprovedResult = {
562
+ status: 'approved';
563
+ } & LoginResult;
564
+ type PollDeviceAuthResult = DeviceAuthPendingResult | DeviceAuthApprovedResult;
565
+ /**
566
+ * Park a new device's key and hand back the codes it needs.
567
+ *
568
+ * The caller is unauthenticated by definition — this is what a device does before
569
+ * it has any way to prove anything — so nothing here is attributed to an account.
570
+ * The record gains an owner only when someone approves it.
571
+ */
572
+ declare function startDeviceAuthService(params: StartDeviceAuthParams): Promise<StartDeviceAuthResult>;
573
+ /**
574
+ * What the approver sees before deciding.
575
+ *
576
+ * This is the whole defence against being talked into approving someone else's
577
+ * device: the answer names the device that is waiting, so the person holding the
578
+ * phone can see that it is not theirs. An approval screen that showed only the
579
+ * code would be asking them to confirm a number they were just told.
580
+ */
581
+ declare function getDeviceAuthInfoService(params: DeviceAuthInfoParams): Promise<DeviceAuthInfoResult>;
582
+ /**
583
+ * Bind the record to the approving account.
584
+ *
585
+ * The key is not registered here. The waiting device may never come back, and a
586
+ * key registered for a device that stopped listening is a signing credential
587
+ * nobody asked for — so approval records the decision and the poll acts on it.
588
+ *
589
+ * Answers with the same device description `info` returns, so a client that let
590
+ * a user approve without looking first can still show them what they just let
591
+ * in — which is the moment someone talked into approving an attacker's device
592
+ * has to notice and revoke it.
593
+ */
594
+ declare function approveDeviceAuthService(params: ApproveDeviceAuthParams): Promise<DeviceAuthInfoResult>;
595
+ /**
596
+ * Refuse the record, so the waiting device is told no instead of timing out.
597
+ *
598
+ * Denying binds no user: the point of refusing is that the account owner wants
599
+ * nothing to do with the request.
600
+ */
601
+ declare function denyDeviceAuthService(params: DenyDeviceAuthParams): Promise<void>;
602
+ /**
603
+ * The waiting device asking whether anyone has answered.
604
+ *
605
+ * Approved is the one branch with a side effect, and it is a one-shot: the record
606
+ * is spent by a conditional update that names `approved`, so of two polls that
607
+ * arrive together exactly one registers the key. The loser matches nothing and is
608
+ * answered as if the code were unknown — which by then it is.
609
+ */
610
+ declare function pollDeviceAuthService(params: PollDeviceAuthParams): Promise<PollDeviceAuthResult>;
611
+
612
+ /**
613
+ * @spfn/auth - Passkeys Entity
614
+ *
615
+ * A WebAuthn credential the account owner enrolled on one of their devices.
616
+ * It is a *credential*, not a session: an assertion proves who is asking, and
617
+ * the ordinary device key in `user_public_keys` is what the request afterwards
618
+ * is signed with. The two tables therefore never stand in for each other.
619
+ *
620
+ * Nothing here is a bearer value, so nothing is hashed. `publicKey` is public by
621
+ * construction and `credentialId` is a handle the authenticator hands to any
622
+ * origin that asks — storing either in the clear costs nothing, and the lookup
623
+ * on `credentialId` has to be a plain equality match on an indexed column.
624
+ *
625
+ * Revocation is soft, and `credentialId` stays unique across live and revoked
626
+ * rows alike: a credential someone cut off must never become enrollable again,
627
+ * on this account or on another one.
628
+ */
629
+ /**
630
+ * Whether the credential can leave the authenticator that minted it.
631
+ *
632
+ * `multiDevice` is a synced passkey (iCloud Keychain, Google Password Manager);
633
+ * `singleDevice` is bound to one authenticator. Reported by the authenticator at
634
+ * enrollment and shown in the management list, because "this one is only on that
635
+ * phone" is what the owner needs to know before revoking the other entry.
636
+ */
637
+ declare const PASSKEY_DEVICE_TYPES: readonly ["singleDevice", "multiDevice"];
638
+ type PasskeyDeviceType = typeof PASSKEY_DEVICE_TYPES[number];
639
+ /** How long a label may be — the key list's `deviceName` bound, for the same reason. */
640
+ declare const PASSKEY_LABEL_MAX_LENGTH = 64;
641
+ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
642
+ name: "passkeys";
643
+ schema: string;
644
+ columns: {
645
+ createdAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>, {
646
+ name: string;
647
+ tableName: "passkeys";
648
+ dataType: "object date";
649
+ data: Date;
650
+ driverParam: string;
651
+ notNull: true;
652
+ hasDefault: true;
653
+ isPrimaryKey: false;
654
+ isAutoincrement: false;
655
+ hasRuntimeDefault: false;
656
+ enumValues: undefined;
657
+ identity: undefined;
658
+ generated: undefined;
659
+ }>;
660
+ 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>>>, {
661
+ name: string;
662
+ tableName: "passkeys";
663
+ dataType: "object date";
664
+ data: Date;
665
+ driverParam: string;
666
+ notNull: true;
667
+ hasDefault: true;
668
+ isPrimaryKey: false;
669
+ isAutoincrement: false;
670
+ hasRuntimeDefault: false;
671
+ enumValues: undefined;
672
+ identity: undefined;
673
+ generated: undefined;
674
+ }>;
675
+ id: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
676
+ name: string;
677
+ tableName: "passkeys";
678
+ dataType: "number int53";
679
+ data: number;
680
+ driverParam: number;
681
+ notNull: true;
682
+ hasDefault: true;
683
+ isPrimaryKey: false;
684
+ isAutoincrement: false;
685
+ hasRuntimeDefault: false;
686
+ enumValues: undefined;
687
+ identity: undefined;
688
+ generated: undefined;
689
+ }>;
690
+ userId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
691
+ name: string;
692
+ tableName: "passkeys";
693
+ dataType: "number int53";
694
+ data: number;
695
+ driverParam: string | number;
696
+ notNull: true;
697
+ hasDefault: false;
698
+ isPrimaryKey: false;
699
+ isAutoincrement: false;
700
+ hasRuntimeDefault: false;
701
+ enumValues: undefined;
702
+ identity: undefined;
703
+ generated: undefined;
704
+ }>;
705
+ credentialId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
706
+ name: string;
707
+ tableName: "passkeys";
708
+ dataType: "string";
709
+ data: string;
710
+ driverParam: string;
711
+ notNull: true;
712
+ hasDefault: false;
713
+ isPrimaryKey: false;
714
+ isAutoincrement: false;
715
+ hasRuntimeDefault: false;
716
+ enumValues: undefined;
717
+ identity: undefined;
718
+ generated: undefined;
719
+ }>;
720
+ publicKey: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
721
+ name: string;
722
+ tableName: "passkeys";
723
+ dataType: "string";
724
+ data: string;
725
+ driverParam: string;
726
+ notNull: true;
727
+ hasDefault: false;
728
+ isPrimaryKey: false;
729
+ isAutoincrement: false;
730
+ hasRuntimeDefault: false;
731
+ enumValues: undefined;
732
+ identity: undefined;
733
+ generated: undefined;
734
+ }>;
735
+ counter: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgIntegerBuilder>>, {
736
+ name: string;
737
+ tableName: "passkeys";
738
+ dataType: "number int32";
739
+ data: number;
740
+ driverParam: string | number;
741
+ notNull: true;
742
+ hasDefault: true;
743
+ isPrimaryKey: false;
744
+ isAutoincrement: false;
745
+ hasRuntimeDefault: false;
746
+ enumValues: undefined;
747
+ identity: undefined;
748
+ generated: undefined;
749
+ }>;
750
+ transports: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetDimensions<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, 1>, {
751
+ name: string;
752
+ tableName: "passkeys";
753
+ dataType: "string";
754
+ data: string[];
755
+ driverParam: string | string[];
756
+ notNull: false;
757
+ hasDefault: false;
758
+ isPrimaryKey: false;
759
+ isAutoincrement: false;
760
+ hasRuntimeDefault: false;
761
+ enumValues: undefined;
762
+ identity: undefined;
763
+ generated: undefined;
764
+ }>;
765
+ deviceType: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["singleDevice", "multiDevice"] & [string, ...string[]]>>, {
766
+ name: string;
767
+ tableName: "passkeys";
768
+ dataType: "string enum";
769
+ data: "singleDevice" | "multiDevice";
770
+ driverParam: string;
771
+ notNull: true;
772
+ hasDefault: false;
773
+ isPrimaryKey: false;
774
+ isAutoincrement: false;
775
+ hasRuntimeDefault: false;
776
+ enumValues: ["singleDevice", "multiDevice"] & [string, ...string[]];
777
+ identity: undefined;
778
+ generated: undefined;
779
+ }>;
780
+ backedUp: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
781
+ name: string;
782
+ tableName: "passkeys";
783
+ dataType: "boolean";
784
+ data: boolean;
785
+ driverParam: boolean;
786
+ notNull: true;
787
+ hasDefault: true;
788
+ isPrimaryKey: false;
789
+ isAutoincrement: false;
790
+ hasRuntimeDefault: false;
791
+ enumValues: undefined;
792
+ identity: undefined;
793
+ generated: undefined;
794
+ }>;
795
+ aaguid: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
796
+ name: string;
797
+ tableName: "passkeys";
798
+ dataType: "string";
799
+ data: string;
800
+ driverParam: string;
801
+ notNull: false;
802
+ hasDefault: false;
803
+ isPrimaryKey: false;
804
+ isAutoincrement: false;
805
+ hasRuntimeDefault: false;
806
+ enumValues: undefined;
807
+ identity: undefined;
808
+ generated: undefined;
809
+ }>;
810
+ label: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
811
+ name: string;
812
+ tableName: "passkeys";
813
+ dataType: "string";
814
+ data: string;
815
+ driverParam: string;
816
+ notNull: false;
817
+ hasDefault: false;
818
+ isPrimaryKey: false;
819
+ isAutoincrement: false;
820
+ hasRuntimeDefault: false;
821
+ enumValues: undefined;
822
+ identity: undefined;
823
+ generated: undefined;
824
+ }>;
825
+ lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
826
+ name: string;
827
+ tableName: "passkeys";
828
+ dataType: "object date";
829
+ data: Date;
830
+ driverParam: string;
831
+ notNull: false;
832
+ hasDefault: false;
833
+ isPrimaryKey: false;
834
+ isAutoincrement: false;
835
+ hasRuntimeDefault: false;
836
+ enumValues: undefined;
837
+ identity: undefined;
838
+ generated: undefined;
839
+ }>;
840
+ revokedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
841
+ name: string;
842
+ tableName: "passkeys";
843
+ dataType: "object date";
844
+ data: Date;
845
+ driverParam: string;
846
+ notNull: false;
847
+ hasDefault: false;
848
+ isPrimaryKey: false;
849
+ isAutoincrement: false;
850
+ hasRuntimeDefault: false;
851
+ enumValues: undefined;
852
+ identity: undefined;
853
+ generated: undefined;
854
+ }>;
855
+ revokedReason: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
856
+ name: string;
857
+ tableName: "passkeys";
858
+ dataType: "string";
859
+ data: string;
860
+ driverParam: string;
861
+ notNull: false;
862
+ hasDefault: false;
863
+ isPrimaryKey: false;
864
+ isAutoincrement: false;
865
+ hasRuntimeDefault: false;
866
+ enumValues: undefined;
867
+ identity: undefined;
868
+ generated: undefined;
869
+ }>;
870
+ };
871
+ dialect: "pg";
872
+ }>;
873
+ type Passkey = typeof passkeys.$inferSelect;
874
+ type NewPasskey = typeof passkeys.$inferInsert;
875
+
876
+ /**
877
+ * @spfn/auth - Passkey Service
878
+ *
879
+ * WebAuthn passkeys as an optional account credential, alongside a password and
880
+ * a linked social account rather than in place of either.
881
+ *
882
+ * enroll -> options on an identified session, then verify the attestation
883
+ * sign in -> options with no identifier at all, then verify the assertion
884
+ * manage -> list, rename, revoke
885
+ *
886
+ * A passkey is not a device key. The assertion proves *who* is asking; the
887
+ * device key registered right after it is what every later request is signed
888
+ * with, exactly as after a password login (D2). Nothing in clientProofV1 or in
889
+ * the JWT path changes because a session started this way.
890
+ *
891
+ * Challenges are rows, spent by one conditional UPDATE (D7). Two verifies
892
+ * arriving with the same challenge therefore produce one winner and one refusal,
893
+ * across instances, rather than both reading it as live.
894
+ *
895
+ * Revoking the last thing an account can sign in with is refused (D6) rather
896
+ * than warned about, because that state has no undo. A verified email address
897
+ * counts as one of those things: the password reset flow can always give such an
898
+ * account a password back.
899
+ */
900
+
901
+ /** One enrolled credential as the management surface shows it. */
902
+ interface PasskeySummary {
903
+ passkeyId: string;
904
+ label: string | null;
905
+ deviceType: PasskeyDeviceType;
906
+ backedUp: boolean;
907
+ transports: string[];
908
+ createdAt: string;
909
+ lastUsedAt: string | null;
910
+ }
911
+ interface RecentAuthenticationParams {
912
+ userId: number;
913
+ /** The device key this request is signed with — its age is the signal. */
914
+ keyId: string;
915
+ currentPassword?: string;
916
+ }
917
+ /**
918
+ * Refuse a passkey change unless the caller has recently proved themselves (D4).
919
+ *
920
+ * Two ways to satisfy it. The device key this request is signed with was
921
+ * registered within the window — that is when this device last presented a
922
+ * credential, and it needs no new state. Or the body carries the account
923
+ * password.
924
+ *
925
+ * An account with no password stored cannot satisfy it with a password, however
926
+ * plausible the value (E5): the comparison still runs, against a dummy hash, so
927
+ * "no password on file" costs exactly what "wrong password" costs. Skipping it
928
+ * would turn response time into an oracle for which accounts are OAuth-only.
929
+ *
930
+ * @throws RecentAuthenticationRequiredError
931
+ */
932
+ declare function assertRecentAuthentication(params: RecentAuthenticationParams): Promise<void>;
933
+ /**
934
+ * Refuse to remove the only thing an account can sign in with (D6).
935
+ *
936
+ * The recovery paths are: another live passkey, a password, a linked social
937
+ * account, and a verified email address. The last one is new — a password reset
938
+ * now exists in this package, and an account that can be reset by email can
939
+ * always get a password back, so the refusal has nothing left to protect.
940
+ *
941
+ * What remains refused is the account with none of the four: no other passkey,
942
+ * no password, no social account, and no verified email — a phone-only account
943
+ * among them. Nobody, support included, could undo that state.
944
+ *
945
+ * @throws LastRecoveryCredentialError
946
+ */
947
+ declare function assertNotLastRecoveryCredential(userId: number): Promise<void>;
948
+ interface StartPasskeyEnrollmentParams {
949
+ userId: number;
950
+ keyId: string;
951
+ currentPassword?: string;
952
+ }
953
+ /**
954
+ * Step 1 of enrollment — options for `navigator.credentials.create()`.
955
+ *
956
+ * `excludeCredentials` lists the caller's **live** passkeys only, so the
957
+ * authenticator quietly refuses one already enrolled here. Revoked ones are left
958
+ * out on purpose: they must not be re-enrolled either, and the check that
959
+ * refuses them is the global uniqueness check at verify (E11/M10) — listing them
960
+ * here would hand out credential ids the account no longer uses.
961
+ */
962
+ declare function startPasskeyEnrollmentService(params: StartPasskeyEnrollmentParams): Promise<PublicKeyCredentialCreationOptionsJSON>;
963
+ interface FinishPasskeyEnrollmentParams {
964
+ userId: number;
965
+ response: RegistrationResponseJSON;
966
+ label?: string;
967
+ }
968
+ interface FinishPasskeyEnrollmentResult {
969
+ passkeyId: string;
970
+ label: string | null;
971
+ createdAt: string;
972
+ }
973
+ /**
974
+ * Step 2 of enrollment — verify the attestation and keep the credential.
975
+ *
976
+ * Runs under `Transactional()`: the challenge is spent and the row written
977
+ * together, so a failure after the spend leaves the challenge live and the
978
+ * ceremony retryable, while a success can never be replayed.
979
+ */
980
+ declare function finishPasskeyEnrollmentService(params: FinishPasskeyEnrollmentParams): Promise<FinishPasskeyEnrollmentResult>;
981
+ /**
982
+ * Step 1 of sign-in — options for `navigator.credentials.get()`.
983
+ *
984
+ * Takes nothing and returns the same shape to everyone: `allowCredentials` is
985
+ * always empty and the challenge row names no account (D3). There is no input
986
+ * that could make this answer differ by whether an account exists, which is the
987
+ * point — the discoverable credential on the device is what names the owner.
988
+ */
989
+ declare function startPasskeyLoginService(): Promise<PublicKeyCredentialRequestOptionsJSON>;
990
+ interface FinishPasskeyLoginParams {
991
+ response: AuthenticationResponseJSON;
992
+ publicKey: string;
993
+ keyId: string;
994
+ fingerprint: string;
995
+ algorithm?: KeyAlgorithmType;
996
+ oldKeyId?: string;
997
+ deviceName?: string;
998
+ platform?: KeyPlatformType;
999
+ /** Client address of the request, from `deviceProvenance` at the route. */
1000
+ ip?: string;
1001
+ /** `user-agent` of the request, already truncated at the route. */
1002
+ userAgent?: string;
1003
+ }
1004
+ /**
1005
+ * Step 2 of sign-in — verify the assertion, then sign in exactly as a password
1006
+ * login does.
1007
+ *
1008
+ * The tail from the active-status check onward is the one every identified
1009
+ * sign-in runs (`loginService`, the OAuth flows): revoke the key being replaced,
1010
+ * register the new device key, stamp the last login, announce it after commit.
1011
+ * Passkeys add a way to prove identity, not a second way to hold a session.
1012
+ */
1013
+ declare function finishPasskeyLoginService(params: FinishPasskeyLoginParams): Promise<LoginResult>;
1014
+ /**
1015
+ * The caller's live passkeys, newest first.
1016
+ */
1017
+ declare function listPasskeysService(userId: number): Promise<PasskeySummary[]>;
1018
+ interface RenamePasskeyParams {
1019
+ userId: number;
1020
+ passkeyId: string;
1021
+ label: string;
1022
+ }
1023
+ /**
1024
+ * Rename a passkey. Owner-scoped, so someone else's id is a 404 and nothing
1025
+ * about it is disclosed.
1026
+ *
1027
+ * No recent-authentication gate: a label is display only, and nothing is
1028
+ * authorized by it.
1029
+ */
1030
+ declare function renamePasskeyService(params: RenamePasskeyParams): Promise<{
1031
+ passkeyId: string;
1032
+ label: string;
1033
+ }>;
1034
+ interface RevokePasskeyParams {
1035
+ userId: number;
1036
+ keyId: string;
1037
+ passkeyId: string;
1038
+ currentPassword?: string;
1039
+ }
1040
+ /**
1041
+ * Retire a passkey.
1042
+ *
1043
+ * Gated on recent authentication, because someone who walked up to an unlocked
1044
+ * laptop should not be able to strip the account's credentials; and on the
1045
+ * last-recovery-credential guard, because there is no undo for the state that
1046
+ * would leave.
1047
+ *
1048
+ * The owner row is locked before the guard runs, and the route's
1049
+ * `Transactional()` is what holds that lock to commit. Without it the guard is a
1050
+ * read-modify-write with a gap: an owner with two passkeys and nothing else who
1051
+ * fires two revokes at once has both count two live credentials, both pass, and
1052
+ * both revoke — the exact state the guard exists to refuse. Locking makes the
1053
+ * second revoke count one.
1054
+ */
1055
+ declare function revokePasskeyService(params: RevokePasskeyParams): Promise<{
1056
+ passkeyId: string;
1057
+ }>;
1058
+
1059
+ /**
1060
+ * Auth provider type
1061
+ *
1062
+ * 직접 인증(email/phone) + 등록 가능한 모든 소셜 provider(SOCIAL_PROVIDERS).
1063
+ */
1064
+ declare const AuthProviderSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">, ..._sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]]>;
1065
+ /**
1066
+ * auth.login - 로그인 성공 이벤트
1067
+ *
1068
+ * 발행 시점:
1069
+ * - 이메일/전화 로그인 성공 시
1070
+ * - OAuth 기존 사용자 로그인 시
1071
+ * - 기기 코드 승인이 소비되어 새 기기 키가 등록될 때 (provider: 'device')
1072
+ *
1073
+ * @example
1074
+ * ```typescript
1075
+ * authLoginEvent.subscribe(async (payload) => {
1076
+ * await analytics.trackLogin(payload.userId, payload.provider);
1077
+ * });
1078
+ * ```
1079
+ */
1080
+ declare const authLoginEvent: _spfn_core_event.EventDef<{
1081
+ email?: string | undefined;
1082
+ phone?: string | undefined;
1083
+ userId: string;
1084
+ provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "device" | "passkey";
1085
+ }>;
1086
+ /**
1087
+ * Where a device key was registered — the door the new device came through.
1088
+ *
1089
+ * Required on `RegisterPublicKeyParams` rather than optional with a default: a
1090
+ * new *call site* for key registration must choose one, and a default would let
1091
+ * it inherit somebody else's answer silently. `'register'` and `'signup-link'`
1092
+ * both arrive at `createVerifiedAccount`, so the two name themselves there;
1093
+ * `'invitation'` is the one path that stores a key without the key service.
1094
+ */
1095
+ 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">]>;
1096
+ /** The nine doors a device key is registered through. */
1097
+ type DeviceRegistrationChannel = Static<typeof DeviceRegistrationChannelSchema>;
1098
+ /**
1099
+ * auth.device.registered — a new device key was added to an account
1100
+ *
1101
+ * 발행 시점:
1102
+ * - a key row was created for an account and the transaction that created it
1103
+ * committed, on every one of the nine channels above
1104
+ *
1105
+ * This is the notice an account owner needs and could not get before: a stolen
1106
+ * password used to sign in on a new device was silent, because a login event
1107
+ * says a session began and not what it began on. Rotation is deliberately not
1108
+ * announced — replacing the key of a device that is already signed in is not a
1109
+ * new device, and a notice for it would train the owner to ignore the ones that
1110
+ * matter.
1111
+ *
1112
+ * `ip` and `userAgent` are what the registering request said about itself. Both
1113
+ * are unauthenticated display material: nothing is decided by them, and a field
1114
+ * is absent rather than carrying a placeholder when the request resolved none.
1115
+ *
1116
+ * Neither the full fingerprint nor the public key is carried. The prefix is
1117
+ * enough to point at one entry of `listKeys`, which is what a notice needs.
1118
+ *
1119
+ * @example
1120
+ * ```typescript
1121
+ * authDeviceRegisteredEvent.subscribe(async ({ userId, deviceName, ip, channel }) => {
1122
+ * await notifyOwner(userId, `A new device signed in (${deviceName ?? channel})`);
1123
+ * });
1124
+ * ```
1125
+ */
1126
+ declare const authDeviceRegisteredEvent: _spfn_core_event.EventDef<{
1127
+ deviceName?: string | undefined;
1128
+ platform?: string | undefined;
1129
+ ip?: string | undefined;
1130
+ userAgent?: string | undefined;
1131
+ keyId: string;
1132
+ algorithm: string;
1133
+ userId: string;
1134
+ fingerprintPrefix: string;
1135
+ createdAtMillis: number;
1136
+ channel: "password" | "register" | "oauth-native" | "oauth" | "invitation" | "signup-link" | "password-reset" | "passkey" | "device-code";
1137
+ }>;
1138
+ /**
1139
+ * auth.register - 회원가입 성공 이벤트
1140
+ *
1141
+ * 발행 시점:
1142
+ * - 이메일/전화 회원가입 성공 시
1143
+ * - OAuth 신규 사용자 가입 시
1144
+ *
1145
+ * @example
1146
+ * ```typescript
1147
+ * authRegisterEvent.subscribe(async (payload) => {
1148
+ * await emailService.sendWelcome(payload.email);
1149
+ * });
1150
+ * ```
1151
+ */
1152
+ declare const authRegisterEvent: _spfn_core_event.EventDef<{
1153
+ email?: string | undefined;
1154
+ phone?: string | undefined;
1155
+ metadata?: {
1156
+ [x: string]: unknown;
1157
+ } | undefined;
1158
+ userId: string;
1159
+ provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
1160
+ }>;
1161
+ /**
1162
+ * auth.invitation.created - 초대 생성 이벤트
1163
+ *
1164
+ * 발행 시점:
1165
+ * - createInvitation() 성공 시
1166
+ * - resendInvitation() 성공 시
1167
+ *
1168
+ * @example
1169
+ * ```typescript
1170
+ * invitationCreatedEvent.subscribe(async (payload) => {
1171
+ * const inviteUrl = `${APP_URL}/invite/${payload.token}`;
1172
+ * await notificationService.send({
1173
+ * channel: 'email',
1174
+ * to: payload.email,
1175
+ * subject: 'You are invited!',
1176
+ * html: renderInviteEmail({ inviteUrl, ...payload.metadata }),
1177
+ * });
1178
+ * });
1179
+ * ```
1180
+ */
1181
+ declare const invitationCreatedEvent: _spfn_core_event.EventDef<{
1182
+ metadata?: {
1183
+ [x: string]: unknown;
1184
+ } | undefined;
1185
+ email: string;
1186
+ token: string;
1187
+ expiresAt: string;
1188
+ roleId: number;
1189
+ invitedBy: string;
1190
+ invitationId: string;
1191
+ isResend: boolean;
1192
+ }>;
1193
+ /**
1194
+ * auth.invitation.accepted - 초대 수락 이벤트
1195
+ *
1196
+ * 발행 시점:
1197
+ * - acceptInvitation() 성공 시
1198
+ *
1199
+ * @example
1200
+ * ```typescript
1201
+ * invitationAcceptedEvent.subscribe(async (payload) => {
1202
+ * await onboardingService.start(payload.userId);
1203
+ * });
1204
+ * ```
1205
+ */
1206
+ declare const invitationAcceptedEvent: _spfn_core_event.EventDef<{
1207
+ metadata?: {
1208
+ [x: string]: unknown;
1209
+ } | undefined;
1210
+ email: string;
1211
+ userId: string;
1212
+ roleId: number;
1213
+ invitedBy: string;
1214
+ invitationId: string;
1215
+ }>;
1216
+ /**
1217
+ * auth.deletion.requested - 계정 탈퇴 요청 이벤트
1218
+ *
1219
+ * 발행 시점:
1220
+ * - requestAccountDeletionService() 성공 시 (self/admin 공통)
1221
+ *
1222
+ * @example
1223
+ * ```typescript
1224
+ * authDeletionRequestedEvent.subscribe(async (payload) => {
1225
+ * await analytics.trackChurnRisk(payload.userId);
1226
+ * });
1227
+ * ```
1228
+ */
1229
+ declare const authDeletionRequestedEvent: _spfn_core_event.EventDef<{
1230
+ userId: string;
1231
+ purgeScheduledAt: string;
1232
+ userPublicId: string;
1233
+ requestedBy: "admin" | "self";
1234
+ }>;
1235
+ /**
1236
+ * auth.deletion.cancelled - 계정 탈퇴 복구 이벤트
1237
+ *
1238
+ * 발행 시점:
1239
+ * - cancelAccountDeletionService() 성공 시 (유예 기간 내 복구)
1240
+ */
1241
+ declare const authDeletionCancelledEvent: _spfn_core_event.EventDef<{
1242
+ userId: string;
1243
+ userPublicId: string;
1244
+ }>;
1245
+ /**
1246
+ * auth.deletion.completed - 계정 파기 완료 이벤트
1247
+ *
1248
+ * 발행 시점:
1249
+ * - purge job(또는 즉시 파기 경로)이 유저를 파기한 직후
1250
+ *
1251
+ * PII를 담지 않는다 — userId(내부 순번)/email/phone 없이 userPublicId만 실어
1252
+ * 파기 완료 이후에도 구독자가 식별 정보를 다시 축적하지 않도록 한다.
1253
+ */
1254
+ declare const authDeletionCompletedEvent: _spfn_core_event.EventDef<{
1255
+ userPublicId: string;
1256
+ purgeStrategy: "anonymize" | "hard-delete";
1257
+ }>;
1258
+ /**
1259
+ * auth.oauth.unlinked - provider발 연동 해제 이벤트
1260
+ *
1261
+ * 발행 시점:
1262
+ * - provider(카카오·네이버 등)가 unlink-notify 웹훅으로 연동 해제를 알려와
1263
+ * 소셜 계정 연결과 저장 토큰이 삭제된 직후
1264
+ *
1265
+ * 연결 삭제까지는 프레임워크가 수행하고, 그 이후(계정 탈퇴로 이어갈지 등)는
1266
+ * 앱 정책이므로 이 이벤트를 구독해 처리한다.
1267
+ *
1268
+ * @example
1269
+ * ```typescript
1270
+ * oauthUnlinkedEvent.subscribe(async (payload) => {
1271
+ * await requestAccountDeletionService({ userId: payload.userId, requestedBy: 'self' });
1272
+ * });
1273
+ * ```
1274
+ */
1275
+ declare const oauthUnlinkedEvent: _spfn_core_event.EventDef<{
1276
+ reason?: string | undefined;
1277
+ userId: string;
1278
+ provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself";
1279
+ providerUserId: string;
1280
+ }>;
1281
+ /**
1282
+ * auth.password.reset — an account's password was replaced through a reset link
1283
+ *
1284
+ * 발행 시점:
1285
+ * - completePasswordResetService()가 커밋된 직후
1286
+ *
1287
+ * Distinct from a password *change*, which is made from a session that already
1288
+ * proved itself. This one is made by whoever opened a link in a mailbox, so it
1289
+ * is the event an app hangs a "your password was reset" notice on — and the
1290
+ * signal to look at, if the owner says they did not ask for it.
1291
+ *
1292
+ * @example
1293
+ * ```typescript
1294
+ * authPasswordResetEvent.subscribe(async (payload) => {
1295
+ * await notifyOwner(payload.userId, 'Your password was reset');
1296
+ * });
1297
+ * ```
1298
+ */
1299
+ declare const authPasswordResetEvent: _spfn_core_event.EventDef<{
1300
+ email: string;
1301
+ userId: string;
1302
+ }>;
1303
+ /**
1304
+ * Auth event payload types
1305
+ */
1306
+ type AuthLoginPayload = typeof authLoginEvent._payload;
1307
+ type AuthRegisterPayload = typeof authRegisterEvent._payload;
1308
+ type AuthPasswordResetPayload = typeof authPasswordResetEvent._payload;
1309
+ type AuthDeviceRegisteredPayload = typeof authDeviceRegisteredEvent._payload;
1310
+ type InvitationCreatedPayload = typeof invitationCreatedEvent._payload;
1311
+ type InvitationAcceptedPayload = typeof invitationAcceptedEvent._payload;
1312
+ type AuthDeletionRequestedPayload = typeof authDeletionRequestedEvent._payload;
1313
+ type AuthDeletionCancelledPayload = typeof authDeletionCancelledEvent._payload;
1314
+ type AuthDeletionCompletedPayload = typeof authDeletionCompletedEvent._payload;
1315
+ type OAuthUnlinkedPayload = typeof oauthUnlinkedEvent._payload;
1316
+
1317
+ /**
1318
+ * @spfn/auth - Key Service
1319
+ *
1320
+ * Handles public key registration, rotation, and revocation
1321
+ */
1322
+
1323
+ interface RegisterPublicKeyParams {
1324
+ userId: number;
1325
+ keyId: string;
1326
+ publicKey: string;
1327
+ fingerprint: string;
1328
+ algorithm?: KeyAlgorithmType;
1329
+ /** Device label for the key list. Display only — nothing is authorized by it. */
1330
+ deviceName?: string;
1331
+ platform?: KeyPlatformType;
1332
+ /**
1333
+ * Which door this device came through. Required, so that a registration path
1334
+ * added later has to say what it is rather than inherit an answer.
1335
+ */
1336
+ channel: DeviceRegistrationChannel;
1337
+ /** Client address of the registering request, absent when none resolved. */
1338
+ ip?: string;
1339
+ /** `user-agent` of the registering request, already truncated. */
1340
+ userAgent?: string;
1341
+ /**
1342
+ * The key this one replaces on the same device, when a revocation actually
1343
+ * happened. Its presence is what makes this a rotation rather than a new
1344
+ * device, so no event is announced for it.
1345
+ *
1346
+ * Only ever set from a `revokeKeyService` that returned true: an `oldKeyId`
1347
+ * naming somebody else's key, an already-revoked key or nothing at all
1348
+ * revokes nothing, and treating that as a rotation would be a way to
1349
+ * register a device with the owner's notice switched off.
1350
+ */
1351
+ replacesKeyId?: string;
1352
+ }
1353
+ interface RotateKeyParams {
1354
+ userId: number;
1355
+ oldKeyId: string;
1356
+ newKeyId: string;
1357
+ newPublicKey: string;
1358
+ fingerprint: string;
1359
+ algorithm?: KeyAlgorithmType;
1360
+ /** Omitted: the replaced key's label carries over, so rotation keeps its name. */
1361
+ deviceName?: string;
1362
+ platform?: KeyPlatformType;
1363
+ }
1364
+ interface RotateKeyResult {
1365
+ success: boolean;
1366
+ keyId: string;
1367
+ }
1368
+ interface RevokeKeyParams {
1369
+ userId: number;
1370
+ keyId: string;
1371
+ reason: string;
1372
+ }
1373
+ interface RevokeAllKeysParams {
1374
+ userId: number;
1375
+ /**
1376
+ * The key the request itself is signed with — spared unless includeCurrent.
1377
+ *
1378
+ * Optional because the two branches have different needs and always did: the
1379
+ * sparing branch has to know what to spare, and the `includeCurrent` branch
1380
+ * never reads it. The signed revoke-all link is the caller with no current
1381
+ * key to name — it arrives with no session at all — and requiring a value it
1382
+ * would have to invent is how a claim about a device that made no request
1383
+ * gets into a result.
1384
+ */
1385
+ currentKeyId?: string;
1386
+ /** true signs the caller out too. Default false: "my other devices". */
1387
+ includeCurrent?: boolean;
1388
+ reason: string;
1389
+ }
1390
+ interface RevokeAllKeysResult {
1391
+ revokedCount: number;
1392
+ currentKeyRevoked: boolean;
1393
+ }
1394
+ /** One registered device as the account surface shows it. */
1395
+ interface KeySummary {
1396
+ keyId: string;
1397
+ deviceName?: string;
1398
+ /** One of `KEY_PLATFORM`, which is what the routes accept and the column stores. */
1399
+ platform?: KeyPlatformType;
1400
+ algorithm: KeyAlgorithmType;
1401
+ /** First bytes of the fingerprint — enough to tell two entries apart. */
1402
+ fingerprintPrefix: string;
1403
+ /**
1404
+ * Milliseconds since the Unix epoch, not an ISO string.
1405
+ *
1406
+ * One representation of a moment across the whole surface: a generated Swift
1407
+ * or Kotlin client reads an integer with no date formatter, and
1408
+ * `ISO8601DateFormatter` rejecting fractional seconds by default stops being
1409
+ * a way for the two SDKs to disagree about the same value.
1410
+ */
1411
+ createdAtMillis: number;
1412
+ lastUsedAtMillis?: number;
1413
+ expiresAtMillis?: number;
1414
+ /** The TTL has run out. The key still reads as active; authenticate refuses it. */
1415
+ isExpired: boolean;
1416
+ /** False once revoked. Only ever false when the caller asked for revoked keys. */
1417
+ isActive: boolean;
1418
+ /** When it was revoked, for the "what did I cut off, and when" reading. */
1419
+ revokedAtMillis?: number;
1420
+ /**
1421
+ * Client address the key was registered from, absent when none was resolved
1422
+ * or the key predates the column. Registration only — it does not move when
1423
+ * the device authenticates from somewhere else, which is what makes it
1424
+ * useful for recognising a device that was never yours.
1425
+ */
1426
+ registeredIp?: string;
1427
+ /** `user-agent` of the registering request, on the same terms as above. */
1428
+ registeredUserAgent?: string;
1429
+ }
1430
+ interface ListKeysParams {
1431
+ userId: number;
1432
+ /** Also return keys already revoked. Default false: only what can still sign. */
1433
+ includeRevoked?: boolean;
1434
+ }
1435
+ /** How much of the fingerprint the list returns. */
1436
+ declare const KEY_FINGERPRINT_PREFIX_LENGTH = 8;
1437
+ /**
1438
+ * Register a new public key for a user
1439
+ *
1440
+ * `keyId` is UNIQUE across all users, so the lookup must ignore `isActive` —
1441
+ * filtering on it misses a revoked row and the insert then fails on the unique
1442
+ * index, rolling the whole login transaction back into a 500. Reuse is refused
1443
+ * with a domain error instead, telling the client to generate a fresh keyId.
1444
+ *
1445
+ * @throws KeyIdAlreadyRegisteredError keyId가 이미 쓰인 값일 때 (자기 폐기 키 재사용 · 남의 키)
1446
+ * @throws InvalidKeyFingerprintError fingerprint가 publicKey와 맞지 않을 때
1447
+ * @throws KeyAlgorithmMismatchError 키의 SPKI 타입이 선언된 algorithm과 다를 때
1448
+ */
1449
+ declare function registerPublicKeyService(params: RegisterPublicKeyParams): Promise<void>;
1450
+ /**
1451
+ * Rotate user's public key (revoke old, register new)
1452
+ *
1453
+ * @throws InvalidKeyFingerprintError fingerprint가 newPublicKey와 맞지 않을 때
1454
+ * @throws KeyAlgorithmMismatchError 새 키의 SPKI 타입이 선언된 algorithm과 다를 때
1455
+ */
1456
+ declare function rotateKeyService(params: RotateKeyParams): Promise<RotateKeyResult>;
1457
+ /**
1458
+ * Revoke a user's public key.
1459
+ *
1460
+ * Returns false when this call revoked nothing: the key belongs to somebody
1461
+ * else, or it was already revoked, or there is no such key. A caller acting on
1462
+ * a key id from outside (the device list) can therefore answer "not found"
1463
+ * instead of reporting a revocation that never happened — and the login paths
1464
+ * can tell a device replacement from a brand-new device, which is what decides
1465
+ * whether the owner is told about it. The repository scopes the update by
1466
+ * userId, so someone else's key is never touched either way.
1467
+ */
1468
+ declare function revokeKeyService(params: RevokeKeyParams): Promise<boolean>;
1469
+ /**
1470
+ * List the caller's active keys — one entry per device that can sign for them.
1471
+ *
1472
+ * `isExpired` is computed rather than stored: an expired key keeps `isActive`
1473
+ * true (nothing flips it), and `authenticate` refuses it at request time. A list
1474
+ * that showed it as simply "active" would be telling the user something the
1475
+ * server does not act on.
1476
+ *
1477
+ * The fingerprint is truncated. Its full value is what a native sign-in must
1478
+ * send as its nonce (issue #63), and an account page has no use for it beyond
1479
+ * telling two entries apart.
1480
+ */
1481
+ declare function listKeysService(params: ListKeysParams): Promise<KeySummary[]>;
1482
+ /**
1483
+ * Revoke every active key the user has, optionally sparing the current one.
1484
+ *
1485
+ * The caller's own key is spared by default, so "sign out my other devices"
1486
+ * does not also end the session making the request. Passing
1487
+ * `includeCurrent: true` is the full sign-out, which until now was reachable
1488
+ * only as a side effect of changing a password.
1489
+ *
1490
+ * Live device authorizations are refused as well, in both modes. A device
1491
+ * waiting on an approved code has no key yet, so it is never the caller's own
1492
+ * device and never the one being spared — but its next poll would register a
1493
+ * brand-new active key, which would undo the revocation seconds after it ran.
1494
+ * That is the whole point of the call: the user has decided nothing else is to
1495
+ * stay signed in.
1496
+ *
1497
+ * `revokedCount` counts keys only, since that is the number the caller's screen
1498
+ * means by "devices signed out"; a code nobody had collected was never a session.
1499
+ */
1500
+ declare function revokeAllKeysService(params: RevokeAllKeysParams): Promise<RevokeAllKeysResult>;
1501
+
1502
+ /**
1503
+ * @spfn/auth - RBAC Type Definitions
1504
+ *
1505
+ * Type definitions for role and permission configuration
1506
+ */
1507
+ /**
1508
+ * Permission category enum values
1509
+ * Single source of truth for permission categories
1510
+ */
1511
+ declare const PERMISSION_CATEGORIES: readonly ["auth", "user", "rbac", "system", "custom"];
1512
+ /**
1513
+ * Permission category type derived from the const array
1514
+ */
1515
+ type PermissionCategory = typeof PERMISSION_CATEGORIES[number];
1516
+ interface RoleConfig {
1517
+ name: string;
1518
+ displayName: string;
1519
+ description?: string;
1520
+ priority?: number;
1521
+ isSystem?: boolean;
1522
+ isBuiltin?: boolean;
1523
+ }
1524
+ interface PermissionConfig {
1525
+ name: string;
1526
+ displayName: string;
1527
+ description?: string;
1528
+ category?: PermissionCategory;
1529
+ isSystem?: boolean;
1530
+ isBuiltin?: boolean;
1531
+ }
1532
+ interface AuthInitOptions {
1533
+ /**
1534
+ * Additional roles to create
1535
+ * Built-in roles (user, admin, superadmin) are automatically included
1536
+ */
1537
+ roles?: RoleConfig[];
1538
+ /**
1539
+ * Additional permissions to create
1540
+ * Built-in permissions are automatically included
1541
+ */
1542
+ permissions?: PermissionConfig[];
1543
+ /**
1544
+ * Role-Permission mappings
1545
+ * Built-in mappings are automatically included
1546
+ * You can extend built-in roles or define mappings for custom roles
1547
+ *
1548
+ * @example
1549
+ * ```typescript
1550
+ * {
1551
+ * // Extend built-in admin role
1552
+ * admin: ['project:create', 'project:delete'],
1553
+ *
1554
+ * // Define custom role permissions
1555
+ * 'project-manager': ['project:create', 'task:assign'],
1556
+ * }
1557
+ * ```
1558
+ */
1559
+ rolePermissions?: Record<string, string[]>;
1560
+ /**
1561
+ * Default role name for new users
1562
+ * Must be a valid role name that exists after initialization
1563
+ * @default 'user'
1564
+ */
1565
+ defaultRole?: string;
1566
+ /**
1567
+ * Default session TTL (Time To Live)
1568
+ *
1569
+ * Supports:
1570
+ * - Number: seconds (e.g., 2592000)
1571
+ * - String: duration format ('30d', '12h', '45m', '3600s')
1572
+ *
1573
+ * Can be overridden at runtime with `remember` parameter.
1574
+ *
1575
+ * @default '7d' (7 days)
1576
+ *
1577
+ * @example
1578
+ * ```typescript
1579
+ * {
1580
+ * sessionTtl: '30d', // 30 days
1581
+ * }
1582
+ * ```
1583
+ */
1584
+ sessionTtl?: string | number;
1585
+ }
1586
+
1587
+ /**
1588
+ * One-Time Token Service
1589
+ *
1590
+ * Issues and verifies one-time tokens for direct API access.
1591
+ */
1592
+ interface IssueOneTimeTokenResult {
1593
+ token: string;
1594
+ expiresAt: string;
1595
+ }
1596
+ /**
1597
+ * Issue a one-time token for the authenticated user
1598
+ *
1599
+ * @param userId - Authenticated user's ID
1600
+ * @returns Token string and ISO expiration timestamp
1601
+ */
1602
+ declare function issueOneTimeTokenService(userId: string): Promise<IssueOneTimeTokenResult>;
1603
+ /**
1604
+ * Verify and consume a one-time token
1605
+ *
1606
+ * @param token - The one-time token to verify
1607
+ * @returns userId if valid, null if invalid/expired/consumed
1608
+ */
1609
+ declare function verifyOneTimeTokenService(token: string): Promise<string | null>;
1610
+
1611
+ /**
1612
+ * OAuth Provider 추상화
1613
+ *
1614
+ * Provider별로 하드코딩된 분기를 제거하기 위한 공통 인터페이스와 registry.
1615
+ * - 내장 provider(google)는 패키지 로드 시점에 자기 등록(dogfood)
1616
+ * - 외부 패키지(@superself/auth 등)는 registerOAuthProvider()로 런타임 등록
1617
+ *
1618
+ * @spfn/auth는 토큰 issuer가 아니라 소비(client) 측이므로, 이 추상화는
1619
+ * web 흐름("authorize URL 생성 → code 교환 → 사용자 정보 정규화")과
1620
+ * native 흐름(네이티브/웹 SDK가 받은 id_token 직접 검증)을 다룬다.
1621
+ */
1622
+
1623
+ /**
1624
+ * Provider 사용자 정보를 공통 형태로 정규화한 신원
1625
+ *
1626
+ * provider별 응답 형태(snake_case 등)를 service에 노출하지 않기 위한 경계.
1627
+ */
1628
+ interface NormalizedIdentity {
1629
+ providerUserId: string;
1630
+ email: string | null;
1631
+ emailVerified: boolean;
1632
+ name?: string;
1633
+ avatar?: string;
1634
+ }
1635
+ /**
1636
+ * 정규화된 OAuth 토큰 응답
1637
+ *
1638
+ * @property expiresIn - access token 만료까지 남은 초(seconds)
1639
+ */
1640
+ interface OAuthTokens {
1641
+ accessToken: string;
1642
+ refreshToken?: string;
1643
+ expiresIn: number;
1644
+ }
1645
+ /**
1646
+ * 네이티브 id_token 검증 옵션
1647
+ */
1648
+ interface NativeVerifyOptions {
1649
+ /** 클라이언트가 생성한 raw nonce. provider별 규약(raw 또는 SHA-256 해시)으로 대조된다. */
1650
+ nonce: string;
1651
+ /**
1652
+ * 같은 로그인에서 SDK가 함께 받은 provider access token (선택).
1653
+ *
1654
+ * id_token만으로는 알 수 없는 claim을 provider API로 보강하려는 provider가 쓴다
1655
+ * (카카오: id_token에 email_verified가 없어 /v2/user/me의 이메일 유효·인증 플래그를 본다).
1656
+ *
1657
+ * ⚠️ 클라이언트가 보낸 검증되지 않은 값이다. 다른 사용자의 토큰일 수 있으므로,
1658
+ * 이 값으로 조회한 신원은 반드시 id_token의 sub와 대조한 뒤에만 신뢰해야 한다.
1659
+ */
1660
+ accessToken?: string;
1661
+ }
1662
+ interface OAuthCodeExchangeOptions {
1663
+ /** Provider가 callback에 돌려준 원본 state. 일부 provider는 token 교환에도 요구한다. */
1664
+ state: string;
1665
+ }
1666
+ /**
1667
+ * Provider가 서비스로 보내는 연동 해제 알림의 원재료
1668
+ *
1669
+ * provider마다 전달 방식(query/form/JSON, 헤더 인증)이 달라 route가 정규화해 넘긴다.
1670
+ * fields는 query string과 body(form/JSON)를 병합한 문자열 맵이다.
1671
+ */
1672
+ interface UnlinkNotifyRequest {
1673
+ /** Authorization 헤더 원문 (없으면 null) */
1674
+ authorization: string | null;
1675
+ fields: Record<string, string>;
1676
+ }
1677
+ /**
1678
+ * 검증에 성공한 연동 해제 알림
1679
+ */
1680
+ interface UnlinkNotification {
1681
+ providerUserId: string;
1682
+ /** provider가 전달한 해제 경로 (kakao referrer_type 등) */
1683
+ reason?: string;
1684
+ }
1685
+ /**
1686
+ * 연동 해제 알림 검증 실패
1687
+ *
1688
+ * API 에러 응답 체계를 타지 않는다 — route가 잡아 status만 반환한다.
1689
+ * (provider 웹훅은 사람이 아닌 provider 서버가 호출자라서 에러 본문이 무의미하다)
1690
+ */
1691
+ declare class UnlinkNotifyRejection extends Error {
1692
+ readonly status: 400 | 401 | 403;
1693
+ constructor(status: 400 | 401 | 403, message: string);
1694
+ }
1695
+ /**
1696
+ * OAuth provider 구현 인터페이스
1697
+ *
1698
+ * google, superself 등 모든 provider가 이 형태를 만족해야 registry에 등록된다.
1699
+ */
1700
+ interface OAuthProvider {
1701
+ id: SocialProvider;
1702
+ /**
1703
+ * provider가 사용 가능한 상태인지(필수 env 등) 확인
1704
+ */
1705
+ isEnabled(): boolean;
1706
+ /**
1707
+ * provider 로그인 페이지로 보낼 authorization URL 생성
1708
+ *
1709
+ * @param state - CSRF 방지용 암호화 state
1710
+ * @param scopes - 요청할 scope (미지정 시 provider 기본값)
1711
+ */
1712
+ getAuthUrl(state: string, scopes?: string[]): string;
1713
+ /**
1714
+ * authorization code를 토큰으로 교환
1715
+ */
1716
+ exchangeCodeForTokens(code: string, options: OAuthCodeExchangeOptions): Promise<OAuthTokens>;
1717
+ /**
1718
+ * access token으로 사용자 정보를 조회하고 공통 형태로 정규화
1719
+ */
1720
+ getUserInfo(accessToken: string): Promise<NormalizedIdentity>;
1721
+ /**
1722
+ * refresh token으로 access token 갱신 (provider가 지원하는 경우)
1723
+ *
1724
+ * 저장된 provider 토큰을 이후 API 호출에 재사용할 때 사용한다.
1725
+ * 미구현 provider는 갱신 불가로 간주한다.
1726
+ */
1727
+ refreshTokens?(refreshToken: string): Promise<OAuthTokens>;
1728
+ /**
1729
+ * 네이티브/웹 SDK가 받은 id_token을 직접 검증하고 신원을 정규화한다.
1730
+ *
1731
+ * authorization code 교환 없이 provider JWKS로 서명을 검증하므로 client secret이
1732
+ * 필요 없다. native sign-in을 지원하는 provider만 구현한다(Apple은 web SDK 부재로
1733
+ * Android·웹도 이 경로를 쓴다).
1734
+ */
1735
+ verifyNativeIdToken?(idToken: string, options: NativeVerifyOptions): Promise<NormalizedIdentity>;
1736
+ /**
1737
+ * Provider발 연동 해제 알림(웹훅)을 검증하고 대상 사용자를 식별한다.
1738
+ *
1739
+ * 인증 없는 공개 엔드포인트로 들어오므로 provider별 규격(카카오: 어드민 키 헤더,
1740
+ * 네이버: HMAC 서명 + AES 복호화)의 검증을 통과해야만 처리된다.
1741
+ * 검증 실패는 UnlinkNotifyRejection을 던진다.
1742
+ */
1743
+ verifyUnlinkNotification?(request: UnlinkNotifyRequest): Promise<UnlinkNotification>;
1744
+ /**
1745
+ * 연동 해제 알림 처리 성공 시 응답할 HTTP status (미지정 시 200)
1746
+ *
1747
+ * 네이버는 204 No Content를 요구한다.
1748
+ */
1749
+ unlinkNotifyAckStatus?: 200 | 204;
1750
+ }
1751
+ /**
1752
+ * OAuth provider 등록 (public)
1753
+ *
1754
+ * 동일 id로 다시 등록하면 덮어쓴다(외부 패키지의 override 허용).
1755
+ */
1756
+ declare function registerOAuthProvider(provider: OAuthProvider): void;
1757
+ /**
1758
+ * 등록된 provider 조회. 미등록이면 undefined.
1759
+ */
1760
+ declare function getOAuthProvider(id: SocialProvider): OAuthProvider | undefined;
1761
+ /**
1762
+ * 등록된 모든 provider 목록
1763
+ */
1764
+ declare function getRegisteredProviders(): OAuthProvider[];
1765
+
1766
+ /**
1767
+ * @spfn/auth - OAuth Service
1768
+ *
1769
+ * OAuth 인증 비즈니스 로직
1770
+ * - Google OAuth Authorization Code Flow
1771
+ * - 소셜 계정 연결/생성
1772
+ * - publicKey는 state에서 추출하여 등록
1773
+ */
1774
+
1775
+ interface OAuthStartParams {
1776
+ provider: SocialProvider;
1777
+ returnUrl: string;
1778
+ publicKey: string;
1779
+ keyId: string;
1780
+ fingerprint: string;
1781
+ algorithm: KeyAlgorithmType;
1782
+ metadata?: Record<string, unknown>;
1783
+ /** CSRF nonce bound into the state; the route sets the matching oauth_csrf cookie. */
1784
+ nonce?: string;
1785
+ }
1786
+ interface OAuthStartResult {
1787
+ authUrl: string;
1788
+ }
1789
+ interface OAuthCallbackParams {
1790
+ provider: SocialProvider;
1791
+ code: string;
1792
+ state: string;
1793
+ /**
1794
+ * Value(s) of the oauth_csrf cookie from the callback request. One of them
1795
+ * must equal the nonce bound into the (encrypted) state — otherwise the flow
1796
+ * wasn't initiated by this browser (login CSRF). An array arises because the
1797
+ * cookie name carries the PORT suffix of the process that set it (the Next.js
1798
+ * web process), which differs from the API process in a split deployment, so
1799
+ * the callback collects every spfn_oauth_csrf* candidate. Pass `undefined` or
1800
+ * an empty array when absent; verification then fails closed.
1801
+ */
1802
+ expectedNonce: string | string[] | undefined;
1803
+ /** Client address of the callback request, from `deviceProvenance` at the route. */
1804
+ ip?: string;
1805
+ /** `user-agent` of the callback request, already truncated at the route. */
1806
+ userAgent?: string;
1807
+ }
1808
+ interface OAuthCallbackResult {
1809
+ redirectUrl: string;
1810
+ userId: string;
1811
+ keyId: string;
1812
+ isNewUser: boolean;
1813
+ }
1814
+ /**
1815
+ * registry에서 provider를 찾아 사용 가능한지 검증 후 반환
1816
+ *
1817
+ * 미등록과 비활성을 구분해 디버깅 신호를 남긴다.
1818
+ * 라우트 레이어에서도 재사용한다(중복 조회/non-null 단언 제거).
1819
+ */
1820
+ declare function requireEnabledProvider(provider: SocialProvider): OAuthProvider;
1821
+ /**
1822
+ * OAuth 로그인 시작 - Provider 로그인 페이지로 리다이렉트할 URL 생성
1823
+ *
1824
+ * Next.js에서 키쌍을 생성한 후, publicKey를 state에 포함하여 호출
1825
+ */
1826
+ declare function oauthStartService(params: OAuthStartParams): Promise<OAuthStartResult>;
1827
+ /**
1828
+ * OAuth 콜백 처리 - Code를 Token으로 교환하고 사용자 생성/연결
1829
+ *
1830
+ * state에서 publicKey를 추출하여 서버에 등록
1831
+ * Next.js는 반환된 userId, keyId로 세션을 구성
1832
+ */
1833
+ declare function oauthCallbackService(params: OAuthCallbackParams): Promise<OAuthCallbackResult>;
1834
+ /**
1835
+ * OAuth 에러 리다이렉트 URL 생성
1836
+ */
1837
+ declare function buildOAuthErrorUrl(error: string): string;
1838
+ /**
1839
+ * OAuth provider가 등록되어 있고 활성화되어 있는지 확인
1840
+ */
1841
+ declare function isOAuthProviderEnabled(provider: SocialProvider): boolean;
1842
+ /**
1843
+ * 활성화된 모든 OAuth provider 목록 (registry 기반)
1844
+ */
1845
+ declare function getEnabledOAuthProviders(): SocialProvider[];
1846
+ /**
1847
+ * Google access token 조회 (만료 시 자동 리프레시)
1848
+ *
1849
+ * 저장된 토큰이 만료 임박(5분 이내) 또는 만료 상태이면
1850
+ * refresh token으로 자동 갱신 후 DB 업데이트하여 유효한 토큰 반환.
1851
+ *
1852
+ * @param userId - 사용자 ID
1853
+ * @returns 유효한 Google access token
1854
+ */
1855
+ declare function getGoogleAccessToken(userId: number): Promise<string>;
1856
+ interface UnlinkNotifyResult {
1857
+ /** provider 규격이 요구하는 성공 응답 status */
1858
+ ackStatus: 200 | 204;
1859
+ /** 대상 소셜 계정을 찾아 삭제했는지 (미존재·이미 삭제면 false) */
1860
+ handled: boolean;
1861
+ }
1862
+ /**
1863
+ * Provider발 연동 해제 알림(unlink-notify) 처리
1864
+ *
1865
+ * 검증(verifyUnlinkNotification)을 통과한 요청만 여기 도달한다.
1866
+ * 소셜 계정 연결 row를 삭제해 저장 토큰(access/refresh)까지 함께 파기하고,
1867
+ * 후속 정책(계정 탈퇴 연계 등)은 auth.oauth.unlinked 이벤트 구독에 맡긴다.
1868
+ *
1869
+ * 대상 계정이 없어도 성공으로 응답한다 — provider 재전송·이미 해제된 계정에
1870
+ * 대한 알림은 정상 시나리오다.
1871
+ */
1872
+ declare function oauthUnlinkNotifyService(provider: SocialProvider, notification: UnlinkNotification): Promise<UnlinkNotifyResult>;
1873
+
1874
+ /**
1875
+ * @spfn/auth - Native Social Login Service
1876
+ *
1877
+ * 네이티브/웹 SDK가 받은 id_token을 JWKS로 검증하고, 검증된 신원에 클라이언트가 만든
1878
+ * 공개키를 등록한다. 토큰은 발급하지 않는다 — 클라이언트가 등록한 키로 client token을
1879
+ * 직접 서명해 Bearer로 사용한다(client-signs / server-verifies 모델).
1880
+ *
1881
+ * 흐름은 두 단계로 분리한다:
1882
+ * 1) id_token 검증 — 외부 JWKS 네트워크 조회. DB 트랜잭션 밖에서 수행한다.
1883
+ * 2) persist — 사용자 link/create + 공개키 등록을 한 트랜잭션으로. 이벤트는 커밋 후 발행.
1884
+ */
1885
+
1886
+ interface OAuthNativeParams {
1887
+ provider: SocialProvider;
1888
+ idToken: string;
1889
+ nonce: string;
1890
+ publicKey: string;
1891
+ keyId: string;
1892
+ fingerprint: string;
1893
+ algorithm: KeyAlgorithmType;
1894
+ /** 키 목록에 보일 기기 라벨 (선택). 표시용이라 권한 판정에 쓰이지 않는다. */
1895
+ deviceName?: string;
1896
+ platform?: KeyPlatformType;
1897
+ /**
1898
+ * SDK가 id_token과 함께 받은 provider access token (선택).
1899
+ *
1900
+ * provider가 id_token만으로 확인할 수 없는 claim을 보강할 때만 쓴다. 없으면 provider는
1901
+ * id_token이 담은 정보만으로 신원을 정규화한다.
1902
+ */
1903
+ accessToken?: string;
1904
+ /** Client address of the request, from `deviceProvenance` at the route. */
1905
+ ip?: string;
1906
+ /** `user-agent` of the request, already truncated at the route. */
1907
+ userAgent?: string;
1908
+ /** Apple은 첫 로그인에만 이름을 별도로 주므로 클라이언트가 전달할 수 있다. */
1909
+ profile?: {
1910
+ name?: string;
1911
+ };
1912
+ metadata?: Record<string, unknown>;
1913
+ }
1914
+ interface OAuthNativeResult {
1915
+ userId: string;
1916
+ keyId: string;
1917
+ isNewUser: boolean;
1918
+ }
1919
+ /**
1920
+ * native id_token 로그인 처리
1921
+ *
1922
+ * @throws NativeSignInUnsupportedError provider가 native sign-in을 지원하지 않을 때
1923
+ * @throws InvalidSocialTokenError id_token 검증 실패 시
1924
+ */
1925
+ declare function oauthNativeService(params: OAuthNativeParams): Promise<OAuthNativeResult>;
1926
+
1927
+ /**
1928
+ * OAuth 2.1 Authorize Service
1929
+ *
1930
+ * The API half of the consent screen. The screen itself is a page on the web
1931
+ * app, because that is where the session cookie is; it has no database, so it
1932
+ * asks here what to draw (`describeOAuth2AuthorizeRequest`) and tells here what
1933
+ * the user decided (`approveOAuth2Authorize` / `denyOAuth2Authorize`). Both
1934
+ * calls validate the request from scratch — the second must never trust what the
1935
+ * first was shown, since a form can be edited between them.
1936
+ *
1937
+ * Refusals come in two kinds and the split is the security property, not a
1938
+ * presentation choice:
1939
+ *
1940
+ * - **Not redirectable.** An unknown `client_id`, or a `redirect_uri` the client
1941
+ * never registered. There is no vetted URI to send the error to, and sending
1942
+ * it to the one the request supplied is precisely the open redirect the
1943
+ * registration check exists to prevent. These are shown on the screen.
1944
+ * - **Redirectable.** Everything else — a missing PKCE challenge, a missing
1945
+ * `resource`, an unknown scope, and the user saying no. The client and its
1946
+ * URI are both vetted by then, so RFC 6749 §4.1.2.1 puts the error back on
1947
+ * that URI as query parameters, which is the only form the waiting CLI can
1948
+ * read.
1949
+ */
1950
+ /** An authorize request as the web handler forwards it, before anything is trusted. */
1951
+ interface OAuth2AuthorizeParams {
1952
+ clientId: string;
1953
+ redirectUri: string;
1954
+ codeChallenge?: string;
1955
+ codeChallengeMethod?: string;
1956
+ resource?: string;
1957
+ scope?: string;
1958
+ state?: string;
1959
+ }
1960
+ /** One scope, with the sentence the consent screen shows for it. */
1961
+ interface OAuth2ScopeDescription {
1962
+ name: string;
1963
+ description: string;
1964
+ }
1965
+ /** Everything the consent screen needs, and nothing it does not. */
1966
+ interface OAuth2ConsentView {
1967
+ clientName: string;
1968
+ /** Host the code would be sent to — the one fact about the client that is checkable. */
1969
+ redirectHost: string;
1970
+ scopes: OAuth2ScopeDescription[];
1971
+ resource: string;
1972
+ }
1973
+ /** What the web handler turns into the success redirect. */
1974
+ interface OAuth2AuthorizationCodeIssued {
1975
+ code: string;
1976
+ /** The presented URI, which matched a registered one. Safe to redirect to. */
1977
+ redirectUri: string;
1978
+ /** Echoed back verbatim, or absent when the request carried none. */
1979
+ state?: string;
1980
+ }
1981
+ /**
1982
+ * What to draw on the consent screen for this request.
1983
+ *
1984
+ * Read-only: nothing is recorded by looking, so a user who closes the tab has
1985
+ * consented to nothing and left nothing behind.
1986
+ */
1987
+ declare function describeOAuth2AuthorizeRequestService(params: OAuth2AuthorizeParams): Promise<OAuth2ConsentView>;
1988
+ /**
1989
+ * Record the consent and mint the code.
1990
+ *
1991
+ * The whole request is validated again rather than carried over from the GET:
1992
+ * the form between the two is in the user's browser, and a parameter changed
1993
+ * there must be caught here and not honoured because the screen once looked
1994
+ * right.
1995
+ *
1996
+ * `userId` comes from the approving session. Never from a request body — that
1997
+ * would be the entire authorization.
1998
+ */
1999
+ declare function approveOAuth2AuthorizeService(params: OAuth2AuthorizeParams, userId: number): Promise<OAuth2AuthorizationCodeIssued>;
2000
+ /**
2001
+ * The user said no.
2002
+ *
2003
+ * Validated first, and validated in full — the same `validate` the approval
2004
+ * runs. `access_denied` goes back to the client on its redirect URI like any
2005
+ * other redirectable error, so the URI has to be one the client registered
2006
+ * before anybody is sent to it; and a request that was malformed was malformed
2007
+ * whichever button was pressed, so answering `access_denied` to it would tell
2008
+ * the waiting client the user refused when in fact it never asked properly.
2009
+ * Nothing is recorded — a refusal is not a grant with a flag on it.
2010
+ */
2011
+ declare function denyOAuth2AuthorizeService(params: OAuth2AuthorizeParams): Promise<never>;
2012
+
2013
+ /**
2014
+ * OAuth 2.1 Grant Service
2015
+ *
2016
+ * The user's side of the authorization server: what is connected, and the button
2017
+ * that disconnects it. A grant is the unit because it is the thing a person can
2018
+ * recognise — "Claude Code, on this API, with these permissions" — and because
2019
+ * revoking it takes every code and token underneath with it.
2020
+ *
2021
+ * `revokeAllOAuth2GrantsForUser` is the same act performed on the user's behalf
2022
+ * rather than by them, and it sits at the four places that revoke everything:
2023
+ * revoke-all, a password change, a completed password reset, a deletion request.
2024
+ * A global revocation that left a grant alive would leave a refresh token alive,
2025
+ * and a CLI holding one would be signed in again within the hour — which is
2026
+ * exactly the device the user was cutting off.
2027
+ */
2028
+ /** One connected client, as the account settings screen lists it. */
2029
+ interface OAuth2GrantSummary {
2030
+ id: number;
2031
+ clientId: string;
2032
+ clientName: string;
2033
+ resource: string;
2034
+ scopes: string[];
2035
+ createdAtMillis: number;
2036
+ lastUsedAtMillis?: number;
2037
+ }
2038
+ /** What a user has connected. Revoked grants are not listed — they are gone. */
2039
+ declare function listOAuth2GrantsService(userId: number): Promise<OAuth2GrantSummary[]>;
2040
+ /**
2041
+ * Disconnect one client.
2042
+ *
2043
+ * The user id is part of the statement's condition, not a check before it: the
2044
+ * id comes from a URL, and a grant belonging to somebody else must answer as if
2045
+ * it did not exist rather than as if it were merely not theirs.
2046
+ */
2047
+ declare function revokeOAuth2GrantService(id: number, userId: number): Promise<void>;
2048
+ /**
2049
+ * Revoke every grant a user has — the authorization-server half of a global
2050
+ * revocation, called beside `deviceAuthorizationsRepository.denyAllActiveByUserId`.
2051
+ *
2052
+ * Tokens are revoked as well as the grants. Verification already refuses a token
2053
+ * whose grant is dead, so this changes no decision; it means a `SELECT` against
2054
+ * `oauth2_tokens` after a revoke-all does not show live-looking rows, which is
2055
+ * the sort of thing that gets read as a hole.
2056
+ */
2057
+ declare function revokeAllOAuth2GrantsForUser(userId: number): Promise<void>;
2058
+
2059
+ /**
2060
+ * @spfn/auth - Main Router
2061
+ *
2062
+ * Combines all auth-related routes into a single router
2063
+ */
2064
+ /**
2065
+ * Main auth router
2066
+ * Exports all authentication-related routes
2067
+ *
2068
+ * Routes:
2069
+ * - Auth: /_auth/codes, /_auth/login, /_auth/logout, etc.
2070
+ * - OAuth: /_auth/oauth/google, /_auth/oauth/google/callback, etc.
2071
+ * - Invitations: /_auth/invitations/*
2072
+ * - Users: /_auth/users/*
2073
+ * - Deletion: /_auth/deletion/request, /_auth/deletion/cancel
2074
+ * - Admin: /_auth/admin/* (superadmin only)
2075
+ * - OAuth 2.1 authorization server: /_auth/oauth2/*, /.well-known/oauth-authorization-server
2076
+ */
2077
+ declare const mainAuthRouter: _spfn_core_route.Router<{
2078
+ sendVerificationCode: _spfn_core_route.RouteDef<{
2079
+ body: _sinclair_typebox.TObject<{
2080
+ target: _sinclair_typebox.TString;
2081
+ targetType: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
2082
+ purpose: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"registration">, _sinclair_typebox.TLiteral<"login">, _sinclair_typebox.TLiteral<"password_reset">, _sinclair_typebox.TLiteral<"email_change">, _sinclair_typebox.TLiteral<"phone_change">, _sinclair_typebox.TLiteral<"account_deletion">]>;
2083
+ }>;
2084
+ }, {}, SendVerificationCodeResult>;
2085
+ verifyCode: _spfn_core_route.RouteDef<{
2086
+ body: _sinclair_typebox.TObject<{
2087
+ target: _sinclair_typebox.TString;
2088
+ targetType: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
2089
+ code: _sinclair_typebox.TString;
2090
+ purpose: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"registration">, _sinclair_typebox.TLiteral<"login">, _sinclair_typebox.TLiteral<"password_reset">, _sinclair_typebox.TLiteral<"email_change">, _sinclair_typebox.TLiteral<"phone_change">, _sinclair_typebox.TLiteral<"account_deletion">]>;
2091
+ }>;
2092
+ }, {}, {
2093
+ valid: boolean;
2094
+ verificationToken: string;
2095
+ }>;
2096
+ register: _spfn_core_route.RouteDef<{
2097
+ body: _sinclair_typebox.TObject<{
2098
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2099
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2100
+ verificationToken: _sinclair_typebox.TString;
2101
+ password: _sinclair_typebox.TString;
2102
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2103
+ }>;
2104
+ }, {
2105
+ body: _sinclair_typebox.TObject<{
2106
+ publicKey: _sinclair_typebox.TString;
2107
+ keyId: _sinclair_typebox.TString;
2108
+ fingerprint: _sinclair_typebox.TString;
2109
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2110
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2111
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2112
+ }>;
2113
+ }, RegisterResult>;
2114
+ requestSignupLink: _spfn_core_route.RouteDef<{
2115
+ body: _sinclair_typebox.TObject<{
2116
+ email: _sinclair_typebox.TString;
2117
+ returnPath: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2118
+ }>;
2119
+ }, {}, RequestSignupLinkResult>;
2120
+ confirmSignupLink: _spfn_core_route.RouteDef<{
2121
+ body: _sinclair_typebox.TObject<{
2122
+ token: _sinclair_typebox.TString;
2123
+ }>;
2124
+ }, {}, ConfirmSignupLinkResult>;
2125
+ completeSignup: _spfn_core_route.RouteDef<{
2126
+ body: _sinclair_typebox.TObject<{
2127
+ password: _sinclair_typebox.TString;
2128
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2129
+ }>;
2130
+ }, {
2131
+ body: _sinclair_typebox.TObject<{
2132
+ setupSecret: _sinclair_typebox.TString;
2133
+ publicKey: _sinclair_typebox.TString;
2134
+ keyId: _sinclair_typebox.TString;
2135
+ fingerprint: _sinclair_typebox.TString;
2136
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2137
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2138
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2139
+ }>;
2140
+ }, RegisterResult>;
2141
+ requestPasswordReset: _spfn_core_route.RouteDef<{
2142
+ body: _sinclair_typebox.TObject<{
2143
+ email: _sinclair_typebox.TString;
2144
+ returnPath: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2145
+ }>;
2146
+ }, {}, RequestPasswordResetResult>;
2147
+ confirmPasswordReset: _spfn_core_route.RouteDef<{
2148
+ body: _sinclair_typebox.TObject<{
2149
+ token: _sinclair_typebox.TString;
2150
+ }>;
2151
+ }, {}, ConfirmPasswordResetResult>;
2152
+ completePasswordReset: _spfn_core_route.RouteDef<{
2153
+ body: _sinclair_typebox.TObject<{
2154
+ password: _sinclair_typebox.TString;
2155
+ }>;
2156
+ }, {
2157
+ body: _sinclair_typebox.TObject<{
2158
+ setupSecret: _sinclair_typebox.TString;
2159
+ publicKey: _sinclair_typebox.TString;
2160
+ keyId: _sinclair_typebox.TString;
2161
+ fingerprint: _sinclair_typebox.TString;
2162
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2163
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2164
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2165
+ }>;
2166
+ }, RegisterResult>;
2167
+ login: _spfn_core_route.RouteDef<{
2168
+ body: _sinclair_typebox.TObject<{
2169
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2170
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2171
+ password: _sinclair_typebox.TString;
2172
+ }>;
2173
+ }, {
2174
+ body: _sinclair_typebox.TObject<{
2175
+ publicKey: _sinclair_typebox.TString;
2176
+ keyId: _sinclair_typebox.TString;
2177
+ fingerprint: _sinclair_typebox.TString;
2178
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2179
+ oldKeyId: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2180
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2181
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2182
+ }>;
2183
+ }, LoginResult>;
2184
+ startDeviceAuth: _spfn_core_route.RouteDef<{
2185
+ body: _sinclair_typebox.TObject<{
2186
+ publicKey: _sinclair_typebox.TString;
2187
+ keyId: _sinclair_typebox.TString;
2188
+ fingerprint: _sinclair_typebox.TString;
2189
+ algorithm: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>>;
2190
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2191
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2192
+ }>;
2193
+ }, {}, StartDeviceAuthResult>;
2194
+ pollDeviceAuth: _spfn_core_route.RouteDef<{
2195
+ body: _sinclair_typebox.TObject<{
2196
+ deviceCode: _sinclair_typebox.TString;
2197
+ }>;
2198
+ }, {}, {
2199
+ status: "pending";
2200
+ intervalMillis: number;
2201
+ } | {
2202
+ email?: string | undefined;
2203
+ phone?: string | undefined;
2204
+ status: "approved";
2205
+ userId: string;
2206
+ publicId: string;
2207
+ passwordChangeRequired: boolean;
2208
+ }>;
2209
+ getDeviceAuthInfo: _spfn_core_route.RouteDef<{
2210
+ body: _sinclair_typebox.TObject<{
2211
+ userCode: _sinclair_typebox.TString;
2212
+ }>;
2213
+ }, {}, DeviceAuthInfoResult>;
2214
+ approveDeviceAuth: _spfn_core_route.RouteDef<{
2215
+ body: _sinclair_typebox.TObject<{
2216
+ userCode: _sinclair_typebox.TString;
2217
+ }>;
2218
+ }, {}, DeviceAuthInfoResult>;
2219
+ denyDeviceAuth: _spfn_core_route.RouteDef<{
2220
+ body: _sinclair_typebox.TObject<{
2221
+ userCode: _sinclair_typebox.TString;
2222
+ }>;
2223
+ }, {}, void>;
2224
+ passkeyRegisterOptions: _spfn_core_route.RouteDef<{
2225
+ body: _sinclair_typebox.TObject<{
2226
+ currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2227
+ }>;
2228
+ }, {}, _simplewebauthn_server.PublicKeyCredentialCreationOptionsJSON>;
2229
+ passkeyRegisterVerify: _spfn_core_route.RouteDef<{
2230
+ body: _sinclair_typebox.TObject<{
2231
+ response: _sinclair_typebox.TUnknown;
2232
+ label: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2233
+ }>;
2234
+ }, {}, FinishPasskeyEnrollmentResult>;
2235
+ passkeyLoginOptions: _spfn_core_route.RouteDef<{
2236
+ body: _sinclair_typebox.TObject<{}>;
2237
+ }, {}, _simplewebauthn_server.PublicKeyCredentialRequestOptionsJSON>;
2238
+ passkeyLoginVerify: _spfn_core_route.RouteDef<{
2239
+ body: _sinclair_typebox.TObject<{
2240
+ response: _sinclair_typebox.TUnknown;
2241
+ }>;
2242
+ }, {
2243
+ body: _sinclair_typebox.TObject<{
2244
+ publicKey: _sinclair_typebox.TString;
2245
+ keyId: _sinclair_typebox.TString;
2246
+ fingerprint: _sinclair_typebox.TString;
2247
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2248
+ oldKeyId: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2249
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2250
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2251
+ }>;
2252
+ }, LoginResult>;
2253
+ listPasskeys: _spfn_core_route.RouteDef<{
2254
+ body: _sinclair_typebox.TObject<{}>;
2255
+ }, {}, {
2256
+ passkeys: PasskeySummary[];
2257
+ }>;
2258
+ renamePasskey: _spfn_core_route.RouteDef<{
2259
+ body: _sinclair_typebox.TObject<{
2260
+ passkeyId: _sinclair_typebox.TString;
2261
+ label: _sinclair_typebox.TString;
2262
+ }>;
2263
+ }, {}, {
2264
+ passkeyId: string;
2265
+ label: string;
2266
+ }>;
2267
+ revokePasskey: _spfn_core_route.RouteDef<{
2268
+ body: _sinclair_typebox.TObject<{
2269
+ passkeyId: _sinclair_typebox.TString;
2270
+ currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2271
+ }>;
2272
+ }, {}, {
2273
+ passkeyId: string;
2274
+ }>;
2275
+ logout: _spfn_core_route.RouteDef<{}, {}, void>;
2276
+ rotateKey: _spfn_core_route.RouteDef<{}, {
2277
+ body: _sinclair_typebox.TObject<{
2278
+ publicKey: _sinclair_typebox.TString;
2279
+ keyId: _sinclair_typebox.TString;
2280
+ fingerprint: _sinclair_typebox.TString;
2281
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2282
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2283
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2284
+ }>;
2285
+ }, RotateKeyResult>;
2286
+ listKeys: _spfn_core_route.RouteDef<{
2287
+ body: _sinclair_typebox.TObject<{
2288
+ includeRevoked: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
2289
+ }>;
2290
+ }, {}, {
2291
+ keys: KeySummary[];
2292
+ }>;
2293
+ revokeKey: _spfn_core_route.RouteDef<{
2294
+ body: _sinclair_typebox.TObject<{
2295
+ keyId: _sinclair_typebox.TString;
2296
+ }>;
2297
+ }, {}, {
2298
+ keyId: string;
2299
+ selfRevoked: boolean;
2300
+ }>;
2301
+ revokeAllKeys: _spfn_core_route.RouteDef<{
2302
+ body: _sinclair_typebox.TObject<{
2303
+ includeCurrent: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
2304
+ }>;
2305
+ }, {}, RevokeAllKeysResult>;
2306
+ confirmRevokeAllLink: _spfn_core_route.RouteDef<{
2307
+ body: _sinclair_typebox.TObject<{
2308
+ token: _sinclair_typebox.TString;
2309
+ }>;
2310
+ }, {}, {
2311
+ expiresAt: string;
2312
+ activeKeyCount: number;
2313
+ }>;
2314
+ consumeRevokeAllLink: _spfn_core_route.RouteDef<{
2315
+ body: _sinclair_typebox.TObject<{
2316
+ token: _sinclair_typebox.TString;
2317
+ }>;
2318
+ }, {}, {
2319
+ revokedCount: number;
2320
+ }>;
2321
+ changePassword: _spfn_core_route.RouteDef<{
2322
+ body: _sinclair_typebox.TObject<{
2323
+ currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2324
+ newPassword: _sinclair_typebox.TString;
2325
+ }>;
2326
+ }, {}, void>;
2327
+ getAuthSession: _spfn_core_route.RouteDef<{}, {}, {
2328
+ role: {
2329
+ id: number;
2330
+ name: string;
2331
+ displayName: string;
2332
+ priority: number;
2333
+ };
2334
+ permissions: {
2335
+ id: number;
2336
+ name: string;
2337
+ displayName: string;
2338
+ category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
2339
+ }[];
2340
+ userId: number;
2341
+ publicId: string;
2342
+ email: string | null;
2343
+ emailVerified: boolean;
2344
+ phoneVerified: boolean;
2345
+ hasPassword: boolean;
2346
+ }>;
2347
+ issueOneTimeToken: _spfn_core_route.RouteDef<{}, {}, IssueOneTimeTokenResult>;
2348
+ requestAccountDeletion: _spfn_core_route.RouteDef<{
2349
+ body: _sinclair_typebox.TObject<{
2350
+ password: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2351
+ verificationToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2352
+ reason: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2353
+ immediate: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
2354
+ }>;
2355
+ }, {}, {
2356
+ purgeScheduledAt: string;
2357
+ }>;
2358
+ cancelAccountDeletion: _spfn_core_route.RouteDef<{
2359
+ body: _sinclair_typebox.TObject<{
2360
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2361
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2362
+ password: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2363
+ verificationToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2364
+ }>;
2365
+ }, {}, void>;
2366
+ oauthGoogleStart: _spfn_core_route.RouteDef<{
2367
+ query: _sinclair_typebox.TObject<{
2368
+ state: _sinclair_typebox.TString;
2369
+ }>;
2370
+ }, {}, Response>;
2371
+ oauthGoogleCallback: _spfn_core_route.RouteDef<{
2372
+ query: _sinclair_typebox.TObject<{
2373
+ code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2374
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2375
+ error: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2376
+ error_description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2377
+ }>;
2378
+ }, {}, Response>;
2379
+ oauthStart: _spfn_core_route.RouteDef<{
2380
+ body: _sinclair_typebox.TObject<{
2381
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2382
+ returnUrl: _sinclair_typebox.TString;
2383
+ publicKey: _sinclair_typebox.TString;
2384
+ keyId: _sinclair_typebox.TString;
2385
+ fingerprint: _sinclair_typebox.TString;
2386
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2387
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2388
+ }>;
2389
+ }, {}, OAuthStartResult>;
2390
+ oauthProviders: _spfn_core_route.RouteDef<{}, {}, {
2391
+ providers: ("google" | "apple" | "github" | "kakao" | "naver" | "superself")[];
2392
+ }>;
2393
+ getGoogleOAuthUrl: _spfn_core_route.RouteDef<{
2394
+ body: _sinclair_typebox.TObject<{
2395
+ returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2396
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2397
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2398
+ }>;
2399
+ }, {}, {
2400
+ authUrl: string;
2401
+ }>;
2402
+ oauthFinalize: _spfn_core_route.RouteDef<{
2403
+ body: _sinclair_typebox.TObject<{
2404
+ userId: _sinclair_typebox.TString;
2405
+ keyId: _sinclair_typebox.TString;
2406
+ returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2407
+ }>;
2408
+ }, {}, {
2409
+ success: boolean;
2410
+ userId: string;
2411
+ keyId: string;
2412
+ returnUrl: string;
2413
+ }>;
2414
+ oauthProviderStart: _spfn_core_route.RouteDef<{
2415
+ params: _sinclair_typebox.TObject<{
2416
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2417
+ }>;
2418
+ query: _sinclair_typebox.TObject<{
2419
+ state: _sinclair_typebox.TString;
2420
+ }>;
2421
+ }, {}, Response>;
2422
+ oauthProviderCallback: _spfn_core_route.RouteDef<{
2423
+ params: _sinclair_typebox.TObject<{
2424
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2425
+ }>;
2426
+ query: _sinclair_typebox.TObject<{
2427
+ code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2428
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2429
+ error: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2430
+ error_description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2431
+ }>;
2432
+ }, {}, Response>;
2433
+ getProviderOAuthUrl: _spfn_core_route.RouteDef<{
2434
+ params: _sinclair_typebox.TObject<{
2435
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2436
+ }>;
2437
+ body: _sinclair_typebox.TObject<{
2438
+ returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2439
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2440
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2441
+ }>;
2442
+ }, {}, {
2443
+ authUrl: string;
2444
+ }>;
2445
+ oauthNative: _spfn_core_route.RouteDef<{
2446
+ params: _sinclair_typebox.TObject<{
2447
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2448
+ }>;
2449
+ body: _sinclair_typebox.TObject<{
2450
+ idToken: _sinclair_typebox.TString;
2451
+ nonce: _sinclair_typebox.TString;
2452
+ accessToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2453
+ publicKey: _sinclair_typebox.TString;
2454
+ keyId: _sinclair_typebox.TString;
2455
+ fingerprint: _sinclair_typebox.TString;
2456
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2457
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2458
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2459
+ profile: _sinclair_typebox.TOptional<_sinclair_typebox.TObject<{
2460
+ name: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2461
+ }>>;
2462
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2463
+ }>;
2464
+ }, {}, OAuthNativeResult>;
2465
+ oauthUnlinkNotify: _spfn_core_route.RouteDef<{
2466
+ params: _sinclair_typebox.TObject<{
2467
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2468
+ }>;
2469
+ }, {}, void | Response>;
2470
+ oauthUnlinkNotifyGet: _spfn_core_route.RouteDef<{
2471
+ params: _sinclair_typebox.TObject<{
2472
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2473
+ }>;
2474
+ }, {}, void | Response>;
2475
+ getInvitation: _spfn_core_route.RouteDef<{
2476
+ params: _sinclair_typebox.TObject<{
2477
+ token: _sinclair_typebox.TString;
2478
+ }>;
2479
+ }, {}, {
2480
+ email: string;
2481
+ role: string;
2482
+ roleDisplayName: string;
2483
+ invitedBy: string;
2484
+ expiresAt: string;
2485
+ metadata: Record<string, any> | undefined;
2486
+ }>;
2487
+ acceptInvitation: _spfn_core_route.RouteDef<{
2488
+ body: _sinclair_typebox.TObject<{
2489
+ token: _sinclair_typebox.TString;
2490
+ password: _sinclair_typebox.TString;
2491
+ }>;
2492
+ }, {
2493
+ body: _sinclair_typebox.TObject<{
2494
+ publicKey: _sinclair_typebox.TString;
2495
+ keyId: _sinclair_typebox.TString;
2496
+ fingerprint: _sinclair_typebox.TString;
2497
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2498
+ }>;
2499
+ }, {
2500
+ userId: number;
2501
+ email: string;
2502
+ role: string;
2503
+ }>;
2504
+ createInvitation: _spfn_core_route.RouteDef<{
2505
+ body: _sinclair_typebox.TObject<{
2506
+ email: _sinclair_typebox.TString;
2507
+ roleId: _sinclair_typebox.TNumber;
2508
+ expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2509
+ expiresAt: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2510
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TAny>;
2511
+ }>;
2512
+ }, {}, {
2513
+ id: number;
2514
+ email: string;
2515
+ token: string;
2516
+ roleId: number;
2517
+ expiresAt: string;
2518
+ invitationUrl: string;
2519
+ }>;
2520
+ listInvitations: _spfn_core_route.RouteDef<{
2521
+ query: _sinclair_typebox.TObject<{
2522
+ status: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"pending" | "accepted" | "expired" | "cancelled">[]>>;
2523
+ page: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2524
+ limit: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2525
+ }>;
2526
+ }, {}, {
2527
+ invitations: {
2528
+ id: number;
2529
+ email: string;
2530
+ token: string;
2531
+ roleId: number;
2532
+ invitedBy: number;
2533
+ status: "pending" | "accepted" | "expired" | "cancelled";
2534
+ expiresAt: Date;
2535
+ acceptedAt: Date | null;
2536
+ cancelledAt: Date | null;
2537
+ metadata: Record<string, any> | null;
2538
+ createdAt: Date;
2539
+ updatedAt: Date;
2540
+ role: {
2541
+ id: number;
2542
+ name: string;
2543
+ displayName: string;
2544
+ };
2545
+ inviter: {
2546
+ id: number;
2547
+ email: string | null;
2548
+ };
2549
+ }[];
2550
+ total: number;
2551
+ page: number;
2552
+ limit: number;
2553
+ totalPages: number;
2554
+ }>;
2555
+ cancelInvitation: _spfn_core_route.RouteDef<{
2556
+ body: _sinclair_typebox.TObject<{
2557
+ id: _sinclair_typebox.TNumber;
2558
+ reason: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2559
+ }>;
2560
+ }, {}, {
2561
+ cancelledAt: string;
2562
+ }>;
2563
+ resendInvitation: _spfn_core_route.RouteDef<{
2564
+ body: _sinclair_typebox.TObject<{
2565
+ id: _sinclair_typebox.TNumber;
2566
+ expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2567
+ }>;
2568
+ }, {}, {
2569
+ expiresAt: string;
2570
+ }>;
2571
+ deleteInvitation: _spfn_core_route.RouteDef<{
2572
+ body: _sinclair_typebox.TObject<{
2573
+ id: _sinclair_typebox.TNumber;
2574
+ }>;
2575
+ }, {}, void>;
2576
+ getUserProfile: _spfn_core_route.RouteDef<{}, {}, UserProfile>;
2577
+ updateUserProfile: _spfn_core_route.RouteDef<{
2578
+ body: _sinclair_typebox.TObject<{
2579
+ displayName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2580
+ firstName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2581
+ lastName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2582
+ avatarUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2583
+ bio: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2584
+ locale: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2585
+ timezone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2586
+ dateOfBirth: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2587
+ gender: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2588
+ website: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2589
+ location: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2590
+ company: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2591
+ jobTitle: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2592
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TAny>>;
2593
+ }>;
2594
+ }, {}, ProfileInfo>;
2595
+ checkUsername: _spfn_core_route.RouteDef<{
2596
+ query: _sinclair_typebox.TObject<{
2597
+ username: _sinclair_typebox.TString;
2598
+ }>;
2599
+ }, {}, {
2600
+ available: boolean;
2601
+ }>;
2602
+ updateUsername: _spfn_core_route.RouteDef<{
2603
+ body: _sinclair_typebox.TObject<{
2604
+ username: _sinclair_typebox.TUnion<[_sinclair_typebox.TString, _sinclair_typebox.TNull]>;
2605
+ }>;
2606
+ }, {}, {
2607
+ deletedAt: Date | null;
2608
+ deletedBy: string | null;
2609
+ createdAt: Date;
2610
+ updatedAt: Date;
2611
+ id: number;
2612
+ publicId: string;
2613
+ email: string | null;
2614
+ phone: string | null;
2615
+ username: string | null;
2616
+ passwordHash: string | null;
2617
+ passwordChangeRequired: boolean;
2618
+ roleId: number;
2619
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
2620
+ emailVerifiedAt: Date | null;
2621
+ phoneVerifiedAt: Date | null;
2622
+ keyEpoch: number;
2623
+ lastLoginAt: Date | null;
2624
+ }>;
2625
+ updateLocale: _spfn_core_route.RouteDef<{
2626
+ body: _sinclair_typebox.TObject<{
2627
+ locale: _sinclair_typebox.TString;
2628
+ }>;
2629
+ }, {}, {
2630
+ locale: string;
2631
+ }>;
2632
+ listRoles: _spfn_core_route.RouteDef<{
2633
+ query: _sinclair_typebox.TObject<{
2634
+ includeInactive: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
2635
+ }>;
2636
+ }, {}, {
2637
+ roles: {
2638
+ description: string | null;
2639
+ name: string;
2640
+ id: number;
2641
+ displayName: string;
2642
+ isBuiltin: boolean;
2643
+ isSystem: boolean;
2644
+ isActive: boolean;
2645
+ priority: number;
2646
+ createdAt: Date;
2647
+ updatedAt: Date;
2648
+ }[];
2649
+ }>;
2650
+ createAdminRole: _spfn_core_route.RouteDef<{
2651
+ body: _sinclair_typebox.TObject<{
2652
+ name: _sinclair_typebox.TString;
2653
+ displayName: _sinclair_typebox.TString;
2654
+ description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2655
+ priority: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2656
+ permissionIds: _sinclair_typebox.TOptional<_sinclair_typebox.TArray<_sinclair_typebox.TNumber>>;
2657
+ }>;
2658
+ }, {}, {
2659
+ role: {
2660
+ description: string | null;
2661
+ name: string;
2662
+ id: number;
2663
+ displayName: string;
2664
+ isBuiltin: boolean;
2665
+ isSystem: boolean;
2666
+ isActive: boolean;
2667
+ priority: number;
2668
+ createdAt: Date;
2669
+ updatedAt: Date;
2670
+ };
2671
+ }>;
2672
+ updateAdminRole: _spfn_core_route.RouteDef<{
2673
+ params: _sinclair_typebox.TObject<{
2674
+ id: _sinclair_typebox.TNumber;
2675
+ }>;
2676
+ body: _sinclair_typebox.TObject<{
2677
+ displayName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2678
+ description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2679
+ priority: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2680
+ isActive: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
2681
+ }>;
2682
+ }, {}, {
2683
+ role: {
2684
+ description: string | null;
2685
+ name: string;
2686
+ id: number;
2687
+ displayName: string;
2688
+ isBuiltin: boolean;
2689
+ isSystem: boolean;
2690
+ isActive: boolean;
2691
+ priority: number;
2692
+ createdAt: Date;
2693
+ updatedAt: Date;
2694
+ };
2695
+ }>;
2696
+ deleteAdminRole: _spfn_core_route.RouteDef<{
2697
+ params: _sinclair_typebox.TObject<{
2698
+ id: _sinclair_typebox.TNumber;
2699
+ }>;
2700
+ }, {}, void>;
2701
+ updateUserRole: _spfn_core_route.RouteDef<{
2702
+ params: _sinclair_typebox.TObject<{
2703
+ userId: _sinclair_typebox.TNumber;
2704
+ }>;
2705
+ body: _sinclair_typebox.TObject<{
2706
+ roleId: _sinclair_typebox.TNumber;
2707
+ }>;
2708
+ }, {}, {
2709
+ userId: number;
2710
+ roleId: number;
2711
+ }>;
2712
+ issueOpsToken: _spfn_core_route.RouteDef<{
2713
+ body: _sinclair_typebox.TObject<{
2714
+ name: _sinclair_typebox.TString;
2715
+ scopes: _sinclair_typebox.TArray<_sinclair_typebox.TString>;
2716
+ expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<[_sinclair_typebox.TNumber, _sinclair_typebox.TNull]>>;
2717
+ }>;
2718
+ }, {}, {
2719
+ token: string;
2720
+ opsToken: {
2721
+ id: number;
2722
+ name: string;
2723
+ scopes: string[];
2724
+ expiresAt: string | null;
2725
+ revokedAt: string | null;
2726
+ lastUsedAt: string | null;
2727
+ createdAt: string | null;
2728
+ };
2729
+ }>;
2730
+ listOpsTokens: _spfn_core_route.RouteDef<{}, {}, {
2731
+ opsTokens: {
2732
+ id: number;
2733
+ name: string;
2734
+ scopes: string[];
2735
+ expiresAt: string | null;
2736
+ revokedAt: string | null;
2737
+ lastUsedAt: string | null;
2738
+ createdAt: string | null;
2739
+ }[];
2740
+ }>;
2741
+ revokeOpsToken: _spfn_core_route.RouteDef<{
2742
+ params: _sinclair_typebox.TObject<{
2743
+ id: _sinclair_typebox.TNumber;
2744
+ }>;
2745
+ }, {}, {
2746
+ opsToken: {
2747
+ id: number;
2748
+ name: string;
2749
+ scopes: string[];
2750
+ expiresAt: string | null;
2751
+ revokedAt: string | null;
2752
+ lastUsedAt: string | null;
2753
+ createdAt: string | null;
2754
+ };
2755
+ }>;
2756
+ registerOAuth2Client: _spfn_core_route.RouteDef<{}, {}, Response>;
2757
+ getOAuth2Authorize: _spfn_core_route.RouteDef<{
2758
+ query: _sinclair_typebox.TObject<{
2759
+ client_id: _sinclair_typebox.TString;
2760
+ redirect_uri: _sinclair_typebox.TString;
2761
+ code_challenge: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2762
+ code_challenge_method: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2763
+ resource: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2764
+ scope: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2765
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2766
+ }>;
2767
+ }, {}, OAuth2ConsentView>;
2768
+ createOAuth2AuthorizationCode: _spfn_core_route.RouteDef<{
2769
+ body: _sinclair_typebox.TObject<{
2770
+ approve: _sinclair_typebox.TBoolean;
2771
+ client_id: _sinclair_typebox.TString;
2772
+ redirect_uri: _sinclair_typebox.TString;
2773
+ code_challenge: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2774
+ code_challenge_method: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2775
+ resource: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2776
+ scope: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2777
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2778
+ }>;
2779
+ }, {}, OAuth2AuthorizationCodeIssued>;
2780
+ oauth2Token: _spfn_core_route.RouteDef<{}, {}, Response>;
2781
+ oauth2Revoke: _spfn_core_route.RouteDef<{}, {}, Response>;
2782
+ listOAuth2Grants: _spfn_core_route.RouteDef<{}, {}, {
2783
+ grants: OAuth2GrantSummary[];
2784
+ }>;
2785
+ revokeOAuth2Grant: _spfn_core_route.RouteDef<{
2786
+ params: _sinclair_typebox.TObject<{
2787
+ id: _sinclair_typebox.TNumber;
2788
+ }>;
2789
+ }, {}, {
2790
+ revoked: boolean;
2791
+ }>;
2792
+ oauth2AuthorizationServerMetadata: _spfn_core_route.RouteDef<{}, {}, Response>;
2793
+ }>;
2794
+
2795
+ /**
2796
+ * The auth-profile registry the authenticate middleware dispatches on.
2797
+ *
2798
+ * A request that names `x-spfn-auth-profile` is answered by the verifier
2799
+ * registered for that profile — an O(1) map lookup, never a per-profile if
2800
+ * chain in the middleware body. A request that names no profile falls through
2801
+ * to the existing Bearer path untouched.
2802
+ *
2803
+ * Every verifier converges on the same `AuthContext` the Bearer path sets, so
2804
+ * downstream permission/tenant code consumes one principal shape and never
2805
+ * branches on how it was authenticated.
2806
+ *
2807
+ * An app adds its own scheme with `registerAuthProfile` at boot. The dispatch
2808
+ * it joins is the one below, unchanged: a name nobody registered is still
2809
+ * refused, and profile credentials mixed with an Authorization header are
2810
+ * still refused before either path runs.
2811
+ *
2812
+ * The clientProofV1 verifier reuses the phase-1 admission pieces (header
2813
+ * shape, canonical body, proof-input assembly, ECDSA verification) with two
2814
+ * production substitutions: the key directory is `user_public_keys` via
2815
+ * `keysRepository`, and the replay ledger is the pluggable store from
2816
+ * `client-proof/replay-store` (memory default, Redis opt-in). The admission
2817
+ * order is the contract's — revocation → session → expiry → replay → proof —
2818
+ * and the non-disclosure rule holds: an unregistered keyId shares
2819
+ * PROOF_INVALID with a failed signature, while a revoked or expired key
2820
+ * answers SESSION_REVOKED before the proof is ever examined.
2821
+ *
2822
+ * @module server/middleware/auth-profiles
2823
+ */
2824
+
2825
+ /** What a verified request leaves in the context — one shape for every scheme. */
2826
+ interface AuthContext {
2827
+ user: User;
2828
+ userId: string;
2829
+ keyId: string;
2830
+ role: string | null;
2831
+ locale: string;
2832
+ /**
2833
+ * How the principal was authenticated. Informational — downstream code
2834
+ * never branches on it. The union stays open for the profiles an app
2835
+ * registers itself: the built-in names keep their autocomplete, and a
2836
+ * registered profile names its own scheme without editing this file.
2837
+ */
2838
+ scheme: 'bearer' | 'clientProofV1' | 'oneTimeToken' | (string & {});
2839
+ }
2840
+ /** A profile's verifier: admits the request and returns the principal, or throws. */
2841
+ interface AuthProfileVerifier {
2842
+ verify(c: Context): Promise<AuthContext>;
2843
+ }
2844
+ /**
2845
+ * Routes a request to its profile verifier.
2846
+ *
2847
+ * - no profile header → null: the caller continues on the Bearer path;
2848
+ * - profile header + Authorization header → rejected (mixing prohibited);
2849
+ * - unknown profile value → rejected (unknownProfilePolicy: reject).
2850
+ *
2851
+ * Shared by authenticate and optionalAuth so "presented but invalid" refuses
2852
+ * identically on both — only the "presented nothing" outcome differs.
2853
+ */
2854
+ declare function selectAuthProfile(c: Context): AuthProfileVerifier | null;
2855
+ /**
2856
+ * Loads the user for an authenticated key and applies the account-status
2857
+ * rules. One implementation for every scheme: the Bearer path and the profile
2858
+ * verifiers call this, so a status added here gates both identically.
2859
+ */
2860
+ declare function resolveAuthenticatedUser(userId: number): Promise<{
2861
+ user: User;
2862
+ role: string | null;
2863
+ locale: string;
2864
+ }>;
2865
+ /** What the profile path produced for one request. */
2866
+ type AuthProfileOutcome = {
2867
+ kind: 'none';
2868
+ } | {
2869
+ kind: 'authenticated';
2870
+ auth: AuthContext;
2871
+ } | {
2872
+ kind: 'refused';
2873
+ response: Response;
2874
+ };
2875
+ /**
2876
+ * The profile path from dispatch to answer — what `authenticate` and
2877
+ * `optionalAuth` both run before their own Bearer code.
2878
+ *
2879
+ * `none` means the request named no profile and the caller continues on the
2880
+ * Bearer path. A refusal comes back as a built response rather than a throw:
2881
+ * the answer a proven call gets is the contract's own envelope, and an error
2882
+ * handed to the generic error handler is classified by its class name instead.
2883
+ */
2884
+ declare function runAuthProfile(c: Context): Promise<AuthProfileOutcome>;
2885
+ /**
2886
+ * Registers an app's own verifier under a profile name.
2887
+ *
2888
+ * Call it at boot, before the first request: the registry is a module-global
2889
+ * read on every dispatch, so a profile registered later is simply a profile
2890
+ * the requests before it did not have. There is no freeze and no
2891
+ * unregistration — an auth surface that can be rearranged at runtime is a
2892
+ * surface an app bug can rearrange.
2893
+ *
2894
+ * A duplicate name throws rather than replacing the verifier that holds it,
2895
+ * `clientProofV1` included. A silent override is how a second import order, or
2896
+ * a copied profile name, quietly swaps the code that decides who is admitted.
2897
+ *
2898
+ * The verifier must expose a callable `verify` — a value that cannot admit
2899
+ * anyone is refused at boot rather than becoming a registry entry the dispatch
2900
+ * reads as "no profile header", which is anonymous passage under
2901
+ * `optionalAuth` for a request that presented profile credentials.
2902
+ *
2903
+ * The verifier returns the same `AuthContext` the Bearer path sets and refuses
2904
+ * by throwing. A resolve that carries no `userId` is refused as a throw too —
2905
+ * "no user" is a refusal, never a principal. A throw is not caught here:
2906
+ * `runAuthProfile` answers the internal clientProofV1 contract refusal and
2907
+ * nothing else, so a verifier's own error reaches the app's generic error
2908
+ * handler exactly as the Bearer path's `UnauthorizedError` does — and never
2909
+ * becomes anonymous passage, not even under `optionalAuth`.
2910
+ *
2911
+ * @example
2912
+ * ```typescript
2913
+ * registerAuthProfile('serviceTokenV1', {
2914
+ * verify: async (c) =>
2915
+ * {
2916
+ * const user = await authenticateServiceToken(c.req.header('x-acme-service-token'));
2917
+ * if (user === null)
2918
+ * {
2919
+ * throw new UnauthorizedError({ message: 'Invalid service token' });
2920
+ * }
2921
+ *
2922
+ * return { user, userId: String(user.id), keyId: 'service', role: null, locale: 'en', scheme: 'serviceTokenV1' };
2923
+ * },
2924
+ * });
2925
+ * ```
2926
+ */
2927
+ declare function registerAuthProfile(profileId: string, verifier: AuthProfileVerifier): void;
2928
+
2929
+ declare module 'hono' {
2930
+ interface ContextVariableMap {
2931
+ auth: AuthContext;
2932
+ }
2933
+ }
2934
+ /**
2935
+ * Authentication middleware
2936
+ *
2937
+ * Verifies client-signed JWT token using stored public key
2938
+ * Must be applied to routes that require authentication
2939
+ *
2940
+ * @example
2941
+ * ```typescript
2942
+ * // In server.config.ts
2943
+ * import { authenticate } from '@spfn/auth/server/middleware';
2944
+ *
2945
+ * export default defineServerConfig()
2946
+ * .middlewares([authenticate])
2947
+ * .routes(appRouter)
2948
+ * .build();
2949
+ *
2950
+ * // In route file - skip auth for public routes
2951
+ * export const publicRoute = route.get('/status')
2952
+ * .skip(['auth']) // Type-safe skip
2953
+ * .handler(async (c) => c.success({ status: 'ok' }));
2954
+ *
2955
+ * // Protected route - auth applied automatically
2956
+ * export const protectedRoute = route.get('/profile')
2957
+ * .handler(async (c) => {
2958
+ * const auth = c.get('auth'); // Get auth context
2959
+ * const { user, userId, keyId } = auth;
2960
+ * // Or access directly: c.get('auth').user
2961
+ * });
2962
+ * ```
2963
+ */
2964
+ declare const authenticate: _spfn_core_route.NamedMiddleware<"auth">;
2965
+ /**
2966
+ * Optional authentication middleware
2967
+ *
2968
+ * Same as `authenticate` but does NOT reject unauthenticated requests.
2969
+ * - No token → continues without auth context
2970
+ * - Invalid token → continues without auth context
2971
+ * - Valid token → sets auth context normally
2972
+ *
2973
+ * Auto-skips the global 'auth' middleware when used at route level.
2974
+ *
2975
+ * @example
2976
+ * ```typescript
2977
+ * // No need for .skip(['auth']) — handled automatically
2978
+ * export const getProducts = route.get('/products')
2979
+ * .use([optionalAuth])
2980
+ * .handler(async (c) => {
2981
+ * const auth = getOptionalAuth(c); // AuthContext | undefined
2982
+ *
2983
+ * if (auth)
2984
+ * {
2985
+ * return getPersonalizedProducts(auth.userId);
2986
+ * }
2987
+ *
2988
+ * return getPublicProducts();
2989
+ * });
2990
+ * ```
2991
+ */
2992
+ declare const optionalAuth: _spfn_core_route.NamedMiddleware<"optionalAuth">;
2993
+
2994
+ /** What a verified machine request acts as. Not a user, by construction. */
2995
+ interface MachinePrincipal {
2996
+ /** e.g. 'account' | 'service' | registrant-defined */
2997
+ subjectType: string;
2998
+ subjectId: string;
2999
+ scopes: string[];
3000
+ /** verifier-defined extras (claims, token id for audit, …) */
3001
+ claims?: Record<string, unknown>;
3002
+ /** which registered verifier admitted it */
3003
+ scheme: string;
3004
+ }
3005
+ interface MachineVerifierRegistration {
3006
+ /** unique id; becomes MachinePrincipal.scheme */
3007
+ id: string;
3008
+ /** exactly one discriminator */
3009
+ match: {
3010
+ tokenPrefix: string;
3011
+ } | {
3012
+ kidPrefix: string;
3013
+ };
3014
+ verify(token: string, c: Context): Promise<MachinePrincipal>;
3015
+ }
3016
+ declare module 'hono' {
3017
+ interface ContextVariableMap {
3018
+ machinePrincipal: MachinePrincipal;
3019
+ }
3020
+ }
3021
+ /** Read the verified machine principal a handler runs under. */
3022
+ declare function getMachinePrincipal(c: Context): MachinePrincipal | null;
3023
+ /**
3024
+ * Registers a verifier for one machine credential namespace. Call it at boot.
3025
+ *
3026
+ * Refused at registration: a duplicate `id`, a `match` that does not name
3027
+ * exactly one non-empty discriminator, a verifier without a callable `verify`,
3028
+ * and — the point of the check — a discriminator that shadows or is shadowed by
3029
+ * an already-registered one of the same kind. Two verifiers a token could match
3030
+ * would make admission depend on registration order; that is a boot-time bug,
3031
+ * not something the dispatch should resolve on every request.
3032
+ *
3033
+ * @example
3034
+ * ```typescript
3035
+ * registerMachineVerifier({
3036
+ * id: 'runtimeJwsV1',
3037
+ * match: { kidPrefix: 'machine:runtime:' },
3038
+ * verify: async (token) =>
3039
+ * {
3040
+ * const { payload } = await jwtVerify(token, RUNTIME_JWKS);
3041
+ *
3042
+ * return {
3043
+ * subjectType: 'account',
3044
+ * subjectId: String(payload.sub),
3045
+ * scopes: String(payload.scope ?? '').split(' ').filter(Boolean),
3046
+ * scheme: 'runtimeJwsV1',
3047
+ * };
3048
+ * },
3049
+ * });
3050
+ * ```
3051
+ */
3052
+ declare function registerMachineVerifier(reg: MachineVerifierRegistration): void;
3053
+ /**
3054
+ * Admits a machine credential, or refuses. Sets `machinePrincipal`; `auth`
3055
+ * stays unset, so `getAuth(c)` is as empty here as on an anonymous request.
3056
+ *
3057
+ * Auto-skips the global 'auth' middleware, like `opsTokenAuth`.
3058
+ *
3059
+ * @example
3060
+ * ```typescript
3061
+ * export const ingest = route.post('/v1/ingest')
3062
+ * .use([machineAuth, requireMachineScope('events:write')])
3063
+ * .handler(async (c) => {
3064
+ * const { subjectType, subjectId } = getMachinePrincipal(c.raw)!;
3065
+ * // ...
3066
+ * });
3067
+ * ```
3068
+ */
3069
+ declare const machineAuth: _spfn_core_route.NamedMiddleware<"machineAuth">;
3070
+ /**
3071
+ * Require the verified machine principal to carry every named scope.
3072
+ *
3073
+ * Fails closed: no principal in the context is a 401, not a pass — a route that
3074
+ * mounted this without `machineAuth` refuses rather than running unauthenticated,
3075
+ * exactly as `requireOpsScope` does. Scopes match exactly; there is no wildcard,
3076
+ * because what a scope string means belongs to the verifier that issued it.
3077
+ *
3078
+ * @example
3079
+ * ```ts
3080
+ * export const ingest = route.post('/v1/ingest')
3081
+ * .use([machineAuth, requireMachineScope('events:write')])
3082
+ * .handler(async () => { ... });
3083
+ * ```
3084
+ */
3085
+ declare const requireMachineScope: _spfn_core_route.NamedMiddlewareFactory<"machineScope", string[]>;
3086
+
3087
+ export { type DenyDeviceAuthParams as $, type AuthInitOptions as A, type AuthDeletionRequestedPayload as B, type ConfirmSignupLinkResult as C, type DeviceAuthInfoResult as D, type AuthDeviceRegisteredPayload as E, type FinishPasskeyEnrollmentResult as F, type AuthLoginPayload as G, type AuthPasswordResetPayload as H, type IssueOneTimeTokenResult as I, type AuthProfileOutcome as J, type KeySummary as K, type LoginResult as L, type AuthProfileVerifier as M, type NewPasskey as N, type OAuthStartResult as O, type PermissionConfig as P, AuthProviderSchema as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type AuthRegisterPayload as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type ChangePasswordParams as W, type CompletePasswordResetParams as X, type CompleteSignupParams as Y, type ConfirmPasswordResetParams as Z, type ConfirmSignupLinkParams as _, type RegisterResult as a, assertRecentAuthentication as a$, type DeviceAuthApprovedResult as a0, type DeviceAuthInfoParams as a1, type DeviceAuthPendingResult as a2, DeviceAuthPollResponseSchema as a3, DeviceNameSchema as a4, type DeviceRegistrationChannel as a5, EmailSchema as a6, FingerprintSchema as a7, type FinishPasskeyEnrollmentParams as a8, type FinishPasskeyLoginParams as a9, type PollDeviceAuthResult as aA, PublicKeySchema as aB, type RecentAuthenticationParams as aC, type RegisterParams as aD, type RegisterPublicKeyParams as aE, type RenamePasskeyParams as aF, type RequestPasswordResetParams as aG, type RequestSignupLinkParams as aH, type RevokeAllKeysParams as aI, type RevokeKeyParams as aJ, type RevokePasskeyParams as aK, type RotateKeyParams as aL, type SendVerificationCodeParams as aM, type StartDeviceAuthParams as aN, type StartPasskeyEnrollmentParams as aO, TargetTypeSchema as aP, type UnlinkNotification as aQ, UnlinkNotifyRejection as aR, type UnlinkNotifyRequest as aS, type UnlinkNotifyResult as aT, UserCodeSchema as aU, VerificationPurposeSchema as aV, type VerifyCodeParams as aW, type VerifyCodeResult as aX, approveDeviceAuthService as aY, approveOAuth2AuthorizeService as aZ, assertNotLastRecoveryCredential as a_, type InvitationAcceptedPayload as aa, type InvitationCreatedPayload as ab, KEY_FINGERPRINT_PREFIX_LENGTH as ac, KeyIdSchema as ad, type LoginParams as ae, type LogoutParams as af, type MachinePrincipal as ag, type MachineVerifierRegistration as ah, type NativeVerifyOptions as ai, type NormalizedIdentity as aj, type OAuth2AuthorizeParams as ak, type OAuth2ScopeDescription as al, type OAuthCallbackParams as am, type OAuthCallbackResult as an, type OAuthCodeExchangeOptions as ao, type OAuthNativeParams as ap, type OAuthStartParams as aq, type OAuthTokens as ar, type OAuthUnlinkedPayload as as, PASSKEY_DEVICE_TYPES as at, PASSKEY_LABEL_MAX_LENGTH as au, type PasskeyDeviceType as av, PasswordSchema as aw, PhoneSchema as ax, PlatformSchema as ay, type PollDeviceAuthParams as az, type RequestSignupLinkResult as b, startDeviceAuthService as b$, authDeletionCancelledEvent as b0, authDeletionCompletedEvent as b1, authDeletionRequestedEvent as b2, authDeviceRegisteredEvent as b3, authLoginEvent as b4, authPasswordResetEvent as b5, authRegisterEvent as b6, authenticate as b7, buildOAuthErrorUrl as b8, changePasswordService as b9, oauthNativeService as bA, oauthStartService as bB, oauthUnlinkNotifyService as bC, oauthUnlinkedEvent as bD, optionalAuth as bE, passkeys as bF, pollDeviceAuthService as bG, registerAuthProfile as bH, registerMachineVerifier as bI, registerOAuthProvider as bJ, registerPublicKeyService as bK, registerService as bL, renamePasskeyService as bM, requestPasswordResetService as bN, requestSignupLinkService as bO, requireEnabledProvider as bP, requireMachineScope as bQ, resolveAuthenticatedUser as bR, revokeAllKeysService as bS, revokeAllOAuth2GrantsForUser as bT, revokeKeyService as bU, revokeOAuth2GrantService as bV, revokePasskeyService as bW, rotateKeyService as bX, runAuthProfile as bY, selectAuthProfile as bZ, sendVerificationCodeService as b_, completePasswordResetService as ba, completeSignupService as bb, confirmPasswordResetService as bc, confirmSignupLinkService as bd, denyDeviceAuthService as be, denyOAuth2AuthorizeService as bf, describeOAuth2AuthorizeRequestService as bg, finishPasskeyEnrollmentService as bh, finishPasskeyLoginService as bi, getDeviceAuthInfoService as bj, getEnabledOAuthProviders as bk, getGoogleAccessToken as bl, getMachinePrincipal as bm, getOAuthProvider as bn, getRegisteredProviders as bo, invitationAcceptedEvent as bp, invitationCreatedEvent as bq, isOAuthProviderEnabled as br, issueOneTimeTokenService as bs, listKeysService as bt, listOAuth2GrantsService as bu, listPasskeysService as bv, loginService as bw, logoutService as bx, machineAuth as by, oauthCallbackService as bz, type RequestPasswordResetResult as c, startPasskeyEnrollmentService as c0, startPasskeyLoginService as c1, verifyCodeService as c2, verifyOneTimeTokenService as c3, type ConfirmPasswordResetResult as d, type StartDeviceAuthResult as e, type PasskeySummary as f, type RotateKeyResult as g, type RevokeAllKeysResult as h, type OAuthNativeResult as i, type ProfileInfo as j, type OAuth2ConsentView as k, type OAuth2AuthorizationCodeIssued as l, mainAuthRouter as m, type OAuth2GrantSummary as n, type AuthSession as o, PERMISSION_CATEGORIES as p, type PermissionCategory as q, VERIFICATION_TARGET_TYPES as r, type VerificationPurpose as s, type VerificationTargetType as t, type OAuthProvider as u, type Passkey as v, type AuthContext as w, type ApproveDeviceAuthParams as x, type AuthDeletionCancelledPayload as y, type AuthDeletionCompletedPayload as z };