@fluxpointstudios/orynq-sdk-flight-recorder 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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/dist/crypto/compression.d.ts +37 -0
  3. package/dist/crypto/compression.d.ts.map +1 -0
  4. package/dist/crypto/compression.js +84 -0
  5. package/dist/crypto/compression.js.map +1 -0
  6. package/dist/crypto/encryption.d.ts +44 -0
  7. package/dist/crypto/encryption.d.ts.map +1 -0
  8. package/dist/crypto/encryption.js +129 -0
  9. package/dist/crypto/encryption.js.map +1 -0
  10. package/dist/crypto/hashing.d.ts +63 -0
  11. package/dist/crypto/hashing.d.ts.map +1 -0
  12. package/dist/crypto/hashing.js +182 -0
  13. package/dist/crypto/hashing.js.map +1 -0
  14. package/dist/crypto/index.d.ts +4 -0
  15. package/dist/crypto/index.d.ts.map +1 -0
  16. package/dist/crypto/index.js +4 -0
  17. package/dist/crypto/index.js.map +1 -0
  18. package/dist/index.d.ts +48 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +54 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/integration/index.d.ts +2 -0
  23. package/dist/integration/index.d.ts.map +1 -0
  24. package/dist/integration/index.js +2 -0
  25. package/dist/integration/index.js.map +1 -0
  26. package/dist/integration/openclaw-adapter.d.ts +64 -0
  27. package/dist/integration/openclaw-adapter.d.ts.map +1 -0
  28. package/dist/integration/openclaw-adapter.js +137 -0
  29. package/dist/integration/openclaw-adapter.js.map +1 -0
  30. package/dist/manifest/index.d.ts +2 -0
  31. package/dist/manifest/index.d.ts.map +1 -0
  32. package/dist/manifest/index.js +2 -0
  33. package/dist/manifest/index.js.map +1 -0
  34. package/dist/manifest/manifest-builder.d.ts +72 -0
  35. package/dist/manifest/manifest-builder.d.ts.map +1 -0
  36. package/dist/manifest/manifest-builder.js +84 -0
  37. package/dist/manifest/manifest-builder.js.map +1 -0
  38. package/dist/recorder/chunk-manager.d.ts +56 -0
  39. package/dist/recorder/chunk-manager.d.ts.map +1 -0
  40. package/dist/recorder/chunk-manager.js +172 -0
  41. package/dist/recorder/chunk-manager.js.map +1 -0
  42. package/dist/recorder/event-buffer.d.ts +61 -0
  43. package/dist/recorder/event-buffer.d.ts.map +1 -0
  44. package/dist/recorder/event-buffer.js +101 -0
  45. package/dist/recorder/event-buffer.js.map +1 -0
  46. package/dist/recorder/index.d.ts +4 -0
  47. package/dist/recorder/index.d.ts.map +1 -0
  48. package/dist/recorder/index.js +4 -0
  49. package/dist/recorder/index.js.map +1 -0
  50. package/dist/recorder/stream-recorder.d.ts +66 -0
  51. package/dist/recorder/stream-recorder.d.ts.map +1 -0
  52. package/dist/recorder/stream-recorder.js +329 -0
  53. package/dist/recorder/stream-recorder.js.map +1 -0
  54. package/dist/storage/index.d.ts +2 -0
  55. package/dist/storage/index.d.ts.map +1 -0
  56. package/dist/storage/index.js +2 -0
  57. package/dist/storage/index.js.map +1 -0
  58. package/dist/storage/local-adapter.d.ts +65 -0
  59. package/dist/storage/local-adapter.d.ts.map +1 -0
  60. package/dist/storage/local-adapter.js +152 -0
  61. package/dist/storage/local-adapter.js.map +1 -0
  62. package/dist/types.d.ts +284 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +28 -0
  65. package/dist/types.js.map +1 -0
  66. package/package.json +43 -0
  67. package/src/__tests__/flight-recorder.test.ts +211 -0
  68. package/src/crypto/compression.ts +105 -0
  69. package/src/crypto/encryption.ts +210 -0
  70. package/src/crypto/hashing.ts +225 -0
  71. package/src/crypto/index.ts +3 -0
  72. package/src/index.ts +96 -0
  73. package/src/integration/index.ts +1 -0
  74. package/src/integration/openclaw-adapter.ts +206 -0
  75. package/src/manifest/index.ts +1 -0
  76. package/src/manifest/manifest-builder.ts +152 -0
  77. package/src/recorder/chunk-manager.ts +224 -0
  78. package/src/recorder/event-buffer.ts +124 -0
  79. package/src/recorder/index.ts +3 -0
  80. package/src/recorder/stream-recorder.ts +427 -0
  81. package/src/storage/index.ts +1 -0
  82. package/src/storage/local-adapter.ts +181 -0
  83. package/src/types.ts +347 -0
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Encryption utilities for flight recorder chunks.
3
+ * Uses Web Crypto API for AES-256-GCM encryption.
4
+ */
5
+
6
+ import { webcrypto } from "node:crypto";
7
+
8
+ const crypto = webcrypto as unknown as Crypto;
9
+
10
+ export interface EncryptedData {
11
+ ciphertext: Uint8Array;
12
+ nonce: Uint8Array;
13
+ tag: Uint8Array; // For AES-GCM, tag is appended to ciphertext
14
+ }
15
+
16
+ export interface EncryptionKey {
17
+ keyId: string;
18
+ key: CryptoKey;
19
+ algorithm: "aes-256-gcm" | "chacha20-poly1305";
20
+ createdAt: string;
21
+ }
22
+
23
+ /**
24
+ * Generate a new AES-256-GCM encryption key.
25
+ */
26
+ export async function generateKey(algorithm: "aes-256-gcm" | "chacha20-poly1305" = "aes-256-gcm"): Promise<EncryptionKey> {
27
+ if (algorithm === "chacha20-poly1305") {
28
+ // ChaCha20-Poly1305 not directly supported in Web Crypto
29
+ // Fall back to AES-GCM for now, can add libsodium later
30
+ console.warn("ChaCha20-Poly1305 not supported, using AES-256-GCM");
31
+ algorithm = "aes-256-gcm";
32
+ }
33
+
34
+ const key = await crypto.subtle.generateKey(
35
+ { name: "AES-GCM", length: 256 },
36
+ true, // extractable for export/wrapping
37
+ ["encrypt", "decrypt"]
38
+ );
39
+
40
+ const keyId = generateKeyId();
41
+
42
+ return {
43
+ keyId,
44
+ key,
45
+ algorithm,
46
+ createdAt: new Date().toISOString(),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Generate a random key ID.
52
+ */
53
+ function generateKeyId(): string {
54
+ const bytes = new Uint8Array(16);
55
+ crypto.getRandomValues(bytes);
56
+ return Array.from(bytes)
57
+ .map((b) => b.toString(16).padStart(2, "0"))
58
+ .join("");
59
+ }
60
+
61
+ /**
62
+ * Generate a random nonce/IV for AES-GCM (12 bytes recommended).
63
+ */
64
+ export function generateNonce(): Uint8Array {
65
+ const nonce = new Uint8Array(12);
66
+ crypto.getRandomValues(nonce);
67
+ return nonce;
68
+ }
69
+
70
+ /**
71
+ * Encrypt data using AES-256-GCM.
72
+ */
73
+ export async function encrypt(
74
+ data: Uint8Array,
75
+ key: EncryptionKey,
76
+ additionalData?: Uint8Array
77
+ ): Promise<EncryptedData> {
78
+ const nonce = generateNonce();
79
+
80
+ const params: AesGcmParams = {
81
+ name: "AES-GCM",
82
+ iv: nonce as unknown as ArrayBuffer,
83
+ tagLength: 128, // 16 bytes
84
+ };
85
+ if (additionalData) {
86
+ params.additionalData = additionalData as unknown as ArrayBuffer;
87
+ }
88
+
89
+ const ciphertextWithTag = await crypto.subtle.encrypt(
90
+ params,
91
+ key.key,
92
+ data as unknown as ArrayBuffer
93
+ );
94
+
95
+ const ciphertextWithTagArray = new Uint8Array(ciphertextWithTag);
96
+
97
+ // AES-GCM appends the tag to the ciphertext
98
+ // Split them for clarity in our data structure
99
+ const tagStart = ciphertextWithTagArray.length - 16;
100
+ const ciphertext = ciphertextWithTagArray.slice(0, tagStart);
101
+ const tag = ciphertextWithTagArray.slice(tagStart);
102
+
103
+ return {
104
+ ciphertext,
105
+ nonce,
106
+ tag,
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Decrypt data using AES-256-GCM.
112
+ */
113
+ export async function decrypt(
114
+ encrypted: EncryptedData,
115
+ key: EncryptionKey,
116
+ additionalData?: Uint8Array
117
+ ): Promise<Uint8Array> {
118
+ // Reconstruct ciphertext with tag appended
119
+ const ciphertextWithTag = new Uint8Array(encrypted.ciphertext.length + encrypted.tag.length);
120
+ ciphertextWithTag.set(encrypted.ciphertext, 0);
121
+ ciphertextWithTag.set(encrypted.tag, encrypted.ciphertext.length);
122
+
123
+ const decryptParams: AesGcmParams = {
124
+ name: "AES-GCM",
125
+ iv: encrypted.nonce as unknown as ArrayBuffer,
126
+ tagLength: 128,
127
+ };
128
+ if (additionalData) {
129
+ decryptParams.additionalData = additionalData as unknown as ArrayBuffer;
130
+ }
131
+
132
+ const plaintext = await crypto.subtle.decrypt(
133
+ decryptParams,
134
+ key.key,
135
+ ciphertextWithTag as unknown as ArrayBuffer
136
+ );
137
+
138
+ return new Uint8Array(plaintext);
139
+ }
140
+
141
+ /**
142
+ * Export key as raw bytes (for sealing/wrapping).
143
+ */
144
+ export async function exportKey(key: EncryptionKey): Promise<Uint8Array> {
145
+ const rawKey = await crypto.subtle.exportKey("raw", key.key);
146
+ return new Uint8Array(rawKey);
147
+ }
148
+
149
+ /**
150
+ * Import key from raw bytes.
151
+ */
152
+ export async function importKey(
153
+ rawKey: Uint8Array,
154
+ keyId: string,
155
+ algorithm: "aes-256-gcm" | "chacha20-poly1305" = "aes-256-gcm"
156
+ ): Promise<EncryptionKey> {
157
+ const key = await crypto.subtle.importKey(
158
+ "raw",
159
+ rawKey as unknown as ArrayBuffer,
160
+ { name: "AES-GCM", length: 256 },
161
+ true,
162
+ ["encrypt", "decrypt"]
163
+ );
164
+
165
+ return {
166
+ keyId,
167
+ key,
168
+ algorithm,
169
+ createdAt: new Date().toISOString(),
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Derive a key from a master key using HKDF-SHA256.
175
+ */
176
+ export async function deriveKey(
177
+ masterKey: Uint8Array,
178
+ info: string,
179
+ salt?: Uint8Array
180
+ ): Promise<EncryptionKey> {
181
+ const baseKey = await crypto.subtle.importKey(
182
+ "raw",
183
+ masterKey as unknown as ArrayBuffer,
184
+ "HKDF",
185
+ false,
186
+ ["deriveKey"]
187
+ );
188
+
189
+ const saltBuffer = salt ?? new Uint8Array(32);
190
+ const infoBuffer = new TextEncoder().encode(info);
191
+ const derivedKey = await crypto.subtle.deriveKey(
192
+ {
193
+ name: "HKDF",
194
+ hash: "SHA-256",
195
+ salt: saltBuffer as unknown as ArrayBuffer,
196
+ info: infoBuffer as unknown as ArrayBuffer,
197
+ },
198
+ baseKey,
199
+ { name: "AES-GCM", length: 256 },
200
+ true,
201
+ ["encrypt", "decrypt"]
202
+ );
203
+
204
+ return {
205
+ keyId: generateKeyId(),
206
+ key: derivedKey,
207
+ algorithm: "aes-256-gcm",
208
+ createdAt: new Date().toISOString(),
209
+ };
210
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Hashing utilities for flight recorder.
3
+ * Provides consistent SHA-256 hashing with domain separation.
4
+ */
5
+
6
+ import { webcrypto } from "node:crypto";
7
+
8
+ const crypto = webcrypto as unknown as Crypto;
9
+
10
+ /**
11
+ * Hash domain prefixes for different data types.
12
+ */
13
+ export const HASH_DOMAINS = {
14
+ event: "poi-flight:event:v2|",
15
+ chunk: "poi-flight:chunk:v2|",
16
+ manifest: "poi-flight:manifest:v2|",
17
+ roll: "poi-flight:roll:v2|",
18
+ merkleLeaf: "poi-flight:leaf:v2|",
19
+ merkleNode: "poi-flight:node:v2|",
20
+ encryptedChunk: "poi-flight:encrypted:v2|",
21
+ } as const;
22
+
23
+ export type HashDomain = keyof typeof HASH_DOMAINS;
24
+
25
+ /**
26
+ * Compute SHA-256 hash of data with domain separation.
27
+ */
28
+ export async function sha256(data: Uint8Array, domain?: HashDomain): Promise<string> {
29
+ let input = data;
30
+
31
+ if (domain) {
32
+ const prefix = new TextEncoder().encode(HASH_DOMAINS[domain]);
33
+ const combined = new Uint8Array(prefix.length + data.length);
34
+ combined.set(prefix, 0);
35
+ combined.set(data, prefix.length);
36
+ input = combined;
37
+ }
38
+
39
+ const hashBuffer = await crypto.subtle.digest("SHA-256", input as unknown as ArrayBuffer);
40
+ return bufferToHex(new Uint8Array(hashBuffer));
41
+ }
42
+
43
+ /**
44
+ * Compute SHA-256 hash of a string.
45
+ */
46
+ export async function sha256String(str: string, domain?: HashDomain): Promise<string> {
47
+ const data = new TextEncoder().encode(str);
48
+ return sha256(data, domain);
49
+ }
50
+
51
+ /**
52
+ * Compute SHA-256 hash of JSON-serializable data.
53
+ */
54
+ export async function sha256Json(obj: unknown, domain?: HashDomain): Promise<string> {
55
+ const json = JSON.stringify(obj, null, 0);
56
+ return sha256String(json, domain);
57
+ }
58
+
59
+ /**
60
+ * Compute rolling hash by combining previous hash with new data.
61
+ */
62
+ export async function rollingHash(prevHash: string, newData: Uint8Array): Promise<string> {
63
+ const prevBytes = hexToBuffer(prevHash);
64
+ const combined = new Uint8Array(prevBytes.length + newData.length);
65
+ combined.set(prevBytes, 0);
66
+ combined.set(newData, prevBytes.length);
67
+ return sha256(combined, "roll");
68
+ }
69
+
70
+ /**
71
+ * Compute Merkle tree leaf hash.
72
+ */
73
+ export async function merkleLeaf(data: Uint8Array): Promise<string> {
74
+ return sha256(data, "merkleLeaf");
75
+ }
76
+
77
+ /**
78
+ * Compute Merkle tree internal node hash.
79
+ */
80
+ export async function merkleNode(left: string, right: string): Promise<string> {
81
+ const leftBytes = hexToBuffer(left);
82
+ const rightBytes = hexToBuffer(right);
83
+ const combined = new Uint8Array(leftBytes.length + rightBytes.length);
84
+ combined.set(leftBytes, 0);
85
+ combined.set(rightBytes, leftBytes.length);
86
+ return sha256(combined, "merkleNode");
87
+ }
88
+
89
+ /**
90
+ * Build a Merkle tree from leaf hashes and return the root.
91
+ */
92
+ export async function buildMerkleRoot(leafHashes: string[]): Promise<string> {
93
+ if (leafHashes.length === 0) {
94
+ // Empty tree - hash of empty string
95
+ return sha256(new Uint8Array(0), "merkleNode");
96
+ }
97
+
98
+ if (leafHashes.length === 1) {
99
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
100
+ return leafHashes[0]!;
101
+ }
102
+
103
+ // Build tree level by level
104
+ let currentLevel = leafHashes;
105
+
106
+ while (currentLevel.length > 1) {
107
+ const nextLevel: string[] = [];
108
+
109
+ for (let i = 0; i < currentLevel.length; i += 2) {
110
+ const left = currentLevel[i];
111
+ const right = currentLevel[i + 1];
112
+ if (left !== undefined && right !== undefined) {
113
+ // Pair exists
114
+ const nodeHash = await merkleNode(left, right);
115
+ nextLevel.push(nodeHash);
116
+ } else if (left !== undefined) {
117
+ // Odd element - promote to next level
118
+ nextLevel.push(left);
119
+ }
120
+ }
121
+
122
+ currentLevel = nextLevel;
123
+ }
124
+
125
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
126
+ return currentLevel[0]!;
127
+ }
128
+
129
+ /**
130
+ * Generate a Merkle proof for a leaf at the given index.
131
+ */
132
+ export interface MerkleProof {
133
+ leafIndex: number;
134
+ leafHash: string;
135
+ siblings: Array<{ hash: string; position: "left" | "right" }>;
136
+ root: string;
137
+ }
138
+
139
+ export async function generateMerkleProof(
140
+ leafHashes: string[],
141
+ leafIndex: number
142
+ ): Promise<MerkleProof> {
143
+ if (leafIndex < 0 || leafIndex >= leafHashes.length) {
144
+ throw new Error(`Invalid leaf index: ${leafIndex}`);
145
+ }
146
+
147
+ const siblings: Array<{ hash: string; position: "left" | "right" }> = [];
148
+ let currentLevel = leafHashes;
149
+ let currentIndex = leafIndex;
150
+
151
+ while (currentLevel.length > 1) {
152
+ const isLeft = currentIndex % 2 === 0;
153
+ const siblingIndex = isLeft ? currentIndex + 1 : currentIndex - 1;
154
+
155
+ const siblingHash = currentLevel[siblingIndex];
156
+ if (siblingIndex < currentLevel.length && siblingHash !== undefined) {
157
+ siblings.push({
158
+ hash: siblingHash,
159
+ position: isLeft ? "right" : "left",
160
+ });
161
+ }
162
+
163
+ // Build next level
164
+ const nextLevel: string[] = [];
165
+ for (let i = 0; i < currentLevel.length; i += 2) {
166
+ const left = currentLevel[i];
167
+ const right = currentLevel[i + 1];
168
+ if (left !== undefined && right !== undefined) {
169
+ const nodeHash = await merkleNode(left, right);
170
+ nextLevel.push(nodeHash);
171
+ } else if (left !== undefined) {
172
+ nextLevel.push(left);
173
+ }
174
+ }
175
+
176
+ currentLevel = nextLevel;
177
+ currentIndex = Math.floor(currentIndex / 2);
178
+ }
179
+
180
+ const leafHash = leafHashes[leafIndex];
181
+ const root = currentLevel[0];
182
+ if (leafHash === undefined || root === undefined) {
183
+ throw new Error("Invalid merkle tree state");
184
+ }
185
+
186
+ return {
187
+ leafIndex,
188
+ leafHash,
189
+ siblings,
190
+ root,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Verify a Merkle proof.
196
+ */
197
+ export async function verifyMerkleProof(proof: MerkleProof): Promise<boolean> {
198
+ let currentHash = proof.leafHash;
199
+
200
+ for (const sibling of proof.siblings) {
201
+ if (sibling.position === "left") {
202
+ currentHash = await merkleNode(sibling.hash, currentHash);
203
+ } else {
204
+ currentHash = await merkleNode(currentHash, sibling.hash);
205
+ }
206
+ }
207
+
208
+ return currentHash === proof.root;
209
+ }
210
+
211
+ // === Utility functions ===
212
+
213
+ function bufferToHex(buffer: Uint8Array): string {
214
+ return Array.from(buffer)
215
+ .map((b) => b.toString(16).padStart(2, "0"))
216
+ .join("");
217
+ }
218
+
219
+ function hexToBuffer(hex: string): Uint8Array {
220
+ const bytes = new Uint8Array(hex.length / 2);
221
+ for (let i = 0; i < bytes.length; i++) {
222
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
223
+ }
224
+ return bytes;
225
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./encryption.js";
2
+ export * from "./compression.js";
3
+ export * from "./hashing.js";
package/src/index.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @fluxpointstudios/orynq-sdk-flight-recorder
3
+ *
4
+ * Streaming flight recorder for Proof of Inference (PoI).
5
+ * Captures inference events with chunking, compression, and encryption.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { FlightRecorder, LocalStorageAdapter } from '@fluxpointstudios/orynq-sdk-flight-recorder';
10
+ *
11
+ * const storage = new LocalStorageAdapter({ baseDir: './traces' });
12
+ *
13
+ * const recorder = new FlightRecorder({
14
+ * agentId: 'my-agent',
15
+ * chunkSizeBytes: 4 * 1024 * 1024, // 4MB chunks
16
+ * encryption: {
17
+ * algorithm: 'aes-256-gcm',
18
+ * keyDerivation: 'hkdf-sha256',
19
+ * keyMode: { type: 'ephemeral' },
20
+ * },
21
+ * storage,
22
+ * });
23
+ *
24
+ * await recorder.start();
25
+ *
26
+ * await recorder.record({
27
+ * kind: 'inference:start',
28
+ * requestId: 'req-123',
29
+ * model: 'claude-3-opus',
30
+ * promptHash: '...',
31
+ * params: { temperature: 0.7 },
32
+ * });
33
+ *
34
+ * const result = await recorder.finalize();
35
+ * console.log('Manifest:', result.manifest);
36
+ * ```
37
+ */
38
+
39
+ // Types
40
+ export * from "./types.js";
41
+
42
+ // Core recorder
43
+ export { FlightRecorder } from "./recorder/stream-recorder.js";
44
+ export { EventBuffer } from "./recorder/event-buffer.js";
45
+ export { ChunkManager } from "./recorder/chunk-manager.js";
46
+
47
+ // Crypto utilities
48
+ export {
49
+ generateKey,
50
+ encrypt,
51
+ decrypt,
52
+ exportKey,
53
+ importKey,
54
+ deriveKey,
55
+ type EncryptionKey,
56
+ type EncryptedData,
57
+ } from "./crypto/encryption.js";
58
+
59
+ export {
60
+ compress,
61
+ decompress,
62
+ compressString,
63
+ decompressString,
64
+ isCompressible,
65
+ type CompressionType,
66
+ type CompressionResult,
67
+ } from "./crypto/compression.js";
68
+
69
+ export {
70
+ sha256,
71
+ sha256String,
72
+ sha256Json,
73
+ rollingHash,
74
+ merkleLeaf,
75
+ merkleNode,
76
+ buildMerkleRoot,
77
+ generateMerkleProof,
78
+ verifyMerkleProof,
79
+ HASH_DOMAINS,
80
+ type HashDomain,
81
+ type MerkleProof,
82
+ } from "./crypto/hashing.js";
83
+
84
+ // Manifest
85
+ export { ManifestBuilder, type ManifestInput } from "./manifest/manifest-builder.js";
86
+
87
+ // Storage
88
+ export { LocalStorageAdapter, type LocalStorageConfig } from "./storage/local-adapter.js";
89
+
90
+ // Integration
91
+ export {
92
+ OpenClawAdapter,
93
+ type LegacyTraceEvent,
94
+ type LegacyTraceBundle,
95
+ type LegacyManifest,
96
+ } from "./integration/openclaw-adapter.js";
@@ -0,0 +1 @@
1
+ export { OpenClawAdapter } from "./openclaw-adapter.js";