naro-cli 0.21.0 → 0.23.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 +86 -2
- package/dist/args.d.ts +238 -0
- package/dist/{chunk-FFPOVA7I.js → chunk-ALK4VTL2.js} +2743 -392
- package/dist/commands/confirm.d.ts +12 -0
- package/dist/commands/db.d.ts +32 -0
- package/dist/commands/migration.d.ts +78 -0
- package/dist/context.d.ts +21 -0
- package/dist/data-environment.d.ts +36 -0
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.js +5 -1
- package/dist/functions/deploy.d.ts +70 -0
- package/dist/functions/retry.d.ts +16 -0
- package/dist/index.js +2256 -853
- package/dist/version.d.ts +6 -0
- package/package.json +11 -5
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
ANALYTICS_FILTER_KEYS,
|
|
4
4
|
BACKEND_STEPS,
|
|
5
5
|
BOOKMARK_FORMAT_MESSAGE,
|
|
6
|
+
BUNDLE_MESSAGES,
|
|
7
|
+
DATA_ENV_MESSAGES,
|
|
6
8
|
INDEX_SQL,
|
|
7
9
|
MAX_SINGLE_UPLOAD_BYTES,
|
|
8
10
|
NAMED_POLICY_SQL,
|
|
@@ -19,26 +21,44 @@ import {
|
|
|
19
21
|
SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
|
|
20
22
|
STORAGE_OBJECTS_TABLE,
|
|
21
23
|
STORAGE_SUBQUERIES_SQL,
|
|
24
|
+
UNSHOWN_WARNING_LINE,
|
|
25
|
+
appliedMigrations,
|
|
26
|
+
applyMigrationFiles,
|
|
27
|
+
assertFunctionsNode,
|
|
22
28
|
buildResetPlan,
|
|
29
|
+
cannotAsk,
|
|
23
30
|
cellText,
|
|
24
31
|
checkObjectName,
|
|
25
32
|
clearSession,
|
|
26
33
|
commentText,
|
|
27
|
-
|
|
34
|
+
confirmOrAbort,
|
|
28
35
|
controlPlaneUrl,
|
|
29
36
|
countLintedTables,
|
|
30
37
|
createAuthClient,
|
|
38
|
+
createPrompter,
|
|
39
|
+
dataClient,
|
|
40
|
+
dataEnvironment,
|
|
31
41
|
dataError,
|
|
42
|
+
databaseName,
|
|
43
|
+
defaultSleep,
|
|
32
44
|
deployDirectory,
|
|
45
|
+
deployFunctions,
|
|
33
46
|
detectGitInfo,
|
|
47
|
+
enableBackendCommand,
|
|
34
48
|
ensureGitignoreHasNaro,
|
|
49
|
+
envFlag,
|
|
50
|
+
forEnvironment,
|
|
35
51
|
formatRedirectEntry,
|
|
52
|
+
functionsFolder,
|
|
36
53
|
generateTypes,
|
|
37
54
|
indexesFromRows,
|
|
38
55
|
isBookmark,
|
|
39
56
|
isEnvTargetName,
|
|
40
57
|
isExpired,
|
|
58
|
+
isFunctionName,
|
|
59
|
+
isInteractive,
|
|
41
60
|
isNaroError,
|
|
61
|
+
isPrintable,
|
|
42
62
|
isReservedEnvTarget,
|
|
43
63
|
isStalledBackend,
|
|
44
64
|
isTerminalStatus,
|
|
@@ -49,19 +69,22 @@ import {
|
|
|
49
69
|
login,
|
|
50
70
|
maskEnvValue,
|
|
51
71
|
migrationNameMismatch,
|
|
72
|
+
migrationsDir,
|
|
52
73
|
migrationsPendingError,
|
|
53
74
|
nextMigrationFileName,
|
|
54
75
|
openBrowser,
|
|
55
76
|
parseCacheControl,
|
|
56
77
|
parseDotenvDetailed,
|
|
57
78
|
parseGitHubRemote,
|
|
79
|
+
parseJsonc,
|
|
58
80
|
parsePolicyNode,
|
|
59
81
|
parsePolicyStatement,
|
|
60
82
|
parseRedirectEntry,
|
|
61
83
|
pendingMigrationNames,
|
|
62
|
-
planPolicyMigration,
|
|
63
84
|
policiesForTypes,
|
|
64
85
|
policiesLostByReset,
|
|
86
|
+
policyProblems,
|
|
87
|
+
printable,
|
|
65
88
|
projectPath,
|
|
66
89
|
promoteTargetUnsupported,
|
|
67
90
|
promptForMissingEnv,
|
|
@@ -69,7 +92,9 @@ import {
|
|
|
69
92
|
quoteIdent,
|
|
70
93
|
quotePolicyName,
|
|
71
94
|
readProjectLink,
|
|
95
|
+
readStdin,
|
|
72
96
|
refreshStoredSession,
|
|
97
|
+
relativeToCwd,
|
|
73
98
|
removeProjectLink,
|
|
74
99
|
renderRows,
|
|
75
100
|
resetFileRefusedMessage,
|
|
@@ -77,14 +102,17 @@ import {
|
|
|
77
102
|
resetPoliciesWithTablesMessage,
|
|
78
103
|
resetStorageOutdatedMessage,
|
|
79
104
|
resolveAuth,
|
|
80
|
-
|
|
105
|
+
retryAfterSeconds,
|
|
106
|
+
runMigrationList,
|
|
107
|
+
runMigrationNew,
|
|
108
|
+
runMigrationRepair,
|
|
109
|
+
runMigrationUp,
|
|
81
110
|
saveSession,
|
|
82
111
|
schemaMigrationTitle,
|
|
83
112
|
serializeDotenv,
|
|
84
113
|
slugify,
|
|
85
114
|
snapshotFromRows,
|
|
86
115
|
splitPolicyStatements,
|
|
87
|
-
splitStatements,
|
|
88
116
|
storageRefusalOfFiles,
|
|
89
117
|
storedPolicySql,
|
|
90
118
|
summarizeStatement,
|
|
@@ -96,8 +124,11 @@ import {
|
|
|
96
124
|
unreadablePolicyComment,
|
|
97
125
|
usageMonthOf,
|
|
98
126
|
userForAccessToken,
|
|
127
|
+
version,
|
|
128
|
+
warningLines,
|
|
129
|
+
withFunctionRetries,
|
|
99
130
|
writeProjectLink
|
|
100
|
-
} from "./chunk-
|
|
131
|
+
} from "./chunk-ALK4VTL2.js";
|
|
101
132
|
import "./chunk-K3NQKI34.js";
|
|
102
133
|
|
|
103
134
|
// src/args.ts
|
|
@@ -137,7 +168,11 @@ var BOOLEAN_FLAGS = {
|
|
|
137
168
|
"--recursive": "recursive",
|
|
138
169
|
"-r": "recursive",
|
|
139
170
|
"--upsert": "upsert",
|
|
140
|
-
"--no-branch": "noBranch"
|
|
171
|
+
"--no-branch": "noBranch",
|
|
172
|
+
"--no-verify-jwt": "noVerifyJwt",
|
|
173
|
+
"--skip-check": "skipCheck",
|
|
174
|
+
"--no-sync": "noSync",
|
|
175
|
+
"--registry": "registry"
|
|
141
176
|
};
|
|
142
177
|
var REPEATED_FLAGS = {
|
|
143
178
|
"--column": "columns"
|
|
@@ -197,7 +232,8 @@ var STRING_FLAGS = {
|
|
|
197
232
|
"--copy-env": "copyEnv",
|
|
198
233
|
"--database": "database",
|
|
199
234
|
"--protection": "protection",
|
|
200
|
-
"--to": "to"
|
|
235
|
+
"--to": "to",
|
|
236
|
+
"--import-map": "importMap"
|
|
201
237
|
};
|
|
202
238
|
var SECRET_FLAGS = {
|
|
203
239
|
"--api-key": "--api-key-stdin",
|
|
@@ -212,6 +248,13 @@ function secretFlagProblem(unknown) {
|
|
|
212
248
|
}
|
|
213
249
|
return null;
|
|
214
250
|
}
|
|
251
|
+
var SECRETS_ARGV_REFUSAL = "Secrets are read from stdin or a file, never from the command line: pipe the value into naro functions secrets set <NAME>, or put it in a file for naro functions secrets import <file>.";
|
|
252
|
+
function unknownOptionProblem(parsed) {
|
|
253
|
+
const [area, action] = parsed.positionals;
|
|
254
|
+
if (parsed.command === "functions" && area === "secrets" && action === "set")
|
|
255
|
+
return SECRETS_ARGV_REFUSAL;
|
|
256
|
+
return secretFlagProblem(parsed.unknown) ?? `Unknown option: ${parsed.unknown.join(", ")}. See \`naro help\`.`;
|
|
257
|
+
}
|
|
215
258
|
function listFlag(value) {
|
|
216
259
|
if (value === void 0) return [];
|
|
217
260
|
return value.split(",").map((piece) => piece.trim()).filter((piece) => piece.length > 0);
|
|
@@ -371,123 +414,6 @@ var Output = class {
|
|
|
371
414
|
}
|
|
372
415
|
};
|
|
373
416
|
|
|
374
|
-
// src/prompt.ts
|
|
375
|
-
import { createInterface } from "readline";
|
|
376
|
-
import { Writable } from "stream";
|
|
377
|
-
function confirmHint(defaultYes) {
|
|
378
|
-
return defaultYes ? "(Y/n)" : "(y/N)";
|
|
379
|
-
}
|
|
380
|
-
function readConfirmAnswer(answer, defaultYes) {
|
|
381
|
-
const normalized = answer.trim().toLowerCase();
|
|
382
|
-
if (normalized === "") return defaultYes;
|
|
383
|
-
if (normalized === "y" || normalized === "yes") return true;
|
|
384
|
-
if (!defaultYes) return false;
|
|
385
|
-
if (normalized === "n" || normalized === "no") return false;
|
|
386
|
-
return null;
|
|
387
|
-
}
|
|
388
|
-
function isInteractive() {
|
|
389
|
-
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
390
|
-
}
|
|
391
|
-
async function readStdin() {
|
|
392
|
-
let data = "";
|
|
393
|
-
for await (const chunk2 of process.stdin) data += String(chunk2);
|
|
394
|
-
return data.replace(/\r?\n$/, "");
|
|
395
|
-
}
|
|
396
|
-
var TtyPrompter = class {
|
|
397
|
-
async ask(question) {
|
|
398
|
-
const rl = createInterface({
|
|
399
|
-
input: process.stdin,
|
|
400
|
-
output: process.stdout
|
|
401
|
-
});
|
|
402
|
-
try {
|
|
403
|
-
return await new Promise(
|
|
404
|
-
(resolve7) => rl.question(question, resolve7)
|
|
405
|
-
);
|
|
406
|
-
} finally {
|
|
407
|
-
rl.close();
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
async askHidden(question) {
|
|
411
|
-
let muted = false;
|
|
412
|
-
const output = new Writable({
|
|
413
|
-
write(chunk2, _enc, done) {
|
|
414
|
-
if (!muted) process.stdout.write(chunk2);
|
|
415
|
-
done();
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
const rl = createInterface({
|
|
419
|
-
input: process.stdin,
|
|
420
|
-
output,
|
|
421
|
-
terminal: true
|
|
422
|
-
});
|
|
423
|
-
try {
|
|
424
|
-
const answer = new Promise(
|
|
425
|
-
(resolve7) => rl.question(question, resolve7)
|
|
426
|
-
);
|
|
427
|
-
muted = true;
|
|
428
|
-
const value = await answer;
|
|
429
|
-
process.stdout.write("\n");
|
|
430
|
-
return value;
|
|
431
|
-
} finally {
|
|
432
|
-
rl.close();
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
async confirm(question, options = {}) {
|
|
436
|
-
const defaultYes = options.defaultYes ?? false;
|
|
437
|
-
for (; ; ) {
|
|
438
|
-
const read = readConfirmAnswer(
|
|
439
|
-
await this.ask(`${question} ${confirmHint(defaultYes)} `),
|
|
440
|
-
defaultYes
|
|
441
|
-
);
|
|
442
|
-
if (read !== null) return read;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
async select(question, options) {
|
|
446
|
-
process.stdout.write(`${question}
|
|
447
|
-
`);
|
|
448
|
-
options.forEach((option, index) => {
|
|
449
|
-
process.stdout.write(` ${String(index + 1)}. ${option}
|
|
450
|
-
`);
|
|
451
|
-
});
|
|
452
|
-
for (; ; ) {
|
|
453
|
-
const answer = Number.parseInt(
|
|
454
|
-
(await this.ask(`Choose 1-${String(options.length)}: `)).trim(),
|
|
455
|
-
10
|
|
456
|
-
);
|
|
457
|
-
if (Number.isInteger(answer) && answer >= 1 && answer <= options.length)
|
|
458
|
-
return answer - 1;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
};
|
|
462
|
-
var NonInteractivePrompter = class {
|
|
463
|
-
constructor(assumeYes) {
|
|
464
|
-
this.assumeYes = assumeYes;
|
|
465
|
-
}
|
|
466
|
-
refuse(question) {
|
|
467
|
-
throw new NaroError(
|
|
468
|
-
"VALIDATION_FAILED",
|
|
469
|
-
`Interactive input required (${question.trim()}). Pass the value via flags/stdin or run without --json in a terminal.`
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
async ask(question) {
|
|
473
|
-
this.refuse(question);
|
|
474
|
-
}
|
|
475
|
-
async askHidden(question) {
|
|
476
|
-
this.refuse(question);
|
|
477
|
-
}
|
|
478
|
-
async confirm() {
|
|
479
|
-
return this.assumeYes;
|
|
480
|
-
}
|
|
481
|
-
async select(question) {
|
|
482
|
-
this.refuse(question);
|
|
483
|
-
}
|
|
484
|
-
};
|
|
485
|
-
function createPrompter(options) {
|
|
486
|
-
if (options.yes || options.json || !isInteractive())
|
|
487
|
-
return new NonInteractivePrompter(options.yes);
|
|
488
|
-
return new TtyPrompter();
|
|
489
|
-
}
|
|
490
|
-
|
|
491
417
|
// src/context.ts
|
|
492
418
|
function createContext(flags, positionals = []) {
|
|
493
419
|
const cwd = flags.cwd ?? process.cwd();
|
|
@@ -521,7 +447,8 @@ function createContext(flags, positionals = []) {
|
|
|
521
447
|
const resolved = await auth();
|
|
522
448
|
resolvedClient = new NaroClient({
|
|
523
449
|
baseUrl: controlPlaneUrl(),
|
|
524
|
-
getAccessToken: () => resolved.getAccessToken()
|
|
450
|
+
getAccessToken: () => resolved.getAccessToken(),
|
|
451
|
+
clientName: `naro-cli/${version()}`
|
|
525
452
|
});
|
|
526
453
|
return resolvedClient;
|
|
527
454
|
},
|
|
@@ -538,264 +465,6 @@ function createContext(flags, positionals = []) {
|
|
|
538
465
|
};
|
|
539
466
|
}
|
|
540
467
|
|
|
541
|
-
// src/router.ts
|
|
542
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
543
|
-
import { dirname as dirname4, join as join6 } from "path";
|
|
544
|
-
import { fileURLToPath } from "url";
|
|
545
|
-
|
|
546
|
-
// src/commands/migration.ts
|
|
547
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
548
|
-
import { join, relative } from "path";
|
|
549
|
-
|
|
550
|
-
// src/commands/confirm.ts
|
|
551
|
-
function cannotAsk(context) {
|
|
552
|
-
return context.flags.json || !isInteractive();
|
|
553
|
-
}
|
|
554
|
-
async function confirmOrAbort(context, question) {
|
|
555
|
-
if (context.flags.yes) return;
|
|
556
|
-
if (cannotAsk(context)) {
|
|
557
|
-
throw new NaroError(
|
|
558
|
-
"CONFIRMATION_REQUIRED",
|
|
559
|
-
`${question} Re-run with --yes to confirm.`
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
const confirmed = await context.prompter.confirm(question);
|
|
563
|
-
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// src/commands/migration.ts
|
|
567
|
-
var processStdin = {
|
|
568
|
-
stdinIsTty: () => process.stdin.isTTY === true,
|
|
569
|
-
readStdin
|
|
570
|
-
};
|
|
571
|
-
function migrationsDir(context) {
|
|
572
|
-
return resolveMigrationsDir({
|
|
573
|
-
baseDir: context.cwd,
|
|
574
|
-
rootDirectory: context.flags.root,
|
|
575
|
-
dir: context.flags.dir
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
function relativeToCwd(context, path) {
|
|
579
|
-
const rel = relative(context.cwd, path);
|
|
580
|
-
return rel === "" ? "." : rel;
|
|
581
|
-
}
|
|
582
|
-
async function appliedMigrations(client, projectId) {
|
|
583
|
-
try {
|
|
584
|
-
return await client.db.migrations(projectId);
|
|
585
|
-
} catch (error) {
|
|
586
|
-
if (isNaroError(error) && error.code === "DATABASE_NOT_READY") return [];
|
|
587
|
-
throw error;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
var UNPRINTABLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
591
|
-
function printable(text) {
|
|
592
|
-
return text.replace(UNPRINTABLE, "?");
|
|
593
|
-
}
|
|
594
|
-
function isPrintable(text) {
|
|
595
|
-
return printable(text) === text;
|
|
596
|
-
}
|
|
597
|
-
var UNSHOWN_WARNING_LINE = "(a line of this warning has characters the terminal cannot show; run with --json to see it exactly)";
|
|
598
|
-
function warningLines(message) {
|
|
599
|
-
return message.split("\n").map(
|
|
600
|
-
(line, index) => index === 0 ? `\u26A0 ${printable(line)}` : ` ${isPrintable(line) ? line : UNSHOWN_WARNING_LINE}`
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
function policyProblems(dir, names) {
|
|
604
|
-
return names.flatMap((name) => {
|
|
605
|
-
const split = splitStatements(readFileSync(join(dir, name), "utf8"));
|
|
606
|
-
if (!split.ok) return [];
|
|
607
|
-
const planned = planPolicyMigration(
|
|
608
|
-
split.statements.map((statement) => statement.text)
|
|
609
|
-
);
|
|
610
|
-
return planned.ok ? [] : [{ migration: name, message: planned.message }];
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
function warningLine(warning) {
|
|
614
|
-
if (warning === "out-of-order")
|
|
615
|
-
return " \u26A0 older than an applied migration \u2014 db push will still apply it";
|
|
616
|
-
if (warning === "ledger-name-missing-extension")
|
|
617
|
-
return " \u26A0 recorded without .sql in the ledger";
|
|
618
|
-
return null;
|
|
619
|
-
}
|
|
620
|
-
async function runMigrationNew(context, deps = processStdin) {
|
|
621
|
-
const [name] = context.positionalOrThrow(
|
|
622
|
-
1,
|
|
623
|
-
"Usage: naro migration new <name>"
|
|
624
|
-
);
|
|
625
|
-
const dir = migrationsDir(context);
|
|
626
|
-
const fileName = nextMigrationFileName(
|
|
627
|
-
listMigrationFiles(dir),
|
|
628
|
-
name
|
|
629
|
-
);
|
|
630
|
-
mkdirSync(dir, { recursive: true });
|
|
631
|
-
const piped = !deps.stdinIsTty() ? await deps.readStdin() : "";
|
|
632
|
-
const fromStdin = piped.length > 0;
|
|
633
|
-
const body = fromStdin ? piped.endsWith("\n") ? piped : `${piped}
|
|
634
|
-
` : `-- Migration: ${fileName}
|
|
635
|
-
`;
|
|
636
|
-
const path = join(dir, fileName);
|
|
637
|
-
writeFileSync(path, body, { flag: "wx" });
|
|
638
|
-
const relPath = relativeToCwd(context, path);
|
|
639
|
-
context.output.result({ file: fileName, path: relPath, fromStdin }, [
|
|
640
|
-
`\u2705 Created ${relPath}`
|
|
641
|
-
]);
|
|
642
|
-
}
|
|
643
|
-
async function runMigrationList(context) {
|
|
644
|
-
const client = await context.client();
|
|
645
|
-
const projectId = await context.resolveProjectId();
|
|
646
|
-
const dir = migrationsDir(context);
|
|
647
|
-
const local = listMigrationFiles(dir);
|
|
648
|
-
const ledger = await appliedMigrations(client, projectId);
|
|
649
|
-
const migrations = compareMigrations(local, ledger);
|
|
650
|
-
const relDir = relativeToCwd(context, dir);
|
|
651
|
-
const lines = migrations.length === 0 ? ["No migrations yet. Create one with `naro migration new <name>`."] : [
|
|
652
|
-
`Migrations in ${relDir} (${String(migrations.length)}):`,
|
|
653
|
-
...migrations.flatMap((row) => {
|
|
654
|
-
const line = ` ${row.status.padEnd(12)}${printable(row.name)}${row.appliedAt ? ` ${printable(row.appliedAt)}` : ""}`;
|
|
655
|
-
const warning = warningLine(row.warning);
|
|
656
|
-
return warning ? [line, warning] : [line];
|
|
657
|
-
})
|
|
658
|
-
];
|
|
659
|
-
context.output.result({ projectId, dir: relDir, migrations }, lines);
|
|
660
|
-
}
|
|
661
|
-
async function applyMigrationFiles(context, client, projectId, dir, names) {
|
|
662
|
-
const applied = [];
|
|
663
|
-
const warnings = [];
|
|
664
|
-
for (const name of names) {
|
|
665
|
-
const sql = readFileSync(join(dir, name), "utf8");
|
|
666
|
-
let answer;
|
|
667
|
-
try {
|
|
668
|
-
answer = await client.db.applyMigration(projectId, { name, sql });
|
|
669
|
-
} catch (error) {
|
|
670
|
-
if (!isNaroError(error)) throw error;
|
|
671
|
-
for (const message of serverWarnings(error.details))
|
|
672
|
-
for (const line of warningLines(message)) context.output.warn(line);
|
|
673
|
-
const shown = context.output.json ? name : printable(name);
|
|
674
|
-
throw new NaroError(error.code, `${shown}: ${error.message}`, {
|
|
675
|
-
...error.status === void 0 ? {} : { status: error.status },
|
|
676
|
-
details: {
|
|
677
|
-
...error.details,
|
|
678
|
-
migration: name,
|
|
679
|
-
applied,
|
|
680
|
-
appliedWarnings: warnings
|
|
681
|
-
},
|
|
682
|
-
cause: error
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
context.output.log(`\u2713 ${printable(name)}`);
|
|
686
|
-
applied.push(name);
|
|
687
|
-
for (const message of answer.warnings) {
|
|
688
|
-
warnings.push({ migration: name, message });
|
|
689
|
-
for (const line of warningLines(message)) context.output.warn(line);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
return { applied, warnings };
|
|
693
|
-
}
|
|
694
|
-
function serverWarnings(details) {
|
|
695
|
-
const warnings = details?.warnings;
|
|
696
|
-
return Array.isArray(warnings) ? warnings.filter(
|
|
697
|
-
(warning) => typeof warning === "string"
|
|
698
|
-
) : [];
|
|
699
|
-
}
|
|
700
|
-
var MIGRATION_NAME_RE = /^[A-Za-z0-9_.-]{1,200}\.sql$/;
|
|
701
|
-
var LEDGER_DDL = "CREATE TABLE IF NOT EXISTS d1_migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)";
|
|
702
|
-
var LEDGER_INSERT = "INSERT INTO d1_migrations (name) SELECT ? WHERE NOT EXISTS (SELECT 1 FROM d1_migrations WHERE name = ?)";
|
|
703
|
-
var LEDGER_DELETE = "DELETE FROM d1_migrations WHERE name IN (?, ?)";
|
|
704
|
-
function withoutSuffix(key) {
|
|
705
|
-
return key.slice(0, -".sql".length);
|
|
706
|
-
}
|
|
707
|
-
async function runMigrationRepair(context) {
|
|
708
|
-
const [name] = context.positionalOrThrow(
|
|
709
|
-
1,
|
|
710
|
-
"Usage: naro migration repair <name> --status applied|reverted"
|
|
711
|
-
);
|
|
712
|
-
const key = name;
|
|
713
|
-
if (!MIGRATION_NAME_RE.test(key))
|
|
714
|
-
throw new NaroError(
|
|
715
|
-
"VALIDATION_FAILED",
|
|
716
|
-
"name must end with .sql and contain only letters, digits, _ . -"
|
|
717
|
-
);
|
|
718
|
-
const status = context.flags.status;
|
|
719
|
-
if (status !== "applied" && status !== "reverted")
|
|
720
|
-
throw new NaroError(
|
|
721
|
-
"VALIDATION_FAILED",
|
|
722
|
-
"Pass --status applied or --status reverted."
|
|
723
|
-
);
|
|
724
|
-
const client = await context.client();
|
|
725
|
-
const projectId = await context.resolveProjectId();
|
|
726
|
-
const ledger = await appliedMigrations(client, projectId);
|
|
727
|
-
const recorded = ledger.some(
|
|
728
|
-
(row) => row.name === key || row.name === withoutSuffix(key)
|
|
729
|
-
);
|
|
730
|
-
const nothingToDo = status === "applied" ? `${key} was already applied; nothing to do.` : `${key} was not in the ledger; nothing to do.`;
|
|
731
|
-
if (status === "applied" ? recorded : !recorded) {
|
|
732
|
-
context.output.warn(nothingToDo);
|
|
733
|
-
context.output.result({ name: key, status, changed: false });
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
await confirmOrAbort(
|
|
737
|
-
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.`
|
|
739
|
-
);
|
|
740
|
-
if (status === "applied" && ledger.length === 0)
|
|
741
|
-
await client.db.query(projectId, { sql: LEDGER_DDL });
|
|
742
|
-
const answer = await client.db.query(projectId, {
|
|
743
|
-
sql: status === "applied" ? LEDGER_INSERT : LEDGER_DELETE,
|
|
744
|
-
params: status === "applied" ? [key, key] : [key, withoutSuffix(key)]
|
|
745
|
-
});
|
|
746
|
-
const changed = (answer.statements[0]?.meta.changes ?? 0) > 0;
|
|
747
|
-
if (!changed) {
|
|
748
|
-
context.output.warn(nothingToDo);
|
|
749
|
-
context.output.result({ name: key, status, changed: false });
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
context.output.result({ name: key, status, changed: true }, [
|
|
753
|
-
status === "applied" ? `\u2713 Marked ${key} as applied.` : `\u2713 Removed ${key} from the ledger.`
|
|
754
|
-
]);
|
|
755
|
-
}
|
|
756
|
-
async function runMigrationUp(context) {
|
|
757
|
-
const client = await context.client();
|
|
758
|
-
const projectId = await context.resolveProjectId();
|
|
759
|
-
const dir = migrationsDir(context);
|
|
760
|
-
const local = listMigrationFiles(dir);
|
|
761
|
-
const ledger = await appliedMigrations(client, projectId);
|
|
762
|
-
const pending = compareMigrations(local, ledger).filter((row) => row.status === "pending").map((row) => row.name);
|
|
763
|
-
if (pending.length === 0) {
|
|
764
|
-
context.output.result({ projectId, applied: [], pending: [] }, [
|
|
765
|
-
"No pending migrations."
|
|
766
|
-
]);
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
if (context.flags.dryRun) {
|
|
770
|
-
const problems = policyProblems(dir, pending);
|
|
771
|
-
context.output.result({ projectId, dryRun: true, pending, problems }, [
|
|
772
|
-
`Pending migrations (${String(pending.length)}):`,
|
|
773
|
-
...pending.map((name) => ` ${printable(name)}`),
|
|
774
|
-
...problems.map(
|
|
775
|
-
(problem) => `${printable(problem.migration)}: ${printable(problem.message)}`
|
|
776
|
-
)
|
|
777
|
-
]);
|
|
778
|
-
if (problems.length > 0) process.exitCode = 1;
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
context.output.log(`Pending migrations (${String(pending.length)}):`);
|
|
782
|
-
for (const name of pending) context.output.log(` ${printable(name)}`);
|
|
783
|
-
await confirmOrAbort(
|
|
784
|
-
context,
|
|
785
|
-
`Apply ${String(pending.length)} migration(s) to the project database? Each file is applied all-or-nothing; a failure stops the run.`
|
|
786
|
-
);
|
|
787
|
-
const { applied, warnings } = await applyMigrationFiles(
|
|
788
|
-
context,
|
|
789
|
-
client,
|
|
790
|
-
projectId,
|
|
791
|
-
dir,
|
|
792
|
-
pending
|
|
793
|
-
);
|
|
794
|
-
context.output.result({ projectId, applied, pending: [], warnings }, [
|
|
795
|
-
`\u2705 Applied ${String(applied.length)} migration(s).`
|
|
796
|
-
]);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
468
|
// src/commands/filters.ts
|
|
800
469
|
function filtersOf(flags, keys) {
|
|
801
470
|
const filters = {};
|
|
@@ -1180,6 +849,86 @@ async function runToken(context) {
|
|
|
1180
849
|
);
|
|
1181
850
|
}
|
|
1182
851
|
|
|
852
|
+
// src/functions/sync-loop.ts
|
|
853
|
+
var FUNCTIONS_SYNC_MAX_CALLS = 110;
|
|
854
|
+
var FUNCTIONS_SYNC_STUCK_CALLS = 3;
|
|
855
|
+
var FUNCTIONS_SYNC_BUSY_RETRIES = 3;
|
|
856
|
+
var FUNCTIONS_SYNC_BUSY_WAIT_MS = 5e3;
|
|
857
|
+
var FUNCTIONS_SYNC_RATE_RETRIES = 3;
|
|
858
|
+
function functionCount(n) {
|
|
859
|
+
return n === 1 ? "1 function" : `${n.toLocaleString("en-US")} functions`;
|
|
860
|
+
}
|
|
861
|
+
function lastPerName(failures) {
|
|
862
|
+
const byName = /* @__PURE__ */ new Map();
|
|
863
|
+
for (const failure2 of failures) {
|
|
864
|
+
byName.delete(failure2.name);
|
|
865
|
+
byName.set(failure2.name, failure2);
|
|
866
|
+
}
|
|
867
|
+
return [...byName.values()];
|
|
868
|
+
}
|
|
869
|
+
async function syncUntilDone(client, projectId, options = {}) {
|
|
870
|
+
const sleep2 = options.sleep ?? defaultSleep;
|
|
871
|
+
const synced = /* @__PURE__ */ new Set();
|
|
872
|
+
const removed = /* @__PURE__ */ new Set();
|
|
873
|
+
const state = {
|
|
874
|
+
complete: false,
|
|
875
|
+
synced: [],
|
|
876
|
+
removed: [],
|
|
877
|
+
pending: 0,
|
|
878
|
+
failures: [],
|
|
879
|
+
calls: 0
|
|
880
|
+
};
|
|
881
|
+
let busy = 0;
|
|
882
|
+
let limited = 0;
|
|
883
|
+
let stuck = 0;
|
|
884
|
+
let previous = null;
|
|
885
|
+
const body = options.registry ? { registry: true } : {};
|
|
886
|
+
while (state.calls < FUNCTIONS_SYNC_MAX_CALLS) {
|
|
887
|
+
state.calls += 1;
|
|
888
|
+
let answer;
|
|
889
|
+
try {
|
|
890
|
+
answer = await client.functions.sync(projectId, body);
|
|
891
|
+
} catch (error) {
|
|
892
|
+
if (isNaroError(error) && error.code === "FUNCTIONS_RATE_LIMITED" && limited < FUNCTIONS_SYNC_RATE_RETRIES) {
|
|
893
|
+
limited += 1;
|
|
894
|
+
const seconds = retryAfterSeconds(error);
|
|
895
|
+
options.log?.(
|
|
896
|
+
`\u23F3 Cloudflare is rate limiting function uploads; syncing again in ${String(seconds)} seconds.`
|
|
897
|
+
);
|
|
898
|
+
await sleep2(seconds * 1e3);
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
const before = previous;
|
|
904
|
+
previous = answer.pending;
|
|
905
|
+
const progress = answer.synced.length > 0 || answer.removed.length > 0 || before !== null && answer.pending < before;
|
|
906
|
+
for (const name of answer.synced) synced.add(name);
|
|
907
|
+
for (const name of answer.removed) removed.add(name);
|
|
908
|
+
state.synced = [...synced];
|
|
909
|
+
state.removed = [...removed];
|
|
910
|
+
state.pending = answer.pending;
|
|
911
|
+
state.failures = lastPerName(answer.failures);
|
|
912
|
+
options.onCall?.({ ...state });
|
|
913
|
+
if (answer.pending === 0) {
|
|
914
|
+
state.complete = true;
|
|
915
|
+
return state;
|
|
916
|
+
}
|
|
917
|
+
if (progress) {
|
|
918
|
+
busy = 0;
|
|
919
|
+
limited = 0;
|
|
920
|
+
stuck = before !== null && answer.pending >= before ? stuck + 1 : 0;
|
|
921
|
+
if (stuck >= FUNCTIONS_SYNC_STUCK_CALLS) return state;
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const onlyBusy = state.failures.length > 0 && state.failures.every((failure2) => failure2.code === "FUNCTION_BUSY");
|
|
925
|
+
if (!onlyBusy || busy >= FUNCTIONS_SYNC_BUSY_RETRIES) return state;
|
|
926
|
+
busy += 1;
|
|
927
|
+
await sleep2(FUNCTIONS_SYNC_BUSY_WAIT_MS);
|
|
928
|
+
}
|
|
929
|
+
return state;
|
|
930
|
+
}
|
|
931
|
+
|
|
1183
932
|
// src/commands/backend.ts
|
|
1184
933
|
var SERVICE_KEY_WARNING = "\u26A0 The service key skips every policy. Keep it on a server (NARO_SERVICE_KEY) and never put it in browser code.";
|
|
1185
934
|
var ROTATE_USAGE = "Usage: naro backend keys rotate --anon | --service";
|
|
@@ -1227,14 +976,20 @@ function backendLines(info, now2 = Date.now()) {
|
|
|
1227
976
|
);
|
|
1228
977
|
return lines;
|
|
1229
978
|
}
|
|
1230
|
-
|
|
979
|
+
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.";
|
|
980
|
+
function adviceLines(info, now2, environment = null) {
|
|
1231
981
|
if (!info.enabled && info.status !== "none") {
|
|
1232
982
|
if (!info.turningOff) return [OFF_ADVICE];
|
|
1233
|
-
|
|
983
|
+
if (leaseHeld(info, now2)) return [TURNING_OFF_NOW_ADVICE];
|
|
984
|
+
return [
|
|
985
|
+
environment === null ? TURNING_OFF_ADVICE : ENVIRONMENT_TURNING_OFF_ADVICE
|
|
986
|
+
];
|
|
1234
987
|
}
|
|
1235
988
|
switch (info.status) {
|
|
1236
989
|
case "none":
|
|
1237
|
-
return [
|
|
990
|
+
return [
|
|
991
|
+
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\`.`
|
|
992
|
+
];
|
|
1238
993
|
case "error":
|
|
1239
994
|
return [
|
|
1240
995
|
...info.error ? [`Error: ${printable(info.error)}`] : [],
|
|
@@ -1255,17 +1010,21 @@ function stoppedAt(info) {
|
|
|
1255
1010
|
return `stopped at ${step}${info.error ? `: ${sentence(info.error)}` : "."}`;
|
|
1256
1011
|
}
|
|
1257
1012
|
async function runBackendInfo(context) {
|
|
1258
|
-
const client = await context
|
|
1013
|
+
const client = await dataClient(context);
|
|
1259
1014
|
const projectId = await context.resolveProjectId();
|
|
1260
1015
|
const backend = await client.backend.info(projectId);
|
|
1261
1016
|
const now2 = Date.now();
|
|
1017
|
+
const environment = dataEnvironment(context.flags);
|
|
1262
1018
|
context.output.result(
|
|
1263
1019
|
{ projectId, backend, stalled: isStalledBackend(backend, now2) },
|
|
1264
|
-
[
|
|
1020
|
+
[
|
|
1021
|
+
...backendLines(backend, now2),
|
|
1022
|
+
...adviceLines(backend, now2, environment)
|
|
1023
|
+
].map((line) => forEnvironment(line, environment))
|
|
1265
1024
|
);
|
|
1266
1025
|
}
|
|
1267
1026
|
async function runBackendEnable(context) {
|
|
1268
|
-
const client = await context
|
|
1027
|
+
const client = await dataClient(context);
|
|
1269
1028
|
const projectId = await context.resolveProjectId();
|
|
1270
1029
|
let backend;
|
|
1271
1030
|
try {
|
|
@@ -1309,7 +1068,7 @@ async function runBackendEnable(context) {
|
|
|
1309
1068
|
}
|
|
1310
1069
|
async function runBackendDisable(context) {
|
|
1311
1070
|
await confirmOrAbort(context, DISABLE_QUESTION);
|
|
1312
|
-
const client = await context
|
|
1071
|
+
const client = await dataClient(context);
|
|
1313
1072
|
const projectId = await context.resolveProjectId();
|
|
1314
1073
|
const backend = await client.backend.disable(projectId);
|
|
1315
1074
|
if (backend.status === "none") {
|
|
@@ -1326,7 +1085,8 @@ async function runBackendDisable(context) {
|
|
|
1326
1085
|
]);
|
|
1327
1086
|
}
|
|
1328
1087
|
async function runBackendKeys(context) {
|
|
1329
|
-
const
|
|
1088
|
+
const environment = dataEnvironment(context.flags);
|
|
1089
|
+
const client = await dataClient(context);
|
|
1330
1090
|
const projectId = await context.resolveProjectId();
|
|
1331
1091
|
const keys = await client.backend.keys(projectId);
|
|
1332
1092
|
if (!context.flags.reveal) {
|
|
@@ -1339,7 +1099,10 @@ async function runBackendKeys(context) {
|
|
|
1339
1099
|
[
|
|
1340
1100
|
`Anon key: ${keys.anonKey}`,
|
|
1341
1101
|
`Service key: ${keys.serviceKeyPrefix}\u2026 (hidden)`,
|
|
1342
|
-
|
|
1102
|
+
forEnvironment(
|
|
1103
|
+
"`naro backend keys --reveal` prints the service key (needs database write access).",
|
|
1104
|
+
environment
|
|
1105
|
+
)
|
|
1343
1106
|
]
|
|
1344
1107
|
);
|
|
1345
1108
|
return;
|
|
@@ -1359,41 +1122,94 @@ function rotateKind(context) {
|
|
|
1359
1122
|
function keyLabel(kind) {
|
|
1360
1123
|
return kind === "anon" ? "Anon key: " : "Service key: ";
|
|
1361
1124
|
}
|
|
1362
|
-
function rotationWarning(kind, backend) {
|
|
1125
|
+
function rotationWarning(kind, backend, environment = null) {
|
|
1363
1126
|
if (backend.status === "error")
|
|
1364
|
-
return `\u26A0 The ${kind} key is replaced, but provisioning ${stoppedAt(backend)} Run \`
|
|
1127
|
+
return `\u26A0 The ${kind} key is replaced, but provisioning ${stoppedAt(backend)} Run \`${enableBackendCommand(environment)}\` to finish.`;
|
|
1365
1128
|
if (backend.status !== "ready")
|
|
1366
|
-
return `\u26A0 The ${kind} key is replaced; another provisioning run is finishing the backend. Check it with \`naro backend\`.`;
|
|
1129
|
+
return `\u26A0 The ${kind} key is replaced; another provisioning run is finishing the backend. Check it with \`naro backend${envFlag(environment)}\`.`;
|
|
1367
1130
|
return null;
|
|
1368
1131
|
}
|
|
1369
|
-
|
|
1132
|
+
var FUNCTIONS_ROTATE_NOTE = (kind) => `Functions pick up the new key within a few minutes; until then their calls with the old ${kind} key fail.`;
|
|
1133
|
+
async function liveFunctionCount(client, projectId) {
|
|
1134
|
+
try {
|
|
1135
|
+
const { functions } = await client.functions.list(projectId);
|
|
1136
|
+
return functions.filter(
|
|
1137
|
+
(fn) => fn.version !== null && fn.status !== "deleting"
|
|
1138
|
+
).length;
|
|
1139
|
+
} catch {
|
|
1140
|
+
return 0;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async function syncAfterRotation(context, client, projectId, kind, live2) {
|
|
1144
|
+
context.output.warn(
|
|
1145
|
+
`\u23F3 Replaced the ${kind} key; syncing ${functionCount(live2)}\u2026`
|
|
1146
|
+
);
|
|
1147
|
+
try {
|
|
1148
|
+
const sync = await syncUntilDone(client, projectId, {
|
|
1149
|
+
log: (line) => context.output.warn(line),
|
|
1150
|
+
onCall: (progress) => {
|
|
1151
|
+
context.output.warn(
|
|
1152
|
+
`\u23F3 Syncing functions\u2026 ${String(Math.min(progress.synced.length, live2))}/${String(live2)}`
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
if (!sync.complete)
|
|
1157
|
+
context.output.warn(
|
|
1158
|
+
`\u26A0 The ${kind} key is replaced, but ${functionCount(sync.pending)} still ${sync.pending === 1 ? "uses" : "use"} the old one. Run \`naro functions secrets sync\` to finish (the sweep also brings them up within minutes).`
|
|
1159
|
+
);
|
|
1160
|
+
return {
|
|
1161
|
+
complete: sync.complete,
|
|
1162
|
+
synced: sync.synced,
|
|
1163
|
+
pending: sync.pending
|
|
1164
|
+
};
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
const message = printable(
|
|
1167
|
+
isNaroError(error) ? error.message : String(error)
|
|
1168
|
+
);
|
|
1169
|
+
context.output.warn(
|
|
1170
|
+
`\u26A0 The ${kind} key is replaced, but the functions could not be synced now: ${message} The sweep brings them up within minutes.`
|
|
1171
|
+
);
|
|
1172
|
+
return {
|
|
1173
|
+
complete: false,
|
|
1174
|
+
synced: [],
|
|
1175
|
+
pending: live2,
|
|
1176
|
+
error: {
|
|
1177
|
+
code: isNaroError(error) ? error.code : "INTERNAL_ERROR",
|
|
1178
|
+
message
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function checkKeysSentence(kind, environment = null) {
|
|
1370
1184
|
const keys = kind === "service" ? "naro backend keys --reveal" : "naro backend keys";
|
|
1371
|
-
return `Run \`${keys}\` to see which key is live, then
|
|
1185
|
+
return `Run \`${keys}${envFlag(environment)}\` to see which key is live, then \`${enableBackendCommand(environment)}\`.`;
|
|
1372
1186
|
}
|
|
1373
|
-
function rotationHint(kind) {
|
|
1374
|
-
return `The rotation may have gone through anyway. ${checkKeysSentence(kind)}`;
|
|
1187
|
+
function rotationHint(kind, environment = null) {
|
|
1188
|
+
return `The rotation may have gone through anyway. ${checkKeysSentence(kind, environment)}`;
|
|
1375
1189
|
}
|
|
1376
1190
|
function outcomeUnknown(error) {
|
|
1377
1191
|
return error.code === "BACKEND_ROTATION_FAILED" || error.code === "INTERNAL_ERROR" || error.code === "NETWORK_ERROR" || error.status !== void 0 && error.status >= 500;
|
|
1378
1192
|
}
|
|
1379
1193
|
async function runBackendKeysRotate(context) {
|
|
1380
1194
|
const kind = rotateKind(context);
|
|
1195
|
+
const client = await dataClient(context);
|
|
1196
|
+
const environment = dataEnvironment(context.flags);
|
|
1197
|
+
const projectId = await context.resolveProjectId();
|
|
1198
|
+
const functions = environment === null ? await liveFunctionCount(client, projectId) : 0;
|
|
1381
1199
|
await confirmOrAbort(
|
|
1382
1200
|
context,
|
|
1383
|
-
`Replace the ${kind} key? Every client still sending the old one gets 401 INVALID_API_KEY as soon as the new key is live
|
|
1201
|
+
`Replace the ${kind} key? Every client still sending the old one gets 401 INVALID_API_KEY as soon as the new key is live.${functions > 0 ? ` ${FUNCTIONS_ROTATE_NOTE(kind)}` : ""}`
|
|
1384
1202
|
);
|
|
1385
|
-
const client = await context.client();
|
|
1386
|
-
const projectId = await context.resolveProjectId();
|
|
1387
1203
|
let rotated;
|
|
1388
1204
|
try {
|
|
1389
1205
|
rotated = await client.backend.rotateKey(projectId, kind);
|
|
1390
1206
|
} catch (error) {
|
|
1391
1207
|
if (!isNaroError(error)) throw error;
|
|
1392
|
-
const hint = rotationHint(kind);
|
|
1208
|
+
const hint = rotationHint(kind, environment);
|
|
1393
1209
|
if (error.code === "TIMEOUT")
|
|
1394
1210
|
throw new NaroError(
|
|
1395
1211
|
"TIMEOUT",
|
|
1396
|
-
`Rotating the key did not answer within 2 minutes, so whether it happened is not known. ${checkKeysSentence(kind)}`,
|
|
1212
|
+
`Rotating the key did not answer within 2 minutes, so whether it happened is not known. ${checkKeysSentence(kind, environment)}`,
|
|
1397
1213
|
{ details: { hint }, cause: error }
|
|
1398
1214
|
);
|
|
1399
1215
|
if (!outcomeUnknown(error)) throw error;
|
|
@@ -1405,21 +1221,32 @@ async function runBackendKeysRotate(context) {
|
|
|
1405
1221
|
});
|
|
1406
1222
|
}
|
|
1407
1223
|
const { backend, key } = rotated;
|
|
1408
|
-
const warning = rotationWarning(kind, backend);
|
|
1224
|
+
const warning = rotationWarning(kind, backend, environment);
|
|
1225
|
+
const synced = functions > 0 && !context.flags.noSync ? await syncAfterRotation(context, client, projectId, kind, functions) : null;
|
|
1409
1226
|
if (kind === "service") context.output.warn(SERVICE_KEY_WARNING);
|
|
1410
1227
|
if (warning) context.output.warn(warning);
|
|
1411
1228
|
context.output.result(
|
|
1412
|
-
{
|
|
1229
|
+
{
|
|
1230
|
+
projectId,
|
|
1231
|
+
kind,
|
|
1232
|
+
key,
|
|
1233
|
+
backend,
|
|
1234
|
+
...warning ? { warning } : {},
|
|
1235
|
+
...synced ? { functions: synced } : {}
|
|
1236
|
+
},
|
|
1413
1237
|
[
|
|
1414
1238
|
`\u2705 Replaced the ${kind} key. The old one no longer works.`,
|
|
1415
1239
|
`${keyLabel(kind)}${key}`,
|
|
1416
|
-
kind === "anon" ? "Put the new key in your app and redeploy." : "Update NARO_SERVICE_KEY wherever your servers read it."
|
|
1240
|
+
kind === "anon" ? "Put the new key in your app and redeploy." : "Update NARO_SERVICE_KEY wherever your servers read it.",
|
|
1241
|
+
...synced?.complete ? [
|
|
1242
|
+
`\u2705 Functions carry the new key: ${functionCount(synced.synced.length)} synced.`
|
|
1243
|
+
] : []
|
|
1417
1244
|
]
|
|
1418
1245
|
);
|
|
1419
1246
|
}
|
|
1420
1247
|
|
|
1421
1248
|
// src/commands/backend-auth.ts
|
|
1422
|
-
var
|
|
1249
|
+
var processStdin = {
|
|
1423
1250
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1424
1251
|
readStdin
|
|
1425
1252
|
};
|
|
@@ -1442,14 +1269,21 @@ function sentence2(text) {
|
|
|
1442
1269
|
const line = printable(text).trim();
|
|
1443
1270
|
return /[.!?…]$/.test(line) ? line : `${line}.`;
|
|
1444
1271
|
}
|
|
1445
|
-
function stoppedMessage(backend) {
|
|
1272
|
+
function stoppedMessage(backend, environment = null) {
|
|
1273
|
+
const enable = enableBackendCommand(environment);
|
|
1446
1274
|
const step = backend.step ?? "worker";
|
|
1447
1275
|
const reason = backend.error?.trim() ? `: ${sentence2(backend.error)}` : ".";
|
|
1448
1276
|
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
|
|
1277
|
+
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.`;
|
|
1278
|
+
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
1279
|
}
|
|
1452
1280
|
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.";
|
|
1281
|
+
function notAppliedWarning(environment) {
|
|
1282
|
+
return environment === null ? NOT_APPLIED_WARNING : NOT_APPLIED_WARNING.replace(
|
|
1283
|
+
"`naro backend enable`",
|
|
1284
|
+
`\`${enableBackendCommand(environment)}\``
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1453
1287
|
function authLines(auth) {
|
|
1454
1288
|
const lines = [
|
|
1455
1289
|
auth.email ? `Email: ${auth.email.provider} \u2014 ${printable(auth.email.from)} (API key ${printable(auth.email.apiKeyPrefix)}\u2026)` : "Email: not set up"
|
|
@@ -1495,14 +1329,18 @@ function adviceLines2(auth) {
|
|
|
1495
1329
|
return lines;
|
|
1496
1330
|
}
|
|
1497
1331
|
async function runBackendAuthStatus(context) {
|
|
1498
|
-
const client = await context
|
|
1332
|
+
const client = await dataClient(context);
|
|
1499
1333
|
const projectId = await context.resolveProjectId();
|
|
1500
1334
|
const auth = await client.backend.auth(projectId);
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1335
|
+
const environment = dataEnvironment(context.flags);
|
|
1336
|
+
if (auth.applied === false)
|
|
1337
|
+
context.output.warn(notAppliedWarning(environment));
|
|
1338
|
+
context.output.result(
|
|
1339
|
+
{ projectId, auth },
|
|
1340
|
+
[...authLines(auth), ...adviceLines2(auth)].map(
|
|
1341
|
+
(line) => forEnvironment(line, environment)
|
|
1342
|
+
)
|
|
1343
|
+
);
|
|
1506
1344
|
}
|
|
1507
1345
|
async function readSecret(context, input, given, flag, what, usage) {
|
|
1508
1346
|
if (!given)
|
|
@@ -1518,6 +1356,7 @@ async function readSecret(context, input, given, flag, what, usage) {
|
|
|
1518
1356
|
}
|
|
1519
1357
|
async function change(context, send2, done, after) {
|
|
1520
1358
|
const projectId = await context.resolveProjectId();
|
|
1359
|
+
const environment = dataEnvironment(context.flags);
|
|
1521
1360
|
let answer;
|
|
1522
1361
|
try {
|
|
1523
1362
|
answer = await send2(projectId);
|
|
@@ -1525,13 +1364,13 @@ async function change(context, send2, done, after) {
|
|
|
1525
1364
|
if (isNaroError(error) && error.code === "TIMEOUT")
|
|
1526
1365
|
throw new NaroError(
|
|
1527
1366
|
"TIMEOUT",
|
|
1528
|
-
|
|
1367
|
+
`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
1368
|
{ cause: error }
|
|
1530
1369
|
);
|
|
1531
1370
|
const saved = isNaroError(error) ? error.details?.auth : void 0;
|
|
1532
1371
|
if (isNaroError(error) && error.code === "BACKEND_PROVISION_FAILED" && typeof saved === "object" && saved !== null) {
|
|
1533
1372
|
for (const line of authLines(saved))
|
|
1534
|
-
context.output.log(line);
|
|
1373
|
+
context.output.log(forEnvironment(line, environment));
|
|
1535
1374
|
throw new NaroError(error.code, error.message, {
|
|
1536
1375
|
details: { projectId, ...error.details }
|
|
1537
1376
|
});
|
|
@@ -1540,22 +1379,31 @@ async function change(context, send2, done, after) {
|
|
|
1540
1379
|
}
|
|
1541
1380
|
const { auth, backend } = answer;
|
|
1542
1381
|
if (backend.status === "error") {
|
|
1543
|
-
for (const line of authLines(auth))
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1382
|
+
for (const line of authLines(auth))
|
|
1383
|
+
context.output.log(forEnvironment(line, environment));
|
|
1384
|
+
throw new NaroError(
|
|
1385
|
+
"BACKEND_PROVISION_FAILED",
|
|
1386
|
+
stoppedMessage(backend, environment),
|
|
1387
|
+
{
|
|
1388
|
+
details: { projectId, auth, backend }
|
|
1389
|
+
}
|
|
1390
|
+
);
|
|
1547
1391
|
}
|
|
1548
1392
|
if (backend.status !== "ready")
|
|
1549
1393
|
context.output.warn(
|
|
1550
|
-
|
|
1394
|
+
forEnvironment(
|
|
1395
|
+
"\u26A0 Saved; another provisioning run is finishing the backend. Check it with `naro backend`.",
|
|
1396
|
+
environment
|
|
1397
|
+
)
|
|
1551
1398
|
);
|
|
1552
|
-
context.output.result(
|
|
1553
|
-
|
|
1554
|
-
...after ? after(auth) : [],
|
|
1555
|
-
|
|
1556
|
-
|
|
1399
|
+
context.output.result(
|
|
1400
|
+
{ projectId, auth, backend },
|
|
1401
|
+
[...done, ...after ? after(auth) : [], ...authLines(auth)].map(
|
|
1402
|
+
(line) => forEnvironment(line, environment)
|
|
1403
|
+
)
|
|
1404
|
+
);
|
|
1557
1405
|
}
|
|
1558
|
-
async function runBackendAuthEmailSet(context, input =
|
|
1406
|
+
async function runBackendAuthEmailSet(context, input = processStdin) {
|
|
1559
1407
|
const { provider, from } = context.flags;
|
|
1560
1408
|
if (provider === void 0 || from === void 0)
|
|
1561
1409
|
throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.email);
|
|
@@ -1572,7 +1420,7 @@ async function runBackendAuthEmailSet(context, input = processStdin2) {
|
|
|
1572
1420
|
"Resend API key",
|
|
1573
1421
|
AUTH_USAGE.email
|
|
1574
1422
|
);
|
|
1575
|
-
const client = await context
|
|
1423
|
+
const client = await dataClient(context);
|
|
1576
1424
|
await change(
|
|
1577
1425
|
context,
|
|
1578
1426
|
(projectId) => client.backend.setAuth(projectId, {
|
|
@@ -1588,7 +1436,7 @@ async function runBackendAuthEmailRemove(context) {
|
|
|
1588
1436
|
context,
|
|
1589
1437
|
"Remove email? Confirmation and password reset emails stop at once."
|
|
1590
1438
|
);
|
|
1591
|
-
const client = await context
|
|
1439
|
+
const client = await dataClient(context);
|
|
1592
1440
|
await change(
|
|
1593
1441
|
context,
|
|
1594
1442
|
(projectId) => client.backend.removeAuth(projectId, "email"),
|
|
@@ -1601,7 +1449,7 @@ function providerOf(context, usage) {
|
|
|
1601
1449
|
throw new NaroError("VALIDATION_FAILED", usage);
|
|
1602
1450
|
return provider;
|
|
1603
1451
|
}
|
|
1604
|
-
async function runBackendAuthOAuthSet(context, input =
|
|
1452
|
+
async function runBackendAuthOAuthSet(context, input = processStdin) {
|
|
1605
1453
|
const provider = providerOf(context, AUTH_USAGE.oauth);
|
|
1606
1454
|
const clientId = context.flags.clientId;
|
|
1607
1455
|
if (clientId === void 0)
|
|
@@ -1614,7 +1462,7 @@ async function runBackendAuthOAuthSet(context, input = processStdin2) {
|
|
|
1614
1462
|
`${LABELS[provider]} client secret`,
|
|
1615
1463
|
AUTH_USAGE.oauth
|
|
1616
1464
|
);
|
|
1617
|
-
const client = await context
|
|
1465
|
+
const client = await dataClient(context);
|
|
1618
1466
|
const patch = {
|
|
1619
1467
|
oauth: { [provider]: { clientId, clientSecret } }
|
|
1620
1468
|
};
|
|
@@ -1632,7 +1480,7 @@ async function runBackendAuthOAuthRemove(context) {
|
|
|
1632
1480
|
context,
|
|
1633
1481
|
`Remove ${LABELS[provider]} sign-in? Users who signed in with ${LABELS[provider]} cannot sign in with it until it is set up again.`
|
|
1634
1482
|
);
|
|
1635
|
-
const client = await context
|
|
1483
|
+
const client = await dataClient(context);
|
|
1636
1484
|
await change(
|
|
1637
1485
|
context,
|
|
1638
1486
|
(projectId) => client.backend.removeAuth(projectId, provider),
|
|
@@ -1640,7 +1488,7 @@ async function runBackendAuthOAuthRemove(context) {
|
|
|
1640
1488
|
);
|
|
1641
1489
|
}
|
|
1642
1490
|
async function runBackendAuthRedirectsList(context) {
|
|
1643
|
-
const client = await context
|
|
1491
|
+
const client = await dataClient(context);
|
|
1644
1492
|
const projectId = await context.resolveProjectId();
|
|
1645
1493
|
const auth = await client.backend.auth(projectId);
|
|
1646
1494
|
const production = productionOrigins(auth);
|
|
@@ -1657,7 +1505,10 @@ async function runBackendAuthRedirectsList(context) {
|
|
|
1657
1505
|
),
|
|
1658
1506
|
...auth.redirectOrigins,
|
|
1659
1507
|
...auth.redirectOrigins.length === 0 ? [
|
|
1660
|
-
|
|
1508
|
+
forEnvironment(
|
|
1509
|
+
"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/.",
|
|
1510
|
+
dataEnvironment(context.flags)
|
|
1511
|
+
)
|
|
1661
1512
|
] : []
|
|
1662
1513
|
]
|
|
1663
1514
|
);
|
|
@@ -1670,7 +1521,7 @@ function originOf(context) {
|
|
|
1670
1521
|
}
|
|
1671
1522
|
async function runBackendAuthRedirectsAdd(context) {
|
|
1672
1523
|
const origin = originOf(context);
|
|
1673
|
-
const client = await context
|
|
1524
|
+
const client = await dataClient(context);
|
|
1674
1525
|
await change(
|
|
1675
1526
|
context,
|
|
1676
1527
|
(projectId) => client.backend.setAuth(projectId, {
|
|
@@ -1684,7 +1535,7 @@ function allowing(list, origin) {
|
|
|
1684
1535
|
}
|
|
1685
1536
|
async function runBackendAuthRedirectsRemove(context) {
|
|
1686
1537
|
const given = originOf(context);
|
|
1687
|
-
const client = await context
|
|
1538
|
+
const client = await dataClient(context);
|
|
1688
1539
|
const projectId = await context.resolveProjectId();
|
|
1689
1540
|
const parsed = parseRedirectEntry(given);
|
|
1690
1541
|
if (parsed) {
|
|
@@ -1730,7 +1581,7 @@ async function runBackendAuthRequireVerification(context) {
|
|
|
1730
1581
|
if (value !== "on" && value !== "off")
|
|
1731
1582
|
throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.verification);
|
|
1732
1583
|
const on = value === "on";
|
|
1733
|
-
const client = await context
|
|
1584
|
+
const client = await dataClient(context);
|
|
1734
1585
|
await change(
|
|
1735
1586
|
context,
|
|
1736
1587
|
(projectId) => client.backend.setAuth(projectId, { requireEmailVerification: on }),
|
|
@@ -1744,7 +1595,7 @@ async function runBackendAuthSignups(context) {
|
|
|
1744
1595
|
if (value !== "on" && value !== "off")
|
|
1745
1596
|
throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.signups);
|
|
1746
1597
|
const open = value === "on";
|
|
1747
|
-
const client = await context
|
|
1598
|
+
const client = await dataClient(context);
|
|
1748
1599
|
await change(
|
|
1749
1600
|
context,
|
|
1750
1601
|
(projectId) => client.backend.setAuth(projectId, { signupsEnabled: open }),
|
|
@@ -1763,7 +1614,7 @@ async function runBackendAuthPassword(context) {
|
|
|
1763
1614
|
"VALIDATION_FAILED",
|
|
1764
1615
|
`--min-length must be a whole number from ${String(MIN_LENGTH_FLOOR)} to ${String(MIN_LENGTH_CEILING)}. ${AUTH_USAGE.password}`
|
|
1765
1616
|
);
|
|
1766
|
-
const client = await context
|
|
1617
|
+
const client = await dataClient(context);
|
|
1767
1618
|
await change(
|
|
1768
1619
|
context,
|
|
1769
1620
|
(projectId) => client.backend.setAuth(projectId, { passwordMinLength: length }),
|
|
@@ -1781,7 +1632,7 @@ var USERS_USAGE = {
|
|
|
1781
1632
|
var PASSWORD_FLAG_WARNING = "\u26A0 --password puts the password in your shell history. Pipe it in with --password-stdin instead.";
|
|
1782
1633
|
var OUTDATED_MESSAGE = "This project's backend is older than user management. Run `naro backend enable` once to update this project's backend. Then run this command again.";
|
|
1783
1634
|
var PAGE_SIZE = 1e3;
|
|
1784
|
-
var
|
|
1635
|
+
var processStdin2 = {
|
|
1785
1636
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1786
1637
|
readStdin
|
|
1787
1638
|
};
|
|
@@ -1835,13 +1686,16 @@ function userTable(users) {
|
|
|
1835
1686
|
);
|
|
1836
1687
|
}
|
|
1837
1688
|
async function runBackendUsersList(context) {
|
|
1838
|
-
const client = await context
|
|
1689
|
+
const client = await dataClient(context);
|
|
1839
1690
|
const projectId = await context.resolveProjectId();
|
|
1840
1691
|
const { users, total } = await allUsers(client, projectId);
|
|
1841
1692
|
context.output.result(
|
|
1842
1693
|
{ projectId, users, total },
|
|
1843
1694
|
users.length === 0 ? [
|
|
1844
|
-
|
|
1695
|
+
forEnvironment(
|
|
1696
|
+
"No users yet. Create one with `naro backend users create --username <name> --password-stdin`.",
|
|
1697
|
+
dataEnvironment(context.flags)
|
|
1698
|
+
)
|
|
1845
1699
|
] : [
|
|
1846
1700
|
`${String(total)} ${total === 1 ? "user" : "users"}, newest first:`,
|
|
1847
1701
|
...userTable(users)
|
|
@@ -1880,12 +1734,12 @@ function userLines(user) {
|
|
|
1880
1734
|
`Created: ${printable(user.createdAt)}`
|
|
1881
1735
|
];
|
|
1882
1736
|
}
|
|
1883
|
-
async function runBackendUsersCreate(context, input =
|
|
1737
|
+
async function runBackendUsersCreate(context, input = processStdin2) {
|
|
1884
1738
|
const { username, email } = context.flags;
|
|
1885
1739
|
if (username === void 0 && email === void 0 || context.positionals.length > 0)
|
|
1886
1740
|
throw new NaroError("VALIDATION_FAILED", USERS_USAGE.create);
|
|
1887
1741
|
const password = await passwordOf(context, input);
|
|
1888
|
-
const client = await context
|
|
1742
|
+
const client = await dataClient(context);
|
|
1889
1743
|
const projectId = await context.resolveProjectId();
|
|
1890
1744
|
const body = { password };
|
|
1891
1745
|
if (username !== void 0) body.username = username;
|
|
@@ -1911,7 +1765,7 @@ async function runBackendUsersDelete(context) {
|
|
|
1911
1765
|
const [given] = context.positionals;
|
|
1912
1766
|
if (given === void 0 || context.positionals.length > 1)
|
|
1913
1767
|
throw new NaroError("VALIDATION_FAILED", USERS_USAGE.delete);
|
|
1914
|
-
const client = await context
|
|
1768
|
+
const client = await dataClient(context);
|
|
1915
1769
|
const projectId = await context.resolveProjectId();
|
|
1916
1770
|
const { users } = await allUsers(client, projectId);
|
|
1917
1771
|
const user = find(users, given);
|
|
@@ -1958,9 +1812,9 @@ async function runCancel(context) {
|
|
|
1958
1812
|
}
|
|
1959
1813
|
|
|
1960
1814
|
// src/commands/db.ts
|
|
1961
|
-
import { readFileSync
|
|
1815
|
+
import { readFileSync } from "fs";
|
|
1962
1816
|
import { resolve } from "path";
|
|
1963
|
-
var
|
|
1817
|
+
var processStdin3 = {
|
|
1964
1818
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1965
1819
|
readStdin
|
|
1966
1820
|
};
|
|
@@ -2018,7 +1872,7 @@ function usageLines(info) {
|
|
|
2018
1872
|
}
|
|
2019
1873
|
return lines;
|
|
2020
1874
|
}
|
|
2021
|
-
function describeLines(info) {
|
|
1875
|
+
function describeLines(info, environment = null) {
|
|
2022
1876
|
const lines = [
|
|
2023
1877
|
`Provider: ${info.provider}`,
|
|
2024
1878
|
`Status: ${info.status}`,
|
|
@@ -2039,17 +1893,22 @@ function describeLines(info) {
|
|
|
2039
1893
|
);
|
|
2040
1894
|
lines.push(...usageLines(info));
|
|
2041
1895
|
if (info.provider === "none")
|
|
2042
|
-
lines.push(
|
|
1896
|
+
lines.push(
|
|
1897
|
+
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.`
|
|
1898
|
+
);
|
|
2043
1899
|
return lines;
|
|
2044
1900
|
}
|
|
2045
1901
|
async function runDbInfo(context) {
|
|
2046
|
-
const client = await context
|
|
1902
|
+
const client = await dataClient(context);
|
|
2047
1903
|
const projectId = await context.resolveProjectId();
|
|
2048
1904
|
const database = await client.db.info(projectId);
|
|
2049
|
-
context.output.result(
|
|
1905
|
+
context.output.result(
|
|
1906
|
+
{ projectId, database },
|
|
1907
|
+
describeLines(database, dataEnvironment(context.flags))
|
|
1908
|
+
);
|
|
2050
1909
|
}
|
|
2051
1910
|
async function runDbEnable(context) {
|
|
2052
|
-
const client = await context
|
|
1911
|
+
const client = await dataClient(context);
|
|
2053
1912
|
const projectId = await context.resolveProjectId();
|
|
2054
1913
|
const database = await client.db.enable(projectId);
|
|
2055
1914
|
context.output.result({ projectId, database }, [
|
|
@@ -2064,7 +1923,7 @@ async function readSql(context, deps) {
|
|
|
2064
1923
|
sql = positional;
|
|
2065
1924
|
} else if (context.flags.file !== void 0) {
|
|
2066
1925
|
try {
|
|
2067
|
-
sql =
|
|
1926
|
+
sql = readFileSync(resolve(context.cwd, context.flags.file), "utf8");
|
|
2068
1927
|
} catch {
|
|
2069
1928
|
throw new NaroError(
|
|
2070
1929
|
"VALIDATION_FAILED",
|
|
@@ -2078,9 +1937,9 @@ async function readSql(context, deps) {
|
|
|
2078
1937
|
throw new NaroError("VALIDATION_FAILED", USAGE);
|
|
2079
1938
|
return sql;
|
|
2080
1939
|
}
|
|
2081
|
-
async function runDbQuery(context, deps =
|
|
1940
|
+
async function runDbQuery(context, deps = processStdin3) {
|
|
2082
1941
|
const sql = await readSql(context, deps);
|
|
2083
|
-
const client = await context
|
|
1942
|
+
const client = await dataClient(context);
|
|
2084
1943
|
const projectId = await context.resolveProjectId();
|
|
2085
1944
|
const result = await client.db.query(projectId, { sql });
|
|
2086
1945
|
const lines = [];
|
|
@@ -2097,13 +1956,13 @@ async function runDbQuery(context, deps = processStdin4) {
|
|
|
2097
1956
|
// src/commands/db-backup.ts
|
|
2098
1957
|
import {
|
|
2099
1958
|
createWriteStream,
|
|
2100
|
-
mkdirSync
|
|
1959
|
+
mkdirSync,
|
|
2101
1960
|
renameSync,
|
|
2102
1961
|
rmSync,
|
|
2103
1962
|
statSync,
|
|
2104
|
-
writeFileSync
|
|
1963
|
+
writeFileSync
|
|
2105
1964
|
} from "fs";
|
|
2106
|
-
import { basename, dirname, join
|
|
1965
|
+
import { basename, dirname, join, resolve as resolve2 } from "path";
|
|
2107
1966
|
import { Transform } from "stream";
|
|
2108
1967
|
import { pipeline } from "stream/promises";
|
|
2109
1968
|
var defaults = {
|
|
@@ -2111,18 +1970,20 @@ var defaults = {
|
|
|
2111
1970
|
stdout: process.stdout,
|
|
2112
1971
|
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
2113
1972
|
writeFile: (path, data) => {
|
|
2114
|
-
|
|
2115
|
-
|
|
1973
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1974
|
+
writeFileSync(path, data, { flag: "wx" });
|
|
2116
1975
|
}
|
|
2117
1976
|
};
|
|
2118
|
-
|
|
1977
|
+
function exportNotice(environment) {
|
|
1978
|
+
return `Exporting ${databaseName(environment)}. Cloudflare holds other queries to this database until the export finishes.`;
|
|
1979
|
+
}
|
|
2119
1980
|
function reasonOf(error) {
|
|
2120
1981
|
return error instanceof Error ? error.message : String(error);
|
|
2121
1982
|
}
|
|
2122
1983
|
function withoutUrl(text, url) {
|
|
2123
1984
|
return text.split(url).join("the signed URL").replace(/\S*X-Amz-Signature=\S*/gi, "the signed URL");
|
|
2124
1985
|
}
|
|
2125
|
-
function downloadFailed(url, reason, original) {
|
|
1986
|
+
function downloadFailed(url, reason, original, again = "naro db dump") {
|
|
2126
1987
|
const scrubbed = withoutUrl(reason, url);
|
|
2127
1988
|
let cause;
|
|
2128
1989
|
if (original !== void 0) {
|
|
@@ -2135,7 +1996,7 @@ function downloadFailed(url, reason, original) {
|
|
|
2135
1996
|
}
|
|
2136
1997
|
return new NaroError(
|
|
2137
1998
|
"DATABASE_UNAVAILABLE",
|
|
2138
|
-
`Could not download the dump (${scrubbed}). Run
|
|
1999
|
+
`Could not download the dump (${scrubbed}). Run ${again} again.`,
|
|
2139
2000
|
cause === void 0 ? {} : { cause }
|
|
2140
2001
|
);
|
|
2141
2002
|
}
|
|
@@ -2153,17 +2014,22 @@ var DownloadStreamError = class extends Error {
|
|
|
2153
2014
|
this.name = "DownloadStreamError";
|
|
2154
2015
|
}
|
|
2155
2016
|
};
|
|
2156
|
-
async function openDownload(deps, url) {
|
|
2017
|
+
async function openDownload(deps, url, again) {
|
|
2157
2018
|
let response;
|
|
2158
2019
|
try {
|
|
2159
2020
|
response = await deps.fetch(url);
|
|
2160
2021
|
} catch (error) {
|
|
2161
|
-
throw downloadFailed(url, reasonOf(error), error);
|
|
2022
|
+
throw downloadFailed(url, reasonOf(error), error, again);
|
|
2162
2023
|
}
|
|
2163
2024
|
if (!response.ok || response.body === null) {
|
|
2164
2025
|
await response.body?.cancel().catch(() => {
|
|
2165
2026
|
});
|
|
2166
|
-
throw downloadFailed(
|
|
2027
|
+
throw downloadFailed(
|
|
2028
|
+
url,
|
|
2029
|
+
`HTTP ${String(response.status)}`,
|
|
2030
|
+
void 0,
|
|
2031
|
+
again
|
|
2032
|
+
);
|
|
2167
2033
|
}
|
|
2168
2034
|
return response.body;
|
|
2169
2035
|
}
|
|
@@ -2198,8 +2064,8 @@ async function streamInto(body, destination, end) {
|
|
|
2198
2064
|
await pipeline(chunksOf(body), counter, destination, { end });
|
|
2199
2065
|
return bytes;
|
|
2200
2066
|
}
|
|
2201
|
-
function streamFailed(error, url, shown) {
|
|
2202
|
-
return error instanceof DownloadStreamError ? downloadFailed(url, reasonOf(error.original), error.original) : writeFailed(shown, error);
|
|
2067
|
+
function streamFailed(error, url, shown, again) {
|
|
2068
|
+
return error instanceof DownloadStreamError ? downloadFailed(url, reasonOf(error.original), error.original, again) : writeFailed(shown, error);
|
|
2203
2069
|
}
|
|
2204
2070
|
function refuseDirectory(path, shown) {
|
|
2205
2071
|
let isDirectory = false;
|
|
@@ -2215,14 +2081,14 @@ function refuseDirectory(path, shown) {
|
|
|
2215
2081
|
}
|
|
2216
2082
|
function makeOutputDirectory(path, shown) {
|
|
2217
2083
|
try {
|
|
2218
|
-
|
|
2084
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2219
2085
|
} catch (error) {
|
|
2220
2086
|
throw writeFailed(shown, error);
|
|
2221
2087
|
}
|
|
2222
2088
|
}
|
|
2223
|
-
async function downloadToFile(deps, url, path, shown) {
|
|
2224
|
-
const body = await openDownload(deps, url);
|
|
2225
|
-
const partial =
|
|
2089
|
+
async function downloadToFile(deps, url, path, shown, again) {
|
|
2090
|
+
const body = await openDownload(deps, url, again);
|
|
2091
|
+
const partial = join(
|
|
2226
2092
|
dirname(path),
|
|
2227
2093
|
`.${basename(path)}.${String(process.pid)}.part`
|
|
2228
2094
|
);
|
|
@@ -2235,15 +2101,15 @@ async function downloadToFile(deps, url, path, shown) {
|
|
|
2235
2101
|
rmSync(partial, { force: true });
|
|
2236
2102
|
} catch {
|
|
2237
2103
|
}
|
|
2238
|
-
throw streamFailed(error, url, shown);
|
|
2104
|
+
throw streamFailed(error, url, shown, again);
|
|
2239
2105
|
}
|
|
2240
2106
|
}
|
|
2241
|
-
async function downloadToStdout(deps, url) {
|
|
2242
|
-
const body = await openDownload(deps, url);
|
|
2107
|
+
async function downloadToStdout(deps, url, again) {
|
|
2108
|
+
const body = await openDownload(deps, url, again);
|
|
2243
2109
|
try {
|
|
2244
2110
|
return await streamInto(body, deps.stdout, false);
|
|
2245
2111
|
} catch (error) {
|
|
2246
|
-
throw streamFailed(error, url, "stdout");
|
|
2112
|
+
throw streamFailed(error, url, "stdout", again);
|
|
2247
2113
|
}
|
|
2248
2114
|
}
|
|
2249
2115
|
async function runDbDump(context, overrides = {}) {
|
|
@@ -2272,10 +2138,12 @@ async function runDbDump(context, overrides = {}) {
|
|
|
2272
2138
|
};
|
|
2273
2139
|
const output = flags.output === void 0 ? null : { path: resolve2(context.cwd, flags.output), shown: flags.output };
|
|
2274
2140
|
if (output) refuseDirectory(output.path, output.shown);
|
|
2275
|
-
const client = await context
|
|
2141
|
+
const client = await dataClient(context);
|
|
2276
2142
|
const projectId = await context.resolveProjectId();
|
|
2277
2143
|
if (output) makeOutputDirectory(output.path, output.shown);
|
|
2278
|
-
context.
|
|
2144
|
+
const environment = dataEnvironment(context.flags);
|
|
2145
|
+
const again = `naro db dump${envFlag(environment)}`;
|
|
2146
|
+
context.output.warn(exportNotice(environment));
|
|
2279
2147
|
const step = await client.db.export(projectId, options, {
|
|
2280
2148
|
sleep: deps.sleep
|
|
2281
2149
|
});
|
|
@@ -2283,16 +2151,16 @@ async function runDbDump(context, overrides = {}) {
|
|
|
2283
2151
|
if (file === null)
|
|
2284
2152
|
throw new NaroError(
|
|
2285
2153
|
"DATABASE_UNAVAILABLE",
|
|
2286
|
-
|
|
2154
|
+
`Could not download the dump (Cloudflare finished the export without a file). Run ${again} again.`
|
|
2287
2155
|
);
|
|
2288
|
-
const bytes = output === null ? await downloadToStdout(deps, file.url) : await downloadToFile(deps, file.url, output.path, output.shown);
|
|
2156
|
+
const bytes = output === null ? await downloadToStdout(deps, file.url, again) : await downloadToFile(deps, file.url, output.path, output.shown, again);
|
|
2289
2157
|
const bookmark = step.atBookmark;
|
|
2290
2158
|
if (!context.output.json) {
|
|
2291
2159
|
context.output.warn(
|
|
2292
2160
|
`\u2713 Wrote ${flags.output ?? "stdout"} (${humanSize(bytes)}) at bookmark ${bookmark}.`
|
|
2293
2161
|
);
|
|
2294
2162
|
context.output.warn(
|
|
2295
|
-
` naro db restore --bookmark ${bookmark} returns the database to this point.`
|
|
2163
|
+
` naro db restore --bookmark ${bookmark}${envFlag(dataEnvironment(context.flags))} returns the database to this point.`
|
|
2296
2164
|
);
|
|
2297
2165
|
}
|
|
2298
2166
|
context.output.result({ output: flags.output, bytes, atBookmark: bookmark });
|
|
@@ -2313,7 +2181,7 @@ function withWayBack(context, error, before) {
|
|
|
2313
2181
|
return error;
|
|
2314
2182
|
if (!context.output.json)
|
|
2315
2183
|
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.`
|
|
2184
|
+
`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
2185
|
);
|
|
2318
2186
|
return new NaroError(error.code, error.message, {
|
|
2319
2187
|
...error.status === void 0 ? {} : { status: error.status },
|
|
@@ -2331,7 +2199,7 @@ async function runDbRestore(context) {
|
|
|
2331
2199
|
);
|
|
2332
2200
|
if ("bookmark" in given && !isBookmark(given.bookmark))
|
|
2333
2201
|
throw new NaroError("VALIDATION_FAILED", BOOKMARK_FORMAT_MESSAGE);
|
|
2334
|
-
const client = await context
|
|
2202
|
+
const client = await dataClient(context);
|
|
2335
2203
|
const projectId = await context.resolveProjectId();
|
|
2336
2204
|
let target;
|
|
2337
2205
|
let at = null;
|
|
@@ -2358,7 +2226,7 @@ async function runDbRestore(context) {
|
|
|
2358
2226
|
}
|
|
2359
2227
|
await confirmOrAbort(
|
|
2360
2228
|
context,
|
|
2361
|
-
`Restore
|
|
2229
|
+
`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
2230
|
);
|
|
2363
2231
|
const before = await bookmarkNow(context, client, projectId);
|
|
2364
2232
|
let restored;
|
|
@@ -2373,8 +2241,8 @@ async function runDbRestore(context) {
|
|
|
2373
2241
|
previousBookmark: restored.previousBookmark
|
|
2374
2242
|
},
|
|
2375
2243
|
[
|
|
2376
|
-
`\u2713 Restored
|
|
2377
|
-
` To undo: naro db restore --bookmark ${restored.previousBookmark}`
|
|
2244
|
+
`\u2713 Restored ${databaseName(dataEnvironment(context.flags))} to bookmark ${restored.bookmark}.`,
|
|
2245
|
+
` To undo: naro db restore --bookmark ${restored.previousBookmark}${envFlag(dataEnvironment(context.flags))}`
|
|
2378
2246
|
]
|
|
2379
2247
|
);
|
|
2380
2248
|
}
|
|
@@ -2388,9 +2256,9 @@ async function runDbPull(context, overrides = {}) {
|
|
|
2388
2256
|
`${relativeToCwd(context, dir)} already has ${String(existing.length)} migration file(s). naro db pull only baselines a project without migrations; to see the current schema, run naro db dump --schema-only.`
|
|
2389
2257
|
);
|
|
2390
2258
|
const name = nextMigrationFileName(existing, "remote_schema");
|
|
2391
|
-
const path =
|
|
2259
|
+
const path = join(dir, name);
|
|
2392
2260
|
const shown = relativeToCwd(context, path);
|
|
2393
|
-
const client = await context
|
|
2261
|
+
const client = await dataClient(context);
|
|
2394
2262
|
const projectId = await context.resolveProjectId();
|
|
2395
2263
|
await confirmOrAbort(
|
|
2396
2264
|
context,
|
|
@@ -2470,12 +2338,12 @@ async function readPolicies(client, projectId, warn) {
|
|
|
2470
2338
|
return void 0;
|
|
2471
2339
|
}
|
|
2472
2340
|
const policies = [];
|
|
2473
|
-
const
|
|
2341
|
+
const unreadable2 = [];
|
|
2474
2342
|
for (const row of objectsOf(stored)) {
|
|
2475
2343
|
const policy = loadPolicy(row);
|
|
2476
2344
|
if (policy === void 0) continue;
|
|
2477
2345
|
if (policy.invalid !== null)
|
|
2478
|
-
|
|
2346
|
+
unreadable2.push({
|
|
2479
2347
|
table: policy.table,
|
|
2480
2348
|
name: policy.name,
|
|
2481
2349
|
reason: policy.invalid
|
|
@@ -2496,7 +2364,7 @@ async function readPolicies(client, projectId, warn) {
|
|
|
2496
2364
|
return {
|
|
2497
2365
|
tables: tableNames(snapshot),
|
|
2498
2366
|
policies,
|
|
2499
|
-
unreadable,
|
|
2367
|
+
unreadable: unreadable2,
|
|
2500
2368
|
snapshot,
|
|
2501
2369
|
indexes: indexesFromRows(objectsOf(indexes))
|
|
2502
2370
|
};
|
|
@@ -2524,7 +2392,7 @@ function issueLines(issues, tables) {
|
|
|
2524
2392
|
];
|
|
2525
2393
|
}
|
|
2526
2394
|
async function runDbLint(context) {
|
|
2527
|
-
const client = await context
|
|
2395
|
+
const client = await dataClient(context);
|
|
2528
2396
|
const projectId = await context.resolveProjectId();
|
|
2529
2397
|
const tables = await client.db.tables(projectId);
|
|
2530
2398
|
const answer = await client.db.query(projectId, { sql: DIAGNOSTICS_SQL });
|
|
@@ -2573,8 +2441,8 @@ async function runDbLint(context) {
|
|
|
2573
2441
|
}
|
|
2574
2442
|
|
|
2575
2443
|
// src/commands/db-reset.ts
|
|
2576
|
-
import { readFileSync as
|
|
2577
|
-
import { join as
|
|
2444
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2445
|
+
import { join as join2 } from "path";
|
|
2578
2446
|
var MAX_STATEMENTS = 50;
|
|
2579
2447
|
var SCHEMA_FILTER = `${systemTablePredicate("name")} AND ${systemTablePredicate("tbl_name")}`;
|
|
2580
2448
|
var SCHEMA_SQL2 = [
|
|
@@ -2730,7 +2598,7 @@ function resetStopped(context, error, bookmark, lead, message) {
|
|
|
2730
2598
|
const naroError = isNaroError(error) ? error : toNaroError(error);
|
|
2731
2599
|
if (!context.output.json)
|
|
2732
2600
|
context.output.warn(
|
|
2733
|
-
bookmark === null ? lead : `${lead} The database was at bookmark ${bookmark} \u2014 naro db restore --bookmark ${bookmark} undoes the reset.`
|
|
2601
|
+
bookmark === null ? lead : `${lead} The database was at bookmark ${bookmark} \u2014 naro db restore --bookmark ${bookmark}${envFlag(dataEnvironment(context.flags))} undoes the reset.`
|
|
2734
2602
|
);
|
|
2735
2603
|
return new NaroError(naroError.code, message(naroError.message), {
|
|
2736
2604
|
...naroError.status === void 0 ? {} : { status: naroError.status },
|
|
@@ -2756,8 +2624,8 @@ async function runDbReset(context) {
|
|
|
2756
2624
|
${problemLines.join("\n")}`,
|
|
2757
2625
|
{ details: { problems } }
|
|
2758
2626
|
);
|
|
2759
|
-
const texts = files.map((name) =>
|
|
2760
|
-
const client = await context
|
|
2627
|
+
const texts = files.map((name) => readFileSync2(join2(dir, name), "utf8"));
|
|
2628
|
+
const client = await dataClient(context);
|
|
2761
2629
|
const projectId = await context.resolveProjectId();
|
|
2762
2630
|
const bookmark = context.flags.dryRun ? null : await bookmarkNow(context, client, projectId, "resetting");
|
|
2763
2631
|
const { policyTable: policyTable2, storageTable, ...input } = await readSchema(
|
|
@@ -2831,11 +2699,11 @@ ${problemLines.join("\n")}`,
|
|
|
2831
2699
|
}
|
|
2832
2700
|
if (bookmark !== null)
|
|
2833
2701
|
context.output.log(
|
|
2834
|
-
`The database is at bookmark ${bookmark} \u2014 naro db restore --bookmark ${bookmark} undoes the reset.`
|
|
2702
|
+
`The database is at bookmark ${bookmark} \u2014 naro db restore --bookmark ${bookmark}${envFlag(dataEnvironment(context.flags))} undoes the reset.`
|
|
2835
2703
|
);
|
|
2836
2704
|
await confirmOrAbort(
|
|
2837
2705
|
context,
|
|
2838
|
-
`Reset
|
|
2706
|
+
`Reset ${databaseName(dataEnvironment(context.flags))}? This drops ${counts}, clears the migration ledger, then re-applies ${String(files.length)} migration file(s) from ${shownDir}.`
|
|
2839
2707
|
);
|
|
2840
2708
|
let done = 0;
|
|
2841
2709
|
const total = plan.batches.length;
|
|
@@ -2887,18 +2755,20 @@ ${problemLines.join("\n")}`,
|
|
|
2887
2755
|
warnings
|
|
2888
2756
|
},
|
|
2889
2757
|
[
|
|
2890
|
-
`\u2713 Reset
|
|
2891
|
-
...bookmark === null ? [] : [
|
|
2758
|
+
`\u2713 Reset ${databaseName(dataEnvironment(context.flags))}: dropped ${String(dropped)} object(s), re-applied ${String(reapplied.length)} migration(s).`,
|
|
2759
|
+
...bookmark === null ? [] : [
|
|
2760
|
+
` To undo: naro db restore --bookmark ${bookmark}${envFlag(dataEnvironment(context.flags))}`
|
|
2761
|
+
]
|
|
2892
2762
|
]
|
|
2893
2763
|
);
|
|
2894
2764
|
}
|
|
2895
2765
|
|
|
2896
2766
|
// src/commands/db-schema.ts
|
|
2897
|
-
import { mkdirSync as
|
|
2898
|
-
import { join as
|
|
2767
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2768
|
+
import { join as join3 } from "path";
|
|
2899
2769
|
|
|
2900
2770
|
// src/commands/policy.ts
|
|
2901
|
-
import { readFileSync as
|
|
2771
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2902
2772
|
import { resolve as resolve3 } from "path";
|
|
2903
2773
|
|
|
2904
2774
|
// ../../packages/naro-sdk/src/query/filters.ts
|
|
@@ -3316,7 +3186,7 @@ function conditionsFrom(flags) {
|
|
|
3316
3186
|
function patchFromFile(context, path, table3) {
|
|
3317
3187
|
let text;
|
|
3318
3188
|
try {
|
|
3319
|
-
text =
|
|
3189
|
+
text = readFileSync3(resolve3(context.cwd, path), "utf8");
|
|
3320
3190
|
} catch {
|
|
3321
3191
|
refuse2(`Could not read ${path}.`);
|
|
3322
3192
|
}
|
|
@@ -3478,12 +3348,12 @@ function conditionText(action, value) {
|
|
|
3478
3348
|
if (isInvalid(value))
|
|
3479
3349
|
return `cannot be read (${value.invalid}); it denies everyone but the service key`;
|
|
3480
3350
|
const using = value.using ? filterText(value.using) : "";
|
|
3481
|
-
const
|
|
3482
|
-
if (using === void 0 ||
|
|
3351
|
+
const check2 = value.check ? filterText(value.check) : "";
|
|
3352
|
+
if (using === void 0 || check2 === void 0)
|
|
3483
3353
|
return `as JSON ${JSON.stringify(value)}`;
|
|
3484
3354
|
const parts = [];
|
|
3485
3355
|
if (using !== "") parts.push(`using ${using}`);
|
|
3486
|
-
if (
|
|
3356
|
+
if (check2 !== "") parts.push(`check ${check2}`);
|
|
3487
3357
|
else if (action === "update") parts.push("check = using");
|
|
3488
3358
|
const text = parts.join("; ");
|
|
3489
3359
|
return value.role === "authenticated" ? `${text} (signed-in users only)` : text;
|
|
@@ -3524,6 +3394,12 @@ function policyTable(policies) {
|
|
|
3524
3394
|
];
|
|
3525
3395
|
}
|
|
3526
3396
|
var UNSHOWN = "has characters that cannot be shown here";
|
|
3397
|
+
function hintsFor(lines, environment) {
|
|
3398
|
+
if (environment === null) return lines;
|
|
3399
|
+
return lines.map(
|
|
3400
|
+
(line) => line.includes(UNSHOWN) || line.startsWith("Add one with ") ? forEnvironment(line, environment) : line
|
|
3401
|
+
);
|
|
3402
|
+
}
|
|
3527
3403
|
function statementLine(policy) {
|
|
3528
3404
|
if (policy.invalid !== null)
|
|
3529
3405
|
return printable(
|
|
@@ -3556,7 +3432,7 @@ function policyStatements(policies) {
|
|
|
3556
3432
|
}
|
|
3557
3433
|
async function runPolicyList(context) {
|
|
3558
3434
|
const table3 = context.flags.table;
|
|
3559
|
-
const client = await context
|
|
3435
|
+
const client = await dataClient(context);
|
|
3560
3436
|
const projectId = await context.resolveProjectId();
|
|
3561
3437
|
const policies = await client.backend.rlsPolicies(
|
|
3562
3438
|
projectId,
|
|
@@ -3574,7 +3450,10 @@ async function runPolicyList(context) {
|
|
|
3574
3450
|
"No policies yet: every table denies anon and signed-in requests.",
|
|
3575
3451
|
'Add one with `naro policy create "CREATE POLICY \u2026"` or `naro policy set <table> --select public`.'
|
|
3576
3452
|
];
|
|
3577
|
-
context.output.result(
|
|
3453
|
+
context.output.result(
|
|
3454
|
+
{ projectId, policies },
|
|
3455
|
+
hintsFor(lines, dataEnvironment(context.flags))
|
|
3456
|
+
);
|
|
3578
3457
|
}
|
|
3579
3458
|
function statementText(context) {
|
|
3580
3459
|
const [positional] = context.positionals;
|
|
@@ -3584,7 +3463,7 @@ function statementText(context) {
|
|
|
3584
3463
|
let text = positional;
|
|
3585
3464
|
if (file !== void 0) {
|
|
3586
3465
|
try {
|
|
3587
|
-
text =
|
|
3466
|
+
text = readFileSync3(resolve3(context.cwd, file), "utf8");
|
|
3588
3467
|
} catch {
|
|
3589
3468
|
refuse2(`Could not read ${file}.`);
|
|
3590
3469
|
}
|
|
@@ -3648,7 +3527,7 @@ async function runStatements(client, projectId, statements, allowUnindexed) {
|
|
|
3648
3527
|
}
|
|
3649
3528
|
async function runPolicyCreate(context) {
|
|
3650
3529
|
const statements = policyStatementsOf(statementText(context));
|
|
3651
|
-
const client = await context
|
|
3530
|
+
const client = await dataClient(context);
|
|
3652
3531
|
const projectId = await context.resolveProjectId();
|
|
3653
3532
|
const applied = await runStatements(
|
|
3654
3533
|
client,
|
|
@@ -3669,7 +3548,7 @@ async function runPolicyDrop(context) {
|
|
|
3669
3548
|
context,
|
|
3670
3549
|
`Drop policy ${printable(name)} on ${printable(table3)}? It stops applying within about 10 seconds.`
|
|
3671
3550
|
);
|
|
3672
|
-
const client = await context
|
|
3551
|
+
const client = await dataClient(context);
|
|
3673
3552
|
const projectId = await context.resolveProjectId();
|
|
3674
3553
|
const dropped = await client.backend.dropRlsPolicy(
|
|
3675
3554
|
projectId,
|
|
@@ -3683,7 +3562,7 @@ async function runPolicyDrop(context) {
|
|
|
3683
3562
|
async function runPolicySet(context) {
|
|
3684
3563
|
const [table3] = context.positionalOrThrow(1, SET_USAGE);
|
|
3685
3564
|
const patch = policyPatch(context.flags, context, table3);
|
|
3686
|
-
const client = await context
|
|
3565
|
+
const client = await dataClient(context);
|
|
3687
3566
|
const projectId = await context.resolveProjectId();
|
|
3688
3567
|
const policy = await client.backend.setPolicy(
|
|
3689
3568
|
projectId,
|
|
@@ -3700,11 +3579,14 @@ async function runPolicySet(context) {
|
|
|
3700
3579
|
}
|
|
3701
3580
|
context.output.result(
|
|
3702
3581
|
{ projectId, policy, ...named === void 0 ? {} : { policies: named } },
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3582
|
+
hintsFor(
|
|
3583
|
+
[
|
|
3584
|
+
`\u2705 Saved the policy for ${printable(policy.table)}. ${TAKES_EFFECT}`,
|
|
3585
|
+
...policyTable([policy]),
|
|
3586
|
+
...named === void 0 || named.length === 0 ? [] : ["", ...policyStatements(named)]
|
|
3587
|
+
],
|
|
3588
|
+
dataEnvironment(context.flags)
|
|
3589
|
+
)
|
|
3708
3590
|
);
|
|
3709
3591
|
}
|
|
3710
3592
|
async function runPolicyRemove(context) {
|
|
@@ -3713,7 +3595,7 @@ async function runPolicyRemove(context) {
|
|
|
3713
3595
|
context,
|
|
3714
3596
|
`Remove every policy on ${printable(table3)}? Anon and signed-in requests to it are denied from then on.`
|
|
3715
3597
|
);
|
|
3716
|
-
const client = await context
|
|
3598
|
+
const client = await dataClient(context);
|
|
3717
3599
|
const projectId = await context.resolveProjectId();
|
|
3718
3600
|
const policy = await client.backend.removePolicy(projectId, table3);
|
|
3719
3601
|
const names = policy.policies ?? [];
|
|
@@ -3846,7 +3728,7 @@ function ruleText(value) {
|
|
|
3846
3728
|
return "invalid" in value ? "invalid" : "condition";
|
|
3847
3729
|
}
|
|
3848
3730
|
var SUGAR_NAME = /^naro_(?:select|insert|update|delete)$/;
|
|
3849
|
-
function removedLines(policy, dryRun) {
|
|
3731
|
+
function removedLines(policy, dryRun, environment) {
|
|
3850
3732
|
const verb = dryRun ? "Would remove" : "\u2713 Removed";
|
|
3851
3733
|
const names = policy.policies ?? [];
|
|
3852
3734
|
let line;
|
|
@@ -3884,7 +3766,11 @@ function removedLines(policy, dryRun) {
|
|
|
3884
3766
|
lines.push(
|
|
3885
3767
|
` 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
3768
|
// The server's bytes, or not shown: a name may hold a line break.
|
|
3887
|
-
|
|
3769
|
+
// Under --env, the command acts on the environment (PR #343 review 2).
|
|
3770
|
+
typeof command !== "string" ? forEnvironment(
|
|
3771
|
+
" naro policy set <table> --<action> owner --owner <column> sets one.",
|
|
3772
|
+
environment
|
|
3773
|
+
) : isPrintable(command) ? forEnvironment(command, environment) : " The command has characters that cannot be shown here: --json has the exact text."
|
|
3888
3774
|
);
|
|
3889
3775
|
}
|
|
3890
3776
|
return lines;
|
|
@@ -3899,7 +3785,7 @@ async function runSchemaChange(context, operations, question) {
|
|
|
3899
3785
|
const { confirm: _confirm, ...rest } = operation;
|
|
3900
3786
|
return rest;
|
|
3901
3787
|
}) : operations;
|
|
3902
|
-
const client = await context
|
|
3788
|
+
const client = await dataClient(context);
|
|
3903
3789
|
const projectId = await context.resolveProjectId();
|
|
3904
3790
|
const dir = migrationsDir(context);
|
|
3905
3791
|
const local = listMigrationFiles(dir);
|
|
@@ -3924,7 +3810,9 @@ async function runSchemaChange(context, operations, question) {
|
|
|
3924
3810
|
...sqlLines,
|
|
3925
3811
|
`Dry run \u2014 nothing was applied. It would be recorded as ${printable(result.name)}.`,
|
|
3926
3812
|
...result.policies.map((policy) => policyLine(policy, true)),
|
|
3927
|
-
...removed.flatMap(
|
|
3813
|
+
...removed.flatMap(
|
|
3814
|
+
(policy) => removedLines(policy, true, dataEnvironment(context.flags))
|
|
3815
|
+
)
|
|
3928
3816
|
]);
|
|
3929
3817
|
return;
|
|
3930
3818
|
}
|
|
@@ -3932,13 +3820,13 @@ async function runSchemaChange(context, operations, question) {
|
|
|
3932
3820
|
let file = null;
|
|
3933
3821
|
let fileWarning;
|
|
3934
3822
|
const relDir = relativeToCwd(context, dir);
|
|
3935
|
-
const rel =
|
|
3823
|
+
const rel = join3(relDir === "." ? "" : relDir, name);
|
|
3936
3824
|
if (context.flags.noWrite) {
|
|
3937
3825
|
fileWarning = `Not written to ${relDir} (--no-write): naro db reset will not re-apply ${result.name}, and naro migration list shows it as remote-only.`;
|
|
3938
3826
|
} else {
|
|
3939
3827
|
try {
|
|
3940
|
-
|
|
3941
|
-
|
|
3828
|
+
mkdirSync2(dir, { recursive: true });
|
|
3829
|
+
writeFileSync2(join3(dir, name), result.sql, { flag: "wx" });
|
|
3942
3830
|
file = rel;
|
|
3943
3831
|
} catch (error) {
|
|
3944
3832
|
const reason = (error instanceof Error ? error.message : String(error)).replace(/[.\s]+$/, "");
|
|
@@ -3946,7 +3834,10 @@ async function runSchemaChange(context, operations, question) {
|
|
|
3946
3834
|
}
|
|
3947
3835
|
}
|
|
3948
3836
|
for (const warning of allWarnings)
|
|
3949
|
-
context.output.warn(
|
|
3837
|
+
context.output.warn(
|
|
3838
|
+
`\u26A0 ${printable(forEnvironment(warning, dataEnvironment(context.flags)))}`
|
|
3839
|
+
);
|
|
3840
|
+
fileWarning &&= forEnvironment(fileWarning, dataEnvironment(context.flags));
|
|
3950
3841
|
if (fileWarning) context.output.warn(`\u26A0 ${printable(fileWarning)}`);
|
|
3951
3842
|
context.output.result(
|
|
3952
3843
|
{
|
|
@@ -3960,7 +3851,9 @@ async function runSchemaChange(context, operations, question) {
|
|
|
3960
3851
|
...sqlLines,
|
|
3961
3852
|
file === null ? `\u2705 Applied ${printable(result.name)}.` : `\u2705 Applied ${printable(result.name)} and wrote ${printable(file)}.`,
|
|
3962
3853
|
...result.policies.map((policy) => policyLine(policy)),
|
|
3963
|
-
...removed.flatMap(
|
|
3854
|
+
...removed.flatMap(
|
|
3855
|
+
(policy) => removedLines(policy, false, dataEnvironment(context.flags))
|
|
3856
|
+
)
|
|
3964
3857
|
]
|
|
3965
3858
|
);
|
|
3966
3859
|
}
|
|
@@ -4011,24 +3904,29 @@ async function runDbTableRename(context) {
|
|
|
4011
3904
|
if (!table3 || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
|
|
4012
3905
|
await runSchemaChange(context, [{ op: "renameTable", table: table3, to }]);
|
|
4013
3906
|
}
|
|
3907
|
+
function inDatabase(environment) {
|
|
3908
|
+
return environment === null ? "" : ` in ${databaseName(environment)}`;
|
|
3909
|
+
}
|
|
4014
3910
|
async function runDbTableDropColumn(context) {
|
|
4015
3911
|
const [table3, column] = context.positionals;
|
|
4016
3912
|
if (!table3 || !column)
|
|
4017
3913
|
throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
|
|
4018
3914
|
const target = `${table3}.${column}`;
|
|
3915
|
+
const env = dataEnvironment(context.flags);
|
|
4019
3916
|
await runSchemaChange(
|
|
4020
3917
|
context,
|
|
4021
3918
|
[{ 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).`
|
|
3919
|
+
`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
3920
|
);
|
|
4024
3921
|
}
|
|
4025
3922
|
async function runDbTableDrop(context) {
|
|
4026
3923
|
const [table3] = context.positionals;
|
|
4027
3924
|
if (!table3) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
|
|
3925
|
+
const env = dataEnvironment(context.flags);
|
|
4028
3926
|
await runSchemaChange(
|
|
4029
3927
|
context,
|
|
4030
3928
|
[{ 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).`
|
|
3929
|
+
`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
3930
|
);
|
|
4033
3931
|
}
|
|
4034
3932
|
async function runDbIndexCreate(context) {
|
|
@@ -4057,7 +3955,7 @@ async function runDbIndexDrop(context) {
|
|
|
4057
3955
|
var defaults2 = {
|
|
4058
3956
|
detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
|
|
4059
3957
|
openBrowser,
|
|
4060
|
-
sleep: (ms) => new Promise((
|
|
3958
|
+
sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms))
|
|
4061
3959
|
};
|
|
4062
3960
|
function connected(context, project) {
|
|
4063
3961
|
const branch = project.defaultBranch;
|
|
@@ -4588,7 +4486,7 @@ async function runInspect(context) {
|
|
|
4588
4486
|
|
|
4589
4487
|
// src/commands/domains.ts
|
|
4590
4488
|
var defaults3 = {
|
|
4591
|
-
sleep: (ms) => new Promise((
|
|
4489
|
+
sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms)),
|
|
4592
4490
|
now: () => Date.now()
|
|
4593
4491
|
};
|
|
4594
4492
|
var FIRST_DELAY_MS = 5e3;
|
|
@@ -4806,7 +4704,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4806
4704
|
]);
|
|
4807
4705
|
return;
|
|
4808
4706
|
}
|
|
4809
|
-
let
|
|
4707
|
+
let timedOut2 = false;
|
|
4810
4708
|
let abandoned = false;
|
|
4811
4709
|
if (wait && !domains.every(isReady)) {
|
|
4812
4710
|
const deadline = deps.now() + WAIT_TIMEOUT_MS;
|
|
@@ -4819,7 +4717,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4819
4717
|
}
|
|
4820
4718
|
const remaining = deadline - deps.now();
|
|
4821
4719
|
if (remaining <= 0) {
|
|
4822
|
-
|
|
4720
|
+
timedOut2 = true;
|
|
4823
4721
|
break;
|
|
4824
4722
|
}
|
|
4825
4723
|
for (const domain of pending)
|
|
@@ -4839,7 +4737,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4839
4737
|
const notReady = domains.filter((domain) => !isReady(domain));
|
|
4840
4738
|
const names = notReady.map((domain) => domain.hostname).join(", ");
|
|
4841
4739
|
const details = { projectId, domains, ready: false };
|
|
4842
|
-
if (
|
|
4740
|
+
if (timedOut2) {
|
|
4843
4741
|
const cause2 = notReady[0]?.cause;
|
|
4844
4742
|
throw new NaroError(
|
|
4845
4743
|
"TIMEOUT",
|
|
@@ -4886,7 +4784,7 @@ async function runDomainsRemove(context) {
|
|
|
4886
4784
|
}
|
|
4887
4785
|
|
|
4888
4786
|
// src/commands/env.ts
|
|
4889
|
-
import { chmodSync, existsSync, readFileSync as
|
|
4787
|
+
import { chmodSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4890
4788
|
import { resolve as resolve4 } from "path";
|
|
4891
4789
|
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4892
4790
|
var KEY_MAX_LENGTH = 256;
|
|
@@ -5001,7 +4899,7 @@ async function runEnvImport(context) {
|
|
|
5001
4899
|
const target = parseTarget(targetArg);
|
|
5002
4900
|
let text;
|
|
5003
4901
|
try {
|
|
5004
|
-
text =
|
|
4902
|
+
text = readFileSync4(resolve4(context.cwd, file), "utf8");
|
|
5005
4903
|
} catch {
|
|
5006
4904
|
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
5007
4905
|
}
|
|
@@ -5092,7 +4990,7 @@ ${serializeDotenv(vars)}`;
|
|
|
5092
4990
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
5093
4991
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
5094
4992
|
}
|
|
5095
|
-
|
|
4993
|
+
writeFileSync3(path, text, { mode: 384 });
|
|
5096
4994
|
chmodSync(path, 384);
|
|
5097
4995
|
context.output.result({ projectId, file, environment: surface, count: count2 }, [
|
|
5098
4996
|
`\u2705 Pulled ${String(count2)} variable(s) into ${file}`
|
|
@@ -5103,6 +5001,7 @@ ${serializeDotenv(vars)}`;
|
|
|
5103
5001
|
var CREATE_USAGE2 = "Usage: naro environments create <slug> [--branch <branch>] [--copy-env preview|production|none] [--database schema|empty]";
|
|
5104
5002
|
var SET_USAGE2 = "Usage: naro environments set <slug> [--branch <branch> | --no-branch] [--protection on|off]";
|
|
5105
5003
|
var DELETE_USAGE = "Usage: naro environments delete <slug> [--yes]";
|
|
5004
|
+
var PROVISION_USAGE = "Usage: naro environments provision <slug>";
|
|
5106
5005
|
var NO_BRANCH = "\u2014";
|
|
5107
5006
|
function parseCopyEnv(value) {
|
|
5108
5007
|
if (value === void 0) return void 0;
|
|
@@ -5137,14 +5036,26 @@ function table2(rows) {
|
|
|
5137
5036
|
).join(" ")
|
|
5138
5037
|
);
|
|
5139
5038
|
}
|
|
5039
|
+
function dataCell(environment) {
|
|
5040
|
+
const status = environment.data?.status ?? "none";
|
|
5041
|
+
return status === "none" ? NO_BRANCH : status;
|
|
5042
|
+
}
|
|
5140
5043
|
function environmentRow(environment) {
|
|
5141
5044
|
return [
|
|
5142
5045
|
environment.slug,
|
|
5143
5046
|
environment.url,
|
|
5144
5047
|
environment.branch ?? NO_BRANCH,
|
|
5145
|
-
environment.protectionEnabled ? "on" : "off"
|
|
5048
|
+
environment.protectionEnabled ? "on" : "off",
|
|
5049
|
+
dataCell(environment)
|
|
5146
5050
|
];
|
|
5147
5051
|
}
|
|
5052
|
+
function dataErrorLines(environments) {
|
|
5053
|
+
return environments.flatMap(
|
|
5054
|
+
(environment) => environment.data?.status === "error" ? [
|
|
5055
|
+
`\u26A0 ${environment.slug}: ${environment.data.error ?? "its data could not be made."} Run \`naro environments provision ${environment.slug}\` to try again.`
|
|
5056
|
+
] : []
|
|
5057
|
+
);
|
|
5058
|
+
}
|
|
5148
5059
|
async function runEnvironmentsList(context) {
|
|
5149
5060
|
const projectId = await context.resolveProjectId();
|
|
5150
5061
|
const client = await context.client();
|
|
@@ -5152,9 +5063,10 @@ async function runEnvironmentsList(context) {
|
|
|
5152
5063
|
const count2 = String(environments.length);
|
|
5153
5064
|
context.output.result({ projectId, environments, limit }, [
|
|
5154
5065
|
...environments.length === 0 ? ["No environments. Create one with `naro environments create qa`."] : table2([
|
|
5155
|
-
["slug", "url", "branch", "protection"],
|
|
5066
|
+
["slug", "url", "branch", "protection", "data"],
|
|
5156
5067
|
...environments.map(environmentRow)
|
|
5157
5068
|
]),
|
|
5069
|
+
...dataErrorLines(environments),
|
|
5158
5070
|
limit === null ? `${count2} environment${environments.length === 1 ? "" : "s"}` : `${count2} of ${String(limit)} environments`
|
|
5159
5071
|
]);
|
|
5160
5072
|
}
|
|
@@ -5217,7 +5129,7 @@ async function runEnvironmentsDelete(context) {
|
|
|
5217
5129
|
const projectId = await context.resolveProjectId();
|
|
5218
5130
|
await confirmOrAbort(
|
|
5219
5131
|
context,
|
|
5220
|
-
`Delete environment ${slug}? Its worker
|
|
5132
|
+
`Delete environment ${slug}? Its worker, protection key and data \u2014 database, backend and Storage files \u2014 are removed.`
|
|
5221
5133
|
);
|
|
5222
5134
|
const client = await context.client();
|
|
5223
5135
|
const found = await client.environments.get(projectId, slug);
|
|
@@ -5226,132 +5138,111 @@ async function runEnvironmentsDelete(context) {
|
|
|
5226
5138
|
`\u2705 Deleted environment ${slug}`
|
|
5227
5139
|
]);
|
|
5228
5140
|
}
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
5232
|
-
import { dirname as dirname2, resolve as resolve5 } from "path";
|
|
5233
|
-
var LANG_MESSAGE = "naro gen types writes TypeScript only. Pass --lang typescript, or leave --lang out.";
|
|
5234
|
-
async function runGenTypes(context) {
|
|
5235
|
-
const { lang } = context.flags;
|
|
5236
|
-
if (lang !== void 0 && lang !== "typescript")
|
|
5237
|
-
throw new NaroError("VALIDATION_FAILED", LANG_MESSAGE);
|
|
5238
|
-
const client = await context.client();
|
|
5141
|
+
async function runEnvironmentsProvision(context) {
|
|
5142
|
+
const [slug] = context.positionalOrThrow(1, PROVISION_USAGE);
|
|
5239
5143
|
const projectId = await context.resolveProjectId();
|
|
5240
|
-
const
|
|
5241
|
-
const
|
|
5242
|
-
const
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
);
|
|
5246
|
-
const undescribed = tables.flatMap(
|
|
5247
|
-
(table3) => table3.error === void 0 ? [] : [{ name: table3.name, error: table3.error }]
|
|
5248
|
-
);
|
|
5249
|
-
for (const { name, error } of undescribed)
|
|
5250
|
-
context.output.warn(
|
|
5251
|
-
`\u26A0 ${printable(name)} could not be described and was generated as an empty interface: ${printable(error)}`
|
|
5252
|
-
);
|
|
5253
|
-
if (read.policies === void 0)
|
|
5254
|
-
context.output.warn(
|
|
5255
|
-
`\u26A0 The backend's policies could not be read (${printable(read.reason)}), so owner columns stay required in Insert.`
|
|
5256
|
-
);
|
|
5257
|
-
const extra = {
|
|
5258
|
-
...undescribed.length > 0 ? { undescribed } : {},
|
|
5259
|
-
...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
|
|
5260
|
-
};
|
|
5261
|
-
if (context.flags.output) {
|
|
5262
|
-
const path = resolve5(context.cwd, context.flags.output);
|
|
5263
|
-
mkdirSync4(dirname2(path), { recursive: true });
|
|
5264
|
-
writeFileSync5(path, types);
|
|
5265
|
-
context.output.result(
|
|
5266
|
-
{
|
|
5267
|
-
projectId,
|
|
5268
|
-
output: context.flags.output,
|
|
5269
|
-
tables: tables.length,
|
|
5270
|
-
...extra
|
|
5271
|
-
},
|
|
5272
|
-
[
|
|
5273
|
-
`\u2705 Wrote types for ${String(tables.length)} table(s) to ${context.flags.output}`
|
|
5274
|
-
]
|
|
5275
|
-
);
|
|
5276
|
-
return;
|
|
5277
|
-
}
|
|
5278
|
-
context.output.result({ projectId, types, ...extra }, [
|
|
5279
|
-
types.replace(/\n$/, "")
|
|
5144
|
+
const client = await context.client();
|
|
5145
|
+
const found = await client.environments.get(projectId, slug);
|
|
5146
|
+
const environment = await client.environments.provision(projectId, found.id);
|
|
5147
|
+
context.output.result({ projectId, environment }, [
|
|
5148
|
+
(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.`
|
|
5280
5149
|
]);
|
|
5281
5150
|
}
|
|
5282
5151
|
|
|
5283
|
-
// src/commands/
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5152
|
+
// src/commands/functions.ts
|
|
5153
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5154
|
+
import { join as join5 } from "path";
|
|
5155
|
+
|
|
5156
|
+
// src/functions/scaffold.ts
|
|
5157
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
5158
|
+
import { createRequire } from "module";
|
|
5159
|
+
import { dirname as dirname2, join as join4, relative, resolve as resolve5, sep } from "path";
|
|
5160
|
+
var NARO_JS_FUNCTIONS_MISSING = 'Cannot find "naro-js/functions". Install naro-js 0.7.0 or later: npm install naro-js@latest.';
|
|
5161
|
+
function scaffoldSource(name) {
|
|
5162
|
+
return `import { withNaro } from "naro-js/functions";
|
|
5163
|
+
|
|
5164
|
+
// Called at https://<your-app>.naro.sh/_naro/v1/functions/${name}.
|
|
5165
|
+
// \`naro\` acts as the caller, so the tables' row-level security applies;
|
|
5166
|
+
// \`naroAdmin\` uses the service key.
|
|
5167
|
+
Deno.serve(
|
|
5168
|
+
withNaro(async (req, { user }) => {
|
|
5169
|
+
const { name } = await req.json().catch(() => ({ name: "world" }));
|
|
5170
|
+
return Response.json({ message: \`Hello \${name}!\`, user: user?.id ?? null });
|
|
5171
|
+
}),
|
|
5172
|
+
);
|
|
5173
|
+
`;
|
|
5292
5174
|
}
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
);
|
|
5299
|
-
const client = await context.client();
|
|
5300
|
-
const projectId = await context.resolveProjectId();
|
|
5301
|
-
const query = queryOf2(context.flags);
|
|
5302
|
-
const filters = query.filters ?? {};
|
|
5303
|
-
if (context.flags.route !== void 0) {
|
|
5304
|
-
const result = await client.insights.paths(projectId, {
|
|
5305
|
-
...query,
|
|
5306
|
-
route: context.flags.route
|
|
5307
|
-
});
|
|
5308
|
-
context.output.result(
|
|
5309
|
-
{ projectId, ...result },
|
|
5310
|
-
routePathsLines(result, filters)
|
|
5311
|
-
);
|
|
5312
|
-
return;
|
|
5175
|
+
function naroJsInstalled(root) {
|
|
5176
|
+
for (let dir = resolve5(root); ; dir = dirname2(dir)) {
|
|
5177
|
+
if (existsSync2(join4(dir, "node_modules", "naro-js", "package.json")))
|
|
5178
|
+
return true;
|
|
5179
|
+
if (dirname2(dir) === dir) return false;
|
|
5313
5180
|
}
|
|
5314
|
-
const overview = await client.insights.overview(projectId, query);
|
|
5315
|
-
if (overview.deploymentsError)
|
|
5316
|
-
context.output.warn(
|
|
5317
|
-
`Deployments are unavailable right now: ${printable(overview.deploymentsError.message)}`
|
|
5318
|
-
);
|
|
5319
|
-
context.output.result(
|
|
5320
|
-
{ projectId, ...overview },
|
|
5321
|
-
reportLines(overview.report, overview.deployments, filters)
|
|
5322
|
-
);
|
|
5323
5181
|
}
|
|
5324
|
-
|
|
5325
|
-
if (
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
throw new NaroError(
|
|
5333
|
-
"VALIDATION_FAILED",
|
|
5334
|
-
`Filters only narrow a report; drop --${filter}.`
|
|
5335
|
-
);
|
|
5336
|
-
const client = await context.client();
|
|
5337
|
-
const projectId = await context.resolveProjectId();
|
|
5338
|
-
const result = await client.insights.setEnabled(projectId, enabled);
|
|
5339
|
-
context.output.result({ projectId, enabled: result.enabled }, [
|
|
5340
|
-
result.enabled ? TURNED_ON : TURNED_OFF
|
|
5341
|
-
]);
|
|
5182
|
+
function naroJsFunctionsResolves(root) {
|
|
5183
|
+
if (!naroJsInstalled(root)) return false;
|
|
5184
|
+
try {
|
|
5185
|
+
createRequire(join4(root, "package.json")).resolve("naro-js/functions");
|
|
5186
|
+
return true;
|
|
5187
|
+
} catch {
|
|
5188
|
+
return false;
|
|
5189
|
+
}
|
|
5342
5190
|
}
|
|
5343
|
-
|
|
5344
|
-
|
|
5191
|
+
var COVERS = /^(\.\/)?(\*|\*\*|naro)(\/|$)|^\.\/?$/;
|
|
5192
|
+
var LEAVES_OUT = /^(\.\/)?naro(\/functions)?\/?(\*\*)?$/;
|
|
5193
|
+
function tsconfigCovers(root, functionsLabel) {
|
|
5194
|
+
const file = join4(root, "tsconfig.json");
|
|
5195
|
+
if (!existsSync2(file)) return false;
|
|
5196
|
+
let config;
|
|
5197
|
+
try {
|
|
5198
|
+
config = parseJsonc(readFileSync5(file, "utf8"));
|
|
5199
|
+
} catch {
|
|
5200
|
+
return false;
|
|
5201
|
+
}
|
|
5202
|
+
const include = Array.isArray(config.include) ? config.include.filter((p) => typeof p === "string") : null;
|
|
5203
|
+
const exclude = Array.isArray(config.exclude) ? config.exclude.filter((p) => typeof p === "string") : [];
|
|
5204
|
+
const covered = include === null || include.some((pattern) => COVERS.test(pattern));
|
|
5205
|
+
const leftOut = exclude.some(
|
|
5206
|
+
(pattern) => LEAVES_OUT.test(pattern) || pattern === functionsLabel
|
|
5207
|
+
);
|
|
5208
|
+
return covered && !leftOut;
|
|
5209
|
+
}
|
|
5210
|
+
function gitignoreCovers(root, envPath) {
|
|
5211
|
+
const file = join4(root, ".gitignore");
|
|
5212
|
+
if (!existsSync2(file)) return false;
|
|
5213
|
+
const accepted = /* @__PURE__ */ new Set([
|
|
5214
|
+
envPath,
|
|
5215
|
+
`/${envPath}`,
|
|
5216
|
+
".env",
|
|
5217
|
+
"/.env",
|
|
5218
|
+
"**/.env",
|
|
5219
|
+
".env*",
|
|
5220
|
+
"*.env"
|
|
5221
|
+
]);
|
|
5222
|
+
return readFileSync5(file, "utf8").split(/\r?\n/).some((line) => accepted.has(line.trim()));
|
|
5345
5223
|
}
|
|
5346
|
-
function
|
|
5347
|
-
|
|
5224
|
+
function scaffoldNotes(folder) {
|
|
5225
|
+
const notes = [];
|
|
5226
|
+
if (!naroJsFunctionsResolves(folder.root))
|
|
5227
|
+
notes.push(`\u26A0 ${NARO_JS_FUNCTIONS_MISSING}`);
|
|
5228
|
+
const functionsLabel = relative(folder.root, folder.dir).split(sep).join("/");
|
|
5229
|
+
if (!folder.explicit && tsconfigCovers(folder.root, functionsLabel))
|
|
5230
|
+
notes.push(
|
|
5231
|
+
`\u26A0 tsconfig.json type-checks naro/functions too. If your app's type check trips on the functions' Deno code, add "exclude": ["naro/functions"] to it.`
|
|
5232
|
+
);
|
|
5233
|
+
const envPath = `${functionsLabel}/.env`;
|
|
5234
|
+
if (!gitignoreCovers(folder.root, envPath))
|
|
5235
|
+
notes.push(
|
|
5236
|
+
`\u26A0 ${envPath} is not in .gitignore. Add it before you keep local secrets there.`
|
|
5237
|
+
);
|
|
5238
|
+
return notes;
|
|
5348
5239
|
}
|
|
5349
5240
|
|
|
5350
5241
|
// src/commands/logs.ts
|
|
5351
5242
|
var FOLLOW_INTERVAL_MS = 3e3;
|
|
5352
5243
|
var FOLLOW_TIMEOUT_MS = 30 * 6e4;
|
|
5353
5244
|
function sleep(ms) {
|
|
5354
|
-
return new Promise((
|
|
5245
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
5355
5246
|
}
|
|
5356
5247
|
function formatBuildLog(log) {
|
|
5357
5248
|
return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
|
|
@@ -5361,6 +5252,27 @@ function formatRuntimeLog(entry) {
|
|
|
5361
5252
|
const request = entry.method && entry.path ? ` ${entry.method} ${entry.path}${entry.status ? ` ${String(entry.status)}` : ""}` : "";
|
|
5362
5253
|
return `${time} [${entry.level}]${request} ${entry.message}`.trimEnd();
|
|
5363
5254
|
}
|
|
5255
|
+
var FOLLOW_JSON_REFUSAL = "--follow streams runtime logs until you stop it, so it has no JSON result. Drop --json to stream, or drop --follow for the current entries as JSON.";
|
|
5256
|
+
async function followRuntimeEntries(context, read, format, wait = sleep) {
|
|
5257
|
+
let lastTs = -1;
|
|
5258
|
+
let boundary = /* @__PURE__ */ new Set();
|
|
5259
|
+
for (; ; ) {
|
|
5260
|
+
const entries = await read();
|
|
5261
|
+
for (const entry of entries) {
|
|
5262
|
+
if (entry.ts < lastTs) continue;
|
|
5263
|
+
const key = JSON.stringify(entry);
|
|
5264
|
+
if (entry.ts === lastTs) {
|
|
5265
|
+
if (boundary.has(key)) continue;
|
|
5266
|
+
} else {
|
|
5267
|
+
lastTs = entry.ts;
|
|
5268
|
+
boundary = /* @__PURE__ */ new Set();
|
|
5269
|
+
}
|
|
5270
|
+
boundary.add(key);
|
|
5271
|
+
context.output.log(format(entry));
|
|
5272
|
+
}
|
|
5273
|
+
await wait(FOLLOW_INTERVAL_MS);
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5364
5276
|
async function latestDeploymentId(client, projectId) {
|
|
5365
5277
|
const [latest] = await client.deployments.list(projectId);
|
|
5366
5278
|
if (!latest)
|
|
@@ -5422,24 +5334,11 @@ async function runtimeLogs(context, client, projectId) {
|
|
|
5422
5334
|
context.output.log(
|
|
5423
5335
|
`Following ${environment} runtime logs (Ctrl-C to stop)...`
|
|
5424
5336
|
);
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
if (entry.ts < lastTs) continue;
|
|
5431
|
-
const key = JSON.stringify(entry);
|
|
5432
|
-
if (entry.ts === lastTs) {
|
|
5433
|
-
if (boundary.has(key)) continue;
|
|
5434
|
-
} else {
|
|
5435
|
-
lastTs = entry.ts;
|
|
5436
|
-
boundary = /* @__PURE__ */ new Set();
|
|
5437
|
-
}
|
|
5438
|
-
boundary.add(key);
|
|
5439
|
-
context.output.log(formatRuntimeLog(entry));
|
|
5440
|
-
}
|
|
5441
|
-
await sleep(FOLLOW_INTERVAL_MS);
|
|
5442
|
-
}
|
|
5337
|
+
await followRuntimeEntries(
|
|
5338
|
+
context,
|
|
5339
|
+
() => client.logs.runtime(projectId, options),
|
|
5340
|
+
formatRuntimeLog
|
|
5341
|
+
);
|
|
5443
5342
|
}
|
|
5444
5343
|
async function runLogs(context) {
|
|
5445
5344
|
const [deploymentId] = context.positionals;
|
|
@@ -5450,10 +5349,7 @@ async function runLogs(context) {
|
|
|
5450
5349
|
"--type must be build or runtime."
|
|
5451
5350
|
);
|
|
5452
5351
|
if (type === "runtime" && context.flags.follow && context.flags.json)
|
|
5453
|
-
throw new NaroError(
|
|
5454
|
-
"VALIDATION_FAILED",
|
|
5455
|
-
"--follow streams runtime logs until you stop it, so it has no JSON result. Drop --json to stream, or drop --follow for the current entries as JSON."
|
|
5456
|
-
);
|
|
5352
|
+
throw new NaroError("VALIDATION_FAILED", FOLLOW_JSON_REFUSAL);
|
|
5457
5353
|
if (type === "build" && context.flags.env)
|
|
5458
5354
|
throw new NaroError(
|
|
5459
5355
|
"VALIDATION_FAILED",
|
|
@@ -5478,47 +5374,651 @@ async function runLogs(context) {
|
|
|
5478
5374
|
await buildLogs(context, client, target);
|
|
5479
5375
|
}
|
|
5480
5376
|
|
|
5481
|
-
// src/commands/
|
|
5482
|
-
|
|
5483
|
-
|
|
5377
|
+
// src/commands/functions.ts
|
|
5378
|
+
var FUNCTIONS_ENV_REFUSAL = "naro functions works on the production backend only. Drop --env.";
|
|
5379
|
+
var VERSIONS_USAGE = "Usage: naro functions versions <name>";
|
|
5380
|
+
var LOGS_USAGE = "Usage: naro functions logs <name> [--level <l>] [--follow]";
|
|
5381
|
+
function functionName(context, usage) {
|
|
5382
|
+
const [name] = context.positionals;
|
|
5383
|
+
if (name === void 0 || name === "")
|
|
5384
|
+
throw new NaroError("VALIDATION_FAILED", usage);
|
|
5385
|
+
return name;
|
|
5484
5386
|
}
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5387
|
+
function statusWord(fn) {
|
|
5388
|
+
if (fn.status !== "error") return fn.status;
|
|
5389
|
+
return `error at ${fn.step ?? "upload"}: ${printable(fn.error ?? "unknown error")}`;
|
|
5390
|
+
}
|
|
5391
|
+
var cpuHours = (ms) => (ms / 36e5).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
5392
|
+
function functionLines(list) {
|
|
5393
|
+
if (list.functions.length === 0)
|
|
5394
|
+
return [
|
|
5395
|
+
"No functions yet. Create one with naro functions new <name>, then naro functions deploy <name>."
|
|
5396
|
+
];
|
|
5397
|
+
const width = Math.max(...list.functions.map((fn) => fn.name.length));
|
|
5398
|
+
const versionWidth = Math.max(
|
|
5399
|
+
...list.functions.map(
|
|
5400
|
+
(fn) => fn.version === null ? 1 : String(fn.version).length + 1
|
|
5401
|
+
)
|
|
5402
|
+
);
|
|
5403
|
+
const lines = list.functions.map((fn) => {
|
|
5404
|
+
const version2 = fn.version === null ? "\u2014" : `v${String(fn.version)}`;
|
|
5405
|
+
const verify = fn.verifyJwt ? "verify on " : "verify off";
|
|
5406
|
+
const stale = fn.stale ? " (stale: naro functions secrets sync brings it up)" : "";
|
|
5407
|
+
return `${fn.name.padEnd(width)} ${version2.padEnd(versionWidth)} ${verify} ${statusWord(fn)} ${fn.url}${stale}`;
|
|
5408
|
+
});
|
|
5409
|
+
lines.push(
|
|
5410
|
+
`Up to ${list.limits.functions.toLocaleString("en-US")} functions per ${list.limits.functionsScope} on this plan.`
|
|
5411
|
+
);
|
|
5412
|
+
const usage = list.usage;
|
|
5413
|
+
if (usage !== null) {
|
|
5414
|
+
const percent2 = usage.cpuMsLimit > 0 ? Math.floor(usage.cpuMs / usage.cpuMsLimit * 100) : 0;
|
|
5415
|
+
lines.push(
|
|
5416
|
+
`Function CPU this month (UTC, the team's): ${cpuHours(usage.cpuMs)} of ${cpuHours(usage.cpuMsLimit)} CPU-hours (${String(percent2)}%).`
|
|
5417
|
+
);
|
|
5418
|
+
if (usage.blocked)
|
|
5419
|
+
lines.push(
|
|
5420
|
+
"\u26A0 This team has used its monthly function CPU time. Functions answer 429 FUNCTIONS_QUOTA_EXCEEDED until the 1st at 00:00 UTC."
|
|
5421
|
+
);
|
|
5422
|
+
else if (usage.cpuMs >= Math.ceil(usage.cpuMsLimit * 0.8))
|
|
5423
|
+
lines.push(
|
|
5424
|
+
`\u26A0 This team has used ${String(percent2)}% of its monthly function CPU time.`
|
|
5425
|
+
);
|
|
5426
|
+
}
|
|
5427
|
+
return lines;
|
|
5428
|
+
}
|
|
5429
|
+
var kib = (bytes) => `${(bytes / 1024).toFixed(1)} KB`;
|
|
5430
|
+
function versionLines(detail) {
|
|
5431
|
+
const statusWidth = Math.max(...detail.versions.map((v) => v.status.length));
|
|
5432
|
+
return detail.versions.map(
|
|
5433
|
+
(v) => `v${String(v.number)} ${v.status.padEnd(statusWidth)} ${v.active ? "live" : " "} ${kib(v.size)} ${v.verifyJwt ? "verify on " : "verify off"} ${v.createdAt}`
|
|
5508
5434
|
);
|
|
5509
5435
|
}
|
|
5510
|
-
async function
|
|
5436
|
+
async function runFunctionsList(context) {
|
|
5511
5437
|
const client = await context.client();
|
|
5512
5438
|
const projectId = await context.resolveProjectId();
|
|
5513
|
-
const
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5439
|
+
const list = await client.functions.list(projectId);
|
|
5440
|
+
context.output.result({ projectId, ...list }, functionLines(list));
|
|
5441
|
+
}
|
|
5442
|
+
async function runFunctionsVersions(context) {
|
|
5443
|
+
const name = functionName(context, VERSIONS_USAGE);
|
|
5444
|
+
const client = await context.client();
|
|
5445
|
+
const projectId = await context.resolveProjectId();
|
|
5446
|
+
const detail = await client.functions.get(projectId, name);
|
|
5447
|
+
context.output.result({ projectId, ...detail }, versionLines(detail));
|
|
5448
|
+
}
|
|
5449
|
+
async function runFunctionsLogs(context) {
|
|
5450
|
+
if (context.flags.follow && context.flags.json)
|
|
5451
|
+
throw new NaroError("VALIDATION_FAILED", FOLLOW_JSON_REFUSAL);
|
|
5452
|
+
const name = functionName(context, LOGS_USAGE);
|
|
5453
|
+
const client = await context.client();
|
|
5454
|
+
const projectId = await context.resolveProjectId();
|
|
5455
|
+
const options = context.flags.level ? { level: context.flags.level } : {};
|
|
5456
|
+
const format = (entry) => printable(formatRuntimeLog(entry));
|
|
5457
|
+
if (!context.flags.follow) {
|
|
5458
|
+
const entries = await client.functions.logs(projectId, name, options);
|
|
5459
|
+
context.output.result(
|
|
5460
|
+
{ projectId, name, entries },
|
|
5461
|
+
entries.length > 0 ? entries.map(format) : [`(no logs from ${name} yet)`]
|
|
5462
|
+
);
|
|
5463
|
+
return;
|
|
5464
|
+
}
|
|
5465
|
+
context.output.log(`Following ${name}'s logs (Ctrl-C to stop)...`);
|
|
5466
|
+
await followRuntimeEntries(
|
|
5467
|
+
context,
|
|
5468
|
+
() => client.functions.logs(projectId, name, options),
|
|
5469
|
+
format
|
|
5470
|
+
);
|
|
5471
|
+
}
|
|
5472
|
+
var NEW_USAGE = "Usage: naro functions new <name>";
|
|
5473
|
+
var ROLLBACK_USAGE = "Usage: naro functions rollback <name> [<version>]";
|
|
5474
|
+
var DELETE_USAGE2 = "Usage: naro functions delete <name>";
|
|
5475
|
+
async function runFunctionsNew(context) {
|
|
5476
|
+
const name = functionName(context, NEW_USAGE);
|
|
5477
|
+
if (!isFunctionName(name))
|
|
5478
|
+
throw new NaroError("VALIDATION_FAILED", BUNDLE_MESSAGES.badName(name));
|
|
5479
|
+
const folder = functionsFolder({
|
|
5480
|
+
cwd: context.cwd,
|
|
5481
|
+
root: context.flags.root,
|
|
5482
|
+
dir: context.flags.dir
|
|
5483
|
+
});
|
|
5484
|
+
const target = join5(folder.dir, name);
|
|
5485
|
+
mkdirSync3(folder.dir, { recursive: true });
|
|
5486
|
+
try {
|
|
5487
|
+
mkdirSync3(target);
|
|
5488
|
+
} catch (error) {
|
|
5489
|
+
if (error.code !== "EEXIST") throw error;
|
|
5490
|
+
throw new NaroError(
|
|
5491
|
+
"VALIDATION_FAILED",
|
|
5492
|
+
`${folder.label}/${name} already exists.`
|
|
5493
|
+
);
|
|
5494
|
+
}
|
|
5495
|
+
writeFileSync4(join5(target, "index.ts"), scaffoldSource(name));
|
|
5496
|
+
const file = `${folder.label}/${name}/index.ts`;
|
|
5497
|
+
const notes = scaffoldNotes(folder);
|
|
5498
|
+
context.output.result({ file, notes }, [
|
|
5499
|
+
`\u2705 Created ${file}. Deploy it with naro functions deploy ${name}.`,
|
|
5500
|
+
...notes
|
|
5501
|
+
]);
|
|
5502
|
+
}
|
|
5503
|
+
async function runFunctionsDeploy(context) {
|
|
5504
|
+
assertFunctionsNode();
|
|
5505
|
+
const client = await context.client();
|
|
5506
|
+
const projectId = await context.resolveProjectId();
|
|
5507
|
+
const { deployed } = await deployFunctions({
|
|
5508
|
+
client,
|
|
5509
|
+
projectId,
|
|
5510
|
+
cwd: context.cwd,
|
|
5511
|
+
root: context.flags.root,
|
|
5512
|
+
dir: context.flags.dir,
|
|
5513
|
+
names: context.positionals,
|
|
5514
|
+
verifyJwt: context.flags.noVerifyJwt ? false : void 0,
|
|
5515
|
+
importMap: context.flags.importMap,
|
|
5516
|
+
skipCheck: context.flags.skipCheck,
|
|
5517
|
+
cliVersion: version(),
|
|
5518
|
+
log: (line) => context.output.log(line)
|
|
5519
|
+
});
|
|
5520
|
+
context.output.result({ projectId, functions: deployed });
|
|
5521
|
+
}
|
|
5522
|
+
async function runFunctionsRollback(context) {
|
|
5523
|
+
const name = functionName(context, ROLLBACK_USAGE);
|
|
5524
|
+
const written = context.positionals[1];
|
|
5525
|
+
if (written !== void 0 && !/^[1-9]\d*$/.test(written))
|
|
5526
|
+
throw new NaroError(
|
|
5527
|
+
"VALIDATION_FAILED",
|
|
5528
|
+
"The version must be a whole number from 1, like 3."
|
|
5529
|
+
);
|
|
5530
|
+
const client = await context.client();
|
|
5531
|
+
const projectId = await context.resolveProjectId();
|
|
5532
|
+
const io = {
|
|
5533
|
+
sleep: defaultSleep,
|
|
5534
|
+
log: (line) => context.output.log(line)
|
|
5535
|
+
};
|
|
5536
|
+
const live2 = await withFunctionRetries(
|
|
5537
|
+
() => client.functions.rollback(
|
|
5538
|
+
projectId,
|
|
5539
|
+
name,
|
|
5540
|
+
written === void 0 ? void 0 : Number(written)
|
|
5541
|
+
),
|
|
5542
|
+
name,
|
|
5543
|
+
io
|
|
5544
|
+
);
|
|
5545
|
+
context.output.result({ projectId, function: live2 }, [
|
|
5546
|
+
`\u2705 Rolled ${name} back to v${String(live2.version)} \u2014 ${live2.url}`
|
|
5547
|
+
]);
|
|
5548
|
+
}
|
|
5549
|
+
async function runFunctionsDelete(context) {
|
|
5550
|
+
const name = functionName(context, DELETE_USAGE2);
|
|
5551
|
+
const client = await context.client();
|
|
5552
|
+
const projectId = await context.resolveProjectId();
|
|
5553
|
+
const detail = await client.functions.get(projectId, name);
|
|
5554
|
+
const stored = detail.versions.filter((v) => v.status !== "pruned").length;
|
|
5555
|
+
await confirmOrAbort(
|
|
5556
|
+
context,
|
|
5557
|
+
stored === 1 ? `Delete function ${name}? Its URL stops working and its 1 stored version is removed.` : `Delete function ${name}? Its URL stops working and its ${String(stored)} stored versions are removed.`
|
|
5558
|
+
);
|
|
5559
|
+
const io = {
|
|
5560
|
+
sleep: defaultSleep,
|
|
5561
|
+
log: (line) => context.output.log(line)
|
|
5562
|
+
};
|
|
5563
|
+
await withFunctionRetries(
|
|
5564
|
+
() => client.functions.remove(projectId, name),
|
|
5565
|
+
name,
|
|
5566
|
+
io
|
|
5567
|
+
);
|
|
5568
|
+
context.output.result({ projectId, name, deleted: true }, [
|
|
5569
|
+
`\u2705 Deleted ${name}. Its URL stops working within seconds.`
|
|
5570
|
+
]);
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
// src/commands/functions-secrets.ts
|
|
5574
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5575
|
+
import { resolve as resolve6 } from "path";
|
|
5576
|
+
var SET_USAGE3 = "Usage: naro functions secrets set <NAME>";
|
|
5577
|
+
var IMPORT_USAGE = "Usage: naro functions secrets import <file>";
|
|
5578
|
+
var UNSET_USAGE = "Usage: naro functions secrets unset <NAME>\u2026";
|
|
5579
|
+
var NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5580
|
+
var NAME_MAX = 256;
|
|
5581
|
+
var VALUE_MAX_BYTES2 = 5120;
|
|
5582
|
+
var RESERVED = /* @__PURE__ */ new Set(["ASSETS", "DB"]);
|
|
5583
|
+
var PROTO_LINE = /^\s*(?:export\s+)?__proto__(?:\s*=|:\s)/m;
|
|
5584
|
+
function secretNameProblem(name) {
|
|
5585
|
+
if (!NAME.test(name) || name.length > NAME_MAX)
|
|
5586
|
+
return "Secret names must look like NAME: letters, digits and _, not starting with a digit.";
|
|
5587
|
+
if (name === "__proto__") return "__proto__ cannot be a secret name.";
|
|
5588
|
+
if (name.startsWith("NARO_") || RESERVED.has(name))
|
|
5589
|
+
return `${name} is reserved for Naro. Secret names cannot start with NARO_ or be ASSETS or DB.`;
|
|
5590
|
+
return null;
|
|
5591
|
+
}
|
|
5592
|
+
function secretValueProblem(name, value) {
|
|
5593
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
5594
|
+
return bytes > VALUE_MAX_BYTES2 ? `The value of ${name} is ${bytes.toLocaleString("en-US")} bytes; secrets must be 5,120 bytes or less.` : null;
|
|
5595
|
+
}
|
|
5596
|
+
function check(problem) {
|
|
5597
|
+
if (problem !== null) throw new NaroError("VALIDATION_FAILED", problem);
|
|
5598
|
+
}
|
|
5599
|
+
var plural = (n, one, many) => n === 1 ? one : many;
|
|
5600
|
+
function byteCount(value) {
|
|
5601
|
+
const n = Buffer.byteLength(value, "utf8");
|
|
5602
|
+
return `${n.toLocaleString("en-US")} ${plural(n, "byte", "bytes")}`;
|
|
5603
|
+
}
|
|
5604
|
+
function warnFailures(context, sync) {
|
|
5605
|
+
for (const failure2 of sync.failures)
|
|
5606
|
+
context.output.warn(
|
|
5607
|
+
` ${printable(failure2.name)}: ${printable(failure2.message)}`
|
|
5608
|
+
);
|
|
5609
|
+
}
|
|
5610
|
+
var nameList = (list) => list.map(printable).join(", ");
|
|
5611
|
+
async function applyChange(context, client, projectId, pending) {
|
|
5612
|
+
if (pending === 0)
|
|
5613
|
+
return { lines: ["No live functions to update."], sync: null };
|
|
5614
|
+
if (context.flags.noSync)
|
|
5615
|
+
return {
|
|
5616
|
+
lines: [
|
|
5617
|
+
pending === null ? "The functions carry the change once synced: the sweep does it within minutes, or run naro functions secrets sync." : (
|
|
5618
|
+
// The save counted before the control plane's own hook brought
|
|
5619
|
+
// some up: "up to", not how many are left.
|
|
5620
|
+
`Up to ${functionCount(pending)} still ${plural(pending, "runs", "run")} the old values: the sweep syncs them within minutes, or run naro functions secrets sync.`
|
|
5621
|
+
)
|
|
5622
|
+
],
|
|
5623
|
+
sync: null
|
|
5624
|
+
};
|
|
5625
|
+
if (pending === null)
|
|
5626
|
+
context.output.log(
|
|
5627
|
+
"\u26A0 Naro could not count the functions to update; syncing until none is left."
|
|
5628
|
+
);
|
|
5629
|
+
const total = pending;
|
|
5630
|
+
let last = null;
|
|
5631
|
+
let sync;
|
|
5632
|
+
try {
|
|
5633
|
+
sync = await syncUntilDone(client, projectId, {
|
|
5634
|
+
log: (line) => context.output.log(line),
|
|
5635
|
+
onCall: (progress) => {
|
|
5636
|
+
last = progress;
|
|
5637
|
+
if (total !== null)
|
|
5638
|
+
context.output.log(
|
|
5639
|
+
`\u23F3 Applying secrets to ${functionCount(total)}\u2026 ${String(Math.min(progress.synced.length, total))}/${String(total)}`
|
|
5640
|
+
);
|
|
5641
|
+
}
|
|
5642
|
+
});
|
|
5643
|
+
} catch (error) {
|
|
5644
|
+
if (last !== null) warnFailures(context, last);
|
|
5645
|
+
const cause = toNaroError(error);
|
|
5646
|
+
const retryAfter = cause.details?.retryAfter;
|
|
5647
|
+
throw new NaroError(
|
|
5648
|
+
"FUNCTIONS_SYNC_INCOMPLETE",
|
|
5649
|
+
`Secrets are saved, but the sync stopped: ${printable(cause.message)} Run \`naro functions secrets sync\` to finish.`,
|
|
5650
|
+
{
|
|
5651
|
+
details: {
|
|
5652
|
+
saved: true,
|
|
5653
|
+
cause: {
|
|
5654
|
+
code: cause.code,
|
|
5655
|
+
message: cause.message,
|
|
5656
|
+
...typeof retryAfter === "number" ? { retryAfter } : {}
|
|
5657
|
+
},
|
|
5658
|
+
pending
|
|
5659
|
+
},
|
|
5660
|
+
cause: error
|
|
5661
|
+
}
|
|
5662
|
+
);
|
|
5663
|
+
}
|
|
5664
|
+
if (!sync.complete) {
|
|
5665
|
+
warnFailures(context, sync);
|
|
5666
|
+
throw new NaroError(
|
|
5667
|
+
"FUNCTIONS_SYNC_INCOMPLETE",
|
|
5668
|
+
`Secrets are saved, but ${functionCount(sync.pending)} still ${plural(sync.pending, "runs", "run")} the old values. Run \`naro functions secrets sync\` to finish.`,
|
|
5669
|
+
{
|
|
5670
|
+
details: {
|
|
5671
|
+
saved: true,
|
|
5672
|
+
pending: sync.pending,
|
|
5673
|
+
failures: sync.failures,
|
|
5674
|
+
synced: sync.synced
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
);
|
|
5678
|
+
}
|
|
5679
|
+
const live2 = Math.max(total ?? 0, sync.synced.length);
|
|
5680
|
+
return {
|
|
5681
|
+
lines: [
|
|
5682
|
+
live2 === 0 ? "\u2705 All functions are up to date." : `\u2705 Secrets are live on all ${functionCount(live2)}.`
|
|
5683
|
+
],
|
|
5684
|
+
sync
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
5687
|
+
async function readyToChange(context) {
|
|
5688
|
+
const projectId = await context.resolveProjectId();
|
|
5689
|
+
const client = await context.client();
|
|
5690
|
+
await client.functions.list(projectId);
|
|
5691
|
+
return { client, projectId };
|
|
5692
|
+
}
|
|
5693
|
+
async function runFunctionsSecretsList(context) {
|
|
5694
|
+
const client = await context.client();
|
|
5695
|
+
const projectId = await context.resolveProjectId();
|
|
5696
|
+
const secrets = await client.functions.secrets(projectId);
|
|
5697
|
+
const width = Math.max(0, ...secrets.map((s) => s.name.length));
|
|
5698
|
+
context.output.result(
|
|
5699
|
+
{ projectId, secrets },
|
|
5700
|
+
secrets.length === 0 ? ["No function secrets. Set one with naro functions secrets set <NAME>."] : secrets.map(
|
|
5701
|
+
(s) => `${s.name.padEnd(width)} ${s.digest} ${s.updatedAt}`
|
|
5702
|
+
)
|
|
5703
|
+
);
|
|
5704
|
+
}
|
|
5705
|
+
async function runFunctionsSecretsSet(context) {
|
|
5706
|
+
const [name] = context.positionals;
|
|
5707
|
+
if (name === void 0) throw new NaroError("VALIDATION_FAILED", SET_USAGE3);
|
|
5708
|
+
if (context.positionals.length > 1 || name.includes("="))
|
|
5709
|
+
throw new NaroError("VALIDATION_FAILED", SECRETS_ARGV_REFUSAL);
|
|
5710
|
+
check(secretNameProblem(name));
|
|
5711
|
+
const typed = process.stdin.isTTY === true;
|
|
5712
|
+
let value;
|
|
5713
|
+
let client = null;
|
|
5714
|
+
let projectId = null;
|
|
5715
|
+
if (typed) {
|
|
5716
|
+
projectId = await context.resolveProjectId();
|
|
5717
|
+
client = await context.client();
|
|
5718
|
+
await client.functions.list(projectId);
|
|
5719
|
+
value = await context.prompter.askHidden(`? What's the value of ${name}? `);
|
|
5720
|
+
} else {
|
|
5721
|
+
value = await readStdin();
|
|
5722
|
+
}
|
|
5723
|
+
if (value.length === 0)
|
|
5724
|
+
throw new NaroError("VALIDATION_FAILED", `No value provided for ${name}.`);
|
|
5725
|
+
check(secretValueProblem(name, value));
|
|
5726
|
+
projectId ??= await context.resolveProjectId();
|
|
5727
|
+
client ??= await context.client();
|
|
5728
|
+
const saved = await client.functions.setSecrets(projectId, {
|
|
5729
|
+
set: { [name]: value }
|
|
5730
|
+
});
|
|
5731
|
+
context.output.log(`\u2705 Saved ${name}.`);
|
|
5732
|
+
const { lines, sync } = await applyChange(
|
|
5733
|
+
context,
|
|
5734
|
+
client,
|
|
5735
|
+
projectId,
|
|
5736
|
+
saved.pending
|
|
5737
|
+
);
|
|
5738
|
+
context.output.result(
|
|
5739
|
+
{ projectId, secrets: saved.secrets, pending: saved.pending, sync },
|
|
5740
|
+
lines
|
|
5741
|
+
);
|
|
5742
|
+
}
|
|
5743
|
+
async function runFunctionsSecretsImport(context) {
|
|
5744
|
+
const [file] = context.positionals;
|
|
5745
|
+
if (file === void 0)
|
|
5746
|
+
throw new NaroError("VALIDATION_FAILED", IMPORT_USAGE);
|
|
5747
|
+
let text;
|
|
5748
|
+
try {
|
|
5749
|
+
text = readFileSync6(resolve6(context.cwd, file), "utf8");
|
|
5750
|
+
} catch {
|
|
5751
|
+
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
5752
|
+
}
|
|
5753
|
+
const parsed = parseDotenvDetailed(text);
|
|
5754
|
+
const [unclosed] = parsed.unclosedQuotes;
|
|
5755
|
+
if (unclosed !== void 0)
|
|
5756
|
+
throw new NaroError(
|
|
5757
|
+
"VALIDATION_FAILED",
|
|
5758
|
+
`Unclosed quote in ${file} at line ${String(unclosed)}.`
|
|
5759
|
+
);
|
|
5760
|
+
if (PROTO_LINE.test(text)) check(secretNameProblem("__proto__"));
|
|
5761
|
+
const entries = Object.entries(parsed.vars);
|
|
5762
|
+
if (entries.length === 0)
|
|
5763
|
+
throw new NaroError("VALIDATION_FAILED", `No secrets found in ${file}.`);
|
|
5764
|
+
const invalid = entries.map(([name]) => name).filter((name) => !NAME.test(name) || name.length > NAME_MAX);
|
|
5765
|
+
if (invalid.length > 0)
|
|
5766
|
+
throw new NaroError(
|
|
5767
|
+
"VALIDATION_FAILED",
|
|
5768
|
+
`Invalid keys in ${file}: ${invalid.map(printable).join(", ")}.`
|
|
5769
|
+
);
|
|
5770
|
+
for (const [name, value] of entries) {
|
|
5771
|
+
check(secretNameProblem(name));
|
|
5772
|
+
check(secretValueProblem(name, value));
|
|
5773
|
+
}
|
|
5774
|
+
const { client, projectId } = await readyToChange(context);
|
|
5775
|
+
context.output.log(`Secrets in ${file}:`);
|
|
5776
|
+
for (const [name, value] of entries)
|
|
5777
|
+
context.output.log(
|
|
5778
|
+
` ${name.padEnd(32)} ${value === "" ? "(empty)" : `\u2022\u2022\u2022\u2022\u2022\u2022 (${byteCount(value)})`}`
|
|
5779
|
+
);
|
|
5780
|
+
await confirmOrAbort(
|
|
5781
|
+
context,
|
|
5782
|
+
`Set ${String(entries.length)} function ${plural(entries.length, "secret", "secrets")} for all of this project's functions?`
|
|
5783
|
+
);
|
|
5784
|
+
const saved = await client.functions.setSecrets(projectId, {
|
|
5785
|
+
set: Object.fromEntries(entries)
|
|
5786
|
+
});
|
|
5787
|
+
context.output.log(
|
|
5788
|
+
`\u2705 Saved ${String(entries.length)} ${plural(entries.length, "secret", "secrets")}.`
|
|
5789
|
+
);
|
|
5790
|
+
const { lines, sync } = await applyChange(
|
|
5791
|
+
context,
|
|
5792
|
+
client,
|
|
5793
|
+
projectId,
|
|
5794
|
+
saved.pending
|
|
5795
|
+
);
|
|
5796
|
+
context.output.result(
|
|
5797
|
+
{ projectId, secrets: saved.secrets, pending: saved.pending, sync },
|
|
5798
|
+
lines
|
|
5799
|
+
);
|
|
5800
|
+
}
|
|
5801
|
+
async function runFunctionsSecretsUnset(context) {
|
|
5802
|
+
const names = context.positionals;
|
|
5803
|
+
if (names.length === 0) throw new NaroError("VALIDATION_FAILED", UNSET_USAGE);
|
|
5804
|
+
for (const name of names) check(secretNameProblem(name));
|
|
5805
|
+
const { client, projectId } = await readyToChange(context);
|
|
5806
|
+
await confirmOrAbort(
|
|
5807
|
+
context,
|
|
5808
|
+
`Unset ${names.join(", ")} from all of this project's functions? Functions that read them get undefined once they are synced.`
|
|
5809
|
+
);
|
|
5810
|
+
const saved = await client.functions.setSecrets(projectId, {
|
|
5811
|
+
unset: [...names]
|
|
5812
|
+
});
|
|
5813
|
+
context.output.log(
|
|
5814
|
+
`\u2705 Unset ${String(names.length)} ${plural(names.length, "secret", "secrets")}.`
|
|
5815
|
+
);
|
|
5816
|
+
const { lines, sync } = await applyChange(
|
|
5817
|
+
context,
|
|
5818
|
+
client,
|
|
5819
|
+
projectId,
|
|
5820
|
+
saved.pending
|
|
5821
|
+
);
|
|
5822
|
+
context.output.result(
|
|
5823
|
+
{ projectId, secrets: saved.secrets, pending: saved.pending, sync },
|
|
5824
|
+
lines
|
|
5825
|
+
);
|
|
5826
|
+
}
|
|
5827
|
+
async function runFunctionsSecretsSync(context) {
|
|
5828
|
+
const client = await context.client();
|
|
5829
|
+
const projectId = await context.resolveProjectId();
|
|
5830
|
+
const registry = context.flags.registry === true;
|
|
5831
|
+
const sync = await syncUntilDone(client, projectId, {
|
|
5832
|
+
registry,
|
|
5833
|
+
log: (line) => context.output.log(line)
|
|
5834
|
+
});
|
|
5835
|
+
if (!sync.complete) {
|
|
5836
|
+
warnFailures(context, sync);
|
|
5837
|
+
throw new NaroError(
|
|
5838
|
+
"FUNCTIONS_SYNC_INCOMPLETE",
|
|
5839
|
+
`The sync stopped with ${sync.pending.toLocaleString("en-US")} left to do. Run \`naro functions secrets sync${registry ? " --registry" : ""}\` again.`,
|
|
5840
|
+
{
|
|
5841
|
+
details: {
|
|
5842
|
+
pending: sync.pending,
|
|
5843
|
+
failures: sync.failures,
|
|
5844
|
+
synced: sync.synced,
|
|
5845
|
+
removed: sync.removed
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
);
|
|
5849
|
+
}
|
|
5850
|
+
const lines = [];
|
|
5851
|
+
if (sync.synced.length > 0)
|
|
5852
|
+
lines.push(
|
|
5853
|
+
`Synced ${functionCount(sync.synced.length)}: ${nameList(sync.synced)}`
|
|
5854
|
+
);
|
|
5855
|
+
if (sync.removed.length > 0)
|
|
5856
|
+
lines.push(`Removed from the function list: ${nameList(sync.removed)}`);
|
|
5857
|
+
lines.push("\u2705 All functions are up to date.");
|
|
5858
|
+
context.output.result({ projectId, ...sync }, lines);
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
// src/commands/gen.ts
|
|
5862
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
5863
|
+
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
5864
|
+
var LANG_MESSAGE = "naro gen types writes TypeScript only. Pass --lang typescript, or leave --lang out.";
|
|
5865
|
+
async function runGenTypes(context) {
|
|
5866
|
+
const { lang } = context.flags;
|
|
5867
|
+
if (lang !== void 0 && lang !== "typescript")
|
|
5868
|
+
throw new NaroError("VALIDATION_FAILED", LANG_MESSAGE);
|
|
5869
|
+
const client = await dataClient(context);
|
|
5870
|
+
const projectId = await context.resolveProjectId();
|
|
5871
|
+
const tables = await client.db.tables(projectId);
|
|
5872
|
+
const read = await policiesForTypes(client, projectId);
|
|
5873
|
+
const types = generateTypes(
|
|
5874
|
+
tables,
|
|
5875
|
+
read.policies === void 0 ? {} : { policies: read.policies }
|
|
5876
|
+
);
|
|
5877
|
+
const undescribed = tables.flatMap(
|
|
5878
|
+
(table3) => table3.error === void 0 ? [] : [{ name: table3.name, error: table3.error }]
|
|
5879
|
+
);
|
|
5880
|
+
for (const { name, error } of undescribed)
|
|
5881
|
+
context.output.warn(
|
|
5882
|
+
`\u26A0 ${printable(name)} could not be described and was generated as an empty interface: ${printable(error)}`
|
|
5883
|
+
);
|
|
5884
|
+
if (read.policies === void 0)
|
|
5885
|
+
context.output.warn(
|
|
5886
|
+
`\u26A0 The backend's policies could not be read (${printable(read.reason)}), so owner columns stay required in Insert.`
|
|
5887
|
+
);
|
|
5888
|
+
const extra = {
|
|
5889
|
+
...undescribed.length > 0 ? { undescribed } : {},
|
|
5890
|
+
...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
|
|
5891
|
+
};
|
|
5892
|
+
if (context.flags.output) {
|
|
5893
|
+
const path = resolve7(context.cwd, context.flags.output);
|
|
5894
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
5895
|
+
writeFileSync5(path, types);
|
|
5896
|
+
context.output.result(
|
|
5897
|
+
{
|
|
5898
|
+
projectId,
|
|
5899
|
+
output: context.flags.output,
|
|
5900
|
+
tables: tables.length,
|
|
5901
|
+
...extra
|
|
5902
|
+
},
|
|
5903
|
+
[
|
|
5904
|
+
`\u2705 Wrote types for ${String(tables.length)} table(s) to ${context.flags.output}`
|
|
5905
|
+
]
|
|
5906
|
+
);
|
|
5907
|
+
return;
|
|
5908
|
+
}
|
|
5909
|
+
context.output.result({ projectId, types, ...extra }, [
|
|
5910
|
+
types.replace(/\n$/, "")
|
|
5911
|
+
]);
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
// src/commands/insights.ts
|
|
5915
|
+
function queryOf2(flags) {
|
|
5916
|
+
const filters = filtersOf(flags, SPEED_INSIGHTS_FILTER_KEYS);
|
|
5917
|
+
return {
|
|
5918
|
+
...flags.range !== void 0 ? { range: flags.range } : {},
|
|
5919
|
+
...flags.metric !== void 0 ? { metric: flags.metric } : {},
|
|
5920
|
+
...flags.env !== void 0 ? { environment: flags.env } : {},
|
|
5921
|
+
...Object.keys(filters).length > 0 ? { filters } : {}
|
|
5922
|
+
};
|
|
5923
|
+
}
|
|
5924
|
+
async function runInsights(context) {
|
|
5925
|
+
if (context.flags.referrer !== void 0 && context.flags.referrer !== "")
|
|
5926
|
+
throw new NaroError(
|
|
5927
|
+
"VALIDATION_FAILED",
|
|
5928
|
+
"Speed Insights has no referrer; drop --referrer."
|
|
5929
|
+
);
|
|
5930
|
+
const client = await context.client();
|
|
5931
|
+
const projectId = await context.resolveProjectId();
|
|
5932
|
+
const query = queryOf2(context.flags);
|
|
5933
|
+
const filters = query.filters ?? {};
|
|
5934
|
+
if (context.flags.route !== void 0) {
|
|
5935
|
+
const result = await client.insights.paths(projectId, {
|
|
5936
|
+
...query,
|
|
5937
|
+
route: context.flags.route
|
|
5938
|
+
});
|
|
5939
|
+
context.output.result(
|
|
5940
|
+
{ projectId, ...result },
|
|
5941
|
+
routePathsLines(result, filters)
|
|
5942
|
+
);
|
|
5943
|
+
return;
|
|
5944
|
+
}
|
|
5945
|
+
const overview = await client.insights.overview(projectId, query);
|
|
5946
|
+
if (overview.deploymentsError)
|
|
5947
|
+
context.output.warn(
|
|
5948
|
+
`Deployments are unavailable right now: ${printable(overview.deploymentsError.message)}`
|
|
5949
|
+
);
|
|
5950
|
+
context.output.result(
|
|
5951
|
+
{ projectId, ...overview },
|
|
5952
|
+
reportLines(overview.report, overview.deployments, filters)
|
|
5953
|
+
);
|
|
5954
|
+
}
|
|
5955
|
+
async function setInsights(context, enabled) {
|
|
5956
|
+
if (context.flags.env !== void 0)
|
|
5957
|
+
throw new NaroError(
|
|
5958
|
+
"VALIDATION_FAILED",
|
|
5959
|
+
"Speed Insights has one switch for production and preview; drop --env."
|
|
5960
|
+
);
|
|
5961
|
+
const filter = givenFilterFlag(context.flags);
|
|
5962
|
+
if (filter !== void 0)
|
|
5963
|
+
throw new NaroError(
|
|
5964
|
+
"VALIDATION_FAILED",
|
|
5965
|
+
`Filters only narrow a report; drop --${filter}.`
|
|
5966
|
+
);
|
|
5967
|
+
const client = await context.client();
|
|
5968
|
+
const projectId = await context.resolveProjectId();
|
|
5969
|
+
const result = await client.insights.setEnabled(projectId, enabled);
|
|
5970
|
+
context.output.result({ projectId, enabled: result.enabled }, [
|
|
5971
|
+
result.enabled ? TURNED_ON : TURNED_OFF
|
|
5972
|
+
]);
|
|
5973
|
+
}
|
|
5974
|
+
function runInsightsEnable(context) {
|
|
5975
|
+
return setInsights(context, true);
|
|
5976
|
+
}
|
|
5977
|
+
function runInsightsDisable(context) {
|
|
5978
|
+
return setInsights(context, false);
|
|
5979
|
+
}
|
|
5980
|
+
|
|
5981
|
+
// src/commands/projects.ts
|
|
5982
|
+
function productionUrl(subdomain) {
|
|
5983
|
+
return `https://${subdomain}.${new URL(controlPlaneUrl()).hostname}`;
|
|
5984
|
+
}
|
|
5985
|
+
async function runProjects(context) {
|
|
5986
|
+
const client = await context.client();
|
|
5987
|
+
const teams = await client.teams.list();
|
|
5988
|
+
const team = await resolveTeam(context, teams, { intent: "read" });
|
|
5989
|
+
const projects = await client.projects.list(team.slug);
|
|
5990
|
+
context.output.result(
|
|
5991
|
+
{
|
|
5992
|
+
team: team.slug,
|
|
5993
|
+
projects: projects.map((project) => ({
|
|
5994
|
+
id: project.id,
|
|
5995
|
+
name: project.name,
|
|
5996
|
+
subdomain: project.subdomain,
|
|
5997
|
+
url: productionUrl(project.subdomain),
|
|
5998
|
+
defaultBranch: project.defaultBranch,
|
|
5999
|
+
updatedAt: project.updatedAt
|
|
6000
|
+
}))
|
|
6001
|
+
},
|
|
6002
|
+
[
|
|
6003
|
+
`Projects in ${team.slug} (${String(projects.length)}):`,
|
|
6004
|
+
...projects.map(
|
|
6005
|
+
(project) => ` ${project.name.padEnd(24)} ${productionUrl(project.subdomain).padEnd(40)} ${project.id}`
|
|
6006
|
+
)
|
|
6007
|
+
]
|
|
6008
|
+
);
|
|
6009
|
+
}
|
|
6010
|
+
async function runProjectInspect(context) {
|
|
6011
|
+
const client = await context.client();
|
|
6012
|
+
const projectId = await context.resolveProjectId();
|
|
6013
|
+
const project = await client.projects.get(projectId);
|
|
6014
|
+
const url = productionUrl(project.subdomain);
|
|
6015
|
+
context.output.result({ project: { ...project, url } }, [
|
|
6016
|
+
`${project.name}`,
|
|
6017
|
+
` id: ${project.id}`,
|
|
6018
|
+
` team: ${project.teamId}`,
|
|
6019
|
+
` production URL: ${url}`,
|
|
6020
|
+
` preview URL: https://${project.subdomain}-preview.${new URL(controlPlaneUrl()).hostname}`,
|
|
6021
|
+
` default branch: ${project.defaultBranch}`,
|
|
5522
6022
|
` root directory: ${project.rootDirectory ?? "(repo root)"}`,
|
|
5523
6023
|
` repository: ${project.repositoryFullName ?? project.repositoryUrl}`
|
|
5524
6024
|
]);
|
|
@@ -5526,7 +6026,7 @@ async function runProjectInspect(context) {
|
|
|
5526
6026
|
|
|
5527
6027
|
// src/commands/promote.ts
|
|
5528
6028
|
var PROMOTE_USAGE = "Usage: naro promote <deployment-id> [--to <environment>]";
|
|
5529
|
-
var
|
|
6029
|
+
var ROLLBACK_USAGE2 = "Usage: naro rollback [deployment-id]";
|
|
5530
6030
|
var REBUILD_TIMEOUT_MS = 15 * 6e4;
|
|
5531
6031
|
var REUPLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
5532
6032
|
async function promoteAndWait(context, client, project, sourceDeploymentId, action, target) {
|
|
@@ -5590,9 +6090,9 @@ function refuseWhatIsNotRead(context, usage, envMessage) {
|
|
|
5590
6090
|
throw new NaroError("VALIDATION_FAILED", envMessage);
|
|
5591
6091
|
}
|
|
5592
6092
|
async function previewPromotion(context, client, projectId, sourceDeploymentId, target) {
|
|
5593
|
-
let
|
|
6093
|
+
let check2;
|
|
5594
6094
|
try {
|
|
5595
|
-
|
|
6095
|
+
check2 = await client.deployments.promoteCheck(
|
|
5596
6096
|
projectId,
|
|
5597
6097
|
sourceDeploymentId,
|
|
5598
6098
|
target
|
|
@@ -5604,9 +6104,9 @@ async function previewPromotion(context, client, projectId, sourceDeploymentId,
|
|
|
5604
6104
|
return;
|
|
5605
6105
|
}
|
|
5606
6106
|
context.output.log(
|
|
5607
|
-
|
|
6107
|
+
check2.mode === "rebuild" ? `\u25B8 rebuild: builds the same commit again with ${target ?? "production"}'s variables` : "\u25B8 reupload: publishes the stored build"
|
|
5608
6108
|
);
|
|
5609
|
-
for (const warning of
|
|
6109
|
+
for (const warning of check2.warnings)
|
|
5610
6110
|
for (const line of warningLines(warning)) context.output.log(line);
|
|
5611
6111
|
}
|
|
5612
6112
|
async function runPromote(context) {
|
|
@@ -5648,7 +6148,7 @@ async function runRollback(context) {
|
|
|
5648
6148
|
);
|
|
5649
6149
|
refuseWhatIsNotRead(
|
|
5650
6150
|
context,
|
|
5651
|
-
|
|
6151
|
+
ROLLBACK_USAGE2,
|
|
5652
6152
|
"--env is not an option of naro rollback: naro rollback always rolls production back."
|
|
5653
6153
|
);
|
|
5654
6154
|
const client = await context.client();
|
|
@@ -5731,6 +6231,9 @@ function tableName(table3) {
|
|
|
5731
6231
|
const name = printable(table3.name);
|
|
5732
6232
|
return table3.exists ? name : `${name} (dropped)`;
|
|
5733
6233
|
}
|
|
6234
|
+
function linesFor(lines, environment) {
|
|
6235
|
+
return lines.map((line) => forEnvironment(line, environment));
|
|
6236
|
+
}
|
|
5734
6237
|
function realtimeLines(info) {
|
|
5735
6238
|
if (!info.enabled)
|
|
5736
6239
|
return [
|
|
@@ -5744,14 +6247,14 @@ function realtimeLines(info) {
|
|
|
5744
6247
|
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
6248
|
];
|
|
5746
6249
|
}
|
|
5747
|
-
async function enableRealtime(client, projectId) {
|
|
6250
|
+
async function enableRealtime(client, projectId, environment) {
|
|
5748
6251
|
try {
|
|
5749
6252
|
return await client.realtime.enable(projectId);
|
|
5750
6253
|
} catch (error) {
|
|
5751
6254
|
if (isNaroError(error) && error.code === "TIMEOUT")
|
|
5752
6255
|
throw new NaroError(
|
|
5753
6256
|
"TIMEOUT",
|
|
5754
|
-
|
|
6257
|
+
`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
6258
|
{ cause: error }
|
|
5756
6259
|
);
|
|
5757
6260
|
throw error;
|
|
@@ -5763,28 +6266,41 @@ function tableNames2(context, usage) {
|
|
|
5763
6266
|
return context.positionals;
|
|
5764
6267
|
}
|
|
5765
6268
|
async function runRealtimeStatus(context) {
|
|
5766
|
-
const client = await context
|
|
6269
|
+
const client = await dataClient(context);
|
|
5767
6270
|
const projectId = await context.resolveProjectId();
|
|
5768
6271
|
const realtime = await client.realtime.info(projectId);
|
|
5769
|
-
context.output.result(
|
|
6272
|
+
context.output.result(
|
|
6273
|
+
{ projectId, realtime },
|
|
6274
|
+
linesFor(realtimeLines(realtime), dataEnvironment(context.flags))
|
|
6275
|
+
);
|
|
5770
6276
|
}
|
|
5771
6277
|
async function runRealtimeEnable(context) {
|
|
5772
|
-
const client = await context
|
|
6278
|
+
const client = await dataClient(context);
|
|
5773
6279
|
const projectId = await context.resolveProjectId();
|
|
5774
|
-
const
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
6280
|
+
const environment = dataEnvironment(context.flags);
|
|
6281
|
+
const realtime = await enableRealtime(client, projectId, environment);
|
|
6282
|
+
context.output.result(
|
|
6283
|
+
{ projectId, realtime },
|
|
6284
|
+
linesFor(
|
|
6285
|
+
[
|
|
6286
|
+
"\u2705 Realtime is on. Publish a table with naro realtime tables add <table>.",
|
|
6287
|
+
...realtimeLines(realtime)
|
|
6288
|
+
],
|
|
6289
|
+
environment
|
|
6290
|
+
)
|
|
6291
|
+
);
|
|
5779
6292
|
}
|
|
5780
6293
|
async function runRealtimeTablesList(context) {
|
|
5781
|
-
const client = await context
|
|
6294
|
+
const client = await dataClient(context);
|
|
5782
6295
|
const projectId = await context.resolveProjectId();
|
|
5783
6296
|
const tables = await client.realtime.listTables(projectId);
|
|
5784
6297
|
context.output.result(
|
|
5785
6298
|
{ projectId, tables },
|
|
5786
6299
|
tables.length === 0 ? [
|
|
5787
|
-
|
|
6300
|
+
forEnvironment(
|
|
6301
|
+
"No published tables. `naro realtime tables add <table>` publishes one.",
|
|
6302
|
+
dataEnvironment(context.flags)
|
|
6303
|
+
)
|
|
5788
6304
|
] : tables.map(
|
|
5789
6305
|
(table3) => table3.exists ? ` ${printable(table3.name)}` : ` ${printable(table3.name)} (dropped: no such table now)`
|
|
5790
6306
|
)
|
|
@@ -5792,7 +6308,7 @@ async function runRealtimeTablesList(context) {
|
|
|
5792
6308
|
}
|
|
5793
6309
|
async function runRealtimeTablesAdd(context) {
|
|
5794
6310
|
const names = tableNames2(context, ADD_USAGE);
|
|
5795
|
-
const client = await context
|
|
6311
|
+
const client = await dataClient(context);
|
|
5796
6312
|
const projectId = await context.resolveProjectId();
|
|
5797
6313
|
const tables = [];
|
|
5798
6314
|
for (const name of names) {
|
|
@@ -5814,7 +6330,7 @@ async function runRealtimeTablesRemove(context) {
|
|
|
5814
6330
|
context,
|
|
5815
6331
|
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
6332
|
);
|
|
5817
|
-
const client = await context
|
|
6333
|
+
const client = await dataClient(context);
|
|
5818
6334
|
const projectId = await context.resolveProjectId();
|
|
5819
6335
|
const tables = [];
|
|
5820
6336
|
for (const name of names) {
|
|
@@ -5934,6 +6450,9 @@ function meterLines(info) {
|
|
|
5934
6450
|
);
|
|
5935
6451
|
return lines;
|
|
5936
6452
|
}
|
|
6453
|
+
function linesFor2(lines, environment) {
|
|
6454
|
+
return lines.map((line) => forEnvironment(line, environment));
|
|
6455
|
+
}
|
|
5937
6456
|
function storageLines(info) {
|
|
5938
6457
|
if (info.disabledAt !== null)
|
|
5939
6458
|
return [
|
|
@@ -5957,27 +6476,27 @@ function storageLines(info) {
|
|
|
5957
6476
|
...info.buckets.map(bucketLine)
|
|
5958
6477
|
];
|
|
5959
6478
|
}
|
|
5960
|
-
async function enableStorage(client, projectId) {
|
|
6479
|
+
async function enableStorage(client, projectId, environment = null) {
|
|
5961
6480
|
try {
|
|
5962
6481
|
return await client.storage.enable(projectId);
|
|
5963
6482
|
} catch (error) {
|
|
5964
6483
|
if (isNaroError(error) && error.code === "TIMEOUT")
|
|
5965
6484
|
throw new NaroError(
|
|
5966
6485
|
"TIMEOUT",
|
|
5967
|
-
|
|
6486
|
+
`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
6487
|
{ cause: error }
|
|
5969
6488
|
);
|
|
5970
6489
|
throw error;
|
|
5971
6490
|
}
|
|
5972
6491
|
}
|
|
5973
|
-
async function disableStorage(client, projectId) {
|
|
6492
|
+
async function disableStorage(client, projectId, environment = null) {
|
|
5974
6493
|
try {
|
|
5975
6494
|
return await client.storage.disable(projectId);
|
|
5976
6495
|
} catch (error) {
|
|
5977
6496
|
if (isNaroError(error) && error.code === "TIMEOUT")
|
|
5978
6497
|
throw new NaroError(
|
|
5979
6498
|
"TIMEOUT",
|
|
5980
|
-
|
|
6499
|
+
`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
6500
|
{ cause: error }
|
|
5982
6501
|
);
|
|
5983
6502
|
throw error;
|
|
@@ -6004,23 +6523,34 @@ function bucketFields(flags) {
|
|
|
6004
6523
|
return fields;
|
|
6005
6524
|
}
|
|
6006
6525
|
async function runStorageStatus(context) {
|
|
6007
|
-
const client = await context
|
|
6526
|
+
const client = await dataClient(context);
|
|
6008
6527
|
const projectId = await context.resolveProjectId();
|
|
6009
6528
|
const storage = await client.storage.info(projectId);
|
|
6010
|
-
context.output.result(
|
|
6529
|
+
context.output.result(
|
|
6530
|
+
{ projectId, storage },
|
|
6531
|
+
linesFor2(storageLines(storage), dataEnvironment(context.flags))
|
|
6532
|
+
);
|
|
6011
6533
|
}
|
|
6012
6534
|
async function runStorageEnable(context) {
|
|
6013
|
-
const client = await context
|
|
6535
|
+
const client = await dataClient(context);
|
|
6014
6536
|
const projectId = await context.resolveProjectId();
|
|
6015
|
-
const
|
|
6537
|
+
const environment = dataEnvironment(context.flags);
|
|
6538
|
+
const storage = await enableStorage(client, projectId, environment);
|
|
6016
6539
|
context.output.result({ projectId, storage }, [
|
|
6017
6540
|
`\u2705 Storage is on: R2 bucket ${printable(storage.bucket ?? "unknown")}.`,
|
|
6018
|
-
...
|
|
6019
|
-
|
|
6541
|
+
...linesFor2(
|
|
6542
|
+
[
|
|
6543
|
+
...storage.usage === null ? [
|
|
6544
|
+
"\u23F3 The backend is still finishing; check `naro backend` in a minute."
|
|
6545
|
+
] : [],
|
|
6546
|
+
...storageLines(storage)
|
|
6547
|
+
],
|
|
6548
|
+
environment
|
|
6549
|
+
)
|
|
6020
6550
|
]);
|
|
6021
6551
|
}
|
|
6022
6552
|
async function runStorageDisable(context) {
|
|
6023
|
-
const client = await context
|
|
6553
|
+
const client = await dataClient(context);
|
|
6024
6554
|
const projectId = await context.resolveProjectId();
|
|
6025
6555
|
const current = await client.storage.info(projectId);
|
|
6026
6556
|
if (current.bucket === null) {
|
|
@@ -6029,27 +6559,34 @@ async function runStorageDisable(context) {
|
|
|
6029
6559
|
]);
|
|
6030
6560
|
return;
|
|
6031
6561
|
}
|
|
6562
|
+
const environment = dataEnvironment(context.flags);
|
|
6032
6563
|
if (current.disabledAt === null)
|
|
6033
|
-
await confirmOrAbort(
|
|
6034
|
-
|
|
6564
|
+
await confirmOrAbort(
|
|
6565
|
+
context,
|
|
6566
|
+
forEnvironment(DISABLE_QUESTION3, environment)
|
|
6567
|
+
);
|
|
6568
|
+
const storage = await disableStorage(client, projectId, environment);
|
|
6035
6569
|
context.output.result({ projectId, storage }, [
|
|
6036
|
-
|
|
6037
|
-
...storageLines(storage)
|
|
6570
|
+
`\u2705 Storage is off. Buckets and files are kept; naro storage enable${envFlag(environment)} turns it on again.`,
|
|
6571
|
+
...linesFor2(storageLines(storage), environment)
|
|
6038
6572
|
]);
|
|
6039
6573
|
}
|
|
6040
6574
|
async function runStorageBucketList(context) {
|
|
6041
|
-
const client = await context
|
|
6575
|
+
const client = await dataClient(context);
|
|
6042
6576
|
const projectId = await context.resolveProjectId();
|
|
6043
6577
|
const buckets = await client.storage.listBuckets(projectId);
|
|
6044
6578
|
context.output.result(
|
|
6045
6579
|
{ projectId, buckets },
|
|
6046
|
-
|
|
6580
|
+
linesFor2(
|
|
6581
|
+
buckets.length === 0 ? [NO_BUCKETS] : buckets.map(bucketLine),
|
|
6582
|
+
dataEnvironment(context.flags)
|
|
6583
|
+
)
|
|
6047
6584
|
);
|
|
6048
6585
|
}
|
|
6049
6586
|
async function runStorageBucketCreate(context) {
|
|
6050
6587
|
const name = bucketName(context, CREATE_USAGE3);
|
|
6051
6588
|
const input = { name, ...bucketFields(context.flags) };
|
|
6052
|
-
const client = await context
|
|
6589
|
+
const client = await dataClient(context);
|
|
6053
6590
|
const projectId = await context.resolveProjectId();
|
|
6054
6591
|
let bucket;
|
|
6055
6592
|
try {
|
|
@@ -6058,12 +6595,12 @@ async function runStorageBucketCreate(context) {
|
|
|
6058
6595
|
if (!isNaroError(error) || error.code !== "STORAGE_NOT_ENABLED")
|
|
6059
6596
|
throw error;
|
|
6060
6597
|
context.output.log(ENABLE_FIRST);
|
|
6061
|
-
await enableStorage(client, projectId);
|
|
6598
|
+
await enableStorage(client, projectId, dataEnvironment(context.flags));
|
|
6062
6599
|
bucket = await client.storage.createBucket(projectId, input);
|
|
6063
6600
|
}
|
|
6064
6601
|
context.output.result({ projectId, bucket }, [
|
|
6065
6602
|
`\u2705 Created bucket ${printable(bucket.name)}.`,
|
|
6066
|
-
bucketLine(bucket)
|
|
6603
|
+
forEnvironment(bucketLine(bucket), dataEnvironment(context.flags))
|
|
6067
6604
|
]);
|
|
6068
6605
|
}
|
|
6069
6606
|
async function runStorageBucketUpdate(context) {
|
|
@@ -6074,13 +6611,13 @@ async function runStorageBucketUpdate(context) {
|
|
|
6074
6611
|
context,
|
|
6075
6612
|
`Make bucket ${name} public? Anyone with an object's URL can download it.`
|
|
6076
6613
|
);
|
|
6077
|
-
const client = await context
|
|
6614
|
+
const client = await dataClient(context);
|
|
6078
6615
|
const projectId = await context.resolveProjectId();
|
|
6079
6616
|
const bucket = await client.storage.updateBucket(projectId, name, patch);
|
|
6080
6617
|
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
6618
|
context.output.result({ projectId, bucket }, [
|
|
6082
6619
|
`\u2705 Updated bucket ${printable(bucket.name)}. ${takesEffect}`,
|
|
6083
|
-
bucketLine(bucket)
|
|
6620
|
+
forEnvironment(bucketLine(bucket), dataEnvironment(context.flags))
|
|
6084
6621
|
]);
|
|
6085
6622
|
}
|
|
6086
6623
|
function refuseSearch(context, why) {
|
|
@@ -6116,7 +6653,7 @@ async function runStorageBucketEmpty(context) {
|
|
|
6116
6653
|
context,
|
|
6117
6654
|
`bucket empty deletes every object in the bucket. ${EMPTY_USAGE}`
|
|
6118
6655
|
);
|
|
6119
|
-
const client = await context
|
|
6656
|
+
const client = await dataClient(context);
|
|
6120
6657
|
const projectId = await context.resolveProjectId();
|
|
6121
6658
|
const bucket = await client.storage.getBucket(projectId, name);
|
|
6122
6659
|
await confirmOrAbort(
|
|
@@ -6139,7 +6676,7 @@ async function runStorageBucketRemove(context) {
|
|
|
6139
6676
|
context,
|
|
6140
6677
|
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
6678
|
);
|
|
6142
|
-
const client = await context
|
|
6679
|
+
const client = await dataClient(context);
|
|
6143
6680
|
const projectId = await context.resolveProjectId();
|
|
6144
6681
|
let question = `Remove bucket ${name}?`;
|
|
6145
6682
|
if (empty) {
|
|
@@ -6164,7 +6701,7 @@ async function runStorageBucketRemove(context) {
|
|
|
6164
6701
|
import { randomUUID } from "crypto";
|
|
6165
6702
|
import { createWriteStream as createWriteStream2, openAsBlob, rmSync as rmSync2 } from "fs";
|
|
6166
6703
|
import { rename, rm, stat } from "fs/promises";
|
|
6167
|
-
import { basename as basename2, dirname as
|
|
6704
|
+
import { basename as basename2, dirname as dirname4, extname, join as join6, resolve as resolve8 } from "path";
|
|
6168
6705
|
import { Readable } from "stream";
|
|
6169
6706
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
6170
6707
|
|
|
@@ -6297,6 +6834,17 @@ function serviceKeyOffServer() {
|
|
|
6297
6834
|
message: SERVICE_KEY_OFF_SERVER_MESSAGE
|
|
6298
6835
|
};
|
|
6299
6836
|
}
|
|
6837
|
+
function accessTokenUnavailable(cause) {
|
|
6838
|
+
return {
|
|
6839
|
+
code: "ACCESS_TOKEN_UNAVAILABLE",
|
|
6840
|
+
message: "createClient's accessToken function failed or returned something that is not a string or null, so the request was not sent.",
|
|
6841
|
+
details: { cause: describe(cause) }
|
|
6842
|
+
};
|
|
6843
|
+
}
|
|
6844
|
+
function accessTokenAuthMessage(name) {
|
|
6845
|
+
return `naro.auth.${name} is not available on a client made with accessToken: its token comes from your accessToken function. Use a client made without accessToken to sign in.`;
|
|
6846
|
+
}
|
|
6847
|
+
var ACCESS_TOKEN_CHANNEL_MESSAGE = "naro.channel() is not available on a client made with accessToken: Realtime signs in with a session, not an access token. Broadcast from a function with naroAdmin.";
|
|
6300
6848
|
function notJoined(channel) {
|
|
6301
6849
|
return {
|
|
6302
6850
|
code: "NOT_JOINED",
|
|
@@ -6362,9 +6910,139 @@ var SESSION_PREFIXES = ["/auth/", "/realtime/"];
|
|
|
6362
6910
|
function storeFor(config, path) {
|
|
6363
6911
|
return SESSION_PREFIXES.some((prefix) => path.startsWith(prefix)) ? config.tokens : config.accessJwt ?? null;
|
|
6364
6912
|
}
|
|
6365
|
-
|
|
6366
|
-
|
|
6913
|
+
var ACCESS_JWT_MARGIN_MS = 6e4;
|
|
6914
|
+
function deadlineOf(timeoutMs) {
|
|
6915
|
+
return timeoutMs === void 0 ? null : { at: Date.now() + timeoutMs, ms: timeoutMs };
|
|
6916
|
+
}
|
|
6917
|
+
function remainingMs(deadline) {
|
|
6918
|
+
return Math.max(0, deadline.at - Date.now());
|
|
6919
|
+
}
|
|
6920
|
+
var EXPIRED = Symbol("expired");
|
|
6921
|
+
function timedOut(deadline) {
|
|
6922
|
+
return requestTimeout(deadline?.ms ?? 0);
|
|
6923
|
+
}
|
|
6924
|
+
function timesOf(jwt) {
|
|
6925
|
+
const payload = jwt.split(".")[1];
|
|
6926
|
+
if (payload === void 0 || payload === "") return null;
|
|
6927
|
+
try {
|
|
6928
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
6929
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
6930
|
+
const claims = JSON.parse(atob(padded));
|
|
6931
|
+
if (!isRecord(claims)) return null;
|
|
6932
|
+
const { exp, iat } = claims;
|
|
6933
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
|
|
6934
|
+
return {
|
|
6935
|
+
exp: exp * 1e3,
|
|
6936
|
+
iat: typeof iat === "number" && Number.isFinite(iat) ? iat * 1e3 : null
|
|
6937
|
+
};
|
|
6938
|
+
} catch {
|
|
6939
|
+
return null;
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
function expiryOf(jwt) {
|
|
6943
|
+
return timesOf(jwt)?.exp ?? null;
|
|
6944
|
+
}
|
|
6945
|
+
var skews = /* @__PURE__ */ new WeakMap();
|
|
6946
|
+
function localExpiry(config, jwt) {
|
|
6947
|
+
const exp = expiryOf(jwt);
|
|
6948
|
+
return exp === null ? null : exp - (skews.get(config.tokens) ?? 0);
|
|
6949
|
+
}
|
|
6950
|
+
var SESSION_GONE_CODES = /* @__PURE__ */ new Set([
|
|
6951
|
+
"UNAUTHORIZED",
|
|
6952
|
+
"INVALID_SESSION",
|
|
6953
|
+
"FAILED_TO_GET_SESSION"
|
|
6954
|
+
]);
|
|
6955
|
+
var renewing = /* @__PURE__ */ new WeakMap();
|
|
6956
|
+
function renewAccessJwt(config, jwtStore, session) {
|
|
6957
|
+
const pending = renewing.get(config.tokens);
|
|
6958
|
+
if (pending?.session === session) return pending.done;
|
|
6959
|
+
const done = (async () => {
|
|
6960
|
+
const result = await callWith(
|
|
6961
|
+
config,
|
|
6962
|
+
{ method: "GET", path: "/auth/token", withToken: true },
|
|
6963
|
+
void 0,
|
|
6964
|
+
session
|
|
6965
|
+
);
|
|
6966
|
+
if (result.error !== null) {
|
|
6967
|
+
if (result.answer?.status !== 401 || !SESSION_GONE_CODES.has(result.error.code))
|
|
6968
|
+
return result.error;
|
|
6969
|
+
await config.tokens.clearIf(session);
|
|
6970
|
+
await config.tokens.whileIs(null, () => jwtStore.clear());
|
|
6971
|
+
return invalidSession();
|
|
6972
|
+
}
|
|
6973
|
+
const { body, status } = result.answer;
|
|
6974
|
+
const jwt = isRecord(body) ? body.token : void 0;
|
|
6975
|
+
if (typeof jwt !== "string" || jwt === "") return invalidResponse(status);
|
|
6976
|
+
const arrived = Date.now();
|
|
6977
|
+
await config.tokens.whileIs(session, async () => {
|
|
6978
|
+
const iat = timesOf(jwt)?.iat;
|
|
6979
|
+
if (iat !== void 0 && iat !== null)
|
|
6980
|
+
skews.set(config.tokens, iat - arrived);
|
|
6981
|
+
await jwtStore.set(jwt);
|
|
6982
|
+
});
|
|
6983
|
+
return null;
|
|
6984
|
+
})();
|
|
6985
|
+
const entry = { session, done };
|
|
6986
|
+
renewing.set(config.tokens, entry);
|
|
6987
|
+
void done.finally(() => {
|
|
6988
|
+
if (renewing.get(config.tokens) === entry) renewing.delete(config.tokens);
|
|
6989
|
+
});
|
|
6990
|
+
return done;
|
|
6991
|
+
}
|
|
6992
|
+
async function accessJwtFor(config, jwtStore) {
|
|
6993
|
+
for (let round = 0; round < 2; round += 1) {
|
|
6994
|
+
const session = await config.tokens.get();
|
|
6995
|
+
const jwt = await jwtStore.get();
|
|
6996
|
+
if (session === null || config.service) return { token: jwt, session };
|
|
6997
|
+
const expiry = jwt === null ? null : localExpiry(config, jwt);
|
|
6998
|
+
if (jwt !== null && (expiry === null || expiry - ACCESS_JWT_MARGIN_MS > Date.now()))
|
|
6999
|
+
return { token: jwt, session };
|
|
7000
|
+
const error = await renewAccessJwt(config, jwtStore, session);
|
|
7001
|
+
const stored = await config.tokens.get();
|
|
7002
|
+
if (stored !== session) {
|
|
7003
|
+
if (stored === null) return error ? { error } : { token: null, session };
|
|
7004
|
+
continue;
|
|
7005
|
+
}
|
|
7006
|
+
if (error === null) {
|
|
7007
|
+
const renewed = await jwtStore.get();
|
|
7008
|
+
return renewed === null ? { error: invalidSession() } : { token: renewed, session };
|
|
7009
|
+
}
|
|
7010
|
+
if (jwt !== null && expiry !== null && expiry > Date.now())
|
|
7011
|
+
return { token: jwt, session };
|
|
7012
|
+
return { error };
|
|
7013
|
+
}
|
|
7014
|
+
return { error: invalidSession() };
|
|
7015
|
+
}
|
|
7016
|
+
async function bearerFor(config, path, withToken) {
|
|
7017
|
+
if (!withToken) return { token: null, session: null };
|
|
7018
|
+
const store = storeFor(config, path);
|
|
7019
|
+
if (config.accessToken !== void 0 && store !== config.tokens) {
|
|
7020
|
+
try {
|
|
7021
|
+
return { token: await config.accessToken.get(), session: null };
|
|
7022
|
+
} catch (cause) {
|
|
7023
|
+
return { error: accessTokenUnavailable(cause) };
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
if (store === null) return { token: null, session: null };
|
|
7027
|
+
if (store === config.tokens) {
|
|
7028
|
+
const token = await store.get();
|
|
7029
|
+
return { token, session: token };
|
|
7030
|
+
}
|
|
7031
|
+
return accessJwtFor(config, store);
|
|
7032
|
+
}
|
|
7033
|
+
async function retryBearer(config, path, error, first) {
|
|
7034
|
+
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)
|
|
7035
|
+
return null;
|
|
7036
|
+
const renewed = await bearerFor(config, path, true);
|
|
7037
|
+
if ("error" in renewed) return renewed;
|
|
7038
|
+
return renewed.token === null || renewed.token === first.token || renewed.session !== first.session ? null : renewed;
|
|
7039
|
+
}
|
|
7040
|
+
async function receive(config, options, init, deadline) {
|
|
7041
|
+
if (deadline !== null && remainingMs(deadline) === 0)
|
|
7042
|
+
return { error: timedOut(deadline) };
|
|
7043
|
+
const controller = deadline !== null && typeof AbortController === "function" ? new AbortController() : null;
|
|
6367
7044
|
if (controller) init.signal = controller.signal;
|
|
7045
|
+
let gaveUp = false;
|
|
6368
7046
|
const request = (async () => {
|
|
6369
7047
|
try {
|
|
6370
7048
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
@@ -6372,6 +7050,10 @@ async function receive(config, options, init) {
|
|
|
6372
7050
|
`${config.baseUrl}/_naro/v1${options.path}`,
|
|
6373
7051
|
init
|
|
6374
7052
|
);
|
|
7053
|
+
if (gaveUp) {
|
|
7054
|
+
discard(response);
|
|
7055
|
+
return { error: timedOut(deadline) };
|
|
7056
|
+
}
|
|
6375
7057
|
return {
|
|
6376
7058
|
status: response.status,
|
|
6377
7059
|
headers: response.headers,
|
|
@@ -6381,14 +7063,14 @@ async function receive(config, options, init) {
|
|
|
6381
7063
|
return { error: networkError(error) };
|
|
6382
7064
|
}
|
|
6383
7065
|
})();
|
|
6384
|
-
|
|
6385
|
-
if (timeoutMs === void 0) return request;
|
|
7066
|
+
if (deadline === null) return request;
|
|
6386
7067
|
let timer;
|
|
6387
|
-
const expired = new Promise((
|
|
7068
|
+
const expired = new Promise((resolve9) => {
|
|
6388
7069
|
timer = setTimeout(() => {
|
|
7070
|
+
gaveUp = true;
|
|
6389
7071
|
controller?.abort();
|
|
6390
|
-
|
|
6391
|
-
},
|
|
7072
|
+
resolve9({ error: timedOut(deadline) });
|
|
7073
|
+
}, remainingMs(deadline));
|
|
6392
7074
|
});
|
|
6393
7075
|
try {
|
|
6394
7076
|
return await Promise.race([request, expired]);
|
|
@@ -6405,8 +7087,35 @@ async function call(config, options) {
|
|
|
6405
7087
|
return { error: invalidQuery("", "is not JSON"), answer: null };
|
|
6406
7088
|
}
|
|
6407
7089
|
}
|
|
7090
|
+
const deadline = deadlineOf(options.timeoutMs);
|
|
7091
|
+
const bearer = await untilStopped(
|
|
7092
|
+
bearerFor(config, options.path, options.withToken),
|
|
7093
|
+
void 0,
|
|
7094
|
+
deadline
|
|
7095
|
+
);
|
|
7096
|
+
if (bearer === EXPIRED || bearer === ABORTED)
|
|
7097
|
+
return { error: timedOut(deadline), answer: null };
|
|
7098
|
+
if ("error" in bearer) return { error: bearer.error, answer: null };
|
|
7099
|
+
const result = await callWith(
|
|
7100
|
+
config,
|
|
7101
|
+
options,
|
|
7102
|
+
payload,
|
|
7103
|
+
bearer.token,
|
|
7104
|
+
deadline
|
|
7105
|
+
);
|
|
7106
|
+
const again = await untilStopped(
|
|
7107
|
+
retryBearer(config, options.path, result.error, bearer),
|
|
7108
|
+
void 0,
|
|
7109
|
+
deadline
|
|
7110
|
+
);
|
|
7111
|
+
if (again === EXPIRED || again === ABORTED)
|
|
7112
|
+
return { error: timedOut(deadline), answer: null };
|
|
7113
|
+
if (again === null) return result;
|
|
7114
|
+
if ("error" in again) return { error: again.error, answer: null };
|
|
7115
|
+
return callWith(config, options, payload, again.token, deadline);
|
|
7116
|
+
}
|
|
7117
|
+
async function callWith(config, options, payload, token, deadline = null) {
|
|
6408
7118
|
const store = storeFor(config, options.path);
|
|
6409
|
-
const token = options.withToken && store ? await store.get() : null;
|
|
6410
7119
|
const headers = {
|
|
6411
7120
|
...options.headers,
|
|
6412
7121
|
apikey: config.apiKey
|
|
@@ -6425,7 +7134,7 @@ async function call(config, options) {
|
|
|
6425
7134
|
headers["Content-Type"] = "application/json";
|
|
6426
7135
|
init.body = payload;
|
|
6427
7136
|
}
|
|
6428
|
-
const received = await receive(config, options, init);
|
|
7137
|
+
const received = await receive(config, options, init, deadline);
|
|
6429
7138
|
if ("error" in received) return { error: received.error, answer: null };
|
|
6430
7139
|
const { status, headers: responseHeaders, text } = received;
|
|
6431
7140
|
let body;
|
|
@@ -6450,8 +7159,69 @@ async function settled(store, error, headers, token) {
|
|
|
6450
7159
|
return RETRY_AFTER_CODES.has(error.code) ? withRetryAfter(error, headers) : error;
|
|
6451
7160
|
}
|
|
6452
7161
|
async function send(config, options) {
|
|
7162
|
+
const bearer = await untilAborted(
|
|
7163
|
+
bearerFor(config, options.path, options.withToken),
|
|
7164
|
+
options.signal
|
|
7165
|
+
);
|
|
7166
|
+
if (bearer === ABORTED) return { error: uploadAborted(), status: null };
|
|
7167
|
+
if ("error" in bearer) return { error: bearer.error, status: null };
|
|
7168
|
+
const result = await sendWith(config, options, bearer.token);
|
|
7169
|
+
if (result.error?.code !== "INVALID_SESSION" || isStream(options.body))
|
|
7170
|
+
return result;
|
|
7171
|
+
const again = await untilAborted(
|
|
7172
|
+
retryBearer(config, options.path, result.error, bearer),
|
|
7173
|
+
options.signal
|
|
7174
|
+
);
|
|
7175
|
+
if (again === ABORTED) return { error: uploadAborted(), status: null };
|
|
7176
|
+
if (again === null) return result;
|
|
7177
|
+
if ("error" in again) return { error: again.error, status: null };
|
|
7178
|
+
return sendWith(config, options, again.token);
|
|
7179
|
+
}
|
|
7180
|
+
var ABORTED = Symbol("aborted");
|
|
7181
|
+
function untilAborted(promise, signal) {
|
|
7182
|
+
if (!signal) return promise;
|
|
7183
|
+
if (signal.aborted) return Promise.resolve(ABORTED);
|
|
7184
|
+
return new Promise((resolve9, reject) => {
|
|
7185
|
+
const stop = () => resolve9(ABORTED);
|
|
7186
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
7187
|
+
promise.then(
|
|
7188
|
+
(value) => {
|
|
7189
|
+
signal.removeEventListener("abort", stop);
|
|
7190
|
+
resolve9(value);
|
|
7191
|
+
},
|
|
7192
|
+
(error) => {
|
|
7193
|
+
signal.removeEventListener("abort", stop);
|
|
7194
|
+
reject(error);
|
|
7195
|
+
}
|
|
7196
|
+
);
|
|
7197
|
+
});
|
|
7198
|
+
}
|
|
7199
|
+
function untilStopped(promise, signal, deadline) {
|
|
7200
|
+
if (signal?.aborted) return Promise.resolve(ABORTED);
|
|
7201
|
+
if (!signal && deadline === null) return promise;
|
|
7202
|
+
return new Promise((resolve9, reject) => {
|
|
7203
|
+
let timer;
|
|
7204
|
+
const stop = () => finish(ABORTED);
|
|
7205
|
+
const finish = (value) => {
|
|
7206
|
+
clearTimeout(timer);
|
|
7207
|
+
signal?.removeEventListener("abort", stop);
|
|
7208
|
+
resolve9(value);
|
|
7209
|
+
};
|
|
7210
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
7211
|
+
if (deadline !== null)
|
|
7212
|
+
timer = setTimeout(() => finish(EXPIRED), remainingMs(deadline));
|
|
7213
|
+
promise.then(finish, (error) => {
|
|
7214
|
+
clearTimeout(timer);
|
|
7215
|
+
signal?.removeEventListener("abort", stop);
|
|
7216
|
+
reject(error);
|
|
7217
|
+
});
|
|
7218
|
+
});
|
|
7219
|
+
}
|
|
7220
|
+
function isStream(body) {
|
|
7221
|
+
return typeof ReadableStream === "function" && body instanceof ReadableStream;
|
|
7222
|
+
}
|
|
7223
|
+
async function sendWith(config, options, token) {
|
|
6453
7224
|
const store = storeFor(config, options.path);
|
|
6454
|
-
const token = options.withToken && store ? await store.get() : null;
|
|
6455
7225
|
const headers = {
|
|
6456
7226
|
...options.headers,
|
|
6457
7227
|
apikey: config.apiKey
|
|
@@ -6506,6 +7276,143 @@ async function send(config, options) {
|
|
|
6506
7276
|
status
|
|
6507
7277
|
};
|
|
6508
7278
|
}
|
|
7279
|
+
async function fetchUnread(config, options) {
|
|
7280
|
+
const { signal } = options;
|
|
7281
|
+
if (signal?.aborted) return { failed: "aborted", cause: signal.reason };
|
|
7282
|
+
const deadline = deadlineOf(options.timeoutMs);
|
|
7283
|
+
if (options.ready) {
|
|
7284
|
+
const ready = await untilStopped(options.ready(), signal, deadline);
|
|
7285
|
+
if (ready === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
7286
|
+
if (ready === EXPIRED) return { failed: "timeout", cause: null };
|
|
7287
|
+
}
|
|
7288
|
+
const own = options.headers.has("authorization");
|
|
7289
|
+
const bearer = await untilStopped(
|
|
7290
|
+
bearerFor(config, options.path, !own),
|
|
7291
|
+
signal,
|
|
7292
|
+
deadline
|
|
7293
|
+
);
|
|
7294
|
+
if (bearer === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
7295
|
+
if (bearer === EXPIRED) return { failed: "timeout", cause: null };
|
|
7296
|
+
if ("error" in bearer) return { refused: bearer.error };
|
|
7297
|
+
const first = await fetchOnce(config, options, bearer.token, deadline);
|
|
7298
|
+
if (!("response" in first) || bearer.token === null) return first;
|
|
7299
|
+
const refused = await refusesToken(first.response, signal, deadline);
|
|
7300
|
+
if (refused === EXPIRED) {
|
|
7301
|
+
discard(first.response);
|
|
7302
|
+
return { failed: "timeout", cause: null };
|
|
7303
|
+
}
|
|
7304
|
+
if (refused === ABORTED) {
|
|
7305
|
+
discard(first.response);
|
|
7306
|
+
return { failed: "aborted", cause: signal?.reason };
|
|
7307
|
+
}
|
|
7308
|
+
if (!refused) return first;
|
|
7309
|
+
await storeFor(config, options.path)?.clearIf(bearer.token);
|
|
7310
|
+
if (isStream(options.body)) return first;
|
|
7311
|
+
const again = await untilStopped(
|
|
7312
|
+
retryBearer(config, options.path, invalidSession(), bearer),
|
|
7313
|
+
signal,
|
|
7314
|
+
deadline
|
|
7315
|
+
);
|
|
7316
|
+
if (again === EXPIRED) {
|
|
7317
|
+
discard(first.response);
|
|
7318
|
+
return { failed: "timeout", cause: null };
|
|
7319
|
+
}
|
|
7320
|
+
if (again === null || again !== ABORTED && "error" in again) return first;
|
|
7321
|
+
discard(first.response);
|
|
7322
|
+
if (again === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
7323
|
+
const second = await fetchOnce(config, options, again.token, deadline);
|
|
7324
|
+
if (!("response" in second) || again.token === null) return second;
|
|
7325
|
+
const refusedAgain = await refusesToken(second.response, signal, deadline);
|
|
7326
|
+
if (refusedAgain === ABORTED) {
|
|
7327
|
+
discard(second.response);
|
|
7328
|
+
return { failed: "aborted", cause: signal?.reason };
|
|
7329
|
+
}
|
|
7330
|
+
if (refusedAgain === true || refusedAgain === EXPIRED)
|
|
7331
|
+
await storeFor(config, options.path)?.clearIf(again.token);
|
|
7332
|
+
return second;
|
|
7333
|
+
}
|
|
7334
|
+
async function refusesToken(response, signal, deadline) {
|
|
7335
|
+
if (response.status !== 401 || response.headers.get("x-relay-error") !== "true")
|
|
7336
|
+
return false;
|
|
7337
|
+
let body;
|
|
7338
|
+
try {
|
|
7339
|
+
body = await untilStopped(response.clone().json(), signal, deadline);
|
|
7340
|
+
} catch {
|
|
7341
|
+
return signal?.aborted ? ABORTED : false;
|
|
7342
|
+
}
|
|
7343
|
+
if (body === ABORTED || body === EXPIRED) return body;
|
|
7344
|
+
return isRecord(body) && isRecord(body.error) && body.error.code === "INVALID_SESSION";
|
|
7345
|
+
}
|
|
7346
|
+
var releases = /* @__PURE__ */ new WeakMap();
|
|
7347
|
+
function discard(response) {
|
|
7348
|
+
releases.get(response)?.();
|
|
7349
|
+
releases.delete(response);
|
|
7350
|
+
response.body?.cancel().catch(() => {
|
|
7351
|
+
});
|
|
7352
|
+
}
|
|
7353
|
+
async function fetchOnce(config, options, token, deadline) {
|
|
7354
|
+
const { signal } = options;
|
|
7355
|
+
if (signal?.aborted) return { failed: "aborted", cause: signal.reason };
|
|
7356
|
+
if (deadline !== null && remainingMs(deadline) === 0)
|
|
7357
|
+
return { failed: "timeout", cause: null };
|
|
7358
|
+
const headers = new Headers(options.headers);
|
|
7359
|
+
headers.set("apikey", config.apiKey);
|
|
7360
|
+
if (token !== null) headers.set("authorization", `Bearer ${token}`);
|
|
7361
|
+
const init = {
|
|
7362
|
+
method: options.method,
|
|
7363
|
+
headers,
|
|
7364
|
+
credentials: "omit"
|
|
7365
|
+
};
|
|
7366
|
+
if (options.body !== void 0) init.body = options.body;
|
|
7367
|
+
if (isStream(options.body)) init.duplex = "half";
|
|
7368
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
7369
|
+
if (controller) init.signal = controller.signal;
|
|
7370
|
+
let timer;
|
|
7371
|
+
let stop = () => {
|
|
7372
|
+
};
|
|
7373
|
+
const stopped = new Promise((resolve9) => {
|
|
7374
|
+
stop = resolve9;
|
|
7375
|
+
});
|
|
7376
|
+
const onAbort = () => {
|
|
7377
|
+
stop({ failed: "aborted", cause: signal?.reason });
|
|
7378
|
+
controller?.abort(signal?.reason);
|
|
7379
|
+
};
|
|
7380
|
+
if (deadline !== null)
|
|
7381
|
+
timer = setTimeout(() => {
|
|
7382
|
+
stop({ failed: "timeout", cause: null });
|
|
7383
|
+
controller?.abort();
|
|
7384
|
+
}, remainingMs(deadline));
|
|
7385
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
7386
|
+
const request = (async () => {
|
|
7387
|
+
try {
|
|
7388
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
7389
|
+
return {
|
|
7390
|
+
response: await fetchImpl(
|
|
7391
|
+
`${config.baseUrl}/_naro/v1${options.path}`,
|
|
7392
|
+
init
|
|
7393
|
+
)
|
|
7394
|
+
};
|
|
7395
|
+
} catch (cause) {
|
|
7396
|
+
return { failed: "network", cause };
|
|
7397
|
+
}
|
|
7398
|
+
})();
|
|
7399
|
+
const release = () => signal?.removeEventListener("abort", onAbort);
|
|
7400
|
+
let outcome;
|
|
7401
|
+
try {
|
|
7402
|
+
outcome = await Promise.race([request, stopped]);
|
|
7403
|
+
return outcome;
|
|
7404
|
+
} finally {
|
|
7405
|
+
clearTimeout(timer);
|
|
7406
|
+
if (outcome !== void 0 && "response" in outcome)
|
|
7407
|
+
releases.set(outcome.response, release);
|
|
7408
|
+
else {
|
|
7409
|
+
release();
|
|
7410
|
+
void request.then((late) => {
|
|
7411
|
+
if ("response" in late) discard(late.response);
|
|
7412
|
+
});
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7415
|
+
}
|
|
6509
7416
|
var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
6510
7417
|
"RATE_LIMITED",
|
|
6511
7418
|
"WRITE_QUOTA_EXCEEDED",
|
|
@@ -6514,7 +7421,12 @@ var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
|
6514
7421
|
"OBJECT_RATE_LIMITED",
|
|
6515
7422
|
"STORAGE_OPS_QUOTA_EXCEEDED",
|
|
6516
7423
|
// Realtime's REST broadcast over the project's message budget (x-retry-after: 1).
|
|
6517
|
-
"TOO_MANY_MESSAGES"
|
|
7424
|
+
"TOO_MANY_MESSAGES",
|
|
7425
|
+
// Naro Functions (design F20): a team over its monthly function CPU, and
|
|
7426
|
+
// the control plane's Cloudflare limit. `invoke` reads x-retry-after on
|
|
7427
|
+
// every relay error anyway (invoke/client.ts).
|
|
7428
|
+
"FUNCTIONS_QUOTA_EXCEEDED",
|
|
7429
|
+
"FUNCTIONS_RATE_LIMITED"
|
|
6518
7430
|
]);
|
|
6519
7431
|
function withRetryAfter(error, headers) {
|
|
6520
7432
|
const header = headers.get("x-retry-after")?.trim() ?? "";
|
|
@@ -6828,14 +7740,14 @@ var AuthClient = class {
|
|
|
6828
7740
|
*/
|
|
6829
7741
|
async signInWithOAuth(options) {
|
|
6830
7742
|
const { provider } = options;
|
|
6831
|
-
const
|
|
7743
|
+
const failed2 = (error) => ({
|
|
6832
7744
|
data: { provider, url: null },
|
|
6833
7745
|
error
|
|
6834
7746
|
});
|
|
6835
|
-
if (!OAUTH_PROVIDERS.includes(provider)) return
|
|
7747
|
+
if (!OAUTH_PROVIDERS.includes(provider)) return failed2(invalidProvider());
|
|
6836
7748
|
await this.#flow.recovery.forget();
|
|
6837
7749
|
const pair = await createPkcePair();
|
|
6838
|
-
if (!pair) return
|
|
7750
|
+
if (!pair) return failed2(cryptoUnavailable());
|
|
6839
7751
|
await this.#flow.verifiers.add(pair.challenge, pair.verifier);
|
|
6840
7752
|
const redirectTo = options.redirectTo ?? currentPage();
|
|
6841
7753
|
const url = new URL(
|
|
@@ -7305,6 +8217,23 @@ var TokenStore = class {
|
|
|
7305
8217
|
this.#changed();
|
|
7306
8218
|
});
|
|
7307
8219
|
}
|
|
8220
|
+
/**
|
|
8221
|
+
* Runs `operation` only while `expected` is the stored token (null: none
|
|
8222
|
+
* is stored), inside this
|
|
8223
|
+
* store's queue, and says whether it ran: no write to this store lands
|
|
8224
|
+
* between the check and the end of the operation. A refreshed access JWT
|
|
8225
|
+
* is kept this way (`http.ts`), so one minted for a session that a
|
|
8226
|
+
* sign-in replaced meanwhile never lands on top of the new session's.
|
|
8227
|
+
* `operation` must not call this store's public methods (it would wait
|
|
8228
|
+
* on itself); another store's are fine.
|
|
8229
|
+
*/
|
|
8230
|
+
whileIs(expected, operation) {
|
|
8231
|
+
return this.#serial(async () => {
|
|
8232
|
+
if (await this.#get() !== expected) return false;
|
|
8233
|
+
await operation();
|
|
8234
|
+
return true;
|
|
8235
|
+
});
|
|
8236
|
+
}
|
|
7308
8237
|
/**
|
|
7309
8238
|
* Calls `listener` after every write of the token — a sign-in, a
|
|
7310
8239
|
* sign-out, a renewed session, a cleared INVALID_SESSION — from inside the
|
|
@@ -7384,6 +8313,282 @@ var TokenStore = class {
|
|
|
7384
8313
|
}
|
|
7385
8314
|
}
|
|
7386
8315
|
};
|
|
8316
|
+
function prepareBody(body) {
|
|
8317
|
+
if (body === void 0) return { body: void 0, contentType: null };
|
|
8318
|
+
if (typeof body === "string") return { body, contentType: "text/plain" };
|
|
8319
|
+
if (typeof Blob === "function" && body instanceof Blob)
|
|
8320
|
+
return { body, contentType: "application/octet-stream" };
|
|
8321
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body))
|
|
8322
|
+
return { body, contentType: "application/octet-stream" };
|
|
8323
|
+
if (typeof FormData === "function" && body instanceof FormData)
|
|
8324
|
+
return { body, contentType: null };
|
|
8325
|
+
if (typeof URLSearchParams === "function" && body instanceof URLSearchParams)
|
|
8326
|
+
return { body, contentType: null };
|
|
8327
|
+
if (typeof ReadableStream === "function" && body instanceof ReadableStream)
|
|
8328
|
+
return { body, contentType: null };
|
|
8329
|
+
try {
|
|
8330
|
+
const text = JSON.stringify(body);
|
|
8331
|
+
return text === void 0 ? null : { body: text, contentType: "application/json" };
|
|
8332
|
+
} catch {
|
|
8333
|
+
return null;
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
async function readAnswer(response, method) {
|
|
8337
|
+
if (response.status === 204 || method === "HEAD") return { data: null };
|
|
8338
|
+
const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
8339
|
+
try {
|
|
8340
|
+
if (type === "application/json") {
|
|
8341
|
+
const text = await response.text();
|
|
8342
|
+
return {
|
|
8343
|
+
data: text.trim() === "" ? null : JSON.parse(text)
|
|
8344
|
+
};
|
|
8345
|
+
}
|
|
8346
|
+
if (type === "application/octet-stream" || type === "application/pdf")
|
|
8347
|
+
return { data: await response.blob() };
|
|
8348
|
+
if (type === "text/event-stream") return { data: response };
|
|
8349
|
+
if (type === "multipart/form-data")
|
|
8350
|
+
return { data: await response.formData() };
|
|
8351
|
+
return { data: await response.text() };
|
|
8352
|
+
} catch (cause) {
|
|
8353
|
+
return { unreadable: cause };
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
var FunctionsError = class extends Error {
|
|
8357
|
+
code;
|
|
8358
|
+
context;
|
|
8359
|
+
constructor(code, message, context, details) {
|
|
8360
|
+
super(message);
|
|
8361
|
+
this.name = "FunctionsError";
|
|
8362
|
+
this.code = code;
|
|
8363
|
+
this.context = context;
|
|
8364
|
+
if (details !== void 0)
|
|
8365
|
+
Object.defineProperty(this, "details", {
|
|
8366
|
+
value: details,
|
|
8367
|
+
enumerable: true
|
|
8368
|
+
});
|
|
8369
|
+
}
|
|
8370
|
+
};
|
|
8371
|
+
var FunctionsHttpError = class extends FunctionsError {
|
|
8372
|
+
constructor(response) {
|
|
8373
|
+
super(
|
|
8374
|
+
"FUNCTION_HTTP_ERROR",
|
|
8375
|
+
`The function answered with HTTP ${String(response.status)}.`,
|
|
8376
|
+
response
|
|
8377
|
+
);
|
|
8378
|
+
this.name = "FunctionsHttpError";
|
|
8379
|
+
}
|
|
8380
|
+
};
|
|
8381
|
+
var FunctionsRelayError = class extends FunctionsError {
|
|
8382
|
+
constructor(code, message, response, details) {
|
|
8383
|
+
super(code, message, response, details);
|
|
8384
|
+
this.name = "FunctionsRelayError";
|
|
8385
|
+
}
|
|
8386
|
+
};
|
|
8387
|
+
var FunctionsFetchError = class extends FunctionsError {
|
|
8388
|
+
constructor(code, message, cause, details) {
|
|
8389
|
+
super(code, message, cause, details);
|
|
8390
|
+
this.name = "FunctionsFetchError";
|
|
8391
|
+
}
|
|
8392
|
+
};
|
|
8393
|
+
var FUNCTIONS_MESSAGES = {
|
|
8394
|
+
timeout: (ms) => `The function did not answer within ${String(ms)} ms.`,
|
|
8395
|
+
aborted: "The function call was stopped before the function answered.",
|
|
8396
|
+
invalidBody: "functions.invoke could not send this body: JSON cannot hold it (a BigInt or a cycle). Pass a string, a Blob, bytes, FormData, URLSearchParams or a stream instead.",
|
|
8397
|
+
invalidTimeout: "functions.invoke's timeout is whole milliseconds from 1 to 2147483647.",
|
|
8398
|
+
invalidHeaders: "functions.invoke's headers are not valid HTTP header names and values.",
|
|
8399
|
+
invalidName: "functions.invoke's name is a string: the function's name, then any path and query.",
|
|
8400
|
+
unencodableName: "functions.invoke's name is not well-formed Unicode: it has a lone surrogate.",
|
|
8401
|
+
dotSegments: `functions.invoke's name cannot be "." or "..", and ".." in its path cannot leave /functions/.`,
|
|
8402
|
+
invalidSignal: "functions.invoke's signal is not an AbortSignal.",
|
|
8403
|
+
bodyWithGetOrHead: "functions.invoke cannot send a body with GET or HEAD.",
|
|
8404
|
+
/** `RELAY_ERROR`: a HEAD answer has no body, so Naro's code is not in it. */
|
|
8405
|
+
headRelay: (status) => `Naro answered HTTP ${String(status)} for the function, but a HEAD answer has no body to say why. Call it with GET to read the reason.`,
|
|
8406
|
+
unreadable: (status) => `The function answered HTTP ${String(status)}, but its body could not be read as its content-type says.`
|
|
8407
|
+
};
|
|
8408
|
+
var MAX_TIMER_MS = 2147483647;
|
|
8409
|
+
function functionPath(name) {
|
|
8410
|
+
const cut = name.search(/[/?#]/);
|
|
8411
|
+
const head = cut === -1 ? name : name.slice(0, cut);
|
|
8412
|
+
const tail = cut === -1 ? "" : name.slice(cut);
|
|
8413
|
+
return `/functions/${encodeURIComponent(head)}${tail}`;
|
|
8414
|
+
}
|
|
8415
|
+
function dotSegment(segment) {
|
|
8416
|
+
const dots = segment.toLowerCase().replace(/%2e/g, ".");
|
|
8417
|
+
return dots === "." ? 1 : dots === ".." ? 2 : 0;
|
|
8418
|
+
}
|
|
8419
|
+
function leavesFunctions(input) {
|
|
8420
|
+
const name = input.replace(/[\t\n\r]/g, "");
|
|
8421
|
+
const cut = name.search(/[/?#]/);
|
|
8422
|
+
const head = cut === -1 ? name : name.slice(0, cut);
|
|
8423
|
+
if (head === "." || head === "..") return true;
|
|
8424
|
+
if (cut === -1 || name[cut] !== "/") return false;
|
|
8425
|
+
const rest = name.slice(cut + 1);
|
|
8426
|
+
const end = rest.search(/[?#]/);
|
|
8427
|
+
let depth = 1;
|
|
8428
|
+
for (const segment of (end === -1 ? rest : rest.slice(0, end)).split(
|
|
8429
|
+
/[/\\]/
|
|
8430
|
+
)) {
|
|
8431
|
+
const dots = dotSegment(segment);
|
|
8432
|
+
if (dots === 0) depth += 1;
|
|
8433
|
+
if (dots === 2) depth -= 1;
|
|
8434
|
+
if (depth < 0) return true;
|
|
8435
|
+
}
|
|
8436
|
+
return false;
|
|
8437
|
+
}
|
|
8438
|
+
function pathOf(name) {
|
|
8439
|
+
if (typeof name !== "string")
|
|
8440
|
+
return { invalid: FUNCTIONS_MESSAGES.invalidName };
|
|
8441
|
+
if (leavesFunctions(name)) return { invalid: FUNCTIONS_MESSAGES.dotSegments };
|
|
8442
|
+
try {
|
|
8443
|
+
return { path: functionPath(name) };
|
|
8444
|
+
} catch {
|
|
8445
|
+
return { invalid: FUNCTIONS_MESSAGES.unencodableName };
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
function isSignalOrNone(value) {
|
|
8449
|
+
if (value === void 0 || value === null) return true;
|
|
8450
|
+
if (typeof value !== "object") return false;
|
|
8451
|
+
const signal = value;
|
|
8452
|
+
return typeof signal.aborted === "boolean" && typeof signal.addEventListener === "function" && typeof signal.removeEventListener === "function";
|
|
8453
|
+
}
|
|
8454
|
+
function invalidOptions(message) {
|
|
8455
|
+
return failed(new FunctionsFetchError("INVALID_OPTIONS", message, null));
|
|
8456
|
+
}
|
|
8457
|
+
function failed(error) {
|
|
8458
|
+
return { data: null, error };
|
|
8459
|
+
}
|
|
8460
|
+
function fromNaroError(error, cause) {
|
|
8461
|
+
return new FunctionsFetchError(
|
|
8462
|
+
error.code,
|
|
8463
|
+
error.message,
|
|
8464
|
+
cause,
|
|
8465
|
+
error.details
|
|
8466
|
+
);
|
|
8467
|
+
}
|
|
8468
|
+
function failure(kind, cause, timeout) {
|
|
8469
|
+
if (kind === "timeout")
|
|
8470
|
+
return new FunctionsFetchError(
|
|
8471
|
+
"TIMEOUT",
|
|
8472
|
+
FUNCTIONS_MESSAGES.timeout(timeout ?? 0),
|
|
8473
|
+
cause
|
|
8474
|
+
);
|
|
8475
|
+
if (kind === "aborted")
|
|
8476
|
+
return new FunctionsFetchError(
|
|
8477
|
+
"ABORTED",
|
|
8478
|
+
FUNCTIONS_MESSAGES.aborted,
|
|
8479
|
+
cause
|
|
8480
|
+
);
|
|
8481
|
+
return fromNaroError(networkError(cause), cause);
|
|
8482
|
+
}
|
|
8483
|
+
function unreadable(cause, status, signal) {
|
|
8484
|
+
if (signal?.aborted)
|
|
8485
|
+
return new FunctionsFetchError(
|
|
8486
|
+
"ABORTED",
|
|
8487
|
+
FUNCTIONS_MESSAGES.aborted,
|
|
8488
|
+
cause
|
|
8489
|
+
);
|
|
8490
|
+
if (cause instanceof SyntaxError)
|
|
8491
|
+
return new FunctionsFetchError(
|
|
8492
|
+
"INVALID_RESPONSE",
|
|
8493
|
+
FUNCTIONS_MESSAGES.unreadable(status),
|
|
8494
|
+
cause
|
|
8495
|
+
);
|
|
8496
|
+
return fromNaroError(networkError(cause), cause);
|
|
8497
|
+
}
|
|
8498
|
+
async function relayError(response, head) {
|
|
8499
|
+
let body;
|
|
8500
|
+
try {
|
|
8501
|
+
body = await response.clone().json();
|
|
8502
|
+
} catch {
|
|
8503
|
+
body = void 0;
|
|
8504
|
+
}
|
|
8505
|
+
const { status } = response;
|
|
8506
|
+
const error = withRetryAfter(
|
|
8507
|
+
head && body === void 0 ? {
|
|
8508
|
+
code: "RELAY_ERROR",
|
|
8509
|
+
message: FUNCTIONS_MESSAGES.headRelay(status),
|
|
8510
|
+
details: { status }
|
|
8511
|
+
} : errorFromBody(status, body) ?? invalidResponse(status),
|
|
8512
|
+
response.headers
|
|
8513
|
+
);
|
|
8514
|
+
return new FunctionsRelayError(
|
|
8515
|
+
error.code,
|
|
8516
|
+
error.message,
|
|
8517
|
+
response,
|
|
8518
|
+
error.details
|
|
8519
|
+
);
|
|
8520
|
+
}
|
|
8521
|
+
var FunctionsClient = class {
|
|
8522
|
+
#http;
|
|
8523
|
+
#auth;
|
|
8524
|
+
constructor(http, auth) {
|
|
8525
|
+
this.#http = http;
|
|
8526
|
+
this.#auth = auth;
|
|
8527
|
+
}
|
|
8528
|
+
async invoke(name, options = {}) {
|
|
8529
|
+
const method = options.method ?? "POST";
|
|
8530
|
+
const { timeout, signal } = options;
|
|
8531
|
+
const target = pathOf(name);
|
|
8532
|
+
if ("invalid" in target) return invalidOptions(target.invalid);
|
|
8533
|
+
if (timeout !== void 0 && !(Number.isInteger(timeout) && timeout > 0 && timeout <= MAX_TIMER_MS))
|
|
8534
|
+
return invalidOptions(FUNCTIONS_MESSAGES.invalidTimeout);
|
|
8535
|
+
if (!isSignalOrNone(signal))
|
|
8536
|
+
return invalidOptions(FUNCTIONS_MESSAGES.invalidSignal);
|
|
8537
|
+
const verb = typeof method === "string" ? method.toUpperCase() : "";
|
|
8538
|
+
if (options.body !== void 0 && (verb === "GET" || verb === "HEAD"))
|
|
8539
|
+
return invalidOptions(FUNCTIONS_MESSAGES.bodyWithGetOrHead);
|
|
8540
|
+
let headers;
|
|
8541
|
+
try {
|
|
8542
|
+
headers = new Headers(options.headers);
|
|
8543
|
+
} catch (cause) {
|
|
8544
|
+
return failed(
|
|
8545
|
+
new FunctionsFetchError(
|
|
8546
|
+
"INVALID_OPTIONS",
|
|
8547
|
+
FUNCTIONS_MESSAGES.invalidHeaders,
|
|
8548
|
+
cause
|
|
8549
|
+
)
|
|
8550
|
+
);
|
|
8551
|
+
}
|
|
8552
|
+
const prepared = prepareBody(options.body);
|
|
8553
|
+
if (prepared === null)
|
|
8554
|
+
return failed(
|
|
8555
|
+
new FunctionsFetchError(
|
|
8556
|
+
"INVALID_BODY",
|
|
8557
|
+
FUNCTIONS_MESSAGES.invalidBody,
|
|
8558
|
+
null
|
|
8559
|
+
)
|
|
8560
|
+
);
|
|
8561
|
+
if (prepared.contentType !== null && !headers.has("content-type"))
|
|
8562
|
+
headers.set("content-type", prepared.contentType);
|
|
8563
|
+
const result = await fetchUnread(this.#http, {
|
|
8564
|
+
method,
|
|
8565
|
+
path: target.path,
|
|
8566
|
+
headers,
|
|
8567
|
+
body: prepared.body,
|
|
8568
|
+
timeoutMs: timeout,
|
|
8569
|
+
signal,
|
|
8570
|
+
// A sign-in the page URL carried goes first, as for a query (P2-6) —
|
|
8571
|
+
// within the call's timeout and signal; the exchange is not cut.
|
|
8572
|
+
ready: () => this.#auth.ready()
|
|
8573
|
+
});
|
|
8574
|
+
if ("refused" in result) return failed(fromNaroError(result.refused, null));
|
|
8575
|
+
if ("failed" in result)
|
|
8576
|
+
return failed(failure(result.failed, result.cause, timeout));
|
|
8577
|
+
const { response } = result;
|
|
8578
|
+
if (!response.ok) {
|
|
8579
|
+
const error = response.headers.get("x-relay-error") === "true" ? await relayError(response, verb === "HEAD") : new FunctionsHttpError(response);
|
|
8580
|
+
return { data: null, error, response };
|
|
8581
|
+
}
|
|
8582
|
+
const answer = await readAnswer(response, method);
|
|
8583
|
+
if ("unreadable" in answer)
|
|
8584
|
+
return {
|
|
8585
|
+
data: null,
|
|
8586
|
+
error: unreadable(answer.unreadable, response.status, signal),
|
|
8587
|
+
response
|
|
8588
|
+
};
|
|
8589
|
+
return { data: answer.data, error: null, response };
|
|
8590
|
+
}
|
|
8591
|
+
};
|
|
7387
8592
|
function isSpace2(char) {
|
|
7388
8593
|
return char.trim() === "";
|
|
7389
8594
|
}
|
|
@@ -8529,30 +9734,30 @@ var RealtimeSocket = class {
|
|
|
8529
9734
|
request(topic, event, payload, options = {}) {
|
|
8530
9735
|
const ref = options.ref ?? this.nextRef();
|
|
8531
9736
|
const turn = options.turn ?? "app";
|
|
8532
|
-
return new Promise((
|
|
9737
|
+
return new Promise((resolve9) => {
|
|
8533
9738
|
const lost = {
|
|
8534
9739
|
ok: false,
|
|
8535
9740
|
error: realtimeClosed(1006),
|
|
8536
9741
|
lost: true
|
|
8537
9742
|
};
|
|
8538
|
-
if (!this.isOpen()) return
|
|
9743
|
+
if (!this.isOpen()) return resolve9(lost);
|
|
8539
9744
|
let text;
|
|
8540
9745
|
try {
|
|
8541
9746
|
text = clientFrame(topic, event, payload, ref, options.joinRef);
|
|
8542
9747
|
} catch {
|
|
8543
|
-
return
|
|
9748
|
+
return resolve9(lost);
|
|
8544
9749
|
}
|
|
8545
9750
|
const bytes = utf8Length(text);
|
|
8546
9751
|
const limit = this.#frameLimit;
|
|
8547
9752
|
if (limit !== null && bytes > limit)
|
|
8548
|
-
return
|
|
9753
|
+
return resolve9({ ok: false, error: frameTooLarge(limit), lost: false });
|
|
8549
9754
|
if (turn === "app" && event !== "phx_leave" && !this.#goesInTime())
|
|
8550
|
-
return
|
|
9755
|
+
return resolve9({
|
|
8551
9756
|
ok: false,
|
|
8552
9757
|
error: clientFlooding(CONNECTION_FRAMES_PER_SECOND),
|
|
8553
9758
|
lost: false
|
|
8554
9759
|
});
|
|
8555
|
-
this.#replies.set(ref, { resolve:
|
|
9760
|
+
this.#replies.set(ref, { resolve: resolve9, timer: void 0, bytes, turn });
|
|
8556
9761
|
this.#enqueue({ text, bytes, ref, turn, presence: event === "presence" });
|
|
8557
9762
|
});
|
|
8558
9763
|
}
|
|
@@ -8876,19 +10081,19 @@ var RealtimeSocket = class {
|
|
|
8876
10081
|
#closed(ws, code, reason, waitedMs) {
|
|
8877
10082
|
if (this.#ws !== ws) return;
|
|
8878
10083
|
const wasOpen = this.#state === "open";
|
|
8879
|
-
const
|
|
10084
|
+
const timedOut2 = waitedMs !== null;
|
|
8880
10085
|
detach(ws);
|
|
8881
10086
|
this.#ws = null;
|
|
8882
10087
|
this.#clearTimers();
|
|
8883
|
-
if (
|
|
10088
|
+
if (timedOut2) closeQuietly(ws, 1e3);
|
|
8884
10089
|
if (!wasOpen) this.#unopened += 1;
|
|
8885
10090
|
const said = this.#said;
|
|
8886
10091
|
this.#said = null;
|
|
8887
|
-
if (!
|
|
8888
|
-
const action =
|
|
10092
|
+
if (!timedOut2 && code === 4008) this.#flooded = true;
|
|
10093
|
+
const action = timedOut2 ? { kind: "retry", minMs: 0 } : closeAction(code, said?.code ?? reason);
|
|
8889
10094
|
if (action.kind === "stop")
|
|
8890
10095
|
return this.#stop(said ?? realtimeStopped(code, reason));
|
|
8891
|
-
const error =
|
|
10096
|
+
const error = timedOut2 ? realtimeTimedOut(waitedMs) : said ?? realtimeClosed(code);
|
|
8892
10097
|
this.#failReplies(error);
|
|
8893
10098
|
this.#state = "waiting";
|
|
8894
10099
|
const delay = delayAfter(
|
|
@@ -8901,7 +10106,7 @@ var RealtimeSocket = class {
|
|
|
8901
10106
|
safely(
|
|
8902
10107
|
() => this.#config.events.dropped(
|
|
8903
10108
|
error,
|
|
8904
|
-
|
|
10109
|
+
timedOut2 ? "TIMED_OUT" : "CHANNEL_ERROR"
|
|
8905
10110
|
)
|
|
8906
10111
|
);
|
|
8907
10112
|
}
|
|
@@ -9671,9 +10876,9 @@ var RealtimeChannel = class {
|
|
|
9671
10876
|
*/
|
|
9672
10877
|
#inTurn(joinRef, wait, send2) {
|
|
9673
10878
|
if (!wait && this.#waiting.length === 0) return send2();
|
|
9674
|
-
return new Promise((
|
|
10879
|
+
return new Promise((resolve9) => {
|
|
9675
10880
|
this.#waiting.push(
|
|
9676
|
-
() =>
|
|
10881
|
+
() => resolve9(this.#joinRef === joinRef ? send2() : null)
|
|
9677
10882
|
);
|
|
9678
10883
|
});
|
|
9679
10884
|
}
|
|
@@ -10288,13 +11493,13 @@ async function uploadInParts(a) {
|
|
|
10288
11493
|
}).catch(() => void 0);
|
|
10289
11494
|
};
|
|
10290
11495
|
const etags = [];
|
|
10291
|
-
const
|
|
11496
|
+
const failure2 = {
|
|
10292
11497
|
error: signal?.aborted ? uploadAborted() : null
|
|
10293
11498
|
};
|
|
10294
11499
|
let next = 1;
|
|
10295
11500
|
let sentBytes = 0;
|
|
10296
11501
|
const onAbort = () => {
|
|
10297
|
-
|
|
11502
|
+
failure2.error ??= uploadAborted();
|
|
10298
11503
|
};
|
|
10299
11504
|
const tryPart = async (n, bytes) => {
|
|
10300
11505
|
const sent2 = await send(a.http, {
|
|
@@ -10323,18 +11528,18 @@ async function uploadInParts(a) {
|
|
|
10323
11528
|
progress(sentBytes);
|
|
10324
11529
|
return null;
|
|
10325
11530
|
}
|
|
10326
|
-
if (attempt22 === ATTEMPTS ||
|
|
11531
|
+
if (attempt22 === ATTEMPTS || failure2.error !== null || !retryable(tried.error, tried.status))
|
|
10327
11532
|
return tried.error;
|
|
10328
11533
|
await sleep2(waitAfter(tried.error, attempt22), signal);
|
|
10329
|
-
if (
|
|
11534
|
+
if (failure2.error !== null) return tried.error;
|
|
10330
11535
|
}
|
|
10331
11536
|
};
|
|
10332
11537
|
const worker = async () => {
|
|
10333
|
-
while (
|
|
11538
|
+
while (failure2.error === null && next <= partCount) {
|
|
10334
11539
|
const n = next;
|
|
10335
11540
|
next += 1;
|
|
10336
11541
|
const error = await sendPart(n);
|
|
10337
|
-
if (error !== null)
|
|
11542
|
+
if (error !== null) failure2.error ??= error;
|
|
10338
11543
|
}
|
|
10339
11544
|
};
|
|
10340
11545
|
signal?.addEventListener("abort", onAbort);
|
|
@@ -10342,9 +11547,9 @@ async function uploadInParts(a) {
|
|
|
10342
11547
|
Array.from({ length: Math.min(CONCURRENCY, partCount) }, worker)
|
|
10343
11548
|
);
|
|
10344
11549
|
signal?.removeEventListener("abort", onAbort);
|
|
10345
|
-
if (
|
|
11550
|
+
if (failure2.error !== null) {
|
|
10346
11551
|
await callOff();
|
|
10347
|
-
return fail(
|
|
11552
|
+
return fail(failure2.error);
|
|
10348
11553
|
}
|
|
10349
11554
|
const completed = await call(a.http, {
|
|
10350
11555
|
method: "POST",
|
|
@@ -10571,8 +11776,8 @@ var StorageFileApi = class {
|
|
|
10571
11776
|
const payload = payloadOf(body, options.contentType);
|
|
10572
11777
|
if (!payload.ok) return fail(payload.error);
|
|
10573
11778
|
const cache = `max-age=${options.cacheControl ?? "3600"}`;
|
|
10574
|
-
const
|
|
10575
|
-
if (
|
|
11779
|
+
const unreadable2 = headerProblem(options.contentType, cache);
|
|
11780
|
+
if (unreadable2 !== null) return fail(unreadable2);
|
|
10576
11781
|
const { signal } = options;
|
|
10577
11782
|
if (signal?.aborted) return fail(uploadAborted());
|
|
10578
11783
|
await this.#auth.ready();
|
|
@@ -10873,8 +12078,8 @@ var StorageFileApi = class {
|
|
|
10873
12078
|
const payload = payloadOf(body, given.contentType);
|
|
10874
12079
|
if (!payload.ok) return fail(payload.error);
|
|
10875
12080
|
const cache = `max-age=${given.cacheControl ?? "3600"}`;
|
|
10876
|
-
const
|
|
10877
|
-
if (
|
|
12081
|
+
const unreadable2 = headerProblem(given.contentType, cache);
|
|
12082
|
+
if (unreadable2 !== null) return fail(unreadable2);
|
|
10878
12083
|
const { signal } = given;
|
|
10879
12084
|
if (signal?.aborted) return fail(uploadAborted());
|
|
10880
12085
|
const uploaded = payload.size > MAX_SINGLE_UPLOAD_BYTES2 ? await uploadInParts({
|
|
@@ -10953,12 +12158,14 @@ var CONFIG_MESSAGES = {
|
|
|
10953
12158
|
bothKeys: "Pass createClient an anonKey or a serviceKey, not both.",
|
|
10954
12159
|
serviceKeyOffServer: SERVICE_KEY_OFF_SERVER_MESSAGE,
|
|
10955
12160
|
fetch: "This runtime has no fetch. Pass one to createClient({ fetch }).",
|
|
10956
|
-
realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647."
|
|
12161
|
+
realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647.",
|
|
12162
|
+
accessTokenWithServiceKey: "Pass createClient an accessToken or a serviceKey, not both: the service key skips every policy, so the user's token would not count.",
|
|
12163
|
+
accessToken: "createClient's accessToken is a function that returns the user's access token, or null for none."
|
|
10957
12164
|
};
|
|
10958
12165
|
var SERVICE_KEY_PREFIX = "naro_sk_";
|
|
10959
|
-
var
|
|
12166
|
+
var MAX_TIMER_MS2 = 2147483647;
|
|
10960
12167
|
function isMilliseconds(value) {
|
|
10961
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <=
|
|
12168
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_TIMER_MS2;
|
|
10962
12169
|
}
|
|
10963
12170
|
function realtimeOptionsOf(value) {
|
|
10964
12171
|
if (value === void 0) return {};
|
|
@@ -11008,6 +12215,35 @@ function apiKeyOf(options) {
|
|
|
11008
12215
|
throw new Error(CONFIG_MESSAGES.serviceKeyOffServer);
|
|
11009
12216
|
return { key, service };
|
|
11010
12217
|
}
|
|
12218
|
+
function accessTokenOf(value) {
|
|
12219
|
+
if (value === void 0) return void 0;
|
|
12220
|
+
if (typeof value !== "function") throw new Error(CONFIG_MESSAGES.accessToken);
|
|
12221
|
+
const read = value;
|
|
12222
|
+
return {
|
|
12223
|
+
async get() {
|
|
12224
|
+
const token = await read();
|
|
12225
|
+
if (token === null || token === void 0 || token === "") return null;
|
|
12226
|
+
if (typeof token !== "string")
|
|
12227
|
+
throw new TypeError(`accessToken returned a ${typeof token}`);
|
|
12228
|
+
if (!/^[\x21-\x7E]+$/.test(token))
|
|
12229
|
+
throw new TypeError(
|
|
12230
|
+
"accessToken returned a string that is not a token"
|
|
12231
|
+
);
|
|
12232
|
+
return token;
|
|
12233
|
+
}
|
|
12234
|
+
};
|
|
12235
|
+
}
|
|
12236
|
+
function refusingAuth() {
|
|
12237
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
12238
|
+
get(_target, property) {
|
|
12239
|
+
if (typeof property === "symbol" || property === "then" || property === "toJSON" || property === "constructor" || property === "asymmetricMatch")
|
|
12240
|
+
return void 0;
|
|
12241
|
+
if (property === "toString") return Object.prototype.toString;
|
|
12242
|
+
if (property === "valueOf") return Object.prototype.valueOf;
|
|
12243
|
+
throw new Error(accessTokenAuthMessage(property));
|
|
12244
|
+
}
|
|
12245
|
+
});
|
|
12246
|
+
}
|
|
11011
12247
|
function queryRunner(http, auth) {
|
|
11012
12248
|
return async (query) => {
|
|
11013
12249
|
await auth.ready();
|
|
@@ -11036,7 +12272,14 @@ var NaroClient2 = class {
|
|
|
11036
12272
|
* them, as supabase-js does.
|
|
11037
12273
|
*/
|
|
11038
12274
|
storage;
|
|
12275
|
+
/**
|
|
12276
|
+
* The project's Naro Functions (design §9): `functions.invoke(name, {
|
|
12277
|
+
* body })`, as in supabase-js.
|
|
12278
|
+
*/
|
|
12279
|
+
functions;
|
|
11039
12280
|
#run;
|
|
12281
|
+
/** Made with `accessToken`: `auth` and `channel()` refuse. */
|
|
12282
|
+
#accessToken;
|
|
11040
12283
|
#newRealtime;
|
|
11041
12284
|
/**
|
|
11042
12285
|
* Made by the first channel(), not by the constructor: it listens to every
|
|
@@ -11047,36 +12290,38 @@ var NaroClient2 = class {
|
|
|
11047
12290
|
constructor(options) {
|
|
11048
12291
|
const baseUrl = baseUrlOf(options.url);
|
|
11049
12292
|
const { key: apiKey, service } = apiKeyOf(options);
|
|
12293
|
+
const accessToken = accessTokenOf(options.accessToken);
|
|
12294
|
+
if (accessToken !== void 0 && service)
|
|
12295
|
+
throw new Error(CONFIG_MESSAGES.accessTokenWithServiceKey);
|
|
11050
12296
|
if (options.fetch === void 0 && typeof globalThis.fetch !== "function")
|
|
11051
12297
|
throw new Error(CONFIG_MESSAGES.fetch);
|
|
11052
12298
|
const realtime = realtimeOptionsOf(options.realtime);
|
|
11053
|
-
const storage = options.auth?.storage ?? defaultStorage();
|
|
12299
|
+
const storage = accessToken !== void 0 ? memoryStorage() : options.auth?.storage ?? defaultStorage();
|
|
11054
12300
|
const http = {
|
|
11055
12301
|
baseUrl,
|
|
11056
12302
|
apiKey,
|
|
11057
12303
|
fetch: options.fetch,
|
|
11058
12304
|
tokens: new TokenStore(storage, storageKey(baseUrl)),
|
|
11059
|
-
|
|
12305
|
+
service,
|
|
12306
|
+
...accessToken !== void 0 ? { accessToken } : { accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)) }
|
|
11060
12307
|
};
|
|
11061
|
-
|
|
12308
|
+
const auth = new AuthClient(http, {
|
|
11062
12309
|
verifiers: new FlowVerifiers(
|
|
11063
12310
|
new TokenStore(storage, verifierKey(baseUrl))
|
|
11064
12311
|
),
|
|
11065
12312
|
recovery: new RecoveryToken(
|
|
11066
12313
|
new TokenStore(storage, recoveryKey(baseUrl))
|
|
11067
12314
|
),
|
|
11068
|
-
detectSessionInUrl: options.auth?.detectSessionInUrl ?? true,
|
|
12315
|
+
detectSessionInUrl: accessToken === void 0 && (options.auth?.detectSessionInUrl ?? true),
|
|
11069
12316
|
shared: { storage, key: storageKey(baseUrl) },
|
|
11070
12317
|
serviceKey: service
|
|
11071
12318
|
});
|
|
11072
|
-
this.#
|
|
11073
|
-
this.
|
|
11074
|
-
this.#
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
options: realtime
|
|
11079
|
-
});
|
|
12319
|
+
this.#accessToken = accessToken !== void 0;
|
|
12320
|
+
this.auth = this.#accessToken ? refusingAuth() : auth;
|
|
12321
|
+
this.#run = queryRunner(http, auth);
|
|
12322
|
+
this.storage = new StorageClient(http, auth);
|
|
12323
|
+
this.functions = new FunctionsClient(http, auth);
|
|
12324
|
+
this.#newRealtime = () => new RealtimeClient({ http, auth, service, options: realtime });
|
|
11080
12325
|
}
|
|
11081
12326
|
/** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
|
|
11082
12327
|
from(table3) {
|
|
@@ -11088,6 +12333,7 @@ var NaroClient2 = class {
|
|
|
11088
12333
|
* `removeChannel`. Nothing connects until the first `subscribe()`.
|
|
11089
12334
|
*/
|
|
11090
12335
|
channel(name, options) {
|
|
12336
|
+
if (this.#accessToken) throw new Error(ACCESS_TOKEN_CHANNEL_MESSAGE);
|
|
11091
12337
|
return this.#client().channel(name, options);
|
|
11092
12338
|
}
|
|
11093
12339
|
/** The channels made with `channel()` and not yet removed. */
|
|
@@ -11099,7 +12345,9 @@ var NaroClient2 = class {
|
|
|
11099
12345
|
* one; the connection closes with the last channel.
|
|
11100
12346
|
*/
|
|
11101
12347
|
removeChannel(channel) {
|
|
11102
|
-
|
|
12348
|
+
if (this.#realtime === void 0)
|
|
12349
|
+
return Promise.resolve({ data: null, error: null });
|
|
12350
|
+
return this.#realtime.removeChannel(channel);
|
|
11103
12351
|
}
|
|
11104
12352
|
/** `removeChannel()` for every channel; the connection closes. */
|
|
11105
12353
|
removeAllChannels() {
|
|
@@ -11207,18 +12455,27 @@ function unwrap(result) {
|
|
|
11207
12455
|
}
|
|
11208
12456
|
var ONE_KEY = /^[\x21-\x7e]+$/;
|
|
11209
12457
|
async function objects(context) {
|
|
11210
|
-
const client = await context
|
|
12458
|
+
const client = await dataClient(context);
|
|
11211
12459
|
const projectId = await context.resolveProjectId();
|
|
11212
12460
|
const backend = await client.backend.info(projectId);
|
|
11213
|
-
|
|
12461
|
+
const environment = dataEnvironment(context.flags);
|
|
12462
|
+
if (backend.status === "none") {
|
|
11214
12463
|
throw new NaroError(
|
|
11215
12464
|
"BACKEND_NOT_READY",
|
|
11216
|
-
"This project has no backend yet. Run `naro backend enable`, then `naro storage enable`."
|
|
12465
|
+
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.`
|
|
12466
|
+
);
|
|
12467
|
+
}
|
|
12468
|
+
if (!backend.enabled)
|
|
12469
|
+
throw new NaroError(
|
|
12470
|
+
"BACKEND_OFF",
|
|
12471
|
+
forEnvironment(BACKEND_OFF_MESSAGE, environment)
|
|
11217
12472
|
);
|
|
11218
|
-
if (!backend.enabled) throw new NaroError("BACKEND_OFF", BACKEND_OFF_MESSAGE);
|
|
11219
12473
|
const state = await client.storage.info(projectId);
|
|
11220
12474
|
if (state.disabledAt !== null)
|
|
11221
|
-
throw new NaroError(
|
|
12475
|
+
throw new NaroError(
|
|
12476
|
+
"STORAGE_OFF",
|
|
12477
|
+
forEnvironment(STORAGE_OFF_MESSAGE, environment)
|
|
12478
|
+
);
|
|
11222
12479
|
let serviceKey = process.env.NARO_SERVICE_KEY?.trim() ?? "";
|
|
11223
12480
|
if (serviceKey !== "" && !ONE_KEY.test(serviceKey))
|
|
11224
12481
|
throw new NaroError(
|
|
@@ -11239,7 +12496,10 @@ async function objects(context) {
|
|
|
11239
12496
|
} catch (error) {
|
|
11240
12497
|
throw new NaroError(
|
|
11241
12498
|
"BACKEND_NOT_READY",
|
|
11242
|
-
|
|
12499
|
+
forEnvironment(
|
|
12500
|
+
`The project's backend url cannot be used (${printable(backend.url)}). Run \`naro backend\` to see where the backend got to.`,
|
|
12501
|
+
environment
|
|
12502
|
+
),
|
|
11243
12503
|
{ cause: error }
|
|
11244
12504
|
);
|
|
11245
12505
|
}
|
|
@@ -11313,7 +12573,7 @@ async function storageLs(context) {
|
|
|
11313
12573
|
"VALIDATION_FAILED",
|
|
11314
12574
|
`--search needs a naro:// path. ${LS_USAGE}`
|
|
11315
12575
|
);
|
|
11316
|
-
const client = await context
|
|
12576
|
+
const client = await dataClient(context);
|
|
11317
12577
|
const projectId2 = await context.resolveProjectId();
|
|
11318
12578
|
const buckets = await client.storage.listBuckets(projectId2);
|
|
11319
12579
|
context.output.result(
|
|
@@ -11405,7 +12665,7 @@ function stopOnInterrupt(onFirst) {
|
|
|
11405
12665
|
};
|
|
11406
12666
|
}
|
|
11407
12667
|
async function writeAtomically(bytes, destination) {
|
|
11408
|
-
const partial =
|
|
12668
|
+
const partial = join6(dirname4(destination), `.${randomUUID()}.naro-partial`);
|
|
11409
12669
|
const stopListening = removeOnInterrupt(partial);
|
|
11410
12670
|
try {
|
|
11411
12671
|
await pipeline2(bytes, createWriteStream2(partial, { flags: "wx" }));
|
|
@@ -11446,7 +12706,7 @@ function progressReporter(output, total) {
|
|
|
11446
12706
|
};
|
|
11447
12707
|
}
|
|
11448
12708
|
async function upload(context, local, target) {
|
|
11449
|
-
const file =
|
|
12709
|
+
const file = resolve8(context.cwd, local);
|
|
11450
12710
|
const info = await stat(file).catch(() => null);
|
|
11451
12711
|
if (info === null)
|
|
11452
12712
|
throw new NaroError(
|
|
@@ -11580,10 +12840,10 @@ async function download(context, source, local) {
|
|
|
11580
12840
|
"VALIDATION_FAILED",
|
|
11581
12841
|
"naro storage cp downloads one object: name it, naro://<bucket>/<path>."
|
|
11582
12842
|
);
|
|
11583
|
-
const resolved =
|
|
12843
|
+
const resolved = resolve8(context.cwd, local);
|
|
11584
12844
|
const isDirectory = local.endsWith("/") || (await stat(resolved).catch(() => null))?.isDirectory() === true;
|
|
11585
|
-
const destination = isDirectory ?
|
|
11586
|
-
const folder = await stat(
|
|
12845
|
+
const destination = isDirectory ? join6(resolved, savedAs(source.path)) : resolved;
|
|
12846
|
+
const folder = await stat(dirname4(destination)).catch(() => null);
|
|
11587
12847
|
if (folder === null || !folder.isDirectory())
|
|
11588
12848
|
throw new NaroError(
|
|
11589
12849
|
"VALIDATION_FAILED",
|
|
@@ -11970,12 +13230,14 @@ Usage:
|
|
|
11970
13230
|
naro env pull [file] Write env vars to .env.local
|
|
11971
13231
|
target is production, preview, all or an environment name; add and import use all when none is given
|
|
11972
13232
|
|
|
11973
|
-
naro environments List the project's environments: address, branch, protection, and how many the plan allows
|
|
13233
|
+
naro environments List the project's environments: address, branch, protection, data, and how many the plan allows
|
|
11974
13234
|
naro environments create <slug> [--branch <b>] [--copy-env preview|production|none] [--database schema|empty]
|
|
11975
13235
|
A QA or staging surface served at <subdomain>--<slug>; starts with preview's variables
|
|
11976
13236
|
naro environments set <slug> [--branch <b> | --no-branch] [--protection on|off]
|
|
11977
13237
|
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
|
|
13238
|
+
naro environments delete <slug> Delete it with its worker, protection key and data (asks first; --yes in CI)
|
|
13239
|
+
naro environments provision <slug>
|
|
13240
|
+
Make its data \u2014 database, backend, Storage \u2014 or go on after a stop
|
|
11979
13241
|
|
|
11980
13242
|
naro domains list | add <host> [--no-www] | remove <host>
|
|
11981
13243
|
add example.com also connects www.example.com and redirects the apex to
|
|
@@ -11984,6 +13246,9 @@ Usage:
|
|
|
11984
13246
|
naro domains verify [host] [--wait] Re-check DNS and print what is missing
|
|
11985
13247
|
exits non-zero while a domain is not live yet
|
|
11986
13248
|
|
|
13249
|
+
The data commands below (db, migration, gen, backend, policy, storage, realtime) take --env <name>:
|
|
13250
|
+
a named environment's data instead of production's, which preview shares
|
|
13251
|
+
(naro-cli 0.21.0 and older ignore --env on them and act on production)
|
|
11987
13252
|
naro db enable Create the project's database (Cloudflare D1)
|
|
11988
13253
|
naro db info Database status, size, table count and limits
|
|
11989
13254
|
and today's writes against the daily write limit (UTC)
|
|
@@ -12121,6 +13386,28 @@ Usage:
|
|
|
12121
13386
|
naro realtime tables remove <table>\u2026 [--yes]
|
|
12122
13387
|
Stop publishing tables' changes (asks first)
|
|
12123
13388
|
|
|
13389
|
+
naro functions [list] Naro Functions: name, version, verify_jwt, status and URL of each function
|
|
13390
|
+
naro functions new <name> Write naro/functions/<name>/index.ts, a handler to start from
|
|
13391
|
+
naro functions deploy [name\u2026] [--no-verify-jwt] [--import-map <file>] [--skip-check]
|
|
13392
|
+
Bundle on this machine (Node.js 22 or later), check that it starts, upload;
|
|
13393
|
+
every function in the folder when no name is given
|
|
13394
|
+
naro functions versions <name> Its stored versions, newest first
|
|
13395
|
+
naro functions rollback <name> [<version>]
|
|
13396
|
+
Put an earlier version live again (the one before the live one by default)
|
|
13397
|
+
naro functions delete <name> Delete it: its URL stops working (asks first; --yes to skip)
|
|
13398
|
+
naro functions logs <name> [--level <l>] [--follow]
|
|
13399
|
+
Its newest log lines
|
|
13400
|
+
naro functions secrets [list] The project's function secrets: names and digests, never values
|
|
13401
|
+
naro functions secrets set <NAME> Set one (value from a hidden prompt or stdin), then sync the functions
|
|
13402
|
+
(--no-sync leaves them to the control plane's sweep)
|
|
13403
|
+
naro functions secrets import <file>
|
|
13404
|
+
Set a dotenv file's lines (asks first; --yes in CI)
|
|
13405
|
+
naro functions secrets unset <NAME>\u2026
|
|
13406
|
+
Remove secrets (asks first)
|
|
13407
|
+
naro functions secrets sync [--registry]
|
|
13408
|
+
Upload out-of-date functions again until none is left; --registry also rewrites
|
|
13409
|
+
the function list in the project's database (after naro db restore)
|
|
13410
|
+
|
|
12124
13411
|
naro promote <deployment-id> [--to <environment>]
|
|
12125
13412
|
Publish a build to production, or to an environment with --to; it says what it will do, then asks.
|
|
12126
13413
|
A build moving between an environment and another surface is built again from its commit with the
|
|
@@ -12149,7 +13436,13 @@ Global options:
|
|
|
12149
13436
|
--browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
|
|
12150
13437
|
--os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
|
|
12151
13438
|
--to <environment> production (the default) or an environment name: where naro promote publishes the build (naro promote)
|
|
12152
|
-
--
|
|
13439
|
+
--no-verify-jwt Let calls in without a project JWT (naro functions deploy; wins over function.json)
|
|
13440
|
+
--import-map <file> The import map for functions whose folder has no deno.json (naro functions deploy)
|
|
13441
|
+
--skip-check Upload without booting the bundle in workerd first (naro functions deploy)
|
|
13442
|
+
--no-sync Leave the functions to the control plane's sweep (naro functions secrets set, import, unset; naro backend keys rotate)
|
|
13443
|
+
--registry Also rewrite the function list in the project's database (naro functions secrets sync)
|
|
13444
|
+
--env <e> production or preview (naro logs, naro insights: production by default; naro env pull, naro deploy: preview by default, or an environment name);
|
|
13445
|
+
production or an environment name for the data commands (naro db, migration, gen, backend, policy, storage, realtime: production by default)
|
|
12153
13446
|
--branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
|
|
12154
13447
|
branch of a project this deploy creates or updates (the deploy's own branch follows where it goes)
|
|
12155
13448
|
--no-branch No branch deploys to the environment any more (naro environments set; not with --branch)
|
|
@@ -12162,7 +13455,8 @@ Global options:
|
|
|
12162
13455
|
--dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
|
|
12163
13456
|
--output <file> Write to a file instead of stdout (naro gen types, db dump)
|
|
12164
13457
|
--lang <lang> Language of the types; typescript, the default, is the only one (naro gen types)
|
|
12165
|
-
--dir <path> Migrations directory (default ./migrations, or <--root>/migrations)
|
|
13458
|
+
--dir <path> Migrations directory (default ./migrations, or <--root>/migrations) (naro db push \u2026);
|
|
13459
|
+
functions directory (default ./naro/functions, or <--root>/naro/functions) (naro functions \u2026)
|
|
12166
13460
|
--strict Fail on warnings too, not only errors (naro db lint)
|
|
12167
13461
|
--schema-only Dump the schema without rows (naro db dump)
|
|
12168
13462
|
--data-only Dump rows without the schema (naro db dump)
|
|
@@ -12371,13 +13665,134 @@ function routeRealtime(words) {
|
|
|
12371
13665
|
return null;
|
|
12372
13666
|
}
|
|
12373
13667
|
}
|
|
13668
|
+
function routeFunctions(words) {
|
|
13669
|
+
const [sub] = words;
|
|
13670
|
+
switch (sub) {
|
|
13671
|
+
case void 0:
|
|
13672
|
+
case "list":
|
|
13673
|
+
case "ls":
|
|
13674
|
+
return words.length <= 1 ? { handler: runFunctionsList, positionals: [] } : null;
|
|
13675
|
+
case "versions":
|
|
13676
|
+
return { handler: runFunctionsVersions, positionals: words.slice(1) };
|
|
13677
|
+
case "new":
|
|
13678
|
+
return { handler: runFunctionsNew, positionals: words.slice(1) };
|
|
13679
|
+
case "deploy":
|
|
13680
|
+
return { handler: runFunctionsDeploy, positionals: words.slice(1) };
|
|
13681
|
+
case "rollback":
|
|
13682
|
+
return { handler: runFunctionsRollback, positionals: words.slice(1) };
|
|
13683
|
+
case "delete":
|
|
13684
|
+
case "remove":
|
|
13685
|
+
case "rm":
|
|
13686
|
+
return { handler: runFunctionsDelete, positionals: words.slice(1) };
|
|
13687
|
+
case "logs":
|
|
13688
|
+
return { handler: runFunctionsLogs, positionals: words.slice(1) };
|
|
13689
|
+
case "secrets": {
|
|
13690
|
+
const action = words[1];
|
|
13691
|
+
switch (action) {
|
|
13692
|
+
case void 0:
|
|
13693
|
+
case "list":
|
|
13694
|
+
case "ls":
|
|
13695
|
+
return words.length <= 2 ? { handler: runFunctionsSecretsList, positionals: [] } : null;
|
|
13696
|
+
case "set":
|
|
13697
|
+
return {
|
|
13698
|
+
handler: runFunctionsSecretsSet,
|
|
13699
|
+
positionals: words.slice(2)
|
|
13700
|
+
};
|
|
13701
|
+
case "import":
|
|
13702
|
+
return {
|
|
13703
|
+
handler: runFunctionsSecretsImport,
|
|
13704
|
+
positionals: words.slice(2)
|
|
13705
|
+
};
|
|
13706
|
+
case "unset":
|
|
13707
|
+
return {
|
|
13708
|
+
handler: runFunctionsSecretsUnset,
|
|
13709
|
+
positionals: words.slice(2)
|
|
13710
|
+
};
|
|
13711
|
+
case "sync":
|
|
13712
|
+
return words.length === 2 ? { handler: runFunctionsSecretsSync, positionals: [] } : null;
|
|
13713
|
+
default:
|
|
13714
|
+
return null;
|
|
13715
|
+
}
|
|
13716
|
+
}
|
|
13717
|
+
default:
|
|
13718
|
+
return null;
|
|
13719
|
+
}
|
|
13720
|
+
}
|
|
12374
13721
|
async function refuseTo() {
|
|
12375
13722
|
throw new NaroError("VALIDATION_FAILED", "--to is only for naro promote.");
|
|
12376
13723
|
}
|
|
13724
|
+
async function refuseFunctionsEnv() {
|
|
13725
|
+
throw new NaroError("VALIDATION_FAILED", FUNCTIONS_ENV_REFUSAL);
|
|
13726
|
+
}
|
|
13727
|
+
var DATA_COMMANDS = /* @__PURE__ */ new Set([
|
|
13728
|
+
"db",
|
|
13729
|
+
"migration",
|
|
13730
|
+
"gen",
|
|
13731
|
+
"backend",
|
|
13732
|
+
"policy",
|
|
13733
|
+
"storage",
|
|
13734
|
+
"realtime"
|
|
13735
|
+
]);
|
|
13736
|
+
var PROJECT_WIDE_BACKEND = /* @__PURE__ */ new Set(["enable", "provision", "disable"]);
|
|
13737
|
+
function dataEnvRefusal(parsed) {
|
|
13738
|
+
if (parsed.flags.env === void 0 || !DATA_COMMANDS.has(parsed.command ?? ""))
|
|
13739
|
+
return null;
|
|
13740
|
+
const sub = parsed.positionals[0];
|
|
13741
|
+
if (parsed.command === "backend" && sub !== void 0 && PROJECT_WIDE_BACKEND.has(sub) && parsed.flags.env !== "production")
|
|
13742
|
+
return new NaroError(
|
|
13743
|
+
"VALIDATION_FAILED",
|
|
13744
|
+
DATA_ENV_MESSAGES.projectWide(`naro backend ${sub}`)
|
|
13745
|
+
);
|
|
13746
|
+
try {
|
|
13747
|
+
dataEnvironment(parsed.flags);
|
|
13748
|
+
return null;
|
|
13749
|
+
} catch (error) {
|
|
13750
|
+
return error;
|
|
13751
|
+
}
|
|
13752
|
+
}
|
|
13753
|
+
function guidedError(error, parsed) {
|
|
13754
|
+
if (!isNaroError(error) || !DATA_COMMANDS.has(parsed.command ?? ""))
|
|
13755
|
+
return error;
|
|
13756
|
+
if (dataEnvRefusal(parsed) !== null) return error;
|
|
13757
|
+
let environment;
|
|
13758
|
+
try {
|
|
13759
|
+
environment = dataEnvironment(parsed.flags);
|
|
13760
|
+
} catch {
|
|
13761
|
+
return error;
|
|
13762
|
+
}
|
|
13763
|
+
if (environment === null) return error;
|
|
13764
|
+
const usage = error.message.indexOf("Usage: ");
|
|
13765
|
+
error.message = usage === -1 ? forEnvironment(error.message, environment) : `${forEnvironment(error.message.slice(0, usage), environment)}${error.message.slice(usage)}`;
|
|
13766
|
+
return error;
|
|
13767
|
+
}
|
|
13768
|
+
function unknownFunctionsWords(words) {
|
|
13769
|
+
const [sub = "", action = "", extra = ""] = words;
|
|
13770
|
+
if (sub === "list" || sub === "ls") return [sub, action];
|
|
13771
|
+
if (sub === "secrets")
|
|
13772
|
+
return action === "list" || action === "ls" || action === "sync" ? [sub, action, extra] : [sub, action];
|
|
13773
|
+
return [sub];
|
|
13774
|
+
}
|
|
13775
|
+
function unknownCommandMessage(parsed) {
|
|
13776
|
+
const words = parsed.command === "functions" ? [
|
|
13777
|
+
"functions",
|
|
13778
|
+
...unknownFunctionsWords(parsed.positionals).map(printable)
|
|
13779
|
+
] : [parsed.command, ...parsed.positionals];
|
|
13780
|
+
return `Unknown command: ${words.join(" ")}. See \`naro help\`.`;
|
|
13781
|
+
}
|
|
12377
13782
|
function route(parsed) {
|
|
12378
13783
|
const routed = routeCommand(parsed);
|
|
12379
13784
|
if (routed && parsed.flags.to !== void 0 && parsed.command !== "promote" && parsed.command !== "rollback")
|
|
12380
13785
|
return { handler: refuseTo, positionals: routed.positionals };
|
|
13786
|
+
if (routed && parsed.command === "functions" && parsed.flags.env !== void 0)
|
|
13787
|
+
return { handler: refuseFunctionsEnv, positionals: routed.positionals };
|
|
13788
|
+
const refusal = routed ? dataEnvRefusal(parsed) : null;
|
|
13789
|
+
if (routed && refusal)
|
|
13790
|
+
return {
|
|
13791
|
+
handler: async () => {
|
|
13792
|
+
throw refusal;
|
|
13793
|
+
},
|
|
13794
|
+
positionals: routed.positionals
|
|
13795
|
+
};
|
|
12381
13796
|
return routed;
|
|
12382
13797
|
}
|
|
12383
13798
|
function routeCommand(parsed) {
|
|
@@ -12499,6 +13914,11 @@ function routeCommand(parsed) {
|
|
|
12499
13914
|
handler: runEnvironmentsDelete,
|
|
12500
13915
|
positionals: positionals.slice(1)
|
|
12501
13916
|
};
|
|
13917
|
+
case "provision":
|
|
13918
|
+
return {
|
|
13919
|
+
handler: runEnvironmentsProvision,
|
|
13920
|
+
positionals: positionals.slice(1)
|
|
13921
|
+
};
|
|
12502
13922
|
default:
|
|
12503
13923
|
return null;
|
|
12504
13924
|
}
|
|
@@ -12623,6 +14043,8 @@ function routeCommand(parsed) {
|
|
|
12623
14043
|
return routeStorage(positionals);
|
|
12624
14044
|
case "realtime":
|
|
12625
14045
|
return routeRealtime(positionals);
|
|
14046
|
+
case "functions":
|
|
14047
|
+
return routeFunctions(positionals);
|
|
12626
14048
|
case "policy":
|
|
12627
14049
|
switch (sub) {
|
|
12628
14050
|
case void 0:
|
|
@@ -12666,19 +14088,6 @@ function routeCommand(parsed) {
|
|
|
12666
14088
|
return null;
|
|
12667
14089
|
}
|
|
12668
14090
|
}
|
|
12669
|
-
function version() {
|
|
12670
|
-
try {
|
|
12671
|
-
const pkg = JSON.parse(
|
|
12672
|
-
readFileSync6(
|
|
12673
|
-
join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
12674
|
-
"utf8"
|
|
12675
|
-
)
|
|
12676
|
-
);
|
|
12677
|
-
return pkg.version ?? "unknown";
|
|
12678
|
-
} catch {
|
|
12679
|
-
return "unknown";
|
|
12680
|
-
}
|
|
12681
|
-
}
|
|
12682
14091
|
|
|
12683
14092
|
// src/index.ts
|
|
12684
14093
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -12704,20 +14113,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
12704
14113
|
const output = new Output(parsed.flags.json, parsed.flags.debug);
|
|
12705
14114
|
if (parsed.unknown.length > 0) {
|
|
12706
14115
|
output.fail(
|
|
12707
|
-
new NaroError(
|
|
12708
|
-
"VALIDATION_FAILED",
|
|
12709
|
-
secretFlagProblem(parsed.unknown) ?? `Unknown option: ${parsed.unknown.join(", ")}. See \`naro help\`.`
|
|
12710
|
-
)
|
|
14116
|
+
new NaroError("VALIDATION_FAILED", unknownOptionProblem(parsed))
|
|
12711
14117
|
);
|
|
12712
14118
|
return;
|
|
12713
14119
|
}
|
|
12714
14120
|
const routed = route(parsed);
|
|
12715
14121
|
if (!routed) {
|
|
12716
14122
|
output.fail(
|
|
12717
|
-
new NaroError(
|
|
12718
|
-
"VALIDATION_FAILED",
|
|
12719
|
-
`Unknown command: ${[parsed.command, ...parsed.positionals].join(" ")}. See \`naro help\`.`
|
|
12720
|
-
)
|
|
14123
|
+
new NaroError("VALIDATION_FAILED", unknownCommandMessage(parsed))
|
|
12721
14124
|
);
|
|
12722
14125
|
return;
|
|
12723
14126
|
}
|
|
@@ -12725,7 +14128,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
12725
14128
|
try {
|
|
12726
14129
|
await routed.handler(context);
|
|
12727
14130
|
} catch (error) {
|
|
12728
|
-
context.output.fail(error);
|
|
14131
|
+
context.output.fail(guidedError(error, parsed));
|
|
12729
14132
|
}
|
|
12730
14133
|
}
|
|
12731
14134
|
void main();
|