@somewhere-tech/cli 0.32.5 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@somewhere-tech/cli",
3
- "version": "0.32.5",
3
+ "version": "0.33.0",
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.0",
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.0",
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": "7a2c20ddddd912ae513f3fff2614829b18c3e19954f9c48314ca96f1ce3143a1",
5
+ "worker/containers/compile/runtime-types.cjs": "a458232f605ac60202b58c6cae725a813d22479498f436668b8ad553678576ef",
6
+ "worker/containers/compile/schema-types.cjs": "c16fa8cabc8f5cff50777623f94f8cb009fee3ac47d53b471894be9c698d614a",
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": "98c242359240b8918c1b71af73f17b6b4200559a896f47f6032ef771f543201f",
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": "b092afe7c271405126da9e204084fc5528a42025b5583eba25da3907f995a25b",
12
12
  "worker/src/utils/db-schema-deploy/schema-ownership.ts": "e6c811a87bc657707be3518dfe547c6d97718ac073108b126eb7f791c9c08202"
13
13
  },
14
- "sha256": "8b2ee9f02434710bec063e10044f5f60cf4443343db94cc14a7d479d6bf77bea"
14
+ "sha256": "80171af94d110f5caf717eb7ad6abd95b15ed4125af1cc848151561d67462b8d"
15
15
  }
@@ -48,12 +48,41 @@ 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 } = value.m;
58
+ if (Object.keys(value.m).sort().join(",") !== "g,m,mg,u" || !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))) fail(label);
59
+ return { k: "m", m: { g: [...g], m, u, mg: [...mg] } };
60
+ }
61
+ if (value.k === "a" && keys === "k,p" && Array.isArray(value.p) && value.p.length === 2) {
62
+ const left = policy(value.p[0], label), right = policy(value.p[1], label);
63
+ if (left.k !== "o" || right.k !== "m") fail(label);
64
+ return { k: "a", p: [left, right] };
65
+ }
66
+ if (value.k === "p" && keys === "k,p,pk,t,v" && [value.v, value.t, value.pk].every((item) => typeof item === "string" && identifier.test(item))) {
67
+ const inherited = policy(value.p, label);
68
+ if (!["o", "m", "a"].includes(inherited.k)) fail(label);
69
+ return { k: "p", v: value.v, t: value.t, pk: value.pk, p: inherited };
70
+ }
71
+ fail(label);
72
+ }
73
+ function policyMembershipTables(value, out) {
74
+ if (!value || typeof value !== "object") return;
75
+ if (value.k === "m" && value.m) out.add(value.m.m);
76
+ else if (value.k === "a") value.p.forEach((item) => policyMembershipTables(item, out));
77
+ else if (value.k === "p") policyMembershipTables(value.p, out);
78
+ }
51
79
  function canonicalize2(schema, intents, scopes) {
52
80
  record(schema, "schema");
53
81
  record(intents, "intents");
54
82
  record(scopes, "scopes");
55
83
  const tables = [];
56
84
  const membershipTables = new Set(Object.values(schema).filter((table) => table && table.member).map((table) => table.member.m));
85
+ for (const table of Object.values(schema)) if (table?.policy) policyMembershipTables(table.policy, membershipTables);
57
86
  for (const name of Object.keys(schema).sort()) {
58
87
  const table = schema[name];
59
88
  record(table, `table ${name}`);
@@ -78,14 +107,30 @@ var require_declared_data_contract = __commonJS({
78
107
  if (Object.keys(input).some((key) => !["read", "publicRead", "create", "update", "delete", "identity"].includes(key))) fail(`${name} permissions`);
79
108
  const read = typeof input.read === "boolean" ? input.read : fields(input.read, seen, `${name}.read`);
80
109
  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`);
110
+ let publicRead = input.publicRead;
111
+ if (typeof publicRead !== "boolean") {
112
+ record(publicRead, `${name} public read`);
113
+ if (Object.keys(publicRead).some((key) => key !== "where")) fail(`${name} public read`);
114
+ record(publicRead.where, `${name} public read predicate`);
115
+ const entries = Object.entries(publicRead.where);
116
+ if (!entries.length || entries.length > 8) fail(`${name} public read predicate`);
117
+ const where = {};
118
+ for (const [field, value] of entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
119
+ const column = columns.find((column2) => column2.n === field);
120
+ 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`);
121
+ 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`);
122
+ where[field] = value;
123
+ }
124
+ publicRead = { where };
125
+ }
126
+ if (typeof input.delete !== "boolean" || !["authenticated", "visitor"].includes(input.identity)) fail(`${name} permissions`);
82
127
  const create = input.create === null ? null : fields(input.create, seen, `${name}.create`, true);
83
128
  const update = input.update === null ? null : fields(input.update, seen, `${name}.update`);
84
129
  if (membershipTables.has(name) && (create !== null || update !== null || input.delete)) fail(`${name} membership authority writes`);
85
130
  if ([...create || [], ...update || []].some((field) => columns.find((column) => column.n === field).t === "blob")) fail(`${name} blob browser writes require a server function`);
86
131
  const intent = intents[name];
87
- if (!["scoped", "shared", "member"].includes(intent)) fail(`${name} intent`);
88
- const owner = intent === "scoped" ? scopes[name] : null;
132
+ if (!["scoped", "shared", "member", "policy"].includes(intent)) fail(`${name} intent`);
133
+ let owner = intent === "scoped" ? scopes[name] || null : null;
89
134
  if (intent === "scoped" && (typeof owner !== "string" || !identifier.test(owner))) fail(`${name} owner`);
90
135
  if (table.visitors !== void 0 && table.visitors !== true) fail(`${name} visitors`);
91
136
  const visitors = table.visitors === true;
@@ -95,7 +140,8 @@ var require_declared_data_contract = __commonJS({
95
140
  if (intent === "shared" && (author !== "_sw_author_id" || !columns.some((column) => column.n === author && column.t === "text" && column.nul === 1))) fail(`${name} author`);
96
141
  if (Array.isArray(read) && read.includes(author)) fail(`${name} internal author projection`);
97
142
  if (input.identity === "visitor" && intent !== "scoped") fail(`${name} visitor identity`);
98
- if (input.publicRead && !Array.isArray(read)) fail(`${name} public read`);
143
+ if (publicRead && !Array.isArray(read)) fail(`${name} public read`);
144
+ if (typeof publicRead === "object" && [...create || [], ...update || []].some((field) => Object.prototype.hasOwnProperty.call(publicRead.where, field))) fail(`${name} publication writes require a server function`);
99
145
  if ([...create || [], ...update || []].some((field) => field === pk || field === owner || field === author)) fail(`${name} identity writes`);
100
146
  let member = null;
101
147
  if (intent === "member") {
@@ -107,7 +153,49 @@ var require_declared_data_contract = __commonJS({
107
153
  if ((update || []).some((field) => g.includes(field))) fail(`${name} membership writes`);
108
154
  member = { g: [...g], m, u, mg: [...mg] };
109
155
  }
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 });
156
+ const normalizedPolicy = intent === "policy" ? policy(table.policy, `${name}.policy`) : null;
157
+ if (normalizedPolicy && !["a", "p"].includes(normalizedPolicy.k)) fail(`${name}.policy root`);
158
+ if (normalizedPolicy?.k === "a") {
159
+ owner = normalizedPolicy.p[0].c;
160
+ if ([...create || [], ...update || []].includes(owner) || (update || []).some((field) => normalizedPolicy.p[1].m.g.includes(field))) fail(`${name}.policy identity writes`);
161
+ }
162
+ if (normalizedPolicy?.k === "p" && create !== null && !create.includes(normalizedPolicy.v)) fail(`${name}.policy parent create`);
163
+ tables.push({
164
+ name,
165
+ columns,
166
+ client: { identity: input.identity, read, publicRead, create, update, delete: input.delete },
167
+ primaryKey: pk,
168
+ intent,
169
+ owner,
170
+ author,
171
+ visitors,
172
+ member,
173
+ ...normalizedPolicy ? { policy: normalizedPolicy } : {}
174
+ });
175
+ }
176
+ const tableByName = new Map(tables.map((table) => [table.name, table]));
177
+ for (const table of tables) {
178
+ const declared = schema[table.name].relations;
179
+ if (declared === void 0) continue;
180
+ if (!Array.isArray(declared)) fail(`${table.name}.relations`);
181
+ const relations = [];
182
+ const names = /* @__PURE__ */ new Set();
183
+ for (const relation of declared) {
184
+ record(relation, `${table.name}.relation`);
185
+ if (Object.keys(relation).some((key) => !["name", "table", "fk", "parentKey"].includes(key))) fail(`${table.name}.relation`);
186
+ const { name, table: childName, fk, parentKey } = relation;
187
+ if (![name, childName, fk, parentKey].every((value) => typeof value === "string" && identifier.test(value)) || names.has(name)) {
188
+ fail(`${table.name}.relation`);
189
+ }
190
+ names.add(name);
191
+ const declaredChild = schema[childName];
192
+ if (!declaredChild || !Array.isArray(declaredChild.columns) || parentKey !== table.primaryKey || !declaredChild.columns.some((column) => column && column.n === fk)) fail(`${table.name}.relation`);
193
+ const child = tableByName.get(childName);
194
+ if (table.client.read !== false && child && child.client.read !== false) {
195
+ relations.push({ name, table: childName, fk, parentKey });
196
+ }
197
+ }
198
+ if (relations.length) table.relations = relations.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
111
199
  }
112
200
  return JSON.stringify({ version: 1, tables });
113
201
  }
@@ -115,10 +203,10 @@ var require_declared_data_contract = __commonJS({
115
203
  const type = { integer: "number | string", number: "number", text: "string", timestamp: "string", boolean: "boolean", json: "Json", blob: "number[]" }[column.t];
116
204
  return column.nul ? `${type} | null` : type;
117
205
  }
118
- function shape(columns, names, mode) {
206
+ function shape(columns, names, mode, required = /* @__PURE__ */ new Set()) {
119
207
  const values = names.map((name) => {
120
208
  const column = columns.find((candidate) => candidate.n === name);
121
- const optional = mode === "update" || mode === "create" && (column.d || column.nul);
209
+ const optional = mode === "update" || mode === "create" && !required.has(name) && (column.d || column.nul);
122
210
  return `${JSON.stringify(name)}${optional ? "?" : ""}: ${columnType(column)}`;
123
211
  });
124
212
  return values.length ? `{ ${values.join("; ")} }` : "Record<string, never>";
@@ -143,7 +231,8 @@ var require_declared_data_contract = __commonJS({
143
231
  }
144
232
  const mutation = client.read === false ? "{ count: number; changes: number }" : `{ data: ${row} | null; count: number; changes: number }`;
145
233
  if (client.create !== null) {
146
- operations.push(`create(values: ${shape(columns, client.create, "create")}): Promise<${mutation}>`);
234
+ const required = table.policy?.k === "p" ? /* @__PURE__ */ new Set([table.policy.v]) : /* @__PURE__ */ new Set();
235
+ operations.push(`create(values: ${shape(columns, client.create, "create", required)}): Promise<${mutation}>`);
147
236
  methods.push("create");
148
237
  }
149
238
  if (client.update !== null) {
@@ -154,8 +243,21 @@ var require_declared_data_contract = __commonJS({
154
243
  operations.push(`delete(id: ${id}): Promise<${mutation}>`);
155
244
  methods.push("delete");
156
245
  }
246
+ const relationDeclarations = [];
247
+ const relationNames = [];
248
+ for (const relation of table.relations || []) {
249
+ const child = tables.find((candidate) => candidate.name === relation.table);
250
+ const childReadable = child.client.read === true ? child.columns.filter((column) => column.n !== child.author).map((column) => column.n) : child.client.read;
251
+ const childRow = shape(child.columns, childReadable, "read");
252
+ const childId = columnType(child.columns.find((column) => column.n === child.primaryKey));
253
+ const parentId = columnType(columns.find((column) => column.n === primaryKey));
254
+ const equality = childReadable.filter((field) => !["json", "blob"].includes(child.columns.find((column) => column.n === field).t));
255
+ 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 }> }`);
256
+ relationNames.push(relation.name);
257
+ }
258
+ if (relationDeclarations.length) operations.push(`relations: { ${relationDeclarations.join("; ")} }`);
157
259
  declarations.push(`${JSON.stringify(name)}: { ${operations.join("; ")} }`);
158
- runtimeTables.push([name, methods]);
260
+ runtimeTables.push([name, methods, relationNames]);
159
261
  }
160
262
  const declaration = `declare module ${JSON.stringify("somewhere:data")} {
161
263
  export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
@@ -166,7 +268,7 @@ var require_declared_data_contract = __commonJS({
166
268
  const runtime = `const contract=${JSON.stringify(contract_digest)};
167
269
  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
270
  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})])))])));
271
+ 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
272
  export{data,DataError};
171
273
  `;
172
274
  return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration } };
@@ -216,12 +318,13 @@ var require_schema_types = __commonJS({
216
318
  interface Relation { readonly __somewhereRelation: unique symbol }
217
319
  interface Scope { readonly __somewhereScope: unique symbol }
218
320
  interface OwnerScope extends Scope { readonly __somewhereOwner: unique symbol }
321
+ interface MemberScope extends Scope { readonly __somewhereMember: unique symbol }
219
322
  interface ColumnOptions<Default> {
220
323
  nullable?: boolean; default?: Default | null; unique?: boolean;
221
324
  references?: string; onDelete?: 'cascade' | 'restrict'; renamedFrom?: string;
222
325
  }
223
326
  interface ClientPermissions<Field extends string> {
224
- identity?: 'authenticated' | 'visitor'; read?: boolean | Field[]; publicRead?: boolean;
327
+ identity?: 'authenticated' | 'visitor'; read?: boolean | Field[]; publicRead?: boolean | { where: Partial<Record<Field, string | number | boolean | null>> };
225
328
  create?: Field[] | false | null; update?: Field[] | false | null; delete?: boolean;
226
329
  }
227
330
  interface TableOptions<Field extends string> {
@@ -243,14 +346,18 @@ function blob(options?: SomewhereSchemaDeclaration.ColumnOptions<never>): Somewh
243
346
  function owner(options?: { column?: string; visitors?: boolean }): SomewhereSchemaDeclaration.OwnerScope;
244
347
  function shared(): SomewhereSchemaDeclaration.Scope;
245
348
  function serverOnly(): SomewhereSchemaDeclaration.Scope;
246
- function member(options: { group: string | string[]; membership: string; member_user: string; member_group: string | string[] }): SomewhereSchemaDeclaration.Scope;
349
+ function member(options: { group: string | string[]; membership: string; member_user: string; member_group: string | string[] }): SomewhereSchemaDeclaration.MemberScope;
247
350
  function hasMany(table: string, foreignKey: string): SomewhereSchemaDeclaration.Relation;
248
351
  function removed(): SomewhereSchemaDeclaration.ColumnMarker;
249
352
  function removedTable(): SomewhereSchemaDeclaration.TableMarker;
250
353
  function exported(): SomewhereSchemaDeclaration.TableMarker;
354
+ `;
355
+ var policySignatures = `function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, member: SomewhereSchemaDeclaration.MemberScope): SomewhereSchemaDeclaration.Scope;
356
+ function anyOf(member: SomewhereSchemaDeclaration.MemberScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.Scope;
357
+ function parent(options: { via: string }): SomewhereSchemaDeclaration.Scope;
251
358
  `;
252
359
  var SCHEMA_DECLARATION2 = types + signatures.replace(/^function /gm, "declare function ") + `declare module 'somewhere/db' {
253
- ${signatures.replace(/^function /gm, " export function ")}}
360
+ ${(signatures + policySignatures).replace(/^function /gm, " export function ")}}
254
361
  `;
255
362
  module2.exports = { SCHEMA_DECLARATION: SCHEMA_DECLARATION2 };
256
363
  }
@@ -276,7 +383,10 @@ type __SomewhereDbOperator = {
276
383
  & Partial<Record<Exclude<keyof __SomewhereDbOperators, K>, never>>
277
384
  }[keyof __SomewhereDbOperators];
278
385
  type SomewhereDbCondition = SomewhereDbScalar | __SomewhereDbOperator;
279
- type SomewhereDbWhere = Readonly<Record<string, SomewhereDbCondition | readonly SomewhereDbCondition[]>>;
386
+ type SomewhereDbOrBranch = Readonly<Record<string, SomewhereDbCondition>>;
387
+ type SomewhereDbWhere = Readonly<{
388
+ $or?: readonly SomewhereDbOrBranch[];
389
+ } & Record<string, SomewhereDbCondition | readonly SomewhereDbCondition[] | readonly SomewhereDbOrBranch[] | undefined>>;
280
390
  type SomewhereDbOrder = string | readonly [string, ('asc' | 'desc')?]
281
391
  | readonly (readonly [string, ('asc' | 'desc')?])[];
282
392
  interface SomewhereDbReadOptions {
@@ -291,14 +401,17 @@ interface SomewhereDbReadOptions {
291
401
  interface SomewhereDbCountOptions { where?: SomewhereDbWhere | null }
292
402
  interface SomewhereDbInsertOptions { onConflict?: 'ignore' | 'update' | null }
293
403
  type SomewhereDbValues = Readonly<Record<string, __SomewhereJson>>;
294
- interface SomewhereDbUpdate {
295
- set: SomewhereDbValues;
404
+ type SomewhereDbIncrement = Readonly<Record<string, number>>;
405
+ type SomewhereDbUpdate = {
296
406
  where?: SomewhereDbWhere | null;
297
- }
407
+ } & (
408
+ | { set: SomewhereDbValues; increment?: SomewhereDbIncrement | null }
409
+ | { set?: never; increment: SomewhereDbIncrement }
410
+ );
298
411
  interface SomewhereDbRemove { where?: SomewhereDbWhere | null }
299
412
  type SomewhereDbWriteIntent =
300
413
  | { op: 'insert'; table: string; values: SomewhereDbValues; options?: SomewhereDbInsertOptions | null }
301
- | { op: 'update'; table: string; set: SomewhereDbValues; where?: SomewhereDbWhere | null }
414
+ | ({ op: 'update'; table: string } & SomewhereDbUpdate)
302
415
  | { op: 'remove'; table: string; where?: SomewhereDbWhere | null };
303
416
  interface SomewhereDbResult {
304
417
  // Composed results preserve the database representation. They do not use
@@ -364,7 +477,7 @@ function parseClientPermissions(value, columns, scope) {
364
477
  return fail(`Unknown client permission "${key}".`);
365
478
  }
366
479
  }
367
- if (!["owner", "member", "shared"].includes(scope.kind)) {
480
+ if (!["owner", "member", "shared", "policy"].includes(scope.kind)) {
368
481
  return fail("serverOnly() tables cannot declare client access. Use a server function for business logic.");
369
482
  }
370
483
  const identity = scope.kind === "owner" && scope.visitors === true ? "visitor" : "authenticated";
@@ -375,8 +488,33 @@ function parseClientPermissions(value, columns, scope) {
375
488
  return fail("client.identity must match the owner() declaration. Visitor access requires owner({ visitors: true }); owner() and other scopes require authenticated identity.");
376
489
  }
377
490
  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.");
491
+ const rawPublicRead = input.publicRead ?? false;
492
+ let publicRead;
493
+ if (typeof rawPublicRead === "boolean") publicRead = rawPublicRead;
494
+ else {
495
+ if (!rawPublicRead || typeof rawPublicRead !== "object" || Array.isArray(rawPublicRead) || Object.keys(rawPublicRead).some((key) => key !== "where")) {
496
+ return fail("client.publicRead must be true, false, or { where: { field: literal } }.");
497
+ }
498
+ const where = rawPublicRead.where;
499
+ if (!where || typeof where !== "object" || Array.isArray(where)) return fail("client.publicRead.where must be a non-empty object.");
500
+ const entries = Object.entries(where);
501
+ if (entries.length === 0 || entries.length > 8) return fail("client.publicRead.where must declare 1 to 8 equality fields.");
502
+ const normalized = {};
503
+ for (const [rawName, literal] of entries) {
504
+ const name = rawName.toLowerCase();
505
+ const column = columns.find((candidate) => candidate.name === name);
506
+ if (Object.prototype.hasOwnProperty.call(normalized, name)) return fail(`client.publicRead.where declares field "${rawName}" twice.`);
507
+ if (!column || ["json", "blob"].includes(column.helper)) return fail(`client.publicRead.where names undeclared or non-scalar field "${rawName}".`);
508
+ 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.`);
509
+ if (typeof literal === "string" && literal.length > 256) return fail(`client.publicRead.where field "${rawName}" exceeds 256 characters.`);
510
+ if (literal === null && !column.nullable) return fail(`client.publicRead.where field "${rawName}" cannot equal null because it is required.`);
511
+ 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")) {
512
+ return fail(`client.publicRead.where field "${rawName}" must match its declared type.`);
513
+ }
514
+ normalized[name] = literal;
515
+ }
516
+ publicRead = { where: Object.fromEntries(Object.entries(normalized).sort(([a], [b]) => a.localeCompare(b))) };
517
+ }
380
518
  if (read !== true && read !== false && !Array.isArray(read)) return fail("client.read must list readable fields, or be true or false.");
381
519
  if (publicRead && (!Array.isArray(read) || read.length === 0)) {
382
520
  return fail("client.publicRead requires an explicit non-empty client.read field list.");
@@ -406,11 +544,14 @@ function parseClientPermissions(value, columns, scope) {
406
544
  const name = entry.toLowerCase();
407
545
  const column = fields.get(name);
408
546
  if (!column) return fail(`client.${operation} names undeclared or platform-managed field "${entry}".`);
409
- if (column.helper === "id" || scope.kind === "owner" && name === scope.column) {
547
+ if (column.helper === "id" || (scope.kind === "owner" || scope.kind === "policy") && name === scope.column) {
410
548
  return fail(`client.${operation} cannot write the platform-assigned identity field "${entry}".`);
411
549
  }
412
550
  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)) {
551
+ if (typeof publicRead === "object" && Object.prototype.hasOwnProperty.call(publicRead.where, name)) {
552
+ return fail(`client.${operation} cannot write publication field "${entry}". Use a server operation to publish rows.`);
553
+ }
554
+ if (operation === "update" && (scope.kind === "member" || scope.kind === "policy") && scope.group?.includes(name)) {
414
555
  return fail(`client.update cannot move a row between membership groups by changing "${entry}".`);
415
556
  }
416
557
  if (allowed.includes(name)) return fail(`client.${operation} declares field "${entry}" twice.`);
@@ -420,6 +561,18 @@ function parseClientPermissions(value, columns, scope) {
420
561
  writes[operation] = allowed.sort();
421
562
  }
422
563
  if (writes.create !== null) {
564
+ if (typeof publicRead === "object") {
565
+ let guaranteedPrivate = false;
566
+ for (const [name, publishedValue] of Object.entries(publicRead.where)) {
567
+ const column = fields.get(name);
568
+ if (column.autoNow) continue;
569
+ const defaultValue = column.hasDefault ? column.default : column.nullable ? null : void 0;
570
+ if (defaultValue !== void 0 && defaultValue !== publishedValue) guaranteedPrivate = true;
571
+ }
572
+ if (!guaranteedPrivate) {
573
+ 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.");
574
+ }
575
+ }
423
576
  for (const column of columns) {
424
577
  if (column.helper !== "id" && !column.nullable && !column.hasDefault && !column.autoNow && !writes.create.includes(column.name)) {
425
578
  return fail(`client.create must include required field "${column.name}", or that field needs a declared default.`);
@@ -463,7 +616,14 @@ function bakedTableSchemaFromDeclared(declaredJson) {
463
616
  if (col.nullable === true) entry.nul = 1;
464
617
  if (implicitDefault || col.hasDefault === true || col.autoNow === true) entry.d = 1;
465
618
  out.push(entry);
466
- permissionColumns.push({ name: entry.n, helper: col.helper, nullable: col.nullable === true, hasDefault: col.hasDefault === true, autoNow: col.autoNow === true });
619
+ permissionColumns.push({
620
+ name: entry.n,
621
+ helper: col.helper,
622
+ nullable: col.nullable === true,
623
+ hasDefault: col.hasDefault === true,
624
+ autoNow: col.autoNow === true,
625
+ ...col.default === null || ["string", "number", "boolean"].includes(typeof col.default) ? { default: col.default } : {}
626
+ });
467
627
  if (col.helper === "id") primaryKeys.push(entry.n);
468
628
  }
469
629
  const relations = bakedRelationsFromDeclared(shape.relations);
@@ -479,11 +639,13 @@ function bakedTableSchemaFromDeclared(declaredJson) {
479
639
  if (!scope || typeof scope !== "object" || primaryKeys.length !== 1) return null;
480
640
  const sc = scope;
481
641
  if (typeof sc.kind !== "string") return null;
642
+ const policyIdentity = sc.kind === "policy" ? declaredPolicyIdentity(sc.policy) : {};
482
643
  const parsed = parseClientPermissions(shape.client, permissionColumns, {
483
644
  kind: sc.kind,
484
645
  ...visitors ? { visitors: true } : {},
485
646
  ...typeof sc.column === "string" ? { column: sc.column } : {},
486
- ...Array.isArray(sc.group) && sc.group.every((value) => typeof value === "string") ? { group: sc.group } : {}
647
+ ...Array.isArray(sc.group) && sc.group.every((value) => typeof value === "string") ? { group: sc.group } : {},
648
+ ...policyIdentity
487
649
  });
488
650
  if (!parsed.ok) return null;
489
651
  if (Array.isArray(parsed.permissions.read) && !parsed.permissions.read.includes(primaryKeys[0])) return null;
@@ -494,10 +656,48 @@ function bakedTableSchemaFromDeclared(declaredJson) {
494
656
  if (!member) return null;
495
657
  return relations ? { columns: out, member, relations, ...browser } : { columns: out, member, ...browser };
496
658
  }
659
+ if (scope !== null && typeof scope === "object" && scope.kind === "policy") {
660
+ const policy = bakedPolicyFromDeclared(scope.policy);
661
+ if (!policy || policy.k !== "a" && policy.k !== "p") return null;
662
+ return relations ? { columns: out, policy, relations, ...browser } : { columns: out, policy, ...browser };
663
+ }
497
664
  const author = scope !== null && typeof scope === "object" && scope.kind === "shared" ? SHARED_AUTHOR_COLUMN : void 0;
498
665
  if (author) out.push({ n: author, t: "text", nul: 1 });
499
666
  return { columns: out, ...relations ? { relations } : {}, ...author ? { author } : {}, ...visitors ? { visitors } : {}, ...browser };
500
667
  }
668
+ function declaredPolicyIdentity(value) {
669
+ if (!value || typeof value !== "object") return {};
670
+ const node = value;
671
+ if (node.kind === "owner" && typeof node.column === "string") return { column: node.column };
672
+ if (node.kind === "member" && Array.isArray(node.group) && node.group.every((entry) => typeof entry === "string")) return { group: node.group };
673
+ if (node.kind === "any_of" && Array.isArray(node.policies)) {
674
+ return node.policies.reduce((all, child) => ({ ...all, ...declaredPolicyIdentity(child) }), {});
675
+ }
676
+ return {};
677
+ }
678
+ function bakedPolicyFromDeclared(value, depth = 0) {
679
+ if (depth > 2 || value === null || typeof value !== "object") return null;
680
+ const node = value;
681
+ if (node.kind === "owner") {
682
+ return typeof node.column === "string" && SAFE_SCOPE_IDENTIFIER.test(node.column) ? { k: "o", c: node.column.toLowerCase() } : null;
683
+ }
684
+ if (node.kind === "member") {
685
+ const member = bakedMemberFromDeclared(node);
686
+ return member ? { k: "m", m: member } : null;
687
+ }
688
+ if (node.kind === "any_of" && Array.isArray(node.policies) && node.policies.length === 2) {
689
+ if (depth > 1 || node.policies[0]?.kind !== "owner" || node.policies[1]?.kind !== "member") return null;
690
+ const left = bakedPolicyFromDeclared(node.policies[0], depth + 1);
691
+ const right = bakedPolicyFromDeclared(node.policies[1], depth + 1);
692
+ return left && right ? { k: "a", p: [left, right] } : null;
693
+ }
694
+ 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)) {
695
+ if (depth !== 0 || node.policy?.kind === "parent") return null;
696
+ const inherited = bakedPolicyFromDeclared(node.policy, depth + 1);
697
+ return inherited ? { k: "p", v: node.via.toLowerCase(), t: node.table.toLowerCase(), pk: node.parentKey.toLowerCase(), p: inherited } : null;
698
+ }
699
+ return null;
700
+ }
501
701
  function bakedRelationsFromDeclared(value) {
502
702
  if (!Array.isArray(value) || value.length === 0) return void 0;
503
703
  const out = [];
@@ -536,6 +736,20 @@ function bakedMemberFromDeclared(scope) {
536
736
  }
537
737
 
538
738
  // worker/src/utils/db-schema-deploy/extract-schema-ts.ts
739
+ function policyOwner(policy) {
740
+ if (policy.kind === "owner") return policy;
741
+ if (policy.kind === "any_of") return policy.policies.map(policyOwner).find((value) => value !== null) ?? null;
742
+ return null;
743
+ }
744
+ function scopeOwner(scope) {
745
+ return scope.kind === "owner" ? scope : scope.kind === "policy" ? policyOwner(scope.policy) : null;
746
+ }
747
+ function declaredOwnerColumn(scope) {
748
+ return scopeOwner(scope)?.column ?? null;
749
+ }
750
+ function declaredScopeIntent(scope) {
751
+ return scope.kind === "owner" ? "scoped" : scope.kind;
752
+ }
539
753
  var COLUMN_HELPERS = /* @__PURE__ */ new Set([
540
754
  "id",
541
755
  "text",
@@ -546,7 +760,7 @@ var COLUMN_HELPERS = /* @__PURE__ */ new Set([
546
760
  "json",
547
761
  "blob"
548
762
  ]);
549
- var SCOPE_HELPERS = /* @__PURE__ */ new Set(["owner", "shared", "serverOnly", "member"]);
763
+ var SCOPE_HELPERS = /* @__PURE__ */ new Set(["owner", "shared", "serverOnly", "member", "anyOf", "parent"]);
550
764
  var TABLE_MARKER_HELPERS = /* @__PURE__ */ new Set(["removedTable", "exported"]);
551
765
  var SAFE_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
552
766
  var RESERVED_PREFIXES = ["app_", "sqlite_", "d1_", "__sw_"];
@@ -738,7 +952,15 @@ function readClientPermissions(r) {
738
952
  const key = r.readKey("for a client permission");
739
953
  if (Object.prototype.hasOwnProperty.call(out, key.name)) r.fail(`client declares "${key.name}" twice.`);
740
954
  r.expectPunct(":", `after client permission "${key.name}"`);
741
- if (r.tryPunct("[")) {
955
+ if (key.name === "publicRead" && r.peek().kind === "punct" && r.peek().value === "{") {
956
+ r.expectPunct("{", "opening client.publicRead");
957
+ const whereKey = r.readKey("in client.publicRead");
958
+ if (whereKey.name !== "where") r.fail("client.publicRead only accepts a where object.");
959
+ r.expectPunct(":", "after client.publicRead.where");
960
+ out[key.name] = { where: Object.fromEntries([...r.readLiteralObject("for client.publicRead.where")].map(([name, item]) => [name, item.value])) };
961
+ r.tryPunct(",");
962
+ r.expectPunct("}", "closing client.publicRead");
963
+ } else if (r.tryPunct("[")) {
742
964
  const fields = [];
743
965
  while (!r.tryPunct("]")) {
744
966
  fields.push(r.readLiteral(`in client.${key.name}`));
@@ -995,6 +1217,65 @@ function readMemberScope(r, tableName, scopeLine) {
995
1217
  }
996
1218
  return { kind: "member", group, membership, memberUser, memberGroup };
997
1219
  }
1220
+ function readPolicyOwner(r, tableName, line) {
1221
+ r.expectPunct("(", 'after "owner"');
1222
+ let column = DEFAULT_OWNER_COLUMN;
1223
+ if (!r.tryPunct(")")) {
1224
+ const opts = r.readLiteralObject(`in owner() inside anyOf() for table "${tableName}"`);
1225
+ for (const [key, { value, line: optionLine }] of opts) {
1226
+ if (key !== "column") {
1227
+ throw new SchemaTsError(`line ${optionLine}: owner() inside anyOf() only allows column. Visitor ownership is not part of policy scope.`);
1228
+ }
1229
+ if (typeof value !== "string" || !SAFE_IDENT.test(value) || value.length > MAX_NAME_LENGTH) {
1230
+ throw new SchemaTsError(`line ${optionLine}: owner({ column }) on table "${tableName}" must be a valid column name.`);
1231
+ }
1232
+ column = value.toLowerCase();
1233
+ }
1234
+ r.tryPunct(",");
1235
+ r.expectPunct(")", "closing owner()");
1236
+ }
1237
+ return { kind: "owner", column };
1238
+ }
1239
+ function readPolicyMember(r, tableName, line) {
1240
+ r.expectPunct("(", 'after "member"');
1241
+ const scope = readMemberScope(r, tableName, line);
1242
+ r.tryPunct(",");
1243
+ r.expectPunct(")", "closing member()");
1244
+ if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
1245
+ return scope;
1246
+ }
1247
+ function readAnyOfScope(r, tableName, line) {
1248
+ r.expectPunct("(", 'after "anyOf"');
1249
+ const first = r.expectIdent(`for the first policy in anyOf() on table "${tableName}"`);
1250
+ const firstPolicy = first.value === "owner" ? readPolicyOwner(r, tableName, first.line) : first.value === "member" ? readPolicyMember(r, tableName, first.line) : null;
1251
+ if (!firstPolicy) throw new SchemaTsError(`line ${first.line}: anyOf() only accepts owner() and member().`);
1252
+ r.expectPunct(",", `between the two policies in anyOf() on table "${tableName}"`);
1253
+ const second = r.expectIdent(`for the second policy in anyOf() on table "${tableName}"`);
1254
+ const secondPolicy = second.value === "owner" ? readPolicyOwner(r, tableName, second.line) : second.value === "member" ? readPolicyMember(r, tableName, second.line) : null;
1255
+ if (!secondPolicy) throw new SchemaTsError(`line ${second.line}: anyOf() only accepts owner() and member().`);
1256
+ r.tryPunct(",");
1257
+ r.expectPunct(")", "closing anyOf()");
1258
+ if (firstPolicy.kind === secondPolicy.kind) {
1259
+ throw new SchemaTsError(`line ${line}: anyOf() requires exactly one owner() and one member() policy.`);
1260
+ }
1261
+ const owner = firstPolicy.kind === "owner" ? firstPolicy : secondPolicy;
1262
+ const member = firstPolicy.kind === "member" ? firstPolicy : secondPolicy;
1263
+ return { kind: "policy", policy: { kind: "any_of", policies: [owner, member] } };
1264
+ }
1265
+ function readParentScope(r, tableName, line) {
1266
+ r.expectPunct("(", 'after "parent"');
1267
+ const opts = r.readLiteralObject(`in parent() options for table "${tableName}"`);
1268
+ r.tryPunct(",");
1269
+ r.expectPunct(")", "closing parent()");
1270
+ if (opts.size !== 1 || !opts.has("via")) {
1271
+ throw new SchemaTsError(`line ${line}: parent() on table "${tableName}" needs exactly { via: 'foreign_key' }.`);
1272
+ }
1273
+ const value = opts.get("via").value;
1274
+ if (typeof value !== "string" || !SAFE_IDENT.test(value) || value.length > MAX_NAME_LENGTH) {
1275
+ throw new SchemaTsError(`line ${opts.get("via").line}: parent({ via }) on table "${tableName}" must name a valid declared foreign-key column.`);
1276
+ }
1277
+ return { kind: "policy", policy: { kind: "parent", via: value.toLowerCase(), table: "", parentKey: "", policy: { kind: "owner", column: "" } } };
1278
+ }
998
1279
  function readRelations(r, tableName) {
999
1280
  r.expectPunct("{", `for "relations" of table "${tableName}"`);
1000
1281
  const out = [];
@@ -1041,9 +1322,11 @@ function readScope(r, tableName) {
1041
1322
  const tok = r.expectIdent(`for the scope of table "${tableName}"`);
1042
1323
  if (!SCOPE_HELPERS.has(tok.value)) {
1043
1324
  throw new SchemaTsError(
1044
- `line ${tok.line}: unknown scope "${tok.value}" on table "${tableName}". Use owner(), member(), shared(), or serverOnly().`
1325
+ `line ${tok.line}: unknown scope "${tok.value}" on table "${tableName}". Use owner(), member(), anyOf(), parent(), shared(), or serverOnly().`
1045
1326
  );
1046
1327
  }
1328
+ if (tok.value === "anyOf") return readAnyOfScope(r, tableName, tok.line);
1329
+ if (tok.value === "parent") return readParentScope(r, tableName, tok.line);
1047
1330
  if (tok.value === "member") {
1048
1331
  r.expectPunct("(", 'after "member"');
1049
1332
  const scope = readMemberScope(r, tableName, tok.line);
@@ -1215,19 +1498,20 @@ function readTable(r, rawName, nameLine) {
1215
1498
  r.tryPunct(",");
1216
1499
  r.expectPunct(")", `closing table("${rawName}")`);
1217
1500
  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) {
1501
+ const managedOwner = scopeOwner(scope);
1502
+ 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
1503
  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
1504
  }
1221
- if (scope.kind === "owner") {
1222
- const clash = columns.find((c) => c.name === scope.column);
1505
+ if (managedOwner) {
1506
+ const clash = columns.find((c) => c.name === managedOwner.column);
1223
1507
  if (clash) {
1224
1508
  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' }).`
1509
+ `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
1510
  );
1227
1511
  }
1228
- if (removedColumns.includes(scope.column)) {
1512
+ if (removedColumns.includes(managedOwner.column)) {
1229
1513
  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().`
1514
+ `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
1515
  );
1232
1516
  }
1233
1517
  }
@@ -1240,7 +1524,7 @@ function readTable(r, rawName, nameLine) {
1240
1524
  `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
1525
  );
1242
1526
  }
1243
- if (scope.kind === "owner" && c.renamedFrom === scope.column) {
1527
+ if (managedOwner && c.renamedFrom === managedOwner.column) {
1244
1528
  throw new SchemaTsError(
1245
1529
  `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
1530
  );
@@ -1258,7 +1542,7 @@ function readTable(r, rawName, nameLine) {
1258
1542
  renameSources.add(c.renamedFrom);
1259
1543
  }
1260
1544
  const known = new Set(columns.map((c) => c.name));
1261
- if (scope.kind === "owner") known.add(scope.column);
1545
+ if (managedOwner) known.add(managedOwner.column);
1262
1546
  const indexes = [];
1263
1547
  for (const entry of indexEntries ?? []) {
1264
1548
  for (const colName of entry.columns) {
@@ -1291,7 +1575,12 @@ function readTable(r, rawName, nameLine) {
1291
1575
  if (columns.filter((column) => column.helper === "id").length !== 1) {
1292
1576
  throw new SchemaTsError(`line ${nameLine}: table "${rawName}": client operations require exactly one id() column.`);
1293
1577
  }
1294
- const parsed = parseClientPermissions(clientInput, columns, scope);
1578
+ const policyMember = scope.kind === "policy" && scope.policy.kind === "any_of" ? scope.policy.policies.find((node) => node.kind === "member") : void 0;
1579
+ const parsed = parseClientPermissions(clientInput, columns, {
1580
+ ...scope,
1581
+ ...managedOwner ? { column: managedOwner.column } : {},
1582
+ ...policyMember ? { group: policyMember.group } : {}
1583
+ });
1295
1584
  if (!parsed.ok) throw new SchemaTsError(`line ${nameLine}: table "${rawName}": ${parsed.message}`);
1296
1585
  const key = columns.find((column) => column.helper === "id").name;
1297
1586
  if (Array.isArray(parsed.permissions.read) && !parsed.permissions.read.includes(key)) {
@@ -1394,41 +1683,83 @@ function extractSchemaTs(source) {
1394
1683
  const tableByName = new Map(tables.map((t) => [t.name, t]));
1395
1684
  const knownColumns = (t) => {
1396
1685
  const s = new Set(t.columns.map((c) => c.name));
1397
- if (t.scope.kind === "owner") s.add(t.scope.column);
1686
+ const owner = scopeOwner(t.scope);
1687
+ if (owner) s.add(owner.column);
1398
1688
  return s;
1399
1689
  };
1690
+ const privatePolicy = (scope) => {
1691
+ if (scope.kind === "owner" && !scope.visitors) return { kind: "owner", column: scope.column };
1692
+ if (scope.kind === "member") return scope;
1693
+ if (scope.kind === "policy" && scope.policy.kind !== "parent") return scope.policy;
1694
+ return null;
1695
+ };
1400
1696
  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
- }
1697
+ if (t.scope.kind !== "policy" || t.scope.policy.kind !== "parent") continue;
1698
+ const pending = t.scope.policy;
1699
+ const fk = t.columns.find((column) => column.name === pending.via);
1700
+ if (!fk || !fk.references) {
1701
+ errors.push(`Table "${t.name}" uses parent({ via: '${pending.via}' }), but "${pending.via}" is not a declared scalar foreign key.`);
1702
+ continue;
1410
1703
  }
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
- );
1704
+ const parentTable = tableByName.get(fk.references);
1705
+ const parentKey = parentTable?.columns.find((column) => column.helper === "id");
1706
+ const inherited = parentTable ? privatePolicy(parentTable.scope) : null;
1707
+ if (!parentTable || !parentKey || !inherited) {
1708
+ 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
1709
  continue;
1417
1710
  }
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.`);
1711
+ 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;
1712
+ if (!compatible) {
1713
+ 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}".`);
1714
+ continue;
1421
1715
  }
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
- );
1716
+ t.scope.policy = { kind: "parent", via: pending.via, table: parentTable.name, parentKey: parentKey.name, policy: inherited };
1717
+ if (t.client?.create && !t.client.create.includes(pending.via)) {
1718
+ 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
1719
  }
1427
- for (const mg of sc.memberGroup) {
1428
- if (!membershipCols.has(mg)) {
1720
+ }
1721
+ const memberPolicies = (scope) => {
1722
+ const out = [];
1723
+ const visit = (node) => {
1724
+ if (node.kind === "member") out.push(node);
1725
+ else if (node.kind === "any_of") node.policies.forEach(visit);
1726
+ };
1727
+ if (scope.kind === "member") out.push(scope);
1728
+ else if (scope.kind === "policy") visit(scope.policy);
1729
+ return out;
1730
+ };
1731
+ for (const t of tables) {
1732
+ for (const sc of memberPolicies(t.scope)) {
1733
+ const own = knownColumns(t);
1734
+ for (const g of sc.group) {
1735
+ if (!own.has(g)) {
1736
+ errors.push(
1737
+ `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.`
1738
+ );
1739
+ }
1740
+ }
1741
+ const membershipTable = tableByName.get(sc.membership);
1742
+ if (!membershipTable) {
1429
1743
  errors.push(
1430
- `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
1744
+ `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.`
1431
1745
  );
1746
+ continue;
1747
+ }
1748
+ const membershipCols = knownColumns(membershipTable);
1749
+ if (membershipTable.client && (membershipTable.client.create !== null || membershipTable.client.update !== null || membershipTable.client.delete)) {
1750
+ errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes. Change membership through a server function.`);
1751
+ }
1752
+ if (!membershipCols.has(sc.memberUser)) {
1753
+ errors.push(
1754
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
1755
+ );
1756
+ }
1757
+ for (const mg of sc.memberGroup) {
1758
+ if (!membershipCols.has(mg)) {
1759
+ errors.push(
1760
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
1761
+ );
1762
+ }
1432
1763
  }
1433
1764
  }
1434
1765
  }
@@ -1549,8 +1880,9 @@ function schemaAuthorityFromSource(files) {
1549
1880
  const baked = bakedTableSchemaFromDeclared(JSON.stringify(canonicalTableShape(table)));
1550
1881
  if (!baked) throw new Error(`Could not derive the client contract for declared table "${table.name}".`);
1551
1882
  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);
1883
+ declareEntry(intents, table.name, declaredScopeIntent(table.scope));
1884
+ const ownerColumn = declaredOwnerColumn(table.scope);
1885
+ if (declaredScopeIntent(table.scope) === "scoped" && ownerColumn) declareEntry(scopes, table.name, ownerColumn);
1554
1886
  }
1555
1887
  return { schema, intents, scopes };
1556
1888
  }