@superblocksteam/sdk 2.0.124-next.0 → 2.0.124-next.2

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.
@@ -38,6 +38,7 @@ import {
38
38
  } from "@superblocksteam/vite-plugin-file-sync/lock-service";
39
39
  import {
40
40
  type NpmRegistryClient,
41
+ type NpmRegistryFetchResult,
41
42
  type ParseContext,
42
43
  shouldIgnoreInstallScripts,
43
44
  } from "@superblocksteam/vite-plugin-file-sync/npm-registry";
@@ -224,6 +225,79 @@ async function readPkgJson(cwd: string) {
224
225
  }
225
226
  }
226
227
 
228
+ async function readPackageLock(cwd: string): Promise<string | null> {
229
+ try {
230
+ return await nodeFs.readFile(path.join(cwd, "package-lock.json"), "utf8");
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Canonicalize a lockfile for post-install change detection. Every boot,
238
+ * `stripResolvedFromLockfile` (APPS-4300) removes the `resolved` URLs and
239
+ * the validation install writes them back, so comparing raw bytes flags
240
+ * "changed" on EVERY registry-validation boot — re-uploading the full
241
+ * workspace to DBFS forever. Dropping `resolved` from both sides of the
242
+ * comparison (same npm v2+ `.packages` shape the strip targets) means only
243
+ * material resolution changes — added/removed packages, version bumps,
244
+ * integrity changes — trigger the upload.
245
+ */
246
+ export function lockfileComparisonKey(raw: string | null): string | null {
247
+ if (raw === null) return null;
248
+ try {
249
+ const parsed = JSON.parse(raw) as { packages?: Record<string, unknown> };
250
+ if (parsed?.packages && typeof parsed.packages === "object") {
251
+ for (const entry of Object.values(parsed.packages)) {
252
+ if (entry && typeof entry === "object") {
253
+ delete (entry as { resolved?: unknown }).resolved;
254
+ }
255
+ }
256
+ }
257
+ return JSON.stringify(parsed);
258
+ } catch {
259
+ // Malformed lockfile: fall back to byte-level comparison.
260
+ return raw;
261
+ }
262
+ }
263
+
264
+ async function shouldValidatePrivateRegistryInstall(
265
+ npmRegistryClient: NpmRegistryClient | undefined,
266
+ logger: Logger,
267
+ ): Promise<boolean> {
268
+ if (!npmRegistryClient) {
269
+ return false;
270
+ }
271
+
272
+ try {
273
+ const result: NpmRegistryFetchResult = await npmRegistryClient.getConfig();
274
+ // `configured` / `stale` → the org is known to route installs through a
275
+ // registry (`stale` = last-known-good config during a config-service
276
+ // outage), so the startup install must run even on identical
277
+ // package.json snapshots: with `resolved` stripped from the lockfiles
278
+ // (APPS-4300), npm re-resolves the entire baked tree through that
279
+ // registry, surfacing missing packages at app-creation time instead of
280
+ // at deploy-time bundle build (APPS-4527).
281
+ //
282
+ // `not-configured` and `unreachable` deliberately do NOT force the
283
+ // install. Not-configured orgs gain nothing from re-resolving against
284
+ // public npm on every boot. And when the config service is unreachable
285
+ // with no last-known-good, we cannot materialize the right `.npmrc` —
286
+ // forcing an install would re-resolve a private-registry org's lockfile
287
+ // against whatever registry happens to be on disk, "validating" against
288
+ // the wrong host and writing its URLs back into the lockfile. Fail
289
+ // closed instead (same principle as the AppShell short-circuit,
290
+ // APPS-4370): skip validation and keep today's snapshot decision.
291
+ return result.source === "configured" || result.source === "stale";
292
+ } catch (err) {
293
+ logger.warn(
294
+ "Could not resolve npm registry config for startup install validation; preserving package snapshot decision",
295
+ getErrorMeta(err),
296
+ );
297
+ return false;
298
+ }
299
+ }
300
+
227
301
  async function normalizePackageJsonForNpm(cwd: string, logger: Logger) {
228
302
  const packageJsonPath = path.join(cwd, "package.json");
229
303
  let raw: string;
@@ -1221,13 +1295,22 @@ export async function dev(options: {
1221
1295
  if (!packageJsonBefore && packageJsonRequiresInstall) {
1222
1296
  logger.info("package.json was created, installing packages…");
1223
1297
  }
1298
+ const npmRegistryClient = aiService?.getNpmRegistryClient();
1224
1299
  const forcePackageInstallRequested = !!options.forcePackageInstall;
1225
1300
  let forcePackageInstall = forcePackageInstallRequested;
1301
+ const privateRegistryRequiresInstallValidation = packageJsonAfter
1302
+ ? await shouldValidatePrivateRegistryInstall(
1303
+ npmRegistryClient,
1304
+ logger,
1305
+ )
1306
+ : false;
1226
1307
  if (
1227
1308
  forcePackageInstallRequested &&
1228
1309
  hasPackageJsonSnapshotBeforeRestore
1229
1310
  ) {
1230
- forcePackageInstall = packageJsonRequiresInstall;
1311
+ forcePackageInstall =
1312
+ packageJsonRequiresInstall ||
1313
+ privateRegistryRequiresInstallValidation;
1231
1314
  }
1232
1315
 
1233
1316
  logger.info("Package install decision", {
@@ -1237,6 +1320,7 @@ export async function dev(options: {
1237
1320
  packageJsonRequiresInstall,
1238
1321
  forcePackageInstall,
1239
1322
  forcePackageInstallRequested,
1323
+ privateRegistryRequiresInstallValidation,
1240
1324
  upgradePromiseCount: upgradePromises.length,
1241
1325
  packageJsonSnapshotBefore: packageJsonSnapshotDiagnostic(
1242
1326
  packageJsonSnapshotBefore,
@@ -1280,14 +1364,17 @@ export async function dev(options: {
1280
1364
  }
1281
1365
 
1282
1366
  // Run the verification install when EITHER the package.json
1283
- // requires it, or upgrades just modified package.json/lockfile
1284
- // (re-syncing node_modules), or the caller forced it. Mirrors
1285
- // the original launch condition.
1367
+ // requires it, upgrades just modified package.json/lockfile
1368
+ // (re-syncing node_modules), the caller forced it, or a custom
1369
+ // registry must validate that the required packages resolve there.
1370
+ let packageLockBeforeInstall: string | null = null;
1286
1371
  if (
1287
1372
  packageJsonRequiresInstall ||
1288
1373
  upgradePromises.length > 0 ||
1289
- forcePackageInstall
1374
+ forcePackageInstall ||
1375
+ privateRegistryRequiresInstallValidation
1290
1376
  ) {
1377
+ packageLockBeforeInstall = await readPackageLock(cwd);
1291
1378
  // Launch the install while the upload/restart decisions below
1292
1379
  // are still being evaluated. The synchronous joins at upload,
1293
1380
  // CLI restart, and pre-Vite-startup all observe and surface
@@ -1299,11 +1386,7 @@ export async function dev(options: {
1299
1386
  "installPackages",
1300
1387
  async (span) => {
1301
1388
  try {
1302
- await installPackages(
1303
- cwd,
1304
- logger,
1305
- aiService?.getNpmRegistryClient(),
1306
- );
1389
+ await installPackages(cwd, logger, npmRegistryClient);
1307
1390
  } finally {
1308
1391
  span.end();
1309
1392
  }
@@ -1327,7 +1410,12 @@ export async function dev(options: {
1327
1410
 
1328
1411
  const shouldUploadPackageState =
1329
1412
  hasPackageChanged || forcePackageInstall;
1330
- if (shouldUploadPackageState || uploadFirst) {
1413
+ let shouldUploadAfterInstall = shouldUploadPackageState;
1414
+ if (
1415
+ shouldUploadPackageState ||
1416
+ uploadFirst ||
1417
+ privateRegistryRequiresInstallValidation
1418
+ ) {
1331
1419
  // Upload serializes the post-install lockfile + node_modules
1332
1420
  // tree to DBFS, so it must observe quiesced upgrade+install.
1333
1421
  // Upgrade first — its rejection exits before an install
@@ -1356,7 +1444,18 @@ export async function dev(options: {
1356
1444
  throw joinError;
1357
1445
  }
1358
1446
  }
1359
- if (!skipStartupUploadForCliRestart) {
1447
+ if (
1448
+ privateRegistryRequiresInstallValidation &&
1449
+ !shouldUploadAfterInstall &&
1450
+ lockfileComparisonKey(packageLockBeforeInstall) !==
1451
+ lockfileComparisonKey(await readPackageLock(cwd))
1452
+ ) {
1453
+ shouldUploadAfterInstall = true;
1454
+ }
1455
+ if (
1456
+ !skipStartupUploadForCliRestart &&
1457
+ (shouldUploadAfterInstall || uploadFirst)
1458
+ ) {
1360
1459
  logger.info(
1361
1460
  `Uploading local files to branch '${activeDbfsBranchName}' on server before starting`,
1362
1461
  );