wendkeep 0.86.0 → 0.88.0
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/.githooks/commit-msg +16 -0
- package/.githooks/prepare-commit-msg +16 -0
- package/CHANGELOG.md +32 -0
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/docs/en/commands/commit.md +159 -0
- package/docs/en/commands/observer-security.md +154 -0
- package/docs/en/commands/observer.md +30 -12
- package/docs/pt-BR/commands/commit.md +159 -0
- package/docs/pt-BR/commands/observer-security.md +154 -0
- package/docs/pt-BR/commands/observer.md +30 -12
- package/hooks/observer-publish.mjs +3 -1
- package/package.json +6 -2
- package/packages/cli/src/index.mjs +11 -1
- package/packages/commit/package.json +6 -0
- package/packages/commit/src/cli.mjs +89 -0
- package/packages/commit/src/commit-input.mjs +181 -0
- package/packages/commit/src/commit-message.mjs +51 -0
- package/packages/commit/src/commit-policy.mjs +144 -0
- package/packages/commit/src/git-runtime.mjs +428 -0
- package/packages/commit/src/index.mjs +28 -0
- package/packages/commit/src/proof-validation.mjs +443 -0
- package/packages/mcp/src/executor.mjs +35 -2
- package/packages/observer/package.json +16 -0
- package/packages/observer/src/audit.mjs +1 -0
- package/packages/observer/src/authz.mjs +38 -0
- package/packages/observer/src/encryption.mjs +75 -0
- package/packages/observer/src/index.mjs +7 -0
- package/packages/observer/src/policy.mjs +305 -0
- package/packages/observer/src/purge.mjs +100 -0
- package/packages/observer/src/redaction.mjs +54 -0
- package/packages/observer/src/retention.mjs +39 -0
- package/packages/observer/src/token-registry.mjs +122 -0
- package/schema/commit-message-v1.schema.json +75 -0
- package/schema/observer/006-observer-security.sql +64 -0
- package/schema/observer-policy-v1.schema.json +63 -0
- package/schema/sync-event-v1.schema.json +10 -0
- package/scripts/validate-commit-range.mjs +244 -0
- package/src/doctor.mjs +7 -0
- package/src/git-commit-hooks.mjs +112 -0
- package/src/init.mjs +13 -0
- package/src/observer-auth.mjs +8 -0
- package/src/observer-privacy.mjs +7 -3
- package/src/observer-publish.mjs +31 -0
- package/src/observer-server.mjs +179 -20
- package/src/observer-sql-migrate.mjs +5 -2
- package/src/observer-sql-publish.mjs +114 -39
- package/src/observer-sql-store.mjs +299 -45
- package/src/observer-transcript-store.mjs +23 -8
- package/src/observer.mjs +145 -12
- package/src/skills-seed.mjs +79 -0
- package/src/sync-protocol.mjs +20 -0
- package/web/observer/app.mjs +107 -31
- package/web/observer/index.html +7 -0
- package/web/observer/styles.css +5 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { join, basename, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { decodeTranscript, encodeTranscript } from './observer-transcript-store.mjs';
|
|
7
|
+
import { decryptObserverValue, encryptObserverValue } from '../packages/observer/src/encryption.mjs';
|
|
8
|
+
import { protectObserverEvent, readObserverPolicy } from '../packages/observer/src/policy.mjs';
|
|
7
9
|
import {
|
|
8
10
|
chunkMarkdownDocument, recallEvidence, recallTerms,
|
|
9
11
|
} from '../packages/vault/src/evidence-recall.mjs';
|
|
10
12
|
|
|
11
13
|
export const OBSERVER_SQL_FILE = 'observer.sqlite';
|
|
12
|
-
export const OBSERVER_SQL_SCHEMA_VERSION =
|
|
14
|
+
export const OBSERVER_SQL_SCHEMA_VERSION = 6;
|
|
13
15
|
export const OBSERVER_EVENT_SCHEMA_VERSION = 1;
|
|
14
16
|
|
|
15
17
|
const SCHEMA_DIR = fileURLToPath(new URL('../schema/observer/', import.meta.url));
|
|
@@ -23,6 +25,29 @@ const OBSERVER_SQL_MINIMUM_NODE = '22.13.0';
|
|
|
23
25
|
const require = createRequire(import.meta.url);
|
|
24
26
|
let DatabaseSync;
|
|
25
27
|
const DATABASE_PATHS = new WeakMap();
|
|
28
|
+
const DATABASE_SECURITY = new WeakMap();
|
|
29
|
+
|
|
30
|
+
export function configureObserverDatabaseSecurity(db, { policy = null, encryption = null, enforcePolicy = false } = {}) {
|
|
31
|
+
if (!db) throw new Error('db é obrigatório.');
|
|
32
|
+
DATABASE_SECURITY.set(db, { policy, encryption, enforcePolicy: Boolean(enforcePolicy) });
|
|
33
|
+
return { policy: Boolean(policy) || Boolean(enforcePolicy), encryption: Boolean(encryption) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function databaseSecurity(db) {
|
|
37
|
+
return DATABASE_SECURITY.get(db) || { policy: null, encryption: null, enforcePolicy: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function encryptedJson(encryption, value, aad) {
|
|
41
|
+
return encryption ? json(encryptObserverValue(encryption, json(value), { aad })) : '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function encryptedText(encryption, value, aad) {
|
|
45
|
+
return encryption ? json(encryptObserverValue(encryption, text(value), { aad })) : '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function decryptedJson(encryption, envelope, fallback, aad) {
|
|
49
|
+
return envelope ? parseJson(decryptObserverValue(encryption, parseJson(envelope), { aad }), fallback) : fallback;
|
|
50
|
+
}
|
|
26
51
|
|
|
27
52
|
export function observerSqlRuntimeSupport(version = process.versions.node) {
|
|
28
53
|
const current = String(version || '0.0.0');
|
|
@@ -109,7 +134,50 @@ export function openObserverDatabase(dataDir) {
|
|
|
109
134
|
return db;
|
|
110
135
|
}
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
function encryptedStructuralBackup(db, databasePath, version, encryption) {
|
|
138
|
+
if (!encryption) throw Object.assign(new Error('Migração estrutural exige chave para backup protegido.'), { code: 'observer_encryption_required' });
|
|
139
|
+
const prefix = `${databasePath}.pre-${String(version).padStart(3, '0')}-${Date.now()}.bak`;
|
|
140
|
+
const temporaryPath = `${prefix}.tmp`;
|
|
141
|
+
const encryptedPath = `${prefix}.enc`;
|
|
142
|
+
const manifestPath = `${encryptedPath}.manifest.json`;
|
|
143
|
+
const aad = `observer-backup:${basename(databasePath)}:${version}`;
|
|
144
|
+
try {
|
|
145
|
+
db.exec(`VACUUM INTO '${temporaryPath.replaceAll("'", "''")}'`);
|
|
146
|
+
try { chmodSync(temporaryPath, 0o600); } catch { /* best effort on Windows */ }
|
|
147
|
+
const plaintext = readFileSync(temporaryPath);
|
|
148
|
+
const envelope = encryptObserverValue(encryption, plaintext.toString('base64'), { aad });
|
|
149
|
+
writeFileSync(encryptedPath, `${JSON.stringify(envelope)}\n`, { mode: 0o600 });
|
|
150
|
+
writeFileSync(manifestPath, `${JSON.stringify({
|
|
151
|
+
schema_version: 1,
|
|
152
|
+
encrypted_file: basename(encryptedPath),
|
|
153
|
+
algorithm: envelope.algorithm,
|
|
154
|
+
key_id: envelope.key_id,
|
|
155
|
+
aad,
|
|
156
|
+
plaintext_sha256: createHash('sha256').update(plaintext).digest('hex'),
|
|
157
|
+
source_database: basename(databasePath),
|
|
158
|
+
target_schema_version: version,
|
|
159
|
+
created_at: now(),
|
|
160
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
161
|
+
try { chmodSync(encryptedPath, 0o600); chmodSync(manifestPath, 0o600); } catch { /* best effort on Windows */ }
|
|
162
|
+
return encryptedPath;
|
|
163
|
+
} finally {
|
|
164
|
+
rmSync(temporaryPath, { force: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function restoreObserverEncryptedBackup({ backupPath, destinationPath, encryption } = {}) {
|
|
169
|
+
if (!backupPath || !destinationPath || !encryption) throw new Error('backupPath, destinationPath e encryption são obrigatórios.');
|
|
170
|
+
const manifest = parseJson(readFileSync(`${backupPath}.manifest.json`, 'utf8'), null);
|
|
171
|
+
if (!manifest || manifest.encrypted_file !== basename(backupPath)) throw Object.assign(new Error('Manifest do backup inválido.'), { code: 'observer_backup_manifest_invalid' });
|
|
172
|
+
const envelope = parseJson(readFileSync(backupPath, 'utf8'), null);
|
|
173
|
+
const plaintext = Buffer.from(decryptObserverValue(encryption, envelope, { aad: manifest.aad }), 'base64');
|
|
174
|
+
if (createHash('sha256').update(plaintext).digest('hex') !== manifest.plaintext_sha256) throw Object.assign(new Error('Integridade do backup inválida.'), { code: 'observer_backup_integrity_invalid' });
|
|
175
|
+
writeFileSync(destinationPath, plaintext, { mode: 0o600 });
|
|
176
|
+
try { chmodSync(destinationPath, 0o600); } catch { /* best effort on Windows */ }
|
|
177
|
+
return { restored: true, destination_path: destinationPath, sha256: manifest.plaintext_sha256 };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function migrateObserverDatabase(db, { backupEncryption = null, requireEncryptedBackup = false } = {}) {
|
|
113
181
|
if (!db) throw new Error('db é obrigatório.');
|
|
114
182
|
db.exec('CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL)');
|
|
115
183
|
const migrationColumns = db.prepare('PRAGMA table_info(schema_migrations)').all().map((row) => row.name);
|
|
@@ -136,9 +204,13 @@ export function migrateObserverDatabase(db) {
|
|
|
136
204
|
if (/^\s*--\s*wendkeep:structural\b/m.test(sql) && applied.size > 0) {
|
|
137
205
|
const databasePath = DATABASE_PATHS.get(db);
|
|
138
206
|
if (!databasePath) throw new Error('Caminho do Observer desconhecido para backup estrutural.');
|
|
139
|
-
const backupPath =
|
|
140
|
-
|
|
141
|
-
|
|
207
|
+
const backupPath = requireEncryptedBackup
|
|
208
|
+
? encryptedStructuralBackup(db, databasePath, version, backupEncryption)
|
|
209
|
+
: `${databasePath}.pre-${String(version).padStart(3, '0')}-${Date.now()}.bak`;
|
|
210
|
+
if (!requireEncryptedBackup) {
|
|
211
|
+
const escaped = backupPath.replaceAll("'", "''");
|
|
212
|
+
db.exec(`VACUUM INTO '${escaped}'`);
|
|
213
|
+
}
|
|
142
214
|
backups.push(backupPath);
|
|
143
215
|
}
|
|
144
216
|
db.exec('BEGIN IMMEDIATE');
|
|
@@ -229,6 +301,7 @@ export function rebuildSqlEvidenceIndex(db, { missingOnly = false } = {}) {
|
|
|
229
301
|
const documents = db.prepare(`SELECT d.project_id, d.logical_path, d.content, d.metadata_json, d.entity_type, d.captured_at
|
|
230
302
|
FROM documents d
|
|
231
303
|
WHERE d.deleted_at IS NULL
|
|
304
|
+
AND COALESCE(d.content_envelope, '') = ''
|
|
232
305
|
AND (? = 0 OR NOT EXISTS (
|
|
233
306
|
SELECT 1 FROM document_chunks c
|
|
234
307
|
WHERE c.project_id = d.project_id AND c.logical_path = d.logical_path
|
|
@@ -252,17 +325,98 @@ export function rebuildSqlEvidenceIndex(db, { missingOnly = false } = {}) {
|
|
|
252
325
|
return { documents: documents.length, chunks, fts };
|
|
253
326
|
}
|
|
254
327
|
|
|
255
|
-
export function
|
|
328
|
+
export function bootstrapObserverDatabase(dataDir, { security = {} } = {}) {
|
|
256
329
|
const db = openObserverDatabase(dataDir);
|
|
257
330
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
331
|
+
configureObserverDatabaseSecurity(db, security);
|
|
332
|
+
const databaseMigration = migrateObserverDatabase(db, {
|
|
333
|
+
backupEncryption: security?.encryption || null,
|
|
334
|
+
requireEncryptedBackup: Boolean(security?.encryption?.required),
|
|
335
|
+
});
|
|
336
|
+
const protectedDataMigration = security.encryption
|
|
337
|
+
? migrateObserverProtectedData(db, { encryption: security.encryption })
|
|
338
|
+
: { protected_rows: 0, projects: 0 };
|
|
339
|
+
return { db, databaseMigration, protectedDataMigration };
|
|
260
340
|
} catch (error) {
|
|
261
341
|
db.close();
|
|
262
342
|
throw error;
|
|
263
343
|
}
|
|
264
344
|
}
|
|
265
345
|
|
|
346
|
+
export function ensureObserverDatabase(dataDir, { security = null } = {}) {
|
|
347
|
+
return bootstrapObserverDatabase(dataDir, { security: security || {} }).db;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function migrateObserverProtectedData(db, { projectId = '', encryption } = {}) {
|
|
351
|
+
if (!encryption) return { protected_rows: 0, projects: 0 };
|
|
352
|
+
const projects = projectId
|
|
353
|
+
? [requireProject(db, projectId)]
|
|
354
|
+
: db.prepare('SELECT project_id FROM projects ORDER BY project_id').all();
|
|
355
|
+
let protectedRows = 0;
|
|
356
|
+
for (const project of projects) {
|
|
357
|
+
const id = project.project_id;
|
|
358
|
+
db.exec('BEGIN IMMEDIATE');
|
|
359
|
+
try {
|
|
360
|
+
const documents = db.prepare("SELECT logical_path, content, metadata_json, content_envelope, metadata_envelope FROM documents WHERE project_id = ? AND (content <> '' OR metadata_json <> '{}' OR content_envelope = '' OR metadata_envelope = '')").all(id);
|
|
361
|
+
for (const row of documents) {
|
|
362
|
+
const contentEnvelope = row.content_envelope || encryptedText(encryption, row.content, `${id}:document:${row.logical_path}`);
|
|
363
|
+
const metadataEnvelope = row.metadata_envelope || encryptedJson(encryption, parseJson(row.metadata_json), `${id}:document:${row.logical_path}:metadata`);
|
|
364
|
+
db.prepare("UPDATE documents SET content = '', content_envelope = ?, metadata_json = '{}', metadata_envelope = ? WHERE project_id = ? AND logical_path = ?")
|
|
365
|
+
.run(contentEnvelope, metadataEnvelope, id, row.logical_path);
|
|
366
|
+
db.prepare('DELETE FROM document_chunks WHERE project_id = ? AND logical_path = ?').run(id, row.logical_path);
|
|
367
|
+
if (observerFts5Support(db).supported) db.prepare('DELETE FROM evidence_chunks_fts WHERE project_id = ? AND logical_path = ?').run(id, row.logical_path);
|
|
368
|
+
protectedRows += 1;
|
|
369
|
+
}
|
|
370
|
+
const calls = db.prepare("SELECT call_id, prompt_text, response_text, metadata_json, prompt_envelope, response_envelope, metadata_envelope FROM llm_calls WHERE project_id = ? AND (prompt_text <> '' OR response_text <> '' OR metadata_json <> '{}')").all(id);
|
|
371
|
+
for (const row of calls) {
|
|
372
|
+
db.prepare("UPDATE llm_calls SET prompt_text = '', response_text = '', metadata_json = '{}', prompt_envelope = ?, response_envelope = ?, metadata_envelope = ? WHERE project_id = ? AND call_id = ?").run(
|
|
373
|
+
row.prompt_envelope || encryptedText(encryption, row.prompt_text, `${id}:call:${row.call_id}:prompt`),
|
|
374
|
+
row.response_envelope || encryptedText(encryption, row.response_text, `${id}:call:${row.call_id}:response`),
|
|
375
|
+
row.metadata_envelope || encryptedJson(encryption, parseJson(row.metadata_json), `${id}:call:${row.call_id}:metadata`), id, row.call_id,
|
|
376
|
+
);
|
|
377
|
+
protectedRows += 1;
|
|
378
|
+
}
|
|
379
|
+
const transcripts = db.prepare("SELECT * FROM transcripts WHERE project_id = ? AND codec <> 'aes-256-gcm+gzip'").all(id);
|
|
380
|
+
for (const row of transcripts) {
|
|
381
|
+
const decoded = decodeTranscript(row);
|
|
382
|
+
const encoded = encodeTranscript(decoded.content, { encryption, aad: `${id}:transcript:${row.transcript_id}` });
|
|
383
|
+
db.prepare("UPDATE transcripts SET codec = ?, content_gzip = ?, compressed_bytes = ?, metadata_json = '{}', metadata_envelope = ? WHERE project_id = ? AND transcript_id = ?").run(
|
|
384
|
+
encoded.codec, encoded.content_gzip, encoded.compressed_bytes,
|
|
385
|
+
encryptedJson(encryption, parseJson(row.metadata_json), `${id}:transcript:${row.transcript_id}:metadata`), id, row.transcript_id,
|
|
386
|
+
);
|
|
387
|
+
protectedRows += 1;
|
|
388
|
+
}
|
|
389
|
+
const snapshots = db.prepare("SELECT project_id, snapshot_json, snapshot_envelope FROM project_snapshots WHERE project_id = ? AND snapshot_json <> '{}'").all(id);
|
|
390
|
+
for (const row of snapshots) {
|
|
391
|
+
db.prepare("UPDATE project_snapshots SET snapshot_json = '{}', snapshot_envelope = ? WHERE project_id = ?").run(
|
|
392
|
+
row.snapshot_envelope || encryptedJson(encryption, parseJson(row.snapshot_json), `${id}:snapshot`), id,
|
|
393
|
+
);
|
|
394
|
+
protectedRows += 1;
|
|
395
|
+
}
|
|
396
|
+
for (const table of ['ingest_events', 'memory_events']) {
|
|
397
|
+
const rows = db.prepare(`SELECT event_id, payload_json FROM ${table} WHERE project_id = ?`).all(id);
|
|
398
|
+
for (const row of rows) {
|
|
399
|
+
const source = parseJson(row.payload_json);
|
|
400
|
+
const protectedPayload = { ...source };
|
|
401
|
+
for (const field of ['content', 'prompt', 'promptText', 'prompt_text', 'response', 'responseText', 'response_text']) {
|
|
402
|
+
if (Object.hasOwn(protectedPayload, field)) protectedPayload[field] = '[PROTECTED]';
|
|
403
|
+
}
|
|
404
|
+
if (Object.hasOwn(protectedPayload, 'metadata')) protectedPayload.metadata = '[PROTECTED]';
|
|
405
|
+
db.prepare(`UPDATE ${table} SET payload_json = ? WHERE event_id = ?`).run(json(protectedPayload), row.event_id);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
db.prepare(`INSERT INTO observer_security_backfill(project_id, status, protected_rows, updated_at)
|
|
409
|
+
VALUES (?, 'complete', ?, ?) ON CONFLICT(project_id) DO UPDATE SET status = 'complete', protected_rows = excluded.protected_rows, updated_at = excluded.updated_at`)
|
|
410
|
+
.run(id, protectedRows, now());
|
|
411
|
+
db.exec('COMMIT');
|
|
412
|
+
} catch (error) {
|
|
413
|
+
db.exec('ROLLBACK');
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return { protected_rows: protectedRows, projects: projects.length };
|
|
418
|
+
}
|
|
419
|
+
|
|
266
420
|
export function registerSqlProject(db, { projectId, projectName = projectId, wendkeepVersion = '', registeredAt = now() } = {}) {
|
|
267
421
|
if (!validProjectId(projectId)) return { registered: false, errors: ['project_id inválido.'] };
|
|
268
422
|
const timestamp = now();
|
|
@@ -348,6 +502,7 @@ function ensureAgent(db, projectId, payload) {
|
|
|
348
502
|
function applyDocument(db, event) {
|
|
349
503
|
const p = event.payload;
|
|
350
504
|
const content = text(p.content);
|
|
505
|
+
const { encryption } = databaseSecurity(db);
|
|
351
506
|
const logicalPath = text(p.logical_path || p.logicalPath);
|
|
352
507
|
if (!logicalPath || logicalPath.includes('..') || /^[A-Za-z]:[\\/]/.test(logicalPath)) throw new Error('logical_path inválido.');
|
|
353
508
|
const current = db.prepare('SELECT revision FROM documents WHERE project_id = ? AND logical_path = ?').get(event.project_id, logicalPath);
|
|
@@ -371,15 +526,24 @@ function applyDocument(db, event) {
|
|
|
371
526
|
}
|
|
372
527
|
const title = text(p.title || basename(logicalPath).replace(/\.md$/i, ''));
|
|
373
528
|
const documentId = text(p.document_id || p.documentId) || `${event.project_id}:${logicalPath}`;
|
|
529
|
+
const contentEnvelope = encryption
|
|
530
|
+
? encryptObserverValue(encryption, content, { aad: `${event.project_id}:document:${logicalPath}` })
|
|
531
|
+
: null;
|
|
532
|
+
const metadataEnvelope = encryption
|
|
533
|
+
? encryptedJson(encryption, p.metadata || {}, `${event.project_id}:document:${logicalPath}:metadata`)
|
|
534
|
+
: '';
|
|
535
|
+
const storedContent = contentEnvelope ? '' : content;
|
|
374
536
|
db.prepare(`
|
|
375
|
-
INSERT INTO documents(document_id, project_id, logical_path, entity_type, title, content, metadata_json, content_hash, revision, source_session_id, source_turn_id, captured_at, deleted_at)
|
|
376
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
537
|
+
INSERT INTO documents(document_id, project_id, logical_path, entity_type, title, content, content_envelope, metadata_json, metadata_envelope, content_hash, revision, source_session_id, source_turn_id, captured_at, deleted_at)
|
|
538
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
377
539
|
ON CONFLICT(project_id, logical_path) DO UPDATE SET
|
|
378
540
|
document_id = excluded.document_id,
|
|
379
541
|
entity_type = excluded.entity_type,
|
|
380
542
|
title = excluded.title,
|
|
381
543
|
content = excluded.content,
|
|
544
|
+
content_envelope = excluded.content_envelope,
|
|
382
545
|
metadata_json = excluded.metadata_json,
|
|
546
|
+
metadata_envelope = excluded.metadata_envelope,
|
|
383
547
|
content_hash = excluded.content_hash,
|
|
384
548
|
revision = excluded.revision,
|
|
385
549
|
source_session_id = excluded.source_session_id,
|
|
@@ -387,22 +551,27 @@ function applyDocument(db, event) {
|
|
|
387
551
|
captured_at = excluded.captured_at,
|
|
388
552
|
deleted_at = NULL
|
|
389
553
|
`).run(
|
|
390
|
-
documentId, event.project_id, logicalPath, text(p.entity_type || p.entityType, 'memory'), title,
|
|
391
|
-
revision, text(p.source_session_id || p.sourceSessionId), text(p.source_turn_id || p.sourceTurnId), text(p.captured_at || event.occurred_at),
|
|
554
|
+
documentId, event.project_id, logicalPath, text(p.entity_type || p.entityType, 'memory'), title, storedContent, contentEnvelope ? json(contentEnvelope) : '', encryption ? '{}' : json(p.metadata), metadataEnvelope, contentHash,
|
|
555
|
+
revision, text(p.source_session_id || p.sourceSessionId), text(p.source_turn_id || p.sourceTurnId), text(p.captured_at || p.capturedAt || event.occurred_at),
|
|
392
556
|
);
|
|
393
557
|
db.prepare(`
|
|
394
558
|
INSERT INTO memory_events(event_id, project_id, entity_type, logical_path, operation, revision, content_hash, source_session_id, source_turn_id, occurred_at, payload_json)
|
|
395
559
|
VALUES (?, ?, ?, ?, 'upsert', ?, ?, ?, ?, ?, ?)
|
|
396
560
|
`).run(event.event_id, event.project_id, text(p.entity_type || p.entityType, 'memory'), logicalPath, revision, contentHash,
|
|
397
|
-
text(p.source_session_id || p.sourceSessionId), text(p.source_turn_id || p.sourceTurnId), event.occurred_at,
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
logicalPath
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
561
|
+
text(p.source_session_id || p.sourceSessionId), text(p.source_turn_id || p.sourceTurnId), event.occurred_at,
|
|
562
|
+
json(ledgerPayload(event, encryption)));
|
|
563
|
+
if (contentEnvelope) {
|
|
564
|
+
db.prepare('DELETE FROM document_chunks WHERE project_id = ? AND logical_path = ?').run(event.project_id, logicalPath);
|
|
565
|
+
} else {
|
|
566
|
+
writeSqlDocumentChunks(db, {
|
|
567
|
+
projectId: event.project_id,
|
|
568
|
+
logicalPath,
|
|
569
|
+
content,
|
|
570
|
+
metadata: p.metadata,
|
|
571
|
+
entityType: text(p.entity_type || p.entityType, 'memory'),
|
|
572
|
+
capturedAt: text(p.captured_at || p.capturedAt || event.occurred_at),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
406
575
|
return { stale: false };
|
|
407
576
|
}
|
|
408
577
|
|
|
@@ -461,13 +630,26 @@ function applyCall(db, event) {
|
|
|
461
630
|
throw error;
|
|
462
631
|
}
|
|
463
632
|
const [input, cacheWrite, cacheRead, output, reasoning, total] = tokenFields(p.tokens);
|
|
633
|
+
const { encryption } = databaseSecurity(db);
|
|
634
|
+
const prompt = text(p.prompt_text || p.promptText || p.prompt);
|
|
635
|
+
const response = text(p.response_text || p.responseText || p.response);
|
|
636
|
+
const promptEnvelope = encryption
|
|
637
|
+
? encryptObserverValue(encryption, prompt, { aad: `${event.project_id}:call:${callId}:prompt` })
|
|
638
|
+
: null;
|
|
639
|
+
const responseEnvelope = encryption
|
|
640
|
+
? encryptObserverValue(encryption, response, { aad: `${event.project_id}:call:${callId}:response` })
|
|
641
|
+
: null;
|
|
642
|
+
const metadataEnvelope = encryption
|
|
643
|
+
? encryptedJson(encryption, p.metadata || {}, `${event.project_id}:call:${callId}:metadata`)
|
|
644
|
+
: '';
|
|
464
645
|
db.prepare(`
|
|
465
|
-
INSERT INTO llm_calls(call_pk, call_id, project_id, session_id, agent_id, role, provider, model_provider, model, effort, sequence, occurred_at, tokens_input, tokens_cache_write, tokens_cache_read, tokens_output, tokens_reasoning, tokens_total, cost_usd, cost_status, transcript_id, prompt_text, response_text, status, metadata_json)
|
|
466
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
646
|
+
INSERT INTO llm_calls(call_pk, call_id, project_id, session_id, agent_id, role, provider, model_provider, model, effort, sequence, occurred_at, tokens_input, tokens_cache_write, tokens_cache_read, tokens_output, tokens_reasoning, tokens_total, cost_usd, cost_status, transcript_id, prompt_text, response_text, prompt_envelope, response_envelope, status, metadata_json, metadata_envelope)
|
|
647
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
467
648
|
`).run(
|
|
468
649
|
scopedIdentity(event.project_id, callId), callId, event.project_id, sessionId, agentId, text(p.role, 'main'), text(p.provider), text(p.model_provider || p.modelProvider), text(p.model), text(p.effort), integer(p.sequence),
|
|
469
650
|
text(p.occurred_at || event.occurred_at), input, cacheWrite, cacheRead, output, reasoning, total, number(p.cost_usd ?? p.costUsd), text(p.cost_status || p.costStatus, 'unknown'),
|
|
470
|
-
p.transcript_id || p.transcriptId || null,
|
|
651
|
+
p.transcript_id || p.transcriptId || null, promptEnvelope ? '' : prompt, responseEnvelope ? '' : response,
|
|
652
|
+
promptEnvelope ? json(promptEnvelope) : '', responseEnvelope ? json(responseEnvelope) : '', text(p.status, 'complete'), encryption ? '{}' : json(p.metadata), metadataEnvelope,
|
|
471
653
|
);
|
|
472
654
|
return { stale: false };
|
|
473
655
|
}
|
|
@@ -478,17 +660,22 @@ function applyTranscript(db, event) {
|
|
|
478
660
|
const agentId = ensureAgent(db, event.project_id, p);
|
|
479
661
|
const transcriptId = text(p.transcript_id || p.transcriptId);
|
|
480
662
|
if (!transcriptId) throw new Error('transcript_id ausente.');
|
|
481
|
-
const
|
|
663
|
+
const { encryption } = databaseSecurity(db);
|
|
664
|
+
const encoded = encodeTranscript(p.content, { encryption, aad: `${event.project_id}:transcript:${transcriptId}` });
|
|
665
|
+
const metadataEnvelope = encryption
|
|
666
|
+
? encryptedJson(encryption, p.metadata || {}, `${event.project_id}:transcript:${transcriptId}:metadata`)
|
|
667
|
+
: '';
|
|
482
668
|
db.prepare(`
|
|
483
|
-
INSERT INTO transcripts(transcript_pk, transcript_id, project_id, session_id, agent_id, coverage, codec, content_gzip, content_sha256, original_bytes, compressed_bytes, source, occurred_at, metadata_json)
|
|
484
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
669
|
+
INSERT INTO transcripts(transcript_pk, transcript_id, project_id, session_id, agent_id, coverage, codec, content_gzip, content_sha256, original_bytes, compressed_bytes, source, occurred_at, metadata_json, metadata_envelope)
|
|
670
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
485
671
|
ON CONFLICT(project_id, transcript_id) DO UPDATE SET
|
|
486
672
|
coverage = excluded.coverage, codec = excluded.codec, content_gzip = excluded.content_gzip,
|
|
487
673
|
content_sha256 = excluded.content_sha256, original_bytes = excluded.original_bytes, compressed_bytes = excluded.compressed_bytes,
|
|
488
|
-
source = excluded.source, occurred_at = excluded.occurred_at, metadata_json = excluded.metadata_json
|
|
674
|
+
source = excluded.source, occurred_at = excluded.occurred_at, metadata_json = excluded.metadata_json,
|
|
675
|
+
metadata_envelope = excluded.metadata_envelope
|
|
489
676
|
`).run(
|
|
490
677
|
scopedIdentity(event.project_id, transcriptId), transcriptId, event.project_id, sessionId, agentId, text(p.coverage, 'complete'), encoded.codec, encoded.content_gzip, encoded.content_sha256,
|
|
491
|
-
encoded.original_bytes, encoded.compressed_bytes, text(p.source), event.occurred_at, json(p.metadata),
|
|
678
|
+
encoded.original_bytes, encoded.compressed_bytes, text(p.source), event.occurred_at, encryption ? '{}' : json(p.metadata), metadataEnvelope,
|
|
492
679
|
);
|
|
493
680
|
return { stale: false };
|
|
494
681
|
}
|
|
@@ -504,10 +691,36 @@ function applyEvent(db, event) {
|
|
|
504
691
|
throw new Error('kind não implementado.');
|
|
505
692
|
}
|
|
506
693
|
|
|
694
|
+
function ledgerPayload(event, encryption) {
|
|
695
|
+
if (!encryption) return event.payload;
|
|
696
|
+
const payload = structuredClone(event.payload || {});
|
|
697
|
+
if (event.kind === 'document.upsert' || event.kind === 'transcript.upsert') payload.content = '[PROTECTED]';
|
|
698
|
+
if (event.kind === 'llm_call') {
|
|
699
|
+
for (const key of ['prompt', 'promptText', 'prompt_text', 'response', 'responseText', 'response_text']) {
|
|
700
|
+
if (Object.hasOwn(payload, key)) payload[key] = '[PROTECTED]';
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (['document.upsert', 'transcript.upsert', 'llm_call'].includes(event.kind)
|
|
704
|
+
&& Object.hasOwn(payload, 'metadata')) payload.metadata = '[PROTECTED]';
|
|
705
|
+
return payload;
|
|
706
|
+
}
|
|
707
|
+
|
|
507
708
|
export function ingestObserverEvents(db, { projectId, events = [] } = {}) {
|
|
508
709
|
requireProject(db, projectId);
|
|
509
|
-
const
|
|
510
|
-
|
|
710
|
+
const security = databaseSecurity(db);
|
|
711
|
+
const storedPolicy = readObserverPolicy(db, projectId);
|
|
712
|
+
const effectivePolicy = security.policy || (security.enforcePolicy ? storedPolicy : null);
|
|
713
|
+
if ((security.policy?.encryption_required || storedPolicy.encryption_required) && !security.encryption) {
|
|
714
|
+
throw Object.assign(new Error('A policy exige criptografia antes da ingestão.'), { code: 'observer_encryption_required' });
|
|
715
|
+
}
|
|
716
|
+
const result = { accepted: 0, duplicates: 0, conflicts: 0, stale: 0, rejected: 0, dropped: 0, results: [] };
|
|
717
|
+
for (const incoming of events) {
|
|
718
|
+
const event = effectivePolicy ? protectObserverEvent(incoming, { policy: effectivePolicy }) : incoming;
|
|
719
|
+
if (!event) {
|
|
720
|
+
result.dropped += 1;
|
|
721
|
+
result.results.push({ accepted: false, dropped: true, event_id: incoming?.event_id || '' });
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
511
724
|
const validation = validateEvent(event, projectId);
|
|
512
725
|
if (!validation.ok) {
|
|
513
726
|
result.rejected += 1;
|
|
@@ -537,7 +750,7 @@ export function ingestObserverEvents(db, { projectId, events = [] } = {}) {
|
|
|
537
750
|
let savepointOpen = true;
|
|
538
751
|
try {
|
|
539
752
|
db.prepare('INSERT INTO ingest_events(event_id, project_id, kind, payload_hash, payload_json, occurred_at, ingested_at, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)')
|
|
540
|
-
.run(event.event_id, projectId, event.kind, payloadHash, json(event.
|
|
753
|
+
.run(event.event_id, projectId, event.kind, payloadHash, json(ledgerPayload(event, security.encryption)), event.occurred_at, now(), 'accepted');
|
|
541
754
|
const applied = applyEvent(db, event);
|
|
542
755
|
if (applied.stale) {
|
|
543
756
|
db.prepare('UPDATE ingest_events SET status = ? WHERE event_id = ?').run('stale', event.event_id);
|
|
@@ -647,12 +860,23 @@ export function readUsageCalls(db, projectId, filters = {}) {
|
|
|
647
860
|
offset,
|
|
648
861
|
limit,
|
|
649
862
|
total: integer(rows[0]?.total_count),
|
|
650
|
-
calls: rows.map((row) =>
|
|
863
|
+
calls: rows.map((row) => {
|
|
864
|
+
const { encryption } = databaseSecurity(db);
|
|
865
|
+
const prompt = row.prompt_envelope
|
|
866
|
+
? decryptObserverValue(encryption, parseJson(row.prompt_envelope), { aad: `${projectId}:call:${row.call_id}:prompt` })
|
|
867
|
+
: row.prompt_text;
|
|
868
|
+
const response = row.response_envelope
|
|
869
|
+
? decryptObserverValue(encryption, parseJson(row.response_envelope), { aad: `${projectId}:call:${row.call_id}:response` })
|
|
870
|
+
: row.response_text;
|
|
871
|
+
return ({
|
|
651
872
|
call_id: row.call_id, session_id: row.session_id, agent_id: row.agent_id, role: row.role, provider: row.provider,
|
|
652
873
|
model_provider: row.model_provider, model: row.model, effort: row.effort, sequence: integer(row.sequence), occurred_at: row.occurred_at,
|
|
653
874
|
tokens: tokenObject(row), cost_usd: Number(number(row.cost_usd).toFixed(4)), cost_status: row.cost_status,
|
|
654
|
-
transcript_id: row.transcript_id, prompt
|
|
655
|
-
|
|
875
|
+
transcript_id: row.transcript_id, prompt, response, status: row.status,
|
|
876
|
+
metadata: row.metadata_envelope
|
|
877
|
+
? decryptedJson(encryption, row.metadata_envelope, {}, `${projectId}:call:${row.call_id}:metadata`)
|
|
878
|
+
: parseJson(row.metadata_json),
|
|
879
|
+
}); }),
|
|
656
880
|
};
|
|
657
881
|
}
|
|
658
882
|
|
|
@@ -664,7 +888,14 @@ export function readTranscript(db, projectId, transcriptId) {
|
|
|
664
888
|
error.code = 'transcript_not_found';
|
|
665
889
|
throw error;
|
|
666
890
|
}
|
|
667
|
-
|
|
891
|
+
const decoded = decodeTranscript(row, {
|
|
892
|
+
encryption: databaseSecurity(db).encryption,
|
|
893
|
+
aad: `${projectId}:transcript:${transcriptId}`,
|
|
894
|
+
});
|
|
895
|
+
decoded.metadata = row.metadata_envelope
|
|
896
|
+
? decryptedJson(databaseSecurity(db).encryption, row.metadata_envelope, {}, `${projectId}:transcript:${transcriptId}:metadata`)
|
|
897
|
+
: parseJson(row.metadata_json);
|
|
898
|
+
return decoded;
|
|
668
899
|
}
|
|
669
900
|
|
|
670
901
|
export function readSqlDocument(db, projectId, logicalPath) {
|
|
@@ -680,7 +911,13 @@ export function readSqlDocument(db, projectId, logicalPath) {
|
|
|
680
911
|
error.code = 'memory_not_found';
|
|
681
912
|
throw error;
|
|
682
913
|
}
|
|
683
|
-
|
|
914
|
+
const content = row.content_envelope
|
|
915
|
+
? decryptObserverValue(databaseSecurity(db).encryption, parseJson(row.content_envelope), { aad: `${projectId}:document:${logicalPath}` })
|
|
916
|
+
: row.content;
|
|
917
|
+
const metadata = row.metadata_envelope
|
|
918
|
+
? decryptedJson(databaseSecurity(db).encryption, row.metadata_envelope, {}, `${projectId}:document:${logicalPath}:metadata`)
|
|
919
|
+
: parseJson(row.metadata_json);
|
|
920
|
+
return { ...row, content, metadata };
|
|
684
921
|
}
|
|
685
922
|
|
|
686
923
|
export function readSqlTree(db, projectId, prefix = '') {
|
|
@@ -731,10 +968,16 @@ export function readSqlSync(db, projectId) {
|
|
|
731
968
|
return { mode: 'container-authority', project_id: projectId, document_count: integer(documents.count), event_count: integer(events.count), pending_count: integer(pending.count), database: OBSERVER_SQL_FILE, schema_version: OBSERVER_SQL_SCHEMA_VERSION };
|
|
732
969
|
}
|
|
733
970
|
|
|
734
|
-
export function exportSqlMemoryBundle(db, projectId) {
|
|
971
|
+
export function exportSqlMemoryBundle(db, projectId, { includeContent = false } = {}) {
|
|
735
972
|
requireProject(db, projectId);
|
|
736
973
|
const rows = db.prepare('SELECT logical_path, entity_type, content, content_hash, revision, captured_at FROM documents WHERE project_id = ? AND deleted_at IS NULL ORDER BY logical_path').all(projectId);
|
|
737
|
-
return {
|
|
974
|
+
return {
|
|
975
|
+
schema_version: OBSERVER_SQL_SCHEMA_VERSION,
|
|
976
|
+
project_id: projectId,
|
|
977
|
+
generated_at: now(),
|
|
978
|
+
sanitized: !includeContent,
|
|
979
|
+
documents: rows.map((row) => ({ ...row, content: includeContent ? row.content : '' })),
|
|
980
|
+
};
|
|
738
981
|
}
|
|
739
982
|
|
|
740
983
|
export function readSqlProject(db, projectId) {
|
|
@@ -745,6 +988,11 @@ export function readSqlProject(db, projectId) {
|
|
|
745
988
|
export function upsertSqlProjectSnapshot(db, snapshot) {
|
|
746
989
|
const projectId = text(snapshot?.project_id || snapshot?.projectId);
|
|
747
990
|
requireProject(db, projectId);
|
|
991
|
+
const security = databaseSecurity(db);
|
|
992
|
+
const storedPolicy = readObserverPolicy(db, projectId);
|
|
993
|
+
if ((security.policy?.encryption_required || storedPolicy.encryption_required) && !security.encryption) {
|
|
994
|
+
throw Object.assign(new Error('A policy exige criptografia antes de persistir snapshots.'), { code: 'observer_encryption_required' });
|
|
995
|
+
}
|
|
748
996
|
const eventId = text(snapshot?.event_id);
|
|
749
997
|
const capturedAt = text(snapshot?.captured_at);
|
|
750
998
|
if (!eventId || !capturedAt || Number.isNaN(Date.parse(capturedAt))) throw new Error('snapshot SQL inválido.');
|
|
@@ -754,19 +1002,25 @@ export function upsertSqlProjectSnapshot(db, snapshot) {
|
|
|
754
1002
|
|| (current.captured_at === capturedAt && current.event_id >= eventId))) {
|
|
755
1003
|
return { accepted: false, stale: true, event_id: eventId };
|
|
756
1004
|
}
|
|
757
|
-
|
|
758
|
-
|
|
1005
|
+
const { encryption } = security;
|
|
1006
|
+
const snapshotEnvelope = encryption ? encryptedJson(encryption, snapshot, `${projectId}:snapshot`) : '';
|
|
1007
|
+
db.prepare(`INSERT INTO project_snapshots(project_id, event_id, captured_at, snapshot_json, snapshot_envelope)
|
|
1008
|
+
VALUES (?, ?, ?, ?, ?)
|
|
759
1009
|
ON CONFLICT(project_id) DO UPDATE SET
|
|
760
1010
|
event_id = excluded.event_id,
|
|
761
1011
|
captured_at = excluded.captured_at,
|
|
762
|
-
snapshot_json = excluded.snapshot_json
|
|
1012
|
+
snapshot_json = excluded.snapshot_json,
|
|
1013
|
+
snapshot_envelope = excluded.snapshot_envelope`).run(projectId, eventId, capturedAt, encryption ? '{}' : json(snapshot), snapshotEnvelope);
|
|
763
1014
|
return { accepted: true, duplicate: false, event_id: eventId };
|
|
764
1015
|
}
|
|
765
1016
|
|
|
766
1017
|
export function readSqlProjectSnapshot(db, projectId) {
|
|
767
1018
|
requireProject(db, projectId);
|
|
768
|
-
const row = db.prepare('SELECT snapshot_json FROM project_snapshots WHERE project_id = ?').get(projectId);
|
|
769
|
-
|
|
1019
|
+
const row = db.prepare('SELECT snapshot_json, snapshot_envelope FROM project_snapshots WHERE project_id = ?').get(projectId);
|
|
1020
|
+
if (!row) return null;
|
|
1021
|
+
return row.snapshot_envelope
|
|
1022
|
+
? decryptedJson(databaseSecurity(db).encryption, row.snapshot_envelope, null, `${projectId}:snapshot`)
|
|
1023
|
+
: parseJson(row.snapshot_json, null);
|
|
770
1024
|
}
|
|
771
1025
|
|
|
772
1026
|
export function readSqlProjectOverview(db, projectId) {
|
|
@@ -1,25 +1,40 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { gunzipSync, gzipSync } from 'node:zlib';
|
|
3
|
+
import { decryptObserverValue, encryptObserverValue } from '../packages/observer/src/encryption.mjs';
|
|
3
4
|
|
|
4
5
|
export const TRANSCRIPT_CODEC = 'gzip';
|
|
5
6
|
|
|
6
|
-
export function encodeTranscript(content) {
|
|
7
|
+
export function encodeTranscript(content, { encryption = null, aad = '' } = {}) {
|
|
7
8
|
const text = String(content ?? '');
|
|
8
9
|
const raw = Buffer.from(text, 'utf8');
|
|
9
10
|
const compressed = gzipSync(raw, { mtime: 0 });
|
|
11
|
+
const envelope = encryption
|
|
12
|
+
? encryptObserverValue(encryption, compressed.toString('base64'), { aad })
|
|
13
|
+
: null;
|
|
14
|
+
const stored = envelope ? Buffer.from(JSON.stringify(envelope), 'utf8') : compressed;
|
|
10
15
|
return {
|
|
11
|
-
codec: TRANSCRIPT_CODEC,
|
|
12
|
-
content_gzip:
|
|
16
|
+
codec: envelope ? 'aes-256-gcm+gzip' : TRANSCRIPT_CODEC,
|
|
17
|
+
content_gzip: stored,
|
|
13
18
|
content_sha256: createHash('sha256').update(raw).digest('hex'),
|
|
14
19
|
original_bytes: raw.byteLength,
|
|
15
|
-
compressed_bytes:
|
|
20
|
+
compressed_bytes: stored.byteLength,
|
|
16
21
|
};
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
export function decodeTranscript(row) {
|
|
24
|
+
export function decodeTranscript(row, { encryption = null, aad = '' } = {}) {
|
|
20
25
|
if (!row) return null;
|
|
21
|
-
const
|
|
22
|
-
|
|
26
|
+
const stored = Buffer.from(row.content_gzip || []);
|
|
27
|
+
let compressed = stored;
|
|
28
|
+
if (row.codec === 'aes-256-gcm+gzip') {
|
|
29
|
+
if (!encryption) {
|
|
30
|
+
const error = new Error('Chave do transcript protegido indisponível.');
|
|
31
|
+
error.code = 'observer_encryption_key_unavailable';
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
const envelope = JSON.parse(stored.toString('utf8'));
|
|
35
|
+
compressed = Buffer.from(decryptObserverValue(encryption, envelope, { aad }), 'base64');
|
|
36
|
+
}
|
|
37
|
+
const raw = [TRANSCRIPT_CODEC, 'aes-256-gcm+gzip'].includes(row.codec) ? gunzipSync(compressed) : compressed;
|
|
23
38
|
const content = raw.toString('utf8');
|
|
24
39
|
const contentSha256 = createHash('sha256').update(raw).digest('hex');
|
|
25
40
|
if (row.content_sha256 && row.content_sha256 !== contentSha256) {
|
|
@@ -37,7 +52,7 @@ export function decodeTranscript(row) {
|
|
|
37
52
|
content,
|
|
38
53
|
content_sha256: contentSha256,
|
|
39
54
|
original_bytes: Number(row.original_bytes) || raw.byteLength,
|
|
40
|
-
compressed_bytes: Number(row.compressed_bytes) ||
|
|
55
|
+
compressed_bytes: Number(row.compressed_bytes) || stored.byteLength,
|
|
41
56
|
source: row.source || '',
|
|
42
57
|
occurred_at: row.occurred_at,
|
|
43
58
|
metadata: parseJson(row.metadata_json),
|