akm-cli 0.9.1 → 0.9.2-alpha.2

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 (350) hide show
  1. package/CHANGELOG.md +103 -28
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
  12. package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
  13. package/dist/assets/tasks/core/extract.yml +6 -5
  14. package/dist/assets/tasks/core/improve.yml +6 -5
  15. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  16. package/dist/assets/tasks/core/sync.yml +6 -5
  17. package/dist/assets/tasks/core/version-check.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  21. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  22. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  23. package/dist/cli/confirm.js +2 -2
  24. package/dist/cli/parse-args.js +3 -24
  25. package/dist/cli/retired-commands.js +1 -1
  26. package/dist/cli/shared.js +2 -2
  27. package/dist/cli.js +11 -9
  28. package/dist/commands/agent/agent-dispatch.js +55 -89
  29. package/dist/commands/agent/contribute-cli.js +12 -45
  30. package/dist/commands/command/builtin-action.js +32 -0
  31. package/dist/commands/command/command-cli.js +99 -0
  32. package/dist/commands/command/command-execution.js +308 -0
  33. package/dist/commands/command/execution-source-loader.js +176 -0
  34. package/dist/commands/command/portable-template.js +60 -0
  35. package/dist/commands/config-cli.js +10 -4
  36. package/dist/commands/env/env.js +4 -2
  37. package/dist/commands/feedback-cli.js +1 -1
  38. package/dist/commands/health/checks.js +241 -29
  39. package/dist/commands/health/html-report.js +0 -14
  40. package/dist/commands/health/report-view-model.js +0 -1
  41. package/dist/commands/health/surfaces.js +6 -7
  42. package/dist/commands/health/types.js +0 -2
  43. package/dist/commands/health.js +63 -18
  44. package/dist/commands/improve/collapse-detector.js +5 -6
  45. package/dist/commands/improve/consolidate.js +251 -214
  46. package/dist/commands/improve/distill/promote-memory.js +71 -34
  47. package/dist/commands/improve/distill/quality-gate.js +17 -5
  48. package/dist/commands/improve/distill.js +232 -155
  49. package/dist/commands/improve/eligibility.js +112 -79
  50. package/dist/commands/improve/execution.js +57 -0
  51. package/dist/commands/improve/extract-cli.js +5 -5
  52. package/dist/commands/improve/extract-prompt.js +64 -22
  53. package/dist/commands/improve/extract.js +608 -360
  54. package/dist/commands/improve/improve-strategies.js +43 -14
  55. package/dist/commands/improve/improve.js +249 -29
  56. package/dist/commands/improve/loop-stages.js +11 -17
  57. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  58. package/dist/commands/improve/outcome-loop.js +22 -38
  59. package/dist/commands/improve/planner.js +134 -0
  60. package/dist/commands/improve/preparation.js +730 -409
  61. package/dist/commands/improve/reflect.js +386 -223
  62. package/dist/commands/improve/run-context.js +3 -4
  63. package/dist/commands/improve/salience.js +6 -58
  64. package/dist/commands/improve/session-asset.js +12 -12
  65. package/dist/commands/lint/index.js +101 -29
  66. package/dist/commands/migrate-cli.js +11 -69
  67. package/dist/commands/migration-tool.js +6 -9
  68. package/dist/commands/models-cli.js +27 -0
  69. package/dist/commands/proposal/drain.js +258 -186
  70. package/dist/commands/proposal/proposal-cli.js +32 -10
  71. package/dist/commands/proposal/proposal.js +2 -5
  72. package/dist/commands/proposal/propose.js +192 -172
  73. package/dist/commands/proposal/repository.js +54 -91
  74. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  75. package/dist/commands/read/curate.js +53 -22
  76. package/dist/commands/read/registry-search.js +25 -9
  77. package/dist/commands/read/remember-cli.js +14 -2
  78. package/dist/commands/read/search.js +10 -4
  79. package/dist/commands/read/show.js +139 -153
  80. package/dist/commands/registry-cli.js +16 -7
  81. package/dist/commands/remember.js +33 -18
  82. package/dist/commands/sources/add-cli.js +19 -178
  83. package/dist/commands/sources/bundle-cli.js +15 -3
  84. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  85. package/dist/commands/sources/info.js +2 -1
  86. package/dist/commands/sources/installed-stashes.js +901 -177
  87. package/dist/commands/sources/schema-repair.js +174 -95
  88. package/dist/commands/sources/self-update.js +30 -74
  89. package/dist/commands/sources/source-add.js +3 -5
  90. package/dist/commands/sources/sources-cli.js +2 -15
  91. package/dist/commands/sources/update-transaction.js +220 -0
  92. package/dist/commands/tasks/tasks-cli.js +3 -3
  93. package/dist/commands/tasks/tasks.js +736 -317
  94. package/dist/commands/workflow-cli.js +2 -2
  95. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  96. package/dist/core/adapter/adapters/akm-adapter.js +85 -35
  97. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  98. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  99. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  100. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  101. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  102. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  103. package/dist/core/adapter/adapters/index.js +0 -9
  104. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  105. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  106. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  107. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  108. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  109. package/dist/core/adapter/execution-source.js +308 -0
  110. package/dist/core/adapter/recognize-match.js +36 -13
  111. package/dist/core/adapter/registry.js +0 -9
  112. package/dist/core/asset/stash-meta.js +94 -4
  113. package/dist/core/common.js +6 -11
  114. package/dist/core/config/config-io.js +3 -3
  115. package/dist/core/config/config-schema.js +18 -40
  116. package/dist/core/config/config-sources.js +11 -21
  117. package/dist/core/config/config-walker.js +31 -13
  118. package/dist/core/config/config.js +23 -26
  119. package/dist/core/config/schema/engines.js +8 -7
  120. package/dist/core/config/schema/improve-processes.js +29 -5
  121. package/dist/core/config/schema/index-config.js +0 -27
  122. package/dist/core/config/schema/primitives.js +1 -23
  123. package/dist/core/config/schema/sources-bundles.js +13 -16
  124. package/dist/core/errors.js +2 -0
  125. package/dist/core/events.js +68 -32
  126. package/dist/core/extra-params.js +1 -0
  127. package/dist/core/improve-result.js +315 -0
  128. package/dist/core/lesson-lint.js +0 -6
  129. package/dist/core/maintenance-barrier.js +4 -4
  130. package/dist/core/network-policy.js +152 -0
  131. package/dist/core/paths.js +1 -1
  132. package/dist/core/recognition-util.js +4 -4
  133. package/dist/core/registry-url.js +456 -0
  134. package/dist/core/state/migrations.js +161 -47
  135. package/dist/core/state-db.js +453 -80
  136. package/dist/core/system-error.js +32 -0
  137. package/dist/core/time.js +2 -12
  138. package/dist/core/write-source.js +0 -18
  139. package/dist/execution/directory-identity.js +52 -0
  140. package/dist/execution/executable-identity.js +107 -0
  141. package/dist/execution/guarded-source.js +398 -0
  142. package/dist/execution/json.js +95 -0
  143. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  144. package/dist/execution/record.js +55 -0
  145. package/dist/execution/resolved-request.js +730 -0
  146. package/dist/execution/source.js +320 -0
  147. package/dist/indexer/bundle-identity-guard.js +5 -4
  148. package/dist/indexer/db/graph-db.js +33 -0
  149. package/dist/indexer/graph/graph-boost.js +3 -4
  150. package/dist/indexer/graph/graph-extraction.js +562 -373
  151. package/dist/indexer/index-written-assets.js +78 -39
  152. package/dist/indexer/indexer.js +471 -432
  153. package/dist/indexer/installations.js +6 -0
  154. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  155. package/dist/indexer/materialize-embeddings.js +155 -0
  156. package/dist/indexer/passes/memory-inference.js +227 -174
  157. package/dist/indexer/passes/metadata.js +263 -118
  158. package/dist/indexer/scan/doc-to-entry.js +7 -10
  159. package/dist/indexer/scan/drain-dir.js +51 -23
  160. package/dist/indexer/search/db-search.js +156 -50
  161. package/dist/indexer/search/fts-query.js +40 -40
  162. package/dist/indexer/search/ranking.js +36 -1
  163. package/dist/indexer/search/search-attribution.js +3 -1
  164. package/dist/indexer/search/search-fields.js +23 -14
  165. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  166. package/dist/indexer/search/search-source.js +7 -16
  167. package/dist/indexer/search/semantic-status.js +10 -1
  168. package/dist/indexer/usage/show-usage.js +105 -0
  169. package/dist/indexer/usage/usage-events.js +7 -2
  170. package/dist/indexer/walk/matchers.js +40 -10
  171. package/dist/indexer/walk/path-resolver.js +5 -2
  172. package/dist/indexer/walk/walker.js +20 -2
  173. package/dist/integrations/agent/builder-shared.js +3 -6
  174. package/dist/integrations/agent/conversation-fallback.js +16 -0
  175. package/dist/integrations/agent/engine-resolution.js +87 -87
  176. package/dist/integrations/agent/execution-cascade.js +566 -0
  177. package/dist/integrations/agent/execution-definitions.js +211 -0
  178. package/dist/integrations/agent/execution-lowering.js +811 -0
  179. package/dist/integrations/agent/execution-preparation.js +67 -0
  180. package/dist/integrations/agent/index.js +0 -2
  181. package/dist/integrations/agent/inline-execution.js +74 -0
  182. package/dist/integrations/agent/model-map.js +515 -0
  183. package/dist/integrations/agent/persona-fallback.js +30 -0
  184. package/dist/integrations/agent/request-lowering.js +186 -0
  185. package/dist/integrations/agent/runner-dispatch.js +230 -37
  186. package/dist/integrations/agent/runner.js +12 -83
  187. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/aider/index.js +0 -1
  189. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  191. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  192. package/dist/integrations/harnesses/claude/index.js +1 -5
  193. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  194. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  195. package/dist/integrations/harnesses/codex/index.js +0 -1
  196. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  197. package/dist/integrations/harnesses/copilot/index.js +0 -1
  198. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  199. package/dist/integrations/harnesses/gemini/index.js +0 -1
  200. package/dist/integrations/harnesses/index.js +4 -44
  201. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  202. package/dist/integrations/harnesses/opencode/index.js +0 -2
  203. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  204. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  205. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  206. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  207. package/dist/integrations/harnesses/openhands/index.js +0 -1
  208. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  209. package/dist/integrations/harnesses/pi/index.js +0 -1
  210. package/dist/integrations/harnesses/shared.js +0 -1
  211. package/dist/integrations/harnesses/types.js +1 -3
  212. package/dist/integrations/lockfile.js +82 -79
  213. package/dist/integrations/session-logs/index.js +6 -17
  214. package/dist/integrations/session-logs/provider-base.js +1 -29
  215. package/dist/llm/client.js +10 -5
  216. package/dist/llm/embedder.js +6 -7
  217. package/dist/llm/embedders/local.js +37 -88
  218. package/dist/llm/embedders/types.js +1 -1
  219. package/dist/llm/graph-extract.js +75 -50
  220. package/dist/llm/index-passes.js +43 -5
  221. package/dist/llm/memory-infer.js +8 -6
  222. package/dist/llm/metadata-enhance.js +5 -3
  223. package/dist/llm/structured-call.js +122 -25
  224. package/dist/output/format-exempt.js +1 -1
  225. package/dist/output/render-registry.js +0 -16
  226. package/dist/output/renderers.js +12 -7
  227. package/dist/output/shapes/curate.js +1 -0
  228. package/dist/output/shapes/helpers.js +10 -2
  229. package/dist/output/shapes/passthrough.js +2 -0
  230. package/dist/output/text/command-format.js +31 -33
  231. package/dist/output/text/health-format.js +1 -29
  232. package/dist/output/text/migrate.js +6 -56
  233. package/dist/output/text/proposal-format.js +16 -1
  234. package/dist/output/text/workflow-format.js +16 -0
  235. package/dist/registry/network.js +279 -0
  236. package/dist/registry/pinned-request-helper.js +247 -0
  237. package/dist/registry/pinned-transport.js +717 -0
  238. package/dist/registry/providers/skills-sh.js +18 -6
  239. package/dist/registry/providers/static-index.js +20 -7
  240. package/dist/registry/resolve.js +53 -28
  241. package/dist/scripts/akm-migrate-node.js +19334 -52269
  242. package/dist/scripts/akm-migrate.js +19270 -51612
  243. package/dist/setup/registry-stash-loader.js +64 -20
  244. package/dist/setup/semantic-assets.js +9 -34
  245. package/dist/setup/setup.js +12 -30
  246. package/dist/setup/source-identity.js +17 -0
  247. package/dist/setup/steps/sources.js +36 -15
  248. package/dist/setup/steps/tasks.js +39 -11
  249. package/dist/sources/providers/git-provider.js +3 -3
  250. package/dist/sources/providers/npm.js +2 -2
  251. package/dist/sources/providers/provider-utils.js +4 -3
  252. package/dist/sources/providers/website.js +11 -7
  253. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  254. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  255. package/dist/sources/website-url.js +73 -0
  256. package/dist/storage/engines/sqlite-migrations.js +81 -26
  257. package/dist/storage/managed-db.js +27 -24
  258. package/dist/storage/repositories/events-repository.js +3 -0
  259. package/dist/storage/repositories/index-connection.js +42 -10
  260. package/dist/storage/repositories/index-entries-repository.js +203 -229
  261. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  262. package/dist/storage/repositories/index-entry-schema.js +255 -0
  263. package/dist/storage/repositories/index-fts-repository.js +64 -71
  264. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  265. package/dist/storage/repositories/index-meta-repository.js +0 -11
  266. package/dist/storage/repositories/index-schema.js +74 -350
  267. package/dist/storage/repositories/index-utility-repository.js +12 -17
  268. package/dist/storage/repositories/index-vec-repository.js +56 -7
  269. package/dist/storage/repositories/proposals-repository.js +4 -127
  270. package/dist/storage/repositories/registry-cache.js +2 -1
  271. package/dist/storage/repositories/task-history-repository.js +20 -40
  272. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  273. package/dist/storage/sqlite-read-snapshot.js +148 -0
  274. package/dist/tasks/backends/cron.js +170 -42
  275. package/dist/tasks/backends/index.js +1 -1
  276. package/dist/tasks/backends/launchd.js +787 -202
  277. package/dist/tasks/backends/schtasks.js +282 -83
  278. package/dist/tasks/embedded.js +7 -7
  279. package/dist/tasks/frozen-script.js +50 -0
  280. package/dist/tasks/resolve-akm-bin.js +5 -1
  281. package/dist/tasks/runner.js +239 -251
  282. package/dist/tasks/runtime-v3.js +281 -0
  283. package/dist/tasks/scheduler-binding.js +272 -0
  284. package/dist/tasks/scheduler-invocation.js +57 -43
  285. package/dist/tasks/scheduler-sync.js +654 -0
  286. package/dist/tasks/source-v3.js +752 -0
  287. package/dist/tasks/standalone-script-entry.js +5 -0
  288. package/dist/tasks/task-id.js +29 -0
  289. package/dist/workflows/authoring/authoring.js +15 -32
  290. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  291. package/dist/workflows/exec/exec-unit.js +7 -28
  292. package/dist/workflows/exec/frozen-judge.js +57 -89
  293. package/dist/workflows/exec/lowering-notices.js +23 -0
  294. package/dist/workflows/exec/native-executor.js +301 -458
  295. package/dist/workflows/exec/param-secrets.js +4 -3
  296. package/dist/workflows/exec/run-workflow.js +26 -32
  297. package/dist/workflows/exec/step-work.js +105 -109
  298. package/dist/workflows/exec/unit-dispatch.js +103 -27
  299. package/dist/workflows/exec/unit-writer.js +3 -3
  300. package/dist/workflows/exec/worktree.js +2 -2
  301. package/dist/workflows/ir/compile.js +86 -72
  302. package/dist/workflows/ir/environment-v4.js +328 -0
  303. package/dist/workflows/ir/freeze-v4.js +122 -0
  304. package/dist/workflows/ir/plan-hash.js +13 -7
  305. package/dist/workflows/ir/schema-v4.js +525 -0
  306. package/dist/workflows/ir/schema.js +25 -284
  307. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  308. package/dist/workflows/parser.js +27 -24
  309. package/dist/workflows/program/schema.js +1 -2
  310. package/dist/workflows/renderer.js +42 -29
  311. package/dist/workflows/resource-limits.js +4 -5
  312. package/dist/workflows/runtime/agent-identity.js +11 -13
  313. package/dist/workflows/runtime/plan-classifier.js +8 -8
  314. package/dist/workflows/runtime/runs.js +27 -43
  315. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  316. package/dist/workflows/source-files.js +373 -0
  317. package/dist/workflows/source-ir/compile.js +196 -0
  318. package/dist/workflows/source-ir/github-yaml.js +577 -0
  319. package/dist/workflows/source-ir/ordering.js +38 -0
  320. package/dist/workflows/source-ir/program.js +50 -0
  321. package/dist/workflows/source-ir/result.js +26 -0
  322. package/dist/workflows/source-ir/schema.js +772 -0
  323. package/dist/workflows/source-ir/semantics.js +242 -0
  324. package/dist/workflows/source-ir/uses.js +14 -0
  325. package/docs/README.md +2 -0
  326. package/docs/migration/README.md +3 -1
  327. package/docs/migration/release-notes/0.9.2.md +55 -0
  328. package/docs/migration/release-notes/README.md +5 -0
  329. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  330. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  331. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  332. package/docs/reference/README.md +1 -0
  333. package/docs/reference/cli.md +230 -98
  334. package/docs/reference/configuration.md +159 -36
  335. package/docs/reference/data-and-telemetry.md +19 -1
  336. package/docs/reference/supported-formats.md +23 -3
  337. package/docs/reference/tasks.md +182 -0
  338. package/docs/reference/workflow-schema.md +91 -40
  339. package/docs/reference/workflows.md +33 -6
  340. package/package.json +10 -6
  341. package/schemas/akm-config.json +372 -224
  342. package/schemas/akm-task.json +324 -80
  343. package/schemas/akm-workflow.json +6 -9
  344. package/dist/core/migration-operation.js +0 -75
  345. package/dist/integrations/agent/model-aliases.js +0 -74
  346. package/dist/tasks/parser.js +0 -380
  347. package/dist/tasks/schema.js +0 -123
  348. package/dist/tasks/validator.js +0 -80
  349. package/dist/workflows/ir/freeze.js +0 -320
  350. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -0,0 +1,152 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { lookup as dnsLookup } from "node:dns/promises";
5
+ import { isIP } from "node:net";
6
+ /** WHATWG URL brackets IPv6 literals while node:net expects bare addresses. */
7
+ export function bareHostname(hostname) {
8
+ const lower = hostname.toLowerCase();
9
+ return lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
10
+ }
11
+ /** Resolve every A/AAAA record for a hostname. */
12
+ export async function resolveHostnameAddresses(hostname, resolver) {
13
+ const resolve = resolver ?? defaultResolveHostname;
14
+ return resolve(bareHostname(hostname));
15
+ }
16
+ async function defaultResolveHostname(hostname) {
17
+ const records = await dnsLookup(hostname, { all: true });
18
+ return records.map((record) => record.address);
19
+ }
20
+ /** Classify a hostname literal or well-known metadata/loopback name. */
21
+ export function classifyNetworkHostname(hostname) {
22
+ const bare = bareHostname(hostname);
23
+ if (bare === "metadata.google.internal" || bare === "metadata.goog")
24
+ return "metadata";
25
+ if (bare === "localhost" || bare.endsWith(".localhost"))
26
+ return "loopback";
27
+ if (bare.endsWith(".invalid"))
28
+ return "reserved";
29
+ const addressClass = classifyNetworkAddress(bare);
30
+ return addressClass === "invalid" ? "public" : addressClass;
31
+ }
32
+ /** Classify an IPv4/IPv6 address; non-address strings are `invalid`. */
33
+ export function classifyNetworkAddress(address) {
34
+ const bare = bareHostname(address);
35
+ const version = isIP(bare);
36
+ if (version === 4)
37
+ return classifyIpv4(bare);
38
+ if (version === 6)
39
+ return classifyIpv6(bare);
40
+ return "invalid";
41
+ }
42
+ function classifyIpv4(address) {
43
+ const parts = address.split(".").map((part) => Number.parseInt(part, 10));
44
+ const [a = -1, b = -1, c = -1, d = -1] = parts;
45
+ if (parts.length !== 4 || [a, b, c, d].some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
46
+ return "invalid";
47
+ }
48
+ if (a === 127)
49
+ return "loopback";
50
+ if (a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168))
51
+ return "private";
52
+ if (a === 169 && b === 254)
53
+ return d === 254 && c === 169 ? "metadata" : "link-local";
54
+ if (a === 0 ||
55
+ (a === 100 && b >= 64 && b <= 127) ||
56
+ (a === 192 && b === 0 && c === 0 && d !== 9 && d !== 10) ||
57
+ (a === 192 && b === 0 && c === 2) ||
58
+ (a === 192 && b === 88 && c === 99) ||
59
+ (a === 198 && (b === 18 || b === 19)) ||
60
+ (a === 198 && b === 51 && c === 100) ||
61
+ (a === 203 && b === 0 && c === 113) ||
62
+ a >= 224) {
63
+ return "reserved";
64
+ }
65
+ return "public";
66
+ }
67
+ function classifyIpv6(address) {
68
+ const words = parseIpv6Words(address);
69
+ const value = ipv6Value(address);
70
+ if (!words || value === null)
71
+ return "invalid";
72
+ if (value === 0n)
73
+ return "reserved";
74
+ if (value === 1n)
75
+ return "loopback";
76
+ const embeddedIpv4 = embeddedIpv4Address(words);
77
+ if (embeddedIpv4)
78
+ return classifyIpv4(embeddedIpv4);
79
+ if (value === ipv6Value("fd00:ec2::254"))
80
+ return "metadata";
81
+ if (inIpv6Range(value, "fc00::", 7))
82
+ return "private";
83
+ if (inIpv6Range(value, "fe80::", 10))
84
+ return "link-local";
85
+ if (!inIpv6Range(value, "2000::", 3))
86
+ return "reserved";
87
+ if (FORBIDDEN_IPV6_RANGES.some(([prefix, bits]) => inIpv6Range(value, prefix, bits)))
88
+ return "reserved";
89
+ return "public";
90
+ }
91
+ const FORBIDDEN_IPV6_RANGES = [
92
+ ["64:ff9b:1::", 48],
93
+ ["100::", 64],
94
+ ["2001::", 32],
95
+ ["2001:2::", 48],
96
+ ["2001:10::", 28],
97
+ ["2001:20::", 28],
98
+ ["2001:db8::", 32],
99
+ ["3ffe::", 16],
100
+ ["3fff::", 20],
101
+ ["5f00::", 16],
102
+ ["fec0::", 10],
103
+ ["ff00::", 8],
104
+ ];
105
+ function inIpv6Range(value, prefix, bits) {
106
+ const prefixValue = ipv6Value(prefix);
107
+ if (prefixValue === null)
108
+ return true;
109
+ const shift = BigInt(128 - bits);
110
+ return value >> shift === prefixValue >> shift;
111
+ }
112
+ function parseIpv6Words(address) {
113
+ let normalized;
114
+ try {
115
+ normalized = bareHostname(new URL(`http://[${bareHostname(address)}]/`).hostname);
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ const halves = normalized.split("::");
121
+ if (halves.length > 2)
122
+ return null;
123
+ const left = halves[0] ? halves[0].split(":") : [];
124
+ const right = halves[1] ? halves[1].split(":") : [];
125
+ const missing = 8 - left.length - right.length;
126
+ if ((halves.length === 1 && missing !== 0) || (halves.length === 2 && missing < 1))
127
+ return null;
128
+ const parts = halves.length === 2 ? [...left, ...Array(missing).fill("0"), ...right] : left;
129
+ if (parts.length !== 8 || parts.some((part) => !/^[0-9a-f]{1,4}$/.test(part)))
130
+ return null;
131
+ return parts.map((part) => Number.parseInt(part, 16));
132
+ }
133
+ function ipv6Value(address) {
134
+ const words = parseIpv6Words(address);
135
+ if (!words)
136
+ return null;
137
+ return words.reduce((value, word) => (value << 16n) | BigInt(word), 0n);
138
+ }
139
+ function embeddedIpv4Address(words) {
140
+ const firstSixZero = words.slice(0, 6).every((word) => word === 0);
141
+ const mapped = words.slice(0, 5).every((word) => word === 0) && words[5] === 0xffff;
142
+ const translated = words.slice(0, 4).every((word) => word === 0) && words[4] === 0xffff && words[5] === 0;
143
+ const wellKnownNat64 = words[0] === 0x64 && words[1] === 0xff9b && words.slice(2, 6).every((word) => word === 0);
144
+ if (words[0] === 0x2002)
145
+ return ipv4FromHextets(words[1] ?? 0, words[2] ?? 0);
146
+ if (!firstSixZero && !mapped && !translated && !wellKnownNat64)
147
+ return null;
148
+ return ipv4FromHextets(words[6] ?? 0, words[7] ?? 0);
149
+ }
150
+ function ipv4FromHextets(high, low) {
151
+ return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
152
+ }
@@ -183,7 +183,7 @@ function homelessFallbackDir(kind) {
183
183
  * `%LOCALAPPDATA%\akm\data` on Windows).
184
184
  *
185
185
  * Holds durable, non-regenerable application data: SQLite databases
186
- * (index.db, workflow.db, state.db), akm.lock, and config-backups.
186
+ * (`index.db`, `state.db`), `akm.lock`, and current data artifacts.
187
187
  * Losing this directory loses history and installed state.
188
188
  *
189
189
  * Env overrides (in priority order):
@@ -34,13 +34,13 @@ export const SCRIPT_EXTENSIONS = new Set([
34
34
  ".kts",
35
35
  ]);
36
36
  /**
37
- * Recognized workflow asset extensions. One format now (workflow-format-
38
- * unification): orchestration graph in frontmatter, prose in the body.
37
+ * Recognized workflow asset extensions. Markdown remains first-class and the
38
+ * strict local GitHub-shaped source adapter adds `.yml` as a peer format.
39
39
  * `workflows/<name>` refs resolve against this list.
40
40
  */
41
- export const WORKFLOW_EXTENSIONS = [".md"];
41
+ export const WORKFLOW_EXTENSIONS = [".md", ".yml"];
42
42
  /**
43
- * Strip the recognized workflow extension (`.md`) from a workflow asset
43
+ * Strip a recognized workflow extension (`.md` or `.yml`) from a workflow asset
44
44
  * *name* so `foo` and `foo.md` collapse to one canonical identity — the same
45
45
  * collapse `workflowSpec.toCanonicalName` performs on a resolved file path.
46
46
  * Callers that turn a `workflows/<name>` ref into run identity (the
@@ -0,0 +1,456 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { toErrorMessage } from "./common.js";
5
+ export const REGISTRY_CREDENTIALS_UNSUPPORTED = "Registry URLs must not include username or password credentials. Authenticated registries are not supported by the built-in static-index or skills-sh providers; configure a credential-free HTTPS endpoint.";
6
+ const INVALID_REGISTRY_URL_LABEL = "(invalid registry URL)";
7
+ const HTTP_SCHEME_START = /h[\t\r\n]*t[\t\r\n]*t[\t\r\n]*p[\t\r\n]*(?:s[\t\r\n]*)?:/giu;
8
+ const AUTHORITY_PREFIX_SEPARATOR = new Set(["\t", "\r", "\n", "\\", "/"]);
9
+ const AUTHORITY_STRUCTURAL_BOUNDARY = new Set(["\\", "/", "?", "#"]);
10
+ const AUTHORITY_AMBIGUOUS_BOUNDARY = new Set(["&", ",", ";", ")"]);
11
+ const AUTHORITY_SOFT_BOUNDARY = new Set(["\t", "\r", "\n", " ", '"', "'", "`", "<", ">"]);
12
+ const STRICT_MAX_DEPTH = 8;
13
+ const STRICT_MAX_CANDIDATES = 128;
14
+ const STRICT_MAX_DECODED_BYTES = 65_536;
15
+ const HTTP_SCHEME_EVIDENCE = /h[\t\r\n]*t[\t\r\n]*t[\t\r\n]*p[\t\r\n]*(?:s[\t\r\n]*)?:/iu;
16
+ /**
17
+ * Detect URL userinfo without broadening this issue into general URL/network
18
+ * validation (the SSRF/redirect boundary is tracked separately in #767).
19
+ * Whole configured values use WHATWG parsing; strict bounded scanning catches
20
+ * nested, control-obfuscated, and special-scheme URL starts. Free-text error
21
+ * sanitization uses the separate conservative mode below.
22
+ */
23
+ export function hasRegistryUrlCredentials(raw) {
24
+ return inspectStrictRegistryUrl(raw).unsafe;
25
+ }
26
+ /**
27
+ * The only formatter for configured registry URLs. Credential-free valid URLs
28
+ * retain their authored spelling; userinfo is removed structurally. Invalid
29
+ * strings fail closed rather than being echoed into a diagnostic.
30
+ */
31
+ export function formatRegistryUrl(raw) {
32
+ const parsed = parseWholeHttpUrl(raw);
33
+ const inspection = inspectStrictRegistryUrl(raw);
34
+ if (parsed) {
35
+ if (!inspection.unsafe)
36
+ return raw;
37
+ let candidate = raw;
38
+ if (parsed.username || parsed.password) {
39
+ // A whole configured value is unambiguous. Always clear WHATWG userinfo
40
+ // structurally; textual deletion can otherwise stop at the first of
41
+ // several `@` characters and leave the real credential behind.
42
+ parsed.username = "";
43
+ parsed.password = "";
44
+ candidate = parsed.toString();
45
+ }
46
+ candidate = redactDetectedUserinfo(candidate, "strict");
47
+ return inspectStrictRegistryUrl(candidate).unsafe ? INVALID_REGISTRY_URL_LABEL : candidate;
48
+ }
49
+ const formatted = redactDetectedUserinfo(raw, "strict");
50
+ if (inspection.unsafe &&
51
+ registryUrlStarts(raw)[0]?.start === 0 &&
52
+ formatted !== raw &&
53
+ !inspectStrictRegistryUrl(formatted).unsafe) {
54
+ return formatted;
55
+ }
56
+ return INVALID_REGISTRY_URL_LABEL;
57
+ }
58
+ /** A consistent human label built on the safe URL formatter. */
59
+ export function formatRegistryLabel(entry) {
60
+ const url = formatRegistryUrl(entry.url);
61
+ return entry.name ? `${entry.name} (${url})` : url;
62
+ }
63
+ /** Actionable fail-closed warning for runtime inputs that bypass config load. */
64
+ export function formatRegistryCredentialWarning(entry) {
65
+ return `Registry ${formatRegistryLabel(entry)} was ignored. ${REGISTRY_CREDENTIALS_UNSUPPORTED}`;
66
+ }
67
+ /** Copy a registry entry for any structured-output boundary. */
68
+ export function registryEntryForOutput(entry) {
69
+ return { ...entry, url: formatRegistryUrl(entry.url) };
70
+ }
71
+ /**
72
+ * Remove userinfo from every HTTP(S) URL token in an untrusted error/warning
73
+ * string while leaving ordinary credential-free query/path text untouched.
74
+ */
75
+ export function redactCredentialBearingUrls(message) {
76
+ return redactDetectedUserinfo(message, "diagnostic");
77
+ }
78
+ /** Fetch errors are untrusted: runtimes often include the request URL. */
79
+ export function formatRegistryError(error) {
80
+ return redactCredentialBearingUrls(toErrorMessage(error));
81
+ }
82
+ /**
83
+ * Configured/provider registry values are not prose. Inspect their URL
84
+ * structure recursively so encoded nested URLs cannot bypass validation,
85
+ * while keeping arbitrary diagnostic tokenization deliberately conservative.
86
+ */
87
+ function inspectStrictRegistryUrl(raw) {
88
+ const state = { candidates: 0, decodedBytes: 0 };
89
+ const parsed = parseWholeHttpUrl(raw);
90
+ if (!parsed) {
91
+ inspectStrictComponent(raw, state, 0);
92
+ return state.reason ? { unsafe: true, reason: state.reason } : { unsafe: false };
93
+ }
94
+ inspectParsedStrictUrl(parsed, state, 0);
95
+ return state.reason ? { unsafe: true, reason: state.reason } : { unsafe: false };
96
+ }
97
+ function inspectParsedStrictUrl(parsed, state, depth) {
98
+ if (state.reason)
99
+ return;
100
+ if (parsed.username || parsed.password) {
101
+ state.reason = "credentials";
102
+ return;
103
+ }
104
+ inspectAuthoredStrictComponent(parsed.pathname, state, depth, false);
105
+ if (state.reason)
106
+ return;
107
+ // Inspect each authored query key/value independently. This mirrors
108
+ // URLSearchParams' first decoding layer while retaining whether a delimiter
109
+ // was structural (`&`) or encoded data (`%26`) inside one value.
110
+ const rawQuery = parsed.search.startsWith("?") ? parsed.search.slice(1) : parsed.search;
111
+ for (const pair of rawQuery.split("&")) {
112
+ const equals = pair.indexOf("=");
113
+ const rawKey = equals >= 0 ? pair.slice(0, equals) : pair;
114
+ const rawValue = equals >= 0 ? pair.slice(equals + 1) : "";
115
+ inspectAuthoredStrictComponent(rawKey, state, depth, true);
116
+ inspectAuthoredStrictComponent(rawValue, state, depth, true);
117
+ if (state.reason)
118
+ return;
119
+ }
120
+ const fragment = parsed.hash.startsWith("#") ? parsed.hash.slice(1) : parsed.hash;
121
+ inspectAuthoredStrictComponent(fragment, state, depth, false);
122
+ }
123
+ function inspectAuthoredStrictComponent(raw, state, depth, queryEncoded) {
124
+ for (const authoredPart of splitAuthoredFieldComponents(raw)) {
125
+ let component = authoredPart;
126
+ if (queryEncoded) {
127
+ const decoded = decodePercentSafely(authoredPart, "query");
128
+ if (!decoded.valid && hasCredentialLikeUrlEvidence(authoredPart)) {
129
+ state.reason = "malformed-encoding";
130
+ return;
131
+ }
132
+ component = decoded.value;
133
+ }
134
+ inspectStrictComponent(component, state, depth);
135
+ if (state.reason)
136
+ return;
137
+ }
138
+ }
139
+ function inspectStrictComponent(raw, state, depth) {
140
+ if (state.reason || raw.length === 0)
141
+ return;
142
+ let decoded = raw;
143
+ let currentDepth = depth;
144
+ const seenCandidateKeys = new Set();
145
+ while (true) {
146
+ const starts = registryUrlStarts(decoded);
147
+ if (starts.length > 0) {
148
+ const unseenIndices = unseenCandidateIndices(decoded, starts, seenCandidateKeys);
149
+ const percentFullyCovered = inspectStrictUrlCandidates(decoded, starts, unseenIndices, state, currentDepth);
150
+ if (state.reason)
151
+ return;
152
+ if (percentFullyCovered)
153
+ return;
154
+ }
155
+ if (!decoded.includes("%"))
156
+ return;
157
+ if (currentDepth >= STRICT_MAX_DEPTH) {
158
+ if (looksLikeStrictUrlComponent(decoded))
159
+ state.reason = "inspection-limit";
160
+ return;
161
+ }
162
+ const next = decodePercentSafely(decoded, "component");
163
+ if (!next.valid) {
164
+ if (hasCredentialLikeUrlEvidence(decoded))
165
+ state.reason = "malformed-encoding";
166
+ return;
167
+ }
168
+ if (next.value === decoded)
169
+ return;
170
+ decoded = next.value;
171
+ currentDepth++;
172
+ }
173
+ }
174
+ function unseenCandidateIndices(decoded, starts, seenCandidateKeys) {
175
+ const occurrences = new Map();
176
+ const unseen = new Set();
177
+ for (let index = 0; index < starts.length; index++) {
178
+ const start = starts[index];
179
+ if (!start)
180
+ continue;
181
+ const authority = strictAuthoritySlice(decoded.slice(start.start));
182
+ const occurrence = (occurrences.get(authority) ?? 0) + 1;
183
+ occurrences.set(authority, occurrence);
184
+ const key = `${authority}\u0000${occurrence}`;
185
+ if (seenCandidateKeys.has(key))
186
+ continue;
187
+ seenCandidateKeys.add(key);
188
+ unseen.add(index);
189
+ }
190
+ return unseen;
191
+ }
192
+ function inspectStrictUrlCandidates(decoded, starts, unseenIndices, state, depth) {
193
+ const recursivelyInspectedRanges = [];
194
+ const bytes = new TextEncoder().encode(decoded).byteLength;
195
+ if (bytes > STRICT_MAX_DECODED_BYTES - state.decodedBytes) {
196
+ state.reason = "inspection-limit";
197
+ return false;
198
+ }
199
+ state.decodedBytes += bytes;
200
+ if (unseenIndices.size > STRICT_MAX_CANDIDATES - state.candidates) {
201
+ state.reason = "inspection-limit";
202
+ return false;
203
+ }
204
+ state.candidates += unseenIndices.size;
205
+ for (const start of starts) {
206
+ if (findStrictComponentUserinfoSpan(decoded, start)) {
207
+ state.reason = "credentials";
208
+ return false;
209
+ }
210
+ }
211
+ for (let index = 0; index < starts.length; index++) {
212
+ if (!unseenIndices.has(index))
213
+ continue;
214
+ const start = starts[index];
215
+ if (!start)
216
+ continue;
217
+ const nextStart = starts[index + 1]?.start ?? decoded.length;
218
+ const candidate = trimStrictCandidateSuffix(decoded.slice(start.start, nextStart));
219
+ const candidateDepth = inspectDecodedCandidateUserinfo(candidate, state, depth);
220
+ if (state.reason)
221
+ return false;
222
+ if (candidateDepth >= STRICT_MAX_DEPTH) {
223
+ state.reason = "inspection-limit";
224
+ return false;
225
+ }
226
+ const parsed = parseWholeHttpUrl(candidate);
227
+ if (!parsed)
228
+ continue;
229
+ // A later query/list email can make WHATWG interpret surrounding prose as
230
+ // userinfo. Trust that interpretation only when the component-bounded
231
+ // authority scan above found the same userinfo.
232
+ if (parsed.username || parsed.password)
233
+ continue;
234
+ inspectParsedStrictUrl(parsed, state, candidateDepth + 1);
235
+ if (state.reason)
236
+ return false;
237
+ recursivelyInspectedRanges.push({ start: start.start, end: nextStart });
238
+ }
239
+ return everyPercentEscapeIsCovered(decoded, recursivelyInspectedRanges);
240
+ }
241
+ function everyPercentEscapeIsCovered(raw, ranges) {
242
+ for (let cursor = raw.indexOf("%"); cursor >= 0; cursor = raw.indexOf("%", cursor + 1)) {
243
+ if (!ranges.some((range) => cursor >= range.start && cursor < range.end))
244
+ return false;
245
+ }
246
+ return true;
247
+ }
248
+ function inspectDecodedCandidateUserinfo(raw, state, depth) {
249
+ let decoded = strictAuthoritySlice(raw);
250
+ let currentDepth = depth;
251
+ while (true) {
252
+ if (registryUrlStarts(decoded).some((start) => findStrictComponentUserinfoSpan(decoded, start))) {
253
+ state.reason = "credentials";
254
+ return currentDepth;
255
+ }
256
+ if (!decoded.includes("%"))
257
+ return currentDepth;
258
+ if (currentDepth >= STRICT_MAX_DEPTH) {
259
+ state.reason = "inspection-limit";
260
+ return currentDepth;
261
+ }
262
+ const next = decodePercentSafely(decoded, "component");
263
+ if (!next.valid) {
264
+ if (hasCredentialLikeUrlEvidence(decoded))
265
+ state.reason = "malformed-encoding";
266
+ return currentDepth;
267
+ }
268
+ if (next.value === decoded)
269
+ return currentDepth;
270
+ decoded = strictAuthoritySlice(next.value);
271
+ currentDepth++;
272
+ }
273
+ }
274
+ function strictAuthoritySlice(raw) {
275
+ const start = registryUrlStarts(raw)[0];
276
+ if (!start)
277
+ return raw;
278
+ let end = raw.length;
279
+ for (let cursor = start.authorityStart; cursor < raw.length; cursor++) {
280
+ const char = raw[cursor];
281
+ if (char && AUTHORITY_STRUCTURAL_BOUNDARY.has(char)) {
282
+ end = cursor;
283
+ break;
284
+ }
285
+ }
286
+ return raw.slice(start.start, end);
287
+ }
288
+ function decodePercentSafely(raw, mode) {
289
+ const input = mode === "query" ? raw.replaceAll("+", "%20") : raw;
290
+ let valid = true;
291
+ let value = raw;
292
+ try {
293
+ value = decodeURIComponent(input);
294
+ }
295
+ catch {
296
+ valid = false;
297
+ }
298
+ if (mode === "query") {
299
+ return { value: new URLSearchParams(`value=${raw}`).get("value") ?? "", valid };
300
+ }
301
+ if (!valid)
302
+ return { value: raw, valid: false };
303
+ return { value, valid: true };
304
+ }
305
+ function splitAuthoredFieldComponents(raw) {
306
+ const parts = [];
307
+ let partStart = 0;
308
+ for (let cursor = 0; cursor < raw.length; cursor++) {
309
+ const delimiter = raw[cursor];
310
+ if (!delimiter || !AUTHORITY_AMBIGUOUS_BOUNDARY.has(delimiter))
311
+ continue;
312
+ if (!/^[a-z0-9_.-]+=/iu.test(raw.slice(cursor + 1)))
313
+ continue;
314
+ const prefix = raw.slice(partStart, cursor);
315
+ const starts = registryUrlStarts(prefix);
316
+ const lastStart = starts.at(-1);
317
+ if (!lastStart)
318
+ continue;
319
+ const candidate = trimStrictCandidateSuffix(prefix.slice(lastStart.start));
320
+ const parsed = parseWholeHttpUrl(candidate);
321
+ if (!parsed || parsed.username || parsed.password || !looksLikeDiagnosticHostPrefix(parsed.hostname))
322
+ continue;
323
+ parts.push(raw.slice(partStart, cursor));
324
+ partStart = cursor + 1;
325
+ }
326
+ parts.push(raw.slice(partStart));
327
+ return parts;
328
+ }
329
+ function findStrictComponentUserinfoSpan(raw, urlStart) {
330
+ const conventional = findUserinfoSpan(raw, urlStart, "strict");
331
+ if (conventional)
332
+ return conventional;
333
+ const { authorityStart } = urlStart;
334
+ let lastAt = -1;
335
+ for (let cursor = authorityStart; cursor < raw.length; cursor++) {
336
+ const char = raw[cursor];
337
+ if (char === undefined || AUTHORITY_STRUCTURAL_BOUNDARY.has(char))
338
+ break;
339
+ if (char === "@" && cursor > authorityStart)
340
+ lastAt = cursor;
341
+ }
342
+ if (lastAt <= authorityStart)
343
+ return undefined;
344
+ const prefix = raw.slice(authorityStart, lastAt);
345
+ const ambiguousBoundary = Array.from(prefix).findIndex((char) => AUTHORITY_AMBIGUOUS_BOUNDARY.has(char));
346
+ if (ambiguousBoundary < 0)
347
+ return undefined;
348
+ return { start: authorityStart, end: lastAt + 1 };
349
+ }
350
+ function trimStrictCandidateSuffix(raw) {
351
+ return raw.replace(/[\t\r\n ]+$/u, "").replace(/[,&;]+$/u, "");
352
+ }
353
+ function hasCredentialLikeUrlEvidence(raw) {
354
+ const evidence = decodePercentForEvidence(raw);
355
+ return HTTP_SCHEME_EVIDENCE.test(stripMalformedPercentForEvidence(evidence)) && evidence.includes("@");
356
+ }
357
+ function looksLikeStrictUrlComponent(raw) {
358
+ const evidence = decodePercentForEvidence(raw);
359
+ return HTTP_SCHEME_EVIDENCE.test(stripMalformedPercentForEvidence(evidence));
360
+ }
361
+ function decodePercentForEvidence(raw) {
362
+ let decoded = raw;
363
+ for (let layer = 0; layer <= STRICT_MAX_DEPTH; layer++) {
364
+ const next = decoded.replace(/%([0-9a-f]{2})/giu, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
365
+ if (next === decoded)
366
+ break;
367
+ decoded = next;
368
+ }
369
+ return decoded;
370
+ }
371
+ function stripMalformedPercentForEvidence(raw) {
372
+ return raw.replace(/%[^%]{0,2}/gu, "");
373
+ }
374
+ function redactDetectedUserinfo(raw, mode) {
375
+ let formatted = raw;
376
+ const starts = registryUrlStarts(raw);
377
+ for (let i = starts.length - 1; i >= 0; i--) {
378
+ const start = starts[i];
379
+ if (start === undefined)
380
+ continue;
381
+ const span = findUserinfoSpan(formatted, start, mode);
382
+ if (!span)
383
+ continue;
384
+ formatted = `${formatted.slice(0, span.start)}${formatted.slice(span.end)}`;
385
+ }
386
+ return formatted;
387
+ }
388
+ function registryUrlStarts(raw) {
389
+ return Array.from(raw.matchAll(HTTP_SCHEME_START), (match) => {
390
+ let authorityStart = match.index + match[0].length;
391
+ while (authorityStart < raw.length && AUTHORITY_PREFIX_SEPARATOR.has(raw[authorityStart] ?? "")) {
392
+ authorityStart++;
393
+ }
394
+ return { start: match.index, authorityStart };
395
+ });
396
+ }
397
+ function findUserinfoSpan(raw, urlStart, mode) {
398
+ const { authorityStart } = urlStart;
399
+ let crossedAmbiguousBoundary = false;
400
+ let lastAt = -1;
401
+ for (let cursor = authorityStart; cursor < raw.length; cursor++) {
402
+ const char = raw[cursor];
403
+ if (char === undefined)
404
+ return undefined;
405
+ if (char === "@") {
406
+ if (cursor > authorityStart)
407
+ lastAt = cursor;
408
+ continue;
409
+ }
410
+ if (AUTHORITY_STRUCTURAL_BOUNDARY.has(char))
411
+ return userinfoSpan(authorityStart, lastAt);
412
+ const isSoftBoundary = AUTHORITY_SOFT_BOUNDARY.has(char);
413
+ const isAmbiguousBoundary = AUTHORITY_AMBIGUOUS_BOUNDARY.has(char);
414
+ if (!isSoftBoundary && !isAmbiguousBoundary)
415
+ continue;
416
+ if (mode === "diagnostic" && cursor === authorityStart && isSoftBoundary)
417
+ return undefined;
418
+ if (lastAt >= 0 && mode === "diagnostic") {
419
+ const apparentHost = raw.slice(lastAt + 1, cursor);
420
+ if (looksLikeDiagnosticHostPrefix(apparentHost))
421
+ return userinfoSpan(authorityStart, lastAt);
422
+ }
423
+ if (crossedAmbiguousBoundary)
424
+ continue;
425
+ const prefix = raw.slice(authorityStart, cursor);
426
+ // Strict configured/provider handling crosses soft delimiters even after a
427
+ // host-shaped username: WHATWG accepts those values as nested userinfo.
428
+ // Conservative free-text handling stops at an apparent host so a later
429
+ // ordinary email is not consumed. Ambiguous query/list punctuation still
430
+ // requires an explicit username:password prefix in either mode.
431
+ if ((mode === "diagnostic" && looksLikeDiagnosticHostPrefix(prefix)) ||
432
+ (isAmbiguousBoundary && !prefix.includes(":"))) {
433
+ return userinfoSpan(authorityStart, lastAt);
434
+ }
435
+ crossedAmbiguousBoundary = true;
436
+ }
437
+ return userinfoSpan(authorityStart, lastAt);
438
+ }
439
+ function userinfoSpan(authorityStart, lastAt) {
440
+ return lastAt > authorityStart ? { start: authorityStart, end: lastAt + 1 } : undefined;
441
+ }
442
+ function looksLikeDiagnosticHostPrefix(raw) {
443
+ if (raw.startsWith("["))
444
+ return true;
445
+ const hostname = raw.split(":", 1)[0]?.toLowerCase() ?? "";
446
+ return hostname === "localhost" || hostname.startsWith("xn--") || hostname.includes(".");
447
+ }
448
+ function parseWholeHttpUrl(raw) {
449
+ try {
450
+ const parsed = new URL(raw);
451
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed : undefined;
452
+ }
453
+ catch {
454
+ return undefined;
455
+ }
456
+ }