@somewhere-tech/cli 0.37.18 → 0.37.20
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/npm-shrinkwrap.json +2 -2
- package/package.json +3 -3
- package/runtime/DECLARED-DATA-VENDOR.json +11 -10
- package/runtime/declared-data.cjs +300 -44
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@somewhere-tech/cli",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.20",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@somewhere-tech/cli",
|
|
9
|
-
"version": "0.37.
|
|
9
|
+
"version": "0.37.20",
|
|
10
10
|
"bundleDependencies": [
|
|
11
11
|
"@modelcontextprotocol/sdk",
|
|
12
12
|
"chokidar",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@somewhere-tech/cli",
|
|
3
|
-
"version": "0.37.
|
|
4
|
-
"description": "CLI for somewhere.tech
|
|
3
|
+
"version": "0.37.20",
|
|
4
|
+
"description": "CLI for somewhere.tech \u2014 auth, projects, deploy, pull, promote, db, logs, env, MCP bridge.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"somewhere",
|
|
7
7
|
"somewhere.tech",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"build": "tsc",
|
|
38
38
|
"dev": "tsc --watch",
|
|
39
39
|
"prepublishOnly": "npm run build",
|
|
40
|
-
"test": "npm run build && node --test \"test/*.test.mjs\"",
|
|
40
|
+
"test": "npm run build && node --test --test-concurrency=1 \"test/*.test.mjs\"",
|
|
41
41
|
"test:roundtrip": "node scripts/e2e-roundtrip.mjs",
|
|
42
42
|
"prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\""
|
|
43
43
|
},
|
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"esbuild": "0.24.0",
|
|
3
3
|
"source_files": {
|
|
4
|
-
"worker/containers/compile/declared-data-contract.cjs": "
|
|
4
|
+
"worker/containers/compile/declared-data-contract.cjs": "abdf0a0f238f9d7ecccc8aa3ede7cd25778194d0b9c68cc6c44aa776de46d122",
|
|
5
5
|
"worker/containers/compile/groups-types.cjs": "f0f38ca6f160561f997b20af44763bd07232deb2668a536cd52c1974fcfed2c8",
|
|
6
|
-
"worker/containers/compile/runtime-types.cjs": "
|
|
7
|
-
"worker/containers/compile/schema-types.cjs": "
|
|
6
|
+
"worker/containers/compile/runtime-types.cjs": "7d2e647e6a7301b4158f8d45d5c52da9aac3b6fb6f8118be5a5cde62c40462fc",
|
|
7
|
+
"worker/containers/compile/schema-types.cjs": "97941a4f80f50f4a48b828a6a2695f6f0c606ab6e74b4511fbb99e786dc4f0c4",
|
|
8
8
|
"worker/containers/compile/typed-data.cjs": "57b458e16a373e42d48a0e527317fcdf609299ae368eb539a8b582b9a47abb15",
|
|
9
9
|
"worker/containers/compile/typed-files.cjs": "a0b1818afbc7673f84583c76436a3b3ca1d1fdf63d8d8db138b36646103f0708",
|
|
10
10
|
"worker/src/services/jobs/limits.ts": "f806f045dcb92f7da1e830aab65b74185ebd3071aeade368db776f3a0546de38",
|
|
11
|
-
"worker/src/utils/authority/scope-enforcement.ts": "
|
|
11
|
+
"worker/src/utils/authority/scope-enforcement.ts": "b219c1b38b4d5ffa7067b0ab9349efec17c1001195a23c323ec40ae6fe4f7da6",
|
|
12
12
|
"worker/src/utils/db-schema-deploy/bigint-text.ts": "ef3f581daeecb234d2c1e288bc09495a6facd7b0354b300134918f6c0cf10cc2",
|
|
13
13
|
"worker/src/utils/db-schema-deploy/client-contract-source.ts": "25916e9e5174a3959e5b3f61ee1bc4a3ff3c0de01b155e19be97c836431cb8ec",
|
|
14
|
-
"worker/src/utils/db-schema-deploy/client-permissions.ts": "
|
|
15
|
-
"worker/src/utils/db-schema-deploy/extract-schema-app-roles.ts": "
|
|
14
|
+
"worker/src/utils/db-schema-deploy/client-permissions.ts": "9c0aecd4a9698a9813aa8ec3315c1a8c99ba2fd1150d6d820a86ea3ec7b9890b",
|
|
15
|
+
"worker/src/utils/db-schema-deploy/extract-schema-app-roles.ts": "7f0e6ce3e213d56b678a60de133d10ed6f51f3a42ba73071d821d42a48c676a2",
|
|
16
16
|
"worker/src/utils/db-schema-deploy/extract-schema-bindings.ts": "34a2bf98da533afa60cc3fe46c3ff029300f06fef0bfecd98e028bff181ca1b3",
|
|
17
17
|
"worker/src/utils/db-schema-deploy/extract-schema-files.ts": "dddbb2d914a112e2820598d64f9da781aecec55e121f10de3501c3a7b4238f36",
|
|
18
|
-
"worker/src/utils/db-schema-deploy/extract-schema-groups.ts": "
|
|
18
|
+
"worker/src/utils/db-schema-deploy/extract-schema-groups.ts": "f80b93121e1ece04f43fa728517b5baa72fed1d9c833c23cf87ca558f8ca0263",
|
|
19
|
+
"worker/src/utils/db-schema-deploy/extract-schema-links.ts": "91cd99fc90133e51cbbd92656d592714a8fd71e0ac1c869cf161ad4a07225b57",
|
|
19
20
|
"worker/src/utils/db-schema-deploy/extract-schema-options.ts": "0998748a953fa9df742a7bdcbbce01f4fc56f9735ddbe3adf9e01facd82f87ad",
|
|
20
21
|
"worker/src/utils/db-schema-deploy/extract-schema-parents.ts": "eaec77c4db2c9a1903bd07fb265fc4f3fa8941f23724ab568ccdbf9569dc7a78",
|
|
21
22
|
"worker/src/utils/db-schema-deploy/extract-schema-relations.ts": "3984683d3227e964845836079476687a7295e6907b525ef21b9eebc488825bf8",
|
|
22
23
|
"worker/src/utils/db-schema-deploy/extract-schema-roles.ts": "1b2d7c7279679c726053c5d1586221476587273fc24ce93bde72a4a8ef8e7582",
|
|
23
|
-
"worker/src/utils/db-schema-deploy/extract-schema-ts.ts": "
|
|
24
|
+
"worker/src/utils/db-schema-deploy/extract-schema-ts.ts": "af2065c22a1b2c736ca432e0b1855007a99c3239aefe5d0f9bff208d45201cd2",
|
|
24
25
|
"worker/src/utils/db-schema-deploy/schema-constants.ts": "07e648748409207fa4b97367b8942463c047a0bb0e27a36f16f696f90f2b550d",
|
|
25
26
|
"worker/src/utils/db-schema-deploy/schema-ownership.ts": "e6c811a87bc657707be3518dfe547c6d97718ac073108b126eb7f791c9c08202",
|
|
26
|
-
"worker/src/utils/managed-groups/errors.ts": "
|
|
27
|
+
"worker/src/utils/managed-groups/errors.ts": "4830b1ad7357a79760b0bcc68884e562cfbc07d12a37cc73651a6fcb256d682f",
|
|
27
28
|
"worker/src/utils/managed-groups/policy.ts": "2259a883645c8bfd607ead7c37387f3ffd42ac9dfb86722c72b224adb498511e"
|
|
28
29
|
},
|
|
29
|
-
"sha256": "
|
|
30
|
+
"sha256": "849e3664a55c1c5fb67faf54b0866db9b1d10b10807dd8a5631893a775a626d8"
|
|
30
31
|
}
|
|
@@ -68,7 +68,7 @@ var require_declared_data_contract = __commonJS({
|
|
|
68
68
|
}
|
|
69
69
|
if (value.k === "a" && keys === "k,p" && Array.isArray(value.p) && value.p.length === 2) {
|
|
70
70
|
const left = policy(value.p[0], label), right = policy(value.p[1], label);
|
|
71
|
-
if (left.k !== "o" ||
|
|
71
|
+
if (left.k !== "o" || !["m", "g", "r"].includes(right.k)) fail(label);
|
|
72
72
|
return { k: "a", p: [left, right] };
|
|
73
73
|
}
|
|
74
74
|
if (value.k === "g" && ["c,k,roles", "c,generation,k,roles"].includes(keys) && identifier.test(value.c)) {
|
|
@@ -130,7 +130,7 @@ var require_declared_data_contract = __commonJS({
|
|
|
130
130
|
const pk = table.clientPrimaryKey;
|
|
131
131
|
if (!seen.has(pk) || !["integer", "text"].includes(columns.find((column) => column.n === pk).t)) fail(`${name} primary key`);
|
|
132
132
|
const input = table.client;
|
|
133
|
-
if (Object.keys(input).some((key) => !["read", "publicRead", "create", "update", "delete", "identity"].includes(key))) fail(`${name} permissions`);
|
|
133
|
+
if (Object.keys(input).some((key) => !["read", "publicRead", "create", "update", "delete", "identity", "links"].includes(key))) fail(`${name} permissions`);
|
|
134
134
|
const read = typeof input.read === "boolean" ? input.read : fields(input.read, seen, `${name}.read`);
|
|
135
135
|
if (Array.isArray(read) && !read.includes(pk)) fail(`${name}.read must include the primary key`);
|
|
136
136
|
let publicRead = input.publicRead;
|
|
@@ -186,13 +186,31 @@ var require_declared_data_contract = __commonJS({
|
|
|
186
186
|
if (normalizedPolicy?.k === "g" && create !== null && !create.includes(normalizedPolicy.c)) fail(`${name}.policy group create`);
|
|
187
187
|
if (normalizedPolicy?.k === "a") {
|
|
188
188
|
owner = normalizedPolicy.p[0].c;
|
|
189
|
-
|
|
189
|
+
const branch = normalizedPolicy.p[1];
|
|
190
|
+
const fixed = branch.k === "m" ? branch.m.g : branch.k === "g" ? [branch.c] : [];
|
|
191
|
+
if ([...create || [], ...update || []].includes(owner) || (update || []).some((field) => fixed.includes(field))) fail(`${name}.policy identity writes`);
|
|
192
|
+
if (branch.k === "g" && create !== null && !create.includes(branch.c)) fail(`${name}.policy group create`);
|
|
190
193
|
}
|
|
191
194
|
if (normalizedPolicy?.k === "p" && create !== null && !create.includes(normalizedPolicy.v)) fail(`${name}.policy parent create`);
|
|
195
|
+
let links = null;
|
|
196
|
+
if (input.links !== void 0) {
|
|
197
|
+
record(input.links, `${name}.links`);
|
|
198
|
+
if (Object.keys(input.links).sort().join() !== "children,edit,maxDays,read") fail(`${name}.links`);
|
|
199
|
+
if (!(intent === "scoped" && !visitors) && normalizedPolicy?.k !== "a") fail(`${name}.links owner`);
|
|
200
|
+
const readable = read === true ? columns.filter((column) => column.n !== author).map((column) => column.n) : read || [];
|
|
201
|
+
const linkRead = fields(input.links.read, new Set(readable), `${name}.links.read`);
|
|
202
|
+
if (!linkRead.includes(pk)) fail(`${name}.links.read must include the primary key`);
|
|
203
|
+
const linkEdit = fields(input.links.edit, new Set(update || []), `${name}.links.edit`, true);
|
|
204
|
+
const children = input.links.children;
|
|
205
|
+
if (!Array.isArray(children) || children.some((child) => typeof child !== "string" || !identifier.test(child)) || new Set(children).size !== children.length) fail(`${name}.links.children`);
|
|
206
|
+
const maxDays = input.links.maxDays;
|
|
207
|
+
if (maxDays !== null && !(Number.isSafeInteger(maxDays) && maxDays >= 1 && maxDays <= 365)) fail(`${name}.links.maxDays`);
|
|
208
|
+
links = { read: linkRead, edit: linkEdit, children: [...children].sort(), maxDays };
|
|
209
|
+
}
|
|
192
210
|
tables.push({
|
|
193
211
|
name,
|
|
194
212
|
columns,
|
|
195
|
-
client: { identity: input.identity, read, publicRead, create, update, delete: input.delete },
|
|
213
|
+
client: { identity: input.identity, read, publicRead, create, update, delete: input.delete, ...links ? { links } : {} },
|
|
196
214
|
primaryKey: pk,
|
|
197
215
|
intent,
|
|
198
216
|
owner,
|
|
@@ -233,6 +251,13 @@ var require_declared_data_contract = __commonJS({
|
|
|
233
251
|
}
|
|
234
252
|
if (relations.length) table.relations = relations.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
235
253
|
}
|
|
254
|
+
for (const table of tables) {
|
|
255
|
+
for (const name of table.client.links ? table.client.links.children : []) {
|
|
256
|
+
const relation = (table.relations || []).find((item) => item.name === name);
|
|
257
|
+
const child = relation && tableByName.get(relation.table);
|
|
258
|
+
if (!relation || !child || child.policy?.k !== "p" || child.policy.v !== relation.fk || child.policy.t !== table.name) fail(`${table.name}.links.children`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
236
261
|
return JSON.stringify({ version: 1, tables });
|
|
237
262
|
}
|
|
238
263
|
function columnType(column) {
|
|
@@ -247,11 +272,14 @@ var require_declared_data_contract = __commonJS({
|
|
|
247
272
|
});
|
|
248
273
|
return values.length ? `{ ${values.join("; ")} }` : "Record<string, never>";
|
|
249
274
|
}
|
|
275
|
+
var LINK_CLIENT_RUNTIME = 'const b64url=s=>btoa(unescape(encodeURIComponent(s))).replace(/\\+/g,"-").replace(/\\//g,"_").replace(/=+$/,"");const createLink=async(table,id,options={})=>{const result=await invoke(table,"link_create",{id,access:options.access,...(options.expiresInDays===undefined?{}:{expires_in_days:options.expiresInDays})});const route=typeof options.returnTo==="string"?options.returnTo:location.pathname+location.search+location.hash;return{link_id:result.link_id,url:location.origin+"/__sw/link#"+result.secret+"."+b64url(route),expires_at:result.expires_at}};const linkDoor=async(path,init)=>{const response=await fetch(path,{credentials:"same-origin",cache:"no-store",...init});const payload=await response.json().catch(()=>({error:"INVALID_DATA_RESPONSE",message:"Link operation returned an invalid response"}));if(!response.ok)throw new DataError(response.status,payload);return payload};const links=Object.freeze({opened:()=>{try{const list=JSON.parse(sessionStorage.getItem("__sw_links_opened")||"[]");return Array.isArray(list)?list:[]}catch(_){return[]}},sessions:async()=>{const payload=await linkDoor("/__sw/link/sessions",{method:"GET"});return{data:payload.data}},forget:async linkId=>{await linkDoor("/__sw/link/forget",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(linkId===undefined?{}:{link_id:linkId})})}});const linkedView=spec=>link=>{if(typeof link!=="string"||!/^lnk_[0-9a-f]{32}$/.test(link))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"linked() needs a link id"});const view={};for(const[table,canUpdate,children]of spec){const relations={};const root={get:id=>invoke(table,"get",{id,link})};if(canUpdate)root.update=(id,values)=>invoke(table,"update",{id,values,link});for(const[relation,child,canCreate,canUpdateChild]of children){relations[relation]=Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id,link})});const childOps={};if(canCreate)childOps.create=values=>invoke(child,"create",{values,link});if(canUpdateChild)childOps.update=(id,values)=>invoke(child,"update",{id,values,link});if(canCreate||canUpdateChild)view[child]=Object.freeze(childOps)}if(children.length)root.relations=Object.freeze(relations);view[table]=Object.freeze(root)}return Object.freeze(view)};\n';
|
|
250
276
|
function describeDataClient2(authority, contract_digest) {
|
|
251
277
|
const canonical = authority ? canonicalize2(authority.schema, authority.intents, authority.scopes) : canonicalize2({}, {}, {});
|
|
252
278
|
const { tables } = JSON.parse(canonical);
|
|
253
279
|
const declarations = [];
|
|
254
280
|
const runtimeTables = [];
|
|
281
|
+
const linkedDeclarations = [];
|
|
282
|
+
const linkedSpec = [];
|
|
255
283
|
for (const table of tables) {
|
|
256
284
|
const { name, columns, client, primaryKey } = table;
|
|
257
285
|
const readable = client.read === true ? columns.filter((column) => column.n !== table.author).map((column) => column.n) : client.read || [];
|
|
@@ -306,9 +334,44 @@ var require_declared_data_contract = __commonJS({
|
|
|
306
334
|
relationNames.push(relation.name);
|
|
307
335
|
}
|
|
308
336
|
if (relationDeclarations.length) operations.push(`relations: { ${relationDeclarations.join("; ")} }`);
|
|
337
|
+
const links = client.links;
|
|
338
|
+
if (links) {
|
|
339
|
+
operations.push(`createLink(id: ${id}, options: { access: ${links.edit.length ? "'read' | 'edit'" : "'read'"}; expiresInDays?: number; returnTo?: string }): Promise<{ link_id: string; url: string; expires_at: number | null }>`);
|
|
340
|
+
operations.push(`links(id: ${id}): Promise<{ data: Array<{ link_id: string; access: 'read' | 'edit'; created_at: number; expires_at: number | null }> }>`);
|
|
341
|
+
operations.push(`revokeLink(id: ${id}, linkId: string): Promise<{ revoked: boolean }>`);
|
|
342
|
+
methods.push("createLink", "links", "revokeLink");
|
|
343
|
+
const linkRow = shape(columns, links.read, "read");
|
|
344
|
+
const rootOps = [`get(id: ${id}): Promise<{ data: ${linkRow} | null }>`];
|
|
345
|
+
if (links.edit.length) rootOps.push(`update(id: ${id}, values: ${shape(columns, links.edit, "update")}): Promise<{ data: ${linkRow} | null; count: number; changes: number }>`);
|
|
346
|
+
const childRelations = [];
|
|
347
|
+
const childSpecs = [];
|
|
348
|
+
for (const relationName of links.children) {
|
|
349
|
+
const relation = table.relations.find((item) => item.name === relationName);
|
|
350
|
+
const child = tables.find((candidate) => candidate.name === relation.table);
|
|
351
|
+
const childReadable = child.client.read === true ? child.columns.filter((column) => column.n !== child.author).map((column) => column.n) : child.client.read;
|
|
352
|
+
const childRow = shape(child.columns, childReadable, "read");
|
|
353
|
+
const childId = columnType(child.columns.find((column) => column.n === child.primaryKey));
|
|
354
|
+
childRelations.push(`${JSON.stringify(relationName)}: { list(parentId: ${id}, options?: { limit?: number; after?: ${childId} }): Promise<{ data: ${childRow}[]; next: ${childId} | null; has_more: boolean }> }`);
|
|
355
|
+
const childOps = [];
|
|
356
|
+
const canCreate = links.edit.length > 0 && child.client.create !== null;
|
|
357
|
+
const childUpdate = child.client.update ? child.client.update.filter((field) => field !== relation.fk) : [];
|
|
358
|
+
const canUpdate = links.edit.length > 0 && childUpdate.length > 0;
|
|
359
|
+
if (canCreate) childOps.push(`create(values: ${shape(child.columns, child.client.create, "create", /* @__PURE__ */ new Set([relation.fk]))}): Promise<{ data: ${childRow} | null; count: number; changes: number }>`);
|
|
360
|
+
if (canUpdate) childOps.push(`update(id: ${childId}, values: ${shape(child.columns, childUpdate, "update")}): Promise<{ data: ${childRow} | null; count: number; changes: number }>`);
|
|
361
|
+
if (childOps.length) linkedDeclarations.push(`${JSON.stringify(child.name)}: { ${childOps.join("; ")} }`);
|
|
362
|
+
childSpecs.push([relationName, child.name, canCreate, canUpdate]);
|
|
363
|
+
}
|
|
364
|
+
if (childRelations.length) rootOps.push(`relations: { ${childRelations.join("; ")} }`);
|
|
365
|
+
linkedDeclarations.push(`${JSON.stringify(name)}: { ${rootOps.join("; ")} }`);
|
|
366
|
+
linkedSpec.push([name, links.edit.length > 0, childSpecs]);
|
|
367
|
+
}
|
|
309
368
|
declarations.push(`${JSON.stringify(name)}: { ${operations.join("; ")} }`);
|
|
310
369
|
runtimeTables.push([name, methods, relationNames]);
|
|
311
370
|
}
|
|
371
|
+
if (linkedSpec.length) {
|
|
372
|
+
declarations.push(`links: { opened(): Array<{ link_id: string; table: string; row_id: string; access: 'read' | 'edit' }>; sessions(): Promise<{ data: Array<{ link_id: string; table: string; row_id: string; access: 'read' | 'edit'; expires_at: number }> }>; forget(linkId?: string): Promise<void> }`);
|
|
373
|
+
declarations.push(`linked(linkId: string): { ${linkedDeclarations.join("; ")} }`);
|
|
374
|
+
}
|
|
312
375
|
const declaration = `declare module ${JSON.stringify("somewhere:data")} {
|
|
313
376
|
export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
|
|
314
377
|
export class DataError extends Error { readonly status: number; readonly code: string | null; constructor(status: number, payload: unknown); }
|
|
@@ -318,7 +381,7 @@ var require_declared_data_contract = __commonJS({
|
|
|
318
381
|
const runtime = `const contract=${JSON.stringify(contract_digest)};
|
|
319
382
|
class DataError extends Error{constructor(status,payload){super(payload&&typeof payload.message==="string"?payload.message:payload&&typeof payload.error==="string"?payload.error:"Data operation failed");this.name="DataError";this.status=status;this.code=payload&&typeof payload.error==="string"?payload.error:null}}
|
|
320
383
|
const invoke=async(table,operation,input)=>{const response=await fetch("/__sw/data",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json"},body:JSON.stringify({...input,contract,table,operation})});const payload=await response.json().catch(()=>{throw new DataError(response.status,{error:"INVALID_DATA_RESPONSE",message:"Data operation returned an invalid response"})});if(!response.ok)throw new DataError(response.status,payload);return payload};
|
|
321
|
-
const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?values=>invoke(table,operation,{values}):operation==="update"?(id,values)=>invoke(table,operation,{id,values}):id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];})));
|
|
384
|
+
${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?values=>invoke(table,operation,{values}):operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})));
|
|
322
385
|
export{data,DataError};
|
|
323
386
|
`;
|
|
324
387
|
return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration } };
|
|
@@ -993,6 +1056,15 @@ interface SomewhereAuthRequestOptions {
|
|
|
993
1056
|
interface SomewhereAuthRequireOptions extends SomewhereAuthRequestOptions { role?: 'admin' }
|
|
994
1057
|
interface SomewhereAuthAnonSession { id: string; isAnon: true; expiresAt: number }
|
|
995
1058
|
interface SomewhereAuthModerationRevoked { revoked_sessions: number; revoked_refresh_tokens: number }
|
|
1059
|
+
// The app's own users under explicit server authority (deployed production
|
|
1060
|
+
// functions; not row-bound jobs). Check the caller first: this is not a
|
|
1061
|
+
// browser user list. ids: exact lookup (max 200, no limit/cursor); otherwise
|
|
1062
|
+
// pages ordered by (created_at, id), limit 1-200 (default 50). Banned and
|
|
1063
|
+
// agent accounts are not listed; absent ids are omitted.
|
|
1064
|
+
interface SomewhereAuthDirectoryUser { id: string; email: string; display_name: string | null; email_verified: boolean; created_at: number | null }
|
|
1065
|
+
interface SomewhereRuntimeAuthServer {
|
|
1066
|
+
listUsers(options?: { ids: readonly string[]; limit?: never; cursor?: never } | { ids?: never; limit?: number; cursor?: string }): Promise<{ users: SomewhereAuthDirectoryUser[]; next_cursor: string | null }>;
|
|
1067
|
+
}
|
|
996
1068
|
interface SomewhereRuntimeAuthModeration {
|
|
997
1069
|
ban(req: Request, userId: string, options?: { reason?: string | null } | null): Promise<SomewhereAuthModerationRevoked & {
|
|
998
1070
|
user: { id: string; email: string; banned: true; banned_reason: string | null };
|
|
@@ -1067,13 +1139,21 @@ interface SomewhereRuntimeAuth {
|
|
|
1067
1139
|
// Password change and account deletion end every session of the account; these clear the cookies.
|
|
1068
1140
|
updatePasswordWithCookie(req: Request, options: { new_password: string; current_password?: string }): Promise<{ updated: true }>;
|
|
1069
1141
|
deleteUserWithCookie(req: Request): Promise<{ deleted: true }>;
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1142
|
+
// Start Google/GitHub/Discord sign-in from a server handler: stages this attempt's HttpOnly
|
|
1143
|
+
// verifier cookie on the response and resolves the provider start URL (only its challenge travels).
|
|
1144
|
+
oauthStart(provider: 'google' | 'github' | 'discord', options: { redirect_uri: string }): Promise<string>;
|
|
1145
|
+
/** @deprecated Always throws OAUTH_START_REQUIRES_BINDING: sign-in is bound to the browser that starts it. Use: Response.redirect(await sw.auth.oauthStart('google', { redirect_uri }), 302). The parameter stays assignable so apps on older SDK types still typecheck. */
|
|
1146
|
+
googleUrl(options: { redirect_uri: string }): never;
|
|
1147
|
+
/** @deprecated Always throws OAUTH_START_REQUIRES_BINDING: sign-in is bound to the browser that starts it. Use: Response.redirect(await sw.auth.oauthStart('github', { redirect_uri }), 302). The parameter stays assignable so apps on older SDK types still typecheck. */
|
|
1148
|
+
githubUrl(options: { redirect_uri: string }): never;
|
|
1149
|
+
/** @deprecated Always throws OAUTH_START_REQUIRES_BINDING: sign-in is bound to the browser that starts it. Use: Response.redirect(await sw.auth.oauthStart('discord', { redirect_uri }), 302). The parameter stays assignable so apps on older SDK types still typecheck. */
|
|
1150
|
+
discordUrl(options: { redirect_uri: string }): never;
|
|
1151
|
+
// Exchanges take the callback's ?code= and its public ?attempt= (which selects this browser's attempt cookie).
|
|
1152
|
+
googleExchange(options: { code: string; attempt: string; redirect_uri?: string }): Promise<SomewhereAuthGoogleSession>;
|
|
1153
|
+
githubExchange(options: { code: string; attempt: string }): Promise<SomewhereAuthOAuthSession>;
|
|
1154
|
+
discordExchange(options: { code: string; attempt: string }): Promise<SomewhereAuthOAuthSession>;
|
|
1155
|
+
// Reads ?code and ?attempt, exchanges with that attempt's oauthStart verifier from this request,
|
|
1156
|
+
// stages the session cookies, clears that attempt, returns a 302 to redirectTo (default '/').
|
|
1077
1157
|
googleCallbackWithCookie(req: Request, redirectTo?: string): Promise<Response>;
|
|
1078
1158
|
githubCallbackWithCookie(req: Request, redirectTo?: string): Promise<Response>;
|
|
1079
1159
|
discordCallbackWithCookie(req: Request, redirectTo?: string): Promise<Response>;
|
|
@@ -1094,6 +1174,7 @@ interface SomewhereRuntimeAuth {
|
|
|
1094
1174
|
getAcceptedInvite(req: Request, id: string): Promise<SomewhereAuthInvite>;
|
|
1095
1175
|
anonSession(): Promise<SomewhereAuthAnonSession>;
|
|
1096
1176
|
readonly moderation: SomewhereRuntimeAuthModeration;
|
|
1177
|
+
readonly server: SomewhereRuntimeAuthServer;
|
|
1097
1178
|
}
|
|
1098
1179
|
interface SomewhereRuntimeCrypto {
|
|
1099
1180
|
// Lowercase hex HMAC-SHA256 of message keyed by secret.
|
|
@@ -2266,8 +2347,45 @@ interface SomewhereAiModerationResult {
|
|
|
2266
2347
|
model: string;
|
|
2267
2348
|
provider: string;
|
|
2268
2349
|
}
|
|
2350
|
+
// sw.ai.evaluate: typed decisions. Probabilities are model output, not
|
|
2351
|
+
// calibrated guarantees; choose thresholds from labelled cases.
|
|
2352
|
+
type SomewhereAiEvaluateQuestion =
|
|
2353
|
+
| { type: 'noul'; instructions: string | SomewhereJsonObject | readonly unknown[]; criteria?: { true?: unknown; false?: unknown } }
|
|
2354
|
+
| { type: 'choice'; instructions: string | SomewhereJsonObject | readonly unknown[]; criteria: Record<string, unknown> }
|
|
2355
|
+
| { type: 'score'; instructions: string | SomewhereJsonObject | readonly unknown[]; criteria: readonly unknown[] };
|
|
2356
|
+
type SomewhereAiEvaluateImage =
|
|
2357
|
+
| string
|
|
2358
|
+
| { content_type: 'image/png' | 'image/jpeg' | 'image/webp'; base64: string };
|
|
2359
|
+
type SomewhereAiEvaluateOptions<Q extends Record<string, SomewhereAiEvaluateQuestion>> = {
|
|
2360
|
+
state: string | SomewhereJsonObject | readonly unknown[];
|
|
2361
|
+
questions: Q;
|
|
2362
|
+
} & (
|
|
2363
|
+
// Text routes to jev and images to clef-flash when model is omitted; jev reads text only.
|
|
2364
|
+
| { model: 'jev'; images?: never }
|
|
2365
|
+
| { model?: 'clef-flash'; images?: readonly SomewhereAiEvaluateImage[] }
|
|
2366
|
+
);
|
|
2367
|
+
interface SomewhereAiEvaluateNoulAnswer { type: 'noul'; noul: number }
|
|
2368
|
+
interface SomewhereAiEvaluateChoiceAnswer<O extends string = string> {
|
|
2369
|
+
type: 'choice'; choice: O; probabilities: Record<O, number>; confidence: number;
|
|
2370
|
+
}
|
|
2371
|
+
interface SomewhereAiEvaluateScoreAnswer {
|
|
2372
|
+
type: 'score'; score: number; legend: Record<string, unknown>; probabilities: Record<string, number>; confidence: number;
|
|
2373
|
+
}
|
|
2374
|
+
type SomewhereAiEvaluateAnswer<Q> =
|
|
2375
|
+
Q extends { type: 'noul' } ? SomewhereAiEvaluateNoulAnswer
|
|
2376
|
+
: Q extends { type: 'choice'; criteria: infer C } ? SomewhereAiEvaluateChoiceAnswer<Extract<keyof C, string>>
|
|
2377
|
+
: Q extends { type: 'score' } ? SomewhereAiEvaluateScoreAnswer
|
|
2378
|
+
: never;
|
|
2379
|
+
interface SomewhereAiEvaluateResult<Q extends Record<string, SomewhereAiEvaluateQuestion>> {
|
|
2380
|
+
model: 'clef-flash' | 'jev';
|
|
2381
|
+
provider_model: string;
|
|
2382
|
+
routed_by: 'model' | 'images' | 'text';
|
|
2383
|
+
answers: { [K in keyof Q]: SomewhereAiEvaluateAnswer<Q[K]> };
|
|
2384
|
+
usage: { input_tokens: number };
|
|
2385
|
+
cost: SomewhereAiCost;
|
|
2386
|
+
}
|
|
2269
2387
|
interface SomewhereAiCatalogEntry {
|
|
2270
|
-
feature: 'transcribe' | 'tts' | 'generate_image' | 'remove_background' | 'embed' | 'complete';
|
|
2388
|
+
feature: 'transcribe' | 'tts' | 'generate_image' | 'remove_background' | 'embed' | 'complete' | 'evaluate';
|
|
2271
2389
|
model: string;
|
|
2272
2390
|
provider: string;
|
|
2273
2391
|
label: string;
|
|
@@ -2305,6 +2423,7 @@ interface SomewhereRuntimeAi {
|
|
|
2305
2423
|
removeBackground(options: SomewhereAiRemoveBackgroundOptions): Promise<never>;
|
|
2306
2424
|
embeddings(options: SomewhereAiEmbeddingsOptions): Promise<SomewhereAiEmbeddingsResult>;
|
|
2307
2425
|
moderate(text: string): Promise<SomewhereAiModerationResult>;
|
|
2426
|
+
evaluate<const Q extends Record<string, SomewhereAiEvaluateQuestion>>(options: SomewhereAiEvaluateOptions<Q>): Promise<SomewhereAiEvaluateResult<Q>>;
|
|
2308
2427
|
catalog(): Promise<SomewhereAiCatalog>;
|
|
2309
2428
|
}
|
|
2310
2429
|
|
|
@@ -3436,6 +3555,8 @@ var require_schema_types = __commonJS({
|
|
|
3436
3555
|
interface Scope { readonly __somewhereScope: unique symbol }
|
|
3437
3556
|
interface OwnerScope extends Scope { readonly __somewhereOwner: unique symbol }
|
|
3438
3557
|
interface MemberScope extends Scope { readonly __somewhereMember: unique symbol }
|
|
3558
|
+
interface GroupScope extends Scope { readonly __somewhereGroup: unique symbol }
|
|
3559
|
+
interface AppRoleScope extends Scope { readonly __somewhereAppRole: unique symbol }
|
|
3439
3560
|
interface ColumnOptions<Default> {
|
|
3440
3561
|
nullable?: boolean; default?: Default | null; unique?: boolean;
|
|
3441
3562
|
references?: string; onDelete?: 'cascade' | 'restrict'; renamedFrom?: string;
|
|
@@ -3443,6 +3564,10 @@ var require_schema_types = __commonJS({
|
|
|
3443
3564
|
interface ClientPermissions<Field extends string> {
|
|
3444
3565
|
identity?: 'authenticated' | 'visitor'; read?: boolean | readonly Field[]; publicRead?: boolean | { where: Partial<Record<Field, string | number | boolean | null>> };
|
|
3445
3566
|
create?: readonly Field[] | false | null; update?: readonly Field[] | false | null; delete?: boolean;
|
|
3567
|
+
links?: ClientLinkPermissions<Field>;
|
|
3568
|
+
}
|
|
3569
|
+
interface ClientLinkPermissions<Field extends string> {
|
|
3570
|
+
read: true | readonly Field[]; edit?: readonly Field[] | false; children?: readonly string[]; maxDays?: number;
|
|
3446
3571
|
}
|
|
3447
3572
|
interface FileCollection { readonly __somewhereFileCollection: unique symbol }
|
|
3448
3573
|
type FileOperation = 'read' | 'upload' | 'replace' | 'delete';
|
|
@@ -3486,11 +3611,11 @@ function removed(): SomewhereSchemaDeclaration.ColumnMarker;
|
|
|
3486
3611
|
function removedTable(): SomewhereSchemaDeclaration.TableMarker;
|
|
3487
3612
|
function exported(): SomewhereSchemaDeclaration.TableMarker;
|
|
3488
3613
|
`;
|
|
3489
|
-
var policySignatures = `function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope,
|
|
3490
|
-
function anyOf(
|
|
3614
|
+
var policySignatures = `function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, other: SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope): SomewhereSchemaDeclaration.Scope;
|
|
3615
|
+
function anyOf(other: SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.Scope;
|
|
3491
3616
|
function parent(options: { via: string; table?: string }): SomewhereSchemaDeclaration.Scope;
|
|
3492
|
-
function group(options?: { column?: string; roles?: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.
|
|
3493
|
-
function appRole(options: { roles: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.
|
|
3617
|
+
function group(options?: { column?: string; roles?: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.GroupScope;
|
|
3618
|
+
function appRole(options: { roles: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.AppRoleScope;
|
|
3494
3619
|
`;
|
|
3495
3620
|
var SCHEMA_DECLARATION2 = types + signatures.replace(/^function /gm, "declare function ") + `declare module 'somewhere/db' {
|
|
3496
3621
|
${(signatures + policySignatures).replace(/^function /gm, " export function ")}}
|
|
@@ -3533,7 +3658,7 @@ function parseClientPermissions(value, columns, scope) {
|
|
|
3533
3658
|
}
|
|
3534
3659
|
const input = value;
|
|
3535
3660
|
for (const key of Object.keys(input)) {
|
|
3536
|
-
if (!["identity", "read", "publicRead", "create", "update", "delete"].includes(key)) {
|
|
3661
|
+
if (!["identity", "read", "publicRead", "create", "update", "delete", "links"].includes(key)) {
|
|
3537
3662
|
return fail(`Unknown client permission "${key}".`);
|
|
3538
3663
|
}
|
|
3539
3664
|
}
|
|
@@ -3639,7 +3764,77 @@ function parseClientPermissions(value, columns, scope) {
|
|
|
3639
3764
|
}
|
|
3640
3765
|
}
|
|
3641
3766
|
}
|
|
3642
|
-
|
|
3767
|
+
let links;
|
|
3768
|
+
if (input.links !== void 0) {
|
|
3769
|
+
const parsed = parseLinkPermissions(input.links, read, writes.update, columns, scope);
|
|
3770
|
+
if (typeof parsed === "string") return fail(parsed);
|
|
3771
|
+
links = parsed;
|
|
3772
|
+
}
|
|
3773
|
+
return { ok: true, permissions: { identity, read, publicRead, ...writes, delete: remove, ...links ? { links } : {} } };
|
|
3774
|
+
}
|
|
3775
|
+
function authoredClientFromCanonical(client) {
|
|
3776
|
+
if (!client || typeof client !== "object" || Array.isArray(client)) return client;
|
|
3777
|
+
const links = client.links;
|
|
3778
|
+
if (!links || typeof links !== "object" || Array.isArray(links)) return client;
|
|
3779
|
+
const l = links;
|
|
3780
|
+
const keys = Object.keys(l).sort().join(",");
|
|
3781
|
+
if (keys !== "children,edit,maxDays,read" || !Array.isArray(l.read) || !Array.isArray(l.edit) || !Array.isArray(l.children) || !(l.maxDays === null || typeof l.maxDays === "number")) return client;
|
|
3782
|
+
const authored = { read: l.read, edit: l.edit.length === 0 ? false : l.edit, children: l.children };
|
|
3783
|
+
if (l.maxDays !== null) authored.maxDays = l.maxDays;
|
|
3784
|
+
return { ...client, links: authored };
|
|
3785
|
+
}
|
|
3786
|
+
function parseLinkPermissions(value, read, update, columns, scope) {
|
|
3787
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "client.links must be an object: { read, edit?, children?, maxDays? }.";
|
|
3788
|
+
const input = value;
|
|
3789
|
+
for (const key2 of Object.keys(input)) {
|
|
3790
|
+
if (!["read", "edit", "children", "maxDays"].includes(key2)) return `Unknown client.links option "${key2}". Allowed: read, edit, children, maxDays.`;
|
|
3791
|
+
}
|
|
3792
|
+
const owned = scope.kind === "owner" && scope.visitors !== true || scope.kind === "policy" && typeof scope.column === "string";
|
|
3793
|
+
if (!owned) return "client.links needs rows with one signed-in owner: owner() (without visitors) or anyOf(owner(), ...).";
|
|
3794
|
+
if (read === false) return "client.links needs client.read: a link can only read fields the app data client reads.";
|
|
3795
|
+
const readable = read === true ? columns.filter((column) => column.helper !== "blob").map((column) => column.name) : read;
|
|
3796
|
+
const list = (raw, label, allowed, allowedLabel) => {
|
|
3797
|
+
if (!Array.isArray(raw)) return `client.links.${label} must list field names.`;
|
|
3798
|
+
const names = [];
|
|
3799
|
+
for (const entry of raw) {
|
|
3800
|
+
if (typeof entry !== "string") return `client.links.${label} fields must be column names.`;
|
|
3801
|
+
const name = entry.toLowerCase();
|
|
3802
|
+
if (!allowed.includes(name)) return `client.links.${label} names "${entry}", which is not in ${allowedLabel}.`;
|
|
3803
|
+
if (names.includes(name)) return `client.links.${label} declares field "${entry}" twice.`;
|
|
3804
|
+
names.push(name);
|
|
3805
|
+
}
|
|
3806
|
+
return names.sort();
|
|
3807
|
+
};
|
|
3808
|
+
const linkRead = input.read === true ? [...readable].sort() : list(input.read, "read", readable, "client.read");
|
|
3809
|
+
if (typeof linkRead === "string") return linkRead;
|
|
3810
|
+
if (linkRead.length === 0) return "client.links.read must name at least one field, or be true.";
|
|
3811
|
+
const key = columns.find((column) => column.helper === "id")?.name;
|
|
3812
|
+
if (!key || !linkRead.includes(key)) return "client.links.read must include the primary key.";
|
|
3813
|
+
let edit = [];
|
|
3814
|
+
if (input.edit !== void 0 && input.edit !== false) {
|
|
3815
|
+
if (!update) return "client.links.edit needs client.update: a link can only update fields the app data client updates.";
|
|
3816
|
+
const parsed = list(input.edit, "edit", update, "client.update");
|
|
3817
|
+
if (typeof parsed === "string") return parsed;
|
|
3818
|
+
if (parsed.length === 0) return "client.links.edit must name at least one field, or be false.";
|
|
3819
|
+
if (scope.column && parsed.includes(scope.column)) return `client.links.edit cannot write the owner field "${scope.column}".`;
|
|
3820
|
+
edit = parsed;
|
|
3821
|
+
}
|
|
3822
|
+
let children = [];
|
|
3823
|
+
if (input.children !== void 0) {
|
|
3824
|
+
if (!Array.isArray(input.children) || input.children.some((entry) => typeof entry !== "string" || !/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(entry))) {
|
|
3825
|
+
return "client.links.children must list relation names declared on this table.";
|
|
3826
|
+
}
|
|
3827
|
+
children = [...new Set(input.children.map((entry) => entry.toLowerCase()))].sort();
|
|
3828
|
+
if (children.length !== input.children.length) return "client.links.children names a relation twice.";
|
|
3829
|
+
}
|
|
3830
|
+
let maxDays = null;
|
|
3831
|
+
if (input.maxDays !== void 0) {
|
|
3832
|
+
if (typeof input.maxDays !== "number" || !Number.isSafeInteger(input.maxDays) || input.maxDays < 1 || input.maxDays > 365) {
|
|
3833
|
+
return "client.links.maxDays must be a whole number of days from 1 to 365.";
|
|
3834
|
+
}
|
|
3835
|
+
maxDays = input.maxDays;
|
|
3836
|
+
}
|
|
3837
|
+
return { read: linkRead, edit, children, maxDays };
|
|
3643
3838
|
}
|
|
3644
3839
|
|
|
3645
3840
|
// worker/src/services/jobs/limits.ts
|
|
@@ -3928,7 +4123,7 @@ function bakedTableSchemaFromDeclared(declaredJson) {
|
|
|
3928
4123
|
const sc = scope;
|
|
3929
4124
|
if (typeof sc.kind !== "string") return null;
|
|
3930
4125
|
const policyIdentity = sc.kind === "policy" ? declaredPolicyIdentity(sc.policy) : {};
|
|
3931
|
-
const parsed = parseClientPermissions(shape.client, permissionColumns, {
|
|
4126
|
+
const parsed = parseClientPermissions(authoredClientFromCanonical(shape.client), permissionColumns, {
|
|
3932
4127
|
kind: sc.kind,
|
|
3933
4128
|
...visitors ? { visitors: true } : {},
|
|
3934
4129
|
...typeof sc.column === "string" ? { column: sc.column } : {},
|
|
@@ -3959,7 +4154,11 @@ function declaredPolicyIdentity(value) {
|
|
|
3959
4154
|
if (node.kind === "owner" && typeof node.column === "string") return { column: node.column };
|
|
3960
4155
|
if (node.kind === "member" && Array.isArray(node.group) && node.group.every((entry) => typeof entry === "string")) return { group: node.group };
|
|
3961
4156
|
if (node.kind === "any_of" && Array.isArray(node.policies)) {
|
|
3962
|
-
|
|
4157
|
+
const branchGroup = (child) => {
|
|
4158
|
+
const c = child;
|
|
4159
|
+
return c && c.kind === "group" && typeof c.column === "string" ? { group: [c.column] } : {};
|
|
4160
|
+
};
|
|
4161
|
+
return node.policies.reduce((all, child) => ({ ...all, ...declaredPolicyIdentity(child), ...branchGroup(child) }), {});
|
|
3963
4162
|
}
|
|
3964
4163
|
return {};
|
|
3965
4164
|
}
|
|
@@ -3974,9 +4173,10 @@ function bakedPolicyFromDeclared(value, depth = 0, edges = 0, underParent = fals
|
|
|
3974
4173
|
return member ? { k: "m", m: member } : null;
|
|
3975
4174
|
}
|
|
3976
4175
|
if (node.kind === "any_of" && Array.isArray(node.policies) && node.policies.length === 2) {
|
|
3977
|
-
|
|
4176
|
+
const second = node.policies[1]?.kind;
|
|
4177
|
+
if (depth !== 0 && !underParent || node.policies[0]?.kind !== "owner" || second !== "member" && second !== "group" && second !== "app_role") return null;
|
|
3978
4178
|
const left = bakedPolicyFromDeclared(node.policies[0], depth + 1);
|
|
3979
|
-
const right = bakedPolicyFromDeclared(node.policies[1], depth + 1);
|
|
4179
|
+
const right = bakedPolicyFromDeclared(node.policies[1], depth + 1, edges, second !== "member");
|
|
3980
4180
|
return left && right ? { k: "a", p: [left, right] } : null;
|
|
3981
4181
|
}
|
|
3982
4182
|
if (node.kind === "group") {
|
|
@@ -4414,6 +4614,43 @@ function validateDeclaredRelations(tables, tableByName, knownColumns, markedGone
|
|
|
4414
4614
|
}
|
|
4415
4615
|
}
|
|
4416
4616
|
|
|
4617
|
+
// worker/src/utils/db-schema-deploy/extract-schema-links.ts
|
|
4618
|
+
function validateRowLinks(tables, tableByName, errors) {
|
|
4619
|
+
const childOf = /* @__PURE__ */ new Map();
|
|
4620
|
+
for (const t of tables) {
|
|
4621
|
+
const links = t.client?.links;
|
|
4622
|
+
if (!links) continue;
|
|
4623
|
+
for (const name of links.children) {
|
|
4624
|
+
const relation = t.relations.find((r) => r.name === name);
|
|
4625
|
+
const child = relation ? tableByName.get(relation.table) : void 0;
|
|
4626
|
+
if (!relation || relation.kind !== "hasMany" || !child) {
|
|
4627
|
+
errors.push(`Table "${t.name}" lists "${name}" in client.links.children, which is not a hasMany relation declared on "${t.name}".`);
|
|
4628
|
+
continue;
|
|
4629
|
+
}
|
|
4630
|
+
const scope = child.scope;
|
|
4631
|
+
if (scope.kind !== "policy" || scope.policy.kind !== "parent" || scope.policy.table !== t.name || scope.policy.via !== relation.fk) {
|
|
4632
|
+
errors.push(`Table "${t.name}" lists "${name}" in client.links.children, but "${child.name}" is not scoped parent({ via: '${relation.fk}' }) on "${t.name}". A link reaches only rows that belong to the linked row.`);
|
|
4633
|
+
continue;
|
|
4634
|
+
}
|
|
4635
|
+
if (!child.client || child.client.read === false) {
|
|
4636
|
+
errors.push(`Table "${t.name}" lists "${name}" in client.links.children, but "${child.name}" declares no client.read.`);
|
|
4637
|
+
continue;
|
|
4638
|
+
}
|
|
4639
|
+
const earlier = childOf.get(child.name);
|
|
4640
|
+
if (earlier && earlier !== t.name) {
|
|
4641
|
+
errors.push(`Table "${child.name}" is a link child of both "${earlier}" and "${t.name}". Declare it as a link child of one table.`);
|
|
4642
|
+
continue;
|
|
4643
|
+
}
|
|
4644
|
+
childOf.set(child.name, t.name);
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
for (const [child, parent] of childOf) {
|
|
4648
|
+
if (tableByName.get(child)?.client?.links) {
|
|
4649
|
+
errors.push(`Table "${child}" declares client.links and is also a link child of "${parent}". Links reach one level of children.`);
|
|
4650
|
+
}
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4417
4654
|
// worker/src/utils/db-schema-deploy/extract-schema-files.ts
|
|
4418
4655
|
var FILE_OPERATIONS = ["read", "upload", "replace", "delete"];
|
|
4419
4656
|
var MAX_FILE_COLLECTIONS = 20;
|
|
@@ -4869,10 +5106,13 @@ var sorted = (list) => [...list].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
|
4869
5106
|
function resolveGroupScopes(tables, vocabulary, errors) {
|
|
4870
5107
|
const allRoles = [...BUILT_IN_GROUP_ROLES2, ...vocabulary?.roles ?? []];
|
|
4871
5108
|
for (const t of tables) {
|
|
4872
|
-
if (t.scope.kind !== "policy"
|
|
4873
|
-
const
|
|
5109
|
+
if (t.scope.kind !== "policy") continue;
|
|
5110
|
+
const branch = t.scope.policy.kind === "any_of" ? t.scope.policy.policies.find((p) => p.kind === "group") : void 0;
|
|
5111
|
+
const node = t.scope.policy.kind === "group" ? t.scope.policy : branch?.kind === "group" ? branch : null;
|
|
5112
|
+
if (!node) continue;
|
|
4874
5113
|
const declared = declaredGroupRoles.get(node);
|
|
4875
5114
|
if (!declared) continue;
|
|
5115
|
+
const omitted = branch ? [] : allRoles;
|
|
4876
5116
|
const where = `line ${declared.line}: `;
|
|
4877
5117
|
const column = t.columns.find((c) => c.name === node.column);
|
|
4878
5118
|
if (!column) {
|
|
@@ -4881,7 +5121,7 @@ function resolveGroupScopes(tables, vocabulary, errors) {
|
|
|
4881
5121
|
errors.push(`${where}Table "${t.name}" uses group({ column: '${node.column}' }), which must be a non-nullable text() column: every row belongs to exactly one group.`);
|
|
4882
5122
|
}
|
|
4883
5123
|
for (const op of GROUP_OPERATIONS) {
|
|
4884
|
-
const list = declared.explicit[op] ??
|
|
5124
|
+
const list = declared.explicit[op] ?? omitted;
|
|
4885
5125
|
for (const role of list) {
|
|
4886
5126
|
if (!allRoles.includes(role)) {
|
|
4887
5127
|
errors.push(`${where}group({ roles: { ${op} } }) on table "${t.name}" names "${role}", which is not a group role. Use owner, admin, member, or a role declared in groups.roles.`);
|
|
@@ -4895,10 +5135,12 @@ function resolveGroupScopes(tables, vocabulary, errors) {
|
|
|
4895
5135
|
errors.push(`${where}group() on table "${t.name}" lets ${missing.join(", ")} ${op} rows they cannot read. A role that may ${op} must also read, because the write returns the row.`);
|
|
4896
5136
|
}
|
|
4897
5137
|
}
|
|
5138
|
+
if (branch && GROUP_OPERATIONS.every((op) => node.roles[op].length === 0)) {
|
|
5139
|
+
errors.push(`${where}group() inside anyOf() on table "${t.name}" grants no operation. List the group roles that may read, update or delete every row of the group.`);
|
|
5140
|
+
}
|
|
4898
5141
|
if (t.client?.create && !t.client.create.includes(node.column)) {
|
|
4899
5142
|
errors.push(`${where}Table "${t.name}" uses group() and client.create must include "${node.column}" so every new row names the group it belongs to.`);
|
|
4900
5143
|
}
|
|
4901
|
-
declaredGroupRoles.delete(node);
|
|
4902
5144
|
}
|
|
4903
5145
|
}
|
|
4904
5146
|
function groupLeafOf(t) {
|
|
@@ -5001,8 +5243,10 @@ function readAppRoleScope(r, tableName, line, fail) {
|
|
|
5001
5243
|
function resolveAppRoleScopes(tables, appRoles, errors) {
|
|
5002
5244
|
const vocabulary = new Set((appRoles ?? []).map((role) => role.name));
|
|
5003
5245
|
for (const t of tables) {
|
|
5004
|
-
if (t.scope.kind !== "policy"
|
|
5005
|
-
const
|
|
5246
|
+
if (t.scope.kind !== "policy") continue;
|
|
5247
|
+
const root = t.scope.policy;
|
|
5248
|
+
const node = root.kind === "app_role" ? root : root.kind === "any_of" ? root.policies.find((p) => p.kind === "app_role") : void 0;
|
|
5249
|
+
if (!node) continue;
|
|
5006
5250
|
const declared = declaredLines.get(node);
|
|
5007
5251
|
if (!declared) continue;
|
|
5008
5252
|
for (const { op, name, line } of declared.names) {
|
|
@@ -5016,6 +5260,9 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
|
|
|
5016
5260
|
errors.push(`line ${declared.line}: appRole() on table "${t.name}" lets ${missing.join(", ")} ${op} rows they cannot read. A role that may ${op} must also read, because the write returns the row.`);
|
|
5017
5261
|
}
|
|
5018
5262
|
}
|
|
5263
|
+
if (root.kind === "any_of" && APP_ROLE_OPERATIONS.every((op) => node.roles[op].length === 0)) {
|
|
5264
|
+
errors.push(`line ${declared.line}: appRole() inside anyOf() on table "${t.name}" grants no operation. List the app roles that may read, update or delete every row.`);
|
|
5265
|
+
}
|
|
5019
5266
|
declaredLines.delete(node);
|
|
5020
5267
|
}
|
|
5021
5268
|
}
|
|
@@ -5412,7 +5659,7 @@ function readClientPermissions(r) {
|
|
|
5412
5659
|
}
|
|
5413
5660
|
out[key.name] = fields;
|
|
5414
5661
|
} else {
|
|
5415
|
-
out[key.name] = r.readLiteral(`for client.${key.name}`);
|
|
5662
|
+
out[key.name] = key.name === "links" && r.peek().kind === "punct" && r.peek().value === "{" ? readClientPermissions(r) : r.readLiteral(`for client.${key.name}`);
|
|
5416
5663
|
}
|
|
5417
5664
|
if (!r.tryPunct(",")) {
|
|
5418
5665
|
r.expectPunct("}", "closing client permissions");
|
|
@@ -5713,23 +5960,30 @@ function readPolicyMember(r, tableName, line) {
|
|
|
5713
5960
|
if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
|
|
5714
5961
|
return scope;
|
|
5715
5962
|
}
|
|
5963
|
+
var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), group() or appRole().";
|
|
5964
|
+
function readAnyOfBranch(r, tableName, which) {
|
|
5965
|
+
const tok = r.expectIdent(`for the ${which} policy in anyOf() on table "${tableName}"`);
|
|
5966
|
+
const failAt = (at, message) => {
|
|
5967
|
+
throw new SchemaTsError(`line ${at}: ${message}`);
|
|
5968
|
+
};
|
|
5969
|
+
if (tok.value === "owner") return readPolicyOwner(r, tableName, tok.line);
|
|
5970
|
+
if (tok.value === "member") return readPolicyMember(r, tableName, tok.line);
|
|
5971
|
+
if (tok.value === "group") return readGroupScope(r, tableName, tok.line, failAt);
|
|
5972
|
+
if (tok.value === "appRole") return readAppRoleScope(r, tableName, tok.line, failAt);
|
|
5973
|
+
throw new SchemaTsError(`line ${tok.line}: ${ANY_OF_FORMS}`);
|
|
5974
|
+
}
|
|
5716
5975
|
function readAnyOfScope(r, tableName, line) {
|
|
5717
5976
|
r.expectPunct("(", 'after "anyOf"');
|
|
5718
|
-
const
|
|
5719
|
-
const firstPolicy = first.value === "owner" ? readPolicyOwner(r, tableName, first.line) : first.value === "member" ? readPolicyMember(r, tableName, first.line) : null;
|
|
5720
|
-
if (!firstPolicy) throw new SchemaTsError(`line ${first.line}: anyOf() only accepts owner() and member().`);
|
|
5977
|
+
const firstPolicy = readAnyOfBranch(r, tableName, "first");
|
|
5721
5978
|
r.expectPunct(",", `between the two policies in anyOf() on table "${tableName}"`);
|
|
5722
|
-
const
|
|
5723
|
-
const secondPolicy = second.value === "owner" ? readPolicyOwner(r, tableName, second.line) : second.value === "member" ? readPolicyMember(r, tableName, second.line) : null;
|
|
5724
|
-
if (!secondPolicy) throw new SchemaTsError(`line ${second.line}: anyOf() only accepts owner() and member().`);
|
|
5979
|
+
const secondPolicy = readAnyOfBranch(r, tableName, "second");
|
|
5725
5980
|
r.tryPunct(",");
|
|
5726
5981
|
r.expectPunct(")", "closing anyOf()");
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
const
|
|
5731
|
-
|
|
5732
|
-
return { kind: "policy", policy: { kind: "any_of", policies: [owner, member] } };
|
|
5982
|
+
const owners = [firstPolicy, secondPolicy].filter((p) => p.kind === "owner");
|
|
5983
|
+
if (owners.length !== 1) throw new SchemaTsError(`line ${line}: ${ANY_OF_FORMS}`);
|
|
5984
|
+
const owner = owners[0];
|
|
5985
|
+
const other = firstPolicy.kind === "owner" ? secondPolicy : firstPolicy;
|
|
5986
|
+
return { kind: "policy", policy: { kind: "any_of", policies: [owner, other] } };
|
|
5733
5987
|
}
|
|
5734
5988
|
function readParentScope(r, tableName, line) {
|
|
5735
5989
|
r.expectPunct("(", 'after "parent"');
|
|
@@ -6061,11 +6315,12 @@ function readTable(r, rawName, nameLine) {
|
|
|
6061
6315
|
if (columns.filter((column) => column.helper === "id").length !== 1) {
|
|
6062
6316
|
throw new SchemaTsError(`line ${nameLine}: table "${rawName}": client operations require exactly one id() column.`);
|
|
6063
6317
|
}
|
|
6064
|
-
const
|
|
6318
|
+
const branch = scope.kind === "policy" && scope.policy.kind === "any_of" ? scope.policy.policies.find((node) => node.kind === "member" || node.kind === "group") : void 0;
|
|
6319
|
+
const branchGroup = branch?.kind === "member" ? branch.group : branch?.kind === "group" ? [branch.column] : void 0;
|
|
6065
6320
|
const parsed = parseClientPermissions(clientInput, columns, {
|
|
6066
6321
|
...scope,
|
|
6067
6322
|
...managedOwner ? { column: managedOwner.column } : {},
|
|
6068
|
-
...
|
|
6323
|
+
...branchGroup ? { group: branchGroup } : {}
|
|
6069
6324
|
});
|
|
6070
6325
|
if (!parsed.ok) throw new SchemaTsError(`line ${nameLine}: table "${rawName}": ${parsed.message}`);
|
|
6071
6326
|
const key = columns.find((column) => column.helper === "id").name;
|
|
@@ -6215,6 +6470,7 @@ function extractSchemaTs(source) {
|
|
|
6215
6470
|
}
|
|
6216
6471
|
}
|
|
6217
6472
|
validateDeclaredRelations(tables, tableByName, knownColumns, (table) => removedTables.includes(table) ? "removedTable()" : exportedTables.includes(table) ? "exported()" : null, errors);
|
|
6473
|
+
validateRowLinks(tables, tableByName, errors);
|
|
6218
6474
|
if (fileCollections) {
|
|
6219
6475
|
errors.push(...fileErrors);
|
|
6220
6476
|
validateFileCollections(fileCollections, (name) => {
|