zas-agent 0.6.0 → 0.6.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/CHANGELOG.md +56 -1
- package/dist/cli.js +242 -111
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.2] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The trace records the peer connection state, not only the ICE state.
|
|
15
|
+
ICE `connected` means a candidate pair answered; the peer connection
|
|
16
|
+
reaches `connected` only once DTLS completes. A session that holds an ICE
|
|
17
|
+
path and sends nothing over it used to look the same as one that never
|
|
18
|
+
found a path at all.
|
|
19
|
+
- A failure that had a working ICE pair is now `channel_never_opened`, not
|
|
20
|
+
`connect_timeout`. Same clock, different fault: one is the candidate hunt,
|
|
21
|
+
the other is the handshake above it.
|
|
22
|
+
- The verdict line carries the peer connection state and the data channel's
|
|
23
|
+
own state, so a stalled DTLS handshake and a stalled SCTP association can
|
|
24
|
+
be told apart from one line.
|
|
25
|
+
- The selected candidate pair is read when ICE connects, not only when the
|
|
26
|
+
data channel opens. A run that connected and then went quiet reported
|
|
27
|
+
`pair=none` and looked as if it had never selected one.
|
|
28
|
+
- `onicecandidateerror` is traced, by URL shape and error code only. A
|
|
29
|
+
gather that ends with no relay candidate now says whether the allocation
|
|
30
|
+
was refused or never answered.
|
|
31
|
+
|
|
32
|
+
## [0.6.1] - 2026-09-04
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- A Directo run now answers with the engine's own account of itself, not
|
|
37
|
+
only with one word. `zas_send_direct`, `zas_receive_direct` and `zas_jobs`
|
|
38
|
+
carry the candidate counts by type for both ends, the ICE and gathering
|
|
39
|
+
states, how many TURN URLs were supplied against how many the connection
|
|
40
|
+
kept, the selected pair, the restarts, and a timestamped trace of the
|
|
41
|
+
session: every candidate as it arrived, the state changes, and the verdict.
|
|
42
|
+
Types, transports and timings only — never an address, a port, a host name
|
|
43
|
+
or a credential. A run that fails on a machine you are not sitting at can
|
|
44
|
+
now be diagnosed from the tool result.
|
|
45
|
+
- The thrown error behind a failure is kept as `detail`. The reason stays a
|
|
46
|
+
closed word for grouping; `detail` is the sentence the exception carried,
|
|
47
|
+
which every failure used to discard at the catch.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- The receiver pre-gathers a candidate pool. An agent hands its whole
|
|
52
|
+
candidate set over at once, so the far end could start checking before its
|
|
53
|
+
own relay candidates existed, connect on the first pair it found, and be
|
|
54
|
+
left with nothing when that pair died seconds later.
|
|
55
|
+
- The transport of a candidate is read whichever way the peer wrote the line.
|
|
56
|
+
This package prefixes the SDP attribute, so its candidates used to be
|
|
57
|
+
reported with an unknown transport.
|
|
58
|
+
|
|
10
59
|
## [0.6.0] - 2026-09-03
|
|
11
60
|
|
|
12
61
|
### Added
|
|
@@ -156,7 +205,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
156
205
|
two separate identities that cannot read each other's keys.
|
|
157
206
|
- Install snippets for Claude Code and Codex, printed by `zas-agent pair`.
|
|
158
207
|
|
|
159
|
-
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.
|
|
208
|
+
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.6.2...HEAD
|
|
209
|
+
[0.6.2]: https://github.com/soke1556/zas-agent/compare/v0.6.1...v0.6.2
|
|
210
|
+
[0.6.1]: https://github.com/soke1556/zas-agent/compare/v0.6.0...v0.6.1
|
|
211
|
+
[0.6.0]: https://github.com/soke1556/zas-agent/compare/v0.5.1...v0.6.0
|
|
212
|
+
[0.5.1]: https://github.com/soke1556/zas-agent/compare/v0.5.0...v0.5.1
|
|
213
|
+
[0.5.0]: https://github.com/soke1556/zas-agent/compare/v0.4.0...v0.5.0
|
|
214
|
+
[0.4.0]: https://github.com/soke1556/zas-agent/compare/v0.3.0...v0.4.0
|
|
160
215
|
[0.3.0]: https://github.com/soke1556/zas-agent/compare/v0.2.1...v0.3.0
|
|
161
216
|
[0.2.1]: https://github.com/soke1556/zas-agent/compare/v0.2.0...v0.2.1
|
|
162
217
|
[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,66 @@ 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 urlShape(url) {
|
|
973
|
+
const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase() ?? "other";
|
|
974
|
+
const transport = /[?&]transport=(\w+)/i.exec(url)?.[1]?.toLowerCase();
|
|
975
|
+
return transport ? `${scheme}/${transport}` : scheme;
|
|
976
|
+
}
|
|
977
|
+
function iceShapes(servers) {
|
|
978
|
+
const seen = /* @__PURE__ */ new Map();
|
|
979
|
+
for (const server of servers ?? []) {
|
|
980
|
+
const urls = typeof server.urls === "string" ? [server.urls] : server.urls;
|
|
981
|
+
for (const url of urls ?? []) {
|
|
982
|
+
const shape = urlShape(url);
|
|
983
|
+
seen.set(shape, (seen.get(shape) ?? 0) + 1);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const parts = [...seen].map(([shape, n]) => `${shape}x${n}`);
|
|
987
|
+
return parts.length > 0 ? parts.join(" ") : "none";
|
|
988
|
+
}
|
|
989
|
+
function formatDirectDiagLines(d) {
|
|
990
|
+
const connect = d.msConnect === void 0 ? "-" : `${d.msConnect}ms`;
|
|
991
|
+
const pair = d.pairLocal || d.pairRemote ? `${d.pairLocal || "?"}/${d.pairRemote || "?"}` : "none";
|
|
992
|
+
return [
|
|
993
|
+
`ms=${d.ms} connect=${connect} bytes=${d.bytes} restarts=${d.restarts} pair=${pair}`,
|
|
994
|
+
...d.detail === void 0 ? [] : [`detail ${d.detail}`],
|
|
995
|
+
`local host=${d.localHost} srflx=${d.localSrflx} relay=${d.localRelay} remote host=${d.remoteHost} srflx=${d.remoteSrflx} relay=${d.remoteRelay}`,
|
|
996
|
+
`turn supplied=${d.turnUrlsSupplied} configured=${d.turnUrlsConfigured} ice=${d.iceState} gather=${d.gatherState} remote_desc=${d.hadRemoteDesc}`,
|
|
997
|
+
...d.trace ?? []
|
|
998
|
+
];
|
|
999
|
+
}
|
|
1023
1000
|
function countCand(diag, side, c) {
|
|
1024
|
-
if (!c)
|
|
1001
|
+
if (!c) {
|
|
1002
|
+
traceAdd(diag, `${side} end-of-candidates`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1025
1005
|
const t = candType(c);
|
|
1006
|
+
traceAdd(diag, `${side} ${t} ${candProto(c)}`);
|
|
1026
1007
|
if (t === "host") diag[side === "local" ? "localHost" : "remoteHost"]++;
|
|
1027
1008
|
else if (t === "srflx" || t === "prflx") diag[side === "local" ? "localSrflx" : "remoteSrflx"]++;
|
|
1028
1009
|
else if (t === "relay") diag[side === "local" ? "localRelay" : "remoteRelay"]++;
|
|
@@ -1063,7 +1044,17 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1063
1044
|
const initialIce = ice ?? DIRECT_ICE;
|
|
1064
1045
|
const pc = new RTCPeerConnection({
|
|
1065
1046
|
iceServers: initialIce,
|
|
1066
|
-
iceTransportPolicy
|
|
1047
|
+
iceTransportPolicy,
|
|
1048
|
+
// Gather before there is anything to gather for. A receiver whose peer
|
|
1049
|
+
// is an agent gets the whole remote candidate set at once — the agent
|
|
1050
|
+
// writes it and leaves — so checking starts in the same tick as the local
|
|
1051
|
+
// description, a pair comes up on the first candidate found, and
|
|
1052
|
+
// gathering is reported complete before any relay allocation returns.
|
|
1053
|
+
// Measured 2026-09-04: one host candidate, no relay, gathering done at
|
|
1054
|
+
// 240 ms, and when that pair died five seconds later there was nothing
|
|
1055
|
+
// to fall back to. The pool does the allocations up front, at construction,
|
|
1056
|
+
// so the relay candidates exist before the first check is ever sent.
|
|
1057
|
+
iceCandidatePoolSize: 1
|
|
1067
1058
|
});
|
|
1068
1059
|
let phase = "connecting";
|
|
1069
1060
|
let disconnectTimer;
|
|
@@ -1085,7 +1076,22 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1085
1076
|
turnUrlsSupplied: countTurnUrls(initialIce),
|
|
1086
1077
|
turnUrlsConfigured: 0,
|
|
1087
1078
|
bytes: 0,
|
|
1088
|
-
restarts: 0
|
|
1079
|
+
restarts: 0,
|
|
1080
|
+
trace: []
|
|
1081
|
+
};
|
|
1082
|
+
traceStart.set(diag, startedAt);
|
|
1083
|
+
traceAdd(diag, `ice servers ${iceShapes(initialIce)} policy=${iceTransportPolicy}`);
|
|
1084
|
+
pc.onicegatheringstatechange = () => {
|
|
1085
|
+
traceAdd(diag, `gather ${String(pc.iceGatheringState ?? "")}`);
|
|
1086
|
+
};
|
|
1087
|
+
const connState = () => String(pc.connectionState ?? "");
|
|
1088
|
+
pc.onconnectionstatechange = () => {
|
|
1089
|
+
traceAdd(diag, `conn ${connState()}`);
|
|
1090
|
+
};
|
|
1091
|
+
let channel;
|
|
1092
|
+
pc.onicecandidateerror = (e) => {
|
|
1093
|
+
const err = e;
|
|
1094
|
+
traceAdd(diag, `ice error ${err.url ? urlShape(err.url) : "?"} code=${err.errorCode ?? "?"}`);
|
|
1089
1095
|
};
|
|
1090
1096
|
const cleanup = () => {
|
|
1091
1097
|
clearTimeout(waitTimer);
|
|
@@ -1094,6 +1100,9 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1094
1100
|
clearTimeout(restartTimer);
|
|
1095
1101
|
pc.onicecandidate = null;
|
|
1096
1102
|
pc.oniceconnectionstatechange = null;
|
|
1103
|
+
pc.onicegatheringstatechange = null;
|
|
1104
|
+
pc.onicecandidateerror = null;
|
|
1105
|
+
pc.onconnectionstatechange = null;
|
|
1097
1106
|
pc.close();
|
|
1098
1107
|
};
|
|
1099
1108
|
const setPhase = (p) => {
|
|
@@ -1105,6 +1114,10 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1105
1114
|
diag.gatherState = String(pc.iceGatheringState ?? "");
|
|
1106
1115
|
diag.hadRemoteDesc = hasRemoteDescription(pc);
|
|
1107
1116
|
diag.turnUrlsConfigured = countTurnUrls(pc.getConfiguration().iceServers);
|
|
1117
|
+
traceAdd(
|
|
1118
|
+
diag,
|
|
1119
|
+
`${p}${diag.reason ? ` ${diag.reason}` : ""} ice=${diag.iceState} gather=${diag.gatherState} conn=${connState()} chan=${channel === void 0 ? "-" : String(channel.readyState ?? "")}`
|
|
1120
|
+
);
|
|
1108
1121
|
onDiag?.(diag);
|
|
1109
1122
|
onPhase(p);
|
|
1110
1123
|
cleanup();
|
|
@@ -1112,9 +1125,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1112
1125
|
}
|
|
1113
1126
|
onPhase(p);
|
|
1114
1127
|
};
|
|
1115
|
-
const fail = (reason) => {
|
|
1128
|
+
const fail = (reason, cause) => {
|
|
1116
1129
|
if (phase === "done" || phase === "failed") return;
|
|
1117
1130
|
if (!diag.reason) diag.reason = reason;
|
|
1131
|
+
if (cause !== void 0 && diag.detail === void 0) {
|
|
1132
|
+
diag.detail = errText(cause);
|
|
1133
|
+
traceAdd(diag, `detail ${diag.detail}`);
|
|
1134
|
+
}
|
|
1118
1135
|
setPhase("failed");
|
|
1119
1136
|
};
|
|
1120
1137
|
const waitTimer = setTimeout(() => fail("peer_silent"), DIRECT_SIGNAL_WAIT_MS);
|
|
@@ -1124,7 +1141,10 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1124
1141
|
if (heard) return;
|
|
1125
1142
|
heard = true;
|
|
1126
1143
|
clearTimeout(waitTimer);
|
|
1127
|
-
connectTimer = setTimeout(() =>
|
|
1144
|
+
connectTimer = setTimeout(() => {
|
|
1145
|
+
const iceUp = pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed";
|
|
1146
|
+
fail(iceUp ? "channel_never_opened" : "connect_timeout");
|
|
1147
|
+
}, DIRECT_CONNECT_TIMEOUT_MS);
|
|
1128
1148
|
};
|
|
1129
1149
|
let onStall;
|
|
1130
1150
|
let stallWindowMs = DIRECT_CONNECT_TIMEOUT_MS;
|
|
@@ -1138,12 +1158,14 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1138
1158
|
}
|
|
1139
1159
|
stallState = "restarting";
|
|
1140
1160
|
diag.restarts++;
|
|
1161
|
+
traceAdd(diag, "ice restart");
|
|
1141
1162
|
clearTimeout(restartTimer);
|
|
1142
1163
|
restartTimer = setTimeout(() => fail("disconnected"), stallWindowMs);
|
|
1143
1164
|
onStall();
|
|
1144
1165
|
};
|
|
1145
1166
|
pc.oniceconnectionstatechange = () => {
|
|
1146
1167
|
const s = pc.iceConnectionState;
|
|
1168
|
+
traceAdd(diag, `ice ${String(s ?? "")}`);
|
|
1147
1169
|
if (s === "failed") {
|
|
1148
1170
|
if (diag.msConnect !== void 0 && onStall) stalled();
|
|
1149
1171
|
else fail("ice_failed");
|
|
@@ -1156,6 +1178,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1156
1178
|
clearTimeout(disconnectTimer);
|
|
1157
1179
|
clearTimeout(restartTimer);
|
|
1158
1180
|
stallState = "idle";
|
|
1181
|
+
void pc.getStats().then((stats) => {
|
|
1182
|
+
const pair = pairFromStats(stats);
|
|
1183
|
+
if (!pair || diag.pairLocal) return;
|
|
1184
|
+
diag.pairLocal = pair.local;
|
|
1185
|
+
diag.pairRemote = pair.remote;
|
|
1186
|
+
traceAdd(diag, `pair ${pair.local}/${pair.remote}`);
|
|
1187
|
+
}).catch(() => void 0);
|
|
1159
1188
|
}
|
|
1160
1189
|
};
|
|
1161
1190
|
return {
|
|
@@ -1168,15 +1197,20 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1168
1197
|
connected: () => {
|
|
1169
1198
|
clearTimeout(waitTimer);
|
|
1170
1199
|
clearTimeout(connectTimer);
|
|
1171
|
-
if (diag.msConnect === void 0)
|
|
1200
|
+
if (diag.msConnect === void 0) {
|
|
1201
|
+
diag.msConnect = Date.now() - startedAt;
|
|
1202
|
+
traceAdd(diag, "channel open");
|
|
1203
|
+
}
|
|
1172
1204
|
},
|
|
1173
1205
|
isTerminal: () => phase === "done" || phase === "failed",
|
|
1206
|
+
setChannel: (dc) => void (channel = dc),
|
|
1174
1207
|
setStall: (fn, windowMs) => {
|
|
1175
1208
|
onStall = fn;
|
|
1176
1209
|
stallWindowMs = windowMs;
|
|
1177
1210
|
},
|
|
1178
1211
|
setIceServers: (iceServers) => {
|
|
1179
1212
|
diag.turnUrlsSupplied = countTurnUrls(iceServers);
|
|
1213
|
+
traceAdd(diag, `ice servers replaced ${iceShapes(iceServers)}`);
|
|
1180
1214
|
pc.setConfiguration({ iceServers });
|
|
1181
1215
|
}
|
|
1182
1216
|
};
|
|
@@ -1258,9 +1292,10 @@ function startSender(opts) {
|
|
|
1258
1292
|
const offer = await pc.createOffer({ iceRestart: true });
|
|
1259
1293
|
await pc.setLocalDescription(offer);
|
|
1260
1294
|
await opts.send({ kind: "offer", sdp: offer.sdp, protocol: 2, generation });
|
|
1261
|
-
})().catch(() => s.fail("signaling"));
|
|
1295
|
+
})().catch((e) => s.fail("signaling", e));
|
|
1262
1296
|
}, DIRECT_CONNECT_TIMEOUT_MS);
|
|
1263
1297
|
const dc = pc.createDataChannel("zas-direct", { ordered: true });
|
|
1298
|
+
s.setChannel(dc);
|
|
1264
1299
|
dc.binaryType = "arraybuffer";
|
|
1265
1300
|
dc.bufferedAmountLowThreshold = DIRECT_BUFFERED_HIGH / 8;
|
|
1266
1301
|
const waitLow = () => new Promise((resolve2) => {
|
|
@@ -1285,9 +1320,7 @@ function startSender(opts) {
|
|
|
1285
1320
|
};
|
|
1286
1321
|
const pump = async () => {
|
|
1287
1322
|
const meta = {
|
|
1288
|
-
|
|
1289
|
-
size: opts.file.size,
|
|
1290
|
-
mime: opts.file.type || "application/octet-stream",
|
|
1323
|
+
...directMetaOf(opts.file, opts.name),
|
|
1291
1324
|
...opts.label !== void 0 ? { label: opts.label } : {}
|
|
1292
1325
|
};
|
|
1293
1326
|
dc.send(encodeMeta(meta));
|
|
@@ -1326,7 +1359,7 @@ function startSender(opts) {
|
|
|
1326
1359
|
s.diag.pairRemote = pair.remote;
|
|
1327
1360
|
opts.onPath?.(pathOf(pair));
|
|
1328
1361
|
}).catch(() => void 0);
|
|
1329
|
-
void pump().catch(() => s.fail("transfer"));
|
|
1362
|
+
void pump().catch((e) => s.fail("transfer", e));
|
|
1330
1363
|
};
|
|
1331
1364
|
dc.onmessage = (e) => {
|
|
1332
1365
|
try {
|
|
@@ -1352,7 +1385,7 @@ function startSender(opts) {
|
|
|
1352
1385
|
plumbing.setLocalGeneration(generation);
|
|
1353
1386
|
plumbing.expectRemoteGeneration(generation);
|
|
1354
1387
|
await opts.send({ kind: "offer", sdp: offer.sdp, protocol: 2, generation });
|
|
1355
|
-
})().catch(() => s.fail("signaling"));
|
|
1388
|
+
})().catch((e) => s.fail("signaling", e));
|
|
1356
1389
|
return {
|
|
1357
1390
|
accept: (msg) => {
|
|
1358
1391
|
if (s.isTerminal()) return;
|
|
@@ -1362,7 +1395,7 @@ function startSender(opts) {
|
|
|
1362
1395
|
if (msg.generation !== void 0 && answerGeneration !== generation) return;
|
|
1363
1396
|
peerProtocol = msg.protocol === 2 ? 2 : 1;
|
|
1364
1397
|
plumbing.expectRemoteGeneration(answerGeneration);
|
|
1365
|
-
void pc.setRemoteDescription({ type: "answer", sdp: msg.sdp }).then(() => plumbing.remoteDescriptionSet(answerGeneration)).catch(() => s.fail("signaling"));
|
|
1398
|
+
void pc.setRemoteDescription({ type: "answer", sdp: msg.sdp }).then(() => plumbing.remoteDescriptionSet(answerGeneration)).catch((e) => s.fail("signaling", e));
|
|
1366
1399
|
}
|
|
1367
1400
|
if (msg.kind === "ice") plumbing.applyIce(msg.candidate, msg.generation);
|
|
1368
1401
|
},
|
|
@@ -1411,6 +1444,7 @@ function startReceiver(opts) {
|
|
|
1411
1444
|
let finished = false;
|
|
1412
1445
|
pc.ondatachannel = (e) => {
|
|
1413
1446
|
dc = e.channel;
|
|
1447
|
+
s.setChannel(dc);
|
|
1414
1448
|
dc.binaryType = "arraybuffer";
|
|
1415
1449
|
dc.onopen = () => {
|
|
1416
1450
|
s.connected();
|
|
@@ -1435,7 +1469,7 @@ function startReceiver(opts) {
|
|
|
1435
1469
|
s.fail(frame.meta.size > DIRECT_FILE_MAX_BYTES ? "too_big" : "protocol");
|
|
1436
1470
|
return;
|
|
1437
1471
|
}
|
|
1438
|
-
if (opts.expectedMeta && (frame.meta.name !== opts.expectedMeta.name || frame.meta.size !== opts.expectedMeta.size
|
|
1472
|
+
if (opts.expectedMeta && (frame.meta.name !== opts.expectedMeta.name || frame.meta.size !== opts.expectedMeta.size)) {
|
|
1439
1473
|
dc?.send(ABORT_FRAME);
|
|
1440
1474
|
s.fail("protocol");
|
|
1441
1475
|
return;
|
|
@@ -1447,7 +1481,7 @@ function startReceiver(opts) {
|
|
|
1447
1481
|
sinkRef = sink;
|
|
1448
1482
|
return sink;
|
|
1449
1483
|
});
|
|
1450
|
-
pipeline.catch(() => s.fail("sink"));
|
|
1484
|
+
pipeline.catch((e3) => s.fail("sink", e3));
|
|
1451
1485
|
} else if (frame.type === "chunk") {
|
|
1452
1486
|
if (!pipeline || finished) throw new Error("chunk_before_meta");
|
|
1453
1487
|
queued += frame.payload.length;
|
|
@@ -1465,7 +1499,7 @@ function startReceiver(opts) {
|
|
|
1465
1499
|
}
|
|
1466
1500
|
return sink;
|
|
1467
1501
|
});
|
|
1468
|
-
pipeline.catch(() => s.fail("sink"));
|
|
1502
|
+
pipeline.catch((e3) => s.fail("sink", e3));
|
|
1469
1503
|
} else if (frame.type === "done") {
|
|
1470
1504
|
if (!pipeline || finished) throw new Error("done_before_meta");
|
|
1471
1505
|
finished = true;
|
|
@@ -1476,7 +1510,7 @@ function startReceiver(opts) {
|
|
|
1476
1510
|
await sink.close();
|
|
1477
1511
|
dc?.send(DONE_FRAME);
|
|
1478
1512
|
s.setPhase("done");
|
|
1479
|
-
}).catch(() => s.fail("integrity"));
|
|
1513
|
+
}).catch((e3) => s.fail("integrity", e3));
|
|
1480
1514
|
} else if (frame.type === "abort") {
|
|
1481
1515
|
s.fail("peer_abort");
|
|
1482
1516
|
}
|
|
@@ -1510,7 +1544,7 @@ function startReceiver(opts) {
|
|
|
1510
1544
|
const answer = await pc.createAnswer();
|
|
1511
1545
|
await pc.setLocalDescription(answer);
|
|
1512
1546
|
await opts.send({ kind: "answer", sdp: answer.sdp, protocol: 2, generation });
|
|
1513
|
-
})().catch(() => s.fail("signaling"));
|
|
1547
|
+
})().catch((e) => s.fail("signaling", e));
|
|
1514
1548
|
}
|
|
1515
1549
|
if (msg.kind === "ice") plumbing.applyIce(msg.candidate, msg.generation);
|
|
1516
1550
|
},
|
|
@@ -1527,6 +1561,89 @@ function startReceiver(opts) {
|
|
|
1527
1561
|
};
|
|
1528
1562
|
}
|
|
1529
1563
|
|
|
1564
|
+
// src/direct.ts
|
|
1565
|
+
import { randomBytes } from "node:crypto";
|
|
1566
|
+
import { openAsBlob } from "node:fs";
|
|
1567
|
+
import { basename as basename2 } from "node:path";
|
|
1568
|
+
|
|
1569
|
+
// src/shared/manifest.ts
|
|
1570
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
1571
|
+
|
|
1572
|
+
// src/shared/constants.ts
|
|
1573
|
+
var CHUNKER_VERSION = "chunker_v1";
|
|
1574
|
+
var CHUNK_MIN = 1 * 1024 * 1024;
|
|
1575
|
+
var CHUNK_AVG_BITS = 22;
|
|
1576
|
+
var CHUNK_MAX = 8 * 1024 * 1024;
|
|
1577
|
+
var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
|
|
1578
|
+
var GEAR_SEED = "zas-gear-v1:";
|
|
1579
|
+
var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
|
|
1580
|
+
var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
|
|
1581
|
+
var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
|
|
1582
|
+
var MANIFEST_VERSION = 1;
|
|
1583
|
+
var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
|
|
1584
|
+
var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
|
|
1585
|
+
var SAFE_STORED_FILE_LIMITS = {
|
|
1586
|
+
anon: 30 * 1024 * 1024,
|
|
1587
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
1588
|
+
pro: 200 * 1024 * 1024,
|
|
1589
|
+
max: 200 * 1024 * 1024
|
|
1590
|
+
};
|
|
1591
|
+
var STORED_FILE_LIMITS_V2 = {
|
|
1592
|
+
anon: 30 * 1024 * 1024,
|
|
1593
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
1594
|
+
pro: 5 * 1024 * 1024 * 1024,
|
|
1595
|
+
max: 20 * 1024 * 1024 * 1024
|
|
1596
|
+
};
|
|
1597
|
+
var STORAGE_LIMITS = {
|
|
1598
|
+
anon: 30 * 1024 * 1024,
|
|
1599
|
+
free: 100 * 1024 * 1024,
|
|
1600
|
+
pro: null,
|
|
1601
|
+
max: null
|
|
1602
|
+
};
|
|
1603
|
+
var THUMBNAIL_MAX_EDGE = 640;
|
|
1604
|
+
var THUMBNAIL_MAX_BYTES = 14e4;
|
|
1605
|
+
|
|
1606
|
+
// src/shared/manifest.ts
|
|
1607
|
+
function newManifest(partial) {
|
|
1608
|
+
return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
|
|
1609
|
+
}
|
|
1610
|
+
var ENVELOPE_MAGIC = 90;
|
|
1611
|
+
var KEY_VERSION_LEGACY = 1;
|
|
1612
|
+
function envelopeVersion(sealed) {
|
|
1613
|
+
if (sealed.length > 26 && sealed[0] === ENVELOPE_MAGIC && sealed[1] >= 1) return sealed[1];
|
|
1614
|
+
return KEY_VERSION_LEGACY;
|
|
1615
|
+
}
|
|
1616
|
+
function sealBytes(key, version, plain) {
|
|
1617
|
+
if (version < 1 || version > 255) throw new Error("key version out of range");
|
|
1618
|
+
const nonce = new Uint8Array(24);
|
|
1619
|
+
crypto.getRandomValues(nonce);
|
|
1620
|
+
const ct = xchacha20poly1305(key, nonce).encrypt(plain);
|
|
1621
|
+
return concatBytes(new Uint8Array([ENVELOPE_MAGIC, version]), nonce, ct);
|
|
1622
|
+
}
|
|
1623
|
+
function openSealed(key, sealed) {
|
|
1624
|
+
const versioned = envelopeVersion(sealed) !== KEY_VERSION_LEGACY || sealed[0] === ENVELOPE_MAGIC;
|
|
1625
|
+
if (versioned && sealed.length > 26) {
|
|
1626
|
+
try {
|
|
1627
|
+
return xchacha20poly1305(key, sealed.slice(2, 26)).decrypt(sealed.slice(26));
|
|
1628
|
+
} catch {
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return xchacha20poly1305(key, sealed.slice(0, 24)).decrypt(sealed.slice(24));
|
|
1632
|
+
}
|
|
1633
|
+
var sealRaw = sealBytes;
|
|
1634
|
+
var openRaw = openSealed;
|
|
1635
|
+
function sealManifest(channelKey, manifest, version = KEY_VERSION_LEGACY) {
|
|
1636
|
+
return sealBytes(channelKey, version, new TextEncoder().encode(JSON.stringify(manifest)));
|
|
1637
|
+
}
|
|
1638
|
+
function openManifest(channelKey, sealed) {
|
|
1639
|
+
const m = JSON.parse(new TextDecoder().decode(openSealed(channelKey, sealed)));
|
|
1640
|
+
if (!Array.isArray(m.chunks)) m.chunks = [];
|
|
1641
|
+
return m;
|
|
1642
|
+
}
|
|
1643
|
+
function decryptChannelName(channelKey, sealed) {
|
|
1644
|
+
return new TextDecoder().decode(openSealed(channelKey, sealed));
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1530
1647
|
// src/shared/direct-fallback.ts
|
|
1531
1648
|
function createFallbackMeta(file) {
|
|
1532
1649
|
const key = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -2486,7 +2603,7 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2486
2603
|
const grant = await directGrantFor(ctx, input.channel);
|
|
2487
2604
|
const key = channelKeyOf(ctx.identity, grant);
|
|
2488
2605
|
const channelName = channelLabel(ctx, grant);
|
|
2489
|
-
const file = await (deps.openFile ?? openAsBlob)(input.path).catch(() => {
|
|
2606
|
+
const file = await (deps.openFile ?? openAsBlob)(input.path, { type: mimeFor(input.path) }).catch(() => {
|
|
2490
2607
|
throw new ZasError("upload_failed", 400);
|
|
2491
2608
|
});
|
|
2492
2609
|
if (file.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
|
|
@@ -2498,7 +2615,7 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2498
2615
|
const stamp = { device, owner_uid: owner };
|
|
2499
2616
|
const startedAt = now();
|
|
2500
2617
|
report2("offer");
|
|
2501
|
-
const meta =
|
|
2618
|
+
const meta = directMetaOf(file, name);
|
|
2502
2619
|
const { id } = await ctx.client.api("POST", `/direct/${cid}`, {
|
|
2503
2620
|
meta_enc: seal(meta),
|
|
2504
2621
|
key_version: grant.key_version,
|
|
@@ -2560,6 +2677,7 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2560
2677
|
},
|
|
2561
2678
|
onDiag: (d) => {
|
|
2562
2679
|
diag = d;
|
|
2680
|
+
deps.onDiag?.(d);
|
|
2563
2681
|
}
|
|
2564
2682
|
});
|
|
2565
2683
|
let over = false;
|
|
@@ -3144,6 +3262,7 @@ async function receiveDirect(ctx, input, report2, deps = {}) {
|
|
|
3144
3262
|
},
|
|
3145
3263
|
onDiag: (d) => {
|
|
3146
3264
|
diag = d;
|
|
3265
|
+
deps.onDiag?.(d);
|
|
3147
3266
|
},
|
|
3148
3267
|
sink: async (m) => {
|
|
3149
3268
|
sink.open(m);
|
|
@@ -3292,7 +3411,10 @@ var JobRunner = class {
|
|
|
3292
3411
|
const report2 = (phase) => {
|
|
3293
3412
|
if (job.status === "running") job.phase = phase;
|
|
3294
3413
|
};
|
|
3295
|
-
|
|
3414
|
+
const note = (diag) => {
|
|
3415
|
+
job.diag = diag;
|
|
3416
|
+
};
|
|
3417
|
+
this.settled.set(job, work(report2, note).then(
|
|
3296
3418
|
(result) => {
|
|
3297
3419
|
job.status = "done";
|
|
3298
3420
|
job.result = result;
|
|
@@ -3806,7 +3928,7 @@ async function report(client, properties) {
|
|
|
3806
3928
|
|
|
3807
3929
|
// src/server.ts
|
|
3808
3930
|
function agentVersion() {
|
|
3809
|
-
return true ? "0.6.
|
|
3931
|
+
return true ? "0.6.2" : "0.0.0-dev";
|
|
3810
3932
|
}
|
|
3811
3933
|
var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
|
|
3812
3934
|
var PAIR_ANNOUNCE_MS = 15e3;
|
|
@@ -3866,10 +3988,17 @@ function buildServer(profile, deps = {}) {
|
|
|
3866
3988
|
return { isError: true, ...text2(`${err.code}: ${humanSentence(err)}`) };
|
|
3867
3989
|
};
|
|
3868
3990
|
const settled = (job) => {
|
|
3869
|
-
if (job.status === "done")
|
|
3991
|
+
if (job.status === "done") {
|
|
3992
|
+
return job.diag ? text2({ ...job.result, diag: formatDirectDiagLines(job.diag) }) : text2(job.result);
|
|
3993
|
+
}
|
|
3870
3994
|
if (job.status === "failed") {
|
|
3871
3995
|
const e = job.error;
|
|
3872
|
-
|
|
3996
|
+
const answer = failed(e ? new ZasError(e.code, e.status, e.message, e.retryAfterMs, e.serverCode) : new ZasError("internal", 0));
|
|
3997
|
+
if (!job.diag) return answer;
|
|
3998
|
+
const said = answer.content[0].text;
|
|
3999
|
+
return { isError: true, ...text2(`${said}
|
|
4000
|
+
|
|
4001
|
+
${formatDirectDiagLines(job.diag).join("\n")}`) };
|
|
3873
4002
|
}
|
|
3874
4003
|
return text2({ job_id: job.id, status: "running", phase: job.phase });
|
|
3875
4004
|
};
|
|
@@ -4015,9 +4144,10 @@ function buildServer(profile, deps = {}) {
|
|
|
4015
4144
|
"direct",
|
|
4016
4145
|
input.path,
|
|
4017
4146
|
input.channel ?? "",
|
|
4018
|
-
(report2) => sendDirect(c, input, report2, {
|
|
4147
|
+
(report2, note) => sendDirect(c, input, report2, {
|
|
4019
4148
|
...deps.direct ?? {},
|
|
4020
|
-
onFailed: (record) => rememberFailed(job.id, record)
|
|
4149
|
+
onFailed: (record) => rememberFailed(job.id, record),
|
|
4150
|
+
onDiag: note
|
|
4021
4151
|
})
|
|
4022
4152
|
);
|
|
4023
4153
|
return settled(await runner.wait(job));
|
|
@@ -4067,9 +4197,10 @@ function buildServer(profile, deps = {}) {
|
|
|
4067
4197
|
"receive",
|
|
4068
4198
|
input.dest ?? "Directo",
|
|
4069
4199
|
input.channel ?? "",
|
|
4070
|
-
(report2) => receiveDirect(c, input, report2, {
|
|
4200
|
+
(report2, note) => receiveDirect(c, input, report2, {
|
|
4071
4201
|
...deps.receive ?? {},
|
|
4072
|
-
onFailed: (record) => rememberFailedReceive(job.id, record)
|
|
4202
|
+
onFailed: (record) => rememberFailedReceive(job.id, record),
|
|
4203
|
+
onDiag: note
|
|
4073
4204
|
})
|
|
4074
4205
|
);
|
|
4075
4206
|
return settled(await runner.wait(job));
|
package/package.json
CHANGED