@superblocksteam/sdk 2.0.123 → 2.0.124-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts +37 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +162 -10
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +377 -8
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dependency-install-classifier.d.mts +21 -0
- package/dist/cli-replacement/dependency-install-classifier.d.mts.map +1 -0
- package/dist/cli-replacement/dependency-install-classifier.mjs +83 -0
- package/dist/cli-replacement/dependency-install-classifier.mjs.map +1 -0
- package/dist/cli-replacement/dependency-install-classifier.test.d.mts +2 -0
- package/dist/cli-replacement/dependency-install-classifier.test.d.mts.map +1 -0
- package/dist/cli-replacement/dependency-install-classifier.test.mjs +51 -0
- package/dist/cli-replacement/dependency-install-classifier.test.mjs.map +1 -0
- package/dist/cli-replacement/dev-s3-restore.test.mjs +170 -14
- package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +33 -2
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-token-priming.test.d.mts +31 -0
- package/dist/cli-replacement/dev-token-priming.test.d.mts.map +1 -0
- package/dist/cli-replacement/dev-token-priming.test.mjs +87 -0
- package/dist/cli-replacement/dev-token-priming.test.mjs.map +1 -0
- package/dist/cli-replacement/dev.d.mts +36 -0
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.interception.test.d.mts +2 -0
- package/dist/cli-replacement/dev.interception.test.d.mts.map +1 -0
- package/dist/cli-replacement/dev.interception.test.mjs +68 -0
- package/dist/cli-replacement/dev.interception.test.mjs.map +1 -0
- package/dist/cli-replacement/dev.mjs +396 -62
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/home-npmrc.d.mts +180 -0
- package/dist/cli-replacement/home-npmrc.d.mts.map +1 -0
- package/dist/cli-replacement/home-npmrc.mjs +283 -0
- package/dist/cli-replacement/home-npmrc.mjs.map +1 -0
- package/dist/cli-replacement/home-npmrc.test.d.mts +10 -0
- package/dist/cli-replacement/home-npmrc.test.d.mts.map +1 -0
- package/dist/cli-replacement/home-npmrc.test.mjs +582 -0
- package/dist/cli-replacement/home-npmrc.test.mjs.map +1 -0
- package/dist/cli-replacement/install-packages.classify.test.d.mts +2 -0
- package/dist/cli-replacement/install-packages.classify.test.d.mts.map +1 -0
- package/dist/cli-replacement/install-packages.classify.test.mjs +125 -0
- package/dist/cli-replacement/install-packages.classify.test.mjs.map +1 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.d.mts +2 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.d.mts.map +1 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.mjs +260 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.mjs.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts +58 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs +224 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts +11 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts.map +1 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs +317 -0
- package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs.map +1 -0
- package/dist/cli-replacement/userconfig-env.integration.test.d.mts +26 -0
- package/dist/cli-replacement/userconfig-env.integration.test.d.mts.map +1 -0
- package/dist/cli-replacement/userconfig-env.integration.test.mjs +148 -0
- package/dist/cli-replacement/userconfig-env.integration.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server-metrics.d.mts +25 -0
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.mjs +84 -0
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.test.d.mts +2 -0
- package/dist/dev-utils/dev-server-metrics.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server-metrics.test.mjs +26 -0
- package/dist/dev-utils/dev-server-metrics.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server.d.mts +23 -1
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +21 -9
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/dev-server.status.test.d.mts +2 -0
- package/dist/dev-utils/dev-server.status.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server.status.test.mjs +41 -0
- package/dist/dev-utils/dev-server.status.test.mjs.map +1 -0
- package/dist/dev-utils/token-manager.d.ts +31 -0
- package/dist/dev-utils/token-manager.d.ts.map +1 -1
- package/dist/dev-utils/token-manager.js +34 -0
- package/dist/dev-utils/token-manager.js.map +1 -1
- package/dist/telemetry/local-obs.js +1 -1
- package/dist/telemetry/local-obs.js.map +1 -1
- package/dist/telemetry/util.js +1 -1
- package/dist/types/scoped-jwt-token-payload.d.ts +1 -0
- package/dist/types/scoped-jwt-token-payload.d.ts.map +1 -1
- package/dist/version-control.d.mts.map +1 -1
- package/dist/version-control.mjs +6 -7
- package/dist/version-control.mjs.map +1 -1
- package/package.json +12 -12
- package/src/cli-replacement/automatic-upgrades.test.ts +530 -8
- package/src/cli-replacement/automatic-upgrades.ts +179 -7
- package/src/cli-replacement/dependency-install-classifier.mts +118 -0
- package/src/cli-replacement/dependency-install-classifier.test.mts +72 -0
- package/src/cli-replacement/dev-s3-restore.test.mts +210 -14
- package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +35 -2
- package/src/cli-replacement/dev-token-priming.test.mts +103 -0
- package/src/cli-replacement/dev.interception.test.mts +80 -0
- package/src/cli-replacement/dev.mts +495 -92
- package/src/cli-replacement/home-npmrc.mts +409 -0
- package/src/cli-replacement/home-npmrc.test.mts +757 -0
- package/src/cli-replacement/install-packages.classify.test.mts +168 -0
- package/src/cli-replacement/install-packages.npm-registry.test.mts +345 -0
- package/src/cli-replacement/post-upgrade-lockfile-strip.mts +296 -0
- package/src/cli-replacement/post-upgrade-lockfile-strip.test.mts +482 -0
- package/src/cli-replacement/userconfig-env.integration.test.mts +189 -0
- package/src/dev-utils/dev-server-metrics.mts +96 -0
- package/src/dev-utils/dev-server-metrics.test.mts +38 -0
- package/src/dev-utils/dev-server.mts +48 -8
- package/src/dev-utils/dev-server.status.test.mts +58 -0
- package/src/dev-utils/token-manager.ts +36 -0
- package/src/telemetry/local-obs.ts +1 -1
- package/src/telemetry/util.ts +1 -1
- package/src/types/scoped-jwt-token-payload.ts +1 -0
- package/src/version-control.mts +8 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/.turbo/turbo-publish-package.log +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type NpmRegistryClient,
|
|
7
|
+
type NpmRegistryFetchResult,
|
|
8
|
+
PRESERVE_NPMRC_SCOPES,
|
|
9
|
+
restoreInitialNpmrc,
|
|
10
|
+
snapshotInitialNpmrc,
|
|
11
|
+
type SnapshotInitialNpmrcOutcome,
|
|
12
|
+
writeNpmrc,
|
|
13
|
+
} from "@superblocksteam/vite-plugin-file-sync/npm-registry";
|
|
14
|
+
|
|
15
|
+
import { getErrorMeta, type Logger } from "../telemetry/logging.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* File mode applied to the userconfig after every write. `0o400` (owner
|
|
19
|
+
* read, no write) strips the owner write bit so any `npm config set` or
|
|
20
|
+
* stray write that opens the file `O_WRONLY` fails loudly instead of
|
|
21
|
+
* silently retargeting the configured registry. The atomic rename inside
|
|
22
|
+
* `writeNpmrc` is governed by parent-dir perms — not the target's mode —
|
|
23
|
+
* so this writer can still re-sync the file on subsequent config changes.
|
|
24
|
+
* The stricter mode is a speed bump, not a security boundary: the CLI
|
|
25
|
+
* user can always `chmod +w` first.
|
|
26
|
+
*/
|
|
27
|
+
export const HOME_NPMRC_MODE = 0o400;
|
|
28
|
+
|
|
29
|
+
const LOG_PREFIX = "[home-npmrc]";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Most recent `snapshotInitialNpmrc` outcome, keyed by userconfig target
|
|
33
|
+
* path. `syncHomeNpmrc` runs once per `NpmRegistryClient` cache refresh,
|
|
34
|
+
* so the snapshot (taken on a `configured`/`stale` resolve) and the
|
|
35
|
+
* destructive restore (on a later `not-configured` resolve) happen on
|
|
36
|
+
* SEPARATE invocations within one pod's lifetime. We remember the outcome
|
|
37
|
+
* across invocations so the not-configured branch can refuse to unlink the
|
|
38
|
+
* userconfig when the snapshot is known to have FAILED — a silent EXDEV
|
|
39
|
+
* (target and backup on different filesystems) leaves a real baked-in file
|
|
40
|
+
* with no backup, and unlinking it would fail the customer's install open
|
|
41
|
+
* to public npm (APPS-4428).
|
|
42
|
+
*
|
|
43
|
+
* Keyed by path so the test suite's per-test tmpdir `homeDir`s stay
|
|
44
|
+
* isolated. Absent (never snapshotted this process — e.g. not-configured
|
|
45
|
+
* from boot, or the policy-only path that deliberately skips the snapshot)
|
|
46
|
+
* preserves the original APPS-4328 behaviour: a missing backup is "nothing
|
|
47
|
+
* to preserve", so the unlink is allowed.
|
|
48
|
+
*/
|
|
49
|
+
const lastSnapshotOutcomeByPath = new Map<
|
|
50
|
+
string,
|
|
51
|
+
SnapshotInitialNpmrcOutcome
|
|
52
|
+
>();
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolves the Superblocks-owned npm userconfig path. Centralised so the
|
|
56
|
+
* image-build and CLI auto-upgrade `--userconfig=` plumbing and the
|
|
57
|
+
* runtime writer all agree on the same location. The file lives inside a
|
|
58
|
+
* hidden directory, so it is named `npmrc` (without a leading dot) by
|
|
59
|
+
* convention; the directory itself carries the visibility bit. `homeDir`
|
|
60
|
+
* defaults to `os.homedir()`; tests override it.
|
|
61
|
+
*/
|
|
62
|
+
export function superblocksNpmrcPath(homeDir?: string): string {
|
|
63
|
+
return path.join(homeDir ?? os.homedir(), ".superblocks", "npmrc");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolves the directory npm should write its per-run debug logs to during
|
|
68
|
+
* Superblocks-spawned installs in the live-edit pod. Unlike
|
|
69
|
+
* `superblocksNpmrcPath` (home-based), this is rooted at the application
|
|
70
|
+
* working directory — `<app>/.superblocks/logs` — so each app's install
|
|
71
|
+
* logs are co-located with the app and collectable from a predictable
|
|
72
|
+
* place. Wired into the subprocess env as `NPM_CONFIG_LOGS_DIR` via
|
|
73
|
+
* `buildInstallEnv`; npm creates the directory if it does not exist.
|
|
74
|
+
*/
|
|
75
|
+
export function superblocksLogsPath(appDir: string): string {
|
|
76
|
+
return path.join(appDir, ".superblocks", "logs");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolves the on-disk backup path for the Superblocks-owned npm
|
|
81
|
+
* userconfig. Symmetric with `superblocksNpmrcPath`: lives next to the
|
|
82
|
+
* userconfig under `~/.superblocks/`. `syncHomeNpmrc` hardlinks the
|
|
83
|
+
* image-baked userconfig to this path on the first `configured` resolve
|
|
84
|
+
* so a later transition to `not-configured` has a baseline to restore
|
|
85
|
+
* from (APPS-4328) — mirroring the project-`.npmrc` capture/restore added
|
|
86
|
+
* in APPS-4320.
|
|
87
|
+
*
|
|
88
|
+
* The leading-dot filename mirrors the project-side `NPMRC_DEFAULT_FILENAME`
|
|
89
|
+
* convention even though the live userconfig is `npmrc` (no dot). The dot
|
|
90
|
+
* here is a convention-only marker that this file is the backup, not the
|
|
91
|
+
* active userconfig.
|
|
92
|
+
*/
|
|
93
|
+
export function superblocksNpmrcBackupPath(homeDir?: string): string {
|
|
94
|
+
return path.join(homeDir ?? os.homedir(), ".superblocks", "npmrc.default");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Outcome reported by `syncHomeNpmrc`. Surfaced for tracing + tests rather
|
|
99
|
+
* than passed back to the caller's control flow — every outcome here is a
|
|
100
|
+
* success from the dev-startup point of view (the writer is best-effort
|
|
101
|
+
* and never throws).
|
|
102
|
+
*/
|
|
103
|
+
export type SyncHomeNpmrcOutcome =
|
|
104
|
+
/** Config is set and `~/.superblocks/npmrc` was created or rewritten. */
|
|
105
|
+
| "written"
|
|
106
|
+
/**
|
|
107
|
+
* Server returned `not-configured`. The image-default userconfig (if
|
|
108
|
+
* any) was restored from the `~/.superblocks/npmrc.default` backup
|
|
109
|
+
* captured on a prior configured boot. If no backup exists and the
|
|
110
|
+
* userconfig is Superblocks-owned, the file is removed entirely rather
|
|
111
|
+
* than left pinned at a previous org's registry/token (APPS-4328).
|
|
112
|
+
*
|
|
113
|
+
* This is what the gpoulios-sb review on PR #19621 flagged: long-lived
|
|
114
|
+
* Superblocks-spawned npm/pnpm processes pinned at the userconfig must
|
|
115
|
+
* not silently inherit a configured-org's registry after the org has
|
|
116
|
+
* been deconfigured. While live-edit pods are single-tenant per EE
|
|
117
|
+
* deployment so cross-org leakage isn't reachable, leaving the stale
|
|
118
|
+
* file behind is still wrong — rollback-to-default is the right
|
|
119
|
+
* semantic.
|
|
120
|
+
*/
|
|
121
|
+
| "restored-not-configured"
|
|
122
|
+
/**
|
|
123
|
+
* Cold cache + the registry server is unreachable (or JWT refresh
|
|
124
|
+
* failed). We can't tell "deliberately unset" from a transient outage,
|
|
125
|
+
* so we leave the userconfig whatever it currently is rather than
|
|
126
|
+
* punch a hole in the CLI auto-upgrade about to run.
|
|
127
|
+
*/
|
|
128
|
+
| "skipped-unreachable"
|
|
129
|
+
/**
|
|
130
|
+
* `snapshotInitialNpmrc` failed silently this pod (e.g. EXDEV — target
|
|
131
|
+
* and backup straddle filesystems), so a real baked-in userconfig
|
|
132
|
+
* exists with NO restore baseline. We fail closed on both sides of the
|
|
133
|
+
* lifecycle so the baked file is never lost (APPS-4428):
|
|
134
|
+
*
|
|
135
|
+
* - On a `configured` resolve we refuse to overwrite the baked file
|
|
136
|
+
* with the managed private-registry content. A later
|
|
137
|
+
* `not-configured` transition could not undo the rewrite — restore
|
|
138
|
+
* from backup is impossible (no backup) and unlinking would delete
|
|
139
|
+
* the baseline. Leave the baked file in place; the customer's
|
|
140
|
+
* install routes through the image-baked registry until the next
|
|
141
|
+
* sync (or pod boot) where the snapshot succeeds.
|
|
142
|
+
*
|
|
143
|
+
* - On a `not-configured` resolve (separate cache-refresh invocation)
|
|
144
|
+
* we withhold the unlink for the same reason: the file IS the baked
|
|
145
|
+
* baseline (the configured branch skipped the rewrite), so deleting
|
|
146
|
+
* it would break the install path.
|
|
147
|
+
*
|
|
148
|
+
* Distinct from `restored-not-configured` so telemetry doesn't conflate
|
|
149
|
+
* "rolled back to baseline" with "left alone because we couldn't".
|
|
150
|
+
*/
|
|
151
|
+
| "skipped-snapshot-failed"
|
|
152
|
+
/** The sync failed; the file is unchanged. Details in the warn log. */
|
|
153
|
+
| "error";
|
|
154
|
+
|
|
155
|
+
export interface SyncHomeNpmrcResult {
|
|
156
|
+
outcome: SyncHomeNpmrcOutcome;
|
|
157
|
+
/** Absolute path that was inspected / mutated. */
|
|
158
|
+
path: string;
|
|
159
|
+
/** Resolved fetch source when `outcome === "written"`. */
|
|
160
|
+
source?: NpmRegistryFetchResult["source"];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface SyncHomeNpmrcDeps {
|
|
164
|
+
/**
|
|
165
|
+
* Pre-constructed client with the LD-flag check, JWT provider, and
|
|
166
|
+
* `refreshJwt` hook bound. We deliberately do not instantiate one
|
|
167
|
+
* inside this helper: AppShell + TemplateRenderer share a single
|
|
168
|
+
* client with the dev-server session so cache TTL, 401 retry, and
|
|
169
|
+
* stale-fallback are coherent across all userconfig materialisation
|
|
170
|
+
* sites.
|
|
171
|
+
*/
|
|
172
|
+
npmRegistryClient: NpmRegistryClient;
|
|
173
|
+
logger: Logger;
|
|
174
|
+
/**
|
|
175
|
+
* Override for `os.homedir()`. Tests redirect to an isolated tmpdir so
|
|
176
|
+
* the real `~/.superblocks/` is never touched. Production callers
|
|
177
|
+
* leave it unset.
|
|
178
|
+
*/
|
|
179
|
+
homeDir?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Materialises `~/.superblocks/npmrc` from the server-fetched per-org
|
|
184
|
+
* npm registry config. Designed to run at dev-server CLI startup, after
|
|
185
|
+
* the `NpmRegistryClient` is wired but BEFORE the global Superblocks CLI
|
|
186
|
+
* auto-upgrade: with the file in place and the auto-upgrade invoking
|
|
187
|
+
* npm with `NPM_CONFIG_USERCONFIG=~/.superblocks/npmrc`, the
|
|
188
|
+
* `npm install -g @superblocksteam/cli@…` invocation resolves through
|
|
189
|
+
* the customer's private registry rather than `registry.npmjs.org`.
|
|
190
|
+
*
|
|
191
|
+
* Behaviour:
|
|
192
|
+
*
|
|
193
|
+
* - `client.getConfig()` resolves `configured` (or `stale`): snapshot
|
|
194
|
+
* the image-default userconfig to `~/.superblocks/npmrc.default`
|
|
195
|
+
* (once per pod boot via `link(2)`), then write
|
|
196
|
+
* `~/.superblocks/npmrc` atomically with mode `0o400`. The snapshot
|
|
197
|
+
* captures whatever the image baked (EE GHPR scope + token, or a
|
|
198
|
+
* no-file baseline on a fresh developer laptop) so the
|
|
199
|
+
* `not-configured` branch below has a baseline to restore from. The
|
|
200
|
+
* parent dir is `mkdir -p`'d first so a fresh `$HOME` on a
|
|
201
|
+
* developer's laptop is handled the same as a pod where the image
|
|
202
|
+
* already pre-creates `/home/node/.superblocks/`. If the snapshot
|
|
203
|
+
* attempt FAILS against a real baked file (e.g. EXDEV), we fail
|
|
204
|
+
* closed and SKIP the rewrite: see `skipped-snapshot-failed` in
|
|
205
|
+
* `SyncHomeNpmrcOutcome` for why (APPS-4428).
|
|
206
|
+
*
|
|
207
|
+
* - `client.getConfig()` resolves `not-configured`: restore the
|
|
208
|
+
* image-default userconfig from the `~/.superblocks/npmrc.default`
|
|
209
|
+
* backup captured on a prior `configured` boot. When no backup
|
|
210
|
+
* exists (cold boot where the org was deconfigured before the first
|
|
211
|
+
* `configured` resolve), the userconfig is unlinked entirely so a
|
|
212
|
+
* long-lived Superblocks-spawned npm/pnpm process cannot silently
|
|
213
|
+
* inherit a previously-configured org's registry/token. Closes the
|
|
214
|
+
* stale-userconfig hole flagged on PR #19621 (APPS-4328). One
|
|
215
|
+
* exception: if this process already SAW a snapshot attempt FAIL for
|
|
216
|
+
* this path, the configured branch above will have left the baked
|
|
217
|
+
* userconfig in place (no managed rewrite happened), so unlinking
|
|
218
|
+
* here would delete the baked baseline. Withhold the unlink (APPS-4428).
|
|
219
|
+
*
|
|
220
|
+
* - `client.getConfig()` resolves `unreachable`: cold cache + the
|
|
221
|
+
* registry server is down (or JWT refresh failed). Leave the file
|
|
222
|
+
* alone — the fail-closed gate in the CLI auto-upgrade is the
|
|
223
|
+
* backstop if the upgrade reaches a public registry.
|
|
224
|
+
*
|
|
225
|
+
* - Any throw (fs error, etc.): log + return `outcome: "error"`. We
|
|
226
|
+
* must not crash dev-server startup on a best-effort step.
|
|
227
|
+
*
|
|
228
|
+
* Idempotent for a given resolved config — safe to invoke on every
|
|
229
|
+
* `NpmRegistryClient` cache refresh as well as on startup.
|
|
230
|
+
*/
|
|
231
|
+
export async function syncHomeNpmrc(
|
|
232
|
+
deps: SyncHomeNpmrcDeps,
|
|
233
|
+
): Promise<SyncHomeNpmrcResult> {
|
|
234
|
+
const targetPath = superblocksNpmrcPath(deps.homeDir);
|
|
235
|
+
const backupPath = superblocksNpmrcBackupPath(deps.homeDir);
|
|
236
|
+
|
|
237
|
+
let result: NpmRegistryFetchResult;
|
|
238
|
+
try {
|
|
239
|
+
result = await deps.npmRegistryClient.getConfig();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
// `NpmRegistryClient` only throws for the deliberate-denial branches
|
|
242
|
+
// (RBAC 403, malformed request 400, double-401). All three signal a
|
|
243
|
+
// structurally broken session — surface the warn but keep the
|
|
244
|
+
// userconfig as-is rather than forging a "default" file that masks
|
|
245
|
+
// the real failure.
|
|
246
|
+
deps.logger.warn(
|
|
247
|
+
`${LOG_PREFIX} client.getConfig() failed; ~/.superblocks/npmrc left unchanged`,
|
|
248
|
+
{ path: targetPath, ...getErrorMeta(error) },
|
|
249
|
+
);
|
|
250
|
+
return { outcome: "error", path: targetPath };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (result.source === "unreachable") {
|
|
254
|
+
deps.logger.warn(
|
|
255
|
+
`${LOG_PREFIX} registry server unreachable with no cached config; ~/.superblocks/npmrc left unchanged`,
|
|
256
|
+
{ path: targetPath },
|
|
257
|
+
);
|
|
258
|
+
return { outcome: "skipped-unreachable", path: targetPath };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!result.config.configured) {
|
|
262
|
+
if (result.config.allowInstallScripts === false) {
|
|
263
|
+
// Org policy disallows install scripts even without registry rows.
|
|
264
|
+
// Write a policy-only userconfig with `ignore-scripts=true`.
|
|
265
|
+
//
|
|
266
|
+
// IMPORTANT: we deliberately skip `snapshotInitialNpmrc` here.
|
|
267
|
+
// On a repeated sync the target already contains our own policy
|
|
268
|
+
// file; snapshotting it would poison the backup with policy content
|
|
269
|
+
// instead of the image-baked baseline, so a later flip to
|
|
270
|
+
// allowInstallScripts=true could never restore the original.
|
|
271
|
+
try {
|
|
272
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
273
|
+
await writeNpmrc(targetPath, result.config, {
|
|
274
|
+
preserveScopeLines: PRESERVE_NPMRC_SCOPES,
|
|
275
|
+
mode: HOME_NPMRC_MODE,
|
|
276
|
+
});
|
|
277
|
+
deps.logger.info(
|
|
278
|
+
`${LOG_PREFIX} wrote policy-only userconfig (ignore-scripts)`,
|
|
279
|
+
{ path: targetPath },
|
|
280
|
+
);
|
|
281
|
+
return { outcome: "written", path: targetPath };
|
|
282
|
+
} catch (error) {
|
|
283
|
+
deps.logger.warn(`${LOG_PREFIX} policy-only write failed`, {
|
|
284
|
+
path: targetPath,
|
|
285
|
+
...getErrorMeta(error),
|
|
286
|
+
});
|
|
287
|
+
return { outcome: "error", path: targetPath };
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
// Restore the image-default userconfig from the backup captured on a
|
|
291
|
+
// prior configured boot. When the backup is absent (the org was never
|
|
292
|
+
// configured on this pod, OR the pod was rewritten before this
|
|
293
|
+
// commit landed) the userconfig is unlinked instead — leaving a
|
|
294
|
+
// stale Superblocks-owned userconfig in place would silently pin
|
|
295
|
+
// npm/pnpm at a previous org's registry/token, the exact hole
|
|
296
|
+
// gpoulios-sb flagged on PR #19621.
|
|
297
|
+
//
|
|
298
|
+
// EXCEPT when this process's most recent snapshot attempt FAILED:
|
|
299
|
+
// the snapshot ran against a real baked-in userconfig but `link(2)`
|
|
300
|
+
// failed silently (e.g. EXDEV across filesystems), so no backup
|
|
301
|
+
// exists for an unrelated reason. Unlinking here would delete a file
|
|
302
|
+
// that SHOULD have been preserved and restored. We can't distinguish
|
|
303
|
+
// that from "no backup" inside `restoreInitialNpmrc`, so we withhold
|
|
304
|
+
// the unlink option and leave the userconfig in place (APPS-4428).
|
|
305
|
+
// Absent state (never snapshotted: not-configured from boot, or the
|
|
306
|
+
// policy-only path) keeps the original APPS-4328 unlink behaviour.
|
|
307
|
+
const snapshotFailed =
|
|
308
|
+
lastSnapshotOutcomeByPath.get(targetPath) === "failed";
|
|
309
|
+
if (snapshotFailed) {
|
|
310
|
+
deps.logger.warn(
|
|
311
|
+
`${LOG_PREFIX} registry not configured but the userconfig snapshot failed earlier; leaving the userconfig in place rather than risk deleting an un-backed-up baseline`,
|
|
312
|
+
{ path: targetPath },
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
// Best-effort: `restoreInitialNpmrc` swallows fs errors as
|
|
316
|
+
// warn-and-no-op so dev-server startup never crashes on cleanup.
|
|
317
|
+
try {
|
|
318
|
+
await restoreInitialNpmrc(targetPath, backupPath, {
|
|
319
|
+
unlinkTargetWhenBackupMissing: !snapshotFailed,
|
|
320
|
+
});
|
|
321
|
+
// On the `snapshotFailed` branch we passed
|
|
322
|
+
// `unlinkTargetWhenBackupMissing: false`, so `restoreInitialNpmrc`
|
|
323
|
+
// intentionally no-ops (leaves the userconfig in place). Return
|
|
324
|
+
// the dedicated `skipped-snapshot-failed` outcome rather than
|
|
325
|
+
// `restored-not-configured` — nothing was restored or removed,
|
|
326
|
+
// and conflating the two would mislead telemetry/dashboards into
|
|
327
|
+
// thinking the baseline rollback ran when it didn't. The warn
|
|
328
|
+
// above already describes the decision; an info log here would
|
|
329
|
+
// contradict it during incident investigation.
|
|
330
|
+
if (snapshotFailed) {
|
|
331
|
+
return { outcome: "skipped-snapshot-failed", path: targetPath };
|
|
332
|
+
}
|
|
333
|
+
deps.logger.info(
|
|
334
|
+
`${LOG_PREFIX} registry not configured; restored image-default userconfig (or removed Superblocks-owned file when no backup exists)`,
|
|
335
|
+
{ path: targetPath },
|
|
336
|
+
);
|
|
337
|
+
return { outcome: "restored-not-configured", path: targetPath };
|
|
338
|
+
} catch (error) {
|
|
339
|
+
deps.logger.warn(`${LOG_PREFIX} restore failed`, {
|
|
340
|
+
path: targetPath,
|
|
341
|
+
...getErrorMeta(error),
|
|
342
|
+
});
|
|
343
|
+
return { outcome: "error", path: targetPath };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
// `mkdir -p` so a fresh `$HOME` (developer laptop, unprovisioned
|
|
350
|
+
// sandbox) doesn't ENOENT inside `writeNpmrc`'s atomic-rename. Pod
|
|
351
|
+
// images already create `/home/node/.superblocks/` at build time, so
|
|
352
|
+
// this is a no-op there.
|
|
353
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
354
|
+
// Snapshot the image-default userconfig BEFORE the first rewrite so
|
|
355
|
+
// the not-configured branch above has a restore baseline. Hardlink
|
|
356
|
+
// is atomic + idempotent: only one of N concurrent callers wins
|
|
357
|
+
// (kernel-serialised), and a subsequent `writeNpmrc` tmp+rename
|
|
358
|
+
// gives the userconfig a fresh inode while the backup inode (the
|
|
359
|
+
// baked content) stays alive. EEXIST after the first capture is
|
|
360
|
+
// expected and not a warning.
|
|
361
|
+
//
|
|
362
|
+
// Remember the outcome so a later `not-configured` resolve (a
|
|
363
|
+
// separate `syncHomeNpmrc` invocation) can tell whether a usable
|
|
364
|
+
// backup actually exists before it asks `restoreInitialNpmrc` to
|
|
365
|
+
// unlink the userconfig. A silent `failed` here (e.g. EXDEV) must
|
|
366
|
+
// NOT lead the not-configured branch to delete a real baked file
|
|
367
|
+
// that has no backup (APPS-4428).
|
|
368
|
+
const snapshotOutcome = await snapshotInitialNpmrc(targetPath, backupPath);
|
|
369
|
+
lastSnapshotOutcomeByPath.set(targetPath, snapshotOutcome);
|
|
370
|
+
|
|
371
|
+
// FAIL CLOSED on snapshot failure (APPS-4428, gpoulios-sb review of
|
|
372
|
+
// PR #19690). When a real baked file existed but the hardlink to
|
|
373
|
+
// `backupPath` failed (only `"failed"` reports this — `"no-source"`
|
|
374
|
+
// means there was nothing to snapshot in the first place), we have
|
|
375
|
+
// no way to undo a managed rewrite later: a `not-configured`
|
|
376
|
+
// transition could not restore the baseline (no backup) and could
|
|
377
|
+
// not safely unlink the file (it would delete the baseline). The
|
|
378
|
+
// earlier guard only addressed the unlink side and let the rewrite
|
|
379
|
+
// happen anyway, which still left stale private-registry config and
|
|
380
|
+
// auth on disk after deconfigure. The only safe choice is to leave
|
|
381
|
+
// the baked userconfig alone here — the install path falls back to
|
|
382
|
+
// whatever the image baked (typically EE GHPR), which is the
|
|
383
|
+
// pre-private-registry default behaviour.
|
|
384
|
+
if (snapshotOutcome === "failed") {
|
|
385
|
+
deps.logger.warn(
|
|
386
|
+
`${LOG_PREFIX} userconfig snapshot failed; refusing to overwrite the baked userconfig with managed private-registry content (no restore baseline exists)`,
|
|
387
|
+
{ path: targetPath, source: result.source },
|
|
388
|
+
);
|
|
389
|
+
return { outcome: "skipped-snapshot-failed", path: targetPath };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await writeNpmrc(targetPath, result.config, {
|
|
393
|
+
preserveScopeLines: PRESERVE_NPMRC_SCOPES,
|
|
394
|
+
mode: HOME_NPMRC_MODE,
|
|
395
|
+
});
|
|
396
|
+
deps.logger.info(`${LOG_PREFIX} wrote ~/.superblocks/npmrc`, {
|
|
397
|
+
path: targetPath,
|
|
398
|
+
source: result.source,
|
|
399
|
+
mode: HOME_NPMRC_MODE.toString(8).padStart(4, "0"),
|
|
400
|
+
});
|
|
401
|
+
return { outcome: "written", path: targetPath, source: result.source };
|
|
402
|
+
} catch (error) {
|
|
403
|
+
deps.logger.warn(`${LOG_PREFIX} write failed`, {
|
|
404
|
+
path: targetPath,
|
|
405
|
+
...getErrorMeta(error),
|
|
406
|
+
});
|
|
407
|
+
return { outcome: "error", path: targetPath };
|
|
408
|
+
}
|
|
409
|
+
}
|