@gsengai/core 0.1.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/LICENSE +201 -0
- package/dist/gsengai-audit.js +629 -0
- package/dist/gsengai-audit.js.map +1 -0
- package/dist/index.cjs +767 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +715 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
CSV_COLUMNS: () => CSV_COLUMNS,
|
|
34
|
+
HASH_VERSION: () => HASH_VERSION,
|
|
35
|
+
NotImplementedError: () => NotImplementedError,
|
|
36
|
+
PRD_S9_LIMITS: () => PRD_S9_LIMITS,
|
|
37
|
+
canonicalJson: () => canonicalJson,
|
|
38
|
+
createEvidenceStore: () => createEvidenceStore,
|
|
39
|
+
getLostRecordCount: () => getLostRecordCount,
|
|
40
|
+
hashPrompt: () => hashPrompt,
|
|
41
|
+
hashText: () => hashText,
|
|
42
|
+
instrumentAsyncIterable: () => instrumentAsyncIterable,
|
|
43
|
+
normalizeText: () => normalizeText,
|
|
44
|
+
renderAuditReport: () => renderAuditReport,
|
|
45
|
+
resetLostRecordCount: () => resetLostRecordCount,
|
|
46
|
+
runAuditCli: () => runAuditCli,
|
|
47
|
+
safeAppend: () => safeAppend,
|
|
48
|
+
sha256Hex: () => sha256Hex
|
|
49
|
+
});
|
|
50
|
+
module.exports = __toCommonJS(index_exports);
|
|
51
|
+
|
|
52
|
+
// src/hash.ts
|
|
53
|
+
var import_node_crypto = require("crypto");
|
|
54
|
+
var HASH_VERSION = 1;
|
|
55
|
+
function sha256Hex(data) {
|
|
56
|
+
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
57
|
+
if (typeof data === "string") {
|
|
58
|
+
hash.update(data, "utf8");
|
|
59
|
+
} else {
|
|
60
|
+
hash.update(data);
|
|
61
|
+
}
|
|
62
|
+
return hash.digest("hex");
|
|
63
|
+
}
|
|
64
|
+
function normalizeText(raw) {
|
|
65
|
+
return raw.normalize("NFC").toLowerCase().replace(/\s+/gu, " ").trim();
|
|
66
|
+
}
|
|
67
|
+
function hashText(raw) {
|
|
68
|
+
return {
|
|
69
|
+
outputHash: sha256Hex(raw),
|
|
70
|
+
outputHashNormalized: sha256Hex(normalizeText(raw)),
|
|
71
|
+
hashVersion: HASH_VERSION
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/canonical-json.ts
|
|
76
|
+
function canonicalJson(value) {
|
|
77
|
+
const out = JSON.stringify(value, (_key, val) => {
|
|
78
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
79
|
+
const record = val;
|
|
80
|
+
const sorted = {};
|
|
81
|
+
for (const key of Object.keys(record).sort()) {
|
|
82
|
+
sorted[key] = record[key];
|
|
83
|
+
}
|
|
84
|
+
return sorted;
|
|
85
|
+
}
|
|
86
|
+
return val;
|
|
87
|
+
});
|
|
88
|
+
if (out === void 0) {
|
|
89
|
+
throw new TypeError("canonicalJson: value does not serialize to JSON");
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function hashPrompt(messagesOrInput) {
|
|
94
|
+
return sha256Hex(canonicalJson(messagesOrInput));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/cli.ts
|
|
98
|
+
var import_node_fs2 = require("fs");
|
|
99
|
+
var import_node_util = require("util");
|
|
100
|
+
|
|
101
|
+
// src/store.ts
|
|
102
|
+
var import_node_crypto2 = require("crypto");
|
|
103
|
+
var import_node_events = require("events");
|
|
104
|
+
var import_node_fs = require("fs");
|
|
105
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
106
|
+
|
|
107
|
+
// src/errors.ts
|
|
108
|
+
var NotImplementedError = class extends Error {
|
|
109
|
+
constructor(message) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = "NotImplementedError";
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/export.ts
|
|
116
|
+
var CSV_COLUMNS = [
|
|
117
|
+
"id",
|
|
118
|
+
"ts",
|
|
119
|
+
"modality",
|
|
120
|
+
"model",
|
|
121
|
+
"system_id",
|
|
122
|
+
"prompt_hash",
|
|
123
|
+
"output_hash",
|
|
124
|
+
"output_hash_normalized",
|
|
125
|
+
"hash_version",
|
|
126
|
+
"marking_methods",
|
|
127
|
+
"manifest_ref",
|
|
128
|
+
"disclosure_context",
|
|
129
|
+
"prev_hash",
|
|
130
|
+
"record_hash"
|
|
131
|
+
];
|
|
132
|
+
function csvField(value) {
|
|
133
|
+
if (value === null) {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
const s = String(value);
|
|
137
|
+
return /[",\r\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s;
|
|
138
|
+
}
|
|
139
|
+
function recordToCsvRow(record) {
|
|
140
|
+
return CSV_COLUMNS.map(
|
|
141
|
+
(column) => csvField(
|
|
142
|
+
column === "marking_methods" ? JSON.stringify(record.marking_methods) : record[column]
|
|
143
|
+
)
|
|
144
|
+
).join(",");
|
|
145
|
+
}
|
|
146
|
+
var PRD_S9_LIMITS = `> **What this does NOT do**
|
|
147
|
+
>
|
|
148
|
+
> - It does not make you compliant. It supports compliance with EU AI Act Article 50 and California SB 942; compliance depends on your system, your deployment context, and your processes.
|
|
149
|
+
> - It does not embed imperceptible watermarks (MVP). It implements the signed-metadata and logging/fingerprinting layers of a multi-layer marking strategy.
|
|
150
|
+
> - It cannot prevent downstream metadata stripping \u2014 manifests can be removed by re-encoding, screenshots, or platform uploads. That is exactly why the logging layer exists.
|
|
151
|
+
> - It does not detect third-party AI content.
|
|
152
|
+
> - It is not legal advice. Consult qualified counsel about your obligations.`;
|
|
153
|
+
function cell(value) {
|
|
154
|
+
if (value === null || value === "") {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
return value.replaceAll("|", "\\|").replaceAll(/\r?\n/gu, " ");
|
|
158
|
+
}
|
|
159
|
+
function code(value) {
|
|
160
|
+
return value === null || value === "" ? "" : `\`${cell(value)}\``;
|
|
161
|
+
}
|
|
162
|
+
function describeFilter(filter) {
|
|
163
|
+
const parts = [];
|
|
164
|
+
if (filter?.systemId !== void 0) {
|
|
165
|
+
parts.push(`system_id = \`${cell(filter.systemId)}\``);
|
|
166
|
+
}
|
|
167
|
+
if (filter?.modality !== void 0) {
|
|
168
|
+
parts.push(`modality = \`${filter.modality}\``);
|
|
169
|
+
}
|
|
170
|
+
if (filter?.since !== void 0) {
|
|
171
|
+
parts.push(`ts \u2265 \`${filter.since}\``);
|
|
172
|
+
}
|
|
173
|
+
if (filter?.until !== void 0) {
|
|
174
|
+
parts.push(`ts \u2264 \`${filter.until}\``);
|
|
175
|
+
}
|
|
176
|
+
return parts.length === 0 ? "none (full store)" : parts.join(" \xB7 ");
|
|
177
|
+
}
|
|
178
|
+
function breakdown(records, label) {
|
|
179
|
+
const counts = /* @__PURE__ */ new Map();
|
|
180
|
+
for (const record of records) {
|
|
181
|
+
const key = label(record);
|
|
182
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
183
|
+
}
|
|
184
|
+
const rows = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
185
|
+
return rows.map(([key, n]) => `| ${cell(key)} | ${n} |`).join("\n");
|
|
186
|
+
}
|
|
187
|
+
function integritySection(integrity) {
|
|
188
|
+
if (integrity.ok) {
|
|
189
|
+
return `**Chain verified** \u2014 ${integrity.checked} record(s) checked; the tamper-evident hash chain is intact.`;
|
|
190
|
+
}
|
|
191
|
+
return `**\u26A0 BROKEN at seq ${integrity.brokenAtSeq}** \u2014 tamper-evident hash-chain verification FAILED at record seq ${integrity.brokenAtSeq} (${integrity.checked} record(s) checked up to and including the break). Records at and after this point cannot be trusted as appended; investigate the store before relying on this log.`;
|
|
192
|
+
}
|
|
193
|
+
function renderAuditReport(ctx) {
|
|
194
|
+
const { records, integrity } = ctx;
|
|
195
|
+
const firstTs = records[0]?.ts ?? null;
|
|
196
|
+
const lastTs = records.at(-1)?.ts ?? null;
|
|
197
|
+
const recordRows = records.length === 0 ? "_No records match the filter._" : [
|
|
198
|
+
"| id | ts | modality | model | system_id | output_hash | output_hash_normalized | marking_methods | manifest_ref | disclosure_context |",
|
|
199
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
200
|
+
...records.map(
|
|
201
|
+
(r) => `| ${code(r.id)} | ${r.ts} | ${r.modality} | ${cell(r.model)} | ${cell(r.system_id)} | ${code(r.output_hash)} | ${code(r.output_hash_normalized)} | ${cell(r.marking_methods.join(" + "))} | ${code(r.manifest_ref)} | ${cell(r.disclosure_context)} |`
|
|
202
|
+
)
|
|
203
|
+
].join("\n");
|
|
204
|
+
const summaryBreakdowns = records.length === 0 ? "" : `
|
|
205
|
+
### By modality
|
|
206
|
+
|
|
207
|
+
| Modality | Records |
|
|
208
|
+
| --- | --- |
|
|
209
|
+
${breakdown(records, (r) => r.modality)}
|
|
210
|
+
|
|
211
|
+
### By model
|
|
212
|
+
|
|
213
|
+
| Model | Records |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
${breakdown(records, (r) => r.model)}
|
|
216
|
+
|
|
217
|
+
### By system_id
|
|
218
|
+
|
|
219
|
+
| system_id | Records |
|
|
220
|
+
| --- | --- |
|
|
221
|
+
${breakdown(records, (r) => r.system_id)}
|
|
222
|
+
|
|
223
|
+
### By marking methods
|
|
224
|
+
|
|
225
|
+
| Marking methods | Records |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
${breakdown(records, (r) => r.marking_methods.join(" + "))}
|
|
228
|
+
`;
|
|
229
|
+
return `# Evidence audit report (gsengai)
|
|
230
|
+
|
|
231
|
+
- Generated: ${ctx.generatedAt} (UTC)
|
|
232
|
+
- Store: \`${cell(ctx.storePath)}\`
|
|
233
|
+
- Filter: ${describeFilter(ctx.filter)}
|
|
234
|
+
- Record schema: v1 (PRD \xA74) \u2014 hashes and metadata only; raw content is never stored or exported
|
|
235
|
+
|
|
236
|
+
## Integrity
|
|
237
|
+
|
|
238
|
+
${integritySection(integrity)}
|
|
239
|
+
|
|
240
|
+
Chain verification always covers the entire store, regardless of the export filter above.
|
|
241
|
+
|
|
242
|
+
## Summary
|
|
243
|
+
|
|
244
|
+
- Records (after filter): ${records.length}
|
|
245
|
+
- First record: ${firstTs ?? "\u2014"}
|
|
246
|
+
- Last record: ${lastTs ?? "\u2014"}
|
|
247
|
+
${summaryBreakdowns}
|
|
248
|
+
## Records
|
|
249
|
+
|
|
250
|
+
${recordRows}
|
|
251
|
+
|
|
252
|
+
## What this evidence is \u2014 and is not
|
|
253
|
+
|
|
254
|
+
This report is a machine-generated record of the outputs an AI system generated, marked,
|
|
255
|
+
and disclosed, as captured in an append-only, hash-chained evidence store. It contains
|
|
256
|
+
cryptographic hashes and metadata only \u2014 raw prompts and outputs are never stored and
|
|
257
|
+
never exported. It supports compliance with EU AI Act Article 50 and California SB 942;
|
|
258
|
+
it is not, by itself, a determination of compliance.
|
|
259
|
+
|
|
260
|
+
${PRD_S9_LIMITS}
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/store.ts
|
|
265
|
+
var MODALITIES = /* @__PURE__ */ new Set(["text", "image", "audio", "video"]);
|
|
266
|
+
var SCHEMA_SQL = `
|
|
267
|
+
CREATE TABLE IF NOT EXISTS evidence (
|
|
268
|
+
seq INTEGER PRIMARY KEY,
|
|
269
|
+
id TEXT NOT NULL UNIQUE,
|
|
270
|
+
ts TEXT NOT NULL,
|
|
271
|
+
modality TEXT NOT NULL CHECK (modality IN ('text','image','audio','video')),
|
|
272
|
+
model TEXT NOT NULL,
|
|
273
|
+
system_id TEXT NOT NULL,
|
|
274
|
+
prompt_hash TEXT,
|
|
275
|
+
output_hash TEXT NOT NULL,
|
|
276
|
+
output_hash_normalized TEXT,
|
|
277
|
+
hash_version INTEGER NOT NULL,
|
|
278
|
+
marking_methods TEXT NOT NULL,
|
|
279
|
+
manifest_ref TEXT,
|
|
280
|
+
disclosure_context TEXT,
|
|
281
|
+
prev_hash TEXT,
|
|
282
|
+
record_hash TEXT NOT NULL
|
|
283
|
+
);
|
|
284
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_output_hash
|
|
285
|
+
ON evidence (output_hash);
|
|
286
|
+
CREATE INDEX IF NOT EXISTS idx_evidence_output_hash_normalized
|
|
287
|
+
ON evidence (output_hash_normalized);
|
|
288
|
+
CREATE TRIGGER IF NOT EXISTS evidence_append_only_update
|
|
289
|
+
BEFORE UPDATE ON evidence
|
|
290
|
+
BEGIN
|
|
291
|
+
SELECT RAISE(ABORT, 'gsengai evidence store is append-only: UPDATE is not permitted');
|
|
292
|
+
END;
|
|
293
|
+
CREATE TRIGGER IF NOT EXISTS evidence_append_only_delete
|
|
294
|
+
BEFORE DELETE ON evidence
|
|
295
|
+
BEGIN
|
|
296
|
+
SELECT RAISE(ABORT, 'gsengai evidence store is append-only: DELETE is not permitted');
|
|
297
|
+
END;
|
|
298
|
+
`;
|
|
299
|
+
function rowToRecord(row) {
|
|
300
|
+
return {
|
|
301
|
+
id: row.id,
|
|
302
|
+
ts: row.ts,
|
|
303
|
+
modality: row.modality,
|
|
304
|
+
model: row.model,
|
|
305
|
+
system_id: row.system_id,
|
|
306
|
+
prompt_hash: row.prompt_hash,
|
|
307
|
+
output_hash: row.output_hash,
|
|
308
|
+
output_hash_normalized: row.output_hash_normalized,
|
|
309
|
+
hash_version: row.hash_version,
|
|
310
|
+
marking_methods: JSON.parse(row.marking_methods),
|
|
311
|
+
manifest_ref: row.manifest_ref,
|
|
312
|
+
disclosure_context: row.disclosure_context,
|
|
313
|
+
prev_hash: row.prev_hash,
|
|
314
|
+
record_hash: row.record_hash
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function computeRecordHash(body) {
|
|
318
|
+
return sha256Hex(canonicalJson(body));
|
|
319
|
+
}
|
|
320
|
+
function requireNonEmptyString(value, name) {
|
|
321
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
322
|
+
throw new TypeError(`gsengai: ${name} must be a non-empty string`);
|
|
323
|
+
}
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
function resolveOutputHashes(input) {
|
|
327
|
+
const provided = [input.outputText, input.outputBytes, input.outputHashes].filter(
|
|
328
|
+
(v) => v !== void 0
|
|
329
|
+
);
|
|
330
|
+
if (provided.length !== 1) {
|
|
331
|
+
throw new TypeError("gsengai: provide exactly one of outputText, outputBytes, or outputHashes");
|
|
332
|
+
}
|
|
333
|
+
if (input.outputText !== void 0) {
|
|
334
|
+
const hashes = hashText(input.outputText);
|
|
335
|
+
return {
|
|
336
|
+
output_hash: hashes.outputHash,
|
|
337
|
+
output_hash_normalized: hashes.outputHashNormalized,
|
|
338
|
+
hash_version: hashes.hashVersion
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (input.outputBytes !== void 0) {
|
|
342
|
+
return {
|
|
343
|
+
output_hash: sha256Hex(input.outputBytes),
|
|
344
|
+
output_hash_normalized: null,
|
|
345
|
+
hash_version: HASH_VERSION
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const given = input.outputHashes;
|
|
349
|
+
return {
|
|
350
|
+
output_hash: requireNonEmptyString(given.outputHash, "outputHashes.outputHash"),
|
|
351
|
+
output_hash_normalized: given.outputHashNormalized ?? null,
|
|
352
|
+
hash_version: given.hashVersion ?? HASH_VERSION
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function normalizeInstant(value, name) {
|
|
356
|
+
const ms = Date.parse(requireNonEmptyString(value, name));
|
|
357
|
+
if (Number.isNaN(ms)) {
|
|
358
|
+
throw new TypeError(`gsengai: ${name} must be a Date.parse-able timestamp`);
|
|
359
|
+
}
|
|
360
|
+
return new Date(ms).toISOString();
|
|
361
|
+
}
|
|
362
|
+
function buildFilter(filter) {
|
|
363
|
+
if (filter === void 0) {
|
|
364
|
+
return { where: "", params: {}, normalized: void 0 };
|
|
365
|
+
}
|
|
366
|
+
const conditions = [];
|
|
367
|
+
const params = {};
|
|
368
|
+
const normalized = {};
|
|
369
|
+
if (filter.systemId !== void 0) {
|
|
370
|
+
normalized.systemId = requireNonEmptyString(filter.systemId, "filter.systemId");
|
|
371
|
+
conditions.push("system_id = @systemId");
|
|
372
|
+
params.systemId = normalized.systemId;
|
|
373
|
+
}
|
|
374
|
+
if (filter.modality !== void 0) {
|
|
375
|
+
if (!MODALITIES.has(filter.modality)) {
|
|
376
|
+
throw new TypeError(
|
|
377
|
+
"gsengai: filter.modality must be one of 'text' | 'image' | 'audio' | 'video'"
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
normalized.modality = filter.modality;
|
|
381
|
+
conditions.push("modality = @modality");
|
|
382
|
+
params.modality = normalized.modality;
|
|
383
|
+
}
|
|
384
|
+
if (filter.since !== void 0) {
|
|
385
|
+
normalized.since = normalizeInstant(filter.since, "filter.since");
|
|
386
|
+
conditions.push("ts >= @since");
|
|
387
|
+
params.since = normalized.since;
|
|
388
|
+
}
|
|
389
|
+
if (filter.until !== void 0) {
|
|
390
|
+
normalized.until = normalizeInstant(filter.until, "filter.until");
|
|
391
|
+
conditions.push("ts <= @until");
|
|
392
|
+
params.until = normalized.until;
|
|
393
|
+
}
|
|
394
|
+
if (conditions.length === 0) {
|
|
395
|
+
return { where: "", params: {}, normalized: void 0 };
|
|
396
|
+
}
|
|
397
|
+
return { where: ` WHERE ${conditions.join(" AND ")}`, params, normalized };
|
|
398
|
+
}
|
|
399
|
+
function createEvidenceStore(options) {
|
|
400
|
+
if (options.storeRawContent) {
|
|
401
|
+
throw new NotImplementedError(
|
|
402
|
+
"gsengai: storeRawContent is not implemented in the MVP. Only hashes and metadata persist (PRD C4); encrypted raw-content capture is planned post-MVP."
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const storePath = requireNonEmptyString(options.path, "path");
|
|
406
|
+
const db = new import_better_sqlite3.default(storePath);
|
|
407
|
+
db.pragma("journal_mode = WAL");
|
|
408
|
+
db.exec(SCHEMA_SQL);
|
|
409
|
+
const insertStmt = db.prepare(`
|
|
410
|
+
INSERT INTO evidence (
|
|
411
|
+
id, ts, modality, model, system_id, prompt_hash, output_hash,
|
|
412
|
+
output_hash_normalized, hash_version, marking_methods, manifest_ref,
|
|
413
|
+
disclosure_context, prev_hash, record_hash
|
|
414
|
+
) VALUES (
|
|
415
|
+
@id, @ts, @modality, @model, @system_id, @prompt_hash, @output_hash,
|
|
416
|
+
@output_hash_normalized, @hash_version, @marking_methods, @manifest_ref,
|
|
417
|
+
@disclosure_context, @prev_hash, @record_hash
|
|
418
|
+
)
|
|
419
|
+
`);
|
|
420
|
+
const lastHashStmt = db.prepare("SELECT record_hash FROM evidence ORDER BY seq DESC LIMIT 1");
|
|
421
|
+
const allStmt = db.prepare("SELECT * FROM evidence ORDER BY seq");
|
|
422
|
+
const byOutputHashStmt = db.prepare("SELECT * FROM evidence WHERE output_hash = ? ORDER BY seq");
|
|
423
|
+
const byTextStmt = db.prepare(
|
|
424
|
+
"SELECT * FROM evidence WHERE output_hash = @raw OR output_hash_normalized = @normalized ORDER BY seq"
|
|
425
|
+
);
|
|
426
|
+
const countStmt = db.prepare("SELECT COUNT(*) AS n FROM evidence");
|
|
427
|
+
function filteredRows(filter) {
|
|
428
|
+
const { where, params, normalized } = buildFilter(filter);
|
|
429
|
+
const stmt = db.prepare(`SELECT * FROM evidence${where} ORDER BY seq`);
|
|
430
|
+
return { rows: stmt.iterate(params), normalized };
|
|
431
|
+
}
|
|
432
|
+
function verifyChainImpl() {
|
|
433
|
+
let checked = 0;
|
|
434
|
+
let prevHash = null;
|
|
435
|
+
for (const row of allStmt.iterate()) {
|
|
436
|
+
checked += 1;
|
|
437
|
+
const { record_hash, ...body } = rowToRecord(row);
|
|
438
|
+
if (body.prev_hash !== prevHash || computeRecordHash(body) !== record_hash) {
|
|
439
|
+
return { ok: false, checked, brokenAtSeq: row.seq };
|
|
440
|
+
}
|
|
441
|
+
prevHash = record_hash;
|
|
442
|
+
}
|
|
443
|
+
return { ok: true, checked };
|
|
444
|
+
}
|
|
445
|
+
const runAppend = db.transaction(
|
|
446
|
+
(body) => {
|
|
447
|
+
const last = lastHashStmt.get();
|
|
448
|
+
const chained = {
|
|
449
|
+
...body,
|
|
450
|
+
prev_hash: last ? last.record_hash : null
|
|
451
|
+
};
|
|
452
|
+
const record = { ...chained, record_hash: computeRecordHash(chained) };
|
|
453
|
+
insertStmt.run({ ...record, marking_methods: JSON.stringify(record.marking_methods) });
|
|
454
|
+
return record;
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
return {
|
|
458
|
+
append(input) {
|
|
459
|
+
const modality = input.modality;
|
|
460
|
+
if (typeof modality !== "string" || !MODALITIES.has(modality)) {
|
|
461
|
+
throw new TypeError(
|
|
462
|
+
"gsengai: modality must be one of 'text' | 'image' | 'audio' | 'video'"
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const body = {
|
|
466
|
+
id: (0, import_node_crypto2.randomUUID)(),
|
|
467
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
468
|
+
modality,
|
|
469
|
+
model: requireNonEmptyString(input.model, "model"),
|
|
470
|
+
system_id: requireNonEmptyString(input.systemId, "systemId"),
|
|
471
|
+
prompt_hash: input.promptHash ?? null,
|
|
472
|
+
...resolveOutputHashes(input),
|
|
473
|
+
marking_methods: input.markingMethods ?? ["logging"],
|
|
474
|
+
manifest_ref: input.manifestRef ?? null,
|
|
475
|
+
disclosure_context: input.disclosureContext ?? null
|
|
476
|
+
};
|
|
477
|
+
return runAppend.immediate(body);
|
|
478
|
+
},
|
|
479
|
+
findByOutputHash(hash) {
|
|
480
|
+
const rows = byOutputHashStmt.all(hash);
|
|
481
|
+
return rows.map(rowToRecord);
|
|
482
|
+
},
|
|
483
|
+
findByText(text) {
|
|
484
|
+
const hashes = hashText(text);
|
|
485
|
+
const rows = byTextStmt.all({
|
|
486
|
+
raw: hashes.outputHash,
|
|
487
|
+
normalized: hashes.outputHashNormalized
|
|
488
|
+
});
|
|
489
|
+
return rows.map(rowToRecord);
|
|
490
|
+
},
|
|
491
|
+
async exportJsonl(path) {
|
|
492
|
+
const out = (0, import_node_fs.createWriteStream)(path, { encoding: "utf8" });
|
|
493
|
+
let records = 0;
|
|
494
|
+
try {
|
|
495
|
+
for (const row of allStmt.iterate()) {
|
|
496
|
+
const line = `${canonicalJson(rowToRecord(row))}
|
|
497
|
+
`;
|
|
498
|
+
if (!out.write(line)) {
|
|
499
|
+
await (0, import_node_events.once)(out, "drain");
|
|
500
|
+
}
|
|
501
|
+
records += 1;
|
|
502
|
+
}
|
|
503
|
+
} finally {
|
|
504
|
+
out.end();
|
|
505
|
+
}
|
|
506
|
+
await (0, import_node_events.once)(out, "close");
|
|
507
|
+
return { records, path };
|
|
508
|
+
},
|
|
509
|
+
async exportCsv(path, filter) {
|
|
510
|
+
const { rows } = filteredRows(filter);
|
|
511
|
+
const out = (0, import_node_fs.createWriteStream)(path, { encoding: "utf8" });
|
|
512
|
+
let records = 0;
|
|
513
|
+
try {
|
|
514
|
+
if (!out.write(`${CSV_COLUMNS.join(",")}
|
|
515
|
+
`)) {
|
|
516
|
+
await (0, import_node_events.once)(out, "drain");
|
|
517
|
+
}
|
|
518
|
+
for (const row of rows) {
|
|
519
|
+
const line = `${recordToCsvRow(rowToRecord(row))}
|
|
520
|
+
`;
|
|
521
|
+
if (!out.write(line)) {
|
|
522
|
+
await (0, import_node_events.once)(out, "drain");
|
|
523
|
+
}
|
|
524
|
+
records += 1;
|
|
525
|
+
}
|
|
526
|
+
} finally {
|
|
527
|
+
out.end();
|
|
528
|
+
}
|
|
529
|
+
await (0, import_node_events.once)(out, "close");
|
|
530
|
+
return { records, path };
|
|
531
|
+
},
|
|
532
|
+
buildAuditReport(opts = {}) {
|
|
533
|
+
const { rows, normalized } = filteredRows(opts.filter);
|
|
534
|
+
const records = [...rows].map(rowToRecord);
|
|
535
|
+
const integrity = verifyChainImpl();
|
|
536
|
+
const markdown = renderAuditReport({
|
|
537
|
+
records,
|
|
538
|
+
integrity,
|
|
539
|
+
storePath,
|
|
540
|
+
filter: normalized,
|
|
541
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
542
|
+
});
|
|
543
|
+
if (opts.path !== void 0) {
|
|
544
|
+
(0, import_node_fs.writeFileSync)(opts.path, markdown, "utf8");
|
|
545
|
+
}
|
|
546
|
+
return { markdown, records: records.length, integrity, path: opts.path ?? null };
|
|
547
|
+
},
|
|
548
|
+
verifyChain: verifyChainImpl,
|
|
549
|
+
count() {
|
|
550
|
+
const row = countStmt.get();
|
|
551
|
+
return row.n;
|
|
552
|
+
},
|
|
553
|
+
close() {
|
|
554
|
+
db.close();
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/cli.ts
|
|
560
|
+
var USAGE = `gsengai-audit \u2014 export audit artifacts from an gsengai evidence store.
|
|
561
|
+
Supports compliance with EU AI Act Article 50 and California SB 942. Exports
|
|
562
|
+
contain hashes and metadata only; raw content is never stored or exported.
|
|
563
|
+
Not legal advice.
|
|
564
|
+
|
|
565
|
+
Usage:
|
|
566
|
+
gsengai-audit export --store <evidence.db> --format csv|report --out <path>
|
|
567
|
+
[--system-id <id>] [--modality text|image|audio|video]
|
|
568
|
+
[--since <ISO timestamp>] [--until <ISO timestamp>]
|
|
569
|
+
|
|
570
|
+
Formats:
|
|
571
|
+
csv streaming CSV of evidence records (fixed schema-v1 column order)
|
|
572
|
+
report human-readable Markdown audit report (integrity, summary, records)
|
|
573
|
+
|
|
574
|
+
Tip: render the report Markdown to PDF/HTML yourself before handing it to
|
|
575
|
+
counsel (e.g. pandoc report.md -o report.pdf).`;
|
|
576
|
+
var MODALITIES2 = /* @__PURE__ */ new Set(["text", "image", "audio", "video"]);
|
|
577
|
+
function parseCliArgs(argv) {
|
|
578
|
+
return (0, import_node_util.parseArgs)({
|
|
579
|
+
args: argv,
|
|
580
|
+
allowPositionals: true,
|
|
581
|
+
options: {
|
|
582
|
+
store: { type: "string" },
|
|
583
|
+
format: { type: "string" },
|
|
584
|
+
out: { type: "string" },
|
|
585
|
+
"system-id": { type: "string" },
|
|
586
|
+
modality: { type: "string" },
|
|
587
|
+
since: { type: "string" },
|
|
588
|
+
until: { type: "string" },
|
|
589
|
+
help: { type: "boolean" }
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
async function runAuditCli(argv, io = console) {
|
|
594
|
+
let parsed;
|
|
595
|
+
try {
|
|
596
|
+
parsed = parseCliArgs(argv);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
io.error(err instanceof Error ? err.message : String(err));
|
|
599
|
+
io.error(USAGE);
|
|
600
|
+
return 1;
|
|
601
|
+
}
|
|
602
|
+
const { values, positionals } = parsed;
|
|
603
|
+
if (values.help === true) {
|
|
604
|
+
io.log(USAGE);
|
|
605
|
+
return 0;
|
|
606
|
+
}
|
|
607
|
+
if (positionals[0] !== "export" || positionals.length !== 1) {
|
|
608
|
+
io.error("gsengai-audit: the only supported command is 'export'");
|
|
609
|
+
io.error(USAGE);
|
|
610
|
+
return 1;
|
|
611
|
+
}
|
|
612
|
+
const storePath = values.store;
|
|
613
|
+
const format = values.format;
|
|
614
|
+
const outPath = values.out;
|
|
615
|
+
if (storePath === void 0 || format === void 0 || outPath === void 0) {
|
|
616
|
+
io.error("gsengai-audit: --store, --format, and --out are required");
|
|
617
|
+
io.error(USAGE);
|
|
618
|
+
return 1;
|
|
619
|
+
}
|
|
620
|
+
if (format !== "csv" && format !== "report") {
|
|
621
|
+
io.error(`gsengai-audit: unknown --format '${format}' (expected csv or report)`);
|
|
622
|
+
return 1;
|
|
623
|
+
}
|
|
624
|
+
if (!(0, import_node_fs2.existsSync)(storePath)) {
|
|
625
|
+
io.error(`gsengai-audit: no evidence store found at '${storePath}'`);
|
|
626
|
+
return 1;
|
|
627
|
+
}
|
|
628
|
+
const modality = values.modality;
|
|
629
|
+
if (modality !== void 0 && !MODALITIES2.has(modality)) {
|
|
630
|
+
io.error(`gsengai-audit: unknown --modality '${modality}' (expected text|image|audio|video)`);
|
|
631
|
+
return 1;
|
|
632
|
+
}
|
|
633
|
+
const filter = {};
|
|
634
|
+
if (values["system-id"] !== void 0) {
|
|
635
|
+
filter.systemId = values["system-id"];
|
|
636
|
+
}
|
|
637
|
+
if (modality !== void 0) {
|
|
638
|
+
filter.modality = modality;
|
|
639
|
+
}
|
|
640
|
+
if (values.since !== void 0) {
|
|
641
|
+
filter.since = values.since;
|
|
642
|
+
}
|
|
643
|
+
if (values.until !== void 0) {
|
|
644
|
+
filter.until = values.until;
|
|
645
|
+
}
|
|
646
|
+
const store = createEvidenceStore({ path: storePath });
|
|
647
|
+
try {
|
|
648
|
+
if (format === "csv") {
|
|
649
|
+
const result = await store.exportCsv(outPath, filter);
|
|
650
|
+
io.log(`CSV export: ${result.records} record(s) -> ${result.path}`);
|
|
651
|
+
} else {
|
|
652
|
+
const report = store.buildAuditReport({ path: outPath, filter });
|
|
653
|
+
io.log(`Audit report: ${report.records} record(s) -> ${outPath}`);
|
|
654
|
+
if (report.integrity.ok) {
|
|
655
|
+
io.log(`Integrity: chain verified (${report.integrity.checked} record(s) checked)`);
|
|
656
|
+
} else {
|
|
657
|
+
io.error(
|
|
658
|
+
`WARNING: evidence hash chain BROKEN at seq ${report.integrity.brokenAtSeq} \u2014 the Integrity section of the report records the break.`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} catch (err) {
|
|
663
|
+
io.error(`gsengai-audit: export failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
664
|
+
return 1;
|
|
665
|
+
} finally {
|
|
666
|
+
store.close();
|
|
667
|
+
}
|
|
668
|
+
return 0;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/fail.ts
|
|
672
|
+
var lostRecordCount = 0;
|
|
673
|
+
function getLostRecordCount() {
|
|
674
|
+
return lostRecordCount;
|
|
675
|
+
}
|
|
676
|
+
function resetLostRecordCount() {
|
|
677
|
+
lostRecordCount = 0;
|
|
678
|
+
}
|
|
679
|
+
function safeAppend(store, input, failMode = "open") {
|
|
680
|
+
try {
|
|
681
|
+
return store.append(typeof input === "function" ? input() : input);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
if (failMode === "strict") {
|
|
684
|
+
throw err;
|
|
685
|
+
}
|
|
686
|
+
lostRecordCount += 1;
|
|
687
|
+
console.warn(
|
|
688
|
+
`[gsengai] Evidence record LOST (fail-open): appending to the evidence store failed; the model response was still returned. Lost records in this process so far: ${lostRecordCount}. Opt in to failMode: 'strict' to surface these failures as errors.`,
|
|
689
|
+
err
|
|
690
|
+
);
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/iterable.ts
|
|
696
|
+
function instrumentAsyncIterable(stream, hooks) {
|
|
697
|
+
let finalized = false;
|
|
698
|
+
const finalize = () => {
|
|
699
|
+
if (finalized) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
finalized = true;
|
|
703
|
+
hooks.onDone();
|
|
704
|
+
};
|
|
705
|
+
return new Proxy(stream, {
|
|
706
|
+
get(target, prop) {
|
|
707
|
+
if (prop === Symbol.asyncIterator) {
|
|
708
|
+
return () => {
|
|
709
|
+
const source = target[Symbol.asyncIterator]();
|
|
710
|
+
return {
|
|
711
|
+
async next(...args) {
|
|
712
|
+
let result;
|
|
713
|
+
try {
|
|
714
|
+
result = await source.next(...args);
|
|
715
|
+
} catch (err) {
|
|
716
|
+
finalize();
|
|
717
|
+
throw err;
|
|
718
|
+
}
|
|
719
|
+
if (result.done) {
|
|
720
|
+
finalize();
|
|
721
|
+
} else {
|
|
722
|
+
hooks.onValue(result.value);
|
|
723
|
+
}
|
|
724
|
+
return result;
|
|
725
|
+
},
|
|
726
|
+
async return(value2) {
|
|
727
|
+
finalize();
|
|
728
|
+
if (source.return) {
|
|
729
|
+
return source.return(value2);
|
|
730
|
+
}
|
|
731
|
+
return { done: true, value: value2 };
|
|
732
|
+
},
|
|
733
|
+
async throw(err) {
|
|
734
|
+
finalize();
|
|
735
|
+
if (source.throw) {
|
|
736
|
+
return source.throw(err);
|
|
737
|
+
}
|
|
738
|
+
throw err;
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
const value = Reflect.get(target, prop, target);
|
|
744
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
749
|
+
0 && (module.exports = {
|
|
750
|
+
CSV_COLUMNS,
|
|
751
|
+
HASH_VERSION,
|
|
752
|
+
NotImplementedError,
|
|
753
|
+
PRD_S9_LIMITS,
|
|
754
|
+
canonicalJson,
|
|
755
|
+
createEvidenceStore,
|
|
756
|
+
getLostRecordCount,
|
|
757
|
+
hashPrompt,
|
|
758
|
+
hashText,
|
|
759
|
+
instrumentAsyncIterable,
|
|
760
|
+
normalizeText,
|
|
761
|
+
renderAuditReport,
|
|
762
|
+
resetLostRecordCount,
|
|
763
|
+
runAuditCli,
|
|
764
|
+
safeAppend,
|
|
765
|
+
sha256Hex
|
|
766
|
+
});
|
|
767
|
+
//# sourceMappingURL=index.cjs.map
|