@logto/schemas 1.38.0 → 1.40.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.
- package/alterations/1.39.0-1774752400-add-delete-account-url.ts +20 -0
- package/alterations/1.39.0-1774770686-add-account-center-custom-css.ts +20 -0
- package/alterations/1.39.0-1776502301-add-sign-up-profile-fields.ts +20 -0
- package/alterations/1.40.0-1776516232-add-account-center-profile-fields.ts +20 -0
- package/alterations/1.40.0-1778318116-add-custom-ui-csp-to-sie.ts +20 -0
- package/alterations/1.40.0-1778500000-add-organization-user-relations-user-id-index.ts +41 -0
- package/alterations/1.40.0-1778500001-add-organization-role-user-relations-org-user-index.ts +43 -0
- package/alterations/1.40.0-1779421396-add-application-access-control-schema.ts +90 -0
- package/alterations-js/1.39.0-1774752400-add-delete-account-url.js +16 -0
- package/alterations-js/1.39.0-1774770686-add-account-center-custom-css.js +16 -0
- package/alterations-js/1.39.0-1776502301-add-sign-up-profile-fields.js +16 -0
- package/alterations-js/1.40.0-1776516232-add-account-center-profile-fields.js +16 -0
- package/alterations-js/1.40.0-1778318116-add-custom-ui-csp-to-sie.js +16 -0
- package/alterations-js/1.40.0-1778500000-add-organization-user-relations-user-id-index.js +37 -0
- package/alterations-js/1.40.0-1778500001-add-organization-role-user-relations-org-user-index.js +39 -0
- package/alterations-js/1.40.0-1779421396-add-application-access-control-schema.js +82 -0
- package/lib/consts/application.d.ts +1 -0
- package/lib/consts/application.js +1 -0
- package/lib/consts/index.d.ts +1 -0
- package/lib/consts/index.js +1 -0
- package/lib/db-entries/account-center.d.ts +14 -2
- package/lib/db-entries/account-center.js +13 -1
- package/lib/db-entries/application-access-control-org-role-relation.d.ts +22 -0
- package/lib/db-entries/application-access-control-org-role-relation.js +33 -0
- package/lib/db-entries/application-access-control-organization-relation.d.ts +20 -0
- package/lib/db-entries/application-access-control-organization-relation.js +29 -0
- package/lib/db-entries/application-access-control-user-relation.d.ts +20 -0
- package/lib/db-entries/application-access-control-user-relation.js +29 -0
- package/lib/db-entries/application-access-control-user-role-relation.d.ts +20 -0
- package/lib/db-entries/application-access-control-user-role-relation.js +29 -0
- package/lib/db-entries/application.d.ts +3 -1
- package/lib/db-entries/application.js +4 -0
- package/lib/db-entries/index.d.ts +4 -0
- package/lib/db-entries/index.js +4 -0
- package/lib/db-entries/sign-in-experience.d.ts +8 -2
- package/lib/db-entries/sign-in-experience.js +9 -1
- package/lib/foundations/jsonb-types/account-centers.d.ts +27 -0
- package/lib/foundations/jsonb-types/account-centers.js +12 -0
- package/lib/foundations/jsonb-types/applications.d.ts +3 -0
- package/lib/foundations/jsonb-types/applications.js +4 -0
- package/lib/foundations/jsonb-types/applications.test.d.ts +1 -0
- package/lib/foundations/jsonb-types/applications.test.js +23 -0
- package/lib/foundations/jsonb-types/sign-in-experience.d.ts +27 -1
- package/lib/foundations/jsonb-types/sign-in-experience.js +5 -0
- package/lib/foundations/jsonb-types/sign-in-experience.test.d.ts +1 -0
- package/lib/foundations/jsonb-types/sign-in-experience.test.js +18 -0
- package/lib/seeds/application.js +2 -0
- package/lib/seeds/sign-in-experience.d.ts +13 -1
- package/lib/seeds/sign-in-experience.js +10 -1
- package/lib/seeds/sign-in-experience.test.d.ts +1 -0
- package/lib/seeds/sign-in-experience.test.js +27 -0
- package/lib/types/alteration.d.ts +5 -0
- package/lib/types/application.d.ts +101 -2
- package/lib/types/application.js +55 -0
- package/lib/types/application.test.d.ts +1 -0
- package/lib/types/application.test.js +120 -0
- package/lib/types/consent.d.ts +6 -0
- package/lib/types/custom-profile-fields.d.ts +7 -13
- package/lib/types/custom-profile-fields.js +6 -13
- package/lib/types/logto-config/index.d.ts +93 -2
- package/lib/types/logto-config/index.js +22 -4
- package/lib/types/logto-config/index.test.d.ts +1 -0
- package/lib/types/logto-config/index.test.js +29 -0
- package/lib/types/logto-config/jwt-customizer.d.ts +74 -0
- package/lib/types/logto-config/jwt-customizer.js +1 -0
- package/lib/types/logto-config/jwt-customizer.test.js +14 -2
- package/lib/types/onboarding.d.ts +93 -1
- package/lib/types/onboarding.js +22 -1
- package/lib/types/saml-application.d.ts +3 -0
- package/lib/types/sign-in-experience.d.ts +23 -2
- package/lib/types/sign-in-experience.js +1 -0
- package/lib/types/system.d.ts +46 -7
- package/lib/types/system.js +9 -0
- package/lib/types/user-assets.d.ts +1 -1
- package/lib/types/user-logto-config.d.ts +11 -0
- package/lib/types/user-logto-config.js +6 -0
- package/lib/types/user-sessions.d.ts +2516 -0
- package/lib/types/user-sessions.js +21 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/oidc-private-key.d.ts +88 -0
- package/lib/utils/oidc-private-key.js +163 -0
- package/lib/utils/oidc-private-key.test.d.ts +1 -0
- package/lib/utils/oidc-private-key.test.js +128 -0
- package/package.json +6 -6
- package/tables/account_centers.sql +6 -0
- package/tables/application_access_control_org_role_relations.sql +16 -0
- package/tables/application_access_control_organization_relations.sql +12 -0
- package/tables/application_access_control_user_relations.sql +12 -0
- package/tables/application_access_control_user_role_relations.sql +14 -0
- package/tables/applications.sql +1 -0
- package/tables/organization_role_user_relations.sql +3 -0
- package/tables/organization_user_relations.sql +3 -0
- package/tables/sign_in_experiences.sql +3 -0
|
@@ -35,6 +35,27 @@ export const getUserSessionsResponseGuard = z.object({
|
|
|
35
35
|
sessions: z.array(userExtendedSessionGuard),
|
|
36
36
|
});
|
|
37
37
|
export const getUserSessionResponseGuard = userExtendedSessionGuard;
|
|
38
|
+
/**
|
|
39
|
+
* Account-API-specific extension of `userExtendedSessionGuard`.
|
|
40
|
+
*
|
|
41
|
+
* Adds `isCurrent` so a caller that has its own OIDC session uid (i.e. the Account API)
|
|
42
|
+
* can mark which entry in the list is the session backing the request. Kept separate
|
|
43
|
+
* from `userExtendedSessionGuard` because the management/admin endpoints have no
|
|
44
|
+
* "current session" concept and shouldn't surface this field in their contracts.
|
|
45
|
+
*/
|
|
46
|
+
export const accountUserExtendedSessionGuard = userExtendedSessionGuard.extend({
|
|
47
|
+
/**
|
|
48
|
+
* `true` for the entry whose `payload.uid` matches the calling session, `false` for
|
|
49
|
+
* the others. At most one entry is `true` per response. Zero entries are tagged when
|
|
50
|
+
* the calling access token has no matching session uid — for example, the caller has
|
|
51
|
+
* revoked its own session but the token has not yet expired, or the token was issued
|
|
52
|
+
* from a non-session-backed grant.
|
|
53
|
+
*/
|
|
54
|
+
isCurrent: z.boolean(),
|
|
55
|
+
});
|
|
56
|
+
export const getAccountUserSessionsResponseGuard = z.object({
|
|
57
|
+
sessions: z.array(accountUserExtendedSessionGuard),
|
|
58
|
+
});
|
|
38
59
|
export const userApplicationGrantPayloadGuard = z
|
|
39
60
|
.object({
|
|
40
61
|
/** Expiration time of the grant in seconds since the epoch */
|
package/lib/utils/index.d.ts
CHANGED
package/lib/utils/index.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.40.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": "^5.0.1",
|
|
68
69
|
"@logto/language-kit": "^1.3.0",
|
|
69
|
-
"@logto/core-kit": "^2.
|
|
70
|
-
"@logto/phrases": "^1.
|
|
71
|
-
"@logto/
|
|
72
|
-
"@logto/
|
|
73
|
-
"@logto/phrases-experience": "^1.13.0"
|
|
70
|
+
"@logto/core-kit": "^2.9.0",
|
|
71
|
+
"@logto/phrases-experience": "^1.13.2",
|
|
72
|
+
"@logto/phrases": "^1.28.0",
|
|
73
|
+
"@logto/shared": "^3.4.0"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"zod": "3.24.3"
|
|
@@ -7,5 +7,11 @@ 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,
|
|
14
|
+
/** Ordered list of custom profile fields to show in the prebuilt account center */
|
|
15
|
+
profile_fields jsonb /* @use AccountCenterProfileFields */,
|
|
10
16
|
primary key (tenant_id, id)
|
|
11
17
|
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* init_order = 2 */
|
|
2
|
+
|
|
3
|
+
/** The organization role allow relations for application-level access control. */
|
|
4
|
+
create table application_access_control_org_role_relations (
|
|
5
|
+
tenant_id varchar(21) not null
|
|
6
|
+
references tenants (id) on update cascade on delete cascade,
|
|
7
|
+
application_id varchar(21) not null
|
|
8
|
+
references applications (id) on update cascade on delete cascade,
|
|
9
|
+
organization_id varchar(21) not null
|
|
10
|
+
references organizations (id) on update cascade on delete cascade,
|
|
11
|
+
organization_role_id varchar(21) not null
|
|
12
|
+
references organization_roles (id) on update cascade on delete cascade,
|
|
13
|
+
primary key (tenant_id, application_id, organization_id, organization_role_id),
|
|
14
|
+
constraint application_access_control_org_role_relations__role_type
|
|
15
|
+
check (check_organization_role_type(organization_role_id, 'User'))
|
|
16
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* init_order = 2 */
|
|
2
|
+
|
|
3
|
+
/** The organization membership allow relations for application-level access control. */
|
|
4
|
+
create table application_access_control_organization_relations (
|
|
5
|
+
tenant_id varchar(21) not null
|
|
6
|
+
references tenants (id) on update cascade on delete cascade,
|
|
7
|
+
application_id varchar(21) not null
|
|
8
|
+
references applications (id) on update cascade on delete cascade,
|
|
9
|
+
organization_id varchar(21) not null
|
|
10
|
+
references organizations (id) on update cascade on delete cascade,
|
|
11
|
+
primary key (tenant_id, application_id, organization_id)
|
|
12
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* init_order = 2 */
|
|
2
|
+
|
|
3
|
+
/** The direct user allow relations for application-level access control. */
|
|
4
|
+
create table application_access_control_user_relations (
|
|
5
|
+
tenant_id varchar(21) not null
|
|
6
|
+
references tenants (id) on update cascade on delete cascade,
|
|
7
|
+
application_id varchar(21) not null
|
|
8
|
+
references applications (id) on update cascade on delete cascade,
|
|
9
|
+
user_id varchar(21) not null
|
|
10
|
+
references users (id) on update cascade on delete cascade,
|
|
11
|
+
primary key (tenant_id, application_id, user_id)
|
|
12
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* init_order = 2 */
|
|
2
|
+
|
|
3
|
+
/** The user role allow relations for application-level access control. */
|
|
4
|
+
create table application_access_control_user_role_relations (
|
|
5
|
+
tenant_id varchar(21) not null
|
|
6
|
+
references tenants (id) on update cascade on delete cascade,
|
|
7
|
+
application_id varchar(21) not null
|
|
8
|
+
references applications (id) on update cascade on delete cascade,
|
|
9
|
+
role_id varchar(21) not null
|
|
10
|
+
references roles (id) on update cascade on delete cascade,
|
|
11
|
+
primary key (tenant_id, application_id, role_id),
|
|
12
|
+
constraint application_access_control_user_role_relations__role_type
|
|
13
|
+
check (public.check_role_type(role_id, 'User'))
|
|
14
|
+
);
|
package/tables/applications.sql
CHANGED
|
@@ -16,6 +16,7 @@ create table applications (
|
|
|
16
16
|
protected_app_metadata jsonb /* @use ProtectedAppMetadata */,
|
|
17
17
|
custom_data jsonb /* @use JsonObject */ not null default '{}'::jsonb,
|
|
18
18
|
is_third_party boolean not null default false,
|
|
19
|
+
app_level_access_control_enabled boolean not null default false,
|
|
19
20
|
created_at timestamptz not null default(now()),
|
|
20
21
|
primary key (id)
|
|
21
22
|
);
|
|
@@ -16,3 +16,6 @@ create table organization_role_user_relations (
|
|
|
16
16
|
constraint organization_role_user_relations__role_type
|
|
17
17
|
check (check_organization_role_type(organization_role_id, 'User'))
|
|
18
18
|
);
|
|
19
|
+
|
|
20
|
+
create index organization_role_user_relations__tenant_id_org_id_user_id
|
|
21
|
+
on organization_role_user_relations (tenant_id, organization_id, user_id);
|
|
@@ -13,3 +13,6 @@ create table organization_user_relations (
|
|
|
13
13
|
foreign key (tenant_id, user_id)
|
|
14
14
|
references users (tenant_id, id) on update cascade on delete cascade
|
|
15
15
|
);
|
|
16
|
+
|
|
17
|
+
create index organization_user_relations__tenant_id_user_id
|
|
18
|
+
on organization_user_relations (tenant_id, user_id);
|
|
@@ -21,6 +21,7 @@ create table sign_in_experiences (
|
|
|
21
21
|
custom_css text,
|
|
22
22
|
custom_content jsonb /* @use CustomContent */ not null default '{}'::jsonb,
|
|
23
23
|
custom_ui_assets jsonb /* @use CustomUiAssets */,
|
|
24
|
+
custom_ui_csp jsonb /* @use CustomUiCsp */ not null default '{}'::jsonb,
|
|
24
25
|
password_policy jsonb /* @use PartialPasswordPolicy */ not null default '{}'::jsonb,
|
|
25
26
|
mfa jsonb /* @use Mfa */ not null default '{}'::jsonb,
|
|
26
27
|
adaptive_mfa jsonb /* @use AdaptiveMfa */ not null default '{}'::jsonb,
|
|
@@ -33,5 +34,7 @@ create table sign_in_experiences (
|
|
|
33
34
|
email_blocklist_policy jsonb /* @use EmailBlocklistPolicy */ not null default '{}'::jsonb,
|
|
34
35
|
forgot_password_methods jsonb /* @use ForgotPasswordMethods */ default '[]'::jsonb,
|
|
35
36
|
passkey_sign_in jsonb /* @use PasskeySignIn */ not null default '{}'::jsonb,
|
|
37
|
+
/** Nullable by design: null keeps legacy full-catalog behavior and [] collects no custom profile fields. */
|
|
38
|
+
sign_up_profile_fields jsonb /* @use SignUpProfileFields */,
|
|
36
39
|
primary key (tenant_id, id)
|
|
37
40
|
);
|