naro-cli 0.22.0 → 0.23.0

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