zas-agent 0.6.3 → 0.8.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 +74 -1
- package/README.md +25 -10
- package/dist/cli.js +198 -79
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,77 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-09-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- A channel you joined, but did not create, can now be granted to an agent.
|
|
15
|
+
Its owner has to allow the agent first. Until they answer, the grant is
|
|
16
|
+
written and waiting: the key is real and it opens nothing.
|
|
17
|
+
- `grant_pending`. A tool call on a channel whose owner has not answered says
|
|
18
|
+
so, and says to ask them, rather than reporting the grant as missing.
|
|
19
|
+
- `foreign_limit` on pairing. A plan allows an agent a number of channels of
|
|
20
|
+
other people, and the free plan allows fewer of those than of your own.
|
|
21
|
+
- `zas_status` marks such a channel as waiting for its owner.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Reading a channel somebody else created said `read_forbidden` on a grant
|
|
26
|
+
that plainly said `send · read`, while sending into the same channel
|
|
27
|
+
worked. Sending goes through the API, which knows which account holds a
|
|
28
|
+
channel; reading goes to Firestore directly, and the agent looked under
|
|
29
|
+
its own owner, where a shared channel is not kept. The server now says
|
|
30
|
+
where each channel lives, and reads, Directo offers and Directo fallbacks
|
|
31
|
+
all address that account. A channel of your own is unchanged: it is in
|
|
32
|
+
your own account, which is where it was always read from.
|
|
33
|
+
|
|
34
|
+
## [0.7.0] - 2026-09-05
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `expires_in_days` on `zas_send_file` and `zas_send_note`. An agent can ask
|
|
39
|
+
an item to live for as little as one whole day — worth setting for output
|
|
40
|
+
that is stale tomorrow, such as a build log, a test run, or a screenshot of
|
|
41
|
+
a bug already fixed, so a channel is not left holding five days of noise.
|
|
42
|
+
It can only ever shorten an item. The server clamps the request to what the
|
|
43
|
+
account's plan grants, so asking for longer is answered with the plan's own
|
|
44
|
+
life rather than refused.
|
|
45
|
+
- The lifetime is part of a send's identity. Both replay guards — the receipt
|
|
46
|
+
on disk and the server's idempotency key — hash it in, so "send that again,
|
|
47
|
+
but for one day" no longer matches the five-day receipt and answers with the
|
|
48
|
+
old item's id, having created nothing. A send that asks for nothing hashes
|
|
49
|
+
exactly as it did before, so every receipt already on disk still matches.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- A Directo send of a file of 4 GiB or more delivered only its first
|
|
54
|
+
gigabytes, and both ends agreed the transfer was whole. Node's `openAsBlob`
|
|
55
|
+
truncates `Blob.size` to 32 bits and its own `slice()` clamps to that
|
|
56
|
+
truncated size, so a 9 GiB file offered 1 GiB and sent 1 GiB. The sender now
|
|
57
|
+
trusts the on-disk size and reads true 64-bit offsets. A file under 4 GiB is
|
|
58
|
+
handed to the engine exactly as it was.
|
|
59
|
+
- A receiver restricted to relay candidates could not gather at all, so a
|
|
60
|
+
browser on a `relay` transport policy never received from an agent. Its ICE
|
|
61
|
+
is trimmed only when it genuinely cannot allocate over UDP.
|
|
62
|
+
- Chromium 152 returns no candidate from an ICE url on port 53, and that url
|
|
63
|
+
also suppresses the UDP relay and the server-reflexive candidate, so a
|
|
64
|
+
browser on that version could not reach an agent at all. The port-53 urls
|
|
65
|
+
are dropped for Chromium 152 and later, under every transport policy.
|
|
66
|
+
- The selected candidate pair is read twice — when ICE connects and when the
|
|
67
|
+
data channel opens — and a local `srflx` is re-identified as `prflx` once
|
|
68
|
+
the peer's check reveals the same address. The summary kept the last read
|
|
69
|
+
and the trace kept the first, so one pair was named two ways. The trace now
|
|
70
|
+
says `pair now …` when the read changes.
|
|
71
|
+
|
|
72
|
+
### Changed
|
|
73
|
+
|
|
74
|
+
- The install and register commands know which shell they are pasted into.
|
|
75
|
+
Windows takes `npx.cmd` and a `cmd /d /c ` prefix, because npm installs
|
|
76
|
+
`claude` and `codex` as PowerShell script shims that keep a bare `--` for
|
|
77
|
+
themselves. Every command now names `@latest`, so npm requests the published
|
|
78
|
+
package instead of selecting an unbuilt local workspace copy. The README
|
|
79
|
+
gives both platforms, and the prerequisites, in order.
|
|
80
|
+
|
|
10
81
|
## [0.6.3] - 2026-09-04
|
|
11
82
|
|
|
12
83
|
### Fixed
|
|
@@ -232,7 +303,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
232
303
|
two separate identities that cannot read each other's keys.
|
|
233
304
|
- Install snippets for Claude Code and Codex, printed by `zas-agent pair`.
|
|
234
305
|
|
|
235
|
-
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.
|
|
306
|
+
[Unreleased]: https://github.com/soke1556/zas-agent/compare/v0.8.0...HEAD
|
|
307
|
+
[0.8.0]: https://github.com/soke1556/zas-agent/compare/v0.7.0...v0.8.0
|
|
308
|
+
[0.7.0]: https://github.com/soke1556/zas-agent/compare/v0.6.3...v0.7.0
|
|
236
309
|
[0.6.3]: https://github.com/soke1556/zas-agent/compare/v0.6.2...v0.6.3
|
|
237
310
|
[0.6.2]: https://github.com/soke1556/zas-agent/compare/v0.6.1...v0.6.2
|
|
238
311
|
[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
|
@@ -159,6 +159,7 @@ var SENTENCES = {
|
|
|
159
159
|
agent_forbidden: "This agent cannot do that; only the account owner can.",
|
|
160
160
|
agent_revoked: "The owner revoked this agent. Pair again with \u201Czas-agent pair\u201D.",
|
|
161
161
|
grant_missing: "This agent has no access to that channel. The owner adds it from Settings \u2192 Agents.",
|
|
162
|
+
grant_pending: "That channel belongs to somebody else, and its owner has not allowed this agent yet. Ask them to allow it in Zas.",
|
|
162
163
|
not_found: "That item is not in the channel.",
|
|
163
164
|
invalid_cap: "That file is no longer available.",
|
|
164
165
|
write_failed: "The file could not be saved to the destination.",
|
|
@@ -187,6 +188,7 @@ var SENTENCES = {
|
|
|
187
188
|
pairing_claimed: "This pairing was already claimed. Pair again with \u201Czas-agent pair\u201D.",
|
|
188
189
|
agent_limit: "The account cannot take another agent. The owner deletes one from Settings \u2192 Agents.",
|
|
189
190
|
grant_limit: "The plan allows fewer channels per agent than this pairing grants.",
|
|
191
|
+
foreign_limit: "The plan allows this agent fewer channels of other people than this pairing asks for.",
|
|
190
192
|
feature_disabled: "Agents are not enabled for this account yet.",
|
|
191
193
|
upload_failed: "The upload failed. Try again.",
|
|
192
194
|
oprf_failed: "Zas did not answer correctly while preparing the file. Try again.",
|
|
@@ -598,15 +600,27 @@ var ZasClient = class _ZasClient {
|
|
|
598
600
|
}
|
|
599
601
|
};
|
|
600
602
|
|
|
603
|
+
// src/shared/agent-snippets.ts
|
|
604
|
+
function agentSnippets(profile, windows = false, pkg = "zas-agent") {
|
|
605
|
+
const runner = windows ? "npx.cmd" : "npx";
|
|
606
|
+
const command = `${runner} -y ${pkg}@latest`;
|
|
607
|
+
const shell = windows ? "cmd /d /c " : "";
|
|
608
|
+
return {
|
|
609
|
+
pair: `${command} pair --profile ${profile}`,
|
|
610
|
+
claude: `${shell}claude mcp add zas "--" ${command} --profile ${profile}`,
|
|
611
|
+
codex: `${shell}codex mcp add zas "--" ${command} --profile ${profile}`
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
601
615
|
// src/snippets.ts
|
|
602
616
|
function packageName() {
|
|
603
617
|
return true ? "zas-agent" : "zas-agent";
|
|
604
618
|
}
|
|
605
|
-
function claudeSnippet(profile) {
|
|
606
|
-
return
|
|
619
|
+
function claudeSnippet(profile, windows = process.platform === "win32") {
|
|
620
|
+
return agentSnippets(profile, windows, packageName()).claude;
|
|
607
621
|
}
|
|
608
|
-
function codexSnippet(profile) {
|
|
609
|
-
return
|
|
622
|
+
function codexSnippet(profile, windows = process.platform === "win32") {
|
|
623
|
+
return agentSnippets(profile, windows, packageName()).codex;
|
|
610
624
|
}
|
|
611
625
|
function kindForProfile(profile) {
|
|
612
626
|
if (profile === "claude-code") return "claude_code";
|
|
@@ -830,6 +844,41 @@ async function runPair(opts) {
|
|
|
830
844
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
831
845
|
import { z } from "zod";
|
|
832
846
|
|
|
847
|
+
// src/shared/constants.ts
|
|
848
|
+
var CHUNKER_VERSION = "chunker_v1";
|
|
849
|
+
var CHUNK_MIN = 1 * 1024 * 1024;
|
|
850
|
+
var CHUNK_AVG_BITS = 22;
|
|
851
|
+
var CHUNK_MAX = 8 * 1024 * 1024;
|
|
852
|
+
var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
|
|
853
|
+
var GEAR_SEED = "zas-gear-v1:";
|
|
854
|
+
var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
|
|
855
|
+
var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
|
|
856
|
+
var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
|
|
857
|
+
var MANIFEST_VERSION = 1;
|
|
858
|
+
var MIN_EXPIRY_DAYS = 1;
|
|
859
|
+
var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
|
|
860
|
+
var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
|
|
861
|
+
var SAFE_STORED_FILE_LIMITS = {
|
|
862
|
+
anon: 30 * 1024 * 1024,
|
|
863
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
864
|
+
pro: 200 * 1024 * 1024,
|
|
865
|
+
max: 200 * 1024 * 1024
|
|
866
|
+
};
|
|
867
|
+
var STORED_FILE_LIMITS_V2 = {
|
|
868
|
+
anon: 30 * 1024 * 1024,
|
|
869
|
+
free: FREE_RELAY_MAX_BYTES,
|
|
870
|
+
pro: 5 * 1024 * 1024 * 1024,
|
|
871
|
+
max: 20 * 1024 * 1024 * 1024
|
|
872
|
+
};
|
|
873
|
+
var STORAGE_LIMITS = {
|
|
874
|
+
anon: 30 * 1024 * 1024,
|
|
875
|
+
free: 100 * 1024 * 1024,
|
|
876
|
+
pro: null,
|
|
877
|
+
max: null
|
|
878
|
+
};
|
|
879
|
+
var THUMBNAIL_MAX_EDGE = 640;
|
|
880
|
+
var THUMBNAIL_MAX_BYTES = 14e4;
|
|
881
|
+
|
|
833
882
|
// src/shared/direct.ts
|
|
834
883
|
var DIRECT_OFFER_TTL_MS = 10 * 60 * 1e3;
|
|
835
884
|
var DIRECT_CLAIMED_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
@@ -971,6 +1020,29 @@ function traceAdd(diag, line) {
|
|
|
971
1020
|
const at = t0 === void 0 ? 0 : (Date.now() - t0) / 1e3;
|
|
972
1021
|
diag.trace.push(`${at.toFixed(2)} ${line}`);
|
|
973
1022
|
}
|
|
1023
|
+
function usableForRelay(url) {
|
|
1024
|
+
const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase();
|
|
1025
|
+
if (scheme !== "turn" && scheme !== "turns") return false;
|
|
1026
|
+
return !onPort53(url);
|
|
1027
|
+
}
|
|
1028
|
+
function onPort53(url) {
|
|
1029
|
+
return /^(stuns?|turns?):[^?]*:53(\?|$)/i.test(url);
|
|
1030
|
+
}
|
|
1031
|
+
function withoutPort53(servers) {
|
|
1032
|
+
return keepUrls(servers, (url) => !onPort53(url));
|
|
1033
|
+
}
|
|
1034
|
+
function relayIceServers(servers) {
|
|
1035
|
+
return keepUrls(servers, usableForRelay);
|
|
1036
|
+
}
|
|
1037
|
+
function keepUrls(servers, keep) {
|
|
1038
|
+
const kept = [];
|
|
1039
|
+
for (const server of servers) {
|
|
1040
|
+
const urls = typeof server.urls === "string" ? [server.urls] : server.urls ?? [];
|
|
1041
|
+
const usable2 = urls.filter(keep);
|
|
1042
|
+
if (usable2.length > 0) kept.push({ ...server, urls: usable2 });
|
|
1043
|
+
}
|
|
1044
|
+
return kept;
|
|
1045
|
+
}
|
|
974
1046
|
function urlShape(url) {
|
|
975
1047
|
const scheme = /^(stuns?|turns?):/i.exec(url)?.[1]?.toLowerCase() ?? "other";
|
|
976
1048
|
const transport = /[?&]transport=(\w+)/i.exec(url)?.[1]?.toLowerCase();
|
|
@@ -1061,8 +1133,14 @@ function pathOf(pair) {
|
|
|
1061
1133
|
function hasRemoteDescription(pc) {
|
|
1062
1134
|
return !!pc.remoteDescription?.sdp;
|
|
1063
1135
|
}
|
|
1064
|
-
function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
1065
|
-
const
|
|
1136
|
+
function session(ice, onPhase, onDiag, iceTransportPolicy = "all", skipPort53Urls = false) {
|
|
1137
|
+
const trimIce = (servers) => {
|
|
1138
|
+
if (!skipPort53Urls) return servers;
|
|
1139
|
+
if (iceTransportPolicy !== "relay") return withoutPort53(servers);
|
|
1140
|
+
const trimmed = relayIceServers(servers);
|
|
1141
|
+
return countTurnUrls(trimmed) > 0 ? trimmed : servers;
|
|
1142
|
+
};
|
|
1143
|
+
const initialIce = trimIce(ice ?? DIRECT_ICE);
|
|
1066
1144
|
const pc = new RTCPeerConnection({
|
|
1067
1145
|
iceServers: initialIce,
|
|
1068
1146
|
iceTransportPolicy,
|
|
@@ -1103,6 +1181,13 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1103
1181
|
};
|
|
1104
1182
|
traceStart.set(diag, startedAt);
|
|
1105
1183
|
traceAdd(diag, `ice servers ${iceShapes(initialIce)} policy=${iceTransportPolicy}`);
|
|
1184
|
+
const notePair = (pair) => {
|
|
1185
|
+
if (diag.pairLocal === pair.local && diag.pairRemote === pair.remote) return;
|
|
1186
|
+
const changed = diag.pairLocal !== void 0;
|
|
1187
|
+
diag.pairLocal = pair.local;
|
|
1188
|
+
diag.pairRemote = pair.remote;
|
|
1189
|
+
traceAdd(diag, `${changed ? "pair now" : "pair"} ${pair.local}/${pair.remote}`);
|
|
1190
|
+
};
|
|
1106
1191
|
pc.onicegatheringstatechange = () => {
|
|
1107
1192
|
traceAdd(diag, `gather ${String(pc.iceGatheringState ?? "")}`);
|
|
1108
1193
|
};
|
|
@@ -1230,10 +1315,7 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1230
1315
|
stallState = "idle";
|
|
1231
1316
|
void pc.getStats().then((stats) => {
|
|
1232
1317
|
const pair = pairFromStats(stats);
|
|
1233
|
-
if (
|
|
1234
|
-
diag.pairLocal = pair.local;
|
|
1235
|
-
diag.pairRemote = pair.remote;
|
|
1236
|
-
traceAdd(diag, `pair ${pair.local}/${pair.remote}`);
|
|
1318
|
+
if (pair) notePair(pair);
|
|
1237
1319
|
}).catch(() => void 0);
|
|
1238
1320
|
}
|
|
1239
1321
|
};
|
|
@@ -1258,10 +1340,12 @@ function session(ice, onPhase, onDiag, iceTransportPolicy = "all") {
|
|
|
1258
1340
|
onStall = fn;
|
|
1259
1341
|
stallWindowMs = windowMs;
|
|
1260
1342
|
},
|
|
1343
|
+
notePair,
|
|
1261
1344
|
setIceServers: (iceServers) => {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1345
|
+
const next = trimIce(iceServers);
|
|
1346
|
+
diag.turnUrlsSupplied = countTurnUrls(next);
|
|
1347
|
+
traceAdd(diag, `ice servers replaced ${iceShapes(next)}`);
|
|
1348
|
+
pc.setConfiguration({ iceServers: next });
|
|
1265
1349
|
}
|
|
1266
1350
|
};
|
|
1267
1351
|
}
|
|
@@ -1329,7 +1413,13 @@ function signalPlumbing(pc, send, onSendError, diag) {
|
|
|
1329
1413
|
};
|
|
1330
1414
|
}
|
|
1331
1415
|
function startSender(opts) {
|
|
1332
|
-
const s = session(
|
|
1416
|
+
const s = session(
|
|
1417
|
+
opts.ice,
|
|
1418
|
+
opts.onPhase,
|
|
1419
|
+
opts.onDiag,
|
|
1420
|
+
opts.iceTransportPolicy,
|
|
1421
|
+
opts.skipPort53Urls
|
|
1422
|
+
);
|
|
1333
1423
|
opts.onPhase("connecting");
|
|
1334
1424
|
const { pc } = s;
|
|
1335
1425
|
const plumbing = signalPlumbing(pc, opts.send, () => s.fail("signal_send"), s.diag);
|
|
@@ -1411,8 +1501,7 @@ function startSender(opts) {
|
|
|
1411
1501
|
void pc.getStats().then((stats) => {
|
|
1412
1502
|
const pair = pairFromStats(stats);
|
|
1413
1503
|
if (!pair) return;
|
|
1414
|
-
s.
|
|
1415
|
-
s.diag.pairRemote = pair.remote;
|
|
1504
|
+
s.notePair(pair);
|
|
1416
1505
|
opts.onPath?.(pathOf(pair));
|
|
1417
1506
|
}).catch(() => void 0);
|
|
1418
1507
|
void pump().catch((e) => s.fail("transfer", e));
|
|
@@ -1482,7 +1571,8 @@ function startReceiver(opts) {
|
|
|
1482
1571
|
opts.onPhase(p);
|
|
1483
1572
|
},
|
|
1484
1573
|
opts.onDiag,
|
|
1485
|
-
opts.iceTransportPolicy
|
|
1574
|
+
opts.iceTransportPolicy,
|
|
1575
|
+
opts.skipPort53Urls
|
|
1486
1576
|
);
|
|
1487
1577
|
s.setStall(() => void 0, DIRECT_CONNECT_TIMEOUT_MS * 2);
|
|
1488
1578
|
opts.onPhase("connecting");
|
|
@@ -1508,8 +1598,7 @@ function startReceiver(opts) {
|
|
|
1508
1598
|
void pc.getStats().then((stats) => {
|
|
1509
1599
|
const pair = pairFromStats(stats);
|
|
1510
1600
|
if (!pair) return;
|
|
1511
|
-
s.
|
|
1512
|
-
s.diag.pairRemote = pair.remote;
|
|
1601
|
+
s.notePair(pair);
|
|
1513
1602
|
opts.onPath?.(pathOf(pair));
|
|
1514
1603
|
}).catch(() => void 0);
|
|
1515
1604
|
};
|
|
@@ -1624,42 +1713,6 @@ import { basename as basename2 } from "node:path";
|
|
|
1624
1713
|
|
|
1625
1714
|
// src/shared/manifest.ts
|
|
1626
1715
|
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
1716
|
function newManifest(partial) {
|
|
1664
1717
|
return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
|
|
1665
1718
|
}
|
|
@@ -1700,6 +1753,46 @@ function decryptChannelName(channelKey, sealed) {
|
|
|
1700
1753
|
return new TextDecoder().decode(openSealed(channelKey, sealed));
|
|
1701
1754
|
}
|
|
1702
1755
|
|
|
1756
|
+
// src/file-source.ts
|
|
1757
|
+
import { promises as fsp, statSync } from "node:fs";
|
|
1758
|
+
async function readFileRange(path, start, length) {
|
|
1759
|
+
const fh = await fsp.open(path, "r");
|
|
1760
|
+
try {
|
|
1761
|
+
const buf = Buffer.allocUnsafe(Math.max(0, length));
|
|
1762
|
+
let read = 0;
|
|
1763
|
+
while (read < length) {
|
|
1764
|
+
const { bytesRead } = await fh.read(buf, read, length - read, start + read);
|
|
1765
|
+
if (bytesRead === 0) break;
|
|
1766
|
+
read += bytesRead;
|
|
1767
|
+
}
|
|
1768
|
+
const out = new Uint8Array(read);
|
|
1769
|
+
out.set(buf.subarray(0, read));
|
|
1770
|
+
return out.buffer;
|
|
1771
|
+
} finally {
|
|
1772
|
+
await fh.close();
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function directFileFrom(path, raw) {
|
|
1776
|
+
let statSize;
|
|
1777
|
+
try {
|
|
1778
|
+
statSize = statSync(path).size;
|
|
1779
|
+
} catch {
|
|
1780
|
+
return raw;
|
|
1781
|
+
}
|
|
1782
|
+
if (statSize <= raw.size) return raw;
|
|
1783
|
+
const type = raw.type;
|
|
1784
|
+
const file = {
|
|
1785
|
+
size: statSize,
|
|
1786
|
+
type,
|
|
1787
|
+
slice(start, end) {
|
|
1788
|
+
const from = Math.max(0, Math.min(start, statSize));
|
|
1789
|
+
const to = Math.max(from, Math.min(end, statSize));
|
|
1790
|
+
return { arrayBuffer: () => readFileRange(path, from, to - from) };
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
return file;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1703
1796
|
// src/shared/direct-fallback.ts
|
|
1704
1797
|
function createFallbackMeta(file) {
|
|
1705
1798
|
const key = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -1984,15 +2077,22 @@ function channelNameOf(identity, grant) {
|
|
|
1984
2077
|
throw new ZasError("key_stale", 0);
|
|
1985
2078
|
}
|
|
1986
2079
|
}
|
|
2080
|
+
function channelAccountOf(identity, grant) {
|
|
2081
|
+
return grant.channel_owner_uid !== void 0 && grant.channel_owner_uid !== "" ? grant.channel_owner_uid : identity.owner_uid;
|
|
2082
|
+
}
|
|
1987
2083
|
var fold = (value) => value.trim().toLowerCase();
|
|
2084
|
+
function usable(grant) {
|
|
2085
|
+
if (grant.pending) throw new ZasError("grant_pending", 0);
|
|
2086
|
+
return grant;
|
|
2087
|
+
}
|
|
1988
2088
|
function resolveChannel(identity, grants, channel) {
|
|
1989
2089
|
if (channel === void 0 || fold(channel) === "") {
|
|
1990
|
-
if (grants.length === 1) return grants[0];
|
|
2090
|
+
if (grants.length === 1) return usable(grants[0]);
|
|
1991
2091
|
throw new ZasError("grant_missing", 0);
|
|
1992
2092
|
}
|
|
1993
2093
|
const wanted = channel.trim();
|
|
1994
2094
|
const byId = grants.find((g) => g.channel_id === wanted);
|
|
1995
|
-
if (byId) return byId;
|
|
2095
|
+
if (byId) return usable(byId);
|
|
1996
2096
|
const folded = fold(wanted);
|
|
1997
2097
|
const byName = grants.filter((g) => {
|
|
1998
2098
|
try {
|
|
@@ -2001,12 +2101,12 @@ function resolveChannel(identity, grants, channel) {
|
|
|
2001
2101
|
return false;
|
|
2002
2102
|
}
|
|
2003
2103
|
});
|
|
2004
|
-
if (byName.length === 1) return byName[0];
|
|
2104
|
+
if (byName.length === 1) return usable(byName[0]);
|
|
2005
2105
|
throw new ZasError("grant_missing", 0);
|
|
2006
2106
|
}
|
|
2007
2107
|
|
|
2008
2108
|
// src/send.ts
|
|
2009
|
-
import { promises as
|
|
2109
|
+
import { promises as fsp2 } from "node:fs";
|
|
2010
2110
|
import { basename, extname } from "node:path";
|
|
2011
2111
|
|
|
2012
2112
|
// src/shared/chunker.ts
|
|
@@ -2282,6 +2382,10 @@ async function receiptKey(ctx, channelId, contentHash, title) {
|
|
|
2282
2382
|
const raw = `${ctx.identity.agent_uid}\0${channelId}\0${contentHash}\0${title}`;
|
|
2283
2383
|
return blake3Hex(new TextEncoder().encode(raw));
|
|
2284
2384
|
}
|
|
2385
|
+
async function sendIdentity(contentHash, days) {
|
|
2386
|
+
if (days === void 0) return contentHash;
|
|
2387
|
+
return blake3Hex(new TextEncoder().encode(`${contentHash}\0expires_in_days:${days}`));
|
|
2388
|
+
}
|
|
2285
2389
|
function receiptFor(ctx, key) {
|
|
2286
2390
|
const entries = loadFingerprints(ctx.profile).entries;
|
|
2287
2391
|
if (!Object.prototype.hasOwnProperty.call(entries, key)) return void 0;
|
|
@@ -2430,7 +2534,7 @@ async function placeChunks(ctx, encs) {
|
|
|
2430
2534
|
};
|
|
2431
2535
|
});
|
|
2432
2536
|
}
|
|
2433
|
-
async function postLink(ctx, grant, manifest, placed, idempotencyKey) {
|
|
2537
|
+
async function postLink(ctx, grant, manifest, placed, idempotencyKey, expiresInDays) {
|
|
2434
2538
|
const channelKey = channelKeyOf(ctx.identity, grant);
|
|
2435
2539
|
const sealed = sealManifest(channelKey, manifest, grant.key_version);
|
|
2436
2540
|
const created = await ctx.client.api("POST", "/links", {
|
|
@@ -2441,7 +2545,10 @@ async function postLink(ctx, grant, manifest, placed, idempotencyKey) {
|
|
|
2441
2545
|
// nothing left for the server to verify here. The field stays because the
|
|
2442
2546
|
// server reads it, and an absent array is not the same as an empty one.
|
|
2443
2547
|
proofs: [],
|
|
2444
|
-
idempotency_key: idempotencyKey
|
|
2548
|
+
idempotency_key: idempotencyKey,
|
|
2549
|
+
// Absent unless the caller asked. The server clamps whatever arrives to
|
|
2550
|
+
// the account's own plan, so this can only ever shorten the item's life.
|
|
2551
|
+
...expiresInDays === void 0 ? {} : { expires_in_days: expiresInDays }
|
|
2445
2552
|
});
|
|
2446
2553
|
const bound = created.caps ?? {};
|
|
2447
2554
|
let patched = false;
|
|
@@ -2460,7 +2567,7 @@ async function postLink(ctx, grant, manifest, placed, idempotencyKey) {
|
|
|
2460
2567
|
return created.link_id;
|
|
2461
2568
|
}
|
|
2462
2569
|
async function sendFile(ctx, input, onPhase) {
|
|
2463
|
-
const stat = await
|
|
2570
|
+
const stat = await fsp2.stat(input.path).catch(() => {
|
|
2464
2571
|
throw new ZasError("upload_failed", 400);
|
|
2465
2572
|
});
|
|
2466
2573
|
if (!stat.isFile()) throw new ZasError("upload_failed", 400);
|
|
@@ -2468,10 +2575,10 @@ async function sendFile(ctx, input, onPhase) {
|
|
|
2468
2575
|
const grant = await grantFor(ctx, input.channel);
|
|
2469
2576
|
const name = basename(input.path);
|
|
2470
2577
|
const title = input.title ?? name;
|
|
2471
|
-
const file = await
|
|
2578
|
+
const file = await fsp2.readFile(input.path);
|
|
2472
2579
|
if (file.byteLength > MAX_FILE_BYTES) throw new ZasError("file_too_big", 413);
|
|
2473
2580
|
const bytes = new Uint8Array(file.buffer, file.byteOffset, file.byteLength);
|
|
2474
|
-
const contentHash = await blake3Hex(bytes);
|
|
2581
|
+
const contentHash = await sendIdentity(await blake3Hex(bytes), input.expires_in_days);
|
|
2475
2582
|
const key = await receiptKey(ctx, grant.channel_id, contentHash, title);
|
|
2476
2583
|
const stored = receiptFor(ctx, key);
|
|
2477
2584
|
if (stored) {
|
|
@@ -2522,7 +2629,8 @@ async function sendFile(ctx, input, onPhase) {
|
|
|
2522
2629
|
grant,
|
|
2523
2630
|
manifest,
|
|
2524
2631
|
placed,
|
|
2525
|
-
agentSendIdempotencyKey(grant.channel_id, contentHash, title)
|
|
2632
|
+
agentSendIdempotencyKey(grant.channel_id, contentHash, title),
|
|
2633
|
+
input.expires_in_days
|
|
2526
2634
|
);
|
|
2527
2635
|
const deduplicated = placed.filter((p) => p.proven).length;
|
|
2528
2636
|
remember(ctx, key, {
|
|
@@ -2549,9 +2657,12 @@ async function sendNote(ctx, input) {
|
|
|
2549
2657
|
const grant = await grantFor(ctx, input.channel);
|
|
2550
2658
|
const name = input.title ?? noteName(input.text);
|
|
2551
2659
|
const encoded = new TextEncoder().encode(input.text);
|
|
2552
|
-
const contentHash = await
|
|
2553
|
-
|
|
2554
|
-
|
|
2660
|
+
const contentHash = await sendIdentity(
|
|
2661
|
+
await blake3Hex(new TextEncoder().encode(
|
|
2662
|
+
`${input.lang ?? ""}\0${input.secret ? "1" : "0"}\0${input.text}`
|
|
2663
|
+
)),
|
|
2664
|
+
input.expires_in_days
|
|
2665
|
+
);
|
|
2555
2666
|
const key = await receiptKey(ctx, grant.channel_id, contentHash, name);
|
|
2556
2667
|
const stored = receiptFor(ctx, key);
|
|
2557
2668
|
if (stored) {
|
|
@@ -2584,7 +2695,8 @@ async function sendNote(ctx, input) {
|
|
|
2584
2695
|
grant,
|
|
2585
2696
|
manifest,
|
|
2586
2697
|
[],
|
|
2587
|
-
agentSendIdempotencyKey(grant.channel_id, contentHash, name)
|
|
2698
|
+
agentSendIdempotencyKey(grant.channel_id, contentHash, name),
|
|
2699
|
+
input.expires_in_days
|
|
2588
2700
|
);
|
|
2589
2701
|
remember(ctx, key, {
|
|
2590
2702
|
link_id: linkId,
|
|
@@ -2659,9 +2771,10 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2659
2771
|
const grant = await directGrantFor(ctx, input.channel);
|
|
2660
2772
|
const key = channelKeyOf(ctx.identity, grant);
|
|
2661
2773
|
const channelName = channelLabel(ctx, grant);
|
|
2662
|
-
const
|
|
2774
|
+
const raw = await (deps.openFile ?? openAsBlob)(input.path, { type: mimeFor(input.path) }).catch(() => {
|
|
2663
2775
|
throw new ZasError("upload_failed", 400);
|
|
2664
2776
|
});
|
|
2777
|
+
const file = directFileFrom(input.path, raw);
|
|
2665
2778
|
if (file.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
|
|
2666
2779
|
const name = basename2(input.path);
|
|
2667
2780
|
const cid = grant.channel_id;
|
|
@@ -2680,7 +2793,7 @@ async function sendDirect(ctx, input, report2, deps = {}) {
|
|
|
2680
2793
|
...stamp
|
|
2681
2794
|
});
|
|
2682
2795
|
const route = `/direct/${cid}/${id}`;
|
|
2683
|
-
const offerPath = `accounts/${
|
|
2796
|
+
const offerPath = `accounts/${channelAccountOf(ctx.identity, grant)}/channels/${cid}/direct/${id}`;
|
|
2684
2797
|
const setState = (state) => ctx.client.api("POST", `${route}/state`, { state, ...stamp }).then(() => void 0, () => void 0);
|
|
2685
2798
|
const claimBy = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS);
|
|
2686
2799
|
for (; ; ) {
|
|
@@ -2882,7 +2995,7 @@ import {
|
|
|
2882
2995
|
openSync,
|
|
2883
2996
|
renameSync as renameSync2,
|
|
2884
2997
|
rmdirSync,
|
|
2885
|
-
statSync,
|
|
2998
|
+
statSync as statSync2,
|
|
2886
2999
|
unlinkSync,
|
|
2887
3000
|
writeSync
|
|
2888
3001
|
} from "node:fs";
|
|
@@ -2956,7 +3069,7 @@ function nameFrom(channelKey, grant) {
|
|
|
2956
3069
|
}
|
|
2957
3070
|
}
|
|
2958
3071
|
function linksParent(identity, grant) {
|
|
2959
|
-
return `accounts/${identity
|
|
3072
|
+
return `accounts/${channelAccountOf(identity, grant)}/channels/${grant.channel_id}`;
|
|
2960
3073
|
}
|
|
2961
3074
|
function linkPath(identity, grant, id) {
|
|
2962
3075
|
return `projects/${identity.firestore_project}/databases/(default)/documents/${linksParent(identity, grant)}/links/${id}`;
|
|
@@ -3055,7 +3168,7 @@ function destinationOf(dest, name, fallback) {
|
|
|
3055
3168
|
const created = mkdtempSync(join2(tmpdir(), DOWNLOAD_PREFIX));
|
|
3056
3169
|
return { target: freeName(join2(created, base)), created };
|
|
3057
3170
|
}
|
|
3058
|
-
const at =
|
|
3171
|
+
const at = statSync2(dest, { throwIfNoEntry: false })?.isDirectory() ? join2(dest, base) : dest;
|
|
3059
3172
|
return { target: freeName(at) };
|
|
3060
3173
|
}
|
|
3061
3174
|
async function getItem(ctx, channel, id, dest) {
|
|
@@ -3237,11 +3350,12 @@ async function receiveDirect(ctx, input, report2, deps = {}) {
|
|
|
3237
3350
|
const channelName = channelLabel(ctx, grant);
|
|
3238
3351
|
const cid = grant.channel_id;
|
|
3239
3352
|
const owner = ctx.identity.owner_uid;
|
|
3353
|
+
const account = channelAccountOf(ctx.identity, grant);
|
|
3240
3354
|
const device = deps.device ?? newDeviceToken();
|
|
3241
3355
|
const { seal, open } = sealer(key, grant.key_version);
|
|
3242
3356
|
const stamp = { device, owner_uid: owner };
|
|
3243
3357
|
const startedAt = now();
|
|
3244
|
-
const channelPath = `accounts/${
|
|
3358
|
+
const channelPath = `accounts/${account}/channels/${cid}`;
|
|
3245
3359
|
report2("waiting");
|
|
3246
3360
|
const waitUntil = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS2);
|
|
3247
3361
|
let id;
|
|
@@ -3376,6 +3490,7 @@ async function receiveDirect(ctx, input, report2, deps = {}) {
|
|
|
3376
3490
|
channel_name: channelName,
|
|
3377
3491
|
offer_id: offerId,
|
|
3378
3492
|
owner_uid: owner,
|
|
3493
|
+
channel_account: account,
|
|
3379
3494
|
device,
|
|
3380
3495
|
...input.dest !== void 0 ? { dest: input.dest } : {},
|
|
3381
3496
|
meta: offered,
|
|
@@ -3391,7 +3506,7 @@ async function receiveDirectFallback(ctx, record, report2, deps = {}) {
|
|
|
3391
3506
|
const { open } = sealer(record.key, record.key_version);
|
|
3392
3507
|
const stamp = { device: record.device, owner_uid: record.owner_uid };
|
|
3393
3508
|
const base = `/direct/${record.channel_id}/${record.offer_id}/fallback`;
|
|
3394
|
-
const offerPath = `accounts/${record.
|
|
3509
|
+
const offerPath = `accounts/${record.channel_account}/channels/${record.channel_id}/direct/${record.offer_id}`;
|
|
3395
3510
|
const doc = await ctx.client.firestoreGet(offerPath);
|
|
3396
3511
|
const enc = stringField2(doc, "fallback_meta_enc");
|
|
3397
3512
|
let meta;
|
|
@@ -3984,15 +4099,17 @@ async function report(client, properties) {
|
|
|
3984
4099
|
|
|
3985
4100
|
// src/server.ts
|
|
3986
4101
|
function agentVersion() {
|
|
3987
|
-
return true ? "0.
|
|
4102
|
+
return true ? "0.8.0" : "0.0.0-dev";
|
|
3988
4103
|
}
|
|
3989
4104
|
var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
|
|
3990
4105
|
var PAIR_ANNOUNCE_MS = 15e3;
|
|
3991
4106
|
var NOTE_LABEL_MAX = 40;
|
|
4107
|
+
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
4108
|
var delay = (ms) => new Promise((resolve2) => {
|
|
3993
4109
|
setTimeout(resolve2, ms);
|
|
3994
4110
|
});
|
|
3995
4111
|
function rightsOf(grant) {
|
|
4112
|
+
if (grant.pending) return "waiting for the channel\u2019s owner";
|
|
3996
4113
|
const rights = [];
|
|
3997
4114
|
if (grant.send && grant.mode !== "view") rights.push(grant.direct_mode ? "send (Directo)" : "send");
|
|
3998
4115
|
if (grant.read) rights.push(grant.direct_mode ? "receive (Directo)" : "read");
|
|
@@ -4165,7 +4282,8 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
|
|
|
4165
4282
|
inputSchema: {
|
|
4166
4283
|
path: z.string().describe("Absolute or relative path of the file to send."),
|
|
4167
4284
|
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.")
|
|
4285
|
+
title: z.string().optional().describe("Label for the item. Defaults to the file name."),
|
|
4286
|
+
expires_in_days: EXPIRES_IN_DAYS
|
|
4169
4287
|
}
|
|
4170
4288
|
}, async (input) => {
|
|
4171
4289
|
try {
|
|
@@ -4294,7 +4412,8 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
|
|
|
4294
4412
|
channel: z.string().optional().describe("Channel name or id. Optional when the agent holds exactly one channel."),
|
|
4295
4413
|
title: z.string().optional().describe("Label for the item. Defaults to the first line."),
|
|
4296
4414
|
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.")
|
|
4415
|
+
secret: z.boolean().optional().describe("Hide the body behind a cover until the reader opens it."),
|
|
4416
|
+
expires_in_days: EXPIRES_IN_DAYS
|
|
4298
4417
|
}
|
|
4299
4418
|
}, async (input) => {
|
|
4300
4419
|
try {
|
package/package.json
CHANGED