zas-agent 0.5.1 → 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 +50 -1
- package/README.md +42 -3
- package/dist/cli.js +767 -141
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -211,6 +211,7 @@ var IDENTITY_FILE = "identity.json";
|
|
|
211
211
|
var PENDING_FILE = "pending.json";
|
|
212
212
|
var GRANTS_FILE = "grants.json";
|
|
213
213
|
var FINGERPRINTS_FILE = "fingerprints.json";
|
|
214
|
+
var SETTINGS_FILE = "settings.json";
|
|
214
215
|
function agentHome() {
|
|
215
216
|
return process.env.ZAS_AGENT_HOME || join(homedir(), ".zas", "agent");
|
|
216
217
|
}
|
|
@@ -300,6 +301,13 @@ function loadFingerprints(profile) {
|
|
|
300
301
|
function saveFingerprints(profile, cache) {
|
|
301
302
|
writePrivate(profileDir(profile), FINGERPRINTS_FILE, cache);
|
|
302
303
|
}
|
|
304
|
+
function loadSettings() {
|
|
305
|
+
const stored = readCache(agentHome(), SETTINGS_FILE);
|
|
306
|
+
return isPlainObject(stored) ? stored : null;
|
|
307
|
+
}
|
|
308
|
+
function saveSettings(settings) {
|
|
309
|
+
writePrivate(agentHome(), SETTINGS_FILE, settings);
|
|
310
|
+
}
|
|
303
311
|
function defaultEndpoints() {
|
|
304
312
|
return {
|
|
305
313
|
api_base: process.env.ZAS_API_BASE || "https://zas.red/api",
|
|
@@ -822,89 +830,6 @@ async function runPair(opts) {
|
|
|
822
830
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
823
831
|
import { z } from "zod";
|
|
824
832
|
|
|
825
|
-
// src/direct.ts
|
|
826
|
-
import { randomBytes } from "node:crypto";
|
|
827
|
-
import { openAsBlob } from "node:fs";
|
|
828
|
-
import { basename as basename2 } from "node:path";
|
|
829
|
-
|
|
830
|
-
// src/shared/manifest.ts
|
|
831
|
-
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
832
|
-
|
|
833
|
-
// src/shared/constants.ts
|
|
834
|
-
var CHUNKER_VERSION = "chunker_v1";
|
|
835
|
-
var CHUNK_MIN = 1 * 1024 * 1024;
|
|
836
|
-
var CHUNK_AVG_BITS = 22;
|
|
837
|
-
var CHUNK_MAX = 8 * 1024 * 1024;
|
|
838
|
-
var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
|
|
839
|
-
var GEAR_SEED = "zas-gear-v1:";
|
|
840
|
-
var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
|
|
841
|
-
var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
|
|
842
|
-
var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
|
|
843
|
-
var MANIFEST_VERSION = 1;
|
|
844
|
-
var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
|
|
845
|
-
var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
|
|
846
|
-
var SAFE_STORED_FILE_LIMITS = {
|
|
847
|
-
anon: 30 * 1024 * 1024,
|
|
848
|
-
free: FREE_RELAY_MAX_BYTES,
|
|
849
|
-
pro: 200 * 1024 * 1024,
|
|
850
|
-
max: 200 * 1024 * 1024
|
|
851
|
-
};
|
|
852
|
-
var STORED_FILE_LIMITS_V2 = {
|
|
853
|
-
anon: 30 * 1024 * 1024,
|
|
854
|
-
free: FREE_RELAY_MAX_BYTES,
|
|
855
|
-
pro: 5 * 1024 * 1024 * 1024,
|
|
856
|
-
max: 20 * 1024 * 1024 * 1024
|
|
857
|
-
};
|
|
858
|
-
var STORAGE_LIMITS = {
|
|
859
|
-
anon: 30 * 1024 * 1024,
|
|
860
|
-
free: 100 * 1024 * 1024,
|
|
861
|
-
pro: null,
|
|
862
|
-
max: null
|
|
863
|
-
};
|
|
864
|
-
var THUMBNAIL_MAX_EDGE = 640;
|
|
865
|
-
var THUMBNAIL_MAX_BYTES = 14e4;
|
|
866
|
-
|
|
867
|
-
// src/shared/manifest.ts
|
|
868
|
-
function newManifest(partial) {
|
|
869
|
-
return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
|
|
870
|
-
}
|
|
871
|
-
var ENVELOPE_MAGIC = 90;
|
|
872
|
-
var KEY_VERSION_LEGACY = 1;
|
|
873
|
-
function envelopeVersion(sealed) {
|
|
874
|
-
if (sealed.length > 26 && sealed[0] === ENVELOPE_MAGIC && sealed[1] >= 1) return sealed[1];
|
|
875
|
-
return KEY_VERSION_LEGACY;
|
|
876
|
-
}
|
|
877
|
-
function sealBytes(key, version, plain) {
|
|
878
|
-
if (version < 1 || version > 255) throw new Error("key version out of range");
|
|
879
|
-
const nonce = new Uint8Array(24);
|
|
880
|
-
crypto.getRandomValues(nonce);
|
|
881
|
-
const ct = xchacha20poly1305(key, nonce).encrypt(plain);
|
|
882
|
-
return concatBytes(new Uint8Array([ENVELOPE_MAGIC, version]), nonce, ct);
|
|
883
|
-
}
|
|
884
|
-
function openSealed(key, sealed) {
|
|
885
|
-
const versioned = envelopeVersion(sealed) !== KEY_VERSION_LEGACY || sealed[0] === ENVELOPE_MAGIC;
|
|
886
|
-
if (versioned && sealed.length > 26) {
|
|
887
|
-
try {
|
|
888
|
-
return xchacha20poly1305(key, sealed.slice(2, 26)).decrypt(sealed.slice(26));
|
|
889
|
-
} catch {
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
return xchacha20poly1305(key, sealed.slice(0, 24)).decrypt(sealed.slice(24));
|
|
893
|
-
}
|
|
894
|
-
var sealRaw = sealBytes;
|
|
895
|
-
var openRaw = openSealed;
|
|
896
|
-
function sealManifest(channelKey, manifest, version = KEY_VERSION_LEGACY) {
|
|
897
|
-
return sealBytes(channelKey, version, new TextEncoder().encode(JSON.stringify(manifest)));
|
|
898
|
-
}
|
|
899
|
-
function openManifest(channelKey, sealed) {
|
|
900
|
-
const m = JSON.parse(new TextDecoder().decode(openSealed(channelKey, sealed)));
|
|
901
|
-
if (!Array.isArray(m.chunks)) m.chunks = [];
|
|
902
|
-
return m;
|
|
903
|
-
}
|
|
904
|
-
function decryptChannelName(channelKey, sealed) {
|
|
905
|
-
return new TextDecoder().decode(openSealed(channelKey, sealed));
|
|
906
|
-
}
|
|
907
|
-
|
|
908
833
|
// src/shared/direct.ts
|
|
909
834
|
var DIRECT_OFFER_TTL_MS = 10 * 60 * 1e3;
|
|
910
835
|
var DIRECT_CLAIMED_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -940,6 +865,13 @@ var FRAME_CHUNK = 2;
|
|
|
940
865
|
var FRAME_DONE = 3;
|
|
941
866
|
var FRAME_ABORT = 4;
|
|
942
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
|
+
}
|
|
943
875
|
function encodeMeta(meta) {
|
|
944
876
|
const body = new TextEncoder().encode(JSON.stringify(meta));
|
|
945
877
|
const frame = new Uint8Array(1 + body.length);
|
|
@@ -1012,9 +944,63 @@ function candType(candidate) {
|
|
|
1012
944
|
const m = /\styp\s+(\S+)/.exec(candidate?.candidate ?? "");
|
|
1013
945
|
return m ? m[1] : "other";
|
|
1014
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
|
+
}
|
|
1015
997
|
function countCand(diag, side, c) {
|
|
1016
|
-
if (!c)
|
|
998
|
+
if (!c) {
|
|
999
|
+
traceAdd(diag, `${side} end-of-candidates`);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1017
1002
|
const t = candType(c);
|
|
1003
|
+
traceAdd(diag, `${side} ${t} ${candProto(c)}`);
|
|
1018
1004
|
if (t === "host") diag[side === "local" ? "localHost" : "remoteHost"]++;
|
|
1019
1005
|
else if (t === "srflx" || t === "prflx") diag[side === "local" ? "localSrflx" : "remoteSrflx"]++;
|
|
1020
1006
|
else if (t === "relay") diag[side === "local" ? "localRelay" : "remoteRelay"]++;
|
|
@@ -1055,7 +1041,17 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1055
1041
|
const initialIce = ice ?? DIRECT_ICE;
|
|
1056
1042
|
const pc = new RTCPeerConnection({
|
|
1057
1043
|
iceServers: initialIce,
|
|
1058
|
-
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
|
|
1059
1055
|
});
|
|
1060
1056
|
let phase = "connecting";
|
|
1061
1057
|
let disconnectTimer;
|
|
@@ -1077,7 +1073,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1077
1073
|
turnUrlsSupplied: countTurnUrls(initialIce),
|
|
1078
1074
|
turnUrlsConfigured: 0,
|
|
1079
1075
|
bytes: 0,
|
|
1080
|
-
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 ?? "")}`);
|
|
1081
1083
|
};
|
|
1082
1084
|
const cleanup = () => {
|
|
1083
1085
|
clearTimeout(waitTimer);
|
|
@@ -1086,6 +1088,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1086
1088
|
clearTimeout(restartTimer);
|
|
1087
1089
|
pc.onicecandidate = null;
|
|
1088
1090
|
pc.oniceconnectionstatechange = null;
|
|
1091
|
+
pc.onicegatheringstatechange = null;
|
|
1089
1092
|
pc.close();
|
|
1090
1093
|
};
|
|
1091
1094
|
const setPhase = (p) => {
|
|
@@ -1097,6 +1100,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1097
1100
|
diag.gatherState = String(pc.iceGatheringState ?? "");
|
|
1098
1101
|
diag.hadRemoteDesc = hasRemoteDescription(pc);
|
|
1099
1102
|
diag.turnUrlsConfigured = countTurnUrls(pc.getConfiguration().iceServers);
|
|
1103
|
+
traceAdd(diag, `${p}${diag.reason ? ` ${diag.reason}` : ""} ice=${diag.iceState} gather=${diag.gatherState}`);
|
|
1100
1104
|
onDiag?.(diag);
|
|
1101
1105
|
onPhase(p);
|
|
1102
1106
|
cleanup();
|
|
@@ -1104,9 +1108,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1104
1108
|
}
|
|
1105
1109
|
onPhase(p);
|
|
1106
1110
|
};
|
|
1107
|
-
const fail = (reason) => {
|
|
1111
|
+
const fail = (reason, cause) => {
|
|
1108
1112
|
if (phase === "done" || phase === "failed") return;
|
|
1109
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
|
+
}
|
|
1110
1118
|
setPhase("failed");
|
|
1111
1119
|
};
|
|
1112
1120
|
const waitTimer = setTimeout(() => fail("peer_silent"), DIRECT_SIGNAL_WAIT_MS);
|
|
@@ -1130,12 +1138,14 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1130
1138
|
}
|
|
1131
1139
|
stallState = "restarting";
|
|
1132
1140
|
diag.restarts++;
|
|
1141
|
+
traceAdd(diag, "ice restart");
|
|
1133
1142
|
clearTimeout(restartTimer);
|
|
1134
1143
|
restartTimer = setTimeout(() => fail("disconnected"), stallWindowMs);
|
|
1135
1144
|
onStall();
|
|
1136
1145
|
};
|
|
1137
1146
|
pc.oniceconnectionstatechange = () => {
|
|
1138
1147
|
const s = pc.iceConnectionState;
|
|
1148
|
+
traceAdd(diag, `ice ${String(s ?? "")}`);
|
|
1139
1149
|
if (s === "failed") {
|
|
1140
1150
|
if (diag.msConnect !== void 0 && onStall) stalled();
|
|
1141
1151
|
else fail("ice_failed");
|
|
@@ -1160,7 +1170,10 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1160
1170
|
connected: () => {
|
|
1161
1171
|
clearTimeout(waitTimer);
|
|
1162
1172
|
clearTimeout(connectTimer);
|
|
1163
|
-
if (diag.msConnect === void 0)
|
|
1173
|
+
if (diag.msConnect === void 0) {
|
|
1174
|
+
diag.msConnect = Date.now() - startedAt;
|
|
1175
|
+
traceAdd(diag, "channel open");
|
|
1176
|
+
}
|
|
1164
1177
|
},
|
|
1165
1178
|
isTerminal: () => phase === "done" || phase === "failed",
|
|
1166
1179
|
setStall: (fn, windowMs) => {
|
|
@@ -1169,6 +1182,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1169
1182
|
},
|
|
1170
1183
|
setIceServers: (iceServers) => {
|
|
1171
1184
|
diag.turnUrlsSupplied = countTurnUrls(iceServers);
|
|
1185
|
+
traceAdd(diag, `ice servers replaced ${iceShapes(iceServers)}`);
|
|
1172
1186
|
pc.setConfiguration({ iceServers });
|
|
1173
1187
|
}
|
|
1174
1188
|
};
|
|
@@ -1250,7 +1264,7 @@ function startSender(opts) {
|
|
|
1250
1264
|
const offer = await pc.createOffer({ iceRestart: true });
|
|
1251
1265
|
await pc.setLocalDescription(offer);
|
|
1252
1266
|
await opts.send({ kind: "offer", sdp: offer.sdp, protocol: 2, generation });
|
|
1253
|
-
})().catch(() => s.fail("signaling"));
|
|
1267
|
+
})().catch((e) => s.fail("signaling", e));
|
|
1254
1268
|
}, DIRECT_CONNECT_TIMEOUT_MS);
|
|
1255
1269
|
const dc = pc.createDataChannel("zas-direct", { ordered: true });
|
|
1256
1270
|
dc.binaryType = "arraybuffer";
|
|
@@ -1277,9 +1291,7 @@ function startSender(opts) {
|
|
|
1277
1291
|
};
|
|
1278
1292
|
const pump = async () => {
|
|
1279
1293
|
const meta = {
|
|
1280
|
-
|
|
1281
|
-
size: opts.file.size,
|
|
1282
|
-
mime: opts.file.type || "application/octet-stream",
|
|
1294
|
+
...directMetaOf(opts.file, opts.name),
|
|
1283
1295
|
...opts.label !== void 0 ? { label: opts.label } : {}
|
|
1284
1296
|
};
|
|
1285
1297
|
dc.send(encodeMeta(meta));
|
|
@@ -1318,7 +1330,7 @@ function startSender(opts) {
|
|
|
1318
1330
|
s.diag.pairRemote = pair.remote;
|
|
1319
1331
|
opts.onPath?.(pathOf(pair));
|
|
1320
1332
|
}).catch(() => void 0);
|
|
1321
|
-
void pump().catch(() => s.fail("transfer"));
|
|
1333
|
+
void pump().catch((e) => s.fail("transfer", e));
|
|
1322
1334
|
};
|
|
1323
1335
|
dc.onmessage = (e) => {
|
|
1324
1336
|
try {
|
|
@@ -1344,7 +1356,7 @@ function startSender(opts) {
|
|
|
1344
1356
|
plumbing.setLocalGeneration(generation);
|
|
1345
1357
|
plumbing.expectRemoteGeneration(generation);
|
|
1346
1358
|
await opts.send({ kind: "offer", sdp: offer.sdp, protocol: 2, generation });
|
|
1347
|
-
})().catch(() => s.fail("signaling"));
|
|
1359
|
+
})().catch((e) => s.fail("signaling", e));
|
|
1348
1360
|
return {
|
|
1349
1361
|
accept: (msg) => {
|
|
1350
1362
|
if (s.isTerminal()) return;
|
|
@@ -1354,7 +1366,7 @@ function startSender(opts) {
|
|
|
1354
1366
|
if (msg.generation !== void 0 && answerGeneration !== generation) return;
|
|
1355
1367
|
peerProtocol = msg.protocol === 2 ? 2 : 1;
|
|
1356
1368
|
plumbing.expectRemoteGeneration(answerGeneration);
|
|
1357
|
-
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));
|
|
1358
1370
|
}
|
|
1359
1371
|
if (msg.kind === "ice") plumbing.applyIce(msg.candidate, msg.generation);
|
|
1360
1372
|
},
|
|
@@ -1427,7 +1439,7 @@ function startReceiver(opts) {
|
|
|
1427
1439
|
s.fail(frame.meta.size > DIRECT_FILE_MAX_BYTES ? "too_big" : "protocol");
|
|
1428
1440
|
return;
|
|
1429
1441
|
}
|
|
1430
|
-
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)) {
|
|
1431
1443
|
dc?.send(ABORT_FRAME);
|
|
1432
1444
|
s.fail("protocol");
|
|
1433
1445
|
return;
|
|
@@ -1439,7 +1451,7 @@ function startReceiver(opts) {
|
|
|
1439
1451
|
sinkRef = sink;
|
|
1440
1452
|
return sink;
|
|
1441
1453
|
});
|
|
1442
|
-
pipeline.catch(() => s.fail("sink"));
|
|
1454
|
+
pipeline.catch((e3) => s.fail("sink", e3));
|
|
1443
1455
|
} else if (frame.type === "chunk") {
|
|
1444
1456
|
if (!pipeline || finished) throw new Error("chunk_before_meta");
|
|
1445
1457
|
queued += frame.payload.length;
|
|
@@ -1457,7 +1469,7 @@ function startReceiver(opts) {
|
|
|
1457
1469
|
}
|
|
1458
1470
|
return sink;
|
|
1459
1471
|
});
|
|
1460
|
-
pipeline.catch(() => s.fail("sink"));
|
|
1472
|
+
pipeline.catch((e3) => s.fail("sink", e3));
|
|
1461
1473
|
} else if (frame.type === "done") {
|
|
1462
1474
|
if (!pipeline || finished) throw new Error("done_before_meta");
|
|
1463
1475
|
finished = true;
|
|
@@ -1468,7 +1480,7 @@ function startReceiver(opts) {
|
|
|
1468
1480
|
await sink.close();
|
|
1469
1481
|
dc?.send(DONE_FRAME);
|
|
1470
1482
|
s.setPhase("done");
|
|
1471
|
-
}).catch(() => s.fail("integrity"));
|
|
1483
|
+
}).catch((e3) => s.fail("integrity", e3));
|
|
1472
1484
|
} else if (frame.type === "abort") {
|
|
1473
1485
|
s.fail("peer_abort");
|
|
1474
1486
|
}
|
|
@@ -1502,7 +1514,7 @@ function startReceiver(opts) {
|
|
|
1502
1514
|
const answer = await pc.createAnswer();
|
|
1503
1515
|
await pc.setLocalDescription(answer);
|
|
1504
1516
|
await opts.send({ kind: "answer", sdp: answer.sdp, protocol: 2, generation });
|
|
1505
|
-
})().catch(() => s.fail("signaling"));
|
|
1517
|
+
})().catch((e) => s.fail("signaling", e));
|
|
1506
1518
|
}
|
|
1507
1519
|
if (msg.kind === "ice") plumbing.applyIce(msg.candidate, msg.generation);
|
|
1508
1520
|
},
|
|
@@ -1519,6 +1531,89 @@ function startReceiver(opts) {
|
|
|
1519
1531
|
};
|
|
1520
1532
|
}
|
|
1521
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
|
+
|
|
1522
1617
|
// src/shared/direct-fallback.ts
|
|
1523
1618
|
function createFallbackMeta(file) {
|
|
1524
1619
|
const key = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -1636,15 +1731,15 @@ async function uploadFallback(options) {
|
|
|
1636
1731
|
const key = await aesKey(meta);
|
|
1637
1732
|
const completed = [];
|
|
1638
1733
|
const partProgress = /* @__PURE__ */ new Map();
|
|
1639
|
-
const
|
|
1734
|
+
const report2 = () => options.onProgress?.(
|
|
1640
1735
|
Math.min(meta.cipher_size, [...partProgress.values()].reduce((sum, value) => sum + value, 0)),
|
|
1641
1736
|
meta.cipher_size
|
|
1642
1737
|
);
|
|
1643
1738
|
for (let first = 1; first <= meta.part_count; first += URL_BATCH) {
|
|
1644
1739
|
aborted(signal);
|
|
1645
|
-
const
|
|
1646
|
-
const signed = await options.getUrls(first,
|
|
1647
|
-
if (signed.length !==
|
|
1740
|
+
const count2 = Math.min(URL_BATCH, meta.part_count - first + 1);
|
|
1741
|
+
const signed = await options.getUrls(first, count2);
|
|
1742
|
+
if (signed.length !== count2 || signed.some((entry, i) => entry.part_number !== first + i || typeof entry.url !== "string")) throw new Error("bad_fallback_urls");
|
|
1648
1743
|
let cursor = 0;
|
|
1649
1744
|
const workers = Array.from({ length: Math.min(CONCURRENCY, signed.length) }, async () => {
|
|
1650
1745
|
for (; ; ) {
|
|
@@ -1661,11 +1756,11 @@ async function uploadFallback(options) {
|
|
|
1661
1756
|
for (let attempt = 0; attempt < RETRIES; attempt++) {
|
|
1662
1757
|
aborted(signal);
|
|
1663
1758
|
partProgress.set(partNumber, 0);
|
|
1664
|
-
|
|
1759
|
+
report2();
|
|
1665
1760
|
try {
|
|
1666
1761
|
etag = await options.put(entry.url, cipher, signal, (loaded) => {
|
|
1667
1762
|
partProgress.set(partNumber, loaded);
|
|
1668
|
-
|
|
1763
|
+
report2();
|
|
1669
1764
|
});
|
|
1670
1765
|
break;
|
|
1671
1766
|
} catch (error) {
|
|
@@ -2472,13 +2567,13 @@ function sealer(key, keyVersion) {
|
|
|
2472
2567
|
open: (enc) => JSON.parse(new TextDecoder().decode(openRaw(key, b64ToBytes(enc))))
|
|
2473
2568
|
};
|
|
2474
2569
|
}
|
|
2475
|
-
async function sendDirect(ctx, input,
|
|
2570
|
+
async function sendDirect(ctx, input, report2, deps = {}) {
|
|
2476
2571
|
const now = deps.now ?? (() => Date.now());
|
|
2477
2572
|
const sleep2 = deps.sleep ?? defaultSleep2;
|
|
2478
2573
|
const grant = await directGrantFor(ctx, input.channel);
|
|
2479
2574
|
const key = channelKeyOf(ctx.identity, grant);
|
|
2480
2575
|
const channelName = channelLabel(ctx, grant);
|
|
2481
|
-
const file = await (deps.openFile ?? openAsBlob)(input.path).catch(() => {
|
|
2576
|
+
const file = await (deps.openFile ?? openAsBlob)(input.path, { type: mimeFor(input.path) }).catch(() => {
|
|
2482
2577
|
throw new ZasError("upload_failed", 400);
|
|
2483
2578
|
});
|
|
2484
2579
|
if (file.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
|
|
@@ -2489,8 +2584,8 @@ async function sendDirect(ctx, input, report, deps = {}) {
|
|
|
2489
2584
|
const { seal, open } = sealer(key, grant.key_version);
|
|
2490
2585
|
const stamp = { device, owner_uid: owner };
|
|
2491
2586
|
const startedAt = now();
|
|
2492
|
-
|
|
2493
|
-
const meta =
|
|
2587
|
+
report2("offer");
|
|
2588
|
+
const meta = directMetaOf(file, name);
|
|
2494
2589
|
const { id } = await ctx.client.api("POST", `/direct/${cid}`, {
|
|
2495
2590
|
meta_enc: seal(meta),
|
|
2496
2591
|
key_version: grant.key_version,
|
|
@@ -2523,7 +2618,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
|
|
|
2523
2618
|
}
|
|
2524
2619
|
};
|
|
2525
2620
|
const turn = await fetchIce();
|
|
2526
|
-
|
|
2621
|
+
report2("connecting");
|
|
2527
2622
|
let resolveOutcome;
|
|
2528
2623
|
const outcome = new Promise((resolve2) => {
|
|
2529
2624
|
resolveOutcome = resolve2;
|
|
@@ -2540,7 +2635,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
|
|
|
2540
2635
|
send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "receiver", ...stamp }).then(() => void 0),
|
|
2541
2636
|
onPhase: (phase2) => {
|
|
2542
2637
|
if (phase2 === "flight") {
|
|
2543
|
-
|
|
2638
|
+
report2("flight");
|
|
2544
2639
|
heartbeat ??= setInterval(() => {
|
|
2545
2640
|
void ctx.client.api("POST", `${route}/heartbeat`, stamp).catch(() => void 0);
|
|
2546
2641
|
}, deps.heartbeatMs ?? HEARTBEAT_MS);
|
|
@@ -2552,6 +2647,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
|
|
|
2552
2647
|
},
|
|
2553
2648
|
onDiag: (d) => {
|
|
2554
2649
|
diag = d;
|
|
2650
|
+
deps.onDiag?.(d);
|
|
2555
2651
|
}
|
|
2556
2652
|
});
|
|
2557
2653
|
let over = false;
|
|
@@ -2580,7 +2676,7 @@ async function sendDirect(ctx, input, report, deps = {}) {
|
|
|
2580
2676
|
over = true;
|
|
2581
2677
|
clearInterval(heartbeat);
|
|
2582
2678
|
await pump;
|
|
2583
|
-
|
|
2679
|
+
report2("finishing");
|
|
2584
2680
|
if (phase === "done") {
|
|
2585
2681
|
await setState("done");
|
|
2586
2682
|
return {
|
|
@@ -2619,7 +2715,7 @@ var fetchPut = async (url, bytes, signal, onLoaded) => {
|
|
|
2619
2715
|
onLoaded(bytes.length);
|
|
2620
2716
|
return etag;
|
|
2621
2717
|
};
|
|
2622
|
-
async function sendDirectFallback(ctx, record,
|
|
2718
|
+
async function sendDirectFallback(ctx, record, report2, deps = {}) {
|
|
2623
2719
|
const now = deps.now ?? (() => Date.now());
|
|
2624
2720
|
const startedAt = now();
|
|
2625
2721
|
const opened = await (deps.openFile ?? openAsBlob)(record.path).catch(() => {
|
|
@@ -2630,7 +2726,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
|
|
|
2630
2726
|
const { seal } = sealer(record.key, record.key_version);
|
|
2631
2727
|
const stamp = { device: record.device, owner_uid: record.owner_uid };
|
|
2632
2728
|
const base = `/direct/${record.channel_id}/${record.offer_id}/fallback`;
|
|
2633
|
-
|
|
2729
|
+
report2("encrypting");
|
|
2634
2730
|
const meta = createFallbackMeta(file);
|
|
2635
2731
|
await ctx.client.api("POST", `${base}/start`, {
|
|
2636
2732
|
meta_enc: seal(meta),
|
|
@@ -2639,7 +2735,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
|
|
|
2639
2735
|
part_count: meta.part_count,
|
|
2640
2736
|
...stamp
|
|
2641
2737
|
});
|
|
2642
|
-
|
|
2738
|
+
report2("uploading");
|
|
2643
2739
|
let parts;
|
|
2644
2740
|
try {
|
|
2645
2741
|
parts = await uploadFallback({
|
|
@@ -2647,11 +2743,11 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
|
|
|
2647
2743
|
offerId: record.offer_id,
|
|
2648
2744
|
meta,
|
|
2649
2745
|
put: deps.put ?? fetchPut,
|
|
2650
|
-
getUrls: async (from,
|
|
2746
|
+
getUrls: async (from, count2) => {
|
|
2651
2747
|
const r = await ctx.client.api(
|
|
2652
2748
|
"POST",
|
|
2653
2749
|
`${base}/parts`,
|
|
2654
|
-
{ from, count, ...stamp }
|
|
2750
|
+
{ from, count: count2, ...stamp }
|
|
2655
2751
|
);
|
|
2656
2752
|
return r.urls;
|
|
2657
2753
|
}
|
|
@@ -2661,7 +2757,7 @@ async function sendDirectFallback(ctx, record, report, deps = {}) {
|
|
|
2661
2757
|
if (error instanceof ZasError) throw error;
|
|
2662
2758
|
throw new ZasError("upload_failed", 0, error instanceof Error ? error.message : String(error));
|
|
2663
2759
|
}
|
|
2664
|
-
|
|
2760
|
+
report2("finishing");
|
|
2665
2761
|
await ctx.client.api("POST", `${base}/complete`, {
|
|
2666
2762
|
parts: parts.map((part) => ({ part_number: part.partNumber, etag: part.etag })),
|
|
2667
2763
|
...stamp
|
|
@@ -3047,7 +3143,7 @@ var DiskSink = class {
|
|
|
3047
3143
|
return error;
|
|
3048
3144
|
}
|
|
3049
3145
|
};
|
|
3050
|
-
async function receiveDirect(ctx, input,
|
|
3146
|
+
async function receiveDirect(ctx, input, report2, deps = {}) {
|
|
3051
3147
|
const now = deps.now ?? (() => Date.now());
|
|
3052
3148
|
const sleep2 = deps.sleep ?? defaultSleep2;
|
|
3053
3149
|
const grant = await receiveGrantFor(ctx, input.channel);
|
|
@@ -3060,7 +3156,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3060
3156
|
const stamp = { device, owner_uid: owner };
|
|
3061
3157
|
const startedAt = now();
|
|
3062
3158
|
const channelPath = `accounts/${owner}/channels/${cid}`;
|
|
3063
|
-
|
|
3159
|
+
report2("waiting");
|
|
3064
3160
|
const waitUntil = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS2);
|
|
3065
3161
|
let id;
|
|
3066
3162
|
let meta;
|
|
@@ -3112,7 +3208,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3112
3208
|
}
|
|
3113
3209
|
};
|
|
3114
3210
|
const turn = await fetchIce();
|
|
3115
|
-
|
|
3211
|
+
report2("connecting");
|
|
3116
3212
|
const sink = new DiskSink(input.dest, offerId);
|
|
3117
3213
|
let resolveOutcome;
|
|
3118
3214
|
const outcome = new Promise((resolve2) => {
|
|
@@ -3128,7 +3224,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3128
3224
|
expectedMeta: offered,
|
|
3129
3225
|
send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "sender", ...stamp }).then(() => void 0),
|
|
3130
3226
|
onPhase: (phase2) => {
|
|
3131
|
-
if (phase2 === "flight")
|
|
3227
|
+
if (phase2 === "flight") report2("flight");
|
|
3132
3228
|
if (phase2 === "done" || phase2 === "failed") resolveOutcome(phase2);
|
|
3133
3229
|
},
|
|
3134
3230
|
onPath: (p) => {
|
|
@@ -3136,6 +3232,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3136
3232
|
},
|
|
3137
3233
|
onDiag: (d) => {
|
|
3138
3234
|
diag = d;
|
|
3235
|
+
deps.onDiag?.(d);
|
|
3139
3236
|
},
|
|
3140
3237
|
sink: async (m) => {
|
|
3141
3238
|
sink.open(m);
|
|
@@ -3167,7 +3264,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3167
3264
|
const phase = await outcome;
|
|
3168
3265
|
over = true;
|
|
3169
3266
|
await pump;
|
|
3170
|
-
|
|
3267
|
+
report2("finishing");
|
|
3171
3268
|
if (phase === "done") {
|
|
3172
3269
|
await setState("done");
|
|
3173
3270
|
return {
|
|
@@ -3202,7 +3299,7 @@ async function receiveDirect(ctx, input, report, deps = {}) {
|
|
|
3202
3299
|
});
|
|
3203
3300
|
throw new ZasError("direct_failed", 0, reason);
|
|
3204
3301
|
}
|
|
3205
|
-
async function receiveDirectFallback(ctx, record,
|
|
3302
|
+
async function receiveDirectFallback(ctx, record, report2, deps = {}) {
|
|
3206
3303
|
const now = deps.now ?? (() => Date.now());
|
|
3207
3304
|
const startedAt = now();
|
|
3208
3305
|
const { open } = sealer(record.key, record.key_version);
|
|
@@ -3224,7 +3321,7 @@ async function receiveDirectFallback(ctx, record, report, deps = {}) {
|
|
|
3224
3321
|
throw new ZasError("file_changed", 0);
|
|
3225
3322
|
}
|
|
3226
3323
|
const sink = new DiskSink(record.dest, record.offer_id);
|
|
3227
|
-
|
|
3324
|
+
report2("downloading");
|
|
3228
3325
|
try {
|
|
3229
3326
|
sink.open(meta);
|
|
3230
3327
|
await downloadFallback({
|
|
@@ -3240,7 +3337,7 @@ async function receiveDirectFallback(ctx, record, report, deps = {}) {
|
|
|
3240
3337
|
if (error instanceof ZasError) throw error;
|
|
3241
3338
|
throw new ZasError("network", 0, error instanceof Error ? error.message : String(error));
|
|
3242
3339
|
}
|
|
3243
|
-
|
|
3340
|
+
report2("finishing");
|
|
3244
3341
|
await ctx.client.api("POST", `${base}/received`, stamp);
|
|
3245
3342
|
return {
|
|
3246
3343
|
offer_id: record.offer_id,
|
|
@@ -3281,10 +3378,13 @@ var JobRunner = class {
|
|
|
3281
3378
|
};
|
|
3282
3379
|
this.jobs.unshift(job);
|
|
3283
3380
|
this.jobs.length = Math.min(this.jobs.length, HISTORY);
|
|
3284
|
-
const
|
|
3381
|
+
const report2 = (phase) => {
|
|
3285
3382
|
if (job.status === "running") job.phase = phase;
|
|
3286
3383
|
};
|
|
3287
|
-
|
|
3384
|
+
const note = (diag) => {
|
|
3385
|
+
job.diag = diag;
|
|
3386
|
+
};
|
|
3387
|
+
this.settled.set(job, work(report2, note).then(
|
|
3288
3388
|
(result) => {
|
|
3289
3389
|
job.status = "done";
|
|
3290
3390
|
job.result = result;
|
|
@@ -3328,9 +3428,477 @@ var JobRunner = class {
|
|
|
3328
3428
|
}
|
|
3329
3429
|
};
|
|
3330
3430
|
|
|
3431
|
+
// src/shared/product-analytics.ts
|
|
3432
|
+
var PRODUCT_EVENTS = [
|
|
3433
|
+
"account_created",
|
|
3434
|
+
"item.created",
|
|
3435
|
+
"item.downloaded",
|
|
3436
|
+
"item.copied",
|
|
3437
|
+
"item.shared",
|
|
3438
|
+
"item.pinned",
|
|
3439
|
+
"item.unpinned",
|
|
3440
|
+
"item.deleted",
|
|
3441
|
+
"item.departed",
|
|
3442
|
+
"channel_shared",
|
|
3443
|
+
"channel_mode_set",
|
|
3444
|
+
"invite_created",
|
|
3445
|
+
"invite_link_copied",
|
|
3446
|
+
"invite_qr_shown",
|
|
3447
|
+
"invite_system_sheet_opened",
|
|
3448
|
+
"invite_joined",
|
|
3449
|
+
"invite_converted",
|
|
3450
|
+
"channel.invite_preview",
|
|
3451
|
+
"channel.invite_accept",
|
|
3452
|
+
"channel.invite_signin",
|
|
3453
|
+
"enterprise.invite_preview",
|
|
3454
|
+
"enterprise.invite_identity",
|
|
3455
|
+
"enterprise.invite_signin",
|
|
3456
|
+
"enterprise.invite_account_switch",
|
|
3457
|
+
"enterprise.invite_attach",
|
|
3458
|
+
"enterprise.invite_accept",
|
|
3459
|
+
"enterprise.invite_admin",
|
|
3460
|
+
"enterprise.workspace_opened",
|
|
3461
|
+
"enterprise.workspace_switched",
|
|
3462
|
+
"enterprise.channel_created",
|
|
3463
|
+
"enterprise.channel_members_added",
|
|
3464
|
+
"share_link_copied",
|
|
3465
|
+
"share_qr_shown",
|
|
3466
|
+
"share_system_sheet_opened",
|
|
3467
|
+
"share_opened",
|
|
3468
|
+
"share_content_copied",
|
|
3469
|
+
"share_downloaded",
|
|
3470
|
+
"share_signin_click",
|
|
3471
|
+
"share.saved",
|
|
3472
|
+
"share_bar_on",
|
|
3473
|
+
"share_bar_off",
|
|
3474
|
+
"upload.phase_timing",
|
|
3475
|
+
"auth.signin_started",
|
|
3476
|
+
"auth.signin_result",
|
|
3477
|
+
"auth.session_started",
|
|
3478
|
+
"auth.signed_out",
|
|
3479
|
+
"auth.session_lost",
|
|
3480
|
+
"auth.token_refresh_failed",
|
|
3481
|
+
"account_switch.started",
|
|
3482
|
+
"account_switch.completed",
|
|
3483
|
+
"account_switch.failed",
|
|
3484
|
+
"bootstrap.result",
|
|
3485
|
+
"direct_mode_set",
|
|
3486
|
+
"direct.offered",
|
|
3487
|
+
"direct.claimed",
|
|
3488
|
+
"direct.connected",
|
|
3489
|
+
"direct.completed",
|
|
3490
|
+
"direct.received",
|
|
3491
|
+
"direct.failed",
|
|
3492
|
+
"direct.fallback_started",
|
|
3493
|
+
"direct.fallback_ready",
|
|
3494
|
+
"direct.fallback_received",
|
|
3495
|
+
"direct.fallback_failed",
|
|
3496
|
+
"direct.fallback_aborted",
|
|
3497
|
+
"direct.too_big",
|
|
3498
|
+
"agent.pair_opened",
|
|
3499
|
+
"agent.pair_approved",
|
|
3500
|
+
"agent.pair_refused",
|
|
3501
|
+
"agent.paired",
|
|
3502
|
+
"agent.revoked",
|
|
3503
|
+
"agent.send",
|
|
3504
|
+
"agent.tool_call"
|
|
3505
|
+
];
|
|
3506
|
+
var SERVER_OWNED_PRODUCT_EVENTS = [
|
|
3507
|
+
"account_created",
|
|
3508
|
+
"item.departed",
|
|
3509
|
+
"invite_created",
|
|
3510
|
+
"invite_joined",
|
|
3511
|
+
"invite_converted",
|
|
3512
|
+
"share_opened",
|
|
3513
|
+
"share_content_copied",
|
|
3514
|
+
"share_downloaded",
|
|
3515
|
+
"share_signin_click",
|
|
3516
|
+
"agent.send",
|
|
3517
|
+
"agent.tool_call"
|
|
3518
|
+
];
|
|
3519
|
+
var SERVER_OWNED_PRODUCT_EVENT_SET = new Set(SERVER_OWNED_PRODUCT_EVENTS);
|
|
3520
|
+
var CLIENT_PRODUCT_EVENTS = PRODUCT_EVENTS.filter(
|
|
3521
|
+
(event) => !SERVER_OWNED_PRODUCT_EVENT_SET.has(event)
|
|
3522
|
+
);
|
|
3523
|
+
var bool = { type: "boolean" };
|
|
3524
|
+
var count = { type: "number", min: 0 };
|
|
3525
|
+
var oneOf = (...values) => ({ type: "string", values: new Set(values) });
|
|
3526
|
+
var sizeBucket = oneOf("lt_1mb", "1_10mb", "10_100mb", "100_512mb", "512mb_5gb", "gt_5gb");
|
|
3527
|
+
var itemKind = oneOf("file", "text");
|
|
3528
|
+
var itemKindOrUnknown = oneOf("file", "text", "unknown");
|
|
3529
|
+
var role = oneOf("sender", "receiver");
|
|
3530
|
+
var candidateType = oneOf("", "host", "srflx", "relay", "prflx", "other");
|
|
3531
|
+
var enterpriseRole = oneOf("owner", "admin", "billing_admin", "member", "guest");
|
|
3532
|
+
var agentKind = oneOf("claude_code", "codex", "other");
|
|
3533
|
+
var pairHandoff = oneOf("loopback", "manual", "unknown");
|
|
3534
|
+
var AGENT_TOOL_NAMES = [
|
|
3535
|
+
"status",
|
|
3536
|
+
"pair",
|
|
3537
|
+
"send_file",
|
|
3538
|
+
"send_note",
|
|
3539
|
+
"send_direct",
|
|
3540
|
+
"send_direct_fallback",
|
|
3541
|
+
"receive_direct",
|
|
3542
|
+
"receive_direct_fallback",
|
|
3543
|
+
"list_items",
|
|
3544
|
+
"get_item",
|
|
3545
|
+
"jobs"
|
|
3546
|
+
];
|
|
3547
|
+
var AGENT_ERROR_CODES = [
|
|
3548
|
+
"none",
|
|
3549
|
+
"other",
|
|
3550
|
+
"identity_corrupt",
|
|
3551
|
+
"not_paired",
|
|
3552
|
+
"internal",
|
|
3553
|
+
"agent_forbidden",
|
|
3554
|
+
"agent_revoked",
|
|
3555
|
+
"grant_missing",
|
|
3556
|
+
"not_found",
|
|
3557
|
+
"invalid_cap",
|
|
3558
|
+
"write_failed",
|
|
3559
|
+
"send_forbidden",
|
|
3560
|
+
"read_forbidden",
|
|
3561
|
+
"direct_mode",
|
|
3562
|
+
"not_direct_mode",
|
|
3563
|
+
"not_claimed",
|
|
3564
|
+
"no_offer",
|
|
3565
|
+
"offer_taken",
|
|
3566
|
+
"direct_cancelled",
|
|
3567
|
+
"direct_failed",
|
|
3568
|
+
"direct_not_failed",
|
|
3569
|
+
"file_changed",
|
|
3570
|
+
"webrtc_unavailable",
|
|
3571
|
+
"fallback_unavailable",
|
|
3572
|
+
"key_stale",
|
|
3573
|
+
"quota_exceeded",
|
|
3574
|
+
"rate_limited",
|
|
3575
|
+
"file_too_big",
|
|
3576
|
+
"duplicate",
|
|
3577
|
+
"pairing_expired",
|
|
3578
|
+
"pairing_cancelled",
|
|
3579
|
+
"claim_mismatch",
|
|
3580
|
+
"pairing_not_approved",
|
|
3581
|
+
"pairing_claimed",
|
|
3582
|
+
"agent_limit",
|
|
3583
|
+
"grant_limit",
|
|
3584
|
+
"feature_disabled",
|
|
3585
|
+
"upload_failed",
|
|
3586
|
+
"oprf_failed",
|
|
3587
|
+
"network",
|
|
3588
|
+
"sign_in_failed",
|
|
3589
|
+
"bad_signature",
|
|
3590
|
+
"missing_token"
|
|
3591
|
+
];
|
|
3592
|
+
function agentDurationBucket(ms) {
|
|
3593
|
+
if (ms < 1e3) return "lt_1s";
|
|
3594
|
+
if (ms < 1e4) return "1_10s";
|
|
3595
|
+
if (ms < 6e4) return "10_60s";
|
|
3596
|
+
return "gt_60s";
|
|
3597
|
+
}
|
|
3598
|
+
var EVENT_PROPERTY_RULES = {
|
|
3599
|
+
account_created: {},
|
|
3600
|
+
"item.created": { kind: itemKind, size_bytes: count, size_bucket: sizeBucket },
|
|
3601
|
+
"item.downloaded": { size_bytes: count, size_bucket: sizeBucket },
|
|
3602
|
+
"item.copied": {},
|
|
3603
|
+
"item.shared": { kind: itemKind },
|
|
3604
|
+
"item.pinned": { age_ms: count },
|
|
3605
|
+
"item.unpinned": {},
|
|
3606
|
+
"item.deleted": { age_ms: count, was_pinned: bool, kind: itemKindOrUnknown },
|
|
3607
|
+
"item.departed": {},
|
|
3608
|
+
channel_shared: {},
|
|
3609
|
+
channel_mode_set: { mode: oneOf("edit", "view") },
|
|
3610
|
+
invite_created: {},
|
|
3611
|
+
invite_link_copied: {},
|
|
3612
|
+
invite_qr_shown: {},
|
|
3613
|
+
invite_system_sheet_opened: {},
|
|
3614
|
+
invite_joined: {},
|
|
3615
|
+
invite_converted: {},
|
|
3616
|
+
"channel.invite_preview": {
|
|
3617
|
+
outcome: oneOf("ready", "unavailable", "missing_key"),
|
|
3618
|
+
signed_in: bool
|
|
3619
|
+
},
|
|
3620
|
+
"channel.invite_accept": {
|
|
3621
|
+
outcome: oneOf("started", "completed", "already_member", "unavailable", "failed")
|
|
3622
|
+
},
|
|
3623
|
+
"channel.invite_signin": {
|
|
3624
|
+
stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
|
|
3625
|
+
},
|
|
3626
|
+
"enterprise.invite_preview": {
|
|
3627
|
+
outcome: oneOf("pending", "resumable", "unavailable"),
|
|
3628
|
+
role: enterpriseRole
|
|
3629
|
+
},
|
|
3630
|
+
"enterprise.invite_identity": {
|
|
3631
|
+
outcome: oneOf("matched", "mismatched", "unavailable", "failed")
|
|
3632
|
+
},
|
|
3633
|
+
"enterprise.invite_signin": {
|
|
3634
|
+
stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
|
|
3635
|
+
},
|
|
3636
|
+
"enterprise.invite_account_switch": { stage: oneOf("started", "completed", "failed") },
|
|
3637
|
+
"enterprise.invite_attach": {
|
|
3638
|
+
stage: oneOf("link_requested", "link_sent", "verify_started", "completed", "address_taken", "link_invalid", "failed")
|
|
3639
|
+
},
|
|
3640
|
+
"enterprise.invite_accept": {
|
|
3641
|
+
outcome: oneOf("started", "completed", "recovered", "timeout", "unavailable", "email_mismatch", "seat_limit", "failed"),
|
|
3642
|
+
role: enterpriseRole,
|
|
3643
|
+
destination: oneOf("workspace", "admin_console"),
|
|
3644
|
+
duration_ms: count
|
|
3645
|
+
},
|
|
3646
|
+
"enterprise.invite_admin": {
|
|
3647
|
+
outcome: oneOf("created", "email_sent", "email_failed", "reissued", "revoked"),
|
|
3648
|
+
role: enterpriseRole,
|
|
3649
|
+
external: bool
|
|
3650
|
+
},
|
|
3651
|
+
"enterprise.workspace_opened": { role: enterpriseRole, first_visit: bool },
|
|
3652
|
+
"enterprise.workspace_switched": { destination: oneOf("personal", "organization"), role: enterpriseRole },
|
|
3653
|
+
"enterprise.channel_created": { member_count: count },
|
|
3654
|
+
"enterprise.channel_members_added": { member_count: count },
|
|
3655
|
+
share_link_copied: {},
|
|
3656
|
+
share_qr_shown: {},
|
|
3657
|
+
share_system_sheet_opened: {},
|
|
3658
|
+
share_opened: {},
|
|
3659
|
+
share_content_copied: {},
|
|
3660
|
+
share_downloaded: {},
|
|
3661
|
+
share_signin_click: {},
|
|
3662
|
+
"share.saved": {},
|
|
3663
|
+
share_bar_on: {},
|
|
3664
|
+
share_bar_off: {},
|
|
3665
|
+
"upload.phase_timing": {
|
|
3666
|
+
stage: oneOf("staging", "bootstrap", "thumbnail", "encrypting", "uploading", "finishing"),
|
|
3667
|
+
duration_ms: count,
|
|
3668
|
+
size_bucket: sizeBucket,
|
|
3669
|
+
outcome: oneOf("success", "failure", "cancelled"),
|
|
3670
|
+
attempt: { type: "number", min: 1 }
|
|
3671
|
+
},
|
|
3672
|
+
"auth.signin_started": { provider: oneOf("google.com", "apple.com") },
|
|
3673
|
+
"auth.signin_result": {
|
|
3674
|
+
provider: oneOf("google.com", "apple.com"),
|
|
3675
|
+
outcome: oneOf("signed_in", "cancelled", "taken", "failed", "needs_link"),
|
|
3676
|
+
flow: oneOf("signin", "link", "connect")
|
|
3677
|
+
},
|
|
3678
|
+
"account_switch.started": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
|
|
3679
|
+
"account_switch.completed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
|
|
3680
|
+
"account_switch.failed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
|
|
3681
|
+
"auth.session_started": { cold: bool },
|
|
3682
|
+
"auth.signed_out": { reason: oneOf("user", "account_switch", "abandoned_new_account", "account_deleted") },
|
|
3683
|
+
"auth.session_lost": { session_age_ms: count, foreground: bool },
|
|
3684
|
+
"auth.token_refresh_failed": {
|
|
3685
|
+
code: oneOf("network", "token_invalid", "user_disabled", "user_missing", "rate_limited", "internal", "unknown")
|
|
3686
|
+
},
|
|
3687
|
+
"bootstrap.result": {
|
|
3688
|
+
outcome: oneOf("offline", "ok", "timeout_warm", "timeout", "failed_warm", "failed"),
|
|
3689
|
+
stage: oneOf("cached", "done", "unknown", "keyring", "account_init", "listeners"),
|
|
3690
|
+
duration_ms: count,
|
|
3691
|
+
fault: oneOf("intercepted", "offline", "unknown")
|
|
3692
|
+
},
|
|
3693
|
+
direct_mode_set: { on: bool },
|
|
3694
|
+
"direct.offered": { size_bucket: sizeBucket },
|
|
3695
|
+
// `via_agent`: the offer came from a paired agent's process, not from one
|
|
3696
|
+
// of the person's devices. The claim and the two receiver outcomes carry it.
|
|
3697
|
+
"direct.claimed": { size_bucket: sizeBucket, via_agent: bool },
|
|
3698
|
+
"direct.connected": { role, ms: count },
|
|
3699
|
+
"direct.completed": {
|
|
3700
|
+
via: oneOf("lan", "wan", "relay", "unknown"),
|
|
3701
|
+
pair_local: candidateType,
|
|
3702
|
+
pair_remote: candidateType,
|
|
3703
|
+
connect_ms: count,
|
|
3704
|
+
duration_ms: count,
|
|
3705
|
+
size_bucket: sizeBucket,
|
|
3706
|
+
turn_urls_supplied: count,
|
|
3707
|
+
turn_urls_configured: count,
|
|
3708
|
+
restarts: count
|
|
3709
|
+
},
|
|
3710
|
+
"direct.received": {
|
|
3711
|
+
via_agent: bool,
|
|
3712
|
+
via: oneOf("lan", "wan", "relay", "unknown"),
|
|
3713
|
+
pair_local: candidateType,
|
|
3714
|
+
pair_remote: candidateType,
|
|
3715
|
+
connect_ms: count,
|
|
3716
|
+
duration_ms: count,
|
|
3717
|
+
size_bucket: sizeBucket,
|
|
3718
|
+
turn_urls_supplied: count,
|
|
3719
|
+
turn_urls_configured: count,
|
|
3720
|
+
restarts: count
|
|
3721
|
+
},
|
|
3722
|
+
"direct.failed": {
|
|
3723
|
+
role,
|
|
3724
|
+
via_agent: bool,
|
|
3725
|
+
reason: oneOf(
|
|
3726
|
+
"peer_silent",
|
|
3727
|
+
"connect_timeout",
|
|
3728
|
+
"ice_failed",
|
|
3729
|
+
"disconnected",
|
|
3730
|
+
"peer_closed",
|
|
3731
|
+
"peer_abort",
|
|
3732
|
+
"signaling",
|
|
3733
|
+
"signal_send",
|
|
3734
|
+
"protocol",
|
|
3735
|
+
"sink",
|
|
3736
|
+
"transfer",
|
|
3737
|
+
"integrity",
|
|
3738
|
+
"too_big",
|
|
3739
|
+
"unknown"
|
|
3740
|
+
),
|
|
3741
|
+
ms: count,
|
|
3742
|
+
ice_state: oneOf("", "new", "checking", "connected", "completed", "failed", "disconnected", "closed"),
|
|
3743
|
+
gather_state: oneOf("", "new", "gathering", "complete"),
|
|
3744
|
+
had_remote: bool,
|
|
3745
|
+
local_host: count,
|
|
3746
|
+
local_srflx: count,
|
|
3747
|
+
local_relay: count,
|
|
3748
|
+
remote_host: count,
|
|
3749
|
+
remote_srflx: count,
|
|
3750
|
+
remote_relay: count,
|
|
3751
|
+
turn_urls_supplied: count,
|
|
3752
|
+
turn_urls_configured: count,
|
|
3753
|
+
moved_bucket: sizeBucket,
|
|
3754
|
+
size_bucket: sizeBucket,
|
|
3755
|
+
restarts: count,
|
|
3756
|
+
// Whether the reporting tab was on screen when the run died — the lead a
|
|
3757
|
+
// slept machine or a frozen background tab leaves behind.
|
|
3758
|
+
visibility: oneOf("visible", "hidden", "prerender")
|
|
3759
|
+
},
|
|
3760
|
+
"direct.fallback_started": { size_bucket: sizeBucket },
|
|
3761
|
+
"direct.fallback_ready": { size_bucket: sizeBucket, duration_ms: count, retries: count },
|
|
3762
|
+
"direct.fallback_received": { size_bucket: sizeBucket, duration_ms: count, retries: count },
|
|
3763
|
+
"direct.fallback_failed": {
|
|
3764
|
+
stage: oneOf("upload", "download"),
|
|
3765
|
+
size_bucket: sizeBucket,
|
|
3766
|
+
retries: count
|
|
3767
|
+
},
|
|
3768
|
+
"direct.fallback_aborted": {},
|
|
3769
|
+
"direct.too_big": { size_bucket: sizeBucket },
|
|
3770
|
+
"agent.pair_opened": { agent_kind: agentKind, protocol: count, replaces: bool },
|
|
3771
|
+
"agent.pair_approved": {
|
|
3772
|
+
agent_kind: agentKind,
|
|
3773
|
+
destination: oneOf("new_channel", "existing"),
|
|
3774
|
+
channels: count,
|
|
3775
|
+
handoff: pairHandoff
|
|
3776
|
+
},
|
|
3777
|
+
"agent.pair_refused": {
|
|
3778
|
+
agent_kind: agentKind,
|
|
3779
|
+
step: oneOf("open", "approve", "claim"),
|
|
3780
|
+
reason: oneOf(
|
|
3781
|
+
"agent_limit",
|
|
3782
|
+
"grant_limit",
|
|
3783
|
+
"channel_limit",
|
|
3784
|
+
"code_mismatch",
|
|
3785
|
+
"pairing_expired",
|
|
3786
|
+
"pairing_cancelled",
|
|
3787
|
+
"pairing_claimed",
|
|
3788
|
+
"feature_disabled",
|
|
3789
|
+
"rate_limited",
|
|
3790
|
+
"network",
|
|
3791
|
+
"other"
|
|
3792
|
+
)
|
|
3793
|
+
},
|
|
3794
|
+
"agent.paired": {
|
|
3795
|
+
agent_kind: agentKind,
|
|
3796
|
+
destination: oneOf("new_channel", "existing"),
|
|
3797
|
+
channels: count,
|
|
3798
|
+
handoff: pairHandoff,
|
|
3799
|
+
protocol: count
|
|
3800
|
+
},
|
|
3801
|
+
"agent.revoked": { agent_kind: agentKind },
|
|
3802
|
+
"agent.send": { size_bucket: sizeBucket, agent_kind: agentKind },
|
|
3803
|
+
// What the CLI reports after every tool call: the tool, whether it worked,
|
|
3804
|
+
// the code when it did not, and the version that answered. No path, no file
|
|
3805
|
+
// name, no channel, no sentence.
|
|
3806
|
+
"agent.tool_call": {
|
|
3807
|
+
tool: oneOf(...AGENT_TOOL_NAMES),
|
|
3808
|
+
result: oneOf("ok", "error"),
|
|
3809
|
+
code: oneOf(...AGENT_ERROR_CODES),
|
|
3810
|
+
agent_kind: agentKind,
|
|
3811
|
+
duration_bucket: oneOf("lt_1s", "1_10s", "10_60s", "gt_60s"),
|
|
3812
|
+
version_major: count,
|
|
3813
|
+
version_minor: count,
|
|
3814
|
+
version_patch: count
|
|
3815
|
+
}
|
|
3816
|
+
};
|
|
3817
|
+
var platformRule = oneOf("web", "android", "ios", "agent", "unknown");
|
|
3818
|
+
var PERSON_PROPERTY_RULES = {
|
|
3819
|
+
plan: oneOf("anon", "free", "pro", "max"),
|
|
3820
|
+
locale: oneOf("es-AR", "pt-BR", "en"),
|
|
3821
|
+
storage_bytes: count
|
|
3822
|
+
};
|
|
3823
|
+
|
|
3824
|
+
// src/telemetry.ts
|
|
3825
|
+
var OFF_WORDS = /* @__PURE__ */ new Set(["0", "off", "false", "no"]);
|
|
3826
|
+
var ON_WORDS = /* @__PURE__ */ new Set(["1", "on", "true", "yes"]);
|
|
3827
|
+
function telemetryState() {
|
|
3828
|
+
const env = (process.env.ZAS_AGENT_TELEMETRY ?? "").toLowerCase();
|
|
3829
|
+
if (OFF_WORDS.has(env)) return { on: false, source: "env" };
|
|
3830
|
+
if (ON_WORDS.has(env)) return { on: true, source: "env" };
|
|
3831
|
+
if (process.env.DO_NOT_TRACK === "1") return { on: false, source: "do_not_track" };
|
|
3832
|
+
const stored = loadSettings();
|
|
3833
|
+
if (stored && typeof stored.telemetry === "boolean") return { on: stored.telemetry, source: "file" };
|
|
3834
|
+
return { on: true, source: "default" };
|
|
3835
|
+
}
|
|
3836
|
+
function setTelemetry(on) {
|
|
3837
|
+
const stored = loadSettings();
|
|
3838
|
+
saveSettings({ ...stored ?? {}, version: 1, telemetry: on, decided_at: Date.now() });
|
|
3839
|
+
}
|
|
3840
|
+
function noticeShown() {
|
|
3841
|
+
return typeof loadSettings()?.notified_at === "number";
|
|
3842
|
+
}
|
|
3843
|
+
function markNoticeShown() {
|
|
3844
|
+
const stored = loadSettings();
|
|
3845
|
+
saveSettings({ ...stored ?? {}, version: 1, notified_at: Date.now() });
|
|
3846
|
+
}
|
|
3847
|
+
var TELEMETRY_NOTICE = [
|
|
3848
|
+
"Zas collects usage data from this agent to improve the product: which tool",
|
|
3849
|
+
"ran, whether it worked, the error code when it did not, how long it took,",
|
|
3850
|
+
"and the version. Never file names, file contents, paths or channel names.",
|
|
3851
|
+
"The data is linked to your Zas account.",
|
|
3852
|
+
"Turn it off with: npx -y zas-agent telemetry off"
|
|
3853
|
+
].join("\n");
|
|
3854
|
+
var SOURCE_WORDS = {
|
|
3855
|
+
env: "ZAS_AGENT_TELEMETRY",
|
|
3856
|
+
do_not_track: "DO_NOT_TRACK",
|
|
3857
|
+
file: "zas-agent telemetry",
|
|
3858
|
+
default: "default"
|
|
3859
|
+
};
|
|
3860
|
+
function telemetryLine() {
|
|
3861
|
+
const state = telemetryState();
|
|
3862
|
+
return `telemetry: ${state.on ? "on" : "off"} (${SOURCE_WORDS[state.source]})`;
|
|
3863
|
+
}
|
|
3864
|
+
var TOOL_NAMES = new Set(AGENT_TOOL_NAMES);
|
|
3865
|
+
function versionParts(version) {
|
|
3866
|
+
const [major = 0, minor = 0, patch = 0] = version.split("-")[0].split(".").map((part) => {
|
|
3867
|
+
const value = Number.parseInt(part, 10);
|
|
3868
|
+
return Number.isFinite(value) && value >= 0 ? value : 0;
|
|
3869
|
+
});
|
|
3870
|
+
return { major, minor, patch };
|
|
3871
|
+
}
|
|
3872
|
+
function toolCallProperties(input) {
|
|
3873
|
+
const tool = input.tool.startsWith("zas_") ? input.tool.slice(4) : input.tool;
|
|
3874
|
+
if (!TOOL_NAMES.has(tool)) return null;
|
|
3875
|
+
const version = versionParts(input.version);
|
|
3876
|
+
return {
|
|
3877
|
+
tool,
|
|
3878
|
+
result: input.code === null ? "ok" : "error",
|
|
3879
|
+
// `other` is the server's word for a code outside the vocabulary. The
|
|
3880
|
+
// shared list is the one that decides, and it drops anything else.
|
|
3881
|
+
code: input.code ?? "none",
|
|
3882
|
+
duration_bucket: agentDurationBucket(input.ms),
|
|
3883
|
+
version_major: version.major,
|
|
3884
|
+
version_minor: version.minor,
|
|
3885
|
+
version_patch: version.patch
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
var REPORT_TIMEOUT_MS = 5e3;
|
|
3889
|
+
async function report(client, properties) {
|
|
3890
|
+
if (properties === null || !telemetryState().on) return;
|
|
3891
|
+
const timeout = new Promise((resolve2) => {
|
|
3892
|
+
const timer = setTimeout(resolve2, REPORT_TIMEOUT_MS);
|
|
3893
|
+
timer.unref?.();
|
|
3894
|
+
});
|
|
3895
|
+
const sent = client.api("POST", "/agents/telemetry", { event: "agent.tool_call", properties }).then(() => void 0).catch(() => void 0);
|
|
3896
|
+
await Promise.race([sent, timeout]);
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3331
3899
|
// src/server.ts
|
|
3332
3900
|
function agentVersion() {
|
|
3333
|
-
return true ? "0.
|
|
3901
|
+
return true ? "0.6.1" : "0.0.0-dev";
|
|
3334
3902
|
}
|
|
3335
3903
|
var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
|
|
3336
3904
|
var PAIR_ANNOUNCE_MS = 15e3;
|
|
@@ -3344,9 +3912,33 @@ function rightsOf(grant) {
|
|
|
3344
3912
|
if (grant.read) rights.push(grant.direct_mode ? "receive (Directo)" : "read");
|
|
3345
3913
|
return rights.length > 0 ? rights.join(" \xB7 ") : "no access";
|
|
3346
3914
|
}
|
|
3915
|
+
function answeredCode(answer) {
|
|
3916
|
+
if (!answer?.isError) return null;
|
|
3917
|
+
const match = /^([a-z_]{2,40}):/.exec(answer.content?.[0]?.text ?? "");
|
|
3918
|
+
return match ? match[1] : "other";
|
|
3919
|
+
}
|
|
3347
3920
|
function buildServer(profile, deps = {}) {
|
|
3348
3921
|
const server = new McpServer({ name: "zas", version: agentVersion() });
|
|
3349
3922
|
const runner = deps.runner ?? new JobRunner();
|
|
3923
|
+
const reportCall = async (tool, ms, code) => {
|
|
3924
|
+
try {
|
|
3925
|
+
if (!(deps.identity ?? loadIdentity(profile))) return;
|
|
3926
|
+
await report(ctx().client, toolCallProperties({ tool, ms, code, version: agentVersion() }));
|
|
3927
|
+
} catch {
|
|
3928
|
+
}
|
|
3929
|
+
};
|
|
3930
|
+
const registerRaw = server.registerTool.bind(server);
|
|
3931
|
+
server.registerTool = (name, meta, handler) => registerRaw(name, meta, async (...args) => {
|
|
3932
|
+
const started = Date.now();
|
|
3933
|
+
try {
|
|
3934
|
+
const answer = await handler(...args);
|
|
3935
|
+
void reportCall(name, Date.now() - started, answeredCode(answer));
|
|
3936
|
+
return answer;
|
|
3937
|
+
} catch (e) {
|
|
3938
|
+
void reportCall(name, Date.now() - started, e instanceof ZasError ? e.code : "internal");
|
|
3939
|
+
throw e;
|
|
3940
|
+
}
|
|
3941
|
+
});
|
|
3350
3942
|
let client = null;
|
|
3351
3943
|
const ctx = () => {
|
|
3352
3944
|
const identity = deps.identity ?? loadIdentity(profile);
|
|
@@ -3366,10 +3958,17 @@ function buildServer(profile, deps = {}) {
|
|
|
3366
3958
|
return { isError: true, ...text2(`${err.code}: ${humanSentence(err)}`) };
|
|
3367
3959
|
};
|
|
3368
3960
|
const settled = (job) => {
|
|
3369
|
-
if (job.status === "done")
|
|
3961
|
+
if (job.status === "done") {
|
|
3962
|
+
return job.diag ? text2({ ...job.result, diag: formatDirectDiagLines(job.diag) }) : text2(job.result);
|
|
3963
|
+
}
|
|
3370
3964
|
if (job.status === "failed") {
|
|
3371
3965
|
const e = job.error;
|
|
3372
|
-
|
|
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")}`) };
|
|
3373
3972
|
}
|
|
3374
3973
|
return text2({ job_id: job.id, status: "running", phase: job.phase });
|
|
3375
3974
|
};
|
|
@@ -3394,7 +3993,8 @@ function buildServer(profile, deps = {}) {
|
|
|
3394
3993
|
`Paired as \u201C${c.identity.name}\u201D (${c.identity.kind}) with account ${c.identity.owner_uid}.`,
|
|
3395
3994
|
grants.length > 0 ? "Channels:" : "No channels: the owner has not granted access to any yet.",
|
|
3396
3995
|
...lines,
|
|
3397
|
-
`${packageName()} ${agentVersion()} \xB7 profile ${profile}
|
|
3996
|
+
`${packageName()} ${agentVersion()} \xB7 profile ${profile}`,
|
|
3997
|
+
`${telemetryLine()} \xB7 change it with \u201Cnpx -y ${packageName()} telemetry off\u201D`
|
|
3398
3998
|
].join("\n"));
|
|
3399
3999
|
} catch (e) {
|
|
3400
4000
|
return failed(e);
|
|
@@ -3488,7 +4088,7 @@ function buildServer(profile, deps = {}) {
|
|
|
3488
4088
|
"file",
|
|
3489
4089
|
input.title ?? input.path,
|
|
3490
4090
|
input.channel ?? "",
|
|
3491
|
-
(
|
|
4091
|
+
(report2) => sendFile(c, input, report2)
|
|
3492
4092
|
);
|
|
3493
4093
|
return settled(await runner.wait(job));
|
|
3494
4094
|
} catch (e) {
|
|
@@ -3514,9 +4114,10 @@ function buildServer(profile, deps = {}) {
|
|
|
3514
4114
|
"direct",
|
|
3515
4115
|
input.path,
|
|
3516
4116
|
input.channel ?? "",
|
|
3517
|
-
(
|
|
4117
|
+
(report2, note) => sendDirect(c, input, report2, {
|
|
3518
4118
|
...deps.direct ?? {},
|
|
3519
|
-
onFailed: (record) => rememberFailed(job.id, record)
|
|
4119
|
+
onFailed: (record) => rememberFailed(job.id, record),
|
|
4120
|
+
onDiag: note
|
|
3520
4121
|
})
|
|
3521
4122
|
);
|
|
3522
4123
|
return settled(await runner.wait(job));
|
|
@@ -3538,7 +4139,7 @@ function buildServer(profile, deps = {}) {
|
|
|
3538
4139
|
"fallback",
|
|
3539
4140
|
record.name,
|
|
3540
4141
|
record.channel_name,
|
|
3541
|
-
(
|
|
4142
|
+
(report2) => sendDirectFallback(c, record, report2, deps.direct)
|
|
3542
4143
|
);
|
|
3543
4144
|
const outcome = await runner.wait(job);
|
|
3544
4145
|
if (outcome.status === "done") failedDirects.delete(input.job);
|
|
@@ -3566,9 +4167,10 @@ function buildServer(profile, deps = {}) {
|
|
|
3566
4167
|
"receive",
|
|
3567
4168
|
input.dest ?? "Directo",
|
|
3568
4169
|
input.channel ?? "",
|
|
3569
|
-
(
|
|
4170
|
+
(report2, note) => receiveDirect(c, input, report2, {
|
|
3570
4171
|
...deps.receive ?? {},
|
|
3571
|
-
onFailed: (record) => rememberFailedReceive(job.id, record)
|
|
4172
|
+
onFailed: (record) => rememberFailedReceive(job.id, record),
|
|
4173
|
+
onDiag: note
|
|
3572
4174
|
})
|
|
3573
4175
|
);
|
|
3574
4176
|
return settled(await runner.wait(job));
|
|
@@ -3590,7 +4192,7 @@ function buildServer(profile, deps = {}) {
|
|
|
3590
4192
|
"fallback",
|
|
3591
4193
|
record.meta.name,
|
|
3592
4194
|
record.channel_name,
|
|
3593
|
-
(
|
|
4195
|
+
(report2) => receiveDirectFallback(c, record, report2, deps.receive)
|
|
3594
4196
|
);
|
|
3595
4197
|
const outcome = await runner.wait(job);
|
|
3596
4198
|
if (outcome.status === "done") failedReceives.delete(input.job);
|
|
@@ -3667,7 +4269,16 @@ function parseArgs(argv) {
|
|
|
3667
4269
|
const inline = arg.startsWith("--") && eq > 0 ? arg.slice(eq + 1) : void 0;
|
|
3668
4270
|
const take = () => inline !== void 0 ? inline : argv[++i] ?? "";
|
|
3669
4271
|
if (name === "pair") parsed.command = "pair";
|
|
3670
|
-
else if (name === "
|
|
4272
|
+
else if (name === "telemetry") {
|
|
4273
|
+
parsed.command = "telemetry";
|
|
4274
|
+
const next = argv[i + 1];
|
|
4275
|
+
if (next === "on" || next === "off") {
|
|
4276
|
+
parsed.telemetry = next;
|
|
4277
|
+
i++;
|
|
4278
|
+
} else if (next !== void 0 && !next.startsWith("--")) {
|
|
4279
|
+
return { ...parsed, command: "invalid", message: `Unknown telemetry setting \u201C${next}\u201D. Use \u201Con\u201D or \u201Coff\u201D.` };
|
|
4280
|
+
}
|
|
4281
|
+
} else if (name === "--version" || name === "-v") parsed.command = "version";
|
|
3671
4282
|
else if (name === "--help" || name === "-h") parsed.command = "help";
|
|
3672
4283
|
else if (name === "--profile") {
|
|
3673
4284
|
parsed.profile = take() || DEFAULT_PROFILE;
|
|
@@ -3690,6 +4301,7 @@ var USAGE = [
|
|
|
3690
4301
|
" zas-agent [--profile <name>] serve the MCP tools over stdio",
|
|
3691
4302
|
" zas-agent pair [--profile <name>] pair this agent with a Zas account",
|
|
3692
4303
|
" [--kind claude_code|codex|other] [--host <name>] [--no-open]",
|
|
4304
|
+
" zas-agent telemetry [on|off] say what this machine reports, or change it",
|
|
3693
4305
|
" zas-agent --version"
|
|
3694
4306
|
].join("\n");
|
|
3695
4307
|
async function main(argv, log = (l) => console.error(l)) {
|
|
@@ -3707,7 +4319,17 @@ async function main(argv, log = (l) => console.error(l)) {
|
|
|
3707
4319
|
log(USAGE);
|
|
3708
4320
|
return args.unknown ? 2 : 0;
|
|
3709
4321
|
}
|
|
4322
|
+
if (args.command === "telemetry") {
|
|
4323
|
+
if (args.telemetry) setTelemetry(args.telemetry === "on");
|
|
4324
|
+
markNoticeShown();
|
|
4325
|
+
log(telemetryLine());
|
|
4326
|
+
log(args.telemetry === "off" ? `Nothing is sent. Turn it on again with: npx -y ${packageName()} telemetry on` : TELEMETRY_NOTICE);
|
|
4327
|
+
return 0;
|
|
4328
|
+
}
|
|
3710
4329
|
if (args.command === "pair") {
|
|
4330
|
+
log(TELEMETRY_NOTICE);
|
|
4331
|
+
log("");
|
|
4332
|
+
markNoticeShown();
|
|
3711
4333
|
let rl = null;
|
|
3712
4334
|
const askCode = process.stdin.isTTY ? () => {
|
|
3713
4335
|
rl ??= createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -3736,6 +4358,10 @@ async function main(argv, log = (l) => console.error(l)) {
|
|
|
3736
4358
|
}
|
|
3737
4359
|
return 0;
|
|
3738
4360
|
}
|
|
4361
|
+
if (!noticeShown()) {
|
|
4362
|
+
log(TELEMETRY_NOTICE);
|
|
4363
|
+
markNoticeShown();
|
|
4364
|
+
}
|
|
3739
4365
|
await buildServer(args.profile).connect(new StdioServerTransport());
|
|
3740
4366
|
return 0;
|
|
3741
4367
|
}
|