@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.
@@ -105,22 +105,26 @@ var require_declared_data_contract = __commonJS({
105
105
  if (value.k === "o" && keys === "c,k" && identifier.test(value.c)) return { k: "o", c: value.c };
106
106
  if (value.k === "m" && keys === "k,m") {
107
107
  record(value.m, label + ".member");
108
- const { g, m, u, mg, o } = value.m;
108
+ const { g, m, u, mg, o, og } = value.m;
109
109
  const memberKeys = Object.keys(value.m).sort().join(",");
110
- 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);
110
+ 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"]');
111
+ 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);
111
112
  const normalizedOperations = o?.length === 4 ? void 0 : o;
112
- return { k: "m", m: { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {} } };
113
+ return { k: "m", m: { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {}, ...og ? { og: true } : {} } };
113
114
  }
114
115
  if (value.k === "a" && keys === "k,p" && Array.isArray(value.p) && value.p.length === 2) {
115
116
  const left = policy(value.p[0], label), right = policy(value.p[1], label);
116
117
  if (left.k !== "o" || !["m", "g", "r"].includes(right.k)) fail(label);
118
+ if (right.k === "m" && right.m.og && left.c !== "user_id") fail(label);
117
119
  return { k: "a", p: [left, right] };
118
120
  }
119
121
  if (value.k === "n" && keys === "g,k,w") {
120
122
  const gate = policy(value.g, label), who = policy(value.w, label);
121
123
  if (!["m", "g", "r"].includes(gate.k) || !["o", "a"].includes(who.k)) fail(label);
124
+ if (gate.k === "m" && gate.m.og) fail(label);
122
125
  if (who.k === "a") {
123
126
  const x = who.p[1];
127
+ if (x.k === "m" && x.m.og) fail(label);
124
128
  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()));
125
129
  if (!same) fail(label);
126
130
  }
@@ -151,14 +155,14 @@ var require_declared_data_contract = __commonJS({
151
155
  }
152
156
  return roles;
153
157
  }
154
- function policyMembershipTables(value, out) {
158
+ function policyMembershipTables(value, out, grants) {
155
159
  if (!value || typeof value !== "object") return;
156
- if (value.k === "m" && value.m) out.add(value.m.m);
157
- else if (value.k === "a") value.p.forEach((item) => policyMembershipTables(item, out));
160
+ if (value.k === "m" && value.m) (value.m.og === true ? grants : out).add(value.m.m);
161
+ else if (value.k === "a") value.p.forEach((item) => policyMembershipTables(item, out, grants));
158
162
  else if (value.k === "n") {
159
- policyMembershipTables(value.g, out);
160
- policyMembershipTables(value.w, out);
161
- } else if (value.k === "p") policyMembershipTables(value.p, out);
163
+ policyMembershipTables(value.g, out, grants);
164
+ policyMembershipTables(value.w, out, grants);
165
+ } else if (value.k === "p") policyMembershipTables(value.p, out, grants);
162
166
  }
163
167
  function validateInsertReceiptCapability(value) {
164
168
  if (value !== void 0 && value !== "insert-v1") fail("insert receipt capability");
@@ -171,7 +175,17 @@ var require_declared_data_contract = __commonJS({
171
175
  record(scopes, "scopes");
172
176
  const tables = [];
173
177
  const membershipTables = new Set(Object.values(schema).filter((table) => table && table.member).map((table) => table.member.m));
174
- for (const table of Object.values(schema)) if (table?.policy) policyMembershipTables(table.policy, membershipTables);
178
+ const grantTables = /* @__PURE__ */ new Set();
179
+ for (const table of Object.values(schema)) if (table?.policy) policyMembershipTables(table.policy, membershipTables, grantTables);
180
+ for (const name of grantTables) {
181
+ const grant = schema[name];
182
+ 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`);
183
+ }
184
+ 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 }] : []);
185
+ for (const rule of grantRules.filter((rule2) => rule2.m.g.length === 2)) {
186
+ const shares = schema[rule.m.m], row = rule.m.mg[1];
187
+ 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`);
188
+ }
175
189
  for (const name of Object.keys(schema).sort()) {
176
190
  const table = schema[name];
177
191
  record(table, `table ${name}`);
@@ -715,7 +729,7 @@ ${generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INS
715
729
  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}}
716
730
  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"})};
717
731
  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};
718
- ${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)})]])` : ""}));
732
+ ${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)})]])` : ""}));
719
733
  export{data,DataError};
720
734
  `;
721
735
  return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration, ...insertReceiptCapability === void 0 ? {} : { insert_receipt: insertReceiptCapability } } };
@@ -1244,6 +1258,9 @@ var VERIFY_SIGNUP_FETCH_PROLOGUE = " const verifySignup = await __sw_captureV
1244
1258
  var SENSITIVE_PARAMS = [
1245
1259
  "access_token",
1246
1260
  "refresh_token",
1261
+ // OAuth callbacks carry one-use credentials even when validation refuses them.
1262
+ "code",
1263
+ "state",
1247
1264
  // A creator or link secret belongs only in a request body; a stray query copy is never kept.
1248
1265
  "creator_secret",
1249
1266
  "secret",
@@ -1260,7 +1277,7 @@ var SENSITIVE_PARAMS = [
1260
1277
  "dsn",
1261
1278
  "password"
1262
1279
  ];
1263
- var PARAM_RE = new RegExp(`([?&](?:${SENSITIVE_PARAMS.join("|")})=)[^&\\s]*`, "gi");
1280
+ var SENSITIVE_PARAM_NAMES = new Set(SENSITIVE_PARAMS);
1264
1281
 
1265
1282
  // shared/draft-execution-authority.ts
1266
1283
  var DRAFT_PLATFORM_SERVICE_BINDING = "PLATFORM_API";
@@ -1526,9 +1543,15 @@ var DB_TYPED_ERRORS_JS = ` // The REST bridge's refusal as the caller sees
1526
1543
  // because one row targets a group the caller cannot write to.
1527
1544
  const guard = msg.match(/SW_(MEMBERSHIP_REQUIRED|POLICY_AUTHORITY_REQUIRED)/);
1528
1545
  if (guard) {
1529
- const refused = new Error(guard[1] === 'MEMBERSHIP_REQUIRED'
1546
+ // A single write raises it too: a per-row share's guard (query-sql.ts grantRow) or a parent()
1547
+ // move only a grant() would allow.
1548
+ const refused = new Error(label !== 'sw.db.tx' && guard[1] === 'POLICY_AUTHORITY_REQUIRED'
1549
+ ? label + ': the access policy of this table does not permit this change by the signed-in user. Nothing was written.'
1550
+ : label !== 'sw.db.tx'
1551
+ ? label + ': this row targets a group the signed-in user is not a member of. Nothing was written.'
1552
+ : guard[1] === 'MEMBERSHIP_REQUIRED'
1530
1553
  ? 'sw.db.tx: an insert targets a group the signed-in user is not a member of. Nothing in the transaction was written.'
1531
- : 'sw.db.tx: the access policy of this table does not permit an insert by the signed-in user. Nothing in the transaction was written.');
1554
+ : 'sw.db.tx: the access policy of a table does not permit one of these changes by the signed-in user. Nothing in the transaction was written.');
1532
1555
  refused.code = guard[1];
1533
1556
  refused.status = 403;
1534
1557
  refused.__sw_expose = true;
@@ -2802,9 +2825,54 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2802
2825
  const media = /^\\/v1\\/(?:ai\\/(?:tts|generate-image|remove-background)|render\\/(?:screenshot|pdf)|web\\/scrape|browser\\/test)$/.test(path);
2803
2826
  const mutation = state && media && method === 'POST';
2804
2827
  if (mutation) { state.generation++; state.pending++; state.envelope = null; }
2805
- try { return await __sw_platformFetchInner(path, opts); }
2828
+ const signal = opts && opts.signal;
2829
+ const stateForDeadline = signal ? { dispatched: false } : null;
2830
+ const started = signal ? Date.now() : 0;
2831
+ const preparedOpts = signal ? Object.assign({}, opts, { __sw_deadlineState: stateForDeadline }) : opts;
2832
+ try { return await __sw_platformFetchInner(path, preparedOpts); }
2833
+ catch (err) {
2834
+ if (signal && !stateForDeadline.dispatched && err && err.code === 'PLATFORM_CALL_CANCELLED') {
2835
+ __sw_recordPlatformCall(path, started, undefined);
2836
+ }
2837
+ throw err;
2838
+ }
2806
2839
  finally { if (mutation) { state.pending--; state.envelope = null; } }
2807
2840
  }
2841
+ // Cancellation stops waiting; a request already sent may still complete.
2842
+ // Nothing is retried, and cancelled preparation never starts a request.
2843
+ function __sw_platformBound(signal, work, state) {
2844
+ if (!signal) return work();
2845
+ function cancelled() {
2846
+ const err = new Error('The platform call stopped waiting. Check its outcome before trying again.');
2847
+ err.code = 'PLATFORM_CALL_CANCELLED';
2848
+ err.status = 504;
2849
+ err.retry = false;
2850
+ err.outcome = state && state.dispatched ? 'unknown' : 'not_attempted';
2851
+ return err;
2852
+ }
2853
+ if (signal.aborted) return Promise.reject(cancelled());
2854
+ return new Promise(function (resolve, reject) {
2855
+ let settled = false;
2856
+ function finish(fn, value) {
2857
+ if (settled) return;
2858
+ settled = true;
2859
+ signal.removeEventListener('abort', abort);
2860
+ fn(value);
2861
+ }
2862
+ function abort() { finish(reject, cancelled()); }
2863
+ signal.addEventListener('abort', abort, { once: true });
2864
+ if (signal.aborted) { abort(); return; }
2865
+ try { Promise.resolve(work()).then(function (value) { finish(resolve, value); }, function (err) { finish(reject, err); }); }
2866
+ catch (err) { finish(reject, err); }
2867
+ });
2868
+ }
2869
+ async function __sw_platformJSONBody(response, signal) {
2870
+ try { return await __sw_platformBound(signal, function () { return response.json(); }, { dispatched: true }); }
2871
+ catch (err) {
2872
+ if (err && err.code === 'PLATFORM_CALL_CANCELLED') throw err;
2873
+ return null;
2874
+ }
2875
+ }
2808
2876
  async function __sw_platformFetchInner(path, opts) {
2809
2877
  opts = opts || {};
2810
2878
  if (path === '/v1/db/query' || path === '/v1/db/batch' || path === '/v1/db/raw-read') {
@@ -2816,7 +2884,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2816
2884
  const __sw_spanStart = Date.now();
2817
2885
  // Inside a job or cron delivery every platform call names its job, so the
2818
2886
  // platform refuses it once that job has ended (middleware/job-invocation-fence.ts).
2819
- const __sw_jobMarker = opts.__swFenceExempt ? null : await __sw_jobInvocationMarker();
2887
+ const __sw_jobMarker = opts.__swFenceExempt ? null : await __sw_platformBound(opts.signal, function () { return __sw_jobInvocationMarker(); }, opts.__sw_deadlineState);
2820
2888
  const headers = {
2821
2889
  ...(opts.headers || {}),
2822
2890
  ...(__sw_jobMarker || {}),
@@ -2860,12 +2928,15 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2860
2928
  __sw_fetchStart = Date.now();
2861
2929
  __sw_prepMs = __sw_fetchStart - __sw_spanStart;
2862
2930
  }
2863
- __sw_response = await __sw_runtimeFetch(platformBase + path, {
2931
+ __sw_response = await __sw_platformBound(opts.signal, function () {
2932
+ if (opts.__sw_deadlineState) opts.__sw_deadlineState.dispatched = true;
2933
+ return __sw_runtimeFetch(platformBase + path, {
2864
2934
  method: __sw_method,
2865
2935
  headers,
2866
2936
  body: opts.body,
2867
2937
  ...(opts.signal ? { signal: opts.signal } : {}),
2868
- });
2938
+ });
2939
+ }, opts.__sw_deadlineState);
2869
2940
  if (__sw_serverTiming) __sw_fetchMs = Date.now() - __sw_fetchStart;
2870
2941
  } catch (err) {
2871
2942
  if (__sw_serverTiming) __sw_fetchMs = Date.now() - __sw_fetchStart;
@@ -2931,8 +3002,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2931
3002
 
2932
3003
  async function platformJSON(path, opts) {
2933
3004
  const r = await platformFetch(path, opts);
2934
- let data;
2935
- try { data = await r.json(); } catch { data = null; }
3005
+ const data = await __sw_platformJSONBody(r, opts && opts.signal);
2936
3006
  if (!r.ok || !data || data.ok === false) {
2937
3007
  throw __sw_platformError(r, data);
2938
3008
  }
@@ -3036,7 +3106,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
3036
3106
  });
3037
3107
  if (ingest) {
3038
3108
  let result = null;
3039
- try { result = await response.json(); } catch (_) {}
3109
+ result = await __sw_platformJSONBody(response, controller.signal);
3040
3110
  const queued = result && typeof result.queued === 'number' ? result.queued : 0;
3041
3111
  const dropped = result && typeof result.dropped === 'number' ? result.dropped : null;
3042
3112
  if (response.status === 202 && dropped === 0) return;
@@ -3048,8 +3118,8 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
3048
3118
  throw failure;
3049
3119
  }
3050
3120
  if (!response.ok) throw new Error('Console log delivery was refused.');
3051
- const result = await response.json();
3052
- if (!result.data || result.data.failed || result.data.unknown) {
3121
+ const result = await __sw_platformJSONBody(response, controller.signal);
3122
+ if (!result || !result.data || result.data.failed || result.data.unknown) {
3053
3123
  throw new Error('Console log delivery could not be confirmed.');
3054
3124
  }
3055
3125
  } finally { clearTimeout(timeout); }
@@ -3061,9 +3131,14 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
3061
3131
  // itself lives in the sw.rateLimit fragment.
3062
3132
  // The platform's counter: one increment, answered with the count it reached.
3063
3133
  async function __sw_rateLimitAsk(key, max, windowSeconds) {
3134
+ // Rate-limit checks stop waiting after five seconds.
3135
+ // An unconfirmed increment is never retried.
3136
+ const controller = new AbortController();
3137
+ const timer = setTimeout(function () { controller.abort(); }, 5000);
3064
3138
  try {
3065
3139
  return await platformJSON('/v1/rate-limit/check', {
3066
3140
  method: 'POST',
3141
+ signal: controller.signal,
3067
3142
  body: JSON.stringify({
3068
3143
  project_id: projectId,
3069
3144
  key,
@@ -3086,7 +3161,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
3086
3161
  };
3087
3162
  }
3088
3163
  throw e;
3089
- }
3164
+ } finally { clearTimeout(timer); }
3090
3165
  }
3091
3166
 
3092
3167
  // Remember what the platform answered for this key's window. A refusal
@@ -4694,7 +4769,14 @@ var DB_STRUCTURED_EXECUTION_JS = ` async function __sw_structExec(label, ir
4694
4769
  // Zero rows back means the EXISTS was false: the verified user does not
4695
4770
  // belong to the group this row targets. Surface it as an explicit 403
4696
4771
  // BEFORE any realtime publish \u2014 the row never landed.
4697
- if (ir.kind === 'insert' && (ir.scope.mode === 'member' || ir.scope.mode === 'policy') && rows.length === 0) {
4772
+ // A per-row share names a row its creator does not own (query-sql.ts grantRow guard).
4773
+ if (ir.kind === 'insert' && (ir.scope.mode === 'user' || ir.scope.mode === 'server') && ir.scope.grantRow && ir.onConflict === undefined && rows.length === 0) {
4774
+ __sw_structThrow403('POLICY_AUTHORITY_REQUIRED', ir.scope.mode === 'server'
4775
+ ? 'sw.db.server.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row its user_id owns.'
4776
+ : 'sw.db.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row the signed-in user owns.');
4777
+ }
4778
+ // An upsert's refusals raise inside the statement (query-sql.ts); its empty result is an ignored conflict.
4779
+ if (ir.kind === 'insert' && (ir.scope.mode === 'member' || ir.scope.mode === 'policy') && ir.onConflict === undefined && rows.length === 0) {
4698
4780
  __sw_structThrow403(ir.scope.mode === 'policy' ? 'POLICY_AUTHORITY_REQUIRED' : 'MEMBERSHIP_REQUIRED',
4699
4781
  ir.scope.mode === 'policy' ? 'sw.db.insert on "' + ir.table.name + '": the proposed row is outside your declared private policy.'
4700
4782
  : 'sw.db.insert on "' + ir.table.name + '": you can only add rows to a group you belong to, and the signed-in user is not a member of the group named in this row. Add the user to the group first, or write to a group they belong to.');
@@ -5118,6 +5200,7 @@ function renderPolicySql(request) {
5118
5200
  return renderPolicySql({
5119
5201
  render: "predicate",
5120
5202
  principal: r.principal,
5203
+ emailDomain: r.emailDomain,
5121
5204
  table: r.table,
5122
5205
  physical: r.physical,
5123
5206
  policy: { k: "m", m: r.member },
@@ -5128,10 +5211,11 @@ function renderPolicySql(request) {
5128
5211
  }
5129
5212
  if (r.render === "insert") {
5130
5213
  if (r.policy && r.policy.k === "n") {
5131
- renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: r.policy, operation: "read", qualifier: null, proposed: null });
5214
+ renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: r.policy, operation: "read", qualifier: null, proposed: null });
5132
5215
  const gateInsert = renderPolicySql({
5133
5216
  render: "predicate",
5134
5217
  principal: r.principal,
5218
+ emailDomain: r.emailDomain,
5135
5219
  table: r.table,
5136
5220
  physical: r.physical,
5137
5221
  policy: r.policy.g,
@@ -5139,7 +5223,7 @@ function renderPolicySql(request) {
5139
5223
  qualifier: null,
5140
5224
  proposed: r.proposed
5141
5225
  });
5142
- const whoInsert = r.policy.w.k === "o" ? { sql: "1", params: [] } : renderPolicySql({ render: "insert", principal: r.principal, table: r.table, physical: r.physical, policy: r.policy.w, proposed: r.proposed });
5226
+ const whoInsert = r.policy.w.k === "o" ? { sql: "1", params: [] } : renderPolicySql({ render: "insert", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: r.policy.w, proposed: r.proposed });
5143
5227
  const insertParams = [];
5144
5228
  for (let i = 0; i < gateInsert.params.length; i++) insertParams.push(gateInsert.params[i]);
5145
5229
  for (let i = 0; i < whoInsert.params.length; i++) insertParams.push(whoInsert.params[i]);
@@ -5148,13 +5232,17 @@ function renderPolicySql(request) {
5148
5232
  const anyOf = r.policy && r.policy.k === "a";
5149
5233
  const branch = anyOf && Array.isArray(r.policy.p) ? r.policy.p[1] : null;
5150
5234
  if (anyOf && (!branch || branch.k !== "m" && branch.k !== "g" && branch.k !== "r")) throw new Error("policy-sql: invalid anyOf policy");
5235
+ if (branch?.k === "m" && branch.m?.og) {
5236
+ renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: r.policy, operation: "read", qualifier: null, proposed: null });
5237
+ return { sql: "1", params: [] };
5238
+ }
5151
5239
  if (branch && branch.k === "r") {
5152
5240
  if (typeof r.principal !== "string" || r.principal.length === 0) throw new Error("policy-sql: invalid principal");
5153
- renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
5241
+ renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
5154
5242
  return { sql: "1", params: [] };
5155
5243
  }
5156
5244
  if (branch && branch.k === "g") {
5157
- renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
5245
+ renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
5158
5246
  let found = 0;
5159
5247
  let groupValue = null;
5160
5248
  for (let j = 0; j < r.proposed.length; j++) {
@@ -5176,6 +5264,7 @@ function renderPolicySql(request) {
5176
5264
  return renderPolicySql({
5177
5265
  render: "predicate",
5178
5266
  principal: r.principal,
5267
+ emailDomain: r.emailDomain,
5179
5268
  table: r.table,
5180
5269
  physical: r.physical,
5181
5270
  policy: anyOf ? r.policy.p[1] : r.policy,
@@ -5210,6 +5299,7 @@ function renderPolicySql(request) {
5210
5299
  return renderPolicySql({
5211
5300
  render: "predicate",
5212
5301
  principal: r.principal,
5302
+ emailDomain: r.emailDomain,
5213
5303
  table: r.table,
5214
5304
  physical: r.physical,
5215
5305
  policy: r.policy,
@@ -5228,6 +5318,7 @@ function renderPolicySql(request) {
5228
5318
  return renderPolicySql({
5229
5319
  render: "predicate",
5230
5320
  principal: r.principal,
5321
+ emailDomain: r.emailDomain,
5231
5322
  table: r.table,
5232
5323
  physical: r.physical,
5233
5324
  policy: r.policy,
@@ -5236,6 +5327,34 @@ function renderPolicySql(request) {
5236
5327
  proposed: r.proposed
5237
5328
  });
5238
5329
  }
5330
+ if (r.render === "membership") {
5331
+ const team = r.policy;
5332
+ if (!team || team.k !== "m" && team.k !== "g") throw new Error("policy-sql: membership is a member or group policy");
5333
+ if (r.principalRef !== void 0 && r.principalRef !== "link_maker") throw new Error("policy-sql: invalid principal reference");
5334
+ const byMaker = r.principalRef === "link_maker";
5335
+ const principal = byMaker ? "link_maker" : r.principal;
5336
+ const who = byMaker ? renderPolicySql({ render: "ident", name: "__sw_lg" }).sql + "." + renderPolicySql({ render: "ident", name: "created_by" }).sql : "?";
5337
+ const rowRef = r.qualifier ? renderPolicySql({ render: "ident", name: r.qualifier }).sql : null;
5338
+ let member;
5339
+ const keys = [];
5340
+ if (team.k === "m") {
5341
+ renderPolicySql({ render: "predicate", principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: team, operation: null, qualifier: r.qualifier, proposed: null, edge: r.edge });
5342
+ const groupsOfWho = [];
5343
+ for (let i = 0; i < team.m.mg.length; i++) groupsOfWho.push(renderPolicySql({ render: "ident", name: team.m.mg[i] }).sql);
5344
+ for (let i = 0; i < team.m.g.length; i++) keys.push((rowRef ? rowRef + "." : "") + renderPolicySql({ render: "ident", name: team.m.g[i] }).sql);
5345
+ member = (keys.length === 1 ? keys[0] : "(" + keys.join(", ") + ")") + " IN (SELECT " + groupsOfWho.join(", ") + " FROM " + renderPolicySql({ render: "table", name: team.m.m, physical: r.physical }).sql + " WHERE " + renderPolicySql({ render: "ident", name: team.m.u }).sql + " = " + who + ")";
5346
+ } else {
5347
+ renderPolicySql({ render: "predicate", principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: team, operation: "read", qualifier: r.qualifier, proposed: null, edge: r.edge });
5348
+ const members = renderPolicySql({ render: "ident", name: "__sw_gm" }).sql;
5349
+ const groups = renderPolicySql({ render: "ident", name: "__sw_g" }).sql;
5350
+ const rowGroup = (rowRef ? rowRef : renderPolicySql({ render: "table", name: r.table, physical: r.physical }).sql) + "." + renderPolicySql({ render: "ident", name: team.c }).sql;
5351
+ keys.push(rowGroup);
5352
+ member = "EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_group_members" }).sql + " AS " + members + " JOIN " + renderPolicySql({ render: "ident", name: "_sw_groups" }).sql + " AS " + groups + " ON " + groups + '."id" = ' + members + '."group_id" WHERE ' + members + '."group_id" = ' + rowGroup + " AND " + members + '."user_id" = ' + who + ")";
5353
+ }
5354
+ let sql = "(";
5355
+ for (let i = 0; i < keys.length; i++) sql += keys[i] + " IS NULL OR ";
5356
+ return { sql: sql + member + ")", params: byMaker ? [] : [r.principal] };
5357
+ }
5239
5358
  if (r.render !== "predicate") throw new Error("policy-sql: unknown render kind");
5240
5359
  const policy = r.policy;
5241
5360
  const qualifier = r.qualifier;
@@ -5254,6 +5373,11 @@ function renderPolicySql(request) {
5254
5373
  if (!m || !Array.isArray(m.g) || !Array.isArray(m.mg) || m.g.length === 0 || m.g.length !== m.mg.length) {
5255
5374
  throw new Error("policy-sql: invalid member scope");
5256
5375
  }
5376
+ if (m.og !== void 0) {
5377
+ const perRow = m.g.length === 2 && m.g[1] !== "user_id" && m.mg[1] !== "user_id" && m.mg[1] !== m.u;
5378
+ if (m.og !== true || m.g[0] !== "user_id" || m.mg[0] !== "user_id" || m.g.length !== 1 && !perRow || m.u === "user_id" || JSON.stringify(m.o) !== '["read"]' && JSON.stringify(m.o) !== '["read","update"]') throw new Error("policy-sql: invalid owner grant");
5379
+ if (proposed || !operation) return { sql: "0", params: [] };
5380
+ }
5257
5381
  if (operation && m.o !== void 0) {
5258
5382
  let allowed = false;
5259
5383
  for (let i = 0; i < m.o.length; i++) if (m.o[i] === operation) allowed = true;
@@ -5303,8 +5427,16 @@ function renderPolicySql(request) {
5303
5427
  if ((policy.p[1].k === "g" || policy.p[1].k === "r") && (proposed || !operation)) {
5304
5428
  throw new Error("policy-sql: an anyOf group or app-role branch needs an operation on an existing row");
5305
5429
  }
5306
- const left = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: policy.p[0], operation: r.operation, qualifier, proposed, edge: r.edge });
5307
- const right = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: policy.p[1], operation: r.operation, qualifier, proposed, edge: r.edge });
5430
+ let left = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p[0], operation: r.operation, qualifier, proposed, edge: r.edge });
5431
+ if (policy.p[1].k === "m" && policy.p[1].m.og && policy.p[0].c !== "user_id") throw new Error("policy-sql: grant requires managed owner");
5432
+ if (policy.p[1].k === "m" && !policy.p[1].m.og || policy.p[1].k === "g") {
5433
+ const team = proposed ? renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p[1], operation: null, qualifier, proposed, edge: r.edge }) : renderPolicySql({ render: "membership", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p[1], qualifier, edge: r.edge });
5434
+ const gated = [];
5435
+ for (let i = 0; i < left.params.length; i++) gated.push(left.params[i]);
5436
+ for (let i = 0; i < team.params.length; i++) gated.push(team.params[i]);
5437
+ left = { sql: "(" + left.sql + " AND " + team.sql + ")", params: gated };
5438
+ }
5439
+ const right = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p[1], operation: r.operation, qualifier, proposed, edge: r.edge });
5308
5440
  for (let i = 0; i < left.params.length; i++) params.push(left.params[i]);
5309
5441
  for (let i = 0; i < right.params.length; i++) params.push(right.params[i]);
5310
5442
  return { sql: "(" + left.sql + " OR " + right.sql + ")", params };
@@ -5315,7 +5447,7 @@ function renderPolicySql(request) {
5315
5447
  if (!gate || gate.k !== "m" && gate.k !== "g" && gate.k !== "r") {
5316
5448
  throw new Error("policy-sql: an allOf gate is a member, group or app-role policy");
5317
5449
  }
5318
- if (gate.k === "m" && (!gate.m || gate.m.o === void 0)) throw new Error("policy-sql: allOf needs explicit operations");
5450
+ if (gate.k === "m" && (!gate.m || gate.m.o === void 0 || gate.m.og)) throw new Error("policy-sql: allOf needs explicit operations");
5319
5451
  if (!who || who.k !== "o" && who.k !== "a") throw new Error("policy-sql: allOf who is owner() or anyOf(owner(), ...)");
5320
5452
  if (who.k === "a") {
5321
5453
  const x = Array.isArray(who.p) && who.p.length === 2 ? who.p[1] : null;
@@ -5325,7 +5457,7 @@ function renderPolicySql(request) {
5325
5457
  compatible = String(x.c).toLowerCase() === String(gate.c).toLowerCase() && x.generation === gate.generation;
5326
5458
  }
5327
5459
  if (x.k === "m" && gate.k === "m") {
5328
- if (!x.m || x.m.o === void 0) throw new Error("policy-sql: allOf needs explicit operations");
5460
+ if (!x.m || x.m.o === void 0 || x.m.og) throw new Error("policy-sql: allOf needs explicit operations");
5329
5461
  const a = gate.m;
5330
5462
  const b = x.m;
5331
5463
  compatible = String(a.m).toLowerCase() === String(b.m).toLowerCase() && String(a.u).toLowerCase() === String(b.u).toLowerCase() && Array.isArray(a.g) && Array.isArray(b.g) && Array.isArray(a.mg) && Array.isArray(b.mg) && a.g.length === b.g.length && a.mg.length === b.mg.length;
@@ -5340,14 +5472,14 @@ function renderPolicySql(request) {
5340
5472
  if (operation !== "read" && operation !== "create" && operation !== "update" && operation !== "delete") {
5341
5473
  throw new Error("policy-sql: an allOf policy needs an operation");
5342
5474
  }
5343
- const gateSql = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: gate, operation, qualifier, proposed: null, edge: r.edge });
5344
- const whoSql = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: who, operation, qualifier, proposed: null, edge: r.edge });
5475
+ const gateSql = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: gate, operation, qualifier, proposed: null, edge: r.edge });
5476
+ const whoSql = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: who, operation, qualifier, proposed: null, edge: r.edge });
5345
5477
  for (let i = 0; i < gateSql.params.length; i++) params.push(gateSql.params[i]);
5346
5478
  for (let i = 0; i < whoSql.params.length; i++) params.push(whoSql.params[i]);
5347
5479
  let sql = "(" + gateSql.sql + " AND " + whoSql.sql + ")";
5348
5480
  if (operation === "update" || operation === "delete") {
5349
- const gateRead = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: gate, operation: "read", qualifier, proposed: null, edge: r.edge });
5350
- const whoRead = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: who, operation: "read", qualifier, proposed: null, edge: r.edge });
5481
+ const gateRead = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: gate, operation: "read", qualifier, proposed: null, edge: r.edge });
5482
+ const whoRead = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: who, operation: "read", qualifier, proposed: null, edge: r.edge });
5351
5483
  for (let i = 0; i < gateRead.params.length; i++) params.push(gateRead.params[i]);
5352
5484
  for (let i = 0; i < whoRead.params.length; i++) params.push(whoRead.params[i]);
5353
5485
  sql = "(" + sql + " AND (" + gateRead.sql + " AND " + whoRead.sql + "))";
@@ -5384,7 +5516,7 @@ function renderPolicySql(request) {
5384
5516
  } else {
5385
5517
  join = parentKey + " = " + (qualifier ? renderPolicySql({ render: "ident", name: qualifier }).sql : renderPolicySql({ render: "table", name: r.table, physical: r.physical }).sql) + "." + renderPolicySql({ render: "ident", name: policy.v }).sql;
5386
5518
  }
5387
- const inner = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: policy.p, operation: r.operation, qualifier: alias, proposed: null, edge });
5519
+ const inner = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p, operation: r.operation, qualifier: alias, proposed: null, edge });
5388
5520
  for (let i = 0; i < inner.params.length; i++) params.push(inner.params[i]);
5389
5521
  return {
5390
5522
  sql: "EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "table", name: policy.t, physical: r.physical }).sql + " AS " + aliasSql + " WHERE " + join + " AND " + inner.sql + ")",
@@ -5484,8 +5616,35 @@ function renderPolicySql(request) {
5484
5616
  marks.push("?");
5485
5617
  params.push(allowed[i]);
5486
5618
  }
5619
+ let held = "EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_app_role_grants" }).sql + " AS " + grants + " WHERE " + grants + '."user_id" = ? AND ' + grants + '."role" IN (' + marks.join(", ") + "))";
5620
+ const domain = r.emailDomain;
5621
+ if (domain !== null && domain !== void 0 && (typeof domain !== "string" || domain.length === 0 || domain.length > 253)) {
5622
+ throw new Error("policy-sql: invalid email domain");
5623
+ }
5624
+ const baked = policy.d;
5625
+ if (baked !== void 0) {
5626
+ if (!baked || typeof baked !== "object" || Array.isArray(baked)) throw new Error("policy-sql: invalid app-role domains");
5627
+ if (domain !== null && domain !== void 0) {
5628
+ for (let i = 0; i < allowed.length; i++) {
5629
+ if (Object.prototype.hasOwnProperty.call(baked, allowed[i]) && baked[allowed[i]] === domain) {
5630
+ held = "(" + held + " OR 1)";
5631
+ break;
5632
+ }
5633
+ }
5634
+ }
5635
+ } else if (domain !== null && domain !== void 0) {
5636
+ const vocabulary = renderPolicySql({ render: "ident", name: "__sw_rv" }).sql;
5637
+ const entry = renderPolicySql({ render: "ident", name: "__sw_re" }).sql;
5638
+ const domainMarks = [];
5639
+ for (let i = 0; i < allowed.length; i++) {
5640
+ domainMarks.push("?");
5641
+ params.push(allowed[i]);
5642
+ }
5643
+ params.push(domain);
5644
+ held = "(" + held + " OR EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_group_policy" }).sql + " AS " + vocabulary + ", json_each(" + vocabulary + `."vocabulary", '$.appRoles') AS ` + entry + " WHERE " + vocabulary + '."id" = 1 AND json_extract(' + entry + `."value", '$.name') IN (` + domainMarks.join(", ") + ") AND json_extract(" + entry + `."value", '$.emailDomain') = ?))`;
5645
+ }
5487
5646
  return {
5488
- sql: "(EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_group_policy" }).sql + " AS " + active + " WHERE " + active + '."id" = 1 AND ' + active + '."generation" = ?) AND EXISTS (SELECT 1 FROM ' + renderPolicySql({ render: "ident", name: "_sw_app_role_grants" }).sql + " AS " + grants + " WHERE " + grants + '."user_id" = ? AND ' + grants + '."role" IN (' + marks.join(", ") + ")))",
5647
+ sql: "(EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_group_policy" }).sql + " AS " + active + " WHERE " + active + '."id" = 1 AND ' + active + '."generation" = ?) AND ' + held + ")",
5489
5648
  params
5490
5649
  };
5491
5650
  }
@@ -5497,7 +5656,19 @@ function policyOwnerColumn(policy) {
5497
5656
  if (policy.k === "n") return policyOwnerColumn(policy.w);
5498
5657
  return null;
5499
5658
  }
5500
- var POLICY_SQL_JS = " const __sw_renderPolicySql = (" + renderPolicySql.toString() + ");\n const __sw_policyOwnerColumn = (" + policyOwnerColumn.toString() + ");\n";
5659
+ function policyWithoutGrants(policy) {
5660
+ if (policy.k === "a") {
5661
+ const second = policy.p[1];
5662
+ if (second.k === "m" && second.m.og) return policy.p[0];
5663
+ return policy;
5664
+ }
5665
+ if (policy.k === "p") {
5666
+ const inner = policyWithoutGrants(policy.p);
5667
+ return inner === policy.p ? policy : { ...policy, p: inner };
5668
+ }
5669
+ return policy;
5670
+ }
5671
+ var POLICY_SQL_JS = " const __sw_renderPolicySql = (" + renderPolicySql.toString() + ");\n const __sw_policyOwnerColumn = (" + policyOwnerColumn.toString() + ");\n const __sw_policyWithoutGrants = (" + policyWithoutGrants.toString() + ");\n";
5501
5672
 
5502
5673
  // worker/src/runtime/query-sql.ts
5503
5674
  function renderQuerySql(ir, principalId, returning, context) {
@@ -5514,6 +5685,26 @@ function renderQuerySql(ir, principalId, returning, context) {
5514
5685
  if (ir.kind === "insert" || ir.kind === "update") {
5515
5686
  context.assertUniqueWriteColumns((ir.values || []).concat(ir.increments || []), "sw.db." + ir.kind);
5516
5687
  }
5688
+ if (ir.kind === "update" && ir.scope) {
5689
+ const fixed = [];
5690
+ if ((ir.scope.mode === "user" || ir.scope.mode === "server") && ir.scope.grantRow) fixed.push(ir.scope.grantRow.c);
5691
+ if (ir.scope.mode === "server" && ir.scope.grantRow) fixed.push("user_id");
5692
+ const grantBranch = ir.scope.mode === "policy" && ir.scope.policy.k === "a" ? ir.scope.policy.p[1] : null;
5693
+ if (grantBranch && grantBranch.k === "m" && grantBranch.m.og && grantBranch.m.g.length === 2) fixed.push(grantBranch.m.g[1]);
5694
+ const written = (ir.values || []).concat(ir.increments || []);
5695
+ for (let i = 0; i < written.length; i++) {
5696
+ for (let j = 0; j < fixed.length; j++) {
5697
+ if (context.lower(written[i].column) !== context.lower(fixed[j])) continue;
5698
+ let pinned = false;
5699
+ const value = i < (ir.values || []).length ? written[i].value : void 0;
5700
+ for (let k = 0; k < ir.where.length; k++) {
5701
+ const w = ir.where[k];
5702
+ if (value !== void 0 && w.op === "eq" && typeof w.column === "string" && context.lower(w.column) === context.lower(fixed[j]) && w.value === value) pinned = true;
5703
+ }
5704
+ if (!pinned) context.fail('sw.db.update: "' + written[i].column + '" ties a share to one row and cannot change. Delete the share and create another.');
5705
+ }
5706
+ }
5707
+ }
5517
5708
  const params = [];
5518
5709
  const helpers = {
5519
5710
  table(name) {
@@ -5619,6 +5810,44 @@ function renderQuerySql(ir, principalId, returning, context) {
5619
5810
  policyInsertPredicate(policy, proposedValues) {
5620
5811
  return helpers.policySql(context.renderPolicy({ render: "insert", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy, proposed: proposedValues }));
5621
5812
  },
5813
+ // The columns that place a row under its rule and never change through an upsert: member()
5814
+ // group keys, a group() column, an allOf gate key, the parent() link and a per-row grant's
5815
+ // row key. The owner is stamped and never assigned, so it is not listed.
5816
+ policyFixedKeys(policy) {
5817
+ const keys = [];
5818
+ if (policy.k === "m") {
5819
+ if (!policy.m.og) for (let i = 0; i < policy.m.g.length; i++) keys.push(policy.m.g[i]);
5820
+ else if (policy.m.g.length === 2) keys.push(policy.m.g[1]);
5821
+ }
5822
+ if (policy.k === "g") keys.push(policy.c);
5823
+ if (policy.k === "p") keys.push(policy.v);
5824
+ if (policy.k === "a" || policy.k === "n") {
5825
+ const parts = policy.k === "a" ? policy.p : [policy.g, policy.w];
5826
+ for (let i = 0; i < parts.length; i++) {
5827
+ const inner = helpers.policyFixedKeys(parts[i]);
5828
+ for (let j = 0; j < inner.length; j++) keys.push(inner[j]);
5829
+ }
5830
+ }
5831
+ return keys;
5832
+ },
5833
+ // Upsert under a private rule (tsk_c98959e4): the conflicting existing row must pass the rule's
5834
+ // UPDATE predicate (rendered by the caller, binds already pushed) and keep every fixed key the
5835
+ // caller names. Otherwise the statement raises, so it is refused whole (a transaction rolls back),
5836
+ // never skipped.
5837
+ conflictUpdateSql(updatePredicate, fixed) {
5838
+ const sets = context.map(ir.values, function(e) {
5839
+ return q(e.column) + " = excluded." + q(e.column);
5840
+ });
5841
+ if (sets.length === 0) context.fail('sw.db.insert with { onConflict: "update" } needs at least one column value to update on conflict.');
5842
+ const held = ["(" + updatePredicate + ")"];
5843
+ for (let i = 0; i < fixed.length; i++) {
5844
+ for (let j = 0; j < ir.values.length; j++) {
5845
+ const column = ir.values[j].column;
5846
+ if (context.lower(column) === context.lower(fixed[i])) held.push(tableSql + "." + q(column) + " IS excluded." + q(column));
5847
+ }
5848
+ }
5849
+ return " ON CONFLICT DO UPDATE SET " + sets.join(", ") + " WHERE CASE WHEN " + held.join(" AND ") + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END";
5850
+ },
5622
5851
  // Row-link authority (tsk_f1529568): ONE correlated EXISTS proving, in the
5623
5852
  // statement itself, that the session token is live, belongs to this exact
5624
5853
  // grant, the grant is unrevoked and unexpired, names this table and THIS
@@ -5639,7 +5868,26 @@ function renderQuerySql(ir, principalId, returning, context) {
5639
5868
  else context.fail("sw.db: a linked row key must be a whole number or text.");
5640
5869
  } else key = "CAST(" + rowRef + " AS TEXT)";
5641
5870
  params.push(link.digest, link.epoch);
5642
- return "EXISTS (SELECT 1 FROM " + q("_sw_row_link_sessions") + " AS " + ls + " JOIN " + q("_sw_row_links") + " AS " + lg + " ON " + lg + "." + q("id") + " = " + ls + "." + q("grant_id") + " JOIN " + q("_sw_row_link_epochs") + " AS " + le + " ON " + le + "." + q("table_name") + " = " + lg + "." + q("table_name") + " WHERE " + ls + "." + q("token_digest") + " = ? AND " + ls + "." + q("expires_at") + " > unixepoch() AND " + lg + "." + q("id") + " = ? AND " + lg + "." + q("table_name") + " = ? AND " + lg + "." + q("row_key") + " = " + key + " AND " + lg + "." + q("revoked_at") + " IS NULL AND (" + lg + "." + q("expires_at") + " IS NULL OR " + lg + "." + q("expires_at") + " > unixepoch()) AND " + lg + "." + q("access") + (link.write ? " = 'edit'" : " IN ('read', 'edit')") + " AND " + le + "." + q("digest") + " = ? AND " + le + "." + q("epoch") + " = ? AND " + lg + "." + q("minted_epoch") + " = " + le + "." + q("epoch") + ")";
5871
+ let team = "";
5872
+ if (link.team) {
5873
+ if (!link.team.key || typeof link.team.key !== "string" || !link.team.policy) context.fail("sw.db: invalid link authority.");
5874
+ const lr = q("__sw_lr");
5875
+ let rootKey = "?";
5876
+ if (rowRef === null) params.push(rowValue);
5877
+ else rootKey = rowRef;
5878
+ const gate = context.renderPolicy({
5879
+ render: "membership",
5880
+ principal: null,
5881
+ principalRef: "link_maker",
5882
+ table: link.root,
5883
+ physical: context.physicalMap(),
5884
+ policy: link.team.policy,
5885
+ qualifier: "__sw_lr"
5886
+ });
5887
+ for (let i = 0; i < gate.params.length; i++) params.push(gate.params[i]);
5888
+ team = " AND EXISTS (SELECT 1 FROM " + context.renderPolicy({ render: "table", name: link.root, physical: context.physicalMap() }).sql + " AS " + lr + " WHERE " + lr + "." + q(link.team.key) + " = " + rootKey + " AND " + gate.sql + ")";
5889
+ }
5890
+ return "EXISTS (SELECT 1 FROM " + q("_sw_row_link_sessions") + " AS " + ls + " JOIN " + q("_sw_row_links") + " AS " + lg + " ON " + lg + "." + q("id") + " = " + ls + "." + q("grant_id") + " JOIN " + q("_sw_row_link_epochs") + " AS " + le + " ON " + le + "." + q("table_name") + " = " + lg + "." + q("table_name") + " WHERE " + ls + "." + q("token_digest") + " = ? AND " + ls + "." + q("expires_at") + " > unixepoch() AND " + lg + "." + q("id") + " = ? AND " + lg + "." + q("table_name") + " = ? AND " + lg + "." + q("row_key") + " = " + key + " AND " + lg + "." + q("revoked_at") + " IS NULL AND (" + lg + "." + q("expires_at") + " IS NULL OR " + lg + "." + q("expires_at") + " > unixepoch()) AND " + lg + "." + q("access") + (link.write ? " = 'edit'" : " IN ('read', 'edit')") + " AND " + le + "." + q("digest") + " = ? AND " + le + "." + q("epoch") + " = ? AND " + lg + "." + q("minted_epoch") + " = " + le + "." + q("epoch") + team + ")";
5643
5891
  },
5644
5892
  // Creator session (creator-authority.ts): ONE correlated EXISTS proving, in the statement, that this
5645
5893
  // creator session token is live for this exact unended creator grant, which holds THIS root row's
@@ -5708,8 +5956,21 @@ function renderQuerySql(ir, principalId, returning, context) {
5708
5956
  frags.push(privateBranch);
5709
5957
  }
5710
5958
  if (ir.kind === "update" && ir.scope.mode === "policy") {
5711
- const destination = context.renderPolicy({ render: "update_destination", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy: ir.scope.policy, proposed: ir.values || [] });
5712
- if (destination.sql)
5959
+ const policy = ir.scope.policy;
5960
+ const destination = context.renderPolicy({ render: "update_destination", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy, proposed: ir.values || [] });
5961
+ const ownerOnly = destination.sql && policy.k === "p" ? context.withoutGrants(policy) : policy;
5962
+ if (destination.sql && ownerOnly !== policy) {
5963
+ const access = [];
5964
+ for (let i = 0; i < ir.where.length; i++)
5965
+ access.push(helpers.renderPredicate(ir.where[i]));
5966
+ access.push(helpers.privateScopePredicate(ir.scope) || "1");
5967
+ access.push(helpers.policySql(destination));
5968
+ const owned = [
5969
+ helpers.policyPredicate(ownerOnly, null, null, "update"),
5970
+ helpers.policySql(context.renderPolicy({ render: "update_destination", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy: ownerOnly, proposed: ir.values || [] }))
5971
+ ];
5972
+ frags.push("(CASE WHEN NOT (" + access.join(" AND ") + ") THEN 0 WHEN (" + owned.join(" AND ") + ") THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END)");
5973
+ } else if (destination.sql)
5713
5974
  frags.push(helpers.policySql(destination));
5714
5975
  }
5715
5976
  for (let i = 0; i < (ir.increments || []).length; i++) {
@@ -5744,6 +6005,8 @@ function renderQuerySql(ir, principalId, returning, context) {
5744
6005
  };
5745
6006
  const q = context.quote;
5746
6007
  const tableSql = helpers.table(ir.table.name);
6008
+ if (ir.kind === "insert" && ir.onConflict === "update" && context.lower(tableSql) === '"excluded"')
6009
+ context.fail('sw.db.insert: a table named "excluded" cannot take onConflict: "update". Rename the table, or use onConflict: "ignore".');
5747
6010
  const policyOperation = ir.kind === "select" || ir.kind === "count" || ir.kind === "aggregate" ? "read" : ir.kind === "insert" ? "create" : ir.kind;
5748
6011
  let sql;
5749
6012
  if (ir.kind === "select") {
@@ -5872,8 +6135,9 @@ function renderQuerySql(ir, principalId, returning, context) {
5872
6135
  existsSql += " AND " + q(m.mg[i]) + " = ?";
5873
6136
  params.push(gv);
5874
6137
  }
5875
- const guard = ir.txGuard ? "CASE WHEN EXISTS (" + existsSql + ") THEN 1 ELSE json_extract('{}', 'SW_MEMBERSHIP_REQUIRED') END" : "EXISTS (" + existsSql + ")";
5876
- sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + context.returningSql(returning);
6138
+ const guard = ir.txGuard || ir.onConflict !== void 0 ? "CASE WHEN EXISTS (" + existsSql + ") THEN 1 ELSE json_extract('{}', 'SW_MEMBERSHIP_REQUIRED') END" : "EXISTS (" + existsSql + ")";
6139
+ const conflict = ir.onConflict === "ignore" ? " ON CONFLICT DO NOTHING" : ir.onConflict === "update" ? helpers.conflictUpdateSql(helpers.memberScopePredicate(m, null, "update"), m.g) : "";
6140
+ sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + conflict + context.returningSql(returning);
5877
6141
  return { sql, params };
5878
6142
  }
5879
6143
  if (ir.scope.mode === "link") {
@@ -5904,8 +6168,38 @@ function renderQuerySql(ir, principalId, returning, context) {
5904
6168
  for (let i = 0; i < ivals.length; i++)
5905
6169
  params.push(ivals[i]);
5906
6170
  const authorized = helpers.policyInsertPredicate(ir.scope.policy, ir.values || []);
5907
- const guard = ir.txGuard ? "CASE WHEN " + authorized + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END" : authorized;
5908
- sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + context.returningSql(returning);
6171
+ const guard = ir.txGuard || ir.onConflict !== void 0 ? "CASE WHEN " + authorized + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END" : authorized;
6172
+ const conflict = ir.onConflict === "ignore" ? " ON CONFLICT DO NOTHING" : ir.onConflict === "update" ? helpers.conflictUpdateSql(helpers.policyPredicate(ir.scope.policy, null, null, "update"), helpers.policyFixedKeys(ir.scope.policy)) : "";
6173
+ sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + conflict + context.returningSql(returning);
6174
+ return { sql, params };
6175
+ }
6176
+ if ((ir.scope.mode === "user" || ir.scope.mode === "server") && ir.scope.grantRow) {
6177
+ const grantRow = ir.scope.grantRow;
6178
+ let named = 0;
6179
+ let rowValue = null;
6180
+ let owners = 0;
6181
+ let shareOwner = ir.scope.mode === "user" ? principalId : null;
6182
+ for (let i = 0; i < ir.values.length; i++) {
6183
+ if (context.lower(ir.values[i].column) === context.lower(grantRow.c)) {
6184
+ named++;
6185
+ rowValue = ir.values[i].value;
6186
+ }
6187
+ if (ir.scope.mode === "server" && context.lower(ir.values[i].column) === "user_id") {
6188
+ owners++;
6189
+ shareOwner = ir.values[i].value;
6190
+ }
6191
+ }
6192
+ if (named !== 1 || rowValue === null) context.fail('sw.db.insert: a share names the "' + grantRow.t + '" row it grants in "' + grantRow.c + '".');
6193
+ if (ir.scope.mode === "server" && (owners !== 1 || shareOwner === null)) context.fail('sw.db.server.insert: a share names its owner in "user_id"; the owner must own the "' + grantRow.t + '" row it grants.');
6194
+ if (ir.onConflict === "update") context.fail('sw.db.insert: a share cannot be updated on conflict; use onConflict: "ignore".');
6195
+ for (let i = 0; i < ivals.length; i++)
6196
+ params.push(ivals[i]);
6197
+ const owned = "EXISTS (SELECT 1 FROM " + helpers.table(grantRow.t) + " WHERE " + q(grantRow.k) + " = ? AND " + q("user_id") + " = ?)";
6198
+ params.push(rowValue, shareOwner);
6199
+ const guard = ir.txGuard || ir.onConflict !== void 0 ? "CASE WHEN " + owned + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END" : owned;
6200
+ sql = "INSERT INTO " + tableSql + " (" + allCols.map(q).join(", ") + ") SELECT " + allCols.map(function() {
6201
+ return "?";
6202
+ }).join(", ") + " WHERE " + guard + (ir.onConflict === "ignore" ? " ON CONFLICT DO NOTHING" : "") + context.returningSql(returning);
5909
6203
  return { sql, params };
5910
6204
  }
5911
6205
  for (let i = 0; i < ivals.length; i++)
@@ -5918,13 +6212,14 @@ function renderQuerySql(ir, principalId, returning, context) {
5918
6212
  if (ir.onConflict === "ignore") {
5919
6213
  sql += " ON CONFLICT DO NOTHING";
5920
6214
  } else if (ir.onConflict === "update") {
5921
- const sets = context.map(ir.values, function(e) {
5922
- return q(e.column) + " = excluded." + q(e.column);
5923
- });
5924
- sql += " ON CONFLICT DO UPDATE SET " + sets.join(", ");
5925
6215
  if (ir.scope.mode === "user") {
5926
- sql += " WHERE " + q(ir.scope.ownerColumn) + " = ?";
5927
6216
  params.push(principalId);
6217
+ sql += helpers.conflictUpdateSql(q(ir.scope.ownerColumn) + " = ?", []);
6218
+ } else {
6219
+ const sets = context.map(ir.values, function(e) {
6220
+ return q(e.column) + " = excluded." + q(e.column);
6221
+ });
6222
+ sql += " ON CONFLICT DO UPDATE SET " + sets.join(", ");
5928
6223
  }
5929
6224
  }
5930
6225
  sql += context.returningSql(returning);
@@ -6035,6 +6330,7 @@ var DB_RENDER_IR_JS = `${POLICY_SQL_JS}${QUERY_SQL_JS} // One write names e
6035
6330
  },
6036
6331
  renderPolicy: __sw_renderPolicySql,
6037
6332
  ownerColumn: __sw_policyOwnerColumn,
6333
+ withoutGrants: __sw_policyWithoutGrants,
6038
6334
  returningSql: __sw_returningSql,
6039
6335
  assertUniqueWriteColumns: __sw_structAssertUniqueWriteColumns,
6040
6336
  fail: __sw_structThrow,
@@ -6631,13 +6927,32 @@ var DB_RELATIONS_JS = ` // \u2500\u2500 Composed joins (tsk_1186c9a) \u2500
6631
6927
  }
6632
6928
  `;
6633
6929
 
6930
+ // worker/src/utils/authority/role-domain-map.ts
6931
+ var ROLE_NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
6932
+ 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]$/;
6933
+ var ROLE_DOMAIN_MAP_JS = `
6934
+ function __sw_structRoleDomains(value) {
6935
+ if (!value || typeof value !== 'object' || __sw_arrayIsArray(value)) return false;
6936
+ const roles = __sw_keys(value);
6937
+ for (let i = 0; i < roles.length; i++) {
6938
+ const domain = value[roles[i]];
6939
+ if (!__sw_reMatch(${ROLE_NAME_PATTERN}, roles[i]) || typeof domain !== 'string' || !__sw_reMatch(${EMAIL_DOMAIN_PATTERN}, domain)) return false;
6940
+ }
6941
+ return true;
6942
+ }`;
6943
+
6634
6944
  // worker/src/runtime/db-policy.ts
6635
- var DB_POLICY_HELPERS_JS = `
6945
+ var DB_POLICY_HELPERS_JS = `${ROLE_DOMAIN_MAP_JS}
6636
6946
  function __sw_structPolicyIdent(value) { return typeof value === 'string' && value && __sw_reMatch(__SW_STRUCT_IDENT_RE, value); }
6637
6947
  function __sw_structPolicyMember(value) {
6638
6948
  if (!value || typeof value !== 'object') return false;
6639
6949
  const keys = __sw_keys(value).slice().sort().join(',');
6640
- if (keys !== 'g,m,mg,u' && keys !== 'g,m,mg,o,u') return false;
6950
+ // Per-owner grant: g/mg ["user_id"]. Per-row: g ["user_id", key], mg ["user_id", row].
6951
+ const grant = keys === 'g,m,mg,o,og,u' && value.og === true && __sw_arrayIsArray(value.g) && __sw_arrayIsArray(value.mg)
6952
+ && value.g[0] === 'user_id' && value.mg[0] === 'user_id'
6953
+ && (value.g.length === 1 || value.g.length === 2 && value.g[1] !== 'user_id' && value.mg[1] !== 'user_id' && value.mg[1] !== value.u)
6954
+ && value.u !== 'user_id' && (JSON.stringify(value.o) === '["read"]' || JSON.stringify(value.o) === '["read","update"]');
6955
+ if (keys !== 'g,m,mg,u' && keys !== 'g,m,mg,o,u' && !grant) return false;
6641
6956
  if (!__sw_arrayIsArray(value.g) || !__sw_arrayIsArray(value.mg) || value.g.length === 0 || value.g.length !== value.mg.length || !__sw_structPolicyIdent(value.m) || !__sw_structPolicyIdent(value.u)) return false;
6642
6957
  for (let i = 0; i < value.g.length; i++) if (!__sw_structPolicyIdent(value.g[i]) || !__sw_structPolicyIdent(value.mg[i])) return false;
6643
6958
  if (value.o !== undefined) {
@@ -6672,9 +6987,12 @@ var DB_POLICY_HELPERS_JS = `
6672
6987
  return true;
6673
6988
  }
6674
6989
  // An app-role leaf, pinned to the release's policy generation. Each
6675
- // present operation lists declared app roles; a missing one denies.
6990
+ // present operation lists declared app roles; a missing one denies. The
6991
+ // release's role -> verified email domain map (d) rides it when baked.
6676
6992
  function __sw_structPolicyRole(value) {
6677
- if (!value || typeof value !== 'object' || value.k !== 'r' || __sw_keys(value).slice().sort().join(',') !== 'generation,k,roles') return false;
6993
+ if (!value || typeof value !== 'object' || value.k !== 'r') return false;
6994
+ const leafKeys = __sw_keys(value).slice().sort().join(',');
6995
+ if (leafKeys !== 'generation,k,roles' && (leafKeys !== 'd,generation,k,roles' || !__sw_structRoleDomains(value.d))) return false;
6678
6996
  if (typeof value.generation !== 'number' || !Number.isSafeInteger(value.generation) || value.generation < 1) return false;
6679
6997
  const roles = value.roles;
6680
6998
  if (!roles || typeof roles !== 'object' || __sw_arrayIsArray(roles)) return false;
@@ -6694,6 +7012,7 @@ var DB_POLICY_HELPERS_JS = `
6694
7012
  if (!value || typeof value !== 'object' || __sw_keys(value).slice().sort().join(',') !== 'k,p' || value.k !== 'a'
6695
7013
  || !__sw_arrayIsArray(value.p) || value.p.length !== 2 || !__sw_structPolicyLeaf(value.p[0]) || value.p[0].k !== 'o') return false;
6696
7014
  const second = value.p[1];
7015
+ if (second && second.k === 'm' && second.m && second.m.og && value.p[0].c !== 'user_id') return false;
6697
7016
  return (__sw_structPolicyLeaf(second) && second.k === 'm') || __sw_structPolicyGroup(second) || __sw_structPolicyRole(second);
6698
7017
  }
6699
7018
  // allOf(gate, who) (AND permissions v1.1): gate is a member() leaf with
@@ -6705,7 +7024,7 @@ var DB_POLICY_HELPERS_JS = `
6705
7024
  if (!value || typeof value !== 'object' || __sw_keys(value).slice().sort().join(',') !== 'g,k,w' || value.k !== 'n') return false;
6706
7025
  const gate = value.g;
6707
7026
  const who = value.w;
6708
- const gateOk = (__sw_structPolicyLeaf(gate) && gate.k === 'm' && gate.m.o !== undefined) || __sw_structPolicyGroup(gate) || __sw_structPolicyRole(gate);
7027
+ const gateOk = (__sw_structPolicyLeaf(gate) && gate.k === 'm' && gate.m.o !== undefined && !gate.m.og) || __sw_structPolicyGroup(gate) || __sw_structPolicyRole(gate);
6709
7028
  if (!gateOk) return false;
6710
7029
  if (__sw_structPolicyLeaf(who) && who.k === 'o') return true;
6711
7030
  if (!__sw_structPolicyAny(who)) return false;
@@ -6713,6 +7032,7 @@ var DB_POLICY_HELPERS_JS = `
6713
7032
  if (x.k === 'r') return true;
6714
7033
  if (x.k === 'g') return gate.k === 'g' && __sw_lower(x.c) === __sw_lower(gate.c) && x.generation === gate.generation;
6715
7034
  if (x.k !== 'm' || gate.k !== 'm' || x.m.o === undefined) return false;
7035
+ if (x.m.og || gate.m.og) return false;
6716
7036
  if (__sw_lower(x.m.m) !== __sw_lower(gate.m.m) || __sw_lower(x.m.u) !== __sw_lower(gate.m.u)
6717
7037
  || x.m.g.length !== gate.m.g.length || x.m.mg.length !== gate.m.mg.length) return false;
6718
7038
  for (let i = 0; i < x.m.g.length; i++) if (__sw_lower(x.m.g[i]) !== __sw_lower(gate.m.g[i])) return false;
@@ -6733,6 +7053,23 @@ var DB_POLICY_HELPERS_JS = `
6733
7053
  if (__sw_structResolveIntent(table) !== 'policy' || !shape || !__sw_own(shape, 'policy') || !__sw_structPolicyValid(shape.policy)) return null;
6734
7054
  return shape.policy;
6735
7055
  }
7056
+ // A per-row grant table (grant({ row })) and the one rule it serves: its shares name rows of t (key k) in c.
7057
+ let __sw_structGrantRows;
7058
+ function __sw_structGrantRowFor(table) {
7059
+ if (!__sw_structGrantRows) {
7060
+ __sw_structGrantRows = {};
7061
+ const names = __sw_keys(SCHEMA);
7062
+ for (let i = 0; i < names.length; i++) {
7063
+ const policy = __sw_structResolvePolicy(names[i]);
7064
+ const branch = policy && policy.k === 'a' ? policy.p[1] : null;
7065
+ if (branch && branch.k === 'm' && branch.m.og === true && branch.m.g.length === 2) {
7066
+ __sw_structGrantRows[__sw_lower(branch.m.m)] = { t: __sw_lower(names[i]), k: branch.m.g[1], c: branch.m.mg[1] };
7067
+ }
7068
+ }
7069
+ }
7070
+ const lc = __sw_lower(table);
7071
+ return __sw_own(__sw_structGrantRows, lc) ? __sw_structGrantRows[lc] : undefined;
7072
+ }
6736
7073
  function __sw_structPolicyHasOwner(policy) {
6737
7074
  return policy.k === 'o' || policy.k === 'a' && (__sw_structPolicyHasOwner(policy.p[0]) || __sw_structPolicyHasOwner(policy.p[1]))
6738
7075
  || policy.k === 'n' && __sw_structPolicyHasOwner(policy.w);
@@ -6776,6 +7113,9 @@ var DB_POLICY_HELPERS_JS = `
6776
7113
  }
6777
7114
  return;
6778
7115
  }
7116
+ // A grant() branch correlates the managed owner (checked above) and, per row, the row's key,
7117
+ // which the shared renderer keeps fixed (query-sql.ts), as the managed compiler does.
7118
+ if (policy.p[1].k === 'm' && policy.p[1].m.og) return;
6779
7119
  __sw_structAssertPolicyWrite(cols, policy.p[1], verb, increments);
6780
7120
  return;
6781
7121
  }
@@ -6822,7 +7162,7 @@ var DB_POLICY_HELPERS_JS = `
6822
7162
  if (policy.k === 'a') {
6823
7163
  // The owner is stamped; an app-role branch has no placement, and a
6824
7164
  // group branch must name the group the row belongs to.
6825
- if (policy.p[1].k === 'r') return;
7165
+ if (policy.p[1].k === 'r' || (policy.p[1].k === 'm' && policy.p[1].m.og)) return;
6826
7166
  if (policy.p[1].k === 'g') {
6827
7167
  for (let i = 0; i < cols.length; i++) if (__sw_lower(cols[i].column) === __sw_lower(policy.p[1].c) && typeof cols[i].value === 'string' && cols[i].value) return;
6828
7168
  __sw_structThrow403('POLICY_GROUP_REQUIRED', 'A group-scoped insert must name the group the row belongs to in "' + policy.p[1].c + '".');
@@ -8625,7 +8965,8 @@ ${DB_POLICY_RESOLVE_SCOPE_JS} const member = __sw_structResolveMember(tab
8625
8965
  if (asServer === true || generatedRead) {
8626
8966
  // Explicit server code or the private generated selector has no local
8627
8967
  // principal. The managed API independently verifies the latter's token.
8628
- return { scope: generatedRead ? { mode: 'user', ownerColumn: ownerColumn } : { mode: 'server' }, principalId: undefined };
8968
+ const serverGrantRow = generatedRead ? undefined : __sw_structGrantRowFor(table);
8969
+ return { scope: generatedRead ? { mode: 'user', ownerColumn: ownerColumn } : serverGrantRow ? { mode: 'server', grantRow: serverGrantRow } : { mode: 'server' }, principalId: undefined };
8629
8970
  }
8630
8971
  const identity = await __sw_structRequestIdentity(allowPublicGuest);
8631
8972
  let id = identity && identity.id;
@@ -8660,7 +9001,8 @@ ${DB_POLICY_RESOLVE_SCOPE_JS} const member = __sw_structResolveMember(tab
8660
9001
  err.__sw_expose = true;
8661
9002
  throw err;
8662
9003
  }
8663
- return { scope: { mode: 'user', ownerColumn: ownerColumn }, principalId: id };
9004
+ const grantRow = __sw_structGrantRowFor(table);
9005
+ return { scope: grantRow ? { mode: 'user', ownerColumn: ownerColumn, grantRow: grantRow } : { mode: 'user', ownerColumn: ownerColumn }, principalId: id };
8664
9006
  }
8665
9007
 
8666
9008
  // The scope argument is GONE (tsk_5b91e0e4): scoping is automatic and
@@ -9463,14 +9805,14 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
9463
9805
  if (decided.scope.mode === 'user') __sw_structAssertNotOwnerColumn(cols, decided.scope.ownerColumn, 'insert');
9464
9806
  if (decided.scope.mode === 'policy') __sw_structAssertPolicyWrite(cols, decided.scope.policy, 'insert');
9465
9807
  if (decided.scope.mode === 'policy') __sw_structAssertPolicyInsert(cols, decided.scope.policy);
9466
- if (decided.scope.mode === 'policy' && onConflict !== undefined) {
9467
- __sw_structThrow('sw.db.insert on policy-scoped tables does not support onConflict yet. Insert without onConflict.');
9808
+ if (decided.scope.mode === 'member') __sw_structAssertMemberGroupPresent(cols, decided.scope.member, 'insert');
9809
+ // An upsert under a rule needs create (checked above) and, for "update", update on the
9810
+ // conflicting row; a rule that never grants update refuses before anything runs.
9811
+ if (onConflict === 'update' && decided.scope.mode === 'policy' && !__sw_structPolicyAllows(decided.scope.policy, 'update')) {
9812
+ __sw_structThrow403('POLICY_OPERATION_NOT_ALLOWED', 'sw.db.insert on "' + table + '": onConflict: "update" updates the existing row, and the declared private policy does not grant update. Use onConflict: "ignore", or grant update.');
9468
9813
  }
9469
- if (decided.scope.mode === 'member') {
9470
- if (onConflict !== undefined) {
9471
- __sw_structThrow('sw.db.insert on "' + table + '": onConflict is not supported on membership-scoped tables yet. Insert without onConflict.');
9472
- }
9473
- __sw_structAssertMemberGroupPresent(cols, decided.scope.member, 'insert');
9814
+ if (onConflict === 'update' && decided.scope.mode === 'member' && !__sw_structMemberAllows(decided.scope.member, 'update')) {
9815
+ __sw_structThrow403('MEMBER_OPERATION_NOT_ALLOWED', 'sw.db.insert on "' + table + '": onConflict: "update" updates the existing row, and member() does not grant update. Use onConflict: "ignore", or grant update.');
9474
9816
  }
9475
9817
  if (cols.length === 0 && decided.scope.mode !== 'user'
9476
9818
  && !(decided.scope.mode === 'policy' && __sw_structPolicyHasOwner(decided.scope.policy))) __sw_structThrow('sw.db.insert requires at least one column value.');
@@ -9601,14 +9943,17 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
9601
9943
  });
9602
9944
  return send(prepared);
9603
9945
  }
9604
- // Standalone exact-key caller writes share the browser's non-disclosing
9605
- // missing-row result. Bulk/conditional writes and atomic intents retain
9606
- // their zero-change contract; no existence probe bypasses authorization.
9607
- async function __sw_structExecCallerWrite(prepared, authority) {
9946
+ // Standalone exact-key caller UPDATES share the browser's non-disclosing
9947
+ // missing-row result. A caller REMOVE that matches nothing returns the
9948
+ // zero-change result instead, like bulk/conditional writes (founder
9949
+ // decision, tsk_b7f42808): a missing row and a row outside the caller's
9950
+ // access give the same { data: [], count: 0, changes: 0 }, so neither
9951
+ // reveals existence. No existence probe bypasses authorization.
9952
+ async function __sw_structExecCallerWrite(prepared, authority, missThrows) {
9608
9953
  const ir = prepared.ir;
9609
9954
  const shape = __sw_structManagedShape(ir.table.name);
9610
9955
  const pk = shape && (shape.primaryKey !== undefined ? shape.primaryKey : shape.clientPrimaryKey);
9611
- const keyed = authority !== __sw_structServerNamespace && ir.scope.mode !== 'server'
9956
+ const keyed = missThrows && authority !== __sw_structServerNamespace && ir.scope.mode !== 'server'
9612
9957
  && !(ir.scope.mode === 'unscoped' && prepared.principalId === undefined && __sw_structResolveIntent(ir.table.name) === 'server_only')
9613
9958
  && typeof pk === 'string' && ir.where.length === 1
9614
9959
  && ir.where[0].op === 'eq' && __sw_lower(ir.where[0].column) === __sw_lower(pk);
@@ -9618,11 +9963,11 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
9618
9963
  }
9619
9964
  async function __sw_structUpdate(table, spec, authority) {
9620
9965
  const prepared = await __sw_structPrepareUpdate(table, spec, authority);
9621
- return __sw_structExecCallerWrite(prepared, authority);
9966
+ return __sw_structExecCallerWrite(prepared, authority, true);
9622
9967
  }
9623
9968
  async function __sw_structRemove(table, spec, authority) {
9624
9969
  const prepared = await __sw_structPrepareRemove(table, spec, authority);
9625
- return __sw_structExecCallerWrite(prepared, authority);
9970
+ return __sw_structExecCallerWrite(prepared, authority, false);
9626
9971
  }
9627
9972
 
9628
9973
  async function __sw_structTransaction(intents, authority) {
@@ -9647,7 +9992,8 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
9647
9992
  // back (checked in the same transaction, after earlier intents).
9648
9993
  // Updates and removes are already limited to rows the caller may
9649
9994
  // reach by their WHERE predicate.
9650
- if (item.ir.kind === 'insert' && (item.ir.scope.mode === 'member' || item.ir.scope.mode === 'policy')) item.ir.txGuard = true;
9995
+ if (item.ir.kind === 'insert' && (item.ir.scope.mode === 'member' || item.ir.scope.mode === 'policy'
9996
+ || (item.ir.scope.mode === 'user' || item.ir.scope.mode === 'server') && item.ir.scope.grantRow)) item.ir.txGuard = true;
9651
9997
  // A transaction is never keyed: an explicit key on one of its inserts refuses before anything runs.
9652
9998
  if (item.idempotencyKey !== undefined) {
9653
9999
  throw __sw_keyedError('IDEMPOTENCY_UNSUPPORTED', 'sw.db.tx: an insert inside a transaction cannot take idempotencyKey. Nothing ran.', 400);
@@ -13593,16 +13939,14 @@ var AUTH_JS = ` auth: (function () {
13593
13939
  return invite;
13594
13940
  },
13595
13941
 
13596
- // MFA / TOTP. Enrollment is activated only after an independent,
13597
- // recent password or stored-email proof. challenge() remains the
13598
- // second-factor call after password /login returned mfa_required:true.
13942
+ // Email sign-in codes (tsk_8fe91eb5). The app owner sets the policy ('off', 'optional', 'required') with a
13943
+ // developer key. A user with the factor answers every password sign-in with { mfa_required: true,
13944
+ // mfa_token, methods: ['email'] } and no session; challenge() (or challengeWithCookie) completes it with the
13945
+ // code. The explicit forms send the user's JWT through userTokenJSON (project-bound); platformJSON would
13946
+ // replace it with the developer key (pfb_ab7215b47cb9).
13599
13947
  mfa: {
13600
- // Begin enrolment. Caller must pass the user's JWT \u2014 MFA is
13601
- // always tied to a logged-in account. These explicit forms send it
13602
- // through userTokenJSON (project-bound); platformJSON would replace
13603
- // it with the developer key (pfb_ab7215b47cb9). Returns { secret,
13604
- // enrollment_id, otpauth_uri, issuer, account } \u2014 render otpauth_uri as a
13605
- // QR code in the UI.
13948
+ // Turn codes on (policy 'optional' or 'required'): mails a code to the verified address.
13949
+ // Resolves { challenge_id, delivery, expires_in }, or { enrolled: true } when already on.
13606
13950
  enroll(opts) {
13607
13951
  opts = opts || {};
13608
13952
  if (!opts.token) {
@@ -13612,9 +13956,10 @@ var AUTH_JS = ` auth: (function () {
13612
13956
  }
13613
13957
  return userTokenJSON('/v1/auth/mfa/enroll', opts.token, {
13614
13958
  method: 'POST',
13615
- body: JSON.stringify({}),
13959
+ body: JSON.stringify({ method: 'email' }),
13616
13960
  });
13617
13961
  },
13962
+ // Authenticator-app activation is not available: answers 501 AUTH_FEATURE_UNAVAILABLE.
13618
13963
  // Establish recent authority for the exact enrollment. Password
13619
13964
  // success returns activation_token directly. Email sends a code and
13620
13965
  // returns challenge_id for verifyReauthentication().
@@ -13650,29 +13995,22 @@ var AUTH_JS = ` auth: (function () {
13650
13995
  }),
13651
13996
  });
13652
13997
  },
13653
- // Confirm enrolment by submitting the first 6-digit code the
13654
- // authenticator generates plus the one-time activation authority.
13655
- // Returns { enabled:true, backup_codes:[...] } once.
13998
+ // Confirm enrolment with the code enroll() mailed. Resolves { enrolled: true, method: 'email' }.
13656
13999
  verify(opts) {
13657
14000
  opts = opts || {};
13658
- if (!opts.token || !opts.enrollment_id || !opts.activation_token || !opts.code) {
13659
- const err = new Error('sw.auth.mfa.verify: token, enrollment_id, activation_token, and code are required');
14001
+ if (!opts.token || !opts.challenge_id || !opts.code) {
14002
+ const err = new Error('sw.auth.mfa.verify: token, challenge_id, and code are required');
13660
14003
  err.code = 'VALIDATION_ERROR';
13661
14004
  throw err;
13662
14005
  }
13663
14006
  return userTokenJSON('/v1/auth/mfa/verify', opts.token, {
13664
14007
  method: 'POST',
13665
- body: JSON.stringify({
13666
- enrollment_id: opts.enrollment_id,
13667
- activation_token: opts.activation_token,
13668
- code: opts.code,
13669
- }),
14008
+ body: JSON.stringify({ challenge_id: opts.challenge_id, code: opts.code }),
13670
14009
  });
13671
14010
  },
13672
14011
  // Second-factor exchange after /login returned
13673
- // { mfa_required:true, mfa_token }. Code is either the 6-digit
13674
- // TOTP or a 10-char backup code. Returns the same shape as
13675
- // /login on success.
14012
+ // { mfa_required:true, mfa_token }: the 6-digit code from the email.
14013
+ // Returns the same shape as /login on success.
13676
14014
  challenge(opts) {
13677
14015
  opts = opts || {};
13678
14016
  if (!opts.mfa_token || !opts.code) {
@@ -13689,6 +14027,20 @@ var AUTH_JS = ` auth: (function () {
13689
14027
  }),
13690
14028
  });
13691
14029
  },
14030
+ // Mail a new code for the same sign-in (60 s apart, 5 an hour per account). The sign-in keeps its
14031
+ // original 5-minute expiry. Resolves { sent, delivery, expires_in }.
14032
+ resend(opts) {
14033
+ opts = opts || {};
14034
+ if (!opts.mfa_token) {
14035
+ const err = new Error('sw.auth.mfa.resend: mfa_token is required');
14036
+ err.code = 'VALIDATION_ERROR';
14037
+ throw err;
14038
+ }
14039
+ return platformJSON('/v1/auth/mfa/challenge', {
14040
+ method: 'POST',
14041
+ body: JSON.stringify({ project_id: projectId, mfa_token: opts.mfa_token, resend: true }),
14042
+ });
14043
+ },
13692
14044
  // Complete the password-login MFA branch as a cookie session. This
13693
14045
  // adds no authority: it brokers the same developer-authenticated
13694
14046
  // challenge(), then keeps the returned access/refresh credentials
@@ -13703,28 +14055,55 @@ var AUTH_JS = ` auth: (function () {
13703
14055
  __sw_setAuthCookies(session.access, session.refresh);
13704
14056
  return { user: session.user, expires_at: __sw_preflightJwt(session.access).expSec * 1000 };
13705
14057
  },
13706
- // Disable MFA. Requires a fresh code (TOTP or backup) so a
13707
- // stolen JWT alone can't turn it off.
14058
+ // Turn codes off, in two steps: { token, password } re-checks the password and mails a code
14059
+ // (resolves { challenge_id, delivery, expires_in }); { token, challenge_id, code } removes the factor
14060
+ // (resolves { enrolled: false }). Refused under a 'required' policy (409 AUTH_MFA_REQUIRED_BY_APP).
13708
14061
  unenroll(opts) {
13709
14062
  opts = opts || {};
13710
- if (!opts.token || !opts.code) {
13711
- const err = new Error('sw.auth.mfa.unenroll: token and code are required');
14063
+ const confirming = opts.challenge_id !== undefined;
14064
+ if (!opts.token || (confirming ? !opts.code : !opts.password)) {
14065
+ const err = new Error('sw.auth.mfa.unenroll: token and either password, or challenge_id and code, are required');
13712
14066
  err.code = 'VALIDATION_ERROR';
13713
14067
  throw err;
13714
14068
  }
13715
14069
  return userTokenJSON('/v1/auth/mfa/unenroll', opts.token, {
13716
14070
  method: 'POST',
13717
- body: JSON.stringify({ code: opts.code }),
14071
+ body: JSON.stringify(confirming ? { challenge_id: opts.challenge_id, code: opts.code } : { password: opts.password }),
14072
+ });
14073
+ },
14074
+ // For a user who cannot receive a code (no session needed): { email, password } starts a 72-hour wait
14075
+ // and mails a notice with a cancel link (202 { removal_pending, removes_at }); the same call after the
14076
+ // wait removes the factor ({ enrolled: false }). Refused (503) when the app cannot email the user.
14077
+ recover(opts) {
14078
+ opts = opts || {};
14079
+ if (!opts.email || !opts.password) {
14080
+ const err = new Error('sw.auth.mfa.recover: email and password are required');
14081
+ err.code = 'VALIDATION_ERROR';
14082
+ throw err;
14083
+ }
14084
+ return platformJSON('/v1/auth/mfa/recovery', {
14085
+ method: 'POST',
14086
+ body: JSON.stringify({ project_id: projectId, email: opts.email, password: opts.password }),
13718
14087
  });
13719
14088
  },
13720
- // The same enrolment steps for the request's own session (cookie or
13721
- // Bearer). enrollment_id, activation_token, challenge_id and the
13722
- // codes are the step's own one-time credentials and stay explicit.
13723
- // Activation ends every session of the account, this one included,
13724
- // so verifyWithCookie clears the cookies after returning the
13725
- // backup codes; the next sign-in asks for the second factor.
14089
+ // Cancel a pending removal with the token from the notice's cancel link (no session or code).
14090
+ // Resolves { outcome: 'cancelled' | 'already_closed' | 'completed', message }.
14091
+ cancelRecovery(opts) {
14092
+ opts = opts || {};
14093
+ if (!opts.token) {
14094
+ const err = new Error('sw.auth.mfa.cancelRecovery: token is required');
14095
+ err.code = 'VALIDATION_ERROR';
14096
+ throw err;
14097
+ }
14098
+ return platformJSON('/v1/auth/mfa/recovery/cancel', {
14099
+ method: 'POST',
14100
+ body: JSON.stringify({ token: opts.token }),
14101
+ });
14102
+ },
14103
+ // The same steps for the request's own session (cookie or Bearer). challenge_id and the codes are the
14104
+ // step's own one-time credentials and stay explicit. Turning codes on or off ends no session.
13726
14105
  async enrollWithCookie(req) {
13727
- return sessionChange('mfa.enrollWithCookie', req, '/v1/auth/mfa/enroll', { body: {} });
14106
+ return sessionChange('mfa.enrollWithCookie', req, '/v1/auth/mfa/enroll', { body: { method: 'email' } });
13728
14107
  },
13729
14108
  async reauthenticateWithCookie(req, opts) {
13730
14109
  return sessionChange('mfa.reauthenticateWithCookie', req, '/v1/auth/mfa/activation/reauth', { body: function () {
@@ -13743,15 +14122,19 @@ var AUTH_JS = ` auth: (function () {
13743
14122
  } });
13744
14123
  },
13745
14124
  async verifyWithCookie(req, opts) {
13746
- return sessionChange('mfa.verifyWithCookie', req, '/v1/auth/mfa/verify', { ends: true, body: function () {
13747
- opts = requireFields('mfa.verifyWithCookie', opts, ['enrollment_id', 'activation_token', 'code']);
13748
- return { enrollment_id: opts.enrollment_id, activation_token: opts.activation_token, code: opts.code };
14125
+ return sessionChange('mfa.verifyWithCookie', req, '/v1/auth/mfa/verify', { body: function () {
14126
+ opts = requireFields('mfa.verifyWithCookie', opts, ['challenge_id', 'code']);
14127
+ return { challenge_id: opts.challenge_id, code: opts.code };
13749
14128
  } });
13750
14129
  },
13751
14130
  async unenrollWithCookie(req, opts) {
13752
14131
  return sessionChange('mfa.unenrollWithCookie', req, '/v1/auth/mfa/unenroll', { body: function () {
13753
- opts = requireFields('mfa.unenrollWithCookie', opts, ['code']);
13754
- return { code: opts.code };
14132
+ if (opts && opts.challenge_id !== undefined) {
14133
+ opts = requireFields('mfa.unenrollWithCookie', opts, ['challenge_id', 'code']);
14134
+ return { challenge_id: opts.challenge_id, code: opts.code };
14135
+ }
14136
+ opts = requireFields('mfa.unenrollWithCookie', opts, ['password']);
14137
+ return { password: opts.password };
13755
14138
  } });
13756
14139
  },
13757
14140
  },
@@ -16031,7 +16414,7 @@ function __sw_dataIsDuplicateConflict(error) {
16031
16414
  }
16032
16415
  function __sw_dataPolicyUsesMembership(policy, table) {
16033
16416
  if (!policy || typeof policy !== 'object') return false;
16034
- if (policy.k === 'm') return policy.m && policy.m.m === table;
16417
+ if (policy.k === 'm') return policy.m && policy.m.m === table && !policy.m.og;
16035
16418
  if (policy.k === 'a') return policy.p.some(item => __sw_dataPolicyUsesMembership(item, table));
16036
16419
  // allOf: a membership table named by either slot stays closed to browser writes.
16037
16420
  if (policy.k === 'n') return __sw_dataPolicyUsesMembership(policy.g, table) || __sw_dataPolicyUsesMembership(policy.w, table);
@@ -17180,20 +17563,25 @@ function reportedFailureFields(platformCtx, requestId) {
17180
17563
  return out;
17181
17564
  }
17182
17565
 
17183
- /** Add the correlation handle without changing responses when tracing is absent. */
17184
- function attachTraceId(response, platformCtx) {
17566
+ /** Add the correlation handle without changing responses when tracing is absent. handlerResponse is true only
17567
+ * at the handler-success site for a Response the handler itself returned. */
17568
+ function attachTraceId(response, platformCtx, handlerResponse) {
17185
17569
  try {
17186
17570
  const t = platformCtx && platformCtx.__sw_trace;
17187
- // X-Sw-Route-Pattern is the PLATFORM's (the edge's completion log): a function's own copy never
17188
- // passes through, with or without a trace; only the runtime's matched pattern is set below.
17571
+ // X-Sw-Route-Pattern and X-Sw-Response-Source are the PLATFORM's (the edge's completion log): a function's
17572
+ // own copy never passes through, with or without a trace; only the runtime's values are set below.
17189
17573
  if (!t) {
17190
- if (!response.headers.has('X-Sw-Route-Pattern')) return response;
17574
+ if (!response.headers.has('X-Sw-Route-Pattern') && !response.headers.has('X-Sw-Response-Source') && handlerResponse !== true) return response;
17191
17575
  const stripped = new Headers(response.headers);
17192
17576
  stripped.delete('X-Sw-Route-Pattern');
17577
+ stripped.delete('X-Sw-Response-Source');
17578
+ if (handlerResponse === true) stripped.set('X-Sw-Response-Source', 'handler');
17193
17579
  return new Response(response.body, { status: response.status, statusText: response.statusText, headers: stripped });
17194
17580
  }
17195
17581
  const headers = new Headers(response.headers);
17196
17582
  headers.delete('X-Sw-Route-Pattern');
17583
+ headers.delete('X-Sw-Response-Source');
17584
+ if (handlerResponse === true) headers.set('X-Sw-Response-Source', 'handler');
17197
17585
  headers.set('X-Trace-Id', t.traceId);
17198
17586
  headers.set('traceparent', '00-' + t.traceId + '-' + t.spanId + '-' + (t.sampled ? '01' : '00'));
17199
17587
  headers.set('X-Trace-Sampling', t.sampled
@@ -17577,6 +17965,9 @@ function isCanonicalBigintText(value) {
17577
17965
  }
17578
17966
 
17579
17967
  // worker/src/utils/db-schema-deploy/client-permissions.ts
17968
+ function tableAllowsClientWrites(table) {
17969
+ return Boolean(table.client && (table.client.create !== null || table.client.update !== null || table.client.delete || table.client.intake !== void 0));
17970
+ }
17580
17971
  function parseClientPermissions(value, columns, scope) {
17581
17972
  const fail = (message) => ({ ok: false, message });
17582
17973
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -18067,6 +18458,11 @@ var SW_INIT_JS = `
18067
18458
  globalThis.sw = globalThis.sw || {};
18068
18459
  ${DB_OUTCOME_ERROR_JS}
18069
18460
 
18461
+ // Responses this runtime made (sw.endpoint refusals and caught throws). index.mjs captures the reader and
18462
+ // removes its public hook before any user module loads, so customer code can neither read nor change it.
18463
+ const __sw_runtimeMadeResponses = new WeakSet();
18464
+ globalThis.sw.__isRuntimeMadeResponse = function (response) { return __sw_runtimeMadeResponses.has(response); };
18465
+
18070
18466
  // Legacy USER_ENV supplies customer-only process.env after native reflection is denied.
18071
18467
  // index.mjs captures this initializer before lazy module load and removes its public hook.
18072
18468
  globalThis.sw.__wireProcessEnv = function __sw_wireProcessEnv(runtimeEnv) {
@@ -18176,23 +18572,32 @@ function __sw_corsHeaders(corsCfg, request) {
18176
18572
  };
18177
18573
  }
18178
18574
 
18575
+ /** Record a Response this runtime made itself (never the handler's). */
18576
+ function __sw_runtimeMade(response) {
18577
+ __sw_runtimeMadeResponses.add(response);
18578
+ return response;
18579
+ }
18580
+
18179
18581
  function __sw_jsonError(status, code, message, extras) {
18180
18582
  var body = Object.assign({ ok: false, error: code, message: message }, extras || {});
18181
- return new Response(JSON.stringify(body), {
18583
+ return __sw_runtimeMade(new Response(JSON.stringify(body), {
18182
18584
  status: status,
18183
18585
  headers: { 'Content-Type': 'application/json' },
18184
- });
18586
+ }));
18185
18587
  }
18186
18588
 
18187
18589
  function __sw_mergeHeaders(response, extra) {
18188
18590
  if (!extra || Object.keys(extra).length === 0) return response;
18189
18591
  var headers = new Headers(response.headers);
18190
18592
  for (var k of Object.keys(extra)) headers.set(k, extra[k]);
18191
- return new Response(response.body, {
18593
+ var merged = new Response(response.body, {
18192
18594
  status: response.status,
18193
18595
  statusText: response.statusText,
18194
18596
  headers: headers,
18195
18597
  });
18598
+ // The copy keeps the original's provenance.
18599
+ if (__sw_runtimeMadeResponses.has(response)) __sw_runtimeMadeResponses.add(merged);
18600
+ return merged;
18196
18601
  }
18197
18602
 
18198
18603
  /**
@@ -18237,7 +18642,7 @@ globalThis.sw.endpoint = function (config) {
18237
18642
  const console = sw.__sw_nativeConsole || globalThis.console;
18238
18643
  var corsHeaders = __sw_corsHeaders(cors, request);
18239
18644
  if (request.method === 'OPTIONS') {
18240
- return new Response(null, { status: 204, headers: corsHeaders });
18645
+ return __sw_runtimeMade(new Response(null, { status: 204, headers: corsHeaders }));
18241
18646
  }
18242
18647
 
18243
18648
  // Auth.
@@ -18293,10 +18698,10 @@ globalThis.sw.endpoint = function (config) {
18293
18698
  var code = (r && r.error) || 'RATE_LIMITED';
18294
18699
  var message = (r && r.message) || 'Too many requests.';
18295
18700
  return __sw_mergeHeaders(
18296
- new Response(
18701
+ __sw_runtimeMade(new Response(
18297
18702
  JSON.stringify({ ok: false, error: code, message: message, retry_after: retry }),
18298
18703
  { status: 429, headers: { 'Content-Type': 'application/json', 'Retry-After': String(retry) } }
18299
- ),
18704
+ )),
18300
18705
  corsHeaders
18301
18706
  );
18302
18707
  }
@@ -18315,10 +18720,10 @@ globalThis.sw.endpoint = function (config) {
18315
18720
  rlErr && rlErr.code, rlErr && rlErr.status, rlErr && rlErr.message
18316
18721
  );
18317
18722
  return __sw_mergeHeaders(
18318
- new Response(
18723
+ __sw_runtimeMade(new Response(
18319
18724
  JSON.stringify({ ok: false, error: 'RATE_LIMIT_UNAVAILABLE', message: 'Rate limiting is temporarily unavailable; this request was blocked. A later identical request can succeed.' }),
18320
18725
  { status: 503, headers: { 'Content-Type': 'application/json', 'Retry-After': String(rateLimit.windowSeconds) } }
18321
- ),
18726
+ )),
18322
18727
  corsHeaders
18323
18728
  );
18324
18729
  }
@@ -18403,7 +18808,7 @@ function isApiIndexHandlerPath(file) {
18403
18808
  return /^api\/.+\/index\.(?:ts|tsx|mts|js|mjs|jsx)$/.test(file) || /^api\/index\.(?:ts|tsx|mts|js|mjs|jsx)$/.test(file);
18404
18809
  }
18405
18810
 
18406
- // ../../../../../../pmwork/wt/files-migration-c0-rig/worker/node_modules/acorn/dist/acorn.mjs
18811
+ // worker/node_modules/acorn/dist/acorn.mjs
18407
18812
  var astralIdentifierCodes = [509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166, 1, 574, 3, 9, 9, 7, 9, 32, 4, 318, 1, 80, 3, 71, 10, 50, 3, 123, 2, 54, 14, 32, 10, 3, 1, 11, 3, 46, 10, 8, 0, 46, 9, 7, 2, 37, 13, 2, 9, 6, 1, 45, 0, 13, 2, 49, 13, 9, 3, 2, 11, 83, 11, 7, 0, 3, 0, 158, 11, 6, 9, 7, 3, 56, 1, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 68, 8, 2, 0, 3, 0, 2, 3, 2, 4, 2, 0, 15, 1, 83, 17, 10, 9, 5, 0, 82, 19, 13, 9, 214, 6, 3, 8, 28, 1, 83, 16, 16, 9, 82, 12, 9, 9, 7, 19, 58, 14, 5, 9, 243, 14, 166, 9, 71, 5, 2, 1, 3, 3, 2, 0, 2, 1, 13, 9, 120, 6, 3, 6, 4, 0, 29, 9, 41, 6, 2, 3, 9, 0, 10, 10, 47, 15, 343, 9, 54, 7, 2, 7, 17, 9, 57, 21, 2, 13, 123, 5, 4, 0, 2, 1, 2, 6, 2, 0, 9, 9, 49, 4, 2, 1, 2, 4, 9, 9, 330, 3, 10, 1, 2, 0, 49, 6, 4, 4, 14, 10, 5350, 0, 7, 14, 11465, 27, 2343, 9, 87, 9, 39, 4, 60, 6, 26, 9, 535, 9, 470, 0, 2, 54, 8, 3, 82, 0, 12, 1, 19628, 1, 4178, 9, 519, 45, 3, 22, 543, 4, 4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 513, 54, 5, 49, 9, 0, 15, 0, 23, 4, 2, 14, 1361, 6, 2, 16, 3, 6, 2, 1, 2, 4, 101, 0, 161, 6, 10, 9, 357, 0, 62, 13, 499, 13, 245, 1, 2, 9, 726, 6, 110, 6, 6, 9, 4759, 9, 787719, 239];
18408
18813
  var astralIdentifierStartCodes = [0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4, 48, 48, 31, 14, 29, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 19, 35, 5, 35, 5, 39, 9, 51, 13, 10, 2, 14, 2, 6, 2, 1, 2, 10, 2, 14, 2, 6, 2, 1, 4, 51, 13, 310, 10, 21, 11, 7, 25, 5, 2, 41, 2, 8, 70, 5, 3, 0, 2, 43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 66, 18, 2, 1, 11, 21, 11, 25, 71, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 28, 43, 28, 4, 28, 36, 7, 2, 27, 28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 56, 50, 14, 50, 14, 35, 39, 27, 10, 22, 251, 41, 7, 1, 17, 2, 60, 28, 11, 0, 9, 21, 43, 17, 47, 20, 28, 22, 13, 52, 58, 1, 3, 0, 14, 44, 33, 24, 27, 35, 30, 0, 3, 0, 9, 34, 4, 0, 13, 47, 15, 3, 22, 0, 2, 0, 36, 17, 2, 24, 20, 1, 64, 6, 2, 0, 2, 3, 2, 14, 2, 9, 8, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 4, 0, 19, 0, 13, 4, 31, 9, 2, 0, 3, 0, 2, 37, 2, 0, 26, 0, 2, 0, 45, 52, 19, 3, 21, 2, 31, 47, 21, 1, 2, 0, 185, 46, 42, 3, 37, 47, 21, 0, 60, 42, 14, 0, 72, 26, 38, 6, 186, 43, 117, 63, 32, 7, 3, 0, 3, 7, 2, 1, 2, 23, 16, 0, 2, 0, 95, 7, 3, 38, 17, 0, 2, 0, 29, 0, 11, 39, 8, 0, 22, 0, 12, 45, 20, 0, 19, 72, 200, 32, 32, 8, 2, 36, 18, 0, 50, 29, 113, 6, 2, 1, 2, 37, 22, 0, 26, 5, 2, 1, 2, 31, 15, 0, 328, 18, 16, 0, 2, 12, 2, 33, 125, 0, 80, 921, 103, 110, 18, 195, 2637, 96, 16, 1071, 18, 5, 26, 3994, 6, 582, 6842, 29, 1763, 568, 8, 30, 18, 78, 18, 29, 19, 47, 17, 3, 32, 20, 6, 18, 433, 44, 212, 63, 129, 74, 6, 0, 67, 12, 65, 1, 2, 0, 29, 6135, 9, 1237, 42, 9, 8936, 3, 2, 6, 2, 1, 2, 290, 16, 0, 30, 2, 3, 0, 15, 3, 9, 395, 2309, 106, 6, 12, 4, 8, 8, 9, 5991, 84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3, 7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 1845, 30, 7, 5, 262, 61, 147, 44, 11, 6, 17, 0, 322, 29, 19, 43, 485, 27, 229, 29, 3, 0, 496, 6, 2, 3, 2, 1, 2, 14, 2, 196, 60, 67, 8, 0, 1205, 3, 2, 26, 2, 1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2, 2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2, 3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42719, 33, 4153, 7, 221, 3, 5761, 15, 7472, 16, 621, 2467, 541, 1507, 4938, 6, 4191];
18409
18814
  var nonASCIIidentifierChars = "\u200C\u200D\xB7\u0300-\u036F\u0387\u0483-\u0487\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u0669\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u06F0-\u06F9\u0711\u0730-\u074A\u07A6-\u07B0\u07C0-\u07C9\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0897-\u089F\u08CA-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0966-\u096F\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09E6-\u09EF\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A66-\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AE6-\u0AEF\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B55-\u0B57\u0B62\u0B63\u0B66-\u0B6F\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0BE6-\u0BEF\u0C00-\u0C04\u0C3C\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0CE6-\u0CEF\u0CF3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D66-\u0D6F\u0D81-\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0E50-\u0E59\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0ED0-\u0ED9\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1040-\u1049\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109D\u135D-\u135F\u1369-\u1371\u1712-\u1715\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u17E0-\u17E9\u180B-\u180D\u180F-\u1819\u18A9\u1920-\u192B\u1930-\u193B\u1946-\u194F\u19D0-\u19DA\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AB0-\u1ABD\u1ABF-\u1ACE\u1B00-\u1B04\u1B34-\u1B44\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BF3\u1C24-\u1C37\u1C40-\u1C49\u1C50-\u1C59\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DFF\u200C\u200D\u203F\u2040\u2054\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\u30FB\uA620-\uA629\uA66F\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA82C\uA880\uA881\uA8B4-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F1\uA8FF-\uA909\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9D0-\uA9D9\uA9E5\uA9F0-\uA9F9\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA50-\uAA59\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uABF0-\uABF9\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFF10-\uFF19\uFF3F\uFF65";
@@ -24015,6 +24420,140 @@ function tokenizer2(input, options) {
24015
24420
  return Parser.tokenizer(input, options);
24016
24421
  }
24017
24422
 
24423
+ // worker/src/utils/db-schema-deploy/extract-schema-grants.ts
24424
+ function readGrantScope(r, table, line, fail) {
24425
+ r.expectPunct("(", "after grant");
24426
+ r.expectPunct("{", "in grant options");
24427
+ let membership, grantee, row;
24428
+ let operations;
24429
+ const seen = /* @__PURE__ */ new Set();
24430
+ while (!r.tryPunct("}")) {
24431
+ const key = r.readKey("in grant options");
24432
+ if (seen.has(key.name)) fail(key.line, `Duplicate grant option "${key.name}".`);
24433
+ seen.add(key.name);
24434
+ r.expectPunct(":", "after grant option");
24435
+ if (key.name === "table" || key.name === "grantee" || key.name === "row") {
24436
+ const value = r.readLiteral("in grant options");
24437
+ 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.");
24438
+ if (key.name === "table") membership = value.toLowerCase();
24439
+ else if (key.name === "grantee") grantee = value.toLowerCase();
24440
+ else row = value.toLowerCase();
24441
+ } else if (key.name === "operations") {
24442
+ const ops = [];
24443
+ r.expectPunct("[", "in grant operations");
24444
+ while (!r.tryPunct("]")) {
24445
+ const op = r.readLiteral("in grant operations");
24446
+ if (op !== "read" && op !== "update") fail(key.line, "grant operations allow only read and update.");
24447
+ if (ops.includes(op)) fail(key.line, "Duplicate grant operation.");
24448
+ ops.push(op);
24449
+ if (!r.tryPunct(",")) {
24450
+ r.expectPunct("]", "closing grant operations");
24451
+ break;
24452
+ }
24453
+ }
24454
+ if (!ops.includes("read")) fail(key.line, "grant operations require read; update alone is not allowed.");
24455
+ operations = ops.includes("update") ? ["read", "update"] : ["read"];
24456
+ } else fail(key.line, `Unknown grant option "${key.name}". Allowed: table, grantee, row, operations.`);
24457
+ if (!r.tryPunct(",")) {
24458
+ r.expectPunct("}", "closing grant options");
24459
+ break;
24460
+ }
24461
+ }
24462
+ r.tryPunct(",");
24463
+ r.expectPunct(")", "closing grant");
24464
+ 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.`);
24465
+ 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.`);
24466
+ return { kind: "member", group: ["user_id"], membership, memberUser: grantee, memberGroup: row === void 0 ? ["user_id"] : ["user_id", row], operations, ownerGrant: true };
24467
+ }
24468
+ function resolveGrantRowKeys(scope, key, table, line, fail) {
24469
+ const visit = (node) => {
24470
+ if (node.kind === "member" && node.ownerGrant && node.memberGroup.length === 2 && node.group.length === 1) {
24471
+ 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.`);
24472
+ node.group = ["user_id", key.toLowerCase()];
24473
+ } else if (node.kind === "any_of") node.policies.forEach(visit);
24474
+ else if (node.kind === "all_of") {
24475
+ visit(node.gate);
24476
+ visit(node.who);
24477
+ }
24478
+ };
24479
+ if (scope.kind === "policy") visit(scope.policy);
24480
+ }
24481
+ function validateDeclaredMemberships(tables, tableByName, knownColumns, errors) {
24482
+ const memberPolicies = (scope) => {
24483
+ const out = [];
24484
+ const visit = (node) => {
24485
+ if (node.kind === "member") out.push(node);
24486
+ else if (node.kind === "any_of") node.policies.forEach(visit);
24487
+ else if (node.kind === "all_of") {
24488
+ visit(node.gate);
24489
+ visit(node.who);
24490
+ }
24491
+ };
24492
+ if (scope.kind === "member") out.push(scope);
24493
+ else if (scope.kind === "policy") visit(scope.policy);
24494
+ return out;
24495
+ };
24496
+ for (const t of tables) {
24497
+ for (const sc of memberPolicies(t.scope)) {
24498
+ const own = knownColumns(t);
24499
+ for (const g of sc.group) {
24500
+ if (!own.has(g)) {
24501
+ errors.push(
24502
+ `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.`
24503
+ );
24504
+ }
24505
+ }
24506
+ const membershipTable = tableByName.get(sc.membership);
24507
+ if (!membershipTable) {
24508
+ errors.push(
24509
+ `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.`
24510
+ );
24511
+ continue;
24512
+ }
24513
+ const membershipCols = knownColumns(membershipTable);
24514
+ if (sc.ownerGrant) {
24515
+ if (membershipTable.scope.kind !== "owner" || membershipTable.scope.column !== "user_id" || membershipTable.scope.visitors) {
24516
+ errors.push(`Grant table "${sc.membership}" must use owner() with managed user_id and authenticated owners.`);
24517
+ }
24518
+ 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.`);
24519
+ const grantee = membershipTable.columns.find((column) => column.name === sc.memberUser);
24520
+ if (!grantee || grantee.helper !== "text" || grantee.nullable) errors.push(`Grant grantee "${sc.membership}"."${sc.memberUser}" must be a required text column.`);
24521
+ if (sc.memberGroup.length === 2) {
24522
+ const row = membershipTable.columns.find((column) => column.name === sc.memberGroup[1]);
24523
+ const key = t.columns.find((column) => column.name === sc.group[1]);
24524
+ const keyHelper = key?.helper === "id" ? key.uuid ? "text" : "integer" : null;
24525
+ if (!row || row.nullable || row.helper !== keyHelper) {
24526
+ errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" must be a required ${keyHelper ?? "id-typed"} column holding a "${t.name}" id.`);
24527
+ } else if (row.hasDefault || row.references !== t.name || row.onDelete !== "cascade") {
24528
+ 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' }).`);
24529
+ }
24530
+ if (membershipTable.client?.update?.includes(sc.memberGroup[1])) {
24531
+ 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.`);
24532
+ }
24533
+ const users = tables.filter((other) => memberPolicies(other.scope).some((m) => m.ownerGrant && m.membership === sc.membership));
24534
+ if (users.length !== 1 || memberPolicies(t.scope).filter((m) => m.ownerGrant && m.membership === sc.membership).length !== 1) {
24535
+ 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.`);
24536
+ }
24537
+ }
24538
+ } else if (tableAllowsClientWrites(membershipTable)) {
24539
+ errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
24540
+ }
24541
+ if (!membershipCols.has(sc.memberUser)) {
24542
+ errors.push(
24543
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
24544
+ );
24545
+ }
24546
+ for (const mg of sc.ownerGrant ? [] : sc.memberGroup) {
24547
+ if (!membershipCols.has(mg)) {
24548
+ errors.push(
24549
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
24550
+ );
24551
+ }
24552
+ }
24553
+ }
24554
+ }
24555
+ }
24556
+
24018
24557
  // worker/src/utils/db-schema-deploy/schema-constants.ts
24019
24558
  var LITERAL_WORDS = /* @__PURE__ */ new Set(["true", "false", "null"]);
24020
24559
  var STATEMENT_WORDS = /* @__PURE__ */ new Set([
@@ -25084,8 +25623,9 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
25084
25623
  }
25085
25624
 
25086
25625
  // worker/src/utils/db-schema-deploy/extract-schema-roles.ts
25626
+ var EMAIL_DOMAIN = EMAIL_DOMAIN_PATTERN;
25087
25627
  var GROUP_KEYS = ["roles", "adminGrantableRoles"];
25088
- var APP_ROLE_KEYS = ["grantableBy"];
25628
+ var APP_ROLE_KEYS = ["grantableBy", "emailDomain"];
25089
25629
  function readStringList2(r, fail, ctx) {
25090
25630
  const open = r.peek();
25091
25631
  if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted role names, such as ['viewer'].`);
@@ -25129,7 +25669,7 @@ function readAppRoles(r, fail) {
25129
25669
  const key = r.readKey("for an app role name");
25130
25670
  if (out.some((role2) => role2.name === key.name)) fail(key.line, `app role "${key.name}" is declared twice.`);
25131
25671
  r.expectPunct(":", `after app role "${key.name}"`);
25132
- r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...] }`);
25672
+ r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...], emailDomain: '...' }`);
25133
25673
  const role = { name: key.name, grantableBy: [] };
25134
25674
  const seen = /* @__PURE__ */ new Set();
25135
25675
  while (!r.tryPunct("}")) {
@@ -25140,7 +25680,16 @@ function readAppRoles(r, fail) {
25140
25680
  if (seen.has(opt.name)) fail(opt.line, `app role "${key.name}" declares ${opt.name} twice.`);
25141
25681
  seen.add(opt.name);
25142
25682
  r.expectPunct(":", `after appRoles.${key.name}.${opt.name}`);
25143
- role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
25683
+ if (opt.name === "emailDomain") {
25684
+ const t = r.next();
25685
+ if (t.kind !== "string") fail(t.line, `appRoles.${key.name}.emailDomain must be one quoted domain, such as 'example.com'.`);
25686
+ if (!EMAIL_DOMAIN.test(t.value)) {
25687
+ 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.`);
25688
+ }
25689
+ role.emailDomain = t.value;
25690
+ } else {
25691
+ role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
25692
+ }
25144
25693
  if (!r.tryPunct(",")) {
25145
25694
  r.expectPunct("}", `closing app role "${key.name}"`);
25146
25695
  break;
@@ -25157,12 +25706,25 @@ function readAppRoles(r, fail) {
25157
25706
  function validateDeclaredRoles(groups, appRoles) {
25158
25707
  const delegation = /* @__PURE__ */ Object.create(null);
25159
25708
  for (const role of appRoles ?? []) delegation[role.name] = role.grantableBy;
25160
- return roleVocabularyErrors({
25709
+ return [...emailDomainRoleErrors(appRoles ?? []), ...roleVocabularyErrors({
25161
25710
  groupRoles: groups?.roles ?? [],
25162
25711
  adminGrantableGroupRoles: groups?.adminGrantableRoles ?? [],
25163
25712
  appRoles: (appRoles ?? []).map((role) => role.name),
25164
25713
  appRoleDelegation: delegation
25165
- });
25714
+ })];
25715
+ }
25716
+ function emailDomainRoleErrors(appRoles) {
25717
+ const errors = [];
25718
+ const byDomain = new Set(appRoles.filter((role) => role.emailDomain !== void 0).map((role) => role.name));
25719
+ for (const role of appRoles) {
25720
+ if (role.emailDomain !== void 0 && !EMAIL_DOMAIN.test(role.emailDomain)) errors.push(`appRoles.${role.name}.emailDomain is not one lowercase domain.`);
25721
+ for (const grantor of role.grantableBy) {
25722
+ if (byDomain.has(grantor)) {
25723
+ 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.`);
25724
+ }
25725
+ }
25726
+ }
25727
+ return errors;
25166
25728
  }
25167
25729
 
25168
25730
  // worker/src/utils/db-schema-deploy/extract-schema-options.ts
@@ -25249,6 +25811,7 @@ var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
25249
25811
  ...TABLE_MARKER_HELPERS,
25250
25812
  "schema",
25251
25813
  "table",
25814
+ "grant",
25252
25815
  "removed",
25253
25816
  "hasMany",
25254
25817
  "belongsTo",
@@ -25363,10 +25926,10 @@ function tokenize2(source) {
25363
25926
  var Reader = class {
25364
25927
  constructor(tokens, options = {}) {
25365
25928
  this.tokens = tokens;
25366
- this.bareIdIsText = !options.legacyIntegerIdDefault;
25929
+ this.bareIdIsText = (table) => !options.legacyIntegerIdDefault && !options.integerKeyTables?.has(table.toLowerCase());
25367
25930
  }
25368
25931
  pos = 0;
25369
- /** What a bare id() declares: a random text key, or (legacy retained source) an integer. */
25932
+ /** What a bare id() on this table declares: a random text key, or an integer (ExtractSchemaOptions). */
25370
25933
  bareIdIsText;
25371
25934
  peek() {
25372
25935
  return this.tokens[this.pos];
@@ -25534,7 +26097,7 @@ function readColumn(r, tableName, rawName, line) {
25534
26097
  const col = {
25535
26098
  name,
25536
26099
  helper,
25537
- uuid: helper === "id" && r.bareIdIsText,
26100
+ uuid: helper === "id" && r.bareIdIsText(tableName),
25538
26101
  nullable: false,
25539
26102
  hasDefault: false,
25540
26103
  default: null,
@@ -25771,7 +26334,7 @@ function readPolicyMember(r, tableName, line) {
25771
26334
  if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
25772
26335
  return scope;
25773
26336
  }
25774
- var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), group() or appRole().";
26337
+ var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), grant(), group() or appRole().";
25775
26338
  function readAnyOfBranch(r, tableName, which) {
25776
26339
  const tok = r.expectIdent(`for the ${which} policy in anyOf() on table "${tableName}"`);
25777
26340
  const failAt = (at2, message) => {
@@ -25779,6 +26342,7 @@ function readAnyOfBranch(r, tableName, which) {
25779
26342
  };
25780
26343
  if (tok.value === "owner") return readPolicyOwner(r, tableName, tok.line);
25781
26344
  if (tok.value === "member") return readPolicyMember(r, tableName, tok.line);
26345
+ if (tok.value === "grant") return readGrantScope(r, tableName, tok.line, failAt);
25782
26346
  if (tok.value === "group") return readGroupScope(r, tableName, tok.line, failAt);
25783
26347
  if (tok.value === "appRole") return readAppRoleScope(r, tableName, tok.line, failAt);
25784
26348
  throw new SchemaTsError(`line ${tok.line}: ${ANY_OF_FORMS}`);
@@ -25794,6 +26358,7 @@ function readAnyOfScope(r, tableName, line) {
25794
26358
  if (owners.length !== 1) throw new SchemaTsError(`line ${line}: ${ANY_OF_FORMS}`);
25795
26359
  const owner = owners[0];
25796
26360
  const other = firstPolicy.kind === "owner" ? secondPolicy : firstPolicy;
26361
+ if (other.kind === "member" && other.ownerGrant && owner.column !== DEFAULT_OWNER_COLUMN) throw new SchemaTsError(`line ${line}: grant() requires owner() with managed user_id.`);
25797
26362
  return { kind: "policy", policy: { kind: "any_of", policies: [owner, other] } };
25798
26363
  }
25799
26364
  var ALL_OF_FORMS = "allOf() takes a gate \u2014 member(), group() or appRole() \u2014 and then owner() or anyOf(owner(), ...).";
@@ -25821,6 +26386,7 @@ function readAllOfScope(r, tableName, line) {
25821
26386
  r.tryPunct(",");
25822
26387
  r.expectPunct(")", "closing allOf()");
25823
26388
  const x = who.kind === "any_of" ? who.policies[1] : null;
26389
+ if (x?.kind === "member" && x.ownerGrant) throw new SchemaTsError(`line ${line}: grant() belongs in anyOf(owner(), grant()), not allOf().`);
25824
26390
  for (const node of [gate, ...x ? [x] : []]) {
25825
26391
  if (node.kind !== "member") continue;
25826
26392
  if (!explicitMemberOperations.has(node)) {
@@ -26056,6 +26622,9 @@ function readTable(r, rawName, nameLine) {
26056
26622
  if (key.name === "scope") {
26057
26623
  if (scope) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "scope" twice.`);
26058
26624
  scope = readScope(r, rawName);
26625
+ resolveGrantRowKeys(scope, idCols[0]?.name, rawName, key.line, (at2, message) => {
26626
+ throw new SchemaTsError(`line ${at2}: ${message}`);
26627
+ });
26059
26628
  } else if (key.name === "indexes") {
26060
26629
  if (indexEntries) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "indexes" twice.`);
26061
26630
  indexEntries = readColumnLists(r, rawName, "indexes");
@@ -26287,55 +26856,7 @@ function extractSchemaTs(source, options = {}) {
26287
26856
  };
26288
26857
  resolveAppRoleScopes(tables, appRoles, errors);
26289
26858
  resolveGroupsAndParents(tables, tableByName, privatePolicy, groupRoles, errors);
26290
- const memberPolicies = (scope) => {
26291
- const out = [];
26292
- const visit = (node) => {
26293
- if (node.kind === "member") out.push(node);
26294
- else if (node.kind === "any_of") node.policies.forEach(visit);
26295
- else if (node.kind === "all_of") {
26296
- visit(node.gate);
26297
- visit(node.who);
26298
- }
26299
- };
26300
- if (scope.kind === "member") out.push(scope);
26301
- else if (scope.kind === "policy") visit(scope.policy);
26302
- return out;
26303
- };
26304
- for (const t of tables) {
26305
- for (const sc of memberPolicies(t.scope)) {
26306
- const own = knownColumns(t);
26307
- for (const g of sc.group) {
26308
- if (!own.has(g)) {
26309
- errors.push(
26310
- `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.`
26311
- );
26312
- }
26313
- }
26314
- const membershipTable = tableByName.get(sc.membership);
26315
- if (!membershipTable) {
26316
- errors.push(
26317
- `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.`
26318
- );
26319
- continue;
26320
- }
26321
- const membershipCols = knownColumns(membershipTable);
26322
- if (membershipTable.client && (membershipTable.client.create !== null || membershipTable.client.update !== null || membershipTable.client.delete || membershipTable.client.intake !== void 0)) {
26323
- errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
26324
- }
26325
- if (!membershipCols.has(sc.memberUser)) {
26326
- errors.push(
26327
- `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
26328
- );
26329
- }
26330
- for (const mg of sc.memberGroup) {
26331
- if (!membershipCols.has(mg)) {
26332
- errors.push(
26333
- `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
26334
- );
26335
- }
26336
- }
26337
- }
26338
- }
26859
+ validateDeclaredMemberships(tables, tableByName, knownColumns, errors);
26339
26860
  validateDeclaredRelations(tables, tableByName, knownColumns, (table) => removedTables.includes(table) ? "removedTable()" : exportedTables.includes(table) ? "exported()" : null, errors);
26340
26861
  validateRowLinks(tables, tableByName, errors);
26341
26862
  validateIntake(tables, tableByName, errors);
@@ -26343,7 +26864,7 @@ function extractSchemaTs(source, options = {}) {
26343
26864
  errors.push(...fileErrors);
26344
26865
  validateFileCollections(fileCollections, (name) => {
26345
26866
  const t = tableByName.get(name);
26346
- 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;
26867
+ return t ? { columns: knownColumns(t), clientWrites: tableAllowsClientWrites(t), parentTarget: fileParentTarget(t) } : null;
26347
26868
  }, errors);
26348
26869
  }
26349
26870
  if (groupRoles || appRoles) errors.push(...validateDeclaredRoles(groupRoles, appRoles));