naro-cli 0.18.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\-._~%!$&()*+,;=:@/[\]]*$/;
@@ -15091,7 +15091,13 @@ var createProjectSchema = external_exports.object({
15091
15091
  rootDirectory: optionalRootDirectory
15092
15092
  });
15093
15093
  var createDeploymentSchema = external_exports.object({
15094
- branch: external_exports.string().trim().max(240).optional().transform((value) => value || null)
15094
+ branch: external_exports.string().trim().max(240).optional().transform((value) => value || null),
15095
+ /**
15096
+ * The surface to deploy to: `production`, `preview`, or the slug of one of
15097
+ * the project's environments. Whether the name exists is the route's to say;
15098
+ * 63 is the DNS label a worker name has to fit in.
15099
+ */
15100
+ environment: external_exports.string().trim().min(1).max(63).optional()
15095
15101
  });
15096
15102
  var connectRepositorySchema = external_exports.object({
15097
15103
  // Web-only route context: the only consumer is apps/web's
@@ -15354,6 +15360,40 @@ function splitStatements(sql) {
15354
15360
  };
15355
15361
  }
15356
15362
 
15363
+ // ../../packages/core/src/surface.ts
15364
+ var PRODUCTION_SURFACE = { kind: "production" };
15365
+ var PREVIEW_SURFACE = { kind: "preview" };
15366
+ var ENVIRONMENT_SLUG_MAX_LENGTH = 20;
15367
+ var RESERVED_ENVIRONMENT_SLUGS = [
15368
+ "production",
15369
+ "preview",
15370
+ "all"
15371
+ ];
15372
+ var SLUG_PATTERN = /^[a-z0-9](-?[a-z0-9])*$/;
15373
+ var ENVIRONMENT_SEPARATOR = "--";
15374
+ var PREVIEW_SUFFIX = "-preview";
15375
+ function environmentSlugProblem(value) {
15376
+ if (value.length < 1 || value.length > ENVIRONMENT_SLUG_MAX_LENGTH)
15377
+ return "invalid";
15378
+ if (!SLUG_PATTERN.test(value)) return "invalid";
15379
+ if (RESERVED_ENVIRONMENT_SLUGS.includes(value))
15380
+ return "reserved";
15381
+ return null;
15382
+ }
15383
+ function environmentWorkerName(subdomain, slug) {
15384
+ return `${subdomain}${ENVIRONMENT_SEPARATOR}${slug}`;
15385
+ }
15386
+ function workerNameForSurface(subdomain, surface) {
15387
+ switch (surface.kind) {
15388
+ case "production":
15389
+ return subdomain;
15390
+ case "preview":
15391
+ return `${subdomain}${PREVIEW_SUFFIX}`;
15392
+ case "environment":
15393
+ return environmentWorkerName(subdomain, surface.slug);
15394
+ }
15395
+ }
15396
+
15357
15397
  // ../../packages/build/src/next-version.ts
15358
15398
  var NEXT_RELEASE_DATES = {
15359
15399
  16: "2025-10-21",
@@ -15573,7 +15613,7 @@ import { access as access5, readFile as readFile5, writeFile as writeFile4 } fro
15573
15613
  import { join as join6, resolve as resolve2 } from "path";
15574
15614
 
15575
15615
  // ../../packages/build/src/open-next.ts
15576
- import { access as access3, readdir, readFile as readFile4 } from "fs/promises";
15616
+ import { access as access3, readdir, readFile as readFile4, realpath } from "fs/promises";
15577
15617
  import {
15578
15618
  basename,
15579
15619
  dirname,
@@ -15689,7 +15729,33 @@ function contentType(path) {
15689
15729
  return "application/octet-stream";
15690
15730
  }
15691
15731
  }
15692
- 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);
15693
15759
  const files = [];
15694
15760
  async function visit(directory) {
15695
15761
  const entries = await readdir(directory, { withFileTypes: true });
@@ -15746,7 +15812,6 @@ var OPEN_NEXT_LAYOUT = {
15746
15812
  outputRoot: ".open-next",
15747
15813
  bundleDir: ".open-next/worker-bundle"
15748
15814
  };
15749
- var listFiles = (root) => listArtifactFiles(root);
15750
15815
  async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15751
15816
  const configPath = await findWranglerConfig(appRoot, wranglerConfigPath);
15752
15817
  return readWorkerArtifact(appRoot, identity, {
@@ -15755,6 +15820,7 @@ async function readOpenNextArtifact(appRoot, identity, wranglerConfigPath) {
15755
15820
  });
15756
15821
  }
15757
15822
  async function readWorkerArtifact(appRoot, identity, layout) {
15823
+ const listFiles = (root) => listArtifactFiles(root, "OpenNext", appRoot);
15758
15824
  try {
15759
15825
  const configPath = await findWranglerConfig(
15760
15826
  appRoot,
@@ -15799,12 +15865,13 @@ async function readWorkerArtifact(appRoot, identity, layout) {
15799
15865
  );
15800
15866
  }
15801
15867
  const bundleRoot = confinedPath(appRoot, layout.bundleDir, "Bundle dir");
15802
- let bundleFiles = null;
15868
+ let bundleExists = true;
15803
15869
  try {
15804
15870
  await access3(bundleRoot);
15805
- bundleFiles = await listFiles(bundleRoot);
15806
15871
  } catch {
15872
+ bundleExists = false;
15807
15873
  }
15874
+ const bundleFiles = bundleExists ? await listFiles(bundleRoot) : null;
15808
15875
  const outputFiles = bundleFiles ?? await listFiles(outputRoot);
15809
15876
  const entrypointName = bundleFiles ? basename(entrypointPath) : relative(outputRoot, entrypointPath).split(sep).join("/");
15810
15877
  const assetsPrefix = `${relative(outputRoot, configuredAssets).split(sep).join("/")}/`;
@@ -16152,7 +16219,7 @@ async function readVinextArtifact(appRoot, identity) {
16152
16219
  const entrypointName = metadata.main.split(/[\\/]/).join("/");
16153
16220
  const matchers = moduleMatchers(metadata.rules);
16154
16221
  const isModule = (name) => matchers.length === 0 || matchers.some((matcher) => matcher.test(name));
16155
- const serverFiles = await listArtifactFiles(serverRoot, "vinext");
16222
+ const serverFiles = await listArtifactFiles(serverRoot, "vinext", appRoot);
16156
16223
  const entrypoint = serverFiles.find((file2) => file2.name === entrypointName);
16157
16224
  if (!entrypoint) {
16158
16225
  throw new DeploymentError(
@@ -16166,7 +16233,7 @@ async function readVinextArtifact(appRoot, identity) {
16166
16233
  modules: serverFiles.filter(
16167
16234
  (file2) => file2.name !== entrypointName && file2.name !== WRANGLER_OUTPUT_NAME && !file2.name.endsWith(".map") && isModule(file2.name)
16168
16235
  ),
16169
- assets: await listArtifactFiles(clientRoot, "vinext"),
16236
+ assets: await listArtifactFiles(clientRoot, "vinext", appRoot),
16170
16237
  compatibilityDate: metadata.compatibility_date,
16171
16238
  compatibilityFlags: Array.isArray(metadata.compatibility_flags) ? metadata.compatibility_flags.filter(
16172
16239
  (flag) => typeof flag === "string"
@@ -16839,7 +16906,8 @@ async function runCommand(spec, observer) {
16839
16906
  }
16840
16907
 
16841
16908
  // ../../packages/build/src/repository.ts
16842
- 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";
16843
16911
  function resolveAppRoot(checkout, configuredRoot) {
16844
16912
  const checkoutRoot = resolve3(checkout);
16845
16913
  if (!configuredRoot) return checkoutRoot;
@@ -16851,12 +16919,34 @@ function resolveAppRoot(checkout, configuredRoot) {
16851
16919
  if (relativeRoot === ".." || relativeRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute2(relativeRoot)) {
16852
16920
  throw new Error("Configured root directory escapes repository");
16853
16921
  }
16922
+ if (relativeRoot.split(/[\\/]/).includes(".git")) {
16923
+ throw new Error("Configured root directory cannot be inside .git");
16924
+ }
16854
16925
  return appRoot;
16855
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
+ }
16856
16946
 
16857
16947
  // ../../packages/build/src/static-artifact.ts
16858
16948
  import { readdir as readdir3, readFile as readFile7 } from "fs/promises";
16859
- 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";
16860
16950
  var SHIM_WORKER_SOURCE = "export default { fetch: (request, env) => env.ASSETS.fetch(request) };\n";
16861
16951
  var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules"]);
16862
16952
  var ALLOWED_DOTFILE = ".well-known";
@@ -16872,11 +16962,16 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16872
16962
  );
16873
16963
  const outputRoot = resolve4(appRoot, outputDirectory);
16874
16964
  const relation = relative3(appRoot, outputRoot);
16875
- if (relation === ".." || relation.startsWith(`..${sep2}`) || isAbsolute3(relation))
16965
+ if (relation === ".." || relation.startsWith(`..${sep3}`) || isAbsolute3(relation))
16876
16966
  throw new DeploymentError(
16877
16967
  "ARTIFACT_INVALID",
16878
16968
  "Output directory escapes the application root."
16879
16969
  );
16970
+ if (relation.split(sep3).includes(".git"))
16971
+ throw new DeploymentError(
16972
+ "ARTIFACT_INVALID",
16973
+ "Output directory cannot be inside .git."
16974
+ );
16880
16975
  const assets = [];
16881
16976
  async function visit(directory) {
16882
16977
  const entries = await readdir3(directory, { withFileTypes: true });
@@ -16893,7 +16988,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16893
16988
  if (entry.isDirectory()) await visit(path);
16894
16989
  else if (entry.isFile()) {
16895
16990
  assets.push({
16896
- name: relative3(outputRoot, path).split(sep2).join("/"),
16991
+ name: relative3(outputRoot, path).split(sep3).join("/"),
16897
16992
  data: await readFile7(path),
16898
16993
  contentType: contentType(path)
16899
16994
  });
@@ -16901,6 +16996,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16901
16996
  }
16902
16997
  }
16903
16998
  try {
16999
+ await assertInsideOnDisk(appRoot, outputRoot, "Static");
16904
17000
  await visit(outputRoot);
16905
17001
  } catch (error51) {
16906
17002
  if (error51 instanceof DeploymentError) throw error51;
@@ -16940,7 +17036,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
16940
17036
  // ../../packages/build/src/strategy/fingerprint.ts
16941
17037
  import { createHash as createHash3 } from "crypto";
16942
17038
  import { readFile as readFile8 } from "fs/promises";
16943
- 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";
16944
17040
  var APP_ROOT_FILES = [
16945
17041
  "package.json",
16946
17042
  "next.config.js",
@@ -16995,7 +17091,7 @@ async function hashStrategyInputs(checkout, appRoot) {
16995
17091
  for (const path of candidates) {
16996
17092
  const digest = await hashFile(path);
16997
17093
  if (digest === null) continue;
16998
- entries.push([relative4(checkout, path).split(sep3).join("/"), digest]);
17094
+ entries.push([relative4(checkout, path).split(sep4).join("/"), digest]);
16999
17095
  }
17000
17096
  return entries.toSorted(
17001
17097
  ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
@@ -17016,8 +17112,76 @@ async function computeStrategyFingerprint(input) {
17016
17112
  }
17017
17113
 
17018
17114
  // ../../packages/build/src/strategy/project-facts.ts
17019
- 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";
17020
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
17021
17185
  var SOURCE_EXTENSIONS2 = ["ts", "tsx", "js", "jsx", "mjs", "mts"];
17022
17186
  var SOURCE_DIRECTORIES = ["", "src"];
17023
17187
  var VITE_CONFIGS = [
@@ -17038,7 +17202,7 @@ async function findSourceFile(appRoot, basename5) {
17038
17202
  for (const dir of SOURCE_DIRECTORIES) {
17039
17203
  for (const ext of SOURCE_EXTENSIONS2) {
17040
17204
  const rel = dir ? `${dir}/${basename5}.${ext}` : `${basename5}.${ext}`;
17041
- if (await exists4(join11(appRoot, rel))) return rel;
17205
+ if (await exists4(join12(appRoot, rel))) return rel;
17042
17206
  }
17043
17207
  }
17044
17208
  return null;
@@ -17058,19 +17222,28 @@ function matcherRoutesFromSource(source) {
17058
17222
  }
17059
17223
  async function readIfExists(path) {
17060
17224
  try {
17061
- return await readFile9(path, "utf8");
17225
+ return await readFile10(path, "utf8");
17062
17226
  } catch {
17063
17227
  return null;
17064
17228
  }
17065
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
+ }
17066
17239
  async function collectProjectFacts(appRoot) {
17067
17240
  const proxyFile = await findSourceFile(appRoot, "proxy");
17068
17241
  const middlewareFile = await findSourceFile(appRoot, "middleware");
17069
17242
  const routeSourceFile = middlewareFile ?? proxyFile;
17070
- const routeSource = routeSourceFile ? await readIfExists(join11(appRoot, routeSourceFile)) : null;
17243
+ const routeSource = routeSourceFile ? await readIfExists(join12(appRoot, routeSourceFile)) : null;
17071
17244
  let usesVinext = false;
17072
17245
  let nextSpec = null;
17073
- const packageJsonSource = await readIfExists(join11(appRoot, "package.json"));
17246
+ const packageJsonSource = await readIfExists(join12(appRoot, "package.json"));
17074
17247
  if (packageJsonSource) {
17075
17248
  try {
17076
17249
  const packageJson = JSON.parse(packageJsonSource);
@@ -17081,7 +17254,7 @@ async function collectProjectFacts(appRoot) {
17081
17254
  nextSpec = dependency("next");
17082
17255
  if (dependency("vinext") !== null) {
17083
17256
  for (const name of VITE_CONFIGS) {
17084
- const config2 = await readIfExists(join11(appRoot, name));
17257
+ const config2 = await readIfExists(join12(appRoot, name));
17085
17258
  if (config2 && /\bvinext\s*\(/.test(config2) && /@cloudflare\/vite-plugin/.test(config2)) {
17086
17259
  usesVinext = true;
17087
17260
  break;
@@ -17096,7 +17269,8 @@ async function collectProjectFacts(appRoot) {
17096
17269
  middlewareFile,
17097
17270
  matcherRoutes: routeSource ? matcherRoutesFromSource(routeSource) : [],
17098
17271
  usesVinext,
17099
- nextSpec
17272
+ nextSpec,
17273
+ adapterConfig: await findAdapterConfig(appRoot)
17100
17274
  };
17101
17275
  }
17102
17276
 
@@ -17104,6 +17278,12 @@ async function collectProjectFacts(appRoot) {
17104
17278
  function label(strategy) {
17105
17279
  return `P${String(strategy.priority)} ${strategy.id}`;
17106
17280
  }
17281
+ function clause(text2) {
17282
+ return text2.endsWith(".") && !text2.endsWith("..") ? text2.slice(0, -1) : text2;
17283
+ }
17284
+ function sentence(text2) {
17285
+ return text2.endsWith(".") ? text2 : `${text2}.`;
17286
+ }
17107
17287
  function describeOutcome(outcome) {
17108
17288
  switch (outcome.status) {
17109
17289
  case "success":
@@ -17128,7 +17308,7 @@ async function evaluateOne(strategy, context, observer) {
17128
17308
  await observer.onLog({
17129
17309
  level: outcome.status === "success" ? "INFO" : "WARN",
17130
17310
  stage: "BUILDING",
17131
- message: `${label(strategy)} ${describeOutcome(outcome)}.`
17311
+ message: sentence(`${label(strategy)} ${describeOutcome(outcome)}`)
17132
17312
  });
17133
17313
  return { id: strategy.id, priority: strategy.priority, outcome };
17134
17314
  }
@@ -17226,10 +17406,13 @@ async function resolveDeploymentStrategy(strategies, context, observer, options)
17226
17406
  attempts
17227
17407
  };
17228
17408
  }
17409
+ const othersLeft = candidates.some(
17410
+ (candidate) => candidate.id !== pinnedStrategy.id
17411
+ );
17229
17412
  await observer.onLog({
17230
17413
  level: "WARN",
17231
17414
  stage: "BUILDING",
17232
- message: `Stored strategy ${label(pinnedStrategy)} no longer works; resolving again with the remaining candidates.`
17415
+ message: othersLeft ? `Stored strategy ${label(pinnedStrategy)} no longer works; resolving again with the remaining candidates.` : `Stored strategy ${label(pinnedStrategy)} no longer works, and no other deployment strategy is left to try for Next.js ${context.nextVersion}.`
17233
17416
  });
17234
17417
  }
17235
17418
  } else if (options.pinned) {
@@ -17242,13 +17425,14 @@ async function resolveDeploymentStrategy(strategies, context, observer, options)
17242
17425
  const remaining = candidates.filter(
17243
17426
  (candidate) => !attempts.some((attempt) => attempt.id === candidate.id)
17244
17427
  );
17245
- await observer.onLog({
17246
- level: "INFO",
17247
- stage: "BUILDING",
17248
- message: `Resolving deployment strategy for Next.js ${context.nextVersion}: ${remaining.map(label).join(
17249
- ", "
17250
- )}${options.concurrency > 1 && remaining.length > 1 ? " (in parallel)" : ""}.`
17251
- });
17428
+ if (remaining.length > 0)
17429
+ await observer.onLog({
17430
+ level: "INFO",
17431
+ stage: "BUILDING",
17432
+ message: `Resolving deployment strategy for Next.js ${context.nextVersion}: ${remaining.map(label).join(
17433
+ ", "
17434
+ )}${options.concurrency > 1 && remaining.length > 1 ? " (in parallel)" : ""}.`
17435
+ });
17252
17436
  if (options.concurrency > 1) {
17253
17437
  const results = await runPool(
17254
17438
  remaining.map(
@@ -17266,11 +17450,25 @@ async function resolveDeploymentStrategy(strategies, context, observer, options)
17266
17450
  }
17267
17451
  const best = pickBest(candidates, attempts);
17268
17452
  if (!best) {
17453
+ const summary = attempts.map(
17454
+ (attempt) => clause(
17455
+ `P${String(attempt.priority)} ${attempt.id} ${describeOutcome(attempt.outcome)}`
17456
+ )
17457
+ ).join("; ");
17458
+ if (attempts.some(
17459
+ (attempt) => attempt.outcome.status === "failed" && attempt.outcome.appBuild
17460
+ ))
17461
+ throw new DeploymentError(
17462
+ "BUILD_FAILED",
17463
+ sentence(
17464
+ `The app's own build failed for Next.js ${context.nextVersion}: ${summary}`
17465
+ )
17466
+ );
17269
17467
  throw new DeploymentError(
17270
17468
  "NO_DEPLOYMENT_STRATEGY",
17271
- `No deployment strategy succeeded for Next.js ${context.nextVersion}: ${attempts.map(
17272
- (attempt) => `P${String(attempt.priority)} ${attempt.id} ${describeOutcome(attempt.outcome)}`
17273
- ).join("; ")}.`
17469
+ sentence(
17470
+ `No deployment strategy succeeded for Next.js ${context.nextVersion}: ${summary}`
17471
+ )
17274
17472
  );
17275
17473
  }
17276
17474
  await observer.onLog({
@@ -17459,11 +17657,11 @@ function defaultSmokeProbes(matcherRoutes) {
17459
17657
 
17460
17658
  // ../../packages/build/src/strategy/strategies/opennext-aws.ts
17461
17659
  import { writeFile as writeFile6 } from "fs/promises";
17462
- import { join as join13 } from "path";
17660
+ import { join as join14 } from "path";
17463
17661
 
17464
17662
  // ../../packages/build/src/strategy/strategies/shared.ts
17465
- import { access as access7, readFile as readFile10, rm, writeFile as writeFile5 } from "fs/promises";
17466
- 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";
17467
17665
  function capturingObserver(observer) {
17468
17666
  const lines = [];
17469
17667
  return {
@@ -17499,6 +17697,9 @@ function describeFailure(step) {
17499
17697
  function failed(stage, reason) {
17500
17698
  return { status: "failed", stage, reason };
17501
17699
  }
17700
+ function appBuildFailed(reason) {
17701
+ return { status: "failed", stage: "build", reason, appBuild: true };
17702
+ }
17502
17703
  async function info(observer, message) {
17503
17704
  await observer.onLog({ level: "INFO", stage: "BUILDING", message });
17504
17705
  }
@@ -17517,7 +17718,7 @@ async function findProjectBin(appRoot, checkout, name) {
17517
17718
  let dir = resolve5(appRoot);
17518
17719
  const stop = resolve5(checkout);
17519
17720
  for (; ; ) {
17520
- const candidate = join12(dir, "node_modules", ".bin", name);
17721
+ const candidate = join13(dir, "node_modules", ".bin", name);
17521
17722
  if (await pathExists2(candidate)) return candidate;
17522
17723
  if (dir === stop) return null;
17523
17724
  const parent = dirname2(dir);
@@ -17531,7 +17732,7 @@ function dotenvQuote(value) {
17531
17732
  return `"${value.replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
17532
17733
  }
17533
17734
  async function withDevVars(wranglerConfigPath, env, run) {
17534
- const path = join12(dirname2(wranglerConfigPath), ".dev.vars");
17735
+ const path = join13(dirname2(wranglerConfigPath), ".dev.vars");
17535
17736
  const keys = Object.keys(env).filter(
17536
17737
  (key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)
17537
17738
  );
@@ -17564,7 +17765,7 @@ async function bundleWorker(context, layout, observer, errorCode2) {
17564
17765
  cwd: layout.appRoot,
17565
17766
  env: {
17566
17767
  CI: "1",
17567
- WRANGLER_LOG_PATH: join12(
17768
+ WRANGLER_LOG_PATH: join13(
17568
17769
  layout.appRoot,
17569
17770
  layout.outputRoot,
17570
17771
  "wrangler.log"
@@ -17597,12 +17798,12 @@ async function smokeWorker(context, layout, observer) {
17597
17798
  "--show-interactive-dev-session=false",
17598
17799
  "--log-level=warn",
17599
17800
  "--persist-to",
17600
- join12(layout.appRoot, layout.outputRoot, ".naro-smoke-state")
17801
+ join13(layout.appRoot, layout.outputRoot, ".naro-smoke-state")
17601
17802
  ],
17602
17803
  cwd: layout.appRoot,
17603
17804
  env: {
17604
17805
  CI: "1",
17605
- WRANGLER_LOG_PATH: join12(
17806
+ WRANGLER_LOG_PATH: join13(
17606
17807
  layout.appRoot,
17607
17808
  layout.outputRoot,
17608
17809
  "wrangler-dev.log"
@@ -17618,10 +17819,10 @@ async function smokeWorker(context, layout, observer) {
17618
17819
  );
17619
17820
  }
17620
17821
  async function approvePnpmBuilds(checkout, packages) {
17621
- const path = join12(checkout, "pnpm-workspace.yaml");
17822
+ const path = join13(checkout, "pnpm-workspace.yaml");
17622
17823
  let source = "";
17623
17824
  try {
17624
- source = await readFile10(path, "utf8");
17825
+ source = await readFile11(path, "utf8");
17625
17826
  } catch {
17626
17827
  }
17627
17828
  let next = source;
@@ -17714,7 +17915,7 @@ function createOpenNextAwsStrategy() {
17714
17915
  } catch (error51) {
17715
17916
  return failed("prepare", describeError(error51));
17716
17917
  }
17717
- const configPath = join13(fork.appRoot, CONFIG_NAME);
17918
+ const configPath = join14(fork.appRoot, CONFIG_NAME);
17718
17919
  try {
17719
17920
  await writeFile6(
17720
17921
  configPath,
@@ -17743,7 +17944,7 @@ function createOpenNextAwsStrategy() {
17743
17944
  observer
17744
17945
  );
17745
17946
  if (!build.ok) return failed("build", describeFailure(build));
17746
- const serverFunction = join13(fork.appRoot, SERVER_FUNCTION);
17947
+ const serverFunction = join14(fork.appRoot, SERVER_FUNCTION);
17747
17948
  if (!await pathExists2(serverFunction))
17748
17949
  return failed("bundle", `OpenNext AWS produced no ${SERVER_FUNCTION}.`);
17749
17950
  const smoke = await context.ports.smoke.run(
@@ -17765,7 +17966,7 @@ function createOpenNextAwsStrategy() {
17765
17966
  status: "success",
17766
17967
  artifact: {
17767
17968
  kind: "opennext-aws",
17768
- outputRoot: join13(fork.appRoot, ".open-next"),
17969
+ outputRoot: join14(fork.appRoot, ".open-next"),
17769
17970
  serverFunction
17770
17971
  },
17771
17972
  notes: [
@@ -17777,8 +17978,8 @@ function createOpenNextAwsStrategy() {
17777
17978
  }
17778
17979
 
17779
17980
  // ../../packages/build/src/strategy/strategies/opennext-cloudflare.ts
17780
- import { readFile as readFile11 } from "fs/promises";
17781
- import { join as join14 } from "path";
17981
+ import { readFile as readFile12 } from "fs/promises";
17982
+ import { join as join15 } from "path";
17782
17983
  var PROXY_TO_MIDDLEWARE_VARIANT = "proxy-to-middleware";
17783
17984
  var NODE_MIDDLEWARE_REFUSAL = /Node\.js middleware is not (?:currently )?supported/i;
17784
17985
  var NODE_MIDDLEWARE_EXPERIMENTAL = /Node\.js middleware support is experimental/i;
@@ -17786,8 +17987,8 @@ var NODE_MIDDLEWARE_NOT_DEPLOYED = 'Node.js-runtime middleware (runtime: "nodejs
17786
17987
  async function buildDeclaresNodeMiddleware(appRoot) {
17787
17988
  try {
17788
17989
  const manifest2 = JSON.parse(
17789
- await readFile11(
17790
- join14(appRoot, ".next", "server", "functions-config-manifest.json"),
17990
+ await readFile12(
17991
+ join15(appRoot, ".next", "server", "functions-config-manifest.json"),
17791
17992
  "utf8"
17792
17993
  )
17793
17994
  );
@@ -17797,7 +17998,7 @@ async function buildDeclaresNodeMiddleware(appRoot) {
17797
17998
  }
17798
17999
  }
17799
18000
  function createOpenNextCloudflareStrategy(options) {
17800
- async function attempt(context, roots, observer) {
18001
+ async function attempt(context, roots, observer, ownBuild, adapterNotice) {
17801
18002
  let platform;
17802
18003
  try {
17803
18004
  platform = await options.createPlatformBuildConfig(
@@ -17832,9 +18033,16 @@ function createOpenNextCloudflareStrategy(options) {
17832
18033
  return {
17833
18034
  ok: false,
17834
18035
  nodeMiddleware: null,
17835
- outcome: failed("build", describeFailure(nextBuild))
18036
+ outcome: ownBuild ? appBuildFailed(describeFailure(nextBuild)) : failed("build", describeFailure(nextBuild))
17836
18037
  };
17837
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
+ }
17838
18046
  if (await buildDeclaresNodeMiddleware(roots.appRoot)) {
17839
18047
  return {
17840
18048
  ok: false,
@@ -17930,6 +18138,7 @@ function createOpenNextCloudflareStrategy(options) {
17930
18138
  target: "cloudflare",
17931
18139
  supports: () => ({ supported: true }),
17932
18140
  async evaluate(context, observer) {
18141
+ const adapterNotice = { warned: false };
17933
18142
  const rememberedRewrite = context.pinnedVariant === PROXY_TO_MIDDLEWARE_VARIANT && context.facts.proxyFile !== null;
17934
18143
  if (rememberedRewrite) {
17935
18144
  await info(
@@ -17941,7 +18150,13 @@ function createOpenNextCloudflareStrategy(options) {
17941
18150
  observer,
17942
18151
  "P2 opennext-cloudflare: building the project as written."
17943
18152
  );
17944
- const first = await attempt(context, context.workspace, observer);
18153
+ const first = await attempt(
18154
+ context,
18155
+ context.workspace,
18156
+ observer,
18157
+ true,
18158
+ adapterNotice
18159
+ );
17945
18160
  if (first.ok) {
17946
18161
  return {
17947
18162
  status: "success",
@@ -17973,8 +18188,54 @@ function createOpenNextCloudflareStrategy(options) {
17973
18188
  observer,
17974
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.`
17975
18190
  );
17976
- const second = await attempt(context, fork, observer);
17977
- if (!second.ok) return second.outcome;
18191
+ const second = await attempt(
18192
+ context,
18193
+ fork,
18194
+ observer,
18195
+ rememberedRewrite,
18196
+ adapterNotice
18197
+ );
18198
+ if (!second.ok) {
18199
+ if (!rememberedRewrite || second.outcome.status !== "failed" || !second.outcome.appBuild)
18200
+ return second.outcome;
18201
+ await info(
18202
+ observer,
18203
+ `P2 opennext-cloudflare: the rewritten copy did not build; building the project as written once to tell the app's own error from the rewrite's.`
18204
+ );
18205
+ const asWritten = await attempt(
18206
+ context,
18207
+ context.workspace,
18208
+ observer,
18209
+ true,
18210
+ adapterNotice
18211
+ );
18212
+ if (asWritten.ok) {
18213
+ await info(
18214
+ observer,
18215
+ "P2 opennext-cloudflare: the project as written builds and ships as it is; the rewrite is no longer needed."
18216
+ );
18217
+ return {
18218
+ status: "success",
18219
+ artifact: {
18220
+ kind: "cloudflare-worker",
18221
+ artifact: asWritten.artifact
18222
+ },
18223
+ notes: []
18224
+ };
18225
+ }
18226
+ if (asWritten.outcome.status === "failed" && asWritten.outcome.appBuild) {
18227
+ await warn(
18228
+ observer,
18229
+ "P2 opennext-cloudflare: the project as written does not build either, so the error is the app's own."
18230
+ );
18231
+ return asWritten.outcome;
18232
+ }
18233
+ await warn(
18234
+ observer,
18235
+ "P2 opennext-cloudflare: the project as written builds, so the rewritten copy's error is the Edge rewrite's (a Node-only API reached from proxy.ts), not the app's code."
18236
+ );
18237
+ return failed("build", second.outcome.reason);
18238
+ }
17978
18239
  return {
17979
18240
  status: "success",
17980
18241
  artifact: { kind: "cloudflare-worker", artifact: second.artifact },
@@ -18015,7 +18276,7 @@ function createAwsMiddlewareCloudflareAppStrategy() {
18015
18276
  }
18016
18277
 
18017
18278
  // ../../packages/build/src/strategy/strategies/vinext-cloudflare.ts
18018
- import { join as join15 } from "path";
18279
+ import { join as join16 } from "path";
18019
18280
  var VINEXT_MIN_NEXT_MAJOR = 16;
18020
18281
  var VINEXT_BUILD_SCRIPTS = ["esbuild", "workerd"];
18021
18282
  var VINEXT_OUTPUT = {
@@ -18171,7 +18432,7 @@ function createVinextCloudflareStrategy(options) {
18171
18432
  context,
18172
18433
  {
18173
18434
  appRoot: fork.appRoot,
18174
- wranglerConfigPath: join15(
18435
+ wranglerConfigPath: join16(
18175
18436
  fork.appRoot,
18176
18437
  "dist",
18177
18438
  "server",
@@ -18196,7 +18457,7 @@ function createVinextCloudflareStrategy(options) {
18196
18457
  // ../../packages/build/src/strategy/workspace.ts
18197
18458
  import { execFile } from "child_process";
18198
18459
  import { cp, mkdir as mkdir2, readdir as readdir4, rm as rm2 } from "fs/promises";
18199
- 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";
18200
18461
  import { promisify } from "util";
18201
18462
  var execFileAsync = promisify(execFile);
18202
18463
  var SKIPPED_ANYWHERE = /* @__PURE__ */ new Set(["node_modules"]);
@@ -18219,14 +18480,14 @@ async function copyInstalledPackages(source, target2) {
18219
18480
  const walk2 = async (dir) => {
18220
18481
  for (const entry of await readdir4(dir, { withFileTypes: true })) {
18221
18482
  if (!entry.isDirectory() || entry.name === ".git") continue;
18222
- const path = join16(dir, entry.name);
18483
+ const path = join17(dir, entry.name);
18223
18484
  if (entry.name === "node_modules") installs.push(path);
18224
18485
  else await walk2(path);
18225
18486
  }
18226
18487
  };
18227
18488
  await walk2(origin);
18228
18489
  for (const install of installs)
18229
- await copyTree(install, join16(target2, relative5(origin, install)));
18490
+ await copyTree(install, join17(target2, relative5(origin, install)));
18230
18491
  }
18231
18492
  async function copyTree(from, to) {
18232
18493
  await mkdir2(dirname3(to), { recursive: true });
@@ -18241,7 +18502,7 @@ async function copyTree(from, to) {
18241
18502
  await cp(from, to, { recursive: true, verbatimSymlinks: true });
18242
18503
  }
18243
18504
  function forkedAppRoot(checkout, appRoot, forkCheckout) {
18244
- return join16(forkCheckout, relative5(checkout, appRoot));
18505
+ return join17(forkCheckout, relative5(checkout, appRoot));
18245
18506
  }
18246
18507
 
18247
18508
  // ../../packages/build/src/local-build-executor.ts
@@ -18263,7 +18524,7 @@ async function withRouteList(artifact, list, appRoot, observer) {
18263
18524
  }
18264
18525
  }
18265
18526
  var defaultLocalBuildDependencies = {
18266
- createWorkspace: () => mkdtemp(join17(tmpdir(), "next-deploy-build-")),
18527
+ createWorkspace: () => mkdtemp(join18(tmpdir(), "next-deploy-build-")),
18267
18528
  removeWorkspace: (path) => rm3(path, { recursive: true, force: true }),
18268
18529
  runCommand,
18269
18530
  detectPackageManager,
@@ -18297,8 +18558,8 @@ async function readInstalledNextVersion(appRoot, checkout) {
18297
18558
  const stop = resolve7(checkout);
18298
18559
  for (; ; ) {
18299
18560
  try {
18300
- const source = await readFile12(
18301
- join17(dir, "node_modules", "next", "package.json"),
18561
+ const source = await readFile13(
18562
+ join18(dir, "node_modules", "next", "package.json"),
18302
18563
  "utf8"
18303
18564
  );
18304
18565
  const version2 = JSON.parse(source).version;
@@ -18391,8 +18652,8 @@ var LocalBuildExecutor = class {
18391
18652
  }
18392
18653
  async build(input, observer, cache) {
18393
18654
  const workspace = await this.dependencies.createWorkspace();
18394
- const checkout = join17(workspace, "checkout");
18395
- const pmCacheDir = join17(workspace, ".pm-store");
18655
+ const checkout = join18(workspace, "checkout");
18656
+ const pmCacheDir = join18(workspace, ".pm-store");
18396
18657
  let cacheSeq = 0;
18397
18658
  const commandAvailable = this.dependencies.commandAvailable ?? hasCommand;
18398
18659
  const compressor = "zstd";
@@ -18401,7 +18662,7 @@ var LocalBuildExecutor = class {
18401
18662
  transport: cache,
18402
18663
  fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
18403
18664
  runCommand: this.dependencies.runCommand,
18404
- tmpFile: (suffix) => join17(workspace, `cache-${String(cacheSeq++)}${suffix}`),
18665
+ tmpFile: (suffix) => join18(workspace, `cache-${String(cacheSeq++)}${suffix}`),
18405
18666
  // One latch for the whole build so a disabled cache warns once.
18406
18667
  report: { unavailable: false }
18407
18668
  } : null;
@@ -18480,6 +18741,7 @@ var LocalBuildExecutor = class {
18480
18741
  checkout,
18481
18742
  input.rootDirectory
18482
18743
  );
18744
+ await assertAppRootInsideCheckout(checkout, appRoot);
18483
18745
  const stacks = await import("./stack-PDV3NPWC.js");
18484
18746
  const stack = await (this.dependencies.detectStack ?? stacks.detectStack)(
18485
18747
  appRoot
@@ -18592,7 +18854,7 @@ var LocalBuildExecutor = class {
18592
18854
  }
18593
18855
  }
18594
18856
  await notifyStatus(observer, "BUILDING");
18595
- const buildCacheDir = resolved.preset.cachePath ? join17(appRoot, resolved.preset.cachePath) : null;
18857
+ const buildCacheDir = resolved.preset.cachePath ? join18(appRoot, resolved.preset.cachePath) : null;
18596
18858
  const buildCacheKey = sanitizeCacheKey(input.branch);
18597
18859
  if (cachePorts && buildCacheDir) {
18598
18860
  await tryRestore(
@@ -18616,13 +18878,13 @@ var LocalBuildExecutor = class {
18616
18878
  entries: fingerprintEntries,
18617
18879
  nextVersion: nextVersion ? `${String(nextVersion.major)}.${String(nextVersion.minor)}.${String(nextVersion.patch)}` : "unknown"
18618
18880
  });
18619
- const pristine = join17(workspace, "pristine");
18881
+ const pristine = join18(workspace, "pristine");
18620
18882
  const fork = this.dependencies.forkWorkspace ?? defaultFork;
18621
18883
  await fork(checkout, pristine);
18622
18884
  let forks = 0;
18623
18885
  const resolverConcurrency = this.dependencies.resolverConcurrency ?? 1;
18624
18886
  const forkWorkspace = async (label2) => {
18625
- const target2 = join17(
18887
+ const target2 = join18(
18626
18888
  workspace,
18627
18889
  "forks",
18628
18890
  `${String(forks++)}-${label2.replace(/[^a-z0-9-]/gi, "-")}`
@@ -18910,12 +19172,12 @@ async function errorFromResponse(response, context) {
18910
19172
  // ../../packages/sdk/src/session.ts
18911
19173
  import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
18912
19174
  import { homedir } from "os";
18913
- import { join as join18 } from "path";
19175
+ import { join as join19 } from "path";
18914
19176
  function credentialsDir(home = homedir()) {
18915
- return join18(home, ".naro");
19177
+ return join19(home, ".naro");
18916
19178
  }
18917
19179
  function credentialsFile(home = homedir()) {
18918
- return join18(credentialsDir(home), "credentials.json");
19180
+ return join19(credentialsDir(home), "credentials.json");
18919
19181
  }
18920
19182
  function saveSession(session, home = homedir()) {
18921
19183
  mkdirSync(credentialsDir(home), { recursive: true, mode: 448 });
@@ -19351,6 +19613,14 @@ function openBrowser(url2) {
19351
19613
  child.unref();
19352
19614
  }
19353
19615
 
19616
+ // ../../packages/sdk/src/environments.ts
19617
+ function isReservedEnvTarget(value) {
19618
+ return RESERVED_ENVIRONMENT_SLUGS.includes(value);
19619
+ }
19620
+ function isEnvTargetName(value) {
19621
+ return environmentSlugProblem(value) !== "invalid";
19622
+ }
19623
+
19354
19624
  // ../../packages/sdk/src/insights.ts
19355
19625
  var SPEED_INSIGHTS_METRICS = [
19356
19626
  "lcp",
@@ -19463,6 +19733,7 @@ var NaroClient = class {
19463
19733
  insights;
19464
19734
  analytics;
19465
19735
  protection;
19736
+ environments;
19466
19737
  constructor(options) {
19467
19738
  this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
19468
19739
  this.getAccessToken = options.getAccessToken;
@@ -19481,6 +19752,7 @@ var NaroClient = class {
19481
19752
  this.insights = new InsightsApi(this);
19482
19753
  this.analytics = new AnalyticsApi(this);
19483
19754
  this.protection = new ProtectionApi(this);
19755
+ this.environments = new EnvironmentsApi(this);
19484
19756
  }
19485
19757
  async request(path, options) {
19486
19758
  const token = await this.getAccessToken();
@@ -19645,7 +19917,10 @@ var DeploymentsApi = class {
19645
19917
  {
19646
19918
  method: "POST",
19647
19919
  // createDeploymentSchema accepts string | undefined — never null.
19648
- body: options.branch ? { branch: options.branch } : {},
19920
+ body: {
19921
+ ...options.branch ? { branch: options.branch } : {},
19922
+ ...options.environment ? { environment: options.environment } : {}
19923
+ },
19649
19924
  context: "Create deployment"
19650
19925
  }
19651
19926
  );
@@ -19729,6 +20004,8 @@ var DeploymentsApi = class {
19729
20004
  }
19730
20005
  }
19731
20006
  };
20007
+ var STATIC_ASSETS_BINDING = "ASSETS";
20008
+ var MANAGED_DATABASE_BINDING = "DB";
19732
20009
  var EnvApi = class {
19733
20010
  constructor(client) {
19734
20011
  this.client = client;
@@ -19758,18 +20035,69 @@ var EnvApi = class {
19758
20035
  }
19759
20036
  );
19760
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
+ }
19761
20069
  async removeById(projectId, envVarId) {
19762
20070
  await this.client.request(
19763
20071
  `/api/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(envVarId)}`,
19764
20072
  { method: "DELETE", context: "Remove environment variable" }
19765
20073
  );
19766
20074
  }
19767
- /** Removes every row for a key (optionally narrowed to one target). */
20075
+ /**
20076
+ * Removes a key's rows: the one `target` names, or, with no target, the
20077
+ * `production`, `preview` and `all` rows. An environment's value goes only
20078
+ * when its slug is the target, so removing a key everywhere else does not
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.
20082
+ */
19768
20083
  async remove(projectId, key, target2) {
19769
20084
  const rows = await this.list(projectId);
19770
20085
  const matches = rows.filter(
19771
- (row) => row.key === key && (target2 === void 0 || row.target === target2)
20086
+ (row) => row.key === key && (target2 === void 0 ? isReservedEnvTarget(row.target) : row.target === target2)
19772
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
+ }
19773
20101
  if (matches.length === 0) {
19774
20102
  throw new NaroError(
19775
20103
  "ENV_NOT_FOUND",
@@ -20061,6 +20389,55 @@ var ProtectionApi = class {
20061
20389
  );
20062
20390
  }
20063
20391
  };
20392
+ var EnvironmentsApi = class {
20393
+ constructor(client) {
20394
+ this.client = client;
20395
+ }
20396
+ base(projectId) {
20397
+ return `/api/projects/${encodeURIComponent(projectId)}/environments`;
20398
+ }
20399
+ /** The project's environments and how many its plan allows (null: no limit). */
20400
+ async list(projectId) {
20401
+ return this.client.request(this.base(projectId), {
20402
+ context: "List environments"
20403
+ });
20404
+ }
20405
+ /** One environment by its slug, from the list: the routes address it by id. */
20406
+ async get(projectId, slug) {
20407
+ const { environments } = await this.list(projectId);
20408
+ const environment = environments.find((entry) => entry.slug === slug);
20409
+ if (!environment)
20410
+ throw new NaroError("ENVIRONMENT_NOT_FOUND", "Environment not found.");
20411
+ return environment;
20412
+ }
20413
+ async create(projectId, input) {
20414
+ const body = await this.client.request(
20415
+ this.base(projectId),
20416
+ {
20417
+ method: "POST",
20418
+ // JSON drops a key that is undefined and keeps a null, which is what
20419
+ // the route reads: no copyEnvFrom is "preview", null copies nothing.
20420
+ body: input,
20421
+ context: "Create environment"
20422
+ }
20423
+ );
20424
+ return body.environment;
20425
+ }
20426
+ async update(projectId, environmentId, patch) {
20427
+ const body = await this.client.request(
20428
+ `${this.base(projectId)}/${encodeURIComponent(environmentId)}`,
20429
+ { method: "PATCH", body: patch, context: "Update environment" }
20430
+ );
20431
+ return body.environment;
20432
+ }
20433
+ /** Removes the environment, its worker and its protection. */
20434
+ async delete(projectId, environmentId) {
20435
+ await this.client.request(
20436
+ `${this.base(projectId)}/${encodeURIComponent(environmentId)}`,
20437
+ { method: "DELETE", context: "Delete environment" }
20438
+ );
20439
+ }
20440
+ };
20064
20441
  var EXPORT_FIRST_POLL_MS = 1e3;
20065
20442
  var EXPORT_MAX_POLL_MS = 5e3;
20066
20443
  var EXPORT_TIMEOUT_MS = 15 * 6e4;
@@ -20245,10 +20622,12 @@ var DbApi = class {
20245
20622
  * `d1_migrations` — all operations or none. `dryRun` returns the SQL and
20246
20623
  * the name without applying anything. Save `sql` as `migrations/<name>` so
20247
20624
  * `naro db reset` re-applies it; a refused operation's index is in
20248
- * `details.operation`. Policies left under a name the change gives to a
20249
- * table are removed before it runs and stay removed if it then fails:
20250
- * that refusal carries them in `details.removedPolicies`, shaped as the
20251
- * result's `removedPolicies`.
20625
+ * `details.operation`, or — refused by the policy translation, or failed
20626
+ * in SQLite — in `statementIndex` (one statement per operation). The Data API policies go in the same migration: a
20627
+ * rename carries the table's and rewrites the others that read it, a
20628
+ * dropped table's go with it, and what an earlier table left under a name
20629
+ * the change gives is removed — `removedPolicies` lists what went. A change
20630
+ * that fails changes none of them.
20252
20631
  */
20253
20632
  async schema(projectId, operations, options = {}) {
20254
20633
  return this.client.request(
@@ -20992,40 +21371,92 @@ function summarizeStatement(statement2) {
20992
21371
 
20993
21372
  // ../../packages/sdk/src/dotenv.ts
20994
21373
  import { existsSync, readFileSync as readFileSync2 } from "fs";
20995
- 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
+ }
20996
21400
  function parseDotenv(text2) {
20997
- const out = {};
20998
- for (const line of text2.split(/\r?\n/)) {
20999
- const trimmed = line.trim();
21000
- if (!trimmed || trimmed.startsWith("#")) continue;
21001
- const eq = trimmed.indexOf("=");
21002
- if (eq <= 0) continue;
21003
- const key = trimmed.slice(0, eq).trim();
21004
- let value = trimmed.slice(eq + 1).trim();
21005
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
21006
- value = value.slice(1, -1);
21007
- out[key] = value;
21008
- }
21009
- return out;
21401
+ return parseDotenvDetailed(text2).vars;
21010
21402
  }
21011
21403
  function readProjectEnv(dir) {
21012
21404
  for (const name of [".env.local", ".env.production", ".env"]) {
21013
- const path = join19(dir, name);
21405
+ const path = join20(dir, name);
21014
21406
  if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
21015
21407
  }
21016
21408
  return {};
21017
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.";
21018
21427
  function serializeDotenv(vars) {
21019
- const needsQuotes = /[\s"'#$`\\]/;
21020
- return `${Object.entries(vars).map(
21021
- ([key, value]) => needsQuotes.test(value) ? `${key}="${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` : `${key}=${value}`
21022
- ).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")}
21023
21454
  `;
21024
21455
  }
21025
21456
 
21026
21457
  // ../../packages/sdk/src/env-scan.ts
21027
21458
  import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
21028
- import { join as join20 } from "path";
21459
+ import { join as join21 } from "path";
21029
21460
  var SOURCE_EXTENSIONS3 = /* @__PURE__ */ new Set([
21030
21461
  ".ts",
21031
21462
  ".tsx",
@@ -21080,7 +21511,7 @@ function collect(dir, out) {
21080
21511
  for (const entry of entries) {
21081
21512
  if (out.length >= MAX_FILES) return;
21082
21513
  if (entry.isSymbolicLink()) continue;
21083
- const path = join20(dir, entry.name);
21514
+ const path = join21(dir, entry.name);
21084
21515
  if (entry.isDirectory()) {
21085
21516
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name))
21086
21517
  continue;
@@ -21112,7 +21543,7 @@ function scanEnvReferences(dir) {
21112
21543
  }
21113
21544
  }
21114
21545
  for (const name of EXAMPLE_FILES) {
21115
- const path = join20(dir, name);
21546
+ const path = join21(dir, name);
21116
21547
  if (!existsSync2(path)) continue;
21117
21548
  let parsed;
21118
21549
  try {
@@ -21130,11 +21561,11 @@ function scanEnvReferences(dir) {
21130
21561
 
21131
21562
  // ../../packages/sdk/src/framework.ts
21132
21563
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
21133
- import { join as join21 } from "path";
21564
+ import { join as join22 } from "path";
21134
21565
  function readPackageJson2(dir) {
21135
21566
  try {
21136
21567
  return JSON.parse(
21137
- readFileSync4(join21(dir, "package.json"), "utf8")
21568
+ readFileSync4(join22(dir, "package.json"), "utf8")
21138
21569
  );
21139
21570
  } catch {
21140
21571
  return null;
@@ -21145,7 +21576,7 @@ function hasDependency(pkg, name) {
21145
21576
  }
21146
21577
  function hasConfig(dir, base2, evidence) {
21147
21578
  for (const ext of ["js", "mjs", "cjs", "ts", "mts"]) {
21148
- if (existsSync3(join21(dir, `${base2}.${ext}`))) {
21579
+ if (existsSync3(join22(dir, `${base2}.${ext}`))) {
21149
21580
  evidence.push(`${base2}.${ext}`);
21150
21581
  return true;
21151
21582
  }
@@ -21253,8 +21684,8 @@ function detectFramework2(dir) {
21253
21684
  if (!byDependency && !byConfig) continue;
21254
21685
  if (byDependency) evidence.push(`package.json: ${detector.dependency}`);
21255
21686
  if (detector.framework === "nextjs") {
21256
- if (existsSync3(join21(dir, "app"))) evidence.push("app/");
21257
- 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/");
21258
21689
  }
21259
21690
  return {
21260
21691
  framework: detector.framework,
@@ -21265,7 +21696,7 @@ function detectFramework2(dir) {
21265
21696
  deployable: true
21266
21697
  };
21267
21698
  }
21268
- if (!pkg && existsSync3(join21(dir, "index.html"))) {
21699
+ if (!pkg && existsSync3(join22(dir, "index.html"))) {
21269
21700
  evidence.push("index.html (no package.json)");
21270
21701
  return {
21271
21702
  framework: "static",
@@ -21286,8 +21717,26 @@ function detectFramework2(dir) {
21286
21717
  };
21287
21718
  }
21288
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
+
21289
21737
  // ../../packages/data-core/src/storage-path.ts
21290
21738
  var MAX_SINGLE_UPLOAD_BYTES = 52428800;
21739
+ var MAX_OBJECT_NAME_BYTES = 900;
21291
21740
  var STORAGE_RESERVED_WORDS = [
21292
21741
  "authenticated",
21293
21742
  "copy",
@@ -21303,6 +21752,32 @@ var RESERVED = new Set(STORAGE_RESERVED_WORDS);
21303
21752
  var ENCODER = new TextEncoder();
21304
21753
  var LONE_SURROGATE2 = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
21305
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
+ }
21306
21781
  function objectParent(name) {
21307
21782
  const slash = name.lastIndexOf("/");
21308
21783
  return slash < 0 ? "" : name.slice(0, slash);
@@ -21332,6 +21807,7 @@ var STORAGE_OBJECTS = "storage.objects";
21332
21807
  var STORAGE_OBJECTS_TABLE = "_naro_storage_objects";
21333
21808
  var STORAGE_OBJECTS_QUALIFIER = "objects";
21334
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`;
21335
21811
  var STORAGE_BUCKETS_TABLE = "_naro_storage_buckets";
21336
21812
  var STORAGE_POLICY_COLUMNS = [
21337
21813
  "id",
@@ -21499,7 +21975,8 @@ var CATALOG = {
21499
21975
  policy,
21500
21976
  recursion,
21501
21977
  values,
21502
- bytes
21978
+ bytes,
21979
+ depth
21503
21980
  }) => {
21504
21981
  if (recursion)
21505
21982
  return `Infinite recursion detected in policy for relation ${table}.`;
@@ -21507,6 +21984,8 @@ var CATALOG = {
21507
21984
  return `The policies of ${table} bind more than ${values === true ? "50" : String(values)} values in one statement.`;
21508
21985
  if (bytes)
21509
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.`;
21510
21989
  if (policy !== void 0)
21511
21990
  return `Policy ${policy} on ${table} cannot be applied.`;
21512
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.`;
@@ -21564,7 +22043,7 @@ var CATALOG = {
21564
22043
  },
21565
22044
  OBJECT_TOO_LARGE: {
21566
22045
  status: 413,
21567
- 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.`
21568
22047
  },
21569
22048
  MIME_TYPE_NOT_ALLOWED: {
21570
22049
  status: 415,
@@ -21576,7 +22055,7 @@ var CATALOG = {
21576
22055
  },
21577
22056
  OBJECT_EXISTS: {
21578
22057
  status: 409,
21579
- 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."}`
21580
22059
  },
21581
22060
  OBJECT_BUSY: {
21582
22061
  status: 409,
@@ -22254,6 +22733,15 @@ function resolvePolicyTable(snapshot, name) {
22254
22733
  if (folded === REALTIME_MESSAGES) return REALTIME_MESSAGES_TABLE;
22255
22734
  return resolveTable(snapshot, name);
22256
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
+ }
22257
22745
  async function readTableNames(db) {
22258
22746
  return tableNamesFromRows(rowsOf(await db.prepare(TABLE_NAMES_SQL).all()));
22259
22747
  }
@@ -22295,6 +22783,14 @@ function utf8ByteLength(text2) {
22295
22783
  function quoteIdentifier(name) {
22296
22784
  return `"${name.replaceAll('"', '""')}"`;
22297
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
+ }
22298
22794
  var Failure = class extends Error {
22299
22795
  constructor(error51, reason) {
22300
22796
  super(error51.code);
@@ -22478,6 +22974,12 @@ var VALUE_KINDS = /* @__PURE__ */ new Set([
22478
22974
  function isValueExpr(expr) {
22479
22975
  return VALUE_KINDS.has(expr.kind);
22480
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
+ }
22481
22983
  function isConditionExpr(expr) {
22482
22984
  if (!isValueExpr(expr)) return true;
22483
22985
  if (expr.kind === "column") return true;
@@ -22529,7 +23031,6 @@ var SPECIAL_WORDS = /* @__PURE__ */ new Set([
22529
23031
  "auth"
22530
23032
  ]);
22531
23033
  var TEXT_TYPES = /* @__PURE__ */ new Set(["text", "varchar", "uuid"]);
22532
- var STORAGE_SUBQUERY_PROBLEM = "A policy cannot read storage.objects in a subquery yet";
22533
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)}`;
22534
23035
  function storageHelperColumnProblem(fn) {
22535
23036
  return `storage.${fn}() takes the name column of storage.objects`;
@@ -22980,8 +23481,12 @@ function atRealtimeMessages(stream) {
22980
23481
  }
22981
23482
  function readTableName(stream) {
22982
23483
  const first = stream.peek();
22983
- if (atStorageObjects(stream))
22984
- 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
+ }
22985
23490
  if (atRealtimeMessages(stream))
22986
23491
  throw new PolicySyntaxError(REALTIME_SUBQUERY_PROBLEM, first.position);
22987
23492
  let token = first;
@@ -23008,12 +23513,6 @@ function readTableName(stream) {
23008
23513
  return name;
23009
23514
  }
23010
23515
  function readPolicyTarget(stream) {
23011
- if (atStorageObjects(stream)) {
23012
- stream.next();
23013
- stream.next();
23014
- stream.next();
23015
- return STORAGE_OBJECTS;
23016
- }
23017
23516
  if (atRealtimeMessages(stream)) {
23018
23517
  stream.next();
23019
23518
  stream.next();
@@ -23433,9 +23932,10 @@ function printValue(expr) {
23433
23932
  }
23434
23933
  function printSubquery(subquery) {
23435
23934
  const select = subquery.select === "*" ? "*" : printValue(subquery.select);
23935
+ const table = asciiLower(subquery.table) === STORAGE_OBJECTS ? STORAGE_OBJECTS : quoteIdent(subquery.table);
23436
23936
  const alias = subquery.alias === null ? "" : ` ${quoteIdent(subquery.alias)}`;
23437
23937
  const where = subquery.where === null ? "" : ` WHERE ${printExpr(subquery.where)}`;
23438
- return `SELECT ${select} FROM ${quoteIdent(subquery.table)}${alias}${where}`;
23938
+ return `SELECT ${select} FROM ${table}${alias}${where}`;
23439
23939
  }
23440
23940
  function printElement(element) {
23441
23941
  if (typeof element === "object") return printValue(element);
@@ -23788,6 +24288,9 @@ var FILTER_OPERATORS = [
23788
24288
  "containedBy",
23789
24289
  "overlaps"
23790
24290
  ];
24291
+ function leastFilters(firstColumn) {
24292
+ return firstColumn === void 0 ? [] : [{ operator: "is", column: firstColumn, value: null }];
24293
+ }
23791
24294
  function isAuthUid(value) {
23792
24295
  return isPlainObject2(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "auth") && value.auth === "uid";
23793
24296
  }
@@ -24401,12 +24904,21 @@ function storedSource(table, names, alias) {
24401
24904
  function tableLabel(table) {
24402
24905
  return table.name === STORAGE_OBJECTS_TABLE ? STORAGE_OBJECTS : table.name;
24403
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
+ }
24404
24913
  function missingColumnReason(name, tables) {
24405
24914
  return `column ${name} does not exist on ${tables.join(" or ")}`;
24406
24915
  }
24407
24916
  function missingRelationReason(table) {
24408
24917
  return `relation ${table} does not exist (a system table cannot be read either)`;
24409
24918
  }
24919
+ function unresolvedSubqueryReason(table) {
24920
+ return asciiLower(table) === STORAGE_OBJECTS ? STORAGE_UNAVAILABLE_REASON : missingRelationReason(table);
24921
+ }
24410
24922
  function resolveColumn(env, ref) {
24411
24923
  const name = asciiLower(ref.name);
24412
24924
  if (ref.qualifier !== null) {
@@ -24516,10 +25028,10 @@ function storageHelperSql(fn, index, operand) {
24516
25028
  }
24517
25029
  function storageCallSql(env, expr) {
24518
25030
  const problem = storageHelperColumnProblem(expr.fn);
24519
- if (asciiLower(env.blame.policy.table) !== STORAGE_OBJECTS)
25031
+ if (!env.scopes.some((scope) => scope.table.name === STORAGE_OBJECTS_TABLE))
24520
25032
  return blamed(env, problem);
24521
25033
  const { source, column: column2 } = resolveColumn(env, expr.column);
24522
- 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")
24523
25035
  return blamed(env, problem);
24524
25036
  const index = expr.fn === "foldername" ? expr.index : null;
24525
25037
  if (expr.fn === "foldername" ? index === null || !Number.isInteger(index) || index < 1 || index > MAX_FOLDERNAME_INDEX : expr.index !== null)
@@ -24533,7 +25045,8 @@ function storageCallSql(env, expr) {
24533
25045
  })
24534
25046
  );
24535
25047
  }
24536
- function operandSide(env, expr) {
25048
+ function operandSide(env, value) {
25049
+ const expr = comparedValue(value);
24537
25050
  if (expr.kind === "cast") return operandSide(env, expr.operand);
24538
25051
  if (expr.kind !== "column" && expr.kind !== "realtime")
24539
25052
  return {
@@ -24594,7 +25107,10 @@ function conditionSql(env, expr) {
24594
25107
  switch (expr.kind) {
24595
25108
  case "and":
24596
25109
  case "or":
24597
- 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
+ );
24598
25114
  case "not":
24599
25115
  return `(NOT ${conditionSql(env, expr.arg)})`;
24600
25116
  default:
@@ -24677,8 +25193,8 @@ function conditionSql(env, expr) {
24677
25193
  }
24678
25194
  }
24679
25195
  function subquerySql(env, subquery, select) {
24680
- const table = resolveTable(env.snapshot, subquery.table);
24681
- 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));
24682
25198
  const { state } = env;
24683
25199
  if (++state.subqueries > MAX_STATEMENT_SUBQUERIES)
24684
25200
  return blamed(env, STATEMENT_SUBQUERIES_REASON);
@@ -24692,7 +25208,7 @@ function subquerySql(env, subquery, select) {
24692
25208
  const alias = `_naro_s${String(++state.aliases)}`;
24693
25209
  const source = storedSource(
24694
25210
  table,
24695
- [asciiLower(subquery.alias ?? table.name)],
25211
+ [asciiLower(subqueryName(subquery))],
24696
25212
  alias
24697
25213
  );
24698
25214
  const inner = { ...env, scopes: [...env.scopes, source] };
@@ -24704,11 +25220,12 @@ function subquerySql(env, subquery, select) {
24704
25220
  selected = valueSql(inner, subquery.select);
24705
25221
  if (env.check) item = operandSide(inner, subquery.select);
24706
25222
  }
24707
- const own2 = storedSource(table, [asciiLower(table.name)], alias);
25223
+ const own2 = storedSource(table, ownNames(table), alias);
24708
25224
  const rls = inlinedPolicies(env, table, own2);
25225
+ const ready = table.name === STORAGE_OBJECTS_TABLE ? `${storageReadySql(alias)} AND ` : "";
24709
25226
  const where = subquery.where === null ? "" : ` AND ${conditionSql(inner, subquery.where)}`;
24710
25227
  return {
24711
- 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}`,
24712
25229
  item
24713
25230
  };
24714
25231
  } finally {
@@ -24761,7 +25278,10 @@ function groupTerms(env, sources, action, permissive, restrictive, pick2) {
24761
25278
  terms.push(term);
24762
25279
  }
24763
25280
  const group = terms.length === 0 ? { sql: "0", params: [] } : terms.length === 1 ? terms[0] : {
24764
- sql: `(${terms.map((term) => term.sql).join(" OR ")})`,
25281
+ sql: joinConditions(
25282
+ terms.map((term) => term.sql),
25283
+ "OR"
25284
+ ),
24765
25285
  params: terms.flatMap((term) => term.params)
24766
25286
  };
24767
25287
  const restricted = restrictive.flatMap((policy) => {
@@ -24771,9 +25291,10 @@ function groupTerms(env, sources, action, permissive, restrictive, pick2) {
24771
25291
  return { permissive: group, restrictive: restricted };
24772
25292
  }
24773
25293
  function inlinedPolicies(env, table, source) {
25294
+ const label2 = tableLabel(table);
24774
25295
  const found = applicablePolicies(
24775
25296
  env.req.policies,
24776
- table.name,
25297
+ label2,
24777
25298
  "select",
24778
25299
  env.req.role
24779
25300
  );
@@ -24785,7 +25306,7 @@ function inlinedPolicies(env, table, source) {
24785
25306
  if (found.permissive.length === 0) return "0";
24786
25307
  return withTableActive(
24787
25308
  env.state,
24788
- table.name,
25309
+ label2,
24789
25310
  [...found.permissive, ...found.restrictive],
24790
25311
  () => {
24791
25312
  const inner = { ...env, check: null, params: [] };
@@ -24804,7 +25325,10 @@ function inlinedPolicies(env, table, source) {
24804
25325
  for (const part of parts) env.params.push(...part.params);
24805
25326
  const [only] = parts;
24806
25327
  if (parts.length === 1 && only) return only.sql;
24807
- return `(${parts.map((part) => part.sql).join(" AND ")})`;
25328
+ return joinConditions(
25329
+ parts.map((part) => part.sql),
25330
+ "AND"
25331
+ );
24808
25332
  }
24809
25333
  );
24810
25334
  }
@@ -24899,11 +25423,8 @@ function scopedValues(operand, scopes) {
24899
25423
  return operandValues(operand).map((value) => ({ value, scopes }));
24900
25424
  }
24901
25425
  function subqueryScopes(snapshot, subquery, scopes) {
24902
- const inner = resolveTable(snapshot, subquery.table);
24903
- return inner ? [
24904
- ...scopes,
24905
- { names: [asciiLower(subquery.alias ?? inner.name)], table: inner }
24906
- ] : void 0;
25426
+ const inner = resolveSubqueryTable(snapshot, subquery.table);
25427
+ return inner ? [...scopes, { names: [asciiLower(subqueryName(subquery))], table: inner }] : void 0;
24907
25428
  }
24908
25429
  function columnsReadOn(snapshot, table, expr, written) {
24909
25430
  const found = [];
@@ -25238,7 +25759,7 @@ function nodeSql(table, node, params, path) {
25238
25759
  const terms = children.map(
25239
25760
  (child, index) => nodeSql(table, child, params, `${path}.${key}[${index}]`)
25240
25761
  );
25241
- return `(${terms.join(key === "or" ? " OR " : " AND ")})`;
25762
+ return joinConditions(terms, key === "or" ? "OR" : "AND");
25242
25763
  }
25243
25764
  function whereClause(table, filters, policy, params) {
25244
25765
  const parts = [];
@@ -25249,7 +25770,7 @@ function whereClause(table, filters, policy, params) {
25249
25770
  const client = filters.map(
25250
25771
  (node, index) => nodeSql(table, node, params, `filters[${index}]`)
25251
25772
  );
25252
- if (client.length > 0) parts.push(`(${client.join(" AND ")})`);
25773
+ if (client.length > 0) parts.push(joinConditions(client, "AND"));
25253
25774
  return parts.length === 0 ? "" : ` WHERE ${parts.join(" AND ")}`;
25254
25775
  }
25255
25776
  function orderClause(table, order) {
@@ -25451,7 +25972,7 @@ var Rls = class {
25451
25972
  const found = this.found(command);
25452
25973
  return [...found.permissive, ...found.restrictive];
25453
25974
  });
25454
- const key = asciiLower(table.name);
25975
+ const key = asciiLower(this.policyTable);
25455
25976
  if (hasSubqueries(all) && !this.state.active.includes(key))
25456
25977
  this.state.active.push(key);
25457
25978
  for (const command of commands) {
@@ -26257,51 +26778,829 @@ function compile(query, snapshot, decision, context, options = {}) {
26257
26778
  }
26258
26779
  }
26259
26780
 
26260
- // ../../packages/data-core/src/policy-index.ts
26261
- 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`;
26262
- function indexesFromRows(rows) {
26263
- const tables = /* @__PURE__ */ new Map();
26264
- for (const row of rows) {
26265
- const table = row.table_name;
26266
- const column2 = row.column_name;
26267
- if (typeof table !== "string" || typeof column2 !== "string") continue;
26268
- if (Number(row.partial) !== 0) continue;
26269
- const key = asciiLower(table);
26270
- const columns = tables.get(key) ?? /* @__PURE__ */ new Map();
26271
- tables.set(key, columns);
26272
- const collations = columns.get(asciiLower(column2)) ?? [];
26273
- collations.push(
26274
- typeof row.collation === "string" ? row.collation.toUpperCase() : "BINARY"
26275
- );
26276
- columns.set(asciiLower(column2), collations);
26277
- }
26278
- 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;
26279
26787
  }
26280
- function resolve8(scopes, qualifier, name) {
26281
- const column2 = asciiLower(name);
26282
- for (let index = scopes.length - 1; index >= 0; index--) {
26283
- const scope = scopes[index];
26284
- if (qualifier !== null && !scope.names.includes(asciiLower(qualifier)))
26285
- continue;
26286
- const found = scope.table.byName.get(column2);
26287
- if (found) return { column: found, scope: index };
26288
- if (qualifier !== null) return void 0;
26289
- }
26290
- return void 0;
26788
+ function isDigit2(code) {
26789
+ return code >= 48 && code <= 57;
26291
26790
  }
26292
- function realtimeTopic2(scopes) {
26293
- const [root] = scopes;
26294
- if (root?.table.name !== REALTIME_MESSAGES) return void 0;
26295
- const column2 = root.table.byName.get("topic");
26296
- return column2 ? { column: column2, scope: 0 } : void 0;
26791
+ function isWordPart(code) {
26792
+ return isWordStart(code) || isDigit2(code) || code === 36;
26297
26793
  }
26298
- function valueScopes(scopes, value) {
26299
- switch (value.kind) {
26300
- case "column": {
26301
- const found = resolve8(scopes, value.qualifier, value.name);
26302
- return found ? [found.scope] : [];
26303
- }
26304
- case "func":
26794
+ function isSpace3(code) {
26795
+ return code === 32 || code >= 9 && code <= 13;
26796
+ }
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" }
26848
+ );
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":
26305
27604
  return value.args.flatMap((arg) => valueScopes(scopes, arg));
26306
27605
  case "cast":
26307
27606
  return valueScopes(scopes, value.operand);
@@ -26352,12 +27651,9 @@ function readsOutside(scopes, expr, depth, snapshot) {
26352
27651
  }
26353
27652
  }
26354
27653
  function subqueryScopes2(scopes, subquery, snapshot) {
26355
- const table = resolveTable(snapshot, subquery.table);
27654
+ const table = resolveSubqueryTable(snapshot, subquery.table);
26356
27655
  if (!table) return void 0;
26357
- return [
26358
- ...scopes,
26359
- { table, names: [asciiLower(subquery.alias ?? table.name)] }
26360
- ];
27656
+ return [...scopes, { table, names: [asciiLower(subqueryName(subquery))] }];
26361
27657
  }
26362
27658
  function conjuncts(expr) {
26363
27659
  if (expr === null) return [];
@@ -26371,24 +27667,27 @@ function columnAffinityOf(column2) {
26371
27667
  if (affinity2 === "BLOB") return "blob";
26372
27668
  return "numeric";
26373
27669
  }
26374
- function operandColumn(scopes, value) {
27670
+ function operandColumn(scopes, operand) {
27671
+ const value = comparedValue(operand);
26375
27672
  if (value.kind === "realtime") return realtimeTopic2(scopes);
26376
27673
  if (value.kind !== "column") return void 0;
26377
27674
  return resolve8(scopes, value.qualifier, value.name);
26378
27675
  }
26379
27676
  function valueAffinity(scopes, value) {
26380
- if (value.kind === "cast") return "text";
27677
+ if (comparedValue(value).kind === "cast") return "text";
26381
27678
  const found = operandColumn(scopes, value);
26382
27679
  return found ? columnAffinityOf(found.column) : "none";
26383
27680
  }
26384
27681
  function valueCollation(scopes, value) {
26385
- if (value.kind === "cast") return valueCollation(scopes, value.operand);
27682
+ const compared = comparedValue(value);
27683
+ if (compared.kind === "cast") return valueCollation(scopes, compared.operand);
26386
27684
  const found = operandColumn(scopes, value);
26387
27685
  return found ? found.column.collation : void 0;
26388
27686
  }
26389
27687
  function waysIn(scopes, where) {
26390
27688
  const inner = scopes.length - 1;
26391
- const own2 = (value) => {
27689
+ const own2 = (operand) => {
27690
+ const value = comparedValue(operand);
26392
27691
  if (value.kind !== "column") return void 0;
26393
27692
  const found = resolve8(scopes, value.qualifier, value.name);
26394
27693
  return found?.scope === inner ? found.column : void 0;
@@ -26436,7 +27735,7 @@ function isRowid(table, column2) {
26436
27735
  function blocked(table, indexes, way) {
26437
27736
  if (isRowid(table, way.column))
26438
27737
  return way.affinity === "text" ? "affinity" : void 0;
26439
- 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));
26440
27739
  if (!collations || collations.length === 0) return "none";
26441
27740
  const wanted = way.collation === void 0 ? way.column.collation : way.collation;
26442
27741
  if (wanted !== null && !collations.includes(wanted)) return "collation";
@@ -26447,9 +27746,17 @@ function blocked(table, indexes, way) {
26447
27746
  }
26448
27747
  function problemMessage(policy, outer, table, ways, why) {
26449
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`;
26450
- 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}`;
26451
27750
  const override = "or send it with allow_unindexed=1 (naro policy create --allow-unindexed) to accept a scan of every row";
26452
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
+ }
26453
27760
  if (!way)
26454
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}.`;
26455
27762
  const column2 = way.column.name;
@@ -26489,12 +27796,12 @@ function unindexedSubqueries(snapshot, indexes, policies) {
26489
27796
  const ways = waysIn(inner, expr.subquery.where);
26490
27797
  const reasons = ways.map((way) => blocked(table, indexes, way));
26491
27798
  if (!reasons.includes(void 0)) {
26492
- const order = ["none", "collation", "affinity"];
27799
+ const order = table.name === STORAGE_OBJECTS_TABLE ? ["collation", "affinity", "none"] : ["none", "collation", "affinity"];
26493
27800
  const best = order.map((reason) => reasons.indexOf(reason)).find((index) => index >= 0);
26494
27801
  const way = best === void 0 ? void 0 : ways[best];
26495
27802
  found.push({
26496
27803
  policy,
26497
- table: table.name,
27804
+ table: tableLabel(table),
26498
27805
  message: problemMessage(
26499
27806
  policy,
26500
27807
  outer,
@@ -26688,6 +27995,14 @@ function privateChannelAccess(input) {
26688
27995
  return { ok: true, statement: { sql, params } };
26689
27996
  }
26690
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
+
26691
28006
  // ../../packages/data-core/src/storage-sql.ts
26692
28007
  var STORAGE_NONCE_MAX_DIGITS = 64;
26693
28008
  function nonceOf(nonce) {
@@ -26817,6 +28132,73 @@ function claimOverwrite(i) {
26817
28132
  [[i.lockId, i.expiresAt, i.pending, i.bucket, i.name], room.values]
26818
28133
  );
26819
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
+ }
26820
28202
  function claimDelete(i) {
26821
28203
  return compose(
26822
28204
  [
@@ -26848,6 +28230,16 @@ function readObjects(i) {
26848
28230
  [[i.bucket, JSON.stringify([...new Set(i.names)])]]
26849
28231
  );
26850
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
+ }
26851
28243
  var SORT_COLUMNS = {
26852
28244
  name: "name",
26853
28245
  created_at: "created_at",
@@ -26939,7 +28331,7 @@ function storageUsing(input) {
26939
28331
  assertFragmentAlias(alias, "storageUsing");
26940
28332
  const on = target(snapshot);
26941
28333
  if ("ok" in on) return on;
26942
- const status = `(${quoteIdentifier(alias)}.${quoteIdentifier("status")} = 'ready')`;
28334
+ const status = storageReadySql(alias);
26943
28335
  if (context.role === "service")
26944
28336
  return { ok: true, fragment: { sql: status, params: [] } };
26945
28337
  try {
@@ -27029,12 +28421,12 @@ var CONTEXTS = [
27029
28421
  userId: "naro-policy-check",
27030
28422
  email: "check@naro.invalid",
27031
28423
  emailVerified: true
27032
- }
28424
+ },
28425
+ { role: "authenticated", userId: "naro-policy-check" }
27033
28426
  ];
27034
28427
  function probes(table, context, owners) {
27035
28428
  const writable = table.columns.filter((column2) => !column2.generated);
27036
- const [first] = table.columns;
27037
- const filters = first ? [{ operator: "is", column: first.name, value: null }] : [];
28429
+ const filters = leastFilters(table.columns[0]?.name);
27038
28430
  const row = Object.fromEntries(
27039
28431
  writable.map((column2) => [
27040
28432
  column2.name,
@@ -27042,7 +28434,7 @@ function probes(table, context, owners) {
27042
28434
  ])
27043
28435
  );
27044
28436
  const queries = [
27045
- { version: 1, action: "select", table: table.name, columns: "*" },
28437
+ { version: 1, action: "select", table: table.name, columns: "*", filters },
27046
28438
  { version: 1, action: "insert", table: table.name, rows: [row] },
27047
28439
  {
27048
28440
  version: 1,
@@ -27065,16 +28457,35 @@ function probes(table, context, owners) {
27065
28457
  }
27066
28458
  var STORAGE_PROBE_ALIAS = "o";
27067
28459
  var STORAGE_PROBE_NONCE = "f".repeat(STORAGE_NONCE_MAX_DIGITS);
27068
- var STORAGE_CONTEXTS = [
27069
- ...CONTEXTS,
27070
- { role: "authenticated", userId: "naro-policy-check" }
27071
- ];
27072
- 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) {
27073
28484
  const refusals = [];
27074
28485
  const row = Object.fromEntries(
27075
28486
  table.columns.map((column2) => [column2.name, ""])
27076
28487
  );
27077
- for (const context of STORAGE_CONTEXTS) {
28488
+ for (const context of /* @__PURE__ */ new Set([...CONTEXTS, ...measuring])) {
27078
28489
  const using = (commands) => {
27079
28490
  const result = storageUsing({
27080
28491
  snapshot,
@@ -27110,25 +28521,33 @@ function storageRefusals(snapshot, table, policies) {
27110
28521
  overwriteCheckRow({ metadata: "", now: "", version: "" }),
27111
28522
  true
27112
28523
  );
27113
- for (const statement2 of storageStatements(
28524
+ const moveCheck = check2("update", moveCheckRow({ to: "" }), true);
28525
+ for (const { label: label2, statement: statement2 } of storageStatements(
27114
28526
  select,
27115
28527
  update,
27116
28528
  remove,
27117
28529
  [insertCheck, upsertCheck],
27118
- updateCheck
28530
+ updateCheck,
28531
+ moveCheck
27119
28532
  )) {
27120
28533
  const problem = storageStatementProblem(statement2, "select");
27121
28534
  if (problem) refusals.push(problem);
28535
+ else if (measuring.includes(context))
28536
+ measured(label2, statement2.sql, callerName(context));
27122
28537
  }
27123
28538
  }
27124
28539
  return refusals;
27125
28540
  }
27126
- function storageStatements(select, update, remove, insertChecks, updateCheck) {
28541
+ function storageStatements(select, update, remove, insertChecks, updateCheck, moveCheck) {
27127
28542
  const statements = [];
28543
+ const add = (label2, ...made) => {
28544
+ for (const statement2 of made) statements.push({ label: label2, statement: statement2 });
28545
+ };
27128
28546
  const value = "";
27129
- for (const insertCheck of insertChecks)
28547
+ for (const [index, insertCheck] of insertChecks.entries())
27130
28548
  if (insertCheck)
27131
- statements.push(
28549
+ add(
28550
+ index === 0 ? "an upload's claim" : "an upsert's claim",
27132
28551
  claimUpload({
27133
28552
  id: value,
27134
28553
  bucket: value,
@@ -27144,7 +28563,8 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27144
28563
  })
27145
28564
  );
27146
28565
  if (select && update && updateCheck)
27147
- statements.push(
28566
+ add(
28567
+ "an overwrite's claim",
27148
28568
  claimOverwrite({
27149
28569
  bucket: value,
27150
28570
  name: value,
@@ -27159,8 +28579,28 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27159
28579
  nonce: STORAGE_PROBE_NONCE
27160
28580
  })
27161
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
+ );
27162
28601
  if (remove)
27163
- statements.push(
28602
+ add(
28603
+ "a delete's claim",
27164
28604
  claimDelete({
27165
28605
  bucket: value,
27166
28606
  names: [],
@@ -27169,13 +28609,27 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27169
28609
  expiresAt: value
27170
28610
  })
27171
28611
  );
27172
- if (select)
27173
- statements.push(
27174
- readObject2({ bucket: value, name: value, using: select }),
27175
- // Several names at once (3-1g): as many values as readObject's, 32
27176
- // bytes more — the listing below is always wider, but the worker
27177
- // builds this one too.
27178
- 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",
27179
28633
  listObjects({
27180
28634
  bucket: value,
27181
28635
  prefix: "p",
@@ -27186,6 +28640,7 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
27186
28640
  using: select
27187
28641
  })
27188
28642
  );
28643
+ }
27189
28644
  return statements;
27190
28645
  }
27191
28646
  var REALTIME_PROBE_CHANNEL = "naro-policy-check";
@@ -27198,13 +28653,13 @@ function joinReason(reason) {
27198
28653
  return JOIN_STATEMENT;
27199
28654
  return reason;
27200
28655
  }
27201
- function realtimeRefusals(snapshot, policies) {
28656
+ function realtimeRefusals(snapshot, policies, measuring, measured) {
27202
28657
  const refusals = [];
27203
28658
  const applying = (command) => policies.filter(
27204
28659
  (policy) => asciiLower(policy.table) !== REALTIME_MESSAGES || policy.command === command || policy.command === "all"
27205
28660
  );
27206
28661
  for (const set2 of [policies, applying("select"), applying("insert")])
27207
- for (const context of STORAGE_CONTEXTS) {
28662
+ for (const context of /* @__PURE__ */ new Set([...CONTEXTS, ...measuring])) {
27208
28663
  const plan = privateChannelAccess({
27209
28664
  snapshot,
27210
28665
  context,
@@ -27212,7 +28667,11 @@ function realtimeRefusals(snapshot, policies) {
27212
28667
  channel: REALTIME_PROBE_CHANNEL,
27213
28668
  nonce: REALTIME_PROBE_NONCE
27214
28669
  });
27215
- 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
+ }
27216
28675
  const reason = joinReason(plan.reason);
27217
28676
  refusals.push(
27218
28677
  reason === void 0 ? { error: plan.error } : { error: plan.error, reason }
@@ -27254,9 +28713,48 @@ function namedInRefusal(message, policies) {
27254
28713
  }
27255
28714
  return found;
27256
28715
  }
27257
- 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) {
27258
28755
  const found = /* @__PURE__ */ new Map();
27259
28756
  const loaded = loadNamed(policies);
28757
+ const measuring = depthContexts(CONTEXTS, claims ?? readsClaims(loaded));
27260
28758
  for (const policy of loaded)
27261
28759
  if (policy.invalid !== null)
27262
28760
  found.set(`read:${policy.table}:${policy.name}`, {
@@ -27318,24 +28816,30 @@ function problems(snapshot, policies, options) {
27318
28816
  }
27319
28817
  }
27320
28818
  const probe = loadNamed(
27321
- [...tables.values()].map((table) => ({
27322
- table: table.name,
27323
- name: "naro_policy_check",
27324
- permissive: true,
27325
- command: "select",
27326
- roles: ["anon", "authenticated"],
27327
- using: "true",
27328
- check: null,
27329
- owner: null
27330
- }))
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
+ })
27331
28836
  );
27332
- const compiled = [...loaded, ...probe];
27333
28837
  const refused = (probed, error51, reason) => {
27334
28838
  if (error51.code !== "POLICY_INVALID") return;
27335
- 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}.`;
27336
28840
  const named = namedInRefusal(error51.message, loaded);
27337
28841
  const blamed2 = named === void 0 ? probed : tableLabelOf(snapshot, named.table);
27338
- const key = `${blamed2}:${message}`;
28842
+ const key = reason === STORAGE_UNAVAILABLE_REASON ? `${blamed2}:${named?.name ?? ""}:${message}` : `${blamed2}:${message}`;
27339
28843
  if (!found.has(key))
27340
28844
  found.set(key, {
27341
28845
  problem: {
@@ -27347,12 +28851,29 @@ function problems(snapshot, policies, options) {
27347
28851
  policy: ""
27348
28852
  });
27349
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
+ };
27350
28867
  for (const table of tables.values()) {
27351
28868
  const owners = new Set(
27352
28869
  policiesOn(loaded, table.name).flatMap(
27353
28870
  (policy) => policy.owner ? [asciiLower(policy.owner)] : []
27354
28871
  )
27355
28872
  );
28873
+ const compiled = [
28874
+ ...loaded,
28875
+ ...probe.filter((policy) => policy.table === table.name)
28876
+ ];
27356
28877
  for (const context of CONTEXTS)
27357
28878
  for (const query of probes(table, context, owners)) {
27358
28879
  const decision = decideQuery(context, query, compiled, table.name);
@@ -27363,13 +28884,27 @@ function problems(snapshot, policies, options) {
27363
28884
  };
27364
28885
  if (!result.ok) refused(table.name, result.error, result.reason);
27365
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
+ }
27366
28899
  }
27367
28900
  if (policiesOn(loaded, STORAGE_OBJECTS).length > 0) {
27368
28901
  if (snapshot.storage)
27369
28902
  for (const { error: error51, reason } of storageRefusals(
27370
28903
  snapshot,
27371
28904
  snapshot.storage,
27372
- loaded
28905
+ loaded,
28906
+ measuring,
28907
+ measured(STORAGE_OBJECTS)
27373
28908
  ))
27374
28909
  refused(STORAGE_OBJECTS, error51, reason);
27375
28910
  else
@@ -27383,8 +28918,22 @@ function problems(snapshot, policies, options) {
27383
28918
  });
27384
28919
  }
27385
28920
  if (policiesOn(loaded, REALTIME_MESSAGES).length > 0)
27386
- 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
+ ))
27387
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
+ });
27388
28937
  if (options.indexes && options.allowUnindexed !== true)
27389
28938
  for (const problem of unindexedSubqueries(
27390
28939
  snapshot,
@@ -27501,16 +29050,16 @@ function readCreate(stream, start) {
27501
29050
  let check2 = null;
27502
29051
  let position = start.position;
27503
29052
  if (stream.acceptKeyword("using")) {
27504
- const clause = readClause(stream);
27505
- using = clause.text;
27506
- position = clause.position;
29053
+ const clause2 = readClause(stream);
29054
+ using = clause2.text;
29055
+ position = clause2.position;
27507
29056
  }
27508
29057
  if (isKeyword(stream.peek(), "with")) {
27509
29058
  stream.next();
27510
29059
  stream.expectKeyword("check");
27511
- const clause = readClause(stream);
27512
- check2 = clause.text;
27513
- position = clause.position;
29060
+ const clause2 = readClause(stream);
29061
+ check2 = clause2.text;
29062
+ position = clause2.position;
27514
29063
  }
27515
29064
  if (!roles.ok) throw new Error("unreachable");
27516
29065
  readEnd(stream);
@@ -27702,10 +29251,10 @@ function isBlankStart(char) {
27702
29251
  function isBlank(char) {
27703
29252
  return isBlankStart(char) || char === "\v";
27704
29253
  }
27705
- function isWordStart(char) {
29254
+ function isWordStart2(char) {
27706
29255
  return /[A-Za-z_]/.test(char) || char.charCodeAt(0) >= 128;
27707
29256
  }
27708
- function isWordPart(char) {
29257
+ function isWordPart2(char) {
27709
29258
  return /[A-Za-z0-9_$]/.test(char) || char.charCodeAt(0) >= 128;
27710
29259
  }
27711
29260
  function skipBlanks(text2, from) {
@@ -27759,9 +29308,9 @@ function headTokens(text2) {
27759
29308
  index = close + 1;
27760
29309
  } else if ((char === "x" || char === "X") && text2.charAt(index + 1) === "'") {
27761
29310
  break;
27762
- } else if (isWordStart(char)) {
29311
+ } else if (isWordStart2(char)) {
27763
29312
  let end = index + 1;
27764
- while (end < text2.length && isWordPart(text2.charAt(end))) end++;
29313
+ while (end < text2.length && isWordPart2(text2.charAt(end))) end++;
27765
29314
  tokens.push({ kind: "word", text: text2.slice(index, end) });
27766
29315
  index = end;
27767
29316
  } else if (char === "." && !/[0-9]/.test(text2.charAt(index + 1))) {
@@ -27777,6 +29326,10 @@ var NO_NAME = {
27777
29326
  kind: "unreadable",
27778
29327
  reason: "the table name could not be read"
27779
29328
  };
29329
+ var NO_COLUMN = {
29330
+ kind: "unreadable",
29331
+ reason: "the column name could not be read"
29332
+ };
27780
29333
  var OTHER_SCHEMA = {
27781
29334
  kind: "unreadable",
27782
29335
  reason: "a table in another schema"
@@ -27802,8 +29355,9 @@ var Head = class {
27802
29355
  return token.kind === "word" || token.kind === "name" ? token.text : void 0;
27803
29356
  }
27804
29357
  /**
27805
- * SQLite's `fullname`. `main.` is the database a migration writes and is
27806
- * 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
27807
29361
  * matches it) is a table that goes with the connection; `public.` is
27808
29362
  * what a Postgres migration writes, said in its own words; any other
27809
29363
  * schema is one D1 does not have.
@@ -27812,17 +29366,21 @@ var Head = class {
27812
29366
  const first = this.nameAt(at);
27813
29367
  if (first === void 0) return { ok: false, head: NO_NAME };
27814
29368
  if (this.token(at + 1).kind !== "dot")
27815
- return { ok: true, name: first, next: at + 1 };
29369
+ return { ok: true, name: first, next: at + 1, main: false };
27816
29370
  const second = this.nameAt(at + 2);
27817
29371
  if (second === void 0) return { ok: false, head: NO_NAME };
27818
29372
  const schema = asciiLower(first);
27819
- 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 };
27820
29375
  let head = OTHER_SCHEMA;
27821
29376
  if (schema === "temp") head = OTHER;
27822
29377
  else if (schema === "public") head = PUBLIC_SCHEMA;
27823
29378
  return { ok: false, head, next: at + 3 };
27824
29379
  }
27825
29380
  };
29381
+ function inMain(read) {
29382
+ return read.main ? { main: true } : {};
29383
+ }
27826
29384
  function readCreate2(head) {
27827
29385
  if (head.is(1, "policy")) return { kind: "create-policy" };
27828
29386
  if (head.is(1, "temp") || head.is(1, "temporary")) return OTHER;
@@ -27846,9 +29404,24 @@ function readDrop2(head) {
27846
29404
  at += 2;
27847
29405
  }
27848
29406
  const table = head.table(at);
27849
- 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;
27850
29408
  }
27851
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
+ }
27852
29425
  function readAlter(head) {
27853
29426
  if (head.is(1, "policy")) return { kind: "alter-policy" };
27854
29427
  if (!head.is(1, "table")) return OTHER;
@@ -27858,9 +29431,18 @@ function readAlter(head) {
27858
29431
  if (head.is(action, "rename") && head.is(action + 1, "to")) {
27859
29432
  if (!sqlite.ok) return sqlite.head;
27860
29433
  const to = head.table(action + 2);
27861
- 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);
27862
29444
  }
27863
- if (head.is(action, "rename") || head.is(action, "add") || head.is(action, "drop"))
29445
+ if (head.is(action, "add") || head.is(action, "drop"))
27864
29446
  return sqlite.ok ? { kind: "alter-table", table: sqlite.name } : sqlite.head;
27865
29447
  }
27866
29448
  let at = 2;
@@ -27885,6 +29467,130 @@ function readSqlHead(text2) {
27885
29467
  return OTHER;
27886
29468
  }
27887
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
+
27888
29594
  // ../../packages/data-core/src/policy-migration.ts
27889
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).";
27890
29596
  function disableRefusal(table) {
@@ -27902,6 +29608,7 @@ function tablesChanged(head) {
27902
29608
  case "create-table":
27903
29609
  case "drop-table":
27904
29610
  case "alter-table":
29611
+ case "rename-column":
27905
29612
  return [asciiLower(head.table)];
27906
29613
  case "rename-table":
27907
29614
  return [asciiLower(head.table), asciiLower(head.to)];
@@ -27929,18 +29636,28 @@ function tablesInScope(policy) {
27929
29636
  }
27930
29637
  return tables;
27931
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
+ }
27932
29648
  function planPolicyMigration(statements) {
27933
29649
  const count2 = statements.length;
27934
29650
  const planned = [];
27935
29651
  let hasPolicyStatements = false;
27936
29652
  let touchesTables = false;
29653
+ let renamesColumns = false;
27937
29654
  const heads = statements.map((text2) => readSqlHead(text2));
27938
29655
  const changes = heads.map(tablesChanged);
27939
29656
  const changedAfter = (index, tables) => changes.slice(index + 1).some(
27940
29657
  (changed) => changed === "any" || changed.some((table) => tables.has(table))
27941
29658
  );
27942
29659
  for (const [index, text2] of statements.entries()) {
27943
- const refuse = (message) => ({
29660
+ const refuse2 = (message) => ({
27944
29661
  ok: false,
27945
29662
  index,
27946
29663
  message: `Statement ${String(index + 1)} of ${String(count2)}: ${message}`
@@ -27949,15 +29666,19 @@ function planPolicyMigration(statements) {
27949
29666
  switch (head.kind) {
27950
29667
  case "grant":
27951
29668
  case "revoke":
27952
- return refuse(GRANT_REFUSAL);
29669
+ return refuse2(GRANT_REFUSAL);
27953
29670
  case "unreadable":
27954
- return refuse(`${head.reason}.`);
29671
+ return refuse2(`${head.reason}.`);
27955
29672
  case "create-table":
27956
29673
  case "drop-table":
27957
29674
  case "rename-table":
27958
29675
  touchesTables = true;
27959
29676
  planned.push({ kind: "verbatim", text: text2, head });
27960
29677
  continue;
29678
+ case "rename-column":
29679
+ renamesColumns = true;
29680
+ planned.push({ kind: "verbatim", text: text2, head });
29681
+ continue;
27961
29682
  case "alter-table":
27962
29683
  case "other":
27963
29684
  planned.push({ kind: "verbatim", text: text2, head });
@@ -27967,17 +29688,17 @@ function planPolicyMigration(statements) {
27967
29688
  }
27968
29689
  const parsed = parseMigrationPolicyStatement(text2);
27969
29690
  if (!parsed.ok)
27970
- return refuse(
29691
+ return refuse2(
27971
29692
  `${parsed.reason} at character ${String(parsed.position)}.`
27972
29693
  );
27973
29694
  const statement2 = parsed.statement;
27974
29695
  const table = statement2.kind === "create" ? statement2.policy.table : statement2.table;
27975
29696
  if (!isStorageObjects(table) && isSystemTable(table))
27976
- return refuse(`${relationMissing(table)}.`);
29697
+ return refuse2(`${relationMissing(table)}.`);
27977
29698
  switch (statement2.kind) {
27978
29699
  case "create":
27979
29700
  if (readsBareSubqueryColumn(statement2.policy) && changedAfter(index, tablesInScope(statement2.policy)))
27980
- return refuse(LATER_TABLE_CHANGE);
29701
+ return refuse2(LATER_TABLE_CHANGE);
27981
29702
  hasPolicyStatements = true;
27982
29703
  planned.push({ kind: "create-policy", policy: statement2.policy });
27983
29704
  break;
@@ -28005,14 +29726,19 @@ function planPolicyMigration(statements) {
28005
29726
  break;
28006
29727
  case "rls":
28007
29728
  if (statement2.action === "disable")
28008
- return refuse(disableRefusal(table));
29729
+ return refuse2(disableRefusal(table));
28009
29730
  planned.push({ kind: "noop" });
28010
29731
  break;
28011
29732
  }
28012
29733
  }
28013
29734
  return {
28014
29735
  ok: true,
28015
- plan: { statements: planned, hasPolicyStatements, touchesTables }
29736
+ plan: {
29737
+ statements: planned,
29738
+ hasPolicyStatements,
29739
+ touchesTables,
29740
+ renamesColumns
29741
+ }
28016
29742
  };
28017
29743
  }
28018
29744
  function policyMigrationSql(policy) {
@@ -28024,8 +29750,10 @@ COMMENT ON POLICY ${quotePolicyName(policy.name)} ON ${policyTableSql(policy.tab
28024
29750
  function unreadablePolicyComment(policy) {
28025
29751
  return `-- Policy ${commentText(quotePolicyName(policy.name))} on ${commentText(policyTableSql(policy.table))} cannot be read (${commentText(policy.invalid)}); the requests it applies to are refused until it is dropped and created again.`;
28026
29752
  }
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`;
28027
29754
  var SYSTEM_PREDICATE = systemTablePredicate("name");
28028
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})`;
28029
29757
 
28030
29758
  // ../../packages/data-core/src/realtime-limits.ts
28031
29759
  var HOBBY_REALTIME_LIMITS = Object.freeze({
@@ -28114,107 +29842,6 @@ var MULTIPART_COMPLETE_LOCK_MS = 15 * 60 * 1e3;
28114
29842
  var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
28115
29843
  var UPLOAD_ID = new RegExp(`^(${UUID})\\.(${UUID})$`);
28116
29844
 
28117
- // ../../packages/data-core/src/system-sql.ts
28118
- async function convertLegacyPolicies(db, found) {
28119
- const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
28120
- if (rows.length === 0) return [];
28121
- const read = rows.map((row) => ({
28122
- policy: policyFromRow(row),
28123
- asRead: legacyRowAsRead(row)
28124
- }));
28125
- const inForce = new Set(
28126
- legacyInForce(
28127
- read.flatMap(({ policy }) => policy ? [policy] : []),
28128
- await readTableNames(db)
28129
- )
28130
- );
28131
- const statements = [];
28132
- for (const { policy, asRead } of read) {
28133
- if (!policy || !asRead) continue;
28134
- if (inForce.has(policy)) {
28135
- for (const named of namedFromLegacy(policy))
28136
- statements.push({
28137
- sql: POLICY_SQL.insertConverted,
28138
- params: [...policyValues(named), ...asRead]
28139
- });
28140
- found.unreadable.push(...unreadableConversions(policy));
28141
- }
28142
- statements.push({ sql: POLICY_SQL.markConverted, params: asRead });
28143
- }
28144
- return statements;
28145
- }
28146
- 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')))";
28147
- var SYSTEM_MIGRATIONS = [
28148
- {
28149
- version: 1,
28150
- name: "system_tables",
28151
- statements: [
28152
- MIGRATIONS_TABLE,
28153
- "CREATE TABLE IF NOT EXISTS _naro_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')))",
28154
- // Spec §11.2, as written there.
28155
- "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')))"
28156
- ]
28157
- },
28158
- {
28159
- // Spec P2-2 §3.1: an action whose rule column says 'condition' reads its
28160
- // condition from these. A worker from before them reads 'condition' as
28161
- // an unknown rule — none.
28162
- version: 2,
28163
- name: "policy_conditions",
28164
- statements: [],
28165
- addColumns: ["select", "insert", "update", "delete"].map((action) => ({
28166
- table: "_naro_policies",
28167
- column: `${action}_condition`,
28168
- definition: "TEXT"
28169
- }))
28170
- },
28171
- {
28172
- // Spec P2-6 §11: the Naro worker's sign-in handoff. A provider sign-in's
28173
- // flow (state → the app's redirect and the browser's PKCE challenge), the
28174
- // one-time codes a redirect hands the app, the sends the per-address
28175
- // email limit counts, and (adversarial review I3) the unconfirmed
28176
- // accounts someone else also tried to sign up as. Times are epoch
28177
- // milliseconds.
28178
- version: 3,
28179
- name: "auth_handoff",
28180
- statements: [
28181
- "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)",
28182
- "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)",
28183
- "CREATE TABLE IF NOT EXISTS _naro_auth_email_log (address_hash TEXT NOT NULL, sent_at INTEGER NOT NULL)",
28184
- "CREATE INDEX IF NOT EXISTS _naro_auth_email_log_address ON _naro_auth_email_log (address_hash, sent_at)",
28185
- "CREATE TABLE IF NOT EXISTS _naro_auth_contested (user_id TEXT PRIMARY KEY, contested_at INTEGER NOT NULL)"
28186
- ]
28187
- },
28188
- {
28189
- // P2-7 §9: named policies, many per table. The old rows are converted
28190
- // in the same batch and their rules set to a word old workers read as
28191
- // `none`.
28192
- version: 4,
28193
- name: "named_policies",
28194
- statements: [NAMED_POLICY_TABLE_SQL],
28195
- compute: convertLegacyPolicies
28196
- },
28197
- {
28198
- // Storage (first-release design §6): the buckets and the objects' index
28199
- // with their counter triggers. Every backend gets them, Storage on or
28200
- // not — what turns it on is the worker's R2 binding. A worker from
28201
- // before them never reads them.
28202
- version: 5,
28203
- name: "storage",
28204
- statements: [...STORAGE_TABLES_SQL]
28205
- },
28206
- {
28207
- // Storage 3-1b (known-issues #107, first-release design §14): the mark
28208
- // a bucket's removal sets before it empties the bucket. The claims and
28209
- // the worker's bucket reads take a marked bucket as not there. A worker
28210
- // from before it never reads the column.
28211
- version: 6,
28212
- name: "storage_bucket_deleting",
28213
- statements: [],
28214
- addColumns: [STORAGE_BUCKET_DELETING_COLUMN]
28215
- }
28216
- ];
28217
-
28218
29845
  // ../../packages/data-core/src/write-quota.ts
28219
29846
  var WRITE_QUOTA_KEY = "write_quota";
28220
29847
  var WRITE_QUOTA_SQL = {
@@ -28460,12 +30087,12 @@ import {
28460
30087
  rmSync as rmSync2,
28461
30088
  writeFileSync as writeFileSync2
28462
30089
  } from "fs";
28463
- import { join as join22 } from "path";
30090
+ import { join as join23 } from "path";
28464
30091
  function linkDir(dir) {
28465
- return join22(dir, ".naro");
30092
+ return join23(dir, ".naro");
28466
30093
  }
28467
30094
  function linkFile(dir) {
28468
- return join22(linkDir(dir), "project.json");
30095
+ return join23(linkDir(dir), "project.json");
28469
30096
  }
28470
30097
  function readProjectLink(dir) {
28471
30098
  try {
@@ -28487,7 +30114,7 @@ function writeProjectLink(dir, link) {
28487
30114
  writeFileSync2(linkFile(dir), `${JSON.stringify(link, null, 2)}
28488
30115
  `);
28489
30116
  writeFileSync2(
28490
- join22(linkDir(dir), "README.txt"),
30117
+ join23(linkDir(dir), "README.txt"),
28491
30118
  "This directory links the project to Naro and should not be committed.\n"
28492
30119
  );
28493
30120
  }
@@ -28497,9 +30124,9 @@ function removeProjectLink(dir) {
28497
30124
  return true;
28498
30125
  }
28499
30126
  function ensureGitignoreHasNaro(dir) {
28500
- const gitignorePath = join22(dir, ".gitignore");
30127
+ const gitignorePath = join23(dir, ".gitignore");
28501
30128
  const hasGitignore = existsSync4(gitignorePath);
28502
- if (!hasGitignore && !existsSync4(join22(dir, ".git"))) return false;
30129
+ if (!hasGitignore && !existsSync4(join23(dir, ".git"))) return false;
28503
30130
  const current = hasGitignore ? readFileSync5(gitignorePath, "utf8") : "";
28504
30131
  const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".naro" || line === ".naro/" || line === "/.naro");
28505
30132
  if (covered) return false;
@@ -28952,15 +30579,15 @@ function policiesFromFiles(files) {
28952
30579
  policies = policies.filter((policy) => !sameTable(policy.table, table));
28953
30580
  };
28954
30581
  for (const [file2, sql] of files.entries()) {
28955
- const split = splitStatements(sql);
28956
- if (!split.ok)
30582
+ const split2 = splitStatements(sql);
30583
+ if (!split2.ok)
28957
30584
  return {
28958
30585
  policies,
28959
30586
  tables,
28960
- refused: { file: file2, reason: `sql could not be parsed: ${split.reason}.` }
30587
+ refused: { file: file2, reason: `sql could not be parsed: ${split2.reason}.` }
28961
30588
  };
28962
30589
  const planned = planPolicyMigration(
28963
- split.statements.map((statement2) => statement2.text)
30590
+ split2.statements.map((statement2) => statement2.text)
28964
30591
  );
28965
30592
  if (!planned.ok)
28966
30593
  return {
@@ -28968,9 +30595,20 @@ function policiesFromFiles(files) {
28968
30595
  tables,
28969
30596
  refused: { file: file2, reason: planned.message }
28970
30597
  };
28971
- for (const statement2 of planned.plan.statements) {
30598
+ for (const [index, statement2] of planned.plan.statements.entries()) {
28972
30599
  if (statement2.kind === "create-policy") {
28973
30600
  const { table, name } = statement2.policy;
30601
+ if (policies.some(
30602
+ (policy) => sameTable(policy.table, table) && policy.name === name
30603
+ ))
30604
+ return {
30605
+ policies,
30606
+ tables,
30607
+ refused: {
30608
+ file: file2,
30609
+ reason: `statement ${String(index + 1)} of ${String(planned.plan.statements.length)} failed and the migration was rolled back: policy "${name}" for table "${table}" already exists`
30610
+ }
30611
+ };
28974
30612
  policies.push({ table, name });
28975
30613
  } else if (statement2.kind === "drop-policy") {
28976
30614
  policies = policies.filter(
@@ -28988,6 +30626,10 @@ function policiesFromFiles(files) {
28988
30626
  if (!tables.has(asciiLower(head.to))) forget(head.to);
28989
30627
  tables.delete(asciiLower(head.table));
28990
30628
  tables.add(asciiLower(head.to));
30629
+ if (!sameTable(head.table, STORAGE_OBJECTS) && !sameTable(head.table, REALTIME_MESSAGES))
30630
+ policies = policies.map(
30631
+ (policy) => sameTable(policy.table, head.table) ? { ...policy, table: head.to } : policy
30632
+ );
28991
30633
  }
28992
30634
  }
28993
30635
  }
@@ -29013,6 +30655,31 @@ function policiesLostByReset(input) {
29013
30655
  withTables: gone.filter((policy) => !remade.has(asciiLower(policy.table)))
29014
30656
  };
29015
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
+ }
29016
30683
  function resetPoliciesLostMessage(lost) {
29017
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:`;
29018
30685
  }
@@ -29091,28 +30758,33 @@ function usageStatus(report, currentMonth, requests) {
29091
30758
 
29092
30759
  // src/env-consent.ts
29093
30760
  import { existsSync as existsSync6 } from "fs";
29094
- import { join as join23 } from "path";
30761
+ import { join as join24 } from "path";
29095
30762
  var DOTENV_FILES = [".env.local", ".env.production", ".env"];
29096
- var PUBLIC_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_"];
30763
+ var PUBLIC_PREFIX = "NEXT_PUBLIC_";
30764
+ function maskSecretValue(value) {
30765
+ return value.length <= 8 ? "****" : `${value.slice(0, 4)}****`;
30766
+ }
29097
30767
  function maskEnvValue(key, value) {
29098
- if (PUBLIC_PREFIXES.some((prefix) => key.startsWith(prefix)))
30768
+ if (key.startsWith(PUBLIC_PREFIX))
29099
30769
  return value.length > 60 ? `${value.slice(0, 60)}\u2026` : value;
29100
- return value.length <= 8 ? "****" : `${value.slice(0, 4)}****`;
30770
+ return maskSecretValue(value);
29101
30771
  }
29102
30772
  function findDotenvFile(dir) {
29103
- 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;
29104
30774
  return null;
29105
30775
  }
29106
- function envAddGuidance(keys) {
30776
+ function envAddGuidance(keys, environment) {
30777
+ const suffix = environment === void 0 ? "" : ` ${environment}`;
29107
30778
  return [
29108
30779
  " Set them yourself:",
29109
- ...keys.map((key) => ` npx naro-cli env add ${key}`),
30780
+ ...keys.map((key) => ` npx naro-cli env add ${key}${suffix}`),
29110
30781
  " (values come from a hidden prompt or stdin, never from argv)"
29111
30782
  ];
29112
30783
  }
29113
30784
  async function promptForMissingEnv(ctx, keys) {
29114
30785
  const guide = (remaining2) => {
29115
- for (const line of envAddGuidance(remaining2)) ctx.output.log(line);
30786
+ for (const line of envAddGuidance(remaining2, ctx.environment))
30787
+ ctx.output.log(line);
29116
30788
  };
29117
30789
  const file2 = findDotenvFile(ctx.cwd);
29118
30790
  if (!file2) {
@@ -29150,6 +30822,20 @@ async function promptForMissingEnv(ctx, keys) {
29150
30822
  return collected;
29151
30823
  }
29152
30824
 
30825
+ // src/env-surface.ts
30826
+ function pullEnv(rows, surface) {
30827
+ const chosen = /* @__PURE__ */ new Map();
30828
+ for (const row of rows) {
30829
+ if (row.target !== "all" && row.target !== surface) continue;
30830
+ const held = chosen.get(row.key);
30831
+ if (!held || held.target === "all" && row.target !== "all")
30832
+ chosen.set(row.key, row);
30833
+ }
30834
+ return Object.fromEntries(
30835
+ [...chosen.values()].map((row) => [row.key, row.value ?? ""])
30836
+ );
30837
+ }
30838
+
29153
30839
  // src/git-info.ts
29154
30840
  import { execFile as execFile2 } from "child_process";
29155
30841
  var SUPPORTED_REMOTE = [
@@ -29315,9 +31001,9 @@ import {
29315
31001
  cp as cp2,
29316
31002
  lstat,
29317
31003
  mkdir as mkdir3,
29318
- readFile as readFile13,
31004
+ readFile as readFile14,
29319
31005
  readlink,
29320
- realpath,
31006
+ realpath as realpath3,
29321
31007
  rm as rm4,
29322
31008
  stat as stat3,
29323
31009
  symlink,
@@ -29328,10 +31014,10 @@ import {
29328
31014
  basename as basename3,
29329
31015
  dirname as dirname5,
29330
31016
  isAbsolute as isAbsolute4,
29331
- join as join24,
31017
+ join as join25,
29332
31018
  relative as relative6,
29333
31019
  resolve as resolve10,
29334
- sep as sep4
31020
+ sep as sep5
29335
31021
  } from "path";
29336
31022
  var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo|\.naro)([\\/]|$)/;
29337
31023
  var ENV_FILE = /^\.env(\.(?!(?:example|sample|template)$)[^\\/]+)?$/i;
@@ -29369,7 +31055,7 @@ async function committedEnvFiles(sourceDir) {
29369
31055
  }
29370
31056
  async function staysInside(link, text2, root) {
29371
31057
  try {
29372
- return inside(await realpath(link), root);
31058
+ return inside(await realpath3(link), root);
29373
31059
  } catch (error51) {
29374
31060
  return error51.code === "ENOENT" && !text2.split(/[\\/]/).includes("..");
29375
31061
  }
@@ -29392,12 +31078,12 @@ async function keptOnly(items, keep, remove) {
29392
31078
  async function readLocal(path) {
29393
31079
  const found = await stat3(path).catch(() => null);
29394
31080
  if (!found?.isFile()) return null;
29395
- return readFile13(path).catch(() => null);
31081
+ return readFile14(path).catch(() => null);
29396
31082
  }
29397
31083
  async function realAncestor(dir) {
29398
31084
  for (let current = dir; ; current = dirname5(current)) {
29399
31085
  try {
29400
- return await realpath(current);
31086
+ return await realpath3(current);
29401
31087
  } catch {
29402
31088
  if (dirname5(current) === current)
29403
31089
  throw new Error(`No ancestor of ${dir}`);
@@ -29408,7 +31094,7 @@ function sameText(a, b) {
29408
31094
  return a.equals(b) || a.toString("utf8").replace(/\r\n/g, "\n") === b.toString("utf8").replace(/\r\n/g, "\n");
29409
31095
  }
29410
31096
  async function copyWorkingTree(source, checkout) {
29411
- const sourceDir = await realpath(source);
31097
+ const sourceDir = await realpath3(source);
29412
31098
  const inWorkingTree = [];
29413
31099
  const absoluteLinks = [];
29414
31100
  const linksOutside = [];
@@ -29427,12 +31113,12 @@ async function copyWorkingTree(source, checkout) {
29427
31113
  if (entry.isFIFO() || entry.isSocket() || entry.isCharacterDevice() || entry.isBlockDevice())
29428
31114
  return false;
29429
31115
  if (ENV_FILE.test(basename3(src)) && !entry.isDirectory()) {
29430
- inWorkingTree.push(relative6(sourceDir, src).split(sep4).join("/"));
31116
+ inWorkingTree.push(relative6(sourceDir, src).split(sep5).join("/"));
29431
31117
  return false;
29432
31118
  }
29433
31119
  if (entry.isSymbolicLink()) {
29434
31120
  const target2 = await readlink(src);
29435
- const path = relative6(sourceDir, src).split(sep4).join("/");
31121
+ const path = relative6(sourceDir, src).split(sep5).join("/");
29436
31122
  if (isAbsolute4(target2)) {
29437
31123
  absoluteLinks.push(path);
29438
31124
  return false;
@@ -29450,17 +31136,17 @@ async function copyWorkingTree(source, checkout) {
29450
31136
  return true;
29451
31137
  }
29452
31138
  });
29453
- const root = await realpath(checkout);
31139
+ const root = await realpath3(checkout);
29454
31140
  const strays = await keptOnly(
29455
31141
  keptLinks,
29456
31142
  async ({ path, text: text2 }) => {
29457
31143
  try {
29458
- return inside(await realpath(join24(checkout, path)), root);
31144
+ return inside(await realpath3(join25(checkout, path)), root);
29459
31145
  } catch (error51) {
29460
31146
  return error51.code === "ENOENT" && climbsOnlyFirst(text2);
29461
31147
  }
29462
31148
  },
29463
- ({ path }) => unlink(join24(checkout, path))
31149
+ ({ path }) => unlink(join25(checkout, path))
29464
31150
  );
29465
31151
  linksElsewhere.push(...strays.map(({ path }) => path));
29466
31152
  const committed = await committedEnvFiles(sourceDir);
@@ -29474,7 +31160,7 @@ async function copyWorkingTree(source, checkout) {
29474
31160
  outsideProject.push(path);
29475
31161
  continue;
29476
31162
  }
29477
- const target2 = join24(checkout, path);
31163
+ const target2 = join25(checkout, path);
29478
31164
  if (!inside(await realAncestor(dirname5(target2)), root)) {
29479
31165
  outsideProject.push(path);
29480
31166
  continue;
@@ -29499,16 +31185,16 @@ async function copyWorkingTree(source, checkout) {
29499
31185
  continue;
29500
31186
  }
29501
31187
  await writeFile7(target2, content, { flag: "wx" });
29502
- const local = await readLocal(join24(sourceDir, path));
31188
+ const local = await readLocal(join25(sourceDir, path));
29503
31189
  if (!local || !sameText(local, content)) committedVersion.push(path);
29504
31190
  }
29505
31191
  const envStrays = await keptOnly(
29506
31192
  envLinks,
29507
31193
  async (path) => {
29508
- const real = await realpath(join24(checkout, path)).catch(() => null);
31194
+ const real = await realpath3(join25(checkout, path)).catch(() => null);
29509
31195
  return real !== null && inside(real, root);
29510
31196
  },
29511
- (path) => unlink(join24(checkout, path))
31197
+ (path) => unlink(join25(checkout, path))
29512
31198
  );
29513
31199
  outsideProject.push(...envStrays);
29514
31200
  const inCommit = new Set(
@@ -29625,7 +31311,7 @@ function slugify2(value) {
29625
31311
  async function projectName(sourceDir) {
29626
31312
  try {
29627
31313
  const pkg = JSON.parse(
29628
- await readFile14(join25(sourceDir, "package.json"), "utf8")
31314
+ await readFile15(join26(sourceDir, "package.json"), "utf8")
29629
31315
  );
29630
31316
  if (pkg.name) return pkg.name;
29631
31317
  } catch {
@@ -29635,7 +31321,7 @@ async function projectName(sourceDir) {
29635
31321
  function resolveBin(name) {
29636
31322
  let dir = dirname6(fileURLToPath(import.meta.url));
29637
31323
  for (; ; ) {
29638
- const candidate = join25(dir, "node_modules", ".bin", name);
31324
+ const candidate = join26(dir, "node_modules", ".bin", name);
29639
31325
  if (existsSync7(candidate)) return candidate;
29640
31326
  const parent = dirname6(dir);
29641
31327
  if (parent === dir) return name;
@@ -29702,7 +31388,7 @@ function workingTreeNotices(copy) {
29702
31388
  }
29703
31389
  function localDependencies(sourceDir, say = () => void 0) {
29704
31390
  return {
29705
- createWorkspace: () => mkdtemp2(join25(tmpdir2(), "naro-build-")),
31391
+ createWorkspace: () => mkdtemp2(join26(tmpdir2(), "naro-build-")),
29706
31392
  removeWorkspace: (path) => rm5(path, { recursive: true, force: true }),
29707
31393
  async runCommand(spec, observer) {
29708
31394
  if (spec.command === "git" && spec.args[0] === "clone") {
@@ -29740,6 +31426,18 @@ function localDependencies(sourceDir, say = () => void 0) {
29740
31426
  async function deployDirectory(sourceDir, options, log2) {
29741
31427
  const cpUrl = controlPlaneUrl();
29742
31428
  const production = options.production ?? false;
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;
31435
+ if (environment === "production" || environment === "preview")
31436
+ throw new NaroError(
31437
+ "VALIDATION_FAILED",
31438
+ "Pass production: true for production; preview is the default."
31439
+ );
31440
+ const surface = production ? "production" : environment ?? "preview";
29743
31441
  const appRoot = options.rootDirectory ? resolve11(sourceDir, options.rootDirectory) : sourceDir;
29744
31442
  const framework = detectFramework2(appRoot);
29745
31443
  if (!framework.deployable && !options.force) {
@@ -29762,6 +31460,8 @@ async function deployDirectory(sourceDir, options, log2) {
29762
31460
  getAccessToken: () => auth.getAccessToken(),
29763
31461
  ...options.fetch ? { fetch: options.fetch } : {}
29764
31462
  });
31463
+ if (environment !== void 0 && options.projectId)
31464
+ await client.environments.get(options.projectId, environment);
29765
31465
  const name = slugify2(options.name ?? await projectName(appRoot));
29766
31466
  const git2 = await (options.gitInfo ?? detectGitInfo)(sourceDir);
29767
31467
  const branch = options.branch ?? git2.branch;
@@ -29779,7 +31479,7 @@ async function deployDirectory(sourceDir, options, log2) {
29779
31479
  if (options.projectId) {
29780
31480
  try {
29781
31481
  const rows = await client.env.list(options.projectId);
29782
- existing = new Set(rows.map((row) => row.key));
31482
+ existing = new Set(Object.keys(pullEnv(rows, surface)));
29783
31483
  } catch {
29784
31484
  }
29785
31485
  }
@@ -29791,8 +31491,10 @@ async function deployDirectory(sourceDir, options, log2) {
29791
31491
  log2(
29792
31492
  `\u25B8 ${String(missing.length)} env var(s) needed, not set on this project: ${missing.join(", ")}`
29793
31493
  );
29794
- if (options.onMissingEnv) collected = await options.onMissingEnv(missing);
29795
- else for (const line of envAddGuidance(missing)) log2(line);
31494
+ const canCollect = environment === void 0 || options.projectId;
31495
+ if (options.onMissingEnv && canCollect)
31496
+ collected = await options.onMissingEnv(missing);
31497
+ else for (const line of envAddGuidance(missing, environment)) log2(line);
29796
31498
  }
29797
31499
  const missingEnvKeys = missing.filter((key) => !(key in collected));
29798
31500
  const envToSend = { ...options.env, ...collected };
@@ -29801,6 +31503,9 @@ async function deployDirectory(sourceDir, options, log2) {
29801
31503
  ...options.projectId ? { projectId: options.projectId } : { name },
29802
31504
  ...options.teamSlug ? { teamSlug: options.teamSlug } : {},
29803
31505
  production,
31506
+ ...options.environment !== void 0 && {
31507
+ environment: options.environment
31508
+ },
29804
31509
  ...git2.repositoryUrl != null && { repositoryUrl: git2.repositoryUrl },
29805
31510
  ...branch != null && { branch },
29806
31511
  ...repositoryRoot != null && { rootDirectory: repositoryRoot },
@@ -29891,6 +31596,8 @@ async function deployDirectory(sourceDir, options, log2) {
29891
31596
  status: detail.deployment.status,
29892
31597
  url: url2,
29893
31598
  production,
31599
+ // A control plane from before named environments sends no environment.
31600
+ environment: ctx.environment ?? null,
29894
31601
  missingEnvKeys,
29895
31602
  teamSlug: ctx.teamSlug,
29896
31603
  projectSlug: ctx.projectSlug,
@@ -29908,9 +31615,10 @@ async function waitForFinish(client, ctx, log2, intervalMs) {
29908
31615
  }
29909
31616
  function deploymentUrl(detail, ctx, production) {
29910
31617
  if (detail.deployment.url) return detail.deployment.url;
29911
- const surface = ctx.subdomain ?? ctx.projectId;
31618
+ const surface = ctx.environment ? { kind: "environment", slug: ctx.environment, environmentId: null } : production ? PRODUCTION_SURFACE : PREVIEW_SURFACE;
31619
+ const worker = workerNameForSurface(ctx.subdomain ?? ctx.projectId, surface);
29912
31620
  const host = new URL(ctx.controlPlaneUrl || controlPlaneUrl()).hostname;
29913
- return `https://${surface}${production ? "" : "-preview"}.${host}`;
31621
+ return `https://${worker}.${host}`;
29914
31622
  }
29915
31623
 
29916
31624
  export {
@@ -29934,6 +31642,8 @@ export {
29934
31642
  suppliedToken,
29935
31643
  resolveAuth,
29936
31644
  openBrowser,
31645
+ isReservedEnvTarget,
31646
+ isEnvTargetName,
29937
31647
  SPEED_INSIGHTS_METRICS,
29938
31648
  SPEED_INSIGHTS_FILTER_KEYS,
29939
31649
  SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
@@ -29949,11 +31659,14 @@ export {
29949
31659
  cellText,
29950
31660
  renderRows,
29951
31661
  summarizeStatement,
31662
+ parseDotenvDetailed,
29952
31663
  serializeDotenv,
29953
31664
  MAX_SINGLE_UPLOAD_BYTES,
31665
+ checkObjectName,
29954
31666
  parseCacheControl,
29955
31667
  STORAGE_OBJECTS_TABLE,
29956
31668
  systemTablePredicate,
31669
+ dataError,
29957
31670
  SCHEMA_SQL,
29958
31671
  tableNames,
29959
31672
  snapshotFromRows,
@@ -29968,6 +31681,7 @@ export {
29968
31681
  indexesFromRows,
29969
31682
  parsePolicyStatement,
29970
31683
  splitPolicyStatements,
31684
+ STORAGE_SUBQUERIES_SQL,
29971
31685
  planPolicyMigration,
29972
31686
  unreadablePolicyComment,
29973
31687
  parseRedirectEntry,
@@ -29990,13 +31704,17 @@ export {
29990
31704
  RESET_BACKEND_OUTDATED,
29991
31705
  buildResetPlan,
29992
31706
  policiesLostByReset,
31707
+ storageRefusalOfFiles,
31708
+ resetStorageOutdatedMessage,
29993
31709
  resetPoliciesLostMessage,
29994
31710
  resetPoliciesWithTablesMessage,
29995
31711
  resetFileRefusedMessage,
29996
31712
  storedPolicySql,
29997
31713
  usageMonthOf,
29998
31714
  summarizeTeamUsage,
31715
+ maskEnvValue,
29999
31716
  promptForMissingEnv,
31717
+ pullEnv,
30000
31718
  detectGitInfo,
30001
31719
  parseDotenv2 as parseDotenv,
30002
31720
  readProjectEnv2 as readProjectEnv,