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.
- package/CHANGELOG.md +27 -1
- package/README.md +83 -8
- package/dist/cli.js +1354 -327
- 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 ||
|
|
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
|
|
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 =
|
|
247
|
-
const tmp = `${target}.${
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
if (
|
|
258
|
-
|
|
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 =
|
|
429
|
+
const target = join2(dir, name);
|
|
263
430
|
if (!existsSync(target)) return null;
|
|
264
431
|
try {
|
|
265
|
-
return JSON.parse(
|
|
432
|
+
return JSON.parse(readFileSync2(target, "utf8"));
|
|
266
433
|
} catch {
|
|
267
434
|
return null;
|
|
268
435
|
}
|
|
269
436
|
}
|
|
270
|
-
function
|
|
271
|
-
const target = resolve(
|
|
272
|
-
const text2 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
517
|
+
writeSecret(profileDir(profile), PENDING_FILE, pending2);
|
|
302
518
|
}
|
|
303
519
|
function clearPending(profile) {
|
|
304
|
-
|
|
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
|
-
|
|
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 [
|
|
342
|
-
const child = spawn(
|
|
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((
|
|
357
|
-
let
|
|
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
|
-
|
|
362
|
-
if (
|
|
583
|
+
size2 += chunk.length;
|
|
584
|
+
if (size2 > BODY_MAX) {
|
|
363
585
|
if (!resolved) {
|
|
364
586
|
resolved = true;
|
|
365
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
681
|
+
close: () => new Promise((resolve4) => {
|
|
460
682
|
server.closeAllConnections();
|
|
461
|
-
server.close(() =>
|
|
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
|
|
593
|
-
const
|
|
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 =
|
|
627
|
-
return host ?
|
|
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
|
|
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
|
|
1097
|
+
const command2 = `${runner} -y ${pkg}@latest`;
|
|
640
1098
|
const shell = windows ? "cmd /d /c " : "";
|
|
641
1099
|
return {
|
|
642
|
-
pair: `${
|
|
643
|
-
claude: `${shell}claude mcp add zas "--" ${
|
|
644
|
-
codex: `${shell}codex mcp add zas "--" ${
|
|
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((
|
|
1133
|
+
var defaultSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
676
1134
|
function deferred() {
|
|
677
|
-
let
|
|
1135
|
+
let resolve4;
|
|
678
1136
|
const promise = new Promise((r) => {
|
|
679
|
-
|
|
1137
|
+
resolve4 = r;
|
|
680
1138
|
});
|
|
681
|
-
return { promise, resolve:
|
|
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
|
|
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
|
-
...
|
|
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(
|
|
931
|
-
if (!Number.isSafeInteger(
|
|
932
|
-
return Math.max(1, Math.ceil(
|
|
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(
|
|
935
|
-
return
|
|
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((
|
|
1903
|
+
const waitLow = () => new Promise((resolve4) => {
|
|
1481
1904
|
const done = () => {
|
|
1482
1905
|
dc.removeEventListener("bufferedamountlow", done);
|
|
1483
|
-
|
|
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((
|
|
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
|
|
1687
|
-
if (protocol >= 2 && (!frame.digest || frame.digest.size !== got || frame.digest.sha256 !==
|
|
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: "
|
|
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:
|
|
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 === "
|
|
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((
|
|
2350
|
-
const timer = setTimeout(
|
|
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((
|
|
2355
|
-
const timer = setTimeout(
|
|
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 =
|
|
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(
|
|
2414
|
-
this.size = Math.max(0,
|
|
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((
|
|
2674
|
-
const timer = setTimeout(
|
|
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
|
|
2695
|
-
if (
|
|
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,
|
|
3353
|
+
const workers = Array.from({ length: Math.min(CONCURRENCY, signed2.length) }, async () => {
|
|
2698
3354
|
for (; ; ) {
|
|
2699
3355
|
const at = cursor++;
|
|
2700
|
-
if (at >=
|
|
2701
|
-
const entry =
|
|
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
|
|
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
|
-
|
|
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
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2821
|
-
|
|
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,
|
|
3680
|
+
function batches(items, size2) {
|
|
3082
3681
|
const out = [];
|
|
3083
|
-
for (let i = 0; i < items.length; i +=
|
|
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((
|
|
3181
|
-
setTimeout(
|
|
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,
|
|
3236
|
-
const samples =
|
|
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:
|
|
3240
|
-
mac: bytesToHex(hmacSha256(b64ToBytes(
|
|
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
|
|
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(
|
|
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((
|
|
3555
|
-
setTimeout(
|
|
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((
|
|
3650
|
-
resolveOutcome =
|
|
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
|
-
|
|
3841
|
-
openSync,
|
|
3842
|
-
renameSync as renameSync2,
|
|
4446
|
+
openSync as openSync2,
|
|
3843
4447
|
rmdirSync,
|
|
3844
|
-
|
|
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,
|
|
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(
|
|
4701
|
+
mkdirSync2(dirname2(target), { recursive: true, mode: 448 });
|
|
4065
4702
|
tmp = `${target}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
4066
|
-
fd =
|
|
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
|
|
4074
|
-
if (
|
|
4075
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4724
|
+
closeSync2(fd);
|
|
4088
4725
|
} catch {
|
|
4089
4726
|
}
|
|
4090
4727
|
if (tmp !== void 0 && existsSync2(tmp)) try {
|
|
4091
|
-
|
|
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
|
|
4875
|
+
closeSync as closeSync3,
|
|
4239
4876
|
existsSync as existsSync3,
|
|
4240
4877
|
mkdirSync as mkdirSync3,
|
|
4241
|
-
openSync as
|
|
4242
|
-
renameSync as renameSync3,
|
|
4878
|
+
openSync as openSync3,
|
|
4243
4879
|
rmdirSync as rmdirSync2,
|
|
4244
|
-
unlinkSync as
|
|
4880
|
+
unlinkSync as unlinkSync3,
|
|
4245
4881
|
writeSync as writeSync2
|
|
4246
4882
|
} from "node:fs";
|
|
4247
|
-
import { dirname as
|
|
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(
|
|
4924
|
+
mkdirSync3(dirname3(this.target), { recursive: true, mode: 448 });
|
|
4289
4925
|
this.tmp = `${this.target}.${randomBytes3(6).toString("hex")}.tmp`;
|
|
4290
|
-
this.fd =
|
|
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
|
-
|
|
4942
|
+
closeSync3(this.fd);
|
|
4307
4943
|
this.fd = void 0;
|
|
4308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
4420
|
-
resolveOutcome =
|
|
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
|
|
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:
|
|
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((
|
|
4653
|
-
timer = setTimeout(() =>
|
|
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.
|
|
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((
|
|
4678
|
-
setTimeout(
|
|
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((
|
|
4801
|
-
state.ask =
|
|
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 === "
|
|
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((
|
|
5161
|
-
rl.question("Type the code shown in the browser: ",
|
|
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
|
-
|
|
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
|
|
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(
|
|
6229
|
+
self = real(fileURLToPath2(selfUrl));
|
|
5203
6230
|
} catch {
|
|
5204
6231
|
return false;
|
|
5205
6232
|
}
|