naro-cli 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -320,8 +320,8 @@ function numberedMigrationName(existing, title) {
320
320
  var MAX_SCHEMA_TITLE_LENGTH = 180;
321
321
  function schemaMigrationTitle(operation) {
322
322
  const op = typeof operation.op === "string" ? operation.op.replace(/[A-Z]/g, (letter) => `_${letter}`) : "";
323
- const target = typeof operation.table === "string" ? operation.table : typeof operation.name === "string" ? operation.name : "";
324
- return normalizeMigrationName(`schema ${op} ${target}`).slice(0, MAX_SCHEMA_TITLE_LENGTH).replace(/_+$/, "");
323
+ const target2 = typeof operation.table === "string" ? operation.table : typeof operation.name === "string" ? operation.name : "";
324
+ return normalizeMigrationName(`schema ${op} ${target2}`).slice(0, MAX_SCHEMA_TITLE_LENGTH).replace(/_+$/, "");
325
325
  }
326
326
 
327
327
  // ../../packages/core/src/paths.ts
@@ -1076,8 +1076,8 @@ function defineLazy(object2, key, getter) {
1076
1076
  function objectClone(obj) {
1077
1077
  return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
1078
1078
  }
1079
- function assignProp(target, prop, value) {
1080
- Object.defineProperty(target, prop, {
1079
+ function assignProp(target2, prop, value) {
1080
+ Object.defineProperty(target2, prop, {
1081
1081
  value,
1082
1082
  writable: true,
1083
1083
  enumerable: true,
@@ -1260,35 +1260,35 @@ function normalizeParams(_params) {
1260
1260
  return params;
1261
1261
  }
1262
1262
  function createTransparentProxy(getter) {
1263
- let target;
1263
+ let target2;
1264
1264
  return new Proxy({}, {
1265
1265
  get(_, prop, receiver) {
1266
- target ?? (target = getter());
1267
- return Reflect.get(target, prop, receiver);
1266
+ target2 ?? (target2 = getter());
1267
+ return Reflect.get(target2, prop, receiver);
1268
1268
  },
1269
1269
  set(_, prop, value, receiver) {
1270
- target ?? (target = getter());
1271
- return Reflect.set(target, prop, value, receiver);
1270
+ target2 ?? (target2 = getter());
1271
+ return Reflect.set(target2, prop, value, receiver);
1272
1272
  },
1273
1273
  has(_, prop) {
1274
- target ?? (target = getter());
1275
- return Reflect.has(target, prop);
1274
+ target2 ?? (target2 = getter());
1275
+ return Reflect.has(target2, prop);
1276
1276
  },
1277
1277
  deleteProperty(_, prop) {
1278
- target ?? (target = getter());
1279
- return Reflect.deleteProperty(target, prop);
1278
+ target2 ?? (target2 = getter());
1279
+ return Reflect.deleteProperty(target2, prop);
1280
1280
  },
1281
1281
  ownKeys(_) {
1282
- target ?? (target = getter());
1283
- return Reflect.ownKeys(target);
1282
+ target2 ?? (target2 = getter());
1283
+ return Reflect.ownKeys(target2);
1284
1284
  },
1285
1285
  getOwnPropertyDescriptor(_, prop) {
1286
- target ?? (target = getter());
1287
- return Reflect.getOwnPropertyDescriptor(target, prop);
1286
+ target2 ?? (target2 = getter());
1287
+ return Reflect.getOwnPropertyDescriptor(target2, prop);
1288
1288
  },
1289
1289
  defineProperty(_, prop, descriptor) {
1290
- target ?? (target = getter());
1291
- return Reflect.defineProperty(target, prop, descriptor);
1290
+ target2 ?? (target2 = getter());
1291
+ return Reflect.defineProperty(target2, prop, descriptor);
1292
1292
  }
1293
1293
  });
1294
1294
  }
@@ -11740,15 +11740,15 @@ function _stringFormat(Class2, format, fnOrRegex, _params = {}) {
11740
11740
 
11741
11741
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/to-json-schema.js
11742
11742
  function initializeContext(params) {
11743
- let target = params?.target ?? "draft-2020-12";
11744
- if (target === "draft-4")
11745
- target = "draft-04";
11746
- if (target === "draft-7")
11747
- target = "draft-07";
11743
+ let target2 = params?.target ?? "draft-2020-12";
11744
+ if (target2 === "draft-4")
11745
+ target2 = "draft-04";
11746
+ if (target2 === "draft-7")
11747
+ target2 = "draft-07";
11748
11748
  return {
11749
11749
  processors: params.processors ?? {},
11750
11750
  metadataRegistry: params?.metadata ?? globalRegistry,
11751
- target,
11751
+ target: target2,
11752
11752
  unrepresentable: params?.unrepresentable ?? "throw",
11753
11753
  override: params?.override ?? (() => {
11754
11754
  }),
@@ -12090,8 +12090,8 @@ var createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
12090
12090
  return finalize(ctx, schema);
12091
12091
  };
12092
12092
  var createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
12093
- const { libraryOptions, target } = params ?? {};
12094
- const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
12093
+ const { libraryOptions, target: target2 } = params ?? {};
12094
+ const ctx = initializeContext({ ...libraryOptions ?? {}, target: target2, io, processors });
12095
12095
  process2(schema, ctx);
12096
12096
  extractDefs(ctx, schema);
12097
12097
  return finalize(ctx, schema);
@@ -13150,8 +13150,8 @@ var ZodType = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
13150
13150
  catch(params) {
13151
13151
  return _catch2(this, params);
13152
13152
  },
13153
- pipe(target) {
13154
- return pipe(this, target);
13153
+ pipe(target2) {
13154
+ return pipe(this, target2);
13155
13155
  },
13156
13156
  readonly() {
13157
13157
  return readonly(this);
@@ -17057,14 +17057,14 @@ function createSmokeTester(options = {}) {
17057
17057
  const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
17058
17058
  const allocatePort = options.allocatePort ?? allocateLoopbackPort;
17059
17059
  const pollIntervalMs = options.pollIntervalMs ?? 1e3;
17060
- async function runOnce(request, observer) {
17061
- const stage = request.stage ?? "BUILDING";
17062
- const errorCode2 = request.errorCode ?? "BUILD_FAILED";
17063
- const bootTimeoutMs = request.bootTimeoutMs ?? 9e4;
17064
- const probeTimeoutMs = request.probeTimeoutMs ?? 15e3;
17060
+ async function runOnce(request2, observer) {
17061
+ const stage = request2.stage ?? "BUILDING";
17062
+ const errorCode2 = request2.errorCode ?? "BUILD_FAILED";
17063
+ const bootTimeoutMs = request2.bootTimeoutMs ?? 9e4;
17064
+ const probeTimeoutMs = request2.probeTimeoutMs ?? 15e3;
17065
17065
  const port = await allocatePort();
17066
17066
  const origin = `http://127.0.0.1:${String(port)}`;
17067
- const boot = request.boot(port);
17067
+ const boot = request2.boot(port);
17068
17068
  const recent = [];
17069
17069
  const server = start(
17070
17070
  { ...boot, stage, errorCode: errorCode2 },
@@ -17130,7 +17130,7 @@ function createSmokeTester(options = {}) {
17130
17130
  probes: probes2
17131
17131
  };
17132
17132
  }
17133
- for (const probe of request.probes) {
17133
+ for (const probe of request2.probes) {
17134
17134
  const answer = await fetchStatus(
17135
17135
  fetchImpl,
17136
17136
  `${origin}${probe.path}`,
@@ -17160,15 +17160,15 @@ function createSmokeTester(options = {}) {
17160
17160
  }
17161
17161
  }
17162
17162
  return {
17163
- async run(request, observer) {
17164
- const first = await runOnce(request, observer);
17163
+ async run(request2, observer) {
17164
+ const first = await runOnce(request2, observer);
17165
17165
  if (first.ok || !first.portTaken) return first;
17166
17166
  await observer.onLog({
17167
17167
  level: "WARN",
17168
- stage: request.stage ?? "BUILDING",
17168
+ stage: request2.stage ?? "BUILDING",
17169
17169
  message: "Smoke port was taken before the server bound it; retrying on a fresh port."
17170
17170
  });
17171
- return runOnce(request, observer);
17171
+ return runOnce(request2, observer);
17172
17172
  }
17173
17173
  };
17174
17174
  }
@@ -17906,9 +17906,9 @@ function shouldCopy(path) {
17906
17906
  if (SKIPPED_ANYWHERE.has(name)) return false;
17907
17907
  return !name.startsWith(".next-deploy-");
17908
17908
  }
17909
- async function copyCheckout(source, target) {
17909
+ async function copyCheckout(source, target2) {
17910
17910
  const origin = resolve6(source);
17911
- await cp(origin, target, {
17911
+ await cp(origin, target2, {
17912
17912
  recursive: true,
17913
17913
  verbatimSymlinks: true,
17914
17914
  filter: (path) => resolve6(path) === origin || shouldCopy(path)
@@ -18287,16 +18287,16 @@ var LocalBuildExecutor = class {
18287
18287
  await fork(checkout, pristine);
18288
18288
  let forks = 0;
18289
18289
  const forkWorkspace = async (label2) => {
18290
- const target = join17(
18290
+ const target2 = join17(
18291
18291
  workspace,
18292
18292
  "forks",
18293
18293
  `${String(forks++)}-${label2.replace(/[^a-z0-9-]/gi, "-")}`
18294
18294
  );
18295
- await fork(pristine, target);
18296
- await install(target);
18295
+ await fork(pristine, target2);
18296
+ await install(target2);
18297
18297
  return {
18298
- checkout: target,
18299
- appRoot: forkedAppRoot(checkout, appRoot, target)
18298
+ checkout: target2,
18299
+ appRoot: forkedAppRoot(checkout, appRoot, target2)
18300
18300
  };
18301
18301
  };
18302
18302
  const vinextCommand = this.dependencies.vinextCommand;
@@ -18488,6 +18488,16 @@ function createLogSender(ports) {
18488
18488
  };
18489
18489
  }
18490
18490
 
18491
+ // ../../packages/sdk/src/analytics.ts
18492
+ var ANALYTICS_FILTER_KEYS = [
18493
+ "path",
18494
+ "referrer",
18495
+ "country",
18496
+ "device",
18497
+ "browser",
18498
+ "os"
18499
+ ];
18500
+
18491
18501
  // ../../packages/sdk/src/auth.ts
18492
18502
  import { homedir as homedir2 } from "os";
18493
18503
 
@@ -18917,6 +18927,7 @@ async function resolveAuth(options = {}) {
18917
18927
  saveSession(session, home);
18918
18928
  }
18919
18929
  }
18930
+ let refreshing = null;
18920
18931
  const auth = {
18921
18932
  source: "session",
18922
18933
  user: session.user,
@@ -18929,9 +18940,19 @@ async function resolveAuth(options = {}) {
18929
18940
  );
18930
18941
  }
18931
18942
  if (!isExpired(current)) return current.access_token;
18932
- const refreshed = await refresh(current.refresh_token);
18933
- saveSession(refreshed, home);
18934
- return refreshed.access_token;
18943
+ const token = current.refresh_token;
18944
+ let flight = refreshing;
18945
+ if (!flight || flight.token !== token) {
18946
+ const promise2 = Promise.resolve(token).then(refresh).then((refreshed) => {
18947
+ saveSession(refreshed, home);
18948
+ return refreshed.access_token;
18949
+ }).finally(() => {
18950
+ if (refreshing?.token === token) refreshing = null;
18951
+ });
18952
+ flight = { token, promise: promise2 };
18953
+ refreshing = flight;
18954
+ }
18955
+ return flight.promise;
18935
18956
  }
18936
18957
  };
18937
18958
  return auth;
@@ -18949,6 +18970,23 @@ function openBrowser(url2) {
18949
18970
  child.unref();
18950
18971
  }
18951
18972
 
18973
+ // ../../packages/sdk/src/insights.ts
18974
+ var SPEED_INSIGHTS_METRICS = [
18975
+ "lcp",
18976
+ "inp",
18977
+ "cls",
18978
+ "fcp",
18979
+ "ttfb"
18980
+ ];
18981
+ var SPEED_INSIGHTS_FILTER_KEYS = [
18982
+ "path",
18983
+ "country",
18984
+ "device",
18985
+ "browser",
18986
+ "os"
18987
+ ];
18988
+ var SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES = 20;
18989
+
18952
18990
  // ../../packages/sdk/src/types.ts
18953
18991
  var TERMINAL_DEPLOYMENT_STATUSES = [
18954
18992
  "READY",
@@ -19039,6 +19077,10 @@ var NaroClient = class {
19039
19077
  logs;
19040
19078
  db;
19041
19079
  backend;
19080
+ storage;
19081
+ insights;
19082
+ analytics;
19083
+ protection;
19042
19084
  constructor(options) {
19043
19085
  this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
19044
19086
  this.getAccessToken = options.getAccessToken;
@@ -19053,6 +19095,10 @@ var NaroClient = class {
19053
19095
  this.logs = new LogsApi(this);
19054
19096
  this.db = new DbApi(this);
19055
19097
  this.backend = new BackendApi(this);
19098
+ this.storage = new StorageApi(this);
19099
+ this.insights = new InsightsApi(this);
19100
+ this.analytics = new AnalyticsApi(this);
19101
+ this.protection = new ProtectionApi(this);
19056
19102
  }
19057
19103
  async request(path, options) {
19058
19104
  const token = await this.getAccessToken();
@@ -19323,16 +19369,16 @@ var EnvApi = class {
19323
19369
  );
19324
19370
  }
19325
19371
  /** Removes every row for a key (optionally narrowed to one target). */
19326
- async remove(projectId, key, target) {
19372
+ async remove(projectId, key, target2) {
19327
19373
  const rows = await this.list(projectId);
19328
19374
  const matches = rows.filter(
19329
- (row) => row.key === key && (target === void 0 || row.target === target)
19375
+ (row) => row.key === key && (target2 === void 0 || row.target === target2)
19330
19376
  );
19331
19377
  if (matches.length === 0) {
19332
19378
  throw new NaroError(
19333
19379
  "ENV_NOT_FOUND",
19334
- target ? `No ${key} variable found for the ${target} environment.` : `No ${key} variable found.`,
19335
- { details: { key, ...target ? { target } : {} } }
19380
+ target2 ? `No ${key} variable found for the ${target2} environment.` : `No ${key} variable found.`,
19381
+ { details: { key, ...target2 ? { target: target2 } : {} } }
19336
19382
  );
19337
19383
  }
19338
19384
  for (const row of matches) await this.removeById(projectId, row.id);
@@ -19460,6 +19506,140 @@ var LogsApi = class {
19460
19506
  return body.entries;
19461
19507
  }
19462
19508
  };
19509
+ function appendFilters(params, keys, filters) {
19510
+ if (!filters) return;
19511
+ for (const key of keys) {
19512
+ const value = filters[key];
19513
+ if (value) params.set(key, value);
19514
+ }
19515
+ }
19516
+ function insightsQuery(options) {
19517
+ const params = new URLSearchParams();
19518
+ if (options.range !== void 0) params.set("range", options.range);
19519
+ if (options.metric !== void 0) params.set("metric", options.metric);
19520
+ if (options.environment !== void 0) params.set("env", options.environment);
19521
+ if (options.route !== void 0) params.set("route", options.route);
19522
+ appendFilters(params, SPEED_INSIGHTS_FILTER_KEYS, options.filters);
19523
+ const text2 = params.toString();
19524
+ return text2 ? `?${text2}` : "";
19525
+ }
19526
+ var InsightsApi = class {
19527
+ constructor(client) {
19528
+ this.client = client;
19529
+ }
19530
+ base(projectId) {
19531
+ return `/api/projects/${encodeURIComponent(projectId)}/speed-insights`;
19532
+ }
19533
+ /** The five metrics' summary, and the trend, pages, devices, browsers, operating systems, countries and elements of one. */
19534
+ async report(projectId, options = {}) {
19535
+ return this.client.request(
19536
+ `${this.base(projectId)}${insightsQuery(options)}`,
19537
+ { context: "Read Speed Insights" }
19538
+ );
19539
+ }
19540
+ /** The paths opened under one route, such as /posts/[id]. */
19541
+ async paths(projectId, options) {
19542
+ return this.client.request(
19543
+ `${this.base(projectId)}/paths${insightsQuery(options)}`,
19544
+ { context: "Read Speed Insights paths" }
19545
+ );
19546
+ }
19547
+ /**
19548
+ * The selected metric for each deployment of the chosen environment
19549
+ * (`options.environment`, production by default), newest first.
19550
+ */
19551
+ async deployments(projectId, options = {}) {
19552
+ return this.client.request(
19553
+ `${this.base(projectId)}/deployments${insightsQuery(options)}`,
19554
+ { context: "Read Speed Insights deployments" }
19555
+ );
19556
+ }
19557
+ /**
19558
+ * The report and the deployments, read side by side. Only the report's
19559
+ * failure fails it: deployments that could not be read come back null,
19560
+ * with the reason, as the dashboard keeps the rest of the panel.
19561
+ */
19562
+ async overview(projectId, options = {}) {
19563
+ const [report, deployments] = await Promise.allSettled([
19564
+ this.report(projectId, options),
19565
+ this.deployments(projectId, options)
19566
+ ]);
19567
+ if (report.status === "rejected") throw report.reason;
19568
+ if (deployments.status === "fulfilled" && Array.isArray(deployments.value.deployments))
19569
+ return {
19570
+ report: report.value,
19571
+ deployments: deployments.value.deployments,
19572
+ deploymentsError: null
19573
+ };
19574
+ const error51 = deployments.status === "fulfilled" ? new NaroError(
19575
+ "INTERNAL_ERROR",
19576
+ "Read Speed Insights deployments failed: the control plane answered without a list of deployments."
19577
+ ) : isNaroError(deployments.reason) ? deployments.reason : toNaroError(deployments.reason);
19578
+ return {
19579
+ report: report.value,
19580
+ deployments: null,
19581
+ deploymentsError: { code: error51.code, message: error51.message }
19582
+ };
19583
+ }
19584
+ /** On or off. The answer is what the row says after the write. */
19585
+ async setEnabled(projectId, enabled) {
19586
+ return this.client.request(this.base(projectId), {
19587
+ method: "PUT",
19588
+ body: { enabled },
19589
+ context: enabled ? "Turn on Speed Insights" : "Turn off Speed Insights"
19590
+ });
19591
+ }
19592
+ };
19593
+ var AnalyticsApi = class {
19594
+ constructor(client) {
19595
+ this.client = client;
19596
+ }
19597
+ /** Visitors, page views, the trend, and the pages, referrers, countries, devices, browsers and operating systems behind them. */
19598
+ async report(projectId, options = {}) {
19599
+ const params = new URLSearchParams();
19600
+ if (options.range !== void 0) params.set("range", options.range);
19601
+ appendFilters(params, ANALYTICS_FILTER_KEYS, options.filters);
19602
+ const text2 = params.toString();
19603
+ return this.client.request(
19604
+ `/api/projects/${encodeURIComponent(projectId)}/analytics${text2 ? `?${text2}` : ""}`,
19605
+ { context: "Read Analytics" }
19606
+ );
19607
+ }
19608
+ };
19609
+ var ProtectionApi = class {
19610
+ constructor(client) {
19611
+ this.client = client;
19612
+ }
19613
+ base(projectId) {
19614
+ return `/api/projects/${encodeURIComponent(projectId)}/preview-protection`;
19615
+ }
19616
+ async get(projectId) {
19617
+ return this.client.request(this.base(projectId), {
19618
+ context: "Read preview protection"
19619
+ });
19620
+ }
19621
+ /** On or off. The edge picks it up within about a minute. */
19622
+ async set(projectId, enabled) {
19623
+ return this.client.request(this.base(projectId), {
19624
+ method: "PUT",
19625
+ body: { enabled },
19626
+ context: enabled ? "Turn on preview protection" : "Turn off preview protection"
19627
+ });
19628
+ }
19629
+ /** A new bypass secret, replacing any earlier one. Shown once. */
19630
+ async createBypass(projectId) {
19631
+ return this.client.request(
19632
+ `${this.base(projectId)}/bypass`,
19633
+ { method: "POST", context: "Create a preview bypass secret" }
19634
+ );
19635
+ }
19636
+ async removeBypass(projectId) {
19637
+ return this.client.request(
19638
+ `${this.base(projectId)}/bypass`,
19639
+ { method: "DELETE", context: "Remove the preview bypass secret" }
19640
+ );
19641
+ }
19642
+ };
19463
19643
  var EXPORT_FIRST_POLL_MS = 1e3;
19464
19644
  var EXPORT_MAX_POLL_MS = 5e3;
19465
19645
  var EXPORT_TIMEOUT_MS = 15 * 6e4;
@@ -19549,8 +19729,8 @@ var DbApi = class {
19549
19729
  * the migration ledger alike - and cancels queries in flight. Restoring to
19550
19730
  * `previousBookmark` undoes it.
19551
19731
  */
19552
- async restore(projectId, target) {
19553
- const given = target;
19732
+ async restore(projectId, target2) {
19733
+ const given = target2;
19554
19734
  const body = await this.client.request(
19555
19735
  this.path(projectId, "/restore"),
19556
19736
  {
@@ -19716,6 +19896,39 @@ async function authChange(answer) {
19716
19896
  }
19717
19897
  return { auth: authConfig(body.auth), backend: backendInfo(body.backend) };
19718
19898
  }
19899
+ function storageBucket(body) {
19900
+ return {
19901
+ name: body.name,
19902
+ public: body.public,
19903
+ fileSizeLimit: body.fileSizeLimit ?? null,
19904
+ allowedMimeTypes: body.allowedMimeTypes ?? null,
19905
+ objects: body.objects,
19906
+ bytes: body.bytes,
19907
+ createdAt: body.createdAt,
19908
+ updatedAt: body.updatedAt,
19909
+ deletingAt: body.deletingAt ?? null
19910
+ };
19911
+ }
19912
+ function storageInfo(body) {
19913
+ return {
19914
+ enabled: body.enabled,
19915
+ bucket: body.bucket ?? null,
19916
+ limits: {
19917
+ bytes: body.limits.bytes,
19918
+ objectBytes: body.limits.objectBytes,
19919
+ singleUploadBytes: body.limits.singleUploadBytes,
19920
+ dailyOps: body.limits.dailyOps ?? null
19921
+ },
19922
+ usage: body.usage ? { bytes: body.usage.bytes, objects: body.usage.objects } : null,
19923
+ today: body.today ?? null,
19924
+ buckets: (body.buckets ?? []).map(storageBucket)
19925
+ };
19926
+ }
19927
+ function bucketFields(fields, into = {}) {
19928
+ for (const key of ["public", "fileSizeLimit", "allowedMimeTypes"])
19929
+ if (fields[key] !== void 0) into[key] = fields[key];
19930
+ return into;
19931
+ }
19719
19932
  var BackendApi = class {
19720
19933
  constructor(client) {
19721
19934
  this.client = client;
@@ -19880,10 +20093,10 @@ var BackendApi = class {
19880
20093
  );
19881
20094
  }
19882
20095
  /** Removes email or one provider; the same answer as `setAuth`. */
19883
- async removeAuth(projectId, target) {
20096
+ async removeAuth(projectId, target2) {
19884
20097
  return authChange(
19885
20098
  this.client.request(
19886
- this.path(projectId, `/auth?target=${encodeURIComponent(target)}`),
20099
+ this.path(projectId, `/auth?target=${encodeURIComponent(target2)}`),
19887
20100
  {
19888
20101
  method: "DELETE",
19889
20102
  context: "Change sign-in settings",
@@ -20054,6 +20267,193 @@ var BackendApi = class {
20054
20267
  return `/rls/policies/${encodeURIComponent(table)}/${encodeURIComponent(name)}`;
20055
20268
  }
20056
20269
  };
20270
+ var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
20271
+ var StorageApi = class {
20272
+ constructor(client) {
20273
+ this.client = client;
20274
+ }
20275
+ path(projectId, suffix = "") {
20276
+ return `/api/projects/${encodeURIComponent(projectId)}/backend/storage${suffix}`;
20277
+ }
20278
+ /**
20279
+ * `encodeURIComponent` leaves `.` and `..` as they are and URL parsing
20280
+ * folds them (`/buckets/..` is `…/backend/storage` itself), an empty
20281
+ * name is the collection, and `encodeURIComponent` throws on a lone
20282
+ * surrogate ("URI malformed", which surfaced as INTERNAL_ERROR — PR #90
20283
+ * review 1): refused here, in naro-js's words for a name no URL can carry
20284
+ * (its `encodeBucket`). No bucket can have one of them.
20285
+ */
20286
+ bucketPath(projectId, bucket, suffix = "") {
20287
+ if (bucket === "" || bucket === "." || bucket === ".." || LONE_SURROGATE.test(bucket))
20288
+ throw new NaroError("INVALID_PATH", "The bucket name is not valid.");
20289
+ return this.path(
20290
+ projectId,
20291
+ `/buckets/${encodeURIComponent(bucket)}${suffix}`
20292
+ );
20293
+ }
20294
+ /**
20295
+ * Whether Storage is on, its R2 bucket, the limits its worker enforces,
20296
+ * what is stored and the buckets. A project that never turned it on answers
20297
+ * `enabled: false` with no buckets.
20298
+ */
20299
+ async info(projectId) {
20300
+ const body = await this.client.request(
20301
+ this.path(projectId),
20302
+ { context: "Read storage" }
20303
+ );
20304
+ return storageInfo(body.storage);
20305
+ }
20306
+ /**
20307
+ * Turns Storage on: the project's R2 bucket (named after the project, so
20308
+ * a repeat finds it), the Naro worker uploaded again with its binding,
20309
+ * the plan's limits recorded and bound with it — again after a plan
20310
+ * change. Safe to repeat, and a repair. The project must have a backend,
20311
+ * turned on, with its database. A backend in `error` or with a run that
20312
+ * stalled is not refused: its provisioning is resumed on the way (with
20313
+ * the bucket's binding). Two minutes, like `backend.enable`.
20314
+ *
20315
+ * Refusals, in the order they are checked: `BACKEND_DISABLED` (the
20316
+ * control plane's backend switch, before the role), `FORBIDDEN`,
20317
+ * `STORAGE_DISABLED` (the control plane's Storage switch),
20318
+ * `DATABASE_NOT_ENABLED` (the project's database is external),
20319
+ * `BACKEND_NOT_READY` (409, only when the project has no backend at all),
20320
+ * `BACKEND_OFF`, `BACKEND_BUSY`, `STORAGE_ENABLE_FAILED` (502: the R2
20321
+ * bucket could not be made — or, on a project that has Storage already, a
20322
+ * plan change's new limits could not be saved; nothing changed), and
20323
+ * `BACKEND_PROVISION_FAILED` (502) when Storage is recorded as on but the
20324
+ * worker's upload stopped. The last one's `details` hold a
20325
+ * `StorageProvisionFailure` — `{ storage: { applied: false, enabled: true
20326
+ * }, backend }`, the backend read like `backend.info` — and `enable`
20327
+ * again finishes.
20328
+ *
20329
+ * A 200 can come back while the backend is still `provisioning` — when
20330
+ * another run (the stall sweep) took the work over before this one
20331
+ * finished. Then `usage` is `null` (it is summed only on a ready backend);
20332
+ * `backend.info` says how far the run got.
20333
+ */
20334
+ async enable(projectId) {
20335
+ let body;
20336
+ try {
20337
+ body = await this.client.request(
20338
+ this.path(projectId),
20339
+ {
20340
+ method: "POST",
20341
+ context: "Turn on Storage",
20342
+ timeoutMs: PROVISION_TIMEOUT_MS
20343
+ }
20344
+ );
20345
+ } catch (error51) {
20346
+ const details = isNaroError(error51) ? error51.details : void 0;
20347
+ if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
20348
+ throw new NaroError(error51.code, error51.message, {
20349
+ ...error51.status !== void 0 ? { status: error51.status } : {},
20350
+ details: {
20351
+ ...details,
20352
+ backend: backendInfo(details.backend)
20353
+ }
20354
+ });
20355
+ throw error51;
20356
+ }
20357
+ return storageInfo(body.storage);
20358
+ }
20359
+ /** Every bucket, by name. `[]` where there can be none (Storage not on). */
20360
+ async listBuckets(projectId) {
20361
+ const body = await this.client.request(
20362
+ this.path(projectId, "/buckets"),
20363
+ { context: "List buckets" }
20364
+ );
20365
+ return (body.buckets ?? []).map(storageBucket);
20366
+ }
20367
+ /**
20368
+ * Makes a bucket. `BUCKET_EXISTS` (409) for a taken name, `BUCKET_LIMIT`
20369
+ * (400) past 100 buckets, `STORAGE_NOT_ENABLED` (409) before `enable`; a
20370
+ * name, limit or type list the control plane does not accept is
20371
+ * `VALIDATION_FAILED` with its reason.
20372
+ */
20373
+ async createBucket(projectId, input) {
20374
+ const body = await this.client.request(
20375
+ this.path(projectId, "/buckets"),
20376
+ {
20377
+ method: "POST",
20378
+ body: bucketFields(input, { name: input.name }),
20379
+ context: "Create a bucket"
20380
+ }
20381
+ );
20382
+ return storageBucket(body.bucket);
20383
+ }
20384
+ /** One bucket; `BUCKET_NOT_FOUND` (404) when there is none of that name. */
20385
+ async getBucket(projectId, bucket) {
20386
+ const body = await this.client.request(
20387
+ this.bucketPath(projectId, bucket),
20388
+ { context: "Read a bucket" }
20389
+ );
20390
+ return storageBucket(body.bucket);
20391
+ }
20392
+ /**
20393
+ * Changes the fields given and returns the bucket as stored. `{}` is
20394
+ * `VALIDATION_FAILED` "Set at least one of public, fileSizeLimit or
20395
+ * allowedMimeTypes." A bucket cannot be renamed.
20396
+ */
20397
+ async updateBucket(projectId, bucket, patch) {
20398
+ const body = await this.client.request(
20399
+ this.bucketPath(projectId, bucket),
20400
+ {
20401
+ method: "PATCH",
20402
+ body: bucketFields(patch),
20403
+ context: "Change a bucket"
20404
+ }
20405
+ );
20406
+ return storageBucket(body.bucket);
20407
+ }
20408
+ /**
20409
+ * Removes a bucket. Without `empty` only an empty one goes
20410
+ * (`BUCKET_NOT_EMPTY`, 409 — its message names the `empty: true` form:
20411
+ * a row whose bytes never reached R2, like a dead upload's, outlives
20412
+ * `emptyBucket`, and only this removal clears it), and that works while
20413
+ * the backend is off. With
20414
+ * `empty: true` the bucket is closed to new objects, emptied through the
20415
+ * project's Naro worker and then removed (the backend must be on and
20416
+ * ready; `BACKEND_UNAVAILABLE` if the worker cannot be reached): `{ ok:
20417
+ * true }` once it is gone, or, for a bucket too big for one call
20418
+ * (answered 202), where it stopped — the same call again goes on. The
20419
+ * emptying form waits two minutes like `enable`: the control plane stops
20420
+ * starting pages after 20 s, but a last one can run 25 s more, plus the
20421
+ * calls that mark and delete the bucket, which would not fit 45 s.
20422
+ */
20423
+ async removeBucket(projectId, bucket, options = {}) {
20424
+ const body = await this.client.request(
20425
+ this.bucketPath(
20426
+ projectId,
20427
+ bucket,
20428
+ options.empty === true ? "?empty=true" : ""
20429
+ ),
20430
+ {
20431
+ method: "DELETE",
20432
+ context: "Remove a bucket",
20433
+ ...options.empty === true ? { timeoutMs: PROVISION_TIMEOUT_MS } : {}
20434
+ }
20435
+ );
20436
+ if (body.done === false)
20437
+ return {
20438
+ deleted: body.deleted ?? 0,
20439
+ done: false,
20440
+ ...typeof body.message === "string" ? { message: body.message } : {}
20441
+ };
20442
+ return { ok: true };
20443
+ }
20444
+ /**
20445
+ * Deletes up to 1,000 of the bucket's objects through the project's Naro
20446
+ * worker, which must be reachable (the backend on and ready). Ask again
20447
+ * until `done`.
20448
+ */
20449
+ async emptyBucket(projectId, bucket) {
20450
+ const body = await this.client.request(
20451
+ this.bucketPath(projectId, bucket, "/empty"),
20452
+ { method: "POST", context: "Empty a bucket" }
20453
+ );
20454
+ return { deleted: body.deleted, done: body.done };
20455
+ }
20456
+ };
20057
20457
 
20058
20458
  // ../../packages/sdk/src/database.ts
20059
20459
  var BOOKMARK_RE = /^[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{32}$/;
@@ -20399,6 +20799,105 @@ function detectFramework2(dir) {
20399
20799
  };
20400
20800
  }
20401
20801
 
20802
+ // ../../packages/data-core/src/storage-path.ts
20803
+ var MAX_SINGLE_UPLOAD_BYTES = 52428800;
20804
+ var STORAGE_RESERVED_WORDS = [
20805
+ "authenticated",
20806
+ "copy",
20807
+ "info",
20808
+ "list",
20809
+ "move",
20810
+ "public",
20811
+ "render",
20812
+ "sign",
20813
+ "upload"
20814
+ ];
20815
+ var RESERVED = new Set(STORAGE_RESERVED_WORDS);
20816
+ var ENCODER = new TextEncoder();
20817
+ var LONE_SURROGATE2 = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
20818
+ var LONE_SURROGATES = new RegExp(LONE_SURROGATE2.source, "g");
20819
+ function objectParent(name) {
20820
+ const slash = name.lastIndexOf("/");
20821
+ return slash < 0 ? "" : name.slice(0, slash);
20822
+ }
20823
+ var TOKEN_CHARACTERS = "[!#$%&'*+\\-.^_`|~0-9A-Za-z]";
20824
+ var TOKEN = new RegExp(`^${TOKEN_CHARACTERS}+$`);
20825
+ var MEDIA_TYPE = new RegExp(`^${TOKEN_CHARACTERS}+/${TOKEN_CHARACTERS}+$`);
20826
+ var DEFAULT_MAX_AGE = 3600;
20827
+ var MAX_MAX_AGE = 31536e3;
20828
+ function parseCacheControl(header) {
20829
+ if (header === null) return { ok: true, maxAge: DEFAULT_MAX_AGE };
20830
+ const match = /^[ \t]*max-age=(\d{1,8})[ \t]*$/i.exec(header);
20831
+ if (match?.[1] === void 0) return { ok: false };
20832
+ const maxAge = Number(match[1]);
20833
+ return maxAge <= MAX_MAX_AGE ? { ok: true, maxAge } : { ok: false };
20834
+ }
20835
+ var MAX_NAME_IN_MESSAGE = 256;
20836
+ function storageName(text2) {
20837
+ if (text2.length <= MAX_NAME_IN_MESSAGE) return text2;
20838
+ const cut = text2.slice(0, MAX_NAME_IN_MESSAGE);
20839
+ const last = cut.charCodeAt(MAX_NAME_IN_MESSAGE - 1);
20840
+ return last >= 55296 && last <= 56319 ? cut.slice(0, MAX_NAME_IN_MESSAGE - 1) : cut;
20841
+ }
20842
+
20843
+ // ../../packages/data-core/src/storage-table.ts
20844
+ var STORAGE_OBJECTS = "storage.objects";
20845
+ var STORAGE_OBJECTS_TABLE = "_naro_storage_objects";
20846
+ var STORAGE_OBJECTS_QUALIFIER = "objects";
20847
+ var STORAGE_UNAVAILABLE_MESSAGE = "storage.objects is not available on this backend yet. Run `naro backend enable` to update it.";
20848
+ var STORAGE_BUCKETS_TABLE = "_naro_storage_buckets";
20849
+ var STORAGE_POLICY_COLUMNS = [
20850
+ "id",
20851
+ "bucket_id",
20852
+ "name",
20853
+ "owner",
20854
+ "owner_id",
20855
+ "metadata",
20856
+ "version",
20857
+ "created_at",
20858
+ "updated_at"
20859
+ ];
20860
+ var STORAGE_TABLES_SQL = [
20861
+ `CREATE TABLE IF NOT EXISTS _naro_storage_buckets (
20862
+ id TEXT PRIMARY KEY,
20863
+ public INTEGER NOT NULL DEFAULT 0 CHECK (public IN (0, 1)),
20864
+ file_size_limit INTEGER CHECK (file_size_limit IS NULL OR file_size_limit > 0),
20865
+ allowed_mime_types TEXT,
20866
+ object_count INTEGER NOT NULL DEFAULT 0,
20867
+ total_bytes INTEGER NOT NULL DEFAULT 0,
20868
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
20869
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
20870
+ )`,
20871
+ `CREATE TABLE IF NOT EXISTS _naro_storage_objects (
20872
+ id TEXT PRIMARY KEY,
20873
+ bucket_id TEXT NOT NULL REFERENCES _naro_storage_buckets (id),
20874
+ name TEXT NOT NULL,
20875
+ owner TEXT,
20876
+ owner_id TEXT,
20877
+ metadata TEXT,
20878
+ version TEXT,
20879
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
20880
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
20881
+ parent TEXT NOT NULL,
20882
+ size INTEGER NOT NULL,
20883
+ status TEXT NOT NULL CHECK (status IN ('uploading', 'ready', 'deleting')),
20884
+ lock_id TEXT,
20885
+ lock_expires_at TEXT,
20886
+ pending TEXT,
20887
+ UNIQUE (bucket_id, name)
20888
+ )`,
20889
+ "CREATE INDEX IF NOT EXISTS _naro_storage_objects_parent ON _naro_storage_objects (bucket_id, parent, name)",
20890
+ "CREATE INDEX IF NOT EXISTS _naro_storage_objects_lock ON _naro_storage_objects (lock_expires_at) WHERE lock_expires_at IS NOT NULL",
20891
+ "CREATE TRIGGER IF NOT EXISTS _naro_storage_objects_count_insert AFTER INSERT ON _naro_storage_objects BEGIN UPDATE _naro_storage_buckets SET object_count = object_count + 1, total_bytes = total_bytes + NEW.size WHERE id = NEW.bucket_id; END",
20892
+ "CREATE TRIGGER IF NOT EXISTS _naro_storage_objects_count_delete AFTER DELETE ON _naro_storage_objects BEGIN UPDATE _naro_storage_buckets SET object_count = object_count - 1, total_bytes = total_bytes - OLD.size WHERE id = OLD.bucket_id; END",
20893
+ "CREATE TRIGGER IF NOT EXISTS _naro_storage_objects_count_size AFTER UPDATE OF size ON _naro_storage_objects BEGIN UPDATE _naro_storage_buckets SET total_bytes = total_bytes + NEW.size - OLD.size WHERE id = NEW.bucket_id; END"
20894
+ ];
20895
+ var STORAGE_BUCKET_DELETING_COLUMN = {
20896
+ table: STORAGE_BUCKETS_TABLE,
20897
+ column: "deleting_at",
20898
+ definition: "TEXT"
20899
+ };
20900
+
20402
20901
  // ../../packages/data-core/src/system-tables.ts
20403
20902
  var SYSTEM_TABLE_PREFIXES = [
20404
20903
  "_naro_",
@@ -20434,6 +20933,7 @@ var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
20434
20933
 
20435
20934
  // ../../packages/data-core/src/errors.ts
20436
20935
  var DATABASE_FULL_MESSAGE = "The database has reached D1's 10 GB limit, which cannot be raised. Delete data to free space; writes fail until it is under the limit.";
20936
+ var STORAGE_MIGRATION_PENDING_MESSAGE = "The project backend is partway through an update. Run `naro backend enable` to finish it, then try again.";
20437
20937
  var CATALOG = {
20438
20938
  INVALID_API_KEY: {
20439
20939
  status: 401,
@@ -20517,9 +21017,9 @@ var CATALOG = {
20517
21017
  if (recursion)
20518
21018
  return `Infinite recursion detected in policy for relation ${table}.`;
20519
21019
  if (values)
20520
- return `The policies of ${table} bind more than 50 values in one statement.`;
21020
+ return `The policies of ${table} bind more than ${values === true ? "50" : String(values)} values in one statement.`;
20521
21021
  if (bytes)
20522
- return `The policies of ${table} write more than 50000 bytes of SQL into one statement.`;
21022
+ return `The policies of ${table} write more than ${bytes === true ? "50000" : String(bytes)} bytes of SQL into one statement.`;
20523
21023
  if (policy !== void 0)
20524
21024
  return `Policy ${policy} on ${table} cannot be applied.`;
20525
21025
  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.`;
@@ -20539,6 +21039,87 @@ var CATALOG = {
20539
21039
  DATABASE_FULL: {
20540
21040
  status: 507,
20541
21041
  message: () => DATABASE_FULL_MESSAGE
21042
+ },
21043
+ // Naro Storage (3-1a, first-release design §10). A path, a bucket or a type
21044
+ // is the caller's own text, so each is cut to 256 characters in the message
21045
+ // (storageName); nothing is shortened in `details`.
21046
+ INVALID_PATH: {
21047
+ status: 400,
21048
+ message: (vars) => "bucket" in vars ? "The bucket name is not valid." : `The object path is not valid: it ${vars.reason}.`
21049
+ },
21050
+ STORAGE_NOT_ENABLED: {
21051
+ status: 404,
21052
+ message: () => "Storage is not enabled for this project."
21053
+ },
21054
+ BUCKET_NOT_FOUND: {
21055
+ status: 404,
21056
+ message: ({ bucket }) => `Bucket ${storageName(bucket)} was not found.`
21057
+ },
21058
+ OBJECT_NOT_FOUND: {
21059
+ status: 404,
21060
+ message: ({ bucket, path }) => `Object ${storageName(path)} was not found in bucket ${storageName(bucket)}.`
21061
+ },
21062
+ INVALID_CONTENT_TYPE: {
21063
+ status: 400,
21064
+ message: ({ type }) => `Content-Type ${storageName(type)} is not a media type like image/png.`
21065
+ },
21066
+ INVALID_REQUEST: {
21067
+ status: 400,
21068
+ message: () => "The storage request is not valid."
21069
+ },
21070
+ LENGTH_REQUIRED: {
21071
+ status: 411,
21072
+ message: () => "Uploads need a Content-Length header."
21073
+ },
21074
+ OBJECT_TOO_LARGE: {
21075
+ status: 413,
21076
+ message: (vars) => "single" in vars ? "Uploads over 50 MiB are not supported yet." : `The object is ${String(vars.size)} bytes; bucket ${storageName(vars.bucket)} takes objects up to ${String(vars.max)} bytes.`
21077
+ },
21078
+ MIME_TYPE_NOT_ALLOWED: {
21079
+ status: 415,
21080
+ message: ({ bucket, type }) => `Bucket ${storageName(bucket)} does not take ${storageName(type)} objects.`
21081
+ },
21082
+ RANGE_NOT_SATISFIABLE: {
21083
+ status: 416,
21084
+ message: ({ size }) => `The range is outside the object's ${String(size)} bytes.`
21085
+ },
21086
+ OBJECT_EXISTS: {
21087
+ status: 409,
21088
+ message: ({ bucket, path }) => `Object ${storageName(path)} already exists in bucket ${storageName(bucket)}. Upload with upsert to replace it.`
21089
+ },
21090
+ OBJECT_BUSY: {
21091
+ status: 409,
21092
+ message: ({ bucket, path }) => `Object ${storageName(path)} in bucket ${storageName(bucket)} is being changed by another request. Try again shortly.`
21093
+ },
21094
+ INVALID_SIGNATURE: {
21095
+ status: 400,
21096
+ message: () => "The signed URL is not valid."
21097
+ },
21098
+ SIGNED_URL_EXPIRED: {
21099
+ status: 400,
21100
+ message: () => "The signed URL has expired."
21101
+ },
21102
+ OBJECT_RATE_LIMITED: {
21103
+ status: 429,
21104
+ message: ({ path }) => `Object ${storageName(path)} was written less than a second ago. Try again in a second.`
21105
+ },
21106
+ // 507 like DATABASE_FULL: no retry helps until objects are deleted. Reads
21107
+ // and deletes still work.
21108
+ STORAGE_QUOTA_EXCEEDED: {
21109
+ status: 507,
21110
+ message: () => "This project has used all of its storage. Delete objects to free space; uploads fail until then."
21111
+ },
21112
+ // R2 or its binding did not answer; the original error is logged, never sent.
21113
+ STORAGE_UNAVAILABLE: {
21114
+ status: 503,
21115
+ message: () => "The storage backend did not answer. Try again."
21116
+ },
21117
+ // Storage 3-1c (known-issues #114): see ErrorVars. 409 like the control
21118
+ // plane's BACKEND_OUTDATED: nothing the caller sent is wrong, and it
21119
+ // clears once the run finishes.
21120
+ BACKEND_OUTDATED: {
21121
+ status: 409,
21122
+ message: () => STORAGE_MIGRATION_PENDING_MESSAGE
20542
21123
  }
20543
21124
  };
20544
21125
  function dataError(code, ...args) {
@@ -20555,6 +21136,9 @@ function dataError(code, ...args) {
20555
21136
  function invalidQuery(path, reason) {
20556
21137
  return dataError("INVALID_QUERY", void 0, { path, reason });
20557
21138
  }
21139
+ var STORAGE_MIGRATION_PENDING = new RegExp(
21140
+ `^no such column: (?:b\\.|${STORAGE_BUCKET_DELETING_COLUMN.table}\\.)?${STORAGE_BUCKET_DELETING_COLUMN.column}\\b`
21141
+ );
20558
21142
 
20559
21143
  // ../../packages/data-core/src/d1.ts
20560
21144
  function rowsOf(result) {
@@ -20958,7 +21542,8 @@ var SCHEMA_SQL = [
20958
21542
  // alone a column's collation is written. A separate statement, so the
20959
21543
  // text comes once per table rather than once per column. D1 answers
20960
21544
  // `sqlite_master` reads (the database routes' `_tables.ts` reads it too).
20961
- `SELECT name AS table_name, sql AS table_sql FROM sqlite_master WHERE type = 'table' AND ${systemTablePredicate("name")}`
21545
+ `SELECT name AS table_name, sql AS table_sql FROM sqlite_master WHERE type = 'table' AND ${systemTablePredicate("name")}`,
21546
+ `SELECT c.cid AS storage_cid, c.name AS storage_column, c.type AS storage_type, c."notnull" AS storage_not_null, c.dflt_value AS storage_default, c.pk AS storage_pk, CASE WHEN c.cid = 0 THEN (SELECT sql FROM sqlite_master WHERE type = 'table' AND name = '${STORAGE_OBJECTS_TABLE}') END AS storage_sql FROM pragma_table_xinfo('${STORAGE_OBJECTS_TABLE}') AS c ORDER BY c.cid`
20962
21547
  ];
20963
21548
  var TABLE_NAMES_SQL = `SELECT name AS table_name FROM pragma_table_list WHERE schema = 'main' AND type IN ('table', 'virtual') AND ${systemTablePredicate("name")}`;
20964
21549
  function tableNamesFromRows(rows) {
@@ -20987,7 +21572,8 @@ function snapshotFromRows(rows) {
20987
21572
  if (Object.hasOwn(row, "table_sql") && typeof row.table_name === "string")
20988
21573
  ddl.set(row.table_name, row.table_sql);
20989
21574
  for (const row of rows) {
20990
- if (Object.hasOwn(row, "table_sql")) continue;
21575
+ if (Object.hasOwn(row, "table_sql") || Object.hasOwn(row, "storage_column"))
21576
+ continue;
20991
21577
  const tableName = text(row.table_name);
20992
21578
  const kind = row.table_type;
20993
21579
  if (tableName === "" || kind !== "table" && kind !== "virtual") continue;
@@ -21038,11 +21624,54 @@ function snapshotFromRows(rows) {
21038
21624
  sql: table.kind === "table" && typeof sql === "string" ? sql : null
21039
21625
  });
21040
21626
  }
21041
- return { tables };
21627
+ const storage = storageFromRows(rows);
21628
+ return storage ? { tables, storage } : { tables };
21629
+ }
21630
+ var STORAGE_POLICY_COLUMN_SET = new Set(
21631
+ STORAGE_POLICY_COLUMNS
21632
+ );
21633
+ function storageFromRows(rows) {
21634
+ const storageRows = rows.filter(
21635
+ (row) => Object.hasOwn(row, "storage_column")
21636
+ );
21637
+ const columns = [];
21638
+ for (const row of storageRows) {
21639
+ const name = text(row.storage_column);
21640
+ if (!STORAGE_POLICY_COLUMN_SET.has(name)) continue;
21641
+ const defaultValue = row.storage_default;
21642
+ columns.push({
21643
+ name,
21644
+ cid: integer2(row.storage_cid),
21645
+ type: text(row.storage_type),
21646
+ notNull: integer2(row.storage_not_null) === 1,
21647
+ primaryKey: integer2(row.storage_pk),
21648
+ defaultValue: defaultValue === null || defaultValue === void 0 ? null : String(defaultValue),
21649
+ generated: false,
21650
+ collation: "BINARY"
21651
+ });
21652
+ }
21653
+ if (columns.length === 0) return void 0;
21654
+ const sorted2 = columns.toSorted((left, right) => left.cid - right.cid);
21655
+ const sql = storageRows.find(
21656
+ (row) => integer2(row.storage_cid) === 0
21657
+ )?.storage_sql;
21658
+ return {
21659
+ name: STORAGE_OBJECTS_TABLE,
21660
+ kind: "table",
21661
+ columns: sorted2,
21662
+ byName: new Map(sorted2.map((column2) => [asciiLower(column2.name), column2])),
21663
+ sql: typeof sql === "string" ? sql : null
21664
+ };
21042
21665
  }
21043
21666
  function resolveTable(snapshot, name) {
21044
21667
  if (isSystemTable(name)) return void 0;
21045
- return snapshot.tables.get(asciiLower(name));
21668
+ const folded = asciiLower(name);
21669
+ if (folded === STORAGE_OBJECTS) return void 0;
21670
+ return snapshot.tables.get(folded);
21671
+ }
21672
+ function resolvePolicyTable(snapshot, name) {
21673
+ if (asciiLower(name) === STORAGE_OBJECTS) return snapshot.storage;
21674
+ return resolveTable(snapshot, name);
21046
21675
  }
21047
21676
  async function readTableNames(db) {
21048
21677
  return tableNamesFromRows(rowsOf(await db.prepare(TABLE_NAMES_SQL).all()));
@@ -21119,15 +21748,15 @@ function patternTooLong(pattern, caseInsensitive) {
21119
21748
  if (pattern.length > MAX_PATTERN_BYTES * 2) return true;
21120
21749
  return utf8ByteLength(boundPattern(pattern, caseInsensitive)) > MAX_PATTERN_BYTES;
21121
21750
  }
21122
- function isBoolean(target, value) {
21123
- return `coalesce(CAST(${target} AS NUMERIC) ${value ? "<>" : "="} 0, 0)`;
21751
+ function isBoolean(target2, value) {
21752
+ return `coalesce(CAST(${target2} AS NUMERIC) ${value ? "<>" : "="} 0, 0)`;
21124
21753
  }
21125
- var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
21754
+ var LONE_SURROGATE3 = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
21126
21755
  function distinctArrayValues(values) {
21127
21756
  const seen = /* @__PURE__ */ new Set();
21128
21757
  const out = [];
21129
21758
  for (const item of values) {
21130
- const bound = typeof item === "string" ? item.replace(LONE_SURROGATE, "\uFFFD") : item;
21759
+ const bound = typeof item === "string" ? item.replace(LONE_SURROGATE3, "\uFFFD") : item;
21131
21760
  const key = `${typeof bound}:${String(bound)}`;
21132
21761
  if (seen.has(key)) continue;
21133
21762
  seen.add(key);
@@ -21137,12 +21766,12 @@ function distinctArrayValues(values) {
21137
21766
  }
21138
21767
  var MAX_ARRAY_TEXT_LENGTH = 65536;
21139
21768
  var ELEMENT = "_naro_j";
21140
- function arraySql(target, operator, count2, bind2, nullElement = false) {
21141
- const isNull = target();
21142
- const length = target();
21143
- const valid = target();
21144
- const type = target();
21145
- const each = () => `json_each(${target()}) AS ${ELEMENT}`;
21769
+ function arraySql(target2, operator, count2, bind2, nullElement = false) {
21770
+ const isNull = target2();
21771
+ const length = target2();
21772
+ const valid = target2();
21773
+ const type = target2();
21774
+ const each = () => `json_each(${target2()}) AS ${ELEMENT}`;
21146
21775
  const element = `${ELEMENT}.value`;
21147
21776
  const list = () => `(${Array.from({ length: count2 }, bind2).join(", ")})`;
21148
21777
  let test;
@@ -21253,13 +21882,16 @@ var POLICY_FUNCTIONS = [
21253
21882
  "coalesce",
21254
21883
  "now"
21255
21884
  ];
21885
+ var STORAGE_HELPERS = ["foldername", "filename", "extension"];
21886
+ var MAX_FOLDERNAME_INDEX = 16;
21256
21887
  var VALUE_KINDS = /* @__PURE__ */ new Set([
21257
21888
  "literal",
21258
21889
  "column",
21259
21890
  "auth",
21260
21891
  "jwt",
21261
21892
  "func",
21262
- "cast"
21893
+ "cast",
21894
+ "storage"
21263
21895
  ]);
21264
21896
  function isValueExpr(expr) {
21265
21897
  return VALUE_KINDS.has(expr.kind);
@@ -21269,7 +21901,7 @@ function isConditionExpr(expr) {
21269
21901
  if (expr.kind === "column") return true;
21270
21902
  return expr.kind === "literal" && (expr.value === null || typeof expr.value === "boolean");
21271
21903
  }
21272
- var RESERVED = /* @__PURE__ */ new Set([
21904
+ var RESERVED2 = /* @__PURE__ */ new Set([
21273
21905
  "all",
21274
21906
  "and",
21275
21907
  "array",
@@ -21309,12 +21941,20 @@ var CLAUSE_WORDS = /* @__PURE__ */ new Set([
21309
21941
  "cross"
21310
21942
  ]);
21311
21943
  var SPECIAL_WORDS = /* @__PURE__ */ new Set([
21312
- ...RESERVED,
21944
+ ...RESERVED2,
21313
21945
  ...CLAUSE_WORDS,
21314
21946
  "symmetric",
21315
21947
  "auth"
21316
21948
  ]);
21317
21949
  var TEXT_TYPES = /* @__PURE__ */ new Set(["text", "varchar", "uuid"]);
21950
+ var STORAGE_SUBQUERY_PROBLEM = "A policy cannot read storage.objects in a subquery yet";
21951
+ var FOLDERNAME_INDEX_PROBLEM = `storage.foldername(name) is available only as (storage.foldername(name))[n] with n from 1 to ${String(MAX_FOLDERNAME_INDEX)}`;
21952
+ function storageHelperColumnProblem(fn) {
21953
+ return `storage.${fn}() takes the name column of storage.objects`;
21954
+ }
21955
+ function quotedStorageObjectsProblem(written) {
21956
+ return `a table named "${written}" cannot be used in policies, as naro keeps the name for Storage: write storage.objects, without quotes, for Storage's objects`;
21957
+ }
21318
21958
  function count(state, token) {
21319
21959
  if (++state.nodes > MAX_EXPRESSION_NODES)
21320
21960
  throw new PolicySyntaxError(
@@ -21361,6 +22001,10 @@ function truthProblem(operand) {
21361
22001
  case "now":
21362
22002
  return "now() is a time: compare it, created_at < now()";
21363
22003
  }
22004
+ case "storage": {
22005
+ const call = operand.fn === "foldername" ? "(storage.foldername(\u2026))[n]" : `storage.${operand.fn}(\u2026)`;
22006
+ return `${call} ${text2}: compare it with text, ${call} = 'true'`;
22007
+ }
21364
22008
  }
21365
22009
  }
21366
22010
  function truthTestProblem(operand) {
@@ -21380,6 +22024,7 @@ function isTextValue(value) {
21380
22024
  case "auth":
21381
22025
  case "jwt":
21382
22026
  case "cast":
22027
+ case "storage":
21383
22028
  return true;
21384
22029
  case "func":
21385
22030
  return value.name !== "length" && value.name !== "coalesce";
@@ -21724,7 +22369,7 @@ function readSubquery(state, start, kind) {
21724
22369
  }
21725
22370
  }
21726
22371
  function isReservedWord(token) {
21727
- return token.kind === "ident" && !token.quoted && (RESERVED.has(asciiLower(token.value)) || CLAUSE_WORDS.has(asciiLower(token.value)));
22372
+ return token.kind === "ident" && !token.quoted && (RESERVED2.has(asciiLower(token.value)) || CLAUSE_WORDS.has(asciiLower(token.value)));
21728
22373
  }
21729
22374
  function readIdentifier(stream, what) {
21730
22375
  const token = stream.peek();
@@ -21736,16 +22381,40 @@ function readIdentifier(stream, what) {
21736
22381
  stream.next();
21737
22382
  return token.value;
21738
22383
  }
22384
+ function isName(token, word) {
22385
+ return token.kind === "ident" && (token.quoted ? token.value === word : asciiLower(token.value) === word);
22386
+ }
22387
+ function atStorageObjects(stream) {
22388
+ return isName(stream.peek(), "storage") && isOperator(stream.peek(1), ".") && isName(stream.peek(2), "objects");
22389
+ }
21739
22390
  function readTableName(stream) {
21740
22391
  const first = stream.peek();
21741
- const name = readIdentifier(stream, "a table name");
21742
- if (!stream.acceptOperator(".")) return name;
21743
- if (first.quoted ? name !== "public" : asciiLower(name) !== "public")
22392
+ if (atStorageObjects(stream))
22393
+ throw new PolicySyntaxError(STORAGE_SUBQUERY_PROBLEM, first.position);
22394
+ let token = first;
22395
+ let name = readIdentifier(stream, "a table name");
22396
+ if (stream.acceptOperator(".")) {
22397
+ if (first.quoted ? name !== "public" : asciiLower(name) !== "public")
22398
+ throw new PolicySyntaxError(
22399
+ `schema ${name} does not exist: naro's tables are in public`,
22400
+ first.position
22401
+ );
22402
+ token = stream.peek();
22403
+ name = readIdentifier(stream, "a table name");
22404
+ }
22405
+ if (asciiLower(name) === STORAGE_OBJECTS)
21744
22406
  throw new PolicySyntaxError(
21745
- `schema ${name} does not exist: naro's tables are in public`,
21746
- first.position
22407
+ quotedStorageObjectsProblem(name),
22408
+ token.position
21747
22409
  );
21748
- return readIdentifier(stream, "a table name");
22410
+ return name;
22411
+ }
22412
+ function readPolicyTarget(stream) {
22413
+ if (!atStorageObjects(stream)) return readTableName(stream);
22414
+ stream.next();
22415
+ stream.next();
22416
+ stream.next();
22417
+ return STORAGE_OBJECTS;
21749
22418
  }
21750
22419
  function readNumber(token, negative) {
21751
22420
  const value = Number(token.value) * (negative ? -1 : 1);
@@ -21883,11 +22552,18 @@ function readPrimary(state) {
21883
22552
  }
21884
22553
  const inner = readOr(state);
21885
22554
  stream.expectOperator(")");
22555
+ if (!isOperator(stream.peek(), "[")) return inner;
22556
+ if (inner.kind !== "storage" || inner.fn !== "foldername" || inner.index !== null)
22557
+ return stream.unexpected();
22558
+ inner.index = readFolderIndex(state);
22559
+ if (isOperator(stream.peek(), "[")) return stream.unexpected();
21886
22560
  return inner;
21887
22561
  }
21888
22562
  return stream.unexpected(token);
21889
22563
  }
21890
22564
  case "ident": {
22565
+ if (isName(token, "storage") && isOperator(stream.peek(), ".") && stream.peek(1).kind === "ident" && isOperator(stream.peek(2), "("))
22566
+ return readStorageCall(state, token);
21891
22567
  if (!token.quoted) {
21892
22568
  const word = asciiLower(token.value);
21893
22569
  if (word === "true") return { kind: "literal", value: true };
@@ -21902,7 +22578,7 @@ function readPrimary(state) {
21902
22578
  return auth;
21903
22579
  }
21904
22580
  if (isOperator(stream.peek(), "(")) return readCall(state, token);
21905
- if (RESERVED.has(word)) return stream.unexpected(token);
22581
+ if (RESERVED2.has(word)) return stream.unexpected(token);
21906
22582
  }
21907
22583
  if (isOperator(stream.peek(), ".")) {
21908
22584
  stream.next();
@@ -21923,14 +22599,72 @@ function readPrimary(state) {
21923
22599
  return stream.unexpected(token);
21924
22600
  }
21925
22601
  }
22602
+ function readStorageCall(state, start) {
22603
+ const { stream } = state;
22604
+ stream.next();
22605
+ const name = stream.next();
22606
+ const fn = STORAGE_HELPERS.find((helper) => isName(name, helper));
22607
+ if (fn === void 0)
22608
+ throw new PolicySyntaxError(
22609
+ `function storage.${name.value} is not available in policies: use storage.foldername, storage.filename or storage.extension`,
22610
+ start.position
22611
+ );
22612
+ stream.expectOperator("(");
22613
+ const node = {
22614
+ kind: "storage",
22615
+ fn,
22616
+ column: readNameColumn(state, fn),
22617
+ index: null
22618
+ };
22619
+ if (fn === "foldername") state.foldernames.push({ node, token: start });
22620
+ return node;
22621
+ }
22622
+ function readNameColumn(state, fn) {
22623
+ const { stream } = state;
22624
+ const first = stream.peek();
22625
+ const identifier = (token) => token.kind === "ident" && (token.quoted || !isReservedWord(token));
22626
+ const qualified = isOperator(stream.peek(1), ".");
22627
+ const column2 = qualified ? stream.peek(2) : first;
22628
+ const after = stream.peek(qualified ? 3 : 1);
22629
+ const isColumn = identifier(first) && identifier(column2) && isName(column2, "name");
22630
+ if (isColumn && after.kind === "eof") {
22631
+ for (let index = qualified ? 3 : 1; index > 0; index--) stream.next();
22632
+ stream.expectOperator(")");
22633
+ }
22634
+ if (!isColumn || !isOperator(after, ")"))
22635
+ throw new PolicySyntaxError(storageHelperColumnProblem(fn), first.position);
22636
+ count(state, first);
22637
+ for (let index = qualified ? 4 : 2; index > 0; index--) stream.next();
22638
+ return {
22639
+ kind: "column",
22640
+ qualifier: qualified ? first.value : null,
22641
+ name: "name"
22642
+ };
22643
+ }
22644
+ function readFolderIndex(state) {
22645
+ const { stream } = state;
22646
+ stream.next();
22647
+ const token = stream.peek();
22648
+ const index = token.kind === "number" && /^[0-9]+$/.test(token.value) ? Number(token.value) : Number.NaN;
22649
+ if (!(index >= 1 && index <= MAX_FOLDERNAME_INDEX))
22650
+ throw new PolicySyntaxError(FOLDERNAME_INDEX_PROBLEM, token.position);
22651
+ stream.next();
22652
+ count(state, token);
22653
+ stream.expectOperator("]");
22654
+ return index;
22655
+ }
21926
22656
  function readCall(state, name) {
21927
22657
  const { stream } = state;
21928
22658
  const word = asciiLower(name.value);
21929
- if (!POLICY_FUNCTIONS.includes(word))
22659
+ if (!POLICY_FUNCTIONS.includes(word)) {
22660
+ const first = stream.peek(1);
22661
+ if (isName(first, "storage") && isOperator(stream.peek(2), ".") && isName(stream.peek(3), "foldername") && isOperator(stream.peek(4), "("))
22662
+ throw new PolicySyntaxError(FOLDERNAME_INDEX_PROBLEM, first.position);
21930
22663
  throw new PolicySyntaxError(
21931
22664
  `function ${name.value} is not available in policies: use lower, upper, coalesce, length or now`,
21932
22665
  name.position
21933
22666
  );
22667
+ }
21934
22668
  stream.expectOperator("(");
21935
22669
  const args = [];
21936
22670
  if (!isOperator(stream.peek(), ")"))
@@ -21961,12 +22695,17 @@ function newState(stream) {
21961
22695
  depth: 0,
21962
22696
  subqueries: 0,
21963
22697
  subqueryDepth: 0,
21964
- arrays: 0
22698
+ arrays: 0,
22699
+ foldernames: []
21965
22700
  };
21966
22701
  }
21967
22702
  function readCondition(stream) {
21968
22703
  const token = stream.peek();
21969
- const expr = readOr(newState(stream));
22704
+ const state = newState(stream);
22705
+ const expr = readOr(state);
22706
+ const bare = state.foldernames.find(({ node }) => node.index === null);
22707
+ if (bare)
22708
+ throw new PolicySyntaxError(FOLDERNAME_INDEX_PROBLEM, bare.token.position);
21970
22709
  return requireCondition(expr, token);
21971
22710
  }
21972
22711
  function checkExpressionSize(expr, position) {
@@ -22032,6 +22771,10 @@ function printValue(expr) {
22032
22771
  return expr.name === "now" ? "now()" : `${expr.name}(${expr.args.map(printValue).join(", ")})`;
22033
22772
  case "cast":
22034
22773
  return `(${printValue(expr.operand)})::text`;
22774
+ case "storage": {
22775
+ const call = `storage.${expr.fn}(${printValue(expr.column)})`;
22776
+ return expr.index === null ? call : `(${call})[${String(expr.index)}]`;
22777
+ }
22035
22778
  }
22036
22779
  }
22037
22780
  function printSubquery(subquery) {
@@ -22086,6 +22829,9 @@ function unqualifiedInSubquery(expr) {
22086
22829
  case "cast":
22087
22830
  value(node.operand, inside);
22088
22831
  return;
22832
+ case "storage":
22833
+ value(node.column, inside);
22834
+ return;
22089
22835
  default:
22090
22836
  return;
22091
22837
  }
@@ -22220,9 +22966,12 @@ function policyShapeProblem(policy) {
22220
22966
  return `${command === "all" ? "An ALL" : "An UPDATE"} policy needs USING (\u2026), WITH CHECK (\u2026) or both.`;
22221
22967
  return void 0;
22222
22968
  }
22969
+ function policyTableSql(table) {
22970
+ return asciiLower(table) === STORAGE_OBJECTS ? STORAGE_OBJECTS : quoteIdent(table);
22971
+ }
22223
22972
  function policySql(policy) {
22224
22973
  const parts = [
22225
- `CREATE POLICY ${quotePolicyName(policy.name)} ON ${quoteIdent(policy.table)}`
22974
+ `CREATE POLICY ${quotePolicyName(policy.name)} ON ${policyTableSql(policy.table)}`
22226
22975
  ];
22227
22976
  if (!policy.permissive) parts.push("AS RESTRICTIVE");
22228
22977
  if (policy.command !== "all")
@@ -22991,6 +23740,9 @@ function storedSource(table, names, alias) {
22991
23740
  })
22992
23741
  };
22993
23742
  }
23743
+ function tableLabel(table) {
23744
+ return table.name === STORAGE_OBJECTS_TABLE ? STORAGE_OBJECTS : table.name;
23745
+ }
22994
23746
  function missingColumnReason(name, tables) {
22995
23747
  return `column ${name} does not exist on ${tables.join(" or ")}`;
22996
23748
  }
@@ -23008,7 +23760,7 @@ function resolveColumn(env, ref) {
23008
23760
  if (!column2)
23009
23761
  return blamed(
23010
23762
  env,
23011
- `column ${ref.qualifier}.${ref.name} does not exist: ${source.table.name} has no column ${ref.name}`
23763
+ `column ${ref.qualifier}.${ref.name} does not exist: ${tableLabel(source.table)} has no column ${ref.name}`
23012
23764
  );
23013
23765
  return { source, column: column2 };
23014
23766
  }
@@ -23028,7 +23780,7 @@ function resolveColumn(env, ref) {
23028
23780
  env,
23029
23781
  missingColumnReason(
23030
23782
  ref.name,
23031
- env.scopes.map((source) => source.table.name)
23783
+ env.scopes.map((source) => tableLabel(source.table))
23032
23784
  )
23033
23785
  );
23034
23786
  }
@@ -23068,7 +23820,47 @@ function valueSql(env, expr, textual = null) {
23068
23820
  }
23069
23821
  case "cast":
23070
23822
  return `CAST(${valueSql(env, expr.operand, "textual")} AS TEXT)`;
23071
- }
23823
+ case "storage":
23824
+ return storageCallSql(env, expr);
23825
+ }
23826
+ }
23827
+ function pathArray(operand) {
23828
+ return `('["' || replace(replace(${operand()}, '"', '\\"'), '/', '","') || '"]')`;
23829
+ }
23830
+ function storageHelperSql(fn, index, operand) {
23831
+ switch (fn) {
23832
+ case "foldername": {
23833
+ if (index === null || !Number.isInteger(index) || index < 1 || index > MAX_FOLDERNAME_INDEX)
23834
+ throw new RangeError(FOLDERNAME_INDEX_PROBLEM);
23835
+ const n = String(index);
23836
+ return `(CASE WHEN ${n} < json_array_length(${pathArray(operand)}) THEN json_extract(${pathArray(operand)}, '$[${String(index - 1)}]') END)`;
23837
+ }
23838
+ case "filename":
23839
+ return `json_extract(${pathArray(operand)}, '$[#-1]')`;
23840
+ case "extension": {
23841
+ const filename = () => storageHelperSql("filename", null, operand);
23842
+ return `substr(${filename()}, length(rtrim(${filename()}, replace(${filename()}, '.', ''))) + 1)`;
23843
+ }
23844
+ }
23845
+ }
23846
+ function storageCallSql(env, expr) {
23847
+ const problem = storageHelperColumnProblem(expr.fn);
23848
+ if (asciiLower(env.blame.policy.table) !== STORAGE_OBJECTS)
23849
+ return blamed(env, problem);
23850
+ const { source, column: column2 } = resolveColumn(env, expr.column);
23851
+ if (source !== env.scopes[0] || source.table.name !== STORAGE_OBJECTS_TABLE || column2.name !== "name")
23852
+ return blamed(env, problem);
23853
+ const index = expr.fn === "foldername" ? expr.index : null;
23854
+ if (expr.fn === "foldername" ? index === null || !Number.isInteger(index) || index < 1 || index > MAX_FOLDERNAME_INDEX : expr.index !== null)
23855
+ return blamed(env, FOLDERNAME_INDEX_PROBLEM);
23856
+ return storageHelperSql(
23857
+ expr.fn,
23858
+ index,
23859
+ () => source.read(column2, env, {
23860
+ qualified: env.scopes.length > 1,
23861
+ textual: "textual"
23862
+ })
23863
+ );
23072
23864
  }
23073
23865
  function operandSide(env, expr) {
23074
23866
  if (expr.kind === "cast") return operandSide(env, expr.operand);
@@ -23149,10 +23941,10 @@ function conditionSql(env, expr) {
23149
23941
  if (untestable !== void 0) return blamed(env, untestable);
23150
23942
  const known = knownNullTest(env, expr);
23151
23943
  if (known !== void 0) return known;
23152
- const target = valueSql(env, expr.operand);
23944
+ const target2 = valueSql(env, expr.operand);
23153
23945
  if (expr.value === null)
23154
- return `(${target} IS ${expr.not ? "NOT " : ""}NULL)`;
23155
- const test = isBoolean(target, expr.value);
23946
+ return `(${target2} IS ${expr.not ? "NOT " : ""}NULL)`;
23947
+ const test = isBoolean(target2, expr.value);
23156
23948
  return expr.not ? `(NOT (${test}))` : `(${test})`;
23157
23949
  }
23158
23950
  case "in": {
@@ -23171,10 +23963,10 @@ function conditionSql(env, expr) {
23171
23963
  case "like": {
23172
23964
  const problem = policyPatternProblem(expr.pattern, expr.ci);
23173
23965
  if (problem !== void 0) return blamed(env, problem);
23174
- const target = valueSql(env, expr.operand, "pattern");
23966
+ const target2 = valueSql(env, expr.operand, "pattern");
23175
23967
  const not = expr.not ? "NOT " : "";
23176
23968
  const pattern = bind(env, boundPattern(expr.pattern, expr.ci));
23177
- return expr.ci ? `(${target} ${not}LIKE ${pattern} ESCAPE '\\')` : `(${target} ${not}GLOB ${pattern})`;
23969
+ return expr.ci ? `(${target2} ${not}LIKE ${pattern} ESCAPE '\\')` : `(${target2} ${not}GLOB ${pattern})`;
23178
23970
  }
23179
23971
  case "between": {
23180
23972
  if (env.check) {
@@ -23183,10 +23975,10 @@ function conditionSql(env, expr) {
23183
23975
  const both = `(${low2} AND ${high2})`;
23184
23976
  return expr.not ? `(NOT ${both})` : both;
23185
23977
  }
23186
- const target = valueSql(env, expr.operand);
23978
+ const target2 = valueSql(env, expr.operand);
23187
23979
  const low = valueSql(env, expr.low);
23188
23980
  const high = valueSql(env, expr.high);
23189
- return `(${target} ${expr.not ? "NOT " : ""}BETWEEN ${low} AND ${high})`;
23981
+ return `(${target2} ${expr.not ? "NOT " : ""}BETWEEN ${low} AND ${high})`;
23190
23982
  }
23191
23983
  case "array": {
23192
23984
  const values = distinctArrayValues(
@@ -23285,7 +24077,7 @@ function groupTerms(env, sources, action, permissive, restrictive, pick2) {
23285
24077
  expr
23286
24078
  );
23287
24079
  const [source] = sources;
23288
- const problem = source === void 0 ? void 0 : textTruthProblem(env.snapshot, source.table, expr);
24080
+ const problem = source === void 0 ? void 0 : textTruthProblem(env.snapshot, source.table, expr, source.names);
23289
24081
  if (problem !== void 0) fail(policyInvalid(policy, action), problem);
23290
24082
  return { sql, params };
23291
24083
  };
@@ -23375,6 +24167,9 @@ function outerColumns(table, expr) {
23375
24167
  case "cast":
23376
24168
  visitValue(value.operand);
23377
24169
  return;
24170
+ case "storage":
24171
+ visitValue(value.column);
24172
+ return;
23378
24173
  default:
23379
24174
  return;
23380
24175
  }
@@ -23457,6 +24252,9 @@ function columnsReadOn(snapshot, table, expr, written) {
23457
24252
  case "cast":
23458
24253
  visitValue(value.operand, scopes);
23459
24254
  return;
24255
+ case "storage":
24256
+ visitValue(value.column, scopes);
24257
+ return;
23460
24258
  default:
23461
24259
  return;
23462
24260
  }
@@ -23510,19 +24308,20 @@ function columnsReadOn(snapshot, table, expr, written) {
23510
24308
  visit(expr, [{ names: [asciiLower(table.name)], table }]);
23511
24309
  return found;
23512
24310
  }
23513
- function truthColumns(snapshot, table, expr) {
23514
- return comparedColumns(snapshot, table, expr, isBooleanConstant, true);
24311
+ function truthColumns(snapshot, table, expr, names = [asciiLower(table.name)]) {
24312
+ return comparedColumns(snapshot, table, expr, isBooleanConstant, true, names);
23515
24313
  }
23516
- function numberComparedColumns(snapshot, table, expr) {
24314
+ function numberComparedColumns(snapshot, table, expr, names = [asciiLower(table.name)]) {
23517
24315
  return comparedColumns(
23518
24316
  snapshot,
23519
24317
  table,
23520
24318
  expr,
23521
24319
  (value) => value.kind === "literal" && typeof value.value === "number",
23522
- false
24320
+ false,
24321
+ names
23523
24322
  );
23524
24323
  }
23525
- function comparedColumns(snapshot, table, expr, constant, truth) {
24324
+ function comparedColumns(snapshot, table, expr, constant, truth, names = [asciiLower(table.name)]) {
23526
24325
  const found = [];
23527
24326
  const read = (ref, scopes) => {
23528
24327
  const resolved = resolveRef2(ref, scopes);
@@ -23568,11 +24367,11 @@ function comparedColumns(snapshot, table, expr, constant, truth) {
23568
24367
  return;
23569
24368
  }
23570
24369
  };
23571
- visit(expr, [{ names: [asciiLower(table.name)], table }]);
24370
+ visit(expr, [{ names, table }]);
23572
24371
  return found;
23573
24372
  }
23574
- function textTruthProblem(snapshot, table, expr) {
23575
- for (const { written, column: column2 } of truthColumns(snapshot, table, expr))
24373
+ function textTruthProblem(snapshot, table, expr, names = [asciiLower(table.name)]) {
24374
+ for (const { written, column: column2 } of truthColumns(snapshot, table, expr, names))
23576
24375
  if (!isNumeric(column2))
23577
24376
  return `${written} is a ${columnAffinity(column2)} column, whose truth value is not what its text says ('true' reads as false): compare it, ${written} = 'true'`;
23578
24377
  return void 0;
@@ -23695,7 +24494,7 @@ function arrayValues(value, path) {
23695
24494
  }
23696
24495
  function filterSql(table, filter, params, path) {
23697
24496
  const resolved = column(table, filter.column);
23698
- const target = quoteIdentifier(resolved.name);
24497
+ const target2 = quoteIdentifier(resolved.name);
23699
24498
  const valuePath = `${path}.value`;
23700
24499
  switch (filter.operator) {
23701
24500
  case "eq":
@@ -23713,7 +24512,7 @@ function filterSql(table, filter, params, path) {
23713
24512
  lte: "<="
23714
24513
  }[filter.operator];
23715
24514
  params.push(toBoundValue(scalar(filter.value, valuePath)));
23716
- return `${target} ${operator} ?`;
24515
+ return `${target2} ${operator} ?`;
23717
24516
  }
23718
24517
  case "in": {
23719
24518
  if (!Array.isArray(filter.value))
@@ -23721,7 +24520,7 @@ function filterSql(table, filter, params, path) {
23721
24520
  if (filter.value.length === 0) return "0 = 1";
23722
24521
  for (const [index, item] of filter.value.entries())
23723
24522
  params.push(toBoundValue(scalar(item, `${valuePath}[${index}]`)));
23724
- return `${target} IN (${filter.value.map(() => "?").join(", ")})`;
24523
+ return `${target2} IN (${filter.value.map(() => "?").join(", ")})`;
23725
24524
  }
23726
24525
  case "is": {
23727
24526
  const value = filter.value;
@@ -23729,9 +24528,9 @@ function filterSql(table, filter, params, path) {
23729
24528
  fail(invalidQuery(valuePath, "must be null, true or false"));
23730
24529
  if (value === null) {
23731
24530
  params.push(null);
23732
- return `${target} IS ?`;
24531
+ return `${target2} IS ?`;
23733
24532
  }
23734
- return isBoolean(target, value);
24533
+ return isBoolean(target2, value);
23735
24534
  }
23736
24535
  case "ilike":
23737
24536
  case "like": {
@@ -23741,13 +24540,13 @@ function filterSql(table, filter, params, path) {
23741
24540
  if (problem !== void 0) fail(invalidQuery(valuePath, problem));
23742
24541
  const ilike = filter.operator === "ilike";
23743
24542
  params.push(boundPattern(filter.value, ilike));
23744
- return ilike ? `${target} LIKE ? ESCAPE '\\'` : `${target} GLOB ?`;
24543
+ return ilike ? `${target2} LIKE ? ESCAPE '\\'` : `${target2} GLOB ?`;
23745
24544
  }
23746
24545
  case "contains":
23747
24546
  case "containedBy":
23748
24547
  case "overlaps": {
23749
24548
  const values = arrayValues(filter.value, valuePath);
23750
- const qualified = `${quoteIdentifier(table.name)}.${target}`;
24549
+ const qualified = `${quoteIdentifier(table.name)}.${target2}`;
23751
24550
  let next = 0;
23752
24551
  return arraySql(
23753
24552
  () => qualified,
@@ -23808,22 +24607,22 @@ function statement(sql, params) {
23808
24607
  function assignments(table, row, path) {
23809
24608
  const resolved = /* @__PURE__ */ new Map();
23810
24609
  for (const [key, value] of Object.entries(row)) {
23811
- const target = column(table, key);
23812
- if (target.generated)
24610
+ const target2 = column(table, key);
24611
+ if (target2.generated)
23813
24612
  fail(
23814
24613
  invalidQuery(
23815
24614
  `${path}.${key}`,
23816
- `Column ${target.name} is generated and cannot be written.`
24615
+ `Column ${target2.name} is generated and cannot be written.`
23817
24616
  )
23818
24617
  );
23819
- if (resolved.has(target.name))
24618
+ if (resolved.has(target2.name))
23820
24619
  fail(
23821
24620
  invalidQuery(
23822
24621
  `${path}.${key}`,
23823
- `Column ${target.name} is given more than once.`
24622
+ `Column ${target2.name} is given more than once.`
23824
24623
  )
23825
24624
  );
23826
- resolved.set(target.name, { column: target, value });
24625
+ resolved.set(target2.name, { column: target2, value });
23827
24626
  }
23828
24627
  return resolved;
23829
24628
  }
@@ -23944,13 +24743,21 @@ function checkProblemReason(column2, problem) {
23944
24743
  function isTrue(term) {
23945
24744
  return term.sql === "1" && term.params.length === 0;
23946
24745
  }
24746
+ function targetNames(target2) {
24747
+ return asciiLower(target2.name) === STORAGE_OBJECTS ? [STORAGE_OBJECTS_QUALIFIER] : [asciiLower(target2.name)];
24748
+ }
23947
24749
  var Rls = class {
23948
- constructor(req, snapshot, table, nonce, commands) {
24750
+ constructor(req, snapshot, table, nonce, commands, scope = {
24751
+ policyTable: table.name,
24752
+ names: [asciiLower(table.name)],
24753
+ alias: null
24754
+ }) {
23949
24755
  this.req = req;
23950
24756
  this.snapshot = snapshot;
23951
24757
  this.table = table;
23952
24758
  this.nonce = nonce;
23953
- this.outer = storedSource(table, [asciiLower(table.name)], null);
24759
+ this.policyTable = scope.policyTable;
24760
+ this.outer = storedSource(table, scope.names, scope.alias);
23954
24761
  const all = commands.flatMap((command) => {
23955
24762
  const found = this.found(command);
23956
24763
  return [...found.permissive, ...found.restrictive];
@@ -23964,7 +24771,7 @@ var Rls = class {
23964
24771
  if (!owner || owner.generated)
23965
24772
  fail(
23966
24773
  policyInvalid(policy, command),
23967
- owner ? `the owner column ${owner.name} is a generated column` : `the owner column ${policy.owner} is not on ${table.name}`
24774
+ owner ? `the owner column ${owner.name} is a generated column` : `the owner column ${policy.owner} is not on ${tableLabel(table)}`
23968
24775
  );
23969
24776
  }
23970
24777
  }
@@ -23973,6 +24780,8 @@ var Rls = class {
23973
24780
  /** Restrictive policies named by the checks' markers. */
23974
24781
  restrictive = [];
23975
24782
  outer;
24783
+ /** The name the table's policies are stored under. */
24784
+ policyTable;
23976
24785
  /**
23977
24786
  * A new statement of the same query — an insert's next group of rows:
23978
24787
  * MAX_STATEMENT_SUBQUERIES is a cap on one statement's SQL, so its count
@@ -23986,7 +24795,7 @@ var Rls = class {
23986
24795
  found(command) {
23987
24796
  const found = applicablePolicies(
23988
24797
  this.req.policies,
23989
- this.table.name,
24798
+ this.policyTable,
23990
24799
  command,
23991
24800
  this.req.role
23992
24801
  );
@@ -24037,7 +24846,14 @@ var Rls = class {
24037
24846
  }
24038
24847
  /** WHERE's policy part (spec §5): the command's USING, and SELECT's. */
24039
24848
  where(action) {
24040
- const commands = action === "select" ? ["select"] : [action, "select"];
24849
+ return this.using(action === "select" ? ["select"] : [action, "select"]);
24850
+ }
24851
+ /**
24852
+ * The USING of exactly these commands, ANDed: per command its permissive
24853
+ * policies ORed — `0` when there is none, nothing passes — and each of its
24854
+ * restrictive ones. Null when every term is true.
24855
+ */
24856
+ using(commands) {
24041
24857
  const terms = [];
24042
24858
  for (const command of commands) {
24043
24859
  const found = this.found(command);
@@ -24192,10 +25008,10 @@ var Rls = class {
24192
25008
  * writes there itself and the owner condition compares as text (spec
24193
25009
  * §10 — owner policies are left out of the refusal above too).
24194
25010
  */
24195
- refuseText(action, path, target, value) {
25011
+ refuseText(action, path, target2, value) {
24196
25012
  if (typeof value !== "string") return;
24197
25013
  const { numbers, truths } = this.textReaders ??= this.readersOfText();
24198
- const reader = refusesText(target, value) ? numbers.get(target.name) : truths.get(target.name);
25014
+ const reader = refusesText(target2, value) ? numbers.get(target2.name) : truths.get(target2.name);
24199
25015
  if (reader)
24200
25016
  fail(
24201
25017
  invalidQuery(
@@ -24203,13 +25019,13 @@ var Rls = class {
24203
25019
  mustBeNumber(reader.command === "all" ? action : reader.command)
24204
25020
  )
24205
25021
  );
24206
- if (numberClass(target.type) === "number" && !storesAsNumber(value) && !this.ownId(target, value))
24207
- fail(invalidQuery(path, mustBeNumberFor(target.type)));
25022
+ if (numberClass(target2.type) === "number" && !storesAsNumber(value) && !this.ownId(target2, value))
25023
+ fail(invalidQuery(path, mustBeNumberFor(target2.type)));
24208
25024
  }
24209
25025
  /** The caller's id, in a column an owner policy of this table names. */
24210
- ownId(target, value) {
25026
+ ownId(target2, value) {
24211
25027
  return value === this.req.uid && this.req.policies.some(
24212
- (policy) => policy.owner !== null && asciiLower(policy.owner) === asciiLower(target.name) && resolveTable(this.snapshot, policy.table) === this.table
25028
+ (policy) => policy.owner !== null && asciiLower(policy.owner) === asciiLower(target2.name) && resolveTable(this.snapshot, policy.table) === this.table
24213
25029
  );
24214
25030
  }
24215
25031
  /**
@@ -24224,19 +25040,19 @@ var Rls = class {
24224
25040
  * text, and a blob `x'…'` — never stored as a number — is refused as text
24225
25041
  * that is no number (P2-7b 최종 리뷰 후속 2).
24226
25042
  */
24227
- refuseOmittedText(path, target) {
24228
- const stored = stringDefault(target);
25043
+ refuseOmittedText(path, target2) {
25044
+ const stored = stringDefault(target2);
24229
25045
  if (stored === void 0) return;
24230
25046
  if (stored.kind === "text" && storesAsNumber(stored.text)) return;
24231
25047
  const { numbers, truths } = this.textReaders ??= this.readersOfText();
24232
- const refused = stored.kind === "text" ? refusesText(target, stored.text) : numberClass(target.type) === "number";
24233
- const reader = refused ? numbers.get(target.name) : truths.get(target.name);
25048
+ const refused = stored.kind === "text" ? refusesText(target2, stored.text) : numberClass(target2.type) === "number";
25049
+ const reader = refused ? numbers.get(target2.name) : truths.get(target2.name);
24234
25050
  if (reader)
24235
25051
  fail(
24236
25052
  invalidQuery(
24237
25053
  path,
24238
25054
  mustIncludeFor(
24239
- target.name,
25055
+ target2.name,
24240
25056
  reader.command === "all" ? "insert" : reader.command
24241
25057
  )
24242
25058
  )
@@ -24272,7 +25088,7 @@ var Rls = class {
24272
25088
  fail(
24273
25089
  dataError("POLICY_INVALID", {
24274
25090
  action: "select",
24275
- table: this.table.name,
25091
+ table: this.policyTable,
24276
25092
  values: true
24277
25093
  }),
24278
25094
  `they bind ${String(count2)}`
@@ -24285,7 +25101,7 @@ var Rls = class {
24285
25101
  fail(
24286
25102
  dataError("POLICY_INVALID", {
24287
25103
  action: "select",
24288
- table: this.table.name,
25104
+ table: this.policyTable,
24289
25105
  bytes: true
24290
25106
  }),
24291
25107
  `they write ${String(bytes)}`
@@ -24294,13 +25110,7 @@ var Rls = class {
24294
25110
  };
24295
25111
  function checkCase(table, terms, guard, pass, action, nonce) {
24296
25112
  if (terms.length === 0) return pass;
24297
- let sql = pass.sql;
24298
- let params = pass.params;
24299
- for (const term of terms.toReversed()) {
24300
- sql = `CASE WHEN ${term.sql} THEN ${sql} ELSE ${term.marker} END`;
24301
- params = [...term.params, ...params];
24302
- }
24303
- if (!guard) return { sql, params };
25113
+ if (!guard) return nestedChecks(terms, pass);
24304
25114
  if (table.sql === null) {
24305
25115
  const first = terms.find((term) => term.policy !== void 0);
24306
25116
  if (first?.kind === "select" || !first?.policy)
@@ -24312,11 +25122,134 @@ function checkCase(table, terms, guard, pass, action, nonce) {
24312
25122
  );
24313
25123
  fail(policyInvalid(first.policy, action));
24314
25124
  }
25125
+ return nestedChecks(
25126
+ [schemaGuard(table.name, table.sql, action, nonce), ...terms],
25127
+ pass
25128
+ );
25129
+ }
25130
+ function schemaGuard(name, sql, kind, nonce) {
24315
25131
  return {
24316
- sql: `CASE WHEN (SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?) = ? THEN ${sql} ELSE ${schemaMarker(nonce)} END`,
24317
- params: [table.name, table.sql, ...params]
25132
+ sql: "(SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?) = ?",
25133
+ params: [name, sql],
25134
+ marker: schemaMarker(nonce),
25135
+ kind,
25136
+ policy: void 0
24318
25137
  };
24319
25138
  }
25139
+ function nestedChecks(terms, pass) {
25140
+ let sql = pass.sql;
25141
+ let params = pass.params;
25142
+ for (const term of terms.toReversed()) {
25143
+ sql = `CASE WHEN ${term.sql} THEN ${sql} ELSE ${term.marker} END`;
25144
+ params = [...term.params, ...params];
25145
+ }
25146
+ return { sql, params };
25147
+ }
25148
+ function assertFragmentAlias(alias, caller) {
25149
+ if (/^_naro_/i.test(alias))
25150
+ throw new Error(
25151
+ `${caller}: the alias ${alias} is one the compiler gives its subqueries (_naro_\u2026): name the table otherwise`
25152
+ );
25153
+ }
25154
+ function checkNonceOf(nonce, caller) {
25155
+ if (!/^[0-9a-f]{1,64}$/.test(nonce))
25156
+ throw new Error(`${caller}: checkNonce must be 1 to 64 hex digits`);
25157
+ return nonce;
25158
+ }
25159
+ function usingFragment(target2, req, snapshot, commands, alias) {
25160
+ assertFragmentAlias(alias, "usingFragment");
25161
+ const rls = new Rls(req, snapshot, target2.table, "0", commands, {
25162
+ policyTable: target2.name,
25163
+ names: targetNames(target2),
25164
+ alias
25165
+ });
25166
+ return rls.using(commands) ?? { sql: "1", params: [] };
25167
+ }
25168
+ function rowSource(target2, row, action, stored) {
25169
+ const { table } = target2;
25170
+ const kept = new Set(stored?.columns ?? []);
25171
+ const storedRow = stored ? storedSource(table, targetNames(target2), stored.alias) : void 0;
25172
+ const source = {
25173
+ table,
25174
+ names: targetNames(target2),
25175
+ used: false,
25176
+ storedRead: false,
25177
+ read: (item, env, how) => {
25178
+ const check2 = env.check;
25179
+ if (!check2) return fail(dataError("INTERNAL_ERROR"));
25180
+ source.used = true;
25181
+ if (item.generated)
25182
+ check2.problem(item.name, "generated", env.blame.policy);
25183
+ if (storedRow && kept.has(item.name)) {
25184
+ source.storedRead = true;
25185
+ return storedRow.read(item, env, how);
25186
+ }
25187
+ if (how.textual && isNumeric(item))
25188
+ check2.problem(item.name, how.textual, env.blame.policy);
25189
+ if (!Object.hasOwn(row, item.name))
25190
+ fail(
25191
+ dataError("INTERNAL_ERROR"),
25192
+ `the new row of ${target2.name} has no value for ${item.name}`
25193
+ );
25194
+ const bound = row[item.name] ?? null;
25195
+ if (typeof bound === "string" && refusesText(item, bound))
25196
+ fail(invalidQuery(`row.${item.name}`, mustBeNumber(action)));
25197
+ env.params.push(bound);
25198
+ return readsAsIs(item, bound) ? "?" : asStored(item, "?");
25199
+ },
25200
+ // A stored column is the statement's own table's: SQLite compares it
25201
+ // with its own collation, the snapshot's — which the guard holds to.
25202
+ operand: (item) => kept.has(item.name) ? { stable: true, newColumnish: true, newCollation: item.collation } : { stable: false, newColumnish: false, newCollation: null }
25203
+ };
25204
+ return source;
25205
+ }
25206
+ function storedColumnsOf(target2, command, row, stored) {
25207
+ if (stored === void 0) return void 0;
25208
+ if (command !== "update")
25209
+ throw new Error("checkFragment: only an update keeps stored columns");
25210
+ assertFragmentAlias(stored.alias, "checkFragment");
25211
+ for (const name of stored.columns) {
25212
+ if (target2.table.byName.get(asciiLower(name))?.name !== name)
25213
+ throw new Error(`checkFragment: ${name} is no column of ${target2.name}`);
25214
+ if (Object.hasOwn(row, name))
25215
+ throw new Error(
25216
+ `checkFragment: ${name} is both a new value and a stored column`
25217
+ );
25218
+ }
25219
+ return stored;
25220
+ }
25221
+ function checkFragment(target2, req, snapshot, command, row, nonce, stored, withSelect = false) {
25222
+ const checked = checkNonceOf(nonce, "checkFragment");
25223
+ const source = rowSource(
25224
+ target2,
25225
+ row,
25226
+ command,
25227
+ storedColumnsOf(target2, command, row, stored)
25228
+ );
25229
+ const commands = withSelect ? [command, "select"] : [command];
25230
+ const rls = new Rls(req, snapshot, target2.table, checked, commands, {
25231
+ policyTable: target2.name,
25232
+ names: targetNames(target2),
25233
+ alias: null
25234
+ });
25235
+ const terms = rls.checks(
25236
+ command,
25237
+ source,
25238
+ withSelect,
25239
+ () => false,
25240
+ () => false
25241
+ );
25242
+ if (!source.storedRead || terms.length === 0) return terms;
25243
+ const { name, sql } = target2.table;
25244
+ if (sql === null) {
25245
+ const first = terms.find((term) => term.policy !== void 0)?.policy;
25246
+ return fail(
25247
+ first ? policyInvalid(first, command) : dataError("INTERNAL_ERROR"),
25248
+ `naro cannot read the schema of ${target2.name}, so a check of the stored row cannot be guarded`
25249
+ );
25250
+ }
25251
+ return [schemaGuard(name, sql, command, checked), ...terms];
25252
+ }
24320
25253
  function compileSelect(query, table, rls) {
24321
25254
  if (query.columns !== "*" && query.columns.length === 0)
24322
25255
  fail(invalidQuery("columns", "must name at least one column"));
@@ -24531,9 +25464,10 @@ function compileDelete(query, table, context, rls) {
24531
25464
  ];
24532
25465
  }
24533
25466
  function compile(query, snapshot, decision, context, options = {}) {
24534
- const nonce = options.checkNonce ?? DEFAULT_CHECK_NONCE;
24535
- if (!/^[0-9a-f]{1,64}$/.test(nonce))
24536
- throw new Error("compile: checkNonce must be 1 to 64 hex digits");
25467
+ const nonce = checkNonceOf(
25468
+ options.checkNonce ?? DEFAULT_CHECK_NONCE,
25469
+ "compile"
25470
+ );
24537
25471
  if (!decision.allow)
24538
25472
  return decision.reason === void 0 ? { ok: false, error: decision.error } : { ok: false, error: decision.error, reason: decision.reason };
24539
25473
  const table = resolveTable(snapshot, query.table);
@@ -24609,6 +25543,8 @@ function valueScopes(scopes, value) {
24609
25543
  return value.args.flatMap((arg) => valueScopes(scopes, arg));
24610
25544
  case "cast":
24611
25545
  return valueScopes(scopes, value.operand);
25546
+ case "storage":
25547
+ return valueScopes(scopes, value.column);
24612
25548
  default:
24613
25549
  return [];
24614
25550
  }
@@ -24741,7 +25677,7 @@ function blocked(table, indexes, way) {
24741
25677
  return void 0;
24742
25678
  }
24743
25679
  function problemMessage(policy, outer, table, ways, why) {
24744
- const start = `Policy ${policy.name} on ${outer.name}: its subquery on ${table.name} runs once for every row of ${outer.name} it looks at`;
25680
+ const start = `Policy ${policy.name} on ${tableLabel(outer)}: its subquery on ${table.name} runs once for every row of ${tableLabel(outer)} it looks at`;
24745
25681
  const override = "or send it with allow_unindexed=1 (naro policy create --allow-unindexed) to accept a scan of every row";
24746
25682
  const [way] = ways;
24747
25683
  if (!way)
@@ -24758,8 +25694,9 @@ function unindexedSubqueries(snapshot, indexes, policies) {
24758
25694
  const found = [];
24759
25695
  for (const policy of policies) {
24760
25696
  if (policy.invalid !== null) continue;
24761
- const outer = resolveTable(snapshot, policy.table);
25697
+ const outer = resolvePolicyTable(snapshot, policy.table);
24762
25698
  if (!outer) continue;
25699
+ const outerName = asciiLower(policy.table) === STORAGE_OBJECTS ? STORAGE_OBJECTS_QUALIFIER : asciiLower(outer.name);
24763
25700
  const visit = (scopes, expr) => {
24764
25701
  if (expr === null || isValueExpr(expr)) return;
24765
25702
  switch (expr.kind) {
@@ -24804,13 +25741,319 @@ function unindexedSubqueries(snapshot, indexes, policies) {
24804
25741
  return;
24805
25742
  }
24806
25743
  };
24807
- const root = [{ table: outer, names: [asciiLower(outer.name)] }];
25744
+ const root = [{ table: outer, names: [outerName] }];
24808
25745
  visit(root, policy.usingExpr);
24809
25746
  visit(root, policy.checkExpr);
24810
25747
  }
24811
25748
  return found;
24812
25749
  }
24813
25750
 
25751
+ // ../../packages/data-core/src/storage-sql.ts
25752
+ var STORAGE_NONCE_MAX_DIGITS = 64;
25753
+ function nonceOf(nonce) {
25754
+ if (!/^[0-9a-f]+$/.test(nonce) || nonce.length > STORAGE_NONCE_MAX_DIGITS)
25755
+ throw new Error(
25756
+ `storage-sql: the nonce must be 1 to ${String(STORAGE_NONCE_MAX_DIGITS)} hex digits`
25757
+ );
25758
+ return nonce;
25759
+ }
25760
+ function marker(guard, nonce) {
25761
+ return `json_extract('{}', 'naro:storage:${guard}:${nonce}')`;
25762
+ }
25763
+ var STORAGE_STATEMENT_PARAM_LIMIT = 100;
25764
+ var STORAGE_STATEMENT_BYTE_LIMIT = 1e5;
25765
+ function storageStatementProblem(statement2, action) {
25766
+ if (statement2.params.length > STORAGE_STATEMENT_PARAM_LIMIT)
25767
+ return {
25768
+ error: dataError("POLICY_INVALID", {
25769
+ action,
25770
+ table: STORAGE_OBJECTS,
25771
+ values: STORAGE_STATEMENT_PARAM_LIMIT
25772
+ }),
25773
+ reason: `the ${STORAGE_OBJECTS} policies need more than ${String(STORAGE_STATEMENT_PARAM_LIMIT)} values in one statement`
25774
+ };
25775
+ const bytes = utf8ByteLength(statement2.sql);
25776
+ if (bytes > STORAGE_STATEMENT_BYTE_LIMIT)
25777
+ return {
25778
+ error: dataError("POLICY_INVALID", {
25779
+ action,
25780
+ table: STORAGE_OBJECTS,
25781
+ bytes: STORAGE_STATEMENT_BYTE_LIMIT
25782
+ }),
25783
+ reason: `the ${STORAGE_OBJECTS} policies make one statement ${String(bytes)} bytes long, more than ${String(STORAGE_STATEMENT_BYTE_LIMIT)}`
25784
+ };
25785
+ return void 0;
25786
+ }
25787
+ var OBJECT_COLUMNS = "o.id, o.bucket_id, o.name, o.owner, o.owner_id, o.version, o.created_at, o.updated_at, o.metadata";
25788
+ var PROJECT_BYTES = `(SELECT coalesce(sum(total_bytes), 0) FROM ${STORAGE_BUCKETS_TABLE})`;
25789
+ var LIVE_BUCKET = `${STORAGE_BUCKET_DELETING_COLUMN.column} IS NULL`;
25790
+ function compose(parts, values) {
25791
+ let sql = "";
25792
+ const params = [];
25793
+ let next = 0;
25794
+ for (const part of parts) {
25795
+ if (typeof part === "string") {
25796
+ sql += part;
25797
+ const count2 = part.split("?").length - 1;
25798
+ const bound = count2 === 0 ? [] : values[next++] ?? [];
25799
+ if (bound.length !== count2)
25800
+ throw new Error(
25801
+ `storage-sql: ${String(count2)} placeholders, ${String(bound.length)} values`
25802
+ );
25803
+ params.push(...bound);
25804
+ } else {
25805
+ sql += part.sql;
25806
+ params.push(...part.params);
25807
+ }
25808
+ }
25809
+ if (next !== values.length) throw new Error("storage-sql: values left over");
25810
+ return { sql, params };
25811
+ }
25812
+ function claimUpload(i) {
25813
+ const nonce = nonceOf(i.nonce);
25814
+ return compose(
25815
+ [
25816
+ `INSERT INTO ${STORAGE_OBJECTS_TABLE} (id, bucket_id, name, owner, owner_id, metadata, version, parent, size, status, lock_id, lock_expires_at) SELECT ?, ?, ?, ?, ?, ?, NULL, ?, ?, 'uploading', ?, ? WHERE CASE WHEN NOT EXISTS (SELECT 1 FROM ${STORAGE_BUCKETS_TABLE} WHERE id = ? AND ${LIVE_BUCKET}) THEN ${marker("bucket", nonce)} WHEN (`,
25817
+ i.check,
25818
+ `) <> 1 THEN 0 WHEN EXISTS (SELECT 1 FROM ${STORAGE_OBJECTS_TABLE} WHERE bucket_id = ? AND name = ? AND status = 'ready') THEN ${marker("exists", nonce)} WHEN EXISTS (SELECT 1 FROM ${STORAGE_OBJECTS_TABLE} WHERE bucket_id = ? AND name = ?) THEN ${marker("busy", nonce)} WHEN ${PROJECT_BYTES} + ? > ? THEN ${marker("quota", nonce)} ELSE 1 END RETURNING id`
25819
+ ],
25820
+ [
25821
+ [
25822
+ i.id,
25823
+ i.bucket,
25824
+ i.name,
25825
+ i.owner,
25826
+ i.owner,
25827
+ i.metadata,
25828
+ objectParent(i.name),
25829
+ i.size,
25830
+ i.lockId,
25831
+ i.expiresAt,
25832
+ i.bucket
25833
+ ],
25834
+ [i.bucket, i.name, i.bucket, i.name, i.size, i.limitBytes]
25835
+ ]
25836
+ );
25837
+ }
25838
+ var STORAGE_OBJECT_ALIAS = "o";
25839
+ var OVERWRITE_KEPT_COLUMNS = [
25840
+ "id",
25841
+ "bucket_id",
25842
+ "name",
25843
+ "owner",
25844
+ "owner_id",
25845
+ "created_at"
25846
+ ];
25847
+ function overwriteCheckRow(i) {
25848
+ return {
25849
+ row: { metadata: i.metadata, version: i.version, updated_at: i.now },
25850
+ stored: { alias: STORAGE_OBJECT_ALIAS, columns: OVERWRITE_KEPT_COLUMNS }
25851
+ };
25852
+ }
25853
+ function claimOverwrite(i) {
25854
+ const nonce = nonceOf(i.nonce);
25855
+ return compose(
25856
+ [
25857
+ `UPDATE ${STORAGE_OBJECTS_TABLE} AS o SET lock_id = ?, lock_expires_at = ?, pending = ? WHERE o.bucket_id = ? AND o.name = ? AND CASE WHEN NOT (`,
25858
+ i.selectUsing,
25859
+ // The row's own bucket (`o.bucket_id`), so no value is bound for it.
25860
+ `) 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.status <> 'ready' OR o.lock_id IS NOT NULL THEN ${marker("busy", nonce)} WHEN NOT (`,
25861
+ i.updateUsing,
25862
+ `) THEN ${marker("denied", nonce)} WHEN ? > o.size AND ${PROJECT_BYTES} + ? - o.size > ? THEN ${marker("quota", nonce)} ELSE (`,
25863
+ i.check,
25864
+ // RETURNING names the columns bare: it does not see the UPDATE's
25865
+ // alias (SQLite 3.53: `no such column: o.id`), and the table is the
25866
+ // only one there.
25867
+ ") END RETURNING id, version, size, owner, owner_id, created_at"
25868
+ ],
25869
+ [
25870
+ [i.lockId, i.expiresAt, i.pending, i.bucket, i.name],
25871
+ [i.size, i.size, i.limitBytes]
25872
+ ]
25873
+ );
25874
+ }
25875
+ function claimDelete(i) {
25876
+ return compose(
25877
+ [
25878
+ `UPDATE ${STORAGE_OBJECTS_TABLE} AS o SET status = 'deleting', lock_id = ?, lock_expires_at = ? WHERE o.bucket_id = ? AND o.name IN (SELECT value FROM json_each(?)) AND o.status = 'ready' AND o.lock_id IS NULL AND (`,
25879
+ i.using,
25880
+ // Bare, as in claimOverwrite: RETURNING does not see the alias.
25881
+ ") RETURNING id, bucket_id, name, owner, owner_id, version, created_at, updated_at, metadata"
25882
+ ],
25883
+ [[i.lockId, i.expiresAt, i.bucket, JSON.stringify(i.names)]]
25884
+ );
25885
+ }
25886
+ function readObject2(i) {
25887
+ return compose(
25888
+ [
25889
+ `SELECT ${OBJECT_COLUMNS} FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND o.name = ? AND (`,
25890
+ i.using,
25891
+ ")"
25892
+ ],
25893
+ [[i.bucket, i.name]]
25894
+ );
25895
+ }
25896
+ var SORT_COLUMNS = {
25897
+ name: "name",
25898
+ created_at: "created_at",
25899
+ updated_at: "updated_at"
25900
+ };
25901
+ var SORT_ORDERS = { asc: "ASC", desc: "DESC" };
25902
+ var WIDEST_LIST_SORT = {
25903
+ column: "updated_at",
25904
+ order: "desc"
25905
+ };
25906
+ function listObjects(i) {
25907
+ const column2 = Object.hasOwn(SORT_COLUMNS, i.sort.column) ? SORT_COLUMNS[i.sort.column] : void 0;
25908
+ const order = Object.hasOwn(SORT_ORDERS, i.sort.order) ? SORT_ORDERS[i.sort.order] : void 0;
25909
+ if (column2 === void 0 || order === void 0)
25910
+ throw new Error("listObjects: not a sort it knows");
25911
+ const root = i.prefix === "";
25912
+ const p = i.prefix;
25913
+ const rest = root ? "o.name" : "substr(o.name, length(?) + 2)";
25914
+ const restValues = root ? [] : [p];
25915
+ const under = root ? "1" : "o.name >= ? || '/' AND o.name < ? || '0'";
25916
+ const underValues = root ? [] : [p, p];
25917
+ return compose(
25918
+ [
25919
+ `SELECT * FROM (SELECT 0 AS kind, substr(${rest}, 1, instr(${rest}, '/') - 1) AS name, NULL AS id, NULL AS bucket_id, NULL AS owner, NULL AS owner_id, NULL AS version, NULL AS created_at, NULL AS updated_at, NULL AS metadata FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND ${under} AND instr(${rest}, '/') > 0 AND (`,
25920
+ i.using,
25921
+ `) GROUP BY 2 UNION ALL SELECT 1, ${rest}, o.id, o.bucket_id, o.owner, o.owner_id, o.version, o.created_at, o.updated_at, o.metadata FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND o.parent = ? AND (`,
25922
+ i.using,
25923
+ `)) ORDER BY kind, CASE WHEN kind = 0 THEN name END ASC, ${column2} ${order}, name LIMIT ? OFFSET ?`
25924
+ ],
25925
+ [
25926
+ [...restValues, ...restValues, i.bucket, ...underValues, ...restValues],
25927
+ [...restValues, i.bucket, p],
25928
+ [i.limit, i.offset]
25929
+ ]
25930
+ );
25931
+ }
25932
+
25933
+ // ../../packages/data-core/src/storage-policy.ts
25934
+ function refusal(error51) {
25935
+ if (!(error51 instanceof Failure)) throw error51;
25936
+ return error51.reason === void 0 ? { ok: false, error: error51.error } : { ok: false, error: error51.error, reason: error51.reason };
25937
+ }
25938
+ function request(context, policies) {
25939
+ return { ...rlsIdentity(context), policies: policies ?? [] };
25940
+ }
25941
+ function budgetProblem(fragment, action) {
25942
+ if (fragment.params.length > STORAGE_STATEMENT_PARAM_LIMIT)
25943
+ return {
25944
+ ok: false,
25945
+ error: dataError("POLICY_INVALID", {
25946
+ action,
25947
+ table: STORAGE_OBJECTS,
25948
+ values: STORAGE_STATEMENT_PARAM_LIMIT
25949
+ }),
25950
+ reason: `the ${STORAGE_OBJECTS} policies need more than ${String(STORAGE_STATEMENT_PARAM_LIMIT)} values in one statement`
25951
+ };
25952
+ const bytes = utf8ByteLength(fragment.sql);
25953
+ if (bytes > MAX_POLICY_STATEMENT_BYTES)
25954
+ return {
25955
+ ok: false,
25956
+ error: dataError("POLICY_INVALID", {
25957
+ action,
25958
+ table: STORAGE_OBJECTS,
25959
+ bytes: true
25960
+ }),
25961
+ reason: `they write ${String(bytes)}`
25962
+ };
25963
+ return void 0;
25964
+ }
25965
+ function target(snapshot) {
25966
+ return snapshot.storage ? { name: STORAGE_OBJECTS, table: snapshot.storage } : { ok: false, error: dataError("STORAGE_NOT_ENABLED") };
25967
+ }
25968
+ function storageUsing(input) {
25969
+ const { snapshot, context, commands, alias } = input;
25970
+ if (commands.length === 0)
25971
+ throw new Error("storageUsing: name at least one command");
25972
+ assertFragmentAlias(alias, "storageUsing");
25973
+ const on = target(snapshot);
25974
+ if ("ok" in on) return on;
25975
+ const status = `(${quoteIdentifier(alias)}.${quoteIdentifier("status")} = 'ready')`;
25976
+ if (context.role === "service")
25977
+ return { ok: true, fragment: { sql: status, params: [] } };
25978
+ try {
25979
+ const policy = usingFragment(
25980
+ on,
25981
+ request(context, input.policies),
25982
+ snapshot,
25983
+ commands,
25984
+ alias
25985
+ );
25986
+ const fragment = {
25987
+ sql: `(CASE WHEN ${status} AND (${policy.sql}) THEN 1 ELSE 0 END)`,
25988
+ params: policy.params
25989
+ };
25990
+ return budgetProblem(fragment, commands[0] ?? "select") ?? {
25991
+ ok: true,
25992
+ fragment
25993
+ };
25994
+ } catch (error51) {
25995
+ return refusal(error51);
25996
+ }
25997
+ }
25998
+ function storageCheck(input) {
25999
+ const { snapshot, context, command, row, stored, nonce } = input;
26000
+ const on = target(snapshot);
26001
+ if ("ok" in on) return on;
26002
+ if (context.role === "service")
26003
+ return { ok: true, terms: [], restrictive: [] };
26004
+ const req = request(context, input.policies);
26005
+ const found = applicablePolicies(
26006
+ req.policies,
26007
+ STORAGE_OBJECTS,
26008
+ command,
26009
+ req.role
26010
+ );
26011
+ if (found.invalid)
26012
+ return {
26013
+ ok: false,
26014
+ error: policyInvalid(found.invalid, command),
26015
+ reason: found.invalid.invalid ?? "it cannot be read"
26016
+ };
26017
+ if (found.permissive.length === 0)
26018
+ return {
26019
+ ok: false,
26020
+ error: dataError("PERMISSION_DENIED", {
26021
+ action: command,
26022
+ table: STORAGE_OBJECTS,
26023
+ check: false
26024
+ })
26025
+ };
26026
+ try {
26027
+ const terms = checkFragment(
26028
+ on,
26029
+ req,
26030
+ snapshot,
26031
+ command,
26032
+ row,
26033
+ nonce,
26034
+ stored,
26035
+ input.withSelect === true
26036
+ );
26037
+ const problem = budgetProblem(
26038
+ {
26039
+ sql: terms.map((term) => term.sql).join(""),
26040
+ params: terms.flatMap((term) => term.params)
26041
+ },
26042
+ command
26043
+ );
26044
+ if (problem) return problem;
26045
+ const restrictive = terms.flatMap(
26046
+ (term) => term.policy !== void 0 && !term.policy.permissive ? [term.policy.name] : []
26047
+ );
26048
+ return { ok: true, terms, restrictive };
26049
+ } catch (error51) {
26050
+ return refusal(error51);
26051
+ }
26052
+ }
26053
+ function storageCheckCase(terms) {
26054
+ return nestedChecks(terms, { sql: "1", params: [] });
26055
+ }
26056
+
24814
26057
  // ../../packages/data-core/src/policy-check.ts
24815
26058
  var CONTEXTS = [
24816
26059
  { role: "anon" },
@@ -24853,6 +26096,134 @@ function probes(table, context, owners) {
24853
26096
  });
24854
26097
  return queries;
24855
26098
  }
26099
+ var STORAGE_PROBE_ALIAS = "o";
26100
+ var STORAGE_PROBE_NONCE = "f".repeat(STORAGE_NONCE_MAX_DIGITS);
26101
+ var STORAGE_CONTEXTS = [
26102
+ ...CONTEXTS,
26103
+ { role: "authenticated", userId: "naro-policy-check" }
26104
+ ];
26105
+ function storageRefusals(snapshot, table, policies) {
26106
+ const refusals = [];
26107
+ const row = Object.fromEntries(
26108
+ table.columns.map((column2) => [column2.name, ""])
26109
+ );
26110
+ for (const context of STORAGE_CONTEXTS) {
26111
+ const using = (commands) => {
26112
+ const result = storageUsing({
26113
+ snapshot,
26114
+ context,
26115
+ policies,
26116
+ commands,
26117
+ alias: STORAGE_PROBE_ALIAS
26118
+ });
26119
+ if (!result.ok) refusals.push(result);
26120
+ return result.ok ? result.fragment : void 0;
26121
+ };
26122
+ const check2 = (command, newRow, withSelect) => {
26123
+ const result = storageCheck({
26124
+ snapshot,
26125
+ context,
26126
+ policies,
26127
+ command,
26128
+ ...newRow,
26129
+ withSelect,
26130
+ nonce: STORAGE_PROBE_NONCE
26131
+ });
26132
+ if (!result.ok) refusals.push(result);
26133
+ return result.ok ? storageCheckCase(result.terms) : void 0;
26134
+ };
26135
+ const select = using(["select"]);
26136
+ const update = using(["update"]);
26137
+ using(["delete"]);
26138
+ const remove = using(["select", "delete"]);
26139
+ const insertCheck = check2("insert", { row }, false);
26140
+ const upsertCheck = check2("insert", { row }, true);
26141
+ const updateCheck = check2(
26142
+ "update",
26143
+ overwriteCheckRow({ metadata: "", now: "", version: "" }),
26144
+ true
26145
+ );
26146
+ for (const statement2 of storageStatements(
26147
+ select,
26148
+ update,
26149
+ remove,
26150
+ [insertCheck, upsertCheck],
26151
+ updateCheck
26152
+ )) {
26153
+ const problem = storageStatementProblem(statement2, "select");
26154
+ if (problem) refusals.push(problem);
26155
+ }
26156
+ }
26157
+ return refusals;
26158
+ }
26159
+ function storageStatements(select, update, remove, insertChecks, updateCheck) {
26160
+ const statements = [];
26161
+ const value = "";
26162
+ for (const insertCheck of insertChecks)
26163
+ if (insertCheck)
26164
+ statements.push(
26165
+ claimUpload({
26166
+ id: value,
26167
+ bucket: value,
26168
+ name: value,
26169
+ owner: value,
26170
+ metadata: value,
26171
+ size: 0,
26172
+ lockId: value,
26173
+ expiresAt: value,
26174
+ limitBytes: 0,
26175
+ check: insertCheck,
26176
+ nonce: STORAGE_PROBE_NONCE
26177
+ })
26178
+ );
26179
+ if (select && update && updateCheck)
26180
+ statements.push(
26181
+ claimOverwrite({
26182
+ bucket: value,
26183
+ name: value,
26184
+ size: 0,
26185
+ pending: value,
26186
+ lockId: value,
26187
+ expiresAt: value,
26188
+ limitBytes: 0,
26189
+ selectUsing: select,
26190
+ updateUsing: update,
26191
+ check: updateCheck,
26192
+ nonce: STORAGE_PROBE_NONCE
26193
+ })
26194
+ );
26195
+ if (remove)
26196
+ statements.push(
26197
+ claimDelete({
26198
+ bucket: value,
26199
+ names: [],
26200
+ using: remove,
26201
+ lockId: value,
26202
+ expiresAt: value
26203
+ })
26204
+ );
26205
+ if (select)
26206
+ statements.push(
26207
+ readObject2({ bucket: value, name: value, using: select }),
26208
+ listObjects({
26209
+ bucket: value,
26210
+ prefix: "p",
26211
+ limit: 0,
26212
+ offset: 0,
26213
+ sort: WIDEST_LIST_SORT,
26214
+ using: select
26215
+ })
26216
+ );
26217
+ return statements;
26218
+ }
26219
+ function policyOn(snapshot, name) {
26220
+ const table = resolvePolicyTable(snapshot, name);
26221
+ if (!table) return void 0;
26222
+ return asciiLower(name) === STORAGE_OBJECTS ? { table, label: STORAGE_OBJECTS, names: [STORAGE_OBJECTS_QUALIFIER] } : { table, label: table.name, names: [asciiLower(table.name)] };
26223
+ }
26224
+ function tableLabelOf(snapshot, name) {
26225
+ return asciiLower(name) === STORAGE_OBJECTS ? STORAGE_OBJECTS : resolveTable(snapshot, name)?.name ?? name;
26226
+ }
24856
26227
  function namedInRefusal(message, policies) {
24857
26228
  let found;
24858
26229
  for (const policy of policies) {
@@ -24883,16 +26254,21 @@ function problems(snapshot, policies, options) {
24883
26254
  if (table) tables.set(table.name, table);
24884
26255
  }
24885
26256
  for (const policy of loaded) {
24886
- const table = resolveTable(snapshot, policy.table);
24887
- if (!table || policy.invalid !== null) continue;
26257
+ const on = policyOn(snapshot, policy.table);
26258
+ if (!on || policy.invalid !== null) continue;
24888
26259
  for (const expr of [policy.usingExpr, policy.checkExpr]) {
24889
26260
  if (expr === null) continue;
24890
- for (const { written, column: column2 } of truthColumns(snapshot, table, expr)) {
26261
+ for (const { written, column: column2 } of truthColumns(
26262
+ snapshot,
26263
+ on.table,
26264
+ expr,
26265
+ on.names
26266
+ )) {
24891
26267
  if (isNumeric(column2)) continue;
24892
26268
  const message = `Policy ${policy.name} on ${policy.table} cannot be applied: ${written} is a ${columnAffinity(column2)} column, whose truth value is not what its text says ('true' reads as false): compare it, ${written} = 'true'.`;
24893
- found.set(`${table.name}:${message}`, {
26269
+ found.set(`${on.label}:${message}`, {
24894
26270
  problem: {
24895
- table: table.name,
26271
+ table: on.label,
24896
26272
  message,
24897
26273
  policy: policy.name,
24898
26274
  kind: "compile"
@@ -24903,16 +26279,16 @@ function problems(snapshot, policies, options) {
24903
26279
  }
24904
26280
  }
24905
26281
  for (const policy of loaded) {
24906
- const table = resolveTable(snapshot, policy.table);
24907
- if (!table || policy.invalid !== null) continue;
24908
- for (const { written, column: column2 } of numberReadsOf(snapshot, table, policy)) {
26282
+ const on = policyOn(snapshot, policy.table);
26283
+ if (!on || policy.invalid !== null) continue;
26284
+ for (const { written, column: column2 } of numberReadsOf(snapshot, on, policy)) {
24909
26285
  const stored = stringDefault(column2);
24910
26286
  if (stored === void 0) continue;
24911
26287
  if (stored.kind === "text" && storesAsNumber(stored.text)) continue;
24912
26288
  const kept = stored.kind === "text" ? "text" : "a blob";
24913
- found.set(`default:${table.name}:${policy.name}:${written}`, {
26289
+ found.set(`default:${on.label}:${policy.name}:${written}`, {
24914
26290
  problem: {
24915
- table: table.name,
26291
+ table: on.label,
24916
26292
  message: `Policy ${policy.name} on ${policy.table} reads ${written} as a boolean or a number, but its DEFAULT ${String(column2.defaultValue)} is stored as ${kept}, which the policy does not read as it says ('true' reads as false): the rows that took it \u2014 every row there when the column was added, and each service-key insert that leaves it out \u2014 hold that value. Write the default as a number (1, 0, TRUE or FALSE) and update those rows.`,
24917
26293
  policy: policy.name,
24918
26294
  kind: "default"
@@ -24934,6 +26310,23 @@ function problems(snapshot, policies, options) {
24934
26310
  }))
24935
26311
  );
24936
26312
  const compiled = [...loaded, ...probe];
26313
+ const refused = (probed, error51, reason) => {
26314
+ if (error51.code !== "POLICY_INVALID") return;
26315
+ const message = reason === void 0 ? error51.message : `${error51.message.replace(/\.$/, "")}: ${reason}.`;
26316
+ const named = namedInRefusal(error51.message, loaded);
26317
+ const blamed2 = named === void 0 ? probed : tableLabelOf(snapshot, named.table);
26318
+ const key = `${blamed2}:${message}`;
26319
+ if (!found.has(key))
26320
+ found.set(key, {
26321
+ problem: {
26322
+ table: blamed2,
26323
+ message,
26324
+ ...named === void 0 ? {} : { policy: named.name },
26325
+ kind: "compile"
26326
+ },
26327
+ policy: ""
26328
+ });
26329
+ };
24937
26330
  for (const table of tables.values()) {
24938
26331
  const owners = new Set(
24939
26332
  policiesOn(loaded, table.name).flatMap(
@@ -24948,23 +26341,27 @@ function problems(snapshot, policies, options) {
24948
26341
  error: decision.error,
24949
26342
  reason: decision.reason
24950
26343
  };
24951
- if (result.ok || result.error.code !== "POLICY_INVALID") continue;
24952
- const message = result.reason === void 0 ? result.error.message : `${result.error.message.replace(/\.$/, "")}: ${result.reason}.`;
24953
- const named = namedInRefusal(result.error.message, loaded);
24954
- const blamed2 = named === void 0 ? table.name : resolveTable(snapshot, named.table)?.name ?? named.table;
24955
- const key = `${blamed2}:${message}`;
24956
- if (!found.has(key))
24957
- found.set(key, {
24958
- problem: {
24959
- table: blamed2,
24960
- message,
24961
- ...named === void 0 ? {} : { policy: named.name },
24962
- kind: "compile"
24963
- },
24964
- policy: ""
24965
- });
26344
+ if (!result.ok) refused(table.name, result.error, result.reason);
24966
26345
  }
24967
26346
  }
26347
+ if (policiesOn(loaded, STORAGE_OBJECTS).length > 0) {
26348
+ if (snapshot.storage)
26349
+ for (const { error: error51, reason } of storageRefusals(
26350
+ snapshot,
26351
+ snapshot.storage,
26352
+ loaded
26353
+ ))
26354
+ refused(STORAGE_OBJECTS, error51, reason);
26355
+ else
26356
+ found.set(`${STORAGE_OBJECTS}:${STORAGE_UNAVAILABLE_MESSAGE}`, {
26357
+ problem: {
26358
+ table: STORAGE_OBJECTS,
26359
+ message: STORAGE_UNAVAILABLE_MESSAGE,
26360
+ kind: "compile"
26361
+ },
26362
+ policy: ""
26363
+ });
26364
+ }
24968
26365
  if (options.indexes && options.allowUnindexed !== true)
24969
26366
  for (const problem of unindexedSubqueries(
24970
26367
  snapshot,
@@ -24993,13 +26390,13 @@ function policySetProblems(snapshot, policies, options = {}) {
24993
26390
  (a, b) => byText(a.problem.table, b.problem.table) || byText(a.policy, b.policy) || byText(a.problem.message, b.problem.message)
24994
26391
  ).map(({ problem }) => problem);
24995
26392
  }
24996
- function numberReadsOf(snapshot, table, policy) {
26393
+ function numberReadsOf(snapshot, on, policy) {
24997
26394
  const reads = /* @__PURE__ */ new Map();
24998
26395
  for (const expr of [policy.usingExpr, policy.checkExpr]) {
24999
26396
  if (expr === null) continue;
25000
26397
  for (const read of [
25001
- ...truthColumns(snapshot, table, expr),
25002
- ...numberComparedColumns(snapshot, table, expr)
26398
+ ...truthColumns(snapshot, on.table, expr, on.names),
26399
+ ...numberComparedColumns(snapshot, on.table, expr, on.names)
25003
26400
  ]) {
25004
26401
  const key = JSON.stringify([read.table.name, read.column.name]);
25005
26402
  if (isNumeric(read.column) && !reads.has(key)) reads.set(key, read);
@@ -25053,7 +26450,7 @@ function readCreate(stream, start) {
25053
26450
  stream.expectKeyword("policy");
25054
26451
  const name = readPolicyName(stream);
25055
26452
  stream.expectKeyword("on");
25056
- const table = readTableName(stream);
26453
+ const table = readPolicyTarget(stream);
25057
26454
  let permissive = true;
25058
26455
  if (stream.acceptKeyword("as")) {
25059
26456
  const token = stream.next();
@@ -25118,7 +26515,7 @@ function readDrop(stream) {
25118
26515
  }
25119
26516
  const name = readPolicyName(stream);
25120
26517
  stream.expectKeyword("on");
25121
- const table = readTableName(stream);
26518
+ const table = readPolicyTarget(stream);
25122
26519
  if (!stream.acceptKeyword("cascade")) stream.acceptKeyword("restrict");
25123
26520
  readEnd(stream);
25124
26521
  return { kind: "drop", name, table, ifExists };
@@ -25213,7 +26610,7 @@ function readComment(stream) {
25213
26610
  stream.expectKeyword("policy");
25214
26611
  const name = readPolicyName(stream);
25215
26612
  stream.expectKeyword("on");
25216
- const table = readTableName(stream);
26613
+ const table = readPolicyTarget(stream);
25217
26614
  stream.expectKeyword("is");
25218
26615
  const token = stream.next();
25219
26616
  if (isKeyword(token, "null"))
@@ -25229,7 +26626,7 @@ function readComment(stream) {
25229
26626
  function readRowLevelSecurity(stream) {
25230
26627
  if (stream.acceptKeyword("if")) stream.expectKeyword("exists");
25231
26628
  stream.acceptKeyword("only");
25232
- const table = readTableName(stream);
26629
+ const table = readPolicyTarget(stream);
25233
26630
  const token = stream.next();
25234
26631
  let action;
25235
26632
  if (isKeyword(token, "enable")) action = "enable";
@@ -25468,7 +26865,10 @@ function readSqlHead(text2) {
25468
26865
  // ../../packages/data-core/src/policy-migration.ts
25469
26866
  var GRANT_REFUSAL = "naro has no GRANT or REVOKE: every table is behind row-level security, so write a policy instead (CREATE POLICY \u2026).";
25470
26867
  function disableRefusal(table) {
25471
- return `naro keeps row-level security on for every table. To let everyone through, create a policy: CREATE POLICY "open" ON ${quoteIdent(table)} USING (true) WITH CHECK (true);`;
26868
+ return `naro keeps row-level security on for every table. To let everyone through, create a policy: CREATE POLICY "open" ON ${policyTableSql(table)} USING (true) WITH CHECK (true);`;
26869
+ }
26870
+ function isStorageObjects(table) {
26871
+ return asciiLower(table) === STORAGE_OBJECTS;
25472
26872
  }
25473
26873
  function relationMissing(table) {
25474
26874
  return `relation "${table}" does not exist`;
@@ -25549,7 +26949,8 @@ function planPolicyMigration(statements) {
25549
26949
  );
25550
26950
  const statement2 = parsed.statement;
25551
26951
  const table = statement2.kind === "create" ? statement2.policy.table : statement2.table;
25552
- if (isSystemTable(table)) return refuse(`${relationMissing(table)}.`);
26952
+ if (!isStorageObjects(table) && isSystemTable(table))
26953
+ return refuse(`${relationMissing(table)}.`);
25553
26954
  switch (statement2.kind) {
25554
26955
  case "create":
25555
26956
  if (readsBareSubqueryColumn(statement2.policy) && changedAfter(index, tablesInScope(statement2.policy)))
@@ -25595,12 +26996,13 @@ function policyMigrationSql(policy) {
25595
26996
  const create = policySql({ ...policy, owner: null });
25596
26997
  if (policy.owner === null) return create;
25597
26998
  return `${create}
25598
- COMMENT ON POLICY ${quotePolicyName(policy.name)} ON ${quoteIdent(policy.table)} IS ${sqlStringLiteral(`naro:owner ${quoteIdent(policy.owner)}`)};`;
26999
+ COMMENT ON POLICY ${quotePolicyName(policy.name)} ON ${policyTableSql(policy.table)} IS ${sqlStringLiteral(`naro:owner ${quoteIdent(policy.owner)}`)};`;
25599
27000
  }
25600
27001
  function unreadablePolicyComment(policy) {
25601
- return `-- Policy ${commentText(quotePolicyName(policy.name))} on ${commentText(quoteIdent(policy.table))} cannot be read (${commentText(policy.invalid)}); the requests it applies to are refused until it is dropped and created again.`;
27002
+ 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.`;
25602
27003
  }
25603
27004
  var SYSTEM_PREDICATE = systemTablePredicate("name");
27005
+ var STORAGE_EXISTS = `EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${sqlStringLiteral(STORAGE_OBJECTS_TABLE)})`;
25604
27006
 
25605
27007
  // ../../packages/data-core/src/redirect-origins.ts
25606
27008
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
@@ -25725,6 +27127,25 @@ var SYSTEM_MIGRATIONS = [
25725
27127
  name: "named_policies",
25726
27128
  statements: [NAMED_POLICY_TABLE_SQL],
25727
27129
  compute: convertLegacyPolicies
27130
+ },
27131
+ {
27132
+ // Storage (first-release design §6): the buckets and the objects' index
27133
+ // with their counter triggers. Every backend gets them, Storage on or
27134
+ // not — what turns it on is the worker's R2 binding. A worker from
27135
+ // before them never reads them.
27136
+ version: 5,
27137
+ name: "storage",
27138
+ statements: [...STORAGE_TABLES_SQL]
27139
+ },
27140
+ {
27141
+ // Storage 3-1b (known-issues #107, first-release design §14): the mark
27142
+ // a bucket's removal sets before it empties the bucket. The claims and
27143
+ // the worker's bucket reads take a marked bucket as not there. A worker
27144
+ // from before it never reads the column.
27145
+ version: 6,
27146
+ name: "storage_bucket_deleting",
27147
+ statements: [],
27148
+ addColumns: [STORAGE_BUCKET_DELETING_COLUMN]
25728
27149
  }
25729
27150
  ];
25730
27151
 
@@ -26120,7 +27541,10 @@ var POLICY_WARNING_RULES = [
26120
27541
  "rls_policy_unindexed"
26121
27542
  ];
26122
27543
  function policyIssues(input) {
26123
- const present = new Set(input.tables.map(asciiLower));
27544
+ const present = /* @__PURE__ */ new Set([
27545
+ ...input.tables.map(asciiLower),
27546
+ ...input.snapshot.storage ? [STORAGE_OBJECTS] : []
27547
+ ]);
26124
27548
  const found = [
26125
27549
  ...policySetProblems(
26126
27550
  input.snapshot,
@@ -26454,6 +27878,7 @@ function policiesFromFiles(files) {
26454
27878
  let policies = [];
26455
27879
  const tables = /* @__PURE__ */ new Set();
26456
27880
  const forget = (table) => {
27881
+ if (sameTable(table, STORAGE_OBJECTS)) return;
26457
27882
  policies = policies.filter((policy) => !sameTable(policy.table, table));
26458
27883
  };
26459
27884
  for (const [file2, sql] of files.entries()) {
@@ -26501,18 +27926,19 @@ function policiesFromFiles(files) {
26501
27926
  }
26502
27927
  function policiesLostByReset(input) {
26503
27928
  const present = new Set(input.tables.map(asciiLower));
27929
+ if (input.storage === true) present.add(STORAGE_OBJECTS);
26504
27930
  const made = policiesFromFiles(input.files);
26505
27931
  if (made.refused) return { lost: [], withTables: [], refused: made.refused };
27932
+ const remade = new Set(made.tables);
27933
+ if (input.storage === true) remade.add(STORAGE_OBJECTS);
26506
27934
  const gone = input.current.filter(
26507
27935
  (policy) => present.has(asciiLower(policy.table)) && !made.policies.some(
26508
27936
  (other) => sameTable(other.table, policy.table) && other.name === policy.name
26509
27937
  )
26510
27938
  );
26511
27939
  return {
26512
- lost: gone.filter((policy) => made.tables.has(asciiLower(policy.table))),
26513
- withTables: gone.filter(
26514
- (policy) => !made.tables.has(asciiLower(policy.table))
26515
- )
27940
+ lost: gone.filter((policy) => remade.has(asciiLower(policy.table))),
27941
+ withTables: gone.filter((policy) => !remade.has(asciiLower(policy.table)))
26516
27942
  };
26517
27943
  }
26518
27944
  function resetPoliciesLostMessage(lost) {
@@ -27081,6 +28507,7 @@ export {
27081
28507
  schemaMigrationTitle,
27082
28508
  projectPath,
27083
28509
  splitStatements,
28510
+ ANALYTICS_FILTER_KEYS,
27084
28511
  NaroError,
27085
28512
  isNaroError,
27086
28513
  toNaroError,
@@ -27094,6 +28521,9 @@ export {
27094
28521
  login,
27095
28522
  resolveAuth,
27096
28523
  openBrowser,
28524
+ SPEED_INSIGHTS_METRICS,
28525
+ SPEED_INSIGHTS_FILTER_KEYS,
28526
+ SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
27097
28527
  isTerminalStatus,
27098
28528
  BACKEND_STEPS,
27099
28529
  isStalledBackend,
@@ -27107,7 +28537,11 @@ export {
27107
28537
  renderRows,
27108
28538
  summarizeStatement,
27109
28539
  serializeDotenv,
28540
+ MAX_SINGLE_UPLOAD_BYTES,
28541
+ parseCacheControl,
28542
+ STORAGE_OBJECTS_TABLE,
27110
28543
  systemTablePredicate,
28544
+ dataError,
27111
28545
  SCHEMA_SQL,
27112
28546
  tableNames,
27113
28547
  snapshotFromRows,