naro-cli 0.12.0 → 0.13.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;
@@ -18917,6 +18917,7 @@ async function resolveAuth(options = {}) {
18917
18917
  saveSession(session, home);
18918
18918
  }
18919
18919
  }
18920
+ let refreshing = null;
18920
18921
  const auth = {
18921
18922
  source: "session",
18922
18923
  user: session.user,
@@ -18929,9 +18930,19 @@ async function resolveAuth(options = {}) {
18929
18930
  );
18930
18931
  }
18931
18932
  if (!isExpired(current)) return current.access_token;
18932
- const refreshed = await refresh(current.refresh_token);
18933
- saveSession(refreshed, home);
18934
- return refreshed.access_token;
18933
+ const token = current.refresh_token;
18934
+ let flight = refreshing;
18935
+ if (!flight || flight.token !== token) {
18936
+ const promise2 = Promise.resolve(token).then(refresh).then((refreshed) => {
18937
+ saveSession(refreshed, home);
18938
+ return refreshed.access_token;
18939
+ }).finally(() => {
18940
+ if (refreshing?.token === token) refreshing = null;
18941
+ });
18942
+ flight = { token, promise: promise2 };
18943
+ refreshing = flight;
18944
+ }
18945
+ return flight.promise;
18935
18946
  }
18936
18947
  };
18937
18948
  return auth;
@@ -19039,6 +19050,9 @@ var NaroClient = class {
19039
19050
  logs;
19040
19051
  db;
19041
19052
  backend;
19053
+ storage;
19054
+ insights;
19055
+ protection;
19042
19056
  constructor(options) {
19043
19057
  this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
19044
19058
  this.getAccessToken = options.getAccessToken;
@@ -19053,6 +19067,9 @@ var NaroClient = class {
19053
19067
  this.logs = new LogsApi(this);
19054
19068
  this.db = new DbApi(this);
19055
19069
  this.backend = new BackendApi(this);
19070
+ this.storage = new StorageApi(this);
19071
+ this.insights = new InsightsApi(this);
19072
+ this.protection = new ProtectionApi(this);
19056
19073
  }
19057
19074
  async request(path, options) {
19058
19075
  const token = await this.getAccessToken();
@@ -19323,16 +19340,16 @@ var EnvApi = class {
19323
19340
  );
19324
19341
  }
19325
19342
  /** Removes every row for a key (optionally narrowed to one target). */
19326
- async remove(projectId, key, target) {
19343
+ async remove(projectId, key, target2) {
19327
19344
  const rows = await this.list(projectId);
19328
19345
  const matches = rows.filter(
19329
- (row) => row.key === key && (target === void 0 || row.target === target)
19346
+ (row) => row.key === key && (target2 === void 0 || row.target === target2)
19330
19347
  );
19331
19348
  if (matches.length === 0) {
19332
19349
  throw new NaroError(
19333
19350
  "ENV_NOT_FOUND",
19334
- target ? `No ${key} variable found for the ${target} environment.` : `No ${key} variable found.`,
19335
- { details: { key, ...target ? { target } : {} } }
19351
+ target2 ? `No ${key} variable found for the ${target2} environment.` : `No ${key} variable found.`,
19352
+ { details: { key, ...target2 ? { target: target2 } : {} } }
19336
19353
  );
19337
19354
  }
19338
19355
  for (const row of matches) await this.removeById(projectId, row.id);
@@ -19460,6 +19477,116 @@ var LogsApi = class {
19460
19477
  return body.entries;
19461
19478
  }
19462
19479
  };
19480
+ function insightsQuery(options) {
19481
+ const params = new URLSearchParams();
19482
+ if (options.range !== void 0) params.set("range", options.range);
19483
+ if (options.metric !== void 0) params.set("metric", options.metric);
19484
+ if (options.environment !== void 0) params.set("env", options.environment);
19485
+ if (options.route !== void 0) params.set("route", options.route);
19486
+ const text2 = params.toString();
19487
+ return text2 ? `?${text2}` : "";
19488
+ }
19489
+ var InsightsApi = class {
19490
+ constructor(client) {
19491
+ this.client = client;
19492
+ }
19493
+ base(projectId) {
19494
+ return `/api/projects/${encodeURIComponent(projectId)}/speed-insights`;
19495
+ }
19496
+ /** The five metrics' summary, and the trend, pages, devices and elements of one. */
19497
+ async report(projectId, options = {}) {
19498
+ return this.client.request(
19499
+ `${this.base(projectId)}${insightsQuery(options)}`,
19500
+ { context: "Read Speed Insights" }
19501
+ );
19502
+ }
19503
+ /** The paths opened under one route, such as /posts/[id]. */
19504
+ async paths(projectId, options) {
19505
+ return this.client.request(
19506
+ `${this.base(projectId)}/paths${insightsQuery(options)}`,
19507
+ { context: "Read Speed Insights paths" }
19508
+ );
19509
+ }
19510
+ /**
19511
+ * The selected metric for each deployment of the chosen environment
19512
+ * (`options.environment`, production by default), newest first.
19513
+ */
19514
+ async deployments(projectId, options = {}) {
19515
+ return this.client.request(
19516
+ `${this.base(projectId)}/deployments${insightsQuery(options)}`,
19517
+ { context: "Read Speed Insights deployments" }
19518
+ );
19519
+ }
19520
+ /**
19521
+ * The report and the deployments, read side by side. Only the report's
19522
+ * failure fails it: deployments that could not be read come back null,
19523
+ * with the reason, as the dashboard keeps the rest of the panel.
19524
+ */
19525
+ async overview(projectId, options = {}) {
19526
+ const [report, deployments] = await Promise.allSettled([
19527
+ this.report(projectId, options),
19528
+ this.deployments(projectId, options)
19529
+ ]);
19530
+ if (report.status === "rejected") throw report.reason;
19531
+ if (deployments.status === "fulfilled" && Array.isArray(deployments.value.deployments))
19532
+ return {
19533
+ report: report.value,
19534
+ deployments: deployments.value.deployments,
19535
+ deploymentsError: null
19536
+ };
19537
+ const error51 = deployments.status === "fulfilled" ? new NaroError(
19538
+ "INTERNAL_ERROR",
19539
+ "Read Speed Insights deployments failed: the control plane answered without a list of deployments."
19540
+ ) : isNaroError(deployments.reason) ? deployments.reason : toNaroError(deployments.reason);
19541
+ return {
19542
+ report: report.value,
19543
+ deployments: null,
19544
+ deploymentsError: { code: error51.code, message: error51.message }
19545
+ };
19546
+ }
19547
+ /** On or off. The answer is what the row says after the write. */
19548
+ async setEnabled(projectId, enabled) {
19549
+ return this.client.request(this.base(projectId), {
19550
+ method: "PUT",
19551
+ body: { enabled },
19552
+ context: enabled ? "Turn on Speed Insights" : "Turn off Speed Insights"
19553
+ });
19554
+ }
19555
+ };
19556
+ var ProtectionApi = class {
19557
+ constructor(client) {
19558
+ this.client = client;
19559
+ }
19560
+ base(projectId) {
19561
+ return `/api/projects/${encodeURIComponent(projectId)}/preview-protection`;
19562
+ }
19563
+ async get(projectId) {
19564
+ return this.client.request(this.base(projectId), {
19565
+ context: "Read preview protection"
19566
+ });
19567
+ }
19568
+ /** On or off. The edge picks it up within about a minute. */
19569
+ async set(projectId, enabled) {
19570
+ return this.client.request(this.base(projectId), {
19571
+ method: "PUT",
19572
+ body: { enabled },
19573
+ context: enabled ? "Turn on preview protection" : "Turn off preview protection"
19574
+ });
19575
+ }
19576
+ /** A new bypass secret, replacing any earlier one. Shown once. */
19577
+ async createBypass(projectId) {
19578
+ return this.client.request(
19579
+ `${this.base(projectId)}/bypass`,
19580
+ { method: "POST", context: "Create a preview bypass secret" }
19581
+ );
19582
+ }
19583
+ async removeBypass(projectId) {
19584
+ return this.client.request(
19585
+ `${this.base(projectId)}/bypass`,
19586
+ { method: "DELETE", context: "Remove the preview bypass secret" }
19587
+ );
19588
+ }
19589
+ };
19463
19590
  var EXPORT_FIRST_POLL_MS = 1e3;
19464
19591
  var EXPORT_MAX_POLL_MS = 5e3;
19465
19592
  var EXPORT_TIMEOUT_MS = 15 * 6e4;
@@ -19549,8 +19676,8 @@ var DbApi = class {
19549
19676
  * the migration ledger alike - and cancels queries in flight. Restoring to
19550
19677
  * `previousBookmark` undoes it.
19551
19678
  */
19552
- async restore(projectId, target) {
19553
- const given = target;
19679
+ async restore(projectId, target2) {
19680
+ const given = target2;
19554
19681
  const body = await this.client.request(
19555
19682
  this.path(projectId, "/restore"),
19556
19683
  {
@@ -19716,6 +19843,39 @@ async function authChange(answer) {
19716
19843
  }
19717
19844
  return { auth: authConfig(body.auth), backend: backendInfo(body.backend) };
19718
19845
  }
19846
+ function storageBucket(body) {
19847
+ return {
19848
+ name: body.name,
19849
+ public: body.public,
19850
+ fileSizeLimit: body.fileSizeLimit ?? null,
19851
+ allowedMimeTypes: body.allowedMimeTypes ?? null,
19852
+ objects: body.objects,
19853
+ bytes: body.bytes,
19854
+ createdAt: body.createdAt,
19855
+ updatedAt: body.updatedAt,
19856
+ deletingAt: body.deletingAt ?? null
19857
+ };
19858
+ }
19859
+ function storageInfo(body) {
19860
+ return {
19861
+ enabled: body.enabled,
19862
+ bucket: body.bucket ?? null,
19863
+ limits: {
19864
+ bytes: body.limits.bytes,
19865
+ objectBytes: body.limits.objectBytes,
19866
+ singleUploadBytes: body.limits.singleUploadBytes,
19867
+ dailyOps: body.limits.dailyOps ?? null
19868
+ },
19869
+ usage: body.usage ? { bytes: body.usage.bytes, objects: body.usage.objects } : null,
19870
+ today: body.today ?? null,
19871
+ buckets: (body.buckets ?? []).map(storageBucket)
19872
+ };
19873
+ }
19874
+ function bucketFields(fields, into = {}) {
19875
+ for (const key of ["public", "fileSizeLimit", "allowedMimeTypes"])
19876
+ if (fields[key] !== void 0) into[key] = fields[key];
19877
+ return into;
19878
+ }
19719
19879
  var BackendApi = class {
19720
19880
  constructor(client) {
19721
19881
  this.client = client;
@@ -19880,10 +20040,10 @@ var BackendApi = class {
19880
20040
  );
19881
20041
  }
19882
20042
  /** Removes email or one provider; the same answer as `setAuth`. */
19883
- async removeAuth(projectId, target) {
20043
+ async removeAuth(projectId, target2) {
19884
20044
  return authChange(
19885
20045
  this.client.request(
19886
- this.path(projectId, `/auth?target=${encodeURIComponent(target)}`),
20046
+ this.path(projectId, `/auth?target=${encodeURIComponent(target2)}`),
19887
20047
  {
19888
20048
  method: "DELETE",
19889
20049
  context: "Change sign-in settings",
@@ -20054,6 +20214,193 @@ var BackendApi = class {
20054
20214
  return `/rls/policies/${encodeURIComponent(table)}/${encodeURIComponent(name)}`;
20055
20215
  }
20056
20216
  };
20217
+ var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
20218
+ var StorageApi = class {
20219
+ constructor(client) {
20220
+ this.client = client;
20221
+ }
20222
+ path(projectId, suffix = "") {
20223
+ return `/api/projects/${encodeURIComponent(projectId)}/backend/storage${suffix}`;
20224
+ }
20225
+ /**
20226
+ * `encodeURIComponent` leaves `.` and `..` as they are and URL parsing
20227
+ * folds them (`/buckets/..` is `…/backend/storage` itself), an empty
20228
+ * name is the collection, and `encodeURIComponent` throws on a lone
20229
+ * surrogate ("URI malformed", which surfaced as INTERNAL_ERROR — PR #90
20230
+ * review 1): refused here, in naro-js's words for a name no URL can carry
20231
+ * (its `encodeBucket`). No bucket can have one of them.
20232
+ */
20233
+ bucketPath(projectId, bucket, suffix = "") {
20234
+ if (bucket === "" || bucket === "." || bucket === ".." || LONE_SURROGATE.test(bucket))
20235
+ throw new NaroError("INVALID_PATH", "The bucket name is not valid.");
20236
+ return this.path(
20237
+ projectId,
20238
+ `/buckets/${encodeURIComponent(bucket)}${suffix}`
20239
+ );
20240
+ }
20241
+ /**
20242
+ * Whether Storage is on, its R2 bucket, the limits its worker enforces,
20243
+ * what is stored and the buckets. A project that never turned it on answers
20244
+ * `enabled: false` with no buckets.
20245
+ */
20246
+ async info(projectId) {
20247
+ const body = await this.client.request(
20248
+ this.path(projectId),
20249
+ { context: "Read storage" }
20250
+ );
20251
+ return storageInfo(body.storage);
20252
+ }
20253
+ /**
20254
+ * Turns Storage on: the project's R2 bucket (named after the project, so
20255
+ * a repeat finds it), the Naro worker uploaded again with its binding,
20256
+ * the plan's limits recorded and bound with it — again after a plan
20257
+ * change. Safe to repeat, and a repair. The project must have a backend,
20258
+ * turned on, with its database. A backend in `error` or with a run that
20259
+ * stalled is not refused: its provisioning is resumed on the way (with
20260
+ * the bucket's binding). Two minutes, like `backend.enable`.
20261
+ *
20262
+ * Refusals, in the order they are checked: `BACKEND_DISABLED` (the
20263
+ * control plane's backend switch, before the role), `FORBIDDEN`,
20264
+ * `STORAGE_DISABLED` (the control plane's Storage switch),
20265
+ * `DATABASE_NOT_ENABLED` (the project's database is external),
20266
+ * `BACKEND_NOT_READY` (409, only when the project has no backend at all),
20267
+ * `BACKEND_OFF`, `BACKEND_BUSY`, `STORAGE_ENABLE_FAILED` (502: the R2
20268
+ * bucket could not be made — or, on a project that has Storage already, a
20269
+ * plan change's new limits could not be saved; nothing changed), and
20270
+ * `BACKEND_PROVISION_FAILED` (502) when Storage is recorded as on but the
20271
+ * worker's upload stopped. The last one's `details` hold a
20272
+ * `StorageProvisionFailure` — `{ storage: { applied: false, enabled: true
20273
+ * }, backend }`, the backend read like `backend.info` — and `enable`
20274
+ * again finishes.
20275
+ *
20276
+ * A 200 can come back while the backend is still `provisioning` — when
20277
+ * another run (the stall sweep) took the work over before this one
20278
+ * finished. Then `usage` is `null` (it is summed only on a ready backend);
20279
+ * `backend.info` says how far the run got.
20280
+ */
20281
+ async enable(projectId) {
20282
+ let body;
20283
+ try {
20284
+ body = await this.client.request(
20285
+ this.path(projectId),
20286
+ {
20287
+ method: "POST",
20288
+ context: "Turn on Storage",
20289
+ timeoutMs: PROVISION_TIMEOUT_MS
20290
+ }
20291
+ );
20292
+ } catch (error51) {
20293
+ const details = isNaroError(error51) ? error51.details : void 0;
20294
+ if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.storage) && isRecord(details.backend))
20295
+ throw new NaroError(error51.code, error51.message, {
20296
+ ...error51.status !== void 0 ? { status: error51.status } : {},
20297
+ details: {
20298
+ ...details,
20299
+ backend: backendInfo(details.backend)
20300
+ }
20301
+ });
20302
+ throw error51;
20303
+ }
20304
+ return storageInfo(body.storage);
20305
+ }
20306
+ /** Every bucket, by name. `[]` where there can be none (Storage not on). */
20307
+ async listBuckets(projectId) {
20308
+ const body = await this.client.request(
20309
+ this.path(projectId, "/buckets"),
20310
+ { context: "List buckets" }
20311
+ );
20312
+ return (body.buckets ?? []).map(storageBucket);
20313
+ }
20314
+ /**
20315
+ * Makes a bucket. `BUCKET_EXISTS` (409) for a taken name, `BUCKET_LIMIT`
20316
+ * (400) past 100 buckets, `STORAGE_NOT_ENABLED` (409) before `enable`; a
20317
+ * name, limit or type list the control plane does not accept is
20318
+ * `VALIDATION_FAILED` with its reason.
20319
+ */
20320
+ async createBucket(projectId, input) {
20321
+ const body = await this.client.request(
20322
+ this.path(projectId, "/buckets"),
20323
+ {
20324
+ method: "POST",
20325
+ body: bucketFields(input, { name: input.name }),
20326
+ context: "Create a bucket"
20327
+ }
20328
+ );
20329
+ return storageBucket(body.bucket);
20330
+ }
20331
+ /** One bucket; `BUCKET_NOT_FOUND` (404) when there is none of that name. */
20332
+ async getBucket(projectId, bucket) {
20333
+ const body = await this.client.request(
20334
+ this.bucketPath(projectId, bucket),
20335
+ { context: "Read a bucket" }
20336
+ );
20337
+ return storageBucket(body.bucket);
20338
+ }
20339
+ /**
20340
+ * Changes the fields given and returns the bucket as stored. `{}` is
20341
+ * `VALIDATION_FAILED` "Set at least one of public, fileSizeLimit or
20342
+ * allowedMimeTypes." A bucket cannot be renamed.
20343
+ */
20344
+ async updateBucket(projectId, bucket, patch) {
20345
+ const body = await this.client.request(
20346
+ this.bucketPath(projectId, bucket),
20347
+ {
20348
+ method: "PATCH",
20349
+ body: bucketFields(patch),
20350
+ context: "Change a bucket"
20351
+ }
20352
+ );
20353
+ return storageBucket(body.bucket);
20354
+ }
20355
+ /**
20356
+ * Removes a bucket. Without `empty` only an empty one goes
20357
+ * (`BUCKET_NOT_EMPTY`, 409 — its message names the `empty: true` form:
20358
+ * a row whose bytes never reached R2, like a dead upload's, outlives
20359
+ * `emptyBucket`, and only this removal clears it), and that works while
20360
+ * the backend is off. With
20361
+ * `empty: true` the bucket is closed to new objects, emptied through the
20362
+ * project's Naro worker and then removed (the backend must be on and
20363
+ * ready; `BACKEND_UNAVAILABLE` if the worker cannot be reached): `{ ok:
20364
+ * true }` once it is gone, or, for a bucket too big for one call
20365
+ * (answered 202), where it stopped — the same call again goes on. The
20366
+ * emptying form waits two minutes like `enable`: the control plane stops
20367
+ * starting pages after 20 s, but a last one can run 25 s more, plus the
20368
+ * calls that mark and delete the bucket, which would not fit 45 s.
20369
+ */
20370
+ async removeBucket(projectId, bucket, options = {}) {
20371
+ const body = await this.client.request(
20372
+ this.bucketPath(
20373
+ projectId,
20374
+ bucket,
20375
+ options.empty === true ? "?empty=true" : ""
20376
+ ),
20377
+ {
20378
+ method: "DELETE",
20379
+ context: "Remove a bucket",
20380
+ ...options.empty === true ? { timeoutMs: PROVISION_TIMEOUT_MS } : {}
20381
+ }
20382
+ );
20383
+ if (body.done === false)
20384
+ return {
20385
+ deleted: body.deleted ?? 0,
20386
+ done: false,
20387
+ ...typeof body.message === "string" ? { message: body.message } : {}
20388
+ };
20389
+ return { ok: true };
20390
+ }
20391
+ /**
20392
+ * Deletes up to 1,000 of the bucket's objects through the project's Naro
20393
+ * worker, which must be reachable (the backend on and ready). Ask again
20394
+ * until `done`.
20395
+ */
20396
+ async emptyBucket(projectId, bucket) {
20397
+ const body = await this.client.request(
20398
+ this.bucketPath(projectId, bucket, "/empty"),
20399
+ { method: "POST", context: "Empty a bucket" }
20400
+ );
20401
+ return { deleted: body.deleted, done: body.done };
20402
+ }
20403
+ };
20057
20404
 
20058
20405
  // ../../packages/sdk/src/database.ts
20059
20406
  var BOOKMARK_RE = /^[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{32}$/;
@@ -20399,6 +20746,105 @@ function detectFramework2(dir) {
20399
20746
  };
20400
20747
  }
20401
20748
 
20749
+ // ../../packages/data-core/src/storage-path.ts
20750
+ var MAX_SINGLE_UPLOAD_BYTES = 52428800;
20751
+ var STORAGE_RESERVED_WORDS = [
20752
+ "authenticated",
20753
+ "copy",
20754
+ "info",
20755
+ "list",
20756
+ "move",
20757
+ "public",
20758
+ "render",
20759
+ "sign",
20760
+ "upload"
20761
+ ];
20762
+ var RESERVED = new Set(STORAGE_RESERVED_WORDS);
20763
+ var ENCODER = new TextEncoder();
20764
+ var LONE_SURROGATE2 = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
20765
+ var LONE_SURROGATES = new RegExp(LONE_SURROGATE2.source, "g");
20766
+ function objectParent(name) {
20767
+ const slash = name.lastIndexOf("/");
20768
+ return slash < 0 ? "" : name.slice(0, slash);
20769
+ }
20770
+ var TOKEN_CHARACTERS = "[!#$%&'*+\\-.^_`|~0-9A-Za-z]";
20771
+ var TOKEN = new RegExp(`^${TOKEN_CHARACTERS}+$`);
20772
+ var MEDIA_TYPE = new RegExp(`^${TOKEN_CHARACTERS}+/${TOKEN_CHARACTERS}+$`);
20773
+ var DEFAULT_MAX_AGE = 3600;
20774
+ var MAX_MAX_AGE = 31536e3;
20775
+ function parseCacheControl(header) {
20776
+ if (header === null) return { ok: true, maxAge: DEFAULT_MAX_AGE };
20777
+ const match = /^[ \t]*max-age=(\d{1,8})[ \t]*$/i.exec(header);
20778
+ if (match?.[1] === void 0) return { ok: false };
20779
+ const maxAge = Number(match[1]);
20780
+ return maxAge <= MAX_MAX_AGE ? { ok: true, maxAge } : { ok: false };
20781
+ }
20782
+ var MAX_NAME_IN_MESSAGE = 256;
20783
+ function storageName(text2) {
20784
+ if (text2.length <= MAX_NAME_IN_MESSAGE) return text2;
20785
+ const cut = text2.slice(0, MAX_NAME_IN_MESSAGE);
20786
+ const last = cut.charCodeAt(MAX_NAME_IN_MESSAGE - 1);
20787
+ return last >= 55296 && last <= 56319 ? cut.slice(0, MAX_NAME_IN_MESSAGE - 1) : cut;
20788
+ }
20789
+
20790
+ // ../../packages/data-core/src/storage-table.ts
20791
+ var STORAGE_OBJECTS = "storage.objects";
20792
+ var STORAGE_OBJECTS_TABLE = "_naro_storage_objects";
20793
+ var STORAGE_OBJECTS_QUALIFIER = "objects";
20794
+ var STORAGE_UNAVAILABLE_MESSAGE = "storage.objects is not available on this backend yet. Run `naro backend enable` to update it.";
20795
+ var STORAGE_BUCKETS_TABLE = "_naro_storage_buckets";
20796
+ var STORAGE_POLICY_COLUMNS = [
20797
+ "id",
20798
+ "bucket_id",
20799
+ "name",
20800
+ "owner",
20801
+ "owner_id",
20802
+ "metadata",
20803
+ "version",
20804
+ "created_at",
20805
+ "updated_at"
20806
+ ];
20807
+ var STORAGE_TABLES_SQL = [
20808
+ `CREATE TABLE IF NOT EXISTS _naro_storage_buckets (
20809
+ id TEXT PRIMARY KEY,
20810
+ public INTEGER NOT NULL DEFAULT 0 CHECK (public IN (0, 1)),
20811
+ file_size_limit INTEGER CHECK (file_size_limit IS NULL OR file_size_limit > 0),
20812
+ allowed_mime_types TEXT,
20813
+ object_count INTEGER NOT NULL DEFAULT 0,
20814
+ total_bytes INTEGER NOT NULL DEFAULT 0,
20815
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
20816
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
20817
+ )`,
20818
+ `CREATE TABLE IF NOT EXISTS _naro_storage_objects (
20819
+ id TEXT PRIMARY KEY,
20820
+ bucket_id TEXT NOT NULL REFERENCES _naro_storage_buckets (id),
20821
+ name TEXT NOT NULL,
20822
+ owner TEXT,
20823
+ owner_id TEXT,
20824
+ metadata TEXT,
20825
+ version TEXT,
20826
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
20827
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
20828
+ parent TEXT NOT NULL,
20829
+ size INTEGER NOT NULL,
20830
+ status TEXT NOT NULL CHECK (status IN ('uploading', 'ready', 'deleting')),
20831
+ lock_id TEXT,
20832
+ lock_expires_at TEXT,
20833
+ pending TEXT,
20834
+ UNIQUE (bucket_id, name)
20835
+ )`,
20836
+ "CREATE INDEX IF NOT EXISTS _naro_storage_objects_parent ON _naro_storage_objects (bucket_id, parent, name)",
20837
+ "CREATE INDEX IF NOT EXISTS _naro_storage_objects_lock ON _naro_storage_objects (lock_expires_at) WHERE lock_expires_at IS NOT NULL",
20838
+ "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",
20839
+ "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",
20840
+ "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"
20841
+ ];
20842
+ var STORAGE_BUCKET_DELETING_COLUMN = {
20843
+ table: STORAGE_BUCKETS_TABLE,
20844
+ column: "deleting_at",
20845
+ definition: "TEXT"
20846
+ };
20847
+
20402
20848
  // ../../packages/data-core/src/system-tables.ts
20403
20849
  var SYSTEM_TABLE_PREFIXES = [
20404
20850
  "_naro_",
@@ -20434,6 +20880,7 @@ var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
20434
20880
 
20435
20881
  // ../../packages/data-core/src/errors.ts
20436
20882
  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.";
20883
+ var STORAGE_MIGRATION_PENDING_MESSAGE = "The project backend is partway through an update. Run `naro backend enable` to finish it, then try again.";
20437
20884
  var CATALOG = {
20438
20885
  INVALID_API_KEY: {
20439
20886
  status: 401,
@@ -20517,9 +20964,9 @@ var CATALOG = {
20517
20964
  if (recursion)
20518
20965
  return `Infinite recursion detected in policy for relation ${table}.`;
20519
20966
  if (values)
20520
- return `The policies of ${table} bind more than 50 values in one statement.`;
20967
+ return `The policies of ${table} bind more than ${values === true ? "50" : String(values)} values in one statement.`;
20521
20968
  if (bytes)
20522
- return `The policies of ${table} write more than 50000 bytes of SQL into one statement.`;
20969
+ return `The policies of ${table} write more than ${bytes === true ? "50000" : String(bytes)} bytes of SQL into one statement.`;
20523
20970
  if (policy !== void 0)
20524
20971
  return `Policy ${policy} on ${table} cannot be applied.`;
20525
20972
  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 +20986,87 @@ var CATALOG = {
20539
20986
  DATABASE_FULL: {
20540
20987
  status: 507,
20541
20988
  message: () => DATABASE_FULL_MESSAGE
20989
+ },
20990
+ // Naro Storage (3-1a, first-release design §10). A path, a bucket or a type
20991
+ // is the caller's own text, so each is cut to 256 characters in the message
20992
+ // (storageName); nothing is shortened in `details`.
20993
+ INVALID_PATH: {
20994
+ status: 400,
20995
+ message: (vars) => "bucket" in vars ? "The bucket name is not valid." : `The object path is not valid: it ${vars.reason}.`
20996
+ },
20997
+ STORAGE_NOT_ENABLED: {
20998
+ status: 404,
20999
+ message: () => "Storage is not enabled for this project."
21000
+ },
21001
+ BUCKET_NOT_FOUND: {
21002
+ status: 404,
21003
+ message: ({ bucket }) => `Bucket ${storageName(bucket)} was not found.`
21004
+ },
21005
+ OBJECT_NOT_FOUND: {
21006
+ status: 404,
21007
+ message: ({ bucket, path }) => `Object ${storageName(path)} was not found in bucket ${storageName(bucket)}.`
21008
+ },
21009
+ INVALID_CONTENT_TYPE: {
21010
+ status: 400,
21011
+ message: ({ type }) => `Content-Type ${storageName(type)} is not a media type like image/png.`
21012
+ },
21013
+ INVALID_REQUEST: {
21014
+ status: 400,
21015
+ message: () => "The storage request is not valid."
21016
+ },
21017
+ LENGTH_REQUIRED: {
21018
+ status: 411,
21019
+ message: () => "Uploads need a Content-Length header."
21020
+ },
21021
+ OBJECT_TOO_LARGE: {
21022
+ status: 413,
21023
+ 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.`
21024
+ },
21025
+ MIME_TYPE_NOT_ALLOWED: {
21026
+ status: 415,
21027
+ message: ({ bucket, type }) => `Bucket ${storageName(bucket)} does not take ${storageName(type)} objects.`
21028
+ },
21029
+ RANGE_NOT_SATISFIABLE: {
21030
+ status: 416,
21031
+ message: ({ size }) => `The range is outside the object's ${String(size)} bytes.`
21032
+ },
21033
+ OBJECT_EXISTS: {
21034
+ status: 409,
21035
+ message: ({ bucket, path }) => `Object ${storageName(path)} already exists in bucket ${storageName(bucket)}. Upload with upsert to replace it.`
21036
+ },
21037
+ OBJECT_BUSY: {
21038
+ status: 409,
21039
+ message: ({ bucket, path }) => `Object ${storageName(path)} in bucket ${storageName(bucket)} is being changed by another request. Try again shortly.`
21040
+ },
21041
+ INVALID_SIGNATURE: {
21042
+ status: 400,
21043
+ message: () => "The signed URL is not valid."
21044
+ },
21045
+ SIGNED_URL_EXPIRED: {
21046
+ status: 400,
21047
+ message: () => "The signed URL has expired."
21048
+ },
21049
+ OBJECT_RATE_LIMITED: {
21050
+ status: 429,
21051
+ message: ({ path }) => `Object ${storageName(path)} was written less than a second ago. Try again in a second.`
21052
+ },
21053
+ // 507 like DATABASE_FULL: no retry helps until objects are deleted. Reads
21054
+ // and deletes still work.
21055
+ STORAGE_QUOTA_EXCEEDED: {
21056
+ status: 507,
21057
+ message: () => "This project has used all of its storage. Delete objects to free space; uploads fail until then."
21058
+ },
21059
+ // R2 or its binding did not answer; the original error is logged, never sent.
21060
+ STORAGE_UNAVAILABLE: {
21061
+ status: 503,
21062
+ message: () => "The storage backend did not answer. Try again."
21063
+ },
21064
+ // Storage 3-1c (known-issues #114): see ErrorVars. 409 like the control
21065
+ // plane's BACKEND_OUTDATED: nothing the caller sent is wrong, and it
21066
+ // clears once the run finishes.
21067
+ BACKEND_OUTDATED: {
21068
+ status: 409,
21069
+ message: () => STORAGE_MIGRATION_PENDING_MESSAGE
20542
21070
  }
20543
21071
  };
20544
21072
  function dataError(code, ...args) {
@@ -20555,6 +21083,9 @@ function dataError(code, ...args) {
20555
21083
  function invalidQuery(path, reason) {
20556
21084
  return dataError("INVALID_QUERY", void 0, { path, reason });
20557
21085
  }
21086
+ var STORAGE_MIGRATION_PENDING = new RegExp(
21087
+ `^no such column: (?:b\\.|${STORAGE_BUCKET_DELETING_COLUMN.table}\\.)?${STORAGE_BUCKET_DELETING_COLUMN.column}\\b`
21088
+ );
20558
21089
 
20559
21090
  // ../../packages/data-core/src/d1.ts
20560
21091
  function rowsOf(result) {
@@ -20958,7 +21489,8 @@ var SCHEMA_SQL = [
20958
21489
  // alone a column's collation is written. A separate statement, so the
20959
21490
  // text comes once per table rather than once per column. D1 answers
20960
21491
  // `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")}`
21492
+ `SELECT name AS table_name, sql AS table_sql FROM sqlite_master WHERE type = 'table' AND ${systemTablePredicate("name")}`,
21493
+ `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
21494
  ];
20963
21495
  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
21496
  function tableNamesFromRows(rows) {
@@ -20987,7 +21519,8 @@ function snapshotFromRows(rows) {
20987
21519
  if (Object.hasOwn(row, "table_sql") && typeof row.table_name === "string")
20988
21520
  ddl.set(row.table_name, row.table_sql);
20989
21521
  for (const row of rows) {
20990
- if (Object.hasOwn(row, "table_sql")) continue;
21522
+ if (Object.hasOwn(row, "table_sql") || Object.hasOwn(row, "storage_column"))
21523
+ continue;
20991
21524
  const tableName = text(row.table_name);
20992
21525
  const kind = row.table_type;
20993
21526
  if (tableName === "" || kind !== "table" && kind !== "virtual") continue;
@@ -21038,11 +21571,54 @@ function snapshotFromRows(rows) {
21038
21571
  sql: table.kind === "table" && typeof sql === "string" ? sql : null
21039
21572
  });
21040
21573
  }
21041
- return { tables };
21574
+ const storage = storageFromRows(rows);
21575
+ return storage ? { tables, storage } : { tables };
21576
+ }
21577
+ var STORAGE_POLICY_COLUMN_SET = new Set(
21578
+ STORAGE_POLICY_COLUMNS
21579
+ );
21580
+ function storageFromRows(rows) {
21581
+ const storageRows = rows.filter(
21582
+ (row) => Object.hasOwn(row, "storage_column")
21583
+ );
21584
+ const columns = [];
21585
+ for (const row of storageRows) {
21586
+ const name = text(row.storage_column);
21587
+ if (!STORAGE_POLICY_COLUMN_SET.has(name)) continue;
21588
+ const defaultValue = row.storage_default;
21589
+ columns.push({
21590
+ name,
21591
+ cid: integer2(row.storage_cid),
21592
+ type: text(row.storage_type),
21593
+ notNull: integer2(row.storage_not_null) === 1,
21594
+ primaryKey: integer2(row.storage_pk),
21595
+ defaultValue: defaultValue === null || defaultValue === void 0 ? null : String(defaultValue),
21596
+ generated: false,
21597
+ collation: "BINARY"
21598
+ });
21599
+ }
21600
+ if (columns.length === 0) return void 0;
21601
+ const sorted2 = columns.toSorted((left, right) => left.cid - right.cid);
21602
+ const sql = storageRows.find(
21603
+ (row) => integer2(row.storage_cid) === 0
21604
+ )?.storage_sql;
21605
+ return {
21606
+ name: STORAGE_OBJECTS_TABLE,
21607
+ kind: "table",
21608
+ columns: sorted2,
21609
+ byName: new Map(sorted2.map((column2) => [asciiLower(column2.name), column2])),
21610
+ sql: typeof sql === "string" ? sql : null
21611
+ };
21042
21612
  }
21043
21613
  function resolveTable(snapshot, name) {
21044
21614
  if (isSystemTable(name)) return void 0;
21045
- return snapshot.tables.get(asciiLower(name));
21615
+ const folded = asciiLower(name);
21616
+ if (folded === STORAGE_OBJECTS) return void 0;
21617
+ return snapshot.tables.get(folded);
21618
+ }
21619
+ function resolvePolicyTable(snapshot, name) {
21620
+ if (asciiLower(name) === STORAGE_OBJECTS) return snapshot.storage;
21621
+ return resolveTable(snapshot, name);
21046
21622
  }
21047
21623
  async function readTableNames(db) {
21048
21624
  return tableNamesFromRows(rowsOf(await db.prepare(TABLE_NAMES_SQL).all()));
@@ -21119,15 +21695,15 @@ function patternTooLong(pattern, caseInsensitive) {
21119
21695
  if (pattern.length > MAX_PATTERN_BYTES * 2) return true;
21120
21696
  return utf8ByteLength(boundPattern(pattern, caseInsensitive)) > MAX_PATTERN_BYTES;
21121
21697
  }
21122
- function isBoolean(target, value) {
21123
- return `coalesce(CAST(${target} AS NUMERIC) ${value ? "<>" : "="} 0, 0)`;
21698
+ function isBoolean(target2, value) {
21699
+ return `coalesce(CAST(${target2} AS NUMERIC) ${value ? "<>" : "="} 0, 0)`;
21124
21700
  }
21125
- var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
21701
+ var LONE_SURROGATE3 = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
21126
21702
  function distinctArrayValues(values) {
21127
21703
  const seen = /* @__PURE__ */ new Set();
21128
21704
  const out = [];
21129
21705
  for (const item of values) {
21130
- const bound = typeof item === "string" ? item.replace(LONE_SURROGATE, "\uFFFD") : item;
21706
+ const bound = typeof item === "string" ? item.replace(LONE_SURROGATE3, "\uFFFD") : item;
21131
21707
  const key = `${typeof bound}:${String(bound)}`;
21132
21708
  if (seen.has(key)) continue;
21133
21709
  seen.add(key);
@@ -21137,12 +21713,12 @@ function distinctArrayValues(values) {
21137
21713
  }
21138
21714
  var MAX_ARRAY_TEXT_LENGTH = 65536;
21139
21715
  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}`;
21716
+ function arraySql(target2, operator, count2, bind2, nullElement = false) {
21717
+ const isNull = target2();
21718
+ const length = target2();
21719
+ const valid = target2();
21720
+ const type = target2();
21721
+ const each = () => `json_each(${target2()}) AS ${ELEMENT}`;
21146
21722
  const element = `${ELEMENT}.value`;
21147
21723
  const list = () => `(${Array.from({ length: count2 }, bind2).join(", ")})`;
21148
21724
  let test;
@@ -21253,13 +21829,16 @@ var POLICY_FUNCTIONS = [
21253
21829
  "coalesce",
21254
21830
  "now"
21255
21831
  ];
21832
+ var STORAGE_HELPERS = ["foldername", "filename", "extension"];
21833
+ var MAX_FOLDERNAME_INDEX = 16;
21256
21834
  var VALUE_KINDS = /* @__PURE__ */ new Set([
21257
21835
  "literal",
21258
21836
  "column",
21259
21837
  "auth",
21260
21838
  "jwt",
21261
21839
  "func",
21262
- "cast"
21840
+ "cast",
21841
+ "storage"
21263
21842
  ]);
21264
21843
  function isValueExpr(expr) {
21265
21844
  return VALUE_KINDS.has(expr.kind);
@@ -21269,7 +21848,7 @@ function isConditionExpr(expr) {
21269
21848
  if (expr.kind === "column") return true;
21270
21849
  return expr.kind === "literal" && (expr.value === null || typeof expr.value === "boolean");
21271
21850
  }
21272
- var RESERVED = /* @__PURE__ */ new Set([
21851
+ var RESERVED2 = /* @__PURE__ */ new Set([
21273
21852
  "all",
21274
21853
  "and",
21275
21854
  "array",
@@ -21309,12 +21888,20 @@ var CLAUSE_WORDS = /* @__PURE__ */ new Set([
21309
21888
  "cross"
21310
21889
  ]);
21311
21890
  var SPECIAL_WORDS = /* @__PURE__ */ new Set([
21312
- ...RESERVED,
21891
+ ...RESERVED2,
21313
21892
  ...CLAUSE_WORDS,
21314
21893
  "symmetric",
21315
21894
  "auth"
21316
21895
  ]);
21317
21896
  var TEXT_TYPES = /* @__PURE__ */ new Set(["text", "varchar", "uuid"]);
21897
+ var STORAGE_SUBQUERY_PROBLEM = "A policy cannot read storage.objects in a subquery yet";
21898
+ var FOLDERNAME_INDEX_PROBLEM = `storage.foldername(name) is available only as (storage.foldername(name))[n] with n from 1 to ${String(MAX_FOLDERNAME_INDEX)}`;
21899
+ function storageHelperColumnProblem(fn) {
21900
+ return `storage.${fn}() takes the name column of storage.objects`;
21901
+ }
21902
+ function quotedStorageObjectsProblem(written) {
21903
+ 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`;
21904
+ }
21318
21905
  function count(state, token) {
21319
21906
  if (++state.nodes > MAX_EXPRESSION_NODES)
21320
21907
  throw new PolicySyntaxError(
@@ -21361,6 +21948,10 @@ function truthProblem(operand) {
21361
21948
  case "now":
21362
21949
  return "now() is a time: compare it, created_at < now()";
21363
21950
  }
21951
+ case "storage": {
21952
+ const call = operand.fn === "foldername" ? "(storage.foldername(\u2026))[n]" : `storage.${operand.fn}(\u2026)`;
21953
+ return `${call} ${text2}: compare it with text, ${call} = 'true'`;
21954
+ }
21364
21955
  }
21365
21956
  }
21366
21957
  function truthTestProblem(operand) {
@@ -21380,6 +21971,7 @@ function isTextValue(value) {
21380
21971
  case "auth":
21381
21972
  case "jwt":
21382
21973
  case "cast":
21974
+ case "storage":
21383
21975
  return true;
21384
21976
  case "func":
21385
21977
  return value.name !== "length" && value.name !== "coalesce";
@@ -21724,7 +22316,7 @@ function readSubquery(state, start, kind) {
21724
22316
  }
21725
22317
  }
21726
22318
  function isReservedWord(token) {
21727
- return token.kind === "ident" && !token.quoted && (RESERVED.has(asciiLower(token.value)) || CLAUSE_WORDS.has(asciiLower(token.value)));
22319
+ return token.kind === "ident" && !token.quoted && (RESERVED2.has(asciiLower(token.value)) || CLAUSE_WORDS.has(asciiLower(token.value)));
21728
22320
  }
21729
22321
  function readIdentifier(stream, what) {
21730
22322
  const token = stream.peek();
@@ -21736,16 +22328,40 @@ function readIdentifier(stream, what) {
21736
22328
  stream.next();
21737
22329
  return token.value;
21738
22330
  }
22331
+ function isName(token, word) {
22332
+ return token.kind === "ident" && (token.quoted ? token.value === word : asciiLower(token.value) === word);
22333
+ }
22334
+ function atStorageObjects(stream) {
22335
+ return isName(stream.peek(), "storage") && isOperator(stream.peek(1), ".") && isName(stream.peek(2), "objects");
22336
+ }
21739
22337
  function readTableName(stream) {
21740
22338
  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")
22339
+ if (atStorageObjects(stream))
22340
+ throw new PolicySyntaxError(STORAGE_SUBQUERY_PROBLEM, first.position);
22341
+ let token = first;
22342
+ let name = readIdentifier(stream, "a table name");
22343
+ if (stream.acceptOperator(".")) {
22344
+ if (first.quoted ? name !== "public" : asciiLower(name) !== "public")
22345
+ throw new PolicySyntaxError(
22346
+ `schema ${name} does not exist: naro's tables are in public`,
22347
+ first.position
22348
+ );
22349
+ token = stream.peek();
22350
+ name = readIdentifier(stream, "a table name");
22351
+ }
22352
+ if (asciiLower(name) === STORAGE_OBJECTS)
21744
22353
  throw new PolicySyntaxError(
21745
- `schema ${name} does not exist: naro's tables are in public`,
21746
- first.position
22354
+ quotedStorageObjectsProblem(name),
22355
+ token.position
21747
22356
  );
21748
- return readIdentifier(stream, "a table name");
22357
+ return name;
22358
+ }
22359
+ function readPolicyTarget(stream) {
22360
+ if (!atStorageObjects(stream)) return readTableName(stream);
22361
+ stream.next();
22362
+ stream.next();
22363
+ stream.next();
22364
+ return STORAGE_OBJECTS;
21749
22365
  }
21750
22366
  function readNumber(token, negative) {
21751
22367
  const value = Number(token.value) * (negative ? -1 : 1);
@@ -21883,11 +22499,18 @@ function readPrimary(state) {
21883
22499
  }
21884
22500
  const inner = readOr(state);
21885
22501
  stream.expectOperator(")");
22502
+ if (!isOperator(stream.peek(), "[")) return inner;
22503
+ if (inner.kind !== "storage" || inner.fn !== "foldername" || inner.index !== null)
22504
+ return stream.unexpected();
22505
+ inner.index = readFolderIndex(state);
22506
+ if (isOperator(stream.peek(), "[")) return stream.unexpected();
21886
22507
  return inner;
21887
22508
  }
21888
22509
  return stream.unexpected(token);
21889
22510
  }
21890
22511
  case "ident": {
22512
+ if (isName(token, "storage") && isOperator(stream.peek(), ".") && stream.peek(1).kind === "ident" && isOperator(stream.peek(2), "("))
22513
+ return readStorageCall(state, token);
21891
22514
  if (!token.quoted) {
21892
22515
  const word = asciiLower(token.value);
21893
22516
  if (word === "true") return { kind: "literal", value: true };
@@ -21902,7 +22525,7 @@ function readPrimary(state) {
21902
22525
  return auth;
21903
22526
  }
21904
22527
  if (isOperator(stream.peek(), "(")) return readCall(state, token);
21905
- if (RESERVED.has(word)) return stream.unexpected(token);
22528
+ if (RESERVED2.has(word)) return stream.unexpected(token);
21906
22529
  }
21907
22530
  if (isOperator(stream.peek(), ".")) {
21908
22531
  stream.next();
@@ -21923,14 +22546,72 @@ function readPrimary(state) {
21923
22546
  return stream.unexpected(token);
21924
22547
  }
21925
22548
  }
22549
+ function readStorageCall(state, start) {
22550
+ const { stream } = state;
22551
+ stream.next();
22552
+ const name = stream.next();
22553
+ const fn = STORAGE_HELPERS.find((helper) => isName(name, helper));
22554
+ if (fn === void 0)
22555
+ throw new PolicySyntaxError(
22556
+ `function storage.${name.value} is not available in policies: use storage.foldername, storage.filename or storage.extension`,
22557
+ start.position
22558
+ );
22559
+ stream.expectOperator("(");
22560
+ const node = {
22561
+ kind: "storage",
22562
+ fn,
22563
+ column: readNameColumn(state, fn),
22564
+ index: null
22565
+ };
22566
+ if (fn === "foldername") state.foldernames.push({ node, token: start });
22567
+ return node;
22568
+ }
22569
+ function readNameColumn(state, fn) {
22570
+ const { stream } = state;
22571
+ const first = stream.peek();
22572
+ const identifier = (token) => token.kind === "ident" && (token.quoted || !isReservedWord(token));
22573
+ const qualified = isOperator(stream.peek(1), ".");
22574
+ const column2 = qualified ? stream.peek(2) : first;
22575
+ const after = stream.peek(qualified ? 3 : 1);
22576
+ const isColumn = identifier(first) && identifier(column2) && isName(column2, "name");
22577
+ if (isColumn && after.kind === "eof") {
22578
+ for (let index = qualified ? 3 : 1; index > 0; index--) stream.next();
22579
+ stream.expectOperator(")");
22580
+ }
22581
+ if (!isColumn || !isOperator(after, ")"))
22582
+ throw new PolicySyntaxError(storageHelperColumnProblem(fn), first.position);
22583
+ count(state, first);
22584
+ for (let index = qualified ? 4 : 2; index > 0; index--) stream.next();
22585
+ return {
22586
+ kind: "column",
22587
+ qualifier: qualified ? first.value : null,
22588
+ name: "name"
22589
+ };
22590
+ }
22591
+ function readFolderIndex(state) {
22592
+ const { stream } = state;
22593
+ stream.next();
22594
+ const token = stream.peek();
22595
+ const index = token.kind === "number" && /^[0-9]+$/.test(token.value) ? Number(token.value) : Number.NaN;
22596
+ if (!(index >= 1 && index <= MAX_FOLDERNAME_INDEX))
22597
+ throw new PolicySyntaxError(FOLDERNAME_INDEX_PROBLEM, token.position);
22598
+ stream.next();
22599
+ count(state, token);
22600
+ stream.expectOperator("]");
22601
+ return index;
22602
+ }
21926
22603
  function readCall(state, name) {
21927
22604
  const { stream } = state;
21928
22605
  const word = asciiLower(name.value);
21929
- if (!POLICY_FUNCTIONS.includes(word))
22606
+ if (!POLICY_FUNCTIONS.includes(word)) {
22607
+ const first = stream.peek(1);
22608
+ if (isName(first, "storage") && isOperator(stream.peek(2), ".") && isName(stream.peek(3), "foldername") && isOperator(stream.peek(4), "("))
22609
+ throw new PolicySyntaxError(FOLDERNAME_INDEX_PROBLEM, first.position);
21930
22610
  throw new PolicySyntaxError(
21931
22611
  `function ${name.value} is not available in policies: use lower, upper, coalesce, length or now`,
21932
22612
  name.position
21933
22613
  );
22614
+ }
21934
22615
  stream.expectOperator("(");
21935
22616
  const args = [];
21936
22617
  if (!isOperator(stream.peek(), ")"))
@@ -21961,12 +22642,17 @@ function newState(stream) {
21961
22642
  depth: 0,
21962
22643
  subqueries: 0,
21963
22644
  subqueryDepth: 0,
21964
- arrays: 0
22645
+ arrays: 0,
22646
+ foldernames: []
21965
22647
  };
21966
22648
  }
21967
22649
  function readCondition(stream) {
21968
22650
  const token = stream.peek();
21969
- const expr = readOr(newState(stream));
22651
+ const state = newState(stream);
22652
+ const expr = readOr(state);
22653
+ const bare = state.foldernames.find(({ node }) => node.index === null);
22654
+ if (bare)
22655
+ throw new PolicySyntaxError(FOLDERNAME_INDEX_PROBLEM, bare.token.position);
21970
22656
  return requireCondition(expr, token);
21971
22657
  }
21972
22658
  function checkExpressionSize(expr, position) {
@@ -22032,6 +22718,10 @@ function printValue(expr) {
22032
22718
  return expr.name === "now" ? "now()" : `${expr.name}(${expr.args.map(printValue).join(", ")})`;
22033
22719
  case "cast":
22034
22720
  return `(${printValue(expr.operand)})::text`;
22721
+ case "storage": {
22722
+ const call = `storage.${expr.fn}(${printValue(expr.column)})`;
22723
+ return expr.index === null ? call : `(${call})[${String(expr.index)}]`;
22724
+ }
22035
22725
  }
22036
22726
  }
22037
22727
  function printSubquery(subquery) {
@@ -22086,6 +22776,9 @@ function unqualifiedInSubquery(expr) {
22086
22776
  case "cast":
22087
22777
  value(node.operand, inside);
22088
22778
  return;
22779
+ case "storage":
22780
+ value(node.column, inside);
22781
+ return;
22089
22782
  default:
22090
22783
  return;
22091
22784
  }
@@ -22220,9 +22913,12 @@ function policyShapeProblem(policy) {
22220
22913
  return `${command === "all" ? "An ALL" : "An UPDATE"} policy needs USING (\u2026), WITH CHECK (\u2026) or both.`;
22221
22914
  return void 0;
22222
22915
  }
22916
+ function policyTableSql(table) {
22917
+ return asciiLower(table) === STORAGE_OBJECTS ? STORAGE_OBJECTS : quoteIdent(table);
22918
+ }
22223
22919
  function policySql(policy) {
22224
22920
  const parts = [
22225
- `CREATE POLICY ${quotePolicyName(policy.name)} ON ${quoteIdent(policy.table)}`
22921
+ `CREATE POLICY ${quotePolicyName(policy.name)} ON ${policyTableSql(policy.table)}`
22226
22922
  ];
22227
22923
  if (!policy.permissive) parts.push("AS RESTRICTIVE");
22228
22924
  if (policy.command !== "all")
@@ -22991,6 +23687,9 @@ function storedSource(table, names, alias) {
22991
23687
  })
22992
23688
  };
22993
23689
  }
23690
+ function tableLabel(table) {
23691
+ return table.name === STORAGE_OBJECTS_TABLE ? STORAGE_OBJECTS : table.name;
23692
+ }
22994
23693
  function missingColumnReason(name, tables) {
22995
23694
  return `column ${name} does not exist on ${tables.join(" or ")}`;
22996
23695
  }
@@ -23008,7 +23707,7 @@ function resolveColumn(env, ref) {
23008
23707
  if (!column2)
23009
23708
  return blamed(
23010
23709
  env,
23011
- `column ${ref.qualifier}.${ref.name} does not exist: ${source.table.name} has no column ${ref.name}`
23710
+ `column ${ref.qualifier}.${ref.name} does not exist: ${tableLabel(source.table)} has no column ${ref.name}`
23012
23711
  );
23013
23712
  return { source, column: column2 };
23014
23713
  }
@@ -23028,7 +23727,7 @@ function resolveColumn(env, ref) {
23028
23727
  env,
23029
23728
  missingColumnReason(
23030
23729
  ref.name,
23031
- env.scopes.map((source) => source.table.name)
23730
+ env.scopes.map((source) => tableLabel(source.table))
23032
23731
  )
23033
23732
  );
23034
23733
  }
@@ -23068,7 +23767,47 @@ function valueSql(env, expr, textual = null) {
23068
23767
  }
23069
23768
  case "cast":
23070
23769
  return `CAST(${valueSql(env, expr.operand, "textual")} AS TEXT)`;
23071
- }
23770
+ case "storage":
23771
+ return storageCallSql(env, expr);
23772
+ }
23773
+ }
23774
+ function pathArray(operand) {
23775
+ return `('["' || replace(replace(${operand()}, '"', '\\"'), '/', '","') || '"]')`;
23776
+ }
23777
+ function storageHelperSql(fn, index, operand) {
23778
+ switch (fn) {
23779
+ case "foldername": {
23780
+ if (index === null || !Number.isInteger(index) || index < 1 || index > MAX_FOLDERNAME_INDEX)
23781
+ throw new RangeError(FOLDERNAME_INDEX_PROBLEM);
23782
+ const n = String(index);
23783
+ return `(CASE WHEN ${n} < json_array_length(${pathArray(operand)}) THEN json_extract(${pathArray(operand)}, '$[${String(index - 1)}]') END)`;
23784
+ }
23785
+ case "filename":
23786
+ return `json_extract(${pathArray(operand)}, '$[#-1]')`;
23787
+ case "extension": {
23788
+ const filename = () => storageHelperSql("filename", null, operand);
23789
+ return `substr(${filename()}, length(rtrim(${filename()}, replace(${filename()}, '.', ''))) + 1)`;
23790
+ }
23791
+ }
23792
+ }
23793
+ function storageCallSql(env, expr) {
23794
+ const problem = storageHelperColumnProblem(expr.fn);
23795
+ if (asciiLower(env.blame.policy.table) !== STORAGE_OBJECTS)
23796
+ return blamed(env, problem);
23797
+ const { source, column: column2 } = resolveColumn(env, expr.column);
23798
+ if (source !== env.scopes[0] || source.table.name !== STORAGE_OBJECTS_TABLE || column2.name !== "name")
23799
+ return blamed(env, problem);
23800
+ const index = expr.fn === "foldername" ? expr.index : null;
23801
+ if (expr.fn === "foldername" ? index === null || !Number.isInteger(index) || index < 1 || index > MAX_FOLDERNAME_INDEX : expr.index !== null)
23802
+ return blamed(env, FOLDERNAME_INDEX_PROBLEM);
23803
+ return storageHelperSql(
23804
+ expr.fn,
23805
+ index,
23806
+ () => source.read(column2, env, {
23807
+ qualified: env.scopes.length > 1,
23808
+ textual: "textual"
23809
+ })
23810
+ );
23072
23811
  }
23073
23812
  function operandSide(env, expr) {
23074
23813
  if (expr.kind === "cast") return operandSide(env, expr.operand);
@@ -23149,10 +23888,10 @@ function conditionSql(env, expr) {
23149
23888
  if (untestable !== void 0) return blamed(env, untestable);
23150
23889
  const known = knownNullTest(env, expr);
23151
23890
  if (known !== void 0) return known;
23152
- const target = valueSql(env, expr.operand);
23891
+ const target2 = valueSql(env, expr.operand);
23153
23892
  if (expr.value === null)
23154
- return `(${target} IS ${expr.not ? "NOT " : ""}NULL)`;
23155
- const test = isBoolean(target, expr.value);
23893
+ return `(${target2} IS ${expr.not ? "NOT " : ""}NULL)`;
23894
+ const test = isBoolean(target2, expr.value);
23156
23895
  return expr.not ? `(NOT (${test}))` : `(${test})`;
23157
23896
  }
23158
23897
  case "in": {
@@ -23171,10 +23910,10 @@ function conditionSql(env, expr) {
23171
23910
  case "like": {
23172
23911
  const problem = policyPatternProblem(expr.pattern, expr.ci);
23173
23912
  if (problem !== void 0) return blamed(env, problem);
23174
- const target = valueSql(env, expr.operand, "pattern");
23913
+ const target2 = valueSql(env, expr.operand, "pattern");
23175
23914
  const not = expr.not ? "NOT " : "";
23176
23915
  const pattern = bind(env, boundPattern(expr.pattern, expr.ci));
23177
- return expr.ci ? `(${target} ${not}LIKE ${pattern} ESCAPE '\\')` : `(${target} ${not}GLOB ${pattern})`;
23916
+ return expr.ci ? `(${target2} ${not}LIKE ${pattern} ESCAPE '\\')` : `(${target2} ${not}GLOB ${pattern})`;
23178
23917
  }
23179
23918
  case "between": {
23180
23919
  if (env.check) {
@@ -23183,10 +23922,10 @@ function conditionSql(env, expr) {
23183
23922
  const both = `(${low2} AND ${high2})`;
23184
23923
  return expr.not ? `(NOT ${both})` : both;
23185
23924
  }
23186
- const target = valueSql(env, expr.operand);
23925
+ const target2 = valueSql(env, expr.operand);
23187
23926
  const low = valueSql(env, expr.low);
23188
23927
  const high = valueSql(env, expr.high);
23189
- return `(${target} ${expr.not ? "NOT " : ""}BETWEEN ${low} AND ${high})`;
23928
+ return `(${target2} ${expr.not ? "NOT " : ""}BETWEEN ${low} AND ${high})`;
23190
23929
  }
23191
23930
  case "array": {
23192
23931
  const values = distinctArrayValues(
@@ -23285,7 +24024,7 @@ function groupTerms(env, sources, action, permissive, restrictive, pick2) {
23285
24024
  expr
23286
24025
  );
23287
24026
  const [source] = sources;
23288
- const problem = source === void 0 ? void 0 : textTruthProblem(env.snapshot, source.table, expr);
24027
+ const problem = source === void 0 ? void 0 : textTruthProblem(env.snapshot, source.table, expr, source.names);
23289
24028
  if (problem !== void 0) fail(policyInvalid(policy, action), problem);
23290
24029
  return { sql, params };
23291
24030
  };
@@ -23375,6 +24114,9 @@ function outerColumns(table, expr) {
23375
24114
  case "cast":
23376
24115
  visitValue(value.operand);
23377
24116
  return;
24117
+ case "storage":
24118
+ visitValue(value.column);
24119
+ return;
23378
24120
  default:
23379
24121
  return;
23380
24122
  }
@@ -23457,6 +24199,9 @@ function columnsReadOn(snapshot, table, expr, written) {
23457
24199
  case "cast":
23458
24200
  visitValue(value.operand, scopes);
23459
24201
  return;
24202
+ case "storage":
24203
+ visitValue(value.column, scopes);
24204
+ return;
23460
24205
  default:
23461
24206
  return;
23462
24207
  }
@@ -23510,19 +24255,20 @@ function columnsReadOn(snapshot, table, expr, written) {
23510
24255
  visit(expr, [{ names: [asciiLower(table.name)], table }]);
23511
24256
  return found;
23512
24257
  }
23513
- function truthColumns(snapshot, table, expr) {
23514
- return comparedColumns(snapshot, table, expr, isBooleanConstant, true);
24258
+ function truthColumns(snapshot, table, expr, names = [asciiLower(table.name)]) {
24259
+ return comparedColumns(snapshot, table, expr, isBooleanConstant, true, names);
23515
24260
  }
23516
- function numberComparedColumns(snapshot, table, expr) {
24261
+ function numberComparedColumns(snapshot, table, expr, names = [asciiLower(table.name)]) {
23517
24262
  return comparedColumns(
23518
24263
  snapshot,
23519
24264
  table,
23520
24265
  expr,
23521
24266
  (value) => value.kind === "literal" && typeof value.value === "number",
23522
- false
24267
+ false,
24268
+ names
23523
24269
  );
23524
24270
  }
23525
- function comparedColumns(snapshot, table, expr, constant, truth) {
24271
+ function comparedColumns(snapshot, table, expr, constant, truth, names = [asciiLower(table.name)]) {
23526
24272
  const found = [];
23527
24273
  const read = (ref, scopes) => {
23528
24274
  const resolved = resolveRef2(ref, scopes);
@@ -23568,11 +24314,11 @@ function comparedColumns(snapshot, table, expr, constant, truth) {
23568
24314
  return;
23569
24315
  }
23570
24316
  };
23571
- visit(expr, [{ names: [asciiLower(table.name)], table }]);
24317
+ visit(expr, [{ names, table }]);
23572
24318
  return found;
23573
24319
  }
23574
- function textTruthProblem(snapshot, table, expr) {
23575
- for (const { written, column: column2 } of truthColumns(snapshot, table, expr))
24320
+ function textTruthProblem(snapshot, table, expr, names = [asciiLower(table.name)]) {
24321
+ for (const { written, column: column2 } of truthColumns(snapshot, table, expr, names))
23576
24322
  if (!isNumeric(column2))
23577
24323
  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
24324
  return void 0;
@@ -23695,7 +24441,7 @@ function arrayValues(value, path) {
23695
24441
  }
23696
24442
  function filterSql(table, filter, params, path) {
23697
24443
  const resolved = column(table, filter.column);
23698
- const target = quoteIdentifier(resolved.name);
24444
+ const target2 = quoteIdentifier(resolved.name);
23699
24445
  const valuePath = `${path}.value`;
23700
24446
  switch (filter.operator) {
23701
24447
  case "eq":
@@ -23713,7 +24459,7 @@ function filterSql(table, filter, params, path) {
23713
24459
  lte: "<="
23714
24460
  }[filter.operator];
23715
24461
  params.push(toBoundValue(scalar(filter.value, valuePath)));
23716
- return `${target} ${operator} ?`;
24462
+ return `${target2} ${operator} ?`;
23717
24463
  }
23718
24464
  case "in": {
23719
24465
  if (!Array.isArray(filter.value))
@@ -23721,7 +24467,7 @@ function filterSql(table, filter, params, path) {
23721
24467
  if (filter.value.length === 0) return "0 = 1";
23722
24468
  for (const [index, item] of filter.value.entries())
23723
24469
  params.push(toBoundValue(scalar(item, `${valuePath}[${index}]`)));
23724
- return `${target} IN (${filter.value.map(() => "?").join(", ")})`;
24470
+ return `${target2} IN (${filter.value.map(() => "?").join(", ")})`;
23725
24471
  }
23726
24472
  case "is": {
23727
24473
  const value = filter.value;
@@ -23729,9 +24475,9 @@ function filterSql(table, filter, params, path) {
23729
24475
  fail(invalidQuery(valuePath, "must be null, true or false"));
23730
24476
  if (value === null) {
23731
24477
  params.push(null);
23732
- return `${target} IS ?`;
24478
+ return `${target2} IS ?`;
23733
24479
  }
23734
- return isBoolean(target, value);
24480
+ return isBoolean(target2, value);
23735
24481
  }
23736
24482
  case "ilike":
23737
24483
  case "like": {
@@ -23741,13 +24487,13 @@ function filterSql(table, filter, params, path) {
23741
24487
  if (problem !== void 0) fail(invalidQuery(valuePath, problem));
23742
24488
  const ilike = filter.operator === "ilike";
23743
24489
  params.push(boundPattern(filter.value, ilike));
23744
- return ilike ? `${target} LIKE ? ESCAPE '\\'` : `${target} GLOB ?`;
24490
+ return ilike ? `${target2} LIKE ? ESCAPE '\\'` : `${target2} GLOB ?`;
23745
24491
  }
23746
24492
  case "contains":
23747
24493
  case "containedBy":
23748
24494
  case "overlaps": {
23749
24495
  const values = arrayValues(filter.value, valuePath);
23750
- const qualified = `${quoteIdentifier(table.name)}.${target}`;
24496
+ const qualified = `${quoteIdentifier(table.name)}.${target2}`;
23751
24497
  let next = 0;
23752
24498
  return arraySql(
23753
24499
  () => qualified,
@@ -23808,22 +24554,22 @@ function statement(sql, params) {
23808
24554
  function assignments(table, row, path) {
23809
24555
  const resolved = /* @__PURE__ */ new Map();
23810
24556
  for (const [key, value] of Object.entries(row)) {
23811
- const target = column(table, key);
23812
- if (target.generated)
24557
+ const target2 = column(table, key);
24558
+ if (target2.generated)
23813
24559
  fail(
23814
24560
  invalidQuery(
23815
24561
  `${path}.${key}`,
23816
- `Column ${target.name} is generated and cannot be written.`
24562
+ `Column ${target2.name} is generated and cannot be written.`
23817
24563
  )
23818
24564
  );
23819
- if (resolved.has(target.name))
24565
+ if (resolved.has(target2.name))
23820
24566
  fail(
23821
24567
  invalidQuery(
23822
24568
  `${path}.${key}`,
23823
- `Column ${target.name} is given more than once.`
24569
+ `Column ${target2.name} is given more than once.`
23824
24570
  )
23825
24571
  );
23826
- resolved.set(target.name, { column: target, value });
24572
+ resolved.set(target2.name, { column: target2, value });
23827
24573
  }
23828
24574
  return resolved;
23829
24575
  }
@@ -23944,13 +24690,21 @@ function checkProblemReason(column2, problem) {
23944
24690
  function isTrue(term) {
23945
24691
  return term.sql === "1" && term.params.length === 0;
23946
24692
  }
24693
+ function targetNames(target2) {
24694
+ return asciiLower(target2.name) === STORAGE_OBJECTS ? [STORAGE_OBJECTS_QUALIFIER] : [asciiLower(target2.name)];
24695
+ }
23947
24696
  var Rls = class {
23948
- constructor(req, snapshot, table, nonce, commands) {
24697
+ constructor(req, snapshot, table, nonce, commands, scope = {
24698
+ policyTable: table.name,
24699
+ names: [asciiLower(table.name)],
24700
+ alias: null
24701
+ }) {
23949
24702
  this.req = req;
23950
24703
  this.snapshot = snapshot;
23951
24704
  this.table = table;
23952
24705
  this.nonce = nonce;
23953
- this.outer = storedSource(table, [asciiLower(table.name)], null);
24706
+ this.policyTable = scope.policyTable;
24707
+ this.outer = storedSource(table, scope.names, scope.alias);
23954
24708
  const all = commands.flatMap((command) => {
23955
24709
  const found = this.found(command);
23956
24710
  return [...found.permissive, ...found.restrictive];
@@ -23964,7 +24718,7 @@ var Rls = class {
23964
24718
  if (!owner || owner.generated)
23965
24719
  fail(
23966
24720
  policyInvalid(policy, command),
23967
- owner ? `the owner column ${owner.name} is a generated column` : `the owner column ${policy.owner} is not on ${table.name}`
24721
+ owner ? `the owner column ${owner.name} is a generated column` : `the owner column ${policy.owner} is not on ${tableLabel(table)}`
23968
24722
  );
23969
24723
  }
23970
24724
  }
@@ -23973,6 +24727,8 @@ var Rls = class {
23973
24727
  /** Restrictive policies named by the checks' markers. */
23974
24728
  restrictive = [];
23975
24729
  outer;
24730
+ /** The name the table's policies are stored under. */
24731
+ policyTable;
23976
24732
  /**
23977
24733
  * A new statement of the same query — an insert's next group of rows:
23978
24734
  * MAX_STATEMENT_SUBQUERIES is a cap on one statement's SQL, so its count
@@ -23986,7 +24742,7 @@ var Rls = class {
23986
24742
  found(command) {
23987
24743
  const found = applicablePolicies(
23988
24744
  this.req.policies,
23989
- this.table.name,
24745
+ this.policyTable,
23990
24746
  command,
23991
24747
  this.req.role
23992
24748
  );
@@ -24037,7 +24793,14 @@ var Rls = class {
24037
24793
  }
24038
24794
  /** WHERE's policy part (spec §5): the command's USING, and SELECT's. */
24039
24795
  where(action) {
24040
- const commands = action === "select" ? ["select"] : [action, "select"];
24796
+ return this.using(action === "select" ? ["select"] : [action, "select"]);
24797
+ }
24798
+ /**
24799
+ * The USING of exactly these commands, ANDed: per command its permissive
24800
+ * policies ORed — `0` when there is none, nothing passes — and each of its
24801
+ * restrictive ones. Null when every term is true.
24802
+ */
24803
+ using(commands) {
24041
24804
  const terms = [];
24042
24805
  for (const command of commands) {
24043
24806
  const found = this.found(command);
@@ -24192,10 +24955,10 @@ var Rls = class {
24192
24955
  * writes there itself and the owner condition compares as text (spec
24193
24956
  * §10 — owner policies are left out of the refusal above too).
24194
24957
  */
24195
- refuseText(action, path, target, value) {
24958
+ refuseText(action, path, target2, value) {
24196
24959
  if (typeof value !== "string") return;
24197
24960
  const { numbers, truths } = this.textReaders ??= this.readersOfText();
24198
- const reader = refusesText(target, value) ? numbers.get(target.name) : truths.get(target.name);
24961
+ const reader = refusesText(target2, value) ? numbers.get(target2.name) : truths.get(target2.name);
24199
24962
  if (reader)
24200
24963
  fail(
24201
24964
  invalidQuery(
@@ -24203,13 +24966,13 @@ var Rls = class {
24203
24966
  mustBeNumber(reader.command === "all" ? action : reader.command)
24204
24967
  )
24205
24968
  );
24206
- if (numberClass(target.type) === "number" && !storesAsNumber(value) && !this.ownId(target, value))
24207
- fail(invalidQuery(path, mustBeNumberFor(target.type)));
24969
+ if (numberClass(target2.type) === "number" && !storesAsNumber(value) && !this.ownId(target2, value))
24970
+ fail(invalidQuery(path, mustBeNumberFor(target2.type)));
24208
24971
  }
24209
24972
  /** The caller's id, in a column an owner policy of this table names. */
24210
- ownId(target, value) {
24973
+ ownId(target2, value) {
24211
24974
  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
24975
+ (policy) => policy.owner !== null && asciiLower(policy.owner) === asciiLower(target2.name) && resolveTable(this.snapshot, policy.table) === this.table
24213
24976
  );
24214
24977
  }
24215
24978
  /**
@@ -24224,19 +24987,19 @@ var Rls = class {
24224
24987
  * text, and a blob `x'…'` — never stored as a number — is refused as text
24225
24988
  * that is no number (P2-7b 최종 리뷰 후속 2).
24226
24989
  */
24227
- refuseOmittedText(path, target) {
24228
- const stored = stringDefault(target);
24990
+ refuseOmittedText(path, target2) {
24991
+ const stored = stringDefault(target2);
24229
24992
  if (stored === void 0) return;
24230
24993
  if (stored.kind === "text" && storesAsNumber(stored.text)) return;
24231
24994
  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);
24995
+ const refused = stored.kind === "text" ? refusesText(target2, stored.text) : numberClass(target2.type) === "number";
24996
+ const reader = refused ? numbers.get(target2.name) : truths.get(target2.name);
24234
24997
  if (reader)
24235
24998
  fail(
24236
24999
  invalidQuery(
24237
25000
  path,
24238
25001
  mustIncludeFor(
24239
- target.name,
25002
+ target2.name,
24240
25003
  reader.command === "all" ? "insert" : reader.command
24241
25004
  )
24242
25005
  )
@@ -24272,7 +25035,7 @@ var Rls = class {
24272
25035
  fail(
24273
25036
  dataError("POLICY_INVALID", {
24274
25037
  action: "select",
24275
- table: this.table.name,
25038
+ table: this.policyTable,
24276
25039
  values: true
24277
25040
  }),
24278
25041
  `they bind ${String(count2)}`
@@ -24285,7 +25048,7 @@ var Rls = class {
24285
25048
  fail(
24286
25049
  dataError("POLICY_INVALID", {
24287
25050
  action: "select",
24288
- table: this.table.name,
25051
+ table: this.policyTable,
24289
25052
  bytes: true
24290
25053
  }),
24291
25054
  `they write ${String(bytes)}`
@@ -24294,13 +25057,7 @@ var Rls = class {
24294
25057
  };
24295
25058
  function checkCase(table, terms, guard, pass, action, nonce) {
24296
25059
  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 };
25060
+ if (!guard) return nestedChecks(terms, pass);
24304
25061
  if (table.sql === null) {
24305
25062
  const first = terms.find((term) => term.policy !== void 0);
24306
25063
  if (first?.kind === "select" || !first?.policy)
@@ -24312,11 +25069,134 @@ function checkCase(table, terms, guard, pass, action, nonce) {
24312
25069
  );
24313
25070
  fail(policyInvalid(first.policy, action));
24314
25071
  }
25072
+ return nestedChecks(
25073
+ [schemaGuard(table.name, table.sql, action, nonce), ...terms],
25074
+ pass
25075
+ );
25076
+ }
25077
+ function schemaGuard(name, sql, kind, nonce) {
24315
25078
  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]
25079
+ sql: "(SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?) = ?",
25080
+ params: [name, sql],
25081
+ marker: schemaMarker(nonce),
25082
+ kind,
25083
+ policy: void 0
24318
25084
  };
24319
25085
  }
25086
+ function nestedChecks(terms, pass) {
25087
+ let sql = pass.sql;
25088
+ let params = pass.params;
25089
+ for (const term of terms.toReversed()) {
25090
+ sql = `CASE WHEN ${term.sql} THEN ${sql} ELSE ${term.marker} END`;
25091
+ params = [...term.params, ...params];
25092
+ }
25093
+ return { sql, params };
25094
+ }
25095
+ function assertFragmentAlias(alias, caller) {
25096
+ if (/^_naro_/i.test(alias))
25097
+ throw new Error(
25098
+ `${caller}: the alias ${alias} is one the compiler gives its subqueries (_naro_\u2026): name the table otherwise`
25099
+ );
25100
+ }
25101
+ function checkNonceOf(nonce, caller) {
25102
+ if (!/^[0-9a-f]{1,64}$/.test(nonce))
25103
+ throw new Error(`${caller}: checkNonce must be 1 to 64 hex digits`);
25104
+ return nonce;
25105
+ }
25106
+ function usingFragment(target2, req, snapshot, commands, alias) {
25107
+ assertFragmentAlias(alias, "usingFragment");
25108
+ const rls = new Rls(req, snapshot, target2.table, "0", commands, {
25109
+ policyTable: target2.name,
25110
+ names: targetNames(target2),
25111
+ alias
25112
+ });
25113
+ return rls.using(commands) ?? { sql: "1", params: [] };
25114
+ }
25115
+ function rowSource(target2, row, action, stored) {
25116
+ const { table } = target2;
25117
+ const kept = new Set(stored?.columns ?? []);
25118
+ const storedRow = stored ? storedSource(table, targetNames(target2), stored.alias) : void 0;
25119
+ const source = {
25120
+ table,
25121
+ names: targetNames(target2),
25122
+ used: false,
25123
+ storedRead: false,
25124
+ read: (item, env, how) => {
25125
+ const check2 = env.check;
25126
+ if (!check2) return fail(dataError("INTERNAL_ERROR"));
25127
+ source.used = true;
25128
+ if (item.generated)
25129
+ check2.problem(item.name, "generated", env.blame.policy);
25130
+ if (storedRow && kept.has(item.name)) {
25131
+ source.storedRead = true;
25132
+ return storedRow.read(item, env, how);
25133
+ }
25134
+ if (how.textual && isNumeric(item))
25135
+ check2.problem(item.name, how.textual, env.blame.policy);
25136
+ if (!Object.hasOwn(row, item.name))
25137
+ fail(
25138
+ dataError("INTERNAL_ERROR"),
25139
+ `the new row of ${target2.name} has no value for ${item.name}`
25140
+ );
25141
+ const bound = row[item.name] ?? null;
25142
+ if (typeof bound === "string" && refusesText(item, bound))
25143
+ fail(invalidQuery(`row.${item.name}`, mustBeNumber(action)));
25144
+ env.params.push(bound);
25145
+ return readsAsIs(item, bound) ? "?" : asStored(item, "?");
25146
+ },
25147
+ // A stored column is the statement's own table's: SQLite compares it
25148
+ // with its own collation, the snapshot's — which the guard holds to.
25149
+ operand: (item) => kept.has(item.name) ? { stable: true, newColumnish: true, newCollation: item.collation } : { stable: false, newColumnish: false, newCollation: null }
25150
+ };
25151
+ return source;
25152
+ }
25153
+ function storedColumnsOf(target2, command, row, stored) {
25154
+ if (stored === void 0) return void 0;
25155
+ if (command !== "update")
25156
+ throw new Error("checkFragment: only an update keeps stored columns");
25157
+ assertFragmentAlias(stored.alias, "checkFragment");
25158
+ for (const name of stored.columns) {
25159
+ if (target2.table.byName.get(asciiLower(name))?.name !== name)
25160
+ throw new Error(`checkFragment: ${name} is no column of ${target2.name}`);
25161
+ if (Object.hasOwn(row, name))
25162
+ throw new Error(
25163
+ `checkFragment: ${name} is both a new value and a stored column`
25164
+ );
25165
+ }
25166
+ return stored;
25167
+ }
25168
+ function checkFragment(target2, req, snapshot, command, row, nonce, stored, withSelect = false) {
25169
+ const checked = checkNonceOf(nonce, "checkFragment");
25170
+ const source = rowSource(
25171
+ target2,
25172
+ row,
25173
+ command,
25174
+ storedColumnsOf(target2, command, row, stored)
25175
+ );
25176
+ const commands = withSelect ? [command, "select"] : [command];
25177
+ const rls = new Rls(req, snapshot, target2.table, checked, commands, {
25178
+ policyTable: target2.name,
25179
+ names: targetNames(target2),
25180
+ alias: null
25181
+ });
25182
+ const terms = rls.checks(
25183
+ command,
25184
+ source,
25185
+ withSelect,
25186
+ () => false,
25187
+ () => false
25188
+ );
25189
+ if (!source.storedRead || terms.length === 0) return terms;
25190
+ const { name, sql } = target2.table;
25191
+ if (sql === null) {
25192
+ const first = terms.find((term) => term.policy !== void 0)?.policy;
25193
+ return fail(
25194
+ first ? policyInvalid(first, command) : dataError("INTERNAL_ERROR"),
25195
+ `naro cannot read the schema of ${target2.name}, so a check of the stored row cannot be guarded`
25196
+ );
25197
+ }
25198
+ return [schemaGuard(name, sql, command, checked), ...terms];
25199
+ }
24320
25200
  function compileSelect(query, table, rls) {
24321
25201
  if (query.columns !== "*" && query.columns.length === 0)
24322
25202
  fail(invalidQuery("columns", "must name at least one column"));
@@ -24531,9 +25411,10 @@ function compileDelete(query, table, context, rls) {
24531
25411
  ];
24532
25412
  }
24533
25413
  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");
25414
+ const nonce = checkNonceOf(
25415
+ options.checkNonce ?? DEFAULT_CHECK_NONCE,
25416
+ "compile"
25417
+ );
24537
25418
  if (!decision.allow)
24538
25419
  return decision.reason === void 0 ? { ok: false, error: decision.error } : { ok: false, error: decision.error, reason: decision.reason };
24539
25420
  const table = resolveTable(snapshot, query.table);
@@ -24609,6 +25490,8 @@ function valueScopes(scopes, value) {
24609
25490
  return value.args.flatMap((arg) => valueScopes(scopes, arg));
24610
25491
  case "cast":
24611
25492
  return valueScopes(scopes, value.operand);
25493
+ case "storage":
25494
+ return valueScopes(scopes, value.column);
24612
25495
  default:
24613
25496
  return [];
24614
25497
  }
@@ -24741,7 +25624,7 @@ function blocked(table, indexes, way) {
24741
25624
  return void 0;
24742
25625
  }
24743
25626
  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`;
25627
+ 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
25628
  const override = "or send it with allow_unindexed=1 (naro policy create --allow-unindexed) to accept a scan of every row";
24746
25629
  const [way] = ways;
24747
25630
  if (!way)
@@ -24758,8 +25641,9 @@ function unindexedSubqueries(snapshot, indexes, policies) {
24758
25641
  const found = [];
24759
25642
  for (const policy of policies) {
24760
25643
  if (policy.invalid !== null) continue;
24761
- const outer = resolveTable(snapshot, policy.table);
25644
+ const outer = resolvePolicyTable(snapshot, policy.table);
24762
25645
  if (!outer) continue;
25646
+ const outerName = asciiLower(policy.table) === STORAGE_OBJECTS ? STORAGE_OBJECTS_QUALIFIER : asciiLower(outer.name);
24763
25647
  const visit = (scopes, expr) => {
24764
25648
  if (expr === null || isValueExpr(expr)) return;
24765
25649
  switch (expr.kind) {
@@ -24804,13 +25688,319 @@ function unindexedSubqueries(snapshot, indexes, policies) {
24804
25688
  return;
24805
25689
  }
24806
25690
  };
24807
- const root = [{ table: outer, names: [asciiLower(outer.name)] }];
25691
+ const root = [{ table: outer, names: [outerName] }];
24808
25692
  visit(root, policy.usingExpr);
24809
25693
  visit(root, policy.checkExpr);
24810
25694
  }
24811
25695
  return found;
24812
25696
  }
24813
25697
 
25698
+ // ../../packages/data-core/src/storage-sql.ts
25699
+ var STORAGE_NONCE_MAX_DIGITS = 64;
25700
+ function nonceOf(nonce) {
25701
+ if (!/^[0-9a-f]+$/.test(nonce) || nonce.length > STORAGE_NONCE_MAX_DIGITS)
25702
+ throw new Error(
25703
+ `storage-sql: the nonce must be 1 to ${String(STORAGE_NONCE_MAX_DIGITS)} hex digits`
25704
+ );
25705
+ return nonce;
25706
+ }
25707
+ function marker(guard, nonce) {
25708
+ return `json_extract('{}', 'naro:storage:${guard}:${nonce}')`;
25709
+ }
25710
+ var STORAGE_STATEMENT_PARAM_LIMIT = 100;
25711
+ var STORAGE_STATEMENT_BYTE_LIMIT = 1e5;
25712
+ function storageStatementProblem(statement2, action) {
25713
+ if (statement2.params.length > STORAGE_STATEMENT_PARAM_LIMIT)
25714
+ return {
25715
+ error: dataError("POLICY_INVALID", {
25716
+ action,
25717
+ table: STORAGE_OBJECTS,
25718
+ values: STORAGE_STATEMENT_PARAM_LIMIT
25719
+ }),
25720
+ reason: `the ${STORAGE_OBJECTS} policies need more than ${String(STORAGE_STATEMENT_PARAM_LIMIT)} values in one statement`
25721
+ };
25722
+ const bytes = utf8ByteLength(statement2.sql);
25723
+ if (bytes > STORAGE_STATEMENT_BYTE_LIMIT)
25724
+ return {
25725
+ error: dataError("POLICY_INVALID", {
25726
+ action,
25727
+ table: STORAGE_OBJECTS,
25728
+ bytes: STORAGE_STATEMENT_BYTE_LIMIT
25729
+ }),
25730
+ reason: `the ${STORAGE_OBJECTS} policies make one statement ${String(bytes)} bytes long, more than ${String(STORAGE_STATEMENT_BYTE_LIMIT)}`
25731
+ };
25732
+ return void 0;
25733
+ }
25734
+ 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";
25735
+ var PROJECT_BYTES = `(SELECT coalesce(sum(total_bytes), 0) FROM ${STORAGE_BUCKETS_TABLE})`;
25736
+ var LIVE_BUCKET = `${STORAGE_BUCKET_DELETING_COLUMN.column} IS NULL`;
25737
+ function compose(parts, values) {
25738
+ let sql = "";
25739
+ const params = [];
25740
+ let next = 0;
25741
+ for (const part of parts) {
25742
+ if (typeof part === "string") {
25743
+ sql += part;
25744
+ const count2 = part.split("?").length - 1;
25745
+ const bound = count2 === 0 ? [] : values[next++] ?? [];
25746
+ if (bound.length !== count2)
25747
+ throw new Error(
25748
+ `storage-sql: ${String(count2)} placeholders, ${String(bound.length)} values`
25749
+ );
25750
+ params.push(...bound);
25751
+ } else {
25752
+ sql += part.sql;
25753
+ params.push(...part.params);
25754
+ }
25755
+ }
25756
+ if (next !== values.length) throw new Error("storage-sql: values left over");
25757
+ return { sql, params };
25758
+ }
25759
+ function claimUpload(i) {
25760
+ const nonce = nonceOf(i.nonce);
25761
+ return compose(
25762
+ [
25763
+ `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 (`,
25764
+ i.check,
25765
+ `) <> 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`
25766
+ ],
25767
+ [
25768
+ [
25769
+ i.id,
25770
+ i.bucket,
25771
+ i.name,
25772
+ i.owner,
25773
+ i.owner,
25774
+ i.metadata,
25775
+ objectParent(i.name),
25776
+ i.size,
25777
+ i.lockId,
25778
+ i.expiresAt,
25779
+ i.bucket
25780
+ ],
25781
+ [i.bucket, i.name, i.bucket, i.name, i.size, i.limitBytes]
25782
+ ]
25783
+ );
25784
+ }
25785
+ var STORAGE_OBJECT_ALIAS = "o";
25786
+ var OVERWRITE_KEPT_COLUMNS = [
25787
+ "id",
25788
+ "bucket_id",
25789
+ "name",
25790
+ "owner",
25791
+ "owner_id",
25792
+ "created_at"
25793
+ ];
25794
+ function overwriteCheckRow(i) {
25795
+ return {
25796
+ row: { metadata: i.metadata, version: i.version, updated_at: i.now },
25797
+ stored: { alias: STORAGE_OBJECT_ALIAS, columns: OVERWRITE_KEPT_COLUMNS }
25798
+ };
25799
+ }
25800
+ function claimOverwrite(i) {
25801
+ const nonce = nonceOf(i.nonce);
25802
+ return compose(
25803
+ [
25804
+ `UPDATE ${STORAGE_OBJECTS_TABLE} AS o SET lock_id = ?, lock_expires_at = ?, pending = ? WHERE o.bucket_id = ? AND o.name = ? AND CASE WHEN NOT (`,
25805
+ i.selectUsing,
25806
+ // The row's own bucket (`o.bucket_id`), so no value is bound for it.
25807
+ `) 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 (`,
25808
+ i.updateUsing,
25809
+ `) THEN ${marker("denied", nonce)} WHEN ? > o.size AND ${PROJECT_BYTES} + ? - o.size > ? THEN ${marker("quota", nonce)} ELSE (`,
25810
+ i.check,
25811
+ // RETURNING names the columns bare: it does not see the UPDATE's
25812
+ // alias (SQLite 3.53: `no such column: o.id`), and the table is the
25813
+ // only one there.
25814
+ ") END RETURNING id, version, size, owner, owner_id, created_at"
25815
+ ],
25816
+ [
25817
+ [i.lockId, i.expiresAt, i.pending, i.bucket, i.name],
25818
+ [i.size, i.size, i.limitBytes]
25819
+ ]
25820
+ );
25821
+ }
25822
+ function claimDelete(i) {
25823
+ return compose(
25824
+ [
25825
+ `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 (`,
25826
+ i.using,
25827
+ // Bare, as in claimOverwrite: RETURNING does not see the alias.
25828
+ ") RETURNING id, bucket_id, name, owner, owner_id, version, created_at, updated_at, metadata"
25829
+ ],
25830
+ [[i.lockId, i.expiresAt, i.bucket, JSON.stringify(i.names)]]
25831
+ );
25832
+ }
25833
+ function readObject2(i) {
25834
+ return compose(
25835
+ [
25836
+ `SELECT ${OBJECT_COLUMNS} FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND o.name = ? AND (`,
25837
+ i.using,
25838
+ ")"
25839
+ ],
25840
+ [[i.bucket, i.name]]
25841
+ );
25842
+ }
25843
+ var SORT_COLUMNS = {
25844
+ name: "name",
25845
+ created_at: "created_at",
25846
+ updated_at: "updated_at"
25847
+ };
25848
+ var SORT_ORDERS = { asc: "ASC", desc: "DESC" };
25849
+ var WIDEST_LIST_SORT = {
25850
+ column: "updated_at",
25851
+ order: "desc"
25852
+ };
25853
+ function listObjects(i) {
25854
+ const column2 = Object.hasOwn(SORT_COLUMNS, i.sort.column) ? SORT_COLUMNS[i.sort.column] : void 0;
25855
+ const order = Object.hasOwn(SORT_ORDERS, i.sort.order) ? SORT_ORDERS[i.sort.order] : void 0;
25856
+ if (column2 === void 0 || order === void 0)
25857
+ throw new Error("listObjects: not a sort it knows");
25858
+ const root = i.prefix === "";
25859
+ const p = i.prefix;
25860
+ const rest = root ? "o.name" : "substr(o.name, length(?) + 2)";
25861
+ const restValues = root ? [] : [p];
25862
+ const under = root ? "1" : "o.name >= ? || '/' AND o.name < ? || '0'";
25863
+ const underValues = root ? [] : [p, p];
25864
+ return compose(
25865
+ [
25866
+ `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 (`,
25867
+ i.using,
25868
+ `) 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 (`,
25869
+ i.using,
25870
+ `)) ORDER BY kind, CASE WHEN kind = 0 THEN name END ASC, ${column2} ${order}, name LIMIT ? OFFSET ?`
25871
+ ],
25872
+ [
25873
+ [...restValues, ...restValues, i.bucket, ...underValues, ...restValues],
25874
+ [...restValues, i.bucket, p],
25875
+ [i.limit, i.offset]
25876
+ ]
25877
+ );
25878
+ }
25879
+
25880
+ // ../../packages/data-core/src/storage-policy.ts
25881
+ function refusal(error51) {
25882
+ if (!(error51 instanceof Failure)) throw error51;
25883
+ return error51.reason === void 0 ? { ok: false, error: error51.error } : { ok: false, error: error51.error, reason: error51.reason };
25884
+ }
25885
+ function request(context, policies) {
25886
+ return { ...rlsIdentity(context), policies: policies ?? [] };
25887
+ }
25888
+ function budgetProblem(fragment, action) {
25889
+ if (fragment.params.length > STORAGE_STATEMENT_PARAM_LIMIT)
25890
+ return {
25891
+ ok: false,
25892
+ error: dataError("POLICY_INVALID", {
25893
+ action,
25894
+ table: STORAGE_OBJECTS,
25895
+ values: STORAGE_STATEMENT_PARAM_LIMIT
25896
+ }),
25897
+ reason: `the ${STORAGE_OBJECTS} policies need more than ${String(STORAGE_STATEMENT_PARAM_LIMIT)} values in one statement`
25898
+ };
25899
+ const bytes = utf8ByteLength(fragment.sql);
25900
+ if (bytes > MAX_POLICY_STATEMENT_BYTES)
25901
+ return {
25902
+ ok: false,
25903
+ error: dataError("POLICY_INVALID", {
25904
+ action,
25905
+ table: STORAGE_OBJECTS,
25906
+ bytes: true
25907
+ }),
25908
+ reason: `they write ${String(bytes)}`
25909
+ };
25910
+ return void 0;
25911
+ }
25912
+ function target(snapshot) {
25913
+ return snapshot.storage ? { name: STORAGE_OBJECTS, table: snapshot.storage } : { ok: false, error: dataError("STORAGE_NOT_ENABLED") };
25914
+ }
25915
+ function storageUsing(input) {
25916
+ const { snapshot, context, commands, alias } = input;
25917
+ if (commands.length === 0)
25918
+ throw new Error("storageUsing: name at least one command");
25919
+ assertFragmentAlias(alias, "storageUsing");
25920
+ const on = target(snapshot);
25921
+ if ("ok" in on) return on;
25922
+ const status = `(${quoteIdentifier(alias)}.${quoteIdentifier("status")} = 'ready')`;
25923
+ if (context.role === "service")
25924
+ return { ok: true, fragment: { sql: status, params: [] } };
25925
+ try {
25926
+ const policy = usingFragment(
25927
+ on,
25928
+ request(context, input.policies),
25929
+ snapshot,
25930
+ commands,
25931
+ alias
25932
+ );
25933
+ const fragment = {
25934
+ sql: `(CASE WHEN ${status} AND (${policy.sql}) THEN 1 ELSE 0 END)`,
25935
+ params: policy.params
25936
+ };
25937
+ return budgetProblem(fragment, commands[0] ?? "select") ?? {
25938
+ ok: true,
25939
+ fragment
25940
+ };
25941
+ } catch (error51) {
25942
+ return refusal(error51);
25943
+ }
25944
+ }
25945
+ function storageCheck(input) {
25946
+ const { snapshot, context, command, row, stored, nonce } = input;
25947
+ const on = target(snapshot);
25948
+ if ("ok" in on) return on;
25949
+ if (context.role === "service")
25950
+ return { ok: true, terms: [], restrictive: [] };
25951
+ const req = request(context, input.policies);
25952
+ const found = applicablePolicies(
25953
+ req.policies,
25954
+ STORAGE_OBJECTS,
25955
+ command,
25956
+ req.role
25957
+ );
25958
+ if (found.invalid)
25959
+ return {
25960
+ ok: false,
25961
+ error: policyInvalid(found.invalid, command),
25962
+ reason: found.invalid.invalid ?? "it cannot be read"
25963
+ };
25964
+ if (found.permissive.length === 0)
25965
+ return {
25966
+ ok: false,
25967
+ error: dataError("PERMISSION_DENIED", {
25968
+ action: command,
25969
+ table: STORAGE_OBJECTS,
25970
+ check: false
25971
+ })
25972
+ };
25973
+ try {
25974
+ const terms = checkFragment(
25975
+ on,
25976
+ req,
25977
+ snapshot,
25978
+ command,
25979
+ row,
25980
+ nonce,
25981
+ stored,
25982
+ input.withSelect === true
25983
+ );
25984
+ const problem = budgetProblem(
25985
+ {
25986
+ sql: terms.map((term) => term.sql).join(""),
25987
+ params: terms.flatMap((term) => term.params)
25988
+ },
25989
+ command
25990
+ );
25991
+ if (problem) return problem;
25992
+ const restrictive = terms.flatMap(
25993
+ (term) => term.policy !== void 0 && !term.policy.permissive ? [term.policy.name] : []
25994
+ );
25995
+ return { ok: true, terms, restrictive };
25996
+ } catch (error51) {
25997
+ return refusal(error51);
25998
+ }
25999
+ }
26000
+ function storageCheckCase(terms) {
26001
+ return nestedChecks(terms, { sql: "1", params: [] });
26002
+ }
26003
+
24814
26004
  // ../../packages/data-core/src/policy-check.ts
24815
26005
  var CONTEXTS = [
24816
26006
  { role: "anon" },
@@ -24853,6 +26043,134 @@ function probes(table, context, owners) {
24853
26043
  });
24854
26044
  return queries;
24855
26045
  }
26046
+ var STORAGE_PROBE_ALIAS = "o";
26047
+ var STORAGE_PROBE_NONCE = "f".repeat(STORAGE_NONCE_MAX_DIGITS);
26048
+ var STORAGE_CONTEXTS = [
26049
+ ...CONTEXTS,
26050
+ { role: "authenticated", userId: "naro-policy-check" }
26051
+ ];
26052
+ function storageRefusals(snapshot, table, policies) {
26053
+ const refusals = [];
26054
+ const row = Object.fromEntries(
26055
+ table.columns.map((column2) => [column2.name, ""])
26056
+ );
26057
+ for (const context of STORAGE_CONTEXTS) {
26058
+ const using = (commands) => {
26059
+ const result = storageUsing({
26060
+ snapshot,
26061
+ context,
26062
+ policies,
26063
+ commands,
26064
+ alias: STORAGE_PROBE_ALIAS
26065
+ });
26066
+ if (!result.ok) refusals.push(result);
26067
+ return result.ok ? result.fragment : void 0;
26068
+ };
26069
+ const check2 = (command, newRow, withSelect) => {
26070
+ const result = storageCheck({
26071
+ snapshot,
26072
+ context,
26073
+ policies,
26074
+ command,
26075
+ ...newRow,
26076
+ withSelect,
26077
+ nonce: STORAGE_PROBE_NONCE
26078
+ });
26079
+ if (!result.ok) refusals.push(result);
26080
+ return result.ok ? storageCheckCase(result.terms) : void 0;
26081
+ };
26082
+ const select = using(["select"]);
26083
+ const update = using(["update"]);
26084
+ using(["delete"]);
26085
+ const remove = using(["select", "delete"]);
26086
+ const insertCheck = check2("insert", { row }, false);
26087
+ const upsertCheck = check2("insert", { row }, true);
26088
+ const updateCheck = check2(
26089
+ "update",
26090
+ overwriteCheckRow({ metadata: "", now: "", version: "" }),
26091
+ true
26092
+ );
26093
+ for (const statement2 of storageStatements(
26094
+ select,
26095
+ update,
26096
+ remove,
26097
+ [insertCheck, upsertCheck],
26098
+ updateCheck
26099
+ )) {
26100
+ const problem = storageStatementProblem(statement2, "select");
26101
+ if (problem) refusals.push(problem);
26102
+ }
26103
+ }
26104
+ return refusals;
26105
+ }
26106
+ function storageStatements(select, update, remove, insertChecks, updateCheck) {
26107
+ const statements = [];
26108
+ const value = "";
26109
+ for (const insertCheck of insertChecks)
26110
+ if (insertCheck)
26111
+ statements.push(
26112
+ claimUpload({
26113
+ id: value,
26114
+ bucket: value,
26115
+ name: value,
26116
+ owner: value,
26117
+ metadata: value,
26118
+ size: 0,
26119
+ lockId: value,
26120
+ expiresAt: value,
26121
+ limitBytes: 0,
26122
+ check: insertCheck,
26123
+ nonce: STORAGE_PROBE_NONCE
26124
+ })
26125
+ );
26126
+ if (select && update && updateCheck)
26127
+ statements.push(
26128
+ claimOverwrite({
26129
+ bucket: value,
26130
+ name: value,
26131
+ size: 0,
26132
+ pending: value,
26133
+ lockId: value,
26134
+ expiresAt: value,
26135
+ limitBytes: 0,
26136
+ selectUsing: select,
26137
+ updateUsing: update,
26138
+ check: updateCheck,
26139
+ nonce: STORAGE_PROBE_NONCE
26140
+ })
26141
+ );
26142
+ if (remove)
26143
+ statements.push(
26144
+ claimDelete({
26145
+ bucket: value,
26146
+ names: [],
26147
+ using: remove,
26148
+ lockId: value,
26149
+ expiresAt: value
26150
+ })
26151
+ );
26152
+ if (select)
26153
+ statements.push(
26154
+ readObject2({ bucket: value, name: value, using: select }),
26155
+ listObjects({
26156
+ bucket: value,
26157
+ prefix: "p",
26158
+ limit: 0,
26159
+ offset: 0,
26160
+ sort: WIDEST_LIST_SORT,
26161
+ using: select
26162
+ })
26163
+ );
26164
+ return statements;
26165
+ }
26166
+ function policyOn(snapshot, name) {
26167
+ const table = resolvePolicyTable(snapshot, name);
26168
+ if (!table) return void 0;
26169
+ return asciiLower(name) === STORAGE_OBJECTS ? { table, label: STORAGE_OBJECTS, names: [STORAGE_OBJECTS_QUALIFIER] } : { table, label: table.name, names: [asciiLower(table.name)] };
26170
+ }
26171
+ function tableLabelOf(snapshot, name) {
26172
+ return asciiLower(name) === STORAGE_OBJECTS ? STORAGE_OBJECTS : resolveTable(snapshot, name)?.name ?? name;
26173
+ }
24856
26174
  function namedInRefusal(message, policies) {
24857
26175
  let found;
24858
26176
  for (const policy of policies) {
@@ -24883,16 +26201,21 @@ function problems(snapshot, policies, options) {
24883
26201
  if (table) tables.set(table.name, table);
24884
26202
  }
24885
26203
  for (const policy of loaded) {
24886
- const table = resolveTable(snapshot, policy.table);
24887
- if (!table || policy.invalid !== null) continue;
26204
+ const on = policyOn(snapshot, policy.table);
26205
+ if (!on || policy.invalid !== null) continue;
24888
26206
  for (const expr of [policy.usingExpr, policy.checkExpr]) {
24889
26207
  if (expr === null) continue;
24890
- for (const { written, column: column2 } of truthColumns(snapshot, table, expr)) {
26208
+ for (const { written, column: column2 } of truthColumns(
26209
+ snapshot,
26210
+ on.table,
26211
+ expr,
26212
+ on.names
26213
+ )) {
24891
26214
  if (isNumeric(column2)) continue;
24892
26215
  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}`, {
26216
+ found.set(`${on.label}:${message}`, {
24894
26217
  problem: {
24895
- table: table.name,
26218
+ table: on.label,
24896
26219
  message,
24897
26220
  policy: policy.name,
24898
26221
  kind: "compile"
@@ -24903,16 +26226,16 @@ function problems(snapshot, policies, options) {
24903
26226
  }
24904
26227
  }
24905
26228
  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)) {
26229
+ const on = policyOn(snapshot, policy.table);
26230
+ if (!on || policy.invalid !== null) continue;
26231
+ for (const { written, column: column2 } of numberReadsOf(snapshot, on, policy)) {
24909
26232
  const stored = stringDefault(column2);
24910
26233
  if (stored === void 0) continue;
24911
26234
  if (stored.kind === "text" && storesAsNumber(stored.text)) continue;
24912
26235
  const kept = stored.kind === "text" ? "text" : "a blob";
24913
- found.set(`default:${table.name}:${policy.name}:${written}`, {
26236
+ found.set(`default:${on.label}:${policy.name}:${written}`, {
24914
26237
  problem: {
24915
- table: table.name,
26238
+ table: on.label,
24916
26239
  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
26240
  policy: policy.name,
24918
26241
  kind: "default"
@@ -24934,6 +26257,23 @@ function problems(snapshot, policies, options) {
24934
26257
  }))
24935
26258
  );
24936
26259
  const compiled = [...loaded, ...probe];
26260
+ const refused = (probed, error51, reason) => {
26261
+ if (error51.code !== "POLICY_INVALID") return;
26262
+ const message = reason === void 0 ? error51.message : `${error51.message.replace(/\.$/, "")}: ${reason}.`;
26263
+ const named = namedInRefusal(error51.message, loaded);
26264
+ const blamed2 = named === void 0 ? probed : tableLabelOf(snapshot, named.table);
26265
+ const key = `${blamed2}:${message}`;
26266
+ if (!found.has(key))
26267
+ found.set(key, {
26268
+ problem: {
26269
+ table: blamed2,
26270
+ message,
26271
+ ...named === void 0 ? {} : { policy: named.name },
26272
+ kind: "compile"
26273
+ },
26274
+ policy: ""
26275
+ });
26276
+ };
24937
26277
  for (const table of tables.values()) {
24938
26278
  const owners = new Set(
24939
26279
  policiesOn(loaded, table.name).flatMap(
@@ -24948,23 +26288,27 @@ function problems(snapshot, policies, options) {
24948
26288
  error: decision.error,
24949
26289
  reason: decision.reason
24950
26290
  };
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
- });
26291
+ if (!result.ok) refused(table.name, result.error, result.reason);
24966
26292
  }
24967
26293
  }
26294
+ if (policiesOn(loaded, STORAGE_OBJECTS).length > 0) {
26295
+ if (snapshot.storage)
26296
+ for (const { error: error51, reason } of storageRefusals(
26297
+ snapshot,
26298
+ snapshot.storage,
26299
+ loaded
26300
+ ))
26301
+ refused(STORAGE_OBJECTS, error51, reason);
26302
+ else
26303
+ found.set(`${STORAGE_OBJECTS}:${STORAGE_UNAVAILABLE_MESSAGE}`, {
26304
+ problem: {
26305
+ table: STORAGE_OBJECTS,
26306
+ message: STORAGE_UNAVAILABLE_MESSAGE,
26307
+ kind: "compile"
26308
+ },
26309
+ policy: ""
26310
+ });
26311
+ }
24968
26312
  if (options.indexes && options.allowUnindexed !== true)
24969
26313
  for (const problem of unindexedSubqueries(
24970
26314
  snapshot,
@@ -24993,13 +26337,13 @@ function policySetProblems(snapshot, policies, options = {}) {
24993
26337
  (a, b) => byText(a.problem.table, b.problem.table) || byText(a.policy, b.policy) || byText(a.problem.message, b.problem.message)
24994
26338
  ).map(({ problem }) => problem);
24995
26339
  }
24996
- function numberReadsOf(snapshot, table, policy) {
26340
+ function numberReadsOf(snapshot, on, policy) {
24997
26341
  const reads = /* @__PURE__ */ new Map();
24998
26342
  for (const expr of [policy.usingExpr, policy.checkExpr]) {
24999
26343
  if (expr === null) continue;
25000
26344
  for (const read of [
25001
- ...truthColumns(snapshot, table, expr),
25002
- ...numberComparedColumns(snapshot, table, expr)
26345
+ ...truthColumns(snapshot, on.table, expr, on.names),
26346
+ ...numberComparedColumns(snapshot, on.table, expr, on.names)
25003
26347
  ]) {
25004
26348
  const key = JSON.stringify([read.table.name, read.column.name]);
25005
26349
  if (isNumeric(read.column) && !reads.has(key)) reads.set(key, read);
@@ -25053,7 +26397,7 @@ function readCreate(stream, start) {
25053
26397
  stream.expectKeyword("policy");
25054
26398
  const name = readPolicyName(stream);
25055
26399
  stream.expectKeyword("on");
25056
- const table = readTableName(stream);
26400
+ const table = readPolicyTarget(stream);
25057
26401
  let permissive = true;
25058
26402
  if (stream.acceptKeyword("as")) {
25059
26403
  const token = stream.next();
@@ -25118,7 +26462,7 @@ function readDrop(stream) {
25118
26462
  }
25119
26463
  const name = readPolicyName(stream);
25120
26464
  stream.expectKeyword("on");
25121
- const table = readTableName(stream);
26465
+ const table = readPolicyTarget(stream);
25122
26466
  if (!stream.acceptKeyword("cascade")) stream.acceptKeyword("restrict");
25123
26467
  readEnd(stream);
25124
26468
  return { kind: "drop", name, table, ifExists };
@@ -25213,7 +26557,7 @@ function readComment(stream) {
25213
26557
  stream.expectKeyword("policy");
25214
26558
  const name = readPolicyName(stream);
25215
26559
  stream.expectKeyword("on");
25216
- const table = readTableName(stream);
26560
+ const table = readPolicyTarget(stream);
25217
26561
  stream.expectKeyword("is");
25218
26562
  const token = stream.next();
25219
26563
  if (isKeyword(token, "null"))
@@ -25229,7 +26573,7 @@ function readComment(stream) {
25229
26573
  function readRowLevelSecurity(stream) {
25230
26574
  if (stream.acceptKeyword("if")) stream.expectKeyword("exists");
25231
26575
  stream.acceptKeyword("only");
25232
- const table = readTableName(stream);
26576
+ const table = readPolicyTarget(stream);
25233
26577
  const token = stream.next();
25234
26578
  let action;
25235
26579
  if (isKeyword(token, "enable")) action = "enable";
@@ -25468,7 +26812,10 @@ function readSqlHead(text2) {
25468
26812
  // ../../packages/data-core/src/policy-migration.ts
25469
26813
  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
26814
  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);`;
26815
+ 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);`;
26816
+ }
26817
+ function isStorageObjects(table) {
26818
+ return asciiLower(table) === STORAGE_OBJECTS;
25472
26819
  }
25473
26820
  function relationMissing(table) {
25474
26821
  return `relation "${table}" does not exist`;
@@ -25549,7 +26896,8 @@ function planPolicyMigration(statements) {
25549
26896
  );
25550
26897
  const statement2 = parsed.statement;
25551
26898
  const table = statement2.kind === "create" ? statement2.policy.table : statement2.table;
25552
- if (isSystemTable(table)) return refuse(`${relationMissing(table)}.`);
26899
+ if (!isStorageObjects(table) && isSystemTable(table))
26900
+ return refuse(`${relationMissing(table)}.`);
25553
26901
  switch (statement2.kind) {
25554
26902
  case "create":
25555
26903
  if (readsBareSubqueryColumn(statement2.policy) && changedAfter(index, tablesInScope(statement2.policy)))
@@ -25595,12 +26943,13 @@ function policyMigrationSql(policy) {
25595
26943
  const create = policySql({ ...policy, owner: null });
25596
26944
  if (policy.owner === null) return create;
25597
26945
  return `${create}
25598
- COMMENT ON POLICY ${quotePolicyName(policy.name)} ON ${quoteIdent(policy.table)} IS ${sqlStringLiteral(`naro:owner ${quoteIdent(policy.owner)}`)};`;
26946
+ COMMENT ON POLICY ${quotePolicyName(policy.name)} ON ${policyTableSql(policy.table)} IS ${sqlStringLiteral(`naro:owner ${quoteIdent(policy.owner)}`)};`;
25599
26947
  }
25600
26948
  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.`;
26949
+ 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
26950
  }
25603
26951
  var SYSTEM_PREDICATE = systemTablePredicate("name");
26952
+ var STORAGE_EXISTS = `EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${sqlStringLiteral(STORAGE_OBJECTS_TABLE)})`;
25604
26953
 
25605
26954
  // ../../packages/data-core/src/redirect-origins.ts
25606
26955
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
@@ -25725,6 +27074,25 @@ var SYSTEM_MIGRATIONS = [
25725
27074
  name: "named_policies",
25726
27075
  statements: [NAMED_POLICY_TABLE_SQL],
25727
27076
  compute: convertLegacyPolicies
27077
+ },
27078
+ {
27079
+ // Storage (first-release design §6): the buckets and the objects' index
27080
+ // with their counter triggers. Every backend gets them, Storage on or
27081
+ // not — what turns it on is the worker's R2 binding. A worker from
27082
+ // before them never reads them.
27083
+ version: 5,
27084
+ name: "storage",
27085
+ statements: [...STORAGE_TABLES_SQL]
27086
+ },
27087
+ {
27088
+ // Storage 3-1b (known-issues #107, first-release design §14): the mark
27089
+ // a bucket's removal sets before it empties the bucket. The claims and
27090
+ // the worker's bucket reads take a marked bucket as not there. A worker
27091
+ // from before it never reads the column.
27092
+ version: 6,
27093
+ name: "storage_bucket_deleting",
27094
+ statements: [],
27095
+ addColumns: [STORAGE_BUCKET_DELETING_COLUMN]
25728
27096
  }
25729
27097
  ];
25730
27098
 
@@ -25965,6 +27333,16 @@ function generateTypes(tables, options = {}) {
25965
27333
  `;
25966
27334
  }
25967
27335
 
27336
+ // ../../packages/sdk/src/insights.ts
27337
+ var SPEED_INSIGHTS_METRICS = [
27338
+ "lcp",
27339
+ "inp",
27340
+ "cls",
27341
+ "fcp",
27342
+ "ttfb"
27343
+ ];
27344
+ var SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES = 20;
27345
+
25968
27346
  // ../../packages/sdk/src/link.ts
25969
27347
  import {
25970
27348
  existsSync as existsSync4,
@@ -26120,7 +27498,10 @@ var POLICY_WARNING_RULES = [
26120
27498
  "rls_policy_unindexed"
26121
27499
  ];
26122
27500
  function policyIssues(input) {
26123
- const present = new Set(input.tables.map(asciiLower));
27501
+ const present = /* @__PURE__ */ new Set([
27502
+ ...input.tables.map(asciiLower),
27503
+ ...input.snapshot.storage ? [STORAGE_OBJECTS] : []
27504
+ ]);
26124
27505
  const found = [
26125
27506
  ...policySetProblems(
26126
27507
  input.snapshot,
@@ -26454,6 +27835,7 @@ function policiesFromFiles(files) {
26454
27835
  let policies = [];
26455
27836
  const tables = /* @__PURE__ */ new Set();
26456
27837
  const forget = (table) => {
27838
+ if (sameTable(table, STORAGE_OBJECTS)) return;
26457
27839
  policies = policies.filter((policy) => !sameTable(policy.table, table));
26458
27840
  };
26459
27841
  for (const [file2, sql] of files.entries()) {
@@ -26501,18 +27883,19 @@ function policiesFromFiles(files) {
26501
27883
  }
26502
27884
  function policiesLostByReset(input) {
26503
27885
  const present = new Set(input.tables.map(asciiLower));
27886
+ if (input.storage === true) present.add(STORAGE_OBJECTS);
26504
27887
  const made = policiesFromFiles(input.files);
26505
27888
  if (made.refused) return { lost: [], withTables: [], refused: made.refused };
27889
+ const remade = new Set(made.tables);
27890
+ if (input.storage === true) remade.add(STORAGE_OBJECTS);
26506
27891
  const gone = input.current.filter(
26507
27892
  (policy) => present.has(asciiLower(policy.table)) && !made.policies.some(
26508
27893
  (other) => sameTable(other.table, policy.table) && other.name === policy.name
26509
27894
  )
26510
27895
  );
26511
27896
  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
- )
27897
+ lost: gone.filter((policy) => remade.has(asciiLower(policy.table))),
27898
+ withTables: gone.filter((policy) => !remade.has(asciiLower(policy.table)))
26516
27899
  };
26517
27900
  }
26518
27901
  function resetPoliciesLostMessage(lost) {
@@ -27107,7 +28490,11 @@ export {
27107
28490
  renderRows,
27108
28491
  summarizeStatement,
27109
28492
  serializeDotenv,
28493
+ MAX_SINGLE_UPLOAD_BYTES,
28494
+ parseCacheControl,
28495
+ STORAGE_OBJECTS_TABLE,
27110
28496
  systemTablePredicate,
28497
+ dataError,
27111
28498
  SCHEMA_SQL,
27112
28499
  tableNames,
27113
28500
  snapshotFromRows,
@@ -27128,6 +28515,8 @@ export {
27128
28515
  formatRedirectEntry,
27129
28516
  policiesForTypes,
27130
28517
  generateTypes,
28518
+ SPEED_INSIGHTS_METRICS,
28519
+ SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
27131
28520
  readProjectLink,
27132
28521
  writeProjectLink,
27133
28522
  removeProjectLink,