zas-agent 0.10.0 → 0.11.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +83 -8
  3. package/dist/cli.js +1354 -327
  4. package/package.json +6 -4
package/dist/cli.js CHANGED
@@ -2,9 +2,9 @@
2
2
  import { createRequire as __cr } from "node:module"; const require = __cr(import.meta.url);
3
3
 
4
4
  // src/cli.ts
5
- import { realpathSync } from "node:fs";
5
+ import { realpathSync as realpathSync2 } from "node:fs";
6
6
  import { createInterface } from "node:readline";
7
- import { fileURLToPath } from "node:url";
7
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
9
 
10
10
  // src/shared/agent.ts
@@ -25,7 +25,7 @@ async function blake3Hex(data) {
25
25
  return blake3(data, 256);
26
26
  }
27
27
  function hkdf512(ikm, info, length) {
28
- return hkdf(sha512, ikm, void 0, new TextEncoder().encode(info), length);
28
+ return hkdf(sha512, ikm, void 0, typeof info === "string" ? new TextEncoder().encode(info) : info, length);
29
29
  }
30
30
  function hmacSha256(key, data) {
31
31
  return hmac(sha256, key, data);
@@ -162,6 +162,9 @@ function errorFromResponse(status, body) {
162
162
  return new ZasError(code, status, raw, retryAfterMs, raw);
163
163
  }
164
164
  var SENTENCES = {
165
+ unsafe_endpoint: "Unsafe endpoint configuration ({path}). Use HTTPS; local emulators require ZAS_ALLOW_INSECURE_DEV=1 and an isolated demo-* project with all services on loopback.",
166
+ identity_permissions: "The secret file is not private: {path}. Restrict it to its owner before retrying.",
167
+ secret_store_failed: "The OS secret store could not open or save this profile. Unlock the store, or pair again in a new profile on this machine.",
165
168
  identity_corrupt: "The agent identity file is damaged: {path}. Restore it, or delete it and pair again.",
166
169
  not_paired: "This agent is not paired yet. Run \u201Cnpx -y zas-agent pair\u201D in the terminal.",
167
170
  internal: "Something failed inside the agent. Try again.",
@@ -207,6 +210,8 @@ var SENTENCES = {
207
210
  feature_disabled: "Agents are not enabled for this account yet.",
208
211
  upload_failed: "The upload failed. Try again.",
209
212
  oprf_failed: "Zas did not answer correctly while preparing the file. Try again.",
213
+ invalid_voprf_proof: "Zas could not verify the encryption service response. The file was not sent. Try again later.",
214
+ invalid_voprf_configuration: "The encryption service key is not configured for this agent. Update the agent configuration before sending.",
210
215
  network: "Zas cannot be reached. Try again in a moment.",
211
216
  sign_in_failed: "Zas did not accept this agent session. Try again, or pair again.",
212
217
  bad_signature: "Zas rejected this agent's signature. Pair it again.",
@@ -219,23 +224,189 @@ function humanSentence(error) {
219
224
  }
220
225
 
221
226
  // src/identity.ts
222
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
227
+ import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
228
+ import { randomUUID as randomUUID2 } from "node:crypto";
223
229
  import { homedir } from "node:os";
224
- import { join, resolve } from "node:path";
230
+ import { join as join2, resolve } from "node:path";
225
231
  import { x25519 } from "@noble/curves/ed25519";
226
232
  import { p256 as p2562 } from "@noble/curves/nist.js";
233
+
234
+ // src/endpoints.ts
235
+ var loopback = (url) => ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
236
+ var refuse = (setting) => {
237
+ throw new ZasError("unsafe_endpoint", 0, setting);
238
+ };
239
+ function validateBase(value, setting) {
240
+ let url;
241
+ try {
242
+ url = new URL(value);
243
+ } catch {
244
+ return refuse(setting);
245
+ }
246
+ if (url.username || url.password || url.search || url.hash) return refuse(setting);
247
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback(url) && process.env.ZAS_ALLOW_INSECURE_DEV === "1")) {
248
+ return refuse(setting);
249
+ }
250
+ return url;
251
+ }
252
+ function emulatorBase(setting) {
253
+ const host = process.env[setting];
254
+ if (!host) return void 0;
255
+ const url = validateBase(`http://${host}`, setting);
256
+ if (!loopback(url) || url.pathname !== "/" || process.env.ZAS_ALLOW_INSECURE_DEV !== "1") return refuse(setting);
257
+ return url.origin;
258
+ }
259
+ function validateEndpoints(endpoints) {
260
+ const bases = [
261
+ validateBase(endpoints.api_base, "api_base / ZAS_API_BASE"),
262
+ validateBase(endpoints.token_base, "token_base / ZAS_TOKEN_BASE"),
263
+ validateBase(endpoints.oprf_base, "oprf_base / ZAS_OPRF_BASE")
264
+ ];
265
+ const auth = emulatorBase("FIREBASE_AUTH_EMULATOR_HOST");
266
+ const firestore = emulatorBase("FIRESTORE_EMULATOR_HOST");
267
+ if ((auth || firestore) && (!auth || !firestore || !/^demo-[a-z0-9-]+$/.test(endpoints.firestore_project) || !bases.every(loopback))) {
268
+ refuse("Emulators require both loopback emulator hosts, loopback API/token/OPRF bases and a demo-* Firebase project");
269
+ }
270
+ }
271
+
272
+ // src/secret-store.ts
273
+ import { execFileSync } from "node:child_process";
274
+ import { randomUUID } from "node:crypto";
275
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
276
+ import { join } from "node:path";
277
+ var SERVICE = "red.zas.agent";
278
+ var SECRET_KEYS = ["x25519_private", "p256_private", "poll_secret", "code"];
279
+ function splitSecrets(value) {
280
+ const publicData = { ...value };
281
+ delete publicData.secret_ref;
282
+ const secrets = {};
283
+ for (const key of SECRET_KEYS) {
284
+ if (typeof value[key] === "string") secrets[key] = value[key];
285
+ delete publicData[key];
286
+ }
287
+ if (!secrets.x25519_private || !secrets.p256_private) throw new ZasError("identity_corrupt", 0);
288
+ return { publicData, secrets };
289
+ }
290
+ function checkedRef(raw) {
291
+ const ref = raw;
292
+ if (!ref || !["dpapi", "keychain", "secret-service", "file"].includes(ref.backend) || typeof ref.id !== "string" || !/^[a-f0-9-]{36}$/.test(ref.id)) throw new ZasError("identity_corrupt", 0);
293
+ if (ref.backend === "dpapi" && process.platform !== "win32" || ref.backend === "keychain" && process.platform !== "darwin" || ref.backend === "file" && process.platform === "win32") throw new ZasError("secret_store_failed", 0);
294
+ return ref;
295
+ }
296
+ function readPrivateText(path) {
297
+ let fd;
298
+ try {
299
+ if (lstatSync(path).isSymbolicLink()) throw new ZasError("identity_permissions", 0, path);
300
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
301
+ const info = fstatSync(fd);
302
+ if (!info.isFile() || info.nlink !== 1 || process.platform !== "win32" && ((info.mode & 63) !== 0 || info.uid !== process.getuid?.())) {
303
+ const fix = process.platform === "win32" ? path : `${path}; run chmod 600 -- '${path.replaceAll("'", "'\\''")}'`;
304
+ throw new ZasError("identity_permissions", 0, fix);
305
+ }
306
+ return readFileSync(fd, "utf8");
307
+ } catch (error) {
308
+ if (error.code === "ENOENT") return null;
309
+ throw error;
310
+ } finally {
311
+ if (fd !== void 0) closeSync(fd);
312
+ }
313
+ }
314
+ function command(file, args, input) {
315
+ try {
316
+ return execFileSync(file, args, {
317
+ input,
318
+ encoding: "utf8",
319
+ timeout: 15e3,
320
+ windowsHide: true,
321
+ stdio: ["pipe", "pipe", "pipe"],
322
+ maxBuffer: 1024 * 1024
323
+ }).trim();
324
+ } catch {
325
+ throw new ZasError("secret_store_failed", 0);
326
+ }
327
+ }
328
+ function dpapi(input, decrypt, dir) {
329
+ const script = `$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.Security; $request=[Console]::In.ReadToEnd() | ConvertFrom-Json; ` + (decrypt ? "" : `$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User; $acl=New-Object Security.AccessControl.DirectorySecurity; $acl.SetOwner($sid); $acl.SetAccessRuleProtection($true,$false); $rule=New-Object Security.AccessControl.FileSystemAccessRule($sid,'FullControl','ContainerInherit,ObjectInherit','None','Allow'); $acl.AddAccessRule($rule); [IO.Directory]::SetAccessControl($request.dir,$acl); `) + `$b=[Convert]::FromBase64String($request.data); $out=[Security.Cryptography.ProtectedData]::${decrypt ? "Unprotect" : "Protect"}($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser); [Console]::Out.Write([Convert]::ToBase64String($out))`;
330
+ return command(
331
+ join(process.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
332
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
333
+ JSON.stringify({ data: input, dir })
334
+ );
335
+ }
336
+ var secretPath = (dir, ref) => join(dir, `secret-${ref.id}.json`);
337
+ function storeSecrets(dir, secrets) {
338
+ const id = randomUUID();
339
+ const encoded = Buffer.from(JSON.stringify(secrets)).toString("base64");
340
+ let backend;
341
+ if (process.platform === "win32") {
342
+ backend = "dpapi";
343
+ const encrypted = dpapi(encoded, false, dir);
344
+ writeFileSync(secretPath(dir, { backend, id }), encrypted, { mode: 384, flag: "wx" });
345
+ } else if (process.platform === "darwin") {
346
+ backend = "keychain";
347
+ command("/usr/bin/security", ["-i"], `add-generic-password -a ${id} -s ${SERVICE} -w ${encoded}
348
+ `);
349
+ if (command("/usr/bin/security", ["find-generic-password", "-a", id, "-s", SERVICE, "-w"]) !== encoded) {
350
+ throw new ZasError("secret_store_failed", 0);
351
+ }
352
+ } else {
353
+ let available = Boolean(process.env.DBUS_SESSION_BUS_ADDRESS || process.env.DISPLAY);
354
+ if (available) {
355
+ try {
356
+ command("secret-tool", ["store", "--help"]);
357
+ } catch {
358
+ available = false;
359
+ }
360
+ }
361
+ backend = available ? "secret-service" : "file";
362
+ if (available) {
363
+ command("secret-tool", ["store", "--label=Zas agent", "service", SERVICE, "id", id], encoded);
364
+ if (command("secret-tool", ["lookup", "service", SERVICE, "id", id]) !== encoded) throw new ZasError("secret_store_failed", 0);
365
+ } else {
366
+ writeFileSync(secretPath(dir, { backend, id }), encoded, { mode: 384, flag: "wx" });
367
+ }
368
+ }
369
+ return { backend, id };
370
+ }
371
+ function loadSecrets(dir, raw) {
372
+ const ref = checkedRef(raw);
373
+ try {
374
+ let encoded;
375
+ if (ref.backend === "keychain") encoded = command("/usr/bin/security", ["find-generic-password", "-a", ref.id, "-s", SERVICE, "-w"]);
376
+ else if (ref.backend === "secret-service") encoded = command("secret-tool", ["lookup", "service", SERVICE, "id", ref.id]);
377
+ else {
378
+ const stored = readPrivateText(secretPath(dir, ref));
379
+ if (stored === null) throw new ZasError("secret_store_failed", 0);
380
+ encoded = ref.backend === "dpapi" ? dpapi(stored, true) : stored;
381
+ }
382
+ const value = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
383
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new ZasError("secret_store_failed", 0);
384
+ return splitSecrets(value).secrets;
385
+ } catch (error) {
386
+ if (error instanceof ZasError && error.code === "identity_permissions") throw error;
387
+ throw new ZasError("secret_store_failed", 0);
388
+ }
389
+ }
390
+ function removeSecrets(dir, raw) {
391
+ const ref = checkedRef(raw);
392
+ if (ref.backend === "keychain") command("/usr/bin/security", ["delete-generic-password", "-a", ref.id, "-s", SERVICE]);
393
+ else if (ref.backend === "secret-service") command("secret-tool", ["clear", "service", SERVICE, "id", ref.id]);
394
+ else rmSync(secretPath(dir, ref), { force: true });
395
+ }
396
+
397
+ // src/identity.ts
227
398
  var IDENTITY_FILE = "identity.json";
228
399
  var PENDING_FILE = "pending.json";
229
400
  var GRANTS_FILE = "grants.json";
230
401
  var FINGERPRINTS_FILE = "fingerprints.json";
231
402
  var SETTINGS_FILE = "settings.json";
232
403
  function agentHome() {
233
- return process.env.ZAS_AGENT_HOME || join(homedir(), ".zas", "agent");
404
+ return process.env.ZAS_AGENT_HOME || join2(homedir(), ".zas", "agent");
234
405
  }
235
406
  var PROFILE_RE = /^(?!\.)[A-Za-z0-9._-]{1,64}$/;
236
407
  function profileDir(profile) {
237
408
  if (!PROFILE_RE.test(profile)) throw new ZasError("internal", 0, `Invalid profile: ${profile}`);
238
- return join(agentHome(), profile);
409
+ return join2(agentHome(), profile);
239
410
  }
240
411
  function ensureDir(dir) {
241
412
  mkdirSync(dir, { recursive: true, mode: 448 });
@@ -243,33 +414,29 @@ function ensureDir(dir) {
243
414
  }
244
415
  function writePrivate(dir, name, value) {
245
416
  ensureDir(dir);
246
- const target = join(dir, name);
247
- const tmp = `${target}.${process.pid}.tmp`;
248
- writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
249
- `, { mode: 384 });
250
- if (process.platform !== "win32") chmodSync(tmp, 384);
251
- renameSync(tmp, target);
252
- }
253
- function readText(path) {
417
+ const target = join2(dir, name);
418
+ const tmp = `${target}.${randomUUID2()}.tmp`;
254
419
  try {
255
- return readFileSync(path, "utf8");
256
- } catch (err) {
257
- if (err.code === "ENOENT") return null;
258
- throw err;
420
+ writeFileSync2(tmp, `${JSON.stringify(value, null, 2)}
421
+ `, { mode: 384, flag: "wx" });
422
+ if (process.platform !== "win32") chmodSync(tmp, 384);
423
+ renameSync(tmp, target);
424
+ } finally {
425
+ rmSync2(tmp, { force: true });
259
426
  }
260
427
  }
261
428
  function readCache(dir, name) {
262
- const target = join(dir, name);
429
+ const target = join2(dir, name);
263
430
  if (!existsSync(target)) return null;
264
431
  try {
265
- return JSON.parse(readFileSync(target, "utf8"));
432
+ return JSON.parse(readFileSync2(target, "utf8"));
266
433
  } catch {
267
434
  return null;
268
435
  }
269
436
  }
270
- function readSecret(dir, name) {
271
- const target = resolve(join(dir, name));
272
- const text2 = readText(target);
437
+ function readStored(dir, name) {
438
+ const target = resolve(join2(dir, name));
439
+ const text2 = readPrivateText(target);
273
440
  if (text2 === null) return null;
274
441
  let parsed = null;
275
442
  try {
@@ -277,9 +444,51 @@ function readSecret(dir, name) {
277
444
  } catch {
278
445
  parsed = null;
279
446
  }
280
- if (!parsed || typeof parsed !== "object") throw new ZasError("identity_corrupt", 0, target);
447
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new ZasError("identity_corrupt", 0, target);
281
448
  return parsed;
282
449
  }
450
+ function writeSecret(dir, name, value) {
451
+ validateEndpoints(value);
452
+ const { publicData, secrets } = splitSecrets({ ...value });
453
+ const previous = readStored(dir, name);
454
+ ensureDir(dir);
455
+ const ref = storeSecrets(dir, secrets);
456
+ try {
457
+ writePrivate(dir, name, { ...publicData, secret_ref: ref });
458
+ } catch (error) {
459
+ try {
460
+ removeSecrets(dir, ref);
461
+ } catch {
462
+ }
463
+ throw error;
464
+ }
465
+ if (previous?.secret_ref) {
466
+ try {
467
+ removeSecrets(dir, previous.secret_ref);
468
+ } catch {
469
+ }
470
+ }
471
+ }
472
+ function readSecret(dir, name) {
473
+ const stored = readStored(dir, name);
474
+ if (!stored) return null;
475
+ validateEndpoints(stored);
476
+ if (stored.secret_ref) {
477
+ const { secret_ref, ...publicData } = stored;
478
+ const secrets = loadSecrets(dir, secret_ref);
479
+ return { ...publicData, ...secrets };
480
+ }
481
+ splitSecrets(stored);
482
+ writeSecret(dir, name, stored);
483
+ return stored;
484
+ }
485
+ function secretStorage(profile) {
486
+ const stored = readStored(profileDir(profile), IDENTITY_FILE) ?? readStored(profileDir(profile), PENDING_FILE);
487
+ if (!stored) return "not paired";
488
+ if (!stored.secret_ref) return "legacy file (migration pending)";
489
+ const labels = { dpapi: "Windows DPAPI (current user)", keychain: "macOS Keychain", "secret-service": "Linux Secret Service", file: "owner-only file (OS store unavailable)" };
490
+ return labels[checkedRef(stored.secret_ref).backend];
491
+ }
283
492
  function newKeyMaterial() {
284
493
  const x = x25519.utils.randomSecretKey();
285
494
  const p = p2562.utils.randomSecretKey();
@@ -292,16 +501,26 @@ function newKeyMaterial() {
292
501
  };
293
502
  }
294
503
  function loadIdentity(profile) {
295
- return readSecret(profileDir(profile), IDENTITY_FILE);
504
+ const identity = readSecret(profileDir(profile), IDENTITY_FILE);
505
+ if (identity) validateEndpoints(identity);
506
+ return identity;
296
507
  }
297
508
  function saveIdentity(profile, identity) {
298
- writePrivate(profileDir(profile), IDENTITY_FILE, identity);
509
+ writeSecret(profileDir(profile), IDENTITY_FILE, identity);
510
+ }
511
+ function loadPending(profile) {
512
+ const pending2 = readSecret(profileDir(profile), PENDING_FILE);
513
+ if (pending2) validateEndpoints(pending2);
514
+ return pending2;
299
515
  }
300
516
  function savePending(profile, pending2) {
301
- writePrivate(profileDir(profile), PENDING_FILE, pending2);
517
+ writeSecret(profileDir(profile), PENDING_FILE, pending2);
302
518
  }
303
519
  function clearPending(profile) {
304
- rmSync(join(profileDir(profile), PENDING_FILE), { force: true });
520
+ const dir = profileDir(profile);
521
+ const stored = readStored(dir, PENDING_FILE);
522
+ rmSync2(join2(dir, PENDING_FILE), { force: true });
523
+ if (stored?.secret_ref) removeSecrets(dir, stored.secret_ref);
305
524
  }
306
525
  function loadGrants(profile) {
307
526
  return readCache(profileDir(profile), GRANTS_FILE);
@@ -326,20 +545,23 @@ function saveSettings(settings) {
326
545
  writePrivate(agentHome(), SETTINGS_FILE, settings);
327
546
  }
328
547
  function defaultEndpoints() {
329
- return {
548
+ const endpoints = {
330
549
  api_base: process.env.ZAS_API_BASE || "https://zas.red/api",
331
550
  token_base: process.env.ZAS_TOKEN_BASE || "https://zas.red/anon-token",
332
551
  oprf_base: process.env.ZAS_OPRF_BASE || "https://zas.red/oprf",
333
552
  firestore_project: process.env.ZAS_FIREBASE_PROJECT || "zas-me"
334
553
  };
554
+ validateEndpoints(endpoints);
555
+ if (process.env.ZAS_WEB_BASE) validateBase(process.env.ZAS_WEB_BASE, "ZAS_WEB_BASE");
556
+ return endpoints;
335
557
  }
336
558
 
337
559
  // src/open.ts
338
560
  import { spawn } from "node:child_process";
339
561
  function openInBrowser(url, platform = process.platform) {
340
562
  try {
341
- const [command, args] = platform === "win32" ? ["cmd", ["/c", "start", "", url]] : platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
342
- const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
563
+ const [command2, args] = platform === "win32" ? ["cmd", ["/c", "start", "", url]] : platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
564
+ const child = spawn(command2, args, { detached: true, stdio: "ignore", windowsHide: true });
343
565
  child.on("error", () => void 0);
344
566
  child.unref();
345
567
  } catch {
@@ -353,16 +575,16 @@ import { hostname } from "node:os";
353
575
  import { createServer } from "node:http";
354
576
  var BODY_MAX = 1024;
355
577
  function readBody(req) {
356
- return new Promise((resolve2) => {
357
- let size = 0;
578
+ return new Promise((resolve4) => {
579
+ let size2 = 0;
358
580
  let resolved = false;
359
581
  const chunks = [];
360
582
  req.on("data", (chunk) => {
361
- size += chunk.length;
362
- if (size > BODY_MAX) {
583
+ size2 += chunk.length;
584
+ if (size2 > BODY_MAX) {
363
585
  if (!resolved) {
364
586
  resolved = true;
365
- resolve2(null);
587
+ resolve4(null);
366
588
  }
367
589
  return;
368
590
  }
@@ -371,13 +593,13 @@ function readBody(req) {
371
593
  req.on("end", () => {
372
594
  if (!resolved) {
373
595
  resolved = true;
374
- resolve2(Buffer.concat(chunks).toString("utf8"));
596
+ resolve4(Buffer.concat(chunks).toString("utf8"));
375
597
  }
376
598
  });
377
599
  req.on("error", () => {
378
600
  if (!resolved) {
379
601
  resolved = true;
380
- resolve2(null);
602
+ resolve4(null);
381
603
  }
382
604
  });
383
605
  });
@@ -445,24 +667,236 @@ async function startClaimListener(opts) {
445
667
  if ("error" in outcome) json(outcome.status, { error: outcome.error });
446
668
  else json(200, { status: "claimed" });
447
669
  });
448
- await new Promise((resolve2, reject) => {
670
+ await new Promise((resolve4, reject) => {
449
671
  server.once("error", reject);
450
672
  server.listen(0, "127.0.0.1", () => {
451
673
  server.off("error", reject);
452
- resolve2();
674
+ resolve4();
453
675
  });
454
676
  });
455
677
  const address = server.address();
456
678
  return {
457
679
  host: address.address,
458
680
  port: address.port,
459
- close: () => new Promise((resolve2) => {
681
+ close: () => new Promise((resolve4) => {
460
682
  server.closeAllConnections();
461
- server.close(() => resolve2());
683
+ server.close(() => resolve4());
462
684
  })
463
685
  };
464
686
  }
465
687
 
688
+ // src/shared/oprf.ts
689
+ import { RistrettoPoint } from "@noble/curves/ed25519";
690
+ import { sha512 as sha5122 } from "@noble/hashes/sha2";
691
+ import { invert, mod } from "@noble/curves/abstract/modular";
692
+
693
+ // src/shared/constants.ts
694
+ var CHUNKER_VERSION = "chunker_v1";
695
+ var CHUNK_MIN = 1 * 1024 * 1024;
696
+ var CHUNK_AVG_BITS = 22;
697
+ var CHUNK_MAX = 8 * 1024 * 1024;
698
+ var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
699
+ var GEAR_SEED = "zas-gear-v1:";
700
+ var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
701
+ var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
702
+ var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
703
+ var MANIFEST_VERSION = 1;
704
+ var MIN_EXPIRY_DAYS = 1;
705
+ var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
706
+ var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
707
+ var SAFE_STORED_FILE_LIMITS = {
708
+ anon: 30 * 1024 * 1024,
709
+ free: FREE_RELAY_MAX_BYTES,
710
+ pro: 200 * 1024 * 1024,
711
+ max: 200 * 1024 * 1024
712
+ };
713
+ var STORED_FILE_LIMITS_V2 = {
714
+ anon: 30 * 1024 * 1024,
715
+ free: FREE_RELAY_MAX_BYTES,
716
+ pro: 5 * 1024 * 1024 * 1024,
717
+ max: 20 * 1024 * 1024 * 1024
718
+ };
719
+ var STORAGE_LIMITS = {
720
+ anon: 30 * 1024 * 1024,
721
+ free: 100 * 1024 * 1024,
722
+ pro: null,
723
+ max: null
724
+ };
725
+ var THUMBNAIL_MAX_EDGE = 640;
726
+ var THUMBNAIL_MAX_BYTES = 14e4;
727
+
728
+ // src/shared/oprf.ts
729
+ var ORDER = 2n ** 252n + 27742317777372353535851937790883648493n;
730
+ var te = new TextEncoder();
731
+ function i2osp(value, length) {
732
+ const out = new Uint8Array(length);
733
+ for (let i = length - 1; i >= 0; i--) {
734
+ out[i] = value & 255;
735
+ value >>>= 8;
736
+ }
737
+ return out;
738
+ }
739
+ function expandMessageXmd(msg, dst, lenInBytes) {
740
+ const bInBytes = 64;
741
+ const rInBytes = 128;
742
+ const ell = Math.ceil(lenInBytes / bInBytes);
743
+ if (ell > 255) throw new Error("expand_message_xmd: ell too large");
744
+ const dstPrime = concatBytes(dst, i2osp(dst.length, 1));
745
+ const zPad = new Uint8Array(rInBytes);
746
+ const lIbStr = i2osp(lenInBytes, 2);
747
+ const msgPrime = concatBytes(zPad, msg, lIbStr, i2osp(0, 1), dstPrime);
748
+ const b0 = sha5122(msgPrime);
749
+ const b = [];
750
+ b[0] = sha5122(concatBytes(b0, i2osp(1, 1), dstPrime));
751
+ for (let i = 2; i <= ell; i++) {
752
+ const xored = new Uint8Array(bInBytes);
753
+ for (let j = 0; j < bInBytes; j++) xored[j] = b0[j] ^ b[i - 2][j];
754
+ b[i - 1] = sha5122(concatBytes(xored, i2osp(i, 1), dstPrime));
755
+ }
756
+ return concatBytes(...b).slice(0, lenInBytes);
757
+ }
758
+ function bytesToBigIntBE(bytes) {
759
+ let v = 0n;
760
+ for (const byte of bytes) v = v << 8n | BigInt(byte);
761
+ return v;
762
+ }
763
+ function hashToGroup(input) {
764
+ const dst = te.encode("HashToGroup-" + OPRF_CONTEXT);
765
+ const uniform = expandMessageXmd(input, dst, 64);
766
+ return RistrettoPoint.hashToCurve(uniform);
767
+ }
768
+ function randomScalar() {
769
+ const bytes = new Uint8Array(64);
770
+ crypto.getRandomValues(bytes);
771
+ const s = mod(bytesToBigIntBE(bytes), ORDER);
772
+ return s === 0n ? 1n : s;
773
+ }
774
+ function oprfBlind(input) {
775
+ const blind = randomScalar();
776
+ const P = hashToGroup(input);
777
+ return { blind, blindedElement: P.multiply(blind).toRawBytes() };
778
+ }
779
+ function oprfFinalize(input, blind, evaluatedElement) {
780
+ const E = decodeOprfElement(evaluatedElement);
781
+ const N = E.multiply(invert(blind, ORDER));
782
+ const unblinded = N.toRawBytes();
783
+ const hashInput = concatBytes(
784
+ i2osp(input.length, 2),
785
+ input,
786
+ i2osp(unblinded.length, 2),
787
+ unblinded,
788
+ te.encode("Finalize")
789
+ );
790
+ return sha5122(hashInput);
791
+ }
792
+ function decodeOprfElement(bytes) {
793
+ try {
794
+ if (bytes.length !== 32) throw new Error();
795
+ const point2 = RistrettoPoint.fromHex(bytes);
796
+ if (point2.equals(RistrettoPoint.ZERO)) throw new Error();
797
+ return point2;
798
+ } catch {
799
+ throw new Error("invalid_oprf_element");
800
+ }
801
+ }
802
+
803
+ // src/shared/voprf.ts
804
+ import { RistrettoPoint as Point } from "@noble/curves/ed25519";
805
+ import { sha512 as sha5123 } from "@noble/hashes/sha2";
806
+ var ORDER2 = 2n ** 252n + 27742317777372353535851937790883648493n;
807
+ var CONTEXT = "OPRFV1--ristretto255-SHA512";
808
+ var utf8 = (s) => new TextEncoder().encode(s);
809
+ var size = (n) => new Uint8Array([n >>> 8, n & 255]);
810
+ var framed = (b) => concatBytes(size(b.length), b);
811
+ var reduce = (s) => (s % ORDER2 + ORDER2) % ORDER2;
812
+ var multiply = (p, s) => s === 0n ? Point.ZERO : p.multiply(s);
813
+ function le(bytes) {
814
+ let n = 0n;
815
+ for (let i = bytes.length - 1; i >= 0; i--) n = n << 8n | BigInt(bytes[i]);
816
+ return n;
817
+ }
818
+ function hashScalar(input, label = "HashToScalar-") {
819
+ return reduce(le(expandMessageXmd(input, utf8(label + CONTEXT), 64)));
820
+ }
821
+ function point(bytes) {
822
+ try {
823
+ if (bytes.length !== 32) throw new Error();
824
+ const p = Point.fromHex(bytes);
825
+ if (p.equals(Point.ZERO)) throw new Error();
826
+ return p;
827
+ } catch {
828
+ throw new Error("invalid_oprf_element");
829
+ }
830
+ }
831
+ function voprfBlind(input, blind = randomScalar()) {
832
+ if (input.length > 65535 || blind <= 0n || blind >= ORDER2) throw new Error("invalid_oprf_input");
833
+ const p = Point.hashToCurve(expandMessageXmd(input, utf8("HashToGroup-" + CONTEXT), 64));
834
+ if (p.equals(Point.ZERO)) throw new Error("invalid_oprf_input");
835
+ return { blind, blindedElement: p.multiply(blind).toRawBytes() };
836
+ }
837
+ function composites(publicKey, blinded, evaluated) {
838
+ if (!blinded.length || blinded.length > 256 || blinded.length !== evaluated.length) throw new Error("invalid_oprf_batch");
839
+ const seed = sha5123(concatBytes(framed(publicKey), framed(utf8("Seed-" + CONTEXT))));
840
+ let m = Point.ZERO;
841
+ let z2 = Point.ZERO;
842
+ for (let i = 0; i < blinded.length; i++) {
843
+ const c = point(blinded[i]);
844
+ const d = point(evaluated[i]);
845
+ const coefficient = hashScalar(concatBytes(framed(seed), size(i), framed(blinded[i]), framed(evaluated[i]), utf8("Composite")));
846
+ m = m.add(multiply(c, coefficient));
847
+ z2 = z2.add(multiply(d, coefficient));
848
+ }
849
+ return [m, z2];
850
+ }
851
+ function challenge(publicKey, m, z2, t2, t3) {
852
+ return hashScalar(concatBytes(framed(publicKey), ...[m, z2, t2, t3].map((p) => framed(p.toRawBytes())), utf8("Challenge")));
853
+ }
854
+ function voprfVerify(publicKey, blinded, evaluated, proof) {
855
+ try {
856
+ const pk = point(publicKey);
857
+ if (proof.length !== 64) return false;
858
+ const c = le(proof.slice(0, 32));
859
+ const s = le(proof.slice(32));
860
+ if (c >= ORDER2 || s >= ORDER2) return false;
861
+ const [m, z2] = composites(publicKey, blinded, evaluated);
862
+ return challenge(publicKey, m, z2, multiply(Point.BASE, s).add(multiply(pk, c)), multiply(m, s).add(multiply(z2, c))) === c;
863
+ } catch {
864
+ return false;
865
+ }
866
+ }
867
+
868
+ // src/shared/dedupe-oprf.ts
869
+ function configuredDedupePolicy(capability, pinsJson) {
870
+ if (capability === void 0 || capability.mode === 0) return { mode: 0 };
871
+ try {
872
+ return dedupePolicy(capability, JSON.parse(pinsJson || "{}"));
873
+ } catch {
874
+ throw new Error("invalid_voprf_configuration");
875
+ }
876
+ }
877
+ function dedupePolicy(capability, pinnedKeys) {
878
+ if (capability === void 0 || capability.mode === 0) return { mode: 0 };
879
+ if (capability.mode !== 1 || !capability.key_id || !Object.hasOwn(pinnedKeys, capability.key_id)) throw new Error("invalid_voprf_configuration");
880
+ const publicKey = b64ToBytes(pinnedKeys[capability.key_id]);
881
+ if (publicKey.length !== 32 || publicKey.every((b) => b === 0)) throw new Error("invalid_voprf_configuration");
882
+ return { mode: 1, keyId: capability.key_id, publicKey };
883
+ }
884
+ var blindDedupe = (input, policy) => policy.mode === 1 ? voprfBlind(input) : oprfBlind(input);
885
+ function dedupeRequest(blinded, policy) {
886
+ return { blinded: blinded.map(bytesToB64), ...policy.mode === 1 ? { mode: 1, key_id: policy.keyId } : {} };
887
+ }
888
+ function verifiedDedupeResponse(raw, blinded, policy) {
889
+ try {
890
+ const response = raw;
891
+ if (!Array.isArray(response?.evaluated) || response.evaluated.length !== blinded.length) throw new Error();
892
+ const evaluated = response.evaluated.map(b64ToBytes);
893
+ if (policy.mode === 1 && (response.mode !== 1 || response.key_id !== policy.keyId || typeof response.proof !== "string" || !voprfVerify(policy.publicKey, blinded, evaluated, b64ToBytes(response.proof)))) throw new Error();
894
+ return evaluated;
895
+ } catch {
896
+ throw new Error(policy.mode === 1 ? "invalid_voprf_proof" : "invalid_oprf_response");
897
+ }
898
+ }
899
+
466
900
  // src/client.ts
467
901
  var REFRESH_MARGIN_MS = 5 * 60 * 1e3;
468
902
  async function readBody2(res) {
@@ -477,11 +911,13 @@ async function readBody2(res) {
477
911
  function jsonInit(method, body, headers) {
478
912
  return {
479
913
  method,
914
+ redirect: "error",
480
915
  headers: { ...body === void 0 ? {} : { "Content-Type": "application/json" }, ...headers },
481
916
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
482
917
  };
483
918
  }
484
919
  async function apiPublic(base, method, path, body, headers = {}, fetchImpl) {
920
+ validateBase(base, "API base");
485
921
  const call = fetchImpl ?? ((input, init) => fetch(input, init));
486
922
  const res = await call(`${base}${path}`, jsonInit(method, body, headers));
487
923
  const parsed = res.status === 204 ? void 0 : await readBody2(res);
@@ -491,6 +927,7 @@ async function apiPublic(base, method, path, body, headers = {}, fetchImpl) {
491
927
  var ZasClient = class _ZasClient {
492
928
  constructor(identity, opts = {}) {
493
929
  this.identity = identity;
930
+ validateEndpoints(identity);
494
931
  this.call = opts.fetch ?? ((input, init) => fetch(input, init));
495
932
  this.now = opts.now ?? (() => Date.now());
496
933
  }
@@ -503,6 +940,7 @@ var ZasClient = class _ZasClient {
503
940
  /** Concurrent callers wait on the one sign-in instead of racing three. */
504
941
  pendingSignIn = null;
505
942
  async signIn() {
943
+ validateEndpoints(this.identity);
506
944
  const { agent_uid, owner_uid, token_base, p256_private } = this.identity;
507
945
  const chal = await apiPublic(
508
946
  token_base,
@@ -525,6 +963,7 @@ var ZasClient = class _ZasClient {
525
963
  );
526
964
  const res = await this.call(`${_ZasClient.identityToolkitBase()}/accounts:signInWithCustomToken?key=${_ZasClient.apiKey()}`, {
527
965
  method: "POST",
966
+ redirect: "error",
528
967
  headers: { "Content-Type": "application/json" },
529
968
  body: JSON.stringify({ token: tok.token, returnSecureToken: true })
530
969
  });
@@ -552,6 +991,7 @@ var ZasClient = class _ZasClient {
552
991
  /** Optional traffic must never initiate or invalidate the shared auth session.
553
992
  * One bounded request, no retries, and no response body parsing. */
554
993
  async reportTelemetry(body) {
994
+ validateEndpoints(this.identity);
555
995
  const session2 = this.session;
556
996
  if (this.telemetrySending || !session2 || session2.expiresAt <= this.now() + REFRESH_MARGIN_MS) return;
557
997
  this.telemetrySending = true;
@@ -573,6 +1013,8 @@ var ZasClient = class _ZasClient {
573
1013
  return parsed;
574
1014
  }
575
1015
  async authed(url, method, body, headers) {
1016
+ validateEndpoints(this.identity);
1017
+ validateBase(url, "Request URL");
576
1018
  const token = await this.idToken();
577
1019
  return this.call(url, jsonInit(method, body, { Authorization: `Bearer ${token}`, ...headers }));
578
1020
  }
@@ -589,10 +1031,27 @@ var ZasClient = class _ZasClient {
589
1031
  this.session = null;
590
1032
  return this.authed(url, method, body, headers);
591
1033
  }
592
- async oprfEvaluate(blinded) {
593
- const res = await this.authedRetrying(`${this.identity.oprf_base}/evaluate`, "POST", { blinded }, {});
1034
+ async dedupePolicy() {
1035
+ const capabilities = await this.api("GET", "/agents/me");
1036
+ try {
1037
+ return configuredDedupePolicy(capabilities.dedupe, process.env.ZAS_VOPRF_PUBLIC_KEYS);
1038
+ } catch {
1039
+ throw new ZasError("invalid_voprf_configuration", 0);
1040
+ }
1041
+ }
1042
+ async oprfEvaluate(blinded, policy = { mode: 0 }) {
1043
+ const points = policy.mode === 1 ? blinded.map(b64ToBytes) : [];
1044
+ const body = policy.mode === 1 ? dedupeRequest(points, policy) : { blinded };
1045
+ const res = await this.authedRetrying(`${this.identity.oprf_base}/evaluate`, "POST", body, {});
594
1046
  const parsed = await readBody2(res);
595
1047
  if (!res.ok) throw errorFromResponse(res.status, parsed);
1048
+ if (policy.mode === 1) {
1049
+ try {
1050
+ return verifiedDedupeResponse(parsed, points, policy).map(bytesToB64);
1051
+ } catch {
1052
+ throw new ZasError("invalid_voprf_proof", 0);
1053
+ }
1054
+ }
596
1055
  return parsed.evaluated ?? [];
597
1056
  }
598
1057
  /** Firestore REST runQuery. `parentPath` is relative to the documents root
@@ -623,12 +1082,11 @@ var ZasClient = class _ZasClient {
623
1082
  return process.env.ZAS_FIREBASE_API_KEY || "AIzaSyAiZbAPrxH7EKaJftJoGcEVEL0h6rAVcvE";
624
1083
  }
625
1084
  static identityToolkitBase() {
626
- const host = process.env.FIREBASE_AUTH_EMULATOR_HOST;
627
- return host ? `http://${host}/identitytoolkit.googleapis.com/v1` : "https://identitytoolkit.googleapis.com/v1";
1085
+ const host = emulatorBase("FIREBASE_AUTH_EMULATOR_HOST");
1086
+ return host ? `${host}/identitytoolkit.googleapis.com/v1` : "https://identitytoolkit.googleapis.com/v1";
628
1087
  }
629
1088
  static firestoreBase(project) {
630
- const host = process.env.FIRESTORE_EMULATOR_HOST;
631
- const root = host ? `http://${host}` : "https://firestore.googleapis.com";
1089
+ const root = emulatorBase("FIRESTORE_EMULATOR_HOST") ?? "https://firestore.googleapis.com";
632
1090
  return `${root}/v1/projects/${project}/databases/(default)/documents`;
633
1091
  }
634
1092
  };
@@ -636,12 +1094,12 @@ var ZasClient = class _ZasClient {
636
1094
  // src/shared/agent-snippets.ts
637
1095
  function agentSnippets(profile, windows = false, pkg = "zas-agent") {
638
1096
  const runner = windows ? "npx.cmd" : "npx";
639
- const command = `${runner} -y ${pkg}@latest`;
1097
+ const command2 = `${runner} -y ${pkg}@latest`;
640
1098
  const shell = windows ? "cmd /d /c " : "";
641
1099
  return {
642
- pair: `${command} pair --profile ${profile}`,
643
- claude: `${shell}claude mcp add zas "--" ${command} --profile ${profile}`,
644
- codex: `${shell}codex mcp add zas "--" ${command} --profile ${profile}`
1100
+ pair: `${command2} pair --profile ${profile}`,
1101
+ claude: `${shell}claude mcp add zas "--" ${command2} --profile ${profile}`,
1102
+ codex: `${shell}codex mcp add zas "--" ${command2} --profile ${profile}`
645
1103
  };
646
1104
  }
647
1105
 
@@ -672,13 +1130,13 @@ function pairUrl(webBase, pairingId, port) {
672
1130
  function webOriginOf(webBase) {
673
1131
  return new URL(webBase).origin;
674
1132
  }
675
- var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1133
+ var defaultSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
676
1134
  function deferred() {
677
- let resolve2;
1135
+ let resolve4;
678
1136
  const promise = new Promise((r) => {
679
- resolve2 = r;
1137
+ resolve4 = r;
680
1138
  });
681
- return { promise, resolve: resolve2 };
1139
+ return { promise, resolve: resolve4 };
682
1140
  }
683
1141
  async function runPair(opts) {
684
1142
  const sleep2 = opts.sleep ?? defaultSleep;
@@ -862,12 +1320,12 @@ async function runPair(opts) {
862
1320
  clearPending(opts.profile);
863
1321
  const claudeLines = (lead) => [lead, ` ${claudeSnippet(opts.profile)}`];
864
1322
  const codexLines = (lead) => [lead, ` ${codexSnippet(opts.profile)}`];
865
- const install = opts.kind === "claude_code" ? claudeLines("Add it to Claude Code:") : opts.kind === "codex" ? codexLines("Add it to Codex:") : [...claudeLines("Add it to Claude Code:"), ...codexLines("Or to Codex:")];
1323
+ const install2 = opts.kind === "claude_code" ? claudeLines("Add it to Claude Code:") : opts.kind === "codex" ? codexLines("Add it to Codex:") : [...claudeLines("Add it to Claude Code:"), ...codexLines("Or to Codex:")];
866
1324
  opts.log(
867
1325
  [
868
1326
  `Done: the agent \u201C${identity.name}\u201D is paired with your account.`,
869
1327
  ...replacing ? ["It replaces the previous agent of this profile, which is now revoked."] : [],
870
- ...install
1328
+ ...install2
871
1329
  ].join("\n")
872
1330
  );
873
1331
  return identity;
@@ -877,41 +1335,6 @@ async function runPair(opts) {
877
1335
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
878
1336
  import { z } from "zod";
879
1337
 
880
- // src/shared/constants.ts
881
- var CHUNKER_VERSION = "chunker_v1";
882
- var CHUNK_MIN = 1 * 1024 * 1024;
883
- var CHUNK_AVG_BITS = 22;
884
- var CHUNK_MAX = 8 * 1024 * 1024;
885
- var STORAGE_V2_PART_BYTES = 16 * 1024 * 1024;
886
- var GEAR_SEED = "zas-gear-v1:";
887
- var HKDF_INFO_CHUNK_KEY = "zas-chunk-key-v1";
888
- var HKDF_INFO_CHUNK_NONCE = "zas-chunk-nonce-v1";
889
- var OPRF_CONTEXT = "OPRFV1-\0-ristretto255-SHA512";
890
- var MANIFEST_VERSION = 1;
891
- var MIN_EXPIRY_DAYS = 1;
892
- var ANON_EMPTY_TTL_MS = 30 * 60 * 1e3;
893
- var FREE_RELAY_MAX_BYTES = 50 * 1024 * 1024;
894
- var SAFE_STORED_FILE_LIMITS = {
895
- anon: 30 * 1024 * 1024,
896
- free: FREE_RELAY_MAX_BYTES,
897
- pro: 200 * 1024 * 1024,
898
- max: 200 * 1024 * 1024
899
- };
900
- var STORED_FILE_LIMITS_V2 = {
901
- anon: 30 * 1024 * 1024,
902
- free: FREE_RELAY_MAX_BYTES,
903
- pro: 5 * 1024 * 1024 * 1024,
904
- max: 20 * 1024 * 1024 * 1024
905
- };
906
- var STORAGE_LIMITS = {
907
- anon: 30 * 1024 * 1024,
908
- free: 100 * 1024 * 1024,
909
- pro: null,
910
- max: null
911
- };
912
- var THUMBNAIL_MAX_EDGE = 640;
913
- var THUMBNAIL_MAX_BYTES = 14e4;
914
-
915
1338
  // src/shared/direct.ts
916
1339
  var DIRECT_OFFER_TTL_MS = 10 * 60 * 1e3;
917
1340
  var DIRECT_CLAIMED_TTL_MS = 6 * 60 * 60 * 1e3;
@@ -927,12 +1350,12 @@ var DIRECT_MEMORY_SINK_MAX_BYTES = 128 * 1024 * 1024;
927
1350
  var DIRECT_FALLBACK_PART_BYTES = 16 * 1024 * 1024;
928
1351
  var DIRECT_FALLBACK_TAG_BYTES = 16;
929
1352
  var DIRECT_FALLBACK_TTL_MS = 24 * 60 * 60 * 1e3;
930
- function directFallbackPartCount(size) {
931
- if (!Number.isSafeInteger(size) || size < 0) throw new Error("bad_size");
932
- return Math.max(1, Math.ceil(size / DIRECT_FALLBACK_PART_BYTES));
1353
+ function directFallbackPartCount(size2) {
1354
+ if (!Number.isSafeInteger(size2) || size2 < 0) throw new Error("bad_size");
1355
+ return Math.max(1, Math.ceil(size2 / DIRECT_FALLBACK_PART_BYTES));
933
1356
  }
934
- function directFallbackCipherSize(size) {
935
- return size + directFallbackPartCount(size) * DIRECT_FALLBACK_TAG_BYTES;
1357
+ function directFallbackCipherSize(size2) {
1358
+ return size2 + directFallbackPartCount(size2) * DIRECT_FALLBACK_TAG_BYTES;
936
1359
  }
937
1360
  var DIRECT_CONNECT_TIMEOUT_MS = 30 * 1e3;
938
1361
  var DIRECT_STALL_GRACE_MS = 5 * 1e3;
@@ -1477,10 +1900,10 @@ function startSender(opts) {
1477
1900
  s.setChannel(dc);
1478
1901
  dc.binaryType = "arraybuffer";
1479
1902
  dc.bufferedAmountLowThreshold = DIRECT_BUFFERED_HIGH / 8;
1480
- const waitLow = () => new Promise((resolve2) => {
1903
+ const waitLow = () => new Promise((resolve4) => {
1481
1904
  const done = () => {
1482
1905
  dc.removeEventListener("bufferedamountlow", done);
1483
- resolve2();
1906
+ resolve4();
1484
1907
  };
1485
1908
  dc.addEventListener("bufferedamountlow", done);
1486
1909
  });
@@ -1495,7 +1918,7 @@ function startSender(opts) {
1495
1918
  };
1496
1919
  const waitPersisted = (target) => {
1497
1920
  if (peerProtocol < 2 || persisted >= target || s.isTerminal()) return Promise.resolve();
1498
- return new Promise((resolve2) => creditWaiters.push({ target, resolve: resolve2 }));
1921
+ return new Promise((resolve4) => creditWaiters.push({ target, resolve: resolve4 }));
1499
1922
  };
1500
1923
  const pump = async () => {
1501
1924
  const meta = {
@@ -1683,8 +2106,8 @@ function startReceiver(opts) {
1683
2106
  finished = true;
1684
2107
  void pipeline.then(async (sink) => {
1685
2108
  if (got !== total || queued !== total) throw new Error("size_mismatch");
1686
- const sha2563 = (await hasherPromise).digest();
1687
- if (protocol >= 2 && (!frame.digest || frame.digest.size !== got || frame.digest.sha256 !== sha2563)) throw new Error("digest_mismatch");
2109
+ const sha2564 = (await hasherPromise).digest();
2110
+ if (protocol >= 2 && (!frame.digest || frame.digest.size !== got || frame.digest.sha256 !== sha2564)) throw new Error("digest_mismatch");
1688
2111
  await sink.close();
1689
2112
  dc?.send(DONE_FRAME);
1690
2113
  s.setPhase("done");
@@ -1740,9 +2163,143 @@ function startReceiver(opts) {
1740
2163
  }
1741
2164
 
1742
2165
  // src/transfer-telemetry.ts
1743
- import { randomUUID } from "node:crypto";
2166
+ import { randomUUID as randomUUID3 } from "node:crypto";
2167
+
2168
+ // src/shared/nearby.ts
2169
+ import { sha256 as sha2563 } from "@noble/hashes/sha2";
2170
+
2171
+ // src/shared/manifest.ts
2172
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha";
2173
+ function newManifest(partial) {
2174
+ return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
2175
+ }
2176
+ var ENVELOPE_MAGIC = 90;
2177
+ var KEY_VERSION_LEGACY = 1;
2178
+ var ENVELOPE_V2_PREFIX = new Uint8Array([90, 0, 90, 65, 83, 69, 78, 86]);
2179
+ var V2_HEADER_BYTES = 8 + 1 + 1 + 24;
2180
+ function isV2Envelope(sealed) {
2181
+ return ENVELOPE_V2_PREFIX.every((b, i) => sealed[i] === b);
2182
+ }
2183
+ function validateV2Envelope(sealed) {
2184
+ if (sealed.length < V2_HEADER_BYTES + 16 || sealed[8] !== 2 || sealed[9] < 1) throw new Error("invalid_envelope");
2185
+ }
2186
+ function envelopeVersion(sealed) {
2187
+ if (isV2Envelope(sealed)) {
2188
+ validateV2Envelope(sealed);
2189
+ return sealed[9];
2190
+ }
2191
+ if (sealed.length > 26 && sealed[0] === ENVELOPE_MAGIC && sealed[1] >= 1) return sealed[1];
2192
+ return KEY_VERSION_LEGACY;
2193
+ }
2194
+ function sealBytes(key, version, plain) {
2195
+ if (version < 1 || version > 255) throw new Error("key version out of range");
2196
+ const nonce = new Uint8Array(24);
2197
+ crypto.getRandomValues(nonce);
2198
+ const ct = xchacha20poly1305(key, nonce).encrypt(plain);
2199
+ return concatBytes(new Uint8Array([ENVELOPE_MAGIC, version]), nonce, ct);
2200
+ }
2201
+ function openSealed(key, sealed) {
2202
+ if (isV2Envelope(sealed)) {
2203
+ validateV2Envelope(sealed);
2204
+ return xchacha20poly1305(key, sealed.slice(10, V2_HEADER_BYTES), sealed.slice(0, V2_HEADER_BYTES)).decrypt(sealed.slice(V2_HEADER_BYTES));
2205
+ }
2206
+ const versioned = envelopeVersion(sealed) !== KEY_VERSION_LEGACY || sealed[0] === ENVELOPE_MAGIC;
2207
+ if (versioned && sealed.length > 26) {
2208
+ try {
2209
+ return xchacha20poly1305(key, sealed.slice(2, 26)).decrypt(sealed.slice(26));
2210
+ } catch {
2211
+ }
2212
+ }
2213
+ return xchacha20poly1305(key, sealed.slice(0, 24)).decrypt(sealed.slice(24));
2214
+ }
2215
+ var sealRaw = sealBytes;
2216
+ var openRaw = openSealed;
2217
+ function sealManifest(channelKey, manifest, version = KEY_VERSION_LEGACY) {
2218
+ return sealBytes(channelKey, version, new TextEncoder().encode(JSON.stringify(manifest)));
2219
+ }
2220
+ function openManifest(channelKey, sealed) {
2221
+ const m = JSON.parse(new TextDecoder().decode(openSealed(channelKey, sealed)));
2222
+ if (!Array.isArray(m.chunks)) m.chunks = [];
2223
+ return m;
2224
+ }
2225
+ function decryptChannelName(channelKey, sealed) {
2226
+ return new TextDecoder().decode(openSealed(channelKey, sealed));
2227
+ }
2228
+
2229
+ // src/shared/nearby.ts
2230
+ var NEARBY_TOKEN_BYTES = 16;
2231
+ var NEARBY_FINGERPRINT_BYTES = 16;
2232
+ var NEARBY_CARRIER_BYTES = NEARBY_TOKEN_BYTES + NEARBY_FINGERPRINT_BYTES;
2233
+ var NEARBY_REFUSAL_CODES = [
2234
+ "nearby_token_expired",
2235
+ "nearby_self",
2236
+ "nearby_key_mismatch",
2237
+ "nearby_code_expired",
2238
+ "nearby_code_taken",
2239
+ "nearby_offer_expired",
2240
+ "nearby_delivery_not_ready",
2241
+ "nearby_delivery_conflict",
2242
+ "nearby_delivery_expired",
2243
+ "nearby_channel_exists",
2244
+ "nearby_channel_not_kept",
2245
+ "nearby_channel_closed_membership",
2246
+ "nearby_send_personal_only",
2247
+ // The copy into the nearby channel did not fit the sender's personal
2248
+ // storage (#43). Named so no client falls back to "try again".
2249
+ "storage_limit",
2250
+ "enterprise_sending_disabled",
2251
+ "enterprise_guest_receiving_disabled",
2252
+ "enterprise_plan_inactive",
2253
+ "rate_limited"
2254
+ ];
1744
2255
 
1745
2256
  // src/shared/product-analytics.ts
2257
+ var NEARBY_CLIENT_EVENTS = [
2258
+ "nearby.session_started",
2259
+ "nearby.permission_result",
2260
+ "nearby.bluetooth_enable_result",
2261
+ "nearby.advertising",
2262
+ "nearby.token_failed",
2263
+ "nearby.connect_received",
2264
+ "nearby.scan_summary",
2265
+ "nearby.touch_detected",
2266
+ "nearby.touch_ambiguous",
2267
+ "nearby.candidate_picked",
2268
+ "nearby.carrier_read",
2269
+ "nearby.resolved",
2270
+ "nearby.face_shown",
2271
+ "nearby.confirm_result",
2272
+ "nearby.offer_sent",
2273
+ "nearby.item_copied",
2274
+ "nearby.offer_shown",
2275
+ "nearby.offer_answered",
2276
+ "nearby.answer_seen",
2277
+ "nearby.code_shown",
2278
+ "nearby.link_opened",
2279
+ "nearby.card_state",
2280
+ "nearby.claim_sent",
2281
+ "nearby.claim_decided",
2282
+ "nearby.claim_answer",
2283
+ "nearby.feedback",
2284
+ "nearby.session_ended"
2285
+ ];
2286
+ var NEARBY_FAIL_CODES = [
2287
+ ...NEARBY_REFUSAL_CODES,
2288
+ "network",
2289
+ "timeout",
2290
+ "disconnected",
2291
+ "no_service",
2292
+ "bad_length",
2293
+ "gatt_error",
2294
+ "bt_off",
2295
+ "bt_unauthorized",
2296
+ "bt_unsupported",
2297
+ "permission_denied",
2298
+ "copy_failed",
2299
+ "key_missing",
2300
+ "expired",
2301
+ "other"
2302
+ ];
1746
2303
  var PRODUCT_EVENTS = [
1747
2304
  "account_created",
1748
2305
  "item.created",
@@ -1829,12 +2386,33 @@ var PRODUCT_EVENTS = [
1829
2386
  "agent.tool_call",
1830
2387
  // The public pages, which until now reported nothing at all: `capture_pageview`
1831
2388
  // is off, so a visitor who read the enterprise page and left was invisible.
1832
- // These five are the funnel that ends at `enterprise.org_create`.
2389
+ // These five are the funnel that ends at `enterprise.org_create`; the sixth
2390
+ // says how much of the home trailer people watched.
1833
2391
  "marketing.page_viewed",
1834
2392
  "marketing.section_viewed",
1835
2393
  "marketing.scroll_depth",
1836
2394
  "marketing.cta_clicked",
1837
- "marketing.language_switched"
2395
+ "marketing.language_switched",
2396
+ "marketing.video_progress",
2397
+ // Zas Nearby (#35). The server writes them; none carries a token, a code,
2398
+ // a key or a name.
2399
+ "nearby_token_registered",
2400
+ "nearby_resolved",
2401
+ "nearby_offer_sent",
2402
+ "nearby_offer_accepted",
2403
+ "nearby_offer_declined",
2404
+ "nearby_offer_withdrawn",
2405
+ "nearby_offer_expired",
2406
+ "nearby_code_created",
2407
+ "nearby_code_claimed",
2408
+ "nearby_code_allowed",
2409
+ "nearby_code_denied",
2410
+ "nearby_code_expired",
2411
+ "nearby_refused",
2412
+ // Zas Nearby on the device (#38): what the server cannot see. Bluetooth, the
2413
+ // touch detector, the reads, the choices on screen, the feedback and the
2414
+ // timing. Web sends the QR half only; Android and iOS send all of them.
2415
+ ...NEARBY_CLIENT_EVENTS
1838
2416
  ];
1839
2417
  var SERVER_OWNED_PRODUCT_EVENTS = [
1840
2418
  "account_created",
@@ -1849,7 +2427,20 @@ var SERVER_OWNED_PRODUCT_EVENTS = [
1849
2427
  "agent.send",
1850
2428
  "agent.edit",
1851
2429
  "agent.replace",
1852
- "agent.tool_call"
2430
+ "agent.tool_call",
2431
+ "nearby_token_registered",
2432
+ "nearby_resolved",
2433
+ "nearby_offer_sent",
2434
+ "nearby_offer_accepted",
2435
+ "nearby_offer_declined",
2436
+ "nearby_offer_withdrawn",
2437
+ "nearby_offer_expired",
2438
+ "nearby_code_created",
2439
+ "nearby_code_claimed",
2440
+ "nearby_code_allowed",
2441
+ "nearby_code_denied",
2442
+ "nearby_code_expired",
2443
+ "nearby_refused"
1853
2444
  ];
1854
2445
  var SERVER_OWNED_PRODUCT_EVENT_SET = new Set(SERVER_OWNED_PRODUCT_EVENTS);
1855
2446
  var CLIENT_PRODUCT_EVENTS = PRODUCT_EVENTS.filter(
@@ -1875,7 +2466,8 @@ var marketingSection = oneOf(
1875
2466
  "agents",
1876
2467
  "fit",
1877
2468
  "billing",
1878
- "signin"
2469
+ "signin",
2470
+ "trailer"
1879
2471
  );
1880
2472
  var marketingCta = oneOf(
1881
2473
  "see_enterprise",
@@ -1885,9 +2477,12 @@ var marketingCta = oneOf(
1885
2477
  "sign_in",
1886
2478
  "open_console",
1887
2479
  "switch_language",
1888
- "footer_home"
2480
+ "footer_home",
2481
+ "play_trailer",
2482
+ "get_app",
2483
+ "dismiss_app_banner"
1889
2484
  );
1890
- var marketingPlacement = oneOf("nav", "hero", "band", "fit", "billing", "signin", "footer", "menu", "limit");
2485
+ var marketingPlacement = oneOf("nav", "hero", "band", "fit", "billing", "signin", "footer", "menu", "limit", "trailer", "app_banner");
1891
2486
  var arrivalKind = oneOf("direct", "internal", "search", "social", "other");
1892
2487
  var uiLocale = oneOf("es-AR", "pt-BR", "en");
1893
2488
  var signinContext = oneOf("page", "alpha_access", "handoff_site", "handoff_share");
@@ -1963,8 +2558,9 @@ function agentDurationBucket(ms) {
1963
2558
  if (ms < 6e4) return "10_60s";
1964
2559
  return "gt_60s";
1965
2560
  }
2561
+ var transferErrorCode = oneOf("none", "network", "timeout", "authorization", "storage", "crypto", "worker", "other");
1966
2562
  var transferProperties = {
1967
- transfer_id: { type: "transfer_id" },
2563
+ transfer_id: { type: "uuid" },
1968
2564
  item_kind: oneOf("file", "text", "code", "link", "unknown"),
1969
2565
  transport: oneOf("storage", "manifest", "direct", "fallback"),
1970
2566
  cache_hit: bool,
@@ -1980,10 +2576,88 @@ var transferProperties = {
1980
2576
  foreground: bool,
1981
2577
  online: bool,
1982
2578
  outcome: oneOf("success", "failure", "cancelled"),
1983
- error_code: oneOf("none", "network", "timeout", "authorization", "storage", "crypto", "worker", "other"),
2579
+ error_code: transferErrorCode,
1984
2580
  timing_mode: oneOf("wall", "cumulative_work")
1985
2581
  };
2582
+ var nearbyOutcome = oneOf("ok", "refused", "network");
2583
+ var nearbyRefusal = oneOf(...NEARBY_REFUSAL_CODES);
2584
+ var signed = { type: "number" };
2585
+ var nearby = (props) => ({
2586
+ nearby_session: { type: "uuid" },
2587
+ role,
2588
+ method: oneOf("touch", "qr"),
2589
+ t_ms: count,
2590
+ ...props
2591
+ });
2592
+ var NEARBY_CLIENT_RULES = {
2593
+ "nearby.session_started": nearby({
2594
+ entry: oneOf("item_menu", "item_detail", "rail", "menu", "shortcut", "tile", "control", "app_link", "intent_extra", "offer", "browser"),
2595
+ bt_state: oneOf("on", "off", "unauthorized", "unsupported")
2596
+ }),
2597
+ "nearby.permission_result": nearby({ result: oneOf("granted", "denied", "already"), prompt_ms: count }),
2598
+ "nearby.bluetooth_enable_result": nearby({ result: oneOf("on", "declined") }),
2599
+ "nearby.advertising": nearby({
2600
+ state: oneOf("started", "failed", "stopped"),
2601
+ error_code: signed,
2602
+ reason: oneOf("background", "workspace_switch", "bt_off", "signed_out", "user")
2603
+ }),
2604
+ "nearby.token_failed": nearby({ code: oneOf("rate_limited", "network", "other"), attempt: count }),
2605
+ "nearby.connect_received": nearby({ count_in_session: count }),
2606
+ "nearby.scan_summary": nearby({
2607
+ outcome: oneOf("touched", "ambiguous", "none", "cancelled"),
2608
+ devices_seen: count,
2609
+ readings: count,
2610
+ max_rssi: signed,
2611
+ scan_ms: count
2612
+ }),
2613
+ "nearby.touch_detected": nearby({ median_rssi: signed, hold_ms: count, rivals: count, margin_db: signed, ms_since_scan: count }),
2614
+ "nearby.touch_ambiguous": nearby({ candidates: count, margin_db: signed }),
2615
+ "nearby.candidate_picked": nearby({ picked_rank: { type: "number", min: 1 }, decide_ms: count }),
2616
+ "nearby.carrier_read": nearby({
2617
+ outcome: oneOf("ok", "timeout", "disconnected", "no_service", "bad_length", "gatt_error"),
2618
+ gatt_status: signed,
2619
+ mtu: count,
2620
+ duration_ms: count,
2621
+ attempt: count
2622
+ }),
2623
+ "nearby.resolved": nearby({ outcome: nearbyOutcome, refusal: nearbyRefusal, key_check: oneOf("match", "mismatch"), duration_ms: count }),
2624
+ "nearby.face_shown": nearby({ touch_to_face_ms: count }),
2625
+ "nearby.confirm_result": nearby({ result: oneOf("send", "not_now", "closed"), decide_ms: count }),
2626
+ "nearby.offer_sent": nearby({ outcome: nearbyOutcome, refusal: nearbyRefusal, reused: bool, duration_ms: count }),
2627
+ "nearby.item_copied": nearby({
2628
+ outcome: oneOf("ok", "failed"),
2629
+ kind: itemKind,
2630
+ size_bucket: sizeBucket,
2631
+ // The transfer vocabulary: the copy is a transfer, and mobile has it already.
2632
+ error_code: transferErrorCode,
2633
+ duration_ms: count
2634
+ }),
2635
+ "nearby.offer_shown": nearby({ has_preview: bool, offer_age_ms: count }),
2636
+ "nearby.offer_answered": nearby({ answer: oneOf("accept", "decline", "dismissed", "expired"), keep: bool, decide_ms: count }),
2637
+ "nearby.answer_seen": nearby({ answer: oneOf("accepted", "declined", "no_answer"), wait_ms: count }),
2638
+ "nearby.code_shown": nearby({ refresh: oneOf("first", "timer", "manual", "expired") }),
2639
+ // `browser` is the web: the /n/ page opened in a browser, not in an app.
2640
+ "nearby.link_opened": nearby({ source: oneOf("app_link", "universal_link", "intent_extra", "browser"), signed_in: bool, has_key: bool }),
2641
+ "nearby.card_state": nearby({ state: oneOf("ready", "expired", "done", "taken", "no_key", "error") }),
2642
+ "nearby.claim_sent": nearby({ outcome: nearbyOutcome, refusal: nearbyRefusal, keep: bool }),
2643
+ // `outcome` is the allow or deny call itself; a copy after it fails apart.
2644
+ "nearby.claim_decided": nearby({ decision: oneOf("allow", "deny"), outcome: nearbyOutcome, refusal: nearbyRefusal, decide_ms: count, existing: bool }),
2645
+ "nearby.claim_answer": nearby({ answer: oneOf("allowed", "denied", "taken", "expired"), wait_ms: count }),
2646
+ "nearby.feedback": nearby({
2647
+ moment: oneOf("found", "ambiguous", "request", "accepted", "delivered", "declined", "error"),
2648
+ haptic: oneOf("rich", "basic", "none"),
2649
+ sound: oneOf("played", "muted", "off")
2650
+ }),
2651
+ "nearby.session_ended": nearby({
2652
+ outcome: oneOf("delivered", "declined", "denied", "no_answer", "cancelled", "failed", "abandoned"),
2653
+ fail_step: oneOf(...NEARBY_CLIENT_EVENTS),
2654
+ fail_code: oneOf(...NEARBY_FAIL_CODES),
2655
+ total_ms: count,
2656
+ touch_to_delivered_ms: count
2657
+ })
2658
+ };
1986
2659
  var EVENT_PROPERTY_RULES = {
2660
+ ...NEARBY_CLIENT_RULES,
1987
2661
  "transfer.started": transferProperties,
1988
2662
  "transfer.stage_started": transferProperties,
1989
2663
  "transfer.phase_timing": transferProperties,
@@ -2241,6 +2915,30 @@ var EVENT_PROPERTY_RULES = {
2241
2915
  placement: marketingPlacement
2242
2916
  },
2243
2917
  "marketing.language_switched": { surface: marketingSurface, from: uiLocale, to: uiLocale },
2918
+ // How far into a video the viewer got, once per milestone per page load.
2919
+ // `shape` is the cut that played (16:9 or 9:16); `muted` is whether the
2920
+ // sound was off when the milestone was reached.
2921
+ "marketing.video_progress": {
2922
+ surface: marketingSurface,
2923
+ video: oneOf("trailer"),
2924
+ locale: uiLocale,
2925
+ shape: oneOf("h", "v"),
2926
+ depth: oneOf("p25", "p50", "p75", "p100"),
2927
+ muted: bool
2928
+ },
2929
+ nearby_token_registered: {},
2930
+ nearby_resolved: {},
2931
+ nearby_offer_sent: { reused: bool },
2932
+ nearby_offer_accepted: { keep: bool },
2933
+ nearby_offer_declined: {},
2934
+ nearby_offer_withdrawn: {},
2935
+ nearby_offer_expired: {},
2936
+ nearby_code_created: {},
2937
+ nearby_code_claimed: { keep: bool },
2938
+ nearby_code_allowed: { existing: bool },
2939
+ nearby_code_denied: {},
2940
+ nearby_code_expired: {},
2941
+ nearby_refused: { code: oneOf(...NEARBY_REFUSAL_CODES), step: oneOf("token", "resolve", "offer", "accept", "code", "claim", "allow", "send") },
2244
2942
  "agent.tool_call": {
2245
2943
  tool: oneOf(...AGENT_TOOL_NAMES),
2246
2944
  result: oneOf("ok", "error"),
@@ -2254,7 +2952,7 @@ var EVENT_PROPERTY_RULES = {
2254
2952
  };
2255
2953
  var platformRule = oneOf("web", "android", "ios", "agent", "unknown");
2256
2954
  function valid(rule, value) {
2257
- if (rule.type === "transfer_id") return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
2955
+ if (rule.type === "uuid") return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
2258
2956
  if (rule.type === "boolean") return typeof value === "boolean";
2259
2957
  if (rule.type === "number") {
2260
2958
  return typeof value === "number" && Number.isFinite(value) && (rule.min === void 0 || value >= rule.min);
@@ -2346,13 +3044,13 @@ function toolCallProperties(input) {
2346
3044
  var REPORT_TIMEOUT_MS = 5e3;
2347
3045
  async function report(client, properties) {
2348
3046
  if (properties === null) return;
2349
- await new Promise((resolve2) => {
2350
- const timer = setTimeout(resolve2, 0);
3047
+ await new Promise((resolve4) => {
3048
+ const timer = setTimeout(resolve4, 0);
2351
3049
  timer.unref?.();
2352
3050
  });
2353
3051
  if (!telemetryState().on) return;
2354
- const timeout = new Promise((resolve2) => {
2355
- const timer = setTimeout(resolve2, REPORT_TIMEOUT_MS);
3052
+ const timeout = new Promise((resolve4) => {
3053
+ const timer = setTimeout(resolve4, REPORT_TIMEOUT_MS);
2356
3054
  timer.unref?.();
2357
3055
  });
2358
3056
  const body = { event: "agent.tool_call", properties };
@@ -2400,7 +3098,7 @@ var TransferTelemetry = class {
2400
3098
  this.slow = setTimeout(() => this.capture("transfer.slow"), 3e4);
2401
3099
  this.slow.unref?.();
2402
3100
  }
2403
- id = randomUUID();
3101
+ id = randomUUID3();
2404
3102
  started = performance.now();
2405
3103
  changed = this.started;
2406
3104
  stage = "staging";
@@ -2410,8 +3108,8 @@ var TransferTelemetry = class {
2410
3108
  slow;
2411
3109
  size = 0;
2412
3110
  completed = 0;
2413
- describe(size, kind = this.kind, transport = this.transport) {
2414
- this.size = Math.max(0, size);
3111
+ describe(size2, kind = this.kind, transport = this.transport) {
3112
+ this.size = Math.max(0, size2);
2415
3113
  this.kind = kind;
2416
3114
  this.transport = transport;
2417
3115
  }
@@ -2485,48 +3183,6 @@ import { randomBytes } from "node:crypto";
2485
3183
  import { openAsBlob } from "node:fs";
2486
3184
  import { basename as basename2 } from "node:path";
2487
3185
 
2488
- // src/shared/manifest.ts
2489
- import { xchacha20poly1305 } from "@noble/ciphers/chacha";
2490
- function newManifest(partial) {
2491
- return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
2492
- }
2493
- var ENVELOPE_MAGIC = 90;
2494
- var KEY_VERSION_LEGACY = 1;
2495
- function envelopeVersion(sealed) {
2496
- if (sealed.length > 26 && sealed[0] === ENVELOPE_MAGIC && sealed[1] >= 1) return sealed[1];
2497
- return KEY_VERSION_LEGACY;
2498
- }
2499
- function sealBytes(key, version, plain) {
2500
- if (version < 1 || version > 255) throw new Error("key version out of range");
2501
- const nonce = new Uint8Array(24);
2502
- crypto.getRandomValues(nonce);
2503
- const ct = xchacha20poly1305(key, nonce).encrypt(plain);
2504
- return concatBytes(new Uint8Array([ENVELOPE_MAGIC, version]), nonce, ct);
2505
- }
2506
- function openSealed(key, sealed) {
2507
- const versioned = envelopeVersion(sealed) !== KEY_VERSION_LEGACY || sealed[0] === ENVELOPE_MAGIC;
2508
- if (versioned && sealed.length > 26) {
2509
- try {
2510
- return xchacha20poly1305(key, sealed.slice(2, 26)).decrypt(sealed.slice(26));
2511
- } catch {
2512
- }
2513
- }
2514
- return xchacha20poly1305(key, sealed.slice(0, 24)).decrypt(sealed.slice(24));
2515
- }
2516
- var sealRaw = sealBytes;
2517
- var openRaw = openSealed;
2518
- function sealManifest(channelKey, manifest, version = KEY_VERSION_LEGACY) {
2519
- return sealBytes(channelKey, version, new TextEncoder().encode(JSON.stringify(manifest)));
2520
- }
2521
- function openManifest(channelKey, sealed) {
2522
- const m = JSON.parse(new TextDecoder().decode(openSealed(channelKey, sealed)));
2523
- if (!Array.isArray(m.chunks)) m.chunks = [];
2524
- return m;
2525
- }
2526
- function decryptChannelName(channelKey, sealed) {
2527
- return new TextDecoder().decode(openSealed(channelKey, sealed));
2528
- }
2529
-
2530
3186
  // src/file-source.ts
2531
3187
  import { promises as fsp, statSync } from "node:fs";
2532
3188
  async function readFileRange(path, start, length) {
@@ -2670,8 +3326,8 @@ var CONCURRENCY = 2;
2670
3326
  function aborted(signal) {
2671
3327
  if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
2672
3328
  }
2673
- var pause = (ms, signal) => new Promise((resolve2, reject) => {
2674
- const timer = setTimeout(resolve2, ms);
3329
+ var pause = (ms, signal) => new Promise((resolve4, reject) => {
3330
+ const timer = setTimeout(resolve4, ms);
2675
3331
  signal?.addEventListener("abort", () => {
2676
3332
  clearTimeout(timer);
2677
3333
  reject(new DOMException("Aborted", "AbortError"));
@@ -2691,14 +3347,14 @@ async function uploadFallback(options) {
2691
3347
  for (let first = 1; first <= meta.part_count; first += URL_BATCH) {
2692
3348
  aborted(signal);
2693
3349
  const count2 = Math.min(URL_BATCH, meta.part_count - first + 1);
2694
- const signed = await options.getUrls(first, count2);
2695
- if (signed.length !== count2 || signed.some((entry, i) => entry.part_number !== first + i || typeof entry.url !== "string")) throw new Error("bad_fallback_urls");
3350
+ const signed2 = await options.getUrls(first, count2);
3351
+ if (signed2.length !== count2 || signed2.some((entry, i) => entry.part_number !== first + i || typeof entry.url !== "string")) throw new Error("bad_fallback_urls");
2696
3352
  let cursor = 0;
2697
- const workers = Array.from({ length: Math.min(CONCURRENCY, signed.length) }, async () => {
3353
+ const workers = Array.from({ length: Math.min(CONCURRENCY, signed2.length) }, async () => {
2698
3354
  for (; ; ) {
2699
3355
  const at = cursor++;
2700
- if (at >= signed.length) return;
2701
- const entry = signed[at];
3356
+ if (at >= signed2.length) return;
3357
+ const entry = signed2[at];
2702
3358
  const partNumber = entry.part_number;
2703
3359
  const plainStart = (partNumber - 1) * meta.part_bytes;
2704
3360
  const plainEnd = Math.min(file.size, plainStart + meta.part_bytes);
@@ -2808,17 +3464,28 @@ import { x25519 as x255192 } from "@noble/curves/ed25519";
2808
3464
  var ASSIGNMENT_MAGIC = new Uint8Array([90, 67, 1]);
2809
3465
  var ASSIGNMENT_HEADER_BYTES = 3 + 1 + 32 + 24;
2810
3466
  function assignmentEnvelope(envelope) {
2811
- return envelope.length >= ASSIGNMENT_HEADER_BYTES + 16 && envelope[0] === ASSIGNMENT_MAGIC[0] && envelope[1] === ASSIGNMENT_MAGIC[1] && envelope[2] === ASSIGNMENT_MAGIC[2];
3467
+ return envelope[0] === ASSIGNMENT_MAGIC[0] && envelope[1] === ASSIGNMENT_MAGIC[1] && (envelope[2] === 1 || envelope[2] === 2);
3468
+ }
3469
+ function validateAssignment(envelope) {
3470
+ if (!assignmentEnvelope(envelope) || envelope.length !== ASSIGNMENT_HEADER_BYTES + 32 + 16 || envelope[3] < 1) throw new Error("bad envelope");
3471
+ }
3472
+ function assignmentKdf(shared, header) {
3473
+ return hkdf512(shared, header[2] === 2 ? concatBytes(new TextEncoder().encode("ZAS-CHANNEL-ASSIGNMENT-ENVELOPE-V2"), header) : "ZAS-CHANNEL-ASSIGNMENT-ENVELOPE-V1", 32);
2812
3474
  }
2813
3475
  function openChannelAssignment(privateKey, envelope) {
2814
- if (!assignmentEnvelope(envelope)) throw new Error("bad envelope");
3476
+ validateAssignment(envelope);
2815
3477
  const ephemeralPublic = envelope.slice(4, 36);
2816
3478
  const nonce = envelope.slice(36, 60);
2817
3479
  const shared = x255192.getSharedSecret(privateKey, ephemeralPublic);
2818
- const key = hkdf512(shared, "ZAS-CHANNEL-ASSIGNMENT-ENVELOPE-V1", 32);
2819
- const plain = xchacha20poly13052(key, nonce).decrypt(envelope.slice(60));
2820
- shared.fill(0);
2821
- key.fill(0);
3480
+ const header = envelope.slice(0, ASSIGNMENT_HEADER_BYTES);
3481
+ const key = assignmentKdf(shared, header);
3482
+ let plain;
3483
+ try {
3484
+ plain = xchacha20poly13052(key, nonce, envelope[2] === 2 ? header : void 0).decrypt(envelope.slice(60));
3485
+ } finally {
3486
+ shared.fill(0);
3487
+ key.fill(0);
3488
+ }
2822
3489
  if (plain.length !== 32) throw new Error("bad channel key");
2823
3490
  return plain;
2824
3491
  }
@@ -2975,74 +3642,6 @@ function decryptChunk(key, nonce, ciphertext) {
2975
3642
  return xchacha20poly13053(key, nonce).decrypt(ciphertext);
2976
3643
  }
2977
3644
 
2978
- // src/shared/oprf.ts
2979
- import { RistrettoPoint } from "@noble/curves/ed25519";
2980
- import { sha512 as sha5122 } from "@noble/hashes/sha2";
2981
- import { invert, mod } from "@noble/curves/abstract/modular";
2982
- var ORDER = 2n ** 252n + 27742317777372353535851937790883648493n;
2983
- var te = new TextEncoder();
2984
- function i2osp(value, length) {
2985
- const out = new Uint8Array(length);
2986
- for (let i = length - 1; i >= 0; i--) {
2987
- out[i] = value & 255;
2988
- value >>>= 8;
2989
- }
2990
- return out;
2991
- }
2992
- function expandMessageXmd(msg, dst, lenInBytes) {
2993
- const bInBytes = 64;
2994
- const rInBytes = 128;
2995
- const ell = Math.ceil(lenInBytes / bInBytes);
2996
- if (ell > 255) throw new Error("expand_message_xmd: ell too large");
2997
- const dstPrime = concatBytes(dst, i2osp(dst.length, 1));
2998
- const zPad = new Uint8Array(rInBytes);
2999
- const lIbStr = i2osp(lenInBytes, 2);
3000
- const msgPrime = concatBytes(zPad, msg, lIbStr, i2osp(0, 1), dstPrime);
3001
- const b0 = sha5122(msgPrime);
3002
- const b = [];
3003
- b[0] = sha5122(concatBytes(b0, i2osp(1, 1), dstPrime));
3004
- for (let i = 2; i <= ell; i++) {
3005
- const xored = new Uint8Array(bInBytes);
3006
- for (let j = 0; j < bInBytes; j++) xored[j] = b0[j] ^ b[i - 2][j];
3007
- b[i - 1] = sha5122(concatBytes(xored, i2osp(i, 1), dstPrime));
3008
- }
3009
- return concatBytes(...b).slice(0, lenInBytes);
3010
- }
3011
- function bytesToBigIntBE(bytes) {
3012
- let v = 0n;
3013
- for (const byte of bytes) v = v << 8n | BigInt(byte);
3014
- return v;
3015
- }
3016
- function hashToGroup(input) {
3017
- const dst = te.encode("HashToGroup-" + OPRF_CONTEXT);
3018
- const uniform = expandMessageXmd(input, dst, 64);
3019
- return RistrettoPoint.hashToCurve(uniform);
3020
- }
3021
- function randomScalar() {
3022
- const bytes = new Uint8Array(64);
3023
- crypto.getRandomValues(bytes);
3024
- const s = mod(bytesToBigIntBE(bytes), ORDER);
3025
- return s === 0n ? 1n : s;
3026
- }
3027
- function oprfBlind(input) {
3028
- const blind = randomScalar();
3029
- const P = hashToGroup(input);
3030
- return { blind, blindedElement: P.multiply(blind).toRawBytes() };
3031
- }
3032
- function oprfFinalize(input, blind, evaluatedElement) {
3033
- const E = RistrettoPoint.fromHex(evaluatedElement);
3034
- const N = E.multiply(invert(blind, ORDER));
3035
- const unblinded = N.toRawBytes();
3036
- const hashInput = concatBytes(
3037
- i2osp(input.length, 2),
3038
- input,
3039
- i2osp(unblinded.length, 2),
3040
- unblinded,
3041
- te.encode("Finalize")
3042
- );
3043
- return sha5122(hashInput);
3044
- }
3045
-
3046
3645
  // src/thumbnail.ts
3047
3646
  import { Jimp } from "jimp";
3048
3647
  var PREFIX = "data:image/jpeg;base64,";
@@ -3078,9 +3677,9 @@ var UPLOAD_CONCURRENCY = 4;
3078
3677
  var OPRF_PROBE_BATCH = 200;
3079
3678
  var MAX_REFUSALS_PER_SEND = 8;
3080
3679
  var NOTE_NAME_MAX = 40;
3081
- function batches(items, size) {
3680
+ function batches(items, size2) {
3082
3681
  const out = [];
3083
- for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
3682
+ for (let i = 0; i < items.length; i += size2) out.push(items.slice(i, i + size2));
3084
3683
  return out;
3085
3684
  }
3086
3685
  function sliceSize(ctx) {
@@ -3177,8 +3776,8 @@ function remember(ctx, key, receipt) {
3177
3776
  saveFingerprints(ctx.profile, { entries });
3178
3777
  }
3179
3778
  function sleep(ms) {
3180
- return new Promise((resolve2) => {
3181
- setTimeout(resolve2, ms);
3779
+ return new Promise((resolve4) => {
3780
+ setTimeout(resolve4, ms);
3182
3781
  });
3183
3782
  }
3184
3783
  var CHUNK_PUT_RETRY_DELAY_MS = 1e3;
@@ -3232,12 +3831,12 @@ async function uploadChunk(ctx, enc) {
3232
3831
  }
3233
3832
  return commitChunk(ctx, enc.blobId, reserved.upload_id);
3234
3833
  }
3235
- async function proveChunk(ctx, enc, challenge) {
3236
- const samples = challenge.offsets.map((o) => enc.ciphertext.slice(o, o + challenge.sample_len));
3834
+ async function proveChunk(ctx, enc, challenge2) {
3835
+ const samples = challenge2.offsets.map((o) => enc.ciphertext.slice(o, o + challenge2.sample_len));
3237
3836
  try {
3238
3837
  const { cap } = await ctx.client.api("POST", `/blobs/${enc.blobId}/prove`, {
3239
- challenge_id: challenge.challenge_id,
3240
- mac: bytesToHex(hmacSha256(b64ToBytes(challenge.nonce), concatBytes(...samples)))
3838
+ challenge_id: challenge2.challenge_id,
3839
+ mac: bytesToHex(hmacSha256(b64ToBytes(challenge2.nonce), concatBytes(...samples)))
3241
3840
  });
3242
3841
  return cap;
3243
3842
  } catch (err) {
@@ -3358,16 +3957,23 @@ async function placeFile(ctx, bytes, onPhase) {
3358
3957
  const plains = [];
3359
3958
  for await (const piece of chunkStream([bytes])) plains.push(piece);
3360
3959
  const hashes = await Promise.all(plains.map((p) => blake3Bytes(p)));
3361
- const blinds = hashes.map((h) => oprfBlind(h));
3960
+ const policy = await ctx.client.dedupePolicy();
3961
+ const blinds = hashes.map((h) => blindDedupe(h, policy));
3362
3962
  const evaluated = [];
3363
3963
  for (const slice of batches(blinds.map((b) => bytesToB64(b.blindedElement)), sliceSize(ctx))) {
3364
- const answers = await ctx.client.oprfEvaluate(slice);
3964
+ const answers = await ctx.client.oprfEvaluate(slice, policy);
3365
3965
  if (answers.length !== slice.length) throw new ZasError("oprf_failed", 0);
3366
3966
  for (const one of answers) evaluated.push(one);
3367
3967
  }
3368
3968
  onPhase?.("encrypting");
3969
+ let outputs;
3970
+ try {
3971
+ outputs = hashes.map((hash, i) => oprfFinalize(hash, blinds[i].blind, b64ToBytes(evaluated[i])));
3972
+ } catch {
3973
+ throw new ZasError("oprf_failed", 0);
3974
+ }
3369
3975
  const encs = await Promise.all(plains.map(
3370
- (plain, i) => encryptChunk(oprfFinalize(hashes[i], blinds[i].blind, b64ToBytes(evaluated[i])), plain)
3976
+ (plain, i) => encryptChunk(outputs[i], plain)
3371
3977
  ));
3372
3978
  onPhase?.("uploading");
3373
3979
  return placeChunks(ctx, encs);
@@ -3551,8 +4157,8 @@ var SIGNALS_FOR_SENDER = {
3551
4157
  where: { fieldFilter: { field: { fieldPath: "for" }, op: "EQUAL", value: { stringValue: "sender" } } }
3552
4158
  };
3553
4159
  var stringField = (doc, key) => doc?.fields?.[key]?.stringValue;
3554
- var defaultSleep2 = (ms) => new Promise((resolve2) => {
3555
- setTimeout(resolve2, ms);
4160
+ var defaultSleep2 = (ms) => new Promise((resolve4) => {
4161
+ setTimeout(resolve4, ms);
3556
4162
  });
3557
4163
  var newDeviceToken = () => randomBytes(16).toString("base64url");
3558
4164
  var webrtc = null;
@@ -3646,8 +4252,8 @@ async function sendDirectMeasured(ctx, input, report2, deps = {}) {
3646
4252
  const turn = await fetchIce();
3647
4253
  report2("connecting");
3648
4254
  let resolveOutcome;
3649
- const outcome = new Promise((resolve2) => {
3650
- resolveOutcome = resolve2;
4255
+ const outcome = new Promise((resolve4) => {
4256
+ resolveOutcome = resolve4;
3651
4257
  });
3652
4258
  let diag;
3653
4259
  let path;
@@ -3834,25 +4440,79 @@ async function sendDirectFallback(...args) {
3834
4440
  // src/read.ts
3835
4441
  import { randomBytes as randomBytes2 } from "node:crypto";
3836
4442
  import {
3837
- closeSync,
4443
+ closeSync as closeSync2,
3838
4444
  existsSync as existsSync2,
3839
4445
  mkdirSync as mkdirSync2,
3840
- mkdtempSync,
3841
- openSync,
3842
- renameSync as renameSync2,
4446
+ openSync as openSync2,
3843
4447
  rmdirSync,
3844
- statSync as statSync2,
3845
- unlinkSync,
4448
+ unlinkSync as unlinkSync2,
3846
4449
  writeSync
3847
4450
  } from "node:fs";
4451
+ import { dirname as dirname2 } from "node:path";
4452
+
4453
+ // src/destination.ts
4454
+ import { linkSync, mkdtempSync, statSync as statSync2, unlinkSync } from "node:fs";
3848
4455
  import { tmpdir } from "node:os";
3849
- import { dirname, extname as extname2, join as join2 } from "node:path";
4456
+ import { basename as basename3, dirname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "node:path";
4457
+
4458
+ // src/shared/filename.ts
4459
+ function safeFileName(raw, fallback = "file") {
4460
+ const clean = (value) => (value.split(/[\\/]/).pop() ?? "").replace(/[\x00-\x1f\x7f-\x9f<>:"|?*]/g, "_").trim().replace(/^\.+/, "").replace(/[. ]+$/, "");
4461
+ let name = clean(typeof raw === "string" ? raw : "") || clean(fallback) || "file";
4462
+ if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(name)) name = "_" + name;
4463
+ if (name.length <= 180) return name;
4464
+ const dot = name.lastIndexOf(".");
4465
+ const ext = dot > 0 ? name.slice(dot) : "";
4466
+ return ext.length > 1 && ext.length <= 17 ? name.slice(0, 180 - ext.length) + ext : name.slice(0, 180);
4467
+ }
4468
+
4469
+ // src/destination.ts
4470
+ function contained(folder, name) {
4471
+ const target = resolve2(folder, name);
4472
+ const rel = relative(resolve2(folder), target);
4473
+ if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
4474
+ throw new ZasError("write_failed", 0);
4475
+ }
4476
+ return target;
4477
+ }
4478
+ function destinationOf(dest, name, fallback) {
4479
+ const base = safeFileName(name, fallback);
4480
+ if (dest === void 0) {
4481
+ const created = mkdtempSync(join3(tmpdir(), "zas-agent-"));
4482
+ return { target: contained(created, base), created };
4483
+ }
4484
+ if (statSync2(dest, { throwIfNoEntry: false })?.isDirectory()) return { target: contained(dest, base) };
4485
+ const leaf = basename3(dest);
4486
+ if (!leaf || leaf !== safeFileName(leaf)) throw new ZasError("write_failed", 0);
4487
+ return { target: contained(dirname(dest), leaf) };
4488
+ }
4489
+ function publishDownload(tmp, target) {
4490
+ const folder = dirname(target);
4491
+ const base = basename3(target);
4492
+ const dot = base.lastIndexOf(".");
4493
+ const ext = dot > 0 && base.length - dot <= 17 ? base.slice(dot) : "";
4494
+ const stem = ext ? base.slice(0, -ext.length) : base;
4495
+ for (let n = 0; n <= 100; n++) {
4496
+ const suffix = n ? ` (${n})` : "";
4497
+ const name = stem.slice(0, 180 - suffix.length - ext.length) + suffix + ext;
4498
+ const candidate = contained(folder, name);
4499
+ try {
4500
+ linkSync(tmp, candidate);
4501
+ } catch (error) {
4502
+ if (error.code === "EEXIST") continue;
4503
+ throw error;
4504
+ }
4505
+ unlinkSync(tmp);
4506
+ return candidate;
4507
+ }
4508
+ throw new ZasError("write_failed", 0);
4509
+ }
4510
+
4511
+ // src/read.ts
3850
4512
  var DEFAULT_LIMIT = 20;
3851
4513
  var MAX_LIMIT = 50;
3852
- var DOWNLOAD_PREFIX = "zas-agent-";
3853
4514
  var ID_SEGMENT = /^(?!__)[A-Za-z0-9_-]{1,128}$/;
3854
4515
  var MAX_CHUNKS = 8192;
3855
- var MAX_DUPLICATES = 100;
3856
4516
  function stringOf(value) {
3857
4517
  return typeof value?.stringValue === "string" ? value.stringValue : void 0;
3858
4518
  }
@@ -3998,29 +4658,6 @@ async function fetchChunk(ctx, chunk) {
3998
4658
  throw new ZasError("invalid_cap", 403);
3999
4659
  }
4000
4660
  }
4001
- function safeName(name, fallback) {
4002
- const base = (typeof name === "string" ? name : "").split(/[\\/]/).pop() ?? "";
4003
- return base.replace(/^\.+/, "").trim() || fallback;
4004
- }
4005
- function freeName(target) {
4006
- if (!existsSync2(target)) return target;
4007
- const ext = extname2(target);
4008
- const stem = target.slice(0, target.length - ext.length);
4009
- for (let n = 1; n <= MAX_DUPLICATES; n++) {
4010
- const candidate = `${stem} (${n})${ext}`;
4011
- if (!existsSync2(candidate)) return candidate;
4012
- }
4013
- throw new ZasError("write_failed", 0, target);
4014
- }
4015
- function destinationOf(dest, name, fallback) {
4016
- const base = safeName(name, fallback);
4017
- if (dest === void 0) {
4018
- const created = mkdtempSync(join2(tmpdir(), DOWNLOAD_PREFIX));
4019
- return { target: freeName(join2(created, base)), created };
4020
- }
4021
- const at = statSync2(dest, { throwIfNoEntry: false })?.isDirectory() ? join2(dest, base) : dest;
4022
- return { target: freeName(at) };
4023
- }
4024
4661
  async function loadItem(ctx, channel, id) {
4025
4662
  const grant = await readGrant(ctx, channel);
4026
4663
  if (!ID_SEGMENT.test(id)) throw new ZasError("not_found", 404);
@@ -4061,20 +4698,20 @@ async function getItemMeasured(ctx, channel, id, dest) {
4061
4698
  const chosen = destinationOf(dest, manifest.name, row.id);
4062
4699
  target = chosen.target;
4063
4700
  created = chosen.created;
4064
- mkdirSync2(dirname(target), { recursive: true, mode: 448 });
4701
+ mkdirSync2(dirname2(target), { recursive: true, mode: 448 });
4065
4702
  tmp = `${target}.${randomBytes2(6).toString("hex")}.tmp`;
4066
- fd = openSync(tmp, "wx", 384);
4703
+ fd = openSync2(tmp, "wx", 384);
4067
4704
  for (const chunk of manifest.chunks) {
4068
4705
  const bytes = await fetchChunk(ctx, chunk);
4069
4706
  ctx.transferTelemetry?.mark("assembling");
4070
4707
  written += writeSync(fd, bytes);
4071
4708
  ctx.transferTelemetry?.progress(written);
4072
4709
  }
4073
- const size = typeof manifest.size === "number" ? manifest.size : 0;
4074
- if (size > 0 && written !== size) throw new ZasError("not_found", 404);
4075
- closeSync(fd);
4710
+ const size2 = typeof manifest.size === "number" ? manifest.size : 0;
4711
+ if (size2 > 0 && written !== size2) throw new ZasError("not_found", 404);
4712
+ closeSync2(fd);
4076
4713
  fd = void 0;
4077
- renameSync2(tmp, target);
4714
+ target = publishDownload(tmp, target);
4078
4715
  } catch (err) {
4079
4716
  if (err instanceof ZasError) throw err;
4080
4717
  const failure = err;
@@ -4084,11 +4721,11 @@ async function getItemMeasured(ctx, channel, id, dest) {
4084
4721
  throw new ZasError("network", 0, String(err?.message ?? err));
4085
4722
  } finally {
4086
4723
  if (fd !== void 0) try {
4087
- closeSync(fd);
4724
+ closeSync2(fd);
4088
4725
  } catch {
4089
4726
  }
4090
4727
  if (tmp !== void 0 && existsSync2(tmp)) try {
4091
- unlinkSync(tmp);
4728
+ unlinkSync2(tmp);
4092
4729
  } catch {
4093
4730
  }
4094
4731
  if (created !== void 0) try {
@@ -4235,16 +4872,15 @@ async function replaceFile(ctx, input, onPhase) {
4235
4872
  // src/receive.ts
4236
4873
  import { randomBytes as randomBytes3 } from "node:crypto";
4237
4874
  import {
4238
- closeSync as closeSync2,
4875
+ closeSync as closeSync3,
4239
4876
  existsSync as existsSync3,
4240
4877
  mkdirSync as mkdirSync3,
4241
- openSync as openSync2,
4242
- renameSync as renameSync3,
4878
+ openSync as openSync3,
4243
4879
  rmdirSync as rmdirSync2,
4244
- unlinkSync as unlinkSync2,
4880
+ unlinkSync as unlinkSync3,
4245
4881
  writeSync as writeSync2
4246
4882
  } from "node:fs";
4247
- import { dirname as dirname2 } from "node:path";
4883
+ import { dirname as dirname3 } from "node:path";
4248
4884
  var OFFER_WAIT_MS2 = 9.5 * 60 * 1e3;
4249
4885
  var OFFER_POLL_MS2 = 1e3;
4250
4886
  var SIGNAL_POLL_MS2 = 400;
@@ -4285,9 +4921,9 @@ var DiskSink = class {
4285
4921
  const chosen = destinationOf(this.dest, meta.name, this.fallbackName);
4286
4922
  this.target = chosen.target;
4287
4923
  this.created = chosen.created;
4288
- mkdirSync3(dirname2(this.target), { recursive: true, mode: 448 });
4924
+ mkdirSync3(dirname3(this.target), { recursive: true, mode: 448 });
4289
4925
  this.tmp = `${this.target}.${randomBytes3(6).toString("hex")}.tmp`;
4290
- this.fd = openSync2(this.tmp, "wx", 384);
4926
+ this.fd = openSync3(this.tmp, "wx", 384);
4291
4927
  } catch (error) {
4292
4928
  throw this.remember(error);
4293
4929
  }
@@ -4303,9 +4939,9 @@ var DiskSink = class {
4303
4939
  close() {
4304
4940
  try {
4305
4941
  if (this.fd === void 0) return;
4306
- closeSync2(this.fd);
4942
+ closeSync3(this.fd);
4307
4943
  this.fd = void 0;
4308
- renameSync3(this.tmp, this.target);
4944
+ this.target = publishDownload(this.tmp, this.target);
4309
4945
  this.tmp = void 0;
4310
4946
  } catch (error) {
4311
4947
  throw this.remember(error);
@@ -4316,14 +4952,14 @@ var DiskSink = class {
4316
4952
  abort() {
4317
4953
  if (this.fd !== void 0) {
4318
4954
  try {
4319
- closeSync2(this.fd);
4955
+ closeSync3(this.fd);
4320
4956
  } catch {
4321
4957
  }
4322
4958
  this.fd = void 0;
4323
4959
  }
4324
4960
  if (this.tmp !== void 0) {
4325
4961
  if (existsSync3(this.tmp)) try {
4326
- unlinkSync2(this.tmp);
4962
+ unlinkSync3(this.tmp);
4327
4963
  } catch {
4328
4964
  }
4329
4965
  this.tmp = void 0;
@@ -4342,6 +4978,7 @@ var DiskSink = class {
4342
4978
  * also carries. */
4343
4979
  remember(error) {
4344
4980
  const failure = error;
4981
+ if (error instanceof ZasError && error.code === "write_failed") this.failure = error;
4345
4982
  if (typeof failure?.syscall === "string" || typeof failure?.errno === "number") this.failure = failure;
4346
4983
  return error;
4347
4984
  }
@@ -4416,8 +5053,8 @@ async function receiveDirectMeasured(ctx, input, report2, deps = {}) {
4416
5053
  report2("connecting");
4417
5054
  const sink = new DiskSink(input.dest, offerId);
4418
5055
  let resolveOutcome;
4419
- const outcome = new Promise((resolve2) => {
4420
- resolveOutcome = resolve2;
5056
+ const outcome = new Promise((resolve4) => {
5057
+ resolveOutcome = resolve4;
4421
5058
  });
4422
5059
  let diag;
4423
5060
  let via;
@@ -4589,7 +5226,7 @@ async function receiveDirectFallback(...args) {
4589
5226
  }
4590
5227
 
4591
5228
  // src/jobs.ts
4592
- import { randomUUID as randomUUID2 } from "node:crypto";
5229
+ import { randomUUID as randomUUID4 } from "node:crypto";
4593
5230
  var DEFAULT_WAIT_MS = 6e4;
4594
5231
  var HISTORY = 50;
4595
5232
  var JobRunner = class {
@@ -4606,7 +5243,7 @@ var JobRunner = class {
4606
5243
  }
4607
5244
  start(kind, title, channel, work) {
4608
5245
  const job = {
4609
- id: randomUUID2(),
5246
+ id: randomUUID4(),
4610
5247
  kind,
4611
5248
  title,
4612
5249
  channel,
@@ -4649,8 +5286,8 @@ var JobRunner = class {
4649
5286
  const settled = this.settled.get(job);
4650
5287
  if (!settled) return job;
4651
5288
  let timer;
4652
- const deadline = new Promise((resolve2) => {
4653
- timer = setTimeout(() => resolve2(job), this.waitMs);
5289
+ const deadline = new Promise((resolve4) => {
5290
+ timer = setTimeout(() => resolve4(job), this.waitMs);
4654
5291
  });
4655
5292
  try {
4656
5293
  return await Promise.race([settled, deadline]);
@@ -4666,16 +5303,351 @@ var JobRunner = class {
4666
5303
  }
4667
5304
  };
4668
5305
 
5306
+ // src/updates.ts
5307
+ import { spawn as spawn2, execFile } from "node:child_process";
5308
+ import { randomUUID as randomUUID5 } from "node:crypto";
5309
+ import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync3, realpathSync, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
5310
+ import { delimiter, dirname as dirname4, join as join4, resolve as resolve3 } from "node:path";
5311
+ import { fileURLToPath } from "node:url";
5312
+ var DAY = 24 * 60 * 60 * 1e3;
5313
+ var REGISTRY = "https://registry.npmjs.org";
5314
+ var RELEASE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
5315
+ function newerRelease(candidate, current) {
5316
+ if (!RELEASE.test(candidate) || !RELEASE.test(current)) return false;
5317
+ const a = candidate.split(".").map(BigInt), b = current.split(".").map(BigInt);
5318
+ for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
5319
+ return false;
5320
+ }
5321
+ function updatesEnabled() {
5322
+ const env = process.env.ZAS_AGENT_AUTO_UPDATE;
5323
+ if (env !== void 0) return env === "1";
5324
+ return loadSettings()?.auto_update !== false;
5325
+ }
5326
+ function setUpdates(enabled) {
5327
+ saveSettings({ ...loadSettings(), version: 1, auto_update: enabled });
5328
+ }
5329
+ function isReleaseInstall(version, selfUrl) {
5330
+ if (!RELEASE.test(version)) return false;
5331
+ const root = dirname4(dirname4(fileURLToPath(selfUrl)));
5332
+ return !existsSync4(join4(root, ".git"));
5333
+ }
5334
+ var updateHome = () => resolve3(agentHome(), ".updates");
5335
+ function checkedDirectory(path) {
5336
+ const stat = lstatSync2(path);
5337
+ if (!stat.isDirectory() || stat.isSymbolicLink() || process.platform !== "win32" && (stat.uid !== process.getuid?.() || (stat.mode & 18) !== 0)) {
5338
+ throw new Error("Update directory must be owned by this user and not writable by others.");
5339
+ }
5340
+ }
5341
+ async function prepareHome(signal) {
5342
+ const home = updateHome();
5343
+ mkdirSync4(home, { recursive: true, mode: 448 });
5344
+ checkedDirectory(home);
5345
+ if (process.platform === "win32") {
5346
+ const script = `$ErrorActionPreference='Stop'; $dir=[Console]::In.ReadToEnd(); $sid=[Security.Principal.WindowsIdentity]::GetCurrent().User; $acl=New-Object Security.AccessControl.DirectorySecurity; $acl.SetOwner($sid); $acl.SetAccessRuleProtection($true,$false); $rule=New-Object Security.AccessControl.FileSystemAccessRule($sid,'FullControl','ContainerInherit,ObjectInherit','None','Allow'); $acl.AddAccessRule($rule); [IO.Directory]::SetAccessControl($dir,$acl)`;
5347
+ await new Promise((done, fail) => {
5348
+ const child = execFile(
5349
+ join4(process.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
5350
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
5351
+ { windowsHide: true, timeout: 15e3, signal, maxBuffer: 4096 },
5352
+ (error) => error ? fail(new Error("Could not protect the update directory.")) : done()
5353
+ );
5354
+ child.stdin?.on("error", () => {
5355
+ });
5356
+ child.stdin?.end(home);
5357
+ });
5358
+ }
5359
+ return home;
5360
+ }
5361
+ function readState() {
5362
+ try {
5363
+ checkedDirectory(updateHome());
5364
+ const value = JSON.parse(readFileSync3(join4(updateHome(), "state.json"), "utf8"));
5365
+ return value && typeof value === "object" ? value : {};
5366
+ } catch {
5367
+ return {};
5368
+ }
5369
+ }
5370
+ function writeState(home, state) {
5371
+ const tmp = join4(home, `state-${randomUUID5()}.tmp`);
5372
+ try {
5373
+ writeFileSync3(tmp, JSON.stringify(state), { flag: "wx", mode: 384 });
5374
+ renameSync2(tmp, join4(home, "state.json"));
5375
+ } finally {
5376
+ rmSync3(tmp, { force: true });
5377
+ }
5378
+ }
5379
+ function releaseEntry(ready) {
5380
+ if (!ready || !RELEASE.test(ready.version) || typeof ready.directory !== "string" || !/^release-[a-f0-9-]{36}$/.test(ready.directory)) throw new Error("Invalid staged release.");
5381
+ let dir = updateHome();
5382
+ checkedDirectory(dir);
5383
+ for (const part of [ready.directory, "node_modules", "zas-agent", "dist"]) {
5384
+ dir = join4(dir, part);
5385
+ checkedDirectory(dir);
5386
+ }
5387
+ const entry = join4(dir, "cli.js");
5388
+ const stat = lstatSync2(entry);
5389
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Invalid release entry point.");
5390
+ const pkg = JSON.parse(readFileSync3(join4(dirname4(dir), "package.json"), "utf8"));
5391
+ if (pkg.name !== "zas-agent" || pkg.version !== ready.version) throw new Error("Release version does not match.");
5392
+ return entry;
5393
+ }
5394
+ function stagedRelease(current) {
5395
+ if (!updatesEnabled()) return null;
5396
+ try {
5397
+ const ready = readState().ready;
5398
+ return ready && newerRelease(ready.version, current) ? { version: ready.version, entry: releaseEntry(ready) } : null;
5399
+ } catch {
5400
+ return null;
5401
+ }
5402
+ }
5403
+ async function verifiedStagedRelease(current) {
5404
+ const ready = stagedRelease(current);
5405
+ if (!ready) return null;
5406
+ try {
5407
+ await verify(ready.entry, ready.version);
5408
+ return ready;
5409
+ } catch {
5410
+ return null;
5411
+ }
5412
+ }
5413
+ function updateLine(current) {
5414
+ const ready = stagedRelease(current);
5415
+ return `Updates: ${updatesEnabled() ? "on" : "off"}${process.env.ZAS_AGENT_AUTO_UPDATE !== void 0 ? " (ZAS_AGENT_AUTO_UPDATE)" : ""}. Running ${current}.` + (ready ? ` ${ready.version} ready; restart the agent to use it.` : "");
5416
+ }
5417
+ function npmCommand() {
5418
+ const candidates = [
5419
+ ...process.env.npm_execpath ? [process.env.npm_execpath] : [],
5420
+ join4(dirname4(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
5421
+ join4(dirname4(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
5422
+ ...(process.env.PATH ?? "").split(delimiter).flatMap((dir) => [
5423
+ join4(dir, "node_modules", "npm", "bin", "npm-cli.js"),
5424
+ join4(dir, "npm")
5425
+ ])
5426
+ ];
5427
+ for (const candidate of candidates) {
5428
+ try {
5429
+ const path = realpathSync(candidate);
5430
+ if (path.endsWith(`${process.platform === "win32" ? "\\" : "/"}npm-cli.js`)) return [process.execPath, path];
5431
+ } catch {
5432
+ }
5433
+ }
5434
+ throw new Error("npm is required to install updates.");
5435
+ }
5436
+ function runUpdateCommand(command2, cwd, signal, timeout = 18e4) {
5437
+ return new Promise((resolveResult, reject) => {
5438
+ if (signal?.aborted) {
5439
+ reject(new Error("Update canceled."));
5440
+ return;
5441
+ }
5442
+ const child = spawn2(command2[0], command2.slice(1), {
5443
+ cwd,
5444
+ windowsHide: true,
5445
+ detached: process.platform !== "win32",
5446
+ stdio: ["ignore", "pipe", "ignore"]
5447
+ });
5448
+ let output = "", canceled = false;
5449
+ const stop = () => {
5450
+ canceled = true;
5451
+ if (!child.pid) return;
5452
+ if (process.platform === "win32") {
5453
+ const killer = spawn2(
5454
+ join4(process.env.SystemRoot || "C:\\Windows", "System32", "taskkill.exe"),
5455
+ ["/PID", String(child.pid), "/T", "/F"],
5456
+ { windowsHide: true, stdio: "ignore" }
5457
+ );
5458
+ killer.on("error", () => {
5459
+ child.kill();
5460
+ });
5461
+ } else {
5462
+ try {
5463
+ process.kill(-child.pid, "SIGKILL");
5464
+ } catch {
5465
+ child.kill();
5466
+ }
5467
+ }
5468
+ };
5469
+ const timer = setTimeout(stop, timeout);
5470
+ signal?.addEventListener("abort", stop, { once: true });
5471
+ const cleanup = () => {
5472
+ clearTimeout(timer);
5473
+ signal?.removeEventListener("abort", stop);
5474
+ };
5475
+ child.stdout.on("data", (chunk) => {
5476
+ if (output.length < 4096) output += chunk.toString().slice(0, 4096 - output.length);
5477
+ });
5478
+ child.on("error", () => {
5479
+ cleanup();
5480
+ reject(new Error("Could not start the update command."));
5481
+ });
5482
+ child.on("close", (code) => {
5483
+ cleanup();
5484
+ if (code === 0 && !canceled) resolveResult(output.trim());
5485
+ else reject(new Error(canceled ? "Update canceled or timed out." : "Update installation or verification failed."));
5486
+ });
5487
+ });
5488
+ }
5489
+ async function install(directory, version, signal) {
5490
+ writeFileSync3(join4(directory, "package.json"), '{"private":true}', { mode: 384, flag: "wx" });
5491
+ await runUpdateCommand([
5492
+ ...npmCommand(),
5493
+ "install",
5494
+ "--prefix",
5495
+ directory,
5496
+ "--no-save",
5497
+ "--no-audit",
5498
+ "--no-fund",
5499
+ "--engine-strict",
5500
+ "--progress=false",
5501
+ "--loglevel=error",
5502
+ `--registry=${REGISTRY}`,
5503
+ `zas-agent@${version}`
5504
+ ], directory, signal);
5505
+ }
5506
+ async function verify(entry, version, signal) {
5507
+ if (await runUpdateCommand([process.execPath, entry, "--version"], dirname4(entry), signal, 15e3) !== version) {
5508
+ throw new Error("The staged agent did not report the expected version.");
5509
+ }
5510
+ }
5511
+ async function checkForUpdate(current, force = false, signal, deps = {}) {
5512
+ if (!RELEASE.test(current)) return "Development builds do not update themselves.";
5513
+ if (!force && !updatesEnabled()) return "Automatic updates are off.";
5514
+ const now = (deps.now ?? Date.now)();
5515
+ let state = readState();
5516
+ if (!force && typeof state.checked_at === "number" && now >= state.checked_at && now - state.checked_at < DAY) return "";
5517
+ const home = await prepareHome(signal);
5518
+ const lock = join4(home, "check.lock");
5519
+ if (existsSync4(lock)) {
5520
+ try {
5521
+ const pid = Number(readFileSync3(lock, "utf8"));
5522
+ if (Date.now() - lstatSync2(lock).mtimeMs < 10 * 6e4) {
5523
+ if (!Number.isInteger(pid) || pid <= 0) return "Another update check is in progress.";
5524
+ try {
5525
+ process.kill(pid, 0);
5526
+ return "Another update check is in progress.";
5527
+ } catch (error) {
5528
+ if (error.code !== "ESRCH") return "Another update check is in progress.";
5529
+ }
5530
+ }
5531
+ rmSync3(lock);
5532
+ } catch {
5533
+ return "Another update check is in progress.";
5534
+ }
5535
+ }
5536
+ try {
5537
+ writeFileSync3(lock, String(process.pid), { flag: "wx", mode: 384 });
5538
+ } catch {
5539
+ return "Another update check is in progress.";
5540
+ }
5541
+ try {
5542
+ state = readState();
5543
+ writeState(home, { ...state, checked_at: now });
5544
+ const response = await (deps.fetch ?? fetch)(`${REGISTRY}/zas-agent/latest`, {
5545
+ redirect: "error",
5546
+ signal: AbortSignal.any([AbortSignal.timeout(5e3), ...signal ? [signal] : []])
5547
+ });
5548
+ if (!response.ok) throw new Error("The npm registry is unavailable.");
5549
+ const latest = await response.json();
5550
+ if (latest?.name !== "zas-agent" || typeof latest.version !== "string" || !RELEASE.test(latest.version)) {
5551
+ throw new Error("The npm registry returned an invalid release.");
5552
+ }
5553
+ if (!newerRelease(latest.version, current)) return `Already up to date (${current}).`;
5554
+ if (state.ready && !newerRelease(latest.version, state.ready.version)) {
5555
+ try {
5556
+ const entry2 = releaseEntry(state.ready);
5557
+ await (deps.verify ?? verify)(entry2, state.ready.version, signal);
5558
+ return `${state.ready.version} is ready for the next launch.`;
5559
+ } catch {
5560
+ }
5561
+ }
5562
+ const ready = { version: latest.version, directory: `release-${randomUUID5()}` };
5563
+ const directory = join4(home, ready.directory);
5564
+ mkdirSync4(directory, { mode: 448 });
5565
+ await (deps.install ?? install)(directory, ready.version, signal);
5566
+ const entry = releaseEntry(ready);
5567
+ await (deps.verify ?? verify)(entry, ready.version, signal);
5568
+ if (signal?.aborted) throw new Error("Update canceled.");
5569
+ writeState(home, { checked_at: now, ready });
5570
+ return `${ready.version} is ready. Restart the agent to use it.${updatesEnabled() ? "" : " First enable updates with: zas-agent updates on"}`;
5571
+ } finally {
5572
+ rmSync3(lock, { force: true });
5573
+ }
5574
+ }
5575
+ function startUpdateChecks(current, log) {
5576
+ const controller = new AbortController();
5577
+ let checking = false;
5578
+ const check = async () => {
5579
+ if (checking || controller.signal.aborted) return;
5580
+ checking = true;
5581
+ try {
5582
+ const result = await checkForUpdate(current, false, controller.signal);
5583
+ if (result.includes("ready")) log(`Zas update: ${result}`);
5584
+ } catch {
5585
+ if (!controller.signal.aborted) log("Zas update check failed; continuing with the current version.");
5586
+ } finally {
5587
+ checking = false;
5588
+ }
5589
+ };
5590
+ const first = setTimeout(() => void check(), 5e3).unref();
5591
+ const interval = setInterval(() => void check(), DAY).unref();
5592
+ const terminate = () => {
5593
+ stop();
5594
+ process.kill(process.pid, "SIGTERM");
5595
+ };
5596
+ const interrupt = () => {
5597
+ stop();
5598
+ process.kill(process.pid, "SIGINT");
5599
+ };
5600
+ const stop = () => {
5601
+ clearTimeout(first);
5602
+ clearInterval(interval);
5603
+ controller.abort();
5604
+ process.stdin.removeListener("end", stop);
5605
+ process.removeListener("exit", stop);
5606
+ process.removeListener("SIGTERM", terminate);
5607
+ process.removeListener("SIGINT", interrupt);
5608
+ };
5609
+ process.stdin.once("end", stop);
5610
+ process.once("exit", stop);
5611
+ process.once("SIGTERM", terminate);
5612
+ process.once("SIGINT", interrupt);
5613
+ return stop;
5614
+ }
5615
+ function launchStaged(entry, argv) {
5616
+ return new Promise((done) => {
5617
+ const child = spawn2(process.execPath, [entry, ...argv], { stdio: "inherit", windowsHide: true });
5618
+ const term = () => {
5619
+ child.kill("SIGTERM");
5620
+ };
5621
+ const interrupt = () => {
5622
+ child.kill("SIGINT");
5623
+ };
5624
+ process.once("SIGTERM", term);
5625
+ process.once("SIGINT", interrupt);
5626
+ const cleanup = () => {
5627
+ process.removeListener("SIGTERM", term);
5628
+ process.removeListener("SIGINT", interrupt);
5629
+ };
5630
+ child.once("error", () => {
5631
+ cleanup();
5632
+ done(1);
5633
+ });
5634
+ child.once("exit", (code, signal) => {
5635
+ cleanup();
5636
+ done(code ?? (signal === "SIGINT" ? 130 : 143));
5637
+ });
5638
+ });
5639
+ }
5640
+
4669
5641
  // src/server.ts
4670
5642
  function agentVersion() {
4671
- return true ? "0.10.0" : "0.0.0-dev";
5643
+ return true ? "0.11.0" : "0.0.0-dev";
4672
5644
  }
4673
5645
  var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
4674
5646
  var PAIR_ANNOUNCE_MS = 15e3;
4675
5647
  var NOTE_LABEL_MAX = 40;
4676
5648
  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.");
4677
- var delay = (ms) => new Promise((resolve2) => {
4678
- setTimeout(resolve2, ms);
5649
+ var delay = (ms) => new Promise((resolve4) => {
5650
+ setTimeout(resolve4, ms);
4679
5651
  });
4680
5652
  function rightsOf(grant) {
4681
5653
  if (grant.pending) return "waiting for the channel\u2019s owner";
@@ -4765,6 +5737,8 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
4765
5737
  });
4766
5738
  return text2([
4767
5739
  `Paired as \u201C${c.identity.name}\u201D (${c.identity.kind}) with account ${c.identity.owner_uid}.`,
5740
+ `Secret storage: ${secretStorage(profile)}.`,
5741
+ updateLine(agentVersion()),
4768
5742
  grants.length > 0 ? "Channels:" : "No channels: the owner has not granted access to any yet.",
4769
5743
  ...lines,
4770
5744
  `${packageName()} ${agentVersion()} \xB7 profile ${profile}`,
@@ -4797,8 +5771,8 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
4797
5771
  },
4798
5772
  // The person cannot type into this process; the coding agent relays
4799
5773
  // the code as an argument, and this promise is what it answers.
4800
- askCode: () => new Promise((resolve2) => {
4801
- state.ask = resolve2;
5774
+ askCode: () => new Promise((resolve4) => {
5775
+ state.ask = resolve4;
4802
5776
  }),
4803
5777
  ...process.env.ZAS_NO_OPEN === "1" ? {} : { open: openInBrowser }
4804
5778
  }).then(
@@ -5084,7 +6058,17 @@ function parseArgs(argv) {
5084
6058
  const inline = arg.startsWith("--") && eq > 0 ? arg.slice(eq + 1) : void 0;
5085
6059
  const take = () => inline !== void 0 ? inline : argv[++i] ?? "";
5086
6060
  if (name === "pair") parsed.command = "pair";
5087
- else if (name === "telemetry") {
6061
+ else if (name === "update") parsed.command = "update";
6062
+ else if (name === "updates") {
6063
+ parsed.command = "updates";
6064
+ const next = argv[i + 1];
6065
+ if (next === "on" || next === "off") {
6066
+ parsed.updates = next;
6067
+ i++;
6068
+ } else if (next !== void 0 && !next.startsWith("--")) {
6069
+ return { ...parsed, command: "invalid", message: `Unknown update setting "${next}". Use "on" or "off".` };
6070
+ }
6071
+ } else if (name === "telemetry") {
5088
6072
  parsed.command = "telemetry";
5089
6073
  const next = argv[i + 1];
5090
6074
  if (next === "on" || next === "off") {
@@ -5117,6 +6101,8 @@ var USAGE = [
5117
6101
  " zas-agent pair [--profile <name>] pair this agent with a Zas account",
5118
6102
  " [--kind claude_code|codex|other] [--host <name>] [--no-open]",
5119
6103
  " zas-agent telemetry [on|off] say what this machine reports, or change it",
6104
+ " zas-agent updates [on|off] show or change automatic updates",
6105
+ " zas-agent update stage the latest release for the next launch",
5120
6106
  " zas-agent --version"
5121
6107
  ].join("\n");
5122
6108
  function noticeOnServe(log) {
@@ -5150,6 +6136,38 @@ async function main(argv, log = (l) => console.error(l)) {
5150
6136
  log(args.telemetry === "off" ? `Nothing is sent. Turn it on again with: npx -y ${packageName()} telemetry on` : TELEMETRY_NOTICE);
5151
6137
  return 0;
5152
6138
  }
6139
+ if (args.command === "updates") {
6140
+ if (args.updates) setUpdates(args.updates === "on");
6141
+ log(updateLine(agentVersion()));
6142
+ log("Updates take effect on the next launch. Active transfers keep running.");
6143
+ return 0;
6144
+ }
6145
+ const released = isReleaseInstall(agentVersion(), import.meta.url);
6146
+ if (args.command === "update") {
6147
+ if (!released) {
6148
+ log("Development checkouts do not update themselves.");
6149
+ return 0;
6150
+ }
6151
+ try {
6152
+ log(await checkForUpdate(agentVersion(), true));
6153
+ return 0;
6154
+ } catch {
6155
+ log("Update failed. The current agent is unchanged; check your network and npm installation, then retry.");
6156
+ return 1;
6157
+ }
6158
+ }
6159
+ if (released) {
6160
+ const ready = await verifiedStagedRelease(agentVersion());
6161
+ if (ready) return launchStaged(ready.entry, argv);
6162
+ }
6163
+ try {
6164
+ defaultEndpoints();
6165
+ loadIdentity(args.profile);
6166
+ loadPending(args.profile);
6167
+ } catch (error) {
6168
+ log(humanSentence(error instanceof ZasError ? error : new ZasError("internal", 0)));
6169
+ return 1;
6170
+ }
5153
6171
  if (args.command === "pair") {
5154
6172
  log(TELEMETRY_NOTICE);
5155
6173
  log("");
@@ -5157,8 +6175,8 @@ async function main(argv, log = (l) => console.error(l)) {
5157
6175
  let rl = null;
5158
6176
  const askCode = process.stdin.isTTY ? () => {
5159
6177
  rl ??= createInterface({ input: process.stdin, output: process.stderr });
5160
- return new Promise((resolve2) => {
5161
- rl.question("Type the code shown in the browser: ", resolve2);
6178
+ return new Promise((resolve4) => {
6179
+ rl.question("Type the code shown in the browser: ", resolve4);
5162
6180
  });
5163
6181
  } : void 0;
5164
6182
  const noOpen = args.noOpen === true || process.env.ZAS_NO_OPEN === "1";
@@ -5183,12 +6201,21 @@ async function main(argv, log = (l) => console.error(l)) {
5183
6201
  return 0;
5184
6202
  }
5185
6203
  noticeOnServe(log);
5186
- await buildServer(args.profile).connect(new StdioServerTransport());
6204
+ const transport = new StdioServerTransport();
6205
+ await buildServer(args.profile).connect(transport);
6206
+ if (released) {
6207
+ const stop = startUpdateChecks(agentVersion(), log);
6208
+ const onclose = transport.onclose;
6209
+ transport.onclose = () => {
6210
+ stop();
6211
+ onclose?.();
6212
+ };
6213
+ }
5187
6214
  return 0;
5188
6215
  }
5189
6216
  var real = (path) => {
5190
6217
  try {
5191
- return realpathSync(path);
6218
+ return realpathSync2(path);
5192
6219
  } catch {
5193
6220
  return "";
5194
6221
  }
@@ -5199,7 +6226,7 @@ function isInvokedDirectly(argv1, selfUrl) {
5199
6226
  if (entry === "") return false;
5200
6227
  let self = "";
5201
6228
  try {
5202
- self = real(fileURLToPath(selfUrl));
6229
+ self = real(fileURLToPath2(selfUrl));
5203
6230
  } catch {
5204
6231
  return false;
5205
6232
  }