@sema-agent/server 7.135.0 → 7.136.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 (185) hide show
  1. package/MIGRATION.md +64 -2
  2. package/USAGE.md +62 -25
  3. package/deploy/sema-up/README.md +1 -1
  4. package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-card.d.ts +60 -4
  7. package/dist/approval-card.js +18 -1
  8. package/dist/approval-content-kind.d.ts +1 -0
  9. package/dist/approval-content-kind.js +1 -1
  10. package/dist/auth-bridge.d.ts +34 -5
  11. package/dist/auth-bridge.js +20 -3
  12. package/dist/boot/budget-tracing.d.ts +1 -2
  13. package/dist/boot/budget-tracing.js +2 -2
  14. package/dist/boot/org-memory.js +1 -1
  15. package/dist/boot/resolve-spec.js +10 -7
  16. package/dist/boot/runner-deps.js +4 -2
  17. package/dist/boot/stage-04-stores.d.ts +1 -1
  18. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  19. package/dist/boot/stage-06-runners.d.ts +1 -1
  20. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  21. package/dist/boot/stage-08-reapers.d.ts +1 -1
  22. package/dist/boot/stage-09-leader.d.ts +1 -1
  23. package/dist/boot/stage-10-http-server.d.ts +1 -1
  24. package/dist/boot/stores.js +2 -3
  25. package/dist/boot/web-admin.js +10 -17
  26. package/dist/brain.js +12 -3
  27. package/dist/budget.d.ts +3 -3
  28. package/dist/budget.js +4 -0
  29. package/dist/capabilities/mcp-probe.d.ts +5 -4
  30. package/dist/capability-facts.d.ts +3 -7
  31. package/dist/capability-facts.js +3 -5
  32. package/dist/center-credential.d.ts +18 -4
  33. package/dist/center-credential.js +18 -3
  34. package/dist/config-catalog.js +12 -10
  35. package/dist/config-d-migration.js +1 -1
  36. package/dist/config-invariants.d.ts +1 -1
  37. package/dist/config-invariants.js +8 -0
  38. package/dist/config-lkg.d.ts +0 -38
  39. package/dist/config-lkg.js +1 -83
  40. package/dist/config-types.d.ts +12 -4
  41. package/dist/config.js +26 -8
  42. package/dist/deployment-shape.d.ts +1 -1
  43. package/dist/elicitation.d.ts +8 -7
  44. package/dist/elicitation.js +9 -8
  45. package/dist/file-history-disclosure.d.ts +6 -4
  46. package/dist/file-history-disclosure.js +22 -6
  47. package/dist/fleet-client.d.ts +32 -1
  48. package/dist/fleet-client.js +24 -4
  49. package/dist/fleet-lease.d.ts +7 -3
  50. package/dist/fleet-lease.js +31 -23
  51. package/dist/hooks/hook-llm.js +14 -8
  52. package/dist/hooks/hook-runner.d.ts +4 -4
  53. package/dist/hooks/hook-runner.js +2 -2
  54. package/dist/http/checkpoint-ctx.d.ts +28 -9
  55. package/dist/http/checkpoint-ctx.js +5 -1
  56. package/dist/http/dispatch.js +9 -9
  57. package/dist/http/resume-ctx-read.d.ts +2 -16
  58. package/dist/http/resume-ctx-read.js +2 -3
  59. package/dist/http/routes/approvals-assistant.d.ts +0 -2
  60. package/dist/http/routes/approvals-assistant.js +47 -12
  61. package/dist/http/routes/runs.js +2 -2
  62. package/dist/http/routes/session-sync.js +31 -9
  63. package/dist/http/routes/sessions.js +18 -14
  64. package/dist/http/submit-ctx.js +1 -1
  65. package/dist/http/wire-types.d.ts +5 -3
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.js +1 -0
  68. package/dist/model-route-gap.d.ts +49 -4
  69. package/dist/model-route-gap.js +34 -1
  70. package/dist/object-store-buckets.d.ts +13 -0
  71. package/dist/object-store-buckets.js +3 -0
  72. package/dist/observability/fail-open.d.ts +6 -6
  73. package/dist/observability/fail-open.js +6 -6
  74. package/dist/observability/metrics.js +1 -1
  75. package/dist/observability/principal-context.d.ts +4 -1
  76. package/dist/observability/principal-context.js +6 -1
  77. package/dist/parked-decide.d.ts +4 -3
  78. package/dist/parked-decide.js +26 -7
  79. package/dist/peer-directory.d.ts +2 -1
  80. package/dist/peer-directory.js +5 -5
  81. package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
  82. package/dist/plugins/approval-ask-store-sql.js +3 -0
  83. package/dist/plugins/blob-backend.js +2 -1
  84. package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
  85. package/dist/plugins/checkpoint-index-columns.js +28 -0
  86. package/dist/plugins/checkpoint-store-sql.d.ts +27 -3
  87. package/dist/plugins/checkpoint-store-sql.js +58 -29
  88. package/dist/plugins/file-history-store-sql.d.ts +6 -0
  89. package/dist/plugins/file-history-store-sql.js +30 -23
  90. package/dist/plugins/local-checkpoint-store.js +4 -3
  91. package/dist/plugins/one-time-migrations.d.ts +21 -28
  92. package/dist/plugins/one-time-migrations.js +39 -123
  93. package/dist/plugins/remote-env-k8s.d.ts +18 -5
  94. package/dist/plugins/remote-env-k8s.js +24 -7
  95. package/dist/plugins/s3-answer.d.ts +15 -0
  96. package/dist/plugins/s3-answer.js +5 -0
  97. package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
  98. package/dist/plugins/snapshot-bucket-presence.js +35 -7
  99. package/dist/plugins/sql-errors.d.ts +39 -0
  100. package/dist/plugins/sql-errors.js +49 -1
  101. package/dist/plugins/store-fault.d.ts +4 -0
  102. package/dist/plugins/store-fault.js +8 -0
  103. package/dist/plugins/tidb-pool.js +1 -1
  104. package/dist/plugins/web-search.d.ts +1 -1
  105. package/dist/plugins/web-search.js +2 -2
  106. package/dist/request-declaration-refusal.d.ts +94 -0
  107. package/dist/request-declaration-refusal.js +78 -0
  108. package/dist/request-image-urls.d.ts +6 -0
  109. package/dist/request-image-urls.js +27 -0
  110. package/dist/request-key-closure.d.ts +5 -0
  111. package/dist/request-key-closure.js +4 -1
  112. package/dist/runtime-caps-resolver.js +1 -1
  113. package/dist/sandbox-pkg-source.d.ts +28 -1
  114. package/dist/sandbox-pkg-source.js +1 -2
  115. package/dist/sealed-key.d.ts +19 -0
  116. package/dist/sealed-key.js +47 -4
  117. package/dist/session-incarnation.d.ts +16 -5
  118. package/dist/session-incarnation.js +7 -4
  119. package/dist/session-row.d.ts +3 -17
  120. package/dist/session-row.js +4 -0
  121. package/dist/session-sync.d.ts +15 -3
  122. package/dist/session-sync.js +5 -2
  123. package/dist/task-a2a.d.ts +52 -81
  124. package/dist/task-a2a.js +219 -89
  125. package/dist/task-mcp.d.ts +6 -14
  126. package/dist/task-mcp.js +16 -51
  127. package/dist/tool-approval.d.ts +23 -10
  128. package/dist/tool-approval.js +51 -36
  129. package/dist/trace/core-keyset-guard.d.ts +2 -2
  130. package/dist/trace/exit-seat.js +37 -13
  131. package/dist/trace/redact.d.ts +28 -12
  132. package/dist/trace/redact.js +233 -94
  133. package/dist/trace/self-minted-wire.d.ts +267 -0
  134. package/dist/trace/self-minted-wire.js +79 -0
  135. package/dist/url-credential-refusal.d.ts +52 -0
  136. package/dist/url-credential-refusal.js +14 -0
  137. package/dist/url-refusal.d.ts +13 -4
  138. package/dist/url-refusal.js +8 -2
  139. package/dist/workspace-space.d.ts +27 -0
  140. package/dist/workspace-space.js +26 -0
  141. package/package.json +3 -2
  142. package/scripts/drain-check-local.mjs +2 -1
  143. package/scripts/drain-check.mysql.sql +3 -1
  144. package/scripts/drain-check.pg.sql +3 -1
  145. package/skills/schedule.md +1 -1
  146. package/wire-changes/server@7.104.0.json +134 -0
  147. package/wire-changes/server@7.105.0.json +53 -0
  148. package/wire-changes/server@7.106.0.json +475 -0
  149. package/wire-changes/server@7.107.0.json +64 -0
  150. package/wire-changes/server@7.108.0.json +153 -0
  151. package/wire-changes/server@7.109.0.json +46 -0
  152. package/wire-changes/server@7.110.0.json +90 -0
  153. package/wire-changes/server@7.111.0.json +86 -0
  154. package/wire-changes/server@7.112.0.json +60 -0
  155. package/wire-changes/server@7.113.0.json +26 -0
  156. package/wire-changes/server@7.114.0.json +26 -0
  157. package/wire-changes/server@7.115.0.json +97 -0
  158. package/wire-changes/server@7.116.0.json +218 -0
  159. package/wire-changes/server@7.116.1.json +26 -0
  160. package/wire-changes/server@7.117.0.json +65 -0
  161. package/wire-changes/server@7.118.0.json +129 -0
  162. package/wire-changes/server@7.119.0.json +65 -0
  163. package/wire-changes/server@7.120.0.json +89 -0
  164. package/wire-changes/server@7.121.0.json +219 -0
  165. package/wire-changes/server@7.122.0.json +54 -0
  166. package/wire-changes/server@7.123.0.json +26 -0
  167. package/wire-changes/server@7.124.0.json +336 -0
  168. package/wire-changes/server@7.124.1.json +26 -0
  169. package/wire-changes/server@7.125.0.json +111 -0
  170. package/wire-changes/server@7.126.0.json +285 -0
  171. package/wire-changes/server@7.127.0.json +77 -0
  172. package/wire-changes/server@7.127.1.json +26 -0
  173. package/wire-changes/server@7.127.2.json +53 -0
  174. package/wire-changes/server@7.128.0.json +26 -0
  175. package/wire-changes/server@7.129.0.json +106 -0
  176. package/wire-changes/server@7.129.1.json +26 -0
  177. package/wire-changes/server@7.130.0.json +26 -0
  178. package/wire-changes/server@7.131.0.json +118 -0
  179. package/wire-changes/server@7.132.0.json +226 -0
  180. package/wire-changes/server@7.133.0.json +1155 -0
  181. package/wire-changes/server@7.134.0.json +107 -0
  182. package/wire-changes/server@7.135.0.json +174 -0
  183. package/wire-changes/server@7.136.0.json +448 -0
  184. package/dist/url-credential.d.ts +0 -52
  185. package/dist/url-credential.js +0 -49
@@ -1,5 +1,5 @@
1
1
  import { displayUrlOf, stripFormatCharacters } from "@sema-agent/core";
2
- import { CONTROL_AND_BIDI_CHARS, isHierarchicalUrl } from "@sema-agent/settings-schema";
2
+ import { CONTROL_AND_BIDI_CHARS, CREDENTIAL_PARAM_NAMES, isHierarchicalUrl } from "@sema-agent/settings-schema";
3
3
  import { recordFailOpen } from "../observability/fail-open.js";
4
4
  const normalizeForRedaction = stripFormatCharacters;
5
5
  export function mapNormalizedToOriginal(text, normalized) {
@@ -836,15 +836,14 @@ const CREDENTIAL_NAME_ANCHORS = {
836
836
  "x-amz-signature": "structure-only", "x-amz-security-token": "structure-only", "x-amz-credential": "structure-only",
837
837
  sas: "structure-only",
838
838
  };
839
- const CREDENTIAL_NAME_WORDS = Object.keys(CREDENTIAL_NAME_ANCHORS);
840
839
  const CREDENTIAL_NAME_ALT = (() => {
841
- for (const w of CREDENTIAL_NAME_WORDS) {
840
+ for (const w of CREDENTIAL_PARAM_NAMES) {
842
841
  if (!/^[a-z][a-z0-9_-]*$/.test(w))
843
842
  throw new Error(`redact: credential name ${JSON.stringify(w)} must be lowercase [a-z0-9_-] (the alternation is built without escaping)`);
844
843
  }
845
- return [...CREDENTIAL_NAME_WORDS].sort((a, b) => b.length - a.length || (a < b ? -1 : 1)).join("|");
844
+ return [...CREDENTIAL_PARAM_NAMES].sort((a, b) => b.length - a.length || (a < b ? -1 : 1)).join("|");
846
845
  })();
847
- const CREDENTIAL_NAME_MAX = Math.max(...CREDENTIAL_NAME_WORDS.map((w) => w.length));
846
+ const CREDENTIAL_NAME_MAX = Math.max(...CREDENTIAL_PARAM_NAMES.map((w) => w.length));
848
847
  const asciiLower = (s) => s.replace(/[A-Z]/g, (c) => c.toLowerCase());
849
848
  function credentialNameAnchor(name) {
850
849
  return CREDENTIAL_ANCHOR_OF.get(asciiLower(name));
@@ -1085,14 +1084,17 @@ function isPemFenceLabel(s, from, to) {
1085
1084
  return false;
1086
1085
  if (!s.startsWith(PEM_KEY_WORD, end - PEM_KEY_WORD.length))
1087
1086
  return false;
1087
+ return !pemLabelDashRuleBroken(s, from, to);
1088
+ }
1089
+ function pemLabelDashRuleBroken(s, from, to) {
1088
1090
  if (s.charCodeAt(from) === 45)
1089
- return false;
1091
+ return true;
1090
1092
  if (s.charCodeAt(from + 1) === 45)
1091
- return false;
1093
+ return true;
1092
1094
  for (let i = from + 1; i < to; i++)
1093
1095
  if (s.charCodeAt(i) === 45 && s.charCodeAt(i + 1) === 45)
1094
- return false;
1095
- return true;
1096
+ return true;
1097
+ return false;
1096
1098
  }
1097
1099
  const PEM_FENCE_OPEN = "-----BEGIN";
1098
1100
  const PEM_FENCE_CLOSE = "-----";
@@ -1137,7 +1139,7 @@ function unitCountWithin(scan, from, to, max) {
1137
1139
  }
1138
1140
  return n <= max;
1139
1141
  }
1140
- function collectSsh2FenceSpans(scan, text, span) {
1142
+ function collectSsh2FenceSpans(scan, text, span, isHead) {
1141
1143
  let cachedFrom = 0;
1142
1144
  let cachedAt;
1143
1145
  const nextClose = (from) => {
@@ -1163,6 +1165,10 @@ function collectSsh2FenceSpans(scan, text, span) {
1163
1165
  continue;
1164
1166
  }
1165
1167
  const bodyFrom = fence + SSH2_FENCE_OPEN.length;
1168
+ if (isHead && unitCountWithin(scan, bodyFrom, text.length, SSH2_BODY_MAX)) {
1169
+ span(fence, text.length);
1170
+ return;
1171
+ }
1166
1172
  const close = nextClose(bodyFrom);
1167
1173
  if (close < 0)
1168
1174
  return;
@@ -1177,11 +1183,20 @@ function collectSsh2FenceSpans(scan, text, span) {
1177
1183
  }
1178
1184
  const PEM_END_OPEN = "-----END";
1179
1185
  const PEM_FENCE_BODY_MAX = 8192;
1186
+ const PEM_LINE_INVALID = -1;
1187
+ const PEM_LINE_OPEN = -2;
1180
1188
  function pemFenceLineEnd(text, labelFrom) {
1181
1189
  const close = text.indexOf(PEM_FENCE_CLOSE, labelFrom);
1182
- return close >= 0 && isPemFenceLabel(text, labelFrom, close) ? close + PEM_FENCE_CLOSE.length : -1;
1190
+ if (close < 0) {
1191
+ let to = text.length;
1192
+ while (to > labelFrom && text.charCodeAt(to - 1) === 45)
1193
+ to -= 1;
1194
+ const stillPossible = text.length - to >= 2 ? isPemFenceLabel(text, labelFrom, to) : !pemLabelDashRuleBroken(text, labelFrom, to);
1195
+ return stillPossible ? PEM_LINE_OPEN : PEM_LINE_INVALID;
1196
+ }
1197
+ return isPemFenceLabel(text, labelFrom, close) ? close + PEM_FENCE_CLOSE.length : PEM_LINE_INVALID;
1183
1198
  }
1184
- function collectPemFenceSpans(text, span) {
1199
+ function collectPemFenceSpans(text, span, isHead) {
1185
1200
  let endFrom = -1;
1186
1201
  let endAt = -1;
1187
1202
  let endAfter = -1;
@@ -1201,13 +1216,17 @@ function collectPemFenceSpans(text, span) {
1201
1216
  if (fence < 0)
1202
1217
  return;
1203
1218
  const bodyFrom = pemFenceLineEnd(text, fence + PEM_FENCE_OPEN.length);
1204
- if (bodyFrom < 0) {
1219
+ if (bodyFrom === PEM_LINE_INVALID) {
1205
1220
  from = fence + 1;
1206
1221
  continue;
1207
1222
  }
1208
- earliestEndFence(bodyFrom);
1209
- if (endAt < 0)
1223
+ if (bodyFrom !== PEM_LINE_OPEN)
1224
+ earliestEndFence(bodyFrom);
1225
+ if (bodyFrom === PEM_LINE_OPEN || endAt < 0) {
1226
+ if (isHead)
1227
+ span(fence, text.length);
1210
1228
  return;
1229
+ }
1211
1230
  if (endAt - bodyFrom > PEM_FENCE_BODY_MAX) {
1212
1231
  from = fence + 1;
1213
1232
  continue;
@@ -1248,9 +1267,19 @@ function collectPemFragmentSpans(scan, text, json, span) {
1248
1267
  cur = next;
1249
1268
  }
1250
1269
  }
1251
- function collectHeadlessPpkSpans(scan, text, span) {
1270
+ function collectHeadlessPpkSpans(scan, text, span, isHead) {
1252
1271
  const KEY = "Private-Lines: ";
1253
1272
  const n = text.length;
1273
+ const LINE_NONE = -1;
1274
+ const LINE_OPEN = -2;
1275
+ const openAt = (i) => {
1276
+ if (!isHead)
1277
+ return false;
1278
+ let k = i;
1279
+ while (k < n && opaqueUnitEnd(scan, k) < 0 && text.charCodeAt(k) === 92)
1280
+ k += 1;
1281
+ return k >= n;
1282
+ };
1254
1283
  const isB64 = (c) => (c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === 43 || c === 47 || c === 61;
1255
1284
  const b64At = (i) => opaqueUnitEnd(scan, i) < 0 && isB64(text.charCodeAt(i));
1256
1285
  const skipHs = (i) => { while (i < n && opaqueUnitEnd(scan, i) < 0 && (text.charCodeAt(i) === 32 || text.charCodeAt(i) === 9))
@@ -1289,7 +1318,9 @@ function collectHeadlessPpkSpans(scan, text, span) {
1289
1318
  seen = true;
1290
1319
  q = skipHs(q);
1291
1320
  }
1292
- return seen ? q : -1;
1321
+ if (openAt(q))
1322
+ return LINE_OPEN;
1323
+ return seen ? q : LINE_NONE;
1293
1324
  };
1294
1325
  const escapedNewlineLen = (i) => {
1295
1326
  if (opaqueUnitEnd(scan, i) >= 0)
@@ -1304,13 +1335,35 @@ function collectHeadlessPpkSpans(scan, text, span) {
1304
1335
  };
1305
1336
  const atLineEnd = (i) => {
1306
1337
  const b = skipHs(i);
1307
- if (b >= n)
1338
+ if (b >= n || openAt(b))
1308
1339
  return true;
1309
1340
  if (opaqueUnitEnd(scan, b) >= 0)
1310
1341
  return false;
1311
1342
  const c = text.charCodeAt(b);
1312
1343
  return c === 10 || c === 13 || c === 34 || escapedNewlineLen(b) > 0;
1313
1344
  };
1345
+ const bodyLineAt = (from) => {
1346
+ const q0 = skipNewlines(from);
1347
+ if (q0 < 0)
1348
+ return q0;
1349
+ let q = q0;
1350
+ const mk = opaqueUnitEnd(scan, q) < 0 ? text.charCodeAt(q) : -1;
1351
+ if (mk === 43 || mk === 45)
1352
+ q = skipHs(q + 1);
1353
+ let r = q;
1354
+ while (r < n && b64At(r))
1355
+ r += 1;
1356
+ if (r - q < 4) {
1357
+ if (isHead && r >= n)
1358
+ return LINE_OPEN;
1359
+ r = q0;
1360
+ while (r < n && b64At(r))
1361
+ r += 1;
1362
+ if (r - q0 < 4)
1363
+ return LINE_NONE;
1364
+ }
1365
+ return atLineEnd(r) ? r : LINE_NONE;
1366
+ };
1314
1367
  let search = 0;
1315
1368
  for (;;) {
1316
1369
  const at = text.indexOf(KEY, search);
@@ -1324,36 +1377,21 @@ function collectHeadlessPpkSpans(scan, text, span) {
1324
1377
  const d0 = p;
1325
1378
  while (p < n && opaqueUnitEnd(scan, p) < 0 && text.charCodeAt(p) >= 48 && text.charCodeAt(p) <= 57)
1326
1379
  p += 1;
1327
- if (p === d0) {
1380
+ if (p === d0 && p < n) {
1328
1381
  search = at + KEY.length;
1329
1382
  continue;
1330
1383
  }
1331
- let end = p;
1332
- let lines = 0;
1333
- for (;;) {
1334
- const q0 = skipNewlines(end);
1335
- if (q0 === -1)
1384
+ let end = -1;
1385
+ for (let from = p;;) {
1386
+ const r = bodyLineAt(from);
1387
+ if (r === LINE_NONE)
1336
1388
  break;
1337
- let q = q0;
1338
- const mk = opaqueUnitEnd(scan, q) < 0 ? text.charCodeAt(q) : -1;
1339
- if (mk === 43 || mk === 45)
1340
- q = skipHs(q + 1);
1341
- let r = q;
1342
- while (r < n && b64At(r))
1343
- r += 1;
1344
- if (r - q < 4) {
1345
- r = q0;
1346
- while (r < n && b64At(r))
1347
- r += 1;
1348
- if (r - q0 < 4)
1349
- break;
1350
- }
1351
- if (!atLineEnd(r))
1389
+ end = r === LINE_OPEN ? n : r;
1390
+ if (r === LINE_OPEN)
1352
1391
  break;
1353
- lines += 1;
1354
- end = r;
1392
+ from = r;
1355
1393
  }
1356
- if (lines === 0) {
1394
+ if (end < 0) {
1357
1395
  search = p;
1358
1396
  continue;
1359
1397
  }
@@ -1442,10 +1480,6 @@ const isRegexSpace = (c) => c === 32 || (c >= 9 && c <= 13) || c === 0xa0 || c =
1442
1480
  const isLineTerminator = (c) => c === 10 || c === 13 || c === 0x2028 || c === 0x2029;
1443
1481
  const isControlCode = (c) => c < 0x20 || c === 0x7f;
1444
1482
  const TOKEN_UNIT_Y = new RegExp(REDACTION_TOKEN_RE_SRC, "y");
1445
- export const CREDENTIAL_PARAM_NAMES = Object.freeze([...CREDENTIAL_NAME_WORDS]);
1446
- export function isCredentialParamName(decodedName) {
1447
- return credentialNameAnchor(decodedName) !== undefined;
1448
- }
1449
1483
  const CREDENTIAL_NAME_SCAN_MAX = CREDENTIAL_NAME_MAX * 3 + 1;
1450
1484
  function decodeAsciiName(name) {
1451
1485
  let out = "";
@@ -1757,10 +1791,12 @@ function scanUrlRun(scan, text, i, kind, json) {
1757
1791
  noteEvidenceRead(scan, i);
1758
1792
  return i;
1759
1793
  }
1760
- function matchUserinfoAt(scan, text, p, json) {
1794
+ function matchUserinfoAt(scan, text, p, json, isHead) {
1761
1795
  const probeEnd = scanUrlRun(scan, text, p, "plain-user", json);
1762
1796
  const keyId = probeEnd > p && text.charCodeAt(probeEnd) === 58 && credentialSpansCover(scan, p, probeEnd);
1763
- scanUrlRun(scan, text, p, keyId ? "token-pass" : "plain-pass", json);
1797
+ const runEnd = scanUrlRun(scan, text, p, keyId ? "token-pass" : "plain-pass", json);
1798
+ if (isHead && runEnd >= text.length && runEnd > p)
1799
+ return runEnd;
1764
1800
  const at = scan.lastAt;
1765
1801
  if (at <= p)
1766
1802
  return null;
@@ -1778,7 +1814,7 @@ function growUrlSegments(list) {
1778
1814
  list.a = bigger;
1779
1815
  return true;
1780
1816
  }
1781
- function collectUrlCredentialSpans(scan, text, json, span) {
1817
+ function collectUrlCredentialSpans(scan, text, json, span, isHead) {
1782
1818
  const dirty = scan.dirty;
1783
1819
  const hr = scan.headsRead;
1784
1820
  const hw = scan.headsWrite;
@@ -1814,7 +1850,7 @@ function collectUrlCredentialSpans(scan, text, json, span) {
1814
1850
  else {
1815
1851
  scan.footLo = FOOT_NONE;
1816
1852
  scan.footHi = -1;
1817
- at = matchUserinfoAt(scan, text, start, json) ?? -1;
1853
+ at = matchUserinfoAt(scan, text, start, json, isHead) ?? -1;
1818
1854
  lo = scan.footLo;
1819
1855
  hi = scan.footHi;
1820
1856
  urlSegmentRuns += 1;
@@ -1915,7 +1951,7 @@ function jsonSecretFieldValueQuoteAt(text, p) {
1915
1951
  i += 1;
1916
1952
  return text.charCodeAt(i) === CH_QUOTE ? i : -1;
1917
1953
  }
1918
- function jsonStringEndAt(text, q) {
1954
+ function jsonStringEndAt(text, q, isHead) {
1919
1955
  const n = text.length;
1920
1956
  for (let i = q + 1; i < n;) {
1921
1957
  const c = text.charCodeAt(i);
@@ -1926,16 +1962,16 @@ function jsonStringEndAt(text, q) {
1926
1962
  i += 2;
1927
1963
  continue;
1928
1964
  }
1929
- return -1;
1965
+ return isHead && i + 1 >= n ? n : -1;
1930
1966
  }
1931
1967
  i += 1;
1932
1968
  }
1933
- return -1;
1969
+ return isHead ? n : -1;
1934
1970
  }
1935
- function collectJsonSecretFieldSpans(text, span) {
1971
+ function collectJsonSecretFieldSpans(text, span, isHead) {
1936
1972
  for (let p = text.indexOf(JSON_FIELD_ANCHOR); p >= 0;) {
1937
1973
  const q = jsonSecretFieldValueQuoteAt(text, p);
1938
- const end = q < 0 ? -1 : jsonStringEndAt(text, q);
1974
+ const end = q < 0 ? -1 : jsonStringEndAt(text, q, isHead);
1939
1975
  if (end < 0) {
1940
1976
  p = text.indexOf(JSON_FIELD_ANCHOR, p + 1);
1941
1977
  continue;
@@ -2088,7 +2124,7 @@ function anchorHeadAt(text, op, json) {
2088
2124
  }
2089
2125
  return { strength, quotedName: quote !== 0, spacedLeft, listSep };
2090
2126
  }
2091
- function quotedValueEnd(text, cs, quote, json) {
2127
+ function quotedValueEnd(text, cs, quote, json, isHead) {
2092
2128
  const n = text.length;
2093
2129
  let i = cs;
2094
2130
  while (i < n) {
@@ -2097,18 +2133,18 @@ function quotedValueEnd(text, cs, quote, json) {
2097
2133
  if (ch === quote)
2098
2134
  return i;
2099
2135
  if (ch === JSON_STRING_END)
2100
- break;
2136
+ return -1;
2101
2137
  i += unitLenOf(unit);
2102
2138
  if (ch === CH_BACKSLASH && quote !== JSON_STRING_END) {
2103
2139
  if (i >= n)
2104
2140
  break;
2105
2141
  const escaped = readAnchorUnit(text, i, json);
2106
2142
  if (unitChOf(escaped) === JSON_STRING_END)
2107
- break;
2143
+ return -1;
2108
2144
  i += unitLenOf(escaped);
2109
2145
  }
2110
2146
  }
2111
- return -1;
2147
+ return isHead ? n : -1;
2112
2148
  }
2113
2149
  function bareValueEnd(text, s, json, listSep) {
2114
2150
  const n = text.length;
@@ -2153,7 +2189,7 @@ function bareValueEnd(text, s, json, listSep) {
2153
2189
  }
2154
2190
  return e;
2155
2191
  }
2156
- function literalValueAt(text, s, json, declaredIfBare, listSep) {
2192
+ function literalValueAt(text, s, json, declaredIfBare, listSep, isHead) {
2157
2193
  const n = text.length;
2158
2194
  if (s >= n)
2159
2195
  return undefined;
@@ -2166,7 +2202,7 @@ function literalValueAt(text, s, json, declaredIfBare, listSep) {
2166
2202
  return undefined;
2167
2203
  if (!isAsciiAlnumCode(unitChOf(readAnchorUnit(text, vs, json))))
2168
2204
  return undefined;
2169
- const close = quotedValueEnd(text, vs, first, json);
2205
+ const close = quotedValueEnd(text, vs, first, json, isHead);
2170
2206
  if (close >= 0)
2171
2207
  return { start: vs, end: close, quoted: true };
2172
2208
  }
@@ -2234,7 +2270,7 @@ function schemeValueAt(text, s, json) {
2234
2270
  return undefined;
2235
2271
  return { start: vs, end: e, quoted: false };
2236
2272
  }
2237
- function collectAnchoredValueSpans(text, json, span) {
2273
+ function collectAnchoredValueSpans(text, json, span, isHead) {
2238
2274
  const n = text.length;
2239
2275
  let resume = 0;
2240
2276
  let nextEq = text.indexOf("=");
@@ -2275,10 +2311,10 @@ function collectAnchoredValueSpans(text, json, span) {
2275
2311
  v = schemeValueAt(text, s, json);
2276
2312
  break;
2277
2313
  case "declarative":
2278
- v = literalValueAt(text, s, json, head.quotedName || (!head.spacedLeft && !spacedRight), head.listSep);
2314
+ v = literalValueAt(text, s, json, head.quotedName || (!head.spacedLeft && !spacedRight), head.listSep, isHead);
2279
2315
  break;
2280
2316
  case "any-form":
2281
- v = literalValueAt(text, s, json, true, head.listSep);
2317
+ v = literalValueAt(text, s, json, true, head.listSep, isHead);
2282
2318
  break;
2283
2319
  default: {
2284
2320
  const unhandled = head.strength;
@@ -2294,7 +2330,7 @@ function collectAnchoredValueSpans(text, json, span) {
2294
2330
  export function structuralScannerForTest(arm) {
2295
2331
  const collect = (scan) => (text) => {
2296
2332
  const out = [];
2297
- scan(text, (start, end) => out.push([start, end]));
2333
+ scan(text, (start, end) => out.push([start, end]), false);
2298
2334
  return out;
2299
2335
  };
2300
2336
  switch (arm) {
@@ -2307,7 +2343,7 @@ export function structuralScannerForTest(arm) {
2307
2343
  let ppkScanBytes = 0;
2308
2344
  export function ppkScanBytesForTest() { return ppkScanBytes; }
2309
2345
  export function resetPpkScanBytesForTest() { ppkScanBytes = 0; }
2310
- function collectPpkContainerSpans(scan, text, eat, span) {
2346
+ function collectPpkContainerSpans(scan, text, eat, span, isHead) {
2311
2347
  const HEAD = "PuTTY-User-Key-File-";
2312
2348
  const MAC = "Private-MAC: ";
2313
2349
  const HASH = "Private-Hash: ";
@@ -2323,9 +2359,8 @@ function collectPpkContainerSpans(scan, text, eat, span) {
2323
2359
  cache.set(needle, v);
2324
2360
  return v;
2325
2361
  };
2326
- const isVersionedHead = (at) => {
2362
+ const versionEnd = (at) => {
2327
2363
  let p = at + HEAD.length;
2328
- const start = p;
2329
2364
  while (p < text.length && opaqueUnitEnd(scan, p) < 0) {
2330
2365
  const c = text.charCodeAt(p);
2331
2366
  if (c >= 48 && c <= 57)
@@ -2333,7 +2368,15 @@ function collectPpkContainerSpans(scan, text, eat, span) {
2333
2368
  else
2334
2369
  break;
2335
2370
  }
2336
- return p > start && text.startsWith(": ", p);
2371
+ return p;
2372
+ };
2373
+ const isVersionedHead = (at) => {
2374
+ const p = versionEnd(at);
2375
+ return p > at + HEAD.length && text.startsWith(": ", p);
2376
+ };
2377
+ const isOpenHead = (at) => {
2378
+ const p = versionEnd(at);
2379
+ return p >= text.length || (p === text.length - 1 && p > at + HEAD.length && text.charCodeAt(p) === 58);
2337
2380
  };
2338
2381
  const nextVersionedHead = (from) => {
2339
2382
  let p = find(HEAD, from);
@@ -2356,7 +2399,7 @@ function collectPpkContainerSpans(scan, text, eat, span) {
2356
2399
  while (at !== Number.POSITIVE_INFINITY) {
2357
2400
  let pos = at + HEAD.length + 1;
2358
2401
  let end = text.length;
2359
- let ateToEnd = true;
2402
+ let ateToEnd = !isHead;
2360
2403
  for (;;) {
2361
2404
  const mac = find(MAC, pos);
2362
2405
  const hash = find(HASH, pos);
@@ -2389,8 +2432,9 @@ function collectPpkContainerSpans(scan, text, eat, span) {
2389
2432
  }
2390
2433
  }
2391
2434
  }
2435
+ const undecided = isHead && limit === Number.POSITIVE_INFINITY;
2392
2436
  ateToEnd = straggler < limit || !gapBlank;
2393
- end = ateToEnd ? text.length : hexEnd;
2437
+ end = ateToEnd || undecided ? text.length : hexEnd;
2394
2438
  break;
2395
2439
  }
2396
2440
  if (ateToEnd && eat !== undefined && eat.from < 0) {
@@ -2400,6 +2444,9 @@ function collectPpkContainerSpans(scan, text, eat, span) {
2400
2444
  span(at, end);
2401
2445
  at = end >= text.length ? Number.POSITIVE_INFINITY : nextVersionedHead(end);
2402
2446
  }
2447
+ const last = isHead ? text.lastIndexOf(HEAD) : -1;
2448
+ if (last >= 0 && !inOpaqueUnit(scan, last) && isOpenHead(last))
2449
+ span(last, text.length);
2403
2450
  }
2404
2451
  const armPool = (() => {
2405
2452
  const spanRepl = (token) => {
@@ -2444,7 +2491,7 @@ const armPool = (() => {
2444
2491
  order,
2445
2492
  phase: 1,
2446
2493
  lexical,
2447
- collect: (text, json, eat, out) => arm(text, json, eat, (start, end) => out.push({ start, end, repl, rank, order })),
2494
+ collect: (text, json, eat, out, isHead) => arm(text, json, eat, (start, end) => out.push({ start, end, repl, rank, order }), isHead),
2448
2495
  retry: undefined,
2449
2496
  headSrc: undefined,
2450
2497
  headFirst: undefined,
@@ -2459,7 +2506,7 @@ const armPool = (() => {
2459
2506
  order,
2460
2507
  phase: 2,
2461
2508
  lexical: false,
2462
- collect: (scan, text, json, eat, out) => arm(scan, text, json, eat, (start, end) => out.push({ start, end, repl, rank, order })),
2509
+ collect: (scan, text, json, eat, out, isHead) => arm(scan, text, json, eat, (start, end) => out.push({ start, end, repl, rank, order }), isHead),
2463
2510
  retry: undefined,
2464
2511
  headSrc: undefined,
2465
2512
  headFirst: undefined,
@@ -2467,22 +2514,22 @@ const armPool = (() => {
2467
2514
  };
2468
2515
  const structuralScannerMember = (row, order) => {
2469
2516
  if (row === PEM_FENCE_PATTERN)
2470
- return scannerMember(row[1], order, (text, _json, _eat, span) => collectPemFenceSpans(text, span));
2517
+ return scannerMember(row[1], order, (text, _json, _eat, span, isHead) => collectPemFenceSpans(text, span, isHead));
2471
2518
  if (row === SSH2_FENCE_PATTERN)
2472
- return forensicMember(row[1], order, (scan, text, _json, _eat, span) => collectSsh2FenceSpans(scan, text, span));
2519
+ return forensicMember(row[1], order, (scan, text, _json, _eat, span, isHead) => collectSsh2FenceSpans(scan, text, span, isHead));
2473
2520
  if (row === URL_CREDENTIAL_PATTERN)
2474
- return forensicMember(row[1], order, (scan, text, json, _eat, span) => collectUrlCredentialSpans(scan, text, json, span));
2521
+ return forensicMember(row[1], order, (scan, text, json, _eat, span, isHead) => collectUrlCredentialSpans(scan, text, json, span, isHead));
2475
2522
  if (row === JSON_SECRET_FIELD_PATTERN)
2476
- return scannerMember(row[1], order, (text, _json, _eat, span) => collectJsonSecretFieldSpans(text, span));
2523
+ return scannerMember(row[1], order, (text, _json, _eat, span, isHead) => collectJsonSecretFieldSpans(text, span, isHead));
2477
2524
  throw new Error(`redact: structural row /${row[0].source}/ has no hand-written scanner — structural grammars are declared as regexes but never executed as one (S-102 / S-265)`);
2478
2525
  };
2479
2526
  const members = [];
2480
2527
  for (const row of STRUCTURAL_SECRET_PATTERNS)
2481
2528
  members.push(structuralScannerMember(row, members.length));
2482
- members.push(forensicMember(PPK_CONTAINER_TOKEN, members.length, (scan, text, _json, eat, span) => collectPpkContainerSpans(scan, text, eat, span)));
2529
+ members.push(forensicMember(PPK_CONTAINER_TOKEN, members.length, (scan, text, _json, eat, span, isHead) => collectPpkContainerSpans(scan, text, eat, span, isHead)));
2483
2530
  members.push(forensicMember(PEM_FRAGMENT_TOKEN, members.length, (scan, text, json, _eat, span) => collectPemFragmentSpans(scan, text, json, span)));
2484
- members.push(forensicMember(HEADLESS_PPK_TOKEN, members.length, (scan, text, _json, _eat, span) => collectHeadlessPpkSpans(scan, text, span)));
2485
- members.push(scannerMember(ANCHORED_VALUE_TOKEN, members.length, (text, json, _eat, span) => collectAnchoredValueSpans(text, json, span), ARM_RANK.keyword, true));
2531
+ members.push(forensicMember(HEADLESS_PPK_TOKEN, members.length, (scan, text, _json, _eat, span, isHead) => collectHeadlessPpkSpans(scan, text, span, isHead)));
2532
+ members.push(scannerMember(ANCHORED_VALUE_TOKEN, members.length, (text, json, _eat, span, isHead) => collectAnchoredValueSpans(text, json, span, isHead), ARM_RANK.keyword, true));
2486
2533
  for (const row of KEYWORD_SECRET_PATTERNS)
2487
2534
  members.push(regexMember(row, ARM_RANK.keyword, members.length));
2488
2535
  for (const row of SHAPE_SECRET_PATTERNS)
@@ -2513,7 +2560,7 @@ const armPool = (() => {
2513
2560
  const headFirstUnit = unionMembers(retryMembers.map((m) => m.headFirst)) ?? new Uint32Array(0x10000 >>> 5).fill(0xffffffff);
2514
2561
  poolArmCount = members.length;
2515
2562
  poolBoundaryArmCount = retryMembers.length;
2516
- return (text, wantCount, json, eat, scope) => {
2563
+ return (text, wantCount, json, eat, scope, isHead) => {
2517
2564
  readings += 1;
2518
2565
  const lane = LANES[scope];
2519
2566
  const spans = [];
@@ -2550,7 +2597,7 @@ const armPool = (() => {
2550
2597
  }
2551
2598
  };
2552
2599
  for (const m of lane.phase1)
2553
- m.collect(text, json, eat, spans);
2600
+ m.collect(text, json, eat, spans, isHead);
2554
2601
  closeCoveragePositions(0);
2555
2602
  if (lane.phase2.length > 0) {
2556
2603
  const scan = buildForensicScan(credentialSpans(text), lane.phase2.length);
@@ -2567,7 +2614,7 @@ const armPool = (() => {
2567
2614
  }
2568
2615
  scan.footLo = FOOT_NONE;
2569
2616
  scan.footHi = -1;
2570
- lane.phase2[j].collect(scan, text, json, level === 0 ? eat : undefined, spans);
2617
+ lane.phase2[j].collect(scan, text, json, level === 0 ? eat : undefined, spans, isHead);
2571
2618
  foot[2 * j] = scan.footLo;
2572
2619
  foot[2 * j + 1] = scan.footHi;
2573
2620
  forensicRuns += 1;
@@ -2613,8 +2660,8 @@ let onRedaction;
2613
2660
  export function setRedactionObserver(fn) {
2614
2661
  onRedaction = fn;
2615
2662
  }
2616
- function applyRules(text, wantCount, json, eat, scope) {
2617
- return armPool(text, wantCount, json, eat, scope);
2663
+ function applyRules(text, wantCount, json, eat, scope, isHead) {
2664
+ return armPool(text, wantCount, json, eat, scope, isHead);
2618
2665
  }
2619
2666
  function reportHits(hits) {
2620
2667
  if (onRedaction === undefined)
@@ -2639,7 +2686,99 @@ function invisibleClassesOf(layout) {
2639
2686
  }
2640
2687
  }
2641
2688
  }
2642
- function runRedaction(text, observe, json, wantHits, scope, layout) {
2689
+ function trailingPartialEscapeStart(s) {
2690
+ const n = s.length;
2691
+ const runStart = (end) => {
2692
+ let k = end;
2693
+ while (k > 0 && s.charCodeAt(k - 1) === CH_BACKSLASH)
2694
+ k -= 1;
2695
+ return k;
2696
+ };
2697
+ const highSurrogateEscapeStartBefore = (end) => {
2698
+ let j = end;
2699
+ let value = -1;
2700
+ let bodyStart = -1;
2701
+ if (j >= 1 && s.charCodeAt(j - 1) === CH_RBRACE) {
2702
+ let k = j - 1;
2703
+ while (k > 0 && hexDigitValue(s.charCodeAt(k - 1)) >= 0)
2704
+ k -= 1;
2705
+ if (k >= 2 && s.charCodeAt(k - 1) === CH_LBRACE && s.charCodeAt(k - 2) === CH_u && j - 1 - k >= 1) {
2706
+ value = readHexRunValue(s, k, j - 1);
2707
+ bodyStart = k - 2;
2708
+ }
2709
+ }
2710
+ else if (j >= 5 && s.charCodeAt(j - 5) === CH_u) {
2711
+ value = readHexRun(s, j - 4, 4);
2712
+ bodyStart = j - 5;
2713
+ }
2714
+ if (value < 0xd800 || value > 0xdbff)
2715
+ return -1;
2716
+ const k = runStart(bodyStart);
2717
+ return k < bodyStart ? k : -1;
2718
+ };
2719
+ let j = n;
2720
+ while (j > 0 && hexDigitValue(s.charCodeAt(j - 1)) >= 0)
2721
+ j -= 1;
2722
+ const hexRun = n - j;
2723
+ let element = -1;
2724
+ if (j >= 2 && s.charCodeAt(j - 1) === CH_LBRACE && s.charCodeAt(j - 2) === CH_u)
2725
+ element = j - 2;
2726
+ else if (j >= 1 && s.charCodeAt(j - 1) === CH_u && hexRun <= 3)
2727
+ element = j - 1;
2728
+ else if (j >= 1 && s.charCodeAt(j - 1) === CH_x && hexRun <= 1)
2729
+ element = j - 1;
2730
+ else if (hexRun === 0 && n >= 1 && s.charCodeAt(n - 1) === CH_BACKSLASH)
2731
+ element = n;
2732
+ if (element >= 0) {
2733
+ const k = runStart(element);
2734
+ if (k >= element)
2735
+ return -1;
2736
+ const high = highSurrogateEscapeStartBefore(k);
2737
+ return high >= 0 ? high : k;
2738
+ }
2739
+ return highSurrogateEscapeStartBefore(n);
2740
+ }
2741
+ function readHexRunValue(s, from, to) {
2742
+ let v = 0;
2743
+ for (let k = from; k < to; k++) {
2744
+ const d = hexDigitValue(s.charCodeAt(k));
2745
+ if (d < 0)
2746
+ return -1;
2747
+ v = v * 16 + d;
2748
+ if (v > 0x10ffff)
2749
+ return -1;
2750
+ }
2751
+ return v;
2752
+ }
2753
+ function shortestReading(text, layout) {
2754
+ const raw = collectRawControlCuts(text, invisibleClassesOf(layout));
2755
+ const normalized = normalizeForRedaction(deleteInvisibleCharacters(text, raw, raw.present).text);
2756
+ const open = trailingPartialEscapeStart(normalized);
2757
+ const spelled = collectSpelledCuts(normalized);
2758
+ const length = deleteInvisibleCharacters(normalized, spelled, spelled.present).text.length;
2759
+ return open < 0 ? { visible: length, partial: false } : { visible: length - (normalized.length - open), partial: true };
2760
+ }
2761
+ export function readingHeadEnd(text, layout, units, from = 0) {
2762
+ const rest = text.length - from;
2763
+ let len = Math.min(rest, units);
2764
+ let first = true;
2765
+ let grow = 64;
2766
+ while (len < rest) {
2767
+ const { visible, partial } = shortestReading(text.slice(from, from + len), layout);
2768
+ if (visible >= units) {
2769
+ if (!partial)
2770
+ return from + len;
2771
+ len = Math.min(rest, len + grow);
2772
+ grow *= 2;
2773
+ continue;
2774
+ }
2775
+ const step = first && visible * 2 >= len ? Math.ceil((units - visible) / (visible / len)) + 1 : Math.max(units - visible, len);
2776
+ first = false;
2777
+ len = Math.min(rest, len + step);
2778
+ }
2779
+ return text.length;
2780
+ }
2781
+ function runRedaction(text, observe, json, wantHits, scope, layout, extent) {
2643
2782
  const wantCount = wantHits || (onRedaction !== undefined && observe);
2644
2783
  const eat = observe ? { from: -1, len: 0 } : undefined;
2645
2784
  let spans = [];
@@ -2647,7 +2786,7 @@ function runRedaction(text, observe, json, wantHits, scope, layout) {
2647
2786
  let readsSoFar = 0;
2648
2787
  let unioned = false;
2649
2788
  const read = (layers, view) => {
2650
- const r = applyRules(view, wantCount, json, eat, scope);
2789
+ const r = applyRules(view, wantCount, json, eat, scope, extent === "head");
2651
2790
  forgetEscapedSeparatorTails();
2652
2791
  readsSoFar += 1;
2653
2792
  if (readsSoFar === 1) {
@@ -2720,18 +2859,18 @@ function spansInOriginal(layers, kept) {
2720
2859
  return out;
2721
2860
  }
2722
2861
  export function redactSecrets(text, opts) {
2723
- return runRedaction(text, opts?.observe ?? true, opts?.json === true, false, "free-text", "multi-line").text;
2862
+ return runRedaction(text, opts?.observe ?? true, opts?.json === true, false, "free-text", "multi-line", "whole").text;
2724
2863
  }
2725
2864
  export function redactSecretsWithArms(text, opts) {
2726
- const r = runRedaction(text, opts?.observe ?? true, opts?.json === true, true, "free-text", "multi-line");
2865
+ const r = runRedaction(text, opts?.observe ?? true, opts?.json === true, true, "free-text", "multi-line", "whole");
2727
2866
  return { text: r.text, arms: new Set(r.hits.map((h) => h.arm)) };
2728
2867
  }
2729
2868
  export function redactionSpansForTest(text, opts) {
2730
- const r = runRedaction(text, false, opts?.json === true, false, "free-text", "multi-line");
2869
+ const r = runRedaction(text, false, opts?.json === true, false, "free-text", "multi-line", "whole");
2731
2870
  return { text: r.text, spans: r.spans.map((sp) => ({ start: sp.start, end: sp.end })) };
2732
2871
  }
2733
- export function proposeRedactionSpans(text, scope, layout) {
2734
- return runRedaction(text, true, false, false, scope, layout).spans.map((sp) => ({ start: sp.start, end: sp.end, repl: sp.repl }));
2872
+ export function proposeRedactionSpans(text, scope, layout, extent) {
2873
+ return runRedaction(text, true, false, false, scope, layout, extent).spans.map((sp) => ({ start: sp.start, end: sp.end, repl: sp.repl }));
2735
2874
  }
2736
2875
  const REDACT_MAX_DEPTH = 64;
2737
2876
  export function redactDeep(value) {
@@ -2833,7 +2972,7 @@ function unitCarriesCredential(unit) {
2833
2972
  for (const v of armValueClassViews())
2834
2973
  views.add(boundedUnitView(spelling, v.inClass, v.filler));
2835
2974
  views.delete("");
2836
- const hit = [...views].some((view) => runRedaction(view, true, false, false, "bounded-unit", "multi-line").text !== view);
2975
+ const hit = [...views].some((view) => runRedaction(view, true, false, false, "bounded-unit", "multi-line", "whole").text !== view);
2837
2976
  return { hit, unreadable: decoded.residue };
2838
2977
  }
2839
2978
  function displaySafePathname(pathname) {
@@ -2863,7 +3002,7 @@ function shapeLawCredentialInQueryOrFragment(canonical) {
2863
3002
  const boundary = canonical.indexOf("?");
2864
3003
  if (boundary < 0)
2865
3004
  return false;
2866
- if (runRedaction(canonical, false, false, false, "free-text", "multi-line").spans.some((sp) => sp.end > boundary))
3005
+ if (runRedaction(canonical, false, false, false, "free-text", "multi-line", "whole").spans.some((sp) => sp.end > boundary))
2867
3006
  return true;
2868
3007
  for (const unit of canonical.slice(boundary + 1).split("&").flatMap((sg) => {
2869
3008
  const eq = sg.indexOf("=");