naro-cli 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/args.d.ts +238 -0
- package/dist/{chunk-MBSO3K6L.js → chunk-ALK4VTL2.js} +2171 -346
- package/dist/commands/confirm.d.ts +12 -0
- package/dist/commands/db.d.ts +32 -0
- package/dist/commands/migration.d.ts +78 -0
- package/dist/context.d.ts +21 -0
- package/dist/data-environment.d.ts +36 -0
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.js +5 -1
- package/dist/functions/deploy.d.ts +70 -0
- package/dist/functions/retry.d.ts +16 -0
- package/dist/index.js +1696 -721
- package/dist/version.d.ts +6 -0
- package/package.json +10 -4
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-ALK4VTL2.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,31 @@ 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
|
}
|
|
1468
1247
|
|
|
1469
1248
|
// src/commands/backend-auth.ts
|
|
1470
|
-
var
|
|
1249
|
+
var processStdin = {
|
|
1471
1250
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1472
1251
|
readStdin
|
|
1473
1252
|
};
|
|
@@ -1624,7 +1403,7 @@ async function change(context, send2, done, after) {
|
|
|
1624
1403
|
)
|
|
1625
1404
|
);
|
|
1626
1405
|
}
|
|
1627
|
-
async function runBackendAuthEmailSet(context, input =
|
|
1406
|
+
async function runBackendAuthEmailSet(context, input = processStdin) {
|
|
1628
1407
|
const { provider, from } = context.flags;
|
|
1629
1408
|
if (provider === void 0 || from === void 0)
|
|
1630
1409
|
throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.email);
|
|
@@ -1670,7 +1449,7 @@ function providerOf(context, usage) {
|
|
|
1670
1449
|
throw new NaroError("VALIDATION_FAILED", usage);
|
|
1671
1450
|
return provider;
|
|
1672
1451
|
}
|
|
1673
|
-
async function runBackendAuthOAuthSet(context, input =
|
|
1452
|
+
async function runBackendAuthOAuthSet(context, input = processStdin) {
|
|
1674
1453
|
const provider = providerOf(context, AUTH_USAGE.oauth);
|
|
1675
1454
|
const clientId = context.flags.clientId;
|
|
1676
1455
|
if (clientId === void 0)
|
|
@@ -1853,7 +1632,7 @@ var USERS_USAGE = {
|
|
|
1853
1632
|
var PASSWORD_FLAG_WARNING = "\u26A0 --password puts the password in your shell history. Pipe it in with --password-stdin instead.";
|
|
1854
1633
|
var OUTDATED_MESSAGE = "This project's backend is older than user management. Run `naro backend enable` once to update this project's backend. Then run this command again.";
|
|
1855
1634
|
var PAGE_SIZE = 1e3;
|
|
1856
|
-
var
|
|
1635
|
+
var processStdin2 = {
|
|
1857
1636
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1858
1637
|
readStdin
|
|
1859
1638
|
};
|
|
@@ -1955,7 +1734,7 @@ function userLines(user) {
|
|
|
1955
1734
|
`Created: ${printable(user.createdAt)}`
|
|
1956
1735
|
];
|
|
1957
1736
|
}
|
|
1958
|
-
async function runBackendUsersCreate(context, input =
|
|
1737
|
+
async function runBackendUsersCreate(context, input = processStdin2) {
|
|
1959
1738
|
const { username, email } = context.flags;
|
|
1960
1739
|
if (username === void 0 && email === void 0 || context.positionals.length > 0)
|
|
1961
1740
|
throw new NaroError("VALIDATION_FAILED", USERS_USAGE.create);
|
|
@@ -2033,9 +1812,9 @@ async function runCancel(context) {
|
|
|
2033
1812
|
}
|
|
2034
1813
|
|
|
2035
1814
|
// src/commands/db.ts
|
|
2036
|
-
import { readFileSync
|
|
1815
|
+
import { readFileSync } from "fs";
|
|
2037
1816
|
import { resolve } from "path";
|
|
2038
|
-
var
|
|
1817
|
+
var processStdin3 = {
|
|
2039
1818
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
2040
1819
|
readStdin
|
|
2041
1820
|
};
|
|
@@ -2144,7 +1923,7 @@ async function readSql(context, deps) {
|
|
|
2144
1923
|
sql = positional;
|
|
2145
1924
|
} else if (context.flags.file !== void 0) {
|
|
2146
1925
|
try {
|
|
2147
|
-
sql =
|
|
1926
|
+
sql = readFileSync(resolve(context.cwd, context.flags.file), "utf8");
|
|
2148
1927
|
} catch {
|
|
2149
1928
|
throw new NaroError(
|
|
2150
1929
|
"VALIDATION_FAILED",
|
|
@@ -2158,7 +1937,7 @@ async function readSql(context, deps) {
|
|
|
2158
1937
|
throw new NaroError("VALIDATION_FAILED", USAGE);
|
|
2159
1938
|
return sql;
|
|
2160
1939
|
}
|
|
2161
|
-
async function runDbQuery(context, deps =
|
|
1940
|
+
async function runDbQuery(context, deps = processStdin3) {
|
|
2162
1941
|
const sql = await readSql(context, deps);
|
|
2163
1942
|
const client = await dataClient(context);
|
|
2164
1943
|
const projectId = await context.resolveProjectId();
|
|
@@ -2177,13 +1956,13 @@ async function runDbQuery(context, deps = processStdin4) {
|
|
|
2177
1956
|
// src/commands/db-backup.ts
|
|
2178
1957
|
import {
|
|
2179
1958
|
createWriteStream,
|
|
2180
|
-
mkdirSync
|
|
1959
|
+
mkdirSync,
|
|
2181
1960
|
renameSync,
|
|
2182
1961
|
rmSync,
|
|
2183
1962
|
statSync,
|
|
2184
|
-
writeFileSync
|
|
1963
|
+
writeFileSync
|
|
2185
1964
|
} from "fs";
|
|
2186
|
-
import { basename, dirname, join
|
|
1965
|
+
import { basename, dirname, join, resolve as resolve2 } from "path";
|
|
2187
1966
|
import { Transform } from "stream";
|
|
2188
1967
|
import { pipeline } from "stream/promises";
|
|
2189
1968
|
var defaults = {
|
|
@@ -2191,8 +1970,8 @@ var defaults = {
|
|
|
2191
1970
|
stdout: process.stdout,
|
|
2192
1971
|
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
2193
1972
|
writeFile: (path, data) => {
|
|
2194
|
-
|
|
2195
|
-
|
|
1973
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1974
|
+
writeFileSync(path, data, { flag: "wx" });
|
|
2196
1975
|
}
|
|
2197
1976
|
};
|
|
2198
1977
|
function exportNotice(environment) {
|
|
@@ -2302,14 +2081,14 @@ function refuseDirectory(path, shown) {
|
|
|
2302
2081
|
}
|
|
2303
2082
|
function makeOutputDirectory(path, shown) {
|
|
2304
2083
|
try {
|
|
2305
|
-
|
|
2084
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2306
2085
|
} catch (error) {
|
|
2307
2086
|
throw writeFailed(shown, error);
|
|
2308
2087
|
}
|
|
2309
2088
|
}
|
|
2310
2089
|
async function downloadToFile(deps, url, path, shown, again) {
|
|
2311
2090
|
const body = await openDownload(deps, url, again);
|
|
2312
|
-
const partial =
|
|
2091
|
+
const partial = join(
|
|
2313
2092
|
dirname(path),
|
|
2314
2093
|
`.${basename(path)}.${String(process.pid)}.part`
|
|
2315
2094
|
);
|
|
@@ -2477,7 +2256,7 @@ async function runDbPull(context, overrides = {}) {
|
|
|
2477
2256
|
`${relativeToCwd(context, dir)} already has ${String(existing.length)} migration file(s). naro db pull only baselines a project without migrations; to see the current schema, run naro db dump --schema-only.`
|
|
2478
2257
|
);
|
|
2479
2258
|
const name = nextMigrationFileName(existing, "remote_schema");
|
|
2480
|
-
const path =
|
|
2259
|
+
const path = join(dir, name);
|
|
2481
2260
|
const shown = relativeToCwd(context, path);
|
|
2482
2261
|
const client = await dataClient(context);
|
|
2483
2262
|
const projectId = await context.resolveProjectId();
|
|
@@ -2559,12 +2338,12 @@ async function readPolicies(client, projectId, warn) {
|
|
|
2559
2338
|
return void 0;
|
|
2560
2339
|
}
|
|
2561
2340
|
const policies = [];
|
|
2562
|
-
const
|
|
2341
|
+
const unreadable2 = [];
|
|
2563
2342
|
for (const row of objectsOf(stored)) {
|
|
2564
2343
|
const policy = loadPolicy(row);
|
|
2565
2344
|
if (policy === void 0) continue;
|
|
2566
2345
|
if (policy.invalid !== null)
|
|
2567
|
-
|
|
2346
|
+
unreadable2.push({
|
|
2568
2347
|
table: policy.table,
|
|
2569
2348
|
name: policy.name,
|
|
2570
2349
|
reason: policy.invalid
|
|
@@ -2585,7 +2364,7 @@ async function readPolicies(client, projectId, warn) {
|
|
|
2585
2364
|
return {
|
|
2586
2365
|
tables: tableNames(snapshot),
|
|
2587
2366
|
policies,
|
|
2588
|
-
unreadable,
|
|
2367
|
+
unreadable: unreadable2,
|
|
2589
2368
|
snapshot,
|
|
2590
2369
|
indexes: indexesFromRows(objectsOf(indexes))
|
|
2591
2370
|
};
|
|
@@ -2662,8 +2441,8 @@ async function runDbLint(context) {
|
|
|
2662
2441
|
}
|
|
2663
2442
|
|
|
2664
2443
|
// src/commands/db-reset.ts
|
|
2665
|
-
import { readFileSync as
|
|
2666
|
-
import { join as
|
|
2444
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2445
|
+
import { join as join2 } from "path";
|
|
2667
2446
|
var MAX_STATEMENTS = 50;
|
|
2668
2447
|
var SCHEMA_FILTER = `${systemTablePredicate("name")} AND ${systemTablePredicate("tbl_name")}`;
|
|
2669
2448
|
var SCHEMA_SQL2 = [
|
|
@@ -2845,7 +2624,7 @@ async function runDbReset(context) {
|
|
|
2845
2624
|
${problemLines.join("\n")}`,
|
|
2846
2625
|
{ details: { problems } }
|
|
2847
2626
|
);
|
|
2848
|
-
const texts = files.map((name) =>
|
|
2627
|
+
const texts = files.map((name) => readFileSync2(join2(dir, name), "utf8"));
|
|
2849
2628
|
const client = await dataClient(context);
|
|
2850
2629
|
const projectId = await context.resolveProjectId();
|
|
2851
2630
|
const bookmark = context.flags.dryRun ? null : await bookmarkNow(context, client, projectId, "resetting");
|
|
@@ -2985,11 +2764,11 @@ ${problemLines.join("\n")}`,
|
|
|
2985
2764
|
}
|
|
2986
2765
|
|
|
2987
2766
|
// src/commands/db-schema.ts
|
|
2988
|
-
import { mkdirSync as
|
|
2989
|
-
import { join as
|
|
2767
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2768
|
+
import { join as join3 } from "path";
|
|
2990
2769
|
|
|
2991
2770
|
// src/commands/policy.ts
|
|
2992
|
-
import { readFileSync as
|
|
2771
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2993
2772
|
import { resolve as resolve3 } from "path";
|
|
2994
2773
|
|
|
2995
2774
|
// ../../packages/naro-sdk/src/query/filters.ts
|
|
@@ -3407,7 +3186,7 @@ function conditionsFrom(flags) {
|
|
|
3407
3186
|
function patchFromFile(context, path, table3) {
|
|
3408
3187
|
let text;
|
|
3409
3188
|
try {
|
|
3410
|
-
text =
|
|
3189
|
+
text = readFileSync3(resolve3(context.cwd, path), "utf8");
|
|
3411
3190
|
} catch {
|
|
3412
3191
|
refuse2(`Could not read ${path}.`);
|
|
3413
3192
|
}
|
|
@@ -3569,12 +3348,12 @@ function conditionText(action, value) {
|
|
|
3569
3348
|
if (isInvalid(value))
|
|
3570
3349
|
return `cannot be read (${value.invalid}); it denies everyone but the service key`;
|
|
3571
3350
|
const using = value.using ? filterText(value.using) : "";
|
|
3572
|
-
const
|
|
3573
|
-
if (using === void 0 ||
|
|
3351
|
+
const check2 = value.check ? filterText(value.check) : "";
|
|
3352
|
+
if (using === void 0 || check2 === void 0)
|
|
3574
3353
|
return `as JSON ${JSON.stringify(value)}`;
|
|
3575
3354
|
const parts = [];
|
|
3576
3355
|
if (using !== "") parts.push(`using ${using}`);
|
|
3577
|
-
if (
|
|
3356
|
+
if (check2 !== "") parts.push(`check ${check2}`);
|
|
3578
3357
|
else if (action === "update") parts.push("check = using");
|
|
3579
3358
|
const text = parts.join("; ");
|
|
3580
3359
|
return value.role === "authenticated" ? `${text} (signed-in users only)` : text;
|
|
@@ -3684,7 +3463,7 @@ function statementText(context) {
|
|
|
3684
3463
|
let text = positional;
|
|
3685
3464
|
if (file !== void 0) {
|
|
3686
3465
|
try {
|
|
3687
|
-
text =
|
|
3466
|
+
text = readFileSync3(resolve3(context.cwd, file), "utf8");
|
|
3688
3467
|
} catch {
|
|
3689
3468
|
refuse2(`Could not read ${file}.`);
|
|
3690
3469
|
}
|
|
@@ -4041,13 +3820,13 @@ async function runSchemaChange(context, operations, question) {
|
|
|
4041
3820
|
let file = null;
|
|
4042
3821
|
let fileWarning;
|
|
4043
3822
|
const relDir = relativeToCwd(context, dir);
|
|
4044
|
-
const rel =
|
|
3823
|
+
const rel = join3(relDir === "." ? "" : relDir, name);
|
|
4045
3824
|
if (context.flags.noWrite) {
|
|
4046
3825
|
fileWarning = `Not written to ${relDir} (--no-write): naro db reset will not re-apply ${result.name}, and naro migration list shows it as remote-only.`;
|
|
4047
3826
|
} else {
|
|
4048
3827
|
try {
|
|
4049
|
-
|
|
4050
|
-
|
|
3828
|
+
mkdirSync2(dir, { recursive: true });
|
|
3829
|
+
writeFileSync2(join3(dir, name), result.sql, { flag: "wx" });
|
|
4051
3830
|
file = rel;
|
|
4052
3831
|
} catch (error) {
|
|
4053
3832
|
const reason = (error instanceof Error ? error.message : String(error)).replace(/[.\s]+$/, "");
|
|
@@ -4176,7 +3955,7 @@ async function runDbIndexDrop(context) {
|
|
|
4176
3955
|
var defaults2 = {
|
|
4177
3956
|
detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
|
|
4178
3957
|
openBrowser,
|
|
4179
|
-
sleep: (ms) => new Promise((
|
|
3958
|
+
sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms))
|
|
4180
3959
|
};
|
|
4181
3960
|
function connected(context, project) {
|
|
4182
3961
|
const branch = project.defaultBranch;
|
|
@@ -4707,7 +4486,7 @@ async function runInspect(context) {
|
|
|
4707
4486
|
|
|
4708
4487
|
// src/commands/domains.ts
|
|
4709
4488
|
var defaults3 = {
|
|
4710
|
-
sleep: (ms) => new Promise((
|
|
4489
|
+
sleep: (ms) => new Promise((resolve9) => setTimeout(resolve9, ms)),
|
|
4711
4490
|
now: () => Date.now()
|
|
4712
4491
|
};
|
|
4713
4492
|
var FIRST_DELAY_MS = 5e3;
|
|
@@ -4925,7 +4704,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4925
4704
|
]);
|
|
4926
4705
|
return;
|
|
4927
4706
|
}
|
|
4928
|
-
let
|
|
4707
|
+
let timedOut2 = false;
|
|
4929
4708
|
let abandoned = false;
|
|
4930
4709
|
if (wait && !domains.every(isReady)) {
|
|
4931
4710
|
const deadline = deps.now() + WAIT_TIMEOUT_MS;
|
|
@@ -4938,7 +4717,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4938
4717
|
}
|
|
4939
4718
|
const remaining = deadline - deps.now();
|
|
4940
4719
|
if (remaining <= 0) {
|
|
4941
|
-
|
|
4720
|
+
timedOut2 = true;
|
|
4942
4721
|
break;
|
|
4943
4722
|
}
|
|
4944
4723
|
for (const domain of pending)
|
|
@@ -4958,7 +4737,7 @@ async function runDomainsVerify(context, overrides = {}) {
|
|
|
4958
4737
|
const notReady = domains.filter((domain) => !isReady(domain));
|
|
4959
4738
|
const names = notReady.map((domain) => domain.hostname).join(", ");
|
|
4960
4739
|
const details = { projectId, domains, ready: false };
|
|
4961
|
-
if (
|
|
4740
|
+
if (timedOut2) {
|
|
4962
4741
|
const cause2 = notReady[0]?.cause;
|
|
4963
4742
|
throw new NaroError(
|
|
4964
4743
|
"TIMEOUT",
|
|
@@ -5005,7 +4784,7 @@ async function runDomainsRemove(context) {
|
|
|
5005
4784
|
}
|
|
5006
4785
|
|
|
5007
4786
|
// src/commands/env.ts
|
|
5008
|
-
import { chmodSync, existsSync, readFileSync as
|
|
4787
|
+
import { chmodSync, existsSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
5009
4788
|
import { resolve as resolve4 } from "path";
|
|
5010
4789
|
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5011
4790
|
var KEY_MAX_LENGTH = 256;
|
|
@@ -5120,7 +4899,7 @@ async function runEnvImport(context) {
|
|
|
5120
4899
|
const target = parseTarget(targetArg);
|
|
5121
4900
|
let text;
|
|
5122
4901
|
try {
|
|
5123
|
-
text =
|
|
4902
|
+
text = readFileSync4(resolve4(context.cwd, file), "utf8");
|
|
5124
4903
|
} catch {
|
|
5125
4904
|
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
5126
4905
|
}
|
|
@@ -5211,7 +4990,7 @@ ${serializeDotenv(vars)}`;
|
|
|
5211
4990
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
5212
4991
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
5213
4992
|
}
|
|
5214
|
-
|
|
4993
|
+
writeFileSync3(path, text, { mode: 384 });
|
|
5215
4994
|
chmodSync(path, 384);
|
|
5216
4995
|
context.output.result({ projectId, file, environment: surface, count: count2 }, [
|
|
5217
4996
|
`\u2705 Pulled ${String(count2)} variable(s) into ${file}`
|
|
@@ -5370,131 +5149,100 @@ async function runEnvironmentsProvision(context) {
|
|
|
5370
5149
|
]);
|
|
5371
5150
|
}
|
|
5372
5151
|
|
|
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;
|
|
5152
|
+
// src/commands/functions.ts
|
|
5153
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5154
|
+
import { join as join5 } from "path";
|
|
5155
|
+
|
|
5156
|
+
// src/functions/scaffold.ts
|
|
5157
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
5158
|
+
import { createRequire } from "module";
|
|
5159
|
+
import { dirname as dirname2, join as join4, relative, resolve as resolve5, sep } from "path";
|
|
5160
|
+
var NARO_JS_FUNCTIONS_MISSING = 'Cannot find "naro-js/functions". Install naro-js 0.7.0 or later: npm install naro-js@latest.';
|
|
5161
|
+
function scaffoldSource(name) {
|
|
5162
|
+
return `import { withNaro } from "naro-js/functions";
|
|
5163
|
+
|
|
5164
|
+
// Called at https://<your-app>.naro.sh/_naro/v1/functions/${name}.
|
|
5165
|
+
// \`naro\` acts as the caller, so the tables' row-level security applies;
|
|
5166
|
+
// \`naroAdmin\` uses the service key.
|
|
5167
|
+
Deno.serve(
|
|
5168
|
+
withNaro(async (req, { user }) => {
|
|
5169
|
+
const { name } = await req.json().catch(() => ({ name: "world" }));
|
|
5170
|
+
return Response.json({ message: \`Hello \${name}!\`, user: user?.id ?? null });
|
|
5171
|
+
}),
|
|
5172
|
+
);
|
|
5173
|
+
`;
|
|
5174
|
+
}
|
|
5175
|
+
function naroJsInstalled(root) {
|
|
5176
|
+
for (let dir = resolve5(root); ; dir = dirname2(dir)) {
|
|
5177
|
+
if (existsSync2(join4(dir, "node_modules", "naro-js", "package.json")))
|
|
5178
|
+
return true;
|
|
5179
|
+
if (dirname2(dir) === dir) return false;
|
|
5420
5180
|
}
|
|
5421
|
-
context.output.result({ projectId, types, ...extra }, [
|
|
5422
|
-
types.replace(/\n$/, "")
|
|
5423
|
-
]);
|
|
5424
5181
|
}
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
...Object.keys(filters).length > 0 ? { filters } : {}
|
|
5434
|
-
};
|
|
5182
|
+
function naroJsFunctionsResolves(root) {
|
|
5183
|
+
if (!naroJsInstalled(root)) return false;
|
|
5184
|
+
try {
|
|
5185
|
+
createRequire(join4(root, "package.json")).resolve("naro-js/functions");
|
|
5186
|
+
return true;
|
|
5187
|
+
} catch {
|
|
5188
|
+
return false;
|
|
5189
|
+
}
|
|
5435
5190
|
}
|
|
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;
|
|
5191
|
+
var COVERS = /^(\.\/)?(\*|\*\*|naro)(\/|$)|^\.\/?$/;
|
|
5192
|
+
var LEAVES_OUT = /^(\.\/)?naro(\/functions)?\/?(\*\*)?$/;
|
|
5193
|
+
function tsconfigCovers(root, functionsLabel) {
|
|
5194
|
+
const file = join4(root, "tsconfig.json");
|
|
5195
|
+
if (!existsSync2(file)) return false;
|
|
5196
|
+
let config;
|
|
5197
|
+
try {
|
|
5198
|
+
config = parseJsonc(readFileSync5(file, "utf8"));
|
|
5199
|
+
} catch {
|
|
5200
|
+
return false;
|
|
5456
5201
|
}
|
|
5457
|
-
const
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
)
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5202
|
+
const include = Array.isArray(config.include) ? config.include.filter((p) => typeof p === "string") : null;
|
|
5203
|
+
const exclude = Array.isArray(config.exclude) ? config.exclude.filter((p) => typeof p === "string") : [];
|
|
5204
|
+
const covered = include === null || include.some((pattern) => COVERS.test(pattern));
|
|
5205
|
+
const leftOut = exclude.some(
|
|
5206
|
+
(pattern) => LEAVES_OUT.test(pattern) || pattern === functionsLabel
|
|
5207
|
+
);
|
|
5208
|
+
return covered && !leftOut;
|
|
5209
|
+
}
|
|
5210
|
+
function gitignoreCovers(root, envPath) {
|
|
5211
|
+
const file = join4(root, ".gitignore");
|
|
5212
|
+
if (!existsSync2(file)) return false;
|
|
5213
|
+
const accepted = /* @__PURE__ */ new Set([
|
|
5214
|
+
envPath,
|
|
5215
|
+
`/${envPath}`,
|
|
5216
|
+
".env",
|
|
5217
|
+
"/.env",
|
|
5218
|
+
"**/.env",
|
|
5219
|
+
".env*",
|
|
5220
|
+
"*.env"
|
|
5221
|
+
]);
|
|
5222
|
+
return readFileSync5(file, "utf8").split(/\r?\n/).some((line) => accepted.has(line.trim()));
|
|
5466
5223
|
}
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5224
|
+
function scaffoldNotes(folder) {
|
|
5225
|
+
const notes = [];
|
|
5226
|
+
if (!naroJsFunctionsResolves(folder.root))
|
|
5227
|
+
notes.push(`\u26A0 ${NARO_JS_FUNCTIONS_MISSING}`);
|
|
5228
|
+
const functionsLabel = relative(folder.root, folder.dir).split(sep).join("/");
|
|
5229
|
+
if (!folder.explicit && tsconfigCovers(folder.root, functionsLabel))
|
|
5230
|
+
notes.push(
|
|
5231
|
+
`\u26A0 tsconfig.json type-checks naro/functions too. If your app's type check trips on the functions' Deno code, add "exclude": ["naro/functions"] to it.`
|
|
5472
5232
|
);
|
|
5473
|
-
const
|
|
5474
|
-
if (
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
`Filters only narrow a report; drop --${filter}.`
|
|
5233
|
+
const envPath = `${functionsLabel}/.env`;
|
|
5234
|
+
if (!gitignoreCovers(folder.root, envPath))
|
|
5235
|
+
notes.push(
|
|
5236
|
+
`\u26A0 ${envPath} is not in .gitignore. Add it before you keep local secrets there.`
|
|
5478
5237
|
);
|
|
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);
|
|
5238
|
+
return notes;
|
|
5491
5239
|
}
|
|
5492
5240
|
|
|
5493
5241
|
// src/commands/logs.ts
|
|
5494
5242
|
var FOLLOW_INTERVAL_MS = 3e3;
|
|
5495
5243
|
var FOLLOW_TIMEOUT_MS = 30 * 6e4;
|
|
5496
5244
|
function sleep(ms) {
|
|
5497
|
-
return new Promise((
|
|
5245
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
5498
5246
|
}
|
|
5499
5247
|
function formatBuildLog(log) {
|
|
5500
5248
|
return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
|
|
@@ -5504,6 +5252,27 @@ function formatRuntimeLog(entry) {
|
|
|
5504
5252
|
const request = entry.method && entry.path ? ` ${entry.method} ${entry.path}${entry.status ? ` ${String(entry.status)}` : ""}` : "";
|
|
5505
5253
|
return `${time} [${entry.level}]${request} ${entry.message}`.trimEnd();
|
|
5506
5254
|
}
|
|
5255
|
+
var FOLLOW_JSON_REFUSAL = "--follow streams runtime logs until you stop it, so it has no JSON result. Drop --json to stream, or drop --follow for the current entries as JSON.";
|
|
5256
|
+
async function followRuntimeEntries(context, read, format, wait = sleep) {
|
|
5257
|
+
let lastTs = -1;
|
|
5258
|
+
let boundary = /* @__PURE__ */ new Set();
|
|
5259
|
+
for (; ; ) {
|
|
5260
|
+
const entries = await read();
|
|
5261
|
+
for (const entry of entries) {
|
|
5262
|
+
if (entry.ts < lastTs) continue;
|
|
5263
|
+
const key = JSON.stringify(entry);
|
|
5264
|
+
if (entry.ts === lastTs) {
|
|
5265
|
+
if (boundary.has(key)) continue;
|
|
5266
|
+
} else {
|
|
5267
|
+
lastTs = entry.ts;
|
|
5268
|
+
boundary = /* @__PURE__ */ new Set();
|
|
5269
|
+
}
|
|
5270
|
+
boundary.add(key);
|
|
5271
|
+
context.output.log(format(entry));
|
|
5272
|
+
}
|
|
5273
|
+
await wait(FOLLOW_INTERVAL_MS);
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5507
5276
|
async function latestDeploymentId(client, projectId) {
|
|
5508
5277
|
const [latest] = await client.deployments.list(projectId);
|
|
5509
5278
|
if (!latest)
|
|
@@ -5562,63 +5331,651 @@ async function runtimeLogs(context, client, projectId) {
|
|
|
5562
5331
|
);
|
|
5563
5332
|
return;
|
|
5564
5333
|
}
|
|
5565
|
-
context.output.log(
|
|
5566
|
-
`Following ${environment} runtime logs (Ctrl-C to stop)...`
|
|
5334
|
+
context.output.log(
|
|
5335
|
+
`Following ${environment} runtime logs (Ctrl-C to stop)...`
|
|
5336
|
+
);
|
|
5337
|
+
await followRuntimeEntries(
|
|
5338
|
+
context,
|
|
5339
|
+
() => client.logs.runtime(projectId, options),
|
|
5340
|
+
formatRuntimeLog
|
|
5341
|
+
);
|
|
5342
|
+
}
|
|
5343
|
+
async function runLogs(context) {
|
|
5344
|
+
const [deploymentId] = context.positionals;
|
|
5345
|
+
const type = context.flags.type ?? (deploymentId !== void 0 ? "build" : "runtime");
|
|
5346
|
+
if (type !== "build" && type !== "runtime")
|
|
5347
|
+
throw new NaroError(
|
|
5348
|
+
"VALIDATION_FAILED",
|
|
5349
|
+
"--type must be build or runtime."
|
|
5350
|
+
);
|
|
5351
|
+
if (type === "runtime" && context.flags.follow && context.flags.json)
|
|
5352
|
+
throw new NaroError("VALIDATION_FAILED", FOLLOW_JSON_REFUSAL);
|
|
5353
|
+
if (type === "build" && context.flags.env)
|
|
5354
|
+
throw new NaroError(
|
|
5355
|
+
"VALIDATION_FAILED",
|
|
5356
|
+
"--env is for runtime logs. Build logs belong to one deployment: pass its id (naro deployments lists them)."
|
|
5357
|
+
);
|
|
5358
|
+
const client = await context.client();
|
|
5359
|
+
if (type === "runtime") {
|
|
5360
|
+
if (deploymentId)
|
|
5361
|
+
throw new NaroError(
|
|
5362
|
+
"VALIDATION_FAILED",
|
|
5363
|
+
"Runtime logs are per project, not per deployment. Drop the deployment id or use --type build."
|
|
5364
|
+
);
|
|
5365
|
+
const projectId = await context.resolveProjectId();
|
|
5366
|
+
await runtimeLogs(context, client, projectId);
|
|
5367
|
+
return;
|
|
5368
|
+
}
|
|
5369
|
+
let target = deploymentId;
|
|
5370
|
+
if (target === void 0) {
|
|
5371
|
+
target = await latestDeploymentId(client, await context.resolveProjectId());
|
|
5372
|
+
context.output.log(`Build logs of the latest deployment, ${target}`);
|
|
5373
|
+
}
|
|
5374
|
+
await buildLogs(context, client, target);
|
|
5375
|
+
}
|
|
5376
|
+
|
|
5377
|
+
// src/commands/functions.ts
|
|
5378
|
+
var FUNCTIONS_ENV_REFUSAL = "naro functions works on the production backend only. Drop --env.";
|
|
5379
|
+
var VERSIONS_USAGE = "Usage: naro functions versions <name>";
|
|
5380
|
+
var LOGS_USAGE = "Usage: naro functions logs <name> [--level <l>] [--follow]";
|
|
5381
|
+
function functionName(context, usage) {
|
|
5382
|
+
const [name] = context.positionals;
|
|
5383
|
+
if (name === void 0 || name === "")
|
|
5384
|
+
throw new NaroError("VALIDATION_FAILED", usage);
|
|
5385
|
+
return name;
|
|
5386
|
+
}
|
|
5387
|
+
function statusWord(fn) {
|
|
5388
|
+
if (fn.status !== "error") return fn.status;
|
|
5389
|
+
return `error at ${fn.step ?? "upload"}: ${printable(fn.error ?? "unknown error")}`;
|
|
5390
|
+
}
|
|
5391
|
+
var cpuHours = (ms) => (ms / 36e5).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
5392
|
+
function functionLines(list) {
|
|
5393
|
+
if (list.functions.length === 0)
|
|
5394
|
+
return [
|
|
5395
|
+
"No functions yet. Create one with naro functions new <name>, then naro functions deploy <name>."
|
|
5396
|
+
];
|
|
5397
|
+
const width = Math.max(...list.functions.map((fn) => fn.name.length));
|
|
5398
|
+
const versionWidth = Math.max(
|
|
5399
|
+
...list.functions.map(
|
|
5400
|
+
(fn) => fn.version === null ? 1 : String(fn.version).length + 1
|
|
5401
|
+
)
|
|
5402
|
+
);
|
|
5403
|
+
const lines = list.functions.map((fn) => {
|
|
5404
|
+
const version2 = fn.version === null ? "\u2014" : `v${String(fn.version)}`;
|
|
5405
|
+
const verify = fn.verifyJwt ? "verify on " : "verify off";
|
|
5406
|
+
const stale = fn.stale ? " (stale: naro functions secrets sync brings it up)" : "";
|
|
5407
|
+
return `${fn.name.padEnd(width)} ${version2.padEnd(versionWidth)} ${verify} ${statusWord(fn)} ${fn.url}${stale}`;
|
|
5408
|
+
});
|
|
5409
|
+
lines.push(
|
|
5410
|
+
`Up to ${list.limits.functions.toLocaleString("en-US")} functions per ${list.limits.functionsScope} on this plan.`
|
|
5411
|
+
);
|
|
5412
|
+
const usage = list.usage;
|
|
5413
|
+
if (usage !== null) {
|
|
5414
|
+
const percent2 = usage.cpuMsLimit > 0 ? Math.floor(usage.cpuMs / usage.cpuMsLimit * 100) : 0;
|
|
5415
|
+
lines.push(
|
|
5416
|
+
`Function CPU this month (UTC, the team's): ${cpuHours(usage.cpuMs)} of ${cpuHours(usage.cpuMsLimit)} CPU-hours (${String(percent2)}%).`
|
|
5417
|
+
);
|
|
5418
|
+
if (usage.blocked)
|
|
5419
|
+
lines.push(
|
|
5420
|
+
"\u26A0 This team has used its monthly function CPU time. Functions answer 429 FUNCTIONS_QUOTA_EXCEEDED until the 1st at 00:00 UTC."
|
|
5421
|
+
);
|
|
5422
|
+
else if (usage.cpuMs >= Math.ceil(usage.cpuMsLimit * 0.8))
|
|
5423
|
+
lines.push(
|
|
5424
|
+
`\u26A0 This team has used ${String(percent2)}% of its monthly function CPU time.`
|
|
5425
|
+
);
|
|
5426
|
+
}
|
|
5427
|
+
return lines;
|
|
5428
|
+
}
|
|
5429
|
+
var kib = (bytes) => `${(bytes / 1024).toFixed(1)} KB`;
|
|
5430
|
+
function versionLines(detail) {
|
|
5431
|
+
const statusWidth = Math.max(...detail.versions.map((v) => v.status.length));
|
|
5432
|
+
return detail.versions.map(
|
|
5433
|
+
(v) => `v${String(v.number)} ${v.status.padEnd(statusWidth)} ${v.active ? "live" : " "} ${kib(v.size)} ${v.verifyJwt ? "verify on " : "verify off"} ${v.createdAt}`
|
|
5434
|
+
);
|
|
5435
|
+
}
|
|
5436
|
+
async function runFunctionsList(context) {
|
|
5437
|
+
const client = await context.client();
|
|
5438
|
+
const projectId = await context.resolveProjectId();
|
|
5439
|
+
const list = await client.functions.list(projectId);
|
|
5440
|
+
context.output.result({ projectId, ...list }, functionLines(list));
|
|
5441
|
+
}
|
|
5442
|
+
async function runFunctionsVersions(context) {
|
|
5443
|
+
const name = functionName(context, VERSIONS_USAGE);
|
|
5444
|
+
const client = await context.client();
|
|
5445
|
+
const projectId = await context.resolveProjectId();
|
|
5446
|
+
const detail = await client.functions.get(projectId, name);
|
|
5447
|
+
context.output.result({ projectId, ...detail }, versionLines(detail));
|
|
5448
|
+
}
|
|
5449
|
+
async function runFunctionsLogs(context) {
|
|
5450
|
+
if (context.flags.follow && context.flags.json)
|
|
5451
|
+
throw new NaroError("VALIDATION_FAILED", FOLLOW_JSON_REFUSAL);
|
|
5452
|
+
const name = functionName(context, LOGS_USAGE);
|
|
5453
|
+
const client = await context.client();
|
|
5454
|
+
const projectId = await context.resolveProjectId();
|
|
5455
|
+
const options = context.flags.level ? { level: context.flags.level } : {};
|
|
5456
|
+
const format = (entry) => printable(formatRuntimeLog(entry));
|
|
5457
|
+
if (!context.flags.follow) {
|
|
5458
|
+
const entries = await client.functions.logs(projectId, name, options);
|
|
5459
|
+
context.output.result(
|
|
5460
|
+
{ projectId, name, entries },
|
|
5461
|
+
entries.length > 0 ? entries.map(format) : [`(no logs from ${name} yet)`]
|
|
5462
|
+
);
|
|
5463
|
+
return;
|
|
5464
|
+
}
|
|
5465
|
+
context.output.log(`Following ${name}'s logs (Ctrl-C to stop)...`);
|
|
5466
|
+
await followRuntimeEntries(
|
|
5467
|
+
context,
|
|
5468
|
+
() => client.functions.logs(projectId, name, options),
|
|
5469
|
+
format
|
|
5470
|
+
);
|
|
5471
|
+
}
|
|
5472
|
+
var NEW_USAGE = "Usage: naro functions new <name>";
|
|
5473
|
+
var ROLLBACK_USAGE = "Usage: naro functions rollback <name> [<version>]";
|
|
5474
|
+
var DELETE_USAGE2 = "Usage: naro functions delete <name>";
|
|
5475
|
+
async function runFunctionsNew(context) {
|
|
5476
|
+
const name = functionName(context, NEW_USAGE);
|
|
5477
|
+
if (!isFunctionName(name))
|
|
5478
|
+
throw new NaroError("VALIDATION_FAILED", BUNDLE_MESSAGES.badName(name));
|
|
5479
|
+
const folder = functionsFolder({
|
|
5480
|
+
cwd: context.cwd,
|
|
5481
|
+
root: context.flags.root,
|
|
5482
|
+
dir: context.flags.dir
|
|
5483
|
+
});
|
|
5484
|
+
const target = join5(folder.dir, name);
|
|
5485
|
+
mkdirSync3(folder.dir, { recursive: true });
|
|
5486
|
+
try {
|
|
5487
|
+
mkdirSync3(target);
|
|
5488
|
+
} catch (error) {
|
|
5489
|
+
if (error.code !== "EEXIST") throw error;
|
|
5490
|
+
throw new NaroError(
|
|
5491
|
+
"VALIDATION_FAILED",
|
|
5492
|
+
`${folder.label}/${name} already exists.`
|
|
5493
|
+
);
|
|
5494
|
+
}
|
|
5495
|
+
writeFileSync4(join5(target, "index.ts"), scaffoldSource(name));
|
|
5496
|
+
const file = `${folder.label}/${name}/index.ts`;
|
|
5497
|
+
const notes = scaffoldNotes(folder);
|
|
5498
|
+
context.output.result({ file, notes }, [
|
|
5499
|
+
`\u2705 Created ${file}. Deploy it with naro functions deploy ${name}.`,
|
|
5500
|
+
...notes
|
|
5501
|
+
]);
|
|
5502
|
+
}
|
|
5503
|
+
async function runFunctionsDeploy(context) {
|
|
5504
|
+
assertFunctionsNode();
|
|
5505
|
+
const client = await context.client();
|
|
5506
|
+
const projectId = await context.resolveProjectId();
|
|
5507
|
+
const { deployed } = await deployFunctions({
|
|
5508
|
+
client,
|
|
5509
|
+
projectId,
|
|
5510
|
+
cwd: context.cwd,
|
|
5511
|
+
root: context.flags.root,
|
|
5512
|
+
dir: context.flags.dir,
|
|
5513
|
+
names: context.positionals,
|
|
5514
|
+
verifyJwt: context.flags.noVerifyJwt ? false : void 0,
|
|
5515
|
+
importMap: context.flags.importMap,
|
|
5516
|
+
skipCheck: context.flags.skipCheck,
|
|
5517
|
+
cliVersion: version(),
|
|
5518
|
+
log: (line) => context.output.log(line)
|
|
5519
|
+
});
|
|
5520
|
+
context.output.result({ projectId, functions: deployed });
|
|
5521
|
+
}
|
|
5522
|
+
async function runFunctionsRollback(context) {
|
|
5523
|
+
const name = functionName(context, ROLLBACK_USAGE);
|
|
5524
|
+
const written = context.positionals[1];
|
|
5525
|
+
if (written !== void 0 && !/^[1-9]\d*$/.test(written))
|
|
5526
|
+
throw new NaroError(
|
|
5527
|
+
"VALIDATION_FAILED",
|
|
5528
|
+
"The version must be a whole number from 1, like 3."
|
|
5529
|
+
);
|
|
5530
|
+
const client = await context.client();
|
|
5531
|
+
const projectId = await context.resolveProjectId();
|
|
5532
|
+
const io = {
|
|
5533
|
+
sleep: defaultSleep,
|
|
5534
|
+
log: (line) => context.output.log(line)
|
|
5535
|
+
};
|
|
5536
|
+
const live2 = await withFunctionRetries(
|
|
5537
|
+
() => client.functions.rollback(
|
|
5538
|
+
projectId,
|
|
5539
|
+
name,
|
|
5540
|
+
written === void 0 ? void 0 : Number(written)
|
|
5541
|
+
),
|
|
5542
|
+
name,
|
|
5543
|
+
io
|
|
5544
|
+
);
|
|
5545
|
+
context.output.result({ projectId, function: live2 }, [
|
|
5546
|
+
`\u2705 Rolled ${name} back to v${String(live2.version)} \u2014 ${live2.url}`
|
|
5547
|
+
]);
|
|
5548
|
+
}
|
|
5549
|
+
async function runFunctionsDelete(context) {
|
|
5550
|
+
const name = functionName(context, DELETE_USAGE2);
|
|
5551
|
+
const client = await context.client();
|
|
5552
|
+
const projectId = await context.resolveProjectId();
|
|
5553
|
+
const detail = await client.functions.get(projectId, name);
|
|
5554
|
+
const stored = detail.versions.filter((v) => v.status !== "pruned").length;
|
|
5555
|
+
await confirmOrAbort(
|
|
5556
|
+
context,
|
|
5557
|
+
stored === 1 ? `Delete function ${name}? Its URL stops working and its 1 stored version is removed.` : `Delete function ${name}? Its URL stops working and its ${String(stored)} stored versions are removed.`
|
|
5558
|
+
);
|
|
5559
|
+
const io = {
|
|
5560
|
+
sleep: defaultSleep,
|
|
5561
|
+
log: (line) => context.output.log(line)
|
|
5562
|
+
};
|
|
5563
|
+
await withFunctionRetries(
|
|
5564
|
+
() => client.functions.remove(projectId, name),
|
|
5565
|
+
name,
|
|
5566
|
+
io
|
|
5567
|
+
);
|
|
5568
|
+
context.output.result({ projectId, name, deleted: true }, [
|
|
5569
|
+
`\u2705 Deleted ${name}. Its URL stops working within seconds.`
|
|
5570
|
+
]);
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
// src/commands/functions-secrets.ts
|
|
5574
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5575
|
+
import { resolve as resolve6 } from "path";
|
|
5576
|
+
var SET_USAGE3 = "Usage: naro functions secrets set <NAME>";
|
|
5577
|
+
var IMPORT_USAGE = "Usage: naro functions secrets import <file>";
|
|
5578
|
+
var UNSET_USAGE = "Usage: naro functions secrets unset <NAME>\u2026";
|
|
5579
|
+
var NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5580
|
+
var NAME_MAX = 256;
|
|
5581
|
+
var VALUE_MAX_BYTES2 = 5120;
|
|
5582
|
+
var RESERVED = /* @__PURE__ */ new Set(["ASSETS", "DB"]);
|
|
5583
|
+
var PROTO_LINE = /^\s*(?:export\s+)?__proto__(?:\s*=|:\s)/m;
|
|
5584
|
+
function secretNameProblem(name) {
|
|
5585
|
+
if (!NAME.test(name) || name.length > NAME_MAX)
|
|
5586
|
+
return "Secret names must look like NAME: letters, digits and _, not starting with a digit.";
|
|
5587
|
+
if (name === "__proto__") return "__proto__ cannot be a secret name.";
|
|
5588
|
+
if (name.startsWith("NARO_") || RESERVED.has(name))
|
|
5589
|
+
return `${name} is reserved for Naro. Secret names cannot start with NARO_ or be ASSETS or DB.`;
|
|
5590
|
+
return null;
|
|
5591
|
+
}
|
|
5592
|
+
function secretValueProblem(name, value) {
|
|
5593
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
5594
|
+
return bytes > VALUE_MAX_BYTES2 ? `The value of ${name} is ${bytes.toLocaleString("en-US")} bytes; secrets must be 5,120 bytes or less.` : null;
|
|
5595
|
+
}
|
|
5596
|
+
function check(problem) {
|
|
5597
|
+
if (problem !== null) throw new NaroError("VALIDATION_FAILED", problem);
|
|
5598
|
+
}
|
|
5599
|
+
var plural = (n, one, many) => n === 1 ? one : many;
|
|
5600
|
+
function byteCount(value) {
|
|
5601
|
+
const n = Buffer.byteLength(value, "utf8");
|
|
5602
|
+
return `${n.toLocaleString("en-US")} ${plural(n, "byte", "bytes")}`;
|
|
5603
|
+
}
|
|
5604
|
+
function warnFailures(context, sync) {
|
|
5605
|
+
for (const failure2 of sync.failures)
|
|
5606
|
+
context.output.warn(
|
|
5607
|
+
` ${printable(failure2.name)}: ${printable(failure2.message)}`
|
|
5608
|
+
);
|
|
5609
|
+
}
|
|
5610
|
+
var nameList = (list) => list.map(printable).join(", ");
|
|
5611
|
+
async function applyChange(context, client, projectId, pending) {
|
|
5612
|
+
if (pending === 0)
|
|
5613
|
+
return { lines: ["No live functions to update."], sync: null };
|
|
5614
|
+
if (context.flags.noSync)
|
|
5615
|
+
return {
|
|
5616
|
+
lines: [
|
|
5617
|
+
pending === null ? "The functions carry the change once synced: the sweep does it within minutes, or run naro functions secrets sync." : (
|
|
5618
|
+
// The save counted before the control plane's own hook brought
|
|
5619
|
+
// some up: "up to", not how many are left.
|
|
5620
|
+
`Up to ${functionCount(pending)} still ${plural(pending, "runs", "run")} the old values: the sweep syncs them within minutes, or run naro functions secrets sync.`
|
|
5621
|
+
)
|
|
5622
|
+
],
|
|
5623
|
+
sync: null
|
|
5624
|
+
};
|
|
5625
|
+
if (pending === null)
|
|
5626
|
+
context.output.log(
|
|
5627
|
+
"\u26A0 Naro could not count the functions to update; syncing until none is left."
|
|
5628
|
+
);
|
|
5629
|
+
const total = pending;
|
|
5630
|
+
let last = null;
|
|
5631
|
+
let sync;
|
|
5632
|
+
try {
|
|
5633
|
+
sync = await syncUntilDone(client, projectId, {
|
|
5634
|
+
log: (line) => context.output.log(line),
|
|
5635
|
+
onCall: (progress) => {
|
|
5636
|
+
last = progress;
|
|
5637
|
+
if (total !== null)
|
|
5638
|
+
context.output.log(
|
|
5639
|
+
`\u23F3 Applying secrets to ${functionCount(total)}\u2026 ${String(Math.min(progress.synced.length, total))}/${String(total)}`
|
|
5640
|
+
);
|
|
5641
|
+
}
|
|
5642
|
+
});
|
|
5643
|
+
} catch (error) {
|
|
5644
|
+
if (last !== null) warnFailures(context, last);
|
|
5645
|
+
const cause = toNaroError(error);
|
|
5646
|
+
const retryAfter = cause.details?.retryAfter;
|
|
5647
|
+
throw new NaroError(
|
|
5648
|
+
"FUNCTIONS_SYNC_INCOMPLETE",
|
|
5649
|
+
`Secrets are saved, but the sync stopped: ${printable(cause.message)} Run \`naro functions secrets sync\` to finish.`,
|
|
5650
|
+
{
|
|
5651
|
+
details: {
|
|
5652
|
+
saved: true,
|
|
5653
|
+
cause: {
|
|
5654
|
+
code: cause.code,
|
|
5655
|
+
message: cause.message,
|
|
5656
|
+
...typeof retryAfter === "number" ? { retryAfter } : {}
|
|
5657
|
+
},
|
|
5658
|
+
pending
|
|
5659
|
+
},
|
|
5660
|
+
cause: error
|
|
5661
|
+
}
|
|
5662
|
+
);
|
|
5663
|
+
}
|
|
5664
|
+
if (!sync.complete) {
|
|
5665
|
+
warnFailures(context, sync);
|
|
5666
|
+
throw new NaroError(
|
|
5667
|
+
"FUNCTIONS_SYNC_INCOMPLETE",
|
|
5668
|
+
`Secrets are saved, but ${functionCount(sync.pending)} still ${plural(sync.pending, "runs", "run")} the old values. Run \`naro functions secrets sync\` to finish.`,
|
|
5669
|
+
{
|
|
5670
|
+
details: {
|
|
5671
|
+
saved: true,
|
|
5672
|
+
pending: sync.pending,
|
|
5673
|
+
failures: sync.failures,
|
|
5674
|
+
synced: sync.synced
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
);
|
|
5678
|
+
}
|
|
5679
|
+
const live2 = Math.max(total ?? 0, sync.synced.length);
|
|
5680
|
+
return {
|
|
5681
|
+
lines: [
|
|
5682
|
+
live2 === 0 ? "\u2705 All functions are up to date." : `\u2705 Secrets are live on all ${functionCount(live2)}.`
|
|
5683
|
+
],
|
|
5684
|
+
sync
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
5687
|
+
async function readyToChange(context) {
|
|
5688
|
+
const projectId = await context.resolveProjectId();
|
|
5689
|
+
const client = await context.client();
|
|
5690
|
+
await client.functions.list(projectId);
|
|
5691
|
+
return { client, projectId };
|
|
5692
|
+
}
|
|
5693
|
+
async function runFunctionsSecretsList(context) {
|
|
5694
|
+
const client = await context.client();
|
|
5695
|
+
const projectId = await context.resolveProjectId();
|
|
5696
|
+
const secrets = await client.functions.secrets(projectId);
|
|
5697
|
+
const width = Math.max(0, ...secrets.map((s) => s.name.length));
|
|
5698
|
+
context.output.result(
|
|
5699
|
+
{ projectId, secrets },
|
|
5700
|
+
secrets.length === 0 ? ["No function secrets. Set one with naro functions secrets set <NAME>."] : secrets.map(
|
|
5701
|
+
(s) => `${s.name.padEnd(width)} ${s.digest} ${s.updatedAt}`
|
|
5702
|
+
)
|
|
5703
|
+
);
|
|
5704
|
+
}
|
|
5705
|
+
async function runFunctionsSecretsSet(context) {
|
|
5706
|
+
const [name] = context.positionals;
|
|
5707
|
+
if (name === void 0) throw new NaroError("VALIDATION_FAILED", SET_USAGE3);
|
|
5708
|
+
if (context.positionals.length > 1 || name.includes("="))
|
|
5709
|
+
throw new NaroError("VALIDATION_FAILED", SECRETS_ARGV_REFUSAL);
|
|
5710
|
+
check(secretNameProblem(name));
|
|
5711
|
+
const typed = process.stdin.isTTY === true;
|
|
5712
|
+
let value;
|
|
5713
|
+
let client = null;
|
|
5714
|
+
let projectId = null;
|
|
5715
|
+
if (typed) {
|
|
5716
|
+
projectId = await context.resolveProjectId();
|
|
5717
|
+
client = await context.client();
|
|
5718
|
+
await client.functions.list(projectId);
|
|
5719
|
+
value = await context.prompter.askHidden(`? What's the value of ${name}? `);
|
|
5720
|
+
} else {
|
|
5721
|
+
value = await readStdin();
|
|
5722
|
+
}
|
|
5723
|
+
if (value.length === 0)
|
|
5724
|
+
throw new NaroError("VALIDATION_FAILED", `No value provided for ${name}.`);
|
|
5725
|
+
check(secretValueProblem(name, value));
|
|
5726
|
+
projectId ??= await context.resolveProjectId();
|
|
5727
|
+
client ??= await context.client();
|
|
5728
|
+
const saved = await client.functions.setSecrets(projectId, {
|
|
5729
|
+
set: { [name]: value }
|
|
5730
|
+
});
|
|
5731
|
+
context.output.log(`\u2705 Saved ${name}.`);
|
|
5732
|
+
const { lines, sync } = await applyChange(
|
|
5733
|
+
context,
|
|
5734
|
+
client,
|
|
5735
|
+
projectId,
|
|
5736
|
+
saved.pending
|
|
5737
|
+
);
|
|
5738
|
+
context.output.result(
|
|
5739
|
+
{ projectId, secrets: saved.secrets, pending: saved.pending, sync },
|
|
5740
|
+
lines
|
|
5741
|
+
);
|
|
5742
|
+
}
|
|
5743
|
+
async function runFunctionsSecretsImport(context) {
|
|
5744
|
+
const [file] = context.positionals;
|
|
5745
|
+
if (file === void 0)
|
|
5746
|
+
throw new NaroError("VALIDATION_FAILED", IMPORT_USAGE);
|
|
5747
|
+
let text;
|
|
5748
|
+
try {
|
|
5749
|
+
text = readFileSync6(resolve6(context.cwd, file), "utf8");
|
|
5750
|
+
} catch {
|
|
5751
|
+
throw new NaroError("VALIDATION_FAILED", `Cannot read ${file}.`);
|
|
5752
|
+
}
|
|
5753
|
+
const parsed = parseDotenvDetailed(text);
|
|
5754
|
+
const [unclosed] = parsed.unclosedQuotes;
|
|
5755
|
+
if (unclosed !== void 0)
|
|
5756
|
+
throw new NaroError(
|
|
5757
|
+
"VALIDATION_FAILED",
|
|
5758
|
+
`Unclosed quote in ${file} at line ${String(unclosed)}.`
|
|
5759
|
+
);
|
|
5760
|
+
if (PROTO_LINE.test(text)) check(secretNameProblem("__proto__"));
|
|
5761
|
+
const entries = Object.entries(parsed.vars);
|
|
5762
|
+
if (entries.length === 0)
|
|
5763
|
+
throw new NaroError("VALIDATION_FAILED", `No secrets found in ${file}.`);
|
|
5764
|
+
const invalid = entries.map(([name]) => name).filter((name) => !NAME.test(name) || name.length > NAME_MAX);
|
|
5765
|
+
if (invalid.length > 0)
|
|
5766
|
+
throw new NaroError(
|
|
5767
|
+
"VALIDATION_FAILED",
|
|
5768
|
+
`Invalid keys in ${file}: ${invalid.map(printable).join(", ")}.`
|
|
5769
|
+
);
|
|
5770
|
+
for (const [name, value] of entries) {
|
|
5771
|
+
check(secretNameProblem(name));
|
|
5772
|
+
check(secretValueProblem(name, value));
|
|
5773
|
+
}
|
|
5774
|
+
const { client, projectId } = await readyToChange(context);
|
|
5775
|
+
context.output.log(`Secrets in ${file}:`);
|
|
5776
|
+
for (const [name, value] of entries)
|
|
5777
|
+
context.output.log(
|
|
5778
|
+
` ${name.padEnd(32)} ${value === "" ? "(empty)" : `\u2022\u2022\u2022\u2022\u2022\u2022 (${byteCount(value)})`}`
|
|
5779
|
+
);
|
|
5780
|
+
await confirmOrAbort(
|
|
5781
|
+
context,
|
|
5782
|
+
`Set ${String(entries.length)} function ${plural(entries.length, "secret", "secrets")} for all of this project's functions?`
|
|
5783
|
+
);
|
|
5784
|
+
const saved = await client.functions.setSecrets(projectId, {
|
|
5785
|
+
set: Object.fromEntries(entries)
|
|
5786
|
+
});
|
|
5787
|
+
context.output.log(
|
|
5788
|
+
`\u2705 Saved ${String(entries.length)} ${plural(entries.length, "secret", "secrets")}.`
|
|
5789
|
+
);
|
|
5790
|
+
const { lines, sync } = await applyChange(
|
|
5791
|
+
context,
|
|
5792
|
+
client,
|
|
5793
|
+
projectId,
|
|
5794
|
+
saved.pending
|
|
5795
|
+
);
|
|
5796
|
+
context.output.result(
|
|
5797
|
+
{ projectId, secrets: saved.secrets, pending: saved.pending, sync },
|
|
5798
|
+
lines
|
|
5799
|
+
);
|
|
5800
|
+
}
|
|
5801
|
+
async function runFunctionsSecretsUnset(context) {
|
|
5802
|
+
const names = context.positionals;
|
|
5803
|
+
if (names.length === 0) throw new NaroError("VALIDATION_FAILED", UNSET_USAGE);
|
|
5804
|
+
for (const name of names) check(secretNameProblem(name));
|
|
5805
|
+
const { client, projectId } = await readyToChange(context);
|
|
5806
|
+
await confirmOrAbort(
|
|
5807
|
+
context,
|
|
5808
|
+
`Unset ${names.join(", ")} from all of this project's functions? Functions that read them get undefined once they are synced.`
|
|
5809
|
+
);
|
|
5810
|
+
const saved = await client.functions.setSecrets(projectId, {
|
|
5811
|
+
unset: [...names]
|
|
5812
|
+
});
|
|
5813
|
+
context.output.log(
|
|
5814
|
+
`\u2705 Unset ${String(names.length)} ${plural(names.length, "secret", "secrets")}.`
|
|
5815
|
+
);
|
|
5816
|
+
const { lines, sync } = await applyChange(
|
|
5817
|
+
context,
|
|
5818
|
+
client,
|
|
5819
|
+
projectId,
|
|
5820
|
+
saved.pending
|
|
5821
|
+
);
|
|
5822
|
+
context.output.result(
|
|
5823
|
+
{ projectId, secrets: saved.secrets, pending: saved.pending, sync },
|
|
5824
|
+
lines
|
|
5825
|
+
);
|
|
5826
|
+
}
|
|
5827
|
+
async function runFunctionsSecretsSync(context) {
|
|
5828
|
+
const client = await context.client();
|
|
5829
|
+
const projectId = await context.resolveProjectId();
|
|
5830
|
+
const registry = context.flags.registry === true;
|
|
5831
|
+
const sync = await syncUntilDone(client, projectId, {
|
|
5832
|
+
registry,
|
|
5833
|
+
log: (line) => context.output.log(line)
|
|
5834
|
+
});
|
|
5835
|
+
if (!sync.complete) {
|
|
5836
|
+
warnFailures(context, sync);
|
|
5837
|
+
throw new NaroError(
|
|
5838
|
+
"FUNCTIONS_SYNC_INCOMPLETE",
|
|
5839
|
+
`The sync stopped with ${sync.pending.toLocaleString("en-US")} left to do. Run \`naro functions secrets sync${registry ? " --registry" : ""}\` again.`,
|
|
5840
|
+
{
|
|
5841
|
+
details: {
|
|
5842
|
+
pending: sync.pending,
|
|
5843
|
+
failures: sync.failures,
|
|
5844
|
+
synced: sync.synced,
|
|
5845
|
+
removed: sync.removed
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
);
|
|
5849
|
+
}
|
|
5850
|
+
const lines = [];
|
|
5851
|
+
if (sync.synced.length > 0)
|
|
5852
|
+
lines.push(
|
|
5853
|
+
`Synced ${functionCount(sync.synced.length)}: ${nameList(sync.synced)}`
|
|
5854
|
+
);
|
|
5855
|
+
if (sync.removed.length > 0)
|
|
5856
|
+
lines.push(`Removed from the function list: ${nameList(sync.removed)}`);
|
|
5857
|
+
lines.push("\u2705 All functions are up to date.");
|
|
5858
|
+
context.output.result({ projectId, ...sync }, lines);
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
// src/commands/gen.ts
|
|
5862
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
5863
|
+
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
5864
|
+
var LANG_MESSAGE = "naro gen types writes TypeScript only. Pass --lang typescript, or leave --lang out.";
|
|
5865
|
+
async function runGenTypes(context) {
|
|
5866
|
+
const { lang } = context.flags;
|
|
5867
|
+
if (lang !== void 0 && lang !== "typescript")
|
|
5868
|
+
throw new NaroError("VALIDATION_FAILED", LANG_MESSAGE);
|
|
5869
|
+
const client = await dataClient(context);
|
|
5870
|
+
const projectId = await context.resolveProjectId();
|
|
5871
|
+
const tables = await client.db.tables(projectId);
|
|
5872
|
+
const read = await policiesForTypes(client, projectId);
|
|
5873
|
+
const types = generateTypes(
|
|
5874
|
+
tables,
|
|
5875
|
+
read.policies === void 0 ? {} : { policies: read.policies }
|
|
5876
|
+
);
|
|
5877
|
+
const undescribed = tables.flatMap(
|
|
5878
|
+
(table3) => table3.error === void 0 ? [] : [{ name: table3.name, error: table3.error }]
|
|
5879
|
+
);
|
|
5880
|
+
for (const { name, error } of undescribed)
|
|
5881
|
+
context.output.warn(
|
|
5882
|
+
`\u26A0 ${printable(name)} could not be described and was generated as an empty interface: ${printable(error)}`
|
|
5883
|
+
);
|
|
5884
|
+
if (read.policies === void 0)
|
|
5885
|
+
context.output.warn(
|
|
5886
|
+
`\u26A0 The backend's policies could not be read (${printable(read.reason)}), so owner columns stay required in Insert.`
|
|
5887
|
+
);
|
|
5888
|
+
const extra = {
|
|
5889
|
+
...undescribed.length > 0 ? { undescribed } : {},
|
|
5890
|
+
...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
|
|
5891
|
+
};
|
|
5892
|
+
if (context.flags.output) {
|
|
5893
|
+
const path = resolve7(context.cwd, context.flags.output);
|
|
5894
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
5895
|
+
writeFileSync5(path, types);
|
|
5896
|
+
context.output.result(
|
|
5897
|
+
{
|
|
5898
|
+
projectId,
|
|
5899
|
+
output: context.flags.output,
|
|
5900
|
+
tables: tables.length,
|
|
5901
|
+
...extra
|
|
5902
|
+
},
|
|
5903
|
+
[
|
|
5904
|
+
`\u2705 Wrote types for ${String(tables.length)} table(s) to ${context.flags.output}`
|
|
5905
|
+
]
|
|
5906
|
+
);
|
|
5907
|
+
return;
|
|
5908
|
+
}
|
|
5909
|
+
context.output.result({ projectId, types, ...extra }, [
|
|
5910
|
+
types.replace(/\n$/, "")
|
|
5911
|
+
]);
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
// src/commands/insights.ts
|
|
5915
|
+
function queryOf2(flags) {
|
|
5916
|
+
const filters = filtersOf(flags, SPEED_INSIGHTS_FILTER_KEYS);
|
|
5917
|
+
return {
|
|
5918
|
+
...flags.range !== void 0 ? { range: flags.range } : {},
|
|
5919
|
+
...flags.metric !== void 0 ? { metric: flags.metric } : {},
|
|
5920
|
+
...flags.env !== void 0 ? { environment: flags.env } : {},
|
|
5921
|
+
...Object.keys(filters).length > 0 ? { filters } : {}
|
|
5922
|
+
};
|
|
5923
|
+
}
|
|
5924
|
+
async function runInsights(context) {
|
|
5925
|
+
if (context.flags.referrer !== void 0 && context.flags.referrer !== "")
|
|
5926
|
+
throw new NaroError(
|
|
5927
|
+
"VALIDATION_FAILED",
|
|
5928
|
+
"Speed Insights has no referrer; drop --referrer."
|
|
5929
|
+
);
|
|
5930
|
+
const client = await context.client();
|
|
5931
|
+
const projectId = await context.resolveProjectId();
|
|
5932
|
+
const query = queryOf2(context.flags);
|
|
5933
|
+
const filters = query.filters ?? {};
|
|
5934
|
+
if (context.flags.route !== void 0) {
|
|
5935
|
+
const result = await client.insights.paths(projectId, {
|
|
5936
|
+
...query,
|
|
5937
|
+
route: context.flags.route
|
|
5938
|
+
});
|
|
5939
|
+
context.output.result(
|
|
5940
|
+
{ projectId, ...result },
|
|
5941
|
+
routePathsLines(result, filters)
|
|
5942
|
+
);
|
|
5943
|
+
return;
|
|
5944
|
+
}
|
|
5945
|
+
const overview = await client.insights.overview(projectId, query);
|
|
5946
|
+
if (overview.deploymentsError)
|
|
5947
|
+
context.output.warn(
|
|
5948
|
+
`Deployments are unavailable right now: ${printable(overview.deploymentsError.message)}`
|
|
5949
|
+
);
|
|
5950
|
+
context.output.result(
|
|
5951
|
+
{ projectId, ...overview },
|
|
5952
|
+
reportLines(overview.report, overview.deployments, filters)
|
|
5567
5953
|
);
|
|
5568
|
-
let lastTs = -1;
|
|
5569
|
-
let boundary = /* @__PURE__ */ new Set();
|
|
5570
|
-
for (; ; ) {
|
|
5571
|
-
const entries = await client.logs.runtime(projectId, options);
|
|
5572
|
-
for (const entry of entries) {
|
|
5573
|
-
if (entry.ts < lastTs) continue;
|
|
5574
|
-
const key = JSON.stringify(entry);
|
|
5575
|
-
if (entry.ts === lastTs) {
|
|
5576
|
-
if (boundary.has(key)) continue;
|
|
5577
|
-
} else {
|
|
5578
|
-
lastTs = entry.ts;
|
|
5579
|
-
boundary = /* @__PURE__ */ new Set();
|
|
5580
|
-
}
|
|
5581
|
-
boundary.add(key);
|
|
5582
|
-
context.output.log(formatRuntimeLog(entry));
|
|
5583
|
-
}
|
|
5584
|
-
await sleep(FOLLOW_INTERVAL_MS);
|
|
5585
|
-
}
|
|
5586
5954
|
}
|
|
5587
|
-
async function
|
|
5588
|
-
|
|
5589
|
-
const type = context.flags.type ?? (deploymentId !== void 0 ? "build" : "runtime");
|
|
5590
|
-
if (type !== "build" && type !== "runtime")
|
|
5591
|
-
throw new NaroError(
|
|
5592
|
-
"VALIDATION_FAILED",
|
|
5593
|
-
"--type must be build or runtime."
|
|
5594
|
-
);
|
|
5595
|
-
if (type === "runtime" && context.flags.follow && context.flags.json)
|
|
5955
|
+
async function setInsights(context, enabled) {
|
|
5956
|
+
if (context.flags.env !== void 0)
|
|
5596
5957
|
throw new NaroError(
|
|
5597
5958
|
"VALIDATION_FAILED",
|
|
5598
|
-
"
|
|
5959
|
+
"Speed Insights has one switch for production and preview; drop --env."
|
|
5599
5960
|
);
|
|
5600
|
-
|
|
5961
|
+
const filter = givenFilterFlag(context.flags);
|
|
5962
|
+
if (filter !== void 0)
|
|
5601
5963
|
throw new NaroError(
|
|
5602
5964
|
"VALIDATION_FAILED",
|
|
5603
|
-
|
|
5965
|
+
`Filters only narrow a report; drop --${filter}.`
|
|
5604
5966
|
);
|
|
5605
5967
|
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);
|
|
5968
|
+
const projectId = await context.resolveProjectId();
|
|
5969
|
+
const result = await client.insights.setEnabled(projectId, enabled);
|
|
5970
|
+
context.output.result({ projectId, enabled: result.enabled }, [
|
|
5971
|
+
result.enabled ? TURNED_ON : TURNED_OFF
|
|
5972
|
+
]);
|
|
5973
|
+
}
|
|
5974
|
+
function runInsightsEnable(context) {
|
|
5975
|
+
return setInsights(context, true);
|
|
5976
|
+
}
|
|
5977
|
+
function runInsightsDisable(context) {
|
|
5978
|
+
return setInsights(context, false);
|
|
5622
5979
|
}
|
|
5623
5980
|
|
|
5624
5981
|
// src/commands/projects.ts
|
|
@@ -5669,7 +6026,7 @@ async function runProjectInspect(context) {
|
|
|
5669
6026
|
|
|
5670
6027
|
// src/commands/promote.ts
|
|
5671
6028
|
var PROMOTE_USAGE = "Usage: naro promote <deployment-id> [--to <environment>]";
|
|
5672
|
-
var
|
|
6029
|
+
var ROLLBACK_USAGE2 = "Usage: naro rollback [deployment-id]";
|
|
5673
6030
|
var REBUILD_TIMEOUT_MS = 15 * 6e4;
|
|
5674
6031
|
var REUPLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
5675
6032
|
async function promoteAndWait(context, client, project, sourceDeploymentId, action, target) {
|
|
@@ -5733,9 +6090,9 @@ function refuseWhatIsNotRead(context, usage, envMessage) {
|
|
|
5733
6090
|
throw new NaroError("VALIDATION_FAILED", envMessage);
|
|
5734
6091
|
}
|
|
5735
6092
|
async function previewPromotion(context, client, projectId, sourceDeploymentId, target) {
|
|
5736
|
-
let
|
|
6093
|
+
let check2;
|
|
5737
6094
|
try {
|
|
5738
|
-
|
|
6095
|
+
check2 = await client.deployments.promoteCheck(
|
|
5739
6096
|
projectId,
|
|
5740
6097
|
sourceDeploymentId,
|
|
5741
6098
|
target
|
|
@@ -5747,9 +6104,9 @@ async function previewPromotion(context, client, projectId, sourceDeploymentId,
|
|
|
5747
6104
|
return;
|
|
5748
6105
|
}
|
|
5749
6106
|
context.output.log(
|
|
5750
|
-
|
|
6107
|
+
check2.mode === "rebuild" ? `\u25B8 rebuild: builds the same commit again with ${target ?? "production"}'s variables` : "\u25B8 reupload: publishes the stored build"
|
|
5751
6108
|
);
|
|
5752
|
-
for (const warning of
|
|
6109
|
+
for (const warning of check2.warnings)
|
|
5753
6110
|
for (const line of warningLines(warning)) context.output.log(line);
|
|
5754
6111
|
}
|
|
5755
6112
|
async function runPromote(context) {
|
|
@@ -5791,7 +6148,7 @@ async function runRollback(context) {
|
|
|
5791
6148
|
);
|
|
5792
6149
|
refuseWhatIsNotRead(
|
|
5793
6150
|
context,
|
|
5794
|
-
|
|
6151
|
+
ROLLBACK_USAGE2,
|
|
5795
6152
|
"--env is not an option of naro rollback: naro rollback always rolls production back."
|
|
5796
6153
|
);
|
|
5797
6154
|
const client = await context.client();
|
|
@@ -6344,7 +6701,7 @@ async function runStorageBucketRemove(context) {
|
|
|
6344
6701
|
import { randomUUID } from "crypto";
|
|
6345
6702
|
import { createWriteStream as createWriteStream2, openAsBlob, rmSync as rmSync2 } from "fs";
|
|
6346
6703
|
import { rename, rm, stat } from "fs/promises";
|
|
6347
|
-
import { basename as basename2, dirname as
|
|
6704
|
+
import { basename as basename2, dirname as dirname4, extname, join as join6, resolve as resolve8 } from "path";
|
|
6348
6705
|
import { Readable } from "stream";
|
|
6349
6706
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
6350
6707
|
|
|
@@ -6477,6 +6834,17 @@ function serviceKeyOffServer() {
|
|
|
6477
6834
|
message: SERVICE_KEY_OFF_SERVER_MESSAGE
|
|
6478
6835
|
};
|
|
6479
6836
|
}
|
|
6837
|
+
function accessTokenUnavailable(cause) {
|
|
6838
|
+
return {
|
|
6839
|
+
code: "ACCESS_TOKEN_UNAVAILABLE",
|
|
6840
|
+
message: "createClient's accessToken function failed or returned something that is not a string or null, so the request was not sent.",
|
|
6841
|
+
details: { cause: describe(cause) }
|
|
6842
|
+
};
|
|
6843
|
+
}
|
|
6844
|
+
function accessTokenAuthMessage(name) {
|
|
6845
|
+
return `naro.auth.${name} is not available on a client made with accessToken: its token comes from your accessToken function. Use a client made without accessToken to sign in.`;
|
|
6846
|
+
}
|
|
6847
|
+
var ACCESS_TOKEN_CHANNEL_MESSAGE = "naro.channel() is not available on a client made with accessToken: Realtime signs in with a session, not an access token. Broadcast from a function with naroAdmin.";
|
|
6480
6848
|
function notJoined(channel) {
|
|
6481
6849
|
return {
|
|
6482
6850
|
code: "NOT_JOINED",
|
|
@@ -6543,6 +6911,16 @@ function storeFor(config, path) {
|
|
|
6543
6911
|
return SESSION_PREFIXES.some((prefix) => path.startsWith(prefix)) ? config.tokens : config.accessJwt ?? null;
|
|
6544
6912
|
}
|
|
6545
6913
|
var ACCESS_JWT_MARGIN_MS = 6e4;
|
|
6914
|
+
function deadlineOf(timeoutMs) {
|
|
6915
|
+
return timeoutMs === void 0 ? null : { at: Date.now() + timeoutMs, ms: timeoutMs };
|
|
6916
|
+
}
|
|
6917
|
+
function remainingMs(deadline) {
|
|
6918
|
+
return Math.max(0, deadline.at - Date.now());
|
|
6919
|
+
}
|
|
6920
|
+
var EXPIRED = Symbol("expired");
|
|
6921
|
+
function timedOut(deadline) {
|
|
6922
|
+
return requestTimeout(deadline?.ms ?? 0);
|
|
6923
|
+
}
|
|
6546
6924
|
function timesOf(jwt) {
|
|
6547
6925
|
const payload = jwt.split(".")[1];
|
|
6548
6926
|
if (payload === void 0 || payload === "") return null;
|
|
@@ -6638,6 +7016,13 @@ async function accessJwtFor(config, jwtStore) {
|
|
|
6638
7016
|
async function bearerFor(config, path, withToken) {
|
|
6639
7017
|
if (!withToken) return { token: null, session: null };
|
|
6640
7018
|
const store = storeFor(config, path);
|
|
7019
|
+
if (config.accessToken !== void 0 && store !== config.tokens) {
|
|
7020
|
+
try {
|
|
7021
|
+
return { token: await config.accessToken.get(), session: null };
|
|
7022
|
+
} catch (cause) {
|
|
7023
|
+
return { error: accessTokenUnavailable(cause) };
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
6641
7026
|
if (store === null) return { token: null, session: null };
|
|
6642
7027
|
if (store === config.tokens) {
|
|
6643
7028
|
const token = await store.get();
|
|
@@ -6652,9 +7037,12 @@ async function retryBearer(config, path, error, first) {
|
|
|
6652
7037
|
if ("error" in renewed) return renewed;
|
|
6653
7038
|
return renewed.token === null || renewed.token === first.token || renewed.session !== first.session ? null : renewed;
|
|
6654
7039
|
}
|
|
6655
|
-
async function receive(config, options, init) {
|
|
6656
|
-
|
|
7040
|
+
async function receive(config, options, init, deadline) {
|
|
7041
|
+
if (deadline !== null && remainingMs(deadline) === 0)
|
|
7042
|
+
return { error: timedOut(deadline) };
|
|
7043
|
+
const controller = deadline !== null && typeof AbortController === "function" ? new AbortController() : null;
|
|
6657
7044
|
if (controller) init.signal = controller.signal;
|
|
7045
|
+
let gaveUp = false;
|
|
6658
7046
|
const request = (async () => {
|
|
6659
7047
|
try {
|
|
6660
7048
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
@@ -6662,6 +7050,10 @@ async function receive(config, options, init) {
|
|
|
6662
7050
|
`${config.baseUrl}/_naro/v1${options.path}`,
|
|
6663
7051
|
init
|
|
6664
7052
|
);
|
|
7053
|
+
if (gaveUp) {
|
|
7054
|
+
discard(response);
|
|
7055
|
+
return { error: timedOut(deadline) };
|
|
7056
|
+
}
|
|
6665
7057
|
return {
|
|
6666
7058
|
status: response.status,
|
|
6667
7059
|
headers: response.headers,
|
|
@@ -6671,14 +7063,14 @@ async function receive(config, options, init) {
|
|
|
6671
7063
|
return { error: networkError(error) };
|
|
6672
7064
|
}
|
|
6673
7065
|
})();
|
|
6674
|
-
|
|
6675
|
-
if (timeoutMs === void 0) return request;
|
|
7066
|
+
if (deadline === null) return request;
|
|
6676
7067
|
let timer;
|
|
6677
|
-
const expired = new Promise((
|
|
7068
|
+
const expired = new Promise((resolve9) => {
|
|
6678
7069
|
timer = setTimeout(() => {
|
|
7070
|
+
gaveUp = true;
|
|
6679
7071
|
controller?.abort();
|
|
6680
|
-
|
|
6681
|
-
},
|
|
7072
|
+
resolve9({ error: timedOut(deadline) });
|
|
7073
|
+
}, remainingMs(deadline));
|
|
6682
7074
|
});
|
|
6683
7075
|
try {
|
|
6684
7076
|
return await Promise.race([request, expired]);
|
|
@@ -6695,15 +7087,34 @@ async function call(config, options) {
|
|
|
6695
7087
|
return { error: invalidQuery("", "is not JSON"), answer: null };
|
|
6696
7088
|
}
|
|
6697
7089
|
}
|
|
6698
|
-
const
|
|
7090
|
+
const deadline = deadlineOf(options.timeoutMs);
|
|
7091
|
+
const bearer = await untilStopped(
|
|
7092
|
+
bearerFor(config, options.path, options.withToken),
|
|
7093
|
+
void 0,
|
|
7094
|
+
deadline
|
|
7095
|
+
);
|
|
7096
|
+
if (bearer === EXPIRED || bearer === ABORTED)
|
|
7097
|
+
return { error: timedOut(deadline), answer: null };
|
|
6699
7098
|
if ("error" in bearer) return { error: bearer.error, answer: null };
|
|
6700
|
-
const result = await callWith(
|
|
6701
|
-
|
|
7099
|
+
const result = await callWith(
|
|
7100
|
+
config,
|
|
7101
|
+
options,
|
|
7102
|
+
payload,
|
|
7103
|
+
bearer.token,
|
|
7104
|
+
deadline
|
|
7105
|
+
);
|
|
7106
|
+
const again = await untilStopped(
|
|
7107
|
+
retryBearer(config, options.path, result.error, bearer),
|
|
7108
|
+
void 0,
|
|
7109
|
+
deadline
|
|
7110
|
+
);
|
|
7111
|
+
if (again === EXPIRED || again === ABORTED)
|
|
7112
|
+
return { error: timedOut(deadline), answer: null };
|
|
6702
7113
|
if (again === null) return result;
|
|
6703
7114
|
if ("error" in again) return { error: again.error, answer: null };
|
|
6704
|
-
return callWith(config, options, payload, again.token);
|
|
7115
|
+
return callWith(config, options, payload, again.token, deadline);
|
|
6705
7116
|
}
|
|
6706
|
-
async function callWith(config, options, payload, token) {
|
|
7117
|
+
async function callWith(config, options, payload, token, deadline = null) {
|
|
6707
7118
|
const store = storeFor(config, options.path);
|
|
6708
7119
|
const headers = {
|
|
6709
7120
|
...options.headers,
|
|
@@ -6723,7 +7134,7 @@ async function callWith(config, options, payload, token) {
|
|
|
6723
7134
|
headers["Content-Type"] = "application/json";
|
|
6724
7135
|
init.body = payload;
|
|
6725
7136
|
}
|
|
6726
|
-
const received = await receive(config, options, init);
|
|
7137
|
+
const received = await receive(config, options, init, deadline);
|
|
6727
7138
|
if ("error" in received) return { error: received.error, answer: null };
|
|
6728
7139
|
const { status, headers: responseHeaders, text } = received;
|
|
6729
7140
|
let body;
|
|
@@ -6770,13 +7181,13 @@ var ABORTED = Symbol("aborted");
|
|
|
6770
7181
|
function untilAborted(promise, signal) {
|
|
6771
7182
|
if (!signal) return promise;
|
|
6772
7183
|
if (signal.aborted) return Promise.resolve(ABORTED);
|
|
6773
|
-
return new Promise((
|
|
6774
|
-
const stop = () =>
|
|
7184
|
+
return new Promise((resolve9, reject) => {
|
|
7185
|
+
const stop = () => resolve9(ABORTED);
|
|
6775
7186
|
signal.addEventListener("abort", stop, { once: true });
|
|
6776
7187
|
promise.then(
|
|
6777
7188
|
(value) => {
|
|
6778
7189
|
signal.removeEventListener("abort", stop);
|
|
6779
|
-
|
|
7190
|
+
resolve9(value);
|
|
6780
7191
|
},
|
|
6781
7192
|
(error) => {
|
|
6782
7193
|
signal.removeEventListener("abort", stop);
|
|
@@ -6785,6 +7196,27 @@ function untilAborted(promise, signal) {
|
|
|
6785
7196
|
);
|
|
6786
7197
|
});
|
|
6787
7198
|
}
|
|
7199
|
+
function untilStopped(promise, signal, deadline) {
|
|
7200
|
+
if (signal?.aborted) return Promise.resolve(ABORTED);
|
|
7201
|
+
if (!signal && deadline === null) return promise;
|
|
7202
|
+
return new Promise((resolve9, reject) => {
|
|
7203
|
+
let timer;
|
|
7204
|
+
const stop = () => finish(ABORTED);
|
|
7205
|
+
const finish = (value) => {
|
|
7206
|
+
clearTimeout(timer);
|
|
7207
|
+
signal?.removeEventListener("abort", stop);
|
|
7208
|
+
resolve9(value);
|
|
7209
|
+
};
|
|
7210
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
7211
|
+
if (deadline !== null)
|
|
7212
|
+
timer = setTimeout(() => finish(EXPIRED), remainingMs(deadline));
|
|
7213
|
+
promise.then(finish, (error) => {
|
|
7214
|
+
clearTimeout(timer);
|
|
7215
|
+
signal?.removeEventListener("abort", stop);
|
|
7216
|
+
reject(error);
|
|
7217
|
+
});
|
|
7218
|
+
});
|
|
7219
|
+
}
|
|
6788
7220
|
function isStream(body) {
|
|
6789
7221
|
return typeof ReadableStream === "function" && body instanceof ReadableStream;
|
|
6790
7222
|
}
|
|
@@ -6844,6 +7276,143 @@ async function sendWith(config, options, token) {
|
|
|
6844
7276
|
status
|
|
6845
7277
|
};
|
|
6846
7278
|
}
|
|
7279
|
+
async function fetchUnread(config, options) {
|
|
7280
|
+
const { signal } = options;
|
|
7281
|
+
if (signal?.aborted) return { failed: "aborted", cause: signal.reason };
|
|
7282
|
+
const deadline = deadlineOf(options.timeoutMs);
|
|
7283
|
+
if (options.ready) {
|
|
7284
|
+
const ready = await untilStopped(options.ready(), signal, deadline);
|
|
7285
|
+
if (ready === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
7286
|
+
if (ready === EXPIRED) return { failed: "timeout", cause: null };
|
|
7287
|
+
}
|
|
7288
|
+
const own = options.headers.has("authorization");
|
|
7289
|
+
const bearer = await untilStopped(
|
|
7290
|
+
bearerFor(config, options.path, !own),
|
|
7291
|
+
signal,
|
|
7292
|
+
deadline
|
|
7293
|
+
);
|
|
7294
|
+
if (bearer === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
7295
|
+
if (bearer === EXPIRED) return { failed: "timeout", cause: null };
|
|
7296
|
+
if ("error" in bearer) return { refused: bearer.error };
|
|
7297
|
+
const first = await fetchOnce(config, options, bearer.token, deadline);
|
|
7298
|
+
if (!("response" in first) || bearer.token === null) return first;
|
|
7299
|
+
const refused = await refusesToken(first.response, signal, deadline);
|
|
7300
|
+
if (refused === EXPIRED) {
|
|
7301
|
+
discard(first.response);
|
|
7302
|
+
return { failed: "timeout", cause: null };
|
|
7303
|
+
}
|
|
7304
|
+
if (refused === ABORTED) {
|
|
7305
|
+
discard(first.response);
|
|
7306
|
+
return { failed: "aborted", cause: signal?.reason };
|
|
7307
|
+
}
|
|
7308
|
+
if (!refused) return first;
|
|
7309
|
+
await storeFor(config, options.path)?.clearIf(bearer.token);
|
|
7310
|
+
if (isStream(options.body)) return first;
|
|
7311
|
+
const again = await untilStopped(
|
|
7312
|
+
retryBearer(config, options.path, invalidSession(), bearer),
|
|
7313
|
+
signal,
|
|
7314
|
+
deadline
|
|
7315
|
+
);
|
|
7316
|
+
if (again === EXPIRED) {
|
|
7317
|
+
discard(first.response);
|
|
7318
|
+
return { failed: "timeout", cause: null };
|
|
7319
|
+
}
|
|
7320
|
+
if (again === null || again !== ABORTED && "error" in again) return first;
|
|
7321
|
+
discard(first.response);
|
|
7322
|
+
if (again === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
7323
|
+
const second = await fetchOnce(config, options, again.token, deadline);
|
|
7324
|
+
if (!("response" in second) || again.token === null) return second;
|
|
7325
|
+
const refusedAgain = await refusesToken(second.response, signal, deadline);
|
|
7326
|
+
if (refusedAgain === ABORTED) {
|
|
7327
|
+
discard(second.response);
|
|
7328
|
+
return { failed: "aborted", cause: signal?.reason };
|
|
7329
|
+
}
|
|
7330
|
+
if (refusedAgain === true || refusedAgain === EXPIRED)
|
|
7331
|
+
await storeFor(config, options.path)?.clearIf(again.token);
|
|
7332
|
+
return second;
|
|
7333
|
+
}
|
|
7334
|
+
async function refusesToken(response, signal, deadline) {
|
|
7335
|
+
if (response.status !== 401 || response.headers.get("x-relay-error") !== "true")
|
|
7336
|
+
return false;
|
|
7337
|
+
let body;
|
|
7338
|
+
try {
|
|
7339
|
+
body = await untilStopped(response.clone().json(), signal, deadline);
|
|
7340
|
+
} catch {
|
|
7341
|
+
return signal?.aborted ? ABORTED : false;
|
|
7342
|
+
}
|
|
7343
|
+
if (body === ABORTED || body === EXPIRED) return body;
|
|
7344
|
+
return isRecord(body) && isRecord(body.error) && body.error.code === "INVALID_SESSION";
|
|
7345
|
+
}
|
|
7346
|
+
var releases = /* @__PURE__ */ new WeakMap();
|
|
7347
|
+
function discard(response) {
|
|
7348
|
+
releases.get(response)?.();
|
|
7349
|
+
releases.delete(response);
|
|
7350
|
+
response.body?.cancel().catch(() => {
|
|
7351
|
+
});
|
|
7352
|
+
}
|
|
7353
|
+
async function fetchOnce(config, options, token, deadline) {
|
|
7354
|
+
const { signal } = options;
|
|
7355
|
+
if (signal?.aborted) return { failed: "aborted", cause: signal.reason };
|
|
7356
|
+
if (deadline !== null && remainingMs(deadline) === 0)
|
|
7357
|
+
return { failed: "timeout", cause: null };
|
|
7358
|
+
const headers = new Headers(options.headers);
|
|
7359
|
+
headers.set("apikey", config.apiKey);
|
|
7360
|
+
if (token !== null) headers.set("authorization", `Bearer ${token}`);
|
|
7361
|
+
const init = {
|
|
7362
|
+
method: options.method,
|
|
7363
|
+
headers,
|
|
7364
|
+
credentials: "omit"
|
|
7365
|
+
};
|
|
7366
|
+
if (options.body !== void 0) init.body = options.body;
|
|
7367
|
+
if (isStream(options.body)) init.duplex = "half";
|
|
7368
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
7369
|
+
if (controller) init.signal = controller.signal;
|
|
7370
|
+
let timer;
|
|
7371
|
+
let stop = () => {
|
|
7372
|
+
};
|
|
7373
|
+
const stopped = new Promise((resolve9) => {
|
|
7374
|
+
stop = resolve9;
|
|
7375
|
+
});
|
|
7376
|
+
const onAbort = () => {
|
|
7377
|
+
stop({ failed: "aborted", cause: signal?.reason });
|
|
7378
|
+
controller?.abort(signal?.reason);
|
|
7379
|
+
};
|
|
7380
|
+
if (deadline !== null)
|
|
7381
|
+
timer = setTimeout(() => {
|
|
7382
|
+
stop({ failed: "timeout", cause: null });
|
|
7383
|
+
controller?.abort();
|
|
7384
|
+
}, remainingMs(deadline));
|
|
7385
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
7386
|
+
const request = (async () => {
|
|
7387
|
+
try {
|
|
7388
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
7389
|
+
return {
|
|
7390
|
+
response: await fetchImpl(
|
|
7391
|
+
`${config.baseUrl}/_naro/v1${options.path}`,
|
|
7392
|
+
init
|
|
7393
|
+
)
|
|
7394
|
+
};
|
|
7395
|
+
} catch (cause) {
|
|
7396
|
+
return { failed: "network", cause };
|
|
7397
|
+
}
|
|
7398
|
+
})();
|
|
7399
|
+
const release = () => signal?.removeEventListener("abort", onAbort);
|
|
7400
|
+
let outcome;
|
|
7401
|
+
try {
|
|
7402
|
+
outcome = await Promise.race([request, stopped]);
|
|
7403
|
+
return outcome;
|
|
7404
|
+
} finally {
|
|
7405
|
+
clearTimeout(timer);
|
|
7406
|
+
if (outcome !== void 0 && "response" in outcome)
|
|
7407
|
+
releases.set(outcome.response, release);
|
|
7408
|
+
else {
|
|
7409
|
+
release();
|
|
7410
|
+
void request.then((late) => {
|
|
7411
|
+
if ("response" in late) discard(late.response);
|
|
7412
|
+
});
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7415
|
+
}
|
|
6847
7416
|
var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
6848
7417
|
"RATE_LIMITED",
|
|
6849
7418
|
"WRITE_QUOTA_EXCEEDED",
|
|
@@ -6852,7 +7421,12 @@ var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
|
6852
7421
|
"OBJECT_RATE_LIMITED",
|
|
6853
7422
|
"STORAGE_OPS_QUOTA_EXCEEDED",
|
|
6854
7423
|
// Realtime's REST broadcast over the project's message budget (x-retry-after: 1).
|
|
6855
|
-
"TOO_MANY_MESSAGES"
|
|
7424
|
+
"TOO_MANY_MESSAGES",
|
|
7425
|
+
// Naro Functions (design F20): a team over its monthly function CPU, and
|
|
7426
|
+
// the control plane's Cloudflare limit. `invoke` reads x-retry-after on
|
|
7427
|
+
// every relay error anyway (invoke/client.ts).
|
|
7428
|
+
"FUNCTIONS_QUOTA_EXCEEDED",
|
|
7429
|
+
"FUNCTIONS_RATE_LIMITED"
|
|
6856
7430
|
]);
|
|
6857
7431
|
function withRetryAfter(error, headers) {
|
|
6858
7432
|
const header = headers.get("x-retry-after")?.trim() ?? "";
|
|
@@ -7166,14 +7740,14 @@ var AuthClient = class {
|
|
|
7166
7740
|
*/
|
|
7167
7741
|
async signInWithOAuth(options) {
|
|
7168
7742
|
const { provider } = options;
|
|
7169
|
-
const
|
|
7743
|
+
const failed2 = (error) => ({
|
|
7170
7744
|
data: { provider, url: null },
|
|
7171
7745
|
error
|
|
7172
7746
|
});
|
|
7173
|
-
if (!OAUTH_PROVIDERS.includes(provider)) return
|
|
7747
|
+
if (!OAUTH_PROVIDERS.includes(provider)) return failed2(invalidProvider());
|
|
7174
7748
|
await this.#flow.recovery.forget();
|
|
7175
7749
|
const pair = await createPkcePair();
|
|
7176
|
-
if (!pair) return
|
|
7750
|
+
if (!pair) return failed2(cryptoUnavailable());
|
|
7177
7751
|
await this.#flow.verifiers.add(pair.challenge, pair.verifier);
|
|
7178
7752
|
const redirectTo = options.redirectTo ?? currentPage();
|
|
7179
7753
|
const url = new URL(
|
|
@@ -7739,6 +8313,282 @@ var TokenStore = class {
|
|
|
7739
8313
|
}
|
|
7740
8314
|
}
|
|
7741
8315
|
};
|
|
8316
|
+
function prepareBody(body) {
|
|
8317
|
+
if (body === void 0) return { body: void 0, contentType: null };
|
|
8318
|
+
if (typeof body === "string") return { body, contentType: "text/plain" };
|
|
8319
|
+
if (typeof Blob === "function" && body instanceof Blob)
|
|
8320
|
+
return { body, contentType: "application/octet-stream" };
|
|
8321
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body))
|
|
8322
|
+
return { body, contentType: "application/octet-stream" };
|
|
8323
|
+
if (typeof FormData === "function" && body instanceof FormData)
|
|
8324
|
+
return { body, contentType: null };
|
|
8325
|
+
if (typeof URLSearchParams === "function" && body instanceof URLSearchParams)
|
|
8326
|
+
return { body, contentType: null };
|
|
8327
|
+
if (typeof ReadableStream === "function" && body instanceof ReadableStream)
|
|
8328
|
+
return { body, contentType: null };
|
|
8329
|
+
try {
|
|
8330
|
+
const text = JSON.stringify(body);
|
|
8331
|
+
return text === void 0 ? null : { body: text, contentType: "application/json" };
|
|
8332
|
+
} catch {
|
|
8333
|
+
return null;
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
async function readAnswer(response, method) {
|
|
8337
|
+
if (response.status === 204 || method === "HEAD") return { data: null };
|
|
8338
|
+
const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
8339
|
+
try {
|
|
8340
|
+
if (type === "application/json") {
|
|
8341
|
+
const text = await response.text();
|
|
8342
|
+
return {
|
|
8343
|
+
data: text.trim() === "" ? null : JSON.parse(text)
|
|
8344
|
+
};
|
|
8345
|
+
}
|
|
8346
|
+
if (type === "application/octet-stream" || type === "application/pdf")
|
|
8347
|
+
return { data: await response.blob() };
|
|
8348
|
+
if (type === "text/event-stream") return { data: response };
|
|
8349
|
+
if (type === "multipart/form-data")
|
|
8350
|
+
return { data: await response.formData() };
|
|
8351
|
+
return { data: await response.text() };
|
|
8352
|
+
} catch (cause) {
|
|
8353
|
+
return { unreadable: cause };
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
var FunctionsError = class extends Error {
|
|
8357
|
+
code;
|
|
8358
|
+
context;
|
|
8359
|
+
constructor(code, message, context, details) {
|
|
8360
|
+
super(message);
|
|
8361
|
+
this.name = "FunctionsError";
|
|
8362
|
+
this.code = code;
|
|
8363
|
+
this.context = context;
|
|
8364
|
+
if (details !== void 0)
|
|
8365
|
+
Object.defineProperty(this, "details", {
|
|
8366
|
+
value: details,
|
|
8367
|
+
enumerable: true
|
|
8368
|
+
});
|
|
8369
|
+
}
|
|
8370
|
+
};
|
|
8371
|
+
var FunctionsHttpError = class extends FunctionsError {
|
|
8372
|
+
constructor(response) {
|
|
8373
|
+
super(
|
|
8374
|
+
"FUNCTION_HTTP_ERROR",
|
|
8375
|
+
`The function answered with HTTP ${String(response.status)}.`,
|
|
8376
|
+
response
|
|
8377
|
+
);
|
|
8378
|
+
this.name = "FunctionsHttpError";
|
|
8379
|
+
}
|
|
8380
|
+
};
|
|
8381
|
+
var FunctionsRelayError = class extends FunctionsError {
|
|
8382
|
+
constructor(code, message, response, details) {
|
|
8383
|
+
super(code, message, response, details);
|
|
8384
|
+
this.name = "FunctionsRelayError";
|
|
8385
|
+
}
|
|
8386
|
+
};
|
|
8387
|
+
var FunctionsFetchError = class extends FunctionsError {
|
|
8388
|
+
constructor(code, message, cause, details) {
|
|
8389
|
+
super(code, message, cause, details);
|
|
8390
|
+
this.name = "FunctionsFetchError";
|
|
8391
|
+
}
|
|
8392
|
+
};
|
|
8393
|
+
var FUNCTIONS_MESSAGES = {
|
|
8394
|
+
timeout: (ms) => `The function did not answer within ${String(ms)} ms.`,
|
|
8395
|
+
aborted: "The function call was stopped before the function answered.",
|
|
8396
|
+
invalidBody: "functions.invoke could not send this body: JSON cannot hold it (a BigInt or a cycle). Pass a string, a Blob, bytes, FormData, URLSearchParams or a stream instead.",
|
|
8397
|
+
invalidTimeout: "functions.invoke's timeout is whole milliseconds from 1 to 2147483647.",
|
|
8398
|
+
invalidHeaders: "functions.invoke's headers are not valid HTTP header names and values.",
|
|
8399
|
+
invalidName: "functions.invoke's name is a string: the function's name, then any path and query.",
|
|
8400
|
+
unencodableName: "functions.invoke's name is not well-formed Unicode: it has a lone surrogate.",
|
|
8401
|
+
dotSegments: `functions.invoke's name cannot be "." or "..", and ".." in its path cannot leave /functions/.`,
|
|
8402
|
+
invalidSignal: "functions.invoke's signal is not an AbortSignal.",
|
|
8403
|
+
bodyWithGetOrHead: "functions.invoke cannot send a body with GET or HEAD.",
|
|
8404
|
+
/** `RELAY_ERROR`: a HEAD answer has no body, so Naro's code is not in it. */
|
|
8405
|
+
headRelay: (status) => `Naro answered HTTP ${String(status)} for the function, but a HEAD answer has no body to say why. Call it with GET to read the reason.`,
|
|
8406
|
+
unreadable: (status) => `The function answered HTTP ${String(status)}, but its body could not be read as its content-type says.`
|
|
8407
|
+
};
|
|
8408
|
+
var MAX_TIMER_MS = 2147483647;
|
|
8409
|
+
function functionPath(name) {
|
|
8410
|
+
const cut = name.search(/[/?#]/);
|
|
8411
|
+
const head = cut === -1 ? name : name.slice(0, cut);
|
|
8412
|
+
const tail = cut === -1 ? "" : name.slice(cut);
|
|
8413
|
+
return `/functions/${encodeURIComponent(head)}${tail}`;
|
|
8414
|
+
}
|
|
8415
|
+
function dotSegment(segment) {
|
|
8416
|
+
const dots = segment.toLowerCase().replace(/%2e/g, ".");
|
|
8417
|
+
return dots === "." ? 1 : dots === ".." ? 2 : 0;
|
|
8418
|
+
}
|
|
8419
|
+
function leavesFunctions(input) {
|
|
8420
|
+
const name = input.replace(/[\t\n\r]/g, "");
|
|
8421
|
+
const cut = name.search(/[/?#]/);
|
|
8422
|
+
const head = cut === -1 ? name : name.slice(0, cut);
|
|
8423
|
+
if (head === "." || head === "..") return true;
|
|
8424
|
+
if (cut === -1 || name[cut] !== "/") return false;
|
|
8425
|
+
const rest = name.slice(cut + 1);
|
|
8426
|
+
const end = rest.search(/[?#]/);
|
|
8427
|
+
let depth = 1;
|
|
8428
|
+
for (const segment of (end === -1 ? rest : rest.slice(0, end)).split(
|
|
8429
|
+
/[/\\]/
|
|
8430
|
+
)) {
|
|
8431
|
+
const dots = dotSegment(segment);
|
|
8432
|
+
if (dots === 0) depth += 1;
|
|
8433
|
+
if (dots === 2) depth -= 1;
|
|
8434
|
+
if (depth < 0) return true;
|
|
8435
|
+
}
|
|
8436
|
+
return false;
|
|
8437
|
+
}
|
|
8438
|
+
function pathOf(name) {
|
|
8439
|
+
if (typeof name !== "string")
|
|
8440
|
+
return { invalid: FUNCTIONS_MESSAGES.invalidName };
|
|
8441
|
+
if (leavesFunctions(name)) return { invalid: FUNCTIONS_MESSAGES.dotSegments };
|
|
8442
|
+
try {
|
|
8443
|
+
return { path: functionPath(name) };
|
|
8444
|
+
} catch {
|
|
8445
|
+
return { invalid: FUNCTIONS_MESSAGES.unencodableName };
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
function isSignalOrNone(value) {
|
|
8449
|
+
if (value === void 0 || value === null) return true;
|
|
8450
|
+
if (typeof value !== "object") return false;
|
|
8451
|
+
const signal = value;
|
|
8452
|
+
return typeof signal.aborted === "boolean" && typeof signal.addEventListener === "function" && typeof signal.removeEventListener === "function";
|
|
8453
|
+
}
|
|
8454
|
+
function invalidOptions(message) {
|
|
8455
|
+
return failed(new FunctionsFetchError("INVALID_OPTIONS", message, null));
|
|
8456
|
+
}
|
|
8457
|
+
function failed(error) {
|
|
8458
|
+
return { data: null, error };
|
|
8459
|
+
}
|
|
8460
|
+
function fromNaroError(error, cause) {
|
|
8461
|
+
return new FunctionsFetchError(
|
|
8462
|
+
error.code,
|
|
8463
|
+
error.message,
|
|
8464
|
+
cause,
|
|
8465
|
+
error.details
|
|
8466
|
+
);
|
|
8467
|
+
}
|
|
8468
|
+
function failure(kind, cause, timeout) {
|
|
8469
|
+
if (kind === "timeout")
|
|
8470
|
+
return new FunctionsFetchError(
|
|
8471
|
+
"TIMEOUT",
|
|
8472
|
+
FUNCTIONS_MESSAGES.timeout(timeout ?? 0),
|
|
8473
|
+
cause
|
|
8474
|
+
);
|
|
8475
|
+
if (kind === "aborted")
|
|
8476
|
+
return new FunctionsFetchError(
|
|
8477
|
+
"ABORTED",
|
|
8478
|
+
FUNCTIONS_MESSAGES.aborted,
|
|
8479
|
+
cause
|
|
8480
|
+
);
|
|
8481
|
+
return fromNaroError(networkError(cause), cause);
|
|
8482
|
+
}
|
|
8483
|
+
function unreadable(cause, status, signal) {
|
|
8484
|
+
if (signal?.aborted)
|
|
8485
|
+
return new FunctionsFetchError(
|
|
8486
|
+
"ABORTED",
|
|
8487
|
+
FUNCTIONS_MESSAGES.aborted,
|
|
8488
|
+
cause
|
|
8489
|
+
);
|
|
8490
|
+
if (cause instanceof SyntaxError)
|
|
8491
|
+
return new FunctionsFetchError(
|
|
8492
|
+
"INVALID_RESPONSE",
|
|
8493
|
+
FUNCTIONS_MESSAGES.unreadable(status),
|
|
8494
|
+
cause
|
|
8495
|
+
);
|
|
8496
|
+
return fromNaroError(networkError(cause), cause);
|
|
8497
|
+
}
|
|
8498
|
+
async function relayError(response, head) {
|
|
8499
|
+
let body;
|
|
8500
|
+
try {
|
|
8501
|
+
body = await response.clone().json();
|
|
8502
|
+
} catch {
|
|
8503
|
+
body = void 0;
|
|
8504
|
+
}
|
|
8505
|
+
const { status } = response;
|
|
8506
|
+
const error = withRetryAfter(
|
|
8507
|
+
head && body === void 0 ? {
|
|
8508
|
+
code: "RELAY_ERROR",
|
|
8509
|
+
message: FUNCTIONS_MESSAGES.headRelay(status),
|
|
8510
|
+
details: { status }
|
|
8511
|
+
} : errorFromBody(status, body) ?? invalidResponse(status),
|
|
8512
|
+
response.headers
|
|
8513
|
+
);
|
|
8514
|
+
return new FunctionsRelayError(
|
|
8515
|
+
error.code,
|
|
8516
|
+
error.message,
|
|
8517
|
+
response,
|
|
8518
|
+
error.details
|
|
8519
|
+
);
|
|
8520
|
+
}
|
|
8521
|
+
var FunctionsClient = class {
|
|
8522
|
+
#http;
|
|
8523
|
+
#auth;
|
|
8524
|
+
constructor(http, auth) {
|
|
8525
|
+
this.#http = http;
|
|
8526
|
+
this.#auth = auth;
|
|
8527
|
+
}
|
|
8528
|
+
async invoke(name, options = {}) {
|
|
8529
|
+
const method = options.method ?? "POST";
|
|
8530
|
+
const { timeout, signal } = options;
|
|
8531
|
+
const target = pathOf(name);
|
|
8532
|
+
if ("invalid" in target) return invalidOptions(target.invalid);
|
|
8533
|
+
if (timeout !== void 0 && !(Number.isInteger(timeout) && timeout > 0 && timeout <= MAX_TIMER_MS))
|
|
8534
|
+
return invalidOptions(FUNCTIONS_MESSAGES.invalidTimeout);
|
|
8535
|
+
if (!isSignalOrNone(signal))
|
|
8536
|
+
return invalidOptions(FUNCTIONS_MESSAGES.invalidSignal);
|
|
8537
|
+
const verb = typeof method === "string" ? method.toUpperCase() : "";
|
|
8538
|
+
if (options.body !== void 0 && (verb === "GET" || verb === "HEAD"))
|
|
8539
|
+
return invalidOptions(FUNCTIONS_MESSAGES.bodyWithGetOrHead);
|
|
8540
|
+
let headers;
|
|
8541
|
+
try {
|
|
8542
|
+
headers = new Headers(options.headers);
|
|
8543
|
+
} catch (cause) {
|
|
8544
|
+
return failed(
|
|
8545
|
+
new FunctionsFetchError(
|
|
8546
|
+
"INVALID_OPTIONS",
|
|
8547
|
+
FUNCTIONS_MESSAGES.invalidHeaders,
|
|
8548
|
+
cause
|
|
8549
|
+
)
|
|
8550
|
+
);
|
|
8551
|
+
}
|
|
8552
|
+
const prepared = prepareBody(options.body);
|
|
8553
|
+
if (prepared === null)
|
|
8554
|
+
return failed(
|
|
8555
|
+
new FunctionsFetchError(
|
|
8556
|
+
"INVALID_BODY",
|
|
8557
|
+
FUNCTIONS_MESSAGES.invalidBody,
|
|
8558
|
+
null
|
|
8559
|
+
)
|
|
8560
|
+
);
|
|
8561
|
+
if (prepared.contentType !== null && !headers.has("content-type"))
|
|
8562
|
+
headers.set("content-type", prepared.contentType);
|
|
8563
|
+
const result = await fetchUnread(this.#http, {
|
|
8564
|
+
method,
|
|
8565
|
+
path: target.path,
|
|
8566
|
+
headers,
|
|
8567
|
+
body: prepared.body,
|
|
8568
|
+
timeoutMs: timeout,
|
|
8569
|
+
signal,
|
|
8570
|
+
// A sign-in the page URL carried goes first, as for a query (P2-6) —
|
|
8571
|
+
// within the call's timeout and signal; the exchange is not cut.
|
|
8572
|
+
ready: () => this.#auth.ready()
|
|
8573
|
+
});
|
|
8574
|
+
if ("refused" in result) return failed(fromNaroError(result.refused, null));
|
|
8575
|
+
if ("failed" in result)
|
|
8576
|
+
return failed(failure(result.failed, result.cause, timeout));
|
|
8577
|
+
const { response } = result;
|
|
8578
|
+
if (!response.ok) {
|
|
8579
|
+
const error = response.headers.get("x-relay-error") === "true" ? await relayError(response, verb === "HEAD") : new FunctionsHttpError(response);
|
|
8580
|
+
return { data: null, error, response };
|
|
8581
|
+
}
|
|
8582
|
+
const answer = await readAnswer(response, method);
|
|
8583
|
+
if ("unreadable" in answer)
|
|
8584
|
+
return {
|
|
8585
|
+
data: null,
|
|
8586
|
+
error: unreadable(answer.unreadable, response.status, signal),
|
|
8587
|
+
response
|
|
8588
|
+
};
|
|
8589
|
+
return { data: answer.data, error: null, response };
|
|
8590
|
+
}
|
|
8591
|
+
};
|
|
7742
8592
|
function isSpace2(char) {
|
|
7743
8593
|
return char.trim() === "";
|
|
7744
8594
|
}
|
|
@@ -8884,30 +9734,30 @@ var RealtimeSocket = class {
|
|
|
8884
9734
|
request(topic, event, payload, options = {}) {
|
|
8885
9735
|
const ref = options.ref ?? this.nextRef();
|
|
8886
9736
|
const turn = options.turn ?? "app";
|
|
8887
|
-
return new Promise((
|
|
9737
|
+
return new Promise((resolve9) => {
|
|
8888
9738
|
const lost = {
|
|
8889
9739
|
ok: false,
|
|
8890
9740
|
error: realtimeClosed(1006),
|
|
8891
9741
|
lost: true
|
|
8892
9742
|
};
|
|
8893
|
-
if (!this.isOpen()) return
|
|
9743
|
+
if (!this.isOpen()) return resolve9(lost);
|
|
8894
9744
|
let text;
|
|
8895
9745
|
try {
|
|
8896
9746
|
text = clientFrame(topic, event, payload, ref, options.joinRef);
|
|
8897
9747
|
} catch {
|
|
8898
|
-
return
|
|
9748
|
+
return resolve9(lost);
|
|
8899
9749
|
}
|
|
8900
9750
|
const bytes = utf8Length(text);
|
|
8901
9751
|
const limit = this.#frameLimit;
|
|
8902
9752
|
if (limit !== null && bytes > limit)
|
|
8903
|
-
return
|
|
9753
|
+
return resolve9({ ok: false, error: frameTooLarge(limit), lost: false });
|
|
8904
9754
|
if (turn === "app" && event !== "phx_leave" && !this.#goesInTime())
|
|
8905
|
-
return
|
|
9755
|
+
return resolve9({
|
|
8906
9756
|
ok: false,
|
|
8907
9757
|
error: clientFlooding(CONNECTION_FRAMES_PER_SECOND),
|
|
8908
9758
|
lost: false
|
|
8909
9759
|
});
|
|
8910
|
-
this.#replies.set(ref, { resolve:
|
|
9760
|
+
this.#replies.set(ref, { resolve: resolve9, timer: void 0, bytes, turn });
|
|
8911
9761
|
this.#enqueue({ text, bytes, ref, turn, presence: event === "presence" });
|
|
8912
9762
|
});
|
|
8913
9763
|
}
|
|
@@ -9231,19 +10081,19 @@ var RealtimeSocket = class {
|
|
|
9231
10081
|
#closed(ws, code, reason, waitedMs) {
|
|
9232
10082
|
if (this.#ws !== ws) return;
|
|
9233
10083
|
const wasOpen = this.#state === "open";
|
|
9234
|
-
const
|
|
10084
|
+
const timedOut2 = waitedMs !== null;
|
|
9235
10085
|
detach(ws);
|
|
9236
10086
|
this.#ws = null;
|
|
9237
10087
|
this.#clearTimers();
|
|
9238
|
-
if (
|
|
10088
|
+
if (timedOut2) closeQuietly(ws, 1e3);
|
|
9239
10089
|
if (!wasOpen) this.#unopened += 1;
|
|
9240
10090
|
const said = this.#said;
|
|
9241
10091
|
this.#said = null;
|
|
9242
|
-
if (!
|
|
9243
|
-
const action =
|
|
10092
|
+
if (!timedOut2 && code === 4008) this.#flooded = true;
|
|
10093
|
+
const action = timedOut2 ? { kind: "retry", minMs: 0 } : closeAction(code, said?.code ?? reason);
|
|
9244
10094
|
if (action.kind === "stop")
|
|
9245
10095
|
return this.#stop(said ?? realtimeStopped(code, reason));
|
|
9246
|
-
const error =
|
|
10096
|
+
const error = timedOut2 ? realtimeTimedOut(waitedMs) : said ?? realtimeClosed(code);
|
|
9247
10097
|
this.#failReplies(error);
|
|
9248
10098
|
this.#state = "waiting";
|
|
9249
10099
|
const delay = delayAfter(
|
|
@@ -9256,7 +10106,7 @@ var RealtimeSocket = class {
|
|
|
9256
10106
|
safely(
|
|
9257
10107
|
() => this.#config.events.dropped(
|
|
9258
10108
|
error,
|
|
9259
|
-
|
|
10109
|
+
timedOut2 ? "TIMED_OUT" : "CHANNEL_ERROR"
|
|
9260
10110
|
)
|
|
9261
10111
|
);
|
|
9262
10112
|
}
|
|
@@ -10026,9 +10876,9 @@ var RealtimeChannel = class {
|
|
|
10026
10876
|
*/
|
|
10027
10877
|
#inTurn(joinRef, wait, send2) {
|
|
10028
10878
|
if (!wait && this.#waiting.length === 0) return send2();
|
|
10029
|
-
return new Promise((
|
|
10879
|
+
return new Promise((resolve9) => {
|
|
10030
10880
|
this.#waiting.push(
|
|
10031
|
-
() =>
|
|
10881
|
+
() => resolve9(this.#joinRef === joinRef ? send2() : null)
|
|
10032
10882
|
);
|
|
10033
10883
|
});
|
|
10034
10884
|
}
|
|
@@ -10643,13 +11493,13 @@ async function uploadInParts(a) {
|
|
|
10643
11493
|
}).catch(() => void 0);
|
|
10644
11494
|
};
|
|
10645
11495
|
const etags = [];
|
|
10646
|
-
const
|
|
11496
|
+
const failure2 = {
|
|
10647
11497
|
error: signal?.aborted ? uploadAborted() : null
|
|
10648
11498
|
};
|
|
10649
11499
|
let next = 1;
|
|
10650
11500
|
let sentBytes = 0;
|
|
10651
11501
|
const onAbort = () => {
|
|
10652
|
-
|
|
11502
|
+
failure2.error ??= uploadAborted();
|
|
10653
11503
|
};
|
|
10654
11504
|
const tryPart = async (n, bytes) => {
|
|
10655
11505
|
const sent2 = await send(a.http, {
|
|
@@ -10678,18 +11528,18 @@ async function uploadInParts(a) {
|
|
|
10678
11528
|
progress(sentBytes);
|
|
10679
11529
|
return null;
|
|
10680
11530
|
}
|
|
10681
|
-
if (attempt22 === ATTEMPTS ||
|
|
11531
|
+
if (attempt22 === ATTEMPTS || failure2.error !== null || !retryable(tried.error, tried.status))
|
|
10682
11532
|
return tried.error;
|
|
10683
11533
|
await sleep2(waitAfter(tried.error, attempt22), signal);
|
|
10684
|
-
if (
|
|
11534
|
+
if (failure2.error !== null) return tried.error;
|
|
10685
11535
|
}
|
|
10686
11536
|
};
|
|
10687
11537
|
const worker = async () => {
|
|
10688
|
-
while (
|
|
11538
|
+
while (failure2.error === null && next <= partCount) {
|
|
10689
11539
|
const n = next;
|
|
10690
11540
|
next += 1;
|
|
10691
11541
|
const error = await sendPart(n);
|
|
10692
|
-
if (error !== null)
|
|
11542
|
+
if (error !== null) failure2.error ??= error;
|
|
10693
11543
|
}
|
|
10694
11544
|
};
|
|
10695
11545
|
signal?.addEventListener("abort", onAbort);
|
|
@@ -10697,9 +11547,9 @@ async function uploadInParts(a) {
|
|
|
10697
11547
|
Array.from({ length: Math.min(CONCURRENCY, partCount) }, worker)
|
|
10698
11548
|
);
|
|
10699
11549
|
signal?.removeEventListener("abort", onAbort);
|
|
10700
|
-
if (
|
|
11550
|
+
if (failure2.error !== null) {
|
|
10701
11551
|
await callOff();
|
|
10702
|
-
return fail(
|
|
11552
|
+
return fail(failure2.error);
|
|
10703
11553
|
}
|
|
10704
11554
|
const completed = await call(a.http, {
|
|
10705
11555
|
method: "POST",
|
|
@@ -10926,8 +11776,8 @@ var StorageFileApi = class {
|
|
|
10926
11776
|
const payload = payloadOf(body, options.contentType);
|
|
10927
11777
|
if (!payload.ok) return fail(payload.error);
|
|
10928
11778
|
const cache = `max-age=${options.cacheControl ?? "3600"}`;
|
|
10929
|
-
const
|
|
10930
|
-
if (
|
|
11779
|
+
const unreadable2 = headerProblem(options.contentType, cache);
|
|
11780
|
+
if (unreadable2 !== null) return fail(unreadable2);
|
|
10931
11781
|
const { signal } = options;
|
|
10932
11782
|
if (signal?.aborted) return fail(uploadAborted());
|
|
10933
11783
|
await this.#auth.ready();
|
|
@@ -11228,8 +12078,8 @@ var StorageFileApi = class {
|
|
|
11228
12078
|
const payload = payloadOf(body, given.contentType);
|
|
11229
12079
|
if (!payload.ok) return fail(payload.error);
|
|
11230
12080
|
const cache = `max-age=${given.cacheControl ?? "3600"}`;
|
|
11231
|
-
const
|
|
11232
|
-
if (
|
|
12081
|
+
const unreadable2 = headerProblem(given.contentType, cache);
|
|
12082
|
+
if (unreadable2 !== null) return fail(unreadable2);
|
|
11233
12083
|
const { signal } = given;
|
|
11234
12084
|
if (signal?.aborted) return fail(uploadAborted());
|
|
11235
12085
|
const uploaded = payload.size > MAX_SINGLE_UPLOAD_BYTES2 ? await uploadInParts({
|
|
@@ -11308,12 +12158,14 @@ var CONFIG_MESSAGES = {
|
|
|
11308
12158
|
bothKeys: "Pass createClient an anonKey or a serviceKey, not both.",
|
|
11309
12159
|
serviceKeyOffServer: SERVICE_KEY_OFF_SERVER_MESSAGE,
|
|
11310
12160
|
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."
|
|
12161
|
+
realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647.",
|
|
12162
|
+
accessTokenWithServiceKey: "Pass createClient an accessToken or a serviceKey, not both: the service key skips every policy, so the user's token would not count.",
|
|
12163
|
+
accessToken: "createClient's accessToken is a function that returns the user's access token, or null for none."
|
|
11312
12164
|
};
|
|
11313
12165
|
var SERVICE_KEY_PREFIX = "naro_sk_";
|
|
11314
|
-
var
|
|
12166
|
+
var MAX_TIMER_MS2 = 2147483647;
|
|
11315
12167
|
function isMilliseconds(value) {
|
|
11316
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <=
|
|
12168
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_TIMER_MS2;
|
|
11317
12169
|
}
|
|
11318
12170
|
function realtimeOptionsOf(value) {
|
|
11319
12171
|
if (value === void 0) return {};
|
|
@@ -11363,6 +12215,35 @@ function apiKeyOf(options) {
|
|
|
11363
12215
|
throw new Error(CONFIG_MESSAGES.serviceKeyOffServer);
|
|
11364
12216
|
return { key, service };
|
|
11365
12217
|
}
|
|
12218
|
+
function accessTokenOf(value) {
|
|
12219
|
+
if (value === void 0) return void 0;
|
|
12220
|
+
if (typeof value !== "function") throw new Error(CONFIG_MESSAGES.accessToken);
|
|
12221
|
+
const read = value;
|
|
12222
|
+
return {
|
|
12223
|
+
async get() {
|
|
12224
|
+
const token = await read();
|
|
12225
|
+
if (token === null || token === void 0 || token === "") return null;
|
|
12226
|
+
if (typeof token !== "string")
|
|
12227
|
+
throw new TypeError(`accessToken returned a ${typeof token}`);
|
|
12228
|
+
if (!/^[\x21-\x7E]+$/.test(token))
|
|
12229
|
+
throw new TypeError(
|
|
12230
|
+
"accessToken returned a string that is not a token"
|
|
12231
|
+
);
|
|
12232
|
+
return token;
|
|
12233
|
+
}
|
|
12234
|
+
};
|
|
12235
|
+
}
|
|
12236
|
+
function refusingAuth() {
|
|
12237
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
12238
|
+
get(_target, property) {
|
|
12239
|
+
if (typeof property === "symbol" || property === "then" || property === "toJSON" || property === "constructor" || property === "asymmetricMatch")
|
|
12240
|
+
return void 0;
|
|
12241
|
+
if (property === "toString") return Object.prototype.toString;
|
|
12242
|
+
if (property === "valueOf") return Object.prototype.valueOf;
|
|
12243
|
+
throw new Error(accessTokenAuthMessage(property));
|
|
12244
|
+
}
|
|
12245
|
+
});
|
|
12246
|
+
}
|
|
11366
12247
|
function queryRunner(http, auth) {
|
|
11367
12248
|
return async (query) => {
|
|
11368
12249
|
await auth.ready();
|
|
@@ -11391,7 +12272,14 @@ var NaroClient2 = class {
|
|
|
11391
12272
|
* them, as supabase-js does.
|
|
11392
12273
|
*/
|
|
11393
12274
|
storage;
|
|
12275
|
+
/**
|
|
12276
|
+
* The project's Naro Functions (design §9): `functions.invoke(name, {
|
|
12277
|
+
* body })`, as in supabase-js.
|
|
12278
|
+
*/
|
|
12279
|
+
functions;
|
|
11394
12280
|
#run;
|
|
12281
|
+
/** Made with `accessToken`: `auth` and `channel()` refuse. */
|
|
12282
|
+
#accessToken;
|
|
11395
12283
|
#newRealtime;
|
|
11396
12284
|
/**
|
|
11397
12285
|
* Made by the first channel(), not by the constructor: it listens to every
|
|
@@ -11402,37 +12290,38 @@ var NaroClient2 = class {
|
|
|
11402
12290
|
constructor(options) {
|
|
11403
12291
|
const baseUrl = baseUrlOf(options.url);
|
|
11404
12292
|
const { key: apiKey, service } = apiKeyOf(options);
|
|
12293
|
+
const accessToken = accessTokenOf(options.accessToken);
|
|
12294
|
+
if (accessToken !== void 0 && service)
|
|
12295
|
+
throw new Error(CONFIG_MESSAGES.accessTokenWithServiceKey);
|
|
11405
12296
|
if (options.fetch === void 0 && typeof globalThis.fetch !== "function")
|
|
11406
12297
|
throw new Error(CONFIG_MESSAGES.fetch);
|
|
11407
12298
|
const realtime = realtimeOptionsOf(options.realtime);
|
|
11408
|
-
const storage = options.auth?.storage ?? defaultStorage();
|
|
12299
|
+
const storage = accessToken !== void 0 ? memoryStorage() : options.auth?.storage ?? defaultStorage();
|
|
11409
12300
|
const http = {
|
|
11410
12301
|
baseUrl,
|
|
11411
12302
|
apiKey,
|
|
11412
12303
|
fetch: options.fetch,
|
|
11413
12304
|
tokens: new TokenStore(storage, storageKey(baseUrl)),
|
|
11414
|
-
|
|
11415
|
-
|
|
12305
|
+
service,
|
|
12306
|
+
...accessToken !== void 0 ? { accessToken } : { accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)) }
|
|
11416
12307
|
};
|
|
11417
|
-
|
|
12308
|
+
const auth = new AuthClient(http, {
|
|
11418
12309
|
verifiers: new FlowVerifiers(
|
|
11419
12310
|
new TokenStore(storage, verifierKey(baseUrl))
|
|
11420
12311
|
),
|
|
11421
12312
|
recovery: new RecoveryToken(
|
|
11422
12313
|
new TokenStore(storage, recoveryKey(baseUrl))
|
|
11423
12314
|
),
|
|
11424
|
-
detectSessionInUrl: options.auth?.detectSessionInUrl ?? true,
|
|
12315
|
+
detectSessionInUrl: accessToken === void 0 && (options.auth?.detectSessionInUrl ?? true),
|
|
11425
12316
|
shared: { storage, key: storageKey(baseUrl) },
|
|
11426
12317
|
serviceKey: service
|
|
11427
12318
|
});
|
|
11428
|
-
this.#
|
|
11429
|
-
this.
|
|
11430
|
-
this.#
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
options: realtime
|
|
11435
|
-
});
|
|
12319
|
+
this.#accessToken = accessToken !== void 0;
|
|
12320
|
+
this.auth = this.#accessToken ? refusingAuth() : auth;
|
|
12321
|
+
this.#run = queryRunner(http, auth);
|
|
12322
|
+
this.storage = new StorageClient(http, auth);
|
|
12323
|
+
this.functions = new FunctionsClient(http, auth);
|
|
12324
|
+
this.#newRealtime = () => new RealtimeClient({ http, auth, service, options: realtime });
|
|
11436
12325
|
}
|
|
11437
12326
|
/** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
|
|
11438
12327
|
from(table3) {
|
|
@@ -11444,6 +12333,7 @@ var NaroClient2 = class {
|
|
|
11444
12333
|
* `removeChannel`. Nothing connects until the first `subscribe()`.
|
|
11445
12334
|
*/
|
|
11446
12335
|
channel(name, options) {
|
|
12336
|
+
if (this.#accessToken) throw new Error(ACCESS_TOKEN_CHANNEL_MESSAGE);
|
|
11447
12337
|
return this.#client().channel(name, options);
|
|
11448
12338
|
}
|
|
11449
12339
|
/** The channels made with `channel()` and not yet removed. */
|
|
@@ -11455,7 +12345,9 @@ var NaroClient2 = class {
|
|
|
11455
12345
|
* one; the connection closes with the last channel.
|
|
11456
12346
|
*/
|
|
11457
12347
|
removeChannel(channel) {
|
|
11458
|
-
|
|
12348
|
+
if (this.#realtime === void 0)
|
|
12349
|
+
return Promise.resolve({ data: null, error: null });
|
|
12350
|
+
return this.#realtime.removeChannel(channel);
|
|
11459
12351
|
}
|
|
11460
12352
|
/** `removeChannel()` for every channel; the connection closes. */
|
|
11461
12353
|
removeAllChannels() {
|
|
@@ -11773,7 +12665,7 @@ function stopOnInterrupt(onFirst) {
|
|
|
11773
12665
|
};
|
|
11774
12666
|
}
|
|
11775
12667
|
async function writeAtomically(bytes, destination) {
|
|
11776
|
-
const partial =
|
|
12668
|
+
const partial = join6(dirname4(destination), `.${randomUUID()}.naro-partial`);
|
|
11777
12669
|
const stopListening = removeOnInterrupt(partial);
|
|
11778
12670
|
try {
|
|
11779
12671
|
await pipeline2(bytes, createWriteStream2(partial, { flags: "wx" }));
|
|
@@ -11814,7 +12706,7 @@ function progressReporter(output, total) {
|
|
|
11814
12706
|
};
|
|
11815
12707
|
}
|
|
11816
12708
|
async function upload(context, local, target) {
|
|
11817
|
-
const file =
|
|
12709
|
+
const file = resolve8(context.cwd, local);
|
|
11818
12710
|
const info = await stat(file).catch(() => null);
|
|
11819
12711
|
if (info === null)
|
|
11820
12712
|
throw new NaroError(
|
|
@@ -11948,10 +12840,10 @@ async function download(context, source, local) {
|
|
|
11948
12840
|
"VALIDATION_FAILED",
|
|
11949
12841
|
"naro storage cp downloads one object: name it, naro://<bucket>/<path>."
|
|
11950
12842
|
);
|
|
11951
|
-
const resolved =
|
|
12843
|
+
const resolved = resolve8(context.cwd, local);
|
|
11952
12844
|
const isDirectory = local.endsWith("/") || (await stat(resolved).catch(() => null))?.isDirectory() === true;
|
|
11953
|
-
const destination = isDirectory ?
|
|
11954
|
-
const folder = await stat(
|
|
12845
|
+
const destination = isDirectory ? join6(resolved, savedAs(source.path)) : resolved;
|
|
12846
|
+
const folder = await stat(dirname4(destination)).catch(() => null);
|
|
11955
12847
|
if (folder === null || !folder.isDirectory())
|
|
11956
12848
|
throw new NaroError(
|
|
11957
12849
|
"VALIDATION_FAILED",
|
|
@@ -12494,6 +13386,28 @@ Usage:
|
|
|
12494
13386
|
naro realtime tables remove <table>\u2026 [--yes]
|
|
12495
13387
|
Stop publishing tables' changes (asks first)
|
|
12496
13388
|
|
|
13389
|
+
naro functions [list] Naro Functions: name, version, verify_jwt, status and URL of each function
|
|
13390
|
+
naro functions new <name> Write naro/functions/<name>/index.ts, a handler to start from
|
|
13391
|
+
naro functions deploy [name\u2026] [--no-verify-jwt] [--import-map <file>] [--skip-check]
|
|
13392
|
+
Bundle on this machine (Node.js 22 or later), check that it starts, upload;
|
|
13393
|
+
every function in the folder when no name is given
|
|
13394
|
+
naro functions versions <name> Its stored versions, newest first
|
|
13395
|
+
naro functions rollback <name> [<version>]
|
|
13396
|
+
Put an earlier version live again (the one before the live one by default)
|
|
13397
|
+
naro functions delete <name> Delete it: its URL stops working (asks first; --yes to skip)
|
|
13398
|
+
naro functions logs <name> [--level <l>] [--follow]
|
|
13399
|
+
Its newest log lines
|
|
13400
|
+
naro functions secrets [list] The project's function secrets: names and digests, never values
|
|
13401
|
+
naro functions secrets set <NAME> Set one (value from a hidden prompt or stdin), then sync the functions
|
|
13402
|
+
(--no-sync leaves them to the control plane's sweep)
|
|
13403
|
+
naro functions secrets import <file>
|
|
13404
|
+
Set a dotenv file's lines (asks first; --yes in CI)
|
|
13405
|
+
naro functions secrets unset <NAME>\u2026
|
|
13406
|
+
Remove secrets (asks first)
|
|
13407
|
+
naro functions secrets sync [--registry]
|
|
13408
|
+
Upload out-of-date functions again until none is left; --registry also rewrites
|
|
13409
|
+
the function list in the project's database (after naro db restore)
|
|
13410
|
+
|
|
12497
13411
|
naro promote <deployment-id> [--to <environment>]
|
|
12498
13412
|
Publish a build to production, or to an environment with --to; it says what it will do, then asks.
|
|
12499
13413
|
A build moving between an environment and another surface is built again from its commit with the
|
|
@@ -12522,6 +13436,11 @@ Global options:
|
|
|
12522
13436
|
--browser <b> Only this browser: chrome, edge, firefox, opera, safari or other (naro analytics, naro insights)
|
|
12523
13437
|
--os <o> Only this operating system: android, ios, linux, macos, windows or other (naro analytics, naro insights)
|
|
12524
13438
|
--to <environment> production (the default) or an environment name: where naro promote publishes the build (naro promote)
|
|
13439
|
+
--no-verify-jwt Let calls in without a project JWT (naro functions deploy; wins over function.json)
|
|
13440
|
+
--import-map <file> The import map for functions whose folder has no deno.json (naro functions deploy)
|
|
13441
|
+
--skip-check Upload without booting the bundle in workerd first (naro functions deploy)
|
|
13442
|
+
--no-sync Leave the functions to the control plane's sweep (naro functions secrets set, import, unset; naro backend keys rotate)
|
|
13443
|
+
--registry Also rewrite the function list in the project's database (naro functions secrets sync)
|
|
12525
13444
|
--env <e> production or preview (naro logs, naro insights: production by default; naro env pull, naro deploy: preview by default, or an environment name);
|
|
12526
13445
|
production or an environment name for the data commands (naro db, migration, gen, backend, policy, storage, realtime: production by default)
|
|
12527
13446
|
--branch <b> naro environments create, set: the branch whose pushes deploy to that environment. naro deploy: the default
|
|
@@ -12536,7 +13455,8 @@ Global options:
|
|
|
12536
13455
|
--dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
|
|
12537
13456
|
--output <file> Write to a file instead of stdout (naro gen types, db dump)
|
|
12538
13457
|
--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)
|
|
13458
|
+
--dir <path> Migrations directory (default ./migrations, or <--root>/migrations) (naro db push \u2026);
|
|
13459
|
+
functions directory (default ./naro/functions, or <--root>/naro/functions) (naro functions \u2026)
|
|
12540
13460
|
--strict Fail on warnings too, not only errors (naro db lint)
|
|
12541
13461
|
--schema-only Dump the schema without rows (naro db dump)
|
|
12542
13462
|
--data-only Dump rows without the schema (naro db dump)
|
|
@@ -12745,9 +13665,65 @@ function routeRealtime(words) {
|
|
|
12745
13665
|
return null;
|
|
12746
13666
|
}
|
|
12747
13667
|
}
|
|
13668
|
+
function routeFunctions(words) {
|
|
13669
|
+
const [sub] = words;
|
|
13670
|
+
switch (sub) {
|
|
13671
|
+
case void 0:
|
|
13672
|
+
case "list":
|
|
13673
|
+
case "ls":
|
|
13674
|
+
return words.length <= 1 ? { handler: runFunctionsList, positionals: [] } : null;
|
|
13675
|
+
case "versions":
|
|
13676
|
+
return { handler: runFunctionsVersions, positionals: words.slice(1) };
|
|
13677
|
+
case "new":
|
|
13678
|
+
return { handler: runFunctionsNew, positionals: words.slice(1) };
|
|
13679
|
+
case "deploy":
|
|
13680
|
+
return { handler: runFunctionsDeploy, positionals: words.slice(1) };
|
|
13681
|
+
case "rollback":
|
|
13682
|
+
return { handler: runFunctionsRollback, positionals: words.slice(1) };
|
|
13683
|
+
case "delete":
|
|
13684
|
+
case "remove":
|
|
13685
|
+
case "rm":
|
|
13686
|
+
return { handler: runFunctionsDelete, positionals: words.slice(1) };
|
|
13687
|
+
case "logs":
|
|
13688
|
+
return { handler: runFunctionsLogs, positionals: words.slice(1) };
|
|
13689
|
+
case "secrets": {
|
|
13690
|
+
const action = words[1];
|
|
13691
|
+
switch (action) {
|
|
13692
|
+
case void 0:
|
|
13693
|
+
case "list":
|
|
13694
|
+
case "ls":
|
|
13695
|
+
return words.length <= 2 ? { handler: runFunctionsSecretsList, positionals: [] } : null;
|
|
13696
|
+
case "set":
|
|
13697
|
+
return {
|
|
13698
|
+
handler: runFunctionsSecretsSet,
|
|
13699
|
+
positionals: words.slice(2)
|
|
13700
|
+
};
|
|
13701
|
+
case "import":
|
|
13702
|
+
return {
|
|
13703
|
+
handler: runFunctionsSecretsImport,
|
|
13704
|
+
positionals: words.slice(2)
|
|
13705
|
+
};
|
|
13706
|
+
case "unset":
|
|
13707
|
+
return {
|
|
13708
|
+
handler: runFunctionsSecretsUnset,
|
|
13709
|
+
positionals: words.slice(2)
|
|
13710
|
+
};
|
|
13711
|
+
case "sync":
|
|
13712
|
+
return words.length === 2 ? { handler: runFunctionsSecretsSync, positionals: [] } : null;
|
|
13713
|
+
default:
|
|
13714
|
+
return null;
|
|
13715
|
+
}
|
|
13716
|
+
}
|
|
13717
|
+
default:
|
|
13718
|
+
return null;
|
|
13719
|
+
}
|
|
13720
|
+
}
|
|
12748
13721
|
async function refuseTo() {
|
|
12749
13722
|
throw new NaroError("VALIDATION_FAILED", "--to is only for naro promote.");
|
|
12750
13723
|
}
|
|
13724
|
+
async function refuseFunctionsEnv() {
|
|
13725
|
+
throw new NaroError("VALIDATION_FAILED", FUNCTIONS_ENV_REFUSAL);
|
|
13726
|
+
}
|
|
12751
13727
|
var DATA_COMMANDS = /* @__PURE__ */ new Set([
|
|
12752
13728
|
"db",
|
|
12753
13729
|
"migration",
|
|
@@ -12789,10 +13765,26 @@ function guidedError(error, parsed) {
|
|
|
12789
13765
|
error.message = usage === -1 ? forEnvironment(error.message, environment) : `${forEnvironment(error.message.slice(0, usage), environment)}${error.message.slice(usage)}`;
|
|
12790
13766
|
return error;
|
|
12791
13767
|
}
|
|
13768
|
+
function unknownFunctionsWords(words) {
|
|
13769
|
+
const [sub = "", action = "", extra = ""] = words;
|
|
13770
|
+
if (sub === "list" || sub === "ls") return [sub, action];
|
|
13771
|
+
if (sub === "secrets")
|
|
13772
|
+
return action === "list" || action === "ls" || action === "sync" ? [sub, action, extra] : [sub, action];
|
|
13773
|
+
return [sub];
|
|
13774
|
+
}
|
|
13775
|
+
function unknownCommandMessage(parsed) {
|
|
13776
|
+
const words = parsed.command === "functions" ? [
|
|
13777
|
+
"functions",
|
|
13778
|
+
...unknownFunctionsWords(parsed.positionals).map(printable)
|
|
13779
|
+
] : [parsed.command, ...parsed.positionals];
|
|
13780
|
+
return `Unknown command: ${words.join(" ")}. See \`naro help\`.`;
|
|
13781
|
+
}
|
|
12792
13782
|
function route(parsed) {
|
|
12793
13783
|
const routed = routeCommand(parsed);
|
|
12794
13784
|
if (routed && parsed.flags.to !== void 0 && parsed.command !== "promote" && parsed.command !== "rollback")
|
|
12795
13785
|
return { handler: refuseTo, positionals: routed.positionals };
|
|
13786
|
+
if (routed && parsed.command === "functions" && parsed.flags.env !== void 0)
|
|
13787
|
+
return { handler: refuseFunctionsEnv, positionals: routed.positionals };
|
|
12796
13788
|
const refusal = routed ? dataEnvRefusal(parsed) : null;
|
|
12797
13789
|
if (routed && refusal)
|
|
12798
13790
|
return {
|
|
@@ -13051,6 +14043,8 @@ function routeCommand(parsed) {
|
|
|
13051
14043
|
return routeStorage(positionals);
|
|
13052
14044
|
case "realtime":
|
|
13053
14045
|
return routeRealtime(positionals);
|
|
14046
|
+
case "functions":
|
|
14047
|
+
return routeFunctions(positionals);
|
|
13054
14048
|
case "policy":
|
|
13055
14049
|
switch (sub) {
|
|
13056
14050
|
case void 0:
|
|
@@ -13094,19 +14088,6 @@ function routeCommand(parsed) {
|
|
|
13094
14088
|
return null;
|
|
13095
14089
|
}
|
|
13096
14090
|
}
|
|
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
14091
|
|
|
13111
14092
|
// src/index.ts
|
|
13112
14093
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -13132,20 +14113,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
13132
14113
|
const output = new Output(parsed.flags.json, parsed.flags.debug);
|
|
13133
14114
|
if (parsed.unknown.length > 0) {
|
|
13134
14115
|
output.fail(
|
|
13135
|
-
new NaroError(
|
|
13136
|
-
"VALIDATION_FAILED",
|
|
13137
|
-
secretFlagProblem(parsed.unknown) ?? `Unknown option: ${parsed.unknown.join(", ")}. See \`naro help\`.`
|
|
13138
|
-
)
|
|
14116
|
+
new NaroError("VALIDATION_FAILED", unknownOptionProblem(parsed))
|
|
13139
14117
|
);
|
|
13140
14118
|
return;
|
|
13141
14119
|
}
|
|
13142
14120
|
const routed = route(parsed);
|
|
13143
14121
|
if (!routed) {
|
|
13144
14122
|
output.fail(
|
|
13145
|
-
new NaroError(
|
|
13146
|
-
"VALIDATION_FAILED",
|
|
13147
|
-
`Unknown command: ${[parsed.command, ...parsed.positionals].join(" ")}. See \`naro help\`.`
|
|
13148
|
-
)
|
|
14123
|
+
new NaroError("VALIDATION_FAILED", unknownCommandMessage(parsed))
|
|
13149
14124
|
);
|
|
13150
14125
|
return;
|
|
13151
14126
|
}
|