@somewhere-tech/cli 0.32.5 → 0.33.1

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@somewhere-tech/cli",
3
- "version": "0.32.5",
3
+ "version": "0.33.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@somewhere-tech/cli",
9
- "version": "0.32.5",
9
+ "version": "0.33.1",
10
10
  "bundleDependencies": [
11
11
  "@modelcontextprotocol/sdk",
12
12
  "chokidar",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@somewhere-tech/cli",
3
- "version": "0.32.5",
3
+ "version": "0.33.1",
4
4
  "description": "CLI for somewhere.tech — auth, projects, deploy, pull, promote, db, logs, env, MCP bridge.",
5
5
  "keywords": [
6
6
  "somewhere",
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "esbuild": "0.24.0",
3
3
  "source_files": {
4
- "worker/containers/compile/declared-data-contract.cjs": "b4e8ea6b90ff61198211e78acc4d8b97de26a53d257a971cba7ecca19a0c833f",
5
- "worker/containers/compile/runtime-types.cjs": "4ddaaae77e1047df7d6203c3672eda15fef1801a448eb901a39f618bdc0eefcd",
6
- "worker/containers/compile/schema-types.cjs": "a3b9c113b036c2c86409a8e753b582f87deb231bf982d87004aaa492a46785eb",
4
+ "worker/containers/compile/declared-data-contract.cjs": "b91609429f4e54d54a80848db8cf13b5e1965625c45f423f56e51e941c5daf9c",
5
+ "worker/containers/compile/runtime-types.cjs": "a458232f605ac60202b58c6cae725a813d22479498f436668b8ad553678576ef",
6
+ "worker/containers/compile/schema-types.cjs": "d6985d40a1f4462615eb65e0350fdd23d53de7c4beaa3ff574e45a036fbdec3e",
7
7
  "worker/containers/compile/typed-data.cjs": "57b458e16a373e42d48a0e527317fcdf609299ae368eb539a8b582b9a47abb15",
8
- "worker/src/utils/authority/scope-enforcement.ts": "e97d8c8a7e1708b8b2dcd1aa8ec2699771aefc044168ab3e283e3286972fccac",
9
- "worker/src/utils/db-schema-deploy/client-contract-source.ts": "5e82cfb0f313220ab2847425866f79e13431c74d16f532e659546653d73f645f",
10
- "worker/src/utils/db-schema-deploy/client-permissions.ts": "cfd486e28281809013916953822285744f25d99e0fba54952876519c005da095",
11
- "worker/src/utils/db-schema-deploy/extract-schema-ts.ts": "924eeaa09c8ff95afc3a450bb8dee67d48bb052c496b4223c6563928d6bc4c4c",
8
+ "worker/src/utils/authority/scope-enforcement.ts": "25c4aa81f0d2dd4b58784c0ada03c041ce48b66728b52cfee3790a841aba766c",
9
+ "worker/src/utils/db-schema-deploy/client-contract-source.ts": "fc37a8d18affc0d814f40357884e1764eb4bcc9af446626933729b7021fa3c6d",
10
+ "worker/src/utils/db-schema-deploy/client-permissions.ts": "05b1d68afae151569abfa16b452834a96b3c0a2bf4b04f63d322f05548827222",
11
+ "worker/src/utils/db-schema-deploy/extract-schema-ts.ts": "0da51e6884afbbf9b04104f9bf669f0d0163d7cb2b5723a6e9df05b661115a1e",
12
12
  "worker/src/utils/db-schema-deploy/schema-ownership.ts": "e6c811a87bc657707be3518dfe547c6d97718ac073108b126eb7f791c9c08202"
13
13
  },
14
- "sha256": "8b2ee9f02434710bec063e10044f5f60cf4443343db94cc14a7d479d6bf77bea"
14
+ "sha256": "b9f9cf5b17aa489ddc8ba9937a268a369ec73c91769f90eb20ab38ebc750f4c9"
15
15
  }
@@ -48,12 +48,43 @@ var require_declared_data_contract = __commonJS({
48
48
  if (result.some((field) => typeof field !== "string" || !columns.has(field)) || new Set(result).size !== result.length) fail(name);
49
49
  return result.sort();
50
50
  }
51
+ function policy(value, label) {
52
+ record(value, label);
53
+ const keys = Object.keys(value).sort().join(",");
54
+ if (value.k === "o" && keys === "c,k" && identifier.test(value.c)) return { k: "o", c: value.c };
55
+ if (value.k === "m" && keys === "k,m") {
56
+ record(value.m, label + ".member");
57
+ const { g, m, u, mg, o } = value.m;
58
+ const memberKeys = Object.keys(value.m).sort().join(",");
59
+ if (!["g,m,mg,u", "g,m,mg,o,u"].includes(memberKeys) || !identifier.test(m) || !identifier.test(u) || !Array.isArray(g) || !g.length || !Array.isArray(mg) || g.length !== mg.length || [...g, ...mg].some((field) => typeof field !== "string" || !identifier.test(field)) || o !== void 0 && (!Array.isArray(o) || JSON.stringify(o) !== JSON.stringify(["read", "create", "update", "delete"].filter((operation) => o.includes(operation))) || !o.includes("read") && (o.includes("update") || o.includes("delete")))) fail(label);
60
+ const normalizedOperations = o?.length === 4 ? void 0 : o;
61
+ return { k: "m", m: { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {} } };
62
+ }
63
+ if (value.k === "a" && keys === "k,p" && Array.isArray(value.p) && value.p.length === 2) {
64
+ const left = policy(value.p[0], label), right = policy(value.p[1], label);
65
+ if (left.k !== "o" || right.k !== "m") fail(label);
66
+ return { k: "a", p: [left, right] };
67
+ }
68
+ if (value.k === "p" && keys === "k,p,pk,t,v" && [value.v, value.t, value.pk].every((item) => typeof item === "string" && identifier.test(item))) {
69
+ const inherited = policy(value.p, label);
70
+ if (!["o", "m", "a"].includes(inherited.k)) fail(label);
71
+ return { k: "p", v: value.v, t: value.t, pk: value.pk, p: inherited };
72
+ }
73
+ fail(label);
74
+ }
75
+ function policyMembershipTables(value, out) {
76
+ if (!value || typeof value !== "object") return;
77
+ if (value.k === "m" && value.m) out.add(value.m.m);
78
+ else if (value.k === "a") value.p.forEach((item) => policyMembershipTables(item, out));
79
+ else if (value.k === "p") policyMembershipTables(value.p, out);
80
+ }
51
81
  function canonicalize2(schema, intents, scopes) {
52
82
  record(schema, "schema");
53
83
  record(intents, "intents");
54
84
  record(scopes, "scopes");
55
85
  const tables = [];
56
86
  const membershipTables = new Set(Object.values(schema).filter((table) => table && table.member).map((table) => table.member.m));
87
+ for (const table of Object.values(schema)) if (table?.policy) policyMembershipTables(table.policy, membershipTables);
57
88
  for (const name of Object.keys(schema).sort()) {
58
89
  const table = schema[name];
59
90
  record(table, `table ${name}`);
@@ -78,14 +109,30 @@ var require_declared_data_contract = __commonJS({
78
109
  if (Object.keys(input).some((key) => !["read", "publicRead", "create", "update", "delete", "identity"].includes(key))) fail(`${name} permissions`);
79
110
  const read = typeof input.read === "boolean" ? input.read : fields(input.read, seen, `${name}.read`);
80
111
  if (Array.isArray(read) && !read.includes(pk)) fail(`${name}.read must include the primary key`);
81
- if (typeof input.publicRead !== "boolean" || typeof input.delete !== "boolean" || !["authenticated", "visitor"].includes(input.identity)) fail(`${name} permissions`);
112
+ let publicRead = input.publicRead;
113
+ if (typeof publicRead !== "boolean") {
114
+ record(publicRead, `${name} public read`);
115
+ if (Object.keys(publicRead).some((key) => key !== "where")) fail(`${name} public read`);
116
+ record(publicRead.where, `${name} public read predicate`);
117
+ const entries = Object.entries(publicRead.where);
118
+ if (!entries.length || entries.length > 8) fail(`${name} public read predicate`);
119
+ const where = {};
120
+ for (const [field, value] of entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
121
+ const column = columns.find((column2) => column2.n === field);
122
+ if (!column || ["json", "blob"].includes(column.t) || !(value === null || typeof value === "string" && value.length <= 256 || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))) fail(`${name} public read predicate`);
123
+ if (value === null ? column.nul !== 1 : column.t === "boolean" ? typeof value !== "boolean" : ["integer", "number"].includes(column.t) ? typeof value !== "number" : ["text", "timestamp"].includes(column.t) ? typeof value !== "string" : true) fail(`${name} public read predicate type`);
124
+ where[field] = value;
125
+ }
126
+ publicRead = { where };
127
+ }
128
+ if (typeof input.delete !== "boolean" || !["authenticated", "visitor"].includes(input.identity)) fail(`${name} permissions`);
82
129
  const create = input.create === null ? null : fields(input.create, seen, `${name}.create`, true);
83
130
  const update = input.update === null ? null : fields(input.update, seen, `${name}.update`);
84
131
  if (membershipTables.has(name) && (create !== null || update !== null || input.delete)) fail(`${name} membership authority writes`);
85
132
  if ([...create || [], ...update || []].some((field) => columns.find((column) => column.n === field).t === "blob")) fail(`${name} blob browser writes require a server function`);
86
133
  const intent = intents[name];
87
- if (!["scoped", "shared", "member"].includes(intent)) fail(`${name} intent`);
88
- const owner = intent === "scoped" ? scopes[name] : null;
134
+ if (!["scoped", "shared", "member", "policy"].includes(intent)) fail(`${name} intent`);
135
+ let owner = intent === "scoped" ? scopes[name] || null : null;
89
136
  if (intent === "scoped" && (typeof owner !== "string" || !identifier.test(owner))) fail(`${name} owner`);
90
137
  if (table.visitors !== void 0 && table.visitors !== true) fail(`${name} visitors`);
91
138
  const visitors = table.visitors === true;
@@ -95,19 +142,64 @@ var require_declared_data_contract = __commonJS({
95
142
  if (intent === "shared" && (author !== "_sw_author_id" || !columns.some((column) => column.n === author && column.t === "text" && column.nul === 1))) fail(`${name} author`);
96
143
  if (Array.isArray(read) && read.includes(author)) fail(`${name} internal author projection`);
97
144
  if (input.identity === "visitor" && intent !== "scoped") fail(`${name} visitor identity`);
98
- if (input.publicRead && !Array.isArray(read)) fail(`${name} public read`);
145
+ if (publicRead && !Array.isArray(read)) fail(`${name} public read`);
146
+ if (typeof publicRead === "object" && [...create || [], ...update || []].some((field) => Object.prototype.hasOwnProperty.call(publicRead.where, field))) fail(`${name} publication writes require a server function`);
99
147
  if ([...create || [], ...update || []].some((field) => field === pk || field === owner || field === author)) fail(`${name} identity writes`);
100
148
  let member = null;
101
149
  if (intent === "member") {
102
150
  record(table.member, `${name}.member`);
103
- const { g, m, u, mg } = table.member;
151
+ const { g, m, u, mg, o } = table.member;
104
152
  if (typeof m !== "string" || typeof u !== "string" || !identifier.test(m) || !identifier.test(u) || !Array.isArray(mg) || mg.some((field) => typeof field !== "string" || !identifier.test(field))) fail(`${name}.member`);
105
153
  fields(g, seen, `${name}.member.g`);
106
154
  if (g.length !== mg.length || new Set(mg).size !== mg.length) fail(`${name}.member`);
107
155
  if ((update || []).some((field) => g.includes(field))) fail(`${name} membership writes`);
108
- member = { g: [...g], m, u, mg: [...mg] };
156
+ if (o !== void 0 && (!Array.isArray(o) || JSON.stringify(o) !== JSON.stringify(["read", "create", "update", "delete"].filter((operation) => o.includes(operation))) || !o.includes("read") && (o.includes("update") || o.includes("delete")))) fail(`${name}.member operations`);
157
+ const normalizedOperations = o?.length === 4 ? void 0 : o;
158
+ member = { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {} };
159
+ }
160
+ const normalizedPolicy = intent === "policy" ? policy(table.policy, `${name}.policy`) : null;
161
+ if (normalizedPolicy && !["a", "p"].includes(normalizedPolicy.k)) fail(`${name}.policy root`);
162
+ if (normalizedPolicy?.k === "a") {
163
+ owner = normalizedPolicy.p[0].c;
164
+ if ([...create || [], ...update || []].includes(owner) || (update || []).some((field) => normalizedPolicy.p[1].m.g.includes(field))) fail(`${name}.policy identity writes`);
165
+ }
166
+ if (normalizedPolicy?.k === "p" && create !== null && !create.includes(normalizedPolicy.v)) fail(`${name}.policy parent create`);
167
+ tables.push({
168
+ name,
169
+ columns,
170
+ client: { identity: input.identity, read, publicRead, create, update, delete: input.delete },
171
+ primaryKey: pk,
172
+ intent,
173
+ owner,
174
+ author,
175
+ visitors,
176
+ member,
177
+ ...normalizedPolicy ? { policy: normalizedPolicy } : {}
178
+ });
179
+ }
180
+ const tableByName = new Map(tables.map((table) => [table.name, table]));
181
+ for (const table of tables) {
182
+ const declared = schema[table.name].relations;
183
+ if (declared === void 0) continue;
184
+ if (!Array.isArray(declared)) fail(`${table.name}.relations`);
185
+ const relations = [];
186
+ const names = /* @__PURE__ */ new Set();
187
+ for (const relation of declared) {
188
+ record(relation, `${table.name}.relation`);
189
+ if (Object.keys(relation).some((key) => !["name", "table", "fk", "parentKey"].includes(key))) fail(`${table.name}.relation`);
190
+ const { name, table: childName, fk, parentKey } = relation;
191
+ if (![name, childName, fk, parentKey].every((value) => typeof value === "string" && identifier.test(value)) || names.has(name)) {
192
+ fail(`${table.name}.relation`);
193
+ }
194
+ names.add(name);
195
+ const declaredChild = schema[childName];
196
+ if (!declaredChild || !Array.isArray(declaredChild.columns) || parentKey !== table.primaryKey || !declaredChild.columns.some((column) => column && column.n === fk)) fail(`${table.name}.relation`);
197
+ const child = tableByName.get(childName);
198
+ if (table.client.read !== false && child && child.client.read !== false) {
199
+ relations.push({ name, table: childName, fk, parentKey });
200
+ }
109
201
  }
110
- tables.push({ name, columns, client: { identity: input.identity, read, publicRead: input.publicRead, create, update, delete: input.delete }, primaryKey: pk, intent, owner, author, visitors, member });
202
+ if (relations.length) table.relations = relations.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
111
203
  }
112
204
  return JSON.stringify({ version: 1, tables });
113
205
  }
@@ -115,10 +207,10 @@ var require_declared_data_contract = __commonJS({
115
207
  const type = { integer: "number | string", number: "number", text: "string", timestamp: "string", boolean: "boolean", json: "Json", blob: "number[]" }[column.t];
116
208
  return column.nul ? `${type} | null` : type;
117
209
  }
118
- function shape(columns, names, mode) {
210
+ function shape(columns, names, mode, required = /* @__PURE__ */ new Set()) {
119
211
  const values = names.map((name) => {
120
212
  const column = columns.find((candidate) => candidate.n === name);
121
- const optional = mode === "update" || mode === "create" && (column.d || column.nul);
213
+ const optional = mode === "update" || mode === "create" && !required.has(name) && (column.d || column.nul);
122
214
  return `${JSON.stringify(name)}${optional ? "?" : ""}: ${columnType(column)}`;
123
215
  });
124
216
  return values.length ? `{ ${values.join("; ")} }` : "Record<string, never>";
@@ -143,7 +235,8 @@ var require_declared_data_contract = __commonJS({
143
235
  }
144
236
  const mutation = client.read === false ? "{ count: number; changes: number }" : `{ data: ${row} | null; count: number; changes: number }`;
145
237
  if (client.create !== null) {
146
- operations.push(`create(values: ${shape(columns, client.create, "create")}): Promise<${mutation}>`);
238
+ const required = table.policy?.k === "p" ? /* @__PURE__ */ new Set([table.policy.v]) : /* @__PURE__ */ new Set();
239
+ operations.push(`create(values: ${shape(columns, client.create, "create", required)}): Promise<${mutation}>`);
147
240
  methods.push("create");
148
241
  }
149
242
  if (client.update !== null) {
@@ -154,8 +247,21 @@ var require_declared_data_contract = __commonJS({
154
247
  operations.push(`delete(id: ${id}): Promise<${mutation}>`);
155
248
  methods.push("delete");
156
249
  }
250
+ const relationDeclarations = [];
251
+ const relationNames = [];
252
+ for (const relation of table.relations || []) {
253
+ const child = tables.find((candidate) => candidate.name === relation.table);
254
+ const childReadable = child.client.read === true ? child.columns.filter((column) => column.n !== child.author).map((column) => column.n) : child.client.read;
255
+ const childRow = shape(child.columns, childReadable, "read");
256
+ const childId = columnType(child.columns.find((column) => column.n === child.primaryKey));
257
+ const parentId = columnType(columns.find((column) => column.n === primaryKey));
258
+ const equality = childReadable.filter((field) => !["json", "blob"].includes(child.columns.find((column) => column.n === field).t));
259
+ relationDeclarations.push(`${JSON.stringify(relation.name)}: { list(parentId: ${parentId}, options?: { limit?: number; after?: ${childId}; where?: ${shape(child.columns, equality, "update")} }): Promise<{ data: ${childRow}[]; next: ${childId} | null; has_more: boolean }> }`);
260
+ relationNames.push(relation.name);
261
+ }
262
+ if (relationDeclarations.length) operations.push(`relations: { ${relationDeclarations.join("; ")} }`);
157
263
  declarations.push(`${JSON.stringify(name)}: { ${operations.join("; ")} }`);
158
- runtimeTables.push([name, methods]);
264
+ runtimeTables.push([name, methods, relationNames]);
159
265
  }
160
266
  const declaration = `declare module ${JSON.stringify("somewhere:data")} {
161
267
  export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
@@ -166,7 +272,7 @@ var require_declared_data_contract = __commonJS({
166
272
  const runtime = `const contract=${JSON.stringify(contract_digest)};
167
273
  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}}
168
274
  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};
169
- const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations])=>[table,Object.freeze(Object.fromEntries(operations.map(operation=>[operation,operation==="list"?(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})])))])));
275
+ const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"?(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))];})));
170
276
  export{data,DataError};
171
277
  `;
172
278
  return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration } };
@@ -216,12 +322,13 @@ var require_schema_types = __commonJS({
216
322
  interface Relation { readonly __somewhereRelation: unique symbol }
217
323
  interface Scope { readonly __somewhereScope: unique symbol }
218
324
  interface OwnerScope extends Scope { readonly __somewhereOwner: unique symbol }
325
+ interface MemberScope extends Scope { readonly __somewhereMember: unique symbol }
219
326
  interface ColumnOptions<Default> {
220
327
  nullable?: boolean; default?: Default | null; unique?: boolean;
221
328
  references?: string; onDelete?: 'cascade' | 'restrict'; renamedFrom?: string;
222
329
  }
223
330
  interface ClientPermissions<Field extends string> {
224
- identity?: 'authenticated' | 'visitor'; read?: boolean | Field[]; publicRead?: boolean;
331
+ identity?: 'authenticated' | 'visitor'; read?: boolean | Field[]; publicRead?: boolean | { where: Partial<Record<Field, string | number | boolean | null>> };
225
332
  create?: Field[] | false | null; update?: Field[] | false | null; delete?: boolean;
226
333
  }
227
334
  interface TableOptions<Field extends string> {
@@ -243,14 +350,18 @@ function blob(options?: SomewhereSchemaDeclaration.ColumnOptions<never>): Somewh
243
350
  function owner(options?: { column?: string; visitors?: boolean }): SomewhereSchemaDeclaration.OwnerScope;
244
351
  function shared(): SomewhereSchemaDeclaration.Scope;
245
352
  function serverOnly(): SomewhereSchemaDeclaration.Scope;
246
- function member(options: { group: string | string[]; membership: string; member_user: string; member_group: string | string[] }): SomewhereSchemaDeclaration.Scope;
353
+ function member(options: { group: string | string[]; membership: string; member_user: string; member_group: string | string[]; operations?: Array<'read' | 'create' | 'update' | 'delete'> }): SomewhereSchemaDeclaration.MemberScope;
247
354
  function hasMany(table: string, foreignKey: string): SomewhereSchemaDeclaration.Relation;
248
355
  function removed(): SomewhereSchemaDeclaration.ColumnMarker;
249
356
  function removedTable(): SomewhereSchemaDeclaration.TableMarker;
250
357
  function exported(): SomewhereSchemaDeclaration.TableMarker;
358
+ `;
359
+ var policySignatures = `function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, member: SomewhereSchemaDeclaration.MemberScope): SomewhereSchemaDeclaration.Scope;
360
+ function anyOf(member: SomewhereSchemaDeclaration.MemberScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.Scope;
361
+ function parent(options: { via: string }): SomewhereSchemaDeclaration.Scope;
251
362
  `;
252
363
  var SCHEMA_DECLARATION2 = types + signatures.replace(/^function /gm, "declare function ") + `declare module 'somewhere/db' {
253
- ${signatures.replace(/^function /gm, " export function ")}}
364
+ ${(signatures + policySignatures).replace(/^function /gm, " export function ")}}
254
365
  `;
255
366
  module2.exports = { SCHEMA_DECLARATION: SCHEMA_DECLARATION2 };
256
367
  }
@@ -276,7 +387,10 @@ type __SomewhereDbOperator = {
276
387
  & Partial<Record<Exclude<keyof __SomewhereDbOperators, K>, never>>
277
388
  }[keyof __SomewhereDbOperators];
278
389
  type SomewhereDbCondition = SomewhereDbScalar | __SomewhereDbOperator;
279
- type SomewhereDbWhere = Readonly<Record<string, SomewhereDbCondition | readonly SomewhereDbCondition[]>>;
390
+ type SomewhereDbOrBranch = Readonly<Record<string, SomewhereDbCondition>>;
391
+ type SomewhereDbWhere = Readonly<{
392
+ $or?: readonly SomewhereDbOrBranch[];
393
+ } & Record<string, SomewhereDbCondition | readonly SomewhereDbCondition[] | readonly SomewhereDbOrBranch[] | undefined>>;
280
394
  type SomewhereDbOrder = string | readonly [string, ('asc' | 'desc')?]
281
395
  | readonly (readonly [string, ('asc' | 'desc')?])[];
282
396
  interface SomewhereDbReadOptions {
@@ -291,14 +405,17 @@ interface SomewhereDbReadOptions {
291
405
  interface SomewhereDbCountOptions { where?: SomewhereDbWhere | null }
292
406
  interface SomewhereDbInsertOptions { onConflict?: 'ignore' | 'update' | null }
293
407
  type SomewhereDbValues = Readonly<Record<string, __SomewhereJson>>;
294
- interface SomewhereDbUpdate {
295
- set: SomewhereDbValues;
408
+ type SomewhereDbIncrement = Readonly<Record<string, number>>;
409
+ type SomewhereDbUpdate = {
296
410
  where?: SomewhereDbWhere | null;
297
- }
411
+ } & (
412
+ | { set: SomewhereDbValues; increment?: SomewhereDbIncrement | null }
413
+ | { set?: never; increment: SomewhereDbIncrement }
414
+ );
298
415
  interface SomewhereDbRemove { where?: SomewhereDbWhere | null }
299
416
  type SomewhereDbWriteIntent =
300
417
  | { op: 'insert'; table: string; values: SomewhereDbValues; options?: SomewhereDbInsertOptions | null }
301
- | { op: 'update'; table: string; set: SomewhereDbValues; where?: SomewhereDbWhere | null }
418
+ | ({ op: 'update'; table: string } & SomewhereDbUpdate)
302
419
  | { op: 'remove'; table: string; where?: SomewhereDbWhere | null };
303
420
  interface SomewhereDbResult {
304
421
  // Composed results preserve the database representation. They do not use
@@ -364,7 +481,7 @@ function parseClientPermissions(value, columns, scope) {
364
481
  return fail(`Unknown client permission "${key}".`);
365
482
  }
366
483
  }
367
- if (!["owner", "member", "shared"].includes(scope.kind)) {
484
+ if (!["owner", "member", "shared", "policy"].includes(scope.kind)) {
368
485
  return fail("serverOnly() tables cannot declare client access. Use a server function for business logic.");
369
486
  }
370
487
  const identity = scope.kind === "owner" && scope.visitors === true ? "visitor" : "authenticated";
@@ -375,8 +492,33 @@ function parseClientPermissions(value, columns, scope) {
375
492
  return fail("client.identity must match the owner() declaration. Visitor access requires owner({ visitors: true }); owner() and other scopes require authenticated identity.");
376
493
  }
377
494
  let read = input.read ?? false;
378
- const publicRead = input.publicRead ?? false;
379
- if (typeof publicRead !== "boolean") return fail("client.publicRead must be true or false.");
495
+ const rawPublicRead = input.publicRead ?? false;
496
+ let publicRead;
497
+ if (typeof rawPublicRead === "boolean") publicRead = rawPublicRead;
498
+ else {
499
+ if (!rawPublicRead || typeof rawPublicRead !== "object" || Array.isArray(rawPublicRead) || Object.keys(rawPublicRead).some((key) => key !== "where")) {
500
+ return fail("client.publicRead must be true, false, or { where: { field: literal } }.");
501
+ }
502
+ const where = rawPublicRead.where;
503
+ if (!where || typeof where !== "object" || Array.isArray(where)) return fail("client.publicRead.where must be a non-empty object.");
504
+ const entries = Object.entries(where);
505
+ if (entries.length === 0 || entries.length > 8) return fail("client.publicRead.where must declare 1 to 8 equality fields.");
506
+ const normalized = {};
507
+ for (const [rawName, literal] of entries) {
508
+ const name = rawName.toLowerCase();
509
+ const column = columns.find((candidate) => candidate.name === name);
510
+ if (Object.prototype.hasOwnProperty.call(normalized, name)) return fail(`client.publicRead.where declares field "${rawName}" twice.`);
511
+ if (!column || ["json", "blob"].includes(column.helper)) return fail(`client.publicRead.where names undeclared or non-scalar field "${rawName}".`);
512
+ if (!(literal === null || typeof literal === "boolean" || typeof literal === "string" || typeof literal === "number" && Number.isFinite(literal))) return fail(`client.publicRead.where field "${rawName}" must equal a literal scalar.`);
513
+ if (typeof literal === "string" && literal.length > 256) return fail(`client.publicRead.where field "${rawName}" exceeds 256 characters.`);
514
+ if (literal === null && !column.nullable) return fail(`client.publicRead.where field "${rawName}" cannot equal null because it is required.`);
515
+ if (literal !== null && (column.helper === "boolean" && typeof literal !== "boolean" || ["integer", "number"].includes(column.helper) && typeof literal !== "number" || ["text", "timestamp"].includes(column.helper) && typeof literal !== "string")) {
516
+ return fail(`client.publicRead.where field "${rawName}" must match its declared type.`);
517
+ }
518
+ normalized[name] = literal;
519
+ }
520
+ publicRead = { where: Object.fromEntries(Object.entries(normalized).sort(([a], [b]) => a.localeCompare(b))) };
521
+ }
380
522
  if (read !== true && read !== false && !Array.isArray(read)) return fail("client.read must list readable fields, or be true or false.");
381
523
  if (publicRead && (!Array.isArray(read) || read.length === 0)) {
382
524
  return fail("client.publicRead requires an explicit non-empty client.read field list.");
@@ -406,11 +548,14 @@ function parseClientPermissions(value, columns, scope) {
406
548
  const name = entry.toLowerCase();
407
549
  const column = fields.get(name);
408
550
  if (!column) return fail(`client.${operation} names undeclared or platform-managed field "${entry}".`);
409
- if (column.helper === "id" || scope.kind === "owner" && name === scope.column) {
551
+ if (column.helper === "id" || (scope.kind === "owner" || scope.kind === "policy") && name === scope.column) {
410
552
  return fail(`client.${operation} cannot write the platform-assigned identity field "${entry}".`);
411
553
  }
412
554
  if (column.helper === "blob") return fail(`client.${operation} cannot write binary field "${entry}". Use project files for uploads.`);
413
- if (operation === "update" && scope.kind === "member" && scope.group?.includes(name)) {
555
+ if (typeof publicRead === "object" && Object.prototype.hasOwnProperty.call(publicRead.where, name)) {
556
+ return fail(`client.${operation} cannot write publication field "${entry}". Use a server operation to publish rows.`);
557
+ }
558
+ if (operation === "update" && (scope.kind === "member" || scope.kind === "policy") && scope.group?.includes(name)) {
414
559
  return fail(`client.update cannot move a row between membership groups by changing "${entry}".`);
415
560
  }
416
561
  if (allowed.includes(name)) return fail(`client.${operation} declares field "${entry}" twice.`);
@@ -420,6 +565,18 @@ function parseClientPermissions(value, columns, scope) {
420
565
  writes[operation] = allowed.sort();
421
566
  }
422
567
  if (writes.create !== null) {
568
+ if (typeof publicRead === "object") {
569
+ let guaranteedPrivate = false;
570
+ for (const [name, publishedValue] of Object.entries(publicRead.where)) {
571
+ const column = fields.get(name);
572
+ if (column.autoNow) continue;
573
+ const defaultValue = column.hasDefault ? column.default : column.nullable ? null : void 0;
574
+ if (defaultValue !== void 0 && defaultValue !== publishedValue) guaranteedPrivate = true;
575
+ }
576
+ if (!guaranteedPrivate) {
577
+ return fail("client.create has no fixed publication-field default that guarantees a private row. Give at least one predicate field a private default, or use a server operation.");
578
+ }
579
+ }
423
580
  for (const column of columns) {
424
581
  if (column.helper !== "id" && !column.nullable && !column.hasDefault && !column.autoNow && !writes.create.includes(column.name)) {
425
582
  return fail(`client.create must include required field "${column.name}", or that field needs a declared default.`);
@@ -463,7 +620,14 @@ function bakedTableSchemaFromDeclared(declaredJson) {
463
620
  if (col.nullable === true) entry.nul = 1;
464
621
  if (implicitDefault || col.hasDefault === true || col.autoNow === true) entry.d = 1;
465
622
  out.push(entry);
466
- permissionColumns.push({ name: entry.n, helper: col.helper, nullable: col.nullable === true, hasDefault: col.hasDefault === true, autoNow: col.autoNow === true });
623
+ permissionColumns.push({
624
+ name: entry.n,
625
+ helper: col.helper,
626
+ nullable: col.nullable === true,
627
+ hasDefault: col.hasDefault === true,
628
+ autoNow: col.autoNow === true,
629
+ ...col.default === null || ["string", "number", "boolean"].includes(typeof col.default) ? { default: col.default } : {}
630
+ });
467
631
  if (col.helper === "id") primaryKeys.push(entry.n);
468
632
  }
469
633
  const relations = bakedRelationsFromDeclared(shape.relations);
@@ -479,11 +643,13 @@ function bakedTableSchemaFromDeclared(declaredJson) {
479
643
  if (!scope || typeof scope !== "object" || primaryKeys.length !== 1) return null;
480
644
  const sc = scope;
481
645
  if (typeof sc.kind !== "string") return null;
646
+ const policyIdentity = sc.kind === "policy" ? declaredPolicyIdentity(sc.policy) : {};
482
647
  const parsed = parseClientPermissions(shape.client, permissionColumns, {
483
648
  kind: sc.kind,
484
649
  ...visitors ? { visitors: true } : {},
485
650
  ...typeof sc.column === "string" ? { column: sc.column } : {},
486
- ...Array.isArray(sc.group) && sc.group.every((value) => typeof value === "string") ? { group: sc.group } : {}
651
+ ...Array.isArray(sc.group) && sc.group.every((value) => typeof value === "string") ? { group: sc.group } : {},
652
+ ...policyIdentity
487
653
  });
488
654
  if (!parsed.ok) return null;
489
655
  if (Array.isArray(parsed.permissions.read) && !parsed.permissions.read.includes(primaryKeys[0])) return null;
@@ -494,10 +660,48 @@ function bakedTableSchemaFromDeclared(declaredJson) {
494
660
  if (!member) return null;
495
661
  return relations ? { columns: out, member, relations, ...browser } : { columns: out, member, ...browser };
496
662
  }
663
+ if (scope !== null && typeof scope === "object" && scope.kind === "policy") {
664
+ const policy = bakedPolicyFromDeclared(scope.policy);
665
+ if (!policy || policy.k !== "a" && policy.k !== "p") return null;
666
+ return relations ? { columns: out, policy, relations, ...browser } : { columns: out, policy, ...browser };
667
+ }
497
668
  const author = scope !== null && typeof scope === "object" && scope.kind === "shared" ? SHARED_AUTHOR_COLUMN : void 0;
498
669
  if (author) out.push({ n: author, t: "text", nul: 1 });
499
670
  return { columns: out, ...relations ? { relations } : {}, ...author ? { author } : {}, ...visitors ? { visitors } : {}, ...browser };
500
671
  }
672
+ function declaredPolicyIdentity(value) {
673
+ if (!value || typeof value !== "object") return {};
674
+ const node = value;
675
+ if (node.kind === "owner" && typeof node.column === "string") return { column: node.column };
676
+ if (node.kind === "member" && Array.isArray(node.group) && node.group.every((entry) => typeof entry === "string")) return { group: node.group };
677
+ if (node.kind === "any_of" && Array.isArray(node.policies)) {
678
+ return node.policies.reduce((all, child) => ({ ...all, ...declaredPolicyIdentity(child) }), {});
679
+ }
680
+ return {};
681
+ }
682
+ function bakedPolicyFromDeclared(value, depth = 0) {
683
+ if (depth > 2 || value === null || typeof value !== "object") return null;
684
+ const node = value;
685
+ if (node.kind === "owner") {
686
+ return typeof node.column === "string" && SAFE_SCOPE_IDENTIFIER.test(node.column) ? { k: "o", c: node.column.toLowerCase() } : null;
687
+ }
688
+ if (node.kind === "member") {
689
+ const member = bakedMemberFromDeclared(node);
690
+ return member ? { k: "m", m: member } : null;
691
+ }
692
+ if (node.kind === "any_of" && Array.isArray(node.policies) && node.policies.length === 2) {
693
+ if (depth > 1 || node.policies[0]?.kind !== "owner" || node.policies[1]?.kind !== "member") return null;
694
+ const left = bakedPolicyFromDeclared(node.policies[0], depth + 1);
695
+ const right = bakedPolicyFromDeclared(node.policies[1], depth + 1);
696
+ return left && right ? { k: "a", p: [left, right] } : null;
697
+ }
698
+ if (node.kind === "parent" && typeof node.via === "string" && SAFE_SCOPE_IDENTIFIER.test(node.via) && typeof node.table === "string" && SAFE_SCOPE_IDENTIFIER.test(node.table) && typeof node.parentKey === "string" && SAFE_SCOPE_IDENTIFIER.test(node.parentKey)) {
699
+ if (depth !== 0 || node.policy?.kind === "parent") return null;
700
+ const inherited = bakedPolicyFromDeclared(node.policy, depth + 1);
701
+ return inherited ? { k: "p", v: node.via.toLowerCase(), t: node.table.toLowerCase(), pk: node.parentKey.toLowerCase(), p: inherited } : null;
702
+ }
703
+ return null;
704
+ }
501
705
  function bakedRelationsFromDeclared(value) {
502
706
  if (!Array.isArray(value) || value.length === 0) return void 0;
503
707
  const out = [];
@@ -532,10 +736,32 @@ function bakedMemberFromDeclared(scope) {
532
736
  if (!g || !mg || g.length !== mg.length) return null;
533
737
  if (typeof s.membership !== "string" || !SAFE_SCOPE_IDENTIFIER.test(s.membership)) return null;
534
738
  if (typeof s.memberUser !== "string" || !SAFE_SCOPE_IDENTIFIER.test(s.memberUser)) return null;
535
- return { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg };
739
+ let o;
740
+ if (s.operations !== void 0) {
741
+ if (!Array.isArray(s.operations)) return null;
742
+ const operations = s.operations;
743
+ const allowed = ["read", "create", "update", "delete"];
744
+ if (JSON.stringify(operations) !== JSON.stringify(allowed.filter((operation) => operations.includes(operation))) || !operations.includes("read") && (operations.includes("update") || operations.includes("delete"))) return null;
745
+ o = operations.length === allowed.length ? void 0 : operations;
746
+ }
747
+ return { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg, ...o !== void 0 ? { o } : {} };
536
748
  }
537
749
 
538
750
  // worker/src/utils/db-schema-deploy/extract-schema-ts.ts
751
+ function policyOwner(policy) {
752
+ if (policy.kind === "owner") return policy;
753
+ if (policy.kind === "any_of") return policy.policies.map(policyOwner).find((value) => value !== null) ?? null;
754
+ return null;
755
+ }
756
+ function scopeOwner(scope) {
757
+ return scope.kind === "owner" ? scope : scope.kind === "policy" ? policyOwner(scope.policy) : null;
758
+ }
759
+ function declaredOwnerColumn(scope) {
760
+ return scopeOwner(scope)?.column ?? null;
761
+ }
762
+ function declaredScopeIntent(scope) {
763
+ return scope.kind === "owner" ? "scoped" : scope.kind;
764
+ }
539
765
  var COLUMN_HELPERS = /* @__PURE__ */ new Set([
540
766
  "id",
541
767
  "text",
@@ -546,7 +772,7 @@ var COLUMN_HELPERS = /* @__PURE__ */ new Set([
546
772
  "json",
547
773
  "blob"
548
774
  ]);
549
- var SCOPE_HELPERS = /* @__PURE__ */ new Set(["owner", "shared", "serverOnly", "member"]);
775
+ var SCOPE_HELPERS = /* @__PURE__ */ new Set(["owner", "shared", "serverOnly", "member", "anyOf", "parent"]);
550
776
  var TABLE_MARKER_HELPERS = /* @__PURE__ */ new Set(["removedTable", "exported"]);
551
777
  var SAFE_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
552
778
  var RESERVED_PREFIXES = ["app_", "sqlite_", "d1_", "__sw_"];
@@ -557,6 +783,7 @@ var MAX_INDEX_COLUMNS = 8;
557
783
  var MAX_NAME_LENGTH = 64;
558
784
  var MAX_SOURCE_BYTES = 256 * 1024;
559
785
  var DEFAULT_OWNER_COLUMN = "user_id";
786
+ var MEMBER_OPERATIONS = ["read", "create", "update", "delete"];
560
787
  var SchemaTsError = class extends Error {
561
788
  };
562
789
  function tokenize(source) {
@@ -738,7 +965,15 @@ function readClientPermissions(r) {
738
965
  const key = r.readKey("for a client permission");
739
966
  if (Object.prototype.hasOwnProperty.call(out, key.name)) r.fail(`client declares "${key.name}" twice.`);
740
967
  r.expectPunct(":", `after client permission "${key.name}"`);
741
- if (r.tryPunct("[")) {
968
+ if (key.name === "publicRead" && r.peek().kind === "punct" && r.peek().value === "{") {
969
+ r.expectPunct("{", "opening client.publicRead");
970
+ const whereKey = r.readKey("in client.publicRead");
971
+ if (whereKey.name !== "where") r.fail("client.publicRead only accepts a where object.");
972
+ r.expectPunct(":", "after client.publicRead.where");
973
+ out[key.name] = { where: Object.fromEntries([...r.readLiteralObject("for client.publicRead.where")].map(([name, item]) => [name, item.value])) };
974
+ r.tryPunct(",");
975
+ r.expectPunct("}", "closing client.publicRead");
976
+ } else if (r.tryPunct("[")) {
742
977
  const fields = [];
743
978
  while (!r.tryPunct("]")) {
744
979
  fields.push(r.readLiteral(`in client.${key.name}`));
@@ -944,12 +1179,33 @@ function validateMemberColumns(names, optionLabel, tableName, line) {
944
1179
  seen.add(name);
945
1180
  }
946
1181
  }
1182
+ function readMemberOperations(r, tableName) {
1183
+ r.expectPunct("[", `for member({ operations }) on table "${tableName}"`);
1184
+ const seen = /* @__PURE__ */ new Set();
1185
+ while (!r.tryPunct("]")) {
1186
+ const token = r.next();
1187
+ if (token.kind !== "string" || !MEMBER_OPERATIONS.includes(token.value)) {
1188
+ throw new SchemaTsError(`line ${token.line}: member({ operations }) on table "${tableName}" only accepts 'read', 'create', 'update', and 'delete'.`);
1189
+ }
1190
+ seen.add(token.value);
1191
+ if (!r.tryPunct(",")) {
1192
+ r.expectPunct("]", `closing member({ operations }) on table "${tableName}"`);
1193
+ break;
1194
+ }
1195
+ }
1196
+ const normalized = MEMBER_OPERATIONS.filter((operation) => seen.has(operation));
1197
+ if (!seen.has("read") && (seen.has("update") || seen.has("delete"))) {
1198
+ throw new SchemaTsError(`member({ operations }) on table "${tableName}" must include 'read' when it includes 'update' or 'delete', because mutations return the affected row.`);
1199
+ }
1200
+ return normalized.length === MEMBER_OPERATIONS.length ? void 0 : normalized;
1201
+ }
947
1202
  function readMemberScope(r, tableName, scopeLine) {
948
1203
  r.expectPunct("{", `in member() options for table "${tableName}" \u2014 member() needs { group, membership, member_user, member_group }`);
949
1204
  let group = null;
950
1205
  let membership = null;
951
1206
  let memberUser = null;
952
1207
  let memberGroup = null;
1208
+ let operations, operationsSeen = false;
953
1209
  while (!r.tryPunct("}")) {
954
1210
  const key = r.readKey(`for a member() option of table "${tableName}"`);
955
1211
  r.expectPunct(":", `after "${key.name}" in member() for table "${tableName}"`);
@@ -961,9 +1217,13 @@ function readMemberScope(r, tableName, scopeLine) {
961
1217
  memberUser = readSingleColumn(r, `member({ member_user }) on table "${tableName}"`);
962
1218
  } else if (key.name === "member_group") {
963
1219
  memberGroup = readColumnOrColumnList(r, `member({ member_group }) on table "${tableName}"`);
1220
+ } else if (key.name === "operations") {
1221
+ if (operationsSeen) throw new SchemaTsError(`line ${key.line}: member() on table "${tableName}" declares operations more than once.`);
1222
+ operationsSeen = true;
1223
+ operations = readMemberOperations(r, tableName);
964
1224
  } else {
965
1225
  throw new SchemaTsError(
966
- `line ${key.line}: member() on table "${tableName}" has an unknown option "${key.name}". Allowed: group, membership, member_user, member_group.`
1226
+ `line ${key.line}: member() on table "${tableName}" has an unknown option "${key.name}". Allowed: group, membership, member_user, member_group, operations.`
967
1227
  );
968
1228
  }
969
1229
  if (!r.tryPunct(",")) {
@@ -993,7 +1253,66 @@ function readMemberScope(r, tableName, scopeLine) {
993
1253
  `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.`
994
1254
  );
995
1255
  }
996
- return { kind: "member", group, membership, memberUser, memberGroup };
1256
+ return { kind: "member", group, membership, memberUser, memberGroup, ...operations !== void 0 ? { operations } : {} };
1257
+ }
1258
+ function readPolicyOwner(r, tableName, line) {
1259
+ r.expectPunct("(", 'after "owner"');
1260
+ let column = DEFAULT_OWNER_COLUMN;
1261
+ if (!r.tryPunct(")")) {
1262
+ const opts = r.readLiteralObject(`in owner() inside anyOf() for table "${tableName}"`);
1263
+ for (const [key, { value, line: optionLine }] of opts) {
1264
+ if (key !== "column") {
1265
+ throw new SchemaTsError(`line ${optionLine}: owner() inside anyOf() only allows column. Visitor ownership is not part of policy scope.`);
1266
+ }
1267
+ if (typeof value !== "string" || !SAFE_IDENT.test(value) || value.length > MAX_NAME_LENGTH) {
1268
+ throw new SchemaTsError(`line ${optionLine}: owner({ column }) on table "${tableName}" must be a valid column name.`);
1269
+ }
1270
+ column = value.toLowerCase();
1271
+ }
1272
+ r.tryPunct(",");
1273
+ r.expectPunct(")", "closing owner()");
1274
+ }
1275
+ return { kind: "owner", column };
1276
+ }
1277
+ function readPolicyMember(r, tableName, line) {
1278
+ r.expectPunct("(", 'after "member"');
1279
+ const scope = readMemberScope(r, tableName, line);
1280
+ r.tryPunct(",");
1281
+ r.expectPunct(")", "closing member()");
1282
+ if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
1283
+ return scope;
1284
+ }
1285
+ function readAnyOfScope(r, tableName, line) {
1286
+ r.expectPunct("(", 'after "anyOf"');
1287
+ const first = r.expectIdent(`for the first policy in anyOf() on table "${tableName}"`);
1288
+ const firstPolicy = first.value === "owner" ? readPolicyOwner(r, tableName, first.line) : first.value === "member" ? readPolicyMember(r, tableName, first.line) : null;
1289
+ if (!firstPolicy) throw new SchemaTsError(`line ${first.line}: anyOf() only accepts owner() and member().`);
1290
+ r.expectPunct(",", `between the two policies in anyOf() on table "${tableName}"`);
1291
+ const second = r.expectIdent(`for the second policy in anyOf() on table "${tableName}"`);
1292
+ const secondPolicy = second.value === "owner" ? readPolicyOwner(r, tableName, second.line) : second.value === "member" ? readPolicyMember(r, tableName, second.line) : null;
1293
+ if (!secondPolicy) throw new SchemaTsError(`line ${second.line}: anyOf() only accepts owner() and member().`);
1294
+ r.tryPunct(",");
1295
+ r.expectPunct(")", "closing anyOf()");
1296
+ if (firstPolicy.kind === secondPolicy.kind) {
1297
+ throw new SchemaTsError(`line ${line}: anyOf() requires exactly one owner() and one member() policy.`);
1298
+ }
1299
+ const owner = firstPolicy.kind === "owner" ? firstPolicy : secondPolicy;
1300
+ const member = firstPolicy.kind === "member" ? firstPolicy : secondPolicy;
1301
+ return { kind: "policy", policy: { kind: "any_of", policies: [owner, member] } };
1302
+ }
1303
+ function readParentScope(r, tableName, line) {
1304
+ r.expectPunct("(", 'after "parent"');
1305
+ const opts = r.readLiteralObject(`in parent() options for table "${tableName}"`);
1306
+ r.tryPunct(",");
1307
+ r.expectPunct(")", "closing parent()");
1308
+ if (opts.size !== 1 || !opts.has("via")) {
1309
+ throw new SchemaTsError(`line ${line}: parent() on table "${tableName}" needs exactly { via: 'foreign_key' }.`);
1310
+ }
1311
+ const value = opts.get("via").value;
1312
+ if (typeof value !== "string" || !SAFE_IDENT.test(value) || value.length > MAX_NAME_LENGTH) {
1313
+ throw new SchemaTsError(`line ${opts.get("via").line}: parent({ via }) on table "${tableName}" must name a valid declared foreign-key column.`);
1314
+ }
1315
+ return { kind: "policy", policy: { kind: "parent", via: value.toLowerCase(), table: "", parentKey: "", policy: { kind: "owner", column: "" } } };
997
1316
  }
998
1317
  function readRelations(r, tableName) {
999
1318
  r.expectPunct("{", `for "relations" of table "${tableName}"`);
@@ -1041,9 +1360,11 @@ function readScope(r, tableName) {
1041
1360
  const tok = r.expectIdent(`for the scope of table "${tableName}"`);
1042
1361
  if (!SCOPE_HELPERS.has(tok.value)) {
1043
1362
  throw new SchemaTsError(
1044
- `line ${tok.line}: unknown scope "${tok.value}" on table "${tableName}". Use owner(), member(), shared(), or serverOnly().`
1363
+ `line ${tok.line}: unknown scope "${tok.value}" on table "${tableName}". Use owner(), member(), anyOf(), parent(), shared(), or serverOnly().`
1045
1364
  );
1046
1365
  }
1366
+ if (tok.value === "anyOf") return readAnyOfScope(r, tableName, tok.line);
1367
+ if (tok.value === "parent") return readParentScope(r, tableName, tok.line);
1047
1368
  if (tok.value === "member") {
1048
1369
  r.expectPunct("(", 'after "member"');
1049
1370
  const scope = readMemberScope(r, tableName, tok.line);
@@ -1215,19 +1536,20 @@ function readTable(r, rawName, nameLine) {
1215
1536
  r.tryPunct(",");
1216
1537
  r.expectPunct(")", `closing table("${rawName}")`);
1217
1538
  scope ??= { kind: "owner", column: DEFAULT_OWNER_COLUMN };
1218
- if (columns.some((column) => column.name === SHARED_AUTHOR_COLUMN || column.renamedFrom === SHARED_AUTHOR_COLUMN) || removedColumns.includes(SHARED_AUTHOR_COLUMN) || scope.kind === "owner" && scope.column === SHARED_AUTHOR_COLUMN) {
1539
+ const managedOwner = scopeOwner(scope);
1540
+ if (columns.some((column) => column.name === SHARED_AUTHOR_COLUMN || column.renamedFrom === SHARED_AUTHOR_COLUMN) || removedColumns.includes(SHARED_AUTHOR_COLUMN) || managedOwner?.column === SHARED_AUTHOR_COLUMN) {
1219
1541
  throw new SchemaTsError(`line ${nameLine}: "${SHARED_AUTHOR_COLUMN}" is platform-maintained row authorship and cannot be declared, removed, renamed, or used as a custom owner column.`);
1220
1542
  }
1221
- if (scope.kind === "owner") {
1222
- const clash = columns.find((c) => c.name === scope.column);
1543
+ if (managedOwner) {
1544
+ const clash = columns.find((c) => c.name === managedOwner.column);
1223
1545
  if (clash) {
1224
1546
  throw new SchemaTsError(
1225
- `line ${nameLine}: table "${rawName}" declares the column "${scope.column}", but that is its owner column, which the platform creates and manages. Remove it from the columns \u2014 or pick a different owner column with owner({ column: '\u2026' }).`
1547
+ `line ${nameLine}: table "${rawName}" declares the column "${managedOwner.column}", but that is its owner column, which the platform creates and manages. Remove it from the columns \u2014 or pick a different owner column with owner({ column: '\u2026' }).`
1226
1548
  );
1227
1549
  }
1228
- if (removedColumns.includes(scope.column)) {
1550
+ if (removedColumns.includes(managedOwner.column)) {
1229
1551
  throw new SchemaTsError(
1230
- `line ${nameLine}: table "${rawName}" marks its owner column "${scope.column}" as removed(). The owner column carries the table's per-user scope and cannot be removed while the scope is owner().`
1552
+ `line ${nameLine}: table "${rawName}" marks its owner column "${managedOwner.column}" as removed(). The owner column carries the table's private scope and cannot be removed while owner() is part of that scope.`
1231
1553
  );
1232
1554
  }
1233
1555
  }
@@ -1240,7 +1562,7 @@ function readTable(r, rawName, nameLine) {
1240
1562
  `line ${nameLine}: "${rawName}"."${c.name}" declares renamedFrom: '${c.renamedFrom}', but "${c.renamedFrom}" is still declared as a column. A rename replaces the old column \u2014 remove its declaration.`
1241
1563
  );
1242
1564
  }
1243
- if (scope.kind === "owner" && c.renamedFrom === scope.column) {
1565
+ if (managedOwner && c.renamedFrom === managedOwner.column) {
1244
1566
  throw new SchemaTsError(
1245
1567
  `line ${nameLine}: "${rawName}"."${c.name}" declares renamedFrom: '${c.renamedFrom}', but that is the table's owner column, which the platform creates and manages \u2014 it cannot be renamed into an ordinary column.`
1246
1568
  );
@@ -1258,7 +1580,7 @@ function readTable(r, rawName, nameLine) {
1258
1580
  renameSources.add(c.renamedFrom);
1259
1581
  }
1260
1582
  const known = new Set(columns.map((c) => c.name));
1261
- if (scope.kind === "owner") known.add(scope.column);
1583
+ if (managedOwner) known.add(managedOwner.column);
1262
1584
  const indexes = [];
1263
1585
  for (const entry of indexEntries ?? []) {
1264
1586
  for (const colName of entry.columns) {
@@ -1291,7 +1613,12 @@ function readTable(r, rawName, nameLine) {
1291
1613
  if (columns.filter((column) => column.helper === "id").length !== 1) {
1292
1614
  throw new SchemaTsError(`line ${nameLine}: table "${rawName}": client operations require exactly one id() column.`);
1293
1615
  }
1294
- const parsed = parseClientPermissions(clientInput, columns, scope);
1616
+ const policyMember = scope.kind === "policy" && scope.policy.kind === "any_of" ? scope.policy.policies.find((node) => node.kind === "member") : void 0;
1617
+ const parsed = parseClientPermissions(clientInput, columns, {
1618
+ ...scope,
1619
+ ...managedOwner ? { column: managedOwner.column } : {},
1620
+ ...policyMember ? { group: policyMember.group } : {}
1621
+ });
1295
1622
  if (!parsed.ok) throw new SchemaTsError(`line ${nameLine}: table "${rawName}": ${parsed.message}`);
1296
1623
  const key = columns.find((column) => column.helper === "id").name;
1297
1624
  if (Array.isArray(parsed.permissions.read) && !parsed.permissions.read.includes(key)) {
@@ -1394,42 +1721,84 @@ function extractSchemaTs(source) {
1394
1721
  const tableByName = new Map(tables.map((t) => [t.name, t]));
1395
1722
  const knownColumns = (t) => {
1396
1723
  const s = new Set(t.columns.map((c) => c.name));
1397
- if (t.scope.kind === "owner") s.add(t.scope.column);
1724
+ const owner = scopeOwner(t.scope);
1725
+ if (owner) s.add(owner.column);
1398
1726
  return s;
1399
1727
  };
1728
+ const privatePolicy = (scope) => {
1729
+ if (scope.kind === "owner" && !scope.visitors) return { kind: "owner", column: scope.column };
1730
+ if (scope.kind === "member") return scope;
1731
+ if (scope.kind === "policy" && scope.policy.kind !== "parent") return scope.policy;
1732
+ return null;
1733
+ };
1400
1734
  for (const t of tables) {
1401
- if (t.scope.kind !== "member") continue;
1402
- const sc = t.scope;
1403
- const own = knownColumns(t);
1404
- for (const g of sc.group) {
1405
- if (!own.has(g)) {
1406
- errors.push(
1407
- `Table "${t.name}" is member-scoped on "${g}", but "${g}" is not one of its declared columns. The group-key columns must be real columns on the table.`
1408
- );
1409
- }
1735
+ if (t.scope.kind !== "policy" || t.scope.policy.kind !== "parent") continue;
1736
+ const pending = t.scope.policy;
1737
+ const fk = t.columns.find((column) => column.name === pending.via);
1738
+ if (!fk || !fk.references) {
1739
+ errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but "${pending.via}" is not a declared scalar foreign key.`);
1740
+ continue;
1410
1741
  }
1411
- const membershipTable = tableByName.get(sc.membership);
1412
- if (!membershipTable) {
1413
- errors.push(
1414
- `Table "${t.name}" is member-scoped through the membership table "${sc.membership}", which is not declared in db/schema.ts. The membership table must be a managed table in the same schema.`
1415
- );
1742
+ const parentTable = tableByName.get(fk.references);
1743
+ const parentKey = parentTable?.columns.find((column) => column.helper === "id");
1744
+ const inherited = parentTable ? privatePolicy(parentTable.scope) : null;
1745
+ if (!parentTable || !parentKey || !inherited) {
1746
+ errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but its referenced parent must have one id() key and a direct owner(), member(), or anyOf(owner(), member()) private scope.`);
1416
1747
  continue;
1417
1748
  }
1418
- const membershipCols = knownColumns(membershipTable);
1419
- if (membershipTable.client && (membershipTable.client.create !== null || membershipTable.client.update !== null || membershipTable.client.delete)) {
1420
- errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes. Change membership through a server function.`);
1749
+ const compatible = fk.helper === parentKey.helper && (fk.helper !== "id" || fk.uuid === parentKey.uuid) || fk.helper === "integer" && parentKey.helper === "id" && !parentKey.uuid || fk.helper === "text" && parentKey.helper === "id" && parentKey.uuid;
1750
+ if (!compatible) {
1751
+ errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but that foreign key does not have the same scalar type as "${parentTable.name}"."${parentKey.name}".`);
1752
+ continue;
1421
1753
  }
1422
- if (!membershipCols.has(sc.memberUser)) {
1423
- errors.push(
1424
- `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
1425
- );
1754
+ t.scope.policy = { kind: "parent", via: pending.via, table: parentTable.name, parentKey: parentKey.name, policy: inherited };
1755
+ if (t.client?.create && !t.client.create.includes(pending.via)) {
1756
+ errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }) and client.create must include "${pending.via}" so every new row explicitly selects an authorized parent.`);
1426
1757
  }
1427
- for (const mg of sc.memberGroup) {
1428
- if (!membershipCols.has(mg)) {
1758
+ }
1759
+ const memberPolicies = (scope) => {
1760
+ const out = [];
1761
+ const visit = (node) => {
1762
+ if (node.kind === "member") out.push(node);
1763
+ else if (node.kind === "any_of") node.policies.forEach(visit);
1764
+ };
1765
+ if (scope.kind === "member") out.push(scope);
1766
+ else if (scope.kind === "policy") visit(scope.policy);
1767
+ return out;
1768
+ };
1769
+ for (const t of tables) {
1770
+ for (const sc of memberPolicies(t.scope)) {
1771
+ const own = knownColumns(t);
1772
+ for (const g of sc.group) {
1773
+ if (!own.has(g)) {
1774
+ errors.push(
1775
+ `Table "${t.name}" is member-scoped on "${g}", but "${g}" is not one of its declared columns. The group-key columns must be real columns on the table.`
1776
+ );
1777
+ }
1778
+ }
1779
+ const membershipTable = tableByName.get(sc.membership);
1780
+ if (!membershipTable) {
1781
+ errors.push(
1782
+ `Table "${t.name}" is member-scoped through the membership table "${sc.membership}", which is not declared in db/schema.ts. The membership table must be a managed table in the same schema.`
1783
+ );
1784
+ continue;
1785
+ }
1786
+ const membershipCols = knownColumns(membershipTable);
1787
+ if (membershipTable.client && (membershipTable.client.create !== null || membershipTable.client.update !== null || membershipTable.client.delete)) {
1788
+ errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes. Change membership through a server function.`);
1789
+ }
1790
+ if (!membershipCols.has(sc.memberUser)) {
1429
1791
  errors.push(
1430
- `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
1792
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
1431
1793
  );
1432
1794
  }
1795
+ for (const mg of sc.memberGroup) {
1796
+ if (!membershipCols.has(mg)) {
1797
+ errors.push(
1798
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
1799
+ );
1800
+ }
1801
+ }
1433
1802
  }
1434
1803
  }
1435
1804
  const columnByName = (t, col) => t.columns.find((c) => c.name === col);
@@ -1549,8 +1918,9 @@ function schemaAuthorityFromSource(files) {
1549
1918
  const baked = bakedTableSchemaFromDeclared(JSON.stringify(canonicalTableShape(table)));
1550
1919
  if (!baked) throw new Error(`Could not derive the client contract for declared table "${table.name}".`);
1551
1920
  declareEntry(schema, table.name, baked);
1552
- declareEntry(intents, table.name, table.scope.kind === "owner" ? "scoped" : table.scope.kind);
1553
- if (table.scope.kind === "owner") declareEntry(scopes, table.name, table.scope.column);
1921
+ declareEntry(intents, table.name, declaredScopeIntent(table.scope));
1922
+ const ownerColumn = declaredOwnerColumn(table.scope);
1923
+ if (declaredScopeIntent(table.scope) === "scoped" && ownerColumn) declareEntry(scopes, table.name, ownerColumn);
1554
1924
  }
1555
1925
  return { schema, intents, scopes };
1556
1926
  }