@somewhere-cloud/cli 0.38.19 → 0.38.21
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/CHANGELOG.md +28 -0
- package/README.md +1 -0
- package/dist/commands/auth.js +128 -1
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/lint.js +3 -0
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/typecheck.js +3 -0
- package/dist/commands/typecheck.js.map +1 -1
- package/dist/lib/http.js +12 -0
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/platform-contract.js +86 -0
- package/dist/lib/platform-contract.js.map +1 -0
- package/dist/lib/source-lint.js +6 -1
- package/dist/lib/source-lint.js.map +1 -1
- package/dist/lib/typecheck.js +11 -2
- package/dist/lib/typecheck.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/runtime/DECLARED-DATA-VENDOR.json +12 -11
- package/runtime/PLATFORM-SOURCE.json +1 -1
- package/runtime/SOURCE-LINT-VENDOR.json +37 -36
- package/runtime/declared-data.cjs +284 -40
- package/runtime/source-lint.cjs +247 -166
package/runtime/source-lint.cjs
CHANGED
|
@@ -112,11 +112,12 @@ var require_declared_data_contract = __commonJS({
|
|
|
112
112
|
const normalizedOperations = o?.length === 4 ? void 0 : o;
|
|
113
113
|
return { k: "m", m: { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {}, ...og ? { og: true } : {} } };
|
|
114
114
|
}
|
|
115
|
-
if (value.k === "a" && keys === "k,p" && Array.isArray(value.p) && value.p.length === 2) {
|
|
115
|
+
if (value.k === "a" && (keys === "k,p" || keys === "k,p,reads" && edges === 0) && Array.isArray(value.p) && value.p.length === 2) {
|
|
116
116
|
const left = policy(value.p[0], label), right = policy(value.p[1], label);
|
|
117
117
|
if (left.k !== "o" || !["m", "g", "r"].includes(right.k)) fail(label);
|
|
118
118
|
if (right.k === "m" && right.m.og && left.c !== "user_id") fail(label);
|
|
119
|
-
|
|
119
|
+
const reads = value.reads === void 0 ? void 0 : branchReads(value.reads, label);
|
|
120
|
+
return { k: "a", p: [left, right], ...reads ? { reads } : {} };
|
|
120
121
|
}
|
|
121
122
|
if (value.k === "n" && keys === "g,k,w") {
|
|
122
123
|
const gate = policy(value.g, label), who = policy(value.w, label);
|
|
@@ -144,6 +145,23 @@ var require_declared_data_contract = __commonJS({
|
|
|
144
145
|
}
|
|
145
146
|
fail(label);
|
|
146
147
|
}
|
|
148
|
+
function branchReads(value, label) {
|
|
149
|
+
if (!Array.isArray(value) || value.length !== 2 || value[0] === null && value[1] === null) fail(label + ".reads");
|
|
150
|
+
return value.map((list) => {
|
|
151
|
+
if (list === null) return null;
|
|
152
|
+
if (!Array.isArray(list) || !list.length || list.some((name) => typeof name !== "string" || !identifier.test(name)) || new Set(list).size !== list.length) fail(label + ".reads");
|
|
153
|
+
return [...list].sort();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function hiddenByBranch(table) {
|
|
157
|
+
const reads = table.policy && table.policy.k === "a" ? table.policy.reads : void 0;
|
|
158
|
+
if (!reads) return /* @__PURE__ */ new Set();
|
|
159
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
160
|
+
for (const list of reads) if (list) {
|
|
161
|
+
for (const column of table.columns) if (!list.includes(column.n)) hidden.add(column.n);
|
|
162
|
+
}
|
|
163
|
+
return hidden;
|
|
164
|
+
}
|
|
147
165
|
function policyRoles(value, label) {
|
|
148
166
|
record(value, label + ".roles");
|
|
149
167
|
if (Object.keys(value).sort().join(",") !== "create,delete,read,update") fail(label);
|
|
@@ -381,10 +399,10 @@ var require_declared_data_contract = __commonJS({
|
|
|
381
399
|
const type = { integer: "number", bigint: "string", number: "number", text: "string", timestamp: "string", boolean: "boolean", json: "Json", blob: "number[]" }[column.t];
|
|
382
400
|
return column.nul ? `${type} | null` : type;
|
|
383
401
|
}
|
|
384
|
-
function shape(columns, names, mode, required = /* @__PURE__ */ new Set()) {
|
|
402
|
+
function shape(columns, names, mode, required = /* @__PURE__ */ new Set(), hidden = /* @__PURE__ */ new Set()) {
|
|
385
403
|
const values = names.map((name) => {
|
|
386
404
|
const column = columns.find((candidate) => candidate.n === name);
|
|
387
|
-
const optional = mode === "update" || mode === "create" && !required.has(name) && (column.d || column.nul);
|
|
405
|
+
const optional = mode === "update" || mode === "create" && !required.has(name) && (column.d || column.nul) || mode === "read" && hidden.has(name);
|
|
388
406
|
return `${JSON.stringify(name)}${optional ? "?" : ""}: ${columnType(column)}`;
|
|
389
407
|
});
|
|
390
408
|
return values.length ? `{ ${values.join("; ")} }` : "Record<string, never>";
|
|
@@ -548,13 +566,14 @@ async function intake(table, reference, values, extra) {
|
|
|
548
566
|
export interface IntakeResult { readonly accepted: true; readonly idempotency: InsertReceipt; readonly request: InsertRequest<IntakeResult> }
|
|
549
567
|
`;
|
|
550
568
|
var ROW_CAN_DECLARATION = " /** Row capability hint (list/get with can: true): whether the signed-in caller may update or delete this row now, by the same rule the write enforces. A hint for UI; the write is still the final answer. */\n export interface RowCan { readonly update: boolean; readonly delete: boolean; }\n";
|
|
569
|
+
var ROW_HINTS_DECLARATION = ROW_CAN_DECLARATION + ' /** Row hints (list/get with can: true): can says what the signed-in caller may change; mine is true when this caller created the row (the owner or author the platform recorded). Show edit and delete controls, a "you" label or a "mine" filter from these; the identity column itself is never sent. */\n export interface RowHints { readonly can: RowCan; readonly mine: boolean; }\n';
|
|
551
570
|
var INSERT_CLIENT_DECLARATIONS = `
|
|
552
571
|
export interface InsertReceipt { readonly replayed: boolean; readonly created_at: number; readonly result_until: number; readonly refuse_until: number; }
|
|
553
572
|
export interface InsertRequest<T> { readonly key: string; readonly createdAt: number; retry(): Promise<T>; }
|
|
554
573
|
export type InsertResult<T> = T & { readonly idempotency: InsertReceipt; readonly request: InsertRequest<InsertResult<T>> };
|
|
555
574
|
`;
|
|
556
|
-
var CLIENT_GENERATION =
|
|
557
|
-
var ACCEPTED_CLIENT_GENERATIONS = Object.freeze([3, 2, 1]);
|
|
575
|
+
var CLIENT_GENERATION = 5;
|
|
576
|
+
var ACCEPTED_CLIENT_GENERATIONS = Object.freeze([5, 4, 3, 2, 1]);
|
|
558
577
|
var PUBLIC_BOOTSTRAP_CLIENT_RUNTIME = 'const swCanon=(v,d)=>{if(v===null||typeof v==="string"||typeof v==="boolean"||(typeof v==="number"&&Number.isFinite(v)))return JSON.stringify(v);if(d===0||typeof v!=="object"||Array.isArray(v))return null;const parts=[];for(const k of Object.keys(v).sort()){if(v[k]===undefined)continue;const e=swCanon(v[k],d-1);if(e===null)return null;parts.push(JSON.stringify(k)+":"+e)}return"{"+parts.join(",")+"}"};let swBoot;const swBootTake=body=>{try{if(swBoot===undefined){swBoot=null;const el=typeof document!=="undefined"&&document.getElementById?document.getElementById("__sw_public_data"):null;if(el&&el.type==="application/json"&&(el.parentNode===document.head||el.parentNode===document.body)){const p=JSON.parse(el.textContent||"null");if(p&&p.v===1&&p.contract===contract&&Number.isSafeInteger(p.validated_at)&&p.validated_at<=Date.now()&&Number.isInteger(p.max_age)&&p.max_age>0&&p.max_age<=120&&p.entries&&typeof p.entries==="object"&&!Array.isArray(p.entries)&&typeof p.path==="string"&&typeof location!=="undefined")swBoot={until:p.validated_at+p.max_age*1000,path:p.path,entries:new Map(Object.entries(p.entries))}}if(el&&el.parentNode)el.parentNode.removeChild(el)}if(!swBoot)return undefined;if(Date.now()>=swBoot.until||location.pathname!==swBoot.path){swBoot=null;return undefined}const key=swCanon(body,2);if(key===null||!swBoot.entries.has(key))return undefined;const value=swBoot.entries.get(key);swBoot.entries.delete(key);return value}catch(error){swBoot=null;return undefined}};';
|
|
559
578
|
var PUBLIC_BOOTSTRAP_INVOKE = 'if(operation==="list"||operation==="get"||operation==="aggregate"||operation==="relation_list"){const booted=swBootTake({...input,contract,table,operation});if(booted!==undefined)return booted}else swBoot=null;';
|
|
560
579
|
function describeDataClient(authority, contract_digest, insertReceiptCapability, localTypes = false, generation = CLIENT_GENERATION) {
|
|
@@ -574,17 +593,19 @@ async function intake(table, reference, values, extra) {
|
|
|
574
593
|
for (const table of tables) {
|
|
575
594
|
const { name, columns, client, primaryKey } = table;
|
|
576
595
|
const readable = client.read === true ? columns.filter((column) => column.n !== table.author).map((column) => column.n) : client.read || [];
|
|
577
|
-
const
|
|
596
|
+
const hidden = generation >= 5 ? hiddenByBranch(table) : /* @__PURE__ */ new Set();
|
|
597
|
+
const row = shape(columns, readable, "read", /* @__PURE__ */ new Set(), hidden);
|
|
578
598
|
const id = columnType(columns.find((column) => column.n === primaryKey));
|
|
579
599
|
const operations = [];
|
|
580
600
|
const methods = [];
|
|
581
601
|
if (client.read !== false) {
|
|
582
|
-
const equality = readable.filter((field) => !["json", "blob"].includes(columns.find((column) => column.n === field).t));
|
|
602
|
+
const equality = readable.filter((field) => !["json", "blob"].includes(columns.find((column) => column.n === field).t) && !hidden.has(field));
|
|
583
603
|
if (generation >= 2) {
|
|
584
604
|
const listOptions = `limit?: L & (number extends L ? unknown : [L] extends [PageSize] ? unknown : never); after?: ${id}; where?: ${shape(columns, equality, "update")}`;
|
|
585
|
-
|
|
605
|
+
const hinted = generation >= 4 ? "RowHints" : "{ can: RowCan }";
|
|
606
|
+
operations.push(`list<const L extends number = number>(options: { ${listOptions}; can: true }): Promise<{ data: (${row} & ${hinted})[]; next: ${id} | null; has_more: boolean }>`);
|
|
586
607
|
operations.push(`list<const L extends number = number>(options?: { ${listOptions}; can?: false }): Promise<{ data: ${row}[]; next: ${id} | null; has_more: boolean }>`);
|
|
587
|
-
operations.push(`get(id: ${id}, options: { can: true }): Promise<{ data: (${row} & {
|
|
608
|
+
operations.push(`get(id: ${id}, options: { can: true }): Promise<{ data: (${row} & ${hinted}) | null }>`);
|
|
588
609
|
operations.push(`get(id: ${id}, options?: { can?: false }): Promise<{ data: ${row} | null }>`);
|
|
589
610
|
} else {
|
|
590
611
|
operations.push(`list<const L extends number = number>(options?: { limit?: L & (number extends L ? unknown : [L] extends [PageSize] ? unknown : never); after?: ${id}; where?: ${shape(columns, equality, "update")} }): Promise<{ data: ${row}[]; next: ${id} | null; has_more: boolean }>`);
|
|
@@ -721,7 +742,7 @@ async function intake(table, reference, values, extra) {
|
|
|
721
742
|
const declaration = `declare module ${JSON.stringify("somewhere:data")} {
|
|
722
743
|
type PageSize = ${Array.from({ length: 100 }, (_, i) => i + 1).join(" | ")};
|
|
723
744
|
export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
|
|
724
|
-
${generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INSERT_CLIENT_DECLARATIONS.replace("readonly idempotency:", "readonly idempotency?:").replace("readonly request:", "readonly request?:") : INSERT_CLIENT_DECLARATIONS : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_DECLARATIONS : ""}${intakeTables.length ? INTAKE_CLIENT_DECLARATIONS : ""} export class DataError extends Error { readonly status: number; readonly code: string | null;${receiptCapable ? " readonly request?: InsertRequest<unknown>; readonly outcome?: string; readonly write_committed?: boolean; readonly retry?: false; readonly retryable?: false; readonly idempotency?: InsertReceipt;" : ""} constructor(status: number, payload: unknown); }
|
|
745
|
+
${generation >= 4 ? ROW_HINTS_DECLARATION : generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INSERT_CLIENT_DECLARATIONS.replace("readonly idempotency:", "readonly idempotency?:").replace("readonly request:", "readonly request?:") : INSERT_CLIENT_DECLARATIONS : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_DECLARATIONS : ""}${intakeTables.length ? INTAKE_CLIENT_DECLARATIONS : ""} export class DataError extends Error { readonly status: number; readonly code: string | null;${receiptCapable ? " readonly request?: InsertRequest<unknown>; readonly outcome?: string; readonly write_committed?: boolean; readonly retry?: false; readonly retryable?: false; readonly idempotency?: InsertReceipt;" : ""} constructor(status: number, payload: unknown); }
|
|
725
746
|
export const data: { ${declarations.join(";\n ")} };
|
|
726
747
|
}
|
|
727
748
|
`;
|
|
@@ -1279,104 +1300,6 @@ var SENSITIVE_PARAMS = [
|
|
|
1279
1300
|
];
|
|
1280
1301
|
var SENSITIVE_PARAM_NAMES = new Set(SENSITIVE_PARAMS);
|
|
1281
1302
|
|
|
1282
|
-
// shared/draft-execution-authority.ts
|
|
1283
|
-
var DRAFT_PLATFORM_SERVICE_BINDING = "PLATFORM_API";
|
|
1284
|
-
|
|
1285
|
-
// worker/src/runtime/draft-auth-capability.ts
|
|
1286
|
-
var DRAFT_AUTH_CAPABILITY_JS = `
|
|
1287
|
-
const __sw_authOwnDescriptor = Object.getOwnPropertyDescriptor;
|
|
1288
|
-
const __sw_authBearerMatches = Function.prototype.call.bind(RegExp.prototype.test, /^Bearer [A-Za-z0-9_-]+[.][A-Za-z0-9_-]+[.][A-Za-z0-9_-]+$/);
|
|
1289
|
-
function __sw_authOwnValue(object, key) {
|
|
1290
|
-
if (!object || typeof object !== 'object') return undefined;
|
|
1291
|
-
const descriptor = __sw_authOwnDescriptor(object, key);
|
|
1292
|
-
return descriptor && descriptor.value;
|
|
1293
|
-
}
|
|
1294
|
-
function __sw_draftAuthRead(input, init, projectId) {
|
|
1295
|
-
if (input !== 'https://api.somewhere.tech/v1/auth/me') return null;
|
|
1296
|
-
const method = __sw_authOwnValue(init, 'method');
|
|
1297
|
-
const headers = __sw_authOwnValue(init, 'headers');
|
|
1298
|
-
const bearer = __sw_authOwnValue(headers, 'Authorization');
|
|
1299
|
-
if ((method !== undefined && method !== 'GET') || __sw_authOwnValue(init, 'body') != null
|
|
1300
|
-
|| typeof bearer !== 'string' || !__sw_authBearerMatches(bearer)) {
|
|
1301
|
-
const error = new Error('Draft identity verification requires an existing app access token.');
|
|
1302
|
-
error.code = 'DRAFT_PLATFORM_CAPABILITY_DENIED';
|
|
1303
|
-
throw error;
|
|
1304
|
-
}
|
|
1305
|
-
return {
|
|
1306
|
-
__proto__: null, method: 'GET', redirect: 'manual',
|
|
1307
|
-
headers: { __proto__: null, Authorization: bearer, 'X-Sw-Executing-Project': projectId, 'X-No-Auto-Refresh': '1' },
|
|
1308
|
-
};
|
|
1309
|
-
}
|
|
1310
|
-
`;
|
|
1311
|
-
|
|
1312
|
-
// worker/src/runtime/draft-platform-transport.ts
|
|
1313
|
-
var DRAFT_PLATFORM_TRANSPORT_JS = `// Exact draft database/files/log calls use a separate service binding that
|
|
1314
|
-
// native disallow_importable_env keeps outside customer module authority.
|
|
1315
|
-
let __sw_platformService = null;
|
|
1316
|
-
let __sw_draftPlatformProjectId = null;
|
|
1317
|
-
function __sw_bindPlatformService(env) {
|
|
1318
|
-
const binding = env && env.${DRAFT_PLATFORM_SERVICE_BINDING};
|
|
1319
|
-
if (!binding || typeof binding.fetch !== 'function') {
|
|
1320
|
-
throw new Error('Draft platform transport is unavailable.');
|
|
1321
|
-
}
|
|
1322
|
-
if (__sw_platformService && __sw_platformService !== binding) {
|
|
1323
|
-
throw new Error('Draft platform transport changed within one function isolate.');
|
|
1324
|
-
}
|
|
1325
|
-
const projectId = env && env.PROJECT_ID;
|
|
1326
|
-
if (typeof projectId !== 'string' || !projectId) {
|
|
1327
|
-
throw new Error('Draft platform transport has no project identity.');
|
|
1328
|
-
}
|
|
1329
|
-
if (__sw_draftPlatformProjectId && __sw_draftPlatformProjectId !== projectId) {
|
|
1330
|
-
throw new Error('Draft platform project identity changed within one function isolate.');
|
|
1331
|
-
}
|
|
1332
|
-
__sw_platformService = binding;
|
|
1333
|
-
__sw_draftPlatformProjectId = projectId;
|
|
1334
|
-
}
|
|
1335
|
-
${DRAFT_AUTH_CAPABILITY_JS}
|
|
1336
|
-
function __sw_platformFetch(input, init) {
|
|
1337
|
-
if (!__sw_platformService || !__sw_draftPlatformProjectId) {
|
|
1338
|
-
throw new Error('Draft platform transport is unavailable.');
|
|
1339
|
-
}
|
|
1340
|
-
const authRead = __sw_draftAuthRead(input, init, __sw_draftPlatformProjectId);
|
|
1341
|
-
if (authRead) return __sw_platformService.fetch('https://api.somewhere.tech/v1/auth/me', authRead);
|
|
1342
|
-
let url;
|
|
1343
|
-
let method;
|
|
1344
|
-
try {
|
|
1345
|
-
const request = input instanceof Request ? input : null;
|
|
1346
|
-
url = new URL(request ? request.url : String(input));
|
|
1347
|
-
method = String((init && init.method) || (request && request.method) || 'GET').toUpperCase();
|
|
1348
|
-
} catch (_) {
|
|
1349
|
-
const error = new Error('Draft platform capability denied an unreadable request.');
|
|
1350
|
-
error.code = 'DRAFT_PLATFORM_CAPABILITY_DENIED';
|
|
1351
|
-
throw error;
|
|
1352
|
-
}
|
|
1353
|
-
const fsRoot = '/v1/fs/' + encodeURIComponent(__sw_draftPlatformProjectId);
|
|
1354
|
-
const allowed =
|
|
1355
|
-
((url.pathname === '/v1/db/query' || url.pathname === '/v1/db/batch' || url.pathname === '/v1/db/raw-read' || url.pathname === '/v1/fs/upload-url') && method === 'POST') ||
|
|
1356
|
-
// Split storage: the structured managed endpoints. Each one requires this
|
|
1357
|
-
// exact draft identity and the artifact's signed release, and resolves the
|
|
1358
|
-
// ready clone of that draft and candidate itself (readManagedExecution).
|
|
1359
|
-
((url.pathname === '/v1/db/managed/user-memory' || url.pathname === '/v1/db/managed/user' || url.pathname === '/v1/db/managed/server' || url.pathname === '/v1/db/managed/public'
|
|
1360
|
-
|| url.pathname === '/v1/db/managed/visitor' || url.pathname === '/v1/db/managed/visitor-session') && method === 'POST') ||
|
|
1361
|
-
(url.pathname === '/v1/logs' && (method === 'GET' || method === 'POST')) ||
|
|
1362
|
-
(url.pathname === '/v1/traces' && method === 'POST') ||
|
|
1363
|
-
// A preview job's signed delivery: verified only for a job bound to this draft.
|
|
1364
|
-
(url.pathname === '/v1/jobs/verify-invocation' && method === 'POST') ||
|
|
1365
|
-
((url.pathname === fsRoot || url.pathname.startsWith(fsRoot + '/')) &&
|
|
1366
|
-
(method === 'GET' || method === 'PUT' || method === 'POST' || method === 'DELETE'));
|
|
1367
|
-
if (!allowed) {
|
|
1368
|
-
const error = new Error('Draft platform capability denied ' + method + ' ' + url.pathname + '.');
|
|
1369
|
-
error.code = 'DRAFT_PLATFORM_CAPABILITY_DENIED';
|
|
1370
|
-
throw error;
|
|
1371
|
-
}
|
|
1372
|
-
return __sw_platformService.fetch(input, init);
|
|
1373
|
-
}
|
|
1374
|
-
function __sw_hasExactDraftIdentity(request) {
|
|
1375
|
-
const draftId = request.headers.get('X-Sw-Draft-Id');
|
|
1376
|
-
const candidate = request.headers.get('X-Sw-Draft-Candidate');
|
|
1377
|
-
return !!(draftId && candidate);
|
|
1378
|
-
}`;
|
|
1379
|
-
|
|
1380
1303
|
// worker/src/utils/function-bundle-transport.ts
|
|
1381
1304
|
var NATIVE_RUNTIME_SERVICE_BINDING = "RUNTIME_PLATFORM_API";
|
|
1382
1305
|
var MANAGED_RUNTIME_SERVICE_BINDING = "RUNTIME_MANAGED_DATA";
|
|
@@ -2059,6 +1982,10 @@ async function __sw_hotConfigPull(env) {
|
|
|
2059
1982
|
}
|
|
2060
1983
|
`;
|
|
2061
1984
|
|
|
1985
|
+
// worker/src/utils/app-access-lifetime.ts
|
|
1986
|
+
var APP_ACCESS_MAX_SECONDS = 60;
|
|
1987
|
+
var APP_ACCESS_SELECTION_MARGIN_SECONDS = 5;
|
|
1988
|
+
|
|
2062
1989
|
// worker/src/runtime/access-token.ts
|
|
2063
1990
|
var ACCESS_TOKEN_JS = `const __sw_accessNow = Date.now.bind(Date);
|
|
2064
1991
|
const __sw_accessInteger = Number.isSafeInteger.bind(Number);
|
|
@@ -2079,7 +2006,7 @@ async function __sw_verifyAccessToken(binding, token, audience) {
|
|
|
2079
2006
|
|| typeof p.email !== 'string' || typeof p.sid !== 'string' || !p.sid || p.session_binding_version !== 1
|
|
2080
2007
|
|| !__sw_accessInteger(p.tv) || (p.role !== 'user' && p.role !== 'admin')
|
|
2081
2008
|
|| !__sw_accessInteger(p.iat) || !__sw_accessInteger(p.exp) || p.iat * 1000 > now || p.exp * 1000 <= now
|
|
2082
|
-
|| p.exp <= p.iat || p.exp - p.iat >
|
|
2009
|
+
|| p.exp <= p.iat || p.exp - p.iat > ${APP_ACCESS_MAX_SECONDS}
|
|
2083
2010
|
|| !p.user || Array.isArray(p.user) || p.user.id !== p.sub || p.user.email !== p.email || p.user.role !== p.role
|
|
2084
2011
|
|| 'impersonating' in p || 'impersonator_id' in p || 'impersonated_by' in p || 'registration_id' in p) return null;
|
|
2085
2012
|
return p;
|
|
@@ -2116,7 +2043,7 @@ var CONTEXT_HEAD_JS = `// \u2500\u2500 Primordial freeze (tsk_327fe8a4, opiniona
|
|
|
2116
2043
|
// JSON.parse = ... or Array.prototype.push = ... is a no-op in sloppy mode and
|
|
2117
2044
|
// throws in strict; either way the platform guard is untouched. Freezing the
|
|
2118
2045
|
// prototypes also blocks Object.prototype pollution. Monkey-patching core
|
|
2119
|
-
// built-ins is a
|
|
2046
|
+
// built-ins is a rare practice (a handful of old polyfill packages);
|
|
2120
2047
|
// modern code never needs it and it is explicitly UNSUPPORTED. We freeze only
|
|
2121
2048
|
// what the guard depends on (Reflect/Date are deliberately NOT frozen \u2014
|
|
2122
2049
|
// reflect-metadata / decorator libraries add to Reflect, and the guard no longer
|
|
@@ -2618,7 +2545,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
2618
2545
|
}
|
|
2619
2546
|
const isDraftExecution = draftId !== null;
|
|
2620
2547
|
const projectEnv = isDraftExecution ? 'draft' : (env.PROJECT_ENV || 'dev');
|
|
2621
|
-
const platformDomain = env.PLATFORM_DOMAIN || 'somewhere.
|
|
2548
|
+
const platformDomain = env.PLATFORM_DOMAIN || 'somewhere.cloud';
|
|
2622
2549
|
const tenantDomain = env.TENANT_DOMAIN || platformDomain;
|
|
2623
2550
|
const platformBase = env.PLATFORM_API_BASE || ('https://api.' + platformDomain);
|
|
2624
2551
|
const apiKey = env.PROJECT_API_KEY;
|
|
@@ -2658,8 +2585,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
2658
2585
|
// WHERE A STATEMENT ACTUALLY WENT, said once per request (tsk_5037f8f1).
|
|
2659
2586
|
// The per-statement number is the same 30-40 ms whether one statement runs
|
|
2660
2587
|
// or thirty, so the question is never "what did we add" but "how far away is
|
|
2661
|
-
// the database and which transport reached it"
|
|
2662
|
-
// unanswerable from a response. sw_db_via names the transport (the binding,
|
|
2588
|
+
// the database and which transport reached it". sw_db_via names the transport (the binding,
|
|
2663
2589
|
// or the platform data plane), sw_db_edge the colo the function ran in, and
|
|
2664
2590
|
// sw_db_home where the database answered from. Same-value edge and home mean
|
|
2665
2591
|
// the residue is ours; different values mean it is distance, and no amount
|
|
@@ -4343,7 +4269,7 @@ function createBoundedReplyReader(maxBytes) {
|
|
|
4343
4269
|
} catch {
|
|
4344
4270
|
return { ok: false, reason: "transport" };
|
|
4345
4271
|
} finally {
|
|
4346
|
-
clearTimeout(timer);
|
|
4272
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4347
4273
|
}
|
|
4348
4274
|
}
|
|
4349
4275
|
};
|
|
@@ -5360,6 +5286,7 @@ function renderPolicySql(request) {
|
|
|
5360
5286
|
const qualifier = r.qualifier;
|
|
5361
5287
|
const proposed = r.proposed;
|
|
5362
5288
|
const params = [];
|
|
5289
|
+
if (r.branch !== void 0 && (!policy || policy.k !== "a")) throw new Error("policy-sql: a branch admission needs an anyOf policy");
|
|
5363
5290
|
if (policy && policy.k === "o") {
|
|
5364
5291
|
if (proposed) return { sql: "1", params: [] };
|
|
5365
5292
|
params.push(r.principal);
|
|
@@ -5436,7 +5363,10 @@ function renderPolicySql(request) {
|
|
|
5436
5363
|
for (let i = 0; i < team.params.length; i++) gated.push(team.params[i]);
|
|
5437
5364
|
left = { sql: "(" + left.sql + " AND " + team.sql + ")", params: gated };
|
|
5438
5365
|
}
|
|
5366
|
+
if (r.branch === 0) return left;
|
|
5439
5367
|
const right = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p[1], operation: r.operation, qualifier, proposed, edge: r.edge });
|
|
5368
|
+
if (r.branch === 1) return right;
|
|
5369
|
+
if (r.branch !== void 0) throw new Error("policy-sql: an anyOf branch is 0 or 1");
|
|
5440
5370
|
for (let i = 0; i < left.params.length; i++) params.push(left.params[i]);
|
|
5441
5371
|
for (let i = 0; i < right.params.length; i++) params.push(right.params[i]);
|
|
5442
5372
|
return { sql: "(" + left.sql + " OR " + right.sql + ")", params };
|
|
@@ -5801,8 +5731,10 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
5801
5731
|
params.push(rendered.params[i]);
|
|
5802
5732
|
return rendered.sql;
|
|
5803
5733
|
},
|
|
5804
|
-
policyPredicate(policy, qualifier, proposedValues, operation) {
|
|
5805
|
-
|
|
5734
|
+
policyPredicate(policy, qualifier, proposedValues, operation, branch) {
|
|
5735
|
+
const request = { render: "predicate", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy, operation: operation || null, qualifier: qualifier || null, proposed: proposedValues || null };
|
|
5736
|
+
if (branch !== void 0) request.branch = branch;
|
|
5737
|
+
return helpers.policySql(context.renderPolicy(request));
|
|
5806
5738
|
},
|
|
5807
5739
|
memberScopePredicate(member, qualifier, operation) {
|
|
5808
5740
|
return helpers.policySql(context.renderPolicy({ render: "member", principal: principalId, table: ir.table.name, physical: context.physicalMap(), member, operation: operation || null, qualifier: qualifier || null }));
|
|
@@ -5926,7 +5858,7 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
5926
5858
|
if (scope.mode === "member")
|
|
5927
5859
|
return helpers.memberScopePredicate(scope.member, null, op);
|
|
5928
5860
|
if (scope.mode === "policy")
|
|
5929
|
-
return helpers.policyPredicate(scope.policy, null, null, op);
|
|
5861
|
+
return helpers.policyPredicate(scope.policy, null, null, op, scope.branch);
|
|
5930
5862
|
if (scope.mode === "link")
|
|
5931
5863
|
return helpers.linkGuard(scope.link, tableSql + "." + q(scope.link.column), null);
|
|
5932
5864
|
return null;
|
|
@@ -6011,8 +5943,11 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
6011
5943
|
let sql;
|
|
6012
5944
|
if (ir.kind === "select") {
|
|
6013
5945
|
const cols = ir.columns && ir.columns.length ? context.map(ir.columns, q).join(", ") : "*";
|
|
6014
|
-
const canCols = ir.can ? ", " + helpers.canHint(ir.can.update, "update") + " AS " + q("__sw_can_update") + ", " + helpers.canHint(ir.can.delete, "delete") + " AS " + q("__sw_can_delete") : "";
|
|
6015
|
-
|
|
5946
|
+
const canCols = ir.can ? ", " + helpers.canHint(ir.can.update, "update") + " AS " + q("__sw_can_update") + ", " + helpers.canHint(ir.can.delete, "delete") + " AS " + q("__sw_can_delete") + ", " + helpers.canHint(ir.can.mine, "read") + " AS " + q("__sw_mine") : "";
|
|
5947
|
+
let branchCols = "";
|
|
5948
|
+
for (let i = 0; i < (ir.branchReads || []).length; i++)
|
|
5949
|
+
branchCols += ", " + helpers.canHint(ir.branchReads[i], "read") + " AS " + q("__sw_branch_" + i);
|
|
5950
|
+
sql = "SELECT " + cols + canCols + branchCols + " FROM " + tableSql + helpers.whereSql();
|
|
6016
5951
|
if (ir.order && ir.order.length) {
|
|
6017
5952
|
sql += " ORDER BY " + context.map(ir.order, function(o) {
|
|
6018
5953
|
return q(o.column) + " " + (o.dir === "desc" ? "DESC" : "ASC");
|
|
@@ -6371,7 +6306,7 @@ var DB_GENERATED_OWNER_READ_JS = ` function __sw_registerGeneratedOwnerRead
|
|
|
6371
6306
|
if (!payload || payload.type !== 'app_user' || payload.project_id !== projectId
|
|
6372
6307
|
|| typeof payload.sub !== 'string' || !payload.sub || payload.sub.length > 200
|
|
6373
6308
|
|| typeof payload.exp !== 'number' || !__sw_generatedOwnerFinite(payload.exp)
|
|
6374
|
-
|| payload.exp - __sw_generatedOwnerNow() / 1000 <
|
|
6309
|
+
|| payload.exp - __sw_generatedOwnerNow() / 1000 < ${APP_ACCESS_SELECTION_MARGIN_SECONDS}) return null;
|
|
6375
6310
|
// A private header selector, never a principal/auth memo or live brand.
|
|
6376
6311
|
const selected = { token: token, selector: payload.sub };
|
|
6377
6312
|
return function (options) { return __sw_structFrom(table, options, undefined, selected); };
|
|
@@ -6446,7 +6381,11 @@ var DB_PUBLIC_READ_JS = DB_GENERATED_OWNER_READ_JS + ` function __sw_shared
|
|
|
6446
6381
|
}
|
|
6447
6382
|
if (canPrincipal === undefined) refuse();
|
|
6448
6383
|
const sameCaller = function (resolved) { return resolved && resolved.principalId === canPrincipal ? resolved.scope : null; };
|
|
6449
|
-
|
|
6384
|
+
// mine (tsk_311011c1): the column that records who created a row \u2014 the owner column, or a
|
|
6385
|
+
// shared table's author column \u2014 when the table has one. The hint renders it as a boolean.
|
|
6386
|
+
const creatorShape = __sw_structManagedShape(table);
|
|
6387
|
+
const creatorColumn = __sw_structResolveOwnerColumn(table) || (creatorShape && creatorShape.author) || null;
|
|
6388
|
+
can = { update: sameCaller(forUpdate), delete: sameCaller(forDelete), mine: creatorColumn ? { mode: 'user', ownerColumn: creatorColumn } : null };
|
|
6450
6389
|
}
|
|
6451
6390
|
// Composed joins (tsk_1186c9a): resolve declared relations. Both absent \u2192
|
|
6452
6391
|
// ir.include / ir.relationWhere stay unset and the IR (and its shape
|
|
@@ -6478,16 +6417,18 @@ var DB_PUBLIC_READ_JS = DB_GENERATED_OWNER_READ_JS + ` function __sw_shared
|
|
|
6478
6417
|
return result;
|
|
6479
6418
|
}
|
|
6480
6419
|
|
|
6481
|
-
// The renderer returns the hints as
|
|
6482
|
-
// can: { update, delete } and the internal columns never reach the caller.
|
|
6420
|
+
// The renderer returns the hints as internal 0/1 columns; each row instead carries
|
|
6421
|
+
// can: { update, delete } and mine, and the internal columns never reach the caller.
|
|
6483
6422
|
function __sw_structAttachCan(result) {
|
|
6484
6423
|
const rows = result && __sw_arrayIsArray(result.data) ? result.data : [];
|
|
6485
6424
|
for (let i = 0; i < rows.length; i++) {
|
|
6486
6425
|
const row = rows[i];
|
|
6487
6426
|
if (!row || typeof row !== 'object') continue;
|
|
6488
6427
|
row.can = { update: row.__sw_can_update === 1 || row.__sw_can_update === true, delete: row.__sw_can_delete === 1 || row.__sw_can_delete === true };
|
|
6428
|
+
row.mine = row.__sw_mine === 1 || row.__sw_mine === true;
|
|
6489
6429
|
delete row.__sw_can_update;
|
|
6490
6430
|
delete row.__sw_can_delete;
|
|
6431
|
+
delete row.__sw_mine;
|
|
6491
6432
|
}
|
|
6492
6433
|
}
|
|
6493
6434
|
|
|
@@ -7006,11 +6947,28 @@ var DB_POLICY_HELPERS_JS = `${ROLE_DOMAIN_MAP_JS}
|
|
|
7006
6947
|
}
|
|
7007
6948
|
return true;
|
|
7008
6949
|
}
|
|
6950
|
+
// Per-branch read lists (tsk_1ac01f6a): [owner list | null, other list | null], at the table's
|
|
6951
|
+
// own anyOf only; each list is a non-empty sorted set of lowercase column identifiers.
|
|
6952
|
+
function __sw_structPolicyReads(value) {
|
|
6953
|
+
if (!__sw_arrayIsArray(value) || value.length !== 2 || (value[0] === null && value[1] === null)) return false;
|
|
6954
|
+
for (let i = 0; i < 2; i++) {
|
|
6955
|
+
const list = value[i];
|
|
6956
|
+
if (list === null) continue;
|
|
6957
|
+
if (!__sw_arrayIsArray(list) || list.length === 0) return false;
|
|
6958
|
+
for (let j = 0; j < list.length; j++) {
|
|
6959
|
+
if (typeof list[j] !== 'string' || !__sw_reMatch(/^[a-z_][a-z0-9_]*$/, list[j]) || (j > 0 && !(list[j - 1] < list[j]))) return false;
|
|
6960
|
+
}
|
|
6961
|
+
}
|
|
6962
|
+
return true;
|
|
6963
|
+
}
|
|
7009
6964
|
// anyOf(owner, member | group | app role); a group or app-role branch is
|
|
7010
|
-
// a pinned leaf, exactly as at the root.
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
6965
|
+
// a pinned leaf, exactly as at the root. root: the table's own rule may
|
|
6966
|
+
// carry per-branch read lists (reads); under allOf or a parent it may not.
|
|
6967
|
+
function __sw_structPolicyAny(value, root) {
|
|
6968
|
+
if (!value || typeof value !== 'object' || value.k !== 'a') return false;
|
|
6969
|
+
const anyKeys = __sw_keys(value).slice().sort().join(',');
|
|
6970
|
+
if (anyKeys !== 'k,p' && !(root === true && anyKeys === 'k,p,reads' && __sw_structPolicyReads(value.reads))) return false;
|
|
6971
|
+
if (!__sw_arrayIsArray(value.p) || value.p.length !== 2 || !__sw_structPolicyLeaf(value.p[0]) || value.p[0].k !== 'o') return false;
|
|
7014
6972
|
const second = value.p[1];
|
|
7015
6973
|
if (second && second.k === 'm' && second.m && second.m.og && value.p[0].c !== 'user_id') return false;
|
|
7016
6974
|
return (__sw_structPolicyLeaf(second) && second.k === 'm') || __sw_structPolicyGroup(second) || __sw_structPolicyRole(second);
|
|
@@ -7041,7 +6999,7 @@ var DB_POLICY_HELPERS_JS = `${ROLE_DOMAIN_MAP_JS}
|
|
|
7041
6999
|
}
|
|
7042
7000
|
// A parent chain: at most four edges, each ending in a leaf, anyOf or allOf.
|
|
7043
7001
|
function __sw_structPolicyValid(value, edges) {
|
|
7044
|
-
if (!edges && (__sw_structPolicyAny(value) || __sw_structPolicyAllOf(value) || __sw_structPolicyGroup(value) || __sw_structPolicyRole(value))) return true;
|
|
7002
|
+
if (!edges && (__sw_structPolicyAny(value, true) || __sw_structPolicyAllOf(value) || __sw_structPolicyGroup(value) || __sw_structPolicyRole(value))) return true;
|
|
7045
7003
|
const above = edges || 0;
|
|
7046
7004
|
return value && typeof value === 'object' && __sw_keys(value).slice().sort().join(',') === 'k,p,pk,t,v' && value.k === 'p'
|
|
7047
7005
|
&& above + 1 <= 4
|
|
@@ -7752,7 +7710,7 @@ var DB_JS = ` db: (function () {
|
|
|
7752
7710
|
// silently running the SQL as written \u2014 is what keeps every existing
|
|
7753
7711
|
// scoped call from becoming an unscoped full-table read.
|
|
7754
7712
|
function __sw_throwRawSqlCannotBePlatformScoped(api) {
|
|
7755
|
-
const err = new Error(api + '
|
|
7713
|
+
const err = new Error(api + ' does not apply platform scoping to raw SQL. ' +
|
|
7756
7714
|
'Platform-proven scoping exists only on structured queries the platform composes. ' +
|
|
7757
7715
|
'Use sw.db.from/count/insert/update/remove/tx for declared operations. ' +
|
|
7758
7716
|
'For intentional server-authority SQL, authorize the caller first, then use sw.db.server.query/batch; your function is responsible for that authorization.');
|
|
@@ -9458,7 +9416,7 @@ ${DB_AGGREGATE_JS}
|
|
|
9458
9416
|
// Structured query builder (tsk_5b91e0e4). The platform composes,
|
|
9459
9417
|
// renders, and scopes these \u2014 see the __sw_struct* helpers above.
|
|
9460
9418
|
// sw.db.delete is a trivial alias of sw.db.remove (both call the same
|
|
9461
|
-
// composer).
|
|
9419
|
+
// composer). A scope argument fails loudly, never silently.
|
|
9462
9420
|
from(table, opts, legacyScope) { __sw_structNoScopeArg(legacyScope, 'from'); return __sw_structFrom(table, opts); },
|
|
9463
9421
|
live(name, read) {
|
|
9464
9422
|
if (arguments.length !== 2) __sw_structThrow('sw.db.live takes exactly (name, read).');
|
|
@@ -9469,9 +9427,9 @@ ${DB_AGGREGATE_JS}
|
|
|
9469
9427
|
// group-by over the rows the caller is allowed to read.
|
|
9470
9428
|
aggregate(table, opts, legacyScope) { __sw_structNoScopeArg(legacyScope, 'aggregate'); return __sw_structAggregate(table, opts); },
|
|
9471
9429
|
// insert's third argument is the OPTIONS object ({ onConflict } \u2014
|
|
9472
|
-
// tsk_3e9b033c). The
|
|
9473
|
-
//
|
|
9474
|
-
//
|
|
9430
|
+
// tsk_3e9b033c). The scope-argument shapes ({ user }, { unscoped })
|
|
9431
|
+
// fail loudly: they would change semantics, so they are never read
|
|
9432
|
+
// as options.
|
|
9475
9433
|
insert(table, values, opts) {
|
|
9476
9434
|
if (opts !== undefined && opts !== null && typeof opts === 'object' && !__sw_arrayIsArray(opts) && (__sw_own(opts, 'user') || __sw_own(opts, 'unscoped'))) {
|
|
9477
9435
|
__sw_structNoScopeArg(opts, 'insert');
|
|
@@ -9639,7 +9597,7 @@ var DB_RAW_JS = ` var __sw_rawReadDbInstance = null;
|
|
|
9639
9597
|
__sw_assertSingleStatement(sql);
|
|
9640
9598
|
__sw_assertNoDdl(sql);
|
|
9641
9599
|
options = options || {};
|
|
9642
|
-
// { user } is
|
|
9600
|
+
// { user } is refused (tsk_fde5d117): the platform never rewrites raw
|
|
9643
9601
|
// SQL to inject an owner filter. Preserve its specific loud failure.
|
|
9644
9602
|
if (__sw_own(options, 'user')) {
|
|
9645
9603
|
__sw_throwRawSqlCannotBePlatformScoped('sw.db.query');
|
|
@@ -11973,7 +11931,7 @@ var AI_JS = ` ai: (function () {
|
|
|
11973
11931
|
// "undefined is not a function". Build the tool explicitly with
|
|
11974
11932
|
// sw.db + sw.ai.chatWithTools, or use the manifest-based tool when it lands.
|
|
11975
11933
|
function catalogTool() {
|
|
11976
|
-
const err = new Error('sw.ai.catalogTool
|
|
11934
|
+
const err = new Error('sw.ai.catalogTool is not part of the runtime. Assemble the search tool explicitly from sw.db + sw.ai.chatWithTools, or use the manifest-based AI DB tool. The platform docs (ai) show both.');
|
|
11977
11935
|
err.code = 'CATALOG_TOOL_RETIRED';
|
|
11978
11936
|
err.status = 400;
|
|
11979
11937
|
throw __sw_markExpected(err);
|
|
@@ -14301,7 +14259,7 @@ ${AUTH_SERVER_JS}
|
|
|
14301
14259
|
return { id: visitor.id, isAnon: true, expiresAt: visitor.expiresAt };
|
|
14302
14260
|
},
|
|
14303
14261
|
async migrateAnon() {
|
|
14304
|
-
const err = new Error('Caller-selected anonymous row migrations are
|
|
14262
|
+
const err = new Error('Caller-selected anonymous row migrations are not supported. Use declared visitor ownership.');
|
|
14305
14263
|
err.code = 'MIGRATE_ANON_REMOVED'; err.status = 400; err.__sw_expose = true;
|
|
14306
14264
|
throw err;
|
|
14307
14265
|
},
|
|
@@ -16009,7 +15967,7 @@ var CONTEXT_TAIL_JS = ` };
|
|
|
16009
15967
|
// teaches nothing (pfb_3645abbfa717). One thrower, mounted on both the
|
|
16010
15968
|
// standard sw and the draft proxy, so the answer is identical either way.
|
|
16011
15969
|
function __sw_realtimeRetired() {
|
|
16012
|
-
const err = new Error('sw.realtime is
|
|
15970
|
+
const err = new Error('sw.realtime is not part of the runtime. For browser live updates, declare the read in a function with sw.db.live(name, sw.db.from(...)) and subscribe with watchLive from /__sw/live/client.js. For durable events and history, write rows with sw.db.');
|
|
16013
15971
|
err.code = 'REALTIME_RETIRED';
|
|
16014
15972
|
err.status = 410;
|
|
16015
15973
|
throw __sw_markExpected(err);
|
|
@@ -16231,6 +16189,17 @@ var import_declared_data_contract = __toESM(require_declared_data_contract());
|
|
|
16231
16189
|
var PUBLIC_RESULT_MAX_AGE_SECONDS = 120;
|
|
16232
16190
|
|
|
16233
16191
|
// worker/src/runtime/declared-data-read-plan.ts
|
|
16192
|
+
function declaredBranchReads(shape) {
|
|
16193
|
+
const policy = shape.policy;
|
|
16194
|
+
if (!policy || policy.k !== "a" || !Array.isArray(policy.reads)) return null;
|
|
16195
|
+
const reads = policy.reads;
|
|
16196
|
+
const hidden = [];
|
|
16197
|
+
for (const list of reads) {
|
|
16198
|
+
if (!list) continue;
|
|
16199
|
+
for (const column of shape.columns) if (list.indexOf(column.n) === -1 && hidden.indexOf(column.n) === -1) hidden.push(column.n);
|
|
16200
|
+
}
|
|
16201
|
+
return { reads, hidden };
|
|
16202
|
+
}
|
|
16234
16203
|
function declaredReadFields(shape) {
|
|
16235
16204
|
const policy = shape.client;
|
|
16236
16205
|
return policy.read === true ? shape.columns.filter((column) => column.n !== shape.author).map((column) => column.n) : Array.isArray(policy.read) ? policy.read : [];
|
|
@@ -16256,6 +16225,14 @@ function declaredListGetPlan(operation, input, shape, fields, fail) {
|
|
|
16256
16225
|
};
|
|
16257
16226
|
const primaryKey = shape.clientPrimaryKey;
|
|
16258
16227
|
const primaryColumn = shape.columns.find((column) => column.n === primaryKey);
|
|
16228
|
+
const branchPolicy = shape.policy;
|
|
16229
|
+
const branchHidden = [];
|
|
16230
|
+
if (branchPolicy && branchPolicy.k === "a" && Array.isArray(branchPolicy.reads)) {
|
|
16231
|
+
for (const list of branchPolicy.reads) {
|
|
16232
|
+
if (!list) continue;
|
|
16233
|
+
for (const column of shape.columns) if (list.indexOf(column.n) === -1 && branchHidden.indexOf(column.n) === -1) branchHidden.push(column.n);
|
|
16234
|
+
}
|
|
16235
|
+
}
|
|
16259
16236
|
const where = /* @__PURE__ */ Object.create(null);
|
|
16260
16237
|
if (operation === "get") helpers.and(where, primaryKey, helpers.key(input.id, primaryColumn));
|
|
16261
16238
|
else {
|
|
@@ -16265,6 +16242,9 @@ function declaredListGetPlan(operation, input, shape, fields, fail) {
|
|
|
16265
16242
|
for (const field of Object.keys(filters)) {
|
|
16266
16243
|
const column = shape.columns.find((item) => item.n === field);
|
|
16267
16244
|
const value = filters[field];
|
|
16245
|
+
if (branchHidden.indexOf(field) !== -1) {
|
|
16246
|
+
fail(400, "DATA_INPUT_INVALID", 'where cannot name "' + field + '": an access branch of this table leaves it out, so only columns every reader may read can filter.');
|
|
16247
|
+
}
|
|
16268
16248
|
if (!fields.includes(field) || !column || column.t === "json" || column.t === "blob" || !(value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))) {
|
|
16269
16249
|
fail(400, "DATA_INPUT_INVALID", "where accepts only equality values for readable scalar fields.");
|
|
16270
16250
|
}
|
|
@@ -16285,9 +16265,12 @@ function declaredListGetPayload(operation, rows, fields, primaryKey, limit) {
|
|
|
16285
16265
|
project(row) {
|
|
16286
16266
|
if (row === null || row === void 0) return null;
|
|
16287
16267
|
const result = /* @__PURE__ */ Object.create(null);
|
|
16288
|
-
for (const field of fields) result[field] = row[field];
|
|
16268
|
+
for (const field of fields) if (Object.prototype.hasOwnProperty.call(row, field)) result[field] = row[field];
|
|
16289
16269
|
const can = row.can;
|
|
16290
|
-
if (can && typeof can === "object")
|
|
16270
|
+
if (can && typeof can === "object") {
|
|
16271
|
+
result.can = { update: can.update === true, delete: can.delete === true };
|
|
16272
|
+
result.mine = row.mine === true;
|
|
16273
|
+
}
|
|
16291
16274
|
return result;
|
|
16292
16275
|
}
|
|
16293
16276
|
};
|
|
@@ -16300,7 +16283,8 @@ function declaredListGetPayload(operation, rows, fields, primaryKey, limit) {
|
|
|
16300
16283
|
has_more: hasMore
|
|
16301
16284
|
};
|
|
16302
16285
|
}
|
|
16303
|
-
var DECLARED_DATA_READ_PLAN_SOURCE = `const
|
|
16286
|
+
var DECLARED_DATA_READ_PLAN_SOURCE = `const __sw_dataBranchReads = (${declaredBranchReads.toString()});
|
|
16287
|
+
const __sw_dataReadFields = (${declaredReadFields.toString()});
|
|
16304
16288
|
const __sw_dataListGetPlan = (${declaredListGetPlan.toString()});
|
|
16305
16289
|
const __sw_dataListGetPayload = (${declaredListGetPayload.toString()});`;
|
|
16306
16290
|
|
|
@@ -16487,9 +16471,10 @@ function __sw_dataAnd(where, field, predicate) {
|
|
|
16487
16471
|
// decoded once at the database seam (booleans, json, blob), so this only
|
|
16488
16472
|
// narrows a row to the fields the client may read.
|
|
16489
16473
|
function __sw_dataProject(row, fields) {
|
|
16474
|
+
// A column an access branch withheld from this row (tsk_1ac01f6a) is absent, not null.
|
|
16490
16475
|
if (row === null || row === undefined) return null;
|
|
16491
16476
|
const result = Object.create(null);
|
|
16492
|
-
for (const field of fields) result[field] = row[field];
|
|
16477
|
+
for (const field of fields) if (Object.prototype.hasOwnProperty.call(row, field)) result[field] = row[field];
|
|
16493
16478
|
return result;
|
|
16494
16479
|
}
|
|
16495
16480
|
// Named spans for this handler's own awaits (tsk_c276e2d8). The context's
|
|
@@ -16536,8 +16521,9 @@ async function __sw_dataRead(sw, table, policy, options, verb) {
|
|
|
16536
16521
|
}
|
|
16537
16522
|
}
|
|
16538
16523
|
/** Whether this release's pinned contract grants the browser any operation (hasDeclaredDataAccess, at run time). */
|
|
16524
|
+
// Load-time Object.values and Array#some: customer code may replace globals in an earlier request (tsk_ab656f8e).
|
|
16539
16525
|
function __sw_dataAnyClientGrant() {
|
|
16540
|
-
return
|
|
16526
|
+
return __sw_generatedOwnerSome(__sw_generatedOwnerValues(__SW_DATA_SCHEMA), ({ client }) => client && (client.read !== false || client.create !== null
|
|
16541
16527
|
|| client.update !== null || client.delete || client.intake !== undefined || client.links !== undefined));
|
|
16542
16528
|
}
|
|
16543
16529
|
/** The exact current data contract digest (one computation per isolate). */
|
|
@@ -16644,7 +16630,7 @@ async function __sw_handleDeclaredData(request, sw, env) {
|
|
|
16644
16630
|
}
|
|
16645
16631
|
// Membership records are authority, even if a stale or forged bake exposed
|
|
16646
16632
|
// them. Never let the generic browser writer grant membership in a group.
|
|
16647
|
-
if (!reading &&
|
|
16633
|
+
if (!reading && __sw_generatedOwnerSome(__sw_generatedOwnerValues(__SW_DATA_SCHEMA), item => item.member && item.member.m === table || __sw_dataPolicyUsesMembership(item.policy, table))) {
|
|
16648
16634
|
__sw_dataFail(403, 'DATA_ACCESS_DENIED', 'Membership changes require a server function.');
|
|
16649
16635
|
}
|
|
16650
16636
|
if (policy.identity === 'visitor' && shape.visitors !== true) {
|
|
@@ -16674,12 +16660,16 @@ async function __sw_handleDeclaredData(request, sw, env) {
|
|
|
16674
16660
|
const childFields = childPolicy.read === true ? child.columns.filter(column => column.n !== child.author).map(column => column.n) : childPolicy.read;
|
|
16675
16661
|
const childPrimaryKey = child.clientPrimaryKey;
|
|
16676
16662
|
const childPrimaryColumn = child.columns.find(column => column.n === childPrimaryKey);
|
|
16663
|
+
const childBranch = __sw_dataBranchReads(child);
|
|
16677
16664
|
const where = Object.create(null);
|
|
16678
16665
|
__sw_dataAnd(where, relation.fk, parentId);
|
|
16679
16666
|
if (input.where !== undefined) {
|
|
16680
16667
|
__sw_dataObject(input.where, 'where');
|
|
16681
16668
|
for (const field of Object.keys(input.where)) {
|
|
16682
16669
|
const column = child.columns.find(column => column.n === field), value = input.where[field];
|
|
16670
|
+
if (childBranch && childBranch.hidden.includes(field)) {
|
|
16671
|
+
__sw_dataFail(400, 'DATA_INPUT_INVALID', 'where cannot name "' + field + '": an access branch of this table leaves it out, so only columns every reader may read can filter.');
|
|
16672
|
+
}
|
|
16683
16673
|
if (!childFields.includes(field) || !column || column.t === 'json' || column.t === 'blob'
|
|
16684
16674
|
|| !(value === null || typeof value === 'string' || typeof value === 'boolean'
|
|
16685
16675
|
|| typeof value === 'number' && Number.isFinite(value))) {
|
|
@@ -16709,9 +16699,11 @@ async function __sw_handleDeclaredData(request, sw, env) {
|
|
|
16709
16699
|
// COUNT or MAX over a non-readable column would answer questions about
|
|
16710
16700
|
// fields this client was never granted.
|
|
16711
16701
|
const options = Object.create(null);
|
|
16702
|
+
// A column some access branch leaves out (tsk_1ac01f6a) never feeds a filter, grouping or aggregate.
|
|
16703
|
+
const branchReads = __sw_dataBranchReads(shape);
|
|
16712
16704
|
const readableScalar = fields.filter(field => {
|
|
16713
16705
|
const column = shape.columns.find(item => item.n === field);
|
|
16714
|
-
return column && column.t !== 'json' && column.t !== 'blob';
|
|
16706
|
+
return column && column.t !== 'json' && column.t !== 'blob' && !(branchReads && branchReads.hidden.includes(field));
|
|
16715
16707
|
});
|
|
16716
16708
|
const aggregateField = (key) => {
|
|
16717
16709
|
const field = input[key];
|
|
@@ -16836,7 +16828,7 @@ async function __sw_handleDeclaredData(request, sw, env) {
|
|
|
16836
16828
|
// operation must be declared for the browser client, and membership tables are never written
|
|
16837
16829
|
// from the browser. A hint only; the write itself still enforces.
|
|
16838
16830
|
if (plan.options.can) {
|
|
16839
|
-
const membership =
|
|
16831
|
+
const membership = __sw_generatedOwnerSome(__sw_generatedOwnerValues(__SW_DATA_SCHEMA), item => item.member && item.member.m === table || __sw_dataPolicyUsesMembership(item.policy, table));
|
|
16840
16832
|
const declared = (op) => !membership && !!policy[op] && policy[op] !== null;
|
|
16841
16833
|
for (const row of result.data || []) {
|
|
16842
16834
|
if (row && row.can) row.can = { update: row.can.update === true && declared('update'), delete: row.can.delete === true && declared('delete') };
|
|
@@ -17803,7 +17795,7 @@ function __sw_postTrace(env, ctx, t, spans, status, platformFetch) {
|
|
|
17803
17795
|
try {
|
|
17804
17796
|
__sw_traceClose(t);
|
|
17805
17797
|
if (__sw_traceWouldBeDropped(t.traceId, spans, status)) return;
|
|
17806
|
-
const platformDomain = env.PLATFORM_DOMAIN || 'somewhere.
|
|
17798
|
+
const platformDomain = env.PLATFORM_DOMAIN || 'somewhere.cloud';
|
|
17807
17799
|
const platformBase = env.PLATFORM_API_BASE || ('https://api.' + platformDomain);
|
|
17808
17800
|
const ingest = __sw_telemetryIngestActive(env);
|
|
17809
17801
|
ctx.waitUntil(
|
|
@@ -17854,7 +17846,7 @@ function __sw_fitErrorData(data, ingest) {
|
|
|
17854
17846
|
/** Best-effort function exception reporting correlated to the same trace. */
|
|
17855
17847
|
async function reportFunctionError(env, request, code, err, responseStatus, requestId, routePath, platformFetch, platformCtx, responseCode) {
|
|
17856
17848
|
try {
|
|
17857
|
-
const platformDomain = env.PLATFORM_DOMAIN || 'somewhere.
|
|
17849
|
+
const platformDomain = env.PLATFORM_DOMAIN || 'somewhere.cloud';
|
|
17858
17850
|
const platformBase = env.PLATFORM_API_BASE || ('https://api.' + platformDomain);
|
|
17859
17851
|
const apiKey = env.PROJECT_API_KEY;
|
|
17860
17852
|
if (!apiKey || !env.PROJECT_ID) return;
|
|
@@ -18016,6 +18008,9 @@ function parseClientPermissions(value, columns, scope) {
|
|
|
18016
18008
|
if (Array.isArray(read)) {
|
|
18017
18009
|
const names = [];
|
|
18018
18010
|
for (const entry of read) {
|
|
18011
|
+
if (typeof entry === "string" && ((scope.kind === "owner" || scope.kind === "policy") && entry.toLowerCase() === scope.column || entry.toLowerCase() === SHARED_AUTHOR_COLUMN)) {
|
|
18012
|
+
return fail(`client.read cannot list "${entry}": the platform assigns who created each row and never sends that identity to the browser. To show the signed-in user which rows are theirs, read with { can: true }: each row then carries mine (true for rows this user created) and can.update / can.delete.`);
|
|
18013
|
+
}
|
|
18019
18014
|
if (typeof entry !== "string" || !fields.has(entry.toLowerCase())) return fail("client.read must contain only declared column names.");
|
|
18020
18015
|
const name = entry.toLowerCase();
|
|
18021
18016
|
if (names.includes(name)) return fail(`client.read declares field "${entry}" twice.`);
|
|
@@ -24426,6 +24421,7 @@ function readGrantScope(r, table, line, fail) {
|
|
|
24426
24421
|
r.expectPunct("{", "in grant options");
|
|
24427
24422
|
let membership, grantee, row;
|
|
24428
24423
|
let operations;
|
|
24424
|
+
let read;
|
|
24429
24425
|
const seen = /* @__PURE__ */ new Set();
|
|
24430
24426
|
while (!r.tryPunct("}")) {
|
|
24431
24427
|
const key = r.readKey("in grant options");
|
|
@@ -24453,7 +24449,9 @@ function readGrantScope(r, table, line, fail) {
|
|
|
24453
24449
|
}
|
|
24454
24450
|
if (!ops.includes("read")) fail(key.line, "grant operations require read; update alone is not allowed.");
|
|
24455
24451
|
operations = ops.includes("update") ? ["read", "update"] : ["read"];
|
|
24456
|
-
} else
|
|
24452
|
+
} else if (key.name === "read") {
|
|
24453
|
+
read = readBranchReadColumns(r, `grant({ read }) on table "${table}"`);
|
|
24454
|
+
} else fail(key.line, `Unknown grant option "${key.name}". Allowed: table, grantee, row, operations, read.`);
|
|
24457
24455
|
if (!r.tryPunct(",")) {
|
|
24458
24456
|
r.expectPunct("}", "closing grant options");
|
|
24459
24457
|
break;
|
|
@@ -24463,7 +24461,7 @@ function readGrantScope(r, table, line, fail) {
|
|
|
24463
24461
|
r.expectPunct(")", "closing grant");
|
|
24464
24462
|
if (!membership || !grantee || !operations || grantee === "user_id") fail(line, `grant() on "${table}" requires table, a grantee column distinct from managed user_id, and explicit operations.`);
|
|
24465
24463
|
if (row !== void 0 && (row === "user_id" || row === grantee)) fail(line, `grant() on "${table}": row names the shares column holding the shared row's id, distinct from user_id and the grantee.`);
|
|
24466
|
-
return { kind: "member", group: ["user_id"], membership, memberUser: grantee, memberGroup: row === void 0 ? ["user_id"] : ["user_id", row], operations, ownerGrant: true };
|
|
24464
|
+
return { kind: "member", group: ["user_id"], membership, memberUser: grantee, memberGroup: row === void 0 ? ["user_id"] : ["user_id", row], operations, ownerGrant: true, ...read ? { read } : {} };
|
|
24467
24465
|
}
|
|
24468
24466
|
function resolveGrantRowKeys(scope, key, table, line, fail) {
|
|
24469
24467
|
const visit = (node) => {
|
|
@@ -25391,6 +25389,7 @@ function readRoleList(r, fail, ctx) {
|
|
|
25391
25389
|
function readGroupScope(r, tableName, line, fail) {
|
|
25392
25390
|
r.expectPunct("(", 'after "group"');
|
|
25393
25391
|
let column = DEFAULT_GROUP_COLUMN;
|
|
25392
|
+
let read;
|
|
25394
25393
|
const explicit = {};
|
|
25395
25394
|
if (!r.tryPunct(")")) {
|
|
25396
25395
|
r.expectPunct("{", `in group() options for table "${tableName}"`);
|
|
@@ -25419,8 +25418,10 @@ function readGroupScope(r, tableName, line, fail) {
|
|
|
25419
25418
|
break;
|
|
25420
25419
|
}
|
|
25421
25420
|
}
|
|
25421
|
+
} else if (key.name === "read") {
|
|
25422
|
+
read = readBranchReadColumns(r, `group({ read }) on table "${tableName}"`);
|
|
25422
25423
|
} else {
|
|
25423
|
-
fail(key.line, `group() on table "${tableName}" has an unknown option "${key.name}". Allowed: column, roles.`);
|
|
25424
|
+
fail(key.line, `group() on table "${tableName}" has an unknown option "${key.name}". Allowed: column, roles, read.`);
|
|
25424
25425
|
}
|
|
25425
25426
|
if (!r.tryPunct(",")) {
|
|
25426
25427
|
r.expectPunct("}", `closing group() options for table "${tableName}"`);
|
|
@@ -25430,7 +25431,7 @@ function readGroupScope(r, tableName, line, fail) {
|
|
|
25430
25431
|
r.tryPunct(",");
|
|
25431
25432
|
r.expectPunct(")", "closing group()");
|
|
25432
25433
|
}
|
|
25433
|
-
const node = { kind: "group", column, roles: { read: [], create: [], update: [], delete: [] } };
|
|
25434
|
+
const node = { kind: "group", column, roles: { read: [], create: [], update: [], delete: [] }, ...read ? { read } : {} };
|
|
25434
25435
|
declaredGroupRoles.set(node, { line, explicit });
|
|
25435
25436
|
return node;
|
|
25436
25437
|
}
|
|
@@ -25539,7 +25540,17 @@ function readAppRoleScope(r, tableName, line, fail) {
|
|
|
25539
25540
|
let sawRoles = false;
|
|
25540
25541
|
while (!r.tryPunct("}")) {
|
|
25541
25542
|
const key = r.readKey(`for an option of ${where}`);
|
|
25542
|
-
if (key.name
|
|
25543
|
+
if (key.name === "read") {
|
|
25544
|
+
if (node.read) fail(key.line, `${where} declares read twice.`);
|
|
25545
|
+
r.expectPunct(":", `after "read" in ${where}`);
|
|
25546
|
+
node.read = readBranchReadColumns(r, `appRole({ read }) on table "${tableName}"`);
|
|
25547
|
+
if (!r.tryPunct(",")) {
|
|
25548
|
+
r.expectPunct("}", `closing ${where}`);
|
|
25549
|
+
break;
|
|
25550
|
+
}
|
|
25551
|
+
continue;
|
|
25552
|
+
}
|
|
25553
|
+
if (key.name !== "roles") fail(key.line, `${where} has an unknown option "${key.name}". Allowed: roles, read.`);
|
|
25543
25554
|
if (sawRoles) fail(key.line, `${where} declares roles twice.`);
|
|
25544
25555
|
sawRoles = true;
|
|
25545
25556
|
r.expectPunct(":", `after "roles" in ${where}`);
|
|
@@ -25819,6 +25830,7 @@ var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
|
|
|
25819
25830
|
]);
|
|
25820
25831
|
var SAFE_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
25821
25832
|
var RESERVED_PREFIXES = ["app_", "sqlite_", "d1_", "__sw_"];
|
|
25833
|
+
var ROW_HINT_FIELDS = ["can", "mine"];
|
|
25822
25834
|
var MAX_TABLES = 100;
|
|
25823
25835
|
var MAX_COLUMNS = 100;
|
|
25824
25836
|
var MAX_INDEXES = 40;
|
|
@@ -26066,6 +26078,9 @@ function checkDeclaredName(kind, raw, line) {
|
|
|
26066
26078
|
if (name.startsWith("_") || RESERVED_PREFIXES.some((p) => name.startsWith(p))) {
|
|
26067
26079
|
throw new SchemaTsError(`line ${line}: ${kind} name "${raw}" uses a reserved platform prefix and cannot be declared: names starting with "app_" or "_" belong to the platform.`);
|
|
26068
26080
|
}
|
|
26081
|
+
if (kind === "Column" && ROW_HINT_FIELDS.includes(name)) {
|
|
26082
|
+
throw new SchemaTsError(`line ${line}: column name "${raw}" is reserved for row hints: a read with { can: true } returns can and mine on every row, so a column cannot take either name.`);
|
|
26083
|
+
}
|
|
26069
26084
|
return name;
|
|
26070
26085
|
}
|
|
26071
26086
|
function readColumn(r, tableName, rawName, line) {
|
|
@@ -26255,6 +26270,7 @@ function readMemberScope(r, tableName, scopeLine) {
|
|
|
26255
26270
|
let memberUser = null;
|
|
26256
26271
|
let memberGroup = null;
|
|
26257
26272
|
let operations, operationsSeen = false;
|
|
26273
|
+
let read;
|
|
26258
26274
|
while (!r.tryPunct("}")) {
|
|
26259
26275
|
const key = r.readKey(`for a member() option of table "${tableName}"`);
|
|
26260
26276
|
r.expectPunct(":", `after "${key.name}" in member() for table "${tableName}"`);
|
|
@@ -26270,9 +26286,12 @@ function readMemberScope(r, tableName, scopeLine) {
|
|
|
26270
26286
|
if (operationsSeen) throw new SchemaTsError(`line ${key.line}: member() on table "${tableName}" declares operations more than once.`);
|
|
26271
26287
|
operationsSeen = true;
|
|
26272
26288
|
operations = readMemberOperations(r, tableName);
|
|
26289
|
+
} else if (key.name === "read") {
|
|
26290
|
+
if (read) throw new SchemaTsError(`line ${key.line}: member() on table "${tableName}" declares read more than once.`);
|
|
26291
|
+
read = readBranchReadColumns(r, `member({ read }) on table "${tableName}"`);
|
|
26273
26292
|
} else {
|
|
26274
26293
|
throw new SchemaTsError(
|
|
26275
|
-
`line ${key.line}: member() on table "${tableName}" has an unknown option "${key.name}". Allowed: group, membership, member_user, member_group, operations.`
|
|
26294
|
+
`line ${key.line}: member() on table "${tableName}" has an unknown option "${key.name}". Allowed: group, membership, member_user, member_group, operations, read.`
|
|
26276
26295
|
);
|
|
26277
26296
|
}
|
|
26278
26297
|
if (!r.tryPunct(",")) {
|
|
@@ -26302,7 +26321,7 @@ function readMemberScope(r, tableName, scopeLine) {
|
|
|
26302
26321
|
`line ${scopeLine}: member() on table "${tableName}" has a ${group.length}-column group but a ${memberGroup.length}-column member_group. The group key must have the same number of columns on both sides.`
|
|
26303
26322
|
);
|
|
26304
26323
|
}
|
|
26305
|
-
const node = { kind: "member", group, membership, memberUser, memberGroup, ...operations !== void 0 ? { operations } : {} };
|
|
26324
|
+
const node = { kind: "member", group, membership, memberUser, memberGroup, ...operations !== void 0 ? { operations } : {}, ...read ? { read } : {} };
|
|
26306
26325
|
if (operationsSeen) explicitMemberOperations.add(node);
|
|
26307
26326
|
return node;
|
|
26308
26327
|
}
|
|
@@ -26310,21 +26329,56 @@ var explicitMemberOperations = /* @__PURE__ */ new WeakSet();
|
|
|
26310
26329
|
function readPolicyOwner(r, tableName, line) {
|
|
26311
26330
|
r.expectPunct("(", 'after "owner"');
|
|
26312
26331
|
let column = DEFAULT_OWNER_COLUMN;
|
|
26332
|
+
let read;
|
|
26313
26333
|
if (!r.tryPunct(")")) {
|
|
26314
|
-
|
|
26315
|
-
|
|
26316
|
-
|
|
26317
|
-
|
|
26334
|
+
r.expectPunct("{", `in owner() inside anyOf() for table "${tableName}"`);
|
|
26335
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26336
|
+
while (!r.tryPunct("}")) {
|
|
26337
|
+
const key = r.readKey(`for an owner() option inside anyOf() for table "${tableName}"`);
|
|
26338
|
+
if (seen.has(key.name)) throw new SchemaTsError(`line ${key.line}: option "${key.name}" appears twice in owner() inside anyOf() for table "${tableName}".`);
|
|
26339
|
+
seen.add(key.name);
|
|
26340
|
+
r.expectPunct(":", `after "${key.name}" in owner() inside anyOf() for table "${tableName}"`);
|
|
26341
|
+
if (key.name === "column") {
|
|
26342
|
+
const value = r.readLiteral(`for "column" in owner() inside anyOf() for table "${tableName}"`);
|
|
26343
|
+
if (typeof value !== "string" || !SAFE_IDENT.test(value) || value.length > MAX_NAME_LENGTH) {
|
|
26344
|
+
throw new SchemaTsError(`line ${key.line}: owner({ column }) on table "${tableName}" must be a valid column name.`);
|
|
26345
|
+
}
|
|
26346
|
+
column = value.toLowerCase();
|
|
26347
|
+
} else if (key.name === "read") {
|
|
26348
|
+
read = readBranchReadColumns(r, `owner({ read }) on table "${tableName}"`);
|
|
26349
|
+
} else {
|
|
26350
|
+
throw new SchemaTsError(`line ${key.line}: owner() inside anyOf() only allows column and read. Visitor ownership is not part of policy scope.`);
|
|
26318
26351
|
}
|
|
26319
|
-
if (
|
|
26320
|
-
|
|
26352
|
+
if (!r.tryPunct(",")) {
|
|
26353
|
+
r.expectPunct("}", `closing owner() options inside anyOf() for table "${tableName}"`);
|
|
26354
|
+
break;
|
|
26321
26355
|
}
|
|
26322
|
-
column = value.toLowerCase();
|
|
26323
26356
|
}
|
|
26324
26357
|
r.tryPunct(",");
|
|
26325
26358
|
r.expectPunct(")", "closing owner()");
|
|
26326
26359
|
}
|
|
26327
|
-
return { kind: "owner", column };
|
|
26360
|
+
return { kind: "owner", column, ...read ? { read } : {} };
|
|
26361
|
+
}
|
|
26362
|
+
function readBranchReadColumns(r, ctx) {
|
|
26363
|
+
const open = r.next();
|
|
26364
|
+
if (open.kind !== "punct" || open.value !== "[") throw new SchemaTsError(`line ${open.line}: ${ctx} must be a list of quoted column names, such as ['id', 'subject'].`);
|
|
26365
|
+
const out = [];
|
|
26366
|
+
while (!r.tryPunct("]")) {
|
|
26367
|
+
const t = r.next();
|
|
26368
|
+
if (t.kind !== "string" || !SAFE_IDENT.test(t.value) || t.value.length > MAX_NAME_LENGTH) throw new SchemaTsError(`line ${t.line}: ${ctx} must be a list of quoted column names; values must be literal strings.`);
|
|
26369
|
+
const name = t.value.toLowerCase();
|
|
26370
|
+
if (out.includes(name)) throw new SchemaTsError(`line ${t.line}: ${ctx} lists "${t.value}" twice.`);
|
|
26371
|
+
out.push(name);
|
|
26372
|
+
if (!r.tryPunct(",")) {
|
|
26373
|
+
r.expectPunct("]", `closing ${ctx}`);
|
|
26374
|
+
break;
|
|
26375
|
+
}
|
|
26376
|
+
}
|
|
26377
|
+
if (out.length === 0) throw new SchemaTsError(`line ${open.line}: ${ctx} must name at least one column; leave read out for every column.`);
|
|
26378
|
+
return out;
|
|
26379
|
+
}
|
|
26380
|
+
function refuseBranchRead(node, line, tableName, where) {
|
|
26381
|
+
if (node.read) throw new SchemaTsError(`line ${line}: read on ${where} of table "${tableName}" is accepted only on the branches of the table's own anyOf(); it names the columns that branch may read.`);
|
|
26328
26382
|
}
|
|
26329
26383
|
function readPolicyMember(r, tableName, line) {
|
|
26330
26384
|
r.expectPunct("(", 'after "member"');
|
|
@@ -26359,7 +26413,11 @@ function readAnyOfScope(r, tableName, line) {
|
|
|
26359
26413
|
const owner = owners[0];
|
|
26360
26414
|
const other = firstPolicy.kind === "owner" ? secondPolicy : firstPolicy;
|
|
26361
26415
|
if (other.kind === "member" && other.ownerGrant && owner.column !== DEFAULT_OWNER_COLUMN) throw new SchemaTsError(`line ${line}: grant() requires owner() with managed user_id.`);
|
|
26362
|
-
|
|
26416
|
+
const otherRead = other;
|
|
26417
|
+
const reads = [owner.read ?? null, otherRead.read ?? null];
|
|
26418
|
+
delete owner.read;
|
|
26419
|
+
delete otherRead.read;
|
|
26420
|
+
return { kind: "policy", policy: { kind: "any_of", policies: [owner, other], ...reads[0] || reads[1] ? { reads } : {} } };
|
|
26363
26421
|
}
|
|
26364
26422
|
var ALL_OF_FORMS = "allOf() takes a gate \u2014 member(), group() or appRole() \u2014 and then owner() or anyOf(owner(), ...).";
|
|
26365
26423
|
var ALL_OF_EXPLICIT = "allOf needs explicit operations";
|
|
@@ -26385,6 +26443,9 @@ function readAllOfScope(r, tableName, line) {
|
|
|
26385
26443
|
} else throw new SchemaTsError(`line ${whoTok.line}: ${ALL_OF_FORMS}`);
|
|
26386
26444
|
r.tryPunct(",");
|
|
26387
26445
|
r.expectPunct(")", "closing allOf()");
|
|
26446
|
+
refuseBranchRead(gate, line, tableName, "the gate of allOf()");
|
|
26447
|
+
refuseBranchRead(who, line, tableName, "who in allOf()");
|
|
26448
|
+
if (who.kind === "any_of" && who.reads) throw new SchemaTsError(`line ${line}: read on a branch of anyOf() inside allOf() on table "${tableName}" is not accepted; per-branch columns belong on the table's own anyOf().`);
|
|
26388
26449
|
const x = who.kind === "any_of" ? who.policies[1] : null;
|
|
26389
26450
|
if (x?.kind === "member" && x.ownerGrant) throw new SchemaTsError(`line ${line}: grant() belongs in anyOf(owner(), grant()), not allOf().`);
|
|
26390
26451
|
for (const node of [gate, ...x ? [x] : []]) {
|
|
@@ -26476,13 +26537,17 @@ function readScope(r, tableName) {
|
|
|
26476
26537
|
const failAt = (line, message) => {
|
|
26477
26538
|
throw new SchemaTsError(`line ${line}: ${message}`);
|
|
26478
26539
|
};
|
|
26479
|
-
if (tok.value === "group"
|
|
26480
|
-
|
|
26540
|
+
if (tok.value === "group" || tok.value === "appRole") {
|
|
26541
|
+
const policy = tok.value === "group" ? readGroupScope(r, tableName, tok.line, failAt) : readAppRoleScope(r, tableName, tok.line, failAt);
|
|
26542
|
+
refuseBranchRead(policy, tok.line, tableName, `the ${tok.value}() scope`);
|
|
26543
|
+
return { kind: "policy", policy };
|
|
26544
|
+
}
|
|
26481
26545
|
if (tok.value === "member") {
|
|
26482
26546
|
r.expectPunct("(", 'after "member"');
|
|
26483
26547
|
const scope = readMemberScope(r, tableName, tok.line);
|
|
26484
26548
|
r.tryPunct(",");
|
|
26485
26549
|
r.expectPunct(")", "closing member()");
|
|
26550
|
+
if (scope.kind === "member") refuseBranchRead(scope, tok.line, tableName, "the member() scope");
|
|
26486
26551
|
return scope;
|
|
26487
26552
|
}
|
|
26488
26553
|
r.expectPunct("(", `after "${tok.value}"`);
|
|
@@ -26732,6 +26797,22 @@ function readTable(r, rawName, nameLine) {
|
|
|
26732
26797
|
}
|
|
26733
26798
|
uniques.push(entry.columns);
|
|
26734
26799
|
}
|
|
26800
|
+
if (scope.kind === "policy" && scope.policy.kind === "any_of" && scope.policy.reads) {
|
|
26801
|
+
const key = columns.filter((column) => column.helper === "id").length === 1 ? columns.find((column) => column.helper === "id").name : null;
|
|
26802
|
+
const declared = new Set(columns.map((column) => column.name));
|
|
26803
|
+
scope.policy.reads.forEach((list, i) => {
|
|
26804
|
+
if (!list) return;
|
|
26805
|
+
const branch = scope.policy.kind === "any_of" ? scope.policy.policies[i] : null;
|
|
26806
|
+
const label = branch?.kind === "owner" ? "owner()" : branch?.kind === "member" ? branch.ownerGrant ? "grant()" : "member()" : branch?.kind === "group" ? "group()" : "appRole()";
|
|
26807
|
+
for (const column of list) {
|
|
26808
|
+
if (!declared.has(column)) throw new SchemaTsError(`line ${nameLine}: table "${rawName}": ${label} read lists "${column}", which is not one of its declared columns.`);
|
|
26809
|
+
}
|
|
26810
|
+
if (key && !list.includes(key)) throw new SchemaTsError(`line ${nameLine}: table "${rawName}": ${label} read must include the id column "${key}", which every reader needs to page and address rows.`);
|
|
26811
|
+
for (const relation of relations) {
|
|
26812
|
+
if (relation.kind === "belongsTo" && !list.includes(relation.fk)) throw new SchemaTsError(`line ${nameLine}: table "${rawName}": ${label} read must include "${relation.fk}", the key relation "${relation.name}" joins by; a reader without it would get an empty relation.`);
|
|
26813
|
+
}
|
|
26814
|
+
});
|
|
26815
|
+
}
|
|
26735
26816
|
let client;
|
|
26736
26817
|
if (clientInput) {
|
|
26737
26818
|
if (columns.filter((column) => column.helper === "id").length !== 1) {
|