@somewhere-cloud/cli 0.38.15 → 0.38.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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}`);
@@ -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,13 @@ 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
+ : guard[1] === 'MEMBERSHIP_REQUIRED'
1530
1551
  ? '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.');
1552
+ : '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
1553
  refused.code = guard[1];
1533
1554
  refused.status = 403;
1534
1555
  refused.__sw_expose = true;
@@ -2802,9 +2823,54 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2802
2823
  const media = /^\\/v1\\/(?:ai\\/(?:tts|generate-image|remove-background)|render\\/(?:screenshot|pdf)|web\\/scrape|browser\\/test)$/.test(path);
2803
2824
  const mutation = state && media && method === 'POST';
2804
2825
  if (mutation) { state.generation++; state.pending++; state.envelope = null; }
2805
- try { return await __sw_platformFetchInner(path, opts); }
2826
+ const signal = opts && opts.signal;
2827
+ const stateForDeadline = signal ? { dispatched: false } : null;
2828
+ const started = signal ? Date.now() : 0;
2829
+ const preparedOpts = signal ? Object.assign({}, opts, { __sw_deadlineState: stateForDeadline }) : opts;
2830
+ try { return await __sw_platformFetchInner(path, preparedOpts); }
2831
+ catch (err) {
2832
+ if (signal && !stateForDeadline.dispatched && err && err.code === 'PLATFORM_CALL_CANCELLED') {
2833
+ __sw_recordPlatformCall(path, started, undefined);
2834
+ }
2835
+ throw err;
2836
+ }
2806
2837
  finally { if (mutation) { state.pending--; state.envelope = null; } }
2807
2838
  }
2839
+ // Cancellation stops waiting; a request already sent may still complete.
2840
+ // Nothing is retried, and cancelled preparation never starts a request.
2841
+ function __sw_platformBound(signal, work, state) {
2842
+ if (!signal) return work();
2843
+ function cancelled() {
2844
+ const err = new Error('The platform call stopped waiting. Check its outcome before trying again.');
2845
+ err.code = 'PLATFORM_CALL_CANCELLED';
2846
+ err.status = 504;
2847
+ err.retry = false;
2848
+ err.outcome = state && state.dispatched ? 'unknown' : 'not_attempted';
2849
+ return err;
2850
+ }
2851
+ if (signal.aborted) return Promise.reject(cancelled());
2852
+ return new Promise(function (resolve, reject) {
2853
+ let settled = false;
2854
+ function finish(fn, value) {
2855
+ if (settled) return;
2856
+ settled = true;
2857
+ signal.removeEventListener('abort', abort);
2858
+ fn(value);
2859
+ }
2860
+ function abort() { finish(reject, cancelled()); }
2861
+ signal.addEventListener('abort', abort, { once: true });
2862
+ if (signal.aborted) { abort(); return; }
2863
+ try { Promise.resolve(work()).then(function (value) { finish(resolve, value); }, function (err) { finish(reject, err); }); }
2864
+ catch (err) { finish(reject, err); }
2865
+ });
2866
+ }
2867
+ async function __sw_platformJSONBody(response, signal) {
2868
+ try { return await __sw_platformBound(signal, function () { return response.json(); }, { dispatched: true }); }
2869
+ catch (err) {
2870
+ if (err && err.code === 'PLATFORM_CALL_CANCELLED') throw err;
2871
+ return null;
2872
+ }
2873
+ }
2808
2874
  async function __sw_platformFetchInner(path, opts) {
2809
2875
  opts = opts || {};
2810
2876
  if (path === '/v1/db/query' || path === '/v1/db/batch' || path === '/v1/db/raw-read') {
@@ -2816,7 +2882,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2816
2882
  const __sw_spanStart = Date.now();
2817
2883
  // Inside a job or cron delivery every platform call names its job, so the
2818
2884
  // platform refuses it once that job has ended (middleware/job-invocation-fence.ts).
2819
- const __sw_jobMarker = opts.__swFenceExempt ? null : await __sw_jobInvocationMarker();
2885
+ const __sw_jobMarker = opts.__swFenceExempt ? null : await __sw_platformBound(opts.signal, function () { return __sw_jobInvocationMarker(); }, opts.__sw_deadlineState);
2820
2886
  const headers = {
2821
2887
  ...(opts.headers || {}),
2822
2888
  ...(__sw_jobMarker || {}),
@@ -2860,12 +2926,15 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2860
2926
  __sw_fetchStart = Date.now();
2861
2927
  __sw_prepMs = __sw_fetchStart - __sw_spanStart;
2862
2928
  }
2863
- __sw_response = await __sw_runtimeFetch(platformBase + path, {
2929
+ __sw_response = await __sw_platformBound(opts.signal, function () {
2930
+ if (opts.__sw_deadlineState) opts.__sw_deadlineState.dispatched = true;
2931
+ return __sw_runtimeFetch(platformBase + path, {
2864
2932
  method: __sw_method,
2865
2933
  headers,
2866
2934
  body: opts.body,
2867
2935
  ...(opts.signal ? { signal: opts.signal } : {}),
2868
- });
2936
+ });
2937
+ }, opts.__sw_deadlineState);
2869
2938
  if (__sw_serverTiming) __sw_fetchMs = Date.now() - __sw_fetchStart;
2870
2939
  } catch (err) {
2871
2940
  if (__sw_serverTiming) __sw_fetchMs = Date.now() - __sw_fetchStart;
@@ -2931,8 +3000,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
2931
3000
 
2932
3001
  async function platformJSON(path, opts) {
2933
3002
  const r = await platformFetch(path, opts);
2934
- let data;
2935
- try { data = await r.json(); } catch { data = null; }
3003
+ const data = await __sw_platformJSONBody(r, opts && opts.signal);
2936
3004
  if (!r.ok || !data || data.ok === false) {
2937
3005
  throw __sw_platformError(r, data);
2938
3006
  }
@@ -3036,7 +3104,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
3036
3104
  });
3037
3105
  if (ingest) {
3038
3106
  let result = null;
3039
- try { result = await response.json(); } catch (_) {}
3107
+ result = await __sw_platformJSONBody(response, controller.signal);
3040
3108
  const queued = result && typeof result.queued === 'number' ? result.queued : 0;
3041
3109
  const dropped = result && typeof result.dropped === 'number' ? result.dropped : null;
3042
3110
  if (response.status === 202 && dropped === 0) return;
@@ -3048,8 +3116,8 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
3048
3116
  throw failure;
3049
3117
  }
3050
3118
  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) {
3119
+ const result = await __sw_platformJSONBody(response, controller.signal);
3120
+ if (!result || !result.data || result.data.failed || result.data.unknown) {
3053
3121
  throw new Error('Console log delivery could not be confirmed.');
3054
3122
  }
3055
3123
  } finally { clearTimeout(timeout); }
@@ -3061,9 +3129,14 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
3061
3129
  // itself lives in the sw.rateLimit fragment.
3062
3130
  // The platform's counter: one increment, answered with the count it reached.
3063
3131
  async function __sw_rateLimitAsk(key, max, windowSeconds) {
3132
+ // Rate-limit checks stop waiting after five seconds.
3133
+ // An unconfirmed increment is never retried.
3134
+ const controller = new AbortController();
3135
+ const timer = setTimeout(function () { controller.abort(); }, 5000);
3064
3136
  try {
3065
3137
  return await platformJSON('/v1/rate-limit/check', {
3066
3138
  method: 'POST',
3139
+ signal: controller.signal,
3067
3140
  body: JSON.stringify({
3068
3141
  project_id: projectId,
3069
3142
  key,
@@ -3086,7 +3159,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
3086
3159
  };
3087
3160
  }
3088
3161
  throw e;
3089
- }
3162
+ } finally { clearTimeout(timer); }
3090
3163
  }
3091
3164
 
3092
3165
  // Remember what the platform answered for this key's window. A refusal
@@ -4694,6 +4767,12 @@ var DB_STRUCTURED_EXECUTION_JS = ` async function __sw_structExec(label, ir
4694
4767
  // Zero rows back means the EXISTS was false: the verified user does not
4695
4768
  // belong to the group this row targets. Surface it as an explicit 403
4696
4769
  // BEFORE any realtime publish \u2014 the row never landed.
4770
+ // A per-row share names a row its creator does not own (query-sql.ts grantRow guard).
4771
+ if (ir.kind === 'insert' && (ir.scope.mode === 'user' || ir.scope.mode === 'server') && ir.scope.grantRow && ir.onConflict === undefined && rows.length === 0) {
4772
+ __sw_structThrow403('POLICY_AUTHORITY_REQUIRED', ir.scope.mode === 'server'
4773
+ ? 'sw.db.server.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row its user_id owns.'
4774
+ : 'sw.db.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row the signed-in user owns.');
4775
+ }
4697
4776
  if (ir.kind === 'insert' && (ir.scope.mode === 'member' || ir.scope.mode === 'policy') && rows.length === 0) {
4698
4777
  __sw_structThrow403(ir.scope.mode === 'policy' ? 'POLICY_AUTHORITY_REQUIRED' : 'MEMBERSHIP_REQUIRED',
4699
4778
  ir.scope.mode === 'policy' ? 'sw.db.insert on "' + ir.table.name + '": the proposed row is outside your declared private policy.'
@@ -5118,6 +5197,7 @@ function renderPolicySql(request) {
5118
5197
  return renderPolicySql({
5119
5198
  render: "predicate",
5120
5199
  principal: r.principal,
5200
+ emailDomain: r.emailDomain,
5121
5201
  table: r.table,
5122
5202
  physical: r.physical,
5123
5203
  policy: { k: "m", m: r.member },
@@ -5128,10 +5208,11 @@ function renderPolicySql(request) {
5128
5208
  }
5129
5209
  if (r.render === "insert") {
5130
5210
  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 });
5211
+ 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
5212
  const gateInsert = renderPolicySql({
5133
5213
  render: "predicate",
5134
5214
  principal: r.principal,
5215
+ emailDomain: r.emailDomain,
5135
5216
  table: r.table,
5136
5217
  physical: r.physical,
5137
5218
  policy: r.policy.g,
@@ -5139,7 +5220,7 @@ function renderPolicySql(request) {
5139
5220
  qualifier: null,
5140
5221
  proposed: r.proposed
5141
5222
  });
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 });
5223
+ 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
5224
  const insertParams = [];
5144
5225
  for (let i = 0; i < gateInsert.params.length; i++) insertParams.push(gateInsert.params[i]);
5145
5226
  for (let i = 0; i < whoInsert.params.length; i++) insertParams.push(whoInsert.params[i]);
@@ -5148,13 +5229,17 @@ function renderPolicySql(request) {
5148
5229
  const anyOf = r.policy && r.policy.k === "a";
5149
5230
  const branch = anyOf && Array.isArray(r.policy.p) ? r.policy.p[1] : null;
5150
5231
  if (anyOf && (!branch || branch.k !== "m" && branch.k !== "g" && branch.k !== "r")) throw new Error("policy-sql: invalid anyOf policy");
5232
+ if (branch?.k === "m" && branch.m?.og) {
5233
+ renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: r.policy, operation: "read", qualifier: null, proposed: null });
5234
+ return { sql: "1", params: [] };
5235
+ }
5151
5236
  if (branch && branch.k === "r") {
5152
5237
  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 });
5238
+ renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
5154
5239
  return { sql: "1", params: [] };
5155
5240
  }
5156
5241
  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 });
5242
+ renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
5158
5243
  let found = 0;
5159
5244
  let groupValue = null;
5160
5245
  for (let j = 0; j < r.proposed.length; j++) {
@@ -5176,6 +5261,7 @@ function renderPolicySql(request) {
5176
5261
  return renderPolicySql({
5177
5262
  render: "predicate",
5178
5263
  principal: r.principal,
5264
+ emailDomain: r.emailDomain,
5179
5265
  table: r.table,
5180
5266
  physical: r.physical,
5181
5267
  policy: anyOf ? r.policy.p[1] : r.policy,
@@ -5210,6 +5296,7 @@ function renderPolicySql(request) {
5210
5296
  return renderPolicySql({
5211
5297
  render: "predicate",
5212
5298
  principal: r.principal,
5299
+ emailDomain: r.emailDomain,
5213
5300
  table: r.table,
5214
5301
  physical: r.physical,
5215
5302
  policy: r.policy,
@@ -5228,6 +5315,7 @@ function renderPolicySql(request) {
5228
5315
  return renderPolicySql({
5229
5316
  render: "predicate",
5230
5317
  principal: r.principal,
5318
+ emailDomain: r.emailDomain,
5231
5319
  table: r.table,
5232
5320
  physical: r.physical,
5233
5321
  policy: r.policy,
@@ -5236,6 +5324,34 @@ function renderPolicySql(request) {
5236
5324
  proposed: r.proposed
5237
5325
  });
5238
5326
  }
5327
+ if (r.render === "membership") {
5328
+ const team = r.policy;
5329
+ if (!team || team.k !== "m" && team.k !== "g") throw new Error("policy-sql: membership is a member or group policy");
5330
+ if (r.principalRef !== void 0 && r.principalRef !== "link_maker") throw new Error("policy-sql: invalid principal reference");
5331
+ const byMaker = r.principalRef === "link_maker";
5332
+ const principal = byMaker ? "link_maker" : r.principal;
5333
+ const who = byMaker ? renderPolicySql({ render: "ident", name: "__sw_lg" }).sql + "." + renderPolicySql({ render: "ident", name: "created_by" }).sql : "?";
5334
+ const rowRef = r.qualifier ? renderPolicySql({ render: "ident", name: r.qualifier }).sql : null;
5335
+ let member;
5336
+ const keys = [];
5337
+ if (team.k === "m") {
5338
+ 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 });
5339
+ const groupsOfWho = [];
5340
+ for (let i = 0; i < team.m.mg.length; i++) groupsOfWho.push(renderPolicySql({ render: "ident", name: team.m.mg[i] }).sql);
5341
+ for (let i = 0; i < team.m.g.length; i++) keys.push((rowRef ? rowRef + "." : "") + renderPolicySql({ render: "ident", name: team.m.g[i] }).sql);
5342
+ 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 + ")";
5343
+ } else {
5344
+ 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 });
5345
+ const members = renderPolicySql({ render: "ident", name: "__sw_gm" }).sql;
5346
+ const groups = renderPolicySql({ render: "ident", name: "__sw_g" }).sql;
5347
+ const rowGroup = (rowRef ? rowRef : renderPolicySql({ render: "table", name: r.table, physical: r.physical }).sql) + "." + renderPolicySql({ render: "ident", name: team.c }).sql;
5348
+ keys.push(rowGroup);
5349
+ 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 + ")";
5350
+ }
5351
+ let sql = "(";
5352
+ for (let i = 0; i < keys.length; i++) sql += keys[i] + " IS NULL OR ";
5353
+ return { sql: sql + member + ")", params: byMaker ? [] : [r.principal] };
5354
+ }
5239
5355
  if (r.render !== "predicate") throw new Error("policy-sql: unknown render kind");
5240
5356
  const policy = r.policy;
5241
5357
  const qualifier = r.qualifier;
@@ -5254,6 +5370,11 @@ function renderPolicySql(request) {
5254
5370
  if (!m || !Array.isArray(m.g) || !Array.isArray(m.mg) || m.g.length === 0 || m.g.length !== m.mg.length) {
5255
5371
  throw new Error("policy-sql: invalid member scope");
5256
5372
  }
5373
+ if (m.og !== void 0) {
5374
+ const perRow = m.g.length === 2 && m.g[1] !== "user_id" && m.mg[1] !== "user_id" && m.mg[1] !== m.u;
5375
+ 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");
5376
+ if (proposed || !operation) return { sql: "0", params: [] };
5377
+ }
5257
5378
  if (operation && m.o !== void 0) {
5258
5379
  let allowed = false;
5259
5380
  for (let i = 0; i < m.o.length; i++) if (m.o[i] === operation) allowed = true;
@@ -5303,8 +5424,16 @@ function renderPolicySql(request) {
5303
5424
  if ((policy.p[1].k === "g" || policy.p[1].k === "r") && (proposed || !operation)) {
5304
5425
  throw new Error("policy-sql: an anyOf group or app-role branch needs an operation on an existing row");
5305
5426
  }
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 });
5427
+ 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 });
5428
+ 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");
5429
+ if (policy.p[1].k === "m" && !policy.p[1].m.og || policy.p[1].k === "g") {
5430
+ 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 });
5431
+ const gated = [];
5432
+ for (let i = 0; i < left.params.length; i++) gated.push(left.params[i]);
5433
+ for (let i = 0; i < team.params.length; i++) gated.push(team.params[i]);
5434
+ left = { sql: "(" + left.sql + " AND " + team.sql + ")", params: gated };
5435
+ }
5436
+ 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
5437
  for (let i = 0; i < left.params.length; i++) params.push(left.params[i]);
5309
5438
  for (let i = 0; i < right.params.length; i++) params.push(right.params[i]);
5310
5439
  return { sql: "(" + left.sql + " OR " + right.sql + ")", params };
@@ -5315,7 +5444,7 @@ function renderPolicySql(request) {
5315
5444
  if (!gate || gate.k !== "m" && gate.k !== "g" && gate.k !== "r") {
5316
5445
  throw new Error("policy-sql: an allOf gate is a member, group or app-role policy");
5317
5446
  }
5318
- if (gate.k === "m" && (!gate.m || gate.m.o === void 0)) throw new Error("policy-sql: allOf needs explicit operations");
5447
+ if (gate.k === "m" && (!gate.m || gate.m.o === void 0 || gate.m.og)) throw new Error("policy-sql: allOf needs explicit operations");
5319
5448
  if (!who || who.k !== "o" && who.k !== "a") throw new Error("policy-sql: allOf who is owner() or anyOf(owner(), ...)");
5320
5449
  if (who.k === "a") {
5321
5450
  const x = Array.isArray(who.p) && who.p.length === 2 ? who.p[1] : null;
@@ -5325,7 +5454,7 @@ function renderPolicySql(request) {
5325
5454
  compatible = String(x.c).toLowerCase() === String(gate.c).toLowerCase() && x.generation === gate.generation;
5326
5455
  }
5327
5456
  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");
5457
+ if (!x.m || x.m.o === void 0 || x.m.og) throw new Error("policy-sql: allOf needs explicit operations");
5329
5458
  const a = gate.m;
5330
5459
  const b = x.m;
5331
5460
  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 +5469,14 @@ function renderPolicySql(request) {
5340
5469
  if (operation !== "read" && operation !== "create" && operation !== "update" && operation !== "delete") {
5341
5470
  throw new Error("policy-sql: an allOf policy needs an operation");
5342
5471
  }
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 });
5472
+ 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 });
5473
+ 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
5474
  for (let i = 0; i < gateSql.params.length; i++) params.push(gateSql.params[i]);
5346
5475
  for (let i = 0; i < whoSql.params.length; i++) params.push(whoSql.params[i]);
5347
5476
  let sql = "(" + gateSql.sql + " AND " + whoSql.sql + ")";
5348
5477
  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 });
5478
+ 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 });
5479
+ 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
5480
  for (let i = 0; i < gateRead.params.length; i++) params.push(gateRead.params[i]);
5352
5481
  for (let i = 0; i < whoRead.params.length; i++) params.push(whoRead.params[i]);
5353
5482
  sql = "(" + sql + " AND (" + gateRead.sql + " AND " + whoRead.sql + "))";
@@ -5384,7 +5513,7 @@ function renderPolicySql(request) {
5384
5513
  } else {
5385
5514
  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
5515
  }
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 });
5516
+ 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
5517
  for (let i = 0; i < inner.params.length; i++) params.push(inner.params[i]);
5389
5518
  return {
5390
5519
  sql: "EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "table", name: policy.t, physical: r.physical }).sql + " AS " + aliasSql + " WHERE " + join + " AND " + inner.sql + ")",
@@ -5484,8 +5613,35 @@ function renderPolicySql(request) {
5484
5613
  marks.push("?");
5485
5614
  params.push(allowed[i]);
5486
5615
  }
5616
+ 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(", ") + "))";
5617
+ const domain = r.emailDomain;
5618
+ if (domain !== null && domain !== void 0 && (typeof domain !== "string" || domain.length === 0 || domain.length > 253)) {
5619
+ throw new Error("policy-sql: invalid email domain");
5620
+ }
5621
+ const baked = policy.d;
5622
+ if (baked !== void 0) {
5623
+ if (!baked || typeof baked !== "object" || Array.isArray(baked)) throw new Error("policy-sql: invalid app-role domains");
5624
+ if (domain !== null && domain !== void 0) {
5625
+ for (let i = 0; i < allowed.length; i++) {
5626
+ if (Object.prototype.hasOwnProperty.call(baked, allowed[i]) && baked[allowed[i]] === domain) {
5627
+ held = "(" + held + " OR 1)";
5628
+ break;
5629
+ }
5630
+ }
5631
+ }
5632
+ } else if (domain !== null && domain !== void 0) {
5633
+ const vocabulary = renderPolicySql({ render: "ident", name: "__sw_rv" }).sql;
5634
+ const entry = renderPolicySql({ render: "ident", name: "__sw_re" }).sql;
5635
+ const domainMarks = [];
5636
+ for (let i = 0; i < allowed.length; i++) {
5637
+ domainMarks.push("?");
5638
+ params.push(allowed[i]);
5639
+ }
5640
+ params.push(domain);
5641
+ 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') = ?))`;
5642
+ }
5487
5643
  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(", ") + ")))",
5644
+ 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
5645
  params
5490
5646
  };
5491
5647
  }
@@ -5497,7 +5653,19 @@ function policyOwnerColumn(policy) {
5497
5653
  if (policy.k === "n") return policyOwnerColumn(policy.w);
5498
5654
  return null;
5499
5655
  }
5500
- var POLICY_SQL_JS = " const __sw_renderPolicySql = (" + renderPolicySql.toString() + ");\n const __sw_policyOwnerColumn = (" + policyOwnerColumn.toString() + ");\n";
5656
+ function policyWithoutGrants(policy) {
5657
+ if (policy.k === "a") {
5658
+ const second = policy.p[1];
5659
+ if (second.k === "m" && second.m.og) return policy.p[0];
5660
+ return policy;
5661
+ }
5662
+ if (policy.k === "p") {
5663
+ const inner = policyWithoutGrants(policy.p);
5664
+ return inner === policy.p ? policy : { ...policy, p: inner };
5665
+ }
5666
+ return policy;
5667
+ }
5668
+ var POLICY_SQL_JS = " const __sw_renderPolicySql = (" + renderPolicySql.toString() + ");\n const __sw_policyOwnerColumn = (" + policyOwnerColumn.toString() + ");\n const __sw_policyWithoutGrants = (" + policyWithoutGrants.toString() + ");\n";
5501
5669
 
5502
5670
  // worker/src/runtime/query-sql.ts
5503
5671
  function renderQuerySql(ir, principalId, returning, context) {
@@ -5514,6 +5682,26 @@ function renderQuerySql(ir, principalId, returning, context) {
5514
5682
  if (ir.kind === "insert" || ir.kind === "update") {
5515
5683
  context.assertUniqueWriteColumns((ir.values || []).concat(ir.increments || []), "sw.db." + ir.kind);
5516
5684
  }
5685
+ if (ir.kind === "update" && ir.scope) {
5686
+ const fixed = [];
5687
+ if ((ir.scope.mode === "user" || ir.scope.mode === "server") && ir.scope.grantRow) fixed.push(ir.scope.grantRow.c);
5688
+ if (ir.scope.mode === "server" && ir.scope.grantRow) fixed.push("user_id");
5689
+ const grantBranch = ir.scope.mode === "policy" && ir.scope.policy.k === "a" ? ir.scope.policy.p[1] : null;
5690
+ if (grantBranch && grantBranch.k === "m" && grantBranch.m.og && grantBranch.m.g.length === 2) fixed.push(grantBranch.m.g[1]);
5691
+ const written = (ir.values || []).concat(ir.increments || []);
5692
+ for (let i = 0; i < written.length; i++) {
5693
+ for (let j = 0; j < fixed.length; j++) {
5694
+ if (context.lower(written[i].column) !== context.lower(fixed[j])) continue;
5695
+ let pinned = false;
5696
+ const value = i < (ir.values || []).length ? written[i].value : void 0;
5697
+ for (let k = 0; k < ir.where.length; k++) {
5698
+ const w = ir.where[k];
5699
+ if (value !== void 0 && w.op === "eq" && typeof w.column === "string" && context.lower(w.column) === context.lower(fixed[j]) && w.value === value) pinned = true;
5700
+ }
5701
+ 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.');
5702
+ }
5703
+ }
5704
+ }
5517
5705
  const params = [];
5518
5706
  const helpers = {
5519
5707
  table(name) {
@@ -5639,7 +5827,26 @@ function renderQuerySql(ir, principalId, returning, context) {
5639
5827
  else context.fail("sw.db: a linked row key must be a whole number or text.");
5640
5828
  } else key = "CAST(" + rowRef + " AS TEXT)";
5641
5829
  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") + ")";
5830
+ let team = "";
5831
+ if (link.team) {
5832
+ if (!link.team.key || typeof link.team.key !== "string" || !link.team.policy) context.fail("sw.db: invalid link authority.");
5833
+ const lr = q("__sw_lr");
5834
+ let rootKey = "?";
5835
+ if (rowRef === null) params.push(rowValue);
5836
+ else rootKey = rowRef;
5837
+ const gate = context.renderPolicy({
5838
+ render: "membership",
5839
+ principal: null,
5840
+ principalRef: "link_maker",
5841
+ table: link.root,
5842
+ physical: context.physicalMap(),
5843
+ policy: link.team.policy,
5844
+ qualifier: "__sw_lr"
5845
+ });
5846
+ for (let i = 0; i < gate.params.length; i++) params.push(gate.params[i]);
5847
+ 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 + ")";
5848
+ }
5849
+ 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
5850
  },
5644
5851
  // Creator session (creator-authority.ts): ONE correlated EXISTS proving, in the statement, that this
5645
5852
  // creator session token is live for this exact unended creator grant, which holds THIS root row's
@@ -5708,8 +5915,21 @@ function renderQuerySql(ir, principalId, returning, context) {
5708
5915
  frags.push(privateBranch);
5709
5916
  }
5710
5917
  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)
5918
+ const policy = ir.scope.policy;
5919
+ const destination = context.renderPolicy({ render: "update_destination", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy, proposed: ir.values || [] });
5920
+ const ownerOnly = destination.sql && policy.k === "p" ? context.withoutGrants(policy) : policy;
5921
+ if (destination.sql && ownerOnly !== policy) {
5922
+ const access = [];
5923
+ for (let i = 0; i < ir.where.length; i++)
5924
+ access.push(helpers.renderPredicate(ir.where[i]));
5925
+ access.push(helpers.privateScopePredicate(ir.scope) || "1");
5926
+ access.push(helpers.policySql(destination));
5927
+ const owned = [
5928
+ helpers.policyPredicate(ownerOnly, null, null, "update"),
5929
+ helpers.policySql(context.renderPolicy({ render: "update_destination", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy: ownerOnly, proposed: ir.values || [] }))
5930
+ ];
5931
+ frags.push("(CASE WHEN NOT (" + access.join(" AND ") + ") THEN 0 WHEN (" + owned.join(" AND ") + ") THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END)");
5932
+ } else if (destination.sql)
5713
5933
  frags.push(helpers.policySql(destination));
5714
5934
  }
5715
5935
  for (let i = 0; i < (ir.increments || []).length; i++) {
@@ -5908,6 +6128,35 @@ function renderQuerySql(ir, principalId, returning, context) {
5908
6128
  sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + context.returningSql(returning);
5909
6129
  return { sql, params };
5910
6130
  }
6131
+ if ((ir.scope.mode === "user" || ir.scope.mode === "server") && ir.scope.grantRow) {
6132
+ const grantRow = ir.scope.grantRow;
6133
+ let named = 0;
6134
+ let rowValue = null;
6135
+ let owners = 0;
6136
+ let shareOwner = ir.scope.mode === "user" ? principalId : null;
6137
+ for (let i = 0; i < ir.values.length; i++) {
6138
+ if (context.lower(ir.values[i].column) === context.lower(grantRow.c)) {
6139
+ named++;
6140
+ rowValue = ir.values[i].value;
6141
+ }
6142
+ if (ir.scope.mode === "server" && context.lower(ir.values[i].column) === "user_id") {
6143
+ owners++;
6144
+ shareOwner = ir.values[i].value;
6145
+ }
6146
+ }
6147
+ if (named !== 1 || rowValue === null) context.fail('sw.db.insert: a share names the "' + grantRow.t + '" row it grants in "' + grantRow.c + '".');
6148
+ 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.');
6149
+ if (ir.onConflict === "update") context.fail('sw.db.insert: a share cannot be updated on conflict; use onConflict: "ignore".');
6150
+ for (let i = 0; i < ivals.length; i++)
6151
+ params.push(ivals[i]);
6152
+ const owned = "EXISTS (SELECT 1 FROM " + helpers.table(grantRow.t) + " WHERE " + q(grantRow.k) + " = ? AND " + q("user_id") + " = ?)";
6153
+ params.push(rowValue, shareOwner);
6154
+ const guard = ir.txGuard || ir.onConflict !== void 0 ? "CASE WHEN " + owned + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END" : owned;
6155
+ sql = "INSERT INTO " + tableSql + " (" + allCols.map(q).join(", ") + ") SELECT " + allCols.map(function() {
6156
+ return "?";
6157
+ }).join(", ") + " WHERE " + guard + (ir.onConflict === "ignore" ? " ON CONFLICT DO NOTHING" : "") + context.returningSql(returning);
6158
+ return { sql, params };
6159
+ }
5911
6160
  for (let i = 0; i < ivals.length; i++)
5912
6161
  params.push(ivals[i]);
5913
6162
  const visitorGuard = ir.scope.mode === "user" ? helpers.visitorPredicate() : null;
@@ -6035,6 +6284,7 @@ var DB_RENDER_IR_JS = `${POLICY_SQL_JS}${QUERY_SQL_JS} // One write names e
6035
6284
  },
6036
6285
  renderPolicy: __sw_renderPolicySql,
6037
6286
  ownerColumn: __sw_policyOwnerColumn,
6287
+ withoutGrants: __sw_policyWithoutGrants,
6038
6288
  returningSql: __sw_returningSql,
6039
6289
  assertUniqueWriteColumns: __sw_structAssertUniqueWriteColumns,
6040
6290
  fail: __sw_structThrow,
@@ -6631,13 +6881,32 @@ var DB_RELATIONS_JS = ` // \u2500\u2500 Composed joins (tsk_1186c9a) \u2500
6631
6881
  }
6632
6882
  `;
6633
6883
 
6884
+ // worker/src/utils/authority/role-domain-map.ts
6885
+ var ROLE_NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
6886
+ 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]$/;
6887
+ var ROLE_DOMAIN_MAP_JS = `
6888
+ function __sw_structRoleDomains(value) {
6889
+ if (!value || typeof value !== 'object' || __sw_arrayIsArray(value)) return false;
6890
+ const roles = __sw_keys(value);
6891
+ for (let i = 0; i < roles.length; i++) {
6892
+ const domain = value[roles[i]];
6893
+ if (!__sw_reMatch(${ROLE_NAME_PATTERN}, roles[i]) || typeof domain !== 'string' || !__sw_reMatch(${EMAIL_DOMAIN_PATTERN}, domain)) return false;
6894
+ }
6895
+ return true;
6896
+ }`;
6897
+
6634
6898
  // worker/src/runtime/db-policy.ts
6635
- var DB_POLICY_HELPERS_JS = `
6899
+ var DB_POLICY_HELPERS_JS = `${ROLE_DOMAIN_MAP_JS}
6636
6900
  function __sw_structPolicyIdent(value) { return typeof value === 'string' && value && __sw_reMatch(__SW_STRUCT_IDENT_RE, value); }
6637
6901
  function __sw_structPolicyMember(value) {
6638
6902
  if (!value || typeof value !== 'object') return false;
6639
6903
  const keys = __sw_keys(value).slice().sort().join(',');
6640
- if (keys !== 'g,m,mg,u' && keys !== 'g,m,mg,o,u') return false;
6904
+ // Per-owner grant: g/mg ["user_id"]. Per-row: g ["user_id", key], mg ["user_id", row].
6905
+ const grant = keys === 'g,m,mg,o,og,u' && value.og === true && __sw_arrayIsArray(value.g) && __sw_arrayIsArray(value.mg)
6906
+ && value.g[0] === 'user_id' && value.mg[0] === 'user_id'
6907
+ && (value.g.length === 1 || value.g.length === 2 && value.g[1] !== 'user_id' && value.mg[1] !== 'user_id' && value.mg[1] !== value.u)
6908
+ && value.u !== 'user_id' && (JSON.stringify(value.o) === '["read"]' || JSON.stringify(value.o) === '["read","update"]');
6909
+ if (keys !== 'g,m,mg,u' && keys !== 'g,m,mg,o,u' && !grant) return false;
6641
6910
  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
6911
  for (let i = 0; i < value.g.length; i++) if (!__sw_structPolicyIdent(value.g[i]) || !__sw_structPolicyIdent(value.mg[i])) return false;
6643
6912
  if (value.o !== undefined) {
@@ -6672,9 +6941,12 @@ var DB_POLICY_HELPERS_JS = `
6672
6941
  return true;
6673
6942
  }
6674
6943
  // An app-role leaf, pinned to the release's policy generation. Each
6675
- // present operation lists declared app roles; a missing one denies.
6944
+ // present operation lists declared app roles; a missing one denies. The
6945
+ // release's role -> verified email domain map (d) rides it when baked.
6676
6946
  function __sw_structPolicyRole(value) {
6677
- if (!value || typeof value !== 'object' || value.k !== 'r' || __sw_keys(value).slice().sort().join(',') !== 'generation,k,roles') return false;
6947
+ if (!value || typeof value !== 'object' || value.k !== 'r') return false;
6948
+ const leafKeys = __sw_keys(value).slice().sort().join(',');
6949
+ if (leafKeys !== 'generation,k,roles' && (leafKeys !== 'd,generation,k,roles' || !__sw_structRoleDomains(value.d))) return false;
6678
6950
  if (typeof value.generation !== 'number' || !Number.isSafeInteger(value.generation) || value.generation < 1) return false;
6679
6951
  const roles = value.roles;
6680
6952
  if (!roles || typeof roles !== 'object' || __sw_arrayIsArray(roles)) return false;
@@ -6694,6 +6966,7 @@ var DB_POLICY_HELPERS_JS = `
6694
6966
  if (!value || typeof value !== 'object' || __sw_keys(value).slice().sort().join(',') !== 'k,p' || value.k !== 'a'
6695
6967
  || !__sw_arrayIsArray(value.p) || value.p.length !== 2 || !__sw_structPolicyLeaf(value.p[0]) || value.p[0].k !== 'o') return false;
6696
6968
  const second = value.p[1];
6969
+ if (second && second.k === 'm' && second.m && second.m.og && value.p[0].c !== 'user_id') return false;
6697
6970
  return (__sw_structPolicyLeaf(second) && second.k === 'm') || __sw_structPolicyGroup(second) || __sw_structPolicyRole(second);
6698
6971
  }
6699
6972
  // allOf(gate, who) (AND permissions v1.1): gate is a member() leaf with
@@ -6705,7 +6978,7 @@ var DB_POLICY_HELPERS_JS = `
6705
6978
  if (!value || typeof value !== 'object' || __sw_keys(value).slice().sort().join(',') !== 'g,k,w' || value.k !== 'n') return false;
6706
6979
  const gate = value.g;
6707
6980
  const who = value.w;
6708
- const gateOk = (__sw_structPolicyLeaf(gate) && gate.k === 'm' && gate.m.o !== undefined) || __sw_structPolicyGroup(gate) || __sw_structPolicyRole(gate);
6981
+ const gateOk = (__sw_structPolicyLeaf(gate) && gate.k === 'm' && gate.m.o !== undefined && !gate.m.og) || __sw_structPolicyGroup(gate) || __sw_structPolicyRole(gate);
6709
6982
  if (!gateOk) return false;
6710
6983
  if (__sw_structPolicyLeaf(who) && who.k === 'o') return true;
6711
6984
  if (!__sw_structPolicyAny(who)) return false;
@@ -6713,6 +6986,7 @@ var DB_POLICY_HELPERS_JS = `
6713
6986
  if (x.k === 'r') return true;
6714
6987
  if (x.k === 'g') return gate.k === 'g' && __sw_lower(x.c) === __sw_lower(gate.c) && x.generation === gate.generation;
6715
6988
  if (x.k !== 'm' || gate.k !== 'm' || x.m.o === undefined) return false;
6989
+ if (x.m.og || gate.m.og) return false;
6716
6990
  if (__sw_lower(x.m.m) !== __sw_lower(gate.m.m) || __sw_lower(x.m.u) !== __sw_lower(gate.m.u)
6717
6991
  || x.m.g.length !== gate.m.g.length || x.m.mg.length !== gate.m.mg.length) return false;
6718
6992
  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 +7007,23 @@ var DB_POLICY_HELPERS_JS = `
6733
7007
  if (__sw_structResolveIntent(table) !== 'policy' || !shape || !__sw_own(shape, 'policy') || !__sw_structPolicyValid(shape.policy)) return null;
6734
7008
  return shape.policy;
6735
7009
  }
7010
+ // A per-row grant table (grant({ row })) and the one rule it serves: its shares name rows of t (key k) in c.
7011
+ let __sw_structGrantRows;
7012
+ function __sw_structGrantRowFor(table) {
7013
+ if (!__sw_structGrantRows) {
7014
+ __sw_structGrantRows = {};
7015
+ const names = __sw_keys(SCHEMA);
7016
+ for (let i = 0; i < names.length; i++) {
7017
+ const policy = __sw_structResolvePolicy(names[i]);
7018
+ const branch = policy && policy.k === 'a' ? policy.p[1] : null;
7019
+ if (branch && branch.k === 'm' && branch.m.og === true && branch.m.g.length === 2) {
7020
+ __sw_structGrantRows[__sw_lower(branch.m.m)] = { t: __sw_lower(names[i]), k: branch.m.g[1], c: branch.m.mg[1] };
7021
+ }
7022
+ }
7023
+ }
7024
+ const lc = __sw_lower(table);
7025
+ return __sw_own(__sw_structGrantRows, lc) ? __sw_structGrantRows[lc] : undefined;
7026
+ }
6736
7027
  function __sw_structPolicyHasOwner(policy) {
6737
7028
  return policy.k === 'o' || policy.k === 'a' && (__sw_structPolicyHasOwner(policy.p[0]) || __sw_structPolicyHasOwner(policy.p[1]))
6738
7029
  || policy.k === 'n' && __sw_structPolicyHasOwner(policy.w);
@@ -6776,6 +7067,9 @@ var DB_POLICY_HELPERS_JS = `
6776
7067
  }
6777
7068
  return;
6778
7069
  }
7070
+ // A grant() branch correlates the managed owner (checked above) and, per row, the row's key,
7071
+ // which the shared renderer keeps fixed (query-sql.ts), as the managed compiler does.
7072
+ if (policy.p[1].k === 'm' && policy.p[1].m.og) return;
6779
7073
  __sw_structAssertPolicyWrite(cols, policy.p[1], verb, increments);
6780
7074
  return;
6781
7075
  }
@@ -6822,7 +7116,7 @@ var DB_POLICY_HELPERS_JS = `
6822
7116
  if (policy.k === 'a') {
6823
7117
  // The owner is stamped; an app-role branch has no placement, and a
6824
7118
  // group branch must name the group the row belongs to.
6825
- if (policy.p[1].k === 'r') return;
7119
+ if (policy.p[1].k === 'r' || (policy.p[1].k === 'm' && policy.p[1].m.og)) return;
6826
7120
  if (policy.p[1].k === 'g') {
6827
7121
  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
7122
  __sw_structThrow403('POLICY_GROUP_REQUIRED', 'A group-scoped insert must name the group the row belongs to in "' + policy.p[1].c + '".');
@@ -8625,7 +8919,8 @@ ${DB_POLICY_RESOLVE_SCOPE_JS} const member = __sw_structResolveMember(tab
8625
8919
  if (asServer === true || generatedRead) {
8626
8920
  // Explicit server code or the private generated selector has no local
8627
8921
  // principal. The managed API independently verifies the latter's token.
8628
- return { scope: generatedRead ? { mode: 'user', ownerColumn: ownerColumn } : { mode: 'server' }, principalId: undefined };
8922
+ const serverGrantRow = generatedRead ? undefined : __sw_structGrantRowFor(table);
8923
+ return { scope: generatedRead ? { mode: 'user', ownerColumn: ownerColumn } : serverGrantRow ? { mode: 'server', grantRow: serverGrantRow } : { mode: 'server' }, principalId: undefined };
8629
8924
  }
8630
8925
  const identity = await __sw_structRequestIdentity(allowPublicGuest);
8631
8926
  let id = identity && identity.id;
@@ -8660,7 +8955,8 @@ ${DB_POLICY_RESOLVE_SCOPE_JS} const member = __sw_structResolveMember(tab
8660
8955
  err.__sw_expose = true;
8661
8956
  throw err;
8662
8957
  }
8663
- return { scope: { mode: 'user', ownerColumn: ownerColumn }, principalId: id };
8958
+ const grantRow = __sw_structGrantRowFor(table);
8959
+ return { scope: grantRow ? { mode: 'user', ownerColumn: ownerColumn, grantRow: grantRow } : { mode: 'user', ownerColumn: ownerColumn }, principalId: id };
8664
8960
  }
8665
8961
 
8666
8962
  // The scope argument is GONE (tsk_5b91e0e4): scoping is automatic and
@@ -9601,14 +9897,17 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
9601
9897
  });
9602
9898
  return send(prepared);
9603
9899
  }
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) {
9900
+ // Standalone exact-key caller UPDATES share the browser's non-disclosing
9901
+ // missing-row result. A caller REMOVE that matches nothing returns the
9902
+ // zero-change result instead, like bulk/conditional writes (founder
9903
+ // decision, tsk_b7f42808): a missing row and a row outside the caller's
9904
+ // access give the same { data: [], count: 0, changes: 0 }, so neither
9905
+ // reveals existence. No existence probe bypasses authorization.
9906
+ async function __sw_structExecCallerWrite(prepared, authority, missThrows) {
9608
9907
  const ir = prepared.ir;
9609
9908
  const shape = __sw_structManagedShape(ir.table.name);
9610
9909
  const pk = shape && (shape.primaryKey !== undefined ? shape.primaryKey : shape.clientPrimaryKey);
9611
- const keyed = authority !== __sw_structServerNamespace && ir.scope.mode !== 'server'
9910
+ const keyed = missThrows && authority !== __sw_structServerNamespace && ir.scope.mode !== 'server'
9612
9911
  && !(ir.scope.mode === 'unscoped' && prepared.principalId === undefined && __sw_structResolveIntent(ir.table.name) === 'server_only')
9613
9912
  && typeof pk === 'string' && ir.where.length === 1
9614
9913
  && ir.where[0].op === 'eq' && __sw_lower(ir.where[0].column) === __sw_lower(pk);
@@ -9618,11 +9917,11 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
9618
9917
  }
9619
9918
  async function __sw_structUpdate(table, spec, authority) {
9620
9919
  const prepared = await __sw_structPrepareUpdate(table, spec, authority);
9621
- return __sw_structExecCallerWrite(prepared, authority);
9920
+ return __sw_structExecCallerWrite(prepared, authority, true);
9622
9921
  }
9623
9922
  async function __sw_structRemove(table, spec, authority) {
9624
9923
  const prepared = await __sw_structPrepareRemove(table, spec, authority);
9625
- return __sw_structExecCallerWrite(prepared, authority);
9924
+ return __sw_structExecCallerWrite(prepared, authority, false);
9626
9925
  }
9627
9926
 
9628
9927
  async function __sw_structTransaction(intents, authority) {
@@ -9647,7 +9946,8 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
9647
9946
  // back (checked in the same transaction, after earlier intents).
9648
9947
  // Updates and removes are already limited to rows the caller may
9649
9948
  // 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;
9949
+ if (item.ir.kind === 'insert' && (item.ir.scope.mode === 'member' || item.ir.scope.mode === 'policy'
9950
+ || (item.ir.scope.mode === 'user' || item.ir.scope.mode === 'server') && item.ir.scope.grantRow)) item.ir.txGuard = true;
9651
9951
  // A transaction is never keyed: an explicit key on one of its inserts refuses before anything runs.
9652
9952
  if (item.idempotencyKey !== undefined) {
9653
9953
  throw __sw_keyedError('IDEMPOTENCY_UNSUPPORTED', 'sw.db.tx: an insert inside a transaction cannot take idempotencyKey. Nothing ran.', 400);
@@ -13593,16 +13893,14 @@ var AUTH_JS = ` auth: (function () {
13593
13893
  return invite;
13594
13894
  },
13595
13895
 
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.
13896
+ // Email sign-in codes (tsk_8fe91eb5). The app owner sets the policy ('off', 'optional', 'required') with a
13897
+ // developer key. A user with the factor answers every password sign-in with { mfa_required: true,
13898
+ // mfa_token, methods: ['email'] } and no session; challenge() (or challengeWithCookie) completes it with the
13899
+ // code. The explicit forms send the user's JWT through userTokenJSON (project-bound); platformJSON would
13900
+ // replace it with the developer key (pfb_ab7215b47cb9).
13599
13901
  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.
13902
+ // Turn codes on (policy 'optional' or 'required'): mails a code to the verified address.
13903
+ // Resolves { challenge_id, delivery, expires_in }, or { enrolled: true } when already on.
13606
13904
  enroll(opts) {
13607
13905
  opts = opts || {};
13608
13906
  if (!opts.token) {
@@ -13612,9 +13910,10 @@ var AUTH_JS = ` auth: (function () {
13612
13910
  }
13613
13911
  return userTokenJSON('/v1/auth/mfa/enroll', opts.token, {
13614
13912
  method: 'POST',
13615
- body: JSON.stringify({}),
13913
+ body: JSON.stringify({ method: 'email' }),
13616
13914
  });
13617
13915
  },
13916
+ // Authenticator-app activation is not available: answers 501 AUTH_FEATURE_UNAVAILABLE.
13618
13917
  // Establish recent authority for the exact enrollment. Password
13619
13918
  // success returns activation_token directly. Email sends a code and
13620
13919
  // returns challenge_id for verifyReauthentication().
@@ -13650,29 +13949,22 @@ var AUTH_JS = ` auth: (function () {
13650
13949
  }),
13651
13950
  });
13652
13951
  },
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.
13952
+ // Confirm enrolment with the code enroll() mailed. Resolves { enrolled: true, method: 'email' }.
13656
13953
  verify(opts) {
13657
13954
  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');
13955
+ if (!opts.token || !opts.challenge_id || !opts.code) {
13956
+ const err = new Error('sw.auth.mfa.verify: token, challenge_id, and code are required');
13660
13957
  err.code = 'VALIDATION_ERROR';
13661
13958
  throw err;
13662
13959
  }
13663
13960
  return userTokenJSON('/v1/auth/mfa/verify', opts.token, {
13664
13961
  method: 'POST',
13665
- body: JSON.stringify({
13666
- enrollment_id: opts.enrollment_id,
13667
- activation_token: opts.activation_token,
13668
- code: opts.code,
13669
- }),
13962
+ body: JSON.stringify({ challenge_id: opts.challenge_id, code: opts.code }),
13670
13963
  });
13671
13964
  },
13672
13965
  // 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.
13966
+ // { mfa_required:true, mfa_token }: the 6-digit code from the email.
13967
+ // Returns the same shape as /login on success.
13676
13968
  challenge(opts) {
13677
13969
  opts = opts || {};
13678
13970
  if (!opts.mfa_token || !opts.code) {
@@ -13689,6 +13981,20 @@ var AUTH_JS = ` auth: (function () {
13689
13981
  }),
13690
13982
  });
13691
13983
  },
13984
+ // Mail a new code for the same sign-in (60 s apart, 5 an hour per account). The sign-in keeps its
13985
+ // original 5-minute expiry. Resolves { sent, delivery, expires_in }.
13986
+ resend(opts) {
13987
+ opts = opts || {};
13988
+ if (!opts.mfa_token) {
13989
+ const err = new Error('sw.auth.mfa.resend: mfa_token is required');
13990
+ err.code = 'VALIDATION_ERROR';
13991
+ throw err;
13992
+ }
13993
+ return platformJSON('/v1/auth/mfa/challenge', {
13994
+ method: 'POST',
13995
+ body: JSON.stringify({ project_id: projectId, mfa_token: opts.mfa_token, resend: true }),
13996
+ });
13997
+ },
13692
13998
  // Complete the password-login MFA branch as a cookie session. This
13693
13999
  // adds no authority: it brokers the same developer-authenticated
13694
14000
  // challenge(), then keeps the returned access/refresh credentials
@@ -13703,28 +14009,55 @@ var AUTH_JS = ` auth: (function () {
13703
14009
  __sw_setAuthCookies(session.access, session.refresh);
13704
14010
  return { user: session.user, expires_at: __sw_preflightJwt(session.access).expSec * 1000 };
13705
14011
  },
13706
- // Disable MFA. Requires a fresh code (TOTP or backup) so a
13707
- // stolen JWT alone can't turn it off.
14012
+ // Turn codes off, in two steps: { token, password } re-checks the password and mails a code
14013
+ // (resolves { challenge_id, delivery, expires_in }); { token, challenge_id, code } removes the factor
14014
+ // (resolves { enrolled: false }). Refused under a 'required' policy (409 AUTH_MFA_REQUIRED_BY_APP).
13708
14015
  unenroll(opts) {
13709
14016
  opts = opts || {};
13710
- if (!opts.token || !opts.code) {
13711
- const err = new Error('sw.auth.mfa.unenroll: token and code are required');
14017
+ const confirming = opts.challenge_id !== undefined;
14018
+ if (!opts.token || (confirming ? !opts.code : !opts.password)) {
14019
+ const err = new Error('sw.auth.mfa.unenroll: token and either password, or challenge_id and code, are required');
13712
14020
  err.code = 'VALIDATION_ERROR';
13713
14021
  throw err;
13714
14022
  }
13715
14023
  return userTokenJSON('/v1/auth/mfa/unenroll', opts.token, {
13716
14024
  method: 'POST',
13717
- body: JSON.stringify({ code: opts.code }),
14025
+ body: JSON.stringify(confirming ? { challenge_id: opts.challenge_id, code: opts.code } : { password: opts.password }),
14026
+ });
14027
+ },
14028
+ // For a user who cannot receive a code (no session needed): { email, password } starts a 72-hour wait
14029
+ // and mails a notice with a cancel link (202 { removal_pending, removes_at }); the same call after the
14030
+ // wait removes the factor ({ enrolled: false }). Refused (503) when the app cannot email the user.
14031
+ recover(opts) {
14032
+ opts = opts || {};
14033
+ if (!opts.email || !opts.password) {
14034
+ const err = new Error('sw.auth.mfa.recover: email and password are required');
14035
+ err.code = 'VALIDATION_ERROR';
14036
+ throw err;
14037
+ }
14038
+ return platformJSON('/v1/auth/mfa/recovery', {
14039
+ method: 'POST',
14040
+ body: JSON.stringify({ project_id: projectId, email: opts.email, password: opts.password }),
13718
14041
  });
13719
14042
  },
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.
14043
+ // Cancel a pending removal with the token from the notice's cancel link (no session or code).
14044
+ // Resolves { outcome: 'cancelled' | 'already_closed' | 'completed', message }.
14045
+ cancelRecovery(opts) {
14046
+ opts = opts || {};
14047
+ if (!opts.token) {
14048
+ const err = new Error('sw.auth.mfa.cancelRecovery: token is required');
14049
+ err.code = 'VALIDATION_ERROR';
14050
+ throw err;
14051
+ }
14052
+ return platformJSON('/v1/auth/mfa/recovery/cancel', {
14053
+ method: 'POST',
14054
+ body: JSON.stringify({ token: opts.token }),
14055
+ });
14056
+ },
14057
+ // The same steps for the request's own session (cookie or Bearer). challenge_id and the codes are the
14058
+ // step's own one-time credentials and stay explicit. Turning codes on or off ends no session.
13726
14059
  async enrollWithCookie(req) {
13727
- return sessionChange('mfa.enrollWithCookie', req, '/v1/auth/mfa/enroll', { body: {} });
14060
+ return sessionChange('mfa.enrollWithCookie', req, '/v1/auth/mfa/enroll', { body: { method: 'email' } });
13728
14061
  },
13729
14062
  async reauthenticateWithCookie(req, opts) {
13730
14063
  return sessionChange('mfa.reauthenticateWithCookie', req, '/v1/auth/mfa/activation/reauth', { body: function () {
@@ -13743,15 +14076,19 @@ var AUTH_JS = ` auth: (function () {
13743
14076
  } });
13744
14077
  },
13745
14078
  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 };
14079
+ return sessionChange('mfa.verifyWithCookie', req, '/v1/auth/mfa/verify', { body: function () {
14080
+ opts = requireFields('mfa.verifyWithCookie', opts, ['challenge_id', 'code']);
14081
+ return { challenge_id: opts.challenge_id, code: opts.code };
13749
14082
  } });
13750
14083
  },
13751
14084
  async unenrollWithCookie(req, opts) {
13752
14085
  return sessionChange('mfa.unenrollWithCookie', req, '/v1/auth/mfa/unenroll', { body: function () {
13753
- opts = requireFields('mfa.unenrollWithCookie', opts, ['code']);
13754
- return { code: opts.code };
14086
+ if (opts && opts.challenge_id !== undefined) {
14087
+ opts = requireFields('mfa.unenrollWithCookie', opts, ['challenge_id', 'code']);
14088
+ return { challenge_id: opts.challenge_id, code: opts.code };
14089
+ }
14090
+ opts = requireFields('mfa.unenrollWithCookie', opts, ['password']);
14091
+ return { password: opts.password };
13755
14092
  } });
13756
14093
  },
13757
14094
  },
@@ -16031,7 +16368,7 @@ function __sw_dataIsDuplicateConflict(error) {
16031
16368
  }
16032
16369
  function __sw_dataPolicyUsesMembership(policy, table) {
16033
16370
  if (!policy || typeof policy !== 'object') return false;
16034
- if (policy.k === 'm') return policy.m && policy.m.m === table;
16371
+ if (policy.k === 'm') return policy.m && policy.m.m === table && !policy.m.og;
16035
16372
  if (policy.k === 'a') return policy.p.some(item => __sw_dataPolicyUsesMembership(item, table));
16036
16373
  // allOf: a membership table named by either slot stays closed to browser writes.
16037
16374
  if (policy.k === 'n') return __sw_dataPolicyUsesMembership(policy.g, table) || __sw_dataPolicyUsesMembership(policy.w, table);
@@ -17180,20 +17517,25 @@ function reportedFailureFields(platformCtx, requestId) {
17180
17517
  return out;
17181
17518
  }
17182
17519
 
17183
- /** Add the correlation handle without changing responses when tracing is absent. */
17184
- function attachTraceId(response, platformCtx) {
17520
+ /** Add the correlation handle without changing responses when tracing is absent. handlerResponse is true only
17521
+ * at the handler-success site for a Response the handler itself returned. */
17522
+ function attachTraceId(response, platformCtx, handlerResponse) {
17185
17523
  try {
17186
17524
  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.
17525
+ // X-Sw-Route-Pattern and X-Sw-Response-Source are the PLATFORM's (the edge's completion log): a function's
17526
+ // own copy never passes through, with or without a trace; only the runtime's values are set below.
17189
17527
  if (!t) {
17190
- if (!response.headers.has('X-Sw-Route-Pattern')) return response;
17528
+ if (!response.headers.has('X-Sw-Route-Pattern') && !response.headers.has('X-Sw-Response-Source') && handlerResponse !== true) return response;
17191
17529
  const stripped = new Headers(response.headers);
17192
17530
  stripped.delete('X-Sw-Route-Pattern');
17531
+ stripped.delete('X-Sw-Response-Source');
17532
+ if (handlerResponse === true) stripped.set('X-Sw-Response-Source', 'handler');
17193
17533
  return new Response(response.body, { status: response.status, statusText: response.statusText, headers: stripped });
17194
17534
  }
17195
17535
  const headers = new Headers(response.headers);
17196
17536
  headers.delete('X-Sw-Route-Pattern');
17537
+ headers.delete('X-Sw-Response-Source');
17538
+ if (handlerResponse === true) headers.set('X-Sw-Response-Source', 'handler');
17197
17539
  headers.set('X-Trace-Id', t.traceId);
17198
17540
  headers.set('traceparent', '00-' + t.traceId + '-' + t.spanId + '-' + (t.sampled ? '01' : '00'));
17199
17541
  headers.set('X-Trace-Sampling', t.sampled
@@ -17577,6 +17919,9 @@ function isCanonicalBigintText(value) {
17577
17919
  }
17578
17920
 
17579
17921
  // worker/src/utils/db-schema-deploy/client-permissions.ts
17922
+ function tableAllowsClientWrites(table) {
17923
+ return Boolean(table.client && (table.client.create !== null || table.client.update !== null || table.client.delete || table.client.intake !== void 0));
17924
+ }
17580
17925
  function parseClientPermissions(value, columns, scope) {
17581
17926
  const fail = (message) => ({ ok: false, message });
17582
17927
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -18067,6 +18412,11 @@ var SW_INIT_JS = `
18067
18412
  globalThis.sw = globalThis.sw || {};
18068
18413
  ${DB_OUTCOME_ERROR_JS}
18069
18414
 
18415
+ // Responses this runtime made (sw.endpoint refusals and caught throws). index.mjs captures the reader and
18416
+ // removes its public hook before any user module loads, so customer code can neither read nor change it.
18417
+ const __sw_runtimeMadeResponses = new WeakSet();
18418
+ globalThis.sw.__isRuntimeMadeResponse = function (response) { return __sw_runtimeMadeResponses.has(response); };
18419
+
18070
18420
  // Legacy USER_ENV supplies customer-only process.env after native reflection is denied.
18071
18421
  // index.mjs captures this initializer before lazy module load and removes its public hook.
18072
18422
  globalThis.sw.__wireProcessEnv = function __sw_wireProcessEnv(runtimeEnv) {
@@ -18176,23 +18526,32 @@ function __sw_corsHeaders(corsCfg, request) {
18176
18526
  };
18177
18527
  }
18178
18528
 
18529
+ /** Record a Response this runtime made itself (never the handler's). */
18530
+ function __sw_runtimeMade(response) {
18531
+ __sw_runtimeMadeResponses.add(response);
18532
+ return response;
18533
+ }
18534
+
18179
18535
  function __sw_jsonError(status, code, message, extras) {
18180
18536
  var body = Object.assign({ ok: false, error: code, message: message }, extras || {});
18181
- return new Response(JSON.stringify(body), {
18537
+ return __sw_runtimeMade(new Response(JSON.stringify(body), {
18182
18538
  status: status,
18183
18539
  headers: { 'Content-Type': 'application/json' },
18184
- });
18540
+ }));
18185
18541
  }
18186
18542
 
18187
18543
  function __sw_mergeHeaders(response, extra) {
18188
18544
  if (!extra || Object.keys(extra).length === 0) return response;
18189
18545
  var headers = new Headers(response.headers);
18190
18546
  for (var k of Object.keys(extra)) headers.set(k, extra[k]);
18191
- return new Response(response.body, {
18547
+ var merged = new Response(response.body, {
18192
18548
  status: response.status,
18193
18549
  statusText: response.statusText,
18194
18550
  headers: headers,
18195
18551
  });
18552
+ // The copy keeps the original's provenance.
18553
+ if (__sw_runtimeMadeResponses.has(response)) __sw_runtimeMadeResponses.add(merged);
18554
+ return merged;
18196
18555
  }
18197
18556
 
18198
18557
  /**
@@ -18237,7 +18596,7 @@ globalThis.sw.endpoint = function (config) {
18237
18596
  const console = sw.__sw_nativeConsole || globalThis.console;
18238
18597
  var corsHeaders = __sw_corsHeaders(cors, request);
18239
18598
  if (request.method === 'OPTIONS') {
18240
- return new Response(null, { status: 204, headers: corsHeaders });
18599
+ return __sw_runtimeMade(new Response(null, { status: 204, headers: corsHeaders }));
18241
18600
  }
18242
18601
 
18243
18602
  // Auth.
@@ -18293,10 +18652,10 @@ globalThis.sw.endpoint = function (config) {
18293
18652
  var code = (r && r.error) || 'RATE_LIMITED';
18294
18653
  var message = (r && r.message) || 'Too many requests.';
18295
18654
  return __sw_mergeHeaders(
18296
- new Response(
18655
+ __sw_runtimeMade(new Response(
18297
18656
  JSON.stringify({ ok: false, error: code, message: message, retry_after: retry }),
18298
18657
  { status: 429, headers: { 'Content-Type': 'application/json', 'Retry-After': String(retry) } }
18299
- ),
18658
+ )),
18300
18659
  corsHeaders
18301
18660
  );
18302
18661
  }
@@ -18315,10 +18674,10 @@ globalThis.sw.endpoint = function (config) {
18315
18674
  rlErr && rlErr.code, rlErr && rlErr.status, rlErr && rlErr.message
18316
18675
  );
18317
18676
  return __sw_mergeHeaders(
18318
- new Response(
18677
+ __sw_runtimeMade(new Response(
18319
18678
  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
18679
  { status: 503, headers: { 'Content-Type': 'application/json', 'Retry-After': String(rateLimit.windowSeconds) } }
18321
- ),
18680
+ )),
18322
18681
  corsHeaders
18323
18682
  );
18324
18683
  }
@@ -18403,7 +18762,7 @@ function isApiIndexHandlerPath(file) {
18403
18762
  return /^api\/.+\/index\.(?:ts|tsx|mts|js|mjs|jsx)$/.test(file) || /^api\/index\.(?:ts|tsx|mts|js|mjs|jsx)$/.test(file);
18404
18763
  }
18405
18764
 
18406
- // ../../../../../../pmwork/wt/files-migration-c0-rig/worker/node_modules/acorn/dist/acorn.mjs
18765
+ // worker/node_modules/acorn/dist/acorn.mjs
18407
18766
  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
18767
  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
18768
  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 +24374,140 @@ function tokenizer2(input, options) {
24015
24374
  return Parser.tokenizer(input, options);
24016
24375
  }
24017
24376
 
24377
+ // worker/src/utils/db-schema-deploy/extract-schema-grants.ts
24378
+ function readGrantScope(r, table, line, fail) {
24379
+ r.expectPunct("(", "after grant");
24380
+ r.expectPunct("{", "in grant options");
24381
+ let membership, grantee, row;
24382
+ let operations;
24383
+ const seen = /* @__PURE__ */ new Set();
24384
+ while (!r.tryPunct("}")) {
24385
+ const key = r.readKey("in grant options");
24386
+ if (seen.has(key.name)) fail(key.line, `Duplicate grant option "${key.name}".`);
24387
+ seen.add(key.name);
24388
+ r.expectPunct(":", "after grant option");
24389
+ if (key.name === "table" || key.name === "grantee" || key.name === "row") {
24390
+ const value = r.readLiteral("in grant options");
24391
+ 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.");
24392
+ if (key.name === "table") membership = value.toLowerCase();
24393
+ else if (key.name === "grantee") grantee = value.toLowerCase();
24394
+ else row = value.toLowerCase();
24395
+ } else if (key.name === "operations") {
24396
+ const ops = [];
24397
+ r.expectPunct("[", "in grant operations");
24398
+ while (!r.tryPunct("]")) {
24399
+ const op = r.readLiteral("in grant operations");
24400
+ if (op !== "read" && op !== "update") fail(key.line, "grant operations allow only read and update.");
24401
+ if (ops.includes(op)) fail(key.line, "Duplicate grant operation.");
24402
+ ops.push(op);
24403
+ if (!r.tryPunct(",")) {
24404
+ r.expectPunct("]", "closing grant operations");
24405
+ break;
24406
+ }
24407
+ }
24408
+ if (!ops.includes("read")) fail(key.line, "grant operations require read; update alone is not allowed.");
24409
+ operations = ops.includes("update") ? ["read", "update"] : ["read"];
24410
+ } else fail(key.line, `Unknown grant option "${key.name}". Allowed: table, grantee, row, operations.`);
24411
+ if (!r.tryPunct(",")) {
24412
+ r.expectPunct("}", "closing grant options");
24413
+ break;
24414
+ }
24415
+ }
24416
+ r.tryPunct(",");
24417
+ r.expectPunct(")", "closing grant");
24418
+ 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.`);
24419
+ 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.`);
24420
+ return { kind: "member", group: ["user_id"], membership, memberUser: grantee, memberGroup: row === void 0 ? ["user_id"] : ["user_id", row], operations, ownerGrant: true };
24421
+ }
24422
+ function resolveGrantRowKeys(scope, key, table, line, fail) {
24423
+ const visit = (node) => {
24424
+ if (node.kind === "member" && node.ownerGrant && node.memberGroup.length === 2 && node.group.length === 1) {
24425
+ 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.`);
24426
+ node.group = ["user_id", key.toLowerCase()];
24427
+ } else if (node.kind === "any_of") node.policies.forEach(visit);
24428
+ else if (node.kind === "all_of") {
24429
+ visit(node.gate);
24430
+ visit(node.who);
24431
+ }
24432
+ };
24433
+ if (scope.kind === "policy") visit(scope.policy);
24434
+ }
24435
+ function validateDeclaredMemberships(tables, tableByName, knownColumns, errors) {
24436
+ const memberPolicies = (scope) => {
24437
+ const out = [];
24438
+ const visit = (node) => {
24439
+ if (node.kind === "member") out.push(node);
24440
+ else if (node.kind === "any_of") node.policies.forEach(visit);
24441
+ else if (node.kind === "all_of") {
24442
+ visit(node.gate);
24443
+ visit(node.who);
24444
+ }
24445
+ };
24446
+ if (scope.kind === "member") out.push(scope);
24447
+ else if (scope.kind === "policy") visit(scope.policy);
24448
+ return out;
24449
+ };
24450
+ for (const t of tables) {
24451
+ for (const sc of memberPolicies(t.scope)) {
24452
+ const own = knownColumns(t);
24453
+ for (const g of sc.group) {
24454
+ if (!own.has(g)) {
24455
+ errors.push(
24456
+ `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.`
24457
+ );
24458
+ }
24459
+ }
24460
+ const membershipTable = tableByName.get(sc.membership);
24461
+ if (!membershipTable) {
24462
+ errors.push(
24463
+ `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.`
24464
+ );
24465
+ continue;
24466
+ }
24467
+ const membershipCols = knownColumns(membershipTable);
24468
+ if (sc.ownerGrant) {
24469
+ if (membershipTable.scope.kind !== "owner" || membershipTable.scope.column !== "user_id" || membershipTable.scope.visitors) {
24470
+ errors.push(`Grant table "${sc.membership}" must use owner() with managed user_id and authenticated owners.`);
24471
+ }
24472
+ 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.`);
24473
+ const grantee = membershipTable.columns.find((column) => column.name === sc.memberUser);
24474
+ if (!grantee || grantee.helper !== "text" || grantee.nullable) errors.push(`Grant grantee "${sc.membership}"."${sc.memberUser}" must be a required text column.`);
24475
+ if (sc.memberGroup.length === 2) {
24476
+ const row = membershipTable.columns.find((column) => column.name === sc.memberGroup[1]);
24477
+ const key = t.columns.find((column) => column.name === sc.group[1]);
24478
+ const keyHelper = key?.helper === "id" ? key.uuid ? "text" : "integer" : null;
24479
+ if (!row || row.nullable || row.helper !== keyHelper) {
24480
+ errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" must be a required ${keyHelper ?? "id-typed"} column holding a "${t.name}" id.`);
24481
+ } else if (row.hasDefault || row.references !== t.name || row.onDelete !== "cascade") {
24482
+ 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' }).`);
24483
+ }
24484
+ if (membershipTable.client?.update?.includes(sc.memberGroup[1])) {
24485
+ 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.`);
24486
+ }
24487
+ const users = tables.filter((other) => memberPolicies(other.scope).some((m) => m.ownerGrant && m.membership === sc.membership));
24488
+ if (users.length !== 1 || memberPolicies(t.scope).filter((m) => m.ownerGrant && m.membership === sc.membership).length !== 1) {
24489
+ 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.`);
24490
+ }
24491
+ }
24492
+ } else if (tableAllowsClientWrites(membershipTable)) {
24493
+ errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
24494
+ }
24495
+ if (!membershipCols.has(sc.memberUser)) {
24496
+ errors.push(
24497
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
24498
+ );
24499
+ }
24500
+ for (const mg of sc.ownerGrant ? [] : sc.memberGroup) {
24501
+ if (!membershipCols.has(mg)) {
24502
+ errors.push(
24503
+ `The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
24504
+ );
24505
+ }
24506
+ }
24507
+ }
24508
+ }
24509
+ }
24510
+
24018
24511
  // worker/src/utils/db-schema-deploy/schema-constants.ts
24019
24512
  var LITERAL_WORDS = /* @__PURE__ */ new Set(["true", "false", "null"]);
24020
24513
  var STATEMENT_WORDS = /* @__PURE__ */ new Set([
@@ -25084,8 +25577,9 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
25084
25577
  }
25085
25578
 
25086
25579
  // worker/src/utils/db-schema-deploy/extract-schema-roles.ts
25580
+ var EMAIL_DOMAIN = EMAIL_DOMAIN_PATTERN;
25087
25581
  var GROUP_KEYS = ["roles", "adminGrantableRoles"];
25088
- var APP_ROLE_KEYS = ["grantableBy"];
25582
+ var APP_ROLE_KEYS = ["grantableBy", "emailDomain"];
25089
25583
  function readStringList2(r, fail, ctx) {
25090
25584
  const open = r.peek();
25091
25585
  if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted role names, such as ['viewer'].`);
@@ -25129,7 +25623,7 @@ function readAppRoles(r, fail) {
25129
25623
  const key = r.readKey("for an app role name");
25130
25624
  if (out.some((role2) => role2.name === key.name)) fail(key.line, `app role "${key.name}" is declared twice.`);
25131
25625
  r.expectPunct(":", `after app role "${key.name}"`);
25132
- r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...] }`);
25626
+ r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...], emailDomain: '...' }`);
25133
25627
  const role = { name: key.name, grantableBy: [] };
25134
25628
  const seen = /* @__PURE__ */ new Set();
25135
25629
  while (!r.tryPunct("}")) {
@@ -25140,7 +25634,16 @@ function readAppRoles(r, fail) {
25140
25634
  if (seen.has(opt.name)) fail(opt.line, `app role "${key.name}" declares ${opt.name} twice.`);
25141
25635
  seen.add(opt.name);
25142
25636
  r.expectPunct(":", `after appRoles.${key.name}.${opt.name}`);
25143
- role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
25637
+ if (opt.name === "emailDomain") {
25638
+ const t = r.next();
25639
+ if (t.kind !== "string") fail(t.line, `appRoles.${key.name}.emailDomain must be one quoted domain, such as 'example.com'.`);
25640
+ if (!EMAIL_DOMAIN.test(t.value)) {
25641
+ 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.`);
25642
+ }
25643
+ role.emailDomain = t.value;
25644
+ } else {
25645
+ role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
25646
+ }
25144
25647
  if (!r.tryPunct(",")) {
25145
25648
  r.expectPunct("}", `closing app role "${key.name}"`);
25146
25649
  break;
@@ -25157,12 +25660,25 @@ function readAppRoles(r, fail) {
25157
25660
  function validateDeclaredRoles(groups, appRoles) {
25158
25661
  const delegation = /* @__PURE__ */ Object.create(null);
25159
25662
  for (const role of appRoles ?? []) delegation[role.name] = role.grantableBy;
25160
- return roleVocabularyErrors({
25663
+ return [...emailDomainRoleErrors(appRoles ?? []), ...roleVocabularyErrors({
25161
25664
  groupRoles: groups?.roles ?? [],
25162
25665
  adminGrantableGroupRoles: groups?.adminGrantableRoles ?? [],
25163
25666
  appRoles: (appRoles ?? []).map((role) => role.name),
25164
25667
  appRoleDelegation: delegation
25165
- });
25668
+ })];
25669
+ }
25670
+ function emailDomainRoleErrors(appRoles) {
25671
+ const errors = [];
25672
+ const byDomain = new Set(appRoles.filter((role) => role.emailDomain !== void 0).map((role) => role.name));
25673
+ for (const role of appRoles) {
25674
+ if (role.emailDomain !== void 0 && !EMAIL_DOMAIN.test(role.emailDomain)) errors.push(`appRoles.${role.name}.emailDomain is not one lowercase domain.`);
25675
+ for (const grantor of role.grantableBy) {
25676
+ if (byDomain.has(grantor)) {
25677
+ 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.`);
25678
+ }
25679
+ }
25680
+ }
25681
+ return errors;
25166
25682
  }
25167
25683
 
25168
25684
  // worker/src/utils/db-schema-deploy/extract-schema-options.ts
@@ -25249,6 +25765,7 @@ var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
25249
25765
  ...TABLE_MARKER_HELPERS,
25250
25766
  "schema",
25251
25767
  "table",
25768
+ "grant",
25252
25769
  "removed",
25253
25770
  "hasMany",
25254
25771
  "belongsTo",
@@ -25363,10 +25880,10 @@ function tokenize2(source) {
25363
25880
  var Reader = class {
25364
25881
  constructor(tokens, options = {}) {
25365
25882
  this.tokens = tokens;
25366
- this.bareIdIsText = !options.legacyIntegerIdDefault;
25883
+ this.bareIdIsText = (table) => !options.legacyIntegerIdDefault && !options.integerKeyTables?.has(table.toLowerCase());
25367
25884
  }
25368
25885
  pos = 0;
25369
- /** What a bare id() declares: a random text key, or (legacy retained source) an integer. */
25886
+ /** What a bare id() on this table declares: a random text key, or an integer (ExtractSchemaOptions). */
25370
25887
  bareIdIsText;
25371
25888
  peek() {
25372
25889
  return this.tokens[this.pos];
@@ -25534,7 +26051,7 @@ function readColumn(r, tableName, rawName, line) {
25534
26051
  const col = {
25535
26052
  name,
25536
26053
  helper,
25537
- uuid: helper === "id" && r.bareIdIsText,
26054
+ uuid: helper === "id" && r.bareIdIsText(tableName),
25538
26055
  nullable: false,
25539
26056
  hasDefault: false,
25540
26057
  default: null,
@@ -25771,7 +26288,7 @@ function readPolicyMember(r, tableName, line) {
25771
26288
  if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
25772
26289
  return scope;
25773
26290
  }
25774
- var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), group() or appRole().";
26291
+ var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), grant(), group() or appRole().";
25775
26292
  function readAnyOfBranch(r, tableName, which) {
25776
26293
  const tok = r.expectIdent(`for the ${which} policy in anyOf() on table "${tableName}"`);
25777
26294
  const failAt = (at2, message) => {
@@ -25779,6 +26296,7 @@ function readAnyOfBranch(r, tableName, which) {
25779
26296
  };
25780
26297
  if (tok.value === "owner") return readPolicyOwner(r, tableName, tok.line);
25781
26298
  if (tok.value === "member") return readPolicyMember(r, tableName, tok.line);
26299
+ if (tok.value === "grant") return readGrantScope(r, tableName, tok.line, failAt);
25782
26300
  if (tok.value === "group") return readGroupScope(r, tableName, tok.line, failAt);
25783
26301
  if (tok.value === "appRole") return readAppRoleScope(r, tableName, tok.line, failAt);
25784
26302
  throw new SchemaTsError(`line ${tok.line}: ${ANY_OF_FORMS}`);
@@ -25794,6 +26312,7 @@ function readAnyOfScope(r, tableName, line) {
25794
26312
  if (owners.length !== 1) throw new SchemaTsError(`line ${line}: ${ANY_OF_FORMS}`);
25795
26313
  const owner = owners[0];
25796
26314
  const other = firstPolicy.kind === "owner" ? secondPolicy : firstPolicy;
26315
+ if (other.kind === "member" && other.ownerGrant && owner.column !== DEFAULT_OWNER_COLUMN) throw new SchemaTsError(`line ${line}: grant() requires owner() with managed user_id.`);
25797
26316
  return { kind: "policy", policy: { kind: "any_of", policies: [owner, other] } };
25798
26317
  }
25799
26318
  var ALL_OF_FORMS = "allOf() takes a gate \u2014 member(), group() or appRole() \u2014 and then owner() or anyOf(owner(), ...).";
@@ -25821,6 +26340,7 @@ function readAllOfScope(r, tableName, line) {
25821
26340
  r.tryPunct(",");
25822
26341
  r.expectPunct(")", "closing allOf()");
25823
26342
  const x = who.kind === "any_of" ? who.policies[1] : null;
26343
+ if (x?.kind === "member" && x.ownerGrant) throw new SchemaTsError(`line ${line}: grant() belongs in anyOf(owner(), grant()), not allOf().`);
25824
26344
  for (const node of [gate, ...x ? [x] : []]) {
25825
26345
  if (node.kind !== "member") continue;
25826
26346
  if (!explicitMemberOperations.has(node)) {
@@ -26056,6 +26576,9 @@ function readTable(r, rawName, nameLine) {
26056
26576
  if (key.name === "scope") {
26057
26577
  if (scope) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "scope" twice.`);
26058
26578
  scope = readScope(r, rawName);
26579
+ resolveGrantRowKeys(scope, idCols[0]?.name, rawName, key.line, (at2, message) => {
26580
+ throw new SchemaTsError(`line ${at2}: ${message}`);
26581
+ });
26059
26582
  } else if (key.name === "indexes") {
26060
26583
  if (indexEntries) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "indexes" twice.`);
26061
26584
  indexEntries = readColumnLists(r, rawName, "indexes");
@@ -26287,55 +26810,7 @@ function extractSchemaTs(source, options = {}) {
26287
26810
  };
26288
26811
  resolveAppRoleScopes(tables, appRoles, errors);
26289
26812
  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
- }
26813
+ validateDeclaredMemberships(tables, tableByName, knownColumns, errors);
26339
26814
  validateDeclaredRelations(tables, tableByName, knownColumns, (table) => removedTables.includes(table) ? "removedTable()" : exportedTables.includes(table) ? "exported()" : null, errors);
26340
26815
  validateRowLinks(tables, tableByName, errors);
26341
26816
  validateIntake(tables, tableByName, errors);
@@ -26343,7 +26818,7 @@ function extractSchemaTs(source, options = {}) {
26343
26818
  errors.push(...fileErrors);
26344
26819
  validateFileCollections(fileCollections, (name) => {
26345
26820
  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;
26821
+ return t ? { columns: knownColumns(t), clientWrites: tableAllowsClientWrites(t), parentTarget: fileParentTarget(t) } : null;
26347
26822
  }, errors);
26348
26823
  }
26349
26824
  if (groupRoles || appRoles) errors.push(...validateDeclaredRoles(groupRoles, appRoles));