@logto/schemas 1.37.1 → 1.39.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 (73) hide show
  1. package/alterations/1.38.0-1772615848-add-oidc-model-instances-grant-id-partial-index.ts +26 -0
  2. package/alterations/1.38.0-1772619963-tune-oidc-model-instances-autovacuum.ts +28 -0
  3. package/alterations/1.38.0-1772621060-add-oidc-model-instances-grant-account-id-index.ts +26 -0
  4. package/alterations/1.39.0-1774752400-add-delete-account-url.ts +20 -0
  5. package/alterations/1.39.0-1774770686-add-account-center-custom-css.ts +20 -0
  6. package/alterations/1.39.0-1776502301-add-sign-up-profile-fields.ts +20 -0
  7. package/alterations-js/1.38.0-1772615848-add-oidc-model-instances-grant-id-partial-index.js +22 -0
  8. package/alterations-js/1.38.0-1772619963-tune-oidc-model-instances-autovacuum.js +24 -0
  9. package/alterations-js/1.38.0-1772621060-add-oidc-model-instances-grant-account-id-index.js +22 -0
  10. package/alterations-js/1.39.0-1774752400-add-delete-account-url.js +16 -0
  11. package/alterations-js/1.39.0-1774770686-add-account-center-custom-css.js +16 -0
  12. package/alterations-js/1.39.0-1776502301-add-sign-up-profile-fields.js +16 -0
  13. package/lib/consts/cookie.d.ts +1 -0
  14. package/lib/consts/cookie.js +1 -0
  15. package/lib/consts/experience.d.ts +1 -0
  16. package/lib/consts/experience.js +1 -0
  17. package/lib/consts/oidc.d.ts +3 -0
  18. package/lib/consts/oidc.js +3 -0
  19. package/lib/consts/system.d.ts +4 -0
  20. package/lib/consts/system.js +4 -0
  21. package/lib/db-entries/account-center.d.ts +9 -1
  22. package/lib/db-entries/account-center.js +8 -0
  23. package/lib/db-entries/sign-in-experience.d.ts +6 -2
  24. package/lib/db-entries/sign-in-experience.js +5 -1
  25. package/lib/foundations/jsonb-types/account-centers.d.ts +1 -0
  26. package/lib/foundations/jsonb-types/account-centers.js +8 -0
  27. package/lib/foundations/jsonb-types/oidc-module.d.ts +26 -7
  28. package/lib/foundations/jsonb-types/oidc-module.js +16 -1
  29. package/lib/foundations/jsonb-types/sign-in-experience.d.ts +36 -6
  30. package/lib/foundations/jsonb-types/sign-in-experience.js +10 -2
  31. package/lib/seeds/application.d.ts +3 -1
  32. package/lib/seeds/application.js +26 -1
  33. package/lib/types/alteration.d.ts +5 -0
  34. package/lib/types/application.d.ts +14 -2
  35. package/lib/types/connector.d.ts +8 -0
  36. package/lib/types/consent.d.ts +11 -3
  37. package/lib/types/consent.js +2 -1
  38. package/lib/types/custom-profile-fields.d.ts +7 -13
  39. package/lib/types/custom-profile-fields.js +6 -13
  40. package/lib/types/log/interaction.d.ts +4 -2
  41. package/lib/types/log/interaction.js +2 -0
  42. package/lib/types/log/token.d.ts +5 -3
  43. package/lib/types/log/token.js +2 -0
  44. package/lib/types/logto-config/index.d.ts +331 -15
  45. package/lib/types/logto-config/index.js +28 -4
  46. package/lib/types/logto-config/index.test.d.ts +1 -0
  47. package/lib/types/logto-config/index.test.js +29 -0
  48. package/lib/types/logto-config/jwt-customizer.d.ts +787 -253
  49. package/lib/types/logto-config/jwt-customizer.js +8 -3
  50. package/lib/types/logto-config/jwt-customizer.test.js +14 -2
  51. package/lib/types/oidc-config.d.ts +2 -1
  52. package/lib/types/oidc-config.js +1 -0
  53. package/lib/types/onboarding.d.ts +93 -1
  54. package/lib/types/onboarding.js +22 -1
  55. package/lib/types/sign-in-experience.d.ts +15 -4
  56. package/lib/types/user-logto-config.d.ts +49 -0
  57. package/lib/types/user-logto-config.js +19 -0
  58. package/lib/types/user-sessions.d.ts +712 -112
  59. package/lib/types/user-sessions.js +33 -2
  60. package/lib/types/verification-records/verification-type.d.ts +1 -1
  61. package/lib/types/verification-records/verification-type.js +1 -1
  62. package/lib/types/verification-records/web-authn-verification.d.ts +11 -11
  63. package/lib/types/verification-records/web-authn-verification.js +3 -3
  64. package/lib/utils/index.d.ts +1 -0
  65. package/lib/utils/index.js +1 -0
  66. package/lib/utils/oidc-private-key.d.ts +88 -0
  67. package/lib/utils/oidc-private-key.js +163 -0
  68. package/lib/utils/oidc-private-key.test.d.ts +1 -0
  69. package/lib/utils/oidc-private-key.test.js +128 -0
  70. package/package.json +9 -8
  71. package/tables/account_centers.sql +4 -0
  72. package/tables/oidc_model_instances.sql +16 -0
  73. package/tables/sign_in_experiences.sql +2 -0
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { OidcModelInstances } from '../db-entries/oidc-model-instance.js';
3
2
  import { oidcSessionInstancePayloadGuard } from '../foundations/index.js';
4
3
  import { jwtCustomizerUserInteractionContextGuard } from './logto-config/jwt-customizer.js';
5
4
  export const userSessionSignInContextGuard = z
@@ -14,13 +13,45 @@ export const userSessionSignInContextGuard = z
14
13
  botVerified: z.string().optional(),
15
14
  })
16
15
  .catchall(z.string());
17
- export const userExtendedSessionGuard = OidcModelInstances.guard.extend({
16
+ export var SessionGrantRevokeTarget;
17
+ (function (SessionGrantRevokeTarget) {
18
+ SessionGrantRevokeTarget["All"] = "all";
19
+ SessionGrantRevokeTarget["FirstParty"] = "firstParty";
20
+ })(SessionGrantRevokeTarget || (SessionGrantRevokeTarget = {}));
21
+ /**
22
+ * Public session shape for session management APIs.
23
+ *
24
+ * We intentionally expose only fields needed by management/account-center session views and actions.
25
+ * Internal OIDC storage fields (e.g. `tenantId`, `id`, `consumedAt`) are omitted on purpose.
26
+ */
27
+ export const userExtendedSessionGuard = z.object({
18
28
  payload: oidcSessionInstancePayloadGuard,
19
29
  lastSubmission: jwtCustomizerUserInteractionContextGuard.nullable(),
20
30
  clientId: z.string().nullable(),
21
31
  accountId: z.string().nullable(),
32
+ expiresAt: z.number(),
22
33
  });
23
34
  export const getUserSessionsResponseGuard = z.object({
24
35
  sessions: z.array(userExtendedSessionGuard),
25
36
  });
26
37
  export const getUserSessionResponseGuard = userExtendedSessionGuard;
38
+ export const userApplicationGrantPayloadGuard = z
39
+ .object({
40
+ /** Expiration time of the grant in seconds since the epoch */
41
+ exp: z.number(),
42
+ /** Issued at time of the grant in seconds since the epoch */
43
+ iat: z.number(),
44
+ jti: z.string(),
45
+ kind: z.literal('Grant'),
46
+ clientId: z.string(),
47
+ accountId: z.string(),
48
+ })
49
+ .catchall(z.unknown());
50
+ export const userApplicationGrantGuard = z.object({
51
+ id: z.string(),
52
+ payload: userApplicationGrantPayloadGuard,
53
+ expiresAt: z.number(),
54
+ });
55
+ export const getUserApplicationGrantsResponseGuard = z.object({
56
+ grants: z.array(userApplicationGrantGuard),
57
+ });
@@ -9,7 +9,7 @@ export declare enum VerificationType {
9
9
  EnterpriseSso = "EnterpriseSso",
10
10
  TOTP = "Totp",
11
11
  WebAuthn = "WebAuthn",
12
- SignInWebAuthn = "SignInWebAuthn",
12
+ SignInPasskey = "SignInPasskey",
13
13
  BackupCode = "BackupCode",
14
14
  NewPasswordIdentity = "NewPasswordIdentity",
15
15
  OneTimeToken = "OneTimeToken"
@@ -10,7 +10,7 @@ export var VerificationType;
10
10
  VerificationType["EnterpriseSso"] = "EnterpriseSso";
11
11
  VerificationType["TOTP"] = "Totp";
12
12
  VerificationType["WebAuthn"] = "WebAuthn";
13
- VerificationType["SignInWebAuthn"] = "SignInWebAuthn";
13
+ VerificationType["SignInPasskey"] = "SignInPasskey";
14
14
  VerificationType["BackupCode"] = "BackupCode";
15
15
  VerificationType["NewPasswordIdentity"] = "NewPasswordIdentity";
16
16
  VerificationType["OneTimeToken"] = "OneTimeToken";
@@ -139,13 +139,13 @@ export declare const sanitizedWebAuthnVerificationRecordDataGuard: z.ZodObject<O
139
139
  userId: string;
140
140
  verified: boolean;
141
141
  }>;
142
- export type SignInWebAuthnVerificationRecordData = BaseWebAuthnVerificationRecordData & {
143
- type: VerificationType.SignInWebAuthn;
142
+ export type SignInPasskeyVerificationRecordData = BaseWebAuthnVerificationRecordData & {
143
+ type: VerificationType.SignInPasskey;
144
144
  userId?: string;
145
145
  /** The rpId used when generating the authentication options */
146
146
  authenticationRpId?: string;
147
147
  };
148
- export declare const signInWebAuthnVerificationRecordDataGuard: z.ZodObject<{
148
+ export declare const signInPasskeyVerificationRecordDataGuard: z.ZodObject<{
149
149
  id: z.ZodString;
150
150
  verified: z.ZodBoolean;
151
151
  registrationChallenge: z.ZodOptional<z.ZodString>;
@@ -180,11 +180,11 @@ export declare const signInWebAuthnVerificationRecordDataGuard: z.ZodObject<{
180
180
  name?: string | undefined;
181
181
  }>>;
182
182
  } & {
183
- type: z.ZodLiteral<VerificationType.SignInWebAuthn>;
183
+ type: z.ZodLiteral<VerificationType.SignInPasskey>;
184
184
  userId: z.ZodOptional<z.ZodString>;
185
185
  authenticationRpId: z.ZodOptional<z.ZodString>;
186
186
  }, "strip", z.ZodTypeAny, {
187
- type: VerificationType.SignInWebAuthn;
187
+ type: VerificationType.SignInPasskey;
188
188
  id: string;
189
189
  verified: boolean;
190
190
  userId?: string | undefined;
@@ -203,7 +203,7 @@ export declare const signInWebAuthnVerificationRecordDataGuard: z.ZodObject<{
203
203
  } | undefined;
204
204
  authenticationRpId?: string | undefined;
205
205
  }, {
206
- type: VerificationType.SignInWebAuthn;
206
+ type: VerificationType.SignInPasskey;
207
207
  id: string;
208
208
  verified: boolean;
209
209
  userId?: string | undefined;
@@ -222,8 +222,8 @@ export declare const signInWebAuthnVerificationRecordDataGuard: z.ZodObject<{
222
222
  } | undefined;
223
223
  authenticationRpId?: string | undefined;
224
224
  }>;
225
- export type SanitizedSignInWebAuthnVerificationRecordData = Omit<SignInWebAuthnVerificationRecordData, 'registrationInfo' | 'registrationChallenge' | 'registrationRpId' | 'authenticationChallenge' | 'authenticationRpId'>;
226
- export declare const sanitizedSignInWebAuthnVerificationRecordDataGuard: z.ZodObject<Omit<{
225
+ export type SanitizedSignInPasskeyVerificationRecordData = Omit<SignInPasskeyVerificationRecordData, 'registrationInfo' | 'registrationChallenge' | 'registrationRpId' | 'authenticationChallenge' | 'authenticationRpId'>;
226
+ export declare const sanitizedSignInPasskeyVerificationRecordDataGuard: z.ZodObject<Omit<{
227
227
  id: z.ZodString;
228
228
  verified: z.ZodBoolean;
229
229
  registrationChallenge: z.ZodOptional<z.ZodString>;
@@ -258,16 +258,16 @@ export declare const sanitizedSignInWebAuthnVerificationRecordDataGuard: z.ZodOb
258
258
  name?: string | undefined;
259
259
  }>>;
260
260
  } & {
261
- type: z.ZodLiteral<VerificationType.SignInWebAuthn>;
261
+ type: z.ZodLiteral<VerificationType.SignInPasskey>;
262
262
  userId: z.ZodOptional<z.ZodString>;
263
263
  authenticationRpId: z.ZodOptional<z.ZodString>;
264
264
  }, "registrationChallenge" | "registrationRpId" | "authenticationChallenge" | "registrationInfo" | "authenticationRpId">, "strip", z.ZodTypeAny, {
265
- type: VerificationType.SignInWebAuthn;
265
+ type: VerificationType.SignInPasskey;
266
266
  id: string;
267
267
  verified: boolean;
268
268
  userId?: string | undefined;
269
269
  }, {
270
- type: VerificationType.SignInWebAuthn;
270
+ type: VerificationType.SignInPasskey;
271
271
  id: string;
272
272
  verified: boolean;
273
273
  userId?: string | undefined;
@@ -19,12 +19,12 @@ export const sanitizedWebAuthnVerificationRecordDataGuard = webAuthnVerification
19
19
  registrationRpId: true,
20
20
  authenticationChallenge: true,
21
21
  });
22
- export const signInWebAuthnVerificationRecordDataGuard = baseWebAuthnVerificationRecordDataGuard.extend({
23
- type: z.literal(VerificationType.SignInWebAuthn),
22
+ export const signInPasskeyVerificationRecordDataGuard = baseWebAuthnVerificationRecordDataGuard.extend({
23
+ type: z.literal(VerificationType.SignInPasskey),
24
24
  userId: z.string().optional(),
25
25
  authenticationRpId: z.string().optional(),
26
26
  });
27
- export const sanitizedSignInWebAuthnVerificationRecordDataGuard = signInWebAuthnVerificationRecordDataGuard.omit({
27
+ export const sanitizedSignInPasskeyVerificationRecordDataGuard = signInPasskeyVerificationRecordDataGuard.omit({
28
28
  registrationInfo: true,
29
29
  registrationChallenge: true,
30
30
  registrationRpId: true,
@@ -2,3 +2,4 @@ export * from './application.js';
2
2
  export * from './role.js';
3
3
  export * from './management-api.js';
4
4
  export * from './domain.js';
5
+ export * from './oidc-private-key.js';
@@ -2,3 +2,4 @@ export * from './application.js';
2
2
  export * from './role.js';
3
3
  export * from './management-api.js';
4
4
  export * from './domain.js';
5
+ export * from './oidc-private-key.js';
@@ -0,0 +1,88 @@
1
+ import type { LogtoOidcConfigType, OidcPrivateKey, SigningKeyRotationState } from '../types/index.js';
2
+ import { OidcSigningKeyStatus } from '../types/index.js';
3
+ export type NormalizedOidcPrivateKey = OidcPrivateKey & {
4
+ status: OidcSigningKeyStatus;
5
+ };
6
+ /**
7
+ * Normalize OIDC private signing keys into an explicit status-based model.
8
+ *
9
+ * Legacy keys without `status` are interpreted by index order:
10
+ * the first key becomes `Current` and the second key becomes `Previous`.
11
+ * The helper also validates that the key set contains exactly one `Current`
12
+ * and at most one `Next` and `Previous`.
13
+ */
14
+ export declare const normalizeOidcPrivateKeys: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"]) => NormalizedOidcPrivateKey[];
15
+ /**
16
+ * Return private keys in canonical business order: `Next`, then `Current`, then `Previous`.
17
+ *
18
+ * This order is useful when the caller wants a stable persisted view of key lifecycle state.
19
+ */
20
+ export declare const getCanonicalOidcPrivateKeys: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"]) => OidcPrivateKey[];
21
+ /**
22
+ * Return the currently active signing key from a private-key set.
23
+ *
24
+ * This helper reads explicit key status rather than relying on array index.
25
+ */
26
+ export declare const getCurrentOidcPrivateKey: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"]) => OidcPrivateKey;
27
+ /**
28
+ * Return private keys in the order expected by `oidc-provider` for signing and JWKS exposure.
29
+ *
30
+ * The active `Current` key comes first, followed by `Next`, then `Previous`.
31
+ */
32
+ export declare const getOidcProviderPrivateKeys: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"]) => OidcPrivateKey[];
33
+ /**
34
+ * Normalize seeded private keys into the explicit status model used by core and CLI.
35
+ *
36
+ * Seeding only supports the legacy one-key or two-key layout, so the helper rejects
37
+ * larger key arrays instead of trying to infer a more complex state machine.
38
+ */
39
+ export declare const getSeededOidcPrivateKeys: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"]) => OidcPrivateKey[];
40
+ /**
41
+ * Build the persisted private-key state for immediate rotation.
42
+ *
43
+ * The new key becomes `Current`, the previous `Current` key becomes `Previous`,
44
+ * and any older `Previous` key is dropped.
45
+ */
46
+ export declare const getImmediatelyRotatedOidcPrivateKeys: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"], newPrivateKey: OidcPrivateKey) => OidcPrivateKey[];
47
+ /**
48
+ * Build the persisted private-key state for staged rotation.
49
+ *
50
+ * The new key becomes `Next`, the existing `Current` key stays `Current`,
51
+ * and the existing `Previous` key is preserved when present.
52
+ */
53
+ export declare const getStagedRotatedOidcPrivateKeys: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"], newPrivateKey: OidcPrivateKey) => OidcPrivateKey[];
54
+ /**
55
+ * Promote a staged `Next` key into `Current` and demote the previous `Current` key into `Previous`.
56
+ *
57
+ * If no staged rotation is pending, the helper returns the original key array when it already
58
+ * uses explicit statuses, or the normalized array when the input is still in legacy form.
59
+ */
60
+ export declare const rotateOidcPrivateKeyStatuses: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"]) => OidcPrivateKey[];
61
+ /**
62
+ * Remove a single private key from the canonical key set after delete validation has already passed.
63
+ *
64
+ * The helper keeps the remaining keys in canonical status order and does not attempt to infer
65
+ * new lifecycle transitions beyond dropping the deleted key.
66
+ */
67
+ export declare const getOidcPrivateKeysAfterDeletion: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"], deletedKeyId: string) => OidcPrivateKey[];
68
+ /**
69
+ * Trim one or more `Previous` private keys from the end of the normalized key set.
70
+ *
71
+ * Only `Previous` keys are trim-able; attempting to trim past the available `Previous`
72
+ * keys indicates an invalid operation.
73
+ */
74
+ export declare const getTrimmedOidcPrivateKeys: (privateKeys: LogtoOidcConfigType["oidc.privateKeys"], length: number) => OidcPrivateKey[];
75
+ /**
76
+ * Build rotation state for immediate tenant cache invalidation.
77
+ *
78
+ * This records when a tenant instance should be considered stale so the next reload
79
+ * can pick up newly written signing key data.
80
+ */
81
+ export declare const getRotationStateForCacheInvalidation: (currentRotationState: SigningKeyRotationState | undefined, now?: number) => SigningKeyRotationState;
82
+ /**
83
+ * Build rotation state for staged private-key rotation.
84
+ *
85
+ * In addition to immediate tenant invalidation, this records the future activation time
86
+ * when the staged `Next` key should be promoted to `Current`.
87
+ */
88
+ export declare const getRotationStateForStagedRotation: (currentRotationState: SigningKeyRotationState | undefined, rotationGracePeriod: number, now?: number) => SigningKeyRotationState;
@@ -0,0 +1,163 @@
1
+ import { OidcSigningKeyStatus } from '../types/index.js';
2
+ const oidcPrivateKeyStatusOrder = {
3
+ [OidcSigningKeyStatus.Next]: 0,
4
+ [OidcSigningKeyStatus.Current]: 1,
5
+ [OidcSigningKeyStatus.Previous]: 2,
6
+ };
7
+ const oidcProviderPrivateKeyOrder = {
8
+ [OidcSigningKeyStatus.Current]: 0,
9
+ [OidcSigningKeyStatus.Next]: 1,
10
+ [OidcSigningKeyStatus.Previous]: 2,
11
+ };
12
+ const sortOidcPrivateKeys = (privateKeys, order) => privateKeys.toSorted((leftKey, rightKey) => order[leftKey.status] - order[rightKey.status]);
13
+ /**
14
+ * Normalize OIDC private signing keys into an explicit status-based model.
15
+ *
16
+ * Legacy keys without `status` are interpreted by index order:
17
+ * the first key becomes `Current` and the second key becomes `Previous`.
18
+ * The helper also validates that the key set contains exactly one `Current`
19
+ * and at most one `Next` and `Previous`.
20
+ */
21
+ export const normalizeOidcPrivateKeys = (privateKeys) => {
22
+ const normalizedPrivateKeys = privateKeys.map((privateKey, index) => ({
23
+ ...privateKey,
24
+ status: privateKey.status ??
25
+ (index === 0 ? OidcSigningKeyStatus.Current : OidcSigningKeyStatus.Previous),
26
+ }));
27
+ const currentKeys = normalizedPrivateKeys.filter(({ status }) => status === OidcSigningKeyStatus.Current);
28
+ const nextKeys = normalizedPrivateKeys.filter(({ status }) => status === OidcSigningKeyStatus.Next);
29
+ const previousKeys = normalizedPrivateKeys.filter(({ status }) => status === OidcSigningKeyStatus.Previous);
30
+ if (currentKeys.length !== 1 || nextKeys.length > 1 || previousKeys.length > 1) {
31
+ throw new Error('Malformed OIDC private key status configuration: expected exactly one Current key and at most one Next and Previous key.');
32
+ }
33
+ return normalizedPrivateKeys;
34
+ };
35
+ /**
36
+ * Return private keys in canonical business order: `Next`, then `Current`, then `Previous`.
37
+ *
38
+ * This order is useful when the caller wants a stable persisted view of key lifecycle state.
39
+ */
40
+ export const getCanonicalOidcPrivateKeys = (privateKeys) => sortOidcPrivateKeys(normalizeOidcPrivateKeys(privateKeys), oidcPrivateKeyStatusOrder);
41
+ /**
42
+ * Return the currently active signing key from a private-key set.
43
+ *
44
+ * This helper reads explicit key status rather than relying on array index.
45
+ */
46
+ export const getCurrentOidcPrivateKey = (privateKeys) => {
47
+ const currentKey = normalizeOidcPrivateKeys(privateKeys).find(({ status }) => status === OidcSigningKeyStatus.Current);
48
+ if (!currentKey) {
49
+ throw new Error('Malformed OIDC private key status configuration: missing Current key.');
50
+ }
51
+ return currentKey;
52
+ };
53
+ /**
54
+ * Return private keys in the order expected by `oidc-provider` for signing and JWKS exposure.
55
+ *
56
+ * The active `Current` key comes first, followed by `Next`, then `Previous`.
57
+ */
58
+ export const getOidcProviderPrivateKeys = (privateKeys) => sortOidcPrivateKeys(normalizeOidcPrivateKeys(privateKeys), oidcProviderPrivateKeyOrder);
59
+ /**
60
+ * Normalize seeded private keys into the explicit status model used by core and CLI.
61
+ *
62
+ * Seeding only supports the legacy one-key or two-key layout, so the helper rejects
63
+ * larger key arrays instead of trying to infer a more complex state machine.
64
+ */
65
+ export const getSeededOidcPrivateKeys = (privateKeys) => {
66
+ if (privateKeys.length > 2) {
67
+ throw new TypeError('CLI seed supports at most 2 OIDC private keys');
68
+ }
69
+ return normalizeOidcPrivateKeys(privateKeys);
70
+ };
71
+ /**
72
+ * Build the persisted private-key state for immediate rotation.
73
+ *
74
+ * The new key becomes `Current`, the previous `Current` key becomes `Previous`,
75
+ * and any older `Previous` key is dropped.
76
+ */
77
+ export const getImmediatelyRotatedOidcPrivateKeys = (privateKeys, newPrivateKey) => {
78
+ const normalizedPrivateKeys = normalizeOidcPrivateKeys(privateKeys);
79
+ if (normalizedPrivateKeys.some(({ status }) => status === OidcSigningKeyStatus.Next)) {
80
+ throw new TypeError('Immediate OIDC private key rotation is not allowed when a Next key exists');
81
+ }
82
+ const currentKey = getCurrentOidcPrivateKey(normalizedPrivateKeys);
83
+ return [
84
+ { ...newPrivateKey, status: OidcSigningKeyStatus.Current },
85
+ { ...currentKey, status: OidcSigningKeyStatus.Previous },
86
+ ];
87
+ };
88
+ /**
89
+ * Build the persisted private-key state for staged rotation.
90
+ *
91
+ * The new key becomes `Next`, the existing `Current` key stays `Current`,
92
+ * and the existing `Previous` key is preserved when present.
93
+ */
94
+ export const getStagedRotatedOidcPrivateKeys = (privateKeys, newPrivateKey) => {
95
+ const normalizedPrivateKeys = normalizeOidcPrivateKeys(privateKeys);
96
+ const currentKey = getCurrentOidcPrivateKey(normalizedPrivateKeys);
97
+ const previousKey = normalizedPrivateKeys.find(({ status }) => status === OidcSigningKeyStatus.Previous);
98
+ return [
99
+ { ...newPrivateKey, status: OidcSigningKeyStatus.Next },
100
+ { ...currentKey, status: OidcSigningKeyStatus.Current },
101
+ ...(previousKey ? [{ ...previousKey, status: OidcSigningKeyStatus.Previous }] : []),
102
+ ];
103
+ };
104
+ /**
105
+ * Promote a staged `Next` key into `Current` and demote the previous `Current` key into `Previous`.
106
+ *
107
+ * If no staged rotation is pending, the helper returns the original key array when it already
108
+ * uses explicit statuses, or the normalized array when the input is still in legacy form.
109
+ */
110
+ export const rotateOidcPrivateKeyStatuses = (privateKeys) => {
111
+ const normalizedPrivateKeys = normalizeOidcPrivateKeys(privateKeys);
112
+ const nextKey = normalizedPrivateKeys.find(({ status }) => status === OidcSigningKeyStatus.Next);
113
+ if (!nextKey) {
114
+ return privateKeys.every(({ status }) => status) ? privateKeys : normalizedPrivateKeys;
115
+ }
116
+ const currentKey = getCurrentOidcPrivateKey(normalizedPrivateKeys);
117
+ return [
118
+ { ...nextKey, status: OidcSigningKeyStatus.Current },
119
+ { ...currentKey, status: OidcSigningKeyStatus.Previous },
120
+ ];
121
+ };
122
+ /**
123
+ * Remove a single private key from the canonical key set after delete validation has already passed.
124
+ *
125
+ * The helper keeps the remaining keys in canonical status order and does not attempt to infer
126
+ * new lifecycle transitions beyond dropping the deleted key.
127
+ */
128
+ export const getOidcPrivateKeysAfterDeletion = (privateKeys, deletedKeyId) => getCanonicalOidcPrivateKeys(privateKeys).filter(({ id }) => id !== deletedKeyId);
129
+ /**
130
+ * Trim one or more `Previous` private keys from the end of the normalized key set.
131
+ *
132
+ * Only `Previous` keys are trim-able; attempting to trim past the available `Previous`
133
+ * keys indicates an invalid operation.
134
+ */
135
+ export const getTrimmedOidcPrivateKeys = (privateKeys, length) => {
136
+ const normalizedPrivateKeys = normalizeOidcPrivateKeys(privateKeys);
137
+ const previousKeys = normalizedPrivateKeys.filter(({ status }) => status === OidcSigningKeyStatus.Previous);
138
+ if (length > previousKeys.length) {
139
+ throw new TypeError('Only Previous OIDC private keys can be trimmed');
140
+ }
141
+ return getCanonicalOidcPrivateKeys(normalizedPrivateKeys).slice(0, -length);
142
+ };
143
+ /**
144
+ * Build rotation state for immediate tenant cache invalidation.
145
+ *
146
+ * This records when a tenant instance should be considered stale so the next reload
147
+ * can pick up newly written signing key data.
148
+ */
149
+ export const getRotationStateForCacheInvalidation = (currentRotationState, now = Date.now()) => ({
150
+ ...currentRotationState,
151
+ tenantCacheExpiresAt: now,
152
+ });
153
+ /**
154
+ * Build rotation state for staged private-key rotation.
155
+ *
156
+ * In addition to immediate tenant invalidation, this records the future activation time
157
+ * when the staged `Next` key should be promoted to `Current`.
158
+ */
159
+ export const getRotationStateForStagedRotation = (currentRotationState, rotationGracePeriod, now = Date.now()) => ({
160
+ ...currentRotationState,
161
+ tenantCacheExpiresAt: now,
162
+ signingKeyRotationAt: now + rotationGracePeriod * 1000,
163
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,128 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { OidcSigningKeyStatus } from '../types/index.js';
3
+ import { getCanonicalOidcPrivateKeys, getCurrentOidcPrivateKey, getImmediatelyRotatedOidcPrivateKeys, getOidcPrivateKeysAfterDeletion, getOidcProviderPrivateKeys, getRotationStateForCacheInvalidation, getRotationStateForStagedRotation, getSeededOidcPrivateKeys, getStagedRotatedOidcPrivateKeys, getTrimmedOidcPrivateKeys, normalizeOidcPrivateKeys, rotateOidcPrivateKeyStatuses, } from './oidc-private-key.js';
4
+ const createPrivateKey = (id, createdAt, status) => ({
5
+ id,
6
+ value: `private-key-${id}`,
7
+ createdAt,
8
+ status,
9
+ });
10
+ describe('OIDC private key helpers', () => {
11
+ it('normalizes legacy private keys without status into Current and Previous', () => {
12
+ expect(normalizeOidcPrivateKeys([createPrivateKey('current', 1), createPrivateKey('previous', 2)])).toEqual([
13
+ createPrivateKey('current', 1, OidcSigningKeyStatus.Current),
14
+ createPrivateKey('previous', 2, OidcSigningKeyStatus.Previous),
15
+ ]);
16
+ });
17
+ it('throws for malformed status configurations', () => {
18
+ expect(() => normalizeOidcPrivateKeys([
19
+ createPrivateKey('current-a', 1, OidcSigningKeyStatus.Current),
20
+ createPrivateKey('current-b', 2, OidcSigningKeyStatus.Current),
21
+ ])).toThrow('Malformed OIDC private key status configuration');
22
+ });
23
+ it('orders private keys in canonical business order', () => {
24
+ expect(getCanonicalOidcPrivateKeys([
25
+ createPrivateKey('previous', 3, OidcSigningKeyStatus.Previous),
26
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
27
+ createPrivateKey('next', 1, OidcSigningKeyStatus.Next),
28
+ ])).toEqual([
29
+ createPrivateKey('next', 1, OidcSigningKeyStatus.Next),
30
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
31
+ createPrivateKey('previous', 3, OidcSigningKeyStatus.Previous),
32
+ ]);
33
+ });
34
+ it('orders private keys for oidc-provider consumption', () => {
35
+ expect(getOidcProviderPrivateKeys([
36
+ createPrivateKey('previous', 3, OidcSigningKeyStatus.Previous),
37
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
38
+ createPrivateKey('next', 1, OidcSigningKeyStatus.Next),
39
+ ])).toEqual([
40
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
41
+ createPrivateKey('next', 1, OidcSigningKeyStatus.Next),
42
+ createPrivateKey('previous', 3, OidcSigningKeyStatus.Previous),
43
+ ]);
44
+ });
45
+ it('finds the current signing key by status', () => {
46
+ expect(getCurrentOidcPrivateKey([
47
+ createPrivateKey('previous', 3, OidcSigningKeyStatus.Previous),
48
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
49
+ createPrivateKey('next', 1, OidcSigningKeyStatus.Next),
50
+ ])).toEqual(createPrivateKey('current', 2, OidcSigningKeyStatus.Current));
51
+ });
52
+ it('assigns statuses to seeded keys', () => {
53
+ expect(getSeededOidcPrivateKeys([createPrivateKey('current', 2), createPrivateKey('previous', 1)])).toEqual([
54
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
55
+ createPrivateKey('previous', 1, OidcSigningKeyStatus.Previous),
56
+ ]);
57
+ });
58
+ it('rejects more than two seeded private keys', () => {
59
+ expect(() => getSeededOidcPrivateKeys([
60
+ createPrivateKey('current', 3),
61
+ createPrivateKey('previous-a', 2),
62
+ createPrivateKey('previous-b', 1),
63
+ ])).toThrow('CLI seed supports at most 2 OIDC private keys');
64
+ });
65
+ it('immediately rotates the new key into Current', () => {
66
+ expect(getImmediatelyRotatedOidcPrivateKeys([
67
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
68
+ createPrivateKey('previous', 1, OidcSigningKeyStatus.Previous),
69
+ ], createPrivateKey('new', 3))).toEqual([
70
+ createPrivateKey('new', 3, OidcSigningKeyStatus.Current),
71
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Previous),
72
+ ]);
73
+ });
74
+ it('stages the new key as Next while preserving Current and Previous', () => {
75
+ expect(getStagedRotatedOidcPrivateKeys([
76
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
77
+ createPrivateKey('previous', 1, OidcSigningKeyStatus.Previous),
78
+ ], createPrivateKey('new', 3))).toEqual([
79
+ createPrivateKey('new', 3, OidcSigningKeyStatus.Next),
80
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
81
+ createPrivateKey('previous', 1, OidcSigningKeyStatus.Previous),
82
+ ]);
83
+ });
84
+ it('promotes Next to Current during activation', () => {
85
+ expect(rotateOidcPrivateKeyStatuses([
86
+ createPrivateKey('next', 3, OidcSigningKeyStatus.Next),
87
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
88
+ createPrivateKey('previous', 1, OidcSigningKeyStatus.Previous),
89
+ ])).toEqual([
90
+ createPrivateKey('next', 3, OidcSigningKeyStatus.Current),
91
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Previous),
92
+ ]);
93
+ });
94
+ it('preserves status order when deleting Previous', () => {
95
+ expect(getOidcPrivateKeysAfterDeletion([
96
+ createPrivateKey('next', 3, OidcSigningKeyStatus.Next),
97
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
98
+ createPrivateKey('previous', 1, OidcSigningKeyStatus.Previous),
99
+ ], 'previous')).toEqual([
100
+ createPrivateKey('next', 3, OidcSigningKeyStatus.Next),
101
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
102
+ ]);
103
+ });
104
+ it('only trims Previous keys', () => {
105
+ expect(getTrimmedOidcPrivateKeys([
106
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
107
+ createPrivateKey('previous', 1, OidcSigningKeyStatus.Previous),
108
+ ], 1)).toEqual([createPrivateKey('current', 2, OidcSigningKeyStatus.Current)]);
109
+ });
110
+ it('trims the Previous key even when the input order is not canonical', () => {
111
+ expect(getTrimmedOidcPrivateKeys([
112
+ createPrivateKey('previous', 1, OidcSigningKeyStatus.Previous),
113
+ createPrivateKey('current', 2, OidcSigningKeyStatus.Current),
114
+ ], 1)).toEqual([createPrivateKey('current', 2, OidcSigningKeyStatus.Current)]);
115
+ });
116
+ it('creates invalidation-only state', () => {
117
+ expect(getRotationStateForCacheInvalidation({ signingKeyRotationAt: 456 }, 123)).toEqual({
118
+ tenantCacheExpiresAt: 123,
119
+ signingKeyRotationAt: 456,
120
+ });
121
+ });
122
+ it('creates staged rotation state with tenant invalidation and activation timestamps', () => {
123
+ expect(getRotationStateForStagedRotation({ tenantCacheExpiresAt: 1 }, 60, 123)).toEqual({
124
+ tenantCacheExpiresAt: 123,
125
+ signingKeyRotationAt: 60_123,
126
+ });
127
+ });
128
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/schemas",
3
- "version": "1.37.1",
3
+ "version": "1.39.0",
4
4
  "author": "Silverhand Inc. <contact@silverhand.io>",
5
5
  "license": "MPL-2.0",
6
6
  "type": "module",
@@ -65,12 +65,12 @@
65
65
  "dependencies": {
66
66
  "@withtyped/server": "^0.14.0",
67
67
  "nanoid": "^5.0.9",
68
- "@logto/connector-kit": "^4.7.0",
69
- "@logto/core-kit": "^2.7.1",
70
- "@logto/language-kit": "^1.2.0",
71
- "@logto/phrases": "^1.26.0",
72
- "@logto/shared": "^3.3.1",
73
- "@logto/phrases-experience": "^1.12.2"
68
+ "@logto/core-kit": "^2.9.0",
69
+ "@logto/connector-kit": "^5.0.0",
70
+ "@logto/phrases": "^1.28.0",
71
+ "@logto/language-kit": "^1.3.0",
72
+ "@logto/phrases-experience": "^1.13.1",
73
+ "@logto/shared": "^3.4.0"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "zod": "3.24.3"
@@ -85,7 +85,8 @@
85
85
  "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput --incremental",
86
86
  "lint": "eslint --ext .ts src",
87
87
  "lint:report": "pnpm lint --format json --output-file report.json",
88
- "test": "vitest src",
88
+ "test": "vitest run src",
89
+ "test:watch": "vitest src --watch",
89
90
  "test:ci": "pnpm run test --silent --coverage"
90
91
  }
91
92
  }
@@ -7,5 +7,9 @@ create table account_centers (
7
7
  /** Control each fields */
8
8
  fields jsonb /* @use AccountCenterFieldControl */ not null default '{}'::jsonb,
9
9
  webauthn_related_origins jsonb /* @use WebauthnRelatedOrigins */ not null default '[]'::jsonb,
10
+ /** URL for custom account deletion endpoint */
11
+ delete_account_url varchar(2048),
12
+ /** User-defined custom CSS for the account center */
13
+ custom_css text,
10
14
  primary key (tenant_id, id)
11
15
  );
@@ -27,6 +27,7 @@ create index oidc_model_instances__model_name_payload_uid
27
27
  (payload->>'uid')
28
28
  );
29
29
 
30
+ /* TODO: Consider dropping this full data index if the partial index proves to be effective and safe. */
30
31
  create index oidc_model_instances__model_name_payload_grant_id
31
32
  on oidc_model_instances (
32
33
  tenant_id,
@@ -34,9 +35,24 @@ create index oidc_model_instances__model_name_payload_grant_id
34
35
  (payload->>'grantId')
35
36
  );
36
37
 
38
+ create index oidc_model_instances__model_name_payload_grant_id_partial
39
+ on oidc_model_instances (tenant_id, model_name, (payload->>'grantId'))
40
+ where payload ? 'grantId';
41
+
37
42
  create index oidc_model_instances__expires_at
38
43
  on oidc_model_instances (tenant_id, expires_at);
39
44
 
40
45
  create index oidc_model_instances__session_payload_account_id_expires_at
41
46
  on oidc_model_instances (tenant_id, (payload->>'accountId'), expires_at)
42
47
  WHERE model_name = 'Session';
48
+
49
+ create index oidc_model_instances__grant_payload_account_id_expires_at
50
+ on oidc_model_instances (tenant_id, (payload->>'accountId'), expires_at)
51
+ WHERE model_name = 'Grant';
52
+
53
+ alter table oidc_model_instances set (
54
+ autovacuum_vacuum_scale_factor = 0.05,
55
+ autovacuum_analyze_scale_factor = 0.02,
56
+ autovacuum_vacuum_threshold = 5000,
57
+ autovacuum_analyze_threshold = 2000
58
+ );
@@ -33,5 +33,7 @@ create table sign_in_experiences (
33
33
  email_blocklist_policy jsonb /* @use EmailBlocklistPolicy */ not null default '{}'::jsonb,
34
34
  forgot_password_methods jsonb /* @use ForgotPasswordMethods */ default '[]'::jsonb,
35
35
  passkey_sign_in jsonb /* @use PasskeySignIn */ not null default '{}'::jsonb,
36
+ /** Nullable by design: null keeps legacy full-catalog behavior and [] collects no custom profile fields. */
37
+ sign_up_profile_fields jsonb /* @use SignUpProfileFields */,
36
38
  primary key (tenant_id, id)
37
39
  );