@logto/schemas 1.43.0 → 1.44.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 (98) hide show
  1. package/alterations/1.44.0-1788946735-add-saml-authn-request-config.ts +18 -0
  2. package/alterations/1.44.0-1789000000-extend-user-id-length.ts +61 -0
  3. package/alterations-js/1.44.0-1788946735-add-saml-authn-request-config.js +14 -0
  4. package/alterations-js/1.44.0-1789000000-extend-user-id-length.js +47 -0
  5. package/lib/consts/experience.d.ts +3 -0
  6. package/lib/consts/experience.js +3 -0
  7. package/lib/consts/index.d.ts +1 -0
  8. package/lib/consts/index.js +1 -0
  9. package/lib/consts/license.d.ts +222 -0
  10. package/lib/consts/license.js +81 -0
  11. package/lib/consts/license.test.d.ts +1 -0
  12. package/lib/consts/license.test.js +96 -0
  13. package/lib/consts/oidc.d.ts +15 -1
  14. package/lib/consts/oidc.js +11 -0
  15. package/lib/consts/oidc.test.d.ts +1 -0
  16. package/lib/consts/oidc.test.js +15 -0
  17. package/lib/consts/subscriptions.d.ts +13 -1
  18. package/lib/consts/subscriptions.js +12 -0
  19. package/lib/db-entries/aggregated-daily-active-user.js +2 -2
  20. package/lib/db-entries/application-access-control-user-relation.js +2 -2
  21. package/lib/db-entries/application-user-consent-organization.js +2 -2
  22. package/lib/db-entries/cimd-grant-organization.js +2 -2
  23. package/lib/db-entries/daily-active-user.js +2 -2
  24. package/lib/db-entries/oidc-session-extension.js +2 -2
  25. package/lib/db-entries/organization-invitation.js +4 -4
  26. package/lib/db-entries/organization-role-user-relation.js +2 -2
  27. package/lib/db-entries/organization-user-relation.js +2 -2
  28. package/lib/db-entries/personal-access-token.js +2 -2
  29. package/lib/db-entries/saml-application-config.d.ts +4 -2
  30. package/lib/db-entries/saml-application-config.js +5 -1
  31. package/lib/db-entries/secret.js +2 -2
  32. package/lib/db-entries/subject-token.js +2 -2
  33. package/lib/db-entries/trusted-device.js +2 -2
  34. package/lib/db-entries/user-geo-location.js +2 -2
  35. package/lib/db-entries/user-sign-in-country.js +2 -2
  36. package/lib/db-entries/user-sso-identity.js +2 -2
  37. package/lib/db-entries/user.js +2 -2
  38. package/lib/db-entries/users-role.js +2 -2
  39. package/lib/db-entries/verification-record.js +2 -2
  40. package/lib/db-entries/verification-status.js +2 -2
  41. package/lib/foundations/jsonb-types/captcha.d.ts +47 -1
  42. package/lib/foundations/jsonb-types/captcha.js +20 -0
  43. package/lib/foundations/jsonb-types/hooks.d.ts +0 -3
  44. package/lib/foundations/jsonb-types/hooks.js +2 -6
  45. package/lib/foundations/jsonb-types/saml-application-configs.d.ts +23 -0
  46. package/lib/foundations/jsonb-types/saml-application-configs.js +16 -0
  47. package/lib/foundations/jsonb-types/saml-application-configs.test.js +24 -1
  48. package/lib/types/authentication-context.d.ts +224 -0
  49. package/lib/types/authentication-context.js +238 -0
  50. package/lib/types/authentication-context.test.d.ts +1 -0
  51. package/lib/types/authentication-context.test.js +186 -0
  52. package/lib/types/cookie.d.ts +4 -0
  53. package/lib/types/cookie.js +6 -1
  54. package/lib/types/index.d.ts +1 -0
  55. package/lib/types/index.js +1 -0
  56. package/lib/types/interactions.d.ts +161 -0
  57. package/lib/types/interactions.js +23 -0
  58. package/lib/types/log/interaction.d.ts +15 -2
  59. package/lib/types/log/interaction.js +5 -0
  60. package/lib/types/logto-config/index.d.ts +47 -0
  61. package/lib/types/logto-config/index.js +1 -0
  62. package/lib/types/logto-config/jwt-customizer.d.ts +92 -0
  63. package/lib/types/saml-application.d.ts +55 -1
  64. package/lib/types/saml-application.js +7 -1
  65. package/lib/types/sign-in-experience.d.ts +11 -7
  66. package/lib/types/sign-in-experience.js +2 -2
  67. package/lib/types/ssr.d.ts +2 -0
  68. package/lib/types/system.d.ts +57 -5
  69. package/lib/types/system.js +36 -0
  70. package/lib/types/system.test.d.ts +1 -0
  71. package/lib/types/system.test.js +26 -0
  72. package/lib/types/user-sessions.d.ts +156 -0
  73. package/lib/types/verification-records/code-verification.d.ts +17 -0
  74. package/lib/types/verification-records/code-verification.js +1 -0
  75. package/lib/utils/oidc-private-key.test.js +44 -0
  76. package/package.json +7 -7
  77. package/tables/aggregated_daily_active_users.sql +1 -1
  78. package/tables/application_access_control_user_relations.sql +1 -1
  79. package/tables/application_user_consent_organizations.sql +1 -1
  80. package/tables/cimd_grant_organizations.sql +1 -1
  81. package/tables/daily_active_users.sql +1 -1
  82. package/tables/oidc_session_extensions.sql +1 -1
  83. package/tables/organization_invitations.sql +2 -2
  84. package/tables/organization_role_user_relations.sql +1 -1
  85. package/tables/organization_user_relations.sql +1 -1
  86. package/tables/personal_access_tokens.sql +1 -1
  87. package/tables/saml_application_configs.sql +1 -0
  88. package/tables/secrets.sql +1 -1
  89. package/tables/subject_tokens.sql +1 -1
  90. package/tables/trusted_devices.sql +1 -1
  91. package/tables/user_geo_locations.sql +1 -1
  92. package/tables/user_sign_in_countries.sql +1 -1
  93. package/tables/user_sso_identities.sql +1 -1
  94. package/tables/users.sql +1 -1
  95. package/tables/users_roles.sql +1 -1
  96. package/tables/verification_records.sql +1 -1
  97. package/tables/verification_statuses.sql +1 -1
  98. package/LICENSE +0 -373
@@ -0,0 +1,18 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ up: async (pool) => {
7
+ await pool.query(sql`
8
+ alter table saml_application_configs add column authn_request_config jsonb;
9
+ `);
10
+ },
11
+ down: async (pool) => {
12
+ await pool.query(sql`
13
+ alter table saml_application_configs drop column authn_request_config;
14
+ `);
15
+ },
16
+ };
17
+
18
+ export default alteration;
@@ -0,0 +1,61 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ /**
6
+ * Widen `users.id` and every column that references it to varchar(128).
7
+ *
8
+ * Users migrated from other identity providers may carry over their original IDs, which are
9
+ * often longer than the 12 or 21 characters previously allowed.
10
+ */
11
+
12
+ type Column = [table: string, column: string, originalLength: 12 | 21];
13
+
14
+ const columns: Column[] = [
15
+ ['users', 'id', 12],
16
+ ['user_sign_in_countries', 'user_id', 12],
17
+ ['user_geo_locations', 'user_id', 12],
18
+ ['trusted_devices', 'user_id', 12],
19
+ ['user_sso_identities', 'user_id', 12],
20
+ ['oidc_session_extensions', 'account_id', 12],
21
+ ['users_roles', 'user_id', 21],
22
+ ['organization_user_relations', 'user_id', 21],
23
+ ['organization_role_user_relations', 'user_id', 21],
24
+ ['organization_invitations', 'inviter_id', 21],
25
+ ['organization_invitations', 'accepted_user_id', 21],
26
+ ['subject_tokens', 'user_id', 21],
27
+ ['verification_statuses', 'user_id', 21],
28
+ ['verification_records', 'user_id', 21],
29
+ ['secrets', 'user_id', 21],
30
+ ['personal_access_tokens', 'user_id', 21],
31
+ ['application_access_control_user_relations', 'user_id', 21],
32
+ ['application_user_consent_organizations', 'user_id', 21],
33
+ ['cimd_grant_organizations', 'user_id', 21],
34
+ ['daily_active_users', 'user_id', 21],
35
+ ['aggregated_daily_active_users', 'user_id', 21],
36
+ ];
37
+
38
+ const alteration: AlterationScript = {
39
+ up: async (pool) => {
40
+ for (const [table, column] of columns) {
41
+ // eslint-disable-next-line no-await-in-loop -- DDL statements must run sequentially
42
+ await pool.query(sql`
43
+ alter table ${sql.identifier([table])}
44
+ alter column ${sql.identifier([column])} type varchar(128)
45
+ `);
46
+ }
47
+ },
48
+ down: async (pool) => {
49
+ // Shrinking fails if any existing value is longer than the original limit; there is no
50
+ // safe way to truncate an ID, so let Postgres reject the rollback in that case.
51
+ for (const [table, column, originalLength] of columns) {
52
+ // eslint-disable-next-line no-await-in-loop -- DDL statements must run sequentially
53
+ await pool.query(sql`
54
+ alter table ${sql.identifier([table])}
55
+ alter column ${sql.identifier([column])} type varchar(${sql.raw(String(originalLength))})
56
+ `);
57
+ }
58
+ },
59
+ };
60
+
61
+ export default alteration;
@@ -0,0 +1,14 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ up: async (pool) => {
4
+ await pool.query(sql `
5
+ alter table saml_application_configs add column authn_request_config jsonb;
6
+ `);
7
+ },
8
+ down: async (pool) => {
9
+ await pool.query(sql `
10
+ alter table saml_application_configs drop column authn_request_config;
11
+ `);
12
+ },
13
+ };
14
+ export default alteration;
@@ -0,0 +1,47 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const columns = [
3
+ ['users', 'id', 12],
4
+ ['user_sign_in_countries', 'user_id', 12],
5
+ ['user_geo_locations', 'user_id', 12],
6
+ ['trusted_devices', 'user_id', 12],
7
+ ['user_sso_identities', 'user_id', 12],
8
+ ['oidc_session_extensions', 'account_id', 12],
9
+ ['users_roles', 'user_id', 21],
10
+ ['organization_user_relations', 'user_id', 21],
11
+ ['organization_role_user_relations', 'user_id', 21],
12
+ ['organization_invitations', 'inviter_id', 21],
13
+ ['organization_invitations', 'accepted_user_id', 21],
14
+ ['subject_tokens', 'user_id', 21],
15
+ ['verification_statuses', 'user_id', 21],
16
+ ['verification_records', 'user_id', 21],
17
+ ['secrets', 'user_id', 21],
18
+ ['personal_access_tokens', 'user_id', 21],
19
+ ['application_access_control_user_relations', 'user_id', 21],
20
+ ['application_user_consent_organizations', 'user_id', 21],
21
+ ['cimd_grant_organizations', 'user_id', 21],
22
+ ['daily_active_users', 'user_id', 21],
23
+ ['aggregated_daily_active_users', 'user_id', 21],
24
+ ];
25
+ const alteration = {
26
+ up: async (pool) => {
27
+ for (const [table, column] of columns) {
28
+ // eslint-disable-next-line no-await-in-loop -- DDL statements must run sequentially
29
+ await pool.query(sql `
30
+ alter table ${sql.identifier([table])}
31
+ alter column ${sql.identifier([column])} type varchar(128)
32
+ `);
33
+ }
34
+ },
35
+ down: async (pool) => {
36
+ // Shrinking fails if any existing value is longer than the original limit; there is no
37
+ // safe way to truncate an ID, so let Postgres reject the rollback in that case.
38
+ for (const [table, column, originalLength] of columns) {
39
+ // eslint-disable-next-line no-await-in-loop -- DDL statements must run sequentially
40
+ await pool.query(sql `
41
+ alter table ${sql.identifier([table])}
42
+ alter column ${sql.identifier([column])} type varchar(${sql.raw(String(originalLength))})
43
+ `);
44
+ }
45
+ },
46
+ };
47
+ export default alteration;
@@ -11,6 +11,9 @@ export declare const experience: Readonly<{
11
11
  readonly switchAccount: "switch-account";
12
12
  readonly oneTimeToken: "one-time-token";
13
13
  readonly accountSuspended: "account-suspended";
14
+ readonly trustedDevice: "trusted-device";
15
+ /** Pure step-up: an authenticated session pins the subject and only the missing assurance is asked. */
16
+ readonly stepUp: "step-up";
14
17
  }>;
15
18
  }>;
16
19
  /** `postMessage` sender tag from admin Console sign-in experience live preview. */
@@ -10,6 +10,9 @@ const routes = Object.freeze({
10
10
  switchAccount: 'switch-account',
11
11
  oneTimeToken: 'one-time-token',
12
12
  accountSuspended: 'account-suspended',
13
+ trustedDevice: 'trusted-device',
14
+ /** Pure step-up: an authenticated session pins the subject and only the missing assurance is asked. */
15
+ stepUp: 'step-up',
13
16
  });
14
17
  export const experience = Object.freeze({
15
18
  routes,
@@ -11,3 +11,4 @@ export * from './verification-code.js';
11
11
  export * from './product-event.js';
12
12
  export * from './application.js';
13
13
  export * from './social-state.js';
14
+ export * from './license.js';
@@ -11,3 +11,4 @@ export * from './verification-code.js';
11
11
  export * from './product-event.js';
12
12
  export * from './application.js';
13
13
  export * from './social-state.js';
14
+ export * from './license.js';
@@ -0,0 +1,222 @@
1
+ import { z } from 'zod';
2
+ import { ReservedPlanId } from './subscriptions.js';
3
+ /**
4
+ * The deployment a license key is valid for.
5
+ *
6
+ * Logto signs one key per environment for the same license, identical except for this claim, so a
7
+ * customer can run their staging or CI deployment without sharing the production key. Which key is
8
+ * installed where is the customer's declaration; Logto does not verify it.
9
+ */
10
+ export declare enum LicenseEnv {
11
+ Production = "production",
12
+ NonProduction = "non-production"
13
+ }
14
+ /** The plans a license key can grant. A Cloud-only plan is never a valid license `plan`. */
15
+ export declare const selfHostedPlanIds: readonly [ReservedPlanId.SelfHostedPro, ReservedPlanId.SelfHostedEnterprise];
16
+ /** The plan a license key grants. See {@link selfHostedPlanIds}. */
17
+ export type SelfHostedPlanId = (typeof selfHostedPlanIds)[number];
18
+ /**
19
+ * The entitlements of a self-hosted instance: the features a license unlocks, plus the numeric
20
+ * quotas it raises.
21
+ *
22
+ * The names follow the license payload rather than the Cloud `SubscriptionQuota` vocabulary
23
+ * (`bringYourUi`, not `bringYourUiEnabled`): these values are resolved locally from an installed
24
+ * key and never travel through the Cloud subscription API or its usage reporting.
25
+ * `samlApplicationsLimit` is the one name shared with Cloud, and it keeps the same meaning there —
26
+ * `null` is unlimited, a number is the hard cap.
27
+ */
28
+ export type LicenseQuota = {
29
+ /** Whether the sign-in experience may hide the Logto branding. */
30
+ hideLogtoBranding: boolean;
31
+ /** Whether custom sign-in experience assets and a custom CSP may be configured. */
32
+ bringYourUi: boolean;
33
+ /** Whether IdP-initiated SSO may be configured on an enterprise SSO connector. */
34
+ idpInitiatedSso: boolean;
35
+ /** Whether the instance's Console may be administered by more than one member. */
36
+ consoleCollaboration: boolean;
37
+ /** Whether the sign-in experience MFA policy may be set to mandatory. */
38
+ mandatoryMfa: boolean;
39
+ /**
40
+ * Whether Logto's hosted email service may be used.
41
+ *
42
+ * Always `false` for {@link ReservedPlanId.SelfHostedPro}. The Enterprise offering is deferred,
43
+ * so the flag is carried from the first signed key onwards and simply never set, which keeps the
44
+ * signed shape stable when Enterprise ships.
45
+ */
46
+ hostedEmail: boolean;
47
+ /** How many SAML applications the instance may create. `null` means unlimited. */
48
+ samlApplicationsLimit: number | null;
49
+ };
50
+ export declare const licenseQuotaGuard: z.ZodObject<{
51
+ hideLogtoBranding: z.ZodBoolean;
52
+ bringYourUi: z.ZodBoolean;
53
+ idpInitiatedSso: z.ZodBoolean;
54
+ consoleCollaboration: z.ZodBoolean;
55
+ mandatoryMfa: z.ZodBoolean;
56
+ hostedEmail: z.ZodBoolean;
57
+ samlApplicationsLimit: z.ZodNullable<z.ZodNumber>;
58
+ }, "strip", z.ZodTypeAny, {
59
+ hideLogtoBranding: boolean;
60
+ bringYourUi: boolean;
61
+ idpInitiatedSso: boolean;
62
+ consoleCollaboration: boolean;
63
+ mandatoryMfa: boolean;
64
+ hostedEmail: boolean;
65
+ samlApplicationsLimit: number | null;
66
+ }, {
67
+ hideLogtoBranding: boolean;
68
+ bringYourUi: boolean;
69
+ idpInitiatedSso: boolean;
70
+ consoleCollaboration: boolean;
71
+ mandatoryMfa: boolean;
72
+ hostedEmail: boolean;
73
+ samlApplicationsLimit: number | null;
74
+ }>;
75
+ /**
76
+ * The entitlements a license key grants, as they appear in its payload.
77
+ *
78
+ * Every field is optional: a key only carries what it changes, and an absent field falls back to
79
+ * {@link ossDefaultQuota}. Keeping it partial means a key signed before a new entitlement existed
80
+ * still verifies on a newer Logto, and the missing entitlement stays locked rather than breaking
81
+ * the whole license. Use {@link resolveLicenseQuota} to apply the overrides.
82
+ */
83
+ export declare const licenseQuotaOverridesGuard: z.ZodObject<{
84
+ hideLogtoBranding: z.ZodOptional<z.ZodBoolean>;
85
+ bringYourUi: z.ZodOptional<z.ZodBoolean>;
86
+ idpInitiatedSso: z.ZodOptional<z.ZodBoolean>;
87
+ consoleCollaboration: z.ZodOptional<z.ZodBoolean>;
88
+ mandatoryMfa: z.ZodOptional<z.ZodBoolean>;
89
+ hostedEmail: z.ZodOptional<z.ZodBoolean>;
90
+ samlApplicationsLimit: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
91
+ }, "strip", z.ZodTypeAny, {
92
+ hideLogtoBranding?: boolean | undefined;
93
+ bringYourUi?: boolean | undefined;
94
+ idpInitiatedSso?: boolean | undefined;
95
+ consoleCollaboration?: boolean | undefined;
96
+ mandatoryMfa?: boolean | undefined;
97
+ hostedEmail?: boolean | undefined;
98
+ samlApplicationsLimit?: number | null | undefined;
99
+ }, {
100
+ hideLogtoBranding?: boolean | undefined;
101
+ bringYourUi?: boolean | undefined;
102
+ idpInitiatedSso?: boolean | undefined;
103
+ consoleCollaboration?: boolean | undefined;
104
+ mandatoryMfa?: boolean | undefined;
105
+ hostedEmail?: boolean | undefined;
106
+ samlApplicationsLimit?: number | null | undefined;
107
+ }>;
108
+ export type LicenseQuotaOverrides = z.infer<typeof licenseQuotaOverridesGuard>;
109
+ /**
110
+ * The payload of a license key, i.e. the claims of the Ed25519-signed JWT an operator installs on a
111
+ * self-hosted instance.
112
+ *
113
+ * The signature is what makes the payload trustworthy, so none of it is editable after issuing.
114
+ * Unknown claims are stripped rather than rejected, so a newer license service can add one without
115
+ * invalidating the key on an older Logto.
116
+ */
117
+ export type LicensePayload = {
118
+ /** The plan the license grants. */
119
+ plan: SelfHostedPlanId;
120
+ /** The deployment the key is valid for. */
121
+ env: LicenseEnv;
122
+ /** The Logto customer the license was issued to, for support and renewal lookups. */
123
+ customerId: string;
124
+ /** The license's own identifier, shared by its production and non-production keys. */
125
+ licenseId: string;
126
+ /** When the key was signed, in seconds since the Unix epoch. */
127
+ iat: number;
128
+ /** When the key expires, in seconds since the Unix epoch. */
129
+ exp: number;
130
+ /** The entitlements the license grants on top of {@link ossDefaultQuota}. */
131
+ quota: LicenseQuotaOverrides;
132
+ };
133
+ export declare const licensePayloadGuard: z.ZodObject<{
134
+ plan: z.ZodEnum<[ReservedPlanId.SelfHostedPro, ReservedPlanId.SelfHostedEnterprise]>;
135
+ env: z.ZodNativeEnum<typeof LicenseEnv>;
136
+ customerId: z.ZodString;
137
+ licenseId: z.ZodString;
138
+ iat: z.ZodNumber;
139
+ exp: z.ZodNumber;
140
+ quota: z.ZodObject<{
141
+ hideLogtoBranding: z.ZodOptional<z.ZodBoolean>;
142
+ bringYourUi: z.ZodOptional<z.ZodBoolean>;
143
+ idpInitiatedSso: z.ZodOptional<z.ZodBoolean>;
144
+ consoleCollaboration: z.ZodOptional<z.ZodBoolean>;
145
+ mandatoryMfa: z.ZodOptional<z.ZodBoolean>;
146
+ hostedEmail: z.ZodOptional<z.ZodBoolean>;
147
+ samlApplicationsLimit: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
148
+ }, "strip", z.ZodTypeAny, {
149
+ hideLogtoBranding?: boolean | undefined;
150
+ bringYourUi?: boolean | undefined;
151
+ idpInitiatedSso?: boolean | undefined;
152
+ consoleCollaboration?: boolean | undefined;
153
+ mandatoryMfa?: boolean | undefined;
154
+ hostedEmail?: boolean | undefined;
155
+ samlApplicationsLimit?: number | null | undefined;
156
+ }, {
157
+ hideLogtoBranding?: boolean | undefined;
158
+ bringYourUi?: boolean | undefined;
159
+ idpInitiatedSso?: boolean | undefined;
160
+ consoleCollaboration?: boolean | undefined;
161
+ mandatoryMfa?: boolean | undefined;
162
+ hostedEmail?: boolean | undefined;
163
+ samlApplicationsLimit?: number | null | undefined;
164
+ }>;
165
+ }, "strip", z.ZodTypeAny, {
166
+ exp: number;
167
+ iat: number;
168
+ plan: ReservedPlanId.SelfHostedPro | ReservedPlanId.SelfHostedEnterprise;
169
+ env: LicenseEnv;
170
+ customerId: string;
171
+ licenseId: string;
172
+ quota: {
173
+ hideLogtoBranding?: boolean | undefined;
174
+ bringYourUi?: boolean | undefined;
175
+ idpInitiatedSso?: boolean | undefined;
176
+ consoleCollaboration?: boolean | undefined;
177
+ mandatoryMfa?: boolean | undefined;
178
+ hostedEmail?: boolean | undefined;
179
+ samlApplicationsLimit?: number | null | undefined;
180
+ };
181
+ }, {
182
+ exp: number;
183
+ iat: number;
184
+ plan: ReservedPlanId.SelfHostedPro | ReservedPlanId.SelfHostedEnterprise;
185
+ env: LicenseEnv;
186
+ customerId: string;
187
+ licenseId: string;
188
+ quota: {
189
+ hideLogtoBranding?: boolean | undefined;
190
+ bringYourUi?: boolean | undefined;
191
+ idpInitiatedSso?: boolean | undefined;
192
+ consoleCollaboration?: boolean | undefined;
193
+ mandatoryMfa?: boolean | undefined;
194
+ hostedEmail?: boolean | undefined;
195
+ samlApplicationsLimit?: number | null | undefined;
196
+ };
197
+ }>;
198
+ /**
199
+ * The entitlements of a self-hosted instance with no license key installed, i.e. today's OSS.
200
+ *
201
+ * Every feature is locked and SAML applications keep the long-standing cap of 3. Core falls back to
202
+ * this when no valid license is installed, and Console displays it, so there is exactly one
203
+ * definition of "self-hosted, no license".
204
+ */
205
+ export declare const ossDefaultQuota: Readonly<{
206
+ hideLogtoBranding: false;
207
+ bringYourUi: false;
208
+ idpInitiatedSso: false;
209
+ consoleCollaboration: false;
210
+ mandatoryMfa: false;
211
+ hostedEmail: false;
212
+ samlApplicationsLimit: number;
213
+ }>;
214
+ /**
215
+ * Resolve the effective entitlements of a self-hosted instance by applying a license key's
216
+ * overrides on top of {@link ossDefaultQuota}. Without a key, the OSS defaults are the answer.
217
+ *
218
+ * Core and Console both call this so they can never disagree on what a license grants. The fields
219
+ * are applied one by one on purpose: `samlApplicationsLimit` is nullable and `null` means
220
+ * unlimited, so a `??` merge would silently turn an unlimited license back into the OSS cap.
221
+ */
222
+ export declare const resolveLicenseQuota: (overrides?: LicenseQuotaOverrides) => LicenseQuota;
@@ -0,0 +1,81 @@
1
+ import { z } from 'zod';
2
+ import { ReservedPlanId } from './subscriptions.js';
3
+ /**
4
+ * The deployment a license key is valid for.
5
+ *
6
+ * Logto signs one key per environment for the same license, identical except for this claim, so a
7
+ * customer can run their staging or CI deployment without sharing the production key. Which key is
8
+ * installed where is the customer's declaration; Logto does not verify it.
9
+ */
10
+ export var LicenseEnv;
11
+ (function (LicenseEnv) {
12
+ LicenseEnv["Production"] = "production";
13
+ LicenseEnv["NonProduction"] = "non-production";
14
+ })(LicenseEnv || (LicenseEnv = {}));
15
+ /** The plans a license key can grant. A Cloud-only plan is never a valid license `plan`. */
16
+ export const selfHostedPlanIds = Object.freeze([
17
+ ReservedPlanId.SelfHostedPro,
18
+ ReservedPlanId.SelfHostedEnterprise,
19
+ ]);
20
+ export const licenseQuotaGuard = z.object({
21
+ hideLogtoBranding: z.boolean(),
22
+ bringYourUi: z.boolean(),
23
+ idpInitiatedSso: z.boolean(),
24
+ consoleCollaboration: z.boolean(),
25
+ mandatoryMfa: z.boolean(),
26
+ hostedEmail: z.boolean(),
27
+ samlApplicationsLimit: z.number().int().nonnegative().nullable(),
28
+ });
29
+ /**
30
+ * The entitlements a license key grants, as they appear in its payload.
31
+ *
32
+ * Every field is optional: a key only carries what it changes, and an absent field falls back to
33
+ * {@link ossDefaultQuota}. Keeping it partial means a key signed before a new entitlement existed
34
+ * still verifies on a newer Logto, and the missing entitlement stays locked rather than breaking
35
+ * the whole license. Use {@link resolveLicenseQuota} to apply the overrides.
36
+ */
37
+ export const licenseQuotaOverridesGuard = licenseQuotaGuard.partial();
38
+ export const licensePayloadGuard = z.object({
39
+ plan: z.enum(selfHostedPlanIds),
40
+ env: z.nativeEnum(LicenseEnv),
41
+ customerId: z.string(),
42
+ licenseId: z.string(),
43
+ iat: z.number().int().nonnegative(),
44
+ exp: z.number().int().nonnegative(),
45
+ quota: licenseQuotaOverridesGuard,
46
+ });
47
+ /**
48
+ * The entitlements of a self-hosted instance with no license key installed, i.e. today's OSS.
49
+ *
50
+ * Every feature is locked and SAML applications keep the long-standing cap of 3. Core falls back to
51
+ * this when no valid license is installed, and Console displays it, so there is exactly one
52
+ * definition of "self-hosted, no license".
53
+ */
54
+ export const ossDefaultQuota = Object.freeze({
55
+ hideLogtoBranding: false,
56
+ bringYourUi: false,
57
+ idpInitiatedSso: false,
58
+ consoleCollaboration: false,
59
+ mandatoryMfa: false,
60
+ hostedEmail: false,
61
+ samlApplicationsLimit: 3,
62
+ });
63
+ /**
64
+ * Resolve the effective entitlements of a self-hosted instance by applying a license key's
65
+ * overrides on top of {@link ossDefaultQuota}. Without a key, the OSS defaults are the answer.
66
+ *
67
+ * Core and Console both call this so they can never disagree on what a license grants. The fields
68
+ * are applied one by one on purpose: `samlApplicationsLimit` is nullable and `null` means
69
+ * unlimited, so a `??` merge would silently turn an unlimited license back into the OSS cap.
70
+ */
71
+ export const resolveLicenseQuota = (overrides = {}) => ({
72
+ hideLogtoBranding: overrides.hideLogtoBranding ?? ossDefaultQuota.hideLogtoBranding,
73
+ bringYourUi: overrides.bringYourUi ?? ossDefaultQuota.bringYourUi,
74
+ idpInitiatedSso: overrides.idpInitiatedSso ?? ossDefaultQuota.idpInitiatedSso,
75
+ consoleCollaboration: overrides.consoleCollaboration ?? ossDefaultQuota.consoleCollaboration,
76
+ mandatoryMfa: overrides.mandatoryMfa ?? ossDefaultQuota.mandatoryMfa,
77
+ hostedEmail: overrides.hostedEmail ?? ossDefaultQuota.hostedEmail,
78
+ samlApplicationsLimit: overrides.samlApplicationsLimit === undefined
79
+ ? ossDefaultQuota.samlApplicationsLimit
80
+ : overrides.samlApplicationsLimit,
81
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { LicenseEnv, licensePayloadGuard, ossDefaultQuota, resolveLicenseQuota, } from './license.js';
3
+ import { ReservedPlanId } from './subscriptions.js';
4
+ const validPayload = Object.freeze({
5
+ plan: ReservedPlanId.SelfHostedPro,
6
+ env: LicenseEnv.Production,
7
+ customerId: 'cus_1234567890',
8
+ licenseId: 'lic_1234567890',
9
+ iat: 1_760_000_000,
10
+ exp: 1_791_536_000,
11
+ quota: {
12
+ hideLogtoBranding: true,
13
+ bringYourUi: true,
14
+ idpInitiatedSso: true,
15
+ consoleCollaboration: true,
16
+ mandatoryMfa: true,
17
+ hostedEmail: false,
18
+ samlApplicationsLimit: null,
19
+ },
20
+ });
21
+ describe('licensePayloadGuard', () => {
22
+ it('parses a production Pro license payload', () => {
23
+ expect(licensePayloadGuard.parse(validPayload)).toEqual(validPayload);
24
+ });
25
+ it('parses a non-production Enterprise license payload', () => {
26
+ const payload = {
27
+ ...validPayload,
28
+ plan: ReservedPlanId.SelfHostedEnterprise,
29
+ env: LicenseEnv.NonProduction,
30
+ };
31
+ expect(licensePayloadGuard.parse(payload)).toEqual(payload);
32
+ });
33
+ it('parses a payload carrying only the entitlements it changes', () => {
34
+ const payload = { ...validPayload, quota: { hideLogtoBranding: true } };
35
+ expect(licensePayloadGuard.parse(payload)).toEqual(payload);
36
+ });
37
+ it.each([ReservedPlanId.Free, ReservedPlanId.Pro202509, ReservedPlanId.Development])('rejects the Cloud-only plan %s', (plan) => {
38
+ expect(licensePayloadGuard.safeParse({ ...validPayload, plan }).success).toBe(false);
39
+ });
40
+ it('rejects an unknown environment', () => {
41
+ expect(licensePayloadGuard.safeParse({ ...validPayload, env: 'staging' }).success).toBe(false);
42
+ });
43
+ it.each([-1, 1.5, '3'])('rejects the SAML application limit %j', (samlApplicationsLimit) => {
44
+ expect(licensePayloadGuard.safeParse({
45
+ ...validPayload,
46
+ quota: { ...validPayload.quota, samlApplicationsLimit },
47
+ }).success).toBe(false);
48
+ });
49
+ it('strips unknown claims instead of rejecting them', () => {
50
+ expect(licensePayloadGuard.parse({ ...validPayload, iss: 'https://logto.io' })).toEqual(validPayload);
51
+ });
52
+ });
53
+ describe('ossDefaultQuota', () => {
54
+ it('locks every feature and caps SAML applications at 3', () => {
55
+ expect(ossDefaultQuota).toStrictEqual({
56
+ hideLogtoBranding: false,
57
+ bringYourUi: false,
58
+ idpInitiatedSso: false,
59
+ consoleCollaboration: false,
60
+ mandatoryMfa: false,
61
+ hostedEmail: false,
62
+ samlApplicationsLimit: 3,
63
+ });
64
+ });
65
+ });
66
+ describe('resolveLicenseQuota', () => {
67
+ it('falls back to the OSS defaults without a license', () => {
68
+ expect(resolveLicenseQuota()).toStrictEqual(ossDefaultQuota);
69
+ });
70
+ it('applies every entitlement a license grants', () => {
71
+ expect(resolveLicenseQuota(validPayload.quota)).toStrictEqual({
72
+ hideLogtoBranding: true,
73
+ bringYourUi: true,
74
+ idpInitiatedSso: true,
75
+ consoleCollaboration: true,
76
+ mandatoryMfa: true,
77
+ hostedEmail: false,
78
+ samlApplicationsLimit: null,
79
+ });
80
+ });
81
+ it('keeps the OSS default for an entitlement the license does not carry', () => {
82
+ expect(resolveLicenseQuota({ hideLogtoBranding: true })).toStrictEqual({
83
+ ...ossDefaultQuota,
84
+ hideLogtoBranding: true,
85
+ });
86
+ });
87
+ it('keeps an unlimited SAML application quota instead of falling back to the cap', () => {
88
+ expect(resolveLicenseQuota({ samlApplicationsLimit: null }).samlApplicationsLimit).toBeNull();
89
+ });
90
+ it('applies a SAML application quota of 0', () => {
91
+ expect(resolveLicenseQuota({ samlApplicationsLimit: 0 }).samlApplicationsLimit).toBe(0);
92
+ });
93
+ it('applies an explicitly disabled entitlement', () => {
94
+ expect(resolveLicenseQuota({ hostedEmail: false }).hostedEmail).toBe(false);
95
+ });
96
+ });
@@ -67,7 +67,17 @@ export declare enum ExtraParamsKey {
67
67
  /**
68
68
  * The Google One Tap credential JWT token for external website integration.
69
69
  */
70
- GoogleOneTapCredential = "google_one_tap_credential"
70
+ GoogleOneTapCredential = "google_one_tap_credential",
71
+ /**
72
+ * Overrides the theme that the sign-in experience would otherwise resolve from the end-user's
73
+ * OS settings, so an application with its own light / dark toggle can keep Logto in sync.
74
+ *
75
+ * The parameter is ignored when dark mode is disabled in the sign-in experience configuration,
76
+ * and an unrecognized value is ignored without affecting the other parameters.
77
+ *
78
+ * The available values are `light` and `dark`.
79
+ */
80
+ Theme = "theme"
71
81
  }
72
82
  /** @deprecated Use {@link FirstScreen} instead. */
73
83
  export declare enum InteractionMode {
@@ -94,6 +104,7 @@ export declare const extraParamsObjectGuard: z.ZodObject<{
94
104
  identifier: z.ZodOptional<z.ZodString>;
95
105
  one_time_token: z.ZodOptional<z.ZodString>;
96
106
  google_one_tap_credential: z.ZodOptional<z.ZodString>;
107
+ theme: z.ZodOptional<z.ZodString>;
97
108
  }, "strip", z.ZodTypeAny, {
98
109
  interaction_mode?: InteractionMode | undefined;
99
110
  first_screen?: FirstScreen | undefined;
@@ -104,6 +115,7 @@ export declare const extraParamsObjectGuard: z.ZodObject<{
104
115
  identifier?: string | undefined;
105
116
  one_time_token?: string | undefined;
106
117
  google_one_tap_credential?: string | undefined;
118
+ theme?: string | undefined;
107
119
  }, {
108
120
  interaction_mode?: InteractionMode | undefined;
109
121
  first_screen?: FirstScreen | undefined;
@@ -114,6 +126,7 @@ export declare const extraParamsObjectGuard: z.ZodObject<{
114
126
  identifier?: string | undefined;
115
127
  one_time_token?: string | undefined;
116
128
  google_one_tap_credential?: string | undefined;
129
+ theme?: string | undefined;
117
130
  }>;
118
131
  export type ExtraParamsObject = Partial<{
119
132
  [ExtraParamsKey.InteractionMode]: InteractionMode;
@@ -125,4 +138,5 @@ export type ExtraParamsObject = Partial<{
125
138
  [ExtraParamsKey.Identifier]: string;
126
139
  [ExtraParamsKey.OneTimeToken]: string;
127
140
  [ExtraParamsKey.GoogleOneTapCredential]: string;
141
+ [ExtraParamsKey.Theme]: string;
128
142
  }>;
@@ -70,6 +70,16 @@ export var ExtraParamsKey;
70
70
  * The Google One Tap credential JWT token for external website integration.
71
71
  */
72
72
  ExtraParamsKey["GoogleOneTapCredential"] = "google_one_tap_credential";
73
+ /**
74
+ * Overrides the theme that the sign-in experience would otherwise resolve from the end-user's
75
+ * OS settings, so an application with its own light / dark toggle can keep Logto in sync.
76
+ *
77
+ * The parameter is ignored when dark mode is disabled in the sign-in experience configuration,
78
+ * and an unrecognized value is ignored without affecting the other parameters.
79
+ *
80
+ * The available values are `light` and `dark`.
81
+ */
82
+ ExtraParamsKey["Theme"] = "theme";
73
83
  })(ExtraParamsKey || (ExtraParamsKey = {}));
74
84
  /** @deprecated Use {@link FirstScreen} instead. */
75
85
  export var InteractionMode;
@@ -99,5 +109,6 @@ export const extraParamsObjectGuard = z
99
109
  [ExtraParamsKey.Identifier]: z.string(),
100
110
  [ExtraParamsKey.OneTimeToken]: z.string(),
101
111
  [ExtraParamsKey.GoogleOneTapCredential]: z.string(),
112
+ [ExtraParamsKey.Theme]: z.string(),
102
113
  })
103
114
  .partial();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ExtraParamsKey, FirstScreen, extraParamsObjectGuard } from './oidc.js';
3
+ describe('extraParamsObjectGuard', () => {
4
+ it('should keep the other parameters when the theme is not a supported value', () => {
5
+ // Core parses every extra param in one call and falls back to an empty object when the
6
+ // parse throws, so a loose value here must never discard the parameters next to it.
7
+ const params = extraParamsObjectGuard.parse({
8
+ [ExtraParamsKey.Theme]: 'sepia',
9
+ [ExtraParamsKey.FirstScreen]: FirstScreen.Register,
10
+ [ExtraParamsKey.LoginHint]: 'foo@logto.io',
11
+ });
12
+ expect(params[ExtraParamsKey.FirstScreen]).toBe(FirstScreen.Register);
13
+ expect(params[ExtraParamsKey.LoginHint]).toBe('foo@logto.io');
14
+ });
15
+ });