@axiom-lattice/local-stores 3.1.8 → 3.1.9
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +9 -0
- package/dist/index.d.mts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +177 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +176 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/LocalA2AApiKeyStore.test.ts +9 -0
- package/src/__tests__/LocalOpenAuditStore.test.ts +116 -0
- package/src/__tests__/LocalProjectRoomStores.test.ts +22 -0
- package/src/createLocalStoreConfig.ts +2 -0
- package/src/index.ts +2 -0
- package/src/stores/LocalA2AApiKeyStore.ts +41 -3
- package/src/stores/LocalOpenAuditStore.ts +110 -0
- package/src/stores/LocalProjectRoomMessageStore.ts +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/local-stores",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.9",
|
|
4
4
|
"description": "Local SQLite-based stores for Axiom Lattice framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@types/sql.js": "^1.4.11",
|
|
26
26
|
"sql.js": "^1.14.1",
|
|
27
27
|
"uuid": "^14.0.1",
|
|
28
|
-
"@axiom-lattice/core": "4.2.
|
|
29
|
-
"@axiom-lattice/protocols": "4.2.
|
|
28
|
+
"@axiom-lattice/core": "4.2.9",
|
|
29
|
+
"@axiom-lattice/protocols": "4.2.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/jest": "^29.5.14",
|
|
@@ -116,4 +116,13 @@ describe("LocalA2AApiKeyStore", () => {
|
|
|
116
116
|
expect(entry.assistantIds).toEqual(["a1"]);
|
|
117
117
|
expect("workspaceId" in entry).toBe(false);
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
it("create persists grants and findById projects them back", async () => {
|
|
121
|
+
const grants = [{ domain: "kb", items: ["search"] }];
|
|
122
|
+
const created = await store.create({
|
|
123
|
+
tenantId: "t1", projectId: "p1", grants,
|
|
124
|
+
});
|
|
125
|
+
const record = await store.findById(created.id);
|
|
126
|
+
expect(record?.grants).toEqual(grants);
|
|
127
|
+
});
|
|
119
128
|
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { initDatabase, closeDatabase, type DatabaseWrapper } from "../database";
|
|
5
|
+
import { LocalOpenAuditStore } from "../stores/LocalOpenAuditStore";
|
|
6
|
+
|
|
7
|
+
const nextDbPath = () =>
|
|
8
|
+
path.join(tmpdir(), `lt-open-audit-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
9
|
+
|
|
10
|
+
describe("LocalOpenAuditStore", () => {
|
|
11
|
+
let store: LocalOpenAuditStore;
|
|
12
|
+
let db: DatabaseWrapper;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
db = await initDatabase({ dbPath: nextDbPath() });
|
|
16
|
+
store = new LocalOpenAuditStore(db);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
closeDatabase();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("appends rows with redactable args and rounded duration", async () => {
|
|
24
|
+
await store.append({
|
|
25
|
+
tenantId: "t1",
|
|
26
|
+
credentialId: "k1",
|
|
27
|
+
credentialKind: "api_key",
|
|
28
|
+
projectId: "p1",
|
|
29
|
+
domain: "kb",
|
|
30
|
+
action: "search",
|
|
31
|
+
args: { query: "退款" },
|
|
32
|
+
status: "ok",
|
|
33
|
+
durationMs: 4.6,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const row = db
|
|
37
|
+
.prepare(`SELECT * FROM lt_open_audit`)
|
|
38
|
+
.get() as Record<string, unknown>;
|
|
39
|
+
expect(row.tenant_id).toBe("t1");
|
|
40
|
+
expect(row.domain).toBe("kb");
|
|
41
|
+
expect(row.status).toBe("ok");
|
|
42
|
+
expect(row.duration_ms).toBe(5);
|
|
43
|
+
expect(JSON.parse(row.args as string)).toEqual({ query: "退款" });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("stores null args and an error code", async () => {
|
|
47
|
+
await store.append({
|
|
48
|
+
tenantId: "t1",
|
|
49
|
+
credentialId: "c",
|
|
50
|
+
credentialKind: "browser_token",
|
|
51
|
+
projectId: "p1",
|
|
52
|
+
domain: "sandbox",
|
|
53
|
+
action: "shell_exec",
|
|
54
|
+
args: undefined,
|
|
55
|
+
status: "error",
|
|
56
|
+
errorCode: "GRANT_DENIED",
|
|
57
|
+
durationMs: 2,
|
|
58
|
+
});
|
|
59
|
+
const row = db
|
|
60
|
+
.prepare(`SELECT * FROM lt_open_audit`)
|
|
61
|
+
.get() as Record<string, unknown>;
|
|
62
|
+
expect(row.args).toBeNull();
|
|
63
|
+
expect(row.error_code).toBe("GRANT_DENIED");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("LocalOpenAuditStore.query", () => {
|
|
68
|
+
let store: LocalOpenAuditStore;
|
|
69
|
+
let db: DatabaseWrapper;
|
|
70
|
+
|
|
71
|
+
beforeEach(async () => {
|
|
72
|
+
db = await initDatabase({ dbPath: nextDbPath() });
|
|
73
|
+
store = new LocalOpenAuditStore(db);
|
|
74
|
+
for (const [domain, status] of [["kb", "ok"], ["sandbox", "error"], ["kb", "ok"]] as const) {
|
|
75
|
+
await store.append({
|
|
76
|
+
tenantId: "t1",
|
|
77
|
+
credentialId: "k1",
|
|
78
|
+
credentialKind: "api_key",
|
|
79
|
+
projectId: "p1",
|
|
80
|
+
domain,
|
|
81
|
+
action: "x",
|
|
82
|
+
args: {},
|
|
83
|
+
status,
|
|
84
|
+
durationMs: 1,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// A different tenant must not leak.
|
|
88
|
+
await store.append({
|
|
89
|
+
tenantId: "t2",
|
|
90
|
+
credentialId: "k2",
|
|
91
|
+
credentialKind: "api_key",
|
|
92
|
+
projectId: "p9",
|
|
93
|
+
domain: "kb",
|
|
94
|
+
action: "x",
|
|
95
|
+
args: {},
|
|
96
|
+
status: "ok",
|
|
97
|
+
durationMs: 1,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterEach(() => closeDatabase());
|
|
102
|
+
|
|
103
|
+
it("filters by tenant and domain", async () => {
|
|
104
|
+
const rows = await store.query({ tenantId: "t1", domain: "kb" });
|
|
105
|
+
expect(rows).toHaveLength(2);
|
|
106
|
+
expect(rows.every((r) => r.domain === "kb")).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("filters by status and paginates newest-first", async () => {
|
|
110
|
+
const errors = await store.query({ tenantId: "t1", status: "error" });
|
|
111
|
+
expect(errors).toHaveLength(1);
|
|
112
|
+
expect(errors[0].domain).toBe("sandbox");
|
|
113
|
+
const limited = await store.query({ tenantId: "t1", limit: 1, offset: 0 });
|
|
114
|
+
expect(limited).toHaveLength(1);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -392,4 +392,26 @@ describe("local project room stores", () => {
|
|
|
392
392
|
|
|
393
393
|
await expect(store.create({ id: "race-id", tenantId: "t1", workspaceId: "w1", projectId: "p1", roomId: "r1", author: { type: "system" }, content: { type: "text", text: "message" }, mentions: [], source: "system" })).rejects.toBeInstanceOf(ProjectRoomMessageIdConflictError);
|
|
394
394
|
});
|
|
395
|
+
|
|
396
|
+
it("skips individually invalid rows when listing while preserving valid rows", async () => {
|
|
397
|
+
const db = await initDatabase({ dbPath });
|
|
398
|
+
const store = new LocalProjectRoomMessageStore(db);
|
|
399
|
+
await store.create({
|
|
400
|
+
id: "m1", tenantId: "t1", workspaceId: "w1", projectId: "p1", roomId: "r1",
|
|
401
|
+
author: { type: "human", userId: "u1" }, content: { type: "text", text: "hello" },
|
|
402
|
+
mentions: [], source: "user",
|
|
403
|
+
});
|
|
404
|
+
transaction(db, () => {
|
|
405
|
+
write(db, "INSERT INTO lt_project_room_messages (id, tenant_id, workspace_id, project_id, room_id, author, content, mentions, reply_to_message_id, source, source_id, idempotency_key, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
406
|
+
["bad", "t1", "w1", "p1", "r1", '{"type":"human","userId":"u1"}', '{"text":"missing type"}', "[]", null, "user", null, null, "2026-01-01T00:00:00.000Z"]);
|
|
407
|
+
});
|
|
408
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
409
|
+
const page = await store.list({ tenantId: "t1", roomId: "r1", limit: 10 });
|
|
410
|
+
expect(page.map((message) => message.id)).toEqual(["m1"]);
|
|
411
|
+
expect(warn).toHaveBeenCalledWith(
|
|
412
|
+
"[ProjectRoomMessageStore] Skipping invalid message row",
|
|
413
|
+
expect.objectContaining({ tenantId: "t1", roomId: "r1", rowId: "bad" }),
|
|
414
|
+
);
|
|
415
|
+
warn.mockRestore();
|
|
416
|
+
});
|
|
395
417
|
});
|
|
@@ -37,6 +37,7 @@ import { LocalEvalStore } from "./stores/LocalEvalStore";
|
|
|
37
37
|
import { LocalChannelBindingStore } from "./stores/LocalChannelBindingStore";
|
|
38
38
|
import { LocalChannelInstallationStore } from "./stores/LocalChannelInstallationStore";
|
|
39
39
|
import { LocalA2AApiKeyStore } from "./stores/LocalA2AApiKeyStore";
|
|
40
|
+
import { LocalOpenAuditStore } from "./stores/LocalOpenAuditStore";
|
|
40
41
|
import { LocalThreadMessageQueueStore } from "./stores/LocalThreadMessageQueueStore";
|
|
41
42
|
import { LocalSkillStore } from "./stores/LocalSkillStore";
|
|
42
43
|
import { LocalScheduleStorage } from "./stores/LocalScheduleStorage";
|
|
@@ -88,6 +89,7 @@ export async function createLocalStoreConfig(options: LocalStoreConfigOptions =
|
|
|
88
89
|
channelBinding: new LocalChannelBindingStore(db),
|
|
89
90
|
channelInstallation: new LocalChannelInstallationStore(db),
|
|
90
91
|
a2aApiKey: new LocalA2AApiKeyStore(db),
|
|
92
|
+
openAudit: new LocalOpenAuditStore(db),
|
|
91
93
|
threadMessageQueue: new LocalThreadMessageQueueStore(db),
|
|
92
94
|
skill: new LocalSkillStore(db),
|
|
93
95
|
schedule: new LocalScheduleStorage(db),
|
package/src/index.ts
CHANGED
|
@@ -55,3 +55,5 @@ export { LocalTaskStore } from "./stores/LocalTaskStore";
|
|
|
55
55
|
export { LocalTaskWorkItemStore } from "./stores/LocalTaskWorkItemStore";
|
|
56
56
|
export { LocalCapabilityBundleStore } from "./stores/LocalCapabilityBundleStore";
|
|
57
57
|
export { InMemoryConversationStore } from "./stores/InMemoryConversationStore";
|
|
58
|
+
|
|
59
|
+
export * from "./stores/LocalOpenAuditStore";
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { DatabaseWrapper } from "../database";
|
|
6
6
|
import type {
|
|
7
|
+
UpdateA2AApiKeyInput,
|
|
7
8
|
A2AApiKeyStore,
|
|
8
9
|
A2AApiKeyRecord,
|
|
9
10
|
CreateA2AApiKeyInput,
|
|
10
11
|
A2AApiKeyEntry,
|
|
12
|
+
OpenGrant,
|
|
11
13
|
} from "@axiom-lattice/protocols";
|
|
12
14
|
import { ensureTable, nowISO, parseISO } from "../database";
|
|
13
15
|
import { encrypt, decrypt } from "@axiom-lattice/core";
|
|
@@ -20,6 +22,7 @@ CREATE TABLE IF NOT EXISTS lt_a2a_api_keys (
|
|
|
20
22
|
tenant_id TEXT NOT NULL,
|
|
21
23
|
project_id TEXT,
|
|
22
24
|
assistant_ids TEXT,
|
|
25
|
+
grants TEXT,
|
|
23
26
|
label TEXT,
|
|
24
27
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
25
28
|
created_at TEXT NOT NULL,
|
|
@@ -33,6 +36,7 @@ interface KeyRow {
|
|
|
33
36
|
tenant_id: string;
|
|
34
37
|
project_id: string;
|
|
35
38
|
assistant_ids: string | null;
|
|
39
|
+
grants: string | null;
|
|
36
40
|
label: string | null;
|
|
37
41
|
enabled: number;
|
|
38
42
|
created_at: string;
|
|
@@ -43,6 +47,16 @@ function generateApiKey(): string {
|
|
|
43
47
|
return `a2a_${randomUUID().replace(/-/g, "")}`;
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
function parseGrants(raw: string | null): OpenGrant[] | undefined {
|
|
51
|
+
if (!raw) return undefined;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
return Array.isArray(parsed) ? (parsed as OpenGrant[]) : undefined;
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
function parseAssistantIds(raw: string | null): string[] | undefined {
|
|
47
61
|
if (!raw) return undefined;
|
|
48
62
|
try {
|
|
@@ -62,6 +76,7 @@ export class LocalA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
62
76
|
ensureTable(db, DDL);
|
|
63
77
|
// Existing installs: add columns shipped after the original schema.
|
|
64
78
|
this.ensureColumn("lt_a2a_api_keys", "assistant_ids", "assistant_ids TEXT");
|
|
79
|
+
this.ensureColumn("lt_a2a_api_keys", "grants", "grants TEXT");
|
|
65
80
|
this.repairManagementIds();
|
|
66
81
|
}
|
|
67
82
|
|
|
@@ -128,9 +143,9 @@ export class LocalA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
128
143
|
const key = generateApiKey();
|
|
129
144
|
const now = nowISO();
|
|
130
145
|
this.db.prepare(
|
|
131
|
-
`INSERT INTO lt_a2a_api_keys (id, key_value, tenant_id, project_id, assistant_ids, label, created_at, updated_at)
|
|
132
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
133
|
-
).run(id, encrypt(key), input.tenantId, input.projectId, input.assistantIds ? JSON.stringify(input.assistantIds) : null, input.label || null, now, now);
|
|
146
|
+
`INSERT INTO lt_a2a_api_keys (id, key_value, tenant_id, project_id, assistant_ids, grants, label, created_at, updated_at)
|
|
147
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
148
|
+
).run(id, encrypt(key), input.tenantId, input.projectId, input.assistantIds ? JSON.stringify(input.assistantIds) : null, input.grants ? JSON.stringify(input.grants) : null, input.label || null, now, now);
|
|
134
149
|
|
|
135
150
|
const record: A2AApiKeyRecord = {
|
|
136
151
|
id,
|
|
@@ -166,6 +181,28 @@ export class LocalA2AApiKeyStore implements A2AApiKeyStore {
|
|
|
166
181
|
return mapRowToRecord(row);
|
|
167
182
|
}
|
|
168
183
|
|
|
184
|
+
|
|
185
|
+
async update(id: string, input: UpdateA2AApiKeyInput): Promise<A2AApiKeyRecord> {
|
|
186
|
+
const sets: string[] = [];
|
|
187
|
+
const values: unknown[] = [];
|
|
188
|
+
if (input.label !== undefined) { sets.push("label = ?"); values.push(input.label || null); }
|
|
189
|
+
if (input.projectId !== undefined) { sets.push("project_id = ?"); values.push(input.projectId); }
|
|
190
|
+
if (input.assistantIds !== undefined) { sets.push("assistant_ids = ?"); values.push(input.assistantIds ? JSON.stringify(input.assistantIds) : null); }
|
|
191
|
+
if (input.grants !== undefined) { sets.push("grants = ?"); values.push(input.grants ? JSON.stringify(input.grants) : null); }
|
|
192
|
+
if (sets.length === 0) {
|
|
193
|
+
const row = this.db.prepare("SELECT * FROM lt_a2a_api_keys WHERE id = ?").get(id) as unknown as KeyRow | undefined;
|
|
194
|
+
if (!row) throw new Error(`A2A API key not found: ${id}`);
|
|
195
|
+
return mapRowToRecord(row);
|
|
196
|
+
}
|
|
197
|
+
sets.push("updated_at = ?");
|
|
198
|
+
values.push(nowISO());
|
|
199
|
+
values.push(id);
|
|
200
|
+
this.db.prepare(`UPDATE lt_a2a_api_keys SET ${sets.join(", ")} WHERE id = ?`).run(...values);
|
|
201
|
+
const row = this.db.prepare("SELECT * FROM lt_a2a_api_keys WHERE id = ?").get(id) as unknown as KeyRow | undefined;
|
|
202
|
+
if (!row) throw new Error(`A2A API key not found: ${id}`);
|
|
203
|
+
return mapRowToRecord(row);
|
|
204
|
+
}
|
|
205
|
+
|
|
169
206
|
async rotate(id: string): Promise<A2AApiKeyRecord> {
|
|
170
207
|
const key = generateApiKey();
|
|
171
208
|
const now = nowISO();
|
|
@@ -212,6 +249,7 @@ function mapRowToRecord(row: KeyRow): A2AApiKeyRecord {
|
|
|
212
249
|
tenantId: row.tenant_id,
|
|
213
250
|
projectId: row.project_id,
|
|
214
251
|
assistantIds: parseAssistantIds(row.assistant_ids),
|
|
252
|
+
grants: parseGrants(row.grants),
|
|
215
253
|
label: row.label || undefined,
|
|
216
254
|
enabled: row.enabled === 1,
|
|
217
255
|
createdAt: parseISO(row.created_at),
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local SQLite implementation of the Open audit store (append-only).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DatabaseWrapper, ensureTable, nowISO, parseISO } from "../database";
|
|
6
|
+
import type {
|
|
7
|
+
OpenAuditStore,
|
|
8
|
+
OpenAuditAppendInput,
|
|
9
|
+
OpenAuditQuery,
|
|
10
|
+
OpenAuditRecord,
|
|
11
|
+
} from "@axiom-lattice/protocols";
|
|
12
|
+
import { randomUUID } from "crypto";
|
|
13
|
+
|
|
14
|
+
const DDL = `
|
|
15
|
+
CREATE TABLE IF NOT EXISTS lt_open_audit (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
tenant_id TEXT NOT NULL,
|
|
18
|
+
credential_id TEXT NOT NULL,
|
|
19
|
+
credential_kind TEXT NOT NULL,
|
|
20
|
+
project_id TEXT NOT NULL,
|
|
21
|
+
domain TEXT NOT NULL,
|
|
22
|
+
action TEXT NOT NULL,
|
|
23
|
+
args TEXT,
|
|
24
|
+
status TEXT NOT NULL,
|
|
25
|
+
error_code TEXT,
|
|
26
|
+
duration_ms INTEGER NOT NULL,
|
|
27
|
+
created_at TEXT NOT NULL
|
|
28
|
+
);
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
/** SQLite-backed append-only audit store for the Open API surface. */
|
|
32
|
+
export class LocalOpenAuditStore implements OpenAuditStore {
|
|
33
|
+
private db: DatabaseWrapper;
|
|
34
|
+
|
|
35
|
+
constructor(db: DatabaseWrapper) {
|
|
36
|
+
this.db = db;
|
|
37
|
+
ensureTable(db, DDL);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async query(params: OpenAuditQuery): Promise<OpenAuditRecord[]> {
|
|
41
|
+
const clauses = ["tenant_id = ?"];
|
|
42
|
+
const values: unknown[] = [params.tenantId];
|
|
43
|
+
if (params.credentialId) {
|
|
44
|
+
clauses.push("credential_id = ?");
|
|
45
|
+
values.push(params.credentialId);
|
|
46
|
+
}
|
|
47
|
+
if (params.domain) {
|
|
48
|
+
clauses.push("domain = ?");
|
|
49
|
+
values.push(params.domain);
|
|
50
|
+
}
|
|
51
|
+
if (params.status) {
|
|
52
|
+
clauses.push("status = ?");
|
|
53
|
+
values.push(params.status);
|
|
54
|
+
}
|
|
55
|
+
if (params.from) {
|
|
56
|
+
clauses.push("created_at >= ?");
|
|
57
|
+
values.push(params.from);
|
|
58
|
+
}
|
|
59
|
+
if (params.to) {
|
|
60
|
+
clauses.push("created_at < ?");
|
|
61
|
+
values.push(params.to);
|
|
62
|
+
}
|
|
63
|
+
const limit = params.limit ?? 100;
|
|
64
|
+
const offset = params.offset ?? 0;
|
|
65
|
+
values.push(limit, offset);
|
|
66
|
+
const rows = this.db
|
|
67
|
+
.prepare(
|
|
68
|
+
`SELECT * FROM lt_open_audit WHERE ${clauses.join(" AND ")}
|
|
69
|
+
ORDER BY created_at DESC LIMIT ? OFFSET ?`,
|
|
70
|
+
)
|
|
71
|
+
.all(...values) as unknown as Array<Record<string, unknown>>;
|
|
72
|
+
return rows.map((row) => ({
|
|
73
|
+
id: row.id as string,
|
|
74
|
+
tenantId: row.tenant_id as string,
|
|
75
|
+
credentialId: row.credential_id as string,
|
|
76
|
+
credentialKind: row.credential_kind as OpenAuditRecord["credentialKind"],
|
|
77
|
+
projectId: row.project_id as string,
|
|
78
|
+
domain: row.domain as string,
|
|
79
|
+
action: row.action as string,
|
|
80
|
+
args: row.args ? JSON.parse(row.args as string) : undefined,
|
|
81
|
+
status: row.status as "ok" | "error",
|
|
82
|
+
errorCode: (row.error_code as string) ?? undefined,
|
|
83
|
+
durationMs: row.duration_ms as number,
|
|
84
|
+
createdAt: parseISO(row.created_at as string),
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async append(record: OpenAuditAppendInput): Promise<void> {
|
|
89
|
+
this.db
|
|
90
|
+
.prepare(
|
|
91
|
+
`INSERT INTO lt_open_audit
|
|
92
|
+
(id, tenant_id, credential_id, credential_kind, project_id, domain, action, args, status, error_code, duration_ms, created_at)
|
|
93
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
94
|
+
)
|
|
95
|
+
.run(
|
|
96
|
+
randomUUID(),
|
|
97
|
+
record.tenantId,
|
|
98
|
+
record.credentialId,
|
|
99
|
+
record.credentialKind,
|
|
100
|
+
record.projectId,
|
|
101
|
+
record.domain,
|
|
102
|
+
record.action,
|
|
103
|
+
record.args === undefined ? null : JSON.stringify(record.args),
|
|
104
|
+
record.status,
|
|
105
|
+
record.errorCode ?? null,
|
|
106
|
+
Math.max(0, Math.round(record.durationMs)),
|
|
107
|
+
nowISO(),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -26,6 +26,26 @@ function mapRow(row: Row): ProjectRoomMessage {
|
|
|
26
26
|
...(row.source_id === null ? {} : { sourceId: row.source_id }), ...(row.idempotency_key === null ? {} : { idempotencyKey: row.idempotency_key }), createdAt };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function mapRowsTolerant(
|
|
30
|
+
rows: Row[],
|
|
31
|
+
scope: { tenantId: string; roomId: string },
|
|
32
|
+
): ProjectRoomMessage[] {
|
|
33
|
+
const messages: ProjectRoomMessage[] = [];
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
try {
|
|
36
|
+
messages.push(mapRow(row));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn("[ProjectRoomMessageStore] Skipping invalid message row", {
|
|
39
|
+
tenantId: scope.tenantId,
|
|
40
|
+
roomId: scope.roomId,
|
|
41
|
+
rowId: typeof row.id === "string" ? row.id : undefined,
|
|
42
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return messages;
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
/** Persists validated project room messages in local SQLite. */
|
|
30
50
|
export class LocalProjectRoomMessageStore implements ProjectRoomMessageStore {
|
|
31
51
|
constructor(private readonly db: DatabaseWrapper) { ensureProjectRoomSchema(db); }
|
|
@@ -37,7 +57,7 @@ export class LocalProjectRoomMessageStore implements ProjectRoomMessageStore {
|
|
|
37
57
|
const rows = input.before
|
|
38
58
|
? this.db.prepare(`SELECT ${columns} FROM lt_project_room_messages WHERE tenant_id = ? AND room_id = ? AND (created_at < ? OR (created_at = ? AND id < ?)) ORDER BY created_at DESC, id DESC LIMIT ?`).all(input.tenantId, input.roomId, input.before.createdAt.toISOString(), input.before.createdAt.toISOString(), input.before.id, limit)
|
|
39
59
|
: this.db.prepare(`SELECT ${columns} FROM lt_project_room_messages WHERE tenant_id = ? AND room_id = ? ORDER BY created_at DESC, id DESC LIMIT ?`).all(input.tenantId, input.roomId, limit);
|
|
40
|
-
return (rows as Row[]
|
|
60
|
+
return mapRowsTolerant(rows as Row[], { tenantId: input.tenantId, roomId: input.roomId });
|
|
41
61
|
}
|
|
42
62
|
async findById(tenantId: string, id: string): Promise<ProjectRoomMessage | null> { const row = this.byId(tenantId, id); return row ? mapRow(row) : null; }
|
|
43
63
|
async countAfter(input: {
|