@somewhere-cloud/cli 0.38.16 → 0.38.18

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}`);
@@ -670,7 +684,7 @@ ${generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INS
670
684
  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}}
671
685
  const validateListLimit=input=>{if(input.limit!==undefined&&(!Number.isInteger(input.limit)||input.limit<1||input.limit>100))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"limit must be an integer from 1 to 100"})};
672
686
  let swReadSession=null,swInFlight=0,swStarts=0;const swReadSessionVoid=()=>{swReadSession=null;swStarts++};${generation >= 3 ? PUBLIC_BOOTSTRAP_CLIENT_RUNTIME : ""}const invoke=async(table,operation,input)=>{if(operation==="list"||operation==="relation_list")validateListLimit(input);${generation >= 3 ? PUBLIC_BOOTSTRAP_INVOKE : ""}const alone=swInFlight===0,start=++swStarts,isRead=operation==="list"||operation==="get"||operation==="aggregate"||operation==="relation_list";swInFlight++;let response;try{response=await fetch("/__sw/data",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json",...(swReadSession?{"X-Sw-Read-Session":swReadSession}:{})},body:JSON.stringify({...input,contract,table,operation})})}catch(error){if(!isRead)swReadSession=null;throw error}finally{swInFlight--}const next=response.headers&&typeof response.headers.get==="function"?response.headers.get("X-Sw-Read-Session"):null;if(!alone||swStarts!==start||swInFlight!==0)swReadSession=null;else if(next&&/^swrs1[.][A-Za-z0-9_-]+[.][A-Za-z0-9_-]+$/.test(next))swReadSession=next;else if(!isRead||!response.ok)swReadSession=null;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};
673
- ${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : "values=>invoke(table,operation,{values})"}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
687
+ ${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : '(values,...extra)=>{if(extra.length!==0)throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"create takes only the values to insert. Nothing was sent."});return invoke(table,operation,{values})}'}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
674
688
  export{data,DataError};
675
689
  `;
676
690
  return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration, ...insertReceiptCapability === void 0 ? {} : { insert_receipt: insertReceiptCapability } } };
@@ -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
 
@@ -949,6 +963,13 @@ interface SomewhereDbPlainInsertOptions { onConflict?: 'ignore' | 'update' | nul
949
963
  // signed-in user or visitor on a table keyed by id() with no blob() column. Any other insert
950
964
  // given a key refuses before anything runs. Eligible inserts without one get a fresh key.
951
965
  interface SomewhereDbInsertOptions extends SomewhereDbPlainInsertOptions { idempotencyKey?: string }
966
+ // A caller upsert runs under the table's declared rule: 'update' also needs update on the conflicting
967
+ // row, so a declared table whose rule never grants update (or a per-row shares table) takes 'ignore'
968
+ // only. A key never combines with onConflict.
969
+ type SomewhereDbConflict<T extends string> = __SomewhereDeclared<T, 'conflict', 'ignore' | 'update'>;
970
+ type SomewhereDbCallerInsertOptions<T extends string> =
971
+ | { onConflict?: SomewhereDbConflict<T> | null; idempotencyKey?: never }
972
+ | { onConflict?: null; idempotencyKey?: string };
952
973
  interface SomewhereDbReceipt { readonly replayed: boolean; readonly created_at: number; readonly result_until: number; readonly refuse_until: number }
953
974
  // Process-local: retry() repeats this exact request with the same key under the current
954
975
  // context's authority, until eight days after createdAt. It never runs by itself.
@@ -966,7 +987,7 @@ interface SomewhereDbRemove { where?: SomewhereDbWhere | null }
966
987
  // (insert values, update set/increment). With no declared tables every name and
967
988
  // value stays accepted, as before. Insert and Set pick caller or server identity.
968
989
  type __SomewhereDbIntentFor<T extends string, Insert extends 'insert' | 'serverInsert', Set extends 'set' | 'serverSet'> =
969
- | { op: 'insert'; table: T; values: __SomewhereDeclared<T, Insert, SomewhereDbValues>; options?: SomewhereDbPlainInsertOptions | null }
990
+ | { op: 'insert'; table: T; values: __SomewhereDeclared<T, Insert, SomewhereDbValues>; options?: (Insert extends 'insert' ? { onConflict?: SomewhereDbConflict<T> | null } : SomewhereDbPlainInsertOptions) | null }
970
991
  | ({ op: 'update'; table: T } & SomewhereDbUpdateFor<T, Set>)
971
992
  | { op: 'remove'; table: T; where?: SomewhereDbWhere | null };
972
993
  // sw.db.server.tx: server authority may name row identity (owner column, shared authorship).
@@ -1052,7 +1073,7 @@ interface SomewhereServerDb {
1052
1073
  }
1053
1074
  interface SomewhereCallerDb extends Omit<SomewhereServerDb, 'from' | 'count' | 'insert' | 'update' | 'tx'> {
1054
1075
  // The platform composes row identity from the verified user.
1055
- insert<T extends SomewhereDbTable>(table: T, values: __SomewhereDeclared<T, 'insert', SomewhereDbValues>, options?: SomewhereDbInsertOptions | null): Promise<SomewhereDbInsertResult<SomewhereDbRow<T>>>;
1076
+ insert<T extends SomewhereDbTable>(table: T, values: __SomewhereDeclared<T, 'insert', SomewhereDbValues>, options?: SomewhereDbCallerInsertOptions<T> | null): Promise<SomewhereDbInsertResult<SomewhereDbRow<T>>>;
1056
1077
  update<T extends SomewhereDbTable>(table: T, spec: SomewhereDbUpdateFor<T, 'set'>): Promise<SomewhereDbResult<SomewhereDbRow<T>>>;
1057
1078
  tx(intents: readonly SomewhereDbCallerWriteIntent[]): Promise<SomewhereDbResult[]>;
1058
1079
  from<T extends SomewhereDbTable, const C extends readonly SomewhereDbColumn<T>[] = readonly SomewhereDbColumn<T>[], const I extends readonly SomewhereDbRelationName<T>[] = []>(
@@ -1424,35 +1445,31 @@ interface SomewhereRuntimeAuthModeration {
1424
1445
  user: { id: string; email: string; role: SomewhereAuthRole };
1425
1446
  }>;
1426
1447
  }
1427
- // Replaces the inline mfa type in SomewhereRuntimeAuth (challenge/challengeWithCookie unchanged).
1448
+ // Email sign-in codes (tsk_8fe91eb5). Authenticator-app activation (reauthenticate*) answers 501 AUTH_FEATURE_UNAVAILABLE.
1449
+ type SomewhereMfaDelivery = 'sent' | 'pending' | 'unknown';
1450
+ interface SomewhereMfaCodeSent { challenge_id: string; delivery: SomewhereMfaDelivery; expires_in: number }
1451
+ type SomewhereMfaRecovery =
1452
+ | { removal_pending: true; removes_at: string; wait_seconds: number; notice_resent?: boolean }
1453
+ | { enrolled: false };
1428
1454
  interface SomewhereRuntimeAuthMfa {
1429
1455
  challenge(options: { mfa_token: string; code: string }): Promise<SomewhereAuthSession>;
1430
1456
  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 }>;
1457
+ resend(options: { mfa_token: string }): Promise<{ sent: boolean; delivery: SomewhereMfaDelivery; expires_in: number }>;
1458
+ enroll(options: { token: string }): Promise<SomewhereMfaCodeSent | { enrolled: true; method: 'email' }>;
1459
+ verify(options: { token: string; challenge_id: string; code: string }): Promise<{ enrolled: true; method: 'email' }>;
1460
+ unenroll(options: { token: string; password: string }): Promise<SomewhereMfaCodeSent | { enrolled: false }>;
1461
+ unenroll(options: { token: string; challenge_id: string; code: string }): Promise<{ enrolled: false }>;
1462
+ recover(options: { email: string; password: string }): Promise<SomewhereMfaRecovery>;
1463
+ cancelRecovery(options: { token: string }): Promise<{ outcome: 'cancelled' | 'already_closed' | 'completed'; message: string }>;
1464
+ reauthenticate(options: { token: string; enrollment_id: string; method: 'password' | 'email'; password?: string }): Promise<never>;
1465
+ verifyReauthentication(options: { token: string; enrollment_id: string; challenge_id: string; code: string }): Promise<never>;
1466
+ // The same steps for the request's own session (cookie or Bearer). Turning codes on or off ends no session.
1467
+ enrollWithCookie(req: Request): Promise<SomewhereMfaCodeSent | { enrolled: true; method: 'email' }>;
1468
+ verifyWithCookie(req: Request, options: { challenge_id: string; code: string }): Promise<{ enrolled: true; method: 'email' }>;
1469
+ unenrollWithCookie(req: Request, options: { password: string }): Promise<SomewhereMfaCodeSent | { enrolled: false }>;
1470
+ unenrollWithCookie(req: Request, options: { challenge_id: string; code: string }): Promise<{ enrolled: false }>;
1471
+ reauthenticateWithCookie(req: Request, options: { enrollment_id: string; method: 'password' | 'email'; password?: string }): Promise<never>;
1472
+ verifyReauthenticationWithCookie(req: Request, options: { enrollment_id: string; challenge_id: string; code: string }): Promise<never>;
1456
1473
  }
1457
1474
  interface SomewhereRuntimeAuth {
1458
1475
  signup(options: SomewhereAuthSignupOptions): Promise<SomewhereAuthIssuedSession>;
@@ -3831,11 +3848,26 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
3831
3848
  const find = (policy) => !policy ? null : policy.k === "o" ? policy.c : policy.k === "a" ? find(policy.p[0]) || find(policy.p[1]) : policy.k === "n" ? find(policy.w) : null;
3832
3849
  return find(table.policy);
3833
3850
  }
3851
+ function declaredRuleGrants(policy, op) {
3852
+ if (!policy) return true;
3853
+ if (policy.k === "o") return true;
3854
+ if (policy.k === "m") return !policy.m.o || policy.m.o.includes(op);
3855
+ if (policy.k === "a") return declaredRuleGrants(policy.p[0], op) || declaredRuleGrants(policy.p[1], op);
3856
+ if (policy.k === "n") return declaredRuleGrants(policy.g, op) && declaredRuleGrants(policy.w, op) && (op !== "update" || declaredRuleGrants(policy.g, "read") && declaredRuleGrants(policy.w, "read"));
3857
+ if (policy.k === "g" || policy.k === "r") return !!(policy.roles && policy.roles[op] && policy.roles[op].length);
3858
+ if (policy.k === "p") return declaredRuleGrants(policy.p, op);
3859
+ return true;
3860
+ }
3834
3861
  function declaredTablesDeclaration3(authority) {
3835
3862
  const schema = authority && authority.schema || {};
3836
3863
  const scopes = authority && authority.scopes || {};
3837
3864
  const names = Object.keys(schema).sort();
3838
3865
  if (!names.length) return "";
3866
+ const rowShares = /* @__PURE__ */ new Set();
3867
+ for (const name of names) {
3868
+ const policy = schema[name].policy, branch = policy && policy.k === "a" ? policy.p[1] : null;
3869
+ if (branch && branch.k === "m" && branch.m.og && branch.m.g.length === 2) rowShares.add(String(branch.m.m).toLowerCase());
3870
+ }
3839
3871
  const key = JSON.stringify;
3840
3872
  const shape = (fields) => fields.length ? "{ " + fields.join("; ") + " }" : "Record<string, never>";
3841
3873
  const union = (columns) => columns.length ? columns.map((column) => key(column.n)).join(" | ") : "never";
@@ -3853,7 +3885,7 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
3853
3885
  if (owner && !columns.some((column) => column.n === owner)) columns.push({ n: owner, t: "text", optional: false, identity: true, type: "string" });
3854
3886
  const declared = columns.filter((column) => !column.identity);
3855
3887
  const relations = (table.relations || []).filter((relation) => Object.prototype.hasOwnProperty.call(schema, relation.table)).map((relation) => key(relation.name) + ": SomewhereDeclaredTables[" + key(relation.table) + "]['row']" + (relation.kind === "belongsTo" ? " | null" : "[]"));
3856
- return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(columns.filter((column) => (!column.identity || column.n === owner) && column.t !== "json" && column.t !== "blob")) + ";\n relations: " + (relations.length ? shape(relations) : "{}") + ";\n };\n";
3888
+ return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(columns.filter((column) => (!column.identity || column.n === owner) && column.t !== "json" && column.t !== "blob")) + ";\n relations: " + (relations.length ? shape(relations) : "{}") + ";\n conflict: " + (rowShares.has(name.toLowerCase()) || !declaredRuleGrants(table.policy || (table.member ? { k: "m", m: table.member } : null), "update") ? "'ignore'" : "'ignore' | 'update'") + ";\n };\n";
3857
3889
  });
3858
3890
  return "interface SomewhereDeclaredTables {\n" + entries.join("") + "}\n";
3859
3891
  }
@@ -4044,6 +4076,7 @@ var require_schema_types = __commonJS({
4044
4076
  interface Scope { readonly __somewhereScope: unique symbol }
4045
4077
  interface OwnerScope extends Scope { readonly __somewhereOwner: unique symbol }
4046
4078
  interface MemberScope extends Scope { readonly __somewhereMember: unique symbol }
4079
+ interface GrantScope extends Scope { readonly __somewhereGrant: unique symbol }
4047
4080
  interface GroupScope extends Scope { readonly __somewhereGroup: unique symbol }
4048
4081
  interface AppRoleScope extends Scope { readonly __somewhereAppRole: unique symbol }
4049
4082
  interface AnyOfScope extends Scope { readonly __somewhereAnyOf: unique symbol }
@@ -4102,8 +4135,9 @@ function removed(): SomewhereSchemaDeclaration.ColumnMarker;
4102
4135
  function removedTable(): SomewhereSchemaDeclaration.TableMarker;
4103
4136
  function exported(): SomewhereSchemaDeclaration.TableMarker;
4104
4137
  `;
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;
4138
+ var policySignatures = `function grant(options: { table: string; grantee: string; row?: string; operations: readonly ['read'] | readonly ['read', 'update'] }): SomewhereSchemaDeclaration.GrantScope;
4139
+ function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, other: SomewhereSchemaDeclaration.GrantScope | SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope): SomewhereSchemaDeclaration.AnyOfScope;
4140
+ function anyOf(other: SomewhereSchemaDeclaration.GrantScope | SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.AnyOfScope;
4107
4141
  function allOf(gate: SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, who: SomewhereSchemaDeclaration.OwnerScope | SomewhereSchemaDeclaration.AnyOfScope): SomewhereSchemaDeclaration.Scope;
4108
4142
  function parent(options: { via: string; table?: string }): SomewhereSchemaDeclaration.Scope;
4109
4143
  function group(options?: { column?: string; roles?: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.GroupScope;
@@ -4306,6 +4340,9 @@ function isCanonicalBigintText(value) {
4306
4340
  }
4307
4341
 
4308
4342
  // worker/src/utils/db-schema-deploy/client-permissions.ts
4343
+ function tableAllowsClientWrites(table) {
4344
+ return Boolean(table.client && (table.client.create !== null || table.client.update !== null || table.client.delete || table.client.intake !== void 0));
4345
+ }
4309
4346
  function parseClientPermissions(value, columns, scope) {
4310
4347
  const fail = (message) => ({ ok: false, message });
4311
4348
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -4839,6 +4876,23 @@ function validateBindings(input, tableInfo, errors) {
4839
4876
  }
4840
4877
  }
4841
4878
 
4879
+ // worker/src/utils/authority/role-domain-map.ts
4880
+ var ROLE_NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
4881
+ 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]$/;
4882
+ function validRoleDomainMap(value) {
4883
+ 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));
4884
+ }
4885
+ var ROLE_DOMAIN_MAP_JS = `
4886
+ function __sw_structRoleDomains(value) {
4887
+ if (!value || typeof value !== 'object' || __sw_arrayIsArray(value)) return false;
4888
+ const roles = __sw_keys(value);
4889
+ for (let i = 0; i < roles.length; i++) {
4890
+ const domain = value[roles[i]];
4891
+ if (!__sw_reMatch(${ROLE_NAME_PATTERN}, roles[i]) || typeof domain !== 'string' || !__sw_reMatch(${EMAIL_DOMAIN_PATTERN}, domain)) return false;
4892
+ }
4893
+ return true;
4894
+ }`;
4895
+
4842
4896
  // worker/src/utils/authority/scope-enforcement.ts
4843
4897
  var SAFE_SCOPE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
4844
4898
  var GROUP_ROLE_NAME = /^[a-z][a-z0-9_]{0,63}$/;
@@ -4859,7 +4913,9 @@ function validBakedMemberScope(value) {
4859
4913
  const operations = member.o;
4860
4914
  const operationOrder = ["read", "create", "update", "delete"];
4861
4915
  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);
4916
+ const g = member.g, mg = member.mg;
4917
+ 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"]');
4918
+ 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
4919
  }
4864
4920
  var MAX_PARENT_EDGES = 4;
4865
4921
  function pinShape(policy) {
@@ -4873,7 +4929,7 @@ function sameAllOfRelation(gate, x) {
4873
4929
  if (x.k !== "m" || gate.k !== "m") return false;
4874
4930
  const a = gate.m;
4875
4931
  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]));
4932
+ 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
4933
  }
4878
4934
  function validBakedPolicy(value, root = true, edges = 0) {
4879
4935
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
@@ -4886,7 +4942,7 @@ function validBakedPolicy(value, root = true, edges = 0) {
4886
4942
  }
4887
4943
  if (node.k === "a") {
4888
4944
  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));
4945
+ 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
4946
  }
4891
4947
  if (root && node.k === "n") {
4892
4948
  if (!ALLOF_POLICY_ADMITTED || !exactKeys(node, ["k", "g", "w"])) return false;
@@ -4894,7 +4950,7 @@ function validBakedPolicy(value, root = true, edges = 0) {
4894
4950
  const who = node.w;
4895
4951
  if (!gate || !who) return false;
4896
4952
  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);
4953
+ const gateValid = gate.k === "m" ? validBakedPolicy(gate, false) && gateMember?.o !== void 0 && !gateMember.og : (gate.k === "g" || gate.k === "r") && validBakedPolicy(gate, true);
4898
4954
  if (!gateValid) return false;
4899
4955
  if (who.k === "o") return validBakedPolicy(who, false);
4900
4956
  const whoBranches = who.p;
@@ -4912,7 +4968,9 @@ function validBakedPolicy(value, root = true, edges = 0) {
4912
4968
  return exactKeys(node, ["k", "c", "roles", "generation"]) && ident(node.c) && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
4913
4969
  }
4914
4970
  if (root && node.k === "r") {
4915
- return exactKeys(node, ["k", "roles", "generation"]) && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
4971
+ const domains = node.d;
4972
+ const validDomains = domains === void 0 || validRoleDomainMap(domains);
4973
+ return (exactKeys(node, ["k", "roles", "generation"]) || exactKeys(node, ["k", "roles", "generation", "d"])) && validDomains && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
4916
4974
  }
4917
4975
  return false;
4918
4976
  }
@@ -5033,7 +5091,8 @@ function bakedPolicyFromDeclared(value, depth = 0, edges = 0, underParent = fals
5033
5091
  if (depth !== 0 && !underParent || node.policies[0]?.kind !== "owner" || second !== "member" && second !== "group" && second !== "app_role") return null;
5034
5092
  const left = bakedPolicyFromDeclared(node.policies[0], depth + 1);
5035
5093
  const right = bakedPolicyFromDeclared(node.policies[1], depth + 1, edges, second !== "member");
5036
- return left && right ? { k: "a", p: [left, right] } : null;
5094
+ const baked = left && right ? { k: "a", p: [left, right] } : null;
5095
+ return baked && validBakedPolicy(pinShape(baked), true, edges) ? baked : null;
5037
5096
  }
5038
5097
  if (node.kind === "all_of") {
5039
5098
  if (depth !== 0 && !underParent) return null;
@@ -5125,7 +5184,143 @@ function bakedMemberFromDeclared(scope, explicitOperations = false) {
5125
5184
  if (JSON.stringify(operations) !== JSON.stringify(allowed.filter((operation) => operations.includes(operation))) || !operations.includes("read") && (operations.includes("update") || operations.includes("delete"))) return null;
5126
5185
  o = operations.length === allowed.length && !explicitOperations ? void 0 : [...operations];
5127
5186
  }
5128
- return { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg, ...o !== void 0 ? { o } : {} };
5187
+ if (s.ownerGrant !== void 0 && s.ownerGrant !== true) return null;
5188
+ const member = { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg, ...o !== void 0 ? { o } : {}, ...s.ownerGrant ? { og: true } : {} };
5189
+ return validBakedMemberScope(member) ? member : null;
5190
+ }
5191
+
5192
+ // worker/src/utils/db-schema-deploy/extract-schema-grants.ts
5193
+ function readGrantScope(r, table, line, fail) {
5194
+ r.expectPunct("(", "after grant");
5195
+ r.expectPunct("{", "in grant options");
5196
+ let membership, grantee, row;
5197
+ let operations;
5198
+ const seen = /* @__PURE__ */ new Set();
5199
+ while (!r.tryPunct("}")) {
5200
+ const key = r.readKey("in grant options");
5201
+ if (seen.has(key.name)) fail(key.line, `Duplicate grant option "${key.name}".`);
5202
+ seen.add(key.name);
5203
+ r.expectPunct(":", "after grant option");
5204
+ if (key.name === "table" || key.name === "grantee" || key.name === "row") {
5205
+ const value = r.readLiteral("in grant options");
5206
+ 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.");
5207
+ if (key.name === "table") membership = value.toLowerCase();
5208
+ else if (key.name === "grantee") grantee = value.toLowerCase();
5209
+ else row = value.toLowerCase();
5210
+ } else if (key.name === "operations") {
5211
+ const ops = [];
5212
+ r.expectPunct("[", "in grant operations");
5213
+ while (!r.tryPunct("]")) {
5214
+ const op = r.readLiteral("in grant operations");
5215
+ if (op !== "read" && op !== "update") fail(key.line, "grant operations allow only read and update.");
5216
+ if (ops.includes(op)) fail(key.line, "Duplicate grant operation.");
5217
+ ops.push(op);
5218
+ if (!r.tryPunct(",")) {
5219
+ r.expectPunct("]", "closing grant operations");
5220
+ break;
5221
+ }
5222
+ }
5223
+ if (!ops.includes("read")) fail(key.line, "grant operations require read; update alone is not allowed.");
5224
+ operations = ops.includes("update") ? ["read", "update"] : ["read"];
5225
+ } else fail(key.line, `Unknown grant option "${key.name}". Allowed: table, grantee, row, operations.`);
5226
+ if (!r.tryPunct(",")) {
5227
+ r.expectPunct("}", "closing grant options");
5228
+ break;
5229
+ }
5230
+ }
5231
+ r.tryPunct(",");
5232
+ r.expectPunct(")", "closing grant");
5233
+ 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.`);
5234
+ 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.`);
5235
+ return { kind: "member", group: ["user_id"], membership, memberUser: grantee, memberGroup: row === void 0 ? ["user_id"] : ["user_id", row], operations, ownerGrant: true };
5236
+ }
5237
+ function resolveGrantRowKeys(scope, key, table, line, fail) {
5238
+ const visit = (node) => {
5239
+ if (node.kind === "member" && node.ownerGrant && node.memberGroup.length === 2 && node.group.length === 1) {
5240
+ 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.`);
5241
+ node.group = ["user_id", key.toLowerCase()];
5242
+ } else if (node.kind === "any_of") node.policies.forEach(visit);
5243
+ else if (node.kind === "all_of") {
5244
+ visit(node.gate);
5245
+ visit(node.who);
5246
+ }
5247
+ };
5248
+ if (scope.kind === "policy") visit(scope.policy);
5249
+ }
5250
+ function validateDeclaredMemberships(tables, tableByName, knownColumns, errors) {
5251
+ const memberPolicies = (scope) => {
5252
+ const out = [];
5253
+ const visit = (node) => {
5254
+ if (node.kind === "member") out.push(node);
5255
+ else if (node.kind === "any_of") node.policies.forEach(visit);
5256
+ else if (node.kind === "all_of") {
5257
+ visit(node.gate);
5258
+ visit(node.who);
5259
+ }
5260
+ };
5261
+ if (scope.kind === "member") out.push(scope);
5262
+ else if (scope.kind === "policy") visit(scope.policy);
5263
+ return out;
5264
+ };
5265
+ for (const t of tables) {
5266
+ for (const sc of memberPolicies(t.scope)) {
5267
+ const own = knownColumns(t);
5268
+ for (const g of sc.group) {
5269
+ if (!own.has(g)) {
5270
+ errors.push(
5271
+ `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.`
5272
+ );
5273
+ }
5274
+ }
5275
+ const membershipTable = tableByName.get(sc.membership);
5276
+ if (!membershipTable) {
5277
+ errors.push(
5278
+ `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.`
5279
+ );
5280
+ continue;
5281
+ }
5282
+ const membershipCols = knownColumns(membershipTable);
5283
+ if (sc.ownerGrant) {
5284
+ if (membershipTable.scope.kind !== "owner" || membershipTable.scope.column !== "user_id" || membershipTable.scope.visitors) {
5285
+ errors.push(`Grant table "${sc.membership}" must use owner() with managed user_id and authenticated owners.`);
5286
+ }
5287
+ 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.`);
5288
+ const grantee = membershipTable.columns.find((column) => column.name === sc.memberUser);
5289
+ if (!grantee || grantee.helper !== "text" || grantee.nullable) errors.push(`Grant grantee "${sc.membership}"."${sc.memberUser}" must be a required text column.`);
5290
+ if (sc.memberGroup.length === 2) {
5291
+ const row = membershipTable.columns.find((column) => column.name === sc.memberGroup[1]);
5292
+ const key = t.columns.find((column) => column.name === sc.group[1]);
5293
+ const keyHelper = key?.helper === "id" ? key.uuid ? "text" : "integer" : null;
5294
+ if (!row || row.nullable || row.helper !== keyHelper) {
5295
+ errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" must be a required ${keyHelper ?? "id-typed"} column holding a "${t.name}" id.`);
5296
+ } else if (row.hasDefault || row.references !== t.name || row.onDelete !== "cascade") {
5297
+ 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' }).`);
5298
+ }
5299
+ if (membershipTable.client?.update?.includes(sc.memberGroup[1])) {
5300
+ 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.`);
5301
+ }
5302
+ const users = tables.filter((other) => memberPolicies(other.scope).some((m) => m.ownerGrant && m.membership === sc.membership));
5303
+ if (users.length !== 1 || memberPolicies(t.scope).filter((m) => m.ownerGrant && m.membership === sc.membership).length !== 1) {
5304
+ 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.`);
5305
+ }
5306
+ }
5307
+ } else if (tableAllowsClientWrites(membershipTable)) {
5308
+ errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
5309
+ }
5310
+ if (!membershipCols.has(sc.memberUser)) {
5311
+ errors.push(
5312
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
5313
+ );
5314
+ }
5315
+ for (const mg of sc.ownerGrant ? [] : sc.memberGroup) {
5316
+ if (!membershipCols.has(mg)) {
5317
+ errors.push(
5318
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
5319
+ );
5320
+ }
5321
+ }
5322
+ }
5323
+ }
5129
5324
  }
5130
5325
 
5131
5326
  // worker/src/utils/db-schema-deploy/schema-constants.ts
@@ -6219,8 +6414,9 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
6219
6414
  }
6220
6415
 
6221
6416
  // worker/src/utils/db-schema-deploy/extract-schema-roles.ts
6417
+ var EMAIL_DOMAIN = EMAIL_DOMAIN_PATTERN;
6222
6418
  var GROUP_KEYS = ["roles", "adminGrantableRoles"];
6223
- var APP_ROLE_KEYS = ["grantableBy"];
6419
+ var APP_ROLE_KEYS = ["grantableBy", "emailDomain"];
6224
6420
  function readStringList2(r, fail, ctx) {
6225
6421
  const open = r.peek();
6226
6422
  if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted role names, such as ['viewer'].`);
@@ -6264,7 +6460,7 @@ function readAppRoles(r, fail) {
6264
6460
  const key = r.readKey("for an app role name");
6265
6461
  if (out.some((role2) => role2.name === key.name)) fail(key.line, `app role "${key.name}" is declared twice.`);
6266
6462
  r.expectPunct(":", `after app role "${key.name}"`);
6267
- r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...] }`);
6463
+ r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...], emailDomain: '...' }`);
6268
6464
  const role = { name: key.name, grantableBy: [] };
6269
6465
  const seen = /* @__PURE__ */ new Set();
6270
6466
  while (!r.tryPunct("}")) {
@@ -6275,7 +6471,16 @@ function readAppRoles(r, fail) {
6275
6471
  if (seen.has(opt.name)) fail(opt.line, `app role "${key.name}" declares ${opt.name} twice.`);
6276
6472
  seen.add(opt.name);
6277
6473
  r.expectPunct(":", `after appRoles.${key.name}.${opt.name}`);
6278
- role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
6474
+ if (opt.name === "emailDomain") {
6475
+ const t = r.next();
6476
+ if (t.kind !== "string") fail(t.line, `appRoles.${key.name}.emailDomain must be one quoted domain, such as 'example.com'.`);
6477
+ if (!EMAIL_DOMAIN.test(t.value)) {
6478
+ 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.`);
6479
+ }
6480
+ role.emailDomain = t.value;
6481
+ } else {
6482
+ role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
6483
+ }
6279
6484
  if (!r.tryPunct(",")) {
6280
6485
  r.expectPunct("}", `closing app role "${key.name}"`);
6281
6486
  break;
@@ -6295,18 +6500,31 @@ function canonicalRoleVocabulary(groups, appRoles) {
6295
6500
  v: 1,
6296
6501
  groupRoles: sorted2(groups?.roles ?? []),
6297
6502
  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) }))
6503
+ 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
6504
  };
6300
6505
  }
6301
6506
  function validateDeclaredRoles(groups, appRoles) {
6302
6507
  const delegation = /* @__PURE__ */ Object.create(null);
6303
6508
  for (const role of appRoles ?? []) delegation[role.name] = role.grantableBy;
6304
- return roleVocabularyErrors({
6509
+ return [...emailDomainRoleErrors(appRoles ?? []), ...roleVocabularyErrors({
6305
6510
  groupRoles: groups?.roles ?? [],
6306
6511
  adminGrantableGroupRoles: groups?.adminGrantableRoles ?? [],
6307
6512
  appRoles: (appRoles ?? []).map((role) => role.name),
6308
6513
  appRoleDelegation: delegation
6309
- });
6514
+ })];
6515
+ }
6516
+ function emailDomainRoleErrors(appRoles) {
6517
+ const errors = [];
6518
+ const byDomain = new Set(appRoles.filter((role) => role.emailDomain !== void 0).map((role) => role.name));
6519
+ for (const role of appRoles) {
6520
+ if (role.emailDomain !== void 0 && !EMAIL_DOMAIN.test(role.emailDomain)) errors.push(`appRoles.${role.name}.emailDomain is not one lowercase domain.`);
6521
+ for (const grantor of role.grantableBy) {
6522
+ if (byDomain.has(grantor)) {
6523
+ 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.`);
6524
+ }
6525
+ }
6526
+ }
6527
+ return errors;
6310
6528
  }
6311
6529
 
6312
6530
  // worker/src/utils/db-schema-deploy/extract-schema-options.ts
@@ -6396,6 +6614,7 @@ var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
6396
6614
  ...TABLE_MARKER_HELPERS,
6397
6615
  "schema",
6398
6616
  "table",
6617
+ "grant",
6399
6618
  "removed",
6400
6619
  "hasMany",
6401
6620
  "belongsTo",
@@ -6510,10 +6729,10 @@ function tokenize(source) {
6510
6729
  var Reader = class {
6511
6730
  constructor(tokens, options = {}) {
6512
6731
  this.tokens = tokens;
6513
- this.bareIdIsText = !options.legacyIntegerIdDefault;
6732
+ this.bareIdIsText = (table) => !options.legacyIntegerIdDefault && !options.integerKeyTables?.has(table.toLowerCase());
6514
6733
  }
6515
6734
  pos = 0;
6516
- /** What a bare id() declares: a random text key, or (legacy retained source) an integer. */
6735
+ /** What a bare id() on this table declares: a random text key, or an integer (ExtractSchemaOptions). */
6517
6736
  bareIdIsText;
6518
6737
  peek() {
6519
6738
  return this.tokens[this.pos];
@@ -6681,7 +6900,7 @@ function readColumn(r, tableName, rawName, line) {
6681
6900
  const col = {
6682
6901
  name,
6683
6902
  helper,
6684
- uuid: helper === "id" && r.bareIdIsText,
6903
+ uuid: helper === "id" && r.bareIdIsText(tableName),
6685
6904
  nullable: false,
6686
6905
  hasDefault: false,
6687
6906
  default: null,
@@ -6918,7 +7137,7 @@ function readPolicyMember(r, tableName, line) {
6918
7137
  if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
6919
7138
  return scope;
6920
7139
  }
6921
- var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), group() or appRole().";
7140
+ var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), grant(), group() or appRole().";
6922
7141
  function readAnyOfBranch(r, tableName, which) {
6923
7142
  const tok = r.expectIdent(`for the ${which} policy in anyOf() on table "${tableName}"`);
6924
7143
  const failAt = (at, message) => {
@@ -6926,6 +7145,7 @@ function readAnyOfBranch(r, tableName, which) {
6926
7145
  };
6927
7146
  if (tok.value === "owner") return readPolicyOwner(r, tableName, tok.line);
6928
7147
  if (tok.value === "member") return readPolicyMember(r, tableName, tok.line);
7148
+ if (tok.value === "grant") return readGrantScope(r, tableName, tok.line, failAt);
6929
7149
  if (tok.value === "group") return readGroupScope(r, tableName, tok.line, failAt);
6930
7150
  if (tok.value === "appRole") return readAppRoleScope(r, tableName, tok.line, failAt);
6931
7151
  throw new SchemaTsError(`line ${tok.line}: ${ANY_OF_FORMS}`);
@@ -6941,6 +7161,7 @@ function readAnyOfScope(r, tableName, line) {
6941
7161
  if (owners.length !== 1) throw new SchemaTsError(`line ${line}: ${ANY_OF_FORMS}`);
6942
7162
  const owner = owners[0];
6943
7163
  const other = firstPolicy.kind === "owner" ? secondPolicy : firstPolicy;
7164
+ if (other.kind === "member" && other.ownerGrant && owner.column !== DEFAULT_OWNER_COLUMN) throw new SchemaTsError(`line ${line}: grant() requires owner() with managed user_id.`);
6944
7165
  return { kind: "policy", policy: { kind: "any_of", policies: [owner, other] } };
6945
7166
  }
6946
7167
  var ALL_OF_FORMS = "allOf() takes a gate \u2014 member(), group() or appRole() \u2014 and then owner() or anyOf(owner(), ...).";
@@ -6968,6 +7189,7 @@ function readAllOfScope(r, tableName, line) {
6968
7189
  r.tryPunct(",");
6969
7190
  r.expectPunct(")", "closing allOf()");
6970
7191
  const x = who.kind === "any_of" ? who.policies[1] : null;
7192
+ if (x?.kind === "member" && x.ownerGrant) throw new SchemaTsError(`line ${line}: grant() belongs in anyOf(owner(), grant()), not allOf().`);
6971
7193
  for (const node of [gate, ...x ? [x] : []]) {
6972
7194
  if (node.kind !== "member") continue;
6973
7195
  if (!explicitMemberOperations.has(node)) {
@@ -7203,6 +7425,9 @@ function readTable(r, rawName, nameLine) {
7203
7425
  if (key.name === "scope") {
7204
7426
  if (scope) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "scope" twice.`);
7205
7427
  scope = readScope(r, rawName);
7428
+ resolveGrantRowKeys(scope, idCols[0]?.name, rawName, key.line, (at, message) => {
7429
+ throw new SchemaTsError(`line ${at}: ${message}`);
7430
+ });
7206
7431
  } else if (key.name === "indexes") {
7207
7432
  if (indexEntries) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "indexes" twice.`);
7208
7433
  indexEntries = readColumnLists(r, rawName, "indexes");
@@ -7434,55 +7659,7 @@ function extractSchemaTs(source, options = {}) {
7434
7659
  };
7435
7660
  resolveAppRoleScopes(tables, appRoles, errors);
7436
7661
  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
- }
7662
+ validateDeclaredMemberships(tables, tableByName, knownColumns, errors);
7486
7663
  validateDeclaredRelations(tables, tableByName, knownColumns, (table) => removedTables.includes(table) ? "removedTable()" : exportedTables.includes(table) ? "exported()" : null, errors);
7487
7664
  validateRowLinks(tables, tableByName, errors);
7488
7665
  validateIntake(tables, tableByName, errors);
@@ -7490,7 +7667,7 @@ function extractSchemaTs(source, options = {}) {
7490
7667
  errors.push(...fileErrors);
7491
7668
  validateFileCollections(fileCollections, (name) => {
7492
7669
  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;
7670
+ return t ? { columns: knownColumns(t), clientWrites: tableAllowsClientWrites(t), parentTarget: fileParentTarget(t) } : null;
7494
7671
  }, errors);
7495
7672
  }
7496
7673
  if (groupRoles || appRoles) errors.push(...validateDeclaredRoles(groupRoles, appRoles));
@@ -7601,7 +7778,10 @@ function declaredRolesFromSource(files) {
7601
7778
  if (source === void 0) return void 0;
7602
7779
  const parsed = extractSchemaTs(source);
7603
7780
  if (!parsed.ok || !parsed.declaration.groups && !parsed.declaration.appRoles) return void 0;
7604
- return JSON.stringify(canonicalRoleVocabulary(parsed.declaration.groups, parsed.declaration.appRoles));
7781
+ return JSON.stringify(canonicalRoleVocabulary(
7782
+ parsed.declaration.groups,
7783
+ parsed.declaration.appRoles?.map((role) => ({ name: role.name, grantableBy: role.grantableBy }))
7784
+ ));
7605
7785
  }
7606
7786
 
7607
7787
  // declared-data-vendor-entry.js
@@ -7619,8 +7799,8 @@ var checkerContract = __toESM(require_checker_contract());
7619
7799
  var import_runtime_types = __toESM(require_runtime_types());
7620
7800
  var import_generated_declarations = __toESM(require_generated_declarations());
7621
7801
  var import_generated_declarations_render = __toESM(require_generated_declarations_render());
7622
- function referenceCheckDeclaredTables(graphFiles) {
7623
- const authority = schemaAuthorityFromSource(graphFiles);
7802
+ function referenceCheckDeclaredTables(graphFiles, schemaOptions = {}) {
7803
+ const authority = schemaAuthorityFromSource(graphFiles, schemaOptions);
7624
7804
  return authority ? (0, import_runtime_types.declaredTablesDeclaration)(authority) : void 0;
7625
7805
  }
7626
7806
  function referenceCheckDeclarations(graphFiles) {