@tangle-network/agent-app 0.45.58 → 0.45.59

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.
@@ -7,6 +7,43 @@ async function runSqliteStatements(db, statements) {
7
7
  for (const statement of statements) results.push(await statement);
8
8
  return results;
9
9
  }
10
+ async function runAtomicSqliteStatements(db, statements) {
11
+ if (statements.some((statement) => typeof statement !== "function")) {
12
+ throw new TypeError(
13
+ "runAtomicSqliteStatements: every statement must be a lazy function that receives the transaction connection"
14
+ );
15
+ }
16
+ if (typeof db.transaction === "function") {
17
+ return await db.transaction(async (connection2) => {
18
+ const results = [];
19
+ for (const statement of statements) results.push(await connection2.execute(statement));
20
+ return results;
21
+ });
22
+ }
23
+ const connection = db.fallbackConnection;
24
+ if (!connection || typeof connection.exec !== "function" || typeof connection.execute !== "function") {
25
+ throw new Error(
26
+ "runAtomicSqliteStatements: the injected driver must expose transaction() or one fallbackConnection with exec() and execute()"
27
+ );
28
+ }
29
+ await connection.exec("BEGIN IMMEDIATE");
30
+ try {
31
+ const results = [];
32
+ for (const statement of statements) results.push(await connection.execute(statement));
33
+ await connection.exec("COMMIT");
34
+ return results;
35
+ } catch (error) {
36
+ try {
37
+ await connection.exec("ROLLBACK");
38
+ } catch (rollbackError) {
39
+ throw new AggregateError(
40
+ [error, rollbackError],
41
+ "runAtomicSqliteStatements: statement execution and rollback both failed"
42
+ );
43
+ }
44
+ throw error;
45
+ }
46
+ }
10
47
  function createDatabaseProvider(options = {}) {
11
48
  const message = options.notReadyMessage ?? "Database not initialized \u2014 call setDatabase() first.";
12
49
  let current = null;
@@ -61,7 +98,8 @@ function createInMemoryKV(initial) {
61
98
 
62
99
  export {
63
100
  runSqliteStatements,
101
+ runAtomicSqliteStatements,
64
102
  createDatabaseProvider,
65
103
  createInMemoryKV
66
104
  };
67
- //# sourceMappingURL=chunk-LRHVCVEW.js.map
105
+ //# sourceMappingURL=chunk-Q4ZER4HI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/index.ts"],"sourcesContent":["/**\n * Swappable database provider — the seam that decouples the agent's persistence\n * from any one driver.\n *\n * The agent core (and the app's server modules) import a single `db` and use it\n * directly. That `db` is a lazy proxy: it forwards to whatever database instance\n * the runtime injects via {@link DatabaseProvider.setDatabase}. So the SAME core\n * runs on:\n * - Cloudflare D1 (`setDatabase(drizzle(d1, schema))`) — prod\n * - SQLite / miniflare (`setDatabase(drizzle(betterSqlite, schema))`) — eval / the portable inner shell\n * - libsql / Turso, Postgres (`setDatabase(drizzle(client, schema))`) — a future hosted DB\n *\n * Adding a new database is one adapter (a drizzle instance over a new driver) +\n * a `setDatabase` call. None of the modules importing `db` change. Substrate-\n * free and driver-agnostic: this module knows nothing about D1, drizzle, or any\n * schema — it only forwards property access to the injected instance.\n */\n\nexport interface DatabaseProvider<DB> {\n /** The injected database, as a lazy proxy. Throws (with `notReadyMessage`)\n * on any access before {@link setDatabase} is called. */\n readonly db: DB\n /** Inject the active database instance (any driver's client). */\n setDatabase(database: DB): void\n /** True once a database has been injected. */\n isReady(): boolean\n /** Clear the injected database (next access throws again). Mainly for tests. */\n reset(): void\n}\n\n/** Define options for configuring database provider behavior including error messaging */\nexport interface DatabaseProviderOptions {\n /** Error thrown when `db` is accessed before injection. Keep the product's\n * existing wording so callers see a familiar message. */\n notReadyMessage?: string\n}\n\n/** A database driver that can execute related SQLite statements as one batch.\n * Cloudflare D1 and libsql expose this method; portable local drivers may not. */\nexport interface SqliteBatchDatabase {\n batch?: (statements: [unknown, ...unknown[]]) => Promise<unknown[]>\n}\n\n/** The three raw SQL control statements used by the explicit atomic path. */\nexport type SqliteTransactionCommand = 'BEGIN IMMEDIATE' | 'COMMIT' | 'ROLLBACK'\n\n/** A statement that has not started before the transaction opens. The callback\n * receives the transaction connection, never a detached query promise. */\nexport type SqliteLazyStatement<T = unknown> = (connection: SqliteAtomicConnection) => T | Promise<T>\n\n/** The single connection exposed inside a native transaction callback. */\nexport interface SqliteAtomicConnection {\n /** Execute one lazy operation on this transaction's connection. */\n execute(statement: SqliteLazyStatement): unknown | Promise<unknown>\n}\n\n/** A manually controlled connection owns both transaction commands and queries.\n * Keeping them on one value prevents a transaction from spanning two handles. */\nexport interface SqliteManualTransactionConnection extends SqliteAtomicConnection {\n exec(command: SqliteTransactionCommand): unknown | Promise<unknown>\n}\n\n/**\n * A SQLite driver that can execute a group of statements atomically.\n *\n * Drivers should expose `transaction`; its callback receives the one connection\n * that owns the transaction. A portable driver may instead expose one\n * `fallbackConnection` containing both `exec` and `execute`. The fallback uses\n * lazy operations, so no query can start before `BEGIN IMMEDIATE`.\n */\nexport interface AtomicSqliteDatabase {\n transaction?: (\n callback: (connection: SqliteAtomicConnection) => unknown[] | Promise<unknown[]>,\n ) => unknown[] | Promise<unknown[]>\n fallbackConnection?: SqliteManualTransactionConnection\n}\n\n/** Execute related SQLite statements in one transactional driver batch when\n * supported, or sequentially in the same order for portable local drivers. */\nexport async function runSqliteStatements(\n db: SqliteBatchDatabase,\n statements: [unknown, ...unknown[]],\n): Promise<unknown[]> {\n if (typeof db.batch === 'function') {\n return await db.batch(statements)\n }\n const results: unknown[] = []\n for (const statement of statements) results.push(await statement)\n return results\n}\n\n/**\n * Execute related SQLite statements atomically.\n *\n * This helper is intentionally separate from {@link runSqliteStatements}.\n * The older helper preserves its portable sequential fallback; this helper\n * fails closed when the injected driver cannot prove atomicity.\n */\nexport async function runAtomicSqliteStatements(\n db: AtomicSqliteDatabase,\n statements: [SqliteLazyStatement, ...SqliteLazyStatement[]],\n): Promise<unknown[]> {\n if (statements.some((statement) => typeof statement !== 'function')) {\n throw new TypeError(\n 'runAtomicSqliteStatements: every statement must be a lazy function that receives the transaction connection',\n )\n }\n\n if (typeof db.transaction === 'function') {\n return await db.transaction(async (connection) => {\n const results: unknown[] = []\n for (const statement of statements) results.push(await connection.execute(statement))\n return results\n })\n }\n\n const connection = db.fallbackConnection\n if (\n !connection ||\n typeof connection.exec !== 'function' ||\n typeof connection.execute !== 'function'\n ) {\n throw new Error(\n 'runAtomicSqliteStatements: the injected driver must expose transaction() or one fallbackConnection with exec() and execute()',\n )\n }\n\n await connection.exec('BEGIN IMMEDIATE')\n try {\n const results: unknown[] = []\n for (const statement of statements) results.push(await connection.execute(statement))\n await connection.exec('COMMIT')\n return results\n } catch (error) {\n try {\n await connection.exec('ROLLBACK')\n } catch (rollbackError) {\n throw new AggregateError(\n [error, rollbackError],\n 'runAtomicSqliteStatements: statement execution and rollback both failed',\n )\n }\n throw error\n }\n}\n\n/**\n * Create a swappable database provider. `DB` is the injected instance's type\n * (e.g. a drizzle `Database`); the proxy is typed as `DB` so callers keep full\n * typing and their existing query syntax.\n */\nexport function createDatabaseProvider<DB extends object>(\n options: DatabaseProviderOptions = {},\n): DatabaseProvider<DB> {\n const message = options.notReadyMessage ?? 'Database not initialized — call setDatabase() first.'\n let current: DB | null = null\n\n const db = new Proxy({} as DB, {\n get(_target, prop) {\n if (!current) throw new Error(message)\n const value = (current as Record<string | symbol, unknown>)[prop]\n // Bind methods to the real instance so `this` resolves correctly through\n // the proxy (works for drizzle's query builders and class-based stores).\n return typeof value === 'function' ? (value as (...args: unknown[]) => unknown).bind(current) : value\n },\n has(_target, prop) {\n return current !== null && prop in (current as object)\n },\n })\n\n return {\n db,\n setDatabase(database: DB) {\n current = database\n },\n isReady() {\n return current !== null\n },\n reset() {\n current = null\n },\n }\n}\n\n// ── KV store port (the vault backend) ───────────────────────────────────────\n//\n// The vault (workspace files) is a key/value store. In production it's a\n// Cloudflare `KVNamespace`; the portable inner shell injects an in-memory (or\n// other) implementation. This is the subset of the KV API the vault uses —\n// `KVNamespace` satisfies it structurally, so prod passes the binding unchanged,\n// and `createInMemoryKV()` supplies the portable adapter for sandbox/eval.\n\n/** Describe the result of listing keys with completion status and optional pagination cursor */\nexport interface KVListResult {\n keys: { name: string }[]\n list_complete: boolean\n cursor?: string\n}\n\n/** Define options for storing a key-value pair with expiration and metadata settings */\nexport interface KVPutOptions {\n expiration?: number\n expirationTtl?: number\n metadata?: unknown\n}\n\n/** Resolve a key-value pair retrieval including its associated metadata and value */\nexport interface KVGetWithMetadataResult {\n value: string | null\n metadata: unknown | null\n}\n\n/** Define a key-value store interface for asynchronous data retrieval, storage, deletion, and listing */\nexport interface KVStore {\n get(key: string): Promise<string | null>\n /** Read a value with its stored metadata (e.g. the vault's encrypted/hasPII flags). */\n getWithMetadata(key: string): Promise<KVGetWithMetadataResult>\n put(key: string, value: string, options?: KVPutOptions): Promise<void>\n delete(key: string): Promise<void>\n list(options?: { prefix?: string; cursor?: string; limit?: number }): Promise<KVListResult>\n}\n\n/**\n * In-memory {@link KVStore} — the portable vault backend for sandbox/eval runs.\n * Backed by a Map; `list` returns all prefix-matched keys in one complete page\n * (no real pagination needed in-process). Seed with `initial` entries if useful.\n */\nexport function createInMemoryKV(initial?: Record<string, string>): KVStore {\n const store = new Map<string, { value: string; metadata: unknown }>(\n initial ? Object.entries(initial).map(([k, v]) => [k, { value: v, metadata: null }]) : [],\n )\n return {\n async get(key) {\n return store.get(key)?.value ?? null\n },\n async getWithMetadata(key) {\n const entry = store.get(key)\n return { value: entry?.value ?? null, metadata: entry?.metadata ?? null }\n },\n async put(key, value, options) {\n store.set(key, { value, metadata: options?.metadata ?? null })\n },\n async delete(key) {\n store.delete(key)\n },\n async list(options) {\n const prefix = options?.prefix ?? ''\n const keys = [...store.keys()]\n .filter((k) => k.startsWith(prefix))\n .sort()\n .map((name) => ({ name }))\n return { keys, list_complete: true }\n },\n }\n}\n"],"mappings":"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","names":["connection"]}
@@ -59,6 +59,7 @@ export interface RedactForIngestionOptions {
59
59
  * path. Each span becomes `[REDACTED:<kind>]`.
60
60
  */
61
61
  export declare function maskSpans(text: string, patterns?: readonly RedactionPattern[]): string;
62
+ export declare function redactErrorMessage(input: unknown, fallback?: string): string;
62
63
  /**
63
64
  * One-way PII scrub for telemetry/ingestion. Backward-compatible: called with no
64
65
  * options it behaves exactly as before (SSN/EIN strings + sensitive object keys
@@ -1,124 +1,18 @@
1
- // src/redact/index.ts
2
- var DEFAULT_REDACTION_PATTERNS = [
3
- { kind: "ssn", pattern: /\d{3}-\d{2}-\d{4}/ },
4
- { kind: "ein", pattern: /\d{2}-\d{7}/ }
5
- ];
6
- var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
7
- "ssn",
8
- "ein",
9
- "password",
10
- "apikey",
11
- "token",
12
- "secret",
13
- "authorization",
14
- "email",
15
- "phone"
16
- ]);
17
- function redactString(value, patterns) {
18
- for (const { kind, pattern, validate } of patterns) {
19
- if (!validate) {
20
- if (pattern.test(value)) return `[REDACTED:${kind}]`;
21
- continue;
22
- }
23
- const g = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
24
- for (const m of value.matchAll(g)) {
25
- if (m[0].length > 0 && validate(m[0])) return `[REDACTED:${kind}]`;
26
- }
27
- }
28
- return value;
29
- }
30
- function maskSpans(text, patterns = DEFAULT_REDACTION_PATTERNS) {
31
- const spans = detectSpans(text, patterns);
32
- if (spans.length === 0) return text;
33
- let out = "";
34
- let pos = 0;
35
- for (const s of spans) {
36
- if (s.start > pos) out += text.slice(pos, s.start);
37
- out += `[REDACTED:${s.kind}]`;
38
- pos = s.end;
39
- }
40
- if (pos < text.length) out += text.slice(pos);
41
- return out;
42
- }
43
- function isPlainObject(value) {
44
- if (value === null || typeof value !== "object") return false;
45
- const proto = Object.getPrototypeOf(value);
46
- return proto === Object.prototype || proto === null;
47
- }
48
- function redactForIngestion(value, options = {}) {
49
- const patterns = options.extraPatterns ? [...DEFAULT_REDACTION_PATTERNS, ...options.extraPatterns] : DEFAULT_REDACTION_PATTERNS;
50
- const sensitiveKeys = options.extraSensitiveKeys ? /* @__PURE__ */ new Set([...SENSITIVE_KEYS, ...options.extraSensitiveKeys.map((k) => k.toLowerCase())]) : SENSITIVE_KEYS;
51
- const maskString = options.stringMode === "mask-spans" ? (s) => maskSpans(s, patterns) : (s) => redactString(s, patterns);
52
- const seen = /* @__PURE__ */ new WeakSet();
53
- const walk = (v) => {
54
- if (typeof v === "string") return maskString(v);
55
- if (Array.isArray(v)) {
56
- if (seen.has(v)) return v;
57
- seen.add(v);
58
- return v.map(walk);
59
- }
60
- if (isPlainObject(v)) {
61
- if (seen.has(v)) return v;
62
- seen.add(v);
63
- const out = {};
64
- for (const [k, val] of Object.entries(v)) {
65
- out[k] = sensitiveKeys.has(k.toLowerCase()) ? "[REDACTED:field]" : walk(val);
66
- }
67
- return out;
68
- }
69
- return v;
70
- };
71
- return walk(value);
72
- }
73
- function detectSpans(text, patterns = DEFAULT_REDACTION_PATTERNS) {
74
- const raw = [];
75
- for (const { kind, pattern, validate } of patterns) {
76
- const g = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
77
- for (const m of text.matchAll(g)) {
78
- if (m.index === void 0 || m[0].length === 0) continue;
79
- if (validate && !validate(m[0])) continue;
80
- raw.push({ kind, start: m.index, end: m.index + m[0].length, text: m[0] });
81
- }
82
- }
83
- raw.sort((a, b) => a.start - b.start || b.end - a.end);
84
- const spans = [];
85
- let cursor = -1;
86
- let i = 0;
87
- for (const s of raw) {
88
- if (s.start < cursor) continue;
89
- spans.push({ id: `span-${i++}`, ...s });
90
- cursor = s.end;
91
- }
92
- return spans;
93
- }
94
- async function buildRedactedDocument(text, options) {
95
- const spans = detectSpans(text, options.patterns);
96
- const segments = [];
97
- let pos = 0;
98
- for (const span of spans) {
99
- if (span.start > pos) segments.push({ type: "text", text: text.slice(pos, span.start) });
100
- segments.push({ type: "redacted", id: span.id, kind: span.kind, cipher: await options.encrypt(span.text) });
101
- pos = span.end;
102
- }
103
- if (pos < text.length) segments.push({ type: "text", text: text.slice(pos) });
104
- return { segments };
105
- }
106
- async function revealSpan(doc, spanId, options) {
107
- const seg = doc.segments.find(
108
- (s) => s.type === "redacted" && s.id === spanId
109
- );
110
- if (!seg) return { ok: false, reason: "not_found" };
111
- const allowed = await options.canReveal({ id: seg.id, kind: seg.kind });
112
- if (!allowed) return { ok: false, reason: "forbidden" };
113
- const value = await options.decrypt(seg.cipher);
114
- if (options.onReveal) await options.onReveal({ id: seg.id, kind: seg.kind });
115
- return { ok: true, value };
116
- }
1
+ import {
2
+ DEFAULT_REDACTION_PATTERNS,
3
+ buildRedactedDocument,
4
+ detectSpans,
5
+ maskSpans,
6
+ redactErrorMessage,
7
+ redactForIngestion,
8
+ revealSpan
9
+ } from "../chunk-CDC5HFKL.js";
117
10
  export {
118
11
  DEFAULT_REDACTION_PATTERNS,
119
12
  buildRedactedDocument,
120
13
  detectSpans,
121
14
  maskSpans,
15
+ redactErrorMessage,
122
16
  redactForIngestion,
123
17
  revealSpan
124
18
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/redact/index.ts"],"sourcesContent":["/**\n * PII redaction — two complementary modes.\n *\n * 1. ONE-WAY scrub (`redactForIngestion`): for production trace payloads. Tool\n * args + results (and once, the LLM span's prompt) cross the wire into the\n * ingestion store, which also feeds the analyst-loop's LLM prompts, so\n * personal identifiers MUST be stripped before they leave the request path.\n * Destructive — the original is gone, replaced by a sentinel.\n *\n * 2. REVERSIBLE redaction (`buildRedactedDocument` / `revealSpan`): for the UI.\n * A document is split into text + redacted segments; each redacted original\n * is kept ENCRYPTED (via a caller-supplied `encrypt` seam → `agent-app/crypto`)\n * so a viewer can reveal a single span on demand, gated by an authorization\n * callback and an audit hook. The mask is presentation; the original is\n * recoverable by an authorized reveal, not lost.\n *\n * Discipline: cheap deterministic string patterns + well-known sensitive object\n * keys (value replaced, key kept, so the shape stays debuggable); recurse arrays\n * + plain objects only; NEVER throw on the one-way path.\n */\n\n/** A named PII pattern. `pattern` is matched case-insensitively at the string\n * level; keep it non-global (global instances are derived where needed). */\nexport interface RedactionPattern {\n kind: string\n pattern: RegExp\n /** Optional predicate over each match — the pattern fires only when it returns\n * true. For matches a regex alone can't decide (e.g. a Luhn check on a\n * card-number candidate). When set, the value is scanned globally and the\n * first match that passes wins; when absent, a plain `pattern.test` decides. */\n validate?: (match: string) => boolean\n}\n\n/** The default deterministic patterns. Extend via the `extraPatterns` /\n * `patterns` options rather than forking this module (the seam that lets a\n * product add e.g. a credit-card matcher without a local copy). */\nexport const DEFAULT_REDACTION_PATTERNS: readonly RedactionPattern[] = [\n { kind: 'ssn', pattern: /\\d{3}-\\d{2}-\\d{4}/ },\n { kind: 'ein', pattern: /\\d{2}-\\d{7}/ },\n]\n\nconst SENSITIVE_KEYS = new Set([\n 'ssn',\n 'ein',\n 'password',\n 'apikey',\n 'token',\n 'secret',\n 'authorization',\n 'email',\n 'phone',\n])\n\n/** Define options to customize sensitive data redaction patterns and key names for ingestion */\nexport interface RedactForIngestionOptions {\n /** Extra patterns appended to {@link DEFAULT_REDACTION_PATTERNS} for the\n * string-level scrub (e.g. credit-card). Additive — defaults still apply. */\n extraPatterns?: readonly RedactionPattern[]\n /** Extra sensitive object-key names (case-insensitive) added to the built-in\n * set, e.g. the snake_case `api_key` an intake form uses. Additive. */\n extraSensitiveKeys?: readonly string[]\n /**\n * How a matched string is rewritten:\n * - `'collapse'` (default) — the whole string becomes `[REDACTED:<kind>]` on\n * the first matching pattern. Safest for telemetry: nothing of the original\n * survives.\n * - `'mask-spans'` — only the matched substrings are replaced (each with\n * `[REDACTED:<kind>]`), preserving surrounding text. Use when a downstream\n * reader needs the non-PII context (e.g. an analyst loop reading prose).\n */\n stringMode?: 'collapse' | 'mask-spans'\n}\n\nfunction redactString(value: string, patterns: readonly RedactionPattern[]): string {\n for (const { kind, pattern, validate } of patterns) {\n if (!validate) {\n if (pattern.test(value)) return `[REDACTED:${kind}]`\n continue\n }\n const g = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)\n for (const m of value.matchAll(g)) {\n if (m[0].length > 0 && validate(m[0])) return `[REDACTED:${kind}]`\n }\n }\n return value\n}\n\n/**\n * Replace only the PII substrings in `text`, preserving everything around them\n * (the `mask-spans` string mode). Built on {@link detectSpans} so matching,\n * non-overlap, and `validate` predicates behave identically to the reversible\n * path. Each span becomes `[REDACTED:<kind>]`.\n */\nexport function maskSpans(\n text: string,\n patterns: readonly RedactionPattern[] = DEFAULT_REDACTION_PATTERNS,\n): string {\n const spans = detectSpans(text, patterns)\n if (spans.length === 0) return text\n let out = ''\n let pos = 0\n for (const s of spans) {\n if (s.start > pos) out += text.slice(pos, s.start)\n out += `[REDACTED:${s.kind}]`\n pos = s.end\n }\n if (pos < text.length) out += text.slice(pos)\n return out\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object') return false\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n}\n\n/**\n * One-way PII scrub for telemetry/ingestion. Backward-compatible: called with no\n * options it behaves exactly as before (SSN/EIN strings + sensitive object keys\n * → sentinels). `extraPatterns` lets a product add matchers (e.g. credit-card)\n * without forking this module.\n */\nexport function redactForIngestion(value: unknown, options: RedactForIngestionOptions = {}): unknown {\n const patterns = options.extraPatterns\n ? [...DEFAULT_REDACTION_PATTERNS, ...options.extraPatterns]\n : DEFAULT_REDACTION_PATTERNS\n const sensitiveKeys = options.extraSensitiveKeys\n ? new Set([...SENSITIVE_KEYS, ...options.extraSensitiveKeys.map((k) => k.toLowerCase())])\n : SENSITIVE_KEYS\n const maskString =\n options.stringMode === 'mask-spans'\n ? (s: string) => maskSpans(s, patterns)\n : (s: string) => redactString(s, patterns)\n // Cycle guard: a payload with a circular reference would otherwise recurse\n // forever. On re-encountering an object/array, return it untouched to break\n // the cycle (the same value was already redacted on its first visit).\n const seen = new WeakSet<object>()\n const walk = (v: unknown): unknown => {\n if (typeof v === 'string') return maskString(v)\n if (Array.isArray(v)) {\n if (seen.has(v)) return v\n seen.add(v)\n return v.map(walk)\n }\n if (isPlainObject(v)) {\n if (seen.has(v)) return v\n seen.add(v)\n const out: Record<string, unknown> = {}\n for (const [k, val] of Object.entries(v)) {\n out[k] = sensitiveKeys.has(k.toLowerCase()) ? '[REDACTED:field]' : walk(val)\n }\n return out\n }\n return v\n }\n return walk(value)\n}\n\n// ── Reversible document redaction (the UI path) ─────────────────────────────\n\n/** A detected PII span in a source string. */\nexport interface RedactionSpan {\n /** Stable within a document (index-derived) — used for reveal + audit. */\n id: string\n kind: string\n start: number\n end: number\n text: string\n}\n\n/**\n * Find non-overlapping PII spans in `text`. Matches every pattern, sorts by\n * position, and drops overlaps (first match wins). Deterministic — no ids that\n * vary per call.\n */\nexport function detectSpans(\n text: string,\n patterns: readonly RedactionPattern[] = DEFAULT_REDACTION_PATTERNS,\n): RedactionSpan[] {\n const raw: Array<{ kind: string; start: number; end: number; text: string }> = []\n for (const { kind, pattern, validate } of patterns) {\n const g = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)\n for (const m of text.matchAll(g)) {\n if (m.index === undefined || m[0].length === 0) continue\n if (validate && !validate(m[0])) continue\n raw.push({ kind, start: m.index, end: m.index + m[0].length, text: m[0] })\n }\n }\n raw.sort((a, b) => a.start - b.start || b.end - a.end)\n const spans: RedactionSpan[] = []\n let cursor = -1\n let i = 0\n for (const s of raw) {\n if (s.start < cursor) continue // overlaps an earlier (higher-priority) span\n spans.push({ id: `span-${i++}`, ...s })\n cursor = s.end\n }\n return spans\n}\n\n/** A redacted document segment: literal text, or a masked span with the\n * original kept ENCRYPTED for an authorized reveal. */\nexport type RedactedDocSegment =\n | { type: 'text'; text: string }\n | { type: 'redacted'; id: string; kind: string; cipher: string }\n\n/** Define a document composed of multiple redacted content segments */\nexport interface RedactedDocument {\n segments: RedactedDocSegment[]\n}\n\n/** Define options to encrypt text and specify patterns for redacting sensitive document content */\nexport interface BuildRedactedDocumentOptions {\n /** Encrypt one original span value. Wire it to `agent-app/crypto`\n * (`encryptWithKey` / `createFieldCrypto`). The cipher is what's stored. */\n encrypt: (plaintext: string) => string | Promise<string>\n /** Patterns to detect (default: {@link DEFAULT_REDACTION_PATTERNS}). */\n patterns?: readonly RedactionPattern[]\n}\n\n/**\n * Split `text` into text + redacted segments, encrypting each redacted span's\n * original. The result carries NO plaintext PII — only the masked structure and\n * ciphertext — so it is safe to ship to a client; reveal happens server-side via\n * {@link revealSpan}.\n */\nexport async function buildRedactedDocument(\n text: string,\n options: BuildRedactedDocumentOptions,\n): Promise<RedactedDocument> {\n const spans = detectSpans(text, options.patterns)\n const segments: RedactedDocSegment[] = []\n let pos = 0\n for (const span of spans) {\n if (span.start > pos) segments.push({ type: 'text', text: text.slice(pos, span.start) })\n segments.push({ type: 'redacted', id: span.id, kind: span.kind, cipher: await options.encrypt(span.text) })\n pos = span.end\n }\n if (pos < text.length) segments.push({ type: 'text', text: text.slice(pos) })\n return { segments }\n}\n\n/** Define options to decrypt, authorize, and audit the reveal of a span segment */\nexport interface RevealSpanOptions {\n /** Decrypt a span cipher. Wire to `agent-app/crypto` (`decryptWithKey`). */\n decrypt: (cipher: string) => string | Promise<string>\n /** Authorization gate — return false to deny the reveal (fail-closed). */\n canReveal: (segment: { id: string; kind: string }) => boolean | Promise<boolean>\n /** Audit hook — invoked only on a granted reveal (the caller records who/when). */\n onReveal?: (segment: { id: string; kind: string }) => void | Promise<void>\n}\n\n/** Describe the outcome of a reveal operation including success status, value, and failure reason */\nexport interface RevealResult {\n ok: boolean\n value?: string\n /** `not_found` | `forbidden` when `ok` is false. */\n reason?: string\n}\n\n/**\n * Reveal one redacted span's original, gated + audited. Fail-closed: an unknown\n * id or a denied `canReveal` returns `{ ok: false }` and never decrypts; a\n * granted reveal decrypts, fires `onReveal` for the audit trail, and returns the\n * value.\n */\nexport async function revealSpan(\n doc: RedactedDocument,\n spanId: string,\n options: RevealSpanOptions,\n): Promise<RevealResult> {\n const seg = doc.segments.find((s): s is Extract<RedactedDocSegment, { type: 'redacted' }> =>\n s.type === 'redacted' && s.id === spanId,\n )\n if (!seg) return { ok: false, reason: 'not_found' }\n const allowed = await options.canReveal({ id: seg.id, kind: seg.kind })\n if (!allowed) return { ok: false, reason: 'forbidden' }\n const value = await options.decrypt(seg.cipher)\n if (options.onReveal) await options.onReveal({ id: seg.id, kind: seg.kind })\n return { ok: true, value }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -37,9 +37,44 @@ export interface DatabaseProviderOptions {
37
37
  export interface SqliteBatchDatabase {
38
38
  batch?: (statements: [unknown, ...unknown[]]) => Promise<unknown[]>;
39
39
  }
40
+ /** The three raw SQL control statements used by the explicit atomic path. */
41
+ export type SqliteTransactionCommand = 'BEGIN IMMEDIATE' | 'COMMIT' | 'ROLLBACK';
42
+ /** A statement that has not started before the transaction opens. The callback
43
+ * receives the transaction connection, never a detached query promise. */
44
+ export type SqliteLazyStatement<T = unknown> = (connection: SqliteAtomicConnection) => T | Promise<T>;
45
+ /** The single connection exposed inside a native transaction callback. */
46
+ export interface SqliteAtomicConnection {
47
+ /** Execute one lazy operation on this transaction's connection. */
48
+ execute(statement: SqliteLazyStatement): unknown | Promise<unknown>;
49
+ }
50
+ /** A manually controlled connection owns both transaction commands and queries.
51
+ * Keeping them on one value prevents a transaction from spanning two handles. */
52
+ export interface SqliteManualTransactionConnection extends SqliteAtomicConnection {
53
+ exec(command: SqliteTransactionCommand): unknown | Promise<unknown>;
54
+ }
55
+ /**
56
+ * A SQLite driver that can execute a group of statements atomically.
57
+ *
58
+ * Drivers should expose `transaction`; its callback receives the one connection
59
+ * that owns the transaction. A portable driver may instead expose one
60
+ * `fallbackConnection` containing both `exec` and `execute`. The fallback uses
61
+ * lazy operations, so no query can start before `BEGIN IMMEDIATE`.
62
+ */
63
+ export interface AtomicSqliteDatabase {
64
+ transaction?: (callback: (connection: SqliteAtomicConnection) => unknown[] | Promise<unknown[]>) => unknown[] | Promise<unknown[]>;
65
+ fallbackConnection?: SqliteManualTransactionConnection;
66
+ }
40
67
  /** Execute related SQLite statements in one transactional driver batch when
41
68
  * supported, or sequentially in the same order for portable local drivers. */
42
69
  export declare function runSqliteStatements(db: SqliteBatchDatabase, statements: [unknown, ...unknown[]]): Promise<unknown[]>;
70
+ /**
71
+ * Execute related SQLite statements atomically.
72
+ *
73
+ * This helper is intentionally separate from {@link runSqliteStatements}.
74
+ * The older helper preserves its portable sequential fallback; this helper
75
+ * fails closed when the injected driver cannot prove atomicity.
76
+ */
77
+ export declare function runAtomicSqliteStatements(db: AtomicSqliteDatabase, statements: [SqliteLazyStatement, ...SqliteLazyStatement[]]): Promise<unknown[]>;
43
78
  /**
44
79
  * Create a swappable database provider. `DB` is the injected instance's type
45
80
  * (e.g. a drizzle `Database`); the proxy is typed as `DB` so callers keep full
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  createDatabaseProvider,
3
3
  createInMemoryKV,
4
+ runAtomicSqliteStatements,
4
5
  runSqliteStatements
5
- } from "../chunk-LRHVCVEW.js";
6
+ } from "../chunk-Q4ZER4HI.js";
6
7
  export {
7
8
  createDatabaseProvider,
8
9
  createInMemoryKV,
10
+ runAtomicSqliteStatements,
9
11
  runSqliteStatements
10
12
  };
11
13
  //# sourceMappingURL=index.js.map
@@ -131,7 +131,7 @@ import {
131
131
  withoutRecordGridCreated,
132
132
  withoutRecordGridRemoved,
133
133
  withoutRecordGridUpdate
134
- } from "../chunk-UPXDBHEE.js";
134
+ } from "../chunk-FOXGPGXF.js";
135
135
  import "../chunk-FBVLEGEG.js";
136
136
  import {
137
137
  EvidenceLineageTable,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.45.58",
3
+ "version": "0.45.59",
4
4
  "packageManager": "pnpm@11.17.0",
5
5
  "description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
6
6
  "keywords": [
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/store/index.ts"],"sourcesContent":["/**\n * Swappable database provider — the seam that decouples the agent's persistence\n * from any one driver.\n *\n * The agent core (and the app's server modules) import a single `db` and use it\n * directly. That `db` is a lazy proxy: it forwards to whatever database instance\n * the runtime injects via {@link DatabaseProvider.setDatabase}. So the SAME core\n * runs on:\n * - Cloudflare D1 (`setDatabase(drizzle(d1, schema))`) — prod\n * - SQLite / miniflare (`setDatabase(drizzle(betterSqlite, schema))`) — eval / the portable inner shell\n * - libsql / Turso, Postgres (`setDatabase(drizzle(client, schema))`) — a future hosted DB\n *\n * Adding a new database is one adapter (a drizzle instance over a new driver) +\n * a `setDatabase` call. None of the modules importing `db` change. Substrate-\n * free and driver-agnostic: this module knows nothing about D1, drizzle, or any\n * schema — it only forwards property access to the injected instance.\n */\n\nexport interface DatabaseProvider<DB> {\n /** The injected database, as a lazy proxy. Throws (with `notReadyMessage`)\n * on any access before {@link setDatabase} is called. */\n readonly db: DB\n /** Inject the active database instance (any driver's client). */\n setDatabase(database: DB): void\n /** True once a database has been injected. */\n isReady(): boolean\n /** Clear the injected database (next access throws again). Mainly for tests. */\n reset(): void\n}\n\n/** Define options for configuring database provider behavior including error messaging */\nexport interface DatabaseProviderOptions {\n /** Error thrown when `db` is accessed before injection. Keep the product's\n * existing wording so callers see a familiar message. */\n notReadyMessage?: string\n}\n\n/** A database driver that can execute related SQLite statements as one batch.\n * Cloudflare D1 and libsql expose this method; portable local drivers may not. */\nexport interface SqliteBatchDatabase {\n batch?: (statements: [unknown, ...unknown[]]) => Promise<unknown[]>\n}\n\n/** Execute related SQLite statements in one transactional driver batch when\n * supported, or sequentially in the same order for portable local drivers. */\nexport async function runSqliteStatements(\n db: SqliteBatchDatabase,\n statements: [unknown, ...unknown[]],\n): Promise<unknown[]> {\n if (typeof db.batch === 'function') {\n return await db.batch(statements)\n }\n const results: unknown[] = []\n for (const statement of statements) results.push(await statement)\n return results\n}\n\n/**\n * Create a swappable database provider. `DB` is the injected instance's type\n * (e.g. a drizzle `Database`); the proxy is typed as `DB` so callers keep full\n * typing and their existing query syntax.\n */\nexport function createDatabaseProvider<DB extends object>(\n options: DatabaseProviderOptions = {},\n): DatabaseProvider<DB> {\n const message = options.notReadyMessage ?? 'Database not initialized — call setDatabase() first.'\n let current: DB | null = null\n\n const db = new Proxy({} as DB, {\n get(_target, prop) {\n if (!current) throw new Error(message)\n const value = (current as Record<string | symbol, unknown>)[prop]\n // Bind methods to the real instance so `this` resolves correctly through\n // the proxy (works for drizzle's query builders and class-based stores).\n return typeof value === 'function' ? (value as (...args: unknown[]) => unknown).bind(current) : value\n },\n has(_target, prop) {\n return current !== null && prop in (current as object)\n },\n })\n\n return {\n db,\n setDatabase(database: DB) {\n current = database\n },\n isReady() {\n return current !== null\n },\n reset() {\n current = null\n },\n }\n}\n\n// ── KV store port (the vault backend) ───────────────────────────────────────\n//\n// The vault (workspace files) is a key/value store. In production it's a\n// Cloudflare `KVNamespace`; the portable inner shell injects an in-memory (or\n// other) implementation. This is the subset of the KV API the vault uses —\n// `KVNamespace` satisfies it structurally, so prod passes the binding unchanged,\n// and `createInMemoryKV()` supplies the portable adapter for sandbox/eval.\n\n/** Describe the result of listing keys with completion status and optional pagination cursor */\nexport interface KVListResult {\n keys: { name: string }[]\n list_complete: boolean\n cursor?: string\n}\n\n/** Define options for storing a key-value pair with expiration and metadata settings */\nexport interface KVPutOptions {\n expiration?: number\n expirationTtl?: number\n metadata?: unknown\n}\n\n/** Resolve a key-value pair retrieval including its associated metadata and value */\nexport interface KVGetWithMetadataResult {\n value: string | null\n metadata: unknown | null\n}\n\n/** Define a key-value store interface for asynchronous data retrieval, storage, deletion, and listing */\nexport interface KVStore {\n get(key: string): Promise<string | null>\n /** Read a value with its stored metadata (e.g. the vault's encrypted/hasPII flags). */\n getWithMetadata(key: string): Promise<KVGetWithMetadataResult>\n put(key: string, value: string, options?: KVPutOptions): Promise<void>\n delete(key: string): Promise<void>\n list(options?: { prefix?: string; cursor?: string; limit?: number }): Promise<KVListResult>\n}\n\n/**\n * In-memory {@link KVStore} — the portable vault backend for sandbox/eval runs.\n * Backed by a Map; `list` returns all prefix-matched keys in one complete page\n * (no real pagination needed in-process). Seed with `initial` entries if useful.\n */\nexport function createInMemoryKV(initial?: Record<string, string>): KVStore {\n const store = new Map<string, { value: string; metadata: unknown }>(\n initial ? Object.entries(initial).map(([k, v]) => [k, { value: v, metadata: null }]) : [],\n )\n return {\n async get(key) {\n return store.get(key)?.value ?? null\n },\n async getWithMetadata(key) {\n const entry = store.get(key)\n return { value: entry?.value ?? null, metadata: entry?.metadata ?? null }\n },\n async put(key, value, options) {\n store.set(key, { value, metadata: options?.metadata ?? null })\n },\n async delete(key) {\n store.delete(key)\n },\n async list(options) {\n const prefix = options?.prefix ?? ''\n const keys = [...store.keys()]\n .filter((k) => k.startsWith(prefix))\n .sort()\n .map((name) => ({ name }))\n return { keys, list_complete: true }\n },\n }\n}\n"],"mappings":";AA6CA,eAAsB,oBACpB,IACA,YACoB;AACpB,MAAI,OAAO,GAAG,UAAU,YAAY;AAClC,WAAO,MAAM,GAAG,MAAM,UAAU;AAAA,EAClC;AACA,QAAM,UAAqB,CAAC;AAC5B,aAAW,aAAa,WAAY,SAAQ,KAAK,MAAM,SAAS;AAChE,SAAO;AACT;AAOO,SAAS,uBACd,UAAmC,CAAC,GACd;AACtB,QAAM,UAAU,QAAQ,mBAAmB;AAC3C,MAAI,UAAqB;AAEzB,QAAM,KAAK,IAAI,MAAM,CAAC,GAAS;AAAA,IAC7B,IAAI,SAAS,MAAM;AACjB,UAAI,CAAC,QAAS,OAAM,IAAI,MAAM,OAAO;AACrC,YAAM,QAAS,QAA6C,IAAI;AAGhE,aAAO,OAAO,UAAU,aAAc,MAA0C,KAAK,OAAO,IAAI;AAAA,IAClG;AAAA,IACA,IAAI,SAAS,MAAM;AACjB,aAAO,YAAY,QAAQ,QAAS;AAAA,IACtC;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA,YAAY,UAAc;AACxB,gBAAU;AAAA,IACZ;AAAA,IACA,UAAU;AACR,aAAO,YAAY;AAAA,IACrB;AAAA,IACA,QAAQ;AACN,gBAAU;AAAA,IACZ;AAAA,EACF;AACF;AA6CO,SAAS,iBAAiB,SAA2C;AAC1E,QAAM,QAAQ,IAAI;AAAA,IAChB,UAAU,OAAO,QAAQ,OAAO,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,KAAK,CAAC,CAAC,IAAI,CAAC;AAAA,EAC1F;AACA,SAAO;AAAA,IACL,MAAM,IAAI,KAAK;AACb,aAAO,MAAM,IAAI,GAAG,GAAG,SAAS;AAAA,IAClC;AAAA,IACA,MAAM,gBAAgB,KAAK;AACzB,YAAM,QAAQ,MAAM,IAAI,GAAG;AAC3B,aAAO,EAAE,OAAO,OAAO,SAAS,MAAM,UAAU,OAAO,YAAY,KAAK;AAAA,IAC1E;AAAA,IACA,MAAM,IAAI,KAAK,OAAO,SAAS;AAC7B,YAAM,IAAI,KAAK,EAAE,OAAO,UAAU,SAAS,YAAY,KAAK,CAAC;AAAA,IAC/D;AAAA,IACA,MAAM,OAAO,KAAK;AAChB,YAAM,OAAO,GAAG;AAAA,IAClB;AAAA,IACA,MAAM,KAAK,SAAS;AAClB,YAAM,SAAS,SAAS,UAAU;AAClC,YAAM,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC,EAC1B,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM,CAAC,EAClC,KAAK,EACL,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE;AAC3B,aAAO,EAAE,MAAM,eAAe,KAAK;AAAA,IACrC;AAAA,EACF;AACF;","names":[]}