naro-cli 0.22.0 → 0.24.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 +58 -0
- package/dist/args.d.ts +238 -0
- package/dist/{chunk-MBSO3K6L.js → chunk-XJIWWZ2T.js} +2264 -372
- 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/env-services.d.ts +44 -0
- package/dist/functions/deploy.d.ts +70 -0
- package/dist/functions/retry.d.ts +16 -0
- package/dist/index.js +1743 -721
- 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,28 +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,
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
envFlag,
|
|
50
|
+
forEnvironment,
|
|
37
51
|
formatRedirectEntry,
|
|
52
|
+
functionsFolder,
|
|
38
53
|
generateTypes,
|
|
39
54
|
indexesFromRows,
|
|
40
55
|
isBookmark,
|
|
41
56
|
isEnvTargetName,
|
|
42
57
|
isExpired,
|
|
58
|
+
isFunctionName,
|
|
59
|
+
isInteractive,
|
|
43
60
|
isNaroError,
|
|
61
|
+
isPrintable,
|
|
44
62
|
isReservedEnvTarget,
|
|
45
63
|
isStalledBackend,
|
|
46
64
|
isTerminalStatus,
|
|
@@ -51,19 +69,22 @@ import {
|
|
|
51
69
|
login,
|
|
52
70
|
maskEnvValue,
|
|
53
71
|
migrationNameMismatch,
|
|
72
|
+
migrationsDir,
|
|
54
73
|
migrationsPendingError,
|
|
55
74
|
nextMigrationFileName,
|
|
56
75
|
openBrowser,
|
|
57
76
|
parseCacheControl,
|
|
58
77
|
parseDotenvDetailed,
|
|
59
78
|
parseGitHubRemote,
|
|
79
|
+
parseJsonc,
|
|
60
80
|
parsePolicyNode,
|
|
61
81
|
parsePolicyStatement,
|
|
62
82
|
parseRedirectEntry,
|
|
63
83
|
pendingMigrationNames,
|
|
64
|
-
planPolicyMigration,
|
|
65
84
|
policiesForTypes,
|
|
66
85
|
policiesLostByReset,
|
|
86
|
+
policyProblems,
|
|
87
|
+
printable,
|
|
67
88
|
projectPath,
|
|
68
89
|
promoteTargetUnsupported,
|
|
69
90
|
promptForMissingEnv,
|
|
@@ -71,7 +92,9 @@ import {
|
|
|
71
92
|
quoteIdent,
|
|
72
93
|
quotePolicyName,
|
|
73
94
|
readProjectLink,
|
|
95
|
+
readStdin,
|
|
74
96
|
refreshStoredSession,
|
|
97
|
+
relativeToCwd,
|
|
75
98
|
removeProjectLink,
|
|
76
99
|
renderRows,
|
|
77
100
|
resetFileRefusedMessage,
|
|
@@ -79,14 +102,17 @@ import {
|
|
|
79
102
|
resetPoliciesWithTablesMessage,
|
|
80
103
|
resetStorageOutdatedMessage,
|
|
81
104
|
resolveAuth,
|
|
82
|
-
|
|
105
|
+
retryAfterSeconds,
|
|
106
|
+
runMigrationList,
|
|
107
|
+
runMigrationNew,
|
|
108
|
+
runMigrationRepair,
|
|
109
|
+
runMigrationUp,
|
|
83
110
|
saveSession,
|
|
84
111
|
schemaMigrationTitle,
|
|
85
112
|
serializeDotenv,
|
|
86
113
|
slugify,
|
|
87
114
|
snapshotFromRows,
|
|
88
115
|
splitPolicyStatements,
|
|
89
|
-
splitStatements,
|
|
90
116
|
storageRefusalOfFiles,
|
|
91
117
|
storedPolicySql,
|
|
92
118
|
summarizeStatement,
|
|
@@ -98,8 +124,11 @@ import {
|
|
|
98
124
|
unreadablePolicyComment,
|
|
99
125
|
usageMonthOf,
|
|
100
126
|
userForAccessToken,
|
|
127
|
+
version,
|
|
128
|
+
warningLines,
|
|
129
|
+
withFunctionRetries,
|
|
101
130
|
writeProjectLink
|
|
102
|
-
} from "./chunk-
|
|
131
|
+
} from "./chunk-XJIWWZ2T.js";
|
|
103
132
|
import "./chunk-K3NQKI34.js";
|
|
104
133
|
|
|
105
134
|
// src/args.ts
|
|
@@ -139,7 +168,11 @@ var BOOLEAN_FLAGS = {
|
|
|
139
168
|
"--recursive": "recursive",
|
|
140
169
|
"-r": "recursive",
|
|
141
170
|
"--upsert": "upsert",
|
|
142
|
-
"--no-branch": "noBranch"
|
|
171
|
+
"--no-branch": "noBranch",
|
|
172
|
+
"--no-verify-jwt": "noVerifyJwt",
|
|
173
|
+
"--skip-check": "skipCheck",
|
|
174
|
+
"--no-sync": "noSync",
|
|
175
|
+
"--registry": "registry"
|
|
143
176
|
};
|
|
144
177
|
var REPEATED_FLAGS = {
|
|
145
178
|
"--column": "columns"
|
|
@@ -199,7 +232,8 @@ var STRING_FLAGS = {
|
|
|
199
232
|
"--copy-env": "copyEnv",
|
|
200
233
|
"--database": "database",
|
|
201
234
|
"--protection": "protection",
|
|
202
|
-
"--to": "to"
|
|
235
|
+
"--to": "to",
|
|
236
|
+
"--import-map": "importMap"
|
|
203
237
|
};
|
|
204
238
|
var SECRET_FLAGS = {
|
|
205
239
|
"--api-key": "--api-key-stdin",
|
|
@@ -214,6 +248,13 @@ function secretFlagProblem(unknown) {
|
|
|
214
248
|
}
|
|
215
249
|
return null;
|
|
216
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
|
+
}
|
|
217
258
|
function listFlag(value) {
|
|
218
259
|
if (value === void 0) return [];
|
|
219
260
|
return value.split(",").map((piece) => piece.trim()).filter((piece) => piece.length > 0);
|
|
@@ -373,123 +414,6 @@ var Output = class {
|
|
|
373
414
|
}
|
|
374
415
|
};
|
|
375
416
|
|
|
376
|
-
// src/prompt.ts
|
|
377
|
-
import { createInterface } from "readline";
|
|
378
|
-
import { Writable } from "stream";
|
|
379
|
-
function confirmHint(defaultYes) {
|
|
380
|
-
return defaultYes ? "(Y/n)" : "(y/N)";
|
|
381
|
-
}
|
|
382
|
-
function readConfirmAnswer(answer, defaultYes) {
|
|
383
|
-
const normalized = answer.trim().toLowerCase();
|
|
384
|
-
if (normalized === "") return defaultYes;
|
|
385
|
-
if (normalized === "y" || normalized === "yes") return true;
|
|
386
|
-
if (!defaultYes) return false;
|
|
387
|
-
if (normalized === "n" || normalized === "no") return false;
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
function isInteractive() {
|
|
391
|
-
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
392
|
-
}
|
|
393
|
-
async function readStdin() {
|
|
394
|
-
let data = "";
|
|
395
|
-
for await (const chunk2 of process.stdin) data += String(chunk2);
|
|
396
|
-
return data.replace(/\r?\n$/, "");
|
|
397
|
-
}
|
|
398
|
-
var TtyPrompter = class {
|
|
399
|
-
async ask(question) {
|
|
400
|
-
const rl = createInterface({
|
|
401
|
-
input: process.stdin,
|
|
402
|
-
output: process.stdout
|
|
403
|
-
});
|
|
404
|
-
try {
|
|
405
|
-
return await new Promise(
|
|
406
|
-
(resolve7) => rl.question(question, resolve7)
|
|
407
|
-
);
|
|
408
|
-
} finally {
|
|
409
|
-
rl.close();
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
async askHidden(question) {
|
|
413
|
-
let muted = false;
|
|
414
|
-
const output = new Writable({
|
|
415
|
-
write(chunk2, _enc, done) {
|
|
416
|
-
if (!muted) process.stdout.write(chunk2);
|
|
417
|
-
done();
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
const rl = createInterface({
|
|
421
|
-
input: process.stdin,
|
|
422
|
-
output,
|
|
423
|
-
terminal: true
|
|
424
|
-
});
|
|
425
|
-
try {
|
|
426
|
-
const answer = new Promise(
|
|
427
|
-
(resolve7) => rl.question(question, resolve7)
|
|
428
|
-
);
|
|
429
|
-
muted = true;
|
|
430
|
-
const value = await answer;
|
|
431
|
-
process.stdout.write("\n");
|
|
432
|
-
return value;
|
|
433
|
-
} finally {
|
|
434
|
-
rl.close();
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
async confirm(question, options = {}) {
|
|
438
|
-
const defaultYes = options.defaultYes ?? false;
|
|
439
|
-
for (; ; ) {
|
|
440
|
-
const read = readConfirmAnswer(
|
|
441
|
-
await this.ask(`${question} ${confirmHint(defaultYes)} `),
|
|
442
|
-
defaultYes
|
|
443
|
-
);
|
|
444
|
-
if (read !== null) return read;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
async select(question, options) {
|
|
448
|
-
process.stdout.write(`${question}
|
|
449
|
-
`);
|
|
450
|
-
options.forEach((option, index) => {
|
|
451
|
-
process.stdout.write(` ${String(index + 1)}. ${option}
|
|
452
|
-
`);
|
|
453
|
-
});
|
|
454
|
-
for (; ; ) {
|
|
455
|
-
const answer = Number.parseInt(
|
|
456
|
-
(await this.ask(`Choose 1-${String(options.length)}: `)).trim(),
|
|
457
|
-
10
|
|
458
|
-
);
|
|
459
|
-
if (Number.isInteger(answer) && answer >= 1 && answer <= options.length)
|
|
460
|
-
return answer - 1;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
var NonInteractivePrompter = class {
|
|
465
|
-
constructor(assumeYes) {
|
|
466
|
-
this.assumeYes = assumeYes;
|
|
467
|
-
}
|
|
468
|
-
refuse(question) {
|
|
469
|
-
throw new NaroError(
|
|
470
|
-
"VALIDATION_FAILED",
|
|
471
|
-
`Interactive input required (${question.trim()}). Pass the value via flags/stdin or run without --json in a terminal.`
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
async ask(question) {
|
|
475
|
-
this.refuse(question);
|
|
476
|
-
}
|
|
477
|
-
async askHidden(question) {
|
|
478
|
-
this.refuse(question);
|
|
479
|
-
}
|
|
480
|
-
async confirm() {
|
|
481
|
-
return this.assumeYes;
|
|
482
|
-
}
|
|
483
|
-
async select(question) {
|
|
484
|
-
this.refuse(question);
|
|
485
|
-
}
|
|
486
|
-
};
|
|
487
|
-
function createPrompter(options) {
|
|
488
|
-
if (options.yes || options.json || !isInteractive())
|
|
489
|
-
return new NonInteractivePrompter(options.yes);
|
|
490
|
-
return new TtyPrompter();
|
|
491
|
-
}
|
|
492
|
-
|
|
493
417
|
// src/context.ts
|
|
494
418
|
function createContext(flags, positionals = []) {
|
|
495
419
|
const cwd = flags.cwd ?? process.cwd();
|
|
@@ -523,7 +447,8 @@ function createContext(flags, positionals = []) {
|
|
|
523
447
|
const resolved = await auth();
|
|
524
448
|
resolvedClient = new NaroClient({
|
|
525
449
|
baseUrl: controlPlaneUrl(),
|
|
526
|
-
getAccessToken: () => resolved.getAccessToken()
|
|
450
|
+
getAccessToken: () => resolved.getAccessToken(),
|
|
451
|
+
clientName: `naro-cli/${version()}`
|
|
527
452
|
});
|
|
528
453
|
return resolvedClient;
|
|
529
454
|
},
|
|
@@ -540,295 +465,6 @@ function createContext(flags, positionals = []) {
|
|
|
540
465
|
};
|
|
541
466
|
}
|
|
542
467
|
|
|
543
|
-
// src/router.ts
|
|
544
|
-
import { readFileSync as readFileSync6 } from "fs";
|
|
545
|
-
import { dirname as dirname4, join as join6 } from "path";
|
|
546
|
-
import { fileURLToPath } from "url";
|
|
547
|
-
|
|
548
|
-
// src/data-environment.ts
|
|
549
|
-
var DATA_ENV_MESSAGES = {
|
|
550
|
-
name: "--env must be production or an environment name.",
|
|
551
|
-
preview: "--env preview has no data of its own: preview deployments use production's. Drop --env, or name an environment.",
|
|
552
|
-
projectWide: (command) => `--env is not an option of ${command}: turning the backend on or off applies to every environment.`
|
|
553
|
-
};
|
|
554
|
-
function dataEnvironment(flags) {
|
|
555
|
-
const value = flags.env;
|
|
556
|
-
if (value === void 0 || value === "production") return null;
|
|
557
|
-
if (value === "preview")
|
|
558
|
-
throw new NaroError("VALIDATION_FAILED", DATA_ENV_MESSAGES.preview);
|
|
559
|
-
if (environmentSlugProblem(value) !== null)
|
|
560
|
-
throw new NaroError("VALIDATION_FAILED", DATA_ENV_MESSAGES.name);
|
|
561
|
-
return value;
|
|
562
|
-
}
|
|
563
|
-
async function dataClient(context) {
|
|
564
|
-
return (await context.client()).inEnvironment(dataEnvironment(context.flags));
|
|
565
|
-
}
|
|
566
|
-
function databaseName(environment) {
|
|
567
|
-
return environment === null ? "the project database" : `the ${environment} environment's database`;
|
|
568
|
-
}
|
|
569
|
-
function enableBackendCommand(environment) {
|
|
570
|
-
return environment === null ? "naro backend enable" : `naro environments provision ${environment}`;
|
|
571
|
-
}
|
|
572
|
-
function envFlag(environment) {
|
|
573
|
-
return environment === null ? "" : ` --env ${environment}`;
|
|
574
|
-
}
|
|
575
|
-
function forEnvironment(text, environment) {
|
|
576
|
-
return environmentGuidance(text, environment);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// src/commands/migration.ts
|
|
580
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
581
|
-
import { join, relative } from "path";
|
|
582
|
-
|
|
583
|
-
// src/commands/confirm.ts
|
|
584
|
-
function cannotAsk(context) {
|
|
585
|
-
return context.flags.json || !isInteractive();
|
|
586
|
-
}
|
|
587
|
-
async function confirmOrAbort(context, question) {
|
|
588
|
-
if (context.flags.yes) return;
|
|
589
|
-
if (cannotAsk(context)) {
|
|
590
|
-
throw new NaroError(
|
|
591
|
-
"CONFIRMATION_REQUIRED",
|
|
592
|
-
`${question} Re-run with --yes to confirm.`
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
const confirmed = await context.prompter.confirm(question);
|
|
596
|
-
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// src/commands/migration.ts
|
|
600
|
-
var processStdin = {
|
|
601
|
-
stdinIsTty: () => process.stdin.isTTY === true,
|
|
602
|
-
readStdin
|
|
603
|
-
};
|
|
604
|
-
function migrationsDir(context) {
|
|
605
|
-
return resolveMigrationsDir({
|
|
606
|
-
baseDir: context.cwd,
|
|
607
|
-
rootDirectory: context.flags.root,
|
|
608
|
-
dir: context.flags.dir
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
function relativeToCwd(context, path) {
|
|
612
|
-
const rel = relative(context.cwd, path);
|
|
613
|
-
return rel === "" ? "." : rel;
|
|
614
|
-
}
|
|
615
|
-
async function appliedMigrations(client, projectId) {
|
|
616
|
-
try {
|
|
617
|
-
return await client.db.migrations(projectId);
|
|
618
|
-
} catch (error) {
|
|
619
|
-
if (isNaroError(error) && error.code === "DATABASE_NOT_READY") return [];
|
|
620
|
-
throw error;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
var UNPRINTABLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
624
|
-
function printable(text) {
|
|
625
|
-
return text.replace(UNPRINTABLE, "?");
|
|
626
|
-
}
|
|
627
|
-
function isPrintable(text) {
|
|
628
|
-
return printable(text) === text;
|
|
629
|
-
}
|
|
630
|
-
var UNSHOWN_WARNING_LINE = "(a line of this warning has characters the terminal cannot show; run with --json to see it exactly)";
|
|
631
|
-
function warningLines(message) {
|
|
632
|
-
return message.split("\n").map(
|
|
633
|
-
(line, index) => index === 0 ? `\u26A0 ${printable(line)}` : ` ${isPrintable(line) ? line : UNSHOWN_WARNING_LINE}`
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
function policyProblems(dir, names) {
|
|
637
|
-
return names.flatMap((name) => {
|
|
638
|
-
const split = splitStatements(readFileSync(join(dir, name), "utf8"));
|
|
639
|
-
if (!split.ok) return [];
|
|
640
|
-
const planned = planPolicyMigration(
|
|
641
|
-
split.statements.map((statement) => statement.text)
|
|
642
|
-
);
|
|
643
|
-
return planned.ok ? [] : [{ migration: name, message: planned.message }];
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
function warningLine(warning) {
|
|
647
|
-
if (warning === "out-of-order")
|
|
648
|
-
return " \u26A0 older than an applied migration \u2014 db push will still apply it";
|
|
649
|
-
if (warning === "ledger-name-missing-extension")
|
|
650
|
-
return " \u26A0 recorded without .sql in the ledger";
|
|
651
|
-
return null;
|
|
652
|
-
}
|
|
653
|
-
async function runMigrationNew(context, deps = processStdin) {
|
|
654
|
-
const [name] = context.positionalOrThrow(
|
|
655
|
-
1,
|
|
656
|
-
"Usage: naro migration new <name>"
|
|
657
|
-
);
|
|
658
|
-
const dir = migrationsDir(context);
|
|
659
|
-
const fileName = nextMigrationFileName(
|
|
660
|
-
listMigrationFiles(dir),
|
|
661
|
-
name
|
|
662
|
-
);
|
|
663
|
-
mkdirSync(dir, { recursive: true });
|
|
664
|
-
const piped = !deps.stdinIsTty() ? await deps.readStdin() : "";
|
|
665
|
-
const fromStdin = piped.length > 0;
|
|
666
|
-
const body = fromStdin ? piped.endsWith("\n") ? piped : `${piped}
|
|
667
|
-
` : `-- Migration: ${fileName}
|
|
668
|
-
`;
|
|
669
|
-
const path = join(dir, fileName);
|
|
670
|
-
writeFileSync(path, body, { flag: "wx" });
|
|
671
|
-
const relPath = relativeToCwd(context, path);
|
|
672
|
-
context.output.result({ file: fileName, path: relPath, fromStdin }, [
|
|
673
|
-
`\u2705 Created ${relPath}`
|
|
674
|
-
]);
|
|
675
|
-
}
|
|
676
|
-
async function runMigrationList(context) {
|
|
677
|
-
const client = await dataClient(context);
|
|
678
|
-
const projectId = await context.resolveProjectId();
|
|
679
|
-
const dir = migrationsDir(context);
|
|
680
|
-
const local = listMigrationFiles(dir);
|
|
681
|
-
const ledger = await appliedMigrations(client, projectId);
|
|
682
|
-
const migrations = compareMigrations(local, ledger);
|
|
683
|
-
const relDir = relativeToCwd(context, dir);
|
|
684
|
-
const lines = migrations.length === 0 ? ["No migrations yet. Create one with `naro migration new <name>`."] : [
|
|
685
|
-
`Migrations in ${relDir} (${String(migrations.length)}):`,
|
|
686
|
-
...migrations.flatMap((row) => {
|
|
687
|
-
const line = ` ${row.status.padEnd(12)}${printable(row.name)}${row.appliedAt ? ` ${printable(row.appliedAt)}` : ""}`;
|
|
688
|
-
const warning = warningLine(row.warning);
|
|
689
|
-
return warning ? [line, warning] : [line];
|
|
690
|
-
})
|
|
691
|
-
];
|
|
692
|
-
context.output.result({ projectId, dir: relDir, migrations }, lines);
|
|
693
|
-
}
|
|
694
|
-
async function applyMigrationFiles(context, client, projectId, dir, names) {
|
|
695
|
-
const applied = [];
|
|
696
|
-
const warnings = [];
|
|
697
|
-
for (const name of names) {
|
|
698
|
-
const sql = readFileSync(join(dir, name), "utf8");
|
|
699
|
-
let answer;
|
|
700
|
-
try {
|
|
701
|
-
answer = await client.db.applyMigration(projectId, { name, sql });
|
|
702
|
-
} catch (error) {
|
|
703
|
-
if (!isNaroError(error)) throw error;
|
|
704
|
-
for (const message of serverWarnings(error.details))
|
|
705
|
-
for (const line of warningLines(message)) context.output.warn(line);
|
|
706
|
-
const shown = context.output.json ? name : printable(name);
|
|
707
|
-
throw new NaroError(error.code, `${shown}: ${error.message}`, {
|
|
708
|
-
...error.status === void 0 ? {} : { status: error.status },
|
|
709
|
-
details: {
|
|
710
|
-
...error.details,
|
|
711
|
-
migration: name,
|
|
712
|
-
applied,
|
|
713
|
-
appliedWarnings: warnings
|
|
714
|
-
},
|
|
715
|
-
cause: error
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
context.output.log(`\u2713 ${printable(name)}`);
|
|
719
|
-
applied.push(name);
|
|
720
|
-
for (const message of answer.warnings) {
|
|
721
|
-
warnings.push({ migration: name, message });
|
|
722
|
-
for (const line of warningLines(message)) context.output.warn(line);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
return { applied, warnings };
|
|
726
|
-
}
|
|
727
|
-
function serverWarnings(details) {
|
|
728
|
-
const warnings = details?.warnings;
|
|
729
|
-
return Array.isArray(warnings) ? warnings.filter(
|
|
730
|
-
(warning) => typeof warning === "string"
|
|
731
|
-
) : [];
|
|
732
|
-
}
|
|
733
|
-
var MIGRATION_NAME_RE = /^[A-Za-z0-9_.-]{1,200}\.sql$/;
|
|
734
|
-
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)";
|
|
735
|
-
var LEDGER_INSERT = "INSERT INTO d1_migrations (name) SELECT ? WHERE NOT EXISTS (SELECT 1 FROM d1_migrations WHERE name = ?)";
|
|
736
|
-
var LEDGER_DELETE = "DELETE FROM d1_migrations WHERE name IN (?, ?)";
|
|
737
|
-
function withoutSuffix(key) {
|
|
738
|
-
return key.slice(0, -".sql".length);
|
|
739
|
-
}
|
|
740
|
-
async function runMigrationRepair(context) {
|
|
741
|
-
const [name] = context.positionalOrThrow(
|
|
742
|
-
1,
|
|
743
|
-
"Usage: naro migration repair <name> --status applied|reverted"
|
|
744
|
-
);
|
|
745
|
-
const key = name;
|
|
746
|
-
if (!MIGRATION_NAME_RE.test(key))
|
|
747
|
-
throw new NaroError(
|
|
748
|
-
"VALIDATION_FAILED",
|
|
749
|
-
"name must end with .sql and contain only letters, digits, _ . -"
|
|
750
|
-
);
|
|
751
|
-
const status = context.flags.status;
|
|
752
|
-
if (status !== "applied" && status !== "reverted")
|
|
753
|
-
throw new NaroError(
|
|
754
|
-
"VALIDATION_FAILED",
|
|
755
|
-
"Pass --status applied or --status reverted."
|
|
756
|
-
);
|
|
757
|
-
const client = await dataClient(context);
|
|
758
|
-
const projectId = await context.resolveProjectId();
|
|
759
|
-
const ledger = await appliedMigrations(client, projectId);
|
|
760
|
-
const recorded = ledger.some(
|
|
761
|
-
(row) => row.name === key || row.name === withoutSuffix(key)
|
|
762
|
-
);
|
|
763
|
-
const nothingToDo = status === "applied" ? `${key} was already applied; nothing to do.` : `${key} was not in the ledger; nothing to do.`;
|
|
764
|
-
if (status === "applied" ? recorded : !recorded) {
|
|
765
|
-
context.output.warn(nothingToDo);
|
|
766
|
-
context.output.result({ name: key, status, changed: false });
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
await confirmOrAbort(
|
|
770
|
-
context,
|
|
771
|
-
status === "applied" ? `Mark ${key} as applied in ${databaseName(dataEnvironment(context.flags))}'s ledger? Its SQL is not run.` : `Remove ${key} from ${databaseName(dataEnvironment(context.flags))}'s ledger? Its SQL is not rolled back.`
|
|
772
|
-
);
|
|
773
|
-
if (status === "applied" && ledger.length === 0)
|
|
774
|
-
await client.db.query(projectId, { sql: LEDGER_DDL });
|
|
775
|
-
const answer = await client.db.query(projectId, {
|
|
776
|
-
sql: status === "applied" ? LEDGER_INSERT : LEDGER_DELETE,
|
|
777
|
-
params: status === "applied" ? [key, key] : [key, withoutSuffix(key)]
|
|
778
|
-
});
|
|
779
|
-
const changed = (answer.statements[0]?.meta.changes ?? 0) > 0;
|
|
780
|
-
if (!changed) {
|
|
781
|
-
context.output.warn(nothingToDo);
|
|
782
|
-
context.output.result({ name: key, status, changed: false });
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
context.output.result({ name: key, status, changed: true }, [
|
|
786
|
-
status === "applied" ? `\u2713 Marked ${key} as applied.` : `\u2713 Removed ${key} from the ledger.`
|
|
787
|
-
]);
|
|
788
|
-
}
|
|
789
|
-
async function runMigrationUp(context) {
|
|
790
|
-
const client = await dataClient(context);
|
|
791
|
-
const projectId = await context.resolveProjectId();
|
|
792
|
-
const dir = migrationsDir(context);
|
|
793
|
-
const local = listMigrationFiles(dir);
|
|
794
|
-
const ledger = await appliedMigrations(client, projectId);
|
|
795
|
-
const pending = compareMigrations(local, ledger).filter((row) => row.status === "pending").map((row) => row.name);
|
|
796
|
-
if (pending.length === 0) {
|
|
797
|
-
context.output.result({ projectId, applied: [], pending: [] }, [
|
|
798
|
-
"No pending migrations."
|
|
799
|
-
]);
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
if (context.flags.dryRun) {
|
|
803
|
-
const problems = policyProblems(dir, pending);
|
|
804
|
-
context.output.result({ projectId, dryRun: true, pending, problems }, [
|
|
805
|
-
`Pending migrations (${String(pending.length)}):`,
|
|
806
|
-
...pending.map((name) => ` ${printable(name)}`),
|
|
807
|
-
...problems.map(
|
|
808
|
-
(problem) => `${printable(problem.migration)}: ${printable(problem.message)}`
|
|
809
|
-
)
|
|
810
|
-
]);
|
|
811
|
-
if (problems.length > 0) process.exitCode = 1;
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
context.output.log(`Pending migrations (${String(pending.length)}):`);
|
|
815
|
-
for (const name of pending) context.output.log(` ${printable(name)}`);
|
|
816
|
-
await confirmOrAbort(
|
|
817
|
-
context,
|
|
818
|
-
`Apply ${String(pending.length)} migration(s) to ${databaseName(dataEnvironment(context.flags))}? Each file is applied all-or-nothing; a failure stops the run.`
|
|
819
|
-
);
|
|
820
|
-
const { applied, warnings } = await applyMigrationFiles(
|
|
821
|
-
context,
|
|
822
|
-
client,
|
|
823
|
-
projectId,
|
|
824
|
-
dir,
|
|
825
|
-
pending
|
|
826
|
-
);
|
|
827
|
-
context.output.result({ projectId, applied, pending: [], warnings }, [
|
|
828
|
-
`\u2705 Applied ${String(applied.length)} migration(s).`
|
|
829
|
-
]);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
468
|
// src/commands/filters.ts
|
|
833
469
|
function filtersOf(flags, keys) {
|
|
834
470
|
const filters = {};
|
|
@@ -1213,6 +849,86 @@ async function runToken(context) {
|
|
|
1213
849
|
);
|
|
1214
850
|
}
|
|
1215
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
|
+
|
|
1216
932
|
// src/commands/backend.ts
|
|
1217
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.";
|
|
1218
934
|
var ROTATE_USAGE = "Usage: naro backend keys rotate --anon | --service";
|
|
@@ -1413,6 +1129,57 @@ function rotationWarning(kind, backend, environment = null) {
|
|
|
1413
1129
|
return `\u26A0 The ${kind} key is replaced; another provisioning run is finishing the backend. Check it with \`naro backend${envFlag(environment)}\`.`;
|
|
1414
1130
|
return null;
|
|
1415
1131
|
}
|
|
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
|
+
}
|
|
1416
1183
|
function checkKeysSentence(kind, environment = null) {
|
|
1417
1184
|
const keys = kind === "service" ? "naro backend keys --reveal" : "naro backend keys";
|
|
1418
1185
|
return `Run \`${keys}${envFlag(environment)}\` to see which key is live, then \`${enableBackendCommand(environment)}\`.`;
|
|
@@ -1425,13 +1192,14 @@ function outcomeUnknown(error) {
|
|
|
1425
1192
|
}
|
|
1426
1193
|
async function runBackendKeysRotate(context) {
|
|
1427
1194
|
const kind = rotateKind(context);
|
|
1428
|
-
await confirmOrAbort(
|
|
1429
|
-
context,
|
|
1430
|
-
`Replace the ${kind} key? Every client still sending the old one gets 401 INVALID_API_KEY as soon as the new key is live.`
|
|
1431
|
-
);
|
|
1432
1195
|
const client = await dataClient(context);
|
|
1433
1196
|
const environment = dataEnvironment(context.flags);
|
|
1434
1197
|
const projectId = await context.resolveProjectId();
|
|
1198
|
+
const functions = environment === null ? await liveFunctionCount(client, projectId) : 0;
|
|
1199
|
+
await confirmOrAbort(
|
|
1200
|
+
context,
|
|
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)}` : ""}`
|
|
1202
|
+
);
|
|
1435
1203
|
let rotated;
|
|
1436
1204
|
try {
|
|
1437
1205
|
rotated = await client.backend.rotateKey(projectId, kind);
|
|
@@ -1454,20 +1222,48 @@ async function runBackendKeysRotate(context) {
|
|
|
1454
1222
|
}
|
|
1455
1223
|
const { backend, key } = rotated;
|
|
1456
1224
|
const warning = rotationWarning(kind, backend, environment);
|
|
1225
|
+
const synced = functions > 0 && !context.flags.noSync ? await syncAfterRotation(context, client, projectId, kind, functions) : null;
|
|
1457
1226
|
if (kind === "service") context.output.warn(SERVICE_KEY_WARNING);
|
|
1458
1227
|
if (warning) context.output.warn(warning);
|
|
1459
1228
|
context.output.result(
|
|
1460
|
-
{
|
|
1229
|
+
{
|
|
1230
|
+
projectId,
|
|
1231
|
+
kind,
|
|
1232
|
+
key,
|
|
1233
|
+
backend,
|
|
1234
|
+
...warning ? { warning } : {},
|
|
1235
|
+
...synced ? { functions: synced } : {}
|
|
1236
|
+
},
|
|
1461
1237
|
[
|
|
1462
1238
|
`\u2705 Replaced the ${kind} key. The old one no longer works.`,
|
|
1463
1239
|
`${keyLabel(kind)}${key}`,
|
|
1464
|
-
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
|
+
] : []
|
|
1465
1244
|
]
|
|
1466
1245
|
);
|
|
1467
1246
|
}
|
|
1247
|
+
async function runBackendSigningKeyRotate(context) {
|
|
1248
|
+
const client = await dataClient(context);
|
|
1249
|
+
const environment = dataEnvironment(context.flags);
|
|
1250
|
+
const projectId = await context.resolveProjectId();
|
|
1251
|
+
await confirmOrAbort(
|
|
1252
|
+
context,
|
|
1253
|
+
"Replace the key that signs access tokens? New tokens are signed with the new key at once; tokens already issued keep working until they expire (up to 15 minutes). Nobody is signed out."
|
|
1254
|
+
);
|
|
1255
|
+
const rotated = await client.backend.rotateSigningKey(projectId);
|
|
1256
|
+
context.output.result(
|
|
1257
|
+
{ projectId, ...rotated },
|
|
1258
|
+
[
|
|
1259
|
+
`\u2705 Replaced the signing key (kid ${rotated.kid}).`,
|
|
1260
|
+
`Tokens signed with the old key work until ${rotated.previousKeysExpireAt}.`
|
|
1261
|
+
].map((line) => forEnvironment(line, environment))
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1468
1264
|
|
|
1469
1265
|
// src/commands/backend-auth.ts
|
|
1470
|
-
var
|
|
1266
|
+
var processStdin = {
|
|
1471
1267
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1472
1268
|
readStdin
|
|
1473
1269
|
};
|
|
@@ -1624,7 +1420,7 @@ async function change(context, send2, done, after) {
|
|
|
1624
1420
|
)
|
|
1625
1421
|
);
|
|
1626
1422
|
}
|
|
1627
|
-
async function runBackendAuthEmailSet(context, input =
|
|
1423
|
+
async function runBackendAuthEmailSet(context, input = processStdin) {
|
|
1628
1424
|
const { provider, from } = context.flags;
|
|
1629
1425
|
if (provider === void 0 || from === void 0)
|
|
1630
1426
|
throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.email);
|
|
@@ -1670,7 +1466,7 @@ function providerOf(context, usage) {
|
|
|
1670
1466
|
throw new NaroError("VALIDATION_FAILED", usage);
|
|
1671
1467
|
return provider;
|
|
1672
1468
|
}
|
|
1673
|
-
async function runBackendAuthOAuthSet(context, input =
|
|
1469
|
+
async function runBackendAuthOAuthSet(context, input = processStdin) {
|
|
1674
1470
|
const provider = providerOf(context, AUTH_USAGE.oauth);
|
|
1675
1471
|
const clientId = context.flags.clientId;
|
|
1676
1472
|
if (clientId === void 0)
|
|
@@ -1853,7 +1649,7 @@ var USERS_USAGE = {
|
|
|
1853
1649
|
var PASSWORD_FLAG_WARNING = "\u26A0 --password puts the password in your shell history. Pipe it in with --password-stdin instead.";
|
|
1854
1650
|
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.";
|
|
1855
1651
|
var PAGE_SIZE = 1e3;
|
|
1856
|
-
var
|
|
1652
|
+
var processStdin2 = {
|
|
1857
1653
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1858
1654
|
readStdin
|
|
1859
1655
|
};
|
|
@@ -1955,7 +1751,7 @@ function userLines(user) {
|
|
|
1955
1751
|
`Created: ${printable(user.createdAt)}`
|
|
1956
1752
|
];
|
|
1957
1753
|
}
|
|
1958
|
-
async function runBackendUsersCreate(context, input =
|
|
1754
|
+
async function runBackendUsersCreate(context, input = processStdin2) {
|
|
1959
1755
|
const { username, email } = context.flags;
|
|
1960
1756
|
if (username === void 0 && email === void 0 || context.positionals.length > 0)
|
|
1961
1757
|
throw new NaroError("VALIDATION_FAILED", USERS_USAGE.create);
|
|
@@ -2033,9 +1829,9 @@ async function runCancel(context) {
|
|
|
2033
1829
|
}
|
|
2034
1830
|
|
|
2035
1831
|
// src/commands/db.ts
|
|
2036
|
-
import { readFileSync
|
|
1832
|
+
import { readFileSync } from "fs";
|
|
2037
1833
|
import { resolve } from "path";
|
|
2038
|
-
var
|
|
1834
|
+
var processStdin3 = {
|
|
2039
1835
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
2040
1836
|
readStdin
|
|
2041
1837
|
};
|
|
@@ -2144,7 +1940,7 @@ async function readSql(context, deps) {
|
|
|
2144
1940
|
sql = positional;
|
|
2145
1941
|
} else if (context.flags.file !== void 0) {
|
|
2146
1942
|
try {
|
|
2147
|
-
sql =
|
|
1943
|
+
sql = readFileSync(resolve(context.cwd, context.flags.file), "utf8");
|
|
2148
1944
|
} catch {
|
|
2149
1945
|
throw new NaroError(
|
|
2150
1946
|
"VALIDATION_FAILED",
|
|
@@ -2158,7 +1954,7 @@ async function readSql(context, deps) {
|
|
|
2158
1954
|
throw new NaroError("VALIDATION_FAILED", USAGE);
|
|
2159
1955
|
return sql;
|
|
2160
1956
|
}
|
|
2161
|
-
async function runDbQuery(context, deps =
|
|
1957
|
+
async function runDbQuery(context, deps = processStdin3) {
|
|
2162
1958
|
const sql = await readSql(context, deps);
|
|
2163
1959
|
const client = await dataClient(context);
|
|
2164
1960
|
const projectId = await context.resolveProjectId();
|
|
@@ -2177,13 +1973,13 @@ async function runDbQuery(context, deps = processStdin4) {
|
|
|
2177
1973
|
// src/commands/db-backup.ts
|
|
2178
1974
|
import {
|
|
2179
1975
|
createWriteStream,
|
|
2180
|
-
mkdirSync
|
|
1976
|
+
mkdirSync,
|
|
2181
1977
|
renameSync,
|
|
2182
1978
|
rmSync,
|
|
2183
1979
|
statSync,
|
|
2184
|
-
writeFileSync
|
|
1980
|
+
writeFileSync
|
|
2185
1981
|
} from "fs";
|
|
2186
|
-
import { basename, dirname, join
|
|
1982
|
+
import { basename, dirname, join, resolve as resolve2 } from "path";
|
|
2187
1983
|
import { Transform } from "stream";
|
|
2188
1984
|
import { pipeline } from "stream/promises";
|
|
2189
1985
|
var defaults = {
|
|
@@ -2191,8 +1987,8 @@ var defaults = {
|
|
|
2191
1987
|
stdout: process.stdout,
|
|
2192
1988
|
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
2193
1989
|
writeFile: (path, data) => {
|
|
2194
|
-
|
|
2195
|
-
|
|
1990
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1991
|
+
writeFileSync(path, data, { flag: "wx" });
|
|
2196
1992
|
}
|
|
2197
1993
|
};
|
|
2198
1994
|
function exportNotice(environment) {
|
|
@@ -2302,14 +2098,14 @@ function refuseDirectory(path, shown) {
|
|
|
2302
2098
|
}
|
|
2303
2099
|
function makeOutputDirectory(path, shown) {
|
|
2304
2100
|
try {
|
|
2305
|
-
|
|
2101
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2306
2102
|
} catch (error) {
|
|
2307
2103
|
throw writeFailed(shown, error);
|
|
2308
2104
|
}
|
|
2309
2105
|
}
|
|
2310
2106
|
async function downloadToFile(deps, url, path, shown, again) {
|
|
2311
2107
|
const body = await openDownload(deps, url, again);
|
|
2312
|
-
const partial =
|
|
2108
|
+
const partial = join(
|
|
2313
2109
|
dirname(path),
|
|
2314
2110
|
`.${basename(path)}.${String(process.pid)}.part`
|
|
2315
2111
|
);
|
|
@@ -2477,7 +2273,7 @@ async function runDbPull(context, overrides = {}) {
|
|
|
2477
2273
|
`${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.`
|
|
2478
2274
|
);
|
|
2479
2275
|
const name = nextMigrationFileName(existing, "remote_schema");
|
|
2480
|
-
const path =
|
|
2276
|
+
const path = join(dir, name);
|
|
2481
2277
|
const shown = relativeToCwd(context, path);
|
|
2482
2278
|
const client = await dataClient(context);
|
|
2483
2279
|
const projectId = await context.resolveProjectId();
|
|
@@ -2559,12 +2355,12 @@ async function readPolicies(client, projectId, warn) {
|
|
|
2559
2355
|
return void 0;
|
|
2560
2356
|
}
|
|
2561
2357
|
const policies = [];
|
|
2562
|
-
const
|
|
2358
|
+
const unreadable2 = [];
|
|
2563
2359
|
for (const row of objectsOf(stored)) {
|
|
2564
2360
|
const policy = loadPolicy(row);
|
|
2565
2361
|
if (policy === void 0) continue;
|
|
2566
2362
|
if (policy.invalid !== null)
|
|
2567
|
-
|
|
2363
|
+
unreadable2.push({
|
|
2568
2364
|
table: policy.table,
|
|
2569
2365
|
name: policy.name,
|
|
2570
2366
|
reason: policy.invalid
|
|
@@ -2585,7 +2381,7 @@ async function readPolicies(client, projectId, warn) {
|
|
|
2585
2381
|
return {
|
|
2586
2382
|
tables: tableNames(snapshot),
|
|
2587
2383
|
policies,
|
|
2588
|
-
unreadable,
|
|
2384
|
+
unreadable: unreadable2,
|
|
2589
2385
|
snapshot,
|
|
2590
2386
|
indexes: indexesFromRows(objectsOf(indexes))
|
|
2591
2387
|
};
|
|
@@ -2662,8 +2458,8 @@ async function runDbLint(context) {
|
|
|
2662
2458
|
}
|
|
2663
2459
|
|
|
2664
2460
|
// src/commands/db-reset.ts
|
|
2665
|
-
import { readFileSync as
|
|
2666
|
-
import { join as
|
|
2461
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2462
|
+
import { join as join2 } from "path";
|
|
2667
2463
|
var MAX_STATEMENTS = 50;
|
|
2668
2464
|
var SCHEMA_FILTER = `${systemTablePredicate("name")} AND ${systemTablePredicate("tbl_name")}`;
|
|
2669
2465
|
var SCHEMA_SQL2 = [
|
|
@@ -2845,7 +2641,7 @@ async function runDbReset(context) {
|
|
|
2845
2641
|
${problemLines.join("\n")}`,
|
|
2846
2642
|
{ details: { problems } }
|
|
2847
2643
|
);
|
|
2848
|
-
const texts = files.map((name) =>
|
|
2644
|
+
const texts = files.map((name) => readFileSync2(join2(dir, name), "utf8"));
|
|
2849
2645
|
const client = await dataClient(context);
|
|
2850
2646
|
const projectId = await context.resolveProjectId();
|
|
2851
2647
|
const bookmark = context.flags.dryRun ? null : await bookmarkNow(context, client, projectId, "resetting");
|
|
@@ -2985,11 +2781,11 @@ ${problemLines.join("\n")}`,
|
|
|
2985
2781
|
}
|
|
2986
2782
|
|
|
2987
2783
|
// src/commands/db-schema.ts
|
|
2988
|
-
import { mkdirSync as
|
|
2989
|
-
import { join as
|
|
2784
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2785
|
+
import { join as join3 } from "path";
|
|
2990
2786
|
|
|
2991
2787
|
// src/commands/policy.ts
|
|
2992
|
-
import { readFileSync as
|
|
2788
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2993
2789
|
import { resolve as resolve3 } from "path";
|
|
2994
2790
|
|
|
2995
2791
|
// ../../packages/naro-sdk/src/query/filters.ts
|
|
@@ -3407,7 +3203,7 @@ function conditionsFrom(flags) {
|
|
|
3407
3203
|
function patchFromFile(context, path, table3) {
|
|
3408
3204
|
let text;
|
|
3409
3205
|
try {
|
|
3410
|
-
text =
|
|
3206
|
+
text = readFileSync3(resolve3(context.cwd, path), "utf8");
|
|
3411
3207
|
} catch {
|
|
3412
3208
|
refuse2(`Could not read ${path}.`);
|
|
3413
3209
|
}
|
|
@@ -3569,12 +3365,12 @@ function conditionText(action, value) {
|
|
|
3569
3365
|
if (isInvalid(value))
|
|
3570
3366
|
return `cannot be read (${value.invalid}); it denies everyone but the service key`;
|
|
3571
3367
|
const using = value.using ? filterText(value.using) : "";
|
|
3572
|
-
const
|
|
3573
|
-
if (using === void 0 ||
|
|
3368
|
+
const check2 = value.check ? filterText(value.check) : "";
|
|
3369
|
+
if (using === void 0 || check2 === void 0)
|
|
3574
3370
|
return `as JSON ${JSON.stringify(value)}`;
|
|
3575
3371
|
const parts = [];
|
|
3576
3372
|
if (using !== "") parts.push(`using ${using}`);
|
|
3577
|
-
if (
|
|
3373
|
+
if (check2 !== "") parts.push(`check ${check2}`);
|
|
3578
3374
|
else if (action === "update") parts.push("check = using");
|
|
3579
3375
|
const text = parts.join("; ");
|
|
3580
3376
|
return value.role === "authenticated" ? `${text} (signed-in users only)` : text;
|
|
@@ -3684,7 +3480,7 @@ function statementText(context) {
|
|
|
3684
3480
|
let text = positional;
|
|
3685
3481
|
if (file !== void 0) {
|
|
3686
3482
|
try {
|
|
3687
|
-
text =
|
|
3483
|
+
text = readFileSync3(resolve3(context.cwd, file), "utf8");
|
|
3688
3484
|
} catch {
|
|
3689
3485
|
refuse2(`Could not read ${file}.`);
|
|
3690
3486
|
}
|
|
@@ -4041,13 +3837,13 @@ async function runSchemaChange(context, operations, question) {
|
|
|
4041
3837
|
let file = null;
|
|
4042
3838
|
let fileWarning;
|
|
4043
3839
|
const relDir = relativeToCwd(context, dir);
|
|
4044
|
-
const rel =
|
|
3840
|
+
const rel = join3(relDir === "." ? "" : relDir, name);
|
|
4045
3841
|
if (context.flags.noWrite) {
|
|
4046
3842
|
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.`;
|
|
4047
3843
|
} else {
|
|
4048
3844
|
try {
|
|
4049
|
-
|
|
4050
|
-
|
|
3845
|
+
mkdirSync2(dir, { recursive: true });
|
|
3846
|
+
writeFileSync2(join3(dir, name), result.sql, { flag: "wx" });
|
|
4051
3847
|
file = rel;
|
|
4052
3848
|
} catch (error) {
|
|
4053
3849
|
const reason = (error instanceof Error ? error.message : String(error)).replace(/[.\s]+$/, "");
|
|
@@ -4176,7 +3972,7 @@ async function runDbIndexDrop(context) {
|
|
|
4176
3972
|
var defaults2 = {
|
|
4177
3973
|
detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
|
|
4178
3974
|
openBrowser,
|
|
4179
|
-
sleep: (ms) => new Promise((
|
|
3975
|
+
sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms))
|
|
4180
3976
|
};
|
|
4181
3977
|
function connected(context, project) {
|
|
4182
3978
|
const branch = project.defaultBranch;
|
|
@@ -4707,7 +4503,7 @@ async function runInspect(context) {
|
|
|
4707
4503
|
|
|
4708
4504
|
// src/commands/domains.ts
|
|
4709
4505
|
var defaults3 = {
|
|
4710
|
-
sleep: (ms) => new Promise((
|
|
4506
|
+
sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms)),
|
|
4711
4507
|
now: () => Date.now()
|
|
4712
4508
|
};
|
|
4713
4509
|
var FIRST_DELAY_MS = 5e3;
|
|
@@ -4925,7 +4721,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4925
4721
|
]);
|
|
4926
4722
|
return;
|
|
4927
4723
|
}
|
|
4928
|
-
let
|
|
4724
|
+
let timedOut2 = false;
|
|
4929
4725
|
let abandoned = false;
|
|
4930
4726
|
if (wait && !domains.every(isReady)) {
|
|
4931
4727
|
const deadline = deps.now() + WAIT_TIMEOUT_MS;
|
|
@@ -4938,7 +4734,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4938
4734
|
}
|
|
4939
4735
|
const remaining = deadline - deps.now();
|
|
4940
4736
|
if (remaining <= 0) {
|
|
4941
|
-
|
|
4737
|
+
timedOut2 = true;
|
|
4942
4738
|
break;
|
|
4943
4739
|
}
|
|
4944
4740
|
for (const domain of pending)
|
|
@@ -4958,7 +4754,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4958
4754
|
const notReady = domains.filter((domain) => !isReady(domain));
|
|
4959
4755
|
const names = notReady.map((domain) => domain.hostname).join(", ");
|
|
4960
4756
|
const details = { projectId, domains, ready: false };
|
|
4961
|
-
if (
|
|
4757
|
+
if (timedOut2) {
|
|
4962
4758
|
const cause2 = notReady[0]?.cause;
|
|
4963
4759
|
throw new NaroError(
|
|
4964
4760
|
"TIMEOUT",
|
|
@@ -5005,7 +4801,7 @@ async function runDomainsRemove(context) {
|
|
|
5005
4801
|
}
|
|
5006
4802
|
|
|
5007
4803
|
// src/commands/env.ts
|
|
5008
|
-
import { chmodSync, existsSync, readFileSync as
|
|
4804
|
+
import { chmodSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
5009
4805
|
import { resolve as resolve4 } from "path";
|
|
5010
4806
|
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5011
4807
|
var KEY_MAX_LENGTH = 256;
|
|
@@ -5120,7 +4916,7 @@ async function runEnvImport(context) {
|
|
|
5120
4916
|
const target = parseTarget(targetArg);
|
|
5121
4917
|
let text;
|
|
5122
4918
|
try {
|
|
5123
|
-
text =
|
|
4919
|
+
text = readFileSync4(resolve4(context.cwd, file), "utf8");
|
|
5124
4920
|
} catch {
|
|
5125
4921
|
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
5126
4922
|
}
|
|
@@ -5211,7 +5007,7 @@ ${serializeDotenv(vars)}`;
|
|
|
5211
5007
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
5212
5008
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
5213
5009
|
}
|
|
5214
|
-
|
|
5010
|
+
writeFileSync3(path, text, { mode: 384 });
|
|
5215
5011
|
chmodSync(path, 384);
|
|
5216
5012
|
context.output.result({ projectId, file, environment: surface, count: count2 }, [
|
|
5217
5013
|
`\u2705 Pulled ${String(count2)} variable(s) into ${file}`
|
|
@@ -5370,131 +5166,100 @@ async function runEnvironmentsProvision(context) {
|
|
|
5370
5166
|
]);
|
|
5371
5167
|
}
|
|
5372
5168
|
|
|
5373
|
-
// src/commands/
|
|
5374
|
-
import { mkdirSync as
|
|
5375
|
-
import {
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
...undescribed.length > 0 ? { undescribed } : {},
|
|
5402
|
-
...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
|
|
5403
|
-
};
|
|
5404
|
-
if (context.flags.output) {
|
|
5405
|
-
const path = resolve5(context.cwd, context.flags.output);
|
|
5406
|
-
mkdirSync4(dirname2(path), { recursive: true });
|
|
5407
|
-
writeFileSync5(path, types);
|
|
5408
|
-
context.output.result(
|
|
5409
|
-
{
|
|
5410
|
-
projectId,
|
|
5411
|
-
output: context.flags.output,
|
|
5412
|
-
tables: tables.length,
|
|
5413
|
-
...extra
|
|
5414
|
-
},
|
|
5415
|
-
[
|
|
5416
|
-
`\u2705 Wrote types for ${String(tables.length)} table(s) to ${context.flags.output}`
|
|
5417
|
-
]
|
|
5418
|
-
);
|
|
5419
|
-
return;
|
|
5169
|
+
// src/commands/functions.ts
|
|
5170
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5171
|
+
import { join as join5 } from "path";
|
|
5172
|
+
|
|
5173
|
+
// src/functions/scaffold.ts
|
|
5174
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
5175
|
+
import { createRequire } from "module";
|
|
5176
|
+
import { dirname as dirname2, join as join4, relative, resolve as resolve5, sep } from "path";
|
|
5177
|
+
var NARO_JS_FUNCTIONS_MISSING = 'Cannot find "naro-js/functions". Install naro-js 0.7.0 or later: npm install naro-js@latest.';
|
|
5178
|
+
function scaffoldSource(name) {
|
|
5179
|
+
return `import { withNaro } from "naro-js/functions";
|
|
5180
|
+
|
|
5181
|
+
// Called at https://<your-app>.naro.sh/_naro/v1/functions/${name}.
|
|
5182
|
+
// \`naro\` acts as the caller, so the tables' row-level security applies;
|
|
5183
|
+
// \`naroAdmin\` uses the service key.
|
|
5184
|
+
Deno.serve(
|
|
5185
|
+
withNaro(async (req, { user }) => {
|
|
5186
|
+
const { name } = await req.json().catch(() => ({ name: "world" }));
|
|
5187
|
+
return Response.json({ message: \`Hello \${name}!\`, user: user?.id ?? null });
|
|
5188
|
+
}),
|
|
5189
|
+
);
|
|
5190
|
+
`;
|
|
5191
|
+
}
|
|
5192
|
+
function naroJsInstalled(root) {
|
|
5193
|
+
for (let dir = resolve5(root); ; dir = dirname2(dir)) {
|
|
5194
|
+
if (existsSync2(join4(dir, "node_modules", "naro-js", "package.json")))
|
|
5195
|
+
return true;
|
|
5196
|
+
if (dirname2(dir) === dir) return false;
|
|
5420
5197
|
}
|
|
5421
|
-
context.output.result({ projectId, types, ...extra }, [
|
|
5422
|
-
types.replace(/\n$/, "")
|
|
5423
|
-
]);
|
|
5424
5198
|
}
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
...Object.keys(filters).length > 0 ? { filters } : {}
|
|
5434
|
-
};
|
|
5199
|
+
function naroJsFunctionsResolves(root) {
|
|
5200
|
+
if (!naroJsInstalled(root)) return false;
|
|
5201
|
+
try {
|
|
5202
|
+
createRequire(join4(root, "package.json")).resolve("naro-js/functions");
|
|
5203
|
+
return true;
|
|
5204
|
+
} catch {
|
|
5205
|
+
return false;
|
|
5206
|
+
}
|
|
5435
5207
|
}
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
if (context.flags.route !== void 0) {
|
|
5447
|
-
const result = await client.insights.paths(projectId, {
|
|
5448
|
-
...query,
|
|
5449
|
-
route: context.flags.route
|
|
5450
|
-
});
|
|
5451
|
-
context.output.result(
|
|
5452
|
-
{ projectId, ...result },
|
|
5453
|
-
routePathsLines(result, filters)
|
|
5454
|
-
);
|
|
5455
|
-
return;
|
|
5208
|
+
var COVERS = /^(\.\/)?(\*|\*\*|naro)(\/|$)|^\.\/?$/;
|
|
5209
|
+
var LEAVES_OUT = /^(\.\/)?naro(\/functions)?\/?(\*\*)?$/;
|
|
5210
|
+
function tsconfigCovers(root, functionsLabel) {
|
|
5211
|
+
const file = join4(root, "tsconfig.json");
|
|
5212
|
+
if (!existsSync2(file)) return false;
|
|
5213
|
+
let config;
|
|
5214
|
+
try {
|
|
5215
|
+
config = parseJsonc(readFileSync5(file, "utf8"));
|
|
5216
|
+
} catch {
|
|
5217
|
+
return false;
|
|
5456
5218
|
}
|
|
5457
|
-
const
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
)
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5219
|
+
const include = Array.isArray(config.include) ? config.include.filter((p) => typeof p === "string") : null;
|
|
5220
|
+
const exclude = Array.isArray(config.exclude) ? config.exclude.filter((p) => typeof p === "string") : [];
|
|
5221
|
+
const covered = include === null || include.some((pattern) => COVERS.test(pattern));
|
|
5222
|
+
const leftOut = exclude.some(
|
|
5223
|
+
(pattern) => LEAVES_OUT.test(pattern) || pattern === functionsLabel
|
|
5224
|
+
);
|
|
5225
|
+
return covered && !leftOut;
|
|
5226
|
+
}
|
|
5227
|
+
function gitignoreCovers(root, envPath) {
|
|
5228
|
+
const file = join4(root, ".gitignore");
|
|
5229
|
+
if (!existsSync2(file)) return false;
|
|
5230
|
+
const accepted = /* @__PURE__ */ new Set([
|
|
5231
|
+
envPath,
|
|
5232
|
+
`/${envPath}`,
|
|
5233
|
+
".env",
|
|
5234
|
+
"/.env",
|
|
5235
|
+
"**/.env",
|
|
5236
|
+
".env*",
|
|
5237
|
+
"*.env"
|
|
5238
|
+
]);
|
|
5239
|
+
return readFileSync5(file, "utf8").split(/\r?\n/).some((line) => accepted.has(line.trim()));
|
|
5466
5240
|
}
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5241
|
+
function scaffoldNotes(folder) {
|
|
5242
|
+
const notes = [];
|
|
5243
|
+
if (!naroJsFunctionsResolves(folder.root))
|
|
5244
|
+
notes.push(`\u26A0 ${NARO_JS_FUNCTIONS_MISSING}`);
|
|
5245
|
+
const functionsLabel = relative(folder.root, folder.dir).split(sep).join("/");
|
|
5246
|
+
if (!folder.explicit && tsconfigCovers(folder.root, functionsLabel))
|
|
5247
|
+
notes.push(
|
|
5248
|
+
`\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.`
|
|
5472
5249
|
);
|
|
5473
|
-
const
|
|
5474
|
-
if (
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
`Filters only narrow a report; drop --${filter}.`
|
|
5250
|
+
const envPath = `${functionsLabel}/.env`;
|
|
5251
|
+
if (!gitignoreCovers(folder.root, envPath))
|
|
5252
|
+
notes.push(
|
|
5253
|
+
`\u26A0 ${envPath} is not in .gitignore. Add it before you keep local secrets there.`
|
|
5478
5254
|
);
|
|
5479
|
-
|
|
5480
|
-
const projectId = await context.resolveProjectId();
|
|
5481
|
-
const result = await client.insights.setEnabled(projectId, enabled);
|
|
5482
|
-
context.output.result({ projectId, enabled: result.enabled }, [
|
|
5483
|
-
result.enabled ? TURNED_ON : TURNED_OFF
|
|
5484
|
-
]);
|
|
5485
|
-
}
|
|
5486
|
-
function runInsightsEnable(context) {
|
|
5487
|
-
return setInsights(context, true);
|
|
5488
|
-
}
|
|
5489
|
-
function runInsightsDisable(context) {
|
|
5490
|
-
return setInsights(context, false);
|
|
5255
|
+
return notes;
|
|
5491
5256
|
}
|
|
5492
5257
|
|
|
5493
5258
|
// src/commands/logs.ts
|
|
5494
5259
|
var FOLLOW_INTERVAL_MS = 3e3;
|
|
5495
5260
|
var FOLLOW_TIMEOUT_MS = 30 * 6e4;
|
|
5496
5261
|
function sleep(ms) {
|
|
5497
|
-
return new Promise((
|
|
5262
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
5498
5263
|
}
|
|
5499
5264
|
function formatBuildLog(log) {
|
|
5500
5265
|
return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
|
|
@@ -5504,6 +5269,27 @@ function formatRuntimeLog(entry) {
|
|
|
5504
5269
|
const request = entry.method && entry.path ? ` ${entry.method} ${entry.path}${entry.status ? ` ${String(entry.status)}` : ""}` : "";
|
|
5505
5270
|
return `${time} [${entry.level}]${request} ${entry.message}`.trimEnd();
|
|
5506
5271
|
}
|
|
5272
|
+
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.";
|
|
5273
|
+
async function followRuntimeEntries(context, read, format, wait = sleep) {
|
|
5274
|
+
let lastTs = -1;
|
|
5275
|
+
let boundary = /* @__PURE__ */ new Set();
|
|
5276
|
+
for (; ; ) {
|
|
5277
|
+
const entries = await read();
|
|
5278
|
+
for (const entry of entries) {
|
|
5279
|
+
if (entry.ts < lastTs) continue;
|
|
5280
|
+
const key = JSON.stringify(entry);
|
|
5281
|
+
if (entry.ts === lastTs) {
|
|
5282
|
+
if (boundary.has(key)) continue;
|
|
5283
|
+
} else {
|
|
5284
|
+
lastTs = entry.ts;
|
|
5285
|
+
boundary = /* @__PURE__ */ new Set();
|
|
5286
|
+
}
|
|
5287
|
+
boundary.add(key);
|
|
5288
|
+
context.output.log(format(entry));
|
|
5289
|
+
}
|
|
5290
|
+
await wait(FOLLOW_INTERVAL_MS);
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5507
5293
|
async function latestDeploymentId(client, projectId) {
|
|
5508
5294
|
const [latest] = await client.deployments.list(projectId);
|
|
5509
5295
|
if (!latest)
|
|
@@ -5565,60 +5351,648 @@ async function runtimeLogs(context, client, projectId) {
|
|
|
5565
5351
|
context.output.log(
|
|
5566
5352
|
`Following ${environment} runtime logs (Ctrl-C to stop)...`
|
|
5567
5353
|
);
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5354
|
+
await followRuntimeEntries(
|
|
5355
|
+
context,
|
|
5356
|
+
() => client.logs.runtime(projectId, options),
|
|
5357
|
+
formatRuntimeLog
|
|
5358
|
+
);
|
|
5359
|
+
}
|
|
5360
|
+
async function runLogs(context) {
|
|
5361
|
+
const [deploymentId] = context.positionals;
|
|
5362
|
+
const type = context.flags.type ?? (deploymentId !== void 0 ? "build" : "runtime");
|
|
5363
|
+
if (type !== "build" && type !== "runtime")
|
|
5364
|
+
throw new NaroError(
|
|
5365
|
+
"VALIDATION_FAILED",
|
|
5366
|
+
"--type must be build or runtime."
|
|
5367
|
+
);
|
|
5368
|
+
if (type === "runtime" && context.flags.follow && context.flags.json)
|
|
5369
|
+
throw new NaroError("VALIDATION_FAILED", FOLLOW_JSON_REFUSAL);
|
|
5370
|
+
if (type === "build" && context.flags.env)
|
|
5371
|
+
throw new NaroError(
|
|
5372
|
+
"VALIDATION_FAILED",
|
|
5373
|
+
"--env is for runtime logs. Build logs belong to one deployment: pass its id (naro deployments lists them)."
|
|
5374
|
+
);
|
|
5375
|
+
const client = await context.client();
|
|
5376
|
+
if (type === "runtime") {
|
|
5377
|
+
if (deploymentId)
|
|
5378
|
+
throw new NaroError(
|
|
5379
|
+
"VALIDATION_FAILED",
|
|
5380
|
+
"Runtime logs are per project, not per deployment. Drop the deployment id or use --type build."
|
|
5381
|
+
);
|
|
5382
|
+
const projectId = await context.resolveProjectId();
|
|
5383
|
+
await runtimeLogs(context, client, projectId);
|
|
5384
|
+
return;
|
|
5385
|
+
}
|
|
5386
|
+
let target = deploymentId;
|
|
5387
|
+
if (target === void 0) {
|
|
5388
|
+
target = await latestDeploymentId(client, await context.resolveProjectId());
|
|
5389
|
+
context.output.log(`Build logs of the latest deployment, ${target}`);
|
|
5390
|
+
}
|
|
5391
|
+
await buildLogs(context, client, target);
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
// src/commands/functions.ts
|
|
5395
|
+
var FUNCTIONS_ENV_REFUSAL = "naro functions works on the production backend only. Drop --env.";
|
|
5396
|
+
var VERSIONS_USAGE = "Usage: naro functions versions <name>";
|
|
5397
|
+
var LOGS_USAGE = "Usage: naro functions logs <name> [--level <l>] [--follow]";
|
|
5398
|
+
function functionName(context, usage) {
|
|
5399
|
+
const [name] = context.positionals;
|
|
5400
|
+
if (name === void 0 || name === "")
|
|
5401
|
+
throw new NaroError("VALIDATION_FAILED", usage);
|
|
5402
|
+
return name;
|
|
5403
|
+
}
|
|
5404
|
+
function statusWord(fn) {
|
|
5405
|
+
if (fn.status !== "error") return fn.status;
|
|
5406
|
+
return `error at ${fn.step ?? "upload"}: ${printable(fn.error ?? "unknown error")}`;
|
|
5407
|
+
}
|
|
5408
|
+
var cpuHours = (ms) => (ms / 36e5).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
5409
|
+
function functionLines(list) {
|
|
5410
|
+
if (list.functions.length === 0)
|
|
5411
|
+
return [
|
|
5412
|
+
"No functions yet. Create one with naro functions new <name>, then naro functions deploy <name>."
|
|
5413
|
+
];
|
|
5414
|
+
const width = Math.max(...list.functions.map((fn) => fn.name.length));
|
|
5415
|
+
const versionWidth = Math.max(
|
|
5416
|
+
...list.functions.map(
|
|
5417
|
+
(fn) => fn.version === null ? 1 : String(fn.version).length + 1
|
|
5418
|
+
)
|
|
5419
|
+
);
|
|
5420
|
+
const lines = list.functions.map((fn) => {
|
|
5421
|
+
const version2 = fn.version === null ? "\u2014" : `v${String(fn.version)}`;
|
|
5422
|
+
const verify = fn.verifyJwt ? "verify on " : "verify off";
|
|
5423
|
+
const stale = fn.stale ? " (stale: naro functions secrets sync brings it up)" : "";
|
|
5424
|
+
return `${fn.name.padEnd(width)} ${version2.padEnd(versionWidth)} ${verify} ${statusWord(fn)} ${fn.url}${stale}`;
|
|
5425
|
+
});
|
|
5426
|
+
lines.push(
|
|
5427
|
+
`Up to ${list.limits.functions.toLocaleString("en-US")} functions per ${list.limits.functionsScope} on this plan.`
|
|
5428
|
+
);
|
|
5429
|
+
const usage = list.usage;
|
|
5430
|
+
if (usage !== null) {
|
|
5431
|
+
const percent2 = usage.cpuMsLimit > 0 ? Math.floor(usage.cpuMs / usage.cpuMsLimit * 100) : 0;
|
|
5432
|
+
lines.push(
|
|
5433
|
+
`Function CPU this month (UTC, the team's): ${cpuHours(usage.cpuMs)} of ${cpuHours(usage.cpuMsLimit)} CPU-hours (${String(percent2)}%).`
|
|
5434
|
+
);
|
|
5435
|
+
if (usage.blocked)
|
|
5436
|
+
lines.push(
|
|
5437
|
+
"\u26A0 This team has used its monthly function CPU time. Functions answer 429 FUNCTIONS_QUOTA_EXCEEDED until the 1st at 00:00 UTC."
|
|
5438
|
+
);
|
|
5439
|
+
else if (usage.cpuMs >= Math.ceil(usage.cpuMsLimit * 0.8))
|
|
5440
|
+
lines.push(
|
|
5441
|
+
`\u26A0 This team has used ${String(percent2)}% of its monthly function CPU time.`
|
|
5442
|
+
);
|
|
5443
|
+
}
|
|
5444
|
+
return lines;
|
|
5445
|
+
}
|
|
5446
|
+
var kib = (bytes) => `${(bytes / 1024).toFixed(1)} KB`;
|
|
5447
|
+
function versionLines(detail) {
|
|
5448
|
+
const statusWidth = Math.max(...detail.versions.map((v) => v.status.length));
|
|
5449
|
+
return detail.versions.map(
|
|
5450
|
+
(v) => `v${String(v.number)} ${v.status.padEnd(statusWidth)} ${v.active ? "live" : " "} ${kib(v.size)} ${v.verifyJwt ? "verify on " : "verify off"} ${v.createdAt}`
|
|
5451
|
+
);
|
|
5452
|
+
}
|
|
5453
|
+
async function runFunctionsList(context) {
|
|
5454
|
+
const client = await context.client();
|
|
5455
|
+
const projectId = await context.resolveProjectId();
|
|
5456
|
+
const list = await client.functions.list(projectId);
|
|
5457
|
+
context.output.result({ projectId, ...list }, functionLines(list));
|
|
5458
|
+
}
|
|
5459
|
+
async function runFunctionsVersions(context) {
|
|
5460
|
+
const name = functionName(context, VERSIONS_USAGE);
|
|
5461
|
+
const client = await context.client();
|
|
5462
|
+
const projectId = await context.resolveProjectId();
|
|
5463
|
+
const detail = await client.functions.get(projectId, name);
|
|
5464
|
+
context.output.result({ projectId, ...detail }, versionLines(detail));
|
|
5465
|
+
}
|
|
5466
|
+
async function runFunctionsLogs(context) {
|
|
5467
|
+
if (context.flags.follow && context.flags.json)
|
|
5468
|
+
throw new NaroError("VALIDATION_FAILED", FOLLOW_JSON_REFUSAL);
|
|
5469
|
+
const name = functionName(context, LOGS_USAGE);
|
|
5470
|
+
const client = await context.client();
|
|
5471
|
+
const projectId = await context.resolveProjectId();
|
|
5472
|
+
const options = context.flags.level ? { level: context.flags.level } : {};
|
|
5473
|
+
const format = (entry) => printable(formatRuntimeLog(entry));
|
|
5474
|
+
if (!context.flags.follow) {
|
|
5475
|
+
const entries = await client.functions.logs(projectId, name, options);
|
|
5476
|
+
context.output.result(
|
|
5477
|
+
{ projectId, name, entries },
|
|
5478
|
+
entries.length > 0 ? entries.map(format) : [`(no logs from ${name} yet)`]
|
|
5479
|
+
);
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5482
|
+
context.output.log(`Following ${name}'s logs (Ctrl-C to stop)...`);
|
|
5483
|
+
await followRuntimeEntries(
|
|
5484
|
+
context,
|
|
5485
|
+
() => client.functions.logs(projectId, name, options),
|
|
5486
|
+
format
|
|
5487
|
+
);
|
|
5488
|
+
}
|
|
5489
|
+
var NEW_USAGE = "Usage: naro functions new <name>";
|
|
5490
|
+
var ROLLBACK_USAGE = "Usage: naro functions rollback <name> [<version>]";
|
|
5491
|
+
var DELETE_USAGE2 = "Usage: naro functions delete <name>";
|
|
5492
|
+
async function runFunctionsNew(context) {
|
|
5493
|
+
const name = functionName(context, NEW_USAGE);
|
|
5494
|
+
if (!isFunctionName(name))
|
|
5495
|
+
throw new NaroError("VALIDATION_FAILED", BUNDLE_MESSAGES.badName(name));
|
|
5496
|
+
const folder = functionsFolder({
|
|
5497
|
+
cwd: context.cwd,
|
|
5498
|
+
root: context.flags.root,
|
|
5499
|
+
dir: context.flags.dir
|
|
5500
|
+
});
|
|
5501
|
+
const target = join5(folder.dir, name);
|
|
5502
|
+
mkdirSync3(folder.dir, { recursive: true });
|
|
5503
|
+
try {
|
|
5504
|
+
mkdirSync3(target);
|
|
5505
|
+
} catch (error) {
|
|
5506
|
+
if (error.code !== "EEXIST") throw error;
|
|
5507
|
+
throw new NaroError(
|
|
5508
|
+
"VALIDATION_FAILED",
|
|
5509
|
+
`${folder.label}/${name} already exists.`
|
|
5510
|
+
);
|
|
5511
|
+
}
|
|
5512
|
+
writeFileSync4(join5(target, "index.ts"), scaffoldSource(name));
|
|
5513
|
+
const file = `${folder.label}/${name}/index.ts`;
|
|
5514
|
+
const notes = scaffoldNotes(folder);
|
|
5515
|
+
context.output.result({ file, notes }, [
|
|
5516
|
+
`\u2705 Created ${file}. Deploy it with naro functions deploy ${name}.`,
|
|
5517
|
+
...notes
|
|
5518
|
+
]);
|
|
5519
|
+
}
|
|
5520
|
+
async function runFunctionsDeploy(context) {
|
|
5521
|
+
assertFunctionsNode();
|
|
5522
|
+
const client = await context.client();
|
|
5523
|
+
const projectId = await context.resolveProjectId();
|
|
5524
|
+
const { deployed } = await deployFunctions({
|
|
5525
|
+
client,
|
|
5526
|
+
projectId,
|
|
5527
|
+
cwd: context.cwd,
|
|
5528
|
+
root: context.flags.root,
|
|
5529
|
+
dir: context.flags.dir,
|
|
5530
|
+
names: context.positionals,
|
|
5531
|
+
verifyJwt: context.flags.noVerifyJwt ? false : void 0,
|
|
5532
|
+
importMap: context.flags.importMap,
|
|
5533
|
+
skipCheck: context.flags.skipCheck,
|
|
5534
|
+
cliVersion: version(),
|
|
5535
|
+
log: (line) => context.output.log(line)
|
|
5536
|
+
});
|
|
5537
|
+
context.output.result({ projectId, functions: deployed });
|
|
5538
|
+
}
|
|
5539
|
+
async function runFunctionsRollback(context) {
|
|
5540
|
+
const name = functionName(context, ROLLBACK_USAGE);
|
|
5541
|
+
const written = context.positionals[1];
|
|
5542
|
+
if (written !== void 0 && !/^[1-9]\d*$/.test(written))
|
|
5543
|
+
throw new NaroError(
|
|
5544
|
+
"VALIDATION_FAILED",
|
|
5545
|
+
"The version must be a whole number from 1, like 3."
|
|
5546
|
+
);
|
|
5547
|
+
const client = await context.client();
|
|
5548
|
+
const projectId = await context.resolveProjectId();
|
|
5549
|
+
const io = {
|
|
5550
|
+
sleep: defaultSleep,
|
|
5551
|
+
log: (line) => context.output.log(line)
|
|
5552
|
+
};
|
|
5553
|
+
const live2 = await withFunctionRetries(
|
|
5554
|
+
() => client.functions.rollback(
|
|
5555
|
+
projectId,
|
|
5556
|
+
name,
|
|
5557
|
+
written === void 0 ? void 0 : Number(written)
|
|
5558
|
+
),
|
|
5559
|
+
name,
|
|
5560
|
+
io
|
|
5561
|
+
);
|
|
5562
|
+
context.output.result({ projectId, function: live2 }, [
|
|
5563
|
+
`\u2705 Rolled ${name} back to v${String(live2.version)} \u2014 ${live2.url}`
|
|
5564
|
+
]);
|
|
5565
|
+
}
|
|
5566
|
+
async function runFunctionsDelete(context) {
|
|
5567
|
+
const name = functionName(context, DELETE_USAGE2);
|
|
5568
|
+
const client = await context.client();
|
|
5569
|
+
const projectId = await context.resolveProjectId();
|
|
5570
|
+
const detail = await client.functions.get(projectId, name);
|
|
5571
|
+
const stored = detail.versions.filter((v) => v.status !== "pruned").length;
|
|
5572
|
+
await confirmOrAbort(
|
|
5573
|
+
context,
|
|
5574
|
+
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.`
|
|
5575
|
+
);
|
|
5576
|
+
const io = {
|
|
5577
|
+
sleep: defaultSleep,
|
|
5578
|
+
log: (line) => context.output.log(line)
|
|
5579
|
+
};
|
|
5580
|
+
await withFunctionRetries(
|
|
5581
|
+
() => client.functions.remove(projectId, name),
|
|
5582
|
+
name,
|
|
5583
|
+
io
|
|
5584
|
+
);
|
|
5585
|
+
context.output.result({ projectId, name, deleted: true }, [
|
|
5586
|
+
`\u2705 Deleted ${name}. Its URL stops working within seconds.`
|
|
5587
|
+
]);
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5590
|
+
// src/commands/functions-secrets.ts
|
|
5591
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5592
|
+
import { resolve as resolve6 } from "path";
|
|
5593
|
+
var SET_USAGE3 = "Usage: naro functions secrets set <NAME>";
|
|
5594
|
+
var IMPORT_USAGE = "Usage: naro functions secrets import <file>";
|
|
5595
|
+
var UNSET_USAGE = "Usage: naro functions secrets unset <NAME>\u2026";
|
|
5596
|
+
var NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5597
|
+
var NAME_MAX = 256;
|
|
5598
|
+
var VALUE_MAX_BYTES2 = 5120;
|
|
5599
|
+
var RESERVED = /* @__PURE__ */ new Set(["ASSETS", "DB"]);
|
|
5600
|
+
var PROTO_LINE = /^\s*(?:export\s+)?__proto__(?:\s*=|:\s)/m;
|
|
5601
|
+
function secretNameProblem(name) {
|
|
5602
|
+
if (!NAME.test(name) || name.length > NAME_MAX)
|
|
5603
|
+
return "Secret names must look like NAME: letters, digits and _, not starting with a digit.";
|
|
5604
|
+
if (name === "__proto__") return "__proto__ cannot be a secret name.";
|
|
5605
|
+
if (name.startsWith("NARO_") || RESERVED.has(name))
|
|
5606
|
+
return `${name} is reserved for Naro. Secret names cannot start with NARO_ or be ASSETS or DB.`;
|
|
5607
|
+
return null;
|
|
5608
|
+
}
|
|
5609
|
+
function secretValueProblem(name, value) {
|
|
5610
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
5611
|
+
return bytes > VALUE_MAX_BYTES2 ? `The value of ${name} is ${bytes.toLocaleString("en-US")} bytes; secrets must be 5,120 bytes or less.` : null;
|
|
5612
|
+
}
|
|
5613
|
+
function check(problem) {
|
|
5614
|
+
if (problem !== null) throw new NaroError("VALIDATION_FAILED", problem);
|
|
5615
|
+
}
|
|
5616
|
+
var plural = (n, one, many) => n === 1 ? one : many;
|
|
5617
|
+
function byteCount(value) {
|
|
5618
|
+
const n = Buffer.byteLength(value, "utf8");
|
|
5619
|
+
return `${n.toLocaleString("en-US")} ${plural(n, "byte", "bytes")}`;
|
|
5620
|
+
}
|
|
5621
|
+
function warnFailures(context, sync) {
|
|
5622
|
+
for (const failure2 of sync.failures)
|
|
5623
|
+
context.output.warn(
|
|
5624
|
+
` ${printable(failure2.name)}: ${printable(failure2.message)}`
|
|
5625
|
+
);
|
|
5626
|
+
}
|
|
5627
|
+
var nameList = (list) => list.map(printable).join(", ");
|
|
5628
|
+
async function applyChange(context, client, projectId, pending) {
|
|
5629
|
+
if (pending === 0)
|
|
5630
|
+
return { lines: ["No live functions to update."], sync: null };
|
|
5631
|
+
if (context.flags.noSync)
|
|
5632
|
+
return {
|
|
5633
|
+
lines: [
|
|
5634
|
+
pending === null ? "The functions carry the change once synced: the sweep does it within minutes, or run naro functions secrets sync." : (
|
|
5635
|
+
// The save counted before the control plane's own hook brought
|
|
5636
|
+
// some up: "up to", not how many are left.
|
|
5637
|
+
`Up to ${functionCount(pending)} still ${plural(pending, "runs", "run")} the old values: the sweep syncs them within minutes, or run naro functions secrets sync.`
|
|
5638
|
+
)
|
|
5639
|
+
],
|
|
5640
|
+
sync: null
|
|
5641
|
+
};
|
|
5642
|
+
if (pending === null)
|
|
5643
|
+
context.output.log(
|
|
5644
|
+
"\u26A0 Naro could not count the functions to update; syncing until none is left."
|
|
5645
|
+
);
|
|
5646
|
+
const total = pending;
|
|
5647
|
+
let last = null;
|
|
5648
|
+
let sync;
|
|
5649
|
+
try {
|
|
5650
|
+
sync = await syncUntilDone(client, projectId, {
|
|
5651
|
+
log: (line) => context.output.log(line),
|
|
5652
|
+
onCall: (progress) => {
|
|
5653
|
+
last = progress;
|
|
5654
|
+
if (total !== null)
|
|
5655
|
+
context.output.log(
|
|
5656
|
+
`\u23F3 Applying secrets to ${functionCount(total)}\u2026 ${String(Math.min(progress.synced.length, total))}/${String(total)}`
|
|
5657
|
+
);
|
|
5580
5658
|
}
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5659
|
+
});
|
|
5660
|
+
} catch (error) {
|
|
5661
|
+
if (last !== null) warnFailures(context, last);
|
|
5662
|
+
const cause = toNaroError(error);
|
|
5663
|
+
const retryAfter = cause.details?.retryAfter;
|
|
5664
|
+
throw new NaroError(
|
|
5665
|
+
"FUNCTIONS_SYNC_INCOMPLETE",
|
|
5666
|
+
`Secrets are saved, but the sync stopped: ${printable(cause.message)} Run \`naro functions secrets sync\` to finish.`,
|
|
5667
|
+
{
|
|
5668
|
+
details: {
|
|
5669
|
+
saved: true,
|
|
5670
|
+
cause: {
|
|
5671
|
+
code: cause.code,
|
|
5672
|
+
message: cause.message,
|
|
5673
|
+
...typeof retryAfter === "number" ? { retryAfter } : {}
|
|
5674
|
+
},
|
|
5675
|
+
pending
|
|
5676
|
+
},
|
|
5677
|
+
cause: error
|
|
5678
|
+
}
|
|
5679
|
+
);
|
|
5680
|
+
}
|
|
5681
|
+
if (!sync.complete) {
|
|
5682
|
+
warnFailures(context, sync);
|
|
5683
|
+
throw new NaroError(
|
|
5684
|
+
"FUNCTIONS_SYNC_INCOMPLETE",
|
|
5685
|
+
`Secrets are saved, but ${functionCount(sync.pending)} still ${plural(sync.pending, "runs", "run")} the old values. Run \`naro functions secrets sync\` to finish.`,
|
|
5686
|
+
{
|
|
5687
|
+
details: {
|
|
5688
|
+
saved: true,
|
|
5689
|
+
pending: sync.pending,
|
|
5690
|
+
failures: sync.failures,
|
|
5691
|
+
synced: sync.synced
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
);
|
|
5695
|
+
}
|
|
5696
|
+
const live2 = Math.max(total ?? 0, sync.synced.length);
|
|
5697
|
+
return {
|
|
5698
|
+
lines: [
|
|
5699
|
+
live2 === 0 ? "\u2705 All functions are up to date." : `\u2705 Secrets are live on all ${functionCount(live2)}.`
|
|
5700
|
+
],
|
|
5701
|
+
sync
|
|
5702
|
+
};
|
|
5703
|
+
}
|
|
5704
|
+
async function readyToChange(context) {
|
|
5705
|
+
const projectId = await context.resolveProjectId();
|
|
5706
|
+
const client = await context.client();
|
|
5707
|
+
await client.functions.list(projectId);
|
|
5708
|
+
return { client, projectId };
|
|
5709
|
+
}
|
|
5710
|
+
async function runFunctionsSecretsList(context) {
|
|
5711
|
+
const client = await context.client();
|
|
5712
|
+
const projectId = await context.resolveProjectId();
|
|
5713
|
+
const secrets = await client.functions.secrets(projectId);
|
|
5714
|
+
const width = Math.max(0, ...secrets.map((s) => s.name.length));
|
|
5715
|
+
context.output.result(
|
|
5716
|
+
{ projectId, secrets },
|
|
5717
|
+
secrets.length === 0 ? ["No function secrets. Set one with naro functions secrets set <NAME>."] : secrets.map(
|
|
5718
|
+
(s) => `${s.name.padEnd(width)} ${s.digest} ${s.updatedAt}`
|
|
5719
|
+
)
|
|
5720
|
+
);
|
|
5721
|
+
}
|
|
5722
|
+
async function runFunctionsSecretsSet(context) {
|
|
5723
|
+
const [name] = context.positionals;
|
|
5724
|
+
if (name === void 0) throw new NaroError("VALIDATION_FAILED", SET_USAGE3);
|
|
5725
|
+
if (context.positionals.length > 1 || name.includes("="))
|
|
5726
|
+
throw new NaroError("VALIDATION_FAILED", SECRETS_ARGV_REFUSAL);
|
|
5727
|
+
check(secretNameProblem(name));
|
|
5728
|
+
const typed = process.stdin.isTTY === true;
|
|
5729
|
+
let value;
|
|
5730
|
+
let client = null;
|
|
5731
|
+
let projectId = null;
|
|
5732
|
+
if (typed) {
|
|
5733
|
+
projectId = await context.resolveProjectId();
|
|
5734
|
+
client = await context.client();
|
|
5735
|
+
await client.functions.list(projectId);
|
|
5736
|
+
value = await context.prompter.askHidden(`? What's the value of ${name}? `);
|
|
5737
|
+
} else {
|
|
5738
|
+
value = await readStdin();
|
|
5739
|
+
}
|
|
5740
|
+
if (value.length === 0)
|
|
5741
|
+
throw new NaroError("VALIDATION_FAILED", `No value provided for ${name}.`);
|
|
5742
|
+
check(secretValueProblem(name, value));
|
|
5743
|
+
projectId ??= await context.resolveProjectId();
|
|
5744
|
+
client ??= await context.client();
|
|
5745
|
+
const saved = await client.functions.setSecrets(projectId, {
|
|
5746
|
+
set: { [name]: value }
|
|
5747
|
+
});
|
|
5748
|
+
context.output.log(`\u2705 Saved ${name}.`);
|
|
5749
|
+
const { lines, sync } = await applyChange(
|
|
5750
|
+
context,
|
|
5751
|
+
client,
|
|
5752
|
+
projectId,
|
|
5753
|
+
saved.pending
|
|
5754
|
+
);
|
|
5755
|
+
context.output.result(
|
|
5756
|
+
{ projectId, secrets: saved.secrets, pending: saved.pending, sync },
|
|
5757
|
+
lines
|
|
5758
|
+
);
|
|
5759
|
+
}
|
|
5760
|
+
async function runFunctionsSecretsImport(context) {
|
|
5761
|
+
const [file] = context.positionals;
|
|
5762
|
+
if (file === void 0)
|
|
5763
|
+
throw new NaroError("VALIDATION_FAILED", IMPORT_USAGE);
|
|
5764
|
+
let text;
|
|
5765
|
+
try {
|
|
5766
|
+
text = readFileSync6(resolve6(context.cwd, file), "utf8");
|
|
5767
|
+
} catch {
|
|
5768
|
+
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
5769
|
+
}
|
|
5770
|
+
const parsed = parseDotenvDetailed(text);
|
|
5771
|
+
const [unclosed] = parsed.unclosedQuotes;
|
|
5772
|
+
if (unclosed !== void 0)
|
|
5773
|
+
throw new NaroError(
|
|
5774
|
+
"VALIDATION_FAILED",
|
|
5775
|
+
`Unclosed quote in ${file} at line ${String(unclosed)}.`
|
|
5776
|
+
);
|
|
5777
|
+
if (PROTO_LINE.test(text)) check(secretNameProblem("__proto__"));
|
|
5778
|
+
const entries = Object.entries(parsed.vars);
|
|
5779
|
+
if (entries.length === 0)
|
|
5780
|
+
throw new NaroError("VALIDATION_FAILED", `No secrets found in ${file}.`);
|
|
5781
|
+
const invalid = entries.map(([name]) => name).filter((name) => !NAME.test(name) || name.length > NAME_MAX);
|
|
5782
|
+
if (invalid.length > 0)
|
|
5783
|
+
throw new NaroError(
|
|
5784
|
+
"VALIDATION_FAILED",
|
|
5785
|
+
`Invalid keys in ${file}: ${invalid.map(printable).join(", ")}.`
|
|
5786
|
+
);
|
|
5787
|
+
for (const [name, value] of entries) {
|
|
5788
|
+
check(secretNameProblem(name));
|
|
5789
|
+
check(secretValueProblem(name, value));
|
|
5790
|
+
}
|
|
5791
|
+
const { client, projectId } = await readyToChange(context);
|
|
5792
|
+
context.output.log(`Secrets in ${file}:`);
|
|
5793
|
+
for (const [name, value] of entries)
|
|
5794
|
+
context.output.log(
|
|
5795
|
+
` ${name.padEnd(32)} ${value === "" ? "(empty)" : `\u2022\u2022\u2022\u2022\u2022\u2022 (${byteCount(value)})`}`
|
|
5796
|
+
);
|
|
5797
|
+
await confirmOrAbort(
|
|
5798
|
+
context,
|
|
5799
|
+
`Set ${String(entries.length)} function ${plural(entries.length, "secret", "secrets")} for all of this project's functions?`
|
|
5800
|
+
);
|
|
5801
|
+
const saved = await client.functions.setSecrets(projectId, {
|
|
5802
|
+
set: Object.fromEntries(entries)
|
|
5803
|
+
});
|
|
5804
|
+
context.output.log(
|
|
5805
|
+
`\u2705 Saved ${String(entries.length)} ${plural(entries.length, "secret", "secrets")}.`
|
|
5806
|
+
);
|
|
5807
|
+
const { lines, sync } = await applyChange(
|
|
5808
|
+
context,
|
|
5809
|
+
client,
|
|
5810
|
+
projectId,
|
|
5811
|
+
saved.pending
|
|
5812
|
+
);
|
|
5813
|
+
context.output.result(
|
|
5814
|
+
{ projectId, secrets: saved.secrets, pending: saved.pending, sync },
|
|
5815
|
+
lines
|
|
5816
|
+
);
|
|
5817
|
+
}
|
|
5818
|
+
async function runFunctionsSecretsUnset(context) {
|
|
5819
|
+
const names = context.positionals;
|
|
5820
|
+
if (names.length === 0) throw new NaroError("VALIDATION_FAILED", UNSET_USAGE);
|
|
5821
|
+
for (const name of names) check(secretNameProblem(name));
|
|
5822
|
+
const { client, projectId } = await readyToChange(context);
|
|
5823
|
+
await confirmOrAbort(
|
|
5824
|
+
context,
|
|
5825
|
+
`Unset ${names.join(", ")} from all of this project's functions? Functions that read them get undefined once they are synced.`
|
|
5826
|
+
);
|
|
5827
|
+
const saved = await client.functions.setSecrets(projectId, {
|
|
5828
|
+
unset: [...names]
|
|
5829
|
+
});
|
|
5830
|
+
context.output.log(
|
|
5831
|
+
`\u2705 Unset ${String(names.length)} ${plural(names.length, "secret", "secrets")}.`
|
|
5832
|
+
);
|
|
5833
|
+
const { lines, sync } = await applyChange(
|
|
5834
|
+
context,
|
|
5835
|
+
client,
|
|
5836
|
+
projectId,
|
|
5837
|
+
saved.pending
|
|
5838
|
+
);
|
|
5839
|
+
context.output.result(
|
|
5840
|
+
{ projectId, secrets: saved.secrets, pending: saved.pending, sync },
|
|
5841
|
+
lines
|
|
5842
|
+
);
|
|
5843
|
+
}
|
|
5844
|
+
async function runFunctionsSecretsSync(context) {
|
|
5845
|
+
const client = await context.client();
|
|
5846
|
+
const projectId = await context.resolveProjectId();
|
|
5847
|
+
const registry = context.flags.registry === true;
|
|
5848
|
+
const sync = await syncUntilDone(client, projectId, {
|
|
5849
|
+
registry,
|
|
5850
|
+
log: (line) => context.output.log(line)
|
|
5851
|
+
});
|
|
5852
|
+
if (!sync.complete) {
|
|
5853
|
+
warnFailures(context, sync);
|
|
5854
|
+
throw new NaroError(
|
|
5855
|
+
"FUNCTIONS_SYNC_INCOMPLETE",
|
|
5856
|
+
`The sync stopped with ${sync.pending.toLocaleString("en-US")} left to do. Run \`naro functions secrets sync${registry ? " --registry" : ""}\` again.`,
|
|
5857
|
+
{
|
|
5858
|
+
details: {
|
|
5859
|
+
pending: sync.pending,
|
|
5860
|
+
failures: sync.failures,
|
|
5861
|
+
synced: sync.synced,
|
|
5862
|
+
removed: sync.removed
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
);
|
|
5866
|
+
}
|
|
5867
|
+
const lines = [];
|
|
5868
|
+
if (sync.synced.length > 0)
|
|
5869
|
+
lines.push(
|
|
5870
|
+
`Synced ${functionCount(sync.synced.length)}: ${nameList(sync.synced)}`
|
|
5871
|
+
);
|
|
5872
|
+
if (sync.removed.length > 0)
|
|
5873
|
+
lines.push(`Removed from the function list: ${nameList(sync.removed)}`);
|
|
5874
|
+
lines.push("\u2705 All functions are up to date.");
|
|
5875
|
+
context.output.result({ projectId, ...sync }, lines);
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
// src/commands/gen.ts
|
|
5879
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
5880
|
+
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
5881
|
+
var LANG_MESSAGE = "naro gen types writes TypeScript only. Pass --lang typescript, or leave --lang out.";
|
|
5882
|
+
async function runGenTypes(context) {
|
|
5883
|
+
const { lang } = context.flags;
|
|
5884
|
+
if (lang !== void 0 && lang !== "typescript")
|
|
5885
|
+
throw new NaroError("VALIDATION_FAILED", LANG_MESSAGE);
|
|
5886
|
+
const client = await dataClient(context);
|
|
5887
|
+
const projectId = await context.resolveProjectId();
|
|
5888
|
+
const tables = await client.db.tables(projectId);
|
|
5889
|
+
const read = await policiesForTypes(client, projectId);
|
|
5890
|
+
const types = generateTypes(
|
|
5891
|
+
tables,
|
|
5892
|
+
read.policies === void 0 ? {} : { policies: read.policies }
|
|
5893
|
+
);
|
|
5894
|
+
const undescribed = tables.flatMap(
|
|
5895
|
+
(table3) => table3.error === void 0 ? [] : [{ name: table3.name, error: table3.error }]
|
|
5896
|
+
);
|
|
5897
|
+
for (const { name, error } of undescribed)
|
|
5898
|
+
context.output.warn(
|
|
5899
|
+
`\u26A0 ${printable(name)} could not be described and was generated as an empty interface: ${printable(error)}`
|
|
5900
|
+
);
|
|
5901
|
+
if (read.policies === void 0)
|
|
5902
|
+
context.output.warn(
|
|
5903
|
+
`\u26A0 The backend's policies could not be read (${printable(read.reason)}), so owner columns stay required in Insert.`
|
|
5904
|
+
);
|
|
5905
|
+
const extra = {
|
|
5906
|
+
...undescribed.length > 0 ? { undescribed } : {},
|
|
5907
|
+
...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
|
|
5908
|
+
};
|
|
5909
|
+
if (context.flags.output) {
|
|
5910
|
+
const path = resolve7(context.cwd, context.flags.output);
|
|
5911
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
5912
|
+
writeFileSync5(path, types);
|
|
5913
|
+
context.output.result(
|
|
5914
|
+
{
|
|
5915
|
+
projectId,
|
|
5916
|
+
output: context.flags.output,
|
|
5917
|
+
tables: tables.length,
|
|
5918
|
+
...extra
|
|
5919
|
+
},
|
|
5920
|
+
[
|
|
5921
|
+
`\u2705 Wrote types for ${String(tables.length)} table(s) to ${context.flags.output}`
|
|
5922
|
+
]
|
|
5923
|
+
);
|
|
5924
|
+
return;
|
|
5925
|
+
}
|
|
5926
|
+
context.output.result({ projectId, types, ...extra }, [
|
|
5927
|
+
types.replace(/\n$/, "")
|
|
5928
|
+
]);
|
|
5929
|
+
}
|
|
5930
|
+
|
|
5931
|
+
// src/commands/insights.ts
|
|
5932
|
+
function queryOf2(flags) {
|
|
5933
|
+
const filters = filtersOf(flags, SPEED_INSIGHTS_FILTER_KEYS);
|
|
5934
|
+
return {
|
|
5935
|
+
...flags.range !== void 0 ? { range: flags.range } : {},
|
|
5936
|
+
...flags.metric !== void 0 ? { metric: flags.metric } : {},
|
|
5937
|
+
...flags.env !== void 0 ? { environment: flags.env } : {},
|
|
5938
|
+
...Object.keys(filters).length > 0 ? { filters } : {}
|
|
5939
|
+
};
|
|
5940
|
+
}
|
|
5941
|
+
async function runInsights(context) {
|
|
5942
|
+
if (context.flags.referrer !== void 0 && context.flags.referrer !== "")
|
|
5943
|
+
throw new NaroError(
|
|
5944
|
+
"VALIDATION_FAILED",
|
|
5945
|
+
"Speed Insights has no referrer; drop --referrer."
|
|
5946
|
+
);
|
|
5947
|
+
const client = await context.client();
|
|
5948
|
+
const projectId = await context.resolveProjectId();
|
|
5949
|
+
const query = queryOf2(context.flags);
|
|
5950
|
+
const filters = query.filters ?? {};
|
|
5951
|
+
if (context.flags.route !== void 0) {
|
|
5952
|
+
const result = await client.insights.paths(projectId, {
|
|
5953
|
+
...query,
|
|
5954
|
+
route: context.flags.route
|
|
5955
|
+
});
|
|
5956
|
+
context.output.result(
|
|
5957
|
+
{ projectId, ...result },
|
|
5958
|
+
routePathsLines(result, filters)
|
|
5959
|
+
);
|
|
5960
|
+
return;
|
|
5585
5961
|
}
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
if (type !== "build" && type !== "runtime")
|
|
5591
|
-
throw new NaroError(
|
|
5592
|
-
"VALIDATION_FAILED",
|
|
5593
|
-
"--type must be build or runtime."
|
|
5962
|
+
const overview = await client.insights.overview(projectId, query);
|
|
5963
|
+
if (overview.deploymentsError)
|
|
5964
|
+
context.output.warn(
|
|
5965
|
+
`Deployments are unavailable right now: ${printable(overview.deploymentsError.message)}`
|
|
5594
5966
|
);
|
|
5595
|
-
|
|
5967
|
+
context.output.result(
|
|
5968
|
+
{ projectId, ...overview },
|
|
5969
|
+
reportLines(overview.report, overview.deployments, filters)
|
|
5970
|
+
);
|
|
5971
|
+
}
|
|
5972
|
+
async function setInsights(context, enabled) {
|
|
5973
|
+
if (context.flags.env !== void 0)
|
|
5596
5974
|
throw new NaroError(
|
|
5597
5975
|
"VALIDATION_FAILED",
|
|
5598
|
-
"
|
|
5976
|
+
"Speed Insights has one switch for production and preview; drop --env."
|
|
5599
5977
|
);
|
|
5600
|
-
|
|
5978
|
+
const filter = givenFilterFlag(context.flags);
|
|
5979
|
+
if (filter !== void 0)
|
|
5601
5980
|
throw new NaroError(
|
|
5602
5981
|
"VALIDATION_FAILED",
|
|
5603
|
-
|
|
5982
|
+
`Filters only narrow a report; drop --${filter}.`
|
|
5604
5983
|
);
|
|
5605
5984
|
const client = await context.client();
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
if (target === void 0) {
|
|
5618
|
-
target = await latestDeploymentId(client, await context.resolveProjectId());
|
|
5619
|
-
context.output.log(`Build logs of the latest deployment, ${target}`);
|
|
5620
|
-
}
|
|
5621
|
-
await buildLogs(context, client, target);
|
|
5985
|
+
const projectId = await context.resolveProjectId();
|
|
5986
|
+
const result = await client.insights.setEnabled(projectId, enabled);
|
|
5987
|
+
context.output.result({ projectId, enabled: result.enabled }, [
|
|
5988
|
+
result.enabled ? TURNED_ON : TURNED_OFF
|
|
5989
|
+
]);
|
|
5990
|
+
}
|
|
5991
|
+
function runInsightsEnable(context) {
|
|
5992
|
+
return setInsights(context, true);
|
|
5993
|
+
}
|
|
5994
|
+
function runInsightsDisable(context) {
|
|
5995
|
+
return setInsights(context, false);
|
|
5622
5996
|
}
|
|
5623
5997
|
|
|
5624
5998
|
// src/commands/projects.ts
|
|
@@ -5669,7 +6043,7 @@ async function runProjectInspect(context) {
|
|
|
5669
6043
|
|
|
5670
6044
|
// src/commands/promote.ts
|
|
5671
6045
|
var PROMOTE_USAGE = "Usage: naro promote <deployment-id> [--to <environment>]";
|
|
5672
|
-
var
|
|
6046
|
+
var ROLLBACK_USAGE2 = "Usage: naro rollback [deployment-id]";
|
|
5673
6047
|
var REBUILD_TIMEOUT_MS = 15 * 6e4;
|
|
5674
6048
|
var REUPLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
5675
6049
|
async function promoteAndWait(context, client, project, sourceDeploymentId, action, target) {
|
|
@@ -5733,9 +6107,9 @@ function refuseWhatIsNotRead(context, usage, envMessage) {
|
|
|
5733
6107
|
throw new NaroError("VALIDATION_FAILED", envMessage);
|
|
5734
6108
|
}
|
|
5735
6109
|
async function previewPromotion(context, client, projectId, sourceDeploymentId, target) {
|
|
5736
|
-
let
|
|
6110
|
+
let check2;
|
|
5737
6111
|
try {
|
|
5738
|
-
|
|
6112
|
+
check2 = await client.deployments.promoteCheck(
|
|
5739
6113
|
projectId,
|
|
5740
6114
|
sourceDeploymentId,
|
|
5741
6115
|
target
|
|
@@ -5747,9 +6121,9 @@ async function previewPromotion(context, client, projectId, sourceDeploymentId,
|
|
|
5747
6121
|
return;
|
|
5748
6122
|
}
|
|
5749
6123
|
context.output.log(
|
|
5750
|
-
|
|
6124
|
+
check2.mode === "rebuild" ? `\u25B8 rebuild: builds the same commit again with ${target ?? "production"}'s variables` : "\u25B8 reupload: publishes the stored build"
|
|
5751
6125
|
);
|
|
5752
|
-
for (const warning of
|
|
6126
|
+
for (const warning of check2.warnings)
|
|
5753
6127
|
for (const line of warningLines(warning)) context.output.log(line);
|
|
5754
6128
|
}
|
|
5755
6129
|
async function runPromote(context) {
|
|
@@ -5791,7 +6165,7 @@ async function runRollback(context) {
|
|
|
5791
6165
|
);
|
|
5792
6166
|
refuseWhatIsNotRead(
|
|
5793
6167
|
context,
|
|
5794
|
-
|
|
6168
|
+
ROLLBACK_USAGE2,
|
|
5795
6169
|
"--env is not an option of naro rollback: naro rollback always rolls production back."
|
|
5796
6170
|
);
|
|
5797
6171
|
const client = await context.client();
|
|
@@ -5877,6 +6251,21 @@ function tableName(table3) {
|
|
|
5877
6251
|
function linesFor(lines, environment) {
|
|
5878
6252
|
return lines.map((line) => forEnvironment(line, environment));
|
|
5879
6253
|
}
|
|
6254
|
+
function usageLines2(usage) {
|
|
6255
|
+
if (usage === null) return ["This month (UTC): not metered yet"];
|
|
6256
|
+
const lines = [
|
|
6257
|
+
`This month (UTC, all environments): ${number(usage.messages)} messages \xB7 est. $${usage.estCostUsd.toFixed(2)}`
|
|
6258
|
+
];
|
|
6259
|
+
if (usage.blocked)
|
|
6260
|
+
lines.push(
|
|
6261
|
+
"\u26A0 This project has used its realtime messages for this month. Realtime resumes on the 1st at 00:00 UTC."
|
|
6262
|
+
);
|
|
6263
|
+
else if (usage.messageLimit !== null && usage.messages >= Math.ceil(usage.messageLimit * 0.8))
|
|
6264
|
+
lines.push(
|
|
6265
|
+
`\u26A0 This project has used ${String(Math.floor(usage.messages * 100 / usage.messageLimit))}% of its monthly realtime message limit (${number(usage.messageLimit)}).`
|
|
6266
|
+
);
|
|
6267
|
+
return lines;
|
|
6268
|
+
}
|
|
5880
6269
|
function realtimeLines(info) {
|
|
5881
6270
|
if (!info.enabled)
|
|
5882
6271
|
return [
|
|
@@ -5887,7 +6276,9 @@ function realtimeLines(info) {
|
|
|
5887
6276
|
return [
|
|
5888
6277
|
"Realtime: on (public channels: off)",
|
|
5889
6278
|
info.tables.length === 0 ? "Published tables: none \u2014 naro realtime tables add <table>" : `Published tables: ${info.tables.map(tableName).join(", ")}`,
|
|
5890
|
-
live2 === null ? "Connections now: not known \u2014 the backend is off or not ready, or the hub did not answer" : `Connections now: ${number(live2.connections)} \xB7 channels ${number(live2.channels)} \xB7 presences ${number(live2.presences)}
|
|
6279
|
+
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)}`,
|
|
6280
|
+
// A control plane older than 3-2e sends no `usage`.
|
|
6281
|
+
...usageLines2(info.usage ?? null)
|
|
5891
6282
|
];
|
|
5892
6283
|
}
|
|
5893
6284
|
async function enableRealtime(client, projectId, environment) {
|
|
@@ -6344,7 +6735,7 @@ async function runStorageBucketRemove(context) {
|
|
|
6344
6735
|
import { randomUUID } from "crypto";
|
|
6345
6736
|
import { createWriteStream as createWriteStream2, openAsBlob, rmSync as rmSync2 } from "fs";
|
|
6346
6737
|
import { rename, rm, stat } from "fs/promises";
|
|
6347
|
-
import { basename as basename2, dirname as
|
|
6738
|
+
import { basename as basename2, dirname as dirname4, extname, join as join6, resolve as resolve8 } from "path";
|
|
6348
6739
|
import { Readable } from "stream";
|
|
6349
6740
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
6350
6741
|
|
|
@@ -6477,6 +6868,17 @@ function serviceKeyOffServer() {
|
|
|
6477
6868
|
message: SERVICE_KEY_OFF_SERVER_MESSAGE
|
|
6478
6869
|
};
|
|
6479
6870
|
}
|
|
6871
|
+
function accessTokenUnavailable(cause) {
|
|
6872
|
+
return {
|
|
6873
|
+
code: "ACCESS_TOKEN_UNAVAILABLE",
|
|
6874
|
+
message: "createClient's accessToken function failed or returned something that is not a string or null, so the request was not sent.",
|
|
6875
|
+
details: { cause: describe(cause) }
|
|
6876
|
+
};
|
|
6877
|
+
}
|
|
6878
|
+
function accessTokenAuthMessage(name) {
|
|
6879
|
+
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.`;
|
|
6880
|
+
}
|
|
6881
|
+
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.";
|
|
6480
6882
|
function notJoined(channel) {
|
|
6481
6883
|
return {
|
|
6482
6884
|
code: "NOT_JOINED",
|
|
@@ -6543,6 +6945,16 @@ function storeFor(config, path) {
|
|
|
6543
6945
|
return SESSION_PREFIXES.some((prefix) => path.startsWith(prefix)) ? config.tokens : config.accessJwt ?? null;
|
|
6544
6946
|
}
|
|
6545
6947
|
var ACCESS_JWT_MARGIN_MS = 6e4;
|
|
6948
|
+
function deadlineOf(timeoutMs) {
|
|
6949
|
+
return timeoutMs === void 0 ? null : { at: Date.now() + timeoutMs, ms: timeoutMs };
|
|
6950
|
+
}
|
|
6951
|
+
function remainingMs(deadline) {
|
|
6952
|
+
return Math.max(0, deadline.at - Date.now());
|
|
6953
|
+
}
|
|
6954
|
+
var EXPIRED = Symbol("expired");
|
|
6955
|
+
function timedOut(deadline) {
|
|
6956
|
+
return requestTimeout(deadline?.ms ?? 0);
|
|
6957
|
+
}
|
|
6546
6958
|
function timesOf(jwt) {
|
|
6547
6959
|
const payload = jwt.split(".")[1];
|
|
6548
6960
|
if (payload === void 0 || payload === "") return null;
|
|
@@ -6638,6 +7050,13 @@ async function accessJwtFor(config, jwtStore) {
|
|
|
6638
7050
|
async function bearerFor(config, path, withToken) {
|
|
6639
7051
|
if (!withToken) return { token: null, session: null };
|
|
6640
7052
|
const store = storeFor(config, path);
|
|
7053
|
+
if (config.accessToken !== void 0 && store !== config.tokens) {
|
|
7054
|
+
try {
|
|
7055
|
+
return { token: await config.accessToken.get(), session: null };
|
|
7056
|
+
} catch (cause) {
|
|
7057
|
+
return { error: accessTokenUnavailable(cause) };
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
6641
7060
|
if (store === null) return { token: null, session: null };
|
|
6642
7061
|
if (store === config.tokens) {
|
|
6643
7062
|
const token = await store.get();
|
|
@@ -6652,9 +7071,12 @@ async function retryBearer(config, path, error, first) {
|
|
|
6652
7071
|
if ("error" in renewed) return renewed;
|
|
6653
7072
|
return renewed.token === null || renewed.token === first.token || renewed.session !== first.session ? null : renewed;
|
|
6654
7073
|
}
|
|
6655
|
-
async function receive(config, options, init) {
|
|
6656
|
-
|
|
7074
|
+
async function receive(config, options, init, deadline) {
|
|
7075
|
+
if (deadline !== null && remainingMs(deadline) === 0)
|
|
7076
|
+
return { error: timedOut(deadline) };
|
|
7077
|
+
const controller = deadline !== null && typeof AbortController === "function" ? new AbortController() : null;
|
|
6657
7078
|
if (controller) init.signal = controller.signal;
|
|
7079
|
+
let gaveUp = false;
|
|
6658
7080
|
const request = (async () => {
|
|
6659
7081
|
try {
|
|
6660
7082
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
@@ -6662,6 +7084,10 @@ async function receive(config, options, init) {
|
|
|
6662
7084
|
`${config.baseUrl}/_naro/v1${options.path}`,
|
|
6663
7085
|
init
|
|
6664
7086
|
);
|
|
7087
|
+
if (gaveUp) {
|
|
7088
|
+
discard(response);
|
|
7089
|
+
return { error: timedOut(deadline) };
|
|
7090
|
+
}
|
|
6665
7091
|
return {
|
|
6666
7092
|
status: response.status,
|
|
6667
7093
|
headers: response.headers,
|
|
@@ -6671,14 +7097,14 @@ async function receive(config, options, init) {
|
|
|
6671
7097
|
return { error: networkError(error) };
|
|
6672
7098
|
}
|
|
6673
7099
|
})();
|
|
6674
|
-
|
|
6675
|
-
if (timeoutMs === void 0) return request;
|
|
7100
|
+
if (deadline === null) return request;
|
|
6676
7101
|
let timer;
|
|
6677
|
-
const expired = new Promise((
|
|
7102
|
+
const expired = new Promise((resolve9) => {
|
|
6678
7103
|
timer = setTimeout(() => {
|
|
7104
|
+
gaveUp = true;
|
|
6679
7105
|
controller?.abort();
|
|
6680
|
-
|
|
6681
|
-
},
|
|
7106
|
+
resolve9({ error: timedOut(deadline) });
|
|
7107
|
+
}, remainingMs(deadline));
|
|
6682
7108
|
});
|
|
6683
7109
|
try {
|
|
6684
7110
|
return await Promise.race([request, expired]);
|
|
@@ -6695,15 +7121,34 @@ async function call(config, options) {
|
|
|
6695
7121
|
return { error: invalidQuery("", "is not JSON"), answer: null };
|
|
6696
7122
|
}
|
|
6697
7123
|
}
|
|
6698
|
-
const
|
|
7124
|
+
const deadline = deadlineOf(options.timeoutMs);
|
|
7125
|
+
const bearer = await untilStopped(
|
|
7126
|
+
bearerFor(config, options.path, options.withToken),
|
|
7127
|
+
void 0,
|
|
7128
|
+
deadline
|
|
7129
|
+
);
|
|
7130
|
+
if (bearer === EXPIRED || bearer === ABORTED)
|
|
7131
|
+
return { error: timedOut(deadline), answer: null };
|
|
6699
7132
|
if ("error" in bearer) return { error: bearer.error, answer: null };
|
|
6700
|
-
const result = await callWith(
|
|
6701
|
-
|
|
7133
|
+
const result = await callWith(
|
|
7134
|
+
config,
|
|
7135
|
+
options,
|
|
7136
|
+
payload,
|
|
7137
|
+
bearer.token,
|
|
7138
|
+
deadline
|
|
7139
|
+
);
|
|
7140
|
+
const again = await untilStopped(
|
|
7141
|
+
retryBearer(config, options.path, result.error, bearer),
|
|
7142
|
+
void 0,
|
|
7143
|
+
deadline
|
|
7144
|
+
);
|
|
7145
|
+
if (again === EXPIRED || again === ABORTED)
|
|
7146
|
+
return { error: timedOut(deadline), answer: null };
|
|
6702
7147
|
if (again === null) return result;
|
|
6703
7148
|
if ("error" in again) return { error: again.error, answer: null };
|
|
6704
|
-
return callWith(config, options, payload, again.token);
|
|
7149
|
+
return callWith(config, options, payload, again.token, deadline);
|
|
6705
7150
|
}
|
|
6706
|
-
async function callWith(config, options, payload, token) {
|
|
7151
|
+
async function callWith(config, options, payload, token, deadline = null) {
|
|
6707
7152
|
const store = storeFor(config, options.path);
|
|
6708
7153
|
const headers = {
|
|
6709
7154
|
...options.headers,
|
|
@@ -6723,7 +7168,7 @@ async function callWith(config, options, payload, token) {
|
|
|
6723
7168
|
headers["Content-Type"] = "application/json";
|
|
6724
7169
|
init.body = payload;
|
|
6725
7170
|
}
|
|
6726
|
-
const received = await receive(config, options, init);
|
|
7171
|
+
const received = await receive(config, options, init, deadline);
|
|
6727
7172
|
if ("error" in received) return { error: received.error, answer: null };
|
|
6728
7173
|
const { status, headers: responseHeaders, text } = received;
|
|
6729
7174
|
let body;
|
|
@@ -6770,13 +7215,13 @@ var ABORTED = Symbol("aborted");
|
|
|
6770
7215
|
function untilAborted(promise, signal) {
|
|
6771
7216
|
if (!signal) return promise;
|
|
6772
7217
|
if (signal.aborted) return Promise.resolve(ABORTED);
|
|
6773
|
-
return new Promise((
|
|
6774
|
-
const stop = () =>
|
|
7218
|
+
return new Promise((resolve9, reject) => {
|
|
7219
|
+
const stop = () => resolve9(ABORTED);
|
|
6775
7220
|
signal.addEventListener("abort", stop, { once: true });
|
|
6776
7221
|
promise.then(
|
|
6777
7222
|
(value) => {
|
|
6778
7223
|
signal.removeEventListener("abort", stop);
|
|
6779
|
-
|
|
7224
|
+
resolve9(value);
|
|
6780
7225
|
},
|
|
6781
7226
|
(error) => {
|
|
6782
7227
|
signal.removeEventListener("abort", stop);
|
|
@@ -6785,6 +7230,27 @@ function untilAborted(promise, signal) {
|
|
|
6785
7230
|
);
|
|
6786
7231
|
});
|
|
6787
7232
|
}
|
|
7233
|
+
function untilStopped(promise, signal, deadline) {
|
|
7234
|
+
if (signal?.aborted) return Promise.resolve(ABORTED);
|
|
7235
|
+
if (!signal && deadline === null) return promise;
|
|
7236
|
+
return new Promise((resolve9, reject) => {
|
|
7237
|
+
let timer;
|
|
7238
|
+
const stop = () => finish(ABORTED);
|
|
7239
|
+
const finish = (value) => {
|
|
7240
|
+
clearTimeout(timer);
|
|
7241
|
+
signal?.removeEventListener("abort", stop);
|
|
7242
|
+
resolve9(value);
|
|
7243
|
+
};
|
|
7244
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
7245
|
+
if (deadline !== null)
|
|
7246
|
+
timer = setTimeout(() => finish(EXPIRED), remainingMs(deadline));
|
|
7247
|
+
promise.then(finish, (error) => {
|
|
7248
|
+
clearTimeout(timer);
|
|
7249
|
+
signal?.removeEventListener("abort", stop);
|
|
7250
|
+
reject(error);
|
|
7251
|
+
});
|
|
7252
|
+
});
|
|
7253
|
+
}
|
|
6788
7254
|
function isStream(body) {
|
|
6789
7255
|
return typeof ReadableStream === "function" && body instanceof ReadableStream;
|
|
6790
7256
|
}
|
|
@@ -6844,6 +7310,143 @@ async function sendWith(config, options, token) {
|
|
|
6844
7310
|
status
|
|
6845
7311
|
};
|
|
6846
7312
|
}
|
|
7313
|
+
async function fetchUnread(config, options) {
|
|
7314
|
+
const { signal } = options;
|
|
7315
|
+
if (signal?.aborted) return { failed: "aborted", cause: signal.reason };
|
|
7316
|
+
const deadline = deadlineOf(options.timeoutMs);
|
|
7317
|
+
if (options.ready) {
|
|
7318
|
+
const ready = await untilStopped(options.ready(), signal, deadline);
|
|
7319
|
+
if (ready === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
7320
|
+
if (ready === EXPIRED) return { failed: "timeout", cause: null };
|
|
7321
|
+
}
|
|
7322
|
+
const own = options.headers.has("authorization");
|
|
7323
|
+
const bearer = await untilStopped(
|
|
7324
|
+
bearerFor(config, options.path, !own),
|
|
7325
|
+
signal,
|
|
7326
|
+
deadline
|
|
7327
|
+
);
|
|
7328
|
+
if (bearer === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
7329
|
+
if (bearer === EXPIRED) return { failed: "timeout", cause: null };
|
|
7330
|
+
if ("error" in bearer) return { refused: bearer.error };
|
|
7331
|
+
const first = await fetchOnce(config, options, bearer.token, deadline);
|
|
7332
|
+
if (!("response" in first) || bearer.token === null) return first;
|
|
7333
|
+
const refused = await refusesToken(first.response, signal, deadline);
|
|
7334
|
+
if (refused === EXPIRED) {
|
|
7335
|
+
discard(first.response);
|
|
7336
|
+
return { failed: "timeout", cause: null };
|
|
7337
|
+
}
|
|
7338
|
+
if (refused === ABORTED) {
|
|
7339
|
+
discard(first.response);
|
|
7340
|
+
return { failed: "aborted", cause: signal?.reason };
|
|
7341
|
+
}
|
|
7342
|
+
if (!refused) return first;
|
|
7343
|
+
await storeFor(config, options.path)?.clearIf(bearer.token);
|
|
7344
|
+
if (isStream(options.body)) return first;
|
|
7345
|
+
const again = await untilStopped(
|
|
7346
|
+
retryBearer(config, options.path, invalidSession(), bearer),
|
|
7347
|
+
signal,
|
|
7348
|
+
deadline
|
|
7349
|
+
);
|
|
7350
|
+
if (again === EXPIRED) {
|
|
7351
|
+
discard(first.response);
|
|
7352
|
+
return { failed: "timeout", cause: null };
|
|
7353
|
+
}
|
|
7354
|
+
if (again === null || again !== ABORTED && "error" in again) return first;
|
|
7355
|
+
discard(first.response);
|
|
7356
|
+
if (again === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
7357
|
+
const second = await fetchOnce(config, options, again.token, deadline);
|
|
7358
|
+
if (!("response" in second) || again.token === null) return second;
|
|
7359
|
+
const refusedAgain = await refusesToken(second.response, signal, deadline);
|
|
7360
|
+
if (refusedAgain === ABORTED) {
|
|
7361
|
+
discard(second.response);
|
|
7362
|
+
return { failed: "aborted", cause: signal?.reason };
|
|
7363
|
+
}
|
|
7364
|
+
if (refusedAgain === true || refusedAgain === EXPIRED)
|
|
7365
|
+
await storeFor(config, options.path)?.clearIf(again.token);
|
|
7366
|
+
return second;
|
|
7367
|
+
}
|
|
7368
|
+
async function refusesToken(response, signal, deadline) {
|
|
7369
|
+
if (response.status !== 401 || response.headers.get("x-relay-error") !== "true")
|
|
7370
|
+
return false;
|
|
7371
|
+
let body;
|
|
7372
|
+
try {
|
|
7373
|
+
body = await untilStopped(response.clone().json(), signal, deadline);
|
|
7374
|
+
} catch {
|
|
7375
|
+
return signal?.aborted ? ABORTED : false;
|
|
7376
|
+
}
|
|
7377
|
+
if (body === ABORTED || body === EXPIRED) return body;
|
|
7378
|
+
return isRecord(body) && isRecord(body.error) && body.error.code === "INVALID_SESSION";
|
|
7379
|
+
}
|
|
7380
|
+
var releases = /* @__PURE__ */ new WeakMap();
|
|
7381
|
+
function discard(response) {
|
|
7382
|
+
releases.get(response)?.();
|
|
7383
|
+
releases.delete(response);
|
|
7384
|
+
response.body?.cancel().catch(() => {
|
|
7385
|
+
});
|
|
7386
|
+
}
|
|
7387
|
+
async function fetchOnce(config, options, token, deadline) {
|
|
7388
|
+
const { signal } = options;
|
|
7389
|
+
if (signal?.aborted) return { failed: "aborted", cause: signal.reason };
|
|
7390
|
+
if (deadline !== null && remainingMs(deadline) === 0)
|
|
7391
|
+
return { failed: "timeout", cause: null };
|
|
7392
|
+
const headers = new Headers(options.headers);
|
|
7393
|
+
headers.set("apikey", config.apiKey);
|
|
7394
|
+
if (token !== null) headers.set("authorization", `Bearer ${token}`);
|
|
7395
|
+
const init = {
|
|
7396
|
+
method: options.method,
|
|
7397
|
+
headers,
|
|
7398
|
+
credentials: "omit"
|
|
7399
|
+
};
|
|
7400
|
+
if (options.body !== void 0) init.body = options.body;
|
|
7401
|
+
if (isStream(options.body)) init.duplex = "half";
|
|
7402
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
7403
|
+
if (controller) init.signal = controller.signal;
|
|
7404
|
+
let timer;
|
|
7405
|
+
let stop = () => {
|
|
7406
|
+
};
|
|
7407
|
+
const stopped = new Promise((resolve9) => {
|
|
7408
|
+
stop = resolve9;
|
|
7409
|
+
});
|
|
7410
|
+
const onAbort = () => {
|
|
7411
|
+
stop({ failed: "aborted", cause: signal?.reason });
|
|
7412
|
+
controller?.abort(signal?.reason);
|
|
7413
|
+
};
|
|
7414
|
+
if (deadline !== null)
|
|
7415
|
+
timer = setTimeout(() => {
|
|
7416
|
+
stop({ failed: "timeout", cause: null });
|
|
7417
|
+
controller?.abort();
|
|
7418
|
+
}, remainingMs(deadline));
|
|
7419
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
7420
|
+
const request = (async () => {
|
|
7421
|
+
try {
|
|
7422
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
7423
|
+
return {
|
|
7424
|
+
response: await fetchImpl(
|
|
7425
|
+
`${config.baseUrl}/_naro/v1${options.path}`,
|
|
7426
|
+
init
|
|
7427
|
+
)
|
|
7428
|
+
};
|
|
7429
|
+
} catch (cause) {
|
|
7430
|
+
return { failed: "network", cause };
|
|
7431
|
+
}
|
|
7432
|
+
})();
|
|
7433
|
+
const release = () => signal?.removeEventListener("abort", onAbort);
|
|
7434
|
+
let outcome;
|
|
7435
|
+
try {
|
|
7436
|
+
outcome = await Promise.race([request, stopped]);
|
|
7437
|
+
return outcome;
|
|
7438
|
+
} finally {
|
|
7439
|
+
clearTimeout(timer);
|
|
7440
|
+
if (outcome !== void 0 && "response" in outcome)
|
|
7441
|
+
releases.set(outcome.response, release);
|
|
7442
|
+
else {
|
|
7443
|
+
release();
|
|
7444
|
+
void request.then((late) => {
|
|
7445
|
+
if ("response" in late) discard(late.response);
|
|
7446
|
+
});
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
6847
7450
|
var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
6848
7451
|
"RATE_LIMITED",
|
|
6849
7452
|
"WRITE_QUOTA_EXCEEDED",
|
|
@@ -6852,7 +7455,15 @@ var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
|
6852
7455
|
"OBJECT_RATE_LIMITED",
|
|
6853
7456
|
"STORAGE_OPS_QUOTA_EXCEEDED",
|
|
6854
7457
|
// Realtime's REST broadcast over the project's message budget (x-retry-after: 1).
|
|
6855
|
-
"TOO_MANY_MESSAGES"
|
|
7458
|
+
"TOO_MANY_MESSAGES",
|
|
7459
|
+
// The dispatcher's REST 429 of a project over its month of Realtime (3-2e):
|
|
7460
|
+
// `x-retry-after` is the seconds to the 1st at 00:00 UTC.
|
|
7461
|
+
"REALTIME_QUOTA_EXCEEDED",
|
|
7462
|
+
// Naro Functions (design F20): a team over its monthly function CPU, and
|
|
7463
|
+
// the control plane's Cloudflare limit. `invoke` reads x-retry-after on
|
|
7464
|
+
// every relay error anyway (invoke/client.ts).
|
|
7465
|
+
"FUNCTIONS_QUOTA_EXCEEDED",
|
|
7466
|
+
"FUNCTIONS_RATE_LIMITED"
|
|
6856
7467
|
]);
|
|
6857
7468
|
function withRetryAfter(error, headers) {
|
|
6858
7469
|
const header = headers.get("x-retry-after")?.trim() ?? "";
|
|
@@ -7166,14 +7777,14 @@ var AuthClient = class {
|
|
|
7166
7777
|
*/
|
|
7167
7778
|
async signInWithOAuth(options) {
|
|
7168
7779
|
const { provider } = options;
|
|
7169
|
-
const
|
|
7780
|
+
const failed2 = (error) => ({
|
|
7170
7781
|
data: { provider, url: null },
|
|
7171
7782
|
error
|
|
7172
7783
|
});
|
|
7173
|
-
if (!OAUTH_PROVIDERS.includes(provider)) return
|
|
7784
|
+
if (!OAUTH_PROVIDERS.includes(provider)) return failed2(invalidProvider());
|
|
7174
7785
|
await this.#flow.recovery.forget();
|
|
7175
7786
|
const pair = await createPkcePair();
|
|
7176
|
-
if (!pair) return
|
|
7787
|
+
if (!pair) return failed2(cryptoUnavailable());
|
|
7177
7788
|
await this.#flow.verifiers.add(pair.challenge, pair.verifier);
|
|
7178
7789
|
const redirectTo = options.redirectTo ?? currentPage();
|
|
7179
7790
|
const url = new URL(
|
|
@@ -7739,6 +8350,282 @@ var TokenStore = class {
|
|
|
7739
8350
|
}
|
|
7740
8351
|
}
|
|
7741
8352
|
};
|
|
8353
|
+
function prepareBody(body) {
|
|
8354
|
+
if (body === void 0) return { body: void 0, contentType: null };
|
|
8355
|
+
if (typeof body === "string") return { body, contentType: "text/plain" };
|
|
8356
|
+
if (typeof Blob === "function" && body instanceof Blob)
|
|
8357
|
+
return { body, contentType: "application/octet-stream" };
|
|
8358
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body))
|
|
8359
|
+
return { body, contentType: "application/octet-stream" };
|
|
8360
|
+
if (typeof FormData === "function" && body instanceof FormData)
|
|
8361
|
+
return { body, contentType: null };
|
|
8362
|
+
if (typeof URLSearchParams === "function" && body instanceof URLSearchParams)
|
|
8363
|
+
return { body, contentType: null };
|
|
8364
|
+
if (typeof ReadableStream === "function" && body instanceof ReadableStream)
|
|
8365
|
+
return { body, contentType: null };
|
|
8366
|
+
try {
|
|
8367
|
+
const text = JSON.stringify(body);
|
|
8368
|
+
return text === void 0 ? null : { body: text, contentType: "application/json" };
|
|
8369
|
+
} catch {
|
|
8370
|
+
return null;
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
async function readAnswer(response, method) {
|
|
8374
|
+
if (response.status === 204 || method === "HEAD") return { data: null };
|
|
8375
|
+
const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
8376
|
+
try {
|
|
8377
|
+
if (type === "application/json") {
|
|
8378
|
+
const text = await response.text();
|
|
8379
|
+
return {
|
|
8380
|
+
data: text.trim() === "" ? null : JSON.parse(text)
|
|
8381
|
+
};
|
|
8382
|
+
}
|
|
8383
|
+
if (type === "application/octet-stream" || type === "application/pdf")
|
|
8384
|
+
return { data: await response.blob() };
|
|
8385
|
+
if (type === "text/event-stream") return { data: response };
|
|
8386
|
+
if (type === "multipart/form-data")
|
|
8387
|
+
return { data: await response.formData() };
|
|
8388
|
+
return { data: await response.text() };
|
|
8389
|
+
} catch (cause) {
|
|
8390
|
+
return { unreadable: cause };
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
var FunctionsError = class extends Error {
|
|
8394
|
+
code;
|
|
8395
|
+
context;
|
|
8396
|
+
constructor(code, message, context, details) {
|
|
8397
|
+
super(message);
|
|
8398
|
+
this.name = "FunctionsError";
|
|
8399
|
+
this.code = code;
|
|
8400
|
+
this.context = context;
|
|
8401
|
+
if (details !== void 0)
|
|
8402
|
+
Object.defineProperty(this, "details", {
|
|
8403
|
+
value: details,
|
|
8404
|
+
enumerable: true
|
|
8405
|
+
});
|
|
8406
|
+
}
|
|
8407
|
+
};
|
|
8408
|
+
var FunctionsHttpError = class extends FunctionsError {
|
|
8409
|
+
constructor(response) {
|
|
8410
|
+
super(
|
|
8411
|
+
"FUNCTION_HTTP_ERROR",
|
|
8412
|
+
`The function answered with HTTP ${String(response.status)}.`,
|
|
8413
|
+
response
|
|
8414
|
+
);
|
|
8415
|
+
this.name = "FunctionsHttpError";
|
|
8416
|
+
}
|
|
8417
|
+
};
|
|
8418
|
+
var FunctionsRelayError = class extends FunctionsError {
|
|
8419
|
+
constructor(code, message, response, details) {
|
|
8420
|
+
super(code, message, response, details);
|
|
8421
|
+
this.name = "FunctionsRelayError";
|
|
8422
|
+
}
|
|
8423
|
+
};
|
|
8424
|
+
var FunctionsFetchError = class extends FunctionsError {
|
|
8425
|
+
constructor(code, message, cause, details) {
|
|
8426
|
+
super(code, message, cause, details);
|
|
8427
|
+
this.name = "FunctionsFetchError";
|
|
8428
|
+
}
|
|
8429
|
+
};
|
|
8430
|
+
var FUNCTIONS_MESSAGES = {
|
|
8431
|
+
timeout: (ms) => `The function did not answer within ${String(ms)} ms.`,
|
|
8432
|
+
aborted: "The function call was stopped before the function answered.",
|
|
8433
|
+
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.",
|
|
8434
|
+
invalidTimeout: "functions.invoke's timeout is whole milliseconds from 1 to 2147483647.",
|
|
8435
|
+
invalidHeaders: "functions.invoke's headers are not valid HTTP header names and values.",
|
|
8436
|
+
invalidName: "functions.invoke's name is a string: the function's name, then any path and query.",
|
|
8437
|
+
unencodableName: "functions.invoke's name is not well-formed Unicode: it has a lone surrogate.",
|
|
8438
|
+
dotSegments: `functions.invoke's name cannot be "." or "..", and ".." in its path cannot leave /functions/.`,
|
|
8439
|
+
invalidSignal: "functions.invoke's signal is not an AbortSignal.",
|
|
8440
|
+
bodyWithGetOrHead: "functions.invoke cannot send a body with GET or HEAD.",
|
|
8441
|
+
/** `RELAY_ERROR`: a HEAD answer has no body, so Naro's code is not in it. */
|
|
8442
|
+
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.`,
|
|
8443
|
+
unreadable: (status) => `The function answered HTTP ${String(status)}, but its body could not be read as its content-type says.`
|
|
8444
|
+
};
|
|
8445
|
+
var MAX_TIMER_MS = 2147483647;
|
|
8446
|
+
function functionPath(name) {
|
|
8447
|
+
const cut = name.search(/[/?#]/);
|
|
8448
|
+
const head = cut === -1 ? name : name.slice(0, cut);
|
|
8449
|
+
const tail = cut === -1 ? "" : name.slice(cut);
|
|
8450
|
+
return `/functions/${encodeURIComponent(head)}${tail}`;
|
|
8451
|
+
}
|
|
8452
|
+
function dotSegment(segment) {
|
|
8453
|
+
const dots = segment.toLowerCase().replace(/%2e/g, ".");
|
|
8454
|
+
return dots === "." ? 1 : dots === ".." ? 2 : 0;
|
|
8455
|
+
}
|
|
8456
|
+
function leavesFunctions(input) {
|
|
8457
|
+
const name = input.replace(/[\t\n\r]/g, "");
|
|
8458
|
+
const cut = name.search(/[/?#]/);
|
|
8459
|
+
const head = cut === -1 ? name : name.slice(0, cut);
|
|
8460
|
+
if (head === "." || head === "..") return true;
|
|
8461
|
+
if (cut === -1 || name[cut] !== "/") return false;
|
|
8462
|
+
const rest = name.slice(cut + 1);
|
|
8463
|
+
const end = rest.search(/[?#]/);
|
|
8464
|
+
let depth = 1;
|
|
8465
|
+
for (const segment of (end === -1 ? rest : rest.slice(0, end)).split(
|
|
8466
|
+
/[/\\]/
|
|
8467
|
+
)) {
|
|
8468
|
+
const dots = dotSegment(segment);
|
|
8469
|
+
if (dots === 0) depth += 1;
|
|
8470
|
+
if (dots === 2) depth -= 1;
|
|
8471
|
+
if (depth < 0) return true;
|
|
8472
|
+
}
|
|
8473
|
+
return false;
|
|
8474
|
+
}
|
|
8475
|
+
function pathOf(name) {
|
|
8476
|
+
if (typeof name !== "string")
|
|
8477
|
+
return { invalid: FUNCTIONS_MESSAGES.invalidName };
|
|
8478
|
+
if (leavesFunctions(name)) return { invalid: FUNCTIONS_MESSAGES.dotSegments };
|
|
8479
|
+
try {
|
|
8480
|
+
return { path: functionPath(name) };
|
|
8481
|
+
} catch {
|
|
8482
|
+
return { invalid: FUNCTIONS_MESSAGES.unencodableName };
|
|
8483
|
+
}
|
|
8484
|
+
}
|
|
8485
|
+
function isSignalOrNone(value) {
|
|
8486
|
+
if (value === void 0 || value === null) return true;
|
|
8487
|
+
if (typeof value !== "object") return false;
|
|
8488
|
+
const signal = value;
|
|
8489
|
+
return typeof signal.aborted === "boolean" && typeof signal.addEventListener === "function" && typeof signal.removeEventListener === "function";
|
|
8490
|
+
}
|
|
8491
|
+
function invalidOptions(message) {
|
|
8492
|
+
return failed(new FunctionsFetchError("INVALID_OPTIONS", message, null));
|
|
8493
|
+
}
|
|
8494
|
+
function failed(error) {
|
|
8495
|
+
return { data: null, error };
|
|
8496
|
+
}
|
|
8497
|
+
function fromNaroError(error, cause) {
|
|
8498
|
+
return new FunctionsFetchError(
|
|
8499
|
+
error.code,
|
|
8500
|
+
error.message,
|
|
8501
|
+
cause,
|
|
8502
|
+
error.details
|
|
8503
|
+
);
|
|
8504
|
+
}
|
|
8505
|
+
function failure(kind, cause, timeout) {
|
|
8506
|
+
if (kind === "timeout")
|
|
8507
|
+
return new FunctionsFetchError(
|
|
8508
|
+
"TIMEOUT",
|
|
8509
|
+
FUNCTIONS_MESSAGES.timeout(timeout ?? 0),
|
|
8510
|
+
cause
|
|
8511
|
+
);
|
|
8512
|
+
if (kind === "aborted")
|
|
8513
|
+
return new FunctionsFetchError(
|
|
8514
|
+
"ABORTED",
|
|
8515
|
+
FUNCTIONS_MESSAGES.aborted,
|
|
8516
|
+
cause
|
|
8517
|
+
);
|
|
8518
|
+
return fromNaroError(networkError(cause), cause);
|
|
8519
|
+
}
|
|
8520
|
+
function unreadable(cause, status, signal) {
|
|
8521
|
+
if (signal?.aborted)
|
|
8522
|
+
return new FunctionsFetchError(
|
|
8523
|
+
"ABORTED",
|
|
8524
|
+
FUNCTIONS_MESSAGES.aborted,
|
|
8525
|
+
cause
|
|
8526
|
+
);
|
|
8527
|
+
if (cause instanceof SyntaxError)
|
|
8528
|
+
return new FunctionsFetchError(
|
|
8529
|
+
"INVALID_RESPONSE",
|
|
8530
|
+
FUNCTIONS_MESSAGES.unreadable(status),
|
|
8531
|
+
cause
|
|
8532
|
+
);
|
|
8533
|
+
return fromNaroError(networkError(cause), cause);
|
|
8534
|
+
}
|
|
8535
|
+
async function relayError(response, head) {
|
|
8536
|
+
let body;
|
|
8537
|
+
try {
|
|
8538
|
+
body = await response.clone().json();
|
|
8539
|
+
} catch {
|
|
8540
|
+
body = void 0;
|
|
8541
|
+
}
|
|
8542
|
+
const { status } = response;
|
|
8543
|
+
const error = withRetryAfter(
|
|
8544
|
+
head && body === void 0 ? {
|
|
8545
|
+
code: "RELAY_ERROR",
|
|
8546
|
+
message: FUNCTIONS_MESSAGES.headRelay(status),
|
|
8547
|
+
details: { status }
|
|
8548
|
+
} : errorFromBody(status, body) ?? invalidResponse(status),
|
|
8549
|
+
response.headers
|
|
8550
|
+
);
|
|
8551
|
+
return new FunctionsRelayError(
|
|
8552
|
+
error.code,
|
|
8553
|
+
error.message,
|
|
8554
|
+
response,
|
|
8555
|
+
error.details
|
|
8556
|
+
);
|
|
8557
|
+
}
|
|
8558
|
+
var FunctionsClient = class {
|
|
8559
|
+
#http;
|
|
8560
|
+
#auth;
|
|
8561
|
+
constructor(http, auth) {
|
|
8562
|
+
this.#http = http;
|
|
8563
|
+
this.#auth = auth;
|
|
8564
|
+
}
|
|
8565
|
+
async invoke(name, options = {}) {
|
|
8566
|
+
const method = options.method ?? "POST";
|
|
8567
|
+
const { timeout, signal } = options;
|
|
8568
|
+
const target = pathOf(name);
|
|
8569
|
+
if ("invalid" in target) return invalidOptions(target.invalid);
|
|
8570
|
+
if (timeout !== void 0 && !(Number.isInteger(timeout) && timeout > 0 && timeout <= MAX_TIMER_MS))
|
|
8571
|
+
return invalidOptions(FUNCTIONS_MESSAGES.invalidTimeout);
|
|
8572
|
+
if (!isSignalOrNone(signal))
|
|
8573
|
+
return invalidOptions(FUNCTIONS_MESSAGES.invalidSignal);
|
|
8574
|
+
const verb = typeof method === "string" ? method.toUpperCase() : "";
|
|
8575
|
+
if (options.body !== void 0 && (verb === "GET" || verb === "HEAD"))
|
|
8576
|
+
return invalidOptions(FUNCTIONS_MESSAGES.bodyWithGetOrHead);
|
|
8577
|
+
let headers;
|
|
8578
|
+
try {
|
|
8579
|
+
headers = new Headers(options.headers);
|
|
8580
|
+
} catch (cause) {
|
|
8581
|
+
return failed(
|
|
8582
|
+
new FunctionsFetchError(
|
|
8583
|
+
"INVALID_OPTIONS",
|
|
8584
|
+
FUNCTIONS_MESSAGES.invalidHeaders,
|
|
8585
|
+
cause
|
|
8586
|
+
)
|
|
8587
|
+
);
|
|
8588
|
+
}
|
|
8589
|
+
const prepared = prepareBody(options.body);
|
|
8590
|
+
if (prepared === null)
|
|
8591
|
+
return failed(
|
|
8592
|
+
new FunctionsFetchError(
|
|
8593
|
+
"INVALID_BODY",
|
|
8594
|
+
FUNCTIONS_MESSAGES.invalidBody,
|
|
8595
|
+
null
|
|
8596
|
+
)
|
|
8597
|
+
);
|
|
8598
|
+
if (prepared.contentType !== null && !headers.has("content-type"))
|
|
8599
|
+
headers.set("content-type", prepared.contentType);
|
|
8600
|
+
const result = await fetchUnread(this.#http, {
|
|
8601
|
+
method,
|
|
8602
|
+
path: target.path,
|
|
8603
|
+
headers,
|
|
8604
|
+
body: prepared.body,
|
|
8605
|
+
timeoutMs: timeout,
|
|
8606
|
+
signal,
|
|
8607
|
+
// A sign-in the page URL carried goes first, as for a query (P2-6) —
|
|
8608
|
+
// within the call's timeout and signal; the exchange is not cut.
|
|
8609
|
+
ready: () => this.#auth.ready()
|
|
8610
|
+
});
|
|
8611
|
+
if ("refused" in result) return failed(fromNaroError(result.refused, null));
|
|
8612
|
+
if ("failed" in result)
|
|
8613
|
+
return failed(failure(result.failed, result.cause, timeout));
|
|
8614
|
+
const { response } = result;
|
|
8615
|
+
if (!response.ok) {
|
|
8616
|
+
const error = response.headers.get("x-relay-error") === "true" ? await relayError(response, verb === "HEAD") : new FunctionsHttpError(response);
|
|
8617
|
+
return { data: null, error, response };
|
|
8618
|
+
}
|
|
8619
|
+
const answer = await readAnswer(response, method);
|
|
8620
|
+
if ("unreadable" in answer)
|
|
8621
|
+
return {
|
|
8622
|
+
data: null,
|
|
8623
|
+
error: unreadable(answer.unreadable, response.status, signal),
|
|
8624
|
+
response
|
|
8625
|
+
};
|
|
8626
|
+
return { data: answer.data, error: null, response };
|
|
8627
|
+
}
|
|
8628
|
+
};
|
|
7742
8629
|
function isSpace2(char) {
|
|
7743
8630
|
return char.trim() === "";
|
|
7744
8631
|
}
|
|
@@ -8493,6 +9380,7 @@ var TableBuilder = class {
|
|
|
8493
9380
|
var BACKOFF_STEPS_MS = [1e3, 2e3, 5e3, 1e4];
|
|
8494
9381
|
var RATE_LIMITED_MIN_MS = 5e3;
|
|
8495
9382
|
var IP_LIMIT_WAIT_MS = 6e4;
|
|
9383
|
+
var QUOTA_WAIT_MS = 9e5;
|
|
8496
9384
|
var RESTART_JITTER_MS = 1e4;
|
|
8497
9385
|
var UNOPENED_SLOWDOWN_AFTER = 6;
|
|
8498
9386
|
var UNOPENED_DELAY_MS = 6e4;
|
|
@@ -8509,7 +9397,7 @@ function closeAction(code, reason) {
|
|
|
8509
9397
|
if (code === 4401) return { kind: "session" };
|
|
8510
9398
|
if (code === 4503) return { kind: "restart" };
|
|
8511
9399
|
if (code === 4029)
|
|
8512
|
-
return reason === "IP_RATE_LIMITED" ? { kind: "wait", ms: IP_LIMIT_WAIT_MS } : { kind: "retry", minMs: RATE_LIMITED_MIN_MS };
|
|
9400
|
+
return reason === "IP_RATE_LIMITED" ? { kind: "wait", ms: IP_LIMIT_WAIT_MS } : reason === "REALTIME_QUOTA_EXCEEDED" ? { kind: "wait", ms: QUOTA_WAIT_MS } : { kind: "retry", minMs: RATE_LIMITED_MIN_MS };
|
|
8513
9401
|
return { kind: "retry", minMs: 0 };
|
|
8514
9402
|
}
|
|
8515
9403
|
function backoffMs(attempt22, random) {
|
|
@@ -8884,30 +9772,30 @@ var RealtimeSocket = class {
|
|
|
8884
9772
|
request(topic, event, payload, options = {}) {
|
|
8885
9773
|
const ref = options.ref ?? this.nextRef();
|
|
8886
9774
|
const turn = options.turn ?? "app";
|
|
8887
|
-
return new Promise((
|
|
9775
|
+
return new Promise((resolve9) => {
|
|
8888
9776
|
const lost = {
|
|
8889
9777
|
ok: false,
|
|
8890
9778
|
error: realtimeClosed(1006),
|
|
8891
9779
|
lost: true
|
|
8892
9780
|
};
|
|
8893
|
-
if (!this.isOpen()) return
|
|
9781
|
+
if (!this.isOpen()) return resolve9(lost);
|
|
8894
9782
|
let text;
|
|
8895
9783
|
try {
|
|
8896
9784
|
text = clientFrame(topic, event, payload, ref, options.joinRef);
|
|
8897
9785
|
} catch {
|
|
8898
|
-
return
|
|
9786
|
+
return resolve9(lost);
|
|
8899
9787
|
}
|
|
8900
9788
|
const bytes = utf8Length(text);
|
|
8901
9789
|
const limit = this.#frameLimit;
|
|
8902
9790
|
if (limit !== null && bytes > limit)
|
|
8903
|
-
return
|
|
9791
|
+
return resolve9({ ok: false, error: frameTooLarge(limit), lost: false });
|
|
8904
9792
|
if (turn === "app" && event !== "phx_leave" && !this.#goesInTime())
|
|
8905
|
-
return
|
|
9793
|
+
return resolve9({
|
|
8906
9794
|
ok: false,
|
|
8907
9795
|
error: clientFlooding(CONNECTION_FRAMES_PER_SECOND),
|
|
8908
9796
|
lost: false
|
|
8909
9797
|
});
|
|
8910
|
-
this.#replies.set(ref, { resolve:
|
|
9798
|
+
this.#replies.set(ref, { resolve: resolve9, timer: void 0, bytes, turn });
|
|
8911
9799
|
this.#enqueue({ text, bytes, ref, turn, presence: event === "presence" });
|
|
8912
9800
|
});
|
|
8913
9801
|
}
|
|
@@ -9231,19 +10119,19 @@ var RealtimeSocket = class {
|
|
|
9231
10119
|
#closed(ws, code, reason, waitedMs) {
|
|
9232
10120
|
if (this.#ws !== ws) return;
|
|
9233
10121
|
const wasOpen = this.#state === "open";
|
|
9234
|
-
const
|
|
10122
|
+
const timedOut2 = waitedMs !== null;
|
|
9235
10123
|
detach(ws);
|
|
9236
10124
|
this.#ws = null;
|
|
9237
10125
|
this.#clearTimers();
|
|
9238
|
-
if (
|
|
10126
|
+
if (timedOut2) closeQuietly(ws, 1e3);
|
|
9239
10127
|
if (!wasOpen) this.#unopened += 1;
|
|
9240
10128
|
const said = this.#said;
|
|
9241
10129
|
this.#said = null;
|
|
9242
|
-
if (!
|
|
9243
|
-
const action =
|
|
10130
|
+
if (!timedOut2 && code === 4008) this.#flooded = true;
|
|
10131
|
+
const action = timedOut2 ? { kind: "retry", minMs: 0 } : closeAction(code, said?.code ?? reason);
|
|
9244
10132
|
if (action.kind === "stop")
|
|
9245
10133
|
return this.#stop(said ?? realtimeStopped(code, reason));
|
|
9246
|
-
const error =
|
|
10134
|
+
const error = timedOut2 ? realtimeTimedOut(waitedMs) : said ?? realtimeClosed(code);
|
|
9247
10135
|
this.#failReplies(error);
|
|
9248
10136
|
this.#state = "waiting";
|
|
9249
10137
|
const delay = delayAfter(
|
|
@@ -9256,7 +10144,7 @@ var RealtimeSocket = class {
|
|
|
9256
10144
|
safely(
|
|
9257
10145
|
() => this.#config.events.dropped(
|
|
9258
10146
|
error,
|
|
9259
|
-
|
|
10147
|
+
timedOut2 ? "TIMED_OUT" : "CHANNEL_ERROR"
|
|
9260
10148
|
)
|
|
9261
10149
|
);
|
|
9262
10150
|
}
|
|
@@ -10026,9 +10914,9 @@ var RealtimeChannel = class {
|
|
|
10026
10914
|
*/
|
|
10027
10915
|
#inTurn(joinRef, wait, send2) {
|
|
10028
10916
|
if (!wait && this.#waiting.length === 0) return send2();
|
|
10029
|
-
return new Promise((
|
|
10917
|
+
return new Promise((resolve9) => {
|
|
10030
10918
|
this.#waiting.push(
|
|
10031
|
-
() =>
|
|
10919
|
+
() => resolve9(this.#joinRef === joinRef ? send2() : null)
|
|
10032
10920
|
);
|
|
10033
10921
|
});
|
|
10034
10922
|
}
|
|
@@ -10643,13 +11531,13 @@ async function uploadInParts(a) {
|
|
|
10643
11531
|
}).catch(() => void 0);
|
|
10644
11532
|
};
|
|
10645
11533
|
const etags = [];
|
|
10646
|
-
const
|
|
11534
|
+
const failure2 = {
|
|
10647
11535
|
error: signal?.aborted ? uploadAborted() : null
|
|
10648
11536
|
};
|
|
10649
11537
|
let next = 1;
|
|
10650
11538
|
let sentBytes = 0;
|
|
10651
11539
|
const onAbort = () => {
|
|
10652
|
-
|
|
11540
|
+
failure2.error ??= uploadAborted();
|
|
10653
11541
|
};
|
|
10654
11542
|
const tryPart = async (n, bytes) => {
|
|
10655
11543
|
const sent2 = await send(a.http, {
|
|
@@ -10678,18 +11566,18 @@ async function uploadInParts(a) {
|
|
|
10678
11566
|
progress(sentBytes);
|
|
10679
11567
|
return null;
|
|
10680
11568
|
}
|
|
10681
|
-
if (attempt22 === ATTEMPTS ||
|
|
11569
|
+
if (attempt22 === ATTEMPTS || failure2.error !== null || !retryable(tried.error, tried.status))
|
|
10682
11570
|
return tried.error;
|
|
10683
11571
|
await sleep2(waitAfter(tried.error, attempt22), signal);
|
|
10684
|
-
if (
|
|
11572
|
+
if (failure2.error !== null) return tried.error;
|
|
10685
11573
|
}
|
|
10686
11574
|
};
|
|
10687
11575
|
const worker = async () => {
|
|
10688
|
-
while (
|
|
11576
|
+
while (failure2.error === null && next <= partCount) {
|
|
10689
11577
|
const n = next;
|
|
10690
11578
|
next += 1;
|
|
10691
11579
|
const error = await sendPart(n);
|
|
10692
|
-
if (error !== null)
|
|
11580
|
+
if (error !== null) failure2.error ??= error;
|
|
10693
11581
|
}
|
|
10694
11582
|
};
|
|
10695
11583
|
signal?.addEventListener("abort", onAbort);
|
|
@@ -10697,9 +11585,9 @@ async function uploadInParts(a) {
|
|
|
10697
11585
|
Array.from({ length: Math.min(CONCURRENCY, partCount) }, worker)
|
|
10698
11586
|
);
|
|
10699
11587
|
signal?.removeEventListener("abort", onAbort);
|
|
10700
|
-
if (
|
|
11588
|
+
if (failure2.error !== null) {
|
|
10701
11589
|
await callOff();
|
|
10702
|
-
return fail(
|
|
11590
|
+
return fail(failure2.error);
|
|
10703
11591
|
}
|
|
10704
11592
|
const completed = await call(a.http, {
|
|
10705
11593
|
method: "POST",
|
|
@@ -10926,8 +11814,8 @@ var StorageFileApi = class {
|
|
|
10926
11814
|
const payload = payloadOf(body, options.contentType);
|
|
10927
11815
|
if (!payload.ok) return fail(payload.error);
|
|
10928
11816
|
const cache = `max-age=${options.cacheControl ?? "3600"}`;
|
|
10929
|
-
const
|
|
10930
|
-
if (
|
|
11817
|
+
const unreadable2 = headerProblem(options.contentType, cache);
|
|
11818
|
+
if (unreadable2 !== null) return fail(unreadable2);
|
|
10931
11819
|
const { signal } = options;
|
|
10932
11820
|
if (signal?.aborted) return fail(uploadAborted());
|
|
10933
11821
|
await this.#auth.ready();
|
|
@@ -11228,8 +12116,8 @@ var StorageFileApi = class {
|
|
|
11228
12116
|
const payload = payloadOf(body, given.contentType);
|
|
11229
12117
|
if (!payload.ok) return fail(payload.error);
|
|
11230
12118
|
const cache = `max-age=${given.cacheControl ?? "3600"}`;
|
|
11231
|
-
const
|
|
11232
|
-
if (
|
|
12119
|
+
const unreadable2 = headerProblem(given.contentType, cache);
|
|
12120
|
+
if (unreadable2 !== null) return fail(unreadable2);
|
|
11233
12121
|
const { signal } = given;
|
|
11234
12122
|
if (signal?.aborted) return fail(uploadAborted());
|
|
11235
12123
|
const uploaded = payload.size > MAX_SINGLE_UPLOAD_BYTES2 ? await uploadInParts({
|
|
@@ -11308,12 +12196,14 @@ var CONFIG_MESSAGES = {
|
|
|
11308
12196
|
bothKeys: "Pass createClient an anonKey or a serviceKey, not both.",
|
|
11309
12197
|
serviceKeyOffServer: SERVICE_KEY_OFF_SERVER_MESSAGE,
|
|
11310
12198
|
fetch: "This runtime has no fetch. Pass one to createClient({ fetch }).",
|
|
11311
|
-
realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647."
|
|
12199
|
+
realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647.",
|
|
12200
|
+
accessTokenWithServiceKey: "Pass createClient an accessToken or a serviceKey, not both: the service key skips every policy, so the user's token would not count.",
|
|
12201
|
+
accessToken: "createClient's accessToken is a function that returns the user's access token, or null for none."
|
|
11312
12202
|
};
|
|
11313
12203
|
var SERVICE_KEY_PREFIX = "naro_sk_";
|
|
11314
|
-
var
|
|
12204
|
+
var MAX_TIMER_MS2 = 2147483647;
|
|
11315
12205
|
function isMilliseconds(value) {
|
|
11316
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <=
|
|
12206
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_TIMER_MS2;
|
|
11317
12207
|
}
|
|
11318
12208
|
function realtimeOptionsOf(value) {
|
|
11319
12209
|
if (value === void 0) return {};
|
|
@@ -11363,6 +12253,35 @@ function apiKeyOf(options) {
|
|
|
11363
12253
|
throw new Error(CONFIG_MESSAGES.serviceKeyOffServer);
|
|
11364
12254
|
return { key, service };
|
|
11365
12255
|
}
|
|
12256
|
+
function accessTokenOf(value) {
|
|
12257
|
+
if (value === void 0) return void 0;
|
|
12258
|
+
if (typeof value !== "function") throw new Error(CONFIG_MESSAGES.accessToken);
|
|
12259
|
+
const read = value;
|
|
12260
|
+
return {
|
|
12261
|
+
async get() {
|
|
12262
|
+
const token = await read();
|
|
12263
|
+
if (token === null || token === void 0 || token === "") return null;
|
|
12264
|
+
if (typeof token !== "string")
|
|
12265
|
+
throw new TypeError(`accessToken returned a ${typeof token}`);
|
|
12266
|
+
if (!/^[\x21-\x7E]+$/.test(token))
|
|
12267
|
+
throw new TypeError(
|
|
12268
|
+
"accessToken returned a string that is not a token"
|
|
12269
|
+
);
|
|
12270
|
+
return token;
|
|
12271
|
+
}
|
|
12272
|
+
};
|
|
12273
|
+
}
|
|
12274
|
+
function refusingAuth() {
|
|
12275
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
12276
|
+
get(_target, property) {
|
|
12277
|
+
if (typeof property === "symbol" || property === "then" || property === "toJSON" || property === "constructor" || property === "asymmetricMatch")
|
|
12278
|
+
return void 0;
|
|
12279
|
+
if (property === "toString") return Object.prototype.toString;
|
|
12280
|
+
if (property === "valueOf") return Object.prototype.valueOf;
|
|
12281
|
+
throw new Error(accessTokenAuthMessage(property));
|
|
12282
|
+
}
|
|
12283
|
+
});
|
|
12284
|
+
}
|
|
11366
12285
|
function queryRunner(http, auth) {
|
|
11367
12286
|
return async (query) => {
|
|
11368
12287
|
await auth.ready();
|
|
@@ -11391,7 +12310,14 @@ var NaroClient2 = class {
|
|
|
11391
12310
|
* them, as supabase-js does.
|
|
11392
12311
|
*/
|
|
11393
12312
|
storage;
|
|
12313
|
+
/**
|
|
12314
|
+
* The project's Naro Functions (design §9): `functions.invoke(name, {
|
|
12315
|
+
* body })`, as in supabase-js.
|
|
12316
|
+
*/
|
|
12317
|
+
functions;
|
|
11394
12318
|
#run;
|
|
12319
|
+
/** Made with `accessToken`: `auth` and `channel()` refuse. */
|
|
12320
|
+
#accessToken;
|
|
11395
12321
|
#newRealtime;
|
|
11396
12322
|
/**
|
|
11397
12323
|
* Made by the first channel(), not by the constructor: it listens to every
|
|
@@ -11402,37 +12328,38 @@ var NaroClient2 = class {
|
|
|
11402
12328
|
constructor(options) {
|
|
11403
12329
|
const baseUrl = baseUrlOf(options.url);
|
|
11404
12330
|
const { key: apiKey, service } = apiKeyOf(options);
|
|
12331
|
+
const accessToken = accessTokenOf(options.accessToken);
|
|
12332
|
+
if (accessToken !== void 0 && service)
|
|
12333
|
+
throw new Error(CONFIG_MESSAGES.accessTokenWithServiceKey);
|
|
11405
12334
|
if (options.fetch === void 0 && typeof globalThis.fetch !== "function")
|
|
11406
12335
|
throw new Error(CONFIG_MESSAGES.fetch);
|
|
11407
12336
|
const realtime = realtimeOptionsOf(options.realtime);
|
|
11408
|
-
const storage = options.auth?.storage ?? defaultStorage();
|
|
12337
|
+
const storage = accessToken !== void 0 ? memoryStorage() : options.auth?.storage ?? defaultStorage();
|
|
11409
12338
|
const http = {
|
|
11410
12339
|
baseUrl,
|
|
11411
12340
|
apiKey,
|
|
11412
12341
|
fetch: options.fetch,
|
|
11413
12342
|
tokens: new TokenStore(storage, storageKey(baseUrl)),
|
|
11414
|
-
|
|
11415
|
-
|
|
12343
|
+
service,
|
|
12344
|
+
...accessToken !== void 0 ? { accessToken } : { accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)) }
|
|
11416
12345
|
};
|
|
11417
|
-
|
|
12346
|
+
const auth = new AuthClient(http, {
|
|
11418
12347
|
verifiers: new FlowVerifiers(
|
|
11419
12348
|
new TokenStore(storage, verifierKey(baseUrl))
|
|
11420
12349
|
),
|
|
11421
12350
|
recovery: new RecoveryToken(
|
|
11422
12351
|
new TokenStore(storage, recoveryKey(baseUrl))
|
|
11423
12352
|
),
|
|
11424
|
-
detectSessionInUrl: options.auth?.detectSessionInUrl ?? true,
|
|
12353
|
+
detectSessionInUrl: accessToken === void 0 && (options.auth?.detectSessionInUrl ?? true),
|
|
11425
12354
|
shared: { storage, key: storageKey(baseUrl) },
|
|
11426
12355
|
serviceKey: service
|
|
11427
12356
|
});
|
|
11428
|
-
this.#
|
|
11429
|
-
this.
|
|
11430
|
-
this.#
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
options: realtime
|
|
11435
|
-
});
|
|
12357
|
+
this.#accessToken = accessToken !== void 0;
|
|
12358
|
+
this.auth = this.#accessToken ? refusingAuth() : auth;
|
|
12359
|
+
this.#run = queryRunner(http, auth);
|
|
12360
|
+
this.storage = new StorageClient(http, auth);
|
|
12361
|
+
this.functions = new FunctionsClient(http, auth);
|
|
12362
|
+
this.#newRealtime = () => new RealtimeClient({ http, auth, service, options: realtime });
|
|
11436
12363
|
}
|
|
11437
12364
|
/** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
|
|
11438
12365
|
from(table3) {
|
|
@@ -11444,6 +12371,7 @@ var NaroClient2 = class {
|
|
|
11444
12371
|
* `removeChannel`. Nothing connects until the first `subscribe()`.
|
|
11445
12372
|
*/
|
|
11446
12373
|
channel(name, options) {
|
|
12374
|
+
if (this.#accessToken) throw new Error(ACCESS_TOKEN_CHANNEL_MESSAGE);
|
|
11447
12375
|
return this.#client().channel(name, options);
|
|
11448
12376
|
}
|
|
11449
12377
|
/** The channels made with `channel()` and not yet removed. */
|
|
@@ -11455,7 +12383,9 @@ var NaroClient2 = class {
|
|
|
11455
12383
|
* one; the connection closes with the last channel.
|
|
11456
12384
|
*/
|
|
11457
12385
|
removeChannel(channel) {
|
|
11458
|
-
|
|
12386
|
+
if (this.#realtime === void 0)
|
|
12387
|
+
return Promise.resolve({ data: null, error: null });
|
|
12388
|
+
return this.#realtime.removeChannel(channel);
|
|
11459
12389
|
}
|
|
11460
12390
|
/** `removeChannel()` for every channel; the connection closes. */
|
|
11461
12391
|
removeAllChannels() {
|
|
@@ -11773,7 +12703,7 @@ function stopOnInterrupt(onFirst) {
|
|
|
11773
12703
|
};
|
|
11774
12704
|
}
|
|
11775
12705
|
async function writeAtomically(bytes, destination) {
|
|
11776
|
-
const partial =
|
|
12706
|
+
const partial = join6(dirname4(destination), `.${randomUUID()}.naro-partial`);
|
|
11777
12707
|
const stopListening = removeOnInterrupt(partial);
|
|
11778
12708
|
try {
|
|
11779
12709
|
await pipeline2(bytes, createWriteStream2(partial, { flags: "wx" }));
|
|
@@ -11814,7 +12744,7 @@ function progressReporter(output, total) {
|
|
|
11814
12744
|
};
|
|
11815
12745
|
}
|
|
11816
12746
|
async function upload(context, local, target) {
|
|
11817
|
-
const file =
|
|
12747
|
+
const file = resolve8(context.cwd, local);
|
|
11818
12748
|
const info = await stat(file).catch(() => null);
|
|
11819
12749
|
if (info === null)
|
|
11820
12750
|
throw new NaroError(
|
|
@@ -11948,10 +12878,10 @@ async function download(context, source, local) {
|
|
|
11948
12878
|
"VALIDATION_FAILED",
|
|
11949
12879
|
"naro storage cp downloads one object: name it, naro://<bucket>/<path>."
|
|
11950
12880
|
);
|
|
11951
|
-
const resolved =
|
|
12881
|
+
const resolved = resolve8(context.cwd, local);
|
|
11952
12882
|
const isDirectory = local.endsWith("/") || (await stat(resolved).catch(() => null))?.isDirectory() === true;
|
|
11953
|
-
const destination = isDirectory ?
|
|
11954
|
-
const folder = await stat(
|
|
12883
|
+
const destination = isDirectory ? join6(resolved, savedAs(source.path)) : resolved;
|
|
12884
|
+
const folder = await stat(dirname4(destination)).catch(() => null);
|
|
11955
12885
|
if (folder === null || !folder.isDirectory())
|
|
11956
12886
|
throw new NaroError(
|
|
11957
12887
|
"VALIDATION_FAILED",
|
|
@@ -12239,7 +13169,7 @@ function projectName(project) {
|
|
|
12239
13169
|
return `Moved to another team ${printable(project.projectId)}`;
|
|
12240
13170
|
return printable(project.name ?? project.projectId);
|
|
12241
13171
|
}
|
|
12242
|
-
function
|
|
13172
|
+
function usageLines3(report, summary) {
|
|
12243
13173
|
const lines = [
|
|
12244
13174
|
`${printable(report.team.slug)} \xB7 ${planName(report.team.plan)} plan \xB7 ${printable(report.month)} (UTC)`
|
|
12245
13175
|
];
|
|
@@ -12286,7 +13216,7 @@ async function runUsage(context) {
|
|
|
12286
13216
|
month !== void 0 ? { month } : {}
|
|
12287
13217
|
);
|
|
12288
13218
|
const summary = summarizeTeamUsage(report, usageMonthOf(/* @__PURE__ */ new Date()));
|
|
12289
|
-
context.output.result({ ...report, summary },
|
|
13219
|
+
context.output.result({ ...report, summary }, usageLines3(report, summary));
|
|
12290
13220
|
}
|
|
12291
13221
|
|
|
12292
13222
|
// src/router.ts
|
|
@@ -12411,6 +13341,8 @@ Usage:
|
|
|
12411
13341
|
naro backend keys [--reveal] API keys; --reveal prints the service key (a secret; needs database write)
|
|
12412
13342
|
naro backend keys rotate --anon|--service
|
|
12413
13343
|
Replace a key; the old one stops working at once (asks first)
|
|
13344
|
+
naro backend signing-key rotate Replace the key that signs access tokens (if it may have leaked);
|
|
13345
|
+
tokens already issued keep working up to 15 minutes (asks first)
|
|
12414
13346
|
naro backend auth Sign-in settings: email, Google, GitHub, redirects, callback URLs,
|
|
12415
13347
|
sign-ups and the minimum password length
|
|
12416
13348
|
naro backend auth signups <on|off>
|
|
@@ -12494,6 +13426,28 @@ Usage:
|
|
|
12494
13426
|
naro realtime tables remove <table>\u2026 [--yes]
|
|
12495
13427
|
Stop publishing tables' changes (asks first)
|
|
12496
13428
|
|
|
13429
|
+
naro functions [list] Naro Functions: name, version, verify_jwt, status and URL of each function
|
|
13430
|
+
naro functions new <name> Write naro/functions/<name>/index.ts, a handler to start from
|
|
13431
|
+
naro functions deploy [name\u2026] [--no-verify-jwt] [--import-map <file>] [--skip-check]
|
|
13432
|
+
Bundle on this machine (Node.js 22 or later), check that it starts, upload;
|
|
13433
|
+
every function in the folder when no name is given
|
|
13434
|
+
naro functions versions <name> Its stored versions, newest first
|
|
13435
|
+
naro functions rollback <name> [<version>]
|
|
13436
|
+
Put an earlier version live again (the one before the live one by default)
|
|
13437
|
+
naro functions delete <name> Delete it: its URL stops working (asks first; --yes to skip)
|
|
13438
|
+
naro functions logs <name> [--level <l>] [--follow]
|
|
13439
|
+
Its newest log lines
|
|
13440
|
+
naro functions secrets [list] The project's function secrets: names and digests, never values
|
|
13441
|
+
naro functions secrets set <NAME> Set one (value from a hidden prompt or stdin), then sync the functions
|
|
13442
|
+
(--no-sync leaves them to the control plane's sweep)
|
|
13443
|
+
naro functions secrets import <file>
|
|
13444
|
+
Set a dotenv file's lines (asks first; --yes in CI)
|
|
13445
|
+
naro functions secrets unset <NAME>\u2026
|
|
13446
|
+
Remove secrets (asks first)
|
|
13447
|
+
naro functions secrets sync [--registry]
|
|
13448
|
+
Upload out-of-date functions again until none is left; --registry also rewrites
|
|
13449
|
+
the function list in the project's database (after naro db restore)
|
|
13450
|
+
|
|
12497
13451
|
naro promote <deployment-id> [--to <environment>]
|
|
12498
13452
|
Publish a build to production, or to an environment with --to; it says what it will do, then asks.
|
|
12499
13453
|
A build moving between an environment and another surface is built again from its commit with the
|
|
@@ -12522,6 +13476,11 @@ Global options:
|
|
|
12522
13476
|
--browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
|
|
12523
13477
|
--os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
|
|
12524
13478
|
--to <environment> production (the default) or an environment name: where naro promote publishes the build (naro promote)
|
|
13479
|
+
--no-verify-jwt Let calls in without a project JWT (naro functions deploy; wins over function.json)
|
|
13480
|
+
--import-map <file> The import map for functions whose folder has no deno.json (naro functions deploy)
|
|
13481
|
+
--skip-check Upload without booting the bundle in workerd first (naro functions deploy)
|
|
13482
|
+
--no-sync Leave the functions to the control plane's sweep (naro functions secrets set, import, unset; naro backend keys rotate)
|
|
13483
|
+
--registry Also rewrite the function list in the project's database (naro functions secrets sync)
|
|
12525
13484
|
--env <e> production or preview (naro logs, naro insights: production by default; naro env pull, naro deploy: preview by default, or an environment name);
|
|
12526
13485
|
production or an environment name for the data commands (naro db, migration, gen, backend, policy, storage, realtime: production by default)
|
|
12527
13486
|
--branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
|
|
@@ -12536,7 +13495,8 @@ Global options:
|
|
|
12536
13495
|
--dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
|
|
12537
13496
|
--output <file> Write to a file instead of stdout (naro gen types, db dump)
|
|
12538
13497
|
--lang <lang> Language of the types; typescript, the default, is the only one (naro gen types)
|
|
12539
|
-
--dir <path> Migrations directory (default ./migrations, or <--root>/migrations)
|
|
13498
|
+
--dir <path> Migrations directory (default ./migrations, or <--root>/migrations) (naro db push \u2026);
|
|
13499
|
+
functions directory (default ./naro/functions, or <--root>/naro/functions) (naro functions \u2026)
|
|
12540
13500
|
--strict Fail on warnings too, not only errors (naro db lint)
|
|
12541
13501
|
--schema-only Dump the schema without rows (naro db dump)
|
|
12542
13502
|
--data-only Dump rows without the schema (naro db dump)
|
|
@@ -12745,9 +13705,65 @@ function routeRealtime(words) {
|
|
|
12745
13705
|
return null;
|
|
12746
13706
|
}
|
|
12747
13707
|
}
|
|
13708
|
+
function routeFunctions(words) {
|
|
13709
|
+
const [sub] = words;
|
|
13710
|
+
switch (sub) {
|
|
13711
|
+
case void 0:
|
|
13712
|
+
case "list":
|
|
13713
|
+
case "ls":
|
|
13714
|
+
return words.length <= 1 ? { handler: runFunctionsList, positionals: [] } : null;
|
|
13715
|
+
case "versions":
|
|
13716
|
+
return { handler: runFunctionsVersions, positionals: words.slice(1) };
|
|
13717
|
+
case "new":
|
|
13718
|
+
return { handler: runFunctionsNew, positionals: words.slice(1) };
|
|
13719
|
+
case "deploy":
|
|
13720
|
+
return { handler: runFunctionsDeploy, positionals: words.slice(1) };
|
|
13721
|
+
case "rollback":
|
|
13722
|
+
return { handler: runFunctionsRollback, positionals: words.slice(1) };
|
|
13723
|
+
case "delete":
|
|
13724
|
+
case "remove":
|
|
13725
|
+
case "rm":
|
|
13726
|
+
return { handler: runFunctionsDelete, positionals: words.slice(1) };
|
|
13727
|
+
case "logs":
|
|
13728
|
+
return { handler: runFunctionsLogs, positionals: words.slice(1) };
|
|
13729
|
+
case "secrets": {
|
|
13730
|
+
const action = words[1];
|
|
13731
|
+
switch (action) {
|
|
13732
|
+
case void 0:
|
|
13733
|
+
case "list":
|
|
13734
|
+
case "ls":
|
|
13735
|
+
return words.length <= 2 ? { handler: runFunctionsSecretsList, positionals: [] } : null;
|
|
13736
|
+
case "set":
|
|
13737
|
+
return {
|
|
13738
|
+
handler: runFunctionsSecretsSet,
|
|
13739
|
+
positionals: words.slice(2)
|
|
13740
|
+
};
|
|
13741
|
+
case "import":
|
|
13742
|
+
return {
|
|
13743
|
+
handler: runFunctionsSecretsImport,
|
|
13744
|
+
positionals: words.slice(2)
|
|
13745
|
+
};
|
|
13746
|
+
case "unset":
|
|
13747
|
+
return {
|
|
13748
|
+
handler: runFunctionsSecretsUnset,
|
|
13749
|
+
positionals: words.slice(2)
|
|
13750
|
+
};
|
|
13751
|
+
case "sync":
|
|
13752
|
+
return words.length === 2 ? { handler: runFunctionsSecretsSync, positionals: [] } : null;
|
|
13753
|
+
default:
|
|
13754
|
+
return null;
|
|
13755
|
+
}
|
|
13756
|
+
}
|
|
13757
|
+
default:
|
|
13758
|
+
return null;
|
|
13759
|
+
}
|
|
13760
|
+
}
|
|
12748
13761
|
async function refuseTo() {
|
|
12749
13762
|
throw new NaroError("VALIDATION_FAILED", "--to is only for naro promote.");
|
|
12750
13763
|
}
|
|
13764
|
+
async function refuseFunctionsEnv() {
|
|
13765
|
+
throw new NaroError("VALIDATION_FAILED", FUNCTIONS_ENV_REFUSAL);
|
|
13766
|
+
}
|
|
12751
13767
|
var DATA_COMMANDS = /* @__PURE__ */ new Set([
|
|
12752
13768
|
"db",
|
|
12753
13769
|
"migration",
|
|
@@ -12789,10 +13805,26 @@ function guidedError(error, parsed) {
|
|
|
12789
13805
|
error.message = usage === -1 ? forEnvironment(error.message, environment) : `${forEnvironment(error.message.slice(0, usage), environment)}${error.message.slice(usage)}`;
|
|
12790
13806
|
return error;
|
|
12791
13807
|
}
|
|
13808
|
+
function unknownFunctionsWords(words) {
|
|
13809
|
+
const [sub = "", action = "", extra = ""] = words;
|
|
13810
|
+
if (sub === "list" || sub === "ls") return [sub, action];
|
|
13811
|
+
if (sub === "secrets")
|
|
13812
|
+
return action === "list" || action === "ls" || action === "sync" ? [sub, action, extra] : [sub, action];
|
|
13813
|
+
return [sub];
|
|
13814
|
+
}
|
|
13815
|
+
function unknownCommandMessage(parsed) {
|
|
13816
|
+
const words = parsed.command === "functions" ? [
|
|
13817
|
+
"functions",
|
|
13818
|
+
...unknownFunctionsWords(parsed.positionals).map(printable)
|
|
13819
|
+
] : [parsed.command, ...parsed.positionals];
|
|
13820
|
+
return `Unknown command: ${words.join(" ")}. See \`naro help\`.`;
|
|
13821
|
+
}
|
|
12792
13822
|
function route(parsed) {
|
|
12793
13823
|
const routed = routeCommand(parsed);
|
|
12794
13824
|
if (routed && parsed.flags.to !== void 0 && parsed.command !== "promote" && parsed.command !== "rollback")
|
|
12795
13825
|
return { handler: refuseTo, positionals: routed.positionals };
|
|
13826
|
+
if (routed && parsed.command === "functions" && parsed.flags.env !== void 0)
|
|
13827
|
+
return { handler: refuseFunctionsEnv, positionals: routed.positionals };
|
|
12796
13828
|
const refusal = routed ? dataEnvRefusal(parsed) : null;
|
|
12797
13829
|
if (routed && refusal)
|
|
12798
13830
|
return {
|
|
@@ -13040,6 +14072,13 @@ function routeCommand(parsed) {
|
|
|
13040
14072
|
positionals: positionals.slice(2)
|
|
13041
14073
|
};
|
|
13042
14074
|
return null;
|
|
14075
|
+
case "signing-key":
|
|
14076
|
+
if (positionals[1] === "rotate")
|
|
14077
|
+
return {
|
|
14078
|
+
handler: runBackendSigningKeyRotate,
|
|
14079
|
+
positionals: positionals.slice(2)
|
|
14080
|
+
};
|
|
14081
|
+
return null;
|
|
13043
14082
|
case "auth":
|
|
13044
14083
|
return routeBackendAuth(positionals.slice(1));
|
|
13045
14084
|
case "users":
|
|
@@ -13051,6 +14090,8 @@ function routeCommand(parsed) {
|
|
|
13051
14090
|
return routeStorage(positionals);
|
|
13052
14091
|
case "realtime":
|
|
13053
14092
|
return routeRealtime(positionals);
|
|
14093
|
+
case "functions":
|
|
14094
|
+
return routeFunctions(positionals);
|
|
13054
14095
|
case "policy":
|
|
13055
14096
|
switch (sub) {
|
|
13056
14097
|
case void 0:
|
|
@@ -13094,19 +14135,6 @@ function routeCommand(parsed) {
|
|
|
13094
14135
|
return null;
|
|
13095
14136
|
}
|
|
13096
14137
|
}
|
|
13097
|
-
function version() {
|
|
13098
|
-
try {
|
|
13099
|
-
const pkg = JSON.parse(
|
|
13100
|
-
readFileSync6(
|
|
13101
|
-
join6(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
13102
|
-
"utf8"
|
|
13103
|
-
)
|
|
13104
|
-
);
|
|
13105
|
-
return pkg.version ?? "unknown";
|
|
13106
|
-
} catch {
|
|
13107
|
-
return "unknown";
|
|
13108
|
-
}
|
|
13109
|
-
}
|
|
13110
14138
|
|
|
13111
14139
|
// src/index.ts
|
|
13112
14140
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -13132,20 +14160,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
13132
14160
|
const output = new Output(parsed.flags.json, parsed.flags.debug);
|
|
13133
14161
|
if (parsed.unknown.length > 0) {
|
|
13134
14162
|
output.fail(
|
|
13135
|
-
new NaroError(
|
|
13136
|
-
"VALIDATION_FAILED",
|
|
13137
|
-
secretFlagProblem(parsed.unknown) ?? `Unknown option: ${parsed.unknown.join(", ")}. See \`naro help\`.`
|
|
13138
|
-
)
|
|
14163
|
+
new NaroError("VALIDATION_FAILED", unknownOptionProblem(parsed))
|
|
13139
14164
|
);
|
|
13140
14165
|
return;
|
|
13141
14166
|
}
|
|
13142
14167
|
const routed = route(parsed);
|
|
13143
14168
|
if (!routed) {
|
|
13144
14169
|
output.fail(
|
|
13145
|
-
new NaroError(
|
|
13146
|
-
"VALIDATION_FAILED",
|
|
13147
|
-
`Unknown command: ${[parsed.command, ...parsed.positionals].join(" ")}. See \`naro help\`.`
|
|
13148
|
-
)
|
|
14170
|
+
new NaroError("VALIDATION_FAILED", unknownCommandMessage(parsed))
|
|
13149
14171
|
);
|
|
13150
14172
|
return;
|
|
13151
14173
|
}
|