naro-cli 0.19.0 → 0.19.1

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\-._~%!$&()*+,;=:@/[\]]*$/;
@@ -15613,7 +15613,7 @@ import { access as access5, readFile as readFile5, writeFile as writeFile4 } fro
15613
15613
  import { join as join6, resolve as resolve2 } from "path";
15614
15614
 
15615
15615
  // ../../packages/build/src/open-next.ts
15616
- import { access as access3, readdir, readFile as readFile4 } from "fs/promises";
15616
+ import { access as access3, readdir, readFile as readFile4, realpath } from "fs/promises";
15617
15617
  import {
15618
15618
  basename,
15619
15619
  dirname,
@@ -15729,7 +15729,33 @@ function contentType(path) {
15729
15729
  return "application/octet-stream";
15730
15730
  }
15731
15731
  }
15732
- async function listArtifactFiles(root, label2 = "OpenNext") {
15732
+ async function assertInsideOnDisk(appRoot, directory, label2) {
15733
+ let realRoot;
15734
+ let realDirectory;
15735
+ try {
15736
+ [realRoot, realDirectory] = await Promise.all([
15737
+ realpath(appRoot),
15738
+ realpath(directory)
15739
+ ]);
15740
+ } catch {
15741
+ return;
15742
+ }
15743
+ const relation = relative(realRoot, realDirectory);
15744
+ if (relation === ".." || relation.startsWith(`..${sep}`) || isAbsolute(relation)) {
15745
+ throw new DeploymentError(
15746
+ "ARTIFACT_INVALID",
15747
+ `${label2} output leads out of the application root through a symbolic link.`
15748
+ );
15749
+ }
15750
+ if (relation.split(sep).includes(".git")) {
15751
+ throw new DeploymentError(
15752
+ "ARTIFACT_INVALID",
15753
+ `${label2} output leads into .git through a symbolic link.`
15754
+ );
15755
+ }
15756
+ }
15757
+ async function listArtifactFiles(root, label2 = "OpenNext", appRoot) {
15758
+ if (appRoot !== void 0) await assertInsideOnDisk(appRoot, root, label2);
15733
15759
  const files = [];
15734
15760
  async function visit(directory) {
15735
15761
  const entries = await readdir(directory, { withFileTypes: true });
@@ -15786,7 +15812,6 @@ var OPEN_NEXT_LAYOUT = {
15786
15812
  outputRoot: ".open-next",
15787
15813
  bundleDir: ".open-next/worker-bundle"
15788
15814
  };
15789
- var listFiles = (root) => listArtifactFiles(root);
15790
15815
  async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15791
15816
  const configPath = await findWranglerConfig(appRoot, wranglerConfigPath);
15792
15817
  return readWorkerArtifact(appRoot, identity, {
@@ -15795,6 +15820,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15795
15820
  });
15796
15821
  }
15797
15822
  async function readWorkerArtifact(appRoot, identity, layout) {
15823
+ const listFiles = (root) => listArtifactFiles(root, "OpenNext", appRoot);
15798
15824
  try {
15799
15825
  const configPath = await findWranglerConfig(
15800
15826
  appRoot,
@@ -15839,12 +15865,13 @@ async function readWorkerArtifact(appRoot, identity, layout) {
15839
15865
  );
15840
15866
  }
15841
15867
  const bundleRoot = confinedPath(appRoot, layout.bundleDir, "Bundle dir");
15842
- let bundleFiles = null;
15868
+ let bundleExists = true;
15843
15869
  try {
15844
15870
  await access3(bundleRoot);
15845
- bundleFiles = await listFiles(bundleRoot);
15846
15871
  } catch {
15872
+ bundleExists = false;
15847
15873
  }
15874
+ const bundleFiles = bundleExists ? await listFiles(bundleRoot) : null;
15848
15875
  const outputFiles = bundleFiles ?? await listFiles(outputRoot);
15849
15876
  const entrypointName = bundleFiles ? basename(entrypointPath) : relative(outputRoot, entrypointPath).split(sep).join("/");
15850
15877
  const assetsPrefix = `${relative(outputRoot, configuredAssets).split(sep).join("/")}/`;
@@ -16192,7 +16219,7 @@ async function readVinextArtifact(appRoot, identity) {
16192
16219
  const entrypointName = metadata.main.split(/[\\/]/).join("/");
16193
16220
  const matchers = moduleMatchers(metadata.rules);
16194
16221
  const isModule = (name) => matchers.length === 0 || matchers.some((matcher) => matcher.test(name));
16195
- const serverFiles = await listArtifactFiles(serverRoot, "vinext");
16222
+ const serverFiles = await listArtifactFiles(serverRoot, "vinext", appRoot);
16196
16223
  const entrypoint = serverFiles.find((file2) => file2.name === entrypointName);
16197
16224
  if (!entrypoint) {
16198
16225
  throw new DeploymentError(
@@ -16206,7 +16233,7 @@ async function readVinextArtifact(appRoot, identity) {
16206
16233
  modules: serverFiles.filter(
16207
16234
  (file2) => file2.name !== entrypointName && file2.name !== WRANGLER_OUTPUT_NAME && !file2.name.endsWith(".map") && isModule(file2.name)
16208
16235
  ),
16209
- assets: await listArtifactFiles(clientRoot, "vinext"),
16236
+ assets: await listArtifactFiles(clientRoot, "vinext", appRoot),
16210
16237
  compatibilityDate: metadata.compatibility_date,
16211
16238
  compatibilityFlags: Array.isArray(metadata.compatibility_flags) ? metadata.compatibility_flags.filter(
16212
16239
  (flag) => typeof flag === "string"
@@ -16879,7 +16906,8 @@ async function runCommand(spec, observer) {
16879
16906
  }
16880
16907
 
16881
16908
  // ../../packages/build/src/repository.ts
16882
- import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "path";
16909
+ import { realpath as realpath2 } from "fs/promises";
16910
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
16883
16911
  function resolveAppRoot(checkout, configuredRoot) {
16884
16912
  const checkoutRoot = resolve3(checkout);
16885
16913
  if (!configuredRoot) return checkoutRoot;
@@ -16891,12 +16919,34 @@ function resolveAppRoot(checkout, configuredRoot) {
16891
16919
  if (relativeRoot === ".." || relativeRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute2(relativeRoot)) {
16892
16920
  throw new Error("Configured root directory escapes repository");
16893
16921
  }
16922
+ if (relativeRoot.split(/[\\/]/).includes(".git")) {
16923
+ throw new Error("Configured root directory cannot be inside .git");
16924
+ }
16894
16925
  return appRoot;
16895
16926
  }
16927
+ async function assertAppRootInsideCheckout(checkout, appRoot) {
16928
+ let realCheckout;
16929
+ let realApp;
16930
+ try {
16931
+ [realCheckout, realApp] = await Promise.all([
16932
+ realpath2(checkout),
16933
+ realpath2(appRoot)
16934
+ ]);
16935
+ } catch {
16936
+ return;
16937
+ }
16938
+ const relation = relative2(realCheckout, realApp);
16939
+ if (relation === ".." || relation.startsWith(`..${sep2}`) || isAbsolute2(relation)) {
16940
+ throw new Error("Configured root directory escapes repository");
16941
+ }
16942
+ if (relation.split(sep2).includes(".git")) {
16943
+ throw new Error("Configured root directory cannot be inside .git");
16944
+ }
16945
+ }
16896
16946
 
16897
16947
  // ../../packages/build/src/static-artifact.ts
16898
16948
  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";
16949
+ import { isAbsolute as isAbsolute3, join as join9, relative as relative3, resolve as resolve4, sep as sep3 } from "path";
16900
16950
  var SHIM_WORKER_SOURCE = "export default { fetch: (request, env) => env.ASSETS.fetch(request) };\n";
16901
16951
  var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules"]);
16902
16952
  var ALLOWED_DOTFILE = ".well-known";
@@ -16912,11 +16962,16 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16912
16962
  );
16913
16963
  const outputRoot = resolve4(appRoot, outputDirectory);
16914
16964
  const relation = relative3(appRoot, outputRoot);
16915
- if (relation === ".." || relation.startsWith(`..${sep2}`) || isAbsolute3(relation))
16965
+ if (relation === ".." || relation.startsWith(`..${sep3}`) || isAbsolute3(relation))
16916
16966
  throw new DeploymentError(
16917
16967
  "ARTIFACT_INVALID",
16918
16968
  "Output directory escapes the application root."
16919
16969
  );
16970
+ if (relation.split(sep3).includes(".git"))
16971
+ throw new DeploymentError(
16972
+ "ARTIFACT_INVALID",
16973
+ "Output directory cannot be inside .git."
16974
+ );
16920
16975
  const assets = [];
16921
16976
  async function visit(directory) {
16922
16977
  const entries = await readdir3(directory, { withFileTypes: true });
@@ -16933,7 +16988,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16933
16988
  if (entry.isDirectory()) await visit(path);
16934
16989
  else if (entry.isFile()) {
16935
16990
  assets.push({
16936
- name: relative3(outputRoot, path).split(sep2).join("/"),
16991
+ name: relative3(outputRoot, path).split(sep3).join("/"),
16937
16992
  data: await readFile7(path),
16938
16993
  contentType: contentType(path)
16939
16994
  });
@@ -16941,6 +16996,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16941
16996
  }
16942
16997
  }
16943
16998
  try {
16999
+ await assertInsideOnDisk(appRoot, outputRoot, "Static");
16944
17000
  await visit(outputRoot);
16945
17001
  } catch (error51) {
16946
17002
  if (error51 instanceof DeploymentError) throw error51;
@@ -16980,7 +17036,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16980
17036
  // ../../packages/build/src/strategy/fingerprint.ts
16981
17037
  import { createHash as createHash3 } from "crypto";
16982
17038
  import { readFile as readFile8 } from "fs/promises";
16983
- import { join as join10, relative as relative4, sep as sep3 } from "path";
17039
+ import { join as join10, relative as relative4, sep as sep4 } from "path";
16984
17040
  var APP_ROOT_FILES = [
16985
17041
  "package.json",
16986
17042
  "next.config.js",
@@ -17035,7 +17091,7 @@ async function hashStrategyInputs(checkout, appRoot) {
17035
17091
  for (const path of candidates) {
17036
17092
  const digest = await hashFile(path);
17037
17093
  if (digest === null) continue;
17038
- entries.push([relative4(checkout, path).split(sep3).join("/"), digest]);
17094
+ entries.push([relative4(checkout, path).split(sep4).join("/"), digest]);
17039
17095
  }
17040
17096
  return entries.toSorted(
17041
17097
  ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
@@ -17056,8 +17112,76 @@ async function computeStrategyFingerprint(input) {
17056
17112
  }
17057
17113
 
17058
17114
  // ../../packages/build/src/strategy/project-facts.ts
17059
- import { access as access6, readFile as readFile9 } from "fs/promises";
17115
+ import { access as access6, readFile as readFile10 } from "fs/promises";
17116
+ import { join as join12 } from "path";
17117
+
17118
+ // ../../packages/build/src/strategy/user-adapter.ts
17119
+ import { readFile as readFile9 } from "fs/promises";
17060
17120
  import { join as join11 } from "path";
17121
+ var ADAPTER_PATH = /\badapterPath\b/;
17122
+ var EXPERIMENTAL_ADAPTER_PATH = /\bexperimental\s*:\s*\{[^{}]*\badapterPath\b/;
17123
+ function adapterKeyFromConfigSource(source) {
17124
+ const code = stripComments(source);
17125
+ if (!ADAPTER_PATH.test(code)) return null;
17126
+ return EXPERIMENTAL_ADAPTER_PATH.test(code) ? "experimental.adapterPath" : "adapterPath";
17127
+ }
17128
+ function keysNextReads(nextVersion) {
17129
+ const version2 = parseNextVersion(nextVersion);
17130
+ if (!version2) return ["adapterPath", "experimental.adapterPath"];
17131
+ if (version2.major < 15 || version2.major === 15 && version2.minor < 4)
17132
+ return [];
17133
+ if (version2.major > 16 || version2.major === 16 && version2.minor >= 2)
17134
+ return ["adapterPath", "experimental.adapterPath"];
17135
+ return ["experimental.adapterPath"];
17136
+ }
17137
+ function userAdapterOf(input) {
17138
+ const keys = keysNextReads(input.nextVersion);
17139
+ if (keys.length === 0) return null;
17140
+ const config2 = input.facts.adapterConfig;
17141
+ if (config2 && keys.includes(config2.key))
17142
+ return { source: "next-config", file: config2.file, key: config2.key };
17143
+ if (input.env.NEXT_ADAPTER_PATH) return { source: "env" };
17144
+ return null;
17145
+ }
17146
+ async function adapterInBuildOutput(appRoot, nextVersion) {
17147
+ const keys = keysNextReads(nextVersion);
17148
+ if (keys.length === 0) return null;
17149
+ let manifest2;
17150
+ try {
17151
+ const parsed = JSON.parse(
17152
+ await readFile9(
17153
+ join11(appRoot, ".next", "required-server-files.json"),
17154
+ "utf8"
17155
+ )
17156
+ );
17157
+ if (typeof parsed !== "object" || parsed === null) return null;
17158
+ manifest2 = parsed;
17159
+ } catch {
17160
+ return null;
17161
+ }
17162
+ const topLevel = manifest2.config?.adapterPath;
17163
+ if (keys.includes("adapterPath") && typeof topLevel === "string" && topLevel)
17164
+ return { source: "build-output", key: "adapterPath" };
17165
+ const experimental = manifest2.config?.experimental?.adapterPath;
17166
+ if (keys.includes("experimental.adapterPath") && typeof experimental === "string" && experimental)
17167
+ return { source: "build-output", key: "experimental.adapterPath" };
17168
+ return null;
17169
+ }
17170
+ function describeUserAdapter(adapter) {
17171
+ switch (adapter.source) {
17172
+ case "next-config":
17173
+ return `${adapter.file} ${adapter.key}`;
17174
+ case "env":
17175
+ return "NEXT_ADAPTER_PATH";
17176
+ case "build-output":
17177
+ return `${adapter.key} in the Next.js build output`;
17178
+ }
17179
+ }
17180
+ function openNextAdapterWarning(adapter) {
17181
+ return `P2 opennext-cloudflare: The project sets its own Next.js adapter (${describeUserAdapter(adapter)}); its modifyConfig and onBuildComplete also run in the OpenNext build.`;
17182
+ }
17183
+
17184
+ // ../../packages/build/src/strategy/project-facts.ts
17061
17185
  var SOURCE_EXTENSIONS2 = ["ts", "tsx", "js", "jsx", "mjs", "mts"];
17062
17186
  var SOURCE_DIRECTORIES = ["", "src"];
17063
17187
  var VITE_CONFIGS = [
@@ -17078,7 +17202,7 @@ async function findSourceFile(appRoot, basename5) {
17078
17202
  for (const dir of SOURCE_DIRECTORIES) {
17079
17203
  for (const ext of SOURCE_EXTENSIONS2) {
17080
17204
  const rel = dir ? `${dir}/${basename5}.${ext}` : `${basename5}.${ext}`;
17081
- if (await exists4(join11(appRoot, rel))) return rel;
17205
+ if (await exists4(join12(appRoot, rel))) return rel;
17082
17206
  }
17083
17207
  }
17084
17208
  return null;
@@ -17098,19 +17222,28 @@ function matcherRoutesFromSource(source) {
17098
17222
  }
17099
17223
  async function readIfExists(path) {
17100
17224
  try {
17101
- return await readFile9(path, "utf8");
17225
+ return await readFile10(path, "utf8");
17102
17226
  } catch {
17103
17227
  return null;
17104
17228
  }
17105
17229
  }
17230
+ async function findAdapterConfig(appRoot) {
17231
+ for (const file2 of NEXT_CONFIG_FILES) {
17232
+ const source = await readIfExists(join12(appRoot, file2));
17233
+ if (source === null) continue;
17234
+ const key = adapterKeyFromConfigSource(source);
17235
+ return key ? { file: file2, key } : null;
17236
+ }
17237
+ return null;
17238
+ }
17106
17239
  async function collectProjectFacts(appRoot) {
17107
17240
  const proxyFile = await findSourceFile(appRoot, "proxy");
17108
17241
  const middlewareFile = await findSourceFile(appRoot, "middleware");
17109
17242
  const routeSourceFile = middlewareFile ?? proxyFile;
17110
- const routeSource = routeSourceFile ? await readIfExists(join11(appRoot, routeSourceFile)) : null;
17243
+ const routeSource = routeSourceFile ? await readIfExists(join12(appRoot, routeSourceFile)) : null;
17111
17244
  let usesVinext = false;
17112
17245
  let nextSpec = null;
17113
- const packageJsonSource = await readIfExists(join11(appRoot, "package.json"));
17246
+ const packageJsonSource = await readIfExists(join12(appRoot, "package.json"));
17114
17247
  if (packageJsonSource) {
17115
17248
  try {
17116
17249
  const packageJson = JSON.parse(packageJsonSource);
@@ -17121,7 +17254,7 @@ async function collectProjectFacts(appRoot) {
17121
17254
  nextSpec = dependency("next");
17122
17255
  if (dependency("vinext") !== null) {
17123
17256
  for (const name of VITE_CONFIGS) {
17124
- const config2 = await readIfExists(join11(appRoot, name));
17257
+ const config2 = await readIfExists(join12(appRoot, name));
17125
17258
  if (config2 && /\bvinext\s*\(/.test(config2) && /@cloudflare\/vite-plugin/.test(config2)) {
17126
17259
  usesVinext = true;
17127
17260
  break;
@@ -17136,7 +17269,8 @@ async function collectProjectFacts(appRoot) {
17136
17269
  middlewareFile,
17137
17270
  matcherRoutes: routeSource ? matcherRoutesFromSource(routeSource) : [],
17138
17271
  usesVinext,
17139
- nextSpec
17272
+ nextSpec,
17273
+ adapterConfig: await findAdapterConfig(appRoot)
17140
17274
  };
17141
17275
  }
17142
17276
 
@@ -17523,11 +17657,11 @@ function defaultSmokeProbes(matcherRoutes) {
17523
17657
 
17524
17658
  // ../../packages/build/src/strategy/strategies/opennext-aws.ts
17525
17659
  import { writeFile as writeFile6 } from "fs/promises";
17526
- import { join as join13 } from "path";
17660
+ import { join as join14 } from "path";
17527
17661
 
17528
17662
  // ../../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";
17663
+ import { access as access7, readFile as readFile11, rm, writeFile as writeFile5 } from "fs/promises";
17664
+ import { dirname as dirname2, join as join13, resolve as resolve5 } from "path";
17531
17665
  function capturingObserver(observer) {
17532
17666
  const lines = [];
17533
17667
  return {
@@ -17584,7 +17718,7 @@ async function findProjectBin(appRoot, checkout, name) {
17584
17718
  let dir = resolve5(appRoot);
17585
17719
  const stop = resolve5(checkout);
17586
17720
  for (; ; ) {
17587
- const candidate = join12(dir, "node_modules", ".bin", name);
17721
+ const candidate = join13(dir, "node_modules", ".bin", name);
17588
17722
  if (await pathExists2(candidate)) return candidate;
17589
17723
  if (dir === stop) return null;
17590
17724
  const parent = dirname2(dir);
@@ -17598,7 +17732,7 @@ function dotenvQuote(value) {
17598
17732
  return `"${value.replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
17599
17733
  }
17600
17734
  async function withDevVars(wranglerConfigPath, env, run) {
17601
- const path = join12(dirname2(wranglerConfigPath), ".dev.vars");
17735
+ const path = join13(dirname2(wranglerConfigPath), ".dev.vars");
17602
17736
  const keys = Object.keys(env).filter(
17603
17737
  (key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)
17604
17738
  );
@@ -17631,7 +17765,7 @@ async function bundleWorker(context, layout, observer, errorCode2) {
17631
17765
  cwd: layout.appRoot,
17632
17766
  env: {
17633
17767
  CI: "1",
17634
- WRANGLER_LOG_PATH: join12(
17768
+ WRANGLER_LOG_PATH: join13(
17635
17769
  layout.appRoot,
17636
17770
  layout.outputRoot,
17637
17771
  "wrangler.log"
@@ -17664,12 +17798,12 @@ async function smokeWorker(context, layout, observer) {
17664
17798
  "--show-interactive-dev-session=false",
17665
17799
  "--log-level=warn",
17666
17800
  "--persist-to",
17667
- join12(layout.appRoot, layout.outputRoot, ".naro-smoke-state")
17801
+ join13(layout.appRoot, layout.outputRoot, ".naro-smoke-state")
17668
17802
  ],
17669
17803
  cwd: layout.appRoot,
17670
17804
  env: {
17671
17805
  CI: "1",
17672
- WRANGLER_LOG_PATH: join12(
17806
+ WRANGLER_LOG_PATH: join13(
17673
17807
  layout.appRoot,
17674
17808
  layout.outputRoot,
17675
17809
  "wrangler-dev.log"
@@ -17685,10 +17819,10 @@ async function smokeWorker(context, layout, observer) {
17685
17819
  );
17686
17820
  }
17687
17821
  async function approvePnpmBuilds(checkout, packages) {
17688
- const path = join12(checkout, "pnpm-workspace.yaml");
17822
+ const path = join13(checkout, "pnpm-workspace.yaml");
17689
17823
  let source = "";
17690
17824
  try {
17691
- source = await readFile10(path, "utf8");
17825
+ source = await readFile11(path, "utf8");
17692
17826
  } catch {
17693
17827
  }
17694
17828
  let next = source;
@@ -17781,7 +17915,7 @@ function createOpenNextAwsStrategy() {
17781
17915
  } catch (error51) {
17782
17916
  return failed("prepare", describeError(error51));
17783
17917
  }
17784
- const configPath = join13(fork.appRoot, CONFIG_NAME);
17918
+ const configPath = join14(fork.appRoot, CONFIG_NAME);
17785
17919
  try {
17786
17920
  await writeFile6(
17787
17921
  configPath,
@@ -17810,7 +17944,7 @@ function createOpenNextAwsStrategy() {
17810
17944
  observer
17811
17945
  );
17812
17946
  if (!build.ok) return failed("build", describeFailure(build));
17813
- const serverFunction = join13(fork.appRoot, SERVER_FUNCTION);
17947
+ const serverFunction = join14(fork.appRoot, SERVER_FUNCTION);
17814
17948
  if (!await pathExists2(serverFunction))
17815
17949
  return failed("bundle", `OpenNext AWS produced no ${SERVER_FUNCTION}.`);
17816
17950
  const smoke = await context.ports.smoke.run(
@@ -17832,7 +17966,7 @@ function createOpenNextAwsStrategy() {
17832
17966
  status: "success",
17833
17967
  artifact: {
17834
17968
  kind: "opennext-aws",
17835
- outputRoot: join13(fork.appRoot, ".open-next"),
17969
+ outputRoot: join14(fork.appRoot, ".open-next"),
17836
17970
  serverFunction
17837
17971
  },
17838
17972
  notes: [
@@ -17844,8 +17978,8 @@ function createOpenNextAwsStrategy() {
17844
17978
  }
17845
17979
 
17846
17980
  // ../../packages/build/src/strategy/strategies/opennext-cloudflare.ts
17847
- import { readFile as readFile11 } from "fs/promises";
17848
- import { join as join14 } from "path";
17981
+ import { readFile as readFile12 } from "fs/promises";
17982
+ import { join as join15 } from "path";
17849
17983
  var PROXY_TO_MIDDLEWARE_VARIANT = "proxy-to-middleware";
17850
17984
  var NODE_MIDDLEWARE_REFUSAL = /Node\.js middleware is not (?:currently )?supported/i;
17851
17985
  var NODE_MIDDLEWARE_EXPERIMENTAL = /Node\.js middleware support is experimental/i;
@@ -17853,8 +17987,8 @@ var NODE_MIDDLEWARE_NOT_DEPLOYED = 'Node.js-runtime middleware (runtime: "nodejs
17853
17987
  async function buildDeclaresNodeMiddleware(appRoot) {
17854
17988
  try {
17855
17989
  const manifest2 = JSON.parse(
17856
- await readFile11(
17857
- join14(appRoot, ".next", "server", "functions-config-manifest.json"),
17990
+ await readFile12(
17991
+ join15(appRoot, ".next", "server", "functions-config-manifest.json"),
17858
17992
  "utf8"
17859
17993
  )
17860
17994
  );
@@ -17864,7 +17998,7 @@ async function buildDeclaresNodeMiddleware(appRoot) {
17864
17998
  }
17865
17999
  }
17866
18000
  function createOpenNextCloudflareStrategy(options) {
17867
- async function attempt(context, roots, observer, ownBuild) {
18001
+ async function attempt(context, roots, observer, ownBuild, adapterNotice) {
17868
18002
  let platform;
17869
18003
  try {
17870
18004
  platform = await options.createPlatformBuildConfig(
@@ -17902,6 +18036,13 @@ function createOpenNextCloudflareStrategy(options) {
17902
18036
  outcome: ownBuild ? appBuildFailed(describeFailure(nextBuild)) : failed("build", describeFailure(nextBuild))
17903
18037
  };
17904
18038
  }
18039
+ if (!adapterNotice.warned) {
18040
+ const adapter = userAdapterOf(context) ?? await adapterInBuildOutput(roots.appRoot, context.nextVersion);
18041
+ if (adapter) {
18042
+ adapterNotice.warned = true;
18043
+ await warn(observer, openNextAdapterWarning(adapter));
18044
+ }
18045
+ }
17905
18046
  if (await buildDeclaresNodeMiddleware(roots.appRoot)) {
17906
18047
  return {
17907
18048
  ok: false,
@@ -17997,6 +18138,7 @@ function createOpenNextCloudflareStrategy(options) {
17997
18138
  target: "cloudflare",
17998
18139
  supports: () => ({ supported: true }),
17999
18140
  async evaluate(context, observer) {
18141
+ const adapterNotice = { warned: false };
18000
18142
  const rememberedRewrite = context.pinnedVariant === PROXY_TO_MIDDLEWARE_VARIANT && context.facts.proxyFile !== null;
18001
18143
  if (rememberedRewrite) {
18002
18144
  await info(
@@ -18008,7 +18150,13 @@ function createOpenNextCloudflareStrategy(options) {
18008
18150
  observer,
18009
18151
  "P2 opennext-cloudflare: building the project as written."
18010
18152
  );
18011
- const first = await attempt(context, context.workspace, observer, true);
18153
+ const first = await attempt(
18154
+ context,
18155
+ context.workspace,
18156
+ observer,
18157
+ true,
18158
+ adapterNotice
18159
+ );
18012
18160
  if (first.ok) {
18013
18161
  return {
18014
18162
  status: "success",
@@ -18040,7 +18188,13 @@ function createOpenNextCloudflareStrategy(options) {
18040
18188
  observer,
18041
18189
  `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
18190
  );
18043
- const second = await attempt(context, fork, observer, rememberedRewrite);
18191
+ const second = await attempt(
18192
+ context,
18193
+ fork,
18194
+ observer,
18195
+ rememberedRewrite,
18196
+ adapterNotice
18197
+ );
18044
18198
  if (!second.ok) {
18045
18199
  if (!rememberedRewrite || second.outcome.status !== "failed" || !second.outcome.appBuild)
18046
18200
  return second.outcome;
@@ -18052,7 +18206,8 @@ function createOpenNextCloudflareStrategy(options) {
18052
18206
  context,
18053
18207
  context.workspace,
18054
18208
  observer,
18055
- true
18209
+ true,
18210
+ adapterNotice
18056
18211
  );
18057
18212
  if (asWritten.ok) {
18058
18213
  await info(
@@ -18121,7 +18276,7 @@ function createAwsMiddlewareCloudflareAppStrategy() {
18121
18276
  }
18122
18277
 
18123
18278
  // ../../packages/build/src/strategy/strategies/vinext-cloudflare.ts
18124
- import { join as join15 } from "path";
18279
+ import { join as join16 } from "path";
18125
18280
  var VINEXT_MIN_NEXT_MAJOR = 16;
18126
18281
  var VINEXT_BUILD_SCRIPTS = ["esbuild", "workerd"];
18127
18282
  var VINEXT_OUTPUT = {
@@ -18277,7 +18432,7 @@ function createVinextCloudflareStrategy(options) {
18277
18432
  context,
18278
18433
  {
18279
18434
  appRoot: fork.appRoot,
18280
- wranglerConfigPath: join15(
18435
+ wranglerConfigPath: join16(
18281
18436
  fork.appRoot,
18282
18437
  "dist",
18283
18438
  "server",
@@ -18302,7 +18457,7 @@ function createVinextCloudflareStrategy(options) {
18302
18457
  // ../../packages/build/src/strategy/workspace.ts
18303
18458
  import { execFile } from "child_process";
18304
18459
  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";
18460
+ import { basename as basename2, dirname as dirname3, join as join17, relative as relative5, resolve as resolve6 } from "path";
18306
18461
  import { promisify } from "util";
18307
18462
  var execFileAsync = promisify(execFile);
18308
18463
  var SKIPPED_ANYWHERE = /* @__PURE__ */ new Set(["node_modules"]);
@@ -18325,14 +18480,14 @@ async function copyInstalledPackages(source, target2) {
18325
18480
  const walk2 = async (dir) => {
18326
18481
  for (const entry of await readdir4(dir, { withFileTypes: true })) {
18327
18482
  if (!entry.isDirectory() || entry.name === ".git") continue;
18328
- const path = join16(dir, entry.name);
18483
+ const path = join17(dir, entry.name);
18329
18484
  if (entry.name === "node_modules") installs.push(path);
18330
18485
  else await walk2(path);
18331
18486
  }
18332
18487
  };
18333
18488
  await walk2(origin);
18334
18489
  for (const install of installs)
18335
- await copyTree(install, join16(target2, relative5(origin, install)));
18490
+ await copyTree(install, join17(target2, relative5(origin, install)));
18336
18491
  }
18337
18492
  async function copyTree(from, to) {
18338
18493
  await mkdir2(dirname3(to), { recursive: true });
@@ -18347,7 +18502,7 @@ async function copyTree(from, to) {
18347
18502
  await cp(from, to, { recursive: true, verbatimSymlinks: true });
18348
18503
  }
18349
18504
  function forkedAppRoot(checkout, appRoot, forkCheckout) {
18350
- return join16(forkCheckout, relative5(checkout, appRoot));
18505
+ return join17(forkCheckout, relative5(checkout, appRoot));
18351
18506
  }
18352
18507
 
18353
18508
  // ../../packages/build/src/local-build-executor.ts
@@ -18369,7 +18524,7 @@ async function withRouteList(artifact, list, appRoot, observer) {
18369
18524
  }
18370
18525
  }
18371
18526
  var defaultLocalBuildDependencies = {
18372
- createWorkspace: () => mkdtemp(join17(tmpdir(), "next-deploy-build-")),
18527
+ createWorkspace: () => mkdtemp(join18(tmpdir(), "next-deploy-build-")),
18373
18528
  removeWorkspace: (path) => rm3(path, { recursive: true, force: true }),
18374
18529
  runCommand,
18375
18530
  detectPackageManager,
@@ -18403,8 +18558,8 @@ async function readInstalledNextVersion(appRoot, checkout) {
18403
18558
  const stop = resolve7(checkout);
18404
18559
  for (; ; ) {
18405
18560
  try {
18406
- const source = await readFile12(
18407
- join17(dir, "node_modules", "next", "package.json"),
18561
+ const source = await readFile13(
18562
+ join18(dir, "node_modules", "next", "package.json"),
18408
18563
  "utf8"
18409
18564
  );
18410
18565
  const version2 = JSON.parse(source).version;
@@ -18497,8 +18652,8 @@ var LocalBuildExecutor = class {
18497
18652
  }
18498
18653
  async build(input, observer, cache) {
18499
18654
  const workspace = await this.dependencies.createWorkspace();
18500
- const checkout = join17(workspace, "checkout");
18501
- const pmCacheDir = join17(workspace, ".pm-store");
18655
+ const checkout = join18(workspace, "checkout");
18656
+ const pmCacheDir = join18(workspace, ".pm-store");
18502
18657
  let cacheSeq = 0;
18503
18658
  const commandAvailable = this.dependencies.commandAvailable ?? hasCommand;
18504
18659
  const compressor = "zstd";
@@ -18507,7 +18662,7 @@ var LocalBuildExecutor = class {
18507
18662
  transport: cache,
18508
18663
  fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
18509
18664
  runCommand: this.dependencies.runCommand,
18510
- tmpFile: (suffix) => join17(workspace, `cache-${String(cacheSeq++)}${suffix}`),
18665
+ tmpFile: (suffix) => join18(workspace, `cache-${String(cacheSeq++)}${suffix}`),
18511
18666
  // One latch for the whole build so a disabled cache warns once.
18512
18667
  report: { unavailable: false }
18513
18668
  } : null;
@@ -18586,6 +18741,7 @@ var LocalBuildExecutor = class {
18586
18741
  checkout,
18587
18742
  input.rootDirectory
18588
18743
  );
18744
+ await assertAppRootInsideCheckout(checkout, appRoot);
18589
18745
  const stacks = await import("./stack-PDV3NPWC.js");
18590
18746
  const stack = await (this.dependencies.detectStack ?? stacks.detectStack)(
18591
18747
  appRoot
@@ -18698,7 +18854,7 @@ var LocalBuildExecutor = class {
18698
18854
  }
18699
18855
  }
18700
18856
  await notifyStatus(observer, "BUILDING");
18701
- const buildCacheDir = resolved.preset.cachePath ? join17(appRoot, resolved.preset.cachePath) : null;
18857
+ const buildCacheDir = resolved.preset.cachePath ? join18(appRoot, resolved.preset.cachePath) : null;
18702
18858
  const buildCacheKey = sanitizeCacheKey(input.branch);
18703
18859
  if (cachePorts && buildCacheDir) {
18704
18860
  await tryRestore(
@@ -18722,13 +18878,13 @@ var LocalBuildExecutor = class {
18722
18878
  entries: fingerprintEntries,
18723
18879
  nextVersion: nextVersion ? `${String(nextVersion.major)}.${String(nextVersion.minor)}.${String(nextVersion.patch)}` : "unknown"
18724
18880
  });
18725
- const pristine = join17(workspace, "pristine");
18881
+ const pristine = join18(workspace, "pristine");
18726
18882
  const fork = this.dependencies.forkWorkspace ?? defaultFork;
18727
18883
  await fork(checkout, pristine);
18728
18884
  let forks = 0;
18729
18885
  const resolverConcurrency = this.dependencies.resolverConcurrency ?? 1;
18730
18886
  const forkWorkspace = async (label2) => {
18731
- const target2 = join17(
18887
+ const target2 = join18(
18732
18888
  workspace,
18733
18889
  "forks",
18734
18890
  `${String(forks++)}-${label2.replace(/[^a-z0-9-]/gi, "-")}`
@@ -19016,12 +19172,12 @@ async function errorFromResponse(response, context) {
19016
19172
  // ../../packages/sdk/src/session.ts
19017
19173
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
19018
19174
  import { homedir } from "os";
19019
- import { join as join18 } from "path";
19175
+ import { join as join19 } from "path";
19020
19176
  function credentialsDir(home = homedir()) {
19021
- return join18(home, ".naro");
19177
+ return join19(home, ".naro");
19022
19178
  }
19023
19179
  function credentialsFile(home = homedir()) {
19024
- return join18(credentialsDir(home), "credentials.json");
19180
+ return join19(credentialsDir(home), "credentials.json");
19025
19181
  }
19026
19182
  function saveSession(session, home = homedir()) {
19027
19183
  mkdirSync(credentialsDir(home), { recursive: true, mode: 448 });
@@ -19848,6 +20004,8 @@ var DeploymentsApi = class {
19848
20004
  }
19849
20005
  }
19850
20006
  };
20007
+ var STATIC_ASSETS_BINDING = "ASSETS";
20008
+ var MANAGED_DATABASE_BINDING = "DB";
19851
20009
  var EnvApi = class {
19852
20010
  constructor(client) {
19853
20011
  this.client = client;
@@ -19877,6 +20035,37 @@ var EnvApi = class {
19877
20035
  }
19878
20036
  );
19879
20037
  }
20038
+ /**
20039
+ * The names the env route refuses as a key — `409 VALIDATION_FAILED`
20040
+ * `<KEY> is already used by a storage binding.` — read the way it reads
20041
+ * them: `ASSETS` (the static files), every binding of the project's storage
20042
+ * and, on a project using the managed database (`database_provider` is
20043
+ * `d1`), `DB`, which may not be a storage row yet. For a caller that sends
20044
+ * many entries in several requests (`naro env import`) and has to refuse
20045
+ * before the first one is saved. A control plane whose project row has no
20046
+ * `database_provider` reads as having no managed database, so `DB` is only
20047
+ * left out there.
20048
+ */
20049
+ async takenBindingNames(projectId) {
20050
+ const base2 = `/api/projects/${encodeURIComponent(projectId)}`;
20051
+ const [storage, project] = await Promise.all([
20052
+ this.client.request(
20053
+ `${base2}/storage`,
20054
+ { context: "List storage" }
20055
+ ),
20056
+ this.client.request(
20057
+ base2,
20058
+ { context: "Read project" }
20059
+ )
20060
+ ]);
20061
+ const names = /* @__PURE__ */ new Set([
20062
+ STATIC_ASSETS_BINDING,
20063
+ ...storage.storage.map((row) => row.bindingName)
20064
+ ]);
20065
+ if (project.project.database_provider === "d1")
20066
+ names.add(MANAGED_DATABASE_BINDING);
20067
+ return [...names];
20068
+ }
19880
20069
  async removeById(projectId, envVarId) {
19881
20070
  await this.client.request(
19882
20071
  `/api/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(envVarId)}`,
@@ -19887,13 +20076,28 @@ var EnvApi = class {
19887
20076
  * Removes a key's rows: the one `target` names, or, with no target, the
19888
20077
  * `production`, `preview` and `all` rows. An environment's value goes only
19889
20078
  * when its slug is the target, so removing a key everywhere else does not
19890
- * take a QA or staging value with it.
20079
+ * take a QA or staging value with it. A key only environments hold fails
20080
+ * ENV_NOT_FOUND naming them (`details.environments`), not as if there were
20081
+ * none.
19891
20082
  */
19892
20083
  async remove(projectId, key, target2) {
19893
20084
  const rows = await this.list(projectId);
19894
20085
  const matches = rows.filter(
19895
20086
  (row) => row.key === key && (target2 === void 0 ? isReservedEnvTarget(row.target) : row.target === target2)
19896
20087
  );
20088
+ if (matches.length === 0 && target2 === void 0) {
20089
+ const environments = [
20090
+ ...new Set(
20091
+ rows.filter((row) => row.key === key).map((row) => row.target).filter((slug) => !isReservedEnvTarget(slug))
20092
+ )
20093
+ ].toSorted();
20094
+ if (environments.length > 0)
20095
+ throw new NaroError(
20096
+ "ENV_NOT_FOUND",
20097
+ `No ${key} variable found outside named environments. Pass the environment to remove it: ${environments.join(", ")}.`,
20098
+ { details: { key, environments } }
20099
+ );
20100
+ }
19897
20101
  if (matches.length === 0) {
19898
20102
  throw new NaroError(
19899
20103
  "ENV_NOT_FOUND",
@@ -21167,40 +21371,92 @@ function summarizeStatement(statement2) {
21167
21371
 
21168
21372
  // ../../packages/sdk/src/dotenv.ts
21169
21373
  import { existsSync, readFileSync as readFileSync2 } from "fs";
21170
- import { join as join19 } from "path";
21374
+ import { join as join20 } from "path";
21375
+ var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/dgm;
21376
+ var QUOTES = /* @__PURE__ */ new Set(["'", '"', "`"]);
21377
+ function parseDotenvDetailed(text2) {
21378
+ const vars = {};
21379
+ const unclosedQuotes = [];
21380
+ const lines = text2.replace(/\r\n?/g, "\n");
21381
+ let line = 1;
21382
+ let counted = 0;
21383
+ for (const match of lines.matchAll(LINE)) {
21384
+ const key = match[1];
21385
+ const raw = match[2] ?? "";
21386
+ let value = raw.trim();
21387
+ const maybeQuote = value[0];
21388
+ if (maybeQuote !== void 0 && QUOTES.has(maybeQuote) && !value.includes(maybeQuote, 1)) {
21389
+ const at = (match.indices?.[2]?.[0] ?? match.index) + (raw.length - raw.trimStart().length);
21390
+ for (; counted < at; counted += 1) if (lines[counted] === "\n") line += 1;
21391
+ unclosedQuotes.push(line);
21392
+ }
21393
+ value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
21394
+ if (maybeQuote === '"')
21395
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r");
21396
+ vars[key] = value;
21397
+ }
21398
+ return { vars, unclosedQuotes };
21399
+ }
21171
21400
  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;
21401
+ return parseDotenvDetailed(text2).vars;
21185
21402
  }
21186
21403
  function readProjectEnv(dir) {
21187
21404
  for (const name of [".env.local", ".env.production", ".env"]) {
21188
- const path = join19(dir, name);
21405
+ const path = join20(dir, name);
21189
21406
  if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
21190
21407
  }
21191
21408
  return {};
21192
21409
  }
21410
+ var PLAIN = /^[^\s"'#$`\\]*$/;
21411
+ function bareReadsBack(value) {
21412
+ return !/[#\r\n\u2028\u2029]/.test(value) && value.trim() === value && !QUOTES.has(value[0] ?? "");
21413
+ }
21414
+ function dotenvValue(value) {
21415
+ if (PLAIN.test(value)) return value;
21416
+ const endsInBackslash = value.endsWith("\\");
21417
+ if (!value.includes("\r") && !endsInBackslash) {
21418
+ if (!value.includes("'")) return `'${value}'`;
21419
+ if (!value.includes("`")) return `\`${value}\``;
21420
+ }
21421
+ if (!value.includes('"') && !/\\[nr]/.test(value) && !endsInBackslash)
21422
+ return `"${value.replaceAll("\r", "\\r")}"`;
21423
+ if (bareReadsBack(value)) return value;
21424
+ return void 0;
21425
+ }
21426
+ 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
21427
  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")}
21428
+ const lines = [];
21429
+ const allQuotes = [];
21430
+ const other = [];
21431
+ for (const [key, value] of Object.entries(vars)) {
21432
+ const written = dotenvValue(value);
21433
+ if (written !== void 0) lines.push(`${key}=${written}`);
21434
+ else if (value.includes("'") && value.includes('"') && value.includes("`"))
21435
+ allQuotes.push(key);
21436
+ else other.push(key);
21437
+ }
21438
+ const problems2 = [];
21439
+ if (allQuotes.length > 0)
21440
+ problems2.push(
21441
+ `Cannot write ${allQuotes.join(", ")} to a dotenv file: their values hold ', " and \` together.`
21442
+ );
21443
+ if (other.length > 0)
21444
+ problems2.push(
21445
+ `Cannot write ${other.join(", ")} to a dotenv file: dotenv would not read their values back unchanged.`
21446
+ );
21447
+ if (problems2.length > 0)
21448
+ throw new NaroError(
21449
+ "VALIDATION_FAILED",
21450
+ `${problems2.join(" ")} ${UNWRITABLE_HINT}`,
21451
+ { details: { keys: [...allQuotes, ...other] } }
21452
+ );
21453
+ return `${lines.join("\n")}
21198
21454
  `;
21199
21455
  }
21200
21456
 
21201
21457
  // ../../packages/sdk/src/env-scan.ts
21202
21458
  import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
21203
- import { join as join20 } from "path";
21459
+ import { join as join21 } from "path";
21204
21460
  var SOURCE_EXTENSIONS3 = /* @__PURE__ */ new Set([
21205
21461
  ".ts",
21206
21462
  ".tsx",
@@ -21255,7 +21511,7 @@ function collect(dir, out) {
21255
21511
  for (const entry of entries) {
21256
21512
  if (out.length >= MAX_FILES) return;
21257
21513
  if (entry.isSymbolicLink()) continue;
21258
- const path = join20(dir, entry.name);
21514
+ const path = join21(dir, entry.name);
21259
21515
  if (entry.isDirectory()) {
21260
21516
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name))
21261
21517
  continue;
@@ -21287,7 +21543,7 @@ function scanEnvReferences(dir) {
21287
21543
  }
21288
21544
  }
21289
21545
  for (const name of EXAMPLE_FILES) {
21290
- const path = join20(dir, name);
21546
+ const path = join21(dir, name);
21291
21547
  if (!existsSync2(path)) continue;
21292
21548
  let parsed;
21293
21549
  try {
@@ -21305,11 +21561,11 @@ function scanEnvReferences(dir) {
21305
21561
 
21306
21562
  // ../../packages/sdk/src/framework.ts
21307
21563
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
21308
- import { join as join21 } from "path";
21564
+ import { join as join22 } from "path";
21309
21565
  function readPackageJson2(dir) {
21310
21566
  try {
21311
21567
  return JSON.parse(
21312
- readFileSync4(join21(dir, "package.json"), "utf8")
21568
+ readFileSync4(join22(dir, "package.json"), "utf8")
21313
21569
  );
21314
21570
  } catch {
21315
21571
  return null;
@@ -21320,7 +21576,7 @@ function hasDependency(pkg, name) {
21320
21576
  }
21321
21577
  function hasConfig(dir, base2, evidence) {
21322
21578
  for (const ext of ["js", "mjs", "cjs", "ts", "mts"]) {
21323
- if (existsSync3(join21(dir, `${base2}.${ext}`))) {
21579
+ if (existsSync3(join22(dir, `${base2}.${ext}`))) {
21324
21580
  evidence.push(`${base2}.${ext}`);
21325
21581
  return true;
21326
21582
  }
@@ -21428,8 +21684,8 @@ function detectFramework2(dir) {
21428
21684
  if (!byDependency && !byConfig) continue;
21429
21685
  if (byDependency) evidence.push(`package.json: ${detector.dependency}`);
21430
21686
  if (detector.framework === "nextjs") {
21431
- if (existsSync3(join21(dir, "app"))) evidence.push("app/");
21432
- if (existsSync3(join21(dir, "pages"))) evidence.push("pages/");
21687
+ if (existsSync3(join22(dir, "app"))) evidence.push("app/");
21688
+ if (existsSync3(join22(dir, "pages"))) evidence.push("pages/");
21433
21689
  }
21434
21690
  return {
21435
21691
  framework: detector.framework,
@@ -21440,7 +21696,7 @@ function detectFramework2(dir) {
21440
21696
  deployable: true
21441
21697
  };
21442
21698
  }
21443
- if (!pkg && existsSync3(join21(dir, "index.html"))) {
21699
+ if (!pkg && existsSync3(join22(dir, "index.html"))) {
21444
21700
  evidence.push("index.html (no package.json)");
21445
21701
  return {
21446
21702
  framework: "static",
@@ -21461,8 +21717,26 @@ function detectFramework2(dir) {
21461
21717
  };
21462
21718
  }
21463
21719
 
21720
+ // ../../packages/data-core/src/unicode-controls.ts
21721
+ function hasControlCharacter(text2) {
21722
+ for (let i = 0; i < text2.length; i += 1) {
21723
+ const code = text2.charCodeAt(i);
21724
+ if (code <= 31 || code >= 127 && code <= 159) return true;
21725
+ }
21726
+ return false;
21727
+ }
21728
+ function hasBidiControl(text2) {
21729
+ for (let i = 0; i < text2.length; i += 1) {
21730
+ const code = text2.charCodeAt(i);
21731
+ if (code === 1564 || code === 8206 || code === 8207 || code >= 8234 && code <= 8238 || code >= 8294 && code <= 8297)
21732
+ return true;
21733
+ }
21734
+ return false;
21735
+ }
21736
+
21464
21737
  // ../../packages/data-core/src/storage-path.ts
21465
21738
  var MAX_SINGLE_UPLOAD_BYTES = 52428800;
21739
+ var MAX_OBJECT_NAME_BYTES = 900;
21466
21740
  var STORAGE_RESERVED_WORDS = [
21467
21741
  "authenticated",
21468
21742
  "copy",
@@ -21478,6 +21752,32 @@ var RESERVED = new Set(STORAGE_RESERVED_WORDS);
21478
21752
  var ENCODER = new TextEncoder();
21479
21753
  var LONE_SURROGATE2 = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
21480
21754
  var LONE_SURROGATES = new RegExp(LONE_SURROGATE2.source, "g");
21755
+ function hasLoneSurrogate(text2) {
21756
+ return LONE_SURROGATE2.test(text2);
21757
+ }
21758
+ function refuse(reason) {
21759
+ return { ok: false, reason };
21760
+ }
21761
+ function checkWellFormed(name) {
21762
+ if (name === "") return refuse("is empty");
21763
+ if (name.length > MAX_OBJECT_NAME_BYTES || ENCODER.encode(name).length > MAX_OBJECT_NAME_BYTES)
21764
+ return refuse(`is longer than ${String(MAX_OBJECT_NAME_BYTES)} bytes`);
21765
+ if (name.startsWith("/") || name.endsWith("/"))
21766
+ return refuse("starts or ends with /");
21767
+ if (name.includes("//")) return refuse("has an empty segment (//)");
21768
+ for (const segment of name.split("/"))
21769
+ if (segment === "." || segment === "..")
21770
+ return refuse("has a . or .. segment");
21771
+ if (hasControlCharacter(name)) return refuse("has a control character");
21772
+ if (name.includes("\\")) return refuse("has a backslash");
21773
+ if (hasBidiControl(name))
21774
+ return refuse("has a bidirectional control character");
21775
+ return { ok: true, name };
21776
+ }
21777
+ function checkObjectName(name) {
21778
+ if (hasLoneSurrogate(name)) return refuse("is not valid UTF-8");
21779
+ return checkWellFormed(name);
21780
+ }
21481
21781
  function objectParent(name) {
21482
21782
  const slash = name.lastIndexOf("/");
21483
21783
  return slash < 0 ? "" : name.slice(0, slash);
@@ -21507,6 +21807,7 @@ var STORAGE_OBJECTS = "storage.objects";
21507
21807
  var STORAGE_OBJECTS_TABLE = "_naro_storage_objects";
21508
21808
  var STORAGE_OBJECTS_QUALIFIER = "objects";
21509
21809
  var STORAGE_UNAVAILABLE_MESSAGE = "storage.objects is not available on this backend yet. Run `naro backend enable` to update it.";
21810
+ var STORAGE_UNAVAILABLE_REASON = `${STORAGE_OBJECTS} is not available on this backend yet`;
21510
21811
  var STORAGE_BUCKETS_TABLE = "_naro_storage_buckets";
21511
21812
  var STORAGE_POLICY_COLUMNS = [
21512
21813
  "id",
@@ -21674,7 +21975,8 @@ var CATALOG = {
21674
21975
  policy,
21675
21976
  recursion,
21676
21977
  values,
21677
- bytes
21978
+ bytes,
21979
+ depth
21678
21980
  }) => {
21679
21981
  if (recursion)
21680
21982
  return `Infinite recursion detected in policy for relation ${table}.`;
@@ -21682,6 +21984,8 @@ var CATALOG = {
21682
21984
  return `The policies of ${table} bind more than ${values === true ? "50" : String(values)} values in one statement.`;
21683
21985
  if (bytes)
21684
21986
  return `The policies of ${table} write more than ${bytes === true ? "50000" : String(bytes)} bytes of SQL into one statement.`;
21987
+ if (depth)
21988
+ return `The policies of ${table} nest an expression more than 100 deep into one statement.`;
21685
21989
  if (policy !== void 0)
21686
21990
  return `Policy ${policy} on ${table} cannot be applied.`;
21687
21991
  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 +22043,7 @@ var CATALOG = {
21739
22043
  },
21740
22044
  OBJECT_TOO_LARGE: {
21741
22045
  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.`
22046
+ 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
22047
  },
21744
22048
  MIME_TYPE_NOT_ALLOWED: {
21745
22049
  status: 415,
@@ -21751,7 +22055,7 @@ var CATALOG = {
21751
22055
  },
21752
22056
  OBJECT_EXISTS: {
21753
22057
  status: 409,
21754
- message: ({ bucket, path }) => `Object ${storageName(path)} already exists in bucket ${storageName(bucket)}. Upload with upsert to replace it.`
22058
+ 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
22059
  },
21756
22060
  OBJECT_BUSY: {
21757
22061
  status: 409,
@@ -22429,6 +22733,15 @@ function resolvePolicyTable(snapshot, name) {
22429
22733
  if (folded === REALTIME_MESSAGES) return REALTIME_MESSAGES_TABLE;
22430
22734
  return resolveTable(snapshot, name);
22431
22735
  }
22736
+ function resolveSubqueryTable(snapshot, name) {
22737
+ if (asciiLower(name) === STORAGE_OBJECTS)
22738
+ return snapshot.storageSubqueries === false ? void 0 : snapshot.storage;
22739
+ return resolveTable(snapshot, name);
22740
+ }
22741
+ function subqueryName(subquery) {
22742
+ if (subquery.alias !== null) return subquery.alias;
22743
+ return asciiLower(subquery.table) === STORAGE_OBJECTS ? STORAGE_OBJECTS_QUALIFIER : subquery.table;
22744
+ }
22432
22745
  async function readTableNames(db) {
22433
22746
  return tableNamesFromRows(rowsOf(await db.prepare(TABLE_NAMES_SQL).all()));
22434
22747
  }
@@ -22470,6 +22783,14 @@ function utf8ByteLength(text2) {
22470
22783
  function quoteIdentifier(name) {
22471
22784
  return `"${name.replaceAll('"', '""')}"`;
22472
22785
  }
22786
+ var SIDE_BY_SIDE = 8;
22787
+ function joinConditions(terms, op) {
22788
+ if (terms.length <= SIDE_BY_SIDE) return `(${terms.join(` ${op} `)})`;
22789
+ const groups = [];
22790
+ for (let at = 0; at < terms.length; at += SIDE_BY_SIDE)
22791
+ groups.push(joinConditions(terms.slice(at, at + SIDE_BY_SIDE), op));
22792
+ return joinConditions(groups, op);
22793
+ }
22473
22794
  var Failure = class extends Error {
22474
22795
  constructor(error51, reason) {
22475
22796
  super(error51.code);
@@ -22653,6 +22974,12 @@ var VALUE_KINDS = /* @__PURE__ */ new Set([
22653
22974
  function isValueExpr(expr) {
22654
22975
  return VALUE_KINDS.has(expr.kind);
22655
22976
  }
22977
+ function comparedValue(value) {
22978
+ let current = value;
22979
+ while (current.kind === "func" && current.name === "coalesce" && current.args.length === 1 && current.args[0] !== void 0)
22980
+ current = current.args[0];
22981
+ return current;
22982
+ }
22656
22983
  function isConditionExpr(expr) {
22657
22984
  if (!isValueExpr(expr)) return true;
22658
22985
  if (expr.kind === "column") return true;
@@ -22704,7 +23031,6 @@ var SPECIAL_WORDS = /* @__PURE__ */ new Set([
22704
23031
  "auth"
22705
23032
  ]);
22706
23033
  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
23034
  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
23035
  function storageHelperColumnProblem(fn) {
22710
23036
  return `storage.${fn}() takes the name column of storage.objects`;
@@ -23155,8 +23481,12 @@ function atRealtimeMessages(stream) {
23155
23481
  }
23156
23482
  function readTableName(stream) {
23157
23483
  const first = stream.peek();
23158
- if (atStorageObjects(stream))
23159
- throw new PolicySyntaxError(STORAGE_SUBQUERY_PROBLEM, first.position);
23484
+ if (atStorageObjects(stream)) {
23485
+ stream.next();
23486
+ stream.next();
23487
+ stream.next();
23488
+ return STORAGE_OBJECTS;
23489
+ }
23160
23490
  if (atRealtimeMessages(stream))
23161
23491
  throw new PolicySyntaxError(REALTIME_SUBQUERY_PROBLEM, first.position);
23162
23492
  let token = first;
@@ -23183,12 +23513,6 @@ function readTableName(stream) {
23183
23513
  return name;
23184
23514
  }
23185
23515
  function readPolicyTarget(stream) {
23186
- if (atStorageObjects(stream)) {
23187
- stream.next();
23188
- stream.next();
23189
- stream.next();
23190
- return STORAGE_OBJECTS;
23191
- }
23192
23516
  if (atRealtimeMessages(stream)) {
23193
23517
  stream.next();
23194
23518
  stream.next();
@@ -23608,9 +23932,10 @@ function printValue(expr) {
23608
23932
  }
23609
23933
  function printSubquery(subquery) {
23610
23934
  const select = subquery.select === "*" ? "*" : printValue(subquery.select);
23935
+ const table = asciiLower(subquery.table) === STORAGE_OBJECTS ? STORAGE_OBJECTS : quoteIdent(subquery.table);
23611
23936
  const alias = subquery.alias === null ? "" : ` ${quoteIdent(subquery.alias)}`;
23612
23937
  const where = subquery.where === null ? "" : ` WHERE ${printExpr(subquery.where)}`;
23613
- return `SELECT ${select} FROM ${quoteIdent(subquery.table)}${alias}${where}`;
23938
+ return `SELECT ${select} FROM ${table}${alias}${where}`;
23614
23939
  }
23615
23940
  function printElement(element) {
23616
23941
  if (typeof element === "object") return printValue(element);
@@ -23963,6 +24288,9 @@ var FILTER_OPERATORS = [
23963
24288
  "containedBy",
23964
24289
  "overlaps"
23965
24290
  ];
24291
+ function leastFilters(firstColumn) {
24292
+ return firstColumn === void 0 ? [] : [{ operator: "is", column: firstColumn, value: null }];
24293
+ }
23966
24294
  function isAuthUid(value) {
23967
24295
  return isPlainObject2(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "auth") && value.auth === "uid";
23968
24296
  }
@@ -24576,12 +24904,21 @@ function storedSource(table, names, alias) {
24576
24904
  function tableLabel(table) {
24577
24905
  return table.name === STORAGE_OBJECTS_TABLE ? STORAGE_OBJECTS : table.name;
24578
24906
  }
24907
+ function storageReadySql(alias) {
24908
+ return `(${quoteIdentifier(alias)}.${quoteIdentifier("status")} = 'ready')`;
24909
+ }
24910
+ function ownNames(table) {
24911
+ return table.name === STORAGE_OBJECTS_TABLE ? [STORAGE_OBJECTS_QUALIFIER] : [asciiLower(table.name)];
24912
+ }
24579
24913
  function missingColumnReason(name, tables) {
24580
24914
  return `column ${name} does not exist on ${tables.join(" or ")}`;
24581
24915
  }
24582
24916
  function missingRelationReason(table) {
24583
24917
  return `relation ${table} does not exist (a system table cannot be read either)`;
24584
24918
  }
24919
+ function unresolvedSubqueryReason(table) {
24920
+ return asciiLower(table) === STORAGE_OBJECTS ? STORAGE_UNAVAILABLE_REASON : missingRelationReason(table);
24921
+ }
24585
24922
  function resolveColumn(env, ref) {
24586
24923
  const name = asciiLower(ref.name);
24587
24924
  if (ref.qualifier !== null) {
@@ -24691,10 +25028,10 @@ function storageHelperSql(fn, index, operand) {
24691
25028
  }
24692
25029
  function storageCallSql(env, expr) {
24693
25030
  const problem = storageHelperColumnProblem(expr.fn);
24694
- if (asciiLower(env.blame.policy.table) !== STORAGE_OBJECTS)
25031
+ if (!env.scopes.some((scope) => scope.table.name === STORAGE_OBJECTS_TABLE))
24695
25032
  return blamed(env, problem);
24696
25033
  const { source, column: column2 } = resolveColumn(env, expr.column);
24697
- if (source !== env.scopes[0] || source.table.name !== STORAGE_OBJECTS_TABLE || column2.name !== "name")
25034
+ if (source.table.name !== STORAGE_OBJECTS_TABLE || column2.name !== "name")
24698
25035
  return blamed(env, problem);
24699
25036
  const index = expr.fn === "foldername" ? expr.index : null;
24700
25037
  if (expr.fn === "foldername" ? index === null || !Number.isInteger(index) || index < 1 || index > MAX_FOLDERNAME_INDEX : expr.index !== null)
@@ -24708,7 +25045,8 @@ function storageCallSql(env, expr) {
24708
25045
  })
24709
25046
  );
24710
25047
  }
24711
- function operandSide(env, expr) {
25048
+ function operandSide(env, value) {
25049
+ const expr = comparedValue(value);
24712
25050
  if (expr.kind === "cast") return operandSide(env, expr.operand);
24713
25051
  if (expr.kind !== "column" && expr.kind !== "realtime")
24714
25052
  return {
@@ -24769,7 +25107,10 @@ function conditionSql(env, expr) {
24769
25107
  switch (expr.kind) {
24770
25108
  case "and":
24771
25109
  case "or":
24772
- return `(${expr.args.map((arg) => conditionSql(env, arg)).join(expr.kind === "and" ? " AND " : " OR ")})`;
25110
+ return joinConditions(
25111
+ expr.args.map((arg) => conditionSql(env, arg)),
25112
+ expr.kind === "and" ? "AND" : "OR"
25113
+ );
24773
25114
  case "not":
24774
25115
  return `(NOT ${conditionSql(env, expr.arg)})`;
24775
25116
  default:
@@ -24852,8 +25193,8 @@ function conditionSql(env, expr) {
24852
25193
  }
24853
25194
  }
24854
25195
  function subquerySql(env, subquery, select) {
24855
- const table = resolveTable(env.snapshot, subquery.table);
24856
- if (!table) return blamed(env, missingRelationReason(subquery.table));
25196
+ const table = resolveSubqueryTable(env.snapshot, subquery.table);
25197
+ if (!table) return blamed(env, unresolvedSubqueryReason(subquery.table));
24857
25198
  const { state } = env;
24858
25199
  if (++state.subqueries > MAX_STATEMENT_SUBQUERIES)
24859
25200
  return blamed(env, STATEMENT_SUBQUERIES_REASON);
@@ -24867,7 +25208,7 @@ function subquerySql(env, subquery, select) {
24867
25208
  const alias = `_naro_s${String(++state.aliases)}`;
24868
25209
  const source = storedSource(
24869
25210
  table,
24870
- [asciiLower(subquery.alias ?? table.name)],
25211
+ [asciiLower(subqueryName(subquery))],
24871
25212
  alias
24872
25213
  );
24873
25214
  const inner = { ...env, scopes: [...env.scopes, source] };
@@ -24879,11 +25220,12 @@ function subquerySql(env, subquery, select) {
24879
25220
  selected = valueSql(inner, subquery.select);
24880
25221
  if (env.check) item = operandSide(inner, subquery.select);
24881
25222
  }
24882
- const own2 = storedSource(table, [asciiLower(table.name)], alias);
25223
+ const own2 = storedSource(table, ownNames(table), alias);
24883
25224
  const rls = inlinedPolicies(env, table, own2);
25225
+ const ready = table.name === STORAGE_OBJECTS_TABLE ? `${storageReadySql(alias)} AND ` : "";
24884
25226
  const where = subquery.where === null ? "" : ` AND ${conditionSql(inner, subquery.where)}`;
24885
25227
  return {
24886
- sql: `SELECT ${selected} FROM ${quoteIdentifier(table.name)} AS ${quoteIdentifier(alias)} WHERE ${rls}${where}`,
25228
+ sql: `SELECT ${selected} FROM ${quoteIdentifier(table.name)} AS ${quoteIdentifier(alias)} WHERE ${ready}${rls}${where}`,
24887
25229
  item
24888
25230
  };
24889
25231
  } finally {
@@ -24936,7 +25278,10 @@ function groupTerms(env, sources, action, permissive, restrictive, pick2) {
24936
25278
  terms.push(term);
24937
25279
  }
24938
25280
  const group = terms.length === 0 ? { sql: "0", params: [] } : terms.length === 1 ? terms[0] : {
24939
- sql: `(${terms.map((term) => term.sql).join(" OR ")})`,
25281
+ sql: joinConditions(
25282
+ terms.map((term) => term.sql),
25283
+ "OR"
25284
+ ),
24940
25285
  params: terms.flatMap((term) => term.params)
24941
25286
  };
24942
25287
  const restricted = restrictive.flatMap((policy) => {
@@ -24946,9 +25291,10 @@ function groupTerms(env, sources, action, permissive, restrictive, pick2) {
24946
25291
  return { permissive: group, restrictive: restricted };
24947
25292
  }
24948
25293
  function inlinedPolicies(env, table, source) {
25294
+ const label2 = tableLabel(table);
24949
25295
  const found = applicablePolicies(
24950
25296
  env.req.policies,
24951
- table.name,
25297
+ label2,
24952
25298
  "select",
24953
25299
  env.req.role
24954
25300
  );
@@ -24960,7 +25306,7 @@ function inlinedPolicies(env, table, source) {
24960
25306
  if (found.permissive.length === 0) return "0";
24961
25307
  return withTableActive(
24962
25308
  env.state,
24963
- table.name,
25309
+ label2,
24964
25310
  [...found.permissive, ...found.restrictive],
24965
25311
  () => {
24966
25312
  const inner = { ...env, check: null, params: [] };
@@ -24979,7 +25325,10 @@ function inlinedPolicies(env, table, source) {
24979
25325
  for (const part of parts) env.params.push(...part.params);
24980
25326
  const [only] = parts;
24981
25327
  if (parts.length === 1 && only) return only.sql;
24982
- return `(${parts.map((part) => part.sql).join(" AND ")})`;
25328
+ return joinConditions(
25329
+ parts.map((part) => part.sql),
25330
+ "AND"
25331
+ );
24983
25332
  }
24984
25333
  );
24985
25334
  }
@@ -25074,11 +25423,8 @@ function scopedValues(operand, scopes) {
25074
25423
  return operandValues(operand).map((value) => ({ value, scopes }));
25075
25424
  }
25076
25425
  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;
25426
+ const inner = resolveSubqueryTable(snapshot, subquery.table);
25427
+ return inner ? [...scopes, { names: [asciiLower(subqueryName(subquery))], table: inner }] : void 0;
25082
25428
  }
25083
25429
  function columnsReadOn(snapshot, table, expr, written) {
25084
25430
  const found = [];
@@ -25413,7 +25759,7 @@ function nodeSql(table, node, params, path) {
25413
25759
  const terms = children.map(
25414
25760
  (child, index) => nodeSql(table, child, params, `${path}.${key}[${index}]`)
25415
25761
  );
25416
- return `(${terms.join(key === "or" ? " OR " : " AND ")})`;
25762
+ return joinConditions(terms, key === "or" ? "OR" : "AND");
25417
25763
  }
25418
25764
  function whereClause(table, filters, policy, params) {
25419
25765
  const parts = [];
@@ -25424,7 +25770,7 @@ function whereClause(table, filters, policy, params) {
25424
25770
  const client = filters.map(
25425
25771
  (node, index) => nodeSql(table, node, params, `filters[${index}]`)
25426
25772
  );
25427
- if (client.length > 0) parts.push(`(${client.join(" AND ")})`);
25773
+ if (client.length > 0) parts.push(joinConditions(client, "AND"));
25428
25774
  return parts.length === 0 ? "" : ` WHERE ${parts.join(" AND ")}`;
25429
25775
  }
25430
25776
  function orderClause(table, order) {
@@ -25626,7 +25972,7 @@ var Rls = class {
25626
25972
  const found = this.found(command);
25627
25973
  return [...found.permissive, ...found.restrictive];
25628
25974
  });
25629
- const key = asciiLower(table.name);
25975
+ const key = asciiLower(this.policyTable);
25630
25976
  if (hasSubqueries(all) && !this.state.active.includes(key))
25631
25977
  this.state.active.push(key);
25632
25978
  for (const command of commands) {
@@ -26432,138 +26778,916 @@ function compile(query, snapshot, decision, context, options = {}) {
26432
26778
  }
26433
26779
  }
26434
26780
 
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;
26781
+ // ../../packages/data-core/src/sql-depth.ts
26782
+ var D1_EXPRESSION_DEPTH = 100;
26783
+ var Unreadable = class extends Error {
26784
+ };
26785
+ function isWordStart(code) {
26786
+ return code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code >= 128;
26454
26787
  }
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;
26788
+ function isDigit2(code) {
26789
+ return code >= 48 && code <= 57;
26466
26790
  }
26467
- function realtimeTopic2(scopes) {
26468
- const [root] = scopes;
26469
- if (root?.table.name !== REALTIME_MESSAGES) return void 0;
26470
- const column2 = root.table.byName.get("topic");
26471
- return column2 ? { column: column2, scope: 0 } : void 0;
26791
+ function isWordPart(code) {
26792
+ return isWordStart(code) || isDigit2(code) || code === 36;
26472
26793
  }
26473
- function valueScopes(scopes, value) {
26474
- switch (value.kind) {
26475
- case "column": {
26476
- const found = resolve8(scopes, value.qualifier, value.name);
26477
- return found ? [found.scope] : [];
26478
- }
26479
- case "func":
26480
- return value.args.flatMap((arg) => valueScopes(scopes, arg));
26481
- case "cast":
26482
- return valueScopes(scopes, value.operand);
26483
- case "storage":
26484
- return valueScopes(scopes, value.column);
26485
- case "realtime": {
26486
- const found = realtimeTopic2(scopes);
26487
- return found ? [found.scope] : [];
26488
- }
26489
- default:
26490
- return [];
26491
- }
26794
+ function isSpace3(code) {
26795
+ return code === 32 || code >= 9 && code <= 13;
26492
26796
  }
26493
- function readsOutside(scopes, expr, depth, snapshot) {
26494
- if (expr === null) return false;
26495
- if (isValueExpr(expr))
26496
- return valueScopes(scopes, expr).some((scope) => scope < depth);
26497
- const values = (...items) => items.some(
26498
- (item) => valueScopes(scopes, item).some((scope) => scope < depth)
26499
- );
26500
- switch (expr.kind) {
26501
- case "and":
26502
- case "or":
26503
- return expr.args.some(
26504
- (arg) => readsOutside(scopes, arg, depth, snapshot)
26797
+ var OPERATORS_2 = /* @__PURE__ */ new Set([
26798
+ "->",
26799
+ "||",
26800
+ "<=",
26801
+ ">=",
26802
+ "<>",
26803
+ "!=",
26804
+ "==",
26805
+ "<<",
26806
+ ">>"
26807
+ ]);
26808
+ var OPERATORS_1 = "=<>+-*/%&|~(),.;";
26809
+ var NUMBER2 = /^(?:0[xX][0-9A-Fa-f_]+|[0-9_]*\.?[0-9_]*(?:[eE][+-]?[0-9_]+)?)/;
26810
+ function tokenize3(sql) {
26811
+ const tokens = [];
26812
+ const length = sql.length;
26813
+ let at = 0;
26814
+ const closing = (close) => {
26815
+ let from = at + 1;
26816
+ for (; ; ) {
26817
+ const found = sql.indexOf(close, from);
26818
+ if (found < 0) throw new Unreadable("an unclosed quote");
26819
+ if (close === "]" || sql[found + 1] !== close) return found;
26820
+ from = found + 2;
26821
+ }
26822
+ };
26823
+ while (at < length) {
26824
+ const code = sql.charCodeAt(at);
26825
+ const next = sql.charCodeAt(at + 1);
26826
+ if (isSpace3(code)) {
26827
+ at++;
26828
+ } else if (code === 45 && next === 45) {
26829
+ const end = sql.indexOf("\n", at);
26830
+ at = end < 0 ? length : end + 1;
26831
+ } else if (code === 47 && next === 42) {
26832
+ const end = sql.indexOf("*/", at + 2);
26833
+ at = end < 0 ? length : end + 2;
26834
+ } else if (code === 39) {
26835
+ at = closing("'") + 1;
26836
+ tokens.push({ kind: "literal" });
26837
+ } else if (code === 34 || code === 96 || code === 91) {
26838
+ at = closing(code === 91 ? "]" : String.fromCharCode(code)) + 1;
26839
+ tokens.push({ kind: "name" });
26840
+ } else if ((code === 88 || code === 120) && next === 39) {
26841
+ at++;
26842
+ at = closing("'") + 1;
26843
+ tokens.push({ kind: "literal" });
26844
+ } else if (isDigit2(code) || code === 46 && isDigit2(next)) {
26845
+ const text2 = NUMBER2.exec(sql.slice(at, at + 64))?.[0] || sql[at] || "";
26846
+ tokens.push(
26847
+ /^[0-9]+$/.test(text2) ? { kind: "literal", integer: Number(text2) } : { kind: "literal" }
26505
26848
  );
26506
- case "not":
26507
- return readsOutside(scopes, expr.arg, depth, snapshot);
26508
- case "compare":
26509
- case "distinct":
26510
- return values(expr.left, expr.right);
26511
- case "is":
26512
- case "like":
26513
- return values(expr.operand);
26514
- case "in":
26515
- return values(expr.operand, ...expr.list);
26516
- case "between":
26517
- return values(expr.operand, expr.low, expr.high);
26518
- case "array":
26519
- return values(expr.operand);
26520
- case "exists":
26521
- case "inSelect": {
26522
- if (expr.kind === "inSelect" && values(expr.operand)) return true;
26523
- const inner = subqueryScopes2(scopes, expr.subquery, snapshot);
26524
- if (!inner) return false;
26525
- return expr.subquery.select !== "*" && readsOutside(inner, expr.subquery.select, depth, snapshot) || readsOutside(inner, expr.subquery.where, depth, snapshot);
26526
- }
26527
- }
26528
- }
26529
- function subqueryScopes2(scopes, subquery, snapshot) {
26530
- const table = resolveTable(snapshot, subquery.table);
26531
- if (!table) return void 0;
26532
- return [
26533
- ...scopes,
26534
- { table, names: [asciiLower(subquery.alias ?? table.name)] }
26535
- ];
26536
- }
26537
- function conjuncts(expr) {
26538
- if (expr === null) return [];
26539
- if (!isValueExpr(expr) && expr.kind === "and")
26540
- return expr.args.flatMap(conjuncts);
26541
- return [expr];
26542
- }
26543
- function columnAffinityOf(column2) {
26544
- const affinity2 = columnAffinity(column2);
26545
- if (affinity2 === "TEXT") return "text";
26546
- if (affinity2 === "BLOB") return "blob";
26547
- return "numeric";
26548
- }
26549
- function operandColumn(scopes, value) {
26550
- if (value.kind === "realtime") return realtimeTopic2(scopes);
26551
- if (value.kind !== "column") return void 0;
26552
- return resolve8(scopes, value.qualifier, value.name);
26553
- }
26554
- function valueAffinity(scopes, value) {
26555
- if (value.kind === "cast") return "text";
26556
- const found = operandColumn(scopes, value);
26557
- return found ? columnAffinityOf(found.column) : "none";
26849
+ at += text2.length;
26850
+ } else if (code === 63 || code === 58 || code === 64 || code === 36) {
26851
+ at++;
26852
+ while (at < length && isWordPart(sql.charCodeAt(at))) at++;
26853
+ tokens.push({ kind: "param" });
26854
+ } else if (isWordStart(code)) {
26855
+ const start = at;
26856
+ while (at < length && isWordPart(sql.charCodeAt(at))) at++;
26857
+ const text2 = sql.slice(start, at);
26858
+ tokens.push({ kind: "word", text: text2, upper: text2.toUpperCase() });
26859
+ } else if (sql.startsWith("->>", at)) {
26860
+ tokens.push({ kind: "op", text: "->>" });
26861
+ at += 3;
26862
+ } else if (OPERATORS_2.has(sql.slice(at, at + 2))) {
26863
+ tokens.push({ kind: "op", text: sql.slice(at, at + 2) });
26864
+ at += 2;
26865
+ } else if (OPERATORS_1.includes(sql[at] ?? "")) {
26866
+ tokens.push({ kind: "op", text: sql[at] ?? "" });
26867
+ at++;
26868
+ } else throw new Unreadable(`an unexpected ${sql[at] ?? ""}`);
26869
+ }
26870
+ tokens.push({ kind: "end" });
26871
+ return tokens;
26872
+ }
26873
+ var CLAUSE_WORDS2 = /* @__PURE__ */ new Set([
26874
+ "AS",
26875
+ "CROSS",
26876
+ "DEFAULT",
26877
+ "DO",
26878
+ "EXCEPT",
26879
+ "FROM",
26880
+ "FULL",
26881
+ "GROUP",
26882
+ "HAVING",
26883
+ "INDEXED",
26884
+ "INNER",
26885
+ "INTERSECT",
26886
+ "JOIN",
26887
+ "LEFT",
26888
+ "LIMIT",
26889
+ "NATURAL",
26890
+ "NOT",
26891
+ "OFFSET",
26892
+ "ON",
26893
+ "ORDER",
26894
+ "OUTER",
26895
+ "RETURNING",
26896
+ "RIGHT",
26897
+ "SET",
26898
+ "UNION",
26899
+ "USING",
26900
+ "VALUES",
26901
+ "WHERE",
26902
+ "WINDOW"
26903
+ ]);
26904
+ var NOT_CONSTANT = /* @__PURE__ */ new Set([
26905
+ "avg",
26906
+ "changes",
26907
+ "count",
26908
+ "group_concat",
26909
+ "json_group_array",
26910
+ "json_group_object",
26911
+ "jsonb_group_array",
26912
+ "jsonb_group_object",
26913
+ "last_insert_rowid",
26914
+ "random",
26915
+ "randomblob",
26916
+ "sqlite_offset",
26917
+ "string_agg",
26918
+ "sum",
26919
+ "total",
26920
+ "total_changes"
26921
+ ]);
26922
+ var OR = 1;
26923
+ var AND = 2;
26924
+ var NOT = 3;
26925
+ var COMPARE = 4;
26926
+ var ORDER = 5;
26927
+ var ESCAPE = 6;
26928
+ var BITS = 7;
26929
+ var SUM = 8;
26930
+ var PRODUCT = 9;
26931
+ var CONCAT = 10;
26932
+ var COLLATE = 11;
26933
+ var UNARY = 12;
26934
+ var BINARY = {
26935
+ "=": COMPARE,
26936
+ "==": COMPARE,
26937
+ "!=": COMPARE,
26938
+ "<>": COMPARE,
26939
+ "<": ORDER,
26940
+ "<=": ORDER,
26941
+ ">": ORDER,
26942
+ ">=": ORDER,
26943
+ "&": BITS,
26944
+ "|": BITS,
26945
+ "<<": BITS,
26946
+ ">>": BITS,
26947
+ "+": SUM,
26948
+ "-": SUM,
26949
+ "*": PRODUCT,
26950
+ "/": PRODUCT,
26951
+ "%": PRODUCT,
26952
+ "||": CONCAT,
26953
+ "->": CONCAT,
26954
+ "->>": CONCAT
26955
+ };
26956
+ var LIKE_WORDS = /* @__PURE__ */ new Set(["LIKE", "GLOB", "REGEXP", "MATCH"]);
26957
+ var USABLE_OPERATORS = /* @__PURE__ */ new Set(["=", "==", "<", "<=", ">", ">="]);
26958
+ var Reader = class {
26959
+ constructor(tokens, planner) {
26960
+ this.tokens = tokens;
26961
+ this.planner = planner;
26962
+ }
26963
+ at = 0;
26964
+ /** The deepest node built, kept or not: each is checked as it is built. */
26965
+ deepest = 0;
26966
+ peek(offset = 0) {
26967
+ return this.tokens[this.at + offset] ?? { kind: "end" };
26968
+ }
26969
+ isWord(word, offset = 0) {
26970
+ const token = this.peek(offset);
26971
+ return token.kind === "word" && token.upper === word;
26972
+ }
26973
+ isOp(op, offset = 0) {
26974
+ const token = this.peek(offset);
26975
+ return token.kind === "op" && token.text === op;
26976
+ }
26977
+ isName(offset = 0) {
26978
+ const kind = this.peek(offset).kind;
26979
+ return kind === "word" || kind === "name";
26980
+ }
26981
+ takeWord(word) {
26982
+ if (!this.isWord(word)) return false;
26983
+ this.at++;
26984
+ return true;
26985
+ }
26986
+ takeOp(op) {
26987
+ if (!this.isOp(op)) return false;
26988
+ this.at++;
26989
+ return true;
26990
+ }
26991
+ expectWord(word) {
26992
+ if (!this.takeWord(word)) throw new Unreadable(`no ${word}`);
26993
+ }
26994
+ expectOp(op) {
26995
+ if (!this.takeOp(op)) throw new Unreadable(`no ${op}`);
26996
+ }
26997
+ name() {
26998
+ if (!this.isName()) throw new Unreadable("no name");
26999
+ this.at++;
27000
+ }
27001
+ /** `name` or `schema.name`. */
27002
+ qualifiedName() {
27003
+ this.name();
27004
+ if (this.isOp(".") && this.isName(1)) this.at += 2;
27005
+ }
27006
+ atEnd() {
27007
+ this.takeOp(";");
27008
+ return this.peek().kind === "end";
27009
+ }
27010
+ // --- nodes, as expr.c builds them ---
27011
+ made(node) {
27012
+ this.deepest = Math.max(this.deepest, node.height);
27013
+ return node;
27014
+ }
27015
+ leaf(extra = {}) {
27016
+ return this.made({ height: 1, constant: true, subqueries: [], ...extra });
27017
+ }
27018
+ /** sqlite3PExpr, exprSetHeight: one deeper than the deepest part. */
27019
+ over(parts, extra = {}) {
27020
+ return this.made(this.unchecked(parts, extra));
27021
+ }
27022
+ /**
27023
+ * sqlite3ExprAttachSubtrees alone, as for a CAST: its height is set and
27024
+ * not checked, so it counts only through what holds it.
27025
+ */
27026
+ unchecked(parts, extra = {}) {
27027
+ let height = 0;
27028
+ let constant = true;
27029
+ let hasFunc = false;
27030
+ let hasSubquery = false;
27031
+ let subqueries = [];
27032
+ for (const part of parts) {
27033
+ if (part.height > height) height = part.height;
27034
+ if (!part.constant) constant = false;
27035
+ if (part.hasFunc) hasFunc = true;
27036
+ if (part.hasSubquery) hasSubquery = true;
27037
+ if (part.subqueries.length > 0)
27038
+ subqueries = subqueries.length === 0 ? part.subqueries : [...subqueries, ...part.subqueries];
27039
+ }
27040
+ const node = { height: height + 1, constant, subqueries, ...extra };
27041
+ if (hasFunc) node.hasFunc ??= true;
27042
+ if (hasSubquery) node.hasSubquery ??= true;
27043
+ return node;
27044
+ }
27045
+ /** A node holding a SELECT: EXISTS, `(SELECT …)`, `x IN (SELECT …)`. */
27046
+ holding(left, select) {
27047
+ const node = {
27048
+ height: Math.max(select.height, left?.height ?? 0) + 1,
27049
+ constant: false,
27050
+ subqueries: [...left?.subqueries ?? [], select],
27051
+ hasSubquery: true
27052
+ };
27053
+ if (left?.hasFunc) node.hasFunc = true;
27054
+ if (left?.column) node.usable = true;
27055
+ return this.made(node);
27056
+ }
27057
+ // --- expressions, by parse.y's precedence ---
27058
+ expr(min = OR) {
27059
+ let left = this.prefix();
27060
+ for (; ; ) {
27061
+ const token = this.peek();
27062
+ if (token.kind === "op") {
27063
+ const level = BINARY[token.text];
27064
+ if (level === void 0 || level < min) return left;
27065
+ this.at++;
27066
+ const right = this.expr(level + 1);
27067
+ if (token.text === "->" || token.text === "->>") {
27068
+ left = this.over([left, right], { hasFunc: true });
27069
+ continue;
27070
+ }
27071
+ const compared = USABLE_OPERATORS.has(token.text);
27072
+ left = this.over(
27073
+ [left, right],
27074
+ compared && (left.column || right.column) ? { usable: true } : {}
27075
+ );
27076
+ continue;
27077
+ }
27078
+ if (token.kind !== "word") return left;
27079
+ const word = token.upper;
27080
+ if (word === "OR" || word === "AND") {
27081
+ if ((word === "OR" ? OR : AND) < min) return left;
27082
+ this.at++;
27083
+ const right = this.expr((word === "OR" ? OR : AND) + 1);
27084
+ if (word === "OR")
27085
+ left = this.over([left, right], { or: [left, right] });
27086
+ else if ((left.isFalse || right.isFalse) && !left.hasFunc && !right.hasFunc)
27087
+ left = this.leaf({ isFalse: true, literal: true });
27088
+ else left = this.over([left, right], { and: [left, right] });
27089
+ continue;
27090
+ }
27091
+ if (word === "COLLATE") {
27092
+ if (COLLATE < min) return left;
27093
+ this.at++;
27094
+ this.name();
27095
+ left = this.made({
27096
+ height: 1,
27097
+ constant: left.constant,
27098
+ subqueries: left.subqueries,
27099
+ ...left.column ? { column: true } : {}
27100
+ });
27101
+ continue;
27102
+ }
27103
+ if (COMPARE < min) return left;
27104
+ const negated = word === "NOT" ? 1 : 0;
27105
+ const after = this.peek(negated);
27106
+ const verb = after.kind === "word" ? after.upper : "";
27107
+ if (word === "IS") {
27108
+ this.at++;
27109
+ let not = this.takeWord("NOT");
27110
+ if (this.takeWord("DISTINCT")) {
27111
+ this.expectWord("FROM");
27112
+ not = !not;
27113
+ }
27114
+ const right = this.expr(COMPARE + 1);
27115
+ left = right.isNull ? this.nullTest(left, !not) : this.over(
27116
+ [left, right],
27117
+ !not && (left.column || right.column) ? { usable: true } : {}
27118
+ );
27119
+ } else if (word === "ISNULL" || word === "NOTNULL") {
27120
+ this.at++;
27121
+ left = this.nullTest(left, word === "ISNULL");
27122
+ } else if (negated && verb === "NULL") {
27123
+ this.at += 2;
27124
+ left = this.nullTest(left, false);
27125
+ } else if (LIKE_WORDS.has(verb)) {
27126
+ this.at += negated + 1;
27127
+ const args = [this.expr(COMPARE + 1), left];
27128
+ if (this.takeWord("ESCAPE")) args.push(this.expr(ESCAPE + 1));
27129
+ left = this.over(args, { hasFunc: true });
27130
+ if (negated) left = this.over([left]);
27131
+ } else if (verb === "BETWEEN") {
27132
+ this.at += negated + 1;
27133
+ const low = this.expr(NOT);
27134
+ this.expectWord("AND");
27135
+ const high = this.expr(COMPARE + 1);
27136
+ left = this.over([left, low, high]);
27137
+ if (negated) left = this.over([left]);
27138
+ } else if (verb === "IN") {
27139
+ this.at += negated + 1;
27140
+ left = this.inList(left, negated === 1);
27141
+ } else return left;
27142
+ }
27143
+ }
27144
+ /**
27145
+ * IS NULL (`isNull`) or IS NOT NULL: on a literal, the answer itself; on
27146
+ * a column, a term the planner can use when it is IS NULL.
27147
+ */
27148
+ nullTest(left, isNull) {
27149
+ if (left.literal)
27150
+ return this.leaf(
27151
+ isNull ? { isFalse: true, literal: true } : { literal: true }
27152
+ );
27153
+ return this.over([left], isNull && left.column ? { usable: true } : {});
27154
+ }
27155
+ inList(left, negated) {
27156
+ this.expectOp("(");
27157
+ if (this.takeOp(")"))
27158
+ return left.hasFunc ? this.over([left]) : this.leaf(negated ? {} : { isFalse: true });
27159
+ let node;
27160
+ if (this.startsSelect()) {
27161
+ node = this.holding(left, this.select());
27162
+ } else {
27163
+ const items = this.list();
27164
+ const [only] = items;
27165
+ const usable = left.column ? { usable: true } : {};
27166
+ if (items.length === 1 && only?.select)
27167
+ node = this.holding(left, only.select);
27168
+ else if (items.length === 1 && only?.constant)
27169
+ node = this.over([left, this.over([only])], usable);
27170
+ else node = this.over([left, ...items], usable);
27171
+ }
27172
+ this.expectOp(")");
27173
+ return negated ? this.over([node]) : node;
27174
+ }
27175
+ list() {
27176
+ const items = [this.expr()];
27177
+ while (this.takeOp(",")) items.push(this.expr());
27178
+ return items;
27179
+ }
27180
+ prefix() {
27181
+ const token = this.peek();
27182
+ this.at++;
27183
+ switch (token.kind) {
27184
+ case "literal":
27185
+ return this.leaf(
27186
+ token.integer === 0 ? { isFalse: true, literal: true } : { literal: true }
27187
+ );
27188
+ case "param":
27189
+ return this.leaf();
27190
+ case "name":
27191
+ return this.column();
27192
+ case "end":
27193
+ throw new Unreadable("no expression");
27194
+ case "op":
27195
+ if (token.text === "(") {
27196
+ if (this.startsSelect()) {
27197
+ const select = this.select();
27198
+ this.expectOp(")");
27199
+ return { ...this.holding(null, select), select };
27200
+ }
27201
+ const items = this.list();
27202
+ this.expectOp(")");
27203
+ if (items.length === 1 && items[0]) return items[0];
27204
+ throw new Unreadable("a row value");
27205
+ }
27206
+ if (token.text === "-" || token.text === "+") {
27207
+ const operand = this.expr(UNARY);
27208
+ return this.over([operand], operand.literal ? { literal: true } : {});
27209
+ }
27210
+ if (token.text === "~") return this.over([this.expr(UNARY)]);
27211
+ throw new Unreadable(`an unexpected ${token.text}`);
27212
+ case "word":
27213
+ break;
27214
+ }
27215
+ switch (token.upper) {
27216
+ case "NULL":
27217
+ return this.leaf({ isNull: true });
27218
+ case "TRUE":
27219
+ case "FALSE":
27220
+ return this.leaf();
27221
+ case "NOT":
27222
+ return this.over([this.expr(NOT)]);
27223
+ case "EXISTS": {
27224
+ this.expectOp("(");
27225
+ const select = this.select();
27226
+ this.expectOp(")");
27227
+ return this.holding(null, select);
27228
+ }
27229
+ case "CAST": {
27230
+ this.expectOp("(");
27231
+ const value = this.expr();
27232
+ this.expectWord("AS");
27233
+ this.typeName();
27234
+ this.expectOp(")");
27235
+ return this.unchecked([value]);
27236
+ }
27237
+ case "CASE": {
27238
+ const parts = [];
27239
+ if (!this.isWord("WHEN")) parts.push(this.expr());
27240
+ while (this.takeWord("WHEN")) {
27241
+ parts.push(this.expr());
27242
+ this.expectWord("THEN");
27243
+ parts.push(this.expr());
27244
+ }
27245
+ if (this.takeWord("ELSE")) parts.push(this.expr());
27246
+ this.expectWord("END");
27247
+ return this.over(parts);
27248
+ }
27249
+ default:
27250
+ break;
27251
+ }
27252
+ if (!this.takeOp("(")) return this.column();
27253
+ let args = [];
27254
+ if (!this.takeOp("*") && !this.isOp(")")) {
27255
+ if (!this.takeWord("DISTINCT")) this.takeWord("ALL");
27256
+ args = this.list();
27257
+ }
27258
+ this.expectOp(")");
27259
+ if (this.isWord("FILTER") || this.isWord("OVER"))
27260
+ throw new Unreadable("a window function");
27261
+ return this.over(args, {
27262
+ hasFunc: true,
27263
+ constant: !NOT_CONSTANT.has(token.text.toLowerCase()) && args.every((arg) => arg.constant)
27264
+ });
27265
+ }
27266
+ /**
27267
+ * `a`, `a.b` or `a.b.c`, its first name just read: TK_ID, and a TK_DOT a
27268
+ * dot — a.b.c is DOT(a, DOT(b, c)).
27269
+ */
27270
+ column() {
27271
+ let node = this.leaf({ constant: false, column: true });
27272
+ while (this.isOp(".") && this.isName(1)) {
27273
+ this.at += 2;
27274
+ node = this.over([this.leaf({ constant: false }), node], {
27275
+ column: true
27276
+ });
27277
+ }
27278
+ return node;
27279
+ }
27280
+ typeName() {
27281
+ do
27282
+ this.name();
27283
+ while (this.isName());
27284
+ if (this.takeOp("(")) {
27285
+ while (!this.takeOp(")")) {
27286
+ if (this.peek().kind === "end") throw new Unreadable("no )");
27287
+ this.at++;
27288
+ }
27289
+ }
27290
+ }
27291
+ /**
27292
+ * A WHERE, and the chain an OR in it may bring when each of its branches
27293
+ * could be read through an index — a column compared, or an AND holding
27294
+ * one (exprAnalyzeOrTerm) — whether or not the table has that index: the
27295
+ * other terms the planner can use, without a subquery, ANDed one by one
27296
+ * from the first, under one more AND (wherecode.c, WHERE_MULTI_OR).
27297
+ */
27298
+ where() {
27299
+ const where = this.expr();
27300
+ if (!this.planner) return where;
27301
+ const terms = split(where, "and");
27302
+ for (const or of terms) {
27303
+ if (!or.or || !split(or, "or").every(
27304
+ (branch) => branch.usable || split(branch, "and").some((term) => term.usable)
27305
+ ))
27306
+ continue;
27307
+ let chain = 0;
27308
+ for (const term of terms)
27309
+ if (term !== or && (term.usable || term.or) && !term.hasSubquery)
27310
+ chain = chain === 0 ? term.height : Math.max(chain, term.height) + 1;
27311
+ if (chain > 0) this.deepest = Math.max(this.deepest, chain + 1);
27312
+ }
27313
+ return where;
27314
+ }
27315
+ // --- statements ---
27316
+ startsSelect() {
27317
+ return this.isWord("SELECT") || this.isWord("VALUES") || this.isWord("WITH");
27318
+ }
27319
+ /** A SELECT, compound or not, with its ORDER BY and LIMIT. */
27320
+ select() {
27321
+ if (this.isWord("WITH")) throw new Unreadable("WITH");
27322
+ const whole = { height: 0, resolved: [], from: [] };
27323
+ const counted = (node) => {
27324
+ whole.height = Math.max(whole.height, node.height);
27325
+ whole.resolved.push(node);
27326
+ };
27327
+ do {
27328
+ if (this.takeWord("VALUES")) {
27329
+ do {
27330
+ this.expectOp("(");
27331
+ for (const item of this.list()) counted(item);
27332
+ this.expectOp(")");
27333
+ } while (this.takeOp(","));
27334
+ continue;
27335
+ }
27336
+ this.expectWord("SELECT");
27337
+ if (!this.takeWord("DISTINCT")) this.takeWord("ALL");
27338
+ const stars = [];
27339
+ do {
27340
+ if (this.takeOp("*")) stars.push(this.leaf());
27341
+ else if (this.isName() && this.isOp(".", 1) && this.isOp("*", 2)) {
27342
+ this.at += 3;
27343
+ stars.push(this.over([this.leaf(), this.leaf()]));
27344
+ } else {
27345
+ counted(this.expr());
27346
+ this.alias();
27347
+ }
27348
+ } while (this.takeOp(","));
27349
+ const sources = this.takeWord("FROM") ? this.from(whole) : 0;
27350
+ for (const star of stars) {
27351
+ whole.height = Math.max(whole.height, star.height);
27352
+ whole.resolved.push(
27353
+ sources > 1 ? this.made({ height: 3, constant: false, subqueries: [] }) : star
27354
+ );
27355
+ }
27356
+ if (this.takeWord("WHERE")) counted(this.where());
27357
+ if (this.takeWord("GROUP")) {
27358
+ this.expectWord("BY");
27359
+ for (const item of this.list()) counted(item);
27360
+ if (this.takeWord("HAVING")) counted(this.expr());
27361
+ }
27362
+ } while (this.compound());
27363
+ if (this.takeWord("ORDER")) {
27364
+ this.expectWord("BY");
27365
+ do {
27366
+ counted(this.expr());
27367
+ if (!this.takeWord("ASC")) this.takeWord("DESC");
27368
+ if (this.takeWord("NULLS") && !this.takeWord("FIRST"))
27369
+ this.expectWord("LAST");
27370
+ } while (this.takeOp(","));
27371
+ }
27372
+ if (this.takeWord("LIMIT")) {
27373
+ const parts = [this.expr()];
27374
+ if (this.takeWord("OFFSET") || this.takeOp(",")) parts.push(this.expr());
27375
+ counted(this.over(parts));
27376
+ }
27377
+ return whole;
27378
+ }
27379
+ compound() {
27380
+ if (this.takeWord("UNION")) {
27381
+ this.takeWord("ALL");
27382
+ return true;
27383
+ }
27384
+ return this.takeWord("INTERSECT") || this.takeWord("EXCEPT");
27385
+ }
27386
+ alias() {
27387
+ if (this.takeWord("AS")) {
27388
+ this.name();
27389
+ return;
27390
+ }
27391
+ const token = this.peek();
27392
+ if (token.kind === "name" || token.kind === "word" && !CLAUSE_WORDS2.has(token.upper))
27393
+ this.at++;
27394
+ }
27395
+ /** The FROM items, into `into`; how many there are. */
27396
+ from(into) {
27397
+ let sources = 0;
27398
+ do {
27399
+ sources++;
27400
+ if (this.takeOp("(")) {
27401
+ if (!this.startsSelect()) throw new Unreadable("a parenthesized join");
27402
+ into.from.push(this.select());
27403
+ this.expectOp(")");
27404
+ } else {
27405
+ this.qualifiedName();
27406
+ if (this.takeOp("(")) {
27407
+ if (!this.isOp(")"))
27408
+ for (const arg of this.list()) {
27409
+ into.resolved.push(arg);
27410
+ this.deepest = Math.max(this.deepest, arg.height + 2);
27411
+ }
27412
+ this.expectOp(")");
27413
+ }
27414
+ }
27415
+ this.alias();
27416
+ if (this.takeWord("INDEXED")) {
27417
+ this.expectWord("BY");
27418
+ this.name();
27419
+ } else if (this.isWord("NOT") && this.isWord("INDEXED", 1)) this.at += 2;
27420
+ if (this.isWord("ON") || this.isWord("USING"))
27421
+ throw new Unreadable("a join constraint");
27422
+ } while (this.join());
27423
+ return sources;
27424
+ }
27425
+ join() {
27426
+ if (this.takeOp(",")) return true;
27427
+ const start = this.at;
27428
+ this.takeWord("NATURAL");
27429
+ if (this.takeWord("LEFT") || this.takeWord("RIGHT") || this.takeWord("FULL"))
27430
+ this.takeWord("OUTER");
27431
+ else if (!this.takeWord("INNER")) this.takeWord("CROSS");
27432
+ if (this.takeWord("JOIN")) return true;
27433
+ this.at = start;
27434
+ return false;
27435
+ }
27436
+ names() {
27437
+ this.expectOp("(");
27438
+ do
27439
+ this.name();
27440
+ while (this.takeOp(","));
27441
+ this.expectOp(")");
27442
+ }
27443
+ returning(into) {
27444
+ if (!this.takeWord("RETURNING")) return;
27445
+ do {
27446
+ if (this.takeOp("*")) into.push(this.leaf());
27447
+ else {
27448
+ into.push(this.expr());
27449
+ this.alias();
27450
+ }
27451
+ } while (this.takeOp(","));
27452
+ }
27453
+ assignments(into) {
27454
+ do {
27455
+ if (this.isOp("(")) this.names();
27456
+ else this.name();
27457
+ this.expectOp("=");
27458
+ into.push(this.expr());
27459
+ } while (this.takeOp(","));
27460
+ }
27461
+ /** The statement: each of its expressions resolved from 0. */
27462
+ statement() {
27463
+ const top = { height: 0, resolved: [], from: [] };
27464
+ if (this.startsSelect()) {
27465
+ top.from.push(this.select());
27466
+ } else if (this.takeWord("INSERT") || this.takeWord("REPLACE")) {
27467
+ if (this.takeWord("OR")) this.name();
27468
+ this.expectWord("INTO");
27469
+ this.qualifiedName();
27470
+ if (this.takeWord("AS")) this.name();
27471
+ if (this.isOp("(")) this.names();
27472
+ if (this.takeWord("DEFAULT")) this.expectWord("VALUES");
27473
+ else top.from.push(this.select());
27474
+ while (this.takeWord("ON")) {
27475
+ this.expectWord("CONFLICT");
27476
+ if (this.takeOp("(")) {
27477
+ top.resolved.push(...this.list());
27478
+ this.expectOp(")");
27479
+ if (this.takeWord("WHERE")) top.resolved.push(this.expr());
27480
+ }
27481
+ this.expectWord("DO");
27482
+ if (this.takeWord("NOTHING")) continue;
27483
+ this.expectWord("UPDATE");
27484
+ this.expectWord("SET");
27485
+ this.assignments(top.resolved);
27486
+ if (this.takeWord("WHERE")) top.resolved.push(this.expr());
27487
+ }
27488
+ this.returning(top.resolved);
27489
+ } else if (this.takeWord("UPDATE")) {
27490
+ if (this.takeWord("OR")) this.name();
27491
+ this.qualifiedName();
27492
+ this.alias();
27493
+ if (this.takeWord("INDEXED")) {
27494
+ this.expectWord("BY");
27495
+ this.name();
27496
+ }
27497
+ this.expectWord("SET");
27498
+ this.assignments(top.resolved);
27499
+ if (this.isWord("FROM")) throw new Unreadable("UPDATE \u2026 FROM");
27500
+ if (this.takeWord("WHERE")) top.resolved.push(this.where());
27501
+ this.returning(top.resolved);
27502
+ } else if (this.takeWord("DELETE")) {
27503
+ this.expectWord("FROM");
27504
+ this.qualifiedName();
27505
+ this.alias();
27506
+ if (this.takeWord("INDEXED")) {
27507
+ this.expectWord("BY");
27508
+ this.name();
27509
+ }
27510
+ if (this.takeWord("WHERE")) top.resolved.push(this.where());
27511
+ this.returning(top.resolved);
27512
+ } else throw new Unreadable("not a statement naro compiles");
27513
+ return top;
27514
+ }
27515
+ };
27516
+ function split(node, at) {
27517
+ const found = [];
27518
+ const pending = [node];
27519
+ for (let next = pending.pop(); next; next = pending.pop()) {
27520
+ const sides = next[at];
27521
+ if (sides) pending.push(sides[1], sides[0]);
27522
+ else found.push(next);
27523
+ }
27524
+ return found;
27525
+ }
27526
+ function nodeNeed(node, base2) {
27527
+ const at = base2 + node.height;
27528
+ let need = at;
27529
+ for (const select of node.subqueries)
27530
+ need = Math.max(need, selectNeed(select, at));
27531
+ return need;
27532
+ }
27533
+ function selectNeed(select, base2) {
27534
+ let need = base2;
27535
+ for (const node of select.resolved)
27536
+ need = Math.max(need, nodeNeed(node, base2));
27537
+ for (const inner of select.from)
27538
+ need = Math.max(need, selectNeed(inner, base2));
27539
+ return need;
27540
+ }
27541
+ function expressionDepth(sql, { planner = true } = {}) {
27542
+ try {
27543
+ const reader = new Reader(tokenize3(sql), planner);
27544
+ const top = reader.statement();
27545
+ if (!reader.atEnd()) return void 0;
27546
+ return Math.max(reader.deepest, selectNeed(top, 0));
27547
+ } catch (error51) {
27548
+ if (error51 instanceof Unreadable) return void 0;
27549
+ throw error51;
27550
+ }
27551
+ }
27552
+
27553
+ // ../../packages/data-core/src/policy-index.ts
27554
+ 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`;
27555
+ function indexesFromRows(rows) {
27556
+ const tables = /* @__PURE__ */ new Map();
27557
+ for (const row of rows) {
27558
+ const table = row.table_name;
27559
+ const column2 = row.column_name;
27560
+ if (typeof table !== "string" || typeof column2 !== "string") continue;
27561
+ if (Number(row.partial) !== 0) continue;
27562
+ const key = asciiLower(table);
27563
+ const columns = tables.get(key) ?? /* @__PURE__ */ new Map();
27564
+ tables.set(key, columns);
27565
+ const collations = columns.get(asciiLower(column2)) ?? [];
27566
+ collations.push(
27567
+ typeof row.collation === "string" ? row.collation.toUpperCase() : "BINARY"
27568
+ );
27569
+ columns.set(asciiLower(column2), collations);
27570
+ }
27571
+ return tables;
27572
+ }
27573
+ var STORAGE_OBJECTS_INDEXES = /* @__PURE__ */ new Map(
27574
+ [
27575
+ ["id", ["BINARY"]],
27576
+ ["bucket_id", ["BINARY"]]
27577
+ ]
27578
+ );
27579
+ function resolve8(scopes, qualifier, name) {
27580
+ const column2 = asciiLower(name);
27581
+ for (let index = scopes.length - 1; index >= 0; index--) {
27582
+ const scope = scopes[index];
27583
+ if (qualifier !== null && !scope.names.includes(asciiLower(qualifier)))
27584
+ continue;
27585
+ const found = scope.table.byName.get(column2);
27586
+ if (found) return { column: found, scope: index };
27587
+ if (qualifier !== null) return void 0;
27588
+ }
27589
+ return void 0;
27590
+ }
27591
+ function realtimeTopic2(scopes) {
27592
+ const [root] = scopes;
27593
+ if (root?.table.name !== REALTIME_MESSAGES) return void 0;
27594
+ const column2 = root.table.byName.get("topic");
27595
+ return column2 ? { column: column2, scope: 0 } : void 0;
27596
+ }
27597
+ function valueScopes(scopes, value) {
27598
+ switch (value.kind) {
27599
+ case "column": {
27600
+ const found = resolve8(scopes, value.qualifier, value.name);
27601
+ return found ? [found.scope] : [];
27602
+ }
27603
+ case "func":
27604
+ return value.args.flatMap((arg) => valueScopes(scopes, arg));
27605
+ case "cast":
27606
+ return valueScopes(scopes, value.operand);
27607
+ case "storage":
27608
+ return valueScopes(scopes, value.column);
27609
+ case "realtime": {
27610
+ const found = realtimeTopic2(scopes);
27611
+ return found ? [found.scope] : [];
27612
+ }
27613
+ default:
27614
+ return [];
27615
+ }
27616
+ }
27617
+ function readsOutside(scopes, expr, depth, snapshot) {
27618
+ if (expr === null) return false;
27619
+ if (isValueExpr(expr))
27620
+ return valueScopes(scopes, expr).some((scope) => scope < depth);
27621
+ const values = (...items) => items.some(
27622
+ (item) => valueScopes(scopes, item).some((scope) => scope < depth)
27623
+ );
27624
+ switch (expr.kind) {
27625
+ case "and":
27626
+ case "or":
27627
+ return expr.args.some(
27628
+ (arg) => readsOutside(scopes, arg, depth, snapshot)
27629
+ );
27630
+ case "not":
27631
+ return readsOutside(scopes, expr.arg, depth, snapshot);
27632
+ case "compare":
27633
+ case "distinct":
27634
+ return values(expr.left, expr.right);
27635
+ case "is":
27636
+ case "like":
27637
+ return values(expr.operand);
27638
+ case "in":
27639
+ return values(expr.operand, ...expr.list);
27640
+ case "between":
27641
+ return values(expr.operand, expr.low, expr.high);
27642
+ case "array":
27643
+ return values(expr.operand);
27644
+ case "exists":
27645
+ case "inSelect": {
27646
+ if (expr.kind === "inSelect" && values(expr.operand)) return true;
27647
+ const inner = subqueryScopes2(scopes, expr.subquery, snapshot);
27648
+ if (!inner) return false;
27649
+ return expr.subquery.select !== "*" && readsOutside(inner, expr.subquery.select, depth, snapshot) || readsOutside(inner, expr.subquery.where, depth, snapshot);
27650
+ }
27651
+ }
27652
+ }
27653
+ function subqueryScopes2(scopes, subquery, snapshot) {
27654
+ const table = resolveSubqueryTable(snapshot, subquery.table);
27655
+ if (!table) return void 0;
27656
+ return [...scopes, { table, names: [asciiLower(subqueryName(subquery))] }];
27657
+ }
27658
+ function conjuncts(expr) {
27659
+ if (expr === null) return [];
27660
+ if (!isValueExpr(expr) && expr.kind === "and")
27661
+ return expr.args.flatMap(conjuncts);
27662
+ return [expr];
27663
+ }
27664
+ function columnAffinityOf(column2) {
27665
+ const affinity2 = columnAffinity(column2);
27666
+ if (affinity2 === "TEXT") return "text";
27667
+ if (affinity2 === "BLOB") return "blob";
27668
+ return "numeric";
27669
+ }
27670
+ function operandColumn(scopes, operand) {
27671
+ const value = comparedValue(operand);
27672
+ if (value.kind === "realtime") return realtimeTopic2(scopes);
27673
+ if (value.kind !== "column") return void 0;
27674
+ return resolve8(scopes, value.qualifier, value.name);
27675
+ }
27676
+ function valueAffinity(scopes, value) {
27677
+ if (comparedValue(value).kind === "cast") return "text";
27678
+ const found = operandColumn(scopes, value);
27679
+ return found ? columnAffinityOf(found.column) : "none";
26558
27680
  }
26559
27681
  function valueCollation(scopes, value) {
26560
- if (value.kind === "cast") return valueCollation(scopes, value.operand);
27682
+ const compared = comparedValue(value);
27683
+ if (compared.kind === "cast") return valueCollation(scopes, compared.operand);
26561
27684
  const found = operandColumn(scopes, value);
26562
27685
  return found ? found.column.collation : void 0;
26563
27686
  }
26564
27687
  function waysIn(scopes, where) {
26565
27688
  const inner = scopes.length - 1;
26566
- const own2 = (value) => {
27689
+ const own2 = (operand) => {
27690
+ const value = comparedValue(operand);
26567
27691
  if (value.kind !== "column") return void 0;
26568
27692
  const found = resolve8(scopes, value.qualifier, value.name);
26569
27693
  return found?.scope === inner ? found.column : void 0;
@@ -26611,7 +27735,7 @@ function isRowid(table, column2) {
26611
27735
  function blocked(table, indexes, way) {
26612
27736
  if (isRowid(table, way.column))
26613
27737
  return way.affinity === "text" ? "affinity" : void 0;
26614
- const collations = indexes.get(asciiLower(table.name))?.get(asciiLower(way.column.name));
27738
+ const collations = (table.name === STORAGE_OBJECTS_TABLE ? STORAGE_OBJECTS_INDEXES : indexes.get(asciiLower(table.name)))?.get(asciiLower(way.column.name));
26615
27739
  if (!collations || collations.length === 0) return "none";
26616
27740
  const wanted = way.collation === void 0 ? way.column.collation : way.collation;
26617
27741
  if (wanted !== null && !collations.includes(wanted)) return "collation";
@@ -26622,9 +27746,17 @@ function blocked(table, indexes, way) {
26622
27746
  }
26623
27747
  function problemMessage(policy, outer, table, ways, why) {
26624
27748
  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}`;
27749
+ const start = `Policy ${policy.name} on ${tableLabel(outer)}: its subquery on ${tableLabel(table)} ${runs}`;
26626
27750
  const override = "or send it with allow_unindexed=1 (naro policy create --allow-unindexed) to accept a scan of every row";
26627
27751
  const [way] = ways;
27752
+ if (table.name === STORAGE_OBJECTS_TABLE) {
27753
+ const column3 = way ? `${STORAGE_OBJECTS}.${way.column.name}` : "";
27754
+ if (way && why === "collation")
27755
+ 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}.`;
27756
+ if (way && why === "affinity")
27757
+ 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}.`;
27758
+ 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}.`;
27759
+ }
26628
27760
  if (!way)
26629
27761
  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
27762
  const column2 = way.column.name;
@@ -26664,12 +27796,12 @@ function unindexedSubqueries(snapshot, indexes, policies) {
26664
27796
  const ways = waysIn(inner, expr.subquery.where);
26665
27797
  const reasons = ways.map((way) => blocked(table, indexes, way));
26666
27798
  if (!reasons.includes(void 0)) {
26667
- const order = ["none", "collation", "affinity"];
27799
+ const order = table.name === STORAGE_OBJECTS_TABLE ? ["collation", "affinity", "none"] : ["none", "collation", "affinity"];
26668
27800
  const best = order.map((reason) => reasons.indexOf(reason)).find((index) => index >= 0);
26669
27801
  const way = best === void 0 ? void 0 : ways[best];
26670
27802
  found.push({
26671
27803
  policy,
26672
- table: table.name,
27804
+ table: tableLabel(table),
26673
27805
  message: problemMessage(
26674
27806
  policy,
26675
27807
  outer,
@@ -26863,6 +27995,14 @@ function privateChannelAccess(input) {
26863
27995
  return { ok: true, statement: { sql, params } };
26864
27996
  }
26865
27997
 
27998
+ // ../../packages/data-core/src/storage-transfer.ts
27999
+ function moveToPending(to) {
28000
+ return JSON.stringify({ moveTo: to });
28001
+ }
28002
+ function moveFromPending(from) {
28003
+ return JSON.stringify({ moveFrom: from });
28004
+ }
28005
+
26866
28006
  // ../../packages/data-core/src/storage-sql.ts
26867
28007
  var STORAGE_NONCE_MAX_DIGITS = 64;
26868
28008
  function nonceOf(nonce) {
@@ -26992,6 +28132,73 @@ function claimOverwrite(i) {
26992
28132
  [[i.lockId, i.expiresAt, i.pending, i.bucket, i.name], room.values]
26993
28133
  );
26994
28134
  }
28135
+ var MOVE_KEPT_COLUMNS = [
28136
+ "id",
28137
+ "bucket_id",
28138
+ "owner",
28139
+ "owner_id",
28140
+ "version",
28141
+ "created_at",
28142
+ "updated_at",
28143
+ "metadata"
28144
+ ];
28145
+ function moveCheckRow(i) {
28146
+ return {
28147
+ row: { name: i.to },
28148
+ stored: { alias: STORAGE_OBJECT_ALIAS, columns: MOVE_KEPT_COLUMNS }
28149
+ };
28150
+ }
28151
+ function claimMove(i) {
28152
+ const nonce = nonceOf(i.nonce);
28153
+ const slotNonce = nonceOf(i.slotNonce);
28154
+ const source = compose(
28155
+ [
28156
+ `UPDATE ${STORAGE_OBJECTS_TABLE} AS o SET lock_id = ?, lock_expires_at = ?, pending = ? WHERE o.id = ? AND o.name = ? AND CASE WHEN NOT (`,
28157
+ i.selectUsing,
28158
+ `) 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 (`,
28159
+ i.updateUsing,
28160
+ `) THEN ${marker("denied", nonce)} WHEN (`,
28161
+ i.check,
28162
+ // The check is 1 or fails with its marker; anything else is no
28163
+ // check that held — refused as the update policy refuses.
28164
+ `) <> 1 THEN ${marker("denied", nonce)} WHEN ${PROJECT_BYTES} + o.size > ? THEN ${marker("quota", nonce)} ELSE 1 END RETURNING id, size`
28165
+ ],
28166
+ [
28167
+ [i.lockId, i.expiresAt, moveToPending(i.to), i.id, i.from],
28168
+ [i.version],
28169
+ [i.limitBytes]
28170
+ ]
28171
+ );
28172
+ const slot = compose(
28173
+ [
28174
+ `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`
28175
+ ],
28176
+ [
28177
+ [
28178
+ i.slotId,
28179
+ i.to,
28180
+ objectParent(i.to),
28181
+ moveFromPending(i.from),
28182
+ i.id,
28183
+ i.lockId,
28184
+ i.to,
28185
+ i.to
28186
+ ]
28187
+ ]
28188
+ );
28189
+ return [source, slot];
28190
+ }
28191
+ function copySourceGuard(i) {
28192
+ const nonce = nonceOf(i.nonce);
28193
+ return compose(
28194
+ [
28195
+ `SELECT CASE WHEN EXISTS (SELECT 1 FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.id = ? AND o.name = ? AND (`,
28196
+ i.using,
28197
+ `)) THEN 1 ELSE ${marker("source", nonce)} END AS ok`
28198
+ ],
28199
+ [[i.id, i.name]]
28200
+ );
28201
+ }
26995
28202
  function claimDelete(i) {
26996
28203
  return compose(
26997
28204
  [
@@ -27023,6 +28230,16 @@ function readObjects(i) {
27023
28230
  [[i.bucket, JSON.stringify([...new Set(i.names)])]]
27024
28231
  );
27025
28232
  }
28233
+ function readTransferSource(i) {
28234
+ return compose(
28235
+ [
28236
+ `SELECT o.id, o.version, o.size, o.metadata FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND o.name = ? AND (`,
28237
+ i.using,
28238
+ ")"
28239
+ ],
28240
+ [[i.bucket, i.name]]
28241
+ );
28242
+ }
27026
28243
  var SORT_COLUMNS = {
27027
28244
  name: "name",
27028
28245
  created_at: "created_at",
@@ -27114,7 +28331,7 @@ function storageUsing(input) {
27114
28331
  assertFragmentAlias(alias, "storageUsing");
27115
28332
  const on = target(snapshot);
27116
28333
  if ("ok" in on) return on;
27117
- const status = `(${quoteIdentifier(alias)}.${quoteIdentifier("status")} = 'ready')`;
28334
+ const status = storageReadySql(alias);
27118
28335
  if (context.role === "service")
27119
28336
  return { ok: true, fragment: { sql: status, params: [] } };
27120
28337
  try {
@@ -27204,12 +28421,12 @@ var CONTEXTS = [
27204
28421
  userId: "naro-policy-check",
27205
28422
  email: "check@naro.invalid",
27206
28423
  emailVerified: true
27207
- }
28424
+ },
28425
+ { role: "authenticated", userId: "naro-policy-check" }
27208
28426
  ];
27209
28427
  function probes(table, context, owners) {
27210
28428
  const writable = table.columns.filter((column2) => !column2.generated);
27211
- const [first] = table.columns;
27212
- const filters = first ? [{ operator: "is", column: first.name, value: null }] : [];
28429
+ const filters = leastFilters(table.columns[0]?.name);
27213
28430
  const row = Object.fromEntries(
27214
28431
  writable.map((column2) => [
27215
28432
  column2.name,
@@ -27217,7 +28434,7 @@ function probes(table, context, owners) {
27217
28434
  ])
27218
28435
  );
27219
28436
  const queries = [
27220
- { version: 1, action: "select", table: table.name, columns: "*" },
28437
+ { version: 1, action: "select", table: table.name, columns: "*", filters },
27221
28438
  { version: 1, action: "insert", table: table.name, rows: [row] },
27222
28439
  {
27223
28440
  version: 1,
@@ -27240,16 +28457,35 @@ function probes(table, context, owners) {
27240
28457
  }
27241
28458
  var STORAGE_PROBE_ALIAS = "o";
27242
28459
  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) {
28460
+ var UNVERIFIED = {
28461
+ role: "authenticated",
28462
+ userId: "naro-policy-check",
28463
+ emailVerified: false
28464
+ };
28465
+ function depthContexts(contexts, claims) {
28466
+ if (claims) return [...contexts, UNVERIFIED];
28467
+ return contexts.filter(
28468
+ (context) => context.role !== "authenticated" || context.emailVerified === true
28469
+ );
28470
+ }
28471
+ function readsClaims(policies) {
28472
+ return policies.some(
28473
+ (policy) => [policy.using, policy.check].some(
28474
+ (text2) => text2 !== null && /auth\s*\.\s*jwt\s*\(/i.test(text2)
28475
+ )
28476
+ );
28477
+ }
28478
+ function callerName(context) {
28479
+ if (context.role !== "authenticated") return context.role;
28480
+ if (context.emailVerified === true) return "authenticated";
28481
+ return context.emailVerified === false ? "authenticated with no verified email" : "authenticated with no email claims";
28482
+ }
28483
+ function storageRefusals(snapshot, table, policies, measuring, measured) {
27248
28484
  const refusals = [];
27249
28485
  const row = Object.fromEntries(
27250
28486
  table.columns.map((column2) => [column2.name, ""])
27251
28487
  );
27252
- for (const context of STORAGE_CONTEXTS) {
28488
+ for (const context of /* @__PURE__ */ new Set([...CONTEXTS, ...measuring])) {
27253
28489
  const using = (commands) => {
27254
28490
  const result = storageUsing({
27255
28491
  snapshot,
@@ -27285,25 +28521,33 @@ function storageRefusals(snapshot, table, policies) {
27285
28521
  overwriteCheckRow({ metadata: "", now: "", version: "" }),
27286
28522
  true
27287
28523
  );
27288
- for (const statement2 of storageStatements(
28524
+ const moveCheck = check2("update", moveCheckRow({ to: "" }), true);
28525
+ for (const { label: label2, statement: statement2 } of storageStatements(
27289
28526
  select,
27290
28527
  update,
27291
28528
  remove,
27292
28529
  [insertCheck, upsertCheck],
27293
- updateCheck
28530
+ updateCheck,
28531
+ moveCheck
27294
28532
  )) {
27295
28533
  const problem = storageStatementProblem(statement2, "select");
27296
28534
  if (problem) refusals.push(problem);
28535
+ else if (measuring.includes(context))
28536
+ measured(label2, statement2.sql, callerName(context));
27297
28537
  }
27298
28538
  }
27299
28539
  return refusals;
27300
28540
  }
27301
- function storageStatements(select, update, remove, insertChecks, updateCheck) {
28541
+ function storageStatements(select, update, remove, insertChecks, updateCheck, moveCheck) {
27302
28542
  const statements = [];
28543
+ const add = (label2, ...made) => {
28544
+ for (const statement2 of made) statements.push({ label: label2, statement: statement2 });
28545
+ };
27303
28546
  const value = "";
27304
- for (const insertCheck of insertChecks)
28547
+ for (const [index, insertCheck] of insertChecks.entries())
27305
28548
  if (insertCheck)
27306
- statements.push(
28549
+ add(
28550
+ index === 0 ? "an upload's claim" : "an upsert's claim",
27307
28551
  claimUpload({
27308
28552
  id: value,
27309
28553
  bucket: value,
@@ -27319,7 +28563,8 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27319
28563
  })
27320
28564
  );
27321
28565
  if (select && update && updateCheck)
27322
- statements.push(
28566
+ add(
28567
+ "an overwrite's claim",
27323
28568
  claimOverwrite({
27324
28569
  bucket: value,
27325
28570
  name: value,
@@ -27334,8 +28579,28 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27334
28579
  nonce: STORAGE_PROBE_NONCE
27335
28580
  })
27336
28581
  );
28582
+ if (select && update && moveCheck)
28583
+ add(
28584
+ "a move's claim",
28585
+ ...claimMove({
28586
+ id: value,
28587
+ version: value,
28588
+ from: value,
28589
+ to: value,
28590
+ slotId: value,
28591
+ lockId: value,
28592
+ expiresAt: value,
28593
+ limitBytes: 0,
28594
+ selectUsing: select,
28595
+ updateUsing: update,
28596
+ check: moveCheck,
28597
+ nonce: STORAGE_PROBE_NONCE,
28598
+ slotNonce: STORAGE_PROBE_NONCE
28599
+ })
28600
+ );
27337
28601
  if (remove)
27338
- statements.push(
28602
+ add(
28603
+ "a delete's claim",
27339
28604
  claimDelete({
27340
28605
  bucket: value,
27341
28606
  names: [],
@@ -27344,13 +28609,27 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27344
28609
  expiresAt: value
27345
28610
  })
27346
28611
  );
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 }),
28612
+ if (select) {
28613
+ add("a read", readObject2({ bucket: value, name: value, using: select }));
28614
+ add(
28615
+ "a read of several objects",
28616
+ readObjects({ bucket: value, names: [], using: select })
28617
+ );
28618
+ add(
28619
+ "a move's or a copy's read",
28620
+ readTransferSource({ bucket: value, name: value, using: select })
28621
+ );
28622
+ add(
28623
+ "a copy's guard",
28624
+ copySourceGuard({
28625
+ id: value,
28626
+ name: value,
28627
+ using: select,
28628
+ nonce: STORAGE_PROBE_NONCE
28629
+ })
28630
+ );
28631
+ add(
28632
+ "a listing",
27354
28633
  listObjects({
27355
28634
  bucket: value,
27356
28635
  prefix: "p",
@@ -27361,6 +28640,7 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27361
28640
  using: select
27362
28641
  })
27363
28642
  );
28643
+ }
27364
28644
  return statements;
27365
28645
  }
27366
28646
  var REALTIME_PROBE_CHANNEL = "naro-policy-check";
@@ -27373,13 +28653,13 @@ function joinReason(reason) {
27373
28653
  return JOIN_STATEMENT;
27374
28654
  return reason;
27375
28655
  }
27376
- function realtimeRefusals(snapshot, policies) {
28656
+ function realtimeRefusals(snapshot, policies, measuring, measured) {
27377
28657
  const refusals = [];
27378
28658
  const applying = (command) => policies.filter(
27379
28659
  (policy) => asciiLower(policy.table) !== REALTIME_MESSAGES || policy.command === command || policy.command === "all"
27380
28660
  );
27381
28661
  for (const set2 of [policies, applying("select"), applying("insert")])
27382
- for (const context of STORAGE_CONTEXTS) {
28662
+ for (const context of /* @__PURE__ */ new Set([...CONTEXTS, ...measuring])) {
27383
28663
  const plan = privateChannelAccess({
27384
28664
  snapshot,
27385
28665
  context,
@@ -27387,7 +28667,11 @@ function realtimeRefusals(snapshot, policies) {
27387
28667
  channel: REALTIME_PROBE_CHANNEL,
27388
28668
  nonce: REALTIME_PROBE_NONCE
27389
28669
  });
27390
- if (plan.ok) continue;
28670
+ if (plan.ok) {
28671
+ if ("statement" in plan && measuring.includes(context))
28672
+ measured("a join", plan.statement.sql, callerName(context));
28673
+ continue;
28674
+ }
27391
28675
  const reason = joinReason(plan.reason);
27392
28676
  refusals.push(
27393
28677
  reason === void 0 ? { error: plan.error } : { error: plan.error, reason }
@@ -27429,9 +28713,48 @@ function namedInRefusal(message, policies) {
27429
28713
  }
27430
28714
  return found;
27431
28715
  }
27432
- function problems(snapshot, policies, options) {
28716
+ function probeLabel(query) {
28717
+ switch (query.action) {
28718
+ case "select":
28719
+ return "a select";
28720
+ case "insert":
28721
+ return query.returning === void 0 ? "an insert" : "an insert with returning";
28722
+ case "update":
28723
+ return "an update";
28724
+ case "delete":
28725
+ return "a delete";
28726
+ }
28727
+ }
28728
+ function inlinedTables(sql, probed) {
28729
+ const names = /* @__PURE__ */ new Set();
28730
+ for (const [, written] of sql.matchAll(
28731
+ /FROM ("(?:[^"]|"")*"|[A-Za-z_][A-Za-z0-9_$]*) AS "?_naro_s\d+"?/g
28732
+ )) {
28733
+ if (written === void 0) continue;
28734
+ const name = written.startsWith('"') ? written.slice(1, -1).replaceAll('""', '"') : written;
28735
+ const label2 = name === STORAGE_OBJECTS_TABLE ? STORAGE_OBJECTS : name;
28736
+ if (label2 !== probed) names.add(label2);
28737
+ }
28738
+ return [...names];
28739
+ }
28740
+ function listed(names) {
28741
+ if (names.length < 2) return names.join("");
28742
+ return `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
28743
+ }
28744
+ function tooDeepMessage({
28745
+ table,
28746
+ label: label2,
28747
+ caller,
28748
+ depth,
28749
+ inlined
28750
+ }) {
28751
+ const others = inlined.length === 0 ? "" : `, with those of ${listed(inlined)} that they read,`;
28752
+ 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.`;
28753
+ }
28754
+ function problems(snapshot, policies, options, claims) {
27433
28755
  const found = /* @__PURE__ */ new Map();
27434
28756
  const loaded = loadNamed(policies);
28757
+ const measuring = depthContexts(CONTEXTS, claims ?? readsClaims(loaded));
27435
28758
  for (const policy of loaded)
27436
28759
  if (policy.invalid !== null)
27437
28760
  found.set(`read:${policy.table}:${policy.name}`, {
@@ -27493,24 +28816,30 @@ function problems(snapshot, policies, options) {
27493
28816
  }
27494
28817
  }
27495
28818
  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
- }))
28819
+ [...tables.values()].flatMap((table) => {
28820
+ const roles = POLICY_ROLES.filter(
28821
+ (role) => applicablePolicies(loaded, table.name, "select", role).permissive.length === 0
28822
+ );
28823
+ return roles.length === 0 ? [] : [
28824
+ {
28825
+ table: table.name,
28826
+ name: "naro_policy_check",
28827
+ permissive: true,
28828
+ command: "select",
28829
+ roles,
28830
+ using: "true",
28831
+ check: null,
28832
+ owner: null
28833
+ }
28834
+ ];
28835
+ })
27506
28836
  );
27507
- const compiled = [...loaded, ...probe];
27508
28837
  const refused = (probed, error51, reason) => {
27509
28838
  if (error51.code !== "POLICY_INVALID") return;
27510
- const message = reason === void 0 ? error51.message : `${error51.message.replace(/\.$/, "")}: ${reason}.`;
28839
+ const message = reason === STORAGE_UNAVAILABLE_REASON ? STORAGE_UNAVAILABLE_MESSAGE : reason === void 0 ? error51.message : `${error51.message.replace(/\.$/, "")}: ${reason}.`;
27511
28840
  const named = namedInRefusal(error51.message, loaded);
27512
28841
  const blamed2 = named === void 0 ? probed : tableLabelOf(snapshot, named.table);
27513
- const key = `${blamed2}:${message}`;
28842
+ const key = reason === STORAGE_UNAVAILABLE_REASON ? `${blamed2}:${named?.name ?? ""}:${message}` : `${blamed2}:${message}`;
27514
28843
  if (!found.has(key))
27515
28844
  found.set(key, {
27516
28845
  problem: {
@@ -27522,12 +28851,29 @@ function problems(snapshot, policies, options) {
27522
28851
  policy: ""
27523
28852
  });
27524
28853
  };
28854
+ const deepest = /* @__PURE__ */ new Map();
28855
+ const measured = (table) => (label2, sql, caller) => {
28856
+ const depth = expressionDepth(sql);
28857
+ const key = `depth:${table}:${label2}:${caller}`;
28858
+ if (depth !== void 0 && depth > (deepest.get(key)?.depth ?? D1_EXPRESSION_DEPTH))
28859
+ deepest.set(key, {
28860
+ table,
28861
+ label: label2,
28862
+ caller,
28863
+ depth,
28864
+ inlined: inlinedTables(sql, table)
28865
+ });
28866
+ };
27525
28867
  for (const table of tables.values()) {
27526
28868
  const owners = new Set(
27527
28869
  policiesOn(loaded, table.name).flatMap(
27528
28870
  (policy) => policy.owner ? [asciiLower(policy.owner)] : []
27529
28871
  )
27530
28872
  );
28873
+ const compiled = [
28874
+ ...loaded,
28875
+ ...probe.filter((policy) => policy.table === table.name)
28876
+ ];
27531
28877
  for (const context of CONTEXTS)
27532
28878
  for (const query of probes(table, context, owners)) {
27533
28879
  const decision = decideQuery(context, query, compiled, table.name);
@@ -27538,13 +28884,27 @@ function problems(snapshot, policies, options) {
27538
28884
  };
27539
28885
  if (!result.ok) refused(table.name, result.error, result.reason);
27540
28886
  }
28887
+ for (const context of measuring)
28888
+ for (const query of probes(table, context, owners)) {
28889
+ const real = decideQuery(context, query, loaded, table.name);
28890
+ const made = real.allow ? compile(query, snapshot, real, context) : void 0;
28891
+ if (made?.ok)
28892
+ for (const statement2 of made.statements)
28893
+ measured(table.name)(
28894
+ probeLabel(query),
28895
+ statement2.sql,
28896
+ callerName(context)
28897
+ );
28898
+ }
27541
28899
  }
27542
28900
  if (policiesOn(loaded, STORAGE_OBJECTS).length > 0) {
27543
28901
  if (snapshot.storage)
27544
28902
  for (const { error: error51, reason } of storageRefusals(
27545
28903
  snapshot,
27546
28904
  snapshot.storage,
27547
- loaded
28905
+ loaded,
28906
+ measuring,
28907
+ measured(STORAGE_OBJECTS)
27548
28908
  ))
27549
28909
  refused(STORAGE_OBJECTS, error51, reason);
27550
28910
  else
@@ -27558,8 +28918,22 @@ function problems(snapshot, policies, options) {
27558
28918
  });
27559
28919
  }
27560
28920
  if (policiesOn(loaded, REALTIME_MESSAGES).length > 0)
27561
- for (const { error: error51, reason } of realtimeRefusals(snapshot, loaded))
28921
+ for (const { error: error51, reason } of realtimeRefusals(
28922
+ snapshot,
28923
+ loaded,
28924
+ measuring,
28925
+ measured(REALTIME_MESSAGES)
28926
+ ))
27562
28927
  refused(REALTIME_MESSAGES, error51, reason);
28928
+ for (const [key, tooDeep] of deepest)
28929
+ found.set(key, {
28930
+ problem: {
28931
+ table: tooDeep.table,
28932
+ message: tooDeepMessage(tooDeep),
28933
+ kind: "compile"
28934
+ },
28935
+ policy: ""
28936
+ });
27563
28937
  if (options.indexes && options.allowUnindexed !== true)
27564
28938
  for (const problem of unindexedSubqueries(
27565
28939
  snapshot,
@@ -27877,10 +29251,10 @@ function isBlankStart(char) {
27877
29251
  function isBlank(char) {
27878
29252
  return isBlankStart(char) || char === "\v";
27879
29253
  }
27880
- function isWordStart(char) {
29254
+ function isWordStart2(char) {
27881
29255
  return /[A-Za-z_]/.test(char) || char.charCodeAt(0) >= 128;
27882
29256
  }
27883
- function isWordPart(char) {
29257
+ function isWordPart2(char) {
27884
29258
  return /[A-Za-z0-9_$]/.test(char) || char.charCodeAt(0) >= 128;
27885
29259
  }
27886
29260
  function skipBlanks(text2, from) {
@@ -27934,9 +29308,9 @@ function headTokens(text2) {
27934
29308
  index = close + 1;
27935
29309
  } else if ((char === "x" || char === "X") && text2.charAt(index + 1) === "'") {
27936
29310
  break;
27937
- } else if (isWordStart(char)) {
29311
+ } else if (isWordStart2(char)) {
27938
29312
  let end = index + 1;
27939
- while (end < text2.length && isWordPart(text2.charAt(end))) end++;
29313
+ while (end < text2.length && isWordPart2(text2.charAt(end))) end++;
27940
29314
  tokens.push({ kind: "word", text: text2.slice(index, end) });
27941
29315
  index = end;
27942
29316
  } else if (char === "." && !/[0-9]/.test(text2.charAt(index + 1))) {
@@ -27952,6 +29326,10 @@ var NO_NAME = {
27952
29326
  kind: "unreadable",
27953
29327
  reason: "the table name could not be read"
27954
29328
  };
29329
+ var NO_COLUMN = {
29330
+ kind: "unreadable",
29331
+ reason: "the column name could not be read"
29332
+ };
27955
29333
  var OTHER_SCHEMA = {
27956
29334
  kind: "unreadable",
27957
29335
  reason: "a table in another schema"
@@ -27977,8 +29355,9 @@ var Head = class {
27977
29355
  return token.kind === "word" || token.kind === "name" ? token.text : void 0;
27978
29356
  }
27979
29357
  /**
27980
- * SQLite's `fullname`. `main.` is the database a migration writes and is
27981
- * dropped; `temp.` (in any case, quoted or not, as sqlite3FindDbName
29358
+ * SQLite's `fullname`. `main.` is the database a migration writes: the
29359
+ * name goes without it, and `main` says it was written (known-issues
29360
+ * #226); `temp.` (in any case, quoted or not, as sqlite3FindDbName
27982
29361
  * matches it) is a table that goes with the connection; `public.` is
27983
29362
  * what a Postgres migration writes, said in its own words; any other
27984
29363
  * schema is one D1 does not have.
@@ -27987,17 +29366,21 @@ var Head = class {
27987
29366
  const first = this.nameAt(at);
27988
29367
  if (first === void 0) return { ok: false, head: NO_NAME };
27989
29368
  if (this.token(at + 1).kind !== "dot")
27990
- return { ok: true, name: first, next: at + 1 };
29369
+ return { ok: true, name: first, next: at + 1, main: false };
27991
29370
  const second = this.nameAt(at + 2);
27992
29371
  if (second === void 0) return { ok: false, head: NO_NAME };
27993
29372
  const schema = asciiLower(first);
27994
- if (schema === "main") return { ok: true, name: second, next: at + 3 };
29373
+ if (schema === "main")
29374
+ return { ok: true, name: second, next: at + 3, main: true };
27995
29375
  let head = OTHER_SCHEMA;
27996
29376
  if (schema === "temp") head = OTHER;
27997
29377
  else if (schema === "public") head = PUBLIC_SCHEMA;
27998
29378
  return { ok: false, head, next: at + 3 };
27999
29379
  }
28000
29380
  };
29381
+ function inMain(read) {
29382
+ return read.main ? { main: true } : {};
29383
+ }
28001
29384
  function readCreate2(head) {
28002
29385
  if (head.is(1, "policy")) return { kind: "create-policy" };
28003
29386
  if (head.is(1, "temp") || head.is(1, "temporary")) return OTHER;
@@ -28021,9 +29404,24 @@ function readDrop2(head) {
28021
29404
  at += 2;
28022
29405
  }
28023
29406
  const table = head.table(at);
28024
- return table.ok ? { kind: "drop-table", table: table.name } : table.head;
29407
+ return table.ok ? { kind: "drop-table", table: table.name, ...inMain(table) } : table.head;
28025
29408
  }
28026
29409
  var RLS_ACTIONS = ["enable", "disable", "force", "no"];
29410
+ function readRenameColumn(head, table, at) {
29411
+ let from = at;
29412
+ if (head.is(from, "column") && head.nameAt(from + 1) !== void 0 && head.is(from + 2, "to"))
29413
+ from += 1;
29414
+ const column2 = head.nameAt(from);
29415
+ if (column2 === void 0 || !head.is(from + 1, "to")) return NO_COLUMN;
29416
+ const to = head.nameAt(from + 2);
29417
+ return to === void 0 ? NO_COLUMN : {
29418
+ kind: "rename-column",
29419
+ table: table.name,
29420
+ column: column2,
29421
+ to,
29422
+ ...inMain(table)
29423
+ };
29424
+ }
28027
29425
  function readAlter(head) {
28028
29426
  if (head.is(1, "policy")) return { kind: "alter-policy" };
28029
29427
  if (!head.is(1, "table")) return OTHER;
@@ -28033,9 +29431,18 @@ function readAlter(head) {
28033
29431
  if (head.is(action, "rename") && head.is(action + 1, "to")) {
28034
29432
  if (!sqlite.ok) return sqlite.head;
28035
29433
  const to = head.table(action + 2);
28036
- return to.ok ? { kind: "rename-table", table: sqlite.name, to: to.name } : to.head;
29434
+ return to.ok ? {
29435
+ kind: "rename-table",
29436
+ table: sqlite.name,
29437
+ to: to.name,
29438
+ ...inMain(sqlite)
29439
+ } : to.head;
29440
+ }
29441
+ if (head.is(action, "rename")) {
29442
+ if (!sqlite.ok) return sqlite.head;
29443
+ return readRenameColumn(head, sqlite, action + 1);
28037
29444
  }
28038
- if (head.is(action, "rename") || head.is(action, "add") || head.is(action, "drop"))
29445
+ if (head.is(action, "add") || head.is(action, "drop"))
28039
29446
  return sqlite.ok ? { kind: "alter-table", table: sqlite.name } : sqlite.head;
28040
29447
  }
28041
29448
  let at = 2;
@@ -28060,6 +29467,130 @@ function readSqlHead(text2) {
28060
29467
  return OTHER;
28061
29468
  }
28062
29469
 
29470
+ // ../../packages/data-core/src/system-sql.ts
29471
+ async function convertLegacyPolicies(db, found) {
29472
+ const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
29473
+ if (rows.length === 0) return [];
29474
+ const read = rows.map((row) => ({
29475
+ policy: policyFromRow(row),
29476
+ asRead: legacyRowAsRead(row)
29477
+ }));
29478
+ const inForce = new Set(
29479
+ legacyInForce(
29480
+ read.flatMap(({ policy }) => policy ? [policy] : []),
29481
+ await readTableNames(db)
29482
+ )
29483
+ );
29484
+ const statements = [];
29485
+ for (const { policy, asRead } of read) {
29486
+ if (!policy || !asRead) continue;
29487
+ if (inForce.has(policy)) {
29488
+ for (const named of namedFromLegacy(policy))
29489
+ statements.push({
29490
+ sql: POLICY_SQL.insertConverted,
29491
+ params: [...policyValues(named), ...asRead]
29492
+ });
29493
+ found.unreadable.push(...unreadableConversions(policy));
29494
+ }
29495
+ statements.push({ sql: POLICY_SQL.markConverted, params: asRead });
29496
+ }
29497
+ return statements;
29498
+ }
29499
+ 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')))";
29500
+ var STORAGE_SUBQUERIES_VERSION = 7;
29501
+ var STORAGE_SUBQUERIES_NAME = "storage_subquery_policies";
29502
+ var STORAGE_SUBQUERIES_SQL = `SELECT version FROM _naro_migrations WHERE version = ${String(STORAGE_SUBQUERIES_VERSION)} AND name = '${STORAGE_SUBQUERIES_NAME}'`;
29503
+ var SYSTEM_MIGRATIONS = [
29504
+ {
29505
+ version: 1,
29506
+ name: "system_tables",
29507
+ statements: [
29508
+ MIGRATIONS_TABLE,
29509
+ "CREATE TABLE IF NOT EXISTS _naro_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')))",
29510
+ // Spec §11.2, as written there.
29511
+ "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')))"
29512
+ ]
29513
+ },
29514
+ {
29515
+ // Spec P2-2 §3.1: an action whose rule column says 'condition' reads its
29516
+ // condition from these. A worker from before them reads 'condition' as
29517
+ // an unknown rule — none.
29518
+ version: 2,
29519
+ name: "policy_conditions",
29520
+ statements: [],
29521
+ addColumns: ["select", "insert", "update", "delete"].map((action) => ({
29522
+ table: "_naro_policies",
29523
+ column: `${action}_condition`,
29524
+ definition: "TEXT"
29525
+ }))
29526
+ },
29527
+ {
29528
+ // Spec P2-6 §11: the Naro worker's sign-in handoff. A provider sign-in's
29529
+ // flow (state → the app's redirect and the browser's PKCE challenge), the
29530
+ // one-time codes a redirect hands the app, the sends the per-address
29531
+ // email limit counts, and (adversarial review I3) the unconfirmed
29532
+ // accounts someone else also tried to sign up as. Times are epoch
29533
+ // milliseconds.
29534
+ version: 3,
29535
+ name: "auth_handoff",
29536
+ statements: [
29537
+ "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)",
29538
+ "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)",
29539
+ "CREATE TABLE IF NOT EXISTS _naro_auth_email_log (address_hash TEXT NOT NULL, sent_at INTEGER NOT NULL)",
29540
+ "CREATE INDEX IF NOT EXISTS _naro_auth_email_log_address ON _naro_auth_email_log (address_hash, sent_at)",
29541
+ "CREATE TABLE IF NOT EXISTS _naro_auth_contested (user_id TEXT PRIMARY KEY, contested_at INTEGER NOT NULL)"
29542
+ ]
29543
+ },
29544
+ {
29545
+ // P2-7 §9: named policies, many per table. The old rows are converted
29546
+ // in the same batch and their rules set to a word old workers read as
29547
+ // `none`.
29548
+ version: 4,
29549
+ name: "named_policies",
29550
+ statements: [NAMED_POLICY_TABLE_SQL],
29551
+ compute: convertLegacyPolicies
29552
+ },
29553
+ {
29554
+ // Storage (first-release design §6): the buckets and the objects' index
29555
+ // with their counter triggers. Every backend gets them, Storage on or
29556
+ // not — what turns it on is the worker's R2 binding. A worker from
29557
+ // before them never reads them.
29558
+ version: 5,
29559
+ name: "storage",
29560
+ statements: [...STORAGE_TABLES_SQL]
29561
+ },
29562
+ {
29563
+ // Storage 3-1b (known-issues #107, first-release design §14): the mark
29564
+ // a bucket's removal sets before it empties the bucket. The claims and
29565
+ // the worker's bucket reads take a marked bucket as not there. A worker
29566
+ // from before it never reads the column.
29567
+ version: 6,
29568
+ name: "storage_bucket_deleting",
29569
+ statements: [],
29570
+ addColumns: [STORAGE_BUCKET_DELETING_COLUMN]
29571
+ },
29572
+ {
29573
+ // Storage 3-1j (plan correction (p)): no schema — the mark that a worker
29574
+ // which reads storage.objects in a policy's subquery has migrated this
29575
+ // database. Only the Naro worker's own `internal/migrate` applies system
29576
+ // migrations, so the row is there once such a worker has run, and stays:
29577
+ // every later worker reads them too — as long as the worker bundle does
29578
+ // not go back. Deploying web from before 3-1j makes the next upgrade
29579
+ // put an older worker back, and a run that lost its lease may upload
29580
+ // one late; such a policy then answers 500 POLICY_INVALID (closed, no
29581
+ // rows) until it is dropped or the newer worker is uploaded again
29582
+ // (3-1j 최종 리뷰). Migration 5 alone cannot tell —
29583
+ // backends migrated by 3-1a to 3-1i have it, and their worker refuses
29584
+ // such a policy ("A policy cannot read storage.objects in a subquery
29585
+ // yet") and answers every request of its table 500 POLICY_INVALID until
29586
+ // the next upgrade. The control plane takes one only where this row is
29587
+ // (`STORAGE_SUBQUERIES_SQL`, `buildPolicyBatch`).
29588
+ version: STORAGE_SUBQUERIES_VERSION,
29589
+ name: STORAGE_SUBQUERIES_NAME,
29590
+ statements: []
29591
+ }
29592
+ ];
29593
+
28063
29594
  // ../../packages/data-core/src/policy-migration.ts
28064
29595
  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
29596
  function disableRefusal(table) {
@@ -28077,6 +29608,7 @@ function tablesChanged(head) {
28077
29608
  case "create-table":
28078
29609
  case "drop-table":
28079
29610
  case "alter-table":
29611
+ case "rename-column":
28080
29612
  return [asciiLower(head.table)];
28081
29613
  case "rename-table":
28082
29614
  return [asciiLower(head.table), asciiLower(head.to)];
@@ -28104,18 +29636,28 @@ function tablesInScope(policy) {
28104
29636
  }
28105
29637
  return tables;
28106
29638
  }
29639
+ function readsStorageObjects(policy) {
29640
+ return [policy.using, policy.check].some((text2) => {
29641
+ if (text2 === null) return false;
29642
+ const parsed = parseExpression(text2);
29643
+ return parsed.ok && subqueriesOf(parsed.expr).some(
29644
+ (subquery) => isStorageObjects(subquery.table)
29645
+ );
29646
+ });
29647
+ }
28107
29648
  function planPolicyMigration(statements) {
28108
29649
  const count2 = statements.length;
28109
29650
  const planned = [];
28110
29651
  let hasPolicyStatements = false;
28111
29652
  let touchesTables = false;
29653
+ let renamesColumns = false;
28112
29654
  const heads = statements.map((text2) => readSqlHead(text2));
28113
29655
  const changes = heads.map(tablesChanged);
28114
29656
  const changedAfter = (index, tables) => changes.slice(index + 1).some(
28115
29657
  (changed) => changed === "any" || changed.some((table) => tables.has(table))
28116
29658
  );
28117
29659
  for (const [index, text2] of statements.entries()) {
28118
- const refuse = (message) => ({
29660
+ const refuse2 = (message) => ({
28119
29661
  ok: false,
28120
29662
  index,
28121
29663
  message: `Statement ${String(index + 1)} of ${String(count2)}: ${message}`
@@ -28124,15 +29666,19 @@ function planPolicyMigration(statements) {
28124
29666
  switch (head.kind) {
28125
29667
  case "grant":
28126
29668
  case "revoke":
28127
- return refuse(GRANT_REFUSAL);
29669
+ return refuse2(GRANT_REFUSAL);
28128
29670
  case "unreadable":
28129
- return refuse(`${head.reason}.`);
29671
+ return refuse2(`${head.reason}.`);
28130
29672
  case "create-table":
28131
29673
  case "drop-table":
28132
29674
  case "rename-table":
28133
29675
  touchesTables = true;
28134
29676
  planned.push({ kind: "verbatim", text: text2, head });
28135
29677
  continue;
29678
+ case "rename-column":
29679
+ renamesColumns = true;
29680
+ planned.push({ kind: "verbatim", text: text2, head });
29681
+ continue;
28136
29682
  case "alter-table":
28137
29683
  case "other":
28138
29684
  planned.push({ kind: "verbatim", text: text2, head });
@@ -28142,17 +29688,17 @@ function planPolicyMigration(statements) {
28142
29688
  }
28143
29689
  const parsed = parseMigrationPolicyStatement(text2);
28144
29690
  if (!parsed.ok)
28145
- return refuse(
29691
+ return refuse2(
28146
29692
  `${parsed.reason} at character ${String(parsed.position)}.`
28147
29693
  );
28148
29694
  const statement2 = parsed.statement;
28149
29695
  const table = statement2.kind === "create" ? statement2.policy.table : statement2.table;
28150
29696
  if (!isStorageObjects(table) && isSystemTable(table))
28151
- return refuse(`${relationMissing(table)}.`);
29697
+ return refuse2(`${relationMissing(table)}.`);
28152
29698
  switch (statement2.kind) {
28153
29699
  case "create":
28154
29700
  if (readsBareSubqueryColumn(statement2.policy) && changedAfter(index, tablesInScope(statement2.policy)))
28155
- return refuse(LATER_TABLE_CHANGE);
29701
+ return refuse2(LATER_TABLE_CHANGE);
28156
29702
  hasPolicyStatements = true;
28157
29703
  planned.push({ kind: "create-policy", policy: statement2.policy });
28158
29704
  break;
@@ -28180,14 +29726,19 @@ function planPolicyMigration(statements) {
28180
29726
  break;
28181
29727
  case "rls":
28182
29728
  if (statement2.action === "disable")
28183
- return refuse(disableRefusal(table));
29729
+ return refuse2(disableRefusal(table));
28184
29730
  planned.push({ kind: "noop" });
28185
29731
  break;
28186
29732
  }
28187
29733
  }
28188
29734
  return {
28189
29735
  ok: true,
28190
- plan: { statements: planned, hasPolicyStatements, touchesTables }
29736
+ plan: {
29737
+ statements: planned,
29738
+ hasPolicyStatements,
29739
+ touchesTables,
29740
+ renamesColumns
29741
+ }
28191
29742
  };
28192
29743
  }
28193
29744
  function policyMigrationSql(policy) {
@@ -28202,6 +29753,7 @@ function unreadablePolicyComment(policy) {
28202
29753
  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
29754
  var SYSTEM_PREDICATE = systemTablePredicate("name");
28204
29755
  var STORAGE_EXISTS = `EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${sqlStringLiteral(STORAGE_OBJECTS_TABLE)})`;
29756
+ var STORAGE_SUBQUERIES_READY = `EXISTS (${STORAGE_SUBQUERIES_SQL})`;
28205
29757
 
28206
29758
  // ../../packages/data-core/src/realtime-limits.ts
28207
29759
  var HOBBY_REALTIME_LIMITS = Object.freeze({
@@ -28290,107 +29842,6 @@ var MULTIPART_COMPLETE_LOCK_MS = 15 * 60 * 1e3;
28290
29842
  var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
28291
29843
  var UPLOAD_ID = new RegExp(`^(${UUID})\\.(${UUID})$`);
28292
29844
 
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
29845
  // ../../packages/data-core/src/write-quota.ts
28395
29846
  var WRITE_QUOTA_KEY = "write_quota";
28396
29847
  var WRITE_QUOTA_SQL = {
@@ -28636,12 +30087,12 @@ import {
28636
30087
  rmSync as rmSync2,
28637
30088
  writeFileSync as writeFileSync2
28638
30089
  } from "fs";
28639
- import { join as join22 } from "path";
30090
+ import { join as join23 } from "path";
28640
30091
  function linkDir(dir) {
28641
- return join22(dir, ".naro");
30092
+ return join23(dir, ".naro");
28642
30093
  }
28643
30094
  function linkFile(dir) {
28644
- return join22(linkDir(dir), "project.json");
30095
+ return join23(linkDir(dir), "project.json");
28645
30096
  }
28646
30097
  function readProjectLink(dir) {
28647
30098
  try {
@@ -28663,7 +30114,7 @@ function writeProjectLink(dir, link) {
28663
30114
  writeFileSync2(linkFile(dir), `${JSON.stringify(link, null, 2)}
28664
30115
  `);
28665
30116
  writeFileSync2(
28666
- join22(linkDir(dir), "README.txt"),
30117
+ join23(linkDir(dir), "README.txt"),
28667
30118
  "This directory links the project to Naro and should not be committed.\n"
28668
30119
  );
28669
30120
  }
@@ -28673,9 +30124,9 @@ function removeProjectLink(dir) {
28673
30124
  return true;
28674
30125
  }
28675
30126
  function ensureGitignoreHasNaro(dir) {
28676
- const gitignorePath = join22(dir, ".gitignore");
30127
+ const gitignorePath = join23(dir, ".gitignore");
28677
30128
  const hasGitignore = existsSync4(gitignorePath);
28678
- if (!hasGitignore && !existsSync4(join22(dir, ".git"))) return false;
30129
+ if (!hasGitignore && !existsSync4(join23(dir, ".git"))) return false;
28679
30130
  const current = hasGitignore ? readFileSync5(gitignorePath, "utf8") : "";
28680
30131
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".naro" || line === ".naro/" || line === "/.naro");
28681
30132
  if (covered) return false;
@@ -29128,15 +30579,15 @@ function policiesFromFiles(files) {
29128
30579
  policies = policies.filter((policy) => !sameTable(policy.table, table));
29129
30580
  };
29130
30581
  for (const [file2, sql] of files.entries()) {
29131
- const split = splitStatements(sql);
29132
- if (!split.ok)
30582
+ const split2 = splitStatements(sql);
30583
+ if (!split2.ok)
29133
30584
  return {
29134
30585
  policies,
29135
30586
  tables,
29136
- refused: { file: file2, reason: `sql could not be parsed: ${split.reason}.` }
30587
+ refused: { file: file2, reason: `sql could not be parsed: ${split2.reason}.` }
29137
30588
  };
29138
30589
  const planned = planPolicyMigration(
29139
- split.statements.map((statement2) => statement2.text)
30590
+ split2.statements.map((statement2) => statement2.text)
29140
30591
  );
29141
30592
  if (!planned.ok)
29142
30593
  return {
@@ -29204,6 +30655,31 @@ function policiesLostByReset(input) {
29204
30655
  withTables: gone.filter((policy) => !remade.has(asciiLower(policy.table)))
29205
30656
  };
29206
30657
  }
30658
+ function storageRefusalOfFiles(files, backend) {
30659
+ for (const [file2, sql] of files.entries()) {
30660
+ const split2 = splitStatements(sql);
30661
+ if (!split2.ok) return void 0;
30662
+ const planned = planPolicyMigration(
30663
+ split2.statements.map((statement2) => statement2.text)
30664
+ );
30665
+ if (!planned.ok) return void 0;
30666
+ const count2 = planned.plan.statements.length;
30667
+ for (const [index, statement2] of planned.plan.statements.entries()) {
30668
+ let needs;
30669
+ if (statement2.kind === "create-policy")
30670
+ needs = readsStorageObjects(statement2.policy) ? "subqueries" : sameTable(statement2.policy.table, STORAGE_OBJECTS) ? "storage" : void 0;
30671
+ else if (statement2.kind === "drop-policy" && !statement2.ifExists || statement2.kind === "owner")
30672
+ needs = sameTable(statement2.table, STORAGE_OBJECTS) ? "storage" : void 0;
30673
+ const missing = needs === "subqueries" ? !backend.storage || !backend.storageSubqueries : needs === "storage" && !backend.storage;
30674
+ if (needs !== void 0 && missing)
30675
+ return { file: file2, statement: index + 1, count: count2, needs };
30676
+ }
30677
+ }
30678
+ return void 0;
30679
+ }
30680
+ function resetStorageOutdatedMessage(file2, at) {
30681
+ 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.`;
30682
+ }
29207
30683
  function resetPoliciesLostMessage(lost) {
29208
30684
  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
30685
  }
@@ -29282,7 +30758,7 @@ function usageStatus(report, currentMonth, requests) {
29282
30758
 
29283
30759
  // src/env-consent.ts
29284
30760
  import { existsSync as existsSync6 } from "fs";
29285
- import { join as join23 } from "path";
30761
+ import { join as join24 } from "path";
29286
30762
  var DOTENV_FILES = [".env.local", ".env.production", ".env"];
29287
30763
  var PUBLIC_PREFIX = "NEXT_PUBLIC_";
29288
30764
  function maskSecretValue(value) {
@@ -29294,7 +30770,7 @@ function maskEnvValue(key, value) {
29294
30770
  return maskSecretValue(value);
29295
30771
  }
29296
30772
  function findDotenvFile(dir) {
29297
- for (const name of DOTENV_FILES) if (existsSync6(join23(dir, name))) return name;
30773
+ for (const name of DOTENV_FILES) if (existsSync6(join24(dir, name))) return name;
29298
30774
  return null;
29299
30775
  }
29300
30776
  function envAddGuidance(keys, environment) {
@@ -29525,9 +31001,9 @@ import {
29525
31001
  cp as cp2,
29526
31002
  lstat,
29527
31003
  mkdir as mkdir3,
29528
- readFile as readFile13,
31004
+ readFile as readFile14,
29529
31005
  readlink,
29530
- realpath,
31006
+ realpath as realpath3,
29531
31007
  rm as rm4,
29532
31008
  stat as stat3,
29533
31009
  symlink,
@@ -29538,10 +31014,10 @@ import {
29538
31014
  basename as basename3,
29539
31015
  dirname as dirname5,
29540
31016
  isAbsolute as isAbsolute4,
29541
- join as join24,
31017
+ join as join25,
29542
31018
  relative as relative6,
29543
31019
  resolve as resolve10,
29544
- sep as sep4
31020
+ sep as sep5
29545
31021
  } from "path";
29546
31022
  var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo|\.naro)([\\/]|$)/;
29547
31023
  var ENV_FILE = /^\.env(\.(?!(?:example|sample|template)$)[^\\/]+)?$/i;
@@ -29579,7 +31055,7 @@ async function committedEnvFiles(sourceDir) {
29579
31055
  }
29580
31056
  async function staysInside(link, text2, root) {
29581
31057
  try {
29582
- return inside(await realpath(link), root);
31058
+ return inside(await realpath3(link), root);
29583
31059
  } catch (error51) {
29584
31060
  return error51.code === "ENOENT" && !text2.split(/[\\/]/).includes("..");
29585
31061
  }
@@ -29602,12 +31078,12 @@ async function keptOnly(items, keep, remove) {
29602
31078
  async function readLocal(path) {
29603
31079
  const found = await stat3(path).catch(() => null);
29604
31080
  if (!found?.isFile()) return null;
29605
- return readFile13(path).catch(() => null);
31081
+ return readFile14(path).catch(() => null);
29606
31082
  }
29607
31083
  async function realAncestor(dir) {
29608
31084
  for (let current = dir; ; current = dirname5(current)) {
29609
31085
  try {
29610
- return await realpath(current);
31086
+ return await realpath3(current);
29611
31087
  } catch {
29612
31088
  if (dirname5(current) === current)
29613
31089
  throw new Error(`No ancestor of ${dir}`);
@@ -29618,7 +31094,7 @@ function sameText(a, b) {
29618
31094
  return a.equals(b) || a.toString("utf8").replace(/\r\n/g, "\n") === b.toString("utf8").replace(/\r\n/g, "\n");
29619
31095
  }
29620
31096
  async function copyWorkingTree(source, checkout) {
29621
- const sourceDir = await realpath(source);
31097
+ const sourceDir = await realpath3(source);
29622
31098
  const inWorkingTree = [];
29623
31099
  const absoluteLinks = [];
29624
31100
  const linksOutside = [];
@@ -29637,12 +31113,12 @@ async function copyWorkingTree(source, checkout) {
29637
31113
  if (entry.isFIFO() || entry.isSocket() || entry.isCharacterDevice() || entry.isBlockDevice())
29638
31114
  return false;
29639
31115
  if (ENV_FILE.test(basename3(src)) && !entry.isDirectory()) {
29640
- inWorkingTree.push(relative6(sourceDir, src).split(sep4).join("/"));
31116
+ inWorkingTree.push(relative6(sourceDir, src).split(sep5).join("/"));
29641
31117
  return false;
29642
31118
  }
29643
31119
  if (entry.isSymbolicLink()) {
29644
31120
  const target2 = await readlink(src);
29645
- const path = relative6(sourceDir, src).split(sep4).join("/");
31121
+ const path = relative6(sourceDir, src).split(sep5).join("/");
29646
31122
  if (isAbsolute4(target2)) {
29647
31123
  absoluteLinks.push(path);
29648
31124
  return false;
@@ -29660,17 +31136,17 @@ async function copyWorkingTree(source, checkout) {
29660
31136
  return true;
29661
31137
  }
29662
31138
  });
29663
- const root = await realpath(checkout);
31139
+ const root = await realpath3(checkout);
29664
31140
  const strays = await keptOnly(
29665
31141
  keptLinks,
29666
31142
  async ({ path, text: text2 }) => {
29667
31143
  try {
29668
- return inside(await realpath(join24(checkout, path)), root);
31144
+ return inside(await realpath3(join25(checkout, path)), root);
29669
31145
  } catch (error51) {
29670
31146
  return error51.code === "ENOENT" && climbsOnlyFirst(text2);
29671
31147
  }
29672
31148
  },
29673
- ({ path }) => unlink(join24(checkout, path))
31149
+ ({ path }) => unlink(join25(checkout, path))
29674
31150
  );
29675
31151
  linksElsewhere.push(...strays.map(({ path }) => path));
29676
31152
  const committed = await committedEnvFiles(sourceDir);
@@ -29684,7 +31160,7 @@ async function copyWorkingTree(source, checkout) {
29684
31160
  outsideProject.push(path);
29685
31161
  continue;
29686
31162
  }
29687
- const target2 = join24(checkout, path);
31163
+ const target2 = join25(checkout, path);
29688
31164
  if (!inside(await realAncestor(dirname5(target2)), root)) {
29689
31165
  outsideProject.push(path);
29690
31166
  continue;
@@ -29709,16 +31185,16 @@ async function copyWorkingTree(source, checkout) {
29709
31185
  continue;
29710
31186
  }
29711
31187
  await writeFile7(target2, content, { flag: "wx" });
29712
- const local = await readLocal(join24(sourceDir, path));
31188
+ const local = await readLocal(join25(sourceDir, path));
29713
31189
  if (!local || !sameText(local, content)) committedVersion.push(path);
29714
31190
  }
29715
31191
  const envStrays = await keptOnly(
29716
31192
  envLinks,
29717
31193
  async (path) => {
29718
- const real = await realpath(join24(checkout, path)).catch(() => null);
31194
+ const real = await realpath3(join25(checkout, path)).catch(() => null);
29719
31195
  return real !== null && inside(real, root);
29720
31196
  },
29721
- (path) => unlink(join24(checkout, path))
31197
+ (path) => unlink(join25(checkout, path))
29722
31198
  );
29723
31199
  outsideProject.push(...envStrays);
29724
31200
  const inCommit = new Set(
@@ -29835,7 +31311,7 @@ function slugify2(value) {
29835
31311
  async function projectName(sourceDir) {
29836
31312
  try {
29837
31313
  const pkg = JSON.parse(
29838
- await readFile14(join25(sourceDir, "package.json"), "utf8")
31314
+ await readFile15(join26(sourceDir, "package.json"), "utf8")
29839
31315
  );
29840
31316
  if (pkg.name) return pkg.name;
29841
31317
  } catch {
@@ -29845,7 +31321,7 @@ async function projectName(sourceDir) {
29845
31321
  function resolveBin(name) {
29846
31322
  let dir = dirname6(fileURLToPath(import.meta.url));
29847
31323
  for (; ; ) {
29848
- const candidate = join25(dir, "node_modules", ".bin", name);
31324
+ const candidate = join26(dir, "node_modules", ".bin", name);
29849
31325
  if (existsSync7(candidate)) return candidate;
29850
31326
  const parent = dirname6(dir);
29851
31327
  if (parent === dir) return name;
@@ -29912,7 +31388,7 @@ function workingTreeNotices(copy) {
29912
31388
  }
29913
31389
  function localDependencies(sourceDir, say = () => void 0) {
29914
31390
  return {
29915
- createWorkspace: () => mkdtemp2(join25(tmpdir2(), "naro-build-")),
31391
+ createWorkspace: () => mkdtemp2(join26(tmpdir2(), "naro-build-")),
29916
31392
  removeWorkspace: (path) => rm5(path, { recursive: true, force: true }),
29917
31393
  async runCommand(spec, observer) {
29918
31394
  if (spec.command === "git" && spec.args[0] === "clone") {
@@ -29950,7 +31426,12 @@ function localDependencies(sourceDir, say = () => void 0) {
29950
31426
  async function deployDirectory(sourceDir, options, log2) {
29951
31427
  const cpUrl = controlPlaneUrl();
29952
31428
  const production = options.production ?? false;
29953
- const environment = production ? void 0 : options.environment;
31429
+ if (production && options.environment !== void 0)
31430
+ throw new NaroError(
31431
+ "VALIDATION_FAILED",
31432
+ "environment and production cannot be used together."
31433
+ );
31434
+ const environment = options.environment;
29954
31435
  if (environment === "production" || environment === "preview")
29955
31436
  throw new NaroError(
29956
31437
  "VALIDATION_FAILED",
@@ -30178,12 +31659,14 @@ export {
30178
31659
  cellText,
30179
31660
  renderRows,
30180
31661
  summarizeStatement,
30181
- parseDotenv,
31662
+ parseDotenvDetailed,
30182
31663
  serializeDotenv,
30183
31664
  MAX_SINGLE_UPLOAD_BYTES,
31665
+ checkObjectName,
30184
31666
  parseCacheControl,
30185
31667
  STORAGE_OBJECTS_TABLE,
30186
31668
  systemTablePredicate,
31669
+ dataError,
30187
31670
  SCHEMA_SQL,
30188
31671
  tableNames,
30189
31672
  snapshotFromRows,
@@ -30198,6 +31681,7 @@ export {
30198
31681
  indexesFromRows,
30199
31682
  parsePolicyStatement,
30200
31683
  splitPolicyStatements,
31684
+ STORAGE_SUBQUERIES_SQL,
30201
31685
  planPolicyMigration,
30202
31686
  unreadablePolicyComment,
30203
31687
  parseRedirectEntry,
@@ -30220,6 +31704,8 @@ export {
30220
31704
  RESET_BACKEND_OUTDATED,
30221
31705
  buildResetPlan,
30222
31706
  policiesLostByReset,
31707
+ storageRefusalOfFiles,
31708
+ resetStorageOutdatedMessage,
30223
31709
  resetPoliciesLostMessage,
30224
31710
  resetPoliciesWithTablesMessage,
30225
31711
  resetFileRefusedMessage,
@@ -30230,7 +31716,7 @@ export {
30230
31716
  promptForMissingEnv,
30231
31717
  pullEnv,
30232
31718
  detectGitInfo,
30233
- parseDotenv2,
31719
+ parseDotenv2 as parseDotenv,
30234
31720
  readProjectEnv2 as readProjectEnv,
30235
31721
  eventsObserver,
30236
31722
  slugify2 as slugify,