naro-cli 0.21.0 → 0.23.0

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