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