@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.
Files changed (115) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cli-replacement/automatic-upgrades.d.ts +37 -1
  3. package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
  4. package/dist/cli-replacement/automatic-upgrades.js +162 -10
  5. package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
  6. package/dist/cli-replacement/automatic-upgrades.test.js +377 -8
  7. package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
  8. package/dist/cli-replacement/dependency-install-classifier.d.mts +21 -0
  9. package/dist/cli-replacement/dependency-install-classifier.d.mts.map +1 -0
  10. package/dist/cli-replacement/dependency-install-classifier.mjs +83 -0
  11. package/dist/cli-replacement/dependency-install-classifier.mjs.map +1 -0
  12. package/dist/cli-replacement/dependency-install-classifier.test.d.mts +2 -0
  13. package/dist/cli-replacement/dependency-install-classifier.test.d.mts.map +1 -0
  14. package/dist/cli-replacement/dependency-install-classifier.test.mjs +51 -0
  15. package/dist/cli-replacement/dependency-install-classifier.test.mjs.map +1 -0
  16. package/dist/cli-replacement/dev-s3-restore.test.mjs +170 -14
  17. package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
  18. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +33 -2
  19. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
  20. package/dist/cli-replacement/dev-token-priming.test.d.mts +31 -0
  21. package/dist/cli-replacement/dev-token-priming.test.d.mts.map +1 -0
  22. package/dist/cli-replacement/dev-token-priming.test.mjs +87 -0
  23. package/dist/cli-replacement/dev-token-priming.test.mjs.map +1 -0
  24. package/dist/cli-replacement/dev.d.mts +36 -0
  25. package/dist/cli-replacement/dev.d.mts.map +1 -1
  26. package/dist/cli-replacement/dev.interception.test.d.mts +2 -0
  27. package/dist/cli-replacement/dev.interception.test.d.mts.map +1 -0
  28. package/dist/cli-replacement/dev.interception.test.mjs +68 -0
  29. package/dist/cli-replacement/dev.interception.test.mjs.map +1 -0
  30. package/dist/cli-replacement/dev.mjs +396 -62
  31. package/dist/cli-replacement/dev.mjs.map +1 -1
  32. package/dist/cli-replacement/home-npmrc.d.mts +180 -0
  33. package/dist/cli-replacement/home-npmrc.d.mts.map +1 -0
  34. package/dist/cli-replacement/home-npmrc.mjs +283 -0
  35. package/dist/cli-replacement/home-npmrc.mjs.map +1 -0
  36. package/dist/cli-replacement/home-npmrc.test.d.mts +10 -0
  37. package/dist/cli-replacement/home-npmrc.test.d.mts.map +1 -0
  38. package/dist/cli-replacement/home-npmrc.test.mjs +582 -0
  39. package/dist/cli-replacement/home-npmrc.test.mjs.map +1 -0
  40. package/dist/cli-replacement/install-packages.classify.test.d.mts +2 -0
  41. package/dist/cli-replacement/install-packages.classify.test.d.mts.map +1 -0
  42. package/dist/cli-replacement/install-packages.classify.test.mjs +125 -0
  43. package/dist/cli-replacement/install-packages.classify.test.mjs.map +1 -0
  44. package/dist/cli-replacement/install-packages.npm-registry.test.d.mts +2 -0
  45. package/dist/cli-replacement/install-packages.npm-registry.test.d.mts.map +1 -0
  46. package/dist/cli-replacement/install-packages.npm-registry.test.mjs +260 -0
  47. package/dist/cli-replacement/install-packages.npm-registry.test.mjs.map +1 -0
  48. package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts +58 -0
  49. package/dist/cli-replacement/post-upgrade-lockfile-strip.d.mts.map +1 -0
  50. package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs +224 -0
  51. package/dist/cli-replacement/post-upgrade-lockfile-strip.mjs.map +1 -0
  52. package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts +11 -0
  53. package/dist/cli-replacement/post-upgrade-lockfile-strip.test.d.mts.map +1 -0
  54. package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs +317 -0
  55. package/dist/cli-replacement/post-upgrade-lockfile-strip.test.mjs.map +1 -0
  56. package/dist/cli-replacement/userconfig-env.integration.test.d.mts +26 -0
  57. package/dist/cli-replacement/userconfig-env.integration.test.d.mts.map +1 -0
  58. package/dist/cli-replacement/userconfig-env.integration.test.mjs +148 -0
  59. package/dist/cli-replacement/userconfig-env.integration.test.mjs.map +1 -0
  60. package/dist/dev-utils/dev-server-metrics.d.mts +25 -0
  61. package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
  62. package/dist/dev-utils/dev-server-metrics.mjs +84 -0
  63. package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
  64. package/dist/dev-utils/dev-server-metrics.test.d.mts +2 -0
  65. package/dist/dev-utils/dev-server-metrics.test.d.mts.map +1 -0
  66. package/dist/dev-utils/dev-server-metrics.test.mjs +26 -0
  67. package/dist/dev-utils/dev-server-metrics.test.mjs.map +1 -0
  68. package/dist/dev-utils/dev-server.d.mts +23 -1
  69. package/dist/dev-utils/dev-server.d.mts.map +1 -1
  70. package/dist/dev-utils/dev-server.mjs +21 -9
  71. package/dist/dev-utils/dev-server.mjs.map +1 -1
  72. package/dist/dev-utils/dev-server.status.test.d.mts +2 -0
  73. package/dist/dev-utils/dev-server.status.test.d.mts.map +1 -0
  74. package/dist/dev-utils/dev-server.status.test.mjs +41 -0
  75. package/dist/dev-utils/dev-server.status.test.mjs.map +1 -0
  76. package/dist/dev-utils/token-manager.d.ts +31 -0
  77. package/dist/dev-utils/token-manager.d.ts.map +1 -1
  78. package/dist/dev-utils/token-manager.js +34 -0
  79. package/dist/dev-utils/token-manager.js.map +1 -1
  80. package/dist/telemetry/local-obs.js +1 -1
  81. package/dist/telemetry/local-obs.js.map +1 -1
  82. package/dist/telemetry/util.js +1 -1
  83. package/dist/types/scoped-jwt-token-payload.d.ts +1 -0
  84. package/dist/types/scoped-jwt-token-payload.d.ts.map +1 -1
  85. package/dist/version-control.d.mts.map +1 -1
  86. package/dist/version-control.mjs +6 -7
  87. package/dist/version-control.mjs.map +1 -1
  88. package/package.json +12 -12
  89. package/src/cli-replacement/automatic-upgrades.test.ts +530 -8
  90. package/src/cli-replacement/automatic-upgrades.ts +179 -7
  91. package/src/cli-replacement/dependency-install-classifier.mts +118 -0
  92. package/src/cli-replacement/dependency-install-classifier.test.mts +72 -0
  93. package/src/cli-replacement/dev-s3-restore.test.mts +210 -14
  94. package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +35 -2
  95. package/src/cli-replacement/dev-token-priming.test.mts +103 -0
  96. package/src/cli-replacement/dev.interception.test.mts +80 -0
  97. package/src/cli-replacement/dev.mts +495 -92
  98. package/src/cli-replacement/home-npmrc.mts +409 -0
  99. package/src/cli-replacement/home-npmrc.test.mts +757 -0
  100. package/src/cli-replacement/install-packages.classify.test.mts +168 -0
  101. package/src/cli-replacement/install-packages.npm-registry.test.mts +345 -0
  102. package/src/cli-replacement/post-upgrade-lockfile-strip.mts +296 -0
  103. package/src/cli-replacement/post-upgrade-lockfile-strip.test.mts +482 -0
  104. package/src/cli-replacement/userconfig-env.integration.test.mts +189 -0
  105. package/src/dev-utils/dev-server-metrics.mts +96 -0
  106. package/src/dev-utils/dev-server-metrics.test.mts +38 -0
  107. package/src/dev-utils/dev-server.mts +48 -8
  108. package/src/dev-utils/dev-server.status.test.mts +58 -0
  109. package/src/dev-utils/token-manager.ts +36 -0
  110. package/src/telemetry/local-obs.ts +1 -1
  111. package/src/telemetry/util.ts +1 -1
  112. package/src/types/scoped-jwt-token-payload.ts +1 -0
  113. package/src/version-control.mts +8 -6
  114. package/tsconfig.tsbuildinfo +1 -1
  115. 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
+ }