@superblocksteam/vite-plugin-file-sync 2.0.137 → 2.0.138-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 (229) hide show
  1. package/dist/ai-service/agent/middleware.d.ts.map +1 -1
  2. package/dist/ai-service/agent/middleware.js +2 -22
  3. package/dist/ai-service/agent/middleware.js.map +1 -1
  4. package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts +1 -0
  5. package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
  6. package/dist/ai-service/agent/tools/build-capture-screenshot.js +150 -7
  7. package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
  8. package/dist/ai-service/agent/tools/build-read-file.d.ts +10 -0
  9. package/dist/ai-service/agent/tools/build-read-file.d.ts.map +1 -1
  10. package/dist/ai-service/agent/tools/build-read-file.js +14 -1
  11. package/dist/ai-service/agent/tools/build-read-file.js.map +1 -1
  12. package/dist/ai-service/agent/tools/index.d.ts +1 -1
  13. package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
  14. package/dist/ai-service/agent/tools/index.js +1 -1
  15. package/dist/ai-service/agent/tools/index.js.map +1 -1
  16. package/dist/ai-service/agent/tools.d.ts +1 -1
  17. package/dist/ai-service/agent/tools.d.ts.map +1 -1
  18. package/dist/ai-service/agent/tools.js +15 -1
  19. package/dist/ai-service/agent/tools.js.map +1 -1
  20. package/dist/ai-service/agent/tools2/tools/grep.d.ts.map +1 -1
  21. package/dist/ai-service/agent/tools2/tools/grep.js +2 -1
  22. package/dist/ai-service/agent/tools2/tools/grep.js.map +1 -1
  23. package/dist/ai-service/agent/tools2/tools/remember-knowledge.d.ts.map +1 -1
  24. package/dist/ai-service/agent/tools2/tools/remember-knowledge.js +8 -0
  25. package/dist/ai-service/agent/tools2/tools/remember-knowledge.js.map +1 -1
  26. package/dist/ai-service/agent/tools2/tools/start-test-run.d.ts.map +1 -1
  27. package/dist/ai-service/agent/tools2/tools/start-test-run.js +14 -6
  28. package/dist/ai-service/agent/tools2/tools/start-test-run.js.map +1 -1
  29. package/dist/ai-service/agent/tools2/tools/web-fetch.d.ts +2 -0
  30. package/dist/ai-service/agent/tools2/tools/web-fetch.d.ts.map +1 -1
  31. package/dist/ai-service/agent/tools2/tools/web-fetch.js +10 -5
  32. package/dist/ai-service/agent/tools2/tools/web-fetch.js.map +1 -1
  33. package/dist/ai-service/agent/utils.d.ts.map +1 -1
  34. package/dist/ai-service/agent/utils.js +25 -7
  35. package/dist/ai-service/agent/utils.js.map +1 -1
  36. package/dist/ai-service/app-interface/linter.d.ts.map +1 -1
  37. package/dist/ai-service/app-interface/linter.js +14 -3
  38. package/dist/ai-service/app-interface/linter.js.map +1 -1
  39. package/dist/ai-service/app-interface/npm-package-lookup.d.ts +19 -14
  40. package/dist/ai-service/app-interface/npm-package-lookup.d.ts.map +1 -1
  41. package/dist/ai-service/app-interface/npm-package-lookup.js +56 -23
  42. package/dist/ai-service/app-interface/npm-package-lookup.js.map +1 -1
  43. package/dist/ai-service/app-interface/npm-registry.d.ts +96 -48
  44. package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -1
  45. package/dist/ai-service/app-interface/npm-registry.js +247 -220
  46. package/dist/ai-service/app-interface/npm-registry.js.map +1 -1
  47. package/dist/ai-service/app-interface/shell.d.ts +36 -0
  48. package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
  49. package/dist/ai-service/app-interface/shell.js +183 -8
  50. package/dist/ai-service/app-interface/shell.js.map +1 -1
  51. package/dist/ai-service/app-skills/helpers.d.ts.map +1 -1
  52. package/dist/ai-service/app-skills/helpers.js +48 -8
  53. package/dist/ai-service/app-skills/helpers.js.map +1 -1
  54. package/dist/ai-service/app-skills/manager.d.ts +5 -0
  55. package/dist/ai-service/app-skills/manager.d.ts.map +1 -1
  56. package/dist/ai-service/app-skills/manager.js +13 -4
  57. package/dist/ai-service/app-skills/manager.js.map +1 -1
  58. package/dist/ai-service/attachments/uploaded-content-part.d.ts.map +1 -1
  59. package/dist/ai-service/attachments/uploaded-content-part.js +12 -5
  60. package/dist/ai-service/attachments/uploaded-content-part.js.map +1 -1
  61. package/dist/ai-service/index.d.ts +1 -2
  62. package/dist/ai-service/index.d.ts.map +1 -1
  63. package/dist/ai-service/index.js +16 -175
  64. package/dist/ai-service/index.js.map +1 -1
  65. package/dist/ai-service/judge/judge-eval-service-runner.js +0 -4
  66. package/dist/ai-service/judge/judge-eval-service-runner.js.map +1 -1
  67. package/dist/ai-service/judge/judge-service.js +1 -1
  68. package/dist/ai-service/judge/judge-service.js.map +1 -1
  69. package/dist/ai-service/knowledge/knowledge-metrics-emitters.d.ts +20 -0
  70. package/dist/ai-service/knowledge/knowledge-metrics-emitters.d.ts.map +1 -0
  71. package/dist/ai-service/knowledge/knowledge-metrics-emitters.js +77 -0
  72. package/dist/ai-service/knowledge/knowledge-metrics-emitters.js.map +1 -0
  73. package/dist/ai-service/knowledge/knowledge-metrics.d.ts +43 -0
  74. package/dist/ai-service/knowledge/knowledge-metrics.d.ts.map +1 -0
  75. package/dist/ai-service/knowledge/knowledge-metrics.js +217 -0
  76. package/dist/ai-service/knowledge/knowledge-metrics.js.map +1 -0
  77. package/dist/ai-service/llm/client.d.ts +144 -18
  78. package/dist/ai-service/llm/client.d.ts.map +1 -1
  79. package/dist/ai-service/llm/client.js +303 -48
  80. package/dist/ai-service/llm/client.js.map +1 -1
  81. package/dist/ai-service/llm/context-v2/context-metrics.d.ts +1 -0
  82. package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -1
  83. package/dist/ai-service/llm/context-v2/context-metrics.js +14 -1
  84. package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -1
  85. package/dist/ai-service/llm/context-v2/context.d.ts +3 -0
  86. package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
  87. package/dist/ai-service/llm/context-v2/context.js +34 -1
  88. package/dist/ai-service/llm/context-v2/context.js.map +1 -1
  89. package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
  90. package/dist/ai-service/llm/context-v2/manager.js +4 -1
  91. package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
  92. package/dist/ai-service/llm/interaction/adapters/vercel.d.ts +1 -1
  93. package/dist/ai-service/llm/interaction/adapters/vercel.d.ts.map +1 -1
  94. package/dist/ai-service/llm/interaction/adapters/vercel.js +19 -4
  95. package/dist/ai-service/llm/interaction/adapters/vercel.js.map +1 -1
  96. package/dist/ai-service/llm/interaction/provider.d.ts +17 -3
  97. package/dist/ai-service/llm/interaction/provider.d.ts.map +1 -1
  98. package/dist/ai-service/llm/provider.d.ts.map +1 -1
  99. package/dist/ai-service/llm/provider.js +19 -15
  100. package/dist/ai-service/llm/provider.js.map +1 -1
  101. package/dist/ai-service/llm/stream/managed-stream.d.ts.map +1 -1
  102. package/dist/ai-service/llm/stream/managed-stream.js +25 -2
  103. package/dist/ai-service/llm/stream/managed-stream.js.map +1 -1
  104. package/dist/ai-service/llm/stream/observers/llmobs.d.ts +9 -0
  105. package/dist/ai-service/llm/stream/observers/llmobs.d.ts.map +1 -1
  106. package/dist/ai-service/llm/stream/observers/llmobs.js +22 -5
  107. package/dist/ai-service/llm/stream/observers/llmobs.js.map +1 -1
  108. package/dist/ai-service/llm/stream/observers/logging.d.ts.map +1 -1
  109. package/dist/ai-service/llm/stream/observers/logging.js +17 -12
  110. package/dist/ai-service/llm/stream/observers/logging.js.map +1 -1
  111. package/dist/ai-service/llm/stream/orchestrator.d.ts +1 -0
  112. package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
  113. package/dist/ai-service/llm/stream/orchestrator.js +131 -0
  114. package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
  115. package/dist/ai-service/llm/types.d.ts +17 -6
  116. package/dist/ai-service/llm/types.d.ts.map +1 -1
  117. package/dist/ai-service/prompt-builder-service/classifiers/prompt-interpret-task.d.ts +3 -1
  118. package/dist/ai-service/prompt-builder-service/classifiers/prompt-interpret-task.d.ts.map +1 -1
  119. package/dist/ai-service/prompt-builder-service/classifiers/prompt-interpret-task.js +13 -5
  120. package/dist/ai-service/prompt-builder-service/classifiers/prompt-interpret-task.js.map +1 -1
  121. package/dist/ai-service/security/safety-classifier.d.ts +14 -1
  122. package/dist/ai-service/security/safety-classifier.d.ts.map +1 -1
  123. package/dist/ai-service/security/safety-classifier.js +26 -12
  124. package/dist/ai-service/security/safety-classifier.js.map +1 -1
  125. package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.d.ts +1 -1
  126. package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.d.ts.map +1 -1
  127. package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.js +35 -18
  128. package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.js.map +1 -1
  129. package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts +1 -1
  130. package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts.map +1 -1
  131. package/dist/ai-service/skills/system/third-party-migration/skill.generated.js +7 -5
  132. package/dist/ai-service/skills/system/third-party-migration/skill.generated.js.map +1 -1
  133. package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
  134. package/dist/ai-service/state-machine/clark-fsm.js +3 -2
  135. package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
  136. package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
  137. package/dist/ai-service/state-machine/handlers/agent-planning.js +14 -7
  138. package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
  139. package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts +1 -1
  140. package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts.map +1 -1
  141. package/dist/ai-service/state-machine/handlers/awaiting-user.js +40 -27
  142. package/dist/ai-service/state-machine/handlers/awaiting-user.js.map +1 -1
  143. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts +14 -0
  144. package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
  145. package/dist/ai-service/state-machine/handlers/llm-generating.js +72 -16
  146. package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
  147. package/dist/ai-service/state-machine/handlers/next-steps.d.ts +8 -1
  148. package/dist/ai-service/state-machine/handlers/next-steps.d.ts.map +1 -1
  149. package/dist/ai-service/state-machine/handlers/next-steps.js +13 -11
  150. package/dist/ai-service/state-machine/handlers/next-steps.js.map +1 -1
  151. package/dist/ai-service/state-machine/helpers/peer.d.ts.map +1 -1
  152. package/dist/ai-service/state-machine/helpers/peer.js +3 -2
  153. package/dist/ai-service/state-machine/helpers/peer.js.map +1 -1
  154. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts +5 -0
  155. package/dist/ai-service/state-machine/helpers/stable-peer.d.ts.map +1 -1
  156. package/dist/ai-service/state-machine/helpers/stable-peer.js +37 -0
  157. package/dist/ai-service/state-machine/helpers/stable-peer.js.map +1 -1
  158. package/dist/ai-service/template-renderer.d.ts.map +1 -1
  159. package/dist/ai-service/template-renderer.js +2 -2
  160. package/dist/ai-service/template-renderer.js.map +1 -1
  161. package/dist/ai-service/types.d.ts +2 -2
  162. package/dist/ai-service/types.d.ts.map +1 -1
  163. package/dist/dev-server-ws-smoke-client.d.ts +41 -0
  164. package/dist/dev-server-ws-smoke-client.d.ts.map +1 -0
  165. package/dist/dev-server-ws-smoke-client.js +314 -0
  166. package/dist/dev-server-ws-smoke-client.js.map +1 -0
  167. package/dist/early-error-relay.d.ts +31 -0
  168. package/dist/early-error-relay.d.ts.map +1 -0
  169. package/dist/early-error-relay.js +88 -0
  170. package/dist/early-error-relay.js.map +1 -0
  171. package/dist/file-sync-vite-plugin.d.ts +26 -1
  172. package/dist/file-sync-vite-plugin.d.ts.map +1 -1
  173. package/dist/file-sync-vite-plugin.js +82 -43
  174. package/dist/file-sync-vite-plugin.js.map +1 -1
  175. package/dist/file-system-helpers.d.ts +13 -0
  176. package/dist/file-system-helpers.d.ts.map +1 -1
  177. package/dist/file-system-helpers.js +18 -1
  178. package/dist/file-system-helpers.js.map +1 -1
  179. package/dist/file-system-manager.d.ts +0 -1
  180. package/dist/file-system-manager.d.ts.map +1 -1
  181. package/dist/file-system-manager.js +0 -53
  182. package/dist/file-system-manager.js.map +1 -1
  183. package/dist/injected-index.d.ts +6 -0
  184. package/dist/injected-index.d.ts.map +1 -1
  185. package/dist/injected-index.js +59 -14
  186. package/dist/injected-index.js.map +1 -1
  187. package/dist/migration/migration-routes.d.ts +3 -0
  188. package/dist/migration/migration-routes.d.ts.map +1 -1
  189. package/dist/migration/migration-routes.js +93 -108
  190. package/dist/migration/migration-routes.js.map +1 -1
  191. package/dist/migration/restructure.d.ts +15 -0
  192. package/dist/migration/restructure.d.ts.map +1 -1
  193. package/dist/migration/restructure.js +50 -1
  194. package/dist/migration/restructure.js.map +1 -1
  195. package/dist/migration-templates/app-fullstack/eslint.config.js +6 -1
  196. package/dist/migration-templates/app-fullstack/package.json +2 -1
  197. package/dist/plugin-options.d.ts +7 -0
  198. package/dist/plugin-options.d.ts.map +1 -1
  199. package/dist/plugin-options.js.map +1 -1
  200. package/dist/socket-manager.d.ts.map +1 -1
  201. package/dist/socket-manager.js +0 -5
  202. package/dist/socket-manager.js.map +1 -1
  203. package/dist/sync-service/list-dir.d.ts +1 -1
  204. package/dist/sync-service/list-dir.js +3 -3
  205. package/dist/sync-service/list-dir.js.map +1 -1
  206. package/dist/util/log-sanitizer.d.ts.map +1 -1
  207. package/dist/util/log-sanitizer.js +13 -6
  208. package/dist/util/log-sanitizer.js.map +1 -1
  209. package/dist/util/summarize-for-logging.d.ts +14 -0
  210. package/dist/util/summarize-for-logging.d.ts.map +1 -0
  211. package/dist/util/summarize-for-logging.js +94 -0
  212. package/dist/util/summarize-for-logging.js.map +1 -0
  213. package/package.json +8 -8
  214. package/dist/ai-service/agent/tools2/tools/explain-code-finalize.d.ts +0 -4
  215. package/dist/ai-service/agent/tools2/tools/explain-code-finalize.d.ts.map +0 -1
  216. package/dist/ai-service/agent/tools2/tools/explain-code-finalize.js +0 -19
  217. package/dist/ai-service/agent/tools2/tools/explain-code-finalize.js.map +0 -1
  218. package/dist/ai-service/llm/impl/anthropic.d.ts +0 -3
  219. package/dist/ai-service/llm/impl/anthropic.d.ts.map +0 -1
  220. package/dist/ai-service/llm/impl/anthropic.js +0 -15
  221. package/dist/ai-service/llm/impl/anthropic.js.map +0 -1
  222. package/dist/ai-service/prompts/explain-code.d.ts +0 -7
  223. package/dist/ai-service/prompts/explain-code.d.ts.map +0 -1
  224. package/dist/ai-service/prompts/explain-code.js +0 -23
  225. package/dist/ai-service/prompts/explain-code.js.map +0 -1
  226. package/dist/ai-service/util/json-stream-parser.d.ts +0 -20
  227. package/dist/ai-service/util/json-stream-parser.d.ts.map +0 -1
  228. package/dist/ai-service/util/json-stream-parser.js +0 -139
  229. package/dist/ai-service/util/json-stream-parser.js.map +0 -1
@@ -26,6 +26,23 @@ const noControlChars = (s) => !/[\x00-\x1f]/.test(s);
26
26
  // Mirrors the server's npm scope grammar: must start with `@`, then a
27
27
  // lowercase letter/digit, then up to 254 lowercase letters/digits/`._-`.
28
28
  const NPM_SCOPE_RE = /^@[a-z0-9][a-z0-9._-]{0,254}$/;
29
+ /**
30
+ * Test-only escape hatch that mirrors the server's `httpRegistryUrlAllowed()`
31
+ * (see `packages/server/src/routes/api/v1/organization/npmRegistry.ts`). The
32
+ * private-registry Playwright tests point the dev server at a local registry
33
+ * that only serves http. The server POST controller and the admin UI form
34
+ * already relax their https check behind this same env var, and the CLI's
35
+ * response schema has to match. Otherwise the fetched config fails to parse,
36
+ * `getConfig()` returns `unreachable`, the `~/.superblocks/npmrc` file is
37
+ * never written, and the first install quietly falls back to public npm. It
38
+ * is read on every `getConfig()` call so it follows the dev server's env, and
39
+ * it is unset in production, where https stays required. The dev server
40
+ * subprocess inherits it because the CLI's process spawner merges
41
+ * `process.env` (see `SuperblocksCli.customizedEnv` in cli-system-tests).
42
+ */
43
+ function httpRegistryUrlAllowedForE2E() {
44
+ return process.env.SUPERBLOCKS_E2E_ALLOW_HTTP_NPM_REGISTRY === "true";
45
+ }
29
46
  const registryEntrySchema = z.object({
30
47
  id: z.string().uuid(),
31
48
  scope: z
@@ -38,24 +55,14 @@ const registryEntrySchema = z.object({
38
55
  .url()
39
56
  .refine(noControlChars, "registryUrl contains a C0 control character")
40
57
  .refine((s) => s.startsWith("https://") ||
41
- (process.env.SUPERBLOCKS_E2E_ALLOW_HTTP_NPM_REGISTRY === "true" &&
42
- s.startsWith("http://")), "registryUrl must be https"),
43
- // Matches the server's `OrgNpmRegistryDto.token` (see
44
- // `packages/server/src/controllers/v1/orgNpmRegistry/handlers.ts`),
45
- // which is documented as `string | null` but in practice may also be
46
- // omitted from the wire payload entirely on older server builds.
47
- // `.nullish()` accepts `null` AND missing/undefined; the transform
48
- // coalesces both to `null` so downstream code keeps the canonical
49
- // `NpmRegistryEntry.token: string | null` shape and never sees
50
- // `undefined`. An absent token is a legitimate "unauthenticated
51
- // registry" state, NOT wire-shape drift — treating it as drift would
52
- // route through `serveStaleOrUnconfigured` and brick the install
53
- // path on orgs whose registry intentionally has no token.
54
- token: z
55
- .string()
56
- .refine(noControlChars, "token contains a C0 control character")
57
- .nullish()
58
- .transform((v) => v ?? null),
58
+ (s.startsWith("http://") && httpRegistryUrlAllowedForE2E()), "registryUrl must be https"),
59
+ // No token on the wire. `registryUrl` is the Superblocks npm proxy URL the
60
+ // server rewrites each row to (see the server's `/npm-registry/config`
61
+ // route); the CLI authenticates to the proxy with the machine's own login
62
+ // token, which it stamps in locally (see `registriesToConfig`). The
63
+ // customer's upstream registry password is held only on the server. Any
64
+ // `token` field an older server still emits is ignored: this schema strips
65
+ // unknown keys rather than rejecting them.
59
66
  });
60
67
  const registryListSchema = z.object({
61
68
  registries: z.array(registryEntrySchema),
@@ -81,16 +88,6 @@ const envelopeSchema = z
81
88
  data: z.unknown(),
82
89
  })
83
90
  .passthrough();
84
- /**
85
- * Scopes whose existing `<scope>:registry=` lines in a project / pod `.npmrc`
86
- * must survive `writeNpmrc` calls. Shared between AppShell, TemplateRenderer,
87
- * and any other call site so a new scope only needs to be added in one place.
88
- *
89
- * `setup-template.sh` bakes
90
- * `@superblocksteam:registry=https://npm.pkg.github.com/` into the template
91
- * `.npmrc` for ephemeral environments; we must not clobber it.
92
- */
93
- export const PRESERVE_NPMRC_SCOPES = ["@superblocksteam"];
94
91
  /**
95
92
  * Resolves the org `allow_install_scripts` policy from a `prepareForPrivateRegistry`
96
93
  * (or `maybeWriteNpmrcForDir`) result. `false` means "the org has explicitly
@@ -106,11 +103,12 @@ export function shouldIgnoreInstallScripts(result) {
106
103
  return result?.config.allowInstallScripts === false;
107
104
  }
108
105
  /**
109
- * Map a fresh `getConfig` fetch result to the `config.lookup_total` outcome
110
- * (APPS-4378): a populated `configured` result is a cache `miss`; an empty
111
- * `not-configured` is `not_configured`; `stale` / `unreachable` (served during
112
- * an outage, or no usable cache) are `error`. Hard-fail throws are recorded as
113
- * `error` at the call site, and in-TTL cache hits as `hit`.
106
+ * Map a fresh `getConfig` fetch result to the `config.lookup_total` outcome: a
107
+ * populated `configured` result is a cache `miss`; an empty `not-configured`
108
+ * is `not_configured`; `stale` (still serving the cached registry during an
109
+ * outage) is its own `stale` result; `unreachable` (no usable cache during an
110
+ * outage) is `error`. Throws that hard-fail are recorded as `error` at the
111
+ * call site, and cache hits within the TTL as `hit`.
114
112
  */
115
113
  function configLookupFromFetch(result) {
116
114
  switch (result.source) {
@@ -119,6 +117,7 @@ function configLookupFromFetch(result) {
119
117
  case "not-configured":
120
118
  return "not_configured";
121
119
  case "stale":
120
+ return "stale";
122
121
  case "unreachable":
123
122
  return "error";
124
123
  default: {
@@ -137,6 +136,14 @@ function configLookupFromFetch(result) {
137
136
  * `NpmRegistryConfig`. Centralised so `writeNpmrc`, tests, and any future
138
137
  * consumer all interpret the wire shape the same way.
139
138
  *
139
+ * `authToken` is the machine's own Superblocks login token. The server's
140
+ * `/config` route points every `registryUrl` at the Superblocks npm proxy and
141
+ * returns no upstream password, so the same login token authenticates every
142
+ * proxied registry. It is stamped onto each entry here (rather than threaded
143
+ * through `writeNpmrc` / the package-lookup path) so those consumers keep
144
+ * reading `config.*.token` unchanged. Omitted only in tests that render URLs
145
+ * without auth.
146
+ *
140
147
  * Multiple `scope === null` entries should never happen — the server's
141
148
  * UNIQUE (organization_id, scope) constraint forbids it — but we are
142
149
  * defensive: the FIRST default wins, subsequent ones are logged and
@@ -146,7 +153,62 @@ function configLookupFromFetch(result) {
146
153
  * Duplicate scoped entries (same `scope` string) follow the same rule:
147
154
  * first wins.
148
155
  */
149
- export function registriesToConfig(entries, allowInstallScripts) {
156
+ /**
157
+ * Path the server mounts the npm proxy under for an org. Mirrors the server's
158
+ * `npmProxyUrlPrefix`
159
+ * (`packages/server/src/controllers/v1/orgNpmRegistry/proxy.ts`): every
160
+ * `registryUrl` the `/config` route returns lives at
161
+ * `<external server URL>/api/v1/organizations/<orgId>/npm-proxy` (default) or
162
+ * a `…/npm-proxy/_scope/<scope>` sub-path. A server-side path change is a
163
+ * coordinated server+client change; until then this is the one client-side
164
+ * source of truth for "is this URL our proxy?".
165
+ */
166
+ function superblocksNpmProxyPathPrefix(organizationId) {
167
+ return `/api/v1/organizations/${organizationId}/npm-proxy`;
168
+ }
169
+ /**
170
+ * True only when `registryUrl` is genuinely the Superblocks npm proxy for THIS
171
+ * org on THIS server (matching origin AND the org-scoped `/npm-proxy` path).
172
+ *
173
+ * The client stamps the machine's own Superblocks login token onto every row
174
+ * `/config` returns. That is only safe if every row really is our proxy: a
175
+ * version-skewed or misconfigured server could return a customer's upstream
176
+ * registry URL (or a broken one built from an empty external-server URL), and
177
+ * blindly stamping the login token onto that would leak a Superblocks
178
+ * credential to a third-party host. So the client proves the proxy invariant
179
+ * before stamping and fails closed when it does not hold.
180
+ *
181
+ * Compares parsed origin + pathname (not a raw string prefix) so trailing
182
+ * slashes and default ports can't cause a false negative, and requires a path
183
+ * boundary so a lookalike like `…/npm-proxyEVIL` can't slip through. Also
184
+ * rejects anything the canonical proxy URL never carries — embedded
185
+ * userinfo (`https://user:pass@host/…` shares the host's `origin` but is not
186
+ * our proxy URL) and any query/fragment — so a crafted URL can't satisfy the
187
+ * origin check while smuggling something else into `.npmrc`.
188
+ */
189
+ export function isSuperblocksNpmProxyUrl(registryUrl, baseUrl, organizationId) {
190
+ let registry;
191
+ let base;
192
+ try {
193
+ registry = new URL(registryUrl);
194
+ base = new URL(baseUrl);
195
+ }
196
+ catch {
197
+ return false;
198
+ }
199
+ if (registry.origin !== base.origin) {
200
+ return false;
201
+ }
202
+ if (registry.username || registry.password) {
203
+ return false;
204
+ }
205
+ if (registry.search || registry.hash) {
206
+ return false;
207
+ }
208
+ const prefix = superblocksNpmProxyPathPrefix(organizationId);
209
+ return (registry.pathname === prefix || registry.pathname.startsWith(`${prefix}/`));
210
+ }
211
+ export function registriesToConfig(entries, allowInstallScripts, authToken) {
150
212
  if (entries.length === 0) {
151
213
  return {
152
214
  configured: false,
@@ -158,7 +220,7 @@ export function registriesToConfig(entries, allowInstallScripts) {
158
220
  for (const entry of entries) {
159
221
  const value = {
160
222
  url: entry.registryUrl,
161
- ...(entry.token ? { token: entry.token } : {}),
223
+ ...(authToken ? { token: authToken } : {}),
162
224
  };
163
225
  if (entry.scope === null) {
164
226
  if (defaultReg) {
@@ -430,21 +492,41 @@ export class NpmRegistryClient {
430
492
  /**
431
493
  * Resolve the current npm registry configuration for the active org. See
432
494
  * the class docstring for the full state machine.
495
+ *
496
+ * `forceRefresh` bypasses the TTL cache-hit return below so a just-in-time
497
+ * caller (the `.npmrc` writer before an install, a package lookup, the
498
+ * home-npmrc sync) observes an admin clearing or changing the registry
499
+ * immediately instead of waiting out the TTL. `forceRefreshMaxAgeMs` lets a
500
+ * high-frequency caller (the package lookup) keep a small freshness floor so
501
+ * a tight sequential batch collapses onto one fetch; left at 0 (install /
502
+ * home sync) a forced read always re-fetches. It does NOT touch the cache
503
+ * before fetching: `serveStaleOrUnconfigured` still needs the last-known-good
504
+ * entries to fall back to during a 5xx/network outage. The flag-off
505
+ * short-circuit, inflight dedup, and all hard-fail branches behave
506
+ * identically.
433
507
  */
434
- async getConfig() {
508
+ async getConfig(options = {}) {
435
509
  // Captured up front so every config.lookup emit carries the time getConfig
436
510
  // spent resolving (~0 for a cache hit, the network round-trip for a miss).
437
511
  const start = this.deps.now();
512
+ const forceRefresh = options.forceRefresh === true;
438
513
  if (!this.deps.isFlagEnabled()) {
439
514
  // Short-circuit BEFORE inspecting the cache: a flag flip to "off"
440
515
  // must not keep serving from a previously-fetched config. The
441
516
  // ticket explicitly calls this out ("Flag off → return
442
- // {configured: false} immediately, no server call").
443
- npmRegistryEmitter.recordConfigLookup("not_configured", this.deps.now() - start);
517
+ // {configured: false} immediately, no server call"). `forceRefresh`
518
+ // does not change this: flag off means no config regardless.
519
+ npmRegistryEmitter.recordConfigLookup("not_configured", this.deps.now() - start, forceRefresh);
444
520
  return NOT_CONFIGURED;
445
521
  }
446
522
  const now = this.deps.now();
447
- if (this.cache && now - this.cache.fetchedAt < this.deps.ttlMs) {
523
+ // A forced read may still be served from the cache if it is younger than
524
+ // the caller's freshness floor (default 0 = always re-fetch). An unforced
525
+ // read uses the full TTL.
526
+ const maxAgeMs = forceRefresh
527
+ ? (options.forceRefreshMaxAgeMs ?? 0)
528
+ : this.deps.ttlMs;
529
+ if (this.cache && now - this.cache.fetchedAt < maxAgeMs) {
448
530
  // A cached empty list (from a prior 404 or `registries: []` response)
449
531
  // must surface as `not-configured` on every hit within the TTL, not
450
532
  // `configured` with `config.configured: false`. The discriminator's
@@ -454,15 +536,12 @@ export class NpmRegistryClient {
454
536
  // alongside the not-configured result so AppShell can honor
455
537
  // `--ignore-scripts` even when there are no registry rows.
456
538
  if (this.cache.entries.length === 0) {
457
- npmRegistryEmitter.recordConfigLookup("not_configured", this.deps.now() - start);
539
+ npmRegistryEmitter.recordConfigLookup("not_configured", this.deps.now() - start, forceRefresh);
458
540
  return notConfigured(this.cache.npmAllowInstallScripts);
459
541
  }
460
542
  await this.markPolicyConfigured();
461
- npmRegistryEmitter.recordConfigLookup("hit", this.deps.now() - start);
462
- return {
463
- source: "configured",
464
- config: registriesToConfig(this.cache.entries, this.cache.npmAllowInstallScripts),
465
- };
543
+ npmRegistryEmitter.recordConfigLookup("hit", this.deps.now() - start, forceRefresh);
544
+ return this.renderConfigured(this.cache.entries, this.cache.npmAllowInstallScripts);
466
545
  }
467
546
  // Deduplicate concurrent cache-miss callers onto a single network
468
547
  // request so we don't multiply 401-refresh storms or lose last-known-
@@ -475,13 +554,13 @@ export class NpmRegistryClient {
475
554
  // Cache miss → fresh fetch. configured ⇒ miss; empty/404 ⇒
476
555
  // not_configured; stale/unreachable ⇒ error. Deduped concurrent
477
556
  // callers share this single emit, so the counter tracks fetch rate.
478
- npmRegistryEmitter.recordConfigLookup(configLookupFromFetch(result), this.deps.now() - start);
557
+ npmRegistryEmitter.recordConfigLookup(configLookupFromFetch(result), this.deps.now() - start, forceRefresh);
479
558
  return result;
480
559
  })
481
560
  .catch((err) => {
482
561
  // Hard-fail throws (double-401, refresh reject, other-4xx, schema
483
562
  // parse) surface as `error`.
484
- npmRegistryEmitter.recordConfigLookup("error", this.deps.now() - start);
563
+ npmRegistryEmitter.recordConfigLookup("error", this.deps.now() - start, forceRefresh);
485
564
  throw err;
486
565
  })
487
566
  .finally(() => {
@@ -584,6 +663,7 @@ export class NpmRegistryClient {
584
663
  // survives independently of the registry credentials it rode in
585
664
  // with, and reading from cache alone would clobber it back to
586
665
  // `undefined` here.
666
+ this.signalPolicyFallback(this.cache?.npmAllowInstallScripts, "not_configured_404");
587
667
  const preservedPolicy = this.cache?.npmAllowInstallScripts ?? this.lastKnownPolicy;
588
668
  this.cache = {
589
669
  entries: [],
@@ -666,6 +746,25 @@ export class NpmRegistryClient {
666
746
  }
667
747
  const entries = parsed.data.registries;
668
748
  const npmAllowInstallScripts = parsed.data.npmAllowInstallScripts;
749
+ // Prove every row is genuinely our proxy before trusting the payload.
750
+ // `renderConfig` stamps the machine's Superblocks login token onto each
751
+ // row's URL; if a version-skewed or misconfigured server returned a
752
+ // customer's upstream registry URL (or a broken one) we'd leak that login
753
+ // token to a third-party host. A row that fails the invariant means the
754
+ // payload is untrustworthy, so we treat it like a schema failure: don't
755
+ // cache it, and fail closed through `serveStaleOrUnconfigured` (serves
756
+ // last-known-good if we have it, else `unreachable`).
757
+ const nonProxyEntry = entries.find((entry) => !isSuperblocksNpmProxyUrl(entry.registryUrl, this.deps.baseUrl, this.deps.organizationId));
758
+ if (nonProxyEntry) {
759
+ const invariantError = new Error(`[npm-registry] server returned a registryUrl that is not the Superblocks npm proxy for this org (org ${this.deps.organizationId}); refusing to stamp the login token onto it`);
760
+ getLogger().error(invariantError.message, {
761
+ error: {
762
+ kind: "NpmRegistryProxyInvariantViolated",
763
+ message: invariantError.message,
764
+ },
765
+ });
766
+ return this.serveStaleOrUnconfigured(invariantError);
767
+ }
669
768
  this.cache = {
670
769
  entries,
671
770
  npmAllowInstallScripts,
@@ -676,10 +775,7 @@ export class NpmRegistryClient {
676
775
  return notConfigured(npmAllowInstallScripts);
677
776
  }
678
777
  await this.markPolicyConfigured();
679
- return {
680
- source: "configured",
681
- config: registriesToConfig(entries, npmAllowInstallScripts),
682
- };
778
+ return this.renderConfigured(entries, npmAllowInstallScripts);
683
779
  }
684
780
  /**
685
781
  * One full fetch attempt with the current JWT. Throws on getJwt failure
@@ -688,7 +784,12 @@ export class NpmRegistryClient {
688
784
  async attempt() {
689
785
  const jwt = await this.deps.getJwt();
690
786
  const base = this.deps.baseUrl.replace(/\/$/, "");
691
- const url = `${base}/api/v1/organizations/${encodeURIComponent(this.deps.organizationId)}/npm-registry`;
787
+ // `/config` returns each registry row with its `registryUrl` rewritten to
788
+ // the Superblocks npm proxy. No upstream tokens cross the wire anymore;
789
+ // the caller stamps the machine's own login token onto every row to auth
790
+ // against the proxy. Restricted to superblocks_jwt + access_token callers;
791
+ // the plain list route is redacted-only and serves the dashboard.
792
+ const url = `${base}/api/v1/organizations/${encodeURIComponent(this.deps.organizationId)}/npm-registry/config`;
692
793
  return await this.deps.fetch(url, {
693
794
  method: "GET",
694
795
  headers: {
@@ -697,22 +798,89 @@ export class NpmRegistryClient {
697
798
  },
698
799
  });
699
800
  }
801
+ /**
802
+ * Signal when `lastKnownPolicy` supplied the install-scripts policy that the
803
+ * in-memory cache could not (`cachePolicy == null && lastKnownPolicy !=
804
+ * null`). That combination means an earlier auth-failure or 4xx branch wiped
805
+ * the cache and we fell back to the surviving policy, which is a sign
806
+ * something went wrong. Emit a WARN (with `orgId` and which branch ran) plus
807
+ * a low-cardinality counter so it can be alerted on from a dashboard without
808
+ * digging through logs or traces. `cachePolicy` is passed in (not re-read)
809
+ * because the 404 branch overwrites `this.cache` before this runs.
810
+ */
811
+ signalPolicyFallback(cachePolicy, branch) {
812
+ if (cachePolicy == null && this.lastKnownPolicy != null) {
813
+ getLogger().warn("[npm-registry] fell back to the last known install-scripts policy because the cache had been cleared by an earlier auth or 4xx failure", { orgId: this.deps.organizationId, branch });
814
+ npmRegistryEmitter.recordPolicyFallback(branch);
815
+ }
816
+ }
817
+ /**
818
+ * Render cached/fetched entries into a config, stamping the machine's current
819
+ * Superblocks login token onto every proxied registry (`registryUrl` is the
820
+ * proxy URL the server rewrote each row to). Read fresh each call so the
821
+ * `.npmrc` the caller writes right after carries a current token. `getJwt()`
822
+ * resolves immediately when a token is already primed, so this stays cheap on
823
+ * the cache-hit fast path; it throws only when no token is available, in
824
+ * which case there is no way to build a working proxy `.npmrc`.
825
+ */
826
+ async renderConfig(entries, allowInstallScripts) {
827
+ const authToken = await this.deps.getJwt();
828
+ return registriesToConfig(entries, allowInstallScripts, authToken);
829
+ }
830
+ /**
831
+ * Build a `configured` result, but route a JWT-read failure through the same
832
+ * fail-closed path as `serveStaleOrUnconfigured`. `renderConfig` reads the
833
+ * login token via `getJwt()`, which can reject (no token primed). A missing
834
+ * token is the same "we can't build a working proxy `.npmrc`" condition the
835
+ * stale path already treats as `unreachable`, so both the cache-hit and the
836
+ * fresh-fetch success paths must resolve it the same way rather than throwing
837
+ * out of `getConfig()`. With a warm cache this serves last-known-good; with
838
+ * no usable cache it reports `unreachable`.
839
+ */
840
+ async renderConfigured(entries, allowInstallScripts) {
841
+ let config;
842
+ try {
843
+ config = await this.renderConfig(entries, allowInstallScripts);
844
+ }
845
+ catch (jwtError) {
846
+ return this.serveStaleOrUnconfigured(jwtError);
847
+ }
848
+ return { source: "configured", config };
849
+ }
700
850
  async serveStaleOrUnconfigured(error) {
701
851
  if (this.cache && this.cache.entries.length > 0) {
702
852
  const ageMs = this.deps.now() - this.cache.fetchedAt;
703
- getLogger().warn("[npm-registry] server unreachable; serving last-known-good config", {
704
- ageMs,
705
- error: error instanceof Error ? error.message : String(error),
706
- });
707
- // APPS-4370: stale serves a known-configured org, so refresh the
708
- // disk marker too. A long outage that spans a pod recycle would
709
- // otherwise lose the signal: the next cold-boot would see `unreachable`
710
- // with no marker and proceed instead of failing closed.
711
- await this.markPolicyConfigured();
712
- return {
713
- source: "stale",
714
- config: registriesToConfig(this.cache.entries, this.cache.npmAllowInstallScripts),
715
- };
853
+ // Render before logging "serving last-known-good": the proxy config
854
+ // needs the machine's login token stamped onto every row, and that
855
+ // read can fail (no JWT). A tokenless proxy `.npmrc` is useless —
856
+ // every install would 401 against the proxy — so on that failure we
857
+ // fail closed by falling through to the unreachable path below rather
858
+ // than handing back a config that can't authenticate.
859
+ let config;
860
+ try {
861
+ config = await this.renderConfig(this.cache.entries, this.cache.npmAllowInstallScripts);
862
+ }
863
+ catch (jwtError) {
864
+ getLogger().warn("[npm-registry] server unreachable and no login token available to render last-known-good config; treating as unreachable", {
865
+ ageMs,
866
+ error: jwtError instanceof Error ? jwtError.message : String(jwtError),
867
+ });
868
+ }
869
+ if (config) {
870
+ getLogger().warn("[npm-registry] server unreachable; serving last-known-good config", {
871
+ ageMs,
872
+ error: error instanceof Error ? error.message : String(error),
873
+ });
874
+ // APPS-4370: stale serves a known-configured org, so refresh the
875
+ // disk marker too. A long outage that spans a pod recycle would
876
+ // otherwise lose the signal: the next cold-boot would see `unreachable`
877
+ // with no marker and proceed instead of failing closed.
878
+ await this.markPolicyConfigured();
879
+ return {
880
+ source: "stale",
881
+ config,
882
+ };
883
+ }
716
884
  }
717
885
  // No usable cache: either nothing was ever fetched successfully (cold
718
886
  // start) or the only successful fetch returned an empty list. Both
@@ -730,6 +898,7 @@ export class NpmRegistryClient {
730
898
  // above — the policy survives independently of the registry
731
899
  // credentials it rode in with.
732
900
  getLogger().warn("[npm-registry] server unreachable and no usable cached config; treating as unreachable", { error: error instanceof Error ? error.message : String(error) });
901
+ this.signalPolicyFallback(this.cache?.npmAllowInstallScripts, "unreachable");
733
902
  const base = unreachable(this.cache?.npmAllowInstallScripts ?? this.lastKnownPolicy);
734
903
  // APPS-4370: consult the disk-backed marker so AppShell can decide
735
904
  // whether to fail closed. Only attach the field when a store is
@@ -798,14 +967,6 @@ function normalizeRegistryUrl(registryUrl) {
798
967
  * does not validate scope names.
799
968
  * - https-only registry URLs: npm itself accepts http.
800
969
  *
801
- * When `preserveScopeLines` is provided, any existing `<scope>:registry=`
802
- * keys for those scopes are preserved (so EE-baked
803
- * `@superblocksteam:registry=https://npm.pkg.github.com/` survives) UNLESS
804
- * the same scope is explicitly overridden in `config.scopes`. The
805
- * preserved value is read via `ini.parse` from the existing file, so
806
- * `ini`'s own quoting rules apply (rather than our older line-grep that
807
- * could mis-handle quoted values).
808
- *
809
970
  * Idempotent: rewrites the file in full each call. Writes are atomic (tmp +
810
971
  * rename) and the file mode is restricted to 0600 because the file may carry
811
972
  * an auth token.
@@ -833,144 +994,6 @@ export async function writeNpmrc(targetPath, config, options = {}) {
833
994
  // layer (all keys are top-level), so `ini.stringify` on a flat object
834
995
  // produces the exact wire format npm expects.
835
996
  const obj = {};
836
- const preserveScopes = options.preserveScopeLines ?? [];
837
- if (preserveScopes.length > 0) {
838
- let existing;
839
- try {
840
- existing = await readFile(targetPath, "utf-8");
841
- }
842
- catch (error) {
843
- // ENOENT is the expected case on first write — every other code
844
- // (EACCES, EISDIR, EIO) we surface as a warning rather than throw
845
- // because preserve-scope is a best-effort fallback and we'd rather
846
- // emit a fresh `.npmrc` than fail the whole install path. The
847
- // tradeoff: a transient read error could silently lose a baked
848
- // `@superblocksteam:registry=` line (the exact failure class APPS-2053
849
- // was meant to prevent).
850
- const code = error?.code;
851
- if (code !== "ENOENT") {
852
- getLogger().warn("[npm-registry] failed to read existing .npmrc for scope preservation; proceeding without preserve", {
853
- path: targetPath,
854
- code,
855
- error: error instanceof Error ? error.message : String(error),
856
- });
857
- }
858
- }
859
- if (existing) {
860
- // `ini.parse` skips lines it cannot decode rather than throwing
861
- // (CRLF artifacts, partial writes, hand-edits with stray bytes —
862
- // see ini@6 `decode`'s regex-or-continue loop). Guard with try/catch
863
- // for future versions that may throw, and treat any unrecognised
864
- // result as "no preserved value" — but warn so an operator can repair
865
- // a malformed `.npmrc` instead of silently shipping without the
866
- // EE-baked `@superblocksteam:registry=` line.
867
- let parsedExisting = {};
868
- try {
869
- parsedExisting = ini.parse(existing);
870
- }
871
- catch (err) {
872
- getLogger().warn("[npm-registry] failed to parse existing .npmrc for scope preservation; proceeding without preserve", {
873
- path: targetPath,
874
- error: err instanceof Error ? err.message : String(err),
875
- });
876
- }
877
- for (const scope of preserveScopes) {
878
- // Skip preservation if config explicitly overrides this scope —
879
- // the override will be written below and we don't want to emit
880
- // two `<scope>:registry=` lines (or have the preserved value
881
- // shadow the explicit one). The override-wins symmetry also
882
- // applies to the auth-family lines for the old origin: dropping
883
- // the registry line while keeping a `//old-origin/:_authToken=`
884
- // around would leave a dangling secret pointed at no registry
885
- // entry in the rendered file.
886
- if (config.scopes && config.scopes[scope]) {
887
- continue;
888
- }
889
- const key = `${scope}:registry`;
890
- const value = parsedExisting[key];
891
- if (typeof value === "string") {
892
- obj[key] = value;
893
- // Also preserve every auth-family key keyed on the same
894
- // `//<origin>/` prefix as the preserved registry URL. Without
895
- // this, EE-baked `.npmrc` files that ship both
896
- // `@superblocksteam:registry=https://npm.pkg.github.com/` AND
897
- // `//npm.pkg.github.com/:_authToken=<ghpr-token>` lose the
898
- // token on the first `writeNpmrc` call, and every
899
- // `@superblocksteam/*` install 401s against GHPR (APPS-4300).
900
- //
901
- // npm/pnpm support multiple auth shapes against the same
902
- // origin: bearer (`_authToken`), Basic (`_password` +
903
- // `username` + `email`), and the legacy base64 (`_auth`).
904
- // We preserve all of them so a future EE image change that
905
- // swaps from bearer to Basic doesn't silently regress to the
906
- // same failure mode.
907
- //
908
- // `always-auth` is deliberately NOT preserved: npm removed it
909
- // as a recognized key in npm 7+, and npm 11 (the dev-server's
910
- // pinned version) emits `npm warn Unknown user config
911
- // "always-auth" … This will stop working in the next major
912
- // version of npm` on every invocation that reads it — auth
913
- // still attaches from the sibling `_authToken`, so carrying it
914
- // forward is a no-op that only generates stderr noise today and
915
- // risks a hard error once we move to npm 12. Dropping it from
916
- // the preserve set means a customer `.npmrc` that happens to
917
- // carry `//origin/:always-auth` simply loses a dead key (APPS-4430).
918
- //
919
- // Each `parsedExisting[copyKey]` returns the same `unknown`
920
- // shape as the registry line above; only string values are
921
- // safe to round-trip back through `ini.stringify`. Non-string
922
- // values follow the same drop+warn handling.
923
- const preservedAuthOrigin = toNpmAuthOrigin(value);
924
- if (preservedAuthOrigin) {
925
- const authFamilyKeys = [
926
- "_authToken",
927
- "_password",
928
- "username",
929
- "email",
930
- "_auth",
931
- ];
932
- for (const authKey of authFamilyKeys) {
933
- const copyKey = `${preservedAuthOrigin}:${authKey}`;
934
- const authValue = parsedExisting[copyKey];
935
- // `ini.parse` coerces a bare `true`/`false`/numeric value to
936
- // its JS primitive equivalent. The auth-family keys above are
937
- // all string-valued, but coerce back to a string defensively
938
- // so `ini.stringify` emits a valid line (it accepts strings,
939
- // booleans, and numbers, but our internal `obj` map is typed
940
- // `Record<string, string>` for the registry/auth lines
941
- // elsewhere in this writer, so the cast is contained).
942
- if (typeof authValue === "string" ||
943
- typeof authValue === "boolean" ||
944
- typeof authValue === "number") {
945
- obj[copyKey] = String(authValue);
946
- }
947
- else if (authValue !== undefined) {
948
- getLogger().warn("[npm-registry] preserve-scope auth-family key parsed to unsupported type; dropping", {
949
- path: targetPath,
950
- scope,
951
- authKey,
952
- valueType: typeof authValue,
953
- });
954
- }
955
- }
956
- }
957
- }
958
- else if (value !== undefined) {
959
- // Key parsed but value is not a string — e.g. a bare key (`true`),
960
- // a section header above it, or a quoted value whose `JSON.parse`
961
- // failed inside `ini`. We're about to silently drop the preserve
962
- // target, so surface so the operator can repair the file.
963
- getLogger().warn("[npm-registry] preserve-scope key parsed to non-string; dropping", {
964
- path: targetPath,
965
- scope,
966
- valueType: typeof value,
967
- });
968
- }
969
- // value === undefined is the normal case when the existing .npmrc
970
- // simply doesn't list this scope — silent on that.
971
- }
972
- }
973
- }
974
997
  if (config.default) {
975
998
  const normalizedDefaultUrl = normalizeRegistryUrl(config.default.url);
976
999
  obj["registry"] = normalizedDefaultUrl;
@@ -1862,11 +1885,17 @@ export async function ensureNpmrcGitignored(dir) {
1862
1885
  * Returns the resolved fetch result so callers can branch on `source` for
1863
1886
  * observability, or `undefined` when no client was provided.
1864
1887
  */
1865
- export async function maybeWriteNpmrcForDir(dir, options = {}, client) {
1888
+ export async function maybeWriteNpmrcForDir(dir, client) {
1866
1889
  if (!client) {
1867
1890
  return undefined;
1868
1891
  }
1869
- const result = await client.getConfig();
1892
+ // Force-refresh: this is the `.npmrc` synthesis / install write boundary,
1893
+ // so it must observe an admin clearing or changing the registry
1894
+ // immediately rather than waiting out the TTL. A cleared
1895
+ // registry resolves fresh as `not-configured`, which the branch below
1896
+ // tears down the project `.npmrc` for. The last-known-good fallback on a
1897
+ // server outage still applies — only the cache-hit read is bypassed.
1898
+ const result = await client.getConfig({ forceRefresh: true });
1870
1899
  if (result.source === "unreachable") {
1871
1900
  // Transient cold-cache outage. We do NOT know the org's current
1872
1901
  // registry state, so we refuse to act on this signal — neither
@@ -1887,15 +1916,13 @@ export async function maybeWriteNpmrcForDir(dir, options = {}, client) {
1887
1916
  // Even without registry rows the org policy disallows install
1888
1917
  // scripts. Bake `ignore-scripts=true` into the project `.npmrc`
1889
1918
  // so any npm/pnpm invocation (patch-package, husky, ad-hoc
1890
- // `bash npm install`) honours the policy. `writeNpmrc` with a
1891
- // policy-only config preserves existing scope lines (e.g. the
1892
- // EE-baked `@superblocksteam:registry=`) via `preserveScopeLines`.
1919
+ // `bash npm install`) honours the policy.
1893
1920
  //
1894
1921
  // Skip `snapshotInitialNpmrc`: on a repeated sync the target
1895
1922
  // already contains our policy file; snapshotting it would poison
1896
1923
  // the backup so a later flip to allowInstallScripts=true could
1897
1924
  // never restore the original baked content.
1898
- await writeNpmrc(npmrcPath, result.config, options);
1925
+ await writeNpmrc(npmrcPath, result.config);
1899
1926
  }
1900
1927
  else {
1901
1928
  // Clean up a stale policy-only `.npmrc` left by a previous call
@@ -1955,7 +1982,7 @@ export async function maybeWriteNpmrcForDir(dir, options = {}, client) {
1955
1982
  // Snapshot AFTER the gitignore check (we don't need a backup if we're
1956
1983
  // refusing to write).
1957
1984
  await snapshotInitialNpmrc(npmrcPath, backupPath);
1958
- await writeNpmrc(npmrcPath, result.config, options);
1985
+ await writeNpmrc(npmrcPath, result.config);
1959
1986
  return result;
1960
1987
  }
1961
1988
  /**
@@ -1991,11 +2018,11 @@ export async function maybeWriteNpmrcForDir(dir, options = {}, client) {
1991
2018
  * can branch on `source` for observability), or `undefined` when no client
1992
2019
  * was provided. The lockfile strip happens in both cases.
1993
2020
  */
1994
- export async function prepareForPrivateRegistry(dir, options = {}, client) {
2021
+ export async function prepareForPrivateRegistry(dir, client) {
1995
2022
  return withNpmRegistrySpan(NPM_REGISTRY_SPAN.PREPARE_FOR_PRIVATE_REGISTRY, client?.organizationId
1996
2023
  ? { [NPM_REGISTRY_SPAN_ATTR.ORG_ID]: client.organizationId }
1997
2024
  : {}, async (span) => {
1998
- const result = await maybeWriteNpmrcForDir(dir, options, client);
2025
+ const result = await maybeWriteNpmrcForDir(dir, client);
1999
2026
  await stripResolvedFromLockfile(dir);
2000
2027
  if (result) {
2001
2028
  span.setAttributes({