@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 +63 -46
- package/dist/index.js +64 -47
- package/dist/sodium-ready.d.ts +64 -34
- package/package.json +2 -1
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5120
|
-
|
|
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: () =>
|
|
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: () =>
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4976
|
-
|
|
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: () =>
|
|
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: () =>
|
|
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,
|
package/dist/sodium-ready.d.ts
CHANGED
|
@@ -1,43 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Warm libsodium once per process, before any file-crypto call.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
11
|
-
* internally
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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.
|
|
22
|
-
* `if (warmed) return; await ...;
|
|
23
|
-
* run the body. The bridge has two
|
|
24
|
-
* receive), so that concurrency is real
|
|
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
|
-
*
|
|
28
|
-
* `warming ??= ...`
|
|
29
|
-
* if the FIRST warm-up
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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.
|
|
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"
|