@somewhere-cloud/cli 0.38.16 → 0.38.17

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.
@@ -60,22 +60,26 @@ var require_declared_data_contract = __commonJS({
60
60
  if (value.k === "o" && keys === "c,k" && identifier.test(value.c)) return { k: "o", c: value.c };
61
61
  if (value.k === "m" && keys === "k,m") {
62
62
  record2(value.m, label + ".member");
63
- const { g, m, u, mg, o } = value.m;
63
+ const { g, m, u, mg, o, og } = value.m;
64
64
  const memberKeys = Object.keys(value.m).sort().join(",");
65
- 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);
65
+ const grant = memberKeys === "g,m,mg,o,og,u" && og === true && Array.isArray(g) && Array.isArray(mg) && g[0] === "user_id" && mg[0] === "user_id" && (g.length === 1 || g.length === 2 && g[1] !== "user_id" && mg[1] !== "user_id" && mg[1] !== u) && u !== "user_id" && (JSON.stringify(o) === '["read"]' || JSON.stringify(o) === '["read","update"]');
66
+ if (!["g,m,mg,u", "g,m,mg,o,u"].includes(memberKeys) && !grant || !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);
66
67
  const normalizedOperations = o?.length === 4 ? void 0 : o;
67
- return { k: "m", m: { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {} } };
68
+ return { k: "m", m: { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {}, ...og ? { og: true } : {} } };
68
69
  }
69
70
  if (value.k === "a" && keys === "k,p" && Array.isArray(value.p) && value.p.length === 2) {
70
71
  const left = policy(value.p[0], label), right = policy(value.p[1], label);
71
72
  if (left.k !== "o" || !["m", "g", "r"].includes(right.k)) fail(label);
73
+ if (right.k === "m" && right.m.og && left.c !== "user_id") fail(label);
72
74
  return { k: "a", p: [left, right] };
73
75
  }
74
76
  if (value.k === "n" && keys === "g,k,w") {
75
77
  const gate = policy(value.g, label), who = policy(value.w, label);
76
78
  if (!["m", "g", "r"].includes(gate.k) || !["o", "a"].includes(who.k)) fail(label);
79
+ if (gate.k === "m" && gate.m.og) fail(label);
77
80
  if (who.k === "a") {
78
81
  const x = who.p[1];
82
+ if (x.k === "m" && x.m.og) fail(label);
79
83
  const same = x.k === "r" || x.k === "g" && gate.k === "g" && x.c.toLowerCase() === gate.c.toLowerCase() || x.k === "m" && gate.k === "m" && x.m.m.toLowerCase() === gate.m.m.toLowerCase() && x.m.u.toLowerCase() === gate.m.u.toLowerCase() && JSON.stringify(x.m.g.map((c) => c.toLowerCase())) === JSON.stringify(gate.m.g.map((c) => c.toLowerCase())) && JSON.stringify(x.m.mg.map((c) => c.toLowerCase())) === JSON.stringify(gate.m.mg.map((c) => c.toLowerCase()));
80
84
  if (!same) fail(label);
81
85
  }
@@ -106,14 +110,14 @@ var require_declared_data_contract = __commonJS({
106
110
  }
107
111
  return roles;
108
112
  }
109
- function policyMembershipTables(value, out) {
113
+ function policyMembershipTables(value, out, grants) {
110
114
  if (!value || typeof value !== "object") return;
111
- if (value.k === "m" && value.m) out.add(value.m.m);
112
- else if (value.k === "a") value.p.forEach((item) => policyMembershipTables(item, out));
115
+ if (value.k === "m" && value.m) (value.m.og === true ? grants : out).add(value.m.m);
116
+ else if (value.k === "a") value.p.forEach((item) => policyMembershipTables(item, out, grants));
113
117
  else if (value.k === "n") {
114
- policyMembershipTables(value.g, out);
115
- policyMembershipTables(value.w, out);
116
- } else if (value.k === "p") policyMembershipTables(value.p, out);
118
+ policyMembershipTables(value.g, out, grants);
119
+ policyMembershipTables(value.w, out, grants);
120
+ } else if (value.k === "p") policyMembershipTables(value.p, out, grants);
117
121
  }
118
122
  function validateInsertReceiptCapability(value) {
119
123
  if (value !== void 0 && value !== "insert-v1") fail("insert receipt capability");
@@ -126,7 +130,17 @@ var require_declared_data_contract = __commonJS({
126
130
  record2(scopes, "scopes");
127
131
  const tables = [];
128
132
  const membershipTables = new Set(Object.values(schema).filter((table) => table && table.member).map((table) => table.member.m));
129
- for (const table of Object.values(schema)) if (table?.policy) policyMembershipTables(table.policy, membershipTables);
133
+ const grantTables = /* @__PURE__ */ new Set();
134
+ for (const table of Object.values(schema)) if (table?.policy) policyMembershipTables(table.policy, membershipTables, grantTables);
135
+ for (const name of grantTables) {
136
+ const grant = schema[name];
137
+ if (!grant || intents[name] !== "scoped" || scopes[name] !== "user_id" || grant.visitors || grant.policy || grant.member || grant.client?.links || grant.client?.intake || grant.client?.identity === "visitor") fail(`${name} owner grant authority`);
138
+ }
139
+ const grantRules = Object.entries(schema).flatMap(([name, table]) => table?.policy?.k === "a" && table.policy.p?.[1]?.k === "m" && table.policy.p[1].m?.og === true ? [{ name, table, m: table.policy.p[1].m }] : []);
140
+ for (const rule of grantRules.filter((rule2) => rule2.m.g.length === 2)) {
141
+ const shares = schema[rule.m.m], row = rule.m.mg[1];
142
+ if (grantRules.filter((other) => other.m.m === rule.m.m).length !== 1 || rule.table.primaryKey !== rule.m.g[1] || !shares?.columns?.some((column) => column?.n === row) || Array.isArray(shares.client?.update) && shares.client.update.includes(row)) fail(`${rule.m.m} row grant authority`);
143
+ }
130
144
  for (const name of Object.keys(schema).sort()) {
131
145
  const table = schema[name];
132
146
  record2(table, `table ${name}`);
@@ -802,7 +816,7 @@ interface SomewhereAppRoleGrant {
802
816
  user_id: string;
803
817
  role: SomewhereAppRole;
804
818
  granted_by: string;
805
- grant_source: 'platform_admin' | 'delegation';
819
+ grant_source: 'platform_admin' | 'delegation' | 'email_domain';
806
820
  created_at: number;
807
821
  }
808
822
 
@@ -871,7 +885,7 @@ type SomewhereGroupsResult<T> = { data: T; error: null } | { data: null; error:
871
885
  */
872
886
  interface SomewhereGroups {
873
887
  create(req: Request, input: { name: string }): Promise<SomewhereGroupsResult<SomewhereGroupDetail>>;
874
- /** Also completes (and reports) up to 3 of the signed-in user's invitations not yet confirmed at sign-in. */
888
+ /** Also completes (and reports) up to 10 of the signed-in user's invitations not yet confirmed at sign-in. */
875
889
  list(req: Request, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereGroup> & { invitations: SomewhereGroupInvitationOutcome[] }>>;
876
890
  get(req: Request, groupId: string): Promise<SomewhereGroupsResult<SomewhereGroupDetail>>;
877
891
  members(req: Request, groupId: string, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereGroupMember>>>;
@@ -895,7 +909,7 @@ interface SomewhereGroups {
895
909
  appRoles: {
896
910
  list(req: Request, userId?: string, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereAppRoleGrant>>>;
897
911
  grant(req: Request, userId: string, role: SomewhereAppRole): Promise<SomewhereGroupsResult<SomewhereAppRoleGrant>>;
898
- revoke(req: Request, userId: string, role: SomewhereAppRole): Promise<SomewhereGroupsResult<{ revoked: boolean }>>;
912
+ revoke(req: Request, userId: string, role: SomewhereAppRole): Promise<SomewhereGroupsResult<{ revoked: boolean; held_by?: 'email_domain'; domain?: string; message?: string }>>;
899
913
  };
900
914
  }
901
915
 
@@ -1424,35 +1438,31 @@ interface SomewhereRuntimeAuthModeration {
1424
1438
  user: { id: string; email: string; role: SomewhereAuthRole };
1425
1439
  }>;
1426
1440
  }
1427
- // Replaces the inline mfa type in SomewhereRuntimeAuth (challenge/challengeWithCookie unchanged).
1441
+ // Email sign-in codes (tsk_8fe91eb5). Authenticator-app activation (reauthenticate*) answers 501 AUTH_FEATURE_UNAVAILABLE.
1442
+ type SomewhereMfaDelivery = 'sent' | 'pending' | 'unknown';
1443
+ interface SomewhereMfaCodeSent { challenge_id: string; delivery: SomewhereMfaDelivery; expires_in: number }
1444
+ type SomewhereMfaRecovery =
1445
+ | { removal_pending: true; removes_at: string; wait_seconds: number; notice_resent?: boolean }
1446
+ | { enrolled: false };
1428
1447
  interface SomewhereRuntimeAuthMfa {
1429
1448
  challenge(options: { mfa_token: string; code: string }): Promise<SomewhereAuthSession>;
1430
1449
  challengeWithCookie(options: { mfa_token: string; code: string }): Promise<{ user: SomewhereAuthUser }>;
1431
- enroll(options: { token: string }): Promise<{
1432
- secret: string; enrollment_id: string; otpauth_uri: string; issuer: string; account: string;
1433
- }>;
1434
- reauthenticate(options: { token: string; enrollment_id: string; method: 'password'; password: string }):
1435
- Promise<{ method: 'password'; activation_token: string; expires_in_seconds: number }>;
1436
- reauthenticate(options: { token: string; enrollment_id: string; method: 'email' }):
1437
- Promise<{ method: 'email'; challenge_id: string; expires_in_seconds: number; pending?: true }>;
1438
- verifyReauthentication(options: { token: string; enrollment_id: string; challenge_id: string; code: string }):
1439
- Promise<{ method: 'email'; activation_token: string; expires_in_seconds: number }>;
1440
- verify(options: { token: string; enrollment_id: string; activation_token: string; code: string }):
1441
- Promise<{ enabled: true; backup_codes: string[] }>;
1442
- unenroll(options: { token: string; code: string }): Promise<{ enabled: false }>;
1443
- // The same steps for the request's own session (cookie or Bearer). verifyWithCookie ends every session and clears the cookies.
1444
- enrollWithCookie(req: Request): Promise<{
1445
- secret: string; enrollment_id: string; otpauth_uri: string; issuer: string; account: string;
1446
- }>;
1447
- reauthenticateWithCookie(req: Request, options: { enrollment_id: string; method: 'password'; password: string }):
1448
- Promise<{ method: 'password'; activation_token: string; expires_in_seconds: number }>;
1449
- reauthenticateWithCookie(req: Request, options: { enrollment_id: string; method: 'email' }):
1450
- Promise<{ method: 'email'; challenge_id: string; expires_in_seconds: number; pending?: true }>;
1451
- verifyReauthenticationWithCookie(req: Request, options: { enrollment_id: string; challenge_id: string; code: string }):
1452
- Promise<{ method: 'email'; activation_token: string; expires_in_seconds: number }>;
1453
- verifyWithCookie(req: Request, options: { enrollment_id: string; activation_token: string; code: string }):
1454
- Promise<{ enabled: true; backup_codes: string[] }>;
1455
- unenrollWithCookie(req: Request, options: { code: string }): Promise<{ enabled: false }>;
1450
+ resend(options: { mfa_token: string }): Promise<{ sent: boolean; delivery: SomewhereMfaDelivery; expires_in: number }>;
1451
+ enroll(options: { token: string }): Promise<SomewhereMfaCodeSent | { enrolled: true; method: 'email' }>;
1452
+ verify(options: { token: string; challenge_id: string; code: string }): Promise<{ enrolled: true; method: 'email' }>;
1453
+ unenroll(options: { token: string; password: string }): Promise<SomewhereMfaCodeSent | { enrolled: false }>;
1454
+ unenroll(options: { token: string; challenge_id: string; code: string }): Promise<{ enrolled: false }>;
1455
+ recover(options: { email: string; password: string }): Promise<SomewhereMfaRecovery>;
1456
+ cancelRecovery(options: { token: string }): Promise<{ outcome: 'cancelled' | 'already_closed' | 'completed'; message: string }>;
1457
+ reauthenticate(options: { token: string; enrollment_id: string; method: 'password' | 'email'; password?: string }): Promise<never>;
1458
+ verifyReauthentication(options: { token: string; enrollment_id: string; challenge_id: string; code: string }): Promise<never>;
1459
+ // The same steps for the request's own session (cookie or Bearer). Turning codes on or off ends no session.
1460
+ enrollWithCookie(req: Request): Promise<SomewhereMfaCodeSent | { enrolled: true; method: 'email' }>;
1461
+ verifyWithCookie(req: Request, options: { challenge_id: string; code: string }): Promise<{ enrolled: true; method: 'email' }>;
1462
+ unenrollWithCookie(req: Request, options: { password: string }): Promise<SomewhereMfaCodeSent | { enrolled: false }>;
1463
+ unenrollWithCookie(req: Request, options: { challenge_id: string; code: string }): Promise<{ enrolled: false }>;
1464
+ reauthenticateWithCookie(req: Request, options: { enrollment_id: string; method: 'password' | 'email'; password?: string }): Promise<never>;
1465
+ verifyReauthenticationWithCookie(req: Request, options: { enrollment_id: string; challenge_id: string; code: string }): Promise<never>;
1456
1466
  }
1457
1467
  interface SomewhereRuntimeAuth {
1458
1468
  signup(options: SomewhereAuthSignupOptions): Promise<SomewhereAuthIssuedSession>;
@@ -4044,6 +4054,7 @@ var require_schema_types = __commonJS({
4044
4054
  interface Scope { readonly __somewhereScope: unique symbol }
4045
4055
  interface OwnerScope extends Scope { readonly __somewhereOwner: unique symbol }
4046
4056
  interface MemberScope extends Scope { readonly __somewhereMember: unique symbol }
4057
+ interface GrantScope extends Scope { readonly __somewhereGrant: unique symbol }
4047
4058
  interface GroupScope extends Scope { readonly __somewhereGroup: unique symbol }
4048
4059
  interface AppRoleScope extends Scope { readonly __somewhereAppRole: unique symbol }
4049
4060
  interface AnyOfScope extends Scope { readonly __somewhereAnyOf: unique symbol }
@@ -4102,8 +4113,9 @@ function removed(): SomewhereSchemaDeclaration.ColumnMarker;
4102
4113
  function removedTable(): SomewhereSchemaDeclaration.TableMarker;
4103
4114
  function exported(): SomewhereSchemaDeclaration.TableMarker;
4104
4115
  `;
4105
- var policySignatures = `function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, other: SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope): SomewhereSchemaDeclaration.AnyOfScope;
4106
- function anyOf(other: SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.AnyOfScope;
4116
+ var policySignatures = `function grant(options: { table: string; grantee: string; row?: string; operations: readonly ['read'] | readonly ['read', 'update'] }): SomewhereSchemaDeclaration.GrantScope;
4117
+ function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, other: SomewhereSchemaDeclaration.GrantScope | SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope): SomewhereSchemaDeclaration.AnyOfScope;
4118
+ function anyOf(other: SomewhereSchemaDeclaration.GrantScope | SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.AnyOfScope;
4107
4119
  function allOf(gate: SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, who: SomewhereSchemaDeclaration.OwnerScope | SomewhereSchemaDeclaration.AnyOfScope): SomewhereSchemaDeclaration.Scope;
4108
4120
  function parent(options: { via: string; table?: string }): SomewhereSchemaDeclaration.Scope;
4109
4121
  function group(options?: { column?: string; roles?: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.GroupScope;
@@ -4306,6 +4318,9 @@ function isCanonicalBigintText(value) {
4306
4318
  }
4307
4319
 
4308
4320
  // worker/src/utils/db-schema-deploy/client-permissions.ts
4321
+ function tableAllowsClientWrites(table) {
4322
+ return Boolean(table.client && (table.client.create !== null || table.client.update !== null || table.client.delete || table.client.intake !== void 0));
4323
+ }
4309
4324
  function parseClientPermissions(value, columns, scope) {
4310
4325
  const fail = (message) => ({ ok: false, message });
4311
4326
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4839,6 +4854,23 @@ function validateBindings(input, tableInfo, errors) {
4839
4854
  }
4840
4855
  }
4841
4856
 
4857
+ // worker/src/utils/authority/role-domain-map.ts
4858
+ var ROLE_NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
4859
+ var EMAIL_DOMAIN_PATTERN = /^(?=.{4,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/;
4860
+ function validRoleDomainMap(value) {
4861
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([role, domain]) => ROLE_NAME_PATTERN.test(role) && typeof domain === "string" && EMAIL_DOMAIN_PATTERN.test(domain));
4862
+ }
4863
+ var ROLE_DOMAIN_MAP_JS = `
4864
+ function __sw_structRoleDomains(value) {
4865
+ if (!value || typeof value !== 'object' || __sw_arrayIsArray(value)) return false;
4866
+ const roles = __sw_keys(value);
4867
+ for (let i = 0; i < roles.length; i++) {
4868
+ const domain = value[roles[i]];
4869
+ if (!__sw_reMatch(${ROLE_NAME_PATTERN}, roles[i]) || typeof domain !== 'string' || !__sw_reMatch(${EMAIL_DOMAIN_PATTERN}, domain)) return false;
4870
+ }
4871
+ return true;
4872
+ }`;
4873
+
4842
4874
  // worker/src/utils/authority/scope-enforcement.ts
4843
4875
  var SAFE_SCOPE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
4844
4876
  var GROUP_ROLE_NAME = /^[a-z][a-z0-9_]{0,63}$/;
@@ -4859,7 +4891,9 @@ function validBakedMemberScope(value) {
4859
4891
  const operations = member.o;
4860
4892
  const operationOrder = ["read", "create", "update", "delete"];
4861
4893
  const validOperations = operations === void 0 || Array.isArray(operations) && JSON.stringify(operations) === JSON.stringify(operationOrder.filter((operation) => operations.includes(operation))) && (operations.includes("read") || !operations.includes("update") && !operations.includes("delete"));
4862
- return (memberKeys === "g,m,mg,u" || memberKeys === "g,m,mg,o,u") && validOperations && Array.isArray(member.g) && member.g.length > 0 && member.g.every(ident) && Array.isArray(member.mg) && member.mg.length === member.g.length && member.mg.every(ident) && ident(member.m) && ident(member.u);
4894
+ const g = member.g, mg = member.mg;
4895
+ const grant = memberKeys === "g,m,mg,o,og,u" && member.og === true && Array.isArray(g) && Array.isArray(mg) && g[0] === "user_id" && mg[0] === "user_id" && (g.length === 1 || g.length === 2 && g[1] !== "user_id" && mg[1] !== "user_id" && mg[1] !== member.u) && member.u !== "user_id" && (JSON.stringify(operations) === '["read"]' || JSON.stringify(operations) === '["read","update"]');
4896
+ return (memberKeys === "g,m,mg,u" || memberKeys === "g,m,mg,o,u" || grant) && validOperations && Array.isArray(member.g) && member.g.length > 0 && member.g.every(ident) && Array.isArray(member.mg) && member.mg.length === member.g.length && member.mg.every(ident) && ident(member.m) && ident(member.u);
4863
4897
  }
4864
4898
  var MAX_PARENT_EDGES = 4;
4865
4899
  function pinShape(policy) {
@@ -4873,7 +4907,7 @@ function sameAllOfRelation(gate, x) {
4873
4907
  if (x.k !== "m" || gate.k !== "m") return false;
4874
4908
  const a = gate.m;
4875
4909
  const b = x.m;
4876
- return lower(a.m) === lower(b.m) && lower(a.u) === lower(b.u) && a.g.length === b.g.length && a.g.every((column, i) => lower(column) === lower(b.g[i])) && a.mg.length === b.mg.length && a.mg.every((column, i) => lower(column) === lower(b.mg[i]));
4910
+ return !a.og && !b.og && lower(a.m) === lower(b.m) && lower(a.u) === lower(b.u) && a.g.length === b.g.length && a.g.every((column, i) => lower(column) === lower(b.g[i])) && a.mg.length === b.mg.length && a.mg.every((column, i) => lower(column) === lower(b.mg[i]));
4877
4911
  }
4878
4912
  function validBakedPolicy(value, root = true, edges = 0) {
4879
4913
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
@@ -4886,7 +4920,7 @@ function validBakedPolicy(value, root = true, edges = 0) {
4886
4920
  }
4887
4921
  if (node.k === "a") {
4888
4922
  const second = node.p?.[1]?.k;
4889
- return exactKeys(node, ["k", "p"]) && Array.isArray(node.p) && node.p.length === 2 && node.p[0]?.k === "o" && validBakedPolicy(node.p[0], false) && (second === "m" ? validBakedPolicy(node.p[1], false) : (second === "g" || second === "r") && validBakedPolicy(node.p[1], true));
4923
+ return exactKeys(node, ["k", "p"]) && Array.isArray(node.p) && node.p.length === 2 && node.p[0]?.k === "o" && validBakedPolicy(node.p[0], false) && (!(node.p[1]?.k === "m" && node.p[1].m?.og) || node.p[0].c === "user_id") && (second === "m" ? validBakedPolicy(node.p[1], false) : (second === "g" || second === "r") && validBakedPolicy(node.p[1], true));
4890
4924
  }
4891
4925
  if (root && node.k === "n") {
4892
4926
  if (!ALLOF_POLICY_ADMITTED || !exactKeys(node, ["k", "g", "w"])) return false;
@@ -4894,7 +4928,7 @@ function validBakedPolicy(value, root = true, edges = 0) {
4894
4928
  const who = node.w;
4895
4929
  if (!gate || !who) return false;
4896
4930
  const gateMember = gate.m;
4897
- const gateValid = gate.k === "m" ? validBakedPolicy(gate, false) && gateMember?.o !== void 0 : (gate.k === "g" || gate.k === "r") && validBakedPolicy(gate, true);
4931
+ const gateValid = gate.k === "m" ? validBakedPolicy(gate, false) && gateMember?.o !== void 0 && !gateMember.og : (gate.k === "g" || gate.k === "r") && validBakedPolicy(gate, true);
4898
4932
  if (!gateValid) return false;
4899
4933
  if (who.k === "o") return validBakedPolicy(who, false);
4900
4934
  const whoBranches = who.p;
@@ -4912,7 +4946,9 @@ function validBakedPolicy(value, root = true, edges = 0) {
4912
4946
  return exactKeys(node, ["k", "c", "roles", "generation"]) && ident(node.c) && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
4913
4947
  }
4914
4948
  if (root && node.k === "r") {
4915
- return exactKeys(node, ["k", "roles", "generation"]) && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
4949
+ const domains = node.d;
4950
+ const validDomains = domains === void 0 || validRoleDomainMap(domains);
4951
+ return (exactKeys(node, ["k", "roles", "generation"]) || exactKeys(node, ["k", "roles", "generation", "d"])) && validDomains && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
4916
4952
  }
4917
4953
  return false;
4918
4954
  }
@@ -5033,7 +5069,8 @@ function bakedPolicyFromDeclared(value, depth = 0, edges = 0, underParent = fals
5033
5069
  if (depth !== 0 && !underParent || node.policies[0]?.kind !== "owner" || second !== "member" && second !== "group" && second !== "app_role") return null;
5034
5070
  const left = bakedPolicyFromDeclared(node.policies[0], depth + 1);
5035
5071
  const right = bakedPolicyFromDeclared(node.policies[1], depth + 1, edges, second !== "member");
5036
- return left && right ? { k: "a", p: [left, right] } : null;
5072
+ const baked = left && right ? { k: "a", p: [left, right] } : null;
5073
+ return baked && validBakedPolicy(pinShape(baked), true, edges) ? baked : null;
5037
5074
  }
5038
5075
  if (node.kind === "all_of") {
5039
5076
  if (depth !== 0 && !underParent) return null;
@@ -5125,7 +5162,143 @@ function bakedMemberFromDeclared(scope, explicitOperations = false) {
5125
5162
  if (JSON.stringify(operations) !== JSON.stringify(allowed.filter((operation) => operations.includes(operation))) || !operations.includes("read") && (operations.includes("update") || operations.includes("delete"))) return null;
5126
5163
  o = operations.length === allowed.length && !explicitOperations ? void 0 : [...operations];
5127
5164
  }
5128
- return { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg, ...o !== void 0 ? { o } : {} };
5165
+ if (s.ownerGrant !== void 0 && s.ownerGrant !== true) return null;
5166
+ const member = { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg, ...o !== void 0 ? { o } : {}, ...s.ownerGrant ? { og: true } : {} };
5167
+ return validBakedMemberScope(member) ? member : null;
5168
+ }
5169
+
5170
+ // worker/src/utils/db-schema-deploy/extract-schema-grants.ts
5171
+ function readGrantScope(r, table, line, fail) {
5172
+ r.expectPunct("(", "after grant");
5173
+ r.expectPunct("{", "in grant options");
5174
+ let membership, grantee, row;
5175
+ let operations;
5176
+ const seen = /* @__PURE__ */ new Set();
5177
+ while (!r.tryPunct("}")) {
5178
+ const key = r.readKey("in grant options");
5179
+ if (seen.has(key.name)) fail(key.line, `Duplicate grant option "${key.name}".`);
5180
+ seen.add(key.name);
5181
+ r.expectPunct(":", "after grant option");
5182
+ if (key.name === "table" || key.name === "grantee" || key.name === "row") {
5183
+ const value = r.readLiteral("in grant options");
5184
+ if (typeof value !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/.test(value)) fail(key.line, "grant table, grantee and row must be column/table identifiers.");
5185
+ if (key.name === "table") membership = value.toLowerCase();
5186
+ else if (key.name === "grantee") grantee = value.toLowerCase();
5187
+ else row = value.toLowerCase();
5188
+ } else if (key.name === "operations") {
5189
+ const ops = [];
5190
+ r.expectPunct("[", "in grant operations");
5191
+ while (!r.tryPunct("]")) {
5192
+ const op = r.readLiteral("in grant operations");
5193
+ if (op !== "read" && op !== "update") fail(key.line, "grant operations allow only read and update.");
5194
+ if (ops.includes(op)) fail(key.line, "Duplicate grant operation.");
5195
+ ops.push(op);
5196
+ if (!r.tryPunct(",")) {
5197
+ r.expectPunct("]", "closing grant operations");
5198
+ break;
5199
+ }
5200
+ }
5201
+ if (!ops.includes("read")) fail(key.line, "grant operations require read; update alone is not allowed.");
5202
+ operations = ops.includes("update") ? ["read", "update"] : ["read"];
5203
+ } else fail(key.line, `Unknown grant option "${key.name}". Allowed: table, grantee, row, operations.`);
5204
+ if (!r.tryPunct(",")) {
5205
+ r.expectPunct("}", "closing grant options");
5206
+ break;
5207
+ }
5208
+ }
5209
+ r.tryPunct(",");
5210
+ r.expectPunct(")", "closing grant");
5211
+ if (!membership || !grantee || !operations || grantee === "user_id") fail(line, `grant() on "${table}" requires table, a grantee column distinct from managed user_id, and explicit operations.`);
5212
+ if (row !== void 0 && (row === "user_id" || row === grantee)) fail(line, `grant() on "${table}": row names the shares column holding the shared row's id, distinct from user_id and the grantee.`);
5213
+ return { kind: "member", group: ["user_id"], membership, memberUser: grantee, memberGroup: row === void 0 ? ["user_id"] : ["user_id", row], operations, ownerGrant: true };
5214
+ }
5215
+ function resolveGrantRowKeys(scope, key, table, line, fail) {
5216
+ const visit = (node) => {
5217
+ if (node.kind === "member" && node.ownerGrant && node.memberGroup.length === 2 && node.group.length === 1) {
5218
+ if (key === void 0) fail(line, `grant() with row on "${table}" needs the table's one id() key: each share names one row by it.`);
5219
+ node.group = ["user_id", key.toLowerCase()];
5220
+ } else if (node.kind === "any_of") node.policies.forEach(visit);
5221
+ else if (node.kind === "all_of") {
5222
+ visit(node.gate);
5223
+ visit(node.who);
5224
+ }
5225
+ };
5226
+ if (scope.kind === "policy") visit(scope.policy);
5227
+ }
5228
+ function validateDeclaredMemberships(tables, tableByName, knownColumns, errors) {
5229
+ const memberPolicies = (scope) => {
5230
+ const out = [];
5231
+ const visit = (node) => {
5232
+ if (node.kind === "member") out.push(node);
5233
+ else if (node.kind === "any_of") node.policies.forEach(visit);
5234
+ else if (node.kind === "all_of") {
5235
+ visit(node.gate);
5236
+ visit(node.who);
5237
+ }
5238
+ };
5239
+ if (scope.kind === "member") out.push(scope);
5240
+ else if (scope.kind === "policy") visit(scope.policy);
5241
+ return out;
5242
+ };
5243
+ for (const t of tables) {
5244
+ for (const sc of memberPolicies(t.scope)) {
5245
+ const own = knownColumns(t);
5246
+ for (const g of sc.group) {
5247
+ if (!own.has(g)) {
5248
+ errors.push(
5249
+ `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.`
5250
+ );
5251
+ }
5252
+ }
5253
+ const membershipTable = tableByName.get(sc.membership);
5254
+ if (!membershipTable) {
5255
+ errors.push(
5256
+ `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.`
5257
+ );
5258
+ continue;
5259
+ }
5260
+ const membershipCols = knownColumns(membershipTable);
5261
+ if (sc.ownerGrant) {
5262
+ if (membershipTable.scope.kind !== "owner" || membershipTable.scope.column !== "user_id" || membershipTable.scope.visitors) {
5263
+ errors.push(`Grant table "${sc.membership}" must use owner() with managed user_id and authenticated owners.`);
5264
+ }
5265
+ if (membershipTable.client?.intake || membershipTable.client?.links) errors.push(`Grant table "${sc.membership}" cannot expose public intake or row links; only its authenticated owner manages grants.`);
5266
+ const grantee = membershipTable.columns.find((column) => column.name === sc.memberUser);
5267
+ if (!grantee || grantee.helper !== "text" || grantee.nullable) errors.push(`Grant grantee "${sc.membership}"."${sc.memberUser}" must be a required text column.`);
5268
+ if (sc.memberGroup.length === 2) {
5269
+ const row = membershipTable.columns.find((column) => column.name === sc.memberGroup[1]);
5270
+ const key = t.columns.find((column) => column.name === sc.group[1]);
5271
+ const keyHelper = key?.helper === "id" ? key.uuid ? "text" : "integer" : null;
5272
+ if (!row || row.nullable || row.helper !== keyHelper) {
5273
+ errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" must be a required ${keyHelper ?? "id-typed"} column holding a "${t.name}" id.`);
5274
+ } else if (row.hasDefault || row.references !== t.name || row.onDelete !== "cascade") {
5275
+ errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" must reference its row with no default: ${sc.memberGroup[1]}: ${keyHelper}({ references: '${t.name}', onDelete: 'cascade' }).`);
5276
+ }
5277
+ if (membershipTable.client?.update?.includes(sc.memberGroup[1])) {
5278
+ errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" cannot be in client.update: a share names one row for its life. Delete the share and create another.`);
5279
+ }
5280
+ const users = tables.filter((other) => memberPolicies(other.scope).some((m) => m.ownerGrant && m.membership === sc.membership));
5281
+ if (users.length !== 1 || memberPolicies(t.scope).filter((m) => m.ownerGrant && m.membership === sc.membership).length !== 1) {
5282
+ errors.push(`Grant table "${sc.membership}" names rows of "${t.name}", so it serves only that one grant() rule. Declare another shares table for other grants.`);
5283
+ }
5284
+ }
5285
+ } else if (tableAllowsClientWrites(membershipTable)) {
5286
+ errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
5287
+ }
5288
+ if (!membershipCols.has(sc.memberUser)) {
5289
+ errors.push(
5290
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
5291
+ );
5292
+ }
5293
+ for (const mg of sc.ownerGrant ? [] : sc.memberGroup) {
5294
+ if (!membershipCols.has(mg)) {
5295
+ errors.push(
5296
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
5297
+ );
5298
+ }
5299
+ }
5300
+ }
5301
+ }
5129
5302
  }
5130
5303
 
5131
5304
  // worker/src/utils/db-schema-deploy/schema-constants.ts
@@ -6219,8 +6392,9 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
6219
6392
  }
6220
6393
 
6221
6394
  // worker/src/utils/db-schema-deploy/extract-schema-roles.ts
6395
+ var EMAIL_DOMAIN = EMAIL_DOMAIN_PATTERN;
6222
6396
  var GROUP_KEYS = ["roles", "adminGrantableRoles"];
6223
- var APP_ROLE_KEYS = ["grantableBy"];
6397
+ var APP_ROLE_KEYS = ["grantableBy", "emailDomain"];
6224
6398
  function readStringList2(r, fail, ctx) {
6225
6399
  const open = r.peek();
6226
6400
  if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted role names, such as ['viewer'].`);
@@ -6264,7 +6438,7 @@ function readAppRoles(r, fail) {
6264
6438
  const key = r.readKey("for an app role name");
6265
6439
  if (out.some((role2) => role2.name === key.name)) fail(key.line, `app role "${key.name}" is declared twice.`);
6266
6440
  r.expectPunct(":", `after app role "${key.name}"`);
6267
- r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...] }`);
6441
+ r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...], emailDomain: '...' }`);
6268
6442
  const role = { name: key.name, grantableBy: [] };
6269
6443
  const seen = /* @__PURE__ */ new Set();
6270
6444
  while (!r.tryPunct("}")) {
@@ -6275,7 +6449,16 @@ function readAppRoles(r, fail) {
6275
6449
  if (seen.has(opt.name)) fail(opt.line, `app role "${key.name}" declares ${opt.name} twice.`);
6276
6450
  seen.add(opt.name);
6277
6451
  r.expectPunct(":", `after appRoles.${key.name}.${opt.name}`);
6278
- role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
6452
+ if (opt.name === "emailDomain") {
6453
+ const t = r.next();
6454
+ if (t.kind !== "string") fail(t.line, `appRoles.${key.name}.emailDomain must be one quoted domain, such as 'example.com'.`);
6455
+ if (!EMAIL_DOMAIN.test(t.value)) {
6456
+ fail(t.line, `appRoles.${key.name}.emailDomain "${t.value}" must be one lowercase domain such as 'example.com': no "@", no wildcard, no scheme or path. Users whose verified email is at exactly that domain hold the role; subdomains are other domains.`);
6457
+ }
6458
+ role.emailDomain = t.value;
6459
+ } else {
6460
+ role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
6461
+ }
6279
6462
  if (!r.tryPunct(",")) {
6280
6463
  r.expectPunct("}", `closing app role "${key.name}"`);
6281
6464
  break;
@@ -6295,18 +6478,31 @@ function canonicalRoleVocabulary(groups, appRoles) {
6295
6478
  v: 1,
6296
6479
  groupRoles: sorted2(groups?.roles ?? []),
6297
6480
  adminGrantableGroupRoles: sorted2(groups?.adminGrantableRoles ?? []),
6298
- appRoles: [...appRoles ?? []].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((role) => ({ name: role.name, grantableBy: sorted2(role.grantableBy) }))
6481
+ appRoles: [...appRoles ?? []].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((role) => ({ name: role.name, grantableBy: sorted2(role.grantableBy), ...role.emailDomain ? { emailDomain: role.emailDomain } : {} }))
6299
6482
  };
6300
6483
  }
6301
6484
  function validateDeclaredRoles(groups, appRoles) {
6302
6485
  const delegation = /* @__PURE__ */ Object.create(null);
6303
6486
  for (const role of appRoles ?? []) delegation[role.name] = role.grantableBy;
6304
- return roleVocabularyErrors({
6487
+ return [...emailDomainRoleErrors(appRoles ?? []), ...roleVocabularyErrors({
6305
6488
  groupRoles: groups?.roles ?? [],
6306
6489
  adminGrantableGroupRoles: groups?.adminGrantableRoles ?? [],
6307
6490
  appRoles: (appRoles ?? []).map((role) => role.name),
6308
6491
  appRoleDelegation: delegation
6309
- });
6492
+ })];
6493
+ }
6494
+ function emailDomainRoleErrors(appRoles) {
6495
+ const errors = [];
6496
+ const byDomain = new Set(appRoles.filter((role) => role.emailDomain !== void 0).map((role) => role.name));
6497
+ for (const role of appRoles) {
6498
+ if (role.emailDomain !== void 0 && !EMAIL_DOMAIN.test(role.emailDomain)) errors.push(`appRoles.${role.name}.emailDomain is not one lowercase domain.`);
6499
+ for (const grantor of role.grantableBy) {
6500
+ if (byDomain.has(grantor)) {
6501
+ errors.push(`appRoles.${role.name}.grantableBy names "${grantor}", which is held by email domain. A role held by email domain cannot grant other roles; list a role that is granted explicitly.`);
6502
+ }
6503
+ }
6504
+ }
6505
+ return errors;
6310
6506
  }
6311
6507
 
6312
6508
  // worker/src/utils/db-schema-deploy/extract-schema-options.ts
@@ -6396,6 +6592,7 @@ var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
6396
6592
  ...TABLE_MARKER_HELPERS,
6397
6593
  "schema",
6398
6594
  "table",
6595
+ "grant",
6399
6596
  "removed",
6400
6597
  "hasMany",
6401
6598
  "belongsTo",
@@ -6510,10 +6707,10 @@ function tokenize(source) {
6510
6707
  var Reader = class {
6511
6708
  constructor(tokens, options = {}) {
6512
6709
  this.tokens = tokens;
6513
- this.bareIdIsText = !options.legacyIntegerIdDefault;
6710
+ this.bareIdIsText = (table) => !options.legacyIntegerIdDefault && !options.integerKeyTables?.has(table.toLowerCase());
6514
6711
  }
6515
6712
  pos = 0;
6516
- /** What a bare id() declares: a random text key, or (legacy retained source) an integer. */
6713
+ /** What a bare id() on this table declares: a random text key, or an integer (ExtractSchemaOptions). */
6517
6714
  bareIdIsText;
6518
6715
  peek() {
6519
6716
  return this.tokens[this.pos];
@@ -6681,7 +6878,7 @@ function readColumn(r, tableName, rawName, line) {
6681
6878
  const col = {
6682
6879
  name,
6683
6880
  helper,
6684
- uuid: helper === "id" && r.bareIdIsText,
6881
+ uuid: helper === "id" && r.bareIdIsText(tableName),
6685
6882
  nullable: false,
6686
6883
  hasDefault: false,
6687
6884
  default: null,
@@ -6918,7 +7115,7 @@ function readPolicyMember(r, tableName, line) {
6918
7115
  if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
6919
7116
  return scope;
6920
7117
  }
6921
- var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), group() or appRole().";
7118
+ var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), grant(), group() or appRole().";
6922
7119
  function readAnyOfBranch(r, tableName, which) {
6923
7120
  const tok = r.expectIdent(`for the ${which} policy in anyOf() on table "${tableName}"`);
6924
7121
  const failAt = (at, message) => {
@@ -6926,6 +7123,7 @@ function readAnyOfBranch(r, tableName, which) {
6926
7123
  };
6927
7124
  if (tok.value === "owner") return readPolicyOwner(r, tableName, tok.line);
6928
7125
  if (tok.value === "member") return readPolicyMember(r, tableName, tok.line);
7126
+ if (tok.value === "grant") return readGrantScope(r, tableName, tok.line, failAt);
6929
7127
  if (tok.value === "group") return readGroupScope(r, tableName, tok.line, failAt);
6930
7128
  if (tok.value === "appRole") return readAppRoleScope(r, tableName, tok.line, failAt);
6931
7129
  throw new SchemaTsError(`line ${tok.line}: ${ANY_OF_FORMS}`);
@@ -6941,6 +7139,7 @@ function readAnyOfScope(r, tableName, line) {
6941
7139
  if (owners.length !== 1) throw new SchemaTsError(`line ${line}: ${ANY_OF_FORMS}`);
6942
7140
  const owner = owners[0];
6943
7141
  const other = firstPolicy.kind === "owner" ? secondPolicy : firstPolicy;
7142
+ if (other.kind === "member" && other.ownerGrant && owner.column !== DEFAULT_OWNER_COLUMN) throw new SchemaTsError(`line ${line}: grant() requires owner() with managed user_id.`);
6944
7143
  return { kind: "policy", policy: { kind: "any_of", policies: [owner, other] } };
6945
7144
  }
6946
7145
  var ALL_OF_FORMS = "allOf() takes a gate \u2014 member(), group() or appRole() \u2014 and then owner() or anyOf(owner(), ...).";
@@ -6968,6 +7167,7 @@ function readAllOfScope(r, tableName, line) {
6968
7167
  r.tryPunct(",");
6969
7168
  r.expectPunct(")", "closing allOf()");
6970
7169
  const x = who.kind === "any_of" ? who.policies[1] : null;
7170
+ if (x?.kind === "member" && x.ownerGrant) throw new SchemaTsError(`line ${line}: grant() belongs in anyOf(owner(), grant()), not allOf().`);
6971
7171
  for (const node of [gate, ...x ? [x] : []]) {
6972
7172
  if (node.kind !== "member") continue;
6973
7173
  if (!explicitMemberOperations.has(node)) {
@@ -7203,6 +7403,9 @@ function readTable(r, rawName, nameLine) {
7203
7403
  if (key.name === "scope") {
7204
7404
  if (scope) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "scope" twice.`);
7205
7405
  scope = readScope(r, rawName);
7406
+ resolveGrantRowKeys(scope, idCols[0]?.name, rawName, key.line, (at, message) => {
7407
+ throw new SchemaTsError(`line ${at}: ${message}`);
7408
+ });
7206
7409
  } else if (key.name === "indexes") {
7207
7410
  if (indexEntries) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "indexes" twice.`);
7208
7411
  indexEntries = readColumnLists(r, rawName, "indexes");
@@ -7434,55 +7637,7 @@ function extractSchemaTs(source, options = {}) {
7434
7637
  };
7435
7638
  resolveAppRoleScopes(tables, appRoles, errors);
7436
7639
  resolveGroupsAndParents(tables, tableByName, privatePolicy, groupRoles, errors);
7437
- const memberPolicies = (scope) => {
7438
- const out = [];
7439
- const visit = (node) => {
7440
- if (node.kind === "member") out.push(node);
7441
- else if (node.kind === "any_of") node.policies.forEach(visit);
7442
- else if (node.kind === "all_of") {
7443
- visit(node.gate);
7444
- visit(node.who);
7445
- }
7446
- };
7447
- if (scope.kind === "member") out.push(scope);
7448
- else if (scope.kind === "policy") visit(scope.policy);
7449
- return out;
7450
- };
7451
- for (const t of tables) {
7452
- for (const sc of memberPolicies(t.scope)) {
7453
- const own = knownColumns(t);
7454
- for (const g of sc.group) {
7455
- if (!own.has(g)) {
7456
- errors.push(
7457
- `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.`
7458
- );
7459
- }
7460
- }
7461
- const membershipTable = tableByName.get(sc.membership);
7462
- if (!membershipTable) {
7463
- errors.push(
7464
- `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.`
7465
- );
7466
- continue;
7467
- }
7468
- const membershipCols = knownColumns(membershipTable);
7469
- if (membershipTable.client && (membershipTable.client.create !== null || membershipTable.client.update !== null || membershipTable.client.delete || membershipTable.client.intake !== void 0)) {
7470
- errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
7471
- }
7472
- if (!membershipCols.has(sc.memberUser)) {
7473
- errors.push(
7474
- `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
7475
- );
7476
- }
7477
- for (const mg of sc.memberGroup) {
7478
- if (!membershipCols.has(mg)) {
7479
- errors.push(
7480
- `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
7481
- );
7482
- }
7483
- }
7484
- }
7485
- }
7640
+ validateDeclaredMemberships(tables, tableByName, knownColumns, errors);
7486
7641
  validateDeclaredRelations(tables, tableByName, knownColumns, (table) => removedTables.includes(table) ? "removedTable()" : exportedTables.includes(table) ? "exported()" : null, errors);
7487
7642
  validateRowLinks(tables, tableByName, errors);
7488
7643
  validateIntake(tables, tableByName, errors);
@@ -7490,7 +7645,7 @@ function extractSchemaTs(source, options = {}) {
7490
7645
  errors.push(...fileErrors);
7491
7646
  validateFileCollections(fileCollections, (name) => {
7492
7647
  const t = tableByName.get(name);
7493
- return t ? { columns: knownColumns(t), clientWrites: Boolean(t.client && (t.client.create !== null || t.client.update !== null || t.client.delete || t.client.intake !== void 0)), parentTarget: fileParentTarget(t) } : null;
7648
+ return t ? { columns: knownColumns(t), clientWrites: tableAllowsClientWrites(t), parentTarget: fileParentTarget(t) } : null;
7494
7649
  }, errors);
7495
7650
  }
7496
7651
  if (groupRoles || appRoles) errors.push(...validateDeclaredRoles(groupRoles, appRoles));
@@ -7601,7 +7756,10 @@ function declaredRolesFromSource(files) {
7601
7756
  if (source === void 0) return void 0;
7602
7757
  const parsed = extractSchemaTs(source);
7603
7758
  if (!parsed.ok || !parsed.declaration.groups && !parsed.declaration.appRoles) return void 0;
7604
- return JSON.stringify(canonicalRoleVocabulary(parsed.declaration.groups, parsed.declaration.appRoles));
7759
+ return JSON.stringify(canonicalRoleVocabulary(
7760
+ parsed.declaration.groups,
7761
+ parsed.declaration.appRoles?.map((role) => ({ name: role.name, grantableBy: role.grantableBy }))
7762
+ ));
7605
7763
  }
7606
7764
 
7607
7765
  // declared-data-vendor-entry.js
@@ -7619,8 +7777,8 @@ var checkerContract = __toESM(require_checker_contract());
7619
7777
  var import_runtime_types = __toESM(require_runtime_types());
7620
7778
  var import_generated_declarations = __toESM(require_generated_declarations());
7621
7779
  var import_generated_declarations_render = __toESM(require_generated_declarations_render());
7622
- function referenceCheckDeclaredTables(graphFiles) {
7623
- const authority = schemaAuthorityFromSource(graphFiles);
7780
+ function referenceCheckDeclaredTables(graphFiles, schemaOptions = {}) {
7781
+ const authority = schemaAuthorityFromSource(graphFiles, schemaOptions);
7624
7782
  return authority ? (0, import_runtime_types.declaredTablesDeclaration)(authority) : void 0;
7625
7783
  }
7626
7784
  function referenceCheckDeclarations(graphFiles) {