@seamless-auth/types 0.24.0 → 0.26.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 (51) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +202 -661
  3. package/README.md +2 -2
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/schemas/auth/auth.schema.d.ts +27 -0
  9. package/dist/schemas/auth/auth.schema.d.ts.map +1 -1
  10. package/dist/schemas/auth/auth.schema.js +10 -0
  11. package/dist/schemas/auth/auth.schema.js.map +1 -1
  12. package/dist/schemas/authEvent/schema.d.ts +10 -1
  13. package/dist/schemas/authEvent/schema.d.ts.map +1 -1
  14. package/dist/schemas/authEvent/schema.js +3 -0
  15. package/dist/schemas/authEvent/schema.js.map +1 -1
  16. package/dist/schemas/common/schema.d.ts +2 -0
  17. package/dist/schemas/common/schema.d.ts.map +1 -1
  18. package/dist/schemas/common/schema.js +3 -0
  19. package/dist/schemas/common/schema.js.map +1 -1
  20. package/dist/schemas/enrollment/schema.d.ts +171 -0
  21. package/dist/schemas/enrollment/schema.d.ts.map +1 -0
  22. package/dist/schemas/enrollment/schema.js +89 -0
  23. package/dist/schemas/enrollment/schema.js.map +1 -0
  24. package/dist/schemas/me/schema.d.ts +2 -0
  25. package/dist/schemas/me/schema.d.ts.map +1 -1
  26. package/dist/schemas/messaging/schema.d.ts +4 -0
  27. package/dist/schemas/messaging/schema.d.ts.map +1 -1
  28. package/dist/schemas/messaging/schema.js +6 -0
  29. package/dist/schemas/messaging/schema.js.map +1 -1
  30. package/dist/schemas/oauth/schema.d.ts +8 -1
  31. package/dist/schemas/oauth/schema.d.ts.map +1 -1
  32. package/dist/schemas/oauth/schema.js +13 -2
  33. package/dist/schemas/oauth/schema.js.map +1 -1
  34. package/dist/schemas/organization/schema.d.ts +10 -0
  35. package/dist/schemas/organization/schema.d.ts.map +1 -1
  36. package/dist/schemas/organization/schema.js +9 -1
  37. package/dist/schemas/organization/schema.js.map +1 -1
  38. package/dist/schemas/systemConfig/schema.d.ts +23 -12
  39. package/dist/schemas/systemConfig/schema.d.ts.map +1 -1
  40. package/dist/schemas/systemConfig/schema.js +27 -2
  41. package/dist/schemas/systemConfig/schema.js.map +1 -1
  42. package/package.json +2 -2
  43. package/src/index.ts +1 -0
  44. package/src/schemas/auth/auth.schema.ts +13 -0
  45. package/src/schemas/authEvent/schema.ts +3 -0
  46. package/src/schemas/common/schema.ts +6 -0
  47. package/src/schemas/enrollment/schema.ts +118 -0
  48. package/src/schemas/messaging/schema.ts +6 -0
  49. package/src/schemas/oauth/schema.ts +13 -2
  50. package/src/schemas/organization/schema.ts +12 -1
  51. package/src/schemas/systemConfig/schema.ts +34 -5
@@ -0,0 +1,118 @@
1
+ import { z } from 'zod';
2
+ import { IsoDate } from '../../shared.js';
3
+ import { PaginationQuerySchema } from '../common/schema.js';
4
+ import { AuthDeliverySchema } from '../messaging/schema.js';
5
+ import { RedirectTargetSchema } from '../systemConfig/schema.js';
6
+
7
+ /**
8
+ * How far a user is through passkey enrollment, counted in WebAuthn credentials
9
+ * (passkeys and security keys; TOTP does not count). Two or more is the goal, so
10
+ * losing one authenticator does not lock the user out.
11
+ */
12
+ export const EnrollmentStatusSchema = z.enum(['none', 'one', 'two_or_more']);
13
+
14
+ export type EnrollmentStatus = z.infer<typeof EnrollmentStatusSchema>;
15
+
16
+ // Accepts the parsed boolean as well as the query string, so parsing an already
17
+ // parsed query (as the server does to recover coerced types) gives the same answer.
18
+ const BooleanQuerySchema = z.union([
19
+ z.boolean(),
20
+ z.enum(['true', 'false']).transform((value) => value === 'true'),
21
+ ]);
22
+
23
+ export const AdminEnrollmentQuerySchema = PaginationQuerySchema.extend({
24
+ organizationId: z.uuid().optional(),
25
+ status: EnrollmentStatusSchema.optional(),
26
+ /** Only users brought in through `POST /admin/users/import`. */
27
+ imported: BooleanQuerySchema.optional(),
28
+ search: z.string().trim().min(1).max(120).optional(),
29
+ });
30
+
31
+ export type AdminEnrollmentQuery = z.infer<typeof AdminEnrollmentQuerySchema>;
32
+
33
+ export const EnrollmentSummarySchema = z.object({
34
+ total: z.number().int().nonnegative(),
35
+ none: z.number().int().nonnegative(),
36
+ one: z.number().int().nonnegative(),
37
+ twoOrMore: z.number().int().nonnegative(),
38
+ });
39
+
40
+ export type EnrollmentSummary = z.infer<typeof EnrollmentSummarySchema>;
41
+
42
+ export const EnrollmentUserSchema = z.object({
43
+ id: z.string(),
44
+ email: z.string(),
45
+ imported: z.boolean(),
46
+ credentialCount: z.number().int().nonnegative(),
47
+ status: EnrollmentStatusSchema,
48
+ lastLogin: IsoDate.nullable(),
49
+ enrollmentInvitedAt: IsoDate.nullable(),
50
+ });
51
+
52
+ export type EnrollmentUser = z.infer<typeof EnrollmentUserSchema>;
53
+
54
+ /**
55
+ * `summary` counts every user matching `organizationId` and `imported`, whatever the
56
+ * `status` filter; `users` and `total` are the filtered, paginated list.
57
+ */
58
+ export const AdminEnrollmentResponseSchema = z.object({
59
+ summary: EnrollmentSummarySchema,
60
+ users: z.array(EnrollmentUserSchema),
61
+ total: z.number().int().nonnegative(),
62
+ });
63
+
64
+ export type AdminEnrollmentResponse = z.infer<typeof AdminEnrollmentResponseSchema>;
65
+
66
+ export const ENROLLMENT_INVITE_MAX_USERS = 200;
67
+
68
+ /**
69
+ * Invites either the listed users or, with `organizationId`, the organization's members
70
+ * whose enrollment is at or below `status`. An organization-wide call sends at most
71
+ * `ENROLLMENT_INVITE_MAX_USERS` and reports how many remain, skipping anyone invited
72
+ * within the last day, so repeating it works through a large organization.
73
+ */
74
+ export const EnrollmentInviteRequestSchema = z
75
+ .object({
76
+ userIds: z.array(z.uuid()).min(1).max(ENROLLMENT_INVITE_MAX_USERS).optional(),
77
+ organizationId: z.uuid().optional(),
78
+ status: z.enum(['none', 'one']).default('none'),
79
+ /** Where the email sends the user. Defaults to the tenant's sign-in page. */
80
+ signInUrl: RedirectTargetSchema.optional(),
81
+ })
82
+ .strict()
83
+ .refine((value) => Boolean(value.userIds) !== Boolean(value.organizationId), {
84
+ message: 'exactly one of userIds or organizationId is required',
85
+ });
86
+
87
+ export type EnrollmentInviteRequest = z.infer<typeof EnrollmentInviteRequestSchema>;
88
+
89
+ export const ENROLLMENT_INVITE_SKIP_REASONS = [
90
+ 'not_found',
91
+ 'already_enrolled',
92
+ 'recently_invited',
93
+ 'delivery_failed',
94
+ ] as const;
95
+
96
+ export const EnrollmentInviteSkipReasonSchema = z.enum(ENROLLMENT_INVITE_SKIP_REASONS);
97
+
98
+ export type EnrollmentInviteSkipReason = z.infer<typeof EnrollmentInviteSkipReasonSchema>;
99
+
100
+ export const EnrollmentInviteResultSchema = z.object({
101
+ userId: z.string(),
102
+ status: z.enum(['sent', 'skipped']),
103
+ reason: EnrollmentInviteSkipReasonSchema.optional(),
104
+ /** Present in external delivery mode, for the caller to send itself. */
105
+ delivery: AuthDeliverySchema.optional(),
106
+ });
107
+
108
+ export type EnrollmentInviteResult = z.infer<typeof EnrollmentInviteResultSchema>;
109
+
110
+ export const EnrollmentInviteResponseSchema = z.object({
111
+ sent: z.number().int().nonnegative(),
112
+ skipped: z.number().int().nonnegative(),
113
+ /** Organization-wide calls only: eligible members left for a later call. */
114
+ remaining: z.number().int().nonnegative().optional(),
115
+ results: z.array(EnrollmentInviteResultSchema),
116
+ });
117
+
118
+ export type EnrollmentInviteResponse = z.infer<typeof EnrollmentInviteResponseSchema>;
@@ -81,6 +81,12 @@ export const AuthDeliverySchema = z.discriminatedUnion('kind', [
81
81
  token: z.string().optional(),
82
82
  magicLinkUrl: z.string(),
83
83
  }),
84
+ z.object({
85
+ kind: z.literal('enrollment_invite_email'),
86
+ to: z.string(),
87
+ // The sign-in page. It carries no credential, so it signs nobody in on its own.
88
+ signInUrl: z.string(),
89
+ }),
84
90
  ]);
85
91
 
86
92
  export type AuthDeliveryInstruction = z.infer<typeof AuthDeliverySchema>;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
- import { RefreshSuccessResponseSchema } from '../auth/auth.schema.js';
3
- import { OAuthProviderIdSchema, RedirectTargetSchema } from '../systemConfig/schema.js';
2
+ import { NextStepSchema, RefreshSuccessResponseSchema } from '../auth/auth.schema.js';
3
+ import { OAuthProviderIdSchema } from '../common/schema.js';
4
+ import { RedirectTargetSchema } from '../systemConfig/schema.js';
4
5
 
5
6
  export const OAuthProviderParamSchema = z.object({
6
7
  providerId: OAuthProviderIdSchema,
@@ -58,6 +59,13 @@ export const OAuthLoginSuccessResponseSchema = RefreshSuccessResponseSchema.omit
58
59
  * as an error.
59
60
  */
60
61
  returnTo: RedirectTargetSchema.optional(),
62
+ /**
63
+ * Present when the provider has `promptPasskeyEnrollment` set and the user has no
64
+ * passkey. The session in this response is a full access session, so the client sends
65
+ * the user into passkey enrollment before the app. Absent means there is nothing
66
+ * further to do.
67
+ */
68
+ nextStep: NextStepSchema.optional(),
61
69
  });
62
70
 
63
71
  export type OAuthLoginSuccessResponse = z.infer<typeof OAuthLoginSuccessResponseSchema>;
@@ -66,6 +74,9 @@ export const OAUTH_ERROR_CODES = [
66
74
  'oauth_missing_email',
67
75
  'oauth_email_not_verified',
68
76
  'oauth_missing_subject',
77
+ 'oauth_invalid_id_token',
78
+ // A member of an organization that has retired this provider. Answered with 403.
79
+ 'oauth_provider_retired',
69
80
  ] as const;
70
81
 
71
82
  export const OAuthErrorCodeSchema = z.enum(OAUTH_ERROR_CODES);
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { IsoDate } from '../../shared.js';
3
- import { MetadataSchema } from '../common/schema.js';
3
+ import { MetadataSchema, OAuthProviderIdSchema } from '../common/schema.js';
4
4
 
5
5
  export const OrganizationIdParamSchema = z.object({
6
6
  organizationId: z.uuid(),
@@ -8,6 +8,12 @@ export const OrganizationIdParamSchema = z.object({
8
8
 
9
9
  export type OrganizationIdParam = z.infer<typeof OrganizationIdParamSchema>;
10
10
 
11
+ export const OrganizationOAuthProviderParamSchema = OrganizationIdParamSchema.extend({
12
+ providerId: OAuthProviderIdSchema,
13
+ });
14
+
15
+ export type OrganizationOAuthProviderParam = z.infer<typeof OrganizationOAuthProviderParamSchema>;
16
+
11
17
  export const OrganizationMemberParamSchema = OrganizationIdParamSchema.extend({
12
18
  userId: z.uuid(),
13
19
  });
@@ -86,6 +92,11 @@ export const OrganizationSchema = z.object({
86
92
  updatedAt: IsoDate,
87
93
  membership: OrganizationMembershipSchema.optional(),
88
94
  memberCount: z.number().int().nonnegative().optional(),
95
+ /**
96
+ * OAuth providers this organization's members can no longer sign in with, for cutting
97
+ * over from a legacy identity provider.
98
+ */
99
+ retiredOAuthProviders: z.array(OAuthProviderIdSchema).optional(),
89
100
  });
90
101
 
91
102
  export type Organization = z.infer<typeof OrganizationSchema>;
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { OAuthProviderIdSchema } from '../common/schema.js';
2
3
  import { UserImportSourceSchema } from '../userImport/schema.js';
3
4
  import { RoleNameSchema } from '../role/schema.js';
4
5
 
@@ -44,10 +45,6 @@ export const RedirectTargetSchema = z.url().refine(
44
45
 
45
46
  export type RedirectTarget = z.infer<typeof RedirectTargetSchema>;
46
47
 
47
- export const OAuthProviderIdSchema = z.string().regex(/^[a-z0-9-]{2,40}$/);
48
-
49
- export type OAuthProviderId = z.infer<typeof OAuthProviderIdSchema>;
50
-
51
48
  export const OAuthProviderConfigSchema = z.object({
52
49
  id: OAuthProviderIdSchema,
53
50
  name: z.string().trim().min(1).max(80),
@@ -85,6 +82,12 @@ export const OAuthProviderConfigSchema = z.object({
85
82
  */
86
83
  externalIdSource: UserImportSourceSchema.optional(),
87
84
  externalIdJsonPath: z.string().trim().min(1).optional(),
85
+ /**
86
+ * After a sign-in through this provider by a user with no passkey, the callback
87
+ * response carries `nextStep: 'enroll_passkey'`. Meant for a legacy identity provider
88
+ * that users are being moved off.
89
+ */
90
+ promptPasskeyEnrollment: z.boolean().default(false),
88
91
  });
89
92
 
90
93
  export type OAuthProviderConfig = z.infer<typeof OAuthProviderConfigSchema>;
@@ -280,6 +283,13 @@ export const SystemConfigSchema = z.object({
280
283
  available_roles: z.array(RoleNameSchema).min(1),
281
284
  login_methods: z.array(LoginMethodSchema).min(1),
282
285
  passkey_login_fallback_enabled: z.boolean(),
286
+ /**
287
+ * After an email or phone code sign-in, or a magic link sign-in, by a user with no
288
+ * passkey, the session response carries `nextStep: 'enroll_passkey'`. For moving an
289
+ * organization onto passkeys, for example after importing its users. OAuth
290
+ * providers have their own `promptPasskeyEnrollment`.
291
+ */
292
+ prompt_passkey_enrollment: z.boolean().default(false),
283
293
  oauth_providers: z.array(OAuthProviderConfigSchema).default([]),
284
294
  lockout_policy: LockoutPolicySchema.default(DefaultLockoutPolicy),
285
295
  // Derived from the field defaults rather than restated, so the two cannot drift.
@@ -354,6 +364,7 @@ export const SystemConfigPatchSchema = z
354
364
  login_methods: SystemConfigSchema.shape.login_methods.optional(),
355
365
  passkey_login_fallback_enabled:
356
366
  SystemConfigSchema.shape.passkey_login_fallback_enabled.optional(),
367
+ prompt_passkey_enrollment: z.boolean().optional(),
357
368
  oauth_providers: z.array(OAuthProviderConfigSchema).optional(),
358
369
  lockout_policy: LockoutPolicySchema.optional(),
359
370
  authenticator_policy: AuthenticatorPolicySchema.optional(),
@@ -413,10 +424,28 @@ export const OAuthProviderIdParamSchema = z.object({
413
424
 
414
425
  export type OAuthProviderIdParam = z.infer<typeof OAuthProviderIdParamSchema>;
415
426
 
427
+ type WithoutDefaults<Shape extends z.ZodRawShape> = {
428
+ [Key in keyof Shape]: Shape[Key] extends z.ZodDefault<infer Inner> ? Inner : Shape[Key];
429
+ };
430
+
431
+ function withoutDefaults<Shape extends z.ZodRawShape>(shape: Shape): WithoutDefaults<Shape> {
432
+ return Object.fromEntries(
433
+ Object.entries(shape).map(([key, field]) => [
434
+ key,
435
+ field instanceof z.ZodDefault ? field.unwrap() : field,
436
+ ]),
437
+ ) as WithoutDefaults<Shape>;
438
+ }
439
+
416
440
  // The id is immutable and taken from the path, so it is omitted here. Every other
417
441
  // field is optional so callers can patch a single attribute without resending the
418
442
  // whole provider; the merged result is re-validated against the full schema.
419
- export const OAuthProviderUpdateSchema = OAuthProviderConfigSchema.omit({ id: true })
443
+ //
444
+ // Defaults are stripped first: `.partial()` keeps them, so a patch of one field would
445
+ // parse to that field plus every default, and merging that over the stored provider
446
+ // silently reset settings such as `accountLinking` and `allowSignup`.
447
+ export const OAuthProviderUpdateSchema = z
448
+ .object(withoutDefaults(OAuthProviderConfigSchema.omit({ id: true }).shape))
420
449
  .partial()
421
450
  .strict();
422
451