@seamless-auth/types 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +22 -1
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/schemas/auth/auth.schema.d.ts +3 -0
  8. package/dist/schemas/auth/auth.schema.d.ts.map +1 -1
  9. package/dist/schemas/auth/auth.schema.js +2 -0
  10. package/dist/schemas/auth/auth.schema.js.map +1 -1
  11. package/dist/schemas/authEvent/schema.d.ts +43 -4
  12. package/dist/schemas/authEvent/schema.d.ts.map +1 -1
  13. package/dist/schemas/authEvent/schema.js +14 -1
  14. package/dist/schemas/authEvent/schema.js.map +1 -1
  15. package/dist/schemas/oauth/schema.d.ts +3 -0
  16. package/dist/schemas/oauth/schema.d.ts.map +1 -1
  17. package/dist/schemas/oauth/schema.js +8 -0
  18. package/dist/schemas/oauth/schema.js.map +1 -1
  19. package/dist/schemas/policy/schema.d.ts +555 -0
  20. package/dist/schemas/policy/schema.d.ts.map +1 -0
  21. package/dist/schemas/policy/schema.js +125 -0
  22. package/dist/schemas/policy/schema.js.map +1 -0
  23. package/dist/schemas/recovery/schema.d.ts +130 -0
  24. package/dist/schemas/recovery/schema.d.ts.map +1 -0
  25. package/dist/schemas/recovery/schema.js +81 -0
  26. package/dist/schemas/recovery/schema.js.map +1 -0
  27. package/dist/schemas/systemConfig/schema.d.ts +63 -2
  28. package/dist/schemas/systemConfig/schema.d.ts.map +1 -1
  29. package/dist/schemas/systemConfig/schema.js +51 -5
  30. package/dist/schemas/systemConfig/schema.js.map +1 -1
  31. package/package.json +4 -3
  32. package/src/index.ts +2 -0
  33. package/src/schemas/auth/auth.schema.ts +2 -0
  34. package/src/schemas/authEvent/schema.ts +14 -1
  35. package/src/schemas/oauth/schema.ts +8 -0
  36. package/src/schemas/policy/schema.ts +168 -0
  37. package/src/schemas/recovery/schema.ts +117 -0
  38. package/src/schemas/systemConfig/schema.ts +68 -5
@@ -0,0 +1,168 @@
1
+ import { z } from 'zod';
2
+
3
+ import { IsoDate } from '../../shared.js';
4
+ import { SystemConfigSchema } from '../systemConfig/schema.js';
5
+
6
+ /**
7
+ * The system config keys that decide who can sign in, and how. These are the ones a
8
+ * change can lock people out with, so each published change is kept as a version that
9
+ * can be restored in one step, and can be staged as a draft and dry-run first.
10
+ */
11
+ export const POLICY_KEYS = [
12
+ 'login_methods',
13
+ 'passkey_login_fallback_enabled',
14
+ 'phishing_resistant_only',
15
+ 'prompt_passkey_enrollment',
16
+ 'authenticator_policy',
17
+ 'lockout_policy',
18
+ 'recovery_codes',
19
+ 'account_recovery',
20
+ ] as const;
21
+
22
+ export const PolicyKeySchema = z.enum(POLICY_KEYS);
23
+
24
+ export type PolicyKey = z.infer<typeof PolicyKeySchema>;
25
+
26
+ export const PolicyDocumentSchema = SystemConfigSchema.pick({
27
+ login_methods: true,
28
+ passkey_login_fallback_enabled: true,
29
+ phishing_resistant_only: true,
30
+ prompt_passkey_enrollment: true,
31
+ authenticator_policy: true,
32
+ lockout_policy: true,
33
+ recovery_codes: true,
34
+ account_recovery: true,
35
+ });
36
+
37
+ export type PolicyDocument = z.infer<typeof PolicyDocumentSchema>;
38
+
39
+ /**
40
+ * How a version came to be. Drafts are optional: a whole-config PATCH that touches a
41
+ * policy key still applies immediately, and is recorded as a `config_patch` version so
42
+ * it can be rolled back like any other.
43
+ */
44
+ export const PolicyVersionSourceSchema = z.enum([
45
+ 'publish',
46
+ 'rollback',
47
+ 'config_patch',
48
+ 'bootstrap',
49
+ ]);
50
+
51
+ export type PolicyVersionSource = z.infer<typeof PolicyVersionSourceSchema>;
52
+
53
+ export const PolicyVersionSchema = z.object({
54
+ version: z.number().int().positive(),
55
+ policy: PolicyDocumentSchema,
56
+ source: PolicyVersionSourceSchema,
57
+ publishedAt: IsoDate,
58
+ /** User id of the administrator, or null for a version the server wrote itself. */
59
+ publishedBy: z.string().nullable(),
60
+ /** The version a rollback restored. */
61
+ rolledBackTo: z.number().int().positive().optional(),
62
+ });
63
+
64
+ export type PolicyVersion = z.infer<typeof PolicyVersionSchema>;
65
+
66
+ export const PolicyDraftSchema = z.object({
67
+ policy: PolicyDocumentSchema,
68
+ /** The published version the draft was started from. */
69
+ basedOnVersion: z.number().int().positive(),
70
+ updatedAt: IsoDate,
71
+ updatedBy: z.string().nullable(),
72
+ });
73
+
74
+ export type PolicyDraft = z.infer<typeof PolicyDraftSchema>;
75
+
76
+ /** `GET /system-config/policy`. */
77
+ export const PolicyStateResponseSchema = z.object({
78
+ published: PolicyVersionSchema,
79
+ draft: PolicyDraftSchema.nullable(),
80
+ });
81
+
82
+ export type PolicyStateResponse = z.infer<typeof PolicyStateResponseSchema>;
83
+
84
+ /** `PUT /system-config/policy/draft`. */
85
+ export const PolicyDraftSaveRequestSchema = z
86
+ .object({
87
+ policy: PolicyDocumentSchema,
88
+ })
89
+ .strict();
90
+
91
+ export type PolicyDraftSaveRequest = z.infer<typeof PolicyDraftSaveRequestSchema>;
92
+
93
+ /**
94
+ * `POST /system-config/policy/publish`. `expectedVersion` is the published version the
95
+ * caller reviewed; publishing answers `409` if another version was published since, so
96
+ * a stale draft cannot silently undo someone else's change.
97
+ */
98
+ export const PolicyPublishRequestSchema = z
99
+ .object({
100
+ expectedVersion: z.number().int().positive(),
101
+ })
102
+ .strict();
103
+
104
+ export type PolicyPublishRequest = z.infer<typeof PolicyPublishRequestSchema>;
105
+
106
+ /** `POST /system-config/policy/rollback`: publishes a copy of `version` as a new version. */
107
+ export const PolicyRollbackRequestSchema = z
108
+ .object({
109
+ version: z.number().int().positive(),
110
+ expectedVersion: z.number().int().positive(),
111
+ })
112
+ .strict();
113
+
114
+ export type PolicyRollbackRequest = z.infer<typeof PolicyRollbackRequestSchema>;
115
+
116
+ /** `GET /system-config/policy/versions`, newest first. */
117
+ export const PolicyVersionsResponseSchema = z.object({
118
+ versions: z.array(PolicyVersionSchema),
119
+ });
120
+
121
+ export type PolicyVersionsResponse = z.infer<typeof PolicyVersionsResponseSchema>;
122
+
123
+ /**
124
+ * `POST /system-config/policy/dry-run`. Without `policy`, evaluates the current draft.
125
+ */
126
+ export const PolicyDryRunRequestSchema = z
127
+ .object({
128
+ policy: PolicyDocumentSchema.optional(),
129
+ days: z.number().int().min(1).max(30).default(7),
130
+ })
131
+ .strict();
132
+
133
+ export type PolicyDryRunRequest = z.infer<typeof PolicyDryRunRequestSchema>;
134
+
135
+ export const PolicyDryRunReasonSchema = z.enum([
136
+ 'method_disabled',
137
+ 'phishing_resistant_required',
138
+ 'fallback_disabled',
139
+ 'synced_passkey_blocked',
140
+ 'authenticator_not_allowed',
141
+ 'user_verification_required',
142
+ ]);
143
+
144
+ export type PolicyDryRunReason = z.infer<typeof PolicyDryRunReasonSchema>;
145
+
146
+ export const PolicyDryRunUserSchema = z.object({
147
+ userId: z.string(),
148
+ email: z.string(),
149
+ /** Sign-ins in the window this policy would have refused. */
150
+ signIns: z.number().int().nonnegative(),
151
+ reasons: z.array(PolicyDryRunReasonSchema).min(1),
152
+ /** True when none of the user's enrolled factors would still sign them in. */
153
+ lockedOut: z.boolean(),
154
+ });
155
+
156
+ export type PolicyDryRunUser = z.infer<typeof PolicyDryRunUserSchema>;
157
+
158
+ export const PolicyDryRunResponseSchema = z.object({
159
+ window: z.object({ from: IsoDate, to: IsoDate }),
160
+ signInsEvaluated: z.number().int().nonnegative(),
161
+ usersEvaluated: z.number().int().nonnegative(),
162
+ wouldFail: z.array(PolicyDryRunUserSchema),
163
+ usersLockedOut: z.number().int().nonnegative(),
164
+ /** Break-glass accounts are exempt from policy, so they are counted, never refused. */
165
+ breakGlassAccounts: z.number().int().nonnegative(),
166
+ });
167
+
168
+ export type PolicyDryRunResponse = z.infer<typeof PolicyDryRunResponseSchema>;
@@ -0,0 +1,117 @@
1
+ import { z } from 'zod';
2
+
3
+ import { IsoDate } from '../../shared.js';
4
+ import { NextStepSchema, RefreshSuccessResponseSchema } from '../auth/auth.schema.js';
5
+ import { AuthDeliverySchema } from '../messaging/schema.js';
6
+
7
+ /**
8
+ * Ten base32 characters, shown as `XXXXX-XXXXX`. Accepted with or without the dash
9
+ * and in either case, because people copy them from paper.
10
+ */
11
+ export const RecoveryCodeSchema = z
12
+ .string()
13
+ .trim()
14
+ .regex(/^[A-Za-z2-7]{5}-?[A-Za-z2-7]{5}$/, { message: 'Not a recovery code' });
15
+
16
+ export type RecoveryCode = z.infer<typeof RecoveryCodeSchema>;
17
+
18
+ /** `GET /recovery-codes`. `createdAt` is null until the user has generated a set. */
19
+ export const RecoveryCodesStatusSchema = z.object({
20
+ enabled: z.boolean(),
21
+ remaining: z.number().int().nonnegative(),
22
+ total: z.number().int().nonnegative(),
23
+ createdAt: IsoDate.nullable(),
24
+ });
25
+
26
+ export type RecoveryCodesStatus = z.infer<typeof RecoveryCodesStatusSchema>;
27
+
28
+ /**
29
+ * `POST /recovery-codes`. The only time the codes are ever returned; the server keeps
30
+ * them hashed. Generating a set invalidates the previous one.
31
+ */
32
+ export const RecoveryCodesGeneratedSchema = z.object({
33
+ message: z.string(),
34
+ codes: z.array(RecoveryCodeSchema).min(1),
35
+ createdAt: IsoDate,
36
+ });
37
+
38
+ export type RecoveryCodesGenerated = z.infer<typeof RecoveryCodesGeneratedSchema>;
39
+
40
+ /** `POST /recovery-codes/redeem`, with the pre-auth token from `/login`. */
41
+ export const RecoveryCodeRedeemRequestSchema = z.object({
42
+ code: RecoveryCodeSchema,
43
+ });
44
+
45
+ export type RecoveryCodeRedeemRequest = z.infer<typeof RecoveryCodeRedeemRequestSchema>;
46
+
47
+ /**
48
+ * The session a recovery issues. `recovery: true` marks a restricted session: until
49
+ * the user enrolls a new passkey or TOTP, the API answers `403 enrollment_required` on
50
+ * everything but enrollment, recovery codes, `/users/me` and logout, and the server
51
+ * adapters' `requireAuth` refuse it unless told otherwise. Enrolling rotates it into a
52
+ * normal session.
53
+ */
54
+ export const RecoverySessionResponseSchema = RefreshSuccessResponseSchema.omit({
55
+ sessionId: true,
56
+ }).extend({
57
+ recovery: z.literal(true),
58
+ nextStep: NextStepSchema,
59
+ });
60
+
61
+ export type RecoverySessionResponse = z.infer<typeof RecoverySessionResponseSchema>;
62
+
63
+ export const AccountRecoveryChannelSchema = z.enum(['email', 'phone']);
64
+
65
+ export type AccountRecoveryChannel = z.infer<typeof AccountRecoveryChannelSchema>;
66
+
67
+ /**
68
+ * `POST /recovery/start`, with the pre-auth token from `/login`. Sends a code to the
69
+ * verified address on file for that channel. The answer is the same whether or not
70
+ * the account has one, so it does not say which channels an account has.
71
+ */
72
+ export const AccountRecoveryStartRequestSchema = z.object({
73
+ channel: AccountRecoveryChannelSchema.default('email'),
74
+ });
75
+
76
+ export type AccountRecoveryStartRequest = z.infer<typeof AccountRecoveryStartRequestSchema>;
77
+
78
+ export const AccountRecoveryStartResponseSchema = z.object({
79
+ message: z.string(),
80
+ delivery: AuthDeliverySchema.optional(),
81
+ });
82
+
83
+ export type AccountRecoveryStartResponse = z.infer<typeof AccountRecoveryStartResponseSchema>;
84
+
85
+ /** `POST /recovery/verify`: the code from `/recovery/start` opens the request. */
86
+ export const AccountRecoveryVerifyRequestSchema = z.object({
87
+ code: z.string().trim().min(1),
88
+ });
89
+
90
+ export type AccountRecoveryVerifyRequest = z.infer<typeof AccountRecoveryVerifyRequestSchema>;
91
+
92
+ export const AccountRecoveryStateSchema = z.enum(['pending', 'ready', 'cancelled', 'completed']);
93
+
94
+ export type AccountRecoveryState = z.infer<typeof AccountRecoveryStateSchema>;
95
+
96
+ /**
97
+ * `POST /recovery/verify` and `GET /recovery/status`. `pending` until `completesAt`,
98
+ * then `ready`; every other channel and active session was told when it opened, and
99
+ * any of them can cancel it.
100
+ */
101
+ export const AccountRecoveryStatusSchema = z.object({
102
+ state: AccountRecoveryStateSchema,
103
+ requestedAt: IsoDate,
104
+ completesAt: IsoDate,
105
+ });
106
+
107
+ export type AccountRecoveryStatus = z.infer<typeof AccountRecoveryStatusSchema>;
108
+
109
+ /**
110
+ * `POST /recovery/cancel`. Either the token from the notice sent to the other
111
+ * channels, or no body with an access session for the account.
112
+ */
113
+ export const AccountRecoveryCancelRequestSchema = z.object({
114
+ cancelToken: z.string().trim().min(1).optional(),
115
+ });
116
+
117
+ export type AccountRecoveryCancelRequest = z.infer<typeof AccountRecoveryCancelRequestSchema>;
@@ -45,6 +45,24 @@ export const RedirectTargetSchema = z.url().refine(
45
45
 
46
46
  export type RedirectTarget = z.infer<typeof RedirectTargetSchema>;
47
47
 
48
+ // Provider endpoints the server fetches, and the authorization URL the browser is sent
49
+ // to. `z.url()` alone accepts any scheme, including `javascript:`.
50
+ const ProviderUrlSchema = z.url({ protocol: /^https?$/ });
51
+
52
+ /**
53
+ * The auth server reads a provider's secret from this env var, so the name is held to
54
+ * the shape of an OAuth client secret: configuration cannot point the token exchange at
55
+ * any other secret the server holds (GHSA-qx82-28gf-w744).
56
+ */
57
+ export const OAuthClientSecretEnvSchema = z
58
+ .string()
59
+ .trim()
60
+ .regex(/^[A-Z][A-Z0-9_]{0,59}_CLIENT_SECRET$/, {
61
+ message: 'Must be an env var name ending in _CLIENT_SECRET, for example GOOGLE_CLIENT_SECRET',
62
+ });
63
+
64
+ export type OAuthClientSecretEnv = z.infer<typeof OAuthClientSecretEnvSchema>;
65
+
48
66
  export const OAuthProviderConfigSchema = z.object({
49
67
  id: OAuthProviderIdSchema,
50
68
  name: z.string().trim().min(1).max(80),
@@ -52,10 +70,10 @@ export const OAuthProviderConfigSchema = z.object({
52
70
  clientId: z.string().trim().min(1),
53
71
  // The secret itself never lives in config; this names the environment
54
72
  // variable the server reads it from.
55
- clientSecretEnv: z.string().trim().min(1),
56
- authorizationUrl: z.url(),
57
- tokenUrl: z.url(),
58
- userInfoUrl: z.url(),
73
+ clientSecretEnv: OAuthClientSecretEnvSchema,
74
+ authorizationUrl: ProviderUrlSchema,
75
+ tokenUrl: ProviderUrlSchema,
76
+ userInfoUrl: ProviderUrlSchema,
59
77
  scopes: z.array(z.string().trim().min(1)).default([]),
60
78
  redirectUri: z.url().optional(),
61
79
  redirectUris: z.array(z.url()).default([]),
@@ -73,7 +91,7 @@ export const OAuthProviderConfigSchema = z.object({
73
91
  * read for the profile claims instead of calling `userInfoUrl`.
74
92
  */
75
93
  issuer: z.url().optional(),
76
- jwksUri: z.url().optional(),
94
+ jwksUri: ProviderUrlSchema.optional(),
77
95
  /**
78
96
  * Links a first sign-in to a user imported under this source (see
79
97
  * `ImportUsersRequestSchema`) whose external id equals the ID token claim at
@@ -277,6 +295,41 @@ export const DefaultAuthenticatorPolicy = {
277
295
 
278
296
  const DurationSchema = z.string().regex(/^\d+[smhd]$/);
279
297
 
298
+ const DURATION_SECONDS = { s: 1, m: 60, h: 3600, d: 86400 } as const;
299
+
300
+ function durationSeconds(value: string) {
301
+ const unit = value.slice(-1) as keyof typeof DURATION_SECONDS;
302
+ return Number(value.slice(0, -1)) * DURATION_SECONDS[unit];
303
+ }
304
+
305
+ /**
306
+ * Single-use recovery codes. On by default: a user only has codes after generating
307
+ * them, so the switch only matters once someone has. Not a login method, so no
308
+ * `login_methods` change can take a user's way back in away from them.
309
+ */
310
+ export const RecoveryCodesPolicySchema = z.object({
311
+ enabled: z.boolean().default(true),
312
+ /** Ask for a set of codes as part of first passkey enrollment. */
313
+ requireAtEnrollment: z.boolean().default(false),
314
+ });
315
+
316
+ export type RecoveryCodesPolicy = z.infer<typeof RecoveryCodesPolicySchema>;
317
+
318
+ /**
319
+ * Self-service recovery through a verified email or phone, completed only after
320
+ * `delay` and announced on every other channel and session so the owner can cancel.
321
+ * Off by default: it is a way into an account that does not need its factors.
322
+ */
323
+ export const AccountRecoveryPolicySchema = z.object({
324
+ enabled: z.boolean().default(false),
325
+ delay: DurationSchema.refine(
326
+ (value) => durationSeconds(value) >= 3600 && durationSeconds(value) <= 30 * 86400,
327
+ { message: 'delay must be between 1h and 30d' },
328
+ ).default('24h'),
329
+ });
330
+
331
+ export type AccountRecoveryPolicy = z.infer<typeof AccountRecoveryPolicySchema>;
332
+
280
333
  export const SystemConfigSchema = z.object({
281
334
  app_name: z.string().min(3),
282
335
  default_roles: z.array(RoleNameSchema).min(1),
@@ -305,6 +358,10 @@ export const SystemConfigSchema = z.object({
305
358
  authenticator_policy: AuthenticatorPolicySchema.default(() =>
306
359
  AuthenticatorPolicySchema.parse({}),
307
360
  ),
361
+ recovery_codes: RecoveryCodesPolicySchema.default(() => RecoveryCodesPolicySchema.parse({})),
362
+ account_recovery: AccountRecoveryPolicySchema.default(() =>
363
+ AccountRecoveryPolicySchema.parse({}),
364
+ ),
308
365
 
309
366
  access_token_ttl: z.string().regex(/^\d+[smhd]$/),
310
367
 
@@ -377,6 +434,8 @@ export const SystemConfigPatchSchema = z
377
434
  oauth_providers: z.array(OAuthProviderConfigSchema).optional(),
378
435
  lockout_policy: LockoutPolicySchema.optional(),
379
436
  authenticator_policy: AuthenticatorPolicySchema.optional(),
437
+ recovery_codes: RecoveryCodesPolicySchema.optional(),
438
+ account_recovery: AccountRecoveryPolicySchema.optional(),
380
439
  access_token_ttl: SystemConfigSchema.shape.access_token_ttl.optional(),
381
440
  session_idle_ttl: DurationSchema.optional(),
382
441
  refresh_token_ttl: SystemConfigSchema.shape.refresh_token_ttl.optional(),
@@ -511,6 +570,10 @@ export type AvailableRolesResponse = z.infer<typeof AvailableRolesResponseSchema
511
570
  */
512
571
  export const PublicSystemConfigResponseSchema = z.object({
513
572
  loginMethods: z.array(LoginMethodSchema).min(1),
573
+ // Optional so a client still parses a server from before v0.20. Lets the sign-in
574
+ // screens offer "use a recovery code" and "can't sign in?" only where they work.
575
+ recoveryCodes: z.boolean().optional(),
576
+ accountRecovery: z.boolean().optional(),
514
577
  });
515
578
 
516
579
  export type PublicSystemConfigResponse = z.infer<typeof PublicSystemConfigResponseSchema>;