zas-agent 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/README.md +44 -7
  3. package/dist/cli.js +1620 -1003
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -123,7 +123,16 @@ var RENAMES = {
123
123
  // agent (expired, or cancelled from the receiving side); `direct_too_big`
124
124
  // is the plan's Directo ceiling.
125
125
  not_live: "direct_cancelled",
126
- direct_too_big: "file_too_big"
126
+ direct_too_big: "file_too_big",
127
+ // The item-change routes. `not_allowed` there means the item is not this
128
+ // agent's; a share of any kind, armed or not, is the one reason a replace
129
+ // is refused that the owner fixes from the item itself.
130
+ not_allowed: "not_yours",
131
+ shared: "item_shared",
132
+ bar_armed: "item_shared",
133
+ // An organization whose plan stopped pauses every send in its channels.
134
+ // Retrying cannot help; only the owner or a billing admin can.
135
+ enterprise_plan_inactive: "plan_inactive"
127
136
  };
128
137
  var ZasError = class extends Error {
129
138
  constructor(code, status, message, retryAfterMs, serverCode) {
@@ -178,9 +187,15 @@ var SENTENCES = {
178
187
  fallback_unavailable: "Reliable delivery is not available right now. Try again later.",
179
188
  key_stale: "The channel key changed. The owner refreshes it by opening Zas.",
180
189
  quota_exceeded: "The account reached its storage limit.",
190
+ plan_inactive: "The organization's plan is inactive, so sending to its channels is paused. The owner or a billing admin reactivates it in Zas.",
181
191
  rate_limited: "Too many sends in a row. Wait a moment.",
182
192
  file_too_big: "The file is over the plan limit.",
183
193
  duplicate: "That item is already in the channel.",
194
+ not_yours: "That item was not sent by this agent. It can only change its own items.",
195
+ stale: "That item changed while this agent was working on it. Read it again and retry.",
196
+ not_a_note: "That item is a file, not a note. Only its title can change; use zas_replace_file for the bytes.",
197
+ not_a_file: "That item is a note, not a file. Use zas_edit_item.",
198
+ item_shared: "That item has a public share. The owner removes the share first.",
184
199
  pairing_expired: "The pairing expired. Run \u201Czas-agent pair\u201D again.",
185
200
  pairing_cancelled: "The owner cancelled the pairing.",
186
201
  claim_mismatch: "The code does not match. Try again.",
@@ -282,8 +297,8 @@ function loadIdentity(profile) {
282
297
  function saveIdentity(profile, identity) {
283
298
  writePrivate(profileDir(profile), IDENTITY_FILE, identity);
284
299
  }
285
- function savePending(profile, pending) {
286
- writePrivate(profileDir(profile), PENDING_FILE, pending);
300
+ function savePending(profile, pending2) {
301
+ writePrivate(profileDir(profile), PENDING_FILE, pending2);
287
302
  }
288
303
  function clearPending(profile) {
289
304
  rmSync(join(profileDir(profile), PENDING_FILE), { force: true });
@@ -533,6 +548,24 @@ var ZasClient = class _ZasClient {
533
548
  }
534
549
  await this.pendingSignIn;
535
550
  }
551
+ telemetrySending = false;
552
+ /** Optional traffic must never initiate or invalidate the shared auth session.
553
+ * One bounded request, no retries, and no response body parsing. */
554
+ async reportTelemetry(body) {
555
+ const session2 = this.session;
556
+ if (this.telemetrySending || !session2 || session2.expiresAt <= this.now() + REFRESH_MARGIN_MS) return;
557
+ this.telemetrySending = true;
558
+ try {
559
+ const res = await this.call(`${this.identity.api_base}/v1/agents/telemetry`, {
560
+ ...jsonInit("POST", body, { Authorization: `Bearer ${session2.idToken}` }),
561
+ signal: AbortSignal.timeout(2e3)
562
+ });
563
+ await res.body?.cancel().catch(() => void 0);
564
+ } catch {
565
+ } finally {
566
+ this.telemetrySending = false;
567
+ }
568
+ }
536
569
  async api(method, path, body, headers = {}) {
537
570
  const res = await this.authedRetrying(`${this.identity.api_base}/v1${path}`, method, body, headers);
538
571
  const parsed = res.status === 204 ? void 0 : await readBody2(res);
@@ -717,7 +750,7 @@ async function runPair(opts) {
717
750
  try {
718
751
  const port = listener?.port;
719
752
  const link = pairUrl(opts.webBase, created.pairing_id, port);
720
- const pending = {
753
+ const pending2 = {
721
754
  version: 1,
722
755
  profile: opts.profile,
723
756
  pairing_id: created.pairing_id,
@@ -732,7 +765,7 @@ async function runPair(opts) {
732
765
  protocol: 2,
733
766
  ...port ? { port } : {}
734
767
  };
735
- savePending(opts.profile, pending);
768
+ savePending(opts.profile, pending2);
736
769
  const minutesLeft = Math.max(0, Math.round((created.expires_at - now()) / 6e4));
737
770
  opts.log(
738
771
  [
@@ -1706,116 +1739,857 @@ function startReceiver(opts) {
1706
1739
  };
1707
1740
  }
1708
1741
 
1709
- // src/direct.ts
1710
- import { randomBytes } from "node:crypto";
1711
- import { openAsBlob } from "node:fs";
1712
- import { basename as basename2 } from "node:path";
1713
-
1714
- // src/shared/manifest.ts
1715
- import { xchacha20poly1305 } from "@noble/ciphers/chacha";
1716
- function newManifest(partial) {
1717
- return { v: MANIFEST_VERSION, chunker: CHUNKER_VERSION, ...partial };
1718
- }
1719
- var ENVELOPE_MAGIC = 90;
1720
- var KEY_VERSION_LEGACY = 1;
1721
- function envelopeVersion(sealed) {
1722
- if (sealed.length > 26 && sealed[0] === ENVELOPE_MAGIC && sealed[1] >= 1) return sealed[1];
1723
- return KEY_VERSION_LEGACY;
1724
- }
1725
- function sealBytes(key, version, plain) {
1726
- if (version < 1 || version > 255) throw new Error("key version out of range");
1727
- const nonce = new Uint8Array(24);
1728
- crypto.getRandomValues(nonce);
1729
- const ct = xchacha20poly1305(key, nonce).encrypt(plain);
1730
- return concatBytes(new Uint8Array([ENVELOPE_MAGIC, version]), nonce, ct);
1731
- }
1732
- function openSealed(key, sealed) {
1733
- const versioned = envelopeVersion(sealed) !== KEY_VERSION_LEGACY || sealed[0] === ENVELOPE_MAGIC;
1734
- if (versioned && sealed.length > 26) {
1735
- try {
1736
- return xchacha20poly1305(key, sealed.slice(2, 26)).decrypt(sealed.slice(26));
1737
- } catch {
1738
- }
1739
- }
1740
- return xchacha20poly1305(key, sealed.slice(0, 24)).decrypt(sealed.slice(24));
1741
- }
1742
- var sealRaw = sealBytes;
1743
- var openRaw = openSealed;
1744
- function sealManifest(channelKey, manifest, version = KEY_VERSION_LEGACY) {
1745
- return sealBytes(channelKey, version, new TextEncoder().encode(JSON.stringify(manifest)));
1746
- }
1747
- function openManifest(channelKey, sealed) {
1748
- const m = JSON.parse(new TextDecoder().decode(openSealed(channelKey, sealed)));
1749
- if (!Array.isArray(m.chunks)) m.chunks = [];
1750
- return m;
1751
- }
1752
- function decryptChannelName(channelKey, sealed) {
1753
- return new TextDecoder().decode(openSealed(channelKey, sealed));
1754
- }
1742
+ // src/transfer-telemetry.ts
1743
+ import { randomUUID } from "node:crypto";
1755
1744
 
1756
- // src/file-source.ts
1757
- import { promises as fsp, statSync } from "node:fs";
1758
- async function readFileRange(path, start, length) {
1759
- const fh = await fsp.open(path, "r");
1760
- try {
1761
- const buf = Buffer.allocUnsafe(Math.max(0, length));
1762
- let read = 0;
1763
- while (read < length) {
1764
- const { bytesRead } = await fh.read(buf, read, length - read, start + read);
1765
- if (bytesRead === 0) break;
1766
- read += bytesRead;
1767
- }
1768
- const out = new Uint8Array(read);
1769
- out.set(buf.subarray(0, read));
1770
- return out.buffer;
1771
- } finally {
1772
- await fh.close();
1773
- }
1745
+ // src/shared/product-analytics.ts
1746
+ var PRODUCT_EVENTS = [
1747
+ "account_created",
1748
+ "item.created",
1749
+ "item.downloaded",
1750
+ "item.copied",
1751
+ "item.shared",
1752
+ "item.pinned",
1753
+ "item.unpinned",
1754
+ "item.deleted",
1755
+ "item.departed",
1756
+ "channel_shared",
1757
+ "channel_mode_set",
1758
+ "invite_created",
1759
+ "invite_link_copied",
1760
+ "invite_qr_shown",
1761
+ "invite_system_sheet_opened",
1762
+ "invite_joined",
1763
+ "invite_converted",
1764
+ "channel.invite_preview",
1765
+ "channel.invite_accept",
1766
+ "channel.invite_signin",
1767
+ "enterprise.invite_preview",
1768
+ "enterprise.invite_identity",
1769
+ "enterprise.invite_signin",
1770
+ "enterprise.invite_account_switch",
1771
+ "enterprise.invite_attach",
1772
+ "enterprise.invite_accept",
1773
+ "enterprise.invite_admin",
1774
+ "enterprise.signin",
1775
+ "enterprise.alpha_waitlist",
1776
+ "enterprise.alpha_access",
1777
+ "enterprise.org_create",
1778
+ "enterprise.workspace_opened",
1779
+ "enterprise.workspace_switched",
1780
+ "enterprise.channel_created",
1781
+ "enterprise.channel_members_added",
1782
+ "share_link_copied",
1783
+ "share_qr_shown",
1784
+ "share_system_sheet_opened",
1785
+ "share_opened",
1786
+ "share_content_copied",
1787
+ "share_downloaded",
1788
+ "share_signin_click",
1789
+ "share.saved",
1790
+ "share_bar_on",
1791
+ "share_bar_off",
1792
+ "upload.phase_timing",
1793
+ "transfer.started",
1794
+ "transfer.stage_started",
1795
+ "transfer.phase_timing",
1796
+ "transfer.slow",
1797
+ "transfer.finished",
1798
+ "auth.signin_started",
1799
+ "auth.signin_result",
1800
+ "auth.session_started",
1801
+ "auth.signed_out",
1802
+ "auth.session_lost",
1803
+ "auth.token_refresh_failed",
1804
+ "account_switch.started",
1805
+ "account_switch.completed",
1806
+ "account_switch.failed",
1807
+ "bootstrap.result",
1808
+ "direct_mode_set",
1809
+ "direct.offered",
1810
+ "direct.claimed",
1811
+ "direct.connected",
1812
+ "direct.completed",
1813
+ "direct.received",
1814
+ "direct.failed",
1815
+ "direct.fallback_started",
1816
+ "direct.fallback_ready",
1817
+ "direct.fallback_received",
1818
+ "direct.fallback_failed",
1819
+ "direct.fallback_aborted",
1820
+ "direct.too_big",
1821
+ "agent.pair_opened",
1822
+ "agent.pair_approved",
1823
+ "agent.pair_refused",
1824
+ "agent.paired",
1825
+ "agent.revoked",
1826
+ "agent.send",
1827
+ "agent.edit",
1828
+ "agent.replace",
1829
+ "agent.tool_call",
1830
+ // The public pages, which until now reported nothing at all: `capture_pageview`
1831
+ // 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`.
1833
+ "marketing.page_viewed",
1834
+ "marketing.section_viewed",
1835
+ "marketing.scroll_depth",
1836
+ "marketing.cta_clicked",
1837
+ "marketing.language_switched"
1838
+ ];
1839
+ var SERVER_OWNED_PRODUCT_EVENTS = [
1840
+ "account_created",
1841
+ "item.departed",
1842
+ "invite_created",
1843
+ "invite_joined",
1844
+ "invite_converted",
1845
+ "share_opened",
1846
+ "share_content_copied",
1847
+ "share_downloaded",
1848
+ "share_signin_click",
1849
+ "agent.send",
1850
+ "agent.edit",
1851
+ "agent.replace",
1852
+ "agent.tool_call"
1853
+ ];
1854
+ var SERVER_OWNED_PRODUCT_EVENT_SET = new Set(SERVER_OWNED_PRODUCT_EVENTS);
1855
+ var CLIENT_PRODUCT_EVENTS = PRODUCT_EVENTS.filter(
1856
+ (event) => !SERVER_OWNED_PRODUCT_EVENT_SET.has(event)
1857
+ );
1858
+ var bool = { type: "boolean" };
1859
+ var count = { type: "number", min: 0 };
1860
+ var oneOf = (...values) => ({ type: "string", values: new Set(values) });
1861
+ var sizeBucket = oneOf("lt_1mb", "1_10mb", "10_100mb", "100_512mb", "512mb_5gb", "gt_5gb");
1862
+ var itemKind = oneOf("file", "text");
1863
+ var itemKindOrUnknown = oneOf("file", "text", "unknown");
1864
+ var role = oneOf("sender", "receiver");
1865
+ var candidateType = oneOf("", "host", "srflx", "relay", "prflx", "other");
1866
+ var enterpriseRole = oneOf("owner", "admin", "billing_admin", "member", "guest");
1867
+ var agentKind = oneOf("claude_code", "codex", "other");
1868
+ var pairHandoff = oneOf("loopback", "manual", "unknown");
1869
+ var marketingSurface = oneOf("home", "plans", "enterprise", "app");
1870
+ var marketingSection = oneOf(
1871
+ "enterprise_band",
1872
+ "hero",
1873
+ "what",
1874
+ "benefits",
1875
+ "agents",
1876
+ "fit",
1877
+ "billing",
1878
+ "signin"
1879
+ );
1880
+ var marketingCta = oneOf(
1881
+ "see_enterprise",
1882
+ "see_plans",
1883
+ "contact",
1884
+ "start_org",
1885
+ "sign_in",
1886
+ "open_console",
1887
+ "switch_language",
1888
+ "footer_home"
1889
+ );
1890
+ var marketingPlacement = oneOf("nav", "hero", "band", "fit", "billing", "signin", "footer", "menu", "limit");
1891
+ var arrivalKind = oneOf("direct", "internal", "search", "social", "other");
1892
+ var uiLocale = oneOf("es-AR", "pt-BR", "en");
1893
+ var signinContext = oneOf("page", "alpha_access", "handoff_site", "handoff_share");
1894
+ var AGENT_TOOL_NAMES = [
1895
+ "status",
1896
+ "pair",
1897
+ "send_file",
1898
+ "send_note",
1899
+ "send_direct",
1900
+ "send_direct_fallback",
1901
+ "receive_direct",
1902
+ "receive_direct_fallback",
1903
+ "list_items",
1904
+ "get_item",
1905
+ "jobs",
1906
+ "edit_item",
1907
+ "replace_file"
1908
+ ];
1909
+ var AGENT_ERROR_CODES = [
1910
+ "none",
1911
+ "other",
1912
+ "identity_corrupt",
1913
+ "not_paired",
1914
+ "internal",
1915
+ "agent_forbidden",
1916
+ "agent_revoked",
1917
+ "grant_missing",
1918
+ "not_found",
1919
+ "invalid_cap",
1920
+ "write_failed",
1921
+ "send_forbidden",
1922
+ "read_forbidden",
1923
+ "direct_mode",
1924
+ "not_direct_mode",
1925
+ "not_claimed",
1926
+ "no_offer",
1927
+ "offer_taken",
1928
+ "direct_cancelled",
1929
+ "direct_failed",
1930
+ "direct_not_failed",
1931
+ "file_changed",
1932
+ "webrtc_unavailable",
1933
+ "fallback_unavailable",
1934
+ "key_stale",
1935
+ "quota_exceeded",
1936
+ "rate_limited",
1937
+ "file_too_big",
1938
+ "duplicate",
1939
+ "pairing_expired",
1940
+ "pairing_cancelled",
1941
+ "claim_mismatch",
1942
+ "pairing_not_approved",
1943
+ "pairing_claimed",
1944
+ "agent_limit",
1945
+ "grant_limit",
1946
+ "feature_disabled",
1947
+ "upload_failed",
1948
+ "oprf_failed",
1949
+ "network",
1950
+ "sign_in_failed",
1951
+ "bad_signature",
1952
+ "missing_token",
1953
+ // An agent changing its own items (2026-09-09).
1954
+ "not_yours",
1955
+ "stale",
1956
+ "not_a_note",
1957
+ "not_a_file",
1958
+ "item_shared"
1959
+ ];
1960
+ function agentDurationBucket(ms) {
1961
+ if (ms < 1e3) return "lt_1s";
1962
+ if (ms < 1e4) return "1_10s";
1963
+ if (ms < 6e4) return "10_60s";
1964
+ return "gt_60s";
1774
1965
  }
1775
- function directFileFrom(path, raw) {
1776
- let statSize;
1777
- try {
1778
- statSize = statSync(path).size;
1779
- } catch {
1780
- return raw;
1781
- }
1782
- if (statSize <= raw.size) return raw;
1783
- const type = raw.type;
1784
- const file = {
1785
- size: statSize,
1786
- type,
1787
- slice(start, end) {
1788
- const from = Math.max(0, Math.min(start, statSize));
1789
- const to = Math.max(from, Math.min(end, statSize));
1790
- return { arrayBuffer: () => readFileRange(path, from, to - from) };
1791
- }
1792
- };
1793
- return file;
1966
+ var transferProperties = {
1967
+ transfer_id: { type: "transfer_id" },
1968
+ item_kind: oneOf("file", "text", "code", "link", "unknown"),
1969
+ transport: oneOf("storage", "manifest", "direct", "fallback"),
1970
+ cache_hit: bool,
1971
+ telemetry_version: count,
1972
+ direction: oneOf("upload", "download"),
1973
+ purpose: oneOf("send", "download", "preview", "export"),
1974
+ scope: oneOf("personal", "organization"),
1975
+ size_bucket: sizeBucket,
1976
+ stage: oneOf("staging", "encrypting", "uploading", "finishing", "authorization", "downloading", "decrypting", "assembling"),
1977
+ duration_ms: count,
1978
+ bytes_completed: count,
1979
+ size_bytes: count,
1980
+ foreground: bool,
1981
+ online: bool,
1982
+ outcome: oneOf("success", "failure", "cancelled"),
1983
+ error_code: oneOf("none", "network", "timeout", "authorization", "storage", "crypto", "worker", "other"),
1984
+ timing_mode: oneOf("wall", "cumulative_work")
1985
+ };
1986
+ var EVENT_PROPERTY_RULES = {
1987
+ "transfer.started": transferProperties,
1988
+ "transfer.stage_started": transferProperties,
1989
+ "transfer.phase_timing": transferProperties,
1990
+ "transfer.slow": transferProperties,
1991
+ "transfer.finished": transferProperties,
1992
+ account_created: {},
1993
+ "item.created": { kind: itemKind, size_bytes: count, size_bucket: sizeBucket },
1994
+ "item.downloaded": { size_bytes: count, size_bucket: sizeBucket },
1995
+ "item.copied": {},
1996
+ "item.shared": { kind: itemKind },
1997
+ "item.pinned": { age_ms: count },
1998
+ "item.unpinned": {},
1999
+ "item.deleted": { age_ms: count, was_pinned: bool, kind: itemKindOrUnknown },
2000
+ "item.departed": {},
2001
+ channel_shared: {},
2002
+ channel_mode_set: { mode: oneOf("edit", "view") },
2003
+ invite_created: {},
2004
+ invite_link_copied: {},
2005
+ invite_qr_shown: {},
2006
+ invite_system_sheet_opened: {},
2007
+ invite_joined: {},
2008
+ invite_converted: {},
2009
+ "channel.invite_preview": {
2010
+ outcome: oneOf("ready", "unavailable", "missing_key"),
2011
+ signed_in: bool
2012
+ },
2013
+ "channel.invite_accept": {
2014
+ outcome: oneOf("started", "completed", "already_member", "unavailable", "failed")
2015
+ },
2016
+ "channel.invite_signin": {
2017
+ stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
2018
+ },
2019
+ "enterprise.invite_preview": {
2020
+ outcome: oneOf("pending", "resumable", "unavailable"),
2021
+ role: enterpriseRole
2022
+ },
2023
+ "enterprise.invite_identity": {
2024
+ outcome: oneOf("matched", "mismatched", "unavailable", "failed")
2025
+ },
2026
+ "enterprise.invite_signin": {
2027
+ stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
2028
+ },
2029
+ "enterprise.invite_account_switch": { stage: oneOf("started", "completed", "failed") },
2030
+ "enterprise.invite_attach": {
2031
+ stage: oneOf("link_requested", "link_sent", "verify_started", "completed", "address_taken", "link_invalid", "failed")
2032
+ },
2033
+ "enterprise.invite_accept": {
2034
+ outcome: oneOf("started", "completed", "recovered", "timeout", "unavailable", "email_mismatch", "seat_limit", "failed"),
2035
+ role: enterpriseRole,
2036
+ destination: oneOf("workspace", "admin_console"),
2037
+ duration_ms: count
2038
+ },
2039
+ "enterprise.invite_admin": {
2040
+ outcome: oneOf("created", "email_sent", "email_failed", "reissued", "revoked"),
2041
+ role: enterpriseRole,
2042
+ external: bool
2043
+ },
2044
+ // The workspace sign-in card, wherever it is shown: the public enterprise
2045
+ // page, a `?site=` handoff from a customer domain, or a `?share=` link.
2046
+ "enterprise.signin": {
2047
+ context: signinContext,
2048
+ stage: oneOf(
2049
+ "shown",
2050
+ "email_submitted",
2051
+ "email_sent",
2052
+ "email_failed",
2053
+ "provider_clicked",
2054
+ "link_completed",
2055
+ "link_failed"
2056
+ ),
2057
+ provider: oneOf("google.com", "apple.com", "email_link", "none")
2058
+ },
2059
+ // Closed-alpha funnel. It intentionally names only a step: never an email,
2060
+ // password, password cohort, access token, or raw HTTP error.
2061
+ "enterprise.alpha_waitlist": { stage: oneOf("submitted", "joined", "failed") },
2062
+ "enterprise.alpha_access": { stage: oneOf("revealed", "submitted", "granted", "denied") },
2063
+ // The end of the funnel. `opened` is the create form appearing, which is
2064
+ // what someone signing in with no organization yet is shown.
2065
+ "enterprise.org_create": { stage: oneOf("opened", "submitted", "created", "failed", "access_required") },
2066
+ "enterprise.workspace_opened": { role: enterpriseRole, first_visit: bool },
2067
+ "enterprise.workspace_switched": { destination: oneOf("personal", "organization"), role: enterpriseRole },
2068
+ "enterprise.channel_created": { member_count: count },
2069
+ "enterprise.channel_members_added": { member_count: count },
2070
+ share_link_copied: {},
2071
+ share_qr_shown: {},
2072
+ share_system_sheet_opened: {},
2073
+ share_opened: {},
2074
+ share_content_copied: {},
2075
+ share_downloaded: {},
2076
+ share_signin_click: {},
2077
+ "share.saved": {},
2078
+ share_bar_on: {},
2079
+ share_bar_off: {},
2080
+ "upload.phase_timing": {
2081
+ stage: oneOf("staging", "bootstrap", "thumbnail", "encrypting", "uploading", "finishing"),
2082
+ duration_ms: count,
2083
+ size_bucket: sizeBucket,
2084
+ outcome: oneOf("success", "failure", "cancelled"),
2085
+ attempt: { type: "number", min: 1 }
2086
+ },
2087
+ "auth.signin_started": { provider: oneOf("google.com", "apple.com") },
2088
+ "auth.signin_result": {
2089
+ provider: oneOf("google.com", "apple.com"),
2090
+ outcome: oneOf("signed_in", "cancelled", "taken", "failed", "needs_link"),
2091
+ flow: oneOf("signin", "link", "connect")
2092
+ },
2093
+ "account_switch.started": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
2094
+ "account_switch.completed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
2095
+ "account_switch.failed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
2096
+ "auth.session_started": { cold: bool },
2097
+ "auth.signed_out": { reason: oneOf("user", "account_switch", "abandoned_new_account", "account_deleted") },
2098
+ "auth.session_lost": { session_age_ms: count, foreground: bool },
2099
+ "auth.token_refresh_failed": {
2100
+ code: oneOf("network", "token_invalid", "user_disabled", "user_missing", "rate_limited", "internal", "unknown")
2101
+ },
2102
+ "bootstrap.result": {
2103
+ outcome: oneOf("offline", "ok", "timeout_warm", "timeout", "failed_warm", "failed"),
2104
+ stage: oneOf("cached", "done", "unknown", "keyring", "account_init", "listeners"),
2105
+ duration_ms: count,
2106
+ fault: oneOf("intercepted", "offline", "unknown")
2107
+ },
2108
+ direct_mode_set: { on: bool },
2109
+ "direct.offered": { size_bucket: sizeBucket },
2110
+ // `via_agent`: the offer came from a paired agent's process, not from one
2111
+ // of the person's devices. The claim and the two receiver outcomes carry it.
2112
+ "direct.claimed": { size_bucket: sizeBucket, via_agent: bool },
2113
+ "direct.connected": { role, ms: count },
2114
+ "direct.completed": {
2115
+ via: oneOf("lan", "wan", "relay", "unknown"),
2116
+ pair_local: candidateType,
2117
+ pair_remote: candidateType,
2118
+ connect_ms: count,
2119
+ duration_ms: count,
2120
+ size_bucket: sizeBucket,
2121
+ turn_urls_supplied: count,
2122
+ turn_urls_configured: count,
2123
+ restarts: count
2124
+ },
2125
+ "direct.received": {
2126
+ via_agent: bool,
2127
+ via: oneOf("lan", "wan", "relay", "unknown"),
2128
+ pair_local: candidateType,
2129
+ pair_remote: candidateType,
2130
+ connect_ms: count,
2131
+ duration_ms: count,
2132
+ size_bucket: sizeBucket,
2133
+ turn_urls_supplied: count,
2134
+ turn_urls_configured: count,
2135
+ restarts: count
2136
+ },
2137
+ "direct.failed": {
2138
+ role,
2139
+ via_agent: bool,
2140
+ reason: oneOf(
2141
+ "peer_silent",
2142
+ "connect_timeout",
2143
+ "ice_failed",
2144
+ "disconnected",
2145
+ "peer_closed",
2146
+ "peer_abort",
2147
+ "signaling",
2148
+ "signal_send",
2149
+ "protocol",
2150
+ "sink",
2151
+ "transfer",
2152
+ "integrity",
2153
+ "too_big",
2154
+ "unknown"
2155
+ ),
2156
+ ms: count,
2157
+ ice_state: oneOf("", "new", "checking", "connected", "completed", "failed", "disconnected", "closed"),
2158
+ gather_state: oneOf("", "new", "gathering", "complete"),
2159
+ had_remote: bool,
2160
+ local_host: count,
2161
+ local_srflx: count,
2162
+ local_relay: count,
2163
+ remote_host: count,
2164
+ remote_srflx: count,
2165
+ remote_relay: count,
2166
+ turn_urls_supplied: count,
2167
+ turn_urls_configured: count,
2168
+ moved_bucket: sizeBucket,
2169
+ size_bucket: sizeBucket,
2170
+ restarts: count,
2171
+ // Whether the reporting tab was on screen when the run died — the lead a
2172
+ // slept machine or a frozen background tab leaves behind.
2173
+ visibility: oneOf("visible", "hidden", "prerender")
2174
+ },
2175
+ "direct.fallback_started": { size_bucket: sizeBucket },
2176
+ "direct.fallback_ready": { size_bucket: sizeBucket, duration_ms: count, retries: count },
2177
+ "direct.fallback_received": { size_bucket: sizeBucket, duration_ms: count, retries: count },
2178
+ "direct.fallback_failed": {
2179
+ stage: oneOf("upload", "download"),
2180
+ size_bucket: sizeBucket,
2181
+ retries: count
2182
+ },
2183
+ "direct.fallback_aborted": {},
2184
+ "direct.too_big": { size_bucket: sizeBucket },
2185
+ "agent.pair_opened": { agent_kind: agentKind, protocol: count, replaces: bool },
2186
+ "agent.pair_approved": {
2187
+ agent_kind: agentKind,
2188
+ destination: oneOf("new_channel", "existing"),
2189
+ channels: count,
2190
+ handoff: pairHandoff
2191
+ },
2192
+ "agent.pair_refused": {
2193
+ agent_kind: agentKind,
2194
+ step: oneOf("open", "approve", "claim"),
2195
+ reason: oneOf(
2196
+ "agent_limit",
2197
+ "grant_limit",
2198
+ "channel_limit",
2199
+ "code_mismatch",
2200
+ "pairing_expired",
2201
+ "pairing_cancelled",
2202
+ "pairing_claimed",
2203
+ "feature_disabled",
2204
+ "rate_limited",
2205
+ "network",
2206
+ "other"
2207
+ )
2208
+ },
2209
+ "agent.paired": {
2210
+ agent_kind: agentKind,
2211
+ destination: oneOf("new_channel", "existing"),
2212
+ channels: count,
2213
+ handoff: pairHandoff,
2214
+ protocol: count
2215
+ },
2216
+ "agent.revoked": { agent_kind: agentKind },
2217
+ "agent.send": { size_bucket: sizeBucket, agent_kind: agentKind },
2218
+ "agent.edit": { agent_kind: agentKind },
2219
+ "agent.replace": { size_bucket: sizeBucket, agent_kind: agentKind },
2220
+ // What the CLI reports after every tool call: the tool, whether it worked,
2221
+ // the code when it did not, and the version that answered. No path, no file
2222
+ // name, no channel, no sentence.
2223
+ "marketing.page_viewed": {
2224
+ surface: marketingSurface,
2225
+ locale: uiLocale,
2226
+ // Whether the URL carried a language prefix. `/en/enterprise` and
2227
+ // `/enterprise` are the same page in different languages, and which one
2228
+ // search sends people to is worth knowing on its own.
2229
+ prefixed: bool,
2230
+ signed_in: bool,
2231
+ arrival: arrivalKind
2232
+ },
2233
+ "marketing.section_viewed": { surface: marketingSurface, section: marketingSection },
2234
+ "marketing.scroll_depth": {
2235
+ surface: marketingSurface,
2236
+ depth: oneOf("p25", "p50", "p75", "p100")
2237
+ },
2238
+ "marketing.cta_clicked": {
2239
+ surface: marketingSurface,
2240
+ cta: marketingCta,
2241
+ placement: marketingPlacement
2242
+ },
2243
+ "marketing.language_switched": { surface: marketingSurface, from: uiLocale, to: uiLocale },
2244
+ "agent.tool_call": {
2245
+ tool: oneOf(...AGENT_TOOL_NAMES),
2246
+ result: oneOf("ok", "error"),
2247
+ code: oneOf(...AGENT_ERROR_CODES),
2248
+ agent_kind: agentKind,
2249
+ duration_bucket: oneOf("lt_1s", "1_10s", "10_60s", "gt_60s"),
2250
+ version_major: count,
2251
+ version_minor: count,
2252
+ version_patch: count
2253
+ }
2254
+ };
2255
+ var platformRule = oneOf("web", "android", "ios", "agent", "unknown");
2256
+ function valid(rule, value) {
2257
+ if (rule.type === "transfer_id") return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
2258
+ if (rule.type === "boolean") return typeof value === "boolean";
2259
+ if (rule.type === "number") {
2260
+ return typeof value === "number" && Number.isFinite(value) && (rule.min === void 0 || value >= rule.min);
2261
+ }
2262
+ return typeof value === "string" && rule.values.has(value);
2263
+ }
2264
+ function sanitizeProductEventProperties(event, properties) {
2265
+ if (!properties || typeof properties !== "object" || Array.isArray(properties)) return {};
2266
+ const input = properties;
2267
+ const rules = EVENT_PROPERTY_RULES[event];
2268
+ const output = {};
2269
+ for (const [key, value] of Object.entries(input)) {
2270
+ const rule = key === "platform" ? platformRule : rules[key];
2271
+ if (rule && valid(rule, value)) output[key] = value;
2272
+ }
2273
+ return output;
1794
2274
  }
2275
+ var PERSON_PROPERTY_RULES = {
2276
+ plan: oneOf("anon", "free", "pro", "max"),
2277
+ locale: oneOf("es-AR", "pt-BR", "en"),
2278
+ storage_bytes: count
2279
+ };
1795
2280
 
1796
- // src/shared/direct-fallback.ts
1797
- function createFallbackMeta(file) {
1798
- const key = crypto.getRandomValues(new Uint8Array(32));
1799
- const nonce = crypto.getRandomValues(new Uint8Array(8));
1800
- return {
1801
- v: 1,
1802
- name: file.name,
1803
- size: file.size,
1804
- mime: file.type,
1805
- key_b64: bytesToB64(key),
1806
- nonce_b64: bytesToB64(nonce),
1807
- part_bytes: DIRECT_FALLBACK_PART_BYTES,
1808
- tag_bytes: DIRECT_FALLBACK_TAG_BYTES,
1809
- part_count: directFallbackPartCount(file.size),
1810
- cipher_size: directFallbackCipherSize(file.size)
1811
- };
1812
- }
1813
- function fallbackMetaOf(value) {
1814
- const meta = value;
1815
- if (!meta || meta.v !== 1 || typeof meta.name !== "string" || meta.name.length === 0 || typeof meta.mime !== "string" || !Number.isSafeInteger(meta.size) || (meta.size ?? -1) < 0 || typeof meta.key_b64 !== "string" || typeof meta.nonce_b64 !== "string" || meta.part_bytes !== DIRECT_FALLBACK_PART_BYTES || meta.tag_bytes !== DIRECT_FALLBACK_TAG_BYTES || meta.part_count !== directFallbackPartCount(meta.size) || meta.cipher_size !== directFallbackCipherSize(meta.size)) throw new Error("bad_fallback_meta");
1816
- let key;
1817
- let nonce;
1818
- try {
2281
+ // src/telemetry.ts
2282
+ var OFF_WORDS = /* @__PURE__ */ new Set(["0", "off", "false", "no"]);
2283
+ var ON_WORDS = /* @__PURE__ */ new Set(["1", "on", "true", "yes"]);
2284
+ function telemetryState() {
2285
+ const env = (process.env.ZAS_AGENT_TELEMETRY ?? "").toLowerCase();
2286
+ if (OFF_WORDS.has(env)) return { on: false, source: "env" };
2287
+ if (ON_WORDS.has(env)) return { on: true, source: "env" };
2288
+ if (process.env.DO_NOT_TRACK === "1") return { on: false, source: "do_not_track" };
2289
+ const stored = loadSettings();
2290
+ if (stored && typeof stored.telemetry === "boolean") return { on: stored.telemetry, source: "file" };
2291
+ return { on: true, source: "default" };
2292
+ }
2293
+ function setTelemetry(on) {
2294
+ const stored = loadSettings();
2295
+ saveSettings({ ...stored ?? {}, version: 1, telemetry: on, decided_at: Date.now() });
2296
+ }
2297
+ function noticeShown() {
2298
+ return typeof loadSettings()?.notified_at === "number";
2299
+ }
2300
+ function markNoticeShown() {
2301
+ const stored = loadSettings();
2302
+ saveSettings({ ...stored ?? {}, version: 1, notified_at: Date.now() });
2303
+ }
2304
+ var TELEMETRY_NOTICE = [
2305
+ "Zas collects usage data from this agent to improve the product: which tool",
2306
+ "ran, whether it worked, the error code when it did not, how long it took,",
2307
+ "item type, transfer stages, byte counts, outcomes, and the version.",
2308
+ "Never file names, file contents, paths or channel names.",
2309
+ "The data is linked to your Zas account.",
2310
+ "Turn it off with: npx -y zas-agent telemetry off"
2311
+ ].join("\n");
2312
+ var SOURCE_WORDS = {
2313
+ env: "ZAS_AGENT_TELEMETRY",
2314
+ do_not_track: "DO_NOT_TRACK",
2315
+ file: "zas-agent telemetry",
2316
+ default: "default"
2317
+ };
2318
+ function telemetryLine() {
2319
+ const state = telemetryState();
2320
+ return `telemetry: ${state.on ? "on" : "off"} (${SOURCE_WORDS[state.source]})`;
2321
+ }
2322
+ var TOOL_NAMES = new Set(AGENT_TOOL_NAMES);
2323
+ function versionParts(version) {
2324
+ const [major = 0, minor = 0, patch = 0] = version.split("-")[0].split(".").map((part) => {
2325
+ const value = Number.parseInt(part, 10);
2326
+ return Number.isFinite(value) && value >= 0 ? value : 0;
2327
+ });
2328
+ return { major, minor, patch };
2329
+ }
2330
+ function toolCallProperties(input) {
2331
+ const tool = input.tool.startsWith("zas_") ? input.tool.slice(4) : input.tool;
2332
+ if (!TOOL_NAMES.has(tool)) return null;
2333
+ const version = versionParts(input.version);
2334
+ return {
2335
+ tool,
2336
+ result: input.code === null ? "ok" : "error",
2337
+ // `other` is the server's word for a code outside the vocabulary. The
2338
+ // shared list is the one that decides, and it drops anything else.
2339
+ code: input.code ?? "none",
2340
+ duration_bucket: agentDurationBucket(input.ms),
2341
+ version_major: version.major,
2342
+ version_minor: version.minor,
2343
+ version_patch: version.patch
2344
+ };
2345
+ }
2346
+ var REPORT_TIMEOUT_MS = 5e3;
2347
+ async function report(client, properties) {
2348
+ if (properties === null) return;
2349
+ await new Promise((resolve2) => {
2350
+ const timer = setTimeout(resolve2, 0);
2351
+ timer.unref?.();
2352
+ });
2353
+ if (!telemetryState().on) return;
2354
+ const timeout = new Promise((resolve2) => {
2355
+ const timer = setTimeout(resolve2, REPORT_TIMEOUT_MS);
2356
+ timer.unref?.();
2357
+ });
2358
+ const body = { event: "agent.tool_call", properties };
2359
+ const sent = (client.reportTelemetry ? client.reportTelemetry(body) : client.api("POST", "/agents/telemetry", body)).then(() => void 0).catch(() => void 0);
2360
+ await Promise.race([sent, timeout]);
2361
+ }
2362
+
2363
+ // src/transfer-telemetry.ts
2364
+ var pending = /* @__PURE__ */ new WeakMap();
2365
+ function flush(client) {
2366
+ const queue = pending.get(client);
2367
+ if (!queue) return;
2368
+ clearTimeout(queue.timer);
2369
+ pending.delete(client);
2370
+ try {
2371
+ if (!telemetryState().on) return;
2372
+ const body = { events: queue.events };
2373
+ void (client.reportTelemetry ? client.reportTelemetry(body) : client.api("POST", "/agents/telemetry", body)).catch(() => void 0);
2374
+ } catch {
2375
+ }
2376
+ }
2377
+ function enqueue(client, report2) {
2378
+ let queue = pending.get(client);
2379
+ if (!queue) {
2380
+ const timer = setTimeout(() => flush(client), 0);
2381
+ timer.unref?.();
2382
+ queue = { events: [], timer };
2383
+ pending.set(client, queue);
2384
+ }
2385
+ if (queue.events.length < 32) queue.events.push(report2);
2386
+ }
2387
+ function itemKind2(item) {
2388
+ if (item.kind === "file") return "file";
2389
+ if (item.code || item.lang) return "code";
2390
+ if (item.link_preview || /^https?:\/\/\S+$/.test(item.text?.trim() ?? "")) return "link";
2391
+ return item.kind === "text" || item.text !== void 0 ? "text" : "unknown";
2392
+ }
2393
+ var TransferTelemetry = class {
2394
+ constructor(client, direction, kind, transport) {
2395
+ this.client = client;
2396
+ this.direction = direction;
2397
+ this.kind = kind;
2398
+ this.transport = transport;
2399
+ this.capture("transfer.started");
2400
+ this.slow = setTimeout(() => this.capture("transfer.slow"), 3e4);
2401
+ this.slow.unref?.();
2402
+ }
2403
+ id = randomUUID();
2404
+ started = performance.now();
2405
+ changed = this.started;
2406
+ stage = "staging";
2407
+ seen = /* @__PURE__ */ new Set();
2408
+ spans = /* @__PURE__ */ new Map();
2409
+ ended = false;
2410
+ slow;
2411
+ size = 0;
2412
+ completed = 0;
2413
+ describe(size, kind = this.kind, transport = this.transport) {
2414
+ this.size = Math.max(0, size);
2415
+ this.kind = kind;
2416
+ this.transport = transport;
2417
+ }
2418
+ progress(bytes) {
2419
+ this.completed = Math.max(this.completed, bytes);
2420
+ }
2421
+ mark(phase) {
2422
+ if (this.ended) return;
2423
+ const stage = {
2424
+ hashing: "staging",
2425
+ waiting: "staging",
2426
+ connecting: "authorization",
2427
+ flight: this.direction === "upload" ? "uploading" : "downloading",
2428
+ done: "finishing"
2429
+ }[phase] ?? phase;
2430
+ if (!["staging", "encrypting", "uploading", "finishing", "authorization", "downloading", "decrypting", "assembling"].includes(stage)) return;
2431
+ const now = performance.now();
2432
+ this.spans.set(this.stage, (this.spans.get(this.stage) ?? 0) + now - this.changed);
2433
+ this.changed = now;
2434
+ this.stage = stage;
2435
+ if (!this.seen.has(stage)) {
2436
+ this.seen.add(stage);
2437
+ this.capture("transfer.stage_started");
2438
+ }
2439
+ }
2440
+ finish(outcome, error = "none", cacheHit = false) {
2441
+ if (this.ended) return;
2442
+ this.mark(this.stage);
2443
+ this.ended = true;
2444
+ if (outcome === "success") this.completed = this.size;
2445
+ clearTimeout(this.slow);
2446
+ for (const [stage, duration] of this.spans) this.capture("transfer.phase_timing", {
2447
+ stage,
2448
+ duration_ms: Math.round(duration),
2449
+ timing_mode: "wall",
2450
+ outcome
2451
+ });
2452
+ this.capture("transfer.finished", { outcome, error_code: error, cache_hit: cacheHit });
2453
+ }
2454
+ failed(error) {
2455
+ const e = error;
2456
+ const cancelled = e?.name === "AbortError" || e?.code === "cancelled";
2457
+ const code = cancelled ? "none" : this.stage === "decrypting" ? "crypto" : e?.status === 401 || e?.status === 403 ? "authorization" : e?.code === "timeout" ? "timeout" : e?.code === "network" || error instanceof TypeError ? "network" : e?.code === "write_failed" ? "storage" : this.stage === "decrypting" ? "crypto" : "other";
2458
+ this.finish(cancelled ? "cancelled" : "failure", code);
2459
+ }
2460
+ capture(event, extra = {}) {
2461
+ try {
2462
+ const mb = this.size / 1048576;
2463
+ const properties = sanitizeProductEventProperties(event, {
2464
+ transfer_id: this.id,
2465
+ telemetry_version: 1,
2466
+ direction: this.direction,
2467
+ purpose: this.direction === "upload" ? "send" : "download",
2468
+ item_kind: this.kind,
2469
+ transport: this.transport,
2470
+ size_bytes: this.size,
2471
+ bytes_completed: this.completed,
2472
+ size_bucket: mb < 1 ? "lt_1mb" : mb < 10 ? "1_10mb" : mb < 100 ? "10_100mb" : mb < 512 ? "100_512mb" : mb < 5120 ? "512mb_5gb" : "gt_5gb",
2473
+ stage: this.stage,
2474
+ duration_ms: Math.round(performance.now() - this.started),
2475
+ ...extra
2476
+ });
2477
+ enqueue(this.client, { event, properties });
2478
+ } catch {
2479
+ }
2480
+ }
2481
+ };
2482
+
2483
+ // src/direct.ts
2484
+ import { randomBytes } from "node:crypto";
2485
+ import { openAsBlob } from "node:fs";
2486
+ import { basename as basename2 } from "node:path";
2487
+
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
+ // src/file-source.ts
2531
+ import { promises as fsp, statSync } from "node:fs";
2532
+ async function readFileRange(path, start, length) {
2533
+ const fh = await fsp.open(path, "r");
2534
+ try {
2535
+ const buf = Buffer.allocUnsafe(Math.max(0, length));
2536
+ let read = 0;
2537
+ while (read < length) {
2538
+ const { bytesRead } = await fh.read(buf, read, length - read, start + read);
2539
+ if (bytesRead === 0) break;
2540
+ read += bytesRead;
2541
+ }
2542
+ const out = new Uint8Array(read);
2543
+ out.set(buf.subarray(0, read));
2544
+ return out.buffer;
2545
+ } finally {
2546
+ await fh.close();
2547
+ }
2548
+ }
2549
+ function directFileFrom(path, raw) {
2550
+ let statSize;
2551
+ try {
2552
+ statSize = statSync(path).size;
2553
+ } catch {
2554
+ return raw;
2555
+ }
2556
+ if (statSize <= raw.size) return raw;
2557
+ const type = raw.type;
2558
+ const file = {
2559
+ size: statSize,
2560
+ type,
2561
+ slice(start, end) {
2562
+ const from = Math.max(0, Math.min(start, statSize));
2563
+ const to = Math.max(from, Math.min(end, statSize));
2564
+ return { arrayBuffer: () => readFileRange(path, from, to - from) };
2565
+ }
2566
+ };
2567
+ return file;
2568
+ }
2569
+
2570
+ // src/shared/direct-fallback.ts
2571
+ function createFallbackMeta(file) {
2572
+ const key = crypto.getRandomValues(new Uint8Array(32));
2573
+ const nonce = crypto.getRandomValues(new Uint8Array(8));
2574
+ return {
2575
+ v: 1,
2576
+ name: file.name,
2577
+ size: file.size,
2578
+ mime: file.type,
2579
+ key_b64: bytesToB64(key),
2580
+ nonce_b64: bytesToB64(nonce),
2581
+ part_bytes: DIRECT_FALLBACK_PART_BYTES,
2582
+ tag_bytes: DIRECT_FALLBACK_TAG_BYTES,
2583
+ part_count: directFallbackPartCount(file.size),
2584
+ cipher_size: directFallbackCipherSize(file.size)
2585
+ };
2586
+ }
2587
+ function fallbackMetaOf(value) {
2588
+ const meta = value;
2589
+ if (!meta || meta.v !== 1 || typeof meta.name !== "string" || meta.name.length === 0 || typeof meta.mime !== "string" || !Number.isSafeInteger(meta.size) || (meta.size ?? -1) < 0 || typeof meta.key_b64 !== "string" || typeof meta.nonce_b64 !== "string" || meta.part_bytes !== DIRECT_FALLBACK_PART_BYTES || meta.tag_bytes !== DIRECT_FALLBACK_TAG_BYTES || meta.part_count !== directFallbackPartCount(meta.size) || meta.cipher_size !== directFallbackCipherSize(meta.size)) throw new Error("bad_fallback_meta");
2590
+ let key;
2591
+ let nonce;
2592
+ try {
1819
2593
  key = b64ToBytes(meta.key_b64);
1820
2594
  nonce = b64ToBytes(meta.nonce_b64);
1821
2595
  } catch {
@@ -2151,17 +2925,17 @@ function cutPoint(buf, gear, eof) {
2151
2925
  }
2152
2926
  async function* chunkStream(source) {
2153
2927
  const gear = await gearTable();
2154
- let pending = [];
2928
+ let pending2 = [];
2155
2929
  let pendingLen = 0;
2156
2930
  const compact = () => {
2157
- if (pending.length === 1) return pending[0];
2931
+ if (pending2.length === 1) return pending2[0];
2158
2932
  const merged = new Uint8Array(pendingLen);
2159
2933
  let off = 0;
2160
- for (const p of pending) {
2934
+ for (const p of pending2) {
2161
2935
  merged.set(p, off);
2162
2936
  off += p.length;
2163
2937
  }
2164
- pending = [merged];
2938
+ pending2 = [merged];
2165
2939
  return merged;
2166
2940
  };
2167
2941
  const drain = function* (eof) {
@@ -2170,13 +2944,13 @@ async function* chunkStream(source) {
2170
2944
  const cut = cutPoint(buf, gear, eof);
2171
2945
  if (cut === null) return;
2172
2946
  yield buf.slice(0, cut);
2173
- pending = cut < buf.length ? [buf.slice(cut)] : [];
2947
+ pending2 = cut < buf.length ? [buf.slice(cut)] : [];
2174
2948
  pendingLen = buf.length - cut;
2175
2949
  }
2176
2950
  };
2177
2951
  for await (const piece of source) {
2178
2952
  if (piece.length === 0) continue;
2179
- pending.push(piece);
2953
+ pending2.push(piece);
2180
2954
  pendingLen += piece.length;
2181
2955
  if (pendingLen >= CHUNK_MAX) yield* drain(false);
2182
2956
  }
@@ -2566,32 +3340,20 @@ async function postLink(ctx, grant, manifest, placed, idempotencyKey, expiresInD
2566
3340
  }
2567
3341
  return created.link_id;
2568
3342
  }
2569
- async function sendFile(ctx, input, onPhase) {
2570
- const stat = await fsp2.stat(input.path).catch(() => {
3343
+ async function checkFilePath(path) {
3344
+ const stat = await fsp2.stat(path).catch(() => {
2571
3345
  throw new ZasError("upload_failed", 400);
2572
3346
  });
2573
3347
  if (!stat.isFile()) throw new ZasError("upload_failed", 400);
2574
3348
  if (stat.size > MAX_FILE_BYTES) throw new ZasError("file_too_big", 413);
2575
- const grant = await grantFor(ctx, input.channel);
2576
- const name = basename(input.path);
2577
- const title = input.title ?? name;
2578
- const file = await fsp2.readFile(input.path);
3349
+ }
3350
+ async function readFileChecked(path) {
3351
+ await checkFilePath(path);
3352
+ const file = await fsp2.readFile(path);
2579
3353
  if (file.byteLength > MAX_FILE_BYTES) throw new ZasError("file_too_big", 413);
2580
- const bytes = new Uint8Array(file.buffer, file.byteOffset, file.byteLength);
2581
- const contentHash = await sendIdentity(await blake3Hex(bytes), input.expires_in_days);
2582
- const key = await receiptKey(ctx, grant.channel_id, contentHash, title);
2583
- const stored = receiptFor(ctx, key);
2584
- if (stored) {
2585
- return {
2586
- link_id: stored.link_id,
2587
- channel_id: grant.channel_id,
2588
- channel_name: channelNameOf(ctx.identity, grant),
2589
- bytes: stored.bytes,
2590
- chunks: stored.chunks,
2591
- deduplicated: stored.deduplicated,
2592
- replayed: true
2593
- };
2594
- }
3354
+ return { bytes: new Uint8Array(file.buffer, file.byteOffset, file.byteLength), name: basename(path) };
3355
+ }
3356
+ async function placeFile(ctx, bytes, onPhase) {
2595
3357
  onPhase?.("hashing");
2596
3358
  const plains = [];
2597
3359
  for await (const piece of chunkStream([bytes])) plains.push(piece);
@@ -2608,7 +3370,38 @@ async function sendFile(ctx, input, onPhase) {
2608
3370
  (plain, i) => encryptChunk(oprfFinalize(hashes[i], blinds[i].blind, b64ToBytes(evaluated[i])), plain)
2609
3371
  ));
2610
3372
  onPhase?.("uploading");
2611
- const placed = await placeChunks(ctx, encs);
3373
+ return placeChunks(ctx, encs);
3374
+ }
3375
+ function forgetLink(profile, linkId) {
3376
+ const entries = {};
3377
+ let dropped = false;
3378
+ for (const [k, v] of Object.entries(loadFingerprints(profile).entries)) {
3379
+ if (v && v.link_id === linkId) dropped = true;
3380
+ else entries[k] = v;
3381
+ }
3382
+ if (dropped) saveFingerprints(profile, { entries });
3383
+ }
3384
+ async function sendFileMeasured(ctx, input, onPhase) {
3385
+ await checkFilePath(input.path);
3386
+ const grant = await grantFor(ctx, input.channel);
3387
+ const { bytes, name } = await readFileChecked(input.path);
3388
+ ctx.transferTelemetry?.describe(bytes.length);
3389
+ const title = input.title ?? name;
3390
+ const contentHash = await sendIdentity(await blake3Hex(bytes), input.expires_in_days);
3391
+ const key = await receiptKey(ctx, grant.channel_id, contentHash, title);
3392
+ const stored = receiptFor(ctx, key);
3393
+ if (stored) {
3394
+ return {
3395
+ link_id: stored.link_id,
3396
+ channel_id: grant.channel_id,
3397
+ channel_name: channelNameOf(ctx.identity, grant),
3398
+ bytes: stored.bytes,
3399
+ chunks: stored.chunks,
3400
+ deduplicated: stored.deduplicated,
3401
+ replayed: true
3402
+ };
3403
+ }
3404
+ const placed = await placeFile(ctx, bytes, onPhase);
2612
3405
  onPhase?.("finishing");
2613
3406
  const mime = mimeFor(input.path);
2614
3407
  const thumb = await thumbnailFor(bytes, mime);
@@ -2624,6 +3417,7 @@ async function sendFile(ctx, input, onPhase) {
2624
3417
  ...thumb ? { thumb_data: thumb } : {},
2625
3418
  chunks: placed.map((p) => p.entry)
2626
3419
  });
3420
+ ctx.transferTelemetry?.mark("finishing");
2627
3421
  const linkId = await postLink(
2628
3422
  ctx,
2629
3423
  grant,
@@ -2653,10 +3447,12 @@ async function sendFile(ctx, input, onPhase) {
2653
3447
  function noteName(text2) {
2654
3448
  return text2.split("\n", 1)[0].trim().slice(0, NOTE_NAME_MAX) || "nota";
2655
3449
  }
2656
- async function sendNote(ctx, input) {
3450
+ async function sendNoteMeasured(ctx, input) {
2657
3451
  const grant = await grantFor(ctx, input.channel);
2658
3452
  const name = input.title ?? noteName(input.text);
2659
3453
  const encoded = new TextEncoder().encode(input.text);
3454
+ ctx.transferTelemetry?.describe(encoded.length);
3455
+ ctx.transferTelemetry?.mark("encrypting");
2660
3456
  const contentHash = await sendIdentity(
2661
3457
  await blake3Hex(new TextEncoder().encode(
2662
3458
  `${input.lang ?? ""}\0${input.secret ? "1" : "0"}\0${input.text}`
@@ -2690,6 +3486,7 @@ async function sendNote(ctx, input) {
2690
3486
  ...input.secret ? { sensitive: true } : {},
2691
3487
  chunks: []
2692
3488
  });
3489
+ ctx.transferTelemetry?.mark("finishing");
2693
3490
  const linkId = await postLink(
2694
3491
  ctx,
2695
3492
  grant,
@@ -2715,6 +3512,34 @@ async function sendNote(ctx, input) {
2715
3512
  replayed: false
2716
3513
  };
2717
3514
  }
3515
+ async function sendFile(...args) {
3516
+ const telemetry = new TransferTelemetry(args[0].client, "upload", "file", "storage");
3517
+ try {
3518
+ const report2 = args[2];
3519
+ const result = await sendFileMeasured({ ...args[0], transferTelemetry: telemetry }, args[1], (phase) => {
3520
+ telemetry.mark(phase);
3521
+ report2?.(phase);
3522
+ });
3523
+ telemetry.describe(result.bytes);
3524
+ telemetry.finish("success", "none", result.replayed);
3525
+ return result;
3526
+ } catch (error) {
3527
+ telemetry.failed(error);
3528
+ throw error;
3529
+ }
3530
+ }
3531
+ async function sendNote(...args) {
3532
+ const telemetry = new TransferTelemetry(args[0].client, "upload", itemKind2({ text: args[1].text, lang: args[1].lang }), "manifest");
3533
+ try {
3534
+ const result = await sendNoteMeasured({ ...args[0], transferTelemetry: telemetry }, ...args.slice(1));
3535
+ telemetry.describe(result.bytes);
3536
+ telemetry.finish("success", "none", result.replayed);
3537
+ return result;
3538
+ } catch (error) {
3539
+ telemetry.failed(error);
3540
+ throw error;
3541
+ }
3542
+ }
2718
3543
 
2719
3544
  // src/direct.ts
2720
3545
  var OFFER_WAIT_MS = 9.5 * 60 * 1e3;
@@ -2765,16 +3590,18 @@ function sealer(key, keyVersion) {
2765
3590
  open: (enc) => JSON.parse(new TextDecoder().decode(openRaw(key, b64ToBytes(enc))))
2766
3591
  };
2767
3592
  }
2768
- async function sendDirect(ctx, input, report2, deps = {}) {
3593
+ async function sendDirectMeasured(ctx, input, report2, deps = {}) {
2769
3594
  const now = deps.now ?? (() => Date.now());
2770
3595
  const sleep2 = deps.sleep ?? defaultSleep2;
2771
3596
  const grant = await directGrantFor(ctx, input.channel);
2772
3597
  const key = channelKeyOf(ctx.identity, grant);
2773
3598
  const channelName = channelLabel(ctx, grant);
3599
+ ctx.transferTelemetry?.mark("authorization");
2774
3600
  const raw = await (deps.openFile ?? openAsBlob)(input.path, { type: mimeFor(input.path) }).catch(() => {
2775
3601
  throw new ZasError("upload_failed", 400);
2776
3602
  });
2777
3603
  const file = directFileFrom(input.path, raw);
3604
+ ctx.transferTelemetry?.describe(file.size);
2778
3605
  if (file.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
2779
3606
  const name = basename2(input.path);
2780
3607
  const cid = grant.channel_id;
@@ -2914,13 +3741,14 @@ var fetchPut = async (url, bytes, signal, onLoaded) => {
2914
3741
  onLoaded(bytes.length);
2915
3742
  return etag;
2916
3743
  };
2917
- async function sendDirectFallback(ctx, record, report2, deps = {}) {
3744
+ async function sendDirectFallbackMeasured(ctx, record, report2, deps = {}) {
2918
3745
  const now = deps.now ?? (() => Date.now());
2919
3746
  const startedAt = now();
2920
3747
  const opened = await (deps.openFile ?? openAsBlob)(record.path).catch(() => {
2921
3748
  throw new ZasError("upload_failed", 400);
2922
3749
  });
2923
3750
  if (opened.size !== record.size) throw new ZasError("file_changed", 0);
3751
+ ctx.transferTelemetry?.describe(record.size);
2924
3752
  const file = new File([opened], record.name, { type: mimeFor(record.path) });
2925
3753
  const { seal } = sealer(record.key, record.key_version);
2926
3754
  const stamp = { device: record.device, owner_uid: record.owner_uid };
@@ -2970,20 +3798,38 @@ async function sendDirectFallback(ctx, record, report2, deps = {}) {
2970
3798
  parts: parts.length
2971
3799
  };
2972
3800
  }
2973
-
2974
- // src/receive.ts
2975
- import { randomBytes as randomBytes3 } from "node:crypto";
2976
- import {
2977
- closeSync as closeSync2,
2978
- existsSync as existsSync3,
2979
- mkdirSync as mkdirSync3,
2980
- openSync as openSync2,
2981
- renameSync as renameSync3,
2982
- rmdirSync as rmdirSync2,
2983
- unlinkSync as unlinkSync2,
2984
- writeSync as writeSync2
2985
- } from "node:fs";
2986
- import { dirname as dirname2 } from "node:path";
3801
+ async function sendDirect(...args) {
3802
+ const telemetry = new TransferTelemetry(args[0].client, "upload", "file", "direct");
3803
+ try {
3804
+ const report2 = args[2];
3805
+ const result = await sendDirectMeasured({ ...args[0], transferTelemetry: telemetry }, args[1], (phase) => {
3806
+ telemetry.mark(phase);
3807
+ report2?.(phase);
3808
+ }, args[3]);
3809
+ telemetry.describe(result.bytes);
3810
+ telemetry.finish("success", "none", false);
3811
+ return result;
3812
+ } catch (error) {
3813
+ telemetry.failed(error);
3814
+ throw error;
3815
+ }
3816
+ }
3817
+ async function sendDirectFallback(...args) {
3818
+ const telemetry = new TransferTelemetry(args[0].client, "upload", "file", "fallback");
3819
+ try {
3820
+ const report2 = args[2];
3821
+ const result = await sendDirectFallbackMeasured({ ...args[0], transferTelemetry: telemetry }, args[1], (phase) => {
3822
+ telemetry.mark(phase);
3823
+ report2?.(phase);
3824
+ }, args[3]);
3825
+ telemetry.describe(result.bytes);
3826
+ telemetry.finish("success", "none", false);
3827
+ return result;
3828
+ } catch (error) {
3829
+ telemetry.failed(error);
3830
+ throw error;
3831
+ }
3832
+ }
2987
3833
 
2988
3834
  // src/read.ts
2989
3835
  import { randomBytes as randomBytes2 } from "node:crypto";
@@ -3024,7 +3870,8 @@ function rowOf(doc) {
3024
3870
  agent: stringOf(fields.agent),
3025
3871
  createdAt: timeOf(fields.created_at),
3026
3872
  expiresAt: timeOf(fields.expires_at),
3027
- bar: fields.bar?.booleanValue === true
3873
+ bar: fields.bar?.booleanValue === true,
3874
+ updateTime: timeOf({ timestampValue: document.updateTime })
3028
3875
  };
3029
3876
  }
3030
3877
  function readable(row) {
@@ -3114,6 +3961,7 @@ function redeemFailure(status) {
3114
3961
  return new ZasError("invalid_cap", 403);
3115
3962
  }
3116
3963
  async function fetchChunk(ctx, chunk) {
3964
+ ctx.transferTelemetry?.mark("authorization");
3117
3965
  if (typeof chunk.cap !== "string" || chunk.cap === "") throw new ZasError("invalid_cap", 403);
3118
3966
  let headers = {};
3119
3967
  try {
@@ -3136,6 +3984,7 @@ async function fetchChunk(ctx, chunk) {
3136
3984
  throw err;
3137
3985
  }
3138
3986
  if (url === "") throw new ZasError("invalid_cap", 403);
3987
+ ctx.transferTelemetry?.mark("downloading");
3139
3988
  const res = await fetch(url);
3140
3989
  if (!res.ok) {
3141
3990
  await res.body?.cancel().catch(() => void 0);
@@ -3143,6 +3992,7 @@ async function fetchChunk(ctx, chunk) {
3143
3992
  }
3144
3993
  const ciphertext = new Uint8Array(await res.arrayBuffer());
3145
3994
  try {
3995
+ ctx.transferTelemetry?.mark("decrypting");
3146
3996
  return decryptChunk(b64ToBytes(chunk.key), b64ToBytes(chunk.nonce), ciphertext);
3147
3997
  } catch {
3148
3998
  throw new ZasError("invalid_cap", 403);
@@ -3171,7 +4021,7 @@ function destinationOf(dest, name, fallback) {
3171
4021
  const at = statSync2(dest, { throwIfNoEntry: false })?.isDirectory() ? join2(dest, base) : dest;
3172
4022
  return { target: freeName(at) };
3173
4023
  }
3174
- async function getItem(ctx, channel, id, dest) {
4024
+ async function loadItem(ctx, channel, id) {
3175
4025
  const grant = await readGrant(ctx, channel);
3176
4026
  if (!ID_SEGMENT.test(id)) throw new ZasError("not_found", 404);
3177
4027
  const channelKey = channelKeyOf(ctx.identity, grant);
@@ -3190,6 +4040,12 @@ async function getItem(ctx, channel, id, dest) {
3190
4040
  if (!row || !readable(row)) throw new ZasError("not_found", 404);
3191
4041
  const manifest = openFor(channelKey, row);
3192
4042
  if (!manifest) throw new ZasError("not_found", 404);
4043
+ return { grant, channelKey, row, manifest };
4044
+ }
4045
+ async function getItemMeasured(ctx, channel, id, dest) {
4046
+ ctx.transferTelemetry?.mark("authorization");
4047
+ const { row, manifest } = await loadItem(ctx, channel, id);
4048
+ ctx.transferTelemetry?.describe(manifest.size, itemKind2(manifest), manifest.kind === "text" ? "manifest" : "storage");
3193
4049
  if (manifest.kind === "text") {
3194
4050
  const text2 = manifest.text ?? "";
3195
4051
  return { text: text2, bytes: new TextEncoder().encode(text2).length };
@@ -3209,7 +4065,10 @@ async function getItem(ctx, channel, id, dest) {
3209
4065
  tmp = `${target}.${randomBytes2(6).toString("hex")}.tmp`;
3210
4066
  fd = openSync(tmp, "wx", 384);
3211
4067
  for (const chunk of manifest.chunks) {
3212
- written += writeSync(fd, await fetchChunk(ctx, chunk));
4068
+ const bytes = await fetchChunk(ctx, chunk);
4069
+ ctx.transferTelemetry?.mark("assembling");
4070
+ written += writeSync(fd, bytes);
4071
+ ctx.transferTelemetry?.progress(written);
3213
4072
  }
3214
4073
  const size = typeof manifest.size === "number" ? manifest.size : 0;
3215
4074
  if (size > 0 && written !== size) throw new ZasError("not_found", 404);
@@ -3239,867 +4098,577 @@ async function getItem(ctx, channel, id, dest) {
3239
4098
  }
3240
4099
  return { path: target, bytes: written };
3241
4100
  }
4101
+ async function getItem(...args) {
4102
+ const telemetry = new TransferTelemetry(args[0].client, "download", "unknown", "manifest");
4103
+ try {
4104
+ const result = await getItemMeasured({ ...args[0], transferTelemetry: telemetry }, ...args.slice(1));
4105
+ telemetry.describe(result.bytes);
4106
+ telemetry.finish("success", "none", false);
4107
+ return result;
4108
+ } catch (error) {
4109
+ telemetry.failed(error);
4110
+ throw error;
4111
+ }
4112
+ }
3242
4113
 
3243
- // src/receive.ts
3244
- var OFFER_WAIT_MS2 = 9.5 * 60 * 1e3;
3245
- var OFFER_POLL_MS2 = 1e3;
3246
- var SIGNAL_POLL_MS2 = 400;
3247
- var OFFER_PAGE = 10;
3248
- var OPEN_OFFERS = {
3249
- from: [{ collectionId: "direct" }],
3250
- orderBy: [{ field: { fieldPath: "created_at" }, direction: "DESCENDING" }],
3251
- limit: OFFER_PAGE
3252
- };
3253
- var SIGNALS_FOR_RECEIVER = {
3254
- from: [{ collectionId: "signals" }],
3255
- where: { fieldFilter: { field: { fieldPath: "for" }, op: "EQUAL", value: { stringValue: "receiver" } } }
3256
- };
3257
- var stringField2 = (doc, key) => doc?.fields?.[key]?.stringValue;
3258
- var idOf = (doc) => (doc.name ?? "").split("/").pop() ?? "";
3259
- async function receiveGrantFor(ctx, channel) {
3260
- const grant = resolveChannel(ctx.identity, await grantsFor(ctx.client, ctx.profile), channel);
3261
- if (!grant.read) throw new ZasError("read_forbidden", 403);
3262
- if (!grant.direct_mode) throw new ZasError("not_direct_mode", 409);
3263
- return grant;
4114
+ // src/edit.ts
4115
+ function writable(grant) {
4116
+ if (!grant.send || grant.mode === "view") throw new ZasError("send_forbidden", 403);
4117
+ if (grant.direct_mode) throw new ZasError("direct_mode", 409);
3264
4118
  }
3265
- var DiskSink = class {
3266
- constructor(dest, fallbackName) {
3267
- this.dest = dest;
3268
- this.fallbackName = fallbackName;
3269
- }
3270
- /** Where the bytes will be once `close` has run. */
3271
- target;
3272
- written = 0;
3273
- /** A filesystem refusal, kept so the run can answer `write_failed` rather
3274
- * than reporting the engine's generic sink failure. */
3275
- failure;
3276
- created;
3277
- tmp;
3278
- fd;
3279
- open(meta) {
3280
- try {
3281
- const chosen = destinationOf(this.dest, meta.name, this.fallbackName);
3282
- this.target = chosen.target;
3283
- this.created = chosen.created;
3284
- mkdirSync3(dirname2(this.target), { recursive: true, mode: 448 });
3285
- this.tmp = `${this.target}.${randomBytes3(6).toString("hex")}.tmp`;
3286
- this.fd = openSync2(this.tmp, "wx", 384);
3287
- } catch (error) {
3288
- throw this.remember(error);
3289
- }
3290
- }
3291
- write(bytes) {
3292
- try {
3293
- if (this.fd === void 0) throw new Error("sink_closed");
3294
- this.written += writeSync2(this.fd, bytes);
3295
- } catch (error) {
3296
- throw this.remember(error);
3297
- }
3298
- }
3299
- close() {
3300
- try {
3301
- if (this.fd === void 0) return;
3302
- closeSync2(this.fd);
3303
- this.fd = void 0;
3304
- renameSync3(this.tmp, this.target);
3305
- this.tmp = void 0;
3306
- } catch (error) {
3307
- throw this.remember(error);
3308
- }
4119
+ async function ownItem(ctx, channel, id) {
4120
+ const loaded = await loadItem(ctx, channel, id);
4121
+ writable(loaded.grant);
4122
+ if (loaded.row.agent !== ctx.identity.agent_uid) throw new ZasError("not_yours", 403);
4123
+ return loaded;
4124
+ }
4125
+ function sealedFor(item, manifest) {
4126
+ return bytesToB64(sealManifest(item.channelKey, manifest, item.grant.key_version));
4127
+ }
4128
+ function guardOf(item) {
4129
+ return item.row.updateTime === null ? {} : { expected_update_time: item.row.updateTime };
4130
+ }
4131
+ async function editItem(ctx, input) {
4132
+ const item = await ownItem(ctx, input.channel, input.id);
4133
+ const { grant, row, manifest } = item;
4134
+ const kind = manifest.kind === "text" ? "text" : "file";
4135
+ const noteFields = input.text !== void 0 || input.lang !== void 0 || input.secret !== void 0;
4136
+ if (noteFields && kind !== "text") throw new ZasError("not_a_note", 400);
4137
+ const done = (updateTime) => ({
4138
+ link_id: row.id,
4139
+ channel_id: grant.channel_id,
4140
+ channel_name: channelNameOf(ctx.identity, grant),
4141
+ kind,
4142
+ title: manifest.title ?? manifest.name,
4143
+ update_time: updateTime
4144
+ });
4145
+ if (input.title === void 0 && !noteFields) return done(row.updateTime ?? Date.now());
4146
+ if (input.title !== void 0) {
4147
+ if (input.title === "") delete manifest.title;
4148
+ else manifest.title = input.title;
3309
4149
  }
3310
- /** Best effort, and safe to call twice: the engine aborts on every failure
3311
- * path of its own, and the job aborts again on the way out. */
3312
- abort() {
3313
- if (this.fd !== void 0) {
3314
- try {
3315
- closeSync2(this.fd);
3316
- } catch {
3317
- }
3318
- this.fd = void 0;
3319
- }
3320
- if (this.tmp !== void 0) {
3321
- if (existsSync3(this.tmp)) try {
3322
- unlinkSync2(this.tmp);
3323
- } catch {
3324
- }
3325
- this.tmp = void 0;
3326
- }
3327
- if (this.created !== void 0) {
3328
- try {
3329
- rmdirSync2(this.created);
3330
- } catch {
3331
- }
3332
- this.created = void 0;
3333
- }
4150
+ if (input.text !== void 0) {
4151
+ manifest.text = input.text;
4152
+ manifest.size = new TextEncoder().encode(input.text).length;
4153
+ delete manifest.html;
4154
+ if (manifest.title === void 0) manifest.name = noteName(input.text);
3334
4155
  }
3335
- /** A `ZasError` says what happened already. An errno does not, so it is kept
3336
- * and turned into one sentence by the caller — recognised by the shape Node
3337
- * puts on it (`syscall`/`errno`) rather than by `code`, which `ZasError`
3338
- * also carries. */
3339
- remember(error) {
3340
- const failure = error;
3341
- if (typeof failure?.syscall === "string" || typeof failure?.errno === "number") this.failure = failure;
3342
- return error;
4156
+ if (input.lang !== void 0) {
4157
+ if (input.lang === "") delete manifest.code;
4158
+ else manifest.code = { lang: input.lang, auto: false };
3343
4159
  }
3344
- };
3345
- async function receiveDirect(ctx, input, report2, deps = {}) {
3346
- const now = deps.now ?? (() => Date.now());
3347
- const sleep2 = deps.sleep ?? defaultSleep2;
3348
- const grant = await receiveGrantFor(ctx, input.channel);
3349
- const key = channelKeyOf(ctx.identity, grant);
3350
- const channelName = channelLabel(ctx, grant);
3351
- const cid = grant.channel_id;
3352
- const owner = ctx.identity.owner_uid;
3353
- const account = channelAccountOf(ctx.identity, grant);
3354
- const device = deps.device ?? newDeviceToken();
3355
- const { seal, open } = sealer(key, grant.key_version);
3356
- const stamp = { device, owner_uid: owner };
3357
- const startedAt = now();
3358
- const channelPath = `accounts/${account}/channels/${cid}`;
3359
- report2("waiting");
3360
- const waitUntil = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS2);
3361
- let id;
3362
- let meta;
3363
- while (id === void 0) {
3364
- const rows = await ctx.client.firestoreRunQuery(channelPath, OPEN_OFFERS).catch(() => []);
3365
- for (const row of rows) {
3366
- if (stringField2(row, "state") !== "open") continue;
3367
- if (stringField2(row, "sender") === ctx.identity.agent_uid) continue;
3368
- const enc = stringField2(row, "meta_enc");
3369
- if (enc === void 0) continue;
3370
- let candidate;
3371
- try {
3372
- candidate = open(enc);
3373
- } catch {
3374
- continue;
3375
- }
3376
- if (typeof candidate?.name !== "string" || typeof candidate?.size !== "number") continue;
3377
- if (candidate.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
3378
- const offerId2 = idOf(row);
3379
- if (offerId2 === "") continue;
3380
- try {
3381
- await ctx.client.api("POST", `/direct/${cid}/${offerId2}/claim`, { size_bytes: candidate.size, ...stamp });
3382
- } catch (error) {
3383
- const word = error instanceof ZasError ? error.serverCode ?? error.code : "";
3384
- if (word === "claimed") throw new ZasError("offer_taken", 409);
3385
- if (word === "expired" || word === "own_offer" || word === "not_found") continue;
3386
- throw error;
3387
- }
3388
- id = offerId2;
3389
- meta = candidate;
3390
- break;
3391
- }
3392
- if (id !== void 0) break;
3393
- if (now() >= waitUntil) throw new ZasError("no_offer", 0);
3394
- await sleep2(deps.offerPollMs ?? OFFER_POLL_MS2);
4160
+ if (input.secret !== void 0) {
4161
+ if (input.secret) manifest.sensitive = true;
4162
+ else delete manifest.sensitive;
3395
4163
  }
3396
- const offered = meta;
3397
- const offerId = id;
3398
- const route = `/direct/${cid}/${offerId}`;
3399
- const offerPath = `${channelPath}/direct/${offerId}`;
3400
- const setState = (state) => ctx.client.api("POST", `${route}/state`, { state, ...stamp }).then(() => void 0, () => void 0);
3401
- await (deps.installWebRtc ?? installWebRtc)();
3402
- const fetchIce = async () => {
3403
- try {
3404
- const r = await ctx.client.api("POST", `${route}/ice`, stamp);
3405
- return Array.isArray(r.ice) ? r.ice : [];
3406
- } catch {
3407
- return [];
4164
+ const answer = await ctx.client.api(
4165
+ "PATCH",
4166
+ `/links/${grant.channel_id}/${row.id}`,
4167
+ {
4168
+ manifest_enc: sealedFor(item, manifest),
4169
+ // The caller is changing what the item says: the server records it.
4170
+ edit: true,
4171
+ ...guardOf(item)
3408
4172
  }
3409
- };
3410
- const turn = await fetchIce();
3411
- report2("connecting");
3412
- const sink = new DiskSink(input.dest, offerId);
3413
- let resolveOutcome;
3414
- const outcome = new Promise((resolve2) => {
3415
- resolveOutcome = resolve2;
4173
+ );
4174
+ forgetLink(ctx.profile, row.id);
4175
+ return done(typeof answer.update_time === "number" ? answer.update_time : Date.now());
4176
+ }
4177
+ async function replaceFile(ctx, input, onPhase) {
4178
+ await checkFilePath(input.path);
4179
+ const item = await ownItem(ctx, input.channel, input.id);
4180
+ const { grant, row, manifest } = item;
4181
+ if (manifest.kind === "text") throw new ZasError("not_a_file", 400);
4182
+ const { bytes, name } = await readFileChecked(input.path);
4183
+ const title = input.title ?? manifest.title;
4184
+ const contentHash = await blake3Hex(bytes);
4185
+ const placed = await placeFile(ctx, bytes, onPhase);
4186
+ onPhase?.("finishing");
4187
+ const mime = mimeFor(input.path);
4188
+ const thumb = await thumbnailFor(bytes, mime);
4189
+ const next = newManifest({
4190
+ kind: "file",
4191
+ name,
4192
+ ...title !== void 0 ? { title } : {},
4193
+ mime,
4194
+ size: bytes.length,
4195
+ // The item's own time: a replace changes what the item holds, not when the
4196
+ // owner got it, and the row keeps its place in the channel either way.
4197
+ created_at: manifest.created_at || (/* @__PURE__ */ new Date()).toISOString(),
4198
+ ...thumb ? { thumb_data: thumb } : {},
4199
+ chunks: placed.map((p) => p.entry)
3416
4200
  });
3417
- let diag;
3418
- let via;
3419
- const handle = (deps.engine ?? startReceiver)({
3420
- ice: [...DIRECT_ICE, ...turn],
3421
- refreshIce: async () => [...DIRECT_ICE, ...await fetchIce()],
3422
- // The name and size this end read off the offer. The engine refuses a
3423
- // sender that announces anything else.
3424
- expectedMeta: offered,
3425
- send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "sender", ...stamp }).then(() => void 0),
3426
- onPhase: (phase2) => {
3427
- if (phase2 === "flight") report2("flight");
3428
- if (phase2 === "done" || phase2 === "failed") resolveOutcome(phase2);
3429
- },
3430
- onPath: (p) => {
3431
- via = p;
3432
- },
3433
- onDiag: (d) => {
3434
- diag = d;
3435
- deps.onDiag?.(d);
3436
- },
3437
- sink: async (m) => {
3438
- sink.open(m);
3439
- return sink;
3440
- }
4201
+ const release = manifest.chunks.map((c) => c.cap).filter((cap) => typeof cap === "string");
4202
+ const answer = await ctx.client.api("POST", `/links/${grant.channel_id}/${row.id}/replace`, {
4203
+ manifest_enc: sealedFor(item, next),
4204
+ caps: placed.map((p) => p.entry.cap).filter((cap) => typeof cap === "string"),
4205
+ release,
4206
+ // Scoped to the link as well as the channel: the same bytes sent as a new
4207
+ // item are a different request from these bytes replacing this one.
4208
+ idempotency_key: agentSendIdempotencyKey(`${grant.channel_id}/${row.id}`, contentHash, title ?? name),
4209
+ ...guardOf(item)
3441
4210
  });
3442
- let over = false;
3443
- const pump = (async () => {
3444
- const seen = /* @__PURE__ */ new Set();
3445
- while (!over) {
3446
- let rows = [];
3447
- try {
3448
- rows = await ctx.client.firestoreRunQuery(offerPath, SIGNALS_FOR_RECEIVER);
3449
- } catch {
3450
- }
3451
- for (const row of rows) {
3452
- if (!row?.name || seen.has(row.name)) continue;
3453
- seen.add(row.name);
3454
- const enc = stringField2(row, "payload_enc");
3455
- if (!enc) continue;
3456
- try {
3457
- handle.accept(open(enc));
3458
- } catch {
3459
- }
3460
- }
3461
- if (!over) await Promise.race([outcome, sleep2(deps.signalPollMs ?? SIGNAL_POLL_MS2)]);
3462
- }
3463
- })();
3464
- const phase = await outcome;
3465
- over = true;
3466
- await pump;
3467
- report2("finishing");
3468
- if (phase === "done") {
3469
- await setState("done");
3470
- return {
3471
- offer_id: offerId,
3472
- channel_id: cid,
3473
- channel_name: channelName,
3474
- name: offered.name,
3475
- bytes: sink.written,
3476
- path: sink.target,
3477
- ...via ? { via } : {},
3478
- duration_ms: now() - startedAt
3479
- };
3480
- }
3481
- sink.abort();
3482
- const doc = await ctx.client.firestoreGet(offerPath).catch(() => null);
3483
- const withdrawn = doc === null || stringField2(doc, "state") === "cancelled";
3484
- await setState("failed");
3485
- if (sink.failure) throw new ZasError("write_failed", 0, sink.failure.message);
3486
- if (withdrawn) throw new ZasError("direct_cancelled", 0);
3487
- const reason = diag?.reason || "unknown";
3488
- deps.onFailed?.({
3489
- channel_id: cid,
3490
- channel_name: channelName,
3491
- offer_id: offerId,
3492
- owner_uid: owner,
3493
- channel_account: account,
3494
- device,
3495
- ...input.dest !== void 0 ? { dest: input.dest } : {},
3496
- meta: offered,
3497
- key,
3498
- key_version: grant.key_version,
3499
- reason
3500
- });
3501
- throw new ZasError("direct_failed", 0, reason);
3502
- }
3503
- async function receiveDirectFallback(ctx, record, report2, deps = {}) {
3504
- const now = deps.now ?? (() => Date.now());
3505
- const startedAt = now();
3506
- const { open } = sealer(record.key, record.key_version);
3507
- const stamp = { device: record.device, owner_uid: record.owner_uid };
3508
- const base = `/direct/${record.channel_id}/${record.offer_id}/fallback`;
3509
- const offerPath = `accounts/${record.channel_account}/channels/${record.channel_id}/direct/${record.offer_id}`;
3510
- const doc = await ctx.client.firestoreGet(offerPath);
3511
- const enc = stringField2(doc, "fallback_meta_enc");
3512
- let meta;
3513
- if (enc !== void 0) {
3514
- try {
3515
- meta = fallbackMetaOf(open(enc));
3516
- } catch {
3517
- meta = void 0;
3518
- }
3519
- }
3520
- if (!meta) throw new ZasError("fallback_unavailable", 409);
3521
- if (meta.name !== record.meta.name || meta.size !== record.meta.size || meta.mime !== record.meta.mime) {
3522
- throw new ZasError("file_changed", 0);
3523
- }
3524
- const sink = new DiskSink(record.dest, record.offer_id);
3525
- report2("downloading");
3526
- try {
3527
- sink.open(meta);
3528
- await downloadFallback({
3529
- offerId: record.offer_id,
3530
- meta,
3531
- sink,
3532
- getUrl: async () => (await ctx.client.api("POST", `${base}/download`, stamp)).url,
3533
- ...deps.fetcher ? { fetcher: deps.fetcher } : {}
3534
- });
3535
- } catch (error) {
3536
- sink.abort();
3537
- if (sink.failure) throw new ZasError("write_failed", 0, sink.failure.message);
3538
- if (error instanceof ZasError) throw error;
3539
- throw new ZasError("network", 0, error instanceof Error ? error.message : String(error));
3540
- }
3541
- report2("finishing");
3542
- await ctx.client.api("POST", `${base}/received`, stamp);
3543
- return {
3544
- offer_id: record.offer_id,
3545
- channel_id: record.channel_id,
3546
- channel_name: record.channel_name,
3547
- name: meta.name,
3548
- bytes: sink.written,
3549
- path: sink.target,
3550
- duration_ms: now() - startedAt
3551
- };
3552
- }
3553
-
3554
- // src/jobs.ts
3555
- import { randomUUID } from "node:crypto";
3556
- var DEFAULT_WAIT_MS = 6e4;
3557
- var HISTORY = 50;
3558
- var JobRunner = class {
3559
- now;
3560
- waitMs;
3561
- /** Newest first, and trimmed. */
3562
- jobs = [];
3563
- /** Keyed on the job object, not its id, so a caller holding a job that has
3564
- * already aged out of the list can still wait for it to finish. */
3565
- settled = /* @__PURE__ */ new WeakMap();
3566
- constructor(opts = {}) {
3567
- this.now = opts.now ?? (() => Date.now());
3568
- this.waitMs = opts.waitMs ?? DEFAULT_WAIT_MS;
3569
- }
3570
- start(kind, title, channel, work) {
3571
- const job = {
3572
- id: randomUUID(),
3573
- kind,
3574
- title,
3575
- channel,
3576
- started_at: this.now(),
3577
- phase: null,
3578
- status: "running"
3579
- };
3580
- this.jobs.unshift(job);
3581
- this.jobs.length = Math.min(this.jobs.length, HISTORY);
3582
- const report2 = (phase) => {
3583
- if (job.status === "running") job.phase = phase;
3584
- };
3585
- const note = (diag) => {
3586
- job.diag = diag;
3587
- };
3588
- this.settled.set(job, work(report2, note).then(
3589
- (result) => {
3590
- job.status = "done";
3591
- job.result = result;
3592
- return job;
3593
- },
3594
- (error) => {
3595
- job.status = "failed";
3596
- job.error = error instanceof ZasError ? {
3597
- code: error.code,
3598
- status: error.status,
3599
- sentence: humanSentence(error),
3600
- message: error.message,
3601
- ...error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {},
3602
- ...error.serverCode !== void 0 ? { serverCode: error.serverCode } : {}
3603
- } : { code: "upload_failed", status: 0, sentence: humanSentence(new ZasError("upload_failed", 0)) };
3604
- return job;
3605
- }
3606
- ));
3607
- return job;
3608
- }
3609
- /** Resolves when the work settles, or when the wait runs out — the same job
3610
- * object either way, so the caller reads `status` rather than guessing. */
3611
- async wait(job) {
3612
- const settled = this.settled.get(job);
3613
- if (!settled) return job;
3614
- let timer;
3615
- const deadline = new Promise((resolve2) => {
3616
- timer = setTimeout(() => resolve2(job), this.waitMs);
3617
- });
3618
- try {
3619
- return await Promise.race([settled, deadline]);
3620
- } finally {
3621
- clearTimeout(timer);
3622
- }
3623
- }
3624
- list() {
3625
- return this.jobs.slice(0, HISTORY);
3626
- }
3627
- get(id) {
3628
- return this.jobs.find((job) => job.id === id);
3629
- }
3630
- };
3631
-
3632
- // src/shared/product-analytics.ts
3633
- var PRODUCT_EVENTS = [
3634
- "account_created",
3635
- "item.created",
3636
- "item.downloaded",
3637
- "item.copied",
3638
- "item.shared",
3639
- "item.pinned",
3640
- "item.unpinned",
3641
- "item.deleted",
3642
- "item.departed",
3643
- "channel_shared",
3644
- "channel_mode_set",
3645
- "invite_created",
3646
- "invite_link_copied",
3647
- "invite_qr_shown",
3648
- "invite_system_sheet_opened",
3649
- "invite_joined",
3650
- "invite_converted",
3651
- "channel.invite_preview",
3652
- "channel.invite_accept",
3653
- "channel.invite_signin",
3654
- "enterprise.invite_preview",
3655
- "enterprise.invite_identity",
3656
- "enterprise.invite_signin",
3657
- "enterprise.invite_account_switch",
3658
- "enterprise.invite_attach",
3659
- "enterprise.invite_accept",
3660
- "enterprise.invite_admin",
3661
- "enterprise.workspace_opened",
3662
- "enterprise.workspace_switched",
3663
- "enterprise.channel_created",
3664
- "enterprise.channel_members_added",
3665
- "share_link_copied",
3666
- "share_qr_shown",
3667
- "share_system_sheet_opened",
3668
- "share_opened",
3669
- "share_content_copied",
3670
- "share_downloaded",
3671
- "share_signin_click",
3672
- "share.saved",
3673
- "share_bar_on",
3674
- "share_bar_off",
3675
- "upload.phase_timing",
3676
- "auth.signin_started",
3677
- "auth.signin_result",
3678
- "auth.session_started",
3679
- "auth.signed_out",
3680
- "auth.session_lost",
3681
- "auth.token_refresh_failed",
3682
- "account_switch.started",
3683
- "account_switch.completed",
3684
- "account_switch.failed",
3685
- "bootstrap.result",
3686
- "direct_mode_set",
3687
- "direct.offered",
3688
- "direct.claimed",
3689
- "direct.connected",
3690
- "direct.completed",
3691
- "direct.received",
3692
- "direct.failed",
3693
- "direct.fallback_started",
3694
- "direct.fallback_ready",
3695
- "direct.fallback_received",
3696
- "direct.fallback_failed",
3697
- "direct.fallback_aborted",
3698
- "direct.too_big",
3699
- "agent.pair_opened",
3700
- "agent.pair_approved",
3701
- "agent.pair_refused",
3702
- "agent.paired",
3703
- "agent.revoked",
3704
- "agent.send",
3705
- "agent.tool_call"
3706
- ];
3707
- var SERVER_OWNED_PRODUCT_EVENTS = [
3708
- "account_created",
3709
- "item.departed",
3710
- "invite_created",
3711
- "invite_joined",
3712
- "invite_converted",
3713
- "share_opened",
3714
- "share_content_copied",
3715
- "share_downloaded",
3716
- "share_signin_click",
3717
- "agent.send",
3718
- "agent.tool_call"
3719
- ];
3720
- var SERVER_OWNED_PRODUCT_EVENT_SET = new Set(SERVER_OWNED_PRODUCT_EVENTS);
3721
- var CLIENT_PRODUCT_EVENTS = PRODUCT_EVENTS.filter(
3722
- (event) => !SERVER_OWNED_PRODUCT_EVENT_SET.has(event)
3723
- );
3724
- var bool = { type: "boolean" };
3725
- var count = { type: "number", min: 0 };
3726
- var oneOf = (...values) => ({ type: "string", values: new Set(values) });
3727
- var sizeBucket = oneOf("lt_1mb", "1_10mb", "10_100mb", "100_512mb", "512mb_5gb", "gt_5gb");
3728
- var itemKind = oneOf("file", "text");
3729
- var itemKindOrUnknown = oneOf("file", "text", "unknown");
3730
- var role = oneOf("sender", "receiver");
3731
- var candidateType = oneOf("", "host", "srflx", "relay", "prflx", "other");
3732
- var enterpriseRole = oneOf("owner", "admin", "billing_admin", "member", "guest");
3733
- var agentKind = oneOf("claude_code", "codex", "other");
3734
- var pairHandoff = oneOf("loopback", "manual", "unknown");
3735
- var AGENT_TOOL_NAMES = [
3736
- "status",
3737
- "pair",
3738
- "send_file",
3739
- "send_note",
3740
- "send_direct",
3741
- "send_direct_fallback",
3742
- "receive_direct",
3743
- "receive_direct_fallback",
3744
- "list_items",
3745
- "get_item",
3746
- "jobs"
3747
- ];
3748
- var AGENT_ERROR_CODES = [
3749
- "none",
3750
- "other",
3751
- "identity_corrupt",
3752
- "not_paired",
3753
- "internal",
3754
- "agent_forbidden",
3755
- "agent_revoked",
3756
- "grant_missing",
3757
- "not_found",
3758
- "invalid_cap",
3759
- "write_failed",
3760
- "send_forbidden",
3761
- "read_forbidden",
3762
- "direct_mode",
3763
- "not_direct_mode",
3764
- "not_claimed",
3765
- "no_offer",
3766
- "offer_taken",
3767
- "direct_cancelled",
3768
- "direct_failed",
3769
- "direct_not_failed",
3770
- "file_changed",
3771
- "webrtc_unavailable",
3772
- "fallback_unavailable",
3773
- "key_stale",
3774
- "quota_exceeded",
3775
- "rate_limited",
3776
- "file_too_big",
3777
- "duplicate",
3778
- "pairing_expired",
3779
- "pairing_cancelled",
3780
- "claim_mismatch",
3781
- "pairing_not_approved",
3782
- "pairing_claimed",
3783
- "agent_limit",
3784
- "grant_limit",
3785
- "feature_disabled",
3786
- "upload_failed",
3787
- "oprf_failed",
3788
- "network",
3789
- "sign_in_failed",
3790
- "bad_signature",
3791
- "missing_token"
3792
- ];
3793
- function agentDurationBucket(ms) {
3794
- if (ms < 1e3) return "lt_1s";
3795
- if (ms < 1e4) return "1_10s";
3796
- if (ms < 6e4) return "10_60s";
3797
- return "gt_60s";
3798
- }
3799
- var EVENT_PROPERTY_RULES = {
3800
- account_created: {},
3801
- "item.created": { kind: itemKind, size_bytes: count, size_bucket: sizeBucket },
3802
- "item.downloaded": { size_bytes: count, size_bucket: sizeBucket },
3803
- "item.copied": {},
3804
- "item.shared": { kind: itemKind },
3805
- "item.pinned": { age_ms: count },
3806
- "item.unpinned": {},
3807
- "item.deleted": { age_ms: count, was_pinned: bool, kind: itemKindOrUnknown },
3808
- "item.departed": {},
3809
- channel_shared: {},
3810
- channel_mode_set: { mode: oneOf("edit", "view") },
3811
- invite_created: {},
3812
- invite_link_copied: {},
3813
- invite_qr_shown: {},
3814
- invite_system_sheet_opened: {},
3815
- invite_joined: {},
3816
- invite_converted: {},
3817
- "channel.invite_preview": {
3818
- outcome: oneOf("ready", "unavailable", "missing_key"),
3819
- signed_in: bool
3820
- },
3821
- "channel.invite_accept": {
3822
- outcome: oneOf("started", "completed", "already_member", "unavailable", "failed")
3823
- },
3824
- "channel.invite_signin": {
3825
- stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
3826
- },
3827
- "enterprise.invite_preview": {
3828
- outcome: oneOf("pending", "resumable", "unavailable"),
3829
- role: enterpriseRole
3830
- },
3831
- "enterprise.invite_identity": {
3832
- outcome: oneOf("matched", "mismatched", "unavailable", "failed")
3833
- },
3834
- "enterprise.invite_signin": {
3835
- stage: oneOf("link_requested", "link_sent", "link_completed", "failed")
3836
- },
3837
- "enterprise.invite_account_switch": { stage: oneOf("started", "completed", "failed") },
3838
- "enterprise.invite_attach": {
3839
- stage: oneOf("link_requested", "link_sent", "verify_started", "completed", "address_taken", "link_invalid", "failed")
3840
- },
3841
- "enterprise.invite_accept": {
3842
- outcome: oneOf("started", "completed", "recovered", "timeout", "unavailable", "email_mismatch", "seat_limit", "failed"),
3843
- role: enterpriseRole,
3844
- destination: oneOf("workspace", "admin_console"),
3845
- duration_ms: count
3846
- },
3847
- "enterprise.invite_admin": {
3848
- outcome: oneOf("created", "email_sent", "email_failed", "reissued", "revoked"),
3849
- role: enterpriseRole,
3850
- external: bool
3851
- },
3852
- "enterprise.workspace_opened": { role: enterpriseRole, first_visit: bool },
3853
- "enterprise.workspace_switched": { destination: oneOf("personal", "organization"), role: enterpriseRole },
3854
- "enterprise.channel_created": { member_count: count },
3855
- "enterprise.channel_members_added": { member_count: count },
3856
- share_link_copied: {},
3857
- share_qr_shown: {},
3858
- share_system_sheet_opened: {},
3859
- share_opened: {},
3860
- share_content_copied: {},
3861
- share_downloaded: {},
3862
- share_signin_click: {},
3863
- "share.saved": {},
3864
- share_bar_on: {},
3865
- share_bar_off: {},
3866
- "upload.phase_timing": {
3867
- stage: oneOf("staging", "bootstrap", "thumbnail", "encrypting", "uploading", "finishing"),
3868
- duration_ms: count,
3869
- size_bucket: sizeBucket,
3870
- outcome: oneOf("success", "failure", "cancelled"),
3871
- attempt: { type: "number", min: 1 }
3872
- },
3873
- "auth.signin_started": { provider: oneOf("google.com", "apple.com") },
3874
- "auth.signin_result": {
3875
- provider: oneOf("google.com", "apple.com"),
3876
- outcome: oneOf("signed_in", "cancelled", "taken", "failed", "needs_link"),
3877
- flow: oneOf("signin", "link", "connect")
3878
- },
3879
- "account_switch.started": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
3880
- "account_switch.completed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
3881
- "account_switch.failed": { operation: oneOf("remember", "switch"), provider: oneOf("google.com", "apple.com", "email_link", "qr", "code") },
3882
- "auth.session_started": { cold: bool },
3883
- "auth.signed_out": { reason: oneOf("user", "account_switch", "abandoned_new_account", "account_deleted") },
3884
- "auth.session_lost": { session_age_ms: count, foreground: bool },
3885
- "auth.token_refresh_failed": {
3886
- code: oneOf("network", "token_invalid", "user_disabled", "user_missing", "rate_limited", "internal", "unknown")
3887
- },
3888
- "bootstrap.result": {
3889
- outcome: oneOf("offline", "ok", "timeout_warm", "timeout", "failed_warm", "failed"),
3890
- stage: oneOf("cached", "done", "unknown", "keyring", "account_init", "listeners"),
3891
- duration_ms: count,
3892
- fault: oneOf("intercepted", "offline", "unknown")
3893
- },
3894
- direct_mode_set: { on: bool },
3895
- "direct.offered": { size_bucket: sizeBucket },
3896
- // `via_agent`: the offer came from a paired agent's process, not from one
3897
- // of the person's devices. The claim and the two receiver outcomes carry it.
3898
- "direct.claimed": { size_bucket: sizeBucket, via_agent: bool },
3899
- "direct.connected": { role, ms: count },
3900
- "direct.completed": {
3901
- via: oneOf("lan", "wan", "relay", "unknown"),
3902
- pair_local: candidateType,
3903
- pair_remote: candidateType,
3904
- connect_ms: count,
3905
- duration_ms: count,
3906
- size_bucket: sizeBucket,
3907
- turn_urls_supplied: count,
3908
- turn_urls_configured: count,
3909
- restarts: count
3910
- },
3911
- "direct.received": {
3912
- via_agent: bool,
3913
- via: oneOf("lan", "wan", "relay", "unknown"),
3914
- pair_local: candidateType,
3915
- pair_remote: candidateType,
3916
- connect_ms: count,
3917
- duration_ms: count,
3918
- size_bucket: sizeBucket,
3919
- turn_urls_supplied: count,
3920
- turn_urls_configured: count,
3921
- restarts: count
3922
- },
3923
- "direct.failed": {
3924
- role,
3925
- via_agent: bool,
3926
- reason: oneOf(
3927
- "peer_silent",
3928
- "connect_timeout",
3929
- "ice_failed",
3930
- "disconnected",
3931
- "peer_closed",
3932
- "peer_abort",
3933
- "signaling",
3934
- "signal_send",
3935
- "protocol",
3936
- "sink",
3937
- "transfer",
3938
- "integrity",
3939
- "too_big",
3940
- "unknown"
3941
- ),
3942
- ms: count,
3943
- ice_state: oneOf("", "new", "checking", "connected", "completed", "failed", "disconnected", "closed"),
3944
- gather_state: oneOf("", "new", "gathering", "complete"),
3945
- had_remote: bool,
3946
- local_host: count,
3947
- local_srflx: count,
3948
- local_relay: count,
3949
- remote_host: count,
3950
- remote_srflx: count,
3951
- remote_relay: count,
3952
- turn_urls_supplied: count,
3953
- turn_urls_configured: count,
3954
- moved_bucket: sizeBucket,
3955
- size_bucket: sizeBucket,
3956
- restarts: count,
3957
- // Whether the reporting tab was on screen when the run died — the lead a
3958
- // slept machine or a frozen background tab leaves behind.
3959
- visibility: oneOf("visible", "hidden", "prerender")
3960
- },
3961
- "direct.fallback_started": { size_bucket: sizeBucket },
3962
- "direct.fallback_ready": { size_bucket: sizeBucket, duration_ms: count, retries: count },
3963
- "direct.fallback_received": { size_bucket: sizeBucket, duration_ms: count, retries: count },
3964
- "direct.fallback_failed": {
3965
- stage: oneOf("upload", "download"),
3966
- size_bucket: sizeBucket,
3967
- retries: count
3968
- },
3969
- "direct.fallback_aborted": {},
3970
- "direct.too_big": { size_bucket: sizeBucket },
3971
- "agent.pair_opened": { agent_kind: agentKind, protocol: count, replaces: bool },
3972
- "agent.pair_approved": {
3973
- agent_kind: agentKind,
3974
- destination: oneOf("new_channel", "existing"),
3975
- channels: count,
3976
- handoff: pairHandoff
3977
- },
3978
- "agent.pair_refused": {
3979
- agent_kind: agentKind,
3980
- step: oneOf("open", "approve", "claim"),
3981
- reason: oneOf(
3982
- "agent_limit",
3983
- "grant_limit",
3984
- "channel_limit",
3985
- "code_mismatch",
3986
- "pairing_expired",
3987
- "pairing_cancelled",
3988
- "pairing_claimed",
3989
- "feature_disabled",
3990
- "rate_limited",
3991
- "network",
3992
- "other"
3993
- )
3994
- },
3995
- "agent.paired": {
3996
- agent_kind: agentKind,
3997
- destination: oneOf("new_channel", "existing"),
3998
- channels: count,
3999
- handoff: pairHandoff,
4000
- protocol: count
4001
- },
4002
- "agent.revoked": { agent_kind: agentKind },
4003
- "agent.send": { size_bucket: sizeBucket, agent_kind: agentKind },
4004
- // What the CLI reports after every tool call: the tool, whether it worked,
4005
- // the code when it did not, and the version that answered. No path, no file
4006
- // name, no channel, no sentence.
4007
- "agent.tool_call": {
4008
- tool: oneOf(...AGENT_TOOL_NAMES),
4009
- result: oneOf("ok", "error"),
4010
- code: oneOf(...AGENT_ERROR_CODES),
4011
- agent_kind: agentKind,
4012
- duration_bucket: oneOf("lt_1s", "1_10s", "10_60s", "gt_60s"),
4013
- version_major: count,
4014
- version_minor: count,
4015
- version_patch: count
4211
+ const bound = answer.caps ?? {};
4212
+ let patched = false;
4213
+ for (const p of placed) {
4214
+ const cap = bound[p.entry.blob_id];
4215
+ if (cap) {
4216
+ p.entry.cap = cap;
4217
+ patched = true;
4218
+ }
4219
+ }
4220
+ if (patched) {
4221
+ await ctx.client.api("PATCH", `/links/${grant.channel_id}/${row.id}`, { manifest_enc: sealedFor(item, next) });
4016
4222
  }
4223
+ forgetLink(ctx.profile, row.id);
4224
+ return {
4225
+ link_id: row.id,
4226
+ channel_id: grant.channel_id,
4227
+ channel_name: channelNameOf(ctx.identity, grant),
4228
+ bytes: bytes.length,
4229
+ chunks: placed.length,
4230
+ deduplicated: placed.filter((p) => p.proven).length,
4231
+ replayed: answer.replayed === true
4232
+ };
4233
+ }
4234
+
4235
+ // src/receive.ts
4236
+ import { randomBytes as randomBytes3 } from "node:crypto";
4237
+ import {
4238
+ closeSync as closeSync2,
4239
+ existsSync as existsSync3,
4240
+ mkdirSync as mkdirSync3,
4241
+ openSync as openSync2,
4242
+ renameSync as renameSync3,
4243
+ rmdirSync as rmdirSync2,
4244
+ unlinkSync as unlinkSync2,
4245
+ writeSync as writeSync2
4246
+ } from "node:fs";
4247
+ import { dirname as dirname2 } from "node:path";
4248
+ var OFFER_WAIT_MS2 = 9.5 * 60 * 1e3;
4249
+ var OFFER_POLL_MS2 = 1e3;
4250
+ var SIGNAL_POLL_MS2 = 400;
4251
+ var OFFER_PAGE = 10;
4252
+ var OPEN_OFFERS = {
4253
+ from: [{ collectionId: "direct" }],
4254
+ orderBy: [{ field: { fieldPath: "created_at" }, direction: "DESCENDING" }],
4255
+ limit: OFFER_PAGE
4017
4256
  };
4018
- var platformRule = oneOf("web", "android", "ios", "agent", "unknown");
4019
- var PERSON_PROPERTY_RULES = {
4020
- plan: oneOf("anon", "free", "pro", "max"),
4021
- locale: oneOf("es-AR", "pt-BR", "en"),
4022
- storage_bytes: count
4257
+ var SIGNALS_FOR_RECEIVER = {
4258
+ from: [{ collectionId: "signals" }],
4259
+ where: { fieldFilter: { field: { fieldPath: "for" }, op: "EQUAL", value: { stringValue: "receiver" } } }
4023
4260
  };
4024
-
4025
- // src/telemetry.ts
4026
- var OFF_WORDS = /* @__PURE__ */ new Set(["0", "off", "false", "no"]);
4027
- var ON_WORDS = /* @__PURE__ */ new Set(["1", "on", "true", "yes"]);
4028
- function telemetryState() {
4029
- const env = (process.env.ZAS_AGENT_TELEMETRY ?? "").toLowerCase();
4030
- if (OFF_WORDS.has(env)) return { on: false, source: "env" };
4031
- if (ON_WORDS.has(env)) return { on: true, source: "env" };
4032
- if (process.env.DO_NOT_TRACK === "1") return { on: false, source: "do_not_track" };
4033
- const stored = loadSettings();
4034
- if (stored && typeof stored.telemetry === "boolean") return { on: stored.telemetry, source: "file" };
4035
- return { on: true, source: "default" };
4036
- }
4037
- function setTelemetry(on) {
4038
- const stored = loadSettings();
4039
- saveSettings({ ...stored ?? {}, version: 1, telemetry: on, decided_at: Date.now() });
4040
- }
4041
- function noticeShown() {
4042
- return typeof loadSettings()?.notified_at === "number";
4043
- }
4044
- function markNoticeShown() {
4045
- const stored = loadSettings();
4046
- saveSettings({ ...stored ?? {}, version: 1, notified_at: Date.now() });
4261
+ var stringField2 = (doc, key) => doc?.fields?.[key]?.stringValue;
4262
+ var idOf = (doc) => (doc.name ?? "").split("/").pop() ?? "";
4263
+ async function receiveGrantFor(ctx, channel) {
4264
+ const grant = resolveChannel(ctx.identity, await grantsFor(ctx.client, ctx.profile), channel);
4265
+ if (!grant.read) throw new ZasError("read_forbidden", 403);
4266
+ if (!grant.direct_mode) throw new ZasError("not_direct_mode", 409);
4267
+ return grant;
4047
4268
  }
4048
- var TELEMETRY_NOTICE = [
4049
- "Zas collects usage data from this agent to improve the product: which tool",
4050
- "ran, whether it worked, the error code when it did not, how long it took,",
4051
- "and the version. Never file names, file contents, paths or channel names.",
4052
- "The data is linked to your Zas account.",
4053
- "Turn it off with: npx -y zas-agent telemetry off"
4054
- ].join("\n");
4055
- var SOURCE_WORDS = {
4056
- env: "ZAS_AGENT_TELEMETRY",
4057
- do_not_track: "DO_NOT_TRACK",
4058
- file: "zas-agent telemetry",
4059
- default: "default"
4269
+ var DiskSink = class {
4270
+ constructor(dest, fallbackName) {
4271
+ this.dest = dest;
4272
+ this.fallbackName = fallbackName;
4273
+ }
4274
+ /** Where the bytes will be once `close` has run. */
4275
+ target;
4276
+ written = 0;
4277
+ /** A filesystem refusal, kept so the run can answer `write_failed` rather
4278
+ * than reporting the engine's generic sink failure. */
4279
+ failure;
4280
+ created;
4281
+ tmp;
4282
+ fd;
4283
+ open(meta) {
4284
+ try {
4285
+ const chosen = destinationOf(this.dest, meta.name, this.fallbackName);
4286
+ this.target = chosen.target;
4287
+ this.created = chosen.created;
4288
+ mkdirSync3(dirname2(this.target), { recursive: true, mode: 448 });
4289
+ this.tmp = `${this.target}.${randomBytes3(6).toString("hex")}.tmp`;
4290
+ this.fd = openSync2(this.tmp, "wx", 384);
4291
+ } catch (error) {
4292
+ throw this.remember(error);
4293
+ }
4294
+ }
4295
+ write(bytes) {
4296
+ try {
4297
+ if (this.fd === void 0) throw new Error("sink_closed");
4298
+ this.written += writeSync2(this.fd, bytes);
4299
+ } catch (error) {
4300
+ throw this.remember(error);
4301
+ }
4302
+ }
4303
+ close() {
4304
+ try {
4305
+ if (this.fd === void 0) return;
4306
+ closeSync2(this.fd);
4307
+ this.fd = void 0;
4308
+ renameSync3(this.tmp, this.target);
4309
+ this.tmp = void 0;
4310
+ } catch (error) {
4311
+ throw this.remember(error);
4312
+ }
4313
+ }
4314
+ /** Best effort, and safe to call twice: the engine aborts on every failure
4315
+ * path of its own, and the job aborts again on the way out. */
4316
+ abort() {
4317
+ if (this.fd !== void 0) {
4318
+ try {
4319
+ closeSync2(this.fd);
4320
+ } catch {
4321
+ }
4322
+ this.fd = void 0;
4323
+ }
4324
+ if (this.tmp !== void 0) {
4325
+ if (existsSync3(this.tmp)) try {
4326
+ unlinkSync2(this.tmp);
4327
+ } catch {
4328
+ }
4329
+ this.tmp = void 0;
4330
+ }
4331
+ if (this.created !== void 0) {
4332
+ try {
4333
+ rmdirSync2(this.created);
4334
+ } catch {
4335
+ }
4336
+ this.created = void 0;
4337
+ }
4338
+ }
4339
+ /** A `ZasError` says what happened already. An errno does not, so it is kept
4340
+ * and turned into one sentence by the caller — recognised by the shape Node
4341
+ * puts on it (`syscall`/`errno`) rather than by `code`, which `ZasError`
4342
+ * also carries. */
4343
+ remember(error) {
4344
+ const failure = error;
4345
+ if (typeof failure?.syscall === "string" || typeof failure?.errno === "number") this.failure = failure;
4346
+ return error;
4347
+ }
4060
4348
  };
4061
- function telemetryLine() {
4062
- const state = telemetryState();
4063
- return `telemetry: ${state.on ? "on" : "off"} (${SOURCE_WORDS[state.source]})`;
4064
- }
4065
- var TOOL_NAMES = new Set(AGENT_TOOL_NAMES);
4066
- function versionParts(version) {
4067
- const [major = 0, minor = 0, patch = 0] = version.split("-")[0].split(".").map((part) => {
4068
- const value = Number.parseInt(part, 10);
4069
- return Number.isFinite(value) && value >= 0 ? value : 0;
4349
+ async function receiveDirectMeasured(ctx, input, report2, deps = {}) {
4350
+ const now = deps.now ?? (() => Date.now());
4351
+ const sleep2 = deps.sleep ?? defaultSleep2;
4352
+ const grant = await receiveGrantFor(ctx, input.channel);
4353
+ const key = channelKeyOf(ctx.identity, grant);
4354
+ const channelName = channelLabel(ctx, grant);
4355
+ const cid = grant.channel_id;
4356
+ const owner = ctx.identity.owner_uid;
4357
+ const account = channelAccountOf(ctx.identity, grant);
4358
+ const device = deps.device ?? newDeviceToken();
4359
+ const { seal, open } = sealer(key, grant.key_version);
4360
+ const stamp = { device, owner_uid: owner };
4361
+ const startedAt = now();
4362
+ const channelPath = `accounts/${account}/channels/${cid}`;
4363
+ report2("waiting");
4364
+ const waitUntil = startedAt + (deps.offerWaitMs ?? OFFER_WAIT_MS2);
4365
+ let id;
4366
+ let meta;
4367
+ while (id === void 0) {
4368
+ const rows = await ctx.client.firestoreRunQuery(channelPath, OPEN_OFFERS).catch(() => []);
4369
+ for (const row of rows) {
4370
+ if (stringField2(row, "state") !== "open") continue;
4371
+ if (stringField2(row, "sender") === ctx.identity.agent_uid) continue;
4372
+ const enc = stringField2(row, "meta_enc");
4373
+ if (enc === void 0) continue;
4374
+ let candidate;
4375
+ try {
4376
+ candidate = open(enc);
4377
+ } catch {
4378
+ continue;
4379
+ }
4380
+ if (typeof candidate?.name !== "string" || typeof candidate?.size !== "number") continue;
4381
+ if (candidate.size > DIRECT_FILE_MAX_BYTES) throw new ZasError("file_too_big", 413);
4382
+ const offerId2 = idOf(row);
4383
+ if (offerId2 === "") continue;
4384
+ try {
4385
+ await ctx.client.api("POST", `/direct/${cid}/${offerId2}/claim`, { size_bytes: candidate.size, ...stamp });
4386
+ } catch (error) {
4387
+ const word = error instanceof ZasError ? error.serverCode ?? error.code : "";
4388
+ if (word === "claimed") throw new ZasError("offer_taken", 409);
4389
+ if (word === "expired" || word === "own_offer" || word === "not_found") continue;
4390
+ throw error;
4391
+ }
4392
+ id = offerId2;
4393
+ meta = candidate;
4394
+ break;
4395
+ }
4396
+ if (id !== void 0) break;
4397
+ if (now() >= waitUntil) throw new ZasError("no_offer", 0);
4398
+ await sleep2(deps.offerPollMs ?? OFFER_POLL_MS2);
4399
+ }
4400
+ const offered = meta;
4401
+ ctx.transferTelemetry?.describe(offered.size);
4402
+ const offerId = id;
4403
+ const route = `/direct/${cid}/${offerId}`;
4404
+ const offerPath = `${channelPath}/direct/${offerId}`;
4405
+ const setState = (state) => ctx.client.api("POST", `${route}/state`, { state, ...stamp }).then(() => void 0, () => void 0);
4406
+ await (deps.installWebRtc ?? installWebRtc)();
4407
+ const fetchIce = async () => {
4408
+ try {
4409
+ const r = await ctx.client.api("POST", `${route}/ice`, stamp);
4410
+ return Array.isArray(r.ice) ? r.ice : [];
4411
+ } catch {
4412
+ return [];
4413
+ }
4414
+ };
4415
+ const turn = await fetchIce();
4416
+ report2("connecting");
4417
+ const sink = new DiskSink(input.dest, offerId);
4418
+ let resolveOutcome;
4419
+ const outcome = new Promise((resolve2) => {
4420
+ resolveOutcome = resolve2;
4421
+ });
4422
+ let diag;
4423
+ let via;
4424
+ const handle = (deps.engine ?? startReceiver)({
4425
+ ice: [...DIRECT_ICE, ...turn],
4426
+ refreshIce: async () => [...DIRECT_ICE, ...await fetchIce()],
4427
+ // The name and size this end read off the offer. The engine refuses a
4428
+ // sender that announces anything else.
4429
+ expectedMeta: offered,
4430
+ send: (msg) => ctx.client.api("POST", `${route}/signal`, { payload_enc: seal(msg), for: "sender", ...stamp }).then(() => void 0),
4431
+ onPhase: (phase2) => {
4432
+ if (phase2 === "flight") report2("flight");
4433
+ if (phase2 === "done" || phase2 === "failed") resolveOutcome(phase2);
4434
+ },
4435
+ onPath: (p) => {
4436
+ via = p;
4437
+ },
4438
+ onDiag: (d) => {
4439
+ diag = d;
4440
+ deps.onDiag?.(d);
4441
+ },
4442
+ sink: async (m) => {
4443
+ sink.open(m);
4444
+ return sink;
4445
+ }
4070
4446
  });
4071
- return { major, minor, patch };
4447
+ let over = false;
4448
+ const pump = (async () => {
4449
+ const seen = /* @__PURE__ */ new Set();
4450
+ while (!over) {
4451
+ let rows = [];
4452
+ try {
4453
+ rows = await ctx.client.firestoreRunQuery(offerPath, SIGNALS_FOR_RECEIVER);
4454
+ } catch {
4455
+ }
4456
+ for (const row of rows) {
4457
+ if (!row?.name || seen.has(row.name)) continue;
4458
+ seen.add(row.name);
4459
+ const enc = stringField2(row, "payload_enc");
4460
+ if (!enc) continue;
4461
+ try {
4462
+ handle.accept(open(enc));
4463
+ } catch {
4464
+ }
4465
+ }
4466
+ if (!over) await Promise.race([outcome, sleep2(deps.signalPollMs ?? SIGNAL_POLL_MS2)]);
4467
+ }
4468
+ })();
4469
+ const phase = await outcome;
4470
+ over = true;
4471
+ await pump;
4472
+ report2("finishing");
4473
+ if (phase === "done") {
4474
+ await setState("done");
4475
+ return {
4476
+ offer_id: offerId,
4477
+ channel_id: cid,
4478
+ channel_name: channelName,
4479
+ name: offered.name,
4480
+ bytes: sink.written,
4481
+ path: sink.target,
4482
+ ...via ? { via } : {},
4483
+ duration_ms: now() - startedAt
4484
+ };
4485
+ }
4486
+ sink.abort();
4487
+ const doc = await ctx.client.firestoreGet(offerPath).catch(() => null);
4488
+ const withdrawn = doc === null || stringField2(doc, "state") === "cancelled";
4489
+ await setState("failed");
4490
+ if (sink.failure) throw new ZasError("write_failed", 0, sink.failure.message);
4491
+ if (withdrawn) throw new ZasError("direct_cancelled", 0);
4492
+ const reason = diag?.reason || "unknown";
4493
+ deps.onFailed?.({
4494
+ channel_id: cid,
4495
+ channel_name: channelName,
4496
+ offer_id: offerId,
4497
+ owner_uid: owner,
4498
+ channel_account: account,
4499
+ device,
4500
+ ...input.dest !== void 0 ? { dest: input.dest } : {},
4501
+ meta: offered,
4502
+ key,
4503
+ key_version: grant.key_version,
4504
+ reason
4505
+ });
4506
+ throw new ZasError("direct_failed", 0, reason);
4072
4507
  }
4073
- function toolCallProperties(input) {
4074
- const tool = input.tool.startsWith("zas_") ? input.tool.slice(4) : input.tool;
4075
- if (!TOOL_NAMES.has(tool)) return null;
4076
- const version = versionParts(input.version);
4508
+ async function receiveDirectFallbackMeasured(ctx, record, report2, deps = {}) {
4509
+ const now = deps.now ?? (() => Date.now());
4510
+ const startedAt = now();
4511
+ const { open } = sealer(record.key, record.key_version);
4512
+ const stamp = { device: record.device, owner_uid: record.owner_uid };
4513
+ const base = `/direct/${record.channel_id}/${record.offer_id}/fallback`;
4514
+ const offerPath = `accounts/${record.channel_account}/channels/${record.channel_id}/direct/${record.offer_id}`;
4515
+ const doc = await ctx.client.firestoreGet(offerPath);
4516
+ const enc = stringField2(doc, "fallback_meta_enc");
4517
+ let meta;
4518
+ if (enc !== void 0) {
4519
+ try {
4520
+ meta = fallbackMetaOf(open(enc));
4521
+ } catch {
4522
+ meta = void 0;
4523
+ }
4524
+ }
4525
+ if (!meta) throw new ZasError("fallback_unavailable", 409);
4526
+ if (meta.name !== record.meta.name || meta.size !== record.meta.size || meta.mime !== record.meta.mime) {
4527
+ throw new ZasError("file_changed", 0);
4528
+ }
4529
+ const sink = new DiskSink(record.dest, record.offer_id);
4530
+ report2("downloading");
4531
+ try {
4532
+ sink.open(meta);
4533
+ await downloadFallback({
4534
+ offerId: record.offer_id,
4535
+ meta,
4536
+ sink,
4537
+ getUrl: async () => (await ctx.client.api("POST", `${base}/download`, stamp)).url,
4538
+ ...deps.fetcher ? { fetcher: deps.fetcher } : {}
4539
+ });
4540
+ } catch (error) {
4541
+ sink.abort();
4542
+ if (sink.failure) throw new ZasError("write_failed", 0, sink.failure.message);
4543
+ if (error instanceof ZasError) throw error;
4544
+ throw new ZasError("network", 0, error instanceof Error ? error.message : String(error));
4545
+ }
4546
+ report2("finishing");
4547
+ await ctx.client.api("POST", `${base}/received`, stamp);
4077
4548
  return {
4078
- tool,
4079
- result: input.code === null ? "ok" : "error",
4080
- // `other` is the server's word for a code outside the vocabulary. The
4081
- // shared list is the one that decides, and it drops anything else.
4082
- code: input.code ?? "none",
4083
- duration_bucket: agentDurationBucket(input.ms),
4084
- version_major: version.major,
4085
- version_minor: version.minor,
4086
- version_patch: version.patch
4549
+ offer_id: record.offer_id,
4550
+ channel_id: record.channel_id,
4551
+ channel_name: record.channel_name,
4552
+ name: meta.name,
4553
+ bytes: sink.written,
4554
+ path: sink.target,
4555
+ duration_ms: now() - startedAt
4087
4556
  };
4088
4557
  }
4089
- var REPORT_TIMEOUT_MS = 5e3;
4090
- async function report(client, properties) {
4091
- if (properties === null || !telemetryState().on) return;
4092
- const timeout = new Promise((resolve2) => {
4093
- const timer = setTimeout(resolve2, REPORT_TIMEOUT_MS);
4094
- timer.unref?.();
4095
- });
4096
- const sent = client.api("POST", "/agents/telemetry", { event: "agent.tool_call", properties }).then(() => void 0).catch(() => void 0);
4097
- await Promise.race([sent, timeout]);
4558
+ async function receiveDirect(...args) {
4559
+ const telemetry = new TransferTelemetry(args[0].client, "download", "file", "direct");
4560
+ try {
4561
+ const report2 = args[2];
4562
+ const result = await receiveDirectMeasured({ ...args[0], transferTelemetry: telemetry }, args[1], (phase) => {
4563
+ telemetry.mark(phase);
4564
+ report2?.(phase);
4565
+ }, args[3]);
4566
+ telemetry.describe(result.bytes);
4567
+ telemetry.finish("success", "none", false);
4568
+ return result;
4569
+ } catch (error) {
4570
+ telemetry.failed(error);
4571
+ throw error;
4572
+ }
4573
+ }
4574
+ async function receiveDirectFallback(...args) {
4575
+ const telemetry = new TransferTelemetry(args[0].client, "download", "file", "fallback");
4576
+ try {
4577
+ const report2 = args[2];
4578
+ const result = await receiveDirectFallbackMeasured({ ...args[0], transferTelemetry: telemetry }, args[1], (phase) => {
4579
+ telemetry.mark(phase);
4580
+ report2?.(phase);
4581
+ }, args[3]);
4582
+ telemetry.describe(result.bytes);
4583
+ telemetry.finish("success", "none", false);
4584
+ return result;
4585
+ } catch (error) {
4586
+ telemetry.failed(error);
4587
+ throw error;
4588
+ }
4098
4589
  }
4099
4590
 
4591
+ // src/jobs.ts
4592
+ import { randomUUID as randomUUID2 } from "node:crypto";
4593
+ var DEFAULT_WAIT_MS = 6e4;
4594
+ var HISTORY = 50;
4595
+ var JobRunner = class {
4596
+ now;
4597
+ waitMs;
4598
+ /** Newest first, and trimmed. */
4599
+ jobs = [];
4600
+ /** Keyed on the job object, not its id, so a caller holding a job that has
4601
+ * already aged out of the list can still wait for it to finish. */
4602
+ settled = /* @__PURE__ */ new WeakMap();
4603
+ constructor(opts = {}) {
4604
+ this.now = opts.now ?? (() => Date.now());
4605
+ this.waitMs = opts.waitMs ?? DEFAULT_WAIT_MS;
4606
+ }
4607
+ start(kind, title, channel, work) {
4608
+ const job = {
4609
+ id: randomUUID2(),
4610
+ kind,
4611
+ title,
4612
+ channel,
4613
+ started_at: this.now(),
4614
+ phase: null,
4615
+ status: "running"
4616
+ };
4617
+ this.jobs.unshift(job);
4618
+ this.jobs.length = Math.min(this.jobs.length, HISTORY);
4619
+ const report2 = (phase) => {
4620
+ if (job.status === "running") job.phase = phase;
4621
+ };
4622
+ const note = (diag) => {
4623
+ job.diag = diag;
4624
+ };
4625
+ this.settled.set(job, work(report2, note).then(
4626
+ (result) => {
4627
+ job.status = "done";
4628
+ job.result = result;
4629
+ return job;
4630
+ },
4631
+ (error) => {
4632
+ job.status = "failed";
4633
+ job.error = error instanceof ZasError ? {
4634
+ code: error.code,
4635
+ status: error.status,
4636
+ sentence: humanSentence(error),
4637
+ message: error.message,
4638
+ ...error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {},
4639
+ ...error.serverCode !== void 0 ? { serverCode: error.serverCode } : {}
4640
+ } : { code: "upload_failed", status: 0, sentence: humanSentence(new ZasError("upload_failed", 0)) };
4641
+ return job;
4642
+ }
4643
+ ));
4644
+ return job;
4645
+ }
4646
+ /** Resolves when the work settles, or when the wait runs out — the same job
4647
+ * object either way, so the caller reads `status` rather than guessing. */
4648
+ async wait(job) {
4649
+ const settled = this.settled.get(job);
4650
+ if (!settled) return job;
4651
+ let timer;
4652
+ const deadline = new Promise((resolve2) => {
4653
+ timer = setTimeout(() => resolve2(job), this.waitMs);
4654
+ });
4655
+ try {
4656
+ return await Promise.race([settled, deadline]);
4657
+ } finally {
4658
+ clearTimeout(timer);
4659
+ }
4660
+ }
4661
+ list() {
4662
+ return this.jobs.slice(0, HISTORY);
4663
+ }
4664
+ get(id) {
4665
+ return this.jobs.find((job) => job.id === id);
4666
+ }
4667
+ };
4668
+
4100
4669
  // src/server.ts
4101
4670
  function agentVersion() {
4102
- return true ? "0.8.0" : "0.0.0-dev";
4671
+ return true ? "0.10.0" : "0.0.0-dev";
4103
4672
  }
4104
4673
  var AGENT_CUE = " The owner sees every item this agent sends with the >_ agent mark and this agent's name, on every device.";
4105
4674
  var PAIR_ANNOUNCE_MS = 15e3;
@@ -4135,10 +4704,12 @@ function buildServer(profile, deps = {}) {
4135
4704
  const started = Date.now();
4136
4705
  try {
4137
4706
  const answer = await handler(...args);
4138
- void reportCall(name, Date.now() - started, answeredCode(answer));
4707
+ const elapsed = Date.now() - started;
4708
+ setImmediate(() => void reportCall(name, elapsed, answeredCode(answer))).unref();
4139
4709
  return answer;
4140
4710
  } catch (e) {
4141
- void reportCall(name, Date.now() - started, e instanceof ZasError ? e.code : "internal");
4711
+ const elapsed = Date.now() - started;
4712
+ setImmediate(() => void reportCall(name, elapsed, e instanceof ZasError ? e.code : "internal")).unref();
4142
4713
  throw e;
4143
4714
  }
4144
4715
  });
@@ -4456,6 +5027,45 @@ ${formatDirectDiagLines(job.diag).join("\n")}`) };
4456
5027
  return failed(e);
4457
5028
  }
4458
5029
  });
5030
+ server.registerTool("zas_edit_item", {
5031
+ description: "Change an item this agent sent, keeping its id: the title of a file or a note, or a note's text, language and secret cover. Refuses items sent by anyone else. A file's bytes change with zas_replace_file. Needs a grant that includes reading and sending. Pass only the fields to change; an empty title clears it." + AGENT_CUE,
5032
+ inputSchema: {
5033
+ id: z.string().describe("Item id, as `zas_list_items` reports it."),
5034
+ channel: z.string().optional().describe("Channel name or id. Optional when the agent holds exactly one channel."),
5035
+ title: z.string().optional().describe("New label. An empty string clears it, so the file name or the first line shows again."),
5036
+ text: z.string().optional().describe("New body, for a note."),
5037
+ lang: z.string().optional().describe('Language of the snippet, for highlighting (for example "ts", "py"). An empty string makes it plain text.'),
5038
+ secret: z.boolean().optional().describe("Hide the body behind a cover until the reader opens it. False removes the cover.")
5039
+ }
5040
+ }, async (input) => {
5041
+ try {
5042
+ return text2(await editItem(ctx(), input));
5043
+ } catch (e) {
5044
+ return failed(e);
5045
+ }
5046
+ });
5047
+ server.registerTool("zas_replace_file", {
5048
+ description: "Replace the bytes of a file this agent sent with a file from this machine, keeping the item id, its place in the channel and its pin. Refuses items sent by anyone else, notes, and an item with a public share. Returns the item id, or a job id when the upload takes longer than a minute. Sends any file this process can read; confirm with the owner before sending secrets, keys or credentials." + AGENT_CUE,
5049
+ inputSchema: {
5050
+ id: z.string().describe("Item id, as `zas_list_items` reports it."),
5051
+ path: z.string().describe("Absolute or relative path of the new file."),
5052
+ channel: z.string().optional().describe("Channel name or id. Optional when the agent holds exactly one channel."),
5053
+ title: z.string().optional().describe("New label. Defaults to the label the item has.")
5054
+ }
5055
+ }, async (input) => {
5056
+ try {
5057
+ const c = ctx();
5058
+ const job = runner.start(
5059
+ "replace",
5060
+ input.title ?? input.path,
5061
+ input.channel ?? "",
5062
+ (report2) => replaceFile(c, input, report2)
5063
+ );
5064
+ return settled(await runner.wait(job));
5065
+ } catch (e) {
5066
+ return failed(e);
5067
+ }
5068
+ });
4459
5069
  server.registerTool("zas_jobs", {
4460
5070
  description: "List the sends and Directo transfers this server started, newest first, with the phase each one reached and how it ended \u2014 including any `job_id` a send returned; a finished job keeps its result here." + AGENT_CUE
4461
5071
  }, async () => text2(runner.list()));
@@ -4509,6 +5119,15 @@ var USAGE = [
4509
5119
  " zas-agent telemetry [on|off] say what this machine reports, or change it",
4510
5120
  " zas-agent --version"
4511
5121
  ].join("\n");
5122
+ function noticeOnServe(log) {
5123
+ try {
5124
+ if (noticeShown()) return;
5125
+ log(TELEMETRY_NOTICE);
5126
+ markNoticeShown();
5127
+ } catch (e) {
5128
+ log(`Could not record that the notice was shown (${String(e?.message ?? e)}). Serving anyway.`);
5129
+ }
5130
+ }
4512
5131
  async function main(argv, log = (l) => console.error(l)) {
4513
5132
  const args = parseArgs(argv);
4514
5133
  if (args.command === "version") {
@@ -4563,10 +5182,7 @@ async function main(argv, log = (l) => console.error(l)) {
4563
5182
  }
4564
5183
  return 0;
4565
5184
  }
4566
- if (!noticeShown()) {
4567
- log(TELEMETRY_NOTICE);
4568
- markNoticeShown();
4569
- }
5185
+ noticeOnServe(log);
4570
5186
  await buildServer(args.profile).connect(new StdioServerTransport());
4571
5187
  return 0;
4572
5188
  }
@@ -4603,5 +5219,6 @@ if (isInvokedDirectly(process.argv[1], import.meta.url)) {
4603
5219
  export {
4604
5220
  isInvokedDirectly,
4605
5221
  main,
5222
+ noticeOnServe,
4606
5223
  parseArgs
4607
5224
  };