@warmhub/sdk-ts 0.45.0 → 0.46.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.
@@ -51,6 +51,115 @@ function splitLocalPath(name) {
51
51
  return { shapePrefix: name.slice(0, idx), bareName: name.slice(idx + 1) };
52
52
  }
53
53
 
54
+ // ../rules/src/component-cli-signing.ts
55
+ var CLI_INSTALL_REPO_HEADER = "X-WarmHub-Install-Repo";
56
+ var CLI_SIGNATURE_HEADER = "X-WarmHub-Signature";
57
+ var CLI_TIMESTAMP_HEADER = "X-WarmHub-Timestamp";
58
+ var RFC3986_UNRESERVED = /^[A-Za-z0-9\-._~]$/;
59
+ function pctEncode(value) {
60
+ let out = "";
61
+ const bytes = new TextEncoder().encode(value);
62
+ for (const byte of bytes) {
63
+ const ch = String.fromCharCode(byte);
64
+ if (RFC3986_UNRESERVED.test(ch)) {
65
+ out += ch;
66
+ } else {
67
+ out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+ function serializeQueryValue(value) {
73
+ if (typeof value === "boolean") return value ? "true" : "false";
74
+ if (typeof value === "number") return JSON.stringify(value);
75
+ return value;
76
+ }
77
+ function cliMethodHasBody(method) {
78
+ return method === "POST" || method === "PUT" || method === "PATCH";
79
+ }
80
+ function canonicalCliQueryString(args) {
81
+ const entries = [];
82
+ for (const [k, v] of Object.entries(args)) {
83
+ if (v === void 0 || v === null) continue;
84
+ entries.push([k, serializeQueryValue(v)]);
85
+ }
86
+ entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
87
+ return entries.map(([k, v]) => `${pctEncode(k)}=${pctEncode(v)}`).join("&");
88
+ }
89
+ function bytesToHex(bytes) {
90
+ let out = "";
91
+ for (const byte of bytes) {
92
+ out += byte.toString(16).padStart(2, "0");
93
+ }
94
+ return out;
95
+ }
96
+ async function sha256Hex(text) {
97
+ const buf = await crypto.subtle.digest(
98
+ "SHA-256",
99
+ new TextEncoder().encode(text)
100
+ );
101
+ return bytesToHex(new Uint8Array(buf));
102
+ }
103
+ var EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
104
+ function buildCliSigningInput(input) {
105
+ return [
106
+ input.method,
107
+ input.path,
108
+ input.canonicalQuery,
109
+ input.installRepo,
110
+ input.bodyHash,
111
+ String(input.timestamp)
112
+ ].join("\n");
113
+ }
114
+ async function hmacSha256Hex(secret, message) {
115
+ const encoder = new TextEncoder();
116
+ const key = await crypto.subtle.importKey(
117
+ "raw",
118
+ encoder.encode(secret),
119
+ { name: "HMAC", hash: "SHA-256" },
120
+ false,
121
+ ["sign"]
122
+ );
123
+ const signature = await crypto.subtle.sign(
124
+ "HMAC",
125
+ key,
126
+ encoder.encode(message)
127
+ );
128
+ return bytesToHex(new Uint8Array(signature));
129
+ }
130
+ var SIGNATURE_RE = /^sha256=([0-9a-f]{64})$/;
131
+ var DEFAULT_TOLERANCE_SEC = 300;
132
+ async function verifyCliRequest(input) {
133
+ const match = SIGNATURE_RE.exec(input.signature);
134
+ if (!match) return { ok: false, reason: "invalid-format" };
135
+ const providedHex = match[1];
136
+ const tolerance = input.toleranceSec ?? DEFAULT_TOLERANCE_SEC;
137
+ const skew = Math.abs(input.nowUnixSeconds - input.timestamp);
138
+ if (skew > tolerance) return { ok: false, reason: "expired" };
139
+ const bodyHash = input.body.length === 0 ? EMPTY_BODY_SHA256 : await sha256Hex(input.body);
140
+ const canonical = buildCliSigningInput({
141
+ method: input.method,
142
+ path: input.path,
143
+ canonicalQuery: canonicalCliQueryString(input.query),
144
+ installRepo: input.installRepo,
145
+ bodyHash,
146
+ timestamp: input.timestamp
147
+ });
148
+ const expectedHex = await hmacSha256Hex(input.secret, canonical);
149
+ if (!constantTimeEqualHex(providedHex, expectedHex)) {
150
+ return { ok: false, reason: "invalid-signature" };
151
+ }
152
+ return { ok: true };
153
+ }
154
+ function constantTimeEqualHex(a, b) {
155
+ if (a.length !== b.length) return false;
156
+ let diff = 0;
157
+ for (let i = 0; i < a.length; i++) {
158
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
159
+ }
160
+ return diff === 0;
161
+ }
162
+
54
163
  // ../rules/src/repo-auth-scopes.ts
55
164
  var REPO_AUTH_SCOPES = [
56
165
  "repo:read",
@@ -1442,6 +1551,23 @@ function inferKind(name, operation) {
1442
1551
  // src/stream-submit.ts
1443
1552
  var DEFAULT_STREAM_CHUNK_SIZE = DEFAULT_STREAM_APPEND_CHUNK_SIZE;
1444
1553
  var MAX_STREAM_APPEND_OPERATION_COUNT2 = MAX_STREAM_APPEND_OPERATION_COUNT;
1554
+ function streamAppendResultStatus(result) {
1555
+ if (result.status === "failed") {
1556
+ return "error";
1557
+ }
1558
+ return result.status === "noop" || result.operation === "noop" ? "noop" : "applied";
1559
+ }
1560
+ function countStreamAppendResultStatuses(results) {
1561
+ const statusCounts = {
1562
+ applied: 0,
1563
+ noop: 0,
1564
+ error: 0
1565
+ };
1566
+ for (const result of results) {
1567
+ statusCounts[streamAppendResultStatus(result)]++;
1568
+ }
1569
+ return statusCounts;
1570
+ }
1445
1571
  var DEFAULT_RETRY_POLICY = {
1446
1572
  maxAttempts: 3,
1447
1573
  baseDelayMs: 250,
@@ -1509,12 +1635,41 @@ var PartialStreamSubmissionError = class extends Error {
1509
1635
  this.completedOperations = input.completedOperations;
1510
1636
  }
1511
1637
  };
1638
+ var AllStreamOperationsFailedError = class extends Error {
1639
+ code = "STREAM_ALL_OPERATIONS_FAILED";
1640
+ result;
1641
+ operations;
1642
+ statusCounts;
1643
+ cause;
1644
+ constructor(result) {
1645
+ const primaryFailure = result.operations.find(
1646
+ (operation) => operation.error !== void 0
1647
+ )?.error;
1648
+ super(
1649
+ primaryFailure ? `All ${result.operationCount} stream operations failed: ${primaryFailure.message}` : `All ${result.operationCount} stream operations failed.`
1650
+ );
1651
+ this.name = "WarmHubError";
1652
+ this.cause = primaryFailure;
1653
+ this.result = result;
1654
+ this.operations = result.operations;
1655
+ this.statusCounts = result.statusCounts ?? {
1656
+ applied: 0,
1657
+ noop: 0,
1658
+ error: result.operationCount
1659
+ };
1660
+ }
1661
+ };
1512
1662
  async function submitOperationsViaStream(client, args) {
1513
1663
  if (args.operations.length === 0) {
1514
1664
  throw new StreamValidationError(
1515
1665
  "At least one operation is required for stream submission."
1516
1666
  );
1517
1667
  }
1668
+ if ((args.allocatedTokens?.length ?? 0) > 0 && args.streamId === void 0) {
1669
+ throw new StreamValidationError(
1670
+ "Manual stream resume with allocatedTokens requires the original streamId."
1671
+ );
1672
+ }
1518
1673
  const operations = args.operations.map((operation, index) => {
1519
1674
  let streamOperation;
1520
1675
  try {
@@ -1535,8 +1690,13 @@ async function submitOperationsViaStream(client, args) {
1535
1690
  const isManualResume = args.streamId !== void 0 || (args.allocatedTokens?.length ?? 0) > 0;
1536
1691
  const policy = isManualResume ? false : resolveRetryPolicy(args.retry);
1537
1692
  let allocatedTokens = args.allocatedTokens ?? [];
1693
+ let createdByEmail;
1538
1694
  const chunkResults = [];
1539
- for (const chunk of chunkOperations(operations, chunkSize)) {
1695
+ let sawAmbiguousAttempt = false;
1696
+ for (const { operations: chunk, start: chunkStart } of chunkOperations(
1697
+ operations,
1698
+ chunkSize
1699
+ )) {
1540
1700
  let attempt = 1;
1541
1701
  let priorAttemptAmbiguous = false;
1542
1702
  const chunkIsAtomic = chunk.length === 1;
@@ -1550,10 +1710,16 @@ async function submitOperationsViaStream(client, args) {
1550
1710
  streamId,
1551
1711
  componentId: args.componentId,
1552
1712
  committer: args.committer,
1713
+ message: args.message,
1553
1714
  operations: chunk
1554
1715
  });
1555
1716
  allocatedTokens = appendResult.allocatedTokens;
1556
- chunkResults.push(...appendResult.results);
1717
+ if (appendResult.createdByEmail !== void 0) {
1718
+ createdByEmail = appendResult.createdByEmail;
1719
+ }
1720
+ chunkResults.push(
1721
+ ...offsetChunkResultIndexes(appendResult.results, chunkStart)
1722
+ );
1557
1723
  break;
1558
1724
  } catch (cause) {
1559
1725
  if (chunkResults.length === 0 && !priorAttemptAmbiguous && isDefiniteClientError(cause)) {
@@ -1563,18 +1729,23 @@ async function submitOperationsViaStream(client, args) {
1563
1729
  await sleep(computeBackoffDelayMs(attempt, policy));
1564
1730
  attempt += 1;
1565
1731
  priorAttemptAmbiguous = true;
1732
+ sawAmbiguousAttempt = true;
1566
1733
  streamId = createStreamId();
1567
1734
  allocatedTokens = [];
1568
1735
  continue;
1569
1736
  }
1570
- const completedOperations = toSubmittedStreamResult(
1571
- {
1572
- operationCount: chunkResults.length
1573
- },
1574
- args.committer,
1575
- args.message,
1576
- chunkResults
1577
- ).operations;
1737
+ const completedOperations = completedOperationsFrom(
1738
+ toSubmittedStreamResult(
1739
+ {
1740
+ operationCount: chunkResults.length
1741
+ },
1742
+ args.committer,
1743
+ createdByEmail,
1744
+ args.message,
1745
+ chunkResults,
1746
+ operations
1747
+ )
1748
+ );
1578
1749
  throw new PartialStreamSubmissionError({
1579
1750
  cause,
1580
1751
  completedOperations
@@ -1582,20 +1753,54 @@ async function submitOperationsViaStream(client, args) {
1582
1753
  }
1583
1754
  }
1584
1755
  }
1585
- return toSubmittedStreamResult(
1756
+ const result = toSubmittedStreamResult(
1586
1757
  {
1587
1758
  operationCount: chunkResults.length
1588
1759
  },
1589
1760
  args.committer,
1761
+ createdByEmail,
1590
1762
  args.message,
1591
- chunkResults
1763
+ chunkResults,
1764
+ operations
1592
1765
  );
1766
+ if (isAllSubmittedOperationsFailed(result, operations.length)) {
1767
+ const failure = new AllStreamOperationsFailedError(result);
1768
+ if (sawAmbiguousAttempt) {
1769
+ throw new PartialStreamSubmissionError({
1770
+ cause: failure,
1771
+ completedOperations: completedOperationsFrom(result)
1772
+ });
1773
+ }
1774
+ throw failure;
1775
+ }
1776
+ return result;
1777
+ }
1778
+ function isAllSubmittedOperationsFailed(result, submittedOperationCount) {
1779
+ if (submittedOperationCount <= 0) {
1780
+ return false;
1781
+ }
1782
+ const inputRowsFailed = /* @__PURE__ */ new Map();
1783
+ for (const operation of result.operations) {
1784
+ if (operation.opIndex === void 0 || operation.opIndex < 0 || operation.opIndex >= submittedOperationCount) {
1785
+ continue;
1786
+ }
1787
+ inputRowsFailed.set(
1788
+ operation.opIndex,
1789
+ (inputRowsFailed.get(operation.opIndex) ?? true) && operation.status === "error"
1790
+ );
1791
+ }
1792
+ return inputRowsFailed.size === submittedOperationCount && [...inputRowsFailed.values()].every(Boolean);
1793
+ }
1794
+ function completedOperationsFrom(result) {
1795
+ return result.operations.filter((operation) => operation.status !== "error");
1593
1796
  }
1594
1797
  var DEFINITE_CLIENT_REJECT_CODES = /* @__PURE__ */ new Set([
1595
1798
  "UNAUTHENTICATED",
1596
1799
  "FORBIDDEN",
1597
1800
  "VALIDATION_ERROR",
1598
1801
  "SHAPE_MISMATCH",
1802
+ "RESERVED_NAME",
1803
+ "ILLEGAL_OP_SEQUENCE",
1599
1804
  "NOT_FOUND",
1600
1805
  "KIND_MISMATCH",
1601
1806
  "CONFLICT",
@@ -1625,13 +1830,18 @@ function isDefiniteClientError(cause) {
1625
1830
  }
1626
1831
  function isTransientStreamFailure(cause) {
1627
1832
  if (isDefiniteClientError(cause)) return false;
1833
+ if (isFetchNetworkTypeError(cause)) return true;
1628
1834
  if (cause instanceof TypeError) return false;
1629
1835
  if (cause instanceof SyntaxError) return false;
1630
1836
  const inner = cause?.cause;
1837
+ if (isFetchNetworkTypeError(inner)) return true;
1631
1838
  if (inner instanceof TypeError) return false;
1632
1839
  if (inner instanceof SyntaxError) return false;
1633
1840
  return true;
1634
1841
  }
1842
+ function isFetchNetworkTypeError(cause) {
1843
+ return cause instanceof TypeError && /fetch/i.test(cause.message);
1844
+ }
1635
1845
  function computeBackoffDelayMs(attempt, policy) {
1636
1846
  const exponential = policy.baseDelayMs * 2 ** Math.max(0, attempt - 1);
1637
1847
  const jitter = Math.random() * policy.baseDelayMs;
@@ -1644,12 +1854,22 @@ var TOKEN_REF_REGEX = /[$#]\d+/;
1644
1854
  function stringHasTokenRef(value) {
1645
1855
  return typeof value === "string" && TOKEN_REF_REGEX.test(value);
1646
1856
  }
1647
- function membersHaveTokenRef(members) {
1648
- if (!Array.isArray(members)) return false;
1649
- return members.some(stringHasTokenRef);
1857
+ function structuralValueHasTokenRef(value, seen = /* @__PURE__ */ new WeakSet()) {
1858
+ if (stringHasTokenRef(value)) return true;
1859
+ if (value !== null && typeof value === "object") {
1860
+ if (seen.has(value)) return false;
1861
+ seen.add(value);
1862
+ if (Array.isArray(value)) {
1863
+ return value.some((entry) => structuralValueHasTokenRef(entry, seen));
1864
+ }
1865
+ return Object.values(value).some(
1866
+ (entry) => structuralValueHasTokenRef(entry, seen)
1867
+ );
1868
+ }
1869
+ return false;
1650
1870
  }
1651
1871
  function opUsesTokens(op) {
1652
- return stringHasTokenRef(op.name) || stringHasTokenRef(op.wref) || stringHasTokenRef(op.about) || stringHasTokenRef(op.aboutWref) || membersHaveTokenRef(op.members);
1872
+ return stringHasTokenRef(op.name) || stringHasTokenRef(op.wref) || structuralValueHasTokenRef(op.about) || stringHasTokenRef(op.aboutWref) || structuralValueHasTokenRef(op.members) || structuralValueHasTokenRef(op.data);
1653
1873
  }
1654
1874
  function createStreamId() {
1655
1875
  return globalThis.crypto?.randomUUID?.() ?? `stream-${Date.now()}-${Math.random()}`;
@@ -1667,27 +1887,213 @@ function normalizeChunkSize(chunkSize) {
1667
1887
  function chunkOperations(operations, chunkSize) {
1668
1888
  const chunks = [];
1669
1889
  for (let start = 0; start < operations.length; start += chunkSize) {
1670
- chunks.push(operations.slice(start, start + chunkSize));
1890
+ chunks.push({
1891
+ operations: operations.slice(start, start + chunkSize),
1892
+ start
1893
+ });
1671
1894
  }
1672
1895
  return chunks;
1673
1896
  }
1674
- function toSubmittedStreamResult(writeResult, committer, message, results) {
1897
+ function offsetChunkResultIndexes(results, chunkStart) {
1898
+ return results.map((result, index) => ({
1899
+ ...result,
1900
+ opIndex: chunkStart + (result.opIndex ?? index)
1901
+ }));
1902
+ }
1903
+ function toSubmittedStreamResult(writeResult, committer, createdByEmail, message, results, operations) {
1904
+ const statusCounts = countStreamAppendResultStatuses(results);
1905
+ const partial = statusCounts.error > 0;
1675
1906
  return {
1676
1907
  committer,
1908
+ ...createdByEmail !== void 0 ? { createdByEmail } : {},
1677
1909
  message,
1678
1910
  operationCount: writeResult.operationCount,
1911
+ ...partial ? { partial, statusCounts } : {},
1679
1912
  operations: results.map((result) => ({
1913
+ ...result.opIndex !== void 0 ? { opIndex: result.opIndex } : {},
1680
1914
  name: result.name ?? "",
1681
1915
  operation: result.operation === "revise" || result.operation === "retract" || result.operation === "noop" ? result.operation : "add",
1682
1916
  dataHash: result.dataHash ?? "",
1683
1917
  version: result.version ?? 0,
1684
- status: result.status,
1918
+ status: streamAppendResultStatus(result),
1685
1919
  error: result.error,
1920
+ ...result.status === "failed" && result.opIndex !== void 0 && operations[result.opIndex]?.name !== void 0 ? { submittedName: operations[result.opIndex]?.name } : {},
1921
+ ...result.resolvedName !== void 0 ? { resolvedName: result.resolvedName } : {},
1922
+ ...result.retryable !== void 0 ? { retryable: result.retryable } : {},
1686
1923
  ...result.warnings ? { warnings: result.warnings } : {}
1687
1924
  }))
1688
1925
  };
1689
1926
  }
1690
1927
 
1928
+ // src/component-cli.ts
1929
+ var SUPPORTED_METHODS = /* @__PURE__ */ new Set([
1930
+ "GET",
1931
+ "POST",
1932
+ "PUT",
1933
+ "PATCH",
1934
+ "DELETE"
1935
+ ]);
1936
+ var CliCallVerificationError = class extends Error {
1937
+ reason;
1938
+ constructor(reason, message) {
1939
+ super(message);
1940
+ this.name = "CliCallVerificationError";
1941
+ this.reason = reason;
1942
+ }
1943
+ };
1944
+ async function verifyCliCall(request, secrets, opts) {
1945
+ const method = request.method.toUpperCase();
1946
+ if (!SUPPORTED_METHODS.has(method)) {
1947
+ throw new CliCallVerificationError(
1948
+ "unsupported-method",
1949
+ `Unsupported HTTP method "${request.method}" \u2014 component CLI dispatch only emits GET, POST, PUT, or PATCH`
1950
+ );
1951
+ }
1952
+ const cliMethod = method;
1953
+ const installRepo = request.headers.get(CLI_INSTALL_REPO_HEADER);
1954
+ if (!installRepo) {
1955
+ throw new CliCallVerificationError(
1956
+ "missing-install-repo",
1957
+ `Missing ${CLI_INSTALL_REPO_HEADER} header`
1958
+ );
1959
+ }
1960
+ const hasSigning = isNonEmpty(secrets.CLI_SIGNING_SECRET);
1961
+ const hasBearer = isNonEmpty(secrets.CLI_BEARER_TOKEN);
1962
+ const hasApiKey = isNonEmpty(secrets.CLI_API_KEY);
1963
+ const hasBasic = isNonEmpty(secrets.CLI_BASIC_USERNAME) && isNonEmpty(secrets.CLI_BASIC_PASSWORD);
1964
+ if (!hasSigning && !hasBearer && !hasApiKey && !hasBasic) {
1965
+ throw new CliCallVerificationError(
1966
+ "no-scheme-configured",
1967
+ "no-scheme-configured: need at least one of CLI_SIGNING_SECRET, CLI_BEARER_TOKEN, CLI_API_KEY, or CLI_BASIC_USERNAME + CLI_BASIC_PASSWORD"
1968
+ );
1969
+ }
1970
+ const url = new URL(request.url);
1971
+ const path = url.pathname;
1972
+ let query;
1973
+ let body;
1974
+ let args;
1975
+ if (!cliMethodHasBody(cliMethod)) {
1976
+ query = Object.fromEntries(url.searchParams.entries());
1977
+ body = "";
1978
+ args = { ...query };
1979
+ } else {
1980
+ query = {};
1981
+ body = await request.text();
1982
+ let parsed;
1983
+ try {
1984
+ parsed = JSON.parse(body);
1985
+ } catch {
1986
+ throw new CliCallVerificationError(
1987
+ "invalid-body",
1988
+ "POST body is not valid JSON"
1989
+ );
1990
+ }
1991
+ if (!isArgsRecord(parsed)) {
1992
+ throw new CliCallVerificationError(
1993
+ "invalid-body",
1994
+ "POST body must be a JSON object of primitive args (string / number / boolean)"
1995
+ );
1996
+ }
1997
+ args = parsed;
1998
+ }
1999
+ if (hasSigning) {
2000
+ const signature = request.headers.get(CLI_SIGNATURE_HEADER);
2001
+ if (!signature) {
2002
+ throw new CliCallVerificationError(
2003
+ "missing-signature",
2004
+ `Missing ${CLI_SIGNATURE_HEADER} header`
2005
+ );
2006
+ }
2007
+ const timestampHeader = request.headers.get(CLI_TIMESTAMP_HEADER);
2008
+ if (!timestampHeader) {
2009
+ throw new CliCallVerificationError(
2010
+ "missing-timestamp",
2011
+ `Missing ${CLI_TIMESTAMP_HEADER} header`
2012
+ );
2013
+ }
2014
+ const timestamp = Number(timestampHeader);
2015
+ if (!Number.isFinite(timestamp) || !Number.isInteger(timestamp)) {
2016
+ throw new CliCallVerificationError(
2017
+ "invalid-timestamp",
2018
+ `${CLI_TIMESTAMP_HEADER} must be an integer unix seconds value; got "${timestampHeader}"`
2019
+ );
2020
+ }
2021
+ const verification = await verifyCliRequest({
2022
+ secret: secrets.CLI_SIGNING_SECRET,
2023
+ method: cliMethod,
2024
+ path,
2025
+ query,
2026
+ installRepo,
2027
+ body,
2028
+ signature,
2029
+ timestamp,
2030
+ nowUnixSeconds: opts?.nowUnixSeconds ?? Math.floor(Date.now() / 1e3),
2031
+ toleranceSec: opts?.toleranceSec
2032
+ });
2033
+ if (!verification.ok) {
2034
+ throw new CliCallVerificationError(
2035
+ verification.reason,
2036
+ `HMAC verification failed: ${verification.reason}`
2037
+ );
2038
+ }
2039
+ }
2040
+ if (hasBearer) {
2041
+ const expected = `Bearer ${secrets.CLI_BEARER_TOKEN}`;
2042
+ const provided = request.headers.get("authorization") ?? "";
2043
+ if (!constantTimeEqual(provided, expected)) {
2044
+ throw new CliCallVerificationError(
2045
+ "invalid-bearer",
2046
+ "invalid-bearer: bearer token mismatch"
2047
+ );
2048
+ }
2049
+ }
2050
+ if (hasApiKey) {
2051
+ const headerName = isNonEmpty(secrets.CLI_API_KEY_HEADER) ? secrets.CLI_API_KEY_HEADER : "X-API-Key";
2052
+ const provided = request.headers.get(headerName) ?? "";
2053
+ if (!constantTimeEqual(provided, secrets.CLI_API_KEY)) {
2054
+ throw new CliCallVerificationError(
2055
+ "invalid-api-key",
2056
+ `invalid-api-key: mismatch on header "${headerName}"`
2057
+ );
2058
+ }
2059
+ }
2060
+ if (hasBasic) {
2061
+ const expected = `Basic ${btoa(`${secrets.CLI_BASIC_USERNAME}:${secrets.CLI_BASIC_PASSWORD}`)}`;
2062
+ const provided = request.headers.get("authorization") ?? "";
2063
+ if (!constantTimeEqual(provided, expected)) {
2064
+ throw new CliCallVerificationError(
2065
+ "invalid-basic",
2066
+ "invalid-basic: basic auth mismatch"
2067
+ );
2068
+ }
2069
+ }
2070
+ return { method: cliMethod, installRepo, args };
2071
+ }
2072
+ function isNonEmpty(value) {
2073
+ return typeof value === "string" && value.length > 0;
2074
+ }
2075
+ function isArgsRecord(value) {
2076
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2077
+ return false;
2078
+ }
2079
+ for (const v of Object.values(value)) {
2080
+ if (v === void 0) continue;
2081
+ if (typeof v === "string") continue;
2082
+ if (typeof v === "number") continue;
2083
+ if (typeof v === "boolean") continue;
2084
+ return false;
2085
+ }
2086
+ return true;
2087
+ }
2088
+ function constantTimeEqual(a, b) {
2089
+ if (a.length !== b.length) return false;
2090
+ let diff = 0;
2091
+ for (let i = 0; i < a.length; i++) {
2092
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
2093
+ }
2094
+ return diff === 0;
2095
+ }
2096
+
1691
2097
  // src/operation-builder.ts
1692
2098
  var OperationBuilder = class {
1693
2099
  ops = [];
@@ -1952,7 +2358,7 @@ var OperationBuilder = class {
1952
2358
  /**
1953
2359
  * Validate, submit, and seal the builder.
1954
2360
  *
1955
- * A successful call submits operations through the same stream path as `client.commit.apply` and makes the builder single-use. Validation failures throw before any server request is made. Ambiguous multi-chunk failures surface as `PartialStreamSubmissionError`.
2361
+ * A successful call submits operations through the same stream path as `client.commit.apply` and makes the builder single-use. Validation failures throw before any server request is made. Ambiguous multi-chunk failures surface as `PartialStreamSubmissionError`; deterministic all-failed batches surface as `AllStreamOperationsFailedError` with per-op failure data.
1956
2362
  *
1957
2363
  * @param params.client WarmHub client or compatible stream client used for submission.
1958
2364
  * @param params.message Optional commit message.
@@ -2183,7 +2589,7 @@ function preflightCheckRevise(kind, name, data) {
2183
2589
  function normalizeWref(wref) {
2184
2590
  return wref.replace(/@(?:v\d+|HEAD|ALL)$/i, "");
2185
2591
  }
2186
- var SDK_VERSION = "0.45.0" ;
2592
+ var SDK_VERSION = "0.46.0" ;
2187
2593
  var DEFAULT_API_URL = "https://api.warmhub.ai";
2188
2594
  var UNBATCHED_TRPC_PATHS = /* @__PURE__ */ new Set([
2189
2595
  "repo.shapeInstanceCounts",
@@ -2405,6 +2811,57 @@ function isConnectionError(error) {
2405
2811
  function connectionErrorMessage(url) {
2406
2812
  return `Unable to connect to ${url}. Is the computer able to access the url?`;
2407
2813
  }
2814
+ function parseSemver(value) {
2815
+ const match = value.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
2816
+ if (!match) return void 0;
2817
+ let prerelease = [];
2818
+ if (match[4]) {
2819
+ prerelease = match[4].split(".");
2820
+ for (const id of prerelease) {
2821
+ if (id.length === 0) return void 0;
2822
+ if (/^\d+$/.test(id) && id.length > 1 && id.startsWith("0")) {
2823
+ return void 0;
2824
+ }
2825
+ }
2826
+ }
2827
+ return {
2828
+ major: Number(match[1]),
2829
+ minor: Number(match[2]),
2830
+ patch: Number(match[3]),
2831
+ prerelease
2832
+ };
2833
+ }
2834
+ function comparePrereleaseIdentifiers(a, b) {
2835
+ const aNum = /^\d+$/.test(a);
2836
+ const bNum = /^\d+$/.test(b);
2837
+ if (aNum && bNum) return Number(a) - Number(b);
2838
+ if (aNum && !bNum) return -1;
2839
+ if (!aNum && bNum) return 1;
2840
+ return a < b ? -1 : a > b ? 1 : 0;
2841
+ }
2842
+ function comparePrerelease(a, b) {
2843
+ if (a.length === 0 && b.length > 0) return 1;
2844
+ if (a.length > 0 && b.length === 0) return -1;
2845
+ const max = Math.max(a.length, b.length);
2846
+ for (let i = 0; i < max; i += 1) {
2847
+ const aPart = a[i];
2848
+ const bPart = b[i];
2849
+ if (aPart === void 0) return -1;
2850
+ if (bPart === void 0) return 1;
2851
+ const cmp = comparePrereleaseIdentifiers(aPart, bPart);
2852
+ if (cmp !== 0) return cmp;
2853
+ }
2854
+ return 0;
2855
+ }
2856
+ function sdkVersionIsBelowMinimum(version, minimum) {
2857
+ const v = parseSemver(version);
2858
+ const m = parseSemver(minimum);
2859
+ if (!v || !m) return false;
2860
+ if (v.major !== m.major) return v.major < m.major;
2861
+ if (v.minor !== m.minor) return v.minor < m.minor;
2862
+ if (v.patch !== m.patch) return v.patch < m.patch;
2863
+ return comparePrerelease(v.prerelease, m.prerelease) < 0;
2864
+ }
2408
2865
  var WarmHubClient = class _WarmHubClient {
2409
2866
  /**
2410
2867
  * The resolved API base URL the client issues requests against. Defaults to
@@ -2415,6 +2872,11 @@ var WarmHubClient = class _WarmHubClient {
2415
2872
  accessToken;
2416
2873
  functionLogMode;
2417
2874
  getToken;
2875
+ // Cached promise for diagnostics.assertCompatible(). Stored at instance
2876
+ // level so a successful check is reused across calls, and a failed check
2877
+ // is also cached so a too-old SDK keeps reporting the same actionable
2878
+ // error rather than re-hitting the network. Cleared on construction.
2879
+ compatibilityCheck;
2418
2880
  /**
2419
2881
  * Authentication helpers for browser sign-in flows, session checks, and token diagnostics.
2420
2882
  */
@@ -2533,6 +2995,41 @@ var WarmHubClient = class _WarmHubClient {
2533
2995
  } catch (error) {
2534
2996
  throw toWarmHubError(error);
2535
2997
  }
2998
+ },
2999
+ /**
3000
+ * Verify the installed SDK is at or above the backend's advertised
3001
+ * minimum supported version, throwing a `WarmHubError` with a clear
3002
+ * "upgrade to >=X" message when it is not.
3003
+ *
3004
+ * Call this once at startup (e.g. immediately after constructing the
3005
+ * client) to fail fast on SDK version skew, rather than discovering a
3006
+ * removed route deep in the commit pipeline as an opaque error.
3007
+ * The result is cached on the client instance — repeated calls reuse
3008
+ * the first network round-trip and re-throw the same error if too old.
3009
+ *
3010
+ * @see https://github.com/warmhub/warmhub-app/issues/3081
3011
+ */
3012
+ assertCompatible: () => {
3013
+ if (this.compatibilityCheck) return this.compatibilityCheck;
3014
+ const promise = (async () => {
3015
+ const capabilities = await this.diagnostics.capabilities();
3016
+ if (sdkVersionIsBelowMinimum(SDK_VERSION, capabilities.minSupportedSdk)) {
3017
+ throw new WarmHubError(
3018
+ "VALIDATION_ERROR",
3019
+ `@warmhub/sdk-ts ${SDK_VERSION} is older than the backend's minimum supported SDK (${capabilities.minSupportedSdk}). Upgrade to >=${capabilities.minSupportedSdk}.`,
3020
+ 400,
3021
+ `Run: npm install @warmhub/sdk-ts@latest (or your package manager's equivalent).`
3022
+ );
3023
+ }
3024
+ })();
3025
+ this.compatibilityCheck = promise;
3026
+ void promise.catch((error) => {
3027
+ if (isWarmHubError(error) && error.code === "VALIDATION_ERROR") return;
3028
+ if (this.compatibilityCheck === promise) {
3029
+ this.compatibilityCheck = void 0;
3030
+ }
3031
+ });
3032
+ return promise;
2536
3033
  }
2537
3034
  };
2538
3035
  /**
@@ -2722,7 +3219,7 @@ var WarmHubClient = class _WarmHubClient {
2722
3219
  /**
2723
3220
  * High-level write surface for submitting WarmHub operations through the commit pipeline.
2724
3221
  *
2725
- * @see https://docs.warmhub.ai/sdk/commit-vs-builder/
3222
+ * @see https://docs.warmhub.ai/sdk/write-methods/
2726
3223
  */
2727
3224
  commit = {
2728
3225
  /**
@@ -2730,7 +3227,7 @@ var WarmHubClient = class _WarmHubClient {
2730
3227
  *
2731
3228
  * This is the primary write path for SDK callers. It streams operations to the backend, preserves server-side per-operation results, supports chunking for large submissions, and can attribute writes to a committer wref or installed component.
2732
3229
  *
2733
- * Simple first-chunk transport failures are retried by default. Multi-chunk ambiguous failures are surfaced as `PartialStreamSubmissionError` so callers can inspect repository state before deciding whether to resume or retry. See [Transient Retry](/sdk/transient-retry/) for the full retry and partial-submission rules.
3230
+ * Simple first-chunk transport failures are retried by default. Ambiguous failures surface as `PartialStreamSubmissionError`, including the case where an ambiguous attempt is followed by an all-failed retry; inspect `error.cause` for the underlying `AllStreamOperationsFailedError` or backend error. Deterministic all-failed submissions without prior ambiguity throw `AllStreamOperationsFailedError` directly so callers can inspect per-operation failure rows. Validation, auth, conflict, rate-limit, and other definite backend failures surface as `WarmHubError`. See [Transient Retry](/sdk/transient-retry/) for the full retry and partial-submission rules.
2734
3231
  *
2735
3232
  * Writing to an archived organization or repository fails with an `ARCHIVED` error before any operations are applied.
2736
3233
  *
@@ -2760,7 +3257,7 @@ var WarmHubClient = class _WarmHubClient {
2760
3257
  operations
2761
3258
  });
2762
3259
  } catch (error) {
2763
- if (error instanceof PartialStreamSubmissionError) {
3260
+ if (error instanceof PartialStreamSubmissionError || error instanceof AllStreamOperationsFailedError) {
2764
3261
  throw error;
2765
3262
  }
2766
3263
  throw toWarmHubError(error);
@@ -3031,7 +3528,7 @@ var WarmHubClient = class _WarmHubClient {
3031
3528
  *
3032
3529
  * The returned `total` is the sum of active shapes, things, and assertions. Use this when billing, quota checks, health reports, or per-shape breakdowns need single-repo stats.
3033
3530
  *
3034
- * The per-shape breakdown counts active things by shape; assertions are not included in that map.
3531
+ * The per-shape breakdown counts active things and assertions by shape.
3035
3532
  */
3036
3533
  getStats: async (orgName, repoName) => {
3037
3534
  try {
@@ -4871,6 +5368,6 @@ function isAbortError(error) {
4871
5368
  return error instanceof Error && error.name === "AbortError";
4872
5369
  }
4873
5370
 
4874
- export { CONTENT_FIELD_LIMIT_ERROR, DEFAULT_API_URL, DEFAULT_STREAM_CHUNK_SIZE, MAX_CONTENT_FIELD_BYTES, MAX_STREAM_APPEND_OPERATION_COUNT2 as MAX_STREAM_APPEND_OPERATION_COUNT, OperationBuilder, PartialStreamSubmissionError, SDK_VERSION, WarmHubClient, WarmHubError, connectionErrorMessage, contentFieldLimitError, isConnectionError, isRetryable, isWarmHubError, normalizeWref, resolveFunctionLogMode, sanitizeErrorMessage, submitOperationsViaStream, toWarmHubError, validateAgainstShape };
4875
- //# sourceMappingURL=chunk-ERGCQLFH.js.map
4876
- //# sourceMappingURL=chunk-ERGCQLFH.js.map
5371
+ export { AllStreamOperationsFailedError, CLI_INSTALL_REPO_HEADER, CLI_SIGNATURE_HEADER, CLI_TIMESTAMP_HEADER, CONTENT_FIELD_LIMIT_ERROR, CliCallVerificationError, DEFAULT_API_URL, DEFAULT_STREAM_CHUNK_SIZE, MAX_CONTENT_FIELD_BYTES, MAX_STREAM_APPEND_OPERATION_COUNT2 as MAX_STREAM_APPEND_OPERATION_COUNT, OperationBuilder, PartialStreamSubmissionError, SDK_VERSION, WarmHubClient, WarmHubError, connectionErrorMessage, contentFieldLimitError, countStreamAppendResultStatuses, isConnectionError, isRetryable, isWarmHubError, normalizeWref, resolveFunctionLogMode, sanitizeErrorMessage, sdkVersionIsBelowMinimum, streamAppendResultStatus, submitOperationsViaStream, toWarmHubError, validateAgainstShape, verifyCliCall };
5372
+ //# sourceMappingURL=chunk-RX3ZL6P5.js.map
5373
+ //# sourceMappingURL=chunk-RX3ZL6P5.js.map