@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
package/dist/index.mjs CHANGED
@@ -2950,9 +2950,10 @@ var PostgreSQLUserStore = class {
2950
2950
  `
2951
2951
  SELECT id, email, name, status, metadata, created_at, updated_at
2952
2952
  FROM lattice_users
2953
- WHERE email = $1
2953
+ WHERE lower(email) = lower($1)
2954
+ LIMIT 1
2954
2955
  `,
2955
- [email]
2956
+ [email.trim()]
2956
2957
  );
2957
2958
  if (result.rows.length === 0) return null;
2958
2959
  return this.mapRowToUser(result.rows[0]);
@@ -3493,6 +3494,693 @@ var PostgreSQLUserTenantLinkStore = class {
3493
3494
  }
3494
3495
  };
3495
3496
 
3497
+ // src/stores/PostgreSQLUserExternalIdentityStore.ts
3498
+ import {
3499
+ ExternalIdentityConflictError,
3500
+ ExternalIdentityPersistError
3501
+ } from "@axiom-lattice/protocols";
3502
+ var InvalidUserExternalIdentityRowError = class extends Error {
3503
+ constructor() {
3504
+ super("Invalid user external identity row");
3505
+ this.code = "INVALID_USER_EXTERNAL_IDENTITY_ROW";
3506
+ this.name = "InvalidUserExternalIdentityRowError";
3507
+ }
3508
+ };
3509
+ var columns = "user_id, provider_id, issuer, subject, email, linked_at";
3510
+ function mapRow(row) {
3511
+ if (typeof row.user_id !== "string" || typeof row.provider_id !== "string" || typeof row.issuer !== "string" || typeof row.subject !== "string" || !(row.linked_at instanceof Date) || Number.isNaN(row.linked_at.getTime())) {
3512
+ throw new InvalidUserExternalIdentityRowError();
3513
+ }
3514
+ return {
3515
+ userId: row.user_id,
3516
+ providerId: row.provider_id,
3517
+ issuer: row.issuer,
3518
+ subject: row.subject,
3519
+ ...typeof row.email === "string" ? { email: row.email } : {},
3520
+ linkedAt: row.linked_at
3521
+ };
3522
+ }
3523
+ var PostgreSQLUserExternalIdentityStore = class {
3524
+ constructor(options) {
3525
+ this.pool = options.pool;
3526
+ }
3527
+ async getByProviderSubject(providerId, issuer, subject) {
3528
+ const result = await this.pool.query(
3529
+ `SELECT ${columns} FROM lattice_user_identities
3530
+ WHERE provider_id = $1 AND issuer = $2 AND subject = $3`,
3531
+ [providerId, issuer, subject]
3532
+ );
3533
+ return result.rows[0] ? mapRow(result.rows[0]) : null;
3534
+ }
3535
+ async listByUser(userId) {
3536
+ const result = await this.pool.query(
3537
+ `SELECT ${columns} FROM lattice_user_identities WHERE user_id = $1 ORDER BY linked_at ASC`,
3538
+ [userId]
3539
+ );
3540
+ return result.rows.map(mapRow);
3541
+ }
3542
+ async link(identity) {
3543
+ const inserted = await this.pool.query(
3544
+ `INSERT INTO lattice_user_identities (${columns})
3545
+ VALUES ($1, $2, $3, $4, $5, $6)
3546
+ ON CONFLICT (provider_id, issuer, subject) DO NOTHING
3547
+ RETURNING ${columns}`,
3548
+ [
3549
+ identity.userId,
3550
+ identity.providerId,
3551
+ identity.issuer,
3552
+ identity.subject,
3553
+ identity.email ?? null,
3554
+ identity.linkedAt
3555
+ ]
3556
+ );
3557
+ if (inserted.rows[0]) return;
3558
+ const existing = await this.getByProviderSubject(
3559
+ identity.providerId,
3560
+ identity.issuer,
3561
+ identity.subject
3562
+ );
3563
+ if (!existing) {
3564
+ throw new ExternalIdentityPersistError(
3565
+ identity.providerId,
3566
+ identity.issuer,
3567
+ identity.subject
3568
+ );
3569
+ }
3570
+ if (existing.userId !== identity.userId) {
3571
+ throw new ExternalIdentityConflictError(
3572
+ identity.providerId,
3573
+ identity.issuer,
3574
+ identity.subject
3575
+ );
3576
+ }
3577
+ if (identity.email !== void 0 && identity.email !== existing.email) {
3578
+ await this.pool.query(
3579
+ `UPDATE lattice_user_identities SET email = $1
3580
+ WHERE provider_id = $2 AND issuer = $3 AND subject = $4`,
3581
+ [identity.email, identity.providerId, identity.issuer, identity.subject]
3582
+ );
3583
+ }
3584
+ }
3585
+ async unlink(providerId, issuer, subject) {
3586
+ await this.pool.query(
3587
+ `DELETE FROM lattice_user_identities
3588
+ WHERE provider_id = $1 AND issuer = $2 AND subject = $3`,
3589
+ [providerId, issuer, subject]
3590
+ );
3591
+ }
3592
+ };
3593
+
3594
+ // src/stores/PostgreSQLOidcProviderConfigStore.ts
3595
+ import { safeDecrypt as safeDecrypt4, encrypt as encrypt4 } from "@axiom-lattice/core";
3596
+ import {
3597
+ OidcProviderConfigConflictError
3598
+ } from "@axiom-lattice/protocols";
3599
+ var TABLE2 = "lattice_oidc_provider_configs";
3600
+ function parseAllowedPrivateHosts(value) {
3601
+ if (!Array.isArray(value)) return void 0;
3602
+ return value.filter((entry) => typeof entry === "string");
3603
+ }
3604
+ function serializeAllowedPrivateHosts(value) {
3605
+ return value === void 0 ? null : JSON.stringify(value);
3606
+ }
3607
+ function normalizeTenantId(value) {
3608
+ if (typeof value !== "string") return void 0;
3609
+ const trimmed = value.trim();
3610
+ return trimmed.length > 0 ? trimmed : void 0;
3611
+ }
3612
+ function mapRow2(row) {
3613
+ if (typeof row.id !== "string" || typeof row.display_name !== "string" || typeof row.issuer !== "string" || typeof row.client_id !== "string" || typeof row.client_secret !== "string" || typeof row.scopes !== "string" || typeof row.enabled !== "boolean" || typeof row.jit_provisioning !== "boolean" || typeof row.auto_link_email !== "boolean" || !(row.created_at instanceof Date) || !(row.updated_at instanceof Date)) {
3614
+ throw new Error("Invalid OIDC provider config row");
3615
+ }
3616
+ const allowedPrivateHosts = parseAllowedPrivateHosts(
3617
+ row.allowed_private_hosts
3618
+ );
3619
+ return {
3620
+ id: row.id,
3621
+ displayName: row.display_name,
3622
+ issuer: row.issuer,
3623
+ clientId: row.client_id,
3624
+ clientSecret: safeDecrypt4(
3625
+ row.client_secret,
3626
+ `OIDC provider config "${row.id}" client secret`
3627
+ ),
3628
+ scopes: row.scopes,
3629
+ enabled: row.enabled,
3630
+ jitProvisioning: row.jit_provisioning,
3631
+ autoLinkEmail: row.auto_link_email,
3632
+ ...typeof row.default_tenant_id === "string" ? { defaultTenantId: row.default_tenant_id } : {},
3633
+ ...allowedPrivateHosts !== void 0 ? { allowedPrivateHosts } : {},
3634
+ createdAt: row.created_at,
3635
+ updatedAt: row.updated_at
3636
+ };
3637
+ }
3638
+ function isUniqueViolation(error) {
3639
+ return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
3640
+ }
3641
+ var PostgreSQLOidcProviderConfigStore = class {
3642
+ constructor(options) {
3643
+ this.pool = options.pool;
3644
+ }
3645
+ async list() {
3646
+ const result = await this.pool.query(
3647
+ `SELECT * FROM ${TABLE2} ORDER BY created_at ASC, id ASC`
3648
+ );
3649
+ return result.rows.map(mapRow2);
3650
+ }
3651
+ async get(id) {
3652
+ const result = await this.pool.query(
3653
+ `SELECT * FROM ${TABLE2} WHERE id = $1`,
3654
+ [id]
3655
+ );
3656
+ return result.rows[0] ? mapRow2(result.rows[0]) : null;
3657
+ }
3658
+ async create(id, input) {
3659
+ const tenantId = normalizeTenantId(input.defaultTenantId);
3660
+ try {
3661
+ await this.pool.query(
3662
+ `INSERT INTO ${TABLE2}
3663
+ (id, display_name, issuer, client_id, client_secret, scopes,
3664
+ enabled, jit_provisioning, auto_link_email, default_tenant_id,
3665
+ allowed_private_hosts)
3666
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
3667
+ [
3668
+ id,
3669
+ input.displayName,
3670
+ input.issuer,
3671
+ input.clientId,
3672
+ encrypt4(input.clientSecret),
3673
+ input.scopes,
3674
+ input.enabled,
3675
+ input.jitProvisioning,
3676
+ input.autoLinkEmail,
3677
+ tenantId ?? null,
3678
+ serializeAllowedPrivateHosts(input.allowedPrivateHosts)
3679
+ ]
3680
+ );
3681
+ } catch (error) {
3682
+ if (isUniqueViolation(error)) {
3683
+ throw new OidcProviderConfigConflictError(id);
3684
+ }
3685
+ throw error;
3686
+ }
3687
+ const created = await this.get(id);
3688
+ if (!created) {
3689
+ throw new Error(`OIDC provider config "${id}" was not persisted`);
3690
+ }
3691
+ return created;
3692
+ }
3693
+ async update(id, updates) {
3694
+ const existing = await this.get(id);
3695
+ if (!existing) return null;
3696
+ const setClauses = [];
3697
+ const values = [];
3698
+ let paramIndex = 1;
3699
+ const set = (column, value) => {
3700
+ setClauses.push(`${column} = $${paramIndex++}`);
3701
+ values.push(value);
3702
+ };
3703
+ if (updates.displayName !== void 0) set("display_name", updates.displayName);
3704
+ if (updates.issuer !== void 0) set("issuer", updates.issuer);
3705
+ if (updates.clientId !== void 0) set("client_id", updates.clientId);
3706
+ if (updates.clientSecret !== void 0 && updates.clientSecret !== "") {
3707
+ set("client_secret", encrypt4(updates.clientSecret));
3708
+ }
3709
+ if (updates.scopes !== void 0) set("scopes", updates.scopes);
3710
+ if (updates.enabled !== void 0) set("enabled", updates.enabled);
3711
+ if (updates.jitProvisioning !== void 0) {
3712
+ set("jit_provisioning", updates.jitProvisioning);
3713
+ }
3714
+ if (updates.autoLinkEmail !== void 0) {
3715
+ set("auto_link_email", updates.autoLinkEmail);
3716
+ }
3717
+ if (updates.defaultTenantId !== void 0) {
3718
+ set("default_tenant_id", normalizeTenantId(updates.defaultTenantId) ?? null);
3719
+ }
3720
+ if (updates.allowedPrivateHosts !== void 0) {
3721
+ set(
3722
+ "allowed_private_hosts",
3723
+ serializeAllowedPrivateHosts(updates.allowedPrivateHosts)
3724
+ );
3725
+ }
3726
+ if (setClauses.length === 0) return existing;
3727
+ setClauses.push("updated_at = NOW()");
3728
+ values.push(id);
3729
+ await this.pool.query(
3730
+ `UPDATE ${TABLE2} SET ${setClauses.join(", ")} WHERE id = $${paramIndex}`,
3731
+ values
3732
+ );
3733
+ return this.get(id);
3734
+ }
3735
+ async delete(id) {
3736
+ const result = await this.pool.query(`DELETE FROM ${TABLE2} WHERE id = $1`, [id]);
3737
+ return (result.rowCount ?? 0) > 0;
3738
+ }
3739
+ };
3740
+
3741
+ // src/stores/PostgreSQLPlatformMemberStore.ts
3742
+ var TABLE3 = "lattice_platform_members";
3743
+ var ROLES = ["owner", "admin", "auditor"];
3744
+ function isPlatformRole(value) {
3745
+ return ROLES.includes(value);
3746
+ }
3747
+ function mapRow3(row) {
3748
+ if (typeof row.user_id !== "string" || typeof row.role !== "string" || !isPlatformRole(row.role) || !(row.granted_at instanceof Date) || !(row.updated_at instanceof Date)) {
3749
+ throw new Error("Invalid platform member row");
3750
+ }
3751
+ return {
3752
+ userId: row.user_id,
3753
+ role: row.role,
3754
+ ...typeof row.granted_by === "string" ? { grantedBy: row.granted_by } : {},
3755
+ grantedAt: row.granted_at,
3756
+ updatedAt: row.updated_at
3757
+ };
3758
+ }
3759
+ var PostgreSQLPlatformMemberStore = class {
3760
+ constructor(options) {
3761
+ this.pool = options.pool;
3762
+ }
3763
+ /** Lists every platform member ordered by user id. */
3764
+ async list() {
3765
+ const result = await this.pool.query(
3766
+ `SELECT * FROM ${TABLE3} ORDER BY user_id ASC`
3767
+ );
3768
+ return result.rows.map(mapRow3);
3769
+ }
3770
+ /** Returns the platform member for a user, or null. */
3771
+ async get(userId) {
3772
+ const result = await this.pool.query(
3773
+ `SELECT * FROM ${TABLE3} WHERE user_id = $1`,
3774
+ [userId]
3775
+ );
3776
+ return result.rows[0] ? mapRow3(result.rows[0]) : null;
3777
+ }
3778
+ /**
3779
+ * Inserts or replaces a member keyed by user id.
3780
+ *
3781
+ * The supplied grant replaces any existing row in full (last write wins),
3782
+ * including `grantedAt` and `updatedAt`.
3783
+ */
3784
+ async upsert(member) {
3785
+ const result = await this.pool.query(
3786
+ `INSERT INTO ${TABLE3} (user_id, role, granted_by, granted_at, updated_at)
3787
+ VALUES ($1, $2, $3, $4, $5)
3788
+ ON CONFLICT (user_id) DO UPDATE SET
3789
+ role = EXCLUDED.role,
3790
+ granted_by = EXCLUDED.granted_by,
3791
+ granted_at = EXCLUDED.granted_at,
3792
+ updated_at = EXCLUDED.updated_at
3793
+ RETURNING *`,
3794
+ [
3795
+ member.userId,
3796
+ member.role,
3797
+ member.grantedBy ?? null,
3798
+ member.grantedAt,
3799
+ member.updatedAt
3800
+ ]
3801
+ );
3802
+ return mapRow3(result.rows[0]);
3803
+ }
3804
+ /** Deletes a member. Returns whether a row was removed. */
3805
+ async delete(userId) {
3806
+ const result = await this.pool.query(`DELETE FROM ${TABLE3} WHERE user_id = $1`, [
3807
+ userId
3808
+ ]);
3809
+ return (result.rowCount ?? 0) > 0;
3810
+ }
3811
+ };
3812
+
3813
+ // src/stores/PostgreSQLPlatformInvitationStore.ts
3814
+ import {
3815
+ normalizePlatformInvitationEmail
3816
+ } from "@axiom-lattice/protocols";
3817
+ var TABLE4 = "lattice_platform_invitations";
3818
+ var ROLES2 = ["owner", "admin", "auditor"];
3819
+ function isPlatformRole2(value) {
3820
+ return ROLES2.includes(value);
3821
+ }
3822
+ function mapRow4(row) {
3823
+ if (typeof row.email !== "string" || typeof row.role !== "string" || !isPlatformRole2(row.role) || !(row.created_at instanceof Date)) {
3824
+ throw new Error("Invalid platform invitation row");
3825
+ }
3826
+ return {
3827
+ email: row.email,
3828
+ role: row.role,
3829
+ ...typeof row.invited_by === "string" ? { invitedBy: row.invited_by } : {},
3830
+ createdAt: row.created_at,
3831
+ ...row.expires_at instanceof Date ? { expiresAt: row.expires_at } : {},
3832
+ ...row.consumed_at instanceof Date ? { consumedAt: row.consumed_at } : {}
3833
+ };
3834
+ }
3835
+ var PostgreSQLPlatformInvitationStore = class {
3836
+ constructor(options) {
3837
+ this.pool = options.pool;
3838
+ }
3839
+ /** Lists every invitation ordered by normalized email. */
3840
+ async list() {
3841
+ const result = await this.pool.query(
3842
+ `SELECT * FROM ${TABLE4} ORDER BY email ASC`
3843
+ );
3844
+ return result.rows.map(mapRow4);
3845
+ }
3846
+ /** Returns the invitation for an email, or null. */
3847
+ async get(email) {
3848
+ const result = await this.pool.query(
3849
+ `SELECT * FROM ${TABLE4} WHERE email = $1`,
3850
+ [normalizePlatformInvitationEmail(email)]
3851
+ );
3852
+ return result.rows[0] ? mapRow4(result.rows[0]) : null;
3853
+ }
3854
+ /**
3855
+ * Inserts or replaces an invitation keyed by normalized email.
3856
+ *
3857
+ * The supplied invitation replaces any existing row in full (last write wins),
3858
+ * including `expiresAt` and `consumedAt`.
3859
+ */
3860
+ async upsert(invitation) {
3861
+ const result = await this.pool.query(
3862
+ `INSERT INTO ${TABLE4}
3863
+ (email, role, invited_by, created_at, expires_at, consumed_at)
3864
+ VALUES ($1, $2, $3, $4, $5, $6)
3865
+ ON CONFLICT (email) DO UPDATE SET
3866
+ role = EXCLUDED.role,
3867
+ invited_by = EXCLUDED.invited_by,
3868
+ created_at = EXCLUDED.created_at,
3869
+ expires_at = EXCLUDED.expires_at,
3870
+ consumed_at = EXCLUDED.consumed_at
3871
+ RETURNING *`,
3872
+ [
3873
+ normalizePlatformInvitationEmail(invitation.email),
3874
+ invitation.role,
3875
+ invitation.invitedBy ?? null,
3876
+ invitation.createdAt,
3877
+ invitation.expiresAt ?? null,
3878
+ invitation.consumedAt ?? null
3879
+ ]
3880
+ );
3881
+ return mapRow4(result.rows[0]);
3882
+ }
3883
+ /** Deletes an invitation. Returns whether a row was removed. */
3884
+ async delete(email) {
3885
+ const result = await this.pool.query(
3886
+ `DELETE FROM ${TABLE4} WHERE email = $1`,
3887
+ [normalizePlatformInvitationEmail(email)]
3888
+ );
3889
+ return (result.rowCount ?? 0) > 0;
3890
+ }
3891
+ /**
3892
+ * Atomically consumes a pending, unexpired invitation.
3893
+ *
3894
+ * Returns the consumed invitation, or null when the row is missing, already
3895
+ * consumed, or expired.
3896
+ */
3897
+ async consume(email, at) {
3898
+ const result = await this.pool.query(
3899
+ `UPDATE ${TABLE4}
3900
+ SET consumed_at = $1
3901
+ WHERE email = $2
3902
+ AND consumed_at IS NULL
3903
+ AND (expires_at IS NULL OR expires_at > $1)
3904
+ RETURNING *`,
3905
+ [at, normalizePlatformInvitationEmail(email)]
3906
+ );
3907
+ return result.rows[0] ? mapRow4(result.rows[0]) : null;
3908
+ }
3909
+ };
3910
+
3911
+ // src/stores/PostgreSQLOidcIdentityClaimStore.ts
3912
+ import {
3913
+ normalizeOidcIdentityEmail,
3914
+ OidcIdentityClaimConflictError
3915
+ } from "@axiom-lattice/protocols";
3916
+ var TABLE5 = "lattice_oidc_identity_claims";
3917
+ function mapRow5(row) {
3918
+ return {
3919
+ id: row.id,
3920
+ providerId: row.provider_id,
3921
+ issuer: row.issuer,
3922
+ subject: row.subject,
3923
+ email: row.email,
3924
+ ...row.name !== null ? { name: row.name } : {},
3925
+ ...row.picture !== null ? { picture: row.picture } : {},
3926
+ status: row.status,
3927
+ createdAt: row.created_at,
3928
+ updatedAt: row.updated_at,
3929
+ expiresAt: row.expires_at,
3930
+ ...row.reviewed_by !== null ? { reviewedBy: row.reviewed_by } : {},
3931
+ ...row.reviewed_at !== null ? { reviewedAt: row.reviewed_at } : {},
3932
+ ...row.linked_user_id !== null ? { linkedUserId: row.linked_user_id } : {},
3933
+ ...row.source !== null ? { source: row.source } : {},
3934
+ ...row.invite_link_id !== null ? { inviteLinkId: row.invite_link_id } : {},
3935
+ ...row.requested_user_id !== null ? { requestedUserId: row.requested_user_id } : {},
3936
+ ...row.requested_platform_role !== null ? { requestedPlatformRole: row.requested_platform_role } : {},
3937
+ ...row.requested_tenant_id !== null ? { requestedTenantId: row.requested_tenant_id } : {},
3938
+ ...row.requested_tenant_role !== null ? { requestedTenantRole: row.requested_tenant_role } : {}
3939
+ };
3940
+ }
3941
+ function isUniqueViolation2(error) {
3942
+ return typeof error === "object" && error !== null && error.code === "23505";
3943
+ }
3944
+ var PostgreSQLOidcIdentityClaimStore = class {
3945
+ constructor(options) {
3946
+ this.pool = options.pool;
3947
+ }
3948
+ async list(status) {
3949
+ const result = status ? await this.pool.query(
3950
+ `SELECT * FROM ${TABLE5} WHERE status = $1
3951
+ ORDER BY created_at ASC, id ASC`,
3952
+ [status]
3953
+ ) : await this.pool.query(
3954
+ `SELECT * FROM ${TABLE5} ORDER BY created_at ASC, id ASC`
3955
+ );
3956
+ return result.rows.map(mapRow5);
3957
+ }
3958
+ async get(id) {
3959
+ const result = await this.pool.query(
3960
+ `SELECT * FROM ${TABLE5} WHERE id = $1`,
3961
+ [id]
3962
+ );
3963
+ return result.rows[0] ? mapRow5(result.rows[0]) : null;
3964
+ }
3965
+ async getPendingBySubject(providerId, issuer, subject, now) {
3966
+ const result = await this.pool.query(
3967
+ `SELECT * FROM ${TABLE5}
3968
+ WHERE provider_id = $1 AND issuer = $2 AND subject = $3
3969
+ AND status = 'pending' AND expires_at > $4`,
3970
+ [providerId, issuer, subject, now]
3971
+ );
3972
+ return result.rows[0] ? mapRow5(result.rows[0]) : null;
3973
+ }
3974
+ async create(input) {
3975
+ const now = /* @__PURE__ */ new Date();
3976
+ await this.pool.query(
3977
+ `UPDATE ${TABLE5}
3978
+ SET status = 'expired', updated_at = $1
3979
+ WHERE provider_id = $2 AND issuer = $3 AND subject = $4
3980
+ AND status = 'pending' AND expires_at <= $1`,
3981
+ [now, input.providerId, input.issuer, input.subject]
3982
+ );
3983
+ try {
3984
+ const result = await this.pool.query(
3985
+ `INSERT INTO ${TABLE5}
3986
+ (id, provider_id, issuer, subject, email, name, picture, status,
3987
+ created_at, updated_at, expires_at, source, invite_link_id,
3988
+ requested_user_id, requested_platform_role, requested_tenant_id,
3989
+ requested_tenant_role)
3990
+ VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8, $8, $9,
3991
+ $10, $11, $12, $13, $14, $15)
3992
+ RETURNING *`,
3993
+ [
3994
+ input.id,
3995
+ input.providerId,
3996
+ input.issuer,
3997
+ input.subject,
3998
+ normalizeOidcIdentityEmail(input.email),
3999
+ input.name ?? null,
4000
+ input.picture ?? null,
4001
+ now,
4002
+ input.expiresAt,
4003
+ input.source ?? null,
4004
+ input.inviteLinkId ?? null,
4005
+ input.requestedUserId ?? null,
4006
+ input.requestedPlatformRole ?? null,
4007
+ input.requestedTenantId ?? null,
4008
+ input.requestedTenantRole ?? null
4009
+ ]
4010
+ );
4011
+ return mapRow5(result.rows[0]);
4012
+ } catch (error) {
4013
+ if (isUniqueViolation2(error)) {
4014
+ throw new OidcIdentityClaimConflictError(
4015
+ input.providerId,
4016
+ input.issuer,
4017
+ input.subject
4018
+ );
4019
+ }
4020
+ throw error;
4021
+ }
4022
+ }
4023
+ async updatePending(id, updates, now) {
4024
+ const sets = [];
4025
+ const params = [];
4026
+ if (updates.email !== void 0) {
4027
+ params.push(normalizeOidcIdentityEmail(updates.email));
4028
+ sets.push(`email = $${params.length}`);
4029
+ }
4030
+ if (updates.name !== void 0) {
4031
+ params.push(updates.name);
4032
+ sets.push(`name = $${params.length}`);
4033
+ }
4034
+ if (updates.picture !== void 0) {
4035
+ params.push(updates.picture);
4036
+ sets.push(`picture = $${params.length}`);
4037
+ }
4038
+ if (sets.length === 0) return this.get(id);
4039
+ params.push(now);
4040
+ sets.push(`updated_at = $${params.length}`);
4041
+ params.push(id);
4042
+ const result = await this.pool.query(
4043
+ `UPDATE ${TABLE5} SET ${sets.join(", ")}
4044
+ WHERE id = $${params.length} AND status = 'pending'
4045
+ RETURNING *`,
4046
+ params
4047
+ );
4048
+ return result.rows[0] ? mapRow5(result.rows[0]) : null;
4049
+ }
4050
+ async approve(id, approval, now) {
4051
+ const result = await this.pool.query(
4052
+ `UPDATE ${TABLE5}
4053
+ SET status = 'approved', reviewed_by = $1, reviewed_at = $2,
4054
+ linked_user_id = $3, updated_at = $2
4055
+ WHERE id = $4 AND status = 'pending'
4056
+ RETURNING *`,
4057
+ [approval.reviewedBy, now, approval.linkedUserId, id]
4058
+ );
4059
+ return result.rows[0] ? mapRow5(result.rows[0]) : null;
4060
+ }
4061
+ async reject(id, reviewedBy, now) {
4062
+ const result = await this.pool.query(
4063
+ `UPDATE ${TABLE5}
4064
+ SET status = 'rejected', reviewed_by = $1, reviewed_at = $2,
4065
+ updated_at = $2
4066
+ WHERE id = $3 AND status = 'pending'
4067
+ RETURNING *`,
4068
+ [reviewedBy, now, id]
4069
+ );
4070
+ return result.rows[0] ? mapRow5(result.rows[0]) : null;
4071
+ }
4072
+ async delete(id) {
4073
+ const result = await this.pool.query(
4074
+ `DELETE FROM ${TABLE5} WHERE id = $1`,
4075
+ [id]
4076
+ );
4077
+ return (result.rowCount ?? 0) > 0;
4078
+ }
4079
+ };
4080
+
4081
+ // src/stores/PostgreSQLInviteLinkStore.ts
4082
+ import { encrypt as encrypt5, safeDecrypt as safeDecrypt5 } from "@axiom-lattice/core";
4083
+ import {
4084
+ normalizeInviteLinkEmail
4085
+ } from "@axiom-lattice/protocols";
4086
+ var TABLE6 = "lattice_invite_links";
4087
+ function mapRow6(row) {
4088
+ return {
4089
+ id: row.id,
4090
+ scope: row.scope,
4091
+ token: safeDecrypt5(row.token_encrypted, `Invite link "${row.id}" token`),
4092
+ tokenHash: row.token_hash,
4093
+ ...row.email !== null ? { email: row.email } : {},
4094
+ ...row.platform_role !== null ? { platformRole: row.platform_role } : {},
4095
+ ...row.tenant_id !== null ? { tenantId: row.tenant_id } : {},
4096
+ ...row.tenant_role !== null ? { tenantRole: row.tenant_role } : {},
4097
+ ...row.created_by !== null ? { createdBy: row.created_by } : {},
4098
+ createdAt: row.created_at,
4099
+ updatedAt: row.updated_at,
4100
+ expiresAt: row.expires_at,
4101
+ maxUses: Number(row.max_uses),
4102
+ usedCount: Number(row.used_count),
4103
+ ...row.revoked_at !== null ? { revokedAt: row.revoked_at } : {}
4104
+ };
4105
+ }
4106
+ var PostgreSQLInviteLinkStore = class {
4107
+ constructor(options) {
4108
+ this.options = options;
4109
+ }
4110
+ get pool() {
4111
+ return this.options.pool;
4112
+ }
4113
+ async list() {
4114
+ const result = await this.pool.query(
4115
+ `SELECT * FROM ${TABLE6} ORDER BY created_at DESC, id DESC`
4116
+ );
4117
+ return result.rows.map(mapRow6);
4118
+ }
4119
+ async get(id) {
4120
+ const result = await this.pool.query(
4121
+ `SELECT * FROM ${TABLE6} WHERE id = $1`,
4122
+ [id]
4123
+ );
4124
+ return result.rows[0] ? mapRow6(result.rows[0]) : null;
4125
+ }
4126
+ async getByTokenHash(tokenHash) {
4127
+ const result = await this.pool.query(
4128
+ `SELECT * FROM ${TABLE6} WHERE token_hash = $1`,
4129
+ [tokenHash]
4130
+ );
4131
+ return result.rows[0] ? mapRow6(result.rows[0]) : null;
4132
+ }
4133
+ async create(input) {
4134
+ const now = /* @__PURE__ */ new Date();
4135
+ const result = await this.pool.query(
4136
+ `INSERT INTO ${TABLE6}
4137
+ (id, scope, token_hash, token_encrypted, email, platform_role,
4138
+ tenant_id, tenant_role, created_by, created_at, updated_at,
4139
+ expires_at, max_uses, used_count)
4140
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $10, $11, $12, 0)
4141
+ RETURNING *`,
4142
+ [
4143
+ input.id,
4144
+ input.scope,
4145
+ input.tokenHash,
4146
+ encrypt5(input.token),
4147
+ input.email ? normalizeInviteLinkEmail(input.email) : null,
4148
+ input.platformRole ?? null,
4149
+ input.tenantId ?? null,
4150
+ input.tenantRole ?? null,
4151
+ input.createdBy ?? null,
4152
+ now,
4153
+ input.expiresAt,
4154
+ input.maxUses
4155
+ ]
4156
+ );
4157
+ return mapRow6(result.rows[0]);
4158
+ }
4159
+ async consume(tokenHash, now) {
4160
+ const result = await this.pool.query(
4161
+ `UPDATE ${TABLE6}
4162
+ SET used_count = used_count + 1, updated_at = $1
4163
+ WHERE token_hash = $2
4164
+ AND revoked_at IS NULL
4165
+ AND expires_at > $1
4166
+ AND used_count < max_uses
4167
+ RETURNING *`,
4168
+ [now, tokenHash]
4169
+ );
4170
+ return result.rows[0] ? mapRow6(result.rows[0]) : null;
4171
+ }
4172
+ async revoke(id, now) {
4173
+ const result = await this.pool.query(
4174
+ `UPDATE ${TABLE6}
4175
+ SET revoked_at = COALESCE(revoked_at, $1), updated_at = $1
4176
+ WHERE id = $2
4177
+ RETURNING *`,
4178
+ [now, id]
4179
+ );
4180
+ return result.rows[0] ? mapRow6(result.rows[0]) : null;
4181
+ }
4182
+ };
4183
+
3496
4184
  // src/stores/PostgreSQLWorkflowTrackingStore.ts
3497
4185
  import { randomUUID } from "crypto";
3498
4186
  import { Pool as Pool11 } from "pg";
@@ -5978,7 +6666,7 @@ var alterChannelInstallationsTable = {
5978
6666
  };
5979
6667
 
5980
6668
  // src/stores/PostgreSQLChannelInstallationStore.ts
5981
- import { safeDecrypt as safeDecrypt4, encrypt as encrypt4 } from "@axiom-lattice/core";
6669
+ import { safeDecrypt as safeDecrypt6, encrypt as encrypt6 } from "@axiom-lattice/core";
5982
6670
  var PostgreSQLChannelInstallationStore = class {
5983
6671
  constructor(options) {
5984
6672
  this.initialized = false;
@@ -6200,17 +6888,17 @@ var PostgreSQLChannelInstallationStore = class {
6200
6888
  encryptSecrets(config) {
6201
6889
  return {
6202
6890
  ...config,
6203
- ...typeof config.appSecret === "string" ? { appSecret: encrypt4(config.appSecret) } : {},
6204
- ...typeof config.verificationToken === "string" ? { verificationToken: encrypt4(config.verificationToken) } : {},
6205
- ...typeof config.encryptKey === "string" ? { encryptKey: encrypt4(config.encryptKey) } : {}
6891
+ ...typeof config.appSecret === "string" ? { appSecret: encrypt6(config.appSecret) } : {},
6892
+ ...typeof config.verificationToken === "string" ? { verificationToken: encrypt6(config.verificationToken) } : {},
6893
+ ...typeof config.encryptKey === "string" ? { encryptKey: encrypt6(config.encryptKey) } : {}
6206
6894
  };
6207
6895
  }
6208
6896
  decryptSecrets(config) {
6209
6897
  return {
6210
6898
  ...config,
6211
- ...typeof config.appSecret === "string" ? { appSecret: safeDecrypt4(config.appSecret) } : {},
6212
- ...typeof config.verificationToken === "string" ? { verificationToken: safeDecrypt4(config.verificationToken) } : {},
6213
- ...typeof config.encryptKey === "string" ? { encryptKey: safeDecrypt4(config.encryptKey) } : {}
6899
+ ...typeof config.appSecret === "string" ? { appSecret: safeDecrypt6(config.appSecret) } : {},
6900
+ ...typeof config.verificationToken === "string" ? { verificationToken: safeDecrypt6(config.verificationToken) } : {},
6901
+ ...typeof config.encryptKey === "string" ? { encryptKey: safeDecrypt6(config.encryptKey) } : {}
6214
6902
  };
6215
6903
  }
6216
6904
  };
@@ -6288,7 +6976,7 @@ var addApiKeyGrants = {
6288
6976
  };
6289
6977
 
6290
6978
  // src/stores/PostgreSQLA2AApiKeyStore.ts
6291
- import { encrypt as encrypt5, decrypt, safeDecrypt as safeDecrypt5 } from "@axiom-lattice/core";
6979
+ import { encrypt as encrypt7, decrypt, safeDecrypt as safeDecrypt7 } from "@axiom-lattice/core";
6292
6980
  import { randomUUID as randomUUID2 } from "crypto";
6293
6981
  function generateApiKey() {
6294
6982
  return `a2a_${randomUUID2().replace(/-/g, "")}`;
@@ -6296,7 +6984,7 @@ function generateApiKey() {
6296
6984
  function mapRowToRecord(row) {
6297
6985
  return {
6298
6986
  id: row.id,
6299
- key: safeDecrypt5(row.key_value),
6987
+ key: safeDecrypt7(row.key_value),
6300
6988
  tenantId: row.tenant_id,
6301
6989
  projectId: row.project_id,
6302
6990
  assistantIds: row.assistant_ids ?? void 0,
@@ -6404,7 +7092,7 @@ var PostgreSQLA2AApiKeyStore = class {
6404
7092
  `INSERT INTO lattice_a2a_api_keys (key_value, tenant_id, project_id, assistant_ids, grants, label)
6405
7093
  VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
6406
7094
  [
6407
- encrypt5(key),
7095
+ encrypt7(key),
6408
7096
  input.tenantId,
6409
7097
  input.projectId,
6410
7098
  input.assistantIds ? JSON.stringify(input.assistantIds) : null,
@@ -6473,7 +7161,7 @@ var PostgreSQLA2AApiKeyStore = class {
6473
7161
  const key = generateApiKey();
6474
7162
  const result = await this.pool.query(
6475
7163
  `UPDATE lattice_a2a_api_keys SET key_value = $1, updated_at = NOW() WHERE id = $2 RETURNING *`,
6476
- [encrypt5(key), id]
7164
+ [encrypt7(key), id]
6477
7165
  );
6478
7166
  if (result.rows.length === 0) throw new Error(`A2A API key not found: ${id}`);
6479
7167
  const record = mapRowToRecord(result.rows[0]);
@@ -6742,7 +7430,7 @@ function isIdentity(value) {
6742
7430
  function isStatus(value) {
6743
7431
  return value === "draft" || value === "active" || value === "disabled";
6744
7432
  }
6745
- function mapRow(row) {
7433
+ function mapRow7(row) {
6746
7434
  if (!isStatus(row.status) || !isIntegration(row.integration) || !isScope(row.scope) || !isFeatures(row.features) || !isAppearance(row.appearance) || !isIdentity(row.identity)) {
6747
7435
  throw new Error(`Invalid agent web app row: ${row.id}`);
6748
7436
  }
@@ -6814,7 +7502,7 @@ var PostgreSQLAgentWebAppStore = class {
6814
7502
  ORDER BY created_at DESC, id DESC`,
6815
7503
  [tenantId, assistantId]
6816
7504
  );
6817
- return result.rows.map(mapRow);
7505
+ return result.rows.map(mapRow7);
6818
7506
  }
6819
7507
  async getById(tenantId, webAppId) {
6820
7508
  await this.ensureInitialized();
@@ -6823,7 +7511,7 @@ var PostgreSQLAgentWebAppStore = class {
6823
7511
  WHERE tenant_id = $1 AND id = $2`,
6824
7512
  [tenantId, webAppId]
6825
7513
  );
6826
- return result.rows[0] ? mapRow(result.rows[0]) : null;
7514
+ return result.rows[0] ? mapRow7(result.rows[0]) : null;
6827
7515
  }
6828
7516
  async findById(webAppId) {
6829
7517
  await this.ensureInitialized();
@@ -6832,7 +7520,7 @@ var PostgreSQLAgentWebAppStore = class {
6832
7520
  WHERE id = $1`,
6833
7521
  [webAppId]
6834
7522
  );
6835
- return result.rows[0] ? mapRow(result.rows[0]) : null;
7523
+ return result.rows[0] ? mapRow7(result.rows[0]) : null;
6836
7524
  }
6837
7525
  async create(tenantId, input) {
6838
7526
  await this.ensureInitialized();
@@ -6856,7 +7544,7 @@ var PostgreSQLAgentWebAppStore = class {
6856
7544
  input.identity ? JSON.stringify(input.identity) : null
6857
7545
  ]
6858
7546
  );
6859
- return mapRow(result.rows[0]);
7547
+ return mapRow7(result.rows[0]);
6860
7548
  }
6861
7549
  async update(tenantId, webAppId, patch, options) {
6862
7550
  await this.ensureInitialized();
@@ -6898,7 +7586,7 @@ var PostgreSQLAgentWebAppStore = class {
6898
7586
  RETURNING *`,
6899
7587
  values
6900
7588
  );
6901
- return result.rows[0] ? mapRow(result.rows[0]) : null;
7589
+ return result.rows[0] ? mapRow7(result.rows[0]) : null;
6902
7590
  }
6903
7591
  async delete(tenantId, webAppId) {
6904
7592
  await this.ensureInitialized();
@@ -9991,7 +10679,7 @@ var createChannelIdentityMappingTables = {
9991
10679
  };
9992
10680
 
9993
10681
  // src/migrations/connection_config_migrations.ts
9994
- import { encrypt as encrypt6, isEncryptedValue } from "@axiom-lattice/core";
10682
+ import { encrypt as encrypt8, isEncryptedValue } from "@axiom-lattice/core";
9995
10683
  var createConnectionConfigsTable = {
9996
10684
  version: 160,
9997
10685
  name: "create_connection_configs_table",
@@ -10063,7 +10751,7 @@ var reencryptLegacyConnectionSecrets = {
10063
10751
  for (const field of secretFields) {
10064
10752
  const value = config[field];
10065
10753
  if (typeof value === "string" && value.length > 0 && !isEncryptedValue(value)) {
10066
- config[field] = encrypt6(value);
10754
+ config[field] = encrypt8(value);
10067
10755
  changed = true;
10068
10756
  }
10069
10757
  }
@@ -10079,6 +10767,254 @@ var reencryptLegacyConnectionSecrets = {
10079
10767
  }
10080
10768
  };
10081
10769
 
10770
+ // src/migrations/user_external_identity_migration.ts
10771
+ var createUserExternalIdentityTable = {
10772
+ version: 209,
10773
+ name: "create_user_external_identity",
10774
+ up: async (client) => {
10775
+ await client.query(`
10776
+ CREATE TABLE IF NOT EXISTS lattice_user_identities (
10777
+ user_id VARCHAR(255) NOT NULL,
10778
+ provider_id VARCHAR(255) NOT NULL,
10779
+ issuer TEXT NOT NULL,
10780
+ subject TEXT NOT NULL,
10781
+ email TEXT,
10782
+ linked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10783
+ PRIMARY KEY (provider_id, issuer, subject)
10784
+ )
10785
+ `);
10786
+ await client.query(`
10787
+ CREATE INDEX IF NOT EXISTS idx_lattice_user_identities_user
10788
+ ON lattice_user_identities (user_id)
10789
+ `);
10790
+ },
10791
+ down: async (client) => {
10792
+ await client.query("DROP INDEX IF EXISTS idx_lattice_user_identities_user");
10793
+ await client.query("DROP TABLE IF EXISTS lattice_user_identities");
10794
+ }
10795
+ };
10796
+
10797
+ // src/migrations/user_email_lower_unique_migration.ts
10798
+ var createUserEmailLowerUniqueIndex = {
10799
+ version: 210,
10800
+ name: "create_user_email_lower_unique_index",
10801
+ up: async (client) => {
10802
+ await client.query(`
10803
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_lattice_users_email_lower
10804
+ ON lattice_users (lower(email))
10805
+ `);
10806
+ },
10807
+ down: async (client) => {
10808
+ await client.query("DROP INDEX IF EXISTS idx_lattice_users_email_lower");
10809
+ }
10810
+ };
10811
+
10812
+ // src/migrations/oidc_provider_config_migration.ts
10813
+ var createOidcProviderConfigsTable = {
10814
+ version: 211,
10815
+ name: "create_oidc_provider_configs",
10816
+ up: async (client) => {
10817
+ await client.query(`
10818
+ CREATE TABLE IF NOT EXISTS lattice_oidc_provider_configs (
10819
+ id VARCHAR(64) PRIMARY KEY,
10820
+ display_name TEXT NOT NULL,
10821
+ issuer TEXT NOT NULL,
10822
+ client_id TEXT NOT NULL,
10823
+ client_secret TEXT NOT NULL,
10824
+ scopes TEXT NOT NULL,
10825
+ enabled BOOLEAN NOT NULL DEFAULT TRUE,
10826
+ jit_provisioning BOOLEAN NOT NULL DEFAULT FALSE,
10827
+ auto_link_email BOOLEAN NOT NULL DEFAULT FALSE,
10828
+ default_tenant_id TEXT,
10829
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10830
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
10831
+ )
10832
+ `);
10833
+ },
10834
+ down: async (client) => {
10835
+ await client.query("DROP TABLE IF EXISTS lattice_oidc_provider_configs");
10836
+ }
10837
+ };
10838
+
10839
+ // src/migrations/platform_member_migration.ts
10840
+ var createPlatformMembersTable = {
10841
+ version: 212,
10842
+ name: "create_platform_members",
10843
+ up: async (client) => {
10844
+ await client.query(`
10845
+ CREATE TABLE IF NOT EXISTS lattice_platform_members (
10846
+ user_id VARCHAR(255) PRIMARY KEY,
10847
+ role VARCHAR(16) NOT NULL,
10848
+ granted_by VARCHAR(255),
10849
+ granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10850
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
10851
+ )
10852
+ `);
10853
+ },
10854
+ down: async (client) => {
10855
+ await client.query("DROP TABLE IF EXISTS lattice_platform_members");
10856
+ }
10857
+ };
10858
+
10859
+ // src/migrations/platform_invitation_migration.ts
10860
+ var createPlatformInvitationsTable = {
10861
+ version: 213,
10862
+ name: "create_platform_invitations",
10863
+ up: async (client) => {
10864
+ await client.query(`
10865
+ CREATE TABLE IF NOT EXISTS lattice_platform_invitations (
10866
+ email VARCHAR(320) PRIMARY KEY,
10867
+ role VARCHAR(16) NOT NULL,
10868
+ invited_by VARCHAR(255),
10869
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10870
+ expires_at TIMESTAMPTZ,
10871
+ consumed_at TIMESTAMPTZ
10872
+ )
10873
+ `);
10874
+ },
10875
+ down: async (client) => {
10876
+ await client.query("DROP TABLE IF EXISTS lattice_platform_invitations");
10877
+ }
10878
+ };
10879
+
10880
+ // src/migrations/oidc_identity_claim_migration.ts
10881
+ var createOidcIdentityClaimsTable = {
10882
+ version: 214,
10883
+ name: "create_oidc_identity_claims",
10884
+ up: async (client) => {
10885
+ await client.query(`
10886
+ CREATE TABLE IF NOT EXISTS lattice_oidc_identity_claims (
10887
+ id UUID PRIMARY KEY,
10888
+ provider_id VARCHAR(64) NOT NULL,
10889
+ issuer TEXT NOT NULL,
10890
+ subject TEXT NOT NULL,
10891
+ email VARCHAR(320) NOT NULL,
10892
+ name TEXT,
10893
+ picture TEXT,
10894
+ status VARCHAR(16) NOT NULL,
10895
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10896
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10897
+ expires_at TIMESTAMPTZ NOT NULL,
10898
+ reviewed_by VARCHAR(255),
10899
+ reviewed_at TIMESTAMPTZ,
10900
+ linked_user_id VARCHAR(255)
10901
+ )
10902
+ `);
10903
+ await client.query(`
10904
+ CREATE UNIQUE INDEX IF NOT EXISTS lattice_oidc_identity_claims_pending_subject
10905
+ ON lattice_oidc_identity_claims (provider_id, issuer, subject)
10906
+ WHERE status = 'pending'
10907
+ `);
10908
+ await client.query(`
10909
+ CREATE INDEX IF NOT EXISTS lattice_oidc_identity_claims_status_created
10910
+ ON lattice_oidc_identity_claims (status, created_at)
10911
+ `);
10912
+ },
10913
+ down: async (client) => {
10914
+ await client.query("DROP TABLE IF EXISTS lattice_oidc_identity_claims");
10915
+ }
10916
+ };
10917
+
10918
+ // src/migrations/invite_link_migration.ts
10919
+ var createInviteLinksTable = {
10920
+ version: 215,
10921
+ name: "create_invite_links",
10922
+ up: async (client) => {
10923
+ await client.query(`
10924
+ CREATE TABLE IF NOT EXISTS lattice_invite_links (
10925
+ id UUID PRIMARY KEY,
10926
+ scope VARCHAR(16) NOT NULL,
10927
+ token_hash VARCHAR(64) NOT NULL UNIQUE,
10928
+ token_encrypted TEXT NOT NULL,
10929
+ email VARCHAR(320),
10930
+ platform_role VARCHAR(16),
10931
+ tenant_id VARCHAR(255),
10932
+ tenant_role VARCHAR(16),
10933
+ created_by VARCHAR(255),
10934
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10935
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10936
+ expires_at TIMESTAMPTZ NOT NULL,
10937
+ max_uses INTEGER NOT NULL,
10938
+ used_count INTEGER NOT NULL DEFAULT 0,
10939
+ revoked_at TIMESTAMPTZ
10940
+ )
10941
+ `);
10942
+ await client.query(`
10943
+ CREATE INDEX IF NOT EXISTS lattice_invite_links_created
10944
+ ON lattice_invite_links (created_at DESC)
10945
+ `);
10946
+ },
10947
+ down: async (client) => {
10948
+ await client.query("DROP TABLE IF EXISTS lattice_invite_links");
10949
+ }
10950
+ };
10951
+
10952
+ // src/migrations/oidc_identity_claim_invite_migration.ts
10953
+ var addOidcIdentityClaimInviteFields = {
10954
+ version: 216,
10955
+ name: "add_oidc_identity_claim_invite_fields",
10956
+ up: async (client) => {
10957
+ await client.query(`
10958
+ ALTER TABLE lattice_oidc_identity_claims
10959
+ ADD COLUMN IF NOT EXISTS source VARCHAR(32),
10960
+ ADD COLUMN IF NOT EXISTS invite_link_id UUID,
10961
+ ADD COLUMN IF NOT EXISTS requested_user_id VARCHAR(255),
10962
+ ADD COLUMN IF NOT EXISTS requested_platform_role VARCHAR(16),
10963
+ ADD COLUMN IF NOT EXISTS requested_tenant_id VARCHAR(255),
10964
+ ADD COLUMN IF NOT EXISTS requested_tenant_role VARCHAR(16)
10965
+ `);
10966
+ },
10967
+ down: async (client) => {
10968
+ await client.query(`
10969
+ ALTER TABLE lattice_oidc_identity_claims
10970
+ DROP COLUMN IF EXISTS source,
10971
+ DROP COLUMN IF EXISTS invite_link_id,
10972
+ DROP COLUMN IF EXISTS requested_user_id,
10973
+ DROP COLUMN IF EXISTS requested_platform_role,
10974
+ DROP COLUMN IF EXISTS requested_tenant_id,
10975
+ DROP COLUMN IF EXISTS requested_tenant_role
10976
+ `);
10977
+ }
10978
+ };
10979
+
10980
+ // src/migrations/platform_settings_migration.ts
10981
+ var createPlatformSettingsTable = {
10982
+ version: 217,
10983
+ name: "create_platform_settings",
10984
+ up: async (client) => {
10985
+ await client.query(`
10986
+ CREATE TABLE IF NOT EXISTS lattice_platform_settings (
10987
+ id VARCHAR(32) PRIMARY KEY,
10988
+ document JSONB NOT NULL,
10989
+ version INTEGER NOT NULL,
10990
+ updated_by VARCHAR(255),
10991
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
10992
+ )
10993
+ `);
10994
+ },
10995
+ down: async (client) => {
10996
+ await client.query("DROP TABLE IF EXISTS lattice_platform_settings");
10997
+ }
10998
+ };
10999
+
11000
+ // src/migrations/oidc_provider_allowed_hosts_migration.ts
11001
+ var addOidcProviderAllowedHosts = {
11002
+ version: 218,
11003
+ name: "add_oidc_provider_allowed_hosts",
11004
+ up: async (client) => {
11005
+ await client.query(`
11006
+ ALTER TABLE lattice_oidc_provider_configs
11007
+ ADD COLUMN IF NOT EXISTS allowed_private_hosts JSONB
11008
+ `);
11009
+ },
11010
+ down: async (client) => {
11011
+ await client.query(`
11012
+ ALTER TABLE lattice_oidc_provider_configs
11013
+ DROP COLUMN IF EXISTS allowed_private_hosts
11014
+ `);
11015
+ }
11016
+ };
11017
+
10082
11018
  // src/migrations/collection_migrations.ts
10083
11019
  var createCollectionsTable = {
10084
11020
  version: 137,
@@ -11144,7 +12080,7 @@ var PostgreSQLCapabilityBundleStore = class {
11144
12080
  function isValidDate(value) {
11145
12081
  return value instanceof Date && !Number.isNaN(value.getTime());
11146
12082
  }
11147
- function mapRow2(row) {
12083
+ function mapRow8(row) {
11148
12084
  if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.workspace_id !== "string" || typeof row.project_id !== "string" || row.type !== "main" || typeof row.name !== "string" || !isValidDate(row.created_at) || !isValidDate(row.updated_at)) {
11149
12085
  throw new Error("Invalid project room row");
11150
12086
  }
@@ -11175,7 +12111,7 @@ var PostgreSQLProjectRoomStore = class {
11175
12111
  RETURNING id, tenant_id, workspace_id, project_id, type, name, created_at, updated_at`,
11176
12112
  [input.id, input.tenantId, input.workspaceId, input.projectId, "main", input.name]
11177
12113
  );
11178
- return mapRow2(result.rows[0]);
12114
+ return mapRow8(result.rows[0]);
11179
12115
  }
11180
12116
  /** Finds a tenant-scoped project's main room, or returns null. */
11181
12117
  async getMainRoom(tenantId, projectId) {
@@ -11183,7 +12119,7 @@ var PostgreSQLProjectRoomStore = class {
11183
12119
  "SELECT id, tenant_id, workspace_id, project_id, type, name, created_at, updated_at FROM lattice_project_rooms WHERE tenant_id = $1 AND project_id = $2 AND type = 'main'",
11184
12120
  [tenantId, projectId]
11185
12121
  );
11186
- return result.rows[0] ? mapRow2(result.rows[0]) : null;
12122
+ return result.rows[0] ? mapRow8(result.rows[0]) : null;
11187
12123
  }
11188
12124
  };
11189
12125
 
@@ -11209,7 +12145,7 @@ function isRole(value) {
11209
12145
  function isStatus2(value) {
11210
12146
  return value === "active" || value === "removed";
11211
12147
  }
11212
- function mapRow3(row) {
12148
+ function mapRow9(row) {
11213
12149
  if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.project_id !== "string" || typeof row.user_id !== "string" || !isRole(row.role) || !isStatus2(row.status) || !isDate(row.joined_at) || !isDate(row.updated_at)) throw new Error("Invalid project membership row");
11214
12150
  return {
11215
12151
  id: row.id,
@@ -11230,7 +12166,7 @@ function mapDuplicate(error) {
11230
12166
  if (isDuplicate2(error)) throw new ProjectMembershipIdConflictError();
11231
12167
  throw error;
11232
12168
  }
11233
- var columns = "id, tenant_id, project_id, user_id, role, status, joined_at, updated_at";
12169
+ var columns2 = "id, tenant_id, project_id, user_id, role, status, joined_at, updated_at";
11234
12170
  var PostgreSQLProjectMembershipStore = class {
11235
12171
  /** Creates a store using an externally managed pool; the pool is not migrated or closed. */
11236
12172
  constructor(options) {
@@ -11238,13 +12174,13 @@ var PostgreSQLProjectMembershipStore = class {
11238
12174
  }
11239
12175
  /** Lists memberships in stable joined-time and ID order. */
11240
12176
  async list(tenantId, projectId) {
11241
- const result = await this.pool.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 ORDER BY joined_at ASC, id ASC`, [tenantId, projectId]);
11242
- return result.rows.map(mapRow3);
12177
+ const result = await this.pool.query(`SELECT ${columns2} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 ORDER BY joined_at ASC, id ASC`, [tenantId, projectId]);
12178
+ return result.rows.map(mapRow9);
11243
12179
  }
11244
12180
  /** Finds a membership by exact tenant, project, and user identity. */
11245
12181
  async findByUser(tenantId, projectId, userId) {
11246
- const result = await this.pool.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 AND user_id = $3`, [tenantId, projectId, userId]);
11247
- return result.rows[0] ? mapRow3(result.rows[0]) : null;
12182
+ const result = await this.pool.query(`SELECT ${columns2} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 AND user_id = $3`, [tenantId, projectId, userId]);
12183
+ return result.rows[0] ? mapRow9(result.rows[0]) : null;
11248
12184
  }
11249
12185
  /** Inserts a membership and maps database uniqueness errors to stable typed errors. */
11250
12186
  async create(input) {
@@ -11252,8 +12188,8 @@ var PostgreSQLProjectMembershipStore = class {
11252
12188
  try {
11253
12189
  await client.query("BEGIN");
11254
12190
  await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${input.tenantId}:${input.projectId}`]);
11255
- const result = await client.query(`INSERT INTO lattice_project_memberships (id, tenant_id, project_id, user_id, role, status, joined_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, date_trunc('milliseconds', clock_timestamp()), date_trunc('milliseconds', clock_timestamp())) RETURNING ${columns}`, [input.id, input.tenantId, input.projectId, input.userId, input.role, input.status]);
11256
- const membership = mapRow3(result.rows[0]);
12191
+ const result = await client.query(`INSERT INTO lattice_project_memberships (id, tenant_id, project_id, user_id, role, status, joined_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, date_trunc('milliseconds', clock_timestamp()), date_trunc('milliseconds', clock_timestamp())) RETURNING ${columns2}`, [input.id, input.tenantId, input.projectId, input.userId, input.role, input.status]);
12192
+ const membership = mapRow9(result.rows[0]);
11257
12193
  await client.query("COMMIT");
11258
12194
  return membership;
11259
12195
  } catch (error) {
@@ -11272,16 +12208,16 @@ var PostgreSQLProjectMembershipStore = class {
11272
12208
  try {
11273
12209
  await client.query("BEGIN");
11274
12210
  await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${input.tenantId}:${input.projectId}`]);
11275
- const existingResult = await client.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 FOR UPDATE`, [input.tenantId, input.projectId]);
12211
+ const existingResult = await client.query(`SELECT ${columns2} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 FOR UPDATE`, [input.tenantId, input.projectId]);
11276
12212
  if (existingResult.rows.length > 0) {
11277
- const existing = existingResult.rows.map(mapRow3).find((item) => item.userId === input.userId && item.role === "owner" && item.status === "active");
12213
+ const existing = existingResult.rows.map(mapRow9).find((item) => item.userId === input.userId && item.role === "owner" && item.status === "active");
11278
12214
  await client.query("COMMIT");
11279
12215
  return existing ? { kind: "existing", membership: existing } : { kind: "already_initialized" };
11280
12216
  }
11281
12217
  const idResult = await client.query("SELECT id FROM lattice_project_memberships WHERE tenant_id = $1 AND id = $2", [input.tenantId, input.id]);
11282
12218
  if (idResult.rows.length > 0) throw new ProjectMembershipIdConflictError();
11283
- const inserted = await client.query(`INSERT INTO lattice_project_memberships (id, tenant_id, project_id, user_id, role, status, joined_at, updated_at) VALUES ($1, $2, $3, $4, 'owner', 'active', date_trunc('milliseconds', clock_timestamp()), date_trunc('milliseconds', clock_timestamp())) RETURNING ${columns}`, [input.id, input.tenantId, input.projectId, input.userId]);
11284
- const membership = mapRow3(inserted.rows[0]);
12219
+ const inserted = await client.query(`INSERT INTO lattice_project_memberships (id, tenant_id, project_id, user_id, role, status, joined_at, updated_at) VALUES ($1, $2, $3, $4, 'owner', 'active', date_trunc('milliseconds', clock_timestamp()), date_trunc('milliseconds', clock_timestamp())) RETURNING ${columns2}`, [input.id, input.tenantId, input.projectId, input.userId]);
12220
+ const membership = mapRow9(inserted.rows[0]);
11285
12221
  await client.query("COMMIT");
11286
12222
  return { kind: "created", membership };
11287
12223
  } catch (error) {
@@ -11313,16 +12249,16 @@ var PostgreSQLProjectMembershipStore = class {
11313
12249
  return { kind: "not_found" };
11314
12250
  }
11315
12251
  await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${tenantId}:${identity.rows[0].project_id}`]);
11316
- const activeRows = await client.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 AND status = 'active' FOR UPDATE`, [tenantId, identity.rows[0].project_id]);
11317
- const lockedMemberships = activeRows.rows.map(mapRow3);
12252
+ const activeRows = await client.query(`SELECT ${columns2} FROM lattice_project_memberships WHERE tenant_id = $1 AND project_id = $2 AND status = 'active' FOR UPDATE`, [tenantId, identity.rows[0].project_id]);
12253
+ const lockedMemberships = activeRows.rows.map(mapRow9);
11318
12254
  let current = lockedMemberships.find((membership) => membership.id === id);
11319
12255
  if (!current) {
11320
- const target = await client.query(`SELECT ${columns} FROM lattice_project_memberships WHERE tenant_id = $1 AND id = $2 FOR UPDATE`, [tenantId, id]);
12256
+ const target = await client.query(`SELECT ${columns2} FROM lattice_project_memberships WHERE tenant_id = $1 AND id = $2 FOR UPDATE`, [tenantId, id]);
11321
12257
  if (!target.rows[0]) {
11322
12258
  await client.query("COMMIT");
11323
12259
  return { kind: "not_found" };
11324
12260
  }
11325
- current = mapRow3(target.rows[0]);
12261
+ current = mapRow9(target.rows[0]);
11326
12262
  }
11327
12263
  if (current.updatedAt.getTime() !== expected.getTime()) {
11328
12264
  await client.query("COMMIT");
@@ -11335,13 +12271,13 @@ var PostgreSQLProjectMembershipStore = class {
11335
12271
  return { kind: "last_owner" };
11336
12272
  }
11337
12273
  }
11338
- const result = await client.query(remove ? `UPDATE lattice_project_memberships SET status = 'removed', updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp())) WHERE tenant_id = $1 AND id = $2 AND date_trunc('milliseconds', updated_at) = $3 RETURNING ${columns}` : `UPDATE lattice_project_memberships SET role = $4, updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp())) WHERE tenant_id = $1 AND id = $2 AND date_trunc('milliseconds', updated_at) = $3 RETURNING ${columns}`, remove ? [tenantId, id, expected] : [tenantId, id, expected, role]);
12274
+ const result = await client.query(remove ? `UPDATE lattice_project_memberships SET status = 'removed', updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp())) WHERE tenant_id = $1 AND id = $2 AND date_trunc('milliseconds', updated_at) = $3 RETURNING ${columns2}` : `UPDATE lattice_project_memberships SET role = $4, updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp())) WHERE tenant_id = $1 AND id = $2 AND date_trunc('milliseconds', updated_at) = $3 RETURNING ${columns2}`, remove ? [tenantId, id, expected] : [tenantId, id, expected, role]);
11339
12275
  if (!result.rows[0]) {
11340
12276
  await client.query("COMMIT");
11341
12277
  return { kind: "conflict" };
11342
12278
  }
11343
12279
  await client.query("COMMIT");
11344
- return { kind: remove ? "removed" : "updated", membership: mapRow3(result.rows[0]) };
12280
+ return { kind: remove ? "removed" : "updated", membership: mapRow9(result.rows[0]) };
11345
12281
  } catch (error) {
11346
12282
  try {
11347
12283
  await client.query("ROLLBACK");
@@ -11355,7 +12291,7 @@ var PostgreSQLProjectMembershipStore = class {
11355
12291
  };
11356
12292
 
11357
12293
  // src/stores/PostgreSQLProjectBotMembershipStore.ts
11358
- var columns2 = "id, tenant_id, workspace_id, project_id, room_id, assistant_id, role, title, responsibility, mention_name, status, room_thread_id, joined_at, updated_at";
12294
+ var columns3 = "id, tenant_id, workspace_id, project_id, room_id, assistant_id, role, title, responsibility, mention_name, status, room_thread_id, joined_at, updated_at";
11359
12295
  var ProjectBotMembershipIdConflictError = class extends Error {
11360
12296
  /** Creates an accurate tenant-scoped membership ID collision error. */
11361
12297
  constructor(tenantId, id) {
@@ -11372,7 +12308,7 @@ function isRole2(value) {
11372
12308
  function isStatus3(value) {
11373
12309
  return value === "active" || value === "paused" || value === "removed";
11374
12310
  }
11375
- function mapRow4(row) {
12311
+ function mapRow10(row) {
11376
12312
  if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.workspace_id !== "string" || typeof row.project_id !== "string" || typeof row.room_id !== "string" || typeof row.assistant_id !== "string" || !isRole2(row.role) || typeof row.title !== "string" || row.responsibility !== null && typeof row.responsibility !== "string" || typeof row.mention_name !== "string" || !isStatus3(row.status) || typeof row.room_thread_id !== "string" || !isDate2(row.joined_at) || !isDate2(row.updated_at)) {
11377
12313
  throw new Error("Invalid project bot membership row");
11378
12314
  }
@@ -11393,11 +12329,11 @@ function mapRow4(row) {
11393
12329
  updatedAt: row.updated_at
11394
12330
  };
11395
12331
  }
11396
- function isUniqueViolation(error) {
12332
+ function isUniqueViolation3(error) {
11397
12333
  return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
11398
12334
  }
11399
12335
  function constraintConflict(error) {
11400
- if (!isUniqueViolation(error) || typeof error.constraint !== "string") return void 0;
12336
+ if (!isUniqueViolation3(error) || typeof error.constraint !== "string") return void 0;
11401
12337
  if (error.constraint === "uq_lattice_project_bot_memberships_coordinator") return "coordinator_conflict";
11402
12338
  if (error.constraint === "uq_lattice_project_bot_memberships_mention") return "mention_conflict";
11403
12339
  return void 0;
@@ -11416,26 +12352,26 @@ var PostgreSQLProjectBotMembershipStore = class {
11416
12352
  /** Lists retained memberships in stable join order. */
11417
12353
  async list(tenantId, projectId) {
11418
12354
  const result = await this.pool.query(
11419
- `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND project_id = $2 ORDER BY joined_at ASC, id ASC`,
12355
+ `SELECT ${columns3} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND project_id = $2 ORDER BY joined_at ASC, id ASC`,
11420
12356
  [tenantId, projectId]
11421
12357
  );
11422
- return result.rows.map(mapRow4);
12358
+ return result.rows.map(mapRow10);
11423
12359
  }
11424
12360
  /** Finds a membership by tenant-scoped ID. */
11425
12361
  async findById(tenantId, id) {
11426
12362
  const result = await this.pool.query(
11427
- `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND id = $2`,
12363
+ `SELECT ${columns3} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND id = $2`,
11428
12364
  [tenantId, id]
11429
12365
  );
11430
- return result.rows[0] ? mapRow4(result.rows[0]) : null;
12366
+ return result.rows[0] ? mapRow10(result.rows[0]) : null;
11431
12367
  }
11432
12368
  /** Finds an assistant's durable membership in a tenant-scoped project. */
11433
12369
  async findByAssistant(tenantId, projectId, assistantId) {
11434
12370
  const result = await this.pool.query(
11435
- `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND project_id = $2 AND assistant_id = $3`,
12371
+ `SELECT ${columns3} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND project_id = $2 AND assistant_id = $3`,
11436
12372
  [tenantId, projectId, assistantId]
11437
12373
  );
11438
- return result.rows[0] ? mapRow4(result.rows[0]) : null;
12374
+ return result.rows[0] ? mapRow10(result.rows[0]) : null;
11439
12375
  }
11440
12376
  /** Inserts a new membership or reactivates the assistant's durable membership atomically. */
11441
12377
  async save(input) {
@@ -11444,17 +12380,17 @@ var PostgreSQLProjectBotMembershipStore = class {
11444
12380
  await client.query("BEGIN");
11445
12381
  await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${input.tenantId}:${input.projectId}`]);
11446
12382
  const existingResult = await client.query(
11447
- `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND project_id = $2 AND assistant_id = $3 FOR UPDATE`,
12383
+ `SELECT ${columns3} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND project_id = $2 AND assistant_id = $3 FOR UPDATE`,
11448
12384
  [input.tenantId, input.projectId, input.assistantId]
11449
12385
  );
11450
- const existing = existingResult.rows[0] ? mapRow4(existingResult.rows[0]) : void 0;
12386
+ const existing = existingResult.rows[0] ? mapRow10(existingResult.rows[0]) : void 0;
11451
12387
  let result;
11452
12388
  if (existing) {
11453
12389
  result = await client.query(
11454
12390
  `UPDATE lattice_project_bot_memberships
11455
12391
  SET role = $3, title = $4, responsibility = $5, mention_name = $6, status = 'active',
11456
12392
  updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp()))
11457
- WHERE tenant_id = $1 AND id = $2 RETURNING ${columns2}`,
12393
+ WHERE tenant_id = $1 AND id = $2 RETURNING ${columns3}`,
11458
12394
  [input.tenantId, existing.id, input.role, input.title, input.responsibility ?? null, input.mentionName]
11459
12395
  );
11460
12396
  } else {
@@ -11467,7 +12403,7 @@ var PostgreSQLProjectBotMembershipStore = class {
11467
12403
  `INSERT INTO lattice_project_bot_memberships
11468
12404
  (id, tenant_id, workspace_id, project_id, room_id, assistant_id, role, title, responsibility, mention_name, status, room_thread_id, joined_at, updated_at)
11469
12405
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'active', $11, date_trunc('milliseconds', clock_timestamp()), date_trunc('milliseconds', clock_timestamp()))
11470
- RETURNING ${columns2}`,
12406
+ RETURNING ${columns3}`,
11471
12407
  [
11472
12408
  input.id,
11473
12409
  input.tenantId,
@@ -11483,14 +12419,14 @@ var PostgreSQLProjectBotMembershipStore = class {
11483
12419
  ]
11484
12420
  );
11485
12421
  }
11486
- const membership = mapRow4(result.rows[0]);
12422
+ const membership = mapRow10(result.rows[0]);
11487
12423
  await client.query("COMMIT");
11488
12424
  return { kind: existing === void 0 ? "created" : existing.status === "removed" ? "reactivated" : "updated", membership };
11489
12425
  } catch (error) {
11490
12426
  await rollback(client);
11491
12427
  const conflict = constraintConflict(error);
11492
12428
  if (conflict) return { kind: conflict };
11493
- if (isUniqueViolation(error) && error.constraint === "lattice_project_bot_memberships_pkey") {
12429
+ if (isUniqueViolation3(error) && error.constraint === "lattice_project_bot_memberships_pkey") {
11494
12430
  throw new ProjectBotMembershipIdConflictError(input.tenantId, input.id);
11495
12431
  }
11496
12432
  throw error;
@@ -11514,14 +12450,14 @@ var PostgreSQLProjectBotMembershipStore = class {
11514
12450
  if (typeof identity.rows[0].project_id !== "string") throw new Error("Invalid project bot membership row");
11515
12451
  await client.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${input.tenantId}:${identity.rows[0].project_id}`]);
11516
12452
  const locked = await client.query(
11517
- `SELECT ${columns2} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND id = $2 FOR UPDATE`,
12453
+ `SELECT ${columns3} FROM lattice_project_bot_memberships WHERE tenant_id = $1 AND id = $2 FOR UPDATE`,
11518
12454
  [input.tenantId, input.id]
11519
12455
  );
11520
12456
  if (!locked.rows[0]) {
11521
12457
  await client.query("COMMIT");
11522
12458
  return { kind: "not_found" };
11523
12459
  }
11524
- const existing = mapRow4(locked.rows[0]);
12460
+ const existing = mapRow10(locked.rows[0]);
11525
12461
  if (existing.updatedAt.getTime() !== input.expectedUpdatedAt.getTime()) {
11526
12462
  await client.query("COMMIT");
11527
12463
  return { kind: "conflict" };
@@ -11532,7 +12468,7 @@ var PostgreSQLProjectBotMembershipStore = class {
11532
12468
  SET role = $4, title = $5, responsibility = $6, mention_name = $7, status = $8,
11533
12469
  updated_at = GREATEST(date_trunc('milliseconds', updated_at) + interval '1 millisecond', date_trunc('milliseconds', clock_timestamp()))
11534
12470
  WHERE tenant_id = $1 AND id = $2 AND date_trunc('milliseconds', updated_at) = $3
11535
- RETURNING ${columns2}`,
12471
+ RETURNING ${columns3}`,
11536
12472
  [
11537
12473
  input.tenantId,
11538
12474
  input.id,
@@ -11548,7 +12484,7 @@ var PostgreSQLProjectBotMembershipStore = class {
11548
12484
  await client.query("COMMIT");
11549
12485
  return { kind: "conflict" };
11550
12486
  }
11551
- const membership = mapRow4(updated.rows[0]);
12487
+ const membership = mapRow10(updated.rows[0]);
11552
12488
  await client.query("COMMIT");
11553
12489
  return { kind: "updated", membership };
11554
12490
  } catch (error) {
@@ -11563,7 +12499,7 @@ var PostgreSQLProjectBotMembershipStore = class {
11563
12499
  };
11564
12500
 
11565
12501
  // src/stores/PostgreSQLProjectRoomMessageStore.ts
11566
- var columns3 = "id, tenant_id, workspace_id, project_id, room_id, author, content, mentions, reply_to_message_id, source, source_id, idempotency_key, created_at";
12502
+ var columns4 = "id, tenant_id, workspace_id, project_id, room_id, author, content, mentions, reply_to_message_id, source, source_id, idempotency_key, created_at";
11567
12503
  var ProjectRoomMessageIdConflictError = class extends Error {
11568
12504
  /** Creates an accurate tenant-scoped message ID collision error. */
11569
12505
  constructor(tenantId, id) {
@@ -11604,7 +12540,7 @@ function mapMention(value) {
11604
12540
  function isSource(value) {
11605
12541
  return value === "user" || value === "agent" || value === "task" || value === "routine" || value === "system";
11606
12542
  }
11607
- function mapRow5(row) {
12543
+ function mapRow11(row) {
11608
12544
  const author = mapAuthor(row.author);
11609
12545
  const content = isRecord4(row.content) && row.content.type === "text" && typeof row.content.text === "string" && hasOnlyKeys(row.content, ["type", "text"]) ? { type: "text", text: row.content.text } : void 0;
11610
12546
  const mentions = Array.isArray(row.mentions) ? row.mentions.map(mapMention) : void 0;
@@ -11631,7 +12567,7 @@ function mapRowsTolerant(rows, scope) {
11631
12567
  const messages = [];
11632
12568
  for (const row of rows) {
11633
12569
  try {
11634
- messages.push(mapRow5(row));
12570
+ messages.push(mapRow11(row));
11635
12571
  } catch (error) {
11636
12572
  console.warn("[ProjectRoomMessageStore] Skipping invalid message row", {
11637
12573
  tenantId: scope.tenantId,
@@ -11643,7 +12579,7 @@ function mapRowsTolerant(rows, scope) {
11643
12579
  }
11644
12580
  return messages;
11645
12581
  }
11646
- function isUniqueViolation2(error) {
12582
+ function isUniqueViolation4(error) {
11647
12583
  return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
11648
12584
  }
11649
12585
  var PostgreSQLProjectRoomMessageStore = class {
@@ -11658,15 +12594,15 @@ var PostgreSQLProjectRoomMessageStore = class {
11658
12594
  `INSERT INTO lattice_project_room_messages
11659
12595
  (id, tenant_id, workspace_id, project_id, room_id, author, content, mentions, reply_to_message_id, source, source_id, idempotency_key, created_at)
11660
12596
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, date_trunc('milliseconds', clock_timestamp()))
11661
- RETURNING ${columns3}`,
12597
+ RETURNING ${columns4}`,
11662
12598
  this.parameters(input)
11663
12599
  );
11664
- return mapRow5(result.rows[0]);
12600
+ return mapRow11(result.rows[0]);
11665
12601
  } catch (error) {
11666
- if (isUniqueViolation2(error) && error.constraint === "lattice_project_room_messages_pkey") {
12602
+ if (isUniqueViolation4(error) && error.constraint === "lattice_project_room_messages_pkey") {
11667
12603
  throw new ProjectRoomMessageIdConflictError(input.tenantId, input.id);
11668
12604
  }
11669
- if (isUniqueViolation2(error) && error.constraint === "uq_lattice_project_room_messages_idempotency") {
12605
+ if (isUniqueViolation4(error) && error.constraint === "uq_lattice_project_room_messages_idempotency") {
11670
12606
  throw new DuplicateProjectRoomMessageIdempotencyKeyError(input.tenantId, input.roomId, input.idempotencyKey ?? "");
11671
12607
  }
11672
12608
  throw error;
@@ -11681,12 +12617,12 @@ var PostgreSQLProjectRoomMessageStore = class {
11681
12617
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, date_trunc('milliseconds', clock_timestamp()))
11682
12618
  ON CONFLICT (tenant_id, room_id, idempotency_key) WHERE idempotency_key IS NOT NULL
11683
12619
  DO UPDATE SET idempotency_key = lattice_project_room_messages.idempotency_key
11684
- RETURNING ${columns3}`,
12620
+ RETURNING ${columns4}`,
11685
12621
  this.parameters(input)
11686
12622
  );
11687
- return mapRow5(result.rows[0]);
12623
+ return mapRow11(result.rows[0]);
11688
12624
  } catch (error) {
11689
- if (isUniqueViolation2(error) && error.constraint === "lattice_project_room_messages_pkey") {
12625
+ if (isUniqueViolation4(error) && error.constraint === "lattice_project_room_messages_pkey") {
11690
12626
  throw new ProjectRoomMessageIdConflictError(input.tenantId, input.id);
11691
12627
  }
11692
12628
  throw error;
@@ -11699,12 +12635,12 @@ var PostgreSQLProjectRoomMessageStore = class {
11699
12635
  }
11700
12636
  const limit = Math.min(100, Math.max(1, Math.trunc(Number.isFinite(input.limit) ? input.limit : 1)));
11701
12637
  const result = input.before ? await this.pool.query(
11702
- `SELECT ${columns3} FROM lattice_project_room_messages
12638
+ `SELECT ${columns4} FROM lattice_project_room_messages
11703
12639
  WHERE tenant_id = $1 AND room_id = $2 AND (created_at < $3 OR (created_at = $3 AND id < $4))
11704
12640
  ORDER BY created_at DESC, id DESC LIMIT $5`,
11705
12641
  [input.tenantId, input.roomId, input.before.createdAt, input.before.id, limit]
11706
12642
  ) : await this.pool.query(
11707
- `SELECT ${columns3} FROM lattice_project_room_messages
12643
+ `SELECT ${columns4} FROM lattice_project_room_messages
11708
12644
  WHERE tenant_id = $1 AND room_id = $2 ORDER BY created_at DESC, id DESC LIMIT $3`,
11709
12645
  [input.tenantId, input.roomId, limit]
11710
12646
  );
@@ -11713,10 +12649,10 @@ var PostgreSQLProjectRoomMessageStore = class {
11713
12649
  /** Finds a message by tenant-scoped ID. */
11714
12650
  async findById(tenantId, id) {
11715
12651
  const result = await this.pool.query(
11716
- `SELECT ${columns3} FROM lattice_project_room_messages WHERE tenant_id = $1 AND id = $2`,
12652
+ `SELECT ${columns4} FROM lattice_project_room_messages WHERE tenant_id = $1 AND id = $2`,
11717
12653
  [tenantId, id]
11718
12654
  );
11719
- return result.rows[0] ? mapRow5(result.rows[0]) : null;
12655
+ return result.rows[0] ? mapRow11(result.rows[0]) : null;
11720
12656
  }
11721
12657
  /** Counts messages created strictly after a horizon, optionally excluding one human author. */
11722
12658
  async countAfter(input) {
@@ -11774,7 +12710,7 @@ var PostgreSQLProjectRoomMessageStore = class {
11774
12710
  };
11775
12711
 
11776
12712
  // src/stores/PostgreSQLProjectRoomReadStateStore.ts
11777
- function mapRow6(row) {
12713
+ function mapRow12(row) {
11778
12714
  if (typeof row.tenant_id !== "string" || typeof row.room_id !== "string" || typeof row.user_id !== "string" || !(row.last_read_at instanceof Date) || Number.isNaN(row.last_read_at.getTime()) || !(row.updated_at instanceof Date) || Number.isNaN(row.updated_at.getTime())) {
11779
12715
  throw new Error("Invalid project room read state row");
11780
12716
  }
@@ -11792,7 +12728,7 @@ var PostgreSQLProjectRoomReadStateStore = class {
11792
12728
  FROM lattice_project_room_read_states WHERE tenant_id = $1 AND room_id = $2 AND user_id = $3`,
11793
12729
  [tenantId, roomId, userId]
11794
12730
  );
11795
- return result.rows[0] ? mapRow6(result.rows[0]) : null;
12731
+ return result.rows[0] ? mapRow12(result.rows[0]) : null;
11796
12732
  }
11797
12733
  /** Upserts the marker monotonically using GREATEST so older reports never regress it. */
11798
12734
  async markRead(input) {
@@ -11808,14 +12744,14 @@ var PostgreSQLProjectRoomReadStateStore = class {
11808
12744
  RETURNING tenant_id, room_id, user_id, last_read_at, updated_at`,
11809
12745
  [input.tenantId, input.roomId, input.userId, input.lastReadAt]
11810
12746
  );
11811
- return mapRow6(result.rows[0]);
12747
+ return mapRow12(result.rows[0]);
11812
12748
  }
11813
12749
  };
11814
12750
 
11815
12751
  // src/stores/PostgreSQLUserPushSubscriptionStore.ts
11816
12752
  import { randomUUID as randomUUID5 } from "crypto";
11817
- var columns4 = "id, tenant_id, user_id, endpoint, p256dh, auth, user_agent, created_at";
11818
- function mapRow7(row) {
12753
+ var columns5 = "id, tenant_id, user_id, endpoint, p256dh, auth, user_agent, created_at";
12754
+ function mapRow13(row) {
11819
12755
  if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.user_id !== "string" || typeof row.endpoint !== "string" || typeof row.p256dh !== "string" || typeof row.auth !== "string" || !(row.created_at instanceof Date) || Number.isNaN(row.created_at.getTime())) {
11820
12756
  throw new Error("Invalid user push subscription row");
11821
12757
  }
@@ -11837,22 +12773,22 @@ var PostgreSQLUserPushSubscriptionStore = class {
11837
12773
  /** Lists every subscription for a user. */
11838
12774
  async list(tenantId, userId) {
11839
12775
  const result = await this.pool.query(
11840
- `SELECT ${columns4} FROM lattice_user_push_subscriptions WHERE tenant_id = $1 AND user_id = $2`,
12776
+ `SELECT ${columns5} FROM lattice_user_push_subscriptions WHERE tenant_id = $1 AND user_id = $2`,
11841
12777
  [tenantId, userId]
11842
12778
  );
11843
- return result.rows.map(mapRow7);
12779
+ return result.rows.map(mapRow13);
11844
12780
  }
11845
12781
  /** Upserts a subscription keyed by tenant + user + endpoint. */
11846
12782
  async save(input) {
11847
12783
  const result = await this.pool.query(
11848
- `INSERT INTO lattice_user_push_subscriptions (${columns4})
12784
+ `INSERT INTO lattice_user_push_subscriptions (${columns5})
11849
12785
  VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
11850
12786
  ON CONFLICT (tenant_id, user_id, endpoint) DO UPDATE
11851
12787
  SET p256dh = EXCLUDED.p256dh, auth = EXCLUDED.auth, user_agent = EXCLUDED.user_agent
11852
- RETURNING ${columns4}`,
12788
+ RETURNING ${columns5}`,
11853
12789
  [randomUUID5(), input.tenantId, input.userId, input.endpoint, input.keys.p256dh, input.keys.auth, input.userAgent ?? null]
11854
12790
  );
11855
- return mapRow7(result.rows[0]);
12791
+ return mapRow13(result.rows[0]);
11856
12792
  }
11857
12793
  /** Deletes one subscription by endpoint. */
11858
12794
  async delete(tenantId, userId, endpoint) {
@@ -11866,7 +12802,7 @@ var PostgreSQLUserPushSubscriptionStore = class {
11866
12802
 
11867
12803
  // src/stores/PostgreSQLModelProviderStore.ts
11868
12804
  import { Pool as Pool27 } from "pg";
11869
- import { encrypt as encrypt7, safeDecrypt as safeDecrypt6 } from "@axiom-lattice/core";
12805
+ import { encrypt as encrypt9, safeDecrypt as safeDecrypt8 } from "@axiom-lattice/core";
11870
12806
  function extraOrUndefined(value) {
11871
12807
  return value && Object.keys(value).length > 0 ? value : void 0;
11872
12808
  }
@@ -11990,7 +12926,7 @@ var PostgreSQLModelProviderStore = class {
11990
12926
  await this.ensureInitialized();
11991
12927
  const now = /* @__PURE__ */ new Date();
11992
12928
  const nowString = now.toISOString();
11993
- const apiKeyEnc = data.apiKey ? encrypt7(data.apiKey) : null;
12929
+ const apiKeyEnc = data.apiKey ? encrypt9(data.apiKey) : null;
11994
12930
  const apiKeyHint = data.apiKey ? data.apiKey.slice(-4) : null;
11995
12931
  await this.pool.query(
11996
12932
  `
@@ -12066,7 +13002,7 @@ var PostgreSQLModelProviderStore = class {
12066
13002
  updateData.api_key_enc = null;
12067
13003
  updateData.api_key_hint = null;
12068
13004
  } else if (updates.apiKey) {
12069
- updateData.api_key_enc = encrypt7(updates.apiKey);
13005
+ updateData.api_key_enc = encrypt9(updates.apiKey);
12070
13006
  updateData.api_key_hint = updates.apiKey.slice(-4);
12071
13007
  }
12072
13008
  if (updates.enabled !== void 0) {
@@ -12386,7 +13322,7 @@ var PostgreSQLModelProviderStore = class {
12386
13322
  mapRowToEntry(row) {
12387
13323
  let apiKey;
12388
13324
  if (row.api_key_enc) {
12389
- apiKey = safeDecrypt6(row.api_key_enc);
13325
+ apiKey = safeDecrypt8(row.api_key_enc);
12390
13326
  }
12391
13327
  return {
12392
13328
  id: row.id,
@@ -12639,7 +13575,7 @@ var PostgreSQLOutboxStore = class {
12639
13575
  };
12640
13576
 
12641
13577
  // src/stores/PostgreSQLPluginDeploymentStore.ts
12642
- var columns5 = "id, tenant_id, plugin_type, plugin_version, mode, status, provider, sandbox_name, port, endpoint, image_ref, idle_timeout_ms, last_health_at, last_error, created_at, updated_at";
13578
+ var columns6 = "id, tenant_id, plugin_type, plugin_version, mode, status, provider, sandbox_name, port, endpoint, image_ref, idle_timeout_ms, last_health_at, last_error, created_at, updated_at";
12643
13579
  var statuses = [
12644
13580
  "requested",
12645
13581
  "provisioning",
@@ -12652,7 +13588,7 @@ var statuses = [
12652
13588
  function isStatus4(value) {
12653
13589
  return typeof value === "string" && statuses.includes(value);
12654
13590
  }
12655
- function mapRow8(row) {
13591
+ function mapRow14(row) {
12656
13592
  if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.plugin_type !== "string" || typeof row.plugin_version !== "string" || row.mode !== "onDemand" && row.mode !== "alwaysOn" || !isStatus4(row.status) || typeof row.provider !== "string" || typeof row.sandbox_name !== "string" || typeof row.port !== "number" || typeof row.idle_timeout_ms !== "number" || !(row.created_at instanceof Date) || Number.isNaN(row.created_at.getTime()) || !(row.updated_at instanceof Date) || Number.isNaN(row.updated_at.getTime())) {
12657
13593
  throw new Error("Invalid plugin deployment row");
12658
13594
  }
@@ -12683,23 +13619,23 @@ var PostgreSQLPluginDeploymentStore = class {
12683
13619
  /** Lists every deployment for a tenant, ordered by plugin type. */
12684
13620
  async listByTenant(tenantId) {
12685
13621
  const result = await this.pool.query(
12686
- `SELECT ${columns5} FROM lattice_plugin_deployments WHERE tenant_id = $1 ORDER BY plugin_type`,
13622
+ `SELECT ${columns6} FROM lattice_plugin_deployments WHERE tenant_id = $1 ORDER BY plugin_type`,
12687
13623
  [tenantId]
12688
13624
  );
12689
- return result.rows.map(mapRow8);
13625
+ return result.rows.map(mapRow14);
12690
13626
  }
12691
13627
  /** Returns the deployment for a tenant + plugin type, or undefined. */
12692
13628
  async get(tenantId, pluginType) {
12693
13629
  const result = await this.pool.query(
12694
- `SELECT ${columns5} FROM lattice_plugin_deployments WHERE tenant_id = $1 AND plugin_type = $2`,
13630
+ `SELECT ${columns6} FROM lattice_plugin_deployments WHERE tenant_id = $1 AND plugin_type = $2`,
12695
13631
  [tenantId, pluginType]
12696
13632
  );
12697
- return result.rows[0] ? mapRow8(result.rows[0]) : void 0;
13633
+ return result.rows[0] ? mapRow14(result.rows[0]) : void 0;
12698
13634
  }
12699
13635
  /** Upserts a deployment keyed by tenant + plugin type, preserving the original id and createdAt. */
12700
13636
  async upsert(deployment) {
12701
13637
  const result = await this.pool.query(
12702
- `INSERT INTO lattice_plugin_deployments (${columns5})
13638
+ `INSERT INTO lattice_plugin_deployments (${columns6})
12703
13639
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
12704
13640
  ON CONFLICT (tenant_id, plugin_type) DO UPDATE SET
12705
13641
  plugin_version = EXCLUDED.plugin_version,
@@ -12714,7 +13650,7 @@ var PostgreSQLPluginDeploymentStore = class {
12714
13650
  last_health_at = EXCLUDED.last_health_at,
12715
13651
  last_error = EXCLUDED.last_error,
12716
13652
  updated_at = NOW()
12717
- RETURNING ${columns5}`,
13653
+ RETURNING ${columns6}`,
12718
13654
  [
12719
13655
  deployment.id,
12720
13656
  deployment.tenantId,
@@ -12734,7 +13670,7 @@ var PostgreSQLPluginDeploymentStore = class {
12734
13670
  new Date(deployment.updatedAt)
12735
13671
  ]
12736
13672
  );
12737
- return mapRow8(result.rows[0]);
13673
+ return mapRow14(result.rows[0]);
12738
13674
  }
12739
13675
  /** Applies a partial update to mutable columns; returns undefined when no row exists. */
12740
13676
  async updateStatus(tenantId, pluginType, patch) {
@@ -12762,10 +13698,10 @@ var PostgreSQLPluginDeploymentStore = class {
12762
13698
  const result = await this.pool.query(
12763
13699
  `UPDATE lattice_plugin_deployments SET ${assignments.join(", ")}
12764
13700
  WHERE tenant_id = $${tenantParam} AND plugin_type = $${pluginParam}
12765
- RETURNING ${columns5}`,
13701
+ RETURNING ${columns6}`,
12766
13702
  params
12767
13703
  );
12768
- return result.rows[0] ? mapRow8(result.rows[0]) : void 0;
13704
+ return result.rows[0] ? mapRow14(result.rows[0]) : void 0;
12769
13705
  }
12770
13706
  /** Deletes a deployment by tenant + plugin type; returns whether a row was removed. */
12771
13707
  async delete(tenantId, pluginType) {
@@ -13730,6 +14666,79 @@ function isActiveRunViolation(error) {
13730
14666
  return code === "23505" && constraint === "uq_lattice_agent_runs_one_running";
13731
14667
  }
13732
14668
 
14669
+ // src/stores/PostgreSQLPlatformSettingsStore.ts
14670
+ import {
14671
+ PlatformSettingsVersionConflictError
14672
+ } from "@axiom-lattice/protocols";
14673
+ var TABLE7 = "lattice_platform_settings";
14674
+ var SETTINGS_ID = "default";
14675
+ var COLUMNS = "id, document, version, updated_by, updated_at";
14676
+ function normalizeUpdatedBy(updatedBy) {
14677
+ const trimmed = updatedBy?.trim();
14678
+ return trimmed ? trimmed : null;
14679
+ }
14680
+ function mapRow15(row) {
14681
+ return {
14682
+ ...row.document,
14683
+ version: Number(row.version),
14684
+ ...row.updated_by !== null ? { updatedBy: row.updated_by } : {},
14685
+ updatedAt: row.updated_at
14686
+ };
14687
+ }
14688
+ var PostgreSQLPlatformSettingsStore = class {
14689
+ constructor(options) {
14690
+ this.options = options;
14691
+ }
14692
+ get pool() {
14693
+ return this.options.pool;
14694
+ }
14695
+ async get() {
14696
+ const result = await this.pool.query(
14697
+ `SELECT ${COLUMNS} FROM ${TABLE7} WHERE id = $1`,
14698
+ [SETTINGS_ID]
14699
+ );
14700
+ return result.rows[0] ? mapRow15(result.rows[0]) : null;
14701
+ }
14702
+ async save(document, expectedVersion, updatedBy) {
14703
+ const version = expectedVersion + 1;
14704
+ const result = expectedVersion === 0 ? await this.pool.query(
14705
+ `INSERT INTO ${TABLE7} (id, document, version, updated_by, updated_at)
14706
+ VALUES ($1, $2::jsonb, $3, $4, NOW())
14707
+ ON CONFLICT (id) DO NOTHING
14708
+ RETURNING ${COLUMNS}`,
14709
+ [
14710
+ SETTINGS_ID,
14711
+ JSON.stringify(document),
14712
+ version,
14713
+ normalizeUpdatedBy(updatedBy)
14714
+ ]
14715
+ ) : await this.pool.query(
14716
+ `UPDATE ${TABLE7}
14717
+ SET document = $2::jsonb, version = $3, updated_by = $4, updated_at = NOW()
14718
+ WHERE id = $1 AND version = $5
14719
+ RETURNING ${COLUMNS}`,
14720
+ [
14721
+ SETTINGS_ID,
14722
+ JSON.stringify(document),
14723
+ version,
14724
+ normalizeUpdatedBy(updatedBy),
14725
+ expectedVersion
14726
+ ]
14727
+ );
14728
+ if (result.rows[0]) {
14729
+ return mapRow15(result.rows[0]);
14730
+ }
14731
+ const actual = await this.pool.query(
14732
+ `SELECT version FROM ${TABLE7} WHERE id = $1`,
14733
+ [SETTINGS_ID]
14734
+ );
14735
+ throw new PlatformSettingsVersionConflictError(
14736
+ expectedVersion,
14737
+ actual.rows[0] ? Number(actual.rows[0].version) : 0
14738
+ );
14739
+ }
14740
+ };
14741
+
13733
14742
  // src/stores/PostgreSQLChannelReplyJobStore.ts
13734
14743
  import { Pool as Pool31 } from "pg";
13735
14744
  import {
@@ -13737,7 +14746,7 @@ import {
13737
14746
  normalizeChannelReplyJobListLimit
13738
14747
  } from "@axiom-lattice/protocols";
13739
14748
  var DB_CLOCK_MS2 = "FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
13740
- var COLUMNS = `run_id, tenant_id, assistant_id, thread_id, adapter_channel,
14749
+ var COLUMNS2 = `run_id, tenant_id, assistant_id, thread_id, adapter_channel,
13741
14750
  channel_installation_id, reply_target, input_message_id, stream_cursor, status,
13742
14751
  attempts, available_at, lease_owner, lease_expires_at, last_error,
13743
14752
  output_message_id, created_at, updated_at, delivered_at`;
@@ -13755,7 +14764,7 @@ function toStatus(value) {
13755
14764
  }
13756
14765
  throw new Error(`Invalid channel reply job status: ${value}`);
13757
14766
  }
13758
- function mapRow9(row) {
14767
+ function mapRow16(row) {
13759
14768
  return {
13760
14769
  runId: row.run_id,
13761
14770
  tenantId: row.tenant_id,
@@ -13841,7 +14850,7 @@ var PostgreSQLChannelReplyJobStore = class {
13841
14850
  'pending', 0, $10, NULL, NULL,
13842
14851
  NULL, NULL, clock.now_ms, clock.now_ms, NULL
13843
14852
  FROM clock
13844
- RETURNING ${COLUMNS}`,
14853
+ RETURNING ${COLUMNS2}`,
13845
14854
  [
13846
14855
  input.runId,
13847
14856
  input.tenantId,
@@ -13855,22 +14864,22 @@ var PostgreSQLChannelReplyJobStore = class {
13855
14864
  input.availableAt
13856
14865
  ]
13857
14866
  );
13858
- return mapRow9(result.rows[0]);
14867
+ return mapRow16(result.rows[0]);
13859
14868
  }
13860
14869
  /** Reads one job within a tenant scope. */
13861
14870
  async get(tenantId, runId) {
13862
14871
  const result = await this.pool.query(
13863
- `SELECT ${COLUMNS}
14872
+ `SELECT ${COLUMNS2}
13864
14873
  FROM lattice_channel_reply_jobs
13865
14874
  WHERE tenant_id = $1 AND run_id = $2`,
13866
14875
  [tenantId, runId]
13867
14876
  );
13868
- return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
14877
+ return result.rowCount === 0 ? null : mapRow16(result.rows[0]);
13869
14878
  }
13870
14879
  /** Lists claimable pending jobs that are due at `now`, oldest first. */
13871
14880
  async listPending(now, limit) {
13872
14881
  const result = await this.pool.query(
13873
- `SELECT ${COLUMNS}
14882
+ `SELECT ${COLUMNS2}
13874
14883
  FROM lattice_channel_reply_jobs
13875
14884
  WHERE status = 'pending'
13876
14885
  AND available_at <= $1
@@ -13879,7 +14888,7 @@ var PostgreSQLChannelReplyJobStore = class {
13879
14888
  LIMIT $2`,
13880
14889
  [now, normalizeChannelReplyJobListLimit(limit)]
13881
14890
  );
13882
- return result.rows.map(mapRow9);
14891
+ return result.rows.map(mapRow16);
13883
14892
  }
13884
14893
  /**
13885
14894
  * Atomically leases a claimable job.
@@ -13900,10 +14909,10 @@ var PostgreSQLChannelReplyJobStore = class {
13900
14909
  AND j.status = 'pending'
13901
14910
  AND j.available_at <= clock.now_ms
13902
14911
  AND (j.lease_owner IS NULL OR j.lease_expires_at IS NULL OR j.lease_expires_at <= clock.now_ms)
13903
- RETURNING ${COLUMNS}`,
14912
+ RETURNING ${COLUMNS2}`,
13904
14913
  [tenantId, runId, owner, leaseMs]
13905
14914
  );
13906
- return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
14915
+ return result.rowCount === 0 ? null : mapRow16(result.rows[0]);
13907
14916
  }
13908
14917
  /**
13909
14918
  * Marks a job delivered when `owner` still holds the lease.
@@ -13926,10 +14935,10 @@ var PostgreSQLChannelReplyJobStore = class {
13926
14935
  AND j.run_id = $2
13927
14936
  AND j.lease_owner = $3
13928
14937
  AND j.status = 'pending'
13929
- RETURNING ${COLUMNS}`,
14938
+ RETURNING ${COLUMNS2}`,
13930
14939
  [tenantId, runId, owner, outputMessageId]
13931
14940
  );
13932
- return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
14941
+ return result.rowCount === 0 ? null : mapRow16(result.rows[0]);
13933
14942
  }
13934
14943
  /**
13935
14944
  * Returns a job to pending with an incremented attempt count.
@@ -13953,10 +14962,10 @@ var PostgreSQLChannelReplyJobStore = class {
13953
14962
  AND j.run_id = $2
13954
14963
  AND j.lease_owner = $3
13955
14964
  AND j.status = 'pending'
13956
- RETURNING ${COLUMNS}`,
14965
+ RETURNING ${COLUMNS2}`,
13957
14966
  [tenantId, runId, owner, error, availableAt]
13958
14967
  );
13959
- return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
14968
+ return result.rowCount === 0 ? null : mapRow16(result.rows[0]);
13960
14969
  }
13961
14970
  /**
13962
14971
  * Marks a job terminally failed when `owner` still holds the lease.
@@ -13978,10 +14987,10 @@ var PostgreSQLChannelReplyJobStore = class {
13978
14987
  AND j.run_id = $2
13979
14988
  AND j.lease_owner = $3
13980
14989
  AND j.status = 'pending'
13981
- RETURNING ${COLUMNS}`,
14990
+ RETURNING ${COLUMNS2}`,
13982
14991
  [tenantId, runId, owner, error]
13983
14992
  );
13984
- return result.rowCount === 0 ? null : mapRow9(result.rows[0]);
14993
+ return result.rowCount === 0 ? null : mapRow16(result.rows[0]);
13985
14994
  }
13986
14995
  /** Deletes one job. Returns whether a row was removed. */
13987
14996
  async delete(tenantId, runId) {
@@ -14041,7 +15050,7 @@ async function rollbackQuietly(client) {
14041
15050
  } catch {
14042
15051
  }
14043
15052
  }
14044
- function isUniqueViolation3(error) {
15053
+ function isUniqueViolation5(error) {
14045
15054
  return typeof error === "object" && error !== null && "code" in error && error.code === "23505";
14046
15055
  }
14047
15056
  function isRecord5(value) {
@@ -14169,7 +15178,7 @@ var PostgreSQLCheckpointStorage = class {
14169
15178
  await client.query("COMMIT");
14170
15179
  } catch (error) {
14171
15180
  await rollbackQuietly(client);
14172
- if (isUniqueViolation3(error)) {
15181
+ if (isUniqueViolation5(error)) {
14173
15182
  throw new CheckpointTargetNotEmptyError(targetThreadId);
14174
15183
  }
14175
15184
  throw error;
@@ -14463,7 +15472,7 @@ import {
14463
15472
  storeExpiresAt,
14464
15473
  storePaginationValue
14465
15474
  } from "@axiom-lattice/protocols";
14466
- function mapRow10(row) {
15475
+ function mapRow17(row) {
14467
15476
  return {
14468
15477
  namespace: row.namespace,
14469
15478
  key: row.key,
@@ -14495,7 +15504,7 @@ var PostgreSQLStore = class {
14495
15504
  RETURNING tenant_id, namespace, key, value, created_at, updated_at, expires_at`,
14496
15505
  [tenantId, input.namespace, input.key, JSON.stringify(input.value), now, expiresAt]
14497
15506
  );
14498
- return mapRow10(result.rows[0]);
15507
+ return mapRow17(result.rows[0]);
14499
15508
  }
14500
15509
  async get(tenantId, namespace, key) {
14501
15510
  assertStoreIdentity(namespace, key);
@@ -14506,7 +15515,7 @@ var PostgreSQLStore = class {
14506
15515
  AND (expires_at IS NULL OR expires_at > $4)`,
14507
15516
  [tenantId, namespace, key, /* @__PURE__ */ new Date()]
14508
15517
  );
14509
- return result.rows[0] ? mapRow10(result.rows[0]) : null;
15518
+ return result.rows[0] ? mapRow17(result.rows[0]) : null;
14510
15519
  }
14511
15520
  async delete(tenantId, namespace, key) {
14512
15521
  assertStoreIdentity(namespace, key);
@@ -14550,7 +15559,7 @@ var PostgreSQLStore = class {
14550
15559
  LIMIT $${limitParam} OFFSET $${offsetParam}`,
14551
15560
  params
14552
15561
  );
14553
- return result.rows.map(mapRow10);
15562
+ return result.rows.map(mapRow17);
14554
15563
  }
14555
15564
  async listNamespaces(tenantId, prefix = [], limit = 100) {
14556
15565
  assertStoreNamespacePrefix(prefix);
@@ -14721,6 +15730,16 @@ async function createPgStoreConfig(connectionString) {
14721
15730
  mm.register(agentRunProjectTaskRunningIndexMigration);
14722
15731
  mm.register(addModelProviderKindDefault);
14723
15732
  mm.register(addShareKind);
15733
+ mm.register(createUserExternalIdentityTable);
15734
+ mm.register(createUserEmailLowerUniqueIndex);
15735
+ mm.register(createOidcProviderConfigsTable);
15736
+ mm.register(createPlatformMembersTable);
15737
+ mm.register(createPlatformInvitationsTable);
15738
+ mm.register(createOidcIdentityClaimsTable);
15739
+ mm.register(createInviteLinksTable);
15740
+ mm.register(addOidcIdentityClaimInviteFields);
15741
+ mm.register(createPlatformSettingsTable);
15742
+ mm.register(addOidcProviderAllowedHosts);
14724
15743
  await mm.migrate();
14725
15744
  const checkpoint = PostgresSaver.fromConnString(connectionString);
14726
15745
  try {
@@ -14737,6 +15756,13 @@ async function createPgStoreConfig(connectionString) {
14737
15756
  user: new PostgreSQLUserStore(opts),
14738
15757
  tenant: new PostgreSQLTenantStore(opts),
14739
15758
  userTenantLink: new PostgreSQLUserTenantLinkStore(opts),
15759
+ userExternalIdentity: new PostgreSQLUserExternalIdentityStore(opts),
15760
+ oidcProviderConfig: new PostgreSQLOidcProviderConfigStore(opts),
15761
+ platformMember: new PostgreSQLPlatformMemberStore(opts),
15762
+ platformInvitation: new PostgreSQLPlatformInvitationStore(opts),
15763
+ oidcIdentityClaim: new PostgreSQLOidcIdentityClaimStore(opts),
15764
+ inviteLink: new PostgreSQLInviteLinkStore(opts),
15765
+ platformSettings: new PostgreSQLPlatformSettingsStore(opts),
14740
15766
  channelBinding: new ChannelBindingStore(opts),
14741
15767
  channelInstallation: new PostgreSQLChannelInstallationStore(opts),
14742
15768
  thread: new PostgreSQLThreadStore(opts),
@@ -15333,6 +16359,7 @@ export {
15333
16359
  InvalidFileShareAddressError,
15334
16360
  InvalidMcpAppShareAddressError,
15335
16361
  InvalidMcpAppShareVisibilityError,
16362
+ InvalidUserExternalIdentityRowError,
15336
16363
  MenuStore,
15337
16364
  MigrationManager,
15338
16365
  PGVectorStoreProvider,
@@ -15349,10 +16376,16 @@ export {
15349
16376
  PostgreSQLConnectionStore,
15350
16377
  PostgreSQLDatabaseConfigStore,
15351
16378
  PostgreSQLEvalStore,
16379
+ PostgreSQLInviteLinkStore,
15352
16380
  PostgreSQLMcpServerConfigStore,
15353
16381
  PostgreSQLMetricsServerConfigStore,
15354
16382
  PostgreSQLModelProviderStore,
16383
+ PostgreSQLOidcIdentityClaimStore,
16384
+ PostgreSQLOidcProviderConfigStore,
15355
16385
  PostgreSQLOpenAuditStore,
16386
+ PostgreSQLPlatformInvitationStore,
16387
+ PostgreSQLPlatformMemberStore,
16388
+ PostgreSQLPlatformSettingsStore,
15356
16389
  PostgreSQLPluginDeploymentStore,
15357
16390
  PostgreSQLProjectBotMembershipStore,
15358
16391
  PostgreSQLProjectMembershipStore,
@@ -15366,6 +16399,7 @@ export {
15366
16399
  PostgreSQLTaskWorkItemStore,
15367
16400
  PostgreSQLTenantStore,
15368
16401
  PostgreSQLThreadStore,
16402
+ PostgreSQLUserExternalIdentityStore,
15369
16403
  PostgreSQLUserStore,
15370
16404
  PostgreSQLUserTenantLinkStore,
15371
16405
  PostgreSQLWorkflowTrackingStore,
@@ -15383,6 +16417,7 @@ export {
15383
16417
  addInterruptColumnsToEval,
15384
16418
  addInterruptPolicyToEvalCases,
15385
16419
  addMcpAppContentType,
16420
+ addOidcIdentityClaimInviteFields,
15386
16421
  addProjectConfigColumn,
15387
16422
  addScheduleTenantId,
15388
16423
  addShareKind,
@@ -15412,11 +16447,17 @@ export {
15412
16447
  createEvalRunResultsTable,
15413
16448
  createEvalRunsTable,
15414
16449
  createEvalSuitesTable,
16450
+ createInviteLinksTable,
15415
16451
  createMcpServerConfigsTable,
15416
16452
  createMenuItemsTable,
15417
16453
  createMetricsConfigsTable,
16454
+ createOidcIdentityClaimsTable,
16455
+ createOidcProviderConfigsTable,
15418
16456
  createPGVectorStoreProvider,
15419
16457
  createPgStoreConfig,
16458
+ createPlatformInvitationsTable,
16459
+ createPlatformMembersTable,
16460
+ createPlatformSettingsTable,
15420
16461
  createPluginDeployments,
15421
16462
  createProjectRoomReadStates,
15422
16463
  createProjectRoomTables,
@@ -15428,6 +16469,8 @@ export {
15428
16469
  createTenantsTable,
15429
16470
  createThreadMessageQueueTable,
15430
16471
  createThreadsTable,
16472
+ createUserEmailLowerUniqueIndex,
16473
+ createUserExternalIdentityTable,
15431
16474
  createUsersTable,
15432
16475
  createWorkflowTrackingTables,
15433
16476
  createWorkspacesTable,