naro-cli 0.19.0 → 0.20.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.
@@ -5,9 +5,9 @@ import {
5
5
 
6
6
  // src/deploy.ts
7
7
  import { existsSync as existsSync7 } from "fs";
8
- import { mkdtemp as mkdtemp2, readFile as readFile14, rm as rm5 } from "fs/promises";
8
+ import { mkdtemp as mkdtemp2, readFile as readFile15, rm as rm5 } from "fs/promises";
9
9
  import { tmpdir as tmpdir2 } from "os";
10
- import { basename as basename4, dirname as dirname6, join as join25, resolve as resolve11 } from "path";
10
+ import { basename as basename4, dirname as dirname6, join as join26, resolve as resolve11 } from "path";
11
11
  import { fileURLToPath } from "url";
12
12
 
13
13
  // ../../packages/build/src/artifact-upload.ts
@@ -287,9 +287,9 @@ function hashAsset(data, contentType2) {
287
287
  }
288
288
 
289
289
  // ../../packages/build/src/local-build-executor.ts
290
- import { access as access8, mkdtemp, readFile as readFile12, rm as rm3 } from "fs/promises";
290
+ import { access as access8, mkdtemp, readFile as readFile13, rm as rm3 } from "fs/promises";
291
291
  import { tmpdir } from "os";
292
- import { dirname as dirname4, join as join17, resolve as resolve7 } from "path";
292
+ import { dirname as dirname4, join as join18, resolve as resolve7 } from "path";
293
293
 
294
294
  // ../../packages/core/src/route-patterns.ts
295
295
  var ROUTE_PATTERN = /^\/[A-Za-z0-9\-._~%!$&()*+,;=:@/[\]]*$/;
@@ -345,6 +345,8 @@ var DEPLOYMENT_ERROR_CODES = [
345
345
  "FORBIDDEN",
346
346
  "NOT_FOUND",
347
347
  "DEPLOYMENT_NOT_PROMOTABLE",
348
+ /** A promotion to another surface rebuilds the commit, and the source was not built from Git (spec 2026-10-18-named-environments §9.1). */
349
+ "PROMOTE_NEEDS_GIT_SOURCE",
348
350
  "DEPLOYMENT_NOT_CANCELABLE",
349
351
  "SUBDOMAIN_TAKEN",
350
352
  "FLAG_EXISTS",
@@ -511,6 +513,9 @@ function projectPath(teamSlug, projectSlug) {
511
513
  return `/${teamSlug}/projects/${projectSlug}`;
512
514
  }
513
515
 
516
+ // ../../packages/core/src/platform-mail.ts
517
+ var encoder = new TextEncoder();
518
+
514
519
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
515
520
  var external_exports = {};
516
521
  __export(external_exports, {
@@ -15613,7 +15618,7 @@ import { access as access5, readFile as readFile5, writeFile as writeFile4 } fro
15613
15618
  import { join as join6, resolve as resolve2 } from "path";
15614
15619
 
15615
15620
  // ../../packages/build/src/open-next.ts
15616
- import { access as access3, readdir, readFile as readFile4 } from "fs/promises";
15621
+ import { access as access3, readdir, readFile as readFile4, realpath } from "fs/promises";
15617
15622
  import {
15618
15623
  basename,
15619
15624
  dirname,
@@ -15729,7 +15734,33 @@ function contentType(path) {
15729
15734
  return "application/octet-stream";
15730
15735
  }
15731
15736
  }
15732
- async function listArtifactFiles(root, label2 = "OpenNext") {
15737
+ async function assertInsideOnDisk(appRoot, directory, label2) {
15738
+ let realRoot;
15739
+ let realDirectory;
15740
+ try {
15741
+ [realRoot, realDirectory] = await Promise.all([
15742
+ realpath(appRoot),
15743
+ realpath(directory)
15744
+ ]);
15745
+ } catch {
15746
+ return;
15747
+ }
15748
+ const relation = relative(realRoot, realDirectory);
15749
+ if (relation === ".." || relation.startsWith(`..${sep}`) || isAbsolute(relation)) {
15750
+ throw new DeploymentError(
15751
+ "ARTIFACT_INVALID",
15752
+ `${label2} output leads out of the application root through a symbolic link.`
15753
+ );
15754
+ }
15755
+ if (relation.split(sep).includes(".git")) {
15756
+ throw new DeploymentError(
15757
+ "ARTIFACT_INVALID",
15758
+ `${label2} output leads into .git through a symbolic link.`
15759
+ );
15760
+ }
15761
+ }
15762
+ async function listArtifactFiles(root, label2 = "OpenNext", appRoot) {
15763
+ if (appRoot !== void 0) await assertInsideOnDisk(appRoot, root, label2);
15733
15764
  const files = [];
15734
15765
  async function visit(directory) {
15735
15766
  const entries = await readdir(directory, { withFileTypes: true });
@@ -15786,7 +15817,6 @@ var OPEN_NEXT_LAYOUT = {
15786
15817
  outputRoot: ".open-next",
15787
15818
  bundleDir: ".open-next/worker-bundle"
15788
15819
  };
15789
- var listFiles = (root) => listArtifactFiles(root);
15790
15820
  async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15791
15821
  const configPath = await findWranglerConfig(appRoot, wranglerConfigPath);
15792
15822
  return readWorkerArtifact(appRoot, identity, {
@@ -15795,6 +15825,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15795
15825
  });
15796
15826
  }
15797
15827
  async function readWorkerArtifact(appRoot, identity, layout) {
15828
+ const listFiles = (root) => listArtifactFiles(root, "OpenNext", appRoot);
15798
15829
  try {
15799
15830
  const configPath = await findWranglerConfig(
15800
15831
  appRoot,
@@ -15839,12 +15870,13 @@ async function readWorkerArtifact(appRoot, identity, layout) {
15839
15870
  );
15840
15871
  }
15841
15872
  const bundleRoot = confinedPath(appRoot, layout.bundleDir, "Bundle dir");
15842
- let bundleFiles = null;
15873
+ let bundleExists = true;
15843
15874
  try {
15844
15875
  await access3(bundleRoot);
15845
- bundleFiles = await listFiles(bundleRoot);
15846
15876
  } catch {
15877
+ bundleExists = false;
15847
15878
  }
15879
+ const bundleFiles = bundleExists ? await listFiles(bundleRoot) : null;
15848
15880
  const outputFiles = bundleFiles ?? await listFiles(outputRoot);
15849
15881
  const entrypointName = bundleFiles ? basename(entrypointPath) : relative(outputRoot, entrypointPath).split(sep).join("/");
15850
15882
  const assetsPrefix = `${relative(outputRoot, configuredAssets).split(sep).join("/")}/`;
@@ -16192,7 +16224,7 @@ async function readVinextArtifact(appRoot, identity) {
16192
16224
  const entrypointName = metadata.main.split(/[\\/]/).join("/");
16193
16225
  const matchers = moduleMatchers(metadata.rules);
16194
16226
  const isModule = (name) => matchers.length === 0 || matchers.some((matcher) => matcher.test(name));
16195
- const serverFiles = await listArtifactFiles(serverRoot, "vinext");
16227
+ const serverFiles = await listArtifactFiles(serverRoot, "vinext", appRoot);
16196
16228
  const entrypoint = serverFiles.find((file2) => file2.name === entrypointName);
16197
16229
  if (!entrypoint) {
16198
16230
  throw new DeploymentError(
@@ -16206,7 +16238,7 @@ async function readVinextArtifact(appRoot, identity) {
16206
16238
  modules: serverFiles.filter(
16207
16239
  (file2) => file2.name !== entrypointName && file2.name !== WRANGLER_OUTPUT_NAME && !file2.name.endsWith(".map") && isModule(file2.name)
16208
16240
  ),
16209
- assets: await listArtifactFiles(clientRoot, "vinext"),
16241
+ assets: await listArtifactFiles(clientRoot, "vinext", appRoot),
16210
16242
  compatibilityDate: metadata.compatibility_date,
16211
16243
  compatibilityFlags: Array.isArray(metadata.compatibility_flags) ? metadata.compatibility_flags.filter(
16212
16244
  (flag) => typeof flag === "string"
@@ -16879,7 +16911,8 @@ async function runCommand(spec, observer) {
16879
16911
  }
16880
16912
 
16881
16913
  // ../../packages/build/src/repository.ts
16882
- import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "path";
16914
+ import { realpath as realpath2 } from "fs/promises";
16915
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
16883
16916
  function resolveAppRoot(checkout, configuredRoot) {
16884
16917
  const checkoutRoot = resolve3(checkout);
16885
16918
  if (!configuredRoot) return checkoutRoot;
@@ -16891,12 +16924,34 @@ function resolveAppRoot(checkout, configuredRoot) {
16891
16924
  if (relativeRoot === ".." || relativeRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute2(relativeRoot)) {
16892
16925
  throw new Error("Configured root directory escapes repository");
16893
16926
  }
16927
+ if (relativeRoot.split(/[\\/]/).includes(".git")) {
16928
+ throw new Error("Configured root directory cannot be inside .git");
16929
+ }
16894
16930
  return appRoot;
16895
16931
  }
16932
+ async function assertAppRootInsideCheckout(checkout, appRoot) {
16933
+ let realCheckout;
16934
+ let realApp;
16935
+ try {
16936
+ [realCheckout, realApp] = await Promise.all([
16937
+ realpath2(checkout),
16938
+ realpath2(appRoot)
16939
+ ]);
16940
+ } catch {
16941
+ return;
16942
+ }
16943
+ const relation = relative2(realCheckout, realApp);
16944
+ if (relation === ".." || relation.startsWith(`..${sep2}`) || isAbsolute2(relation)) {
16945
+ throw new Error("Configured root directory escapes repository");
16946
+ }
16947
+ if (relation.split(sep2).includes(".git")) {
16948
+ throw new Error("Configured root directory cannot be inside .git");
16949
+ }
16950
+ }
16896
16951
 
16897
16952
  // ../../packages/build/src/static-artifact.ts
16898
16953
  import { readdir as readdir3, readFile as readFile7 } from "fs/promises";
16899
- import { isAbsolute as isAbsolute3, join as join9, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
16954
+ import { isAbsolute as isAbsolute3, join as join9, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
16900
16955
  var SHIM_WORKER_SOURCE = "export default { fetch: (request, env) => env.ASSETS.fetch(request) };\n";
16901
16956
  var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules"]);
16902
16957
  var ALLOWED_DOTFILE = ".well-known";
@@ -16912,11 +16967,16 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16912
16967
  );
16913
16968
  const outputRoot = resolve4(appRoot, outputDirectory);
16914
16969
  const relation = relative3(appRoot, outputRoot);
16915
- if (relation === ".." || relation.startsWith(`..${sep2}`) || isAbsolute3(relation))
16970
+ if (relation === ".." || relation.startsWith(`..${sep3}`) || isAbsolute3(relation))
16916
16971
  throw new DeploymentError(
16917
16972
  "ARTIFACT_INVALID",
16918
16973
  "Output directory escapes the application root."
16919
16974
  );
16975
+ if (relation.split(sep3).includes(".git"))
16976
+ throw new DeploymentError(
16977
+ "ARTIFACT_INVALID",
16978
+ "Output directory cannot be inside .git."
16979
+ );
16920
16980
  const assets = [];
16921
16981
  async function visit(directory) {
16922
16982
  const entries = await readdir3(directory, { withFileTypes: true });
@@ -16933,7 +16993,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16933
16993
  if (entry.isDirectory()) await visit(path);
16934
16994
  else if (entry.isFile()) {
16935
16995
  assets.push({
16936
- name: relative3(outputRoot, path).split(sep2).join("/"),
16996
+ name: relative3(outputRoot, path).split(sep3).join("/"),
16937
16997
  data: await readFile7(path),
16938
16998
  contentType: contentType(path)
16939
16999
  });
@@ -16941,6 +17001,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16941
17001
  }
16942
17002
  }
16943
17003
  try {
17004
+ await assertInsideOnDisk(appRoot, outputRoot, "Static");
16944
17005
  await visit(outputRoot);
16945
17006
  } catch (error51) {
16946
17007
  if (error51 instanceof DeploymentError) throw error51;
@@ -16980,7 +17041,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16980
17041
  // ../../packages/build/src/strategy/fingerprint.ts
16981
17042
  import { createHash as createHash3 } from "crypto";
16982
17043
  import { readFile as readFile8 } from "fs/promises";
16983
- import { join as join10, relative as relative4, sep as sep3 } from "path";
17044
+ import { join as join10, relative as relative4, sep as sep4 } from "path";
16984
17045
  var APP_ROOT_FILES = [
16985
17046
  "package.json",
16986
17047
  "next.config.js",
@@ -17035,7 +17096,7 @@ async function hashStrategyInputs(checkout, appRoot) {
17035
17096
  for (const path of candidates) {
17036
17097
  const digest = await hashFile(path);
17037
17098
  if (digest === null) continue;
17038
- entries.push([relative4(checkout, path).split(sep3).join("/"), digest]);
17099
+ entries.push([relative4(checkout, path).split(sep4).join("/"), digest]);
17039
17100
  }
17040
17101
  return entries.toSorted(
17041
17102
  ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
@@ -17056,8 +17117,76 @@ async function computeStrategyFingerprint(input) {
17056
17117
  }
17057
17118
 
17058
17119
  // ../../packages/build/src/strategy/project-facts.ts
17059
- import { access as access6, readFile as readFile9 } from "fs/promises";
17120
+ import { access as access6, readFile as readFile10 } from "fs/promises";
17121
+ import { join as join12 } from "path";
17122
+
17123
+ // ../../packages/build/src/strategy/user-adapter.ts
17124
+ import { readFile as readFile9 } from "fs/promises";
17060
17125
  import { join as join11 } from "path";
17126
+ var ADAPTER_PATH = /\badapterPath\b/;
17127
+ var EXPERIMENTAL_ADAPTER_PATH = /\bexperimental\s*:\s*\{[^{}]*\badapterPath\b/;
17128
+ function adapterKeyFromConfigSource(source) {
17129
+ const code = stripComments(source);
17130
+ if (!ADAPTER_PATH.test(code)) return null;
17131
+ return EXPERIMENTAL_ADAPTER_PATH.test(code) ? "experimental.adapterPath" : "adapterPath";
17132
+ }
17133
+ function keysNextReads(nextVersion) {
17134
+ const version2 = parseNextVersion(nextVersion);
17135
+ if (!version2) return ["adapterPath", "experimental.adapterPath"];
17136
+ if (version2.major < 15 || version2.major === 15 && version2.minor < 4)
17137
+ return [];
17138
+ if (version2.major > 16 || version2.major === 16 && version2.minor >= 2)
17139
+ return ["adapterPath", "experimental.adapterPath"];
17140
+ return ["experimental.adapterPath"];
17141
+ }
17142
+ function userAdapterOf(input) {
17143
+ const keys = keysNextReads(input.nextVersion);
17144
+ if (keys.length === 0) return null;
17145
+ const config2 = input.facts.adapterConfig;
17146
+ if (config2 && keys.includes(config2.key))
17147
+ return { source: "next-config", file: config2.file, key: config2.key };
17148
+ if (input.env.NEXT_ADAPTER_PATH) return { source: "env" };
17149
+ return null;
17150
+ }
17151
+ async function adapterInBuildOutput(appRoot, nextVersion) {
17152
+ const keys = keysNextReads(nextVersion);
17153
+ if (keys.length === 0) return null;
17154
+ let manifest2;
17155
+ try {
17156
+ const parsed = JSON.parse(
17157
+ await readFile9(
17158
+ join11(appRoot, ".next", "required-server-files.json"),
17159
+ "utf8"
17160
+ )
17161
+ );
17162
+ if (typeof parsed !== "object" || parsed === null) return null;
17163
+ manifest2 = parsed;
17164
+ } catch {
17165
+ return null;
17166
+ }
17167
+ const topLevel = manifest2.config?.adapterPath;
17168
+ if (keys.includes("adapterPath") && typeof topLevel === "string" && topLevel)
17169
+ return { source: "build-output", key: "adapterPath" };
17170
+ const experimental = manifest2.config?.experimental?.adapterPath;
17171
+ if (keys.includes("experimental.adapterPath") && typeof experimental === "string" && experimental)
17172
+ return { source: "build-output", key: "experimental.adapterPath" };
17173
+ return null;
17174
+ }
17175
+ function describeUserAdapter(adapter) {
17176
+ switch (adapter.source) {
17177
+ case "next-config":
17178
+ return `${adapter.file} ${adapter.key}`;
17179
+ case "env":
17180
+ return "NEXT_ADAPTER_PATH";
17181
+ case "build-output":
17182
+ return `${adapter.key} in the Next.js build output`;
17183
+ }
17184
+ }
17185
+ function openNextAdapterWarning(adapter) {
17186
+ return `P2 opennext-cloudflare: The project sets its own Next.js adapter (${describeUserAdapter(adapter)}); its modifyConfig and onBuildComplete also run in the OpenNext build.`;
17187
+ }
17188
+
17189
+ // ../../packages/build/src/strategy/project-facts.ts
17061
17190
  var SOURCE_EXTENSIONS2 = ["ts", "tsx", "js", "jsx", "mjs", "mts"];
17062
17191
  var SOURCE_DIRECTORIES = ["", "src"];
17063
17192
  var VITE_CONFIGS = [
@@ -17078,7 +17207,7 @@ async function findSourceFile(appRoot, basename5) {
17078
17207
  for (const dir of SOURCE_DIRECTORIES) {
17079
17208
  for (const ext of SOURCE_EXTENSIONS2) {
17080
17209
  const rel = dir ? `${dir}/${basename5}.${ext}` : `${basename5}.${ext}`;
17081
- if (await exists4(join11(appRoot, rel))) return rel;
17210
+ if (await exists4(join12(appRoot, rel))) return rel;
17082
17211
  }
17083
17212
  }
17084
17213
  return null;
@@ -17098,19 +17227,28 @@ function matcherRoutesFromSource(source) {
17098
17227
  }
17099
17228
  async function readIfExists(path) {
17100
17229
  try {
17101
- return await readFile9(path, "utf8");
17230
+ return await readFile10(path, "utf8");
17102
17231
  } catch {
17103
17232
  return null;
17104
17233
  }
17105
17234
  }
17235
+ async function findAdapterConfig(appRoot) {
17236
+ for (const file2 of NEXT_CONFIG_FILES) {
17237
+ const source = await readIfExists(join12(appRoot, file2));
17238
+ if (source === null) continue;
17239
+ const key = adapterKeyFromConfigSource(source);
17240
+ return key ? { file: file2, key } : null;
17241
+ }
17242
+ return null;
17243
+ }
17106
17244
  async function collectProjectFacts(appRoot) {
17107
17245
  const proxyFile = await findSourceFile(appRoot, "proxy");
17108
17246
  const middlewareFile = await findSourceFile(appRoot, "middleware");
17109
17247
  const routeSourceFile = middlewareFile ?? proxyFile;
17110
- const routeSource = routeSourceFile ? await readIfExists(join11(appRoot, routeSourceFile)) : null;
17248
+ const routeSource = routeSourceFile ? await readIfExists(join12(appRoot, routeSourceFile)) : null;
17111
17249
  let usesVinext = false;
17112
17250
  let nextSpec = null;
17113
- const packageJsonSource = await readIfExists(join11(appRoot, "package.json"));
17251
+ const packageJsonSource = await readIfExists(join12(appRoot, "package.json"));
17114
17252
  if (packageJsonSource) {
17115
17253
  try {
17116
17254
  const packageJson = JSON.parse(packageJsonSource);
@@ -17121,7 +17259,7 @@ async function collectProjectFacts(appRoot) {
17121
17259
  nextSpec = dependency("next");
17122
17260
  if (dependency("vinext") !== null) {
17123
17261
  for (const name of VITE_CONFIGS) {
17124
- const config2 = await readIfExists(join11(appRoot, name));
17262
+ const config2 = await readIfExists(join12(appRoot, name));
17125
17263
  if (config2 && /\bvinext\s*\(/.test(config2) && /@cloudflare\/vite-plugin/.test(config2)) {
17126
17264
  usesVinext = true;
17127
17265
  break;
@@ -17136,7 +17274,8 @@ async function collectProjectFacts(appRoot) {
17136
17274
  middlewareFile,
17137
17275
  matcherRoutes: routeSource ? matcherRoutesFromSource(routeSource) : [],
17138
17276
  usesVinext,
17139
- nextSpec
17277
+ nextSpec,
17278
+ adapterConfig: await findAdapterConfig(appRoot)
17140
17279
  };
17141
17280
  }
17142
17281
 
@@ -17523,11 +17662,11 @@ function defaultSmokeProbes(matcherRoutes) {
17523
17662
 
17524
17663
  // ../../packages/build/src/strategy/strategies/opennext-aws.ts
17525
17664
  import { writeFile as writeFile6 } from "fs/promises";
17526
- import { join as join13 } from "path";
17665
+ import { join as join14 } from "path";
17527
17666
 
17528
17667
  // ../../packages/build/src/strategy/strategies/shared.ts
17529
- import { access as access7, readFile as readFile10, rm, writeFile as writeFile5 } from "fs/promises";
17530
- import { dirname as dirname2, join as join12, resolve as resolve5 } from "path";
17668
+ import { access as access7, readFile as readFile11, rm, writeFile as writeFile5 } from "fs/promises";
17669
+ import { dirname as dirname2, join as join13, resolve as resolve5 } from "path";
17531
17670
  function capturingObserver(observer) {
17532
17671
  const lines = [];
17533
17672
  return {
@@ -17584,7 +17723,7 @@ async function findProjectBin(appRoot, checkout, name) {
17584
17723
  let dir = resolve5(appRoot);
17585
17724
  const stop = resolve5(checkout);
17586
17725
  for (; ; ) {
17587
- const candidate = join12(dir, "node_modules", ".bin", name);
17726
+ const candidate = join13(dir, "node_modules", ".bin", name);
17588
17727
  if (await pathExists2(candidate)) return candidate;
17589
17728
  if (dir === stop) return null;
17590
17729
  const parent = dirname2(dir);
@@ -17598,7 +17737,7 @@ function dotenvQuote(value) {
17598
17737
  return `"${value.replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
17599
17738
  }
17600
17739
  async function withDevVars(wranglerConfigPath, env, run) {
17601
- const path = join12(dirname2(wranglerConfigPath), ".dev.vars");
17740
+ const path = join13(dirname2(wranglerConfigPath), ".dev.vars");
17602
17741
  const keys = Object.keys(env).filter(
17603
17742
  (key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)
17604
17743
  );
@@ -17631,7 +17770,7 @@ async function bundleWorker(context, layout, observer, errorCode2) {
17631
17770
  cwd: layout.appRoot,
17632
17771
  env: {
17633
17772
  CI: "1",
17634
- WRANGLER_LOG_PATH: join12(
17773
+ WRANGLER_LOG_PATH: join13(
17635
17774
  layout.appRoot,
17636
17775
  layout.outputRoot,
17637
17776
  "wrangler.log"
@@ -17664,12 +17803,12 @@ async function smokeWorker(context, layout, observer) {
17664
17803
  "--show-interactive-dev-session=false",
17665
17804
  "--log-level=warn",
17666
17805
  "--persist-to",
17667
- join12(layout.appRoot, layout.outputRoot, ".naro-smoke-state")
17806
+ join13(layout.appRoot, layout.outputRoot, ".naro-smoke-state")
17668
17807
  ],
17669
17808
  cwd: layout.appRoot,
17670
17809
  env: {
17671
17810
  CI: "1",
17672
- WRANGLER_LOG_PATH: join12(
17811
+ WRANGLER_LOG_PATH: join13(
17673
17812
  layout.appRoot,
17674
17813
  layout.outputRoot,
17675
17814
  "wrangler-dev.log"
@@ -17685,10 +17824,10 @@ async function smokeWorker(context, layout, observer) {
17685
17824
  );
17686
17825
  }
17687
17826
  async function approvePnpmBuilds(checkout, packages) {
17688
- const path = join12(checkout, "pnpm-workspace.yaml");
17827
+ const path = join13(checkout, "pnpm-workspace.yaml");
17689
17828
  let source = "";
17690
17829
  try {
17691
- source = await readFile10(path, "utf8");
17830
+ source = await readFile11(path, "utf8");
17692
17831
  } catch {
17693
17832
  }
17694
17833
  let next = source;
@@ -17781,7 +17920,7 @@ function createOpenNextAwsStrategy() {
17781
17920
  } catch (error51) {
17782
17921
  return failed("prepare", describeError(error51));
17783
17922
  }
17784
- const configPath = join13(fork.appRoot, CONFIG_NAME);
17923
+ const configPath = join14(fork.appRoot, CONFIG_NAME);
17785
17924
  try {
17786
17925
  await writeFile6(
17787
17926
  configPath,
@@ -17810,7 +17949,7 @@ function createOpenNextAwsStrategy() {
17810
17949
  observer
17811
17950
  );
17812
17951
  if (!build.ok) return failed("build", describeFailure(build));
17813
- const serverFunction = join13(fork.appRoot, SERVER_FUNCTION);
17952
+ const serverFunction = join14(fork.appRoot, SERVER_FUNCTION);
17814
17953
  if (!await pathExists2(serverFunction))
17815
17954
  return failed("bundle", `OpenNext AWS produced no ${SERVER_FUNCTION}.`);
17816
17955
  const smoke = await context.ports.smoke.run(
@@ -17832,7 +17971,7 @@ function createOpenNextAwsStrategy() {
17832
17971
  status: "success",
17833
17972
  artifact: {
17834
17973
  kind: "opennext-aws",
17835
- outputRoot: join13(fork.appRoot, ".open-next"),
17974
+ outputRoot: join14(fork.appRoot, ".open-next"),
17836
17975
  serverFunction
17837
17976
  },
17838
17977
  notes: [
@@ -17844,8 +17983,8 @@ function createOpenNextAwsStrategy() {
17844
17983
  }
17845
17984
 
17846
17985
  // ../../packages/build/src/strategy/strategies/opennext-cloudflare.ts
17847
- import { readFile as readFile11 } from "fs/promises";
17848
- import { join as join14 } from "path";
17986
+ import { readFile as readFile12 } from "fs/promises";
17987
+ import { join as join15 } from "path";
17849
17988
  var PROXY_TO_MIDDLEWARE_VARIANT = "proxy-to-middleware";
17850
17989
  var NODE_MIDDLEWARE_REFUSAL = /Node\.js middleware is not (?:currently )?supported/i;
17851
17990
  var NODE_MIDDLEWARE_EXPERIMENTAL = /Node\.js middleware support is experimental/i;
@@ -17853,8 +17992,8 @@ var NODE_MIDDLEWARE_NOT_DEPLOYED = 'Node.js-runtime middleware (runtime: "nodejs
17853
17992
  async function buildDeclaresNodeMiddleware(appRoot) {
17854
17993
  try {
17855
17994
  const manifest2 = JSON.parse(
17856
- await readFile11(
17857
- join14(appRoot, ".next", "server", "functions-config-manifest.json"),
17995
+ await readFile12(
17996
+ join15(appRoot, ".next", "server", "functions-config-manifest.json"),
17858
17997
  "utf8"
17859
17998
  )
17860
17999
  );
@@ -17864,7 +18003,7 @@ async function buildDeclaresNodeMiddleware(appRoot) {
17864
18003
  }
17865
18004
  }
17866
18005
  function createOpenNextCloudflareStrategy(options) {
17867
- async function attempt(context, roots, observer, ownBuild) {
18006
+ async function attempt(context, roots, observer, ownBuild, adapterNotice) {
17868
18007
  let platform;
17869
18008
  try {
17870
18009
  platform = await options.createPlatformBuildConfig(
@@ -17902,6 +18041,13 @@ function createOpenNextCloudflareStrategy(options) {
17902
18041
  outcome: ownBuild ? appBuildFailed(describeFailure(nextBuild)) : failed("build", describeFailure(nextBuild))
17903
18042
  };
17904
18043
  }
18044
+ if (!adapterNotice.warned) {
18045
+ const adapter = userAdapterOf(context) ?? await adapterInBuildOutput(roots.appRoot, context.nextVersion);
18046
+ if (adapter) {
18047
+ adapterNotice.warned = true;
18048
+ await warn(observer, openNextAdapterWarning(adapter));
18049
+ }
18050
+ }
17905
18051
  if (await buildDeclaresNodeMiddleware(roots.appRoot)) {
17906
18052
  return {
17907
18053
  ok: false,
@@ -17997,6 +18143,7 @@ function createOpenNextCloudflareStrategy(options) {
17997
18143
  target: "cloudflare",
17998
18144
  supports: () => ({ supported: true }),
17999
18145
  async evaluate(context, observer) {
18146
+ const adapterNotice = { warned: false };
18000
18147
  const rememberedRewrite = context.pinnedVariant === PROXY_TO_MIDDLEWARE_VARIANT && context.facts.proxyFile !== null;
18001
18148
  if (rememberedRewrite) {
18002
18149
  await info(
@@ -18008,7 +18155,13 @@ function createOpenNextCloudflareStrategy(options) {
18008
18155
  observer,
18009
18156
  "P2 opennext-cloudflare: building the project as written."
18010
18157
  );
18011
- const first = await attempt(context, context.workspace, observer, true);
18158
+ const first = await attempt(
18159
+ context,
18160
+ context.workspace,
18161
+ observer,
18162
+ true,
18163
+ adapterNotice
18164
+ );
18012
18165
  if (first.ok) {
18013
18166
  return {
18014
18167
  status: "success",
@@ -18040,7 +18193,13 @@ function createOpenNextCloudflareStrategy(options) {
18040
18193
  observer,
18041
18194
  `Converted ${conversion.file ?? context.facts.proxyFile} to edge middleware for this build only. Next.js 16 proxy runs on the Node.js runtime, which Cloudflare does not support; middleware runs on the Edge runtime, so Node.js-only APIs are unavailable. Your repository is unchanged.`
18042
18195
  );
18043
- const second = await attempt(context, fork, observer, rememberedRewrite);
18196
+ const second = await attempt(
18197
+ context,
18198
+ fork,
18199
+ observer,
18200
+ rememberedRewrite,
18201
+ adapterNotice
18202
+ );
18044
18203
  if (!second.ok) {
18045
18204
  if (!rememberedRewrite || second.outcome.status !== "failed" || !second.outcome.appBuild)
18046
18205
  return second.outcome;
@@ -18052,7 +18211,8 @@ function createOpenNextCloudflareStrategy(options) {
18052
18211
  context,
18053
18212
  context.workspace,
18054
18213
  observer,
18055
- true
18214
+ true,
18215
+ adapterNotice
18056
18216
  );
18057
18217
  if (asWritten.ok) {
18058
18218
  await info(
@@ -18121,7 +18281,7 @@ function createAwsMiddlewareCloudflareAppStrategy() {
18121
18281
  }
18122
18282
 
18123
18283
  // ../../packages/build/src/strategy/strategies/vinext-cloudflare.ts
18124
- import { join as join15 } from "path";
18284
+ import { join as join16 } from "path";
18125
18285
  var VINEXT_MIN_NEXT_MAJOR = 16;
18126
18286
  var VINEXT_BUILD_SCRIPTS = ["esbuild", "workerd"];
18127
18287
  var VINEXT_OUTPUT = {
@@ -18277,7 +18437,7 @@ function createVinextCloudflareStrategy(options) {
18277
18437
  context,
18278
18438
  {
18279
18439
  appRoot: fork.appRoot,
18280
- wranglerConfigPath: join15(
18440
+ wranglerConfigPath: join16(
18281
18441
  fork.appRoot,
18282
18442
  "dist",
18283
18443
  "server",
@@ -18302,7 +18462,7 @@ function createVinextCloudflareStrategy(options) {
18302
18462
  // ../../packages/build/src/strategy/workspace.ts
18303
18463
  import { execFile } from "child_process";
18304
18464
  import { cp, mkdir as mkdir2, readdir as readdir4, rm as rm2 } from "fs/promises";
18305
- import { basename as basename2, dirname as dirname3, join as join16, relative as relative5, resolve as resolve6 } from "path";
18465
+ import { basename as basename2, dirname as dirname3, join as join17, relative as relative5, resolve as resolve6 } from "path";
18306
18466
  import { promisify } from "util";
18307
18467
  var execFileAsync = promisify(execFile);
18308
18468
  var SKIPPED_ANYWHERE = /* @__PURE__ */ new Set(["node_modules"]);
@@ -18325,14 +18485,14 @@ async function copyInstalledPackages(source, target2) {
18325
18485
  const walk2 = async (dir) => {
18326
18486
  for (const entry of await readdir4(dir, { withFileTypes: true })) {
18327
18487
  if (!entry.isDirectory() || entry.name === ".git") continue;
18328
- const path = join16(dir, entry.name);
18488
+ const path = join17(dir, entry.name);
18329
18489
  if (entry.name === "node_modules") installs.push(path);
18330
18490
  else await walk2(path);
18331
18491
  }
18332
18492
  };
18333
18493
  await walk2(origin);
18334
18494
  for (const install of installs)
18335
- await copyTree(install, join16(target2, relative5(origin, install)));
18495
+ await copyTree(install, join17(target2, relative5(origin, install)));
18336
18496
  }
18337
18497
  async function copyTree(from, to) {
18338
18498
  await mkdir2(dirname3(to), { recursive: true });
@@ -18347,10 +18507,11 @@ async function copyTree(from, to) {
18347
18507
  await cp(from, to, { recursive: true, verbatimSymlinks: true });
18348
18508
  }
18349
18509
  function forkedAppRoot(checkout, appRoot, forkCheckout) {
18350
- return join16(forkCheckout, relative5(checkout, appRoot));
18510
+ return join17(forkCheckout, relative5(checkout, appRoot));
18351
18511
  }
18352
18512
 
18353
18513
  // ../../packages/build/src/local-build-executor.ts
18514
+ var PINNED_COMMIT_SHA = /^[0-9a-f]{40}$/;
18354
18515
  function concurrencyFromEnvironment() {
18355
18516
  const raw = Number(process.env.NARO_RESOLVER_CONCURRENCY ?? "1");
18356
18517
  return Number.isInteger(raw) && raw > 0 ? raw : 1;
@@ -18369,7 +18530,7 @@ async function withRouteList(artifact, list, appRoot, observer) {
18369
18530
  }
18370
18531
  }
18371
18532
  var defaultLocalBuildDependencies = {
18372
- createWorkspace: () => mkdtemp(join17(tmpdir(), "next-deploy-build-")),
18533
+ createWorkspace: () => mkdtemp(join18(tmpdir(), "next-deploy-build-")),
18373
18534
  removeWorkspace: (path) => rm3(path, { recursive: true, force: true }),
18374
18535
  runCommand,
18375
18536
  detectPackageManager,
@@ -18403,8 +18564,8 @@ async function readInstalledNextVersion(appRoot, checkout) {
18403
18564
  const stop = resolve7(checkout);
18404
18565
  for (; ; ) {
18405
18566
  try {
18406
- const source = await readFile12(
18407
- join17(dir, "node_modules", "next", "package.json"),
18567
+ const source = await readFile13(
18568
+ join18(dir, "node_modules", "next", "package.json"),
18408
18569
  "utf8"
18409
18570
  );
18410
18571
  const version2 = JSON.parse(source).version;
@@ -18495,10 +18656,72 @@ var LocalBuildExecutor = class {
18495
18656
  constructor(dependencies = defaultLocalBuildDependencies) {
18496
18657
  this.dependencies = dependencies;
18497
18658
  }
18659
+ /**
18660
+ * Checks out one commit, whichever ref reaches it: GitHub serves any
18661
+ * reachable commit by SHA, which `clone --branch` cannot ask for. The URL
18662
+ * carries the installation token and is only ever an argument — runCommand
18663
+ * logs a command's output (redacted), never the command line, and a failed
18664
+ * command's message names the command, not its arguments.
18665
+ *
18666
+ * The commit is an argument of `git fetch`, so it is checked before any git
18667
+ * command runs: a value git could read as an option (`--upload-pack=…`) or a
18668
+ * ref name must not get that far. The build container checks it on the way
18669
+ * in; the local runner passes claim's input straight through, so this is the
18670
+ * check that covers it.
18671
+ */
18672
+ async checkoutPinnedCommit(commitSha, repositoryUrl, workspace, checkout, observer) {
18673
+ if (!PINNED_COMMIT_SHA.test(commitSha)) {
18674
+ throw new DeploymentError(
18675
+ "CLONE_FAILED",
18676
+ "The pinned commit is not a 40-character lowercase hex SHA."
18677
+ );
18678
+ }
18679
+ const git2 = (args) => this.dependencies.runCommand(
18680
+ {
18681
+ command: "git",
18682
+ args,
18683
+ cwd: workspace,
18684
+ timeoutMs: 12e4,
18685
+ stage: "CLONING",
18686
+ errorCode: "CLONE_FAILED"
18687
+ },
18688
+ observer
18689
+ );
18690
+ await git2(["init", "--quiet", checkout]);
18691
+ await git2(["-C", checkout, "remote", "add", "origin", "--", repositoryUrl]);
18692
+ try {
18693
+ await git2([
18694
+ "-C",
18695
+ checkout,
18696
+ "fetch",
18697
+ "--quiet",
18698
+ "--depth",
18699
+ "1",
18700
+ "origin",
18701
+ commitSha
18702
+ ]);
18703
+ } catch (error51) {
18704
+ if (!(error51 instanceof CommandExecutionError)) throw error51;
18705
+ if (error51.timedOut) throw error51;
18706
+ throw new DeploymentError(
18707
+ "CLONE_FAILED",
18708
+ `Could not fetch commit ${commitSha.slice(0, 7)}. It may have been removed from the repository.`,
18709
+ { cause: error51 }
18710
+ );
18711
+ }
18712
+ await git2([
18713
+ "-C",
18714
+ checkout,
18715
+ "checkout",
18716
+ "--quiet",
18717
+ "--detach",
18718
+ "FETCH_HEAD"
18719
+ ]);
18720
+ }
18498
18721
  async build(input, observer, cache) {
18499
18722
  const workspace = await this.dependencies.createWorkspace();
18500
- const checkout = join17(workspace, "checkout");
18501
- const pmCacheDir = join17(workspace, ".pm-store");
18723
+ const checkout = join18(workspace, "checkout");
18724
+ const pmCacheDir = join18(workspace, ".pm-store");
18502
18725
  let cacheSeq = 0;
18503
18726
  const commandAvailable = this.dependencies.commandAvailable ?? hasCommand;
18504
18727
  const compressor = "zstd";
@@ -18507,7 +18730,7 @@ var LocalBuildExecutor = class {
18507
18730
  transport: cache,
18508
18731
  fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
18509
18732
  runCommand: this.dependencies.runCommand,
18510
- tmpFile: (suffix) => join17(workspace, `cache-${String(cacheSeq++)}${suffix}`),
18733
+ tmpFile: (suffix) => join18(workspace, `cache-${String(cacheSeq++)}${suffix}`),
18511
18734
  // One latch for the whole build so a disabled cache warns once.
18512
18735
  report: { unavailable: false }
18513
18736
  } : null;
@@ -18520,27 +18743,37 @@ var LocalBuildExecutor = class {
18520
18743
  message: `Build cache disabled: ${compressor} is not installed. Install it to reuse dependencies between deploys.`
18521
18744
  });
18522
18745
  }
18523
- await this.dependencies.runCommand(
18524
- {
18525
- command: "git",
18526
- args: [
18527
- "clone",
18528
- "--depth",
18529
- "1",
18530
- "--branch",
18531
- input.branch,
18532
- "--single-branch",
18533
- "--",
18534
- input.repositoryUrl,
18535
- checkout
18536
- ],
18537
- cwd: workspace,
18538
- timeoutMs: 12e4,
18539
- stage: "CLONING",
18540
- errorCode: "CLONE_FAILED"
18541
- },
18542
- observer
18543
- );
18746
+ if (input.commitSha !== void 0) {
18747
+ await this.checkoutPinnedCommit(
18748
+ input.commitSha,
18749
+ input.repositoryUrl,
18750
+ workspace,
18751
+ checkout,
18752
+ observer
18753
+ );
18754
+ } else {
18755
+ await this.dependencies.runCommand(
18756
+ {
18757
+ command: "git",
18758
+ args: [
18759
+ "clone",
18760
+ "--depth",
18761
+ "1",
18762
+ "--branch",
18763
+ input.branch,
18764
+ "--single-branch",
18765
+ "--",
18766
+ input.repositoryUrl,
18767
+ checkout
18768
+ ],
18769
+ cwd: workspace,
18770
+ timeoutMs: 12e4,
18771
+ stage: "CLONING",
18772
+ errorCode: "CLONE_FAILED"
18773
+ },
18774
+ observer
18775
+ );
18776
+ }
18544
18777
  const revision = await this.dependencies.runCommand(
18545
18778
  {
18546
18779
  command: "git",
@@ -18559,6 +18792,12 @@ var LocalBuildExecutor = class {
18559
18792
  "Git returned an invalid commit SHA."
18560
18793
  );
18561
18794
  }
18795
+ if (input.commitSha !== void 0 && gitCommitSha !== input.commitSha) {
18796
+ throw new DeploymentError(
18797
+ "CLONE_FAILED",
18798
+ `Checked out ${gitCommitSha.slice(0, 7)} instead of the pinned commit ${input.commitSha.slice(0, 7)}.`
18799
+ );
18800
+ }
18562
18801
  const reportedSha = gitCommitSha.length === 40 ? gitCommitSha : null;
18563
18802
  if (!reportedSha)
18564
18803
  await info2(
@@ -18586,6 +18825,7 @@ var LocalBuildExecutor = class {
18586
18825
  checkout,
18587
18826
  input.rootDirectory
18588
18827
  );
18828
+ await assertAppRootInsideCheckout(checkout, appRoot);
18589
18829
  const stacks = await import("./stack-PDV3NPWC.js");
18590
18830
  const stack = await (this.dependencies.detectStack ?? stacks.detectStack)(
18591
18831
  appRoot
@@ -18698,7 +18938,7 @@ var LocalBuildExecutor = class {
18698
18938
  }
18699
18939
  }
18700
18940
  await notifyStatus(observer, "BUILDING");
18701
- const buildCacheDir = resolved.preset.cachePath ? join17(appRoot, resolved.preset.cachePath) : null;
18941
+ const buildCacheDir = resolved.preset.cachePath ? join18(appRoot, resolved.preset.cachePath) : null;
18702
18942
  const buildCacheKey = sanitizeCacheKey(input.branch);
18703
18943
  if (cachePorts && buildCacheDir) {
18704
18944
  await tryRestore(
@@ -18722,13 +18962,13 @@ var LocalBuildExecutor = class {
18722
18962
  entries: fingerprintEntries,
18723
18963
  nextVersion: nextVersion ? `${String(nextVersion.major)}.${String(nextVersion.minor)}.${String(nextVersion.patch)}` : "unknown"
18724
18964
  });
18725
- const pristine = join17(workspace, "pristine");
18965
+ const pristine = join18(workspace, "pristine");
18726
18966
  const fork = this.dependencies.forkWorkspace ?? defaultFork;
18727
18967
  await fork(checkout, pristine);
18728
18968
  let forks = 0;
18729
18969
  const resolverConcurrency = this.dependencies.resolverConcurrency ?? 1;
18730
18970
  const forkWorkspace = async (label2) => {
18731
- const target2 = join17(
18971
+ const target2 = join18(
18732
18972
  workspace,
18733
18973
  "forks",
18734
18974
  `${String(forks++)}-${label2.replace(/[^a-z0-9-]/gi, "-")}`
@@ -18973,6 +19213,14 @@ var NaroError = class extends Error {
18973
19213
  if (options?.details !== void 0) this.details = options.details;
18974
19214
  }
18975
19215
  };
19216
+ var PROMOTE_TARGET_UNSUPPORTED_MESSAGE = "This control plane cannot promote to an environment yet. Deploy the latest web first, then try again.";
19217
+ function promoteTargetUnsupported(cause) {
19218
+ return new NaroError(
19219
+ "PROMOTE_TARGET_UNSUPPORTED",
19220
+ PROMOTE_TARGET_UNSUPPORTED_MESSAGE,
19221
+ cause !== void 0 ? { cause } : void 0
19222
+ );
19223
+ }
18976
19224
  function isNaroError(error51) {
18977
19225
  return error51 instanceof NaroError || error51 instanceof Error && error51.name === "NaroError" && typeof error51.code === "string";
18978
19226
  }
@@ -19016,12 +19264,12 @@ async function errorFromResponse(response, context) {
19016
19264
  // ../../packages/sdk/src/session.ts
19017
19265
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
19018
19266
  import { homedir } from "os";
19019
- import { join as join18 } from "path";
19267
+ import { join as join19 } from "path";
19020
19268
  function credentialsDir(home = homedir()) {
19021
- return join18(home, ".naro");
19269
+ return join19(home, ".naro");
19022
19270
  }
19023
19271
  function credentialsFile(home = homedir()) {
19024
- return join18(credentialsDir(home), "credentials.json");
19272
+ return join19(credentialsDir(home), "credentials.json");
19025
19273
  }
19026
19274
  function saveSession(session, home = homedir()) {
19027
19275
  mkdirSync(credentialsDir(home), { recursive: true, mode: 448 });
@@ -19078,7 +19326,7 @@ var STYLES = `
19078
19326
  *{box-sizing:border-box}
19079
19327
  :root{
19080
19328
  --bg:#000;--surface:#0a0a0a;--surface-2:#111;--border:#232323;
19081
- --text:#ededed;--muted:#a1a1a1;--faint:#6f6f6f;
19329
+ --text:#ededed;--muted:#a1a1a1;--faint:#808080;
19082
19330
  --sans:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
19083
19331
  --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
19084
19332
  }
@@ -19542,6 +19790,8 @@ function deploymentInfo(row) {
19542
19790
  errorMessage: row.error_message,
19543
19791
  gitCommitSha: row.git_commit_sha,
19544
19792
  gitCommitMessage: row.git_commit_message,
19793
+ pinnedCommitSha: row.pinned_commit_sha ?? null,
19794
+ rebuiltFromDeploymentId: row.rebuilt_from_deployment_id ?? null,
19545
19795
  createdAt: row.created_at,
19546
19796
  startedAt: row.started_at,
19547
19797
  finishedAt: row.finished_at
@@ -19793,16 +20043,65 @@ var DeploymentsApi = class {
19793
20043
  return { current, target: production.slice(1).find(differs) ?? null };
19794
20044
  }
19795
20045
  /**
19796
- * Redeploys an existing READY deployment's artifact to production.
19797
- * Used by both `naro promote` (preview → production) and `naro rollback`
19798
- * (older production build → production).
20046
+ * Promotes an existing READY deployment: with no `target` (or
20047
+ * `production`) it goes to production — `naro promote` (preview →
20048
+ * production) and `naro rollback` (older production build → production).
20049
+ * A deployment from an environment is rebuilt from its commit with
20050
+ * production's variables (`mode: "rebuild"`); any other is published as
20051
+ * stored (`mode: "reupload"`). With an environment `target` the control
20052
+ * plane likewise rebuilds the deployment's commit with that environment's
20053
+ * variables or publishes the stored build.
20054
+ *
20055
+ * No `target` sends no body, exactly as clients before targets did. A
20056
+ * control plane from before targets ignores `target` and promotes to
20057
+ * production, so an environment target is first checked with
20058
+ * `promoteCheck`: a 405 there (no such route) stops with
20059
+ * `PROMOTE_TARGET_UNSUPPORTED` before anything is promoted. The target is
20060
+ * trimmed once, as the control plane trims it, so `" production"` is
20061
+ * production and asks no preview.
20062
+ */
20063
+ async promote(projectId, deploymentId, options = {}) {
20064
+ const target2 = options.target?.trim();
20065
+ if (target2 !== void 0 && target2 !== "production") {
20066
+ try {
20067
+ await this.promoteCheck(projectId, deploymentId, target2);
20068
+ } catch (error51) {
20069
+ if (isNaroError(error51) && error51.status === 405) {
20070
+ throw promoteTargetUnsupported(error51);
20071
+ }
20072
+ throw error51;
20073
+ }
20074
+ }
20075
+ const body = await this.client.request(this.promotePath(projectId, deploymentId), {
20076
+ method: "POST",
20077
+ ...target2 !== void 0 ? { body: { target: target2 } } : {},
20078
+ context: "Promote deployment"
20079
+ });
20080
+ return {
20081
+ ...deploymentInfo(body.deployment),
20082
+ mode: body.mode ?? "reupload"
20083
+ };
20084
+ }
20085
+ /**
20086
+ * What promoting to `target` would do, changing nothing: how it gets its
20087
+ * build, the migrations the target's database has not applied (null when
20088
+ * they cannot be compared) and the warnings to show first. Answers 405 from
20089
+ * a control plane that predates promote targets.
19799
20090
  */
19800
- async promote(projectId, deploymentId) {
20091
+ async promoteCheck(projectId, deploymentId, target2) {
20092
+ const query = target2 !== void 0 ? `?target=${encodeURIComponent(target2)}` : "";
19801
20093
  const body = await this.client.request(
19802
- `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`,
19803
- { method: "POST", context: "Promote deployment" }
20094
+ `${this.promotePath(projectId, deploymentId)}${query}`,
20095
+ { context: "Check promotion" }
19804
20096
  );
19805
- return deploymentInfo(body.deployment);
20097
+ return {
20098
+ mode: body.mode ?? "reupload",
20099
+ pendingMigrations: body.pendingMigrations ?? null,
20100
+ warnings: body.warnings ?? []
20101
+ };
20102
+ }
20103
+ promotePath(projectId, deploymentId) {
20104
+ return `/api/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/promote`;
19806
20105
  }
19807
20106
  /**
19808
20107
  * Stops an in-flight build (QUEUED through BUILDING) and marks the row
@@ -19848,6 +20147,8 @@ var DeploymentsApi = class {
19848
20147
  }
19849
20148
  }
19850
20149
  };
20150
+ var STATIC_ASSETS_BINDING = "ASSETS";
20151
+ var MANAGED_DATABASE_BINDING = "DB";
19851
20152
  var EnvApi = class {
19852
20153
  constructor(client) {
19853
20154
  this.client = client;
@@ -19877,6 +20178,37 @@ var EnvApi = class {
19877
20178
  }
19878
20179
  );
19879
20180
  }
20181
+ /**
20182
+ * The names the env route refuses as a key — `409 VALIDATION_FAILED`
20183
+ * `<KEY> is already used by a storage binding.` — read the way it reads
20184
+ * them: `ASSETS` (the static files), every binding of the project's storage
20185
+ * and, on a project using the managed database (`database_provider` is
20186
+ * `d1`), `DB`, which may not be a storage row yet. For a caller that sends
20187
+ * many entries in several requests (`naro env import`) and has to refuse
20188
+ * before the first one is saved. A control plane whose project row has no
20189
+ * `database_provider` reads as having no managed database, so `DB` is only
20190
+ * left out there.
20191
+ */
20192
+ async takenBindingNames(projectId) {
20193
+ const base2 = `/api/projects/${encodeURIComponent(projectId)}`;
20194
+ const [storage, project] = await Promise.all([
20195
+ this.client.request(
20196
+ `${base2}/storage`,
20197
+ { context: "List storage" }
20198
+ ),
20199
+ this.client.request(
20200
+ base2,
20201
+ { context: "Read project" }
20202
+ )
20203
+ ]);
20204
+ const names = /* @__PURE__ */ new Set([
20205
+ STATIC_ASSETS_BINDING,
20206
+ ...storage.storage.map((row) => row.bindingName)
20207
+ ]);
20208
+ if (project.project.database_provider === "d1")
20209
+ names.add(MANAGED_DATABASE_BINDING);
20210
+ return [...names];
20211
+ }
19880
20212
  async removeById(projectId, envVarId) {
19881
20213
  await this.client.request(
19882
20214
  `/api/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(envVarId)}`,
@@ -19887,13 +20219,28 @@ var EnvApi = class {
19887
20219
  * Removes a key's rows: the one `target` names, or, with no target, the
19888
20220
  * `production`, `preview` and `all` rows. An environment's value goes only
19889
20221
  * when its slug is the target, so removing a key everywhere else does not
19890
- * take a QA or staging value with it.
20222
+ * take a QA or staging value with it. A key only environments hold fails
20223
+ * ENV_NOT_FOUND naming them (`details.environments`), not as if there were
20224
+ * none.
19891
20225
  */
19892
20226
  async remove(projectId, key, target2) {
19893
20227
  const rows = await this.list(projectId);
19894
20228
  const matches = rows.filter(
19895
20229
  (row) => row.key === key && (target2 === void 0 ? isReservedEnvTarget(row.target) : row.target === target2)
19896
20230
  );
20231
+ if (matches.length === 0 && target2 === void 0) {
20232
+ const environments = [
20233
+ ...new Set(
20234
+ rows.filter((row) => row.key === key).map((row) => row.target).filter((slug) => !isReservedEnvTarget(slug))
20235
+ )
20236
+ ].toSorted();
20237
+ if (environments.length > 0)
20238
+ throw new NaroError(
20239
+ "ENV_NOT_FOUND",
20240
+ `No ${key} variable found outside named environments. Pass the environment to remove it: ${environments.join(", ")}.`,
20241
+ { details: { key, environments } }
20242
+ );
20243
+ }
19897
20244
  if (matches.length === 0) {
19898
20245
  throw new NaroError(
19899
20246
  "ENV_NOT_FOUND",
@@ -20509,6 +20856,7 @@ function storageInfo(body) {
20509
20856
  return {
20510
20857
  enabled: body.enabled,
20511
20858
  bucket: body.bucket ?? null,
20859
+ disabledAt: body.disabledAt ?? null,
20512
20860
  limits: {
20513
20861
  bytes: body.limits.bytes,
20514
20862
  objectBytes: body.limits.objectBytes,
@@ -20520,6 +20868,18 @@ function storageInfo(body) {
20520
20868
  buckets: (body.buckets ?? []).map(storageBucket)
20521
20869
  };
20522
20870
  }
20871
+ function storageRunFailure(error51) {
20872
+ const details = isNaroError(error51) ? error51.details : void 0;
20873
+ if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
20874
+ return new NaroError(error51.code, error51.message, {
20875
+ ...error51.status !== void 0 ? { status: error51.status } : {},
20876
+ details: {
20877
+ ...details,
20878
+ backend: backendInfo(details.backend)
20879
+ }
20880
+ });
20881
+ return error51;
20882
+ }
20523
20883
  function bucketFields(fields, into = {}) {
20524
20884
  for (const key of ["public", "fileSizeLimit", "allowedMimeTypes"])
20525
20885
  if (fields[key] !== void 0) into[key] = fields[key];
@@ -20948,16 +21308,45 @@ var StorageApi = class {
20948
21308
  }
20949
21309
  );
20950
21310
  } catch (error51) {
20951
- const details = isNaroError(error51) ? error51.details : void 0;
20952
- if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
20953
- throw new NaroError(error51.code, error51.message, {
20954
- ...error51.status !== void 0 ? { status: error51.status } : {},
20955
- details: {
20956
- ...details,
20957
- backend: backendInfo(details.backend)
20958
- }
20959
- });
20960
- throw error51;
21311
+ throw storageRunFailure(error51);
21312
+ }
21313
+ return storageInfo(body.storage);
21314
+ }
21315
+ /**
21316
+ * Turns Storage off and keeps its files (Storage 3-1k): the Naro worker is
21317
+ * uploaded again without the bucket, so the app's Storage calls and file
21318
+ * URLs answer `404 STORAGE_NOT_ENABLED` until `enable` binds the same
21319
+ * bucket again. The bucket, its files and their storage cost stay; while
21320
+ * it is off, bucket writes and the object routes answer `409
21321
+ * STORAGE_OFF`, and reads go on. Safe to repeat: Storage never on, or
21322
+ * already off, comes back as it is, and a turn-off that stopped is
21323
+ * finished. Two minutes, like `enable`.
21324
+ *
21325
+ * Refusals, in the order they are checked: `BACKEND_DISABLED`,
21326
+ * `FORBIDDEN` (`storage.manage`), `DATABASE_NOT_ENABLED`,
21327
+ * `BACKEND_NOT_READY` (no backend), `BACKEND_OFF`, `BACKEND_BUSY` (another
21328
+ * change holds the backend — or turning Storage on again took this
21329
+ * turn-off over, and Storage is on), `STORAGE_DISABLE_FAILED` (502: the
21330
+ * turn-off could not be recorded; nothing changed), and
21331
+ * `BACKEND_PROVISION_FAILED` (502) when it is
21332
+ * recorded but the worker's upload — or a step after it — stopped:
21333
+ * `details` holds `{ storage: { applied: false, enabled: false }, backend
21334
+ * }`, and `disable` again finishes. Never `STORAGE_DISABLED`: the control
21335
+ * plane's Storage switch does not hold a removal back.
21336
+ */
21337
+ async disable(projectId) {
21338
+ let body;
21339
+ try {
21340
+ body = await this.client.request(
21341
+ this.path(projectId),
21342
+ {
21343
+ method: "DELETE",
21344
+ context: "Turn off Storage",
21345
+ timeoutMs: PROVISION_TIMEOUT_MS
21346
+ }
21347
+ );
21348
+ } catch (error51) {
21349
+ throw storageRunFailure(error51);
20961
21350
  }
20962
21351
  return storageInfo(body.storage);
20963
21352
  }
@@ -21167,40 +21556,92 @@ function summarizeStatement(statement2) {
21167
21556
 
21168
21557
  // ../../packages/sdk/src/dotenv.ts
21169
21558
  import { existsSync, readFileSync as readFileSync2 } from "fs";
21170
- import { join as join19 } from "path";
21559
+ import { join as join20 } from "path";
21560
+ var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/dgm;
21561
+ var QUOTES = /* @__PURE__ */ new Set(["'", '"', "`"]);
21562
+ function parseDotenvDetailed(text2) {
21563
+ const vars = {};
21564
+ const unclosedQuotes = [];
21565
+ const lines = text2.replace(/\r\n?/g, "\n");
21566
+ let line = 1;
21567
+ let counted = 0;
21568
+ for (const match of lines.matchAll(LINE)) {
21569
+ const key = match[1];
21570
+ const raw = match[2] ?? "";
21571
+ let value = raw.trim();
21572
+ const maybeQuote = value[0];
21573
+ if (maybeQuote !== void 0 && QUOTES.has(maybeQuote) && !value.includes(maybeQuote, 1)) {
21574
+ const at = (match.indices?.[2]?.[0] ?? match.index) + (raw.length - raw.trimStart().length);
21575
+ for (; counted < at; counted += 1) if (lines[counted] === "\n") line += 1;
21576
+ unclosedQuotes.push(line);
21577
+ }
21578
+ value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
21579
+ if (maybeQuote === '"')
21580
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r");
21581
+ vars[key] = value;
21582
+ }
21583
+ return { vars, unclosedQuotes };
21584
+ }
21171
21585
  function parseDotenv(text2) {
21172
- const out = {};
21173
- for (const line of text2.split(/\r?\n/)) {
21174
- const trimmed = line.trim().replace(/^export\s+(?=[^=\s])/, "");
21175
- if (!trimmed || trimmed.startsWith("#")) continue;
21176
- const eq = trimmed.indexOf("=");
21177
- if (eq <= 0) continue;
21178
- const key = trimmed.slice(0, eq).trim();
21179
- let value = trimmed.slice(eq + 1).trim();
21180
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
21181
- value = value.slice(1, -1);
21182
- out[key] = value;
21183
- }
21184
- return out;
21586
+ return parseDotenvDetailed(text2).vars;
21185
21587
  }
21186
21588
  function readProjectEnv(dir) {
21187
21589
  for (const name of [".env.local", ".env.production", ".env"]) {
21188
- const path = join19(dir, name);
21590
+ const path = join20(dir, name);
21189
21591
  if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
21190
21592
  }
21191
21593
  return {};
21192
21594
  }
21595
+ var PLAIN = /^[^\s"'#$`\\]*$/;
21596
+ function bareReadsBack(value) {
21597
+ return !/[#\r\n\u2028\u2029]/.test(value) && value.trim() === value && !QUOTES.has(value[0] ?? "");
21598
+ }
21599
+ function dotenvValue(value) {
21600
+ if (PLAIN.test(value)) return value;
21601
+ const endsInBackslash = value.endsWith("\\");
21602
+ if (!value.includes("\r") && !endsInBackslash) {
21603
+ if (!value.includes("'")) return `'${value}'`;
21604
+ if (!value.includes("`")) return `\`${value}\``;
21605
+ }
21606
+ if (!value.includes('"') && !/\\[nr]/.test(value) && !endsInBackslash)
21607
+ return `"${value.replaceAll("\r", "\\r")}"`;
21608
+ if (bareReadsBack(value)) return value;
21609
+ return void 0;
21610
+ }
21611
+ var UNWRITABLE_HINT = "Store them without carriage returns or one of the quote characters (for a file: tr -d '\\r' < sa.json | naro env add KEY), or read them with naro env list --reveal.";
21193
21612
  function serializeDotenv(vars) {
21194
- const needsQuotes = /[\s"'#$`\\]/;
21195
- return `${Object.entries(vars).map(
21196
- ([key, value]) => needsQuotes.test(value) ? `${key}="${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` : `${key}=${value}`
21197
- ).join("\n")}
21613
+ const lines = [];
21614
+ const allQuotes = [];
21615
+ const other = [];
21616
+ for (const [key, value] of Object.entries(vars)) {
21617
+ const written = dotenvValue(value);
21618
+ if (written !== void 0) lines.push(`${key}=${written}`);
21619
+ else if (value.includes("'") && value.includes('"') && value.includes("`"))
21620
+ allQuotes.push(key);
21621
+ else other.push(key);
21622
+ }
21623
+ const problems2 = [];
21624
+ if (allQuotes.length > 0)
21625
+ problems2.push(
21626
+ `Cannot write ${allQuotes.join(", ")} to a dotenv file: their values hold ', " and \` together.`
21627
+ );
21628
+ if (other.length > 0)
21629
+ problems2.push(
21630
+ `Cannot write ${other.join(", ")} to a dotenv file: dotenv would not read their values back unchanged.`
21631
+ );
21632
+ if (problems2.length > 0)
21633
+ throw new NaroError(
21634
+ "VALIDATION_FAILED",
21635
+ `${problems2.join(" ")} ${UNWRITABLE_HINT}`,
21636
+ { details: { keys: [...allQuotes, ...other] } }
21637
+ );
21638
+ return `${lines.join("\n")}
21198
21639
  `;
21199
21640
  }
21200
21641
 
21201
21642
  // ../../packages/sdk/src/env-scan.ts
21202
21643
  import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
21203
- import { join as join20 } from "path";
21644
+ import { join as join21 } from "path";
21204
21645
  var SOURCE_EXTENSIONS3 = /* @__PURE__ */ new Set([
21205
21646
  ".ts",
21206
21647
  ".tsx",
@@ -21255,7 +21696,7 @@ function collect(dir, out) {
21255
21696
  for (const entry of entries) {
21256
21697
  if (out.length >= MAX_FILES) return;
21257
21698
  if (entry.isSymbolicLink()) continue;
21258
- const path = join20(dir, entry.name);
21699
+ const path = join21(dir, entry.name);
21259
21700
  if (entry.isDirectory()) {
21260
21701
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name))
21261
21702
  continue;
@@ -21287,7 +21728,7 @@ function scanEnvReferences(dir) {
21287
21728
  }
21288
21729
  }
21289
21730
  for (const name of EXAMPLE_FILES) {
21290
- const path = join20(dir, name);
21731
+ const path = join21(dir, name);
21291
21732
  if (!existsSync2(path)) continue;
21292
21733
  let parsed;
21293
21734
  try {
@@ -21305,11 +21746,11 @@ function scanEnvReferences(dir) {
21305
21746
 
21306
21747
  // ../../packages/sdk/src/framework.ts
21307
21748
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
21308
- import { join as join21 } from "path";
21749
+ import { join as join22 } from "path";
21309
21750
  function readPackageJson2(dir) {
21310
21751
  try {
21311
21752
  return JSON.parse(
21312
- readFileSync4(join21(dir, "package.json"), "utf8")
21753
+ readFileSync4(join22(dir, "package.json"), "utf8")
21313
21754
  );
21314
21755
  } catch {
21315
21756
  return null;
@@ -21320,7 +21761,7 @@ function hasDependency(pkg, name) {
21320
21761
  }
21321
21762
  function hasConfig(dir, base2, evidence) {
21322
21763
  for (const ext of ["js", "mjs", "cjs", "ts", "mts"]) {
21323
- if (existsSync3(join21(dir, `${base2}.${ext}`))) {
21764
+ if (existsSync3(join22(dir, `${base2}.${ext}`))) {
21324
21765
  evidence.push(`${base2}.${ext}`);
21325
21766
  return true;
21326
21767
  }
@@ -21428,8 +21869,8 @@ function detectFramework2(dir) {
21428
21869
  if (!byDependency && !byConfig) continue;
21429
21870
  if (byDependency) evidence.push(`package.json: ${detector.dependency}`);
21430
21871
  if (detector.framework === "nextjs") {
21431
- if (existsSync3(join21(dir, "app"))) evidence.push("app/");
21432
- if (existsSync3(join21(dir, "pages"))) evidence.push("pages/");
21872
+ if (existsSync3(join22(dir, "app"))) evidence.push("app/");
21873
+ if (existsSync3(join22(dir, "pages"))) evidence.push("pages/");
21433
21874
  }
21434
21875
  return {
21435
21876
  framework: detector.framework,
@@ -21440,7 +21881,7 @@ function detectFramework2(dir) {
21440
21881
  deployable: true
21441
21882
  };
21442
21883
  }
21443
- if (!pkg && existsSync3(join21(dir, "index.html"))) {
21884
+ if (!pkg && existsSync3(join22(dir, "index.html"))) {
21444
21885
  evidence.push("index.html (no package.json)");
21445
21886
  return {
21446
21887
  framework: "static",
@@ -21461,8 +21902,26 @@ function detectFramework2(dir) {
21461
21902
  };
21462
21903
  }
21463
21904
 
21905
+ // ../../packages/data-core/src/unicode-controls.ts
21906
+ function hasControlCharacter(text2) {
21907
+ for (let i = 0; i < text2.length; i += 1) {
21908
+ const code = text2.charCodeAt(i);
21909
+ if (code <= 31 || code >= 127 && code <= 159) return true;
21910
+ }
21911
+ return false;
21912
+ }
21913
+ function hasBidiControl(text2) {
21914
+ for (let i = 0; i < text2.length; i += 1) {
21915
+ const code = text2.charCodeAt(i);
21916
+ if (code === 1564 || code === 8206 || code === 8207 || code >= 8234 && code <= 8238 || code >= 8294 && code <= 8297)
21917
+ return true;
21918
+ }
21919
+ return false;
21920
+ }
21921
+
21464
21922
  // ../../packages/data-core/src/storage-path.ts
21465
21923
  var MAX_SINGLE_UPLOAD_BYTES = 52428800;
21924
+ var MAX_OBJECT_NAME_BYTES = 900;
21466
21925
  var STORAGE_RESERVED_WORDS = [
21467
21926
  "authenticated",
21468
21927
  "copy",
@@ -21478,6 +21937,32 @@ var RESERVED = new Set(STORAGE_RESERVED_WORDS);
21478
21937
  var ENCODER = new TextEncoder();
21479
21938
  var LONE_SURROGATE2 = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
21480
21939
  var LONE_SURROGATES = new RegExp(LONE_SURROGATE2.source, "g");
21940
+ function hasLoneSurrogate(text2) {
21941
+ return LONE_SURROGATE2.test(text2);
21942
+ }
21943
+ function refuse(reason) {
21944
+ return { ok: false, reason };
21945
+ }
21946
+ function checkWellFormed(name) {
21947
+ if (name === "") return refuse("is empty");
21948
+ if (name.length > MAX_OBJECT_NAME_BYTES || ENCODER.encode(name).length > MAX_OBJECT_NAME_BYTES)
21949
+ return refuse(`is longer than ${String(MAX_OBJECT_NAME_BYTES)} bytes`);
21950
+ if (name.startsWith("/") || name.endsWith("/"))
21951
+ return refuse("starts or ends with /");
21952
+ if (name.includes("//")) return refuse("has an empty segment (//)");
21953
+ for (const segment of name.split("/"))
21954
+ if (segment === "." || segment === "..")
21955
+ return refuse("has a . or .. segment");
21956
+ if (hasControlCharacter(name)) return refuse("has a control character");
21957
+ if (name.includes("\\")) return refuse("has a backslash");
21958
+ if (hasBidiControl(name))
21959
+ return refuse("has a bidirectional control character");
21960
+ return { ok: true, name };
21961
+ }
21962
+ function checkObjectName(name) {
21963
+ if (hasLoneSurrogate(name)) return refuse("is not valid UTF-8");
21964
+ return checkWellFormed(name);
21965
+ }
21481
21966
  function objectParent(name) {
21482
21967
  const slash = name.lastIndexOf("/");
21483
21968
  return slash < 0 ? "" : name.slice(0, slash);
@@ -21507,6 +21992,7 @@ var STORAGE_OBJECTS = "storage.objects";
21507
21992
  var STORAGE_OBJECTS_TABLE = "_naro_storage_objects";
21508
21993
  var STORAGE_OBJECTS_QUALIFIER = "objects";
21509
21994
  var STORAGE_UNAVAILABLE_MESSAGE = "storage.objects is not available on this backend yet. Run `naro backend enable` to update it.";
21995
+ var STORAGE_UNAVAILABLE_REASON = `${STORAGE_OBJECTS} is not available on this backend yet`;
21510
21996
  var STORAGE_BUCKETS_TABLE = "_naro_storage_buckets";
21511
21997
  var STORAGE_POLICY_COLUMNS = [
21512
21998
  "id",
@@ -21674,7 +22160,8 @@ var CATALOG = {
21674
22160
  policy,
21675
22161
  recursion,
21676
22162
  values,
21677
- bytes
22163
+ bytes,
22164
+ depth
21678
22165
  }) => {
21679
22166
  if (recursion)
21680
22167
  return `Infinite recursion detected in policy for relation ${table}.`;
@@ -21682,6 +22169,8 @@ var CATALOG = {
21682
22169
  return `The policies of ${table} bind more than ${values === true ? "50" : String(values)} values in one statement.`;
21683
22170
  if (bytes)
21684
22171
  return `The policies of ${table} write more than ${bytes === true ? "50000" : String(bytes)} bytes of SQL into one statement.`;
22172
+ if (depth)
22173
+ return `The policies of ${table} nest an expression more than 100 deep into one statement.`;
21685
22174
  if (policy !== void 0)
21686
22175
  return `Policy ${policy} on ${table} cannot be applied.`;
21687
22176
  return condition ? `The ${action} policy of ${table} has a condition that cannot be applied.` : `The ${action} policy of ${table} is owner but has no valid owner column.`;
@@ -21739,7 +22228,7 @@ var CATALOG = {
21739
22228
  },
21740
22229
  OBJECT_TOO_LARGE: {
21741
22230
  status: 413,
21742
- message: (vars) => "single" in vars ? "Uploads over 50 MiB go in parts: use the multipart upload." : `The object is ${String(vars.size)} bytes; bucket ${storageName(vars.bucket)} takes objects up to ${String(vars.max)} bytes.`
22231
+ message: (vars) => "single" in vars ? "Uploads over 50 MiB go in parts: use the multipart upload." : "transfer" in vars ? "Objects over 5,363,466,240 bytes (5 GiB \u2212 5 MiB) cannot be copied or moved." : `The object is ${String(vars.size)} bytes; bucket ${storageName(vars.bucket)} takes objects up to ${String(vars.max)} bytes.`
21743
22232
  },
21744
22233
  MIME_TYPE_NOT_ALLOWED: {
21745
22234
  status: 415,
@@ -21751,7 +22240,7 @@ var CATALOG = {
21751
22240
  },
21752
22241
  OBJECT_EXISTS: {
21753
22242
  status: 409,
21754
- message: ({ bucket, path }) => `Object ${storageName(path)} already exists in bucket ${storageName(bucket)}. Upload with upsert to replace it.`
22243
+ message: ({ bucket, path, move }) => `Object ${storageName(path)} already exists in bucket ${storageName(bucket)}. ${move === true ? "A move never replaces an object: remove it or choose another name." : "Upload with upsert to replace it."}`
21755
22244
  },
21756
22245
  OBJECT_BUSY: {
21757
22246
  status: 409,
@@ -22429,6 +22918,15 @@ function resolvePolicyTable(snapshot, name) {
22429
22918
  if (folded === REALTIME_MESSAGES) return REALTIME_MESSAGES_TABLE;
22430
22919
  return resolveTable(snapshot, name);
22431
22920
  }
22921
+ function resolveSubqueryTable(snapshot, name) {
22922
+ if (asciiLower(name) === STORAGE_OBJECTS)
22923
+ return snapshot.storageSubqueries === false ? void 0 : snapshot.storage;
22924
+ return resolveTable(snapshot, name);
22925
+ }
22926
+ function subqueryName(subquery) {
22927
+ if (subquery.alias !== null) return subquery.alias;
22928
+ return asciiLower(subquery.table) === STORAGE_OBJECTS ? STORAGE_OBJECTS_QUALIFIER : subquery.table;
22929
+ }
22432
22930
  async function readTableNames(db) {
22433
22931
  return tableNamesFromRows(rowsOf(await db.prepare(TABLE_NAMES_SQL).all()));
22434
22932
  }
@@ -22470,6 +22968,14 @@ function utf8ByteLength(text2) {
22470
22968
  function quoteIdentifier(name) {
22471
22969
  return `"${name.replaceAll('"', '""')}"`;
22472
22970
  }
22971
+ var SIDE_BY_SIDE = 8;
22972
+ function joinConditions(terms, op) {
22973
+ if (terms.length <= SIDE_BY_SIDE) return `(${terms.join(` ${op} `)})`;
22974
+ const groups = [];
22975
+ for (let at = 0; at < terms.length; at += SIDE_BY_SIDE)
22976
+ groups.push(joinConditions(terms.slice(at, at + SIDE_BY_SIDE), op));
22977
+ return joinConditions(groups, op);
22978
+ }
22473
22979
  var Failure = class extends Error {
22474
22980
  constructor(error51, reason) {
22475
22981
  super(error51.code);
@@ -22653,6 +23159,12 @@ var VALUE_KINDS = /* @__PURE__ */ new Set([
22653
23159
  function isValueExpr(expr) {
22654
23160
  return VALUE_KINDS.has(expr.kind);
22655
23161
  }
23162
+ function comparedValue(value) {
23163
+ let current = value;
23164
+ while (current.kind === "func" && current.name === "coalesce" && current.args.length === 1 && current.args[0] !== void 0)
23165
+ current = current.args[0];
23166
+ return current;
23167
+ }
22656
23168
  function isConditionExpr(expr) {
22657
23169
  if (!isValueExpr(expr)) return true;
22658
23170
  if (expr.kind === "column") return true;
@@ -22704,7 +23216,6 @@ var SPECIAL_WORDS = /* @__PURE__ */ new Set([
22704
23216
  "auth"
22705
23217
  ]);
22706
23218
  var TEXT_TYPES = /* @__PURE__ */ new Set(["text", "varchar", "uuid"]);
22707
- var STORAGE_SUBQUERY_PROBLEM = "A policy cannot read storage.objects in a subquery yet";
22708
23219
  var FOLDERNAME_INDEX_PROBLEM = `storage.foldername(name) is available only as (storage.foldername(name))[n] with n from 1 to ${String(MAX_FOLDERNAME_INDEX)}`;
22709
23220
  function storageHelperColumnProblem(fn) {
22710
23221
  return `storage.${fn}() takes the name column of storage.objects`;
@@ -23155,8 +23666,12 @@ function atRealtimeMessages(stream) {
23155
23666
  }
23156
23667
  function readTableName(stream) {
23157
23668
  const first = stream.peek();
23158
- if (atStorageObjects(stream))
23159
- throw new PolicySyntaxError(STORAGE_SUBQUERY_PROBLEM, first.position);
23669
+ if (atStorageObjects(stream)) {
23670
+ stream.next();
23671
+ stream.next();
23672
+ stream.next();
23673
+ return STORAGE_OBJECTS;
23674
+ }
23160
23675
  if (atRealtimeMessages(stream))
23161
23676
  throw new PolicySyntaxError(REALTIME_SUBQUERY_PROBLEM, first.position);
23162
23677
  let token = first;
@@ -23183,12 +23698,6 @@ function readTableName(stream) {
23183
23698
  return name;
23184
23699
  }
23185
23700
  function readPolicyTarget(stream) {
23186
- if (atStorageObjects(stream)) {
23187
- stream.next();
23188
- stream.next();
23189
- stream.next();
23190
- return STORAGE_OBJECTS;
23191
- }
23192
23701
  if (atRealtimeMessages(stream)) {
23193
23702
  stream.next();
23194
23703
  stream.next();
@@ -23608,9 +24117,10 @@ function printValue(expr) {
23608
24117
  }
23609
24118
  function printSubquery(subquery) {
23610
24119
  const select = subquery.select === "*" ? "*" : printValue(subquery.select);
24120
+ const table = asciiLower(subquery.table) === STORAGE_OBJECTS ? STORAGE_OBJECTS : quoteIdent(subquery.table);
23611
24121
  const alias = subquery.alias === null ? "" : ` ${quoteIdent(subquery.alias)}`;
23612
24122
  const where = subquery.where === null ? "" : ` WHERE ${printExpr(subquery.where)}`;
23613
- return `SELECT ${select} FROM ${quoteIdent(subquery.table)}${alias}${where}`;
24123
+ return `SELECT ${select} FROM ${table}${alias}${where}`;
23614
24124
  }
23615
24125
  function printElement(element) {
23616
24126
  if (typeof element === "object") return printValue(element);
@@ -23963,6 +24473,9 @@ var FILTER_OPERATORS = [
23963
24473
  "containedBy",
23964
24474
  "overlaps"
23965
24475
  ];
24476
+ function leastFilters(firstColumn) {
24477
+ return firstColumn === void 0 ? [] : [{ operator: "is", column: firstColumn, value: null }];
24478
+ }
23966
24479
  function isAuthUid(value) {
23967
24480
  return isPlainObject2(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "auth") && value.auth === "uid";
23968
24481
  }
@@ -24576,12 +25089,21 @@ function storedSource(table, names, alias) {
24576
25089
  function tableLabel(table) {
24577
25090
  return table.name === STORAGE_OBJECTS_TABLE ? STORAGE_OBJECTS : table.name;
24578
25091
  }
25092
+ function storageReadySql(alias) {
25093
+ return `(${quoteIdentifier(alias)}.${quoteIdentifier("status")} = 'ready')`;
25094
+ }
25095
+ function ownNames(table) {
25096
+ return table.name === STORAGE_OBJECTS_TABLE ? [STORAGE_OBJECTS_QUALIFIER] : [asciiLower(table.name)];
25097
+ }
24579
25098
  function missingColumnReason(name, tables) {
24580
25099
  return `column ${name} does not exist on ${tables.join(" or ")}`;
24581
25100
  }
24582
25101
  function missingRelationReason(table) {
24583
25102
  return `relation ${table} does not exist (a system table cannot be read either)`;
24584
25103
  }
25104
+ function unresolvedSubqueryReason(table) {
25105
+ return asciiLower(table) === STORAGE_OBJECTS ? STORAGE_UNAVAILABLE_REASON : missingRelationReason(table);
25106
+ }
24585
25107
  function resolveColumn(env, ref) {
24586
25108
  const name = asciiLower(ref.name);
24587
25109
  if (ref.qualifier !== null) {
@@ -24691,10 +25213,10 @@ function storageHelperSql(fn, index, operand) {
24691
25213
  }
24692
25214
  function storageCallSql(env, expr) {
24693
25215
  const problem = storageHelperColumnProblem(expr.fn);
24694
- if (asciiLower(env.blame.policy.table) !== STORAGE_OBJECTS)
25216
+ if (!env.scopes.some((scope) => scope.table.name === STORAGE_OBJECTS_TABLE))
24695
25217
  return blamed(env, problem);
24696
25218
  const { source, column: column2 } = resolveColumn(env, expr.column);
24697
- if (source !== env.scopes[0] || source.table.name !== STORAGE_OBJECTS_TABLE || column2.name !== "name")
25219
+ if (source.table.name !== STORAGE_OBJECTS_TABLE || column2.name !== "name")
24698
25220
  return blamed(env, problem);
24699
25221
  const index = expr.fn === "foldername" ? expr.index : null;
24700
25222
  if (expr.fn === "foldername" ? index === null || !Number.isInteger(index) || index < 1 || index > MAX_FOLDERNAME_INDEX : expr.index !== null)
@@ -24708,7 +25230,8 @@ function storageCallSql(env, expr) {
24708
25230
  })
24709
25231
  );
24710
25232
  }
24711
- function operandSide(env, expr) {
25233
+ function operandSide(env, value) {
25234
+ const expr = comparedValue(value);
24712
25235
  if (expr.kind === "cast") return operandSide(env, expr.operand);
24713
25236
  if (expr.kind !== "column" && expr.kind !== "realtime")
24714
25237
  return {
@@ -24769,7 +25292,10 @@ function conditionSql(env, expr) {
24769
25292
  switch (expr.kind) {
24770
25293
  case "and":
24771
25294
  case "or":
24772
- return `(${expr.args.map((arg) => conditionSql(env, arg)).join(expr.kind === "and" ? " AND " : " OR ")})`;
25295
+ return joinConditions(
25296
+ expr.args.map((arg) => conditionSql(env, arg)),
25297
+ expr.kind === "and" ? "AND" : "OR"
25298
+ );
24773
25299
  case "not":
24774
25300
  return `(NOT ${conditionSql(env, expr.arg)})`;
24775
25301
  default:
@@ -24852,8 +25378,8 @@ function conditionSql(env, expr) {
24852
25378
  }
24853
25379
  }
24854
25380
  function subquerySql(env, subquery, select) {
24855
- const table = resolveTable(env.snapshot, subquery.table);
24856
- if (!table) return blamed(env, missingRelationReason(subquery.table));
25381
+ const table = resolveSubqueryTable(env.snapshot, subquery.table);
25382
+ if (!table) return blamed(env, unresolvedSubqueryReason(subquery.table));
24857
25383
  const { state } = env;
24858
25384
  if (++state.subqueries > MAX_STATEMENT_SUBQUERIES)
24859
25385
  return blamed(env, STATEMENT_SUBQUERIES_REASON);
@@ -24867,7 +25393,7 @@ function subquerySql(env, subquery, select) {
24867
25393
  const alias = `_naro_s${String(++state.aliases)}`;
24868
25394
  const source = storedSource(
24869
25395
  table,
24870
- [asciiLower(subquery.alias ?? table.name)],
25396
+ [asciiLower(subqueryName(subquery))],
24871
25397
  alias
24872
25398
  );
24873
25399
  const inner = { ...env, scopes: [...env.scopes, source] };
@@ -24879,11 +25405,12 @@ function subquerySql(env, subquery, select) {
24879
25405
  selected = valueSql(inner, subquery.select);
24880
25406
  if (env.check) item = operandSide(inner, subquery.select);
24881
25407
  }
24882
- const own2 = storedSource(table, [asciiLower(table.name)], alias);
25408
+ const own2 = storedSource(table, ownNames(table), alias);
24883
25409
  const rls = inlinedPolicies(env, table, own2);
25410
+ const ready = table.name === STORAGE_OBJECTS_TABLE ? `${storageReadySql(alias)} AND ` : "";
24884
25411
  const where = subquery.where === null ? "" : ` AND ${conditionSql(inner, subquery.where)}`;
24885
25412
  return {
24886
- sql: `SELECT ${selected} FROM ${quoteIdentifier(table.name)} AS ${quoteIdentifier(alias)} WHERE ${rls}${where}`,
25413
+ sql: `SELECT ${selected} FROM ${quoteIdentifier(table.name)} AS ${quoteIdentifier(alias)} WHERE ${ready}${rls}${where}`,
24887
25414
  item
24888
25415
  };
24889
25416
  } finally {
@@ -24936,7 +25463,10 @@ function groupTerms(env, sources, action, permissive, restrictive, pick2) {
24936
25463
  terms.push(term);
24937
25464
  }
24938
25465
  const group = terms.length === 0 ? { sql: "0", params: [] } : terms.length === 1 ? terms[0] : {
24939
- sql: `(${terms.map((term) => term.sql).join(" OR ")})`,
25466
+ sql: joinConditions(
25467
+ terms.map((term) => term.sql),
25468
+ "OR"
25469
+ ),
24940
25470
  params: terms.flatMap((term) => term.params)
24941
25471
  };
24942
25472
  const restricted = restrictive.flatMap((policy) => {
@@ -24946,9 +25476,10 @@ function groupTerms(env, sources, action, permissive, restrictive, pick2) {
24946
25476
  return { permissive: group, restrictive: restricted };
24947
25477
  }
24948
25478
  function inlinedPolicies(env, table, source) {
25479
+ const label2 = tableLabel(table);
24949
25480
  const found = applicablePolicies(
24950
25481
  env.req.policies,
24951
- table.name,
25482
+ label2,
24952
25483
  "select",
24953
25484
  env.req.role
24954
25485
  );
@@ -24960,7 +25491,7 @@ function inlinedPolicies(env, table, source) {
24960
25491
  if (found.permissive.length === 0) return "0";
24961
25492
  return withTableActive(
24962
25493
  env.state,
24963
- table.name,
25494
+ label2,
24964
25495
  [...found.permissive, ...found.restrictive],
24965
25496
  () => {
24966
25497
  const inner = { ...env, check: null, params: [] };
@@ -24979,7 +25510,10 @@ function inlinedPolicies(env, table, source) {
24979
25510
  for (const part of parts) env.params.push(...part.params);
24980
25511
  const [only] = parts;
24981
25512
  if (parts.length === 1 && only) return only.sql;
24982
- return `(${parts.map((part) => part.sql).join(" AND ")})`;
25513
+ return joinConditions(
25514
+ parts.map((part) => part.sql),
25515
+ "AND"
25516
+ );
24983
25517
  }
24984
25518
  );
24985
25519
  }
@@ -25074,11 +25608,8 @@ function scopedValues(operand, scopes) {
25074
25608
  return operandValues(operand).map((value) => ({ value, scopes }));
25075
25609
  }
25076
25610
  function subqueryScopes(snapshot, subquery, scopes) {
25077
- const inner = resolveTable(snapshot, subquery.table);
25078
- return inner ? [
25079
- ...scopes,
25080
- { names: [asciiLower(subquery.alias ?? inner.name)], table: inner }
25081
- ] : void 0;
25611
+ const inner = resolveSubqueryTable(snapshot, subquery.table);
25612
+ return inner ? [...scopes, { names: [asciiLower(subqueryName(subquery))], table: inner }] : void 0;
25082
25613
  }
25083
25614
  function columnsReadOn(snapshot, table, expr, written) {
25084
25615
  const found = [];
@@ -25413,7 +25944,7 @@ function nodeSql(table, node, params, path) {
25413
25944
  const terms = children.map(
25414
25945
  (child, index) => nodeSql(table, child, params, `${path}.${key}[${index}]`)
25415
25946
  );
25416
- return `(${terms.join(key === "or" ? " OR " : " AND ")})`;
25947
+ return joinConditions(terms, key === "or" ? "OR" : "AND");
25417
25948
  }
25418
25949
  function whereClause(table, filters, policy, params) {
25419
25950
  const parts = [];
@@ -25424,7 +25955,7 @@ function whereClause(table, filters, policy, params) {
25424
25955
  const client = filters.map(
25425
25956
  (node, index) => nodeSql(table, node, params, `filters[${index}]`)
25426
25957
  );
25427
- if (client.length > 0) parts.push(`(${client.join(" AND ")})`);
25958
+ if (client.length > 0) parts.push(joinConditions(client, "AND"));
25428
25959
  return parts.length === 0 ? "" : ` WHERE ${parts.join(" AND ")}`;
25429
25960
  }
25430
25961
  function orderClause(table, order) {
@@ -25626,7 +26157,7 @@ var Rls = class {
25626
26157
  const found = this.found(command);
25627
26158
  return [...found.permissive, ...found.restrictive];
25628
26159
  });
25629
- const key = asciiLower(table.name);
26160
+ const key = asciiLower(this.policyTable);
25630
26161
  if (hasSubqueries(all) && !this.state.active.includes(key))
25631
26162
  this.state.active.push(key);
25632
26163
  for (const command of commands) {
@@ -26432,41 +26963,819 @@ function compile(query, snapshot, decision, context, options = {}) {
26432
26963
  }
26433
26964
  }
26434
26965
 
26435
- // ../../packages/data-core/src/policy-index.ts
26436
- var INDEX_SQL = `SELECT t.name AS table_name, il.name AS index_name, il."partial" AS partial, ix.name AS column_name, ix.coll AS collation FROM pragma_table_list AS t JOIN pragma_index_list(t.name, t.schema) AS il JOIN pragma_index_xinfo(il.name, t.schema) AS ix WHERE t.schema = 'main' AND t.type = 'table' AND ${systemTablePredicate("t.name")} AND ix.seqno = 0`;
26437
- function indexesFromRows(rows) {
26438
- const tables = /* @__PURE__ */ new Map();
26439
- for (const row of rows) {
26440
- const table = row.table_name;
26441
- const column2 = row.column_name;
26442
- if (typeof table !== "string" || typeof column2 !== "string") continue;
26443
- if (Number(row.partial) !== 0) continue;
26444
- const key = asciiLower(table);
26445
- const columns = tables.get(key) ?? /* @__PURE__ */ new Map();
26446
- tables.set(key, columns);
26447
- const collations = columns.get(asciiLower(column2)) ?? [];
26448
- collations.push(
26449
- typeof row.collation === "string" ? row.collation.toUpperCase() : "BINARY"
26450
- );
26451
- columns.set(asciiLower(column2), collations);
26452
- }
26453
- return tables;
26966
+ // ../../packages/data-core/src/sql-depth.ts
26967
+ var D1_EXPRESSION_DEPTH = 100;
26968
+ var Unreadable = class extends Error {
26969
+ };
26970
+ function isWordStart(code) {
26971
+ return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code >= 128;
26454
26972
  }
26455
- function resolve8(scopes, qualifier, name) {
26456
- const column2 = asciiLower(name);
26457
- for (let index = scopes.length - 1; index >= 0; index--) {
26458
- const scope = scopes[index];
26459
- if (qualifier !== null && !scope.names.includes(asciiLower(qualifier)))
26460
- continue;
26461
- const found = scope.table.byName.get(column2);
26462
- if (found) return { column: found, scope: index };
26463
- if (qualifier !== null) return void 0;
26464
- }
26465
- return void 0;
26973
+ function isDigit2(code) {
26974
+ return code >= 48 && code <= 57;
26466
26975
  }
26467
- function realtimeTopic2(scopes) {
26468
- const [root] = scopes;
26469
- if (root?.table.name !== REALTIME_MESSAGES) return void 0;
26976
+ function isWordPart(code) {
26977
+ return isWordStart(code) || isDigit2(code) || code === 36;
26978
+ }
26979
+ function isSpace3(code) {
26980
+ return code === 32 || code >= 9 && code <= 13;
26981
+ }
26982
+ var OPERATORS_2 = /* @__PURE__ */ new Set([
26983
+ "->",
26984
+ "||",
26985
+ "<=",
26986
+ ">=",
26987
+ "<>",
26988
+ "!=",
26989
+ "==",
26990
+ "<<",
26991
+ ">>"
26992
+ ]);
26993
+ var OPERATORS_1 = "=<>+-*/%&|~(),.;";
26994
+ var NUMBER2 = /^(?:0[xX][0-9A-Fa-f_]+|[0-9_]*\.?[0-9_]*(?:[eE][+-]?[0-9_]+)?)/;
26995
+ function tokenize3(sql) {
26996
+ const tokens = [];
26997
+ const length = sql.length;
26998
+ let at = 0;
26999
+ const closing = (close) => {
27000
+ let from = at + 1;
27001
+ for (; ; ) {
27002
+ const found = sql.indexOf(close, from);
27003
+ if (found < 0) throw new Unreadable("an unclosed quote");
27004
+ if (close === "]" || sql[found + 1] !== close) return found;
27005
+ from = found + 2;
27006
+ }
27007
+ };
27008
+ while (at < length) {
27009
+ const code = sql.charCodeAt(at);
27010
+ const next = sql.charCodeAt(at + 1);
27011
+ if (isSpace3(code)) {
27012
+ at++;
27013
+ } else if (code === 45 && next === 45) {
27014
+ const end = sql.indexOf("\n", at);
27015
+ at = end < 0 ? length : end + 1;
27016
+ } else if (code === 47 && next === 42) {
27017
+ const end = sql.indexOf("*/", at + 2);
27018
+ at = end < 0 ? length : end + 2;
27019
+ } else if (code === 39) {
27020
+ at = closing("'") + 1;
27021
+ tokens.push({ kind: "literal" });
27022
+ } else if (code === 34 || code === 96 || code === 91) {
27023
+ at = closing(code === 91 ? "]" : String.fromCharCode(code)) + 1;
27024
+ tokens.push({ kind: "name" });
27025
+ } else if ((code === 88 || code === 120) && next === 39) {
27026
+ at++;
27027
+ at = closing("'") + 1;
27028
+ tokens.push({ kind: "literal" });
27029
+ } else if (isDigit2(code) || code === 46 && isDigit2(next)) {
27030
+ const text2 = NUMBER2.exec(sql.slice(at, at + 64))?.[0] || sql[at] || "";
27031
+ tokens.push(
27032
+ /^[0-9]+$/.test(text2) ? { kind: "literal", integer: Number(text2) } : { kind: "literal" }
27033
+ );
27034
+ at += text2.length;
27035
+ } else if (code === 63 || code === 58 || code === 64 || code === 36) {
27036
+ at++;
27037
+ while (at < length && isWordPart(sql.charCodeAt(at))) at++;
27038
+ tokens.push({ kind: "param" });
27039
+ } else if (isWordStart(code)) {
27040
+ const start = at;
27041
+ while (at < length && isWordPart(sql.charCodeAt(at))) at++;
27042
+ const text2 = sql.slice(start, at);
27043
+ tokens.push({ kind: "word", text: text2, upper: text2.toUpperCase() });
27044
+ } else if (sql.startsWith("->>", at)) {
27045
+ tokens.push({ kind: "op", text: "->>" });
27046
+ at += 3;
27047
+ } else if (OPERATORS_2.has(sql.slice(at, at + 2))) {
27048
+ tokens.push({ kind: "op", text: sql.slice(at, at + 2) });
27049
+ at += 2;
27050
+ } else if (OPERATORS_1.includes(sql[at] ?? "")) {
27051
+ tokens.push({ kind: "op", text: sql[at] ?? "" });
27052
+ at++;
27053
+ } else throw new Unreadable(`an unexpected ${sql[at] ?? ""}`);
27054
+ }
27055
+ tokens.push({ kind: "end" });
27056
+ return tokens;
27057
+ }
27058
+ var CLAUSE_WORDS2 = /* @__PURE__ */ new Set([
27059
+ "AS",
27060
+ "CROSS",
27061
+ "DEFAULT",
27062
+ "DO",
27063
+ "EXCEPT",
27064
+ "FROM",
27065
+ "FULL",
27066
+ "GROUP",
27067
+ "HAVING",
27068
+ "INDEXED",
27069
+ "INNER",
27070
+ "INTERSECT",
27071
+ "JOIN",
27072
+ "LEFT",
27073
+ "LIMIT",
27074
+ "NATURAL",
27075
+ "NOT",
27076
+ "OFFSET",
27077
+ "ON",
27078
+ "ORDER",
27079
+ "OUTER",
27080
+ "RETURNING",
27081
+ "RIGHT",
27082
+ "SET",
27083
+ "UNION",
27084
+ "USING",
27085
+ "VALUES",
27086
+ "WHERE",
27087
+ "WINDOW"
27088
+ ]);
27089
+ var NOT_CONSTANT = /* @__PURE__ */ new Set([
27090
+ "avg",
27091
+ "changes",
27092
+ "count",
27093
+ "group_concat",
27094
+ "json_group_array",
27095
+ "json_group_object",
27096
+ "jsonb_group_array",
27097
+ "jsonb_group_object",
27098
+ "last_insert_rowid",
27099
+ "random",
27100
+ "randomblob",
27101
+ "sqlite_offset",
27102
+ "string_agg",
27103
+ "sum",
27104
+ "total",
27105
+ "total_changes"
27106
+ ]);
27107
+ var OR = 1;
27108
+ var AND = 2;
27109
+ var NOT = 3;
27110
+ var COMPARE = 4;
27111
+ var ORDER = 5;
27112
+ var ESCAPE = 6;
27113
+ var BITS = 7;
27114
+ var SUM = 8;
27115
+ var PRODUCT = 9;
27116
+ var CONCAT = 10;
27117
+ var COLLATE = 11;
27118
+ var UNARY = 12;
27119
+ var BINARY = {
27120
+ "=": COMPARE,
27121
+ "==": COMPARE,
27122
+ "!=": COMPARE,
27123
+ "<>": COMPARE,
27124
+ "<": ORDER,
27125
+ "<=": ORDER,
27126
+ ">": ORDER,
27127
+ ">=": ORDER,
27128
+ "&": BITS,
27129
+ "|": BITS,
27130
+ "<<": BITS,
27131
+ ">>": BITS,
27132
+ "+": SUM,
27133
+ "-": SUM,
27134
+ "*": PRODUCT,
27135
+ "/": PRODUCT,
27136
+ "%": PRODUCT,
27137
+ "||": CONCAT,
27138
+ "->": CONCAT,
27139
+ "->>": CONCAT
27140
+ };
27141
+ var LIKE_WORDS = /* @__PURE__ */ new Set(["LIKE", "GLOB", "REGEXP", "MATCH"]);
27142
+ var USABLE_OPERATORS = /* @__PURE__ */ new Set(["=", "==", "<", "<=", ">", ">="]);
27143
+ var Reader = class {
27144
+ constructor(tokens, planner) {
27145
+ this.tokens = tokens;
27146
+ this.planner = planner;
27147
+ }
27148
+ at = 0;
27149
+ /** The deepest node built, kept or not: each is checked as it is built. */
27150
+ deepest = 0;
27151
+ peek(offset = 0) {
27152
+ return this.tokens[this.at + offset] ?? { kind: "end" };
27153
+ }
27154
+ isWord(word, offset = 0) {
27155
+ const token = this.peek(offset);
27156
+ return token.kind === "word" && token.upper === word;
27157
+ }
27158
+ isOp(op, offset = 0) {
27159
+ const token = this.peek(offset);
27160
+ return token.kind === "op" && token.text === op;
27161
+ }
27162
+ isName(offset = 0) {
27163
+ const kind = this.peek(offset).kind;
27164
+ return kind === "word" || kind === "name";
27165
+ }
27166
+ takeWord(word) {
27167
+ if (!this.isWord(word)) return false;
27168
+ this.at++;
27169
+ return true;
27170
+ }
27171
+ takeOp(op) {
27172
+ if (!this.isOp(op)) return false;
27173
+ this.at++;
27174
+ return true;
27175
+ }
27176
+ expectWord(word) {
27177
+ if (!this.takeWord(word)) throw new Unreadable(`no ${word}`);
27178
+ }
27179
+ expectOp(op) {
27180
+ if (!this.takeOp(op)) throw new Unreadable(`no ${op}`);
27181
+ }
27182
+ name() {
27183
+ if (!this.isName()) throw new Unreadable("no name");
27184
+ this.at++;
27185
+ }
27186
+ /** `name` or `schema.name`. */
27187
+ qualifiedName() {
27188
+ this.name();
27189
+ if (this.isOp(".") && this.isName(1)) this.at += 2;
27190
+ }
27191
+ atEnd() {
27192
+ this.takeOp(";");
27193
+ return this.peek().kind === "end";
27194
+ }
27195
+ // --- nodes, as expr.c builds them ---
27196
+ made(node) {
27197
+ this.deepest = Math.max(this.deepest, node.height);
27198
+ return node;
27199
+ }
27200
+ leaf(extra = {}) {
27201
+ return this.made({ height: 1, constant: true, subqueries: [], ...extra });
27202
+ }
27203
+ /** sqlite3PExpr, exprSetHeight: one deeper than the deepest part. */
27204
+ over(parts, extra = {}) {
27205
+ return this.made(this.unchecked(parts, extra));
27206
+ }
27207
+ /**
27208
+ * sqlite3ExprAttachSubtrees alone, as for a CAST: its height is set and
27209
+ * not checked, so it counts only through what holds it.
27210
+ */
27211
+ unchecked(parts, extra = {}) {
27212
+ let height = 0;
27213
+ let constant = true;
27214
+ let hasFunc = false;
27215
+ let hasSubquery = false;
27216
+ let subqueries = [];
27217
+ for (const part of parts) {
27218
+ if (part.height > height) height = part.height;
27219
+ if (!part.constant) constant = false;
27220
+ if (part.hasFunc) hasFunc = true;
27221
+ if (part.hasSubquery) hasSubquery = true;
27222
+ if (part.subqueries.length > 0)
27223
+ subqueries = subqueries.length === 0 ? part.subqueries : [...subqueries, ...part.subqueries];
27224
+ }
27225
+ const node = { height: height + 1, constant, subqueries, ...extra };
27226
+ if (hasFunc) node.hasFunc ??= true;
27227
+ if (hasSubquery) node.hasSubquery ??= true;
27228
+ return node;
27229
+ }
27230
+ /** A node holding a SELECT: EXISTS, `(SELECT …)`, `x IN (SELECT …)`. */
27231
+ holding(left, select) {
27232
+ const node = {
27233
+ height: Math.max(select.height, left?.height ?? 0) + 1,
27234
+ constant: false,
27235
+ subqueries: [...left?.subqueries ?? [], select],
27236
+ hasSubquery: true
27237
+ };
27238
+ if (left?.hasFunc) node.hasFunc = true;
27239
+ if (left?.column) node.usable = true;
27240
+ return this.made(node);
27241
+ }
27242
+ // --- expressions, by parse.y's precedence ---
27243
+ expr(min = OR) {
27244
+ let left = this.prefix();
27245
+ for (; ; ) {
27246
+ const token = this.peek();
27247
+ if (token.kind === "op") {
27248
+ const level = BINARY[token.text];
27249
+ if (level === void 0 || level < min) return left;
27250
+ this.at++;
27251
+ const right = this.expr(level + 1);
27252
+ if (token.text === "->" || token.text === "->>") {
27253
+ left = this.over([left, right], { hasFunc: true });
27254
+ continue;
27255
+ }
27256
+ const compared = USABLE_OPERATORS.has(token.text);
27257
+ left = this.over(
27258
+ [left, right],
27259
+ compared && (left.column || right.column) ? { usable: true } : {}
27260
+ );
27261
+ continue;
27262
+ }
27263
+ if (token.kind !== "word") return left;
27264
+ const word = token.upper;
27265
+ if (word === "OR" || word === "AND") {
27266
+ if ((word === "OR" ? OR : AND) < min) return left;
27267
+ this.at++;
27268
+ const right = this.expr((word === "OR" ? OR : AND) + 1);
27269
+ if (word === "OR")
27270
+ left = this.over([left, right], { or: [left, right] });
27271
+ else if ((left.isFalse || right.isFalse) && !left.hasFunc && !right.hasFunc)
27272
+ left = this.leaf({ isFalse: true, literal: true });
27273
+ else left = this.over([left, right], { and: [left, right] });
27274
+ continue;
27275
+ }
27276
+ if (word === "COLLATE") {
27277
+ if (COLLATE < min) return left;
27278
+ this.at++;
27279
+ this.name();
27280
+ left = this.made({
27281
+ height: 1,
27282
+ constant: left.constant,
27283
+ subqueries: left.subqueries,
27284
+ ...left.column ? { column: true } : {}
27285
+ });
27286
+ continue;
27287
+ }
27288
+ if (COMPARE < min) return left;
27289
+ const negated = word === "NOT" ? 1 : 0;
27290
+ const after = this.peek(negated);
27291
+ const verb = after.kind === "word" ? after.upper : "";
27292
+ if (word === "IS") {
27293
+ this.at++;
27294
+ let not = this.takeWord("NOT");
27295
+ if (this.takeWord("DISTINCT")) {
27296
+ this.expectWord("FROM");
27297
+ not = !not;
27298
+ }
27299
+ const right = this.expr(COMPARE + 1);
27300
+ left = right.isNull ? this.nullTest(left, !not) : this.over(
27301
+ [left, right],
27302
+ !not && (left.column || right.column) ? { usable: true } : {}
27303
+ );
27304
+ } else if (word === "ISNULL" || word === "NOTNULL") {
27305
+ this.at++;
27306
+ left = this.nullTest(left, word === "ISNULL");
27307
+ } else if (negated && verb === "NULL") {
27308
+ this.at += 2;
27309
+ left = this.nullTest(left, false);
27310
+ } else if (LIKE_WORDS.has(verb)) {
27311
+ this.at += negated + 1;
27312
+ const args = [this.expr(COMPARE + 1), left];
27313
+ if (this.takeWord("ESCAPE")) args.push(this.expr(ESCAPE + 1));
27314
+ left = this.over(args, { hasFunc: true });
27315
+ if (negated) left = this.over([left]);
27316
+ } else if (verb === "BETWEEN") {
27317
+ this.at += negated + 1;
27318
+ const low = this.expr(NOT);
27319
+ this.expectWord("AND");
27320
+ const high = this.expr(COMPARE + 1);
27321
+ left = this.over([left, low, high]);
27322
+ if (negated) left = this.over([left]);
27323
+ } else if (verb === "IN") {
27324
+ this.at += negated + 1;
27325
+ left = this.inList(left, negated === 1);
27326
+ } else return left;
27327
+ }
27328
+ }
27329
+ /**
27330
+ * IS NULL (`isNull`) or IS NOT NULL: on a literal, the answer itself; on
27331
+ * a column, a term the planner can use when it is IS NULL.
27332
+ */
27333
+ nullTest(left, isNull) {
27334
+ if (left.literal)
27335
+ return this.leaf(
27336
+ isNull ? { isFalse: true, literal: true } : { literal: true }
27337
+ );
27338
+ return this.over([left], isNull && left.column ? { usable: true } : {});
27339
+ }
27340
+ inList(left, negated) {
27341
+ this.expectOp("(");
27342
+ if (this.takeOp(")"))
27343
+ return left.hasFunc ? this.over([left]) : this.leaf(negated ? {} : { isFalse: true });
27344
+ let node;
27345
+ if (this.startsSelect()) {
27346
+ node = this.holding(left, this.select());
27347
+ } else {
27348
+ const items = this.list();
27349
+ const [only] = items;
27350
+ const usable = left.column ? { usable: true } : {};
27351
+ if (items.length === 1 && only?.select)
27352
+ node = this.holding(left, only.select);
27353
+ else if (items.length === 1 && only?.constant)
27354
+ node = this.over([left, this.over([only])], usable);
27355
+ else node = this.over([left, ...items], usable);
27356
+ }
27357
+ this.expectOp(")");
27358
+ return negated ? this.over([node]) : node;
27359
+ }
27360
+ list() {
27361
+ const items = [this.expr()];
27362
+ while (this.takeOp(",")) items.push(this.expr());
27363
+ return items;
27364
+ }
27365
+ prefix() {
27366
+ const token = this.peek();
27367
+ this.at++;
27368
+ switch (token.kind) {
27369
+ case "literal":
27370
+ return this.leaf(
27371
+ token.integer === 0 ? { isFalse: true, literal: true } : { literal: true }
27372
+ );
27373
+ case "param":
27374
+ return this.leaf();
27375
+ case "name":
27376
+ return this.column();
27377
+ case "end":
27378
+ throw new Unreadable("no expression");
27379
+ case "op":
27380
+ if (token.text === "(") {
27381
+ if (this.startsSelect()) {
27382
+ const select = this.select();
27383
+ this.expectOp(")");
27384
+ return { ...this.holding(null, select), select };
27385
+ }
27386
+ const items = this.list();
27387
+ this.expectOp(")");
27388
+ if (items.length === 1 && items[0]) return items[0];
27389
+ throw new Unreadable("a row value");
27390
+ }
27391
+ if (token.text === "-" || token.text === "+") {
27392
+ const operand = this.expr(UNARY);
27393
+ return this.over([operand], operand.literal ? { literal: true } : {});
27394
+ }
27395
+ if (token.text === "~") return this.over([this.expr(UNARY)]);
27396
+ throw new Unreadable(`an unexpected ${token.text}`);
27397
+ case "word":
27398
+ break;
27399
+ }
27400
+ switch (token.upper) {
27401
+ case "NULL":
27402
+ return this.leaf({ isNull: true });
27403
+ case "TRUE":
27404
+ case "FALSE":
27405
+ return this.leaf();
27406
+ case "NOT":
27407
+ return this.over([this.expr(NOT)]);
27408
+ case "EXISTS": {
27409
+ this.expectOp("(");
27410
+ const select = this.select();
27411
+ this.expectOp(")");
27412
+ return this.holding(null, select);
27413
+ }
27414
+ case "CAST": {
27415
+ this.expectOp("(");
27416
+ const value = this.expr();
27417
+ this.expectWord("AS");
27418
+ this.typeName();
27419
+ this.expectOp(")");
27420
+ return this.unchecked([value]);
27421
+ }
27422
+ case "CASE": {
27423
+ const parts = [];
27424
+ if (!this.isWord("WHEN")) parts.push(this.expr());
27425
+ while (this.takeWord("WHEN")) {
27426
+ parts.push(this.expr());
27427
+ this.expectWord("THEN");
27428
+ parts.push(this.expr());
27429
+ }
27430
+ if (this.takeWord("ELSE")) parts.push(this.expr());
27431
+ this.expectWord("END");
27432
+ return this.over(parts);
27433
+ }
27434
+ default:
27435
+ break;
27436
+ }
27437
+ if (!this.takeOp("(")) return this.column();
27438
+ let args = [];
27439
+ if (!this.takeOp("*") && !this.isOp(")")) {
27440
+ if (!this.takeWord("DISTINCT")) this.takeWord("ALL");
27441
+ args = this.list();
27442
+ }
27443
+ this.expectOp(")");
27444
+ if (this.isWord("FILTER") || this.isWord("OVER"))
27445
+ throw new Unreadable("a window function");
27446
+ return this.over(args, {
27447
+ hasFunc: true,
27448
+ constant: !NOT_CONSTANT.has(token.text.toLowerCase()) && args.every((arg) => arg.constant)
27449
+ });
27450
+ }
27451
+ /**
27452
+ * `a`, `a.b` or `a.b.c`, its first name just read: TK_ID, and a TK_DOT a
27453
+ * dot — a.b.c is DOT(a, DOT(b, c)).
27454
+ */
27455
+ column() {
27456
+ let node = this.leaf({ constant: false, column: true });
27457
+ while (this.isOp(".") && this.isName(1)) {
27458
+ this.at += 2;
27459
+ node = this.over([this.leaf({ constant: false }), node], {
27460
+ column: true
27461
+ });
27462
+ }
27463
+ return node;
27464
+ }
27465
+ typeName() {
27466
+ do
27467
+ this.name();
27468
+ while (this.isName());
27469
+ if (this.takeOp("(")) {
27470
+ while (!this.takeOp(")")) {
27471
+ if (this.peek().kind === "end") throw new Unreadable("no )");
27472
+ this.at++;
27473
+ }
27474
+ }
27475
+ }
27476
+ /**
27477
+ * A WHERE, and the chain an OR in it may bring when each of its branches
27478
+ * could be read through an index — a column compared, or an AND holding
27479
+ * one (exprAnalyzeOrTerm) — whether or not the table has that index: the
27480
+ * other terms the planner can use, without a subquery, ANDed one by one
27481
+ * from the first, under one more AND (wherecode.c, WHERE_MULTI_OR).
27482
+ */
27483
+ where() {
27484
+ const where = this.expr();
27485
+ if (!this.planner) return where;
27486
+ const terms = split(where, "and");
27487
+ for (const or of terms) {
27488
+ if (!or.or || !split(or, "or").every(
27489
+ (branch) => branch.usable || split(branch, "and").some((term) => term.usable)
27490
+ ))
27491
+ continue;
27492
+ let chain = 0;
27493
+ for (const term of terms)
27494
+ if (term !== or && (term.usable || term.or) && !term.hasSubquery)
27495
+ chain = chain === 0 ? term.height : Math.max(chain, term.height) + 1;
27496
+ if (chain > 0) this.deepest = Math.max(this.deepest, chain + 1);
27497
+ }
27498
+ return where;
27499
+ }
27500
+ // --- statements ---
27501
+ startsSelect() {
27502
+ return this.isWord("SELECT") || this.isWord("VALUES") || this.isWord("WITH");
27503
+ }
27504
+ /** A SELECT, compound or not, with its ORDER BY and LIMIT. */
27505
+ select() {
27506
+ if (this.isWord("WITH")) throw new Unreadable("WITH");
27507
+ const whole = { height: 0, resolved: [], from: [] };
27508
+ const counted = (node) => {
27509
+ whole.height = Math.max(whole.height, node.height);
27510
+ whole.resolved.push(node);
27511
+ };
27512
+ do {
27513
+ if (this.takeWord("VALUES")) {
27514
+ do {
27515
+ this.expectOp("(");
27516
+ for (const item of this.list()) counted(item);
27517
+ this.expectOp(")");
27518
+ } while (this.takeOp(","));
27519
+ continue;
27520
+ }
27521
+ this.expectWord("SELECT");
27522
+ if (!this.takeWord("DISTINCT")) this.takeWord("ALL");
27523
+ const stars = [];
27524
+ do {
27525
+ if (this.takeOp("*")) stars.push(this.leaf());
27526
+ else if (this.isName() && this.isOp(".", 1) && this.isOp("*", 2)) {
27527
+ this.at += 3;
27528
+ stars.push(this.over([this.leaf(), this.leaf()]));
27529
+ } else {
27530
+ counted(this.expr());
27531
+ this.alias();
27532
+ }
27533
+ } while (this.takeOp(","));
27534
+ const sources = this.takeWord("FROM") ? this.from(whole) : 0;
27535
+ for (const star of stars) {
27536
+ whole.height = Math.max(whole.height, star.height);
27537
+ whole.resolved.push(
27538
+ sources > 1 ? this.made({ height: 3, constant: false, subqueries: [] }) : star
27539
+ );
27540
+ }
27541
+ if (this.takeWord("WHERE")) counted(this.where());
27542
+ if (this.takeWord("GROUP")) {
27543
+ this.expectWord("BY");
27544
+ for (const item of this.list()) counted(item);
27545
+ if (this.takeWord("HAVING")) counted(this.expr());
27546
+ }
27547
+ } while (this.compound());
27548
+ if (this.takeWord("ORDER")) {
27549
+ this.expectWord("BY");
27550
+ do {
27551
+ counted(this.expr());
27552
+ if (!this.takeWord("ASC")) this.takeWord("DESC");
27553
+ if (this.takeWord("NULLS") && !this.takeWord("FIRST"))
27554
+ this.expectWord("LAST");
27555
+ } while (this.takeOp(","));
27556
+ }
27557
+ if (this.takeWord("LIMIT")) {
27558
+ const parts = [this.expr()];
27559
+ if (this.takeWord("OFFSET") || this.takeOp(",")) parts.push(this.expr());
27560
+ counted(this.over(parts));
27561
+ }
27562
+ return whole;
27563
+ }
27564
+ compound() {
27565
+ if (this.takeWord("UNION")) {
27566
+ this.takeWord("ALL");
27567
+ return true;
27568
+ }
27569
+ return this.takeWord("INTERSECT") || this.takeWord("EXCEPT");
27570
+ }
27571
+ alias() {
27572
+ if (this.takeWord("AS")) {
27573
+ this.name();
27574
+ return;
27575
+ }
27576
+ const token = this.peek();
27577
+ if (token.kind === "name" || token.kind === "word" && !CLAUSE_WORDS2.has(token.upper))
27578
+ this.at++;
27579
+ }
27580
+ /** The FROM items, into `into`; how many there are. */
27581
+ from(into) {
27582
+ let sources = 0;
27583
+ do {
27584
+ sources++;
27585
+ if (this.takeOp("(")) {
27586
+ if (!this.startsSelect()) throw new Unreadable("a parenthesized join");
27587
+ into.from.push(this.select());
27588
+ this.expectOp(")");
27589
+ } else {
27590
+ this.qualifiedName();
27591
+ if (this.takeOp("(")) {
27592
+ if (!this.isOp(")"))
27593
+ for (const arg of this.list()) {
27594
+ into.resolved.push(arg);
27595
+ this.deepest = Math.max(this.deepest, arg.height + 2);
27596
+ }
27597
+ this.expectOp(")");
27598
+ }
27599
+ }
27600
+ this.alias();
27601
+ if (this.takeWord("INDEXED")) {
27602
+ this.expectWord("BY");
27603
+ this.name();
27604
+ } else if (this.isWord("NOT") && this.isWord("INDEXED", 1)) this.at += 2;
27605
+ if (this.isWord("ON") || this.isWord("USING"))
27606
+ throw new Unreadable("a join constraint");
27607
+ } while (this.join());
27608
+ return sources;
27609
+ }
27610
+ join() {
27611
+ if (this.takeOp(",")) return true;
27612
+ const start = this.at;
27613
+ this.takeWord("NATURAL");
27614
+ if (this.takeWord("LEFT") || this.takeWord("RIGHT") || this.takeWord("FULL"))
27615
+ this.takeWord("OUTER");
27616
+ else if (!this.takeWord("INNER")) this.takeWord("CROSS");
27617
+ if (this.takeWord("JOIN")) return true;
27618
+ this.at = start;
27619
+ return false;
27620
+ }
27621
+ names() {
27622
+ this.expectOp("(");
27623
+ do
27624
+ this.name();
27625
+ while (this.takeOp(","));
27626
+ this.expectOp(")");
27627
+ }
27628
+ returning(into) {
27629
+ if (!this.takeWord("RETURNING")) return;
27630
+ do {
27631
+ if (this.takeOp("*")) into.push(this.leaf());
27632
+ else {
27633
+ into.push(this.expr());
27634
+ this.alias();
27635
+ }
27636
+ } while (this.takeOp(","));
27637
+ }
27638
+ assignments(into) {
27639
+ do {
27640
+ if (this.isOp("(")) this.names();
27641
+ else this.name();
27642
+ this.expectOp("=");
27643
+ into.push(this.expr());
27644
+ } while (this.takeOp(","));
27645
+ }
27646
+ /** The statement: each of its expressions resolved from 0. */
27647
+ statement() {
27648
+ const top = { height: 0, resolved: [], from: [] };
27649
+ if (this.startsSelect()) {
27650
+ top.from.push(this.select());
27651
+ } else if (this.takeWord("INSERT") || this.takeWord("REPLACE")) {
27652
+ if (this.takeWord("OR")) this.name();
27653
+ this.expectWord("INTO");
27654
+ this.qualifiedName();
27655
+ if (this.takeWord("AS")) this.name();
27656
+ if (this.isOp("(")) this.names();
27657
+ if (this.takeWord("DEFAULT")) this.expectWord("VALUES");
27658
+ else top.from.push(this.select());
27659
+ while (this.takeWord("ON")) {
27660
+ this.expectWord("CONFLICT");
27661
+ if (this.takeOp("(")) {
27662
+ top.resolved.push(...this.list());
27663
+ this.expectOp(")");
27664
+ if (this.takeWord("WHERE")) top.resolved.push(this.expr());
27665
+ }
27666
+ this.expectWord("DO");
27667
+ if (this.takeWord("NOTHING")) continue;
27668
+ this.expectWord("UPDATE");
27669
+ this.expectWord("SET");
27670
+ this.assignments(top.resolved);
27671
+ if (this.takeWord("WHERE")) top.resolved.push(this.expr());
27672
+ }
27673
+ this.returning(top.resolved);
27674
+ } else if (this.takeWord("UPDATE")) {
27675
+ if (this.takeWord("OR")) this.name();
27676
+ this.qualifiedName();
27677
+ this.alias();
27678
+ if (this.takeWord("INDEXED")) {
27679
+ this.expectWord("BY");
27680
+ this.name();
27681
+ }
27682
+ this.expectWord("SET");
27683
+ this.assignments(top.resolved);
27684
+ if (this.isWord("FROM")) throw new Unreadable("UPDATE \u2026 FROM");
27685
+ if (this.takeWord("WHERE")) top.resolved.push(this.where());
27686
+ this.returning(top.resolved);
27687
+ } else if (this.takeWord("DELETE")) {
27688
+ this.expectWord("FROM");
27689
+ this.qualifiedName();
27690
+ this.alias();
27691
+ if (this.takeWord("INDEXED")) {
27692
+ this.expectWord("BY");
27693
+ this.name();
27694
+ }
27695
+ if (this.takeWord("WHERE")) top.resolved.push(this.where());
27696
+ this.returning(top.resolved);
27697
+ } else throw new Unreadable("not a statement naro compiles");
27698
+ return top;
27699
+ }
27700
+ };
27701
+ function split(node, at) {
27702
+ const found = [];
27703
+ const pending = [node];
27704
+ for (let next = pending.pop(); next; next = pending.pop()) {
27705
+ const sides = next[at];
27706
+ if (sides) pending.push(sides[1], sides[0]);
27707
+ else found.push(next);
27708
+ }
27709
+ return found;
27710
+ }
27711
+ function nodeNeed(node, base2) {
27712
+ const at = base2 + node.height;
27713
+ let need = at;
27714
+ for (const select of node.subqueries)
27715
+ need = Math.max(need, selectNeed(select, at));
27716
+ return need;
27717
+ }
27718
+ function selectNeed(select, base2) {
27719
+ let need = base2;
27720
+ for (const node of select.resolved)
27721
+ need = Math.max(need, nodeNeed(node, base2));
27722
+ for (const inner of select.from)
27723
+ need = Math.max(need, selectNeed(inner, base2));
27724
+ return need;
27725
+ }
27726
+ function expressionDepth(sql, { planner = true } = {}) {
27727
+ try {
27728
+ const reader = new Reader(tokenize3(sql), planner);
27729
+ const top = reader.statement();
27730
+ if (!reader.atEnd()) return void 0;
27731
+ return Math.max(reader.deepest, selectNeed(top, 0));
27732
+ } catch (error51) {
27733
+ if (error51 instanceof Unreadable) return void 0;
27734
+ throw error51;
27735
+ }
27736
+ }
27737
+
27738
+ // ../../packages/data-core/src/policy-index.ts
27739
+ var INDEX_SQL = `SELECT t.name AS table_name, il.name AS index_name, il."partial" AS partial, ix.name AS column_name, ix.coll AS collation FROM pragma_table_list AS t JOIN pragma_index_list(t.name, t.schema) AS il JOIN pragma_index_xinfo(il.name, t.schema) AS ix WHERE t.schema = 'main' AND t.type = 'table' AND ${systemTablePredicate("t.name")} AND ix.seqno = 0`;
27740
+ function indexesFromRows(rows) {
27741
+ const tables = /* @__PURE__ */ new Map();
27742
+ for (const row of rows) {
27743
+ const table = row.table_name;
27744
+ const column2 = row.column_name;
27745
+ if (typeof table !== "string" || typeof column2 !== "string") continue;
27746
+ if (Number(row.partial) !== 0) continue;
27747
+ const key = asciiLower(table);
27748
+ const columns = tables.get(key) ?? /* @__PURE__ */ new Map();
27749
+ tables.set(key, columns);
27750
+ const collations = columns.get(asciiLower(column2)) ?? [];
27751
+ collations.push(
27752
+ typeof row.collation === "string" ? row.collation.toUpperCase() : "BINARY"
27753
+ );
27754
+ columns.set(asciiLower(column2), collations);
27755
+ }
27756
+ return tables;
27757
+ }
27758
+ var STORAGE_OBJECTS_INDEXES = /* @__PURE__ */ new Map(
27759
+ [
27760
+ ["id", ["BINARY"]],
27761
+ ["bucket_id", ["BINARY"]]
27762
+ ]
27763
+ );
27764
+ function resolve8(scopes, qualifier, name) {
27765
+ const column2 = asciiLower(name);
27766
+ for (let index = scopes.length - 1; index >= 0; index--) {
27767
+ const scope = scopes[index];
27768
+ if (qualifier !== null && !scope.names.includes(asciiLower(qualifier)))
27769
+ continue;
27770
+ const found = scope.table.byName.get(column2);
27771
+ if (found) return { column: found, scope: index };
27772
+ if (qualifier !== null) return void 0;
27773
+ }
27774
+ return void 0;
27775
+ }
27776
+ function realtimeTopic2(scopes) {
27777
+ const [root] = scopes;
27778
+ if (root?.table.name !== REALTIME_MESSAGES) return void 0;
26470
27779
  const column2 = root.table.byName.get("topic");
26471
27780
  return column2 ? { column: column2, scope: 0 } : void 0;
26472
27781
  }
@@ -26527,12 +27836,9 @@ function readsOutside(scopes, expr, depth, snapshot) {
26527
27836
  }
26528
27837
  }
26529
27838
  function subqueryScopes2(scopes, subquery, snapshot) {
26530
- const table = resolveTable(snapshot, subquery.table);
27839
+ const table = resolveSubqueryTable(snapshot, subquery.table);
26531
27840
  if (!table) return void 0;
26532
- return [
26533
- ...scopes,
26534
- { table, names: [asciiLower(subquery.alias ?? table.name)] }
26535
- ];
27841
+ return [...scopes, { table, names: [asciiLower(subqueryName(subquery))] }];
26536
27842
  }
26537
27843
  function conjuncts(expr) {
26538
27844
  if (expr === null) return [];
@@ -26546,24 +27852,27 @@ function columnAffinityOf(column2) {
26546
27852
  if (affinity2 === "BLOB") return "blob";
26547
27853
  return "numeric";
26548
27854
  }
26549
- function operandColumn(scopes, value) {
27855
+ function operandColumn(scopes, operand) {
27856
+ const value = comparedValue(operand);
26550
27857
  if (value.kind === "realtime") return realtimeTopic2(scopes);
26551
27858
  if (value.kind !== "column") return void 0;
26552
27859
  return resolve8(scopes, value.qualifier, value.name);
26553
27860
  }
26554
27861
  function valueAffinity(scopes, value) {
26555
- if (value.kind === "cast") return "text";
27862
+ if (comparedValue(value).kind === "cast") return "text";
26556
27863
  const found = operandColumn(scopes, value);
26557
27864
  return found ? columnAffinityOf(found.column) : "none";
26558
27865
  }
26559
27866
  function valueCollation(scopes, value) {
26560
- if (value.kind === "cast") return valueCollation(scopes, value.operand);
27867
+ const compared = comparedValue(value);
27868
+ if (compared.kind === "cast") return valueCollation(scopes, compared.operand);
26561
27869
  const found = operandColumn(scopes, value);
26562
27870
  return found ? found.column.collation : void 0;
26563
27871
  }
26564
27872
  function waysIn(scopes, where) {
26565
27873
  const inner = scopes.length - 1;
26566
- const own2 = (value) => {
27874
+ const own2 = (operand) => {
27875
+ const value = comparedValue(operand);
26567
27876
  if (value.kind !== "column") return void 0;
26568
27877
  const found = resolve8(scopes, value.qualifier, value.name);
26569
27878
  return found?.scope === inner ? found.column : void 0;
@@ -26611,7 +27920,7 @@ function isRowid(table, column2) {
26611
27920
  function blocked(table, indexes, way) {
26612
27921
  if (isRowid(table, way.column))
26613
27922
  return way.affinity === "text" ? "affinity" : void 0;
26614
- const collations = indexes.get(asciiLower(table.name))?.get(asciiLower(way.column.name));
27923
+ const collations = (table.name === STORAGE_OBJECTS_TABLE ? STORAGE_OBJECTS_INDEXES : indexes.get(asciiLower(table.name)))?.get(asciiLower(way.column.name));
26615
27924
  if (!collations || collations.length === 0) return "none";
26616
27925
  const wanted = way.collation === void 0 ? way.column.collation : way.collation;
26617
27926
  if (wanted !== null && !collations.includes(wanted)) return "collation";
@@ -26622,9 +27931,17 @@ function blocked(table, indexes, way) {
26622
27931
  }
26623
27932
  function problemMessage(policy, outer, table, ways, why) {
26624
27933
  const runs = outer.name === REALTIME_MESSAGES ? "runs once for every channel join it checks" : `runs once for every row of ${tableLabel(outer)} it looks at`;
26625
- const start = `Policy ${policy.name} on ${tableLabel(outer)}: its subquery on ${table.name} ${runs}`;
27934
+ const start = `Policy ${policy.name} on ${tableLabel(outer)}: its subquery on ${tableLabel(table)} ${runs}`;
26626
27935
  const override = "or send it with allow_unindexed=1 (naro policy create --allow-unindexed) to accept a scan of every row";
26627
27936
  const [way] = ways;
27937
+ if (table.name === STORAGE_OBJECTS_TABLE) {
27938
+ const column3 = way ? `${STORAGE_OBJECTS}.${way.column.name}` : "";
27939
+ if (way && why === "collation")
27940
+ return `${start}, and it compares ${column3} in collation ${String(way.collation ?? way.column.collation)}, which its index (BINARY) cannot search, so each run reads all of ${STORAGE_OBJECTS}. Write ${column3} on the left of = so that the comparison is BINARY \u2014 ${override}.`;
27941
+ if (way && why === "affinity")
27942
+ return `${start}, and it compares ${column3} with a number, which its index (text) cannot search, so each run reads all of ${STORAGE_OBJECTS}. Compare it with text, as in <value>::text \u2014 ${override}.`;
27943
+ return `${start}, and no condition of its WHERE compares ${STORAGE_OBJECTS}.bucket_id or ${STORAGE_OBJECTS}.id with = to a value from outside it, so each run reads all of ${STORAGE_OBJECTS}. Add bucket_id = '<bucket>' to it \u2014 ${override}.`;
27944
+ }
26628
27945
  if (!way)
26629
27946
  return `${start}, and no condition of its WHERE compares a column of ${table.name} with = to a value from outside it, so each run reads all of ${table.name}. Compare a column of ${table.name} with = (or IN) and index that column \u2014 ${override}.`;
26630
27947
  const column2 = way.column.name;
@@ -26664,12 +27981,12 @@ function unindexedSubqueries(snapshot, indexes, policies) {
26664
27981
  const ways = waysIn(inner, expr.subquery.where);
26665
27982
  const reasons = ways.map((way) => blocked(table, indexes, way));
26666
27983
  if (!reasons.includes(void 0)) {
26667
- const order = ["none", "collation", "affinity"];
27984
+ const order = table.name === STORAGE_OBJECTS_TABLE ? ["collation", "affinity", "none"] : ["none", "collation", "affinity"];
26668
27985
  const best = order.map((reason) => reasons.indexOf(reason)).find((index) => index >= 0);
26669
27986
  const way = best === void 0 ? void 0 : ways[best];
26670
27987
  found.push({
26671
27988
  policy,
26672
- table: table.name,
27989
+ table: tableLabel(table),
26673
27990
  message: problemMessage(
26674
27991
  policy,
26675
27992
  outer,
@@ -26863,6 +28180,14 @@ function privateChannelAccess(input) {
26863
28180
  return { ok: true, statement: { sql, params } };
26864
28181
  }
26865
28182
 
28183
+ // ../../packages/data-core/src/storage-transfer.ts
28184
+ function moveToPending(to) {
28185
+ return JSON.stringify({ moveTo: to });
28186
+ }
28187
+ function moveFromPending(from) {
28188
+ return JSON.stringify({ moveFrom: from });
28189
+ }
28190
+
26866
28191
  // ../../packages/data-core/src/storage-sql.ts
26867
28192
  var STORAGE_NONCE_MAX_DIGITS = 64;
26868
28193
  function nonceOf(nonce) {
@@ -26992,6 +28317,73 @@ function claimOverwrite(i) {
26992
28317
  [[i.lockId, i.expiresAt, i.pending, i.bucket, i.name], room.values]
26993
28318
  );
26994
28319
  }
28320
+ var MOVE_KEPT_COLUMNS = [
28321
+ "id",
28322
+ "bucket_id",
28323
+ "owner",
28324
+ "owner_id",
28325
+ "version",
28326
+ "created_at",
28327
+ "updated_at",
28328
+ "metadata"
28329
+ ];
28330
+ function moveCheckRow(i) {
28331
+ return {
28332
+ row: { name: i.to },
28333
+ stored: { alias: STORAGE_OBJECT_ALIAS, columns: MOVE_KEPT_COLUMNS }
28334
+ };
28335
+ }
28336
+ function claimMove(i) {
28337
+ const nonce = nonceOf(i.nonce);
28338
+ const slotNonce = nonceOf(i.slotNonce);
28339
+ const source = compose(
28340
+ [
28341
+ `UPDATE ${STORAGE_OBJECTS_TABLE} AS o SET lock_id = ?, lock_expires_at = ?, pending = ? WHERE o.id = ? AND o.name = ? AND CASE WHEN NOT (`,
28342
+ i.selectUsing,
28343
+ `) THEN ${marker("hidden", nonce)} WHEN NOT EXISTS (SELECT 1 FROM ${STORAGE_BUCKETS_TABLE} AS b WHERE b.id = o.bucket_id AND b.${LIVE_BUCKET}) THEN ${marker("bucket", nonce)} WHEN o.lock_id IS NOT NULL OR o.version IS NOT ? THEN ${marker("busy", nonce)} WHEN NOT (`,
28344
+ i.updateUsing,
28345
+ `) THEN ${marker("denied", nonce)} WHEN (`,
28346
+ i.check,
28347
+ // The check is 1 or fails with its marker; anything else is no
28348
+ // check that held — refused as the update policy refuses.
28349
+ `) <> 1 THEN ${marker("denied", nonce)} WHEN ${PROJECT_BYTES} + o.size > ? THEN ${marker("quota", nonce)} ELSE 1 END RETURNING id, size`
28350
+ ],
28351
+ [
28352
+ [i.lockId, i.expiresAt, moveToPending(i.to), i.id, i.from],
28353
+ [i.version],
28354
+ [i.limitBytes]
28355
+ ]
28356
+ );
28357
+ const slot = compose(
28358
+ [
28359
+ `INSERT INTO ${STORAGE_OBJECTS_TABLE} (id, bucket_id, name, owner, owner_id, metadata, version, parent, size, status, lock_id, lock_expires_at, pending) SELECT ?, s.bucket_id, ?, NULL, NULL, '{}', NULL, ?, s.size, 'uploading', s.lock_id, s.lock_expires_at, ? FROM ${STORAGE_OBJECTS_TABLE} AS s WHERE s.id = ? AND s.lock_id = ? AND CASE WHEN EXISTS (SELECT 1 FROM ${STORAGE_OBJECTS_TABLE} AS d WHERE d.bucket_id = s.bucket_id AND d.name = ? AND d.status = 'ready') THEN ${marker("exists", slotNonce)} WHEN EXISTS (SELECT 1 FROM ${STORAGE_OBJECTS_TABLE} AS d WHERE d.bucket_id = s.bucket_id AND d.name = ?) THEN ${marker("busy", slotNonce)} ELSE 1 END RETURNING id`
28360
+ ],
28361
+ [
28362
+ [
28363
+ i.slotId,
28364
+ i.to,
28365
+ objectParent(i.to),
28366
+ moveFromPending(i.from),
28367
+ i.id,
28368
+ i.lockId,
28369
+ i.to,
28370
+ i.to
28371
+ ]
28372
+ ]
28373
+ );
28374
+ return [source, slot];
28375
+ }
28376
+ function copySourceGuard(i) {
28377
+ const nonce = nonceOf(i.nonce);
28378
+ return compose(
28379
+ [
28380
+ `SELECT CASE WHEN EXISTS (SELECT 1 FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.id = ? AND o.name = ? AND (`,
28381
+ i.using,
28382
+ `)) THEN 1 ELSE ${marker("source", nonce)} END AS ok`
28383
+ ],
28384
+ [[i.id, i.name]]
28385
+ );
28386
+ }
26995
28387
  function claimDelete(i) {
26996
28388
  return compose(
26997
28389
  [
@@ -27023,6 +28415,16 @@ function readObjects(i) {
27023
28415
  [[i.bucket, JSON.stringify([...new Set(i.names)])]]
27024
28416
  );
27025
28417
  }
28418
+ function readTransferSource(i) {
28419
+ return compose(
28420
+ [
28421
+ `SELECT o.id, o.version, o.size, o.metadata FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND o.name = ? AND (`,
28422
+ i.using,
28423
+ ")"
28424
+ ],
28425
+ [[i.bucket, i.name]]
28426
+ );
28427
+ }
27026
28428
  var SORT_COLUMNS = {
27027
28429
  name: "name",
27028
28430
  created_at: "created_at",
@@ -27114,7 +28516,7 @@ function storageUsing(input) {
27114
28516
  assertFragmentAlias(alias, "storageUsing");
27115
28517
  const on = target(snapshot);
27116
28518
  if ("ok" in on) return on;
27117
- const status = `(${quoteIdentifier(alias)}.${quoteIdentifier("status")} = 'ready')`;
28519
+ const status = storageReadySql(alias);
27118
28520
  if (context.role === "service")
27119
28521
  return { ok: true, fragment: { sql: status, params: [] } };
27120
28522
  try {
@@ -27204,12 +28606,12 @@ var CONTEXTS = [
27204
28606
  userId: "naro-policy-check",
27205
28607
  email: "check@naro.invalid",
27206
28608
  emailVerified: true
27207
- }
28609
+ },
28610
+ { role: "authenticated", userId: "naro-policy-check" }
27208
28611
  ];
27209
28612
  function probes(table, context, owners) {
27210
28613
  const writable = table.columns.filter((column2) => !column2.generated);
27211
- const [first] = table.columns;
27212
- const filters = first ? [{ operator: "is", column: first.name, value: null }] : [];
28614
+ const filters = leastFilters(table.columns[0]?.name);
27213
28615
  const row = Object.fromEntries(
27214
28616
  writable.map((column2) => [
27215
28617
  column2.name,
@@ -27217,7 +28619,7 @@ function probes(table, context, owners) {
27217
28619
  ])
27218
28620
  );
27219
28621
  const queries = [
27220
- { version: 1, action: "select", table: table.name, columns: "*" },
28622
+ { version: 1, action: "select", table: table.name, columns: "*", filters },
27221
28623
  { version: 1, action: "insert", table: table.name, rows: [row] },
27222
28624
  {
27223
28625
  version: 1,
@@ -27240,16 +28642,35 @@ function probes(table, context, owners) {
27240
28642
  }
27241
28643
  var STORAGE_PROBE_ALIAS = "o";
27242
28644
  var STORAGE_PROBE_NONCE = "f".repeat(STORAGE_NONCE_MAX_DIGITS);
27243
- var STORAGE_CONTEXTS = [
27244
- ...CONTEXTS,
27245
- { role: "authenticated", userId: "naro-policy-check" }
27246
- ];
27247
- function storageRefusals(snapshot, table, policies) {
28645
+ var UNVERIFIED = {
28646
+ role: "authenticated",
28647
+ userId: "naro-policy-check",
28648
+ emailVerified: false
28649
+ };
28650
+ function depthContexts(contexts, claims) {
28651
+ if (claims) return [...contexts, UNVERIFIED];
28652
+ return contexts.filter(
28653
+ (context) => context.role !== "authenticated" || context.emailVerified === true
28654
+ );
28655
+ }
28656
+ function readsClaims(policies) {
28657
+ return policies.some(
28658
+ (policy) => [policy.using, policy.check].some(
28659
+ (text2) => text2 !== null && /auth\s*\.\s*jwt\s*\(/i.test(text2)
28660
+ )
28661
+ );
28662
+ }
28663
+ function callerName(context) {
28664
+ if (context.role !== "authenticated") return context.role;
28665
+ if (context.emailVerified === true) return "authenticated";
28666
+ return context.emailVerified === false ? "authenticated with no verified email" : "authenticated with no email claims";
28667
+ }
28668
+ function storageRefusals(snapshot, table, policies, measuring, measured) {
27248
28669
  const refusals = [];
27249
28670
  const row = Object.fromEntries(
27250
28671
  table.columns.map((column2) => [column2.name, ""])
27251
28672
  );
27252
- for (const context of STORAGE_CONTEXTS) {
28673
+ for (const context of /* @__PURE__ */ new Set([...CONTEXTS, ...measuring])) {
27253
28674
  const using = (commands) => {
27254
28675
  const result = storageUsing({
27255
28676
  snapshot,
@@ -27285,25 +28706,33 @@ function storageRefusals(snapshot, table, policies) {
27285
28706
  overwriteCheckRow({ metadata: "", now: "", version: "" }),
27286
28707
  true
27287
28708
  );
27288
- for (const statement2 of storageStatements(
28709
+ const moveCheck = check2("update", moveCheckRow({ to: "" }), true);
28710
+ for (const { label: label2, statement: statement2 } of storageStatements(
27289
28711
  select,
27290
28712
  update,
27291
28713
  remove,
27292
28714
  [insertCheck, upsertCheck],
27293
- updateCheck
28715
+ updateCheck,
28716
+ moveCheck
27294
28717
  )) {
27295
28718
  const problem = storageStatementProblem(statement2, "select");
27296
28719
  if (problem) refusals.push(problem);
28720
+ else if (measuring.includes(context))
28721
+ measured(label2, statement2.sql, callerName(context));
27297
28722
  }
27298
28723
  }
27299
28724
  return refusals;
27300
28725
  }
27301
- function storageStatements(select, update, remove, insertChecks, updateCheck) {
28726
+ function storageStatements(select, update, remove, insertChecks, updateCheck, moveCheck) {
27302
28727
  const statements = [];
28728
+ const add = (label2, ...made) => {
28729
+ for (const statement2 of made) statements.push({ label: label2, statement: statement2 });
28730
+ };
27303
28731
  const value = "";
27304
- for (const insertCheck of insertChecks)
28732
+ for (const [index, insertCheck] of insertChecks.entries())
27305
28733
  if (insertCheck)
27306
- statements.push(
28734
+ add(
28735
+ index === 0 ? "an upload's claim" : "an upsert's claim",
27307
28736
  claimUpload({
27308
28737
  id: value,
27309
28738
  bucket: value,
@@ -27319,7 +28748,8 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27319
28748
  })
27320
28749
  );
27321
28750
  if (select && update && updateCheck)
27322
- statements.push(
28751
+ add(
28752
+ "an overwrite's claim",
27323
28753
  claimOverwrite({
27324
28754
  bucket: value,
27325
28755
  name: value,
@@ -27334,8 +28764,28 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27334
28764
  nonce: STORAGE_PROBE_NONCE
27335
28765
  })
27336
28766
  );
28767
+ if (select && update && moveCheck)
28768
+ add(
28769
+ "a move's claim",
28770
+ ...claimMove({
28771
+ id: value,
28772
+ version: value,
28773
+ from: value,
28774
+ to: value,
28775
+ slotId: value,
28776
+ lockId: value,
28777
+ expiresAt: value,
28778
+ limitBytes: 0,
28779
+ selectUsing: select,
28780
+ updateUsing: update,
28781
+ check: moveCheck,
28782
+ nonce: STORAGE_PROBE_NONCE,
28783
+ slotNonce: STORAGE_PROBE_NONCE
28784
+ })
28785
+ );
27337
28786
  if (remove)
27338
- statements.push(
28787
+ add(
28788
+ "a delete's claim",
27339
28789
  claimDelete({
27340
28790
  bucket: value,
27341
28791
  names: [],
@@ -27344,13 +28794,27 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27344
28794
  expiresAt: value
27345
28795
  })
27346
28796
  );
27347
- if (select)
27348
- statements.push(
27349
- readObject2({ bucket: value, name: value, using: select }),
27350
- // Several names at once (3-1g): as many values as readObject's, 32
27351
- // bytes more — the listing below is always wider, but the worker
27352
- // builds this one too.
27353
- readObjects({ bucket: value, names: [], using: select }),
28797
+ if (select) {
28798
+ add("a read", readObject2({ bucket: value, name: value, using: select }));
28799
+ add(
28800
+ "a read of several objects",
28801
+ readObjects({ bucket: value, names: [], using: select })
28802
+ );
28803
+ add(
28804
+ "a move's or a copy's read",
28805
+ readTransferSource({ bucket: value, name: value, using: select })
28806
+ );
28807
+ add(
28808
+ "a copy's guard",
28809
+ copySourceGuard({
28810
+ id: value,
28811
+ name: value,
28812
+ using: select,
28813
+ nonce: STORAGE_PROBE_NONCE
28814
+ })
28815
+ );
28816
+ add(
28817
+ "a listing",
27354
28818
  listObjects({
27355
28819
  bucket: value,
27356
28820
  prefix: "p",
@@ -27361,6 +28825,7 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27361
28825
  using: select
27362
28826
  })
27363
28827
  );
28828
+ }
27364
28829
  return statements;
27365
28830
  }
27366
28831
  var REALTIME_PROBE_CHANNEL = "naro-policy-check";
@@ -27373,13 +28838,13 @@ function joinReason(reason) {
27373
28838
  return JOIN_STATEMENT;
27374
28839
  return reason;
27375
28840
  }
27376
- function realtimeRefusals(snapshot, policies) {
28841
+ function realtimeRefusals(snapshot, policies, measuring, measured) {
27377
28842
  const refusals = [];
27378
28843
  const applying = (command) => policies.filter(
27379
28844
  (policy) => asciiLower(policy.table) !== REALTIME_MESSAGES || policy.command === command || policy.command === "all"
27380
28845
  );
27381
28846
  for (const set2 of [policies, applying("select"), applying("insert")])
27382
- for (const context of STORAGE_CONTEXTS) {
28847
+ for (const context of /* @__PURE__ */ new Set([...CONTEXTS, ...measuring])) {
27383
28848
  const plan = privateChannelAccess({
27384
28849
  snapshot,
27385
28850
  context,
@@ -27387,7 +28852,11 @@ function realtimeRefusals(snapshot, policies) {
27387
28852
  channel: REALTIME_PROBE_CHANNEL,
27388
28853
  nonce: REALTIME_PROBE_NONCE
27389
28854
  });
27390
- if (plan.ok) continue;
28855
+ if (plan.ok) {
28856
+ if ("statement" in plan && measuring.includes(context))
28857
+ measured("a join", plan.statement.sql, callerName(context));
28858
+ continue;
28859
+ }
27391
28860
  const reason = joinReason(plan.reason);
27392
28861
  refusals.push(
27393
28862
  reason === void 0 ? { error: plan.error } : { error: plan.error, reason }
@@ -27429,9 +28898,48 @@ function namedInRefusal(message, policies) {
27429
28898
  }
27430
28899
  return found;
27431
28900
  }
27432
- function problems(snapshot, policies, options) {
28901
+ function probeLabel(query) {
28902
+ switch (query.action) {
28903
+ case "select":
28904
+ return "a select";
28905
+ case "insert":
28906
+ return query.returning === void 0 ? "an insert" : "an insert with returning";
28907
+ case "update":
28908
+ return "an update";
28909
+ case "delete":
28910
+ return "a delete";
28911
+ }
28912
+ }
28913
+ function inlinedTables(sql, probed) {
28914
+ const names = /* @__PURE__ */ new Set();
28915
+ for (const [, written] of sql.matchAll(
28916
+ /FROM ("(?:[^"]|"")*"|[A-Za-z_][A-Za-z0-9_$]*) AS "?_naro_s\d+"?/g
28917
+ )) {
28918
+ if (written === void 0) continue;
28919
+ const name = written.startsWith('"') ? written.slice(1, -1).replaceAll('""', '"') : written;
28920
+ const label2 = name === STORAGE_OBJECTS_TABLE ? STORAGE_OBJECTS : name;
28921
+ if (label2 !== probed) names.add(label2);
28922
+ }
28923
+ return [...names];
28924
+ }
28925
+ function listed(names) {
28926
+ if (names.length < 2) return names.join("");
28927
+ return `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
28928
+ }
28929
+ function tooDeepMessage({
28930
+ table,
28931
+ label: label2,
28932
+ caller,
28933
+ depth,
28934
+ inlined
28935
+ }) {
28936
+ const others = inlined.length === 0 ? "" : `, with those of ${listed(inlined)} that they read,`;
28937
+ return `The policies of ${table}${others} nest an expression more than ${String(D1_EXPRESSION_DEPTH)} deep into one statement: ${label2} as ${caller} is ${String(depth)} deep.`;
28938
+ }
28939
+ function problems(snapshot, policies, options, claims) {
27433
28940
  const found = /* @__PURE__ */ new Map();
27434
28941
  const loaded = loadNamed(policies);
28942
+ const measuring = depthContexts(CONTEXTS, claims ?? readsClaims(loaded));
27435
28943
  for (const policy of loaded)
27436
28944
  if (policy.invalid !== null)
27437
28945
  found.set(`read:${policy.table}:${policy.name}`, {
@@ -27493,24 +29001,30 @@ function problems(snapshot, policies, options) {
27493
29001
  }
27494
29002
  }
27495
29003
  const probe = loadNamed(
27496
- [...tables.values()].map((table) => ({
27497
- table: table.name,
27498
- name: "naro_policy_check",
27499
- permissive: true,
27500
- command: "select",
27501
- roles: ["anon", "authenticated"],
27502
- using: "true",
27503
- check: null,
27504
- owner: null
27505
- }))
29004
+ [...tables.values()].flatMap((table) => {
29005
+ const roles = POLICY_ROLES.filter(
29006
+ (role) => applicablePolicies(loaded, table.name, "select", role).permissive.length === 0
29007
+ );
29008
+ return roles.length === 0 ? [] : [
29009
+ {
29010
+ table: table.name,
29011
+ name: "naro_policy_check",
29012
+ permissive: true,
29013
+ command: "select",
29014
+ roles,
29015
+ using: "true",
29016
+ check: null,
29017
+ owner: null
29018
+ }
29019
+ ];
29020
+ })
27506
29021
  );
27507
- const compiled = [...loaded, ...probe];
27508
29022
  const refused = (probed, error51, reason) => {
27509
29023
  if (error51.code !== "POLICY_INVALID") return;
27510
- const message = reason === void 0 ? error51.message : `${error51.message.replace(/\.$/, "")}: ${reason}.`;
29024
+ const message = reason === STORAGE_UNAVAILABLE_REASON ? STORAGE_UNAVAILABLE_MESSAGE : reason === void 0 ? error51.message : `${error51.message.replace(/\.$/, "")}: ${reason}.`;
27511
29025
  const named = namedInRefusal(error51.message, loaded);
27512
29026
  const blamed2 = named === void 0 ? probed : tableLabelOf(snapshot, named.table);
27513
- const key = `${blamed2}:${message}`;
29027
+ const key = reason === STORAGE_UNAVAILABLE_REASON ? `${blamed2}:${named?.name ?? ""}:${message}` : `${blamed2}:${message}`;
27514
29028
  if (!found.has(key))
27515
29029
  found.set(key, {
27516
29030
  problem: {
@@ -27522,12 +29036,29 @@ function problems(snapshot, policies, options) {
27522
29036
  policy: ""
27523
29037
  });
27524
29038
  };
29039
+ const deepest = /* @__PURE__ */ new Map();
29040
+ const measured = (table) => (label2, sql, caller) => {
29041
+ const depth = expressionDepth(sql);
29042
+ const key = `depth:${table}:${label2}:${caller}`;
29043
+ if (depth !== void 0 && depth > (deepest.get(key)?.depth ?? D1_EXPRESSION_DEPTH))
29044
+ deepest.set(key, {
29045
+ table,
29046
+ label: label2,
29047
+ caller,
29048
+ depth,
29049
+ inlined: inlinedTables(sql, table)
29050
+ });
29051
+ };
27525
29052
  for (const table of tables.values()) {
27526
29053
  const owners = new Set(
27527
29054
  policiesOn(loaded, table.name).flatMap(
27528
29055
  (policy) => policy.owner ? [asciiLower(policy.owner)] : []
27529
29056
  )
27530
29057
  );
29058
+ const compiled = [
29059
+ ...loaded,
29060
+ ...probe.filter((policy) => policy.table === table.name)
29061
+ ];
27531
29062
  for (const context of CONTEXTS)
27532
29063
  for (const query of probes(table, context, owners)) {
27533
29064
  const decision = decideQuery(context, query, compiled, table.name);
@@ -27538,13 +29069,27 @@ function problems(snapshot, policies, options) {
27538
29069
  };
27539
29070
  if (!result.ok) refused(table.name, result.error, result.reason);
27540
29071
  }
29072
+ for (const context of measuring)
29073
+ for (const query of probes(table, context, owners)) {
29074
+ const real = decideQuery(context, query, loaded, table.name);
29075
+ const made = real.allow ? compile(query, snapshot, real, context) : void 0;
29076
+ if (made?.ok)
29077
+ for (const statement2 of made.statements)
29078
+ measured(table.name)(
29079
+ probeLabel(query),
29080
+ statement2.sql,
29081
+ callerName(context)
29082
+ );
29083
+ }
27541
29084
  }
27542
29085
  if (policiesOn(loaded, STORAGE_OBJECTS).length > 0) {
27543
29086
  if (snapshot.storage)
27544
29087
  for (const { error: error51, reason } of storageRefusals(
27545
29088
  snapshot,
27546
29089
  snapshot.storage,
27547
- loaded
29090
+ loaded,
29091
+ measuring,
29092
+ measured(STORAGE_OBJECTS)
27548
29093
  ))
27549
29094
  refused(STORAGE_OBJECTS, error51, reason);
27550
29095
  else
@@ -27558,8 +29103,22 @@ function problems(snapshot, policies, options) {
27558
29103
  });
27559
29104
  }
27560
29105
  if (policiesOn(loaded, REALTIME_MESSAGES).length > 0)
27561
- for (const { error: error51, reason } of realtimeRefusals(snapshot, loaded))
29106
+ for (const { error: error51, reason } of realtimeRefusals(
29107
+ snapshot,
29108
+ loaded,
29109
+ measuring,
29110
+ measured(REALTIME_MESSAGES)
29111
+ ))
27562
29112
  refused(REALTIME_MESSAGES, error51, reason);
29113
+ for (const [key, tooDeep] of deepest)
29114
+ found.set(key, {
29115
+ problem: {
29116
+ table: tooDeep.table,
29117
+ message: tooDeepMessage(tooDeep),
29118
+ kind: "compile"
29119
+ },
29120
+ policy: ""
29121
+ });
27563
29122
  if (options.indexes && options.allowUnindexed !== true)
27564
29123
  for (const problem of unindexedSubqueries(
27565
29124
  snapshot,
@@ -27877,10 +29436,10 @@ function isBlankStart(char) {
27877
29436
  function isBlank(char) {
27878
29437
  return isBlankStart(char) || char === "\v";
27879
29438
  }
27880
- function isWordStart(char) {
29439
+ function isWordStart2(char) {
27881
29440
  return /[A-Za-z_]/.test(char) || char.charCodeAt(0) >= 128;
27882
29441
  }
27883
- function isWordPart(char) {
29442
+ function isWordPart2(char) {
27884
29443
  return /[A-Za-z0-9_$]/.test(char) || char.charCodeAt(0) >= 128;
27885
29444
  }
27886
29445
  function skipBlanks(text2, from) {
@@ -27934,9 +29493,9 @@ function headTokens(text2) {
27934
29493
  index = close + 1;
27935
29494
  } else if ((char === "x" || char === "X") && text2.charAt(index + 1) === "'") {
27936
29495
  break;
27937
- } else if (isWordStart(char)) {
29496
+ } else if (isWordStart2(char)) {
27938
29497
  let end = index + 1;
27939
- while (end < text2.length && isWordPart(text2.charAt(end))) end++;
29498
+ while (end < text2.length && isWordPart2(text2.charAt(end))) end++;
27940
29499
  tokens.push({ kind: "word", text: text2.slice(index, end) });
27941
29500
  index = end;
27942
29501
  } else if (char === "." && !/[0-9]/.test(text2.charAt(index + 1))) {
@@ -27952,6 +29511,10 @@ var NO_NAME = {
27952
29511
  kind: "unreadable",
27953
29512
  reason: "the table name could not be read"
27954
29513
  };
29514
+ var NO_COLUMN = {
29515
+ kind: "unreadable",
29516
+ reason: "the column name could not be read"
29517
+ };
27955
29518
  var OTHER_SCHEMA = {
27956
29519
  kind: "unreadable",
27957
29520
  reason: "a table in another schema"
@@ -27977,8 +29540,9 @@ var Head = class {
27977
29540
  return token.kind === "word" || token.kind === "name" ? token.text : void 0;
27978
29541
  }
27979
29542
  /**
27980
- * SQLite's `fullname`. `main.` is the database a migration writes and is
27981
- * dropped; `temp.` (in any case, quoted or not, as sqlite3FindDbName
29543
+ * SQLite's `fullname`. `main.` is the database a migration writes: the
29544
+ * name goes without it, and `main` says it was written (known-issues
29545
+ * #226); `temp.` (in any case, quoted or not, as sqlite3FindDbName
27982
29546
  * matches it) is a table that goes with the connection; `public.` is
27983
29547
  * what a Postgres migration writes, said in its own words; any other
27984
29548
  * schema is one D1 does not have.
@@ -27987,17 +29551,21 @@ var Head = class {
27987
29551
  const first = this.nameAt(at);
27988
29552
  if (first === void 0) return { ok: false, head: NO_NAME };
27989
29553
  if (this.token(at + 1).kind !== "dot")
27990
- return { ok: true, name: first, next: at + 1 };
29554
+ return { ok: true, name: first, next: at + 1, main: false };
27991
29555
  const second = this.nameAt(at + 2);
27992
29556
  if (second === void 0) return { ok: false, head: NO_NAME };
27993
29557
  const schema = asciiLower(first);
27994
- if (schema === "main") return { ok: true, name: second, next: at + 3 };
29558
+ if (schema === "main")
29559
+ return { ok: true, name: second, next: at + 3, main: true };
27995
29560
  let head = OTHER_SCHEMA;
27996
29561
  if (schema === "temp") head = OTHER;
27997
29562
  else if (schema === "public") head = PUBLIC_SCHEMA;
27998
29563
  return { ok: false, head, next: at + 3 };
27999
29564
  }
28000
29565
  };
29566
+ function inMain(read) {
29567
+ return read.main ? { main: true } : {};
29568
+ }
28001
29569
  function readCreate2(head) {
28002
29570
  if (head.is(1, "policy")) return { kind: "create-policy" };
28003
29571
  if (head.is(1, "temp") || head.is(1, "temporary")) return OTHER;
@@ -28021,9 +29589,24 @@ function readDrop2(head) {
28021
29589
  at += 2;
28022
29590
  }
28023
29591
  const table = head.table(at);
28024
- return table.ok ? { kind: "drop-table", table: table.name } : table.head;
29592
+ return table.ok ? { kind: "drop-table", table: table.name, ...inMain(table) } : table.head;
28025
29593
  }
28026
29594
  var RLS_ACTIONS = ["enable", "disable", "force", "no"];
29595
+ function readRenameColumn(head, table, at) {
29596
+ let from = at;
29597
+ if (head.is(from, "column") && head.nameAt(from + 1) !== void 0 && head.is(from + 2, "to"))
29598
+ from += 1;
29599
+ const column2 = head.nameAt(from);
29600
+ if (column2 === void 0 || !head.is(from + 1, "to")) return NO_COLUMN;
29601
+ const to = head.nameAt(from + 2);
29602
+ return to === void 0 ? NO_COLUMN : {
29603
+ kind: "rename-column",
29604
+ table: table.name,
29605
+ column: column2,
29606
+ to,
29607
+ ...inMain(table)
29608
+ };
29609
+ }
28027
29610
  function readAlter(head) {
28028
29611
  if (head.is(1, "policy")) return { kind: "alter-policy" };
28029
29612
  if (!head.is(1, "table")) return OTHER;
@@ -28033,9 +29616,18 @@ function readAlter(head) {
28033
29616
  if (head.is(action, "rename") && head.is(action + 1, "to")) {
28034
29617
  if (!sqlite.ok) return sqlite.head;
28035
29618
  const to = head.table(action + 2);
28036
- return to.ok ? { kind: "rename-table", table: sqlite.name, to: to.name } : to.head;
29619
+ return to.ok ? {
29620
+ kind: "rename-table",
29621
+ table: sqlite.name,
29622
+ to: to.name,
29623
+ ...inMain(sqlite)
29624
+ } : to.head;
29625
+ }
29626
+ if (head.is(action, "rename")) {
29627
+ if (!sqlite.ok) return sqlite.head;
29628
+ return readRenameColumn(head, sqlite, action + 1);
28037
29629
  }
28038
- if (head.is(action, "rename") || head.is(action, "add") || head.is(action, "drop"))
29630
+ if (head.is(action, "add") || head.is(action, "drop"))
28039
29631
  return sqlite.ok ? { kind: "alter-table", table: sqlite.name } : sqlite.head;
28040
29632
  }
28041
29633
  let at = 2;
@@ -28060,6 +29652,130 @@ function readSqlHead(text2) {
28060
29652
  return OTHER;
28061
29653
  }
28062
29654
 
29655
+ // ../../packages/data-core/src/system-sql.ts
29656
+ async function convertLegacyPolicies(db, found) {
29657
+ const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
29658
+ if (rows.length === 0) return [];
29659
+ const read = rows.map((row) => ({
29660
+ policy: policyFromRow(row),
29661
+ asRead: legacyRowAsRead(row)
29662
+ }));
29663
+ const inForce = new Set(
29664
+ legacyInForce(
29665
+ read.flatMap(({ policy }) => policy ? [policy] : []),
29666
+ await readTableNames(db)
29667
+ )
29668
+ );
29669
+ const statements = [];
29670
+ for (const { policy, asRead } of read) {
29671
+ if (!policy || !asRead) continue;
29672
+ if (inForce.has(policy)) {
29673
+ for (const named of namedFromLegacy(policy))
29674
+ statements.push({
29675
+ sql: POLICY_SQL.insertConverted,
29676
+ params: [...policyValues(named), ...asRead]
29677
+ });
29678
+ found.unreadable.push(...unreadableConversions(policy));
29679
+ }
29680
+ statements.push({ sql: POLICY_SQL.markConverted, params: asRead });
29681
+ }
29682
+ return statements;
29683
+ }
29684
+ var MIGRATIONS_TABLE = "CREATE TABLE IF NOT EXISTS _naro_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT (datetime('now')))";
29685
+ var STORAGE_SUBQUERIES_VERSION = 7;
29686
+ var STORAGE_SUBQUERIES_NAME = "storage_subquery_policies";
29687
+ var STORAGE_SUBQUERIES_SQL = `SELECT version FROM _naro_migrations WHERE version = ${String(STORAGE_SUBQUERIES_VERSION)} AND name = '${STORAGE_SUBQUERIES_NAME}'`;
29688
+ var SYSTEM_MIGRATIONS = [
29689
+ {
29690
+ version: 1,
29691
+ name: "system_tables",
29692
+ statements: [
29693
+ MIGRATIONS_TABLE,
29694
+ "CREATE TABLE IF NOT EXISTS _naro_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')))",
29695
+ // Spec §11.2, as written there.
29696
+ "CREATE TABLE IF NOT EXISTS _naro_policies (table_name TEXT PRIMARY KEY, select_rule TEXT NOT NULL DEFAULT 'none', insert_rule TEXT NOT NULL DEFAULT 'none', update_rule TEXT NOT NULL DEFAULT 'none', delete_rule TEXT NOT NULL DEFAULT 'none', owner_column TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')))"
29697
+ ]
29698
+ },
29699
+ {
29700
+ // Spec P2-2 §3.1: an action whose rule column says 'condition' reads its
29701
+ // condition from these. A worker from before them reads 'condition' as
29702
+ // an unknown rule — none.
29703
+ version: 2,
29704
+ name: "policy_conditions",
29705
+ statements: [],
29706
+ addColumns: ["select", "insert", "update", "delete"].map((action) => ({
29707
+ table: "_naro_policies",
29708
+ column: `${action}_condition`,
29709
+ definition: "TEXT"
29710
+ }))
29711
+ },
29712
+ {
29713
+ // Spec P2-6 §11: the Naro worker's sign-in handoff. A provider sign-in's
29714
+ // flow (state → the app's redirect and the browser's PKCE challenge), the
29715
+ // one-time codes a redirect hands the app, the sends the per-address
29716
+ // email limit counts, and (adversarial review I3) the unconfirmed
29717
+ // accounts someone else also tried to sign up as. Times are epoch
29718
+ // milliseconds.
29719
+ version: 3,
29720
+ name: "auth_handoff",
29721
+ statements: [
29722
+ "CREATE TABLE IF NOT EXISTS _naro_auth_flows (state TEXT PRIMARY KEY, provider TEXT NOT NULL, code_challenge TEXT NOT NULL, redirect_to TEXT NOT NULL, expires_at INTEGER NOT NULL)",
29723
+ "CREATE TABLE IF NOT EXISTS _naro_auth_codes (code_hash TEXT PRIMARY KEY, code_challenge TEXT NOT NULL, session_token TEXT NOT NULL, sealed_token TEXT NOT NULL, expires_at INTEGER NOT NULL)",
29724
+ "CREATE TABLE IF NOT EXISTS _naro_auth_email_log (address_hash TEXT NOT NULL, sent_at INTEGER NOT NULL)",
29725
+ "CREATE INDEX IF NOT EXISTS _naro_auth_email_log_address ON _naro_auth_email_log (address_hash, sent_at)",
29726
+ "CREATE TABLE IF NOT EXISTS _naro_auth_contested (user_id TEXT PRIMARY KEY, contested_at INTEGER NOT NULL)"
29727
+ ]
29728
+ },
29729
+ {
29730
+ // P2-7 §9: named policies, many per table. The old rows are converted
29731
+ // in the same batch and their rules set to a word old workers read as
29732
+ // `none`.
29733
+ version: 4,
29734
+ name: "named_policies",
29735
+ statements: [NAMED_POLICY_TABLE_SQL],
29736
+ compute: convertLegacyPolicies
29737
+ },
29738
+ {
29739
+ // Storage (first-release design §6): the buckets and the objects' index
29740
+ // with their counter triggers. Every backend gets them, Storage on or
29741
+ // not — what turns it on is the worker's R2 binding. A worker from
29742
+ // before them never reads them.
29743
+ version: 5,
29744
+ name: "storage",
29745
+ statements: [...STORAGE_TABLES_SQL]
29746
+ },
29747
+ {
29748
+ // Storage 3-1b (known-issues #107, first-release design §14): the mark
29749
+ // a bucket's removal sets before it empties the bucket. The claims and
29750
+ // the worker's bucket reads take a marked bucket as not there. A worker
29751
+ // from before it never reads the column.
29752
+ version: 6,
29753
+ name: "storage_bucket_deleting",
29754
+ statements: [],
29755
+ addColumns: [STORAGE_BUCKET_DELETING_COLUMN]
29756
+ },
29757
+ {
29758
+ // Storage 3-1j (plan correction (p)): no schema — the mark that a worker
29759
+ // which reads storage.objects in a policy's subquery has migrated this
29760
+ // database. Only the Naro worker's own `internal/migrate` applies system
29761
+ // migrations, so the row is there once such a worker has run, and stays:
29762
+ // every later worker reads them too — as long as the worker bundle does
29763
+ // not go back. Deploying web from before 3-1j makes the next upgrade
29764
+ // put an older worker back, and a run that lost its lease may upload
29765
+ // one late; such a policy then answers 500 POLICY_INVALID (closed, no
29766
+ // rows) until it is dropped or the newer worker is uploaded again
29767
+ // (3-1j 최종 리뷰). Migration 5 alone cannot tell —
29768
+ // backends migrated by 3-1a to 3-1i have it, and their worker refuses
29769
+ // such a policy ("A policy cannot read storage.objects in a subquery
29770
+ // yet") and answers every request of its table 500 POLICY_INVALID until
29771
+ // the next upgrade. The control plane takes one only where this row is
29772
+ // (`STORAGE_SUBQUERIES_SQL`, `buildPolicyBatch`).
29773
+ version: STORAGE_SUBQUERIES_VERSION,
29774
+ name: STORAGE_SUBQUERIES_NAME,
29775
+ statements: []
29776
+ }
29777
+ ];
29778
+
28063
29779
  // ../../packages/data-core/src/policy-migration.ts
28064
29780
  var GRANT_REFUSAL = "naro has no GRANT or REVOKE: every table is behind row-level security, so write a policy instead (CREATE POLICY \u2026).";
28065
29781
  function disableRefusal(table) {
@@ -28077,6 +29793,7 @@ function tablesChanged(head) {
28077
29793
  case "create-table":
28078
29794
  case "drop-table":
28079
29795
  case "alter-table":
29796
+ case "rename-column":
28080
29797
  return [asciiLower(head.table)];
28081
29798
  case "rename-table":
28082
29799
  return [asciiLower(head.table), asciiLower(head.to)];
@@ -28104,18 +29821,28 @@ function tablesInScope(policy) {
28104
29821
  }
28105
29822
  return tables;
28106
29823
  }
29824
+ function readsStorageObjects(policy) {
29825
+ return [policy.using, policy.check].some((text2) => {
29826
+ if (text2 === null) return false;
29827
+ const parsed = parseExpression(text2);
29828
+ return parsed.ok && subqueriesOf(parsed.expr).some(
29829
+ (subquery) => isStorageObjects(subquery.table)
29830
+ );
29831
+ });
29832
+ }
28107
29833
  function planPolicyMigration(statements) {
28108
29834
  const count2 = statements.length;
28109
29835
  const planned = [];
28110
29836
  let hasPolicyStatements = false;
28111
29837
  let touchesTables = false;
29838
+ let renamesColumns = false;
28112
29839
  const heads = statements.map((text2) => readSqlHead(text2));
28113
29840
  const changes = heads.map(tablesChanged);
28114
29841
  const changedAfter = (index, tables) => changes.slice(index + 1).some(
28115
29842
  (changed) => changed === "any" || changed.some((table) => tables.has(table))
28116
29843
  );
28117
29844
  for (const [index, text2] of statements.entries()) {
28118
- const refuse = (message) => ({
29845
+ const refuse2 = (message) => ({
28119
29846
  ok: false,
28120
29847
  index,
28121
29848
  message: `Statement ${String(index + 1)} of ${String(count2)}: ${message}`
@@ -28124,15 +29851,19 @@ function planPolicyMigration(statements) {
28124
29851
  switch (head.kind) {
28125
29852
  case "grant":
28126
29853
  case "revoke":
28127
- return refuse(GRANT_REFUSAL);
29854
+ return refuse2(GRANT_REFUSAL);
28128
29855
  case "unreadable":
28129
- return refuse(`${head.reason}.`);
29856
+ return refuse2(`${head.reason}.`);
28130
29857
  case "create-table":
28131
29858
  case "drop-table":
28132
29859
  case "rename-table":
28133
29860
  touchesTables = true;
28134
29861
  planned.push({ kind: "verbatim", text: text2, head });
28135
29862
  continue;
29863
+ case "rename-column":
29864
+ renamesColumns = true;
29865
+ planned.push({ kind: "verbatim", text: text2, head });
29866
+ continue;
28136
29867
  case "alter-table":
28137
29868
  case "other":
28138
29869
  planned.push({ kind: "verbatim", text: text2, head });
@@ -28142,17 +29873,17 @@ function planPolicyMigration(statements) {
28142
29873
  }
28143
29874
  const parsed = parseMigrationPolicyStatement(text2);
28144
29875
  if (!parsed.ok)
28145
- return refuse(
29876
+ return refuse2(
28146
29877
  `${parsed.reason} at character ${String(parsed.position)}.`
28147
29878
  );
28148
29879
  const statement2 = parsed.statement;
28149
29880
  const table = statement2.kind === "create" ? statement2.policy.table : statement2.table;
28150
29881
  if (!isStorageObjects(table) && isSystemTable(table))
28151
- return refuse(`${relationMissing(table)}.`);
29882
+ return refuse2(`${relationMissing(table)}.`);
28152
29883
  switch (statement2.kind) {
28153
29884
  case "create":
28154
29885
  if (readsBareSubqueryColumn(statement2.policy) && changedAfter(index, tablesInScope(statement2.policy)))
28155
- return refuse(LATER_TABLE_CHANGE);
29886
+ return refuse2(LATER_TABLE_CHANGE);
28156
29887
  hasPolicyStatements = true;
28157
29888
  planned.push({ kind: "create-policy", policy: statement2.policy });
28158
29889
  break;
@@ -28180,14 +29911,19 @@ function planPolicyMigration(statements) {
28180
29911
  break;
28181
29912
  case "rls":
28182
29913
  if (statement2.action === "disable")
28183
- return refuse(disableRefusal(table));
29914
+ return refuse2(disableRefusal(table));
28184
29915
  planned.push({ kind: "noop" });
28185
29916
  break;
28186
29917
  }
28187
29918
  }
28188
29919
  return {
28189
29920
  ok: true,
28190
- plan: { statements: planned, hasPolicyStatements, touchesTables }
29921
+ plan: {
29922
+ statements: planned,
29923
+ hasPolicyStatements,
29924
+ touchesTables,
29925
+ renamesColumns
29926
+ }
28191
29927
  };
28192
29928
  }
28193
29929
  function policyMigrationSql(policy) {
@@ -28202,6 +29938,7 @@ function unreadablePolicyComment(policy) {
28202
29938
  var STORED_POLICIES_SQL = `SELECT table_name, name, kind, command, roles, using_expr, check_expr, owner_column, updated_at FROM ${NAMED_POLICY_TABLE} ORDER BY table_name, name`;
28203
29939
  var SYSTEM_PREDICATE = systemTablePredicate("name");
28204
29940
  var STORAGE_EXISTS = `EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${sqlStringLiteral(STORAGE_OBJECTS_TABLE)})`;
29941
+ var STORAGE_SUBQUERIES_READY = `EXISTS (${STORAGE_SUBQUERIES_SQL})`;
28205
29942
 
28206
29943
  // ../../packages/data-core/src/realtime-limits.ts
28207
29944
  var HOBBY_REALTIME_LIMITS = Object.freeze({
@@ -28290,107 +30027,6 @@ var MULTIPART_COMPLETE_LOCK_MS = 15 * 60 * 1e3;
28290
30027
  var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
28291
30028
  var UPLOAD_ID = new RegExp(`^(${UUID})\\.(${UUID})$`);
28292
30029
 
28293
- // ../../packages/data-core/src/system-sql.ts
28294
- async function convertLegacyPolicies(db, found) {
28295
- const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
28296
- if (rows.length === 0) return [];
28297
- const read = rows.map((row) => ({
28298
- policy: policyFromRow(row),
28299
- asRead: legacyRowAsRead(row)
28300
- }));
28301
- const inForce = new Set(
28302
- legacyInForce(
28303
- read.flatMap(({ policy }) => policy ? [policy] : []),
28304
- await readTableNames(db)
28305
- )
28306
- );
28307
- const statements = [];
28308
- for (const { policy, asRead } of read) {
28309
- if (!policy || !asRead) continue;
28310
- if (inForce.has(policy)) {
28311
- for (const named of namedFromLegacy(policy))
28312
- statements.push({
28313
- sql: POLICY_SQL.insertConverted,
28314
- params: [...policyValues(named), ...asRead]
28315
- });
28316
- found.unreadable.push(...unreadableConversions(policy));
28317
- }
28318
- statements.push({ sql: POLICY_SQL.markConverted, params: asRead });
28319
- }
28320
- return statements;
28321
- }
28322
- var MIGRATIONS_TABLE = "CREATE TABLE IF NOT EXISTS _naro_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT (datetime('now')))";
28323
- var SYSTEM_MIGRATIONS = [
28324
- {
28325
- version: 1,
28326
- name: "system_tables",
28327
- statements: [
28328
- MIGRATIONS_TABLE,
28329
- "CREATE TABLE IF NOT EXISTS _naro_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')))",
28330
- // Spec §11.2, as written there.
28331
- "CREATE TABLE IF NOT EXISTS _naro_policies (table_name TEXT PRIMARY KEY, select_rule TEXT NOT NULL DEFAULT 'none', insert_rule TEXT NOT NULL DEFAULT 'none', update_rule TEXT NOT NULL DEFAULT 'none', delete_rule TEXT NOT NULL DEFAULT 'none', owner_column TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')))"
28332
- ]
28333
- },
28334
- {
28335
- // Spec P2-2 §3.1: an action whose rule column says 'condition' reads its
28336
- // condition from these. A worker from before them reads 'condition' as
28337
- // an unknown rule — none.
28338
- version: 2,
28339
- name: "policy_conditions",
28340
- statements: [],
28341
- addColumns: ["select", "insert", "update", "delete"].map((action) => ({
28342
- table: "_naro_policies",
28343
- column: `${action}_condition`,
28344
- definition: "TEXT"
28345
- }))
28346
- },
28347
- {
28348
- // Spec P2-6 §11: the Naro worker's sign-in handoff. A provider sign-in's
28349
- // flow (state → the app's redirect and the browser's PKCE challenge), the
28350
- // one-time codes a redirect hands the app, the sends the per-address
28351
- // email limit counts, and (adversarial review I3) the unconfirmed
28352
- // accounts someone else also tried to sign up as. Times are epoch
28353
- // milliseconds.
28354
- version: 3,
28355
- name: "auth_handoff",
28356
- statements: [
28357
- "CREATE TABLE IF NOT EXISTS _naro_auth_flows (state TEXT PRIMARY KEY, provider TEXT NOT NULL, code_challenge TEXT NOT NULL, redirect_to TEXT NOT NULL, expires_at INTEGER NOT NULL)",
28358
- "CREATE TABLE IF NOT EXISTS _naro_auth_codes (code_hash TEXT PRIMARY KEY, code_challenge TEXT NOT NULL, session_token TEXT NOT NULL, sealed_token TEXT NOT NULL, expires_at INTEGER NOT NULL)",
28359
- "CREATE TABLE IF NOT EXISTS _naro_auth_email_log (address_hash TEXT NOT NULL, sent_at INTEGER NOT NULL)",
28360
- "CREATE INDEX IF NOT EXISTS _naro_auth_email_log_address ON _naro_auth_email_log (address_hash, sent_at)",
28361
- "CREATE TABLE IF NOT EXISTS _naro_auth_contested (user_id TEXT PRIMARY KEY, contested_at INTEGER NOT NULL)"
28362
- ]
28363
- },
28364
- {
28365
- // P2-7 §9: named policies, many per table. The old rows are converted
28366
- // in the same batch and their rules set to a word old workers read as
28367
- // `none`.
28368
- version: 4,
28369
- name: "named_policies",
28370
- statements: [NAMED_POLICY_TABLE_SQL],
28371
- compute: convertLegacyPolicies
28372
- },
28373
- {
28374
- // Storage (first-release design §6): the buckets and the objects' index
28375
- // with their counter triggers. Every backend gets them, Storage on or
28376
- // not — what turns it on is the worker's R2 binding. A worker from
28377
- // before them never reads them.
28378
- version: 5,
28379
- name: "storage",
28380
- statements: [...STORAGE_TABLES_SQL]
28381
- },
28382
- {
28383
- // Storage 3-1b (known-issues #107, first-release design §14): the mark
28384
- // a bucket's removal sets before it empties the bucket. The claims and
28385
- // the worker's bucket reads take a marked bucket as not there. A worker
28386
- // from before it never reads the column.
28387
- version: 6,
28388
- name: "storage_bucket_deleting",
28389
- statements: [],
28390
- addColumns: [STORAGE_BUCKET_DELETING_COLUMN]
28391
- }
28392
- ];
28393
-
28394
30030
  // ../../packages/data-core/src/write-quota.ts
28395
30031
  var WRITE_QUOTA_KEY = "write_quota";
28396
30032
  var WRITE_QUOTA_SQL = {
@@ -28636,12 +30272,12 @@ import {
28636
30272
  rmSync as rmSync2,
28637
30273
  writeFileSync as writeFileSync2
28638
30274
  } from "fs";
28639
- import { join as join22 } from "path";
30275
+ import { join as join23 } from "path";
28640
30276
  function linkDir(dir) {
28641
- return join22(dir, ".naro");
30277
+ return join23(dir, ".naro");
28642
30278
  }
28643
30279
  function linkFile(dir) {
28644
- return join22(linkDir(dir), "project.json");
30280
+ return join23(linkDir(dir), "project.json");
28645
30281
  }
28646
30282
  function readProjectLink(dir) {
28647
30283
  try {
@@ -28663,7 +30299,7 @@ function writeProjectLink(dir, link) {
28663
30299
  writeFileSync2(linkFile(dir), `${JSON.stringify(link, null, 2)}
28664
30300
  `);
28665
30301
  writeFileSync2(
28666
- join22(linkDir(dir), "README.txt"),
30302
+ join23(linkDir(dir), "README.txt"),
28667
30303
  "This directory links the project to Naro and should not be committed.\n"
28668
30304
  );
28669
30305
  }
@@ -28673,9 +30309,9 @@ function removeProjectLink(dir) {
28673
30309
  return true;
28674
30310
  }
28675
30311
  function ensureGitignoreHasNaro(dir) {
28676
- const gitignorePath = join22(dir, ".gitignore");
30312
+ const gitignorePath = join23(dir, ".gitignore");
28677
30313
  const hasGitignore = existsSync4(gitignorePath);
28678
- if (!hasGitignore && !existsSync4(join22(dir, ".git"))) return false;
30314
+ if (!hasGitignore && !existsSync4(join23(dir, ".git"))) return false;
28679
30315
  const current = hasGitignore ? readFileSync5(gitignorePath, "utf8") : "";
28680
30316
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".naro" || line === ".naro/" || line === "/.naro");
28681
30317
  if (covered) return false;
@@ -29128,15 +30764,15 @@ function policiesFromFiles(files) {
29128
30764
  policies = policies.filter((policy) => !sameTable(policy.table, table));
29129
30765
  };
29130
30766
  for (const [file2, sql] of files.entries()) {
29131
- const split = splitStatements(sql);
29132
- if (!split.ok)
30767
+ const split2 = splitStatements(sql);
30768
+ if (!split2.ok)
29133
30769
  return {
29134
30770
  policies,
29135
30771
  tables,
29136
- refused: { file: file2, reason: `sql could not be parsed: ${split.reason}.` }
30772
+ refused: { file: file2, reason: `sql could not be parsed: ${split2.reason}.` }
29137
30773
  };
29138
30774
  const planned = planPolicyMigration(
29139
- split.statements.map((statement2) => statement2.text)
30775
+ split2.statements.map((statement2) => statement2.text)
29140
30776
  );
29141
30777
  if (!planned.ok)
29142
30778
  return {
@@ -29204,6 +30840,31 @@ function policiesLostByReset(input) {
29204
30840
  withTables: gone.filter((policy) => !remade.has(asciiLower(policy.table)))
29205
30841
  };
29206
30842
  }
30843
+ function storageRefusalOfFiles(files, backend) {
30844
+ for (const [file2, sql] of files.entries()) {
30845
+ const split2 = splitStatements(sql);
30846
+ if (!split2.ok) return void 0;
30847
+ const planned = planPolicyMigration(
30848
+ split2.statements.map((statement2) => statement2.text)
30849
+ );
30850
+ if (!planned.ok) return void 0;
30851
+ const count2 = planned.plan.statements.length;
30852
+ for (const [index, statement2] of planned.plan.statements.entries()) {
30853
+ let needs;
30854
+ if (statement2.kind === "create-policy")
30855
+ needs = readsStorageObjects(statement2.policy) ? "subqueries" : sameTable(statement2.policy.table, STORAGE_OBJECTS) ? "storage" : void 0;
30856
+ else if (statement2.kind === "drop-policy" && !statement2.ifExists || statement2.kind === "owner")
30857
+ needs = sameTable(statement2.table, STORAGE_OBJECTS) ? "storage" : void 0;
30858
+ const missing = needs === "subqueries" ? !backend.storage || !backend.storageSubqueries : needs === "storage" && !backend.storage;
30859
+ if (needs !== void 0 && missing)
30860
+ return { file: file2, statement: index + 1, count: count2, needs };
30861
+ }
30862
+ }
30863
+ return void 0;
30864
+ }
30865
+ function resetStorageOutdatedMessage(file2, at) {
30866
+ return `${file2} needs storage.objects (statement ${String(at.statement)} of ${String(at.count)}), which is not available on this backend yet, so the reset would stop there with the schema already dropped. Nothing was changed. Run \`naro backend enable\` to update it, then try again.`;
30867
+ }
29207
30868
  function resetPoliciesLostMessage(lost) {
29208
30869
  return `These ${String(lost.length)} policies are not in your migration files and will be gone after the reset: ${lost.map((policy) => `${policy.table}.${policy.name}`).join(", ")}. To keep them, add this to a migration first:`;
29209
30870
  }
@@ -29282,7 +30943,7 @@ function usageStatus(report, currentMonth, requests) {
29282
30943
 
29283
30944
  // src/env-consent.ts
29284
30945
  import { existsSync as existsSync6 } from "fs";
29285
- import { join as join23 } from "path";
30946
+ import { join as join24 } from "path";
29286
30947
  var DOTENV_FILES = [".env.local", ".env.production", ".env"];
29287
30948
  var PUBLIC_PREFIX = "NEXT_PUBLIC_";
29288
30949
  function maskSecretValue(value) {
@@ -29294,7 +30955,7 @@ function maskEnvValue(key, value) {
29294
30955
  return maskSecretValue(value);
29295
30956
  }
29296
30957
  function findDotenvFile(dir) {
29297
- for (const name of DOTENV_FILES) if (existsSync6(join23(dir, name))) return name;
30958
+ for (const name of DOTENV_FILES) if (existsSync6(join24(dir, name))) return name;
29298
30959
  return null;
29299
30960
  }
29300
30961
  function envAddGuidance(keys, environment) {
@@ -29525,9 +31186,9 @@ import {
29525
31186
  cp as cp2,
29526
31187
  lstat,
29527
31188
  mkdir as mkdir3,
29528
- readFile as readFile13,
31189
+ readFile as readFile14,
29529
31190
  readlink,
29530
- realpath,
31191
+ realpath as realpath3,
29531
31192
  rm as rm4,
29532
31193
  stat as stat3,
29533
31194
  symlink,
@@ -29538,10 +31199,10 @@ import {
29538
31199
  basename as basename3,
29539
31200
  dirname as dirname5,
29540
31201
  isAbsolute as isAbsolute4,
29541
- join as join24,
31202
+ join as join25,
29542
31203
  relative as relative6,
29543
31204
  resolve as resolve10,
29544
- sep as sep4
31205
+ sep as sep5
29545
31206
  } from "path";
29546
31207
  var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo|\.naro)([\\/]|$)/;
29547
31208
  var ENV_FILE = /^\.env(\.(?!(?:example|sample|template)$)[^\\/]+)?$/i;
@@ -29579,7 +31240,7 @@ async function committedEnvFiles(sourceDir) {
29579
31240
  }
29580
31241
  async function staysInside(link, text2, root) {
29581
31242
  try {
29582
- return inside(await realpath(link), root);
31243
+ return inside(await realpath3(link), root);
29583
31244
  } catch (error51) {
29584
31245
  return error51.code === "ENOENT" && !text2.split(/[\\/]/).includes("..");
29585
31246
  }
@@ -29602,12 +31263,12 @@ async function keptOnly(items, keep, remove) {
29602
31263
  async function readLocal(path) {
29603
31264
  const found = await stat3(path).catch(() => null);
29604
31265
  if (!found?.isFile()) return null;
29605
- return readFile13(path).catch(() => null);
31266
+ return readFile14(path).catch(() => null);
29606
31267
  }
29607
31268
  async function realAncestor(dir) {
29608
31269
  for (let current = dir; ; current = dirname5(current)) {
29609
31270
  try {
29610
- return await realpath(current);
31271
+ return await realpath3(current);
29611
31272
  } catch {
29612
31273
  if (dirname5(current) === current)
29613
31274
  throw new Error(`No ancestor of ${dir}`);
@@ -29618,7 +31279,7 @@ function sameText(a, b) {
29618
31279
  return a.equals(b) || a.toString("utf8").replace(/\r\n/g, "\n") === b.toString("utf8").replace(/\r\n/g, "\n");
29619
31280
  }
29620
31281
  async function copyWorkingTree(source, checkout) {
29621
- const sourceDir = await realpath(source);
31282
+ const sourceDir = await realpath3(source);
29622
31283
  const inWorkingTree = [];
29623
31284
  const absoluteLinks = [];
29624
31285
  const linksOutside = [];
@@ -29637,12 +31298,12 @@ async function copyWorkingTree(source, checkout) {
29637
31298
  if (entry.isFIFO() || entry.isSocket() || entry.isCharacterDevice() || entry.isBlockDevice())
29638
31299
  return false;
29639
31300
  if (ENV_FILE.test(basename3(src)) && !entry.isDirectory()) {
29640
- inWorkingTree.push(relative6(sourceDir, src).split(sep4).join("/"));
31301
+ inWorkingTree.push(relative6(sourceDir, src).split(sep5).join("/"));
29641
31302
  return false;
29642
31303
  }
29643
31304
  if (entry.isSymbolicLink()) {
29644
31305
  const target2 = await readlink(src);
29645
- const path = relative6(sourceDir, src).split(sep4).join("/");
31306
+ const path = relative6(sourceDir, src).split(sep5).join("/");
29646
31307
  if (isAbsolute4(target2)) {
29647
31308
  absoluteLinks.push(path);
29648
31309
  return false;
@@ -29660,17 +31321,17 @@ async function copyWorkingTree(source, checkout) {
29660
31321
  return true;
29661
31322
  }
29662
31323
  });
29663
- const root = await realpath(checkout);
31324
+ const root = await realpath3(checkout);
29664
31325
  const strays = await keptOnly(
29665
31326
  keptLinks,
29666
31327
  async ({ path, text: text2 }) => {
29667
31328
  try {
29668
- return inside(await realpath(join24(checkout, path)), root);
31329
+ return inside(await realpath3(join25(checkout, path)), root);
29669
31330
  } catch (error51) {
29670
31331
  return error51.code === "ENOENT" && climbsOnlyFirst(text2);
29671
31332
  }
29672
31333
  },
29673
- ({ path }) => unlink(join24(checkout, path))
31334
+ ({ path }) => unlink(join25(checkout, path))
29674
31335
  );
29675
31336
  linksElsewhere.push(...strays.map(({ path }) => path));
29676
31337
  const committed = await committedEnvFiles(sourceDir);
@@ -29684,7 +31345,7 @@ async function copyWorkingTree(source, checkout) {
29684
31345
  outsideProject.push(path);
29685
31346
  continue;
29686
31347
  }
29687
- const target2 = join24(checkout, path);
31348
+ const target2 = join25(checkout, path);
29688
31349
  if (!inside(await realAncestor(dirname5(target2)), root)) {
29689
31350
  outsideProject.push(path);
29690
31351
  continue;
@@ -29709,16 +31370,16 @@ async function copyWorkingTree(source, checkout) {
29709
31370
  continue;
29710
31371
  }
29711
31372
  await writeFile7(target2, content, { flag: "wx" });
29712
- const local = await readLocal(join24(sourceDir, path));
31373
+ const local = await readLocal(join25(sourceDir, path));
29713
31374
  if (!local || !sameText(local, content)) committedVersion.push(path);
29714
31375
  }
29715
31376
  const envStrays = await keptOnly(
29716
31377
  envLinks,
29717
31378
  async (path) => {
29718
- const real = await realpath(join24(checkout, path)).catch(() => null);
31379
+ const real = await realpath3(join25(checkout, path)).catch(() => null);
29719
31380
  return real !== null && inside(real, root);
29720
31381
  },
29721
- (path) => unlink(join24(checkout, path))
31382
+ (path) => unlink(join25(checkout, path))
29722
31383
  );
29723
31384
  outsideProject.push(...envStrays);
29724
31385
  const inCommit = new Set(
@@ -29835,7 +31496,7 @@ function slugify2(value) {
29835
31496
  async function projectName(sourceDir) {
29836
31497
  try {
29837
31498
  const pkg = JSON.parse(
29838
- await readFile14(join25(sourceDir, "package.json"), "utf8")
31499
+ await readFile15(join26(sourceDir, "package.json"), "utf8")
29839
31500
  );
29840
31501
  if (pkg.name) return pkg.name;
29841
31502
  } catch {
@@ -29845,7 +31506,7 @@ async function projectName(sourceDir) {
29845
31506
  function resolveBin(name) {
29846
31507
  let dir = dirname6(fileURLToPath(import.meta.url));
29847
31508
  for (; ; ) {
29848
- const candidate = join25(dir, "node_modules", ".bin", name);
31509
+ const candidate = join26(dir, "node_modules", ".bin", name);
29849
31510
  if (existsSync7(candidate)) return candidate;
29850
31511
  const parent = dirname6(dir);
29851
31512
  if (parent === dir) return name;
@@ -29912,7 +31573,7 @@ function workingTreeNotices(copy) {
29912
31573
  }
29913
31574
  function localDependencies(sourceDir, say = () => void 0) {
29914
31575
  return {
29915
- createWorkspace: () => mkdtemp2(join25(tmpdir2(), "naro-build-")),
31576
+ createWorkspace: () => mkdtemp2(join26(tmpdir2(), "naro-build-")),
29916
31577
  removeWorkspace: (path) => rm5(path, { recursive: true, force: true }),
29917
31578
  async runCommand(spec, observer) {
29918
31579
  if (spec.command === "git" && spec.args[0] === "clone") {
@@ -29950,7 +31611,12 @@ function localDependencies(sourceDir, say = () => void 0) {
29950
31611
  async function deployDirectory(sourceDir, options, log2) {
29951
31612
  const cpUrl = controlPlaneUrl();
29952
31613
  const production = options.production ?? false;
29953
- const environment = production ? void 0 : options.environment;
31614
+ if (production && options.environment !== void 0)
31615
+ throw new NaroError(
31616
+ "VALIDATION_FAILED",
31617
+ "environment and production cannot be used together."
31618
+ );
31619
+ const environment = options.environment;
29954
31620
  if (environment === "production" || environment === "preview")
29955
31621
  throw new NaroError(
29956
31622
  "VALIDATION_FAILED",
@@ -30147,6 +31813,7 @@ export {
30147
31813
  splitStatements,
30148
31814
  ANALYTICS_FILTER_KEYS,
30149
31815
  NaroError,
31816
+ promoteTargetUnsupported,
30150
31817
  isNaroError,
30151
31818
  toNaroError,
30152
31819
  saveSession,
@@ -30178,12 +31845,14 @@ export {
30178
31845
  cellText,
30179
31846
  renderRows,
30180
31847
  summarizeStatement,
30181
- parseDotenv,
31848
+ parseDotenvDetailed,
30182
31849
  serializeDotenv,
30183
31850
  MAX_SINGLE_UPLOAD_BYTES,
31851
+ checkObjectName,
30184
31852
  parseCacheControl,
30185
31853
  STORAGE_OBJECTS_TABLE,
30186
31854
  systemTablePredicate,
31855
+ dataError,
30187
31856
  SCHEMA_SQL,
30188
31857
  tableNames,
30189
31858
  snapshotFromRows,
@@ -30198,6 +31867,7 @@ export {
30198
31867
  indexesFromRows,
30199
31868
  parsePolicyStatement,
30200
31869
  splitPolicyStatements,
31870
+ STORAGE_SUBQUERIES_SQL,
30201
31871
  planPolicyMigration,
30202
31872
  unreadablePolicyComment,
30203
31873
  parseRedirectEntry,
@@ -30220,6 +31890,8 @@ export {
30220
31890
  RESET_BACKEND_OUTDATED,
30221
31891
  buildResetPlan,
30222
31892
  policiesLostByReset,
31893
+ storageRefusalOfFiles,
31894
+ resetStorageOutdatedMessage,
30223
31895
  resetPoliciesLostMessage,
30224
31896
  resetPoliciesWithTablesMessage,
30225
31897
  resetFileRefusedMessage,
@@ -30230,7 +31902,7 @@ export {
30230
31902
  promptForMissingEnv,
30231
31903
  pullEnv,
30232
31904
  detectGitInfo,
30233
- parseDotenv2,
31905
+ parseDotenv2 as parseDotenv,
30234
31906
  readProjectEnv2 as readProjectEnv,
30235
31907
  eventsObserver,
30236
31908
  slugify2 as slugify,