@axiom-lattice/pg-stores 5.0.3 → 5.1.1

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 (37) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +102 -0
  3. package/dist/index.d.mts +178 -2
  4. package/dist/index.d.ts +178 -2
  5. package/dist/index.js +1243 -197
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +1167 -124
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/src/__tests__/PostgreSQLInviteLinkStore.test.ts +63 -0
  11. package/src/__tests__/PostgreSQLOidcIdentityClaimStore.test.ts +123 -0
  12. package/src/__tests__/PostgreSQLOidcProviderConfigStore.test.ts +233 -0
  13. package/src/__tests__/PostgreSQLPlatformInvitationStore.test.ts +222 -0
  14. package/src/__tests__/PostgreSQLPlatformMemberStore.test.ts +146 -0
  15. package/src/__tests__/PostgreSQLPlatformSettingsStore.test.ts +115 -0
  16. package/src/__tests__/PostgreSQLUserExternalIdentityStore.test.ts +125 -0
  17. package/src/__tests__/PostgreSQLUserStore.test.ts +75 -0
  18. package/src/createPgStoreConfig.ts +34 -0
  19. package/src/index.ts +16 -0
  20. package/src/migrations/invite_link_migration.ts +35 -0
  21. package/src/migrations/oidc_identity_claim_invite_migration.ts +29 -0
  22. package/src/migrations/oidc_identity_claim_migration.ts +39 -0
  23. package/src/migrations/oidc_provider_allowed_hosts_migration.ts +19 -0
  24. package/src/migrations/oidc_provider_config_migration.ts +27 -0
  25. package/src/migrations/platform_invitation_migration.ts +22 -0
  26. package/src/migrations/platform_member_migration.ts +21 -0
  27. package/src/migrations/platform_settings_migration.ts +21 -0
  28. package/src/migrations/user_email_lower_unique_migration.ts +22 -0
  29. package/src/migrations/user_external_identity_migration.ts +27 -0
  30. package/src/stores/PostgreSQLInviteLinkStore.ts +134 -0
  31. package/src/stores/PostgreSQLOidcIdentityClaimStore.ts +251 -0
  32. package/src/stores/PostgreSQLOidcProviderConfigStore.ts +222 -0
  33. package/src/stores/PostgreSQLPlatformInvitationStore.ts +133 -0
  34. package/src/stores/PostgreSQLPlatformMemberStore.ts +105 -0
  35. package/src/stores/PostgreSQLPlatformSettingsStore.ts +98 -0
  36. package/src/stores/PostgreSQLUserExternalIdentityStore.ts +134 -0
  37. package/src/stores/PostgreSQLUserStore.ts +3 -2
@@ -0,0 +1,146 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Pool } from "pg";
3
+ import type { PlatformMember } from "@axiom-lattice/protocols";
4
+ import { createPlatformMembersTable } from "../migrations/platform_member_migration";
5
+ import { PostgreSQLPlatformMemberStore } from "../stores/PostgreSQLPlatformMemberStore";
6
+
7
+ const databaseUrl = process.env.TEST_DATABASE_URL;
8
+ const describeIfDatabase = databaseUrl ? describe : describe.skip;
9
+
10
+ function member(
11
+ userId: string,
12
+ overrides: Partial<PlatformMember> = {},
13
+ ): PlatformMember {
14
+ return {
15
+ userId,
16
+ role: "admin",
17
+ grantedBy: "owner-1",
18
+ grantedAt: new Date("2026-01-01T00:00:00.000Z"),
19
+ updatedAt: new Date("2026-01-01T00:00:00.000Z"),
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ describeIfDatabase("PostgreSQLPlatformMemberStore", () => {
25
+ let pool: Pool;
26
+ let store: PostgreSQLPlatformMemberStore;
27
+ const prefix = `platform-${randomUUID()}`;
28
+
29
+ beforeAll(async () => {
30
+ pool = new Pool({ connectionString: databaseUrl });
31
+ const client = await pool.connect();
32
+ try {
33
+ await createPlatformMembersTable.up(client);
34
+ } finally {
35
+ client.release();
36
+ }
37
+ store = new PostgreSQLPlatformMemberStore({ pool });
38
+ });
39
+
40
+ afterEach(async () => {
41
+ await pool.query(
42
+ `DELETE FROM lattice_platform_members WHERE user_id LIKE $1`,
43
+ [`${prefix}%`],
44
+ );
45
+ });
46
+
47
+ afterAll(async () => {
48
+ await pool.query(
49
+ `DELETE FROM lattice_platform_members WHERE user_id LIKE $1`,
50
+ [`${prefix}%`],
51
+ );
52
+ await pool.end();
53
+ });
54
+
55
+ it("upserts, reads and lists platform members by user id", async () => {
56
+ await store.upsert(member(`${prefix}-b`, { role: "auditor" }));
57
+ await store.upsert(member(`${prefix}-a`, { role: "owner" }));
58
+
59
+ await expect(store.get(`${prefix}-a`)).resolves.toMatchObject({
60
+ userId: `${prefix}-a`,
61
+ role: "owner",
62
+ grantedBy: "owner-1",
63
+ });
64
+ await expect(store.get("missing")).resolves.toBeNull();
65
+
66
+ const listed = (await store.list()).filter((entry) =>
67
+ entry.userId.startsWith(prefix),
68
+ );
69
+ expect(listed.map((entry) => entry.userId)).toEqual([
70
+ `${prefix}-a`,
71
+ `${prefix}-b`,
72
+ ]);
73
+ expect(listed.map((entry) => entry.role)).toEqual(["owner", "auditor"]);
74
+ });
75
+
76
+ it("persists role changes with last-write-wins semantics", async () => {
77
+ const userId = `${prefix}-update`;
78
+ await store.upsert(member(userId, { role: "admin" }));
79
+
80
+ const updated = await store.upsert(
81
+ member(userId, {
82
+ role: "owner",
83
+ grantedBy: "owner-2",
84
+ grantedAt: new Date("2026-02-01T00:00:00.000Z"),
85
+ updatedAt: new Date("2026-02-01T00:00:00.000Z"),
86
+ }),
87
+ );
88
+
89
+ expect(updated).toMatchObject({
90
+ userId,
91
+ role: "owner",
92
+ grantedBy: "owner-2",
93
+ });
94
+ await expect(store.get(userId)).resolves.toMatchObject({
95
+ role: "owner",
96
+ grantedBy: "owner-2",
97
+ });
98
+
99
+ const listed = (await store.list()).filter((entry) =>
100
+ entry.userId.startsWith(prefix),
101
+ );
102
+ expect(listed).toHaveLength(1);
103
+
104
+ const raw = await pool.query<{
105
+ role: string;
106
+ granted_by: string;
107
+ }>(
108
+ `SELECT role, granted_by FROM lattice_platform_members WHERE user_id = $1`,
109
+ [userId],
110
+ );
111
+ expect(raw.rows[0]).toEqual({ role: "owner", granted_by: "owner-2" });
112
+ });
113
+
114
+ it("round-trips grant and update timestamps", async () => {
115
+ const userId = `${prefix}-timestamps`;
116
+ const grantedAt = new Date("2026-03-04T05:06:07.000Z");
117
+ const updatedAt = new Date("2026-03-05T06:07:08.000Z");
118
+
119
+ await store.upsert(member(userId, { grantedAt, updatedAt }));
120
+
121
+ const stored = await store.get(userId);
122
+ expect(stored?.grantedAt).toBeInstanceOf(Date);
123
+ expect(stored?.grantedAt.getTime()).toBe(grantedAt.getTime());
124
+ expect(stored?.updatedAt).toBeInstanceOf(Date);
125
+ expect(stored?.updatedAt.getTime()).toBe(updatedAt.getTime());
126
+ });
127
+
128
+ it("omits grantedBy when it is not provided", async () => {
129
+ const userId = `${prefix}-no-granter`;
130
+ const { grantedBy: _grantedBy, ...withoutGrantedBy } = member(userId);
131
+
132
+ await store.upsert(withoutGrantedBy);
133
+
134
+ const stored = await store.get(userId);
135
+ expect(stored?.grantedBy).toBeUndefined();
136
+ });
137
+
138
+ it("deletes members", async () => {
139
+ const userId = `${prefix}-delete`;
140
+ await store.upsert(member(userId));
141
+
142
+ await expect(store.delete(userId)).resolves.toBe(true);
143
+ await expect(store.get(userId)).resolves.toBeNull();
144
+ await expect(store.delete(userId)).resolves.toBe(false);
145
+ });
146
+ });
@@ -0,0 +1,115 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
2
+ import { Pool } from "pg";
3
+ import { createPlatformSettingsTable } from "../migrations/platform_settings_migration";
4
+ import { PostgreSQLPlatformSettingsStore } from "../stores/PostgreSQLPlatformSettingsStore";
5
+
6
+ const databaseUrl = process.env.TEST_DATABASE_URL;
7
+ const describeIfDatabase = databaseUrl ? describe : describe.skip;
8
+
9
+ describeIfDatabase("PostgreSQLPlatformSettingsStore", () => {
10
+ let pool: Pool;
11
+ let store: PostgreSQLPlatformSettingsStore;
12
+
13
+ beforeAll(async () => {
14
+ pool = new Pool({ connectionString: databaseUrl });
15
+ const client = await pool.connect();
16
+ try {
17
+ await createPlatformSettingsTable.up(client);
18
+ await client.query("DELETE FROM lattice_platform_settings");
19
+ } finally {
20
+ client.release();
21
+ }
22
+ store = new PostgreSQLPlatformSettingsStore({ pool });
23
+ });
24
+
25
+ afterAll(async () => {
26
+ await pool.query("DELETE FROM lattice_platform_settings");
27
+ await pool.end();
28
+ });
29
+
30
+ it("returns null before anything is saved", async () => {
31
+ await expect(store.get()).resolves.toBeNull();
32
+ });
33
+
34
+ it("creates and CAS-updates the default row", async () => {
35
+ const first = await store.save(
36
+ { auth: { emailCompletionEnabled: true } },
37
+ 0,
38
+ "admin-1",
39
+ );
40
+ expect(first.version).toBe(1);
41
+ expect(first.auth).toEqual({ emailCompletionEnabled: true });
42
+ expect(first.updatedBy).toBe("admin-1");
43
+
44
+ await expect(
45
+ store.save({ auth: { emailCompletionEnabled: false } }, 0, "admin-2"),
46
+ ).rejects.toMatchObject({
47
+ code: "PLATFORM_SETTINGS_VERSION_CONFLICT",
48
+ actualVersion: 1,
49
+ });
50
+
51
+ const second = await store.save(
52
+ { rateLimits: { maxActiveRunTasksPerTenant: null } },
53
+ 1,
54
+ "admin-2",
55
+ );
56
+ expect(second.version).toBe(2);
57
+ expect(second.auth).toBeUndefined();
58
+ expect(second.rateLimits).toEqual({ maxActiveRunTasksPerTenant: null });
59
+
60
+ const reloaded = await store.get();
61
+ expect(reloaded).toMatchObject({
62
+ version: 2,
63
+ rateLimits: { maxActiveRunTasksPerTenant: null },
64
+ updatedBy: "admin-2",
65
+ });
66
+ expect(reloaded?.updatedAt).toBeInstanceOf(Date);
67
+ });
68
+
69
+ it("is idempotent when the migration runs twice", async () => {
70
+ const client = await pool.connect();
71
+ try {
72
+ await expect(createPlatformSettingsTable.up(client)).resolves.toBeUndefined();
73
+ await expect(createPlatformSettingsTable.up(client)).resolves.toBeUndefined();
74
+ } finally {
75
+ client.release();
76
+ }
77
+ });
78
+
79
+ it("allows exactly one concurrent create at version 0", async () => {
80
+ await pool.query("DELETE FROM lattice_platform_settings");
81
+
82
+ const first = new PostgreSQLPlatformSettingsStore({ pool });
83
+ const second = new PostgreSQLPlatformSettingsStore({ pool });
84
+
85
+ const results = await Promise.allSettled([
86
+ first.save({ auth: { emailCompletionEnabled: true } }, 0, "admin-1"),
87
+ second.save({ auth: { emailCompletionEnabled: false } }, 0, "admin-2"),
88
+ ]);
89
+
90
+ const fulfilled = results.filter((r) => r.status === "fulfilled");
91
+ const rejected = results.filter((r) => r.status === "rejected");
92
+ expect(fulfilled).toHaveLength(1);
93
+ expect(rejected).toHaveLength(1);
94
+ expect((rejected[0] as PromiseRejectedResult).reason).toMatchObject({
95
+ code: "PLATFORM_SETTINGS_VERSION_CONFLICT",
96
+ actualVersion: 1,
97
+ });
98
+
99
+ const persisted = await first.get();
100
+ expect(persisted?.version).toBe(1);
101
+ expect(persisted?.updatedAt).toBeInstanceOf(Date);
102
+ expect(["admin-1", "admin-2"]).toContain(persisted?.updatedBy);
103
+ });
104
+
105
+ it("omits empty and whitespace-only updatedBy", async () => {
106
+ await pool.query("DELETE FROM lattice_platform_settings");
107
+
108
+ const first = await store.save({ invite: { defaultMaxUses: 2 } }, 0, " ");
109
+ expect(first.updatedBy).toBeUndefined();
110
+ expect((await store.get())?.updatedBy).toBeUndefined();
111
+
112
+ const second = await store.save({ invite: { defaultMaxUses: 3 } }, 1, "");
113
+ expect(second.updatedBy).toBeUndefined();
114
+ });
115
+ });
@@ -0,0 +1,125 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Pool } from "pg";
3
+ import { ExternalIdentityConflictError } from "@axiom-lattice/protocols";
4
+ import { createUserExternalIdentityTable } from "../migrations/user_external_identity_migration";
5
+ import { PostgreSQLUserExternalIdentityStore } from "../stores/PostgreSQLUserExternalIdentityStore";
6
+
7
+ const databaseUrl = process.env.TEST_DATABASE_URL;
8
+ const describeIfDatabase = databaseUrl ? describe : describe.skip;
9
+
10
+ describeIfDatabase("PostgreSQLUserExternalIdentityStore", () => {
11
+ let pool: Pool;
12
+ let store: PostgreSQLUserExternalIdentityStore;
13
+ const providerId = `oidc-${randomUUID()}`;
14
+
15
+ beforeAll(async () => {
16
+ pool = new Pool({ connectionString: databaseUrl });
17
+ const client = await pool.connect();
18
+ try {
19
+ await createUserExternalIdentityTable.up(client);
20
+ } finally {
21
+ client.release();
22
+ }
23
+ store = new PostgreSQLUserExternalIdentityStore({ pool });
24
+ });
25
+
26
+ afterEach(async () => {
27
+ await pool.query(`DELETE FROM lattice_user_identities WHERE provider_id = $1`, [providerId]);
28
+ });
29
+
30
+ afterAll(async () => {
31
+ await pool.query(`DELETE FROM lattice_user_identities WHERE provider_id = $1`, [providerId]);
32
+ await pool.end();
33
+ });
34
+
35
+ it("links, reads, lists and unlinks", async () => {
36
+ await store.link({
37
+ userId: "user-1",
38
+ providerId,
39
+ issuer: "https://idp.example.com",
40
+ subject: "sub-1",
41
+ email: "user@example.com",
42
+ linkedAt: new Date("2026-01-01T00:00:00.000Z"),
43
+ });
44
+
45
+ const found = await store.getByProviderSubject(
46
+ providerId,
47
+ "https://idp.example.com",
48
+ "sub-1",
49
+ );
50
+ expect(found).toMatchObject({
51
+ userId: "user-1",
52
+ providerId,
53
+ issuer: "https://idp.example.com",
54
+ subject: "sub-1",
55
+ email: "user@example.com",
56
+ });
57
+ expect(found?.linkedAt).toBeInstanceOf(Date);
58
+
59
+ await store.unlink(providerId, "https://idp.example.com", "sub-1");
60
+ expect(
61
+ await store.getByProviderSubject(providerId, "https://idp.example.com", "sub-1"),
62
+ ).toBeNull();
63
+ });
64
+
65
+ it("lists all identities for a user ordered by linked_at ascending", async () => {
66
+ await store.link({
67
+ userId: "user-list",
68
+ providerId,
69
+ issuer: "https://idp-a.example.com",
70
+ subject: "sub-1",
71
+ linkedAt: new Date("2026-02-01T00:00:00.000Z"),
72
+ });
73
+ await store.link({
74
+ userId: "user-list",
75
+ providerId,
76
+ issuer: "https://idp-b.example.com",
77
+ subject: "sub-1",
78
+ linkedAt: new Date("2026-01-01T00:00:00.000Z"),
79
+ });
80
+
81
+ const listed = await store.listByUser("user-list");
82
+ expect(listed).toHaveLength(2);
83
+ expect(listed.map((item) => item.issuer)).toEqual([
84
+ "https://idp-b.example.com",
85
+ "https://idp-a.example.com",
86
+ ]);
87
+ });
88
+
89
+ it("isolates the same subject across issuers", async () => {
90
+ await store.link({
91
+ userId: "user-a",
92
+ providerId,
93
+ issuer: "https://idp-a.example.com",
94
+ subject: "sub-1",
95
+ linkedAt: new Date("2026-01-01T00:00:00.000Z"),
96
+ });
97
+ await store.link({
98
+ userId: "user-b",
99
+ providerId,
100
+ issuer: "https://idp-b.example.com",
101
+ subject: "sub-1",
102
+ linkedAt: new Date("2026-01-01T00:00:00.000Z"),
103
+ });
104
+ expect(
105
+ await store.getByProviderSubject(providerId, "https://idp-a.example.com", "sub-1"),
106
+ ).toMatchObject({ userId: "user-a" });
107
+ expect(
108
+ await store.getByProviderSubject(providerId, "https://idp-b.example.com", "sub-1"),
109
+ ).toMatchObject({ userId: "user-b" });
110
+ });
111
+
112
+ it("is idempotent for the same user and rejects a different user", async () => {
113
+ const base = {
114
+ providerId,
115
+ issuer: "https://idp.example.com",
116
+ subject: "sub-1",
117
+ linkedAt: new Date("2026-01-01T00:00:00.000Z"),
118
+ };
119
+ await store.link({ ...base, userId: "user-1" });
120
+ await expect(store.link({ ...base, userId: "user-1" })).resolves.toBeUndefined();
121
+ await expect(store.link({ ...base, userId: "user-2" })).rejects.toBeInstanceOf(
122
+ ExternalIdentityConflictError,
123
+ );
124
+ });
125
+ });
@@ -0,0 +1,75 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Pool } from "pg";
3
+ import { MigrationManager } from "../migrations/migration";
4
+ import { createUsersTable } from "../migrations/user_migrations";
5
+ import { addUserStatusColumn } from "../migrations/user_status_migration";
6
+ import { createUserEmailLowerUniqueIndex } from "../migrations/user_email_lower_unique_migration";
7
+ import { PostgreSQLUserStore } from "../stores/PostgreSQLUserStore";
8
+
9
+ const databaseUrl = process.env.TEST_DATABASE_URL;
10
+ const describeIfDatabase = databaseUrl ? describe : describe.skip;
11
+
12
+ describeIfDatabase("PostgreSQLUserStore", () => {
13
+ let pool: Pool;
14
+ let store: PostgreSQLUserStore;
15
+
16
+ beforeAll(async () => {
17
+ pool = new Pool({ connectionString: databaseUrl });
18
+ const manager = new MigrationManager(pool);
19
+ manager.register(createUsersTable);
20
+ manager.register(addUserStatusColumn);
21
+ manager.register(createUserEmailLowerUniqueIndex);
22
+ await manager.migrate();
23
+ store = new PostgreSQLUserStore({ pool });
24
+ });
25
+
26
+ afterAll(async () => {
27
+ await pool.end();
28
+ });
29
+
30
+ it("matches email case-insensitively", async () => {
31
+ const id = randomUUID();
32
+ try {
33
+ await store.createUser(id, {
34
+ email: `User-${id}@Example.COM`,
35
+ name: "User",
36
+ status: "active",
37
+ });
38
+ await expect(store.getUserByEmail(` user-${id}@example.com `)).resolves.toMatchObject({ id });
39
+ } finally {
40
+ await pool.query("DELETE FROM lattice_users WHERE id = $1", [id]);
41
+ }
42
+ });
43
+
44
+ it("creates the case-insensitive unique email index", async () => {
45
+ const result = await pool.query(
46
+ `SELECT indexname
47
+ FROM pg_indexes
48
+ WHERE schemaname = current_schema()
49
+ AND tablename = 'lattice_users'
50
+ AND indexname = 'idx_lattice_users_email_lower'`,
51
+ );
52
+ expect(result.rows).toHaveLength(1);
53
+ });
54
+
55
+ it("rejects an email that differs only by case", async () => {
56
+ const firstId = randomUUID();
57
+ const secondId = randomUUID();
58
+ try {
59
+ await store.createUser(firstId, {
60
+ email: `Case-${firstId}@Example.COM`,
61
+ name: "First",
62
+ status: "active",
63
+ });
64
+ await expect(
65
+ store.createUser(secondId, {
66
+ email: `case-${firstId}@example.com`,
67
+ name: "Second",
68
+ status: "active",
69
+ }),
70
+ ).rejects.toThrow();
71
+ } finally {
72
+ await pool.query("DELETE FROM lattice_users WHERE id = ANY($1)", [[firstId, secondId]]);
73
+ }
74
+ });
75
+ });
@@ -11,6 +11,12 @@ import { PostgreSQLProjectStore } from "./stores/PostgreSQLProjectStore";
11
11
  import { PostgreSQLUserStore } from "./stores/PostgreSQLUserStore";
12
12
  import { PostgreSQLTenantStore } from "./stores/PostgreSQLTenantStore";
13
13
  import { PostgreSQLUserTenantLinkStore } from "./stores/PostgreSQLUserTenantLinkStore";
14
+ import { PostgreSQLUserExternalIdentityStore } from "./stores/PostgreSQLUserExternalIdentityStore";
15
+ import { PostgreSQLOidcProviderConfigStore } from "./stores/PostgreSQLOidcProviderConfigStore";
16
+ import { PostgreSQLPlatformMemberStore } from "./stores/PostgreSQLPlatformMemberStore";
17
+ import { PostgreSQLPlatformInvitationStore } from "./stores/PostgreSQLPlatformInvitationStore";
18
+ import { PostgreSQLOidcIdentityClaimStore } from "./stores/PostgreSQLOidcIdentityClaimStore";
19
+ import { PostgreSQLInviteLinkStore } from "./stores/PostgreSQLInviteLinkStore";
14
20
  import { PostgreSQLWorkflowTrackingStore } from "./stores/PostgreSQLWorkflowTrackingStore";
15
21
  import { PostgreSQLEvalStore } from "./stores/PostgreSQLEvalStore";
16
22
  import { ThreadMessageQueueStore } from "./stores/ThreadMessageQueueStore";
@@ -67,6 +73,16 @@ import { addApiKeyGrants } from "./migrations/add_api_key_grants";
67
73
  import { createOpenAuditTable } from "./migrations/create_open_audit_table";
68
74
  import { addShareOpenGrants } from "./migrations/add_share_open_grants";
69
75
  import { addShareKind } from "./migrations/add_share_kind";
76
+ import { createUserExternalIdentityTable } from "./migrations/user_external_identity_migration";
77
+ import { createUserEmailLowerUniqueIndex } from "./migrations/user_email_lower_unique_migration";
78
+ import { createOidcProviderConfigsTable } from "./migrations/oidc_provider_config_migration";
79
+ import { createPlatformMembersTable } from "./migrations/platform_member_migration";
80
+ import { createPlatformInvitationsTable } from "./migrations/platform_invitation_migration";
81
+ import { createOidcIdentityClaimsTable } from "./migrations/oidc_identity_claim_migration";
82
+ import { createInviteLinksTable } from "./migrations/invite_link_migration";
83
+ import { addOidcIdentityClaimInviteFields } from "./migrations/oidc_identity_claim_invite_migration";
84
+ import { createPlatformSettingsTable } from "./migrations/platform_settings_migration";
85
+ import { addOidcProviderAllowedHosts } from "./migrations/oidc_provider_allowed_hosts_migration";
70
86
  import { taskMigrations } from "./migrations/task_migration";
71
87
  import { createMenuItemsTable } from "./migrations/menu_items_migration";
72
88
  import { addFileContentType } from "./migrations/menu_items_add_file_type";
@@ -124,6 +140,7 @@ import { PostgreSQLLeaseStore } from "./stores/PostgreSQLLeaseStore";
124
140
  import { PostgreSQLOutboxStore } from "./stores/PostgreSQLOutboxStore";
125
141
  import { PostgreSQLPluginDeploymentStore } from "./stores/PostgreSQLPluginDeploymentStore";
126
142
  import { PostgreSQLAgentRunLedger } from "./stores/PostgreSQLAgentRunLedger";
143
+ import { PostgreSQLPlatformSettingsStore } from "./stores/PostgreSQLPlatformSettingsStore";
127
144
  import { PostgreSQLChannelReplyJobStore } from "./stores/PostgreSQLChannelReplyJobStore";
128
145
  import { PostgreSQLCheckpointStorage } from "./stores/PostgreSQLCheckpointStorage";
129
146
  import { PostgreSQLStore } from "./stores/PostgreSQLStore";
@@ -226,6 +243,16 @@ export async function createPgStoreConfig(connectionString: string) {
226
243
  mm.register(agentRunProjectTaskRunningIndexMigration); // v204
227
244
  mm.register(addModelProviderKindDefault); // v207
228
245
  mm.register(addShareKind); // v208
246
+ mm.register(createUserExternalIdentityTable); // v209
247
+ mm.register(createUserEmailLowerUniqueIndex); // v210
248
+ mm.register(createOidcProviderConfigsTable); // v211
249
+ mm.register(createPlatformMembersTable); // v212
250
+ mm.register(createPlatformInvitationsTable); // v213
251
+ mm.register(createOidcIdentityClaimsTable); // v214
252
+ mm.register(createInviteLinksTable); // v215
253
+ mm.register(addOidcIdentityClaimInviteFields); // v216
254
+ mm.register(createPlatformSettingsTable); // v217
255
+ mm.register(addOidcProviderAllowedHosts); // v218
229
256
 
230
257
  await mm.migrate();
231
258
 
@@ -247,6 +274,13 @@ export async function createPgStoreConfig(connectionString: string) {
247
274
  user: new PostgreSQLUserStore(opts),
248
275
  tenant: new PostgreSQLTenantStore(opts),
249
276
  userTenantLink: new PostgreSQLUserTenantLinkStore(opts),
277
+ userExternalIdentity: new PostgreSQLUserExternalIdentityStore(opts),
278
+ oidcProviderConfig: new PostgreSQLOidcProviderConfigStore(opts),
279
+ platformMember: new PostgreSQLPlatformMemberStore(opts),
280
+ platformInvitation: new PostgreSQLPlatformInvitationStore(opts),
281
+ oidcIdentityClaim: new PostgreSQLOidcIdentityClaimStore(opts),
282
+ inviteLink: new PostgreSQLInviteLinkStore(opts),
283
+ platformSettings: new PostgreSQLPlatformSettingsStore(opts),
250
284
  channelBinding: new ChannelBindingStore(opts),
251
285
  channelInstallation: new PostgreSQLChannelInstallationStore(opts),
252
286
  thread: new PostgreSQLThreadStore(opts),
package/src/index.ts CHANGED
@@ -24,6 +24,22 @@ export * from "./stores/PostgreSQLProjectStore";
24
24
  export * from "./stores/PostgreSQLUserStore";
25
25
  export * from "./stores/PostgreSQLTenantStore";
26
26
  export * from "./stores/PostgreSQLUserTenantLinkStore";
27
+ export * from "./migrations/user_external_identity_migration";
28
+ export * from "./migrations/user_email_lower_unique_migration";
29
+ export * from "./stores/PostgreSQLUserExternalIdentityStore";
30
+ export * from "./migrations/oidc_provider_config_migration";
31
+ export * from "./stores/PostgreSQLOidcProviderConfigStore";
32
+ export * from "./migrations/platform_member_migration";
33
+ export * from "./stores/PostgreSQLPlatformMemberStore";
34
+ export * from "./migrations/platform_invitation_migration";
35
+ export * from "./stores/PostgreSQLPlatformInvitationStore";
36
+ export * from "./migrations/oidc_identity_claim_migration";
37
+ export * from "./stores/PostgreSQLOidcIdentityClaimStore";
38
+ export * from "./migrations/invite_link_migration";
39
+ export * from "./migrations/oidc_identity_claim_invite_migration";
40
+ export * from "./stores/PostgreSQLInviteLinkStore";
41
+ export * from "./migrations/platform_settings_migration";
42
+ export * from "./stores/PostgreSQLPlatformSettingsStore";
27
43
  export * from "./migrations/migration";
28
44
  export * from "./migrations/thread_migrations";
29
45
  export * from "./migrations/thread_tenant_migration";
@@ -0,0 +1,35 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /** Creates durable bearer invitation links. */
4
+ export const createInviteLinksTable: Migration = {
5
+ version: 215,
6
+ name: "create_invite_links",
7
+ up: async (client) => {
8
+ await client.query(`
9
+ CREATE TABLE IF NOT EXISTS lattice_invite_links (
10
+ id UUID PRIMARY KEY,
11
+ scope VARCHAR(16) NOT NULL,
12
+ token_hash VARCHAR(64) NOT NULL UNIQUE,
13
+ token_encrypted TEXT NOT NULL,
14
+ email VARCHAR(320),
15
+ platform_role VARCHAR(16),
16
+ tenant_id VARCHAR(255),
17
+ tenant_role VARCHAR(16),
18
+ created_by VARCHAR(255),
19
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
20
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
21
+ expires_at TIMESTAMPTZ NOT NULL,
22
+ max_uses INTEGER NOT NULL,
23
+ used_count INTEGER NOT NULL DEFAULT 0,
24
+ revoked_at TIMESTAMPTZ
25
+ )
26
+ `);
27
+ await client.query(`
28
+ CREATE INDEX IF NOT EXISTS lattice_invite_links_created
29
+ ON lattice_invite_links (created_at DESC)
30
+ `);
31
+ },
32
+ down: async (client) => {
33
+ await client.query("DROP TABLE IF EXISTS lattice_invite_links");
34
+ },
35
+ };
@@ -0,0 +1,29 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /** Adds invite-link request context to pending identity claims. */
4
+ export const addOidcIdentityClaimInviteFields: Migration = {
5
+ version: 216,
6
+ name: "add_oidc_identity_claim_invite_fields",
7
+ up: async (client) => {
8
+ await client.query(`
9
+ ALTER TABLE lattice_oidc_identity_claims
10
+ ADD COLUMN IF NOT EXISTS source VARCHAR(32),
11
+ ADD COLUMN IF NOT EXISTS invite_link_id UUID,
12
+ ADD COLUMN IF NOT EXISTS requested_user_id VARCHAR(255),
13
+ ADD COLUMN IF NOT EXISTS requested_platform_role VARCHAR(16),
14
+ ADD COLUMN IF NOT EXISTS requested_tenant_id VARCHAR(255),
15
+ ADD COLUMN IF NOT EXISTS requested_tenant_role VARCHAR(16)
16
+ `);
17
+ },
18
+ down: async (client) => {
19
+ await client.query(`
20
+ ALTER TABLE lattice_oidc_identity_claims
21
+ DROP COLUMN IF EXISTS source,
22
+ DROP COLUMN IF EXISTS invite_link_id,
23
+ DROP COLUMN IF EXISTS requested_user_id,
24
+ DROP COLUMN IF EXISTS requested_platform_role,
25
+ DROP COLUMN IF EXISTS requested_tenant_id,
26
+ DROP COLUMN IF EXISTS requested_tenant_role
27
+ `);
28
+ },
29
+ };
@@ -0,0 +1,39 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /** Creates pending enterprise-SSO identity claims. */
4
+ export const createOidcIdentityClaimsTable: Migration = {
5
+ version: 214,
6
+ name: "create_oidc_identity_claims",
7
+ up: async (client) => {
8
+ await client.query(`
9
+ CREATE TABLE IF NOT EXISTS lattice_oidc_identity_claims (
10
+ id UUID PRIMARY KEY,
11
+ provider_id VARCHAR(64) NOT NULL,
12
+ issuer TEXT NOT NULL,
13
+ subject TEXT NOT NULL,
14
+ email VARCHAR(320) NOT NULL,
15
+ name TEXT,
16
+ picture TEXT,
17
+ status VARCHAR(16) NOT NULL,
18
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
19
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
20
+ expires_at TIMESTAMPTZ NOT NULL,
21
+ reviewed_by VARCHAR(255),
22
+ reviewed_at TIMESTAMPTZ,
23
+ linked_user_id VARCHAR(255)
24
+ )
25
+ `);
26
+ await client.query(`
27
+ CREATE UNIQUE INDEX IF NOT EXISTS lattice_oidc_identity_claims_pending_subject
28
+ ON lattice_oidc_identity_claims (provider_id, issuer, subject)
29
+ WHERE status = 'pending'
30
+ `);
31
+ await client.query(`
32
+ CREATE INDEX IF NOT EXISTS lattice_oidc_identity_claims_status_created
33
+ ON lattice_oidc_identity_claims (status, created_at)
34
+ `);
35
+ },
36
+ down: async (client) => {
37
+ await client.query("DROP TABLE IF EXISTS lattice_oidc_identity_claims");
38
+ },
39
+ };
@@ -0,0 +1,19 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /** Adds the per-provider private-network issuer allowlist to OIDC providers. */
4
+ export const addOidcProviderAllowedHosts: Migration = {
5
+ version: 218,
6
+ name: "add_oidc_provider_allowed_hosts",
7
+ up: async (client) => {
8
+ await client.query(`
9
+ ALTER TABLE lattice_oidc_provider_configs
10
+ ADD COLUMN IF NOT EXISTS allowed_private_hosts JSONB
11
+ `);
12
+ },
13
+ down: async (client) => {
14
+ await client.query(`
15
+ ALTER TABLE lattice_oidc_provider_configs
16
+ DROP COLUMN IF EXISTS allowed_private_hosts
17
+ `);
18
+ },
19
+ };