@logto/schemas 1.40.1 → 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.
- package/alterations/1.41.0-1779864280-add-password-expiration-policy.ts +23 -0
- package/alterations/1.41.0-1779864281-add-is-password-expired-to-users.ts +18 -0
- package/alterations/1.41.0-1780358400-drop-oidc-model-instances-legacy-grant-id-index.ts +25 -0
- package/alterations/1.41.0-1780381219-add-username-policy.ts +41 -0
- package/alterations/1.41.0-1780643665-set-sign-up-profile-fields-default.ts +20 -0
- package/alterations/1.41.0-1780906060-add-verification-code-policy.ts +19 -0
- package/alterations/1.41.0-1781689400-add-sentinel-activities-created-at-index.ts +25 -0
- package/alterations/1.41.0-1782354362-set-admin-account-center-profile-fields.ts +28 -0
- package/alterations/1.41.0-1782375106-cover-service-logs-tenant-type-index-with-created-at.ts +36 -0
- package/alterations-js/1.41.0-1779864280-add-password-expiration-policy.js +19 -0
- package/alterations-js/1.41.0-1779864281-add-is-password-expired-to-users.js +14 -0
- package/alterations-js/1.41.0-1780358400-drop-oidc-model-instances-legacy-grant-id-index.js +21 -0
- package/alterations-js/1.41.0-1780381219-add-username-policy.js +37 -0
- package/alterations-js/1.41.0-1780643665-set-sign-up-profile-fields-default.js +16 -0
- package/alterations-js/1.41.0-1780906060-add-verification-code-policy.js +15 -0
- package/alterations-js/1.41.0-1781689400-add-sentinel-activities-created-at-index.js +21 -0
- package/alterations-js/1.41.0-1782354362-set-admin-account-center-profile-fields.js +23 -0
- package/alterations-js/1.41.0-1782375106-cover-service-logs-tenant-type-index-with-created-at.js +32 -0
- package/lib/consts/experience.d.ts +2 -0
- package/lib/consts/experience.js +2 -0
- package/lib/consts/index.d.ts +2 -0
- package/lib/consts/index.js +2 -0
- package/lib/consts/message-rate-limit.d.ts +65 -0
- package/lib/consts/message-rate-limit.js +29 -0
- package/lib/consts/message-rate-limit.test.d.ts +1 -0
- package/lib/consts/message-rate-limit.test.js +20 -0
- package/lib/consts/verification-code.d.ts +10 -0
- package/lib/consts/verification-code.js +10 -0
- package/lib/db-entries/sign-in-experience.d.ts +10 -4
- package/lib/db-entries/sign-in-experience.js +13 -1
- package/lib/db-entries/user.d.ts +5 -1
- package/lib/db-entries/user.js +8 -0
- package/lib/foundations/jsonb-types/account-centers.d.ts +3 -0
- package/lib/foundations/jsonb-types/account-centers.js +1 -0
- package/lib/foundations/jsonb-types/hooks.d.ts +4 -4
- package/lib/foundations/jsonb-types/hooks.js +1 -0
- package/lib/foundations/jsonb-types/sentinel.d.ts +16 -1
- package/lib/foundations/jsonb-types/sentinel.js +15 -0
- package/lib/foundations/jsonb-types/sign-in-experience.d.ts +74 -2
- package/lib/foundations/jsonb-types/sign-in-experience.js +19 -0
- package/lib/foundations/jsonb-types/sign-in-experience.test.js +49 -1
- package/lib/foundations/jsonb-types/users.d.ts +9 -0
- package/lib/foundations/jsonb-types/users.js +1 -0
- package/lib/seeds/account-center.js +1 -0
- package/lib/seeds/sign-in-experience.js +1 -0
- package/lib/seeds/sign-in-experience.test.js +5 -1
- package/lib/types/consent.d.ts +8 -0
- package/lib/types/custom-profile-fields.d.ts +4 -0
- package/lib/types/hook.d.ts +2 -2
- package/lib/types/interactions.js +3 -1
- package/lib/types/logto-config/index.d.ts +69 -4
- package/lib/types/logto-config/index.js +12 -0
- package/lib/types/logto-config/index.test.js +25 -1
- package/lib/types/logto-config/inline-hook.d.ts +76 -0
- package/lib/types/logto-config/inline-hook.js +25 -0
- package/lib/types/logto-config/jwt-customizer.d.ts +133 -1
- package/lib/types/logto-config/jwt-customizer.js +14 -0
- package/lib/types/saml-application.d.ts +3 -0
- package/lib/types/saml-application.js +3 -0
- package/lib/types/sign-in-experience.d.ts +9 -0
- package/lib/types/ssr.d.ts +11 -0
- package/lib/types/user-assets.d.ts +10 -0
- package/lib/types/user-assets.js +17 -0
- package/lib/types/user-sessions.d.ts +231 -5
- package/lib/types/user-sessions.js +5 -0
- package/lib/types/user.d.ts +15 -0
- package/lib/types/user.js +1 -0
- package/package.json +8 -8
- package/tables/oidc_model_instances.sql +0 -8
- package/tables/sentinel_activities.sql +4 -0
- package/tables/service_logs.sql +2 -2
- package/tables/sign_in_experiences.sql +15 -2
- 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;
|
package/alterations-js/1.41.0-1782375106-cover-service-logs-tenant-type-index-with-created-at.js
ADDED
|
@@ -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;
|
package/lib/consts/experience.js
CHANGED
package/lib/consts/index.d.ts
CHANGED
|
@@ -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';
|
package/lib/consts/index.js
CHANGED
|
@@ -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
|
+
}>;
|