@logto/schemas 1.41.0 → 1.43.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 (151) hide show
  1. package/alterations/1.42.0-1783916980-add-domain-verification-files.ts +20 -0
  2. package/alterations/1.42.0-1784619793-add-service-logs-created-at-index.ts +33 -0
  3. package/alterations/1.42.0-1784620000-add-saml-sso-connector-signing-keys-table.ts +38 -0
  4. package/alterations/1.43.0-1785393731-add-oidc-model-instances-account-id-partial-index.ts +33 -0
  5. package/alterations/1.43.0-1785481546-add-tenant-aware-scope-indexes.ts +33 -0
  6. package/alterations/1.43.0-1785488115-add-cimd-permission-ceiling-tables.ts +77 -0
  7. package/alterations/1.43.0-1785722831-add-oidc-session-extensions-account-id-index.ts +27 -0
  8. package/alterations/1.43.0-1785835991-add-cimd-client-identifier-columns.ts +31 -0
  9. package/alterations/1.43.0-1786325989-add-cimd-grant-organizations-table.ts +54 -0
  10. package/alterations/1.43.0-1786333495-add-trusted-devices.ts +57 -0
  11. package/alterations/1.43.0-1786373004-tighten-cimd-grant-organizations-pkey.ts +28 -0
  12. package/alterations/1.43.0-1786414712-add-service-logs-email-indexes.ts +34 -0
  13. package/alterations/1.43.0-1786431364-add-cimd-grant-client-snapshots-table.ts +43 -0
  14. package/alterations/utils/1704934999-tables.ts +22 -1
  15. package/alterations-js/1.42.0-1783916980-add-domain-verification-files.js +16 -0
  16. package/alterations-js/1.42.0-1784619793-add-service-logs-created-at-index.js +29 -0
  17. package/alterations-js/1.42.0-1784620000-add-saml-sso-connector-signing-keys-table.js +33 -0
  18. package/alterations-js/1.43.0-1785393731-add-oidc-model-instances-account-id-partial-index.js +28 -0
  19. package/alterations-js/1.43.0-1785481546-add-tenant-aware-scope-indexes.js +28 -0
  20. package/alterations-js/1.43.0-1785488115-add-cimd-permission-ceiling-tables.js +69 -0
  21. package/alterations-js/1.43.0-1785722831-add-oidc-session-extensions-account-id-index.js +23 -0
  22. package/alterations-js/1.43.0-1785835991-add-cimd-client-identifier-columns.js +27 -0
  23. package/alterations-js/1.43.0-1786325989-add-cimd-grant-organizations-table.js +49 -0
  24. package/alterations-js/1.43.0-1786333495-add-trusted-devices.js +50 -0
  25. package/alterations-js/1.43.0-1786373004-tighten-cimd-grant-organizations-pkey.js +24 -0
  26. package/alterations-js/1.43.0-1786414712-add-service-logs-email-indexes.js +30 -0
  27. package/alterations-js/1.43.0-1786431364-add-cimd-grant-client-snapshots-table.js +38 -0
  28. package/alterations-js/utils/1704934999-tables.js +19 -1
  29. package/lib/consts/experience.d.ts +1 -0
  30. package/lib/consts/experience.js +1 -0
  31. package/lib/consts/index.d.ts +1 -0
  32. package/lib/consts/index.js +1 -0
  33. package/lib/consts/product-event.d.ts +5 -1
  34. package/lib/consts/product-event.js +4 -0
  35. package/lib/consts/social-state.d.ts +12 -0
  36. package/lib/consts/social-state.js +12 -0
  37. package/lib/db-entries/cimd-grant-client-snapshot.d.ts +32 -0
  38. package/lib/db-entries/cimd-grant-client-snapshot.js +45 -0
  39. package/lib/db-entries/cimd-grant-organization.d.ts +30 -0
  40. package/lib/db-entries/cimd-grant-organization.js +37 -0
  41. package/lib/db-entries/cimd-organization-resource-scope.d.ts +20 -0
  42. package/lib/db-entries/cimd-organization-resource-scope.js +25 -0
  43. package/lib/db-entries/cimd-organization-scope.d.ts +20 -0
  44. package/lib/db-entries/cimd-organization-scope.js +25 -0
  45. package/lib/db-entries/cimd-resource-scope.d.ts +20 -0
  46. package/lib/db-entries/cimd-resource-scope.js +25 -0
  47. package/lib/db-entries/cimd-user-scope.d.ts +20 -0
  48. package/lib/db-entries/cimd-user-scope.js +25 -0
  49. package/lib/db-entries/domain.d.ts +4 -2
  50. package/lib/db-entries/domain.js +5 -1
  51. package/lib/db-entries/index.d.ts +8 -0
  52. package/lib/db-entries/index.js +8 -0
  53. package/lib/db-entries/oidc-session-extension.d.ts +3 -1
  54. package/lib/db-entries/oidc-session-extension.js +4 -0
  55. package/lib/db-entries/organization.d.ts +5 -1
  56. package/lib/db-entries/organization.js +4 -0
  57. package/lib/db-entries/saml-sso-connector-signing-key.d.ts +32 -0
  58. package/lib/db-entries/saml-sso-connector-signing-key.js +49 -0
  59. package/lib/db-entries/sign-in-experience.d.ts +4 -2
  60. package/lib/db-entries/sign-in-experience.js +5 -1
  61. package/lib/db-entries/trusted-device.d.ts +36 -0
  62. package/lib/db-entries/trusted-device.js +62 -0
  63. package/lib/db-entries/user.d.ts +3 -1
  64. package/lib/db-entries/user.js +4 -0
  65. package/lib/foundations/jsonb-types/account-centers.d.ts +3 -0
  66. package/lib/foundations/jsonb-types/account-centers.js +1 -0
  67. package/lib/foundations/jsonb-types/account-centers.test.d.ts +1 -0
  68. package/lib/foundations/jsonb-types/account-centers.test.js +13 -0
  69. package/lib/foundations/jsonb-types/custom-domain.d.ts +57 -0
  70. package/lib/foundations/jsonb-types/custom-domain.js +51 -0
  71. package/lib/foundations/jsonb-types/custom-domain.test.d.ts +1 -0
  72. package/lib/foundations/jsonb-types/custom-domain.test.js +38 -0
  73. package/lib/foundations/jsonb-types/hooks.d.ts +9 -5
  74. package/lib/foundations/jsonb-types/hooks.js +8 -0
  75. package/lib/foundations/jsonb-types/logs.d.ts +18 -0
  76. package/lib/foundations/jsonb-types/logs.js +6 -0
  77. package/lib/foundations/jsonb-types/one-time-tokens.d.ts +5 -0
  78. package/lib/foundations/jsonb-types/one-time-tokens.js +2 -0
  79. package/lib/foundations/jsonb-types/sign-in-experience.d.ts +39 -4
  80. package/lib/foundations/jsonb-types/sign-in-experience.js +10 -0
  81. package/lib/foundations/jsonb-types/sign-in-experience.test.js +17 -1
  82. package/lib/types/application.d.ts +131 -1
  83. package/lib/types/application.js +8 -0
  84. package/lib/types/connector.d.ts +8 -0
  85. package/lib/types/consent.d.ts +16 -190
  86. package/lib/types/consent.js +10 -1
  87. package/lib/types/custom-profile-fields.d.ts +2 -1
  88. package/lib/types/domain.d.ts +9 -0
  89. package/lib/types/hook.d.ts +37 -5
  90. package/lib/types/index.d.ts +1 -0
  91. package/lib/types/index.js +1 -0
  92. package/lib/types/interaction-event.d.ts +11 -0
  93. package/lib/types/interaction-event.js +12 -0
  94. package/lib/types/interactions.d.ts +2 -11
  95. package/lib/types/interactions.js +2 -11
  96. package/lib/types/log/action.d.ts +7 -0
  97. package/lib/types/log/action.js +6 -0
  98. package/lib/types/log/index.d.ts +8 -2
  99. package/lib/types/log/index.js +2 -0
  100. package/lib/types/log/trusted-device.d.ts +7 -0
  101. package/lib/types/log/trusted-device.js +6 -0
  102. package/lib/types/logto-config/action.d.ts +963 -0
  103. package/lib/types/logto-config/action.js +77 -0
  104. package/lib/types/logto-config/index.d.ts +68 -26
  105. package/lib/types/logto-config/index.js +20 -7
  106. package/lib/types/logto-config/index.test.js +32 -9
  107. package/lib/types/logto-config/jwt-customizer.d.ts +60 -10
  108. package/lib/types/logto-config/jwt-customizer.js +1 -1
  109. package/lib/types/logto-config/oidc-provider.d.ts +2 -2
  110. package/lib/types/organization.d.ts +136 -1
  111. package/lib/types/organization.js +3 -2
  112. package/lib/types/service-log.d.ts +14 -1
  113. package/lib/types/service-log.js +13 -0
  114. package/lib/types/sign-in-experience.d.ts +18 -10
  115. package/lib/types/sign-in-experience.js +2 -1
  116. package/lib/types/sso-connector.d.ts +58 -0
  117. package/lib/types/sso-connector.js +12 -0
  118. package/lib/types/system.d.ts +2 -2
  119. package/lib/types/trusted-device.d.ts +67 -0
  120. package/lib/types/trusted-device.js +16 -0
  121. package/lib/types/user-sessions.d.ts +117 -238
  122. package/lib/types/user-sessions.js +9 -4
  123. package/lib/types/user.d.ts +10 -3
  124. package/lib/types/user.js +1 -0
  125. package/lib/types/verification-records/one-time-token-verification.d.ts +5 -0
  126. package/lib/utils/cimd.d.ts +14 -0
  127. package/lib/utils/cimd.js +14 -0
  128. package/lib/utils/cimd.test.d.ts +1 -0
  129. package/lib/utils/cimd.test.js +9 -0
  130. package/lib/utils/index.d.ts +1 -0
  131. package/lib/utils/index.js +1 -0
  132. package/package.json +7 -7
  133. package/tables/cimd_grant_client_snapshots.sql +27 -0
  134. package/tables/cimd_grant_organizations.sql +38 -0
  135. package/tables/cimd_organization_resource_scopes.sql +16 -0
  136. package/tables/cimd_organization_scopes.sql +15 -0
  137. package/tables/cimd_resource_scopes.sql +15 -0
  138. package/tables/cimd_user_scopes.sql +13 -0
  139. package/tables/domains.sql +1 -0
  140. package/tables/oidc_model_instances.sql +5 -0
  141. package/tables/oidc_session_extensions.sql +5 -0
  142. package/tables/organization_scopes.sql +4 -0
  143. package/tables/organizations.sql +2 -0
  144. package/tables/saml_sso_connector_signing_keys.sql +21 -0
  145. package/tables/scopes.sql +4 -0
  146. package/tables/service_logs.sql +19 -0
  147. package/tables/sign_in_experiences.sql +1 -0
  148. package/tables/trusted_devices.sql +25 -0
  149. package/tables/users.sql +1 -0
  150. package/lib/types/logto-config/inline-hook.d.ts +0 -76
  151. package/lib/types/logto-config/inline-hook.js +0 -25
@@ -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 domains
9
+ add column verification_files jsonb not null default '[]'::jsonb;
10
+ `);
11
+ },
12
+ down: async (pool) => {
13
+ await pool.query(sql`
14
+ alter table domains
15
+ drop column verification_files;
16
+ `);
17
+ },
18
+ };
19
+
20
+ export default alteration;
@@ -0,0 +1,33 @@
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
+ // Support the age-based `service_logs` retention prune's global oldest-first scan
8
+ // (`where created_at < ? order by created_at asc limit ?`). The existing
9
+ // `(tenant_id, type, created_at)` index leads with `tenant_id`, so it can't drive a
10
+ // tenant-agnostic time-range scan. `concurrently` keeps the write path unlocked — this
11
+ // unbounded table is written on every send.
12
+ //
13
+ // No `if not exists`: fail loud if the index already exists (e.g. a leftover invalid build from a
14
+ // prior failed concurrent creation) instead of silently skipping.
15
+ await pool.query(sql`
16
+ create index concurrently service_logs__created_at
17
+ on service_logs (created_at);
18
+ `);
19
+ },
20
+ up: async () => {
21
+ /** `concurrently` cannot be used inside a transaction. */
22
+ },
23
+ beforeDown: async (pool) => {
24
+ await pool.query(sql`
25
+ drop index concurrently if exists service_logs__created_at;
26
+ `);
27
+ },
28
+ down: async () => {
29
+ /** `concurrently` cannot be used inside a transaction. */
30
+ },
31
+ };
32
+
33
+ export default alteration;
@@ -0,0 +1,38 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
6
+
7
+ const alteration: AlterationScript = {
8
+ up: async (pool) => {
9
+ await pool.query(sql`
10
+ create table saml_sso_connector_signing_keys (
11
+ id varchar(21) not null,
12
+ tenant_id varchar(21) not null
13
+ references tenants (id) on update cascade on delete cascade,
14
+ sso_connector_id varchar(128) not null
15
+ references sso_connectors (id) on update cascade on delete cascade,
16
+ private_key text not null,
17
+ certificate text not null,
18
+ created_at timestamptz not null default now(),
19
+ expires_at timestamptz not null,
20
+ active boolean not null,
21
+ primary key (tenant_id, sso_connector_id, id)
22
+ );
23
+
24
+ create unique index saml_sso_connector_signing_keys__unique_active
25
+ on saml_sso_connector_signing_keys (tenant_id, sso_connector_id, active)
26
+ where active;
27
+ `);
28
+ await applyTableRls(pool, 'saml_sso_connector_signing_keys');
29
+ },
30
+ down: async (pool) => {
31
+ await dropTableRls(pool, 'saml_sso_connector_signing_keys');
32
+ await pool.query(sql`
33
+ drop table saml_sso_connector_signing_keys;
34
+ `);
35
+ },
36
+ };
37
+
38
+ export default alteration;
@@ -0,0 +1,33 @@
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
+ // Add index to optimize revoking access tokens and refresh tokens by accountId.
8
+ // See the index comment in tables/oidc_model_instances.sql for the predicate rationale.
9
+ await pool.query(sql`
10
+ create index concurrently oidc_model_instances__model_name_payload_account_id_partial
11
+ on oidc_model_instances (tenant_id, model_name, (payload->>'accountId'))
12
+ where payload ? 'accountId';
13
+ `);
14
+
15
+ // Collect expression statistics right away instead of waiting for autovacuum.
16
+ await pool.query(sql`
17
+ analyze oidc_model_instances;
18
+ `);
19
+ },
20
+ up: async () => {
21
+ /** 'concurrently' cannot be used inside a transaction, so this up is intentionally left empty. */
22
+ },
23
+ beforeDown: async (pool) => {
24
+ await pool.query(sql`
25
+ drop index concurrently oidc_model_instances__model_name_payload_account_id_partial;
26
+ `);
27
+ },
28
+ down: async () => {
29
+ /** 'concurrently' cannot be used inside a transaction, so this down is intentionally left empty. */
30
+ },
31
+ };
32
+
33
+ export default alteration;
@@ -0,0 +1,33 @@
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 unique index concurrently scopes__tenant_id_id
9
+ on scopes (tenant_id, id);
10
+ `);
11
+
12
+ await pool.query(sql`
13
+ create unique index concurrently organization_scopes__tenant_id_id
14
+ on organization_scopes (tenant_id, id);
15
+ `);
16
+ },
17
+ up: async () => {
18
+ // The indexes must be created outside of a transaction.
19
+ },
20
+ beforeDown: async (pool) => {
21
+ await pool.query(sql`
22
+ drop index concurrently if exists organization_scopes__tenant_id_id;
23
+ `);
24
+ await pool.query(sql`
25
+ drop index concurrently if exists scopes__tenant_id_id;
26
+ `);
27
+ },
28
+ down: async () => {
29
+ // The indexes must be dropped outside of a transaction.
30
+ },
31
+ };
32
+
33
+ export default alteration;
@@ -0,0 +1,77 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
6
+
7
+ /**
8
+ * Add the tenant-level permission ceiling tables for client ID metadata document (CIMD) clients.
9
+ * The ceiling is tenant-wide because CIMD clients are unregistered and have no per-client record
10
+ * to attach configuration to. The tables reference the tenant and scope tables directly (not the
11
+ * CIMD config row), so disabling CIMD does not wipe the permission settings.
12
+ * Rows are an allowlist: an empty table allows nothing (deny-all), and an unrestricted state is not
13
+ * expressible.
14
+ */
15
+ const alteration: AlterationScript = {
16
+ up: async (pool) => {
17
+ await pool.query(sql`
18
+ create table cimd_user_scopes (
19
+ tenant_id varchar(21) not null
20
+ references tenants (id) on update cascade on delete cascade,
21
+ user_scope varchar(64) not null,
22
+ primary key (tenant_id, user_scope)
23
+ );
24
+ `);
25
+ await applyTableRls(pool, 'cimd_user_scopes');
26
+
27
+ await pool.query(sql`
28
+ create table cimd_resource_scopes (
29
+ tenant_id varchar(21) not null
30
+ references tenants (id) on update cascade on delete cascade,
31
+ scope_id varchar(21) not null,
32
+ primary key (tenant_id, scope_id),
33
+ foreign key (tenant_id, scope_id)
34
+ references scopes (tenant_id, id) on update cascade on delete cascade
35
+ );
36
+ `);
37
+ await applyTableRls(pool, 'cimd_resource_scopes');
38
+
39
+ await pool.query(sql`
40
+ create table cimd_organization_scopes (
41
+ tenant_id varchar(21) not null
42
+ references tenants (id) on update cascade on delete cascade,
43
+ organization_scope_id varchar(21) not null,
44
+ primary key (tenant_id, organization_scope_id),
45
+ foreign key (tenant_id, organization_scope_id)
46
+ references organization_scopes (tenant_id, id) on update cascade on delete cascade
47
+ );
48
+ `);
49
+ await applyTableRls(pool, 'cimd_organization_scopes');
50
+
51
+ await pool.query(sql`
52
+ create table cimd_organization_resource_scopes (
53
+ tenant_id varchar(21) not null
54
+ references tenants (id) on update cascade on delete cascade,
55
+ scope_id varchar(21) not null,
56
+ primary key (tenant_id, scope_id),
57
+ foreign key (tenant_id, scope_id)
58
+ references scopes (tenant_id, id) on update cascade on delete cascade
59
+ );
60
+ `);
61
+ await applyTableRls(pool, 'cimd_organization_resource_scopes');
62
+ },
63
+ down: async (pool) => {
64
+ await dropTableRls(pool, 'cimd_organization_resource_scopes');
65
+ await dropTableRls(pool, 'cimd_organization_scopes');
66
+ await dropTableRls(pool, 'cimd_resource_scopes');
67
+ await dropTableRls(pool, 'cimd_user_scopes');
68
+ await pool.query(sql`
69
+ drop table cimd_organization_resource_scopes;
70
+ drop table cimd_organization_scopes;
71
+ drop table cimd_resource_scopes;
72
+ drop table cimd_user_scopes;
73
+ `);
74
+ },
75
+ };
76
+
77
+ export default alteration;
@@ -0,0 +1,27 @@
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
+ // Add index to optimize deleting session extensions by accountId, including the
8
+ // cascades from the users table. See the index comment in tables/oidc_session_extensions.sql.
9
+ await pool.query(sql`
10
+ create index concurrently oidc_session_extensions__account_id
11
+ on oidc_session_extensions (account_id);
12
+ `);
13
+ },
14
+ up: async () => {
15
+ /** 'concurrently' cannot be used inside a transaction, so this up is intentionally left empty. */
16
+ },
17
+ beforeDown: async (pool) => {
18
+ await pool.query(sql`
19
+ drop index concurrently oidc_session_extensions__account_id;
20
+ `);
21
+ },
22
+ down: async () => {
23
+ /** 'concurrently' cannot be used inside a transaction, so this down is intentionally left empty. */
24
+ },
25
+ };
26
+
27
+ export default alteration;
@@ -0,0 +1,31 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ /**
6
+ * Registered application IDs (21 chars, referencing `applications`) and CIMD (client ID metadata
7
+ * document) client identifier URLs are different value spaces, so each gets its own column instead
8
+ * of sharing one.
9
+ */
10
+ const alteration: AlterationScript = {
11
+ up: async (pool) => {
12
+ await pool.query(sql`
13
+ alter table users
14
+ add column cimd_client_id varchar(2048);
15
+
16
+ alter table oidc_session_extensions
17
+ add column cimd_client_id varchar(2048);
18
+ `);
19
+ },
20
+ down: async (pool) => {
21
+ await pool.query(sql`
22
+ alter table users
23
+ drop column cimd_client_id;
24
+
25
+ alter table oidc_session_extensions
26
+ drop column cimd_client_id;
27
+ `);
28
+ },
29
+ };
30
+
31
+ export default alteration;
@@ -0,0 +1,54 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
6
+
7
+ /**
8
+ * Add the grant-scoped organization authorization table for client ID metadata document (CIMD)
9
+ * clients. CIMD clients are unregistered URL identities with no deletion lifecycle and
10
+ * transferable ownership, so organization access follows the single authorization instead of a
11
+ * long-lived cross-grant relation: rows cascade away with the Grant row (revocation hard-deletes
12
+ * it) and with the user's organization membership.
13
+ */
14
+ const alteration: AlterationScript = {
15
+ up: async (pool) => {
16
+ await pool.query(sql`
17
+ create table cimd_grant_organizations (
18
+ tenant_id varchar(21) not null
19
+ references tenants (id) on update cascade on delete cascade,
20
+ grant_id varchar(128) not null,
21
+ grant_model_name varchar(64) not null default 'Grant',
22
+ organization_id varchar(21) not null,
23
+ user_id varchar(21) not null,
24
+ primary key (tenant_id, grant_id, organization_id),
25
+ constraint cimd_grant_organizations__grant_model_name
26
+ check (grant_model_name = 'Grant'),
27
+ foreign key (tenant_id, grant_model_name, grant_id)
28
+ references oidc_model_instances (tenant_id, model_name, id)
29
+ on update cascade on delete cascade,
30
+ foreign key (tenant_id, organization_id, user_id)
31
+ references organization_user_relations (tenant_id, organization_id, user_id)
32
+ on update cascade on delete cascade
33
+ );
34
+ `);
35
+ /**
36
+ * Backs the membership FK's cascade lookup; the PK leaves `organization_id` outside its
37
+ * usable prefix. The table is created empty in this transaction, so a plain (non-concurrent)
38
+ * index build is free.
39
+ */
40
+ await pool.query(sql`
41
+ create index cimd_grant_organizations__organization_id_user_id
42
+ on cimd_grant_organizations (tenant_id, organization_id, user_id);
43
+ `);
44
+ await applyTableRls(pool, 'cimd_grant_organizations');
45
+ },
46
+ down: async (pool) => {
47
+ await dropTableRls(pool, 'cimd_grant_organizations');
48
+ await pool.query(sql`
49
+ drop table cimd_grant_organizations;
50
+ `);
51
+ },
52
+ };
53
+
54
+ export default alteration;
@@ -0,0 +1,57 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
6
+
7
+ const alteration: AlterationScript = {
8
+ up: async (pool) => {
9
+ await pool.query(sql`
10
+ create table trusted_devices (
11
+ tenant_id varchar(21) not null
12
+ references tenants (id) on update cascade on delete cascade,
13
+ id varchar(21) not null,
14
+ user_id varchar(12) not null
15
+ references users (id) on update cascade on delete cascade,
16
+ secret_hash bytea not null,
17
+ user_agent text,
18
+ ip text,
19
+ country varchar(16),
20
+ city text,
21
+ created_at timestamptz not null default(now()),
22
+ last_used_at timestamptz not null default(now()),
23
+ expires_at timestamptz not null,
24
+ primary key (id)
25
+ );
26
+
27
+ create index trusted_devices__tenant_user_expires_at
28
+ on trusted_devices (tenant_id, user_id, expires_at);
29
+
30
+ create index trusted_devices__tenant_expires_at
31
+ on trusted_devices (tenant_id, expires_at);
32
+
33
+ alter table sign_in_experiences
34
+ add column trusted_device jsonb not null default '{}'::jsonb;
35
+
36
+ alter table organizations
37
+ add column is_trusted_device_allowed boolean not null default true;
38
+ `);
39
+
40
+ await applyTableRls(pool, 'trusted_devices');
41
+ },
42
+ down: async (pool) => {
43
+ await dropTableRls(pool, 'trusted_devices');
44
+
45
+ await pool.query(sql`
46
+ drop table trusted_devices;
47
+
48
+ alter table sign_in_experiences
49
+ drop column trusted_device;
50
+
51
+ alter table organizations
52
+ drop column is_trusted_device_allowed;
53
+ `);
54
+ },
55
+ };
56
+
57
+ 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
+ /**
6
+ * Tighten the `cimd_grant_organizations` primary key to `(tenant_id, grant_id)` so the schema
7
+ * itself enforces the at-most-one-organization-per-grant invariant that token issuance relies on,
8
+ * instead of leaving it to the consent flow's composition. The table is dev-feature-gated and
9
+ * empty in every environment, so swapping the constraint needs no data migration.
10
+ */
11
+ const alteration: AlterationScript = {
12
+ up: async (pool) => {
13
+ await pool.query(sql`
14
+ alter table cimd_grant_organizations
15
+ drop constraint cimd_grant_organizations_pkey,
16
+ add primary key (tenant_id, grant_id);
17
+ `);
18
+ },
19
+ down: async (pool) => {
20
+ await pool.query(sql`
21
+ alter table cimd_grant_organizations
22
+ drop constraint cimd_grant_organizations_pkey,
23
+ add primary key (tenant_id, grant_id, organization_id);
24
+ `);
25
+ },
26
+ };
27
+
28
+ export default alteration;
@@ -0,0 +1,34 @@
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 service_logs__email__tenant_id__created_at__id
9
+ on service_logs (tenant_id, created_at desc, id desc)
10
+ where type in ('sendEmail', 'sendEmailFailed');
11
+ `);
12
+ await pool.query(sql`
13
+ create index concurrently service_logs__email__tenant_id__recipient__created_at__id
14
+ on service_logs (tenant_id, lower(payload->'data'->>'to'), created_at desc, id desc)
15
+ where type in ('sendEmail', 'sendEmailFailed');
16
+ `);
17
+ },
18
+ up: async () => {
19
+ /** 'concurrently' cannot be used inside a transaction, so this up is intentionally left empty. */
20
+ },
21
+ beforeDown: async (pool) => {
22
+ await pool.query(sql`
23
+ drop index concurrently service_logs__email__tenant_id__recipient__created_at__id;
24
+ `);
25
+ await pool.query(sql`
26
+ drop index concurrently service_logs__email__tenant_id__created_at__id;
27
+ `);
28
+ },
29
+ down: async () => {
30
+ /** 'concurrently' cannot be used inside a transaction, so this down is intentionally left empty. */
31
+ },
32
+ };
33
+
34
+ export default alteration;
@@ -0,0 +1,43 @@
1
+ import { sql } from '@silverhand/slonik';
2
+
3
+ import type { AlterationScript } from '../lib/types/alteration.js';
4
+
5
+ import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
6
+
7
+ /**
8
+ * Add the per-grant client display snapshot table for client ID metadata document (CIMD) clients.
9
+ * CIMD clients are unregistered URL identities without an `applications` row, and an unvetted
10
+ * client can rewrite its hosted document at any time — so the grant list renders the snapshot
11
+ * captured at consent instead of refetching the document. Rows cascade away with the Grant row.
12
+ */
13
+ const alteration: AlterationScript = {
14
+ up: async (pool) => {
15
+ await pool.query(sql`
16
+ create table cimd_grant_client_snapshots (
17
+ tenant_id varchar(21) not null
18
+ references tenants (id) on update cascade on delete cascade,
19
+ grant_id varchar(128) not null,
20
+ grant_model_name varchar(64) not null default 'Grant',
21
+ client_id varchar(2048) not null,
22
+ name varchar(256),
23
+ logo_uri varchar(2048),
24
+ created_at timestamptz not null default(now()),
25
+ primary key (tenant_id, grant_id),
26
+ constraint cimd_grant_client_snapshots__grant_model_name
27
+ check (grant_model_name = 'Grant'),
28
+ foreign key (tenant_id, grant_model_name, grant_id)
29
+ references oidc_model_instances (tenant_id, model_name, id)
30
+ on update cascade on delete cascade
31
+ );
32
+ `);
33
+ await applyTableRls(pool, 'cimd_grant_client_snapshots');
34
+ },
35
+ down: async (pool) => {
36
+ await dropTableRls(pool, 'cimd_grant_client_snapshots');
37
+ await pool.query(sql`
38
+ drop table cimd_grant_client_snapshots;
39
+ `);
40
+ },
41
+ };
42
+
43
+ export default alteration;
@@ -2,12 +2,33 @@ import { type CommonQueryMethods, sql } from '@silverhand/slonik';
2
2
 
3
3
  const getId = (value: string) => sql.identifier([value]);
4
4
 
5
+ /**
6
+ * Returns the suffix used in the base Logto role name for the current database
7
+ * (`logto_tenant_<suffix>`).
8
+ *
9
+ * Historically the suffix was the raw database name (which may contain hyphens). A later
10
+ * convention normalised it by replacing hyphens with underscores. To remain compatible with
11
+ * both conventions we probe `pg_roles` for the normalised name first and fall back to the
12
+ * raw database name for legacy installations.
13
+ */
5
14
  const getDatabaseName = async (pool: CommonQueryMethods) => {
6
15
  const { currentDatabase } = await pool.one<{ currentDatabase: string }>(sql`
7
16
  select current_database();
8
17
  `);
9
18
 
10
- return currentDatabase.replaceAll('-', '_');
19
+ const normalized = currentDatabase.replaceAll('-', '_');
20
+
21
+ if (normalized === currentDatabase) {
22
+ return currentDatabase;
23
+ }
24
+
25
+ const { exists } = await pool.one<{ exists: boolean }>(sql`
26
+ select exists(
27
+ select 1 from pg_roles where rolname = ${'logto_tenant_' + normalized}
28
+ ) as exists;
29
+ `);
30
+
31
+ return exists ? normalized : currentDatabase;
11
32
  };
12
33
 
13
34
  /**
@@ -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 domains
6
+ add column verification_files jsonb not null default '[]'::jsonb;
7
+ `);
8
+ },
9
+ down: async (pool) => {
10
+ await pool.query(sql `
11
+ alter table domains
12
+ drop column verification_files;
13
+ `);
14
+ },
15
+ };
16
+ export default alteration;
@@ -0,0 +1,29 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ beforeUp: async (pool) => {
4
+ // Support the age-based `service_logs` retention prune's global oldest-first scan
5
+ // (`where created_at < ? order by created_at asc limit ?`). The existing
6
+ // `(tenant_id, type, created_at)` index leads with `tenant_id`, so it can't drive a
7
+ // tenant-agnostic time-range scan. `concurrently` keeps the write path unlocked — this
8
+ // unbounded table is written on every send.
9
+ //
10
+ // No `if not exists`: fail loud if the index already exists (e.g. a leftover invalid build from a
11
+ // prior failed concurrent creation) instead of silently skipping.
12
+ await pool.query(sql `
13
+ create index concurrently service_logs__created_at
14
+ on service_logs (created_at);
15
+ `);
16
+ },
17
+ up: async () => {
18
+ /** `concurrently` cannot be used inside a transaction. */
19
+ },
20
+ beforeDown: async (pool) => {
21
+ await pool.query(sql `
22
+ drop index concurrently if exists service_logs__created_at;
23
+ `);
24
+ },
25
+ down: async () => {
26
+ /** `concurrently` cannot be used inside a transaction. */
27
+ },
28
+ };
29
+ export default alteration;
@@ -0,0 +1,33 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ import { applyTableRls, dropTableRls } from './utils/1704934999-tables.js';
3
+ const alteration = {
4
+ up: async (pool) => {
5
+ await pool.query(sql `
6
+ create table saml_sso_connector_signing_keys (
7
+ id varchar(21) not null,
8
+ tenant_id varchar(21) not null
9
+ references tenants (id) on update cascade on delete cascade,
10
+ sso_connector_id varchar(128) not null
11
+ references sso_connectors (id) on update cascade on delete cascade,
12
+ private_key text not null,
13
+ certificate text not null,
14
+ created_at timestamptz not null default now(),
15
+ expires_at timestamptz not null,
16
+ active boolean not null,
17
+ primary key (tenant_id, sso_connector_id, id)
18
+ );
19
+
20
+ create unique index saml_sso_connector_signing_keys__unique_active
21
+ on saml_sso_connector_signing_keys (tenant_id, sso_connector_id, active)
22
+ where active;
23
+ `);
24
+ await applyTableRls(pool, 'saml_sso_connector_signing_keys');
25
+ },
26
+ down: async (pool) => {
27
+ await dropTableRls(pool, 'saml_sso_connector_signing_keys');
28
+ await pool.query(sql `
29
+ drop table saml_sso_connector_signing_keys;
30
+ `);
31
+ },
32
+ };
33
+ export default alteration;
@@ -0,0 +1,28 @@
1
+ import { sql } from '@silverhand/slonik';
2
+ const alteration = {
3
+ beforeUp: async (pool) => {
4
+ // Add index to optimize revoking access tokens and refresh tokens by accountId.
5
+ // See the index comment in tables/oidc_model_instances.sql for the predicate rationale.
6
+ await pool.query(sql `
7
+ create index concurrently oidc_model_instances__model_name_payload_account_id_partial
8
+ on oidc_model_instances (tenant_id, model_name, (payload->>'accountId'))
9
+ where payload ? 'accountId';
10
+ `);
11
+ // Collect expression statistics right away instead of waiting for autovacuum.
12
+ await pool.query(sql `
13
+ analyze oidc_model_instances;
14
+ `);
15
+ },
16
+ up: async () => {
17
+ /** 'concurrently' cannot be used inside a transaction, so this up is intentionally left empty. */
18
+ },
19
+ beforeDown: async (pool) => {
20
+ await pool.query(sql `
21
+ drop index concurrently oidc_model_instances__model_name_payload_account_id_partial;
22
+ `);
23
+ },
24
+ down: async () => {
25
+ /** 'concurrently' cannot be used inside a transaction, so this down is intentionally left empty. */
26
+ },
27
+ };
28
+ export default alteration;