@somewhere-cloud/cli 0.38.16 → 0.38.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -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 +269 -111
- package/runtime/source-lint.cjs +646 -171
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}`);
|
|
@@ -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,13 @@ var DB_TYPED_ERRORS_JS = ` // The REST bridge's refusal as the caller sees
|
|
|
1526
1543
|
// because one row targets a group the caller cannot write to.
|
|
1527
1544
|
const guard = msg.match(/SW_(MEMBERSHIP_REQUIRED|POLICY_AUTHORITY_REQUIRED)/);
|
|
1528
1545
|
if (guard) {
|
|
1529
|
-
|
|
1546
|
+
// A single write raises it too: a per-row share's guard (query-sql.ts grantRow) or a parent()
|
|
1547
|
+
// move only a grant() would allow.
|
|
1548
|
+
const refused = new Error(label !== 'sw.db.tx' && guard[1] === 'POLICY_AUTHORITY_REQUIRED'
|
|
1549
|
+
? label + ': the access policy of this table does not permit this change by the signed-in user. Nothing was written.'
|
|
1550
|
+
: guard[1] === 'MEMBERSHIP_REQUIRED'
|
|
1530
1551
|
? 'sw.db.tx: an insert targets a group the signed-in user is not a member of. Nothing in the transaction was written.'
|
|
1531
|
-
: 'sw.db.tx: the access policy of
|
|
1552
|
+
: 'sw.db.tx: the access policy of a table does not permit one of these changes by the signed-in user. Nothing in the transaction was written.');
|
|
1532
1553
|
refused.code = guard[1];
|
|
1533
1554
|
refused.status = 403;
|
|
1534
1555
|
refused.__sw_expose = true;
|
|
@@ -2802,9 +2823,54 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
2802
2823
|
const media = /^\\/v1\\/(?:ai\\/(?:tts|generate-image|remove-background)|render\\/(?:screenshot|pdf)|web\\/scrape|browser\\/test)$/.test(path);
|
|
2803
2824
|
const mutation = state && media && method === 'POST';
|
|
2804
2825
|
if (mutation) { state.generation++; state.pending++; state.envelope = null; }
|
|
2805
|
-
|
|
2826
|
+
const signal = opts && opts.signal;
|
|
2827
|
+
const stateForDeadline = signal ? { dispatched: false } : null;
|
|
2828
|
+
const started = signal ? Date.now() : 0;
|
|
2829
|
+
const preparedOpts = signal ? Object.assign({}, opts, { __sw_deadlineState: stateForDeadline }) : opts;
|
|
2830
|
+
try { return await __sw_platformFetchInner(path, preparedOpts); }
|
|
2831
|
+
catch (err) {
|
|
2832
|
+
if (signal && !stateForDeadline.dispatched && err && err.code === 'PLATFORM_CALL_CANCELLED') {
|
|
2833
|
+
__sw_recordPlatformCall(path, started, undefined);
|
|
2834
|
+
}
|
|
2835
|
+
throw err;
|
|
2836
|
+
}
|
|
2806
2837
|
finally { if (mutation) { state.pending--; state.envelope = null; } }
|
|
2807
2838
|
}
|
|
2839
|
+
// Cancellation stops waiting; a request already sent may still complete.
|
|
2840
|
+
// Nothing is retried, and cancelled preparation never starts a request.
|
|
2841
|
+
function __sw_platformBound(signal, work, state) {
|
|
2842
|
+
if (!signal) return work();
|
|
2843
|
+
function cancelled() {
|
|
2844
|
+
const err = new Error('The platform call stopped waiting. Check its outcome before trying again.');
|
|
2845
|
+
err.code = 'PLATFORM_CALL_CANCELLED';
|
|
2846
|
+
err.status = 504;
|
|
2847
|
+
err.retry = false;
|
|
2848
|
+
err.outcome = state && state.dispatched ? 'unknown' : 'not_attempted';
|
|
2849
|
+
return err;
|
|
2850
|
+
}
|
|
2851
|
+
if (signal.aborted) return Promise.reject(cancelled());
|
|
2852
|
+
return new Promise(function (resolve, reject) {
|
|
2853
|
+
let settled = false;
|
|
2854
|
+
function finish(fn, value) {
|
|
2855
|
+
if (settled) return;
|
|
2856
|
+
settled = true;
|
|
2857
|
+
signal.removeEventListener('abort', abort);
|
|
2858
|
+
fn(value);
|
|
2859
|
+
}
|
|
2860
|
+
function abort() { finish(reject, cancelled()); }
|
|
2861
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
2862
|
+
if (signal.aborted) { abort(); return; }
|
|
2863
|
+
try { Promise.resolve(work()).then(function (value) { finish(resolve, value); }, function (err) { finish(reject, err); }); }
|
|
2864
|
+
catch (err) { finish(reject, err); }
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
async function __sw_platformJSONBody(response, signal) {
|
|
2868
|
+
try { return await __sw_platformBound(signal, function () { return response.json(); }, { dispatched: true }); }
|
|
2869
|
+
catch (err) {
|
|
2870
|
+
if (err && err.code === 'PLATFORM_CALL_CANCELLED') throw err;
|
|
2871
|
+
return null;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2808
2874
|
async function __sw_platformFetchInner(path, opts) {
|
|
2809
2875
|
opts = opts || {};
|
|
2810
2876
|
if (path === '/v1/db/query' || path === '/v1/db/batch' || path === '/v1/db/raw-read') {
|
|
@@ -2816,7 +2882,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
2816
2882
|
const __sw_spanStart = Date.now();
|
|
2817
2883
|
// Inside a job or cron delivery every platform call names its job, so the
|
|
2818
2884
|
// platform refuses it once that job has ended (middleware/job-invocation-fence.ts).
|
|
2819
|
-
const __sw_jobMarker = opts.__swFenceExempt ? null : await __sw_jobInvocationMarker();
|
|
2885
|
+
const __sw_jobMarker = opts.__swFenceExempt ? null : await __sw_platformBound(opts.signal, function () { return __sw_jobInvocationMarker(); }, opts.__sw_deadlineState);
|
|
2820
2886
|
const headers = {
|
|
2821
2887
|
...(opts.headers || {}),
|
|
2822
2888
|
...(__sw_jobMarker || {}),
|
|
@@ -2860,12 +2926,15 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
2860
2926
|
__sw_fetchStart = Date.now();
|
|
2861
2927
|
__sw_prepMs = __sw_fetchStart - __sw_spanStart;
|
|
2862
2928
|
}
|
|
2863
|
-
__sw_response = await
|
|
2929
|
+
__sw_response = await __sw_platformBound(opts.signal, function () {
|
|
2930
|
+
if (opts.__sw_deadlineState) opts.__sw_deadlineState.dispatched = true;
|
|
2931
|
+
return __sw_runtimeFetch(platformBase + path, {
|
|
2864
2932
|
method: __sw_method,
|
|
2865
2933
|
headers,
|
|
2866
2934
|
body: opts.body,
|
|
2867
2935
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
2868
|
-
|
|
2936
|
+
});
|
|
2937
|
+
}, opts.__sw_deadlineState);
|
|
2869
2938
|
if (__sw_serverTiming) __sw_fetchMs = Date.now() - __sw_fetchStart;
|
|
2870
2939
|
} catch (err) {
|
|
2871
2940
|
if (__sw_serverTiming) __sw_fetchMs = Date.now() - __sw_fetchStart;
|
|
@@ -2931,8 +3000,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
2931
3000
|
|
|
2932
3001
|
async function platformJSON(path, opts) {
|
|
2933
3002
|
const r = await platformFetch(path, opts);
|
|
2934
|
-
|
|
2935
|
-
try { data = await r.json(); } catch { data = null; }
|
|
3003
|
+
const data = await __sw_platformJSONBody(r, opts && opts.signal);
|
|
2936
3004
|
if (!r.ok || !data || data.ok === false) {
|
|
2937
3005
|
throw __sw_platformError(r, data);
|
|
2938
3006
|
}
|
|
@@ -3036,7 +3104,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
3036
3104
|
});
|
|
3037
3105
|
if (ingest) {
|
|
3038
3106
|
let result = null;
|
|
3039
|
-
|
|
3107
|
+
result = await __sw_platformJSONBody(response, controller.signal);
|
|
3040
3108
|
const queued = result && typeof result.queued === 'number' ? result.queued : 0;
|
|
3041
3109
|
const dropped = result && typeof result.dropped === 'number' ? result.dropped : null;
|
|
3042
3110
|
if (response.status === 202 && dropped === 0) return;
|
|
@@ -3048,8 +3116,8 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
3048
3116
|
throw failure;
|
|
3049
3117
|
}
|
|
3050
3118
|
if (!response.ok) throw new Error('Console log delivery was refused.');
|
|
3051
|
-
const result = await response.
|
|
3052
|
-
if (!result.data || result.data.failed || result.data.unknown) {
|
|
3119
|
+
const result = await __sw_platformJSONBody(response, controller.signal);
|
|
3120
|
+
if (!result || !result.data || result.data.failed || result.data.unknown) {
|
|
3053
3121
|
throw new Error('Console log delivery could not be confirmed.');
|
|
3054
3122
|
}
|
|
3055
3123
|
} finally { clearTimeout(timeout); }
|
|
@@ -3061,9 +3129,14 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
3061
3129
|
// itself lives in the sw.rateLimit fragment.
|
|
3062
3130
|
// The platform's counter: one increment, answered with the count it reached.
|
|
3063
3131
|
async function __sw_rateLimitAsk(key, max, windowSeconds) {
|
|
3132
|
+
// Rate-limit checks stop waiting after five seconds.
|
|
3133
|
+
// An unconfirmed increment is never retried.
|
|
3134
|
+
const controller = new AbortController();
|
|
3135
|
+
const timer = setTimeout(function () { controller.abort(); }, 5000);
|
|
3064
3136
|
try {
|
|
3065
3137
|
return await platformJSON('/v1/rate-limit/check', {
|
|
3066
3138
|
method: 'POST',
|
|
3139
|
+
signal: controller.signal,
|
|
3067
3140
|
body: JSON.stringify({
|
|
3068
3141
|
project_id: projectId,
|
|
3069
3142
|
key,
|
|
@@ -3086,7 +3159,7 @@ function buildPlatformContext(env, request, runtimeFetch = fetch, outboundFetch
|
|
|
3086
3159
|
};
|
|
3087
3160
|
}
|
|
3088
3161
|
throw e;
|
|
3089
|
-
}
|
|
3162
|
+
} finally { clearTimeout(timer); }
|
|
3090
3163
|
}
|
|
3091
3164
|
|
|
3092
3165
|
// Remember what the platform answered for this key's window. A refusal
|
|
@@ -4694,6 +4767,12 @@ var DB_STRUCTURED_EXECUTION_JS = ` async function __sw_structExec(label, ir
|
|
|
4694
4767
|
// Zero rows back means the EXISTS was false: the verified user does not
|
|
4695
4768
|
// belong to the group this row targets. Surface it as an explicit 403
|
|
4696
4769
|
// BEFORE any realtime publish \u2014 the row never landed.
|
|
4770
|
+
// A per-row share names a row its creator does not own (query-sql.ts grantRow guard).
|
|
4771
|
+
if (ir.kind === 'insert' && (ir.scope.mode === 'user' || ir.scope.mode === 'server') && ir.scope.grantRow && ir.onConflict === undefined && rows.length === 0) {
|
|
4772
|
+
__sw_structThrow403('POLICY_AUTHORITY_REQUIRED', ir.scope.mode === 'server'
|
|
4773
|
+
? 'sw.db.server.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row its user_id owns.'
|
|
4774
|
+
: 'sw.db.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row the signed-in user owns.');
|
|
4775
|
+
}
|
|
4697
4776
|
if (ir.kind === 'insert' && (ir.scope.mode === 'member' || ir.scope.mode === 'policy') && rows.length === 0) {
|
|
4698
4777
|
__sw_structThrow403(ir.scope.mode === 'policy' ? 'POLICY_AUTHORITY_REQUIRED' : 'MEMBERSHIP_REQUIRED',
|
|
4699
4778
|
ir.scope.mode === 'policy' ? 'sw.db.insert on "' + ir.table.name + '": the proposed row is outside your declared private policy.'
|
|
@@ -5118,6 +5197,7 @@ function renderPolicySql(request) {
|
|
|
5118
5197
|
return renderPolicySql({
|
|
5119
5198
|
render: "predicate",
|
|
5120
5199
|
principal: r.principal,
|
|
5200
|
+
emailDomain: r.emailDomain,
|
|
5121
5201
|
table: r.table,
|
|
5122
5202
|
physical: r.physical,
|
|
5123
5203
|
policy: { k: "m", m: r.member },
|
|
@@ -5128,10 +5208,11 @@ function renderPolicySql(request) {
|
|
|
5128
5208
|
}
|
|
5129
5209
|
if (r.render === "insert") {
|
|
5130
5210
|
if (r.policy && r.policy.k === "n") {
|
|
5131
|
-
renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: r.policy, operation: "read", qualifier: null, proposed: null });
|
|
5211
|
+
renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: r.policy, operation: "read", qualifier: null, proposed: null });
|
|
5132
5212
|
const gateInsert = renderPolicySql({
|
|
5133
5213
|
render: "predicate",
|
|
5134
5214
|
principal: r.principal,
|
|
5215
|
+
emailDomain: r.emailDomain,
|
|
5135
5216
|
table: r.table,
|
|
5136
5217
|
physical: r.physical,
|
|
5137
5218
|
policy: r.policy.g,
|
|
@@ -5139,7 +5220,7 @@ function renderPolicySql(request) {
|
|
|
5139
5220
|
qualifier: null,
|
|
5140
5221
|
proposed: r.proposed
|
|
5141
5222
|
});
|
|
5142
|
-
const whoInsert = r.policy.w.k === "o" ? { sql: "1", params: [] } : renderPolicySql({ render: "insert", principal: r.principal, table: r.table, physical: r.physical, policy: r.policy.w, proposed: r.proposed });
|
|
5223
|
+
const whoInsert = r.policy.w.k === "o" ? { sql: "1", params: [] } : renderPolicySql({ render: "insert", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: r.policy.w, proposed: r.proposed });
|
|
5143
5224
|
const insertParams = [];
|
|
5144
5225
|
for (let i = 0; i < gateInsert.params.length; i++) insertParams.push(gateInsert.params[i]);
|
|
5145
5226
|
for (let i = 0; i < whoInsert.params.length; i++) insertParams.push(whoInsert.params[i]);
|
|
@@ -5148,13 +5229,17 @@ function renderPolicySql(request) {
|
|
|
5148
5229
|
const anyOf = r.policy && r.policy.k === "a";
|
|
5149
5230
|
const branch = anyOf && Array.isArray(r.policy.p) ? r.policy.p[1] : null;
|
|
5150
5231
|
if (anyOf && (!branch || branch.k !== "m" && branch.k !== "g" && branch.k !== "r")) throw new Error("policy-sql: invalid anyOf policy");
|
|
5232
|
+
if (branch?.k === "m" && branch.m?.og) {
|
|
5233
|
+
renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: r.policy, operation: "read", qualifier: null, proposed: null });
|
|
5234
|
+
return { sql: "1", params: [] };
|
|
5235
|
+
}
|
|
5151
5236
|
if (branch && branch.k === "r") {
|
|
5152
5237
|
if (typeof r.principal !== "string" || r.principal.length === 0) throw new Error("policy-sql: invalid principal");
|
|
5153
|
-
renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
|
|
5238
|
+
renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
|
|
5154
5239
|
return { sql: "1", params: [] };
|
|
5155
5240
|
}
|
|
5156
5241
|
if (branch && branch.k === "g") {
|
|
5157
|
-
renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
|
|
5242
|
+
renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: branch, operation: "read", qualifier: null, proposed: null });
|
|
5158
5243
|
let found = 0;
|
|
5159
5244
|
let groupValue = null;
|
|
5160
5245
|
for (let j = 0; j < r.proposed.length; j++) {
|
|
@@ -5176,6 +5261,7 @@ function renderPolicySql(request) {
|
|
|
5176
5261
|
return renderPolicySql({
|
|
5177
5262
|
render: "predicate",
|
|
5178
5263
|
principal: r.principal,
|
|
5264
|
+
emailDomain: r.emailDomain,
|
|
5179
5265
|
table: r.table,
|
|
5180
5266
|
physical: r.physical,
|
|
5181
5267
|
policy: anyOf ? r.policy.p[1] : r.policy,
|
|
@@ -5210,6 +5296,7 @@ function renderPolicySql(request) {
|
|
|
5210
5296
|
return renderPolicySql({
|
|
5211
5297
|
render: "predicate",
|
|
5212
5298
|
principal: r.principal,
|
|
5299
|
+
emailDomain: r.emailDomain,
|
|
5213
5300
|
table: r.table,
|
|
5214
5301
|
physical: r.physical,
|
|
5215
5302
|
policy: r.policy,
|
|
@@ -5228,6 +5315,7 @@ function renderPolicySql(request) {
|
|
|
5228
5315
|
return renderPolicySql({
|
|
5229
5316
|
render: "predicate",
|
|
5230
5317
|
principal: r.principal,
|
|
5318
|
+
emailDomain: r.emailDomain,
|
|
5231
5319
|
table: r.table,
|
|
5232
5320
|
physical: r.physical,
|
|
5233
5321
|
policy: r.policy,
|
|
@@ -5236,6 +5324,34 @@ function renderPolicySql(request) {
|
|
|
5236
5324
|
proposed: r.proposed
|
|
5237
5325
|
});
|
|
5238
5326
|
}
|
|
5327
|
+
if (r.render === "membership") {
|
|
5328
|
+
const team = r.policy;
|
|
5329
|
+
if (!team || team.k !== "m" && team.k !== "g") throw new Error("policy-sql: membership is a member or group policy");
|
|
5330
|
+
if (r.principalRef !== void 0 && r.principalRef !== "link_maker") throw new Error("policy-sql: invalid principal reference");
|
|
5331
|
+
const byMaker = r.principalRef === "link_maker";
|
|
5332
|
+
const principal = byMaker ? "link_maker" : r.principal;
|
|
5333
|
+
const who = byMaker ? renderPolicySql({ render: "ident", name: "__sw_lg" }).sql + "." + renderPolicySql({ render: "ident", name: "created_by" }).sql : "?";
|
|
5334
|
+
const rowRef = r.qualifier ? renderPolicySql({ render: "ident", name: r.qualifier }).sql : null;
|
|
5335
|
+
let member;
|
|
5336
|
+
const keys = [];
|
|
5337
|
+
if (team.k === "m") {
|
|
5338
|
+
renderPolicySql({ render: "predicate", principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: team, operation: null, qualifier: r.qualifier, proposed: null, edge: r.edge });
|
|
5339
|
+
const groupsOfWho = [];
|
|
5340
|
+
for (let i = 0; i < team.m.mg.length; i++) groupsOfWho.push(renderPolicySql({ render: "ident", name: team.m.mg[i] }).sql);
|
|
5341
|
+
for (let i = 0; i < team.m.g.length; i++) keys.push((rowRef ? rowRef + "." : "") + renderPolicySql({ render: "ident", name: team.m.g[i] }).sql);
|
|
5342
|
+
member = (keys.length === 1 ? keys[0] : "(" + keys.join(", ") + ")") + " IN (SELECT " + groupsOfWho.join(", ") + " FROM " + renderPolicySql({ render: "table", name: team.m.m, physical: r.physical }).sql + " WHERE " + renderPolicySql({ render: "ident", name: team.m.u }).sql + " = " + who + ")";
|
|
5343
|
+
} else {
|
|
5344
|
+
renderPolicySql({ render: "predicate", principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: team, operation: "read", qualifier: r.qualifier, proposed: null, edge: r.edge });
|
|
5345
|
+
const members = renderPolicySql({ render: "ident", name: "__sw_gm" }).sql;
|
|
5346
|
+
const groups = renderPolicySql({ render: "ident", name: "__sw_g" }).sql;
|
|
5347
|
+
const rowGroup = (rowRef ? rowRef : renderPolicySql({ render: "table", name: r.table, physical: r.physical }).sql) + "." + renderPolicySql({ render: "ident", name: team.c }).sql;
|
|
5348
|
+
keys.push(rowGroup);
|
|
5349
|
+
member = "EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_group_members" }).sql + " AS " + members + " JOIN " + renderPolicySql({ render: "ident", name: "_sw_groups" }).sql + " AS " + groups + " ON " + groups + '."id" = ' + members + '."group_id" WHERE ' + members + '."group_id" = ' + rowGroup + " AND " + members + '."user_id" = ' + who + ")";
|
|
5350
|
+
}
|
|
5351
|
+
let sql = "(";
|
|
5352
|
+
for (let i = 0; i < keys.length; i++) sql += keys[i] + " IS NULL OR ";
|
|
5353
|
+
return { sql: sql + member + ")", params: byMaker ? [] : [r.principal] };
|
|
5354
|
+
}
|
|
5239
5355
|
if (r.render !== "predicate") throw new Error("policy-sql: unknown render kind");
|
|
5240
5356
|
const policy = r.policy;
|
|
5241
5357
|
const qualifier = r.qualifier;
|
|
@@ -5254,6 +5370,11 @@ function renderPolicySql(request) {
|
|
|
5254
5370
|
if (!m || !Array.isArray(m.g) || !Array.isArray(m.mg) || m.g.length === 0 || m.g.length !== m.mg.length) {
|
|
5255
5371
|
throw new Error("policy-sql: invalid member scope");
|
|
5256
5372
|
}
|
|
5373
|
+
if (m.og !== void 0) {
|
|
5374
|
+
const perRow = m.g.length === 2 && m.g[1] !== "user_id" && m.mg[1] !== "user_id" && m.mg[1] !== m.u;
|
|
5375
|
+
if (m.og !== true || m.g[0] !== "user_id" || m.mg[0] !== "user_id" || m.g.length !== 1 && !perRow || m.u === "user_id" || JSON.stringify(m.o) !== '["read"]' && JSON.stringify(m.o) !== '["read","update"]') throw new Error("policy-sql: invalid owner grant");
|
|
5376
|
+
if (proposed || !operation) return { sql: "0", params: [] };
|
|
5377
|
+
}
|
|
5257
5378
|
if (operation && m.o !== void 0) {
|
|
5258
5379
|
let allowed = false;
|
|
5259
5380
|
for (let i = 0; i < m.o.length; i++) if (m.o[i] === operation) allowed = true;
|
|
@@ -5303,8 +5424,16 @@ function renderPolicySql(request) {
|
|
|
5303
5424
|
if ((policy.p[1].k === "g" || policy.p[1].k === "r") && (proposed || !operation)) {
|
|
5304
5425
|
throw new Error("policy-sql: an anyOf group or app-role branch needs an operation on an existing row");
|
|
5305
5426
|
}
|
|
5306
|
-
|
|
5307
|
-
|
|
5427
|
+
let left = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p[0], operation: r.operation, qualifier, proposed, edge: r.edge });
|
|
5428
|
+
if (policy.p[1].k === "m" && policy.p[1].m.og && policy.p[0].c !== "user_id") throw new Error("policy-sql: grant requires managed owner");
|
|
5429
|
+
if (policy.p[1].k === "m" && !policy.p[1].m.og || policy.p[1].k === "g") {
|
|
5430
|
+
const team = proposed ? renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p[1], operation: null, qualifier, proposed, edge: r.edge }) : renderPolicySql({ render: "membership", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p[1], qualifier, edge: r.edge });
|
|
5431
|
+
const gated = [];
|
|
5432
|
+
for (let i = 0; i < left.params.length; i++) gated.push(left.params[i]);
|
|
5433
|
+
for (let i = 0; i < team.params.length; i++) gated.push(team.params[i]);
|
|
5434
|
+
left = { sql: "(" + left.sql + " AND " + team.sql + ")", params: gated };
|
|
5435
|
+
}
|
|
5436
|
+
const right = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p[1], operation: r.operation, qualifier, proposed, edge: r.edge });
|
|
5308
5437
|
for (let i = 0; i < left.params.length; i++) params.push(left.params[i]);
|
|
5309
5438
|
for (let i = 0; i < right.params.length; i++) params.push(right.params[i]);
|
|
5310
5439
|
return { sql: "(" + left.sql + " OR " + right.sql + ")", params };
|
|
@@ -5315,7 +5444,7 @@ function renderPolicySql(request) {
|
|
|
5315
5444
|
if (!gate || gate.k !== "m" && gate.k !== "g" && gate.k !== "r") {
|
|
5316
5445
|
throw new Error("policy-sql: an allOf gate is a member, group or app-role policy");
|
|
5317
5446
|
}
|
|
5318
|
-
if (gate.k === "m" && (!gate.m || gate.m.o === void 0)) throw new Error("policy-sql: allOf needs explicit operations");
|
|
5447
|
+
if (gate.k === "m" && (!gate.m || gate.m.o === void 0 || gate.m.og)) throw new Error("policy-sql: allOf needs explicit operations");
|
|
5319
5448
|
if (!who || who.k !== "o" && who.k !== "a") throw new Error("policy-sql: allOf who is owner() or anyOf(owner(), ...)");
|
|
5320
5449
|
if (who.k === "a") {
|
|
5321
5450
|
const x = Array.isArray(who.p) && who.p.length === 2 ? who.p[1] : null;
|
|
@@ -5325,7 +5454,7 @@ function renderPolicySql(request) {
|
|
|
5325
5454
|
compatible = String(x.c).toLowerCase() === String(gate.c).toLowerCase() && x.generation === gate.generation;
|
|
5326
5455
|
}
|
|
5327
5456
|
if (x.k === "m" && gate.k === "m") {
|
|
5328
|
-
if (!x.m || x.m.o === void 0) throw new Error("policy-sql: allOf needs explicit operations");
|
|
5457
|
+
if (!x.m || x.m.o === void 0 || x.m.og) throw new Error("policy-sql: allOf needs explicit operations");
|
|
5329
5458
|
const a = gate.m;
|
|
5330
5459
|
const b = x.m;
|
|
5331
5460
|
compatible = String(a.m).toLowerCase() === String(b.m).toLowerCase() && String(a.u).toLowerCase() === String(b.u).toLowerCase() && Array.isArray(a.g) && Array.isArray(b.g) && Array.isArray(a.mg) && Array.isArray(b.mg) && a.g.length === b.g.length && a.mg.length === b.mg.length;
|
|
@@ -5340,14 +5469,14 @@ function renderPolicySql(request) {
|
|
|
5340
5469
|
if (operation !== "read" && operation !== "create" && operation !== "update" && operation !== "delete") {
|
|
5341
5470
|
throw new Error("policy-sql: an allOf policy needs an operation");
|
|
5342
5471
|
}
|
|
5343
|
-
const gateSql = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: gate, operation, qualifier, proposed: null, edge: r.edge });
|
|
5344
|
-
const whoSql = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: who, operation, qualifier, proposed: null, edge: r.edge });
|
|
5472
|
+
const gateSql = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: gate, operation, qualifier, proposed: null, edge: r.edge });
|
|
5473
|
+
const whoSql = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: who, operation, qualifier, proposed: null, edge: r.edge });
|
|
5345
5474
|
for (let i = 0; i < gateSql.params.length; i++) params.push(gateSql.params[i]);
|
|
5346
5475
|
for (let i = 0; i < whoSql.params.length; i++) params.push(whoSql.params[i]);
|
|
5347
5476
|
let sql = "(" + gateSql.sql + " AND " + whoSql.sql + ")";
|
|
5348
5477
|
if (operation === "update" || operation === "delete") {
|
|
5349
|
-
const gateRead = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: gate, operation: "read", qualifier, proposed: null, edge: r.edge });
|
|
5350
|
-
const whoRead = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: who, operation: "read", qualifier, proposed: null, edge: r.edge });
|
|
5478
|
+
const gateRead = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: gate, operation: "read", qualifier, proposed: null, edge: r.edge });
|
|
5479
|
+
const whoRead = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: who, operation: "read", qualifier, proposed: null, edge: r.edge });
|
|
5351
5480
|
for (let i = 0; i < gateRead.params.length; i++) params.push(gateRead.params[i]);
|
|
5352
5481
|
for (let i = 0; i < whoRead.params.length; i++) params.push(whoRead.params[i]);
|
|
5353
5482
|
sql = "(" + sql + " AND (" + gateRead.sql + " AND " + whoRead.sql + "))";
|
|
@@ -5384,7 +5513,7 @@ function renderPolicySql(request) {
|
|
|
5384
5513
|
} else {
|
|
5385
5514
|
join = parentKey + " = " + (qualifier ? renderPolicySql({ render: "ident", name: qualifier }).sql : renderPolicySql({ render: "table", name: r.table, physical: r.physical }).sql) + "." + renderPolicySql({ render: "ident", name: policy.v }).sql;
|
|
5386
5515
|
}
|
|
5387
|
-
const inner = renderPolicySql({ render: "predicate", principal: r.principal, table: r.table, physical: r.physical, policy: policy.p, operation: r.operation, qualifier: alias, proposed: null, edge });
|
|
5516
|
+
const inner = renderPolicySql({ render: "predicate", principal: r.principal, emailDomain: r.emailDomain, table: r.table, physical: r.physical, policy: policy.p, operation: r.operation, qualifier: alias, proposed: null, edge });
|
|
5388
5517
|
for (let i = 0; i < inner.params.length; i++) params.push(inner.params[i]);
|
|
5389
5518
|
return {
|
|
5390
5519
|
sql: "EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "table", name: policy.t, physical: r.physical }).sql + " AS " + aliasSql + " WHERE " + join + " AND " + inner.sql + ")",
|
|
@@ -5484,8 +5613,35 @@ function renderPolicySql(request) {
|
|
|
5484
5613
|
marks.push("?");
|
|
5485
5614
|
params.push(allowed[i]);
|
|
5486
5615
|
}
|
|
5616
|
+
let held = "EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_app_role_grants" }).sql + " AS " + grants + " WHERE " + grants + '."user_id" = ? AND ' + grants + '."role" IN (' + marks.join(", ") + "))";
|
|
5617
|
+
const domain = r.emailDomain;
|
|
5618
|
+
if (domain !== null && domain !== void 0 && (typeof domain !== "string" || domain.length === 0 || domain.length > 253)) {
|
|
5619
|
+
throw new Error("policy-sql: invalid email domain");
|
|
5620
|
+
}
|
|
5621
|
+
const baked = policy.d;
|
|
5622
|
+
if (baked !== void 0) {
|
|
5623
|
+
if (!baked || typeof baked !== "object" || Array.isArray(baked)) throw new Error("policy-sql: invalid app-role domains");
|
|
5624
|
+
if (domain !== null && domain !== void 0) {
|
|
5625
|
+
for (let i = 0; i < allowed.length; i++) {
|
|
5626
|
+
if (Object.prototype.hasOwnProperty.call(baked, allowed[i]) && baked[allowed[i]] === domain) {
|
|
5627
|
+
held = "(" + held + " OR 1)";
|
|
5628
|
+
break;
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
} else if (domain !== null && domain !== void 0) {
|
|
5633
|
+
const vocabulary = renderPolicySql({ render: "ident", name: "__sw_rv" }).sql;
|
|
5634
|
+
const entry = renderPolicySql({ render: "ident", name: "__sw_re" }).sql;
|
|
5635
|
+
const domainMarks = [];
|
|
5636
|
+
for (let i = 0; i < allowed.length; i++) {
|
|
5637
|
+
domainMarks.push("?");
|
|
5638
|
+
params.push(allowed[i]);
|
|
5639
|
+
}
|
|
5640
|
+
params.push(domain);
|
|
5641
|
+
held = "(" + held + " OR EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_group_policy" }).sql + " AS " + vocabulary + ", json_each(" + vocabulary + `."vocabulary", '$.appRoles') AS ` + entry + " WHERE " + vocabulary + '."id" = 1 AND json_extract(' + entry + `."value", '$.name') IN (` + domainMarks.join(", ") + ") AND json_extract(" + entry + `."value", '$.emailDomain') = ?))`;
|
|
5642
|
+
}
|
|
5487
5643
|
return {
|
|
5488
|
-
sql: "(EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_group_policy" }).sql + " AS " + active + " WHERE " + active + '."id" = 1 AND ' + active + '."generation" = ?) AND
|
|
5644
|
+
sql: "(EXISTS (SELECT 1 FROM " + renderPolicySql({ render: "ident", name: "_sw_group_policy" }).sql + " AS " + active + " WHERE " + active + '."id" = 1 AND ' + active + '."generation" = ?) AND ' + held + ")",
|
|
5489
5645
|
params
|
|
5490
5646
|
};
|
|
5491
5647
|
}
|
|
@@ -5497,7 +5653,19 @@ function policyOwnerColumn(policy) {
|
|
|
5497
5653
|
if (policy.k === "n") return policyOwnerColumn(policy.w);
|
|
5498
5654
|
return null;
|
|
5499
5655
|
}
|
|
5500
|
-
|
|
5656
|
+
function policyWithoutGrants(policy) {
|
|
5657
|
+
if (policy.k === "a") {
|
|
5658
|
+
const second = policy.p[1];
|
|
5659
|
+
if (second.k === "m" && second.m.og) return policy.p[0];
|
|
5660
|
+
return policy;
|
|
5661
|
+
}
|
|
5662
|
+
if (policy.k === "p") {
|
|
5663
|
+
const inner = policyWithoutGrants(policy.p);
|
|
5664
|
+
return inner === policy.p ? policy : { ...policy, p: inner };
|
|
5665
|
+
}
|
|
5666
|
+
return policy;
|
|
5667
|
+
}
|
|
5668
|
+
var POLICY_SQL_JS = " const __sw_renderPolicySql = (" + renderPolicySql.toString() + ");\n const __sw_policyOwnerColumn = (" + policyOwnerColumn.toString() + ");\n const __sw_policyWithoutGrants = (" + policyWithoutGrants.toString() + ");\n";
|
|
5501
5669
|
|
|
5502
5670
|
// worker/src/runtime/query-sql.ts
|
|
5503
5671
|
function renderQuerySql(ir, principalId, returning, context) {
|
|
@@ -5514,6 +5682,26 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
5514
5682
|
if (ir.kind === "insert" || ir.kind === "update") {
|
|
5515
5683
|
context.assertUniqueWriteColumns((ir.values || []).concat(ir.increments || []), "sw.db." + ir.kind);
|
|
5516
5684
|
}
|
|
5685
|
+
if (ir.kind === "update" && ir.scope) {
|
|
5686
|
+
const fixed = [];
|
|
5687
|
+
if ((ir.scope.mode === "user" || ir.scope.mode === "server") && ir.scope.grantRow) fixed.push(ir.scope.grantRow.c);
|
|
5688
|
+
if (ir.scope.mode === "server" && ir.scope.grantRow) fixed.push("user_id");
|
|
5689
|
+
const grantBranch = ir.scope.mode === "policy" && ir.scope.policy.k === "a" ? ir.scope.policy.p[1] : null;
|
|
5690
|
+
if (grantBranch && grantBranch.k === "m" && grantBranch.m.og && grantBranch.m.g.length === 2) fixed.push(grantBranch.m.g[1]);
|
|
5691
|
+
const written = (ir.values || []).concat(ir.increments || []);
|
|
5692
|
+
for (let i = 0; i < written.length; i++) {
|
|
5693
|
+
for (let j = 0; j < fixed.length; j++) {
|
|
5694
|
+
if (context.lower(written[i].column) !== context.lower(fixed[j])) continue;
|
|
5695
|
+
let pinned = false;
|
|
5696
|
+
const value = i < (ir.values || []).length ? written[i].value : void 0;
|
|
5697
|
+
for (let k = 0; k < ir.where.length; k++) {
|
|
5698
|
+
const w = ir.where[k];
|
|
5699
|
+
if (value !== void 0 && w.op === "eq" && typeof w.column === "string" && context.lower(w.column) === context.lower(fixed[j]) && w.value === value) pinned = true;
|
|
5700
|
+
}
|
|
5701
|
+
if (!pinned) context.fail('sw.db.update: "' + written[i].column + '" ties a share to one row and cannot change. Delete the share and create another.');
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5517
5705
|
const params = [];
|
|
5518
5706
|
const helpers = {
|
|
5519
5707
|
table(name) {
|
|
@@ -5639,7 +5827,26 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
5639
5827
|
else context.fail("sw.db: a linked row key must be a whole number or text.");
|
|
5640
5828
|
} else key = "CAST(" + rowRef + " AS TEXT)";
|
|
5641
5829
|
params.push(link.digest, link.epoch);
|
|
5642
|
-
|
|
5830
|
+
let team = "";
|
|
5831
|
+
if (link.team) {
|
|
5832
|
+
if (!link.team.key || typeof link.team.key !== "string" || !link.team.policy) context.fail("sw.db: invalid link authority.");
|
|
5833
|
+
const lr = q("__sw_lr");
|
|
5834
|
+
let rootKey = "?";
|
|
5835
|
+
if (rowRef === null) params.push(rowValue);
|
|
5836
|
+
else rootKey = rowRef;
|
|
5837
|
+
const gate = context.renderPolicy({
|
|
5838
|
+
render: "membership",
|
|
5839
|
+
principal: null,
|
|
5840
|
+
principalRef: "link_maker",
|
|
5841
|
+
table: link.root,
|
|
5842
|
+
physical: context.physicalMap(),
|
|
5843
|
+
policy: link.team.policy,
|
|
5844
|
+
qualifier: "__sw_lr"
|
|
5845
|
+
});
|
|
5846
|
+
for (let i = 0; i < gate.params.length; i++) params.push(gate.params[i]);
|
|
5847
|
+
team = " AND EXISTS (SELECT 1 FROM " + context.renderPolicy({ render: "table", name: link.root, physical: context.physicalMap() }).sql + " AS " + lr + " WHERE " + lr + "." + q(link.team.key) + " = " + rootKey + " AND " + gate.sql + ")";
|
|
5848
|
+
}
|
|
5849
|
+
return "EXISTS (SELECT 1 FROM " + q("_sw_row_link_sessions") + " AS " + ls + " JOIN " + q("_sw_row_links") + " AS " + lg + " ON " + lg + "." + q("id") + " = " + ls + "." + q("grant_id") + " JOIN " + q("_sw_row_link_epochs") + " AS " + le + " ON " + le + "." + q("table_name") + " = " + lg + "." + q("table_name") + " WHERE " + ls + "." + q("token_digest") + " = ? AND " + ls + "." + q("expires_at") + " > unixepoch() AND " + lg + "." + q("id") + " = ? AND " + lg + "." + q("table_name") + " = ? AND " + lg + "." + q("row_key") + " = " + key + " AND " + lg + "." + q("revoked_at") + " IS NULL AND (" + lg + "." + q("expires_at") + " IS NULL OR " + lg + "." + q("expires_at") + " > unixepoch()) AND " + lg + "." + q("access") + (link.write ? " = 'edit'" : " IN ('read', 'edit')") + " AND " + le + "." + q("digest") + " = ? AND " + le + "." + q("epoch") + " = ? AND " + lg + "." + q("minted_epoch") + " = " + le + "." + q("epoch") + team + ")";
|
|
5643
5850
|
},
|
|
5644
5851
|
// Creator session (creator-authority.ts): ONE correlated EXISTS proving, in the statement, that this
|
|
5645
5852
|
// creator session token is live for this exact unended creator grant, which holds THIS root row's
|
|
@@ -5708,8 +5915,21 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
5708
5915
|
frags.push(privateBranch);
|
|
5709
5916
|
}
|
|
5710
5917
|
if (ir.kind === "update" && ir.scope.mode === "policy") {
|
|
5711
|
-
const
|
|
5712
|
-
|
|
5918
|
+
const policy = ir.scope.policy;
|
|
5919
|
+
const destination = context.renderPolicy({ render: "update_destination", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy, proposed: ir.values || [] });
|
|
5920
|
+
const ownerOnly = destination.sql && policy.k === "p" ? context.withoutGrants(policy) : policy;
|
|
5921
|
+
if (destination.sql && ownerOnly !== policy) {
|
|
5922
|
+
const access = [];
|
|
5923
|
+
for (let i = 0; i < ir.where.length; i++)
|
|
5924
|
+
access.push(helpers.renderPredicate(ir.where[i]));
|
|
5925
|
+
access.push(helpers.privateScopePredicate(ir.scope) || "1");
|
|
5926
|
+
access.push(helpers.policySql(destination));
|
|
5927
|
+
const owned = [
|
|
5928
|
+
helpers.policyPredicate(ownerOnly, null, null, "update"),
|
|
5929
|
+
helpers.policySql(context.renderPolicy({ render: "update_destination", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy: ownerOnly, proposed: ir.values || [] }))
|
|
5930
|
+
];
|
|
5931
|
+
frags.push("(CASE WHEN NOT (" + access.join(" AND ") + ") THEN 0 WHEN (" + owned.join(" AND ") + ") THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END)");
|
|
5932
|
+
} else if (destination.sql)
|
|
5713
5933
|
frags.push(helpers.policySql(destination));
|
|
5714
5934
|
}
|
|
5715
5935
|
for (let i = 0; i < (ir.increments || []).length; i++) {
|
|
@@ -5908,6 +6128,35 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
5908
6128
|
sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + context.returningSql(returning);
|
|
5909
6129
|
return { sql, params };
|
|
5910
6130
|
}
|
|
6131
|
+
if ((ir.scope.mode === "user" || ir.scope.mode === "server") && ir.scope.grantRow) {
|
|
6132
|
+
const grantRow = ir.scope.grantRow;
|
|
6133
|
+
let named = 0;
|
|
6134
|
+
let rowValue = null;
|
|
6135
|
+
let owners = 0;
|
|
6136
|
+
let shareOwner = ir.scope.mode === "user" ? principalId : null;
|
|
6137
|
+
for (let i = 0; i < ir.values.length; i++) {
|
|
6138
|
+
if (context.lower(ir.values[i].column) === context.lower(grantRow.c)) {
|
|
6139
|
+
named++;
|
|
6140
|
+
rowValue = ir.values[i].value;
|
|
6141
|
+
}
|
|
6142
|
+
if (ir.scope.mode === "server" && context.lower(ir.values[i].column) === "user_id") {
|
|
6143
|
+
owners++;
|
|
6144
|
+
shareOwner = ir.values[i].value;
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
if (named !== 1 || rowValue === null) context.fail('sw.db.insert: a share names the "' + grantRow.t + '" row it grants in "' + grantRow.c + '".');
|
|
6148
|
+
if (ir.scope.mode === "server" && (owners !== 1 || shareOwner === null)) context.fail('sw.db.server.insert: a share names its owner in "user_id"; the owner must own the "' + grantRow.t + '" row it grants.');
|
|
6149
|
+
if (ir.onConflict === "update") context.fail('sw.db.insert: a share cannot be updated on conflict; use onConflict: "ignore".');
|
|
6150
|
+
for (let i = 0; i < ivals.length; i++)
|
|
6151
|
+
params.push(ivals[i]);
|
|
6152
|
+
const owned = "EXISTS (SELECT 1 FROM " + helpers.table(grantRow.t) + " WHERE " + q(grantRow.k) + " = ? AND " + q("user_id") + " = ?)";
|
|
6153
|
+
params.push(rowValue, shareOwner);
|
|
6154
|
+
const guard = ir.txGuard || ir.onConflict !== void 0 ? "CASE WHEN " + owned + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END" : owned;
|
|
6155
|
+
sql = "INSERT INTO " + tableSql + " (" + allCols.map(q).join(", ") + ") SELECT " + allCols.map(function() {
|
|
6156
|
+
return "?";
|
|
6157
|
+
}).join(", ") + " WHERE " + guard + (ir.onConflict === "ignore" ? " ON CONFLICT DO NOTHING" : "") + context.returningSql(returning);
|
|
6158
|
+
return { sql, params };
|
|
6159
|
+
}
|
|
5911
6160
|
for (let i = 0; i < ivals.length; i++)
|
|
5912
6161
|
params.push(ivals[i]);
|
|
5913
6162
|
const visitorGuard = ir.scope.mode === "user" ? helpers.visitorPredicate() : null;
|
|
@@ -6035,6 +6284,7 @@ var DB_RENDER_IR_JS = `${POLICY_SQL_JS}${QUERY_SQL_JS} // One write names e
|
|
|
6035
6284
|
},
|
|
6036
6285
|
renderPolicy: __sw_renderPolicySql,
|
|
6037
6286
|
ownerColumn: __sw_policyOwnerColumn,
|
|
6287
|
+
withoutGrants: __sw_policyWithoutGrants,
|
|
6038
6288
|
returningSql: __sw_returningSql,
|
|
6039
6289
|
assertUniqueWriteColumns: __sw_structAssertUniqueWriteColumns,
|
|
6040
6290
|
fail: __sw_structThrow,
|
|
@@ -6631,13 +6881,32 @@ var DB_RELATIONS_JS = ` // \u2500\u2500 Composed joins (tsk_1186c9a) \u2500
|
|
|
6631
6881
|
}
|
|
6632
6882
|
`;
|
|
6633
6883
|
|
|
6884
|
+
// worker/src/utils/authority/role-domain-map.ts
|
|
6885
|
+
var ROLE_NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
6886
|
+
var EMAIL_DOMAIN_PATTERN = /^(?=.{4,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/;
|
|
6887
|
+
var ROLE_DOMAIN_MAP_JS = `
|
|
6888
|
+
function __sw_structRoleDomains(value) {
|
|
6889
|
+
if (!value || typeof value !== 'object' || __sw_arrayIsArray(value)) return false;
|
|
6890
|
+
const roles = __sw_keys(value);
|
|
6891
|
+
for (let i = 0; i < roles.length; i++) {
|
|
6892
|
+
const domain = value[roles[i]];
|
|
6893
|
+
if (!__sw_reMatch(${ROLE_NAME_PATTERN}, roles[i]) || typeof domain !== 'string' || !__sw_reMatch(${EMAIL_DOMAIN_PATTERN}, domain)) return false;
|
|
6894
|
+
}
|
|
6895
|
+
return true;
|
|
6896
|
+
}`;
|
|
6897
|
+
|
|
6634
6898
|
// worker/src/runtime/db-policy.ts
|
|
6635
|
-
var DB_POLICY_HELPERS_JS =
|
|
6899
|
+
var DB_POLICY_HELPERS_JS = `${ROLE_DOMAIN_MAP_JS}
|
|
6636
6900
|
function __sw_structPolicyIdent(value) { return typeof value === 'string' && value && __sw_reMatch(__SW_STRUCT_IDENT_RE, value); }
|
|
6637
6901
|
function __sw_structPolicyMember(value) {
|
|
6638
6902
|
if (!value || typeof value !== 'object') return false;
|
|
6639
6903
|
const keys = __sw_keys(value).slice().sort().join(',');
|
|
6640
|
-
|
|
6904
|
+
// Per-owner grant: g/mg ["user_id"]. Per-row: g ["user_id", key], mg ["user_id", row].
|
|
6905
|
+
const grant = keys === 'g,m,mg,o,og,u' && value.og === true && __sw_arrayIsArray(value.g) && __sw_arrayIsArray(value.mg)
|
|
6906
|
+
&& value.g[0] === 'user_id' && value.mg[0] === 'user_id'
|
|
6907
|
+
&& (value.g.length === 1 || value.g.length === 2 && value.g[1] !== 'user_id' && value.mg[1] !== 'user_id' && value.mg[1] !== value.u)
|
|
6908
|
+
&& value.u !== 'user_id' && (JSON.stringify(value.o) === '["read"]' || JSON.stringify(value.o) === '["read","update"]');
|
|
6909
|
+
if (keys !== 'g,m,mg,u' && keys !== 'g,m,mg,o,u' && !grant) return false;
|
|
6641
6910
|
if (!__sw_arrayIsArray(value.g) || !__sw_arrayIsArray(value.mg) || value.g.length === 0 || value.g.length !== value.mg.length || !__sw_structPolicyIdent(value.m) || !__sw_structPolicyIdent(value.u)) return false;
|
|
6642
6911
|
for (let i = 0; i < value.g.length; i++) if (!__sw_structPolicyIdent(value.g[i]) || !__sw_structPolicyIdent(value.mg[i])) return false;
|
|
6643
6912
|
if (value.o !== undefined) {
|
|
@@ -6672,9 +6941,12 @@ var DB_POLICY_HELPERS_JS = `
|
|
|
6672
6941
|
return true;
|
|
6673
6942
|
}
|
|
6674
6943
|
// An app-role leaf, pinned to the release's policy generation. Each
|
|
6675
|
-
// present operation lists declared app roles; a missing one denies.
|
|
6944
|
+
// present operation lists declared app roles; a missing one denies. The
|
|
6945
|
+
// release's role -> verified email domain map (d) rides it when baked.
|
|
6676
6946
|
function __sw_structPolicyRole(value) {
|
|
6677
|
-
if (!value || typeof value !== 'object' || value.k !== 'r'
|
|
6947
|
+
if (!value || typeof value !== 'object' || value.k !== 'r') return false;
|
|
6948
|
+
const leafKeys = __sw_keys(value).slice().sort().join(',');
|
|
6949
|
+
if (leafKeys !== 'generation,k,roles' && (leafKeys !== 'd,generation,k,roles' || !__sw_structRoleDomains(value.d))) return false;
|
|
6678
6950
|
if (typeof value.generation !== 'number' || !Number.isSafeInteger(value.generation) || value.generation < 1) return false;
|
|
6679
6951
|
const roles = value.roles;
|
|
6680
6952
|
if (!roles || typeof roles !== 'object' || __sw_arrayIsArray(roles)) return false;
|
|
@@ -6694,6 +6966,7 @@ var DB_POLICY_HELPERS_JS = `
|
|
|
6694
6966
|
if (!value || typeof value !== 'object' || __sw_keys(value).slice().sort().join(',') !== 'k,p' || value.k !== 'a'
|
|
6695
6967
|
|| !__sw_arrayIsArray(value.p) || value.p.length !== 2 || !__sw_structPolicyLeaf(value.p[0]) || value.p[0].k !== 'o') return false;
|
|
6696
6968
|
const second = value.p[1];
|
|
6969
|
+
if (second && second.k === 'm' && second.m && second.m.og && value.p[0].c !== 'user_id') return false;
|
|
6697
6970
|
return (__sw_structPolicyLeaf(second) && second.k === 'm') || __sw_structPolicyGroup(second) || __sw_structPolicyRole(second);
|
|
6698
6971
|
}
|
|
6699
6972
|
// allOf(gate, who) (AND permissions v1.1): gate is a member() leaf with
|
|
@@ -6705,7 +6978,7 @@ var DB_POLICY_HELPERS_JS = `
|
|
|
6705
6978
|
if (!value || typeof value !== 'object' || __sw_keys(value).slice().sort().join(',') !== 'g,k,w' || value.k !== 'n') return false;
|
|
6706
6979
|
const gate = value.g;
|
|
6707
6980
|
const who = value.w;
|
|
6708
|
-
const gateOk = (__sw_structPolicyLeaf(gate) && gate.k === 'm' && gate.m.o !== undefined) || __sw_structPolicyGroup(gate) || __sw_structPolicyRole(gate);
|
|
6981
|
+
const gateOk = (__sw_structPolicyLeaf(gate) && gate.k === 'm' && gate.m.o !== undefined && !gate.m.og) || __sw_structPolicyGroup(gate) || __sw_structPolicyRole(gate);
|
|
6709
6982
|
if (!gateOk) return false;
|
|
6710
6983
|
if (__sw_structPolicyLeaf(who) && who.k === 'o') return true;
|
|
6711
6984
|
if (!__sw_structPolicyAny(who)) return false;
|
|
@@ -6713,6 +6986,7 @@ var DB_POLICY_HELPERS_JS = `
|
|
|
6713
6986
|
if (x.k === 'r') return true;
|
|
6714
6987
|
if (x.k === 'g') return gate.k === 'g' && __sw_lower(x.c) === __sw_lower(gate.c) && x.generation === gate.generation;
|
|
6715
6988
|
if (x.k !== 'm' || gate.k !== 'm' || x.m.o === undefined) return false;
|
|
6989
|
+
if (x.m.og || gate.m.og) return false;
|
|
6716
6990
|
if (__sw_lower(x.m.m) !== __sw_lower(gate.m.m) || __sw_lower(x.m.u) !== __sw_lower(gate.m.u)
|
|
6717
6991
|
|| x.m.g.length !== gate.m.g.length || x.m.mg.length !== gate.m.mg.length) return false;
|
|
6718
6992
|
for (let i = 0; i < x.m.g.length; i++) if (__sw_lower(x.m.g[i]) !== __sw_lower(gate.m.g[i])) return false;
|
|
@@ -6733,6 +7007,23 @@ var DB_POLICY_HELPERS_JS = `
|
|
|
6733
7007
|
if (__sw_structResolveIntent(table) !== 'policy' || !shape || !__sw_own(shape, 'policy') || !__sw_structPolicyValid(shape.policy)) return null;
|
|
6734
7008
|
return shape.policy;
|
|
6735
7009
|
}
|
|
7010
|
+
// A per-row grant table (grant({ row })) and the one rule it serves: its shares name rows of t (key k) in c.
|
|
7011
|
+
let __sw_structGrantRows;
|
|
7012
|
+
function __sw_structGrantRowFor(table) {
|
|
7013
|
+
if (!__sw_structGrantRows) {
|
|
7014
|
+
__sw_structGrantRows = {};
|
|
7015
|
+
const names = __sw_keys(SCHEMA);
|
|
7016
|
+
for (let i = 0; i < names.length; i++) {
|
|
7017
|
+
const policy = __sw_structResolvePolicy(names[i]);
|
|
7018
|
+
const branch = policy && policy.k === 'a' ? policy.p[1] : null;
|
|
7019
|
+
if (branch && branch.k === 'm' && branch.m.og === true && branch.m.g.length === 2) {
|
|
7020
|
+
__sw_structGrantRows[__sw_lower(branch.m.m)] = { t: __sw_lower(names[i]), k: branch.m.g[1], c: branch.m.mg[1] };
|
|
7021
|
+
}
|
|
7022
|
+
}
|
|
7023
|
+
}
|
|
7024
|
+
const lc = __sw_lower(table);
|
|
7025
|
+
return __sw_own(__sw_structGrantRows, lc) ? __sw_structGrantRows[lc] : undefined;
|
|
7026
|
+
}
|
|
6736
7027
|
function __sw_structPolicyHasOwner(policy) {
|
|
6737
7028
|
return policy.k === 'o' || policy.k === 'a' && (__sw_structPolicyHasOwner(policy.p[0]) || __sw_structPolicyHasOwner(policy.p[1]))
|
|
6738
7029
|
|| policy.k === 'n' && __sw_structPolicyHasOwner(policy.w);
|
|
@@ -6776,6 +7067,9 @@ var DB_POLICY_HELPERS_JS = `
|
|
|
6776
7067
|
}
|
|
6777
7068
|
return;
|
|
6778
7069
|
}
|
|
7070
|
+
// A grant() branch correlates the managed owner (checked above) and, per row, the row's key,
|
|
7071
|
+
// which the shared renderer keeps fixed (query-sql.ts), as the managed compiler does.
|
|
7072
|
+
if (policy.p[1].k === 'm' && policy.p[1].m.og) return;
|
|
6779
7073
|
__sw_structAssertPolicyWrite(cols, policy.p[1], verb, increments);
|
|
6780
7074
|
return;
|
|
6781
7075
|
}
|
|
@@ -6822,7 +7116,7 @@ var DB_POLICY_HELPERS_JS = `
|
|
|
6822
7116
|
if (policy.k === 'a') {
|
|
6823
7117
|
// The owner is stamped; an app-role branch has no placement, and a
|
|
6824
7118
|
// group branch must name the group the row belongs to.
|
|
6825
|
-
if (policy.p[1].k === 'r') return;
|
|
7119
|
+
if (policy.p[1].k === 'r' || (policy.p[1].k === 'm' && policy.p[1].m.og)) return;
|
|
6826
7120
|
if (policy.p[1].k === 'g') {
|
|
6827
7121
|
for (let i = 0; i < cols.length; i++) if (__sw_lower(cols[i].column) === __sw_lower(policy.p[1].c) && typeof cols[i].value === 'string' && cols[i].value) return;
|
|
6828
7122
|
__sw_structThrow403('POLICY_GROUP_REQUIRED', 'A group-scoped insert must name the group the row belongs to in "' + policy.p[1].c + '".');
|
|
@@ -8625,7 +8919,8 @@ ${DB_POLICY_RESOLVE_SCOPE_JS} const member = __sw_structResolveMember(tab
|
|
|
8625
8919
|
if (asServer === true || generatedRead) {
|
|
8626
8920
|
// Explicit server code or the private generated selector has no local
|
|
8627
8921
|
// principal. The managed API independently verifies the latter's token.
|
|
8628
|
-
|
|
8922
|
+
const serverGrantRow = generatedRead ? undefined : __sw_structGrantRowFor(table);
|
|
8923
|
+
return { scope: generatedRead ? { mode: 'user', ownerColumn: ownerColumn } : serverGrantRow ? { mode: 'server', grantRow: serverGrantRow } : { mode: 'server' }, principalId: undefined };
|
|
8629
8924
|
}
|
|
8630
8925
|
const identity = await __sw_structRequestIdentity(allowPublicGuest);
|
|
8631
8926
|
let id = identity && identity.id;
|
|
@@ -8660,7 +8955,8 @@ ${DB_POLICY_RESOLVE_SCOPE_JS} const member = __sw_structResolveMember(tab
|
|
|
8660
8955
|
err.__sw_expose = true;
|
|
8661
8956
|
throw err;
|
|
8662
8957
|
}
|
|
8663
|
-
|
|
8958
|
+
const grantRow = __sw_structGrantRowFor(table);
|
|
8959
|
+
return { scope: grantRow ? { mode: 'user', ownerColumn: ownerColumn, grantRow: grantRow } : { mode: 'user', ownerColumn: ownerColumn }, principalId: id };
|
|
8664
8960
|
}
|
|
8665
8961
|
|
|
8666
8962
|
// The scope argument is GONE (tsk_5b91e0e4): scoping is automatic and
|
|
@@ -9601,14 +9897,17 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
|
|
|
9601
9897
|
});
|
|
9602
9898
|
return send(prepared);
|
|
9603
9899
|
}
|
|
9604
|
-
// Standalone exact-key caller
|
|
9605
|
-
// missing-row result.
|
|
9606
|
-
//
|
|
9607
|
-
|
|
9900
|
+
// Standalone exact-key caller UPDATES share the browser's non-disclosing
|
|
9901
|
+
// missing-row result. A caller REMOVE that matches nothing returns the
|
|
9902
|
+
// zero-change result instead, like bulk/conditional writes (founder
|
|
9903
|
+
// decision, tsk_b7f42808): a missing row and a row outside the caller's
|
|
9904
|
+
// access give the same { data: [], count: 0, changes: 0 }, so neither
|
|
9905
|
+
// reveals existence. No existence probe bypasses authorization.
|
|
9906
|
+
async function __sw_structExecCallerWrite(prepared, authority, missThrows) {
|
|
9608
9907
|
const ir = prepared.ir;
|
|
9609
9908
|
const shape = __sw_structManagedShape(ir.table.name);
|
|
9610
9909
|
const pk = shape && (shape.primaryKey !== undefined ? shape.primaryKey : shape.clientPrimaryKey);
|
|
9611
|
-
const keyed = authority !== __sw_structServerNamespace && ir.scope.mode !== 'server'
|
|
9910
|
+
const keyed = missThrows && authority !== __sw_structServerNamespace && ir.scope.mode !== 'server'
|
|
9612
9911
|
&& !(ir.scope.mode === 'unscoped' && prepared.principalId === undefined && __sw_structResolveIntent(ir.table.name) === 'server_only')
|
|
9613
9912
|
&& typeof pk === 'string' && ir.where.length === 1
|
|
9614
9913
|
&& ir.where[0].op === 'eq' && __sw_lower(ir.where[0].column) === __sw_lower(pk);
|
|
@@ -9618,11 +9917,11 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
|
|
|
9618
9917
|
}
|
|
9619
9918
|
async function __sw_structUpdate(table, spec, authority) {
|
|
9620
9919
|
const prepared = await __sw_structPrepareUpdate(table, spec, authority);
|
|
9621
|
-
return __sw_structExecCallerWrite(prepared, authority);
|
|
9920
|
+
return __sw_structExecCallerWrite(prepared, authority, true);
|
|
9622
9921
|
}
|
|
9623
9922
|
async function __sw_structRemove(table, spec, authority) {
|
|
9624
9923
|
const prepared = await __sw_structPrepareRemove(table, spec, authority);
|
|
9625
|
-
return __sw_structExecCallerWrite(prepared, authority);
|
|
9924
|
+
return __sw_structExecCallerWrite(prepared, authority, false);
|
|
9626
9925
|
}
|
|
9627
9926
|
|
|
9628
9927
|
async function __sw_structTransaction(intents, authority) {
|
|
@@ -9647,7 +9946,8 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
|
|
|
9647
9946
|
// back (checked in the same transaction, after earlier intents).
|
|
9648
9947
|
// Updates and removes are already limited to rows the caller may
|
|
9649
9948
|
// reach by their WHERE predicate.
|
|
9650
|
-
if (item.ir.kind === 'insert' && (item.ir.scope.mode === 'member' || item.ir.scope.mode === 'policy'
|
|
9949
|
+
if (item.ir.kind === 'insert' && (item.ir.scope.mode === 'member' || item.ir.scope.mode === 'policy'
|
|
9950
|
+
|| (item.ir.scope.mode === 'user' || item.ir.scope.mode === 'server') && item.ir.scope.grantRow)) item.ir.txGuard = true;
|
|
9651
9951
|
// A transaction is never keyed: an explicit key on one of its inserts refuses before anything runs.
|
|
9652
9952
|
if (item.idempotencyKey !== undefined) {
|
|
9653
9953
|
throw __sw_keyedError('IDEMPOTENCY_UNSUPPORTED', 'sw.db.tx: an insert inside a transaction cannot take idempotencyKey. Nothing ran.', 400);
|
|
@@ -13593,16 +13893,14 @@ var AUTH_JS = ` auth: (function () {
|
|
|
13593
13893
|
return invite;
|
|
13594
13894
|
},
|
|
13595
13895
|
|
|
13596
|
-
//
|
|
13597
|
-
//
|
|
13598
|
-
//
|
|
13896
|
+
// Email sign-in codes (tsk_8fe91eb5). The app owner sets the policy ('off', 'optional', 'required') with a
|
|
13897
|
+
// developer key. A user with the factor answers every password sign-in with { mfa_required: true,
|
|
13898
|
+
// mfa_token, methods: ['email'] } and no session; challenge() (or challengeWithCookie) completes it with the
|
|
13899
|
+
// code. The explicit forms send the user's JWT through userTokenJSON (project-bound); platformJSON would
|
|
13900
|
+
// replace it with the developer key (pfb_ab7215b47cb9).
|
|
13599
13901
|
mfa: {
|
|
13600
|
-
//
|
|
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.
|
|
13902
|
+
// Turn codes on (policy 'optional' or 'required'): mails a code to the verified address.
|
|
13903
|
+
// Resolves { challenge_id, delivery, expires_in }, or { enrolled: true } when already on.
|
|
13606
13904
|
enroll(opts) {
|
|
13607
13905
|
opts = opts || {};
|
|
13608
13906
|
if (!opts.token) {
|
|
@@ -13612,9 +13910,10 @@ var AUTH_JS = ` auth: (function () {
|
|
|
13612
13910
|
}
|
|
13613
13911
|
return userTokenJSON('/v1/auth/mfa/enroll', opts.token, {
|
|
13614
13912
|
method: 'POST',
|
|
13615
|
-
body: JSON.stringify({}),
|
|
13913
|
+
body: JSON.stringify({ method: 'email' }),
|
|
13616
13914
|
});
|
|
13617
13915
|
},
|
|
13916
|
+
// Authenticator-app activation is not available: answers 501 AUTH_FEATURE_UNAVAILABLE.
|
|
13618
13917
|
// Establish recent authority for the exact enrollment. Password
|
|
13619
13918
|
// success returns activation_token directly. Email sends a code and
|
|
13620
13919
|
// returns challenge_id for verifyReauthentication().
|
|
@@ -13650,29 +13949,22 @@ var AUTH_JS = ` auth: (function () {
|
|
|
13650
13949
|
}),
|
|
13651
13950
|
});
|
|
13652
13951
|
},
|
|
13653
|
-
// Confirm enrolment
|
|
13654
|
-
// authenticator generates plus the one-time activation authority.
|
|
13655
|
-
// Returns { enabled:true, backup_codes:[...] } once.
|
|
13952
|
+
// Confirm enrolment with the code enroll() mailed. Resolves { enrolled: true, method: 'email' }.
|
|
13656
13953
|
verify(opts) {
|
|
13657
13954
|
opts = opts || {};
|
|
13658
|
-
if (!opts.token || !opts.
|
|
13659
|
-
const err = new Error('sw.auth.mfa.verify: token,
|
|
13955
|
+
if (!opts.token || !opts.challenge_id || !opts.code) {
|
|
13956
|
+
const err = new Error('sw.auth.mfa.verify: token, challenge_id, and code are required');
|
|
13660
13957
|
err.code = 'VALIDATION_ERROR';
|
|
13661
13958
|
throw err;
|
|
13662
13959
|
}
|
|
13663
13960
|
return userTokenJSON('/v1/auth/mfa/verify', opts.token, {
|
|
13664
13961
|
method: 'POST',
|
|
13665
|
-
body: JSON.stringify({
|
|
13666
|
-
enrollment_id: opts.enrollment_id,
|
|
13667
|
-
activation_token: opts.activation_token,
|
|
13668
|
-
code: opts.code,
|
|
13669
|
-
}),
|
|
13962
|
+
body: JSON.stringify({ challenge_id: opts.challenge_id, code: opts.code }),
|
|
13670
13963
|
});
|
|
13671
13964
|
},
|
|
13672
13965
|
// Second-factor exchange after /login returned
|
|
13673
|
-
// { mfa_required:true, mfa_token }
|
|
13674
|
-
//
|
|
13675
|
-
// /login on success.
|
|
13966
|
+
// { mfa_required:true, mfa_token }: the 6-digit code from the email.
|
|
13967
|
+
// Returns the same shape as /login on success.
|
|
13676
13968
|
challenge(opts) {
|
|
13677
13969
|
opts = opts || {};
|
|
13678
13970
|
if (!opts.mfa_token || !opts.code) {
|
|
@@ -13689,6 +13981,20 @@ var AUTH_JS = ` auth: (function () {
|
|
|
13689
13981
|
}),
|
|
13690
13982
|
});
|
|
13691
13983
|
},
|
|
13984
|
+
// Mail a new code for the same sign-in (60 s apart, 5 an hour per account). The sign-in keeps its
|
|
13985
|
+
// original 5-minute expiry. Resolves { sent, delivery, expires_in }.
|
|
13986
|
+
resend(opts) {
|
|
13987
|
+
opts = opts || {};
|
|
13988
|
+
if (!opts.mfa_token) {
|
|
13989
|
+
const err = new Error('sw.auth.mfa.resend: mfa_token is required');
|
|
13990
|
+
err.code = 'VALIDATION_ERROR';
|
|
13991
|
+
throw err;
|
|
13992
|
+
}
|
|
13993
|
+
return platformJSON('/v1/auth/mfa/challenge', {
|
|
13994
|
+
method: 'POST',
|
|
13995
|
+
body: JSON.stringify({ project_id: projectId, mfa_token: opts.mfa_token, resend: true }),
|
|
13996
|
+
});
|
|
13997
|
+
},
|
|
13692
13998
|
// Complete the password-login MFA branch as a cookie session. This
|
|
13693
13999
|
// adds no authority: it brokers the same developer-authenticated
|
|
13694
14000
|
// challenge(), then keeps the returned access/refresh credentials
|
|
@@ -13703,28 +14009,55 @@ var AUTH_JS = ` auth: (function () {
|
|
|
13703
14009
|
__sw_setAuthCookies(session.access, session.refresh);
|
|
13704
14010
|
return { user: session.user, expires_at: __sw_preflightJwt(session.access).expSec * 1000 };
|
|
13705
14011
|
},
|
|
13706
|
-
//
|
|
13707
|
-
//
|
|
14012
|
+
// Turn codes off, in two steps: { token, password } re-checks the password and mails a code
|
|
14013
|
+
// (resolves { challenge_id, delivery, expires_in }); { token, challenge_id, code } removes the factor
|
|
14014
|
+
// (resolves { enrolled: false }). Refused under a 'required' policy (409 AUTH_MFA_REQUIRED_BY_APP).
|
|
13708
14015
|
unenroll(opts) {
|
|
13709
14016
|
opts = opts || {};
|
|
13710
|
-
|
|
13711
|
-
|
|
14017
|
+
const confirming = opts.challenge_id !== undefined;
|
|
14018
|
+
if (!opts.token || (confirming ? !opts.code : !opts.password)) {
|
|
14019
|
+
const err = new Error('sw.auth.mfa.unenroll: token and either password, or challenge_id and code, are required');
|
|
13712
14020
|
err.code = 'VALIDATION_ERROR';
|
|
13713
14021
|
throw err;
|
|
13714
14022
|
}
|
|
13715
14023
|
return userTokenJSON('/v1/auth/mfa/unenroll', opts.token, {
|
|
13716
14024
|
method: 'POST',
|
|
13717
|
-
body: JSON.stringify({ code: opts.code }),
|
|
14025
|
+
body: JSON.stringify(confirming ? { challenge_id: opts.challenge_id, code: opts.code } : { password: opts.password }),
|
|
14026
|
+
});
|
|
14027
|
+
},
|
|
14028
|
+
// For a user who cannot receive a code (no session needed): { email, password } starts a 72-hour wait
|
|
14029
|
+
// and mails a notice with a cancel link (202 { removal_pending, removes_at }); the same call after the
|
|
14030
|
+
// wait removes the factor ({ enrolled: false }). Refused (503) when the app cannot email the user.
|
|
14031
|
+
recover(opts) {
|
|
14032
|
+
opts = opts || {};
|
|
14033
|
+
if (!opts.email || !opts.password) {
|
|
14034
|
+
const err = new Error('sw.auth.mfa.recover: email and password are required');
|
|
14035
|
+
err.code = 'VALIDATION_ERROR';
|
|
14036
|
+
throw err;
|
|
14037
|
+
}
|
|
14038
|
+
return platformJSON('/v1/auth/mfa/recovery', {
|
|
14039
|
+
method: 'POST',
|
|
14040
|
+
body: JSON.stringify({ project_id: projectId, email: opts.email, password: opts.password }),
|
|
13718
14041
|
});
|
|
13719
14042
|
},
|
|
13720
|
-
//
|
|
13721
|
-
//
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
14043
|
+
// Cancel a pending removal with the token from the notice's cancel link (no session or code).
|
|
14044
|
+
// Resolves { outcome: 'cancelled' | 'already_closed' | 'completed', message }.
|
|
14045
|
+
cancelRecovery(opts) {
|
|
14046
|
+
opts = opts || {};
|
|
14047
|
+
if (!opts.token) {
|
|
14048
|
+
const err = new Error('sw.auth.mfa.cancelRecovery: token is required');
|
|
14049
|
+
err.code = 'VALIDATION_ERROR';
|
|
14050
|
+
throw err;
|
|
14051
|
+
}
|
|
14052
|
+
return platformJSON('/v1/auth/mfa/recovery/cancel', {
|
|
14053
|
+
method: 'POST',
|
|
14054
|
+
body: JSON.stringify({ token: opts.token }),
|
|
14055
|
+
});
|
|
14056
|
+
},
|
|
14057
|
+
// The same steps for the request's own session (cookie or Bearer). challenge_id and the codes are the
|
|
14058
|
+
// step's own one-time credentials and stay explicit. Turning codes on or off ends no session.
|
|
13726
14059
|
async enrollWithCookie(req) {
|
|
13727
|
-
return sessionChange('mfa.enrollWithCookie', req, '/v1/auth/mfa/enroll', { body: {} });
|
|
14060
|
+
return sessionChange('mfa.enrollWithCookie', req, '/v1/auth/mfa/enroll', { body: { method: 'email' } });
|
|
13728
14061
|
},
|
|
13729
14062
|
async reauthenticateWithCookie(req, opts) {
|
|
13730
14063
|
return sessionChange('mfa.reauthenticateWithCookie', req, '/v1/auth/mfa/activation/reauth', { body: function () {
|
|
@@ -13743,15 +14076,19 @@ var AUTH_JS = ` auth: (function () {
|
|
|
13743
14076
|
} });
|
|
13744
14077
|
},
|
|
13745
14078
|
async verifyWithCookie(req, opts) {
|
|
13746
|
-
return sessionChange('mfa.verifyWithCookie', req, '/v1/auth/mfa/verify', {
|
|
13747
|
-
opts = requireFields('mfa.verifyWithCookie', opts, ['
|
|
13748
|
-
return {
|
|
14079
|
+
return sessionChange('mfa.verifyWithCookie', req, '/v1/auth/mfa/verify', { body: function () {
|
|
14080
|
+
opts = requireFields('mfa.verifyWithCookie', opts, ['challenge_id', 'code']);
|
|
14081
|
+
return { challenge_id: opts.challenge_id, code: opts.code };
|
|
13749
14082
|
} });
|
|
13750
14083
|
},
|
|
13751
14084
|
async unenrollWithCookie(req, opts) {
|
|
13752
14085
|
return sessionChange('mfa.unenrollWithCookie', req, '/v1/auth/mfa/unenroll', { body: function () {
|
|
13753
|
-
opts
|
|
13754
|
-
|
|
14086
|
+
if (opts && opts.challenge_id !== undefined) {
|
|
14087
|
+
opts = requireFields('mfa.unenrollWithCookie', opts, ['challenge_id', 'code']);
|
|
14088
|
+
return { challenge_id: opts.challenge_id, code: opts.code };
|
|
14089
|
+
}
|
|
14090
|
+
opts = requireFields('mfa.unenrollWithCookie', opts, ['password']);
|
|
14091
|
+
return { password: opts.password };
|
|
13755
14092
|
} });
|
|
13756
14093
|
},
|
|
13757
14094
|
},
|
|
@@ -16031,7 +16368,7 @@ function __sw_dataIsDuplicateConflict(error) {
|
|
|
16031
16368
|
}
|
|
16032
16369
|
function __sw_dataPolicyUsesMembership(policy, table) {
|
|
16033
16370
|
if (!policy || typeof policy !== 'object') return false;
|
|
16034
|
-
if (policy.k === 'm') return policy.m && policy.m.m === table;
|
|
16371
|
+
if (policy.k === 'm') return policy.m && policy.m.m === table && !policy.m.og;
|
|
16035
16372
|
if (policy.k === 'a') return policy.p.some(item => __sw_dataPolicyUsesMembership(item, table));
|
|
16036
16373
|
// allOf: a membership table named by either slot stays closed to browser writes.
|
|
16037
16374
|
if (policy.k === 'n') return __sw_dataPolicyUsesMembership(policy.g, table) || __sw_dataPolicyUsesMembership(policy.w, table);
|
|
@@ -17180,20 +17517,25 @@ function reportedFailureFields(platformCtx, requestId) {
|
|
|
17180
17517
|
return out;
|
|
17181
17518
|
}
|
|
17182
17519
|
|
|
17183
|
-
/** Add the correlation handle without changing responses when tracing is absent.
|
|
17184
|
-
|
|
17520
|
+
/** Add the correlation handle without changing responses when tracing is absent. handlerResponse is true only
|
|
17521
|
+
* at the handler-success site for a Response the handler itself returned. */
|
|
17522
|
+
function attachTraceId(response, platformCtx, handlerResponse) {
|
|
17185
17523
|
try {
|
|
17186
17524
|
const t = platformCtx && platformCtx.__sw_trace;
|
|
17187
|
-
// X-Sw-Route-Pattern
|
|
17188
|
-
// passes through, with or without a trace; only the runtime's
|
|
17525
|
+
// X-Sw-Route-Pattern and X-Sw-Response-Source are the PLATFORM's (the edge's completion log): a function's
|
|
17526
|
+
// own copy never passes through, with or without a trace; only the runtime's values are set below.
|
|
17189
17527
|
if (!t) {
|
|
17190
|
-
if (!response.headers.has('X-Sw-Route-Pattern')) return response;
|
|
17528
|
+
if (!response.headers.has('X-Sw-Route-Pattern') && !response.headers.has('X-Sw-Response-Source') && handlerResponse !== true) return response;
|
|
17191
17529
|
const stripped = new Headers(response.headers);
|
|
17192
17530
|
stripped.delete('X-Sw-Route-Pattern');
|
|
17531
|
+
stripped.delete('X-Sw-Response-Source');
|
|
17532
|
+
if (handlerResponse === true) stripped.set('X-Sw-Response-Source', 'handler');
|
|
17193
17533
|
return new Response(response.body, { status: response.status, statusText: response.statusText, headers: stripped });
|
|
17194
17534
|
}
|
|
17195
17535
|
const headers = new Headers(response.headers);
|
|
17196
17536
|
headers.delete('X-Sw-Route-Pattern');
|
|
17537
|
+
headers.delete('X-Sw-Response-Source');
|
|
17538
|
+
if (handlerResponse === true) headers.set('X-Sw-Response-Source', 'handler');
|
|
17197
17539
|
headers.set('X-Trace-Id', t.traceId);
|
|
17198
17540
|
headers.set('traceparent', '00-' + t.traceId + '-' + t.spanId + '-' + (t.sampled ? '01' : '00'));
|
|
17199
17541
|
headers.set('X-Trace-Sampling', t.sampled
|
|
@@ -17577,6 +17919,9 @@ function isCanonicalBigintText(value) {
|
|
|
17577
17919
|
}
|
|
17578
17920
|
|
|
17579
17921
|
// worker/src/utils/db-schema-deploy/client-permissions.ts
|
|
17922
|
+
function tableAllowsClientWrites(table) {
|
|
17923
|
+
return Boolean(table.client && (table.client.create !== null || table.client.update !== null || table.client.delete || table.client.intake !== void 0));
|
|
17924
|
+
}
|
|
17580
17925
|
function parseClientPermissions(value, columns, scope) {
|
|
17581
17926
|
const fail = (message) => ({ ok: false, message });
|
|
17582
17927
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -18067,6 +18412,11 @@ var SW_INIT_JS = `
|
|
|
18067
18412
|
globalThis.sw = globalThis.sw || {};
|
|
18068
18413
|
${DB_OUTCOME_ERROR_JS}
|
|
18069
18414
|
|
|
18415
|
+
// Responses this runtime made (sw.endpoint refusals and caught throws). index.mjs captures the reader and
|
|
18416
|
+
// removes its public hook before any user module loads, so customer code can neither read nor change it.
|
|
18417
|
+
const __sw_runtimeMadeResponses = new WeakSet();
|
|
18418
|
+
globalThis.sw.__isRuntimeMadeResponse = function (response) { return __sw_runtimeMadeResponses.has(response); };
|
|
18419
|
+
|
|
18070
18420
|
// Legacy USER_ENV supplies customer-only process.env after native reflection is denied.
|
|
18071
18421
|
// index.mjs captures this initializer before lazy module load and removes its public hook.
|
|
18072
18422
|
globalThis.sw.__wireProcessEnv = function __sw_wireProcessEnv(runtimeEnv) {
|
|
@@ -18176,23 +18526,32 @@ function __sw_corsHeaders(corsCfg, request) {
|
|
|
18176
18526
|
};
|
|
18177
18527
|
}
|
|
18178
18528
|
|
|
18529
|
+
/** Record a Response this runtime made itself (never the handler's). */
|
|
18530
|
+
function __sw_runtimeMade(response) {
|
|
18531
|
+
__sw_runtimeMadeResponses.add(response);
|
|
18532
|
+
return response;
|
|
18533
|
+
}
|
|
18534
|
+
|
|
18179
18535
|
function __sw_jsonError(status, code, message, extras) {
|
|
18180
18536
|
var body = Object.assign({ ok: false, error: code, message: message }, extras || {});
|
|
18181
|
-
return new Response(JSON.stringify(body), {
|
|
18537
|
+
return __sw_runtimeMade(new Response(JSON.stringify(body), {
|
|
18182
18538
|
status: status,
|
|
18183
18539
|
headers: { 'Content-Type': 'application/json' },
|
|
18184
|
-
});
|
|
18540
|
+
}));
|
|
18185
18541
|
}
|
|
18186
18542
|
|
|
18187
18543
|
function __sw_mergeHeaders(response, extra) {
|
|
18188
18544
|
if (!extra || Object.keys(extra).length === 0) return response;
|
|
18189
18545
|
var headers = new Headers(response.headers);
|
|
18190
18546
|
for (var k of Object.keys(extra)) headers.set(k, extra[k]);
|
|
18191
|
-
|
|
18547
|
+
var merged = new Response(response.body, {
|
|
18192
18548
|
status: response.status,
|
|
18193
18549
|
statusText: response.statusText,
|
|
18194
18550
|
headers: headers,
|
|
18195
18551
|
});
|
|
18552
|
+
// The copy keeps the original's provenance.
|
|
18553
|
+
if (__sw_runtimeMadeResponses.has(response)) __sw_runtimeMadeResponses.add(merged);
|
|
18554
|
+
return merged;
|
|
18196
18555
|
}
|
|
18197
18556
|
|
|
18198
18557
|
/**
|
|
@@ -18237,7 +18596,7 @@ globalThis.sw.endpoint = function (config) {
|
|
|
18237
18596
|
const console = sw.__sw_nativeConsole || globalThis.console;
|
|
18238
18597
|
var corsHeaders = __sw_corsHeaders(cors, request);
|
|
18239
18598
|
if (request.method === 'OPTIONS') {
|
|
18240
|
-
return new Response(null, { status: 204, headers: corsHeaders });
|
|
18599
|
+
return __sw_runtimeMade(new Response(null, { status: 204, headers: corsHeaders }));
|
|
18241
18600
|
}
|
|
18242
18601
|
|
|
18243
18602
|
// Auth.
|
|
@@ -18293,10 +18652,10 @@ globalThis.sw.endpoint = function (config) {
|
|
|
18293
18652
|
var code = (r && r.error) || 'RATE_LIMITED';
|
|
18294
18653
|
var message = (r && r.message) || 'Too many requests.';
|
|
18295
18654
|
return __sw_mergeHeaders(
|
|
18296
|
-
new Response(
|
|
18655
|
+
__sw_runtimeMade(new Response(
|
|
18297
18656
|
JSON.stringify({ ok: false, error: code, message: message, retry_after: retry }),
|
|
18298
18657
|
{ status: 429, headers: { 'Content-Type': 'application/json', 'Retry-After': String(retry) } }
|
|
18299
|
-
),
|
|
18658
|
+
)),
|
|
18300
18659
|
corsHeaders
|
|
18301
18660
|
);
|
|
18302
18661
|
}
|
|
@@ -18315,10 +18674,10 @@ globalThis.sw.endpoint = function (config) {
|
|
|
18315
18674
|
rlErr && rlErr.code, rlErr && rlErr.status, rlErr && rlErr.message
|
|
18316
18675
|
);
|
|
18317
18676
|
return __sw_mergeHeaders(
|
|
18318
|
-
new Response(
|
|
18677
|
+
__sw_runtimeMade(new Response(
|
|
18319
18678
|
JSON.stringify({ ok: false, error: 'RATE_LIMIT_UNAVAILABLE', message: 'Rate limiting is temporarily unavailable; this request was blocked. A later identical request can succeed.' }),
|
|
18320
18679
|
{ status: 503, headers: { 'Content-Type': 'application/json', 'Retry-After': String(rateLimit.windowSeconds) } }
|
|
18321
|
-
),
|
|
18680
|
+
)),
|
|
18322
18681
|
corsHeaders
|
|
18323
18682
|
);
|
|
18324
18683
|
}
|
|
@@ -18403,7 +18762,7 @@ function isApiIndexHandlerPath(file) {
|
|
|
18403
18762
|
return /^api\/.+\/index\.(?:ts|tsx|mts|js|mjs|jsx)$/.test(file) || /^api\/index\.(?:ts|tsx|mts|js|mjs|jsx)$/.test(file);
|
|
18404
18763
|
}
|
|
18405
18764
|
|
|
18406
|
-
//
|
|
18765
|
+
// worker/node_modules/acorn/dist/acorn.mjs
|
|
18407
18766
|
var astralIdentifierCodes = [509, 0, 227, 0, 150, 4, 294, 9, 1368, 2, 2, 1, 6, 3, 41, 2, 5, 0, 166, 1, 574, 3, 9, 9, 7, 9, 32, 4, 318, 1, 80, 3, 71, 10, 50, 3, 123, 2, 54, 14, 32, 10, 3, 1, 11, 3, 46, 10, 8, 0, 46, 9, 7, 2, 37, 13, 2, 9, 6, 1, 45, 0, 13, 2, 49, 13, 9, 3, 2, 11, 83, 11, 7, 0, 3, 0, 158, 11, 6, 9, 7, 3, 56, 1, 2, 6, 3, 1, 3, 2, 10, 0, 11, 1, 3, 6, 4, 4, 68, 8, 2, 0, 3, 0, 2, 3, 2, 4, 2, 0, 15, 1, 83, 17, 10, 9, 5, 0, 82, 19, 13, 9, 214, 6, 3, 8, 28, 1, 83, 16, 16, 9, 82, 12, 9, 9, 7, 19, 58, 14, 5, 9, 243, 14, 166, 9, 71, 5, 2, 1, 3, 3, 2, 0, 2, 1, 13, 9, 120, 6, 3, 6, 4, 0, 29, 9, 41, 6, 2, 3, 9, 0, 10, 10, 47, 15, 343, 9, 54, 7, 2, 7, 17, 9, 57, 21, 2, 13, 123, 5, 4, 0, 2, 1, 2, 6, 2, 0, 9, 9, 49, 4, 2, 1, 2, 4, 9, 9, 330, 3, 10, 1, 2, 0, 49, 6, 4, 4, 14, 10, 5350, 0, 7, 14, 11465, 27, 2343, 9, 87, 9, 39, 4, 60, 6, 26, 9, 535, 9, 470, 0, 2, 54, 8, 3, 82, 0, 12, 1, 19628, 1, 4178, 9, 519, 45, 3, 22, 543, 4, 4, 5, 9, 7, 3, 6, 31, 3, 149, 2, 1418, 49, 513, 54, 5, 49, 9, 0, 15, 0, 23, 4, 2, 14, 1361, 6, 2, 16, 3, 6, 2, 1, 2, 4, 101, 0, 161, 6, 10, 9, 357, 0, 62, 13, 499, 13, 245, 1, 2, 9, 726, 6, 110, 6, 6, 9, 4759, 9, 787719, 239];
|
|
18408
18767
|
var astralIdentifierStartCodes = [0, 11, 2, 25, 2, 18, 2, 1, 2, 14, 3, 13, 35, 122, 70, 52, 268, 28, 4, 48, 48, 31, 14, 29, 6, 37, 11, 29, 3, 35, 5, 7, 2, 4, 43, 157, 19, 35, 5, 35, 5, 39, 9, 51, 13, 10, 2, 14, 2, 6, 2, 1, 2, 10, 2, 14, 2, 6, 2, 1, 4, 51, 13, 310, 10, 21, 11, 7, 25, 5, 2, 41, 2, 8, 70, 5, 3, 0, 2, 43, 2, 1, 4, 0, 3, 22, 11, 22, 10, 30, 66, 18, 2, 1, 11, 21, 11, 25, 71, 55, 7, 1, 65, 0, 16, 3, 2, 2, 2, 28, 43, 28, 4, 28, 36, 7, 2, 27, 28, 53, 11, 21, 11, 18, 14, 17, 111, 72, 56, 50, 14, 50, 14, 35, 39, 27, 10, 22, 251, 41, 7, 1, 17, 2, 60, 28, 11, 0, 9, 21, 43, 17, 47, 20, 28, 22, 13, 52, 58, 1, 3, 0, 14, 44, 33, 24, 27, 35, 30, 0, 3, 0, 9, 34, 4, 0, 13, 47, 15, 3, 22, 0, 2, 0, 36, 17, 2, 24, 20, 1, 64, 6, 2, 0, 2, 3, 2, 14, 2, 9, 8, 46, 39, 7, 3, 1, 3, 21, 2, 6, 2, 1, 2, 4, 4, 0, 19, 0, 13, 4, 31, 9, 2, 0, 3, 0, 2, 37, 2, 0, 26, 0, 2, 0, 45, 52, 19, 3, 21, 2, 31, 47, 21, 1, 2, 0, 185, 46, 42, 3, 37, 47, 21, 0, 60, 42, 14, 0, 72, 26, 38, 6, 186, 43, 117, 63, 32, 7, 3, 0, 3, 7, 2, 1, 2, 23, 16, 0, 2, 0, 95, 7, 3, 38, 17, 0, 2, 0, 29, 0, 11, 39, 8, 0, 22, 0, 12, 45, 20, 0, 19, 72, 200, 32, 32, 8, 2, 36, 18, 0, 50, 29, 113, 6, 2, 1, 2, 37, 22, 0, 26, 5, 2, 1, 2, 31, 15, 0, 328, 18, 16, 0, 2, 12, 2, 33, 125, 0, 80, 921, 103, 110, 18, 195, 2637, 96, 16, 1071, 18, 5, 26, 3994, 6, 582, 6842, 29, 1763, 568, 8, 30, 18, 78, 18, 29, 19, 47, 17, 3, 32, 20, 6, 18, 433, 44, 212, 63, 129, 74, 6, 0, 67, 12, 65, 1, 2, 0, 29, 6135, 9, 1237, 42, 9, 8936, 3, 2, 6, 2, 1, 2, 290, 16, 0, 30, 2, 3, 0, 15, 3, 9, 395, 2309, 106, 6, 12, 4, 8, 8, 9, 5991, 84, 2, 70, 2, 1, 3, 0, 3, 1, 3, 3, 2, 11, 2, 0, 2, 6, 2, 64, 2, 3, 3, 7, 2, 6, 2, 27, 2, 3, 2, 4, 2, 0, 4, 6, 2, 339, 3, 24, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 30, 2, 24, 2, 7, 1845, 30, 7, 5, 262, 61, 147, 44, 11, 6, 17, 0, 322, 29, 19, 43, 485, 27, 229, 29, 3, 0, 496, 6, 2, 3, 2, 1, 2, 14, 2, 196, 60, 67, 8, 0, 1205, 3, 2, 26, 2, 1, 2, 0, 3, 0, 2, 9, 2, 3, 2, 0, 2, 0, 7, 0, 5, 0, 2, 0, 2, 0, 2, 2, 2, 1, 2, 0, 3, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 1, 2, 0, 3, 3, 2, 6, 2, 3, 2, 3, 2, 0, 2, 9, 2, 16, 6, 2, 2, 4, 2, 16, 4421, 42719, 33, 4153, 7, 221, 3, 5761, 15, 7472, 16, 621, 2467, 541, 1507, 4938, 6, 4191];
|
|
18409
18768
|
var nonASCIIidentifierChars = "\u200C\u200D\xB7\u0300-\u036F\u0387\u0483-\u0487\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u0669\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u06F0-\u06F9\u0711\u0730-\u074A\u07A6-\u07B0\u07C0-\u07C9\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0897-\u089F\u08CA-\u08E1\u08E3-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0966-\u096F\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u09E6-\u09EF\u09FE\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A66-\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AE6-\u0AEF\u0AFA-\u0AFF\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B55-\u0B57\u0B62\u0B63\u0B66-\u0B6F\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0BE6-\u0BEF\u0C00-\u0C04\u0C3C\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0CE6-\u0CEF\u0CF3\u0D00-\u0D03\u0D3B\u0D3C\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D66-\u0D6F\u0D81-\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0E50-\u0E59\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0ED0-\u0ED9\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1040-\u1049\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109D\u135D-\u135F\u1369-\u1371\u1712-\u1715\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u17E0-\u17E9\u180B-\u180D\u180F-\u1819\u18A9\u1920-\u192B\u1930-\u193B\u1946-\u194F\u19D0-\u19DA\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AB0-\u1ABD\u1ABF-\u1ACE\u1B00-\u1B04\u1B34-\u1B44\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BF3\u1C24-\u1C37\u1C40-\u1C49\u1C50-\u1C59\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1DC0-\u1DFF\u200C\u200D\u203F\u2040\u2054\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\u30FB\uA620-\uA629\uA66F\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA82C\uA880\uA881\uA8B4-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F1\uA8FF-\uA909\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uA9D0-\uA9D9\uA9E5\uA9F0-\uA9F9\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA50-\uAA59\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uABF0-\uABF9\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFF10-\uFF19\uFF3F\uFF65";
|
|
@@ -24015,6 +24374,140 @@ function tokenizer2(input, options) {
|
|
|
24015
24374
|
return Parser.tokenizer(input, options);
|
|
24016
24375
|
}
|
|
24017
24376
|
|
|
24377
|
+
// worker/src/utils/db-schema-deploy/extract-schema-grants.ts
|
|
24378
|
+
function readGrantScope(r, table, line, fail) {
|
|
24379
|
+
r.expectPunct("(", "after grant");
|
|
24380
|
+
r.expectPunct("{", "in grant options");
|
|
24381
|
+
let membership, grantee, row;
|
|
24382
|
+
let operations;
|
|
24383
|
+
const seen = /* @__PURE__ */ new Set();
|
|
24384
|
+
while (!r.tryPunct("}")) {
|
|
24385
|
+
const key = r.readKey("in grant options");
|
|
24386
|
+
if (seen.has(key.name)) fail(key.line, `Duplicate grant option "${key.name}".`);
|
|
24387
|
+
seen.add(key.name);
|
|
24388
|
+
r.expectPunct(":", "after grant option");
|
|
24389
|
+
if (key.name === "table" || key.name === "grantee" || key.name === "row") {
|
|
24390
|
+
const value = r.readLiteral("in grant options");
|
|
24391
|
+
if (typeof value !== "string" || !/^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/.test(value)) fail(key.line, "grant table, grantee and row must be column/table identifiers.");
|
|
24392
|
+
if (key.name === "table") membership = value.toLowerCase();
|
|
24393
|
+
else if (key.name === "grantee") grantee = value.toLowerCase();
|
|
24394
|
+
else row = value.toLowerCase();
|
|
24395
|
+
} else if (key.name === "operations") {
|
|
24396
|
+
const ops = [];
|
|
24397
|
+
r.expectPunct("[", "in grant operations");
|
|
24398
|
+
while (!r.tryPunct("]")) {
|
|
24399
|
+
const op = r.readLiteral("in grant operations");
|
|
24400
|
+
if (op !== "read" && op !== "update") fail(key.line, "grant operations allow only read and update.");
|
|
24401
|
+
if (ops.includes(op)) fail(key.line, "Duplicate grant operation.");
|
|
24402
|
+
ops.push(op);
|
|
24403
|
+
if (!r.tryPunct(",")) {
|
|
24404
|
+
r.expectPunct("]", "closing grant operations");
|
|
24405
|
+
break;
|
|
24406
|
+
}
|
|
24407
|
+
}
|
|
24408
|
+
if (!ops.includes("read")) fail(key.line, "grant operations require read; update alone is not allowed.");
|
|
24409
|
+
operations = ops.includes("update") ? ["read", "update"] : ["read"];
|
|
24410
|
+
} else fail(key.line, `Unknown grant option "${key.name}". Allowed: table, grantee, row, operations.`);
|
|
24411
|
+
if (!r.tryPunct(",")) {
|
|
24412
|
+
r.expectPunct("}", "closing grant options");
|
|
24413
|
+
break;
|
|
24414
|
+
}
|
|
24415
|
+
}
|
|
24416
|
+
r.tryPunct(",");
|
|
24417
|
+
r.expectPunct(")", "closing grant");
|
|
24418
|
+
if (!membership || !grantee || !operations || grantee === "user_id") fail(line, `grant() on "${table}" requires table, a grantee column distinct from managed user_id, and explicit operations.`);
|
|
24419
|
+
if (row !== void 0 && (row === "user_id" || row === grantee)) fail(line, `grant() on "${table}": row names the shares column holding the shared row's id, distinct from user_id and the grantee.`);
|
|
24420
|
+
return { kind: "member", group: ["user_id"], membership, memberUser: grantee, memberGroup: row === void 0 ? ["user_id"] : ["user_id", row], operations, ownerGrant: true };
|
|
24421
|
+
}
|
|
24422
|
+
function resolveGrantRowKeys(scope, key, table, line, fail) {
|
|
24423
|
+
const visit = (node) => {
|
|
24424
|
+
if (node.kind === "member" && node.ownerGrant && node.memberGroup.length === 2 && node.group.length === 1) {
|
|
24425
|
+
if (key === void 0) fail(line, `grant() with row on "${table}" needs the table's one id() key: each share names one row by it.`);
|
|
24426
|
+
node.group = ["user_id", key.toLowerCase()];
|
|
24427
|
+
} else if (node.kind === "any_of") node.policies.forEach(visit);
|
|
24428
|
+
else if (node.kind === "all_of") {
|
|
24429
|
+
visit(node.gate);
|
|
24430
|
+
visit(node.who);
|
|
24431
|
+
}
|
|
24432
|
+
};
|
|
24433
|
+
if (scope.kind === "policy") visit(scope.policy);
|
|
24434
|
+
}
|
|
24435
|
+
function validateDeclaredMemberships(tables, tableByName, knownColumns, errors) {
|
|
24436
|
+
const memberPolicies = (scope) => {
|
|
24437
|
+
const out = [];
|
|
24438
|
+
const visit = (node) => {
|
|
24439
|
+
if (node.kind === "member") out.push(node);
|
|
24440
|
+
else if (node.kind === "any_of") node.policies.forEach(visit);
|
|
24441
|
+
else if (node.kind === "all_of") {
|
|
24442
|
+
visit(node.gate);
|
|
24443
|
+
visit(node.who);
|
|
24444
|
+
}
|
|
24445
|
+
};
|
|
24446
|
+
if (scope.kind === "member") out.push(scope);
|
|
24447
|
+
else if (scope.kind === "policy") visit(scope.policy);
|
|
24448
|
+
return out;
|
|
24449
|
+
};
|
|
24450
|
+
for (const t of tables) {
|
|
24451
|
+
for (const sc of memberPolicies(t.scope)) {
|
|
24452
|
+
const own = knownColumns(t);
|
|
24453
|
+
for (const g of sc.group) {
|
|
24454
|
+
if (!own.has(g)) {
|
|
24455
|
+
errors.push(
|
|
24456
|
+
`Table "${t.name}" is member-scoped on "${g}", but "${g}" is not one of its declared columns. The group-key columns must be real columns on the table.`
|
|
24457
|
+
);
|
|
24458
|
+
}
|
|
24459
|
+
}
|
|
24460
|
+
const membershipTable = tableByName.get(sc.membership);
|
|
24461
|
+
if (!membershipTable) {
|
|
24462
|
+
errors.push(
|
|
24463
|
+
`Table "${t.name}" is member-scoped through the membership table "${sc.membership}", which is not declared in db/schema.ts. The membership table must be a managed table in the same schema.`
|
|
24464
|
+
);
|
|
24465
|
+
continue;
|
|
24466
|
+
}
|
|
24467
|
+
const membershipCols = knownColumns(membershipTable);
|
|
24468
|
+
if (sc.ownerGrant) {
|
|
24469
|
+
if (membershipTable.scope.kind !== "owner" || membershipTable.scope.column !== "user_id" || membershipTable.scope.visitors) {
|
|
24470
|
+
errors.push(`Grant table "${sc.membership}" must use owner() with managed user_id and authenticated owners.`);
|
|
24471
|
+
}
|
|
24472
|
+
if (membershipTable.client?.intake || membershipTable.client?.links) errors.push(`Grant table "${sc.membership}" cannot expose public intake or row links; only its authenticated owner manages grants.`);
|
|
24473
|
+
const grantee = membershipTable.columns.find((column) => column.name === sc.memberUser);
|
|
24474
|
+
if (!grantee || grantee.helper !== "text" || grantee.nullable) errors.push(`Grant grantee "${sc.membership}"."${sc.memberUser}" must be a required text column.`);
|
|
24475
|
+
if (sc.memberGroup.length === 2) {
|
|
24476
|
+
const row = membershipTable.columns.find((column) => column.name === sc.memberGroup[1]);
|
|
24477
|
+
const key = t.columns.find((column) => column.name === sc.group[1]);
|
|
24478
|
+
const keyHelper = key?.helper === "id" ? key.uuid ? "text" : "integer" : null;
|
|
24479
|
+
if (!row || row.nullable || row.helper !== keyHelper) {
|
|
24480
|
+
errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" must be a required ${keyHelper ?? "id-typed"} column holding a "${t.name}" id.`);
|
|
24481
|
+
} else if (row.hasDefault || row.references !== t.name || row.onDelete !== "cascade") {
|
|
24482
|
+
errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" must reference its row with no default: ${sc.memberGroup[1]}: ${keyHelper}({ references: '${t.name}', onDelete: 'cascade' }).`);
|
|
24483
|
+
}
|
|
24484
|
+
if (membershipTable.client?.update?.includes(sc.memberGroup[1])) {
|
|
24485
|
+
errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" cannot be in client.update: a share names one row for its life. Delete the share and create another.`);
|
|
24486
|
+
}
|
|
24487
|
+
const users = tables.filter((other) => memberPolicies(other.scope).some((m) => m.ownerGrant && m.membership === sc.membership));
|
|
24488
|
+
if (users.length !== 1 || memberPolicies(t.scope).filter((m) => m.ownerGrant && m.membership === sc.membership).length !== 1) {
|
|
24489
|
+
errors.push(`Grant table "${sc.membership}" names rows of "${t.name}", so it serves only that one grant() rule. Declare another shares table for other grants.`);
|
|
24490
|
+
}
|
|
24491
|
+
}
|
|
24492
|
+
} else if (tableAllowsClientWrites(membershipTable)) {
|
|
24493
|
+
errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
|
|
24494
|
+
}
|
|
24495
|
+
if (!membershipCols.has(sc.memberUser)) {
|
|
24496
|
+
errors.push(
|
|
24497
|
+
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
|
|
24498
|
+
);
|
|
24499
|
+
}
|
|
24500
|
+
for (const mg of sc.ownerGrant ? [] : sc.memberGroup) {
|
|
24501
|
+
if (!membershipCols.has(mg)) {
|
|
24502
|
+
errors.push(
|
|
24503
|
+
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
|
|
24504
|
+
);
|
|
24505
|
+
}
|
|
24506
|
+
}
|
|
24507
|
+
}
|
|
24508
|
+
}
|
|
24509
|
+
}
|
|
24510
|
+
|
|
24018
24511
|
// worker/src/utils/db-schema-deploy/schema-constants.ts
|
|
24019
24512
|
var LITERAL_WORDS = /* @__PURE__ */ new Set(["true", "false", "null"]);
|
|
24020
24513
|
var STATEMENT_WORDS = /* @__PURE__ */ new Set([
|
|
@@ -25084,8 +25577,9 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
|
|
|
25084
25577
|
}
|
|
25085
25578
|
|
|
25086
25579
|
// worker/src/utils/db-schema-deploy/extract-schema-roles.ts
|
|
25580
|
+
var EMAIL_DOMAIN = EMAIL_DOMAIN_PATTERN;
|
|
25087
25581
|
var GROUP_KEYS = ["roles", "adminGrantableRoles"];
|
|
25088
|
-
var APP_ROLE_KEYS = ["grantableBy"];
|
|
25582
|
+
var APP_ROLE_KEYS = ["grantableBy", "emailDomain"];
|
|
25089
25583
|
function readStringList2(r, fail, ctx) {
|
|
25090
25584
|
const open = r.peek();
|
|
25091
25585
|
if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted role names, such as ['viewer'].`);
|
|
@@ -25129,7 +25623,7 @@ function readAppRoles(r, fail) {
|
|
|
25129
25623
|
const key = r.readKey("for an app role name");
|
|
25130
25624
|
if (out.some((role2) => role2.name === key.name)) fail(key.line, `app role "${key.name}" is declared twice.`);
|
|
25131
25625
|
r.expectPunct(":", `after app role "${key.name}"`);
|
|
25132
|
-
r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...] }`);
|
|
25626
|
+
r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...], emailDomain: '...' }`);
|
|
25133
25627
|
const role = { name: key.name, grantableBy: [] };
|
|
25134
25628
|
const seen = /* @__PURE__ */ new Set();
|
|
25135
25629
|
while (!r.tryPunct("}")) {
|
|
@@ -25140,7 +25634,16 @@ function readAppRoles(r, fail) {
|
|
|
25140
25634
|
if (seen.has(opt.name)) fail(opt.line, `app role "${key.name}" declares ${opt.name} twice.`);
|
|
25141
25635
|
seen.add(opt.name);
|
|
25142
25636
|
r.expectPunct(":", `after appRoles.${key.name}.${opt.name}`);
|
|
25143
|
-
|
|
25637
|
+
if (opt.name === "emailDomain") {
|
|
25638
|
+
const t = r.next();
|
|
25639
|
+
if (t.kind !== "string") fail(t.line, `appRoles.${key.name}.emailDomain must be one quoted domain, such as 'example.com'.`);
|
|
25640
|
+
if (!EMAIL_DOMAIN.test(t.value)) {
|
|
25641
|
+
fail(t.line, `appRoles.${key.name}.emailDomain "${t.value}" must be one lowercase domain such as 'example.com': no "@", no wildcard, no scheme or path. Users whose verified email is at exactly that domain hold the role; subdomains are other domains.`);
|
|
25642
|
+
}
|
|
25643
|
+
role.emailDomain = t.value;
|
|
25644
|
+
} else {
|
|
25645
|
+
role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
|
|
25646
|
+
}
|
|
25144
25647
|
if (!r.tryPunct(",")) {
|
|
25145
25648
|
r.expectPunct("}", `closing app role "${key.name}"`);
|
|
25146
25649
|
break;
|
|
@@ -25157,12 +25660,25 @@ function readAppRoles(r, fail) {
|
|
|
25157
25660
|
function validateDeclaredRoles(groups, appRoles) {
|
|
25158
25661
|
const delegation = /* @__PURE__ */ Object.create(null);
|
|
25159
25662
|
for (const role of appRoles ?? []) delegation[role.name] = role.grantableBy;
|
|
25160
|
-
return roleVocabularyErrors({
|
|
25663
|
+
return [...emailDomainRoleErrors(appRoles ?? []), ...roleVocabularyErrors({
|
|
25161
25664
|
groupRoles: groups?.roles ?? [],
|
|
25162
25665
|
adminGrantableGroupRoles: groups?.adminGrantableRoles ?? [],
|
|
25163
25666
|
appRoles: (appRoles ?? []).map((role) => role.name),
|
|
25164
25667
|
appRoleDelegation: delegation
|
|
25165
|
-
});
|
|
25668
|
+
})];
|
|
25669
|
+
}
|
|
25670
|
+
function emailDomainRoleErrors(appRoles) {
|
|
25671
|
+
const errors = [];
|
|
25672
|
+
const byDomain = new Set(appRoles.filter((role) => role.emailDomain !== void 0).map((role) => role.name));
|
|
25673
|
+
for (const role of appRoles) {
|
|
25674
|
+
if (role.emailDomain !== void 0 && !EMAIL_DOMAIN.test(role.emailDomain)) errors.push(`appRoles.${role.name}.emailDomain is not one lowercase domain.`);
|
|
25675
|
+
for (const grantor of role.grantableBy) {
|
|
25676
|
+
if (byDomain.has(grantor)) {
|
|
25677
|
+
errors.push(`appRoles.${role.name}.grantableBy names "${grantor}", which is held by email domain. A role held by email domain cannot grant other roles; list a role that is granted explicitly.`);
|
|
25678
|
+
}
|
|
25679
|
+
}
|
|
25680
|
+
}
|
|
25681
|
+
return errors;
|
|
25166
25682
|
}
|
|
25167
25683
|
|
|
25168
25684
|
// worker/src/utils/db-schema-deploy/extract-schema-options.ts
|
|
@@ -25249,6 +25765,7 @@ var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
|
|
|
25249
25765
|
...TABLE_MARKER_HELPERS,
|
|
25250
25766
|
"schema",
|
|
25251
25767
|
"table",
|
|
25768
|
+
"grant",
|
|
25252
25769
|
"removed",
|
|
25253
25770
|
"hasMany",
|
|
25254
25771
|
"belongsTo",
|
|
@@ -25363,10 +25880,10 @@ function tokenize2(source) {
|
|
|
25363
25880
|
var Reader = class {
|
|
25364
25881
|
constructor(tokens, options = {}) {
|
|
25365
25882
|
this.tokens = tokens;
|
|
25366
|
-
this.bareIdIsText = !options.legacyIntegerIdDefault;
|
|
25883
|
+
this.bareIdIsText = (table) => !options.legacyIntegerIdDefault && !options.integerKeyTables?.has(table.toLowerCase());
|
|
25367
25884
|
}
|
|
25368
25885
|
pos = 0;
|
|
25369
|
-
/** What a bare id() declares: a random text key, or
|
|
25886
|
+
/** What a bare id() on this table declares: a random text key, or an integer (ExtractSchemaOptions). */
|
|
25370
25887
|
bareIdIsText;
|
|
25371
25888
|
peek() {
|
|
25372
25889
|
return this.tokens[this.pos];
|
|
@@ -25534,7 +26051,7 @@ function readColumn(r, tableName, rawName, line) {
|
|
|
25534
26051
|
const col = {
|
|
25535
26052
|
name,
|
|
25536
26053
|
helper,
|
|
25537
|
-
uuid: helper === "id" && r.bareIdIsText,
|
|
26054
|
+
uuid: helper === "id" && r.bareIdIsText(tableName),
|
|
25538
26055
|
nullable: false,
|
|
25539
26056
|
hasDefault: false,
|
|
25540
26057
|
default: null,
|
|
@@ -25771,7 +26288,7 @@ function readPolicyMember(r, tableName, line) {
|
|
|
25771
26288
|
if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
|
|
25772
26289
|
return scope;
|
|
25773
26290
|
}
|
|
25774
|
-
var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), group() or appRole().";
|
|
26291
|
+
var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), grant(), group() or appRole().";
|
|
25775
26292
|
function readAnyOfBranch(r, tableName, which) {
|
|
25776
26293
|
const tok = r.expectIdent(`for the ${which} policy in anyOf() on table "${tableName}"`);
|
|
25777
26294
|
const failAt = (at2, message) => {
|
|
@@ -25779,6 +26296,7 @@ function readAnyOfBranch(r, tableName, which) {
|
|
|
25779
26296
|
};
|
|
25780
26297
|
if (tok.value === "owner") return readPolicyOwner(r, tableName, tok.line);
|
|
25781
26298
|
if (tok.value === "member") return readPolicyMember(r, tableName, tok.line);
|
|
26299
|
+
if (tok.value === "grant") return readGrantScope(r, tableName, tok.line, failAt);
|
|
25782
26300
|
if (tok.value === "group") return readGroupScope(r, tableName, tok.line, failAt);
|
|
25783
26301
|
if (tok.value === "appRole") return readAppRoleScope(r, tableName, tok.line, failAt);
|
|
25784
26302
|
throw new SchemaTsError(`line ${tok.line}: ${ANY_OF_FORMS}`);
|
|
@@ -25794,6 +26312,7 @@ function readAnyOfScope(r, tableName, line) {
|
|
|
25794
26312
|
if (owners.length !== 1) throw new SchemaTsError(`line ${line}: ${ANY_OF_FORMS}`);
|
|
25795
26313
|
const owner = owners[0];
|
|
25796
26314
|
const other = firstPolicy.kind === "owner" ? secondPolicy : firstPolicy;
|
|
26315
|
+
if (other.kind === "member" && other.ownerGrant && owner.column !== DEFAULT_OWNER_COLUMN) throw new SchemaTsError(`line ${line}: grant() requires owner() with managed user_id.`);
|
|
25797
26316
|
return { kind: "policy", policy: { kind: "any_of", policies: [owner, other] } };
|
|
25798
26317
|
}
|
|
25799
26318
|
var ALL_OF_FORMS = "allOf() takes a gate \u2014 member(), group() or appRole() \u2014 and then owner() or anyOf(owner(), ...).";
|
|
@@ -25821,6 +26340,7 @@ function readAllOfScope(r, tableName, line) {
|
|
|
25821
26340
|
r.tryPunct(",");
|
|
25822
26341
|
r.expectPunct(")", "closing allOf()");
|
|
25823
26342
|
const x = who.kind === "any_of" ? who.policies[1] : null;
|
|
26343
|
+
if (x?.kind === "member" && x.ownerGrant) throw new SchemaTsError(`line ${line}: grant() belongs in anyOf(owner(), grant()), not allOf().`);
|
|
25824
26344
|
for (const node of [gate, ...x ? [x] : []]) {
|
|
25825
26345
|
if (node.kind !== "member") continue;
|
|
25826
26346
|
if (!explicitMemberOperations.has(node)) {
|
|
@@ -26056,6 +26576,9 @@ function readTable(r, rawName, nameLine) {
|
|
|
26056
26576
|
if (key.name === "scope") {
|
|
26057
26577
|
if (scope) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "scope" twice.`);
|
|
26058
26578
|
scope = readScope(r, rawName);
|
|
26579
|
+
resolveGrantRowKeys(scope, idCols[0]?.name, rawName, key.line, (at2, message) => {
|
|
26580
|
+
throw new SchemaTsError(`line ${at2}: ${message}`);
|
|
26581
|
+
});
|
|
26059
26582
|
} else if (key.name === "indexes") {
|
|
26060
26583
|
if (indexEntries) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "indexes" twice.`);
|
|
26061
26584
|
indexEntries = readColumnLists(r, rawName, "indexes");
|
|
@@ -26287,55 +26810,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
26287
26810
|
};
|
|
26288
26811
|
resolveAppRoleScopes(tables, appRoles, errors);
|
|
26289
26812
|
resolveGroupsAndParents(tables, tableByName, privatePolicy, groupRoles, errors);
|
|
26290
|
-
|
|
26291
|
-
const out = [];
|
|
26292
|
-
const visit = (node) => {
|
|
26293
|
-
if (node.kind === "member") out.push(node);
|
|
26294
|
-
else if (node.kind === "any_of") node.policies.forEach(visit);
|
|
26295
|
-
else if (node.kind === "all_of") {
|
|
26296
|
-
visit(node.gate);
|
|
26297
|
-
visit(node.who);
|
|
26298
|
-
}
|
|
26299
|
-
};
|
|
26300
|
-
if (scope.kind === "member") out.push(scope);
|
|
26301
|
-
else if (scope.kind === "policy") visit(scope.policy);
|
|
26302
|
-
return out;
|
|
26303
|
-
};
|
|
26304
|
-
for (const t of tables) {
|
|
26305
|
-
for (const sc of memberPolicies(t.scope)) {
|
|
26306
|
-
const own = knownColumns(t);
|
|
26307
|
-
for (const g of sc.group) {
|
|
26308
|
-
if (!own.has(g)) {
|
|
26309
|
-
errors.push(
|
|
26310
|
-
`Table "${t.name}" is member-scoped on "${g}", but "${g}" is not one of its declared columns. The group-key columns must be real columns on the table.`
|
|
26311
|
-
);
|
|
26312
|
-
}
|
|
26313
|
-
}
|
|
26314
|
-
const membershipTable = tableByName.get(sc.membership);
|
|
26315
|
-
if (!membershipTable) {
|
|
26316
|
-
errors.push(
|
|
26317
|
-
`Table "${t.name}" is member-scoped through the membership table "${sc.membership}", which is not declared in db/schema.ts. The membership table must be a managed table in the same schema.`
|
|
26318
|
-
);
|
|
26319
|
-
continue;
|
|
26320
|
-
}
|
|
26321
|
-
const membershipCols = knownColumns(membershipTable);
|
|
26322
|
-
if (membershipTable.client && (membershipTable.client.create !== null || membershipTable.client.update !== null || membershipTable.client.delete || membershipTable.client.intake !== void 0)) {
|
|
26323
|
-
errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
|
|
26324
|
-
}
|
|
26325
|
-
if (!membershipCols.has(sc.memberUser)) {
|
|
26326
|
-
errors.push(
|
|
26327
|
-
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
|
|
26328
|
-
);
|
|
26329
|
-
}
|
|
26330
|
-
for (const mg of sc.memberGroup) {
|
|
26331
|
-
if (!membershipCols.has(mg)) {
|
|
26332
|
-
errors.push(
|
|
26333
|
-
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
|
|
26334
|
-
);
|
|
26335
|
-
}
|
|
26336
|
-
}
|
|
26337
|
-
}
|
|
26338
|
-
}
|
|
26813
|
+
validateDeclaredMemberships(tables, tableByName, knownColumns, errors);
|
|
26339
26814
|
validateDeclaredRelations(tables, tableByName, knownColumns, (table) => removedTables.includes(table) ? "removedTable()" : exportedTables.includes(table) ? "exported()" : null, errors);
|
|
26340
26815
|
validateRowLinks(tables, tableByName, errors);
|
|
26341
26816
|
validateIntake(tables, tableByName, errors);
|
|
@@ -26343,7 +26818,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
26343
26818
|
errors.push(...fileErrors);
|
|
26344
26819
|
validateFileCollections(fileCollections, (name) => {
|
|
26345
26820
|
const t = tableByName.get(name);
|
|
26346
|
-
return t ? { columns: knownColumns(t), clientWrites:
|
|
26821
|
+
return t ? { columns: knownColumns(t), clientWrites: tableAllowsClientWrites(t), parentTarget: fileParentTarget(t) } : null;
|
|
26347
26822
|
}, errors);
|
|
26348
26823
|
}
|
|
26349
26824
|
if (groupRoles || appRoles) errors.push(...validateDeclaredRoles(groupRoles, appRoles));
|