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/README.md +53 -2
- package/dist/{chunk-FFPOVA7I.js → chunk-MBSO3K6L.js} +566 -40
- package/dist/deploy.js +1 -1
- package/dist/index.js +603 -175
- package/package.json +4 -4
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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
|
-
[
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 \`
|
|
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
|
|
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
|
|
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
|
|
1450
|
-
return `Saved, but not live yet: updating the Naro worker stopped at the ${step} step${reason} The worker keeps its previous settings until
|
|
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
|
|
1553
|
+
const client = await dataClient(context);
|
|
1499
1554
|
const projectId = await context.resolveProjectId();
|
|
1500
1555
|
const auth = await client.backend.auth(projectId);
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
-
|
|
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))
|
|
1544
|
-
|
|
1545
|
-
|
|
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
|
-
|
|
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(
|
|
1553
|
-
|
|
1554
|
-
...after ? after(auth) : [],
|
|
1555
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
2362
|
+
const client = await dataClient(context);
|
|
2276
2363
|
const projectId = await context.resolveProjectId();
|
|
2277
2364
|
if (output) makeOutputDirectory(output.path, output.shown);
|
|
2278
|
-
context.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2891
|
-
...bookmark === null ? [] : [
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
5921
|
+
const client = await dataClient(context);
|
|
5773
5922
|
const projectId = await context.resolveProjectId();
|
|
5774
|
-
const
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
6178
|
+
const client = await dataClient(context);
|
|
6014
6179
|
const projectId = await context.resolveProjectId();
|
|
6015
|
-
const
|
|
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
|
-
...
|
|
6019
|
-
|
|
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
|
|
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(
|
|
6034
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11566
|
+
const client = await dataClient(context);
|
|
11211
11567
|
const projectId = await context.resolveProjectId();
|
|
11212
11568
|
const backend = await client.backend.info(projectId);
|
|
11213
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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();
|