naro-cli 0.11.0 → 0.13.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
@@ -2,11 +2,20 @@
2
2
  import {
3
3
  BACKEND_STEPS,
4
4
  BOOKMARK_FORMAT_MESSAGE,
5
+ INDEX_SQL,
6
+ MAX_SINGLE_UPLOAD_BYTES,
7
+ NAMED_POLICY_SQL,
8
+ NAMED_POLICY_TABLE,
5
9
  NaroClient,
6
10
  NaroError,
7
11
  POLICY_ACTIONS,
8
12
  POLICY_RULES,
13
+ RESET_BACKEND_OUTDATED,
9
14
  RESTORE_OUTCOME_UNKNOWN_CODES,
15
+ SCHEMA_SQL,
16
+ SPEED_INSIGHTS_METRICS,
17
+ SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
18
+ STORAGE_OBJECTS_TABLE,
10
19
  buildResetPlan,
11
20
  cellText,
12
21
  clearSession,
@@ -15,11 +24,13 @@ import {
15
24
  controlPlaneUrl,
16
25
  countLintedTables,
17
26
  createAuthClient,
27
+ dataError,
18
28
  deployDirectory,
19
29
  detectGitInfo,
20
30
  ensureGitignoreHasNaro,
21
31
  formatRedirectEntry,
22
32
  generateTypes,
33
+ indexesFromRows,
23
34
  isBookmark,
24
35
  isExpired,
25
36
  isNaroError,
@@ -27,18 +38,22 @@ import {
27
38
  isTerminalStatus,
28
39
  lintSchema,
29
40
  listMigrationFiles,
41
+ loadPolicy,
30
42
  loadStoredSession,
31
43
  login,
32
44
  migrationNameMismatch,
33
45
  migrationsPendingError,
34
46
  nextMigrationFileName,
35
47
  openBrowser,
48
+ parseCacheControl,
36
49
  parseGitHubRemote,
37
50
  parsePolicyNode,
38
51
  parsePolicyStatement,
39
52
  parseRedirectEntry,
40
53
  pendingMigrationNames,
54
+ planPolicyMigration,
41
55
  policiesForTypes,
56
+ policiesLostByReset,
42
57
  projectPath,
43
58
  promptForMissingEnv,
44
59
  quoteIdent,
@@ -47,18 +62,26 @@ import {
47
62
  refreshStoredSession,
48
63
  removeProjectLink,
49
64
  renderRows,
65
+ resetFileRefusedMessage,
66
+ resetPoliciesLostMessage,
67
+ resetPoliciesWithTablesMessage,
50
68
  resolveAuth,
51
69
  resolveMigrationsDir,
52
70
  saveSession,
53
71
  schemaMigrationTitle,
54
72
  serializeDotenv,
55
73
  slugify,
74
+ snapshotFromRows,
56
75
  splitPolicyStatements,
76
+ splitStatements,
77
+ storedPolicySql,
57
78
  summarizeStatement,
58
79
  systemTablePredicate,
80
+ tableNames,
59
81
  toNaroError,
82
+ unreadablePolicyComment,
60
83
  writeProjectLink
61
- } from "./chunk-L7JOGWP3.js";
84
+ } from "./chunk-O4NJRDJE.js";
62
85
  import "./chunk-K3NQKI34.js";
63
86
 
64
87
  // src/args.ts
@@ -91,7 +114,13 @@ var BOOLEAN_FLAGS = {
91
114
  "--allow-unindexed": "allowUnindexed",
92
115
  "--api-key-stdin": "apiKeyStdin",
93
116
  "--client-secret-stdin": "clientSecretStdin",
94
- "--password-stdin": "passwordStdin"
117
+ "--password-stdin": "passwordStdin",
118
+ "--public": "public",
119
+ "--private": "private",
120
+ "--empty": "empty",
121
+ "--recursive": "recursive",
122
+ "-r": "recursive",
123
+ "--upsert": "upsert"
95
124
  };
96
125
  var REPEATED_FLAGS = {
97
126
  "--column": "columns"
@@ -107,6 +136,9 @@ var STRING_FLAGS = {
107
136
  "--type": "type",
108
137
  "--env": "env",
109
138
  "--level": "level",
139
+ "--metric": "metric",
140
+ "--range": "range",
141
+ "--route": "route",
110
142
  "--file": "file",
111
143
  "--output": "output",
112
144
  "--lang": "lang",
@@ -132,7 +164,11 @@ var STRING_FLAGS = {
132
164
  "--username": "username",
133
165
  "--email": "email",
134
166
  "--password": "password",
135
- "--min-length": "minLength"
167
+ "--min-length": "minLength",
168
+ "--file-size-limit": "fileSizeLimit",
169
+ "--allowed-mime-types": "allowedMimeTypes",
170
+ "--content-type": "contentType",
171
+ "--cache-control": "cacheControl"
136
172
  };
137
173
  var SECRET_FLAGS = {
138
174
  "--api-key": "--api-key-stdin",
@@ -318,7 +354,7 @@ var TtyPrompter = class {
318
354
  });
319
355
  try {
320
356
  return await new Promise(
321
- (resolve5) => rl.question(question, resolve5)
357
+ (resolve6) => rl.question(question, resolve6)
322
358
  );
323
359
  } finally {
324
360
  rl.close();
@@ -339,7 +375,7 @@ var TtyPrompter = class {
339
375
  });
340
376
  try {
341
377
  const answer = new Promise(
342
- (resolve5) => rl.question(question, resolve5)
378
+ (resolve6) => rl.question(question, resolve6)
343
379
  );
344
380
  muted = true;
345
381
  const value = await answer;
@@ -421,10 +457,10 @@ function createContext(flags, positionals = []) {
421
457
  positionals,
422
458
  output,
423
459
  prompter,
424
- positionalOrThrow: (count, usage) => {
425
- if (positionals.length < count)
460
+ positionalOrThrow: (count2, usage) => {
461
+ if (positionals.length < count2)
426
462
  throw new NaroError("VALIDATION_FAILED", usage);
427
- return positionals.slice(0, count);
463
+ return positionals.slice(0, count2);
428
464
  },
429
465
  auth,
430
466
  client: async () => {
@@ -450,8 +486,8 @@ function createContext(flags, positionals = []) {
450
486
  }
451
487
 
452
488
  // src/router.ts
453
- import { readFileSync as readFileSync4 } from "fs";
454
- import { dirname as dirname3, join as join5 } from "path";
489
+ import { readFileSync as readFileSync5 } from "fs";
490
+ import { dirname as dirname4, join as join7 } from "path";
455
491
  import { fileURLToPath } from "url";
456
492
 
457
493
  // src/commands/auth.ts
@@ -536,9 +572,12 @@ async function runToken(context) {
536
572
  }
537
573
 
538
574
  // src/commands/confirm.ts
575
+ function cannotAsk(context) {
576
+ return context.flags.json || !isInteractive();
577
+ }
539
578
  async function confirmOrAbort(context, question) {
540
579
  if (context.flags.yes) return;
541
- if (context.flags.json || !isInteractive()) {
580
+ if (cannotAsk(context)) {
542
581
  throw new NaroError(
543
582
  "CONFIRMATION_REQUIRED",
544
583
  `${question} Re-run with --yes to confirm.`
@@ -574,18 +613,29 @@ async function appliedMigrations(client, projectId) {
574
613
  throw error;
575
614
  }
576
615
  }
616
+ var UNPRINTABLE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
577
617
  function printable(text) {
578
- let out = "";
579
- for (const char of text) {
580
- const code = char.charCodeAt(0);
581
- const control = code < 32 || code >= 127 && code <= 159;
582
- out += control ? "?" : char;
583
- }
584
- return out;
618
+ return text.replace(UNPRINTABLE, "?");
585
619
  }
586
620
  function isPrintable(text) {
587
621
  return printable(text) === text;
588
622
  }
623
+ var UNSHOWN_WARNING_LINE = "(a line of this warning has characters the terminal cannot show; run with --json to see it exactly)";
624
+ function warningLines(message) {
625
+ return message.split("\n").map(
626
+ (line, index) => index === 0 ? `\u26A0 ${printable(line)}` : ` ${isPrintable(line) ? line : UNSHOWN_WARNING_LINE}`
627
+ );
628
+ }
629
+ function policyProblems(dir, names) {
630
+ return names.flatMap((name) => {
631
+ const split = splitStatements(readFileSync(join(dir, name), "utf8"));
632
+ if (!split.ok) return [];
633
+ const planned = planPolicyMigration(
634
+ split.statements.map((statement) => statement.text)
635
+ );
636
+ return planned.ok ? [] : [{ migration: name, message: planned.message }];
637
+ });
638
+ }
589
639
  function warningLine(warning) {
590
640
  if (warning === "out-of-order")
591
641
  return " \u26A0 older than an applied migration \u2014 db push will still apply it";
@@ -636,23 +686,42 @@ async function runMigrationList(context) {
636
686
  }
637
687
  async function applyMigrationFiles(context, client, projectId, dir, names) {
638
688
  const applied = [];
689
+ const warnings = [];
639
690
  for (const name of names) {
640
691
  const sql = readFileSync(join(dir, name), "utf8");
692
+ let answer;
641
693
  try {
642
- await client.db.applyMigration(projectId, { name, sql });
694
+ answer = await client.db.applyMigration(projectId, { name, sql });
643
695
  } catch (error) {
644
696
  if (!isNaroError(error)) throw error;
697
+ for (const message of serverWarnings(error.details))
698
+ for (const line of warningLines(message)) context.output.warn(line);
645
699
  const shown = context.output.json ? name : printable(name);
646
700
  throw new NaroError(error.code, `${shown}: ${error.message}`, {
647
701
  ...error.status === void 0 ? {} : { status: error.status },
648
- details: { ...error.details, migration: name, applied },
702
+ details: {
703
+ ...error.details,
704
+ migration: name,
705
+ applied,
706
+ appliedWarnings: warnings
707
+ },
649
708
  cause: error
650
709
  });
651
710
  }
652
711
  context.output.log(`\u2713 ${printable(name)}`);
653
712
  applied.push(name);
713
+ for (const message of answer.warnings) {
714
+ warnings.push({ migration: name, message });
715
+ for (const line of warningLines(message)) context.output.warn(line);
716
+ }
654
717
  }
655
- return applied;
718
+ return { applied, warnings };
719
+ }
720
+ function serverWarnings(details) {
721
+ const warnings = details?.warnings;
722
+ return Array.isArray(warnings) ? warnings.filter(
723
+ (warning) => typeof warning === "string"
724
+ ) : [];
656
725
  }
657
726
  var MIGRATION_NAME_RE = /^[A-Za-z0-9_.-]{1,200}\.sql$/;
658
727
  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)";
@@ -724,10 +793,15 @@ async function runMigrationUp(context) {
724
793
  return;
725
794
  }
726
795
  if (context.flags.dryRun) {
727
- context.output.result({ projectId, dryRun: true, pending }, [
796
+ const problems = policyProblems(dir, pending);
797
+ context.output.result({ projectId, dryRun: true, pending, problems }, [
728
798
  `Pending migrations (${String(pending.length)}):`,
729
- ...pending.map((name) => ` ${printable(name)}`)
799
+ ...pending.map((name) => ` ${printable(name)}`),
800
+ ...problems.map(
801
+ (problem) => `${printable(problem.migration)}: ${printable(problem.message)}`
802
+ )
730
803
  ]);
804
+ if (problems.length > 0) process.exitCode = 1;
731
805
  return;
732
806
  }
733
807
  context.output.log(`Pending migrations (${String(pending.length)}):`);
@@ -736,14 +810,14 @@ async function runMigrationUp(context) {
736
810
  context,
737
811
  `Apply ${String(pending.length)} migration(s) to the project database? Each file is applied all-or-nothing; a failure stops the run.`
738
812
  );
739
- const applied = await applyMigrationFiles(
813
+ const { applied, warnings } = await applyMigrationFiles(
740
814
  context,
741
815
  client,
742
816
  projectId,
743
817
  dir,
744
818
  pending
745
819
  );
746
- context.output.result({ projectId, applied, pending: [] }, [
820
+ context.output.result({ projectId, applied, pending: [], warnings }, [
747
821
  `\u2705 Applied ${String(applied.length)} migration(s).`
748
822
  ]);
749
823
  }
@@ -1055,9 +1129,9 @@ function adviceLines2(auth) {
1055
1129
  "Redirect URIs to register with each provider \u2014 the one for the origin your app passes to createClient({ url }):"
1056
1130
  );
1057
1131
  for (const provider of ["google", "github"]) {
1058
- const label2 = ` ${LABELS[provider]}: `;
1132
+ const label3 = ` ${LABELS[provider]}: `;
1059
1133
  callbacksOf(auth, provider).forEach(
1060
- (url, index) => lines.push(`${index === 0 ? label2 : " ".repeat(label2.length)}${url}`)
1134
+ (url, index) => lines.push(`${index === 0 ? label3 : " ".repeat(label3.length)}${url}`)
1061
1135
  );
1062
1136
  }
1063
1137
  return lines;
@@ -1084,11 +1158,11 @@ async function readSecret(context, input, given, flag, what, usage) {
1084
1158
  throw new NaroError("VALIDATION_FAILED", `No ${what} came in on stdin.`);
1085
1159
  return secret;
1086
1160
  }
1087
- async function change(context, send, done, after) {
1161
+ async function change(context, send2, done, after) {
1088
1162
  const projectId = await context.resolveProjectId();
1089
1163
  let answer;
1090
1164
  try {
1091
- answer = await send(projectId);
1165
+ answer = await send2(projectId);
1092
1166
  } catch (error) {
1093
1167
  if (isNaroError(error) && error.code === "TIMEOUT")
1094
1168
  throw new NaroError(
@@ -1353,9 +1427,9 @@ var processStdin3 = {
1353
1427
  stdinIsTty: () => process.stdin.isTTY === true,
1354
1428
  readStdin
1355
1429
  };
1356
- async function usersCall(call) {
1430
+ async function usersCall(call2) {
1357
1431
  try {
1358
- return await call();
1432
+ return await call2();
1359
1433
  } catch (error) {
1360
1434
  if (isNaroError(error) && error.code === "BACKEND_OUTDATED")
1361
1435
  throw new NaroError("BACKEND_OUTDATED", OUTDATED_MESSAGE, {
@@ -1658,6 +1732,8 @@ async function runDbQuery(context, deps = processStdin4) {
1658
1732
  });
1659
1733
  if (result.truncated) lines.push(ROW_CAP_NOTICE);
1660
1734
  context.output.result({ projectId, result }, lines);
1735
+ for (const warning of result.warnings ?? [])
1736
+ for (const line of warningLines(warning)) context.output.warn(line);
1661
1737
  }
1662
1738
 
1663
1739
  // src/commands/db-backup.ts
@@ -1910,7 +1986,7 @@ async function runDbRestore(context) {
1910
1986
  target = resolved.bookmark;
1911
1987
  at = resolved.timestamp ?? given.timestamp;
1912
1988
  }
1913
- const label2 = at === null ? `bookmark ${target}` : `${at} (bookmark ${target})`;
1989
+ const label3 = at === null ? `bookmark ${target}` : `${at} (bookmark ${target})`;
1914
1990
  if (context.flags.dryRun) {
1915
1991
  context.output.result(
1916
1992
  {
@@ -1918,13 +1994,13 @@ async function runDbRestore(context) {
1918
1994
  bookmark: target,
1919
1995
  ...at === null ? {} : { timestamp: at }
1920
1996
  },
1921
- [`Would restore to ${label2}. Nothing was changed.`]
1997
+ [`Would restore to ${label3}. Nothing was changed.`]
1922
1998
  );
1923
1999
  return;
1924
2000
  }
1925
2001
  await confirmOrAbort(
1926
2002
  context,
1927
- `Restore the project database to ${label2}? Everything written after that point is rolled back \u2014 migration history included \u2014 and queries running right now are cancelled.`
2003
+ `Restore the project database to ${label3}? Everything written after that point is rolled back \u2014 migration history included \u2014 and queries running right now are cancelled.`
1928
2004
  );
1929
2005
  const before = await bookmarkNow(context, client, projectId);
1930
2006
  let restored;
@@ -1998,6 +2074,75 @@ var DIAGNOSTICS_SQL = [
1998
2074
  "SELECT * FROM pragma_foreign_key_check;"
1999
2075
  ].join("\n");
2000
2076
  var TRUNCATED_NOTICE = "\u26A0 The diagnostics were cut at 10,000 rows \u2014 there may be more broken rows than these.";
2077
+ var POLICIES_TRUNCATED_NOTICE = "\u26A0 The policies were cut at 10,000 rows, so they were not checked.";
2078
+ var POLICY_TABLE_PROBE = `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '${NAMED_POLICY_TABLE}'`;
2079
+ var POLICY_READ_SQL = [NAMED_POLICY_SQL.list, INDEX_SQL, ...SCHEMA_SQL].map((sql) => `${sql};`).join("\n");
2080
+ function objectsOf(statement) {
2081
+ return statement.rows.map(
2082
+ (row) => Object.fromEntries(
2083
+ statement.columns.map((column, index) => [column, row[index]])
2084
+ )
2085
+ );
2086
+ }
2087
+ async function readPolicies(client, projectId, warn) {
2088
+ let backend;
2089
+ try {
2090
+ backend = await client.backend.info(projectId);
2091
+ } catch (error) {
2092
+ if (error instanceof NaroError && error.code === "BACKEND_DISABLED")
2093
+ return void 0;
2094
+ throw error;
2095
+ }
2096
+ if (backend.status === "none") return void 0;
2097
+ const probe = await client.db.query(projectId, {
2098
+ sql: POLICY_TABLE_PROBE,
2099
+ readOnly: true
2100
+ });
2101
+ if ((probe.statements[0]?.rows.length ?? 0) === 0) return void 0;
2102
+ const answer = await client.db.query(projectId, { sql: POLICY_READ_SQL });
2103
+ const [stored, indexes, ...schema] = answer.statements;
2104
+ const expected = SCHEMA_SQL.length + 2;
2105
+ if (stored === void 0 || indexes === void 0 || answer.statements.length !== expected)
2106
+ throw new NaroError(
2107
+ "DATABASE_QUERY_FAILED",
2108
+ `The policies could not be read: the database answered with ${String(answer.statements.length)} result sets instead of ${String(expected)}.`
2109
+ );
2110
+ if (answer.truncated) {
2111
+ warn(POLICIES_TRUNCATED_NOTICE);
2112
+ return void 0;
2113
+ }
2114
+ const policies = [];
2115
+ const unreadable = [];
2116
+ for (const row of objectsOf(stored)) {
2117
+ const policy = loadPolicy(row);
2118
+ if (policy === void 0) continue;
2119
+ if (policy.invalid !== null)
2120
+ unreadable.push({
2121
+ table: policy.table,
2122
+ name: policy.name,
2123
+ reason: policy.invalid
2124
+ });
2125
+ else
2126
+ policies.push({
2127
+ table: policy.table,
2128
+ name: policy.name,
2129
+ permissive: policy.permissive,
2130
+ command: policy.command,
2131
+ roles: policy.roles,
2132
+ using: policy.using,
2133
+ check: policy.check,
2134
+ owner: policy.owner
2135
+ });
2136
+ }
2137
+ const snapshot = snapshotFromRows(schema.flatMap(objectsOf));
2138
+ return {
2139
+ tables: tableNames(snapshot),
2140
+ policies,
2141
+ unreadable,
2142
+ snapshot,
2143
+ indexes: indexesFromRows(objectsOf(indexes))
2144
+ };
2145
+ }
2001
2146
  function firstCell(row) {
2002
2147
  const value = row[0];
2003
2148
  return value === null || value === void 0 ? "" : String(value);
@@ -2025,17 +2170,17 @@ async function runDbLint(context) {
2025
2170
  const projectId = await context.resolveProjectId();
2026
2171
  const tables = await client.db.tables(projectId);
2027
2172
  const answer = await client.db.query(projectId, { sql: DIAGNOSTICS_SQL });
2028
- const [triggers, objects, quick, broken] = answer.statements;
2029
- if (triggers === void 0 || objects === void 0 || quick === void 0 || broken === void 0)
2173
+ const [triggers, objects2, quick, broken] = answer.statements;
2174
+ if (triggers === void 0 || objects2 === void 0 || quick === void 0 || broken === void 0)
2030
2175
  throw new NaroError(
2031
2176
  "DATABASE_QUERY_FAILED",
2032
2177
  `The diagnostics could not be read: the database answered with ${String(answer.statements.length)} result sets instead of 4.`
2033
2178
  );
2034
2179
  const virtualTables = [];
2035
2180
  const shadow = /* @__PURE__ */ new Set();
2036
- for (const row of objects.rows) {
2037
- const name = cellText(objects, row, "name");
2038
- if (cellText(objects, row, "type") === "shadow") shadow.add(name);
2181
+ for (const row of objects2.rows) {
2182
+ const name = cellText(objects2, row, "name");
2183
+ if (cellText(objects2, row, "type") === "shadow") shadow.add(name);
2039
2184
  else virtualTables.push(name);
2040
2185
  }
2041
2186
  const foreignKeyCheck = broken.rows.map((row) => ({
@@ -2043,12 +2188,18 @@ async function runDbLint(context) {
2043
2188
  rowid: rowidOf(cellText(broken, row, "rowid")),
2044
2189
  parent: cellText(broken, row, "parent")
2045
2190
  }));
2191
+ const policies = await readPolicies(
2192
+ client,
2193
+ projectId,
2194
+ (message) => context.output.warn(message)
2195
+ );
2046
2196
  const input = {
2047
- tables: tables.filter((table) => !shadow.has(table.name)),
2197
+ tables: tables.filter((table2) => !shadow.has(table2.name)),
2048
2198
  foreignKeyCheck,
2049
2199
  quickCheck: quick.rows.map(firstCell),
2050
2200
  triggers: triggers.rows.map((row) => cellText(triggers, row, "name")),
2051
- virtualTables
2201
+ virtualTables,
2202
+ ...policies === void 0 ? {} : { policies }
2052
2203
  };
2053
2204
  const issues = lintSchema(input);
2054
2205
  const counted = countLintedTables(input);
@@ -2064,24 +2215,26 @@ async function runDbLint(context) {
2064
2215
  }
2065
2216
 
2066
2217
  // src/commands/db-reset.ts
2218
+ import { readFileSync as readFileSync3 } from "fs";
2219
+ import { join as join3 } from "path";
2067
2220
  var MAX_STATEMENTS = 50;
2068
2221
  var SCHEMA_FILTER = `${systemTablePredicate("name")} AND ${systemTablePredicate("tbl_name")}`;
2069
- var SCHEMA_SQL = [
2222
+ var SCHEMA_SQL2 = [
2070
2223
  `SELECT type, name FROM sqlite_master WHERE type IN ('table', 'view', 'trigger') AND ${SCHEMA_FILTER} ORDER BY name;`,
2071
2224
  "SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'virtual';",
2072
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('sqlite_sequence', 'd1_migrations');"
2225
+ `SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('sqlite_sequence', 'd1_migrations', '${NAMED_POLICY_TABLE}', '${STORAGE_OBJECTS_TABLE}');`
2073
2226
  ].join("\n");
2074
2227
  function literal(value) {
2075
2228
  return `'${value.replace(/'/g, "''")}'`;
2076
2229
  }
2077
- function chunk(items2, size) {
2230
+ function chunk(items3, size) {
2078
2231
  const pieces = [];
2079
- for (let index = 0; index < items2.length; index += size)
2080
- pieces.push(items2.slice(index, index + size));
2232
+ for (let index = 0; index < items3.length; index += size)
2233
+ pieces.push(items3.slice(index, index + size));
2081
2234
  return pieces;
2082
2235
  }
2083
2236
  async function readSchema(client, projectId) {
2084
- const schema = await client.db.query(projectId, { sql: SCHEMA_SQL });
2237
+ const schema = await client.db.query(projectId, { sql: SCHEMA_SQL2 });
2085
2238
  const [listed, virtual, internal] = schema.statements;
2086
2239
  if (listed === void 0 || virtual === void 0 || internal === void 0)
2087
2240
  throw new NaroError(
@@ -2094,7 +2247,7 @@ async function readSchema(client, projectId) {
2094
2247
  const virtualNames = new Set(
2095
2248
  virtual.rows.map((row) => cellText(virtual, row, "name").toLowerCase())
2096
2249
  );
2097
- const objects = listed.rows.map((row) => {
2250
+ const objects2 = listed.rows.map((row) => {
2098
2251
  const name = cellText(listed, row, "name");
2099
2252
  const type = cellText(listed, row, "type");
2100
2253
  if (type === "view" || type === "trigger") return { type, name };
@@ -2103,7 +2256,7 @@ async function readSchema(client, projectId) {
2103
2256
  name
2104
2257
  };
2105
2258
  });
2106
- const lookups = objects.filter((object) => object.type === "table" || object.type === "virtual").map(
2259
+ const lookups = objects2.filter((object) => object.type === "table" || object.type === "virtual").map(
2107
2260
  (object) => `SELECT ${literal(object.name)} AS child, "table" AS parent FROM pragma_foreign_key_list(${literal(object.name)});`
2108
2261
  );
2109
2262
  const foreignKeys = [];
@@ -2122,12 +2275,63 @@ async function readSchema(client, projectId) {
2122
2275
  });
2123
2276
  }
2124
2277
  return {
2125
- objects,
2278
+ objects: objects2,
2126
2279
  foreignKeys,
2127
2280
  internal: {
2128
2281
  sqliteSequence: internalNames.has("sqlite_sequence"),
2129
2282
  ledger: internalNames.has("d1_migrations")
2130
- }
2283
+ },
2284
+ policyTable: internalNames.has(NAMED_POLICY_TABLE),
2285
+ storageTable: internalNames.has(STORAGE_OBJECTS_TABLE)
2286
+ };
2287
+ }
2288
+ async function hasBackend(client, projectId) {
2289
+ try {
2290
+ return (await client.backend.info(projectId)).status !== "none";
2291
+ } catch (error) {
2292
+ if (isNaroError(error) && error.code === "BACKEND_DISABLED") return false;
2293
+ throw error;
2294
+ }
2295
+ }
2296
+ async function policyPlan(client, projectId, schema, dir, files) {
2297
+ if (!schema.policyTable) {
2298
+ if (await hasBackend(client, projectId))
2299
+ throw new NaroError("BACKEND_OUTDATED", RESET_BACKEND_OUTDATED);
2300
+ return { clearPolicies: false, lost: [], withTables: [] };
2301
+ }
2302
+ const answer = await client.db.query(projectId, {
2303
+ sql: NAMED_POLICY_SQL.list
2304
+ });
2305
+ const stored = answer.statements[0];
2306
+ if (stored === void 0)
2307
+ throw new NaroError(
2308
+ "DATABASE_QUERY_FAILED",
2309
+ "The policies could not be read: the database answered with no result set."
2310
+ );
2311
+ if (answer.truncated)
2312
+ throw new NaroError(
2313
+ "DATABASE_QUERY_FAILED",
2314
+ "The policies are too many to read in one pass: the list was cut at 10,000 rows, so the reset cannot tell which ones your migration files do not make."
2315
+ );
2316
+ const current = stored.rows.map((row) => ({
2317
+ table: cellText(stored, row, "table_name"),
2318
+ name: cellText(stored, row, "name"),
2319
+ ...storedPolicySql(
2320
+ Object.fromEntries(
2321
+ stored.columns.map((column, index) => [column, row[index]])
2322
+ )
2323
+ )
2324
+ }));
2325
+ return {
2326
+ clearPolicies: true,
2327
+ ...policiesLostByReset({
2328
+ current,
2329
+ tables: schema.tables,
2330
+ files: files.map((name) => readFileSync3(join3(dir, name), "utf8")),
2331
+ // Storage's table stays; its policies are emptied with the rest (PR
2332
+ // #85 리뷰).
2333
+ storage: schema.storageTable
2334
+ })
2131
2335
  };
2132
2336
  }
2133
2337
  function resetStopped(context, error, bookmark, lead, message) {
@@ -2149,16 +2353,75 @@ async function runDbReset(context) {
2149
2353
  const dir = migrationsDir(context);
2150
2354
  const shownDir = relativeToCwd(context, dir);
2151
2355
  const files = listMigrationFiles(dir);
2356
+ const problems = policyProblems(dir, files);
2357
+ const problemLines = problems.map(
2358
+ (problem) => `${context.output.json ? problem.migration : printable(problem.migration)}: ${context.output.json ? problem.message : printable(problem.message)}`
2359
+ );
2360
+ if (problems.length > 0 && !context.flags.dryRun)
2361
+ throw new NaroError(
2362
+ "VALIDATION_FAILED",
2363
+ `The control plane would refuse these migration files, so the reset would stop at the first of them with the schema already dropped. Nothing was changed:
2364
+ ${problemLines.join("\n")}`,
2365
+ { details: { problems } }
2366
+ );
2152
2367
  const client = await context.client();
2153
2368
  const projectId = await context.resolveProjectId();
2154
2369
  const bookmark = context.flags.dryRun ? null : await bookmarkNow(context, client, projectId, "resetting");
2155
- const plan = buildResetPlan(await readSchema(client, projectId));
2370
+ const { policyTable: policyTable2, storageTable, ...input } = await readSchema(
2371
+ client,
2372
+ projectId
2373
+ );
2374
+ const plan = buildResetPlan(input, { clearPolicies: policyTable2 });
2156
2375
  const { tables, views, triggers } = plan.dropped;
2157
2376
  const counts = `${String(tables.length)} table(s), ${String(views.length)} view(s) and ${String(triggers.length)} trigger(s)`;
2158
2377
  const dropped = tables.length + views.length + triggers.length;
2378
+ const policies = await policyPlan(
2379
+ client,
2380
+ projectId,
2381
+ { policyTable: policyTable2, storageTable, tables },
2382
+ dir,
2383
+ files
2384
+ );
2385
+ const policiesNotInFiles = policies.lost.map((policy) => ({
2386
+ name: `${policy.table}.${policy.name}`,
2387
+ sql: policy.sql
2388
+ }));
2389
+ const policiesOfTablesNotInFiles = policies.withTables.map(
2390
+ (policy) => `${policy.table}.${policy.name}`
2391
+ );
2392
+ const refused = policies.refused && {
2393
+ migration: files[policies.refused.file] ?? "",
2394
+ message: policies.refused.reason
2395
+ };
2396
+ if (refused && !problems.some((one) => one.migration === refused.migration))
2397
+ context.output.warn(
2398
+ `\u26A0 ${printable(resetFileRefusedMessage(refused.migration, refused.message))}`
2399
+ );
2400
+ if (policies.lost.length > 0) {
2401
+ context.output.warn(
2402
+ `\u26A0 ${printable(resetPoliciesLostMessage(policies.lost))}`
2403
+ );
2404
+ for (const policy of policies.lost)
2405
+ for (const line of policy.statements.every(isPrintable) ? policy.statements : [UNSHOWN_WARNING_LINE])
2406
+ context.output.warn(` ${line}`);
2407
+ }
2408
+ if (policies.withTables.length > 0)
2409
+ context.output.warn(
2410
+ `\u26A0 ${printable(resetPoliciesWithTablesMessage(policies.withTables))}`
2411
+ );
2159
2412
  if (context.flags.dryRun) {
2160
2413
  context.output.result(
2161
- { dryRun: true, tables, views, triggers, migrations: files },
2414
+ {
2415
+ dryRun: true,
2416
+ tables,
2417
+ views,
2418
+ triggers,
2419
+ migrations: files,
2420
+ policiesNotInFiles,
2421
+ policiesOfTablesNotInFiles,
2422
+ problems,
2423
+ ...refused ? { wouldBeRefused: refused } : {}
2424
+ },
2162
2425
  [
2163
2426
  `Would drop ${counts}:`,
2164
2427
  ...views.map((name) => ` ${"view".padEnd(9)}${printable(name)}`),
@@ -2166,9 +2429,11 @@ async function runDbReset(context) {
2166
2429
  ...tables.map((name) => ` ${"table".padEnd(9)}${printable(name)}`),
2167
2430
  `Would re-apply ${String(files.length)} migration file(s) from ${shownDir}:`,
2168
2431
  ...files.map((name) => ` ${printable(name)}`),
2169
- "Nothing was changed."
2432
+ "Nothing was changed.",
2433
+ ...problemLines
2170
2434
  ]
2171
2435
  );
2436
+ if (problems.length > 0 || refused) process.exitCode = 1;
2172
2437
  return;
2173
2438
  }
2174
2439
  if (bookmark !== null)
@@ -2200,15 +2465,16 @@ async function runDbReset(context) {
2200
2465
  `No migration files in ${shownDir} \u2014 the database is now empty.`
2201
2466
  );
2202
2467
  let reapplied = [];
2468
+ let warnings = [];
2203
2469
  try {
2204
2470
  if (files.length > 0)
2205
- reapplied = await applyMigrationFiles(
2471
+ ({ applied: reapplied, warnings } = await applyMigrationFiles(
2206
2472
  context,
2207
2473
  client,
2208
2474
  projectId,
2209
2475
  dir,
2210
2476
  files
2211
- );
2477
+ ));
2212
2478
  } catch (error) {
2213
2479
  throw resetStopped(
2214
2480
  context,
@@ -2218,18 +2484,28 @@ async function runDbReset(context) {
2218
2484
  (underlying) => underlying
2219
2485
  );
2220
2486
  }
2221
- context.output.result({ dropped, reapplied: reapplied.length, bookmark }, [
2222
- `\u2713 Reset the project database: dropped ${String(dropped)} object(s), re-applied ${String(reapplied.length)} migration(s).`,
2223
- ...bookmark === null ? [] : [` To undo: naro db restore --bookmark ${bookmark}`]
2224
- ]);
2487
+ context.output.result(
2488
+ {
2489
+ dropped,
2490
+ reapplied: reapplied.length,
2491
+ bookmark,
2492
+ policiesNotInFiles,
2493
+ policiesOfTablesNotInFiles,
2494
+ warnings
2495
+ },
2496
+ [
2497
+ `\u2713 Reset the project database: dropped ${String(dropped)} object(s), re-applied ${String(reapplied.length)} migration(s).`,
2498
+ ...bookmark === null ? [] : [` To undo: naro db restore --bookmark ${bookmark}`]
2499
+ ]
2500
+ );
2225
2501
  }
2226
2502
 
2227
2503
  // src/commands/db-schema.ts
2228
2504
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
2229
- import { join as join3 } from "path";
2505
+ import { join as join4 } from "path";
2230
2506
 
2231
2507
  // src/commands/policy.ts
2232
- import { readFileSync as readFileSync3 } from "fs";
2508
+ import { readFileSync as readFileSync4 } from "fs";
2233
2509
  import { resolve as resolve3 } from "path";
2234
2510
 
2235
2511
  // ../../packages/naro-sdk/src/query/filters.ts
@@ -2535,9 +2811,9 @@ function keyword(text, at) {
2535
2811
  `needs null, true or false after is at character ${String(at + 1)}`
2536
2812
  );
2537
2813
  }
2538
- function attempt(parse) {
2814
+ function attempt(parse2) {
2539
2815
  try {
2540
- return { ok: true, node: parse() };
2816
+ return { ok: true, node: parse2() };
2541
2817
  } catch (error) {
2542
2818
  if (error instanceof Refusal) return { ok: false, reason: error.message };
2543
2819
  throw error;
@@ -2644,10 +2920,10 @@ function conditionsFrom(flags) {
2644
2920
  );
2645
2921
  return conditions;
2646
2922
  }
2647
- function patchFromFile(context, path, table) {
2923
+ function patchFromFile(context, path, table2) {
2648
2924
  let text;
2649
2925
  try {
2650
- text = readFileSync3(resolve3(context.cwd, path), "utf8");
2926
+ text = readFileSync4(resolve3(context.cwd, path), "utf8");
2651
2927
  } catch {
2652
2928
  refuse2(`Could not read ${path}.`);
2653
2929
  }
@@ -2666,9 +2942,9 @@ function patchFromFile(context, path, table) {
2666
2942
  if (Object.hasOwn(body, "table")) {
2667
2943
  if (typeof named !== "string")
2668
2944
  refuse2(`${path}: table must be a string, the table's name.`);
2669
- if (table !== void 0 && asciiLower(named) !== asciiLower(table))
2945
+ if (table2 !== void 0 && asciiLower(named) !== asciiLower(table2))
2670
2946
  refuse2(
2671
- `${path} is the policy of ${printable(named)}, not ${printable(table)}. Remove its "table" to set it on ${printable(table)}.`
2947
+ `${path} is the policy of ${printable(named)}, not ${printable(table2)}. Remove its "table" to set it on ${printable(table2)}.`
2672
2948
  );
2673
2949
  }
2674
2950
  const allowed = /* @__PURE__ */ new Set([...POLICY_ACTIONS, "ownerColumn"]);
@@ -2681,7 +2957,7 @@ function patchFromFile(context, path, table) {
2681
2957
  function asciiLower(text) {
2682
2958
  return text.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
2683
2959
  }
2684
- function policyPatch(flags, context, table) {
2960
+ function policyPatch(flags, context, table2) {
2685
2961
  if (flags.fromFile !== void 0) {
2686
2962
  if (OTHER_FLAGS.some(
2687
2963
  (key) => flags[key] !== void 0 && flags[key] !== false
@@ -2690,7 +2966,7 @@ function policyPatch(flags, context, table) {
2690
2966
  "--from-file takes the whole policy: leave out the other policy options."
2691
2967
  );
2692
2968
  if (!context) refuse2(SET_USAGE);
2693
- return patchFromFile(context, flags.fromFile, table);
2969
+ return patchFromFile(context, flags.fromFile, table2);
2694
2970
  }
2695
2971
  const patch = {};
2696
2972
  for (const action of POLICY_ACTIONS) {
@@ -2763,9 +3039,9 @@ function leafText(leaf, negated) {
2763
3039
  if (leaf.operator === "in" || short !== void 0) {
2764
3040
  if (!Array.isArray(leaf.value)) return void 0;
2765
3041
  const slot = short === void 0 ? "in" : "array";
2766
- const items2 = leaf.value.map((item) => valueText(item, slot));
2767
- if (items2.some((item) => item === void 0)) return void 0;
2768
- const list = items2.join(",");
3042
+ const items3 = leaf.value.map((item) => valueText(item, slot));
3043
+ if (items3.some((item) => item === void 0)) return void 0;
3044
+ const list = items3.join(",");
2769
3045
  return slot === "in" ? `${prefix}in.(${list})` : `${prefix}${String(short)}.{${list}}`;
2770
3046
  }
2771
3047
  if (Array.isArray(leaf.value)) return void 0;
@@ -2829,7 +3105,7 @@ function policyTable(policies) {
2829
3105
  const widths = header.map(
2830
3106
  (title, index) => Math.max(title.length, ...rows.map((row) => row[index]?.length ?? 0))
2831
3107
  );
2832
- const table = [header, ...rows].map(
3108
+ const table2 = [header, ...rows].map(
2833
3109
  (cells) => cells.map(
2834
3110
  (cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)
2835
3111
  ).join(" ").trimEnd()
@@ -2846,10 +3122,10 @@ function policyTable(policies) {
2846
3122
  }
2847
3123
  )
2848
3124
  );
2849
- if (lines.length === 0) return table;
3125
+ if (lines.length === 0) return table2;
2850
3126
  const width = Math.max(...lines.map((line) => line.label.length));
2851
3127
  return [
2852
- ...table,
3128
+ ...table2,
2853
3129
  "",
2854
3130
  ...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
2855
3131
  ];
@@ -2858,43 +3134,47 @@ var UNSHOWN = "has characters that cannot be shown here";
2858
3134
  function statementLine(policy) {
2859
3135
  if (policy.invalid !== null)
2860
3136
  return printable(
2861
- `-- ${policyNamed(policy.name, policy.table)} cannot be read (${commentText(policy.invalid)}); the requests it applies to are refused until it is dropped or created again.`
3137
+ unreadablePolicyComment({
3138
+ name: policy.name,
3139
+ table: policy.table,
3140
+ invalid: policy.invalid
3141
+ })
2862
3142
  );
2863
3143
  return shownStatement(policy, policy.table, "naro policy list --json");
2864
3144
  }
2865
- function policyNamed(name, table) {
2866
- return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table))}`;
3145
+ function policyNamed(name, table2) {
3146
+ return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table2))}`;
2867
3147
  }
2868
- function shownStatement(statement, table, exact) {
3148
+ function shownStatement(statement, table2, exact) {
2869
3149
  if (isPrintable(statement.sql)) return statement.sql;
2870
3150
  return printable(
2871
- `-- ${policyNamed(statement.name, table)} ${UNSHOWN}, so it is not printed as CREATE POLICY: ${exact} has its exact text.`
3151
+ `-- ${policyNamed(statement.name, table2)} ${UNSHOWN}, so it is not printed as CREATE POLICY: ${exact} has its exact text.`
2872
3152
  );
2873
3153
  }
2874
3154
  function policyStatements(policies) {
2875
3155
  const lines = [];
2876
- let table;
3156
+ let table2;
2877
3157
  for (const policy of policies) {
2878
- if (table !== void 0 && policy.table !== table) lines.push("");
2879
- table = policy.table;
3158
+ if (table2 !== void 0 && policy.table !== table2) lines.push("");
3159
+ table2 = policy.table;
2880
3160
  lines.push(statementLine(policy));
2881
3161
  }
2882
3162
  return lines;
2883
3163
  }
2884
3164
  async function runPolicyList(context) {
2885
- const table = context.flags.table;
3165
+ const table2 = context.flags.table;
2886
3166
  const client = await context.client();
2887
3167
  const projectId = await context.resolveProjectId();
2888
3168
  const policies = await client.backend.rlsPolicies(
2889
3169
  projectId,
2890
- table === void 0 ? {} : { table }
3170
+ table2 === void 0 ? {} : { table: table2 }
2891
3171
  );
2892
3172
  let lines;
2893
3173
  if (policies.length > 0)
2894
3174
  lines = [...policyStatements(policies), "", DEFAULT_DENY];
2895
- else if (table !== void 0)
3175
+ else if (table2 !== void 0)
2896
3176
  lines = [
2897
- `No policies on ${printable(table)}: anon and signed-in requests to it are denied.`
3177
+ `No policies on ${printable(table2)}: anon and signed-in requests to it are denied.`
2898
3178
  ];
2899
3179
  else
2900
3180
  lines = [
@@ -2911,7 +3191,7 @@ function statementText(context) {
2911
3191
  let text = positional;
2912
3192
  if (file !== void 0) {
2913
3193
  try {
2914
- text = readFileSync3(resolve3(context.cwd, file), "utf8");
3194
+ text = readFileSync4(resolve3(context.cwd, file), "utf8");
2915
3195
  } catch {
2916
3196
  refuse2(`Could not read ${file}.`);
2917
3197
  }
@@ -2990,17 +3270,17 @@ async function runPolicyCreate(context) {
2990
3270
  }
2991
3271
  async function runPolicyDrop(context) {
2992
3272
  const [name] = context.positionalOrThrow(1, DROP_USAGE);
2993
- const table = context.flags.table;
2994
- if (table === void 0 || table === "") refuse2(DROP_USAGE);
3273
+ const table2 = context.flags.table;
3274
+ if (table2 === void 0 || table2 === "") refuse2(DROP_USAGE);
2995
3275
  await confirmOrAbort(
2996
3276
  context,
2997
- `Drop policy ${printable(name)} on ${printable(table)}? It stops applying within about 10 seconds.`
3277
+ `Drop policy ${printable(name)} on ${printable(table2)}? It stops applying within about 10 seconds.`
2998
3278
  );
2999
3279
  const client = await context.client();
3000
3280
  const projectId = await context.resolveProjectId();
3001
3281
  const dropped = await client.backend.dropRlsPolicy(
3002
3282
  projectId,
3003
- table,
3283
+ table2,
3004
3284
  name
3005
3285
  );
3006
3286
  context.output.result({ projectId, dropped }, [
@@ -3008,13 +3288,13 @@ async function runPolicyDrop(context) {
3008
3288
  ]);
3009
3289
  }
3010
3290
  async function runPolicySet(context) {
3011
- const [table] = context.positionalOrThrow(1, SET_USAGE);
3012
- const patch = policyPatch(context.flags, context, table);
3291
+ const [table2] = context.positionalOrThrow(1, SET_USAGE);
3292
+ const patch = policyPatch(context.flags, context, table2);
3013
3293
  const client = await context.client();
3014
3294
  const projectId = await context.resolveProjectId();
3015
3295
  const policy = await client.backend.setPolicy(
3016
3296
  projectId,
3017
- table,
3297
+ table2,
3018
3298
  patch
3019
3299
  );
3020
3300
  let named;
@@ -3035,14 +3315,14 @@ async function runPolicySet(context) {
3035
3315
  );
3036
3316
  }
3037
3317
  async function runPolicyRemove(context) {
3038
- const [table] = context.positionalOrThrow(1, REMOVE_USAGE);
3318
+ const [table2] = context.positionalOrThrow(1, REMOVE_USAGE);
3039
3319
  await confirmOrAbort(
3040
3320
  context,
3041
- `Remove every policy on ${printable(table)}? Anon and signed-in requests to it are denied from then on.`
3321
+ `Remove every policy on ${printable(table2)}? Anon and signed-in requests to it are denied from then on.`
3042
3322
  );
3043
3323
  const client = await context.client();
3044
3324
  const projectId = await context.resolveProjectId();
3045
- const policy = await client.backend.removePolicy(projectId, table);
3325
+ const policy = await client.backend.removePolicy(projectId, table2);
3046
3326
  const names = policy.policies ?? [];
3047
3327
  context.output.result({ projectId, removed: true, policy }, [
3048
3328
  names.length === 0 ? `\u2705 Removed the policy for ${printable(policy.table)}. Anon and signed-in requests to it are denied within about 10 seconds.` : `\u2705 Removed ${names.length === 1 ? "policy" : "policies"} ${names.map(printable).join(", ")} on ${printable(policy.table)}. Anon and signed-in requests to it are denied within about 10 seconds.`
@@ -3129,14 +3409,14 @@ function parseColumnSpec(spec) {
3129
3409
  column.default = defaultValue(value);
3130
3410
  else if (value !== void 0 && key === "references") {
3131
3411
  const dot = value.lastIndexOf(".");
3132
- const table = dot < 0 ? "" : value.slice(0, dot);
3412
+ const table2 = dot < 0 ? "" : value.slice(0, dot);
3133
3413
  const target = dot < 0 ? "" : value.slice(dot + 1);
3134
- if (!table || !target)
3414
+ if (!table2 || !target)
3135
3415
  throw new NaroError(
3136
3416
  "VALIDATION_FAILED",
3137
3417
  `${shown}: references needs <table>.<column>.`
3138
3418
  );
3139
- column.references = { table, column: target };
3419
+ column.references = { table: table2, column: target };
3140
3420
  } else if (value !== void 0 && key === "ondelete") {
3141
3421
  onDelete = ON_DELETE[value.toLowerCase()];
3142
3422
  if (!onDelete)
@@ -3273,13 +3553,13 @@ async function runSchemaChange(context, operations, question) {
3273
3553
  let file = null;
3274
3554
  let fileWarning;
3275
3555
  const relDir = relativeToCwd(context, dir);
3276
- const rel = join3(relDir === "." ? "" : relDir, name);
3556
+ const rel = join4(relDir === "." ? "" : relDir, name);
3277
3557
  if (context.flags.noWrite) {
3278
3558
  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.`;
3279
3559
  } else {
3280
3560
  try {
3281
3561
  mkdirSync3(dir, { recursive: true });
3282
- writeFileSync3(join3(dir, name), result.sql, { flag: "wx" });
3562
+ writeFileSync3(join4(dir, name), result.sql, { flag: "wx" });
3283
3563
  file = rel;
3284
3564
  } catch (error) {
3285
3565
  const reason = (error instanceof Error ? error.message : String(error)).replace(/[.\s]+$/, "");
@@ -3315,16 +3595,16 @@ async function runDbIndexUsage() {
3315
3595
  throw new NaroError("VALIDATION_FAILED", USAGE2.index);
3316
3596
  }
3317
3597
  async function runDbTableCreate(context) {
3318
- const [table] = context.positionals;
3598
+ const [table2] = context.positionals;
3319
3599
  const specs = columnsFlag(context);
3320
- if (!table || specs.length === 0)
3600
+ if (!table2 || specs.length === 0)
3321
3601
  throw new NaroError("VALIDATION_FAILED", USAGE2.create);
3322
3602
  const columns = specs.map(parseColumnSpec);
3323
3603
  const owner = context.flags.ownerColumn;
3324
3604
  await runSchemaChange(context, [
3325
3605
  {
3326
3606
  op: "createTable",
3327
- table,
3607
+ table: table2,
3328
3608
  columns,
3329
3609
  ...context.flags.ifNotExists ? { ifNotExists: true } : {},
3330
3610
  ...owner === void 0 ? {} : { owner: { column: owner } }
@@ -3332,56 +3612,56 @@ async function runDbTableCreate(context) {
3332
3612
  ]);
3333
3613
  }
3334
3614
  async function runDbTableAddColumn(context) {
3335
- const [table] = context.positionals;
3615
+ const [table2] = context.positionals;
3336
3616
  const specs = columnsFlag(context);
3337
3617
  const [spec] = specs;
3338
- if (!table || spec === void 0 || specs.length !== 1)
3618
+ if (!table2 || spec === void 0 || specs.length !== 1)
3339
3619
  throw new NaroError("VALIDATION_FAILED", USAGE2.addColumn);
3340
3620
  await runSchemaChange(context, [
3341
- { op: "addColumn", table, column: parseColumnSpec(spec) }
3621
+ { op: "addColumn", table: table2, column: parseColumnSpec(spec) }
3342
3622
  ]);
3343
3623
  }
3344
3624
  async function runDbTableRenameColumn(context) {
3345
- const [table, column, to] = context.positionals;
3346
- if (!table || !column || !to)
3625
+ const [table2, column, to] = context.positionals;
3626
+ if (!table2 || !column || !to)
3347
3627
  throw new NaroError("VALIDATION_FAILED", USAGE2.renameColumn);
3348
- await runSchemaChange(context, [{ op: "renameColumn", table, column, to }]);
3628
+ await runSchemaChange(context, [{ op: "renameColumn", table: table2, column, to }]);
3349
3629
  }
3350
3630
  async function runDbTableRename(context) {
3351
- const [table, to] = context.positionals;
3352
- if (!table || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
3353
- await runSchemaChange(context, [{ op: "renameTable", table, to }]);
3631
+ const [table2, to] = context.positionals;
3632
+ if (!table2 || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
3633
+ await runSchemaChange(context, [{ op: "renameTable", table: table2, to }]);
3354
3634
  }
3355
3635
  async function runDbTableDropColumn(context) {
3356
- const [table, column] = context.positionals;
3357
- if (!table || !column)
3636
+ const [table2, column] = context.positionals;
3637
+ if (!table2 || !column)
3358
3638
  throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
3359
- const target = `${table}.${column}`;
3639
+ const target = `${table2}.${column}`;
3360
3640
  await runSchemaChange(
3361
3641
  context,
3362
- [{ op: "dropColumn", table, column, confirm: target }],
3642
+ [{ op: "dropColumn", table: table2, column, confirm: target }],
3363
3643
  `Drop column ${printable(target)} and its value in every row? naro db restore can bring it back (Time Travel, 30 days).`
3364
3644
  );
3365
3645
  }
3366
3646
  async function runDbTableDrop(context) {
3367
- const [table] = context.positionals;
3368
- if (!table) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
3647
+ const [table2] = context.positionals;
3648
+ if (!table2) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
3369
3649
  await runSchemaChange(
3370
3650
  context,
3371
- [{ op: "dropTable", table, confirm: table }],
3372
- `Drop table ${printable(table)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
3651
+ [{ op: "dropTable", table: table2, confirm: table2 }],
3652
+ `Drop table ${printable(table2)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
3373
3653
  );
3374
3654
  }
3375
3655
  async function runDbIndexCreate(context) {
3376
- const [table, list] = context.positionals;
3656
+ const [table2, list] = context.positionals;
3377
3657
  const columns = listFlag(list);
3378
- if (!table || columns.length === 0)
3658
+ if (!table2 || columns.length === 0)
3379
3659
  throw new NaroError("VALIDATION_FAILED", USAGE2.indexCreate);
3380
3660
  const name = context.flags.name;
3381
3661
  await runSchemaChange(context, [
3382
3662
  {
3383
3663
  op: "createIndex",
3384
- table,
3664
+ table: table2,
3385
3665
  columns,
3386
3666
  ...context.flags.unique ? { unique: true } : {},
3387
3667
  ...name === void 0 ? {} : { name }
@@ -3398,7 +3678,7 @@ async function runDbIndexDrop(context) {
3398
3678
  var defaults2 = {
3399
3679
  detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
3400
3680
  openBrowser,
3401
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
3681
+ sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms))
3402
3682
  };
3403
3683
  function connected(context, project) {
3404
3684
  const branch = project.defaultBranch;
@@ -3468,7 +3748,7 @@ async function waitForInstall(context, projectId, fullName, deps) {
3468
3748
  if (teamSlug) {
3469
3749
  const attempts = deps.pollAttempts ?? 60;
3470
3750
  const interval = deps.pollIntervalMs ?? 3e3;
3471
- for (let attempt2 = 0; attempt2 < attempts; attempt2 += 1) {
3751
+ for (let attempt3 = 0; attempt3 < attempts; attempt3 += 1) {
3472
3752
  await deps.sleep(interval);
3473
3753
  let reachable = false;
3474
3754
  try {
@@ -3595,9 +3875,9 @@ async function resolveTeam(context, teams, deps = {}) {
3595
3875
  );
3596
3876
  const account = deps.accountLabel ? await deps.accountLabel() : "this account";
3597
3877
  if (only) {
3598
- const label2 = only.isPersonal ? `${only.name} (personal)` : only.name;
3878
+ const label3 = only.isPersonal ? `${only.name} (personal)` : only.name;
3599
3879
  context.output.log(`
3600
- ${account} \u2192 ${label2}`);
3880
+ ${account} \u2192 ${label3}`);
3601
3881
  const ok = await context.prompter.confirm(
3602
3882
  "Deploy to this account and team?"
3603
3883
  );
@@ -3886,7 +4166,7 @@ async function runInspect(context) {
3886
4166
 
3887
4167
  // src/commands/domains.ts
3888
4168
  var defaults3 = {
3889
- sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
4169
+ sleep: (ms) => new Promise((resolve6) => setTimeout(resolve6, ms)),
3890
4170
  now: () => Date.now()
3891
4171
  };
3892
4172
  var FIRST_DELAY_MS = 5e3;
@@ -4130,7 +4410,7 @@ async function runDomainsRemove(context) {
4130
4410
 
4131
4411
  // src/commands/env.ts
4132
4412
  import { existsSync, writeFileSync as writeFileSync4 } from "fs";
4133
- import { join as join4 } from "path";
4413
+ import { join as join5 } from "path";
4134
4414
  var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
4135
4415
  var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
4136
4416
  function parseTarget(value) {
@@ -4233,7 +4513,7 @@ async function runEnvPull(context) {
4233
4513
  const vars = Object.fromEntries(
4234
4514
  selected.map((row) => [row.key, row.value ?? ""])
4235
4515
  );
4236
- const path = join4(context.cwd, file);
4516
+ const path = join5(context.cwd, file);
4237
4517
  if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
4238
4518
  const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
4239
4519
  if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
@@ -4267,7 +4547,7 @@ async function runGenTypes(context) {
4267
4547
  read.policies === void 0 ? {} : { policies: read.policies }
4268
4548
  );
4269
4549
  const undescribed = tables.flatMap(
4270
- (table) => table.error === void 0 ? [] : [{ name: table.name, error: table.error }]
4550
+ (table2) => table2.error === void 0 ? [] : [{ name: table2.name, error: table2.error }]
4271
4551
  );
4272
4552
  for (const { name, error } of undescribed)
4273
4553
  context.output.warn(
@@ -4303,10 +4583,242 @@ async function runGenTypes(context) {
4303
4583
  ]);
4304
4584
  }
4305
4585
 
4586
+ // src/commands/insights-format.ts
4587
+ var OFF_LINE = "Speed Insights is off for this project. Turn it on: naro insights enable";
4588
+ var TURNED_ON = "Speed Insights is on. Measuring starts within about a minute; values arrive when visitors leave a page.";
4589
+ var TURNED_OFF = "Speed Insights is off. Measuring stops within about a minute; turning it back on shows past measurements again.";
4590
+ var RANGE_TEXT = {
4591
+ "24h": "last 24 hours",
4592
+ "7d": "last 7 days",
4593
+ "30d": "last 30 days"
4594
+ };
4595
+ var PART_TEXT = {
4596
+ ttfb: "TTFB",
4597
+ loadDelay: "load delay",
4598
+ loadDuration: "load duration",
4599
+ renderDelay: "render delay",
4600
+ inputDelay: "input delay",
4601
+ processing: "processing",
4602
+ presentation: "presentation"
4603
+ };
4604
+ function rangeText(range) {
4605
+ const known = RANGE_TEXT;
4606
+ return known[range] ?? printable(range);
4607
+ }
4608
+ function partText(name) {
4609
+ const known = PART_TEXT;
4610
+ return known[name] ?? printable(name);
4611
+ }
4612
+ var ATTRIBUTED = /* @__PURE__ */ new Set([
4613
+ "lcp",
4614
+ "inp",
4615
+ "cls"
4616
+ ]);
4617
+ var DEPLOYMENT_ROWS = 10;
4618
+ var SUBJECT_LENGTH = 50;
4619
+ function formatMs(ms) {
4620
+ const rounded = Math.round(ms);
4621
+ return rounded < 1e3 ? `${String(rounded)} ms` : `${(rounded / 1e3).toFixed(2)} s`;
4622
+ }
4623
+ function formatValue(metric, value) {
4624
+ return metric === "cls" ? value.toFixed(2) : formatMs(value);
4625
+ }
4626
+ function formatChange(metric, change2) {
4627
+ const size = formatValue(metric, Math.abs(change2));
4628
+ if (Number.parseFloat(size) === 0) return "\xB10";
4629
+ return change2 > 0 ? `\u25B2 +${size}` : `\u25BC \u2212${size}`;
4630
+ }
4631
+ function formatCount(count2) {
4632
+ return Math.round(count2).toLocaleString("en-US");
4633
+ }
4634
+ function percent(part, total) {
4635
+ return `${String(Math.round(part / total * 100))}%`;
4636
+ }
4637
+ function label2(metric) {
4638
+ return metric.toUpperCase();
4639
+ }
4640
+ function environmentSuffix(environment) {
4641
+ return environment === "preview" ? ", preview" : "";
4642
+ }
4643
+ function environmentWord(environment) {
4644
+ return environment === "preview" ? "preview " : "";
4645
+ }
4646
+ function table(rows) {
4647
+ const widths = [];
4648
+ for (const row of rows)
4649
+ row.forEach((cell, i) => {
4650
+ widths[i] = Math.max(widths[i] ?? 0, cell.length);
4651
+ });
4652
+ return rows.map(
4653
+ (row) => ` ${row.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(" ")}`.trimEnd()
4654
+ );
4655
+ }
4656
+ function summaryLines(report) {
4657
+ return table([
4658
+ ["Metric", "p75", "Good", "Needs improvement", "Poor", "Visits"],
4659
+ ...SPEED_INSIGHTS_METRICS.map((metric) => {
4660
+ const summary = report.summary[metric];
4661
+ if (summary.p75 === null || summary.samples <= 0)
4662
+ return [label2(metric), "\u2014", "\u2014", "\u2014", "\u2014", "0"];
4663
+ return [
4664
+ label2(metric),
4665
+ formatValue(metric, summary.p75),
4666
+ percent(summary.good, summary.samples),
4667
+ percent(summary.needsImprovement, summary.samples),
4668
+ percent(summary.poor, summary.samples),
4669
+ formatCount(summary.samples)
4670
+ ];
4671
+ })
4672
+ ]);
4673
+ }
4674
+ function breakdownTable(heading, metric, rows) {
4675
+ if (rows.length === 0) return [" (none)"];
4676
+ return table([
4677
+ ["p75", "Visits", heading],
4678
+ ...rows.map((row) => [
4679
+ formatValue(metric, row.p75),
4680
+ formatCount(row.samples),
4681
+ printable(row.name)
4682
+ ])
4683
+ ]);
4684
+ }
4685
+ function elementLines(metric, elements) {
4686
+ const lines = ["", `${label2(metric)} elements behind slow visits`];
4687
+ if (elements.length === 0) return [...lines, " (none)"];
4688
+ for (const row of elements) {
4689
+ lines.push(
4690
+ ` ${printable(row.element)} on ${printable(row.route)}: ${formatCount(row.slow)} slow visits, average ${formatValue(metric, row.average)}`
4691
+ );
4692
+ if (row.parts && row.parts.length > 0)
4693
+ lines.push(
4694
+ ` ${row.parts.map((part) => `${partText(part.name)} ${formatMs(part.ms)}`).join(" \xB7 ")}`
4695
+ );
4696
+ if (row.url) lines.push(` image ${printable(row.url)}`);
4697
+ }
4698
+ return lines;
4699
+ }
4700
+ function deploymentName(row) {
4701
+ if (row.source === "cli" || !row.commitSha) return row.id.slice(0, 8);
4702
+ const sha = printable(row.commitSha.slice(0, 7));
4703
+ const subject = [
4704
+ ...printable(row.commitMessage?.split("\n")[0]?.trim() ?? "")
4705
+ ];
4706
+ if (subject.length === 0) return sha;
4707
+ return subject.length > SUBJECT_LENGTH ? `${sha} ${subject.slice(0, SUBJECT_LENGTH - 1).join("")}\u2026` : `${sha} ${subject.join("")}`;
4708
+ }
4709
+ function changeText(metric, row) {
4710
+ if (row.samples < SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES)
4711
+ return "too few visits";
4712
+ if (row.change === null) return "\u2013";
4713
+ return formatChange(metric, row.change);
4714
+ }
4715
+ function deploymentLines(metric, deployments, environment) {
4716
+ if (deployments === null) return [];
4717
+ const lines = [
4718
+ "",
4719
+ `${label2(metric)} by ${environmentWord(environment)}deployment, newest first`
4720
+ ];
4721
+ const measured = deployments.filter((row) => row.samples > 0).slice(0, DEPLOYMENT_ROWS);
4722
+ if (measured.length === 0) return [...lines, " (none)"];
4723
+ return [
4724
+ ...lines,
4725
+ ...table([
4726
+ ["", "p75", "Visits", "Change", "Deployment"],
4727
+ ...measured.map((row) => [
4728
+ row.liveUntil === null ? "Live" : "",
4729
+ row.p75 === null ? "\u2014" : formatValue(metric, row.p75),
4730
+ formatCount(row.samples),
4731
+ changeText(metric, row),
4732
+ deploymentName(row)
4733
+ ])
4734
+ ])
4735
+ ];
4736
+ }
4737
+ function reportLines(report, deployments) {
4738
+ if (!report.enabled) return [OFF_LINE];
4739
+ const { metric, range, environment } = report;
4740
+ const head = `Speed Insights \u2014 ${label2(metric)}, ${rangeText(range)}${environmentSuffix(environment)}`;
4741
+ if (SPEED_INSIGHTS_METRICS.every((m) => report.summary[m].samples <= 0))
4742
+ return [
4743
+ head,
4744
+ "",
4745
+ `No ${environmentWord(environment)}measurements in the ${rangeText(range)} yet. Values arrive when visitors leave a page.`
4746
+ ];
4747
+ return [
4748
+ head,
4749
+ "",
4750
+ ...summaryLines(report),
4751
+ "",
4752
+ `${label2(metric)} by page`,
4753
+ ...breakdownTable("Page", metric, report.paths),
4754
+ "",
4755
+ `${label2(metric)} by device`,
4756
+ ...breakdownTable("Device", metric, report.devices),
4757
+ ...ATTRIBUTED.has(metric) ? elementLines(metric, report.elements) : [],
4758
+ ...deploymentLines(metric, deployments, environment)
4759
+ ];
4760
+ }
4761
+ function routePathsLines(result) {
4762
+ return [
4763
+ `${label2(result.metric)} p75 under ${printable(result.route)}, ${rangeText(result.range)}${environmentSuffix(result.environment)}`,
4764
+ ...breakdownTable("Path", result.metric, result.paths)
4765
+ ];
4766
+ }
4767
+
4768
+ // src/commands/insights.ts
4769
+ function queryOf(flags) {
4770
+ return {
4771
+ ...flags.range !== void 0 ? { range: flags.range } : {},
4772
+ ...flags.metric !== void 0 ? { metric: flags.metric } : {},
4773
+ ...flags.env !== void 0 ? { environment: flags.env } : {}
4774
+ };
4775
+ }
4776
+ async function runInsights(context) {
4777
+ const client = await context.client();
4778
+ const projectId = await context.resolveProjectId();
4779
+ const query = queryOf(context.flags);
4780
+ if (context.flags.route !== void 0) {
4781
+ const result = await client.insights.paths(projectId, {
4782
+ ...query,
4783
+ route: context.flags.route
4784
+ });
4785
+ context.output.result({ projectId, ...result }, routePathsLines(result));
4786
+ return;
4787
+ }
4788
+ const overview = await client.insights.overview(projectId, query);
4789
+ if (overview.deploymentsError)
4790
+ context.output.warn(
4791
+ `Deployments are unavailable right now: ${printable(overview.deploymentsError.message)}`
4792
+ );
4793
+ context.output.result(
4794
+ { projectId, ...overview },
4795
+ reportLines(overview.report, overview.deployments)
4796
+ );
4797
+ }
4798
+ async function setInsights(context, enabled) {
4799
+ if (context.flags.env !== void 0)
4800
+ throw new NaroError(
4801
+ "VALIDATION_FAILED",
4802
+ "Speed Insights has one switch for production and preview; drop --env."
4803
+ );
4804
+ const client = await context.client();
4805
+ const projectId = await context.resolveProjectId();
4806
+ const result = await client.insights.setEnabled(projectId, enabled);
4807
+ context.output.result({ projectId, enabled: result.enabled }, [
4808
+ result.enabled ? TURNED_ON : TURNED_OFF
4809
+ ]);
4810
+ }
4811
+ function runInsightsEnable(context) {
4812
+ return setInsights(context, true);
4813
+ }
4814
+ function runInsightsDisable(context) {
4815
+ return setInsights(context, false);
4816
+ }
4817
+
4306
4818
  // src/commands/logs.ts
4307
4819
  var FOLLOW_INTERVAL_MS = 3e3;
4308
4820
  function sleep(ms) {
4309
- return new Promise((resolve5) => setTimeout(resolve5, ms));
4821
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
4310
4822
  }
4311
4823
  function formatBuildLog(log) {
4312
4824
  return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
@@ -4554,80 +5066,3067 @@ async function runRollback(context) {
4554
5066
  await promoteAndWait(context, client, project, target, "rollback");
4555
5067
  }
4556
5068
 
4557
- // src/router.ts
4558
- var HELP = `naro \u2014 deploy Next.js apps to Naro (https://naro.sh)
4559
-
4560
- Usage:
4561
- naro login | logout | whoami
4562
- naro token Print an access token for CI (NARO_TOKEN)
4563
-
4564
- naro link | init Link this directory to a Naro project
4565
- naro unlink Remove the link (.naro/)
4566
- naro projects [--team <slug>] List projects
4567
- naro project inspect Show the linked project
4568
-
4569
- naro deploy [--prod] Build locally and deploy (preview by default)
4570
- [--root <dir>] [--name <name>] --root points at the app in a monorepo
4571
- naro deployments List deployments of the linked project
4572
- naro inspect <deployment-id> Show one deployment + build logs
4573
- naro cancel <deployment-id> Stop a build that is still running
4574
-
4575
- naro git connect [owner/repo] Deploy on every push (defaults to your origin remote)
4576
- naro git disconnect Stop deploying on push
4577
-
4578
- naro logs [deployment-id] Runtime logs (or build logs of a deployment)
4579
- [--type build|runtime] [--env production|preview] [--level error] [--follow]
4580
-
4581
- naro env list List env vars (secrets masked)
4582
- naro env add KEY [environment] Add/update (value from prompt or stdin)
4583
- naro env remove KEY [environment] Remove
4584
- naro env pull [file] Write env vars to .env.local
5069
+ // src/commands/protection.ts
5070
+ var DISABLE_QUESTION2 = "Open this project's preview to anyone with the link? Visitors no longer need to be a member to see it.";
5071
+ var REPLACE_QUESTION = "Replace the bypass secret? The current one stops working, and automation still sending it can no longer open the preview.";
5072
+ var REMOVE_QUESTION = "Remove the bypass secret? Automation that sends it can no longer open the preview.";
5073
+ var EDGE = "The edge picks the change up within about a minute.";
5074
+ function statusLine(state) {
5075
+ return `Preview protection: ${state.enabled ? "on" : "off"} \xB7 bypass secret: ${state.bypass ? "set" : "none"}`;
5076
+ }
5077
+ async function runProtection(context) {
5078
+ const client = await context.client();
5079
+ const projectId = await context.resolveProjectId();
5080
+ const state = await client.protection.get(projectId);
5081
+ context.output.result({ projectId, ...state }, [statusLine(state)]);
5082
+ }
5083
+ async function setProtection(context, enabled) {
5084
+ const client = await context.client();
5085
+ const projectId = await context.resolveProjectId();
5086
+ const state = await client.protection.set(projectId, enabled);
5087
+ context.output.result({ projectId, ...state }, [
5088
+ state.enabled ? `Preview protection is on: only members of this project can open the preview. ${EDGE}` : `Preview protection is off: anyone with the link can open the preview. ${EDGE}`
5089
+ ]);
5090
+ }
5091
+ function runProtectionEnable(context) {
5092
+ return setProtection(context, true);
5093
+ }
5094
+ async function runProtectionDisable(context) {
5095
+ await confirmOrAbort(context, DISABLE_QUESTION2);
5096
+ return setProtection(context, false);
5097
+ }
5098
+ async function runProtectionBypass(context) {
5099
+ const client = await context.client();
5100
+ const projectId = await context.resolveProjectId();
5101
+ if (!context.flags.yes && (await client.protection.get(projectId)).bypass)
5102
+ await confirmOrAbort(context, REPLACE_QUESTION);
5103
+ const result = await client.protection.createBypass(projectId);
5104
+ context.output.result({ projectId, ...result }, [
5105
+ `Bypass secret (shown once): ${result.secret}`,
5106
+ "Send it as the x-naro-protection-bypass header."
5107
+ ]);
5108
+ }
5109
+ async function runProtectionBypassRemove(context) {
5110
+ await confirmOrAbort(context, REMOVE_QUESTION);
5111
+ const client = await context.client();
5112
+ const projectId = await context.resolveProjectId();
5113
+ const state = await client.protection.removeBypass(projectId);
5114
+ context.output.result({ projectId, ...state }, ["Bypass secret removed."]);
5115
+ }
4585
5116
 
4586
- naro domains list | add <host> [--no-www] | remove <host>
4587
- add example.com also connects www.example.com and redirects the apex to
4588
- it, because most registrars cannot put a CNAME on an apex; --no-www
4589
- registers the apex alone
4590
- naro domains verify [host] [--wait] Re-check DNS and print what is missing
4591
- exits non-zero while a domain is not live yet
5117
+ // src/commands/storage-units.ts
5118
+ var SIZE_MESSAGE = "--file-size-limit takes a whole number of bytes, or a size like 5MB (1000s) or 5MiB (1024s), or none.";
5119
+ var UNITS = {
5120
+ b: 1,
5121
+ kb: 1e3,
5122
+ mb: 1e6,
5123
+ gb: 1e9,
5124
+ kib: 1024,
5125
+ mib: 1048576,
5126
+ gib: 1073741824
5127
+ };
5128
+ function parseSize(text) {
5129
+ const match = /^(\d{1,16})\s*([a-z]+)?$/i.exec(text.trim());
5130
+ const unit = UNITS[(match?.[2] ?? "b").toLowerCase()];
5131
+ const bytes = match?.[1] !== void 0 && unit !== void 0 ? Number(match[1]) * unit : Number.NaN;
5132
+ if (!Number.isSafeInteger(bytes) || bytes < 1)
5133
+ throw new NaroError("VALIDATION_FAILED", SIZE_MESSAGE);
5134
+ return bytes;
5135
+ }
5136
+ function binarySize(bytes) {
5137
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
5138
+ let value = bytes;
5139
+ let unit = 0;
5140
+ while (value >= 1024 && unit < units.length - 1) {
5141
+ value /= 1024;
5142
+ unit += 1;
5143
+ }
5144
+ const shown = unit === 0 ? String(value) : String(Number(value.toFixed(1)));
5145
+ return `${shown} ${units[unit] ?? "B"}`;
5146
+ }
5147
+ function count(n, noun) {
5148
+ return `${n.toLocaleString("en-US")} ${noun}${n === 1 ? "" : "s"}`;
5149
+ }
4592
5150
 
4593
- naro db enable Create the project's database (Cloudflare D1)
4594
- naro db info Database status, size, table count and limits
4595
- and today's writes against the daily write limit (UTC)
4596
- naro db query <sql> | --file <f> Run SQL against it (reads stdin when piped)
4597
- naro db push [--dry-run] Apply pending migrations from ./migrations (asks first; --yes to skip)
4598
- naro db dump [--output <f>] Download a SQL dump of the database (stdout by default)
4599
- [--schema-only|--data-only] [--table a,b] Cloudflare holds other queries until it finishes
4600
- naro db restore --bookmark <id> Roll the database back (Time Travel, last 30 days; asks first)
4601
- | --timestamp <time> [--dry-run] prints the command that undoes it
4602
- naro db pull [--dir <d>] Write the remote schema as migrations/0001_remote_schema.sql
4603
- and record it as applied - a baseline (asks first)
4604
- naro db reset [--dir <d>] Drop everything, clear the ledger, re-apply ./migrations
4605
- [--dry-run] [--yes] (asks first; db restore --bookmark undoes it)
4606
- naro db lint [--strict] Check the schema for what D1 actually trips on
4607
- (exits 1 on an error, --strict on a warning too)
4608
- naro db table create <name> --column <spec> [--column <spec>\u2026]
4609
- [--owner <column>] [--if-not-exists] Create a table without SQL. <spec> is name:type[:modifier\u2026]:
4610
- types integer, real, text, blob, numeric, boolean, datetime, json;
4611
- modifiers pk, autoincrement, notnull, unique, default=<v>,
4612
- references=<table>.<column>, ondelete=cascade|set-null|\u2026
4613
- --owner adds that column (TEXT NOT NULL) and an owner policy
4614
- naro db table add-column <table> --column <spec>
4615
- naro db table rename-column <table> <column> <new-name>
4616
- naro db table rename <table> <new-name>
4617
- naro db table drop-column <table> <column> Asks first (--yes to skip)
4618
- naro db table drop <table> Asks first (--yes to skip)
4619
- naro db index create <table> <col[,col\u2026]> [--unique] [--name <name>]
4620
- naro db index drop <name>
4621
- Each runs as one migration in d1_migrations and is written to ./migrations
4622
- (--no-write skips the file; --dry-run prints the SQL and changes nothing)
4623
- naro migration new <name> Create migrations/NNNN_<name>.sql (stdin becomes the body when piped)
4624
- naro migration list Local vs applied migrations
4625
- naro migration up [--dry-run] Same as db push
4626
- naro migration repair <name> Fix the ledger alone: mark a migration applied or
4627
- --status applied|reverted [--yes] remove its row. Its SQL is never run (asks first)
4628
- naro gen types [--lang typescript] [--output <file>]
4629
- TypeScript types for the schema, for createClient<Database>()
4630
- from naro-js (stdout by default)
5151
+ // src/commands/storage.ts
5152
+ var CREATE_USAGE2 = "Usage: naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]";
5153
+ var UPDATE_USAGE = "Usage: naro storage bucket update <name> [--public | --private] [--file-size-limit <size> | none] [--allowed-mime-types <t,\u2026> | any]";
5154
+ var EMPTY_USAGE = "Usage: naro storage bucket empty <name> [--yes]";
5155
+ var REMOVE_USAGE2 = "Usage: naro storage bucket remove <name> [--empty] [--yes]";
5156
+ var ENABLE_FIRST = "Storage is not enabled yet; enabling it first.";
5157
+ var NO_BUCKETS = "No buckets yet. `naro storage bucket create <name>` makes one.";
5158
+ var IDLE_PAGES = 3;
5159
+ function bucketLine(bucket) {
5160
+ const name = printable(bucket.name);
5161
+ const cells = [
5162
+ name.padEnd(20),
5163
+ (bucket.public ? "public" : "private").padEnd(7),
5164
+ count(bucket.objects, "object").padEnd(12),
5165
+ binarySize(bucket.bytes).padEnd(10),
5166
+ bucket.allowedMimeTypes === null ? "any type" : printable(bucket.allowedMimeTypes.join(",")),
5167
+ bucket.fileSizeLimit === null ? "no size limit" : `up to ${binarySize(bucket.fileSizeLimit)}`
5168
+ ];
5169
+ const line = ` ${cells.join(" ")}`;
5170
+ return bucket.deletingAt === null ? line : `${line} (being removed: \`naro storage bucket remove ${name} --empty\` finishes it)`;
5171
+ }
5172
+ function storageLines(info) {
5173
+ if (!info.enabled)
5174
+ return [
5175
+ "Storage: off",
5176
+ "Run `naro storage enable` to turn it on (the project's backend must be on first)."
5177
+ ];
5178
+ const { limits, usage } = info;
5179
+ return [
5180
+ `Storage: on (R2 bucket ${printable(info.bucket ?? "unknown")})`,
5181
+ `Limits: ${binarySize(limits.bytes)} in all, ${binarySize(limits.objectBytes)} per object, ${binarySize(limits.singleUploadBytes)} per upload`,
5182
+ usage === null ? "Used: not known yet: the backend is not ready (`naro backend` shows where it got to)" : `Used: ${binarySize(usage.bytes)} in ${count(usage.objects, "object")}`,
5183
+ info.buckets.length === 0 ? "Buckets: none yet (`naro storage bucket create <name>` makes one)" : `Buckets: ${String(info.buckets.length)}`,
5184
+ ...info.buckets.map(bucketLine)
5185
+ ];
5186
+ }
5187
+ async function enableStorage(client, projectId) {
5188
+ try {
5189
+ return await client.storage.enable(projectId);
5190
+ } catch (error) {
5191
+ if (isNaroError(error) && error.code === "TIMEOUT")
5192
+ throw new NaroError(
5193
+ "TIMEOUT",
5194
+ "Turning Storage on did not answer within 2 minutes. `naro storage` shows where it got to, and `naro storage enable` again picks up where it stopped.",
5195
+ { cause: error }
5196
+ );
5197
+ throw error;
5198
+ }
5199
+ }
5200
+ function bucketName(context, usage) {
5201
+ if (context.positionals.length !== 1)
5202
+ throw new NaroError("VALIDATION_FAILED", usage);
5203
+ return context.positionals[0];
5204
+ }
5205
+ function bucketFields(flags) {
5206
+ if (flags.public === true && flags.private === true)
5207
+ throw new NaroError(
5208
+ "VALIDATION_FAILED",
5209
+ "Pass --public or --private, not both."
5210
+ );
5211
+ const fields = {};
5212
+ if (flags.public === true) fields.public = true;
5213
+ if (flags.private === true) fields.public = false;
5214
+ if (flags.fileSizeLimit !== void 0)
5215
+ fields.fileSizeLimit = flags.fileSizeLimit.trim().toLowerCase() === "none" ? null : parseSize(flags.fileSizeLimit);
5216
+ if (flags.allowedMimeTypes !== void 0)
5217
+ fields.allowedMimeTypes = flags.allowedMimeTypes.trim().toLowerCase() === "any" ? null : listFlag(flags.allowedMimeTypes);
5218
+ return fields;
5219
+ }
5220
+ async function runStorageStatus(context) {
5221
+ const client = await context.client();
5222
+ const projectId = await context.resolveProjectId();
5223
+ const storage = await client.storage.info(projectId);
5224
+ context.output.result({ projectId, storage }, storageLines(storage));
5225
+ }
5226
+ async function runStorageEnable(context) {
5227
+ const client = await context.client();
5228
+ const projectId = await context.resolveProjectId();
5229
+ const storage = await enableStorage(client, projectId);
5230
+ context.output.result({ projectId, storage }, [
5231
+ `\u2705 Storage is on: R2 bucket ${printable(storage.bucket ?? "unknown")}.`,
5232
+ ...storage.usage === null ? ["\u23F3 The backend is still finishing; check `naro backend` in a minute."] : [],
5233
+ ...storageLines(storage)
5234
+ ]);
5235
+ }
5236
+ async function runStorageBucketList(context) {
5237
+ const client = await context.client();
5238
+ const projectId = await context.resolveProjectId();
5239
+ const buckets = await client.storage.listBuckets(projectId);
5240
+ context.output.result(
5241
+ { projectId, buckets },
5242
+ buckets.length === 0 ? [NO_BUCKETS] : buckets.map(bucketLine)
5243
+ );
5244
+ }
5245
+ async function runStorageBucketCreate(context) {
5246
+ const name = bucketName(context, CREATE_USAGE2);
5247
+ const input = { name, ...bucketFields(context.flags) };
5248
+ const client = await context.client();
5249
+ const projectId = await context.resolveProjectId();
5250
+ let bucket;
5251
+ try {
5252
+ bucket = await client.storage.createBucket(projectId, input);
5253
+ } catch (error) {
5254
+ if (!isNaroError(error) || error.code !== "STORAGE_NOT_ENABLED")
5255
+ throw error;
5256
+ context.output.log(ENABLE_FIRST);
5257
+ await enableStorage(client, projectId);
5258
+ bucket = await client.storage.createBucket(projectId, input);
5259
+ }
5260
+ context.output.result({ projectId, bucket }, [
5261
+ `\u2705 Created bucket ${printable(bucket.name)}.`,
5262
+ bucketLine(bucket)
5263
+ ]);
5264
+ }
5265
+ async function runStorageBucketUpdate(context) {
5266
+ const name = bucketName(context, UPDATE_USAGE);
5267
+ const patch = bucketFields(context.flags);
5268
+ if (patch.public === true)
5269
+ await confirmOrAbort(
5270
+ context,
5271
+ `Make bucket ${name} public? Anyone with an object's URL can download it.`
5272
+ );
5273
+ const client = await context.client();
5274
+ const projectId = await context.resolveProjectId();
5275
+ const bucket = await client.storage.updateBucket(projectId, name, patch);
5276
+ context.output.result({ projectId, bucket }, [
5277
+ `\u2705 Updated bucket ${printable(bucket.name)}. It takes effect within about 10 seconds.`,
5278
+ bucketLine(bucket)
5279
+ ]);
5280
+ }
5281
+ async function untilDone(context, bucket, page) {
5282
+ let deleted = 0;
5283
+ let idle = 0;
5284
+ for (; ; ) {
5285
+ const result = await page();
5286
+ deleted += result.deleted;
5287
+ if (result.done) return deleted;
5288
+ idle = result.deleted === 0 ? idle + 1 : 0;
5289
+ if (idle >= IDLE_PAGES)
5290
+ throw new NaroError(
5291
+ "BUCKET_EMPTY_STALLED",
5292
+ `Bucket ${bucket} is not empty yet, but the last calls deleted nothing. Run the same command again in a minute.`,
5293
+ { details: { deleted } }
5294
+ );
5295
+ context.output.log(`\u2026 ${count(deleted, "object")} deleted so far`);
5296
+ }
5297
+ }
5298
+ function objectsSentence(objects2) {
5299
+ return `Its ${count(objects2, "object")} ${objects2 === 1 ? "is" : "are"} deleted and cannot be restored.`;
5300
+ }
5301
+ async function runStorageBucketEmpty(context) {
5302
+ const name = bucketName(context, EMPTY_USAGE);
5303
+ const client = await context.client();
5304
+ const projectId = await context.resolveProjectId();
5305
+ const bucket = await client.storage.getBucket(projectId, name);
5306
+ await confirmOrAbort(
5307
+ context,
5308
+ `Empty bucket ${name}? ${objectsSentence(bucket.objects)}`
5309
+ );
5310
+ const deleted = await untilDone(
5311
+ context,
5312
+ name,
5313
+ () => client.storage.emptyBucket(projectId, name)
5314
+ );
5315
+ context.output.result({ projectId, bucket: name, deleted }, [
5316
+ `\u2705 Emptied ${name}: ${count(deleted, "object")} deleted.`
5317
+ ]);
5318
+ }
5319
+ async function runStorageBucketRemove(context) {
5320
+ const name = bucketName(context, REMOVE_USAGE2);
5321
+ const empty = context.flags.empty === true;
5322
+ const client = await context.client();
5323
+ const projectId = await context.resolveProjectId();
5324
+ let question = `Remove bucket ${name}?`;
5325
+ if (empty) {
5326
+ const bucket = await client.storage.getBucket(projectId, name);
5327
+ question += ` ${objectsSentence(bucket.objects)}`;
5328
+ }
5329
+ await confirmOrAbort(context, question);
5330
+ await untilDone(context, name, async () => {
5331
+ const removal = await client.storage.removeBucket(
5332
+ projectId,
5333
+ name,
5334
+ empty ? { empty: true } : {}
5335
+ );
5336
+ return "ok" in removal ? { deleted: 0, done: true } : removal;
5337
+ });
5338
+ context.output.result({ projectId, bucket: name, removed: true }, [
5339
+ `\u2705 Removed bucket ${name}.`
5340
+ ]);
5341
+ }
5342
+
5343
+ // src/commands/storage-objects.ts
5344
+ import { randomUUID } from "crypto";
5345
+ import { createWriteStream as createWriteStream2, openAsBlob, rmSync as rmSync2 } from "fs";
5346
+ import { rename, rm, stat } from "fs/promises";
5347
+ import { basename as basename2, dirname as dirname3, extname, join as join6, resolve as resolve5 } from "path";
5348
+ import { Readable } from "stream";
5349
+ import { pipeline as pipeline2 } from "stream/promises";
5350
+
5351
+ // ../../packages/naro-sdk/dist/index.js
5352
+ var INVALID_QUERY_MESSAGE = "The query is not a valid Naro query.";
5353
+ var INVALID_SESSION_MESSAGE = "The session is expired or not valid. Sign in again.";
5354
+ function networkError(cause) {
5355
+ return {
5356
+ code: "NETWORK_ERROR",
5357
+ message: "The request did not reach the Naro backend. Check the url and the network.",
5358
+ details: { cause: describe(cause) }
5359
+ };
5360
+ }
5361
+ function requestTimeout(ms) {
5362
+ return {
5363
+ code: "TIMEOUT",
5364
+ message: `The Naro backend did not answer within ${String(Math.round(ms / 1e3))} seconds. Check the network and try again.`
5365
+ };
5366
+ }
5367
+ function invalidResponse(status) {
5368
+ return {
5369
+ code: "INVALID_RESPONSE",
5370
+ message: `The server did not answer like a Naro backend (HTTP ${String(status)}).`,
5371
+ details: { status }
5372
+ };
5373
+ }
5374
+ function invalidQuery(path, reason) {
5375
+ return {
5376
+ code: "INVALID_QUERY",
5377
+ message: INVALID_QUERY_MESSAGE,
5378
+ details: { path, reason }
5379
+ };
5380
+ }
5381
+ function invalidSession() {
5382
+ return { code: "INVALID_SESSION", message: INVALID_SESSION_MESSAGE };
5383
+ }
5384
+ function codeVerifierMissing() {
5385
+ return {
5386
+ code: "CODE_VERIFIER_MISSING",
5387
+ message: "This browser did not start this sign-in, so it cannot finish it. Sign in again here."
5388
+ };
5389
+ }
5390
+ function passwordRecoveryRequired() {
5391
+ return {
5392
+ code: "PASSWORD_RECOVERY_REQUIRED",
5393
+ message: "updateUser({ password }) works in the password recovery flow: open the link resetPasswordForEmail sent, then call it on that page."
5394
+ };
5395
+ }
5396
+ function invalidProvider() {
5397
+ return {
5398
+ code: "VALIDATION_ERROR",
5399
+ message: "provider must be google or github."
5400
+ };
5401
+ }
5402
+ function serviceKeyRequired() {
5403
+ return {
5404
+ code: "PERMISSION_DENIED",
5405
+ message: "Only the service key can manage users."
5406
+ };
5407
+ }
5408
+ function cryptoUnavailable() {
5409
+ return {
5410
+ code: "CRYPTO_UNAVAILABLE",
5411
+ message: "This runtime has no crypto.subtle, which signing in with a provider needs (https or localhost)."
5412
+ };
5413
+ }
5414
+ function describe(cause) {
5415
+ if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
5416
+ return String(cause);
5417
+ }
5418
+ function isRecord(value) {
5419
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5420
+ }
5421
+ function asError(value) {
5422
+ if (!isRecord(value)) return null;
5423
+ const { code, message } = value;
5424
+ if (typeof code !== "string" || code === "") return null;
5425
+ if (typeof message !== "string") return null;
5426
+ const error = { code, message };
5427
+ if (value.details !== void 0) error.details = value.details;
5428
+ return error;
5429
+ }
5430
+ function errorFromBody(status, body) {
5431
+ if (isRecord(body) && body.error !== void 0 && body.error !== null)
5432
+ return asError(body.error) ?? invalidResponse(status);
5433
+ if (status >= 200 && status < 300) return null;
5434
+ return invalidResponse(status);
5435
+ }
5436
+ async function receive(config, options, init) {
5437
+ const controller = options.timeoutMs !== void 0 && typeof AbortController === "function" ? new AbortController() : null;
5438
+ if (controller) init.signal = controller.signal;
5439
+ const request = (async () => {
5440
+ try {
5441
+ const fetchImpl = config.fetch ?? globalThis.fetch;
5442
+ const response = await fetchImpl(
5443
+ `${config.baseUrl}/_naro/v1${options.path}`,
5444
+ init
5445
+ );
5446
+ return {
5447
+ status: response.status,
5448
+ headers: response.headers,
5449
+ text: await response.text()
5450
+ };
5451
+ } catch (error) {
5452
+ return { error: networkError(error) };
5453
+ }
5454
+ })();
5455
+ const { timeoutMs } = options;
5456
+ if (timeoutMs === void 0) return request;
5457
+ let timer;
5458
+ const expired = new Promise((resolve6) => {
5459
+ timer = setTimeout(() => {
5460
+ controller?.abort();
5461
+ resolve6({ error: requestTimeout(timeoutMs) });
5462
+ }, timeoutMs);
5463
+ });
5464
+ try {
5465
+ return await Promise.race([request, expired]);
5466
+ } finally {
5467
+ clearTimeout(timer);
5468
+ }
5469
+ }
5470
+ async function call(config, options) {
5471
+ let payload;
5472
+ if (options.body !== void 0) {
5473
+ try {
5474
+ payload = JSON.stringify(options.body);
5475
+ } catch {
5476
+ return { error: invalidQuery("", "is not JSON"), answer: null };
5477
+ }
5478
+ }
5479
+ const token = options.withToken ? await config.tokens.get() : null;
5480
+ const headers = { apikey: config.apiKey };
5481
+ if (token !== null) headers.Authorization = `Bearer ${token}`;
5482
+ const init = {
5483
+ method: options.method,
5484
+ headers,
5485
+ // Identity is the bearer token alone. The sign-in routes also set a
5486
+ // session cookie (spec §13); a request that carried it would let any site
5487
+ // act as the user. Workers accept the field (measured on workerd
5488
+ // 1.20260815).
5489
+ credentials: "omit"
5490
+ };
5491
+ if (payload !== void 0) {
5492
+ headers["Content-Type"] = "application/json";
5493
+ init.body = payload;
5494
+ }
5495
+ const received = await receive(config, options, init);
5496
+ if ("error" in received) return { error: received.error, answer: null };
5497
+ const { status, headers: responseHeaders, text } = received;
5498
+ let body;
5499
+ if (text.trim() !== "") {
5500
+ try {
5501
+ body = JSON.parse(text);
5502
+ } catch {
5503
+ return { error: invalidResponse(status), answer: null };
5504
+ }
5505
+ }
5506
+ const answer = { status, headers: responseHeaders, body, token };
5507
+ const error = errorFromBody(status, body);
5508
+ if (error === null) return { error: null, answer };
5509
+ return {
5510
+ error: await settled(config, error, responseHeaders, token),
5511
+ answer
5512
+ };
5513
+ }
5514
+ async function settled(config, error, headers, token) {
5515
+ if (error.code === "INVALID_SESSION" && token !== null)
5516
+ await config.tokens.clearIf(token);
5517
+ return RETRY_AFTER_CODES.has(error.code) ? withRetryAfter(error, headers) : error;
5518
+ }
5519
+ async function send(config, options) {
5520
+ const token = options.withToken ? await config.tokens.get() : null;
5521
+ const headers = {
5522
+ ...options.headers,
5523
+ apikey: config.apiKey
5524
+ };
5525
+ if (token !== null) headers.Authorization = `Bearer ${token}`;
5526
+ const init = {
5527
+ method: options.method,
5528
+ headers,
5529
+ credentials: "omit"
5530
+ };
5531
+ if (options.body !== void 0) init.body = options.body;
5532
+ let response;
5533
+ try {
5534
+ const fetchImpl = config.fetch ?? globalThis.fetch;
5535
+ response = await fetchImpl(
5536
+ `${config.baseUrl}/_naro/v1${options.path}`,
5537
+ init
5538
+ );
5539
+ } catch (error2) {
5540
+ return { error: networkError(error2) };
5541
+ }
5542
+ if (response.status >= 200 && response.status < 300)
5543
+ return { error: null, response };
5544
+ let text;
5545
+ try {
5546
+ text = await response.text();
5547
+ } catch (error2) {
5548
+ return { error: networkError(error2) };
5549
+ }
5550
+ let body;
5551
+ if (text.trim() !== "") {
5552
+ try {
5553
+ body = JSON.parse(text);
5554
+ } catch {
5555
+ return { error: invalidResponse(response.status) };
5556
+ }
5557
+ }
5558
+ const error = errorFromBody(response.status, body) ?? invalidResponse(response.status);
5559
+ return { error: await settled(config, error, response.headers, token) };
5560
+ }
5561
+ var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
5562
+ "RATE_LIMITED",
5563
+ "WRITE_QUOTA_EXCEEDED",
5564
+ // Storage (original spec §13): one object written twice within a second
5565
+ // (`x-retry-after: 1`), and the daily operation limit (3-1d).
5566
+ "OBJECT_RATE_LIMITED",
5567
+ "STORAGE_OPS_QUOTA_EXCEEDED"
5568
+ ]);
5569
+ function withRetryAfter(error, headers) {
5570
+ const header = headers.get("x-retry-after")?.trim() ?? "";
5571
+ if (!/^\d{1,9}$/.test(header)) return error;
5572
+ const { details } = error;
5573
+ if (details !== void 0 && !isRecord(details)) return error;
5574
+ return {
5575
+ ...error,
5576
+ details: { ...details, retryAfter: Number(header) }
5577
+ };
5578
+ }
5579
+ function isBrowser(scope = globalThis) {
5580
+ const g = scope;
5581
+ return g.window !== void 0 && g.document !== void 0;
5582
+ }
5583
+ function isWorkerd(g) {
5584
+ return g.navigator?.userAgent === "Cloudflare-Workers";
5585
+ }
5586
+ function hasServerSignal(g) {
5587
+ return isWorkerd(g) || // Vercel's edge runtime and Next.js's edge (middleware, edge routes).
5588
+ typeof g.EdgeRuntime === "string" || typeof g.Deno?.version?.deno === "string" || typeof g.Bun?.version === "string" || // Node, and so the Next.js server. Browser polyfills of `process` leave
5589
+ // `versions.node` out.
5590
+ typeof g.process?.versions?.node === "string";
5591
+ }
5592
+ function hasDeviceSignal(g) {
5593
+ if (g.window !== void 0 || g.document !== void 0) return true;
5594
+ if (g.navigator?.product === "ReactNative") return true;
5595
+ return !isWorkerd(g) && (g.WorkerGlobalScope !== void 0 || g.ServiceWorkerGlobalScope !== void 0);
5596
+ }
5597
+ function isKnownServer(scope = globalThis) {
5598
+ const g = scope;
5599
+ return hasServerSignal(g) && !hasDeviceSignal(g);
5600
+ }
5601
+ var USERS_PATH = "/auth/admin/users";
5602
+ function dataOf(result) {
5603
+ if (result.error !== null) return null;
5604
+ const { body } = result.answer;
5605
+ return isRecord(body) && isRecord(body.data) ? body.data : null;
5606
+ }
5607
+ function errorOf(result) {
5608
+ if (result.error !== null) return result.error;
5609
+ return invalidResponse(result.answer.status);
5610
+ }
5611
+ function userOf(value) {
5612
+ return isRecord(value) && typeof value.id === "string" ? value : null;
5613
+ }
5614
+ var AuthAdminApi = class {
5615
+ #http;
5616
+ #serviceKey;
5617
+ constructor(http, serviceKey) {
5618
+ this.#http = http;
5619
+ this.#serviceKey = serviceKey;
5620
+ }
5621
+ /**
5622
+ * Creates a user with a password: an email, a username or both. It is
5623
+ * made while sign-ups are closed too, and is confirmed unless
5624
+ * `emailConfirm: false`. A username-only user answers `email: null` and
5625
+ * signs in with `signIn({ username, password })`. Refusals come as the
5626
+ * worker says them: USERNAME_TAKEN (409),
5627
+ * USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL (422), PASSWORD_TOO_SHORT ("Use at
5628
+ * least {n} characters."), VALIDATION_ERROR, WRITE_QUOTA_EXCEEDED (429).
5629
+ */
5630
+ async createUser(attributes) {
5631
+ if (!this.#serviceKey)
5632
+ return { data: { user: null }, error: serviceKeyRequired() };
5633
+ const result = await call(this.#http, {
5634
+ method: "POST",
5635
+ path: USERS_PATH,
5636
+ body: attributes,
5637
+ withToken: false
5638
+ });
5639
+ const user = userOf(dataOf(result)?.user);
5640
+ if (user === null) return { data: { user: null }, error: errorOf(result) };
5641
+ return { data: { user }, error: null };
5642
+ }
5643
+ /** The project's users, newest first: page 1 and 50 a page unless given. */
5644
+ async listUsers(params = {}) {
5645
+ const none = { users: [], total: 0 };
5646
+ if (!this.#serviceKey) return { data: none, error: serviceKeyRequired() };
5647
+ const query = new URLSearchParams();
5648
+ if (params.page !== void 0) query.set("page", String(params.page));
5649
+ if (params.perPage !== void 0)
5650
+ query.set("perPage", String(params.perPage));
5651
+ const search = query.toString();
5652
+ const result = await call(this.#http, {
5653
+ method: "GET",
5654
+ path: search === "" ? USERS_PATH : `${USERS_PATH}?${search}`,
5655
+ withToken: false
5656
+ });
5657
+ const data = dataOf(result);
5658
+ const users = Array.isArray(data?.users) ? data.users.map(userOf) : null;
5659
+ if (!data || users === null || users.includes(null) || typeof data.total !== "number")
5660
+ return { data: none, error: errorOf(result) };
5661
+ return {
5662
+ data: { users, total: data.total },
5663
+ error: null
5664
+ };
5665
+ }
5666
+ /**
5667
+ * Deletes a user and its sign-in methods, and ends its sessions. Answers
5668
+ * the user it was; USER_NOT_FOUND (404) for an id that is no user.
5669
+ */
5670
+ async deleteUser(id) {
5671
+ if (!this.#serviceKey)
5672
+ return { data: { user: null }, error: serviceKeyRequired() };
5673
+ const result = await call(this.#http, {
5674
+ method: "DELETE",
5675
+ path: `${USERS_PATH}/${encodeURIComponent(String(id))}`,
5676
+ withToken: false
5677
+ });
5678
+ const user = userOf(dataOf(result)?.user);
5679
+ if (user === null) return { data: { user: null }, error: errorOf(result) };
5680
+ return { data: { user }, error: null };
5681
+ }
5682
+ };
5683
+ var CODE = /^[A-Z0-9_]{1,64}$/;
5684
+ var INVALID_CONFIRMATION = "The confirmation link is not valid. Ask for a new one.";
5685
+ var MESSAGES = {
5686
+ INVALID_STATE: "This sign-in link is not valid or has expired. Start signing in again.",
5687
+ OAUTH_BROWSER_MISMATCH: "This sign-in was started in another browser, or this browser did not keep its cookie. Start signing in again here.",
5688
+ OAUTH_ACCESS_DENIED: "Signing in with the provider was cancelled.",
5689
+ OAUTH_PROVIDER_EMAIL_NOT_VERIFIED: "Your email address is not verified with the provider. Verify it there, then sign in again.",
5690
+ OAUTH_ACCOUNT_NOT_LINKED: "An account with this email already exists and is not linked to this provider. Sign in the way you signed up.",
5691
+ PROVIDER_NOT_CONFIGURED: "Signing in with this provider is not set up for this project.",
5692
+ RATE_LIMITED: "Too many sign-in attempts. Wait a few seconds and try again.",
5693
+ // A first Google or GitHub sign-in while the project's sign-ups are
5694
+ // closed (spec 2026-09-26 §2.1): the worker's own words.
5695
+ SIGNUPS_DISABLED: "Sign-ups are closed for this project.",
5696
+ INTERNAL_ERROR: "Something went wrong. Try again.",
5697
+ PASSWORD_RESET_REQUIRED: "Your email is confirmed. Someone else also signed up with it before it was confirmed, so its password was cleared: choose a new one with a password reset.",
5698
+ TOKEN_EXPIRED: "The confirmation link has expired. Ask for a new one.",
5699
+ USER_NOT_FOUND: INVALID_CONFIRMATION
5700
+ };
5701
+ function flowError(type, raw) {
5702
+ if (!CODE.test(raw))
5703
+ return {
5704
+ code: "AUTH_ERROR",
5705
+ message: "Signing in did not finish. Try again."
5706
+ };
5707
+ if (raw === "INVALID_TOKEN")
5708
+ return {
5709
+ code: raw,
5710
+ message: type === "recovery" ? "The password reset link is not valid, has expired or was already used. Ask for a new one." : INVALID_CONFIRMATION
5711
+ };
5712
+ const message = MESSAGES[raw];
5713
+ if (message !== void 0) return { code: raw, message };
5714
+ if (raw.startsWith("OAUTH_"))
5715
+ return {
5716
+ code: raw,
5717
+ message: `Signing in with the provider did not finish (${raw}). Try again.`
5718
+ };
5719
+ return {
5720
+ code: raw,
5721
+ message: `Signing in did not finish (${raw}). Try again.`
5722
+ };
5723
+ }
5724
+ function base64url(bytes) {
5725
+ let binary = "";
5726
+ for (const byte of bytes) binary += String.fromCharCode(byte);
5727
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
5728
+ }
5729
+ async function createPkcePair() {
5730
+ const subtle = globalThis.crypto?.subtle;
5731
+ if (subtle === void 0) return null;
5732
+ const verifier = base64url(
5733
+ globalThis.crypto.getRandomValues(new Uint8Array(32))
5734
+ );
5735
+ const digest = await subtle.digest(
5736
+ "SHA-256",
5737
+ new TextEncoder().encode(verifier)
5738
+ );
5739
+ return { verifier, challenge: base64url(new Uint8Array(digest)) };
5740
+ }
5741
+ var TOKEN_HEADER = "set-auth-token";
5742
+ var FRAGMENT_PREFIX = "naro_";
5743
+ var OAUTH_PROVIDERS = ["google", "github"];
5744
+ var FLOW_TYPES = [
5745
+ "oauth",
5746
+ "email_verification",
5747
+ "recovery"
5748
+ ];
5749
+ function defaultName(email) {
5750
+ if (typeof email !== "string") return "";
5751
+ const at = email.lastIndexOf("@");
5752
+ return at > 0 ? email.slice(0, at) : email;
5753
+ }
5754
+ function nonEmpty(value) {
5755
+ return typeof value === "string" && value !== "" ? value : null;
5756
+ }
5757
+ function tokenFrom(answer) {
5758
+ return nonEmpty(answer.headers.get(TOKEN_HEADER));
5759
+ }
5760
+ function userFrom(body) {
5761
+ return isRecord(body) && isRecord(body.user) ? body.user : null;
5762
+ }
5763
+ function currentLocation() {
5764
+ if (!isBrowser()) return null;
5765
+ return globalThis.location ?? null;
5766
+ }
5767
+ function currentPage() {
5768
+ const location = currentLocation();
5769
+ if (!location) return void 0;
5770
+ return `${location.origin}${location.pathname}${location.search}`;
5771
+ }
5772
+ function naroFragment() {
5773
+ const hash = currentLocation()?.hash ?? "";
5774
+ if (!hash.startsWith("#")) return null;
5775
+ const params = new URLSearchParams(hash.slice(1));
5776
+ const found = {};
5777
+ for (const [key, value] of params)
5778
+ if (key.startsWith(FRAGMENT_PREFIX)) found[key] = value;
5779
+ return Object.keys(found).length > 0 ? found : null;
5780
+ }
5781
+ function clearFragment() {
5782
+ const location = currentLocation();
5783
+ const history = globalThis.history;
5784
+ if (!location || !history?.replaceState) return;
5785
+ try {
5786
+ history.replaceState(
5787
+ history.state,
5788
+ "",
5789
+ `${location.pathname}${location.search}`
5790
+ );
5791
+ } catch {
5792
+ }
5793
+ }
5794
+ var EXCHANGE_TIMEOUT_MS = 15e3;
5795
+ var inFlight = /* @__PURE__ */ new WeakMap();
5796
+ function sharedInFlight(shared) {
5797
+ return inFlight.get(shared.storage)?.get(shared.key) ?? null;
5798
+ }
5799
+ function share(shared, pending) {
5800
+ let entries = inFlight.get(shared.storage);
5801
+ if (!entries) {
5802
+ entries = /* @__PURE__ */ new Map();
5803
+ inFlight.set(shared.storage, entries);
5804
+ }
5805
+ entries.set(shared.key, pending);
5806
+ void pending.finally(() => {
5807
+ if (entries.get(shared.key) === pending) entries.delete(shared.key);
5808
+ });
5809
+ }
5810
+ var AuthClient = class {
5811
+ /**
5812
+ * The service key's user management — createUser, listUsers, deleteUser,
5813
+ * as in supabase-js (spec 2026-09-26 §2.2). Only on a client made with
5814
+ * `serviceKey`, on a server.
5815
+ */
5816
+ admin;
5817
+ #http;
5818
+ #flow;
5819
+ /** The last `getSessionFromUrl()`, for callers after detection ran it. */
5820
+ #fromUrl = null;
5821
+ constructor(http, flow) {
5822
+ this.#http = http;
5823
+ this.#flow = flow;
5824
+ this.admin = new AuthAdminApi(http, flow.serviceKey);
5825
+ if (flow.detectSessionInUrl) {
5826
+ if (naroFragment() !== null) void this.getSessionFromUrl();
5827
+ else this.#fromUrl = sharedInFlight(flow.shared);
5828
+ }
5829
+ }
5830
+ /**
5831
+ * Creates the account and signs in; the token is stored for later calls.
5832
+ * When the project requires a confirmed email there is no session yet —
5833
+ * `user` comes back, and after the confirmation link the user signs in.
5834
+ * While the project's sign-ups are closed every sign-up answers 403
5835
+ * SIGNUPS_DISABLED; while they are open, a taken `username` answers 409
5836
+ * USERNAME_TAKEN.
5837
+ */
5838
+ async signUp(credentials) {
5839
+ const { email, password, username } = credentials;
5840
+ const name = credentials.name ?? username ?? defaultName(email);
5841
+ const body = { email, password, name };
5842
+ if (username !== void 0) body.username = username;
5843
+ if (credentials.redirectTo !== void 0)
5844
+ body.callbackURL = credentials.redirectTo;
5845
+ return this.#signIn("/auth/sign-up/email", body, false);
5846
+ }
5847
+ /**
5848
+ * Signs in with an email and password, or a username and password (spec
5849
+ * 2026-09-26 §2.3); the token is stored for later calls. A wrong password
5850
+ * answers 401 INVALID_EMAIL_OR_PASSWORD, or INVALID_USERNAME_OR_PASSWORD.
5851
+ */
5852
+ async signIn(credentials) {
5853
+ const { password } = credentials;
5854
+ if (credentials.username !== void 0)
5855
+ return this.#signIn(
5856
+ "/auth/sign-in/username",
5857
+ { username: credentials.username, password },
5858
+ true
5859
+ );
5860
+ return this.#signIn(
5861
+ "/auth/sign-in/email",
5862
+ { email: credentials.email, password },
5863
+ true
5864
+ );
5865
+ }
5866
+ /**
5867
+ * Starts a Google or GitHub sign-in: sends the browser, with a top-level
5868
+ * navigation, to the worker's `GET /_naro/v1/auth/authorize/<provider>`,
5869
+ * which checks `redirectTo`, binds the flow to this browser with a cookie
5870
+ * and goes on to the provider (adversarial review I1). The provider sends
5871
+ * the browser back to `redirectTo`, where `getSessionFromUrl()` (run by
5872
+ * `createClient` on its own) finishes the sign-in. No request is made
5873
+ * here: anything that goes wrong comes back in that page's URL.
5874
+ */
5875
+ async signInWithOAuth(options) {
5876
+ const { provider } = options;
5877
+ const failed = (error) => ({
5878
+ data: { provider, url: null },
5879
+ error
5880
+ });
5881
+ if (!OAUTH_PROVIDERS.includes(provider)) return failed(invalidProvider());
5882
+ await this.#flow.recovery.forget();
5883
+ const pair = await createPkcePair();
5884
+ if (!pair) return failed(cryptoUnavailable());
5885
+ await this.#flow.verifiers.add(pair.challenge, pair.verifier);
5886
+ const redirectTo = options.redirectTo ?? currentPage();
5887
+ const url = new URL(
5888
+ `${this.#http.baseUrl}/_naro/v1/auth/authorize/${provider}`
5889
+ );
5890
+ if (redirectTo !== void 0)
5891
+ url.searchParams.set("redirectTo", redirectTo);
5892
+ url.searchParams.set("code_challenge", pair.challenge);
5893
+ const location = currentLocation();
5894
+ if (location && !options.skipBrowserRedirect) location.assign(url.href);
5895
+ return { data: { provider, url: url.href }, error: null };
5896
+ }
5897
+ /**
5898
+ * Trades the one-time code a sign-in redirect brought for the session,
5899
+ * with the verifier this browser kept when it began that flow — `flow` is
5900
+ * the redirect's `naro_flow`; without it, the newest flow's. Most apps
5901
+ * never call it: `getSessionFromUrl()` does.
5902
+ */
5903
+ async exchangeCodeForSession(code, flow) {
5904
+ const signedOut = { session: null, user: null };
5905
+ const verifier = await this.#flow.verifiers.take(flow);
5906
+ if (verifier === null)
5907
+ return { data: signedOut, error: codeVerifierMissing() };
5908
+ const result = await call(this.#http, {
5909
+ method: "POST",
5910
+ path: "/auth/exchange-code",
5911
+ body: { code, codeVerifier: verifier },
5912
+ withToken: false,
5913
+ // The page's queries wait on this; they must not wait forever.
5914
+ timeoutMs: EXCHANGE_TIMEOUT_MS
5915
+ });
5916
+ if (result.error !== null) return { data: signedOut, error: result.error };
5917
+ const { body } = result.answer;
5918
+ const token = tokenFrom(result.answer);
5919
+ const user = userFrom(body);
5920
+ if (token === null || user === null || !isRecord(body) || !isRecord(body.session))
5921
+ return { data: signedOut, error: invalidResponse(result.answer.status) };
5922
+ await this.#http.tokens.set(token);
5923
+ await this.#flow.recovery.forget();
5924
+ return { data: { session: body.session, user }, error: null };
5925
+ }
5926
+ /**
5927
+ * Reads what a sign-in redirect left in this page's URL fragment, then
5928
+ * clears it: a provider sign-in or a confirmation link hands over a
5929
+ * session (traded here), a reset link a recovery token (kept for
5930
+ * `updateUser`), and a failure its `code` and `message`. With nothing of
5931
+ * Naro's in the URL, `type` is null — or, after `createClient` already
5932
+ * read it, the answer it got.
5933
+ */
5934
+ getSessionFromUrl() {
5935
+ const fragment = naroFragment();
5936
+ if (fragment === null)
5937
+ return this.#fromUrl ?? Promise.resolve({
5938
+ data: { session: null, user: null, type: null },
5939
+ error: null
5940
+ });
5941
+ clearFragment();
5942
+ const answer = this.#answerOf(fragment);
5943
+ this.#fromUrl = answer;
5944
+ share(this.#flow.shared, answer);
5945
+ return answer;
5946
+ }
5947
+ async #answerOf(fragment) {
5948
+ const named = fragment.naro_type;
5949
+ const type = named && FLOW_TYPES.includes(named) ? named : null;
5950
+ const none = { session: null, user: null, type };
5951
+ if (fragment.naro_error) {
5952
+ if (fragment.naro_flow)
5953
+ await this.#flow.verifiers.take(fragment.naro_flow);
5954
+ return { data: none, error: flowError(type, fragment.naro_error) };
5955
+ }
5956
+ if (type === "recovery" && fragment.naro_token) {
5957
+ await this.#http.tokens.clear();
5958
+ await this.#flow.recovery.keep(fragment.naro_token);
5959
+ return { data: none, error: null };
5960
+ }
5961
+ if (fragment.naro_code) {
5962
+ const traded = await this.exchangeCodeForSession(
5963
+ fragment.naro_code,
5964
+ fragment.naro_flow
5965
+ );
5966
+ return { data: { ...traded.data, type }, error: traded.error };
5967
+ }
5968
+ return { data: none, error: null };
5969
+ }
5970
+ /**
5971
+ * Emails a password reset link, when the address has an account. The
5972
+ * answer is the same either way — it does not say whether it had one.
5973
+ */
5974
+ async resetPasswordForEmail(email, options = {}) {
5975
+ const body = { email };
5976
+ if (options.redirectTo !== void 0) body.redirectTo = options.redirectTo;
5977
+ const result = await call(this.#http, {
5978
+ method: "POST",
5979
+ path: "/auth/request-password-reset",
5980
+ body,
5981
+ withToken: false
5982
+ });
5983
+ return { data: null, error: result.error };
5984
+ }
5985
+ /**
5986
+ * Sets a new password in the recovery flow: on the page the reset link
5987
+ * opened, after `getSessionFromUrl()` read its token. Signing in with the
5988
+ * new password comes after — a reset ends every session of the account.
5989
+ * (Changing the password of a signed-in user is not offered: spec §14
5990
+ * "(P1-2 리뷰 반영)".)
5991
+ *
5992
+ * Like supabase-js, it acts on the recovery link's account, which took the
5993
+ * place of any signed-in session when the link was read — so the token is
5994
+ * used only while no session is signed in on this browser, for 15 minutes
5995
+ * after the link was opened, and never after a sign-in, sign-up or
5996
+ * sign-out forgot it (PR #68 review 6). Otherwise PASSWORD_RECOVERY_REQUIRED,
5997
+ * with no request.
5998
+ */
5999
+ async updateUser(attributes) {
6000
+ await this.#pendingUrl();
6001
+ if (await this.#http.tokens.get() !== null)
6002
+ return { data: null, error: passwordRecoveryRequired() };
6003
+ const token = await this.#flow.recovery.current();
6004
+ if (token === null)
6005
+ return { data: null, error: passwordRecoveryRequired() };
6006
+ const result = await call(this.#http, {
6007
+ method: "POST",
6008
+ path: "/auth/reset-password",
6009
+ body: { newPassword: attributes.password, token },
6010
+ withToken: false
6011
+ });
6012
+ if (result.error === null || result.error.code === "INVALID_TOKEN")
6013
+ await this.#flow.recovery.forgetIf(token);
6014
+ return { data: null, error: result.error };
6015
+ }
6016
+ /**
6017
+ * Emails the confirmation link again, when the address has an account that
6018
+ * is not confirmed yet. The answer does not say which.
6019
+ */
6020
+ async resendVerification(email, options = {}) {
6021
+ const body = { email };
6022
+ if (options.redirectTo !== void 0) body.callbackURL = options.redirectTo;
6023
+ const result = await call(this.#http, {
6024
+ method: "POST",
6025
+ path: "/auth/send-verification-email",
6026
+ body,
6027
+ withToken: false
6028
+ });
6029
+ return { data: null, error: result.error };
6030
+ }
6031
+ /**
6032
+ * Ends the session on the worker and forgets the token. The token is
6033
+ * forgotten even when the worker cannot be reached — the error still comes
6034
+ * back, since the session then lives on until it expires.
6035
+ */
6036
+ async signOut() {
6037
+ await this.#pendingUrl();
6038
+ await this.#flow.recovery.forget();
6039
+ const tokens = this.#http.tokens;
6040
+ const token = await tokens.get();
6041
+ if (token === null) return { data: null, error: null };
6042
+ const result = await call(this.#http, {
6043
+ method: "POST",
6044
+ path: "/auth/sign-out",
6045
+ withToken: true
6046
+ });
6047
+ await tokens.clearIf(result.answer?.token ?? token);
6048
+ return { data: null, error: result.error };
6049
+ }
6050
+ /**
6051
+ * The signed-in session and user. Signed out: both null, no request. A
6052
+ * stored token the worker no longer accepts is forgotten and reported as
6053
+ * INVALID_SESSION once (spec §11.1: a session never quietly turns anon).
6054
+ * Waits for a sign-in the page URL carried, if `createClient` found one.
6055
+ */
6056
+ async getSession() {
6057
+ await this.#pendingUrl();
6058
+ const signedOut = { session: null, user: null };
6059
+ const tokens = this.#http.tokens;
6060
+ if (await tokens.get() === null) return { data: signedOut, error: null };
6061
+ const result = await call(this.#http, {
6062
+ method: "GET",
6063
+ path: "/auth/get-session",
6064
+ withToken: true
6065
+ });
6066
+ if (result.error !== null) return { data: signedOut, error: result.error };
6067
+ const { body, token } = result.answer;
6068
+ if (body === null) {
6069
+ if (token !== null) await tokens.clearIf(token);
6070
+ return { data: signedOut, error: invalidSession() };
6071
+ }
6072
+ const user = userFrom(body);
6073
+ if (!isRecord(body) || !isRecord(body.session) || user === null)
6074
+ return { data: signedOut, error: invalidResponse(result.answer.status) };
6075
+ const renewed = tokenFrom(result.answer);
6076
+ if (renewed !== null && token !== null && renewed !== token)
6077
+ await tokens.replaceIf(token, renewed);
6078
+ return { data: { session: body.session, user }, error: null };
6079
+ }
6080
+ /** `{ data: { user } }`; `user` is null when signed out. */
6081
+ async getUser() {
6082
+ const { data, error } = await this.getSession();
6083
+ return { data: { user: data.user }, error };
6084
+ }
6085
+ /**
6086
+ * Resolves once a sign-in the page URL carried has been traded for its
6087
+ * session (or failed), with its error. Queries wait for it, so the first
6088
+ * one after a provider's redirect goes out signed in rather than as anon.
6089
+ * The exchange gives up after 15 seconds (TIMEOUT, PR #68 review 10):
6090
+ * then the queries go out as anon rather than wait forever.
6091
+ */
6092
+ async ready() {
6093
+ if (!this.#fromUrl) return { error: null };
6094
+ return { error: (await this.#fromUrl).error };
6095
+ }
6096
+ /** Waits for a URL sign-in in flight; its outcome is not this call's. */
6097
+ async #pendingUrl() {
6098
+ if (this.#fromUrl) await this.#fromUrl;
6099
+ }
6100
+ async #signIn(path, body, needsToken) {
6101
+ await this.#flow.recovery.forget();
6102
+ const result = await call(this.#http, {
6103
+ method: "POST",
6104
+ path,
6105
+ body,
6106
+ withToken: false
6107
+ });
6108
+ if (result.error !== null)
6109
+ return { data: { user: null }, error: result.error };
6110
+ const { body: answerBody } = result.answer;
6111
+ const user = userFrom(answerBody);
6112
+ const token = tokenFrom(result.answer);
6113
+ const opened = isRecord(answerBody) && nonEmpty(answerBody.token) !== null;
6114
+ if (user === null || (needsToken || opened) && token === null)
6115
+ return {
6116
+ data: { user: null },
6117
+ error: invalidResponse(result.answer.status)
6118
+ };
6119
+ if (token !== null) await this.#http.tokens.set(token);
6120
+ return { data: { user }, error: null };
6121
+ }
6122
+ };
6123
+ var FLOW_TTL_MS = 10 * 6e4;
6124
+ var MAX_FLOWS = 10;
6125
+ function isEntry(value) {
6126
+ return typeof value === "object" && value !== null && typeof value.verifier === "string" && typeof value.expiresAt === "number";
6127
+ }
6128
+ function live(text, now) {
6129
+ if (text === null) return {};
6130
+ let parsed;
6131
+ try {
6132
+ parsed = JSON.parse(text);
6133
+ } catch {
6134
+ return {};
6135
+ }
6136
+ if (typeof parsed !== "object" || parsed === null) return {};
6137
+ const entries = {};
6138
+ for (const [flow, entry] of Object.entries(parsed))
6139
+ if (isEntry(entry) && entry.expiresAt > now) entries[flow] = entry;
6140
+ return entries;
6141
+ }
6142
+ function pick(entries, oldest) {
6143
+ let found;
6144
+ for (const pair of Object.entries(entries))
6145
+ if (found === void 0 || (oldest ? pair[1].expiresAt < found[1].expiresAt : pair[1].expiresAt > found[1].expiresAt))
6146
+ found = pair;
6147
+ return found?.[0];
6148
+ }
6149
+ function serialize(entries) {
6150
+ return Object.keys(entries).length > 0 ? JSON.stringify(entries) : null;
6151
+ }
6152
+ var FlowVerifiers = class {
6153
+ #store;
6154
+ #now;
6155
+ constructor(store, now = Date.now) {
6156
+ this.#store = store;
6157
+ this.#now = now;
6158
+ }
6159
+ /** Keeps `verifier` for the flow `flow`, for ten minutes. */
6160
+ async add(flow, verifier) {
6161
+ const now = this.#now();
6162
+ await this.#store.update((text) => {
6163
+ const entries = live(text, now);
6164
+ entries[flow] = { verifier, expiresAt: now + FLOW_TTL_MS };
6165
+ while (Object.keys(entries).length > MAX_FLOWS) {
6166
+ const oldest = pick(entries, true);
6167
+ if (oldest === void 0) break;
6168
+ delete entries[oldest];
6169
+ }
6170
+ return serialize(entries);
6171
+ });
6172
+ }
6173
+ /**
6174
+ * The verifier of `flow`, taken — one flow, one use. Without a flow id
6175
+ * (a code traded by hand), the newest one. Null when there is none, or it
6176
+ * expired.
6177
+ */
6178
+ async take(flow) {
6179
+ const now = this.#now();
6180
+ let taken = null;
6181
+ await this.#store.update((text) => {
6182
+ const entries = live(text, now);
6183
+ const id = flow ?? pick(entries, false);
6184
+ if (id !== void 0 && Object.hasOwn(entries, id)) {
6185
+ taken = entries[id]?.verifier ?? null;
6186
+ delete entries[id];
6187
+ }
6188
+ return serialize(entries);
6189
+ });
6190
+ return taken;
6191
+ }
6192
+ };
6193
+ var RECOVERY_TTL_MS = 15 * 6e4;
6194
+ function parse(text) {
6195
+ if (text === null) return null;
6196
+ try {
6197
+ const value = JSON.parse(text);
6198
+ if (typeof value === "object" && value !== null && typeof value.token === "string" && value.token !== "" && typeof value.expiresAt === "number")
6199
+ return value;
6200
+ } catch {
6201
+ }
6202
+ return null;
6203
+ }
6204
+ var RecoveryToken = class {
6205
+ #store;
6206
+ #now;
6207
+ constructor(store, now = () => Date.now()) {
6208
+ this.#store = store;
6209
+ this.#now = now;
6210
+ }
6211
+ /** Keeps the token a recovery link brought, for 15 minutes. */
6212
+ async keep(token) {
6213
+ const kept = { token, expiresAt: this.#now() + RECOVERY_TTL_MS };
6214
+ await this.#store.set(JSON.stringify(kept));
6215
+ }
6216
+ /** The token, while it lasts; an expired or unreadable one is cleared. */
6217
+ async current() {
6218
+ const now = this.#now();
6219
+ let token = null;
6220
+ await this.#store.update((text) => {
6221
+ const kept = parse(text);
6222
+ if (kept === null || kept.expiresAt <= now) return null;
6223
+ token = kept.token;
6224
+ return text;
6225
+ });
6226
+ return token;
6227
+ }
6228
+ /** Forgets it: a sign-in, a sign-up, a sign-out. */
6229
+ async forget() {
6230
+ await this.#store.clear();
6231
+ }
6232
+ /** Forgets it if it is still `token` — used, or refused as used. */
6233
+ async forgetIf(token) {
6234
+ await this.#store.update(
6235
+ (text) => parse(text)?.token === token ? null : text
6236
+ );
6237
+ }
6238
+ };
6239
+ function memoryStorage() {
6240
+ const items22 = /* @__PURE__ */ new Map();
6241
+ return {
6242
+ getItem: (key) => items22.get(key) ?? null,
6243
+ setItem: (key, value) => {
6244
+ items22.set(key, value);
6245
+ },
6246
+ removeItem: (key) => {
6247
+ items22.delete(key);
6248
+ }
6249
+ };
6250
+ }
6251
+ function defaultStorage() {
6252
+ if (!isBrowser()) return memoryStorage();
6253
+ try {
6254
+ const storage = globalThis.localStorage;
6255
+ if (storage === void 0 || storage === null) return memoryStorage();
6256
+ const probe = "naro-storage-probe";
6257
+ storage.setItem(probe, probe);
6258
+ storage.removeItem(probe);
6259
+ return storage;
6260
+ } catch {
6261
+ return memoryStorage();
6262
+ }
6263
+ }
6264
+ function storageKey(baseUrl) {
6265
+ return `naro-auth-token:${baseUrl}`;
6266
+ }
6267
+ function verifierKey(baseUrl) {
6268
+ return `naro-auth-code-verifiers:${baseUrl}`;
6269
+ }
6270
+ function recoveryKey(baseUrl) {
6271
+ return `naro-auth-recovery:${baseUrl}`;
6272
+ }
6273
+ var TokenStore = class {
6274
+ #storage;
6275
+ #key;
6276
+ #fallback = null;
6277
+ /** Settles when the last queued operation has finished. */
6278
+ #queue = Promise.resolve();
6279
+ constructor(storage, key) {
6280
+ this.#storage = storage;
6281
+ this.#key = key;
6282
+ }
6283
+ get() {
6284
+ return this.#serial(() => this.#get());
6285
+ }
6286
+ set(token) {
6287
+ return this.#serial(() => this.#set(token));
6288
+ }
6289
+ clear() {
6290
+ return this.#serial(() => this.#clear());
6291
+ }
6292
+ /**
6293
+ * Clears only if the stored token is still the one that was rejected. A
6294
+ * request that left with an old token can come back after the user signed
6295
+ * in again; its INVALID_SESSION must not sign the new session out. The
6296
+ * read and the removal are one operation: with an async storage, a sign-in
6297
+ * that lands between them would otherwise be the token removed.
6298
+ */
6299
+ clearIf(token) {
6300
+ return this.#serial(async () => {
6301
+ if (await this.#get() === token) await this.#clear();
6302
+ });
6303
+ }
6304
+ /**
6305
+ * Reads the value, hands it to `transform` and stores what that returns
6306
+ * (null clears it), as one operation — the per-flow verifiers
6307
+ * (`flows.ts`) change one entry of a JSON object this way.
6308
+ */
6309
+ update(transform) {
6310
+ return this.#serial(async () => {
6311
+ const next = transform(await this.#get());
6312
+ if (next === null) await this.#clear();
6313
+ else await this.#set(next);
6314
+ });
6315
+ }
6316
+ /** Stores `next` only if `expected` is still stored, as one operation. */
6317
+ replaceIf(expected, next) {
6318
+ return this.#serial(async () => {
6319
+ if (await this.#get() === expected) await this.#set(next);
6320
+ });
6321
+ }
6322
+ /**
6323
+ * Runs `operation` once every earlier one has finished. The operations
6324
+ * below never reject, and none of them calls a public method (that would
6325
+ * wait on itself).
6326
+ */
6327
+ #serial(operation) {
6328
+ const result = this.#queue.then(operation);
6329
+ this.#queue = result.catch(() => void 0);
6330
+ return result;
6331
+ }
6332
+ async #get() {
6333
+ if (this.#fallback) return this.#fallback.token;
6334
+ try {
6335
+ const token = await this.#storage.getItem(this.#key);
6336
+ return typeof token === "string" && token !== "" ? token : null;
6337
+ } catch {
6338
+ await this.#fallBack(null, true);
6339
+ return null;
6340
+ }
6341
+ }
6342
+ async #set(token) {
6343
+ if (this.#fallback) {
6344
+ this.#fallback.token = token;
6345
+ return;
6346
+ }
6347
+ try {
6348
+ await this.#storage.setItem(this.#key, token);
6349
+ } catch {
6350
+ await this.#fallBack(token, true);
6351
+ }
6352
+ }
6353
+ async #clear() {
6354
+ if (this.#fallback) {
6355
+ this.#fallback.token = null;
6356
+ return;
6357
+ }
6358
+ try {
6359
+ await this.#storage.removeItem(this.#key);
6360
+ } catch {
6361
+ await this.#fallBack(null, false);
6362
+ }
6363
+ }
6364
+ /**
6365
+ * Stops using the storage, keeping `token` in memory. The token the storage
6366
+ * still holds is removed once, if it lets us: left there it comes back on
6367
+ * the next load — after a sign-out that could not read it, or over a
6368
+ * sign-in whose write failed (the previous user's session).
6369
+ */
6370
+ async #fallBack(token, removeStored) {
6371
+ this.#fallback = { token };
6372
+ if (!removeStored) return;
6373
+ try {
6374
+ await this.#storage.removeItem(this.#key);
6375
+ } catch {
6376
+ }
6377
+ }
6378
+ };
6379
+ function isSpace2(char) {
6380
+ return char.trim() === "";
6381
+ }
6382
+ function tokenize(input) {
6383
+ const tokens = [];
6384
+ let index = 0;
6385
+ const at = () => input[index] ?? "";
6386
+ for (; ; ) {
6387
+ while (index < input.length && isSpace2(at())) index++;
6388
+ let token;
6389
+ if (at() === '"') {
6390
+ index++;
6391
+ let name = "";
6392
+ for (; ; ) {
6393
+ if (index >= input.length)
6394
+ return `has an unclosed quote in column ${String(tokens.length + 1)}`;
6395
+ const char = at();
6396
+ index++;
6397
+ if (char !== '"') name += char;
6398
+ else if (at() === '"') {
6399
+ name += '"';
6400
+ index++;
6401
+ } else break;
6402
+ }
6403
+ while (index < input.length && isSpace2(at())) index++;
6404
+ if (index < input.length && at() !== ",")
6405
+ return `has text after the closing quote of column ${String(tokens.length + 1)}`;
6406
+ token = { name, quoted: true };
6407
+ } else {
6408
+ let name = "";
6409
+ while (index < input.length && at() !== ",") {
6410
+ if (at() === '"')
6411
+ return `has a quote inside column ${String(tokens.length + 1)}; quote the whole name`;
6412
+ name += at();
6413
+ index++;
6414
+ }
6415
+ token = { name: name.trim(), quoted: false };
6416
+ }
6417
+ tokens.push(token);
6418
+ if (index >= input.length) return tokens;
6419
+ index++;
6420
+ }
6421
+ }
6422
+ function parseColumns(input) {
6423
+ if (input === void 0) return { ok: true, columns: "*" };
6424
+ if (typeof input !== "string")
6425
+ return { ok: false, reason: "must be a string of column names" };
6426
+ const tokens = tokenize(input);
6427
+ if (typeof tokens === "string") return { ok: false, reason: tokens };
6428
+ const star = tokens.findIndex((token) => !token.quoted && token.name === "*");
6429
+ if (star >= 0) {
6430
+ if (tokens.length === 1) return { ok: true, columns: "*" };
6431
+ return { ok: false, reason: '"*" cannot be combined with column names' };
6432
+ }
6433
+ const empty = tokens.findIndex((token) => token.name === "");
6434
+ if (empty >= 0)
6435
+ return {
6436
+ ok: false,
6437
+ reason: tokens.length === 1 ? "must name at least one column" : `has an empty column name at position ${String(empty + 1)}`
6438
+ };
6439
+ return { ok: true, columns: tokens.map((token) => token.name) };
6440
+ }
6441
+ var MAX_FILTER_DEPTH2 = 4;
6442
+ var OPERATORS2 = {
6443
+ eq: "eq",
6444
+ neq: "neq",
6445
+ gt: "gt",
6446
+ gte: "gte",
6447
+ lt: "lt",
6448
+ lte: "lte",
6449
+ in: "in",
6450
+ is: "is",
6451
+ like: "like",
6452
+ ilike: "ilike",
6453
+ contains: "contains",
6454
+ containedBy: "containedBy",
6455
+ overlaps: "overlaps",
6456
+ cs: "contains",
6457
+ cd: "containedBy",
6458
+ ov: "overlaps"
6459
+ };
6460
+ function operatorOf2(name) {
6461
+ return typeof name === "string" && Object.hasOwn(OPERATORS2, name) ? OPERATORS2[name] : void 0;
6462
+ }
6463
+ var Refusal2 = class extends Error {
6464
+ };
6465
+ function refuse3(reason) {
6466
+ throw new Refusal2(reason);
6467
+ }
6468
+ var LIST_HINT2 = {
6469
+ in: ["(", ")", "needs a list in parentheses, like in.(a,b),"],
6470
+ array: ["{", "}", "needs a list in braces, like cs.{a,b},"]
6471
+ };
6472
+ function isSpace22(char) {
6473
+ return char !== "" && char.trim() === "";
6474
+ }
6475
+ function looseWord2(text) {
6476
+ return text.replace(/\s+/g, "").toLowerCase().replace(/\(\)$/, "");
6477
+ }
6478
+ var Parser2 = class {
6479
+ constructor(text, tokens = {}) {
6480
+ this.text = text;
6481
+ this.tokens = tokens;
6482
+ }
6483
+ index = 0;
6484
+ /**
6485
+ * A token at the next value, followed only by spaces and then one of
6486
+ * `stops` or the end — `auth.uid()x` is text, not the token.
6487
+ */
6488
+ token(stops) {
6489
+ this.skipSpaces();
6490
+ const at = this.index;
6491
+ for (const [word, value] of Object.entries(this.tokens)) {
6492
+ if (!this.text.startsWith(word, at)) continue;
6493
+ let end = at + word.length;
6494
+ while (isSpace22(this.text[end] ?? "")) end++;
6495
+ const next = this.text[end] ?? "";
6496
+ if (next !== "" && !stops.includes(next)) continue;
6497
+ this.index = end;
6498
+ return { value, word, at };
6499
+ }
6500
+ return void 0;
6501
+ }
6502
+ at() {
6503
+ return this.text[this.index] ?? "";
6504
+ }
6505
+ /**
6506
+ * An unquoted value that is one of the tokens misspelled — `auth.uid`,
6507
+ * `AUTH.UID` — is refused: read as text it would compare with the words
6508
+ * themselves (사용자 리뷰 반영). Quoted, it is the text.
6509
+ */
6510
+ notNearToken(value, at) {
6511
+ const loose = looseWord2(value);
6512
+ for (const word of Object.keys(this.tokens))
6513
+ if (loose !== "" && loose === looseWord2(word))
6514
+ refuse3(
6515
+ `has ${JSON.stringify(value)} ${this.where(at)}: did you mean ${word}? Quote it to mean the text`
6516
+ );
6517
+ }
6518
+ /** `scalar`, with an unquoted value held to notNearToken. */
6519
+ bareOrQuoted(stops) {
6520
+ this.skipSpaces();
6521
+ const at = this.index;
6522
+ const quoted = this.at() === '"';
6523
+ const text = this.scalar(stops);
6524
+ if (!quoted) this.notNearToken(text, at);
6525
+ return { text, quoted };
6526
+ }
6527
+ where(index = this.index) {
6528
+ return `at character ${String(index + 1)}`;
6529
+ }
6530
+ skipSpaces() {
6531
+ while (isSpace22(this.at())) this.index++;
6532
+ }
6533
+ /** `item ("," item)*`, up to a `)` or the end, inside `level` groups. */
6534
+ list(level) {
6535
+ const nodes = [];
6536
+ for (; ; ) {
6537
+ nodes.push(this.item(level));
6538
+ this.skipSpaces();
6539
+ const char = this.at();
6540
+ if (char === ",") this.index++;
6541
+ else if (char === ")" || char === "") return nodes;
6542
+ else refuse3(`has unexpected text ${this.where()}`);
6543
+ }
6544
+ }
6545
+ item(level) {
6546
+ this.skipSpaces();
6547
+ const start = this.index;
6548
+ const char = this.at();
6549
+ if (char === "" || char === "," || char === ")")
6550
+ refuse3(`has an empty item ${this.where()}`);
6551
+ const group = /^(not\.)?(and|or)\(/.exec(this.text.slice(start));
6552
+ if (!group) return this.leaf(level);
6553
+ const negated = group[1] !== void 0;
6554
+ const inner = level + (negated ? 2 : 1);
6555
+ if (inner > MAX_FILTER_DEPTH2)
6556
+ refuse3(`nests deeper than ${String(MAX_FILTER_DEPTH2)} groups`);
6557
+ const open = start + group[0].length - 1;
6558
+ this.index = open + 1;
6559
+ const children = this.list(inner);
6560
+ if (this.at() !== ")") refuse3(`has an unclosed "(" ${this.where(open)}`);
6561
+ this.index++;
6562
+ const node = group[2] === "or" ? { or: children } : { and: children };
6563
+ return negated ? { not: node } : node;
6564
+ }
6565
+ /** `column.[not.]operator.value`. */
6566
+ leaf(level) {
6567
+ const start = this.index;
6568
+ const column = this.at() === '"' ? this.quoted() : this.word().trim();
6569
+ if (column === "" || this.at() !== ".")
6570
+ refuse3(`needs column.operator.value ${this.where(start)}`);
6571
+ this.index++;
6572
+ let nameAt = this.index;
6573
+ let name = this.word();
6574
+ let negated = false;
6575
+ if (name === "not" && this.at() === ".") {
6576
+ if (level + 1 > MAX_FILTER_DEPTH2)
6577
+ refuse3(`nests deeper than ${String(MAX_FILTER_DEPTH2)} groups`);
6578
+ negated = true;
6579
+ this.index++;
6580
+ nameAt = this.index;
6581
+ name = this.word();
6582
+ }
6583
+ const operator = operatorOf2(name);
6584
+ if (this.at() !== ".") {
6585
+ if (name !== "" && operator === void 0)
6586
+ refuse3(`has an unknown operator "${name}" ${this.where(nameAt)}`);
6587
+ refuse3(`needs column.operator.value ${this.where(start)}`);
6588
+ }
6589
+ if (operator === void 0)
6590
+ refuse3(`has an unknown operator "${name}" ${this.where(nameAt)}`);
6591
+ this.index++;
6592
+ const leaf = { operator, column, value: this.value(operator) };
6593
+ return negated ? { not: leaf } : leaf;
6594
+ }
6595
+ /** Up to the next `.`, `,`, `(`, `)` or the end. */
6596
+ word() {
6597
+ const start = this.index;
6598
+ while (this.index < this.text.length && !".,()".includes(this.at()))
6599
+ this.index++;
6600
+ return this.text.slice(start, this.index);
6601
+ }
6602
+ /** `"…"`, with `\` making the next character literal. */
6603
+ quoted() {
6604
+ const start = this.index;
6605
+ this.index++;
6606
+ let text = "";
6607
+ for (; ; ) {
6608
+ if (this.index >= this.text.length)
6609
+ refuse3(`has an unclosed quote ${this.where(start)}`);
6610
+ const char = this.at();
6611
+ this.index++;
6612
+ if (char === '"') return text;
6613
+ if (char === "\\" && this.index < this.text.length) {
6614
+ text += this.at();
6615
+ this.index++;
6616
+ } else text += char;
6617
+ }
6618
+ }
6619
+ /** A quoted value, or everything up to `stops`, trimmed. */
6620
+ scalar(stops) {
6621
+ this.skipSpaces();
6622
+ if (this.at() === '"') {
6623
+ const text = this.quoted();
6624
+ this.skipSpaces();
6625
+ if (this.at() !== "" && !stops.includes(this.at()))
6626
+ refuse3(`has text after a closing quote ${this.where()}`);
6627
+ return text;
6628
+ }
6629
+ const start = this.index;
6630
+ while (this.index < this.text.length && !stops.includes(this.at()))
6631
+ this.index++;
6632
+ return this.text.slice(start, this.index).trim();
6633
+ }
6634
+ /**
6635
+ * `(a,b)` for `in`: text values, except that an unquoted `true` or `false`
6636
+ * is a boolean (사용자 리뷰 반영, as in `eq`). `{a,b}` for the array
6637
+ * operators: text, except that an unquoted exact decimal is a number
6638
+ * (적대적 리뷰 반영, narrowed by 사용자 리뷰 반영) — json_each's values
6639
+ * have no type affinity to turn the text "1" into 1, so as text `cs.{1}`
6640
+ * matched no numeric array and `not.ov.{1}` matched every row. Quoted,
6641
+ * `"1"` and `"true"` stay text.
6642
+ */
6643
+ listValue(kind) {
6644
+ const [open, close, hint] = LIST_HINT2[kind];
6645
+ this.skipSpaces();
6646
+ const openAt = this.index;
6647
+ if (this.at() !== open) refuse3(`${hint} ${this.where()}`);
6648
+ this.index++;
6649
+ this.skipSpaces();
6650
+ const values = [];
6651
+ if (this.at() === close) {
6652
+ this.index++;
6653
+ return values;
6654
+ }
6655
+ for (; ; ) {
6656
+ this.skipSpaces();
6657
+ const itemAt = this.index;
6658
+ const token = this.token(`,${close}`);
6659
+ if (token) values.push(token.value);
6660
+ else {
6661
+ const { text: value, quoted } = this.bareOrQuoted(`,${close}`);
6662
+ if (!quoted && value === "") {
6663
+ if (this.index >= this.text.length)
6664
+ refuse3(`has an unclosed "${open}" ${this.where(openAt)}`);
6665
+ refuse3(`has an empty item ${this.where(itemAt)}`);
6666
+ }
6667
+ values.push(
6668
+ quoted ? value : kind === "array" ? exactNumber2(value) : truth2(value)
6669
+ );
6670
+ }
6671
+ if (this.index >= this.text.length)
6672
+ refuse3(`has an unclosed "${open}" ${this.where(openAt)}`);
6673
+ const char = this.at();
6674
+ this.index++;
6675
+ if (char === close) return values;
6676
+ }
6677
+ }
6678
+ /** The value after `operator.`. */
6679
+ value(operator) {
6680
+ const start = this.index;
6681
+ switch (operator) {
6682
+ case "in":
6683
+ return this.listValue("in");
6684
+ case "contains":
6685
+ case "containedBy":
6686
+ case "overlaps":
6687
+ return this.listValue("array");
6688
+ case "is":
6689
+ return keyword2(this.scalar(",)"), start);
6690
+ case "like":
6691
+ case "ilike": {
6692
+ const token = this.token(",)");
6693
+ if (token)
6694
+ refuse3(
6695
+ `cannot use ${token.word} with ${operator} ${this.where(token.at)}`
6696
+ );
6697
+ return likePattern2(this.bareOrQuoted(",)").text);
6698
+ }
6699
+ case "eq":
6700
+ case "neq": {
6701
+ const token = this.token(",)");
6702
+ if (token) return token.value;
6703
+ const { text, quoted } = this.bareOrQuoted(",)");
6704
+ return quoted ? text : truth2(text);
6705
+ }
6706
+ default: {
6707
+ const token = this.token(",)");
6708
+ return token ? token.value : this.bareOrQuoted(",)").text;
6709
+ }
6710
+ }
6711
+ }
6712
+ };
6713
+ function likePattern2(pattern) {
6714
+ return pattern.replaceAll("*", "%");
6715
+ }
6716
+ function truth2(text) {
6717
+ const word = text.toLowerCase();
6718
+ if (word === "true") return true;
6719
+ if (word === "false") return false;
6720
+ return text;
6721
+ }
6722
+ var DECIMAL3 = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
6723
+ function exactNumber2(text) {
6724
+ if (!DECIMAL3.test(text)) return text;
6725
+ const number = Number(text);
6726
+ if (!text.includes(".")) return Number.isSafeInteger(number) ? number : text;
6727
+ const trimmed = text.replace(/\.?0+$/, "");
6728
+ return String(number) === trimmed ? number : text;
6729
+ }
6730
+ function keyword2(text, at) {
6731
+ const word = text.toLowerCase();
6732
+ if (word === "null") return null;
6733
+ if (word === "true") return true;
6734
+ if (word === "false") return false;
6735
+ return refuse3(
6736
+ `needs null, true or false after is at character ${String(at + 1)}`
6737
+ );
6738
+ }
6739
+ function attempt2(parse2) {
6740
+ try {
6741
+ return { ok: true, node: parse2() };
6742
+ } catch (error) {
6743
+ if (error instanceof Refusal2) return { ok: false, reason: error.message };
6744
+ throw error;
6745
+ }
6746
+ }
6747
+ function parseLogic2(text, kind, options = {}) {
6748
+ if (typeof text !== "string")
6749
+ return { ok: false, reason: "must be a filter string or a function" };
6750
+ if (text.trim() === "") return { ok: false, reason: "must not be empty" };
6751
+ return attempt2(() => {
6752
+ if (options.group === false) {
6753
+ const alone = items2(text, options.tokens, 0);
6754
+ if (alone.length === 1 && alone[0]) return alone[0];
6755
+ }
6756
+ const nodes = items2(text, options.tokens, 1);
6757
+ return kind === "or" ? { or: nodes } : { and: nodes };
6758
+ });
6759
+ }
6760
+ function items2(text, tokens, level) {
6761
+ const parser = new Parser2(text, tokens);
6762
+ const nodes = parser.list(level);
6763
+ if (parser.at() === ")") refuse3(`has an unmatched ")" ${parser.where()}`);
6764
+ return nodes;
6765
+ }
6766
+ function conditionOf(column, name, value) {
6767
+ const operator = operatorOf2(name);
6768
+ if (operator === void 0)
6769
+ return { ok: false, reason: `has an unknown operator "${String(name)}"` };
6770
+ if (typeof value !== "string")
6771
+ return {
6772
+ ok: true,
6773
+ node: {
6774
+ operator,
6775
+ column,
6776
+ value: Array.isArray(value) ? [...value] : value
6777
+ }
6778
+ };
6779
+ return attempt2(() => {
6780
+ const parser = new Parser2(value);
6781
+ let parsed = value;
6782
+ if (["in", "contains", "containedBy", "overlaps", "is"].includes(operator)) {
6783
+ parsed = parser.value(operator);
6784
+ parser.skipSpaces();
6785
+ if (parser.at() !== "") refuse3(`has unexpected text ${parser.where()}`);
6786
+ } else if (operator === "like" || operator === "ilike")
6787
+ parsed = likePattern2(value);
6788
+ else if (operator === "eq" || operator === "neq") parsed = truth2(value);
6789
+ return { operator, column, value: parsed };
6790
+ });
6791
+ }
6792
+ function patternOf(pattern) {
6793
+ return typeof pattern === "string" ? likePattern2(pattern) : pattern;
6794
+ }
6795
+ var MAX_LIMIT = 1e3;
6796
+ var CAP = `one select reads at most ${String(MAX_LIMIT)} rows, by limit() or range()`;
6797
+ function initialState(table2) {
6798
+ return { table: table2, problem: null, filters: [], columns: "*", order: [] };
6799
+ }
6800
+ function copyRow(row) {
6801
+ return { ...row };
6802
+ }
6803
+ var Builder = class {
6804
+ run;
6805
+ state;
6806
+ constructor(run, state) {
6807
+ this.run = run;
6808
+ this.state = state;
6809
+ }
6810
+ /** A new builder of the same kind with `patch` applied. */
6811
+ next(patch) {
6812
+ const Next = this.constructor;
6813
+ return new Next(this.run, { ...this.state, ...patch });
6814
+ }
6815
+ fail(path, reason) {
6816
+ return this.failWith(invalidQuery(path, reason));
6817
+ }
6818
+ /** Keeps the first problem: it is the one the user has to fix first. */
6819
+ failWith(problem) {
6820
+ if (this.state.problem !== null) return this;
6821
+ return this.next({ problem });
6822
+ }
6823
+ /**
6824
+ * Runs the query. Every `await` sends a new request with the same AST.
6825
+ * Never rejects: failures resolve as `{ data: null, error }`.
6826
+ */
6827
+ // oxlint-disable-next-line unicorn/no-thenable -- awaitable builders are the API (spec §15)
6828
+ then(onfulfilled, onrejected) {
6829
+ return this.execute().then(onfulfilled, onrejected);
6830
+ }
6831
+ async execute() {
6832
+ if (this.state.problem !== null)
6833
+ return { data: null, error: this.state.problem };
6834
+ return await this.run(this.build());
6835
+ }
6836
+ /** A mutation's `.select()`: RETURNING these columns. */
6837
+ withReturning(columns) {
6838
+ const parsed = parseColumns(columns);
6839
+ if (!parsed.ok) return this.fail("returning", parsed.reason);
6840
+ return this.next({ returning: parsed.columns });
6841
+ }
6842
+ };
6843
+ function copyList(values) {
6844
+ return Array.isArray(values) ? [...values] : values;
6845
+ }
6846
+ var FilterBuilder = class _FilterBuilder extends Builder {
6847
+ /** One more entry in the top-level list, which is an AND. */
6848
+ add(node) {
6849
+ return this.next({ filters: [...this.state.filters, node] });
6850
+ }
6851
+ condition(operator, column, value) {
6852
+ return this.add({ operator, column, value });
6853
+ }
6854
+ eq(column, value) {
6855
+ return this.condition("eq", column, value);
6856
+ }
6857
+ neq(column, value) {
6858
+ return this.condition("neq", column, value);
6859
+ }
6860
+ gt(column, value) {
6861
+ return this.condition("gt", column, value);
6862
+ }
6863
+ gte(column, value) {
6864
+ return this.condition("gte", column, value);
6865
+ }
6866
+ lt(column, value) {
6867
+ return this.condition("lt", column, value);
6868
+ }
6869
+ lte(column, value) {
6870
+ return this.condition("lte", column, value);
6871
+ }
6872
+ /** An empty list matches nothing. */
6873
+ in(column, values) {
6874
+ return this.condition("in", column, copyList(values));
6875
+ }
6876
+ /** `IS NULL`, `IS TRUE` or `IS FALSE` — any non-zero number is true, NULL neither. */
6877
+ is(column, value) {
6878
+ return this.condition("is", column, value);
6879
+ }
6880
+ /**
6881
+ * Case-sensitive, `%` and `_` as in SQL; `*` is `%` too, as in PostgREST,
6882
+ * so a literal `*` cannot be matched.
6883
+ */
6884
+ like(column, pattern) {
6885
+ return this.condition("like", column, patternOf(pattern));
6886
+ }
6887
+ /** Case-insensitive for ASCII letters; `%`, `_` and `*` as in `like`. */
6888
+ ilike(column, pattern) {
6889
+ return this.condition("ilike", column, patternOf(pattern));
6890
+ }
6891
+ /**
6892
+ * The column — TEXT holding a JSON array — has every one of the values.
6893
+ * A row that is not a JSON array is no match; NULL is neither.
6894
+ */
6895
+ contains(column, values) {
6896
+ return this.condition("contains", column, copyList(values));
6897
+ }
6898
+ /** Every element of the column's JSON array is one of the values (`[]` is). */
6899
+ containedBy(column, values) {
6900
+ return this.condition("containedBy", column, copyList(values));
6901
+ }
6902
+ /** The column's JSON array has at least one of the values. */
6903
+ overlaps(column, values) {
6904
+ return this.condition("overlaps", column, copyList(values));
6905
+ }
6906
+ /** An `eq` for each key, in key order. */
6907
+ match(query) {
6908
+ const conditions = Object.entries(query).map(
6909
+ ([column, value]) => ({
6910
+ operator: "eq",
6911
+ column,
6912
+ value
6913
+ })
6914
+ );
6915
+ return this.next({ filters: [...this.state.filters, ...conditions] });
6916
+ }
6917
+ /**
6918
+ * Any operator by name, Supabase's escape hatch: `filter("id", "in",
6919
+ * "(1,2)")`, `filter("tags", "cs", "{a}")`, `filter("n", "not.gte", 10)`.
6920
+ * A string value is PostgREST syntax where the operator needs one, and text
6921
+ * otherwise.
6922
+ */
6923
+ filter(column, operator, value) {
6924
+ const name = operator;
6925
+ const negated = typeof name === "string" && name.startsWith("not.");
6926
+ const parsed = conditionOf(
6927
+ column,
6928
+ negated ? name.slice(4) : name,
6929
+ value
6930
+ );
6931
+ if (!parsed.ok) return this.fail("filter", parsed.reason);
6932
+ return this.add(negated ? { not: parsed.node } : parsed.node);
6933
+ }
6934
+ not(target, operator, value) {
6935
+ if (typeof target === "function") {
6936
+ const nodes = this.group("not", target);
6937
+ if (!Array.isArray(nodes)) return this.failWith(nodes);
6938
+ const [only] = nodes;
6939
+ return this.add({
6940
+ not: nodes.length === 1 && only ? only : { and: nodes }
6941
+ });
6942
+ }
6943
+ const parsed = conditionOf(target, operator, value);
6944
+ if (!parsed.ok) return this.fail("not", parsed.reason);
6945
+ return this.add({ not: parsed.node });
6946
+ }
6947
+ /**
6948
+ * Rows matching at least one of the conditions: a callback,
6949
+ * `or((q) => q.eq("status", "draft").lt("price", 10))`, or Supabase's
6950
+ * string, `or("status.eq.draft,price.lt.10")`, whose values are text.
6951
+ */
6952
+ or(filters) {
6953
+ return this.logic("or", filters);
6954
+ }
6955
+ /** Rows matching all of the conditions, as a group: for use inside `or`. */
6956
+ and(filters) {
6957
+ return this.logic("and", filters);
6958
+ }
6959
+ logic(kind, filters) {
6960
+ if (typeof filters === "function") {
6961
+ const nodes = this.group(kind, filters);
6962
+ if (!Array.isArray(nodes)) return this.failWith(nodes);
6963
+ return this.add(kind === "or" ? { or: nodes } : { and: nodes });
6964
+ }
6965
+ const parsed = parseLogic2(filters, kind);
6966
+ if (!parsed.ok) return this.fail(kind, parsed.reason);
6967
+ return this.add(parsed.node);
6968
+ }
6969
+ /**
6970
+ * The conditions `build` adds, or why there are none to use. `build` gets
6971
+ * a builder of this kind with no filters; FilterGroup shows only its filter
6972
+ * methods, and only its conditions and first problem are read back.
6973
+ */
6974
+ group(path, build) {
6975
+ const built = build(this.next({ filters: [], problem: null }));
6976
+ if (!(built instanceof _FilterBuilder))
6977
+ return invalidQuery(path, "must return the filter builder it was given");
6978
+ const { filters, problem } = built.state;
6979
+ if (problem !== null) return problem;
6980
+ if (filters.length === 0) return invalidQuery(path, "must not be empty");
6981
+ return [...filters];
6982
+ }
6983
+ filtersPart() {
6984
+ return this.state.filters.length === 0 ? {} : { filters: [...this.state.filters] };
6985
+ }
6986
+ };
6987
+ var SelectBuilder = class extends FilterBuilder {
6988
+ /** Call again to sort by more columns, in call order. */
6989
+ order(column, options = {}) {
6990
+ const term = { column, ascending: options.ascending ?? true };
6991
+ if (options.nullsFirst !== void 0) term.nullsFirst = options.nullsFirst;
6992
+ return this.next({ order: [...this.state.order, term] });
6993
+ }
6994
+ /** At most `count` rows (1–1,000; a select without one reads up to 1,000). */
6995
+ limit(count2) {
6996
+ if (!Number.isSafeInteger(count2) || count2 < 1 || count2 > MAX_LIMIT)
6997
+ return this.fail(
6998
+ "limit",
6999
+ `must be an integer from 1 to ${String(MAX_LIMIT)}; ${CAP}`
7000
+ );
7001
+ return this.next({ limit: count2 });
7002
+ }
7003
+ offset(count2) {
7004
+ return this.next({ offset: count2 });
7005
+ }
7006
+ /**
7007
+ * Rows `from` to `to`, both inclusive and counted from 0 — at most 1,000
7008
+ * of them. Sent as `offset` and `limit`, so it is checked here: past the
7009
+ * cap the worker would name `limit`, which the app never called.
7010
+ */
7011
+ range(from, to) {
7012
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 0 || to < from)
7013
+ return this.fail("range", "must be two integers with 0 <= from <= to");
7014
+ const rows = to - from + 1;
7015
+ if (rows > MAX_LIMIT)
7016
+ return this.fail(
7017
+ "range",
7018
+ `spans ${String(rows)} rows (to - from + 1); ${CAP}`
7019
+ );
7020
+ return this.next({ offset: from, limit: rows });
7021
+ }
7022
+ /** Exactly one row, or NOT_SINGLE_ROW. */
7023
+ single() {
7024
+ return this.next({ single: "one" });
7025
+ }
7026
+ /** One row or null; more than one is NOT_SINGLE_ROW. */
7027
+ maybeSingle() {
7028
+ return this.next({ single: "maybe" });
7029
+ }
7030
+ build() {
7031
+ const { table: table2, columns, order, limit, offset, single } = this.state;
7032
+ return {
7033
+ version: 1,
7034
+ action: "select",
7035
+ table: table2,
7036
+ columns: columns === "*" ? "*" : [...columns],
7037
+ ...this.filtersPart(),
7038
+ ...order.length === 0 ? {} : { order: order.map((term) => ({ ...term })) },
7039
+ ...limit === void 0 ? {} : { limit },
7040
+ ...offset === void 0 ? {} : { offset },
7041
+ ...single === void 0 ? {} : { single }
7042
+ };
7043
+ }
7044
+ };
7045
+ var InsertBuilder = class extends Builder {
7046
+ /** Return the inserted rows (all columns, or the ones named). */
7047
+ select(columns) {
7048
+ return this.withReturning(columns);
7049
+ }
7050
+ build() {
7051
+ const { table: table2, rows = [], returning } = this.state;
7052
+ return {
7053
+ version: 1,
7054
+ action: "insert",
7055
+ table: table2,
7056
+ rows: rows.map(copyRow),
7057
+ ...returning === void 0 ? {} : { returning }
7058
+ };
7059
+ }
7060
+ };
7061
+ var MutationBuilder = class extends FilterBuilder {
7062
+ /** Return the changed rows (all columns, or the ones named). */
7063
+ select(columns) {
7064
+ return this.withReturning(columns);
7065
+ }
7066
+ /** Apply to every row with no filter at all. Service key only. */
7067
+ all() {
7068
+ return this.next({ allowUnfiltered: true });
7069
+ }
7070
+ build() {
7071
+ const {
7072
+ table: table2,
7073
+ action,
7074
+ values = {},
7075
+ returning,
7076
+ allowUnfiltered
7077
+ } = this.state;
7078
+ const tail = {
7079
+ ...this.filtersPart(),
7080
+ ...returning === void 0 ? {} : { returning },
7081
+ ...allowUnfiltered === void 0 ? {} : { allowUnfiltered }
7082
+ };
7083
+ return action === "delete" ? { version: 1, action: "delete", table: table2, ...tail } : {
7084
+ version: 1,
7085
+ action: "update",
7086
+ table: table2,
7087
+ values: copyRow(values),
7088
+ ...tail
7089
+ };
7090
+ }
7091
+ };
7092
+ var TableBuilder = class {
7093
+ #run;
7094
+ #table;
7095
+ constructor(run, table2) {
7096
+ this.#run = run;
7097
+ this.#table = table2;
7098
+ }
7099
+ /** `"*"` (the default) or `"id, title"`. */
7100
+ select(columns) {
7101
+ const state = initialState(this.#table);
7102
+ const parsed = parseColumns(columns);
7103
+ return new SelectBuilder(
7104
+ this.#run,
7105
+ parsed.ok ? { ...state, columns: parsed.columns } : { ...state, problem: invalidQuery("columns", parsed.reason) }
7106
+ );
7107
+ }
7108
+ /** One row or several; one request, one transaction. */
7109
+ insert(rows) {
7110
+ const list = Array.isArray(rows) ? rows : [rows];
7111
+ return new InsertBuilder(this.#run, {
7112
+ ...initialState(this.#table),
7113
+ rows: list.map(copyRow)
7114
+ });
7115
+ }
7116
+ update(values) {
7117
+ return new MutationBuilder(this.#run, {
7118
+ ...initialState(this.#table),
7119
+ action: "update",
7120
+ values: copyRow(values)
7121
+ });
7122
+ }
7123
+ delete() {
7124
+ return new MutationBuilder(this.#run, {
7125
+ ...initialState(this.#table),
7126
+ action: "delete"
7127
+ });
7128
+ }
7129
+ };
7130
+ var MAX_SINGLE_UPLOAD_BYTES2 = 52428800;
7131
+ var STORAGE_MESSAGES = {
7132
+ bucket: "The bucket name is not valid.",
7133
+ tooLarge: "Uploads over 50 MiB are not supported yet.",
7134
+ request: "The storage request is not valid.",
7135
+ body: "upload takes a Blob, File, ArrayBuffer, typed array or string."
7136
+ };
7137
+ var RESERVED_BUCKETS = /* @__PURE__ */ new Set([
7138
+ "authenticated",
7139
+ "copy",
7140
+ "info",
7141
+ "list",
7142
+ "move",
7143
+ "public",
7144
+ "render",
7145
+ "sign",
7146
+ "upload"
7147
+ ]);
7148
+ var DOWNLOAD_NAME_REASON = "must be a file name of up to 255 bytes";
7149
+ var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
7150
+ var OCTET_STREAM = "application/octet-stream";
7151
+ var UTF8_TEXT = "text/plain;charset=utf-8";
7152
+ function invalidPath(reason) {
7153
+ return {
7154
+ code: "INVALID_PATH",
7155
+ message: `The object path is not valid: it ${reason}.`
7156
+ };
7157
+ }
7158
+ function objectTooLarge() {
7159
+ return { code: "OBJECT_TOO_LARGE", message: STORAGE_MESSAGES.tooLarge };
7160
+ }
7161
+ function encodePath(path) {
7162
+ if (typeof path !== "string" || path === "")
7163
+ return { ok: false, error: invalidPath("is empty") };
7164
+ if (LONE_SURROGATE.test(path))
7165
+ return { ok: false, error: invalidPath("is not valid UTF-8") };
7166
+ if (path.startsWith("/") || path.endsWith("/"))
7167
+ return { ok: false, error: invalidPath("starts or ends with /") };
7168
+ if (path.includes("//"))
7169
+ return { ok: false, error: invalidPath("has an empty segment (//)") };
7170
+ const segments2 = path.split("/");
7171
+ if (segments2.some((segment) => segment === "." || segment === ".."))
7172
+ return { ok: false, error: invalidPath("has a . or .. segment") };
7173
+ return { ok: true, encoded: segments2.map(encodeURIComponent).join("/") };
7174
+ }
7175
+ function downloadQuery(download2) {
7176
+ if (download2 === true) return { ok: true, encoded: "?download" };
7177
+ if (typeof download2 !== "string") return { ok: true, encoded: "" };
7178
+ if (LONE_SURROGATE.test(download2))
7179
+ return {
7180
+ ok: false,
7181
+ error: {
7182
+ code: "INVALID_REQUEST",
7183
+ message: STORAGE_MESSAGES.request,
7184
+ details: { path: "download", reason: DOWNLOAD_NAME_REASON }
7185
+ }
7186
+ };
7187
+ return { ok: true, encoded: `?download=${encodeURIComponent(download2)}` };
7188
+ }
7189
+ function encodeBucket(bucket) {
7190
+ if (typeof bucket !== "string" || bucket === "" || bucket === "." || bucket === ".." || LONE_SURROGATE.test(bucket) || RESERVED_BUCKETS.has(bucket))
7191
+ return {
7192
+ ok: false,
7193
+ error: { code: "INVALID_PATH", message: STORAGE_MESSAGES.bucket }
7194
+ };
7195
+ return { ok: true, encoded: encodeURIComponent(bucket) };
7196
+ }
7197
+ var isArrayBuffer = (value) => Object.prototype.toString.call(value) === "[object ArrayBuffer]";
7198
+ function isDetached(buffer) {
7199
+ if (!isArrayBuffer(buffer)) return false;
7200
+ const flag = buffer.detached;
7201
+ if (typeof flag === "boolean") return flag;
7202
+ if (buffer.byteLength > 0) return false;
7203
+ try {
7204
+ buffer.slice(0);
7205
+ return false;
7206
+ } catch {
7207
+ return true;
7208
+ }
7209
+ }
7210
+ var CACHE_CONTROL_REASON = "must be max-age=<seconds> from 0 to 31536000";
7211
+ var MAX_TYPE_IN_MESSAGE = 256;
7212
+ function unsendable(value) {
7213
+ return /[\0\r\n]|[^\0-\xff]/.test(
7214
+ value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "")
7215
+ );
7216
+ }
7217
+ function headerProblem(contentType, cacheControl) {
7218
+ if (typeof contentType === "string" && unsendable(contentType)) {
7219
+ let shown = contentType;
7220
+ if (shown.length > MAX_TYPE_IN_MESSAGE) {
7221
+ shown = shown.slice(0, MAX_TYPE_IN_MESSAGE);
7222
+ if (/[\ud800-\udbff]$/.test(shown)) shown = shown.slice(0, -1);
7223
+ }
7224
+ return {
7225
+ code: "INVALID_CONTENT_TYPE",
7226
+ message: `Content-Type ${shown} is not a media type like image/png.`
7227
+ };
7228
+ }
7229
+ if (unsendable(cacheControl))
7230
+ return {
7231
+ code: "INVALID_REQUEST",
7232
+ message: STORAGE_MESSAGES.request,
7233
+ details: { path: "cache-control", reason: CACHE_CONTROL_REASON }
7234
+ };
7235
+ return null;
7236
+ }
7237
+ function payloadOf(body, contentType) {
7238
+ if (typeof body === "string") {
7239
+ const bytes = new TextEncoder().encode(body);
7240
+ return {
7241
+ ok: true,
7242
+ body: bytes,
7243
+ size: bytes.byteLength,
7244
+ type: contentType ?? UTF8_TEXT
7245
+ };
7246
+ }
7247
+ if (typeof Blob === "function" && body instanceof Blob)
7248
+ return {
7249
+ ok: true,
7250
+ body,
7251
+ size: body.size,
7252
+ type: contentType ?? (body.type === "" ? OCTET_STREAM : body.type)
7253
+ };
7254
+ if (isArrayBuffer(body) && !isDetached(body))
7255
+ return {
7256
+ ok: true,
7257
+ body,
7258
+ size: body.byteLength,
7259
+ type: contentType ?? OCTET_STREAM
7260
+ };
7261
+ if (ArrayBuffer.isView(body) && !isDetached(body.buffer)) {
7262
+ const { buffer, byteOffset, byteLength } = body;
7263
+ const bytes = isArrayBuffer(buffer) ? new Uint8Array(buffer, byteOffset, byteLength) : Uint8Array.from(new Uint8Array(buffer, byteOffset, byteLength));
7264
+ return {
7265
+ ok: true,
7266
+ body: bytes,
7267
+ size: byteLength,
7268
+ type: contentType ?? OCTET_STREAM
7269
+ };
7270
+ }
7271
+ return {
7272
+ ok: false,
7273
+ error: { code: "INVALID_BODY", message: STORAGE_MESSAGES.body }
7274
+ };
7275
+ }
7276
+ var SIGNED_PREFIX = "/_naro/v1/storage/object/sign/";
7277
+ function fail(error) {
7278
+ return { data: null, error };
7279
+ }
7280
+ var isUploadResult = (value) => isRecord(value) && typeof value.id === "string" && typeof value.path === "string" && typeof value.fullPath === "string";
7281
+ var isFileObject = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && isRecord(value.metadata);
7282
+ var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (value.id === null || isFileObject(value));
7283
+ var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
7284
+ function listOf(guard) {
7285
+ return (value) => Array.isArray(value) && value.every(guard);
7286
+ }
7287
+ function dataOf2(status, body, guard) {
7288
+ if (isRecord(body) && Object.hasOwn(body, "data") && guard(body.data))
7289
+ return { data: body.data, error: null };
7290
+ return fail(invalidResponse(status));
7291
+ }
7292
+ async function readJson(response, guard) {
7293
+ let text;
7294
+ try {
7295
+ text = await response.text();
7296
+ } catch (error) {
7297
+ return fail(networkError(error));
7298
+ }
7299
+ let body;
7300
+ try {
7301
+ body = JSON.parse(text);
7302
+ } catch {
7303
+ return fail(invalidResponse(response.status));
7304
+ }
7305
+ return dataOf2(response.status, body, guard);
7306
+ }
7307
+ var StorageFileApi = class {
7308
+ #http;
7309
+ #auth;
7310
+ #bucket;
7311
+ constructor(http, auth, bucket) {
7312
+ this.#http = http;
7313
+ this.#auth = auth;
7314
+ this.#bucket = bucket;
7315
+ }
7316
+ /** `/storage/object/<route><bucket>/<path>`, or the refusal no request needs. */
7317
+ #objectPath(route2, path) {
7318
+ const bucket = encodeBucket(this.#bucket);
7319
+ if (!bucket.ok) return bucket;
7320
+ const name = encodePath(path);
7321
+ if (!name.ok) return name;
7322
+ return {
7323
+ ok: true,
7324
+ path: `/storage/object/${route2}${bucket.encoded}/${name.encoded}`
7325
+ };
7326
+ }
7327
+ /** `/storage/object/<route><bucket>`, for the routes that take names in the body. */
7328
+ #bucketPath(route2) {
7329
+ const bucket = encodeBucket(this.#bucket);
7330
+ if (!bucket.ok) return bucket;
7331
+ return { ok: true, path: `/storage/object/${route2}${bucket.encoded}` };
7332
+ }
7333
+ async #json(request, guard) {
7334
+ await this.#auth.ready();
7335
+ const result = await call(this.#http, { ...request, withToken: true });
7336
+ if (result.error !== null) return fail(result.error);
7337
+ return dataOf2(result.answer.status, result.answer.body, guard);
7338
+ }
7339
+ async #write(method, path, body, options, upsert) {
7340
+ const target = this.#objectPath("", path);
7341
+ if (!target.ok) return fail(target.error);
7342
+ const payload = payloadOf(body, options.contentType);
7343
+ if (!payload.ok) return fail(payload.error);
7344
+ const cache = `max-age=${options.cacheControl ?? "3600"}`;
7345
+ const unreadable = headerProblem(options.contentType, cache);
7346
+ if (unreadable !== null) return fail(unreadable);
7347
+ if (payload.size > MAX_SINGLE_UPLOAD_BYTES2) return fail(objectTooLarge());
7348
+ await this.#auth.ready();
7349
+ const headers = {
7350
+ "content-type": payload.type,
7351
+ "cache-control": cache
7352
+ };
7353
+ if (upsert) headers["x-upsert"] = "true";
7354
+ const sent = await send(this.#http, {
7355
+ method,
7356
+ path: target.path,
7357
+ headers,
7358
+ body: payload.body,
7359
+ withToken: true
7360
+ });
7361
+ if (sent.error !== null) return fail(sent.error);
7362
+ return readJson(sent.response, isUploadResult);
7363
+ }
7364
+ /**
7365
+ * A new object; with `upsert: true`, one of the same name is replaced.
7366
+ * `null` options, as a JS caller may pass, are no options.
7367
+ */
7368
+ upload(path, body, options = {}) {
7369
+ const given = options ?? {};
7370
+ return this.#write("POST", path, body, given, given.upsert === true);
7371
+ }
7372
+ /** Replaces an object that is there (`OBJECT_NOT_FOUND` otherwise). */
7373
+ update(path, body, options = {}) {
7374
+ return this.#write("PUT", path, body, options ?? {}, false);
7375
+ }
7376
+ /**
7377
+ * The object's bytes, as a Blob of the type the worker serves it under:
7378
+ * the stored type for the images, audio, video, PDF, plain text, CSV,
7379
+ * JSON and SVG it serves as themselves, `application/octet-stream` for
7380
+ * anything else (HTML, scripts, archives, unknown types — `info` and
7381
+ * `list` hold the stored type).
7382
+ */
7383
+ async download(path) {
7384
+ const target = this.#objectPath("", path);
7385
+ if (!target.ok) return fail(target.error);
7386
+ await this.#auth.ready();
7387
+ const sent = await send(this.#http, {
7388
+ method: "GET",
7389
+ path: target.path,
7390
+ withToken: true
7391
+ });
7392
+ if (sent.error !== null) return fail(sent.error);
7393
+ try {
7394
+ return { data: await sent.response.blob(), error: null };
7395
+ } catch (error) {
7396
+ return fail(networkError(error));
7397
+ }
7398
+ }
7399
+ async info(path) {
7400
+ const target = this.#objectPath("info/", path);
7401
+ if (!target.ok) return fail(target.error);
7402
+ return this.#json({ method: "GET", path: target.path }, isFileObject);
7403
+ }
7404
+ /**
7405
+ * Whether the caller can see the object: `OBJECT_NOT_FOUND` is false,
7406
+ * every other error comes back as it is (plan C1 — asked with `info`,
7407
+ * not HEAD, whose answer has no body to tell a missing bucket from a
7408
+ * missing object).
7409
+ */
7410
+ async exists(path) {
7411
+ const found = await this.info(path);
7412
+ if (found.error === null) return { data: true, error: null };
7413
+ if (found.error.code === "OBJECT_NOT_FOUND")
7414
+ return { data: false, error: null };
7415
+ return fail(found.error);
7416
+ }
7417
+ /**
7418
+ * What is directly below `prefix` (the whole bucket by default): folders
7419
+ * first, then files. The options go to the worker as they are given.
7420
+ */
7421
+ async list(prefix, options = {}) {
7422
+ const target = this.#bucketPath("list/");
7423
+ if (!target.ok) return fail(target.error);
7424
+ return this.#json(
7425
+ {
7426
+ method: "POST",
7427
+ path: target.path,
7428
+ body: { ...prefix === void 0 ? {} : { prefix }, ...options }
7429
+ },
7430
+ listOf(isEntry2)
7431
+ );
7432
+ }
7433
+ /** Removes the objects named — 1 to 1,000 — and answers the ones it removed. */
7434
+ async remove(paths) {
7435
+ const target = this.#bucketPath("");
7436
+ if (!target.ok) return fail(target.error);
7437
+ return this.#json(
7438
+ { method: "DELETE", path: target.path, body: { prefixes: paths } },
7439
+ listOf(isFileObject)
7440
+ );
7441
+ }
7442
+ /**
7443
+ * A link to the object for `expiresIn` seconds (1–604,800). It stops
7444
+ * working once the object is replaced or removed.
7445
+ */
7446
+ async createSignedUrl(path, expiresIn, options = {}) {
7447
+ const target = this.#objectPath("sign/", path);
7448
+ if (!target.ok) return fail(target.error);
7449
+ const { download: download2 } = options ?? {};
7450
+ const signed = await this.#json(
7451
+ {
7452
+ method: "POST",
7453
+ path: target.path,
7454
+ body: {
7455
+ expiresIn,
7456
+ ...download2 === void 0 || download2 === false ? {} : { download: download2 }
7457
+ }
7458
+ },
7459
+ isSigned
7460
+ );
7461
+ if (signed.error !== null) return signed;
7462
+ return {
7463
+ data: { signedUrl: `${this.#http.baseUrl}${signed.data.signedUrl}` },
7464
+ error: null
7465
+ };
7466
+ }
7467
+ /**
7468
+ * A public bucket's URL for the object. No request is made, so whether
7469
+ * the bucket is public is not checked (as in supabase-js); a path no URL
7470
+ * can carry is INVALID_PATH (plan C3), and a download name none can carry
7471
+ * is the worker's INVALID_REQUEST for it.
7472
+ */
7473
+ getPublicUrl(path, options = {}) {
7474
+ const target = this.#objectPath("public/", path);
7475
+ if (!target.ok) return fail(target.error);
7476
+ const { download: download2 } = options ?? {};
7477
+ const query = downloadQuery(download2);
7478
+ if (!query.ok) return fail(query.error);
7479
+ return {
7480
+ data: {
7481
+ publicUrl: `${this.#http.baseUrl}/_naro/v1${target.path}${query.encoded}`
7482
+ },
7483
+ error: null
7484
+ };
7485
+ }
7486
+ };
7487
+ var StorageClient = class {
7488
+ #http;
7489
+ #auth;
7490
+ constructor(http, auth) {
7491
+ this.#http = http;
7492
+ this.#auth = auth;
7493
+ }
7494
+ /** The objects of one bucket. */
7495
+ from(bucket) {
7496
+ return new StorageFileApi(this.#http, this.#auth, bucket);
7497
+ }
7498
+ };
7499
+ var CONFIG_MESSAGES = {
7500
+ url: "createClient needs the project url, like https://my-app.naro.sh.",
7501
+ urlNotOrigin: "createClient needs the project url alone, like https://my-app.naro.sh: no path, no ? or #, no user:password@. Naro serves its API at /_naro/v1 on that origin.",
7502
+ key: "createClient needs an anonKey (or, on a server, a serviceKey).",
7503
+ bothKeys: "Pass createClient an anonKey or a serviceKey, not both.",
7504
+ serviceKeyOffServer: "A Naro service key (naro_sk_\u2026) skips every policy, so createClient accepts it only on a server (Node, Deno, Bun, Cloudflare Workers, an edge runtime), never in a browser, a web worker or a React Native app. Use the anon key (naro_pk_\u2026) here and keep the service key on your server.",
7505
+ fetch: "This runtime has no fetch. Pass one to createClient({ fetch })."
7506
+ };
7507
+ var SERVICE_KEY_PREFIX = "naro_sk_";
7508
+ function baseUrlOf(url) {
7509
+ if (typeof url !== "string") throw new Error(CONFIG_MESSAGES.url);
7510
+ const trimmed = url.trim();
7511
+ let parsed;
7512
+ try {
7513
+ parsed = new URL(trimmed.replace(/\/+$/, ""));
7514
+ } catch {
7515
+ throw new Error(CONFIG_MESSAGES.url);
7516
+ }
7517
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
7518
+ throw new Error(CONFIG_MESSAGES.url);
7519
+ if (trimmed.includes("?") || trimmed.includes("#") || parsed.pathname !== "/" || parsed.username !== "" || parsed.password !== "")
7520
+ throw new Error(CONFIG_MESSAGES.urlNotOrigin);
7521
+ return parsed.origin;
7522
+ }
7523
+ function apiKeyOf(options) {
7524
+ const { anonKey, serviceKey } = options;
7525
+ const hasAnon = typeof anonKey === "string" && anonKey !== "";
7526
+ const hasService = typeof serviceKey === "string" && serviceKey !== "";
7527
+ if (hasAnon && hasService) throw new Error(CONFIG_MESSAGES.bothKeys);
7528
+ const key = hasService ? serviceKey : hasAnon ? anonKey : void 0;
7529
+ if (key === void 0) throw new Error(CONFIG_MESSAGES.key);
7530
+ const service = hasService || key.startsWith(SERVICE_KEY_PREFIX);
7531
+ if (service && !isKnownServer())
7532
+ throw new Error(CONFIG_MESSAGES.serviceKeyOffServer);
7533
+ return { key, service };
7534
+ }
7535
+ function queryRunner(http, auth) {
7536
+ return async (query) => {
7537
+ await auth.ready();
7538
+ const result = await call(http, {
7539
+ method: "POST",
7540
+ path: "/data/query",
7541
+ body: query,
7542
+ withToken: true
7543
+ });
7544
+ if (result.error !== null) return { data: null, error: result.error };
7545
+ const { body, status } = result.answer;
7546
+ if (!isRecord(body) || !Object.hasOwn(body, "data"))
7547
+ return { data: null, error: invalidResponse(status) };
7548
+ return { data: body.data, error: null };
7549
+ };
7550
+ }
7551
+ var NaroClient2 = class {
7552
+ /**
7553
+ * Sign-up, sign-in, sign-out and the current user (spec §13); with the
7554
+ * service key, `auth.admin` manages users.
7555
+ */
7556
+ auth;
7557
+ /**
7558
+ * Files in the project's Naro Storage (first-release design 2026-10-06
7559
+ * §8.1): `storage.from(bucket)` uploads, downloads, lists and removes
7560
+ * them, as supabase-js does.
7561
+ */
7562
+ storage;
7563
+ #run;
7564
+ constructor(options) {
7565
+ const baseUrl = baseUrlOf(options.url);
7566
+ const { key: apiKey, service } = apiKeyOf(options);
7567
+ if (options.fetch === void 0 && typeof globalThis.fetch !== "function")
7568
+ throw new Error(CONFIG_MESSAGES.fetch);
7569
+ const storage = options.auth?.storage ?? defaultStorage();
7570
+ const http = {
7571
+ baseUrl,
7572
+ apiKey,
7573
+ fetch: options.fetch,
7574
+ tokens: new TokenStore(storage, storageKey(baseUrl))
7575
+ };
7576
+ this.auth = new AuthClient(http, {
7577
+ verifiers: new FlowVerifiers(
7578
+ new TokenStore(storage, verifierKey(baseUrl))
7579
+ ),
7580
+ recovery: new RecoveryToken(
7581
+ new TokenStore(storage, recoveryKey(baseUrl))
7582
+ ),
7583
+ detectSessionInUrl: options.auth?.detectSessionInUrl ?? true,
7584
+ shared: { storage, key: storageKey(baseUrl) },
7585
+ serviceKey: service
7586
+ });
7587
+ this.#run = queryRunner(http, this.auth);
7588
+ this.storage = new StorageClient(http, this.auth);
7589
+ }
7590
+ /** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
7591
+ from(table2) {
7592
+ return new TableBuilder(this.#run, table2);
7593
+ }
7594
+ };
7595
+ function createClient(options) {
7596
+ return new NaroClient2(options);
7597
+ }
7598
+
7599
+ // src/commands/storage-objects.ts
7600
+ var NARO_SCHEME = "naro://";
7601
+ var CP_USAGE = "Usage: naro storage cp <from> <to> (one side naro://<bucket>/<path>, the other a local file)";
7602
+ var RM_USAGE = "Usage: naro storage rm naro://<bucket>/<path>\u2026 [--recursive] [--yes]";
7603
+ var LS_USAGE = "Usage: naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]";
7604
+ var BACKEND_OFF_MESSAGE = "The backend is turned off for this project. Turn it on with `naro backend enable` first.";
7605
+ var PAGE = 1e3;
7606
+ var MAX_OFFSET = 1e5;
7607
+ var TRUNCATED_WARNING = `\u26A0 A folder's listing stopped at ${(MAX_OFFSET + PAGE).toLocaleString("en-US")} entries, as far as the API's offset reaches; there may be more.`;
7608
+ var REMOVE_BATCH = 1e3;
7609
+ var CACHE_CONTROL_RULE = "--cache-control takes whole seconds from 0 to 31536000 (a year), like 3600.";
7610
+ function isMaxAge(text) {
7611
+ return parseCacheControl(`max-age=${text}`).ok;
7612
+ }
7613
+ var TYPES = {
7614
+ ".png": "image/png",
7615
+ ".jpg": "image/jpeg",
7616
+ ".jpeg": "image/jpeg",
7617
+ ".gif": "image/gif",
7618
+ ".webp": "image/webp",
7619
+ ".avif": "image/avif",
7620
+ ".svg": "image/svg+xml",
7621
+ ".ico": "image/x-icon",
7622
+ ".bmp": "image/bmp",
7623
+ ".pdf": "application/pdf",
7624
+ ".json": "application/json",
7625
+ ".txt": "text/plain",
7626
+ ".csv": "text/csv",
7627
+ ".md": "text/markdown",
7628
+ ".html": "text/html",
7629
+ ".htm": "text/html",
7630
+ ".css": "text/css",
7631
+ ".js": "text/javascript",
7632
+ ".mjs": "text/javascript",
7633
+ ".xml": "application/xml",
7634
+ ".zip": "application/zip",
7635
+ ".gz": "application/gzip",
7636
+ ".mp3": "audio/mpeg",
7637
+ ".wav": "audio/wav",
7638
+ ".ogg": "audio/ogg",
7639
+ ".m4a": "audio/mp4",
7640
+ ".mp4": "video/mp4",
7641
+ ".webm": "video/webm",
7642
+ ".mov": "video/quicktime",
7643
+ ".woff": "font/woff",
7644
+ ".woff2": "font/woff2",
7645
+ ".ttf": "font/ttf"
7646
+ };
7647
+ function parseStorageUrl(text) {
7648
+ if (!text.startsWith(NARO_SCHEME)) return null;
7649
+ const rest = text.slice(NARO_SCHEME.length);
7650
+ const slash = rest.indexOf("/");
7651
+ const bucket = slash < 0 ? rest : rest.slice(0, slash);
7652
+ if (bucket === "")
7653
+ throw new NaroError(
7654
+ "VALIDATION_FAILED",
7655
+ "A naro:// path needs a bucket: naro://<bucket>/<path>."
7656
+ );
7657
+ return { bucket, path: slash < 0 ? "" : rest.slice(slash + 1) };
7658
+ }
7659
+ function typeFor(file) {
7660
+ return TYPES[extname(file).toLowerCase()];
7661
+ }
7662
+ function unwrap(result) {
7663
+ if (result.error === null) return result.data;
7664
+ const { code, message, details } = result.error;
7665
+ let shown = typeof details === "object" && details !== null && !Array.isArray(details) ? details : void 0;
7666
+ if (shown && code === "NETWORK_ERROR") {
7667
+ const { cause: _cause, ...rest } = shown;
7668
+ shown = Object.keys(rest).length > 0 ? rest : void 0;
7669
+ }
7670
+ throw new NaroError(code, message, shown ? { details: shown } : {});
7671
+ }
7672
+ var ONE_KEY = /^[\x21-\x7e]+$/;
7673
+ async function objects(context) {
7674
+ const client = await context.client();
7675
+ const projectId = await context.resolveProjectId();
7676
+ const backend = await client.backend.info(projectId);
7677
+ if (backend.status === "none")
7678
+ throw new NaroError(
7679
+ "BACKEND_NOT_READY",
7680
+ "This project has no backend yet. Run `naro backend enable`, then `naro storage enable`."
7681
+ );
7682
+ if (!backend.enabled) throw new NaroError("BACKEND_OFF", BACKEND_OFF_MESSAGE);
7683
+ let serviceKey = process.env.NARO_SERVICE_KEY?.trim() ?? "";
7684
+ if (serviceKey !== "" && !ONE_KEY.test(serviceKey))
7685
+ throw new NaroError(
7686
+ "VALIDATION_FAILED",
7687
+ "NARO_SERVICE_KEY is not a single key: set it to the service key alone."
7688
+ );
7689
+ if (serviceKey === "") {
7690
+ serviceKey = await client.backend.revealServiceKey(projectId);
7691
+ context.output.warn(SERVICE_KEY_WARNING);
7692
+ }
7693
+ let naro;
7694
+ try {
7695
+ naro = createClient({
7696
+ url: backend.url,
7697
+ serviceKey,
7698
+ auth: { detectSessionInUrl: false }
7699
+ });
7700
+ } catch (error) {
7701
+ throw new NaroError(
7702
+ "BACKEND_NOT_READY",
7703
+ `The project's backend url cannot be used (${printable(backend.url)}). Run \`naro backend\` to see where the backend got to.`,
7704
+ { cause: error }
7705
+ );
7706
+ }
7707
+ return { projectId, storage: naro.storage };
7708
+ }
7709
+ function isFolder(entry) {
7710
+ return entry.id === null;
7711
+ }
7712
+ function fileEntry(path, object) {
7713
+ const { size, mimetype } = object.metadata;
7714
+ return {
7715
+ path,
7716
+ kind: "file",
7717
+ size: typeof size === "number" ? size : null,
7718
+ contentType: typeof mimetype === "string" ? mimetype : null,
7719
+ updatedAt: object.updated_at
7720
+ };
7721
+ }
7722
+ async function listAll(storage, bucket, prefix, recursive) {
7723
+ const entries = [];
7724
+ let truncated = false;
7725
+ const folders = [prefix];
7726
+ for (let folder = folders.shift(); folder !== void 0; folder = folders.shift()) {
7727
+ for (let offset = 0; ; offset += PAGE) {
7728
+ if (offset > MAX_OFFSET) {
7729
+ truncated = true;
7730
+ break;
7731
+ }
7732
+ const page = unwrap(
7733
+ await storage.from(bucket).list(folder, { limit: PAGE, offset })
7734
+ );
7735
+ for (const entry of page) {
7736
+ const path = folder === "" ? entry.name : `${folder}/${entry.name}`;
7737
+ if (isFolder(entry)) {
7738
+ if (recursive) folders.push(path);
7739
+ else
7740
+ entries.push({
7741
+ path: `${path}/`,
7742
+ kind: "folder",
7743
+ size: null,
7744
+ contentType: null,
7745
+ updatedAt: null
7746
+ });
7747
+ continue;
7748
+ }
7749
+ entries.push(fileEntry(path, entry));
7750
+ }
7751
+ if (page.length < PAGE) break;
7752
+ }
7753
+ }
7754
+ return { entries, truncated };
7755
+ }
7756
+ function entryLine(bucket, entry) {
7757
+ const where = `${NARO_SCHEME}${bucket}/${printable(entry.path)}`;
7758
+ if (entry.kind === "folder") return `${" ".repeat(38)}${where}`;
7759
+ return `${(entry.size === null ? "?" : binarySize(entry.size)).padStart(10)} ${printable(entry.updatedAt ?? "")} ${where}`;
7760
+ }
7761
+ async function storageLs(context) {
7762
+ const [target, ...extra] = context.positionals;
7763
+ if (extra.length > 0) throw new NaroError("VALIDATION_FAILED", LS_USAGE);
7764
+ if (target === void 0) {
7765
+ const client = await context.client();
7766
+ const projectId2 = await context.resolveProjectId();
7767
+ const buckets = await client.storage.listBuckets(projectId2);
7768
+ context.output.result(
7769
+ { projectId: projectId2, buckets },
7770
+ buckets.length === 0 ? [NO_BUCKETS] : buckets.map((bucket) => `${NARO_SCHEME}${printable(bucket.name)}/`)
7771
+ );
7772
+ return;
7773
+ }
7774
+ const url = parseStorageUrl(target);
7775
+ if (url === null)
7776
+ throw new NaroError(
7777
+ "VALIDATION_FAILED",
7778
+ `${printable(target)} is not a naro:// path. ${LS_USAGE}`
7779
+ );
7780
+ const prefix = url.path.replace(/\/+$/, "");
7781
+ const { projectId, storage } = await objects(context);
7782
+ const { entries, truncated } = await listAll(
7783
+ storage,
7784
+ url.bucket,
7785
+ prefix,
7786
+ context.flags.recursive === true
7787
+ );
7788
+ if (truncated) context.output.warn(TRUNCATED_WARNING);
7789
+ if (entries.length === 0 && prefix !== "" && !url.path.endsWith("/")) {
7790
+ const found = await storage.from(url.bucket).info(prefix);
7791
+ if (found.error === null) entries.push(fileEntry(prefix, found.data));
7792
+ else if (found.error.code !== "OBJECT_NOT_FOUND") unwrap(found);
7793
+ }
7794
+ context.output.result(
7795
+ { projectId, bucket: url.bucket, prefix, entries, truncated },
7796
+ entries.length === 0 ? [`Nothing under ${NARO_SCHEME}${url.bucket}/${printable(prefix)}.`] : entries.map((entry) => entryLine(url.bucket, entry))
7797
+ );
7798
+ }
7799
+ var INTERRUPTS = ["SIGINT", "SIGTERM", "SIGHUP"];
7800
+ function removeOnInterrupt(partial) {
7801
+ const stop = () => {
7802
+ for (const signal of INTERRUPTS) process.off(signal, onSignal);
7803
+ };
7804
+ function onSignal(signal) {
7805
+ stop();
7806
+ try {
7807
+ rmSync2(partial, { force: true });
7808
+ } catch {
7809
+ }
7810
+ try {
7811
+ process.kill(process.pid, signal);
7812
+ } catch {
7813
+ process.exit(1);
7814
+ }
7815
+ }
7816
+ for (const signal of INTERRUPTS) process.on(signal, onSignal);
7817
+ return stop;
7818
+ }
7819
+ async function writeAtomically(blob, destination) {
7820
+ const partial = join6(dirname3(destination), `.${randomUUID()}.naro-partial`);
7821
+ const stopListening = removeOnInterrupt(partial);
7822
+ try {
7823
+ await pipeline2(
7824
+ Readable.fromWeb(blob.stream()),
7825
+ createWriteStream2(partial, { flags: "wx" })
7826
+ );
7827
+ await rename(partial, destination);
7828
+ } catch (error) {
7829
+ await rm(partial, { force: true }).catch(() => void 0);
7830
+ throw error;
7831
+ } finally {
7832
+ stopListening();
7833
+ }
7834
+ }
7835
+ async function upload(context, local, target) {
7836
+ const file = resolve5(context.cwd, local);
7837
+ const info = await stat(file).catch(() => null);
7838
+ if (info === null)
7839
+ throw new NaroError(
7840
+ "VALIDATION_FAILED",
7841
+ `No such file: ${printable(local)}.`
7842
+ );
7843
+ if (info.isDirectory())
7844
+ throw new NaroError(
7845
+ "VALIDATION_FAILED",
7846
+ `${printable(local)} is a directory. naro storage cp copies one file at a time.`
7847
+ );
7848
+ if (!info.isFile())
7849
+ throw new NaroError(
7850
+ "VALIDATION_FAILED",
7851
+ `${printable(local)} is not a regular file.`
7852
+ );
7853
+ if (info.size > MAX_SINGLE_UPLOAD_BYTES)
7854
+ throw new NaroError(
7855
+ "OBJECT_TOO_LARGE",
7856
+ dataError("OBJECT_TOO_LARGE", { single: true }).message
7857
+ );
7858
+ const { cacheControl } = context.flags;
7859
+ if (cacheControl !== void 0 && !isMaxAge(cacheControl))
7860
+ throw new NaroError("VALIDATION_FAILED", CACHE_CONTROL_RULE);
7861
+ const path = target.path === "" || target.path.endsWith("/") ? `${target.path}${basename2(file)}` : target.path;
7862
+ const { projectId, storage } = await objects(context);
7863
+ const body = await openAsBlob(file);
7864
+ const contentType = context.flags.contentType ?? typeFor(file);
7865
+ const result = unwrap(
7866
+ await storage.from(target.bucket).upload(path, body, {
7867
+ ...contentType === void 0 ? {} : { contentType },
7868
+ ...cacheControl === void 0 ? {} : { cacheControl },
7869
+ upsert: context.flags.upsert === true
7870
+ })
7871
+ );
7872
+ const to = `${NARO_SCHEME}${target.bucket}/${path}`;
7873
+ context.output.result(
7874
+ { projectId, from: local, to, bytes: body.size, id: result.id },
7875
+ [
7876
+ `\u2705 Uploaded ${printable(local)} \u2192 ${printable(to)} (${binarySize(body.size)})`
7877
+ ]
7878
+ );
7879
+ }
7880
+ var WINDOWS_DEVICE = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
7881
+ var MAX_FILE_NAME_BYTES = 255;
7882
+ function savedAs(path) {
7883
+ const name = path.slice(path.lastIndexOf("/") + 1);
7884
+ if (name === "." || name === ".." || name.includes("\\") || name.includes(":") || name.includes("\0") || WINDOWS_DEVICE.test(name) || Buffer.byteLength(name, "utf8") > MAX_FILE_NAME_BYTES)
7885
+ throw new NaroError(
7886
+ "VALIDATION_FAILED",
7887
+ `The object's name (${printable(name)}) cannot be used as a file name. Name the file: naro storage cp naro://<bucket>/<path> <file>.`
7888
+ );
7889
+ return name;
7890
+ }
7891
+ async function download(context, source, local) {
7892
+ if (source.path === "" || source.path.endsWith("/"))
7893
+ throw new NaroError(
7894
+ "VALIDATION_FAILED",
7895
+ "naro storage cp downloads one object: name it, naro://<bucket>/<path>."
7896
+ );
7897
+ const resolved = resolve5(context.cwd, local);
7898
+ const isDirectory = local.endsWith("/") || (await stat(resolved).catch(() => null))?.isDirectory() === true;
7899
+ const destination = isDirectory ? join6(resolved, savedAs(source.path)) : resolved;
7900
+ const folder = await stat(dirname3(destination)).catch(() => null);
7901
+ if (folder === null || !folder.isDirectory())
7902
+ throw new NaroError(
7903
+ "VALIDATION_FAILED",
7904
+ `Cannot save to ${printable(local)}: its folder does not exist.`
7905
+ );
7906
+ const { projectId, storage } = await objects(context);
7907
+ const blob = unwrap(await storage.from(source.bucket).download(source.path));
7908
+ await writeAtomically(blob, destination);
7909
+ const from = `${NARO_SCHEME}${source.bucket}/${source.path}`;
7910
+ context.output.result(
7911
+ { projectId, from, to: destination, bytes: blob.size },
7912
+ [
7913
+ `\u2705 Downloaded ${printable(from)} \u2192 ${printable(destination)} (${binarySize(blob.size)})`
7914
+ ]
7915
+ );
7916
+ }
7917
+ async function storageCp(context) {
7918
+ const [from, to, ...extra] = context.positionals;
7919
+ if (from === void 0 || to === void 0 || extra.length > 0)
7920
+ throw new NaroError("VALIDATION_FAILED", CP_USAGE);
7921
+ const source = parseStorageUrl(from);
7922
+ const target = parseStorageUrl(to);
7923
+ if (source !== null && target !== null)
7924
+ throw new NaroError(
7925
+ "VALIDATION_FAILED",
7926
+ "naro storage cp copies between this machine and Storage: one side is naro://<bucket>/<path>, the other a local file. Copying inside Storage is not supported yet."
7927
+ );
7928
+ if (target !== null) return upload(context, from, target);
7929
+ if (source !== null) return download(context, source, to);
7930
+ throw new NaroError("VALIDATION_FAILED", CP_USAGE);
7931
+ }
7932
+ async function namesUnder(storage, bucket, prefixes) {
7933
+ const names = /* @__PURE__ */ new Set();
7934
+ let truncated = false;
7935
+ for (const written of prefixes) {
7936
+ const prefix = written.replace(/\/+$/, "");
7937
+ if (prefix !== "" && !written.endsWith("/") && unwrap(await storage.from(bucket).exists(prefix)))
7938
+ names.add(prefix);
7939
+ const found = await listAll(storage, bucket, prefix, true);
7940
+ for (const entry of found.entries) names.add(entry.path);
7941
+ truncated ||= found.truncated;
7942
+ }
7943
+ return { names: [...names], truncated };
7944
+ }
7945
+ async function storageRm(context) {
7946
+ if (context.positionals.length === 0)
7947
+ throw new NaroError("VALIDATION_FAILED", RM_USAGE);
7948
+ const recursive = context.flags.recursive === true;
7949
+ const byBucket = /* @__PURE__ */ new Map();
7950
+ for (const arg of context.positionals) {
7951
+ const url = parseStorageUrl(arg);
7952
+ if (url === null)
7953
+ throw new NaroError(
7954
+ "VALIDATION_FAILED",
7955
+ `${printable(arg)} is not a naro:// path. ${RM_USAGE}`
7956
+ );
7957
+ if (!recursive && (url.path === "" || url.path.endsWith("/")))
7958
+ throw new NaroError(
7959
+ "VALIDATION_FAILED",
7960
+ `${printable(arg)} is a folder: pass --recursive to remove what is under it.`
7961
+ );
7962
+ byBucket.set(url.bucket, [...byBucket.get(url.bucket) ?? [], url.path]);
7963
+ }
7964
+ if (!recursive)
7965
+ for (const [bucket, paths] of byBucket)
7966
+ await confirmOrAbort(
7967
+ context,
7968
+ `Remove ${count(new Set(paths).size, "object")} from ${bucket}?`
7969
+ );
7970
+ else if (!context.flags.yes && cannotAsk(context))
7971
+ throw new NaroError(
7972
+ "CONFIRMATION_REQUIRED",
7973
+ `Remove everything under ${context.positionals.map((arg) => printable(arg)).join(", ")}? Re-run with --yes to confirm.`
7974
+ );
7975
+ const { projectId, storage } = await objects(context);
7976
+ const results = [];
7977
+ for (const [bucket, paths] of byBucket) {
7978
+ let names;
7979
+ if (recursive) {
7980
+ const found = await namesUnder(storage, bucket, paths);
7981
+ names = found.names;
7982
+ if (found.truncated) context.output.warn(TRUNCATED_WARNING);
7983
+ } else names = [...new Set(paths)];
7984
+ if (names.length === 0) {
7985
+ context.output.log(`Nothing to remove in ${bucket}.`);
7986
+ results.push({ bucket, requested: 0, removed: [] });
7987
+ continue;
7988
+ }
7989
+ if (recursive)
7990
+ await confirmOrAbort(
7991
+ context,
7992
+ `Remove ${count(names.length, "object")} from ${bucket}?`
7993
+ );
7994
+ const removed = [];
7995
+ try {
7996
+ for (let at = 0; at < names.length; at += REMOVE_BATCH) {
7997
+ const page = unwrap(
7998
+ await storage.from(bucket).remove(names.slice(at, at + REMOVE_BATCH))
7999
+ );
8000
+ removed.push(...page.map((object) => object.name));
8001
+ }
8002
+ } catch (error) {
8003
+ if (removed.length > 0)
8004
+ context.output.warn(
8005
+ `\u26A0 Removed ${removed.length.toLocaleString("en-US")} of ${count(names.length, "object")} from ${bucket} before the error.`
8006
+ );
8007
+ throw error;
8008
+ }
8009
+ if (removed.length < names.length)
8010
+ context.output.warn(
8011
+ `\u26A0 ${String(names.length - removed.length)} of ${String(names.length)} were not removed: they were not there, or another request was changing them.`
8012
+ );
8013
+ context.output.log(
8014
+ removed.length === 0 ? `Nothing was removed from ${bucket}.` : `\u2705 Removed ${count(removed.length, "object")} from ${bucket}.`
8015
+ );
8016
+ results.push({ bucket, requested: names.length, removed });
8017
+ }
8018
+ context.output.result({ projectId, removed: results });
8019
+ }
8020
+ var ENV_KEY_REFUSED = "\u26A0 The service key came from NARO_SERVICE_KEY, which is used in every project: it may be another project's. Unset it to use this project's key.";
8021
+ async function explainingKey(context, run) {
8022
+ try {
8023
+ await run(context);
8024
+ } catch (error) {
8025
+ if (isNaroError(error) && error.code === "INVALID_API_KEY" && (process.env.NARO_SERVICE_KEY?.trim() ?? "") !== "")
8026
+ context.output.warn(ENV_KEY_REFUSED);
8027
+ throw error;
8028
+ }
8029
+ }
8030
+ function runStorageLs(context) {
8031
+ return explainingKey(context, storageLs);
8032
+ }
8033
+ function runStorageCp(context) {
8034
+ return explainingKey(context, storageCp);
8035
+ }
8036
+ function runStorageRm(context) {
8037
+ return explainingKey(context, storageRm);
8038
+ }
8039
+
8040
+ // src/router.ts
8041
+ var HELP = `naro \u2014 deploy Next.js apps to Naro (https://naro.sh)
8042
+
8043
+ Usage:
8044
+ naro login | logout | whoami
8045
+ naro token Print an access token for CI (NARO_TOKEN)
8046
+
8047
+ naro link | init Link this directory to a Naro project
8048
+ naro unlink Remove the link (.naro/)
8049
+ naro projects [--team <slug>] List projects
8050
+ naro project inspect Show the linked project
8051
+
8052
+ naro deploy [--prod] Build locally and deploy (preview by default)
8053
+ [--root <dir>] [--name <name>] --root points at the app in a monorepo
8054
+ naro deployments List deployments of the linked project
8055
+ naro inspect <deployment-id> Show one deployment + build logs
8056
+ naro cancel <deployment-id> Stop a build that is still running
8057
+
8058
+ naro git connect [owner/repo] Deploy on every push (defaults to your origin remote)
8059
+ naro git disconnect Stop deploying on push
8060
+
8061
+ naro logs [deployment-id] Runtime logs (or build logs of a deployment)
8062
+ [--type build|runtime] [--env production|preview] [--level error] [--follow]
8063
+
8064
+ naro insights [--metric lcp] [--range 7d] [--env preview]
8065
+ Speed Insights: Web Vitals p75 by page, device, the elements
8066
+ behind slow visits and deployment (production by default)
8067
+ naro insights --route '/posts/[id]' The paths opened under one route
8068
+ naro insights enable | disable Turn measuring on or off (production pages get a
8069
+ same-origin script, preview pages too; past measurements are kept)
8070
+
8071
+ naro protection Preview protection: on or off, and whether a bypass secret is set
8072
+ naro protection enable | disable Protect the preview URL (members only) or open it to anyone with the link (disable asks first; --yes in CI)
8073
+ naro protection bypass [remove] Create (shown once) or remove the secret automation sends as x-naro-protection-bypass (replacing or removing asks first; --yes in CI)
8074
+
8075
+ naro env list List env vars (secrets masked)
8076
+ naro env add KEY [environment] Add/update (value from prompt or stdin)
8077
+ naro env remove KEY [environment] Remove
8078
+ naro env pull [file] Write env vars to .env.local
8079
+
8080
+ naro domains list | add <host> [--no-www] | remove <host>
8081
+ add example.com also connects www.example.com and redirects the apex to
8082
+ it, because most registrars cannot put a CNAME on an apex; --no-www
8083
+ registers the apex alone
8084
+ naro domains verify [host] [--wait] Re-check DNS and print what is missing
8085
+ exits non-zero while a domain is not live yet
8086
+
8087
+ naro db enable Create the project's database (Cloudflare D1)
8088
+ naro db info Database status, size, table count and limits
8089
+ and today's writes against the daily write limit (UTC)
8090
+ naro db query <sql> | --file <f> Run SQL against it (reads stdin when piped)
8091
+ naro db push [--dry-run] Apply pending migrations from ./migrations (asks first; --yes to skip)
8092
+ --dry-run checks their policy statements on this machine and
8093
+ exits 1 on one the server would refuse
8094
+ naro db dump [--output <f>] Download a SQL dump of the database (stdout by default)
8095
+ [--schema-only|--data-only] [--table a,b] Cloudflare holds other queries until it finishes
8096
+ naro db restore --bookmark <id> Roll the database back (Time Travel, last 30 days; asks first)
8097
+ | --timestamp <time> [--dry-run] prints the command that undoes it
8098
+ naro db pull [--dir <d>] Write the remote schema as migrations/0001_remote_schema.sql
8099
+ and record it as applied - a baseline (asks first)
8100
+ naro db reset [--dir <d>] Drop everything and clear the policies and the ledger, then re-apply
8101
+ [--dry-run] [--yes] ./migrations, which create the policies again. Refuses files the server
8102
+ would refuse; lists first the policies the files do not make
8103
+ (asks first; db restore --bookmark undoes it)
8104
+ naro db lint [--strict] Check the schema for what D1 actually trips on, and the RLS policies
8105
+ (rls_no_policy, rls_policy_invalid, rls_policy_text_default,
8106
+ rls_policy_unindexed; exits 1 on an error, --strict on a warning too)
8107
+ naro db table create <name> --column <spec> [--column <spec>\u2026]
8108
+ [--owner <column>] [--if-not-exists] Create a table without SQL. <spec> is name:type[:modifier\u2026]:
8109
+ types integer, real, text, blob, numeric, boolean, datetime, json;
8110
+ modifiers pk, autoincrement, notnull, unique, default=<v>,
8111
+ references=<table>.<column>, ondelete=cascade|set-null|\u2026
8112
+ --owner adds that column (TEXT NOT NULL) and an owner policy
8113
+ naro db table add-column <table> --column <spec>
8114
+ naro db table rename-column <table> <column> <new-name>
8115
+ naro db table rename <table> <new-name>
8116
+ naro db table drop-column <table> <column> Asks first (--yes to skip)
8117
+ naro db table drop <table> Asks first (--yes to skip)
8118
+ naro db index create <table> <col[,col\u2026]> [--unique] [--name <name>]
8119
+ naro db index drop <name>
8120
+ Each runs as one migration in d1_migrations and is written to ./migrations
8121
+ (--no-write skips the file; --dry-run prints the SQL and changes nothing)
8122
+ naro migration new <name> Create migrations/NNNN_<name>.sql (stdin becomes the body when piped)
8123
+ naro migration list Local vs applied migrations
8124
+ naro migration up [--dry-run] Same as db push
8125
+ naro migration repair <name> Fix the ledger alone: mark a migration applied or
8126
+ --status applied|reverted [--yes] remove its row. Its SQL is never run (asks first)
8127
+ naro gen types [--lang typescript] [--output <file>]
8128
+ TypeScript types for the schema, for createClient<Database>()
8129
+ from naro-js (stdout by default)
4631
8130
 
4632
8131
  naro backend [status] On or off, status, URL and anon key (the service key stays hidden)
4633
8132
  naro backend enable Turn the backend on: D1, sign-in and the Data API (safe to re-run;
@@ -4686,6 +8185,27 @@ Usage:
4686
8185
  naro_insert, naro_update, naro_delete (shorthand for CREATE POLICY)
4687
8186
  naro policy remove <table> Drop every policy on it: back to deny (asks first)
4688
8187
 
8188
+ naro storage [status] Naro Storage: on or off, limits, what is stored, the buckets
8189
+ naro storage enable Turn Storage on: the project's R2 bucket, bound to its backend
8190
+ (safe to re-run; applies a plan change's limits)
8191
+ naro storage bucket list The buckets: public or private, objects, bytes, limits
8192
+ naro storage bucket create <name> [--public] [--file-size-limit <size>] [--allowed-mime-types <t,\u2026>]
8193
+ <size>: bytes, 5MB (1000s) or 5MiB (1024s); turns Storage on first if needed
8194
+ naro storage bucket update <name> [--public | --private] [--file-size-limit <size> | none] [--allowed-mime-types <t,\u2026> | any]
8195
+ --public asks first: anyone with an object's URL can then download it
8196
+ naro storage bucket empty <name> Delete every object in it and keep the bucket (asks first)
8197
+ naro storage bucket remove <name> [--empty]
8198
+ Remove an empty bucket; --empty deletes its objects first (asks first)
8199
+ naro storage ls [naro://<bucket>[/<prefix>]] [--recursive]
8200
+ The buckets, or the folders and objects under a prefix
8201
+ naro storage cp <from> <to> [--upsert] [--content-type <t>] [--cache-control <seconds>]
8202
+ One file between this machine and naro://<bucket>/<path>, 50 MiB at most,
8203
+ with the project's service key (NARO_SERVICE_KEY, or fetched with a warning).
8204
+ A download replaces a local file of the same name
8205
+ naro storage rm naro://<bucket>/<path>\u2026 [--recursive]
8206
+ Remove objects; --recursive removes what is under a folder (asks first):
8207
+ <path>/ is the folder alone, <path> also an object of that name
8208
+
4689
8209
  naro promote <deployment-id> Publish a build to production
4690
8210
  naro rollback [deployment-id] Roll production back to the previous build
4691
8211
 
@@ -4700,6 +8220,10 @@ Global options:
4700
8220
  --cwd <dir> Run as if started in <dir>
4701
8221
  --prod Deploy/target production
4702
8222
  --wait Poll until the domain goes live (naro domains verify)
8223
+ --metric <m> lcp, inp, cls, fcp or ttfb; lcp by default (naro insights)
8224
+ --range <r> 24h, 7d or 30d; 7d by default (naro insights)
8225
+ --route <pattern> A route from the pages table, e.g. '/posts/[id]': the paths under it (naro insights)
8226
+ --env <e> production or preview (naro logs, naro insights: production by default; naro env pull: preview by default)
4703
8227
  --no-www Register the apex alone (naro domains add)
4704
8228
  --file <path> SQL file to run (naro db query, naro policy create)
4705
8229
  --dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
@@ -4726,6 +8250,14 @@ Global options:
4726
8250
  --password-stdin Read the new user's password from stdin (a pipe, or a hidden prompt)
4727
8251
  --password <p> The password in argv: works, but lands in shell history (naro backend users create)
4728
8252
  --min-length <n> Fewest characters of a new password, 6-128 (naro backend auth password)
8253
+ --public | --private Who may download a bucket's objects by URL (naro storage bucket create, update)
8254
+ --file-size-limit <size> A bucket's largest object: bytes, 5MB or 5MiB; none clears it
8255
+ --allowed-mime-types <t,\u2026> The types a bucket takes, like image/png,image/*; any clears it
8256
+ --empty Delete the bucket's objects first (naro storage bucket remove)
8257
+ --recursive, -r Everything under a folder (naro storage ls, naro storage rm)
8258
+ --upsert Replace an object that is there (naro storage cp)
8259
+ --content-type <t> The object's type; by default from the file's extension (naro storage cp)
8260
+ --cache-control <seconds> How long a browser may keep it; 3600 by default (naro storage cp)
4729
8261
  --select <rule> Rule for reads; --insert, --update, --delete likewise (naro policy set)
4730
8262
  --owner <column> Column that holds the signed-in user's id, for the owner rule (naro policy set, db table create)
4731
8263
  --select-when <filter> Condition for reads, e.g. 'user_id.eq.auth.uid()'; --insert-when (what a new
@@ -4819,6 +8351,56 @@ function routeBackendUsers(words) {
4819
8351
  return null;
4820
8352
  }
4821
8353
  }
8354
+ function routeStorage(words) {
8355
+ const [sub, action] = words;
8356
+ switch (sub) {
8357
+ case void 0:
8358
+ case "status":
8359
+ return words.length <= 1 ? { handler: runStorageStatus, positionals: [] } : null;
8360
+ case "enable":
8361
+ return words.length === 1 ? { handler: runStorageEnable, positionals: [] } : null;
8362
+ case "bucket":
8363
+ case "buckets":
8364
+ switch (action) {
8365
+ case void 0:
8366
+ case "list":
8367
+ case "ls":
8368
+ return words.length <= 2 ? { handler: runStorageBucketList, positionals: [] } : null;
8369
+ case "create":
8370
+ return {
8371
+ handler: runStorageBucketCreate,
8372
+ positionals: words.slice(2)
8373
+ };
8374
+ case "update":
8375
+ return {
8376
+ handler: runStorageBucketUpdate,
8377
+ positionals: words.slice(2)
8378
+ };
8379
+ case "empty":
8380
+ return {
8381
+ handler: runStorageBucketEmpty,
8382
+ positionals: words.slice(2)
8383
+ };
8384
+ case "remove":
8385
+ case "rm":
8386
+ case "delete":
8387
+ return {
8388
+ handler: runStorageBucketRemove,
8389
+ positionals: words.slice(2)
8390
+ };
8391
+ default:
8392
+ return null;
8393
+ }
8394
+ case "ls":
8395
+ return { handler: runStorageLs, positionals: words.slice(1) };
8396
+ case "cp":
8397
+ return { handler: runStorageCp, positionals: words.slice(1) };
8398
+ case "rm":
8399
+ return { handler: runStorageRm, positionals: words.slice(1) };
8400
+ default:
8401
+ return null;
8402
+ }
8403
+ }
4822
8404
  function route(parsed) {
4823
8405
  const { command, positionals } = parsed;
4824
8406
  const sub = positionals[0];
@@ -4856,6 +8438,39 @@ function route(parsed) {
4856
8438
  return { handler: runCancel, positionals };
4857
8439
  case "logs":
4858
8440
  return { handler: runLogs, positionals };
8441
+ // Spec 2026-10-08 §5.
8442
+ case "insights": {
8443
+ if (positionals.length > 1) return null;
8444
+ switch (sub) {
8445
+ case void 0:
8446
+ return { handler: runInsights, positionals: [] };
8447
+ case "enable":
8448
+ return { handler: runInsightsEnable, positionals: [] };
8449
+ case "disable":
8450
+ return { handler: runInsightsDisable, positionals: [] };
8451
+ default:
8452
+ return null;
8453
+ }
8454
+ }
8455
+ // Spec 2026-10-08-preview-protection §9.
8456
+ case "protection": {
8457
+ const [, second, third] = positionals;
8458
+ if (third !== void 0) return null;
8459
+ switch (sub) {
8460
+ case void 0:
8461
+ return { handler: runProtection, positionals: [] };
8462
+ case "enable":
8463
+ return second === void 0 ? { handler: runProtectionEnable, positionals: [] } : null;
8464
+ case "disable":
8465
+ return second === void 0 ? { handler: runProtectionDisable, positionals: [] } : null;
8466
+ case "bypass":
8467
+ if (second === void 0)
8468
+ return { handler: runProtectionBypass, positionals: [] };
8469
+ return second === "remove" ? { handler: runProtectionBypassRemove, positionals: [] } : null;
8470
+ default:
8471
+ return null;
8472
+ }
8473
+ }
4859
8474
  case "env":
4860
8475
  switch (sub) {
4861
8476
  case void 0:
@@ -4989,6 +8604,8 @@ function route(parsed) {
4989
8604
  default:
4990
8605
  return null;
4991
8606
  }
8607
+ case "storage":
8608
+ return routeStorage(positionals);
4992
8609
  case "policy":
4993
8610
  switch (sub) {
4994
8611
  case void 0:
@@ -5035,8 +8652,8 @@ function route(parsed) {
5035
8652
  function version() {
5036
8653
  try {
5037
8654
  const pkg = JSON.parse(
5038
- readFileSync4(
5039
- join5(dirname3(fileURLToPath(import.meta.url)), "..", "package.json"),
8655
+ readFileSync5(
8656
+ join7(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"),
5040
8657
  "utf8"
5041
8658
  )
5042
8659
  );