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