@securecoms/agent-bridge 0.11.0 → 0.11.2
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/attachments.d.ts +8 -4
- package/dist/attachments.js +49 -0
- package/dist/auth.js +56 -0
- package/dist/bin.js +882 -115
- package/dist/bin.js.pre-c1-f803007 +5795 -0
- package/dist/bin.js.pre-c3-096a9a3 +5832 -0
- package/dist/bin.js.pre-ffrebuild-b162d4a +5639 -0
- package/dist/bin.js.pre-kpheal-20260708-150924 +5531 -0
- package/dist/bin.js.pre-kppersist-20260708-152341 +5543 -0
- package/dist/bin.js.pre-p0-20260708-102415 +5196 -0
- package/dist/bin.js.pre-p1b-02f47e1 +5639 -0
- package/dist/bin.js.pre-warmfix-20260708-153705 +5613 -0
- package/dist/bin.js.pretrace-113205 +5395 -0
- package/dist/bridge-agent.d.ts +94 -19
- package/dist/bridge-agent.js +831 -0
- package/dist/config.js +74 -0
- package/dist/coordination.js +63 -0
- package/dist/errors.js +25 -0
- package/dist/http-server.js +220 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +883 -115
- package/dist/index.js.pre-p0-20260708-102415 +4879 -0
- package/dist/latency.js +87 -0
- package/dist/rate-limit.js +56 -0
- package/dist/service/config.js +26 -0
- package/dist/service/install.js +60 -0
- package/dist/service/launchd.js +30 -0
- package/dist/service/systemd.js +25 -0
- package/dist/sodium-ready.d.ts +65 -0
- package/dist/vault.d.ts +96 -0
- package/dist/ws-server.js +254 -0
- package/package.json +2 -2
package/dist/latency.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// latency.ts — lightweight per-stage latency recorder for the agent-bridge.
|
|
2
|
+
//
|
|
3
|
+
// Captures the SecureComs MESSAGE-ENVELOPE cost (MLS encrypt, relay publish,
|
|
4
|
+
// decrypt, receive cycle) so we can report real p50/p95/p99 SEPARATE from the
|
|
5
|
+
// agent's LLM time. Deliberately dependency-free and bounded: each stage keeps
|
|
6
|
+
// a fixed-size ring buffer of recent samples and percentiles are computed on
|
|
7
|
+
// demand from a sorted copy. Off the hot path beyond a single array write.
|
|
8
|
+
//
|
|
9
|
+
// This is NOT a metrics-pipeline replacement; it is a self-contained, loopback
|
|
10
|
+
// -exposed snapshot (`/v1/metrics`) for profiling + load tests. Per-stage
|
|
11
|
+
// timings are monotonic (performance.now) and local — true cross-process
|
|
12
|
+
// end-to-end (sender-publish → recipient-decrypt) needs clock-synced
|
|
13
|
+
// correlation by message id and is a follow-up.
|
|
14
|
+
const DEFAULT_CAPACITY = 1024;
|
|
15
|
+
function round(n) {
|
|
16
|
+
// Two-decimal ms is plenty; avoids float noise in the JSON snapshot.
|
|
17
|
+
return Math.round(n * 100) / 100;
|
|
18
|
+
}
|
|
19
|
+
/** Nearest-rank percentile (1-indexed ceil) over an ascending-sorted array. */
|
|
20
|
+
function percentile(sorted, p) {
|
|
21
|
+
if (sorted.length === 0)
|
|
22
|
+
return 0;
|
|
23
|
+
const rank = Math.ceil((p / 100) * sorted.length);
|
|
24
|
+
const idx = Math.min(Math.max(rank, 1), sorted.length) - 1;
|
|
25
|
+
return sorted[idx];
|
|
26
|
+
}
|
|
27
|
+
export class LatencyRecorder {
|
|
28
|
+
capacity;
|
|
29
|
+
// Per-stage ring buffer of recent samples (ms).
|
|
30
|
+
buffers = new Map();
|
|
31
|
+
// Write cursor per stage (for the ring buffer wrap).
|
|
32
|
+
cursors = new Map();
|
|
33
|
+
constructor(opts = {}) {
|
|
34
|
+
this.capacity = opts.capacity ?? DEFAULT_CAPACITY;
|
|
35
|
+
}
|
|
36
|
+
/** Record a single sample (ms) for a stage. O(1). */
|
|
37
|
+
record(stage, ms) {
|
|
38
|
+
let buf = this.buffers.get(stage);
|
|
39
|
+
if (!buf) {
|
|
40
|
+
buf = [];
|
|
41
|
+
this.buffers.set(stage, buf);
|
|
42
|
+
this.cursors.set(stage, 0);
|
|
43
|
+
}
|
|
44
|
+
if (buf.length < this.capacity) {
|
|
45
|
+
buf.push(ms);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Ring-buffer overwrite of the oldest slot.
|
|
49
|
+
const cur = this.cursors.get(stage) ?? 0;
|
|
50
|
+
buf[cur] = ms;
|
|
51
|
+
this.cursors.set(stage, (cur + 1) % this.capacity);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Time an async fn, record its elapsed wall time under `stage`, return its
|
|
56
|
+
* value. Records even when the fn throws (the failure still consumed time),
|
|
57
|
+
* then re-throws — so a thrown error never loses the timing sample.
|
|
58
|
+
*/
|
|
59
|
+
async time(stage, fn) {
|
|
60
|
+
const start = performance.now();
|
|
61
|
+
try {
|
|
62
|
+
return await fn();
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
this.record(stage, performance.now() - start);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Percentile snapshot per stage. Rounded to 2 decimals (ms). */
|
|
69
|
+
snapshot() {
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const [stage, buf] of this.buffers) {
|
|
72
|
+
if (buf.length === 0)
|
|
73
|
+
continue;
|
|
74
|
+
const sorted = [...buf].sort((a, b) => a - b);
|
|
75
|
+
const sum = sorted.reduce((acc, n) => acc + n, 0);
|
|
76
|
+
out[stage] = {
|
|
77
|
+
count: sorted.length,
|
|
78
|
+
p50: round(percentile(sorted, 50)),
|
|
79
|
+
p95: round(percentile(sorted, 95)),
|
|
80
|
+
p99: round(percentile(sorted, 99)),
|
|
81
|
+
max: round(sorted[sorted.length - 1]),
|
|
82
|
+
mean: round(sum / sorted.length),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Per-channel token-bucket safety floor for the SecureComs agent bridge.
|
|
2
|
+
//
|
|
3
|
+
// A lazy-refill token bucket: tokens regenerate based on elapsed wall-clock time
|
|
4
|
+
// at each read, so there is no timer/interval. The clock is injectable for
|
|
5
|
+
// deterministic tests. State is per-key; an unseen key starts full.
|
|
6
|
+
//
|
|
7
|
+
// `capacity <= 0` disables the floor entirely (tryConsume always allows),
|
|
8
|
+
// mirroring how `maxBuffer <= 0` disables the bridge's buffer cap.
|
|
9
|
+
export class TokenBucketFloor {
|
|
10
|
+
capacity;
|
|
11
|
+
refillPerMs;
|
|
12
|
+
now;
|
|
13
|
+
buckets = new Map();
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
this.capacity = opts.capacity;
|
|
16
|
+
this.refillPerMs = opts.refillPerMinute / 60_000;
|
|
17
|
+
this.now = opts.now ?? Date.now;
|
|
18
|
+
}
|
|
19
|
+
refill(key) {
|
|
20
|
+
const t = this.now();
|
|
21
|
+
let b = this.buckets.get(key);
|
|
22
|
+
if (!b) {
|
|
23
|
+
b = { tokens: this.capacity, lastRefill: t };
|
|
24
|
+
this.buckets.set(key, b);
|
|
25
|
+
return b;
|
|
26
|
+
}
|
|
27
|
+
const elapsed = t - b.lastRefill;
|
|
28
|
+
if (elapsed > 0) {
|
|
29
|
+
b.tokens = Math.min(this.capacity, b.tokens + elapsed * this.refillPerMs);
|
|
30
|
+
b.lastRefill = t;
|
|
31
|
+
}
|
|
32
|
+
return b;
|
|
33
|
+
}
|
|
34
|
+
/** Consume one token. Returns true if allowed, false if the bucket is empty. */
|
|
35
|
+
tryConsume(key) {
|
|
36
|
+
if (this.capacity <= 0)
|
|
37
|
+
return true; // disabled
|
|
38
|
+
const b = this.refill(key);
|
|
39
|
+
if (b.tokens >= 1) {
|
|
40
|
+
b.tokens -= 1;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/** Milliseconds until one token is available (0 if already available). */
|
|
46
|
+
msUntilNextToken(key) {
|
|
47
|
+
if (this.capacity <= 0)
|
|
48
|
+
return 0;
|
|
49
|
+
const b = this.refill(key);
|
|
50
|
+
if (b.tokens >= 1)
|
|
51
|
+
return 0;
|
|
52
|
+
if (this.refillPerMs <= 0)
|
|
53
|
+
return Infinity;
|
|
54
|
+
return Math.ceil((1 - b.tokens) / this.refillPerMs);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { basename, extname, isAbsolute, resolve } from "node:path";
|
|
2
|
+
function slugify(s) {
|
|
3
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
4
|
+
}
|
|
5
|
+
export function deriveServiceConfig(input) {
|
|
6
|
+
const stem = input.label ?? basename(input.credentialPath, extname(input.credentialPath));
|
|
7
|
+
const slug = slugify(stem);
|
|
8
|
+
const cred = isAbsolute(input.credentialPath) ? input.credentialPath : resolve(input.credentialPath);
|
|
9
|
+
const args = ["--credential", cred];
|
|
10
|
+
if (input.storePath)
|
|
11
|
+
args.push("--store", isAbsolute(input.storePath) ? input.storePath : resolve(input.storePath));
|
|
12
|
+
if (input.port != null)
|
|
13
|
+
args.push("--port", String(input.port));
|
|
14
|
+
return {
|
|
15
|
+
slug,
|
|
16
|
+
label: `com.securecoms.bridge.${slug}`,
|
|
17
|
+
unitName: `securecoms-bridge-${slug}`,
|
|
18
|
+
args,
|
|
19
|
+
execPath: input.execPath,
|
|
20
|
+
binScript: input.binScript,
|
|
21
|
+
uid: input.uid,
|
|
22
|
+
plistPath: `${input.home}/Library/LaunchAgents/com.securecoms.bridge.${slug}.plist`,
|
|
23
|
+
unitPath: `${input.home}/.config/systemd/user/securecoms-bridge-${slug}.service`,
|
|
24
|
+
logPath: `${input.home}/Library/Logs/securecoms-bridge-${slug}.log`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { deriveServiceConfig } from "./config.js";
|
|
3
|
+
import { renderLaunchdPlist } from "./launchd.js";
|
|
4
|
+
import { renderSystemdUnit } from "./systemd.js";
|
|
5
|
+
export class ServiceUnsupportedError extends Error {
|
|
6
|
+
}
|
|
7
|
+
function tokenHint(input) {
|
|
8
|
+
return `${dirname(input.credentialPath)}/bridge-token`;
|
|
9
|
+
}
|
|
10
|
+
function requireOk(r, what) {
|
|
11
|
+
if (r.status !== 0)
|
|
12
|
+
throw new Error(`${what} failed (exit ${r.status})${r.stderr ? `: ${r.stderr.trim()}` : ""}`);
|
|
13
|
+
}
|
|
14
|
+
export function installService(input, deps) {
|
|
15
|
+
const cfg = deriveServiceConfig(input);
|
|
16
|
+
if (deps.platform === "darwin") {
|
|
17
|
+
deps.mkdirp(dirname(cfg.plistPath));
|
|
18
|
+
deps.mkdirp(dirname(cfg.logPath));
|
|
19
|
+
deps.writeFile(cfg.plistPath, renderLaunchdPlist(cfg));
|
|
20
|
+
// Reload-safe: boot out any prior instance (ignore failure), then bootstrap + enable.
|
|
21
|
+
deps.run("launchctl", ["bootout", `gui/${cfg.uid}/${cfg.label}`]);
|
|
22
|
+
// bootstrap actually loads the service — its failure is the real install failure,
|
|
23
|
+
// so surface it (don't let a broken install look silently successful).
|
|
24
|
+
requireOk(deps.run("launchctl", ["bootstrap", `gui/${cfg.uid}`, cfg.plistPath]), "launchctl bootstrap");
|
|
25
|
+
deps.run("launchctl", ["enable", `gui/${cfg.uid}/${cfg.label}`]);
|
|
26
|
+
// Start (or restart) the service now so install doesn't wait for the next login.
|
|
27
|
+
// Mirrors the linux `enable --now`; kickstart -k re-launches if an old instance lingers.
|
|
28
|
+
requireOk(deps.run("launchctl", ["kickstart", "-k", `gui/${cfg.uid}/${cfg.label}`]), "launchctl kickstart");
|
|
29
|
+
return { label: cfg.label, filePath: cfg.plistPath, logHint: cfg.logPath, tokenHint: tokenHint(input) };
|
|
30
|
+
}
|
|
31
|
+
if (deps.platform === "linux") {
|
|
32
|
+
deps.mkdirp(dirname(cfg.unitPath));
|
|
33
|
+
deps.writeFile(cfg.unitPath, renderSystemdUnit(cfg));
|
|
34
|
+
deps.run("systemctl", ["--user", "daemon-reload"]);
|
|
35
|
+
deps.run("loginctl", ["enable-linger", deps.user]);
|
|
36
|
+
// enable --now is the load-bearing command (enable at login + start now); surface failure.
|
|
37
|
+
requireOk(deps.run("systemctl", ["--user", "enable", "--now", cfg.unitName]), "systemctl --user enable --now");
|
|
38
|
+
return { label: cfg.unitName, filePath: cfg.unitPath, logHint: `journalctl --user -u ${cfg.unitName} -f`, tokenHint: tokenHint(input) };
|
|
39
|
+
}
|
|
40
|
+
throw new ServiceUnsupportedError(`Service install isn't supported on ${deps.platform} yet. Run the agent in a terminal, or use a process manager.`);
|
|
41
|
+
}
|
|
42
|
+
export function uninstallService(input, deps) {
|
|
43
|
+
const cfg = deriveServiceConfig(input);
|
|
44
|
+
if (deps.platform === "darwin") {
|
|
45
|
+
const existed = deps.fileExists(cfg.plistPath);
|
|
46
|
+
deps.run("launchctl", ["bootout", `gui/${cfg.uid}/${cfg.label}`]);
|
|
47
|
+
if (existed)
|
|
48
|
+
deps.removeFile(cfg.plistPath);
|
|
49
|
+
return { removed: existed, filePath: cfg.plistPath };
|
|
50
|
+
}
|
|
51
|
+
if (deps.platform === "linux") {
|
|
52
|
+
const existed = deps.fileExists(cfg.unitPath);
|
|
53
|
+
deps.run("systemctl", ["--user", "disable", "--now", cfg.unitName]);
|
|
54
|
+
if (existed)
|
|
55
|
+
deps.removeFile(cfg.unitPath);
|
|
56
|
+
deps.run("systemctl", ["--user", "daemon-reload"]);
|
|
57
|
+
return { removed: existed, filePath: cfg.unitPath };
|
|
58
|
+
}
|
|
59
|
+
throw new ServiceUnsupportedError(`Service uninstall isn't supported on ${deps.platform} yet.`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** A deterministic launchd LaunchAgent plist. ProgramArguments re-invoke the
|
|
2
|
+
* running bin in run mode; KeepAlive restarts on crash, RunAtLoad starts at login. */
|
|
3
|
+
export function renderLaunchdPlist(cfg) {
|
|
4
|
+
const argv = [cfg.execPath, cfg.binScript, ...cfg.args];
|
|
5
|
+
const programArgs = argv.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
6
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
7
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
8
|
+
<plist version="1.0">
|
|
9
|
+
<dict>
|
|
10
|
+
<key>Label</key>
|
|
11
|
+
<string>${cfg.label}</string>
|
|
12
|
+
<key>ProgramArguments</key>
|
|
13
|
+
<array>
|
|
14
|
+
${programArgs}
|
|
15
|
+
</array>
|
|
16
|
+
<key>RunAtLoad</key>
|
|
17
|
+
<true/>
|
|
18
|
+
<key>KeepAlive</key>
|
|
19
|
+
<true/>
|
|
20
|
+
<key>StandardOutPath</key>
|
|
21
|
+
<string>${escapeXml(cfg.logPath)}</string>
|
|
22
|
+
<key>StandardErrorPath</key>
|
|
23
|
+
<string>${escapeXml(cfg.logPath)}</string>
|
|
24
|
+
</dict>
|
|
25
|
+
</plist>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
function escapeXml(s) {
|
|
29
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Quotes a single systemd ExecStart token if it contains whitespace or a
|
|
2
|
+
* double-quote character. Tokens that are already safe pass through unchanged
|
|
3
|
+
* so the common no-space case stays readable in the unit file. */
|
|
4
|
+
function sdQuote(s) {
|
|
5
|
+
return s.includes(" ") || s.includes('"') ? `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : s;
|
|
6
|
+
}
|
|
7
|
+
/** A systemd --user unit. ExecStart re-invokes the running bin in run mode;
|
|
8
|
+
* Restart=always supervises crashes. WantedBy=default.target enables at login. */
|
|
9
|
+
export function renderSystemdUnit(cfg) {
|
|
10
|
+
const execStart = [cfg.execPath, cfg.binScript, ...cfg.args].map(sdQuote).join(" ");
|
|
11
|
+
return `[Unit]
|
|
12
|
+
Description=SecureComs agent bridge (${cfg.slug})
|
|
13
|
+
After=network-online.target
|
|
14
|
+
Wants=network-online.target
|
|
15
|
+
|
|
16
|
+
[Service]
|
|
17
|
+
Type=simple
|
|
18
|
+
ExecStart=${execStart}
|
|
19
|
+
Restart=always
|
|
20
|
+
RestartSec=5
|
|
21
|
+
|
|
22
|
+
[Install]
|
|
23
|
+
WantedBy=default.target
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warm libsodium once per process, before any file-crypto call.
|
|
3
|
+
*
|
|
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 entire
|
|
25
|
+
* synchronous surface, sixteen exported synchronous functions throw when
|
|
26
|
+
* libsodium's WASM heap is not yet initialised (`<member> is not a function`
|
|
27
|
+
* or `Cannot read properties of undefined (reading '_malloc')`), and
|
|
28
|
+
* `generateEphemeralKeypair` is the ASYNC export that awaits `sodium.ready`
|
|
29
|
+
* internally on the SAME module instance those synchronous exports use. ⛔ Do
|
|
30
|
+
* not "simplify" the delegate by inlining a keypair mint back here: that is a
|
|
31
|
+
* second warm-up, not a shortcut.
|
|
32
|
+
*
|
|
33
|
+
* ⛔ 2. DO NOT IMPORT `libsodium-wrappers-sumo` DIRECTLY HERE. ⇒ THIS ONE STILL
|
|
34
|
+
* BINDS ON THIS FILE, UNCHANGED. `esbuild.config.mjs:15` inlines every
|
|
35
|
+
* `@agentvault/connect-*` workspace package into the bridge bundle and that
|
|
36
|
+
* specifier is absent from the EXTERNAL list at `:3-8`, so esbuild would bundle
|
|
37
|
+
* a SECOND libsodium instance out of this file. The await would then warm a
|
|
38
|
+
* heap that nothing reads, and this fix would look correct and do nothing.
|
|
39
|
+
* ⚠️ The same inlining is why the delegate below is not a no-op in the shipped
|
|
40
|
+
* bundle: the SDK barrier is inlined into `dist/bin.js` exactly once, and this
|
|
41
|
+
* delegate points at that one copy.
|
|
42
|
+
*
|
|
43
|
+
* ⛔ 3. MEMOIZE THE PROMISE, NOT A BOOLEAN. ⇒ ITS SUBJECT MOVED. The memo is the
|
|
44
|
+
* SDK module's `warming`. Measured: under `if (warmed) return; await ...;
|
|
45
|
+
* warmed = true;` two CONCURRENT callers both run the body. The bridge has two
|
|
46
|
+
* independent entry points (send and receive), so that concurrency is real
|
|
47
|
+
* here, which is why the finding was written here first. It is pinned now by
|
|
48
|
+
* `packages/connect-sdk-ts/src/sodium-ready.memo.gate.test.ts` (G-C1) and by
|
|
49
|
+
* `src/sodium-ready.retry.test.ts:52-69` in this package, which reaches the SDK
|
|
50
|
+
* memo THROUGH this delegate.
|
|
51
|
+
*
|
|
52
|
+
* ⛔⛔ 4. THE `catch` THAT CLEARS THE MEMO IS NOT REDUNDANT. DO NOT DELETE IT.
|
|
53
|
+
* ⇒ ITS SUBJECT MOVED, AND THE CONSEQUENCE STILL LANDS IN THIS PACKAGE. A bare
|
|
54
|
+
* `warming ??= ...` in the SDK module would hold a REJECTED promise for the
|
|
55
|
+
* life of the process if the FIRST warm-up rejects (WASM instantiation failure,
|
|
56
|
+
* OOM, a corrupt binary), and every later prepareAttachment AND
|
|
57
|
+
* materializeAttachment IN THIS PACKAGE would then reject at its first
|
|
58
|
+
* statement, forever. ⛔ ON THE RECEIVE SIDE THAT IS SILENT: the ingest pump's
|
|
59
|
+
* fail-soft catch acks the message, increments the failed-attachment counter
|
|
60
|
+
* and logs, so one transient failure becomes a PERMANENT, PROCESS-WIDE
|
|
61
|
+
* attachment outage that never surfaces as a crash. The SDK module clears the
|
|
62
|
+
* memo on rejection and RETHROWS, so the first caller still sees the real
|
|
63
|
+
* failure. Pinned by `src/sodium-ready.retry.test.ts`, both halves.
|
|
64
|
+
*/
|
|
65
|
+
export declare function ensureSodiumReady(): Promise<void>;
|
package/dist/vault.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Vault-crypto integrity failures: digest mismatch, DEK-wrap AEAD failure
|
|
2
|
+
* (bad/dropped nonce, wrong KEK, spliced ciphertext), or "no wrap for any
|
|
3
|
+
* generation we hold". Distinguished from generic REST/plumbing errors so
|
|
4
|
+
* the HTTP boundary can map it to a dedicated error code (errors.ts
|
|
5
|
+
* `vault_integrity_failed`) instead of a bare 500. */
|
|
6
|
+
export declare class VaultIntegrityError extends Error {
|
|
7
|
+
constructor(message: string, cause?: unknown);
|
|
8
|
+
}
|
|
9
|
+
export interface VaultWriteDeps {
|
|
10
|
+
exporterKekForGeneration: (gen: number) => Uint8Array;
|
|
11
|
+
currentGeneration: number;
|
|
12
|
+
createNode: (body: any) => Promise<{
|
|
13
|
+
node_id: string;
|
|
14
|
+
presign_post: {
|
|
15
|
+
url: string;
|
|
16
|
+
fields: Record<string, string>;
|
|
17
|
+
} | null;
|
|
18
|
+
}>;
|
|
19
|
+
putBlob: (presign: {
|
|
20
|
+
url: string;
|
|
21
|
+
fields: Record<string, string>;
|
|
22
|
+
}, ciphertext: Uint8Array) => Promise<void>;
|
|
23
|
+
commitNode: (nodeId: string) => Promise<{
|
|
24
|
+
status: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export declare function vaultWriteImpl(args: {
|
|
28
|
+
enclaveId: string;
|
|
29
|
+
nodeId: string;
|
|
30
|
+
name: string;
|
|
31
|
+
bytes: Uint8Array;
|
|
32
|
+
deps: VaultWriteDeps;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
node_id: string;
|
|
35
|
+
}>;
|
|
36
|
+
export interface VaultReadDeps {
|
|
37
|
+
exporterKekForGeneration: (gen: number) => Uint8Array;
|
|
38
|
+
getNodeForDownload: (nodeId: string) => Promise<{
|
|
39
|
+
download_url: string;
|
|
40
|
+
wraps: {
|
|
41
|
+
kek_generation: number;
|
|
42
|
+
wrapped_dek_b64: string;
|
|
43
|
+
}[];
|
|
44
|
+
content_hash: string | null;
|
|
45
|
+
}>;
|
|
46
|
+
getBlob: (url: string) => Promise<Uint8Array>;
|
|
47
|
+
}
|
|
48
|
+
export declare function vaultReadImpl(args: {
|
|
49
|
+
enclaveId: string;
|
|
50
|
+
nodeId: string;
|
|
51
|
+
deps: VaultReadDeps;
|
|
52
|
+
}): Promise<Uint8Array>;
|
|
53
|
+
export interface VaultListRawNode {
|
|
54
|
+
node_id: string;
|
|
55
|
+
kind: "folder" | "file";
|
|
56
|
+
parent_id: string | null;
|
|
57
|
+
enc_name_b64: string;
|
|
58
|
+
name_kek_generation: number;
|
|
59
|
+
ciphertext_len: number | null;
|
|
60
|
+
content_hash: string | null;
|
|
61
|
+
}
|
|
62
|
+
export interface VaultListNode {
|
|
63
|
+
node_id: string;
|
|
64
|
+
kind: "folder" | "file";
|
|
65
|
+
parent_id: string | null;
|
|
66
|
+
name: string;
|
|
67
|
+
size_bytes: number | null;
|
|
68
|
+
content_hash: string | null;
|
|
69
|
+
}
|
|
70
|
+
export interface VaultListDeps {
|
|
71
|
+
exporterKekForGeneration: (gen: number) => Uint8Array;
|
|
72
|
+
listNodes: (parentId: string | null) => Promise<{
|
|
73
|
+
nodes: VaultListRawNode[];
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
export declare function vaultListImpl(args: {
|
|
77
|
+
enclaveId: string;
|
|
78
|
+
parentId: string | null;
|
|
79
|
+
deps: VaultListDeps;
|
|
80
|
+
}): Promise<VaultListNode[]>;
|
|
81
|
+
export interface VaultDeleteDeps {
|
|
82
|
+
deleteNode: (nodeId: string) => Promise<{
|
|
83
|
+
deleted: string;
|
|
84
|
+
bytes_freed: number;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
/** No crypto on delete (the server drops an opaque row) — kept as a thin
|
|
88
|
+
* pass-through so BridgeAgent.vaultDelete has the same deps-injection shape
|
|
89
|
+
* as write/read/list for testability and symmetry. */
|
|
90
|
+
export declare function vaultDeleteImpl(args: {
|
|
91
|
+
nodeId: string;
|
|
92
|
+
deps: VaultDeleteDeps;
|
|
93
|
+
}): Promise<{
|
|
94
|
+
deleted: string;
|
|
95
|
+
bytes_freed: number;
|
|
96
|
+
}>;
|