zas-agent 0.6.0 → 0.6.1
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/CHANGELOG.md +33 -1
- package/dist/cli.js +211 -110
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.1] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- A Directo run now answers with the engine's own account of itself, not
|
|
15
|
+
only with one word. `zas_send_direct`, `zas_receive_direct` and `zas_jobs`
|
|
16
|
+
carry the candidate counts by type for both ends, the ICE and gathering
|
|
17
|
+
states, how many TURN URLs were supplied against how many the connection
|
|
18
|
+
kept, the selected pair, the restarts, and a timestamped trace of the
|
|
19
|
+
session: every candidate as it arrived, the state changes, and the verdict.
|
|
20
|
+
Types, transports and timings only — never an address, a port, a host name
|
|
21
|
+
or a credential. A run that fails on a machine you are not sitting at can
|
|
22
|
+
now be diagnosed from the tool result.
|
|
23
|
+
- The thrown error behind a failure is kept as `detail`. The reason stays a
|
|
24
|
+
closed word for grouping; `detail` is the sentence the exception carried,
|
|
25
|
+
which every failure used to discard at the catch.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- The receiver pre-gathers a candidate pool. An agent hands its whole
|
|
30
|
+
candidate set over at once, so the far end could start checking before its
|
|
31
|
+
own relay candidates existed, connect on the first pair it found, and be
|
|
32
|
+
left with nothing when that pair died seconds later.
|
|
33
|
+
- The transport of a candidate is read whichever way the peer wrote the line.
|
|
34
|
+
This package prefixes the SDP attribute, so its candidates used to be
|
|
35
|
+
reported with an unknown transport.
|
|
36
|
+
|
|
10
37
|
## [0.6.0] - 2026-09-03
|
|
11
38
|
|
|
12
39
|
### Added
|
|
@@ -156,7 +183,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
156
183
|
two separate identities that cannot read each other's keys.
|
|
157
184
|
- Install snippets for Claude Code and Codex, printed by `zas-agent pair`.
|
|
158
185
|
|
|
159
|
-
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.
|
|
186
|
+
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.6.1...HEAD
|
|
187
|
+
[0.6.1]: https://github.com/soke1556/zas-agent/compare/v0.6.0...v0.6.1
|
|
188
|
+
[0.6.0]: https://github.com/soke1556/zas-agent/compare/v0.5.1...v0.6.0
|
|
189
|
+
[0.5.1]: https://github.com/soke1556/zas-agent/compare/v0.5.0...v0.5.1
|
|
190
|
+
[0.5.0]: https://github.com/soke1556/zas-agent/compare/v0.4.0...v0.5.0
|
|
191
|
+
[0.4.0]: https://github.com/soke1556/zas-agent/compare/v0.3.0...v0.4.0
|
|
160
192
|
[0.3.0]: https://github.com/soke1556/zas-agent/compare/v0.2.1...v0.3.0
|
|
161
193
|
[0.2.1]: https://github.com/soke1556/zas-agent/compare/v0.2.0...v0.2.1
|
|
162
194
|
[0.2.0]: https://github.com/soke1556/zas-agent/compare/v0.1.1...v0.2.0
|
package/dist/cli.js
CHANGED
|
@@ -830,89 +830,6 @@ async function runPair(opts) {
|
|
|
830
830
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
831
831
|
import { z } from "zod";
|
|
832
832
|
|
|
833
|
-
// src/direct.ts
|
|
834
|
-
import { randomBytes } from "node:crypto";
|
|
835
|
-
import { openAsBlob } from "node:fs";
|
|
836
|
-
import { basename as basename2 } from "node:path";
|
|
837
|
-
|
|
838
|
-
// src/shared/manifest.ts
|
|
839
|
-
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
840
|
-
|
|
841
|
-
// src/shared/constants.ts
|
|
842
|
-
var CHUNKER_VERSION = "chunker_v1";
|
|
843
|
-
var CHUNK_MIN = 1 * 1024 * 1024;
|
|
844
|
-
var CHUNK_AVG_BITS = 22;
|
|
845
|
-
var CHUNK_MAX = 8 * 1024 * 1024;
|
|
846
|
-
var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
|
|
847
|
-
var GEAR_SEED = "zas-gear-v1:";
|
|
848
|
-
var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
|
|
849
|
-
var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
|
|
850
|
-
var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
|
|
851
|
-
var MANIFEST_VERSION = 1;
|
|
852
|
-
var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
|
|
853
|
-
var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
|
|
854
|
-
var SAFE_STORED_FILE_LIMITS = {
|
|
855
|
-
anon: 30 * 1024 * 1024,
|
|
856
|
-
free: FREE_RELAY_MAX_BYTES,
|
|
857
|
-
pro: 200 * 1024 * 1024,
|
|
858
|
-
max: 200 * 1024 * 1024
|
|
859
|
-
};
|
|
860
|
-
var STORED_FILE_LIMITS_V2 = {
|
|
861
|
-
anon: 30 * 1024 * 1024,
|
|
862
|
-
free: FREE_RELAY_MAX_BYTES,
|
|
863
|
-
pro: 5 * 1024 * 1024 * 1024,
|
|
864
|
-
max: 20 * 1024 * 1024 * 1024
|
|
865
|
-
};
|
|
866
|
-
var STORAGE_LIMITS = {
|
|
867
|
-
anon: 30 * 1024 * 1024,
|
|
868
|
-
free: 100 * 1024 * 1024,
|
|
869
|
-
pro: null,
|
|
870
|
-
max: null
|
|
871
|
-
};
|
|
872
|
-
var THUMBNAIL_MAX_EDGE = 640;
|
|
873
|
-
var THUMBNAIL_MAX_BYTES = 14e4;
|
|
874
|
-
|
|
875
|
-
// src/shared/manifest.ts
|
|
876
|
-
function newManifest(partial) {
|
|
877
|
-
return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
|
|
878
|
-
}
|
|
879
|
-
var ENVELOPE_MAGIC = 90;
|
|
880
|
-
var KEY_VERSION_LEGACY = 1;
|
|
881
|
-
function envelopeVersion(sealed) {
|
|
882
|
-
if (sealed.length > 26 && sealed[0] === ENVELOPE_MAGIC && sealed[1] >= 1) return sealed[1];
|
|
883
|
-
return KEY_VERSION_LEGACY;
|
|
884
|
-
}
|
|
885
|
-
function sealBytes(key, version, plain) {
|
|
886
|
-
if (version < 1 || version > 255) throw new Error("key version out of range");
|
|
887
|
-
const nonce = new Uint8Array(24);
|
|
888
|
-
crypto.getRandomValues(nonce);
|
|
889
|
-
const ct = xchacha20poly1305(key, nonce).encrypt(plain);
|
|
890
|
-
return concatBytes(new Uint8Array([ENVELOPE_MAGIC, version]), nonce, ct);
|
|
891
|
-
}
|
|
892
|
-
function openSealed(key, sealed) {
|
|
893
|
-
const versioned = envelopeVersion(sealed) !== KEY_VERSION_LEGACY || sealed[0] === ENVELOPE_MAGIC;
|
|
894
|
-
if (versioned && sealed.length > 26) {
|
|
895
|
-
try {
|
|
896
|
-
return xchacha20poly1305(key, sealed.slice(2, 26)).decrypt(sealed.slice(26));
|
|
897
|
-
} catch {
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
return xchacha20poly1305(key, sealed.slice(0, 24)).decrypt(sealed.slice(24));
|
|
901
|
-
}
|
|
902
|
-
var sealRaw = sealBytes;
|
|
903
|
-
var openRaw = openSealed;
|
|
904
|
-
function sealManifest(channelKey, manifest, version = KEY_VERSION_LEGACY) {
|
|
905
|
-
return sealBytes(channelKey, version, new TextEncoder().encode(JSON.stringify(manifest)));
|
|
906
|
-
}
|
|
907
|
-
function openManifest(channelKey, sealed) {
|
|
908
|
-
const m = JSON.parse(new TextDecoder().decode(openSealed(channelKey, sealed)));
|
|
909
|
-
if (!Array.isArray(m.chunks)) m.chunks = [];
|
|
910
|
-
return m;
|
|
911
|
-
}
|
|
912
|
-
function decryptChannelName(channelKey, sealed) {
|
|
913
|
-
return new TextDecoder().decode(openSealed(channelKey, sealed));
|
|
914
|
-
}
|
|
915
|
-
|
|
916
833
|
// src/shared/direct.ts
|
|
917
834
|
var DIRECT_OFFER_TTL_MS = 10 * 60 * 1e3;
|
|
918
835
|
var DIRECT_CLAIMED_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -948,6 +865,13 @@ var FRAME_CHUNK = 2;
|
|
|
948
865
|
var FRAME_DONE = 3;
|
|
949
866
|
var FRAME_ABORT = 4;
|
|
950
867
|
var FRAME_CREDIT = 5;
|
|
868
|
+
function directMetaOf(file, fallbackName) {
|
|
869
|
+
return {
|
|
870
|
+
name: file.name || fallbackName || "zas",
|
|
871
|
+
size: file.size,
|
|
872
|
+
mime: file.type || "application/octet-stream"
|
|
873
|
+
};
|
|
874
|
+
}
|
|
951
875
|
function encodeMeta(meta) {
|
|
952
876
|
const body = new TextEncoder().encode(JSON.stringify(meta));
|
|
953
877
|
const frame = new Uint8Array(1 + body.length);
|
|
@@ -1020,9 +944,63 @@ function candType(candidate) {
|
|
|
1020
944
|
const m = /\styp\s+(\S+)/.exec(candidate?.candidate ?? "");
|
|
1021
945
|
return m ? m[1] : "other";
|
|
1022
946
|
}
|
|
947
|
+
function candProto(candidate) {
|
|
948
|
+
const line = candidate?.candidate ?? "";
|
|
949
|
+
const m = /^(?:a=)?(?:candidate:)?\S+\s+\d+\s+(\S+)/.exec(line);
|
|
950
|
+
const proto = m ? m[1].toLowerCase() : "?";
|
|
951
|
+
const tcp = /\stcptype\s+(\S+)/.exec(line);
|
|
952
|
+
return tcp ? `${proto}/${tcp[1]}` : proto;
|
|
953
|
+
}
|
|
954
|
+
var TRACE_MAX = 200;
|
|
955
|
+
var DETAIL_MAX = 200;
|
|
956
|
+
function errText(e) {
|
|
957
|
+
const raw = e instanceof Error ? e.message || e.name : String(e);
|
|
958
|
+
const line = raw.replace(/\s+/g, " ").trim();
|
|
959
|
+
return line.length > DETAIL_MAX ? `${line.slice(0, DETAIL_MAX)}\u2026` : line;
|
|
960
|
+
}
|
|
961
|
+
var traceStart = /* @__PURE__ */ new WeakMap();
|
|
962
|
+
function traceAdd(diag, line) {
|
|
963
|
+
if (diag.trace.length >= TRACE_MAX) return;
|
|
964
|
+
if (diag.trace.length === TRACE_MAX - 1) {
|
|
965
|
+
diag.trace.push("trace full");
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const t0 = traceStart.get(diag);
|
|
969
|
+
const at = t0 === void 0 ? 0 : (Date.now() - t0) / 1e3;
|
|
970
|
+
diag.trace.push(`${at.toFixed(2)} ${line}`);
|
|
971
|
+
}
|
|
972
|
+
function iceShapes(servers) {
|
|
973
|
+
const seen = /* @__PURE__ */ new Map();
|
|
974
|
+
for (const server of servers ?? []) {
|
|
975
|
+
const urls = typeof server.urls === "string" ? [server.urls] : server.urls;
|
|
976
|
+
for (const url of urls ?? []) {
|
|
977
|
+
const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase() ?? "other";
|
|
978
|
+
const transport = /[?&]transport=(\w+)/i.exec(url)?.[1]?.toLowerCase();
|
|
979
|
+
const shape = transport ? `${scheme}/${transport}` : scheme;
|
|
980
|
+
seen.set(shape, (seen.get(shape) ?? 0) + 1);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const parts = [...seen].map(([shape, n]) => `${shape}x${n}`);
|
|
984
|
+
return parts.length > 0 ? parts.join(" ") : "none";
|
|
985
|
+
}
|
|
986
|
+
function formatDirectDiagLines(d) {
|
|
987
|
+
const connect = d.msConnect === void 0 ? "-" : `${d.msConnect}ms`;
|
|
988
|
+
const pair = d.pairLocal || d.pairRemote ? `${d.pairLocal || "?"}/${d.pairRemote || "?"}` : "none";
|
|
989
|
+
return [
|
|
990
|
+
`ms=${d.ms} connect=${connect} bytes=${d.bytes} restarts=${d.restarts} pair=${pair}`,
|
|
991
|
+
...d.detail === void 0 ? [] : [`detail ${d.detail}`],
|
|
992
|
+
`local host=${d.localHost} srflx=${d.localSrflx} relay=${d.localRelay} remote host=${d.remoteHost} srflx=${d.remoteSrflx} relay=${d.remoteRelay}`,
|
|
993
|
+
`turn supplied=${d.turnUrlsSupplied} configured=${d.turnUrlsConfigured} ice=${d.iceState} gather=${d.gatherState} remote_desc=${d.hadRemoteDesc}`,
|
|
994
|
+
...d.trace ?? []
|
|
995
|
+
];
|
|
996
|
+
}
|
|
1023
997
|
function countCand(diag, side, c) {
|
|
1024
|
-
if (!c)
|
|
998
|
+
if (!c) {
|
|
999
|
+
traceAdd(diag, `${side} end-of-candidates`);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1025
1002
|
const t = candType(c);
|
|
1003
|
+
traceAdd(diag, `${side} ${t} ${candProto(c)}`);
|
|
1026
1004
|
if (t === "host") diag[side === "local" ? "localHost" : "remoteHost"]++;
|
|
1027
1005
|
else if (t === "srflx" || t === "prflx") diag[side === "local" ? "localSrflx" : "remoteSrflx"]++;
|
|
1028
1006
|
else if (t === "relay") diag[side === "local" ? "localRelay" : "remoteRelay"]++;
|
|
@@ -1063,7 +1041,17 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1063
1041
|
const initialIce = ice ?? DIRECT_ICE;
|
|
1064
1042
|
const pc = new RTCPeerConnection({
|
|
1065
1043
|
iceServers: initialIce,
|
|
1066
|
-
iceTransportPolicy
|
|
1044
|
+
iceTransportPolicy,
|
|
1045
|
+
// Gather before there is anything to gather for. A receiver whose peer
|
|
1046
|
+
// is an agent gets the whole remote candidate set at once — the agent
|
|
1047
|
+
// writes it and leaves — so checking starts in the same tick as the local
|
|
1048
|
+
// description, a pair comes up on the first candidate found, and
|
|
1049
|
+
// gathering is reported complete before any relay allocation returns.
|
|
1050
|
+
// Measured 2026-09-04: one host candidate, no relay, gathering done at
|
|
1051
|
+
// 240 ms, and when that pair died five seconds later there was nothing
|
|
1052
|
+
// to fall back to. The pool does the allocations up front, at construction,
|
|
1053
|
+
// so the relay candidates exist before the first check is ever sent.
|
|
1054
|
+
iceCandidatePoolSize: 1
|
|
1067
1055
|
});
|
|
1068
1056
|
let phase = "connecting";
|
|
1069
1057
|
let disconnectTimer;
|
|
@@ -1085,7 +1073,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1085
1073
|
turnUrlsSupplied: countTurnUrls(initialIce),
|
|
1086
1074
|
turnUrlsConfigured: 0,
|
|
1087
1075
|
bytes: 0,
|
|
1088
|
-
restarts: 0
|
|
1076
|
+
restarts: 0,
|
|
1077
|
+
trace: []
|
|
1078
|
+
};
|
|
1079
|
+
traceStart.set(diag, startedAt);
|
|
1080
|
+
traceAdd(diag, `ice servers ${iceShapes(initialIce)} policy=${iceTransportPolicy}`);
|
|
1081
|
+
pc.onicegatheringstatechange = () => {
|
|
1082
|
+
traceAdd(diag, `gather ${String(pc.iceGatheringState ?? "")}`);
|
|
1089
1083
|
};
|
|
1090
1084
|
const cleanup = () => {
|
|
1091
1085
|
clearTimeout(waitTimer);
|
|
@@ -1094,6 +1088,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1094
1088
|
clearTimeout(restartTimer);
|
|
1095
1089
|
pc.onicecandidate = null;
|
|
1096
1090
|
pc.oniceconnectionstatechange = null;
|
|
1091
|
+
pc.onicegatheringstatechange = null;
|
|
1097
1092
|
pc.close();
|
|
1098
1093
|
};
|
|
1099
1094
|
const setPhase = (p) => {
|
|
@@ -1105,6 +1100,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1105
1100
|
diag.gatherState = String(pc.iceGatheringState ?? "");
|
|
1106
1101
|
diag.hadRemoteDesc = hasRemoteDescription(pc);
|
|
1107
1102
|
diag.turnUrlsConfigured = countTurnUrls(pc.getConfiguration().iceServers);
|
|
1103
|
+
traceAdd(diag, `${p}${diag.reason ? ` ${diag.reason}` : ""} ice=${diag.iceState} gather=${diag.gatherState}`);
|
|
1108
1104
|
onDiag?.(diag);
|
|
1109
1105
|
onPhase(p);
|
|
1110
1106
|
cleanup();
|
|
@@ -1112,9 +1108,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1112
1108
|
}
|
|
1113
1109
|
onPhase(p);
|
|
1114
1110
|
};
|
|
1115
|
-
const fail = (reason) => {
|
|
1111
|
+
const fail = (reason, cause) => {
|
|
1116
1112
|
if (phase === "done" || phase === "failed") return;
|
|
1117
1113
|
if (!diag.reason) diag.reason = reason;
|
|
1114
|
+
if (cause !== void 0 && diag.detail === void 0) {
|
|
1115
|
+
diag.detail = errText(cause);
|
|
1116
|
+
traceAdd(diag, `detail ${diag.detail}`);
|
|
1117
|
+
}
|
|
1118
1118
|
setPhase("failed");
|
|
1119
1119
|
};
|
|
1120
1120
|
const waitTimer = setTimeout(() => fail("peer_silent"), DIRECT_SIGNAL_WAIT_MS);
|
|
@@ -1138,12 +1138,14 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1138
1138
|
}
|
|
1139
1139
|
stallState = "restarting";
|
|
1140
1140
|
diag.restarts++;
|
|
1141
|
+
traceAdd(diag, "ice restart");
|
|
1141
1142
|
clearTimeout(restartTimer);
|
|
1142
1143
|
restartTimer = setTimeout(() => fail("disconnected"), stallWindowMs);
|
|
1143
1144
|
onStall();
|
|
1144
1145
|
};
|
|
1145
1146
|
pc.oniceconnectionstatechange = () => {
|
|
1146
1147
|
const s = pc.iceConnectionState;
|
|
1148
|
+
traceAdd(diag, `ice ${String(s ?? "")}`);
|
|
1147
1149
|
if (s === "failed") {
|
|
1148
1150
|
if (diag.msConnect !== void 0 && onStall) stalled();
|
|
1149
1151
|
else fail("ice_failed");
|
|
@@ -1168,7 +1170,10 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1168
1170
|
connected: () => {
|
|
1169
1171
|
clearTimeout(waitTimer);
|
|
1170
1172
|
clearTimeout(connectTimer);
|
|
1171
|
-
if (diag.msConnect === void 0)
|
|
1173
|
+
if (diag.msConnect === void 0) {
|
|
1174
|
+
diag.msConnect = Date.now() - startedAt;
|
|
1175
|
+
traceAdd(diag, "channel open");
|
|
1176
|
+
}
|
|
1172
1177
|
},
|
|
1173
1178
|
isTerminal: () => phase === "done" || phase === "failed",
|
|
1174
1179
|
setStall: (fn, windowMs) => {
|
|
@@ -1177,6 +1182,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1177
1182
|
},
|
|
1178
1183
|
setIceServers: (iceServers) => {
|
|
1179
1184
|
diag.turnUrlsSupplied = countTurnUrls(iceServers);
|
|
1185
|
+
traceAdd(diag, `ice servers replaced ${iceShapes(iceServers)}`);
|
|
1180
1186
|
pc.setConfiguration({ iceServers });
|
|
1181
1187
|
}
|
|
1182
1188
|
};
|
|
@@ -1258,7 +1264,7 @@ function startSender(opts) {
|
|
|
1258
1264
|
const offer = await pc.createOffer({ iceRestart: true });
|
|
1259
1265
|
await pc.setLocalDescription(offer);
|
|
1260
1266
|
await opts.send({ kind: "offer", sdp: offer.sdp, protocol: 2, generation });
|
|
1261
|
-
})().catch(() => s.fail("signaling"));
|
|
1267
|
+
})().catch((e) => s.fail("signaling", e));
|
|
1262
1268
|
}, DIRECT_CONNECT_TIMEOUT_MS);
|
|
1263
1269
|
const dc = pc.createDataChannel("zas-direct", { ordered: true });
|
|
1264
1270
|
dc.binaryType = "arraybuffer";
|
|
@@ -1285,9 +1291,7 @@ function startSender(opts) {
|
|
|
1285
1291
|
};
|
|
1286
1292
|
const pump = async () => {
|
|
1287
1293
|
const meta = {
|
|
1288
|
-
|
|
1289
|
-
size: opts.file.size,
|
|
1290
|
-
mime: opts.file.type || "application/octet-stream",
|
|
1294
|
+
...directMetaOf(opts.file, opts.name),
|
|
1291
1295
|
...opts.label !== void 0 ? { label: opts.label } : {}
|
|
1292
1296
|
};
|
|
1293
1297
|
dc.send(encodeMeta(meta));
|
|
@@ -1326,7 +1330,7 @@ function startSender(opts) {
|
|
|
1326
1330
|
s.diag.pairRemote = pair.remote;
|
|
1327
1331
|
opts.onPath?.(pathOf(pair));
|
|
1328
1332
|
}).catch(() => void 0);
|
|
1329
|
-
void pump().catch(() => s.fail("transfer"));
|
|
1333
|
+
void pump().catch((e) => s.fail("transfer", e));
|
|
1330
1334
|
};
|
|
1331
1335
|
dc.onmessage = (e) => {
|
|
1332
1336
|
try {
|
|
@@ -1352,7 +1356,7 @@ function startSender(opts) {
|
|
|
1352
1356
|
plumbing.setLocalGeneration(generation);
|
|
1353
1357
|
plumbing.expectRemoteGeneration(generation);
|
|
1354
1358
|
await opts.send({ kind: "offer", sdp: offer.sdp, protocol: 2, generation });
|
|
1355
|
-
})().catch(() => s.fail("signaling"));
|
|
1359
|
+
})().catch((e) => s.fail("signaling", e));
|
|
1356
1360
|
return {
|
|
1357
1361
|
accept: (msg) => {
|
|
1358
1362
|
if (s.isTerminal()) return;
|
|
@@ -1362,7 +1366,7 @@ function startSender(opts) {
|
|
|
1362
1366
|
if (msg.generation !== void 0 && answerGeneration !== generation) return;
|
|
1363
1367
|
peerProtocol = msg.protocol === 2 ? 2 : 1;
|
|
1364
1368
|
plumbing.expectRemoteGeneration(answerGeneration);
|
|
1365
|
-
void pc.setRemoteDescription({ type: "answer", sdp: msg.sdp }).then(() => plumbing.remoteDescriptionSet(answerGeneration)).catch(() => s.fail("signaling"));
|
|
1369
|
+
void pc.setRemoteDescription({ type: "answer", sdp: msg.sdp }).then(() => plumbing.remoteDescriptionSet(answerGeneration)).catch((e) => s.fail("signaling", e));
|
|
1366
1370
|
}
|
|
1367
1371
|
if (msg.kind === "ice") plumbing.applyIce(msg.candidate, msg.generation);
|
|
1368
1372
|
},
|
|
@@ -1435,7 +1439,7 @@ function startReceiver(opts) {
|
|
|
1435
1439
|
s.fail(frame.meta.size > DIRECT_FILE_MAX_BYTES ? "too_big" : "protocol");
|
|
1436
1440
|
return;
|
|
1437
1441
|
}
|
|
1438
|
-
if (opts.expectedMeta && (frame.meta.name !== opts.expectedMeta.name || frame.meta.size !== opts.expectedMeta.size
|
|
1442
|
+
if (opts.expectedMeta && (frame.meta.name !== opts.expectedMeta.name || frame.meta.size !== opts.expectedMeta.size)) {
|
|
1439
1443
|
dc?.send(ABORT_FRAME);
|
|
1440
1444
|
s.fail("protocol");
|
|
1441
1445
|
return;
|
|
@@ -1447,7 +1451,7 @@ function startReceiver(opts) {
|
|
|
1447
1451
|
sinkRef = sink;
|
|
1448
1452
|
return sink;
|
|
1449
1453
|
});
|
|
1450
|
-
pipeline.catch(() => s.fail("sink"));
|
|
1454
|
+
pipeline.catch((e3) => s.fail("sink", e3));
|
|
1451
1455
|
} else if (frame.type === "chunk") {
|
|
1452
1456
|
if (!pipeline || finished) throw new Error("chunk_before_meta");
|
|
1453
1457
|
queued += frame.payload.length;
|
|
@@ -1465,7 +1469,7 @@ function startReceiver(opts) {
|
|
|
1465
1469
|
}
|
|
1466
1470
|
return sink;
|
|
1467
1471
|
});
|
|
1468
|
-
pipeline.catch(() => s.fail("sink"));
|
|
1472
|
+
pipeline.catch((e3) => s.fail("sink", e3));
|
|
1469
1473
|
} else if (frame.type === "done") {
|
|
1470
1474
|
if (!pipeline || finished) throw new Error("done_before_meta");
|
|
1471
1475
|
finished = true;
|
|
@@ -1476,7 +1480,7 @@ function startReceiver(opts) {
|
|
|
1476
1480
|
await sink.close();
|
|
1477
1481
|
dc?.send(DONE_FRAME);
|
|
1478
1482
|
s.setPhase("done");
|
|
1479
|
-
}).catch(() => s.fail("integrity"));
|
|
1483
|
+
}).catch((e3) => s.fail("integrity", e3));
|
|
1480
1484
|
} else if (frame.type === "abort") {
|
|
1481
1485
|
s.fail("peer_abort");
|
|
1482
1486
|
}
|
|
@@ -1510,7 +1514,7 @@ function startReceiver(opts) {
|
|
|
1510
1514
|
const answer = await pc.createAnswer();
|
|
1511
1515
|
await pc.setLocalDescription(answer);
|
|
1512
1516
|
await opts.send({ kind: "answer", sdp: answer.sdp, protocol: 2, generation });
|
|
1513
|
-
})().catch(() => s.fail("signaling"));
|
|
1517
|
+
})().catch((e) => s.fail("signaling", e));
|
|
1514
1518
|
}
|
|
1515
1519
|
if (msg.kind === "ice") plumbing.applyIce(msg.candidate, msg.generation);
|
|
1516
1520
|
},
|
|
@@ -1527,6 +1531,89 @@ function startReceiver(opts) {
|
|
|
1527
1531
|
};
|
|
1528
1532
|
}
|
|
1529
1533
|
|
|
1534
|
+
// src/direct.ts
|
|
1535
|
+
import { randomBytes } from "node:crypto";
|
|
1536
|
+
import { openAsBlob } from "node:fs";
|
|
1537
|
+
import { basename as basename2 } from "node:path";
|
|
1538
|
+
|
|
1539
|
+
// src/shared/manifest.ts
|
|
1540
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
1541
|
+
|
|
1542
|
+
// src/shared/constants.ts
|
|
1543
|
+
var CHUNKER_VERSION = "chunker_v1";
|
|
1544
|
+
var CHUNK_MIN = 1 * 1024 * 1024;
|
|
1545
|
+
var CHUNK_AVG_BITS = 22;
|
|
1546
|
+
var CHUNK_MAX = 8 * 1024 * 1024;
|
|
1547
|
+
var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
|
|
1548
|
+
var GEAR_SEED = "zas-gear-v1:";
|
|
1549
|
+
var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
|
|
1550
|
+
var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
|
|
1551
|
+
var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
|
|
1552
|
+
var MANIFEST_VERSION = 1;
|
|
1553
|
+
var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
|
|
1554
|
+
var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
|
|
1555
|
+
var SAFE_STORED_FILE_LIMITS = {
|
|
1556
|
+
anon: 30 * 1024 * 1024,
|
|
1557
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
1558
|
+
pro: 200 * 1024 * 1024,
|
|
1559
|
+
max: 200 * 1024 * 1024
|
|
1560
|
+
};
|
|
1561
|
+
var STORED_FILE_LIMITS_V2 = {
|
|
1562
|
+
anon: 30 * 1024 * 1024,
|
|
1563
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
1564
|
+
pro: 5 * 1024 * 1024 * 1024,
|
|
1565
|
+
max: 20 * 1024 * 1024 * 1024
|
|
1566
|
+
};
|
|
1567
|
+
var STORAGE_LIMITS = {
|
|
1568
|
+
anon: 30 * 1024 * 1024,
|
|
1569
|
+
free: 100 * 1024 * 1024,
|
|
1570
|
+
pro: null,
|
|
1571
|
+
max: null
|
|
1572
|
+
};
|
|
1573
|
+
var THUMBNAIL_MAX_EDGE = 640;
|
|
1574
|
+
var THUMBNAIL_MAX_BYTES = 14e4;
|
|
1575
|
+
|
|
1576
|
+
// src/shared/manifest.ts
|
|
1577
|
+
function newManifest(partial) {
|
|
1578
|
+
return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
|
|
1579
|
+
}
|
|
1580
|
+
var ENVELOPE_MAGIC = 90;
|
|
1581
|
+
var KEY_VERSION_LEGACY = 1;
|
|
1582
|
+
function envelopeVersion(sealed) {
|
|
1583
|
+
if (sealed.length > 26 && sealed[0] === ENVELOPE_MAGIC && sealed[1] >= 1) return sealed[1];
|
|
1584
|
+
return KEY_VERSION_LEGACY;
|
|
1585
|
+
}
|
|
1586
|
+
function sealBytes(key, version, plain) {
|
|
1587
|
+
if (version < 1 || version > 255) throw new Error("key version out of range");
|
|
1588
|
+
const nonce = new Uint8Array(24);
|
|
1589
|
+
crypto.getRandomValues(nonce);
|
|
1590
|
+
const ct = xchacha20poly1305(key, nonce).encrypt(plain);
|
|
1591
|
+
return concatBytes(new Uint8Array([ENVELOPE_MAGIC, version]), nonce, ct);
|
|
1592
|
+
}
|
|
1593
|
+
function openSealed(key, sealed) {
|
|
1594
|
+
const versioned = envelopeVersion(sealed) !== KEY_VERSION_LEGACY || sealed[0] === ENVELOPE_MAGIC;
|
|
1595
|
+
if (versioned && sealed.length > 26) {
|
|
1596
|
+
try {
|
|
1597
|
+
return xchacha20poly1305(key, sealed.slice(2, 26)).decrypt(sealed.slice(26));
|
|
1598
|
+
} catch {
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return xchacha20poly1305(key, sealed.slice(0, 24)).decrypt(sealed.slice(24));
|
|
1602
|
+
}
|
|
1603
|
+
var sealRaw = sealBytes;
|
|
1604
|
+
var openRaw = openSealed;
|
|
1605
|
+
function sealManifest(channelKey, manifest, version = KEY_VERSION_LEGACY) {
|
|
1606
|
+
return sealBytes(channelKey, version, new TextEncoder().encode(JSON.stringify(manifest)));
|
|
1607
|
+
}
|
|
1608
|
+
function openManifest(channelKey, sealed) {
|
|
1609
|
+
const m = JSON.parse(new TextDecoder().decode(openSealed(channelKey, sealed)));
|
|
1610
|
+
if (!Array.isArray(m.chunks)) m.chunks = [];
|
|
1611
|
+
return m;
|
|
1612
|
+
}
|
|
1613
|
+
function decryptChannelName(channelKey, sealed) {
|
|
1614
|
+
return new TextDecoder().decode(openSealed(channelKey, sealed));
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1530
1617
|
// src/shared/direct-fallback.ts
|
|
1531
1618
|
function createFallbackMeta(file) {
|
|
1532
1619
|
const key = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -2486,7 +2573,7 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2486
2573
|
const grant = await directGrantFor(ctx, input.channel);
|
|
2487
2574
|
const key = channelKeyOf(ctx.identity, grant);
|
|
2488
2575
|
const channelName = channelLabel(ctx, grant);
|
|
2489
|
-
const file = await (deps.openFile ?? openAsBlob)(input.path).catch(() => {
|
|
2576
|
+
const file = await (deps.openFile ?? openAsBlob)(input.path, { type: mimeFor(input.path) }).catch(() => {
|
|
2490
2577
|
throw new ZasError("upload_failed", 400);
|
|
2491
2578
|
});
|
|
2492
2579
|
if (file.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
|
|
@@ -2498,7 +2585,7 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2498
2585
|
const stamp = { device, owner_uid: owner };
|
|
2499
2586
|
const startedAt = now();
|
|
2500
2587
|
report2("offer");
|
|
2501
|
-
const meta =
|
|
2588
|
+
const meta = directMetaOf(file, name);
|
|
2502
2589
|
const { id } = await ctx.client.api("POST", `/direct/${cid}`, {
|
|
2503
2590
|
meta_enc: seal(meta),
|
|
2504
2591
|
key_version: grant.key_version,
|
|
@@ -2560,6 +2647,7 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2560
2647
|
},
|
|
2561
2648
|
onDiag: (d) => {
|
|
2562
2649
|
diag = d;
|
|
2650
|
+
deps.onDiag?.(d);
|
|
2563
2651
|
}
|
|
2564
2652
|
});
|
|
2565
2653
|
let over = false;
|
|
@@ -3144,6 +3232,7 @@ async function receiveDirect(ctx, input, report2, deps = {}) {
|
|
|
3144
3232
|
},
|
|
3145
3233
|
onDiag: (d) => {
|
|
3146
3234
|
diag = d;
|
|
3235
|
+
deps.onDiag?.(d);
|
|
3147
3236
|
},
|
|
3148
3237
|
sink: async (m) => {
|
|
3149
3238
|
sink.open(m);
|
|
@@ -3292,7 +3381,10 @@ var JobRunner = class {
|
|
|
3292
3381
|
const report2 = (phase) => {
|
|
3293
3382
|
if (job.status === "running") job.phase = phase;
|
|
3294
3383
|
};
|
|
3295
|
-
|
|
3384
|
+
const note = (diag) => {
|
|
3385
|
+
job.diag = diag;
|
|
3386
|
+
};
|
|
3387
|
+
this.settled.set(job, work(report2, note).then(
|
|
3296
3388
|
(result) => {
|
|
3297
3389
|
job.status = "done";
|
|
3298
3390
|
job.result = result;
|
|
@@ -3806,7 +3898,7 @@ async function report(client, properties) {
|
|
|
3806
3898
|
|
|
3807
3899
|
// src/server.ts
|
|
3808
3900
|
function agentVersion() {
|
|
3809
|
-
return true ? "0.6.
|
|
3901
|
+
return true ? "0.6.1" : "0.0.0-dev";
|
|
3810
3902
|
}
|
|
3811
3903
|
var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
|
|
3812
3904
|
var PAIR_ANNOUNCE_MS = 15e3;
|
|
@@ -3866,10 +3958,17 @@ function buildServer(profile, deps = {}) {
|
|
|
3866
3958
|
return { isError: true, ...text2(`${err.code}: ${humanSentence(err)}`) };
|
|
3867
3959
|
};
|
|
3868
3960
|
const settled = (job) => {
|
|
3869
|
-
if (job.status === "done")
|
|
3961
|
+
if (job.status === "done") {
|
|
3962
|
+
return job.diag ? text2({ ...job.result, diag: formatDirectDiagLines(job.diag) }) : text2(job.result);
|
|
3963
|
+
}
|
|
3870
3964
|
if (job.status === "failed") {
|
|
3871
3965
|
const e = job.error;
|
|
3872
|
-
|
|
3966
|
+
const answer = failed(e ? new ZasError(e.code, e.status, e.message, e.retryAfterMs, e.serverCode) : new ZasError("internal", 0));
|
|
3967
|
+
if (!job.diag) return answer;
|
|
3968
|
+
const said = answer.content[0].text;
|
|
3969
|
+
return { isError: true, ...text2(`${said}
|
|
3970
|
+
|
|
3971
|
+
${formatDirectDiagLines(job.diag).join("\n")}`) };
|
|
3873
3972
|
}
|
|
3874
3973
|
return text2({ job_id: job.id, status: "running", phase: job.phase });
|
|
3875
3974
|
};
|
|
@@ -4015,9 +4114,10 @@ function buildServer(profile, deps = {}) {
|
|
|
4015
4114
|
"direct",
|
|
4016
4115
|
input.path,
|
|
4017
4116
|
input.channel ?? "",
|
|
4018
|
-
(report2) => sendDirect(c, input, report2, {
|
|
4117
|
+
(report2, note) => sendDirect(c, input, report2, {
|
|
4019
4118
|
...deps.direct ?? {},
|
|
4020
|
-
onFailed: (record) => rememberFailed(job.id, record)
|
|
4119
|
+
onFailed: (record) => rememberFailed(job.id, record),
|
|
4120
|
+
onDiag: note
|
|
4021
4121
|
})
|
|
4022
4122
|
);
|
|
4023
4123
|
return settled(await runner.wait(job));
|
|
@@ -4067,9 +4167,10 @@ function buildServer(profile, deps = {}) {
|
|
|
4067
4167
|
"receive",
|
|
4068
4168
|
input.dest ?? "Directo",
|
|
4069
4169
|
input.channel ?? "",
|
|
4070
|
-
(report2) => receiveDirect(c, input, report2, {
|
|
4170
|
+
(report2, note) => receiveDirect(c, input, report2, {
|
|
4071
4171
|
...deps.receive ?? {},
|
|
4072
|
-
onFailed: (record) => rememberFailedReceive(job.id, record)
|
|
4172
|
+
onFailed: (record) => rememberFailedReceive(job.id, record),
|
|
4173
|
+
onDiag: note
|
|
4073
4174
|
})
|
|
4074
4175
|
);
|
|
4075
4176
|
return settled(await runner.wait(job));
|
package/package.json
CHANGED