naro-cli 0.10.0 → 0.11.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 +181 -45
- package/dist/artifact-types.d.ts +5 -0
- package/dist/{chunk-5U6YPVZS.js → chunk-L7JOGWP3.js} +2341 -114
- package/dist/deploy.js +1 -1
- package/dist/git-info.d.ts +14 -1
- package/dist/index.js +2061 -1815
- package/package.json +4 -4
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { existsSync as existsSync7 } from "fs";
|
|
8
8
|
import { cp as cp2, mkdtemp as mkdtemp2, readFile as readFile13, rm as rm3 } from "fs/promises";
|
|
9
9
|
import { tmpdir as tmpdir2 } from "os";
|
|
10
|
-
import { basename as basename3, dirname as dirname4, join as
|
|
10
|
+
import { basename as basename3, dirname as dirname4, join as join24, resolve as resolve9 } from "path";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
|
|
13
13
|
// ../../packages/build/src/artifact-upload.ts
|
|
@@ -226,7 +226,22 @@ function hashAsset(data, contentType2) {
|
|
|
226
226
|
// ../../packages/build/src/local-build-executor.ts
|
|
227
227
|
import { access as access8, mkdtemp, readFile as readFile12, rm as rm2 } from "fs/promises";
|
|
228
228
|
import { tmpdir } from "os";
|
|
229
|
-
import { dirname as dirname3, join as
|
|
229
|
+
import { dirname as dirname3, join as join17, resolve as resolve7 } from "path";
|
|
230
|
+
|
|
231
|
+
// ../../packages/core/src/route-patterns.ts
|
|
232
|
+
var ROUTE_PATTERN = /^\/[A-Za-z0-9\-._~%!$&()*+,;=:@/[\]]*$/;
|
|
233
|
+
var MAX_ROUTE_LENGTH = 256;
|
|
234
|
+
var MAX_ROUTES = 2e3;
|
|
235
|
+
var MAX_ROUTE_LIST_BYTES = 64 * 1024;
|
|
236
|
+
function isRoutePattern(value) {
|
|
237
|
+
return typeof value === "string" && value.length <= MAX_ROUTE_LENGTH && ROUTE_PATTERN.test(value);
|
|
238
|
+
}
|
|
239
|
+
function sanitizeRoutes(value) {
|
|
240
|
+
if (!Array.isArray(value)) return void 0;
|
|
241
|
+
const routes = [...new Set(value.filter(isRoutePattern))].sort();
|
|
242
|
+
if (routes.length > MAX_ROUTES) return void 0;
|
|
243
|
+
return JSON.stringify(routes).length > MAX_ROUTE_LIST_BYTES ? void 0 : routes;
|
|
244
|
+
}
|
|
230
245
|
|
|
231
246
|
// ../../packages/core/src/artifact-manifest.ts
|
|
232
247
|
var MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
@@ -4950,8 +4965,8 @@ function az_default() {
|
|
|
4950
4965
|
}
|
|
4951
4966
|
|
|
4952
4967
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/be.js
|
|
4953
|
-
function getBelarusianPlural(
|
|
4954
|
-
const absCount = Math.abs(
|
|
4968
|
+
function getBelarusianPlural(count2, one, few, many) {
|
|
4969
|
+
const absCount = Math.abs(count2);
|
|
4955
4970
|
const lastDigit = absCount % 10;
|
|
4956
4971
|
const lastTwoDigits = absCount % 100;
|
|
4957
4972
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -7029,8 +7044,8 @@ function hu_default() {
|
|
|
7029
7044
|
}
|
|
7030
7045
|
|
|
7031
7046
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/hy.js
|
|
7032
|
-
function getArmenianPlural(
|
|
7033
|
-
return Math.abs(
|
|
7047
|
+
function getArmenianPlural(count2, one, many) {
|
|
7048
|
+
return Math.abs(count2) === 1 ? one : many;
|
|
7034
7049
|
}
|
|
7035
7050
|
function withDefiniteArticle(word) {
|
|
7036
7051
|
if (!word)
|
|
@@ -9158,8 +9173,8 @@ function ro_default() {
|
|
|
9158
9173
|
}
|
|
9159
9174
|
|
|
9160
9175
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ru.js
|
|
9161
|
-
function getRussianPlural(
|
|
9162
|
-
const absCount = Math.abs(
|
|
9176
|
+
function getRussianPlural(count2, one, few, many) {
|
|
9177
|
+
const absCount = Math.abs(count2);
|
|
9163
9178
|
const lastDigit = absCount % 10;
|
|
9164
9179
|
const lastTwoDigits = absCount % 100;
|
|
9165
9180
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -16015,6 +16030,93 @@ async function detectFramework(root, overrides = {}) {
|
|
|
16015
16030
|
};
|
|
16016
16031
|
}
|
|
16017
16032
|
|
|
16033
|
+
// ../../packages/build/src/next-routes.ts
|
|
16034
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
16035
|
+
import { join as join8 } from "path";
|
|
16036
|
+
var APP_PAGE_FILE = /^page\.(?:js|jsx|ts|tsx|md|mdx)$/;
|
|
16037
|
+
var PAGES_ROUTER_FILE = /^(.+)\.(?:js|jsx|ts|tsx)$/;
|
|
16038
|
+
var DECLARATION_FILE = /\.d\.ts$/;
|
|
16039
|
+
var PAGES_ROUTER_SPECIAL = /* @__PURE__ */ new Set(["_app", "_document", "_error", "api"]);
|
|
16040
|
+
var DYNAMIC_SEGMENT = /^\[.+\]$/;
|
|
16041
|
+
var ROUTE_GROUP = /^\(.+\)$/;
|
|
16042
|
+
var INTERCEPTING = /^\(\.{1,3}\)/;
|
|
16043
|
+
var LITERAL_IN_PATHNAME = /%(?:21|24|26|28|29|2A|2B|2C|3B|3D|3A|40)/g;
|
|
16044
|
+
function errorCode(error51) {
|
|
16045
|
+
return error51?.code;
|
|
16046
|
+
}
|
|
16047
|
+
async function firstDirectory(root, candidates) {
|
|
16048
|
+
for (const candidate of candidates) {
|
|
16049
|
+
const path = join8(root, candidate);
|
|
16050
|
+
try {
|
|
16051
|
+
if ((await stat2(path)).isDirectory()) return path;
|
|
16052
|
+
} catch (error51) {
|
|
16053
|
+
const code = errorCode(error51);
|
|
16054
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") throw error51;
|
|
16055
|
+
}
|
|
16056
|
+
}
|
|
16057
|
+
return null;
|
|
16058
|
+
}
|
|
16059
|
+
function urlSegment(name, router) {
|
|
16060
|
+
if (DYNAMIC_SEGMENT.test(name)) return name;
|
|
16061
|
+
const decoded = router === "app" ? name.replace(/%5F/gi, "_") : name;
|
|
16062
|
+
return encodeURIComponent(decoded).replace(/'/g, "%27").replace(
|
|
16063
|
+
LITERAL_IN_PATHNAME,
|
|
16064
|
+
(escape) => String.fromCharCode(Number.parseInt(escape.slice(1), 16))
|
|
16065
|
+
);
|
|
16066
|
+
}
|
|
16067
|
+
function toRoute(segments) {
|
|
16068
|
+
return `/${segments.join("/")}`;
|
|
16069
|
+
}
|
|
16070
|
+
async function readAppRouter(dir, segments, routes) {
|
|
16071
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
16072
|
+
if (entries.some((entry) => entry.isFile() && APP_PAGE_FILE.test(entry.name)))
|
|
16073
|
+
routes.add(toRoute(segments));
|
|
16074
|
+
for (const entry of entries) {
|
|
16075
|
+
if (!entry.isDirectory()) continue;
|
|
16076
|
+
const name = entry.name;
|
|
16077
|
+
if (name.startsWith("_") || INTERCEPTING.test(name)) continue;
|
|
16078
|
+
const next = ROUTE_GROUP.test(name) || name.startsWith("@") ? segments : [...segments, urlSegment(name, "app")];
|
|
16079
|
+
await readAppRouter(join8(dir, name), next, routes);
|
|
16080
|
+
}
|
|
16081
|
+
}
|
|
16082
|
+
async function readPagesRouter(dir, segments, routes) {
|
|
16083
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
16084
|
+
for (const entry of entries) {
|
|
16085
|
+
if (entry.isDirectory()) {
|
|
16086
|
+
if (segments.length === 0 && entry.name === "api") continue;
|
|
16087
|
+
await readPagesRouter(
|
|
16088
|
+
join8(dir, entry.name),
|
|
16089
|
+
[...segments, urlSegment(entry.name, "pages")],
|
|
16090
|
+
routes
|
|
16091
|
+
);
|
|
16092
|
+
continue;
|
|
16093
|
+
}
|
|
16094
|
+
if (!entry.isFile() || DECLARATION_FILE.test(entry.name)) continue;
|
|
16095
|
+
const base2 = PAGES_ROUTER_FILE.exec(entry.name)?.[1];
|
|
16096
|
+
if (!base2) continue;
|
|
16097
|
+
if (segments.length === 0 && PAGES_ROUTER_SPECIAL.has(base2)) continue;
|
|
16098
|
+
routes.add(
|
|
16099
|
+
toRoute(
|
|
16100
|
+
base2 === "index" ? segments : [...segments, urlSegment(base2, "pages")]
|
|
16101
|
+
)
|
|
16102
|
+
);
|
|
16103
|
+
}
|
|
16104
|
+
}
|
|
16105
|
+
async function listNextRoutes(appRoot) {
|
|
16106
|
+
const app = await firstDirectory(appRoot, ["app", join8("src", "app")]);
|
|
16107
|
+
const pages = await firstDirectory(appRoot, ["pages", join8("src", "pages")]);
|
|
16108
|
+
if (!app && !pages) return null;
|
|
16109
|
+
const routes = /* @__PURE__ */ new Set();
|
|
16110
|
+
if (app) await readAppRouter(app, [], routes);
|
|
16111
|
+
if (pages) await readPagesRouter(pages, [], routes);
|
|
16112
|
+
const valid = sanitizeRoutes([...routes]);
|
|
16113
|
+
if (!valid)
|
|
16114
|
+
throw new Error(
|
|
16115
|
+
"the app has more than 2,000 routes or its route list is over 64 KiB"
|
|
16116
|
+
);
|
|
16117
|
+
return valid.length > 0 ? valid : null;
|
|
16118
|
+
}
|
|
16119
|
+
|
|
16018
16120
|
// ../../packages/build/src/process.ts
|
|
16019
16121
|
import { spawn } from "child_process";
|
|
16020
16122
|
import { createInterface } from "readline";
|
|
@@ -16246,8 +16348,8 @@ function resolveAppRoot(checkout, configuredRoot) {
|
|
|
16246
16348
|
}
|
|
16247
16349
|
|
|
16248
16350
|
// ../../packages/build/src/static-artifact.ts
|
|
16249
|
-
import { readdir as
|
|
16250
|
-
import { isAbsolute as isAbsolute3, join as
|
|
16351
|
+
import { readdir as readdir3, readFile as readFile7 } from "fs/promises";
|
|
16352
|
+
import { isAbsolute as isAbsolute3, join as join9, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
|
|
16251
16353
|
var SHIM_WORKER_SOURCE = "export default { fetch: (request, env) => env.ASSETS.fetch(request) };\n";
|
|
16252
16354
|
var EXCLUDED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules"]);
|
|
16253
16355
|
var ALLOWED_DOTFILE = ".well-known";
|
|
@@ -16270,12 +16372,12 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
|
|
|
16270
16372
|
);
|
|
16271
16373
|
const assets = [];
|
|
16272
16374
|
async function visit(directory) {
|
|
16273
|
-
const entries = await
|
|
16375
|
+
const entries = await readdir3(directory, { withFileTypes: true });
|
|
16274
16376
|
for (const entry of entries.sort(
|
|
16275
16377
|
(left, right) => left.name.localeCompare(right.name)
|
|
16276
16378
|
)) {
|
|
16277
16379
|
if (excluded(entry.name)) continue;
|
|
16278
|
-
const path =
|
|
16380
|
+
const path = join9(directory, entry.name);
|
|
16279
16381
|
if (entry.isSymbolicLink())
|
|
16280
16382
|
throw new DeploymentError(
|
|
16281
16383
|
"ARTIFACT_INVALID",
|
|
@@ -16331,7 +16433,7 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
|
|
|
16331
16433
|
// ../../packages/build/src/strategy/fingerprint.ts
|
|
16332
16434
|
import { createHash as createHash3 } from "crypto";
|
|
16333
16435
|
import { readFile as readFile8 } from "fs/promises";
|
|
16334
|
-
import { join as
|
|
16436
|
+
import { join as join10, relative as relative4, sep as sep3 } from "path";
|
|
16335
16437
|
var APP_ROOT_FILES = [
|
|
16336
16438
|
"package.json",
|
|
16337
16439
|
"next.config.js",
|
|
@@ -16375,11 +16477,11 @@ async function hashFile(path) {
|
|
|
16375
16477
|
}
|
|
16376
16478
|
async function hashStrategyInputs(checkout, appRoot) {
|
|
16377
16479
|
const candidates = /* @__PURE__ */ new Set();
|
|
16378
|
-
for (const name of CHECKOUT_FILES) candidates.add(
|
|
16379
|
-
for (const name of APP_ROOT_FILES) candidates.add(
|
|
16480
|
+
for (const name of CHECKOUT_FILES) candidates.add(join10(checkout, name));
|
|
16481
|
+
for (const name of APP_ROOT_FILES) candidates.add(join10(appRoot, name));
|
|
16380
16482
|
for (const base2 of SOURCE_FILES) {
|
|
16381
16483
|
for (const ext of SOURCE_EXTENSIONS) {
|
|
16382
|
-
candidates.add(
|
|
16484
|
+
candidates.add(join10(appRoot, `${base2}.${ext}`));
|
|
16383
16485
|
}
|
|
16384
16486
|
}
|
|
16385
16487
|
const entries = [];
|
|
@@ -16408,7 +16510,7 @@ async function computeStrategyFingerprint(input) {
|
|
|
16408
16510
|
|
|
16409
16511
|
// ../../packages/build/src/strategy/project-facts.ts
|
|
16410
16512
|
import { access as access6, readFile as readFile9 } from "fs/promises";
|
|
16411
|
-
import { join as
|
|
16513
|
+
import { join as join11 } from "path";
|
|
16412
16514
|
var SOURCE_EXTENSIONS2 = ["ts", "tsx", "js", "jsx", "mjs", "mts"];
|
|
16413
16515
|
var SOURCE_DIRECTORIES = ["", "src"];
|
|
16414
16516
|
var VITE_CONFIGS = [
|
|
@@ -16429,7 +16531,7 @@ async function findSourceFile(appRoot, basename4) {
|
|
|
16429
16531
|
for (const dir of SOURCE_DIRECTORIES) {
|
|
16430
16532
|
for (const ext of SOURCE_EXTENSIONS2) {
|
|
16431
16533
|
const rel = dir ? `${dir}/${basename4}.${ext}` : `${basename4}.${ext}`;
|
|
16432
|
-
if (await exists4(
|
|
16534
|
+
if (await exists4(join11(appRoot, rel))) return rel;
|
|
16433
16535
|
}
|
|
16434
16536
|
}
|
|
16435
16537
|
return null;
|
|
@@ -16458,10 +16560,10 @@ async function collectProjectFacts(appRoot) {
|
|
|
16458
16560
|
const proxyFile = await findSourceFile(appRoot, "proxy");
|
|
16459
16561
|
const middlewareFile = await findSourceFile(appRoot, "middleware");
|
|
16460
16562
|
const routeSourceFile = middlewareFile ?? proxyFile;
|
|
16461
|
-
const routeSource = routeSourceFile ? await readIfExists(
|
|
16563
|
+
const routeSource = routeSourceFile ? await readIfExists(join11(appRoot, routeSourceFile)) : null;
|
|
16462
16564
|
let usesVinext = false;
|
|
16463
16565
|
let nextSpec = null;
|
|
16464
|
-
const packageJsonSource = await readIfExists(
|
|
16566
|
+
const packageJsonSource = await readIfExists(join11(appRoot, "package.json"));
|
|
16465
16567
|
if (packageJsonSource) {
|
|
16466
16568
|
try {
|
|
16467
16569
|
const packageJson = JSON.parse(packageJsonSource);
|
|
@@ -16472,7 +16574,7 @@ async function collectProjectFacts(appRoot) {
|
|
|
16472
16574
|
nextSpec = dependency("next");
|
|
16473
16575
|
if (dependency("vinext") !== null) {
|
|
16474
16576
|
for (const name of VITE_CONFIGS) {
|
|
16475
|
-
const config2 = await readIfExists(
|
|
16577
|
+
const config2 = await readIfExists(join11(appRoot, name));
|
|
16476
16578
|
if (config2 && /\bvinext\s*\(/.test(config2) && /@cloudflare\/vite-plugin/.test(config2)) {
|
|
16477
16579
|
usesVinext = true;
|
|
16478
16580
|
break;
|
|
@@ -16726,7 +16828,7 @@ function createSmokeTester(options = {}) {
|
|
|
16726
16828
|
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
16727
16829
|
async function runOnce(request, observer) {
|
|
16728
16830
|
const stage = request.stage ?? "BUILDING";
|
|
16729
|
-
const
|
|
16831
|
+
const errorCode2 = request.errorCode ?? "BUILD_FAILED";
|
|
16730
16832
|
const bootTimeoutMs = request.bootTimeoutMs ?? 9e4;
|
|
16731
16833
|
const probeTimeoutMs = request.probeTimeoutMs ?? 15e3;
|
|
16732
16834
|
const port = await allocatePort();
|
|
@@ -16734,7 +16836,7 @@ function createSmokeTester(options = {}) {
|
|
|
16734
16836
|
const boot = request.boot(port);
|
|
16735
16837
|
const recent = [];
|
|
16736
16838
|
const server = start(
|
|
16737
|
-
{ ...boot, stage, errorCode },
|
|
16839
|
+
{ ...boot, stage, errorCode: errorCode2 },
|
|
16738
16840
|
// The dev server is chatty; keep its stderr from reading as build
|
|
16739
16841
|
// errors in the deployment log.
|
|
16740
16842
|
{
|
|
@@ -16850,11 +16952,11 @@ function defaultSmokeProbes(matcherRoutes) {
|
|
|
16850
16952
|
|
|
16851
16953
|
// ../../packages/build/src/strategy/strategies/opennext-aws.ts
|
|
16852
16954
|
import { writeFile as writeFile6 } from "fs/promises";
|
|
16853
|
-
import { join as
|
|
16955
|
+
import { join as join13 } from "path";
|
|
16854
16956
|
|
|
16855
16957
|
// ../../packages/build/src/strategy/strategies/shared.ts
|
|
16856
16958
|
import { access as access7, readFile as readFile10, rm, writeFile as writeFile5 } from "fs/promises";
|
|
16857
|
-
import { dirname as dirname2, join as
|
|
16959
|
+
import { dirname as dirname2, join as join12, resolve as resolve5 } from "path";
|
|
16858
16960
|
function capturingObserver(observer) {
|
|
16859
16961
|
const lines = [];
|
|
16860
16962
|
return {
|
|
@@ -16907,7 +17009,7 @@ async function findProjectBin(appRoot, checkout, name) {
|
|
|
16907
17009
|
let dir = resolve5(appRoot);
|
|
16908
17010
|
const stop = resolve5(checkout);
|
|
16909
17011
|
for (; ; ) {
|
|
16910
|
-
const candidate =
|
|
17012
|
+
const candidate = join12(dir, "node_modules", ".bin", name);
|
|
16911
17013
|
if (await pathExists2(candidate)) return candidate;
|
|
16912
17014
|
if (dir === stop) return null;
|
|
16913
17015
|
const parent = dirname2(dir);
|
|
@@ -16921,7 +17023,7 @@ function dotenvQuote(value) {
|
|
|
16921
17023
|
return `"${value.replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
|
|
16922
17024
|
}
|
|
16923
17025
|
async function withDevVars(wranglerConfigPath, env, run) {
|
|
16924
|
-
const path =
|
|
17026
|
+
const path = join12(dirname2(wranglerConfigPath), ".dev.vars");
|
|
16925
17027
|
const keys = Object.keys(env).filter(
|
|
16926
17028
|
(key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)
|
|
16927
17029
|
);
|
|
@@ -16938,7 +17040,7 @@ async function withDevVars(wranglerConfigPath, env, run) {
|
|
|
16938
17040
|
await rm(path, { force: true });
|
|
16939
17041
|
}
|
|
16940
17042
|
}
|
|
16941
|
-
async function bundleWorker(context, layout, observer,
|
|
17043
|
+
async function bundleWorker(context, layout, observer, errorCode2) {
|
|
16942
17044
|
return runStep(
|
|
16943
17045
|
context,
|
|
16944
17046
|
{
|
|
@@ -16954,7 +17056,7 @@ async function bundleWorker(context, layout, observer, errorCode) {
|
|
|
16954
17056
|
cwd: layout.appRoot,
|
|
16955
17057
|
env: {
|
|
16956
17058
|
CI: "1",
|
|
16957
|
-
WRANGLER_LOG_PATH:
|
|
17059
|
+
WRANGLER_LOG_PATH: join12(
|
|
16958
17060
|
layout.appRoot,
|
|
16959
17061
|
layout.outputRoot,
|
|
16960
17062
|
"wrangler.log"
|
|
@@ -16962,7 +17064,7 @@ async function bundleWorker(context, layout, observer, errorCode) {
|
|
|
16962
17064
|
},
|
|
16963
17065
|
timeoutMs: 5 * 6e4,
|
|
16964
17066
|
stage: "BUILDING",
|
|
16965
|
-
errorCode
|
|
17067
|
+
errorCode: errorCode2
|
|
16966
17068
|
},
|
|
16967
17069
|
observer
|
|
16968
17070
|
);
|
|
@@ -16987,12 +17089,12 @@ async function smokeWorker(context, layout, observer) {
|
|
|
16987
17089
|
"--show-interactive-dev-session=false",
|
|
16988
17090
|
"--log-level=warn",
|
|
16989
17091
|
"--persist-to",
|
|
16990
|
-
|
|
17092
|
+
join12(layout.appRoot, layout.outputRoot, ".naro-smoke-state")
|
|
16991
17093
|
],
|
|
16992
17094
|
cwd: layout.appRoot,
|
|
16993
17095
|
env: {
|
|
16994
17096
|
CI: "1",
|
|
16995
|
-
WRANGLER_LOG_PATH:
|
|
17097
|
+
WRANGLER_LOG_PATH: join12(
|
|
16996
17098
|
layout.appRoot,
|
|
16997
17099
|
layout.outputRoot,
|
|
16998
17100
|
"wrangler-dev.log"
|
|
@@ -17008,7 +17110,7 @@ async function smokeWorker(context, layout, observer) {
|
|
|
17008
17110
|
);
|
|
17009
17111
|
}
|
|
17010
17112
|
async function approvePnpmBuilds(checkout, packages) {
|
|
17011
|
-
const path =
|
|
17113
|
+
const path = join12(checkout, "pnpm-workspace.yaml");
|
|
17012
17114
|
let source = "";
|
|
17013
17115
|
try {
|
|
17014
17116
|
source = await readFile10(path, "utf8");
|
|
@@ -17104,7 +17206,7 @@ function createOpenNextAwsStrategy() {
|
|
|
17104
17206
|
} catch (error51) {
|
|
17105
17207
|
return failed("prepare", describeError(error51));
|
|
17106
17208
|
}
|
|
17107
|
-
const configPath =
|
|
17209
|
+
const configPath = join13(fork.appRoot, CONFIG_NAME);
|
|
17108
17210
|
try {
|
|
17109
17211
|
await writeFile6(
|
|
17110
17212
|
configPath,
|
|
@@ -17133,7 +17235,7 @@ function createOpenNextAwsStrategy() {
|
|
|
17133
17235
|
observer
|
|
17134
17236
|
);
|
|
17135
17237
|
if (!build.ok) return failed("build", describeFailure(build));
|
|
17136
|
-
const serverFunction =
|
|
17238
|
+
const serverFunction = join13(fork.appRoot, SERVER_FUNCTION);
|
|
17137
17239
|
if (!await pathExists2(serverFunction))
|
|
17138
17240
|
return failed("bundle", `OpenNext AWS produced no ${SERVER_FUNCTION}.`);
|
|
17139
17241
|
const smoke = await context.ports.smoke.run(
|
|
@@ -17155,7 +17257,7 @@ function createOpenNextAwsStrategy() {
|
|
|
17155
17257
|
status: "success",
|
|
17156
17258
|
artifact: {
|
|
17157
17259
|
kind: "opennext-aws",
|
|
17158
|
-
outputRoot:
|
|
17260
|
+
outputRoot: join13(fork.appRoot, ".open-next"),
|
|
17159
17261
|
serverFunction
|
|
17160
17262
|
},
|
|
17161
17263
|
notes: [
|
|
@@ -17168,14 +17270,14 @@ function createOpenNextAwsStrategy() {
|
|
|
17168
17270
|
|
|
17169
17271
|
// ../../packages/build/src/strategy/strategies/opennext-cloudflare.ts
|
|
17170
17272
|
import { readFile as readFile11 } from "fs/promises";
|
|
17171
|
-
import { join as
|
|
17273
|
+
import { join as join14 } from "path";
|
|
17172
17274
|
var PROXY_TO_MIDDLEWARE_VARIANT = "proxy-to-middleware";
|
|
17173
17275
|
var NODE_MIDDLEWARE_REFUSAL = /Node\.js middleware is not (?:currently )?supported/i;
|
|
17174
17276
|
async function buildDeclaresNodeMiddleware(appRoot) {
|
|
17175
17277
|
try {
|
|
17176
17278
|
const manifest2 = JSON.parse(
|
|
17177
17279
|
await readFile11(
|
|
17178
|
-
|
|
17280
|
+
join14(appRoot, ".next", "server", "functions-config-manifest.json"),
|
|
17179
17281
|
"utf8"
|
|
17180
17282
|
)
|
|
17181
17283
|
);
|
|
@@ -17387,7 +17489,7 @@ function createAwsMiddlewareCloudflareAppStrategy() {
|
|
|
17387
17489
|
}
|
|
17388
17490
|
|
|
17389
17491
|
// ../../packages/build/src/strategy/strategies/vinext-cloudflare.ts
|
|
17390
|
-
import { join as
|
|
17492
|
+
import { join as join15 } from "path";
|
|
17391
17493
|
var VINEXT_MIN_NEXT_MAJOR = 16;
|
|
17392
17494
|
var VINEXT_BUILD_SCRIPTS = ["esbuild", "workerd"];
|
|
17393
17495
|
var VINEXT_OUTPUT = {
|
|
@@ -17542,7 +17644,7 @@ function createVinextCloudflareStrategy(options) {
|
|
|
17542
17644
|
context,
|
|
17543
17645
|
{
|
|
17544
17646
|
appRoot: fork.appRoot,
|
|
17545
|
-
wranglerConfigPath:
|
|
17647
|
+
wranglerConfigPath: join15(
|
|
17546
17648
|
fork.appRoot,
|
|
17547
17649
|
"dist",
|
|
17548
17650
|
"server",
|
|
@@ -17566,7 +17668,7 @@ function createVinextCloudflareStrategy(options) {
|
|
|
17566
17668
|
|
|
17567
17669
|
// ../../packages/build/src/strategy/workspace.ts
|
|
17568
17670
|
import { cp } from "fs/promises";
|
|
17569
|
-
import { basename as basename2, join as
|
|
17671
|
+
import { basename as basename2, join as join16, relative as relative5, resolve as resolve6 } from "path";
|
|
17570
17672
|
var SKIPPED_ANYWHERE = /* @__PURE__ */ new Set(["node_modules"]);
|
|
17571
17673
|
function shouldCopy(path) {
|
|
17572
17674
|
const name = basename2(path);
|
|
@@ -17582,7 +17684,7 @@ async function copyCheckout(source, target) {
|
|
|
17582
17684
|
});
|
|
17583
17685
|
}
|
|
17584
17686
|
function forkedAppRoot(checkout, appRoot, forkCheckout) {
|
|
17585
|
-
return
|
|
17687
|
+
return join16(forkCheckout, relative5(checkout, appRoot));
|
|
17586
17688
|
}
|
|
17587
17689
|
|
|
17588
17690
|
// ../../packages/build/src/local-build-executor.ts
|
|
@@ -17590,8 +17692,21 @@ function concurrencyFromEnvironment() {
|
|
|
17590
17692
|
const raw = Number(process.env.NARO_RESOLVER_CONCURRENCY ?? "1");
|
|
17591
17693
|
return Number.isInteger(raw) && raw > 0 ? raw : 1;
|
|
17592
17694
|
}
|
|
17695
|
+
async function withRouteList(artifact, list, appRoot, observer) {
|
|
17696
|
+
try {
|
|
17697
|
+
const routes = await list(appRoot);
|
|
17698
|
+
return routes ? { ...artifact, routes } : artifact;
|
|
17699
|
+
} catch (error51) {
|
|
17700
|
+
await observer.onLog({
|
|
17701
|
+
level: "WARN",
|
|
17702
|
+
stage: "BUILDING",
|
|
17703
|
+
message: `Speed Insights route list skipped: ${error51 instanceof Error ? error51.message : String(error51)}`
|
|
17704
|
+
});
|
|
17705
|
+
return artifact;
|
|
17706
|
+
}
|
|
17707
|
+
}
|
|
17593
17708
|
var defaultLocalBuildDependencies = {
|
|
17594
|
-
createWorkspace: () => mkdtemp(
|
|
17709
|
+
createWorkspace: () => mkdtemp(join17(tmpdir(), "next-deploy-build-")),
|
|
17595
17710
|
removeWorkspace: (path) => rm2(path, { recursive: true, force: true }),
|
|
17596
17711
|
runCommand,
|
|
17597
17712
|
detectPackageManager,
|
|
@@ -17613,6 +17728,7 @@ var defaultLocalBuildDependencies = {
|
|
|
17613
17728
|
vinextCommand: "vinext",
|
|
17614
17729
|
forkWorkspace: copyCheckout,
|
|
17615
17730
|
collectProjectFacts,
|
|
17731
|
+
listNextRoutes,
|
|
17616
17732
|
hashFingerprintInputs: hashStrategyInputs,
|
|
17617
17733
|
computeFingerprint: computeStrategyFingerprint,
|
|
17618
17734
|
targets: { cloudflare: true },
|
|
@@ -17624,7 +17740,7 @@ async function readInstalledNextVersion(appRoot, checkout) {
|
|
|
17624
17740
|
for (; ; ) {
|
|
17625
17741
|
try {
|
|
17626
17742
|
const source = await readFile12(
|
|
17627
|
-
|
|
17743
|
+
join17(dir, "node_modules", "next", "package.json"),
|
|
17628
17744
|
"utf8"
|
|
17629
17745
|
);
|
|
17630
17746
|
const version2 = JSON.parse(source).version;
|
|
@@ -17717,8 +17833,8 @@ var LocalBuildExecutor = class {
|
|
|
17717
17833
|
}
|
|
17718
17834
|
async build(input, observer, cache) {
|
|
17719
17835
|
const workspace = await this.dependencies.createWorkspace();
|
|
17720
|
-
const checkout =
|
|
17721
|
-
const pmCacheDir =
|
|
17836
|
+
const checkout = join17(workspace, "checkout");
|
|
17837
|
+
const pmCacheDir = join17(workspace, ".pm-store");
|
|
17722
17838
|
let cacheSeq = 0;
|
|
17723
17839
|
const commandAvailable = this.dependencies.commandAvailable ?? hasCommand;
|
|
17724
17840
|
const compressor = "zstd";
|
|
@@ -17727,7 +17843,7 @@ var LocalBuildExecutor = class {
|
|
|
17727
17843
|
transport: cache,
|
|
17728
17844
|
fetch: this.dependencies.cacheFetch ?? ((url2, init) => fetch(url2, init)),
|
|
17729
17845
|
runCommand: this.dependencies.runCommand,
|
|
17730
|
-
tmpFile: (suffix) =>
|
|
17846
|
+
tmpFile: (suffix) => join17(workspace, `cache-${String(cacheSeq++)}${suffix}`),
|
|
17731
17847
|
// One latch for the whole build so a disabled cache warns once.
|
|
17732
17848
|
report: { unavailable: false }
|
|
17733
17849
|
} : null;
|
|
@@ -17911,7 +18027,7 @@ var LocalBuildExecutor = class {
|
|
|
17911
18027
|
}
|
|
17912
18028
|
}
|
|
17913
18029
|
await notifyStatus(observer, "BUILDING");
|
|
17914
|
-
const buildCacheDir = resolved.preset.cachePath ?
|
|
18030
|
+
const buildCacheDir = resolved.preset.cachePath ? join17(appRoot, resolved.preset.cachePath) : null;
|
|
17915
18031
|
const buildCacheKey = sanitizeCacheKey(input.branch);
|
|
17916
18032
|
if (cachePorts && buildCacheDir) {
|
|
17917
18033
|
await tryRestore(
|
|
@@ -17935,12 +18051,12 @@ var LocalBuildExecutor = class {
|
|
|
17935
18051
|
entries: fingerprintEntries,
|
|
17936
18052
|
nextVersion: nextVersion ? `${String(nextVersion.major)}.${String(nextVersion.minor)}.${String(nextVersion.patch)}` : "unknown"
|
|
17937
18053
|
});
|
|
17938
|
-
const pristine =
|
|
18054
|
+
const pristine = join17(workspace, "pristine");
|
|
17939
18055
|
const fork = this.dependencies.forkWorkspace ?? defaultFork;
|
|
17940
18056
|
await fork(checkout, pristine);
|
|
17941
18057
|
let forks = 0;
|
|
17942
18058
|
const forkWorkspace = async (label2) => {
|
|
17943
|
-
const target =
|
|
18059
|
+
const target = join17(
|
|
17944
18060
|
workspace,
|
|
17945
18061
|
"forks",
|
|
17946
18062
|
`${String(forks++)}-${label2.replace(/[^a-z0-9-]/gi, "-")}`
|
|
@@ -17997,7 +18113,12 @@ var LocalBuildExecutor = class {
|
|
|
17997
18113
|
`Strategy ${resolution.strategy.id} produced a ${resolution.outcome.artifact.kind} artifact, which this control plane cannot deploy.`
|
|
17998
18114
|
);
|
|
17999
18115
|
}
|
|
18000
|
-
artifact =
|
|
18116
|
+
artifact = await withRouteList(
|
|
18117
|
+
resolution.outcome.artifact.artifact,
|
|
18118
|
+
this.dependencies.listNextRoutes ?? listNextRoutes,
|
|
18119
|
+
appRoot,
|
|
18120
|
+
observer
|
|
18121
|
+
);
|
|
18001
18122
|
strategyRecord = resolution.record;
|
|
18002
18123
|
await observer.onMetadata?.({ strategy: resolution.record });
|
|
18003
18124
|
} else {
|
|
@@ -18034,6 +18155,13 @@ var LocalBuildExecutor = class {
|
|
|
18034
18155
|
}
|
|
18035
18156
|
await notifyStatus(observer, "DEPLOYING");
|
|
18036
18157
|
artifact ??= await this.readStatic(appRoot, resolved, identity, observer);
|
|
18158
|
+
if (resolved.preset.id === "next-export")
|
|
18159
|
+
artifact = await withRouteList(
|
|
18160
|
+
artifact,
|
|
18161
|
+
this.dependencies.listNextRoutes ?? listNextRoutes,
|
|
18162
|
+
appRoot,
|
|
18163
|
+
observer
|
|
18164
|
+
);
|
|
18037
18165
|
return {
|
|
18038
18166
|
gitCommitSha,
|
|
18039
18167
|
artifact,
|
|
@@ -18193,12 +18321,12 @@ async function errorFromResponse(response, context) {
|
|
|
18193
18321
|
// ../../packages/sdk/src/session.ts
|
|
18194
18322
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
18195
18323
|
import { homedir } from "os";
|
|
18196
|
-
import { join as
|
|
18324
|
+
import { join as join18 } from "path";
|
|
18197
18325
|
function credentialsDir(home = homedir()) {
|
|
18198
|
-
return
|
|
18326
|
+
return join18(home, ".naro");
|
|
18199
18327
|
}
|
|
18200
18328
|
function credentialsFile(home = homedir()) {
|
|
18201
|
-
return
|
|
18329
|
+
return join18(credentialsDir(home), "credentials.json");
|
|
18202
18330
|
}
|
|
18203
18331
|
function saveSession(session, home = homedir()) {
|
|
18204
18332
|
mkdirSync(credentialsDir(home), { recursive: true, mode: 448 });
|
|
@@ -19287,7 +19415,10 @@ var DbApi = class {
|
|
|
19287
19415
|
* `d1_migrations` — all operations or none. `dryRun` returns the SQL and
|
|
19288
19416
|
* the name without applying anything. Save `sql` as `migrations/<name>` so
|
|
19289
19417
|
* `naro db reset` re-applies it; a refused operation's index is in
|
|
19290
|
-
* `details.operation`.
|
|
19418
|
+
* `details.operation`. Policies left under a name the change gives to a
|
|
19419
|
+
* table are removed before it runs and stay removed if it then fails:
|
|
19420
|
+
* that refusal carries them in `details.removedPolicies`, shaped as the
|
|
19421
|
+
* result's `removedPolicies`.
|
|
19291
19422
|
*/
|
|
19292
19423
|
async schema(projectId, operations, options = {}) {
|
|
19293
19424
|
return this.client.request(
|
|
@@ -19306,6 +19437,9 @@ var DbApi = class {
|
|
|
19306
19437
|
}
|
|
19307
19438
|
};
|
|
19308
19439
|
var PROVISION_TIMEOUT_MS = 12e4;
|
|
19440
|
+
function writeQuery(options) {
|
|
19441
|
+
return options.allowUnindexed === true ? "?allow_unindexed=1" : "";
|
|
19442
|
+
}
|
|
19309
19443
|
function backendInfo(body) {
|
|
19310
19444
|
return {
|
|
19311
19445
|
status: body.status,
|
|
@@ -19581,7 +19715,12 @@ var BackendApi = class {
|
|
|
19581
19715
|
);
|
|
19582
19716
|
return answer.user;
|
|
19583
19717
|
}
|
|
19584
|
-
/**
|
|
19718
|
+
/**
|
|
19719
|
+
* Every table's policies as rules and conditions, one row per table —
|
|
19720
|
+
* the view `setPolicy` writes through (P2-7 §11.2). A policy that view
|
|
19721
|
+
* cannot show reads `{ invalid }`; `rlsPolicies` shows every one. Tables
|
|
19722
|
+
* without one deny anon and signed-in requests.
|
|
19723
|
+
*/
|
|
19585
19724
|
async policies(projectId) {
|
|
19586
19725
|
const body = await this.client.request(
|
|
19587
19726
|
this.path(projectId, "/policies"),
|
|
@@ -19605,8 +19744,9 @@ var BackendApi = class {
|
|
|
19605
19744
|
return answer.policy;
|
|
19606
19745
|
}
|
|
19607
19746
|
/**
|
|
19608
|
-
* Deletes the table
|
|
19609
|
-
*
|
|
19747
|
+
* Deletes every policy on the table (P2-7: the named ones too, whose
|
|
19748
|
+
* names the answer's `policies` lists), so it denies anon and signed-in
|
|
19749
|
+
* requests again. `POLICY_NOT_FOUND` when the table has none.
|
|
19610
19750
|
*/
|
|
19611
19751
|
async removePolicy(projectId, table) {
|
|
19612
19752
|
const answer = await this.client.request(
|
|
@@ -19615,6 +19755,75 @@ var BackendApi = class {
|
|
|
19615
19755
|
);
|
|
19616
19756
|
return answer.policy;
|
|
19617
19757
|
}
|
|
19758
|
+
/*
|
|
19759
|
+
* ---- Named policies (spec 2026-09-24 P2-7 §11.1) ----
|
|
19760
|
+
* As Postgres's CREATE POLICY: any number per table, each permissive or
|
|
19761
|
+
* restrictive, for a command and roles, with USING and WITH CHECK
|
|
19762
|
+
* expressions. The control plane checks them against the live schema
|
|
19763
|
+
* with the worker's own compiler; the worker picks a change up within
|
|
19764
|
+
* about 10 seconds.
|
|
19765
|
+
*
|
|
19766
|
+
* A write — `runPolicySql`, `replaceRlsPolicy`, `dropRlsPolicy`, and
|
|
19767
|
+
* `setPolicy` above — on a backend turned on before named policies is
|
|
19768
|
+
* `409 BACKEND_OUTDATED` "The project backend is older than named
|
|
19769
|
+
* policies. Run `naro backend enable` to update it, then try again."
|
|
19770
|
+
* (`backend.enable` also turns a backend that is off back on.) Reads
|
|
19771
|
+
* and `removePolicy` work there.
|
|
19772
|
+
*/
|
|
19773
|
+
/** The named policies, by table then name; only one table's with `table`. */
|
|
19774
|
+
async rlsPolicies(projectId, options = {}) {
|
|
19775
|
+
const query = options.table === void 0 ? "" : `?table=${encodeURIComponent(options.table)}`;
|
|
19776
|
+
const body = await this.client.request(
|
|
19777
|
+
this.path(projectId, `/rls/policies${query}`),
|
|
19778
|
+
{ context: "List policies" }
|
|
19779
|
+
);
|
|
19780
|
+
return body.policies;
|
|
19781
|
+
}
|
|
19782
|
+
/**
|
|
19783
|
+
* Runs one CREATE POLICY or DROP POLICY statement. A statement that does
|
|
19784
|
+
* not parse is `VALIDATION_FAILED` with Postgres's words and where
|
|
19785
|
+
* ("syntax error at or near … at character N."); a policy already there
|
|
19786
|
+
* is `POLICY_EXISTS`; DROP POLICY of none is `POLICY_NOT_FOUND` unless
|
|
19787
|
+
* it says IF EXISTS.
|
|
19788
|
+
*/
|
|
19789
|
+
async runPolicySql(projectId, sql, options = {}) {
|
|
19790
|
+
const answer = await this.client.request(this.path(projectId, `/rls/policies${writeQuery(options)}`), {
|
|
19791
|
+
method: "POST",
|
|
19792
|
+
body: { sql },
|
|
19793
|
+
context: "Run policy statement"
|
|
19794
|
+
});
|
|
19795
|
+
return "policy" in answer ? { created: answer.policy } : { dropped: answer.dropped };
|
|
19796
|
+
}
|
|
19797
|
+
/**
|
|
19798
|
+
* Replaces the named policy with a CREATE POLICY on the same table,
|
|
19799
|
+
* which may rename it. `POLICY_NOT_FOUND` when there is none to replace.
|
|
19800
|
+
* `name` is the name as typed: the policy of exactly that name, else the
|
|
19801
|
+
* one of its ASCII lower case — what `CREATE POLICY MyPolicy` stored.
|
|
19802
|
+
*/
|
|
19803
|
+
async replaceRlsPolicy(projectId, table, name, sql, options = {}) {
|
|
19804
|
+
const answer = await this.client.request(
|
|
19805
|
+
this.path(
|
|
19806
|
+
projectId,
|
|
19807
|
+
`${this.namedPath(table, name)}${writeQuery(options)}`
|
|
19808
|
+
),
|
|
19809
|
+
{ method: "PUT", body: { sql }, context: "Replace policy" }
|
|
19810
|
+
);
|
|
19811
|
+
return answer.policy;
|
|
19812
|
+
}
|
|
19813
|
+
/**
|
|
19814
|
+
* DROP POLICY: answers the policy dropped; `POLICY_NOT_FOUND` when none.
|
|
19815
|
+
* `name` is the name as typed, as for `replaceRlsPolicy`.
|
|
19816
|
+
*/
|
|
19817
|
+
async dropRlsPolicy(projectId, table, name) {
|
|
19818
|
+
const answer = await this.client.request(
|
|
19819
|
+
this.path(projectId, this.namedPath(table, name)),
|
|
19820
|
+
{ method: "DELETE", context: "Drop policy" }
|
|
19821
|
+
);
|
|
19822
|
+
return answer.policy;
|
|
19823
|
+
}
|
|
19824
|
+
namedPath(table, name) {
|
|
19825
|
+
return `/rls/policies/${encodeURIComponent(table)}/${encodeURIComponent(name)}`;
|
|
19826
|
+
}
|
|
19618
19827
|
};
|
|
19619
19828
|
|
|
19620
19829
|
// ../../packages/sdk/src/database.ts
|
|
@@ -19667,7 +19876,7 @@ function summarizeStatement(statement) {
|
|
|
19667
19876
|
|
|
19668
19877
|
// ../../packages/sdk/src/dotenv.ts
|
|
19669
19878
|
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
19670
|
-
import { join as
|
|
19879
|
+
import { join as join19 } from "path";
|
|
19671
19880
|
function parseDotenv(text) {
|
|
19672
19881
|
const out = {};
|
|
19673
19882
|
for (const line of text.split(/\r?\n/)) {
|
|
@@ -19685,7 +19894,7 @@ function parseDotenv(text) {
|
|
|
19685
19894
|
}
|
|
19686
19895
|
function readProjectEnv(dir) {
|
|
19687
19896
|
for (const name of [".env.local", ".env.production", ".env"]) {
|
|
19688
|
-
const path =
|
|
19897
|
+
const path = join19(dir, name);
|
|
19689
19898
|
if (existsSync(path)) return parseDotenv(readFileSync2(path, "utf8"));
|
|
19690
19899
|
}
|
|
19691
19900
|
return {};
|
|
@@ -19700,7 +19909,7 @@ function serializeDotenv(vars) {
|
|
|
19700
19909
|
|
|
19701
19910
|
// ../../packages/sdk/src/env-scan.ts
|
|
19702
19911
|
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
|
|
19703
|
-
import { join as
|
|
19912
|
+
import { join as join20 } from "path";
|
|
19704
19913
|
var SOURCE_EXTENSIONS3 = /* @__PURE__ */ new Set([
|
|
19705
19914
|
".ts",
|
|
19706
19915
|
".tsx",
|
|
@@ -19755,7 +19964,7 @@ function collect(dir, out) {
|
|
|
19755
19964
|
for (const entry of entries) {
|
|
19756
19965
|
if (out.length >= MAX_FILES) return;
|
|
19757
19966
|
if (entry.isSymbolicLink()) continue;
|
|
19758
|
-
const path =
|
|
19967
|
+
const path = join20(dir, entry.name);
|
|
19759
19968
|
if (entry.isDirectory()) {
|
|
19760
19969
|
if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name))
|
|
19761
19970
|
continue;
|
|
@@ -19787,7 +19996,7 @@ function scanEnvReferences(dir) {
|
|
|
19787
19996
|
}
|
|
19788
19997
|
}
|
|
19789
19998
|
for (const name of EXAMPLE_FILES) {
|
|
19790
|
-
const path =
|
|
19999
|
+
const path = join20(dir, name);
|
|
19791
20000
|
if (!existsSync2(path)) continue;
|
|
19792
20001
|
let parsed;
|
|
19793
20002
|
try {
|
|
@@ -19805,11 +20014,11 @@ function scanEnvReferences(dir) {
|
|
|
19805
20014
|
|
|
19806
20015
|
// ../../packages/sdk/src/framework.ts
|
|
19807
20016
|
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
19808
|
-
import { join as
|
|
20017
|
+
import { join as join21 } from "path";
|
|
19809
20018
|
function readPackageJson2(dir) {
|
|
19810
20019
|
try {
|
|
19811
20020
|
return JSON.parse(
|
|
19812
|
-
readFileSync4(
|
|
20021
|
+
readFileSync4(join21(dir, "package.json"), "utf8")
|
|
19813
20022
|
);
|
|
19814
20023
|
} catch {
|
|
19815
20024
|
return null;
|
|
@@ -19820,7 +20029,7 @@ function hasDependency(pkg, name) {
|
|
|
19820
20029
|
}
|
|
19821
20030
|
function hasConfig(dir, base2, evidence) {
|
|
19822
20031
|
for (const ext of ["js", "mjs", "cjs", "ts", "mts"]) {
|
|
19823
|
-
if (existsSync3(
|
|
20032
|
+
if (existsSync3(join21(dir, `${base2}.${ext}`))) {
|
|
19824
20033
|
evidence.push(`${base2}.${ext}`);
|
|
19825
20034
|
return true;
|
|
19826
20035
|
}
|
|
@@ -19928,8 +20137,8 @@ function detectFramework2(dir) {
|
|
|
19928
20137
|
if (!byDependency && !byConfig) continue;
|
|
19929
20138
|
if (byDependency) evidence.push(`package.json: ${detector.dependency}`);
|
|
19930
20139
|
if (detector.framework === "nextjs") {
|
|
19931
|
-
if (existsSync3(
|
|
19932
|
-
if (existsSync3(
|
|
20140
|
+
if (existsSync3(join21(dir, "app"))) evidence.push("app/");
|
|
20141
|
+
if (existsSync3(join21(dir, "pages"))) evidence.push("pages/");
|
|
19933
20142
|
}
|
|
19934
20143
|
return {
|
|
19935
20144
|
framework: detector.framework,
|
|
@@ -19940,7 +20149,7 @@ function detectFramework2(dir) {
|
|
|
19940
20149
|
deployable: true
|
|
19941
20150
|
};
|
|
19942
20151
|
}
|
|
19943
|
-
if (!pkg && existsSync3(
|
|
20152
|
+
if (!pkg && existsSync3(join21(dir, "index.html"))) {
|
|
19944
20153
|
evidence.push("index.html (no package.json)");
|
|
19945
20154
|
return {
|
|
19946
20155
|
framework: "static",
|
|
@@ -19994,6 +20203,276 @@ function systemTablePredicate(column = "name") {
|
|
|
19994
20203
|
}
|
|
19995
20204
|
var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
|
|
19996
20205
|
|
|
20206
|
+
// ../../packages/data-core/src/d1.ts
|
|
20207
|
+
function rowsOf(result) {
|
|
20208
|
+
const rows = result?.results ?? [];
|
|
20209
|
+
return rows.filter(
|
|
20210
|
+
(row) => typeof row === "object" && row !== null && !Array.isArray(row)
|
|
20211
|
+
);
|
|
20212
|
+
}
|
|
20213
|
+
|
|
20214
|
+
// ../../packages/data-core/src/policy-lexer.ts
|
|
20215
|
+
var PolicySyntaxError = class extends Error {
|
|
20216
|
+
constructor(reason, position) {
|
|
20217
|
+
super(`${reason} at character ${String(position)}`);
|
|
20218
|
+
this.reason = reason;
|
|
20219
|
+
this.position = position;
|
|
20220
|
+
}
|
|
20221
|
+
};
|
|
20222
|
+
var OPERATORS = [
|
|
20223
|
+
"->>",
|
|
20224
|
+
"->",
|
|
20225
|
+
"::",
|
|
20226
|
+
"<>",
|
|
20227
|
+
"!=",
|
|
20228
|
+
"<=",
|
|
20229
|
+
">=",
|
|
20230
|
+
"<@",
|
|
20231
|
+
"@>",
|
|
20232
|
+
"&&",
|
|
20233
|
+
"(",
|
|
20234
|
+
")",
|
|
20235
|
+
",",
|
|
20236
|
+
".",
|
|
20237
|
+
";",
|
|
20238
|
+
"=",
|
|
20239
|
+
"<",
|
|
20240
|
+
">",
|
|
20241
|
+
"[",
|
|
20242
|
+
"]",
|
|
20243
|
+
"*",
|
|
20244
|
+
"-"
|
|
20245
|
+
];
|
|
20246
|
+
function isSpace(char) {
|
|
20247
|
+
return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
|
|
20248
|
+
}
|
|
20249
|
+
function isIdentStart(char) {
|
|
20250
|
+
return /[A-Za-z_]/.test(char) || char.charCodeAt(0) >= 128;
|
|
20251
|
+
}
|
|
20252
|
+
function isIdentChar(char) {
|
|
20253
|
+
return /[A-Za-z0-9_$]/.test(char) || char.charCodeAt(0) >= 128;
|
|
20254
|
+
}
|
|
20255
|
+
function isDigit(char) {
|
|
20256
|
+
return char >= "0" && char <= "9";
|
|
20257
|
+
}
|
|
20258
|
+
var NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/;
|
|
20259
|
+
function tokenize(text) {
|
|
20260
|
+
const tokens = [];
|
|
20261
|
+
let index = 0;
|
|
20262
|
+
const at = (offset = 0) => text.charAt(index + offset);
|
|
20263
|
+
while (index < text.length) {
|
|
20264
|
+
const char = at();
|
|
20265
|
+
const position = index + 1;
|
|
20266
|
+
if (char === "\0")
|
|
20267
|
+
throw new PolicySyntaxError("NUL characters are not allowed", position);
|
|
20268
|
+
if (isSpace(char)) {
|
|
20269
|
+
index++;
|
|
20270
|
+
continue;
|
|
20271
|
+
}
|
|
20272
|
+
if (char === "-" && at(1) === "-") {
|
|
20273
|
+
while (index < text.length && at() !== "\n" && at() !== "\r") index++;
|
|
20274
|
+
continue;
|
|
20275
|
+
}
|
|
20276
|
+
if (char === "/" && at(1) === "*") {
|
|
20277
|
+
let depth = 0;
|
|
20278
|
+
while (index < text.length) {
|
|
20279
|
+
if (at() === "/" && at(1) === "*") {
|
|
20280
|
+
depth++;
|
|
20281
|
+
index += 2;
|
|
20282
|
+
} else if (at() === "*" && at(1) === "/") {
|
|
20283
|
+
depth--;
|
|
20284
|
+
index += 2;
|
|
20285
|
+
if (depth === 0) break;
|
|
20286
|
+
} else index++;
|
|
20287
|
+
}
|
|
20288
|
+
if (depth !== 0)
|
|
20289
|
+
throw new PolicySyntaxError("unterminated /* comment", position);
|
|
20290
|
+
continue;
|
|
20291
|
+
}
|
|
20292
|
+
if (char === "'") {
|
|
20293
|
+
let value = "";
|
|
20294
|
+
index++;
|
|
20295
|
+
for (; ; ) {
|
|
20296
|
+
if (index >= text.length)
|
|
20297
|
+
throw new PolicySyntaxError("unterminated quoted string", position);
|
|
20298
|
+
if (at() === "'") {
|
|
20299
|
+
if (at(1) === "'") {
|
|
20300
|
+
value += "'";
|
|
20301
|
+
index += 2;
|
|
20302
|
+
continue;
|
|
20303
|
+
}
|
|
20304
|
+
index++;
|
|
20305
|
+
break;
|
|
20306
|
+
}
|
|
20307
|
+
value += at();
|
|
20308
|
+
index++;
|
|
20309
|
+
}
|
|
20310
|
+
tokens.push({ kind: "string", value, quoted: false, position });
|
|
20311
|
+
continue;
|
|
20312
|
+
}
|
|
20313
|
+
if (char === '"') {
|
|
20314
|
+
let value = "";
|
|
20315
|
+
index++;
|
|
20316
|
+
for (; ; ) {
|
|
20317
|
+
if (index >= text.length)
|
|
20318
|
+
throw new PolicySyntaxError(
|
|
20319
|
+
"unterminated quoted identifier",
|
|
20320
|
+
position
|
|
20321
|
+
);
|
|
20322
|
+
if (at() === '"') {
|
|
20323
|
+
if (at(1) === '"') {
|
|
20324
|
+
value += '"';
|
|
20325
|
+
index += 2;
|
|
20326
|
+
continue;
|
|
20327
|
+
}
|
|
20328
|
+
index++;
|
|
20329
|
+
break;
|
|
20330
|
+
}
|
|
20331
|
+
value += at();
|
|
20332
|
+
index++;
|
|
20333
|
+
}
|
|
20334
|
+
if (value === "")
|
|
20335
|
+
throw new PolicySyntaxError("zero-length quoted identifier", position);
|
|
20336
|
+
tokens.push({ kind: "ident", value, quoted: true, position });
|
|
20337
|
+
continue;
|
|
20338
|
+
}
|
|
20339
|
+
if (char === "$")
|
|
20340
|
+
throw new PolicySyntaxError(
|
|
20341
|
+
"dollar-quoted strings are not supported: use '\u2026'",
|
|
20342
|
+
position
|
|
20343
|
+
);
|
|
20344
|
+
if (isDigit(char) || char === "." && isDigit(at(1))) {
|
|
20345
|
+
const match = NUMBER.exec(text.slice(index));
|
|
20346
|
+
const value = match?.[0] ?? char;
|
|
20347
|
+
index += value.length;
|
|
20348
|
+
if (index < text.length && isIdentChar(at()))
|
|
20349
|
+
throw new PolicySyntaxError(
|
|
20350
|
+
`trailing junk after numeric literal ${value}`,
|
|
20351
|
+
position
|
|
20352
|
+
);
|
|
20353
|
+
tokens.push({ kind: "number", value, quoted: false, position });
|
|
20354
|
+
continue;
|
|
20355
|
+
}
|
|
20356
|
+
if (isIdentStart(char)) {
|
|
20357
|
+
let end = index + 1;
|
|
20358
|
+
while (end < text.length && isIdentChar(text.charAt(end))) end++;
|
|
20359
|
+
const value = text.slice(index, end);
|
|
20360
|
+
if (text.charAt(end) === "'")
|
|
20361
|
+
throw new PolicySyntaxError(
|
|
20362
|
+
`${value}'\u2026' strings are not supported: use a plain '\u2026' string`,
|
|
20363
|
+
position
|
|
20364
|
+
);
|
|
20365
|
+
index = end;
|
|
20366
|
+
tokens.push({ kind: "ident", value, quoted: false, position });
|
|
20367
|
+
continue;
|
|
20368
|
+
}
|
|
20369
|
+
const operator = OPERATORS.find(
|
|
20370
|
+
(candidate) => text.startsWith(candidate, index)
|
|
20371
|
+
);
|
|
20372
|
+
if (!operator)
|
|
20373
|
+
throw new PolicySyntaxError(
|
|
20374
|
+
`unexpected character ${JSON.stringify(char)}`,
|
|
20375
|
+
position
|
|
20376
|
+
);
|
|
20377
|
+
index += operator.length;
|
|
20378
|
+
tokens.push({ kind: "op", value: operator, quoted: false, position });
|
|
20379
|
+
}
|
|
20380
|
+
tokens.push({
|
|
20381
|
+
kind: "eof",
|
|
20382
|
+
value: "",
|
|
20383
|
+
quoted: false,
|
|
20384
|
+
position: text.length + 1
|
|
20385
|
+
});
|
|
20386
|
+
return tokens;
|
|
20387
|
+
}
|
|
20388
|
+
function isKeyword(token, word) {
|
|
20389
|
+
return token !== void 0 && token.kind === "ident" && !token.quoted && asciiLower(token.value) === word;
|
|
20390
|
+
}
|
|
20391
|
+
function isOperator(token, op) {
|
|
20392
|
+
return token !== void 0 && token.kind === "op" && token.value === op;
|
|
20393
|
+
}
|
|
20394
|
+
function describeToken(token) {
|
|
20395
|
+
if (token.kind === "eof") return "end of input";
|
|
20396
|
+
if (token.kind === "string") return `'${token.value}'`;
|
|
20397
|
+
if (token.kind === "ident" && token.quoted) return `"${token.value}"`;
|
|
20398
|
+
return `"${token.value}"`;
|
|
20399
|
+
}
|
|
20400
|
+
var TokenStream = class {
|
|
20401
|
+
constructor(tokens) {
|
|
20402
|
+
this.tokens = tokens;
|
|
20403
|
+
}
|
|
20404
|
+
#index = 0;
|
|
20405
|
+
peek(offset = 0) {
|
|
20406
|
+
const token = this.tokens[Math.min(this.#index + offset, this.tokens.length - 1)];
|
|
20407
|
+
if (!token) throw new Error("TokenStream: no eof token");
|
|
20408
|
+
return token;
|
|
20409
|
+
}
|
|
20410
|
+
next() {
|
|
20411
|
+
const token = this.peek();
|
|
20412
|
+
if (token.kind !== "eof") this.#index++;
|
|
20413
|
+
return token;
|
|
20414
|
+
}
|
|
20415
|
+
/** Takes the keyword when it is next. */
|
|
20416
|
+
acceptKeyword(word) {
|
|
20417
|
+
if (!isKeyword(this.peek(), word)) return false;
|
|
20418
|
+
this.next();
|
|
20419
|
+
return true;
|
|
20420
|
+
}
|
|
20421
|
+
acceptOperator(op) {
|
|
20422
|
+
if (!isOperator(this.peek(), op)) return false;
|
|
20423
|
+
this.next();
|
|
20424
|
+
return true;
|
|
20425
|
+
}
|
|
20426
|
+
expectKeyword(word) {
|
|
20427
|
+
const token = this.peek();
|
|
20428
|
+
if (!isKeyword(token, word))
|
|
20429
|
+
throw new PolicySyntaxError(
|
|
20430
|
+
`expected ${word.toUpperCase()}, found ${describeToken(token)}`,
|
|
20431
|
+
token.position
|
|
20432
|
+
);
|
|
20433
|
+
return this.next();
|
|
20434
|
+
}
|
|
20435
|
+
expectOperator(op) {
|
|
20436
|
+
const token = this.peek();
|
|
20437
|
+
if (!isOperator(token, op))
|
|
20438
|
+
throw new PolicySyntaxError(
|
|
20439
|
+
`expected ${op}, found ${describeToken(token)}`,
|
|
20440
|
+
token.position
|
|
20441
|
+
);
|
|
20442
|
+
return this.next();
|
|
20443
|
+
}
|
|
20444
|
+
/** Postgres's own words for anything else. */
|
|
20445
|
+
unexpected(token = this.peek()) {
|
|
20446
|
+
throw new PolicySyntaxError(
|
|
20447
|
+
token.kind === "eof" ? "syntax error at end of input" : `syntax error at or near ${describeToken(token)}`,
|
|
20448
|
+
token.position
|
|
20449
|
+
);
|
|
20450
|
+
}
|
|
20451
|
+
};
|
|
20452
|
+
|
|
20453
|
+
// ../../packages/data-core/src/schema.ts
|
|
20454
|
+
var SCHEMA_SQL = [
|
|
20455
|
+
`SELECT t.name AS table_name, t.type AS table_type, c.cid AS cid, c.name AS column_name, c.type AS column_type, c."notnull" AS not_null, c.dflt_value AS default_value, c.pk AS pk, c.hidden AS hidden FROM pragma_table_list AS t JOIN pragma_table_xinfo(t.name, t.schema) AS c WHERE t.schema = 'main' AND t.type IN ('table', 'virtual') AND ${systemTablePredicate("t.name")} ORDER BY t.name, c.cid`,
|
|
20456
|
+
// Phase 2-2 (적대적 리뷰 반영): each table's CREATE TABLE text, where
|
|
20457
|
+
// alone a column's collation is written. A separate statement, so the
|
|
20458
|
+
// text comes once per table rather than once per column. D1 answers
|
|
20459
|
+
// `sqlite_master` reads (the database routes' `_tables.ts` reads it too).
|
|
20460
|
+
`SELECT name AS table_name, sql AS table_sql FROM sqlite_master WHERE type = 'table' AND ${systemTablePredicate("name")}`
|
|
20461
|
+
];
|
|
20462
|
+
var TABLE_NAMES_SQL = `SELECT name AS table_name FROM pragma_table_list WHERE schema = 'main' AND type IN ('table', 'virtual') AND ${systemTablePredicate("name")}`;
|
|
20463
|
+
function tableNamesFromRows(rows) {
|
|
20464
|
+
const names = [];
|
|
20465
|
+
for (const row of rows) {
|
|
20466
|
+
const name = row.table_name;
|
|
20467
|
+
if (typeof name === "string" && name !== "" && !isSystemTable(name))
|
|
20468
|
+
names.push(name);
|
|
20469
|
+
}
|
|
20470
|
+
return names;
|
|
20471
|
+
}
|
|
20472
|
+
async function readTableNames(db) {
|
|
20473
|
+
return tableNamesFromRows(rowsOf(await db.prepare(TABLE_NAMES_SQL).all()));
|
|
20474
|
+
}
|
|
20475
|
+
|
|
19997
20476
|
// ../../packages/data-core/src/values.ts
|
|
19998
20477
|
function isPlainObject2(value) {
|
|
19999
20478
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
@@ -20004,6 +20483,1121 @@ function isPlainObject2(value) {
|
|
|
20004
20483
|
function isValue(value) {
|
|
20005
20484
|
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
20006
20485
|
}
|
|
20486
|
+
function utf8ByteLength(text) {
|
|
20487
|
+
let bytes = 0;
|
|
20488
|
+
for (let index = 0; index < text.length; index++) {
|
|
20489
|
+
const unit = text.charCodeAt(index);
|
|
20490
|
+
if (unit < 128) bytes += 1;
|
|
20491
|
+
else if (unit < 2048) bytes += 2;
|
|
20492
|
+
else if (unit >= 55296 && unit <= 56319 && index + 1 < text.length) {
|
|
20493
|
+
const next = text.charCodeAt(index + 1);
|
|
20494
|
+
if (next >= 56320 && next <= 57343) {
|
|
20495
|
+
bytes += 4;
|
|
20496
|
+
index++;
|
|
20497
|
+
} else bytes += 3;
|
|
20498
|
+
} else bytes += 3;
|
|
20499
|
+
}
|
|
20500
|
+
return bytes;
|
|
20501
|
+
}
|
|
20502
|
+
|
|
20503
|
+
// ../../packages/data-core/src/sql-fragments.ts
|
|
20504
|
+
function globLiteral(char) {
|
|
20505
|
+
return char === "*" || char === "?" || char === "[" ? `[${char}]` : char;
|
|
20506
|
+
}
|
|
20507
|
+
function likeToGlob(pattern) {
|
|
20508
|
+
let glob = "";
|
|
20509
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
20510
|
+
const char = pattern.charAt(index);
|
|
20511
|
+
if (char === "\\" && index + 1 < pattern.length) {
|
|
20512
|
+
index++;
|
|
20513
|
+
glob += globLiteral(pattern.charAt(index));
|
|
20514
|
+
} else if (char === "%") glob += "*";
|
|
20515
|
+
else if (char === "_") glob += "?";
|
|
20516
|
+
else glob += globLiteral(char);
|
|
20517
|
+
}
|
|
20518
|
+
return glob;
|
|
20519
|
+
}
|
|
20520
|
+
var MAX_PATTERN_BYTES = 50;
|
|
20521
|
+
function boundPattern(pattern, caseInsensitive) {
|
|
20522
|
+
return caseInsensitive ? pattern : likeToGlob(pattern);
|
|
20523
|
+
}
|
|
20524
|
+
function patternTooLong(pattern, caseInsensitive) {
|
|
20525
|
+
if (pattern.length > MAX_PATTERN_BYTES * 2) return true;
|
|
20526
|
+
return utf8ByteLength(boundPattern(pattern, caseInsensitive)) > MAX_PATTERN_BYTES;
|
|
20527
|
+
}
|
|
20528
|
+
|
|
20529
|
+
// ../../packages/data-core/src/policy-expr.ts
|
|
20530
|
+
var MAX_EXPRESSION_BYTES = 3 * 16384;
|
|
20531
|
+
var MAX_EXPRESSION_NODES = 1600;
|
|
20532
|
+
var MAX_EXPRESSION_DEPTH = 32;
|
|
20533
|
+
var MAX_SUBQUERIES = 4;
|
|
20534
|
+
var MAX_SUBQUERY_DEPTH = 2;
|
|
20535
|
+
var MAX_ARRAY_OPERATORS = 4;
|
|
20536
|
+
var MAX_POLICY_ARRAY_VALUES = 100;
|
|
20537
|
+
var MAX_IN_VALUES = 100;
|
|
20538
|
+
var MAX_POLICY_PATTERN_LENGTH = MAX_PATTERN_BYTES;
|
|
20539
|
+
var JWT_CLAIMS = ["sub", "role", "email", "email_verified"];
|
|
20540
|
+
var POLICY_FUNCTIONS = [
|
|
20541
|
+
"lower",
|
|
20542
|
+
"upper",
|
|
20543
|
+
"length",
|
|
20544
|
+
"coalesce",
|
|
20545
|
+
"now"
|
|
20546
|
+
];
|
|
20547
|
+
var VALUE_KINDS = /* @__PURE__ */ new Set([
|
|
20548
|
+
"literal",
|
|
20549
|
+
"column",
|
|
20550
|
+
"auth",
|
|
20551
|
+
"jwt",
|
|
20552
|
+
"func",
|
|
20553
|
+
"cast"
|
|
20554
|
+
]);
|
|
20555
|
+
function isValueExpr(expr) {
|
|
20556
|
+
return VALUE_KINDS.has(expr.kind);
|
|
20557
|
+
}
|
|
20558
|
+
function isConditionExpr(expr) {
|
|
20559
|
+
if (!isValueExpr(expr)) return true;
|
|
20560
|
+
if (expr.kind === "column") return true;
|
|
20561
|
+
return expr.kind === "literal" && (expr.value === null || typeof expr.value === "boolean");
|
|
20562
|
+
}
|
|
20563
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
20564
|
+
"all",
|
|
20565
|
+
"and",
|
|
20566
|
+
"array",
|
|
20567
|
+
"as",
|
|
20568
|
+
"between",
|
|
20569
|
+
"check",
|
|
20570
|
+
"current_timestamp",
|
|
20571
|
+
"distinct",
|
|
20572
|
+
"exists",
|
|
20573
|
+
"false",
|
|
20574
|
+
"for",
|
|
20575
|
+
`from`,
|
|
20576
|
+
"ilike",
|
|
20577
|
+
"in",
|
|
20578
|
+
"is",
|
|
20579
|
+
"like",
|
|
20580
|
+
"not",
|
|
20581
|
+
"null",
|
|
20582
|
+
"on",
|
|
20583
|
+
"or",
|
|
20584
|
+
"select",
|
|
20585
|
+
"to",
|
|
20586
|
+
"true",
|
|
20587
|
+
"using",
|
|
20588
|
+
"where",
|
|
20589
|
+
"with"
|
|
20590
|
+
]);
|
|
20591
|
+
var CLAUSE_WORDS = /* @__PURE__ */ new Set([
|
|
20592
|
+
"join",
|
|
20593
|
+
"group",
|
|
20594
|
+
"order",
|
|
20595
|
+
"limit",
|
|
20596
|
+
"union",
|
|
20597
|
+
"having",
|
|
20598
|
+
"left",
|
|
20599
|
+
"inner",
|
|
20600
|
+
"cross"
|
|
20601
|
+
]);
|
|
20602
|
+
var SPECIAL_WORDS = /* @__PURE__ */ new Set([
|
|
20603
|
+
...RESERVED,
|
|
20604
|
+
...CLAUSE_WORDS,
|
|
20605
|
+
"symmetric",
|
|
20606
|
+
"auth"
|
|
20607
|
+
]);
|
|
20608
|
+
var TEXT_TYPES = /* @__PURE__ */ new Set(["text", "varchar", "uuid"]);
|
|
20609
|
+
function count(state, token) {
|
|
20610
|
+
if (++state.nodes > MAX_EXPRESSION_NODES)
|
|
20611
|
+
throw new PolicySyntaxError(
|
|
20612
|
+
`the expression has more than ${String(MAX_EXPRESSION_NODES)} parts`,
|
|
20613
|
+
token.position
|
|
20614
|
+
);
|
|
20615
|
+
}
|
|
20616
|
+
function nested(state, token, read) {
|
|
20617
|
+
if (++state.depth > MAX_EXPRESSION_DEPTH)
|
|
20618
|
+
throw new PolicySyntaxError(
|
|
20619
|
+
`the expression nests deeper than ${String(MAX_EXPRESSION_DEPTH)} levels`,
|
|
20620
|
+
token.position
|
|
20621
|
+
);
|
|
20622
|
+
try {
|
|
20623
|
+
return read();
|
|
20624
|
+
} finally {
|
|
20625
|
+
state.depth--;
|
|
20626
|
+
}
|
|
20627
|
+
}
|
|
20628
|
+
function truthProblem(operand) {
|
|
20629
|
+
const text = "is text, where 'true' reads as false";
|
|
20630
|
+
switch (operand.kind) {
|
|
20631
|
+
case "column":
|
|
20632
|
+
return void 0;
|
|
20633
|
+
case "literal":
|
|
20634
|
+
return typeof operand.value === "string" ? `a '\u2026' string ${text}: write TRUE or FALSE, or compare the string with =` : void 0;
|
|
20635
|
+
case "jwt": {
|
|
20636
|
+
const claim = printValue(operand);
|
|
20637
|
+
return `${claim} ${text}: compare it with text, ${claim} = 'true'`;
|
|
20638
|
+
}
|
|
20639
|
+
case "auth":
|
|
20640
|
+
return operand.fn === "uid" ? "auth.uid() is text: compare it, auth.uid() = user_id, or test auth.uid() IS NOT NULL" : "auth.role() is text: compare it, auth.role() = 'authenticated'";
|
|
20641
|
+
case "cast":
|
|
20642
|
+
return `a ::text cast ${text}: compare it with text, (\u2026)::text = 'true'`;
|
|
20643
|
+
case "func":
|
|
20644
|
+
switch (operand.name) {
|
|
20645
|
+
case "lower":
|
|
20646
|
+
case "upper":
|
|
20647
|
+
return `${operand.name}(\u2026) ${text}: compare it with text, ${operand.name}(\u2026) = 'true'`;
|
|
20648
|
+
case "coalesce":
|
|
20649
|
+
return "coalesce(\u2026) is not a boolean: compare it, coalesce(\u2026) = 'true'";
|
|
20650
|
+
case "length":
|
|
20651
|
+
return "length(\u2026) is a number: compare it, length(\u2026) > 0";
|
|
20652
|
+
case "now":
|
|
20653
|
+
return "now() is a time: compare it, created_at < now()";
|
|
20654
|
+
}
|
|
20655
|
+
}
|
|
20656
|
+
}
|
|
20657
|
+
function truthTestProblem(operand) {
|
|
20658
|
+
const problem = truthProblem(operand);
|
|
20659
|
+
return problem === void 0 ? void 0 : `IS TRUE and IS FALSE test a boolean, and ${problem}`;
|
|
20660
|
+
}
|
|
20661
|
+
function operandValues(operand) {
|
|
20662
|
+
return operand.kind === "func" && operand.name === "coalesce" ? operand.args.flatMap(operandValues) : [operand];
|
|
20663
|
+
}
|
|
20664
|
+
function isBooleanConstant(value) {
|
|
20665
|
+
return value.kind === "literal" && typeof value.value === "boolean";
|
|
20666
|
+
}
|
|
20667
|
+
function isTextValue(value) {
|
|
20668
|
+
switch (value.kind) {
|
|
20669
|
+
case "literal":
|
|
20670
|
+
return typeof value.value === "string";
|
|
20671
|
+
case "auth":
|
|
20672
|
+
case "jwt":
|
|
20673
|
+
case "cast":
|
|
20674
|
+
return true;
|
|
20675
|
+
case "func":
|
|
20676
|
+
return value.name !== "length" && value.name !== "coalesce";
|
|
20677
|
+
case "column":
|
|
20678
|
+
return false;
|
|
20679
|
+
}
|
|
20680
|
+
}
|
|
20681
|
+
function pair(left, right, inner = false) {
|
|
20682
|
+
return { left, right, inner };
|
|
20683
|
+
}
|
|
20684
|
+
function comparedPairs(expr) {
|
|
20685
|
+
switch (expr.kind) {
|
|
20686
|
+
case "compare":
|
|
20687
|
+
case "distinct":
|
|
20688
|
+
return [pair(expr.left, expr.right)];
|
|
20689
|
+
case "in":
|
|
20690
|
+
return expr.list.map((item) => pair(expr.operand, item));
|
|
20691
|
+
case "inSelect":
|
|
20692
|
+
return expr.subquery.select === "*" ? [] : [pair(expr.operand, expr.subquery.select, true)];
|
|
20693
|
+
case "between":
|
|
20694
|
+
return [pair(expr.operand, expr.low), pair(expr.operand, expr.high)];
|
|
20695
|
+
case "like":
|
|
20696
|
+
return [pair(expr.operand, { kind: "literal", value: expr.pattern })];
|
|
20697
|
+
default:
|
|
20698
|
+
return [];
|
|
20699
|
+
}
|
|
20700
|
+
}
|
|
20701
|
+
function booleanComparisonProblem(expr) {
|
|
20702
|
+
for (const { left, right } of comparedPairs(expr)) {
|
|
20703
|
+
const met = [...operandValues(left), ...operandValues(right)];
|
|
20704
|
+
if (!met.some(isBooleanConstant)) continue;
|
|
20705
|
+
const texts = met.filter(isTextValue);
|
|
20706
|
+
const text = texts.find((value) => value.kind !== "literal") ?? texts[0];
|
|
20707
|
+
if (text === void 0) continue;
|
|
20708
|
+
const problem = text.kind === "literal" ? "a '\u2026' string is text, where 'true' reads as false: write TRUE or FALSE on both sides, or a '\u2026' string on both" : truthProblem(text);
|
|
20709
|
+
return `a comparison with TRUE or FALSE compares a boolean, and ${String(problem)}`;
|
|
20710
|
+
}
|
|
20711
|
+
return void 0;
|
|
20712
|
+
}
|
|
20713
|
+
function comparable(expr, token) {
|
|
20714
|
+
const problem = booleanComparisonProblem(expr);
|
|
20715
|
+
if (problem !== void 0)
|
|
20716
|
+
throw new PolicySyntaxError(problem, token.position);
|
|
20717
|
+
return expr;
|
|
20718
|
+
}
|
|
20719
|
+
function requireCondition(expr, token) {
|
|
20720
|
+
if (!isConditionExpr(expr)) {
|
|
20721
|
+
const problem = isValueExpr(expr) ? truthProblem(expr) : void 0;
|
|
20722
|
+
throw new PolicySyntaxError(
|
|
20723
|
+
`this is a value, not a condition: ${problem ?? "compare it with =, IN, LIKE, IS \u2026 (a column alone reads as its truth value)"}`,
|
|
20724
|
+
token.position
|
|
20725
|
+
);
|
|
20726
|
+
}
|
|
20727
|
+
return expr;
|
|
20728
|
+
}
|
|
20729
|
+
function requireValue(expr, token) {
|
|
20730
|
+
if (!isValueExpr(expr))
|
|
20731
|
+
throw new PolicySyntaxError(
|
|
20732
|
+
"a comparison compares values: combine conditions with AND, OR and NOT",
|
|
20733
|
+
token.position
|
|
20734
|
+
);
|
|
20735
|
+
return expr;
|
|
20736
|
+
}
|
|
20737
|
+
function readOr(state) {
|
|
20738
|
+
const first = state.stream.peek();
|
|
20739
|
+
const args = [readAnd(state)];
|
|
20740
|
+
while (isKeyword(state.stream.peek(), "or")) {
|
|
20741
|
+
const token = state.stream.next();
|
|
20742
|
+
count(state, token);
|
|
20743
|
+
args.push(readAnd(state));
|
|
20744
|
+
}
|
|
20745
|
+
if (args.length === 1) return args[0];
|
|
20746
|
+
for (const arg of args) requireCondition(arg, first);
|
|
20747
|
+
return { kind: "or", args };
|
|
20748
|
+
}
|
|
20749
|
+
function readAnd(state) {
|
|
20750
|
+
const first = state.stream.peek();
|
|
20751
|
+
const args = [readNot(state)];
|
|
20752
|
+
while (isKeyword(state.stream.peek(), "and")) {
|
|
20753
|
+
const token = state.stream.next();
|
|
20754
|
+
count(state, token);
|
|
20755
|
+
args.push(readNot(state));
|
|
20756
|
+
}
|
|
20757
|
+
if (args.length === 1) return args[0];
|
|
20758
|
+
for (const arg of args) requireCondition(arg, first);
|
|
20759
|
+
return { kind: "and", args };
|
|
20760
|
+
}
|
|
20761
|
+
function readNot(state) {
|
|
20762
|
+
const token = state.stream.peek();
|
|
20763
|
+
if (isKeyword(token, "not")) {
|
|
20764
|
+
state.stream.next();
|
|
20765
|
+
count(state, token);
|
|
20766
|
+
return nested(state, token, () => {
|
|
20767
|
+
const inner = state.stream.peek();
|
|
20768
|
+
return { kind: "not", arg: requireCondition(readNot(state), inner) };
|
|
20769
|
+
});
|
|
20770
|
+
}
|
|
20771
|
+
return readPredicate(state);
|
|
20772
|
+
}
|
|
20773
|
+
function readPredicate(state) {
|
|
20774
|
+
const { stream } = state;
|
|
20775
|
+
const token = stream.peek();
|
|
20776
|
+
if (isKeyword(token, "exists")) {
|
|
20777
|
+
stream.next();
|
|
20778
|
+
count(state, token);
|
|
20779
|
+
stream.expectOperator("(");
|
|
20780
|
+
const subquery = readSubquery(state, token, "exists");
|
|
20781
|
+
stream.expectOperator(")");
|
|
20782
|
+
return { kind: "exists", subquery };
|
|
20783
|
+
}
|
|
20784
|
+
const left = readValue(state);
|
|
20785
|
+
const next = stream.peek();
|
|
20786
|
+
const compare2 = ["=", "<>", "!=", "<", "<=", ">", ">="].find(
|
|
20787
|
+
(op) => isOperator(next, op)
|
|
20788
|
+
);
|
|
20789
|
+
if (compare2) {
|
|
20790
|
+
stream.next();
|
|
20791
|
+
count(state, next);
|
|
20792
|
+
const right = readValue(state);
|
|
20793
|
+
return comparable(
|
|
20794
|
+
{
|
|
20795
|
+
kind: "compare",
|
|
20796
|
+
op: compare2 === "!=" ? "<>" : compare2,
|
|
20797
|
+
left: requireValue(left, token),
|
|
20798
|
+
right: requireValue(right, next)
|
|
20799
|
+
},
|
|
20800
|
+
next
|
|
20801
|
+
);
|
|
20802
|
+
}
|
|
20803
|
+
if (isKeyword(next, "is")) {
|
|
20804
|
+
stream.next();
|
|
20805
|
+
count(state, next);
|
|
20806
|
+
const operand = requireValue(left, token);
|
|
20807
|
+
const not2 = stream.acceptKeyword("not");
|
|
20808
|
+
if (stream.acceptKeyword("null"))
|
|
20809
|
+
return { kind: "is", not: not2, operand, value: null };
|
|
20810
|
+
const truth = stream.acceptKeyword("true") ? true : stream.acceptKeyword("false") ? false : void 0;
|
|
20811
|
+
if (truth !== void 0) {
|
|
20812
|
+
const problem = truthTestProblem(operand);
|
|
20813
|
+
if (problem !== void 0)
|
|
20814
|
+
throw new PolicySyntaxError(problem, next.position);
|
|
20815
|
+
return { kind: "is", not: not2, operand, value: truth };
|
|
20816
|
+
}
|
|
20817
|
+
if (stream.acceptKeyword("distinct")) {
|
|
20818
|
+
stream.expectKeyword(`from`);
|
|
20819
|
+
const right = requireValue(readValue(state), stream.peek());
|
|
20820
|
+
return comparable({ kind: "distinct", not: not2, left: operand, right }, next);
|
|
20821
|
+
}
|
|
20822
|
+
stream.unexpected();
|
|
20823
|
+
}
|
|
20824
|
+
if (isOperator(next, "@>") || isOperator(next, "<@") || isOperator(next, "&&"))
|
|
20825
|
+
return readArray(state, left, token);
|
|
20826
|
+
let not = false;
|
|
20827
|
+
let word = next;
|
|
20828
|
+
if (isKeyword(next, "not")) {
|
|
20829
|
+
const after = stream.peek(1);
|
|
20830
|
+
if (isKeyword(after, "in") || isKeyword(after, "like") || isKeyword(after, "ilike") || isKeyword(after, "between")) {
|
|
20831
|
+
stream.next();
|
|
20832
|
+
not = true;
|
|
20833
|
+
word = after;
|
|
20834
|
+
} else return left;
|
|
20835
|
+
}
|
|
20836
|
+
if (isKeyword(word, "in")) {
|
|
20837
|
+
stream.next();
|
|
20838
|
+
count(state, word);
|
|
20839
|
+
const operand = requireValue(left, token);
|
|
20840
|
+
stream.expectOperator("(");
|
|
20841
|
+
if (isKeyword(stream.peek(), "select")) {
|
|
20842
|
+
const subquery = readSubquery(state, word, "in");
|
|
20843
|
+
stream.expectOperator(")");
|
|
20844
|
+
return comparable({ kind: "inSelect", not, operand, subquery }, word);
|
|
20845
|
+
}
|
|
20846
|
+
const list = [];
|
|
20847
|
+
do {
|
|
20848
|
+
if (list.length >= MAX_IN_VALUES)
|
|
20849
|
+
throw new PolicySyntaxError(
|
|
20850
|
+
`IN lists at most ${String(MAX_IN_VALUES)} values`,
|
|
20851
|
+
stream.peek().position
|
|
20852
|
+
);
|
|
20853
|
+
const item = stream.peek();
|
|
20854
|
+
list.push(requireValue(readValue(state), item));
|
|
20855
|
+
} while (stream.acceptOperator(","));
|
|
20856
|
+
stream.expectOperator(")");
|
|
20857
|
+
return comparable({ kind: "in", not, operand, list }, word);
|
|
20858
|
+
}
|
|
20859
|
+
if (isKeyword(word, "like") || isKeyword(word, "ilike")) {
|
|
20860
|
+
stream.next();
|
|
20861
|
+
count(state, word);
|
|
20862
|
+
const operand = requireValue(left, token);
|
|
20863
|
+
const pattern = stream.peek();
|
|
20864
|
+
if (pattern.kind !== "string")
|
|
20865
|
+
throw new PolicySyntaxError(
|
|
20866
|
+
"a LIKE or ILIKE pattern must be a '\u2026' string",
|
|
20867
|
+
pattern.position
|
|
20868
|
+
);
|
|
20869
|
+
stream.next();
|
|
20870
|
+
const ci = isKeyword(word, "ilike");
|
|
20871
|
+
checkPattern(pattern, ci);
|
|
20872
|
+
return comparable(
|
|
20873
|
+
{ kind: "like", not, ci, operand, pattern: pattern.value },
|
|
20874
|
+
word
|
|
20875
|
+
);
|
|
20876
|
+
}
|
|
20877
|
+
if (isKeyword(word, "between")) {
|
|
20878
|
+
stream.next();
|
|
20879
|
+
count(state, word);
|
|
20880
|
+
const operand = requireValue(left, token);
|
|
20881
|
+
if (stream.acceptKeyword("symmetric"))
|
|
20882
|
+
throw new PolicySyntaxError(
|
|
20883
|
+
"BETWEEN SYMMETRIC is not supported",
|
|
20884
|
+
word.position
|
|
20885
|
+
);
|
|
20886
|
+
const low = requireValue(readValue(state), stream.peek());
|
|
20887
|
+
stream.expectKeyword("and");
|
|
20888
|
+
const high = requireValue(readValue(state), stream.peek());
|
|
20889
|
+
return comparable({ kind: "between", not, operand, low, high }, word);
|
|
20890
|
+
}
|
|
20891
|
+
return left;
|
|
20892
|
+
}
|
|
20893
|
+
var PATTERN_SHOWN = 24;
|
|
20894
|
+
function policyPatternProblem(pattern, ci) {
|
|
20895
|
+
if (!patternTooLong(pattern, ci)) return void 0;
|
|
20896
|
+
const chars = [...pattern];
|
|
20897
|
+
const shown = `${commentText(chars.slice(0, PATTERN_SHOWN).join(""))}${chars.length > PATTERN_SHOWN ? "\u2026" : ""}`;
|
|
20898
|
+
const bytes = utf8ByteLength(boundPattern(pattern, ci));
|
|
20899
|
+
return `${ci ? "an ILIKE" : "a LIKE"} pattern may be at most ${String(MAX_POLICY_PATTERN_LENGTH)} bytes of UTF-8, D1's limit, and '${shown}' is ${String(bytes)}${ci ? "" : " (LIKE counts each *, ? and [ as 3 bytes)"}`;
|
|
20900
|
+
}
|
|
20901
|
+
function checkPattern(token, ci) {
|
|
20902
|
+
const pattern = token.value;
|
|
20903
|
+
const problem = policyPatternProblem(pattern, ci);
|
|
20904
|
+
if (problem !== void 0)
|
|
20905
|
+
throw new PolicySyntaxError(problem, token.position);
|
|
20906
|
+
let run = 0;
|
|
20907
|
+
for (let index = pattern.length - 1; index >= 0; index--) {
|
|
20908
|
+
if (pattern[index] !== "\\") break;
|
|
20909
|
+
run++;
|
|
20910
|
+
}
|
|
20911
|
+
if (run % 2 === 1)
|
|
20912
|
+
throw new PolicySyntaxError(
|
|
20913
|
+
"a LIKE pattern must not end with an unescaped backslash",
|
|
20914
|
+
token.position
|
|
20915
|
+
);
|
|
20916
|
+
}
|
|
20917
|
+
function readArray(state, left, start) {
|
|
20918
|
+
const { stream } = state;
|
|
20919
|
+
const opToken = stream.next();
|
|
20920
|
+
count(state, opToken);
|
|
20921
|
+
if (left.kind !== "column")
|
|
20922
|
+
throw new PolicySyntaxError(
|
|
20923
|
+
`the left side of ${opToken.value} must be a column`,
|
|
20924
|
+
start.position
|
|
20925
|
+
);
|
|
20926
|
+
if (++state.arrays > MAX_ARRAY_OPERATORS)
|
|
20927
|
+
throw new PolicySyntaxError(
|
|
20928
|
+
`an expression may use @>, <@ and && at most ${String(MAX_ARRAY_OPERATORS)} times`,
|
|
20929
|
+
opToken.position
|
|
20930
|
+
);
|
|
20931
|
+
stream.expectKeyword("array");
|
|
20932
|
+
stream.expectOperator("[");
|
|
20933
|
+
const values = [];
|
|
20934
|
+
do {
|
|
20935
|
+
const item = stream.peek();
|
|
20936
|
+
if (values.length >= MAX_POLICY_ARRAY_VALUES)
|
|
20937
|
+
throw new PolicySyntaxError(
|
|
20938
|
+
`ARRAY[\u2026] lists at most ${String(MAX_POLICY_ARRAY_VALUES)} values`,
|
|
20939
|
+
item.position
|
|
20940
|
+
);
|
|
20941
|
+
const value = readValue(state);
|
|
20942
|
+
if (value.kind === "auth" && value.fn === "uid") values.push(value);
|
|
20943
|
+
else if (value.kind === "literal" && (typeof value.value === "string" || typeof value.value === "number"))
|
|
20944
|
+
values.push(value.value);
|
|
20945
|
+
else
|
|
20946
|
+
throw new PolicySyntaxError(
|
|
20947
|
+
"ARRAY[\u2026] holds strings, numbers and auth.uid()",
|
|
20948
|
+
item.position
|
|
20949
|
+
);
|
|
20950
|
+
} while (stream.acceptOperator(","));
|
|
20951
|
+
stream.expectOperator("]");
|
|
20952
|
+
return {
|
|
20953
|
+
kind: "array",
|
|
20954
|
+
op: opToken.value,
|
|
20955
|
+
operand: left,
|
|
20956
|
+
values
|
|
20957
|
+
};
|
|
20958
|
+
}
|
|
20959
|
+
function readSubquery(state, start, kind) {
|
|
20960
|
+
const { stream } = state;
|
|
20961
|
+
if (++state.subqueries > MAX_SUBQUERIES)
|
|
20962
|
+
throw new PolicySyntaxError(
|
|
20963
|
+
`an expression may have at most ${String(MAX_SUBQUERIES)} subqueries`,
|
|
20964
|
+
start.position
|
|
20965
|
+
);
|
|
20966
|
+
if (++state.subqueryDepth > MAX_SUBQUERY_DEPTH)
|
|
20967
|
+
throw new PolicySyntaxError(
|
|
20968
|
+
`subqueries nest at most ${String(MAX_SUBQUERY_DEPTH)} deep`,
|
|
20969
|
+
start.position
|
|
20970
|
+
);
|
|
20971
|
+
try {
|
|
20972
|
+
stream.expectKeyword("select");
|
|
20973
|
+
let select;
|
|
20974
|
+
const item = stream.peek();
|
|
20975
|
+
if (stream.acceptOperator("*")) {
|
|
20976
|
+
if (kind === "in")
|
|
20977
|
+
throw new PolicySyntaxError(
|
|
20978
|
+
"IN (SELECT \u2026) selects one value, not *",
|
|
20979
|
+
item.position
|
|
20980
|
+
);
|
|
20981
|
+
select = "*";
|
|
20982
|
+
} else select = requireValue(readValue(state), item);
|
|
20983
|
+
if (isOperator(stream.peek(), ","))
|
|
20984
|
+
throw new PolicySyntaxError(
|
|
20985
|
+
"a subquery here selects one value",
|
|
20986
|
+
stream.peek().position
|
|
20987
|
+
);
|
|
20988
|
+
stream.expectKeyword(`from`);
|
|
20989
|
+
const table = readTableName(stream);
|
|
20990
|
+
let alias = null;
|
|
20991
|
+
if (stream.acceptKeyword("as")) alias = readIdentifier(stream, "an alias");
|
|
20992
|
+
else {
|
|
20993
|
+
const next = stream.peek();
|
|
20994
|
+
if (next.kind === "ident" && (next.quoted || !isReservedWord(next)))
|
|
20995
|
+
alias = readIdentifier(stream, "an alias");
|
|
20996
|
+
}
|
|
20997
|
+
let where = null;
|
|
20998
|
+
if (stream.acceptKeyword("where")) {
|
|
20999
|
+
const token = stream.peek();
|
|
21000
|
+
where = requireCondition(
|
|
21001
|
+
nested(state, token, () => readOr(state)),
|
|
21002
|
+
token
|
|
21003
|
+
);
|
|
21004
|
+
}
|
|
21005
|
+
const after = stream.peek();
|
|
21006
|
+
for (const word of ["join", "group", "order", "limit", "union", "having"])
|
|
21007
|
+
if (isKeyword(after, word) || isOperator(after, ","))
|
|
21008
|
+
throw new PolicySyntaxError(
|
|
21009
|
+
"a subquery in a policy reads one table: SELECT \u2026 FROM t [alias] [WHERE \u2026]",
|
|
21010
|
+
after.position
|
|
21011
|
+
);
|
|
21012
|
+
return { table, alias, select, where };
|
|
21013
|
+
} finally {
|
|
21014
|
+
state.subqueryDepth--;
|
|
21015
|
+
}
|
|
21016
|
+
}
|
|
21017
|
+
function isReservedWord(token) {
|
|
21018
|
+
return token.kind === "ident" && !token.quoted && (RESERVED.has(asciiLower(token.value)) || CLAUSE_WORDS.has(asciiLower(token.value)));
|
|
21019
|
+
}
|
|
21020
|
+
function readIdentifier(stream, what) {
|
|
21021
|
+
const token = stream.peek();
|
|
21022
|
+
if (token.kind !== "ident" || !token.quoted && isReservedWord(token))
|
|
21023
|
+
throw new PolicySyntaxError(
|
|
21024
|
+
`expected ${what}, found ${token.kind === "eof" ? "end of input" : JSON.stringify(token.value)}`,
|
|
21025
|
+
token.position
|
|
21026
|
+
);
|
|
21027
|
+
stream.next();
|
|
21028
|
+
return token.value;
|
|
21029
|
+
}
|
|
21030
|
+
function readTableName(stream) {
|
|
21031
|
+
const first = stream.peek();
|
|
21032
|
+
const name = readIdentifier(stream, "a table name");
|
|
21033
|
+
if (!stream.acceptOperator(".")) return name;
|
|
21034
|
+
if (first.quoted ? name !== "public" : asciiLower(name) !== "public")
|
|
21035
|
+
throw new PolicySyntaxError(
|
|
21036
|
+
`schema ${name} does not exist: naro's tables are in public`,
|
|
21037
|
+
first.position
|
|
21038
|
+
);
|
|
21039
|
+
return readIdentifier(stream, "a table name");
|
|
21040
|
+
}
|
|
21041
|
+
function readNumber(token, negative) {
|
|
21042
|
+
const value = Number(token.value) * (negative ? -1 : 1);
|
|
21043
|
+
if (!Number.isFinite(value))
|
|
21044
|
+
throw new PolicySyntaxError(
|
|
21045
|
+
`${token.value} is out of range`,
|
|
21046
|
+
token.position
|
|
21047
|
+
);
|
|
21048
|
+
if (/[.eE]/.test(token.value)) return { kind: "literal", value };
|
|
21049
|
+
if (!Number.isSafeInteger(value))
|
|
21050
|
+
throw new PolicySyntaxError(
|
|
21051
|
+
`${token.value} is out of range: integers are at most 9007199254740991`,
|
|
21052
|
+
token.position
|
|
21053
|
+
);
|
|
21054
|
+
return { kind: "literal", value: value === 0 ? 0 : value };
|
|
21055
|
+
}
|
|
21056
|
+
function readValue(state) {
|
|
21057
|
+
const { stream } = state;
|
|
21058
|
+
const token = stream.peek();
|
|
21059
|
+
count(state, token);
|
|
21060
|
+
let value = nested(state, token, () => readPrimary(state));
|
|
21061
|
+
while (isOperator(stream.peek(), "::")) {
|
|
21062
|
+
const cast = stream.next();
|
|
21063
|
+
count(state, cast);
|
|
21064
|
+
const type = stream.peek();
|
|
21065
|
+
if (type.kind !== "ident" || type.quoted || !TEXT_TYPES.has(asciiLower(type.value)))
|
|
21066
|
+
throw new PolicySyntaxError(
|
|
21067
|
+
"casts are ::text, ::varchar and ::uuid (all read as text)",
|
|
21068
|
+
type.position
|
|
21069
|
+
);
|
|
21070
|
+
stream.next();
|
|
21071
|
+
value = { kind: "cast", operand: requireValue(value, token) };
|
|
21072
|
+
}
|
|
21073
|
+
return value;
|
|
21074
|
+
}
|
|
21075
|
+
function readAuth(state, start, allowBareJwt) {
|
|
21076
|
+
const { stream } = state;
|
|
21077
|
+
const name = stream.next();
|
|
21078
|
+
const fn = name.kind === "ident" ? asciiLower(name.value) : "";
|
|
21079
|
+
if (!isOperator(stream.peek(), "(")) {
|
|
21080
|
+
if (fn === "uid" || fn === "role" || fn === "jwt")
|
|
21081
|
+
throw new PolicySyntaxError(
|
|
21082
|
+
`auth.${fn} needs parentheses: auth.${fn}()`,
|
|
21083
|
+
start.position
|
|
21084
|
+
);
|
|
21085
|
+
throw new PolicySyntaxError(
|
|
21086
|
+
`auth.${name.value} is not available: use auth.uid(), auth.role() or auth.jwt()`,
|
|
21087
|
+
start.position
|
|
21088
|
+
);
|
|
21089
|
+
}
|
|
21090
|
+
if (fn !== "uid" && fn !== "role" && fn !== "jwt")
|
|
21091
|
+
throw new PolicySyntaxError(
|
|
21092
|
+
`auth.${name.value}() is not available: use auth.uid(), auth.role() or auth.jwt()`,
|
|
21093
|
+
start.position
|
|
21094
|
+
);
|
|
21095
|
+
stream.expectOperator("(");
|
|
21096
|
+
stream.expectOperator(")");
|
|
21097
|
+
if (fn !== "jwt") return { kind: "auth", fn };
|
|
21098
|
+
const access9 = readJwtAccess(stream);
|
|
21099
|
+
if (access9) return access9;
|
|
21100
|
+
if (allowBareJwt) return "jwt";
|
|
21101
|
+
throw new PolicySyntaxError(
|
|
21102
|
+
"auth.jwt() is read one claim at a time: auth.jwt() ->> 'email'",
|
|
21103
|
+
start.position
|
|
21104
|
+
);
|
|
21105
|
+
}
|
|
21106
|
+
function readJwtAccess(stream) {
|
|
21107
|
+
const arrow = stream.peek();
|
|
21108
|
+
if (!isOperator(arrow, "->") && !isOperator(arrow, "->>")) return void 0;
|
|
21109
|
+
stream.next();
|
|
21110
|
+
const claim = stream.peek();
|
|
21111
|
+
if (claim.kind !== "string")
|
|
21112
|
+
throw new PolicySyntaxError(
|
|
21113
|
+
"a claim is a '\u2026' string: auth.jwt() ->> 'email'",
|
|
21114
|
+
claim.position
|
|
21115
|
+
);
|
|
21116
|
+
stream.next();
|
|
21117
|
+
if (!JWT_CLAIMS.includes(claim.value))
|
|
21118
|
+
throw new PolicySyntaxError(
|
|
21119
|
+
`auth.jwt() has ${JWT_CLAIMS.slice(0, -1).join(", ")} and ${String(JWT_CLAIMS.at(-1))}; ${JSON.stringify(claim.value)} is not one of them`,
|
|
21120
|
+
claim.position
|
|
21121
|
+
);
|
|
21122
|
+
const next = stream.peek();
|
|
21123
|
+
if (isOperator(next, "->") || isOperator(next, "->>"))
|
|
21124
|
+
throw new PolicySyntaxError(
|
|
21125
|
+
"naro's auth.jwt() claims are not nested",
|
|
21126
|
+
next.position
|
|
21127
|
+
);
|
|
21128
|
+
return {
|
|
21129
|
+
kind: "jwt",
|
|
21130
|
+
op: arrow.value,
|
|
21131
|
+
claim: claim.value
|
|
21132
|
+
};
|
|
21133
|
+
}
|
|
21134
|
+
function readPrimary(state) {
|
|
21135
|
+
const { stream } = state;
|
|
21136
|
+
const token = stream.next();
|
|
21137
|
+
switch (token.kind) {
|
|
21138
|
+
case "string":
|
|
21139
|
+
return { kind: "literal", value: token.value };
|
|
21140
|
+
case "number":
|
|
21141
|
+
return readNumber(token, false);
|
|
21142
|
+
case "op": {
|
|
21143
|
+
if (token.value === "-") {
|
|
21144
|
+
const number4 = stream.peek();
|
|
21145
|
+
if (number4.kind !== "number")
|
|
21146
|
+
throw new PolicySyntaxError(
|
|
21147
|
+
"only a number can be negated",
|
|
21148
|
+
token.position
|
|
21149
|
+
);
|
|
21150
|
+
stream.next();
|
|
21151
|
+
return readNumber(number4, true);
|
|
21152
|
+
}
|
|
21153
|
+
if (token.value === "(") {
|
|
21154
|
+
if (isKeyword(stream.peek(), "select")) {
|
|
21155
|
+
const select = stream.next();
|
|
21156
|
+
const auth = stream.peek();
|
|
21157
|
+
if (!isKeyword(auth, "auth") || !isOperator(stream.peek(1), "."))
|
|
21158
|
+
throw new PolicySyntaxError(
|
|
21159
|
+
"a scalar subquery here must be (select auth.uid()), (select auth.role()) or (select auth.jwt() ->> '\u2026'): read other tables with EXISTS (\u2026) or IN (SELECT \u2026)",
|
|
21160
|
+
select.position
|
|
21161
|
+
);
|
|
21162
|
+
stream.next();
|
|
21163
|
+
stream.next();
|
|
21164
|
+
const inner2 = readAuth(state, auth, true);
|
|
21165
|
+
stream.expectOperator(")");
|
|
21166
|
+
if (inner2 !== "jwt") return inner2;
|
|
21167
|
+
const access9 = readJwtAccess(stream);
|
|
21168
|
+
if (!access9)
|
|
21169
|
+
throw new PolicySyntaxError(
|
|
21170
|
+
"auth.jwt() is read one claim at a time: (select auth.jwt()) ->> 'email'",
|
|
21171
|
+
auth.position
|
|
21172
|
+
);
|
|
21173
|
+
return access9;
|
|
21174
|
+
}
|
|
21175
|
+
const inner = readOr(state);
|
|
21176
|
+
stream.expectOperator(")");
|
|
21177
|
+
return inner;
|
|
21178
|
+
}
|
|
21179
|
+
return stream.unexpected(token);
|
|
21180
|
+
}
|
|
21181
|
+
case "ident": {
|
|
21182
|
+
if (!token.quoted) {
|
|
21183
|
+
const word = asciiLower(token.value);
|
|
21184
|
+
if (word === "true") return { kind: "literal", value: true };
|
|
21185
|
+
if (word === "false") return { kind: "literal", value: false };
|
|
21186
|
+
if (word === "null") return { kind: "literal", value: null };
|
|
21187
|
+
if (word === "current_timestamp")
|
|
21188
|
+
return { kind: "func", name: "now", args: [] };
|
|
21189
|
+
if (word === "auth" && isOperator(stream.peek(), ".")) {
|
|
21190
|
+
stream.next();
|
|
21191
|
+
const auth = readAuth(state, token, false);
|
|
21192
|
+
if (auth === "jwt") return stream.unexpected();
|
|
21193
|
+
return auth;
|
|
21194
|
+
}
|
|
21195
|
+
if (isOperator(stream.peek(), "(")) return readCall(state, token);
|
|
21196
|
+
if (RESERVED.has(word)) return stream.unexpected(token);
|
|
21197
|
+
}
|
|
21198
|
+
if (isOperator(stream.peek(), ".")) {
|
|
21199
|
+
stream.next();
|
|
21200
|
+
const name = stream.peek();
|
|
21201
|
+
if (name.kind !== "ident" || !name.quoted && isReservedWord(name))
|
|
21202
|
+
return stream.unexpected(name);
|
|
21203
|
+
stream.next();
|
|
21204
|
+
if (isOperator(stream.peek(), "(") || isOperator(stream.peek(), "."))
|
|
21205
|
+
throw new PolicySyntaxError(
|
|
21206
|
+
`${token.value}.${name.value} is not a column: naro reads column or table.column`,
|
|
21207
|
+
token.position
|
|
21208
|
+
);
|
|
21209
|
+
return { kind: "column", qualifier: token.value, name: name.value };
|
|
21210
|
+
}
|
|
21211
|
+
return { kind: "column", qualifier: null, name: token.value };
|
|
21212
|
+
}
|
|
21213
|
+
default:
|
|
21214
|
+
return stream.unexpected(token);
|
|
21215
|
+
}
|
|
21216
|
+
}
|
|
21217
|
+
function readCall(state, name) {
|
|
21218
|
+
const { stream } = state;
|
|
21219
|
+
const word = asciiLower(name.value);
|
|
21220
|
+
if (!POLICY_FUNCTIONS.includes(word))
|
|
21221
|
+
throw new PolicySyntaxError(
|
|
21222
|
+
`function ${name.value} is not available in policies: use lower, upper, coalesce, length or now`,
|
|
21223
|
+
name.position
|
|
21224
|
+
);
|
|
21225
|
+
stream.expectOperator("(");
|
|
21226
|
+
const args = [];
|
|
21227
|
+
if (!isOperator(stream.peek(), ")"))
|
|
21228
|
+
do {
|
|
21229
|
+
const item = stream.peek();
|
|
21230
|
+
args.push(requireValue(readValue(state), item));
|
|
21231
|
+
} while (stream.acceptOperator(","));
|
|
21232
|
+
stream.expectOperator(")");
|
|
21233
|
+
const arity = {
|
|
21234
|
+
lower: [1, 1],
|
|
21235
|
+
upper: [1, 1],
|
|
21236
|
+
length: [1, 1],
|
|
21237
|
+
coalesce: [1, 16],
|
|
21238
|
+
now: [0, 0]
|
|
21239
|
+
};
|
|
21240
|
+
const [min, max] = arity[word];
|
|
21241
|
+
if (args.length < min || args.length > max)
|
|
21242
|
+
throw new PolicySyntaxError(
|
|
21243
|
+
min === max ? `${word}() takes ${String(min)} argument${min === 1 ? "" : "s"}` : `${word}() takes ${String(min)} to ${String(max)} arguments`,
|
|
21244
|
+
name.position
|
|
21245
|
+
);
|
|
21246
|
+
return { kind: "func", name: word, args };
|
|
21247
|
+
}
|
|
21248
|
+
function newState(stream) {
|
|
21249
|
+
return {
|
|
21250
|
+
stream,
|
|
21251
|
+
nodes: 0,
|
|
21252
|
+
depth: 0,
|
|
21253
|
+
subqueries: 0,
|
|
21254
|
+
subqueryDepth: 0,
|
|
21255
|
+
arrays: 0
|
|
21256
|
+
};
|
|
21257
|
+
}
|
|
21258
|
+
function readCondition(stream) {
|
|
21259
|
+
const token = stream.peek();
|
|
21260
|
+
const expr = readOr(newState(stream));
|
|
21261
|
+
return requireCondition(expr, token);
|
|
21262
|
+
}
|
|
21263
|
+
function checkExpressionSize(expr, position) {
|
|
21264
|
+
const text = printExpr(expr);
|
|
21265
|
+
if (utf8ByteLength(text) > MAX_EXPRESSION_BYTES)
|
|
21266
|
+
throw new PolicySyntaxError(
|
|
21267
|
+
`the expression is longer than ${String(MAX_EXPRESSION_BYTES)} bytes`,
|
|
21268
|
+
position
|
|
21269
|
+
);
|
|
21270
|
+
return text;
|
|
21271
|
+
}
|
|
21272
|
+
function parseExpression(text) {
|
|
21273
|
+
try {
|
|
21274
|
+
const stream = new TokenStream(tokenize(text));
|
|
21275
|
+
if (stream.peek().kind === "eof")
|
|
21276
|
+
throw new PolicySyntaxError("the expression is empty", 1);
|
|
21277
|
+
const expr = readCondition(stream);
|
|
21278
|
+
if (stream.peek().kind !== "eof") stream.unexpected();
|
|
21279
|
+
return { ok: true, expr, text: checkExpressionSize(expr, 1) };
|
|
21280
|
+
} catch (error51) {
|
|
21281
|
+
if (error51 instanceof PolicySyntaxError)
|
|
21282
|
+
return { ok: false, reason: error51.reason, position: error51.position };
|
|
21283
|
+
throw error51;
|
|
21284
|
+
}
|
|
21285
|
+
}
|
|
21286
|
+
function quoteIdent(name) {
|
|
21287
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !SPECIAL_WORDS.has(asciiLower(name)) ? name : `"${name.replaceAll('"', '""')}"`;
|
|
21288
|
+
}
|
|
21289
|
+
function commentText(text) {
|
|
21290
|
+
let out = "";
|
|
21291
|
+
for (const char of text) {
|
|
21292
|
+
const code = char.charCodeAt(0);
|
|
21293
|
+
const control = code < 32 || code >= 127 && code <= 159 || code === 8232 || code === 8233;
|
|
21294
|
+
out += control ? `\\u${code.toString(16).padStart(4, "0")}` : char;
|
|
21295
|
+
}
|
|
21296
|
+
return out;
|
|
21297
|
+
}
|
|
21298
|
+
function quoteString(text) {
|
|
21299
|
+
return `'${text.replaceAll("'", "''")}'`;
|
|
21300
|
+
}
|
|
21301
|
+
function printNumber(value) {
|
|
21302
|
+
if (Object.is(value, -0)) return "-0.0";
|
|
21303
|
+
const text = String(value);
|
|
21304
|
+
return Number.isSafeInteger(value) || /[.e]/.test(text) ? text : `${text}.0`;
|
|
21305
|
+
}
|
|
21306
|
+
function printLiteral(value) {
|
|
21307
|
+
if (value === null) return "NULL";
|
|
21308
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
21309
|
+
if (typeof value === "number") return printNumber(value);
|
|
21310
|
+
return quoteString(value);
|
|
21311
|
+
}
|
|
21312
|
+
function printValue(expr) {
|
|
21313
|
+
switch (expr.kind) {
|
|
21314
|
+
case "literal":
|
|
21315
|
+
return printLiteral(expr.value);
|
|
21316
|
+
case "column":
|
|
21317
|
+
return expr.qualifier === null ? quoteIdent(expr.name) : `${quoteIdent(expr.qualifier)}.${quoteIdent(expr.name)}`;
|
|
21318
|
+
case "auth":
|
|
21319
|
+
return `auth.${expr.fn}()`;
|
|
21320
|
+
case "jwt":
|
|
21321
|
+
return `(auth.jwt() ${expr.op} ${quoteString(expr.claim)})`;
|
|
21322
|
+
case "func":
|
|
21323
|
+
return expr.name === "now" ? "now()" : `${expr.name}(${expr.args.map(printValue).join(", ")})`;
|
|
21324
|
+
case "cast":
|
|
21325
|
+
return `(${printValue(expr.operand)})::text`;
|
|
21326
|
+
}
|
|
21327
|
+
}
|
|
21328
|
+
function printSubquery(subquery) {
|
|
21329
|
+
const select = subquery.select === "*" ? "*" : printValue(subquery.select);
|
|
21330
|
+
const alias = subquery.alias === null ? "" : ` ${quoteIdent(subquery.alias)}`;
|
|
21331
|
+
const where = subquery.where === null ? "" : ` WHERE ${printExpr(subquery.where)}`;
|
|
21332
|
+
return `SELECT ${select} FROM ${quoteIdent(subquery.table)}${alias}${where}`;
|
|
21333
|
+
}
|
|
21334
|
+
function printElement(element) {
|
|
21335
|
+
if (typeof element === "object") return printValue(element);
|
|
21336
|
+
return typeof element === "number" ? printNumber(element) : quoteString(element);
|
|
21337
|
+
}
|
|
21338
|
+
function printExpr(expr) {
|
|
21339
|
+
if (isValueExpr(expr)) return printValue(expr);
|
|
21340
|
+
switch (expr.kind) {
|
|
21341
|
+
case "and":
|
|
21342
|
+
case "or":
|
|
21343
|
+
return `(${expr.args.map(printExpr).join(expr.kind === "and" ? " AND " : " OR ")})`;
|
|
21344
|
+
case "not":
|
|
21345
|
+
return `(NOT ${printExpr(expr.arg)})`;
|
|
21346
|
+
case "compare":
|
|
21347
|
+
return `(${printValue(expr.left)} ${expr.op} ${printValue(expr.right)})`;
|
|
21348
|
+
case "distinct":
|
|
21349
|
+
return `(${printValue(expr.left)} IS ${expr.not ? "NOT " : ""}DISTINCT FROM ${printValue(expr.right)})`;
|
|
21350
|
+
case "is":
|
|
21351
|
+
return `(${printValue(expr.operand)} IS ${expr.not ? "NOT " : ""}${printLiteral(expr.value)})`;
|
|
21352
|
+
case "in":
|
|
21353
|
+
return `(${printValue(expr.operand)} ${expr.not ? "NOT " : ""}IN (${expr.list.map(printValue).join(", ")}))`;
|
|
21354
|
+
case "inSelect":
|
|
21355
|
+
return `(${printValue(expr.operand)} ${expr.not ? "NOT " : ""}IN (${printSubquery(expr.subquery)}))`;
|
|
21356
|
+
case "like":
|
|
21357
|
+
return `(${printValue(expr.operand)} ${expr.not ? "NOT " : ""}${expr.ci ? "ILIKE" : "LIKE"} ${quoteString(expr.pattern)})`;
|
|
21358
|
+
case "between":
|
|
21359
|
+
return `(${printValue(expr.operand)} ${expr.not ? "NOT " : ""}BETWEEN ${printValue(expr.low)} AND ${printValue(expr.high)})`;
|
|
21360
|
+
case "array":
|
|
21361
|
+
return `(${printValue(expr.operand)} ${expr.op} ARRAY[${expr.values.map(printElement).join(", ")}])`;
|
|
21362
|
+
case "exists":
|
|
21363
|
+
return `(EXISTS (${printSubquery(expr.subquery)}))`;
|
|
21364
|
+
}
|
|
21365
|
+
}
|
|
21366
|
+
function unqualifiedInSubquery(expr) {
|
|
21367
|
+
let found;
|
|
21368
|
+
function value(node, inside) {
|
|
21369
|
+
if (found !== void 0) return;
|
|
21370
|
+
switch (node.kind) {
|
|
21371
|
+
case "column":
|
|
21372
|
+
if (inside && node.qualifier === null) found = node;
|
|
21373
|
+
return;
|
|
21374
|
+
case "func":
|
|
21375
|
+
for (const arg of node.args) value(arg, inside);
|
|
21376
|
+
return;
|
|
21377
|
+
case "cast":
|
|
21378
|
+
value(node.operand, inside);
|
|
21379
|
+
return;
|
|
21380
|
+
default:
|
|
21381
|
+
return;
|
|
21382
|
+
}
|
|
21383
|
+
}
|
|
21384
|
+
function subquery(node) {
|
|
21385
|
+
if (node.select !== "*") value(node.select, true);
|
|
21386
|
+
if (node.where !== null) visit(node.where, true);
|
|
21387
|
+
}
|
|
21388
|
+
function visit(node, inside) {
|
|
21389
|
+
if (found !== void 0) return;
|
|
21390
|
+
if (isValueExpr(node)) {
|
|
21391
|
+
value(node, inside);
|
|
21392
|
+
return;
|
|
21393
|
+
}
|
|
21394
|
+
switch (node.kind) {
|
|
21395
|
+
case "and":
|
|
21396
|
+
case "or":
|
|
21397
|
+
for (const arg of node.args) visit(arg, inside);
|
|
21398
|
+
return;
|
|
21399
|
+
case "not":
|
|
21400
|
+
visit(node.arg, inside);
|
|
21401
|
+
return;
|
|
21402
|
+
case "compare":
|
|
21403
|
+
case "distinct":
|
|
21404
|
+
value(node.left, inside);
|
|
21405
|
+
value(node.right, inside);
|
|
21406
|
+
return;
|
|
21407
|
+
case "is":
|
|
21408
|
+
case "like":
|
|
21409
|
+
case "array":
|
|
21410
|
+
value(node.operand, inside);
|
|
21411
|
+
return;
|
|
21412
|
+
case "in":
|
|
21413
|
+
value(node.operand, inside);
|
|
21414
|
+
for (const item of node.list) value(item, inside);
|
|
21415
|
+
return;
|
|
21416
|
+
case "between":
|
|
21417
|
+
for (const item of [node.operand, node.low, node.high])
|
|
21418
|
+
value(item, inside);
|
|
21419
|
+
return;
|
|
21420
|
+
case "inSelect":
|
|
21421
|
+
value(node.operand, inside);
|
|
21422
|
+
subquery(node.subquery);
|
|
21423
|
+
return;
|
|
21424
|
+
case "exists":
|
|
21425
|
+
subquery(node.subquery);
|
|
21426
|
+
return;
|
|
21427
|
+
}
|
|
21428
|
+
}
|
|
21429
|
+
visit(expr, false);
|
|
21430
|
+
return found;
|
|
21431
|
+
}
|
|
21432
|
+
function unqualifiedProblem(ref) {
|
|
21433
|
+
const name = quoteIdent(ref.name);
|
|
21434
|
+
return `column ${name} has no table or alias in front of it inside a subquery: naro policy create writes one on every column there, so that a column added to a table later cannot change which table the policy reads it from \u2014 drop the policy and create it again`;
|
|
21435
|
+
}
|
|
21436
|
+
|
|
21437
|
+
// ../../packages/data-core/src/named-policy.ts
|
|
21438
|
+
var POLICY_COMMANDS = [
|
|
21439
|
+
"all",
|
|
21440
|
+
"select",
|
|
21441
|
+
"insert",
|
|
21442
|
+
"update",
|
|
21443
|
+
"delete"
|
|
21444
|
+
];
|
|
21445
|
+
var POLICY_ROLES = ["anon", "authenticated"];
|
|
21446
|
+
var MAX_POLICY_NAME_LENGTH = 63;
|
|
21447
|
+
var MAX_POLICY_SQL_BYTES = 16 * 1024;
|
|
21448
|
+
function rolesText(roles) {
|
|
21449
|
+
const set2 = new Set(roles);
|
|
21450
|
+
if (set2.has("anon") && set2.has("authenticated")) return "public";
|
|
21451
|
+
return set2.has("anon") ? "anon" : "authenticated";
|
|
21452
|
+
}
|
|
21453
|
+
function readRoles(names) {
|
|
21454
|
+
const set2 = /* @__PURE__ */ new Set();
|
|
21455
|
+
if (names.length === 0)
|
|
21456
|
+
return { ok: false, reason: "Roles are public, anon and authenticated." };
|
|
21457
|
+
for (const name of names) {
|
|
21458
|
+
if (name === "public") {
|
|
21459
|
+
set2.add("anon");
|
|
21460
|
+
set2.add("authenticated");
|
|
21461
|
+
} else if (name === "anon" || name === "authenticated") set2.add(name);
|
|
21462
|
+
else
|
|
21463
|
+
return {
|
|
21464
|
+
ok: false,
|
|
21465
|
+
reason: name === "service_role" ? "Roles are public, anon and authenticated: the service key skips every policy." : "Roles are public, anon and authenticated."
|
|
21466
|
+
};
|
|
21467
|
+
}
|
|
21468
|
+
return { ok: true, roles: POLICY_ROLES.filter((role) => set2.has(role)) };
|
|
21469
|
+
}
|
|
21470
|
+
function quotePolicyName(name) {
|
|
21471
|
+
return /^[a-z_][a-z0-9_]*$/.test(name) && quoteIdent(name) === name ? name : `"${name.replaceAll('"', '""')}"`;
|
|
21472
|
+
}
|
|
21473
|
+
function policyShapeProblem(policy) {
|
|
21474
|
+
const { name, command, using, check: check2 } = policy;
|
|
21475
|
+
if (name === "" || [...name].length > MAX_POLICY_NAME_LENGTH || name.includes("\0"))
|
|
21476
|
+
return `Policy names are 1 to ${String(MAX_POLICY_NAME_LENGTH)} characters.`;
|
|
21477
|
+
const upper = command.toUpperCase();
|
|
21478
|
+
if ((command === "select" || command === "delete") && check2 !== null)
|
|
21479
|
+
return "WITH CHECK cannot be applied to SELECT or DELETE.";
|
|
21480
|
+
if (command === "insert" && using !== null)
|
|
21481
|
+
return "Only WITH CHECK expression allowed for INSERT.";
|
|
21482
|
+
if ((command === "select" || command === "delete") && using === null)
|
|
21483
|
+
return `A ${upper} policy needs USING (\u2026).`;
|
|
21484
|
+
if (command === "insert" && check2 === null)
|
|
21485
|
+
return "An INSERT policy needs WITH CHECK (\u2026).";
|
|
21486
|
+
if (using === null && check2 === null)
|
|
21487
|
+
return `${command === "all" ? "An ALL" : "An UPDATE"} policy needs USING (\u2026), WITH CHECK (\u2026) or both.`;
|
|
21488
|
+
return void 0;
|
|
21489
|
+
}
|
|
21490
|
+
function ownerExpression(column) {
|
|
21491
|
+
return printExpr({
|
|
21492
|
+
kind: "compare",
|
|
21493
|
+
op: "=",
|
|
21494
|
+
left: { kind: "column", qualifier: null, name: column },
|
|
21495
|
+
right: { kind: "auth", fn: "uid" }
|
|
21496
|
+
});
|
|
21497
|
+
}
|
|
21498
|
+
function ownerClauses(column, command) {
|
|
21499
|
+
const text = ownerExpression(column);
|
|
21500
|
+
return command === "insert" ? { using: null, check: text } : { using: text, check: null };
|
|
21501
|
+
}
|
|
21502
|
+
var NAMED_POLICY_TABLE = "_naro_policies_v2";
|
|
21503
|
+
var NAMED_POLICY_TABLE_SQL = `CREATE TABLE IF NOT EXISTS ${NAMED_POLICY_TABLE} (table_name TEXT NOT NULL, name TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'permissive', command TEXT NOT NULL DEFAULT 'all', roles TEXT NOT NULL DEFAULT 'public', using_expr TEXT, check_expr TEXT, owner_column TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (table_name, name))`;
|
|
21504
|
+
var NAMED_POLICY_SQL = {
|
|
21505
|
+
list: `SELECT table_name, name, kind, command, roles, using_expr, check_expr, owner_column FROM ${NAMED_POLICY_TABLE} ORDER BY table_name, name`,
|
|
21506
|
+
insert: `INSERT INTO ${NAMED_POLICY_TABLE} (table_name, name, kind, command, roles, using_expr, check_expr, owner_column) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
21507
|
+
replace: `UPDATE ${NAMED_POLICY_TABLE} SET table_name = ?, name = ?, kind = ?, command = ?, roles = ?, using_expr = ?, check_expr = ?, owner_column = ?, updated_at = datetime('now') WHERE table_name = ? AND name = ?`,
|
|
21508
|
+
remove: `DELETE FROM ${NAMED_POLICY_TABLE} WHERE table_name = ? COLLATE NOCASE AND name = ?`,
|
|
21509
|
+
removeTable: `DELETE FROM ${NAMED_POLICY_TABLE} WHERE table_name = ? COLLATE NOCASE`
|
|
21510
|
+
};
|
|
21511
|
+
function policyValues(policy) {
|
|
21512
|
+
return [
|
|
21513
|
+
policy.table,
|
|
21514
|
+
policy.name,
|
|
21515
|
+
policy.permissive ? "permissive" : "restrictive",
|
|
21516
|
+
policy.command,
|
|
21517
|
+
rolesText(policy.roles),
|
|
21518
|
+
policy.using,
|
|
21519
|
+
policy.check,
|
|
21520
|
+
policy.owner
|
|
21521
|
+
];
|
|
21522
|
+
}
|
|
21523
|
+
function storedExpression(value, field) {
|
|
21524
|
+
if (value === null || value === void 0) return { expr: null, text: null };
|
|
21525
|
+
if (typeof value !== "string")
|
|
21526
|
+
return { expr: null, text: null, problem: `${field} is not text` };
|
|
21527
|
+
const parsed = parseExpression(value);
|
|
21528
|
+
if (!parsed.ok)
|
|
21529
|
+
return {
|
|
21530
|
+
expr: null,
|
|
21531
|
+
text: value,
|
|
21532
|
+
problem: `${field}: ${parsed.reason} at character ${String(parsed.position)}`
|
|
21533
|
+
};
|
|
21534
|
+
const unbound = unqualifiedInSubquery(parsed.expr);
|
|
21535
|
+
if (unbound !== void 0)
|
|
21536
|
+
return {
|
|
21537
|
+
expr: null,
|
|
21538
|
+
text: value,
|
|
21539
|
+
problem: `${field}: ${unqualifiedProblem(unbound)}`
|
|
21540
|
+
};
|
|
21541
|
+
return { expr: parsed.expr, text: parsed.text };
|
|
21542
|
+
}
|
|
21543
|
+
function loadPolicy(row, log2) {
|
|
21544
|
+
const table = row.table_name;
|
|
21545
|
+
if (typeof table !== "string" || table === "") return void 0;
|
|
21546
|
+
const problems = [];
|
|
21547
|
+
const name = typeof row.name === "string" && row.name !== "" ? row.name : "(unnamed)";
|
|
21548
|
+
if (name === "(unnamed)") problems.push("the name is missing");
|
|
21549
|
+
const kind = row.kind;
|
|
21550
|
+
if (kind !== "permissive" && kind !== "restrictive")
|
|
21551
|
+
problems.push("kind is not permissive or restrictive");
|
|
21552
|
+
const command = POLICY_COMMANDS.includes(row.command) ? row.command : void 0;
|
|
21553
|
+
if (!command)
|
|
21554
|
+
problems.push("command is not one of all, select, insert, update, delete");
|
|
21555
|
+
const roles = typeof row.roles === "string" && ["public", "anon", "authenticated"].includes(row.roles) ? readRoles([row.roles]) : void 0;
|
|
21556
|
+
if (!roles?.ok) problems.push("roles is not public, anon or authenticated");
|
|
21557
|
+
const using = storedExpression(row.using_expr, "using");
|
|
21558
|
+
const check2 = storedExpression(row.check_expr, "check");
|
|
21559
|
+
if (using.problem) problems.push(using.problem);
|
|
21560
|
+
if (check2.problem) problems.push(check2.problem);
|
|
21561
|
+
const owner = row.owner_column;
|
|
21562
|
+
const policy = {
|
|
21563
|
+
table,
|
|
21564
|
+
name,
|
|
21565
|
+
permissive: kind !== "restrictive",
|
|
21566
|
+
// What cannot be read applies everywhere: it refuses, never allows.
|
|
21567
|
+
command: command ?? "all",
|
|
21568
|
+
roles: roles?.ok ? roles.roles : [...POLICY_ROLES],
|
|
21569
|
+
using: using.text,
|
|
21570
|
+
check: check2.text,
|
|
21571
|
+
owner: typeof owner === "string" ? owner : null,
|
|
21572
|
+
invalid: null,
|
|
21573
|
+
usingExpr: using.expr,
|
|
21574
|
+
checkExpr: check2.expr
|
|
21575
|
+
};
|
|
21576
|
+
if (command && problems.length === 0) {
|
|
21577
|
+
const shape = policyShapeProblem(policy);
|
|
21578
|
+
if (shape) problems.push(shape);
|
|
21579
|
+
}
|
|
21580
|
+
if (policy.owner !== null && policy.owner !== "" && command && problems.length === 0) {
|
|
21581
|
+
const expected = ownerClauses(policy.owner, command);
|
|
21582
|
+
if (policy.using !== expected.using || policy.check !== expected.check)
|
|
21583
|
+
problems.push(
|
|
21584
|
+
"an owner policy's expressions are not its owner condition"
|
|
21585
|
+
);
|
|
21586
|
+
}
|
|
21587
|
+
if (problems.length > 0) {
|
|
21588
|
+
policy.invalid = problems.join("; ");
|
|
21589
|
+
log2?.(
|
|
21590
|
+
`naro data-core: policy ${name} on ${table} cannot be read: ${policy.invalid}`
|
|
21591
|
+
);
|
|
21592
|
+
}
|
|
21593
|
+
return policy;
|
|
21594
|
+
}
|
|
21595
|
+
function foldPolicyName(name, quoted) {
|
|
21596
|
+
return quoted ? name : asciiLower(name);
|
|
21597
|
+
}
|
|
21598
|
+
function policySqlTooLong(text) {
|
|
21599
|
+
return utf8ByteLength(text) > MAX_POLICY_SQL_BYTES;
|
|
21600
|
+
}
|
|
20007
21601
|
|
|
20008
21602
|
// ../../packages/data-core/src/query.ts
|
|
20009
21603
|
var FILTER_OPERATORS = [
|
|
@@ -20035,7 +21629,11 @@ var ARRAY_OPERATORS = [
|
|
|
20035
21629
|
"containedBy",
|
|
20036
21630
|
"overlaps"
|
|
20037
21631
|
];
|
|
20038
|
-
|
|
21632
|
+
function patternProblem(operator, pattern) {
|
|
21633
|
+
if (!patternTooLong(pattern, operator === "ilike")) return void 0;
|
|
21634
|
+
const limit = `must be at most ${String(MAX_PATTERN_BYTES)} bytes of UTF-8, D1's limit for a pattern`;
|
|
21635
|
+
return operator === "ilike" ? limit : `${limit} (like counts each *, ? and [ as 3 bytes)`;
|
|
21636
|
+
}
|
|
20039
21637
|
var MAX_IDENTIFIER_LENGTH = 128;
|
|
20040
21638
|
var Invalid = class extends Error {
|
|
20041
21639
|
constructor(path, reason) {
|
|
@@ -20050,7 +21648,7 @@ function invalid(path, reason) {
|
|
|
20050
21648
|
function own(object2, key) {
|
|
20051
21649
|
return Object.hasOwn(object2, key) ? object2[key] : void 0;
|
|
20052
21650
|
}
|
|
20053
|
-
function
|
|
21651
|
+
function isOperator2(value) {
|
|
20054
21652
|
return FILTER_OPERATORS.includes(value);
|
|
20055
21653
|
}
|
|
20056
21654
|
function readObject(value, path) {
|
|
@@ -20075,7 +21673,7 @@ function readName(value, path) {
|
|
|
20075
21673
|
);
|
|
20076
21674
|
return value;
|
|
20077
21675
|
}
|
|
20078
|
-
function
|
|
21676
|
+
function readValue2(value, path, auth = false) {
|
|
20079
21677
|
if (auth && isAuthUid(value)) return { auth: "uid" };
|
|
20080
21678
|
if (!isValue(value))
|
|
20081
21679
|
invalid(
|
|
@@ -20087,8 +21685,8 @@ function readValue(value, path, auth = false) {
|
|
|
20087
21685
|
function longerThan(text, max) {
|
|
20088
21686
|
if (text.length <= max) return false;
|
|
20089
21687
|
if (text.length > max * 2) return true;
|
|
20090
|
-
let
|
|
20091
|
-
for (const _ of text) if (++
|
|
21688
|
+
let count2 = 0;
|
|
21689
|
+
for (const _ of text) if (++count2 > max) return true;
|
|
20092
21690
|
return false;
|
|
20093
21691
|
}
|
|
20094
21692
|
function endsWithLoneEscape(pattern) {
|
|
@@ -20103,7 +21701,7 @@ function readFilter(value, path, auth) {
|
|
|
20103
21701
|
const object2 = readObject(value, path);
|
|
20104
21702
|
checkKeys(object2, ["operator", "column", "value"], path);
|
|
20105
21703
|
const operator = own(object2, "operator");
|
|
20106
|
-
if (!
|
|
21704
|
+
if (!isOperator2(operator))
|
|
20107
21705
|
invalid(
|
|
20108
21706
|
`${path}.operator`,
|
|
20109
21707
|
`must be one of ${FILTER_OPERATORS.join(", ")}`
|
|
@@ -20116,7 +21714,7 @@ function readFilter(value, path, auth) {
|
|
|
20116
21714
|
case "in": {
|
|
20117
21715
|
if (!Array.isArray(raw)) invalid(valuePath, "must be an array");
|
|
20118
21716
|
const values = raw.map(
|
|
20119
|
-
(item, index) =>
|
|
21717
|
+
(item, index) => readValue2(item, `${valuePath}[${index}]`, auth)
|
|
20120
21718
|
);
|
|
20121
21719
|
return { operator, column, value: values };
|
|
20122
21720
|
}
|
|
@@ -20125,18 +21723,16 @@ function readFilter(value, path, auth) {
|
|
|
20125
21723
|
invalid(valuePath, "must be null, true or false");
|
|
20126
21724
|
return { operator, column, value: raw };
|
|
20127
21725
|
case "like":
|
|
20128
|
-
case "ilike":
|
|
21726
|
+
case "ilike": {
|
|
20129
21727
|
if (typeof raw !== "string") invalid(valuePath, "must be a string");
|
|
20130
|
-
|
|
20131
|
-
|
|
20132
|
-
valuePath,
|
|
20133
|
-
`must be at most ${String(MAX_PATTERN_LENGTH)} characters`
|
|
20134
|
-
);
|
|
21728
|
+
const tooLong = patternProblem(operator, raw);
|
|
21729
|
+
if (tooLong !== void 0) invalid(valuePath, tooLong);
|
|
20135
21730
|
if (raw.includes("\0"))
|
|
20136
21731
|
invalid(valuePath, "must not contain a NUL character");
|
|
20137
21732
|
if (endsWithLoneEscape(raw))
|
|
20138
21733
|
invalid(valuePath, "must not end with an unescaped backslash");
|
|
20139
21734
|
return { operator, column, value: raw };
|
|
21735
|
+
}
|
|
20140
21736
|
case "contains":
|
|
20141
21737
|
case "containedBy":
|
|
20142
21738
|
case "overlaps":
|
|
@@ -20155,7 +21751,7 @@ function readFilter(value, path, auth) {
|
|
|
20155
21751
|
)
|
|
20156
21752
|
};
|
|
20157
21753
|
default:
|
|
20158
|
-
return { operator, column, value:
|
|
21754
|
+
return { operator, column, value: readValue2(raw, valuePath, auth) };
|
|
20159
21755
|
}
|
|
20160
21756
|
}
|
|
20161
21757
|
var NODE_KEYS = ["operator", "or", "and", "not"];
|
|
@@ -20201,18 +21797,579 @@ function parsePolicyNode(value, path) {
|
|
|
20201
21797
|
}
|
|
20202
21798
|
}
|
|
20203
21799
|
|
|
20204
|
-
// ../../packages/data-core/src/schema.ts
|
|
20205
|
-
var SCHEMA_SQL = [
|
|
20206
|
-
`SELECT t.name AS table_name, t.type AS table_type, c.cid AS cid, c.name AS column_name, c.type AS column_type, c."notnull" AS not_null, c.dflt_value AS default_value, c.pk AS pk, c.hidden AS hidden FROM pragma_table_list AS t JOIN pragma_table_xinfo(t.name, t.schema) AS c WHERE t.schema = 'main' AND t.type IN ('table', 'virtual') AND ${systemTablePredicate("t.name")} ORDER BY t.name, c.cid`,
|
|
20207
|
-
// Phase 2-2 (적대적 리뷰 반영): each table's CREATE TABLE text, where
|
|
20208
|
-
// alone a column's collation is written. A separate statement, so the
|
|
20209
|
-
// text comes once per table rather than once per column. D1 answers
|
|
20210
|
-
// `sqlite_master` reads (the database routes' `_tables.ts` reads it too).
|
|
20211
|
-
`SELECT name AS table_name, sql AS table_sql FROM sqlite_master WHERE type = 'table' AND ${systemTablePredicate("name")}`
|
|
20212
|
-
];
|
|
20213
|
-
|
|
20214
21800
|
// ../../packages/data-core/src/condition.ts
|
|
21801
|
+
var CONDITION_RULE = "condition";
|
|
20215
21802
|
var MAX_CONDITION_BYTES = 16 * 1024;
|
|
21803
|
+
var TREES = {
|
|
21804
|
+
select: ["using"],
|
|
21805
|
+
delete: ["using"],
|
|
21806
|
+
insert: ["check"],
|
|
21807
|
+
update: ["using", "check"]
|
|
21808
|
+
};
|
|
21809
|
+
function parseCondition(action, value) {
|
|
21810
|
+
if (!isPlainObject2(value))
|
|
21811
|
+
return { ok: false, path: "", reason: "must be an object" };
|
|
21812
|
+
const trees = TREES[action];
|
|
21813
|
+
for (const key of Object.keys(value))
|
|
21814
|
+
if (key !== "role" && !trees.includes(key))
|
|
21815
|
+
return key.length > MAX_IDENTIFIER_LENGTH ? {
|
|
21816
|
+
ok: false,
|
|
21817
|
+
path: "",
|
|
21818
|
+
reason: "has a field that is not a known field"
|
|
21819
|
+
} : { ok: false, path: key, reason: "is not a known field" };
|
|
21820
|
+
const condition = {};
|
|
21821
|
+
if (Object.hasOwn(value, "role")) {
|
|
21822
|
+
const role = value.role;
|
|
21823
|
+
if (role !== "any" && role !== "authenticated")
|
|
21824
|
+
return {
|
|
21825
|
+
ok: false,
|
|
21826
|
+
path: "role",
|
|
21827
|
+
reason: 'must be "any" or "authenticated"'
|
|
21828
|
+
};
|
|
21829
|
+
if (role === "authenticated") condition.role = role;
|
|
21830
|
+
}
|
|
21831
|
+
const [required2] = trees;
|
|
21832
|
+
for (const key of trees) {
|
|
21833
|
+
if (!Object.hasOwn(value, key)) {
|
|
21834
|
+
if (key === required2)
|
|
21835
|
+
return { ok: false, path: key, reason: "is required" };
|
|
21836
|
+
continue;
|
|
21837
|
+
}
|
|
21838
|
+
const parsed = parsePolicyNode(value[key], key);
|
|
21839
|
+
if (!parsed.ok) return parsed;
|
|
21840
|
+
condition[key] = parsed.node;
|
|
21841
|
+
}
|
|
21842
|
+
return { ok: true, condition };
|
|
21843
|
+
}
|
|
21844
|
+
function conditionsTooLarge(texts) {
|
|
21845
|
+
let total = 0;
|
|
21846
|
+
for (const text of texts)
|
|
21847
|
+
if (typeof text === "string") total += utf8ByteLength(text);
|
|
21848
|
+
return total > MAX_CONDITION_BYTES;
|
|
21849
|
+
}
|
|
21850
|
+
function isInvalidCondition(value) {
|
|
21851
|
+
return isPlainObject2(value) && typeof value.invalid === "string";
|
|
21852
|
+
}
|
|
21853
|
+
function checkOf(action, condition) {
|
|
21854
|
+
if (action === "insert") return condition.check;
|
|
21855
|
+
if (action === "update") return condition.check ?? condition.using;
|
|
21856
|
+
return void 0;
|
|
21857
|
+
}
|
|
21858
|
+
var MAX_CONDITION_VALUES = 50;
|
|
21859
|
+
var ARRAY_OPERATORS2 = ["contains", "containedBy", "overlaps"];
|
|
21860
|
+
function treeValues(node, rebinds) {
|
|
21861
|
+
if ("not" in node) return treeValues(node.not, rebinds);
|
|
21862
|
+
if ("or" in node || "and" in node)
|
|
21863
|
+
return ("or" in node ? node.or : node.and).reduce(
|
|
21864
|
+
(sum, child) => sum + treeValues(child, rebinds),
|
|
21865
|
+
0
|
|
21866
|
+
);
|
|
21867
|
+
const value = node.value;
|
|
21868
|
+
const own2 = Array.isArray(value) ? ARRAY_OPERATORS2.includes(node.operator) ? new Set(value.map((item) => JSON.stringify(item))).size : value.length : node.operator === "is" && value !== null ? 0 : 1;
|
|
21869
|
+
if (!rebinds || Array.isArray(value) && value.length === 0) return own2;
|
|
21870
|
+
return own2 + (ARRAY_OPERATORS2.includes(node.operator) ? 5 : 1);
|
|
21871
|
+
}
|
|
21872
|
+
function conditionValueCount(action, condition) {
|
|
21873
|
+
let count2 = 0;
|
|
21874
|
+
if (action !== "insert" && condition.using)
|
|
21875
|
+
count2 += treeValues(condition.using, false);
|
|
21876
|
+
const check2 = checkOf(action, condition);
|
|
21877
|
+
if (check2) count2 += treeValues(check2, action === "update");
|
|
21878
|
+
return count2;
|
|
21879
|
+
}
|
|
21880
|
+
|
|
21881
|
+
// ../../packages/data-core/src/legacy-policy.ts
|
|
21882
|
+
var RULES = ["none", "public", "authenticated", "owner"];
|
|
21883
|
+
var POLICY_ACTIONS2 = ["select", "insert", "update", "delete"];
|
|
21884
|
+
function isRule(value) {
|
|
21885
|
+
return RULES.includes(value);
|
|
21886
|
+
}
|
|
21887
|
+
function sugarName(action) {
|
|
21888
|
+
return `naro_${action}`;
|
|
21889
|
+
}
|
|
21890
|
+
var CONVERTED_RULE = "rls";
|
|
21891
|
+
var ROW_AS_READ = "table_name = ? AND select_rule IS ? AND insert_rule IS ? AND update_rule IS ? AND delete_rule IS ? AND owner_column IS ? AND select_condition IS ? AND insert_condition IS ? AND update_condition IS ? AND delete_condition IS ? AND updated_at IS ?";
|
|
21892
|
+
var LEGACY_ROW_COLUMNS = [
|
|
21893
|
+
"table_name",
|
|
21894
|
+
"select_rule",
|
|
21895
|
+
"insert_rule",
|
|
21896
|
+
"update_rule",
|
|
21897
|
+
"delete_rule",
|
|
21898
|
+
"owner_column",
|
|
21899
|
+
"select_condition",
|
|
21900
|
+
"insert_condition",
|
|
21901
|
+
"update_condition",
|
|
21902
|
+
"delete_condition",
|
|
21903
|
+
"updated_at"
|
|
21904
|
+
];
|
|
21905
|
+
function legacyRowAsRead(row) {
|
|
21906
|
+
const values = [];
|
|
21907
|
+
for (const column of LEGACY_ROW_COLUMNS) {
|
|
21908
|
+
const value = row[column];
|
|
21909
|
+
if (value === null || value === void 0) values.push(null);
|
|
21910
|
+
else if (typeof value === "string") values.push(value);
|
|
21911
|
+
else return void 0;
|
|
21912
|
+
}
|
|
21913
|
+
return values;
|
|
21914
|
+
}
|
|
21915
|
+
var POLICY_SQL = {
|
|
21916
|
+
list: "SELECT * FROM _naro_policies",
|
|
21917
|
+
upsert: "INSERT INTO _naro_policies (table_name, select_rule, insert_rule, update_rule, delete_rule, owner_column, updated_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(table_name) DO UPDATE SET select_rule = excluded.select_rule, insert_rule = excluded.insert_rule, update_rule = excluded.update_rule, delete_rule = excluded.delete_rule, owner_column = excluded.owner_column, updated_at = excluded.updated_at",
|
|
21918
|
+
upsertWithConditions: "INSERT INTO _naro_policies (table_name, select_rule, insert_rule, update_rule, delete_rule, owner_column, select_condition, insert_condition, update_condition, delete_condition, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(table_name) DO UPDATE SET select_rule = excluded.select_rule, insert_rule = excluded.insert_rule, update_rule = excluded.update_rule, delete_rule = excluded.delete_rule, owner_column = excluded.owner_column, select_condition = excluded.select_condition, insert_condition = excluded.insert_condition, update_condition = excluded.update_condition, delete_condition = excluded.delete_condition, updated_at = excluded.updated_at",
|
|
21919
|
+
remove: "DELETE FROM _naro_policies WHERE table_name = ?",
|
|
21920
|
+
/**
|
|
21921
|
+
* Migration 4: the rules of a converted row, if it is still as read —
|
|
21922
|
+
* binds `legacyRowAsRead(row)`.
|
|
21923
|
+
*/
|
|
21924
|
+
markConverted: `UPDATE _naro_policies SET select_rule = '${CONVERTED_RULE}', insert_rule = '${CONVERTED_RULE}', update_rule = '${CONVERTED_RULE}', delete_rule = '${CONVERTED_RULE}' WHERE ${ROW_AS_READ}`,
|
|
21925
|
+
/**
|
|
21926
|
+
* Migration 4: one named policy a row converts to, written only if the
|
|
21927
|
+
* row is still as read — the same gate as `markConverted`, so a row the
|
|
21928
|
+
* old control plane changed in between is neither converted nor marked.
|
|
21929
|
+
* Binds `policyValues` and then `legacyRowAsRead(row)`.
|
|
21930
|
+
*/
|
|
21931
|
+
insertConverted: `INSERT OR IGNORE INTO _naro_policies_v2 (table_name, name, kind, command, roles, using_expr, check_expr, owner_column) SELECT ?, ?, ?, ?, ?, ?, ?, ? WHERE EXISTS (SELECT 1 FROM _naro_policies WHERE ${ROW_AS_READ})`
|
|
21932
|
+
};
|
|
21933
|
+
function readStoredCondition(action, text, tooLarge) {
|
|
21934
|
+
if (text === void 0 || text === null) return "is missing";
|
|
21935
|
+
if (typeof text !== "string") return "is not JSON";
|
|
21936
|
+
if (tooLarge)
|
|
21937
|
+
return `the conditions of the policy are larger than ${String(MAX_CONDITION_BYTES)} bytes`;
|
|
21938
|
+
let value;
|
|
21939
|
+
try {
|
|
21940
|
+
value = JSON.parse(text);
|
|
21941
|
+
} catch {
|
|
21942
|
+
return "is not JSON";
|
|
21943
|
+
}
|
|
21944
|
+
const parsed = parseCondition(action, value);
|
|
21945
|
+
if (parsed.ok) return parsed.condition;
|
|
21946
|
+
return parsed.path === "" ? parsed.reason : `${parsed.path} ${parsed.reason}`;
|
|
21947
|
+
}
|
|
21948
|
+
function policyFromRow(row, log2) {
|
|
21949
|
+
const table = row.table_name;
|
|
21950
|
+
if (typeof table !== "string" || table === "") return void 0;
|
|
21951
|
+
const texts = POLICY_ACTIONS2.map(
|
|
21952
|
+
(action) => row[`${action}_rule`] === CONDITION_RULE ? row[`${action}_condition`] : void 0
|
|
21953
|
+
);
|
|
21954
|
+
const tooLarge = conditionsTooLarge(
|
|
21955
|
+
texts.filter((text) => typeof text === "string")
|
|
21956
|
+
);
|
|
21957
|
+
const read = POLICY_ACTIONS2.map(
|
|
21958
|
+
(action, index) => {
|
|
21959
|
+
const value = row[`${action}_rule`];
|
|
21960
|
+
if (value !== CONDITION_RULE) return isRule(value) ? value : "none";
|
|
21961
|
+
return readStoredCondition(action, texts[index], tooLarge);
|
|
21962
|
+
}
|
|
21963
|
+
);
|
|
21964
|
+
const values = POLICY_ACTIONS2.reduce((sum, action, index) => {
|
|
21965
|
+
const item = read[index];
|
|
21966
|
+
return typeof item === "object" ? sum + conditionValueCount(action, item) : sum;
|
|
21967
|
+
}, 0);
|
|
21968
|
+
const rule = (action, index) => {
|
|
21969
|
+
let item = read[index] ?? "none";
|
|
21970
|
+
if (typeof item === "object" && values > MAX_CONDITION_VALUES)
|
|
21971
|
+
item = `the conditions of the policy use more than ${String(MAX_CONDITION_VALUES)} values`;
|
|
21972
|
+
if (typeof item === "object" || isRule(item)) return item;
|
|
21973
|
+
log2?.(
|
|
21974
|
+
`naro data-core: the ${action} condition of ${table} cannot be read: ${item}`
|
|
21975
|
+
);
|
|
21976
|
+
return { invalid: item };
|
|
21977
|
+
};
|
|
21978
|
+
const owner = row.owner_column;
|
|
21979
|
+
return {
|
|
21980
|
+
table,
|
|
21981
|
+
select: rule("select", 0),
|
|
21982
|
+
insert: rule("insert", 1),
|
|
21983
|
+
update: rule("update", 2),
|
|
21984
|
+
delete: rule("delete", 3),
|
|
21985
|
+
ownerColumn: typeof owner === "string" && owner !== "" ? owner : null
|
|
21986
|
+
};
|
|
21987
|
+
}
|
|
21988
|
+
function findPolicy(policies, table) {
|
|
21989
|
+
const exact = policies.find((policy) => policy.table === table);
|
|
21990
|
+
if (exact) return exact;
|
|
21991
|
+
const folded = asciiLower(table);
|
|
21992
|
+
const matches = policies.filter(
|
|
21993
|
+
(policy) => asciiLower(policy.table) === folded
|
|
21994
|
+
);
|
|
21995
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
21996
|
+
}
|
|
21997
|
+
function legacyInForce(policies, tables) {
|
|
21998
|
+
const picked = /* @__PURE__ */ new Set();
|
|
21999
|
+
for (const table of tables) {
|
|
22000
|
+
const policy = findPolicy(policies, table);
|
|
22001
|
+
if (policy) picked.add(policy);
|
|
22002
|
+
}
|
|
22003
|
+
return policies.filter((policy) => picked.has(policy));
|
|
22004
|
+
}
|
|
22005
|
+
function valueExpr(value) {
|
|
22006
|
+
if (typeof value === "object" && value !== null)
|
|
22007
|
+
return { kind: "auth", fn: "uid" };
|
|
22008
|
+
return { kind: "literal", value };
|
|
22009
|
+
}
|
|
22010
|
+
var COMPARISONS = {
|
|
22011
|
+
eq: "=",
|
|
22012
|
+
neq: "<>",
|
|
22013
|
+
gt: ">",
|
|
22014
|
+
gte: ">=",
|
|
22015
|
+
lt: "<",
|
|
22016
|
+
lte: "<="
|
|
22017
|
+
};
|
|
22018
|
+
var ARRAYS = { contains: "@>", containedBy: "<@", overlaps: "&&" };
|
|
22019
|
+
var UID = { kind: "auth", fn: "uid" };
|
|
22020
|
+
function uidIsNull(not) {
|
|
22021
|
+
return { kind: "is", not, operand: UID, value: null };
|
|
22022
|
+
}
|
|
22023
|
+
function anonDiffers(expr) {
|
|
22024
|
+
if (expr.kind === "array")
|
|
22025
|
+
return expr.values.some((value) => typeof value === "object");
|
|
22026
|
+
if (expr.kind !== "in") return false;
|
|
22027
|
+
const uids = expr.list.filter((item) => item.kind === "auth").length;
|
|
22028
|
+
return uids > 0 && uids < expr.list.length;
|
|
22029
|
+
}
|
|
22030
|
+
function filterNodeToExpr(node, negated = false) {
|
|
22031
|
+
if ("not" in node)
|
|
22032
|
+
return { kind: "not", arg: filterNodeToExpr(node.not, !negated) };
|
|
22033
|
+
if ("or" in node || "and" in node) {
|
|
22034
|
+
const children = ("or" in node ? node.or : node.and).map(
|
|
22035
|
+
(child) => filterNodeToExpr(child, negated)
|
|
22036
|
+
);
|
|
22037
|
+
if (children.length === 1) return children[0];
|
|
22038
|
+
return { kind: "or" in node ? "or" : "and", args: children };
|
|
22039
|
+
}
|
|
22040
|
+
const leaf = leafToExpr(node);
|
|
22041
|
+
if (!anonDiffers(leaf)) return leaf;
|
|
22042
|
+
return negated ? { kind: "or", args: [leaf, uidIsNull(false)] } : { kind: "and", args: [leaf, uidIsNull(true)] };
|
|
22043
|
+
}
|
|
22044
|
+
function leafToExpr(node) {
|
|
22045
|
+
const column = {
|
|
22046
|
+
kind: "column",
|
|
22047
|
+
qualifier: null,
|
|
22048
|
+
name: node.column
|
|
22049
|
+
};
|
|
22050
|
+
const value = node.value;
|
|
22051
|
+
switch (node.operator) {
|
|
22052
|
+
case "eq":
|
|
22053
|
+
case "neq":
|
|
22054
|
+
case "gt":
|
|
22055
|
+
case "gte":
|
|
22056
|
+
case "lt":
|
|
22057
|
+
case "lte":
|
|
22058
|
+
return {
|
|
22059
|
+
kind: "compare",
|
|
22060
|
+
op: COMPARISONS[node.operator],
|
|
22061
|
+
left: column,
|
|
22062
|
+
right: valueExpr(Array.isArray(value) ? null : value)
|
|
22063
|
+
};
|
|
22064
|
+
case "in": {
|
|
22065
|
+
const list = Array.isArray(value) ? value : [value];
|
|
22066
|
+
if (list.length === 0) return { kind: "literal", value: false };
|
|
22067
|
+
return {
|
|
22068
|
+
kind: "in",
|
|
22069
|
+
not: false,
|
|
22070
|
+
operand: column,
|
|
22071
|
+
list: list.map(valueExpr)
|
|
22072
|
+
};
|
|
22073
|
+
}
|
|
22074
|
+
case "is":
|
|
22075
|
+
return {
|
|
22076
|
+
kind: "is",
|
|
22077
|
+
not: false,
|
|
22078
|
+
operand: column,
|
|
22079
|
+
value: value === true || value === false ? value : null
|
|
22080
|
+
};
|
|
22081
|
+
case "like":
|
|
22082
|
+
case "ilike":
|
|
22083
|
+
return {
|
|
22084
|
+
kind: "like",
|
|
22085
|
+
not: false,
|
|
22086
|
+
ci: node.operator === "ilike",
|
|
22087
|
+
operand: column,
|
|
22088
|
+
pattern: typeof value === "string" ? value : ""
|
|
22089
|
+
};
|
|
22090
|
+
case "contains":
|
|
22091
|
+
case "containedBy":
|
|
22092
|
+
case "overlaps":
|
|
22093
|
+
return {
|
|
22094
|
+
kind: "array",
|
|
22095
|
+
op: ARRAYS[node.operator],
|
|
22096
|
+
operand: { kind: "column", qualifier: null, name: node.column },
|
|
22097
|
+
values: (Array.isArray(value) ? value : []).map(
|
|
22098
|
+
(item) => typeof item === "object" && item !== null ? { kind: "auth", fn: "uid" } : typeof item === "number" ? item : String(item)
|
|
22099
|
+
)
|
|
22100
|
+
};
|
|
22101
|
+
}
|
|
22102
|
+
}
|
|
22103
|
+
var TRUE_TEXT = printExpr({ kind: "literal", value: true });
|
|
22104
|
+
function treeText(node) {
|
|
22105
|
+
return node === void 0 ? null : printExpr(filterNodeToExpr(node));
|
|
22106
|
+
}
|
|
22107
|
+
function namedFromRule(table, action, rule, ownerColumn) {
|
|
22108
|
+
const base2 = {
|
|
22109
|
+
table,
|
|
22110
|
+
name: sugarName(action),
|
|
22111
|
+
permissive: true,
|
|
22112
|
+
command: action,
|
|
22113
|
+
owner: null
|
|
22114
|
+
};
|
|
22115
|
+
const everyone = [...POLICY_ROLES];
|
|
22116
|
+
if (rule === "none" || typeof rule === "string" && !isRule(rule))
|
|
22117
|
+
return null;
|
|
22118
|
+
if (rule === "public" || rule === "authenticated")
|
|
22119
|
+
return {
|
|
22120
|
+
...base2,
|
|
22121
|
+
roles: rule === "public" ? everyone : ["authenticated"],
|
|
22122
|
+
using: action === "insert" ? null : TRUE_TEXT,
|
|
22123
|
+
check: action === "insert" ? TRUE_TEXT : null
|
|
22124
|
+
};
|
|
22125
|
+
if (rule === "owner") {
|
|
22126
|
+
const column = ownerColumn ?? "";
|
|
22127
|
+
return {
|
|
22128
|
+
...base2,
|
|
22129
|
+
roles: ["authenticated"],
|
|
22130
|
+
...column === "" ? { using: null, check: null } : ownerClauses(column, action),
|
|
22131
|
+
owner: column
|
|
22132
|
+
};
|
|
22133
|
+
}
|
|
22134
|
+
if (isInvalidCondition(rule) || (action === "insert" ? rule.check : rule.using) === void 0)
|
|
22135
|
+
return { ...base2, roles: everyone, using: null, check: null };
|
|
22136
|
+
return {
|
|
22137
|
+
...base2,
|
|
22138
|
+
roles: rule.role === "authenticated" ? ["authenticated"] : everyone,
|
|
22139
|
+
using: action === "insert" ? null : treeText(rule.using),
|
|
22140
|
+
check: action === "insert" || action === "update" ? treeText(rule.check) : null
|
|
22141
|
+
};
|
|
22142
|
+
}
|
|
22143
|
+
function namedFromLegacy(policy) {
|
|
22144
|
+
return POLICY_ACTIONS2.flatMap((action) => {
|
|
22145
|
+
const named = namedFromRule(
|
|
22146
|
+
policy.table,
|
|
22147
|
+
action,
|
|
22148
|
+
policy[action],
|
|
22149
|
+
policy.ownerColumn
|
|
22150
|
+
);
|
|
22151
|
+
return named ? [named] : [];
|
|
22152
|
+
});
|
|
22153
|
+
}
|
|
22154
|
+
function loadNamed(policies, log2) {
|
|
22155
|
+
const columns = [
|
|
22156
|
+
"table_name",
|
|
22157
|
+
"name",
|
|
22158
|
+
"kind",
|
|
22159
|
+
"command",
|
|
22160
|
+
"roles",
|
|
22161
|
+
"using_expr",
|
|
22162
|
+
"check_expr",
|
|
22163
|
+
"owner_column"
|
|
22164
|
+
];
|
|
22165
|
+
return policies.flatMap((policy) => {
|
|
22166
|
+
const values = policyValues(policy);
|
|
22167
|
+
const loaded = loadPolicy(
|
|
22168
|
+
Object.fromEntries(
|
|
22169
|
+
columns.map((column, index) => [column, values[index]])
|
|
22170
|
+
),
|
|
22171
|
+
log2
|
|
22172
|
+
);
|
|
22173
|
+
return loaded ? [loaded] : [];
|
|
22174
|
+
});
|
|
22175
|
+
}
|
|
22176
|
+
function unreadableConversions(policy) {
|
|
22177
|
+
const found = [];
|
|
22178
|
+
for (const action of POLICY_ACTIONS2) {
|
|
22179
|
+
const rule = policy[action];
|
|
22180
|
+
const named = namedFromRule(policy.table, action, rule, policy.ownerColumn);
|
|
22181
|
+
if (!named) continue;
|
|
22182
|
+
const [loaded] = loadNamed([named]);
|
|
22183
|
+
if (!loaded || loaded.invalid === null) continue;
|
|
22184
|
+
found.push({
|
|
22185
|
+
table: named.table,
|
|
22186
|
+
policy: named.name,
|
|
22187
|
+
reason: isInvalidCondition(rule) ? `the old ${action} condition could not be read: ${rule.invalid}` : rule === "owner" && named.owner === "" ? `the old ${action} rule is owner, and the row has no owner column` : loaded.invalid
|
|
22188
|
+
});
|
|
22189
|
+
}
|
|
22190
|
+
return found;
|
|
22191
|
+
}
|
|
22192
|
+
|
|
22193
|
+
// ../../packages/data-core/src/policy-index.ts
|
|
22194
|
+
var INDEX_SQL = `SELECT t.name AS table_name, il.name AS index_name, il."partial" AS partial, ix.name AS column_name, ix.coll AS collation FROM pragma_table_list AS t JOIN pragma_index_list(t.name, t.schema) AS il JOIN pragma_index_xinfo(il.name, t.schema) AS ix WHERE t.schema = 'main' AND t.type = 'table' AND ${systemTablePredicate("t.name")} AND ix.seqno = 0`;
|
|
22195
|
+
|
|
22196
|
+
// ../../packages/data-core/src/policy-statement.ts
|
|
22197
|
+
function readPolicyName(stream) {
|
|
22198
|
+
const token = stream.peek();
|
|
22199
|
+
if (token.kind !== "ident")
|
|
22200
|
+
throw new PolicySyntaxError(
|
|
22201
|
+
`expected a policy name, found ${describeToken(token)}`,
|
|
22202
|
+
token.position
|
|
22203
|
+
);
|
|
22204
|
+
stream.next();
|
|
22205
|
+
return foldPolicyName(token.value, token.quoted);
|
|
22206
|
+
}
|
|
22207
|
+
function readRoleName(token) {
|
|
22208
|
+
if (token.kind !== "ident")
|
|
22209
|
+
throw new PolicySyntaxError(
|
|
22210
|
+
`expected a role, found ${describeToken(token)}`,
|
|
22211
|
+
token.position
|
|
22212
|
+
);
|
|
22213
|
+
return token.quoted ? token.value : asciiLower(token.value);
|
|
22214
|
+
}
|
|
22215
|
+
function readCommand(stream) {
|
|
22216
|
+
const token = stream.next();
|
|
22217
|
+
for (const command of [
|
|
22218
|
+
"all",
|
|
22219
|
+
"select",
|
|
22220
|
+
"insert",
|
|
22221
|
+
"update",
|
|
22222
|
+
"delete"
|
|
22223
|
+
])
|
|
22224
|
+
if (isKeyword(token, command)) return command;
|
|
22225
|
+
throw new PolicySyntaxError(
|
|
22226
|
+
`expected ALL, SELECT, INSERT, UPDATE or DELETE, found ${describeToken(token)}`,
|
|
22227
|
+
token.position
|
|
22228
|
+
);
|
|
22229
|
+
}
|
|
22230
|
+
function readClause(stream) {
|
|
22231
|
+
const open = stream.expectOperator("(");
|
|
22232
|
+
const expr = readCondition(stream);
|
|
22233
|
+
stream.expectOperator(")");
|
|
22234
|
+
checkExpressionSize(expr, open.position);
|
|
22235
|
+
return { text: printExpr(expr), position: open.position };
|
|
22236
|
+
}
|
|
22237
|
+
function readCreate(stream, start) {
|
|
22238
|
+
stream.expectKeyword("policy");
|
|
22239
|
+
const name = readPolicyName(stream);
|
|
22240
|
+
stream.expectKeyword("on");
|
|
22241
|
+
const table = readTableName(stream);
|
|
22242
|
+
let permissive = true;
|
|
22243
|
+
if (stream.acceptKeyword("as")) {
|
|
22244
|
+
const token = stream.next();
|
|
22245
|
+
if (isKeyword(token, "restrictive")) permissive = false;
|
|
22246
|
+
else if (!isKeyword(token, "permissive"))
|
|
22247
|
+
throw new PolicySyntaxError(
|
|
22248
|
+
`expected PERMISSIVE or RESTRICTIVE, found ${describeToken(token)}`,
|
|
22249
|
+
token.position
|
|
22250
|
+
);
|
|
22251
|
+
}
|
|
22252
|
+
let command = "all";
|
|
22253
|
+
if (stream.acceptKeyword("for")) command = readCommand(stream);
|
|
22254
|
+
let roles = readRoles(["public"]);
|
|
22255
|
+
if (isKeyword(stream.peek(), "to")) {
|
|
22256
|
+
const to = stream.next();
|
|
22257
|
+
const names = [];
|
|
22258
|
+
do
|
|
22259
|
+
names.push(readRoleName(stream.next()));
|
|
22260
|
+
while (stream.acceptOperator(","));
|
|
22261
|
+
roles = readRoles(names);
|
|
22262
|
+
if (!roles.ok)
|
|
22263
|
+
throw new PolicySyntaxError(roles.reason.replace(/\.$/, ""), to.position);
|
|
22264
|
+
}
|
|
22265
|
+
let using = null;
|
|
22266
|
+
let check2 = null;
|
|
22267
|
+
let position = start.position;
|
|
22268
|
+
if (stream.acceptKeyword("using")) {
|
|
22269
|
+
const clause = readClause(stream);
|
|
22270
|
+
using = clause.text;
|
|
22271
|
+
position = clause.position;
|
|
22272
|
+
}
|
|
22273
|
+
if (isKeyword(stream.peek(), "with")) {
|
|
22274
|
+
stream.next();
|
|
22275
|
+
stream.expectKeyword("check");
|
|
22276
|
+
const clause = readClause(stream);
|
|
22277
|
+
check2 = clause.text;
|
|
22278
|
+
position = clause.position;
|
|
22279
|
+
}
|
|
22280
|
+
if (!roles.ok) throw new Error("unreachable");
|
|
22281
|
+
const policy = {
|
|
22282
|
+
table,
|
|
22283
|
+
name,
|
|
22284
|
+
permissive,
|
|
22285
|
+
command,
|
|
22286
|
+
roles: roles.roles,
|
|
22287
|
+
using,
|
|
22288
|
+
check: check2,
|
|
22289
|
+
owner: null
|
|
22290
|
+
};
|
|
22291
|
+
const problem = policyShapeProblem(policy);
|
|
22292
|
+
if (problem)
|
|
22293
|
+
throw new PolicySyntaxError(problem.replace(/\.$/, ""), position);
|
|
22294
|
+
return { kind: "create", policy };
|
|
22295
|
+
}
|
|
22296
|
+
function readDrop(stream) {
|
|
22297
|
+
stream.expectKeyword("policy");
|
|
22298
|
+
let ifExists = false;
|
|
22299
|
+
if (stream.acceptKeyword("if")) {
|
|
22300
|
+
stream.expectKeyword("exists");
|
|
22301
|
+
ifExists = true;
|
|
22302
|
+
}
|
|
22303
|
+
const name = readPolicyName(stream);
|
|
22304
|
+
stream.expectKeyword("on");
|
|
22305
|
+
const table = readTableName(stream);
|
|
22306
|
+
if (!stream.acceptKeyword("cascade")) stream.acceptKeyword("restrict");
|
|
22307
|
+
return { kind: "drop", name, table, ifExists };
|
|
22308
|
+
}
|
|
22309
|
+
function parsePolicyStatement(text) {
|
|
22310
|
+
if (policySqlTooLong(text))
|
|
22311
|
+
return {
|
|
22312
|
+
ok: false,
|
|
22313
|
+
reason: `the statement is longer than ${String(MAX_POLICY_SQL_BYTES)} bytes`,
|
|
22314
|
+
position: 1
|
|
22315
|
+
};
|
|
22316
|
+
try {
|
|
22317
|
+
const stream = new TokenStream(tokenize(text));
|
|
22318
|
+
const start = stream.next();
|
|
22319
|
+
let statement;
|
|
22320
|
+
if (isKeyword(start, "create")) statement = readCreate(stream, start);
|
|
22321
|
+
else if (isKeyword(start, "drop")) statement = readDrop(stream);
|
|
22322
|
+
else if (isKeyword(start, "alter"))
|
|
22323
|
+
throw new PolicySyntaxError(
|
|
22324
|
+
"ALTER POLICY is not supported here yet: DROP POLICY and CREATE POLICY again",
|
|
22325
|
+
start.position
|
|
22326
|
+
);
|
|
22327
|
+
else if (start.kind === "eof")
|
|
22328
|
+
throw new PolicySyntaxError("the statement is empty", 1);
|
|
22329
|
+
else
|
|
22330
|
+
throw new PolicySyntaxError(
|
|
22331
|
+
"only CREATE POLICY and DROP POLICY are accepted here",
|
|
22332
|
+
start.position
|
|
22333
|
+
);
|
|
22334
|
+
stream.acceptOperator(";");
|
|
22335
|
+
const rest = stream.peek();
|
|
22336
|
+
if (rest.kind !== "eof")
|
|
22337
|
+
throw new PolicySyntaxError(
|
|
22338
|
+
isKeyword(rest, "create") || isKeyword(rest, "drop") ? "one statement at a time" : `syntax error at or near ${describeToken(rest)}`,
|
|
22339
|
+
rest.position
|
|
22340
|
+
);
|
|
22341
|
+
return { ok: true, statement };
|
|
22342
|
+
} catch (error51) {
|
|
22343
|
+
if (error51 instanceof PolicySyntaxError)
|
|
22344
|
+
return { ok: false, reason: error51.reason, position: error51.position };
|
|
22345
|
+
throw error51;
|
|
22346
|
+
}
|
|
22347
|
+
}
|
|
22348
|
+
function splitPolicyStatements(text) {
|
|
22349
|
+
let tokens;
|
|
22350
|
+
try {
|
|
22351
|
+
tokens = tokenize(text);
|
|
22352
|
+
} catch (error51) {
|
|
22353
|
+
if (error51 instanceof PolicySyntaxError)
|
|
22354
|
+
return { ok: false, reason: error51.reason, position: error51.position };
|
|
22355
|
+
throw error51;
|
|
22356
|
+
}
|
|
22357
|
+
const statements = [];
|
|
22358
|
+
let start;
|
|
22359
|
+
for (const token of tokens) {
|
|
22360
|
+
if (token.kind === "eof") break;
|
|
22361
|
+
start ??= token.position - 1;
|
|
22362
|
+
if (token.kind === "op" && token.value === ";") {
|
|
22363
|
+
statements.push(text.slice(start, token.position).trim());
|
|
22364
|
+
start = void 0;
|
|
22365
|
+
}
|
|
22366
|
+
}
|
|
22367
|
+
if (start !== void 0) {
|
|
22368
|
+
const last = text.slice(start).trim();
|
|
22369
|
+
if (last !== "") statements.push(last);
|
|
22370
|
+
}
|
|
22371
|
+
return { ok: true, statements };
|
|
22372
|
+
}
|
|
20216
22373
|
|
|
20217
22374
|
// ../../packages/data-core/src/redirect-origins.ts
|
|
20218
22375
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
@@ -20259,6 +22416,34 @@ function formatRedirectEntry(entry) {
|
|
|
20259
22416
|
}
|
|
20260
22417
|
|
|
20261
22418
|
// ../../packages/data-core/src/system-sql.ts
|
|
22419
|
+
async function convertLegacyPolicies(db, found) {
|
|
22420
|
+
const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
|
|
22421
|
+
if (rows.length === 0) return [];
|
|
22422
|
+
const read = rows.map((row) => ({
|
|
22423
|
+
policy: policyFromRow(row),
|
|
22424
|
+
asRead: legacyRowAsRead(row)
|
|
22425
|
+
}));
|
|
22426
|
+
const inForce = new Set(
|
|
22427
|
+
legacyInForce(
|
|
22428
|
+
read.flatMap(({ policy }) => policy ? [policy] : []),
|
|
22429
|
+
await readTableNames(db)
|
|
22430
|
+
)
|
|
22431
|
+
);
|
|
22432
|
+
const statements = [];
|
|
22433
|
+
for (const { policy, asRead } of read) {
|
|
22434
|
+
if (!policy || !asRead) continue;
|
|
22435
|
+
if (inForce.has(policy)) {
|
|
22436
|
+
for (const named of namedFromLegacy(policy))
|
|
22437
|
+
statements.push({
|
|
22438
|
+
sql: POLICY_SQL.insertConverted,
|
|
22439
|
+
params: [...policyValues(named), ...asRead]
|
|
22440
|
+
});
|
|
22441
|
+
found.unreadable.push(...unreadableConversions(policy));
|
|
22442
|
+
}
|
|
22443
|
+
statements.push({ sql: POLICY_SQL.markConverted, params: asRead });
|
|
22444
|
+
}
|
|
22445
|
+
return statements;
|
|
22446
|
+
}
|
|
20262
22447
|
var MIGRATIONS_TABLE = "CREATE TABLE IF NOT EXISTS _naro_migrations (version INTEGER PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT (datetime('now')))";
|
|
20263
22448
|
var SYSTEM_MIGRATIONS = [
|
|
20264
22449
|
{
|
|
@@ -20300,6 +22485,15 @@ var SYSTEM_MIGRATIONS = [
|
|
|
20300
22485
|
"CREATE INDEX IF NOT EXISTS _naro_auth_email_log_address ON _naro_auth_email_log (address_hash, sent_at)",
|
|
20301
22486
|
"CREATE TABLE IF NOT EXISTS _naro_auth_contested (user_id TEXT PRIMARY KEY, contested_at INTEGER NOT NULL)"
|
|
20302
22487
|
]
|
|
22488
|
+
},
|
|
22489
|
+
{
|
|
22490
|
+
// P2-7 §9: named policies, many per table. The old rows are converted
|
|
22491
|
+
// in the same batch and their rules set to a word old workers read as
|
|
22492
|
+
// `none`.
|
|
22493
|
+
version: 4,
|
|
22494
|
+
name: "named_policies",
|
|
22495
|
+
statements: [NAMED_POLICY_TABLE_SQL],
|
|
22496
|
+
compute: convertLegacyPolicies
|
|
20303
22497
|
}
|
|
20304
22498
|
];
|
|
20305
22499
|
|
|
@@ -20365,12 +22559,12 @@ function sqlSkeleton(sql) {
|
|
|
20365
22559
|
let index = 0;
|
|
20366
22560
|
while (index < sql.length) {
|
|
20367
22561
|
const char = sql[index] ?? "";
|
|
20368
|
-
const
|
|
20369
|
-
if (
|
|
22562
|
+
const pair2 = sql.slice(index, index + 2);
|
|
22563
|
+
if (pair2 === "--") {
|
|
20370
22564
|
const end = sql.indexOf("\n", index);
|
|
20371
22565
|
index = end < 0 ? sql.length : end;
|
|
20372
22566
|
out += " ";
|
|
20373
|
-
} else if (
|
|
22567
|
+
} else if (pair2 === "/*") {
|
|
20374
22568
|
const end = sql.indexOf("*/", index + 2);
|
|
20375
22569
|
index = end < 0 ? sql.length : end + 2;
|
|
20376
22570
|
out += " ";
|
|
@@ -20548,12 +22742,12 @@ import {
|
|
|
20548
22742
|
rmSync as rmSync2,
|
|
20549
22743
|
writeFileSync as writeFileSync2
|
|
20550
22744
|
} from "fs";
|
|
20551
|
-
import { join as
|
|
22745
|
+
import { join as join22 } from "path";
|
|
20552
22746
|
function linkDir(dir) {
|
|
20553
|
-
return
|
|
22747
|
+
return join22(dir, ".naro");
|
|
20554
22748
|
}
|
|
20555
22749
|
function linkFile(dir) {
|
|
20556
|
-
return
|
|
22750
|
+
return join22(linkDir(dir), "project.json");
|
|
20557
22751
|
}
|
|
20558
22752
|
function readProjectLink(dir) {
|
|
20559
22753
|
try {
|
|
@@ -20575,7 +22769,7 @@ function writeProjectLink(dir, link) {
|
|
|
20575
22769
|
writeFileSync2(linkFile(dir), `${JSON.stringify(link, null, 2)}
|
|
20576
22770
|
`);
|
|
20577
22771
|
writeFileSync2(
|
|
20578
|
-
|
|
22772
|
+
join22(linkDir(dir), "README.txt"),
|
|
20579
22773
|
"This directory links the project to Naro and should not be committed.\n"
|
|
20580
22774
|
);
|
|
20581
22775
|
}
|
|
@@ -20585,9 +22779,9 @@ function removeProjectLink(dir) {
|
|
|
20585
22779
|
return true;
|
|
20586
22780
|
}
|
|
20587
22781
|
function ensureGitignoreHasNaro(dir) {
|
|
20588
|
-
const gitignorePath =
|
|
22782
|
+
const gitignorePath = join22(dir, ".gitignore");
|
|
20589
22783
|
const hasGitignore = existsSync4(gitignorePath);
|
|
20590
|
-
if (!hasGitignore && !existsSync4(
|
|
22784
|
+
if (!hasGitignore && !existsSync4(join22(dir, ".git"))) return false;
|
|
20591
22785
|
const current = hasGitignore ? readFileSync5(gitignorePath, "utf8") : "";
|
|
20592
22786
|
const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".naro" || line === ".naro/" || line === "/.naro");
|
|
20593
22787
|
if (covered) return false;
|
|
@@ -20928,10 +23122,10 @@ function buildResetPlan(input) {
|
|
|
20928
23122
|
const triggers = sorted(
|
|
20929
23123
|
objects.filter((object2) => object2.type === "trigger").map((object2) => object2.name)
|
|
20930
23124
|
);
|
|
20931
|
-
const
|
|
23125
|
+
const tableNames2 = sorted(
|
|
20932
23126
|
objects.filter((object2) => object2.type === "table" || object2.type === "virtual").map((object2) => object2.name)
|
|
20933
23127
|
);
|
|
20934
|
-
const canonical = new Map(
|
|
23128
|
+
const canonical = new Map(tableNames2.map((name) => [asciiLower(name), name]));
|
|
20935
23129
|
const edges = [];
|
|
20936
23130
|
for (const edge of input.foreignKeys) {
|
|
20937
23131
|
const child = canonical.get(asciiLower(edge.child));
|
|
@@ -20939,7 +23133,7 @@ function buildResetPlan(input) {
|
|
|
20939
23133
|
if (child !== void 0 && parent !== void 0 && child !== parent)
|
|
20940
23134
|
edges.push({ child, parent });
|
|
20941
23135
|
}
|
|
20942
|
-
const ordered = tableGroups(
|
|
23136
|
+
const ordered = tableGroups(tableNames2, edges);
|
|
20943
23137
|
refuseOversizedCycle(ordered);
|
|
20944
23138
|
const clear = clearInternal(input.internal);
|
|
20945
23139
|
const groups = [
|
|
@@ -20958,7 +23152,7 @@ function buildResetPlan(input) {
|
|
|
20958
23152
|
|
|
20959
23153
|
// src/env-consent.ts
|
|
20960
23154
|
import { existsSync as existsSync6 } from "fs";
|
|
20961
|
-
import { join as
|
|
23155
|
+
import { join as join23 } from "path";
|
|
20962
23156
|
var DOTENV_FILES = [".env.local", ".env.production", ".env"];
|
|
20963
23157
|
var PUBLIC_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_"];
|
|
20964
23158
|
function maskEnvValue(key, value) {
|
|
@@ -20967,7 +23161,7 @@ function maskEnvValue(key, value) {
|
|
|
20967
23161
|
return value.length <= 8 ? "****" : `${value.slice(0, 4)}****`;
|
|
20968
23162
|
}
|
|
20969
23163
|
function findDotenvFile(dir) {
|
|
20970
|
-
for (const name of DOTENV_FILES) if (existsSync6(
|
|
23164
|
+
for (const name of DOTENV_FILES) if (existsSync6(join23(dir, name))) return name;
|
|
20971
23165
|
return null;
|
|
20972
23166
|
}
|
|
20973
23167
|
function envAddGuidance(keys) {
|
|
@@ -21038,6 +23232,24 @@ async function tryExec(exec, args, cwd) {
|
|
|
21038
23232
|
return null;
|
|
21039
23233
|
}
|
|
21040
23234
|
}
|
|
23235
|
+
var COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
23236
|
+
var MAX_COMMIT_MESSAGE = 300;
|
|
23237
|
+
async function detectCommit(exec, dir) {
|
|
23238
|
+
const sha = (await tryExec(exec, ["rev-parse", "HEAD"], dir))?.toLowerCase();
|
|
23239
|
+
if (!sha || !COMMIT_SHA.test(sha)) return null;
|
|
23240
|
+
const subject = await tryExec(exec, ["log", "-1", "--format=%s"], dir);
|
|
23241
|
+
let dirty;
|
|
23242
|
+
try {
|
|
23243
|
+
dirty = (await exec(["status", "--porcelain"], dir)).trim().length > 0;
|
|
23244
|
+
} catch {
|
|
23245
|
+
dirty = true;
|
|
23246
|
+
}
|
|
23247
|
+
return {
|
|
23248
|
+
sha,
|
|
23249
|
+
message: subject === null ? null : [...subject].slice(0, MAX_COMMIT_MESSAGE).join(""),
|
|
23250
|
+
dirty
|
|
23251
|
+
};
|
|
23252
|
+
}
|
|
21041
23253
|
async function detectGitInfo(dir, exec = defaultExec) {
|
|
21042
23254
|
const remote = await tryExec(exec, ["remote", "get-url", "origin"], dir);
|
|
21043
23255
|
const branch = await tryExec(
|
|
@@ -21046,10 +23258,12 @@ async function detectGitInfo(dir, exec = defaultExec) {
|
|
|
21046
23258
|
dir
|
|
21047
23259
|
);
|
|
21048
23260
|
const prefix = await tryExec(exec, ["rev-parse", "--show-prefix"], dir);
|
|
23261
|
+
const commit = await detectCommit(exec, dir);
|
|
21049
23262
|
return {
|
|
21050
23263
|
repositoryUrl: remote !== null && SUPPORTED_REMOTE.some((re) => re.test(remote)) ? remote : null,
|
|
21051
23264
|
branch: branch !== null && branch !== "HEAD" ? branch : null,
|
|
21052
|
-
prefix: prefix === null ? null : prefix.replace(/\/+$/, "") || null
|
|
23265
|
+
prefix: prefix === null ? null : prefix.replace(/\/+$/, "") || null,
|
|
23266
|
+
commit
|
|
21053
23267
|
};
|
|
21054
23268
|
}
|
|
21055
23269
|
|
|
@@ -21133,7 +23347,8 @@ function manifest(artifact) {
|
|
|
21133
23347
|
compatibilityDate: artifact.compatibilityDate,
|
|
21134
23348
|
compatibilityFlags: artifact.compatibilityFlags,
|
|
21135
23349
|
assetsBinding: artifact.assetsBinding,
|
|
21136
|
-
...artifact.assetsConfig ? { assetsConfig: artifact.assetsConfig } : {}
|
|
23350
|
+
...artifact.assetsConfig ? { assetsConfig: artifact.assetsConfig } : {},
|
|
23351
|
+
...artifact.routes ? { routes: artifact.routes } : {}
|
|
21137
23352
|
};
|
|
21138
23353
|
}
|
|
21139
23354
|
async function uploadArtifact(artifact, opts) {
|
|
@@ -21215,8 +23430,14 @@ function eventsObserver(cp3, deploymentId, token, fetchImpl = fetch, logIngestUr
|
|
|
21215
23430
|
onLog: (log2) => batcher.add(log2).catch(() => {
|
|
21216
23431
|
}),
|
|
21217
23432
|
onMetadata: async (metadata) => {
|
|
23433
|
+
const {
|
|
23434
|
+
gitCommitSha: _sha,
|
|
23435
|
+
gitCommitMessage: _message,
|
|
23436
|
+
...rest
|
|
23437
|
+
} = metadata;
|
|
23438
|
+
if (Object.keys(rest).length === 0) return;
|
|
21218
23439
|
await flushLogs();
|
|
21219
|
-
await post({ type: "metadata", metadata }).catch(() => {
|
|
23440
|
+
await post({ type: "metadata", metadata: rest }).catch(() => {
|
|
21220
23441
|
});
|
|
21221
23442
|
}
|
|
21222
23443
|
};
|
|
@@ -21228,7 +23449,7 @@ function slugify2(value) {
|
|
|
21228
23449
|
async function projectName(sourceDir) {
|
|
21229
23450
|
try {
|
|
21230
23451
|
const pkg = JSON.parse(
|
|
21231
|
-
await readFile13(
|
|
23452
|
+
await readFile13(join24(sourceDir, "package.json"), "utf8")
|
|
21232
23453
|
);
|
|
21233
23454
|
if (pkg.name) return pkg.name;
|
|
21234
23455
|
} catch {
|
|
@@ -21239,7 +23460,7 @@ var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.tu
|
|
|
21239
23460
|
function resolveBin(name) {
|
|
21240
23461
|
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
21241
23462
|
for (; ; ) {
|
|
21242
|
-
const candidate =
|
|
23463
|
+
const candidate = join24(dir, "node_modules", ".bin", name);
|
|
21243
23464
|
if (existsSync7(candidate)) return candidate;
|
|
21244
23465
|
const parent = dirname4(dir);
|
|
21245
23466
|
if (parent === dir) return name;
|
|
@@ -21266,7 +23487,7 @@ async function detectPackageManagerLenient(root) {
|
|
|
21266
23487
|
}
|
|
21267
23488
|
function localDependencies(sourceDir) {
|
|
21268
23489
|
return {
|
|
21269
|
-
createWorkspace: () => mkdtemp2(
|
|
23490
|
+
createWorkspace: () => mkdtemp2(join24(tmpdir2(), "naro-build-")),
|
|
21270
23491
|
removeWorkspace: (path) => rm3(path, { recursive: true, force: true }),
|
|
21271
23492
|
async runCommand(spec, observer) {
|
|
21272
23493
|
if (spec.command === "git" && spec.args[0] === "clone") {
|
|
@@ -21371,7 +23592,8 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
21371
23592
|
...git.repositoryUrl != null && { repositoryUrl: git.repositoryUrl },
|
|
21372
23593
|
...branch != null && { branch },
|
|
21373
23594
|
...repositoryRoot != null && { rootDirectory: repositoryRoot },
|
|
21374
|
-
...Object.keys(envToSend).length > 0 && { env: envToSend }
|
|
23595
|
+
...Object.keys(envToSend).length > 0 && { env: envToSend },
|
|
23596
|
+
...git.commit != null && { commit: git.commit }
|
|
21375
23597
|
});
|
|
21376
23598
|
if (ctx.envSkipped?.length)
|
|
21377
23599
|
log2(`\u26A0 skipped env: ${ctx.envSkipped.join(", ")}`);
|
|
@@ -21487,7 +23709,12 @@ export {
|
|
|
21487
23709
|
summarizeStatement,
|
|
21488
23710
|
serializeDotenv,
|
|
21489
23711
|
systemTablePredicate,
|
|
23712
|
+
quoteIdent,
|
|
23713
|
+
commentText,
|
|
23714
|
+
quotePolicyName,
|
|
21490
23715
|
parsePolicyNode,
|
|
23716
|
+
parsePolicyStatement,
|
|
23717
|
+
splitPolicyStatements,
|
|
21491
23718
|
parseRedirectEntry,
|
|
21492
23719
|
formatRedirectEntry,
|
|
21493
23720
|
policiesForTypes,
|