naro-cli 0.8.0 → 0.9.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.
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
- var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
4
- for (var name in all)
5
- __defProp(target, name, { get: all[name], enumerable: true });
6
- };
2
+ import {
3
+ __export
4
+ } from "./chunk-K3NQKI34.js";
7
5
 
8
6
  // src/deploy.ts
9
7
  import { existsSync as existsSync7 } from "fs";
@@ -284,6 +282,33 @@ function pathnameOf(url2) {
284
282
  // ../../packages/core/src/internal-auth.ts
285
283
  import { Buffer as Buffer2 } from "buffer";
286
284
 
285
+ // ../../packages/core/src/migration-names.ts
286
+ function normalizeMigrationName(name) {
287
+ return name.trim().toLowerCase().replace(/\.sql$/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
288
+ }
289
+ function leadingMigrationNumber(name) {
290
+ const match = /^(\d+)/.exec(name);
291
+ return match ? BigInt(match[1] ?? "0") : null;
292
+ }
293
+ function highestMigrationNumber(names) {
294
+ let max = 0n;
295
+ for (const name of names) {
296
+ const number4 = leadingMigrationNumber(name);
297
+ if (number4 !== null && number4 > max) max = number4;
298
+ }
299
+ return max;
300
+ }
301
+ function numberedMigrationName(existing, title) {
302
+ const number4 = (highestMigrationNumber(existing) + 1n).toString().padStart(4, "0");
303
+ return `${number4}_${title}.sql`;
304
+ }
305
+ var MAX_SCHEMA_TITLE_LENGTH = 180;
306
+ function schemaMigrationTitle(operation) {
307
+ const op = typeof operation.op === "string" ? operation.op.replace(/[A-Z]/g, (letter) => `_${letter}`) : "";
308
+ const target = typeof operation.table === "string" ? operation.table : typeof operation.name === "string" ? operation.name : "";
309
+ return normalizeMigrationName(`schema ${op} ${target}`).slice(0, MAX_SCHEMA_TITLE_LENGTH).replace(/_+$/, "");
310
+ }
311
+
287
312
  // ../../packages/core/src/paths.ts
288
313
  function projectPath(teamSlug, projectSlug) {
289
314
  return `/${teamSlug}/projects/${projectSlug}`;
@@ -2474,9 +2499,9 @@ var $ZodCheckEndsWith = /* @__PURE__ */ $constructor("$ZodCheckEndsWith", (inst,
2474
2499
  });
2475
2500
  };
2476
2501
  });
2477
- function handleCheckPropertyResult(result, payload, property2) {
2502
+ function handleCheckPropertyResult(result, payload, property) {
2478
2503
  if (result.issues.length) {
2479
- payload.issues.push(...prefixIssues(property2, result.issues));
2504
+ payload.issues.push(...prefixIssues(property, result.issues));
2480
2505
  }
2481
2506
  }
2482
2507
  var $ZodCheckProperty = /* @__PURE__ */ $constructor("$ZodCheckProperty", (inst, def) => {
@@ -11303,10 +11328,10 @@ function _endsWith(suffix, params) {
11303
11328
  });
11304
11329
  }
11305
11330
  // @__NO_SIDE_EFFECTS__
11306
- function _property(property2, schema, params) {
11331
+ function _property(property, schema, params) {
11307
11332
  return new $ZodCheckProperty({
11308
11333
  check: "property",
11309
- property: property2,
11334
+ property,
11310
11335
  schema,
11311
11336
  ...normalizeParams(params)
11312
11337
  });
@@ -11403,28 +11428,28 @@ function _tuple(Class2, items, _paramsOrRest, _params) {
11403
11428
  });
11404
11429
  }
11405
11430
  // @__NO_SIDE_EFFECTS__
11406
- function _record(Class2, keyType, valueType2, params) {
11431
+ function _record(Class2, keyType, valueType, params) {
11407
11432
  return new Class2({
11408
11433
  type: "record",
11409
11434
  keyType,
11410
- valueType: valueType2,
11435
+ valueType,
11411
11436
  ...normalizeParams(params)
11412
11437
  });
11413
11438
  }
11414
11439
  // @__NO_SIDE_EFFECTS__
11415
- function _map(Class2, keyType, valueType2, params) {
11440
+ function _map(Class2, keyType, valueType, params) {
11416
11441
  return new Class2({
11417
11442
  type: "map",
11418
11443
  keyType,
11419
- valueType: valueType2,
11444
+ valueType,
11420
11445
  ...normalizeParams(params)
11421
11446
  });
11422
11447
  }
11423
11448
  // @__NO_SIDE_EFFECTS__
11424
- function _set(Class2, valueType2, params) {
11449
+ function _set(Class2, valueType, params) {
11425
11450
  return new Class2({
11426
11451
  type: "set",
11427
- valueType: valueType2,
11452
+ valueType,
11428
11453
  ...normalizeParams(params)
11429
11454
  });
11430
11455
  }
@@ -13790,37 +13815,37 @@ var ZodRecord = /* @__PURE__ */ $constructor("ZodRecord", (inst, def) => {
13790
13815
  inst.keyType = def.keyType;
13791
13816
  inst.valueType = def.valueType;
13792
13817
  });
13793
- function record(keyType, valueType2, params) {
13794
- if (!valueType2 || !valueType2._zod) {
13818
+ function record(keyType, valueType, params) {
13819
+ if (!valueType || !valueType._zod) {
13795
13820
  return new ZodRecord({
13796
13821
  type: "record",
13797
13822
  keyType: string2(),
13798
13823
  valueType: keyType,
13799
- ...util_exports.normalizeParams(valueType2)
13824
+ ...util_exports.normalizeParams(valueType)
13800
13825
  });
13801
13826
  }
13802
13827
  return new ZodRecord({
13803
13828
  type: "record",
13804
13829
  keyType,
13805
- valueType: valueType2,
13830
+ valueType,
13806
13831
  ...util_exports.normalizeParams(params)
13807
13832
  });
13808
13833
  }
13809
- function partialRecord(keyType, valueType2, params) {
13834
+ function partialRecord(keyType, valueType, params) {
13810
13835
  const k = clone(keyType);
13811
13836
  k._zod.values = void 0;
13812
13837
  return new ZodRecord({
13813
13838
  type: "record",
13814
13839
  keyType: k,
13815
- valueType: valueType2,
13840
+ valueType,
13816
13841
  ...util_exports.normalizeParams(params)
13817
13842
  });
13818
13843
  }
13819
- function looseRecord(keyType, valueType2, params) {
13844
+ function looseRecord(keyType, valueType, params) {
13820
13845
  return new ZodRecord({
13821
13846
  type: "record",
13822
13847
  keyType,
13823
- valueType: valueType2,
13848
+ valueType,
13824
13849
  mode: "loose",
13825
13850
  ...util_exports.normalizeParams(params)
13826
13851
  });
@@ -13836,11 +13861,11 @@ var ZodMap = /* @__PURE__ */ $constructor("ZodMap", (inst, def) => {
13836
13861
  inst.max = (...args) => inst.check(_maxSize(...args));
13837
13862
  inst.size = (...args) => inst.check(_size(...args));
13838
13863
  });
13839
- function map(keyType, valueType2, params) {
13864
+ function map(keyType, valueType, params) {
13840
13865
  return new ZodMap({
13841
13866
  type: "map",
13842
13867
  keyType,
13843
- valueType: valueType2,
13868
+ valueType,
13844
13869
  ...util_exports.normalizeParams(params)
13845
13870
  });
13846
13871
  }
@@ -13853,10 +13878,10 @@ var ZodSet = /* @__PURE__ */ $constructor("ZodSet", (inst, def) => {
13853
13878
  inst.max = (...args) => inst.check(_maxSize(...args));
13854
13879
  inst.size = (...args) => inst.check(_size(...args));
13855
13880
  });
13856
- function set(valueType2, params) {
13881
+ function set(valueType, params) {
13857
13882
  return new ZodSet({
13858
13883
  type: "set",
13859
- valueType: valueType2,
13884
+ valueType,
13860
13885
  ...util_exports.normalizeParams(params)
13861
13886
  });
13862
13887
  }
@@ -14828,12 +14853,15 @@ var RESERVED_SUBDOMAINS = [
14828
14853
  "support",
14829
14854
  "www"
14830
14855
  ];
14856
+ var BACKEND_WORKER_NAME_PREFIX = "naro-backend-";
14831
14857
  var SUBDOMAIN_PATTERN = /^[a-z0-9](-?[a-z0-9])*$/;
14832
14858
  var SUBDOMAIN_MAX_LENGTH = 54;
14833
14859
  function subdomainProblem(value) {
14834
14860
  if (value.length < 1 || value.length > SUBDOMAIN_MAX_LENGTH) return "invalid";
14835
14861
  if (!SUBDOMAIN_PATTERN.test(value)) return "invalid";
14836
14862
  if (value.endsWith("-preview")) return "reserved";
14863
+ if (value === BACKEND_WORKER_NAME_PREFIX.slice(0, -1) || value.startsWith(BACKEND_WORKER_NAME_PREFIX))
14864
+ return "reserved";
14837
14865
  if (RESERVED_SUBDOMAINS.includes(value))
14838
14866
  return "reserved";
14839
14867
  return null;
@@ -15663,6 +15691,67 @@ var FRAMEWORK_PRESETS = {
15663
15691
  spaFallback: false,
15664
15692
  cachePath: ".next/cache"
15665
15693
  },
15694
+ // The presets below build a static site. Each framework can also render on
15695
+ // a server; detectFramework refuses those setups by name instead of
15696
+ // shipping half an app (see SERVER_ONLY and staticBuildProblem).
15697
+ astro: {
15698
+ id: "astro",
15699
+ detectDependency: "astro",
15700
+ defaultBuildScript: "build",
15701
+ defaultOutputDirectory: "dist",
15702
+ needsServerRuntime: false,
15703
+ spaFallback: false,
15704
+ cachePath: "node_modules/.astro"
15705
+ },
15706
+ sveltekit: {
15707
+ id: "sveltekit",
15708
+ detectDependency: "@sveltejs/kit",
15709
+ defaultBuildScript: "build",
15710
+ // adapter-static's default `pages` directory.
15711
+ defaultOutputDirectory: "build",
15712
+ needsServerRuntime: false,
15713
+ spaFallback: false,
15714
+ cachePath: null
15715
+ },
15716
+ nuxt: {
15717
+ id: "nuxt",
15718
+ detectDependency: "nuxt",
15719
+ // `nuxt generate` prerenders every route; `nuxt build` would need Nitro.
15720
+ defaultBuildScript: "generate",
15721
+ defaultOutputDirectory: ".output/public",
15722
+ needsServerRuntime: false,
15723
+ spaFallback: false,
15724
+ cachePath: "node_modules/.cache"
15725
+ },
15726
+ angular: {
15727
+ id: "angular",
15728
+ detectDependency: "@angular/core",
15729
+ defaultBuildScript: "build",
15730
+ // Documentation only: the real path is read from angular.json by
15731
+ // angularOutput, which never falls back to this string.
15732
+ defaultOutputDirectory: "dist/<project>/browser",
15733
+ needsServerRuntime: false,
15734
+ spaFallback: true,
15735
+ cachePath: ".angular/cache"
15736
+ },
15737
+ docusaurus: {
15738
+ id: "docusaurus",
15739
+ detectDependency: "@docusaurus/core",
15740
+ defaultBuildScript: "build",
15741
+ defaultOutputDirectory: "build",
15742
+ needsServerRuntime: false,
15743
+ spaFallback: false,
15744
+ cachePath: "node_modules/.cache"
15745
+ },
15746
+ gatsby: {
15747
+ id: "gatsby",
15748
+ detectDependency: "gatsby",
15749
+ defaultBuildScript: "build",
15750
+ defaultOutputDirectory: "public",
15751
+ needsServerRuntime: false,
15752
+ spaFallback: false,
15753
+ cachePath: ".cache"
15754
+ },
15666
15755
  vite: {
15667
15756
  id: "vite",
15668
15757
  detectDependency: "vite",
@@ -15691,10 +15780,36 @@ var FRAMEWORK_PRESETS = {
15691
15780
  cachePath: null
15692
15781
  }
15693
15782
  };
15694
- var DETECTION_ORDER = ["next", "vite", "cra"];
15783
+ var DETECTION_ORDER = [
15784
+ "next",
15785
+ "nuxt",
15786
+ "astro",
15787
+ "sveltekit",
15788
+ "angular",
15789
+ "docusaurus",
15790
+ "gatsby",
15791
+ "vite",
15792
+ "cra"
15793
+ ];
15794
+ var SERVER_ONLY = [
15795
+ { dependency: "@tanstack/react-start", name: "TanStack Start" },
15796
+ { dependency: "@tanstack/solid-start", name: "TanStack Start" },
15797
+ { dependency: "@remix-run/dev", name: "Remix" },
15798
+ { dependency: "@react-router/dev", name: "React Router (framework mode)" },
15799
+ { dependency: "@solidjs/start", name: "SolidStart" },
15800
+ { dependency: "@builder.io/qwik-city", name: "Qwik City" },
15801
+ { dependency: "@analogjs/platform", name: "Analog" }
15802
+ ];
15803
+ var SERVER_RUNTIME_HINT = "Naro deploys Next.js, static sites and single-page apps; server rendering for other frameworks is not supported yet.";
15695
15804
  var FRAMEWORK_LABELS = {
15696
15805
  next: "Next.js",
15697
15806
  "next-export": "Next.js static export",
15807
+ astro: "Astro",
15808
+ sveltekit: "SvelteKit",
15809
+ nuxt: "Nuxt",
15810
+ angular: "Angular",
15811
+ docusaurus: "Docusaurus",
15812
+ gatsby: "Gatsby",
15698
15813
  vite: "Vite",
15699
15814
  cra: "Create React App",
15700
15815
  static: "static files"
@@ -15705,8 +15820,7 @@ function dependencyVersion(packageJson, name) {
15705
15820
  }
15706
15821
  var STATIC_EXPORT = /(?:^|[\s,{;])output\s*:\s*(["'`])export\1/;
15707
15822
  function declaresStaticExport(source) {
15708
- const code = source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/.*$/gm, "$1");
15709
- return STATIC_EXPORT.test(code);
15823
+ return STATIC_EXPORT.test(stripComments(source));
15710
15824
  }
15711
15825
  async function usesStaticExport(root) {
15712
15826
  for (const name of NEXT_CONFIG_FILES) {
@@ -15718,6 +15832,102 @@ async function usesStaticExport(root) {
15718
15832
  }
15719
15833
  return false;
15720
15834
  }
15835
+ var ASTRO_CONFIG_FILES = [
15836
+ "astro.config.mjs",
15837
+ "astro.config.js",
15838
+ "astro.config.ts",
15839
+ "astro.config.mts",
15840
+ "astro.config.cjs"
15841
+ ];
15842
+ var SVELTE_CONFIG_FILES = [
15843
+ "svelte.config.js",
15844
+ "svelte.config.mjs",
15845
+ "svelte.config.ts"
15846
+ ];
15847
+ var VITE_CONFIG_FILES = [
15848
+ "vite.config.ts",
15849
+ "vite.config.js",
15850
+ "vite.config.mjs",
15851
+ "vite.config.mts"
15852
+ ];
15853
+ var ASTRO_ADAPTER = /^@astrojs\/(cloudflare|node|vercel|netlify|deno)$/;
15854
+ async function readFirst(root, names) {
15855
+ for (const name of names) {
15856
+ try {
15857
+ return await readFile6(join7(root, name), "utf8");
15858
+ } catch {
15859
+ continue;
15860
+ }
15861
+ }
15862
+ return null;
15863
+ }
15864
+ function stripComments(source) {
15865
+ return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/.*$/gm, "$1");
15866
+ }
15867
+ async function staticBuildProblem(root, id, packageJson) {
15868
+ const has = (name) => dependencyVersion(packageJson, name) !== null;
15869
+ const dependencies = [
15870
+ ...Object.keys(packageJson.dependencies ?? {}),
15871
+ ...Object.keys(packageJson.devDependencies ?? {})
15872
+ ];
15873
+ switch (id) {
15874
+ case "sveltekit": {
15875
+ const configs = await Promise.all(
15876
+ [...SVELTE_CONFIG_FILES, ...VITE_CONFIG_FILES].map(
15877
+ (name) => readFirst(root, [name])
15878
+ )
15879
+ );
15880
+ const usesStatic = configs.some(
15881
+ (source) => source !== null && stripComments(source).includes("@sveltejs/adapter-static")
15882
+ );
15883
+ return usesStatic ? null : "SvelteKit deploys on Naro with @sveltejs/adapter-static only (install it, use it as the adapter in svelte.config.js or vite.config.ts, and prerender your pages).";
15884
+ }
15885
+ case "astro": {
15886
+ const adapter = dependencies.find((name) => ASTRO_ADAPTER.test(name));
15887
+ if (adapter)
15888
+ return `This Astro project uses the ${adapter} adapter, which renders on a server. Remove it to deploy the static build.`;
15889
+ const config2 = await readFirst(root, ASTRO_CONFIG_FILES);
15890
+ if (config2 && /(?:^|[\s,{;])output\s*:\s*(["'`])server\1/.test(stripComments(config2)))
15891
+ return 'This Astro project sets output: "server". Use the default static output to deploy on Naro.';
15892
+ return null;
15893
+ }
15894
+ case "angular":
15895
+ return has("@angular/ssr") ? "This Angular project uses @angular/ssr (server rendering). Remove it to deploy the browser build." : null;
15896
+ default:
15897
+ return null;
15898
+ }
15899
+ }
15900
+ async function angularOutput(root) {
15901
+ const source = await readFirst(root, ["angular.json"]);
15902
+ let workspace = null;
15903
+ try {
15904
+ workspace = source ? JSON.parse(source) : null;
15905
+ } catch {
15906
+ workspace = null;
15907
+ }
15908
+ const project = Object.values(workspace?.projects ?? {}).find(
15909
+ (entry) => entry.projectType === "application"
15910
+ );
15911
+ const build = project?.architect?.build;
15912
+ const outputPath = build?.options?.outputPath;
15913
+ const legacy = build?.builder?.endsWith(":browser") ?? false;
15914
+ const clean = (path) => path.replace(/^\.?\/+/, "").replace(/\/+$/, "");
15915
+ if (typeof outputPath === "string")
15916
+ return legacy ? clean(outputPath) : `${clean(outputPath)}/browser`;
15917
+ if (outputPath && typeof outputPath.base === "string")
15918
+ return `${clean(outputPath.base)}/${outputPath.browser ?? "browser"}`.replace(
15919
+ /\/$/,
15920
+ ""
15921
+ );
15922
+ const name = Object.entries(workspace?.projects ?? {}).find(
15923
+ ([, entry]) => entry === project
15924
+ )?.[0];
15925
+ if (name) return `dist/${name}/browser`;
15926
+ throw new DeploymentError(
15927
+ "FRAMEWORK_DETECTION_FAILED",
15928
+ "Could not find the Angular application's output path in angular.json. Set the output directory in the project settings."
15929
+ );
15930
+ }
15721
15931
  async function readPackageJson(root) {
15722
15932
  let source;
15723
15933
  try {
@@ -15754,6 +15964,14 @@ async function detectFramework(root, overrides = {}) {
15754
15964
  } else if (!packageJson) {
15755
15965
  preset = FRAMEWORK_PRESETS.static;
15756
15966
  } else {
15967
+ const serverOnly = SERVER_ONLY.find(
15968
+ ({ dependency }) => dependencyVersion(packageJson, dependency) !== null
15969
+ );
15970
+ if (serverOnly)
15971
+ throw new DeploymentError(
15972
+ "FRAMEWORK_DETECTION_FAILED",
15973
+ `${serverOnly.name} needs a server runtime. ${SERVER_RUNTIME_HINT}`
15974
+ );
15757
15975
  const detected = DETECTION_ORDER.map((id) => FRAMEWORK_PRESETS[id]).find(
15758
15976
  (candidate) => candidate.detectDependency !== null && dependencyVersion(packageJson, candidate.detectDependency) !== null
15759
15977
  );
@@ -15766,6 +15984,14 @@ async function detectFramework(root, overrides = {}) {
15766
15984
  }
15767
15985
  if (preset.id === "next" && await usesStaticExport(root))
15768
15986
  preset = FRAMEWORK_PRESETS["next-export"];
15987
+ if (!overrides.framework && packageJson) {
15988
+ const problem = await staticBuildProblem(root, preset.id, packageJson);
15989
+ if (problem)
15990
+ throw new DeploymentError(
15991
+ "FRAMEWORK_DETECTION_FAILED",
15992
+ `${problem} ${SERVER_RUNTIME_HINT}`
15993
+ );
15994
+ }
15769
15995
  const buildScript = overrides.buildCommand ?? preset.defaultBuildScript;
15770
15996
  if (buildScript !== null) {
15771
15997
  if (!packageJson)
@@ -15783,7 +16009,7 @@ async function detectFramework(root, overrides = {}) {
15783
16009
  return {
15784
16010
  preset,
15785
16011
  buildScript,
15786
- outputDirectory: overrides.outputDirectory ?? preset.defaultOutputDirectory,
16012
+ outputDirectory: overrides.outputDirectory ?? (preset.id === "angular" ? await angularOutput(root) : preset.defaultOutputDirectory),
15787
16013
  description: version2 ? `${FRAMEWORK_LABELS[preset.id]} ${version2}` : FRAMEWORK_LABELS[preset.id],
15788
16014
  dependencyRange: version2
15789
16015
  };
@@ -17573,12 +17799,21 @@ var LocalBuildExecutor = class {
17573
17799
  checkout,
17574
17800
  input.rootDirectory
17575
17801
  );
17802
+ const stacks = await import("./stack-PDV3NPWC.js");
17803
+ const stack = await (this.dependencies.detectStack ?? stacks.detectStack)(
17804
+ appRoot
17805
+ ).catch(() => null);
17806
+ if (stack) await observer.onMetadata?.({ framework: stack });
17576
17807
  const resolved = await this.dependencies.detectFramework(appRoot, {
17577
17808
  framework: input.framework ?? null,
17578
17809
  buildCommand: input.buildCommand ?? null,
17579
17810
  outputDirectory: input.outputDirectory ?? null
17580
17811
  });
17581
17812
  await info2(observer, "CLONING", `Detected ${resolved.description}`);
17813
+ if (!stack)
17814
+ await observer.onMetadata?.({
17815
+ framework: stacks.stackForPreset(resolved.preset.id)
17816
+ });
17582
17817
  const isNext = resolved.preset.id === "next";
17583
17818
  if (isNext && resolved.dependencyRange) {
17584
17819
  const declared = declaredNextMajor(resolved.dependencyRange);
@@ -17934,6 +18169,12 @@ async function errorFromResponse(response, context) {
17934
18169
  const serverMessage = typeof body?.error?.message === "string" ? body.error.message : null;
17935
18170
  const code = serverCode ?? (response.status === 401 ? "UNAUTHORIZED" : response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "INTERNAL_ERROR");
17936
18171
  const details = {};
18172
+ const serverDetails = body?.error?.details;
18173
+ if (typeof serverDetails === "object" && serverDetails !== null && !Array.isArray(serverDetails))
18174
+ Object.assign(details, serverDetails);
18175
+ const retryAfter = response.headers.get("retry-after")?.trim() ?? "";
18176
+ if (response.status === 429 && /^\d{1,9}$/.test(retryAfter))
18177
+ details.retryAfter = Number(retryAfter);
17937
18178
  if (body?.error?.fields !== void 0) details.fields = body.error.fields;
17938
18179
  if (typeof body?.error?.installUrl === "string")
17939
18180
  details.installUrl = body.error.installUrl;
@@ -18357,6 +18598,21 @@ var TERMINAL_DEPLOYMENT_STATUSES = [
18357
18598
  function isTerminalStatus(status) {
18358
18599
  return TERMINAL_DEPLOYMENT_STATUSES.includes(status);
18359
18600
  }
18601
+ var STALLED_GRACE_MS = 2 * 6e4;
18602
+ function isStalledBackend(info3, now = Date.now()) {
18603
+ if (typeof info3.stalled === "boolean") return info3.stalled;
18604
+ if (info3.status !== "provisioning") return false;
18605
+ if (info3.leaseExpiresAt === null) return true;
18606
+ const until = Date.parse(info3.leaseExpiresAt);
18607
+ return Number.isNaN(until) || until + STALLED_GRACE_MS <= now;
18608
+ }
18609
+ var POLICY_RULES = [
18610
+ "none",
18611
+ "public",
18612
+ "authenticated",
18613
+ "owner"
18614
+ ];
18615
+ var POLICY_ACTIONS = ["select", "insert", "update", "delete"];
18360
18616
  function projectInfo(row) {
18361
18617
  return {
18362
18618
  id: row.id,
@@ -18414,6 +18670,7 @@ var NaroClient = class {
18414
18670
  domains;
18415
18671
  logs;
18416
18672
  db;
18673
+ backend;
18417
18674
  constructor(options) {
18418
18675
  this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
18419
18676
  this.getAccessToken = options.getAccessToken;
@@ -18427,9 +18684,11 @@ var NaroClient = class {
18427
18684
  this.domains = new DomainsApi(this);
18428
18685
  this.logs = new LogsApi(this);
18429
18686
  this.db = new DbApi(this);
18687
+ this.backend = new BackendApi(this);
18430
18688
  }
18431
18689
  async request(path, options) {
18432
18690
  const token = await this.getAccessToken();
18691
+ const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
18433
18692
  let response;
18434
18693
  try {
18435
18694
  response = await this.doFetch(`${this.baseUrl}${path}`, {
@@ -18439,13 +18698,13 @@ var NaroClient = class {
18439
18698
  ...options.body !== void 0 ? { "Content-Type": "application/json" } : {}
18440
18699
  },
18441
18700
  ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
18442
- signal: AbortSignal.timeout(this.requestTimeoutMs)
18701
+ signal: AbortSignal.timeout(timeoutMs)
18443
18702
  });
18444
18703
  } catch (error51) {
18445
18704
  if (error51 instanceof Error && (error51.name === "TimeoutError" || error51.name === "AbortError")) {
18446
18705
  throw new NaroError(
18447
18706
  "TIMEOUT",
18448
- `${options.context} timed out after ${String(Math.round(this.requestTimeoutMs / 1e3))}s.`,
18707
+ `${options.context} timed out after ${String(Math.round(timeoutMs / 1e3))}s.`,
18449
18708
  { cause: error51 }
18450
18709
  );
18451
18710
  }
@@ -19013,6 +19272,255 @@ var DbApi = class {
19013
19272
  context: "Pull schema"
19014
19273
  });
19015
19274
  }
19275
+ /**
19276
+ * Changes the schema without SQL (spec 2026-09-24 P2-4): the server
19277
+ * renders the DDL, checks it and applies it as ONE migration recorded in
19278
+ * `d1_migrations` — all operations or none. `dryRun` returns the SQL and
19279
+ * the name without applying anything. Save `sql` as `migrations/<name>` so
19280
+ * `naro db reset` re-applies it; a refused operation's index is in
19281
+ * `details.operation`.
19282
+ */
19283
+ async schema(projectId, operations, options = {}) {
19284
+ return this.client.request(
19285
+ this.path(projectId, "/schema"),
19286
+ {
19287
+ method: "POST",
19288
+ // Only the keys given: the server validates each one it sees.
19289
+ body: {
19290
+ operations,
19291
+ ...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {},
19292
+ ...options.name !== void 0 ? { name: options.name } : {}
19293
+ },
19294
+ context: "Change schema"
19295
+ }
19296
+ );
19297
+ }
19298
+ };
19299
+ var PROVISION_TIMEOUT_MS = 12e4;
19300
+ function backendInfo(body) {
19301
+ return {
19302
+ status: body.status,
19303
+ step: body.step,
19304
+ error: body.error,
19305
+ url: body.url,
19306
+ anonKey: body.anonKey,
19307
+ serviceKeyPrefix: body.serviceKeyPrefix,
19308
+ leaseExpiresAt: body.leaseExpiresAt ?? null,
19309
+ // Only a real verdict: without it `isStalledBackend` judges locally.
19310
+ ...typeof body.stalled === "boolean" ? { stalled: body.stalled } : {},
19311
+ // A control plane from before the toggle could not turn a backend off:
19312
+ // whatever it has is on.
19313
+ enabled: typeof body.enabled === "boolean" ? body.enabled : body.status !== "none",
19314
+ disabledAt: body.disabledAt ?? null,
19315
+ turningOff: body.turningOff === true
19316
+ };
19317
+ }
19318
+ var BackendApi = class {
19319
+ constructor(client) {
19320
+ this.client = client;
19321
+ }
19322
+ path(projectId, suffix = "") {
19323
+ return `/api/projects/${encodeURIComponent(projectId)}/backend${suffix}`;
19324
+ }
19325
+ /**
19326
+ * `status: "none"` until the backend is first turned on; `enabled` says
19327
+ * whether it is on now.
19328
+ */
19329
+ async info(projectId) {
19330
+ const body = await this.client.request(this.path(projectId), {
19331
+ context: "Read backend"
19332
+ });
19333
+ return backendInfo(body);
19334
+ }
19335
+ /**
19336
+ * Turns the backend on (toggle spec §2.3): the first time it provisions
19337
+ * the D1, the keys and the Naro worker; a backend that was turned off
19338
+ * comes back with the same keys — its worker is uploaded again, nothing
19339
+ * else is made anew. Safe to repeat, and a repair: a ready backend that
19340
+ * is on has its routes written again and its worker checked (no upload),
19341
+ * a failed run resumes at its step. A step that fails is not an
19342
+ * HTTP error — the answer is 200 with `status: "error"`, `step` and
19343
+ * `error`. While another run holds the project's lease this answers the
19344
+ * row as it stands (`provisioning`).
19345
+ */
19346
+ async enable(projectId) {
19347
+ const body = await this.client.request(
19348
+ this.path(projectId, "/enable"),
19349
+ {
19350
+ method: "POST",
19351
+ context: "Enable backend",
19352
+ timeoutMs: PROVISION_TIMEOUT_MS
19353
+ }
19354
+ );
19355
+ return backendInfo(body);
19356
+ }
19357
+ /**
19358
+ * Turns the backend off (toggle spec §2.2): the dispatcher's routes to
19359
+ * the Naro worker go, so the app's sign-ins and data requests answer 404
19360
+ * from `/_naro/*` — in up to about 90 seconds, as the dispatcher's cache
19361
+ * ages out. The D1 with its data and users, the keys, the sign-in
19362
+ * settings and the worker stay; `enable` brings it back as it was.
19363
+ * Safe to repeat, and a repair: a backend already off has its routes
19364
+ * removed again — what finishes a turn-off that stopped half-way
19365
+ * (`turningOff`) — and keeps its first `disabledAt`. No backend at all
19366
+ * answers as it stands. `BACKEND_BUSY` while another run holds the lease.
19367
+ */
19368
+ async disable(projectId) {
19369
+ const body = await this.client.request(
19370
+ this.path(projectId, "/disable"),
19371
+ { method: "POST", context: "Disable backend" }
19372
+ );
19373
+ return backendInfo(body);
19374
+ }
19375
+ /**
19376
+ * `enable` under its first name: the control plane keeps
19377
+ * `POST …/backend/provision` as the same call.
19378
+ */
19379
+ async provision(projectId) {
19380
+ const body = await this.client.request(
19381
+ this.path(projectId, "/provision"),
19382
+ {
19383
+ method: "POST",
19384
+ context: "Provision backend",
19385
+ timeoutMs: PROVISION_TIMEOUT_MS
19386
+ }
19387
+ );
19388
+ return backendInfo(body);
19389
+ }
19390
+ /** The anon key and the service key's prefix; BACKEND_NOT_READY before provisioning. */
19391
+ async keys(projectId) {
19392
+ const body = await this.client.request(
19393
+ this.path(projectId, "/keys"),
19394
+ { context: "Read API keys" }
19395
+ );
19396
+ return { anonKey: body.anonKey, serviceKeyPrefix: body.serviceKeyPrefix };
19397
+ }
19398
+ /**
19399
+ * The whole service key. It skips every policy — servers only. A role
19400
+ * without `database.write` gets `FORBIDDEN` "Your role cannot reveal the
19401
+ * service key."
19402
+ */
19403
+ async revealServiceKey(projectId) {
19404
+ const body = await this.client.request(
19405
+ this.path(projectId, "/keys?reveal=service"),
19406
+ { context: "Reveal service key" }
19407
+ );
19408
+ return body.serviceKey;
19409
+ }
19410
+ /**
19411
+ * Replaces the live key of one kind, in two phases (spec §6 "(P1-3 리뷰
19412
+ * 반영)" I3): the worker takes the new hash first, and only then does the
19413
+ * database swap keys. There is no overlap — the old key stops working once
19414
+ * both are done. Only a `ready` backend that is on rotates
19415
+ * (`BACKEND_NOT_READY`, `BACKEND_BUSY`, or `BACKEND_OFF` while it is
19416
+ * turned off).
19417
+ *
19418
+ * The database decides the outcome. Whenever the new key ends up live —
19419
+ * even if a step after the swap failed — this resolves with the key and
19420
+ * `backend.status` `error`. Otherwise it throws `BACKEND_ROTATION_FAILED`
19421
+ * with no key; that usually leaves the old key working, but a 5xx, a
19422
+ * timeout or a lost connection is not proof either way: `keys()` (and
19423
+ * `revealServiceKey()`) show which key is live, and `enable` puts the
19424
+ * stored keys back on the worker. For a few seconds during any rotation
19425
+ * the worker can already refuse the old key.
19426
+ */
19427
+ async rotateKey(projectId, kind) {
19428
+ const body = await this.client.request(this.path(projectId, "/keys/rotate"), {
19429
+ method: "POST",
19430
+ body: { kind },
19431
+ context: "Rotate API key",
19432
+ timeoutMs: PROVISION_TIMEOUT_MS
19433
+ });
19434
+ const backend = backendInfo(body);
19435
+ const key = kind === "anon" ? backend.anonKey : body.serviceKey;
19436
+ if (typeof key !== "string" || key === "")
19437
+ throw new NaroError(
19438
+ "INTERNAL_ERROR",
19439
+ "The rotation answer carried no new key. `naro backend keys` shows the key stored now (--reveal for the service key).",
19440
+ { details: { backend } }
19441
+ );
19442
+ return { backend, key };
19443
+ }
19444
+ /**
19445
+ * The sign-in settings (spec P2-6 §12): email, Google and GitHub, the
19446
+ * allowed redirect origins, whether sign-in waits for a confirmed email,
19447
+ * and the callback URLs to register with the providers. Never a secret.
19448
+ */
19449
+ async auth(projectId) {
19450
+ return this.client.request(
19451
+ this.path(projectId, "/auth"),
19452
+ {
19453
+ context: "Read sign-in settings"
19454
+ }
19455
+ );
19456
+ }
19457
+ /**
19458
+ * Changes the given settings and uploads the Naro worker again with them,
19459
+ * under the provisioning lease: `BACKEND_BUSY` while another run holds it
19460
+ * (nothing is saved), `BACKEND_NOT_READY` before the first provision,
19461
+ * `BACKEND_OFF` while the backend is turned off (nothing is saved),
19462
+ * `VERIFICATION_NEEDS_EMAIL` for a state where nobody could sign in. A
19463
+ * failed upload is not an error — `backend.status` says `error` and the
19464
+ * settings are saved; `enable` finishes. Two minutes, like enable.
19465
+ */
19466
+ async setAuth(projectId, patch) {
19467
+ const answer = await this.client.request(
19468
+ this.path(projectId, "/auth"),
19469
+ {
19470
+ method: "PUT",
19471
+ body: patch,
19472
+ context: "Change sign-in settings",
19473
+ timeoutMs: PROVISION_TIMEOUT_MS
19474
+ }
19475
+ );
19476
+ return { auth: answer.auth, backend: backendInfo(answer.backend) };
19477
+ }
19478
+ /** Removes email or one provider; the same answer as `setAuth`. */
19479
+ async removeAuth(projectId, target) {
19480
+ const answer = await this.client.request(
19481
+ this.path(projectId, `/auth?target=${encodeURIComponent(target)}`),
19482
+ {
19483
+ method: "DELETE",
19484
+ context: "Change sign-in settings",
19485
+ timeoutMs: PROVISION_TIMEOUT_MS
19486
+ }
19487
+ );
19488
+ return { auth: answer.auth, backend: backendInfo(answer.backend) };
19489
+ }
19490
+ /** Every stored policy, by table name. Tables without one deny anon and signed-in requests. */
19491
+ async policies(projectId) {
19492
+ const body = await this.client.request(
19493
+ this.path(projectId, "/policies"),
19494
+ { context: "List policies" }
19495
+ );
19496
+ return body.policies;
19497
+ }
19498
+ /**
19499
+ * Merges the given fields into the table's policy (what is left out keeps
19500
+ * its stored value, or `none`) and returns the whole row as stored, under
19501
+ * the table's own spelling. An `owner` rule needs `ownerColumn`.
19502
+ */
19503
+ async setPolicy(projectId, table, patch) {
19504
+ const body = {};
19505
+ for (const key of [...POLICY_ACTIONS, "ownerColumn"])
19506
+ if (patch[key] !== void 0) body[key] = patch[key];
19507
+ const answer = await this.client.request(
19508
+ this.path(projectId, `/policies/${encodeURIComponent(table)}`),
19509
+ { method: "PUT", body, context: "Set policy" }
19510
+ );
19511
+ return answer.policy;
19512
+ }
19513
+ /**
19514
+ * Deletes the table's policy, so it denies anon and signed-in requests
19515
+ * again. `POLICY_NOT_FOUND` when the table has none.
19516
+ */
19517
+ async removePolicy(projectId, table) {
19518
+ const answer = await this.client.request(
19519
+ this.path(projectId, `/policies/${encodeURIComponent(table)}`),
19520
+ { method: "DELETE", context: "Remove policy" }
19521
+ );
19522
+ return answer.policy;
19523
+ }
19016
19524
  };
19017
19525
 
19018
19526
  // ../../packages/sdk/src/database.ts
@@ -19027,6 +19535,11 @@ var RESTORE_OUTCOME_UNKNOWN_CODES = /* @__PURE__ */ new Set([
19027
19535
  "TIMEOUT",
19028
19536
  "NETWORK_ERROR"
19029
19537
  ]);
19538
+ function cellText(statement, row, column) {
19539
+ const index = statement.columns.indexOf(column);
19540
+ const value = index < 0 ? void 0 : row[index];
19541
+ return value === null || value === void 0 ? "" : String(value);
19542
+ }
19030
19543
  var MAX_CELL_CHARS = 60;
19031
19544
  function formatCell(value) {
19032
19545
  if (value === null || value === void 0) return "NULL";
@@ -19220,62 +19733,115 @@ function hasConfig(dir, base2, evidence) {
19220
19733
  }
19221
19734
  return false;
19222
19735
  }
19736
+ var SERVER_ONLY2 = [
19737
+ ["@tanstack/react-start", "TanStack Start"],
19738
+ ["@tanstack/solid-start", "TanStack Start"],
19739
+ ["@remix-run/dev", "Remix"],
19740
+ ["@react-router/dev", "React Router (framework mode)"],
19741
+ ["@solidjs/start", "SolidStart"],
19742
+ ["@builder.io/qwik-city", "Qwik City"],
19743
+ ["@analogjs/platform", "Analog"]
19744
+ ];
19745
+ var DETECTORS = [
19746
+ {
19747
+ framework: "nextjs",
19748
+ name: "Next.js",
19749
+ dependency: "next",
19750
+ config: "next.config",
19751
+ buildCommand: "next build",
19752
+ outputDirectory: ".next"
19753
+ },
19754
+ {
19755
+ framework: "nuxt",
19756
+ name: "Nuxt",
19757
+ dependency: "nuxt",
19758
+ config: "nuxt.config",
19759
+ buildCommand: "nuxt generate",
19760
+ outputDirectory: ".output/public"
19761
+ },
19762
+ {
19763
+ framework: "astro",
19764
+ name: "Astro",
19765
+ dependency: "astro",
19766
+ config: "astro.config",
19767
+ buildCommand: "astro build",
19768
+ outputDirectory: "dist"
19769
+ },
19770
+ {
19771
+ framework: "sveltekit",
19772
+ name: "SvelteKit",
19773
+ dependency: "@sveltejs/kit",
19774
+ buildCommand: "vite build",
19775
+ outputDirectory: "build"
19776
+ },
19777
+ {
19778
+ framework: "angular",
19779
+ name: "Angular",
19780
+ dependency: "@angular/core",
19781
+ buildCommand: "ng build",
19782
+ outputDirectory: "dist/<project>/browser"
19783
+ },
19784
+ {
19785
+ framework: "docusaurus",
19786
+ name: "Docusaurus",
19787
+ dependency: "@docusaurus/core",
19788
+ buildCommand: "docusaurus build",
19789
+ outputDirectory: "build"
19790
+ },
19791
+ {
19792
+ framework: "gatsby",
19793
+ name: "Gatsby",
19794
+ dependency: "gatsby",
19795
+ buildCommand: "gatsby build",
19796
+ outputDirectory: "public"
19797
+ },
19798
+ {
19799
+ framework: "vite",
19800
+ name: "Vite",
19801
+ dependency: "vite",
19802
+ config: "vite.config",
19803
+ buildCommand: "vite build",
19804
+ outputDirectory: "dist"
19805
+ },
19806
+ {
19807
+ framework: "react",
19808
+ name: "Create React App",
19809
+ dependency: "react-scripts",
19810
+ buildCommand: "react-scripts build",
19811
+ outputDirectory: "build"
19812
+ }
19813
+ ];
19223
19814
  function detectFramework2(dir) {
19224
19815
  const evidence = [];
19225
19816
  const pkg = readPackageJson2(dir);
19226
- if (hasDependency(pkg, "next") || hasConfig(dir, "next.config", evidence)) {
19227
- if (hasDependency(pkg, "next")) evidence.push("package.json: next");
19228
- if (existsSync3(join20(dir, "app"))) evidence.push("app/");
19229
- if (existsSync3(join20(dir, "pages"))) evidence.push("pages/");
19230
- return {
19231
- framework: "nextjs",
19232
- name: "Next.js",
19233
- buildCommand: "next build",
19234
- outputDirectory: ".next",
19235
- evidence,
19236
- deployable: true
19237
- };
19238
- }
19239
- if (hasDependency(pkg, "nuxt") || hasConfig(dir, "nuxt.config", evidence)) {
19240
- if (hasDependency(pkg, "nuxt")) evidence.push("package.json: nuxt");
19241
- return {
19242
- framework: "nuxt",
19243
- name: "Nuxt",
19244
- buildCommand: "nuxt build",
19245
- outputDirectory: ".output",
19246
- evidence,
19247
- deployable: false
19248
- };
19249
- }
19250
- if (hasDependency(pkg, "astro") || hasConfig(dir, "astro.config", evidence)) {
19251
- if (hasDependency(pkg, "astro")) evidence.push("package.json: astro");
19817
+ const serverOnly = SERVER_ONLY2.find(
19818
+ ([dependency]) => hasDependency(pkg, dependency)
19819
+ );
19820
+ if (serverOnly) {
19821
+ evidence.push(`package.json: ${serverOnly[0]}`);
19252
19822
  return {
19253
- framework: "astro",
19254
- name: "Astro",
19255
- buildCommand: "astro build",
19256
- outputDirectory: "dist",
19823
+ framework: "server-only",
19824
+ name: serverOnly[1],
19825
+ buildCommand: null,
19826
+ outputDirectory: null,
19257
19827
  evidence,
19258
19828
  deployable: false
19259
19829
  };
19260
19830
  }
19261
- if (hasDependency(pkg, "vite") || hasConfig(dir, "vite.config", evidence)) {
19262
- if (hasDependency(pkg, "vite")) evidence.push("package.json: vite");
19263
- return {
19264
- framework: "vite",
19265
- name: "Vite",
19266
- buildCommand: "vite build",
19267
- outputDirectory: "dist",
19268
- evidence,
19269
- deployable: true
19270
- };
19271
- }
19272
- if (hasDependency(pkg, "react-scripts")) {
19273
- evidence.push("package.json: react-scripts");
19831
+ for (const detector of DETECTORS) {
19832
+ const byDependency = hasDependency(pkg, detector.dependency);
19833
+ const byConfig = !byDependency && detector.config ? hasConfig(dir, detector.config, evidence) : false;
19834
+ if (!byDependency && !byConfig) continue;
19835
+ if (byDependency) evidence.push(`package.json: ${detector.dependency}`);
19836
+ if (detector.framework === "nextjs") {
19837
+ if (existsSync3(join20(dir, "app"))) evidence.push("app/");
19838
+ if (existsSync3(join20(dir, "pages"))) evidence.push("pages/");
19839
+ }
19274
19840
  return {
19275
- framework: "react",
19276
- name: "Create React App",
19277
- buildCommand: "react-scripts build",
19278
- outputDirectory: "build",
19841
+ framework: detector.framework,
19842
+ name: detector.name,
19843
+ buildCommand: detector.buildCommand,
19844
+ outputDirectory: detector.outputDirectory,
19279
19845
  evidence,
19280
19846
  deployable: true
19281
19847
  };
@@ -19301,27 +19867,455 @@ function detectFramework2(dir) {
19301
19867
  };
19302
19868
  }
19303
19869
 
19870
+ // ../../packages/data-core/src/system-tables.ts
19871
+ var SYSTEM_TABLE_PREFIXES = [
19872
+ "_naro_",
19873
+ "_cf_",
19874
+ "sqlite_"
19875
+ ];
19876
+ var SYSTEM_TABLE_NAMES = ["d1_migrations"];
19877
+ function asciiLower(text) {
19878
+ return text.replace(/[A-Z]+/g, (run) => run.toLowerCase());
19879
+ }
19880
+ function isSystemTable(name) {
19881
+ const lower = asciiLower(name);
19882
+ return SYSTEM_TABLE_PREFIXES.some((prefix) => lower.startsWith(prefix)) || SYSTEM_TABLE_NAMES.includes(lower);
19883
+ }
19884
+ function likeLiteral(text) {
19885
+ return text.replace(/[\\%_]/g, (char) => `\\${char}`).replace(/'/g, "''");
19886
+ }
19887
+ var COLUMN_REFERENCE = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$/;
19888
+ function systemTablePredicate(column = "name") {
19889
+ if (!COLUMN_REFERENCE.test(column))
19890
+ throw new Error(`Not a column reference: ${column}`);
19891
+ return [
19892
+ ...SYSTEM_TABLE_PREFIXES.map(
19893
+ (prefix) => `${column} NOT LIKE '${likeLiteral(prefix)}%' ESCAPE '\\'`
19894
+ ),
19895
+ ...SYSTEM_TABLE_NAMES.map(
19896
+ (name) => `${column} NOT LIKE '${likeLiteral(name)}' ESCAPE '\\'`
19897
+ ),
19898
+ `${column} NOT IN (SELECT name FROM pragma_table_list WHERE schema = 'main' AND type = 'shadow')`
19899
+ ].join(" AND ");
19900
+ }
19901
+ var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
19902
+
19903
+ // ../../packages/data-core/src/values.ts
19904
+ function isPlainObject2(value) {
19905
+ if (typeof value !== "object" || value === null || Array.isArray(value))
19906
+ return false;
19907
+ const prototype = Object.getPrototypeOf(value);
19908
+ return prototype === Object.prototype || prototype === null;
19909
+ }
19910
+ function isValue(value) {
19911
+ return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
19912
+ }
19913
+
19914
+ // ../../packages/data-core/src/query.ts
19915
+ var FILTER_OPERATORS = [
19916
+ "eq",
19917
+ "neq",
19918
+ "gt",
19919
+ "gte",
19920
+ "lt",
19921
+ "lte",
19922
+ "in",
19923
+ "is",
19924
+ "like",
19925
+ "ilike",
19926
+ // Phase 2-1: for a TEXT column holding a JSON array (spec P2-1 §2).
19927
+ "contains",
19928
+ "containedBy",
19929
+ "overlaps"
19930
+ ];
19931
+ function isAuthUid(value) {
19932
+ return isPlainObject2(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "auth") && value.auth === "uid";
19933
+ }
19934
+ var MAX_BODY_BYTES = 1024 * 1024;
19935
+ var MAX_FILTERS = 100;
19936
+ var MAX_FILTER_DEPTH = 4;
19937
+ var MAX_ARRAY_VALUES = 100;
19938
+ var MAX_ARRAY_CONDITIONS = 4;
19939
+ var ARRAY_OPERATORS = [
19940
+ "contains",
19941
+ "containedBy",
19942
+ "overlaps"
19943
+ ];
19944
+ var MAX_PATTERN_LENGTH = 1024;
19945
+ var MAX_IDENTIFIER_LENGTH = 128;
19946
+ var Invalid = class extends Error {
19947
+ constructor(path, reason) {
19948
+ super(reason);
19949
+ this.path = path;
19950
+ this.reason = reason;
19951
+ }
19952
+ };
19953
+ function invalid(path, reason) {
19954
+ throw new Invalid(path, reason);
19955
+ }
19956
+ function own(object2, key) {
19957
+ return Object.hasOwn(object2, key) ? object2[key] : void 0;
19958
+ }
19959
+ function isOperator(value) {
19960
+ return FILTER_OPERATORS.includes(value);
19961
+ }
19962
+ function readObject(value, path) {
19963
+ if (!isPlainObject2(value)) invalid(path, "must be an object");
19964
+ return value;
19965
+ }
19966
+ function checkKeys(object2, allowed, path) {
19967
+ for (const key of Object.keys(object2)) {
19968
+ if (allowed.includes(key)) continue;
19969
+ if (longerThan(key, MAX_IDENTIFIER_LENGTH))
19970
+ invalid(path, "has a field that is not a known field");
19971
+ invalid(path ? `${path}.${key}` : key, "is not a known field");
19972
+ }
19973
+ }
19974
+ function readName(value, path) {
19975
+ if (typeof value !== "string" || value === "")
19976
+ invalid(path, "must be a non-empty string");
19977
+ if (longerThan(value, MAX_IDENTIFIER_LENGTH))
19978
+ invalid(
19979
+ path,
19980
+ `must be at most ${String(MAX_IDENTIFIER_LENGTH)} characters`
19981
+ );
19982
+ return value;
19983
+ }
19984
+ function readValue(value, path, auth = false) {
19985
+ if (auth && isAuthUid(value)) return { auth: "uid" };
19986
+ if (!isValue(value))
19987
+ invalid(
19988
+ path,
19989
+ auth ? 'must be a string, a finite number, a boolean, null or {"auth":"uid"}' : "must be a string, a finite number, a boolean or null"
19990
+ );
19991
+ return value;
19992
+ }
19993
+ function longerThan(text, max) {
19994
+ if (text.length <= max) return false;
19995
+ if (text.length > max * 2) return true;
19996
+ let count = 0;
19997
+ for (const _ of text) if (++count > max) return true;
19998
+ return false;
19999
+ }
20000
+ function endsWithLoneEscape(pattern) {
20001
+ let run = 0;
20002
+ for (let index = pattern.length - 1; index >= 0; index--) {
20003
+ if (pattern[index] !== "\\") break;
20004
+ run++;
20005
+ }
20006
+ return run % 2 === 1;
20007
+ }
20008
+ function readFilter(value, path, auth) {
20009
+ const object2 = readObject(value, path);
20010
+ checkKeys(object2, ["operator", "column", "value"], path);
20011
+ const operator = own(object2, "operator");
20012
+ if (!isOperator(operator))
20013
+ invalid(
20014
+ `${path}.operator`,
20015
+ `must be one of ${FILTER_OPERATORS.join(", ")}`
20016
+ );
20017
+ const column = readName(own(object2, "column"), `${path}.column`);
20018
+ if (!Object.hasOwn(object2, "value")) invalid(`${path}.value`, "is required");
20019
+ const raw = own(object2, "value");
20020
+ const valuePath = `${path}.value`;
20021
+ switch (operator) {
20022
+ case "in": {
20023
+ if (!Array.isArray(raw)) invalid(valuePath, "must be an array");
20024
+ const values = raw.map(
20025
+ (item, index) => readValue(item, `${valuePath}[${index}]`, auth)
20026
+ );
20027
+ return { operator, column, value: values };
20028
+ }
20029
+ case "is":
20030
+ if (raw !== null && raw !== true && raw !== false)
20031
+ invalid(valuePath, "must be null, true or false");
20032
+ return { operator, column, value: raw };
20033
+ case "like":
20034
+ case "ilike":
20035
+ if (typeof raw !== "string") invalid(valuePath, "must be a string");
20036
+ if (longerThan(raw, MAX_PATTERN_LENGTH))
20037
+ invalid(
20038
+ valuePath,
20039
+ `must be at most ${String(MAX_PATTERN_LENGTH)} characters`
20040
+ );
20041
+ if (raw.includes("\0"))
20042
+ invalid(valuePath, "must not contain a NUL character");
20043
+ if (endsWithLoneEscape(raw))
20044
+ invalid(valuePath, "must not end with an unescaped backslash");
20045
+ return { operator, column, value: raw };
20046
+ case "contains":
20047
+ case "containedBy":
20048
+ case "overlaps":
20049
+ if (!Array.isArray(raw) || raw.length === 0 || raw.length > MAX_ARRAY_VALUES || !raw.every(
20050
+ (item) => typeof item === "string" || typeof item === "number" && Number.isFinite(item) || auth && isAuthUid(item)
20051
+ ))
20052
+ invalid(
20053
+ valuePath,
20054
+ auth ? `must be a non-empty array of strings, numbers and {"auth":"uid"} (at most ${String(MAX_ARRAY_VALUES)})` : `must be a non-empty array of strings and numbers (at most ${String(MAX_ARRAY_VALUES)})`
20055
+ );
20056
+ return {
20057
+ operator,
20058
+ column,
20059
+ value: raw.map(
20060
+ (item) => isAuthUid(item) ? { auth: "uid" } : item
20061
+ )
20062
+ };
20063
+ default:
20064
+ return { operator, column, value: readValue(raw, valuePath, auth) };
20065
+ }
20066
+ }
20067
+ var NODE_KEYS = ["operator", "or", "and", "not"];
20068
+ function readNode(value, path, depth, budget) {
20069
+ const object2 = readObject(value, path);
20070
+ const kinds = NODE_KEYS.filter((key) => Object.hasOwn(object2, key));
20071
+ const [kind] = kinds;
20072
+ if (kinds.length !== 1 || kind === void 0)
20073
+ invalid(path, `needs exactly one of ${NODE_KEYS.join(", ")}`);
20074
+ if (kind === "operator") {
20075
+ if (++budget.conditions > MAX_FILTERS)
20076
+ invalid(budget.path, `has more than ${String(MAX_FILTERS)} conditions`);
20077
+ const filter = readFilter(object2, path, budget.auth);
20078
+ if (ARRAY_OPERATORS.includes(filter.operator) && ++budget.arrays > MAX_ARRAY_CONDITIONS)
20079
+ invalid(
20080
+ budget.path,
20081
+ `has more than ${String(MAX_ARRAY_CONDITIONS)} ${ARRAY_OPERATORS.join("/")} conditions`
20082
+ );
20083
+ return filter;
20084
+ }
20085
+ if (depth >= MAX_FILTER_DEPTH)
20086
+ invalid(path, `nests deeper than ${String(MAX_FILTER_DEPTH)} groups`);
20087
+ checkKeys(object2, [kind], path);
20088
+ const inner = own(object2, kind);
20089
+ const innerPath = `${path}.${kind}`;
20090
+ if (kind === "not")
20091
+ return { not: readNode(inner, innerPath, depth + 1, budget) };
20092
+ if (!Array.isArray(inner)) invalid(innerPath, "must be an array");
20093
+ if (inner.length === 0) invalid(innerPath, "must not be empty");
20094
+ const children = inner.map(
20095
+ (child, index) => readNode(child, `${innerPath}[${index}]`, depth + 1, budget)
20096
+ );
20097
+ return kind === "or" ? { or: children } : { and: children };
20098
+ }
20099
+ function parsePolicyNode(value, path) {
20100
+ try {
20101
+ const budget = { path, conditions: 0, arrays: 0, auth: true };
20102
+ return { ok: true, node: readNode(value, path, 0, budget) };
20103
+ } catch (error51) {
20104
+ if (error51 instanceof Invalid)
20105
+ return { ok: false, path: error51.path, reason: error51.reason };
20106
+ throw error51;
20107
+ }
20108
+ }
20109
+
20110
+ // ../../packages/data-core/src/schema.ts
20111
+ var SCHEMA_SQL = [
20112
+ `SELECT t.name AS table_name, t.type AS table_type, c.cid AS cid, c.name AS column_name, c.type AS column_type, c."notnull" AS not_null, c.dflt_value AS default_value, c.pk AS pk, c.hidden AS hidden FROM pragma_table_list AS t JOIN pragma_table_xinfo(t.name, t.schema) AS c WHERE t.schema = 'main' AND t.type IN ('table', 'virtual') AND ${systemTablePredicate("t.name")} ORDER BY t.name, c.cid`,
20113
+ // Phase 2-2 (적대적 리뷰 반영): each table's CREATE TABLE text, where
20114
+ // alone a column's collation is written. A separate statement, so the
20115
+ // text comes once per table rather than once per column. D1 answers
20116
+ // `sqlite_master` reads (the database routes' `_tables.ts` reads it too).
20117
+ `SELECT name AS table_name, sql AS table_sql FROM sqlite_master WHERE type = 'table' AND ${systemTablePredicate("name")}`
20118
+ ];
20119
+
20120
+ // ../../packages/data-core/src/condition.ts
20121
+ var MAX_CONDITION_BYTES = 16 * 1024;
20122
+
20123
+ // ../../packages/data-core/src/redirect-origins.ts
20124
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
20125
+ "localhost",
20126
+ "127.0.0.1",
20127
+ "[::1]"
20128
+ ]);
20129
+ function isSecureOrLoopback(url2) {
20130
+ if (url2.protocol === "https:") return true;
20131
+ return url2.protocol === "http:" && LOOPBACK_HOSTS.has(url2.hostname);
20132
+ }
20133
+ var MAX_ENTRY_LENGTH = 255;
20134
+ var TYPED_PATH = /^[a-z][a-z0-9+.-]*:\/\/[^/]*(\/.*)?$/i;
20135
+ var SEGMENT = /^[A-Za-z0-9._~-]+$/;
20136
+ function parseRedirectEntry(value) {
20137
+ if (typeof value !== "string") return null;
20138
+ const text = value.trim();
20139
+ if (text === "" || text.length > MAX_ENTRY_LENGTH) return null;
20140
+ if (/[\s?#*%\\]/.test(text)) return null;
20141
+ let url2;
20142
+ try {
20143
+ url2 = new URL(text);
20144
+ } catch {
20145
+ return null;
20146
+ }
20147
+ if (url2.username !== "" || url2.password !== "") return null;
20148
+ if (url2.search !== "" || url2.hash !== "") return null;
20149
+ if (!isSecureOrLoopback(url2)) return null;
20150
+ const typed = TYPED_PATH.exec(text)?.[1] ?? "/";
20151
+ if (typed !== url2.pathname) return null;
20152
+ const path = url2.pathname;
20153
+ if (path !== "/") {
20154
+ if (!path.endsWith("/")) return null;
20155
+ const segments = path.slice(1, -1).split("/");
20156
+ if (!segments.every(
20157
+ (segment) => SEGMENT.test(segment) && segment !== "." && segment !== ".."
20158
+ ))
20159
+ return null;
20160
+ }
20161
+ return { origin: url2.origin, path };
20162
+ }
20163
+ function formatRedirectEntry(entry) {
20164
+ return entry.path === "/" ? entry.origin : `${entry.origin}${entry.path}`;
20165
+ }
20166
+
20167
+ // ../../packages/data-core/src/system-sql.ts
20168
+ 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')))";
20169
+ var SYSTEM_MIGRATIONS = [
20170
+ {
20171
+ version: 1,
20172
+ name: "system_tables",
20173
+ statements: [
20174
+ MIGRATIONS_TABLE,
20175
+ "CREATE TABLE IF NOT EXISTS _naro_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')))",
20176
+ // Spec §11.2, as written there.
20177
+ "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')))"
20178
+ ]
20179
+ },
20180
+ {
20181
+ // Spec P2-2 §3.1: an action whose rule column says 'condition' reads its
20182
+ // condition from these. A worker from before them reads 'condition' as
20183
+ // an unknown rule — none.
20184
+ version: 2,
20185
+ name: "policy_conditions",
20186
+ statements: [],
20187
+ addColumns: ["select", "insert", "update", "delete"].map((action) => ({
20188
+ table: "_naro_policies",
20189
+ column: `${action}_condition`,
20190
+ definition: "TEXT"
20191
+ }))
20192
+ },
20193
+ {
20194
+ // Spec P2-6 §11: the Naro worker's sign-in handoff. A provider sign-in's
20195
+ // flow (state → the app's redirect and the browser's PKCE challenge), the
20196
+ // one-time codes a redirect hands the app, the sends the per-address
20197
+ // email limit counts, and (adversarial review I3) the unconfirmed
20198
+ // accounts someone else also tried to sign up as. Times are epoch
20199
+ // milliseconds.
20200
+ version: 3,
20201
+ name: "auth_handoff",
20202
+ statements: [
20203
+ "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)",
20204
+ "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)",
20205
+ "CREATE TABLE IF NOT EXISTS _naro_auth_email_log (address_hash TEXT NOT NULL, sent_at INTEGER NOT NULL)",
20206
+ "CREATE INDEX IF NOT EXISTS _naro_auth_email_log_address ON _naro_auth_email_log (address_hash, sent_at)",
20207
+ "CREATE TABLE IF NOT EXISTS _naro_auth_contested (user_id TEXT PRIMARY KEY, contested_at INTEGER NOT NULL)"
20208
+ ]
20209
+ }
20210
+ ];
20211
+
20212
+ // ../../packages/data-core/src/write-quota.ts
20213
+ var WRITE_QUOTA_KEY = "write_quota";
20214
+ var WRITE_QUOTA_SQL = {
20215
+ read: `SELECT value FROM _naro_meta WHERE key = '${WRITE_QUOTA_KEY}'`,
20216
+ upsert: `INSERT INTO _naro_meta (key, value, updated_at) VALUES ('${WRITE_QUOTA_KEY}', ?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
20217
+ };
20218
+
19304
20219
  // ../../packages/sdk/src/generate-types.ts
19305
20220
  var HEADER = [
19306
20221
  "// Generated by `naro gen types`. Do not edit - re-run the command instead.",
19307
20222
  "// `D1Database` comes from @cloudflare/workers-types."
19308
20223
  ];
19309
- function valueType(declared) {
20224
+ function affinity(declared, strict) {
19310
20225
  const type = declared.toUpperCase();
19311
- if (type.trim() === "") return "number | string | ArrayBuffer";
19312
- if (type.includes("INT")) return "number";
20226
+ if (type.trim() === "") return "NONE";
20227
+ if (strict && type.trim() === "ANY") return "ANY";
20228
+ if (type.includes("INT")) return "INTEGER";
19313
20229
  if (type.includes("CHAR") || type.includes("CLOB") || type.includes("TEXT"))
19314
- return "string";
19315
- if (type.includes("BLOB")) return "ArrayBuffer";
20230
+ return "TEXT";
20231
+ if (type.includes("BLOB")) return "BLOB";
19316
20232
  if (type.includes("REAL") || type.includes("FLOA") || type.includes("DOUB"))
19317
- return "number";
19318
- return "number | string";
20233
+ return "REAL";
20234
+ if (type.includes("BOOL")) return "BOOLEAN";
20235
+ if (type.includes("JSON")) return "JSON";
20236
+ return "NUMERIC";
20237
+ }
20238
+ var READ = {
20239
+ INTEGER: "number",
20240
+ TEXT: "string",
20241
+ BLOB: "number[]",
20242
+ REAL: "number",
20243
+ NONE: "number | string | number[]",
20244
+ ANY: "number | string | number[]",
20245
+ BOOLEAN: "number",
20246
+ JSON: "number | string",
20247
+ NUMERIC: "number | string"
20248
+ };
20249
+ var WRITE = {
20250
+ INTEGER: "number",
20251
+ TEXT: "string | Json",
20252
+ BLOB: "never",
20253
+ REAL: "number",
20254
+ NONE: "number | string | Json",
20255
+ ANY: "number | string | Json",
20256
+ BOOLEAN: "number | boolean",
20257
+ JSON: "number | string | Json",
20258
+ NUMERIC: "number | string"
20259
+ };
20260
+ var JSON_TYPE = "export type Json = readonly unknown[] | { readonly [key: string]: unknown };";
20261
+ function withNull(type, nullable2) {
20262
+ if (!nullable2) return type;
20263
+ return type === "never" ? "null" : `${type} | null`;
19319
20264
  }
19320
20265
  function interfaceBase(tableName) {
19321
20266
  const pascal = tableName.split(/[^A-Za-z0-9$]+/).filter((word) => word.length > 0).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("").replace(/[^A-Za-z0-9_$]/g, "");
19322
20267
  return pascal === "" || /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
19323
20268
  }
20269
+ function sqlSkeleton(sql) {
20270
+ let out = "";
20271
+ let index = 0;
20272
+ while (index < sql.length) {
20273
+ const char = sql[index] ?? "";
20274
+ const pair = sql.slice(index, index + 2);
20275
+ if (pair === "--") {
20276
+ const end = sql.indexOf("\n", index);
20277
+ index = end < 0 ? sql.length : end;
20278
+ out += " ";
20279
+ } else if (pair === "/*") {
20280
+ const end = sql.indexOf("*/", index + 2);
20281
+ index = end < 0 ? sql.length : end + 2;
20282
+ out += " ";
20283
+ } else if (char === "'" || char === '"' || char === "`" || char === "[") {
20284
+ const close = char === "[" ? "]" : char;
20285
+ let end = index + 1;
20286
+ for (; ; ) {
20287
+ const found = sql.indexOf(close, end);
20288
+ if (found < 0) {
20289
+ end = sql.length;
20290
+ break;
20291
+ }
20292
+ if (close !== "]" && sql[found + 1] === close) {
20293
+ end = found + 2;
20294
+ continue;
20295
+ }
20296
+ end = found + 1;
20297
+ break;
20298
+ }
20299
+ out += `${char}${close}`;
20300
+ index = end;
20301
+ } else {
20302
+ out += char;
20303
+ index += 1;
20304
+ }
20305
+ }
20306
+ return out;
20307
+ }
20308
+ function tableOptions(sql) {
20309
+ const skeleton = sql === null ? "" : sqlSkeleton(sql);
20310
+ const close = skeleton.lastIndexOf(")");
20311
+ const tail = close < 0 ? "" : skeleton.slice(close + 1);
20312
+ return {
20313
+ withoutRowid: /\bWITHOUT\s+ROWID\b/i.test(tail),
20314
+ strict: /\bSTRICT\b/i.test(tail)
20315
+ };
20316
+ }
19324
20317
  function rowidAlias(table) {
20318
+ if (tableOptions(table.sql).withoutRowid) return null;
19325
20319
  const keys = table.columns.filter((column) => column.primaryKey);
19326
20320
  const only = keys.length === 1 ? keys[0] : void 0;
19327
20321
  return only && only.type.trim().toUpperCase() === "INTEGER" ? only.name : null;
@@ -19333,34 +20327,120 @@ function keyLiteral(name) {
19333
20327
  (unit) => `${UNICODE_ESCAPE}${unit.charCodeAt(0).toString(16).padStart(4, "0")}`
19334
20328
  );
19335
20329
  }
19336
- function property(column, alias) {
19337
- const nullable2 = !column.notNull && column.name !== alias;
19338
- return ` ${keyLiteral(column.name)}: ${valueType(column.type)}${nullable2 ? " | null" : ""};`;
20330
+ function ownerFilledOnInsert(policies, table) {
20331
+ const folded = asciiLower(table);
20332
+ const matches = policies.filter(
20333
+ (policy2) => asciiLower(policy2.table) === folded
20334
+ );
20335
+ const policy = policies.find((candidate) => candidate.table === table) ?? (matches.length === 1 ? matches[0] : void 0);
20336
+ return policy?.insert === "owner" ? policy.ownerColumn : null;
20337
+ }
20338
+ async function policiesForTypes(client, projectId) {
20339
+ try {
20340
+ return { policies: await client.backend.policies(projectId) };
20341
+ } catch (error51) {
20342
+ return {
20343
+ policies: void 0,
20344
+ reason: isNaroError(error51) ? error51.code : "UNKNOWN_ERROR"
20345
+ };
20346
+ }
20347
+ }
20348
+ function isNullable(column, alias) {
20349
+ return !column.notNull && column.name !== alias;
20350
+ }
20351
+ function rowProperty(column, { alias, strict }) {
20352
+ const type = READ[affinity(column.type, strict)];
20353
+ return ` ${keyLiteral(column.name)}: ${withNull(type, isNullable(column, alias))};`;
20354
+ }
20355
+ function writeProperty(column, { alias, strict }, optional2) {
20356
+ const type = WRITE[affinity(column.type, strict)];
20357
+ return ` ${keyLiteral(column.name)}${optional2 ? "?" : ""}: ${withNull(type, isNullable(column, alias))};`;
19339
20358
  }
19340
20359
  function block(name, lines) {
19341
20360
  return lines.length === 0 ? [`export interface ${name} {}`] : [`export interface ${name} {`, ...lines, "}"];
19342
20361
  }
19343
- function generateTypes(tables) {
20362
+ function member(indent, name, lines) {
20363
+ return lines.length === 0 ? [`${indent}${name}: {};`] : [`${indent}${name}: {`, ...lines, `${indent}};`];
20364
+ }
20365
+ function tableEntry(described) {
20366
+ const { table, row, alias, owner } = described;
20367
+ const writable = table.columns.filter((column) => column.generated !== true);
20368
+ const optionalOnInsert = (column) => isNullable(column, alias) || column.defaultValue !== null || column.name === alias || asciiLower(column.name) === owner;
20369
+ return [
20370
+ ` ${keyLiteral(table.name)}: {`,
20371
+ ` Row: ${row};`,
20372
+ ...member(
20373
+ " ",
20374
+ "Insert",
20375
+ writable.map(
20376
+ (column) => writeProperty(column, described, optionalOnInsert(column))
20377
+ )
20378
+ ),
20379
+ ...member(
20380
+ " ",
20381
+ "Update",
20382
+ writable.map((column) => writeProperty(column, described, true))
20383
+ ),
20384
+ " };"
20385
+ ];
20386
+ }
20387
+ function viewEntry({ table, row }) {
20388
+ return [` ${keyLiteral(table.name)}: {`, ` Row: ${row};`, " };"];
20389
+ }
20390
+ var ALIASES = [
20391
+ 'export type Tables<Name extends keyof Database["tables"] | keyof Database["views"]> =',
20392
+ ' (Database["tables"] & Database["views"])[Name]["Row"];',
20393
+ 'export type TablesInsert<Name extends keyof Database["tables"]> =',
20394
+ ' Database["tables"][Name]["Insert"];',
20395
+ 'export type TablesUpdate<Name extends keyof Database["tables"]> =',
20396
+ ' Database["tables"][Name]["Update"];'
20397
+ ];
20398
+ function generateTypes(tables, options = {}) {
19344
20399
  const used = /* @__PURE__ */ new Set();
19345
20400
  const out = [...HEADER];
19346
- const map2 = [];
20401
+ const described = [];
19347
20402
  for (const table of tables) {
19348
20403
  const base2 = `${interfaceBase(table.name)}Row`;
19349
- let name = base2;
19350
- for (let suffix = 2; used.has(name); suffix += 1)
19351
- name = `${base2}_${String(suffix)}`;
19352
- used.add(name);
19353
- const alias = rowidAlias(table);
20404
+ let row = base2;
20405
+ for (let suffix = 2; used.has(row); suffix += 1)
20406
+ row = `${base2}_${String(suffix)}`;
20407
+ used.add(row);
20408
+ const owner = ownerFilledOnInsert(options.policies ?? [], table.name);
20409
+ const entry = {
20410
+ table,
20411
+ row,
20412
+ alias: rowidAlias(table),
20413
+ strict: tableOptions(table.sql).strict,
20414
+ owner: owner === null ? null : asciiLower(owner)
20415
+ };
20416
+ described.push(entry);
19354
20417
  out.push(
19355
20418
  "",
19356
20419
  ...block(
19357
- name,
19358
- table.columns.map((column) => property(column, alias))
20420
+ row,
20421
+ table.columns.map((column) => rowProperty(column, entry))
19359
20422
  )
19360
20423
  );
19361
- map2.push(` ${keyLiteral(table.name)}: ${name};`);
19362
20424
  }
19363
- out.push("", ...block("Database", map2));
20425
+ out.push(
20426
+ "",
20427
+ JSON_TYPE,
20428
+ "",
20429
+ "export type Database = {",
20430
+ ...member(
20431
+ " ",
20432
+ "tables",
20433
+ described.filter(({ table }) => table.type !== "view").flatMap(tableEntry)
20434
+ ),
20435
+ ...member(
20436
+ " ",
20437
+ "views",
20438
+ described.filter(({ table }) => table.type === "view").flatMap(viewEntry)
20439
+ ),
20440
+ "};",
20441
+ "",
20442
+ ...ALIASES
20443
+ );
19364
20444
  out.push("", ...block("Env", [" DB: D1Database;"]));
19365
20445
  return `${out.join("\n")}
19366
20446
  `;
@@ -19423,6 +20503,113 @@ function ensureGitignoreHasNaro(dir) {
19423
20503
  return true;
19424
20504
  }
19425
20505
 
20506
+ // ../../packages/sdk/src/lint.ts
20507
+ function byText(left, right) {
20508
+ return left < right ? -1 : left > right ? 1 : 0;
20509
+ }
20510
+ function lintedTables(input) {
20511
+ const virtual = new Set(
20512
+ input.virtualTables.map((name) => name.toLowerCase())
20513
+ );
20514
+ return input.tables.filter(
20515
+ (table) => table.type === "table" && !isSystemTable(table.name) && !virtual.has(table.name.toLowerCase())
20516
+ );
20517
+ }
20518
+ function countLintedTables(input) {
20519
+ return lintedTables(input).length;
20520
+ }
20521
+ function brokenForeignKeys(input) {
20522
+ const groups = /* @__PURE__ */ new Map();
20523
+ for (const violation of input.foreignKeyCheck) {
20524
+ if (isSystemTable(violation.table)) continue;
20525
+ const parents = groups.get(violation.table) ?? /* @__PURE__ */ new Map();
20526
+ const group = parents.get(violation.parent) ?? {
20527
+ rows: /* @__PURE__ */ new Set(),
20528
+ loose: 0
20529
+ };
20530
+ if (violation.rowid === null) group.loose += 1;
20531
+ else group.rows.add(violation.rowid);
20532
+ parents.set(violation.parent, group);
20533
+ groups.set(violation.table, parents);
20534
+ }
20535
+ return [...groups.entries()].toSorted(([left], [right]) => byText(left, right)).flatMap(
20536
+ ([table, parents]) => [...parents.entries()].toSorted(([left], [right]) => byText(left, right)).map(([parent, group]) => ({
20537
+ rule: "broken-foreign-key",
20538
+ severity: "error",
20539
+ object: table,
20540
+ message: `${String(group.rows.size + group.loose)} row(s) point at a ${parent} row that is gone.`
20541
+ }))
20542
+ );
20543
+ }
20544
+ function integrity(input) {
20545
+ return input.quickCheck.filter((line) => line.trim().toLowerCase() !== "ok").map((line) => ({
20546
+ rule: "integrity",
20547
+ severity: "error",
20548
+ object: "database",
20549
+ message: `The database failed SQLite's quick_check: ${line}`
20550
+ }));
20551
+ }
20552
+ function tableIssues(input) {
20553
+ const issues = [];
20554
+ for (const table of lintedTables(input)) {
20555
+ if (table.columns.length === 0) continue;
20556
+ const leading = new Set(
20557
+ table.indexes.flatMap(
20558
+ (index) => index.columns[0] === void 0 ? [] : [index.columns[0].toLowerCase()]
20559
+ )
20560
+ );
20561
+ const keyColumns = table.columns.filter((column) => column.primaryKey);
20562
+ const declared = keyColumns.length === 1 && keyColumns[0]?.type.trim().toUpperCase() === "INTEGER" ? keyColumns[0]?.name.toLowerCase() ?? null : null;
20563
+ const rowidColumn = declared !== null && !leading.has(declared) ? declared : null;
20564
+ if (keyColumns.length === 0)
20565
+ issues.push({
20566
+ rule: "no-primary-key",
20567
+ severity: "warn",
20568
+ object: table.name,
20569
+ message: "The table has no primary key, so its rows are identified by a rowid that changes after a VACUUM or a restore."
20570
+ });
20571
+ for (const column of keyColumns) {
20572
+ if (rowidColumn !== null || column.notNull) continue;
20573
+ issues.push({
20574
+ rule: "nullable-primary-key",
20575
+ severity: "warn",
20576
+ object: `${table.name}.${column.name}`,
20577
+ message: "The primary key column is not INTEGER and has no NOT NULL, so SQLite accepts NULL in it."
20578
+ });
20579
+ }
20580
+ for (const foreignKey of table.foreignKeys) {
20581
+ const from = foreignKey.from.toLowerCase();
20582
+ if (leading.has(from) || from === rowidColumn) continue;
20583
+ issues.push({
20584
+ rule: "unindexed-foreign-key",
20585
+ severity: "warn",
20586
+ object: `${table.name}.${foreignKey.from}`,
20587
+ message: `No index starts with this column, so a DELETE or UPDATE on ${foreignKey.table} scans the whole table.`
20588
+ });
20589
+ }
20590
+ }
20591
+ return issues;
20592
+ }
20593
+ function lintSchema(input) {
20594
+ return [
20595
+ ...brokenForeignKeys(input),
20596
+ ...integrity(input),
20597
+ ...tableIssues(input),
20598
+ ...input.triggers.filter((name) => !isSystemTable(name)).map((name) => ({
20599
+ rule: "trigger-not-reproducible",
20600
+ severity: "warn",
20601
+ object: name,
20602
+ message: "Cloudflare's REST API cannot create a trigger, so db pull writes it out but db push cannot put it back."
20603
+ })),
20604
+ ...input.virtualTables.filter((name) => !isSystemTable(name)).map((name) => ({
20605
+ rule: "virtual-table-blocks-export",
20606
+ severity: "warn",
20607
+ object: name,
20608
+ message: "A virtual table makes db dump refuse the whole database; dump the other tables with --table."
20609
+ }))
20610
+ ];
20611
+ }
20612
+
19426
20613
  // ../../packages/sdk/src/migrations.ts
19427
20614
  import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
19428
20615
  import { resolve as resolve8 } from "path";
@@ -19430,29 +20617,17 @@ function resolveMigrationsDir(input) {
19430
20617
  if (input.dir) return resolve8(input.baseDir, input.dir);
19431
20618
  return resolve8(input.baseDir, input.rootDirectory ?? "", "migrations");
19432
20619
  }
19433
- function normalizeMigrationName(name) {
19434
- return name.trim().toLowerCase().replace(/\.sql$/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
19435
- }
19436
- function leadingNumber(name) {
19437
- const match = /^(\d+)/.exec(name);
19438
- return match ? Number(match[1]) : null;
19439
- }
19440
20620
  function sortMigrationNames(names) {
19441
20621
  return names.toSorted((a, b) => {
19442
- const left = leadingNumber(a);
19443
- const right = leadingNumber(b);
19444
- if (left !== null && right !== null && left !== right) return left - right;
20622
+ const left = leadingMigrationNumber(a);
20623
+ const right = leadingMigrationNumber(b);
20624
+ if (left !== null && right !== null && left !== right)
20625
+ return left < right ? -1 : 1;
19445
20626
  if (left === null && right !== null) return 1;
19446
20627
  if (left !== null && right === null) return -1;
19447
20628
  return a < b ? -1 : a > b ? 1 : 0;
19448
20629
  });
19449
20630
  }
19450
- function highestMigrationNumber(names) {
19451
- return names.reduce(
19452
- (max, name) => Math.max(max, leadingNumber(name) ?? 0),
19453
- 0
19454
- );
19455
- }
19456
20631
  function listMigrationFiles(dir) {
19457
20632
  if (!existsSync5(dir)) return [];
19458
20633
  return sortMigrationNames(
@@ -19466,8 +20641,7 @@ function nextMigrationFileName(existing, name) {
19466
20641
  "VALIDATION_FAILED",
19467
20642
  "Migration name must contain letters or digits."
19468
20643
  );
19469
- const number4 = String(highestMigrationNumber(existing) + 1).padStart(4, "0");
19470
- return `${number4}_${normalized}.sql`;
20644
+ return numberedMigrationName(existing, normalized);
19471
20645
  }
19472
20646
  function compareMigrations(local, applied) {
19473
20647
  const ledger = new Map(applied.map((row) => [row.name, row]));
@@ -19491,7 +20665,7 @@ function compareMigrations(local, applied) {
19491
20665
  });
19492
20666
  continue;
19493
20667
  }
19494
- const number4 = leadingNumber(name);
20668
+ const number4 = leadingMigrationNumber(name);
19495
20669
  rows.push({
19496
20670
  name,
19497
20671
  status: "pending",
@@ -19507,6 +20681,186 @@ function compareMigrations(local, applied) {
19507
20681
  const order = sortMigrationNames(rows.map((row) => row.name));
19508
20682
  return rows.toSorted((a, b) => order.indexOf(a.name) - order.indexOf(b.name));
19509
20683
  }
20684
+ function pendingMigrationNames(local, applied) {
20685
+ return compareMigrations(local, applied).filter((row) => row.status === "pending").map((row) => row.name);
20686
+ }
20687
+ function migrationsPendingError(pending) {
20688
+ return new NaroError(
20689
+ "DATABASE_MIGRATIONS_PENDING",
20690
+ `Run \`naro db push\` first; ${String(pending.length)} local migration(s) are not applied yet: ${pending.join(", ")}.`
20691
+ );
20692
+ }
20693
+ function migrationNameMismatch(sent, recorded) {
20694
+ return `The control plane recorded the migration as ${recorded}, but naro sent ${sent} and wrote the file under that name. Until the two match, naro migration list shows them apart and naro db push would apply the file again.`;
20695
+ }
20696
+
20697
+ // ../../packages/sdk/src/reset-plan.ts
20698
+ var RESET_DEFER_FOREIGN_KEYS = "pragma defer_foreign_keys = true;";
20699
+ var RESET_BATCH_STATEMENTS = 50;
20700
+ function clearInternal(internal) {
20701
+ return [
20702
+ ...internal.sqliteSequence ? ["DELETE FROM sqlite_sequence;"] : [],
20703
+ ...internal.ledger ? ["DELETE FROM d1_migrations;"] : []
20704
+ ];
20705
+ }
20706
+ function quote(name) {
20707
+ return `"${name.replace(/"/g, '""')}"`;
20708
+ }
20709
+ function byName(left, right) {
20710
+ return left < right ? -1 : left > right ? 1 : 0;
20711
+ }
20712
+ function sorted(names) {
20713
+ return [...names].toSorted(byName);
20714
+ }
20715
+ function walk(start, adjacency, seen, onLeave) {
20716
+ seen.add(start);
20717
+ const stack = [{ node: start, next: 0 }];
20718
+ while (stack.length > 0) {
20719
+ const frame = stack[stack.length - 1];
20720
+ if (frame === void 0) break;
20721
+ const neighbours = adjacency.get(frame.node) ?? [];
20722
+ const neighbour = neighbours[frame.next];
20723
+ if (neighbour === void 0) {
20724
+ onLeave(frame.node);
20725
+ stack.pop();
20726
+ continue;
20727
+ }
20728
+ frame.next += 1;
20729
+ if (!seen.has(neighbour)) {
20730
+ seen.add(neighbour);
20731
+ stack.push({ node: neighbour, next: 0 });
20732
+ }
20733
+ }
20734
+ }
20735
+ function components(nodes, edges) {
20736
+ const forward = /* @__PURE__ */ new Map();
20737
+ const backward = /* @__PURE__ */ new Map();
20738
+ for (const node of nodes) {
20739
+ forward.set(node, []);
20740
+ backward.set(node, []);
20741
+ }
20742
+ for (const edge of edges) {
20743
+ forward.get(edge.child)?.push(edge.parent);
20744
+ backward.get(edge.parent)?.push(edge.child);
20745
+ }
20746
+ for (const [node, list] of forward) forward.set(node, list.toSorted(byName));
20747
+ for (const [node, list] of backward)
20748
+ backward.set(node, list.toSorted(byName));
20749
+ const finished = [];
20750
+ const seen = /* @__PURE__ */ new Set();
20751
+ for (const node of nodes)
20752
+ if (!seen.has(node))
20753
+ walk(node, forward, seen, (left) => finished.push(left));
20754
+ const assigned = /* @__PURE__ */ new Set();
20755
+ const found = [];
20756
+ for (let index = finished.length - 1; index >= 0; index--) {
20757
+ const node = finished[index];
20758
+ if (node === void 0 || assigned.has(node)) continue;
20759
+ const members = [];
20760
+ walk(node, backward, assigned, (left) => members.push(left));
20761
+ found.push(sorted(members));
20762
+ }
20763
+ return found;
20764
+ }
20765
+ function tableGroups(names, edges) {
20766
+ const groups = components(names, edges);
20767
+ const indexOf = /* @__PURE__ */ new Map();
20768
+ groups.forEach((group, index) => {
20769
+ for (const member2 of group) indexOf.set(member2, index);
20770
+ });
20771
+ const waitingOn = groups.map(() => 0);
20772
+ const parentsOf = groups.map(() => /* @__PURE__ */ new Set());
20773
+ for (const edge of edges) {
20774
+ const child = indexOf.get(edge.child);
20775
+ const parent = indexOf.get(edge.parent);
20776
+ if (child === void 0 || parent === void 0 || child === parent)
20777
+ continue;
20778
+ if (parentsOf[child]?.has(parent) === true) continue;
20779
+ parentsOf[child]?.add(parent);
20780
+ waitingOn[parent] = (waitingOn[parent] ?? 0) + 1;
20781
+ }
20782
+ const key = (index) => groups[index]?.[0] ?? "";
20783
+ const ready = groups.map((_, index) => index).filter((index) => waitingOn[index] === 0);
20784
+ const order = [];
20785
+ while (ready.length > 0) {
20786
+ ready.sort((left, right) => byName(key(left), key(right)));
20787
+ const next = ready.shift();
20788
+ if (next === void 0) break;
20789
+ const group = groups[next];
20790
+ if (group) order.push(group);
20791
+ for (const parent of parentsOf[next] ?? []) {
20792
+ waitingOn[parent] = (waitingOn[parent] ?? 1) - 1;
20793
+ if (waitingOn[parent] === 0) ready.push(parent);
20794
+ }
20795
+ }
20796
+ for (let index = 0; index < groups.length; index++) {
20797
+ const group = groups[index];
20798
+ if (group && (waitingOn[index] ?? 0) > 0) order.push(group);
20799
+ }
20800
+ return order;
20801
+ }
20802
+ function pack(groups) {
20803
+ const batches = [];
20804
+ let current = [RESET_DEFER_FOREIGN_KEYS];
20805
+ for (const group of groups) {
20806
+ if (current.length > 1 && current.length + group.length > RESET_BATCH_STATEMENTS) {
20807
+ batches.push(current);
20808
+ current = [RESET_DEFER_FOREIGN_KEYS];
20809
+ }
20810
+ current.push(...group);
20811
+ }
20812
+ if (current.length > 1) batches.push(current);
20813
+ return batches;
20814
+ }
20815
+ function nameList(names) {
20816
+ const shown = names.slice(0, 10).join(", ");
20817
+ const rest = names.length - 10;
20818
+ return rest > 0 ? `${shown} and ${String(rest)} more` : shown;
20819
+ }
20820
+ function refuseOversizedCycle(groups) {
20821
+ const room = RESET_BATCH_STATEMENTS - 1;
20822
+ const cycle = groups.find((group) => group.length > room);
20823
+ if (cycle === void 0) return;
20824
+ throw new NaroError(
20825
+ "VALIDATION_FAILED",
20826
+ `A foreign key cycle of ${String(cycle.length)} tables cannot be dropped in one request (the limit is ${String(room)}): ${nameList(cycle)}. Break the cycle or drop those tables by hand.`
20827
+ );
20828
+ }
20829
+ function buildResetPlan(input) {
20830
+ const objects = input.objects.filter((object2) => !isSystemTable(object2.name));
20831
+ const views = sorted(
20832
+ objects.filter((object2) => object2.type === "view").map((object2) => object2.name)
20833
+ );
20834
+ const triggers = sorted(
20835
+ objects.filter((object2) => object2.type === "trigger").map((object2) => object2.name)
20836
+ );
20837
+ const tableNames = sorted(
20838
+ objects.filter((object2) => object2.type === "table" || object2.type === "virtual").map((object2) => object2.name)
20839
+ );
20840
+ const canonical = new Map(tableNames.map((name) => [asciiLower(name), name]));
20841
+ const edges = [];
20842
+ for (const edge of input.foreignKeys) {
20843
+ const child = canonical.get(asciiLower(edge.child));
20844
+ const parent = canonical.get(asciiLower(edge.parent));
20845
+ if (child !== void 0 && parent !== void 0 && child !== parent)
20846
+ edges.push({ child, parent });
20847
+ }
20848
+ const ordered = tableGroups(tableNames, edges);
20849
+ refuseOversizedCycle(ordered);
20850
+ const clear = clearInternal(input.internal);
20851
+ const groups = [
20852
+ ...triggers.map((name) => [`DROP TRIGGER ${quote(name)};`]),
20853
+ ...views.map((name) => [`DROP VIEW ${quote(name)};`]),
20854
+ ...ordered.map(
20855
+ (group) => group.map((name) => `DROP TABLE ${quote(name)};`)
20856
+ ),
20857
+ ...clear.length > 0 ? [clear] : []
20858
+ ];
20859
+ return {
20860
+ batches: pack(groups),
20861
+ dropped: { tables: ordered.flat(), views, triggers }
20862
+ };
20863
+ }
19510
20864
 
19511
20865
  // src/env-consent.ts
19512
20866
  import { existsSync as existsSync6 } from "fs";
@@ -19864,7 +21218,7 @@ async function deployDirectory(sourceDir, options, log2) {
19864
21218
  if (!framework.deployable && !options.force) {
19865
21219
  throw new NaroError(
19866
21220
  "FRAMEWORK_UNSUPPORTED",
19867
- framework.framework === "unknown" ? `No supported framework detected in ${appRoot}. Naro deploys Next.js, Vite, Create React App, and static sites \u2014 run this inside one of those projects, or point at it with --root <dir>.` : `${framework.name} projects are not deployable on Naro yet.`,
21221
+ framework.framework === "unknown" ? `No supported framework detected in ${appRoot}. Naro deploys Next.js, Vite, Create React App, static sites, and static builds of Astro, SvelteKit, Nuxt, Angular, Docusaurus and Gatsby \u2014 run this inside one of those projects, or point at it with --root <dir>.` : `${framework.name} needs a server runtime, which Naro does not deploy yet.`,
19868
21222
  { details: { framework: framework.framework } }
19869
21223
  );
19870
21224
  }
@@ -20010,6 +21364,7 @@ function deploymentUrl(detail, ctx, production) {
20010
21364
 
20011
21365
  export {
20012
21366
  parseGitHubRemote,
21367
+ schemaMigrationTitle,
20013
21368
  projectPath,
20014
21369
  NaroError,
20015
21370
  isNaroError,
@@ -20025,22 +21380,37 @@ export {
20025
21380
  resolveAuth,
20026
21381
  openBrowser,
20027
21382
  isTerminalStatus,
21383
+ isStalledBackend,
21384
+ POLICY_RULES,
21385
+ POLICY_ACTIONS,
20028
21386
  NaroClient,
20029
21387
  BOOKMARK_FORMAT_MESSAGE,
20030
21388
  isBookmark,
20031
21389
  RESTORE_OUTCOME_UNKNOWN_CODES,
21390
+ cellText,
20032
21391
  renderRows,
20033
21392
  summarizeStatement,
20034
21393
  serializeDotenv,
21394
+ systemTablePredicate,
21395
+ parsePolicyNode,
21396
+ parseRedirectEntry,
21397
+ formatRedirectEntry,
21398
+ policiesForTypes,
20035
21399
  generateTypes,
20036
21400
  readProjectLink,
20037
21401
  writeProjectLink,
20038
21402
  removeProjectLink,
20039
21403
  ensureGitignoreHasNaro,
21404
+ countLintedTables,
21405
+ lintSchema,
20040
21406
  resolveMigrationsDir,
20041
21407
  listMigrationFiles,
20042
21408
  nextMigrationFileName,
20043
21409
  compareMigrations,
21410
+ pendingMigrationNames,
21411
+ migrationsPendingError,
21412
+ migrationNameMismatch,
21413
+ buildResetPlan,
20044
21414
  promptForMissingEnv,
20045
21415
  detectGitInfo,
20046
21416
  parseDotenv2 as parseDotenv,