@warmhub/sdk-ts 0.44.1 → 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.44.1" ;
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",
@@ -2307,13 +2713,22 @@ var WarmHubError = class extends Error {
2307
2713
  * responses and other backend signals that carry a Retry-After header.
2308
2714
  */
2309
2715
  retryAfter;
2310
- constructor(code, message, status, hint, retryAfter) {
2716
+ /**
2717
+ * Backend domain code from the response body. Set iff the backend wire
2718
+ * carried a structured `error.code` string. Use this when the question is
2719
+ * "did the backend specifically say this?". For best-effort labelling that
2720
+ * also covers SDK-local transport codes (`NETWORK`, `CANCELLED`, the
2721
+ * generic `BACKEND` fallback), branch on {@link code} or {@link kind}.
2722
+ */
2723
+ backendCode;
2724
+ constructor(code, message, status, hint, retryAfter, backendCode) {
2311
2725
  super(message);
2312
2726
  this.name = "WarmHubError";
2313
2727
  this.code = code;
2314
2728
  this.status = status;
2315
2729
  this.hint = hint;
2316
2730
  this.retryAfter = retryAfter;
2731
+ this.backendCode = backendCode;
2317
2732
  }
2318
2733
  get kind() {
2319
2734
  return this.code;
@@ -2334,13 +2749,15 @@ function toWarmHubError(error) {
2334
2749
  return error.cause;
2335
2750
  }
2336
2751
  const data = error.data;
2752
+ const wireCode = data?.warmhub?.code;
2337
2753
  const message = data?.warmhub?.message ?? sanitizeErrorMessage(error.message);
2338
2754
  return new WarmHubError(
2339
- data?.warmhub?.code ?? "BACKEND",
2755
+ wireCode ?? "BACKEND",
2340
2756
  message,
2341
2757
  data?.warmhub?.status,
2342
2758
  data?.warmhub?.hint,
2343
- data?.warmhub?.retryAfter
2759
+ data?.warmhub?.retryAfter,
2760
+ wireCode
2344
2761
  );
2345
2762
  }
2346
2763
  if (error instanceof Error) {
@@ -2351,7 +2768,8 @@ function toWarmHubError(error) {
2351
2768
  sanitizeErrorMessage(error.message),
2352
2769
  typeof warmhubLike.status === "number" ? warmhubLike.status : void 0,
2353
2770
  typeof warmhubLike.hint === "string" ? warmhubLike.hint : void 0,
2354
- typeof warmhubLike.retryAfter === "number" ? warmhubLike.retryAfter : void 0
2771
+ typeof warmhubLike.retryAfter === "number" ? warmhubLike.retryAfter : void 0,
2772
+ typeof warmhubLike.backendCode === "string" ? warmhubLike.backendCode : void 0
2355
2773
  );
2356
2774
  }
2357
2775
  if (error.name === "AbortError") {
@@ -2393,6 +2811,57 @@ function isConnectionError(error) {
2393
2811
  function connectionErrorMessage(url) {
2394
2812
  return `Unable to connect to ${url}. Is the computer able to access the url?`;
2395
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
+ }
2396
2865
  var WarmHubClient = class _WarmHubClient {
2397
2866
  /**
2398
2867
  * The resolved API base URL the client issues requests against. Defaults to
@@ -2403,6 +2872,11 @@ var WarmHubClient = class _WarmHubClient {
2403
2872
  accessToken;
2404
2873
  functionLogMode;
2405
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;
2406
2880
  /**
2407
2881
  * Authentication helpers for browser sign-in flows, session checks, and token diagnostics.
2408
2882
  */
@@ -2521,6 +2995,41 @@ var WarmHubClient = class _WarmHubClient {
2521
2995
  } catch (error) {
2522
2996
  throw toWarmHubError(error);
2523
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;
2524
3033
  }
2525
3034
  };
2526
3035
  /**
@@ -2710,7 +3219,7 @@ var WarmHubClient = class _WarmHubClient {
2710
3219
  /**
2711
3220
  * High-level write surface for submitting WarmHub operations through the commit pipeline.
2712
3221
  *
2713
- * @see https://docs.warmhub.ai/sdk/commit-vs-builder/
3222
+ * @see https://docs.warmhub.ai/sdk/write-methods/
2714
3223
  */
2715
3224
  commit = {
2716
3225
  /**
@@ -2718,7 +3227,7 @@ var WarmHubClient = class _WarmHubClient {
2718
3227
  *
2719
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.
2720
3229
  *
2721
- * 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.
2722
3231
  *
2723
3232
  * Writing to an archived organization or repository fails with an `ARCHIVED` error before any operations are applied.
2724
3233
  *
@@ -2748,7 +3257,7 @@ var WarmHubClient = class _WarmHubClient {
2748
3257
  operations
2749
3258
  });
2750
3259
  } catch (error) {
2751
- if (error instanceof PartialStreamSubmissionError) {
3260
+ if (error instanceof PartialStreamSubmissionError || error instanceof AllStreamOperationsFailedError) {
2752
3261
  throw error;
2753
3262
  }
2754
3263
  throw toWarmHubError(error);
@@ -3019,7 +3528,7 @@ var WarmHubClient = class _WarmHubClient {
3019
3528
  *
3020
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.
3021
3530
  *
3022
- * 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.
3023
3532
  */
3024
3533
  getStats: async (orgName, repoName) => {
3025
3534
  try {
@@ -4718,6 +5227,7 @@ var WarmHubClient = class _WarmHubClient {
4718
5227
  if (!response.ok) {
4719
5228
  let message = `Request failed with status ${response.status}`;
4720
5229
  let code = httpStatusToWarmHubCode(response.status);
5230
+ let backendCode;
4721
5231
  let hint;
4722
5232
  let retryAfter;
4723
5233
  try {
@@ -4727,6 +5237,7 @@ var WarmHubClient = class _WarmHubClient {
4727
5237
  }
4728
5238
  if (typeof body.error?.code === "string") {
4729
5239
  code = body.error.code;
5240
+ backendCode = body.error.code;
4730
5241
  }
4731
5242
  if (typeof body.error?.hint === "string") {
4732
5243
  hint = body.error.hint;
@@ -4736,7 +5247,14 @@ var WarmHubClient = class _WarmHubClient {
4736
5247
  }
4737
5248
  } catch {
4738
5249
  }
4739
- throw new WarmHubError(code, message, response.status, hint, retryAfter);
5250
+ throw new WarmHubError(
5251
+ code,
5252
+ message,
5253
+ response.status,
5254
+ hint,
5255
+ retryAfter,
5256
+ backendCode
5257
+ );
4740
5258
  }
4741
5259
  return response;
4742
5260
  }
@@ -4850,6 +5368,6 @@ function isAbortError(error) {
4850
5368
  return error instanceof Error && error.name === "AbortError";
4851
5369
  }
4852
5370
 
4853
- 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 };
4854
- //# sourceMappingURL=chunk-KFNNTFDG.js.map
4855
- //# sourceMappingURL=chunk-KFNNTFDG.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