naro-mcp 0.9.1 → 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/dist/index.js +2230 -49
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1020,14 +1020,12 @@ var DbApi = class {
|
|
|
1020
1020
|
* retrying rather than re-running the file blind.
|
|
1021
1021
|
*/
|
|
1022
1022
|
async applyMigration(projectId, input) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
{
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}
|
|
1030
|
-
);
|
|
1023
|
+
const body = await this.client.request(this.path(projectId, "/migrations"), {
|
|
1024
|
+
method: "POST",
|
|
1025
|
+
body: { name: input.name, sql: input.sql },
|
|
1026
|
+
context: "Apply migration"
|
|
1027
|
+
});
|
|
1028
|
+
return { ...body, warnings: body.warnings ?? [] };
|
|
1031
1029
|
}
|
|
1032
1030
|
/**
|
|
1033
1031
|
* The current Time Travel bookmark, or the one `timestamp` (RFC 3339 with a
|
|
@@ -1142,7 +1140,10 @@ var DbApi = class {
|
|
|
1142
1140
|
* `d1_migrations` — all operations or none. `dryRun` returns the SQL and
|
|
1143
1141
|
* the name without applying anything. Save `sql` as `migrations/<name>` so
|
|
1144
1142
|
* `naro db reset` re-applies it; a refused operation's index is in
|
|
1145
|
-
* `details.operation`.
|
|
1143
|
+
* `details.operation`. Policies left under a name the change gives to a
|
|
1144
|
+
* table are removed before it runs and stay removed if it then fails:
|
|
1145
|
+
* that refusal carries them in `details.removedPolicies`, shaped as the
|
|
1146
|
+
* result's `removedPolicies`.
|
|
1146
1147
|
*/
|
|
1147
1148
|
async schema(projectId, operations, options2 = {}) {
|
|
1148
1149
|
return this.client.request(
|
|
@@ -1161,6 +1162,9 @@ var DbApi = class {
|
|
|
1161
1162
|
}
|
|
1162
1163
|
};
|
|
1163
1164
|
var PROVISION_TIMEOUT_MS = 12e4;
|
|
1165
|
+
function writeQuery(options2) {
|
|
1166
|
+
return options2.allowUnindexed === true ? "?allow_unindexed=1" : "";
|
|
1167
|
+
}
|
|
1164
1168
|
function backendInfo(body) {
|
|
1165
1169
|
return {
|
|
1166
1170
|
status: body.status,
|
|
@@ -1179,6 +1183,35 @@ function backendInfo(body) {
|
|
|
1179
1183
|
turningOff: body.turningOff === true
|
|
1180
1184
|
};
|
|
1181
1185
|
}
|
|
1186
|
+
function authConfig(body) {
|
|
1187
|
+
return {
|
|
1188
|
+
...body,
|
|
1189
|
+
signupsEnabled: typeof body.signupsEnabled === "boolean" ? body.signupsEnabled : true,
|
|
1190
|
+
passwordMinLength: typeof body.passwordMinLength === "number" ? body.passwordMinLength : 6
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function isRecord(value) {
|
|
1194
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1195
|
+
}
|
|
1196
|
+
async function authChange(answer) {
|
|
1197
|
+
let body;
|
|
1198
|
+
try {
|
|
1199
|
+
body = await answer;
|
|
1200
|
+
} catch (error51) {
|
|
1201
|
+
const details = isNaroError(error51) ? error51.details : void 0;
|
|
1202
|
+
if (isNaroError(error51) && error51.code === "BACKEND_PROVISION_FAILED" && isRecord(details?.auth) && isRecord(details.backend))
|
|
1203
|
+
throw new NaroError(error51.code, error51.message, {
|
|
1204
|
+
...error51.status !== void 0 ? { status: error51.status } : {},
|
|
1205
|
+
details: {
|
|
1206
|
+
...details,
|
|
1207
|
+
auth: authConfig(details.auth),
|
|
1208
|
+
backend: backendInfo(details.backend)
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
throw error51;
|
|
1212
|
+
}
|
|
1213
|
+
return { auth: authConfig(body.auth), backend: backendInfo(body.backend) };
|
|
1214
|
+
}
|
|
1182
1215
|
var BackendApi = class {
|
|
1183
1216
|
constructor(client) {
|
|
1184
1217
|
this.client = client;
|
|
@@ -1311,11 +1344,13 @@ var BackendApi = class {
|
|
|
1311
1344
|
* and the callback URLs to register with the providers. Never a secret.
|
|
1312
1345
|
*/
|
|
1313
1346
|
async auth(projectId) {
|
|
1314
|
-
return
|
|
1315
|
-
this.
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1347
|
+
return authConfig(
|
|
1348
|
+
await this.client.request(
|
|
1349
|
+
this.path(projectId, "/auth"),
|
|
1350
|
+
{
|
|
1351
|
+
context: "Read sign-in settings"
|
|
1352
|
+
}
|
|
1353
|
+
)
|
|
1319
1354
|
);
|
|
1320
1355
|
}
|
|
1321
1356
|
/**
|
|
@@ -1324,34 +1359,93 @@ var BackendApi = class {
|
|
|
1324
1359
|
* (nothing is saved), `BACKEND_NOT_READY` before the first provision,
|
|
1325
1360
|
* `BACKEND_OFF` while the backend is turned off (nothing is saved),
|
|
1326
1361
|
* `VERIFICATION_NEEDS_EMAIL` for a state where nobody could sign in. A
|
|
1327
|
-
* failed upload
|
|
1328
|
-
*
|
|
1362
|
+
* failed upload throws `BACKEND_PROVISION_FAILED` (502): the settings are
|
|
1363
|
+
* saved, the message says whether the worker runs them yet, `details`
|
|
1364
|
+
* holds `{ auth, backend }`, and `enable` finishes. (A control plane from
|
|
1365
|
+
* before that resolves with `backend.status` `error` instead.) Two
|
|
1366
|
+
* minutes, like enable.
|
|
1329
1367
|
*/
|
|
1330
1368
|
async setAuth(projectId, patch) {
|
|
1331
|
-
|
|
1332
|
-
this.path(projectId, "/auth"),
|
|
1333
|
-
{
|
|
1369
|
+
return authChange(
|
|
1370
|
+
this.client.request(this.path(projectId, "/auth"), {
|
|
1334
1371
|
method: "PUT",
|
|
1335
1372
|
body: patch,
|
|
1336
1373
|
context: "Change sign-in settings",
|
|
1337
1374
|
timeoutMs: PROVISION_TIMEOUT_MS
|
|
1338
|
-
}
|
|
1375
|
+
})
|
|
1339
1376
|
);
|
|
1340
|
-
return { auth: answer.auth, backend: backendInfo(answer.backend) };
|
|
1341
1377
|
}
|
|
1342
1378
|
/** Removes email or one provider; the same answer as `setAuth`. */
|
|
1343
1379
|
async removeAuth(projectId, target) {
|
|
1380
|
+
return authChange(
|
|
1381
|
+
this.client.request(
|
|
1382
|
+
this.path(projectId, `/auth?target=${encodeURIComponent(target)}`),
|
|
1383
|
+
{
|
|
1384
|
+
method: "DELETE",
|
|
1385
|
+
context: "Change sign-in settings",
|
|
1386
|
+
timeoutMs: PROVISION_TIMEOUT_MS
|
|
1387
|
+
}
|
|
1388
|
+
)
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* The project's users, newest first (spec 2026-09-26 §2.2): page 1 and 50
|
|
1393
|
+
* a page unless given, at most 1000. `database.read`. `BACKEND_OFF` while
|
|
1394
|
+
* the backend is turned off, `BACKEND_NOT_READY` before it is ready,
|
|
1395
|
+
* `BACKEND_OUTDATED` while its Naro worker predates user management
|
|
1396
|
+
* (`enable` updates it), `BACKEND_UNAVAILABLE` when it cannot be reached.
|
|
1397
|
+
*/
|
|
1398
|
+
async listUsers(projectId, options2 = {}) {
|
|
1399
|
+
const query = new URLSearchParams();
|
|
1400
|
+
if (options2.page !== void 0) query.set("page", String(options2.page));
|
|
1401
|
+
if (options2.perPage !== void 0)
|
|
1402
|
+
query.set("perPage", String(options2.perPage));
|
|
1403
|
+
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
|
1404
|
+
const body = await this.client.request(
|
|
1405
|
+
this.path(projectId, `/users${suffix}`),
|
|
1406
|
+
{ context: "List users" }
|
|
1407
|
+
);
|
|
1408
|
+
return { users: body.users, total: body.total };
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Creates a user with a password, confirmed unless `emailConfirm: false`
|
|
1412
|
+
* — also while sign-ups are closed and while sign-in waits for a
|
|
1413
|
+
* confirmed email. A username-only user answers `email: null`.
|
|
1414
|
+
* `database.write`. The Naro worker's refusals come through as it said
|
|
1415
|
+
* them: `USERNAME_TAKEN` (409), `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`
|
|
1416
|
+
* (422), `PASSWORD_TOO_SHORT` ("Use at least {n} characters."),
|
|
1417
|
+
* `VALIDATION_ERROR`, `WRITE_QUOTA_EXCEEDED` (429, `details.retryAfter`).
|
|
1418
|
+
*/
|
|
1419
|
+
async createUser(projectId, input) {
|
|
1420
|
+
const body = { password: input.password };
|
|
1421
|
+
if (input.email !== void 0) body.email = input.email;
|
|
1422
|
+
if (input.username !== void 0) body.username = input.username;
|
|
1423
|
+
if (input.emailConfirm !== void 0)
|
|
1424
|
+
body.emailConfirm = input.emailConfirm;
|
|
1344
1425
|
const answer = await this.client.request(
|
|
1345
|
-
this.path(projectId,
|
|
1346
|
-
{
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1426
|
+
this.path(projectId, "/users"),
|
|
1427
|
+
{ method: "POST", body, context: "Create user" }
|
|
1428
|
+
);
|
|
1429
|
+
return answer.user;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Deletes a user with its sign-in methods, and ends its sessions. Answers
|
|
1433
|
+
* the user it was. `USER_NOT_FOUND` (404) for an id that is not one.
|
|
1434
|
+
* `database.write`.
|
|
1435
|
+
*/
|
|
1436
|
+
async deleteUser(projectId, userId) {
|
|
1437
|
+
const answer = await this.client.request(
|
|
1438
|
+
this.path(projectId, `/users/${encodeURIComponent(userId)}`),
|
|
1439
|
+
{ method: "DELETE", context: "Delete user" }
|
|
1351
1440
|
);
|
|
1352
|
-
return
|
|
1441
|
+
return answer.user;
|
|
1353
1442
|
}
|
|
1354
|
-
/**
|
|
1443
|
+
/**
|
|
1444
|
+
* Every table's policies as rules and conditions, one row per table —
|
|
1445
|
+
* the view `setPolicy` writes through (P2-7 §11.2). A policy that view
|
|
1446
|
+
* cannot show reads `{ invalid }`; `rlsPolicies` shows every one. Tables
|
|
1447
|
+
* without one deny anon and signed-in requests.
|
|
1448
|
+
*/
|
|
1355
1449
|
async policies(projectId) {
|
|
1356
1450
|
const body = await this.client.request(
|
|
1357
1451
|
this.path(projectId, "/policies"),
|
|
@@ -1375,8 +1469,9 @@ var BackendApi = class {
|
|
|
1375
1469
|
return answer.policy;
|
|
1376
1470
|
}
|
|
1377
1471
|
/**
|
|
1378
|
-
* Deletes the table
|
|
1379
|
-
*
|
|
1472
|
+
* Deletes every policy on the table (P2-7: the named ones too, whose
|
|
1473
|
+
* names the answer's `policies` lists), so it denies anon and signed-in
|
|
1474
|
+
* requests again. `POLICY_NOT_FOUND` when the table has none.
|
|
1380
1475
|
*/
|
|
1381
1476
|
async removePolicy(projectId, table) {
|
|
1382
1477
|
const answer = await this.client.request(
|
|
@@ -1385,6 +1480,75 @@ var BackendApi = class {
|
|
|
1385
1480
|
);
|
|
1386
1481
|
return answer.policy;
|
|
1387
1482
|
}
|
|
1483
|
+
/*
|
|
1484
|
+
* ---- Named policies (spec 2026-09-24 P2-7 §11.1) ----
|
|
1485
|
+
* As Postgres's CREATE POLICY: any number per table, each permissive or
|
|
1486
|
+
* restrictive, for a command and roles, with USING and WITH CHECK
|
|
1487
|
+
* expressions. The control plane checks them against the live schema
|
|
1488
|
+
* with the worker's own compiler; the worker picks a change up within
|
|
1489
|
+
* about 10 seconds.
|
|
1490
|
+
*
|
|
1491
|
+
* A write — `runPolicySql`, `replaceRlsPolicy`, `dropRlsPolicy`, and
|
|
1492
|
+
* `setPolicy` above — on a backend turned on before named policies is
|
|
1493
|
+
* `409 BACKEND_OUTDATED` "The project backend is older than named
|
|
1494
|
+
* policies. Run `naro backend enable` to update it, then try again."
|
|
1495
|
+
* (`backend.enable` also turns a backend that is off back on.) Reads
|
|
1496
|
+
* and `removePolicy` work there.
|
|
1497
|
+
*/
|
|
1498
|
+
/** The named policies, by table then name; only one table's with `table`. */
|
|
1499
|
+
async rlsPolicies(projectId, options2 = {}) {
|
|
1500
|
+
const query = options2.table === void 0 ? "" : `?table=${encodeURIComponent(options2.table)}`;
|
|
1501
|
+
const body = await this.client.request(
|
|
1502
|
+
this.path(projectId, `/rls/policies${query}`),
|
|
1503
|
+
{ context: "List policies" }
|
|
1504
|
+
);
|
|
1505
|
+
return body.policies;
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Runs one CREATE POLICY or DROP POLICY statement. A statement that does
|
|
1509
|
+
* not parse is `VALIDATION_FAILED` with Postgres's words and where
|
|
1510
|
+
* ("syntax error at or near … at character N."); a policy already there
|
|
1511
|
+
* is `POLICY_EXISTS`; DROP POLICY of none is `POLICY_NOT_FOUND` unless
|
|
1512
|
+
* it says IF EXISTS.
|
|
1513
|
+
*/
|
|
1514
|
+
async runPolicySql(projectId, sql, options2 = {}) {
|
|
1515
|
+
const answer = await this.client.request(this.path(projectId, `/rls/policies${writeQuery(options2)}`), {
|
|
1516
|
+
method: "POST",
|
|
1517
|
+
body: { sql },
|
|
1518
|
+
context: "Run policy statement"
|
|
1519
|
+
});
|
|
1520
|
+
return "policy" in answer ? { created: answer.policy } : { dropped: answer.dropped };
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Replaces the named policy with a CREATE POLICY on the same table,
|
|
1524
|
+
* which may rename it. `POLICY_NOT_FOUND` when there is none to replace.
|
|
1525
|
+
* `name` is the name as typed: the policy of exactly that name, else the
|
|
1526
|
+
* one of its ASCII lower case — what `CREATE POLICY MyPolicy` stored.
|
|
1527
|
+
*/
|
|
1528
|
+
async replaceRlsPolicy(projectId, table, name, sql, options2 = {}) {
|
|
1529
|
+
const answer = await this.client.request(
|
|
1530
|
+
this.path(
|
|
1531
|
+
projectId,
|
|
1532
|
+
`${this.namedPath(table, name)}${writeQuery(options2)}`
|
|
1533
|
+
),
|
|
1534
|
+
{ method: "PUT", body: { sql }, context: "Replace policy" }
|
|
1535
|
+
);
|
|
1536
|
+
return answer.policy;
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* DROP POLICY: answers the policy dropped; `POLICY_NOT_FOUND` when none.
|
|
1540
|
+
* `name` is the name as typed, as for `replaceRlsPolicy`.
|
|
1541
|
+
*/
|
|
1542
|
+
async dropRlsPolicy(projectId, table, name) {
|
|
1543
|
+
const answer = await this.client.request(
|
|
1544
|
+
this.path(projectId, this.namedPath(table, name)),
|
|
1545
|
+
{ method: "DELETE", context: "Drop policy" }
|
|
1546
|
+
);
|
|
1547
|
+
return answer.policy;
|
|
1548
|
+
}
|
|
1549
|
+
namedPath(table, name) {
|
|
1550
|
+
return `/rls/policies/${encodeURIComponent(table)}/${encodeURIComponent(name)}`;
|
|
1551
|
+
}
|
|
1388
1552
|
};
|
|
1389
1553
|
|
|
1390
1554
|
// ../../packages/sdk/src/database.ts
|
|
@@ -1422,6 +1586,10 @@ var SYSTEM_TABLE_NAMES = ["d1_migrations"];
|
|
|
1422
1586
|
function asciiLower(text) {
|
|
1423
1587
|
return text.replace(/[A-Z]+/g, (run) => run.toLowerCase());
|
|
1424
1588
|
}
|
|
1589
|
+
function isSystemTable(name) {
|
|
1590
|
+
const lower = asciiLower(name);
|
|
1591
|
+
return SYSTEM_TABLE_PREFIXES.some((prefix) => lower.startsWith(prefix)) || SYSTEM_TABLE_NAMES.includes(lower);
|
|
1592
|
+
}
|
|
1425
1593
|
function likeLiteral(text) {
|
|
1426
1594
|
return text.replace(/[\\%_]/g, (char) => `\\${char}`).replace(/'/g, "''");
|
|
1427
1595
|
}
|
|
@@ -1441,8 +1609,252 @@ function systemTablePredicate(column = "name") {
|
|
|
1441
1609
|
}
|
|
1442
1610
|
var SYSTEM_TABLE_SQL_PREDICATE = systemTablePredicate("name");
|
|
1443
1611
|
|
|
1444
|
-
// ../../packages/data-core/src/
|
|
1445
|
-
|
|
1612
|
+
// ../../packages/data-core/src/d1.ts
|
|
1613
|
+
function rowsOf(result) {
|
|
1614
|
+
const rows = result?.results ?? [];
|
|
1615
|
+
return rows.filter(
|
|
1616
|
+
(row) => typeof row === "object" && row !== null && !Array.isArray(row)
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// ../../packages/data-core/src/policy-lexer.ts
|
|
1621
|
+
var PolicySyntaxError = class extends Error {
|
|
1622
|
+
constructor(reason, position) {
|
|
1623
|
+
super(`${reason} at character ${String(position)}`);
|
|
1624
|
+
this.reason = reason;
|
|
1625
|
+
this.position = position;
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
var OPERATORS = [
|
|
1629
|
+
"->>",
|
|
1630
|
+
"->",
|
|
1631
|
+
"::",
|
|
1632
|
+
"<>",
|
|
1633
|
+
"!=",
|
|
1634
|
+
"<=",
|
|
1635
|
+
">=",
|
|
1636
|
+
"<@",
|
|
1637
|
+
"@>",
|
|
1638
|
+
"&&",
|
|
1639
|
+
"(",
|
|
1640
|
+
")",
|
|
1641
|
+
",",
|
|
1642
|
+
".",
|
|
1643
|
+
";",
|
|
1644
|
+
"=",
|
|
1645
|
+
"<",
|
|
1646
|
+
">",
|
|
1647
|
+
"[",
|
|
1648
|
+
"]",
|
|
1649
|
+
"*",
|
|
1650
|
+
"-"
|
|
1651
|
+
];
|
|
1652
|
+
function isSpace(char) {
|
|
1653
|
+
return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
|
|
1654
|
+
}
|
|
1655
|
+
function isIdentStart(char) {
|
|
1656
|
+
return /[A-Za-z_]/.test(char) || char.charCodeAt(0) >= 128;
|
|
1657
|
+
}
|
|
1658
|
+
function isIdentChar(char) {
|
|
1659
|
+
return /[A-Za-z0-9_$]/.test(char) || char.charCodeAt(0) >= 128;
|
|
1660
|
+
}
|
|
1661
|
+
function isDigit(char) {
|
|
1662
|
+
return char >= "0" && char <= "9";
|
|
1663
|
+
}
|
|
1664
|
+
var NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/;
|
|
1665
|
+
function tokenize(text) {
|
|
1666
|
+
const tokens = [];
|
|
1667
|
+
let index = 0;
|
|
1668
|
+
const at = (offset = 0) => text.charAt(index + offset);
|
|
1669
|
+
while (index < text.length) {
|
|
1670
|
+
const char = at();
|
|
1671
|
+
const position = index + 1;
|
|
1672
|
+
if (char === "\0")
|
|
1673
|
+
throw new PolicySyntaxError("NUL characters are not allowed", position);
|
|
1674
|
+
if (isSpace(char)) {
|
|
1675
|
+
index++;
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
if (char === "-" && at(1) === "-") {
|
|
1679
|
+
while (index < text.length && at() !== "\n" && at() !== "\r") index++;
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
if (char === "/" && at(1) === "*") {
|
|
1683
|
+
let depth = 0;
|
|
1684
|
+
while (index < text.length) {
|
|
1685
|
+
if (at() === "/" && at(1) === "*") {
|
|
1686
|
+
depth++;
|
|
1687
|
+
index += 2;
|
|
1688
|
+
} else if (at() === "*" && at(1) === "/") {
|
|
1689
|
+
depth--;
|
|
1690
|
+
index += 2;
|
|
1691
|
+
if (depth === 0) break;
|
|
1692
|
+
} else index++;
|
|
1693
|
+
}
|
|
1694
|
+
if (depth !== 0)
|
|
1695
|
+
throw new PolicySyntaxError("unterminated /* comment", position);
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
if (char === "'") {
|
|
1699
|
+
let value = "";
|
|
1700
|
+
index++;
|
|
1701
|
+
for (; ; ) {
|
|
1702
|
+
if (index >= text.length)
|
|
1703
|
+
throw new PolicySyntaxError("unterminated quoted string", position);
|
|
1704
|
+
if (at() === "'") {
|
|
1705
|
+
if (at(1) === "'") {
|
|
1706
|
+
value += "'";
|
|
1707
|
+
index += 2;
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
index++;
|
|
1711
|
+
break;
|
|
1712
|
+
}
|
|
1713
|
+
value += at();
|
|
1714
|
+
index++;
|
|
1715
|
+
}
|
|
1716
|
+
tokens.push({ kind: "string", value, quoted: false, position });
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
if (char === '"') {
|
|
1720
|
+
let value = "";
|
|
1721
|
+
index++;
|
|
1722
|
+
for (; ; ) {
|
|
1723
|
+
if (index >= text.length)
|
|
1724
|
+
throw new PolicySyntaxError(
|
|
1725
|
+
"unterminated quoted identifier",
|
|
1726
|
+
position
|
|
1727
|
+
);
|
|
1728
|
+
if (at() === '"') {
|
|
1729
|
+
if (at(1) === '"') {
|
|
1730
|
+
value += '"';
|
|
1731
|
+
index += 2;
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
index++;
|
|
1735
|
+
break;
|
|
1736
|
+
}
|
|
1737
|
+
value += at();
|
|
1738
|
+
index++;
|
|
1739
|
+
}
|
|
1740
|
+
if (value === "")
|
|
1741
|
+
throw new PolicySyntaxError("zero-length quoted identifier", position);
|
|
1742
|
+
tokens.push({ kind: "ident", value, quoted: true, position });
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
if (char === "$")
|
|
1746
|
+
throw new PolicySyntaxError(
|
|
1747
|
+
"dollar-quoted strings are not supported: use '\u2026'",
|
|
1748
|
+
position
|
|
1749
|
+
);
|
|
1750
|
+
if (isDigit(char) || char === "." && isDigit(at(1))) {
|
|
1751
|
+
const match = NUMBER.exec(text.slice(index));
|
|
1752
|
+
const value = match?.[0] ?? char;
|
|
1753
|
+
index += value.length;
|
|
1754
|
+
if (index < text.length && isIdentChar(at()))
|
|
1755
|
+
throw new PolicySyntaxError(
|
|
1756
|
+
`trailing junk after numeric literal ${value}`,
|
|
1757
|
+
position
|
|
1758
|
+
);
|
|
1759
|
+
tokens.push({ kind: "number", value, quoted: false, position });
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (isIdentStart(char)) {
|
|
1763
|
+
let end = index + 1;
|
|
1764
|
+
while (end < text.length && isIdentChar(text.charAt(end))) end++;
|
|
1765
|
+
const value = text.slice(index, end);
|
|
1766
|
+
if (text.charAt(end) === "'")
|
|
1767
|
+
throw new PolicySyntaxError(
|
|
1768
|
+
`${value}'\u2026' strings are not supported: use a plain '\u2026' string`,
|
|
1769
|
+
position
|
|
1770
|
+
);
|
|
1771
|
+
index = end;
|
|
1772
|
+
tokens.push({ kind: "ident", value, quoted: false, position });
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
const operator = OPERATORS.find(
|
|
1776
|
+
(candidate) => text.startsWith(candidate, index)
|
|
1777
|
+
);
|
|
1778
|
+
if (!operator)
|
|
1779
|
+
throw new PolicySyntaxError(
|
|
1780
|
+
`unexpected character ${JSON.stringify(char)}`,
|
|
1781
|
+
position
|
|
1782
|
+
);
|
|
1783
|
+
index += operator.length;
|
|
1784
|
+
tokens.push({ kind: "op", value: operator, quoted: false, position });
|
|
1785
|
+
}
|
|
1786
|
+
tokens.push({
|
|
1787
|
+
kind: "eof",
|
|
1788
|
+
value: "",
|
|
1789
|
+
quoted: false,
|
|
1790
|
+
position: text.length + 1
|
|
1791
|
+
});
|
|
1792
|
+
return tokens;
|
|
1793
|
+
}
|
|
1794
|
+
function isKeyword(token, word) {
|
|
1795
|
+
return token !== void 0 && token.kind === "ident" && !token.quoted && asciiLower(token.value) === word;
|
|
1796
|
+
}
|
|
1797
|
+
function isOperator(token, op) {
|
|
1798
|
+
return token !== void 0 && token.kind === "op" && token.value === op;
|
|
1799
|
+
}
|
|
1800
|
+
function describeToken(token) {
|
|
1801
|
+
if (token.kind === "eof") return "end of input";
|
|
1802
|
+
if (token.kind === "string") return `'${token.value}'`;
|
|
1803
|
+
if (token.kind === "ident" && token.quoted) return `"${token.value}"`;
|
|
1804
|
+
return `"${token.value}"`;
|
|
1805
|
+
}
|
|
1806
|
+
var TokenStream = class {
|
|
1807
|
+
constructor(tokens) {
|
|
1808
|
+
this.tokens = tokens;
|
|
1809
|
+
}
|
|
1810
|
+
#index = 0;
|
|
1811
|
+
peek(offset = 0) {
|
|
1812
|
+
const token = this.tokens[Math.min(this.#index + offset, this.tokens.length - 1)];
|
|
1813
|
+
if (!token) throw new Error("TokenStream: no eof token");
|
|
1814
|
+
return token;
|
|
1815
|
+
}
|
|
1816
|
+
next() {
|
|
1817
|
+
const token = this.peek();
|
|
1818
|
+
if (token.kind !== "eof") this.#index++;
|
|
1819
|
+
return token;
|
|
1820
|
+
}
|
|
1821
|
+
/** Takes the keyword when it is next. */
|
|
1822
|
+
acceptKeyword(word) {
|
|
1823
|
+
if (!isKeyword(this.peek(), word)) return false;
|
|
1824
|
+
this.next();
|
|
1825
|
+
return true;
|
|
1826
|
+
}
|
|
1827
|
+
acceptOperator(op) {
|
|
1828
|
+
if (!isOperator(this.peek(), op)) return false;
|
|
1829
|
+
this.next();
|
|
1830
|
+
return true;
|
|
1831
|
+
}
|
|
1832
|
+
expectKeyword(word) {
|
|
1833
|
+
const token = this.peek();
|
|
1834
|
+
if (!isKeyword(token, word))
|
|
1835
|
+
throw new PolicySyntaxError(
|
|
1836
|
+
`expected ${word.toUpperCase()}, found ${describeToken(token)}`,
|
|
1837
|
+
token.position
|
|
1838
|
+
);
|
|
1839
|
+
return this.next();
|
|
1840
|
+
}
|
|
1841
|
+
expectOperator(op) {
|
|
1842
|
+
const token = this.peek();
|
|
1843
|
+
if (!isOperator(token, op))
|
|
1844
|
+
throw new PolicySyntaxError(
|
|
1845
|
+
`expected ${op}, found ${describeToken(token)}`,
|
|
1846
|
+
token.position
|
|
1847
|
+
);
|
|
1848
|
+
return this.next();
|
|
1849
|
+
}
|
|
1850
|
+
/** Postgres's own words for anything else. */
|
|
1851
|
+
unexpected(token = this.peek()) {
|
|
1852
|
+
throw new PolicySyntaxError(
|
|
1853
|
+
token.kind === "eof" ? "syntax error at end of input" : `syntax error at or near ${describeToken(token)}`,
|
|
1854
|
+
token.position
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1446
1858
|
|
|
1447
1859
|
// ../../packages/data-core/src/schema.ts
|
|
1448
1860
|
var SCHEMA_SQL = [
|
|
@@ -1453,11 +1865,1763 @@ var SCHEMA_SQL = [
|
|
|
1453
1865
|
// `sqlite_master` reads (the database routes' `_tables.ts` reads it too).
|
|
1454
1866
|
`SELECT name AS table_name, sql AS table_sql FROM sqlite_master WHERE type = 'table' AND ${systemTablePredicate("name")}`
|
|
1455
1867
|
];
|
|
1868
|
+
var TABLE_NAMES_SQL = `SELECT name AS table_name FROM pragma_table_list WHERE schema = 'main' AND type IN ('table', 'virtual') AND ${systemTablePredicate("name")}`;
|
|
1869
|
+
function tableNamesFromRows(rows) {
|
|
1870
|
+
const names = [];
|
|
1871
|
+
for (const row of rows) {
|
|
1872
|
+
const name = row.table_name;
|
|
1873
|
+
if (typeof name === "string" && name !== "" && !isSystemTable(name))
|
|
1874
|
+
names.push(name);
|
|
1875
|
+
}
|
|
1876
|
+
return names;
|
|
1877
|
+
}
|
|
1878
|
+
async function readTableNames(db) {
|
|
1879
|
+
return tableNamesFromRows(rowsOf(await db.prepare(TABLE_NAMES_SQL).all()));
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// ../../packages/data-core/src/values.ts
|
|
1883
|
+
function isPlainObject(value) {
|
|
1884
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1885
|
+
return false;
|
|
1886
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1887
|
+
return prototype === Object.prototype || prototype === null;
|
|
1888
|
+
}
|
|
1889
|
+
function isValue(value) {
|
|
1890
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
1891
|
+
}
|
|
1892
|
+
function utf8ByteLength(text) {
|
|
1893
|
+
let bytes = 0;
|
|
1894
|
+
for (let index = 0; index < text.length; index++) {
|
|
1895
|
+
const unit = text.charCodeAt(index);
|
|
1896
|
+
if (unit < 128) bytes += 1;
|
|
1897
|
+
else if (unit < 2048) bytes += 2;
|
|
1898
|
+
else if (unit >= 55296 && unit <= 56319 && index + 1 < text.length) {
|
|
1899
|
+
const next = text.charCodeAt(index + 1);
|
|
1900
|
+
if (next >= 56320 && next <= 57343) {
|
|
1901
|
+
bytes += 4;
|
|
1902
|
+
index++;
|
|
1903
|
+
} else bytes += 3;
|
|
1904
|
+
} else bytes += 3;
|
|
1905
|
+
}
|
|
1906
|
+
return bytes;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// ../../packages/data-core/src/sql-fragments.ts
|
|
1910
|
+
function globLiteral(char) {
|
|
1911
|
+
return char === "*" || char === "?" || char === "[" ? `[${char}]` : char;
|
|
1912
|
+
}
|
|
1913
|
+
function likeToGlob(pattern) {
|
|
1914
|
+
let glob = "";
|
|
1915
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
1916
|
+
const char = pattern.charAt(index);
|
|
1917
|
+
if (char === "\\" && index + 1 < pattern.length) {
|
|
1918
|
+
index++;
|
|
1919
|
+
glob += globLiteral(pattern.charAt(index));
|
|
1920
|
+
} else if (char === "%") glob += "*";
|
|
1921
|
+
else if (char === "_") glob += "?";
|
|
1922
|
+
else glob += globLiteral(char);
|
|
1923
|
+
}
|
|
1924
|
+
return glob;
|
|
1925
|
+
}
|
|
1926
|
+
var MAX_PATTERN_BYTES = 50;
|
|
1927
|
+
function boundPattern(pattern, caseInsensitive) {
|
|
1928
|
+
return caseInsensitive ? pattern : likeToGlob(pattern);
|
|
1929
|
+
}
|
|
1930
|
+
function patternTooLong(pattern, caseInsensitive) {
|
|
1931
|
+
if (pattern.length > MAX_PATTERN_BYTES * 2) return true;
|
|
1932
|
+
return utf8ByteLength(boundPattern(pattern, caseInsensitive)) > MAX_PATTERN_BYTES;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// ../../packages/data-core/src/policy-expr.ts
|
|
1936
|
+
var MAX_EXPRESSION_BYTES = 3 * 16384;
|
|
1937
|
+
var MAX_EXPRESSION_NODES = 1600;
|
|
1938
|
+
var MAX_EXPRESSION_DEPTH = 32;
|
|
1939
|
+
var MAX_SUBQUERIES = 4;
|
|
1940
|
+
var MAX_SUBQUERY_DEPTH = 2;
|
|
1941
|
+
var MAX_ARRAY_OPERATORS = 4;
|
|
1942
|
+
var MAX_POLICY_ARRAY_VALUES = 100;
|
|
1943
|
+
var MAX_IN_VALUES = 100;
|
|
1944
|
+
var MAX_POLICY_PATTERN_LENGTH = MAX_PATTERN_BYTES;
|
|
1945
|
+
var JWT_CLAIMS = ["sub", "role", "email", "email_verified"];
|
|
1946
|
+
var POLICY_FUNCTIONS = [
|
|
1947
|
+
"lower",
|
|
1948
|
+
"upper",
|
|
1949
|
+
"length",
|
|
1950
|
+
"coalesce",
|
|
1951
|
+
"now"
|
|
1952
|
+
];
|
|
1953
|
+
var VALUE_KINDS = /* @__PURE__ */ new Set([
|
|
1954
|
+
"literal",
|
|
1955
|
+
"column",
|
|
1956
|
+
"auth",
|
|
1957
|
+
"jwt",
|
|
1958
|
+
"func",
|
|
1959
|
+
"cast"
|
|
1960
|
+
]);
|
|
1961
|
+
function isValueExpr(expr) {
|
|
1962
|
+
return VALUE_KINDS.has(expr.kind);
|
|
1963
|
+
}
|
|
1964
|
+
function isConditionExpr(expr) {
|
|
1965
|
+
if (!isValueExpr(expr)) return true;
|
|
1966
|
+
if (expr.kind === "column") return true;
|
|
1967
|
+
return expr.kind === "literal" && (expr.value === null || typeof expr.value === "boolean");
|
|
1968
|
+
}
|
|
1969
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
1970
|
+
"all",
|
|
1971
|
+
"and",
|
|
1972
|
+
"array",
|
|
1973
|
+
"as",
|
|
1974
|
+
"between",
|
|
1975
|
+
"check",
|
|
1976
|
+
"current_timestamp",
|
|
1977
|
+
"distinct",
|
|
1978
|
+
"exists",
|
|
1979
|
+
"false",
|
|
1980
|
+
"for",
|
|
1981
|
+
`from`,
|
|
1982
|
+
"ilike",
|
|
1983
|
+
"in",
|
|
1984
|
+
"is",
|
|
1985
|
+
"like",
|
|
1986
|
+
"not",
|
|
1987
|
+
"null",
|
|
1988
|
+
"on",
|
|
1989
|
+
"or",
|
|
1990
|
+
"select",
|
|
1991
|
+
"to",
|
|
1992
|
+
"true",
|
|
1993
|
+
"using",
|
|
1994
|
+
"where",
|
|
1995
|
+
"with"
|
|
1996
|
+
]);
|
|
1997
|
+
var CLAUSE_WORDS = /* @__PURE__ */ new Set([
|
|
1998
|
+
"join",
|
|
1999
|
+
"group",
|
|
2000
|
+
"order",
|
|
2001
|
+
"limit",
|
|
2002
|
+
"union",
|
|
2003
|
+
"having",
|
|
2004
|
+
"left",
|
|
2005
|
+
"inner",
|
|
2006
|
+
"cross"
|
|
2007
|
+
]);
|
|
2008
|
+
var SPECIAL_WORDS = /* @__PURE__ */ new Set([
|
|
2009
|
+
...RESERVED,
|
|
2010
|
+
...CLAUSE_WORDS,
|
|
2011
|
+
"symmetric",
|
|
2012
|
+
"auth"
|
|
2013
|
+
]);
|
|
2014
|
+
var TEXT_TYPES = /* @__PURE__ */ new Set(["text", "varchar", "uuid"]);
|
|
2015
|
+
function count(state, token) {
|
|
2016
|
+
if (++state.nodes > MAX_EXPRESSION_NODES)
|
|
2017
|
+
throw new PolicySyntaxError(
|
|
2018
|
+
`the expression has more than ${String(MAX_EXPRESSION_NODES)} parts`,
|
|
2019
|
+
token.position
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
function nested(state, token, read) {
|
|
2023
|
+
if (++state.depth > MAX_EXPRESSION_DEPTH)
|
|
2024
|
+
throw new PolicySyntaxError(
|
|
2025
|
+
`the expression nests deeper than ${String(MAX_EXPRESSION_DEPTH)} levels`,
|
|
2026
|
+
token.position
|
|
2027
|
+
);
|
|
2028
|
+
try {
|
|
2029
|
+
return read();
|
|
2030
|
+
} finally {
|
|
2031
|
+
state.depth--;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
function truthProblem(operand) {
|
|
2035
|
+
const text = "is text, where 'true' reads as false";
|
|
2036
|
+
switch (operand.kind) {
|
|
2037
|
+
case "column":
|
|
2038
|
+
return void 0;
|
|
2039
|
+
case "literal":
|
|
2040
|
+
return typeof operand.value === "string" ? `a '\u2026' string ${text}: write TRUE or FALSE, or compare the string with =` : void 0;
|
|
2041
|
+
case "jwt": {
|
|
2042
|
+
const claim = printValue(operand);
|
|
2043
|
+
return `${claim} ${text}: compare it with text, ${claim} = 'true'`;
|
|
2044
|
+
}
|
|
2045
|
+
case "auth":
|
|
2046
|
+
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'";
|
|
2047
|
+
case "cast":
|
|
2048
|
+
return `a ::text cast ${text}: compare it with text, (\u2026)::text = 'true'`;
|
|
2049
|
+
case "func":
|
|
2050
|
+
switch (operand.name) {
|
|
2051
|
+
case "lower":
|
|
2052
|
+
case "upper":
|
|
2053
|
+
return `${operand.name}(\u2026) ${text}: compare it with text, ${operand.name}(\u2026) = 'true'`;
|
|
2054
|
+
case "coalesce":
|
|
2055
|
+
return "coalesce(\u2026) is not a boolean: compare it, coalesce(\u2026) = 'true'";
|
|
2056
|
+
case "length":
|
|
2057
|
+
return "length(\u2026) is a number: compare it, length(\u2026) > 0";
|
|
2058
|
+
case "now":
|
|
2059
|
+
return "now() is a time: compare it, created_at < now()";
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
function truthTestProblem(operand) {
|
|
2064
|
+
const problem = truthProblem(operand);
|
|
2065
|
+
return problem === void 0 ? void 0 : `IS TRUE and IS FALSE test a boolean, and ${problem}`;
|
|
2066
|
+
}
|
|
2067
|
+
function operandValues(operand) {
|
|
2068
|
+
return operand.kind === "func" && operand.name === "coalesce" ? operand.args.flatMap(operandValues) : [operand];
|
|
2069
|
+
}
|
|
2070
|
+
function isBooleanConstant(value) {
|
|
2071
|
+
return value.kind === "literal" && typeof value.value === "boolean";
|
|
2072
|
+
}
|
|
2073
|
+
function isTextValue(value) {
|
|
2074
|
+
switch (value.kind) {
|
|
2075
|
+
case "literal":
|
|
2076
|
+
return typeof value.value === "string";
|
|
2077
|
+
case "auth":
|
|
2078
|
+
case "jwt":
|
|
2079
|
+
case "cast":
|
|
2080
|
+
return true;
|
|
2081
|
+
case "func":
|
|
2082
|
+
return value.name !== "length" && value.name !== "coalesce";
|
|
2083
|
+
case "column":
|
|
2084
|
+
return false;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
function pair(left, right, inner = false) {
|
|
2088
|
+
return { left, right, inner };
|
|
2089
|
+
}
|
|
2090
|
+
function comparedPairs(expr) {
|
|
2091
|
+
switch (expr.kind) {
|
|
2092
|
+
case "compare":
|
|
2093
|
+
case "distinct":
|
|
2094
|
+
return [pair(expr.left, expr.right)];
|
|
2095
|
+
case "in":
|
|
2096
|
+
return expr.list.map((item) => pair(expr.operand, item));
|
|
2097
|
+
case "inSelect":
|
|
2098
|
+
return expr.subquery.select === "*" ? [] : [pair(expr.operand, expr.subquery.select, true)];
|
|
2099
|
+
case "between":
|
|
2100
|
+
return [pair(expr.operand, expr.low), pair(expr.operand, expr.high)];
|
|
2101
|
+
case "like":
|
|
2102
|
+
return [pair(expr.operand, { kind: "literal", value: expr.pattern })];
|
|
2103
|
+
default:
|
|
2104
|
+
return [];
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
function booleanComparisonProblem(expr) {
|
|
2108
|
+
for (const { left, right } of comparedPairs(expr)) {
|
|
2109
|
+
const met = [...operandValues(left), ...operandValues(right)];
|
|
2110
|
+
if (!met.some(isBooleanConstant)) continue;
|
|
2111
|
+
const texts = met.filter(isTextValue);
|
|
2112
|
+
const text = texts.find((value) => value.kind !== "literal") ?? texts[0];
|
|
2113
|
+
if (text === void 0) continue;
|
|
2114
|
+
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);
|
|
2115
|
+
return `a comparison with TRUE or FALSE compares a boolean, and ${String(problem)}`;
|
|
2116
|
+
}
|
|
2117
|
+
return void 0;
|
|
2118
|
+
}
|
|
2119
|
+
function comparable(expr, token) {
|
|
2120
|
+
const problem = booleanComparisonProblem(expr);
|
|
2121
|
+
if (problem !== void 0)
|
|
2122
|
+
throw new PolicySyntaxError(problem, token.position);
|
|
2123
|
+
return expr;
|
|
2124
|
+
}
|
|
2125
|
+
function requireCondition(expr, token) {
|
|
2126
|
+
if (!isConditionExpr(expr)) {
|
|
2127
|
+
const problem = isValueExpr(expr) ? truthProblem(expr) : void 0;
|
|
2128
|
+
throw new PolicySyntaxError(
|
|
2129
|
+
`this is a value, not a condition: ${problem ?? "compare it with =, IN, LIKE, IS \u2026 (a column alone reads as its truth value)"}`,
|
|
2130
|
+
token.position
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
return expr;
|
|
2134
|
+
}
|
|
2135
|
+
function requireValue(expr, token) {
|
|
2136
|
+
if (!isValueExpr(expr))
|
|
2137
|
+
throw new PolicySyntaxError(
|
|
2138
|
+
"a comparison compares values: combine conditions with AND, OR and NOT",
|
|
2139
|
+
token.position
|
|
2140
|
+
);
|
|
2141
|
+
return expr;
|
|
2142
|
+
}
|
|
2143
|
+
function readOr(state) {
|
|
2144
|
+
const first = state.stream.peek();
|
|
2145
|
+
const args = [readAnd(state)];
|
|
2146
|
+
while (isKeyword(state.stream.peek(), "or")) {
|
|
2147
|
+
const token = state.stream.next();
|
|
2148
|
+
count(state, token);
|
|
2149
|
+
args.push(readAnd(state));
|
|
2150
|
+
}
|
|
2151
|
+
if (args.length === 1) return args[0];
|
|
2152
|
+
for (const arg of args) requireCondition(arg, first);
|
|
2153
|
+
return { kind: "or", args };
|
|
2154
|
+
}
|
|
2155
|
+
function readAnd(state) {
|
|
2156
|
+
const first = state.stream.peek();
|
|
2157
|
+
const args = [readNot(state)];
|
|
2158
|
+
while (isKeyword(state.stream.peek(), "and")) {
|
|
2159
|
+
const token = state.stream.next();
|
|
2160
|
+
count(state, token);
|
|
2161
|
+
args.push(readNot(state));
|
|
2162
|
+
}
|
|
2163
|
+
if (args.length === 1) return args[0];
|
|
2164
|
+
for (const arg of args) requireCondition(arg, first);
|
|
2165
|
+
return { kind: "and", args };
|
|
2166
|
+
}
|
|
2167
|
+
function readNot(state) {
|
|
2168
|
+
const token = state.stream.peek();
|
|
2169
|
+
if (isKeyword(token, "not")) {
|
|
2170
|
+
state.stream.next();
|
|
2171
|
+
count(state, token);
|
|
2172
|
+
return nested(state, token, () => {
|
|
2173
|
+
const inner = state.stream.peek();
|
|
2174
|
+
return { kind: "not", arg: requireCondition(readNot(state), inner) };
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
return readPredicate(state);
|
|
2178
|
+
}
|
|
2179
|
+
function readPredicate(state) {
|
|
2180
|
+
const { stream } = state;
|
|
2181
|
+
const token = stream.peek();
|
|
2182
|
+
if (isKeyword(token, "exists")) {
|
|
2183
|
+
stream.next();
|
|
2184
|
+
count(state, token);
|
|
2185
|
+
stream.expectOperator("(");
|
|
2186
|
+
const subquery = readSubquery(state, token, "exists");
|
|
2187
|
+
stream.expectOperator(")");
|
|
2188
|
+
return { kind: "exists", subquery };
|
|
2189
|
+
}
|
|
2190
|
+
const left = readValue(state);
|
|
2191
|
+
const next = stream.peek();
|
|
2192
|
+
const compare = ["=", "<>", "!=", "<", "<=", ">", ">="].find(
|
|
2193
|
+
(op) => isOperator(next, op)
|
|
2194
|
+
);
|
|
2195
|
+
if (compare) {
|
|
2196
|
+
stream.next();
|
|
2197
|
+
count(state, next);
|
|
2198
|
+
const right = readValue(state);
|
|
2199
|
+
return comparable(
|
|
2200
|
+
{
|
|
2201
|
+
kind: "compare",
|
|
2202
|
+
op: compare === "!=" ? "<>" : compare,
|
|
2203
|
+
left: requireValue(left, token),
|
|
2204
|
+
right: requireValue(right, next)
|
|
2205
|
+
},
|
|
2206
|
+
next
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
if (isKeyword(next, "is")) {
|
|
2210
|
+
stream.next();
|
|
2211
|
+
count(state, next);
|
|
2212
|
+
const operand = requireValue(left, token);
|
|
2213
|
+
const not2 = stream.acceptKeyword("not");
|
|
2214
|
+
if (stream.acceptKeyword("null"))
|
|
2215
|
+
return { kind: "is", not: not2, operand, value: null };
|
|
2216
|
+
const truth = stream.acceptKeyword("true") ? true : stream.acceptKeyword("false") ? false : void 0;
|
|
2217
|
+
if (truth !== void 0) {
|
|
2218
|
+
const problem = truthTestProblem(operand);
|
|
2219
|
+
if (problem !== void 0)
|
|
2220
|
+
throw new PolicySyntaxError(problem, next.position);
|
|
2221
|
+
return { kind: "is", not: not2, operand, value: truth };
|
|
2222
|
+
}
|
|
2223
|
+
if (stream.acceptKeyword("distinct")) {
|
|
2224
|
+
stream.expectKeyword(`from`);
|
|
2225
|
+
const right = requireValue(readValue(state), stream.peek());
|
|
2226
|
+
return comparable({ kind: "distinct", not: not2, left: operand, right }, next);
|
|
2227
|
+
}
|
|
2228
|
+
stream.unexpected();
|
|
2229
|
+
}
|
|
2230
|
+
if (isOperator(next, "@>") || isOperator(next, "<@") || isOperator(next, "&&"))
|
|
2231
|
+
return readArray(state, left, token);
|
|
2232
|
+
let not = false;
|
|
2233
|
+
let word = next;
|
|
2234
|
+
if (isKeyword(next, "not")) {
|
|
2235
|
+
const after = stream.peek(1);
|
|
2236
|
+
if (isKeyword(after, "in") || isKeyword(after, "like") || isKeyword(after, "ilike") || isKeyword(after, "between")) {
|
|
2237
|
+
stream.next();
|
|
2238
|
+
not = true;
|
|
2239
|
+
word = after;
|
|
2240
|
+
} else return left;
|
|
2241
|
+
}
|
|
2242
|
+
if (isKeyword(word, "in")) {
|
|
2243
|
+
stream.next();
|
|
2244
|
+
count(state, word);
|
|
2245
|
+
const operand = requireValue(left, token);
|
|
2246
|
+
stream.expectOperator("(");
|
|
2247
|
+
if (isKeyword(stream.peek(), "select")) {
|
|
2248
|
+
const subquery = readSubquery(state, word, "in");
|
|
2249
|
+
stream.expectOperator(")");
|
|
2250
|
+
return comparable({ kind: "inSelect", not, operand, subquery }, word);
|
|
2251
|
+
}
|
|
2252
|
+
const list = [];
|
|
2253
|
+
do {
|
|
2254
|
+
if (list.length >= MAX_IN_VALUES)
|
|
2255
|
+
throw new PolicySyntaxError(
|
|
2256
|
+
`IN lists at most ${String(MAX_IN_VALUES)} values`,
|
|
2257
|
+
stream.peek().position
|
|
2258
|
+
);
|
|
2259
|
+
const item = stream.peek();
|
|
2260
|
+
list.push(requireValue(readValue(state), item));
|
|
2261
|
+
} while (stream.acceptOperator(","));
|
|
2262
|
+
stream.expectOperator(")");
|
|
2263
|
+
return comparable({ kind: "in", not, operand, list }, word);
|
|
2264
|
+
}
|
|
2265
|
+
if (isKeyword(word, "like") || isKeyword(word, "ilike")) {
|
|
2266
|
+
stream.next();
|
|
2267
|
+
count(state, word);
|
|
2268
|
+
const operand = requireValue(left, token);
|
|
2269
|
+
const pattern = stream.peek();
|
|
2270
|
+
if (pattern.kind !== "string")
|
|
2271
|
+
throw new PolicySyntaxError(
|
|
2272
|
+
"a LIKE or ILIKE pattern must be a '\u2026' string",
|
|
2273
|
+
pattern.position
|
|
2274
|
+
);
|
|
2275
|
+
stream.next();
|
|
2276
|
+
const ci = isKeyword(word, "ilike");
|
|
2277
|
+
checkPattern(pattern, ci);
|
|
2278
|
+
return comparable(
|
|
2279
|
+
{ kind: "like", not, ci, operand, pattern: pattern.value },
|
|
2280
|
+
word
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
if (isKeyword(word, "between")) {
|
|
2284
|
+
stream.next();
|
|
2285
|
+
count(state, word);
|
|
2286
|
+
const operand = requireValue(left, token);
|
|
2287
|
+
if (stream.acceptKeyword("symmetric"))
|
|
2288
|
+
throw new PolicySyntaxError(
|
|
2289
|
+
"BETWEEN SYMMETRIC is not supported",
|
|
2290
|
+
word.position
|
|
2291
|
+
);
|
|
2292
|
+
const low = requireValue(readValue(state), stream.peek());
|
|
2293
|
+
stream.expectKeyword("and");
|
|
2294
|
+
const high = requireValue(readValue(state), stream.peek());
|
|
2295
|
+
return comparable({ kind: "between", not, operand, low, high }, word);
|
|
2296
|
+
}
|
|
2297
|
+
return left;
|
|
2298
|
+
}
|
|
2299
|
+
var PATTERN_SHOWN = 24;
|
|
2300
|
+
function policyPatternProblem(pattern, ci) {
|
|
2301
|
+
if (!patternTooLong(pattern, ci)) return void 0;
|
|
2302
|
+
const chars = [...pattern];
|
|
2303
|
+
const shown = `${commentText(chars.slice(0, PATTERN_SHOWN).join(""))}${chars.length > PATTERN_SHOWN ? "\u2026" : ""}`;
|
|
2304
|
+
const bytes = utf8ByteLength(boundPattern(pattern, ci));
|
|
2305
|
+
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)"}`;
|
|
2306
|
+
}
|
|
2307
|
+
function checkPattern(token, ci) {
|
|
2308
|
+
const pattern = token.value;
|
|
2309
|
+
const problem = policyPatternProblem(pattern, ci);
|
|
2310
|
+
if (problem !== void 0)
|
|
2311
|
+
throw new PolicySyntaxError(problem, token.position);
|
|
2312
|
+
let run = 0;
|
|
2313
|
+
for (let index = pattern.length - 1; index >= 0; index--) {
|
|
2314
|
+
if (pattern[index] !== "\\") break;
|
|
2315
|
+
run++;
|
|
2316
|
+
}
|
|
2317
|
+
if (run % 2 === 1)
|
|
2318
|
+
throw new PolicySyntaxError(
|
|
2319
|
+
"a LIKE pattern must not end with an unescaped backslash",
|
|
2320
|
+
token.position
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
function readArray(state, left, start) {
|
|
2324
|
+
const { stream } = state;
|
|
2325
|
+
const opToken = stream.next();
|
|
2326
|
+
count(state, opToken);
|
|
2327
|
+
if (left.kind !== "column")
|
|
2328
|
+
throw new PolicySyntaxError(
|
|
2329
|
+
`the left side of ${opToken.value} must be a column`,
|
|
2330
|
+
start.position
|
|
2331
|
+
);
|
|
2332
|
+
if (++state.arrays > MAX_ARRAY_OPERATORS)
|
|
2333
|
+
throw new PolicySyntaxError(
|
|
2334
|
+
`an expression may use @>, <@ and && at most ${String(MAX_ARRAY_OPERATORS)} times`,
|
|
2335
|
+
opToken.position
|
|
2336
|
+
);
|
|
2337
|
+
stream.expectKeyword("array");
|
|
2338
|
+
stream.expectOperator("[");
|
|
2339
|
+
const values = [];
|
|
2340
|
+
do {
|
|
2341
|
+
const item = stream.peek();
|
|
2342
|
+
if (values.length >= MAX_POLICY_ARRAY_VALUES)
|
|
2343
|
+
throw new PolicySyntaxError(
|
|
2344
|
+
`ARRAY[\u2026] lists at most ${String(MAX_POLICY_ARRAY_VALUES)} values`,
|
|
2345
|
+
item.position
|
|
2346
|
+
);
|
|
2347
|
+
const value = readValue(state);
|
|
2348
|
+
if (value.kind === "auth" && value.fn === "uid") values.push(value);
|
|
2349
|
+
else if (value.kind === "literal" && (typeof value.value === "string" || typeof value.value === "number"))
|
|
2350
|
+
values.push(value.value);
|
|
2351
|
+
else
|
|
2352
|
+
throw new PolicySyntaxError(
|
|
2353
|
+
"ARRAY[\u2026] holds strings, numbers and auth.uid()",
|
|
2354
|
+
item.position
|
|
2355
|
+
);
|
|
2356
|
+
} while (stream.acceptOperator(","));
|
|
2357
|
+
stream.expectOperator("]");
|
|
2358
|
+
return {
|
|
2359
|
+
kind: "array",
|
|
2360
|
+
op: opToken.value,
|
|
2361
|
+
operand: left,
|
|
2362
|
+
values
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
function readSubquery(state, start, kind) {
|
|
2366
|
+
const { stream } = state;
|
|
2367
|
+
if (++state.subqueries > MAX_SUBQUERIES)
|
|
2368
|
+
throw new PolicySyntaxError(
|
|
2369
|
+
`an expression may have at most ${String(MAX_SUBQUERIES)} subqueries`,
|
|
2370
|
+
start.position
|
|
2371
|
+
);
|
|
2372
|
+
if (++state.subqueryDepth > MAX_SUBQUERY_DEPTH)
|
|
2373
|
+
throw new PolicySyntaxError(
|
|
2374
|
+
`subqueries nest at most ${String(MAX_SUBQUERY_DEPTH)} deep`,
|
|
2375
|
+
start.position
|
|
2376
|
+
);
|
|
2377
|
+
try {
|
|
2378
|
+
stream.expectKeyword("select");
|
|
2379
|
+
let select;
|
|
2380
|
+
const item = stream.peek();
|
|
2381
|
+
if (stream.acceptOperator("*")) {
|
|
2382
|
+
if (kind === "in")
|
|
2383
|
+
throw new PolicySyntaxError(
|
|
2384
|
+
"IN (SELECT \u2026) selects one value, not *",
|
|
2385
|
+
item.position
|
|
2386
|
+
);
|
|
2387
|
+
select = "*";
|
|
2388
|
+
} else select = requireValue(readValue(state), item);
|
|
2389
|
+
if (isOperator(stream.peek(), ","))
|
|
2390
|
+
throw new PolicySyntaxError(
|
|
2391
|
+
"a subquery here selects one value",
|
|
2392
|
+
stream.peek().position
|
|
2393
|
+
);
|
|
2394
|
+
stream.expectKeyword(`from`);
|
|
2395
|
+
const table = readTableName(stream);
|
|
2396
|
+
let alias = null;
|
|
2397
|
+
if (stream.acceptKeyword("as")) alias = readIdentifier(stream, "an alias");
|
|
2398
|
+
else {
|
|
2399
|
+
const next = stream.peek();
|
|
2400
|
+
if (next.kind === "ident" && (next.quoted || !isReservedWord(next)))
|
|
2401
|
+
alias = readIdentifier(stream, "an alias");
|
|
2402
|
+
}
|
|
2403
|
+
let where = null;
|
|
2404
|
+
if (stream.acceptKeyword("where")) {
|
|
2405
|
+
const token = stream.peek();
|
|
2406
|
+
where = requireCondition(
|
|
2407
|
+
nested(state, token, () => readOr(state)),
|
|
2408
|
+
token
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
const after = stream.peek();
|
|
2412
|
+
for (const word of ["join", "group", "order", "limit", "union", "having"])
|
|
2413
|
+
if (isKeyword(after, word) || isOperator(after, ","))
|
|
2414
|
+
throw new PolicySyntaxError(
|
|
2415
|
+
"a subquery in a policy reads one table: SELECT \u2026 FROM t [alias] [WHERE \u2026]",
|
|
2416
|
+
after.position
|
|
2417
|
+
);
|
|
2418
|
+
return { table, alias, select, where };
|
|
2419
|
+
} finally {
|
|
2420
|
+
state.subqueryDepth--;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
function isReservedWord(token) {
|
|
2424
|
+
return token.kind === "ident" && !token.quoted && (RESERVED.has(asciiLower(token.value)) || CLAUSE_WORDS.has(asciiLower(token.value)));
|
|
2425
|
+
}
|
|
2426
|
+
function readIdentifier(stream, what) {
|
|
2427
|
+
const token = stream.peek();
|
|
2428
|
+
if (token.kind !== "ident" || !token.quoted && isReservedWord(token))
|
|
2429
|
+
throw new PolicySyntaxError(
|
|
2430
|
+
`expected ${what}, found ${token.kind === "eof" ? "end of input" : JSON.stringify(token.value)}`,
|
|
2431
|
+
token.position
|
|
2432
|
+
);
|
|
2433
|
+
stream.next();
|
|
2434
|
+
return token.value;
|
|
2435
|
+
}
|
|
2436
|
+
function readTableName(stream) {
|
|
2437
|
+
const first = stream.peek();
|
|
2438
|
+
const name = readIdentifier(stream, "a table name");
|
|
2439
|
+
if (!stream.acceptOperator(".")) return name;
|
|
2440
|
+
if (first.quoted ? name !== "public" : asciiLower(name) !== "public")
|
|
2441
|
+
throw new PolicySyntaxError(
|
|
2442
|
+
`schema ${name} does not exist: naro's tables are in public`,
|
|
2443
|
+
first.position
|
|
2444
|
+
);
|
|
2445
|
+
return readIdentifier(stream, "a table name");
|
|
2446
|
+
}
|
|
2447
|
+
function readNumber(token, negative) {
|
|
2448
|
+
const value = Number(token.value) * (negative ? -1 : 1);
|
|
2449
|
+
if (!Number.isFinite(value))
|
|
2450
|
+
throw new PolicySyntaxError(
|
|
2451
|
+
`${token.value} is out of range`,
|
|
2452
|
+
token.position
|
|
2453
|
+
);
|
|
2454
|
+
if (/[.eE]/.test(token.value)) return { kind: "literal", value };
|
|
2455
|
+
if (!Number.isSafeInteger(value))
|
|
2456
|
+
throw new PolicySyntaxError(
|
|
2457
|
+
`${token.value} is out of range: integers are at most 9007199254740991`,
|
|
2458
|
+
token.position
|
|
2459
|
+
);
|
|
2460
|
+
return { kind: "literal", value: value === 0 ? 0 : value };
|
|
2461
|
+
}
|
|
2462
|
+
function readValue(state) {
|
|
2463
|
+
const { stream } = state;
|
|
2464
|
+
const token = stream.peek();
|
|
2465
|
+
count(state, token);
|
|
2466
|
+
let value = nested(state, token, () => readPrimary(state));
|
|
2467
|
+
while (isOperator(stream.peek(), "::")) {
|
|
2468
|
+
const cast = stream.next();
|
|
2469
|
+
count(state, cast);
|
|
2470
|
+
const type = stream.peek();
|
|
2471
|
+
if (type.kind !== "ident" || type.quoted || !TEXT_TYPES.has(asciiLower(type.value)))
|
|
2472
|
+
throw new PolicySyntaxError(
|
|
2473
|
+
"casts are ::text, ::varchar and ::uuid (all read as text)",
|
|
2474
|
+
type.position
|
|
2475
|
+
);
|
|
2476
|
+
stream.next();
|
|
2477
|
+
value = { kind: "cast", operand: requireValue(value, token) };
|
|
2478
|
+
}
|
|
2479
|
+
return value;
|
|
2480
|
+
}
|
|
2481
|
+
function readAuth(state, start, allowBareJwt) {
|
|
2482
|
+
const { stream } = state;
|
|
2483
|
+
const name = stream.next();
|
|
2484
|
+
const fn = name.kind === "ident" ? asciiLower(name.value) : "";
|
|
2485
|
+
if (!isOperator(stream.peek(), "(")) {
|
|
2486
|
+
if (fn === "uid" || fn === "role" || fn === "jwt")
|
|
2487
|
+
throw new PolicySyntaxError(
|
|
2488
|
+
`auth.${fn} needs parentheses: auth.${fn}()`,
|
|
2489
|
+
start.position
|
|
2490
|
+
);
|
|
2491
|
+
throw new PolicySyntaxError(
|
|
2492
|
+
`auth.${name.value} is not available: use auth.uid(), auth.role() or auth.jwt()`,
|
|
2493
|
+
start.position
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
if (fn !== "uid" && fn !== "role" && fn !== "jwt")
|
|
2497
|
+
throw new PolicySyntaxError(
|
|
2498
|
+
`auth.${name.value}() is not available: use auth.uid(), auth.role() or auth.jwt()`,
|
|
2499
|
+
start.position
|
|
2500
|
+
);
|
|
2501
|
+
stream.expectOperator("(");
|
|
2502
|
+
stream.expectOperator(")");
|
|
2503
|
+
if (fn !== "jwt") return { kind: "auth", fn };
|
|
2504
|
+
const access = readJwtAccess(stream);
|
|
2505
|
+
if (access) return access;
|
|
2506
|
+
if (allowBareJwt) return "jwt";
|
|
2507
|
+
throw new PolicySyntaxError(
|
|
2508
|
+
"auth.jwt() is read one claim at a time: auth.jwt() ->> 'email'",
|
|
2509
|
+
start.position
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
function readJwtAccess(stream) {
|
|
2513
|
+
const arrow = stream.peek();
|
|
2514
|
+
if (!isOperator(arrow, "->") && !isOperator(arrow, "->>")) return void 0;
|
|
2515
|
+
stream.next();
|
|
2516
|
+
const claim = stream.peek();
|
|
2517
|
+
if (claim.kind !== "string")
|
|
2518
|
+
throw new PolicySyntaxError(
|
|
2519
|
+
"a claim is a '\u2026' string: auth.jwt() ->> 'email'",
|
|
2520
|
+
claim.position
|
|
2521
|
+
);
|
|
2522
|
+
stream.next();
|
|
2523
|
+
if (!JWT_CLAIMS.includes(claim.value))
|
|
2524
|
+
throw new PolicySyntaxError(
|
|
2525
|
+
`auth.jwt() has ${JWT_CLAIMS.slice(0, -1).join(", ")} and ${String(JWT_CLAIMS.at(-1))}; ${JSON.stringify(claim.value)} is not one of them`,
|
|
2526
|
+
claim.position
|
|
2527
|
+
);
|
|
2528
|
+
const next = stream.peek();
|
|
2529
|
+
if (isOperator(next, "->") || isOperator(next, "->>"))
|
|
2530
|
+
throw new PolicySyntaxError(
|
|
2531
|
+
"naro's auth.jwt() claims are not nested",
|
|
2532
|
+
next.position
|
|
2533
|
+
);
|
|
2534
|
+
return {
|
|
2535
|
+
kind: "jwt",
|
|
2536
|
+
op: arrow.value,
|
|
2537
|
+
claim: claim.value
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
function readPrimary(state) {
|
|
2541
|
+
const { stream } = state;
|
|
2542
|
+
const token = stream.next();
|
|
2543
|
+
switch (token.kind) {
|
|
2544
|
+
case "string":
|
|
2545
|
+
return { kind: "literal", value: token.value };
|
|
2546
|
+
case "number":
|
|
2547
|
+
return readNumber(token, false);
|
|
2548
|
+
case "op": {
|
|
2549
|
+
if (token.value === "-") {
|
|
2550
|
+
const number4 = stream.peek();
|
|
2551
|
+
if (number4.kind !== "number")
|
|
2552
|
+
throw new PolicySyntaxError(
|
|
2553
|
+
"only a number can be negated",
|
|
2554
|
+
token.position
|
|
2555
|
+
);
|
|
2556
|
+
stream.next();
|
|
2557
|
+
return readNumber(number4, true);
|
|
2558
|
+
}
|
|
2559
|
+
if (token.value === "(") {
|
|
2560
|
+
if (isKeyword(stream.peek(), "select")) {
|
|
2561
|
+
const select = stream.next();
|
|
2562
|
+
const auth = stream.peek();
|
|
2563
|
+
if (!isKeyword(auth, "auth") || !isOperator(stream.peek(1), "."))
|
|
2564
|
+
throw new PolicySyntaxError(
|
|
2565
|
+
"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)",
|
|
2566
|
+
select.position
|
|
2567
|
+
);
|
|
2568
|
+
stream.next();
|
|
2569
|
+
stream.next();
|
|
2570
|
+
const inner2 = readAuth(state, auth, true);
|
|
2571
|
+
stream.expectOperator(")");
|
|
2572
|
+
if (inner2 !== "jwt") return inner2;
|
|
2573
|
+
const access = readJwtAccess(stream);
|
|
2574
|
+
if (!access)
|
|
2575
|
+
throw new PolicySyntaxError(
|
|
2576
|
+
"auth.jwt() is read one claim at a time: (select auth.jwt()) ->> 'email'",
|
|
2577
|
+
auth.position
|
|
2578
|
+
);
|
|
2579
|
+
return access;
|
|
2580
|
+
}
|
|
2581
|
+
const inner = readOr(state);
|
|
2582
|
+
stream.expectOperator(")");
|
|
2583
|
+
return inner;
|
|
2584
|
+
}
|
|
2585
|
+
return stream.unexpected(token);
|
|
2586
|
+
}
|
|
2587
|
+
case "ident": {
|
|
2588
|
+
if (!token.quoted) {
|
|
2589
|
+
const word = asciiLower(token.value);
|
|
2590
|
+
if (word === "true") return { kind: "literal", value: true };
|
|
2591
|
+
if (word === "false") return { kind: "literal", value: false };
|
|
2592
|
+
if (word === "null") return { kind: "literal", value: null };
|
|
2593
|
+
if (word === "current_timestamp")
|
|
2594
|
+
return { kind: "func", name: "now", args: [] };
|
|
2595
|
+
if (word === "auth" && isOperator(stream.peek(), ".")) {
|
|
2596
|
+
stream.next();
|
|
2597
|
+
const auth = readAuth(state, token, false);
|
|
2598
|
+
if (auth === "jwt") return stream.unexpected();
|
|
2599
|
+
return auth;
|
|
2600
|
+
}
|
|
2601
|
+
if (isOperator(stream.peek(), "(")) return readCall(state, token);
|
|
2602
|
+
if (RESERVED.has(word)) return stream.unexpected(token);
|
|
2603
|
+
}
|
|
2604
|
+
if (isOperator(stream.peek(), ".")) {
|
|
2605
|
+
stream.next();
|
|
2606
|
+
const name = stream.peek();
|
|
2607
|
+
if (name.kind !== "ident" || !name.quoted && isReservedWord(name))
|
|
2608
|
+
return stream.unexpected(name);
|
|
2609
|
+
stream.next();
|
|
2610
|
+
if (isOperator(stream.peek(), "(") || isOperator(stream.peek(), "."))
|
|
2611
|
+
throw new PolicySyntaxError(
|
|
2612
|
+
`${token.value}.${name.value} is not a column: naro reads column or table.column`,
|
|
2613
|
+
token.position
|
|
2614
|
+
);
|
|
2615
|
+
return { kind: "column", qualifier: token.value, name: name.value };
|
|
2616
|
+
}
|
|
2617
|
+
return { kind: "column", qualifier: null, name: token.value };
|
|
2618
|
+
}
|
|
2619
|
+
default:
|
|
2620
|
+
return stream.unexpected(token);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
function readCall(state, name) {
|
|
2624
|
+
const { stream } = state;
|
|
2625
|
+
const word = asciiLower(name.value);
|
|
2626
|
+
if (!POLICY_FUNCTIONS.includes(word))
|
|
2627
|
+
throw new PolicySyntaxError(
|
|
2628
|
+
`function ${name.value} is not available in policies: use lower, upper, coalesce, length or now`,
|
|
2629
|
+
name.position
|
|
2630
|
+
);
|
|
2631
|
+
stream.expectOperator("(");
|
|
2632
|
+
const args = [];
|
|
2633
|
+
if (!isOperator(stream.peek(), ")"))
|
|
2634
|
+
do {
|
|
2635
|
+
const item = stream.peek();
|
|
2636
|
+
args.push(requireValue(readValue(state), item));
|
|
2637
|
+
} while (stream.acceptOperator(","));
|
|
2638
|
+
stream.expectOperator(")");
|
|
2639
|
+
const arity = {
|
|
2640
|
+
lower: [1, 1],
|
|
2641
|
+
upper: [1, 1],
|
|
2642
|
+
length: [1, 1],
|
|
2643
|
+
coalesce: [1, 16],
|
|
2644
|
+
now: [0, 0]
|
|
2645
|
+
};
|
|
2646
|
+
const [min, max] = arity[word];
|
|
2647
|
+
if (args.length < min || args.length > max)
|
|
2648
|
+
throw new PolicySyntaxError(
|
|
2649
|
+
min === max ? `${word}() takes ${String(min)} argument${min === 1 ? "" : "s"}` : `${word}() takes ${String(min)} to ${String(max)} arguments`,
|
|
2650
|
+
name.position
|
|
2651
|
+
);
|
|
2652
|
+
return { kind: "func", name: word, args };
|
|
2653
|
+
}
|
|
2654
|
+
function newState(stream) {
|
|
2655
|
+
return {
|
|
2656
|
+
stream,
|
|
2657
|
+
nodes: 0,
|
|
2658
|
+
depth: 0,
|
|
2659
|
+
subqueries: 0,
|
|
2660
|
+
subqueryDepth: 0,
|
|
2661
|
+
arrays: 0
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
function readCondition(stream) {
|
|
2665
|
+
const token = stream.peek();
|
|
2666
|
+
const expr = readOr(newState(stream));
|
|
2667
|
+
return requireCondition(expr, token);
|
|
2668
|
+
}
|
|
2669
|
+
function checkExpressionSize(expr, position) {
|
|
2670
|
+
const text = printExpr(expr);
|
|
2671
|
+
if (utf8ByteLength(text) > MAX_EXPRESSION_BYTES)
|
|
2672
|
+
throw new PolicySyntaxError(
|
|
2673
|
+
`the expression is longer than ${String(MAX_EXPRESSION_BYTES)} bytes`,
|
|
2674
|
+
position
|
|
2675
|
+
);
|
|
2676
|
+
return text;
|
|
2677
|
+
}
|
|
2678
|
+
function parseExpression(text) {
|
|
2679
|
+
try {
|
|
2680
|
+
const stream = new TokenStream(tokenize(text));
|
|
2681
|
+
if (stream.peek().kind === "eof")
|
|
2682
|
+
throw new PolicySyntaxError("the expression is empty", 1);
|
|
2683
|
+
const expr = readCondition(stream);
|
|
2684
|
+
if (stream.peek().kind !== "eof") stream.unexpected();
|
|
2685
|
+
return { ok: true, expr, text: checkExpressionSize(expr, 1) };
|
|
2686
|
+
} catch (error51) {
|
|
2687
|
+
if (error51 instanceof PolicySyntaxError)
|
|
2688
|
+
return { ok: false, reason: error51.reason, position: error51.position };
|
|
2689
|
+
throw error51;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
function quoteIdent(name) {
|
|
2693
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !SPECIAL_WORDS.has(asciiLower(name)) ? name : `"${name.replaceAll('"', '""')}"`;
|
|
2694
|
+
}
|
|
2695
|
+
function commentText(text) {
|
|
2696
|
+
let out = "";
|
|
2697
|
+
for (const char of text) {
|
|
2698
|
+
const code = char.charCodeAt(0);
|
|
2699
|
+
const control = code < 32 || code >= 127 && code <= 159 || code === 8232 || code === 8233;
|
|
2700
|
+
out += control ? `\\u${code.toString(16).padStart(4, "0")}` : char;
|
|
2701
|
+
}
|
|
2702
|
+
return out;
|
|
2703
|
+
}
|
|
2704
|
+
function quoteString(text) {
|
|
2705
|
+
return `'${text.replaceAll("'", "''")}'`;
|
|
2706
|
+
}
|
|
2707
|
+
function printNumber(value) {
|
|
2708
|
+
if (Object.is(value, -0)) return "-0.0";
|
|
2709
|
+
const text = String(value);
|
|
2710
|
+
return Number.isSafeInteger(value) || /[.e]/.test(text) ? text : `${text}.0`;
|
|
2711
|
+
}
|
|
2712
|
+
function printLiteral(value) {
|
|
2713
|
+
if (value === null) return "NULL";
|
|
2714
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
2715
|
+
if (typeof value === "number") return printNumber(value);
|
|
2716
|
+
return quoteString(value);
|
|
2717
|
+
}
|
|
2718
|
+
function printValue(expr) {
|
|
2719
|
+
switch (expr.kind) {
|
|
2720
|
+
case "literal":
|
|
2721
|
+
return printLiteral(expr.value);
|
|
2722
|
+
case "column":
|
|
2723
|
+
return expr.qualifier === null ? quoteIdent(expr.name) : `${quoteIdent(expr.qualifier)}.${quoteIdent(expr.name)}`;
|
|
2724
|
+
case "auth":
|
|
2725
|
+
return `auth.${expr.fn}()`;
|
|
2726
|
+
case "jwt":
|
|
2727
|
+
return `(auth.jwt() ${expr.op} ${quoteString(expr.claim)})`;
|
|
2728
|
+
case "func":
|
|
2729
|
+
return expr.name === "now" ? "now()" : `${expr.name}(${expr.args.map(printValue).join(", ")})`;
|
|
2730
|
+
case "cast":
|
|
2731
|
+
return `(${printValue(expr.operand)})::text`;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
function printSubquery(subquery) {
|
|
2735
|
+
const select = subquery.select === "*" ? "*" : printValue(subquery.select);
|
|
2736
|
+
const alias = subquery.alias === null ? "" : ` ${quoteIdent(subquery.alias)}`;
|
|
2737
|
+
const where = subquery.where === null ? "" : ` WHERE ${printExpr(subquery.where)}`;
|
|
2738
|
+
return `SELECT ${select} FROM ${quoteIdent(subquery.table)}${alias}${where}`;
|
|
2739
|
+
}
|
|
2740
|
+
function printElement(element) {
|
|
2741
|
+
if (typeof element === "object") return printValue(element);
|
|
2742
|
+
return typeof element === "number" ? printNumber(element) : quoteString(element);
|
|
2743
|
+
}
|
|
2744
|
+
function printExpr(expr) {
|
|
2745
|
+
if (isValueExpr(expr)) return printValue(expr);
|
|
2746
|
+
switch (expr.kind) {
|
|
2747
|
+
case "and":
|
|
2748
|
+
case "or":
|
|
2749
|
+
return `(${expr.args.map(printExpr).join(expr.kind === "and" ? " AND " : " OR ")})`;
|
|
2750
|
+
case "not":
|
|
2751
|
+
return `(NOT ${printExpr(expr.arg)})`;
|
|
2752
|
+
case "compare":
|
|
2753
|
+
return `(${printValue(expr.left)} ${expr.op} ${printValue(expr.right)})`;
|
|
2754
|
+
case "distinct":
|
|
2755
|
+
return `(${printValue(expr.left)} IS ${expr.not ? "NOT " : ""}DISTINCT FROM ${printValue(expr.right)})`;
|
|
2756
|
+
case "is":
|
|
2757
|
+
return `(${printValue(expr.operand)} IS ${expr.not ? "NOT " : ""}${printLiteral(expr.value)})`;
|
|
2758
|
+
case "in":
|
|
2759
|
+
return `(${printValue(expr.operand)} ${expr.not ? "NOT " : ""}IN (${expr.list.map(printValue).join(", ")}))`;
|
|
2760
|
+
case "inSelect":
|
|
2761
|
+
return `(${printValue(expr.operand)} ${expr.not ? "NOT " : ""}IN (${printSubquery(expr.subquery)}))`;
|
|
2762
|
+
case "like":
|
|
2763
|
+
return `(${printValue(expr.operand)} ${expr.not ? "NOT " : ""}${expr.ci ? "ILIKE" : "LIKE"} ${quoteString(expr.pattern)})`;
|
|
2764
|
+
case "between":
|
|
2765
|
+
return `(${printValue(expr.operand)} ${expr.not ? "NOT " : ""}BETWEEN ${printValue(expr.low)} AND ${printValue(expr.high)})`;
|
|
2766
|
+
case "array":
|
|
2767
|
+
return `(${printValue(expr.operand)} ${expr.op} ARRAY[${expr.values.map(printElement).join(", ")}])`;
|
|
2768
|
+
case "exists":
|
|
2769
|
+
return `(EXISTS (${printSubquery(expr.subquery)}))`;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
function unqualifiedInSubquery(expr) {
|
|
2773
|
+
let found;
|
|
2774
|
+
function value(node, inside) {
|
|
2775
|
+
if (found !== void 0) return;
|
|
2776
|
+
switch (node.kind) {
|
|
2777
|
+
case "column":
|
|
2778
|
+
if (inside && node.qualifier === null) found = node;
|
|
2779
|
+
return;
|
|
2780
|
+
case "func":
|
|
2781
|
+
for (const arg of node.args) value(arg, inside);
|
|
2782
|
+
return;
|
|
2783
|
+
case "cast":
|
|
2784
|
+
value(node.operand, inside);
|
|
2785
|
+
return;
|
|
2786
|
+
default:
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
function subquery(node) {
|
|
2791
|
+
if (node.select !== "*") value(node.select, true);
|
|
2792
|
+
if (node.where !== null) visit(node.where, true);
|
|
2793
|
+
}
|
|
2794
|
+
function visit(node, inside) {
|
|
2795
|
+
if (found !== void 0) return;
|
|
2796
|
+
if (isValueExpr(node)) {
|
|
2797
|
+
value(node, inside);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
switch (node.kind) {
|
|
2801
|
+
case "and":
|
|
2802
|
+
case "or":
|
|
2803
|
+
for (const arg of node.args) visit(arg, inside);
|
|
2804
|
+
return;
|
|
2805
|
+
case "not":
|
|
2806
|
+
visit(node.arg, inside);
|
|
2807
|
+
return;
|
|
2808
|
+
case "compare":
|
|
2809
|
+
case "distinct":
|
|
2810
|
+
value(node.left, inside);
|
|
2811
|
+
value(node.right, inside);
|
|
2812
|
+
return;
|
|
2813
|
+
case "is":
|
|
2814
|
+
case "like":
|
|
2815
|
+
case "array":
|
|
2816
|
+
value(node.operand, inside);
|
|
2817
|
+
return;
|
|
2818
|
+
case "in":
|
|
2819
|
+
value(node.operand, inside);
|
|
2820
|
+
for (const item of node.list) value(item, inside);
|
|
2821
|
+
return;
|
|
2822
|
+
case "between":
|
|
2823
|
+
for (const item of [node.operand, node.low, node.high])
|
|
2824
|
+
value(item, inside);
|
|
2825
|
+
return;
|
|
2826
|
+
case "inSelect":
|
|
2827
|
+
value(node.operand, inside);
|
|
2828
|
+
subquery(node.subquery);
|
|
2829
|
+
return;
|
|
2830
|
+
case "exists":
|
|
2831
|
+
subquery(node.subquery);
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
visit(expr, false);
|
|
2836
|
+
return found;
|
|
2837
|
+
}
|
|
2838
|
+
function unqualifiedProblem(ref) {
|
|
2839
|
+
const name = quoteIdent(ref.name);
|
|
2840
|
+
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`;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// ../../packages/data-core/src/named-policy.ts
|
|
2844
|
+
var POLICY_COMMANDS = [
|
|
2845
|
+
"all",
|
|
2846
|
+
"select",
|
|
2847
|
+
"insert",
|
|
2848
|
+
"update",
|
|
2849
|
+
"delete"
|
|
2850
|
+
];
|
|
2851
|
+
var POLICY_ROLES = ["anon", "authenticated"];
|
|
2852
|
+
var MAX_POLICY_NAME_LENGTH = 63;
|
|
2853
|
+
var MAX_POLICY_SQL_BYTES = 16 * 1024;
|
|
2854
|
+
function rolesText(roles) {
|
|
2855
|
+
const set2 = new Set(roles);
|
|
2856
|
+
if (set2.has("anon") && set2.has("authenticated")) return "public";
|
|
2857
|
+
return set2.has("anon") ? "anon" : "authenticated";
|
|
2858
|
+
}
|
|
2859
|
+
function readRoles(names) {
|
|
2860
|
+
const set2 = /* @__PURE__ */ new Set();
|
|
2861
|
+
if (names.length === 0)
|
|
2862
|
+
return { ok: false, reason: "Roles are public, anon and authenticated." };
|
|
2863
|
+
for (const name of names) {
|
|
2864
|
+
if (name === "public") {
|
|
2865
|
+
set2.add("anon");
|
|
2866
|
+
set2.add("authenticated");
|
|
2867
|
+
} else if (name === "anon" || name === "authenticated") set2.add(name);
|
|
2868
|
+
else
|
|
2869
|
+
return {
|
|
2870
|
+
ok: false,
|
|
2871
|
+
reason: name === "service_role" ? "Roles are public, anon and authenticated: the service key skips every policy." : "Roles are public, anon and authenticated."
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
return { ok: true, roles: POLICY_ROLES.filter((role) => set2.has(role)) };
|
|
2875
|
+
}
|
|
2876
|
+
function policyShapeProblem(policy) {
|
|
2877
|
+
const { name, command, using, check: check2 } = policy;
|
|
2878
|
+
if (name === "" || [...name].length > MAX_POLICY_NAME_LENGTH || name.includes("\0"))
|
|
2879
|
+
return `Policy names are 1 to ${String(MAX_POLICY_NAME_LENGTH)} characters.`;
|
|
2880
|
+
const upper = command.toUpperCase();
|
|
2881
|
+
if ((command === "select" || command === "delete") && check2 !== null)
|
|
2882
|
+
return "WITH CHECK cannot be applied to SELECT or DELETE.";
|
|
2883
|
+
if (command === "insert" && using !== null)
|
|
2884
|
+
return "Only WITH CHECK expression allowed for INSERT.";
|
|
2885
|
+
if ((command === "select" || command === "delete") && using === null)
|
|
2886
|
+
return `A ${upper} policy needs USING (\u2026).`;
|
|
2887
|
+
if (command === "insert" && check2 === null)
|
|
2888
|
+
return "An INSERT policy needs WITH CHECK (\u2026).";
|
|
2889
|
+
if (using === null && check2 === null)
|
|
2890
|
+
return `${command === "all" ? "An ALL" : "An UPDATE"} policy needs USING (\u2026), WITH CHECK (\u2026) or both.`;
|
|
2891
|
+
return void 0;
|
|
2892
|
+
}
|
|
2893
|
+
function ownerExpression(column) {
|
|
2894
|
+
return printExpr({
|
|
2895
|
+
kind: "compare",
|
|
2896
|
+
op: "=",
|
|
2897
|
+
left: { kind: "column", qualifier: null, name: column },
|
|
2898
|
+
right: { kind: "auth", fn: "uid" }
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
function ownerClauses(column, command) {
|
|
2902
|
+
const text = ownerExpression(column);
|
|
2903
|
+
return command === "insert" ? { using: null, check: text } : { using: text, check: null };
|
|
2904
|
+
}
|
|
2905
|
+
var NAMED_POLICY_TABLE = "_naro_policies_v2";
|
|
2906
|
+
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))`;
|
|
2907
|
+
var NAMED_POLICY_SQL = {
|
|
2908
|
+
list: `SELECT table_name, name, kind, command, roles, using_expr, check_expr, owner_column FROM ${NAMED_POLICY_TABLE} ORDER BY table_name, name`,
|
|
2909
|
+
insert: `INSERT INTO ${NAMED_POLICY_TABLE} (table_name, name, kind, command, roles, using_expr, check_expr, owner_column) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2910
|
+
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 = ?`,
|
|
2911
|
+
remove: `DELETE FROM ${NAMED_POLICY_TABLE} WHERE table_name = ? COLLATE NOCASE AND name = ?`,
|
|
2912
|
+
removeTable: `DELETE FROM ${NAMED_POLICY_TABLE} WHERE table_name = ? COLLATE NOCASE`
|
|
2913
|
+
};
|
|
2914
|
+
function policyValues(policy) {
|
|
2915
|
+
return [
|
|
2916
|
+
policy.table,
|
|
2917
|
+
policy.name,
|
|
2918
|
+
policy.permissive ? "permissive" : "restrictive",
|
|
2919
|
+
policy.command,
|
|
2920
|
+
rolesText(policy.roles),
|
|
2921
|
+
policy.using,
|
|
2922
|
+
policy.check,
|
|
2923
|
+
policy.owner
|
|
2924
|
+
];
|
|
2925
|
+
}
|
|
2926
|
+
function storedExpression(value, field) {
|
|
2927
|
+
if (value === null || value === void 0) return { expr: null, text: null };
|
|
2928
|
+
if (typeof value !== "string")
|
|
2929
|
+
return { expr: null, text: null, problem: `${field} is not text` };
|
|
2930
|
+
const parsed = parseExpression(value);
|
|
2931
|
+
if (!parsed.ok)
|
|
2932
|
+
return {
|
|
2933
|
+
expr: null,
|
|
2934
|
+
text: value,
|
|
2935
|
+
problem: `${field}: ${parsed.reason} at character ${String(parsed.position)}`
|
|
2936
|
+
};
|
|
2937
|
+
const unbound = unqualifiedInSubquery(parsed.expr);
|
|
2938
|
+
if (unbound !== void 0)
|
|
2939
|
+
return {
|
|
2940
|
+
expr: null,
|
|
2941
|
+
text: value,
|
|
2942
|
+
problem: `${field}: ${unqualifiedProblem(unbound)}`
|
|
2943
|
+
};
|
|
2944
|
+
return { expr: parsed.expr, text: parsed.text };
|
|
2945
|
+
}
|
|
2946
|
+
function loadPolicy(row, log) {
|
|
2947
|
+
const table = row.table_name;
|
|
2948
|
+
if (typeof table !== "string" || table === "") return void 0;
|
|
2949
|
+
const problems = [];
|
|
2950
|
+
const name = typeof row.name === "string" && row.name !== "" ? row.name : "(unnamed)";
|
|
2951
|
+
if (name === "(unnamed)") problems.push("the name is missing");
|
|
2952
|
+
const kind = row.kind;
|
|
2953
|
+
if (kind !== "permissive" && kind !== "restrictive")
|
|
2954
|
+
problems.push("kind is not permissive or restrictive");
|
|
2955
|
+
const command = POLICY_COMMANDS.includes(row.command) ? row.command : void 0;
|
|
2956
|
+
if (!command)
|
|
2957
|
+
problems.push("command is not one of all, select, insert, update, delete");
|
|
2958
|
+
const roles = typeof row.roles === "string" && ["public", "anon", "authenticated"].includes(row.roles) ? readRoles([row.roles]) : void 0;
|
|
2959
|
+
if (!roles?.ok) problems.push("roles is not public, anon or authenticated");
|
|
2960
|
+
const using = storedExpression(row.using_expr, "using");
|
|
2961
|
+
const check2 = storedExpression(row.check_expr, "check");
|
|
2962
|
+
if (using.problem) problems.push(using.problem);
|
|
2963
|
+
if (check2.problem) problems.push(check2.problem);
|
|
2964
|
+
const owner = row.owner_column;
|
|
2965
|
+
const policy = {
|
|
2966
|
+
table,
|
|
2967
|
+
name,
|
|
2968
|
+
permissive: kind !== "restrictive",
|
|
2969
|
+
// What cannot be read applies everywhere: it refuses, never allows.
|
|
2970
|
+
command: command ?? "all",
|
|
2971
|
+
roles: roles?.ok ? roles.roles : [...POLICY_ROLES],
|
|
2972
|
+
using: using.text,
|
|
2973
|
+
check: check2.text,
|
|
2974
|
+
owner: typeof owner === "string" ? owner : null,
|
|
2975
|
+
invalid: null,
|
|
2976
|
+
usingExpr: using.expr,
|
|
2977
|
+
checkExpr: check2.expr
|
|
2978
|
+
};
|
|
2979
|
+
if (command && problems.length === 0) {
|
|
2980
|
+
const shape = policyShapeProblem(policy);
|
|
2981
|
+
if (shape) problems.push(shape);
|
|
2982
|
+
}
|
|
2983
|
+
if (policy.owner !== null && policy.owner !== "" && command && problems.length === 0) {
|
|
2984
|
+
const expected = ownerClauses(policy.owner, command);
|
|
2985
|
+
if (policy.using !== expected.using || policy.check !== expected.check)
|
|
2986
|
+
problems.push(
|
|
2987
|
+
"an owner policy's expressions are not its owner condition"
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
if (problems.length > 0) {
|
|
2991
|
+
policy.invalid = problems.join("; ");
|
|
2992
|
+
log?.(
|
|
2993
|
+
`naro data-core: policy ${name} on ${table} cannot be read: ${policy.invalid}`
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2996
|
+
return policy;
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// ../../packages/data-core/src/query.ts
|
|
3000
|
+
var FILTER_OPERATORS = [
|
|
3001
|
+
"eq",
|
|
3002
|
+
"neq",
|
|
3003
|
+
"gt",
|
|
3004
|
+
"gte",
|
|
3005
|
+
"lt",
|
|
3006
|
+
"lte",
|
|
3007
|
+
"in",
|
|
3008
|
+
"is",
|
|
3009
|
+
"like",
|
|
3010
|
+
"ilike",
|
|
3011
|
+
// Phase 2-1: for a TEXT column holding a JSON array (spec P2-1 §2).
|
|
3012
|
+
"contains",
|
|
3013
|
+
"containedBy",
|
|
3014
|
+
"overlaps"
|
|
3015
|
+
];
|
|
3016
|
+
function isAuthUid(value) {
|
|
3017
|
+
return isPlainObject(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "auth") && value.auth === "uid";
|
|
3018
|
+
}
|
|
3019
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
3020
|
+
var MAX_FILTERS = 100;
|
|
3021
|
+
var MAX_FILTER_DEPTH = 4;
|
|
3022
|
+
var MAX_ARRAY_VALUES = 100;
|
|
3023
|
+
var MAX_ARRAY_CONDITIONS = 4;
|
|
3024
|
+
var ARRAY_OPERATORS = [
|
|
3025
|
+
"contains",
|
|
3026
|
+
"containedBy",
|
|
3027
|
+
"overlaps"
|
|
3028
|
+
];
|
|
3029
|
+
function patternProblem(operator, pattern) {
|
|
3030
|
+
if (!patternTooLong(pattern, operator === "ilike")) return void 0;
|
|
3031
|
+
const limit = `must be at most ${String(MAX_PATTERN_BYTES)} bytes of UTF-8, D1's limit for a pattern`;
|
|
3032
|
+
return operator === "ilike" ? limit : `${limit} (like counts each *, ? and [ as 3 bytes)`;
|
|
3033
|
+
}
|
|
3034
|
+
var MAX_IDENTIFIER_LENGTH = 128;
|
|
3035
|
+
var Invalid = class extends Error {
|
|
3036
|
+
constructor(path, reason) {
|
|
3037
|
+
super(reason);
|
|
3038
|
+
this.path = path;
|
|
3039
|
+
this.reason = reason;
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
3042
|
+
function invalid(path, reason) {
|
|
3043
|
+
throw new Invalid(path, reason);
|
|
3044
|
+
}
|
|
3045
|
+
function own(object2, key) {
|
|
3046
|
+
return Object.hasOwn(object2, key) ? object2[key] : void 0;
|
|
3047
|
+
}
|
|
3048
|
+
function isOperator2(value) {
|
|
3049
|
+
return FILTER_OPERATORS.includes(value);
|
|
3050
|
+
}
|
|
3051
|
+
function readObject(value, path) {
|
|
3052
|
+
if (!isPlainObject(value)) invalid(path, "must be an object");
|
|
3053
|
+
return value;
|
|
3054
|
+
}
|
|
3055
|
+
function checkKeys(object2, allowed, path) {
|
|
3056
|
+
for (const key of Object.keys(object2)) {
|
|
3057
|
+
if (allowed.includes(key)) continue;
|
|
3058
|
+
if (longerThan(key, MAX_IDENTIFIER_LENGTH))
|
|
3059
|
+
invalid(path, "has a field that is not a known field");
|
|
3060
|
+
invalid(path ? `${path}.${key}` : key, "is not a known field");
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
function readName(value, path) {
|
|
3064
|
+
if (typeof value !== "string" || value === "")
|
|
3065
|
+
invalid(path, "must be a non-empty string");
|
|
3066
|
+
if (longerThan(value, MAX_IDENTIFIER_LENGTH))
|
|
3067
|
+
invalid(
|
|
3068
|
+
path,
|
|
3069
|
+
`must be at most ${String(MAX_IDENTIFIER_LENGTH)} characters`
|
|
3070
|
+
);
|
|
3071
|
+
return value;
|
|
3072
|
+
}
|
|
3073
|
+
function readValue2(value, path, auth = false) {
|
|
3074
|
+
if (auth && isAuthUid(value)) return { auth: "uid" };
|
|
3075
|
+
if (!isValue(value))
|
|
3076
|
+
invalid(
|
|
3077
|
+
path,
|
|
3078
|
+
auth ? 'must be a string, a finite number, a boolean, null or {"auth":"uid"}' : "must be a string, a finite number, a boolean or null"
|
|
3079
|
+
);
|
|
3080
|
+
return value;
|
|
3081
|
+
}
|
|
3082
|
+
function longerThan(text, max) {
|
|
3083
|
+
if (text.length <= max) return false;
|
|
3084
|
+
if (text.length > max * 2) return true;
|
|
3085
|
+
let count2 = 0;
|
|
3086
|
+
for (const _ of text) if (++count2 > max) return true;
|
|
3087
|
+
return false;
|
|
3088
|
+
}
|
|
3089
|
+
function endsWithLoneEscape(pattern) {
|
|
3090
|
+
let run = 0;
|
|
3091
|
+
for (let index = pattern.length - 1; index >= 0; index--) {
|
|
3092
|
+
if (pattern[index] !== "\\") break;
|
|
3093
|
+
run++;
|
|
3094
|
+
}
|
|
3095
|
+
return run % 2 === 1;
|
|
3096
|
+
}
|
|
3097
|
+
function readFilter(value, path, auth) {
|
|
3098
|
+
const object2 = readObject(value, path);
|
|
3099
|
+
checkKeys(object2, ["operator", "column", "value"], path);
|
|
3100
|
+
const operator = own(object2, "operator");
|
|
3101
|
+
if (!isOperator2(operator))
|
|
3102
|
+
invalid(
|
|
3103
|
+
`${path}.operator`,
|
|
3104
|
+
`must be one of ${FILTER_OPERATORS.join(", ")}`
|
|
3105
|
+
);
|
|
3106
|
+
const column = readName(own(object2, "column"), `${path}.column`);
|
|
3107
|
+
if (!Object.hasOwn(object2, "value")) invalid(`${path}.value`, "is required");
|
|
3108
|
+
const raw = own(object2, "value");
|
|
3109
|
+
const valuePath = `${path}.value`;
|
|
3110
|
+
switch (operator) {
|
|
3111
|
+
case "in": {
|
|
3112
|
+
if (!Array.isArray(raw)) invalid(valuePath, "must be an array");
|
|
3113
|
+
const values = raw.map(
|
|
3114
|
+
(item, index) => readValue2(item, `${valuePath}[${index}]`, auth)
|
|
3115
|
+
);
|
|
3116
|
+
return { operator, column, value: values };
|
|
3117
|
+
}
|
|
3118
|
+
case "is":
|
|
3119
|
+
if (raw !== null && raw !== true && raw !== false)
|
|
3120
|
+
invalid(valuePath, "must be null, true or false");
|
|
3121
|
+
return { operator, column, value: raw };
|
|
3122
|
+
case "like":
|
|
3123
|
+
case "ilike": {
|
|
3124
|
+
if (typeof raw !== "string") invalid(valuePath, "must be a string");
|
|
3125
|
+
const tooLong = patternProblem(operator, raw);
|
|
3126
|
+
if (tooLong !== void 0) invalid(valuePath, tooLong);
|
|
3127
|
+
if (raw.includes("\0"))
|
|
3128
|
+
invalid(valuePath, "must not contain a NUL character");
|
|
3129
|
+
if (endsWithLoneEscape(raw))
|
|
3130
|
+
invalid(valuePath, "must not end with an unescaped backslash");
|
|
3131
|
+
return { operator, column, value: raw };
|
|
3132
|
+
}
|
|
3133
|
+
case "contains":
|
|
3134
|
+
case "containedBy":
|
|
3135
|
+
case "overlaps":
|
|
3136
|
+
if (!Array.isArray(raw) || raw.length === 0 || raw.length > MAX_ARRAY_VALUES || !raw.every(
|
|
3137
|
+
(item) => typeof item === "string" || typeof item === "number" && Number.isFinite(item) || auth && isAuthUid(item)
|
|
3138
|
+
))
|
|
3139
|
+
invalid(
|
|
3140
|
+
valuePath,
|
|
3141
|
+
auth ? `must be a non-empty array of strings, numbers and {"auth":"uid"} (at most ${String(MAX_ARRAY_VALUES)})` : `must be a non-empty array of strings and numbers (at most ${String(MAX_ARRAY_VALUES)})`
|
|
3142
|
+
);
|
|
3143
|
+
return {
|
|
3144
|
+
operator,
|
|
3145
|
+
column,
|
|
3146
|
+
value: raw.map(
|
|
3147
|
+
(item) => isAuthUid(item) ? { auth: "uid" } : item
|
|
3148
|
+
)
|
|
3149
|
+
};
|
|
3150
|
+
default:
|
|
3151
|
+
return { operator, column, value: readValue2(raw, valuePath, auth) };
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
var NODE_KEYS = ["operator", "or", "and", "not"];
|
|
3155
|
+
function readNode(value, path, depth, budget) {
|
|
3156
|
+
const object2 = readObject(value, path);
|
|
3157
|
+
const kinds = NODE_KEYS.filter((key) => Object.hasOwn(object2, key));
|
|
3158
|
+
const [kind] = kinds;
|
|
3159
|
+
if (kinds.length !== 1 || kind === void 0)
|
|
3160
|
+
invalid(path, `needs exactly one of ${NODE_KEYS.join(", ")}`);
|
|
3161
|
+
if (kind === "operator") {
|
|
3162
|
+
if (++budget.conditions > MAX_FILTERS)
|
|
3163
|
+
invalid(budget.path, `has more than ${String(MAX_FILTERS)} conditions`);
|
|
3164
|
+
const filter = readFilter(object2, path, budget.auth);
|
|
3165
|
+
if (ARRAY_OPERATORS.includes(filter.operator) && ++budget.arrays > MAX_ARRAY_CONDITIONS)
|
|
3166
|
+
invalid(
|
|
3167
|
+
budget.path,
|
|
3168
|
+
`has more than ${String(MAX_ARRAY_CONDITIONS)} ${ARRAY_OPERATORS.join("/")} conditions`
|
|
3169
|
+
);
|
|
3170
|
+
return filter;
|
|
3171
|
+
}
|
|
3172
|
+
if (depth >= MAX_FILTER_DEPTH)
|
|
3173
|
+
invalid(path, `nests deeper than ${String(MAX_FILTER_DEPTH)} groups`);
|
|
3174
|
+
checkKeys(object2, [kind], path);
|
|
3175
|
+
const inner = own(object2, kind);
|
|
3176
|
+
const innerPath = `${path}.${kind}`;
|
|
3177
|
+
if (kind === "not")
|
|
3178
|
+
return { not: readNode(inner, innerPath, depth + 1, budget) };
|
|
3179
|
+
if (!Array.isArray(inner)) invalid(innerPath, "must be an array");
|
|
3180
|
+
if (inner.length === 0) invalid(innerPath, "must not be empty");
|
|
3181
|
+
const children = inner.map(
|
|
3182
|
+
(child, index) => readNode(child, `${innerPath}[${index}]`, depth + 1, budget)
|
|
3183
|
+
);
|
|
3184
|
+
return kind === "or" ? { or: children } : { and: children };
|
|
3185
|
+
}
|
|
3186
|
+
function parsePolicyNode(value, path) {
|
|
3187
|
+
try {
|
|
3188
|
+
const budget = { path, conditions: 0, arrays: 0, auth: true };
|
|
3189
|
+
return { ok: true, node: readNode(value, path, 0, budget) };
|
|
3190
|
+
} catch (error51) {
|
|
3191
|
+
if (error51 instanceof Invalid)
|
|
3192
|
+
return { ok: false, path: error51.path, reason: error51.reason };
|
|
3193
|
+
throw error51;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
1456
3196
|
|
|
1457
3197
|
// ../../packages/data-core/src/condition.ts
|
|
3198
|
+
var CONDITION_RULE = "condition";
|
|
1458
3199
|
var MAX_CONDITION_BYTES = 16 * 1024;
|
|
3200
|
+
var TREES = {
|
|
3201
|
+
select: ["using"],
|
|
3202
|
+
delete: ["using"],
|
|
3203
|
+
insert: ["check"],
|
|
3204
|
+
update: ["using", "check"]
|
|
3205
|
+
};
|
|
3206
|
+
function parseCondition(action, value) {
|
|
3207
|
+
if (!isPlainObject(value))
|
|
3208
|
+
return { ok: false, path: "", reason: "must be an object" };
|
|
3209
|
+
const trees = TREES[action];
|
|
3210
|
+
for (const key of Object.keys(value))
|
|
3211
|
+
if (key !== "role" && !trees.includes(key))
|
|
3212
|
+
return key.length > MAX_IDENTIFIER_LENGTH ? {
|
|
3213
|
+
ok: false,
|
|
3214
|
+
path: "",
|
|
3215
|
+
reason: "has a field that is not a known field"
|
|
3216
|
+
} : { ok: false, path: key, reason: "is not a known field" };
|
|
3217
|
+
const condition = {};
|
|
3218
|
+
if (Object.hasOwn(value, "role")) {
|
|
3219
|
+
const role = value.role;
|
|
3220
|
+
if (role !== "any" && role !== "authenticated")
|
|
3221
|
+
return {
|
|
3222
|
+
ok: false,
|
|
3223
|
+
path: "role",
|
|
3224
|
+
reason: 'must be "any" or "authenticated"'
|
|
3225
|
+
};
|
|
3226
|
+
if (role === "authenticated") condition.role = role;
|
|
3227
|
+
}
|
|
3228
|
+
const [required3] = trees;
|
|
3229
|
+
for (const key of trees) {
|
|
3230
|
+
if (!Object.hasOwn(value, key)) {
|
|
3231
|
+
if (key === required3)
|
|
3232
|
+
return { ok: false, path: key, reason: "is required" };
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
const parsed = parsePolicyNode(value[key], key);
|
|
3236
|
+
if (!parsed.ok) return parsed;
|
|
3237
|
+
condition[key] = parsed.node;
|
|
3238
|
+
}
|
|
3239
|
+
return { ok: true, condition };
|
|
3240
|
+
}
|
|
3241
|
+
function conditionsTooLarge(texts) {
|
|
3242
|
+
let total = 0;
|
|
3243
|
+
for (const text of texts)
|
|
3244
|
+
if (typeof text === "string") total += utf8ByteLength(text);
|
|
3245
|
+
return total > MAX_CONDITION_BYTES;
|
|
3246
|
+
}
|
|
3247
|
+
function isInvalidCondition(value) {
|
|
3248
|
+
return isPlainObject(value) && typeof value.invalid === "string";
|
|
3249
|
+
}
|
|
3250
|
+
function checkOf(action, condition) {
|
|
3251
|
+
if (action === "insert") return condition.check;
|
|
3252
|
+
if (action === "update") return condition.check ?? condition.using;
|
|
3253
|
+
return void 0;
|
|
3254
|
+
}
|
|
3255
|
+
var MAX_CONDITION_VALUES = 50;
|
|
3256
|
+
var ARRAY_OPERATORS2 = ["contains", "containedBy", "overlaps"];
|
|
3257
|
+
function treeValues(node, rebinds) {
|
|
3258
|
+
if ("not" in node) return treeValues(node.not, rebinds);
|
|
3259
|
+
if ("or" in node || "and" in node)
|
|
3260
|
+
return ("or" in node ? node.or : node.and).reduce(
|
|
3261
|
+
(sum, child) => sum + treeValues(child, rebinds),
|
|
3262
|
+
0
|
|
3263
|
+
);
|
|
3264
|
+
const value = node.value;
|
|
3265
|
+
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;
|
|
3266
|
+
if (!rebinds || Array.isArray(value) && value.length === 0) return own2;
|
|
3267
|
+
return own2 + (ARRAY_OPERATORS2.includes(node.operator) ? 5 : 1);
|
|
3268
|
+
}
|
|
3269
|
+
function conditionValueCount(action, condition) {
|
|
3270
|
+
let count2 = 0;
|
|
3271
|
+
if (action !== "insert" && condition.using)
|
|
3272
|
+
count2 += treeValues(condition.using, false);
|
|
3273
|
+
const check2 = checkOf(action, condition);
|
|
3274
|
+
if (check2) count2 += treeValues(check2, action === "update");
|
|
3275
|
+
return count2;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// ../../packages/data-core/src/legacy-policy.ts
|
|
3279
|
+
var RULES = ["none", "public", "authenticated", "owner"];
|
|
3280
|
+
var POLICY_ACTIONS2 = ["select", "insert", "update", "delete"];
|
|
3281
|
+
function isRule(value) {
|
|
3282
|
+
return RULES.includes(value);
|
|
3283
|
+
}
|
|
3284
|
+
function sugarName(action) {
|
|
3285
|
+
return `naro_${action}`;
|
|
3286
|
+
}
|
|
3287
|
+
var CONVERTED_RULE = "rls";
|
|
3288
|
+
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 ?";
|
|
3289
|
+
var LEGACY_ROW_COLUMNS = [
|
|
3290
|
+
"table_name",
|
|
3291
|
+
"select_rule",
|
|
3292
|
+
"insert_rule",
|
|
3293
|
+
"update_rule",
|
|
3294
|
+
"delete_rule",
|
|
3295
|
+
"owner_column",
|
|
3296
|
+
"select_condition",
|
|
3297
|
+
"insert_condition",
|
|
3298
|
+
"update_condition",
|
|
3299
|
+
"delete_condition",
|
|
3300
|
+
"updated_at"
|
|
3301
|
+
];
|
|
3302
|
+
function legacyRowAsRead(row) {
|
|
3303
|
+
const values = [];
|
|
3304
|
+
for (const column of LEGACY_ROW_COLUMNS) {
|
|
3305
|
+
const value = row[column];
|
|
3306
|
+
if (value === null || value === void 0) values.push(null);
|
|
3307
|
+
else if (typeof value === "string") values.push(value);
|
|
3308
|
+
else return void 0;
|
|
3309
|
+
}
|
|
3310
|
+
return values;
|
|
3311
|
+
}
|
|
3312
|
+
var POLICY_SQL = {
|
|
3313
|
+
list: "SELECT * FROM _naro_policies",
|
|
3314
|
+
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",
|
|
3315
|
+
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",
|
|
3316
|
+
remove: "DELETE FROM _naro_policies WHERE table_name = ?",
|
|
3317
|
+
/**
|
|
3318
|
+
* Migration 4: the rules of a converted row, if it is still as read —
|
|
3319
|
+
* binds `legacyRowAsRead(row)`.
|
|
3320
|
+
*/
|
|
3321
|
+
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}`,
|
|
3322
|
+
/**
|
|
3323
|
+
* Migration 4: one named policy a row converts to, written only if the
|
|
3324
|
+
* row is still as read — the same gate as `markConverted`, so a row the
|
|
3325
|
+
* old control plane changed in between is neither converted nor marked.
|
|
3326
|
+
* Binds `policyValues` and then `legacyRowAsRead(row)`.
|
|
3327
|
+
*/
|
|
3328
|
+
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})`
|
|
3329
|
+
};
|
|
3330
|
+
function readStoredCondition(action, text, tooLarge) {
|
|
3331
|
+
if (text === void 0 || text === null) return "is missing";
|
|
3332
|
+
if (typeof text !== "string") return "is not JSON";
|
|
3333
|
+
if (tooLarge)
|
|
3334
|
+
return `the conditions of the policy are larger than ${String(MAX_CONDITION_BYTES)} bytes`;
|
|
3335
|
+
let value;
|
|
3336
|
+
try {
|
|
3337
|
+
value = JSON.parse(text);
|
|
3338
|
+
} catch {
|
|
3339
|
+
return "is not JSON";
|
|
3340
|
+
}
|
|
3341
|
+
const parsed = parseCondition(action, value);
|
|
3342
|
+
if (parsed.ok) return parsed.condition;
|
|
3343
|
+
return parsed.path === "" ? parsed.reason : `${parsed.path} ${parsed.reason}`;
|
|
3344
|
+
}
|
|
3345
|
+
function policyFromRow(row, log) {
|
|
3346
|
+
const table = row.table_name;
|
|
3347
|
+
if (typeof table !== "string" || table === "") return void 0;
|
|
3348
|
+
const texts = POLICY_ACTIONS2.map(
|
|
3349
|
+
(action) => row[`${action}_rule`] === CONDITION_RULE ? row[`${action}_condition`] : void 0
|
|
3350
|
+
);
|
|
3351
|
+
const tooLarge = conditionsTooLarge(
|
|
3352
|
+
texts.filter((text) => typeof text === "string")
|
|
3353
|
+
);
|
|
3354
|
+
const read = POLICY_ACTIONS2.map(
|
|
3355
|
+
(action, index) => {
|
|
3356
|
+
const value = row[`${action}_rule`];
|
|
3357
|
+
if (value !== CONDITION_RULE) return isRule(value) ? value : "none";
|
|
3358
|
+
return readStoredCondition(action, texts[index], tooLarge);
|
|
3359
|
+
}
|
|
3360
|
+
);
|
|
3361
|
+
const values = POLICY_ACTIONS2.reduce((sum, action, index) => {
|
|
3362
|
+
const item = read[index];
|
|
3363
|
+
return typeof item === "object" ? sum + conditionValueCount(action, item) : sum;
|
|
3364
|
+
}, 0);
|
|
3365
|
+
const rule = (action, index) => {
|
|
3366
|
+
let item = read[index] ?? "none";
|
|
3367
|
+
if (typeof item === "object" && values > MAX_CONDITION_VALUES)
|
|
3368
|
+
item = `the conditions of the policy use more than ${String(MAX_CONDITION_VALUES)} values`;
|
|
3369
|
+
if (typeof item === "object" || isRule(item)) return item;
|
|
3370
|
+
log?.(
|
|
3371
|
+
`naro data-core: the ${action} condition of ${table} cannot be read: ${item}`
|
|
3372
|
+
);
|
|
3373
|
+
return { invalid: item };
|
|
3374
|
+
};
|
|
3375
|
+
const owner = row.owner_column;
|
|
3376
|
+
return {
|
|
3377
|
+
table,
|
|
3378
|
+
select: rule("select", 0),
|
|
3379
|
+
insert: rule("insert", 1),
|
|
3380
|
+
update: rule("update", 2),
|
|
3381
|
+
delete: rule("delete", 3),
|
|
3382
|
+
ownerColumn: typeof owner === "string" && owner !== "" ? owner : null
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
function findPolicy(policies, table) {
|
|
3386
|
+
const exact = policies.find((policy) => policy.table === table);
|
|
3387
|
+
if (exact) return exact;
|
|
3388
|
+
const folded = asciiLower(table);
|
|
3389
|
+
const matches = policies.filter(
|
|
3390
|
+
(policy) => asciiLower(policy.table) === folded
|
|
3391
|
+
);
|
|
3392
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
3393
|
+
}
|
|
3394
|
+
function legacyInForce(policies, tables) {
|
|
3395
|
+
const picked = /* @__PURE__ */ new Set();
|
|
3396
|
+
for (const table of tables) {
|
|
3397
|
+
const policy = findPolicy(policies, table);
|
|
3398
|
+
if (policy) picked.add(policy);
|
|
3399
|
+
}
|
|
3400
|
+
return policies.filter((policy) => picked.has(policy));
|
|
3401
|
+
}
|
|
3402
|
+
function valueExpr(value) {
|
|
3403
|
+
if (typeof value === "object" && value !== null)
|
|
3404
|
+
return { kind: "auth", fn: "uid" };
|
|
3405
|
+
return { kind: "literal", value };
|
|
3406
|
+
}
|
|
3407
|
+
var COMPARISONS = {
|
|
3408
|
+
eq: "=",
|
|
3409
|
+
neq: "<>",
|
|
3410
|
+
gt: ">",
|
|
3411
|
+
gte: ">=",
|
|
3412
|
+
lt: "<",
|
|
3413
|
+
lte: "<="
|
|
3414
|
+
};
|
|
3415
|
+
var ARRAYS = { contains: "@>", containedBy: "<@", overlaps: "&&" };
|
|
3416
|
+
var UID = { kind: "auth", fn: "uid" };
|
|
3417
|
+
function uidIsNull(not) {
|
|
3418
|
+
return { kind: "is", not, operand: UID, value: null };
|
|
3419
|
+
}
|
|
3420
|
+
function anonDiffers(expr) {
|
|
3421
|
+
if (expr.kind === "array")
|
|
3422
|
+
return expr.values.some((value) => typeof value === "object");
|
|
3423
|
+
if (expr.kind !== "in") return false;
|
|
3424
|
+
const uids = expr.list.filter((item) => item.kind === "auth").length;
|
|
3425
|
+
return uids > 0 && uids < expr.list.length;
|
|
3426
|
+
}
|
|
3427
|
+
function filterNodeToExpr(node, negated = false) {
|
|
3428
|
+
if ("not" in node)
|
|
3429
|
+
return { kind: "not", arg: filterNodeToExpr(node.not, !negated) };
|
|
3430
|
+
if ("or" in node || "and" in node) {
|
|
3431
|
+
const children = ("or" in node ? node.or : node.and).map(
|
|
3432
|
+
(child) => filterNodeToExpr(child, negated)
|
|
3433
|
+
);
|
|
3434
|
+
if (children.length === 1) return children[0];
|
|
3435
|
+
return { kind: "or" in node ? "or" : "and", args: children };
|
|
3436
|
+
}
|
|
3437
|
+
const leaf = leafToExpr(node);
|
|
3438
|
+
if (!anonDiffers(leaf)) return leaf;
|
|
3439
|
+
return negated ? { kind: "or", args: [leaf, uidIsNull(false)] } : { kind: "and", args: [leaf, uidIsNull(true)] };
|
|
3440
|
+
}
|
|
3441
|
+
function leafToExpr(node) {
|
|
3442
|
+
const column = {
|
|
3443
|
+
kind: "column",
|
|
3444
|
+
qualifier: null,
|
|
3445
|
+
name: node.column
|
|
3446
|
+
};
|
|
3447
|
+
const value = node.value;
|
|
3448
|
+
switch (node.operator) {
|
|
3449
|
+
case "eq":
|
|
3450
|
+
case "neq":
|
|
3451
|
+
case "gt":
|
|
3452
|
+
case "gte":
|
|
3453
|
+
case "lt":
|
|
3454
|
+
case "lte":
|
|
3455
|
+
return {
|
|
3456
|
+
kind: "compare",
|
|
3457
|
+
op: COMPARISONS[node.operator],
|
|
3458
|
+
left: column,
|
|
3459
|
+
right: valueExpr(Array.isArray(value) ? null : value)
|
|
3460
|
+
};
|
|
3461
|
+
case "in": {
|
|
3462
|
+
const list = Array.isArray(value) ? value : [value];
|
|
3463
|
+
if (list.length === 0) return { kind: "literal", value: false };
|
|
3464
|
+
return {
|
|
3465
|
+
kind: "in",
|
|
3466
|
+
not: false,
|
|
3467
|
+
operand: column,
|
|
3468
|
+
list: list.map(valueExpr)
|
|
3469
|
+
};
|
|
3470
|
+
}
|
|
3471
|
+
case "is":
|
|
3472
|
+
return {
|
|
3473
|
+
kind: "is",
|
|
3474
|
+
not: false,
|
|
3475
|
+
operand: column,
|
|
3476
|
+
value: value === true || value === false ? value : null
|
|
3477
|
+
};
|
|
3478
|
+
case "like":
|
|
3479
|
+
case "ilike":
|
|
3480
|
+
return {
|
|
3481
|
+
kind: "like",
|
|
3482
|
+
not: false,
|
|
3483
|
+
ci: node.operator === "ilike",
|
|
3484
|
+
operand: column,
|
|
3485
|
+
pattern: typeof value === "string" ? value : ""
|
|
3486
|
+
};
|
|
3487
|
+
case "contains":
|
|
3488
|
+
case "containedBy":
|
|
3489
|
+
case "overlaps":
|
|
3490
|
+
return {
|
|
3491
|
+
kind: "array",
|
|
3492
|
+
op: ARRAYS[node.operator],
|
|
3493
|
+
operand: { kind: "column", qualifier: null, name: node.column },
|
|
3494
|
+
values: (Array.isArray(value) ? value : []).map(
|
|
3495
|
+
(item) => typeof item === "object" && item !== null ? { kind: "auth", fn: "uid" } : typeof item === "number" ? item : String(item)
|
|
3496
|
+
)
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
var TRUE_TEXT = printExpr({ kind: "literal", value: true });
|
|
3501
|
+
function treeText(node) {
|
|
3502
|
+
return node === void 0 ? null : printExpr(filterNodeToExpr(node));
|
|
3503
|
+
}
|
|
3504
|
+
function namedFromRule(table, action, rule, ownerColumn) {
|
|
3505
|
+
const base = {
|
|
3506
|
+
table,
|
|
3507
|
+
name: sugarName(action),
|
|
3508
|
+
permissive: true,
|
|
3509
|
+
command: action,
|
|
3510
|
+
owner: null
|
|
3511
|
+
};
|
|
3512
|
+
const everyone = [...POLICY_ROLES];
|
|
3513
|
+
if (rule === "none" || typeof rule === "string" && !isRule(rule))
|
|
3514
|
+
return null;
|
|
3515
|
+
if (rule === "public" || rule === "authenticated")
|
|
3516
|
+
return {
|
|
3517
|
+
...base,
|
|
3518
|
+
roles: rule === "public" ? everyone : ["authenticated"],
|
|
3519
|
+
using: action === "insert" ? null : TRUE_TEXT,
|
|
3520
|
+
check: action === "insert" ? TRUE_TEXT : null
|
|
3521
|
+
};
|
|
3522
|
+
if (rule === "owner") {
|
|
3523
|
+
const column = ownerColumn ?? "";
|
|
3524
|
+
return {
|
|
3525
|
+
...base,
|
|
3526
|
+
roles: ["authenticated"],
|
|
3527
|
+
...column === "" ? { using: null, check: null } : ownerClauses(column, action),
|
|
3528
|
+
owner: column
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
if (isInvalidCondition(rule) || (action === "insert" ? rule.check : rule.using) === void 0)
|
|
3532
|
+
return { ...base, roles: everyone, using: null, check: null };
|
|
3533
|
+
return {
|
|
3534
|
+
...base,
|
|
3535
|
+
roles: rule.role === "authenticated" ? ["authenticated"] : everyone,
|
|
3536
|
+
using: action === "insert" ? null : treeText(rule.using),
|
|
3537
|
+
check: action === "insert" || action === "update" ? treeText(rule.check) : null
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3540
|
+
function namedFromLegacy(policy) {
|
|
3541
|
+
return POLICY_ACTIONS2.flatMap((action) => {
|
|
3542
|
+
const named = namedFromRule(
|
|
3543
|
+
policy.table,
|
|
3544
|
+
action,
|
|
3545
|
+
policy[action],
|
|
3546
|
+
policy.ownerColumn
|
|
3547
|
+
);
|
|
3548
|
+
return named ? [named] : [];
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
function loadNamed(policies, log) {
|
|
3552
|
+
const columns = [
|
|
3553
|
+
"table_name",
|
|
3554
|
+
"name",
|
|
3555
|
+
"kind",
|
|
3556
|
+
"command",
|
|
3557
|
+
"roles",
|
|
3558
|
+
"using_expr",
|
|
3559
|
+
"check_expr",
|
|
3560
|
+
"owner_column"
|
|
3561
|
+
];
|
|
3562
|
+
return policies.flatMap((policy) => {
|
|
3563
|
+
const values = policyValues(policy);
|
|
3564
|
+
const loaded = loadPolicy(
|
|
3565
|
+
Object.fromEntries(
|
|
3566
|
+
columns.map((column, index) => [column, values[index]])
|
|
3567
|
+
),
|
|
3568
|
+
log
|
|
3569
|
+
);
|
|
3570
|
+
return loaded ? [loaded] : [];
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3573
|
+
function unreadableConversions(policy) {
|
|
3574
|
+
const found = [];
|
|
3575
|
+
for (const action of POLICY_ACTIONS2) {
|
|
3576
|
+
const rule = policy[action];
|
|
3577
|
+
const named = namedFromRule(policy.table, action, rule, policy.ownerColumn);
|
|
3578
|
+
if (!named) continue;
|
|
3579
|
+
const [loaded] = loadNamed([named]);
|
|
3580
|
+
if (!loaded || loaded.invalid === null) continue;
|
|
3581
|
+
found.push({
|
|
3582
|
+
table: named.table,
|
|
3583
|
+
policy: named.name,
|
|
3584
|
+
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
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
return found;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
// ../../packages/data-core/src/policy-index.ts
|
|
3591
|
+
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`;
|
|
3592
|
+
|
|
3593
|
+
// ../../packages/data-core/src/policy-migration.ts
|
|
3594
|
+
var SYSTEM_PREDICATE = systemTablePredicate("name");
|
|
1459
3595
|
|
|
1460
3596
|
// ../../packages/data-core/src/system-sql.ts
|
|
3597
|
+
async function convertLegacyPolicies(db, found) {
|
|
3598
|
+
const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
|
|
3599
|
+
if (rows.length === 0) return [];
|
|
3600
|
+
const read = rows.map((row) => ({
|
|
3601
|
+
policy: policyFromRow(row),
|
|
3602
|
+
asRead: legacyRowAsRead(row)
|
|
3603
|
+
}));
|
|
3604
|
+
const inForce = new Set(
|
|
3605
|
+
legacyInForce(
|
|
3606
|
+
read.flatMap(({ policy }) => policy ? [policy] : []),
|
|
3607
|
+
await readTableNames(db)
|
|
3608
|
+
)
|
|
3609
|
+
);
|
|
3610
|
+
const statements = [];
|
|
3611
|
+
for (const { policy, asRead } of read) {
|
|
3612
|
+
if (!policy || !asRead) continue;
|
|
3613
|
+
if (inForce.has(policy)) {
|
|
3614
|
+
for (const named of namedFromLegacy(policy))
|
|
3615
|
+
statements.push({
|
|
3616
|
+
sql: POLICY_SQL.insertConverted,
|
|
3617
|
+
params: [...policyValues(named), ...asRead]
|
|
3618
|
+
});
|
|
3619
|
+
found.unreadable.push(...unreadableConversions(policy));
|
|
3620
|
+
}
|
|
3621
|
+
statements.push({ sql: POLICY_SQL.markConverted, params: asRead });
|
|
3622
|
+
}
|
|
3623
|
+
return statements;
|
|
3624
|
+
}
|
|
1461
3625
|
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')))";
|
|
1462
3626
|
var SYSTEM_MIGRATIONS = [
|
|
1463
3627
|
{
|
|
@@ -1499,6 +3663,15 @@ var SYSTEM_MIGRATIONS = [
|
|
|
1499
3663
|
"CREATE INDEX IF NOT EXISTS _naro_auth_email_log_address ON _naro_auth_email_log (address_hash, sent_at)",
|
|
1500
3664
|
"CREATE TABLE IF NOT EXISTS _naro_auth_contested (user_id TEXT PRIMARY KEY, contested_at INTEGER NOT NULL)"
|
|
1501
3665
|
]
|
|
3666
|
+
},
|
|
3667
|
+
{
|
|
3668
|
+
// P2-7 §9: named policies, many per table. The old rows are converted
|
|
3669
|
+
// in the same batch and their rules set to a word old workers read as
|
|
3670
|
+
// `none`.
|
|
3671
|
+
version: 4,
|
|
3672
|
+
name: "named_policies",
|
|
3673
|
+
statements: [NAMED_POLICY_TABLE_SQL],
|
|
3674
|
+
compute: convertLegacyPolicies
|
|
1502
3675
|
}
|
|
1503
3676
|
];
|
|
1504
3677
|
|
|
@@ -1564,12 +3737,12 @@ function sqlSkeleton(sql) {
|
|
|
1564
3737
|
let index = 0;
|
|
1565
3738
|
while (index < sql.length) {
|
|
1566
3739
|
const char = sql[index] ?? "";
|
|
1567
|
-
const
|
|
1568
|
-
if (
|
|
3740
|
+
const pair2 = sql.slice(index, index + 2);
|
|
3741
|
+
if (pair2 === "--") {
|
|
1569
3742
|
const end = sql.indexOf("\n", index);
|
|
1570
3743
|
index = end < 0 ? sql.length : end;
|
|
1571
3744
|
out += " ";
|
|
1572
|
-
} else if (
|
|
3745
|
+
} else if (pair2 === "/*") {
|
|
1573
3746
|
const end = sql.indexOf("*/", index + 2);
|
|
1574
3747
|
index = end < 0 ? sql.length : end + 2;
|
|
1575
3748
|
out += " ";
|
|
@@ -1739,6 +3912,9 @@ function generateTypes(tables, options2 = {}) {
|
|
|
1739
3912
|
`;
|
|
1740
3913
|
}
|
|
1741
3914
|
|
|
3915
|
+
// ../../packages/core/src/route-patterns.ts
|
|
3916
|
+
var MAX_ROUTE_LIST_BYTES = 64 * 1024;
|
|
3917
|
+
|
|
1742
3918
|
// ../../packages/core/src/artifact-manifest.ts
|
|
1743
3919
|
var MAX_FILE_BYTES2 = 25 * 1024 * 1024;
|
|
1744
3920
|
var MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
@@ -6440,8 +8616,8 @@ function az_default() {
|
|
|
6440
8616
|
}
|
|
6441
8617
|
|
|
6442
8618
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/be.js
|
|
6443
|
-
function getBelarusianPlural(
|
|
6444
|
-
const absCount = Math.abs(
|
|
8619
|
+
function getBelarusianPlural(count2, one, few, many) {
|
|
8620
|
+
const absCount = Math.abs(count2);
|
|
6445
8621
|
const lastDigit = absCount % 10;
|
|
6446
8622
|
const lastTwoDigits = absCount % 100;
|
|
6447
8623
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -8519,8 +10695,8 @@ function hu_default() {
|
|
|
8519
10695
|
}
|
|
8520
10696
|
|
|
8521
10697
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/hy.js
|
|
8522
|
-
function getArmenianPlural(
|
|
8523
|
-
return Math.abs(
|
|
10698
|
+
function getArmenianPlural(count2, one, many) {
|
|
10699
|
+
return Math.abs(count2) === 1 ? one : many;
|
|
8524
10700
|
}
|
|
8525
10701
|
function withDefiniteArticle(word) {
|
|
8526
10702
|
if (!word)
|
|
@@ -10648,8 +12824,8 @@ function ro_default() {
|
|
|
10648
12824
|
}
|
|
10649
12825
|
|
|
10650
12826
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ru.js
|
|
10651
|
-
function getRussianPlural(
|
|
10652
|
-
const absCount = Math.abs(
|
|
12827
|
+
function getRussianPlural(count2, one, few, many) {
|
|
12828
|
+
const absCount = Math.abs(count2);
|
|
10653
12829
|
const lastDigit = absCount % 10;
|
|
10654
12830
|
const lastTwoDigits = absCount % 100;
|
|
10655
12831
|
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
|
|
@@ -16552,6 +18728,9 @@ function migrationNameMismatch(sent, recorded) {
|
|
|
16552
18728
|
return `The control plane recorded the migration as ${recorded}, but naro sent ${sent} and wrote the file under that name. Until the two match, naro migration list shows them apart and naro db push would apply the file again.`;
|
|
16553
18729
|
}
|
|
16554
18730
|
|
|
18731
|
+
// ../../packages/sdk/src/reset-plan.ts
|
|
18732
|
+
var RESET_CLEAR_POLICIES = `DELETE FROM ${NAMED_POLICY_TABLE};`;
|
|
18733
|
+
|
|
16555
18734
|
// src/naro.ts
|
|
16556
18735
|
import { deployDirectory } from "naro-cli";
|
|
16557
18736
|
function createNaroDeps() {
|
|
@@ -17031,7 +19210,7 @@ var DATABASE_TOOLS = [
|
|
|
17031
19210
|
destructiveHint: true,
|
|
17032
19211
|
idempotentHint: false
|
|
17033
19212
|
},
|
|
17034
|
-
description: `${DESTRUCTIVE}executes raw SQL in the D1 database \u2014 it can change or delete data. Use \`apply_migration\` instead for DDL operations. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text; params work with a single statement only. Several statements in one call run as one transaction: if one fails, none of them is applied. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
|
|
19213
|
+
description: `${DESTRUCTIVE}executes raw SQL in the D1 database \u2014 it can change or delete data. Use \`apply_migration\` instead for DDL operations. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text; params work with a single statement only. Several statements in one call run as one transaction: if one fails, none of them is applied. CREATE/DROP POLICY work here too (see \`apply_migration\`; they take no params), and result.warnings, when present, says what a policy needs. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
|
|
17035
19214
|
readOnlyVariant: {
|
|
17036
19215
|
description: `Executes a single read-only SQL statement in the D1 database. This server runs with --read-only: writes, DDL and multi-statement batches are refused with DATABASE_READ_ONLY. ${UNTRUSTED} Pass values through params (bound to ? placeholders) instead of building them into the SQL text. ${SHARED_DATABASE} ${LIMITS} ${SINGLE_THREADED}`,
|
|
17037
19216
|
annotations: {
|
|
@@ -17090,7 +19269,7 @@ var DATABASE_TOOLS = [
|
|
|
17090
19269
|
destructiveHint: true,
|
|
17091
19270
|
idempotentHint: false
|
|
17092
19271
|
},
|
|
17093
|
-
description: `${DESTRUCTIVE}applies a migration (DDL) to the D1 database and records it in the d1_migrations ledger \u2014 the same ledger wrangler uses. \`name\` is a short snake_case title; naro numbers it (NNNN_name.sql) and, when the project directory is known, writes the file to ./migrations so \`naro db push\` sees it as applied. All-or-nothing: if a statement fails, the whole migration is rolled back and not recorded. ${UNTRUSTED} ${SHARED_DATABASE} ${LIMITS}`,
|
|
19272
|
+
description: `${DESTRUCTIVE}applies a migration (DDL) to the D1 database and records it in the d1_migrations ledger \u2014 the same ledger wrangler uses. \`name\` is a short snake_case title; naro numbers it (NNNN_name.sql) and, when the project directory is known, writes the file to ./migrations so \`naro db push\` sees it as applied. All-or-nothing: if a statement fails, the whole migration is rolled back and not recorded. Write CREATE POLICY in the same migration as the table, as in Supabase: it is applied with the table, all or nothing. A table without a policy refuses every anon and signed-in request to the Data API. DROP POLICY [IF EXISTS], COMMENT ON POLICY p ON t IS 'naro:owner <column>' and ALTER TABLE t ENABLE ROW LEVEL SECURITY are accepted too; DISABLE ROW LEVEL SECURITY, GRANT, REVOKE and ALTER POLICY are refused before anything runs. Dropping a table drops its policies, so a SQLite table rebuild (create __new_x, copy, drop x, rename) must create the policies again after the rename: warnings lists the ones it removed, with their SQL. ${UNTRUSTED} ${SHARED_DATABASE} ${LIMITS}`,
|
|
17094
19273
|
inputSchema: {
|
|
17095
19274
|
type: "object",
|
|
17096
19275
|
properties: {
|
|
@@ -17101,7 +19280,7 @@ var DATABASE_TOOLS = [
|
|
|
17101
19280
|
},
|
|
17102
19281
|
query: {
|
|
17103
19282
|
type: "string",
|
|
17104
|
-
description: "The migration's SQL (DDL) to apply."
|
|
19283
|
+
description: "The migration's SQL (DDL) to apply, with CREATE POLICY for its tables in the same SQL."
|
|
17105
19284
|
},
|
|
17106
19285
|
rootDirectory: {
|
|
17107
19286
|
type: "string",
|
|
@@ -17130,6 +19309,8 @@ var DATABASE_TOOLS = [
|
|
|
17130
19309
|
migration: applied.migration,
|
|
17131
19310
|
file: file2,
|
|
17132
19311
|
result: applied.result,
|
|
19312
|
+
// P2-7b: what the migration's policies need, in the server's words.
|
|
19313
|
+
warnings: applied.warnings,
|
|
17133
19314
|
...fileWarning ? { fileWarning } : {}
|
|
17134
19315
|
};
|
|
17135
19316
|
}
|
|
@@ -17143,7 +19324,7 @@ var DATABASE_TOOLS = [
|
|
|
17143
19324
|
destructiveHint: false,
|
|
17144
19325
|
idempotentHint: false
|
|
17145
19326
|
},
|
|
17146
|
-
description: `Creates a table in the project's D1 database without writing SQL: naro renders the CREATE TABLE from columns, applies it as one migration recorded in d1_migrations and, when the project directory is known, writes it to ./migrations as NNNN_schema_create_table_<table>.sql so naro db push and naro db reset see it. Column types: INTEGER, REAL, TEXT, BLOB, NUMERIC, BOOLEAN, DATETIME, JSON \u2014 store JSON in TEXT, since a JSON column turns '123' into a number. default takes a literal (a string, a number, true, false, null) or {"keyword": "CURRENT_TIMESTAMP"}, never an expression. Every primary key but a single INTEGER one is NOT NULL. owner: {column} adds that column as TEXT NOT NULL and, when the project's backend is ready, an owner policy for select, insert, update and delete. Without a policy the Data API refuses every anon and signed-in request to the table; warnings says so. dryRun: true returns the SQL and the migration name without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
|
|
19327
|
+
description: `Creates a table in the project's D1 database without writing SQL: naro renders the CREATE TABLE from columns, applies it as one migration recorded in d1_migrations and, when the project directory is known, writes it to ./migrations as NNNN_schema_create_table_<table>.sql so naro db push and naro db reset see it. Column types: INTEGER, REAL, TEXT, BLOB, NUMERIC, BOOLEAN, DATETIME, JSON \u2014 store JSON in TEXT, since a JSON column turns '123' into a number. default takes a literal (a string, a number, true, false, null) or {"keyword": "CURRENT_TIMESTAMP"}, never an expression. Every primary key but a single INTEGER one is NOT NULL. owner: {column} adds that column as TEXT NOT NULL and, when the project's backend is ready, an owner policy for select, insert, update and delete \u2014 not in that migration file, so naro db reset lists it, with its SQL, before it removes it: add that SQL to a migration to keep it. Without a policy the Data API refuses every anon and signed-in request to the table; warnings says so. Policies an earlier table left under the name are removed before the table is created, whenever the project has a backend: removedPolicies lists them: statements has each as the CREATE POLICY that naro policy create takes back, except an owner rule (its ownerColumn is set), which CREATE POLICY cannot say \u2014 setCommand is the naro policy set command that sets those again. dryRun: true returns the SQL and the migration name without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
|
|
17147
19328
|
inputSchema: {
|
|
17148
19329
|
type: "object",
|
|
17149
19330
|
properties: {
|
|
@@ -17190,7 +19371,7 @@ var DATABASE_TOOLS = [
|
|
|
17190
19371
|
destructiveHint: true,
|
|
17191
19372
|
idempotentHint: false
|
|
17192
19373
|
},
|
|
17193
|
-
description: `${DESTRUCTIVE}changes a table of the project's D1 database without writing SQL, as one migration recorded in d1_migrations and written to ./migrations when the project directory is known. action is add_column (newColumn, shaped like a create_table column), rename_column (column, to), rename_table (to) or drop_column (column) \u2014 which deletes the column and its value in every row and needs confirm set to "<table>.<column>". SQLite cannot add a PRIMARY KEY or UNIQUE column, a NOT NULL column without a default that is not NULL, or a column defaulting to CURRENT_TIMESTAMP, and cannot drop a primary key, UNIQUE or indexed column: naro refuses those up front and says why. It also refuses to drop or rename a table or column that a trigger or view uses (read from their SQL; when the SQL leaves it open, a warning instead) \u2014 drop or recreate that trigger or view with apply_migration first. Changing a column's type is not an action \u2014 it needs a table rebuild, so write that migration with apply_migration. Data API policies are kept by table name and are not moved:
|
|
19374
|
+
description: `${DESTRUCTIVE}changes a table of the project's D1 database without writing SQL, as one migration recorded in d1_migrations and written to ./migrations when the project directory is known. action is add_column (newColumn, shaped like a create_table column), rename_column (column, to), rename_table (to) or drop_column (column) \u2014 which deletes the column and its value in every row and needs confirm set to "<table>.<column>". SQLite cannot add a PRIMARY KEY or UNIQUE column, a NOT NULL column without a default that is not NULL, or a column defaulting to CURRENT_TIMESTAMP, and cannot drop a primary key, UNIQUE or indexed column: naro refuses those up front and says why. It also refuses to drop or rename a table or column that a trigger or view uses (read from their SQL; when the SQL leaves it open, a warning instead) \u2014 drop or recreate that trigger or view with apply_migration first. Changing a column's type is not an action \u2014 it needs a table rebuild, so write that migration with apply_migration. Data API policies are kept by table name and are not moved. A rename leaves the table's Data API policies under the old name, where they apply to no table, and they are removed when a table is next created as that name or renamed to it \u2014 renaming the table back included: removedPolicies lists them: statements has each as the CREATE POLICY that naro policy create takes back, except an owner rule (its ownerColumn is set), which CREATE POLICY cannot say \u2014 setCommand is the naro policy set command that sets those again. Move them to the new name right after a rename. A rename or drop that takes a table or column from under a policy \u2014 this table's or, through a subquery, another table's \u2014 makes the requests that policy applies to answer 500 POLICY_INVALID: warnings names each policy the change breaks, with dryRun: true before anything is applied. Read warnings after a rename or a drop. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
|
|
17194
19375
|
inputSchema: {
|
|
17195
19376
|
type: "object",
|
|
17196
19377
|
properties: {
|
|
@@ -17274,7 +19455,7 @@ var DATABASE_TOOLS = [
|
|
|
17274
19455
|
destructiveHint: true,
|
|
17275
19456
|
idempotentHint: false
|
|
17276
19457
|
},
|
|
17277
|
-
description: `${DESTRUCTIVE}drops a table of the project's D1 database and every row in it, as one migration recorded in d1_migrations. Needs confirm set to the table's name. A table that another table's foreign key references, or that a trigger or view uses, is refused. restore_database can bring it back (Time Travel, 30 days). Its Data API
|
|
19458
|
+
description: `${DESTRUCTIVE}drops a table of the project's D1 database and every row in it, as one migration recorded in d1_migrations. Needs confirm set to the table's name. A table that another table's foreign key references, or that a trigger or view uses, is refused. restore_database can bring it back (Time Travel, 30 days). Its Data API policies are removed with it whenever the project has a backend, on or off: removedPolicies lists them: statements has each as the CREATE POLICY that naro policy create takes back, except an owner rule (its ownerColumn is set), which CREATE POLICY cannot say \u2014 setCommand is the naro policy set command that sets those again. Otherwise they stay under the table's name: read warnings \u2014 which also names each policy of another table that reads this one in a subquery and stops applying (500 POLICY_INVALID); dryRun: true says so before the drop. dryRun: true returns the SQL without applying anything. ${UNTRUSTED} ${SHARED_DATABASE}`,
|
|
17278
19459
|
inputSchema: {
|
|
17279
19460
|
type: "object",
|
|
17280
19461
|
properties: {
|
|
@@ -18339,7 +20520,7 @@ function toolErrorJson(error51) {
|
|
|
18339
20520
|
}
|
|
18340
20521
|
|
|
18341
20522
|
// src/server.ts
|
|
18342
|
-
var SERVER_VERSION = "0.
|
|
20523
|
+
var SERVER_VERSION = "0.11.0";
|
|
18343
20524
|
function visibleTools(options2) {
|
|
18344
20525
|
const { features, readOnly } = options2;
|
|
18345
20526
|
return TOOLS.flatMap((tool) => {
|