@superblocksteam/vite-plugin-file-sync 2.0.123 → 2.0.124-next.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 (271) hide show
  1. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts +16 -1
  2. package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
  3. package/dist/ai-service/agent/prompts/build-base-system-prompt.js +27 -1
  4. package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
  5. package/dist/ai-service/agent/prompts/build-security-scan-prompt.d.ts.map +1 -1
  6. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js +3 -1
  7. package/dist/ai-service/agent/prompts/build-security-scan-prompt.js.map +1 -1
  8. package/dist/ai-service/agent/tools/apis/api-comparator.d.ts.map +1 -1
  9. package/dist/ai-service/agent/tools/apis/api-comparator.js +1 -4
  10. package/dist/ai-service/agent/tools/apis/api-comparator.js.map +1 -1
  11. package/dist/ai-service/agent/tools/apis/api-testing-state.js +7 -0
  12. package/dist/ai-service/agent/tools/apis/api-testing-state.js.map +1 -1
  13. package/dist/ai-service/agent/tools/apis/get-api-docs.d.ts +1 -1
  14. package/dist/ai-service/agent/tools/apis/write-api.d.ts +2 -2
  15. package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
  16. package/dist/ai-service/agent/tools/build-capture-screenshot.js +11 -33
  17. package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
  18. package/dist/ai-service/agent/tools/build-copy-directory.d.ts +1 -1
  19. package/dist/ai-service/agent/tools/build-finalize.d.ts.map +1 -1
  20. package/dist/ai-service/agent/tools/build-finalize.js +11 -2
  21. package/dist/ai-service/agent/tools/build-finalize.js.map +1 -1
  22. package/dist/ai-service/agent/tools/build-install-packages.d.ts +41 -1
  23. package/dist/ai-service/agent/tools/build-install-packages.d.ts.map +1 -1
  24. package/dist/ai-service/agent/tools/build-install-packages.js +208 -0
  25. package/dist/ai-service/agent/tools/build-install-packages.js.map +1 -1
  26. package/dist/ai-service/agent/tools/build-lookup-npm-package.d.ts +28 -0
  27. package/dist/ai-service/agent/tools/build-lookup-npm-package.d.ts.map +1 -0
  28. package/dist/ai-service/agent/tools/build-lookup-npm-package.js +78 -0
  29. package/dist/ai-service/agent/tools/build-lookup-npm-package.js.map +1 -0
  30. package/dist/ai-service/agent/tools/build-manage-checklist.d.ts +4 -4
  31. package/dist/ai-service/agent/tools/build-navigate-preview.d.ts +16 -0
  32. package/dist/ai-service/agent/tools/build-navigate-preview.d.ts.map +1 -0
  33. package/dist/ai-service/agent/tools/build-navigate-preview.js +68 -0
  34. package/dist/ai-service/agent/tools/build-navigate-preview.js.map +1 -0
  35. package/dist/ai-service/agent/tools/build-write-file.d.ts +1 -1
  36. package/dist/ai-service/agent/tools/databases/dev-database.d.ts +9 -9
  37. package/dist/ai-service/agent/tools/index.d.ts +2 -0
  38. package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
  39. package/dist/ai-service/agent/tools/index.js +2 -0
  40. package/dist/ai-service/agent/tools/index.js.map +1 -1
  41. package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +3 -3
  42. package/dist/ai-service/agent/tools/report-security-findings.d.ts +11 -5
  43. package/dist/ai-service/agent/tools/report-security-findings.d.ts.map +1 -1
  44. package/dist/ai-service/agent/tools/report-security-findings.js +1 -0
  45. package/dist/ai-service/agent/tools/report-security-findings.js.map +1 -1
  46. package/dist/ai-service/agent/tools.d.ts.map +1 -1
  47. package/dist/ai-service/agent/tools.js +3 -1
  48. package/dist/ai-service/agent/tools.js.map +1 -1
  49. package/dist/ai-service/agent/tools2/tools/git.d.ts +2 -2
  50. package/dist/ai-service/agent/tools2/tools/grep.d.ts +2 -2
  51. package/dist/ai-service/agent/tools2/tools/update-test-case-status.d.ts +2 -2
  52. package/dist/ai-service/app-interface/npm-error-parser.d.ts +161 -0
  53. package/dist/ai-service/app-interface/npm-error-parser.d.ts.map +1 -0
  54. package/dist/ai-service/app-interface/npm-error-parser.js +510 -0
  55. package/dist/ai-service/app-interface/npm-error-parser.js.map +1 -0
  56. package/dist/ai-service/app-interface/npm-package-lookup.d.ts +286 -0
  57. package/dist/ai-service/app-interface/npm-package-lookup.d.ts.map +1 -0
  58. package/dist/ai-service/app-interface/npm-package-lookup.js +793 -0
  59. package/dist/ai-service/app-interface/npm-package-lookup.js.map +1 -0
  60. package/dist/ai-service/app-interface/npm-registry.d.ts +735 -47
  61. package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -1
  62. package/dist/ai-service/app-interface/npm-registry.js +1882 -153
  63. package/dist/ai-service/app-interface/npm-registry.js.map +1 -1
  64. package/dist/ai-service/app-interface/shell.d.ts +118 -1
  65. package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
  66. package/dist/ai-service/app-interface/shell.js +579 -157
  67. package/dist/ai-service/app-interface/shell.js.map +1 -1
  68. package/dist/ai-service/app-skills/manager.d.ts +1 -1
  69. package/dist/ai-service/app-skills/manager.d.ts.map +1 -1
  70. package/dist/ai-service/app-skills/manager.js +39 -7
  71. package/dist/ai-service/app-skills/manager.js.map +1 -1
  72. package/dist/ai-service/checklist/api-migration-checklist-gate.d.ts +9 -0
  73. package/dist/ai-service/checklist/api-migration-checklist-gate.d.ts.map +1 -0
  74. package/dist/ai-service/checklist/api-migration-checklist-gate.js +30 -0
  75. package/dist/ai-service/checklist/api-migration-checklist-gate.js.map +1 -0
  76. package/dist/ai-service/checklist/api-migration-origins.d.ts +4 -0
  77. package/dist/ai-service/checklist/api-migration-origins.d.ts.map +1 -0
  78. package/dist/ai-service/checklist/api-migration-origins.js +8 -0
  79. package/dist/ai-service/checklist/api-migration-origins.js.map +1 -0
  80. package/dist/ai-service/checklist/persisted-checklist-store.d.ts +2 -3
  81. package/dist/ai-service/checklist/persisted-checklist-store.d.ts.map +1 -1
  82. package/dist/ai-service/checklist/persisted-checklist-store.js +6 -7
  83. package/dist/ai-service/checklist/persisted-checklist-store.js.map +1 -1
  84. package/dist/ai-service/context-archive-paths.d.ts +4 -0
  85. package/dist/ai-service/context-archive-paths.d.ts.map +1 -0
  86. package/dist/ai-service/context-archive-paths.js +28 -0
  87. package/dist/ai-service/context-archive-paths.js.map +1 -0
  88. package/dist/ai-service/context-download.d.ts +3 -1
  89. package/dist/ai-service/context-download.d.ts.map +1 -1
  90. package/dist/ai-service/context-download.js +48 -9
  91. package/dist/ai-service/context-download.js.map +1 -1
  92. package/dist/ai-service/context-upload.d.ts +3 -2
  93. package/dist/ai-service/context-upload.d.ts.map +1 -1
  94. package/dist/ai-service/context-upload.js +44 -26
  95. package/dist/ai-service/context-upload.js.map +1 -1
  96. package/dist/ai-service/dev-database-client.d.ts +3 -11
  97. package/dist/ai-service/dev-database-client.d.ts.map +1 -1
  98. package/dist/ai-service/dev-database-client.js.map +1 -1
  99. package/dist/ai-service/features.d.ts +4 -0
  100. package/dist/ai-service/features.d.ts.map +1 -1
  101. package/dist/ai-service/features.js +8 -0
  102. package/dist/ai-service/features.js.map +1 -1
  103. package/dist/ai-service/filter-disabled-tools-for-migration.d.ts.map +1 -1
  104. package/dist/ai-service/filter-disabled-tools-for-migration.js +4 -0
  105. package/dist/ai-service/filter-disabled-tools-for-migration.js.map +1 -1
  106. package/dist/ai-service/index.d.ts +94 -0
  107. package/dist/ai-service/index.d.ts.map +1 -1
  108. package/dist/ai-service/index.js +564 -17
  109. package/dist/ai-service/index.js.map +1 -1
  110. package/dist/ai-service/llm/client.d.ts +38 -3
  111. package/dist/ai-service/llm/client.d.ts.map +1 -1
  112. package/dist/ai-service/llm/client.js +73 -22
  113. package/dist/ai-service/llm/client.js.map +1 -1
  114. package/dist/ai-service/llm/context-v2/adapter.d.ts +3 -1
  115. package/dist/ai-service/llm/context-v2/adapter.d.ts.map +1 -1
  116. package/dist/ai-service/llm/context-v2/adapter.js +11 -11
  117. package/dist/ai-service/llm/context-v2/adapter.js.map +1 -1
  118. package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts +44 -0
  119. package/dist/ai-service/llm/context-v2/compaction/client-side.d.ts.map +1 -0
  120. package/dist/ai-service/llm/context-v2/compaction/client-side.js +170 -0
  121. package/dist/ai-service/llm/context-v2/compaction/client-side.js.map +1 -0
  122. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts +56 -0
  123. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.d.ts.map +1 -0
  124. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.js +9 -0
  125. package/dist/ai-service/llm/context-v2/compaction/compaction-strategy.js.map +1 -0
  126. package/dist/ai-service/llm/context-v2/compaction/index.d.ts +5 -0
  127. package/dist/ai-service/llm/context-v2/compaction/index.d.ts.map +1 -0
  128. package/dist/ai-service/llm/context-v2/compaction/index.js +3 -0
  129. package/dist/ai-service/llm/context-v2/compaction/index.js.map +1 -0
  130. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts +30 -0
  131. package/dist/ai-service/llm/context-v2/compaction/server-side.d.ts.map +1 -0
  132. package/dist/ai-service/llm/context-v2/compaction/server-side.js +130 -0
  133. package/dist/ai-service/llm/context-v2/compaction/server-side.js.map +1 -0
  134. package/dist/ai-service/llm/context-v2/context-management.d.ts +203 -0
  135. package/dist/ai-service/llm/context-v2/context-management.d.ts.map +1 -0
  136. package/dist/ai-service/llm/context-v2/context-management.js +183 -0
  137. package/dist/ai-service/llm/context-v2/context-management.js.map +1 -0
  138. package/dist/ai-service/llm/context-v2/context.d.ts +64 -22
  139. package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
  140. package/dist/ai-service/llm/context-v2/context.js +233 -157
  141. package/dist/ai-service/llm/context-v2/context.js.map +1 -1
  142. package/dist/ai-service/llm/context-v2/conversation-context.d.ts +24 -7
  143. package/dist/ai-service/llm/context-v2/conversation-context.d.ts.map +1 -1
  144. package/dist/ai-service/llm/context-v2/conversation-context.js +1 -1
  145. package/dist/ai-service/llm/context-v2/index.d.ts +0 -4
  146. package/dist/ai-service/llm/context-v2/index.d.ts.map +1 -1
  147. package/dist/ai-service/llm/context-v2/index.js +0 -4
  148. package/dist/ai-service/llm/context-v2/index.js.map +1 -1
  149. package/dist/ai-service/llm/context-v2/manager.d.ts +24 -3
  150. package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
  151. package/dist/ai-service/llm/context-v2/manager.js +146 -4
  152. package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
  153. package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.d.ts +2 -7
  154. package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.d.ts.map +1 -1
  155. package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.js +5 -73
  156. package/dist/ai-service/llm/context-v2/migrations/v1-to-v2.js.map +1 -1
  157. package/dist/ai-service/llm/context-v2/storage/event-store.d.ts +57 -0
  158. package/dist/ai-service/llm/context-v2/storage/event-store.d.ts.map +1 -0
  159. package/dist/ai-service/llm/context-v2/storage/event-store.js +10 -0
  160. package/dist/ai-service/llm/context-v2/storage/event-store.js.map +1 -0
  161. package/dist/ai-service/llm/context-v2/storage/event-types.d.ts +50 -0
  162. package/dist/ai-service/llm/context-v2/storage/event-types.d.ts.map +1 -0
  163. package/dist/ai-service/llm/context-v2/storage/event-types.js +10 -0
  164. package/dist/ai-service/llm/context-v2/storage/event-types.js.map +1 -0
  165. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts +87 -0
  166. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.d.ts.map +1 -0
  167. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js +184 -0
  168. package/dist/ai-service/llm/context-v2/storage/jsonl-event-store.js.map +1 -0
  169. package/dist/ai-service/llm/context-v2/storage/migration.d.ts +49 -0
  170. package/dist/ai-service/llm/context-v2/storage/migration.d.ts.map +1 -0
  171. package/dist/ai-service/llm/context-v2/storage/migration.js +126 -0
  172. package/dist/ai-service/llm/context-v2/storage/migration.js.map +1 -0
  173. package/dist/ai-service/llm/context-v2/types.d.ts +16 -11
  174. package/dist/ai-service/llm/context-v2/types.d.ts.map +1 -1
  175. package/dist/ai-service/llm/context-v2/types.js +2 -2
  176. package/dist/ai-service/llm/context-v2/types.js.map +1 -1
  177. package/dist/ai-service/llm/impl/clark.d.ts +8 -0
  178. package/dist/ai-service/llm/impl/clark.d.ts.map +1 -1
  179. package/dist/ai-service/llm/impl/clark.js +8 -0
  180. package/dist/ai-service/llm/impl/clark.js.map +1 -1
  181. package/dist/ai-service/llm/interaction/adapters/vercel.d.ts.map +1 -1
  182. package/dist/ai-service/llm/interaction/adapters/vercel.js +17 -1
  183. package/dist/ai-service/llm/interaction/adapters/vercel.js.map +1 -1
  184. package/dist/ai-service/llm/provider.d.ts.map +1 -1
  185. package/dist/ai-service/llm/provider.js +1 -3
  186. package/dist/ai-service/llm/provider.js.map +1 -1
  187. package/dist/ai-service/llm/stream/observers/context.d.ts +23 -0
  188. package/dist/ai-service/llm/stream/observers/context.d.ts.map +1 -1
  189. package/dist/ai-service/llm/stream/observers/context.js +62 -0
  190. package/dist/ai-service/llm/stream/observers/context.js.map +1 -1
  191. package/dist/ai-service/llm/tool-context-integrity.d.ts +25 -0
  192. package/dist/ai-service/llm/tool-context-integrity.d.ts.map +1 -0
  193. package/dist/ai-service/llm/tool-context-integrity.js +141 -0
  194. package/dist/ai-service/llm/tool-context-integrity.js.map +1 -0
  195. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts +1 -1
  196. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.d.ts.map +1 -1
  197. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js +6 -1
  198. package/dist/ai-service/skills/system/superblocks-migration/skill.generated.js.map +1 -1
  199. package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts +1 -1
  200. package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts.map +1 -1
  201. package/dist/ai-service/skills/system/third-party-migration/skill.generated.js +3 -2
  202. package/dist/ai-service/skills/system/third-party-migration/skill.generated.js.map +1 -1
  203. package/dist/ai-service/state-machine/clark-fsm.d.ts +23 -2
  204. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  205. package/dist/ai-service/state-machine/clark-fsm.js +37 -2
  206. package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
  207. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  208. package/dist/ai-service/state-machine/handlers/agent-planning.js +29 -2
  209. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  210. package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts.map +1 -1
  211. package/dist/ai-service/state-machine/handlers/awaiting-user.js +9 -0
  212. package/dist/ai-service/state-machine/handlers/awaiting-user.js.map +1 -1
  213. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  214. package/dist/ai-service/state-machine/handlers/llm-generating.js +25 -2
  215. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  216. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts +6 -0
  217. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.d.ts.map +1 -0
  218. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js +36 -0
  219. package/dist/ai-service/state-machine/helpers/fetch-with-reconnect-retry.js.map +1 -0
  220. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts +30 -1
  221. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts.map +1 -1
  222. package/dist/ai-service/state-machine/helpers/stable-peer.js +85 -2
  223. package/dist/ai-service/state-machine/helpers/stable-peer.js.map +1 -1
  224. package/dist/ai-service/state-machine/helpers/user-preferences.d.ts +8 -0
  225. package/dist/ai-service/state-machine/helpers/user-preferences.d.ts.map +1 -0
  226. package/dist/ai-service/state-machine/helpers/user-preferences.js +11 -0
  227. package/dist/ai-service/state-machine/helpers/user-preferences.js.map +1 -0
  228. package/dist/ai-service/template-renderer.d.ts +18 -1
  229. package/dist/ai-service/template-renderer.d.ts.map +1 -1
  230. package/dist/ai-service/template-renderer.js +73 -12
  231. package/dist/ai-service/template-renderer.js.map +1 -1
  232. package/dist/ai-service/types.d.ts +157 -0
  233. package/dist/ai-service/types.d.ts.map +1 -1
  234. package/dist/ai-service/types.js +137 -0
  235. package/dist/ai-service/types.js.map +1 -1
  236. package/dist/ai-service/util/llm-config-utils.d.ts +21 -22
  237. package/dist/ai-service/util/llm-config-utils.d.ts.map +1 -1
  238. package/dist/ai-service/util/llm-config-utils.js +40 -67
  239. package/dist/ai-service/util/llm-config-utils.js.map +1 -1
  240. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  241. package/dist/file-sync-vite-plugin.js +5 -0
  242. package/dist/file-sync-vite-plugin.js.map +1 -1
  243. package/dist/file-system-helpers.d.ts +17 -0
  244. package/dist/file-system-helpers.d.ts.map +1 -1
  245. package/dist/file-system-helpers.js +22 -0
  246. package/dist/file-system-helpers.js.map +1 -1
  247. package/dist/git-service/index.d.ts.map +1 -1
  248. package/dist/git-service/index.js +15 -1
  249. package/dist/git-service/index.js.map +1 -1
  250. package/dist/migration/migration-routes.d.ts.map +1 -1
  251. package/dist/migration/migration-routes.js +26 -3
  252. package/dist/migration/migration-routes.js.map +1 -1
  253. package/dist/migration/translation-prompt.d.ts.map +1 -1
  254. package/dist/migration/translation-prompt.js +5 -0
  255. package/dist/migration/translation-prompt.js.map +1 -1
  256. package/dist/migration-templates/app-fullstack/package.json +2 -2
  257. package/dist/policy-gate-callback-mapper.d.ts +24 -0
  258. package/dist/policy-gate-callback-mapper.d.ts.map +1 -0
  259. package/dist/policy-gate-callback-mapper.js +61 -0
  260. package/dist/policy-gate-callback-mapper.js.map +1 -0
  261. package/dist/policy-gate-runner.d.ts +32 -0
  262. package/dist/policy-gate-runner.d.ts.map +1 -0
  263. package/dist/policy-gate-runner.js +191 -0
  264. package/dist/policy-gate-runner.js.map +1 -0
  265. package/dist/router-parser.d.ts.map +1 -1
  266. package/dist/router-parser.js +107 -11
  267. package/dist/router-parser.js.map +1 -1
  268. package/dist/sync-service/list-dir.d.ts.map +1 -1
  269. package/dist/sync-service/list-dir.js +13 -3
  270. package/dist/sync-service/list-dir.js.map +1 -1
  271. package/package.json +28 -10
@@ -0,0 +1,793 @@
1
+ import { metrics, } from "@opentelemetry/api";
2
+ import semver from "semver";
3
+ import z from "zod";
4
+ import { getLogger } from "../../util/logger.js";
5
+ /**
6
+ * Meter name for the npm-package-lookup gauge. Picked to match the
7
+ * `superblocks.npm.*` instrument family established by APPS-4189
8
+ * (`superblocks.npm.install.*` counter + histogram); the meter is the
9
+ * "instrumentation scope" the OTel collector tags every emitted metric
10
+ * with, so keeping the npm tools under one scope name makes the family
11
+ * easy to filter on in Grafana.
12
+ */
13
+ const METER_NAME = "superblocks.npm";
14
+ /**
15
+ * Resolve the cache-size gauge against the *currently registered* global
16
+ * meter provider. Resolved per-instance (in the constructor) rather than
17
+ * memoized at module load because:
18
+ *
19
+ * 1. The global meter provider can be installed after this module is
20
+ * imported — telemetry init is async and runs alongside the rest of
21
+ * the dev-server bootstrap. A memoized lookup taken at import time
22
+ * would bind to the no-op meter and never emit.
23
+ * 2. Tests swap the global meter provider per case (so each case has its
24
+ * own in-memory exporter). A memoized gauge would observe through a
25
+ * provider that's been disabled, missing the assertion.
26
+ *
27
+ * The OpenTelemetry SDK already de-duplicates same-name instruments inside
28
+ * the same meter (returns the existing one and logs a warning the first
29
+ * time a mismatched description is seen), so resolving per-instance is
30
+ * cheap; we're not creating multiple physical instruments.
31
+ */
32
+ function resolveCacheSizeGauge() {
33
+ return metrics
34
+ .getMeter(METER_NAME)
35
+ .createObservableGauge("superblocks.npm.lookup.cache_size", {
36
+ description: "Current number of entries in the NpmPackageLookup in-memory cache (successful lookups and 'not_in_registry' misses; transient failures are not cached).",
37
+ unit: "{entry}",
38
+ });
39
+ }
40
+ /**
41
+ * Resolve the eviction counter against the *currently registered* global
42
+ * meter provider. Mirrors `resolveCacheSizeGauge` (same per-instance
43
+ * resolution rationale, same `superblocks.npm.*` instrument family) — the
44
+ * gauge reports the live footprint, this counter reports the cumulative
45
+ * number of entries the size cap has forced out so the two can be read
46
+ * together (sustained eviction at a flat `cache_size` = the cap is the
47
+ * bottleneck, not just churn).
48
+ */
49
+ function resolveCacheEvictionCounter() {
50
+ return metrics
51
+ .getMeter(METER_NAME)
52
+ .createCounter("superblocks.npm.lookup.cache_evictions", {
53
+ description: "Cumulative number of entries evicted from the NpmPackageLookup in-memory cache to stay under the size cap (expired entries first, then oldest-expiring).",
54
+ unit: "{entry}",
55
+ });
56
+ }
57
+ const DEFAULT_TTL_MS = 60_000;
58
+ const DEFAULT_FETCH_TIMEOUT_MS = 5_000;
59
+ /**
60
+ * Hard cap on the in-memory cache size. The lookup instance lives for the
61
+ * whole dev-server process, and TTL is only checked reactively on read, so
62
+ * without a cap a long session full of unique lookups (one per package the
63
+ * agent ever considers) would grow the heap unbounded — expired entries
64
+ * linger until they're re-accessed, which a never-repeated name never is.
65
+ * On insert at/over the cap we evict expired entries first, then the
66
+ * oldest-expiring entries until under the cap (see `evictToCapacity`).
67
+ * Sized well above the sibling caches (`scoped-token-utils` = 100,
68
+ * `secret-scanner` = 200) because a npm name is a far smaller value than a
69
+ * token or a scan result and a single Clark session can legitimately touch
70
+ * thousands of distinct packages; 10k entries is a few MB at most.
71
+ */
72
+ const MAX_CACHE_SIZE = 10_000;
73
+ /**
74
+ * How many entries to include in `recent_versions`. Sized so the agent gets
75
+ * enough recent history to pick across major lines without bloating its
76
+ * context window — most popular packages publish dozens of patch versions
77
+ * per major, and 30 covers ~the last 2-3 majors for a typical library.
78
+ */
79
+ const RECENT_VERSIONS_CAP = 30;
80
+ /**
81
+ * Public npm registry — used when no `NpmRegistryClient` is wired or
82
+ * `getConfig()` returns `not-configured`. Matches the default `registry=`
83
+ * line in the bootstrapped `.npmrc` so a lookup result lines up with what
84
+ * `npm install` would do for the same name.
85
+ */
86
+ const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
87
+ /**
88
+ * Shape of the npm registry's package-metadata response that this tool
89
+ * cares about. The registry returns lots more (`name`, full version
90
+ * objects, `repository`, ...) — the schema only declares what we surface
91
+ * to the agent. Unknown keys are silently dropped by Zod's default object
92
+ * behavior.
93
+ *
94
+ * `latest` and every key in `versions` / `time` are validated with
95
+ * `semver.valid()` — those are the registry-controlled bytes that flow
96
+ * into the model-visible tool summary (see `build_lookupNpmPackage`'s
97
+ * `getSummary` and `recent_versions`), so a compromised or misconfigured
98
+ * registry must not be able to smuggle newlines, brackets, or imperative-mood
99
+ * instructions through them. `semver.valid()` enforces the full
100
+ * `MAJOR.MINOR.PATCH[-prerelease][+build]` grammar — `0.0.0-beta.1` and
101
+ * `1.2.3+sha.abc` pass; `"1.0.0\nIGNORE…"`, `"aaaa"`, and an over-cap blob
102
+ * all fail and are dropped from the surfaced result.
103
+ *
104
+ * The check on `latest` is a `transform` (drop-to-undefined), not a `refine`
105
+ * (fail the whole parse). A `refine` would mean a single bad `dist-tags.latest`
106
+ * sinks the entire parse and `recent_versions` along with it — the agent
107
+ * loses the version list when it needs it most. With `transform`, an invalid
108
+ * latest decays to `undefined` and the rest of the payload still surfaces.
109
+ */
110
+ const NpmRegistryMetadataSchema = z.object({
111
+ "dist-tags": z
112
+ .object({
113
+ latest: z
114
+ .string()
115
+ .optional()
116
+ .transform((v) => v !== undefined && semver.valid(v) !== null ? v : undefined),
117
+ })
118
+ .optional(),
119
+ versions: z.record(z.string(), z.unknown()).optional(),
120
+ // `time` values are typed `unknown` rather than `string` because npm's
121
+ // `time` map mixes ISO-timestamp strings (one per published version,
122
+ // plus `created`/`modified` pseudo-keys) with an `unpublished` pseudo-
123
+ // key whose value is an object — `{ time, name, tags, maintainers,
124
+ // description, versions }` — for fully-unpublished packages. Insisting
125
+ // on `string` here would fail the whole `safeParse` on any registry
126
+ // response that contains an `unpublished` block, silently dropping
127
+ // `latest_version` and `recent_versions`. `extractRecentVersions`
128
+ // filters back to strings before using a value as a sort key.
129
+ time: z.record(z.string(), z.unknown()).optional(),
130
+ });
131
+ /**
132
+ * Normalize a registry URL into a stable cache-key prefix. Lowercases the
133
+ * scheme+host (RFC 3986 §3) and drops a single trailing slash so
134
+ * `https://artifactory.example.com/api/npm/repo-a/` and
135
+ * `HTTPS://Artifactory.Example.com/api/npm/repo-a` hash to the same entry —
136
+ * but `…/repo-a/` and `…/repo-b/` stay distinct, which is the property
137
+ * Artifactory-style virtual repos depend on. The path is preserved
138
+ * case-sensitively because registry sub-paths are usually case-sensitive
139
+ * (Artifactory treats `/repo-a` and `/Repo-A` as different repos).
140
+ */
141
+ function normalizeRegistryUrl(raw) {
142
+ try {
143
+ const u = new URL(raw);
144
+ const origin = `${u.protocol.toLowerCase()}//${u.host.toLowerCase()}`;
145
+ const path = u.pathname.replace(/\/$/, "");
146
+ return `${origin}${path}`;
147
+ }
148
+ catch {
149
+ // Malformed URL — fall back to the raw string trimmed of trailing
150
+ // slashes. This shouldn't happen in practice because `resolveRegistry`
151
+ // only ever returns URLs that the `new URL(registry.url)` call above
152
+ // it accepted, but the cache-key path must not throw.
153
+ return raw.replace(/\/$/, "");
154
+ }
155
+ }
156
+ /**
157
+ * Encode an npm package name into the URL-path segment npm registries expect.
158
+ *
159
+ * For scoped packages (`@scope/name`) the `/` is percent-encoded to `%2F` so
160
+ * the path collapses to a single registry segment — npm, Artifactory, and
161
+ * Verdaccio all accept that form, and it sidesteps the ambiguity where a
162
+ * literal `/` could be confused with a registry sub-path on some proxies.
163
+ * The leading `@` is left as-is to match npm CLI URL shape, but the rest of
164
+ * the scope name is percent-encoded along with the sub-name so a hostile or
165
+ * malformed input (`@evil?probe=1/pkg`) cannot smuggle query-/path-segment
166
+ * characters into the authenticated request.
167
+ */
168
+ function encodePackagePath(name) {
169
+ if (name.startsWith("@")) {
170
+ const slash = name.indexOf("/");
171
+ if (slash > 0) {
172
+ const scope = name.slice(1, slash);
173
+ const sub = name.slice(slash + 1);
174
+ return `@${encodeURIComponent(scope)}%2F${encodeURIComponent(sub)}`;
175
+ }
176
+ }
177
+ return encodeURIComponent(name);
178
+ }
179
+ /**
180
+ * Pick the registry that `npm install` would route a given package name to,
181
+ * given the current `NpmRegistryConfig`. Mirrors npm's scope-mapping rule:
182
+ * `@scope/foo` matches `config.scopes["@scope"]` if present, otherwise the
183
+ * default registry. Unscoped names always use the default.
184
+ *
185
+ * Returns `undefined` only when the config has scope mappings but no default
186
+ * AND the requested name doesn't match any scope — in that case we have no
187
+ * URL to hit. The caller falls back to public npm; `npm install` would do
188
+ * the same thing.
189
+ */
190
+ function selectRegistry(config, name) {
191
+ if (name.startsWith("@")) {
192
+ const slash = name.indexOf("/");
193
+ if (slash > 0) {
194
+ const scope = name.slice(0, slash);
195
+ const scoped = config.scopes?.[scope];
196
+ if (scoped) {
197
+ return scoped;
198
+ }
199
+ }
200
+ }
201
+ return config.default;
202
+ }
203
+ /**
204
+ * Pre-flight check for npm package availability against the registry
205
+ * Clark's install path would actually use. Designed so the agent can call
206
+ * this BEFORE attempting `npm install` and either skip a doomed install
207
+ * (the registry doesn't carry the package), pick a different package up
208
+ * front, or pick a recent version from `recent_versions` — rather than
209
+ * burning ~30s on a build pod recovering from `NpmInstallBlocked` after
210
+ * the fact.
211
+ *
212
+ * Registry resolution per call (mirrors `npm install`'s scope-mapping):
213
+ *
214
+ * 1. `await client.getConfig()` if a client is wired.
215
+ * - `source: configured | stale` → use `config.scopes[@scope]` for a
216
+ * scoped name; else `config.default`. Both yield `mode: "private"`.
217
+ * If neither matches (scoped-only mappings, no default, no scope
218
+ * match) the result is `mode: "fail-closed"` with
219
+ * `reason: "registry_unreachable"` — see `resolveRegistry`.
220
+ * - `source: not-configured` → public npm (`mode: "public-npm"`).
221
+ * - `source: unreachable`, or `getConfig()` threw → `mode: "fail-closed"`.
222
+ * 2. No client wired → public npm (`mode: "public-npm"`).
223
+ * Matches the default `registry=` line we bootstrap into every app's
224
+ * `.npmrc`.
225
+ *
226
+ * Behaviour matrix (post-resolve):
227
+ *
228
+ * | HTTP 200 | `available: true`, `latest_version`, `recent_versions` |
229
+ * | HTTP 404 | `available: false, reason: not_in_registry` |
230
+ * | HTTP 401 / 403 | `available: false, reason: registry_auth_failed` |
231
+ * | HTTP 5xx / DNS / TCP / TLS / fetch throw | `available: false, reason: registry_unreachable` |
232
+ *
233
+ * `mode: "public-npm"` + `reason: "registry_unreachable"` is the strong
234
+ * signal that the operator's network blocks egress to npmjs.org without
235
+ * a configured private registry; the agent can surface this as a
236
+ * connectivity / configuration issue rather than a missing package.
237
+ * `mode: "fail-closed"` + `reason: "registry_unreachable"` covers the
238
+ * cases where the private-registry config itself is in a bad state
239
+ * (auth, transient unreachable) or doesn't cover the requested name
240
+ * (scoped-only mappings, no default, no scope match).
241
+ *
242
+ * Token hygiene: the token comes back from `client.getConfig()` per call
243
+ * and lives only in a local inside `lookup()`. Nothing token-bearing is
244
+ * stored on the instance, so `JSON.stringify(this)` cannot surface it.
245
+ * Bodies of 4xx/5xx responses are drained without inspection — a hostile
246
+ * registry could echo the submitted token back in a 401 body, and the only
247
+ * way to guarantee the token never reaches the tool output / log / span is
248
+ * to never read those bytes at all.
249
+ *
250
+ * Caching: per-instance, in-memory, keyed on
251
+ * `<normalized-registry-base-url>:<name>` with a 60s TTL. The full base
252
+ * URL (not just the host) is required because Artifactory-style proxies
253
+ * host multiple npm repos under one host (`…/api/npm/repo-a/` vs
254
+ * `…/api/npm/repo-b/`) — keying on host alone would let a 200 or 404
255
+ * from one virtual repo bleed into the other. Per-process (the dev-server
256
+ * runs a single AppShell), no cross-pod sharing — same as
257
+ * `NpmRegistryClient`. Only stable outcomes (`available: true` and
258
+ * `not_in_registry`) are cached; `registry_auth_failed` and
259
+ * `registry_unreachable` are transient and must recover on the next call
260
+ * (otherwise a token rotation or a short network blip would pin the
261
+ * failure for a full minute). The URL-keyed prefix also means a swap
262
+ * between a private registry and the public-npm fallback can't accidentally
263
+ * serve a result from the wrong source. The cache is bounded at
264
+ * `MAX_CACHE_SIZE` entries (APPS-4429): TTL is checked only reactively on
265
+ * read, so without a cap a long session of unique lookups would grow the
266
+ * heap unbounded. On insert at/over the cap, `evictToCapacity` drops expired
267
+ * entries first, then the oldest-expiring live entries, emitting the count
268
+ * on the `cache_evictions` counter.
269
+ *
270
+ * In-flight dedup: concurrent `lookup(name)` calls that miss the cache and
271
+ * resolve to the same cache key share a single fetch — the first caller
272
+ * stores the pending promise in an inflight map; subsequent callers await
273
+ * it instead of firing a second HTTP request. Clark batch-installs (one
274
+ * lookup per package) hit this path regularly. The slot is cleared in
275
+ * `.finally()` so a transient-failure resolution doesn't pin every future
276
+ * caller to the same rejected promise. See `inFlight` for the keying
277
+ * contract (same string as the cache, so two scopes never share a result).
278
+ */
279
+ export class NpmPackageLookup {
280
+ client;
281
+ fetchImpl;
282
+ nowFn;
283
+ ttlMs;
284
+ fetchTimeoutMs;
285
+ cache = new Map();
286
+ /**
287
+ * In-flight fetches keyed by the same `<normalized-registry-base-url>:<name>`
288
+ * string the cache uses. The first caller for a given key kicks off the
289
+ * fetch and stores the pending promise here; subsequent concurrent callers
290
+ * (Clark batch-installs hit this regularly) await the same promise instead
291
+ * of firing a second HTTP request against the registry. The slot is cleared
292
+ * in `.finally()` regardless of fetch outcome — including the transient
293
+ * failure paths (`registry_auth_failed`, `registry_unreachable`) that the
294
+ * cache deliberately skips — so a single failed fetch can't pin every
295
+ * future caller to the same rejected promise.
296
+ *
297
+ * Keying on the same string as `cache` (not on `name` alone) means
298
+ * concurrent lookups for the same package name routed to different
299
+ * registries (scoped vs. default, or a swap between private and public)
300
+ * each get their own inflight slot and never share a result across trust
301
+ * boundaries.
302
+ */
303
+ inFlight = new Map();
304
+ /**
305
+ * Resolved gauge and bound callback held so `removeCallback` can detach.
306
+ * The gauge handle is captured at construction so `dispose()` always
307
+ * detaches from the same instrument it attached to, even if the global
308
+ * meter provider has been replaced in the meantime.
309
+ */
310
+ gauge;
311
+ gaugeCallback;
312
+ /**
313
+ * Counter for entries the size cap forces out (see `evictToCapacity`).
314
+ * Cumulative, no labels — same partitioning rationale as the gauge. Held
315
+ * `undefined` when `skipMetrics` is set so tests don't leak instruments
316
+ * against the process-global meter.
317
+ */
318
+ evictionCounter;
319
+ constructor(deps = {}) {
320
+ this.client = deps.client;
321
+ this.fetchImpl = deps.fetch ?? globalThis.fetch;
322
+ this.nowFn = deps.now ?? Date.now;
323
+ this.ttlMs = deps.ttlMs ?? DEFAULT_TTL_MS;
324
+ this.fetchTimeoutMs = deps.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
325
+ if (!deps.skipMetrics) {
326
+ // The callback observes `cache.size` directly. No labels — the meter's
327
+ // resource attributes (`service.name`, `deployment.environment.name`)
328
+ // already partition by process; further per-instance attributes would
329
+ // double-count when more than one `NpmPackageLookup` lives in the same
330
+ // process (e.g. if a future Clark code path constructs a second one).
331
+ // OTel resolves duplicate observations for the same gauge+attributes by
332
+ // taking the last value within a collection cycle, so even then we'd
333
+ // surface only one of them; emitting them under separate attribute sets
334
+ // would inflate the series count without adding signal.
335
+ this.gauge = resolveCacheSizeGauge();
336
+ this.gaugeCallback = (result) => {
337
+ result.observe(this.cache.size);
338
+ };
339
+ this.gauge.addCallback(this.gaugeCallback);
340
+ this.evictionCounter = resolveCacheEvictionCounter();
341
+ }
342
+ }
343
+ /**
344
+ * Detach the cache-size gauge callback. Call when retiring a long-lived
345
+ * instance (e.g. tearing down a Clark session that owned its own lookup).
346
+ * The OTel API keeps a strong reference to every registered callback, so
347
+ * letting the instance go out of scope without calling this leaves the
348
+ * callback (and the cache it closes over) alive for the lifetime of the
349
+ * process meter. Production code paths that hold the lookup for the
350
+ * lifetime of the process never need to call this; it exists so tests and
351
+ * future cleanup paths can detach cleanly.
352
+ */
353
+ dispose() {
354
+ if (this.gauge && this.gaugeCallback) {
355
+ this.gauge.removeCallback(this.gaugeCallback);
356
+ }
357
+ }
358
+ async lookup(name) {
359
+ const resolved = await this.resolveRegistry(name);
360
+ if (resolved === "fail-closed") {
361
+ // We have evidence of a private-registry org but no safe destination
362
+ // for this name (hard-fail auth, or a transient `source: unreachable`).
363
+ // Returning `registry_unreachable` with `mode: "fail-closed"` tells the
364
+ // agent "the lookup didn't run" without leaking a potentially-private
365
+ // package name to npmjs.org.
366
+ return {
367
+ available: false,
368
+ mode: "fail-closed",
369
+ reason: "registry_unreachable",
370
+ registry_host: "",
371
+ };
372
+ }
373
+ const { registry, mode } = resolved;
374
+ const registryBase = registry.url.replace(/\/$/, "");
375
+ const registryHost = new URL(registry.url).host;
376
+ // Key on the normalized registry base URL — host alone collides on
377
+ // Artifactory-style proxies that host multiple npm repos under one
378
+ // host (e.g. `…/api/npm/repo-a/` vs `…/api/npm/repo-b/`). Lowercasing
379
+ // the scheme+host (per RFC 3986 §3) and dropping the trailing slash
380
+ // gives a stable identity for the same registry across capitalisation
381
+ // and slash-trailing variants in the config. `registry_host` in the
382
+ // result keeps the bare host — the cache-key shape is an internal
383
+ // detail callers don't need.
384
+ const cacheKey = `${normalizeRegistryUrl(registry.url)}:${name}`;
385
+ const now = this.nowFn();
386
+ const cached = this.cache.get(cacheKey);
387
+ if (cached && cached.expiresAt > now) {
388
+ return cached.result;
389
+ }
390
+ // Share a single in-flight fetch across concurrent callers for the same
391
+ // key (Clark batch-installs are the hot path). `inFlight.set` happens
392
+ // synchronously before the first `await`, so a second call that enters
393
+ // `lookup()` while the first is still pending always sees the existing
394
+ // promise here and skips the duplicate fetch.
395
+ const inFlight = this.inFlight.get(cacheKey);
396
+ if (inFlight) {
397
+ return inFlight;
398
+ }
399
+ const url = `${registryBase}/${encodePackagePath(name)}`;
400
+ const headers = { accept: "application/json" };
401
+ if (registry.token) {
402
+ headers.authorization = `Bearer ${registry.token}`;
403
+ }
404
+ const promise = this.fetchAndMap(url, headers, registryHost, mode)
405
+ .then((result) => {
406
+ // Cache only stable outcomes. Transient failures must retry on the
407
+ // next call so a token rotation or a short network blip can't pin
408
+ // the failure for a full TTL window.
409
+ if (result.available || result.reason === "not_in_registry") {
410
+ // Bound the cache before inserting a *new* key. Re-inserting an
411
+ // existing key (the common refetch-after-TTL path) is a Map
412
+ // overwrite, not growth, so it never needs eviction — gating on
413
+ // `!has(cacheKey)` keeps the steady-state hot path free of the
414
+ // eviction scan.
415
+ if (!this.cache.has(cacheKey)) {
416
+ this.evictToCapacity(now);
417
+ }
418
+ this.cache.set(cacheKey, { result, expiresAt: now + this.ttlMs });
419
+ }
420
+ return result;
421
+ })
422
+ .finally(() => {
423
+ // Always clear the inflight slot, including on the transient-failure
424
+ // paths the cache skips. Otherwise a single failed fetch would pin
425
+ // every future caller to the same rejected promise.
426
+ this.inFlight.delete(cacheKey);
427
+ });
428
+ this.inFlight.set(cacheKey, promise);
429
+ return promise;
430
+ }
431
+ /**
432
+ * Number of cached entries (successful lookups and `not_in_registry`
433
+ * misses; transient failures aren't cached). Exposed as a gauge for
434
+ * monitoring — keeps the cache footprint visible without poking at
435
+ * private state from outside the class.
436
+ */
437
+ size() {
438
+ return this.cache.size;
439
+ }
440
+ /**
441
+ * Make room for one new entry when the cache is at/over `MAX_CACHE_SIZE`.
442
+ * Called from the insert path before adding a key that isn't already
443
+ * present.
444
+ *
445
+ * Eviction order, keyed on `expiresAt`:
446
+ *
447
+ * 1. Expired entries first (`expiresAt <= now`). TTL is only checked
448
+ * reactively on read, so a never-re-accessed name's entry would
449
+ * otherwise linger forever; dropping the already-dead ones reclaims
450
+ * space without evicting anything a future read could have used.
451
+ * 2. If that didn't get us under the cap, the oldest-expiring live
452
+ * entries (smallest `expiresAt`) — those are closest to expiry anyway,
453
+ * so they're the cheapest live entries to lose.
454
+ *
455
+ * Sorting the whole keyset is O(n log n), but it only runs once the cache
456
+ * is full (n = `MAX_CACHE_SIZE`) and only on inserts of genuinely-new keys,
457
+ * so it's off the steady-state hot path. Emits the number evicted on the
458
+ * eviction counter so a sustained-eviction-at-flat-size signal is visible
459
+ * in Grafana alongside the existing `cache_size` gauge.
460
+ */
461
+ evictToCapacity(now) {
462
+ if (this.cache.size < MAX_CACHE_SIZE) {
463
+ return;
464
+ }
465
+ // Target one slot below the cap so the imminent `set` lands at the cap,
466
+ // not over it.
467
+ const target = MAX_CACHE_SIZE - 1;
468
+ let evicted = 0;
469
+ // Pass 1: expired entries.
470
+ for (const [key, entry] of this.cache) {
471
+ if (this.cache.size <= target) {
472
+ break;
473
+ }
474
+ if (entry.expiresAt <= now) {
475
+ this.cache.delete(key);
476
+ evicted += 1;
477
+ }
478
+ }
479
+ // Pass 2: oldest-expiring live entries, if still over the cap.
480
+ if (this.cache.size > target) {
481
+ const byExpiry = Array.from(this.cache.entries()).sort((a, b) => a[1].expiresAt - b[1].expiresAt);
482
+ for (const [key] of byExpiry) {
483
+ if (this.cache.size <= target) {
484
+ break;
485
+ }
486
+ this.cache.delete(key);
487
+ evicted += 1;
488
+ }
489
+ }
490
+ if (evicted > 0) {
491
+ this.evictionCounter?.add(evicted);
492
+ }
493
+ }
494
+ /**
495
+ * Resolve the registry this lookup will hit:
496
+ *
497
+ * - No client wired (SaaS mode): public npm.
498
+ * - `source: not-configured` (operator never opted in): public npm.
499
+ * - `source: configured | stale`, name matches `config.scopes[@scope]`:
500
+ * scoped private registry.
501
+ * - `source: configured | stale`, no scope match, `config.default`
502
+ * present: default private registry.
503
+ *
504
+ * Fail-closed (returns `"fail-closed"`; caller surfaces
505
+ * `registry_unreachable` and never touches the network):
506
+ *
507
+ * - `getConfig()` threw on hard-fail auth (revoked JWT, double 401
508
+ * against the Superblocks server). In that state we'd silently
509
+ * downgrade from "checking the private registry" to "checking
510
+ * public npm" — different trust boundary, different answer. The
511
+ * agent's reasoning about install feasibility depends on which
512
+ * registry actually answered; failing closed surfaces the auth
513
+ * problem instead of hiding it behind a misleading public-npm
514
+ * result.
515
+ * - `source: unreachable` AND `everConfigured === true` (transient
516
+ * cold-cache outage against the Superblocks server for an org that
517
+ * has at some prior point successfully resolved a configured
518
+ * registry). Same trust gap as the throw branch: we don't know the
519
+ * org's current registry state, but the on-disk marker says it has
520
+ * a private registry, so leaking the name to npmjs would be a silent
521
+ * supply-chain leak during the outage window. Symmetric with
522
+ * `syncHomeNpmrc` and `maybeWriteNpmrcForDir`, both of which refuse
523
+ * to act on `unreachable` for the same reason.
524
+ *
525
+ * When `everConfigured` is `false` (greenfield org / no disk marker)
526
+ * or `undefined` (no store wired in — older call sites, test
527
+ * fixtures), we do NOT fail closed. The `unreachable` config is
528
+ * `{ configured: false }`, so we proceed exactly as the install path
529
+ * does (`AppShell.maybeShortCircuitForUnreachable` only blocks when
530
+ * `everConfigured === true`): fall through to public npm. A
531
+ * greenfield org has no private registry, so a metadata lookup
532
+ * against public npm during a config-server outage leaks nothing
533
+ * sensitive — and keeping the lookup aligned with the install gate
534
+ * removes the lookup-vs-install divergence where the lookup reported
535
+ * `fail-closed` while a direct install of the same package would
536
+ * have proceeded (APPS-4425).
537
+ * - `source: configured | stale`, no scope match, no `config.default`.
538
+ * The operator wired scoped-only mappings and the requested name
539
+ * doesn't match any of them — there's no in-policy destination, so
540
+ * answering from npmjs.org would report availability from the wrong
541
+ * trust boundary. Same shape as the other two: surface the gap to
542
+ * the agent rather than silently downgrading.
543
+ */
544
+ async resolveRegistry(name) {
545
+ if (!this.client) {
546
+ return {
547
+ registry: { url: PUBLIC_NPM_REGISTRY },
548
+ mode: "public-npm",
549
+ };
550
+ }
551
+ let fetchResult;
552
+ try {
553
+ fetchResult = await this.client.getConfig();
554
+ }
555
+ catch (err) {
556
+ getLogger().warn("[npm-package-lookup] registry client getConfig threw; failing closed rather than silently downgrading to public npm", { error: err instanceof Error ? err.message : String(err) });
557
+ return "fail-closed";
558
+ }
559
+ if (fetchResult.source === "unreachable") {
560
+ // Transient cold-cache outage against the Superblocks server. Gate
561
+ // the fail-closed decision on `everConfigured` so the lookup matches
562
+ // the install gate (`AppShell.maybeShortCircuitForUnreachable`), which
563
+ // blocks only when `everConfigured === true` (APPS-4425).
564
+ if (fetchResult.everConfigured === true) {
565
+ // The on-disk marker says this org has at some prior point resolved
566
+ // a configured private registry. We don't know its current state
567
+ // during the outage, so the same trust-gap argument as the
568
+ // `getConfig threw` branch applies: falling through to public npm
569
+ // would leak the requested name — which the agent picked precisely
570
+ // because it's targeting the (possibly-private) registry — to
571
+ // npmjs.org. Fail-closed; caller surfaces `registry_unreachable`.
572
+ // Symmetric with `syncHomeNpmrc` and `maybeWriteNpmrcForDir`, both
573
+ // of which refuse to act on `source: "unreachable"`.
574
+ getLogger().warn("[npm-package-lookup] registry config unreachable for a previously-configured org; failing closed rather than silently downgrading to public npm");
575
+ return "fail-closed";
576
+ }
577
+ // `everConfigured` is `false` (greenfield org / no disk marker) or
578
+ // `undefined` (no store wired in). A greenfield org has no private
579
+ // registry, so a metadata lookup against public npm during the
580
+ // config-server outage leaks nothing sensitive — and a direct
581
+ // install of the same name would proceed through public npm anyway,
582
+ // since the install path only short-circuits when `everConfigured ===
583
+ // true`. Proceed to public npm so the lookup doesn't diverge from
584
+ // install reality. The `unreachable` config is `{ configured: false }`,
585
+ // so this is the same destination `not-configured` resolves to.
586
+ return {
587
+ registry: { url: PUBLIC_NPM_REGISTRY },
588
+ mode: "public-npm",
589
+ };
590
+ }
591
+ if (fetchResult.source === "not-configured") {
592
+ return {
593
+ registry: { url: PUBLIC_NPM_REGISTRY },
594
+ mode: "public-npm",
595
+ };
596
+ }
597
+ const selected = selectRegistry(fetchResult.config, name);
598
+ if (selected) {
599
+ return { registry: selected, mode: "private" };
600
+ }
601
+ // Configured org with scoped-only mappings and no default — the
602
+ // requested name doesn't match any scope, so we have no in-policy
603
+ // destination. Fail closed rather than silently downgrading to public
604
+ // npm: the operator's `npm-registry` config implies a trust boundary
605
+ // (only mapped scopes are blessed for install), and answering
606
+ // `available: true` from npmjs.org for `react` against an org that
607
+ // never wired a default registry reports availability from the wrong
608
+ // boundary. The agent should treat this as a configuration problem,
609
+ // not a green-light to install. (Symmetric with the `getConfig`-threw
610
+ // and `source: unreachable` branches above.)
611
+ getLogger().warn("[npm-package-lookup] configured registry has scopes but no default and the requested name does not match any scope; failing closed rather than silently downgrading to public npm", { name });
612
+ return "fail-closed";
613
+ }
614
+ async fetchAndMap(url, headers, registryHost, mode) {
615
+ let response;
616
+ try {
617
+ response = await this.fetchImpl(url, {
618
+ method: "GET",
619
+ headers,
620
+ signal: AbortSignal.timeout(this.fetchTimeoutMs),
621
+ });
622
+ }
623
+ catch (err) {
624
+ getLogger().warn("[npm-package-lookup] registry unreachable", {
625
+ registryHost,
626
+ error: err instanceof Error ? err.message : String(err),
627
+ });
628
+ return {
629
+ available: false,
630
+ mode,
631
+ reason: "registry_unreachable",
632
+ registry_host: registryHost,
633
+ };
634
+ }
635
+ if (response.status === 200) {
636
+ let latest;
637
+ let recent;
638
+ try {
639
+ const parsed = NpmRegistryMetadataSchema.safeParse(await response.json());
640
+ if (parsed.success) {
641
+ latest = parsed.data["dist-tags"]?.latest;
642
+ recent = extractRecentVersions(parsed.data);
643
+ }
644
+ }
645
+ catch {
646
+ // 200 with non-JSON body: package still exists; just no latest
647
+ // and no version list.
648
+ }
649
+ return {
650
+ available: true,
651
+ mode,
652
+ registry_host: registryHost,
653
+ ...(latest ? { latest_version: latest } : {}),
654
+ ...(recent && recent.length > 0 ? { recent_versions: recent } : {}),
655
+ };
656
+ }
657
+ if (response.status === 404) {
658
+ await drainSilently(response);
659
+ return {
660
+ available: false,
661
+ mode,
662
+ reason: "not_in_registry",
663
+ registry_host: registryHost,
664
+ };
665
+ }
666
+ if (response.status === 401 || response.status === 403) {
667
+ getLogger().warn("[npm-package-lookup] registry auth failed", {
668
+ registryHost,
669
+ status: response.status,
670
+ });
671
+ await drainSilently(response);
672
+ return {
673
+ available: false,
674
+ mode,
675
+ reason: "registry_auth_failed",
676
+ registry_host: registryHost,
677
+ };
678
+ }
679
+ // 5xx, unexpected 4xx, etc. → treat as a transient infra problem rather
680
+ // than `not_in_registry`. Mapping them to `not_in_registry` would let a
681
+ // flaky proxy silently convince Clark the package is missing.
682
+ getLogger().warn("[npm-package-lookup] registry returned unexpected status", {
683
+ registryHost,
684
+ status: response.status,
685
+ });
686
+ await drainSilently(response);
687
+ return {
688
+ available: false,
689
+ mode,
690
+ reason: "registry_unreachable",
691
+ registry_host: registryHost,
692
+ };
693
+ }
694
+ }
695
+ /**
696
+ * Pull recent published versions out of a parsed npm metadata response,
697
+ * capped at `RECENT_VERSIONS_CAP`. Only `semver.valid()` strings are kept
698
+ * so a hostile or misconfigured registry can't smuggle non-version bytes
699
+ * into the model-visible output.
700
+ *
701
+ * Ordering:
702
+ * - If the `time` map is present (npmjs, Verdaccio, and most proxies
703
+ * include it), sort versions that have a string timestamp by
704
+ * published-at descending. This gives the agent the most-recently-
705
+ * released versions first, which matches the "use a recent version"
706
+ * instruction in the base prompt.
707
+ * - Versions present in `versions` but missing a string timestamp in
708
+ * `time` (some private proxies drop per-version entries while keeping
709
+ * others, or npm's object-valued `unpublished` pseudo-key takes the
710
+ * slot a real timestamp would have occupied) sort by semver descending
711
+ * and are appended AFTER the timestamped block. Bucketing this way
712
+ * keeps the comparator transitive — a single mixed comparator that
713
+ * falls back to semver per-pair-when-either-side-is-missing is
714
+ * non-transitive (time-order and semver-order can disagree, producing
715
+ * cycles), which makes JS `Array.prototype.sort` undefined behaviour.
716
+ * - If `time` is absent entirely, sort by semver descending. Some
717
+ * private registries strip `time` from their proxy responses; fall
718
+ * back to a deterministic ordering rather than returning nothing.
719
+ *
720
+ * Pseudo-keys in the `time` map (`created`, `modified`, `unpublished`) are
721
+ * dropped — they aren't real versions, so `semver.valid()` rejects them.
722
+ */
723
+ function extractRecentVersions(parsed) {
724
+ const candidateSet = new Set();
725
+ if (parsed.versions) {
726
+ for (const key of Object.keys(parsed.versions)) {
727
+ if (semver.valid(key) !== null) {
728
+ candidateSet.add(key);
729
+ }
730
+ }
731
+ }
732
+ if (parsed.time) {
733
+ for (const key of Object.keys(parsed.time)) {
734
+ if (semver.valid(key) !== null) {
735
+ candidateSet.add(key);
736
+ }
737
+ }
738
+ }
739
+ if (candidateSet.size === 0) {
740
+ return undefined;
741
+ }
742
+ const candidates = Array.from(candidateSet);
743
+ if (parsed.time) {
744
+ const time = parsed.time;
745
+ const withTimestamp = [];
746
+ const withoutTimestamp = [];
747
+ for (const version of candidates) {
748
+ const t = time[version];
749
+ if (typeof t === "string") {
750
+ withTimestamp.push({ version, timestamp: t });
751
+ }
752
+ else {
753
+ withoutTimestamp.push(version);
754
+ }
755
+ }
756
+ withTimestamp.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
757
+ withoutTimestamp.sort((a, b) => semver.rcompare(a, b));
758
+ return [
759
+ ...withTimestamp.map((entry) => entry.version),
760
+ ...withoutTimestamp,
761
+ ].slice(0, RECENT_VERSIONS_CAP);
762
+ }
763
+ candidates.sort((a, b) => semver.rcompare(a, b));
764
+ return candidates.slice(0, RECENT_VERSIONS_CAP);
765
+ }
766
+ /**
767
+ * Consume the response body and discard it. Called on every non-200 path
768
+ * for two reasons that happen to be solved by the same operation:
769
+ *
770
+ * 1. Connection-pool hygiene. Node's undici-backed `fetch` keeps the
771
+ * underlying TCP connection pinned to a response until its body is
772
+ * consumed. Skipping the read leaves the connection unavailable to the
773
+ * next call until GC reclaims it, which over a long Clark session
774
+ * against the same registry host adds up.
775
+ * 2. Token-echo defense for 401/403 specifically. A hostile or
776
+ * misconfigured registry can include the submitted bearer token in the
777
+ * error body (Artifactory does this in some configs). The only way to
778
+ * guarantee those bytes never reach the tool's output, a log line, or
779
+ * a tracing span is to never read them.
780
+ *
781
+ * The catch is intentionally broad: if the stream errored or was already
782
+ * consumed, there is nothing useful to do, and surfacing that failure would
783
+ * shadow the real status code the caller branches on.
784
+ */
785
+ async function drainSilently(response) {
786
+ try {
787
+ await response.text();
788
+ }
789
+ catch {
790
+ // Body already consumed or stream errored — nothing more to do.
791
+ }
792
+ }
793
+ //# sourceMappingURL=npm-package-lookup.js.map