@superblocksteam/vite-plugin-file-sync 2.0.137 → 2.0.138-next.1
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.
- package/dist/ai-service/agent/middleware.d.ts.map +1 -1
- package/dist/ai-service/agent/middleware.js +2 -22
- package/dist/ai-service/agent/middleware.js.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts +1 -0
- package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.js +150 -7
- package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
- package/dist/ai-service/agent/tools/build-read-file.d.ts +10 -0
- package/dist/ai-service/agent/tools/build-read-file.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-read-file.js +14 -1
- package/dist/ai-service/agent/tools/build-read-file.js.map +1 -1
- package/dist/ai-service/agent/tools/index.d.ts +1 -1
- package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/index.js +1 -1
- package/dist/ai-service/agent/tools/index.js.map +1 -1
- package/dist/ai-service/agent/tools.d.ts +1 -1
- package/dist/ai-service/agent/tools.d.ts.map +1 -1
- package/dist/ai-service/agent/tools.js +15 -1
- package/dist/ai-service/agent/tools.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/grep.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/grep.js +2 -1
- package/dist/ai-service/agent/tools2/tools/grep.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/remember-knowledge.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/remember-knowledge.js +8 -0
- package/dist/ai-service/agent/tools2/tools/remember-knowledge.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/start-test-run.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/start-test-run.js +14 -6
- package/dist/ai-service/agent/tools2/tools/start-test-run.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/web-fetch.d.ts +2 -0
- package/dist/ai-service/agent/tools2/tools/web-fetch.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/web-fetch.js +10 -5
- package/dist/ai-service/agent/tools2/tools/web-fetch.js.map +1 -1
- package/dist/ai-service/agent/utils.d.ts.map +1 -1
- package/dist/ai-service/agent/utils.js +25 -7
- package/dist/ai-service/agent/utils.js.map +1 -1
- package/dist/ai-service/app-interface/linter.d.ts.map +1 -1
- package/dist/ai-service/app-interface/linter.js +14 -3
- package/dist/ai-service/app-interface/linter.js.map +1 -1
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts +19 -14
- package/dist/ai-service/app-interface/npm-package-lookup.d.ts.map +1 -1
- package/dist/ai-service/app-interface/npm-package-lookup.js +56 -23
- package/dist/ai-service/app-interface/npm-package-lookup.js.map +1 -1
- package/dist/ai-service/app-interface/npm-registry.d.ts +96 -48
- package/dist/ai-service/app-interface/npm-registry.d.ts.map +1 -1
- package/dist/ai-service/app-interface/npm-registry.js +247 -220
- package/dist/ai-service/app-interface/npm-registry.js.map +1 -1
- package/dist/ai-service/app-interface/shell.d.ts +36 -0
- package/dist/ai-service/app-interface/shell.d.ts.map +1 -1
- package/dist/ai-service/app-interface/shell.js +183 -8
- package/dist/ai-service/app-interface/shell.js.map +1 -1
- package/dist/ai-service/app-skills/helpers.d.ts.map +1 -1
- package/dist/ai-service/app-skills/helpers.js +48 -8
- package/dist/ai-service/app-skills/helpers.js.map +1 -1
- package/dist/ai-service/app-skills/manager.d.ts +5 -0
- package/dist/ai-service/app-skills/manager.d.ts.map +1 -1
- package/dist/ai-service/app-skills/manager.js +13 -4
- package/dist/ai-service/app-skills/manager.js.map +1 -1
- package/dist/ai-service/attachments/uploaded-content-part.d.ts.map +1 -1
- package/dist/ai-service/attachments/uploaded-content-part.js +12 -5
- package/dist/ai-service/attachments/uploaded-content-part.js.map +1 -1
- package/dist/ai-service/index.d.ts +1 -2
- package/dist/ai-service/index.d.ts.map +1 -1
- package/dist/ai-service/index.js +16 -175
- package/dist/ai-service/index.js.map +1 -1
- package/dist/ai-service/judge/judge-eval-service-runner.js +0 -4
- package/dist/ai-service/judge/judge-eval-service-runner.js.map +1 -1
- package/dist/ai-service/judge/judge-service.js +1 -1
- package/dist/ai-service/judge/judge-service.js.map +1 -1
- package/dist/ai-service/knowledge/knowledge-metrics-emitters.d.ts +20 -0
- package/dist/ai-service/knowledge/knowledge-metrics-emitters.d.ts.map +1 -0
- package/dist/ai-service/knowledge/knowledge-metrics-emitters.js +77 -0
- package/dist/ai-service/knowledge/knowledge-metrics-emitters.js.map +1 -0
- package/dist/ai-service/knowledge/knowledge-metrics.d.ts +43 -0
- package/dist/ai-service/knowledge/knowledge-metrics.d.ts.map +1 -0
- package/dist/ai-service/knowledge/knowledge-metrics.js +217 -0
- package/dist/ai-service/knowledge/knowledge-metrics.js.map +1 -0
- package/dist/ai-service/llm/client.d.ts +144 -18
- package/dist/ai-service/llm/client.d.ts.map +1 -1
- package/dist/ai-service/llm/client.js +303 -48
- package/dist/ai-service/llm/client.js.map +1 -1
- package/dist/ai-service/llm/context-v2/context-metrics.d.ts +1 -0
- package/dist/ai-service/llm/context-v2/context-metrics.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context-metrics.js +14 -1
- package/dist/ai-service/llm/context-v2/context-metrics.js.map +1 -1
- package/dist/ai-service/llm/context-v2/context.d.ts +3 -0
- package/dist/ai-service/llm/context-v2/context.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/context.js +34 -1
- package/dist/ai-service/llm/context-v2/context.js.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.d.ts.map +1 -1
- package/dist/ai-service/llm/context-v2/manager.js +4 -1
- package/dist/ai-service/llm/context-v2/manager.js.map +1 -1
- package/dist/ai-service/llm/interaction/adapters/vercel.d.ts +1 -1
- package/dist/ai-service/llm/interaction/adapters/vercel.d.ts.map +1 -1
- package/dist/ai-service/llm/interaction/adapters/vercel.js +19 -4
- package/dist/ai-service/llm/interaction/adapters/vercel.js.map +1 -1
- package/dist/ai-service/llm/interaction/provider.d.ts +17 -3
- package/dist/ai-service/llm/interaction/provider.d.ts.map +1 -1
- package/dist/ai-service/llm/provider.d.ts.map +1 -1
- package/dist/ai-service/llm/provider.js +19 -15
- package/dist/ai-service/llm/provider.js.map +1 -1
- package/dist/ai-service/llm/stream/managed-stream.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/managed-stream.js +25 -2
- package/dist/ai-service/llm/stream/managed-stream.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/llmobs.d.ts +9 -0
- package/dist/ai-service/llm/stream/observers/llmobs.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/observers/llmobs.js +22 -5
- package/dist/ai-service/llm/stream/observers/llmobs.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/logging.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/observers/logging.js +17 -12
- package/dist/ai-service/llm/stream/observers/logging.js.map +1 -1
- package/dist/ai-service/llm/stream/orchestrator.d.ts +1 -0
- package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/orchestrator.js +131 -0
- package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
- package/dist/ai-service/llm/types.d.ts +17 -6
- package/dist/ai-service/llm/types.d.ts.map +1 -1
- package/dist/ai-service/prompt-builder-service/classifiers/prompt-interpret-task.d.ts +3 -1
- package/dist/ai-service/prompt-builder-service/classifiers/prompt-interpret-task.d.ts.map +1 -1
- package/dist/ai-service/prompt-builder-service/classifiers/prompt-interpret-task.js +13 -5
- package/dist/ai-service/prompt-builder-service/classifiers/prompt-interpret-task.js.map +1 -1
- package/dist/ai-service/security/safety-classifier.d.ts +14 -1
- package/dist/ai-service/security/safety-classifier.d.ts.map +1 -1
- package/dist/ai-service/security/safety-classifier.js +26 -12
- package/dist/ai-service/security/safety-classifier.js.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.js +35 -18
- package/dist/ai-service/skills/system/third-party-migration/claude-design.generated.js.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.d.ts.map +1 -1
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.js +7 -5
- package/dist/ai-service/skills/system/third-party-migration/skill.generated.js.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.js +3 -2
- package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.js +14 -7
- package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts +1 -1
- package/dist/ai-service/state-machine/handlers/awaiting-user.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/awaiting-user.js +40 -27
- package/dist/ai-service/state-machine/handlers/awaiting-user.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts +14 -0
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.js +72 -16
- package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/next-steps.d.ts +8 -1
- package/dist/ai-service/state-machine/handlers/next-steps.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/next-steps.js +13 -11
- package/dist/ai-service/state-machine/handlers/next-steps.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/peer.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/peer.js +3 -2
- package/dist/ai-service/state-machine/helpers/peer.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/stable-peer.d.ts +5 -0
- package/dist/ai-service/state-machine/helpers/stable-peer.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/stable-peer.js +37 -0
- package/dist/ai-service/state-machine/helpers/stable-peer.js.map +1 -1
- package/dist/ai-service/template-renderer.d.ts.map +1 -1
- package/dist/ai-service/template-renderer.js +2 -2
- package/dist/ai-service/template-renderer.js.map +1 -1
- package/dist/ai-service/types.d.ts +2 -2
- package/dist/ai-service/types.d.ts.map +1 -1
- package/dist/dev-server-ws-smoke-client.d.ts +41 -0
- package/dist/dev-server-ws-smoke-client.d.ts.map +1 -0
- package/dist/dev-server-ws-smoke-client.js +314 -0
- package/dist/dev-server-ws-smoke-client.js.map +1 -0
- package/dist/early-error-relay.d.ts +31 -0
- package/dist/early-error-relay.d.ts.map +1 -0
- package/dist/early-error-relay.js +88 -0
- package/dist/early-error-relay.js.map +1 -0
- package/dist/file-sync-vite-plugin.d.ts +26 -1
- package/dist/file-sync-vite-plugin.d.ts.map +1 -1
- package/dist/file-sync-vite-plugin.js +82 -43
- package/dist/file-sync-vite-plugin.js.map +1 -1
- package/dist/file-system-helpers.d.ts +13 -0
- package/dist/file-system-helpers.d.ts.map +1 -1
- package/dist/file-system-helpers.js +18 -1
- package/dist/file-system-helpers.js.map +1 -1
- package/dist/file-system-manager.d.ts +0 -1
- package/dist/file-system-manager.d.ts.map +1 -1
- package/dist/file-system-manager.js +0 -53
- package/dist/file-system-manager.js.map +1 -1
- package/dist/injected-index.d.ts +6 -0
- package/dist/injected-index.d.ts.map +1 -1
- package/dist/injected-index.js +59 -14
- package/dist/injected-index.js.map +1 -1
- package/dist/migration/migration-routes.d.ts +3 -0
- package/dist/migration/migration-routes.d.ts.map +1 -1
- package/dist/migration/migration-routes.js +93 -108
- package/dist/migration/migration-routes.js.map +1 -1
- package/dist/migration/restructure.d.ts +15 -0
- package/dist/migration/restructure.d.ts.map +1 -1
- package/dist/migration/restructure.js +50 -1
- package/dist/migration/restructure.js.map +1 -1
- package/dist/migration-templates/app-fullstack/eslint.config.js +6 -1
- package/dist/migration-templates/app-fullstack/package.json +2 -1
- package/dist/plugin-options.d.ts +7 -0
- package/dist/plugin-options.d.ts.map +1 -1
- package/dist/plugin-options.js.map +1 -1
- package/dist/socket-manager.d.ts.map +1 -1
- package/dist/socket-manager.js +0 -5
- package/dist/socket-manager.js.map +1 -1
- package/dist/sync-service/list-dir.d.ts +1 -1
- package/dist/sync-service/list-dir.js +3 -3
- package/dist/sync-service/list-dir.js.map +1 -1
- package/dist/util/log-sanitizer.d.ts.map +1 -1
- package/dist/util/log-sanitizer.js +13 -6
- package/dist/util/log-sanitizer.js.map +1 -1
- package/dist/util/summarize-for-logging.d.ts +14 -0
- package/dist/util/summarize-for-logging.d.ts.map +1 -0
- package/dist/util/summarize-for-logging.js +94 -0
- package/dist/util/summarize-for-logging.js.map +1 -0
- package/package.json +8 -8
- package/dist/ai-service/agent/tools2/tools/explain-code-finalize.d.ts +0 -4
- package/dist/ai-service/agent/tools2/tools/explain-code-finalize.d.ts.map +0 -1
- package/dist/ai-service/agent/tools2/tools/explain-code-finalize.js +0 -19
- package/dist/ai-service/agent/tools2/tools/explain-code-finalize.js.map +0 -1
- package/dist/ai-service/llm/impl/anthropic.d.ts +0 -3
- package/dist/ai-service/llm/impl/anthropic.d.ts.map +0 -1
- package/dist/ai-service/llm/impl/anthropic.js +0 -15
- package/dist/ai-service/llm/impl/anthropic.js.map +0 -1
- package/dist/ai-service/prompts/explain-code.d.ts +0 -7
- package/dist/ai-service/prompts/explain-code.d.ts.map +0 -1
- package/dist/ai-service/prompts/explain-code.js +0 -23
- package/dist/ai-service/prompts/explain-code.js.map +0 -1
- package/dist/ai-service/util/json-stream-parser.d.ts +0 -20
- package/dist/ai-service/util/json-stream-parser.d.ts.map +0 -1
- package/dist/ai-service/util/json-stream-parser.js +0 -139
- 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
|
-
(
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// which
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
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
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* `error
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
config
|
|
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,
|
|
1888
|
+
export async function maybeWriteNpmrcForDir(dir, client) {
|
|
1866
1889
|
if (!client) {
|
|
1867
1890
|
return undefined;
|
|
1868
1891
|
}
|
|
1869
|
-
|
|
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.
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
2025
|
+
const result = await maybeWriteNpmrcForDir(dir, client);
|
|
1999
2026
|
await stripResolvedFromLockfile(dir);
|
|
2000
2027
|
if (result) {
|
|
2001
2028
|
span.setAttributes({
|