@logto/schemas 1.40.0 → 1.41.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.41.0-1779864280-add-password-expiration-policy.ts +23 -0
  2. package/alterations/1.41.0-1779864281-add-is-password-expired-to-users.ts +18 -0
  3. package/alterations/1.41.0-1780358400-drop-oidc-model-instances-legacy-grant-id-index.ts +25 -0
  4. package/alterations/1.41.0-1780381219-add-username-policy.ts +41 -0
  5. package/alterations/1.41.0-1780643665-set-sign-up-profile-fields-default.ts +20 -0
  6. package/alterations/1.41.0-1780906060-add-verification-code-policy.ts +19 -0
  7. package/alterations/1.41.0-1781689400-add-sentinel-activities-created-at-index.ts +25 -0
  8. package/alterations/1.41.0-1782354362-set-admin-account-center-profile-fields.ts +28 -0
  9. package/alterations/1.41.0-1782375106-cover-service-logs-tenant-type-index-with-created-at.ts +36 -0
  10. package/alterations-js/1.41.0-1779864280-add-password-expiration-policy.js +19 -0
  11. package/alterations-js/1.41.0-1779864281-add-is-password-expired-to-users.js +14 -0
  12. package/alterations-js/1.41.0-1780358400-drop-oidc-model-instances-legacy-grant-id-index.js +21 -0
  13. package/alterations-js/1.41.0-1780381219-add-username-policy.js +37 -0
  14. package/alterations-js/1.41.0-1780643665-set-sign-up-profile-fields-default.js +16 -0
  15. package/alterations-js/1.41.0-1780906060-add-verification-code-policy.js +15 -0
  16. package/alterations-js/1.41.0-1781689400-add-sentinel-activities-created-at-index.js +21 -0
  17. package/alterations-js/1.41.0-1782354362-set-admin-account-center-profile-fields.js +23 -0
  18. package/alterations-js/1.41.0-1782375106-cover-service-logs-tenant-type-index-with-created-at.js +32 -0
  19. package/lib/consts/experience.d.ts +2 -0
  20. package/lib/consts/experience.js +2 -0
  21. package/lib/consts/index.d.ts +2 -0
  22. package/lib/consts/index.js +2 -0
  23. package/lib/consts/message-rate-limit.d.ts +65 -0
  24. package/lib/consts/message-rate-limit.js +29 -0
  25. package/lib/consts/message-rate-limit.test.d.ts +1 -0
  26. package/lib/consts/message-rate-limit.test.js +20 -0
  27. package/lib/consts/verification-code.d.ts +10 -0
  28. package/lib/consts/verification-code.js +10 -0
  29. package/lib/db-entries/sign-in-experience.d.ts +10 -4
  30. package/lib/db-entries/sign-in-experience.js +13 -1
  31. package/lib/db-entries/user.d.ts +5 -1
  32. package/lib/db-entries/user.js +8 -0
  33. package/lib/foundations/jsonb-types/account-centers.d.ts +3 -0
  34. package/lib/foundations/jsonb-types/account-centers.js +1 -0
  35. package/lib/foundations/jsonb-types/hooks.d.ts +4 -4
  36. package/lib/foundations/jsonb-types/hooks.js +1 -0
  37. package/lib/foundations/jsonb-types/sentinel.d.ts +16 -1
  38. package/lib/foundations/jsonb-types/sentinel.js +15 -0
  39. package/lib/foundations/jsonb-types/sign-in-experience.d.ts +74 -2
  40. package/lib/foundations/jsonb-types/sign-in-experience.js +19 -0
  41. package/lib/foundations/jsonb-types/sign-in-experience.test.js +49 -1
  42. package/lib/foundations/jsonb-types/users.d.ts +9 -0
  43. package/lib/foundations/jsonb-types/users.js +1 -0
  44. package/lib/seeds/account-center.js +1 -0
  45. package/lib/seeds/sign-in-experience.js +1 -0
  46. package/lib/seeds/sign-in-experience.test.js +5 -1
  47. package/lib/types/consent.d.ts +8 -0
  48. package/lib/types/custom-profile-fields.d.ts +4 -0
  49. package/lib/types/hook.d.ts +2 -2
  50. package/lib/types/interactions.js +3 -1
  51. package/lib/types/logto-config/index.d.ts +69 -4
  52. package/lib/types/logto-config/index.js +12 -0
  53. package/lib/types/logto-config/index.test.js +25 -1
  54. package/lib/types/logto-config/inline-hook.d.ts +76 -0
  55. package/lib/types/logto-config/inline-hook.js +25 -0
  56. package/lib/types/logto-config/jwt-customizer.d.ts +133 -1
  57. package/lib/types/logto-config/jwt-customizer.js +14 -0
  58. package/lib/types/saml-application.d.ts +3 -0
  59. package/lib/types/saml-application.js +3 -0
  60. package/lib/types/sign-in-experience.d.ts +9 -0
  61. package/lib/types/ssr.d.ts +11 -0
  62. package/lib/types/user-assets.d.ts +10 -0
  63. package/lib/types/user-assets.js +17 -0
  64. package/lib/types/user-sessions.d.ts +231 -5
  65. package/lib/types/user-sessions.js +5 -0
  66. package/lib/types/user.d.ts +15 -0
  67. package/lib/types/user.js +1 -0
  68. package/package.json +8 -8
  69. package/tables/oidc_model_instances.sql +0 -8
  70. package/tables/sentinel_activities.sql +4 -0
  71. package/tables/service_logs.sql +2 -2
  72. package/tables/sign_in_experiences.sql +15 -2
  73. package/tables/users.sql +7 -0
@@ -0,0 +1,23 @@
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 users
9
+ add column password_updated_at timestamptz;
10
+
11
+ alter table sign_in_experiences
12
+ add column password_expiration jsonb not null default '{}'::jsonb;
13
+ `);
14
+ },
15
+ down: async (pool) => {
16
+ await pool.query(sql`
17
+ alter table users drop column password_updated_at;
18
+ alter table sign_in_experiences drop column password_expiration;
19
+ `);
20
+ },
21
+ };
22
+
23
+ export default alteration;
@@ -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 users add column is_password_expired boolean not null default false;
9
+ `);
10
+ },
11
+ down: async (pool) => {
12
+ await pool.query(sql`
13
+ alter table users drop column is_password_expired;
14
+ `);
15
+ },
16
+ };
17
+
18
+ export default alteration;
@@ -0,0 +1,25 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ beforeUp: async (pool) => {
7
+ await pool.query(sql`
8
+ drop index concurrently if exists oidc_model_instances__model_name_payload_grant_id;
9
+ `);
10
+ },
11
+ up: async () => {
12
+ /** `concurrently` cannot be used inside a transaction. */
13
+ },
14
+ beforeDown: async (pool) => {
15
+ await pool.query(sql`
16
+ create index concurrently if not exists oidc_model_instances__model_name_payload_grant_id
17
+ on oidc_model_instances (tenant_id, model_name, (payload->>'grantId'));
18
+ `);
19
+ },
20
+ down: async () => {
21
+ /** `concurrently` cannot be used inside a transaction. */
22
+ },
23
+ };
24
+
25
+ export default alteration;
@@ -0,0 +1,41 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ // `create index concurrently` cannot run in a transaction; the runner wraps up/down in one.
7
+ beforeUp: async (pool) => {
8
+ await pool.query(sql`
9
+ create index concurrently users__tenant_lower_username
10
+ on users (tenant_id, lower(username))
11
+ where username is not null;
12
+ `);
13
+ },
14
+ up: async (pool) => {
15
+ // The default must be inlined: DDL cannot use bound parameters.
16
+ await pool.query(sql`
17
+ alter table sign_in_experiences
18
+ add column username_policy jsonb not null default '{
19
+ "caseSensitive": true,
20
+ "minLength": 1,
21
+ "maxLength": 128,
22
+ "allowedChars": {
23
+ "lowercase": true,
24
+ "uppercase": true,
25
+ "numbers": true,
26
+ "underscore": true
27
+ }
28
+ }'::jsonb;
29
+ `);
30
+ },
31
+ beforeDown: async (pool) => {
32
+ await pool.query(sql`drop index concurrently users__tenant_lower_username;`);
33
+ },
34
+ down: async (pool) => {
35
+ await pool.query(sql`
36
+ alter table sign_in_experiences drop column username_policy;
37
+ `);
38
+ },
39
+ };
40
+
41
+ export default alteration;
@@ -0,0 +1,20 @@
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 sign_in_experiences
9
+ alter column sign_up_profile_fields set default '[]'::jsonb;
10
+ `);
11
+ },
12
+ down: async (pool) => {
13
+ await pool.query(sql`
14
+ alter table sign_in_experiences
15
+ alter column sign_up_profile_fields drop default;
16
+ `);
17
+ },
18
+ };
19
+
20
+ export default alteration;
@@ -0,0 +1,19 @@
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 sign_in_experiences
9
+ add column if not exists verification_code_policy jsonb not null default '{}'::jsonb;
10
+ `);
11
+ },
12
+ down: async (pool) => {
13
+ await pool.query(sql`
14
+ alter table sign_in_experiences drop column if exists verification_code_policy;
15
+ `);
16
+ },
17
+ };
18
+
19
+ export default alteration;
@@ -0,0 +1,25 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ beforeUp: async (pool) => {
7
+ await pool.query(sql`
8
+ create index concurrently if not exists sentinel_activities__created_at
9
+ on sentinel_activities (created_at);
10
+ `);
11
+ },
12
+ up: async () => {
13
+ /** `concurrently` cannot be used inside a transaction. */
14
+ },
15
+ beforeDown: async (pool) => {
16
+ await pool.query(sql`
17
+ drop index concurrently if exists sentinel_activities__created_at;
18
+ `);
19
+ },
20
+ down: async () => {
21
+ /** `concurrently` cannot be used inside a transaction. */
22
+ },
23
+ };
24
+
25
+ export default alteration;
@@ -0,0 +1,28 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const adminTenantId = 'admin';
6
+
7
+ const alteration: AlterationScript = {
8
+ up: async (pool) => {
9
+ await pool.query(sql`
10
+ update account_centers
11
+ set profile_fields = '[{"name": "name"}, {"name": "avatar"}]'::jsonb
12
+ where tenant_id = ${adminTenantId}
13
+ and id = 'default'
14
+ and profile_fields is null
15
+ `);
16
+ },
17
+ down: async (pool) => {
18
+ await pool.query(sql`
19
+ update account_centers
20
+ set profile_fields = null
21
+ where tenant_id = ${adminTenantId}
22
+ and id = 'default'
23
+ and profile_fields = '[{"name": "name"}, {"name": "avatar"}]'::jsonb
24
+ `);
25
+ },
26
+ };
27
+
28
+ export default alteration;
@@ -0,0 +1,36 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ const alteration: AlterationScript = {
6
+ beforeUp: async (pool) => {
7
+ // Cover the windowed hosted-email usage count (`where tenant_id=? and type=? and created_at>=?`) by
8
+ // adding `created_at` to the existing `(tenant_id, type)` index, then drop the now-redundant
9
+ // 2-column index (a left-prefix subset of the new one). `concurrently` keeps the write path
10
+ // unlocked — this unbounded table is written on every send.
11
+ await pool.query(sql`
12
+ create index concurrently if not exists service_logs__tenant_id__type__created_at
13
+ on service_logs (tenant_id, type, created_at);
14
+ `);
15
+ await pool.query(sql`
16
+ drop index concurrently if exists service_logs__tenant_id__type;
17
+ `);
18
+ },
19
+ up: async () => {
20
+ /** `concurrently` cannot be used inside a transaction. */
21
+ },
22
+ beforeDown: async (pool) => {
23
+ await pool.query(sql`
24
+ create index concurrently if not exists service_logs__tenant_id__type
25
+ on service_logs (tenant_id, type);
26
+ `);
27
+ await pool.query(sql`
28
+ drop index concurrently if exists service_logs__tenant_id__type__created_at;
29
+ `);
30
+ },
31
+ down: async () => {
32
+ /** `concurrently` cannot be used inside a transaction. */
33
+ },
34
+ };
35
+
36
+ export default alteration;
@@ -0,0 +1,19 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ up: async (pool) => {
4
+ await pool.query(sql `
5
+ alter table users
6
+ add column password_updated_at timestamptz;
7
+
8
+ alter table sign_in_experiences
9
+ add column password_expiration jsonb not null default '{}'::jsonb;
10
+ `);
11
+ },
12
+ down: async (pool) => {
13
+ await pool.query(sql `
14
+ alter table users drop column password_updated_at;
15
+ alter table sign_in_experiences drop column password_expiration;
16
+ `);
17
+ },
18
+ };
19
+ 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 users add column is_password_expired boolean not null default false;
6
+ `);
7
+ },
8
+ down: async (pool) => {
9
+ await pool.query(sql `
10
+ alter table users drop column is_password_expired;
11
+ `);
12
+ },
13
+ };
14
+ export default alteration;
@@ -0,0 +1,21 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ beforeUp: async (pool) => {
4
+ await pool.query(sql `
5
+ drop index concurrently if exists oidc_model_instances__model_name_payload_grant_id;
6
+ `);
7
+ },
8
+ up: async () => {
9
+ /** `concurrently` cannot be used inside a transaction. */
10
+ },
11
+ beforeDown: async (pool) => {
12
+ await pool.query(sql `
13
+ create index concurrently if not exists oidc_model_instances__model_name_payload_grant_id
14
+ on oidc_model_instances (tenant_id, model_name, (payload->>'grantId'));
15
+ `);
16
+ },
17
+ down: async () => {
18
+ /** `concurrently` cannot be used inside a transaction. */
19
+ },
20
+ };
21
+ export default alteration;
@@ -0,0 +1,37 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ // `create index concurrently` cannot run in a transaction; the runner wraps up/down in one.
4
+ beforeUp: async (pool) => {
5
+ await pool.query(sql `
6
+ create index concurrently users__tenant_lower_username
7
+ on users (tenant_id, lower(username))
8
+ where username is not null;
9
+ `);
10
+ },
11
+ up: async (pool) => {
12
+ // The default must be inlined: DDL cannot use bound parameters.
13
+ await pool.query(sql `
14
+ alter table sign_in_experiences
15
+ add column username_policy jsonb not null default '{
16
+ "caseSensitive": true,
17
+ "minLength": 1,
18
+ "maxLength": 128,
19
+ "allowedChars": {
20
+ "lowercase": true,
21
+ "uppercase": true,
22
+ "numbers": true,
23
+ "underscore": true
24
+ }
25
+ }'::jsonb;
26
+ `);
27
+ },
28
+ beforeDown: async (pool) => {
29
+ await pool.query(sql `drop index concurrently users__tenant_lower_username;`);
30
+ },
31
+ down: async (pool) => {
32
+ await pool.query(sql `
33
+ alter table sign_in_experiences drop column username_policy;
34
+ `);
35
+ },
36
+ };
37
+ export default alteration;
@@ -0,0 +1,16 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ up: async (pool) => {
4
+ await pool.query(sql `
5
+ alter table sign_in_experiences
6
+ alter column sign_up_profile_fields set default '[]'::jsonb;
7
+ `);
8
+ },
9
+ down: async (pool) => {
10
+ await pool.query(sql `
11
+ alter table sign_in_experiences
12
+ alter column sign_up_profile_fields drop default;
13
+ `);
14
+ },
15
+ };
16
+ export default alteration;
@@ -0,0 +1,15 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ up: async (pool) => {
4
+ await pool.query(sql `
5
+ alter table sign_in_experiences
6
+ add column if not exists verification_code_policy jsonb not null default '{}'::jsonb;
7
+ `);
8
+ },
9
+ down: async (pool) => {
10
+ await pool.query(sql `
11
+ alter table sign_in_experiences drop column if exists verification_code_policy;
12
+ `);
13
+ },
14
+ };
15
+ export default alteration;
@@ -0,0 +1,21 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ beforeUp: async (pool) => {
4
+ await pool.query(sql `
5
+ create index concurrently if not exists sentinel_activities__created_at
6
+ on sentinel_activities (created_at);
7
+ `);
8
+ },
9
+ up: async () => {
10
+ /** `concurrently` cannot be used inside a transaction. */
11
+ },
12
+ beforeDown: async (pool) => {
13
+ await pool.query(sql `
14
+ drop index concurrently if exists sentinel_activities__created_at;
15
+ `);
16
+ },
17
+ down: async () => {
18
+ /** `concurrently` cannot be used inside a transaction. */
19
+ },
20
+ };
21
+ export default alteration;
@@ -0,0 +1,23 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const adminTenantId = 'admin';
3
+ const alteration = {
4
+ up: async (pool) => {
5
+ await pool.query(sql `
6
+ update account_centers
7
+ set profile_fields = '[{"name": "name"}, {"name": "avatar"}]'::jsonb
8
+ where tenant_id = ${adminTenantId}
9
+ and id = 'default'
10
+ and profile_fields is null
11
+ `);
12
+ },
13
+ down: async (pool) => {
14
+ await pool.query(sql `
15
+ update account_centers
16
+ set profile_fields = null
17
+ where tenant_id = ${adminTenantId}
18
+ and id = 'default'
19
+ and profile_fields = '[{"name": "name"}, {"name": "avatar"}]'::jsonb
20
+ `);
21
+ },
22
+ };
23
+ export default alteration;
@@ -0,0 +1,32 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ beforeUp: async (pool) => {
4
+ // Cover the windowed hosted-email usage count (`where tenant_id=? and type=? and created_at>=?`) by
5
+ // adding `created_at` to the existing `(tenant_id, type)` index, then drop the now-redundant
6
+ // 2-column index (a left-prefix subset of the new one). `concurrently` keeps the write path
7
+ // unlocked — this unbounded table is written on every send.
8
+ await pool.query(sql `
9
+ create index concurrently if not exists service_logs__tenant_id__type__created_at
10
+ on service_logs (tenant_id, type, created_at);
11
+ `);
12
+ await pool.query(sql `
13
+ drop index concurrently if exists service_logs__tenant_id__type;
14
+ `);
15
+ },
16
+ up: async () => {
17
+ /** `concurrently` cannot be used inside a transaction. */
18
+ },
19
+ beforeDown: async (pool) => {
20
+ await pool.query(sql `
21
+ create index concurrently if not exists service_logs__tenant_id__type
22
+ on service_logs (tenant_id, type);
23
+ `);
24
+ await pool.query(sql `
25
+ drop index concurrently if exists service_logs__tenant_id__type__created_at;
26
+ `);
27
+ },
28
+ down: async () => {
29
+ /** `concurrently` cannot be used inside a transaction. */
30
+ },
31
+ };
32
+ export default alteration;
@@ -12,3 +12,5 @@ export declare const experience: Readonly<{
12
12
  readonly oneTimeToken: "one-time-token";
13
13
  }>;
14
14
  }>;
15
+ /** `postMessage` sender tag from admin Console sign-in experience live preview. */
16
+ export declare const signInExperiencePreviewMessageSender = "ac_preview";
@@ -13,3 +13,5 @@ const routes = Object.freeze({
13
13
  export const experience = Object.freeze({
14
14
  routes,
15
15
  });
16
+ /** `postMessage` sender tag from admin Console sign-in experience live preview. */
17
+ export const signInExperiencePreviewMessageSender = 'ac_preview';
@@ -6,5 +6,7 @@ export * from './tenant.js';
6
6
  export * from './subscriptions.js';
7
7
  export * from './experience.js';
8
8
  export * from './sentinel.js';
9
+ export * from './message-rate-limit.js';
10
+ export * from './verification-code.js';
9
11
  export * from './product-event.js';
10
12
  export * from './application.js';
@@ -6,5 +6,7 @@ export * from './tenant.js';
6
6
  export * from './subscriptions.js';
7
7
  export * from './experience.js';
8
8
  export * from './sentinel.js';
9
+ export * from './message-rate-limit.js';
10
+ export * from './verification-code.js';
9
11
  export * from './product-event.js';
10
12
  export * from './application.js';
@@ -0,0 +1,65 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * System-level rate-limit policy for outbound messages (verification codes and
4
+ * other notifications) over email/SMS.
5
+ *
6
+ * This is intentionally separate from `SentinelPolicy`: the sentinel throttles
7
+ * *failed verification attempts* and is tenant-configurable (a paid security
8
+ * feature), whereas this throttles *successful sends* and is mandatory and
9
+ * system-level — it is not exposed for tenant editing.
10
+ *
11
+ * Note: a per-send cooldown is intentionally omitted here. The experience app
12
+ * already enforces a client-side resend cooldown, and the windowed per-recipient
13
+ * cap below bounds abuse for non-UI callers without risking a server/client
14
+ * cooldown mismatch that could reject a legitimate resend.
15
+ */
16
+ export type MessageRateLimitPolicy = {
17
+ /** Rolling window, in seconds, over which the per-recipient cap is counted. */
18
+ sendWindow: number;
19
+ /** Maximum number of sends to the same recipient within `sendWindow`. */
20
+ maxSendsPerRecipient: number;
21
+ };
22
+ export declare const messageRateLimitPolicyGuard: z.ZodObject<{
23
+ sendWindow: z.ZodNumber;
24
+ maxSendsPerRecipient: z.ZodNumber;
25
+ }, "strip", z.ZodTypeAny, {
26
+ sendWindow: number;
27
+ maxSendsPerRecipient: number;
28
+ }, {
29
+ sendWindow: number;
30
+ maxSendsPerRecipient: number;
31
+ }>;
32
+ /**
33
+ * The default message rate limit policy. Applied to every tenant and not
34
+ * tenant-configurable.
35
+ *
36
+ * - `sendWindow`: 600 seconds (10 minutes)
37
+ * - `maxSendsPerRecipient`: 10 per window
38
+ *
39
+ * The window is short and rolling on purpose: a falsely-throttled recipient
40
+ * regains capacity within ~10 minutes rather than being unable to receive a
41
+ * message for up to an hour.
42
+ */
43
+ export declare const defaultMessageRateLimitPolicy: Readonly<{
44
+ sendWindow: number;
45
+ maxSendsPerRecipient: number;
46
+ }>;
47
+ /**
48
+ * Internal, ops-only per-tenant override of {@link defaultMessageRateLimitPolicy}. Stored under the
49
+ * `messageRateLimitOverride` key in `logto_configs` and not exposed by any API — it exists solely
50
+ * as a relief valve for a legitimately high-volume tenant that trips the system cap.
51
+ *
52
+ * Every field is optional: the effective policy is resolved per field as `override ?? system
53
+ * default`, so a partial override only changes the fields it sets.
54
+ */
55
+ export declare const messageRateLimitOverrideGuard: z.ZodObject<{
56
+ sendWindow: z.ZodOptional<z.ZodNumber>;
57
+ maxSendsPerRecipient: z.ZodOptional<z.ZodNumber>;
58
+ }, "strip", z.ZodTypeAny, {
59
+ sendWindow?: number | undefined;
60
+ maxSendsPerRecipient?: number | undefined;
61
+ }, {
62
+ sendWindow?: number | undefined;
63
+ maxSendsPerRecipient?: number | undefined;
64
+ }>;
65
+ export type MessageRateLimitOverride = z.infer<typeof messageRateLimitOverrideGuard>;
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ export const messageRateLimitPolicyGuard = z.object({
3
+ sendWindow: z.number().int().positive(),
4
+ maxSendsPerRecipient: z.number().int().positive(),
5
+ });
6
+ /**
7
+ * The default message rate limit policy. Applied to every tenant and not
8
+ * tenant-configurable.
9
+ *
10
+ * - `sendWindow`: 600 seconds (10 minutes)
11
+ * - `maxSendsPerRecipient`: 10 per window
12
+ *
13
+ * The window is short and rolling on purpose: a falsely-throttled recipient
14
+ * regains capacity within ~10 minutes rather than being unable to receive a
15
+ * message for up to an hour.
16
+ */
17
+ export const defaultMessageRateLimitPolicy = Object.freeze({
18
+ sendWindow: 600,
19
+ maxSendsPerRecipient: 10,
20
+ });
21
+ /**
22
+ * Internal, ops-only per-tenant override of {@link defaultMessageRateLimitPolicy}. Stored under the
23
+ * `messageRateLimitOverride` key in `logto_configs` and not exposed by any API — it exists solely
24
+ * as a relief valve for a legitimately high-volume tenant that trips the system cap.
25
+ *
26
+ * Every field is optional: the effective policy is resolved per field as `override ?? system
27
+ * default`, so a partial override only changes the fields it sets.
28
+ */
29
+ export const messageRateLimitOverrideGuard = messageRateLimitPolicyGuard.partial();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defaultMessageRateLimitPolicy, messageRateLimitPolicyGuard, } from './message-rate-limit.js';
3
+ describe('messageRateLimitPolicyGuard', () => {
4
+ it('parses a valid policy', () => {
5
+ expect(messageRateLimitPolicyGuard.parse({ sendWindow: 3600, maxSendsPerRecipient: 5 })).toEqual({ sendWindow: 3600, maxSendsPerRecipient: 5 });
6
+ });
7
+ it('rejects a non-numeric value', () => {
8
+ expect(messageRateLimitPolicyGuard.safeParse({ sendWindow: '3600', maxSendsPerRecipient: 5 }).success).toBe(false);
9
+ });
10
+ it.each([
11
+ { sendWindow: 0, maxSendsPerRecipient: 5 },
12
+ { sendWindow: 3600, maxSendsPerRecipient: 0 },
13
+ { sendWindow: 3600.5, maxSendsPerRecipient: 5 },
14
+ ])('rejects out-of-range or fractional values %o', (policy) => {
15
+ expect(messageRateLimitPolicyGuard.safeParse(policy).success).toBe(false);
16
+ });
17
+ it('accepts the default policy', () => {
18
+ expect(messageRateLimitPolicyGuard.parse(defaultMessageRateLimitPolicy)).toEqual(defaultMessageRateLimitPolicy);
19
+ });
20
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The default verification code policy.
3
+ *
4
+ * - `expirationDuration`: 600 seconds (10 minutes)
5
+ * - `maxRetryAttempts`: 10
6
+ */
7
+ export declare const defaultVerificationCodePolicy: Readonly<{
8
+ expirationDuration: number;
9
+ maxRetryAttempts: number;
10
+ }>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The default verification code policy.
3
+ *
4
+ * - `expirationDuration`: 600 seconds (10 minutes)
5
+ * - `maxRetryAttempts`: 10
6
+ */
7
+ export const defaultVerificationCodePolicy = Object.freeze({
8
+ expirationDuration: 600,
9
+ maxRetryAttempts: 10,
10
+ });