@axiom-lattice/pg-stores 1.0.68 → 1.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/pg-stores",
3
- "version": "1.0.68",
3
+ "version": "1.0.69",
4
4
  "description": "PG stores implementation for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -22,8 +22,8 @@
22
22
  "dependencies": {
23
23
  "pg": "^8.16.3",
24
24
  "uuid": "^9.0.1",
25
- "@axiom-lattice/core": "2.1.77",
26
- "@axiom-lattice/protocols": "2.1.39"
25
+ "@axiom-lattice/core": "2.1.78",
26
+ "@axiom-lattice/protocols": "2.1.40"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^20.11.24",
@@ -20,6 +20,7 @@ import { PostgreSQLEvalStore } from "./stores/PostgreSQLEvalStore";
20
20
  import { ThreadMessageQueueStore } from "./stores/ThreadMessageQueueStore";
21
21
  import { ChannelBindingStore } from "./stores/ChannelBindingStore";
22
22
  import { PostgreSQLChannelInstallationStore } from "./stores/PostgreSQLChannelInstallationStore";
23
+ import { PostgreSQLA2AApiKeyStore } from "./stores/PostgreSQLA2AApiKeyStore";
23
24
  import { PostgreSQLScheduleStorage } from "./stores/PostgreSQLScheduleStorage";
24
25
 
25
26
  export function createPgStoreConfig(connectionString: string) {
@@ -42,6 +43,7 @@ export function createPgStoreConfig(connectionString: string) {
42
43
  assistant: new PostgreSQLAssistantStore(opts),
43
44
  workflowTracking: new PostgreSQLWorkflowTrackingStore(optsAuto),
44
45
  threadMessageQueue: new ThreadMessageQueueStore(optsAuto),
46
+ a2aApiKey: new PostgreSQLA2AApiKeyStore(optsAuto),
45
47
  schedule: new PostgreSQLScheduleStorage(opts),
46
48
  };
47
49
  }
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ export * from "./migrations/metrics_config_migrations";
39
39
  export * from "./migrations/mcp_server_config_migrations";
40
40
  export * from "./migrations/workspace_migrations";
41
41
  export * from "./migrations/project_migrations";
42
+ export * from "./migrations/add_project_config_column";
42
43
  export * from "./migrations/user_migrations";
43
44
  export * from "./migrations/tenant_migrations";
44
45
  export * from "./migrations/thread_message_queue_migrations";
@@ -46,11 +47,13 @@ export * from "./migrations/channel_identity_mapping_migration";
46
47
  export * from "./migrations/channel_installation_migrations";
47
48
  export * from "./migrations/channel_installations_alter_migration";
48
49
  export * from "./migrations/channel_bindings_migration";
50
+ export * from "./migrations/a2a_api_key_migration";
49
51
  export * from "./migrations/workflow_tracking_migrations";
50
52
  export * from "./stores/ChannelBindingStore";
51
53
  export * from "./stores/ThreadMessageQueueStore";
52
54
  export * from "./stores/ChannelIdentityMappingStore";
53
55
  export * from "./stores/PostgreSQLChannelInstallationStore";
56
+ export * from "./stores/PostgreSQLA2AApiKeyStore";
54
57
  export * from "./stores/PostgreSQLWorkflowTrackingStore";
55
58
  export * from "./stores/PostgreSQLEvalStore";
56
59
  export * from "./migrations/eval_migrations";
@@ -71,6 +74,7 @@ export { PostgreSQLUserTenantLinkStore } from "./stores/PostgreSQLUserTenantLink
71
74
  export { ChannelBindingStore } from "./stores/ChannelBindingStore";
72
75
  export { ChannelIdentityMappingStore } from "./stores/ChannelIdentityMappingStore";
73
76
  export { PostgreSQLChannelInstallationStore } from "./stores/PostgreSQLChannelInstallationStore";
77
+ export { PostgreSQLA2AApiKeyStore } from "./stores/PostgreSQLA2AApiKeyStore";
74
78
  export { PostgreSQLWorkflowTrackingStore } from "./stores/PostgreSQLWorkflowTrackingStore";
75
79
  export { PostgreSQLEvalStore } from "./stores/PostgreSQLEvalStore";
76
80
 
@@ -0,0 +1,36 @@
1
+ import type { Migration } from "./migration";
2
+ import type { PoolClient } from "pg";
3
+
4
+ export const createA2AApiKeysTable: Migration = {
5
+ version: 130,
6
+ name: "create_a2a_api_keys_table",
7
+ up: async (client: PoolClient) => {
8
+ await client.query(`
9
+ CREATE TABLE IF NOT EXISTS lattice_a2a_api_keys (
10
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
11
+ key_value TEXT NOT NULL UNIQUE,
12
+ tenant_id VARCHAR(255) NOT NULL,
13
+ project_id VARCHAR(255),
14
+ workspace_id VARCHAR(255),
15
+ label VARCHAR(255),
16
+ enabled BOOLEAN NOT NULL DEFAULT true,
17
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
18
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
19
+ )
20
+ `);
21
+
22
+ await client.query(`
23
+ CREATE INDEX IF NOT EXISTS idx_lattice_a2a_keys_tenant
24
+ ON lattice_a2a_api_keys(tenant_id)
25
+ `);
26
+
27
+ await client.query(`
28
+ CREATE INDEX IF NOT EXISTS idx_lattice_a2a_keys_value
29
+ ON lattice_a2a_api_keys(key_value)
30
+ WHERE enabled = true
31
+ `);
32
+ },
33
+ down: async (client: PoolClient) => {
34
+ await client.query("DROP TABLE IF EXISTS lattice_a2a_api_keys");
35
+ },
36
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Migration: Add config column to lattice_projects
3
+ *
4
+ * Adds a JSONB `config` column to the `lattice_projects` table for storing
5
+ * application-specific project configuration. Also creates a GIN index for
6
+ * efficient JSONB queries.
7
+ */
8
+
9
+ import { PoolClient } from "pg";
10
+ import { Migration } from "./migration";
11
+
12
+ /** Adds config JSONB column + GIN index to lattice_projects */
13
+ export const addProjectConfigColumn: Migration = {
14
+ version: 24,
15
+ name: "add_project_config_column",
16
+ up: async (client: PoolClient) => {
17
+ await client.query(`
18
+ ALTER TABLE lattice_projects
19
+ ADD COLUMN IF NOT EXISTS config JSONB
20
+ `);
21
+
22
+ await client.query(`
23
+ CREATE INDEX IF NOT EXISTS idx_lattice_projects_config
24
+ ON lattice_projects USING GIN (config)
25
+ `);
26
+ },
27
+ down: async (client: PoolClient) => {
28
+ await client.query("DROP INDEX IF EXISTS idx_lattice_projects_config");
29
+ await client.query(`
30
+ ALTER TABLE lattice_projects
31
+ DROP COLUMN IF EXISTS config
32
+ `);
33
+ },
34
+ };
@@ -1,5 +1,30 @@
1
1
  /**
2
2
  * Migration system for database schema management
3
+ *
4
+ * ## Migration Version Rules
5
+ *
6
+ * Version numbers are globally unique across all migrations applied to the same
7
+ * database. They are NOT auto-assigned — you must pick one manually.
8
+ *
9
+ * ### Numbering scheme
10
+ *
11
+ * | Range | Purpose |
12
+ * |---------|--------------------------------------|
13
+ * | 1–99 | Core table creation + column changes |
14
+ * | 100+ | Independent feature modules |
15
+ *
16
+ * ### How to pick a version
17
+ *
18
+ * 1. Run `grep -rn "version:" src/migrations/ | sort -t: -k3 -n` to list all versions
19
+ * 2. For a core column change: find the next unused number in the 1–99 range
20
+ * 3. For a new feature module: find the next unused number in the 100+ range
21
+ * 4. Verify your chosen version is NOT in the grep output
22
+ *
23
+ * @remarks
24
+ * - Migrations are sorted and applied in version order on startup
25
+ * - The version must be strictly greater than any migration it depends on
26
+ * - Use `IF NOT EXISTS` / `IF EXISTS` in SQL to keep migrations idempotent
27
+ * - A version number collision will cause one migration to be silently skipped
3
28
  */
4
29
 
5
30
  import { Pool, PoolClient } from "pg";
@@ -0,0 +1,187 @@
1
+ import { Pool } from "pg";
2
+ import type { PoolConfig } from "pg";
3
+ import type {
4
+ A2AApiKeyStore,
5
+ A2AApiKeyRecord,
6
+ CreateA2AApiKeyInput,
7
+ A2AApiKeyEntry,
8
+ } from "@axiom-lattice/protocols";
9
+ import { MigrationManager } from "../migrations/migration";
10
+ import { createA2AApiKeysTable } from "../migrations/a2a_api_key_migration";
11
+ import { encrypt, decrypt } from "@axiom-lattice/core";
12
+ import { randomUUID } from "crypto";
13
+
14
+ export interface PostgreSQLA2AApiKeyStoreOptions {
15
+ poolConfig: string | PoolConfig;
16
+ autoMigrate?: boolean;
17
+ }
18
+
19
+ type KeyRow = {
20
+ id: string;
21
+ key_value: string;
22
+ tenant_id: string;
23
+ project_id: string | null;
24
+ workspace_id: string | null;
25
+ label: string | null;
26
+ enabled: boolean;
27
+ created_at: Date;
28
+ updated_at: Date;
29
+ };
30
+
31
+ function generateApiKey(): string {
32
+ return `a2a_${randomUUID().replace(/-/g, "")}`;
33
+ }
34
+
35
+ function mapRowToRecord(row: KeyRow): A2AApiKeyRecord {
36
+ return {
37
+ id: row.id,
38
+ key: decrypt(row.key_value),
39
+ tenantId: row.tenant_id,
40
+ projectId: row.project_id || undefined,
41
+ workspaceId: row.workspace_id || undefined,
42
+ label: row.label || undefined,
43
+ enabled: row.enabled,
44
+ createdAt: row.created_at,
45
+ updatedAt: row.updated_at,
46
+ };
47
+ }
48
+
49
+ export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
50
+ private pool: Pool;
51
+ private migrationManager: MigrationManager;
52
+ private initialized = false;
53
+ private initPromise: Promise<void> | null = null;
54
+
55
+ constructor(options: PostgreSQLA2AApiKeyStoreOptions) {
56
+ this.pool =
57
+ typeof options.poolConfig === "string"
58
+ ? new Pool({ connectionString: options.poolConfig })
59
+ : new Pool(options.poolConfig);
60
+
61
+ this.migrationManager = new MigrationManager(this.pool);
62
+ this.migrationManager.register(createA2AApiKeysTable);
63
+
64
+ if (options.autoMigrate !== false) {
65
+ this.initialize().catch((error) => {
66
+ console.error("Failed to initialize PostgreSQLA2AApiKeyStore:", error);
67
+ throw error;
68
+ });
69
+ }
70
+ }
71
+
72
+ async initialize(): Promise<void> {
73
+ if (this.initialized) return;
74
+ if (this.initPromise) return this.initPromise;
75
+ this.initPromise = (async () => {
76
+ try {
77
+ await this.migrationManager.migrate();
78
+ this.initialized = true;
79
+ } finally {
80
+ this.initPromise = null;
81
+ }
82
+ })();
83
+ return this.initPromise;
84
+ }
85
+
86
+ private async ensureInitialized(): Promise<void> {
87
+ if (!this.initialized) await this.initialize();
88
+ }
89
+
90
+ async findByKey(key: string): Promise<A2AApiKeyRecord | null> {
91
+ await this.ensureInitialized();
92
+ const all = await this.pool.query<KeyRow>(
93
+ `SELECT * FROM lattice_a2a_api_keys WHERE enabled = true`,
94
+ );
95
+ for (const row of all.rows) {
96
+ if (decrypt(row.key_value) === key) return mapRowToRecord(row);
97
+ }
98
+ return null;
99
+ }
100
+
101
+ async list(params: { tenantId?: string; limit?: number; offset?: number }): Promise<A2AApiKeyRecord[]> {
102
+ await this.ensureInitialized();
103
+ const limit = params.limit || 100;
104
+ const offset = params.offset || 0;
105
+ if (params.tenantId) {
106
+ const result = await this.pool.query<KeyRow>(
107
+ `SELECT * FROM lattice_a2a_api_keys WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
108
+ [params.tenantId, limit, offset],
109
+ );
110
+ return result.rows.map(mapRowToRecord);
111
+ }
112
+ const result = await this.pool.query<KeyRow>(
113
+ `SELECT * FROM lattice_a2a_api_keys ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
114
+ [limit, offset],
115
+ );
116
+ return result.rows.map(mapRowToRecord);
117
+ }
118
+
119
+ async create(input: CreateA2AApiKeyInput): Promise<A2AApiKeyRecord> {
120
+ await this.ensureInitialized();
121
+ const key = generateApiKey();
122
+ const result = await this.pool.query<KeyRow>(
123
+ `INSERT INTO lattice_a2a_api_keys (key_value, tenant_id, project_id, workspace_id, label)
124
+ VALUES ($1, $2, $3, $4, $5) RETURNING *`,
125
+ [encrypt(key), input.tenantId, input.projectId || null, input.workspaceId || null, input.label || null],
126
+ );
127
+ const record = mapRowToRecord(result.rows[0]);
128
+ record.key = key;
129
+ return record;
130
+ }
131
+
132
+ async disable(id: string): Promise<A2AApiKeyRecord> {
133
+ await this.ensureInitialized();
134
+ const result = await this.pool.query<KeyRow>(
135
+ `UPDATE lattice_a2a_api_keys SET enabled = false, updated_at = NOW() WHERE id = $1 RETURNING *`,
136
+ [id],
137
+ );
138
+ if (result.rows.length === 0) throw new Error(`A2A API key not found: ${id}`);
139
+ return mapRowToRecord(result.rows[0]);
140
+ }
141
+
142
+ async enable(id: string): Promise<A2AApiKeyRecord> {
143
+ await this.ensureInitialized();
144
+ const result = await this.pool.query<KeyRow>(
145
+ `UPDATE lattice_a2a_api_keys SET enabled = true, updated_at = NOW() WHERE id = $1 RETURNING *`,
146
+ [id],
147
+ );
148
+ if (result.rows.length === 0) throw new Error(`A2A API key not found: ${id}`);
149
+ return mapRowToRecord(result.rows[0]);
150
+ }
151
+
152
+ async rotate(id: string): Promise<A2AApiKeyRecord> {
153
+ await this.ensureInitialized();
154
+ const key = generateApiKey();
155
+ const result = await this.pool.query<KeyRow>(
156
+ `UPDATE lattice_a2a_api_keys SET key_value = $1, updated_at = NOW() WHERE id = $2 RETURNING *`,
157
+ [encrypt(key), id],
158
+ );
159
+ if (result.rows.length === 0) throw new Error(`A2A API key not found: ${id}`);
160
+ const record = mapRowToRecord(result.rows[0]);
161
+ record.key = key;
162
+ return record;
163
+ }
164
+
165
+ async delete(id: string): Promise<void> {
166
+ await this.ensureInitialized();
167
+ await this.pool.query(`DELETE FROM lattice_a2a_api_keys WHERE id = $1`, [id]);
168
+ }
169
+
170
+ async loadIntoMap(): Promise<Map<string, A2AApiKeyEntry>> {
171
+ await this.ensureInitialized();
172
+ const result = await this.pool.query<KeyRow>(
173
+ `SELECT * FROM lattice_a2a_api_keys WHERE enabled = true`,
174
+ );
175
+ const map = new Map<string, A2AApiKeyEntry>();
176
+ for (const row of result.rows) {
177
+ const key = decrypt(row.key_value);
178
+ map.set(key, {
179
+ key,
180
+ tenantId: row.tenant_id,
181
+ projectId: row.project_id || undefined,
182
+ workspaceId: row.workspace_id || undefined,
183
+ });
184
+ }
185
+ return map;
186
+ }
187
+ }
@@ -12,6 +12,7 @@ import {
12
12
  } from "@axiom-lattice/protocols";
13
13
  import { MigrationManager } from "../migrations/migration";
14
14
  import { createProjectsTable } from "../migrations/project_migrations";
15
+ import { addProjectConfigColumn } from "../migrations/add_project_config_column";
15
16
 
16
17
  /**
17
18
  * PostgreSQL ProjectStore options
@@ -49,6 +50,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
49
50
 
50
51
  this.migrationManager = new MigrationManager(this.pool);
51
52
  this.migrationManager.register(createProjectsTable);
53
+ this.migrationManager.register(addProjectConfigColumn);
52
54
 
53
55
  // Auto-migrate by default
54
56
  if (options.autoMigrate !== false) {
@@ -99,6 +101,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
99
101
  workspace_id: string;
100
102
  name: string;
101
103
  description: string | null;
104
+ config: unknown | null;
102
105
  created_at: Date;
103
106
  updated_at: Date;
104
107
  }): Project {
@@ -108,6 +111,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
108
111
  workspaceId: row.workspace_id,
109
112
  name: row.name,
110
113
  description: row.description || undefined,
114
+ config: row.config as Record<string, unknown> || undefined,
111
115
  createdAt: row.created_at,
112
116
  updatedAt: row.updated_at,
113
117
  };
@@ -125,11 +129,12 @@ export class PostgreSQLProjectStore implements ProjectStore {
125
129
  workspace_id: string;
126
130
  name: string;
127
131
  description: string | null;
132
+ config: unknown | null;
128
133
  created_at: Date;
129
134
  updated_at: Date;
130
135
  }>(
131
136
  `
132
- SELECT id, tenant_id, workspace_id, name, description, created_at, updated_at
137
+ SELECT id, tenant_id, workspace_id, name, description, config, created_at, updated_at
133
138
  FROM lattice_projects
134
139
  WHERE tenant_id = $1 AND workspace_id = $2
135
140
  ORDER BY created_at DESC
@@ -152,11 +157,12 @@ export class PostgreSQLProjectStore implements ProjectStore {
152
157
  workspace_id: string;
153
158
  name: string;
154
159
  description: string | null;
160
+ config: unknown | null;
155
161
  created_at: Date;
156
162
  updated_at: Date;
157
163
  }>(
158
164
  `
159
- SELECT id, tenant_id, workspace_id, name, description, created_at, updated_at
165
+ SELECT id, tenant_id, workspace_id, name, description, config, created_at, updated_at
160
166
  FROM lattice_projects
161
167
  WHERE id = $1 AND tenant_id = $2
162
168
  `,
@@ -185,15 +191,16 @@ export class PostgreSQLProjectStore implements ProjectStore {
185
191
 
186
192
  await this.pool.query(
187
193
  `
188
- INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, created_at, updated_at)
189
- VALUES ($1, $2, $3, $4, $5, $6, $7)
194
+ INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, created_at, updated_at)
195
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
190
196
  ON CONFLICT (id, tenant_id) DO UPDATE SET
191
197
  workspace_id = EXCLUDED.workspace_id,
192
198
  name = EXCLUDED.name,
193
199
  description = EXCLUDED.description,
200
+ config = EXCLUDED.config,
194
201
  updated_at = EXCLUDED.updated_at
195
202
  `,
196
- [id, tenantId, workspaceId, data.name, data.description || null, now, now]
203
+ [id, tenantId, workspaceId, data.name, data.description || null, data.config || null, now, now]
197
204
  );
198
205
 
199
206
  return {
@@ -202,6 +209,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
202
209
  workspaceId,
203
210
  name: data.name,
204
211
  description: data.description,
212
+ config: data.config,
205
213
  createdAt: now,
206
214
  updatedAt: now,
207
215
  };
@@ -209,6 +217,10 @@ export class PostgreSQLProjectStore implements ProjectStore {
209
217
 
210
218
  /**
211
219
  * Update an existing project
220
+ *
221
+ * @remarks
222
+ * - The `config` field uses **replace** semantics: if provided, it completely
223
+ * overwrites the existing config. To preserve the current config, omit this field.
212
224
  */
213
225
  async updateProject(
214
226
  tenantId: string,
@@ -238,6 +250,11 @@ export class PostgreSQLProjectStore implements ProjectStore {
238
250
  updateValues.push(updates.description || null);
239
251
  }
240
252
 
253
+ if (updates.config !== undefined) {
254
+ updateFields.push(`config = $${paramIndex++}`);
255
+ updateValues.push(updates.config || null);
256
+ }
257
+
241
258
  if (updateFields.length === 0) {
242
259
  // No fields to update
243
260
  return existing;