@cplieger/access 0.0.0 → 1.0.0-dev.10

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/src/wire.ts ADDED
@@ -0,0 +1,910 @@
1
+ import type { ApiErr } from "@cplieger/reactive/fetch";
2
+
3
+ /**
4
+ * A validator: reads an untrusted JSON value and returns a fresh, narrowed copy, or undefined
5
+ * when the value is not the type. It never throws.
6
+ */
7
+ export type Validator<T> = (value: unknown) => T | undefined;
8
+
9
+ /** @internal Go's Role constants. */
10
+ export const ROLES = ["admin", "user"] as const;
11
+ /** @internal Go's Method constants. */
12
+ export const METHODS = ["password", "passkey", "oidc", "recovery", "api_key", "bypass"] as const;
13
+ /** @internal Go's TokenPurpose constants. */
14
+ export const TOKEN_PURPOSES = ["setup", "recovery", "invite"] as const;
15
+
16
+ // An open set (types.go: Method, Role, TokenPurpose): the server may send an app-defined
17
+ // value beside Go's constants, so a switch over one needs a default arm.
18
+ type Open<L extends readonly string[]> = L[number] | (string & {});
19
+ type Role = Open<typeof ROLES>;
20
+ type Method = Open<typeof METHODS>;
21
+ type TokenPurpose = Open<typeof TOKEN_PURPOSES>;
22
+
23
+ // Ties each field's optionality to the interface, so a field the interface marks optional
24
+ // cannot be declared required in its validator, or the reverse.
25
+ interface Field<V, O extends boolean> {
26
+ readonly check: Validator<V>;
27
+ readonly optional: O;
28
+ }
29
+ type Fields<T> = {
30
+ readonly [K in keyof T]-?: Field<
31
+ Exclude<T[K], undefined>,
32
+ Pick<T, K> extends Required<Pick<T, K>> ? false : true
33
+ >;
34
+ };
35
+
36
+ const req = <V>(check: Validator<V>): Field<V, false> => ({ check, optional: false });
37
+ const opt = <V>(check: Validator<V>): Field<V, true> => ({ check, optional: true });
38
+
39
+ function isRecord(v: unknown): v is Record<string, unknown> {
40
+ return typeof v === "object" && v !== null && !Array.isArray(v);
41
+ }
42
+
43
+ // strict refuses a key the type does not declare, as accesshttp's decoder does; a loose
44
+ // object (the browser's WebAuthn options) drops it instead.
45
+ function object<T>(fields: Fields<NoInfer<T>>, strict = true): Validator<T> {
46
+ const spec: Readonly<Record<string, Field<unknown, boolean>>> = fields;
47
+ const keys = Object.keys(spec);
48
+ return (v) => {
49
+ if (!isRecord(v)) {
50
+ return undefined;
51
+ }
52
+ if (strict && Object.keys(v).some((k) => !Object.hasOwn(spec, k))) {
53
+ return undefined;
54
+ }
55
+ const out: Record<string, unknown> = {};
56
+ for (const k of keys) {
57
+ const f = spec[k];
58
+ if (f === undefined) {
59
+ return undefined;
60
+ }
61
+ if (!Object.hasOwn(v, k)) {
62
+ if (f.optional) {
63
+ continue;
64
+ }
65
+ return undefined;
66
+ }
67
+ const value = f.check(v[k]);
68
+ if (value === undefined) {
69
+ return undefined;
70
+ }
71
+ out[k] = value;
72
+ }
73
+ return out as T;
74
+ };
75
+ }
76
+
77
+ /** @internal */
78
+ export function list<V>(check: Validator<V>): Validator<readonly V[]> {
79
+ return (v) => {
80
+ if (!Array.isArray(v)) {
81
+ return undefined;
82
+ }
83
+ const out: V[] = [];
84
+ for (const item of v as readonly unknown[]) {
85
+ const value = check(item);
86
+ if (value === undefined) {
87
+ return undefined;
88
+ }
89
+ out.push(value);
90
+ }
91
+ return out;
92
+ };
93
+ }
94
+
95
+ function oneOf<const L extends readonly string[]>(...values: L): Validator<L[number]> {
96
+ const set: ReadonlySet<string> = new Set(values);
97
+ return (v) => (typeof v === "string" && set.has(v) ? v : undefined);
98
+ }
99
+
100
+ const str: Validator<string> = (v) => (typeof v === "string" ? v : undefined);
101
+ const bool: Validator<boolean> = (v) => (typeof v === "boolean" ? v : undefined);
102
+ const int: Validator<number> = (v) =>
103
+ typeof v === "number" && Number.isSafeInteger(v) ? v : undefined;
104
+ const count: Validator<number> = (v) => {
105
+ const n = int(v);
106
+ return n !== undefined && n >= 0 ? n : undefined;
107
+ };
108
+ // User, key and passkey IDs are positive and within Number.MAX_SAFE_INTEGER (contract rule 17).
109
+ const id: Validator<number> = (v) => {
110
+ const n = int(v);
111
+ return n !== undefined && n > 0 ? n : undefined;
112
+ };
113
+ const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
114
+ const time: Validator<string> = (v) =>
115
+ typeof v === "string" && RFC3339.test(v) && !Number.isNaN(Date.parse(v)) ? v : undefined;
116
+ const BASE64URL = /^[A-Za-z0-9_-]*$/;
117
+ const base64url: Validator<string> = (v) =>
118
+ typeof v === "string" && BASE64URL.test(v) && v.length % 4 !== 1 ? v : undefined;
119
+ const nonEmpty: Validator<string> = (v) => (typeof v === "string" && v !== "" ? v : undefined);
120
+ const role: Validator<Role> = nonEmpty;
121
+ const method: Validator<Method> = nonEmpty;
122
+ const purpose: Validator<TokenPurpose> = nonEmpty;
123
+ const ids = list(id);
124
+ const strings = list(str);
125
+
126
+ // Requests.
127
+
128
+ export interface SetupPasswordRequest {
129
+ readonly token?: string;
130
+ readonly username: string;
131
+ readonly password: string;
132
+ }
133
+ export const parseSetupPasswordRequest: Validator<SetupPasswordRequest> = object({
134
+ token: opt(str),
135
+ username: req(str),
136
+ password: req(str),
137
+ });
138
+
139
+ export interface SetupPasskeyBeginRequest {
140
+ readonly token?: string;
141
+ readonly username: string;
142
+ }
143
+ export const parseSetupPasskeyBeginRequest: Validator<SetupPasskeyBeginRequest> = object({
144
+ token: opt(str),
145
+ username: req(str),
146
+ });
147
+
148
+ export interface LoginRequest {
149
+ readonly username: string;
150
+ readonly password: string;
151
+ }
152
+ export const parseLoginRequest: Validator<LoginRequest> = object({
153
+ username: req(str),
154
+ password: req(str),
155
+ });
156
+
157
+ export interface RedirectRequest {
158
+ readonly redirect?: string;
159
+ }
160
+ export const parseRedirectRequest: Validator<RedirectRequest> = object({ redirect: opt(str) });
161
+
162
+ export interface TokenRequest {
163
+ readonly token: string;
164
+ }
165
+ export const parseTokenRequest: Validator<TokenRequest> = object({ token: req(str) });
166
+
167
+ export interface RecoverPasswordRequest {
168
+ readonly password: string;
169
+ readonly keep_sessions?: boolean;
170
+ readonly keep_keys?: boolean;
171
+ }
172
+ export const parseRecoverPasswordRequest: Validator<RecoverPasswordRequest> = object({
173
+ password: req(str),
174
+ keep_sessions: opt(bool),
175
+ keep_keys: opt(bool),
176
+ });
177
+
178
+ export interface InviteAcceptRequest {
179
+ readonly token: string;
180
+ readonly username: string;
181
+ readonly password: string;
182
+ }
183
+ export const parseInviteAcceptRequest: Validator<InviteAcceptRequest> = object({
184
+ token: req(str),
185
+ username: req(str),
186
+ password: req(str),
187
+ });
188
+
189
+ export interface ProfileRequest {
190
+ readonly display_name: string;
191
+ }
192
+ export const parseProfileRequest: Validator<ProfileRequest> = object({ display_name: req(str) });
193
+
194
+ export interface PasswordRequest {
195
+ readonly password: string;
196
+ }
197
+ export const parsePasswordRequest: Validator<PasswordRequest> = object({ password: req(str) });
198
+
199
+ /** current_password is required when the user holds a password. */
200
+ export interface PasswordChangeRequest {
201
+ readonly current_password?: string;
202
+ readonly password: string;
203
+ }
204
+ export const parsePasswordChangeRequest: Validator<PasswordChangeRequest> = object({
205
+ current_password: opt(str),
206
+ password: req(str),
207
+ });
208
+
209
+ export interface PasswordRemoveRequest {
210
+ readonly current_password: string;
211
+ }
212
+ export const parsePasswordRemoveRequest: Validator<PasswordRemoveRequest> = object({
213
+ current_password: req(str),
214
+ });
215
+
216
+ export interface NameRequest {
217
+ readonly name: string;
218
+ }
219
+ export const parseNameRequest: Validator<NameRequest> = object({ name: req(str) });
220
+
221
+ /** expires_in_days absent or 0 is a key that never expires. */
222
+ export interface NewKeyRequest {
223
+ readonly label: string;
224
+ readonly scopes: readonly string[];
225
+ readonly expires_in_days?: number;
226
+ }
227
+ export const parseNewKeyRequest: Validator<NewKeyRequest> = object({
228
+ label: req(str),
229
+ scopes: req(strings),
230
+ expires_in_days: opt(int),
231
+ });
232
+
233
+ /** Without a password the user has no sign-in method until a recovery link. */
234
+ export interface NewUserRequest {
235
+ readonly username: string;
236
+ readonly password?: string;
237
+ readonly role: Role;
238
+ }
239
+ export const parseNewUserRequest: Validator<NewUserRequest> = object({
240
+ username: req(str),
241
+ password: opt(str),
242
+ role: req(role),
243
+ });
244
+
245
+ export interface UserUpdateRequest {
246
+ readonly role?: Role;
247
+ readonly enabled?: boolean;
248
+ }
249
+ export const parseUserUpdateRequest: Validator<UserUpdateRequest> = object({
250
+ role: opt(role),
251
+ enabled: opt(bool),
252
+ });
253
+
254
+ /** hours absent or 0 is the default lifetime. */
255
+ export interface TTLRequest {
256
+ readonly hours?: number;
257
+ }
258
+ export const parseTTLRequest: Validator<TTLRequest> = object({ hours: opt(int) });
259
+
260
+ export interface InviteRequest {
261
+ readonly role: Role;
262
+ readonly hours?: number;
263
+ }
264
+ export const parseInviteRequest: Validator<InviteRequest> = object({
265
+ role: req(role),
266
+ hours: opt(int),
267
+ });
268
+
269
+ // Responses.
270
+
271
+ export interface StateResponse {
272
+ readonly setup_required: boolean;
273
+ readonly password_login: boolean;
274
+ readonly passkeys: boolean;
275
+ readonly oidc: boolean;
276
+ }
277
+ export const parseStateResponse: Validator<StateResponse> = object({
278
+ setup_required: req(bool),
279
+ password_login: req(bool),
280
+ passkeys: req(bool),
281
+ oidc: req(bool),
282
+ });
283
+
284
+ export interface UserMethodsView {
285
+ readonly password: boolean;
286
+ readonly passkey_count: number;
287
+ }
288
+ export const parseUserMethodsView: Validator<UserMethodsView> = object({
289
+ password: req(bool),
290
+ passkey_count: req(count),
291
+ });
292
+
293
+ /** key_lineage and methods are set on the admin list only. */
294
+ export interface UserView {
295
+ readonly id: number;
296
+ readonly username: string;
297
+ readonly display_name?: string;
298
+ readonly role: Role;
299
+ readonly enabled: boolean;
300
+ readonly oidc_linked: boolean;
301
+ readonly created_at: string;
302
+ readonly key_lineage?: readonly number[];
303
+ readonly methods?: UserMethodsView;
304
+ }
305
+ export const parseUserView: Validator<UserView> = object({
306
+ id: req(id),
307
+ username: req(str),
308
+ display_name: opt(str),
309
+ role: req(role),
310
+ enabled: req(bool),
311
+ oidc_linked: req(bool),
312
+ created_at: req(time),
313
+ key_lineage: opt(ids),
314
+ methods: opt(parseUserMethodsView),
315
+ });
316
+
317
+ export interface SignedInResponse {
318
+ readonly user: UserView;
319
+ readonly session_id: string;
320
+ readonly clone_warning?: boolean;
321
+ }
322
+ export const parseSignedInResponse: Validator<SignedInResponse> = object({
323
+ user: req(parseUserView),
324
+ session_id: req(str),
325
+ clone_warning: opt(bool),
326
+ });
327
+
328
+ /** The WebAuthn options of a begin route, in the browser's JSON shape. */
329
+ export type CeremonyOptions =
330
+ | { readonly publicKey: PublicKeyCredentialCreationOptionsJSON }
331
+ | {
332
+ readonly publicKey: PublicKeyCredentialRequestOptionsJSON;
333
+ readonly mediation?: CredentialMediationRequirement;
334
+ };
335
+
336
+ export interface CeremonyResponse {
337
+ /** Sent back in the finish request's X-Auth-Ceremony header. */
338
+ readonly ceremony: string;
339
+ readonly public_key: CeremonyOptions;
340
+ }
341
+
342
+ const descriptor: Validator<PublicKeyCredentialDescriptorJSON> = object(
343
+ { id: req(base64url), type: req(str), transports: opt(list(str)) as Field<string[], true> },
344
+ false,
345
+ );
346
+ const descriptors = list(descriptor) as Validator<PublicKeyCredentialDescriptorJSON[]>;
347
+ const hints = strings as Validator<string[]>;
348
+
349
+ // An extension input accesshttp never sends is refused rather than passed through unchecked.
350
+ const absent: Field<never, true> = { check: () => undefined, optional: true };
351
+ const extensionInputs = object<AuthenticationExtensionsClientInputsJSON>(
352
+ { appid: opt(str), credProps: opt(bool), largeBlob: absent, prf: absent },
353
+ false,
354
+ );
355
+
356
+ const creationFields = object<PublicKeyCredentialCreationOptionsJSON>(
357
+ {
358
+ challenge: req(base64url),
359
+ rp: req(object({ name: req(str), id: opt(str) }, false)),
360
+ user: req(object({ id: req(base64url), name: req(str), displayName: req(str) }, false)),
361
+ pubKeyCredParams: req(
362
+ list(object({ alg: req(int), type: req(oneOf("public-key")) }, false)) as Validator<
363
+ PublicKeyCredentialParameters[]
364
+ >,
365
+ ),
366
+ authenticatorSelection: opt(
367
+ object<AuthenticatorSelectionCriteria>(
368
+ {
369
+ authenticatorAttachment: opt(oneOf("cross-platform", "platform")),
370
+ requireResidentKey: opt(bool),
371
+ residentKey: opt(oneOf("discouraged", "preferred", "required")),
372
+ userVerification: opt(oneOf("discouraged", "preferred", "required")),
373
+ },
374
+ false,
375
+ ),
376
+ ),
377
+ attestation: opt(str),
378
+ excludeCredentials: opt(descriptors),
379
+ extensions: opt(extensionInputs),
380
+ hints: opt(hints),
381
+ timeout: opt(count),
382
+ },
383
+ false,
384
+ );
385
+
386
+ // accesshttp omits an empty pubKeyCredParams, which the browser API requires as a member.
387
+ const creationOptions: Validator<PublicKeyCredentialCreationOptionsJSON> = (v) =>
388
+ isRecord(v) && !Object.hasOwn(v, "pubKeyCredParams")
389
+ ? creationFields({ ...v, pubKeyCredParams: [] })
390
+ : creationFields(v);
391
+
392
+ const requestOptions = object<PublicKeyCredentialRequestOptionsJSON>(
393
+ {
394
+ challenge: req(base64url),
395
+ rpId: opt(str),
396
+ userVerification: opt(str),
397
+ allowCredentials: opt(descriptors),
398
+ hints: opt(hints),
399
+ timeout: opt(count),
400
+ extensions: opt(extensionInputs),
401
+ },
402
+ false,
403
+ );
404
+
405
+ const mediation = oneOf("conditional", "optional", "required", "silent");
406
+
407
+ const ceremonyOptions: Validator<CeremonyOptions> = (v) => {
408
+ if (!isRecord(v)) {
409
+ return undefined;
410
+ }
411
+ const pk = v["publicKey"];
412
+ if (isRecord(pk) && (Object.hasOwn(pk, "rp") || Object.hasOwn(pk, "user"))) {
413
+ const publicKey = creationOptions(pk);
414
+ return publicKey === undefined ? undefined : { publicKey };
415
+ }
416
+ const publicKey = requestOptions(pk);
417
+ if (publicKey === undefined) {
418
+ return undefined;
419
+ }
420
+ if (!Object.hasOwn(v, "mediation")) {
421
+ return { publicKey };
422
+ }
423
+ const m = mediation(v["mediation"]);
424
+ return m === undefined ? undefined : { publicKey, mediation: m };
425
+ };
426
+
427
+ export const parseCeremonyResponse: Validator<CeremonyResponse> = object({
428
+ ceremony: req(str),
429
+ public_key: req(ceremonyOptions),
430
+ });
431
+
432
+ export interface OIDCStartResponse {
433
+ readonly url: string;
434
+ }
435
+ export const parseOIDCStartResponse: Validator<OIDCStartResponse> = object({ url: req(str) });
436
+
437
+ export interface PendingLinkResponse {
438
+ readonly issuer: string;
439
+ readonly username_hint?: string;
440
+ readonly email?: string;
441
+ }
442
+ export const parsePendingLinkResponse: Validator<PendingLinkResponse> = object({
443
+ issuer: req(str),
444
+ username_hint: opt(str),
445
+ email: opt(str),
446
+ });
447
+
448
+ export interface MethodLockView {
449
+ readonly method: Method;
450
+ readonly untrusted: boolean;
451
+ readonly trusted: boolean;
452
+ }
453
+ export const parseMethodLockView: Validator<MethodLockView> = object({
454
+ method: req(method),
455
+ untrusted: req(bool),
456
+ trusted: req(bool),
457
+ });
458
+
459
+ export interface MethodsView {
460
+ readonly password: boolean;
461
+ readonly passkey_count: number;
462
+ readonly oidc_linked: boolean;
463
+ readonly oidc_available: boolean;
464
+ readonly passkeys_available: boolean;
465
+ readonly password_login: boolean;
466
+ readonly locks: readonly MethodLockView[];
467
+ }
468
+ export const parseMethodsView: Validator<MethodsView> = object({
469
+ password: req(bool),
470
+ passkey_count: req(count),
471
+ oidc_linked: req(bool),
472
+ oidc_available: req(bool),
473
+ passkeys_available: req(bool),
474
+ password_login: req(bool),
475
+ locks: req(list(parseMethodLockView)),
476
+ });
477
+
478
+ export interface CurrentSessionView {
479
+ readonly id: string;
480
+ readonly method: Method;
481
+ readonly auth_time: string;
482
+ readonly idle_deadline: string;
483
+ readonly absolute_deadline: string;
484
+ }
485
+ export const parseCurrentSessionView: Validator<CurrentSessionView> = object({
486
+ id: req(str),
487
+ method: req(method),
488
+ auth_time: req(time),
489
+ idle_deadline: req(time),
490
+ absolute_deadline: req(time),
491
+ });
492
+
493
+ export interface MeResponse {
494
+ readonly user: UserView;
495
+ readonly methods: MethodsView;
496
+ readonly can_link_oidc: boolean;
497
+ readonly sso_governed: boolean;
498
+ readonly session: CurrentSessionView;
499
+ }
500
+ export const parseMeResponse: Validator<MeResponse> = object({
501
+ user: req(parseUserView),
502
+ methods: req(parseMethodsView),
503
+ can_link_oidc: req(bool),
504
+ sso_governed: req(bool),
505
+ session: req(parseCurrentSessionView),
506
+ });
507
+
508
+ export interface RecoveryResponse {
509
+ readonly username: string;
510
+ readonly methods: MethodsView;
511
+ }
512
+ export const parseRecoveryResponse: Validator<RecoveryResponse> = object({
513
+ username: req(str),
514
+ methods: req(parseMethodsView),
515
+ });
516
+
517
+ /** A passkey whose rp_id differs from the current RP's ID cannot sign in. */
518
+ export interface PasskeyView {
519
+ readonly id: number;
520
+ readonly name: string;
521
+ readonly created_at: string;
522
+ readonly last_used_at?: string;
523
+ readonly backup_eligible: boolean;
524
+ readonly backed_up: boolean;
525
+ readonly clone_warning?: boolean;
526
+ readonly rp_id: string;
527
+ }
528
+ export const parsePasskeyView: Validator<PasskeyView> = object({
529
+ id: req(id),
530
+ name: req(str),
531
+ created_at: req(time),
532
+ last_used_at: opt(time),
533
+ backup_eligible: req(bool),
534
+ backed_up: req(bool),
535
+ clone_warning: opt(bool),
536
+ rp_id: req(str),
537
+ });
538
+
539
+ export interface SessionView {
540
+ readonly id: string;
541
+ readonly label?: string;
542
+ readonly user_agent?: string;
543
+ readonly last_ip?: string;
544
+ readonly method: Method;
545
+ readonly created_at: string;
546
+ readonly last_activity: string;
547
+ readonly auth_time: string;
548
+ readonly idle_deadline: string;
549
+ readonly absolute_deadline: string;
550
+ readonly current: boolean;
551
+ }
552
+ export const parseSessionView: Validator<SessionView> = object({
553
+ id: req(str),
554
+ label: opt(str),
555
+ user_agent: opt(str),
556
+ last_ip: opt(str),
557
+ method: req(method),
558
+ created_at: req(time),
559
+ last_activity: req(time),
560
+ auth_time: req(time),
561
+ idle_deadline: req(time),
562
+ absolute_deadline: req(time),
563
+ current: req(bool),
564
+ });
565
+
566
+ /** owner is set on the admin list only. */
567
+ export interface KeyView {
568
+ readonly id: number;
569
+ readonly user_id: number;
570
+ readonly owner?: string;
571
+ readonly label: string;
572
+ readonly prefix: string;
573
+ readonly suffix: string;
574
+ readonly scopes: readonly string[];
575
+ readonly created_at: string;
576
+ readonly expires_at?: string;
577
+ readonly last_used_at?: string;
578
+ readonly parent_key_id?: number;
579
+ }
580
+ export const parseKeyView: Validator<KeyView> = object({
581
+ id: req(id),
582
+ user_id: req(id),
583
+ owner: opt(str),
584
+ label: req(str),
585
+ prefix: req(str),
586
+ suffix: req(str),
587
+ scopes: req(strings),
588
+ created_at: req(time),
589
+ expires_at: opt(time),
590
+ last_used_at: opt(time),
591
+ parent_key_id: opt(id),
592
+ });
593
+
594
+ /** key is the plaintext, shown once. */
595
+ export interface KeyCreatedResponse {
596
+ readonly key: string;
597
+ readonly details: KeyView;
598
+ }
599
+ export const parseKeyCreatedResponse: Validator<KeyCreatedResponse> = object({
600
+ key: req(str),
601
+ details: req(parseKeyView),
602
+ });
603
+
604
+ /** token is the plaintext, shown once. */
605
+ export interface TokenResponse {
606
+ readonly token: string;
607
+ readonly expires_at: string;
608
+ }
609
+ export const parseTokenResponse: Validator<TokenResponse> = object({
610
+ token: req(str),
611
+ expires_at: req(time),
612
+ });
613
+
614
+ /** One live invite or recovery token; it carries neither the token nor its hash. */
615
+ export interface TokenView {
616
+ readonly purpose: TokenPurpose;
617
+ readonly role?: Role;
618
+ readonly subject?: number;
619
+ readonly created_at: string;
620
+ readonly expires_at: string;
621
+ readonly issued_by_key?: number;
622
+ readonly issuer_lineage?: readonly number[];
623
+ }
624
+ export const parseTokenView: Validator<TokenView> = object({
625
+ purpose: req(purpose),
626
+ role: opt(role),
627
+ subject: opt(id),
628
+ created_at: req(time),
629
+ expires_at: req(time),
630
+ issued_by_key: opt(id),
631
+ issuer_lineage: opt(ids),
632
+ });
633
+
634
+ /** @internal Go's account.LogClasses(), in its order. */
635
+ export const LOG_CLASSES = ["change", "session"] as const;
636
+ type LogClass = (typeof LOG_CLASSES)[number];
637
+ const logClass = oneOf(...LOG_CLASSES);
638
+
639
+ /** Who made a logged change, with the display data its commit snapshotted. */
640
+ export interface LogActorView {
641
+ readonly user_id?: number;
642
+ readonly session_id?: string;
643
+ readonly key_id?: number;
644
+ readonly out_of_band?: string;
645
+ readonly label?: string;
646
+ readonly user_agent?: string;
647
+ readonly key_suffix?: string;
648
+ }
649
+ export const parseLogActorView: Validator<LogActorView> = object({
650
+ user_id: opt(id),
651
+ session_id: opt(str),
652
+ key_id: opt(id),
653
+ out_of_band: opt(str),
654
+ label: opt(str),
655
+ user_agent: opt(str),
656
+ key_suffix: opt(str),
657
+ });
658
+
659
+ /** at is when the entry was appended; occurred_at is the change's own time. */
660
+ export interface LogEntryView {
661
+ readonly seq: number;
662
+ readonly at: string;
663
+ readonly occurred_at: string;
664
+ readonly class: LogClass;
665
+ readonly kind: string;
666
+ readonly user_id?: number;
667
+ readonly actor: LogActorView;
668
+ readonly ip?: string;
669
+ readonly session_ids?: readonly string[];
670
+ readonly key_id?: number;
671
+ readonly passkey_id?: number;
672
+ readonly method?: Method;
673
+ readonly role?: Role;
674
+ readonly trusted?: boolean;
675
+ readonly detail?: string;
676
+ }
677
+ export const parseLogEntryView: Validator<LogEntryView> = object({
678
+ seq: req(id),
679
+ at: req(time),
680
+ occurred_at: req(time),
681
+ class: req(logClass),
682
+ kind: req(nonEmpty),
683
+ user_id: opt(id),
684
+ actor: req(parseLogActorView),
685
+ ip: opt(str),
686
+ session_ids: opt(strings),
687
+ key_id: opt(id),
688
+ passkey_id: opt(id),
689
+ method: opt(method),
690
+ role: opt(role),
691
+ trusted: opt(bool),
692
+ detail: opt(str),
693
+ });
694
+
695
+ /** evicted counts the entries the size limit removed, ever. */
696
+ export interface LogClassView {
697
+ readonly class: LogClass;
698
+ readonly count: number;
699
+ readonly evicted: number;
700
+ }
701
+ export const parseLogClassView: Validator<LogClassView> = object({
702
+ class: req(logClass),
703
+ count: req(count),
704
+ evicted: req(count),
705
+ });
706
+
707
+ /** One security-log page; classes holds one row per LogClass, in LOG_CLASSES order. */
708
+ export interface SecurityLogView {
709
+ readonly entries: readonly LogEntryView[];
710
+ readonly classes: readonly LogClassView[];
711
+ }
712
+ export const parseSecurityLogView: Validator<SecurityLogView> = object({
713
+ entries: req(list(parseLogEntryView)),
714
+ classes: req(list(parseLogClassView)),
715
+ });
716
+
717
+ /** The arguments of PublicKeyCredential's two signal methods, in their own shape. */
718
+ export interface WebAuthnSignals {
719
+ readonly currentUserDetails: {
720
+ readonly rpId: string;
721
+ readonly userId: string;
722
+ readonly name: string;
723
+ readonly displayName: string;
724
+ };
725
+ readonly allAcceptedCredentials: {
726
+ readonly rpId: string;
727
+ readonly userId: string;
728
+ readonly allAcceptedCredentialIds: readonly string[];
729
+ };
730
+ }
731
+ export const parseWebAuthnSignals: Validator<WebAuthnSignals> = object({
732
+ currentUserDetails: req(
733
+ object({ rpId: req(str), userId: req(base64url), name: req(str), displayName: req(str) }),
734
+ ),
735
+ allAcceptedCredentials: req(
736
+ object({
737
+ rpId: req(str),
738
+ userId: req(base64url),
739
+ allAcceptedCredentialIds: req(list(base64url)),
740
+ }),
741
+ ),
742
+ });
743
+
744
+ // Error codes and reasons.
745
+
746
+ /** accesshttp.Codes(), in its order. @internal */
747
+ export const CODES = [
748
+ "auth_required",
749
+ "auth_session_expired",
750
+ "auth_invalid_credentials",
751
+ "auth_key_invalid",
752
+ "auth_recent_auth_required",
753
+ "auth_session_required",
754
+ "auth_scope_required",
755
+ "auth_forbidden",
756
+ "auth_rate_limited",
757
+ "auth_busy",
758
+ "auth_unavailable",
759
+ "auth_bad_request",
760
+ "auth_body_too_large",
761
+ "auth_username_invalid",
762
+ "auth_username_taken",
763
+ "auth_password_rejected",
764
+ "auth_breach_check_unavailable",
765
+ "auth_password_login_disabled",
766
+ "auth_setup_complete",
767
+ "auth_setup_token_required",
768
+ "auth_token_invalid",
769
+ "auth_last_method",
770
+ "auth_identity_taken",
771
+ "auth_already_linked",
772
+ "auth_method_not_locked",
773
+ "auth_cross_origin",
774
+ "auth_duplicate_credential",
775
+ "auth_unknown_scope",
776
+ "auth_passkeys_unavailable",
777
+ "auth_origin_refused",
778
+ "auth_ceremony_invalid",
779
+ "auth_not_discoverable",
780
+ "auth_unknown_credential",
781
+ "auth_assertion_failed",
782
+ "auth_registration_failed",
783
+ "auth_oidc_unavailable",
784
+ "auth_oidc_failed",
785
+ "auth_not_found",
786
+ "auth_method_not_allowed",
787
+ ] as const;
788
+
789
+ /** Every code accesshttp answers, as accesshttp.Codes() lists them. */
790
+ export type AuthCode = (typeof CODES)[number];
791
+
792
+ /** accesshttp.Reasons(), in its order. @internal */
793
+ export const REASONS = [
794
+ "sso_governed",
795
+ "ttl_too_long",
796
+ "ttl_negative",
797
+ "no_change",
798
+ "expiry_invalid",
799
+ "redirect_too_long",
800
+ "current_password_required",
801
+ "session_gone",
802
+ "pending_too_large",
803
+ "stale_authentication",
804
+ "flow_invalid",
805
+ "discovery_failed",
806
+ "exchange_failed",
807
+ "token_invalid",
808
+ "nonce_mismatch",
809
+ "no_username",
810
+ "provisioning_disabled",
811
+ "not_admitted",
812
+ "username_invalid",
813
+ "recovery",
814
+ "reenable_needs_other_method",
815
+ "trailing_data",
816
+ "malformed_json",
817
+ "content_type",
818
+ "body",
819
+ "unknown_field",
820
+ "token_in_query",
821
+ "too_short",
822
+ "too_long",
823
+ "contains_username",
824
+ "contains_forbidden_word",
825
+ "common",
826
+ "breached",
827
+ "out_of_band_only",
828
+ "key_not_current",
829
+ "admin_target",
830
+ "scopes_not_subset",
831
+ "expiry_after_parent",
832
+ "lineage_too_deep",
833
+ "permission_not_admin",
834
+ "self_recovery",
835
+ "other_user",
836
+ "target_not_eligible",
837
+ "not_principal",
838
+ "unknown_operation",
839
+ "not_enabled_user",
840
+ "not_admin",
841
+ "self_delete",
842
+ "last_admin",
843
+ "last_local_admin",
844
+ "single_owner",
845
+ "not_owner",
846
+ "unknown_via",
847
+ "via_with_actor",
848
+ "empty",
849
+ "disallowed_character",
850
+ "label_disallowed_character",
851
+ "label_too_long",
852
+ "display_name_disallowed_character",
853
+ "display_name_too_long",
854
+ "log_limit",
855
+ "log_range",
856
+ "log_kind",
857
+ "log_id",
858
+ "malformed",
859
+ "trailing_dot",
860
+ "empty_label",
861
+ "non_ascii",
862
+ "no_origin",
863
+ "insecure_scheme",
864
+ "ip_host",
865
+ "host_not_covered",
866
+ "not_allowlisted",
867
+ ] as const;
868
+
869
+ /**
870
+ * Every fixed reason accesshttp answers, as accesshttp.Reasons() lists them. A reason with a
871
+ * dynamic part (provider_error:<code>, a refused field or key name) is outside the union.
872
+ */
873
+ export type AuthReason = (typeof REASONS)[number];
874
+
875
+ const codeSet: ReadonlySet<string> = new Set(CODES);
876
+ const reasonSet: ReadonlySet<string> = new Set(REASONS);
877
+
878
+ // A server code is lifted only from a real error response; fetch's own codes carry status 0,
879
+ // or a 2xx status for "decode", and must never read as a server's.
880
+ function fromServer(e: ApiErr): boolean {
881
+ return e.status >= 400 && e.status <= 599;
882
+ }
883
+
884
+ /** Returns undefined for a code outside the union, so a newer server code is never mistaken for a known one. */
885
+ export function authCode(e: ApiErr): AuthCode | undefined {
886
+ return fromServer(e) && e.code !== undefined ? knownCode(e.code) : undefined;
887
+ }
888
+
889
+ /** The code when it is in the union, else undefined. @internal */
890
+ export function knownCode(c: string): AuthCode | undefined {
891
+ return codeSet.has(c) ? (c as AuthCode) : undefined;
892
+ }
893
+
894
+ /**
895
+ * The error body's non-empty reason, known or not: what a caller tests for presence.
896
+ * @internal
897
+ */
898
+ export function errorReason(e: ApiErr): string | undefined {
899
+ if (!fromServer(e) || !isRecord(e.body) || !Object.hasOwn(e.body, "reason")) {
900
+ return undefined;
901
+ }
902
+ const r = e.body["reason"];
903
+ return typeof r === "string" && r !== "" ? r : undefined;
904
+ }
905
+
906
+ /** Returns undefined for a reason outside the union, as authCode does for codes. */
907
+ export function authReason(e: ApiErr): AuthReason | undefined {
908
+ const r = errorReason(e);
909
+ return r !== undefined && reasonSet.has(r) ? (r as AuthReason) : undefined;
910
+ }