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.
Files changed (3) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/dist/cli.js +211 -110
  3. 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.3.0...HEAD
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) return;
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) diag.msConnect = Date.now() - startedAt;
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
- name: opts.file.name ?? opts.name ?? "zas",
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 || frame.meta.mime !== opts.expectedMeta.mime)) {
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 = { name, size: file.size, mime: mimeFor(input.path) };
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
- this.settled.set(job, work(report2).then(
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.0" : "0.0.0-dev";
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") return text2(job.result);
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
- return failed(e ? new ZasError(e.code, e.status, e.message, e.retryAfterMs, e.serverCode) : new ZasError("internal", 0));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zas-agent",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "MCP server that lets a coding agent send files and notes into your Zas channels, and send or receive files live through Directo.",
5
5
  "type": "module",
6
6
  "license": "MIT",