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