zas-agent 0.6.3 → 0.7.0
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 +49 -1
- package/README.md +25 -10
- package/dist/cli.js +179 -72
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-09-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `expires_in_days` on `zas_send_file` and `zas_send_note`. An agent can ask
|
|
15
|
+
an item to live for as little as one whole day — worth setting for output
|
|
16
|
+
that is stale tomorrow, such as a build log, a test run, or a screenshot of
|
|
17
|
+
a bug already fixed, so a channel is not left holding five days of noise.
|
|
18
|
+
It can only ever shorten an item. The server clamps the request to what the
|
|
19
|
+
account's plan grants, so asking for longer is answered with the plan's own
|
|
20
|
+
life rather than refused.
|
|
21
|
+
- The lifetime is part of a send's identity. Both replay guards — the receipt
|
|
22
|
+
on disk and the server's idempotency key — hash it in, so "send that again,
|
|
23
|
+
but for one day" no longer matches the five-day receipt and answers with the
|
|
24
|
+
old item's id, having created nothing. A send that asks for nothing hashes
|
|
25
|
+
exactly as it did before, so every receipt already on disk still matches.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- A Directo send of a file of 4 GiB or more delivered only its first
|
|
30
|
+
gigabytes, and both ends agreed the transfer was whole. Node's `openAsBlob`
|
|
31
|
+
truncates `Blob.size` to 32 bits and its own `slice()` clamps to that
|
|
32
|
+
truncated size, so a 9 GiB file offered 1 GiB and sent 1 GiB. The sender now
|
|
33
|
+
trusts the on-disk size and reads true 64-bit offsets. A file under 4 GiB is
|
|
34
|
+
handed to the engine exactly as it was.
|
|
35
|
+
- A receiver restricted to relay candidates could not gather at all, so a
|
|
36
|
+
browser on a `relay` transport policy never received from an agent. Its ICE
|
|
37
|
+
is trimmed only when it genuinely cannot allocate over UDP.
|
|
38
|
+
- Chromium 152 returns no candidate from an ICE url on port 53, and that url
|
|
39
|
+
also suppresses the UDP relay and the server-reflexive candidate, so a
|
|
40
|
+
browser on that version could not reach an agent at all. The port-53 urls
|
|
41
|
+
are dropped for Chromium 152 and later, under every transport policy.
|
|
42
|
+
- The selected candidate pair is read twice — when ICE connects and when the
|
|
43
|
+
data channel opens — and a local `srflx` is re-identified as `prflx` once
|
|
44
|
+
the peer's check reveals the same address. The summary kept the last read
|
|
45
|
+
and the trace kept the first, so one pair was named two ways. The trace now
|
|
46
|
+
says `pair now …` when the read changes.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- The install and register commands know which shell they are pasted into.
|
|
51
|
+
Windows takes `npx.cmd` and a `cmd /d /c ` prefix, because npm installs
|
|
52
|
+
`claude` and `codex` as PowerShell script shims that keep a bare `--` for
|
|
53
|
+
themselves. Every command now names `@latest`, so npm requests the published
|
|
54
|
+
package instead of selecting an unbuilt local workspace copy. The README
|
|
55
|
+
gives both platforms, and the prerequisites, in order.
|
|
56
|
+
|
|
10
57
|
## [0.6.3] - 2026-09-04
|
|
11
58
|
|
|
12
59
|
### Fixed
|
|
@@ -232,7 +279,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
232
279
|
two separate identities that cannot read each other's keys.
|
|
233
280
|
- Install snippets for Claude Code and Codex, printed by `zas-agent pair`.
|
|
234
281
|
|
|
235
|
-
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.
|
|
282
|
+
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.7.0...HEAD
|
|
283
|
+
[0.7.0]: https://github.com/soke1556/zas-agent/compare/v0.6.3...v0.7.0
|
|
236
284
|
[0.6.3]: https://github.com/soke1556/zas-agent/compare/v0.6.2...v0.6.3
|
|
237
285
|
[0.6.2]: https://github.com/soke1556/zas-agent/compare/v0.6.1...v0.6.2
|
|
238
286
|
[0.6.1]: https://github.com/soke1556/zas-agent/compare/v0.6.0...v0.6.1
|
package/README.md
CHANGED
|
@@ -36,33 +36,48 @@ get a short-lived session.
|
|
|
36
36
|
|
|
37
37
|
## Install
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Install [Node.js LTS](https://nodejs.org/en/download) (22 or newer) and your
|
|
40
|
+
agent's command-line tool first. Reopen your terminal after installing.
|
|
41
|
+
Run these commands on the computer where you use the agent.
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
Choose your agent and pair it. On Windows (PowerShell or CMD):
|
|
44
|
+
|
|
45
|
+
**Codex**
|
|
42
46
|
|
|
43
47
|
```
|
|
44
|
-
npx -y zas-agent pair --profile
|
|
48
|
+
npx.cmd -y zas-agent@latest pair --profile codex
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
**Claude Code**
|
|
48
52
|
|
|
49
53
|
```
|
|
50
|
-
|
|
54
|
+
npx.cmd -y zas-agent@latest pair --profile claude-code
|
|
51
55
|
```
|
|
52
56
|
|
|
57
|
+
Open the printed link, approve the channels, and enter the code in the terminal
|
|
58
|
+
if the page shows one. Leave the terminal open until pairing finishes.
|
|
59
|
+
Then run the matching registration command:
|
|
60
|
+
|
|
53
61
|
**Codex**
|
|
54
62
|
|
|
55
63
|
```
|
|
56
|
-
codex mcp add zas "--" npx -y zas-agent --profile codex
|
|
64
|
+
cmd /d /c codex mcp add zas "--" npx.cmd -y zas-agent@latest --profile codex
|
|
57
65
|
```
|
|
58
66
|
|
|
59
|
-
|
|
67
|
+
**Claude Code**
|
|
60
68
|
|
|
61
|
-
```toml
|
|
62
|
-
[mcp_servers.zas]
|
|
63
|
-
command = "npx"
|
|
64
|
-
args = ["-y", "zas-agent", "--profile", "codex"]
|
|
65
69
|
```
|
|
70
|
+
cmd /d /c claude mcp add zas "--" npx.cmd -y zas-agent@latest --profile claude-code
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
On macOS, Linux, or WSL, use `npx` in place of `npx.cmd` and omit
|
|
74
|
+
`cmd /d /c `. Pair and register in the same environment. Restart your agent
|
|
75
|
+
and ask it to send something to Zas.
|
|
76
|
+
|
|
77
|
+
If a command is not recognized, install Node.js or the named agent's CLI and
|
|
78
|
+
reopen the terminal. The first download needs internet access and may take a
|
|
79
|
+
few minutes. Use `@latest` as shown so npm requests the published package
|
|
80
|
+
instead of selecting an unbuilt local workspace.
|
|
66
81
|
|
|
67
82
|
### What pairing does
|
|
68
83
|
|
package/dist/cli.js
CHANGED
|
@@ -598,15 +598,27 @@ var ZasClient = class _ZasClient {
|
|
|
598
598
|
}
|
|
599
599
|
};
|
|
600
600
|
|
|
601
|
+
// src/shared/agent-snippets.ts
|
|
602
|
+
function agentSnippets(profile, windows = false, pkg = "zas-agent") {
|
|
603
|
+
const runner = windows ? "npx.cmd" : "npx";
|
|
604
|
+
const command = `${runner} -y ${pkg}@latest`;
|
|
605
|
+
const shell = windows ? "cmd /d /c " : "";
|
|
606
|
+
return {
|
|
607
|
+
pair: `${command} pair --profile ${profile}`,
|
|
608
|
+
claude: `${shell}claude mcp add zas "--" ${command} --profile ${profile}`,
|
|
609
|
+
codex: `${shell}codex mcp add zas "--" ${command} --profile ${profile}`
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
601
613
|
// src/snippets.ts
|
|
602
614
|
function packageName() {
|
|
603
615
|
return true ? "zas-agent" : "zas-agent";
|
|
604
616
|
}
|
|
605
|
-
function claudeSnippet(profile) {
|
|
606
|
-
return
|
|
617
|
+
function claudeSnippet(profile, windows = process.platform === "win32") {
|
|
618
|
+
return agentSnippets(profile, windows, packageName()).claude;
|
|
607
619
|
}
|
|
608
|
-
function codexSnippet(profile) {
|
|
609
|
-
return
|
|
620
|
+
function codexSnippet(profile, windows = process.platform === "win32") {
|
|
621
|
+
return agentSnippets(profile, windows, packageName()).codex;
|
|
610
622
|
}
|
|
611
623
|
function kindForProfile(profile) {
|
|
612
624
|
if (profile === "claude-code") return "claude_code";
|
|
@@ -830,6 +842,41 @@ async function runPair(opts) {
|
|
|
830
842
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
831
843
|
import { z } from "zod";
|
|
832
844
|
|
|
845
|
+
// src/shared/constants.ts
|
|
846
|
+
var CHUNKER_VERSION = "chunker_v1";
|
|
847
|
+
var CHUNK_MIN = 1 * 1024 * 1024;
|
|
848
|
+
var CHUNK_AVG_BITS = 22;
|
|
849
|
+
var CHUNK_MAX = 8 * 1024 * 1024;
|
|
850
|
+
var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
|
|
851
|
+
var GEAR_SEED = "zas-gear-v1:";
|
|
852
|
+
var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
|
|
853
|
+
var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
|
|
854
|
+
var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
|
|
855
|
+
var MANIFEST_VERSION = 1;
|
|
856
|
+
var MIN_EXPIRY_DAYS = 1;
|
|
857
|
+
var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
|
|
858
|
+
var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
|
|
859
|
+
var SAFE_STORED_FILE_LIMITS = {
|
|
860
|
+
anon: 30 * 1024 * 1024,
|
|
861
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
862
|
+
pro: 200 * 1024 * 1024,
|
|
863
|
+
max: 200 * 1024 * 1024
|
|
864
|
+
};
|
|
865
|
+
var STORED_FILE_LIMITS_V2 = {
|
|
866
|
+
anon: 30 * 1024 * 1024,
|
|
867
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
868
|
+
pro: 5 * 1024 * 1024 * 1024,
|
|
869
|
+
max: 20 * 1024 * 1024 * 1024
|
|
870
|
+
};
|
|
871
|
+
var STORAGE_LIMITS = {
|
|
872
|
+
anon: 30 * 1024 * 1024,
|
|
873
|
+
free: 100 * 1024 * 1024,
|
|
874
|
+
pro: null,
|
|
875
|
+
max: null
|
|
876
|
+
};
|
|
877
|
+
var THUMBNAIL_MAX_EDGE = 640;
|
|
878
|
+
var THUMBNAIL_MAX_BYTES = 14e4;
|
|
879
|
+
|
|
833
880
|
// src/shared/direct.ts
|
|
834
881
|
var DIRECT_OFFER_TTL_MS = 10 * 60 * 1e3;
|
|
835
882
|
var DIRECT_CLAIMED_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -971,6 +1018,29 @@ function traceAdd(diag, line) {
|
|
|
971
1018
|
const at = t0 === void 0 ? 0 : (Date.now() - t0) / 1e3;
|
|
972
1019
|
diag.trace.push(`${at.toFixed(2)} ${line}`);
|
|
973
1020
|
}
|
|
1021
|
+
function usableForRelay(url) {
|
|
1022
|
+
const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase();
|
|
1023
|
+
if (scheme !== "turn" && scheme !== "turns") return false;
|
|
1024
|
+
return !onPort53(url);
|
|
1025
|
+
}
|
|
1026
|
+
function onPort53(url) {
|
|
1027
|
+
return /^(stuns?|turns?):[^?]*:53(\?|$)/i.test(url);
|
|
1028
|
+
}
|
|
1029
|
+
function withoutPort53(servers) {
|
|
1030
|
+
return keepUrls(servers, (url) => !onPort53(url));
|
|
1031
|
+
}
|
|
1032
|
+
function relayIceServers(servers) {
|
|
1033
|
+
return keepUrls(servers, usableForRelay);
|
|
1034
|
+
}
|
|
1035
|
+
function keepUrls(servers, keep) {
|
|
1036
|
+
const kept = [];
|
|
1037
|
+
for (const server of servers) {
|
|
1038
|
+
const urls = typeof server.urls === "string" ? [server.urls] : server.urls ?? [];
|
|
1039
|
+
const usable = urls.filter(keep);
|
|
1040
|
+
if (usable.length > 0) kept.push({ ...server, urls: usable });
|
|
1041
|
+
}
|
|
1042
|
+
return kept;
|
|
1043
|
+
}
|
|
974
1044
|
function urlShape(url) {
|
|
975
1045
|
const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase() ?? "other";
|
|
976
1046
|
const transport = /[?&]transport=(\w+)/i.exec(url)?.[1]?.toLowerCase();
|
|
@@ -1061,8 +1131,14 @@ function pathOf(pair) {
|
|
|
1061
1131
|
function hasRemoteDescription(pc) {
|
|
1062
1132
|
return !!pc.remoteDescription?.sdp;
|
|
1063
1133
|
}
|
|
1064
|
-
function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
1065
|
-
const
|
|
1134
|
+
function session(ice, onPhase, onDiag, iceTransportPolicy = "all", skipPort53Urls = false) {
|
|
1135
|
+
const trimIce = (servers) => {
|
|
1136
|
+
if (!skipPort53Urls) return servers;
|
|
1137
|
+
if (iceTransportPolicy !== "relay") return withoutPort53(servers);
|
|
1138
|
+
const trimmed = relayIceServers(servers);
|
|
1139
|
+
return countTurnUrls(trimmed) > 0 ? trimmed : servers;
|
|
1140
|
+
};
|
|
1141
|
+
const initialIce = trimIce(ice ?? DIRECT_ICE);
|
|
1066
1142
|
const pc = new RTCPeerConnection({
|
|
1067
1143
|
iceServers: initialIce,
|
|
1068
1144
|
iceTransportPolicy,
|
|
@@ -1103,6 +1179,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1103
1179
|
};
|
|
1104
1180
|
traceStart.set(diag, startedAt);
|
|
1105
1181
|
traceAdd(diag, `ice servers ${iceShapes(initialIce)} policy=${iceTransportPolicy}`);
|
|
1182
|
+
const notePair = (pair) => {
|
|
1183
|
+
if (diag.pairLocal === pair.local && diag.pairRemote === pair.remote) return;
|
|
1184
|
+
const changed = diag.pairLocal !== void 0;
|
|
1185
|
+
diag.pairLocal = pair.local;
|
|
1186
|
+
diag.pairRemote = pair.remote;
|
|
1187
|
+
traceAdd(diag, `${changed ? "pair now" : "pair"} ${pair.local}/${pair.remote}`);
|
|
1188
|
+
};
|
|
1106
1189
|
pc.onicegatheringstatechange = () => {
|
|
1107
1190
|
traceAdd(diag, `gather ${String(pc.iceGatheringState ?? "")}`);
|
|
1108
1191
|
};
|
|
@@ -1230,10 +1313,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1230
1313
|
stallState = "idle";
|
|
1231
1314
|
void pc.getStats().then((stats) => {
|
|
1232
1315
|
const pair = pairFromStats(stats);
|
|
1233
|
-
if (
|
|
1234
|
-
diag.pairLocal = pair.local;
|
|
1235
|
-
diag.pairRemote = pair.remote;
|
|
1236
|
-
traceAdd(diag, `pair ${pair.local}/${pair.remote}`);
|
|
1316
|
+
if (pair) notePair(pair);
|
|
1237
1317
|
}).catch(() => void 0);
|
|
1238
1318
|
}
|
|
1239
1319
|
};
|
|
@@ -1258,10 +1338,12 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1258
1338
|
onStall = fn;
|
|
1259
1339
|
stallWindowMs = windowMs;
|
|
1260
1340
|
},
|
|
1341
|
+
notePair,
|
|
1261
1342
|
setIceServers: (iceServers) => {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1343
|
+
const next = trimIce(iceServers);
|
|
1344
|
+
diag.turnUrlsSupplied = countTurnUrls(next);
|
|
1345
|
+
traceAdd(diag, `ice servers replaced ${iceShapes(next)}`);
|
|
1346
|
+
pc.setConfiguration({ iceServers: next });
|
|
1265
1347
|
}
|
|
1266
1348
|
};
|
|
1267
1349
|
}
|
|
@@ -1329,7 +1411,13 @@ function signalPlumbing(pc, send, onSendError, diag) {
|
|
|
1329
1411
|
};
|
|
1330
1412
|
}
|
|
1331
1413
|
function startSender(opts) {
|
|
1332
|
-
const s = session(
|
|
1414
|
+
const s = session(
|
|
1415
|
+
opts.ice,
|
|
1416
|
+
opts.onPhase,
|
|
1417
|
+
opts.onDiag,
|
|
1418
|
+
opts.iceTransportPolicy,
|
|
1419
|
+
opts.skipPort53Urls
|
|
1420
|
+
);
|
|
1333
1421
|
opts.onPhase("connecting");
|
|
1334
1422
|
const { pc } = s;
|
|
1335
1423
|
const plumbing = signalPlumbing(pc, opts.send, () => s.fail("signal_send"), s.diag);
|
|
@@ -1411,8 +1499,7 @@ function startSender(opts) {
|
|
|
1411
1499
|
void pc.getStats().then((stats) => {
|
|
1412
1500
|
const pair = pairFromStats(stats);
|
|
1413
1501
|
if (!pair) return;
|
|
1414
|
-
s.
|
|
1415
|
-
s.diag.pairRemote = pair.remote;
|
|
1502
|
+
s.notePair(pair);
|
|
1416
1503
|
opts.onPath?.(pathOf(pair));
|
|
1417
1504
|
}).catch(() => void 0);
|
|
1418
1505
|
void pump().catch((e) => s.fail("transfer", e));
|
|
@@ -1482,7 +1569,8 @@ function startReceiver(opts) {
|
|
|
1482
1569
|
opts.onPhase(p);
|
|
1483
1570
|
},
|
|
1484
1571
|
opts.onDiag,
|
|
1485
|
-
opts.iceTransportPolicy
|
|
1572
|
+
opts.iceTransportPolicy,
|
|
1573
|
+
opts.skipPort53Urls
|
|
1486
1574
|
);
|
|
1487
1575
|
s.setStall(() => void 0, DIRECT_CONNECT_TIMEOUT_MS * 2);
|
|
1488
1576
|
opts.onPhase("connecting");
|
|
@@ -1508,8 +1596,7 @@ function startReceiver(opts) {
|
|
|
1508
1596
|
void pc.getStats().then((stats) => {
|
|
1509
1597
|
const pair = pairFromStats(stats);
|
|
1510
1598
|
if (!pair) return;
|
|
1511
|
-
s.
|
|
1512
|
-
s.diag.pairRemote = pair.remote;
|
|
1599
|
+
s.notePair(pair);
|
|
1513
1600
|
opts.onPath?.(pathOf(pair));
|
|
1514
1601
|
}).catch(() => void 0);
|
|
1515
1602
|
};
|
|
@@ -1624,42 +1711,6 @@ import { basename as basename2 } from "node:path";
|
|
|
1624
1711
|
|
|
1625
1712
|
// src/shared/manifest.ts
|
|
1626
1713
|
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
1627
|
-
|
|
1628
|
-
// src/shared/constants.ts
|
|
1629
|
-
var CHUNKER_VERSION = "chunker_v1";
|
|
1630
|
-
var CHUNK_MIN = 1 * 1024 * 1024;
|
|
1631
|
-
var CHUNK_AVG_BITS = 22;
|
|
1632
|
-
var CHUNK_MAX = 8 * 1024 * 1024;
|
|
1633
|
-
var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
|
|
1634
|
-
var GEAR_SEED = "zas-gear-v1:";
|
|
1635
|
-
var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
|
|
1636
|
-
var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
|
|
1637
|
-
var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
|
|
1638
|
-
var MANIFEST_VERSION = 1;
|
|
1639
|
-
var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
|
|
1640
|
-
var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
|
|
1641
|
-
var SAFE_STORED_FILE_LIMITS = {
|
|
1642
|
-
anon: 30 * 1024 * 1024,
|
|
1643
|
-
free: FREE_RELAY_MAX_BYTES,
|
|
1644
|
-
pro: 200 * 1024 * 1024,
|
|
1645
|
-
max: 200 * 1024 * 1024
|
|
1646
|
-
};
|
|
1647
|
-
var STORED_FILE_LIMITS_V2 = {
|
|
1648
|
-
anon: 30 * 1024 * 1024,
|
|
1649
|
-
free: FREE_RELAY_MAX_BYTES,
|
|
1650
|
-
pro: 5 * 1024 * 1024 * 1024,
|
|
1651
|
-
max: 20 * 1024 * 1024 * 1024
|
|
1652
|
-
};
|
|
1653
|
-
var STORAGE_LIMITS = {
|
|
1654
|
-
anon: 30 * 1024 * 1024,
|
|
1655
|
-
free: 100 * 1024 * 1024,
|
|
1656
|
-
pro: null,
|
|
1657
|
-
max: null
|
|
1658
|
-
};
|
|
1659
|
-
var THUMBNAIL_MAX_EDGE = 640;
|
|
1660
|
-
var THUMBNAIL_MAX_BYTES = 14e4;
|
|
1661
|
-
|
|
1662
|
-
// src/shared/manifest.ts
|
|
1663
1714
|
function newManifest(partial) {
|
|
1664
1715
|
return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
|
|
1665
1716
|
}
|
|
@@ -1700,6 +1751,46 @@ function decryptChannelName(channelKey, sealed) {
|
|
|
1700
1751
|
return new TextDecoder().decode(openSealed(channelKey, sealed));
|
|
1701
1752
|
}
|
|
1702
1753
|
|
|
1754
|
+
// src/file-source.ts
|
|
1755
|
+
import { promises as fsp, statSync } from "node:fs";
|
|
1756
|
+
async function readFileRange(path, start, length) {
|
|
1757
|
+
const fh = await fsp.open(path, "r");
|
|
1758
|
+
try {
|
|
1759
|
+
const buf = Buffer.allocUnsafe(Math.max(0, length));
|
|
1760
|
+
let read = 0;
|
|
1761
|
+
while (read < length) {
|
|
1762
|
+
const { bytesRead } = await fh.read(buf, read, length - read, start + read);
|
|
1763
|
+
if (bytesRead === 0) break;
|
|
1764
|
+
read += bytesRead;
|
|
1765
|
+
}
|
|
1766
|
+
const out = new Uint8Array(read);
|
|
1767
|
+
out.set(buf.subarray(0, read));
|
|
1768
|
+
return out.buffer;
|
|
1769
|
+
} finally {
|
|
1770
|
+
await fh.close();
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function directFileFrom(path, raw) {
|
|
1774
|
+
let statSize;
|
|
1775
|
+
try {
|
|
1776
|
+
statSize = statSync(path).size;
|
|
1777
|
+
} catch {
|
|
1778
|
+
return raw;
|
|
1779
|
+
}
|
|
1780
|
+
if (statSize <= raw.size) return raw;
|
|
1781
|
+
const type = raw.type;
|
|
1782
|
+
const file = {
|
|
1783
|
+
size: statSize,
|
|
1784
|
+
type,
|
|
1785
|
+
slice(start, end) {
|
|
1786
|
+
const from = Math.max(0, Math.min(start, statSize));
|
|
1787
|
+
const to = Math.max(from, Math.min(end, statSize));
|
|
1788
|
+
return { arrayBuffer: () => readFileRange(path, from, to - from) };
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
return file;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1703
1794
|
// src/shared/direct-fallback.ts
|
|
1704
1795
|
function createFallbackMeta(file) {
|
|
1705
1796
|
const key = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -2006,7 +2097,7 @@ function resolveChannel(identity, grants, channel) {
|
|
|
2006
2097
|
}
|
|
2007
2098
|
|
|
2008
2099
|
// src/send.ts
|
|
2009
|
-
import { promises as
|
|
2100
|
+
import { promises as fsp2 } from "node:fs";
|
|
2010
2101
|
import { basename, extname } from "node:path";
|
|
2011
2102
|
|
|
2012
2103
|
// src/shared/chunker.ts
|
|
@@ -2282,6 +2373,10 @@ async function receiptKey(ctx, channelId, contentHash, title) {
|
|
|
2282
2373
|
const raw = `${ctx.identity.agent_uid}\0${channelId}\0${contentHash}\0${title}`;
|
|
2283
2374
|
return blake3Hex(new TextEncoder().encode(raw));
|
|
2284
2375
|
}
|
|
2376
|
+
async function sendIdentity(contentHash, days) {
|
|
2377
|
+
if (days === void 0) return contentHash;
|
|
2378
|
+
return blake3Hex(new TextEncoder().encode(`${contentHash}\0expires_in_days:${days}`));
|
|
2379
|
+
}
|
|
2285
2380
|
function receiptFor(ctx, key) {
|
|
2286
2381
|
const entries = loadFingerprints(ctx.profile).entries;
|
|
2287
2382
|
if (!Object.prototype.hasOwnProperty.call(entries, key)) return void 0;
|
|
@@ -2430,7 +2525,7 @@ async function placeChunks(ctx, encs) {
|
|
|
2430
2525
|
};
|
|
2431
2526
|
});
|
|
2432
2527
|
}
|
|
2433
|
-
async function postLink(ctx, grant, manifest, placed, idempotencyKey) {
|
|
2528
|
+
async function postLink(ctx, grant, manifest, placed, idempotencyKey, expiresInDays) {
|
|
2434
2529
|
const channelKey = channelKeyOf(ctx.identity, grant);
|
|
2435
2530
|
const sealed = sealManifest(channelKey, manifest, grant.key_version);
|
|
2436
2531
|
const created = await ctx.client.api("POST", "/links", {
|
|
@@ -2441,7 +2536,10 @@ async function postLink(ctx, grant, manifest, placed, idempotencyKey) {
|
|
|
2441
2536
|
// nothing left for the server to verify here. The field stays because the
|
|
2442
2537
|
// server reads it, and an absent array is not the same as an empty one.
|
|
2443
2538
|
proofs: [],
|
|
2444
|
-
idempotency_key: idempotencyKey
|
|
2539
|
+
idempotency_key: idempotencyKey,
|
|
2540
|
+
// Absent unless the caller asked. The server clamps whatever arrives to
|
|
2541
|
+
// the account's own plan, so this can only ever shorten the item's life.
|
|
2542
|
+
...expiresInDays === void 0 ? {} : { expires_in_days: expiresInDays }
|
|
2445
2543
|
});
|
|
2446
2544
|
const bound = created.caps ?? {};
|
|
2447
2545
|
let patched = false;
|
|
@@ -2460,7 +2558,7 @@ async function postLink(ctx, grant, manifest, placed, idempotencyKey) {
|
|
|
2460
2558
|
return created.link_id;
|
|
2461
2559
|
}
|
|
2462
2560
|
async function sendFile(ctx, input, onPhase) {
|
|
2463
|
-
const stat = await
|
|
2561
|
+
const stat = await fsp2.stat(input.path).catch(() => {
|
|
2464
2562
|
throw new ZasError("upload_failed", 400);
|
|
2465
2563
|
});
|
|
2466
2564
|
if (!stat.isFile()) throw new ZasError("upload_failed", 400);
|
|
@@ -2468,10 +2566,10 @@ async function sendFile(ctx, input, onPhase) {
|
|
|
2468
2566
|
const grant = await grantFor(ctx, input.channel);
|
|
2469
2567
|
const name = basename(input.path);
|
|
2470
2568
|
const title = input.title ?? name;
|
|
2471
|
-
const file = await
|
|
2569
|
+
const file = await fsp2.readFile(input.path);
|
|
2472
2570
|
if (file.byteLength > MAX_FILE_BYTES) throw new ZasError("file_too_big", 413);
|
|
2473
2571
|
const bytes = new Uint8Array(file.buffer, file.byteOffset, file.byteLength);
|
|
2474
|
-
const contentHash = await blake3Hex(bytes);
|
|
2572
|
+
const contentHash = await sendIdentity(await blake3Hex(bytes), input.expires_in_days);
|
|
2475
2573
|
const key = await receiptKey(ctx, grant.channel_id, contentHash, title);
|
|
2476
2574
|
const stored = receiptFor(ctx, key);
|
|
2477
2575
|
if (stored) {
|
|
@@ -2522,7 +2620,8 @@ async function sendFile(ctx, input, onPhase) {
|
|
|
2522
2620
|
grant,
|
|
2523
2621
|
manifest,
|
|
2524
2622
|
placed,
|
|
2525
|
-
agentSendIdempotencyKey(grant.channel_id, contentHash, title)
|
|
2623
|
+
agentSendIdempotencyKey(grant.channel_id, contentHash, title),
|
|
2624
|
+
input.expires_in_days
|
|
2526
2625
|
);
|
|
2527
2626
|
const deduplicated = placed.filter((p) => p.proven).length;
|
|
2528
2627
|
remember(ctx, key, {
|
|
@@ -2549,9 +2648,12 @@ async function sendNote(ctx, input) {
|
|
|
2549
2648
|
const grant = await grantFor(ctx, input.channel);
|
|
2550
2649
|
const name = input.title ?? noteName(input.text);
|
|
2551
2650
|
const encoded = new TextEncoder().encode(input.text);
|
|
2552
|
-
const contentHash = await
|
|
2553
|
-
|
|
2554
|
-
|
|
2651
|
+
const contentHash = await sendIdentity(
|
|
2652
|
+
await blake3Hex(new TextEncoder().encode(
|
|
2653
|
+
`${input.lang ?? ""}\0${input.secret ? "1" : "0"}\0${input.text}`
|
|
2654
|
+
)),
|
|
2655
|
+
input.expires_in_days
|
|
2656
|
+
);
|
|
2555
2657
|
const key = await receiptKey(ctx, grant.channel_id, contentHash, name);
|
|
2556
2658
|
const stored = receiptFor(ctx, key);
|
|
2557
2659
|
if (stored) {
|
|
@@ -2584,7 +2686,8 @@ async function sendNote(ctx, input) {
|
|
|
2584
2686
|
grant,
|
|
2585
2687
|
manifest,
|
|
2586
2688
|
[],
|
|
2587
|
-
agentSendIdempotencyKey(grant.channel_id, contentHash, name)
|
|
2689
|
+
agentSendIdempotencyKey(grant.channel_id, contentHash, name),
|
|
2690
|
+
input.expires_in_days
|
|
2588
2691
|
);
|
|
2589
2692
|
remember(ctx, key, {
|
|
2590
2693
|
link_id: linkId,
|
|
@@ -2659,9 +2762,10 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2659
2762
|
const grant = await directGrantFor(ctx, input.channel);
|
|
2660
2763
|
const key = channelKeyOf(ctx.identity, grant);
|
|
2661
2764
|
const channelName = channelLabel(ctx, grant);
|
|
2662
|
-
const
|
|
2765
|
+
const raw = await (deps.openFile ?? openAsBlob)(input.path, { type: mimeFor(input.path) }).catch(() => {
|
|
2663
2766
|
throw new ZasError("upload_failed", 400);
|
|
2664
2767
|
});
|
|
2768
|
+
const file = directFileFrom(input.path, raw);
|
|
2665
2769
|
if (file.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
|
|
2666
2770
|
const name = basename2(input.path);
|
|
2667
2771
|
const cid = grant.channel_id;
|
|
@@ -2882,7 +2986,7 @@ import {
|
|
|
2882
2986
|
openSync,
|
|
2883
2987
|
renameSync as renameSync2,
|
|
2884
2988
|
rmdirSync,
|
|
2885
|
-
statSync,
|
|
2989
|
+
statSync as statSync2,
|
|
2886
2990
|
unlinkSync,
|
|
2887
2991
|
writeSync
|
|
2888
2992
|
} from "node:fs";
|
|
@@ -3055,7 +3159,7 @@ function destinationOf(dest, name, fallback) {
|
|
|
3055
3159
|
const created = mkdtempSync(join2(tmpdir(), DOWNLOAD_PREFIX));
|
|
3056
3160
|
return { target: freeName(join2(created, base)), created };
|
|
3057
3161
|
}
|
|
3058
|
-
const at =
|
|
3162
|
+
const at = statSync2(dest, { throwIfNoEntry: false })?.isDirectory() ? join2(dest, base) : dest;
|
|
3059
3163
|
return { target: freeName(at) };
|
|
3060
3164
|
}
|
|
3061
3165
|
async function getItem(ctx, channel, id, dest) {
|
|
@@ -3984,11 +4088,12 @@ async function report(client, properties) {
|
|
|
3984
4088
|
|
|
3985
4089
|
// src/server.ts
|
|
3986
4090
|
function agentVersion() {
|
|
3987
|
-
return true ? "0.
|
|
4091
|
+
return true ? "0.7.0" : "0.0.0-dev";
|
|
3988
4092
|
}
|
|
3989
4093
|
var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
|
|
3990
4094
|
var PAIR_ANNOUNCE_MS = 15e3;
|
|
3991
4095
|
var NOTE_LABEL_MAX = 40;
|
|
4096
|
+
var EXPIRES_IN_DAYS = z.number().int().min(MIN_EXPIRY_DAYS).optional().describe("How many whole days the item should live for, at least 1. Leave it out for the account's normal life (5 days). It can only shorten an item, never extend one: a longer request is clamped to what the plan grants. Worth setting for output that is stale tomorrow, such as a build log or a test run.");
|
|
3992
4097
|
var delay = (ms) => new Promise((resolve2) => {
|
|
3993
4098
|
setTimeout(resolve2, ms);
|
|
3994
4099
|
});
|
|
@@ -4165,7 +4270,8 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
|
|
|
4165
4270
|
inputSchema: {
|
|
4166
4271
|
path: z.string().describe("Absolute or relative path of the file to send."),
|
|
4167
4272
|
channel: z.string().optional().describe("Channel name or id. Optional when the agent holds exactly one channel."),
|
|
4168
|
-
title: z.string().optional().describe("Label for the item. Defaults to the file name.")
|
|
4273
|
+
title: z.string().optional().describe("Label for the item. Defaults to the file name."),
|
|
4274
|
+
expires_in_days: EXPIRES_IN_DAYS
|
|
4169
4275
|
}
|
|
4170
4276
|
}, async (input) => {
|
|
4171
4277
|
try {
|
|
@@ -4294,7 +4400,8 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
|
|
|
4294
4400
|
channel: z.string().optional().describe("Channel name or id. Optional when the agent holds exactly one channel."),
|
|
4295
4401
|
title: z.string().optional().describe("Label for the item. Defaults to the first line."),
|
|
4296
4402
|
lang: z.string().optional().describe('Language of the snippet, for highlighting (for example "ts", "py").'),
|
|
4297
|
-
secret: z.boolean().optional().describe("Hide the body behind a cover until the reader opens it.")
|
|
4403
|
+
secret: z.boolean().optional().describe("Hide the body behind a cover until the reader opens it."),
|
|
4404
|
+
expires_in_days: EXPIRES_IN_DAYS
|
|
4298
4405
|
}
|
|
4299
4406
|
}, async (input) => {
|
|
4300
4407
|
try {
|
package/package.json
CHANGED