@zackbart/connecta 0.37.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +2 -2
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/auth/clerk-sdk/generated.js +2 -1
  8. package/dist/auth/cloudflare-access.d.ts +3 -1
  9. package/dist/auth/cloudflare-access.js +22 -11
  10. package/dist/auth/downstream-oauth.js +195 -270
  11. package/dist/auth/downstream-revocation.js +1 -1
  12. package/dist/auth/oauth-grant.js +11 -0
  13. package/dist/auth/oauth-token-response.js +1 -1
  14. package/dist/auth/vercel-agent.js +2 -1
  15. package/dist/auth-elicitation.js +2 -2
  16. package/dist/branding.d.ts +1 -15
  17. package/dist/branding.js +2 -2
  18. package/dist/bytes.js +42 -0
  19. package/dist/call-admission-context.js +2 -5
  20. package/dist/call-admission.js +5 -0
  21. package/dist/call-recovery.js +0 -3
  22. package/dist/catalog-cache.js +26 -7
  23. package/dist/catalog-fingerprint.js +2 -18
  24. package/dist/catalog-revision.js +9 -0
  25. package/dist/catalog-service.js +51 -20
  26. package/dist/config-defaults.d.ts +1 -0
  27. package/dist/config-defaults.js +1 -0
  28. package/dist/config-schema.d.ts +1 -1
  29. package/dist/config-schema.js +6 -2
  30. package/dist/config.d.ts +41 -14
  31. package/dist/config.js +21 -71
  32. package/dist/connector-caller.js +1 -1
  33. package/dist/connectors/api-connector.js +4 -3
  34. package/dist/connectors/guarded-fetch.d.ts +0 -6
  35. package/dist/connectors/guarded-fetch.js +1 -11
  36. package/dist/connectors/remote-mcp.js +93 -69
  37. package/dist/connectors/resource-uri.js +0 -4
  38. package/dist/credentials.js +3 -0
  39. package/dist/d1.d.ts +0 -89
  40. package/dist/d1.js +0 -34
  41. package/dist/downstream-input.js +2 -2
  42. package/dist/execute.js +161 -116
  43. package/dist/executor-result.js +2 -1
  44. package/dist/executors/quickjs-runtime.js +2 -25
  45. package/dist/executors/quickjs.js +1 -1
  46. package/dist/guest-runtime.js +5 -3
  47. package/dist/guest-types.d.ts +5 -5
  48. package/dist/http/client.js +1 -1
  49. package/dist/http/errors.js +2 -2
  50. package/dist/http/json-schema.js +1 -1
  51. package/dist/http/node-transport.js +0 -1
  52. package/dist/http/oauth.js +1 -1
  53. package/dist/http/paging.js +6 -9
  54. package/dist/http/provider.js +1 -1
  55. package/dist/http/safety.js +1 -1
  56. package/dist/http/services.js +1 -8
  57. package/dist/http/tools.js +1 -7
  58. package/dist/http/vendor.js +7 -7
  59. package/dist/identity.js +2 -6
  60. package/dist/index.js +1 -2
  61. package/dist/invocation-auth.js +2 -1
  62. package/dist/invocation.js +8 -2
  63. package/dist/meta-tools.js +27 -27
  64. package/dist/native-tools.js +2 -1
  65. package/dist/oauth-handoff.js +2 -2
  66. package/dist/operation-vocabulary.js +1 -1
  67. package/dist/operator-record.d.ts +8 -2
  68. package/dist/operator-record.js +18 -50
  69. package/dist/operator-ui/contract.d.ts +4 -2
  70. package/dist/operator-ui/generated.js +3 -3
  71. package/dist/operator-ui/model.d.ts +7 -2
  72. package/dist/program-writes.js +3 -8
  73. package/dist/provider-call.js +0 -7
  74. package/dist/provider.d.ts +1 -1
  75. package/dist/providers/_shared/google/kit.js +1 -6
  76. package/dist/providers/_shared/google/service-account.js +1 -3
  77. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  78. package/dist/providers/_shared/guide.js +3 -0
  79. package/dist/providers/_shared/json.js +12 -0
  80. package/dist/providers/_shared/rest/compact-index.js +1 -1
  81. package/dist/providers/_shared/rest/kit.js +1 -3
  82. package/dist/providers/_shared/rest/operation-index.js +1 -3
  83. package/dist/providers/_shared/rest/value-safety.js +26 -28
  84. package/dist/providers/breeze/api.js +2 -7
  85. package/dist/providers/breeze/index.js +4 -2
  86. package/dist/providers/breeze/tools.js +3 -4
  87. package/dist/providers/ccb/api.js +1 -6
  88. package/dist/providers/ccb/index.js +2 -1
  89. package/dist/providers/ccb/tools.js +1 -6
  90. package/dist/providers/cloudflare/api.js +2 -2
  91. package/dist/providers/cloudflare/index.js +2 -1
  92. package/dist/providers/cloudflare/safety.js +3 -3
  93. package/dist/providers/docs/tools.js +2 -10
  94. package/dist/providers/drive/tools.js +12 -25
  95. package/dist/providers/forms/tools.js +1 -9
  96. package/dist/providers/gmail/tools.js +5 -21
  97. package/dist/providers/overflow/api.js +1 -3
  98. package/dist/providers/overflow/index.js +2 -1
  99. package/dist/providers/overflow/tools.js +3 -7
  100. package/dist/providers/planning-center/api.js +2 -1
  101. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  102. package/dist/providers/planning-center/lib/json-api.js +1 -9
  103. package/dist/providers/planning-center/tools.js +2 -1
  104. package/dist/providers/sheets/tools.js +1 -9
  105. package/dist/providers/slides/tools.js +4 -13
  106. package/dist/providers/stripe/index.js +2 -1
  107. package/dist/providers/stripe/safety.js +2 -2
  108. package/dist/providers/tithely/api.js +2 -7
  109. package/dist/providers/tithely/index.js +3 -2
  110. package/dist/providers/tithely/tools.js +7 -5
  111. package/dist/providers/vercel/api.js +1 -6
  112. package/dist/providers/vercel/lib/pin.js +1 -1
  113. package/dist/providers/vercel/safety.js +1 -1
  114. package/dist/providers/vercel/tools.js +2 -1
  115. package/dist/redaction-marker.js +2 -2
  116. package/dist/registry.d.ts +4 -1
  117. package/dist/registry.js +35 -22
  118. package/dist/request-principal.js +15 -2
  119. package/dist/request-state.js +2 -2
  120. package/dist/routes/access-tokens.js +1 -1
  121. package/dist/routes/activity-disclosure.js +1 -1
  122. package/dist/routes/connect.js +2 -2
  123. package/dist/routes/credentials.js +1 -1
  124. package/dist/routes/dispatch.js +10 -0
  125. package/dist/routes/mcp.js +23 -21
  126. package/dist/routes/oauth-management.js +1 -1
  127. package/dist/routes/operator.js +53 -7
  128. package/dist/routes/shared.d.ts +8 -8
  129. package/dist/routes/shared.js +22 -15
  130. package/dist/routes/ui-config.js +6 -6
  131. package/dist/routes/ui-data.js +2 -2
  132. package/dist/routes/ui.js +9 -4
  133. package/dist/runtime/call-admission.js +1 -1
  134. package/dist/runtime/call.js +1 -1
  135. package/dist/runtime/settlement.js +1 -1
  136. package/dist/runtime/shared-read.js +2 -15
  137. package/dist/sent-secrets.d.ts +14 -6
  138. package/dist/sent-secrets.js +231 -85
  139. package/dist/server.js +4 -4
  140. package/dist/skills.js +8 -35
  141. package/dist/sqlite.d.ts +0 -19
  142. package/dist/sqlite.js +2 -83
  143. package/dist/storage/keys.js +29 -124
  144. package/dist/storage/sql-native.js +14 -111
  145. package/dist/storage/sql.d.ts +1 -24
  146. package/dist/storage/sql.js +1 -12
  147. package/dist/tool-error.js +4 -0
  148. package/dist/types.d.ts +4 -3
  149. package/dist/ui.js +3 -0
  150. package/dist/url-safety.js +0 -4
  151. package/dist/usage-guide.js +2 -2
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/dist/worker.js +1 -1
  155. package/documentation/README.md +6 -42
  156. package/documentation/architecture.md +190 -507
  157. package/documentation/auth.md +172 -58
  158. package/documentation/code-mode.md +98 -66
  159. package/documentation/deploying.md +19 -40
  160. package/documentation/integrating.md +8 -19
  161. package/documentation/meta-tools.md +23 -6
  162. package/documentation/migration-0.33.md +5 -5
  163. package/documentation/migration-0.34.md +2 -4
  164. package/documentation/migration-retired-layouts.md +74 -0
  165. package/documentation/operator-tests.md +19 -119
  166. package/documentation/operator-ui.md +46 -8
  167. package/documentation/provider-auth-migration.md +4 -10
  168. package/documentation/provider-migration-0.29.md +4 -15
  169. package/examples/worker/AGENTS.md +4 -6
  170. package/examples/worker/README.md +20 -115
  171. package/guides/connecting-agents.md +7 -30
  172. package/guides/how-it-works.md +2 -2
  173. package/guides/safety-and-trust.md +4 -1
  174. package/package.json +19 -19
  175. package/templates/node/README.md +9 -26
  176. package/templates/node/package.json +1 -1
  177. package/dist/auth/oauth-v2-migration.js +0 -135
  178. package/dist/provider-call-facts.d.ts +0 -115
  179. package/dist/provider-call.d.ts +0 -112
  180. package/dist/storage/d1-copy.js +0 -255
  181. package/dist/storage/sql-schema.d.ts +0 -6
  182. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  183. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -5,7 +5,7 @@ import { Effect } from "effect";
5
5
  import { classificationCode, ConnectorCallError } from "./errors.js";
6
6
  import { credentialUrlViews } from "./credential-url.js";
7
7
  import { RequestScope, requestOwner } from "./runtime/request-scope.js";
8
- import { viewSecrets, wrapViewCredentials } from "./runtime/call.js";
8
+ import { viewSecrets } from "./runtime/call.js";
9
9
  import { visitPrivateCallArguments } from "./argument-redaction.js";
10
10
  import { keptErrorName, redactedCopyOf } from "./redaction-identity.js";
11
11
  import { REDACTED } from "./redaction-marker.js";
@@ -127,6 +127,12 @@ const TEXT = 0;
127
127
  const NUMBER = 1;
128
128
  const BINARY = 2;
129
129
  const BYTES = 3;
130
+ // A typed binary's joined numbers are withheld together, including on an error.
131
+ const NUMBERS = 4;
132
+ // A changed numeric diagnostic is absent from the error's text projection.
133
+ const ERROR_NUMBER = 5;
134
+ const numeric = (kind) => kind === NUMBER || kind === NUMBERS || kind === ERROR_NUMBER;
135
+ const replacement = (kind) => (kind === BINARY || kind === ERROR_NUMBER ? "" : REDACTED);
130
136
  /** An accessor met while looking up an inherited field: never invoked. */
131
137
  const WITHHELD_ACCESSOR = Symbol("accessor");
132
138
  /** The engine's own getters for what a typed array, DataView or ArrayBuffer holds, so no
@@ -682,7 +688,7 @@ function headerSpace(text, at) {
682
688
  }
683
689
  /**
684
690
  * The echo's spans with the value of every sensitive header line (`headerLine`'s names) added,
685
- * read on the text before the echo is replaced (#703). An echoed private value can take part of a
691
+ * read on the text before the echo is replaced. An echoed private value can take part of a
686
692
  * header's name, its `:`, or a credential prefix such as `Bearer `, so the line cannot be read
687
693
  * after replacement; read here, the whole value goes, every cookie pair and any short tail with
688
694
  * it. A line that is not a sensitive header keeps its text. One forward pass; nothing is kept.
@@ -1310,6 +1316,24 @@ export class SentSecrets {
1310
1316
  text(value) {
1311
1317
  return this.idle() ? value : this.project(value, this.scan(true)).value;
1312
1318
  }
1319
+ /** An SDK response body, checked after it has been fully read so late registrations
1320
+ * still apply. An idle set leaves the original serialization untouched. */
1321
+ serializedAgentOutput(body, json) {
1322
+ if (this.idle())
1323
+ return body;
1324
+ if (json) {
1325
+ // Structured redaction joins text blocks, including secrets split across them.
1326
+ try {
1327
+ return JSON.stringify(redactAgentOutput(this, JSON.parse(body)));
1328
+ }
1329
+ catch {
1330
+ /* Non-JSON diagnostics still pass through the text boundary. */
1331
+ }
1332
+ }
1333
+ // A second serialized pass over parsed JSON would mistake a blob's encoding
1334
+ // for credential bytes. Only non-JSON bodies take this text path.
1335
+ return redactAgentOutput(this, body);
1336
+ }
1313
1337
  /** A plain-data snapshot of `value` with every match removed: own enumerable data properties
1314
1338
  * (an error's own fields, non-enumerable ones included), read once each. Accessors are never
1315
1339
  * invoked, except an error's stack and the accessors its class defines, read once into the
@@ -1566,10 +1590,8 @@ export class SentSecrets {
1566
1590
  * charged to the call's budget, and a part it cannot pay for is withheld. A `bounded` lane is
1567
1591
  * text cut short, which also loses an unfinished tail. Nothing else matches a string that is
1568
1592
  * returned: the matching steps below are called only from here. */
1569
- checked(parts, kinds, scan, bounded = false) {
1570
- const safe = this.safeParts(parts, kinds, scan, bounded);
1571
- this.encodedLeaves(parts, kinds, safe, scan);
1572
- return safe;
1593
+ checked(parts, kinds, scan, bounded = false, normalize) {
1594
+ return this.safeParts(parts, kinds, scan, bounded, normalize);
1573
1595
  }
1574
1596
  /** Cursor claims: the whole text and its encoding under the boundary's rules, then every
1575
1597
  * string in the JSON under payload rules. Keys and numbers are Connecta's own names and
@@ -1690,7 +1712,7 @@ export class SentSecrets {
1690
1712
  // Binary and bytes any match changed are withheld whole already.
1691
1713
  const kind = kinds[index];
1692
1714
  const text = safe[index];
1693
- if (kind === NUMBER || text === REDACTED || this.memo.get(text) === true)
1715
+ if (numeric(kind) || text === REDACTED || this.memo.get(text) === true)
1694
1716
  continue;
1695
1717
  if ((!scan.payload && this.memo.has(text)) || (kind !== TEXT && text !== parts[index]))
1696
1718
  continue;
@@ -1755,11 +1777,63 @@ export class SentSecrets {
1755
1777
  safe[pairLeaves[pair]] = REDACTED;
1756
1778
  }
1757
1779
  }
1758
- /** The parts' joined text, matched once, with each part's replacement. A tiny or short
1759
- * private value equal to a whole part withholds it. A number or a serialized container that
1760
- * a match touches is withheld whole. A bounded lane also loses an unfinished tail, and is
1761
- * withheld whole when the set could not tell where one begins. */
1762
- safeParts(parts, kinds, scan, bounded) {
1780
+ /** Check replacements as returned, including their placeholder seams. Numbers touched by
1781
+ * the first scan get one chance beside the replaced strings; every later hit withholds its
1782
+ * entire part. Each changing round therefore removes at least one remaining part. Unchanged
1783
+ * lanes pay for no retry. Large lanes recheck changed parts and the seams touching them; an
1784
+ * unaffordable check withholds every part it read and all original retention candidates. */
1785
+ settleParts(parts, safe, scan, numbers = [], leafByLeaf = false, kinds, normalize) {
1786
+ let touched = new Set(parts.flatMap((part, index) => (part !== safe[index] ? [index] : [])));
1787
+ if (!touched.size)
1788
+ return safe;
1789
+ const lane = [...safe];
1790
+ for (const index of numbers)
1791
+ lane[index] = parts[index];
1792
+ normalize?.(lane);
1793
+ while (touched.size) {
1794
+ // Pay for round bookkeeping even when no registered matcher could scan (for
1795
+ // example, an oversized private registration governed only by withholdRuns).
1796
+ if (!spend(scan, lane.length)) {
1797
+ const withheld = lane.map((part, index) => part && replacement(kinds?.[index]));
1798
+ normalize?.(withheld);
1799
+ return withheld;
1800
+ }
1801
+ let checked;
1802
+ const recheck = { parts: touched, complete: true };
1803
+ const matches = leafByLeaf ? undefined : this.spansOf(lane.join(""), scan, scan.payload);
1804
+ if (matches && !matches.deep)
1805
+ checked = replaceParts(lane, matches.spans);
1806
+ else
1807
+ checked = this.leafByLeaf(lane, scan, undefined, recheck);
1808
+ const limit = this.limit(scan);
1809
+ if (limit < Infinity)
1810
+ checked = withholdRuns(lane, checked, limit);
1811
+ if (!recheck.complete)
1812
+ for (const index of numbers)
1813
+ if (checked[index] === parts[index])
1814
+ checked[index] = replacement(kinds?.[index]);
1815
+ normalize?.(checked);
1816
+ const next = new Set();
1817
+ for (let index = 0; index < lane.length; index++) {
1818
+ if (checked[index] === lane[index] || (lane[index] === REDACTED && checked[index] !== ""))
1819
+ continue;
1820
+ const withheld = replacement(kinds?.[index]);
1821
+ // Whole-field groups project one marker and empty remaining parts.
1822
+ const value = checked[index] === "" ? "" : withheld;
1823
+ if (lane[index] === value)
1824
+ continue;
1825
+ lane[index] = value;
1826
+ next.add(index);
1827
+ }
1828
+ touched = next;
1829
+ }
1830
+ return lane;
1831
+ }
1832
+ /** The parts' joined text, with each part's replacement. A touched number is checked again
1833
+ * after string replacement before withholding it whole. Tiny or short whole private values,
1834
+ * deep matches and unchecked runs still withhold changed numbers whole. A bounded lane also
1835
+ * loses an unfinished tail, and withholds every touched number without a second check. */
1836
+ safeParts(parts, kinds, scan, bounded, normalize) {
1763
1837
  if (!parts.length)
1764
1838
  return parts;
1765
1839
  const text = parts.join("");
@@ -1769,29 +1843,63 @@ export class SentSecrets {
1769
1843
  return parts.map((part) => part && REDACTED);
1770
1844
  if (bounded)
1771
1845
  this.openSpans(text, matches.spans);
1772
- let safe = matches ? replaceParts(parts, matches.spans) : this.leafByLeaf(parts, scan);
1846
+ const numbers = [];
1847
+ let safe = matches ? replaceParts(parts, matches.spans) : this.leafByLeaf(parts, scan, kinds, undefined, numbers);
1848
+ if (matches && !bounded && !matches.deep)
1849
+ for (let index = 0; index < parts.length; index++)
1850
+ if (numeric(kinds[index]) && safe[index] !== parts[index])
1851
+ numbers.push(index);
1773
1852
  if (matches?.deep)
1774
1853
  safe = safe.map((next, index) => (parts[index].includes("\\") ? REDACTED : next));
1854
+ const whole = new Set();
1775
1855
  if (scan.payload && this.smallForms)
1776
- safe = safe.map((next, index) => (this.whole(parts[index]) ? REDACTED : next));
1856
+ safe = safe.map((next, index) => {
1857
+ // Typed numeric groups checked their individual whole values while walking.
1858
+ if (kinds[index] === NUMBERS || !this.whole(parts[index]))
1859
+ return next;
1860
+ whole.add(index);
1861
+ return REDACTED;
1862
+ });
1777
1863
  const limit = this.limit(scan);
1778
1864
  if (limit < Infinity)
1779
1865
  safe = withholdRuns(parts, safe, limit);
1780
- return safe.map((next, index) => (next !== parts[index] && kinds[index] === NUMBER ? REDACTED : next));
1866
+ safe = safe.map((next, index) => (next !== parts[index] && numeric(kinds[index]) ? REDACTED : next));
1867
+ this.encodedLeaves(parts, kinds, safe, scan);
1868
+ // Declared binary leaves as the checked encoding of the marker, not its partial text.
1869
+ let marker;
1870
+ safe = safe.map((next, index) => {
1871
+ if (next === parts[index] || kinds[index] !== BINARY)
1872
+ return next;
1873
+ const encoded = btoa(REDACTED);
1874
+ marker ??= this.checked([encoded], [TEXT], scan)[0] === encoded ? encoded : "";
1875
+ return marker;
1876
+ });
1877
+ safe = safe.map((next, index) => (next !== parts[index] && kinds[index] === ERROR_NUMBER ? "" : next));
1878
+ normalize?.(safe);
1879
+ return this.settleParts(parts, safe, scan, numbers.filter((index) => !whole.has(index) && limit === Infinity), !matches, kinds, normalize);
1781
1880
  }
1782
1881
  /** Leaves too large to match joined within the work budget: each is matched alone, in order,
1783
1882
  * and one that does not fit is withheld without spending what a smaller one needs. A run
1784
1883
  * crossing from one kept leaf into the next, through any short leaves between them, is then
1785
1884
  * matched SEAM_UNITS deep on each side of the seam. */
1786
- leafByLeaf(parts, scan) {
1787
- const spans = parts.map((part) => {
1885
+ leafByLeaf(parts, scan, kinds, recheck, candidates) {
1886
+ const spans = parts.map((part, index) => {
1887
+ // Only changed parts and their seams need checking on a retry. Kept neighbours
1888
+ // remain exactly as returned, even when their full text would not fit again.
1889
+ if (recheck && !recheck.parts.has(index))
1890
+ return [];
1788
1891
  const matches = this.spansOf(part, scan, scan.payload);
1892
+ if (recheck && (!matches || matches.deep))
1893
+ recheck.complete = false;
1789
1894
  return !matches || (matches.deep && part.includes("\\")) ? undefined : matches.spans;
1790
1895
  });
1791
1896
  let pieces = [];
1792
1897
  const seam = () => {
1793
1898
  const text = pieces.map((piece) => piece.text).join("");
1794
- const matches = pieces.length > 1 ? this.spansOf(text, scan, scan.payload) : { spans: [], deep: false };
1899
+ const needed = pieces.length > 1 && (!recheck || pieces.some((piece) => recheck.parts.has(piece.index)));
1900
+ const matches = needed ? this.spansOf(text, scan, scan.payload) : { spans: [], deep: false };
1901
+ if (recheck && (!matches || matches.deep))
1902
+ recheck.complete = false;
1795
1903
  const ranges = matches ? merged(matches.spans) : [];
1796
1904
  let offset = 0;
1797
1905
  for (const piece of pieces) {
@@ -1806,7 +1914,9 @@ export class SentSecrets {
1806
1914
  pieces = [];
1807
1915
  };
1808
1916
  parts.forEach((part, index) => {
1809
- if (!spans[index])
1917
+ // A failed retry, or a marker already introduced while walking, still
1918
+ // contains this part in the returned lane. Keep its unchecked seams.
1919
+ if (!spans[index] && !recheck && part !== REDACTED)
1810
1920
  return seam();
1811
1921
  if (part.length <= 2 * SEAM_UNITS)
1812
1922
  return void pieces.push({ index, start: 0, text: part });
@@ -1815,10 +1925,21 @@ export class SentSecrets {
1815
1925
  pieces.push({ index, start: part.length - SEAM_UNITS, text: part.slice(-SEAM_UNITS) });
1816
1926
  });
1817
1927
  seam();
1818
- return parts.map((part, index) => {
1928
+ const safe = parts.map((part, index) => {
1819
1929
  const own = spans[index];
1820
1930
  return !own ? part && REDACTED : own.length ? replace(part, own) : part;
1821
1931
  });
1932
+ const numbers = kinds
1933
+ ? kinds.flatMap((kind, index) => numeric(kind) && spans[index]?.length && safe[index] !== parts[index] ? [index] : [])
1934
+ : [];
1935
+ if (candidates) {
1936
+ for (const index of numbers)
1937
+ candidates.push(index);
1938
+ return safe;
1939
+ }
1940
+ return kinds
1941
+ ? this.settleParts(parts, safe.map((next, index) => (next !== parts[index] && numeric(kinds[index]) ? REDACTED : next)), scan, numbers, true, kinds)
1942
+ : safe;
1822
1943
  }
1823
1944
  /** Serialized JSON whose keys, syntax and leaves together hold a K-unit run of a form: one
1824
1945
  * spanning tokens, which no leaf holds alone, withholds the whole text. A run inside one
@@ -1891,10 +2012,12 @@ export class SentSecrets {
1891
2012
  const active = new Set();
1892
2013
  // Keys and scalars repeat across rows; each is checked once per call.
1893
2014
  const seen = { keys: new Map(), scalars: new Map() };
1894
- // Errors' unregistered names, as walked: a copy whose name changed holds the placeholder.
2015
+ // An error name may contain parsed JSON. Any change collapses all of its
2016
+ // projected leaves to one placeholder before the surrounding lane settles.
1895
2017
  const named = [];
1896
- // Typed binary: where its copy was put, and the leaves its numbers and bytes span.
1897
- const binaries = [];
2018
+ // Non-byte typed arrays have an alternate numeric projection, in the same slots as
2019
+ // their returned bytes. Neither representation can interrupt the other's neighbours.
2020
+ const elements = new Map();
1898
2021
  // The accessors each prototype's chain defines, found once per call.
1899
2022
  const chains = new Map();
1900
2023
  let changed = false;
@@ -1903,7 +2026,8 @@ export class SentSecrets {
1903
2026
  if (owner >= 0)
1904
2027
  nested[owner].dirty = true;
1905
2028
  };
1906
- const withheld = (owner) => {
2029
+ const withheld = (frame, slot, owner) => {
2030
+ leaf(REDACTED, TEXT, frame, slot, owner);
1907
2031
  touch(owner);
1908
2032
  return REDACTED;
1909
2033
  };
@@ -2004,8 +2128,9 @@ export class SentSecrets {
2004
2128
  if (level < MAX_DECODE_LEVELS && /^\s*["[{]/.test(item) && spend(scan, item.length)) {
2005
2129
  const parsed = parse(item);
2006
2130
  if (parsed) {
2007
- const entry = { frame, slot, json: undefined, owner, crossing: false, dirty: false };
2008
- entry.crossing = this.spansTokens(item, scan);
2131
+ if (this.spansTokens(item, scan))
2132
+ return withheld(frame, slot, owner);
2133
+ const entry = { frame, slot, json: undefined, owner, dirty: false };
2009
2134
  const index = nested.push(entry) - 1;
2010
2135
  // Parsed JSON is this walk's own, so it is written in place.
2011
2136
  const holder = { target: entry, shared: false, parent: undefined, slot: "json" };
@@ -2017,25 +2142,25 @@ export class SentSecrets {
2017
2142
  return item;
2018
2143
  }
2019
2144
  if (typeof item === "number" || typeof item === "bigint") {
2020
- leaf(String(item), NUMBER, frame, slot, owner);
2145
+ leaf(String(item), frame.target instanceof Error ? ERROR_NUMBER : NUMBER, frame, slot, owner);
2021
2146
  return item;
2022
2147
  }
2023
2148
  if (typeof item === "boolean" || item === null)
2024
- return check(seen.scalars, String(item), NUMBER) === String(item) ? item : withheld(owner);
2149
+ return check(seen.scalars, String(item), NUMBER) === String(item) ? item : withheld(frame, slot, owner);
2025
2150
  // A function carries no data, and every exit's serializer would call a toJSON it holds; a
2026
2151
  // symbol carries none either. Both are withheld.
2027
2152
  if (typeof item === "function" || typeof item === "symbol")
2028
- return withheld(owner);
2153
+ return withheld(frame, slot, owner);
2029
2154
  if (typeof item !== "object")
2030
2155
  return item;
2031
2156
  if (active.has(item) || depth > MAX_WALK_DEPTH)
2032
- return withheld(owner);
2157
+ return withheld(frame, slot, owner);
2033
2158
  active.add(item);
2034
2159
  try {
2035
2160
  return container(item, frame, slot, depth, level, owner, owned);
2036
2161
  }
2037
2162
  catch {
2038
- return withheld(owner); // A proxy trap or a toJSON that threw.
2163
+ return withheld(frame, slot, owner); // A proxy trap or a toJSON that threw.
2039
2164
  }
2040
2165
  finally {
2041
2166
  active.delete(item);
@@ -2063,24 +2188,36 @@ export class SentSecrets {
2063
2188
  leaf(text, BINARY, frame, slot, owner);
2064
2189
  return text;
2065
2190
  };
2066
- /** Typed binary as the fresh Uint8Array of its bytes. Its leaves are the numbers JSON shows
2067
- * for it and for its copy, each element's and each byte's, and its bytes; a match in any
2068
- * withholds it whole, as does a size past the node budget. */
2069
- const typed = ({ bytes, elements }, frame, slot, owner) => {
2070
- const numbers = elements ? new elements(bytes.buffer) : undefined;
2071
- if (!fits(bytes.length + (numbers?.length ?? 0) + 1) || !spend(scan, Math.ceil(bytes.length / 8)))
2072
- return withheld(owner);
2073
- // The leaves write nowhere: the copy's slot is settled once they are checked.
2074
- const sink = { target: [], shared: false, parent: undefined, slot };
2075
- const start = parts.length;
2076
- if (numbers)
2077
- for (let index = 0; index < numbers.length; index++)
2078
- leaf(String(numbers[index]), NUMBER, sink, index, owner);
2079
- for (let index = 0; index < bytes.length; index++)
2080
- leaf(DECIMAL[bytes[index]], NUMBER, sink, index, owner);
2081
- leaf(latin1(bytes), BYTES, sink, 0, owner);
2082
- binaries.push([frame, slot, owner, start, parts.length]);
2083
- return bytes;
2191
+ /** Typed binary is one whole-withheld numeric part: its byte numbers are adjacent to
2192
+ * neighbours in the returned lane, and original elements substitute in a separate lane.
2193
+ * Individual numeric whole-value matches and the raw byte view are also checked. */
2194
+ const typed = ({ bytes, elements: constructor }, frame, slot, owner) => {
2195
+ const numbers = constructor ? new constructor(bytes.buffer) : undefined;
2196
+ let clean = fits(bytes.length + (numbers?.length ?? 0) + 1) && spend(scan, Math.ceil(bytes.length / 8));
2197
+ const decimal = (values) => {
2198
+ const text = [];
2199
+ for (let index = 0; index < values.length; index++) {
2200
+ const number = values === bytes ? DECIMAL[bytes[index]] : String(values[index]);
2201
+ if (scan.payload && this.smallForms && this.whole(number))
2202
+ clean = false;
2203
+ text.push(number);
2204
+ }
2205
+ return text.join("");
2206
+ };
2207
+ // Refuse oversize arrays before building their numeric projection.
2208
+ if (!clean) {
2209
+ return withheld(frame, slot, owner);
2210
+ }
2211
+ const text = decimal(bytes);
2212
+ const original = numbers ? decimal(numbers) : undefined;
2213
+ const binary = latin1(bytes);
2214
+ clean &&= this.checked([binary], [BYTES], scan)[0] === binary;
2215
+ if (clean && original !== undefined)
2216
+ elements.set(parts.length, original);
2217
+ leaf(clean ? text : REDACTED, NUMBERS, frame, slot, owner);
2218
+ if (!clean)
2219
+ touch(owner);
2220
+ return clean ? bytes : REDACTED;
2084
2221
  };
2085
2222
  const container = (item, frame, slot, depth, level, owner, owned) => {
2086
2223
  const error = item instanceof Error;
@@ -2094,7 +2231,7 @@ export class SentSecrets {
2094
2231
  // What serialization would read: a toJSON result, checked in place of the container.
2095
2232
  const toJSON = inherited(item, "toJSON", WITHHELD_ACCESSOR);
2096
2233
  if (toJSON === WITHHELD_ACCESSOR)
2097
- return withheld(owner);
2234
+ return withheld(frame, slot, owner);
2098
2235
  if (typeof toJSON === "function") {
2099
2236
  const result = toJSON.call(item, String(slot));
2100
2237
  if (result !== null && typeof result === "object")
@@ -2111,7 +2248,7 @@ export class SentSecrets {
2111
2248
  const list = (item, frame, slot, depth, level, owner, owned) => {
2112
2249
  const length = item.length;
2113
2250
  if (!fits(length))
2114
- return withheld(owner);
2251
+ return withheld(frame, slot, owner);
2115
2252
  const own = walked(frame, item, slot, owned);
2116
2253
  for (let index = 0; index < length; index++) {
2117
2254
  const descriptor = Object.getOwnPropertyDescriptor(item, index);
@@ -2120,7 +2257,7 @@ export class SentSecrets {
2120
2257
  // and copied in, an accessor is withheld unread, and nothing leaves the hole as it is.
2121
2258
  const found = owned ? undefined : inherited(item, String(index), WITHHELD_ACCESSOR);
2122
2259
  if (found === WITHHELD_ACCESSOR)
2123
- replace(own, index, REDACTED, owner);
2260
+ replace(own, index, withheld(own, index, owner), owner);
2124
2261
  else if (found !== undefined) {
2125
2262
  replaced.push([own, index, found]);
2126
2263
  const next = visit(found, own, index, depth + 1, level, owner, owned);
@@ -2130,7 +2267,7 @@ export class SentSecrets {
2130
2267
  continue;
2131
2268
  }
2132
2269
  if (!("value" in descriptor)) {
2133
- replace(own, index, REDACTED, owner);
2270
+ replace(own, index, withheld(own, index, owner), owner);
2134
2271
  continue;
2135
2272
  }
2136
2273
  const next = visit(descriptor.value, own, index, depth + 1, level, owner, owned);
@@ -2147,7 +2284,7 @@ export class SentSecrets {
2147
2284
  const record = (item, error, frame, slot, depth, level, owner, owned) => {
2148
2285
  const names = Object.getOwnPropertyNames(item);
2149
2286
  if (!fits(names.length))
2150
- return withheld(owner);
2287
+ return withheld(frame, slot, owner);
2151
2288
  const prototype = Object.getPrototypeOf(item);
2152
2289
  let plain = owned ||
2153
2290
  (!error &&
@@ -2244,9 +2381,20 @@ export class SentSecrets {
2244
2381
  else if (typeof value === "string" && binaryField(field, key))
2245
2382
  next = declared(value, target, name, owner);
2246
2383
  else {
2384
+ const start = parts.length;
2385
+ const nestedStart = nested.length;
2247
2386
  next = visit(value, target, name, depth + 1, level, owner, owned);
2248
- if (error && key === "name" && typeof value === "string" && target === own)
2249
- named.push([out, name, value]);
2387
+ if (error && key === "name" && typeof value === "string" && target === own) {
2388
+ if (parts.length === start)
2389
+ leaf("", TEXT, target, name, owner);
2390
+ named.push({
2391
+ copy: out,
2392
+ slot: name,
2393
+ start,
2394
+ end: parts.length,
2395
+ withheld: next !== value || nested.slice(nestedStart).some((entry) => entry.dirty),
2396
+ });
2397
+ }
2250
2398
  }
2251
2399
  if (target !== own)
2252
2400
  continue;
@@ -2262,7 +2410,25 @@ export class SentSecrets {
2262
2410
  };
2263
2411
  const root = { target: { value: undefined }, shared: false, parent: undefined, slot: "value" };
2264
2412
  root.target["value"] = visit(value, root, "value", 0, 0, -1, false);
2265
- const safe = this.checked(parts, kinds, scan);
2413
+ const normalize = named.length
2414
+ ? (lane) => {
2415
+ for (const entry of named) {
2416
+ for (let index = entry.start; !entry.withheld && index < entry.end; index++)
2417
+ if (lane[index] !== parts[index])
2418
+ entry.withheld = true;
2419
+ if (entry.withheld)
2420
+ for (let index = entry.start; index < entry.end; index++)
2421
+ lane[index] = index === entry.start ? REDACTED : "";
2422
+ }
2423
+ }
2424
+ : undefined;
2425
+ let lane = parts;
2426
+ if (elements.size) {
2427
+ const original = parts.map((part, index) => elements.get(index) ?? part);
2428
+ const checked = this.checked(original, kinds, scan, false, normalize);
2429
+ lane = checked.map((part, index) => (part === original[index] ? parts[index] : part));
2430
+ }
2431
+ const safe = this.checked(lane, kinds, scan, false, normalize);
2266
2432
  // Structure first, then the leaves inside it and typed binary, then nested JSON from the
2267
2433
  // innermost out.
2268
2434
  // A slot replaced by a toJSON result walked there takes the copy that result already got,
@@ -2273,7 +2439,6 @@ export class SentSecrets {
2273
2439
  }
2274
2440
  for (const frame of copied)
2275
2441
  writable(frame);
2276
- let marker;
2277
2442
  for (let index = 0; index < parts.length; index++) {
2278
2443
  const next = safe[index];
2279
2444
  const kind = kinds[index];
@@ -2283,34 +2448,20 @@ export class SentSecrets {
2283
2448
  continue;
2284
2449
  }
2285
2450
  const holder = writable(frames[index]);
2286
- // Declared binary is withheld as the encoded marker, itself checked; a typed number on an
2287
- // error is dropped rather than turned into text.
2288
- if (kind === BINARY) {
2289
- const encoded = btoa(REDACTED);
2290
- marker ??= this.checked([encoded], [BINARY], scan)[0] === encoded ? encoded : "";
2291
- }
2292
- put(holder, slots[index], kind === BINARY ? marker : kind === NUMBER && holder instanceof Error ? undefined : next);
2451
+ // A typed number on an error is dropped rather than turned into text.
2452
+ put(holder, slots[index], kind === ERROR_NUMBER ? undefined : next);
2293
2453
  touch(owners[index]);
2294
2454
  }
2295
- for (const [frame, slot, owner, start, end] of binaries) {
2296
- let clean = true;
2297
- for (let index = start; clean && index < end; index++)
2298
- clean = safe[index] === parts[index];
2299
- if (clean)
2300
- continue;
2301
- put(writable(frame), slot, REDACTED);
2302
- touch(owner);
2303
- }
2304
2455
  for (let index = nested.length - 1; index >= 0; index--) {
2305
2456
  const entry = nested[index];
2306
- if (!entry.crossing && !entry.dirty)
2457
+ if (!entry.dirty)
2307
2458
  continue;
2308
- put(writable(entry.frame), entry.slot, entry.crossing ? REDACTED : JSON.stringify(entry.json));
2459
+ put(writable(entry.frame), entry.slot, JSON.stringify(entry.json));
2309
2460
  touch(entry.owner);
2310
2461
  }
2311
- for (const [copy, slot, name] of named)
2312
- if (copy[slot] !== name)
2313
- put(copy, slot, REDACTED);
2462
+ for (const entry of named)
2463
+ if (entry.withheld)
2464
+ put(entry.copy, entry.slot, REDACTED);
2314
2465
  return { value: root.target["value"], changed };
2315
2466
  }
2316
2467
  }
@@ -2361,9 +2512,6 @@ export function registerCredentialValues(secrets, values) {
2361
2512
  secrets.add(value);
2362
2513
  }
2363
2514
  }
2364
- export function trackCredentialReads(ctx) {
2365
- wrapViewCredentials(ctx);
2366
- }
2367
2515
  /** A connector's result or error as it enters: every connector's credentials and the
2368
2516
  * request's private arguments, under payload rules. */
2369
2517
  export function redactSentSecrets(ctx, value) {
@@ -2379,9 +2527,7 @@ export function requestSentSecrets(ctx) {
2379
2527
  * extracted from. Redact it whole first: a cut through an echoed form can leave a remnant
2380
2528
  * shorter than any fragment the output boundary matches, and a normalized number or
2381
2529
  * extracted substring may no longer hold the form at all. */
2382
- export function redactBeforeCut(ctx, value) {
2383
- return requestSentSecrets(ctx).redact(value);
2384
- }
2530
+ export const redactBeforeCut = redactSentSecrets;
2385
2531
  /** Base64, or base64url with `url`, of bytes or UTF-8 text Connecta returns to an agent: a
2386
2532
  * download, an attachment, a REST body, a page cursor. Every such encoding goes through here,
2387
2533
  * since no later pass reads through an encoding the output does not declare: the content is
package/dist/server.js CHANGED
@@ -35,7 +35,7 @@ export function createFetchHandler(opts) {
35
35
  // Which sandbox, not how it is tuned: `connecta doctor` reports the
36
36
  // executor it just exercised rather than assuming one, and a
37
37
  // deployment whose executor identifies as nothing omits the key
38
- // instead of inviting a guess (#368).
38
+ // instead of inviting a guess.
39
39
  ...(opts.executorName !== undefined ? { executor: { name: opts.executorName } } : {}),
40
40
  admission: {
41
41
  policy: "global-fifo",
@@ -64,7 +64,7 @@ export function createFetchHandler(opts) {
64
64
  const defer = runtimeContext ? runtimeContext.waitUntil.bind(runtimeContext) : undefined;
65
65
  const originRefusal = routeMcp.rejectOrigin(request);
66
66
  if (originRefusal) {
67
- return Effect.succeed(withSecurityHeaders(originRefusal, url, path));
67
+ return Effect.succeed(withSecurityHeaders(originRefusal, url));
68
68
  }
69
69
  // Container and orchestrator probes reach /health over plain HTTP on
70
70
  // loopback, where no proxy has set X-Forwarded-Proto. Redirecting them to
@@ -87,7 +87,7 @@ export function createFetchHandler(opts) {
87
87
  return Effect.succeed(withSecurityHeaders(new Response(null, {
88
88
  status: 308,
89
89
  headers: { Location: target.toString() },
90
- }), url, path));
90
+ }), url));
91
91
  }
92
92
  const context = {
93
93
  request,
@@ -98,7 +98,7 @@ export function createFetchHandler(opts) {
98
98
  defer,
99
99
  runtimeContext,
100
100
  };
101
- return Effect.map(route(context), (response) => withSecurityHeaders(response, url, path));
101
+ return Effect.map(route(context), (response) => withSecurityHeaders(response, url));
102
102
  });
103
103
  return function fetch(request, runtimeContext) {
104
104
  return runEdge(serve(request, runtimeContext), { signal: request.signal });