@peac/capture-core 0.10.9 → 0.10.10

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/memory.ts"],"names":[],"mappings":"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","file":"testkit.cjs","sourcesContent":["/**\n * @peac/capture-core - Type Definitions\n *\n * Runtime-neutral types for the capture pipeline.\n * NO Node.js APIs, NO fs, NO path - pure types and interfaces only.\n *\n * Filesystem implementations belong in @peac/capture-node.\n */\n\nimport type { Digest } from '@peac/schema';\n\n// =============================================================================\n// Captured Action (Input to Pipeline)\n// =============================================================================\n\n/**\n * Execution status of a captured action.\n */\nexport type ActionStatus = 'ok' | 'error' | 'timeout' | 'canceled';\n\n/**\n * Policy snapshot at capture time.\n */\nexport interface PolicySnapshot {\n /** Policy decision that allowed/denied the action */\n decision: 'allow' | 'deny' | 'constrained';\n /** Whether sandbox mode was enabled */\n sandbox_enabled?: boolean;\n /** Whether elevated permissions were granted */\n elevated?: boolean;\n /** Hash of the effective policy document (64 lowercase hex) */\n policy_digest?: string;\n}\n\n/**\n * Runtime-neutral captured action.\n *\n * This is the input to the capture pipeline, before any hashing or\n * transformation. Platform-specific adapters convert their events\n * to this common format.\n *\n * IMPORTANT: Timestamps are ISO 8601 strings for deterministic serialization.\n */\nexport interface CapturedAction {\n /** Stable ID for idempotency/dedupe (REQUIRED) */\n id: string;\n\n /** Event kind - \"tool.call\", \"http.request\", etc. */\n kind: string;\n\n /** Platform identifier - \"openclaw\", \"mcp\", \"a2a\", \"claude-code\" */\n platform: string;\n\n /** Platform version (optional) */\n platform_version?: string;\n\n /** Plugin that captured this (optional) */\n plugin_id?: string;\n\n /** Tool name (for tool.call kind) */\n tool_name?: string;\n\n /** Tool provider (optional) */\n tool_provider?: string;\n\n /** Resource URI (for http/fs kinds) */\n resource_uri?: string;\n\n /** HTTP method (for http.request kind) */\n resource_method?: string;\n\n /** Raw input bytes (will be hashed, then discarded) */\n input_bytes?: Uint8Array;\n\n /** Raw output bytes (will be hashed, then discarded) */\n output_bytes?: Uint8Array;\n\n /** Start time (ISO 8601 string for determinism) */\n started_at: string;\n\n /** Completion time (ISO 8601 string) */\n completed_at?: string;\n\n /** Duration in milliseconds from monotonic clock */\n duration_ms?: number;\n\n /** Execution status */\n status?: ActionStatus;\n\n /** Error code if status is 'error' */\n error_code?: string;\n\n /** Whether the error is retryable */\n retryable?: boolean;\n\n /** Policy snapshot at execution time */\n policy?: PolicySnapshot;\n\n /** Platform-specific metadata (will be stored in extensions) */\n metadata?: Record<string, unknown>;\n}\n\n// =============================================================================\n// Spool Entry (Serializable Record)\n// =============================================================================\n\n/**\n * Spool entry - the post-hashing record that can be serialized.\n *\n * This contains computed digests but NOT raw payload bytes (privacy-preserving).\n * The format is deterministic for tamper-evident chaining.\n */\nexport interface SpoolEntry {\n /** When this entry was captured (RFC 3339) */\n captured_at: string;\n\n /** The captured action (without raw bytes) */\n action: Omit<CapturedAction, 'input_bytes' | 'output_bytes'>;\n\n /** Input payload digest (computed inline) */\n input_digest?: Digest;\n\n /** Output payload digest (computed inline) */\n output_digest?: Digest;\n\n // Tamper-evident chain fields\n\n /** Digest of previous entry in spool (chain link) */\n prev_entry_digest: string;\n\n /** Digest of this entry (for next entry's prev) */\n entry_digest: string;\n\n /** Sequence number in the spool (monotonic) */\n sequence: number;\n}\n\n// =============================================================================\n// Spool Store Interface (Abstract - No Implementation)\n// =============================================================================\n\n/**\n * Abstract spool storage interface.\n *\n * Implementations handle the actual storage mechanism:\n * - InMemorySpoolStore (for tests, in this package)\n * - FsSpoolStore (in @peac/capture-node)\n * - CloudSpoolStore (future, for serverless)\n */\nexport interface SpoolStore {\n /**\n * Append an entry to the spool.\n * Returns the assigned sequence number.\n */\n append(entry: SpoolEntry): Promise<number>;\n\n /**\n * Commit/sync the spool to durable storage.\n * No-op for in-memory stores.\n */\n commit(): Promise<void>;\n\n /**\n * Read entries starting from a sequence number.\n * Returns entries in order.\n */\n read(fromSequence: number, limit?: number): Promise<SpoolEntry[]>;\n\n /**\n * Get the current head digest (last entry's digest).\n * Returns genesis digest if spool is empty.\n */\n getHeadDigest(): Promise<string>;\n\n /**\n * Get the current sequence number (last entry's sequence).\n * Returns 0 if spool is empty.\n */\n getSequence(): Promise<number>;\n\n /**\n * Close the store and release resources.\n */\n close(): Promise<void>;\n}\n\n// =============================================================================\n// Dedupe Index Interface (Abstract - No Implementation)\n// =============================================================================\n\n/**\n * Dedupe entry - tracks captured actions to prevent duplicates.\n */\nexport interface DedupeEntry {\n /** Sequence number in spool */\n sequence: number;\n\n /** Entry digest for verification */\n entry_digest: string;\n\n /** When the action was captured */\n captured_at: string;\n\n /** Whether a receipt has been emitted */\n emitted: boolean;\n}\n\n/**\n * Abstract dedupe index interface.\n *\n * All methods are async to support durable backends (sqlite, kv, etc.)\n * without forcing implementers to use sync filesystem calls.\n *\n * Implementations handle the actual storage:\n * - InMemoryDedupeIndex (for tests, in this package)\n * - PersistentDedupeIndex (in @peac/capture-node)\n */\nexport interface DedupeIndex {\n /** Get entry by action ID */\n get(actionId: string): Promise<DedupeEntry | undefined>;\n\n /** Set entry for action ID */\n set(actionId: string, entry: DedupeEntry): Promise<void>;\n\n /** Check if action ID exists */\n has(actionId: string): Promise<boolean>;\n\n /** Mark an entry as emitted */\n markEmitted(actionId: string): Promise<boolean>;\n\n /** Delete entry (for cleanup) */\n delete(actionId: string): Promise<boolean>;\n\n /** Get count of entries */\n size(): Promise<number>;\n\n /** Clear all entries */\n clear(): Promise<void>;\n}\n\n// =============================================================================\n// Hasher Interface\n// =============================================================================\n\n/**\n * Hasher configuration.\n */\nexport interface HasherConfig {\n /** Maximum bytes to hash before truncating (default: 1MB) */\n truncateThreshold?: number;\n}\n\n/**\n * Hasher interface for computing payload digests.\n */\nexport interface Hasher {\n /**\n * Compute digest for payload bytes.\n * Automatically truncates if payload exceeds threshold.\n */\n digest(payload: Uint8Array): Promise<Digest>;\n\n /**\n * Compute digest for a spool entry (for chaining).\n * Uses deterministic serialization (JCS).\n */\n digestEntry(entry: Omit<SpoolEntry, 'entry_digest'>): Promise<string>;\n}\n\n// =============================================================================\n// Capture Session Interface\n// =============================================================================\n\n/**\n * Capture session configuration.\n */\nexport interface CaptureSessionConfig {\n /** Spool store implementation */\n store: SpoolStore;\n\n /** Dedupe index implementation */\n dedupe: DedupeIndex;\n\n /** Hasher implementation */\n hasher: Hasher;\n}\n\n/**\n * Capture result for a single action.\n */\nexport type CaptureResult =\n | { success: true; entry: SpoolEntry }\n | { success: false; code: CaptureErrorCode; message: string };\n\n/**\n * Capture error codes.\n *\n * Layer-separated error codes:\n * - E_CAPTURE_* codes are for capture pipeline failures, NOT schema validation\n * - E_INTERACTION_* codes (in @peac/schema) are for receipt/profile validation\n */\nexport type CaptureErrorCode =\n | 'E_CAPTURE_DUPLICATE'\n | 'E_CAPTURE_HASH_FAILED'\n | 'E_CAPTURE_STORE_FAILED'\n | 'E_CAPTURE_INVALID_ACTION'\n | 'E_CAPTURE_SESSION_CLOSED'\n | 'E_CAPTURE_INTERNAL';\n\n/**\n * Capture session - stateful capture pipeline instance.\n */\nexport interface CaptureSession {\n /**\n * Capture an action.\n * Returns success with entry, or failure with error code.\n */\n capture(action: CapturedAction): Promise<CaptureResult>;\n\n /**\n * Commit any pending writes to durable storage.\n */\n commit(): Promise<void>;\n\n /**\n * Get the current spool head digest.\n */\n getHeadDigest(): Promise<string>;\n\n /**\n * Close the session and release resources.\n */\n close(): Promise<void>;\n}\n\n// =============================================================================\n// Spool Anchor (for External Verifiability)\n// =============================================================================\n\n/**\n * Spool anchor extension data.\n *\n * When included in a receipt, this allows external verifiers to\n * check the spool chain without access to the full spool file.\n */\nexport interface SpoolAnchor {\n /** Current head digest of the spool chain */\n spool_head_digest: string;\n\n /** Sequence number in the spool */\n sequence: number;\n\n /** Timestamp of the anchor */\n anchored_at: string;\n}\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/**\n * Genesis digest - the \"prev\" digest for the first entry in a spool.\n * 64 zeros represents \"no previous entry\".\n */\nexport const GENESIS_DIGEST = '0'.repeat(64);\n\n/**\n * Default truncation threshold: 1MB (1024 * 1024 bytes).\n */\nexport const DEFAULT_TRUNCATE_THRESHOLD = 1024 * 1024;\n\n/**\n * Size constants for truncation algorithms.\n */\nexport const SIZE_CONSTANTS = {\n K: 1024, // 1 KB\n M: 1024 * 1024, // 1 MB\n TRUNC_64K: 64 * 1024, // 64 KB\n TRUNC_1M: 1024 * 1024, // 1 MB\n} as const;\n","/**\n * @peac/capture-core - In-Memory Implementations\n *\n * Reference implementations for testing and development.\n * NOT for production use - use @peac/capture-node for durable storage.\n */\n\nimport type { SpoolStore, SpoolEntry, DedupeIndex, DedupeEntry } from './types';\nimport { GENESIS_DIGEST } from './types';\n\n// =============================================================================\n// In-Memory Spool Store\n// =============================================================================\n\n/**\n * In-memory spool store for testing.\n *\n * Features:\n * - Entries stored in array (ordered by sequence)\n * - commit() is a no-op (no durability)\n * - Useful for unit tests and development\n */\nexport class InMemorySpoolStore implements SpoolStore {\n private entries: SpoolEntry[] = [];\n private headDigest: string = GENESIS_DIGEST;\n private currentSequence: number = 0;\n private closed: boolean = false;\n\n /**\n * Append an entry to the spool.\n */\n async append(entry: SpoolEntry): Promise<number> {\n this.assertNotClosed();\n\n // Validate sequence\n if (entry.sequence !== this.currentSequence + 1) {\n throw new Error(\n `Invalid sequence: expected ${this.currentSequence + 1}, got ${entry.sequence}`\n );\n }\n\n // Validate chain\n if (entry.prev_entry_digest !== this.headDigest) {\n throw new Error(\n `Invalid chain: expected prev_entry_digest ${this.headDigest}, got ${entry.prev_entry_digest}`\n );\n }\n\n this.entries.push(entry);\n this.headDigest = entry.entry_digest;\n this.currentSequence = entry.sequence;\n\n return entry.sequence;\n }\n\n /**\n * Commit is a no-op for in-memory store.\n */\n async commit(): Promise<void> {\n this.assertNotClosed();\n // No-op for in-memory\n }\n\n /**\n * Read entries starting from a sequence number.\n */\n async read(fromSequence: number, limit?: number): Promise<SpoolEntry[]> {\n this.assertNotClosed();\n\n const startIndex = fromSequence > 0 ? fromSequence - 1 : 0;\n const entries = this.entries.slice(startIndex);\n\n if (limit !== undefined && limit > 0) {\n return entries.slice(0, limit);\n }\n\n return entries;\n }\n\n /**\n * Get the current head digest.\n */\n async getHeadDigest(): Promise<string> {\n this.assertNotClosed();\n return this.headDigest;\n }\n\n /**\n * Get the current sequence number.\n */\n async getSequence(): Promise<number> {\n this.assertNotClosed();\n return this.currentSequence;\n }\n\n /**\n * Close the store.\n */\n async close(): Promise<void> {\n this.closed = true;\n }\n\n /**\n * Check if store is closed.\n */\n private assertNotClosed(): void {\n if (this.closed) {\n throw new Error('SpoolStore is closed');\n }\n }\n\n // Test helpers (not part of interface)\n\n /**\n * Get all entries (for testing).\n */\n getAllEntries(): SpoolEntry[] {\n return [...this.entries];\n }\n\n /**\n * Clear all entries (for testing).\n */\n clear(): void {\n this.entries = [];\n this.headDigest = GENESIS_DIGEST;\n this.currentSequence = 0;\n }\n}\n\n// =============================================================================\n// In-Memory Dedupe Index\n// =============================================================================\n\n/**\n * In-memory dedupe index for testing.\n *\n * Features:\n * - Entries stored in Map\n * - No persistence\n * - Returns resolved Promises (async interface, sync implementation)\n * - Useful for unit tests and development\n */\nexport class InMemoryDedupeIndex implements DedupeIndex {\n private entries: Map<string, DedupeEntry> = new Map();\n\n /**\n * Get entry by action ID.\n */\n async get(actionId: string): Promise<DedupeEntry | undefined> {\n return this.entries.get(actionId);\n }\n\n /**\n * Set entry for action ID.\n */\n async set(actionId: string, entry: DedupeEntry): Promise<void> {\n this.entries.set(actionId, entry);\n }\n\n /**\n * Check if action ID exists.\n */\n async has(actionId: string): Promise<boolean> {\n return this.entries.has(actionId);\n }\n\n /**\n * Mark an entry as emitted.\n */\n async markEmitted(actionId: string): Promise<boolean> {\n const entry = this.entries.get(actionId);\n if (!entry) {\n return false;\n }\n entry.emitted = true;\n return true;\n }\n\n /**\n * Delete entry.\n */\n async delete(actionId: string): Promise<boolean> {\n return this.entries.delete(actionId);\n }\n\n /**\n * Get count of entries.\n */\n async size(): Promise<number> {\n return this.entries.size;\n }\n\n /**\n * Clear all entries.\n */\n async clear(): Promise<void> {\n this.entries.clear();\n }\n\n // Test helpers (not part of interface)\n\n /**\n * Get all entries as array (for testing).\n */\n getAllEntries(): Array<[string, DedupeEntry]> {\n return [...this.entries.entries()];\n }\n}\n\n// =============================================================================\n// Factory Functions\n// =============================================================================\n\n/**\n * Create an in-memory spool store.\n */\nexport function createInMemorySpoolStore(): InMemorySpoolStore {\n return new InMemorySpoolStore();\n}\n\n/**\n * Create an in-memory dedupe index.\n */\nexport function createInMemoryDedupeIndex(): InMemoryDedupeIndex {\n return new InMemoryDedupeIndex();\n}\n"]}
@@ -0,0 +1,158 @@
1
+ // src/types.ts
2
+ var GENESIS_DIGEST = "0".repeat(64);
3
+
4
+ // src/memory.ts
5
+ var InMemorySpoolStore = class {
6
+ entries = [];
7
+ headDigest = GENESIS_DIGEST;
8
+ currentSequence = 0;
9
+ closed = false;
10
+ /**
11
+ * Append an entry to the spool.
12
+ */
13
+ async append(entry) {
14
+ this.assertNotClosed();
15
+ if (entry.sequence !== this.currentSequence + 1) {
16
+ throw new Error(
17
+ `Invalid sequence: expected ${this.currentSequence + 1}, got ${entry.sequence}`
18
+ );
19
+ }
20
+ if (entry.prev_entry_digest !== this.headDigest) {
21
+ throw new Error(
22
+ `Invalid chain: expected prev_entry_digest ${this.headDigest}, got ${entry.prev_entry_digest}`
23
+ );
24
+ }
25
+ this.entries.push(entry);
26
+ this.headDigest = entry.entry_digest;
27
+ this.currentSequence = entry.sequence;
28
+ return entry.sequence;
29
+ }
30
+ /**
31
+ * Commit is a no-op for in-memory store.
32
+ */
33
+ async commit() {
34
+ this.assertNotClosed();
35
+ }
36
+ /**
37
+ * Read entries starting from a sequence number.
38
+ */
39
+ async read(fromSequence, limit) {
40
+ this.assertNotClosed();
41
+ const startIndex = fromSequence > 0 ? fromSequence - 1 : 0;
42
+ const entries = this.entries.slice(startIndex);
43
+ if (limit !== void 0 && limit > 0) {
44
+ return entries.slice(0, limit);
45
+ }
46
+ return entries;
47
+ }
48
+ /**
49
+ * Get the current head digest.
50
+ */
51
+ async getHeadDigest() {
52
+ this.assertNotClosed();
53
+ return this.headDigest;
54
+ }
55
+ /**
56
+ * Get the current sequence number.
57
+ */
58
+ async getSequence() {
59
+ this.assertNotClosed();
60
+ return this.currentSequence;
61
+ }
62
+ /**
63
+ * Close the store.
64
+ */
65
+ async close() {
66
+ this.closed = true;
67
+ }
68
+ /**
69
+ * Check if store is closed.
70
+ */
71
+ assertNotClosed() {
72
+ if (this.closed) {
73
+ throw new Error("SpoolStore is closed");
74
+ }
75
+ }
76
+ // Test helpers (not part of interface)
77
+ /**
78
+ * Get all entries (for testing).
79
+ */
80
+ getAllEntries() {
81
+ return [...this.entries];
82
+ }
83
+ /**
84
+ * Clear all entries (for testing).
85
+ */
86
+ clear() {
87
+ this.entries = [];
88
+ this.headDigest = GENESIS_DIGEST;
89
+ this.currentSequence = 0;
90
+ }
91
+ };
92
+ var InMemoryDedupeIndex = class {
93
+ entries = /* @__PURE__ */ new Map();
94
+ /**
95
+ * Get entry by action ID.
96
+ */
97
+ async get(actionId) {
98
+ return this.entries.get(actionId);
99
+ }
100
+ /**
101
+ * Set entry for action ID.
102
+ */
103
+ async set(actionId, entry) {
104
+ this.entries.set(actionId, entry);
105
+ }
106
+ /**
107
+ * Check if action ID exists.
108
+ */
109
+ async has(actionId) {
110
+ return this.entries.has(actionId);
111
+ }
112
+ /**
113
+ * Mark an entry as emitted.
114
+ */
115
+ async markEmitted(actionId) {
116
+ const entry = this.entries.get(actionId);
117
+ if (!entry) {
118
+ return false;
119
+ }
120
+ entry.emitted = true;
121
+ return true;
122
+ }
123
+ /**
124
+ * Delete entry.
125
+ */
126
+ async delete(actionId) {
127
+ return this.entries.delete(actionId);
128
+ }
129
+ /**
130
+ * Get count of entries.
131
+ */
132
+ async size() {
133
+ return this.entries.size;
134
+ }
135
+ /**
136
+ * Clear all entries.
137
+ */
138
+ async clear() {
139
+ this.entries.clear();
140
+ }
141
+ // Test helpers (not part of interface)
142
+ /**
143
+ * Get all entries as array (for testing).
144
+ */
145
+ getAllEntries() {
146
+ return [...this.entries.entries()];
147
+ }
148
+ };
149
+ function createInMemorySpoolStore() {
150
+ return new InMemorySpoolStore();
151
+ }
152
+ function createInMemoryDedupeIndex() {
153
+ return new InMemoryDedupeIndex();
154
+ }
155
+
156
+ export { InMemoryDedupeIndex, InMemorySpoolStore, createInMemoryDedupeIndex, createInMemorySpoolStore };
157
+ //# sourceMappingURL=testkit.mjs.map
158
+ //# sourceMappingURL=testkit.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/memory.ts"],"names":[],"mappings":"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","file":"testkit.mjs","sourcesContent":["/**\n * @peac/capture-core - Type Definitions\n *\n * Runtime-neutral types for the capture pipeline.\n * NO Node.js APIs, NO fs, NO path - pure types and interfaces only.\n *\n * Filesystem implementations belong in @peac/capture-node.\n */\n\nimport type { Digest } from '@peac/schema';\n\n// =============================================================================\n// Captured Action (Input to Pipeline)\n// =============================================================================\n\n/**\n * Execution status of a captured action.\n */\nexport type ActionStatus = 'ok' | 'error' | 'timeout' | 'canceled';\n\n/**\n * Policy snapshot at capture time.\n */\nexport interface PolicySnapshot {\n /** Policy decision that allowed/denied the action */\n decision: 'allow' | 'deny' | 'constrained';\n /** Whether sandbox mode was enabled */\n sandbox_enabled?: boolean;\n /** Whether elevated permissions were granted */\n elevated?: boolean;\n /** Hash of the effective policy document (64 lowercase hex) */\n policy_digest?: string;\n}\n\n/**\n * Runtime-neutral captured action.\n *\n * This is the input to the capture pipeline, before any hashing or\n * transformation. Platform-specific adapters convert their events\n * to this common format.\n *\n * IMPORTANT: Timestamps are ISO 8601 strings for deterministic serialization.\n */\nexport interface CapturedAction {\n /** Stable ID for idempotency/dedupe (REQUIRED) */\n id: string;\n\n /** Event kind - \"tool.call\", \"http.request\", etc. */\n kind: string;\n\n /** Platform identifier - \"openclaw\", \"mcp\", \"a2a\", \"claude-code\" */\n platform: string;\n\n /** Platform version (optional) */\n platform_version?: string;\n\n /** Plugin that captured this (optional) */\n plugin_id?: string;\n\n /** Tool name (for tool.call kind) */\n tool_name?: string;\n\n /** Tool provider (optional) */\n tool_provider?: string;\n\n /** Resource URI (for http/fs kinds) */\n resource_uri?: string;\n\n /** HTTP method (for http.request kind) */\n resource_method?: string;\n\n /** Raw input bytes (will be hashed, then discarded) */\n input_bytes?: Uint8Array;\n\n /** Raw output bytes (will be hashed, then discarded) */\n output_bytes?: Uint8Array;\n\n /** Start time (ISO 8601 string for determinism) */\n started_at: string;\n\n /** Completion time (ISO 8601 string) */\n completed_at?: string;\n\n /** Duration in milliseconds from monotonic clock */\n duration_ms?: number;\n\n /** Execution status */\n status?: ActionStatus;\n\n /** Error code if status is 'error' */\n error_code?: string;\n\n /** Whether the error is retryable */\n retryable?: boolean;\n\n /** Policy snapshot at execution time */\n policy?: PolicySnapshot;\n\n /** Platform-specific metadata (will be stored in extensions) */\n metadata?: Record<string, unknown>;\n}\n\n// =============================================================================\n// Spool Entry (Serializable Record)\n// =============================================================================\n\n/**\n * Spool entry - the post-hashing record that can be serialized.\n *\n * This contains computed digests but NOT raw payload bytes (privacy-preserving).\n * The format is deterministic for tamper-evident chaining.\n */\nexport interface SpoolEntry {\n /** When this entry was captured (RFC 3339) */\n captured_at: string;\n\n /** The captured action (without raw bytes) */\n action: Omit<CapturedAction, 'input_bytes' | 'output_bytes'>;\n\n /** Input payload digest (computed inline) */\n input_digest?: Digest;\n\n /** Output payload digest (computed inline) */\n output_digest?: Digest;\n\n // Tamper-evident chain fields\n\n /** Digest of previous entry in spool (chain link) */\n prev_entry_digest: string;\n\n /** Digest of this entry (for next entry's prev) */\n entry_digest: string;\n\n /** Sequence number in the spool (monotonic) */\n sequence: number;\n}\n\n// =============================================================================\n// Spool Store Interface (Abstract - No Implementation)\n// =============================================================================\n\n/**\n * Abstract spool storage interface.\n *\n * Implementations handle the actual storage mechanism:\n * - InMemorySpoolStore (for tests, in this package)\n * - FsSpoolStore (in @peac/capture-node)\n * - CloudSpoolStore (future, for serverless)\n */\nexport interface SpoolStore {\n /**\n * Append an entry to the spool.\n * Returns the assigned sequence number.\n */\n append(entry: SpoolEntry): Promise<number>;\n\n /**\n * Commit/sync the spool to durable storage.\n * No-op for in-memory stores.\n */\n commit(): Promise<void>;\n\n /**\n * Read entries starting from a sequence number.\n * Returns entries in order.\n */\n read(fromSequence: number, limit?: number): Promise<SpoolEntry[]>;\n\n /**\n * Get the current head digest (last entry's digest).\n * Returns genesis digest if spool is empty.\n */\n getHeadDigest(): Promise<string>;\n\n /**\n * Get the current sequence number (last entry's sequence).\n * Returns 0 if spool is empty.\n */\n getSequence(): Promise<number>;\n\n /**\n * Close the store and release resources.\n */\n close(): Promise<void>;\n}\n\n// =============================================================================\n// Dedupe Index Interface (Abstract - No Implementation)\n// =============================================================================\n\n/**\n * Dedupe entry - tracks captured actions to prevent duplicates.\n */\nexport interface DedupeEntry {\n /** Sequence number in spool */\n sequence: number;\n\n /** Entry digest for verification */\n entry_digest: string;\n\n /** When the action was captured */\n captured_at: string;\n\n /** Whether a receipt has been emitted */\n emitted: boolean;\n}\n\n/**\n * Abstract dedupe index interface.\n *\n * All methods are async to support durable backends (sqlite, kv, etc.)\n * without forcing implementers to use sync filesystem calls.\n *\n * Implementations handle the actual storage:\n * - InMemoryDedupeIndex (for tests, in this package)\n * - PersistentDedupeIndex (in @peac/capture-node)\n */\nexport interface DedupeIndex {\n /** Get entry by action ID */\n get(actionId: string): Promise<DedupeEntry | undefined>;\n\n /** Set entry for action ID */\n set(actionId: string, entry: DedupeEntry): Promise<void>;\n\n /** Check if action ID exists */\n has(actionId: string): Promise<boolean>;\n\n /** Mark an entry as emitted */\n markEmitted(actionId: string): Promise<boolean>;\n\n /** Delete entry (for cleanup) */\n delete(actionId: string): Promise<boolean>;\n\n /** Get count of entries */\n size(): Promise<number>;\n\n /** Clear all entries */\n clear(): Promise<void>;\n}\n\n// =============================================================================\n// Hasher Interface\n// =============================================================================\n\n/**\n * Hasher configuration.\n */\nexport interface HasherConfig {\n /** Maximum bytes to hash before truncating (default: 1MB) */\n truncateThreshold?: number;\n}\n\n/**\n * Hasher interface for computing payload digests.\n */\nexport interface Hasher {\n /**\n * Compute digest for payload bytes.\n * Automatically truncates if payload exceeds threshold.\n */\n digest(payload: Uint8Array): Promise<Digest>;\n\n /**\n * Compute digest for a spool entry (for chaining).\n * Uses deterministic serialization (JCS).\n */\n digestEntry(entry: Omit<SpoolEntry, 'entry_digest'>): Promise<string>;\n}\n\n// =============================================================================\n// Capture Session Interface\n// =============================================================================\n\n/**\n * Capture session configuration.\n */\nexport interface CaptureSessionConfig {\n /** Spool store implementation */\n store: SpoolStore;\n\n /** Dedupe index implementation */\n dedupe: DedupeIndex;\n\n /** Hasher implementation */\n hasher: Hasher;\n}\n\n/**\n * Capture result for a single action.\n */\nexport type CaptureResult =\n | { success: true; entry: SpoolEntry }\n | { success: false; code: CaptureErrorCode; message: string };\n\n/**\n * Capture error codes.\n *\n * Layer-separated error codes:\n * - E_CAPTURE_* codes are for capture pipeline failures, NOT schema validation\n * - E_INTERACTION_* codes (in @peac/schema) are for receipt/profile validation\n */\nexport type CaptureErrorCode =\n | 'E_CAPTURE_DUPLICATE'\n | 'E_CAPTURE_HASH_FAILED'\n | 'E_CAPTURE_STORE_FAILED'\n | 'E_CAPTURE_INVALID_ACTION'\n | 'E_CAPTURE_SESSION_CLOSED'\n | 'E_CAPTURE_INTERNAL';\n\n/**\n * Capture session - stateful capture pipeline instance.\n */\nexport interface CaptureSession {\n /**\n * Capture an action.\n * Returns success with entry, or failure with error code.\n */\n capture(action: CapturedAction): Promise<CaptureResult>;\n\n /**\n * Commit any pending writes to durable storage.\n */\n commit(): Promise<void>;\n\n /**\n * Get the current spool head digest.\n */\n getHeadDigest(): Promise<string>;\n\n /**\n * Close the session and release resources.\n */\n close(): Promise<void>;\n}\n\n// =============================================================================\n// Spool Anchor (for External Verifiability)\n// =============================================================================\n\n/**\n * Spool anchor extension data.\n *\n * When included in a receipt, this allows external verifiers to\n * check the spool chain without access to the full spool file.\n */\nexport interface SpoolAnchor {\n /** Current head digest of the spool chain */\n spool_head_digest: string;\n\n /** Sequence number in the spool */\n sequence: number;\n\n /** Timestamp of the anchor */\n anchored_at: string;\n}\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/**\n * Genesis digest - the \"prev\" digest for the first entry in a spool.\n * 64 zeros represents \"no previous entry\".\n */\nexport const GENESIS_DIGEST = '0'.repeat(64);\n\n/**\n * Default truncation threshold: 1MB (1024 * 1024 bytes).\n */\nexport const DEFAULT_TRUNCATE_THRESHOLD = 1024 * 1024;\n\n/**\n * Size constants for truncation algorithms.\n */\nexport const SIZE_CONSTANTS = {\n K: 1024, // 1 KB\n M: 1024 * 1024, // 1 MB\n TRUNC_64K: 64 * 1024, // 64 KB\n TRUNC_1M: 1024 * 1024, // 1 MB\n} as const;\n","/**\n * @peac/capture-core - In-Memory Implementations\n *\n * Reference implementations for testing and development.\n * NOT for production use - use @peac/capture-node for durable storage.\n */\n\nimport type { SpoolStore, SpoolEntry, DedupeIndex, DedupeEntry } from './types';\nimport { GENESIS_DIGEST } from './types';\n\n// =============================================================================\n// In-Memory Spool Store\n// =============================================================================\n\n/**\n * In-memory spool store for testing.\n *\n * Features:\n * - Entries stored in array (ordered by sequence)\n * - commit() is a no-op (no durability)\n * - Useful for unit tests and development\n */\nexport class InMemorySpoolStore implements SpoolStore {\n private entries: SpoolEntry[] = [];\n private headDigest: string = GENESIS_DIGEST;\n private currentSequence: number = 0;\n private closed: boolean = false;\n\n /**\n * Append an entry to the spool.\n */\n async append(entry: SpoolEntry): Promise<number> {\n this.assertNotClosed();\n\n // Validate sequence\n if (entry.sequence !== this.currentSequence + 1) {\n throw new Error(\n `Invalid sequence: expected ${this.currentSequence + 1}, got ${entry.sequence}`\n );\n }\n\n // Validate chain\n if (entry.prev_entry_digest !== this.headDigest) {\n throw new Error(\n `Invalid chain: expected prev_entry_digest ${this.headDigest}, got ${entry.prev_entry_digest}`\n );\n }\n\n this.entries.push(entry);\n this.headDigest = entry.entry_digest;\n this.currentSequence = entry.sequence;\n\n return entry.sequence;\n }\n\n /**\n * Commit is a no-op for in-memory store.\n */\n async commit(): Promise<void> {\n this.assertNotClosed();\n // No-op for in-memory\n }\n\n /**\n * Read entries starting from a sequence number.\n */\n async read(fromSequence: number, limit?: number): Promise<SpoolEntry[]> {\n this.assertNotClosed();\n\n const startIndex = fromSequence > 0 ? fromSequence - 1 : 0;\n const entries = this.entries.slice(startIndex);\n\n if (limit !== undefined && limit > 0) {\n return entries.slice(0, limit);\n }\n\n return entries;\n }\n\n /**\n * Get the current head digest.\n */\n async getHeadDigest(): Promise<string> {\n this.assertNotClosed();\n return this.headDigest;\n }\n\n /**\n * Get the current sequence number.\n */\n async getSequence(): Promise<number> {\n this.assertNotClosed();\n return this.currentSequence;\n }\n\n /**\n * Close the store.\n */\n async close(): Promise<void> {\n this.closed = true;\n }\n\n /**\n * Check if store is closed.\n */\n private assertNotClosed(): void {\n if (this.closed) {\n throw new Error('SpoolStore is closed');\n }\n }\n\n // Test helpers (not part of interface)\n\n /**\n * Get all entries (for testing).\n */\n getAllEntries(): SpoolEntry[] {\n return [...this.entries];\n }\n\n /**\n * Clear all entries (for testing).\n */\n clear(): void {\n this.entries = [];\n this.headDigest = GENESIS_DIGEST;\n this.currentSequence = 0;\n }\n}\n\n// =============================================================================\n// In-Memory Dedupe Index\n// =============================================================================\n\n/**\n * In-memory dedupe index for testing.\n *\n * Features:\n * - Entries stored in Map\n * - No persistence\n * - Returns resolved Promises (async interface, sync implementation)\n * - Useful for unit tests and development\n */\nexport class InMemoryDedupeIndex implements DedupeIndex {\n private entries: Map<string, DedupeEntry> = new Map();\n\n /**\n * Get entry by action ID.\n */\n async get(actionId: string): Promise<DedupeEntry | undefined> {\n return this.entries.get(actionId);\n }\n\n /**\n * Set entry for action ID.\n */\n async set(actionId: string, entry: DedupeEntry): Promise<void> {\n this.entries.set(actionId, entry);\n }\n\n /**\n * Check if action ID exists.\n */\n async has(actionId: string): Promise<boolean> {\n return this.entries.has(actionId);\n }\n\n /**\n * Mark an entry as emitted.\n */\n async markEmitted(actionId: string): Promise<boolean> {\n const entry = this.entries.get(actionId);\n if (!entry) {\n return false;\n }\n entry.emitted = true;\n return true;\n }\n\n /**\n * Delete entry.\n */\n async delete(actionId: string): Promise<boolean> {\n return this.entries.delete(actionId);\n }\n\n /**\n * Get count of entries.\n */\n async size(): Promise<number> {\n return this.entries.size;\n }\n\n /**\n * Clear all entries.\n */\n async clear(): Promise<void> {\n this.entries.clear();\n }\n\n // Test helpers (not part of interface)\n\n /**\n * Get all entries as array (for testing).\n */\n getAllEntries(): Array<[string, DedupeEntry]> {\n return [...this.entries.entries()];\n }\n}\n\n// =============================================================================\n// Factory Functions\n// =============================================================================\n\n/**\n * Create an in-memory spool store.\n */\nexport function createInMemorySpoolStore(): InMemorySpoolStore {\n return new InMemorySpoolStore();\n}\n\n/**\n * Create an in-memory dedupe index.\n */\nexport function createInMemoryDedupeIndex(): InMemoryDedupeIndex {\n return new InMemoryDedupeIndex();\n}\n"]}
package/package.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "name": "@peac/capture-core",
3
- "version": "0.10.9",
3
+ "version": "0.10.10",
4
4
  "description": "Runtime-neutral capture pipeline for PEAC interaction evidence",
5
- "main": "dist/index.js",
5
+ "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
- "require": "./dist/index.js",
11
- "import": "./dist/index.js",
12
- "default": "./dist/index.js"
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.cjs",
12
+ "default": "./dist/index.mjs"
13
13
  },
14
14
  "./testkit": {
15
15
  "types": "./dist/testkit.d.ts",
16
- "require": "./dist/testkit.js",
17
- "import": "./dist/testkit.js",
18
- "default": "./dist/testkit.js"
19
- }
16
+ "import": "./dist/testkit.mjs",
17
+ "require": "./dist/testkit.cjs"
18
+ },
19
+ "./package.json": "./package.json"
20
20
  },
21
21
  "repository": {
22
22
  "type": "git",
@@ -38,19 +38,23 @@
38
38
  "provenance": true
39
39
  },
40
40
  "dependencies": {
41
- "@peac/crypto": "0.10.9",
42
- "@peac/schema": "0.10.9"
41
+ "@peac/crypto": "0.10.10",
42
+ "@peac/schema": "0.10.10"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^20.10.0",
46
46
  "typescript": "^5.3.3",
47
- "vitest": "^1.1.0"
47
+ "vitest": "^4.0.0",
48
+ "tsup": "^8.0.0"
48
49
  },
49
50
  "scripts": {
50
- "build": "tsc",
51
+ "prebuild": "rm -rf dist",
52
+ "build": "pnpm run build:js && pnpm run build:types",
51
53
  "test": "pnpm -w vitest run packages/capture/core/tests",
52
54
  "test:watch": "pnpm -w vitest packages/capture/core/tests",
53
55
  "test:exports": "node scripts/test-exports.mjs",
54
- "clean": "rm -rf dist"
56
+ "clean": "rm -rf dist",
57
+ "build:js": "tsup",
58
+ "build:types": "rm -f dist/.tsbuildinfo && tsc && rm -f dist/.tsbuildinfo"
55
59
  }
56
60
  }
package/dist/hasher.js DELETED
@@ -1,133 +0,0 @@
1
- "use strict";
2
- /**
3
- * @peac/capture-core - Action Hasher
4
- *
5
- * Deterministic hashing for capture pipeline.
6
- * Uses @peac/crypto for JCS (RFC 8785) and SHA-256.
7
- *
8
- * RUNTIME REQUIREMENT: WebCrypto (crypto.subtle) must be available.
9
- * Works in: Node.js 18+, Deno, Bun, modern browsers, Cloudflare Workers.
10
- */
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.ActionHasher = void 0;
13
- exports.createHasher = createHasher;
14
- const crypto_1 = require("@peac/crypto");
15
- const types_1 = require("./types");
16
- // =============================================================================
17
- // Constants
18
- // =============================================================================
19
- /**
20
- * Valid truncation thresholds (only these are supported).
21
- * Using other values would produce digests that don't match any declared algorithm.
22
- */
23
- const VALID_TRUNCATE_THRESHOLDS = [types_1.SIZE_CONSTANTS.TRUNC_64K, types_1.SIZE_CONSTANTS.TRUNC_1M];
24
- // =============================================================================
25
- // WebCrypto Runtime Check
26
- // =============================================================================
27
- /**
28
- * Get WebCrypto subtle interface with explicit runtime check.
29
- * Prefer globalThis.crypto over bare crypto to avoid bundler ambiguity.
30
- *
31
- * @throws Error if WebCrypto is not available
32
- */
33
- function getSubtle() {
34
- const subtle = globalThis.crypto?.subtle;
35
- if (!subtle) {
36
- throw new Error('WebCrypto (crypto.subtle) is required but not available. ' +
37
- 'Ensure you are running in Node.js 18+, Deno, Bun, or a modern browser.');
38
- }
39
- return subtle;
40
- }
41
- // =============================================================================
42
- // SHA-256 Helper (Web Crypto API - Runtime Neutral)
43
- // =============================================================================
44
- /**
45
- * Compute SHA-256 hash of bytes using Web Crypto API.
46
- * Returns lowercase hex string.
47
- */
48
- async function sha256Hex(data) {
49
- const subtle = getSubtle();
50
- const hashBuffer = await subtle.digest('SHA-256', data);
51
- const hashArray = new Uint8Array(hashBuffer);
52
- return Array.from(hashArray)
53
- .map((b) => b.toString(16).padStart(2, '0'))
54
- .join('');
55
- }
56
- // =============================================================================
57
- // Action Hasher Implementation
58
- // =============================================================================
59
- /**
60
- * Default hasher implementation using JCS + SHA-256.
61
- *
62
- * Determinism guarantees:
63
- * - Same input bytes -> same digest
64
- * - Same SpoolEntry (minus entry_digest) -> same chain hash
65
- * - Truncation algorithm is deterministic (first N bytes)
66
- *
67
- * Supported truncation thresholds:
68
- * - 64k (65536 bytes) -> alg: 'sha-256:trunc-64k'
69
- * - 1m (1048576 bytes) -> alg: 'sha-256:trunc-1m'
70
- */
71
- class ActionHasher {
72
- truncateThreshold;
73
- constructor(config = {}) {
74
- const threshold = config.truncateThreshold ?? types_1.SIZE_CONSTANTS.TRUNC_1M;
75
- // Validate threshold is one of the supported values
76
- if (!VALID_TRUNCATE_THRESHOLDS.includes(threshold)) {
77
- throw new RangeError(`truncateThreshold must be 64k (${types_1.SIZE_CONSTANTS.TRUNC_64K}) or 1m (${types_1.SIZE_CONSTANTS.TRUNC_1M}), ` +
78
- `got ${threshold}`);
79
- }
80
- this.truncateThreshold = threshold;
81
- // Validate WebCrypto is available at construction time
82
- getSubtle();
83
- }
84
- /**
85
- * Compute digest for payload bytes.
86
- * Automatically truncates if payload exceeds threshold.
87
- */
88
- async digest(payload) {
89
- const bytes = payload.length;
90
- // No truncation needed - full SHA-256
91
- if (bytes <= this.truncateThreshold) {
92
- return {
93
- alg: 'sha-256',
94
- value: await sha256Hex(payload),
95
- bytes,
96
- };
97
- }
98
- // Large payload: truncate to threshold
99
- const truncated = payload.slice(0, this.truncateThreshold);
100
- // Determine algorithm label based on truncation size
101
- let alg;
102
- if (this.truncateThreshold === types_1.SIZE_CONSTANTS.TRUNC_64K) {
103
- alg = 'sha-256:trunc-64k';
104
- }
105
- else {
106
- // SIZE_CONSTANTS.TRUNC_1M
107
- alg = 'sha-256:trunc-1m';
108
- }
109
- return {
110
- alg,
111
- value: await sha256Hex(truncated),
112
- bytes, // Original size for audit
113
- };
114
- }
115
- /**
116
- * Compute digest for a spool entry (for chaining).
117
- * Uses JCS (RFC 8785) for deterministic serialization.
118
- */
119
- async digestEntry(entry) {
120
- // JCS canonicalization ensures deterministic serialization
121
- const canonical = (0, crypto_1.canonicalize)(entry);
122
- const bytes = new TextEncoder().encode(canonical);
123
- return sha256Hex(bytes);
124
- }
125
- }
126
- exports.ActionHasher = ActionHasher;
127
- /**
128
- * Create a default hasher instance.
129
- */
130
- function createHasher(config) {
131
- return new ActionHasher(config);
132
- }
133
- //# sourceMappingURL=hasher.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hasher.js","sourceRoot":"","sources":["../src/hasher.ts"],"names":[],"mappings":"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"}
package/dist/index.js DELETED
@@ -1,75 +0,0 @@
1
- "use strict";
2
- /**
3
- * @peac/capture-core
4
- *
5
- * Runtime-neutral capture pipeline for PEAC interaction evidence.
6
- *
7
- * This package provides:
8
- * - Types for captured actions and spool entries
9
- * - Interfaces for storage (SpoolStore) and deduplication (DedupeIndex)
10
- * - Hasher for deterministic payload hashing
11
- * - Mapper for converting to InteractionEvidence
12
- * - CaptureSession for orchestrating the pipeline
13
- *
14
- * NO FILESYSTEM OPERATIONS - those belong in @peac/capture-node.
15
- *
16
- * For in-memory test implementations, import from '@peac/capture-core/testkit'.
17
- *
18
- * @example
19
- * ```typescript
20
- * import {
21
- * createCaptureSession,
22
- * createHasher,
23
- * toInteractionEvidence,
24
- * } from '@peac/capture-core';
25
- * import {
26
- * createInMemorySpoolStore,
27
- * createInMemoryDedupeIndex,
28
- * } from '@peac/capture-core/testkit';
29
- *
30
- * const session = createCaptureSession({
31
- * store: createInMemorySpoolStore(),
32
- * dedupe: createInMemoryDedupeIndex(),
33
- * hasher: createHasher(),
34
- * });
35
- *
36
- * const result = await session.capture({
37
- * id: 'action-123',
38
- * kind: 'tool.call',
39
- * platform: 'my-platform',
40
- * started_at: new Date().toISOString(),
41
- * tool_name: 'search',
42
- * });
43
- *
44
- * if (result.success) {
45
- * const evidence = toInteractionEvidence(result.entry);
46
- * }
47
- * ```
48
- */
49
- Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.createCaptureSession = exports.DefaultCaptureSession = exports.toInteractionEvidenceBatch = exports.toInteractionEvidence = exports.createHasher = exports.ActionHasher = exports.SIZE_CONSTANTS = exports.GENESIS_DIGEST = void 0;
51
- // =============================================================================
52
- // Constants (public API)
53
- // =============================================================================
54
- var types_1 = require("./types");
55
- Object.defineProperty(exports, "GENESIS_DIGEST", { enumerable: true, get: function () { return types_1.GENESIS_DIGEST; } });
56
- Object.defineProperty(exports, "SIZE_CONSTANTS", { enumerable: true, get: function () { return types_1.SIZE_CONSTANTS; } });
57
- // =============================================================================
58
- // Hasher (public API)
59
- // =============================================================================
60
- var hasher_1 = require("./hasher");
61
- Object.defineProperty(exports, "ActionHasher", { enumerable: true, get: function () { return hasher_1.ActionHasher; } });
62
- Object.defineProperty(exports, "createHasher", { enumerable: true, get: function () { return hasher_1.createHasher; } });
63
- // =============================================================================
64
- // Mapper (public API)
65
- // =============================================================================
66
- var mapper_1 = require("./mapper");
67
- Object.defineProperty(exports, "toInteractionEvidence", { enumerable: true, get: function () { return mapper_1.toInteractionEvidence; } });
68
- Object.defineProperty(exports, "toInteractionEvidenceBatch", { enumerable: true, get: function () { return mapper_1.toInteractionEvidenceBatch; } });
69
- // =============================================================================
70
- // Session (public API)
71
- // =============================================================================
72
- var session_1 = require("./session");
73
- Object.defineProperty(exports, "DefaultCaptureSession", { enumerable: true, get: function () { return session_1.DefaultCaptureSession; } });
74
- Object.defineProperty(exports, "createCaptureSession", { enumerable: true, get: function () { return session_1.createCaptureSession; } });
75
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;;;AA8BH,gFAAgF;AAChF,yBAAyB;AACzB,gFAAgF;AAEhF,iCAAyD;AAAhD,uGAAA,cAAc,OAAA;AAAE,uGAAA,cAAc,OAAA;AAEvC,gFAAgF;AAChF,sBAAsB;AACtB,gFAAgF;AAEhF,mCAAsD;AAA7C,sGAAA,YAAY,OAAA;AAAE,sGAAA,YAAY,OAAA;AAEnC,gFAAgF;AAChF,sBAAsB;AACtB,gFAAgF;AAEhF,mCAA6E;AAApE,+GAAA,qBAAqB,OAAA;AAAE,oHAAA,0BAA0B,OAAA;AAG1D,gFAAgF;AAChF,uBAAuB;AACvB,gFAAgF;AAEhF,qCAAwE;AAA/D,gHAAA,qBAAqB,OAAA;AAAE,+GAAA,oBAAoB,OAAA"}