@usherlabs/cex-broker 0.2.43 → 0.2.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -735,7 +735,7 @@ Every successful `CreateOrder` response, successful `GetOrderDetails` response,
735
735
  - Execution values: requested quantity/notional, executed base quantity, executed quote quantity/cost, average execution price, filled amount, remaining amount, fee amount, fee currency, fee rate
736
736
  - Timing: exchange timestamp when present and broker observed timestamp
737
737
 
738
- Use metrics for aggregations and alerts. For the durable execution audit trail, the broker archives every order lifecycle event to `broker_execution.order_events` (and pre-order top-of-book to `broker_execution.market_metadata_snapshots`) through the **archive forwarder** — the same HTTP `/archive` → ClickHouse path used for `market_data.*`. Set `CEX_BROKER_ARCHIVE_ENABLED=true`, an explicit HTTP(S) `CEX_BROKER_ARCHIVE_FORWARDER_URL`, and a writable durable JSONL path in `CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH`; startup fails if either required sink configuration is missing or invalid. In production, that path must be on persistent writable storage or a mounted volume rather than the container's ephemeral filesystem. Queue shedding and rows that remain undeliverable during shutdown are written to that loss journal with their original `{table,row}` payload before being discarded. Setting `CEX_BROKER_ARCHIVE_OTEL_LOGS_ENABLED=true` additionally mirrors execution rows to OTel logs for observability, but OTel is never the archive sink of record. Analysts join Maker action rows to `broker_execution.order_events` using `maker_action_id`, `idempotency_id`, `client_order_id`, or the exchange `order_id`, then compare Maker propAMM execution price against `average_execution_price` and fees. Failed CreateOrder rows keep bounded exchange error detail in `error_message`; their telemetry-shaped `payload_json`, metrics, and ordinary telemetry logs remain redacted. The broker does not emit raw exchange payloads, API keys, secrets, or credentials in telemetry fields.
738
+ Use metrics for aggregations and alerts. For the durable execution audit trail, the broker archives every order lifecycle event to `broker_execution.order_events` (and pre-order top-of-book to `broker_execution.market_metadata_snapshots`) through the **archive forwarder** — the same HTTP `/archive` → ClickHouse path used for `market_data.*`. Set `CEX_BROKER_ARCHIVE_ENABLED=true`, an explicit HTTP(S) `CEX_BROKER_ARCHIVE_FORWARDER_URL`, and a writable durable JSONL path in `CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH`; startup fails if either required sink configuration is missing or invalid. In production, that path must be on persistent writable storage or a mounted volume rather than the container's ephemeral filesystem. Queue shedding, rows that remain undeliverable during shutdown, and batches the forwarder rejected on every attempt are written to that loss journal with their original `{table,row}` payload before being discarded. Replay them with `bun run archive:loss:replay --journal <path>`, which re-posts through the forwarder and reports a per-entry verdict; it is idempotent across reruns, never modifies the journal, and only archives it once every entry is terminal and the operator asserts `--broker-stopped`. When the journal lives on an encrypted enclave mount (SGX deployments), its host-side bytes are sealed ciphertext the replay tool cannot read; set `CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH` to a path on a plain (untrusted) mount and the broker exports a boot-time plaintext snapshot of the journal there, with an adjacent `.sha256` receipt whose digest is also logged from inside the enclave for integrity comparison. The export is one-shot — an existing target file is left untouched, so remove it to request a fresh snapshot — and a failed export fails startup. Setting `CEX_BROKER_ARCHIVE_OTEL_LOGS_ENABLED=true` additionally mirrors execution rows to OTel logs for observability, but OTel is never the archive sink of record. Analysts join Maker action rows to `broker_execution.order_events` using `maker_action_id`, `idempotency_id`, `client_order_id`, or the exchange `order_id`, then compare Maker propAMM execution price against `average_execution_price` and fees. Failed CreateOrder rows keep bounded exchange error detail in `error_message`; their telemetry-shaped `payload_json`, metrics, and ordinary telemetry logs remain redacted. The broker does not emit raw exchange payloads, API keys, secrets, or credentials in telemetry fields.
739
739
 
740
740
  ### User-stream health archive contract
741
741
 
@@ -315811,11 +315811,118 @@ function compactUndefined2(record) {
315811
315811
  // src/helpers/broker-execution-archive/writer.ts
315812
315812
  var import_api_logs2 = __toESM(require_src7(), 1);
315813
315813
  import { randomUUID } from "node:crypto";
315814
- import { closeSync, fsyncSync, openSync, writeSync } from "node:fs";
315814
+ import { closeSync as closeSync2, fsyncSync as fsyncSync2, openSync as openSync2, writeSync as writeSync2 } from "node:fs";
315815
315815
  import {
315816
315816
  request as httpRequest2
315817
315817
  } from "node:http";
315818
315818
  import { request as httpsRequest2 } from "node:https";
315819
+
315820
+ // src/helpers/broker-execution-archive/journal-export.ts
315821
+ import { createHash as createHash3 } from "node:crypto";
315822
+ import {
315823
+ closeSync,
315824
+ existsSync,
315825
+ fsyncSync,
315826
+ openSync,
315827
+ readSync,
315828
+ renameSync,
315829
+ writeSync
315830
+ } from "node:fs";
315831
+ import { basename } from "node:path";
315832
+ var EXPORT_CHUNK_BYTES = 8 * 1024 * 1024;
315833
+
315834
+ class DeadLetterJournalExportError extends Error {
315835
+ constructor(message, options) {
315836
+ super(message, options);
315837
+ this.name = "DeadLetterJournalExportError";
315838
+ }
315839
+ }
315840
+ function exportDeadLetterJournalFromEnv() {
315841
+ const exportPath = process.env.CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH?.trim();
315842
+ if (!exportPath) {
315843
+ return { status: "disabled" };
315844
+ }
315845
+ const journalPath = process.env.CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH?.trim();
315846
+ if (!journalPath) {
315847
+ throw new DeadLetterJournalExportError("CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH is set but CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH is missing");
315848
+ }
315849
+ return exportDeadLetterJournal(journalPath, exportPath);
315850
+ }
315851
+ function exportDeadLetterJournal(journalPath, exportPath) {
315852
+ if (existsSync(exportPath)) {
315853
+ log.info("Dead-letter journal export target already exists, skipping", {
315854
+ export_path: exportPath
315855
+ });
315856
+ return { status: "skipped_export_exists", exportPath };
315857
+ }
315858
+ if (!existsSync(journalPath)) {
315859
+ log.warn("Dead-letter journal export requested but no journal exists, skipping", { journal_path: journalPath });
315860
+ return { status: "skipped_missing_journal", journalPath };
315861
+ }
315862
+ const partialPath = `${exportPath}.partial`;
315863
+ let bytes2 = 0;
315864
+ let digest;
315865
+ try {
315866
+ const hash4 = createHash3("sha256");
315867
+ const sourceFd = openSync(journalPath, "r");
315868
+ try {
315869
+ const targetFd = openSync(partialPath, "w", 384);
315870
+ try {
315871
+ const chunk = Buffer.alloc(EXPORT_CHUNK_BYTES);
315872
+ for (;; ) {
315873
+ const read = readSync(sourceFd, chunk, 0, chunk.length, null);
315874
+ if (read === 0) {
315875
+ break;
315876
+ }
315877
+ const view = chunk.subarray(0, read);
315878
+ hash4.update(view);
315879
+ writeSync(targetFd, view);
315880
+ bytes2 += read;
315881
+ }
315882
+ fsyncSync(targetFd);
315883
+ } finally {
315884
+ closeSync(targetFd);
315885
+ }
315886
+ } finally {
315887
+ closeSync(sourceFd);
315888
+ }
315889
+ digest = hash4.digest("hex");
315890
+ renameSync(partialPath, exportPath);
315891
+ const receiptFd = openSync(`${exportPath}.sha256`, "w", 384);
315892
+ try {
315893
+ writeSync(receiptFd, `${digest} ${basename(exportPath)}
315894
+ `);
315895
+ fsyncSync(receiptFd);
315896
+ } finally {
315897
+ closeSync(receiptFd);
315898
+ }
315899
+ } catch (error) {
315900
+ throw new DeadLetterJournalExportError("Dead-letter journal export failed", { cause: error });
315901
+ }
315902
+ log.info("Dead-letter journal exported", {
315903
+ journal_path: journalPath,
315904
+ export_path: exportPath,
315905
+ bytes: bytes2,
315906
+ sha256: digest
315907
+ });
315908
+ return {
315909
+ status: "exported",
315910
+ journalPath,
315911
+ exportPath,
315912
+ bytes: bytes2,
315913
+ sha256: digest
315914
+ };
315915
+ }
315916
+
315917
+ // src/helpers/broker-execution-archive/loss-journal.ts
315918
+ var LOSS_JOURNAL_RECORD_VERSION = 1;
315919
+ var LOSS_REASONS = new Set([
315920
+ "queue_shed",
315921
+ "shutdown_forwarder_failure",
315922
+ "retry_exhausted"
315923
+ ]);
315924
+
315925
+ // src/helpers/broker-execution-archive/writer.ts
315819
315926
  var BROKER_EXECUTION_ARCHIVE_TABLES = new Set([
315820
315927
  "broker_execution.order_events",
315821
315928
  "broker_execution.market_metadata_snapshots",
@@ -315935,7 +316042,7 @@ class BrokerExecutionArchiver {
315935
316042
  throw new Error("Broker execution archive is enabled but CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH is missing");
315936
316043
  }
315937
316044
  try {
315938
- this.deadLetterFd = openSync(this.deadLetterPath, "a", 384);
316045
+ this.deadLetterFd = openSync2(this.deadLetterPath, "a", 384);
315939
316046
  } catch {
315940
316047
  throw new Error("Broker execution archive cannot open CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH for append");
315941
316048
  }
@@ -316065,11 +316172,9 @@ class BrokerExecutionArchiver {
316065
316172
  continue;
316066
316173
  }
316067
316174
  if (!await this.flushBatch()) {
316068
- const undelivered = [
316069
- ...this.pendingRetry?.rows ?? [],
316070
- ...this.queue
316071
- ];
316072
- this.appendLossRecords(undelivered, "shutdown_forwarder_failure");
316175
+ const pinned = this.pendingRetry;
316176
+ this.appendLossRecords(pinned?.rows ?? [], "shutdown_forwarder_failure", pinned?.batchId ?? null);
316177
+ this.appendLossRecords(this.queue, "shutdown_forwarder_failure");
316073
316178
  this.pendingRetry = null;
316074
316179
  this.queue.length = 0;
316075
316180
  break;
@@ -316182,14 +316287,14 @@ class BrokerExecutionArchiver {
316182
316287
  }
316183
316288
  const fd2 = this.deadLetterFd;
316184
316289
  try {
316185
- closeSync(fd2);
316290
+ closeSync2(fd2);
316186
316291
  } catch (error) {
316187
316292
  throw new BrokerExecutionArchiveDurabilityError("Broker execution archive failed to close the configured CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH loss journal", { cause: error });
316188
316293
  } finally {
316189
316294
  this.deadLetterFd = undefined;
316190
316295
  }
316191
316296
  }
316192
- appendLossRecords(rows, reason) {
316297
+ appendLossRecords(rows, reason, batchId = null) {
316193
316298
  if (rows.length === 0) {
316194
316299
  return;
316195
316300
  }
@@ -316197,22 +316302,26 @@ class BrokerExecutionArchiver {
316197
316302
  throw new BrokerExecutionArchiveDurabilityError(`Broker execution archive cannot record ${reason}: dead-letter file is not open`);
316198
316303
  }
316199
316304
  const timestamp = new Date().toISOString();
316200
- const records = rows.map((payload) => ({
316305
+ const records = rows.map((payload, index2) => ({
316201
316306
  timestamp,
316202
316307
  source: this.source,
316203
316308
  deployment_id: this.deploymentId,
316204
316309
  reason,
316205
- payload
316310
+ payload,
316311
+ record_version: LOSS_JOURNAL_RECORD_VERSION,
316312
+ batch_id: batchId,
316313
+ batch_row_index: index2,
316314
+ batch_row_count: rows.length
316206
316315
  }));
316207
316316
  try {
316208
316317
  const bytes2 = Buffer.from(records.map((record) => JSON.stringify(record)).join(`
316209
316318
  `) + `
316210
316319
  `);
316211
- const written = writeSync(this.deadLetterFd, bytes2);
316320
+ const written = writeSync2(this.deadLetterFd, bytes2);
316212
316321
  if (written !== bytes2.length) {
316213
316322
  throw new Error(`wrote ${written} of ${bytes2.length} bytes`);
316214
316323
  }
316215
- fsyncSync(this.deadLetterFd);
316324
+ fsyncSync2(this.deadLetterFd);
316216
316325
  const byFeedAndTable = new Map;
316217
316326
  for (const row of rows) {
316218
316327
  const key = `${row.table}\x00${archiveFeed(row)}`;
@@ -316257,7 +316366,7 @@ class BrokerExecutionArchiver {
316257
316366
  try {
316258
316367
  if (attempts >= MAX_PINNED_BATCH_ATTEMPTS) {
316259
316368
  this.pendingRetry = null;
316260
- this.appendLossRecords(batch, "retry_exhausted");
316369
+ this.appendLossRecords(batch, "retry_exhausted", batchId);
316261
316370
  log.warn("Broker execution archive gave up on a batch", {
316262
316371
  attempts,
316263
316372
  rows: batch.length
@@ -316519,6 +316628,7 @@ function flattenArchiveAttributes(entry) {
316519
316628
  return attributes;
316520
316629
  }
316521
316630
  function createBrokerExecutionArchiverFromEnv(otelLogs, otelMetrics) {
316631
+ exportDeadLetterJournalFromEnv();
316522
316632
  if (process.env.CEX_BROKER_ARCHIVE_ENABLED !== "true") {
316523
316633
  return BrokerExecutionArchiver.disabled();
316524
316634
  }
@@ -317432,7 +317542,7 @@ class FillArchivePoller {
317432
317542
  }
317433
317543
 
317434
317544
  // src/helpers/market-data-archive/capture-contract.ts
317435
- import { createHash as createHash3 } from "node:crypto";
317545
+ import { createHash as createHash4 } from "node:crypto";
317436
317546
  var MARKET_CAPTURE_SCHEMA_VERSION = "1.0.0";
317437
317547
  var CHECKSUM_ALGORITHM = "sha256-canonical-json-v1";
317438
317548
  var ARCHIVE_SOURCES = ["broker_read", "broker_write"];
@@ -317537,7 +317647,7 @@ function omitChecksumFields(value) {
317537
317647
  return value;
317538
317648
  }
317539
317649
  function sha256Canonical(value) {
317540
- return createHash3("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
317650
+ return createHash4("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
317541
317651
  }
317542
317652
  function normalizeTimestampMs(value, field) {
317543
317653
  let timestamp;
@@ -318118,13 +318228,13 @@ function createOtelLogsFromEnv() {
318118
318228
  }
318119
318229
 
318120
318230
  // src/helpers/stream-health-publisher.ts
318121
- import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
318231
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
318122
318232
  import {
318123
- closeSync as closeSync2,
318124
- fsyncSync as fsyncSync2,
318125
- openSync as openSync2,
318233
+ closeSync as closeSync3,
318234
+ fsyncSync as fsyncSync3,
318235
+ openSync as openSync3,
318126
318236
  readFileSync,
318127
- renameSync,
318237
+ renameSync as renameSync2,
318128
318238
  statSync,
318129
318239
  unlinkSync,
318130
318240
  writeFileSync
@@ -318167,7 +318277,7 @@ function registryRevision(snapshots) {
318167
318277
  account_scope: snapshot.accountScope,
318168
318278
  registry_status: snapshot.registryStatus
318169
318279
  })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
318170
- return createHash4("sha256").update(JSON.stringify(rows)).digest("hex");
318280
+ return createHash5("sha256").update(JSON.stringify(rows)).digest("hex");
318171
318281
  }
318172
318282
  function validState(value) {
318173
318283
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -318431,21 +318541,21 @@ class StreamHealthPublisher {
318431
318541
  const temporary = `${this.#statePath}.${process.pid}.${randomUUID2()}.tmp`;
318432
318542
  let fd2;
318433
318543
  try {
318434
- fd2 = openSync2(temporary, "wx", 384);
318544
+ fd2 = openSync3(temporary, "wx", 384);
318435
318545
  writeFileSync(fd2, JSON.stringify(this.#state));
318436
- fsyncSync2(fd2);
318437
- closeSync2(fd2);
318546
+ fsyncSync3(fd2);
318547
+ closeSync3(fd2);
318438
318548
  fd2 = undefined;
318439
- renameSync(temporary, this.#statePath);
318440
- const parentFd = openSync2(parent, "r");
318549
+ renameSync2(temporary, this.#statePath);
318550
+ const parentFd = openSync3(parent, "r");
318441
318551
  try {
318442
- fsyncSync2(parentFd);
318552
+ fsyncSync3(parentFd);
318443
318553
  } finally {
318444
- closeSync2(parentFd);
318554
+ closeSync3(parentFd);
318445
318555
  }
318446
318556
  } catch (error) {
318447
318557
  if (fd2 !== undefined)
318448
- closeSync2(fd2);
318558
+ closeSync3(fd2);
318449
318559
  try {
318450
318560
  unlinkSync(temporary);
318451
318561
  } catch {}
@@ -1,4 +1,5 @@
1
1
  export { archiveOrderExecutionInBackground, archiveSubscribeStreamInBackground, archiveTransferEventInBackground, archiveWithdrawalObservationsInBackground, captureMarketMetadataSnapshot, captureMarketMetadataSnapshotInBackground, } from "./capture";
2
+ export { DeadLetterJournalExportError, type DeadLetterJournalExportResult, exportDeadLetterJournal, exportDeadLetterJournalFromEnv, } from "./journal-export";
2
3
  export { hashMarketMetadata, redactErrorForArchive, redactSecretLiterals, redactStreamPayload, } from "./redact";
3
4
  export { buildAccountBalanceSnapshotRow, buildCommonArchiveTags, buildFillEventArchiveRow, buildMarketMetadataSnapshotRow, buildOrderEventArchiveRow, buildSubscribeStreamArchiveRow, buildTransferEventArchiveRow, buildUserAssetSnapshotRow, extractBinanceInternalTransferId, type FillArchiveFields, type NormalizedCcxtBalance, type NormalizedCcxtTransfer, type NormalizedUserAssets, normalizeBinanceUserAssetsForArchive, normalizeCcxtBalanceForArchive, normalizeCcxtTradeForArchive, normalizeCcxtTransactionForArchive, normalizeTimestamp, type TransferArchiveFields, } from "./rows";
4
5
  export { ACCOUNT_BALANCE_PRECISION_BASIS, ACCOUNT_BALANCE_SCOPE, ARCHIVE_SCHEMA_VERSION, BROKER_READ_SOURCE, BROKER_WRITE_SOURCE, type BrokerArchiveCommonTags, type BrokerArchiveRow, type BrokerArchiveSource, type BrokerArchiveTable, type OrderArchiveAction, type SubscribeArchiveType, type TransferEventKind, type TransferLifecycleAction, USER_ASSET_BALANCE_SCOPE, USER_ASSET_PRECISION_BASIS, } from "./types";
@@ -0,0 +1,20 @@
1
+ export declare class DeadLetterJournalExportError extends Error {
2
+ constructor(message: string, options?: ErrorOptions);
3
+ }
4
+ export type DeadLetterJournalExportResult = {
5
+ status: "disabled";
6
+ } | {
7
+ status: "skipped_export_exists";
8
+ exportPath: string;
9
+ } | {
10
+ status: "skipped_missing_journal";
11
+ journalPath: string;
12
+ } | {
13
+ status: "exported";
14
+ journalPath: string;
15
+ exportPath: string;
16
+ bytes: number;
17
+ sha256: string;
18
+ };
19
+ export declare function exportDeadLetterJournalFromEnv(): DeadLetterJournalExportResult;
20
+ export declare function exportDeadLetterJournal(journalPath: string, exportPath: string): DeadLetterJournalExportResult;
@@ -0,0 +1,36 @@
1
+ import type { BrokerArchiveRow, BrokerArchiveSource } from "./types";
2
+ export declare const LOSS_JOURNAL_RECORD_VERSION: 1;
3
+ export type ArchiveLossReason = "queue_shed" | "shutdown_forwarder_failure" | "retry_exhausted";
4
+ export type ArchiveLossRecord = {
5
+ timestamp: string;
6
+ source: BrokerArchiveSource;
7
+ deployment_id: string;
8
+ reason: ArchiveLossReason;
9
+ payload: BrokerArchiveRow;
10
+ record_version: typeof LOSS_JOURNAL_RECORD_VERSION;
11
+ batch_id: string | null;
12
+ batch_row_index: number;
13
+ batch_row_count: number;
14
+ };
15
+ type ParsedArchiveLossRecordBase = Omit<ArchiveLossRecord, "payload" | "record_version" | "batch_id" | "batch_row_index" | "batch_row_count"> & {
16
+ payload: {
17
+ table: string;
18
+ row: Record<string, unknown>;
19
+ };
20
+ };
21
+ export type ParsedArchiveLossRecord = ParsedArchiveLossRecordBase & (Pick<ArchiveLossRecord, "record_version" | "batch_id" | "batch_row_index" | "batch_row_count"> | {
22
+ record_version?: never;
23
+ batch_id?: never;
24
+ batch_row_index?: never;
25
+ batch_row_count?: never;
26
+ });
27
+ export type ParsedLossJournalLine = {
28
+ ok: true;
29
+ record: ParsedArchiveLossRecord;
30
+ } | {
31
+ ok: false;
32
+ reason: string;
33
+ };
34
+ export declare function parseLossJournalLine(line: string): ParsedLossJournalLine;
35
+ export declare function lossJournalLineDigest(rawLine: string): string;
36
+ export {};
package/dist/index.js CHANGED
@@ -291325,11 +291325,118 @@ function compactUndefined2(record) {
291325
291325
  // src/helpers/broker-execution-archive/writer.ts
291326
291326
  var import_api_logs2 = __toESM(require_src4(), 1);
291327
291327
  import { randomUUID } from "node:crypto";
291328
- import { closeSync, fsyncSync, openSync, writeSync } from "node:fs";
291328
+ import { closeSync as closeSync2, fsyncSync as fsyncSync2, openSync as openSync2, writeSync as writeSync2 } from "node:fs";
291329
291329
  import {
291330
291330
  request as httpRequest2
291331
291331
  } from "node:http";
291332
291332
  import { request as httpsRequest2 } from "node:https";
291333
+
291334
+ // src/helpers/broker-execution-archive/journal-export.ts
291335
+ import { createHash as createHash3 } from "node:crypto";
291336
+ import {
291337
+ closeSync,
291338
+ existsSync,
291339
+ fsyncSync,
291340
+ openSync,
291341
+ readSync,
291342
+ renameSync,
291343
+ writeSync
291344
+ } from "node:fs";
291345
+ import { basename } from "node:path";
291346
+ var EXPORT_CHUNK_BYTES = 8 * 1024 * 1024;
291347
+
291348
+ class DeadLetterJournalExportError extends Error {
291349
+ constructor(message, options) {
291350
+ super(message, options);
291351
+ this.name = "DeadLetterJournalExportError";
291352
+ }
291353
+ }
291354
+ function exportDeadLetterJournalFromEnv() {
291355
+ const exportPath = process.env.CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH?.trim();
291356
+ if (!exportPath) {
291357
+ return { status: "disabled" };
291358
+ }
291359
+ const journalPath = process.env.CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH?.trim();
291360
+ if (!journalPath) {
291361
+ throw new DeadLetterJournalExportError("CEX_BROKER_ARCHIVE_DEAD_LETTER_EXPORT_PATH is set but CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH is missing");
291362
+ }
291363
+ return exportDeadLetterJournal(journalPath, exportPath);
291364
+ }
291365
+ function exportDeadLetterJournal(journalPath, exportPath) {
291366
+ if (existsSync(exportPath)) {
291367
+ log.info("Dead-letter journal export target already exists, skipping", {
291368
+ export_path: exportPath
291369
+ });
291370
+ return { status: "skipped_export_exists", exportPath };
291371
+ }
291372
+ if (!existsSync(journalPath)) {
291373
+ log.warn("Dead-letter journal export requested but no journal exists, skipping", { journal_path: journalPath });
291374
+ return { status: "skipped_missing_journal", journalPath };
291375
+ }
291376
+ const partialPath = `${exportPath}.partial`;
291377
+ let bytes2 = 0;
291378
+ let digest;
291379
+ try {
291380
+ const hash4 = createHash3("sha256");
291381
+ const sourceFd = openSync(journalPath, "r");
291382
+ try {
291383
+ const targetFd = openSync(partialPath, "w", 384);
291384
+ try {
291385
+ const chunk = Buffer.alloc(EXPORT_CHUNK_BYTES);
291386
+ for (;; ) {
291387
+ const read = readSync(sourceFd, chunk, 0, chunk.length, null);
291388
+ if (read === 0) {
291389
+ break;
291390
+ }
291391
+ const view = chunk.subarray(0, read);
291392
+ hash4.update(view);
291393
+ writeSync(targetFd, view);
291394
+ bytes2 += read;
291395
+ }
291396
+ fsyncSync(targetFd);
291397
+ } finally {
291398
+ closeSync(targetFd);
291399
+ }
291400
+ } finally {
291401
+ closeSync(sourceFd);
291402
+ }
291403
+ digest = hash4.digest("hex");
291404
+ renameSync(partialPath, exportPath);
291405
+ const receiptFd = openSync(`${exportPath}.sha256`, "w", 384);
291406
+ try {
291407
+ writeSync(receiptFd, `${digest} ${basename(exportPath)}
291408
+ `);
291409
+ fsyncSync(receiptFd);
291410
+ } finally {
291411
+ closeSync(receiptFd);
291412
+ }
291413
+ } catch (error) {
291414
+ throw new DeadLetterJournalExportError("Dead-letter journal export failed", { cause: error });
291415
+ }
291416
+ log.info("Dead-letter journal exported", {
291417
+ journal_path: journalPath,
291418
+ export_path: exportPath,
291419
+ bytes: bytes2,
291420
+ sha256: digest
291421
+ });
291422
+ return {
291423
+ status: "exported",
291424
+ journalPath,
291425
+ exportPath,
291426
+ bytes: bytes2,
291427
+ sha256: digest
291428
+ };
291429
+ }
291430
+
291431
+ // src/helpers/broker-execution-archive/loss-journal.ts
291432
+ var LOSS_JOURNAL_RECORD_VERSION = 1;
291433
+ var LOSS_REASONS = new Set([
291434
+ "queue_shed",
291435
+ "shutdown_forwarder_failure",
291436
+ "retry_exhausted"
291437
+ ]);
291438
+
291439
+ // src/helpers/broker-execution-archive/writer.ts
291333
291440
  var BROKER_EXECUTION_ARCHIVE_TABLES = new Set([
291334
291441
  "broker_execution.order_events",
291335
291442
  "broker_execution.market_metadata_snapshots",
@@ -291449,7 +291556,7 @@ class BrokerExecutionArchiver {
291449
291556
  throw new Error("Broker execution archive is enabled but CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH is missing");
291450
291557
  }
291451
291558
  try {
291452
- this.deadLetterFd = openSync(this.deadLetterPath, "a", 384);
291559
+ this.deadLetterFd = openSync2(this.deadLetterPath, "a", 384);
291453
291560
  } catch {
291454
291561
  throw new Error("Broker execution archive cannot open CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH for append");
291455
291562
  }
@@ -291579,11 +291686,9 @@ class BrokerExecutionArchiver {
291579
291686
  continue;
291580
291687
  }
291581
291688
  if (!await this.flushBatch()) {
291582
- const undelivered = [
291583
- ...this.pendingRetry?.rows ?? [],
291584
- ...this.queue
291585
- ];
291586
- this.appendLossRecords(undelivered, "shutdown_forwarder_failure");
291689
+ const pinned = this.pendingRetry;
291690
+ this.appendLossRecords(pinned?.rows ?? [], "shutdown_forwarder_failure", pinned?.batchId ?? null);
291691
+ this.appendLossRecords(this.queue, "shutdown_forwarder_failure");
291587
291692
  this.pendingRetry = null;
291588
291693
  this.queue.length = 0;
291589
291694
  break;
@@ -291696,14 +291801,14 @@ class BrokerExecutionArchiver {
291696
291801
  }
291697
291802
  const fd2 = this.deadLetterFd;
291698
291803
  try {
291699
- closeSync(fd2);
291804
+ closeSync2(fd2);
291700
291805
  } catch (error) {
291701
291806
  throw new BrokerExecutionArchiveDurabilityError("Broker execution archive failed to close the configured CEX_BROKER_ARCHIVE_DEAD_LETTER_PATH loss journal", { cause: error });
291702
291807
  } finally {
291703
291808
  this.deadLetterFd = undefined;
291704
291809
  }
291705
291810
  }
291706
- appendLossRecords(rows, reason) {
291811
+ appendLossRecords(rows, reason, batchId = null) {
291707
291812
  if (rows.length === 0) {
291708
291813
  return;
291709
291814
  }
@@ -291711,22 +291816,26 @@ class BrokerExecutionArchiver {
291711
291816
  throw new BrokerExecutionArchiveDurabilityError(`Broker execution archive cannot record ${reason}: dead-letter file is not open`);
291712
291817
  }
291713
291818
  const timestamp = new Date().toISOString();
291714
- const records = rows.map((payload) => ({
291819
+ const records = rows.map((payload, index2) => ({
291715
291820
  timestamp,
291716
291821
  source: this.source,
291717
291822
  deployment_id: this.deploymentId,
291718
291823
  reason,
291719
- payload
291824
+ payload,
291825
+ record_version: LOSS_JOURNAL_RECORD_VERSION,
291826
+ batch_id: batchId,
291827
+ batch_row_index: index2,
291828
+ batch_row_count: rows.length
291720
291829
  }));
291721
291830
  try {
291722
291831
  const bytes2 = Buffer.from(records.map((record) => JSON.stringify(record)).join(`
291723
291832
  `) + `
291724
291833
  `);
291725
- const written = writeSync(this.deadLetterFd, bytes2);
291834
+ const written = writeSync2(this.deadLetterFd, bytes2);
291726
291835
  if (written !== bytes2.length) {
291727
291836
  throw new Error(`wrote ${written} of ${bytes2.length} bytes`);
291728
291837
  }
291729
- fsyncSync(this.deadLetterFd);
291838
+ fsyncSync2(this.deadLetterFd);
291730
291839
  const byFeedAndTable = new Map;
291731
291840
  for (const row of rows) {
291732
291841
  const key = `${row.table}\x00${archiveFeed(row)}`;
@@ -291771,7 +291880,7 @@ class BrokerExecutionArchiver {
291771
291880
  try {
291772
291881
  if (attempts >= MAX_PINNED_BATCH_ATTEMPTS) {
291773
291882
  this.pendingRetry = null;
291774
- this.appendLossRecords(batch, "retry_exhausted");
291883
+ this.appendLossRecords(batch, "retry_exhausted", batchId);
291775
291884
  log.warn("Broker execution archive gave up on a batch", {
291776
291885
  attempts,
291777
291886
  rows: batch.length
@@ -292033,6 +292142,7 @@ function flattenArchiveAttributes(entry) {
292033
292142
  return attributes;
292034
292143
  }
292035
292144
  function createBrokerExecutionArchiverFromEnv(otelLogs, otelMetrics) {
292145
+ exportDeadLetterJournalFromEnv();
292036
292146
  if (process.env.CEX_BROKER_ARCHIVE_ENABLED !== "true") {
292037
292147
  return BrokerExecutionArchiver.disabled();
292038
292148
  }
@@ -292946,7 +293056,7 @@ class FillArchivePoller {
292946
293056
  }
292947
293057
 
292948
293058
  // src/helpers/market-data-archive/capture-contract.ts
292949
- import { createHash as createHash3 } from "node:crypto";
293059
+ import { createHash as createHash4 } from "node:crypto";
292950
293060
  var MARKET_CAPTURE_SCHEMA_VERSION = "1.0.0";
292951
293061
  var CHECKSUM_ALGORITHM = "sha256-canonical-json-v1";
292952
293062
  var ARCHIVE_SOURCES = ["broker_read", "broker_write"];
@@ -293051,7 +293161,7 @@ function omitChecksumFields(value) {
293051
293161
  return value;
293052
293162
  }
293053
293163
  function sha256Canonical(value) {
293054
- return createHash3("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
293164
+ return createHash4("sha256").update(canonicalSerialize(omitChecksumFields(value))).digest("hex");
293055
293165
  }
293056
293166
  function normalizeTimestampMs(value, field) {
293057
293167
  let timestamp;
@@ -293632,13 +293742,13 @@ function createOtelLogsFromEnv() {
293632
293742
  }
293633
293743
 
293634
293744
  // src/helpers/stream-health-publisher.ts
293635
- import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
293745
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
293636
293746
  import {
293637
- closeSync as closeSync2,
293638
- fsyncSync as fsyncSync2,
293639
- openSync as openSync2,
293747
+ closeSync as closeSync3,
293748
+ fsyncSync as fsyncSync3,
293749
+ openSync as openSync3,
293640
293750
  readFileSync,
293641
- renameSync,
293751
+ renameSync as renameSync2,
293642
293752
  statSync,
293643
293753
  unlinkSync,
293644
293754
  writeFileSync
@@ -293681,7 +293791,7 @@ function registryRevision(snapshots) {
293681
293791
  account_scope: snapshot.accountScope,
293682
293792
  registry_status: snapshot.registryStatus
293683
293793
  })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
293684
- return createHash4("sha256").update(JSON.stringify(rows)).digest("hex");
293794
+ return createHash5("sha256").update(JSON.stringify(rows)).digest("hex");
293685
293795
  }
293686
293796
  function validState(value) {
293687
293797
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -293945,21 +294055,21 @@ class StreamHealthPublisher {
293945
294055
  const temporary = `${this.#statePath}.${process.pid}.${randomUUID2()}.tmp`;
293946
294056
  let fd2;
293947
294057
  try {
293948
- fd2 = openSync2(temporary, "wx", 384);
294058
+ fd2 = openSync3(temporary, "wx", 384);
293949
294059
  writeFileSync(fd2, JSON.stringify(this.#state));
293950
- fsyncSync2(fd2);
293951
- closeSync2(fd2);
294060
+ fsyncSync3(fd2);
294061
+ closeSync3(fd2);
293952
294062
  fd2 = undefined;
293953
- renameSync(temporary, this.#statePath);
293954
- const parentFd = openSync2(parent, "r");
294063
+ renameSync2(temporary, this.#statePath);
294064
+ const parentFd = openSync3(parent, "r");
293955
294065
  try {
293956
- fsyncSync2(parentFd);
294066
+ fsyncSync3(parentFd);
293957
294067
  } finally {
293958
- closeSync2(parentFd);
294068
+ closeSync3(parentFd);
293959
294069
  }
293960
294070
  } catch (error) {
293961
294071
  if (fd2 !== undefined)
293962
- closeSync2(fd2);
294072
+ closeSync3(fd2);
293963
294073
  try {
293964
294074
  unlinkSync(temporary);
293965
294075
  } catch {}
@@ -312456,4 +312566,4 @@ export {
312456
312566
  CEXBroker as default
312457
312567
  };
312458
312568
 
312459
- //# debugId=2BB56A1EBD617FCC64756E2164756E21
312569
+ //# debugId=B0758AED2799A60464756E2164756E21