@securecoms/agent-bridge 0.11.1 → 0.11.3

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/dist/bin.js CHANGED
@@ -1471,6 +1471,16 @@ function isServerExpired(exp, nowMs) {
1471
1471
  return nowMs - SKEW_GUARD_MS >= exp.getTime();
1472
1472
  }
1473
1473
 
1474
+ // ../connect-sdk-ts/dist/sodium-ready.js
1475
+ import { generateEphemeralKeypair } from "@agentvault/crypto";
1476
+ var warming = null;
1477
+ function ensureSodiumReady() {
1478
+ return warming ??= generateEphemeralKeypair().then(() => void 0).catch((e) => {
1479
+ warming = null;
1480
+ throw e;
1481
+ });
1482
+ }
1483
+
1474
1484
  // ../connect-sdk-ts/dist/groupkey/mls.js
1475
1485
  var LABEL = "agentvault-connect-v1";
1476
1486
  var DEFAULT_STUCK_THRESHOLD = 2;
@@ -1546,8 +1556,6 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
1546
1556
  o;
1547
1557
  suite;
1548
1558
  managers = /* @__PURE__ */ new Map();
1549
- /** One-time libsodium (WASM) warm-up guard — see ensureCryptoReady(). */
1550
- cryptoWarmed = false;
1551
1559
  /**
1552
1560
  * Per-CHANNEL Promise-chain mutex (gen2's MlsOperationQueue, ported). Every
1553
1561
  * manager-mutating operation on a channel takes this lock keyed by channelId,
@@ -2072,37 +2080,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
2072
2080
  const w = rows.find((r) => r.message_type === "welcome" && r.connect_channel_id != null);
2073
2081
  return w?.connect_channel_id ?? null;
2074
2082
  }
2075
- /**
2076
- * Give the ts-mls libsodium (WASM) backend time to initialize before any
2077
- * store operation that deserializes a persisted KeyPackage. generateKeyPackage
2078
- * is pure in-memory (it neither publishes to the backend nor writes the
2079
- * store), so the throwaway bundle is a side-effect-free warm-up.
2080
- *
2081
- * ⛔ IT IS A RACE, NOT A BARRIER, and do not read it as one. Nothing on this
2082
- * path awaits sodium.ready: MLSGroupManager defaults _provider to
2083
- * nobleCryptoProvider, and _ensureCiphersuite passes that provider to
2084
- * getCiphersuiteImpl. What the warm-up actually does is BURN WALL CLOCK while
2085
- * libsodium's WASM initializes in PARALLEL. The margin is roughly 1.7 ms and
2086
- * it DRIFTS: libsodium-sumo is reached through a caret and its init timing
2087
- * sets the margin, so an install can move it with no change in this repo.
2088
- * See 02f47e15 (PR #87), whose commit body carries the correct diagnosis.
2089
- *
2090
- * Without it, the FIRST loadPendingBundle at boot (the republish
2091
- * guard, and the join path) runs while WASM is still cold, throws deep in the
2092
- * crypto ("Cannot read properties of undefined (reading '_malloc')"), is caught
2093
- * and misread as "no held bundle", and the daemon mints + republishes a FRESH
2094
- * KeyPackage on every restart — orphaning every Welcome sealed to the prior
2095
- * published KP (the root cause of "fresh DM never opens" across restarts).
2096
- * Idempotent and cheap (~ms); the flag makes repeat calls no-ops.
2097
- */
2098
- async ensureCryptoReady() {
2099
- if (this.cryptoWarmed)
2100
- return;
2101
- await new MLSGroupManager2().generateKeyPackage(this.o.identity);
2102
- this.cryptoWarmed = true;
2103
- }
2104
2083
  async publishKeyPackage() {
2105
- await this.ensureCryptoReady();
2084
+ await ensureSodiumReady();
2106
2085
  return this.publishLock.run(this.o.deviceId, () => this.publishKeyPackageUnlocked());
2107
2086
  }
2108
2087
  /**
@@ -2143,8 +2122,36 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
2143
2122
  * even then a supersede Welcome self-heals next cycle (held == published again).
2144
2123
  *
2145
2124
  * Best-effort: any error resolving either side returns false, so the caller
2146
- * force-republishes (the conservative recovery). */
2125
+ * force-republishes (the conservative recovery).
2126
+ *
2127
+ * ⛔⛔ THE READINESS BARRIER SITS OUTSIDE THE `try`, AND MOVING IT INSIDE
2128
+ * RE-OPENS A SILENT FLEET-WIDE KEYPACKAGE ROTATION. The bare catch below is
2129
+ * deliberate for the two things it was written for (a store read that fails,
2130
+ * a backend fetch that fails), and for those `false` is the conservative
2131
+ * answer. A readiness-barrier rejection is NOT one of those things. It means
2132
+ * libsodium's WASM heap is broken, so this process cannot answer the question
2133
+ * at all — and `false` is not "I don't know", it is an INSTRUCTION:
2134
+ * `connect-agent-bridge/src/bridge-agent.ts:868` routes `false` straight to
2135
+ * `forceRepublishKeyPackage()`, which DELETES the held bundle at `:1069` below
2136
+ * and mints a replacement KeyPackage, orphaning every in-flight Welcome sealed
2137
+ * to the prior one.
2138
+ *
2139
+ * ⛔ AND THE ROTATION COMPLETES RATHER THAN TEARING, WHICH IS WHY IT IS THE
2140
+ * WORSE OUTCOME. `bytesToHex` is `sodium.to_hex` and touches no WASM heap,
2141
+ * `generateKeyPackage` runs on noble, and the envelope seal runs on
2142
+ * `@noble/ciphers` (`connect-core/src/envelope/aead.ts:41-43`). The daemon ends
2143
+ * CONSISTENT and silently stranded, not crashed.
2144
+ *
2145
+ * ⛔ THIS IS THE ROUTE THAT FALSIFIES "a barrier rejection is always a loud
2146
+ * boot failure". `heldBundleMatchesPublishedKp` also runs on the periodic
2147
+ * self-heal sweep, so it is not a boot-only path. Hoisting the await is what
2148
+ * makes the rejection propagate to the caller instead of being read as a
2149
+ * verdict. Pinned by
2150
+ * `packages/connect-mls-store/src/barrier-rejection-rotation.gate.test.ts`
2151
+ * (G-H1), whose control case proves the resolving path still answers both
2152
+ * `true` and `false`. */
2147
2153
  async heldBundleMatchesPublishedKp() {
2154
+ await ensureSodiumReady();
2148
2155
  try {
2149
2156
  const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
2150
2157
  if (!bundle)
@@ -2189,7 +2196,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
2189
2196
  async ensureSessionUnlocked(channelId, opts) {
2190
2197
  if (!opts.forceReinit && await this.o.store.load(channelId))
2191
2198
  return;
2192
- await this.ensureCryptoReady();
2199
+ await ensureSodiumReady();
2193
2200
  let bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
2194
2201
  if (!bundle && (opts.bootstrap || opts.forceReinit)) {
2195
2202
  await this.publishKeyPackage();
@@ -4619,6 +4626,22 @@ var SqliteMlsStateStore = class {
4619
4626
  async discardStaging(channelId) {
4620
4627
  this.db.prepare("UPDATE mls_group_state SET sealed_staging=NULL WHERE identity_id=? AND channel_id=?").run(this.id, channelId);
4621
4628
  }
4629
+ /**
4630
+ * ⛔ THE WRITE PATH GETS NO READINESS BARRIER, AND THAT IS DELIBERATE. Two
4631
+ * independent reasons, and the FIRST is the primary one.
4632
+ *
4633
+ * 1. `:191` below populates `this.bundles` BEFORE the seal at `:192`.
4634
+ * A same-process reload therefore hits the cache at `loadPendingBundle`'s
4635
+ * `:202-203` and NEVER decodes. The decode path is only ever taken for a
4636
+ * bundle written by a PREVIOUS process, which by definition is not this
4637
+ * one. A barrier here could not protect the read that actually fails.
4638
+ * 2. `encodeBundle`'s `bytesToHex` is cold-safe BECAUSE `to_hex` needs no
4639
+ * `_malloc`. ⛔ IT IS NOT "PURE JS" AND THE COMMENT MUST NEVER SAY THAT.
4640
+ * It is a libsodium call like the others; it simply does not touch the
4641
+ * WASM heap. Recording the wrong reason invites a later reader to
4642
+ * "correct" the asymmetry between this method and `loadPendingBundle` in
4643
+ * the wrong direction.
4644
+ */
4622
4645
  async savePendingBundle(ref, bundle) {
4623
4646
  this.bundles.set(ref, bundle);
4624
4647
  const sealed = await this.cipher.seal(encodeBundle(bundle));
@@ -4633,13 +4656,12 @@ var SqliteMlsStateStore = class {
4633
4656
  const row = this.db.prepare("SELECT sealed_bundle FROM mls_pending_bundle WHERE identity_id=? AND ref=?").get(this.id, ref);
4634
4657
  if (!row?.sealed_bundle)
4635
4658
  return null;
4659
+ await ensureSodiumReady();
4636
4660
  let bundle;
4637
4661
  try {
4638
4662
  bundle = decodeBundle(await this.cipher.open(row.sealed_bundle));
4639
4663
  } catch (e) {
4640
- if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
4641
- console.warn(`[trace] loadPendingBundle: unseal/decode FAILED for ref ${ref.slice(0, 8)}: ${e?.message}`);
4642
- }
4664
+ console.warn(`[trace] loadPendingBundle: unseal/decode FAILED for ref ${ref.slice(0, 8)}: ${e?.message}`);
4643
4665
  return null;
4644
4666
  }
4645
4667
  this.bundles.set(ref, bundle);
@@ -5116,13 +5138,8 @@ async function materializeAttachment(body, ctx, deps) {
5116
5138
  }
5117
5139
 
5118
5140
  // src/sodium-ready.ts
5119
- import { generateEphemeralKeypair } from "@agentvault/crypto";
5120
- var warming = null;
5121
- function ensureSodiumReady() {
5122
- return warming ??= generateEphemeralKeypair().then(() => void 0).catch((e) => {
5123
- warming = null;
5124
- throw e;
5125
- });
5141
+ function ensureSodiumReady2() {
5142
+ return ensureSodiumReady();
5126
5143
  }
5127
5144
 
5128
5145
  // src/memory/hermes-session-map.ts
@@ -6538,7 +6555,7 @@ var BridgeAgent = class _BridgeAgent {
6538
6555
  }
6539
6556
  sendDeps() {
6540
6557
  return {
6541
- ready: () => ensureSodiumReady(),
6558
+ ready: () => ensureSodiumReady2(),
6542
6559
  uploadBlob: (ciphertext, channelId) => this.uploadBlobImpl(ciphertext, channelId),
6543
6560
  encryptFile,
6544
6561
  bytesToBase64: bytesToBase642,
@@ -6547,7 +6564,7 @@ var BridgeAgent = class _BridgeAgent {
6547
6564
  }
6548
6565
  receiveDeps() {
6549
6566
  return {
6550
- ready: () => ensureSodiumReady(),
6567
+ ready: () => ensureSodiumReady2(),
6551
6568
  downloadBlob: this.downloadBlobImpl,
6552
6569
  decryptFile,
6553
6570
  computeFileDigest,
package/dist/index.js CHANGED
@@ -1368,6 +1368,16 @@ function isServerExpired(exp, nowMs) {
1368
1368
  return nowMs - SKEW_GUARD_MS >= exp.getTime();
1369
1369
  }
1370
1370
 
1371
+ // ../connect-sdk-ts/dist/sodium-ready.js
1372
+ import { generateEphemeralKeypair } from "@agentvault/crypto";
1373
+ var warming = null;
1374
+ function ensureSodiumReady() {
1375
+ return warming ??= generateEphemeralKeypair().then(() => void 0).catch((e) => {
1376
+ warming = null;
1377
+ throw e;
1378
+ });
1379
+ }
1380
+
1371
1381
  // ../connect-sdk-ts/dist/groupkey/mls.js
1372
1382
  var LABEL = "agentvault-connect-v1";
1373
1383
  var DEFAULT_STUCK_THRESHOLD = 2;
@@ -1443,8 +1453,6 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
1443
1453
  o;
1444
1454
  suite;
1445
1455
  managers = /* @__PURE__ */ new Map();
1446
- /** One-time libsodium (WASM) warm-up guard — see ensureCryptoReady(). */
1447
- cryptoWarmed = false;
1448
1456
  /**
1449
1457
  * Per-CHANNEL Promise-chain mutex (gen2's MlsOperationQueue, ported). Every
1450
1458
  * manager-mutating operation on a channel takes this lock keyed by channelId,
@@ -1969,37 +1977,8 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
1969
1977
  const w = rows.find((r) => r.message_type === "welcome" && r.connect_channel_id != null);
1970
1978
  return w?.connect_channel_id ?? null;
1971
1979
  }
1972
- /**
1973
- * Give the ts-mls libsodium (WASM) backend time to initialize before any
1974
- * store operation that deserializes a persisted KeyPackage. generateKeyPackage
1975
- * is pure in-memory (it neither publishes to the backend nor writes the
1976
- * store), so the throwaway bundle is a side-effect-free warm-up.
1977
- *
1978
- * ⛔ IT IS A RACE, NOT A BARRIER, and do not read it as one. Nothing on this
1979
- * path awaits sodium.ready: MLSGroupManager defaults _provider to
1980
- * nobleCryptoProvider, and _ensureCiphersuite passes that provider to
1981
- * getCiphersuiteImpl. What the warm-up actually does is BURN WALL CLOCK while
1982
- * libsodium's WASM initializes in PARALLEL. The margin is roughly 1.7 ms and
1983
- * it DRIFTS: libsodium-sumo is reached through a caret and its init timing
1984
- * sets the margin, so an install can move it with no change in this repo.
1985
- * See 02f47e15 (PR #87), whose commit body carries the correct diagnosis.
1986
- *
1987
- * Without it, the FIRST loadPendingBundle at boot (the republish
1988
- * guard, and the join path) runs while WASM is still cold, throws deep in the
1989
- * crypto ("Cannot read properties of undefined (reading '_malloc')"), is caught
1990
- * and misread as "no held bundle", and the daemon mints + republishes a FRESH
1991
- * KeyPackage on every restart — orphaning every Welcome sealed to the prior
1992
- * published KP (the root cause of "fresh DM never opens" across restarts).
1993
- * Idempotent and cheap (~ms); the flag makes repeat calls no-ops.
1994
- */
1995
- async ensureCryptoReady() {
1996
- if (this.cryptoWarmed)
1997
- return;
1998
- await new MLSGroupManager2().generateKeyPackage(this.o.identity);
1999
- this.cryptoWarmed = true;
2000
- }
2001
1980
  async publishKeyPackage() {
2002
- await this.ensureCryptoReady();
1981
+ await ensureSodiumReady();
2003
1982
  return this.publishLock.run(this.o.deviceId, () => this.publishKeyPackageUnlocked());
2004
1983
  }
2005
1984
  /**
@@ -2040,8 +2019,36 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
2040
2019
  * even then a supersede Welcome self-heals next cycle (held == published again).
2041
2020
  *
2042
2021
  * Best-effort: any error resolving either side returns false, so the caller
2043
- * force-republishes (the conservative recovery). */
2022
+ * force-republishes (the conservative recovery).
2023
+ *
2024
+ * ⛔⛔ THE READINESS BARRIER SITS OUTSIDE THE `try`, AND MOVING IT INSIDE
2025
+ * RE-OPENS A SILENT FLEET-WIDE KEYPACKAGE ROTATION. The bare catch below is
2026
+ * deliberate for the two things it was written for (a store read that fails,
2027
+ * a backend fetch that fails), and for those `false` is the conservative
2028
+ * answer. A readiness-barrier rejection is NOT one of those things. It means
2029
+ * libsodium's WASM heap is broken, so this process cannot answer the question
2030
+ * at all — and `false` is not "I don't know", it is an INSTRUCTION:
2031
+ * `connect-agent-bridge/src/bridge-agent.ts:868` routes `false` straight to
2032
+ * `forceRepublishKeyPackage()`, which DELETES the held bundle at `:1069` below
2033
+ * and mints a replacement KeyPackage, orphaning every in-flight Welcome sealed
2034
+ * to the prior one.
2035
+ *
2036
+ * ⛔ AND THE ROTATION COMPLETES RATHER THAN TEARING, WHICH IS WHY IT IS THE
2037
+ * WORSE OUTCOME. `bytesToHex` is `sodium.to_hex` and touches no WASM heap,
2038
+ * `generateKeyPackage` runs on noble, and the envelope seal runs on
2039
+ * `@noble/ciphers` (`connect-core/src/envelope/aead.ts:41-43`). The daemon ends
2040
+ * CONSISTENT and silently stranded, not crashed.
2041
+ *
2042
+ * ⛔ THIS IS THE ROUTE THAT FALSIFIES "a barrier rejection is always a loud
2043
+ * boot failure". `heldBundleMatchesPublishedKp` also runs on the periodic
2044
+ * self-heal sweep, so it is not a boot-only path. Hoisting the await is what
2045
+ * makes the rejection propagate to the caller instead of being read as a
2046
+ * verdict. Pinned by
2047
+ * `packages/connect-mls-store/src/barrier-rejection-rotation.gate.test.ts`
2048
+ * (G-H1), whose control case proves the resolving path still answers both
2049
+ * `true` and `false`. */
2044
2050
  async heldBundleMatchesPublishedKp() {
2051
+ await ensureSodiumReady();
2045
2052
  try {
2046
2053
  const bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
2047
2054
  if (!bundle)
@@ -2086,7 +2093,7 @@ var MlsGroupKeyProvider = class _MlsGroupKeyProvider {
2086
2093
  async ensureSessionUnlocked(channelId, opts) {
2087
2094
  if (!opts.forceReinit && await this.o.store.load(channelId))
2088
2095
  return;
2089
- await this.ensureCryptoReady();
2096
+ await ensureSodiumReady();
2090
2097
  let bundle = await this.o.store.loadPendingBundle(this.o.deviceId);
2091
2098
  if (!bundle && (opts.bootstrap || opts.forceReinit)) {
2092
2099
  await this.publishKeyPackage();
@@ -4516,6 +4523,22 @@ var SqliteMlsStateStore = class {
4516
4523
  async discardStaging(channelId) {
4517
4524
  this.db.prepare("UPDATE mls_group_state SET sealed_staging=NULL WHERE identity_id=? AND channel_id=?").run(this.id, channelId);
4518
4525
  }
4526
+ /**
4527
+ * ⛔ THE WRITE PATH GETS NO READINESS BARRIER, AND THAT IS DELIBERATE. Two
4528
+ * independent reasons, and the FIRST is the primary one.
4529
+ *
4530
+ * 1. `:191` below populates `this.bundles` BEFORE the seal at `:192`.
4531
+ * A same-process reload therefore hits the cache at `loadPendingBundle`'s
4532
+ * `:202-203` and NEVER decodes. The decode path is only ever taken for a
4533
+ * bundle written by a PREVIOUS process, which by definition is not this
4534
+ * one. A barrier here could not protect the read that actually fails.
4535
+ * 2. `encodeBundle`'s `bytesToHex` is cold-safe BECAUSE `to_hex` needs no
4536
+ * `_malloc`. ⛔ IT IS NOT "PURE JS" AND THE COMMENT MUST NEVER SAY THAT.
4537
+ * It is a libsodium call like the others; it simply does not touch the
4538
+ * WASM heap. Recording the wrong reason invites a later reader to
4539
+ * "correct" the asymmetry between this method and `loadPendingBundle` in
4540
+ * the wrong direction.
4541
+ */
4519
4542
  async savePendingBundle(ref, bundle) {
4520
4543
  this.bundles.set(ref, bundle);
4521
4544
  const sealed = await this.cipher.seal(encodeBundle(bundle));
@@ -4530,13 +4553,12 @@ var SqliteMlsStateStore = class {
4530
4553
  const row = this.db.prepare("SELECT sealed_bundle FROM mls_pending_bundle WHERE identity_id=? AND ref=?").get(this.id, ref);
4531
4554
  if (!row?.sealed_bundle)
4532
4555
  return null;
4556
+ await ensureSodiumReady();
4533
4557
  let bundle;
4534
4558
  try {
4535
4559
  bundle = decodeBundle(await this.cipher.open(row.sealed_bundle));
4536
4560
  } catch (e) {
4537
- if (typeof process !== "undefined" && process.env?.SECURECOMS_TRACE_DELIVERY) {
4538
- console.warn(`[trace] loadPendingBundle: unseal/decode FAILED for ref ${ref.slice(0, 8)}: ${e?.message}`);
4539
- }
4561
+ console.warn(`[trace] loadPendingBundle: unseal/decode FAILED for ref ${ref.slice(0, 8)}: ${e?.message}`);
4540
4562
  return null;
4541
4563
  }
4542
4564
  this.bundles.set(ref, bundle);
@@ -4972,13 +4994,8 @@ async function materializeAttachment(body, ctx, deps) {
4972
4994
  }
4973
4995
 
4974
4996
  // src/sodium-ready.ts
4975
- import { generateEphemeralKeypair } from "@agentvault/crypto";
4976
- var warming = null;
4977
- function ensureSodiumReady() {
4978
- return warming ??= generateEphemeralKeypair().then(() => void 0).catch((e) => {
4979
- warming = null;
4980
- throw e;
4981
- });
4997
+ function ensureSodiumReady2() {
4998
+ return ensureSodiumReady();
4982
4999
  }
4983
5000
 
4984
5001
  // src/memory/hermes-session-map.ts
@@ -6394,7 +6411,7 @@ var BridgeAgent = class _BridgeAgent {
6394
6411
  }
6395
6412
  sendDeps() {
6396
6413
  return {
6397
- ready: () => ensureSodiumReady(),
6414
+ ready: () => ensureSodiumReady2(),
6398
6415
  uploadBlob: (ciphertext, channelId) => this.uploadBlobImpl(ciphertext, channelId),
6399
6416
  encryptFile,
6400
6417
  bytesToBase64: bytesToBase642,
@@ -6403,7 +6420,7 @@ var BridgeAgent = class _BridgeAgent {
6403
6420
  }
6404
6421
  receiveDeps() {
6405
6422
  return {
6406
- ready: () => ensureSodiumReady(),
6423
+ ready: () => ensureSodiumReady2(),
6407
6424
  downloadBlob: this.downloadBlobImpl,
6408
6425
  decryptFile,
6409
6426
  computeFileDigest,
@@ -8034,7 +8051,7 @@ export {
8034
8051
  attachAgentWebSocket,
8035
8052
  capabilities,
8036
8053
  createHttpServer,
8037
- ensureSodiumReady,
8054
+ ensureSodiumReady2 as ensureSodiumReady,
8038
8055
  errorBody,
8039
8056
  httpStatus,
8040
8057
  isLoopback,
@@ -1,43 +1,73 @@
1
1
  /**
2
2
  * Warm libsodium once per process, before any file-crypto call.
3
3
  *
4
- * ⛔ THE DISCARDED KEYPAIR IS THE READINESS GATE, NOT DEAD CODE. Do not
5
- * delete it. `@agentvault/crypto` has no readiness contract on its entire
6
- * synchronous surface: sixteen exported synchronous functions throw when
7
- * libsodium's WASM heap is not yet initialised, with either
4
+ * ⛔⛔ THIS IS A DELEGATE, AND THE DELEGATION IS THE POINT. #331 moved the
5
+ * warm-up body and its memo to `packages/connect-sdk-ts/src/sodium-ready.ts`.
6
+ * A process that holds TWO independent readiness memos warms libsodium twice,
7
+ * and this bridge process holds one instance of the SDK module already. Keeping
8
+ * a second memo here would have made the two barriers count separately.
9
+ * ⛔ DO NOT restore a local memo, a local `generateEphemeralKeypair()` call, or
10
+ * a local `warming` variable. `src/sodium-ready.delegation.gate.test.ts` (G-E)
11
+ * counts the underlying warm-ups across BOTH barriers and reads 2 the moment
12
+ * one comes back.
13
+ *
14
+ * ⛔ THE NAME AND THE EXPORT STAY. `bridge-agent.ts:64` imports this symbol and
15
+ * `index.ts:7` re-exports it. Callers keep calling `ensureSodiumReady()` from
16
+ * this package; only where the body lives changed.
17
+ *
18
+ * The four findings this file carried before #331, and where each one now
19
+ * lives. None is deleted, because each still binds on something.
20
+ *
21
+ * ⛔ 1. THE DISCARDED KEYPAIR IS THE READINESS GATE, NOT DEAD CODE. ⇒ ITS
22
+ * SUBJECT MOVED. The `generateEphemeralKeypair()` await now sits in the SDK
23
+ * module named above, and the finding travels with it. It still says what it
24
+ * always said: `@agentvault/crypto` has no readiness contract on its
25
+ * synchronous surface. ⛔ THE COUNT IS A WITNESSED SET, NOT A BARE NUMBER, and
26
+ * the earlier "sixteen" was not one — see the SDK module for the cold/warm
27
+ * differential that produced it and for the named nine. Cold-throwers fail with
8
28
  * `<member> is not a function` or
9
- * `Cannot read properties of undefined (reading '_malloc')`.
10
- * `generateEphemeralKeypair` is an ASYNC export that awaits `sodium.ready`
11
- * internally, on the SAME module instance the synchronous exports use, so
12
- * awaiting it is what makes those exports safe to call afterwards. The X25519
13
- * keypair it returns is minted and thrown away: it is never stored, never
14
- * published, and never used for anything.
29
+ * `Cannot read properties of undefined (reading '_malloc')`, and
30
+ * `generateEphemeralKeypair` is the ASYNC export that awaits `sodium.ready`
31
+ * internally on the SAME module instance those synchronous exports use. ⛔ Do
32
+ * not "simplify" the delegate by inlining a keypair mint back here: that is a
33
+ * second warm-up, not a shortcut.
15
34
  *
16
- * ⛔ DO NOT IMPORT `libsodium-wrappers-sumo` DIRECTLY HERE. That specifier is
17
- * absent from esbuild.config.mjs's EXTERNAL list, so esbuild would bundle a
18
- * SECOND libsodium instance. The await would then warm a heap that nothing
19
- * reads, and this fix would look correct and do nothing.
35
+ * ⛔ 2. DO NOT IMPORT `libsodium-wrappers-sumo` DIRECTLY HERE. ⇒ THIS ONE STILL
36
+ * BINDS ON THIS FILE, UNCHANGED. `esbuild.config.mjs:15` inlines every
37
+ * `@agentvault/connect-*` workspace package into the bridge bundle and that
38
+ * specifier is absent from the EXTERNAL list at `:3-8`, so esbuild would bundle
39
+ * a SECOND libsodium instance out of this file. The await would then warm a
40
+ * heap that nothing reads, and this fix would look correct and do nothing.
41
+ * ⚠️ The same inlining is why the delegate below is not a no-op in the shipped
42
+ * bundle: the SDK barrier is inlined into `dist/bin.js` exactly once, and this
43
+ * delegate points at that one copy.
20
44
  *
21
- * ⛔ MEMOIZE THE PROMISE, NOT A BOOLEAN. Measured: under
22
- * `if (warmed) return; await ...; warmed = true;` two CONCURRENT callers both
23
- * run the body. The bridge has two independent entry points (send and
24
- * receive), so that concurrency is real. The promise form runs the body once.
45
+ * ⛔ 3. MEMOIZE THE PROMISE, NOT A BOOLEAN. ⇒ ITS SUBJECT MOVED. The memo is the
46
+ * SDK module's `warming`. Measured: under `if (warmed) return; await ...;
47
+ * warmed = true;` two CONCURRENT callers both run the body. The bridge has two
48
+ * independent entry points (send and receive), so that concurrency is real
49
+ * here, which is why the finding was written here first. It is pinned now by
50
+ * the G-C1 case in `packages/connect-sdk-ts/src/sodium-ready.memo.gate.test.ts`,
51
+ * which is the CONCURRENT form of the property, and by
52
+ * `src/sodium-ready.retry.test.ts:52-69` in this package, which is the SEQUENTIAL
53
+ * form and reaches the SDK memo THROUGH this delegate.
54
+ * ⛔ G-C1 GATES THIS FINDING ONLY. It cannot fail on finding 4's memo-clearing
55
+ * `catch`, because its mock always resolves. Finding 4 names its own pins.
25
56
  *
26
- * ⛔⛔ THE `catch` THAT CLEARS THE MEMO IS NOT REDUNDANT. DO NOT DELETE IT.
27
- * Caching the promise fixes the double-warm race, but a bare
28
- * `warming ??= ...` introduces a failure mode the boolean form did NOT have:
29
- * if the FIRST warm-up REJECTS (WASM instantiation failure, OOM, a corrupt
30
- * binary), `warming` holds a REJECTED promise for the life of the process, and
31
- * every later prepareAttachment AND materializeAttachment then rejects at its
32
- * first statement, forever. ⛔ ON THE RECEIVE SIDE THAT IS SILENT: the ingest
33
- * pump's fail-soft catch acks the message, increments the failed-attachment
34
- * counter and logs, so one transient failure becomes a PERMANENT,
35
- * PROCESS-WIDE attachment outage that never surfaces as a crash. The boolean
36
- * form would have retried. Clearing the memo on rejection restores that retry
37
- * while keeping the single-warm guarantee on the success path.
38
- * ⛔ It RETHROWS: the first caller must still see the real failure. This is
39
- * not a swallow-and-proceed, which is the exact shape that caused the
40
- * production incident this whole finding catalogues.
41
- * Pinned by src/sodium-ready.retry.test.ts, both halves.
57
+ * ⛔⛔ 4. THE `catch` THAT CLEARS THE MEMO IS NOT REDUNDANT. DO NOT DELETE IT.
58
+ * ⇒ ITS SUBJECT MOVED, AND THE CONSEQUENCE STILL LANDS IN THIS PACKAGE. A bare
59
+ * `warming ??= ...` in the SDK module would hold a REJECTED promise for the
60
+ * life of the process if the FIRST warm-up rejects (WASM instantiation failure,
61
+ * OOM, a corrupt binary), and every later prepareAttachment AND
62
+ * materializeAttachment IN THIS PACKAGE would then reject at its first
63
+ * statement, forever. ⛔ ON THE RECEIVE SIDE THAT IS SILENT: the ingest pump's
64
+ * fail-soft catch acks the message, increments the failed-attachment counter
65
+ * and logs, so one transient failure becomes a PERMANENT, PROCESS-WIDE
66
+ * attachment outage that never surfaces as a crash. The SDK module clears the
67
+ * memo on rejection and RETHROWS, so the first caller still sees the real
68
+ * failure. Pinned by `src/sodium-ready.retry.test.ts` in this package, both
69
+ * halves, and by the G-C1b case in
70
+ * `packages/connect-sdk-ts/src/sodium-ready.memo.gate.test.ts`. ⛔ NOT by G-C1,
71
+ * which cannot fail on that `catch` — see finding 3 above.
42
72
  */
43
73
  export declare function ensureSodiumReady(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "scripts": {
25
25
  "prepack": "npm run build",
26
+ "prepublishOnly": "bash scripts/pack-smoke.sh",
26
27
  "build": "tsc && node esbuild.config.mjs",
27
28
  "test": "vitest run",
28
29
  "test:watch": "vitest"