@axiom-lattice/pg-stores 3.3.4 → 3.3.6

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 (29) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +20 -0
  3. package/dist/index.d.mts +6 -4
  4. package/dist/index.d.ts +6 -4
  5. package/dist/index.js +153 -71
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +160 -78
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +2 -2
  10. package/scripts/verify-model-provider-migrations.ts +214 -0
  11. package/scripts/verify-store.ts +232 -0
  12. package/src/__tests__/PostgreSQLA2AApiKeyStore.test.ts +41 -4
  13. package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +48 -4
  14. package/src/__tests__/PostgreSQLConnectionStore.integration.test.ts +60 -1
  15. package/src/__tests__/PostgreSQLDatabaseConfigStore.test.ts +53 -0
  16. package/src/__tests__/PostgreSQLMcpServerConfigStore.test.ts +43 -0
  17. package/src/__tests__/PostgreSQLMetricsServerConfigStore.test.ts +59 -0
  18. package/src/__tests__/PostgreSQLModelProviderStore.test.ts +58 -0
  19. package/src/__tests__/PostgreSQLProjectRoomMessageStore.correlatedInterrupt.integration.test.ts +74 -0
  20. package/src/__tests__/PostgreSQLProjectRoomMessageStore.test.ts +23 -0
  21. package/src/createPgStoreConfig.ts +6 -1
  22. package/src/migrations/connection_config_migrations.ts +56 -0
  23. package/src/migrations/model_provider_migrations.ts +74 -0
  24. package/src/stores/PostgreSQLA2AApiKeyStore.ts +4 -17
  25. package/src/stores/PostgreSQLChannelInstallationStore.ts +4 -4
  26. package/src/stores/PostgreSQLDatabaseConfigStore.ts +2 -7
  27. package/src/stores/PostgreSQLMcpServerConfigStore.ts +9 -19
  28. package/src/stores/PostgreSQLMetricsServerConfigStore.ts +3 -13
  29. package/src/stores/PostgreSQLModelProviderStore.ts +128 -25
@@ -13,7 +13,7 @@ import {
13
13
  } from '@axiom-lattice/protocols';
14
14
  import { MigrationManager } from '../migrations/migration';
15
15
  import { createMetricsConfigsTable } from '../migrations/metrics_config_migrations';
16
- import { encrypt, decrypt } from '@axiom-lattice/core';
16
+ import { encrypt, safeDecrypt } from '@axiom-lattice/core';
17
17
 
18
18
  /**
19
19
  * PostgreSQL MetricsServerConfigStore options
@@ -419,22 +419,12 @@ export class PostgreSQLMetricsServerConfigStore implements MetricsServerConfigSt
419
419
 
420
420
  // Decrypt apiKey if present
421
421
  if (config.apiKey) {
422
- try {
423
- config.apiKey = decrypt(config.apiKey);
424
- } catch (error) {
425
- console.error('Failed to decrypt apiKey:', error);
426
- throw new Error('Failed to decrypt metrics server configuration');
427
- }
422
+ config.apiKey = safeDecrypt(config.apiKey);
428
423
  }
429
424
 
430
425
  // Decrypt password if present
431
426
  if (config.password) {
432
- try {
433
- config.password = decrypt(config.password);
434
- } catch (error) {
435
- console.error('Failed to decrypt password:', error);
436
- throw new Error('Failed to decrypt metrics server configuration');
437
- }
427
+ config.password = safeDecrypt(config.password);
438
428
  }
439
429
 
440
430
  return {
@@ -20,8 +20,12 @@ import { MigrationManager } from '../migrations/migration';
20
20
  import {
21
21
  createModelProvidersTable,
22
22
  createModelProviderModelsTable,
23
+ addModelProviderApiStyle,
24
+ addModelProviderModelStale,
25
+ addModelProviderModelExtra,
26
+ addModelProviderModelDefault,
23
27
  } from '../migrations/model_provider_migrations';
24
- import { encrypt, decrypt } from '@axiom-lattice/core';
28
+ import { encrypt, safeDecrypt } from '@axiom-lattice/core';
25
29
 
26
30
  /**
27
31
  * PostgreSQL ModelProviderStore options
@@ -76,9 +80,25 @@ interface ModelRow {
76
80
  upstream_created: Date | null;
77
81
  enabled: boolean;
78
82
  stale: boolean;
83
+ extra: Record<string, unknown> | null;
84
+ is_default: boolean;
79
85
  discovered_at: Date;
80
86
  }
81
87
 
88
+ /**
89
+ * A model's stored config, or undefined when it carries none.
90
+ *
91
+ * The column is `NOT NULL DEFAULT '{}'`, so "no per-model config" arrives as an
92
+ * empty object. The protocol types `extra` as optional and the in-memory store
93
+ * leaves it undefined, so normalising here keeps the two stores
94
+ * interchangeable rather than letting callers tell which one is behind them.
95
+ */
96
+ function extraOrUndefined(
97
+ value: Record<string, unknown> | null
98
+ ): Record<string, unknown> | undefined {
99
+ return value && Object.keys(value).length > 0 ? value : undefined;
100
+ }
101
+
82
102
  /**
83
103
  * PostgreSQL implementation of ModelProviderStore
84
104
  *
@@ -113,8 +133,17 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
113
133
  }
114
134
 
115
135
  this.migrationManager = new MigrationManager(this.pool);
116
- this.migrationManager.register(createModelProvidersTable);
117
- this.migrationManager.register(createModelProviderModelsTable);
136
+ // Every migration that touches these two tables, not just the ones that
137
+ // create them. This store can migrate itself (standalone `autoMigrate`,
138
+ // without going through `createPgStoreConfig`), and a list that stops at
139
+ // v182 leaves such a database without `stale` or `extra` — the reads below
140
+ // then fail on a missing column rather than returning an empty value.
141
+ this.migrationManager.register(createModelProvidersTable); // v181
142
+ this.migrationManager.register(createModelProviderModelsTable); // v182
143
+ this.migrationManager.register(addModelProviderApiStyle); // v183
144
+ this.migrationManager.register(addModelProviderModelStale); // v184
145
+ this.migrationManager.register(addModelProviderModelExtra); // v191
146
+ this.migrationManager.register(addModelProviderModelDefault); // v192
118
147
 
119
148
  // Auto-migrate by default
120
149
  if (options.autoMigrate !== false) {
@@ -440,7 +469,7 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
440
469
  ? await this.pool.query<ModelRow>(
441
470
  `
442
471
  SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
443
- upstream_created, enabled, stale, discovered_at
472
+ upstream_created, enabled, stale, extra, is_default, discovered_at
444
473
  FROM lattice_model_provider_models
445
474
  WHERE tenant_id = $1 AND provider_id = $2
446
475
  ORDER BY model_id ASC
@@ -450,7 +479,7 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
450
479
  : await this.pool.query<ModelRow>(
451
480
  `
452
481
  SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
453
- upstream_created, enabled, discovered_at
482
+ upstream_created, enabled, stale, extra, is_default, discovered_at
454
483
  FROM lattice_model_provider_models
455
484
  WHERE tenant_id = $1
456
485
  ORDER BY model_id ASC
@@ -476,7 +505,7 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
476
505
  const result = await this.pool.query<ModelRow>(
477
506
  `
478
507
  SELECT id, tenant_id, provider_id, model_id, display_name, owned_by,
479
- upstream_created, enabled, discovered_at
508
+ upstream_created, enabled, stale, extra, is_default, discovered_at
480
509
  FROM lattice_model_provider_models
481
510
  WHERE tenant_id = $1 AND provider_id = $2 AND model_id = $3
482
511
  `,
@@ -498,8 +527,12 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
498
527
  * is an independent model.
499
528
  *
500
529
  * Models the upstream no longer offers are deleted; models that survive are
501
- * upserted *without* touching `enabled`, so the tenant's enable/disable
502
- * choice survives a re-discovery. New rows take the column default (enabled).
530
+ * upserted *without* touching `enabled` or `extra`, so the tenant's choices
531
+ * survive a re-discovery. New rows take the column defaults.
532
+ *
533
+ * The default model is the exception to the delete: it is kept and flagged
534
+ * `stale` instead, because deleting it would take the tenant's default away
535
+ * with no confirmation.
503
536
  */
504
537
  async replaceProviderModels(
505
538
  tenantId: string,
@@ -513,12 +546,17 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
513
546
  try {
514
547
  await client.query('BEGIN');
515
548
 
516
- // Read the current enabled state first, so the write below can state it
517
- // explicitly: discovery leaves `enabled` out (the tenant's choice is kept)
518
- // while an explicit selection supplies it.
519
- const current = await client.query<{ model_id: string; enabled: boolean }>(
549
+ // Read the current per-row state first, so the write below can state it
550
+ // explicitly: discovery leaves `enabled` / `extra` out (the tenant's
551
+ // choice is kept) while an explicit selection supplies it.
552
+ const current = await client.query<{
553
+ model_id: string;
554
+ enabled: boolean;
555
+ extra: Record<string, unknown> | null;
556
+ is_default: boolean;
557
+ }>(
520
558
  `
521
- SELECT model_id, enabled FROM lattice_model_provider_models
559
+ SELECT model_id, enabled, extra, is_default FROM lattice_model_provider_models
522
560
  WHERE tenant_id = $1 AND provider_id = $2
523
561
  `,
524
562
  [tenantId, providerId]
@@ -526,6 +564,12 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
526
564
  const currentEnabled = new Map(
527
565
  current.rows.map((row) => [row.model_id, row.enabled])
528
566
  );
567
+ const currentExtra = new Map(
568
+ current.rows.map((row) => [row.model_id, row.extra])
569
+ );
570
+ const currentIsDefault = new Map(
571
+ current.rows.map((row) => [row.model_id, row.is_default])
572
+ );
529
573
 
530
574
  // De-duplicate by model id: a single `ON CONFLICT DO UPDATE` statement
531
575
  // cannot touch the same target row twice, and upstreams (and callers)
@@ -540,7 +584,63 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
540
584
  (model) => model.enabled ?? currentEnabled.get(model.id) ?? true
541
585
  );
542
586
 
543
- // Drop only what the upstream no longer offers
587
+ // Same rule as `enabled`: an omitted `extra` means "leave the stored
588
+ // config alone", so a sync — which carries no per-model config at all —
589
+ // cannot wipe what the tenant set by hand.
590
+ //
591
+ // The fallback is `{}` rather than null: the column is NOT NULL, and
592
+ // writing an explicit null fails the constraint for every model that has
593
+ // no config, which is most of them.
594
+ const effectiveExtra = rows.map(
595
+ (model) => model.extra ?? currentExtra.get(model.id) ?? {}
596
+ );
597
+
598
+ // Whether this write is naming a default at all.
599
+ //
600
+ // The unique index spans the *tenant*, while this method writes one
601
+ // provider. Two consequences:
602
+ //
603
+ // 1. A swap (A was the default, B now is) would see two true rows
604
+ // mid-statement — PostgreSQL checks a unique index per row, so whether
605
+ // it fails would depend on which row the batch happened to process
606
+ // first. Clearing tenant-wide *before* the upsert makes order
607
+ // irrelevant.
608
+ // 2. Once cleared, an omitted flag must resolve to `false`, not "keep":
609
+ // keeping it would restore the old default alongside the new one.
610
+ const namesDefault = rows.some((model) => model.isDefault === true);
611
+ if (namesDefault) {
612
+ await client.query(
613
+ `UPDATE lattice_model_provider_models SET is_default = FALSE WHERE tenant_id = $1`,
614
+ [tenantId]
615
+ );
616
+ }
617
+
618
+ const effectiveIsDefault = rows.map(
619
+ (model) =>
620
+ model.isDefault ??
621
+ (namesDefault ? false : currentIsDefault.get(model.id)) ??
622
+ false
623
+ );
624
+
625
+ // Drop only what the upstream no longer offers — except the default.
626
+ //
627
+ // Deleting it would take the tenant's default away with no confirmation,
628
+ // and nothing would put it back. That is not hypothetical: a discovery
629
+ // pass replaces the set wholesale, so an upstream that stops offering the
630
+ // default model would silently delete it. Keep it, flagged, the way the
631
+ // tenant-facing sync does.
632
+
633
+ await client.query(
634
+ `
635
+ UPDATE lattice_model_provider_models
636
+ SET stale = TRUE
637
+ WHERE tenant_id = $1
638
+ AND provider_id = $2
639
+ AND model_id <> ALL($3::text[])
640
+ AND is_default
641
+ `,
642
+ [tenantId, providerId, rows.map((model) => model.id)]
643
+ );
544
644
 
545
645
  await client.query(
546
646
  `
@@ -548,26 +648,28 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
548
648
  WHERE tenant_id = $1
549
649
  AND provider_id = $2
550
650
  AND model_id <> ALL($3::text[])
651
+ AND NOT is_default
551
652
  `,
552
653
  [tenantId, providerId, rows.map((model) => model.id)]
553
654
  );
554
655
 
555
656
  if (models.length > 0) {
556
657
  // One statement for the whole snapshot rather than one per model.
557
- // `enabled` is deliberately absent from both the column list and the
558
- // update clause, so existing rows keep their value.
658
+ // `enabled` and `extra` are computed above so that an omitted value
659
+ // resolves to the row's current one rather than being cleared.
559
660
  await client.query(
560
661
  `
561
662
  INSERT INTO lattice_model_provider_models (
562
663
  id, tenant_id, provider_id, model_id, display_name, owned_by,
563
- upstream_created, raw, enabled, stale, discovered_at
664
+ upstream_created, raw, enabled, stale, extra, is_default, discovered_at
564
665
  )
565
666
  SELECT * FROM unnest(
566
667
  $1::text[], $2::text[], $3::text[], $4::text[], $5::text[],
567
- $6::text[], $7::timestamp[], $8::jsonb[], $9::boolean[], $10::boolean[], $11::timestamp[]
668
+ $6::text[], $7::timestamp[], $8::jsonb[], $9::boolean[], $10::boolean[],
669
+ $11::jsonb[], $12::boolean[], $13::timestamp[]
568
670
  ) AS t(
569
671
  id, tenant_id, provider_id, model_id, display_name, owned_by,
570
- upstream_created, raw, enabled, stale, discovered_at
672
+ upstream_created, raw, enabled, stale, extra, is_default, discovered_at
571
673
  )
572
674
  ON CONFLICT (tenant_id, provider_id, model_id) DO UPDATE SET
573
675
  display_name = EXCLUDED.display_name,
@@ -576,6 +678,8 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
576
678
  raw = EXCLUDED.raw,
577
679
  enabled = EXCLUDED.enabled,
578
680
  stale = EXCLUDED.stale,
681
+ extra = EXCLUDED.extra,
682
+ is_default = EXCLUDED.is_default,
579
683
  discovered_at = EXCLUDED.discovered_at
580
684
  `,
581
685
  [
@@ -593,6 +697,8 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
593
697
  ),
594
698
  effectiveEnabled,
595
699
  rows.map((model) => model.stale ?? false),
700
+ effectiveExtra.map((extra) => JSON.stringify(extra)),
701
+ effectiveIsDefault,
596
702
  rows.map(() => new Date().toISOString()),
597
703
  ]
598
704
  );
@@ -682,12 +788,7 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
682
788
  let apiKey: string | undefined;
683
789
 
684
790
  if (row.api_key_enc) {
685
- try {
686
- apiKey = decrypt(row.api_key_enc);
687
- } catch (error) {
688
- console.error('Failed to decrypt model provider API key:', error);
689
- throw new Error('Failed to decrypt model provider API key');
690
- }
791
+ apiKey = safeDecrypt(row.api_key_enc);
691
792
  }
692
793
 
693
794
  return {
@@ -725,6 +826,8 @@ export class PostgreSQLModelProviderStore implements ModelProviderStore {
725
826
  upstreamCreatedAt: row.upstream_created || undefined,
726
827
  enabled: row.enabled,
727
828
  stale: row.stale,
829
+ extra: extraOrUndefined(row.extra),
830
+ isDefault: row.is_default,
728
831
  discoveredAt: row.discovered_at,
729
832
  };
730
833
  }