naro-cli 0.22.0 → 0.24.0

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