naro-cli 0.21.0 → 0.22.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/dist/index.js CHANGED
@@ -32,6 +32,8 @@ import {
32
32
  deployDirectory,
33
33
  detectGitInfo,
34
34
  ensureGitignoreHasNaro,
35
+ environmentGuidance,
36
+ environmentSlugProblem,
35
37
  formatRedirectEntry,
36
38
  generateTypes,
37
39
  indexesFromRows,
@@ -97,7 +99,7 @@ import {
97
99
  usageMonthOf,
98
100
  userForAccessToken,
99
101
  writeProjectLink
100
- } from "./chunk-FFPOVA7I.js";
102
+ } from "./chunk-MBSO3K6L.js";
101
103
  import "./chunk-K3NQKI34.js";
102
104
 
103
105
  // src/args.ts
@@ -543,6 +545,37 @@ import { readFileSync as readFileSync6 } from "fs";
543
545
  import { dirname as dirname4, join as join6 } from "path";
544
546
  import { fileURLToPath } from "url";
545
547
 
548
+ // src/data-environment.ts
549
+ var DATA_ENV_MESSAGES = {
550
+ name: "--env must be production or an environment name.",
551
+ preview: "--env preview has no data of its own: preview deployments use production's. Drop --env, or name an environment.",
552
+ projectWide: (command) => `--env is not an option of ${command}: turning the backend on or off applies to every environment.`
553
+ };
554
+ function dataEnvironment(flags) {
555
+ const value = flags.env;
556
+ if (value === void 0 || value === "production") return null;
557
+ if (value === "preview")
558
+ throw new NaroError("VALIDATION_FAILED", DATA_ENV_MESSAGES.preview);
559
+ if (environmentSlugProblem(value) !== null)
560
+ throw new NaroError("VALIDATION_FAILED", DATA_ENV_MESSAGES.name);
561
+ return value;
562
+ }
563
+ async function dataClient(context) {
564
+ return (await context.client()).inEnvironment(dataEnvironment(context.flags));
565
+ }
566
+ function databaseName(environment) {
567
+ return environment === null ? "the project database" : `the ${environment} environment's database`;
568
+ }
569
+ function enableBackendCommand(environment) {
570
+ return environment === null ? "naro backend enable" : `naro environments provision ${environment}`;
571
+ }
572
+ function envFlag(environment) {
573
+ return environment === null ? "" : ` --env ${environment}`;
574
+ }
575
+ function forEnvironment(text, environment) {
576
+ return environmentGuidance(text, environment);
577
+ }
578
+
546
579
  // src/commands/migration.ts
547
580
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
548
581
  import { join, relative } from "path";
@@ -641,7 +674,7 @@ async function runMigrationNew(context, deps = processStdin) {
641
674
  ]);
642
675
  }
643
676
  async function runMigrationList(context) {
644
- const client = await context.client();
677
+ const client = await dataClient(context);
645
678
  const projectId = await context.resolveProjectId();
646
679
  const dir = migrationsDir(context);
647
680
  const local = listMigrationFiles(dir);
@@ -721,7 +754,7 @@ async function runMigrationRepair(context) {
721
754
  "VALIDATION_FAILED",
722
755
  "Pass --status applied or --status reverted."
723
756
  );
724
- const client = await context.client();
757
+ const client = await dataClient(context);
725
758
  const projectId = await context.resolveProjectId();
726
759
  const ledger = await appliedMigrations(client, projectId);
727
760
  const recorded = ledger.some(
@@ -735,7 +768,7 @@ async function runMigrationRepair(context) {
735
768
  }
736
769
  await confirmOrAbort(
737
770
  context,
738
- status === "applied" ? `Mark ${key} as applied in the project database's ledger? Its SQL is not run.` : `Remove ${key} from the project database's ledger? Its SQL is not rolled back.`
771
+ status === "applied" ? `Mark ${key} as applied in ${databaseName(dataEnvironment(context.flags))}'s ledger? Its SQL is not run.` : `Remove ${key} from ${databaseName(dataEnvironment(context.flags))}'s ledger? Its SQL is not rolled back.`
739
772
  );
740
773
  if (status === "applied" && ledger.length === 0)
741
774
  await client.db.query(projectId, { sql: LEDGER_DDL });
@@ -754,7 +787,7 @@ async function runMigrationRepair(context) {
754
787
  ]);
755
788
  }
756
789
  async function runMigrationUp(context) {
757
- const client = await context.client();
790
+ const client = await dataClient(context);
758
791
  const projectId = await context.resolveProjectId();
759
792
  const dir = migrationsDir(context);
760
793
  const local = listMigrationFiles(dir);
@@ -782,7 +815,7 @@ async function runMigrationUp(context) {
782
815
  for (const name of pending) context.output.log(` ${printable(name)}`);
783
816
  await confirmOrAbort(
784
817
  context,
785
- `Apply ${String(pending.length)} migration(s) to the project database? Each file is applied all-or-nothing; a failure stops the run.`
818
+ `Apply ${String(pending.length)} migration(s) to ${databaseName(dataEnvironment(context.flags))}? Each file is applied all-or-nothing; a failure stops the run.`
786
819
  );
787
820
  const { applied, warnings } = await applyMigrationFiles(
788
821
  context,
@@ -1227,14 +1260,20 @@ function backendLines(info, now2 = Date.now()) {
1227
1260
  );
1228
1261
  return lines;
1229
1262
  }
1230
- function adviceLines(info, now2) {
1263
+ var ENVIRONMENT_TURNING_OFF_ADVICE = "Turning it off did not finish: your app's /_naro/* requests may still be answered. The control plane finishes it within a few minutes: the backend's switch is production's, and every environment follows it.";
1264
+ function adviceLines(info, now2, environment = null) {
1231
1265
  if (!info.enabled && info.status !== "none") {
1232
1266
  if (!info.turningOff) return [OFF_ADVICE];
1233
- return [leaseHeld(info, now2) ? TURNING_OFF_NOW_ADVICE : TURNING_OFF_ADVICE];
1267
+ if (leaseHeld(info, now2)) return [TURNING_OFF_NOW_ADVICE];
1268
+ return [
1269
+ environment === null ? TURNING_OFF_ADVICE : ENVIRONMENT_TURNING_OFF_ADVICE
1270
+ ];
1234
1271
  }
1235
1272
  switch (info.status) {
1236
1273
  case "none":
1237
- return ["Run `naro backend enable` to turn it on."];
1274
+ return [
1275
+ environment === null ? "Run `naro backend enable` to turn it on." : `This environment has no backend yet: \`naro environments provision ${environment}\` makes it while production's backend is on \u2014 turn that on with \`naro backend enable\`.`
1276
+ ];
1238
1277
  case "error":
1239
1278
  return [
1240
1279
  ...info.error ? [`Error: ${printable(info.error)}`] : [],
@@ -1255,17 +1294,21 @@ function stoppedAt(info) {
1255
1294
  return `stopped at ${step}${info.error ? `: ${sentence(info.error)}` : "."}`;
1256
1295
  }
1257
1296
  async function runBackendInfo(context) {
1258
- const client = await context.client();
1297
+ const client = await dataClient(context);
1259
1298
  const projectId = await context.resolveProjectId();
1260
1299
  const backend = await client.backend.info(projectId);
1261
1300
  const now2 = Date.now();
1301
+ const environment = dataEnvironment(context.flags);
1262
1302
  context.output.result(
1263
1303
  { projectId, backend, stalled: isStalledBackend(backend, now2) },
1264
- [...backendLines(backend, now2), ...adviceLines(backend, now2)]
1304
+ [
1305
+ ...backendLines(backend, now2),
1306
+ ...adviceLines(backend, now2, environment)
1307
+ ].map((line) => forEnvironment(line, environment))
1265
1308
  );
1266
1309
  }
1267
1310
  async function runBackendEnable(context) {
1268
- const client = await context.client();
1311
+ const client = await dataClient(context);
1269
1312
  const projectId = await context.resolveProjectId();
1270
1313
  let backend;
1271
1314
  try {
@@ -1309,7 +1352,7 @@ async function runBackendEnable(context) {
1309
1352
  }
1310
1353
  async function runBackendDisable(context) {
1311
1354
  await confirmOrAbort(context, DISABLE_QUESTION);
1312
- const client = await context.client();
1355
+ const client = await dataClient(context);
1313
1356
  const projectId = await context.resolveProjectId();
1314
1357
  const backend = await client.backend.disable(projectId);
1315
1358
  if (backend.status === "none") {
@@ -1326,7 +1369,8 @@ async function runBackendDisable(context) {
1326
1369
  ]);
1327
1370
  }
1328
1371
  async function runBackendKeys(context) {
1329
- const client = await context.client();
1372
+ const environment = dataEnvironment(context.flags);
1373
+ const client = await dataClient(context);
1330
1374
  const projectId = await context.resolveProjectId();
1331
1375
  const keys = await client.backend.keys(projectId);
1332
1376
  if (!context.flags.reveal) {
@@ -1339,7 +1383,10 @@ async function runBackendKeys(context) {
1339
1383
  [
1340
1384
  `Anon key: ${keys.anonKey}`,
1341
1385
  `Service key: ${keys.serviceKeyPrefix}\u2026 (hidden)`,
1342
- "`naro backend keys --reveal` prints the service key (needs database write access)."
1386
+ forEnvironment(
1387
+ "`naro backend keys --reveal` prints the service key (needs database write access).",
1388
+ environment
1389
+ )
1343
1390
  ]
1344
1391
  );
1345
1392
  return;
@@ -1359,19 +1406,19 @@ function rotateKind(context) {
1359
1406
  function keyLabel(kind) {
1360
1407
  return kind === "anon" ? "Anon key: " : "Service key: ";
1361
1408
  }
1362
- function rotationWarning(kind, backend) {
1409
+ function rotationWarning(kind, backend, environment = null) {
1363
1410
  if (backend.status === "error")
1364
- return `\u26A0 The ${kind} key is replaced, but provisioning ${stoppedAt(backend)} Run \`naro backend enable\` to finish.`;
1411
+ return `\u26A0 The ${kind} key is replaced, but provisioning ${stoppedAt(backend)} Run \`${enableBackendCommand(environment)}\` to finish.`;
1365
1412
  if (backend.status !== "ready")
1366
- return `\u26A0 The ${kind} key is replaced; another provisioning run is finishing the backend. Check it with \`naro backend\`.`;
1413
+ return `\u26A0 The ${kind} key is replaced; another provisioning run is finishing the backend. Check it with \`naro backend${envFlag(environment)}\`.`;
1367
1414
  return null;
1368
1415
  }
1369
- function checkKeysSentence(kind) {
1416
+ function checkKeysSentence(kind, environment = null) {
1370
1417
  const keys = kind === "service" ? "naro backend keys --reveal" : "naro backend keys";
1371
- return `Run \`${keys}\` to see which key is live, then \`naro backend enable\`.`;
1418
+ return `Run \`${keys}${envFlag(environment)}\` to see which key is live, then \`${enableBackendCommand(environment)}\`.`;
1372
1419
  }
1373
- function rotationHint(kind) {
1374
- return `The rotation may have gone through anyway. ${checkKeysSentence(kind)}`;
1420
+ function rotationHint(kind, environment = null) {
1421
+ return `The rotation may have gone through anyway. ${checkKeysSentence(kind, environment)}`;
1375
1422
  }
1376
1423
  function outcomeUnknown(error) {
1377
1424
  return error.code === "BACKEND_ROTATION_FAILED" || error.code === "INTERNAL_ERROR" || error.code === "NETWORK_ERROR" || error.status !== void 0 && error.status >= 500;
@@ -1382,18 +1429,19 @@ async function runBackendKeysRotate(context) {
1382
1429
  context,
1383
1430
  `Replace the ${kind} key? Every client still sending the old one gets 401 INVALID_API_KEY as soon as the new key is live.`
1384
1431
  );
1385
- const client = await context.client();
1432
+ const client = await dataClient(context);
1433
+ const environment = dataEnvironment(context.flags);
1386
1434
  const projectId = await context.resolveProjectId();
1387
1435
  let rotated;
1388
1436
  try {
1389
1437
  rotated = await client.backend.rotateKey(projectId, kind);
1390
1438
  } catch (error) {
1391
1439
  if (!isNaroError(error)) throw error;
1392
- const hint = rotationHint(kind);
1440
+ const hint = rotationHint(kind, environment);
1393
1441
  if (error.code === "TIMEOUT")
1394
1442
  throw new NaroError(
1395
1443
  "TIMEOUT",
1396
- `Rotating the key did not answer within 2 minutes, so whether it happened is not known. ${checkKeysSentence(kind)}`,
1444
+ `Rotating the key did not answer within 2 minutes, so whether it happened is not known. ${checkKeysSentence(kind, environment)}`,
1397
1445
  { details: { hint }, cause: error }
1398
1446
  );
1399
1447
  if (!outcomeUnknown(error)) throw error;
@@ -1405,7 +1453,7 @@ async function runBackendKeysRotate(context) {
1405
1453
  });
1406
1454
  }
1407
1455
  const { backend, key } = rotated;
1408
- const warning = rotationWarning(kind, backend);
1456
+ const warning = rotationWarning(kind, backend, environment);
1409
1457
  if (kind === "service") context.output.warn(SERVICE_KEY_WARNING);
1410
1458
  if (warning) context.output.warn(warning);
1411
1459
  context.output.result(
@@ -1442,14 +1490,21 @@ function sentence2(text) {
1442
1490
  const line = printable(text).trim();
1443
1491
  return /[.!?…]$/.test(line) ? line : `${line}.`;
1444
1492
  }
1445
- function stoppedMessage(backend) {
1493
+ function stoppedMessage(backend, environment = null) {
1494
+ const enable = enableBackendCommand(environment);
1446
1495
  const step = backend.step ?? "worker";
1447
1496
  const reason = backend.error?.trim() ? `: ${sentence2(backend.error)}` : ".";
1448
1497
  if (BACKEND_STEPS.indexOf(step) > BACKEND_STEPS.indexOf("worker"))
1449
- return `Saved and live on the Naro worker, but updating the backend stopped after the upload, at the ${step} step${reason} Run \`naro backend enable\` to finish.`;
1450
- return `Saved, but not live yet: updating the Naro worker stopped at the ${step} step${reason} The worker keeps its previous settings until \`naro backend enable\` puts the saved ones on it.`;
1498
+ return `Saved and live on the Naro worker, but updating the backend stopped after the upload, at the ${step} step${reason} Run \`${enable}\` to finish.`;
1499
+ return `Saved, but not live yet: updating the Naro worker stopped at the ${step} step${reason} The worker keeps its previous settings until \`${enable}\` puts the saved ones on it.`;
1451
1500
  }
1452
1501
  var NOT_APPLIED_WARNING = "\u26A0 These are the saved settings. The Naro worker's last update did not get past its upload, so it may still run older ones \u2014 run `naro backend enable` to put these on it.";
1502
+ function notAppliedWarning(environment) {
1503
+ return environment === null ? NOT_APPLIED_WARNING : NOT_APPLIED_WARNING.replace(
1504
+ "`naro backend enable`",
1505
+ `\`${enableBackendCommand(environment)}\``
1506
+ );
1507
+ }
1453
1508
  function authLines(auth) {
1454
1509
  const lines = [
1455
1510
  auth.email ? `Email: ${auth.email.provider} \u2014 ${printable(auth.email.from)} (API key ${printable(auth.email.apiKeyPrefix)}\u2026)` : "Email: not set up"
@@ -1495,14 +1550,18 @@ function adviceLines2(auth) {
1495
1550
  return lines;
1496
1551
  }
1497
1552
  async function runBackendAuthStatus(context) {
1498
- const client = await context.client();
1553
+ const client = await dataClient(context);
1499
1554
  const projectId = await context.resolveProjectId();
1500
1555
  const auth = await client.backend.auth(projectId);
1501
- if (auth.applied === false) context.output.warn(NOT_APPLIED_WARNING);
1502
- context.output.result({ projectId, auth }, [
1503
- ...authLines(auth),
1504
- ...adviceLines2(auth)
1505
- ]);
1556
+ const environment = dataEnvironment(context.flags);
1557
+ if (auth.applied === false)
1558
+ context.output.warn(notAppliedWarning(environment));
1559
+ context.output.result(
1560
+ { projectId, auth },
1561
+ [...authLines(auth), ...adviceLines2(auth)].map(
1562
+ (line) => forEnvironment(line, environment)
1563
+ )
1564
+ );
1506
1565
  }
1507
1566
  async function readSecret(context, input, given, flag, what, usage) {
1508
1567
  if (!given)
@@ -1518,6 +1577,7 @@ async function readSecret(context, input, given, flag, what, usage) {
1518
1577
  }
1519
1578
  async function change(context, send2, done, after) {
1520
1579
  const projectId = await context.resolveProjectId();
1580
+ const environment = dataEnvironment(context.flags);
1521
1581
  let answer;
1522
1582
  try {
1523
1583
  answer = await send2(projectId);
@@ -1525,13 +1585,13 @@ async function change(context, send2, done, after) {
1525
1585
  if (isNaroError(error) && error.code === "TIMEOUT")
1526
1586
  throw new NaroError(
1527
1587
  "TIMEOUT",
1528
- "The change did not answer within 2 minutes. `naro backend auth` shows what is saved, and `naro backend enable` finishes putting it on the Naro worker.",
1588
+ `The change did not answer within 2 minutes. \`naro backend auth${envFlag(environment)}\` shows what is saved, and \`${enableBackendCommand(environment)}\` finishes putting it on the Naro worker.`,
1529
1589
  { cause: error }
1530
1590
  );
1531
1591
  const saved = isNaroError(error) ? error.details?.auth : void 0;
1532
1592
  if (isNaroError(error) && error.code === "BACKEND_PROVISION_FAILED" && typeof saved === "object" && saved !== null) {
1533
1593
  for (const line of authLines(saved))
1534
- context.output.log(line);
1594
+ context.output.log(forEnvironment(line, environment));
1535
1595
  throw new NaroError(error.code, error.message, {
1536
1596
  details: { projectId, ...error.details }
1537
1597
  });
@@ -1540,20 +1600,29 @@ async function change(context, send2, done, after) {
1540
1600
  }
1541
1601
  const { auth, backend } = answer;
1542
1602
  if (backend.status === "error") {
1543
- for (const line of authLines(auth)) context.output.log(line);
1544
- throw new NaroError("BACKEND_PROVISION_FAILED", stoppedMessage(backend), {
1545
- details: { projectId, auth, backend }
1546
- });
1603
+ for (const line of authLines(auth))
1604
+ context.output.log(forEnvironment(line, environment));
1605
+ throw new NaroError(
1606
+ "BACKEND_PROVISION_FAILED",
1607
+ stoppedMessage(backend, environment),
1608
+ {
1609
+ details: { projectId, auth, backend }
1610
+ }
1611
+ );
1547
1612
  }
1548
1613
  if (backend.status !== "ready")
1549
1614
  context.output.warn(
1550
- "\u26A0 Saved; another provisioning run is finishing the backend. Check it with `naro backend`."
1615
+ forEnvironment(
1616
+ "\u26A0 Saved; another provisioning run is finishing the backend. Check it with `naro backend`.",
1617
+ environment
1618
+ )
1551
1619
  );
1552
- context.output.result({ projectId, auth, backend }, [
1553
- ...done,
1554
- ...after ? after(auth) : [],
1555
- ...authLines(auth)
1556
- ]);
1620
+ context.output.result(
1621
+ { projectId, auth, backend },
1622
+ [...done, ...after ? after(auth) : [], ...authLines(auth)].map(
1623
+ (line) => forEnvironment(line, environment)
1624
+ )
1625
+ );
1557
1626
  }
1558
1627
  async function runBackendAuthEmailSet(context, input = processStdin2) {
1559
1628
  const { provider, from } = context.flags;
@@ -1572,7 +1641,7 @@ async function runBackendAuthEmailSet(context, input = processStdin2) {
1572
1641
  "Resend API key",
1573
1642
  AUTH_USAGE.email
1574
1643
  );
1575
- const client = await context.client();
1644
+ const client = await dataClient(context);
1576
1645
  await change(
1577
1646
  context,
1578
1647
  (projectId) => client.backend.setAuth(projectId, {
@@ -1588,7 +1657,7 @@ async function runBackendAuthEmailRemove(context) {
1588
1657
  context,
1589
1658
  "Remove email? Confirmation and password reset emails stop at once."
1590
1659
  );
1591
- const client = await context.client();
1660
+ const client = await dataClient(context);
1592
1661
  await change(
1593
1662
  context,
1594
1663
  (projectId) => client.backend.removeAuth(projectId, "email"),
@@ -1614,7 +1683,7 @@ async function runBackendAuthOAuthSet(context, input = processStdin2) {
1614
1683
  `${LABELS[provider]} client secret`,
1615
1684
  AUTH_USAGE.oauth
1616
1685
  );
1617
- const client = await context.client();
1686
+ const client = await dataClient(context);
1618
1687
  const patch = {
1619
1688
  oauth: { [provider]: { clientId, clientSecret } }
1620
1689
  };
@@ -1632,7 +1701,7 @@ async function runBackendAuthOAuthRemove(context) {
1632
1701
  context,
1633
1702
  `Remove ${LABELS[provider]} sign-in? Users who signed in with ${LABELS[provider]} cannot sign in with it until it is set up again.`
1634
1703
  );
1635
- const client = await context.client();
1704
+ const client = await dataClient(context);
1636
1705
  await change(
1637
1706
  context,
1638
1707
  (projectId) => client.backend.removeAuth(projectId, provider),
@@ -1640,7 +1709,7 @@ async function runBackendAuthOAuthRemove(context) {
1640
1709
  );
1641
1710
  }
1642
1711
  async function runBackendAuthRedirectsList(context) {
1643
- const client = await context.client();
1712
+ const client = await dataClient(context);
1644
1713
  const projectId = await context.resolveProjectId();
1645
1714
  const auth = await client.backend.auth(projectId);
1646
1715
  const production = productionOrigins(auth);
@@ -1657,7 +1726,10 @@ async function runBackendAuthRedirectsList(context) {
1657
1726
  ),
1658
1727
  ...auth.redirectOrigins,
1659
1728
  ...auth.redirectOrigins.length === 0 ? [
1660
- "Only the project's own origin may be sent back to. Add another with `naro backend auth redirects add <url>` \u2014 an origin, or only the pages under a path like https://app.example.com/auth/."
1729
+ forEnvironment(
1730
+ "Only the project's own origin may be sent back to. Add another with `naro backend auth redirects add <url>` \u2014 an origin, or only the pages under a path like https://app.example.com/auth/.",
1731
+ dataEnvironment(context.flags)
1732
+ )
1661
1733
  ] : []
1662
1734
  ]
1663
1735
  );
@@ -1670,7 +1742,7 @@ function originOf(context) {
1670
1742
  }
1671
1743
  async function runBackendAuthRedirectsAdd(context) {
1672
1744
  const origin = originOf(context);
1673
- const client = await context.client();
1745
+ const client = await dataClient(context);
1674
1746
  await change(
1675
1747
  context,
1676
1748
  (projectId) => client.backend.setAuth(projectId, {
@@ -1684,7 +1756,7 @@ function allowing(list, origin) {
1684
1756
  }
1685
1757
  async function runBackendAuthRedirectsRemove(context) {
1686
1758
  const given = originOf(context);
1687
- const client = await context.client();
1759
+ const client = await dataClient(context);
1688
1760
  const projectId = await context.resolveProjectId();
1689
1761
  const parsed = parseRedirectEntry(given);
1690
1762
  if (parsed) {
@@ -1730,7 +1802,7 @@ async function runBackendAuthRequireVerification(context) {
1730
1802
  if (value !== "on" && value !== "off")
1731
1803
  throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.verification);
1732
1804
  const on = value === "on";
1733
- const client = await context.client();
1805
+ const client = await dataClient(context);
1734
1806
  await change(
1735
1807
  context,
1736
1808
  (projectId) => client.backend.setAuth(projectId, { requireEmailVerification: on }),
@@ -1744,7 +1816,7 @@ async function runBackendAuthSignups(context) {
1744
1816
  if (value !== "on" && value !== "off")
1745
1817
  throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.signups);
1746
1818
  const open = value === "on";
1747
- const client = await context.client();
1819
+ const client = await dataClient(context);
1748
1820
  await change(
1749
1821
  context,
1750
1822
  (projectId) => client.backend.setAuth(projectId, { signupsEnabled: open }),
@@ -1763,7 +1835,7 @@ async function runBackendAuthPassword(context) {
1763
1835
  "VALIDATION_FAILED",
1764
1836
  `--min-length must be a whole number from ${String(MIN_LENGTH_FLOOR)} to ${String(MIN_LENGTH_CEILING)}. ${AUTH_USAGE.password}`
1765
1837
  );
1766
- const client = await context.client();
1838
+ const client = await dataClient(context);
1767
1839
  await change(
1768
1840
  context,
1769
1841
  (projectId) => client.backend.setAuth(projectId, { passwordMinLength: length }),
@@ -1835,13 +1907,16 @@ function userTable(users) {
1835
1907
  );
1836
1908
  }
1837
1909
  async function runBackendUsersList(context) {
1838
- const client = await context.client();
1910
+ const client = await dataClient(context);
1839
1911
  const projectId = await context.resolveProjectId();
1840
1912
  const { users, total } = await allUsers(client, projectId);
1841
1913
  context.output.result(
1842
1914
  { projectId, users, total },
1843
1915
  users.length === 0 ? [
1844
- "No users yet. Create one with `naro backend users create --username <name> --password-stdin`."
1916
+ forEnvironment(
1917
+ "No users yet. Create one with `naro backend users create --username <name> --password-stdin`.",
1918
+ dataEnvironment(context.flags)
1919
+ )
1845
1920
  ] : [
1846
1921
  `${String(total)} ${total === 1 ? "user" : "users"}, newest first:`,
1847
1922
  ...userTable(users)
@@ -1885,7 +1960,7 @@ async function runBackendUsersCreate(context, input = processStdin3) {
1885
1960
  if (username === void 0 && email === void 0 || context.positionals.length > 0)
1886
1961
  throw new NaroError("VALIDATION_FAILED", USERS_USAGE.create);
1887
1962
  const password = await passwordOf(context, input);
1888
- const client = await context.client();
1963
+ const client = await dataClient(context);
1889
1964
  const projectId = await context.resolveProjectId();
1890
1965
  const body = { password };
1891
1966
  if (username !== void 0) body.username = username;
@@ -1911,7 +1986,7 @@ async function runBackendUsersDelete(context) {
1911
1986
  const [given] = context.positionals;
1912
1987
  if (given === void 0 || context.positionals.length > 1)
1913
1988
  throw new NaroError("VALIDATION_FAILED", USERS_USAGE.delete);
1914
- const client = await context.client();
1989
+ const client = await dataClient(context);
1915
1990
  const projectId = await context.resolveProjectId();
1916
1991
  const { users } = await allUsers(client, projectId);
1917
1992
  const user = find(users, given);
@@ -2018,7 +2093,7 @@ function usageLines(info) {
2018
2093
  }
2019
2094
  return lines;
2020
2095
  }
2021
- function describeLines(info) {
2096
+ function describeLines(info, environment = null) {
2022
2097
  const lines = [
2023
2098
  `Provider: ${info.provider}`,
2024
2099
  `Status: ${info.status}`,
@@ -2039,17 +2114,22 @@ function describeLines(info) {
2039
2114
  );
2040
2115
  lines.push(...usageLines(info));
2041
2116
  if (info.provider === "none")
2042
- lines.push("Run `naro db enable` to create the database.");
2117
+ lines.push(
2118
+ environment === null ? "Run `naro db enable` to create the database." : `The ${environment} environment has no database yet. Run \`naro environments provision ${environment}\` to make it.`
2119
+ );
2043
2120
  return lines;
2044
2121
  }
2045
2122
  async function runDbInfo(context) {
2046
- const client = await context.client();
2123
+ const client = await dataClient(context);
2047
2124
  const projectId = await context.resolveProjectId();
2048
2125
  const database = await client.db.info(projectId);
2049
- context.output.result({ projectId, database }, describeLines(database));
2126
+ context.output.result(
2127
+ { projectId, database },
2128
+ describeLines(database, dataEnvironment(context.flags))
2129
+ );
2050
2130
  }
2051
2131
  async function runDbEnable(context) {
2052
- const client = await context.client();
2132
+ const client = await dataClient(context);
2053
2133
  const projectId = await context.resolveProjectId();
2054
2134
  const database = await client.db.enable(projectId);
2055
2135
  context.output.result({ projectId, database }, [
@@ -2080,7 +2160,7 @@ async function readSql(context, deps) {
2080
2160
  }
2081
2161
  async function runDbQuery(context, deps = processStdin4) {
2082
2162
  const sql = await readSql(context, deps);
2083
- const client = await context.client();
2163
+ const client = await dataClient(context);
2084
2164
  const projectId = await context.resolveProjectId();
2085
2165
  const result = await client.db.query(projectId, { sql });
2086
2166
  const lines = [];
@@ -2115,14 +2195,16 @@ var defaults = {
2115
2195
  writeFileSync2(path, data, { flag: "wx" });
2116
2196
  }
2117
2197
  };
2118
- var EXPORT_NOTICE = "Exporting the project database. Cloudflare holds other queries to this database until the export finishes.";
2198
+ function exportNotice(environment) {
2199
+ return `Exporting ${databaseName(environment)}. Cloudflare holds other queries to this database until the export finishes.`;
2200
+ }
2119
2201
  function reasonOf(error) {
2120
2202
  return error instanceof Error ? error.message : String(error);
2121
2203
  }
2122
2204
  function withoutUrl(text, url) {
2123
2205
  return text.split(url).join("the signed URL").replace(/\S*X-Amz-Signature=\S*/gi, "the signed URL");
2124
2206
  }
2125
- function downloadFailed(url, reason, original) {
2207
+ function downloadFailed(url, reason, original, again = "naro db dump") {
2126
2208
  const scrubbed = withoutUrl(reason, url);
2127
2209
  let cause;
2128
2210
  if (original !== void 0) {
@@ -2135,7 +2217,7 @@ function downloadFailed(url, reason, original) {
2135
2217
  }
2136
2218
  return new NaroError(
2137
2219
  "DATABASE_UNAVAILABLE",
2138
- `Could not download the dump (${scrubbed}). Run naro db dump again.`,
2220
+ `Could not download the dump (${scrubbed}). Run ${again} again.`,
2139
2221
  cause === void 0 ? {} : { cause }
2140
2222
  );
2141
2223
  }
@@ -2153,17 +2235,22 @@ var DownloadStreamError = class extends Error {
2153
2235
  this.name = "DownloadStreamError";
2154
2236
  }
2155
2237
  };
2156
- async function openDownload(deps, url) {
2238
+ async function openDownload(deps, url, again) {
2157
2239
  let response;
2158
2240
  try {
2159
2241
  response = await deps.fetch(url);
2160
2242
  } catch (error) {
2161
- throw downloadFailed(url, reasonOf(error), error);
2243
+ throw downloadFailed(url, reasonOf(error), error, again);
2162
2244
  }
2163
2245
  if (!response.ok || response.body === null) {
2164
2246
  await response.body?.cancel().catch(() => {
2165
2247
  });
2166
- throw downloadFailed(url, `HTTP ${String(response.status)}`);
2248
+ throw downloadFailed(
2249
+ url,
2250
+ `HTTP ${String(response.status)}`,
2251
+ void 0,
2252
+ again
2253
+ );
2167
2254
  }
2168
2255
  return response.body;
2169
2256
  }
@@ -2198,8 +2285,8 @@ async function streamInto(body, destination, end) {
2198
2285
  await pipeline(chunksOf(body), counter, destination, { end });
2199
2286
  return bytes;
2200
2287
  }
2201
- function streamFailed(error, url, shown) {
2202
- return error instanceof DownloadStreamError ? downloadFailed(url, reasonOf(error.original), error.original) : writeFailed(shown, error);
2288
+ function streamFailed(error, url, shown, again) {
2289
+ return error instanceof DownloadStreamError ? downloadFailed(url, reasonOf(error.original), error.original, again) : writeFailed(shown, error);
2203
2290
  }
2204
2291
  function refuseDirectory(path, shown) {
2205
2292
  let isDirectory = false;
@@ -2220,8 +2307,8 @@ function makeOutputDirectory(path, shown) {
2220
2307
  throw writeFailed(shown, error);
2221
2308
  }
2222
2309
  }
2223
- async function downloadToFile(deps, url, path, shown) {
2224
- const body = await openDownload(deps, url);
2310
+ async function downloadToFile(deps, url, path, shown, again) {
2311
+ const body = await openDownload(deps, url, again);
2225
2312
  const partial = join2(
2226
2313
  dirname(path),
2227
2314
  `.${basename(path)}.${String(process.pid)}.part`
@@ -2235,15 +2322,15 @@ async function downloadToFile(deps, url, path, shown) {
2235
2322
  rmSync(partial, { force: true });
2236
2323
  } catch {
2237
2324
  }
2238
- throw streamFailed(error, url, shown);
2325
+ throw streamFailed(error, url, shown, again);
2239
2326
  }
2240
2327
  }
2241
- async function downloadToStdout(deps, url) {
2242
- const body = await openDownload(deps, url);
2328
+ async function downloadToStdout(deps, url, again) {
2329
+ const body = await openDownload(deps, url, again);
2243
2330
  try {
2244
2331
  return await streamInto(body, deps.stdout, false);
2245
2332
  } catch (error) {
2246
- throw streamFailed(error, url, "stdout");
2333
+ throw streamFailed(error, url, "stdout", again);
2247
2334
  }
2248
2335
  }
2249
2336
  async function runDbDump(context, overrides = {}) {
@@ -2272,10 +2359,12 @@ async function runDbDump(context, overrides = {}) {
2272
2359
  };
2273
2360
  const output = flags.output === void 0 ? null : { path: resolve2(context.cwd, flags.output), shown: flags.output };
2274
2361
  if (output) refuseDirectory(output.path, output.shown);
2275
- const client = await context.client();
2362
+ const client = await dataClient(context);
2276
2363
  const projectId = await context.resolveProjectId();
2277
2364
  if (output) makeOutputDirectory(output.path, output.shown);
2278
- context.output.warn(EXPORT_NOTICE);
2365
+ const environment = dataEnvironment(context.flags);
2366
+ const again = `naro db dump${envFlag(environment)}`;
2367
+ context.output.warn(exportNotice(environment));
2279
2368
  const step = await client.db.export(projectId, options, {
2280
2369
  sleep: deps.sleep
2281
2370
  });
@@ -2283,16 +2372,16 @@ async function runDbDump(context, overrides = {}) {
2283
2372
  if (file === null)
2284
2373
  throw new NaroError(
2285
2374
  "DATABASE_UNAVAILABLE",
2286
- "Could not download the dump (Cloudflare finished the export without a file). Run naro db dump again."
2375
+ `Could not download the dump (Cloudflare finished the export without a file). Run ${again} again.`
2287
2376
  );
2288
- const bytes = output === null ? await downloadToStdout(deps, file.url) : await downloadToFile(deps, file.url, output.path, output.shown);
2377
+ const bytes = output === null ? await downloadToStdout(deps, file.url, again) : await downloadToFile(deps, file.url, output.path, output.shown, again);
2289
2378
  const bookmark = step.atBookmark;
2290
2379
  if (!context.output.json) {
2291
2380
  context.output.warn(
2292
2381
  `\u2713 Wrote ${flags.output ?? "stdout"} (${humanSize(bytes)}) at bookmark ${bookmark}.`
2293
2382
  );
2294
2383
  context.output.warn(
2295
- ` naro db restore --bookmark ${bookmark} returns the database to this point.`
2384
+ ` naro db restore --bookmark ${bookmark}${envFlag(dataEnvironment(context.flags))} returns the database to this point.`
2296
2385
  );
2297
2386
  }
2298
2387
  context.output.result({ output: flags.output, bytes, atBookmark: bookmark });
@@ -2313,7 +2402,7 @@ function withWayBack(context, error, before) {
2313
2402
  return error;
2314
2403
  if (!context.output.json)
2315
2404
  context.output.warn(
2316
- `Before this attempt the database was at bookmark ${before}. If the restore ran anyway, naro db restore --bookmark ${before} undoes it.`
2405
+ `Before this attempt the database was at bookmark ${before}. If the restore ran anyway, naro db restore --bookmark ${before}${envFlag(dataEnvironment(context.flags))} undoes it.`
2317
2406
  );
2318
2407
  return new NaroError(error.code, error.message, {
2319
2408
  ...error.status === void 0 ? {} : { status: error.status },
@@ -2331,7 +2420,7 @@ async function runDbRestore(context) {
2331
2420
  );
2332
2421
  if ("bookmark" in given && !isBookmark(given.bookmark))
2333
2422
  throw new NaroError("VALIDATION_FAILED", BOOKMARK_FORMAT_MESSAGE);
2334
- const client = await context.client();
2423
+ const client = await dataClient(context);
2335
2424
  const projectId = await context.resolveProjectId();
2336
2425
  let target;
2337
2426
  let at = null;
@@ -2358,7 +2447,7 @@ async function runDbRestore(context) {
2358
2447
  }
2359
2448
  await confirmOrAbort(
2360
2449
  context,
2361
- `Restore the project database to ${label3}? Everything written after that point is rolled back \u2014 migration history included \u2014 and queries running right now are cancelled.`
2450
+ `Restore ${databaseName(dataEnvironment(context.flags))} to ${label3}? Everything written after that point is rolled back \u2014 migration history included \u2014 and queries running right now are cancelled.`
2362
2451
  );
2363
2452
  const before = await bookmarkNow(context, client, projectId);
2364
2453
  let restored;
@@ -2373,8 +2462,8 @@ async function runDbRestore(context) {
2373
2462
  previousBookmark: restored.previousBookmark
2374
2463
  },
2375
2464
  [
2376
- `\u2713 Restored the project database to bookmark ${restored.bookmark}.`,
2377
- ` To undo: naro db restore --bookmark ${restored.previousBookmark}`
2465
+ `\u2713 Restored ${databaseName(dataEnvironment(context.flags))} to bookmark ${restored.bookmark}.`,
2466
+ ` To undo: naro db restore --bookmark ${restored.previousBookmark}${envFlag(dataEnvironment(context.flags))}`
2378
2467
  ]
2379
2468
  );
2380
2469
  }
@@ -2390,7 +2479,7 @@ async function runDbPull(context, overrides = {}) {
2390
2479
  const name = nextMigrationFileName(existing, "remote_schema");
2391
2480
  const path = join2(dir, name);
2392
2481
  const shown = relativeToCwd(context, path);
2393
- const client = await context.client();
2482
+ const client = await dataClient(context);
2394
2483
  const projectId = await context.resolveProjectId();
2395
2484
  await confirmOrAbort(
2396
2485
  context,
@@ -2524,7 +2613,7 @@ function issueLines(issues, tables) {
2524
2613
  ];
2525
2614
  }
2526
2615
  async function runDbLint(context) {
2527
- const client = await context.client();
2616
+ const client = await dataClient(context);
2528
2617
  const projectId = await context.resolveProjectId();
2529
2618
  const tables = await client.db.tables(projectId);
2530
2619
  const answer = await client.db.query(projectId, { sql: DIAGNOSTICS_SQL });
@@ -2730,7 +2819,7 @@ function resetStopped(context, error, bookmark, lead, message) {
2730
2819
  const naroError = isNaroError(error) ? error : toNaroError(error);
2731
2820
  if (!context.output.json)
2732
2821
  context.output.warn(
2733
- bookmark === null ? lead : `${lead} The database was at bookmark ${bookmark} \u2014 naro db restore --bookmark ${bookmark} undoes the reset.`
2822
+ bookmark === null ? lead : `${lead} The database was at bookmark ${bookmark} \u2014 naro db restore --bookmark ${bookmark}${envFlag(dataEnvironment(context.flags))} undoes the reset.`
2734
2823
  );
2735
2824
  return new NaroError(naroError.code, message(naroError.message), {
2736
2825
  ...naroError.status === void 0 ? {} : { status: naroError.status },
@@ -2757,7 +2846,7 @@ ${problemLines.join("\n")}`,
2757
2846
  { details: { problems } }
2758
2847
  );
2759
2848
  const texts = files.map((name) => readFileSync3(join3(dir, name), "utf8"));
2760
- const client = await context.client();
2849
+ const client = await dataClient(context);
2761
2850
  const projectId = await context.resolveProjectId();
2762
2851
  const bookmark = context.flags.dryRun ? null : await bookmarkNow(context, client, projectId, "resetting");
2763
2852
  const { policyTable: policyTable2, storageTable, ...input } = await readSchema(
@@ -2831,11 +2920,11 @@ ${problemLines.join("\n")}`,
2831
2920
  }
2832
2921
  if (bookmark !== null)
2833
2922
  context.output.log(
2834
- `The database is at bookmark ${bookmark} \u2014 naro db restore --bookmark ${bookmark} undoes the reset.`
2923
+ `The database is at bookmark ${bookmark} \u2014 naro db restore --bookmark ${bookmark}${envFlag(dataEnvironment(context.flags))} undoes the reset.`
2835
2924
  );
2836
2925
  await confirmOrAbort(
2837
2926
  context,
2838
- `Reset the project database? This drops ${counts}, clears the migration ledger, then re-applies ${String(files.length)} migration file(s) from ${shownDir}.`
2927
+ `Reset ${databaseName(dataEnvironment(context.flags))}? This drops ${counts}, clears the migration ledger, then re-applies ${String(files.length)} migration file(s) from ${shownDir}.`
2839
2928
  );
2840
2929
  let done = 0;
2841
2930
  const total = plan.batches.length;
@@ -2887,8 +2976,10 @@ ${problemLines.join("\n")}`,
2887
2976
  warnings
2888
2977
  },
2889
2978
  [
2890
- `\u2713 Reset the project database: dropped ${String(dropped)} object(s), re-applied ${String(reapplied.length)} migration(s).`,
2891
- ...bookmark === null ? [] : [` To undo: naro db restore --bookmark ${bookmark}`]
2979
+ `\u2713 Reset ${databaseName(dataEnvironment(context.flags))}: dropped ${String(dropped)} object(s), re-applied ${String(reapplied.length)} migration(s).`,
2980
+ ...bookmark === null ? [] : [
2981
+ ` To undo: naro db restore --bookmark ${bookmark}${envFlag(dataEnvironment(context.flags))}`
2982
+ ]
2892
2983
  ]
2893
2984
  );
2894
2985
  }
@@ -3524,6 +3615,12 @@ function policyTable(policies) {
3524
3615
  ];
3525
3616
  }
3526
3617
  var UNSHOWN = "has characters that cannot be shown here";
3618
+ function hintsFor(lines, environment) {
3619
+ if (environment === null) return lines;
3620
+ return lines.map(
3621
+ (line) => line.includes(UNSHOWN) || line.startsWith("Add one with ") ? forEnvironment(line, environment) : line
3622
+ );
3623
+ }
3527
3624
  function statementLine(policy) {
3528
3625
  if (policy.invalid !== null)
3529
3626
  return printable(
@@ -3556,7 +3653,7 @@ function policyStatements(policies) {
3556
3653
  }
3557
3654
  async function runPolicyList(context) {
3558
3655
  const table3 = context.flags.table;
3559
- const client = await context.client();
3656
+ const client = await dataClient(context);
3560
3657
  const projectId = await context.resolveProjectId();
3561
3658
  const policies = await client.backend.rlsPolicies(
3562
3659
  projectId,
@@ -3574,7 +3671,10 @@ async function runPolicyList(context) {
3574
3671
  "No policies yet: every table denies anon and signed-in requests.",
3575
3672
  'Add one with `naro policy create "CREATE POLICY \u2026"` or `naro policy set <table> --select public`.'
3576
3673
  ];
3577
- context.output.result({ projectId, policies }, lines);
3674
+ context.output.result(
3675
+ { projectId, policies },
3676
+ hintsFor(lines, dataEnvironment(context.flags))
3677
+ );
3578
3678
  }
3579
3679
  function statementText(context) {
3580
3680
  const [positional] = context.positionals;
@@ -3648,7 +3748,7 @@ async function runStatements(client, projectId, statements, allowUnindexed) {
3648
3748
  }
3649
3749
  async function runPolicyCreate(context) {
3650
3750
  const statements = policyStatementsOf(statementText(context));
3651
- const client = await context.client();
3751
+ const client = await dataClient(context);
3652
3752
  const projectId = await context.resolveProjectId();
3653
3753
  const applied = await runStatements(
3654
3754
  client,
@@ -3669,7 +3769,7 @@ async function runPolicyDrop(context) {
3669
3769
  context,
3670
3770
  `Drop policy ${printable(name)} on ${printable(table3)}? It stops applying within about 10 seconds.`
3671
3771
  );
3672
- const client = await context.client();
3772
+ const client = await dataClient(context);
3673
3773
  const projectId = await context.resolveProjectId();
3674
3774
  const dropped = await client.backend.dropRlsPolicy(
3675
3775
  projectId,
@@ -3683,7 +3783,7 @@ async function runPolicyDrop(context) {
3683
3783
  async function runPolicySet(context) {
3684
3784
  const [table3] = context.positionalOrThrow(1, SET_USAGE);
3685
3785
  const patch = policyPatch(context.flags, context, table3);
3686
- const client = await context.client();
3786
+ const client = await dataClient(context);
3687
3787
  const projectId = await context.resolveProjectId();
3688
3788
  const policy = await client.backend.setPolicy(
3689
3789
  projectId,
@@ -3700,11 +3800,14 @@ async function runPolicySet(context) {
3700
3800
  }
3701
3801
  context.output.result(
3702
3802
  { projectId, policy, ...named === void 0 ? {} : { policies: named } },
3703
- [
3704
- `\u2705 Saved the policy for ${printable(policy.table)}. ${TAKES_EFFECT}`,
3705
- ...policyTable([policy]),
3706
- ...named === void 0 || named.length === 0 ? [] : ["", ...policyStatements(named)]
3707
- ]
3803
+ hintsFor(
3804
+ [
3805
+ `\u2705 Saved the policy for ${printable(policy.table)}. ${TAKES_EFFECT}`,
3806
+ ...policyTable([policy]),
3807
+ ...named === void 0 || named.length === 0 ? [] : ["", ...policyStatements(named)]
3808
+ ],
3809
+ dataEnvironment(context.flags)
3810
+ )
3708
3811
  );
3709
3812
  }
3710
3813
  async function runPolicyRemove(context) {
@@ -3713,7 +3816,7 @@ async function runPolicyRemove(context) {
3713
3816
  context,
3714
3817
  `Remove every policy on ${printable(table3)}? Anon and signed-in requests to it are denied from then on.`
3715
3818
  );
3716
- const client = await context.client();
3819
+ const client = await dataClient(context);
3717
3820
  const projectId = await context.resolveProjectId();
3718
3821
  const policy = await client.backend.removePolicy(projectId, table3);
3719
3822
  const names = policy.policies ?? [];
@@ -3846,7 +3949,7 @@ function ruleText(value) {
3846
3949
  return "invalid" in value ? "invalid" : "condition";
3847
3950
  }
3848
3951
  var SUGAR_NAME = /^naro_(?:select|insert|update|delete)$/;
3849
- function removedLines(policy, dryRun) {
3952
+ function removedLines(policy, dryRun, environment) {
3850
3953
  const verb = dryRun ? "Would remove" : "\u2713 Removed";
3851
3954
  const names = policy.policies ?? [];
3852
3955
  let line;
@@ -3884,7 +3987,11 @@ function removedLines(policy, dryRun) {
3884
3987
  lines.push(
3885
3988
  ` As a command, for the owner ${one ? "rule" : "rules"} (${owners.map((owner) => printable(owner.name)).join(", ")}): CREATE POLICY has no owner column, so naro policy create cannot make ${one ? "it" : "them"} again.`,
3886
3989
  // The server's bytes, or not shown: a name may hold a line break.
3887
- typeof command !== "string" ? " naro policy set <table> --<action> owner --owner <column> sets one." : isPrintable(command) ? command : " The command has characters that cannot be shown here: --json has the exact text."
3990
+ // Under --env, the command acts on the environment (PR #343 review 2).
3991
+ typeof command !== "string" ? forEnvironment(
3992
+ " naro policy set <table> --<action> owner --owner <column> sets one.",
3993
+ environment
3994
+ ) : isPrintable(command) ? forEnvironment(command, environment) : " The command has characters that cannot be shown here: --json has the exact text."
3888
3995
  );
3889
3996
  }
3890
3997
  return lines;
@@ -3899,7 +4006,7 @@ async function runSchemaChange(context, operations, question) {
3899
4006
  const { confirm: _confirm, ...rest } = operation;
3900
4007
  return rest;
3901
4008
  }) : operations;
3902
- const client = await context.client();
4009
+ const client = await dataClient(context);
3903
4010
  const projectId = await context.resolveProjectId();
3904
4011
  const dir = migrationsDir(context);
3905
4012
  const local = listMigrationFiles(dir);
@@ -3924,7 +4031,9 @@ async function runSchemaChange(context, operations, question) {
3924
4031
  ...sqlLines,
3925
4032
  `Dry run \u2014 nothing was applied. It would be recorded as ${printable(result.name)}.`,
3926
4033
  ...result.policies.map((policy) => policyLine(policy, true)),
3927
- ...removed.flatMap((policy) => removedLines(policy, true))
4034
+ ...removed.flatMap(
4035
+ (policy) => removedLines(policy, true, dataEnvironment(context.flags))
4036
+ )
3928
4037
  ]);
3929
4038
  return;
3930
4039
  }
@@ -3946,7 +4055,10 @@ async function runSchemaChange(context, operations, question) {
3946
4055
  }
3947
4056
  }
3948
4057
  for (const warning of allWarnings)
3949
- context.output.warn(`\u26A0 ${printable(warning)}`);
4058
+ context.output.warn(
4059
+ `\u26A0 ${printable(forEnvironment(warning, dataEnvironment(context.flags)))}`
4060
+ );
4061
+ fileWarning &&= forEnvironment(fileWarning, dataEnvironment(context.flags));
3950
4062
  if (fileWarning) context.output.warn(`\u26A0 ${printable(fileWarning)}`);
3951
4063
  context.output.result(
3952
4064
  {
@@ -3960,7 +4072,9 @@ async function runSchemaChange(context, operations, question) {
3960
4072
  ...sqlLines,
3961
4073
  file === null ? `\u2705 Applied ${printable(result.name)}.` : `\u2705 Applied ${printable(result.name)} and wrote ${printable(file)}.`,
3962
4074
  ...result.policies.map((policy) => policyLine(policy)),
3963
- ...removed.flatMap((policy) => removedLines(policy, false))
4075
+ ...removed.flatMap(
4076
+ (policy) => removedLines(policy, false, dataEnvironment(context.flags))
4077
+ )
3964
4078
  ]
3965
4079
  );
3966
4080
  }
@@ -4011,24 +4125,29 @@ async function runDbTableRename(context) {
4011
4125
  if (!table3 || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
4012
4126
  await runSchemaChange(context, [{ op: "renameTable", table: table3, to }]);
4013
4127
  }
4128
+ function inDatabase(environment) {
4129
+ return environment === null ? "" : ` in ${databaseName(environment)}`;
4130
+ }
4014
4131
  async function runDbTableDropColumn(context) {
4015
4132
  const [table3, column] = context.positionals;
4016
4133
  if (!table3 || !column)
4017
4134
  throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
4018
4135
  const target = `${table3}.${column}`;
4136
+ const env = dataEnvironment(context.flags);
4019
4137
  await runSchemaChange(
4020
4138
  context,
4021
4139
  [{ op: "dropColumn", table: table3, column, confirm: target }],
4022
- `Drop column ${printable(target)} and its value in every row? naro db restore can bring it back (Time Travel, 30 days).`
4140
+ `Drop column ${printable(target)}${inDatabase(env)} and its value in every row? naro db restore${envFlag(env)} can bring it back (Time Travel, 30 days).`
4023
4141
  );
4024
4142
  }
4025
4143
  async function runDbTableDrop(context) {
4026
4144
  const [table3] = context.positionals;
4027
4145
  if (!table3) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
4146
+ const env = dataEnvironment(context.flags);
4028
4147
  await runSchemaChange(
4029
4148
  context,
4030
4149
  [{ op: "dropTable", table: table3, confirm: table3 }],
4031
- `Drop table ${printable(table3)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
4150
+ `Drop table ${printable(table3)}${inDatabase(env)} and every row in it? naro db restore${envFlag(env)} can bring it back (Time Travel, 30 days).`
4032
4151
  );
4033
4152
  }
4034
4153
  async function runDbIndexCreate(context) {
@@ -5103,6 +5222,7 @@ ${serializeDotenv(vars)}`;
5103
5222
  var CREATE_USAGE2 = "Usage: naro environments create <slug> [--branch <branch>] [--copy-env preview|production|none] [--database schema|empty]";
5104
5223
  var SET_USAGE2 = "Usage: naro environments set <slug> [--branch <branch> | --no-branch] [--protection on|off]";
5105
5224
  var DELETE_USAGE = "Usage: naro environments delete <slug> [--yes]";
5225
+ var PROVISION_USAGE = "Usage: naro environments provision <slug>";
5106
5226
  var NO_BRANCH = "\u2014";
5107
5227
  function parseCopyEnv(value) {
5108
5228
  if (value === void 0) return void 0;
@@ -5137,14 +5257,26 @@ function table2(rows) {
5137
5257
  ).join(" ")
5138
5258
  );
5139
5259
  }
5260
+ function dataCell(environment) {
5261
+ const status = environment.data?.status ?? "none";
5262
+ return status === "none" ? NO_BRANCH : status;
5263
+ }
5140
5264
  function environmentRow(environment) {
5141
5265
  return [
5142
5266
  environment.slug,
5143
5267
  environment.url,
5144
5268
  environment.branch ?? NO_BRANCH,
5145
- environment.protectionEnabled ? "on" : "off"
5269
+ environment.protectionEnabled ? "on" : "off",
5270
+ dataCell(environment)
5146
5271
  ];
5147
5272
  }
5273
+ function dataErrorLines(environments) {
5274
+ return environments.flatMap(
5275
+ (environment) => environment.data?.status === "error" ? [
5276
+ `\u26A0 ${environment.slug}: ${environment.data.error ?? "its data could not be made."} Run \`naro environments provision ${environment.slug}\` to try again.`
5277
+ ] : []
5278
+ );
5279
+ }
5148
5280
  async function runEnvironmentsList(context) {
5149
5281
  const projectId = await context.resolveProjectId();
5150
5282
  const client = await context.client();
@@ -5152,9 +5284,10 @@ async function runEnvironmentsList(context) {
5152
5284
  const count2 = String(environments.length);
5153
5285
  context.output.result({ projectId, environments, limit }, [
5154
5286
  ...environments.length === 0 ? ["No environments. Create one with `naro environments create qa`."] : table2([
5155
- ["slug", "url", "branch", "protection"],
5287
+ ["slug", "url", "branch", "protection", "data"],
5156
5288
  ...environments.map(environmentRow)
5157
5289
  ]),
5290
+ ...dataErrorLines(environments),
5158
5291
  limit === null ? `${count2} environment${environments.length === 1 ? "" : "s"}` : `${count2} of ${String(limit)} environments`
5159
5292
  ]);
5160
5293
  }
@@ -5217,7 +5350,7 @@ async function runEnvironmentsDelete(context) {
5217
5350
  const projectId = await context.resolveProjectId();
5218
5351
  await confirmOrAbort(
5219
5352
  context,
5220
- `Delete environment ${slug}? Its worker and protection key are removed.`
5353
+ `Delete environment ${slug}? Its worker, protection key and data \u2014 database, backend and Storage files \u2014 are removed.`
5221
5354
  );
5222
5355
  const client = await context.client();
5223
5356
  const found = await client.environments.get(projectId, slug);
@@ -5226,6 +5359,16 @@ async function runEnvironmentsDelete(context) {
5226
5359
  `\u2705 Deleted environment ${slug}`
5227
5360
  ]);
5228
5361
  }
5362
+ async function runEnvironmentsProvision(context) {
5363
+ const [slug] = context.positionalOrThrow(1, PROVISION_USAGE);
5364
+ const projectId = await context.resolveProjectId();
5365
+ const client = await context.client();
5366
+ const found = await client.environments.get(projectId, slug);
5367
+ const environment = await client.environments.provision(projectId, found.id);
5368
+ context.output.result({ projectId, environment }, [
5369
+ (environment.data?.status ?? "none") === "none" ? `Environment ${slug} has no data to make: the project uses neither a Naro database nor a backend.` : `\u2705 Making environment ${slug}'s data \u2014 \`naro environments\` shows how it goes.`
5370
+ ]);
5371
+ }
5229
5372
 
5230
5373
  // src/commands/gen.ts
5231
5374
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
@@ -5235,7 +5378,7 @@ async function runGenTypes(context) {
5235
5378
  const { lang } = context.flags;
5236
5379
  if (lang !== void 0 && lang !== "typescript")
5237
5380
  throw new NaroError("VALIDATION_FAILED", LANG_MESSAGE);
5238
- const client = await context.client();
5381
+ const client = await dataClient(context);
5239
5382
  const projectId = await context.resolveProjectId();
5240
5383
  const tables = await client.db.tables(projectId);
5241
5384
  const read = await policiesForTypes(client, projectId);
@@ -5731,6 +5874,9 @@ function tableName(table3) {
5731
5874
  const name = printable(table3.name);
5732
5875
  return table3.exists ? name : `${name} (dropped)`;
5733
5876
  }
5877
+ function linesFor(lines, environment) {
5878
+ return lines.map((line) => forEnvironment(line, environment));
5879
+ }
5734
5880
  function realtimeLines(info) {
5735
5881
  if (!info.enabled)
5736
5882
  return [
@@ -5744,14 +5890,14 @@ function realtimeLines(info) {
5744
5890
  live2 === null ? "Connections now: not known \u2014 the backend is off or not ready, or the hub did not answer" : `Connections now: ${number(live2.connections)} \xB7 channels ${number(live2.channels)} \xB7 presences ${number(live2.presences)}`
5745
5891
  ];
5746
5892
  }
5747
- async function enableRealtime(client, projectId) {
5893
+ async function enableRealtime(client, projectId, environment) {
5748
5894
  try {
5749
5895
  return await client.realtime.enable(projectId);
5750
5896
  } catch (error) {
5751
5897
  if (isNaroError(error) && error.code === "TIMEOUT")
5752
5898
  throw new NaroError(
5753
5899
  "TIMEOUT",
5754
- "Turning Realtime on did not answer within 2 minutes. `naro realtime` shows where it got to, and `naro realtime enable` again picks up where it stopped.",
5900
+ `Turning Realtime on did not answer within 2 minutes. \`naro realtime${envFlag(environment)}\` shows where it got to, and \`naro realtime enable${envFlag(environment)}\` again picks up where it stopped.`,
5755
5901
  { cause: error }
5756
5902
  );
5757
5903
  throw error;
@@ -5763,28 +5909,41 @@ function tableNames2(context, usage) {
5763
5909
  return context.positionals;
5764
5910
  }
5765
5911
  async function runRealtimeStatus(context) {
5766
- const client = await context.client();
5912
+ const client = await dataClient(context);
5767
5913
  const projectId = await context.resolveProjectId();
5768
5914
  const realtime = await client.realtime.info(projectId);
5769
- context.output.result({ projectId, realtime }, realtimeLines(realtime));
5915
+ context.output.result(
5916
+ { projectId, realtime },
5917
+ linesFor(realtimeLines(realtime), dataEnvironment(context.flags))
5918
+ );
5770
5919
  }
5771
5920
  async function runRealtimeEnable(context) {
5772
- const client = await context.client();
5921
+ const client = await dataClient(context);
5773
5922
  const projectId = await context.resolveProjectId();
5774
- const realtime = await enableRealtime(client, projectId);
5775
- context.output.result({ projectId, realtime }, [
5776
- "\u2705 Realtime is on. Publish a table with naro realtime tables add <table>.",
5777
- ...realtimeLines(realtime)
5778
- ]);
5923
+ const environment = dataEnvironment(context.flags);
5924
+ const realtime = await enableRealtime(client, projectId, environment);
5925
+ context.output.result(
5926
+ { projectId, realtime },
5927
+ linesFor(
5928
+ [
5929
+ "\u2705 Realtime is on. Publish a table with naro realtime tables add <table>.",
5930
+ ...realtimeLines(realtime)
5931
+ ],
5932
+ environment
5933
+ )
5934
+ );
5779
5935
  }
5780
5936
  async function runRealtimeTablesList(context) {
5781
- const client = await context.client();
5937
+ const client = await dataClient(context);
5782
5938
  const projectId = await context.resolveProjectId();
5783
5939
  const tables = await client.realtime.listTables(projectId);
5784
5940
  context.output.result(
5785
5941
  { projectId, tables },
5786
5942
  tables.length === 0 ? [
5787
- "No published tables. `naro realtime tables add <table>` publishes one."
5943
+ forEnvironment(
5944
+ "No published tables. `naro realtime tables add <table>` publishes one.",
5945
+ dataEnvironment(context.flags)
5946
+ )
5788
5947
  ] : tables.map(
5789
5948
  (table3) => table3.exists ? ` ${printable(table3.name)}` : ` ${printable(table3.name)} (dropped: no such table now)`
5790
5949
  )
@@ -5792,7 +5951,7 @@ async function runRealtimeTablesList(context) {
5792
5951
  }
5793
5952
  async function runRealtimeTablesAdd(context) {
5794
5953
  const names = tableNames2(context, ADD_USAGE);
5795
- const client = await context.client();
5954
+ const client = await dataClient(context);
5796
5955
  const projectId = await context.resolveProjectId();
5797
5956
  const tables = [];
5798
5957
  for (const name of names) {
@@ -5814,7 +5973,7 @@ async function runRealtimeTablesRemove(context) {
5814
5973
  context,
5815
5974
  names.length === 1 ? `Stop publishing ${shown}? Subscribers stop receiving its changes within about 10 seconds.` : `Stop publishing ${shown}? Subscribers stop receiving their changes within about 10 seconds.`
5816
5975
  );
5817
- const client = await context.client();
5976
+ const client = await dataClient(context);
5818
5977
  const projectId = await context.resolveProjectId();
5819
5978
  const tables = [];
5820
5979
  for (const name of names) {
@@ -5934,6 +6093,9 @@ function meterLines(info) {
5934
6093
  );
5935
6094
  return lines;
5936
6095
  }
6096
+ function linesFor2(lines, environment) {
6097
+ return lines.map((line) => forEnvironment(line, environment));
6098
+ }
5937
6099
  function storageLines(info) {
5938
6100
  if (info.disabledAt !== null)
5939
6101
  return [
@@ -5957,27 +6119,27 @@ function storageLines(info) {
5957
6119
  ...info.buckets.map(bucketLine)
5958
6120
  ];
5959
6121
  }
5960
- async function enableStorage(client, projectId) {
6122
+ async function enableStorage(client, projectId, environment = null) {
5961
6123
  try {
5962
6124
  return await client.storage.enable(projectId);
5963
6125
  } catch (error) {
5964
6126
  if (isNaroError(error) && error.code === "TIMEOUT")
5965
6127
  throw new NaroError(
5966
6128
  "TIMEOUT",
5967
- "Turning Storage on did not answer within 2 minutes. `naro storage` shows where it got to, and `naro storage enable` again picks up where it stopped.",
6129
+ `Turning Storage on did not answer within 2 minutes. \`naro storage${envFlag(environment)}\` shows where it got to, and \`naro storage enable${envFlag(environment)}\` again picks up where it stopped.`,
5968
6130
  { cause: error }
5969
6131
  );
5970
6132
  throw error;
5971
6133
  }
5972
6134
  }
5973
- async function disableStorage(client, projectId) {
6135
+ async function disableStorage(client, projectId, environment = null) {
5974
6136
  try {
5975
6137
  return await client.storage.disable(projectId);
5976
6138
  } catch (error) {
5977
6139
  if (isNaroError(error) && error.code === "TIMEOUT")
5978
6140
  throw new NaroError(
5979
6141
  "TIMEOUT",
5980
- "Turning Storage off did not answer within 2 minutes. `naro storage` shows where it got to, and `naro storage disable` again picks up where it stopped.",
6142
+ `Turning Storage off did not answer within 2 minutes. \`naro storage${envFlag(environment)}\` shows where it got to, and \`naro storage disable${envFlag(environment)}\` again picks up where it stopped.`,
5981
6143
  { cause: error }
5982
6144
  );
5983
6145
  throw error;
@@ -6004,23 +6166,34 @@ function bucketFields(flags) {
6004
6166
  return fields;
6005
6167
  }
6006
6168
  async function runStorageStatus(context) {
6007
- const client = await context.client();
6169
+ const client = await dataClient(context);
6008
6170
  const projectId = await context.resolveProjectId();
6009
6171
  const storage = await client.storage.info(projectId);
6010
- context.output.result({ projectId, storage }, storageLines(storage));
6172
+ context.output.result(
6173
+ { projectId, storage },
6174
+ linesFor2(storageLines(storage), dataEnvironment(context.flags))
6175
+ );
6011
6176
  }
6012
6177
  async function runStorageEnable(context) {
6013
- const client = await context.client();
6178
+ const client = await dataClient(context);
6014
6179
  const projectId = await context.resolveProjectId();
6015
- const storage = await enableStorage(client, projectId);
6180
+ const environment = dataEnvironment(context.flags);
6181
+ const storage = await enableStorage(client, projectId, environment);
6016
6182
  context.output.result({ projectId, storage }, [
6017
6183
  `\u2705 Storage is on: R2 bucket ${printable(storage.bucket ?? "unknown")}.`,
6018
- ...storage.usage === null ? ["\u23F3 The backend is still finishing; check `naro backend` in a minute."] : [],
6019
- ...storageLines(storage)
6184
+ ...linesFor2(
6185
+ [
6186
+ ...storage.usage === null ? [
6187
+ "\u23F3 The backend is still finishing; check `naro backend` in a minute."
6188
+ ] : [],
6189
+ ...storageLines(storage)
6190
+ ],
6191
+ environment
6192
+ )
6020
6193
  ]);
6021
6194
  }
6022
6195
  async function runStorageDisable(context) {
6023
- const client = await context.client();
6196
+ const client = await dataClient(context);
6024
6197
  const projectId = await context.resolveProjectId();
6025
6198
  const current = await client.storage.info(projectId);
6026
6199
  if (current.bucket === null) {
@@ -6029,27 +6202,34 @@ async function runStorageDisable(context) {
6029
6202
  ]);
6030
6203
  return;
6031
6204
  }
6205
+ const environment = dataEnvironment(context.flags);
6032
6206
  if (current.disabledAt === null)
6033
- await confirmOrAbort(context, DISABLE_QUESTION3);
6034
- const storage = await disableStorage(client, projectId);
6207
+ await confirmOrAbort(
6208
+ context,
6209
+ forEnvironment(DISABLE_QUESTION3, environment)
6210
+ );
6211
+ const storage = await disableStorage(client, projectId, environment);
6035
6212
  context.output.result({ projectId, storage }, [
6036
- "\u2705 Storage is off. Buckets and files are kept; naro storage enable turns it on again.",
6037
- ...storageLines(storage)
6213
+ `\u2705 Storage is off. Buckets and files are kept; naro storage enable${envFlag(environment)} turns it on again.`,
6214
+ ...linesFor2(storageLines(storage), environment)
6038
6215
  ]);
6039
6216
  }
6040
6217
  async function runStorageBucketList(context) {
6041
- const client = await context.client();
6218
+ const client = await dataClient(context);
6042
6219
  const projectId = await context.resolveProjectId();
6043
6220
  const buckets = await client.storage.listBuckets(projectId);
6044
6221
  context.output.result(
6045
6222
  { projectId, buckets },
6046
- buckets.length === 0 ? [NO_BUCKETS] : buckets.map(bucketLine)
6223
+ linesFor2(
6224
+ buckets.length === 0 ? [NO_BUCKETS] : buckets.map(bucketLine),
6225
+ dataEnvironment(context.flags)
6226
+ )
6047
6227
  );
6048
6228
  }
6049
6229
  async function runStorageBucketCreate(context) {
6050
6230
  const name = bucketName(context, CREATE_USAGE3);
6051
6231
  const input = { name, ...bucketFields(context.flags) };
6052
- const client = await context.client();
6232
+ const client = await dataClient(context);
6053
6233
  const projectId = await context.resolveProjectId();
6054
6234
  let bucket;
6055
6235
  try {
@@ -6058,12 +6238,12 @@ async function runStorageBucketCreate(context) {
6058
6238
  if (!isNaroError(error) || error.code !== "STORAGE_NOT_ENABLED")
6059
6239
  throw error;
6060
6240
  context.output.log(ENABLE_FIRST);
6061
- await enableStorage(client, projectId);
6241
+ await enableStorage(client, projectId, dataEnvironment(context.flags));
6062
6242
  bucket = await client.storage.createBucket(projectId, input);
6063
6243
  }
6064
6244
  context.output.result({ projectId, bucket }, [
6065
6245
  `\u2705 Created bucket ${printable(bucket.name)}.`,
6066
- bucketLine(bucket)
6246
+ forEnvironment(bucketLine(bucket), dataEnvironment(context.flags))
6067
6247
  ]);
6068
6248
  }
6069
6249
  async function runStorageBucketUpdate(context) {
@@ -6074,13 +6254,13 @@ async function runStorageBucketUpdate(context) {
6074
6254
  context,
6075
6255
  `Make bucket ${name} public? Anyone with an object's URL can download it.`
6076
6256
  );
6077
- const client = await context.client();
6257
+ const client = await dataClient(context);
6078
6258
  const projectId = await context.resolveProjectId();
6079
6259
  const bucket = await client.storage.updateBucket(projectId, name, patch);
6080
6260
  const takesEffect = patch.public === false ? "It takes effect within about 10 seconds; copies Naro's edge already holds can be served for up to 5 minutes." : "It takes effect within about 10 seconds.";
6081
6261
  context.output.result({ projectId, bucket }, [
6082
6262
  `\u2705 Updated bucket ${printable(bucket.name)}. ${takesEffect}`,
6083
- bucketLine(bucket)
6263
+ forEnvironment(bucketLine(bucket), dataEnvironment(context.flags))
6084
6264
  ]);
6085
6265
  }
6086
6266
  function refuseSearch(context, why) {
@@ -6116,7 +6296,7 @@ async function runStorageBucketEmpty(context) {
6116
6296
  context,
6117
6297
  `bucket empty deletes every object in the bucket. ${EMPTY_USAGE}`
6118
6298
  );
6119
- const client = await context.client();
6299
+ const client = await dataClient(context);
6120
6300
  const projectId = await context.resolveProjectId();
6121
6301
  const bucket = await client.storage.getBucket(projectId, name);
6122
6302
  await confirmOrAbort(
@@ -6139,7 +6319,7 @@ async function runStorageBucketRemove(context) {
6139
6319
  context,
6140
6320
  empty ? `bucket remove --empty deletes every object in the bucket before it removes the bucket. ${REMOVE_USAGE3}` : `bucket remove removes the whole bucket. ${REMOVE_USAGE3}`
6141
6321
  );
6142
- const client = await context.client();
6322
+ const client = await dataClient(context);
6143
6323
  const projectId = await context.resolveProjectId();
6144
6324
  let question = `Remove bucket ${name}?`;
6145
6325
  if (empty) {
@@ -6362,6 +6542,116 @@ var SESSION_PREFIXES = ["/auth/", "/realtime/"];
6362
6542
  function storeFor(config, path) {
6363
6543
  return SESSION_PREFIXES.some((prefix) => path.startsWith(prefix)) ? config.tokens : config.accessJwt ?? null;
6364
6544
  }
6545
+ var ACCESS_JWT_MARGIN_MS = 6e4;
6546
+ function timesOf(jwt) {
6547
+ const payload = jwt.split(".")[1];
6548
+ if (payload === void 0 || payload === "") return null;
6549
+ try {
6550
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
6551
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
6552
+ const claims = JSON.parse(atob(padded));
6553
+ if (!isRecord(claims)) return null;
6554
+ const { exp, iat } = claims;
6555
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
6556
+ return {
6557
+ exp: exp * 1e3,
6558
+ iat: typeof iat === "number" && Number.isFinite(iat) ? iat * 1e3 : null
6559
+ };
6560
+ } catch {
6561
+ return null;
6562
+ }
6563
+ }
6564
+ function expiryOf(jwt) {
6565
+ return timesOf(jwt)?.exp ?? null;
6566
+ }
6567
+ var skews = /* @__PURE__ */ new WeakMap();
6568
+ function localExpiry(config, jwt) {
6569
+ const exp = expiryOf(jwt);
6570
+ return exp === null ? null : exp - (skews.get(config.tokens) ?? 0);
6571
+ }
6572
+ var SESSION_GONE_CODES = /* @__PURE__ */ new Set([
6573
+ "UNAUTHORIZED",
6574
+ "INVALID_SESSION",
6575
+ "FAILED_TO_GET_SESSION"
6576
+ ]);
6577
+ var renewing = /* @__PURE__ */ new WeakMap();
6578
+ function renewAccessJwt(config, jwtStore, session) {
6579
+ const pending = renewing.get(config.tokens);
6580
+ if (pending?.session === session) return pending.done;
6581
+ const done = (async () => {
6582
+ const result = await callWith(
6583
+ config,
6584
+ { method: "GET", path: "/auth/token", withToken: true },
6585
+ void 0,
6586
+ session
6587
+ );
6588
+ if (result.error !== null) {
6589
+ if (result.answer?.status !== 401 || !SESSION_GONE_CODES.has(result.error.code))
6590
+ return result.error;
6591
+ await config.tokens.clearIf(session);
6592
+ await config.tokens.whileIs(null, () => jwtStore.clear());
6593
+ return invalidSession();
6594
+ }
6595
+ const { body, status } = result.answer;
6596
+ const jwt = isRecord(body) ? body.token : void 0;
6597
+ if (typeof jwt !== "string" || jwt === "") return invalidResponse(status);
6598
+ const arrived = Date.now();
6599
+ await config.tokens.whileIs(session, async () => {
6600
+ const iat = timesOf(jwt)?.iat;
6601
+ if (iat !== void 0 && iat !== null)
6602
+ skews.set(config.tokens, iat - arrived);
6603
+ await jwtStore.set(jwt);
6604
+ });
6605
+ return null;
6606
+ })();
6607
+ const entry = { session, done };
6608
+ renewing.set(config.tokens, entry);
6609
+ void done.finally(() => {
6610
+ if (renewing.get(config.tokens) === entry) renewing.delete(config.tokens);
6611
+ });
6612
+ return done;
6613
+ }
6614
+ async function accessJwtFor(config, jwtStore) {
6615
+ for (let round = 0; round < 2; round += 1) {
6616
+ const session = await config.tokens.get();
6617
+ const jwt = await jwtStore.get();
6618
+ if (session === null || config.service) return { token: jwt, session };
6619
+ const expiry = jwt === null ? null : localExpiry(config, jwt);
6620
+ if (jwt !== null && (expiry === null || expiry - ACCESS_JWT_MARGIN_MS > Date.now()))
6621
+ return { token: jwt, session };
6622
+ const error = await renewAccessJwt(config, jwtStore, session);
6623
+ const stored = await config.tokens.get();
6624
+ if (stored !== session) {
6625
+ if (stored === null) return error ? { error } : { token: null, session };
6626
+ continue;
6627
+ }
6628
+ if (error === null) {
6629
+ const renewed = await jwtStore.get();
6630
+ return renewed === null ? { error: invalidSession() } : { token: renewed, session };
6631
+ }
6632
+ if (jwt !== null && expiry !== null && expiry > Date.now())
6633
+ return { token: jwt, session };
6634
+ return { error };
6635
+ }
6636
+ return { error: invalidSession() };
6637
+ }
6638
+ async function bearerFor(config, path, withToken) {
6639
+ if (!withToken) return { token: null, session: null };
6640
+ const store = storeFor(config, path);
6641
+ if (store === null) return { token: null, session: null };
6642
+ if (store === config.tokens) {
6643
+ const token = await store.get();
6644
+ return { token, session: token };
6645
+ }
6646
+ return accessJwtFor(config, store);
6647
+ }
6648
+ async function retryBearer(config, path, error, first) {
6649
+ if (error?.code !== "INVALID_SESSION" || first.token === null || first.session === null || config.accessJwt === void 0 || storeFor(config, path) !== config.accessJwt || await config.tokens.get() !== first.session)
6650
+ return null;
6651
+ const renewed = await bearerFor(config, path, true);
6652
+ if ("error" in renewed) return renewed;
6653
+ return renewed.token === null || renewed.token === first.token || renewed.session !== first.session ? null : renewed;
6654
+ }
6365
6655
  async function receive(config, options, init) {
6366
6656
  const controller = options.timeoutMs !== void 0 && typeof AbortController === "function" ? new AbortController() : null;
6367
6657
  if (controller) init.signal = controller.signal;
@@ -6405,8 +6695,16 @@ async function call(config, options) {
6405
6695
  return { error: invalidQuery("", "is not JSON"), answer: null };
6406
6696
  }
6407
6697
  }
6698
+ const bearer = await bearerFor(config, options.path, options.withToken);
6699
+ if ("error" in bearer) return { error: bearer.error, answer: null };
6700
+ const result = await callWith(config, options, payload, bearer.token);
6701
+ const again = await retryBearer(config, options.path, result.error, bearer);
6702
+ if (again === null) return result;
6703
+ if ("error" in again) return { error: again.error, answer: null };
6704
+ return callWith(config, options, payload, again.token);
6705
+ }
6706
+ async function callWith(config, options, payload, token) {
6408
6707
  const store = storeFor(config, options.path);
6409
- const token = options.withToken && store ? await store.get() : null;
6410
6708
  const headers = {
6411
6709
  ...options.headers,
6412
6710
  apikey: config.apiKey
@@ -6450,8 +6748,48 @@ async function settled(store, error, headers, token) {
6450
6748
  return RETRY_AFTER_CODES.has(error.code) ? withRetryAfter(error, headers) : error;
6451
6749
  }
6452
6750
  async function send(config, options) {
6751
+ const bearer = await untilAborted(
6752
+ bearerFor(config, options.path, options.withToken),
6753
+ options.signal
6754
+ );
6755
+ if (bearer === ABORTED) return { error: uploadAborted(), status: null };
6756
+ if ("error" in bearer) return { error: bearer.error, status: null };
6757
+ const result = await sendWith(config, options, bearer.token);
6758
+ if (result.error?.code !== "INVALID_SESSION" || isStream(options.body))
6759
+ return result;
6760
+ const again = await untilAborted(
6761
+ retryBearer(config, options.path, result.error, bearer),
6762
+ options.signal
6763
+ );
6764
+ if (again === ABORTED) return { error: uploadAborted(), status: null };
6765
+ if (again === null) return result;
6766
+ if ("error" in again) return { error: again.error, status: null };
6767
+ return sendWith(config, options, again.token);
6768
+ }
6769
+ var ABORTED = Symbol("aborted");
6770
+ function untilAborted(promise, signal) {
6771
+ if (!signal) return promise;
6772
+ if (signal.aborted) return Promise.resolve(ABORTED);
6773
+ return new Promise((resolve7, reject) => {
6774
+ const stop = () => resolve7(ABORTED);
6775
+ signal.addEventListener("abort", stop, { once: true });
6776
+ promise.then(
6777
+ (value) => {
6778
+ signal.removeEventListener("abort", stop);
6779
+ resolve7(value);
6780
+ },
6781
+ (error) => {
6782
+ signal.removeEventListener("abort", stop);
6783
+ reject(error);
6784
+ }
6785
+ );
6786
+ });
6787
+ }
6788
+ function isStream(body) {
6789
+ return typeof ReadableStream === "function" && body instanceof ReadableStream;
6790
+ }
6791
+ async function sendWith(config, options, token) {
6453
6792
  const store = storeFor(config, options.path);
6454
- const token = options.withToken && store ? await store.get() : null;
6455
6793
  const headers = {
6456
6794
  ...options.headers,
6457
6795
  apikey: config.apiKey
@@ -7305,6 +7643,23 @@ var TokenStore = class {
7305
7643
  this.#changed();
7306
7644
  });
7307
7645
  }
7646
+ /**
7647
+ * Runs `operation` only while `expected` is the stored token (null: none
7648
+ * is stored), inside this
7649
+ * store's queue, and says whether it ran: no write to this store lands
7650
+ * between the check and the end of the operation. A refreshed access JWT
7651
+ * is kept this way (`http.ts`), so one minted for a session that a
7652
+ * sign-in replaced meanwhile never lands on top of the new session's.
7653
+ * `operation` must not call this store's public methods (it would wait
7654
+ * on itself); another store's are fine.
7655
+ */
7656
+ whileIs(expected, operation) {
7657
+ return this.#serial(async () => {
7658
+ if (await this.#get() !== expected) return false;
7659
+ await operation();
7660
+ return true;
7661
+ });
7662
+ }
7308
7663
  /**
7309
7664
  * Calls `listener` after every write of the token — a sign-in, a
7310
7665
  * sign-out, a renewed session, a cleared INVALID_SESSION — from inside the
@@ -11056,7 +11411,8 @@ var NaroClient2 = class {
11056
11411
  apiKey,
11057
11412
  fetch: options.fetch,
11058
11413
  tokens: new TokenStore(storage, storageKey(baseUrl)),
11059
- accessJwt: new TokenStore(storage, accessJwtKey(baseUrl))
11414
+ accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)),
11415
+ service
11060
11416
  };
11061
11417
  this.auth = new AuthClient(http, {
11062
11418
  verifiers: new FlowVerifiers(
@@ -11207,18 +11563,27 @@ function unwrap(result) {
11207
11563
  }
11208
11564
  var ONE_KEY = /^[\x21-\x7e]+$/;
11209
11565
  async function objects(context) {
11210
- const client = await context.client();
11566
+ const client = await dataClient(context);
11211
11567
  const projectId = await context.resolveProjectId();
11212
11568
  const backend = await client.backend.info(projectId);
11213
- if (backend.status === "none")
11569
+ const environment = dataEnvironment(context.flags);
11570
+ if (backend.status === "none") {
11214
11571
  throw new NaroError(
11215
11572
  "BACKEND_NOT_READY",
11216
- "This project has no backend yet. Run `naro backend enable`, then `naro storage enable`."
11573
+ environment === null ? "This project has no backend yet. Run `naro backend enable`, then `naro storage enable`." : `The ${environment} environment has no backend yet. Run \`naro environments provision ${environment}\` to make it.`
11574
+ );
11575
+ }
11576
+ if (!backend.enabled)
11577
+ throw new NaroError(
11578
+ "BACKEND_OFF",
11579
+ forEnvironment(BACKEND_OFF_MESSAGE, environment)
11217
11580
  );
11218
- if (!backend.enabled) throw new NaroError("BACKEND_OFF", BACKEND_OFF_MESSAGE);
11219
11581
  const state = await client.storage.info(projectId);
11220
11582
  if (state.disabledAt !== null)
11221
- throw new NaroError("STORAGE_OFF", STORAGE_OFF_MESSAGE);
11583
+ throw new NaroError(
11584
+ "STORAGE_OFF",
11585
+ forEnvironment(STORAGE_OFF_MESSAGE, environment)
11586
+ );
11222
11587
  let serviceKey = process.env.NARO_SERVICE_KEY?.trim() ?? "";
11223
11588
  if (serviceKey !== "" && !ONE_KEY.test(serviceKey))
11224
11589
  throw new NaroError(
@@ -11239,7 +11604,10 @@ async function objects(context) {
11239
11604
  } catch (error) {
11240
11605
  throw new NaroError(
11241
11606
  "BACKEND_NOT_READY",
11242
- `The project's backend url cannot be used (${printable(backend.url)}). Run \`naro backend\` to see where the backend got to.`,
11607
+ forEnvironment(
11608
+ `The project's backend url cannot be used (${printable(backend.url)}). Run \`naro backend\` to see where the backend got to.`,
11609
+ environment
11610
+ ),
11243
11611
  { cause: error }
11244
11612
  );
11245
11613
  }
@@ -11313,7 +11681,7 @@ async function storageLs(context) {
11313
11681
  "VALIDATION_FAILED",
11314
11682
  `--search needs a naro:// path. ${LS_USAGE}`
11315
11683
  );
11316
- const client = await context.client();
11684
+ const client = await dataClient(context);
11317
11685
  const projectId2 = await context.resolveProjectId();
11318
11686
  const buckets = await client.storage.listBuckets(projectId2);
11319
11687
  context.output.result(
@@ -11970,12 +12338,14 @@ Usage:
11970
12338
  naro env pull [file] Write env vars to .env.local
11971
12339
  target is production, preview, all or an environment name; add and import use all when none is given
11972
12340
 
11973
- naro environments List the project's environments: address, branch, protection, and how many the plan allows
12341
+ naro environments List the project's environments: address, branch, protection, data, and how many the plan allows
11974
12342
  naro environments create <slug> [--branch <b>] [--copy-env preview|production|none] [--database schema|empty]
11975
12343
  A QA or staging surface served at <subdomain>--<slug>; starts with preview's variables
11976
12344
  naro environments set <slug> [--branch <b> | --no-branch] [--protection on|off]
11977
12345
  Change the branch whose pushes deploy here, or who can open it (protection off asks first; --yes in CI)
11978
- naro environments delete <slug> Delete it with its worker and protection key (asks first; --yes in CI)
12346
+ naro environments delete <slug> Delete it with its worker, protection key and data (asks first; --yes in CI)
12347
+ naro environments provision <slug>
12348
+ Make its data \u2014 database, backend, Storage \u2014 or go on after a stop
11979
12349
 
11980
12350
  naro domains list | add <host> [--no-www] | remove <host>
11981
12351
  add example.com also connects www.example.com and redirects the apex to
@@ -11984,6 +12354,9 @@ Usage:
11984
12354
  naro domains verify [host] [--wait] Re-check DNS and print what is missing
11985
12355
  exits non-zero while a domain is not live yet
11986
12356
 
12357
+ The data commands below (db, migration, gen, backend, policy, storage, realtime) take --env <name>:
12358
+ a named environment's data instead of production's, which preview shares
12359
+ (naro-cli 0.21.0 and older ignore --env on them and act on production)
11987
12360
  naro db enable Create the project's database (Cloudflare D1)
11988
12361
  naro db info Database status, size, table count and limits
11989
12362
  and today's writes against the daily write limit (UTC)
@@ -12149,7 +12522,8 @@ Global options:
12149
12522
  --browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
12150
12523
  --os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
12151
12524
  --to <environment> production (the default) or an environment name: where naro promote publishes the build (naro promote)
12152
- --env <e> production or preview (naro logs, naro insights: production by default; naro env pull, naro deploy: preview by default, or an environment name)
12525
+ --env <e> production or preview (naro logs, naro insights: production by default; naro env pull, naro deploy: preview by default, or an environment name);
12526
+ production or an environment name for the data commands (naro db, migration, gen, backend, policy, storage, realtime: production by default)
12153
12527
  --branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
12154
12528
  branch of a project this deploy creates or updates (the deploy's own branch follows where it goes)
12155
12529
  --no-branch No branch deploys to the environment any more (naro environments set; not with --branch)
@@ -12374,10 +12748,59 @@ function routeRealtime(words) {
12374
12748
  async function refuseTo() {
12375
12749
  throw new NaroError("VALIDATION_FAILED", "--to is only for naro promote.");
12376
12750
  }
12751
+ var DATA_COMMANDS = /* @__PURE__ */ new Set([
12752
+ "db",
12753
+ "migration",
12754
+ "gen",
12755
+ "backend",
12756
+ "policy",
12757
+ "storage",
12758
+ "realtime"
12759
+ ]);
12760
+ var PROJECT_WIDE_BACKEND = /* @__PURE__ */ new Set(["enable", "provision", "disable"]);
12761
+ function dataEnvRefusal(parsed) {
12762
+ if (parsed.flags.env === void 0 || !DATA_COMMANDS.has(parsed.command ?? ""))
12763
+ return null;
12764
+ const sub = parsed.positionals[0];
12765
+ if (parsed.command === "backend" && sub !== void 0 && PROJECT_WIDE_BACKEND.has(sub) && parsed.flags.env !== "production")
12766
+ return new NaroError(
12767
+ "VALIDATION_FAILED",
12768
+ DATA_ENV_MESSAGES.projectWide(`naro backend ${sub}`)
12769
+ );
12770
+ try {
12771
+ dataEnvironment(parsed.flags);
12772
+ return null;
12773
+ } catch (error) {
12774
+ return error;
12775
+ }
12776
+ }
12777
+ function guidedError(error, parsed) {
12778
+ if (!isNaroError(error) || !DATA_COMMANDS.has(parsed.command ?? ""))
12779
+ return error;
12780
+ if (dataEnvRefusal(parsed) !== null) return error;
12781
+ let environment;
12782
+ try {
12783
+ environment = dataEnvironment(parsed.flags);
12784
+ } catch {
12785
+ return error;
12786
+ }
12787
+ if (environment === null) return error;
12788
+ const usage = error.message.indexOf("Usage: ");
12789
+ error.message = usage === -1 ? forEnvironment(error.message, environment) : `${forEnvironment(error.message.slice(0, usage), environment)}${error.message.slice(usage)}`;
12790
+ return error;
12791
+ }
12377
12792
  function route(parsed) {
12378
12793
  const routed = routeCommand(parsed);
12379
12794
  if (routed && parsed.flags.to !== void 0 && parsed.command !== "promote" && parsed.command !== "rollback")
12380
12795
  return { handler: refuseTo, positionals: routed.positionals };
12796
+ const refusal = routed ? dataEnvRefusal(parsed) : null;
12797
+ if (routed && refusal)
12798
+ return {
12799
+ handler: async () => {
12800
+ throw refusal;
12801
+ },
12802
+ positionals: routed.positionals
12803
+ };
12381
12804
  return routed;
12382
12805
  }
12383
12806
  function routeCommand(parsed) {
@@ -12499,6 +12922,11 @@ function routeCommand(parsed) {
12499
12922
  handler: runEnvironmentsDelete,
12500
12923
  positionals: positionals.slice(1)
12501
12924
  };
12925
+ case "provision":
12926
+ return {
12927
+ handler: runEnvironmentsProvision,
12928
+ positionals: positionals.slice(1)
12929
+ };
12502
12930
  default:
12503
12931
  return null;
12504
12932
  }
@@ -12725,7 +13153,7 @@ async function main(argv = process.argv.slice(2)) {
12725
13153
  try {
12726
13154
  await routed.handler(context);
12727
13155
  } catch (error) {
12728
- context.output.fail(error);
13156
+ context.output.fail(guidedError(error, parsed));
12729
13157
  }
12730
13158
  }
12731
13159
  void main();