@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,206 @@
1
+ /**
2
+ * Adapter for migrating from poi-openclaw to flight-recorder.
3
+ * Bridges legacy trace formats to the new ManifestV2 format.
4
+ */
5
+
6
+ import type {
7
+ RecorderConfig,
8
+ RecordingResult,
9
+ ManifestV2,
10
+ InferenceStartEvent,
11
+ InferenceEndEvent,
12
+ ToolCallEvent,
13
+ ToolResultEvent,
14
+ } from "../types.js";
15
+ import { FlightRecorder } from "../recorder/stream-recorder.js";
16
+ import { sha256String } from "../crypto/hashing.js";
17
+
18
+ /**
19
+ * Partial event types without seq and ts (added by recorder).
20
+ */
21
+ type PartialInferenceStart = Omit<InferenceStartEvent, "seq" | "ts">;
22
+ type PartialInferenceEnd = Omit<InferenceEndEvent, "seq" | "ts">;
23
+ type PartialToolCall = Omit<ToolCallEvent, "seq" | "ts">;
24
+ type PartialToolResult = Omit<ToolResultEvent, "seq" | "ts">;
25
+ type PartialRecorderEvent = PartialInferenceStart | PartialInferenceEnd | PartialToolCall | PartialToolResult;
26
+
27
+ /**
28
+ * Legacy trace event format from poi-openclaw.
29
+ */
30
+ export interface LegacyTraceEvent {
31
+ ts: string;
32
+ kind: "user" | "assistant" | "tool_call" | "tool_result";
33
+ agentId?: string;
34
+ sessionId?: string;
35
+ contentHash: string;
36
+ content?: string | null;
37
+ meta?: Record<string, unknown>;
38
+ }
39
+
40
+ /**
41
+ * Legacy trace bundle from poi-openclaw.
42
+ */
43
+ export interface LegacyTraceBundle {
44
+ bundleId: string;
45
+ events: LegacyTraceEvent[];
46
+ }
47
+
48
+ /**
49
+ * Legacy manifest format from poi-openclaw.
50
+ */
51
+ export interface LegacyManifest {
52
+ formatVersion: string;
53
+ agentId: string;
54
+ rootHash: string;
55
+ manifestHash: string;
56
+ merkleRoot: string;
57
+ totalEvents: number;
58
+ totalSpans: number;
59
+ createdAt: string;
60
+ metadata?: { bundlePath?: string };
61
+ }
62
+
63
+ /**
64
+ * Adapter to bridge poi-openclaw traces to flight-recorder.
65
+ */
66
+ export class OpenClawAdapter {
67
+ constructor(private readonly config: RecorderConfig) {}
68
+
69
+ /**
70
+ * Convert a legacy trace bundle to a new recording.
71
+ */
72
+ async importLegacyBundle(bundle: LegacyTraceBundle): Promise<RecordingResult> {
73
+ const recorder = new FlightRecorder({
74
+ ...this.config,
75
+ sessionId: bundle.bundleId,
76
+ });
77
+
78
+ await recorder.start();
79
+
80
+ for (const event of bundle.events) {
81
+ const converted = await this.convertEvent(event);
82
+ if (converted) {
83
+ await recorder.record(converted);
84
+ }
85
+ }
86
+
87
+ return recorder.finalize();
88
+ }
89
+
90
+ /**
91
+ * Convert a legacy event to a RecorderEvent.
92
+ */
93
+ private convertEvent(event: LegacyTraceEvent): PartialRecorderEvent | null {
94
+ switch (event.kind) {
95
+ case "user": {
96
+ const result: PartialInferenceStart = {
97
+ kind: "inference:start",
98
+ requestId: `req-${Date.now()}`,
99
+ model: "unknown",
100
+ promptHash: event.contentHash,
101
+ params: {},
102
+ };
103
+ if (event.sessionId) result.spanId = event.sessionId;
104
+ return result;
105
+ }
106
+
107
+ case "assistant": {
108
+ const result: PartialInferenceEnd = {
109
+ kind: "inference:end",
110
+ requestId: `req-${Date.now()}`,
111
+ outputHash: event.contentHash,
112
+ tokenCounts: { prompt: 0, completion: 0 },
113
+ durationMs: 0,
114
+ };
115
+ if (event.sessionId) result.spanId = event.sessionId;
116
+ return result;
117
+ }
118
+
119
+ case "tool_call": {
120
+ const result: PartialToolCall = {
121
+ kind: "tool:call",
122
+ toolName: (event.meta?.toolName as string) || "unknown",
123
+ argsHash: event.contentHash,
124
+ visibility: "private",
125
+ };
126
+ if (event.sessionId) result.spanId = event.sessionId;
127
+ return result;
128
+ }
129
+
130
+ case "tool_result": {
131
+ const result: PartialToolResult = {
132
+ kind: "tool:result",
133
+ toolName: (event.meta?.toolName as string) || "unknown",
134
+ resultHash: event.contentHash,
135
+ success: true,
136
+ visibility: "private",
137
+ };
138
+ if (event.sessionId) result.spanId = event.sessionId;
139
+ return result;
140
+ }
141
+
142
+ default:
143
+ return null;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Convert a legacy manifest to ManifestV2 format.
149
+ */
150
+ async convertManifest(legacy: LegacyManifest): Promise<ManifestV2> {
151
+ const now = new Date().toISOString();
152
+
153
+ return {
154
+ formatVersion: "2.0",
155
+ agentId: legacy.agentId,
156
+ sessionId: legacy.metadata?.bundlePath || `migrated-${Date.now()}`,
157
+
158
+ rootHash: legacy.rootHash,
159
+ merkleRoot: legacy.merkleRoot,
160
+ manifestHash: await sha256String(JSON.stringify(legacy)),
161
+
162
+ inputs: {
163
+ promptHash: "", // Not available in legacy format
164
+ },
165
+
166
+ params: {
167
+ model: "unknown",
168
+ },
169
+
170
+ runtime: {
171
+ recorderVersion: "migrated-from-openclaw",
172
+ },
173
+
174
+ chunks: [], // Legacy format doesn't have chunks
175
+
176
+ outputs: {
177
+ transcriptRollingHash: legacy.rootHash,
178
+ toolCallCount: 0,
179
+ totalTokens: 0,
180
+ completionTokens: 0,
181
+ },
182
+
183
+ createdAt: legacy.createdAt,
184
+ startedAt: legacy.createdAt,
185
+ endedAt: now,
186
+ durationMs: 0,
187
+
188
+ totalEvents: legacy.totalEvents,
189
+ totalSpans: legacy.totalSpans,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Check if a manifest is in legacy format.
195
+ */
196
+ static isLegacyManifest(manifest: unknown): manifest is LegacyManifest {
197
+ if (!manifest || typeof manifest !== "object") return false;
198
+ const m = manifest as Record<string, unknown>;
199
+ return (
200
+ typeof m.formatVersion === "string" &&
201
+ m.formatVersion.startsWith("1.") &&
202
+ typeof m.agentId === "string" &&
203
+ typeof m.rootHash === "string"
204
+ );
205
+ }
206
+ }
@@ -0,0 +1 @@
1
+ export { ManifestBuilder } from "./manifest-builder.js";
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Manifest builder for flight recorder.
3
+ * Constructs ManifestV2 from recording data.
4
+ */
5
+
6
+ import type { ManifestV2, ChunkRef, VerifierPolicy, TeeType } from "../types.js";
7
+ import { sha256Json } from "../crypto/hashing.js";
8
+
9
+ export interface ManifestInput {
10
+ agentId: string;
11
+ sessionId: string;
12
+ rootHash: string;
13
+ merkleRoot: string;
14
+ chunks: ChunkRef[];
15
+
16
+ inputs: {
17
+ promptHash: string;
18
+ systemPromptHash?: string;
19
+ toolContextHash?: string;
20
+ };
21
+
22
+ params: {
23
+ model: string;
24
+ temperature?: number;
25
+ topP?: number;
26
+ topK?: number;
27
+ maxTokens?: number;
28
+ };
29
+
30
+ runtime: {
31
+ recorderVersion: string;
32
+ nodeVersion?: string;
33
+ containerDigest?: string;
34
+ gitCommit?: string;
35
+ };
36
+
37
+ outputs: {
38
+ transcriptRollingHash: string;
39
+ toolCallCount: number;
40
+ totalTokens: number;
41
+ completionTokens: number;
42
+ };
43
+
44
+ timing: {
45
+ startedAt: string;
46
+ endedAt: string;
47
+ durationMs: number;
48
+ };
49
+
50
+ stats: {
51
+ totalEvents: number;
52
+ totalSpans: number;
53
+ };
54
+
55
+ attestation?: {
56
+ teeType: TeeType;
57
+ evidenceHash: string;
58
+ evidenceUri?: string;
59
+ verifierPolicy: VerifierPolicy;
60
+ boundHash: "rootHash" | "manifestHash" | "merkleRoot";
61
+ };
62
+ }
63
+
64
+ export class ManifestBuilder {
65
+ /**
66
+ * Build a ManifestV2 from input data.
67
+ */
68
+ async build(input: ManifestInput): Promise<ManifestV2> {
69
+ const manifest: ManifestV2 = {
70
+ formatVersion: "2.0",
71
+
72
+ agentId: input.agentId,
73
+ sessionId: input.sessionId,
74
+
75
+ rootHash: input.rootHash,
76
+ merkleRoot: input.merkleRoot,
77
+ manifestHash: "", // Computed below
78
+
79
+ inputs: input.inputs,
80
+ params: input.params,
81
+ runtime: input.runtime,
82
+ chunks: input.chunks,
83
+ outputs: input.outputs,
84
+
85
+ createdAt: new Date().toISOString(),
86
+ startedAt: input.timing.startedAt,
87
+ endedAt: input.timing.endedAt,
88
+ durationMs: input.timing.durationMs,
89
+
90
+ totalEvents: input.stats.totalEvents,
91
+ totalSpans: input.stats.totalSpans,
92
+ };
93
+
94
+ // Add attestation if provided
95
+ if (input.attestation) {
96
+ manifest.attestation = input.attestation;
97
+ }
98
+
99
+ // Compute manifest hash
100
+ manifest.manifestHash = await this.computeManifestHash(manifest);
101
+
102
+ return manifest;
103
+ }
104
+
105
+ /**
106
+ * Compute the manifest hash.
107
+ * Hash is computed over the manifest with manifestHash set to empty string.
108
+ */
109
+ async computeManifestHash(manifest: ManifestV2): Promise<string> {
110
+ const toHash = { ...manifest, manifestHash: "" };
111
+ return sha256Json(toHash, "manifest");
112
+ }
113
+
114
+ /**
115
+ * Verify a manifest's integrity.
116
+ */
117
+ async verify(manifest: ManifestV2): Promise<boolean> {
118
+ const expectedHash = await this.computeManifestHash(manifest);
119
+ return manifest.manifestHash === expectedHash;
120
+ }
121
+
122
+ /**
123
+ * Create a minimal manifest for testing.
124
+ */
125
+ static createMinimal(agentId: string, sessionId: string): ManifestV2 {
126
+ const now = new Date().toISOString();
127
+ return {
128
+ formatVersion: "2.0",
129
+ agentId,
130
+ sessionId,
131
+ rootHash: "",
132
+ merkleRoot: "",
133
+ manifestHash: "",
134
+ inputs: { promptHash: "" },
135
+ params: { model: "unknown" },
136
+ runtime: { recorderVersion: "0.1.0" },
137
+ chunks: [],
138
+ outputs: {
139
+ transcriptRollingHash: "",
140
+ toolCallCount: 0,
141
+ totalTokens: 0,
142
+ completionTokens: 0,
143
+ },
144
+ createdAt: now,
145
+ startedAt: now,
146
+ endedAt: now,
147
+ durationMs: 0,
148
+ totalEvents: 0,
149
+ totalSpans: 0,
150
+ };
151
+ }
152
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Chunk manager for the flight recorder.
3
+ * Handles chunk creation, compression, encryption, and storage.
4
+ */
5
+
6
+ import type {
7
+ ChunkRef,
8
+ ChunkData,
9
+ StorageAdapter,
10
+ EncryptionConfig,
11
+ } from "../types.js";
12
+ import { generateKey, encrypt, type EncryptionKey } from "../crypto/encryption.js";
13
+ import { compress, type CompressionType } from "../crypto/compression.js";
14
+ import { sha256, merkleLeaf } from "../crypto/hashing.js";
15
+ import type { BufferedEvent } from "./event-buffer.js";
16
+
17
+ export interface ChunkCreateResult {
18
+ chunkRef: ChunkRef;
19
+ chunkData: ChunkData;
20
+ leafHash: string;
21
+ }
22
+
23
+ export class ChunkManager {
24
+ private chunks: ChunkRef[] = [];
25
+ private leafHashes: string[] = [];
26
+ private chunkIndex = 0;
27
+ private encryptionKey: EncryptionKey | null = null;
28
+
29
+ constructor(
30
+ private readonly storage: StorageAdapter,
31
+ private readonly encryptionConfig: EncryptionConfig,
32
+ private readonly compressionType: CompressionType = "gzip"
33
+ ) {}
34
+
35
+ /**
36
+ * Initialize encryption key for the session.
37
+ */
38
+ async initializeKey(): Promise<void> {
39
+ this.encryptionKey = await generateKey(this.encryptionConfig.algorithm);
40
+ }
41
+
42
+ /**
43
+ * Get the current encryption key ID.
44
+ */
45
+ getKeyId(): string | null {
46
+ return this.encryptionKey?.keyId ?? null;
47
+ }
48
+
49
+ /**
50
+ * Create a chunk from buffered events.
51
+ */
52
+ async createChunk(
53
+ events: BufferedEvent[],
54
+ eventRange: [number, number],
55
+ spanIds: string[]
56
+ ): Promise<ChunkCreateResult> {
57
+ if (!this.encryptionKey) {
58
+ throw new Error("Encryption key not initialized. Call initializeKey() first.");
59
+ }
60
+
61
+ // Serialize events to JSONL
62
+ const lines = events.map((e) => JSON.stringify(e.event));
63
+ const jsonl = lines.join("\n");
64
+ const rawData = new TextEncoder().encode(jsonl);
65
+
66
+ // Compress
67
+ const compressed = await compress(rawData, this.compressionType);
68
+
69
+ // Encrypt
70
+ const chunkMeta = {
71
+ index: this.chunkIndex,
72
+ eventRange,
73
+ spanIds,
74
+ createdAt: new Date().toISOString(),
75
+ };
76
+ const additionalData = new TextEncoder().encode(JSON.stringify(chunkMeta));
77
+ const encrypted = await encrypt(compressed.data, this.encryptionKey, additionalData);
78
+
79
+ // Create chunk data structure
80
+ const chunkData: ChunkData = {
81
+ ciphertext: encrypted.ciphertext,
82
+ nonce: encrypted.nonce,
83
+ tag: encrypted.tag,
84
+ meta: chunkMeta,
85
+ };
86
+
87
+ // Compute content hash of encrypted data
88
+ const encryptedPayload = new Uint8Array(
89
+ encrypted.ciphertext.length + encrypted.nonce.length + encrypted.tag.length
90
+ );
91
+ encryptedPayload.set(encrypted.ciphertext, 0);
92
+ encryptedPayload.set(encrypted.nonce, encrypted.ciphertext.length);
93
+ encryptedPayload.set(encrypted.tag, encrypted.ciphertext.length + encrypted.nonce.length);
94
+
95
+ const chunkHash = await sha256(encryptedPayload, "encryptedChunk");
96
+ const chunkId = `chunk-${this.chunkIndex.toString().padStart(6, "0")}-${chunkHash.slice(0, 16)}`;
97
+
98
+ // Store chunk
99
+ const storagePayload = this.serializeChunkForStorage(chunkData);
100
+ const storageRef = await this.storage.store(storagePayload);
101
+
102
+ // Create chunk reference
103
+ const chunkRef: ChunkRef = {
104
+ id: chunkId,
105
+ hash: chunkHash,
106
+ size: storagePayload.length,
107
+ storageUri: storageRef.uri,
108
+ encryptionKeyId: this.encryptionKey.keyId,
109
+ compression: this.compressionType === "gzip" ? "gzip" : "none",
110
+ };
111
+
112
+ // Compute Merkle leaf hash
113
+ const leafHash = await merkleLeaf(encryptedPayload);
114
+
115
+ // Track chunk
116
+ this.chunks.push(chunkRef);
117
+ this.leafHashes.push(leafHash);
118
+ this.chunkIndex++;
119
+
120
+ return {
121
+ chunkRef,
122
+ chunkData,
123
+ leafHash,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Get all chunk references.
129
+ */
130
+ getChunks(): ChunkRef[] {
131
+ return [...this.chunks];
132
+ }
133
+
134
+ /**
135
+ * Get all Merkle leaf hashes.
136
+ */
137
+ getLeafHashes(): string[] {
138
+ return [...this.leafHashes];
139
+ }
140
+
141
+ /**
142
+ * Get chunk count.
143
+ */
144
+ getChunkCount(): number {
145
+ return this.chunks.length;
146
+ }
147
+
148
+ /**
149
+ * Serialize chunk data for storage.
150
+ * Format: [4-byte meta length][meta JSON][nonce][tag][ciphertext]
151
+ */
152
+ private serializeChunkForStorage(chunk: ChunkData): Uint8Array {
153
+ const metaJson = JSON.stringify(chunk.meta);
154
+ const metaBytes = new TextEncoder().encode(metaJson);
155
+ const metaLength = new Uint32Array([metaBytes.length]);
156
+ const metaLengthBytes = new Uint8Array(metaLength.buffer);
157
+
158
+ const totalLength =
159
+ 4 + // meta length
160
+ metaBytes.length +
161
+ chunk.nonce.length +
162
+ chunk.tag.length +
163
+ chunk.ciphertext.length;
164
+
165
+ const result = new Uint8Array(totalLength);
166
+ let offset = 0;
167
+
168
+ result.set(metaLengthBytes, offset);
169
+ offset += 4;
170
+
171
+ result.set(metaBytes, offset);
172
+ offset += metaBytes.length;
173
+
174
+ result.set(chunk.nonce, offset);
175
+ offset += chunk.nonce.length;
176
+
177
+ result.set(chunk.tag, offset);
178
+ offset += chunk.tag.length;
179
+
180
+ result.set(chunk.ciphertext, offset);
181
+
182
+ return result;
183
+ }
184
+
185
+ /**
186
+ * Deserialize chunk data from storage.
187
+ */
188
+ static deserializeChunkFromStorage(data: Uint8Array): ChunkData {
189
+ let offset = 0;
190
+
191
+ // Read meta length
192
+ const metaLengthBytes = data.slice(offset, offset + 4);
193
+ const metaLengthArr = new Uint32Array(metaLengthBytes.buffer);
194
+ const metaLength = metaLengthArr[0];
195
+ if (metaLength === undefined) {
196
+ throw new Error("Invalid chunk data: missing meta length");
197
+ }
198
+ offset += 4;
199
+
200
+ // Read meta
201
+ const metaBytes = data.slice(offset, offset + metaLength);
202
+ const metaJson = new TextDecoder().decode(metaBytes);
203
+ const meta = JSON.parse(metaJson) as ChunkData["meta"];
204
+ offset += metaLength;
205
+
206
+ // Read nonce (12 bytes for AES-GCM)
207
+ const nonce = data.slice(offset, offset + 12);
208
+ offset += 12;
209
+
210
+ // Read tag (16 bytes)
211
+ const tag = data.slice(offset, offset + 16);
212
+ offset += 16;
213
+
214
+ // Read ciphertext (rest)
215
+ const ciphertext = data.slice(offset);
216
+
217
+ return {
218
+ ciphertext,
219
+ nonce,
220
+ tag,
221
+ meta,
222
+ };
223
+ }
224
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * In-memory event buffer for the flight recorder.
3
+ * Buffers events until chunk threshold is reached.
4
+ */
5
+
6
+ import type { RecorderEvent } from "../types.js";
7
+ import { sha256Json } from "../crypto/hashing.js";
8
+
9
+ export interface BufferedEvent {
10
+ event: RecorderEvent;
11
+ serialized: Uint8Array;
12
+ hash: string;
13
+ }
14
+
15
+ export interface BufferStats {
16
+ eventCount: number;
17
+ byteSize: number;
18
+ oldestEventTs: string | null;
19
+ newestEventTs: string | null;
20
+ }
21
+
22
+ export class EventBuffer {
23
+ private events: BufferedEvent[] = [];
24
+ private byteSize = 0;
25
+ private seq = 0;
26
+
27
+ constructor(private readonly maxSizeBytes: number) {}
28
+
29
+ /**
30
+ * Add an event to the buffer.
31
+ * Returns true if buffer should be flushed (threshold reached).
32
+ */
33
+ async add(event: Omit<RecorderEvent, "seq">): Promise<boolean> {
34
+ const seqEvent = { ...event, seq: this.seq++ } as RecorderEvent;
35
+ const serialized = new TextEncoder().encode(JSON.stringify(seqEvent));
36
+ const hash = await sha256Json(seqEvent, "event");
37
+
38
+ this.events.push({
39
+ event: seqEvent,
40
+ serialized,
41
+ hash,
42
+ });
43
+
44
+ this.byteSize += serialized.length;
45
+
46
+ return this.byteSize >= this.maxSizeBytes;
47
+ }
48
+
49
+ /**
50
+ * Get all buffered events and clear the buffer.
51
+ */
52
+ flush(): BufferedEvent[] {
53
+ const events = this.events;
54
+ this.events = [];
55
+ this.byteSize = 0;
56
+ return events;
57
+ }
58
+
59
+ /**
60
+ * Peek at buffered events without clearing.
61
+ */
62
+ peek(): BufferedEvent[] {
63
+ return [...this.events];
64
+ }
65
+
66
+ /**
67
+ * Get buffer statistics.
68
+ */
69
+ getStats(): BufferStats {
70
+ return {
71
+ eventCount: this.events.length,
72
+ byteSize: this.byteSize,
73
+ oldestEventTs: this.events[0]?.event.ts ?? null,
74
+ newestEventTs: this.events[this.events.length - 1]?.event.ts ?? null,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Check if buffer is empty.
80
+ */
81
+ isEmpty(): boolean {
82
+ return this.events.length === 0;
83
+ }
84
+
85
+ /**
86
+ * Get current sequence number.
87
+ */
88
+ getSeq(): number {
89
+ return this.seq;
90
+ }
91
+
92
+ /**
93
+ * Get event range in buffer.
94
+ */
95
+ getEventRange(): [number, number] | null {
96
+ if (this.events.length === 0) return null;
97
+ const first = this.events[0];
98
+ const last = this.events[this.events.length - 1];
99
+ if (!first || !last) return null;
100
+ return [first.event.seq, last.event.seq];
101
+ }
102
+
103
+ /**
104
+ * Get unique span IDs in buffer.
105
+ */
106
+ getSpanIds(): string[] {
107
+ const spanIds = new Set<string>();
108
+ for (const { event } of this.events) {
109
+ if (event.spanId) {
110
+ spanIds.add(event.spanId);
111
+ }
112
+ }
113
+ return Array.from(spanIds);
114
+ }
115
+
116
+ /**
117
+ * Serialize all events to a single byte array (for chunking).
118
+ */
119
+ serialize(): Uint8Array {
120
+ const lines = this.events.map((e) => JSON.stringify(e.event));
121
+ const jsonl = lines.join("\n");
122
+ return new TextEncoder().encode(jsonl);
123
+ }
124
+ }
@@ -0,0 +1,3 @@
1
+ export { FlightRecorder } from "./stream-recorder.js";
2
+ export { EventBuffer } from "./event-buffer.js";
3
+ export { ChunkManager } from "./chunk-manager.js";