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.
- package/CHANGELOG.md +59 -1
- package/README.md +44 -7
- package/dist/cli.js +1620 -1003
- 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,
|
|
286
|
-
writePrivate(profileDir(profile), PENDING_FILE,
|
|
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
|
|
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,
|
|
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/
|
|
1710
|
-
import {
|
|
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/
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
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
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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/
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
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
|
|
2928
|
+
let pending2 = [];
|
|
2155
2929
|
let pendingLen = 0;
|
|
2156
2930
|
const compact = () => {
|
|
2157
|
-
if (
|
|
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
|
|
2934
|
+
for (const p of pending2) {
|
|
2161
2935
|
merged.set(p, off);
|
|
2162
2936
|
off += p.length;
|
|
2163
2937
|
}
|
|
2164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2570
|
-
const stat = await fsp2.stat(
|
|
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
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
const file = await fsp2.readFile(
|
|
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
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
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
|
|
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
|
-
|
|
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/
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
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
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
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
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
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
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
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
|
-
|
|
3346
|
-
|
|
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
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
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
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
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
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
//
|
|
3423
|
-
//
|
|
3424
|
-
|
|
3425
|
-
|
|
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
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
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
|
|
4019
|
-
|
|
4020
|
-
|
|
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
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
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
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
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
|
|
4062
|
-
const
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4069
|
-
|
|
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
|
-
|
|
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
|
|
4074
|
-
const
|
|
4075
|
-
|
|
4076
|
-
const
|
|
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
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
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
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
const
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
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.
|
|
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
|
-
|
|
4707
|
+
const elapsed = Date.now() - started;
|
|
4708
|
+
setImmediate(() => void reportCall(name, elapsed, answeredCode(answer))).unref();
|
|
4139
4709
|
return answer;
|
|
4140
4710
|
} catch (e) {
|
|
4141
|
-
|
|
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
|
-
|
|
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
|
};
|