@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
|
@@ -60,22 +60,26 @@ var require_declared_data_contract = __commonJS({
|
|
|
60
60
|
if (value.k === "o" && keys === "c,k" && identifier.test(value.c)) return { k: "o", c: value.c };
|
|
61
61
|
if (value.k === "m" && keys === "k,m") {
|
|
62
62
|
record2(value.m, label + ".member");
|
|
63
|
-
const { g, m, u, mg, o } = value.m;
|
|
63
|
+
const { g, m, u, mg, o, og } = value.m;
|
|
64
64
|
const memberKeys = Object.keys(value.m).sort().join(",");
|
|
65
|
-
|
|
65
|
+
const grant = memberKeys === "g,m,mg,o,og,u" && og === true && Array.isArray(g) && Array.isArray(mg) && g[0] === "user_id" && mg[0] === "user_id" && (g.length === 1 || g.length === 2 && g[1] !== "user_id" && mg[1] !== "user_id" && mg[1] !== u) && u !== "user_id" && (JSON.stringify(o) === '["read"]' || JSON.stringify(o) === '["read","update"]');
|
|
66
|
+
if (!["g,m,mg,u", "g,m,mg,o,u"].includes(memberKeys) && !grant || !identifier.test(m) || !identifier.test(u) || !Array.isArray(g) || !g.length || !Array.isArray(mg) || g.length !== mg.length || [...g, ...mg].some((field) => typeof field !== "string" || !identifier.test(field)) || o !== void 0 && (!Array.isArray(o) || JSON.stringify(o) !== JSON.stringify(["read", "create", "update", "delete"].filter((operation) => o.includes(operation))) || !o.includes("read") && (o.includes("update") || o.includes("delete")))) fail(label);
|
|
66
67
|
const normalizedOperations = o?.length === 4 ? void 0 : o;
|
|
67
|
-
return { k: "m", m: { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {} } };
|
|
68
|
+
return { k: "m", m: { g: [...g], m, u, mg: [...mg], ...normalizedOperations !== void 0 ? { o: [...normalizedOperations] } : {}, ...og ? { og: true } : {} } };
|
|
68
69
|
}
|
|
69
70
|
if (value.k === "a" && keys === "k,p" && Array.isArray(value.p) && value.p.length === 2) {
|
|
70
71
|
const left = policy(value.p[0], label), right = policy(value.p[1], label);
|
|
71
72
|
if (left.k !== "o" || !["m", "g", "r"].includes(right.k)) fail(label);
|
|
73
|
+
if (right.k === "m" && right.m.og && left.c !== "user_id") fail(label);
|
|
72
74
|
return { k: "a", p: [left, right] };
|
|
73
75
|
}
|
|
74
76
|
if (value.k === "n" && keys === "g,k,w") {
|
|
75
77
|
const gate = policy(value.g, label), who = policy(value.w, label);
|
|
76
78
|
if (!["m", "g", "r"].includes(gate.k) || !["o", "a"].includes(who.k)) fail(label);
|
|
79
|
+
if (gate.k === "m" && gate.m.og) fail(label);
|
|
77
80
|
if (who.k === "a") {
|
|
78
81
|
const x = who.p[1];
|
|
82
|
+
if (x.k === "m" && x.m.og) fail(label);
|
|
79
83
|
const same = x.k === "r" || x.k === "g" && gate.k === "g" && x.c.toLowerCase() === gate.c.toLowerCase() || x.k === "m" && gate.k === "m" && x.m.m.toLowerCase() === gate.m.m.toLowerCase() && x.m.u.toLowerCase() === gate.m.u.toLowerCase() && JSON.stringify(x.m.g.map((c) => c.toLowerCase())) === JSON.stringify(gate.m.g.map((c) => c.toLowerCase())) && JSON.stringify(x.m.mg.map((c) => c.toLowerCase())) === JSON.stringify(gate.m.mg.map((c) => c.toLowerCase()));
|
|
80
84
|
if (!same) fail(label);
|
|
81
85
|
}
|
|
@@ -106,14 +110,14 @@ var require_declared_data_contract = __commonJS({
|
|
|
106
110
|
}
|
|
107
111
|
return roles;
|
|
108
112
|
}
|
|
109
|
-
function policyMembershipTables(value, out) {
|
|
113
|
+
function policyMembershipTables(value, out, grants) {
|
|
110
114
|
if (!value || typeof value !== "object") return;
|
|
111
|
-
if (value.k === "m" && value.m) out.add(value.m.m);
|
|
112
|
-
else if (value.k === "a") value.p.forEach((item) => policyMembershipTables(item, out));
|
|
115
|
+
if (value.k === "m" && value.m) (value.m.og === true ? grants : out).add(value.m.m);
|
|
116
|
+
else if (value.k === "a") value.p.forEach((item) => policyMembershipTables(item, out, grants));
|
|
113
117
|
else if (value.k === "n") {
|
|
114
|
-
policyMembershipTables(value.g, out);
|
|
115
|
-
policyMembershipTables(value.w, out);
|
|
116
|
-
} else if (value.k === "p") policyMembershipTables(value.p, out);
|
|
118
|
+
policyMembershipTables(value.g, out, grants);
|
|
119
|
+
policyMembershipTables(value.w, out, grants);
|
|
120
|
+
} else if (value.k === "p") policyMembershipTables(value.p, out, grants);
|
|
117
121
|
}
|
|
118
122
|
function validateInsertReceiptCapability(value) {
|
|
119
123
|
if (value !== void 0 && value !== "insert-v1") fail("insert receipt capability");
|
|
@@ -126,7 +130,17 @@ var require_declared_data_contract = __commonJS({
|
|
|
126
130
|
record2(scopes, "scopes");
|
|
127
131
|
const tables = [];
|
|
128
132
|
const membershipTables = new Set(Object.values(schema).filter((table) => table && table.member).map((table) => table.member.m));
|
|
129
|
-
|
|
133
|
+
const grantTables = /* @__PURE__ */ new Set();
|
|
134
|
+
for (const table of Object.values(schema)) if (table?.policy) policyMembershipTables(table.policy, membershipTables, grantTables);
|
|
135
|
+
for (const name of grantTables) {
|
|
136
|
+
const grant = schema[name];
|
|
137
|
+
if (!grant || intents[name] !== "scoped" || scopes[name] !== "user_id" || grant.visitors || grant.policy || grant.member || grant.client?.links || grant.client?.intake || grant.client?.identity === "visitor") fail(`${name} owner grant authority`);
|
|
138
|
+
}
|
|
139
|
+
const grantRules = Object.entries(schema).flatMap(([name, table]) => table?.policy?.k === "a" && table.policy.p?.[1]?.k === "m" && table.policy.p[1].m?.og === true ? [{ name, table, m: table.policy.p[1].m }] : []);
|
|
140
|
+
for (const rule of grantRules.filter((rule2) => rule2.m.g.length === 2)) {
|
|
141
|
+
const shares = schema[rule.m.m], row = rule.m.mg[1];
|
|
142
|
+
if (grantRules.filter((other) => other.m.m === rule.m.m).length !== 1 || rule.table.primaryKey !== rule.m.g[1] || !shares?.columns?.some((column) => column?.n === row) || Array.isArray(shares.client?.update) && shares.client.update.includes(row)) fail(`${rule.m.m} row grant authority`);
|
|
143
|
+
}
|
|
130
144
|
for (const name of Object.keys(schema).sort()) {
|
|
131
145
|
const table = schema[name];
|
|
132
146
|
record2(table, `table ${name}`);
|
|
@@ -802,7 +816,7 @@ interface SomewhereAppRoleGrant {
|
|
|
802
816
|
user_id: string;
|
|
803
817
|
role: SomewhereAppRole;
|
|
804
818
|
granted_by: string;
|
|
805
|
-
grant_source: 'platform_admin' | 'delegation';
|
|
819
|
+
grant_source: 'platform_admin' | 'delegation' | 'email_domain';
|
|
806
820
|
created_at: number;
|
|
807
821
|
}
|
|
808
822
|
|
|
@@ -871,7 +885,7 @@ type SomewhereGroupsResult<T> = { data: T; error: null } | { data: null; error:
|
|
|
871
885
|
*/
|
|
872
886
|
interface SomewhereGroups {
|
|
873
887
|
create(req: Request, input: { name: string }): Promise<SomewhereGroupsResult<SomewhereGroupDetail>>;
|
|
874
|
-
/** Also completes (and reports) up to
|
|
888
|
+
/** Also completes (and reports) up to 10 of the signed-in user's invitations not yet confirmed at sign-in. */
|
|
875
889
|
list(req: Request, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereGroup> & { invitations: SomewhereGroupInvitationOutcome[] }>>;
|
|
876
890
|
get(req: Request, groupId: string): Promise<SomewhereGroupsResult<SomewhereGroupDetail>>;
|
|
877
891
|
members(req: Request, groupId: string, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereGroupMember>>>;
|
|
@@ -895,7 +909,7 @@ interface SomewhereGroups {
|
|
|
895
909
|
appRoles: {
|
|
896
910
|
list(req: Request, userId?: string, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereAppRoleGrant>>>;
|
|
897
911
|
grant(req: Request, userId: string, role: SomewhereAppRole): Promise<SomewhereGroupsResult<SomewhereAppRoleGrant>>;
|
|
898
|
-
revoke(req: Request, userId: string, role: SomewhereAppRole): Promise<SomewhereGroupsResult<{ revoked: boolean }>>;
|
|
912
|
+
revoke(req: Request, userId: string, role: SomewhereAppRole): Promise<SomewhereGroupsResult<{ revoked: boolean; held_by?: 'email_domain'; domain?: string; message?: string }>>;
|
|
899
913
|
};
|
|
900
914
|
}
|
|
901
915
|
|
|
@@ -1424,35 +1438,31 @@ interface SomewhereRuntimeAuthModeration {
|
|
|
1424
1438
|
user: { id: string; email: string; role: SomewhereAuthRole };
|
|
1425
1439
|
}>;
|
|
1426
1440
|
}
|
|
1427
|
-
//
|
|
1441
|
+
// Email sign-in codes (tsk_8fe91eb5). Authenticator-app activation (reauthenticate*) answers 501 AUTH_FEATURE_UNAVAILABLE.
|
|
1442
|
+
type SomewhereMfaDelivery = 'sent' | 'pending' | 'unknown';
|
|
1443
|
+
interface SomewhereMfaCodeSent { challenge_id: string; delivery: SomewhereMfaDelivery; expires_in: number }
|
|
1444
|
+
type SomewhereMfaRecovery =
|
|
1445
|
+
| { removal_pending: true; removes_at: string; wait_seconds: number; notice_resent?: boolean }
|
|
1446
|
+
| { enrolled: false };
|
|
1428
1447
|
interface SomewhereRuntimeAuthMfa {
|
|
1429
1448
|
challenge(options: { mfa_token: string; code: string }): Promise<SomewhereAuthSession>;
|
|
1430
1449
|
challengeWithCookie(options: { mfa_token: string; code: string }): Promise<{ user: SomewhereAuthUser }>;
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
}>;
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
}>;
|
|
1447
|
-
reauthenticateWithCookie(req: Request, options: { enrollment_id: string; method: 'password'; password: string }):
|
|
1448
|
-
Promise<{ method: 'password'; activation_token: string; expires_in_seconds: number }>;
|
|
1449
|
-
reauthenticateWithCookie(req: Request, options: { enrollment_id: string; method: 'email' }):
|
|
1450
|
-
Promise<{ method: 'email'; challenge_id: string; expires_in_seconds: number; pending?: true }>;
|
|
1451
|
-
verifyReauthenticationWithCookie(req: Request, options: { enrollment_id: string; challenge_id: string; code: string }):
|
|
1452
|
-
Promise<{ method: 'email'; activation_token: string; expires_in_seconds: number }>;
|
|
1453
|
-
verifyWithCookie(req: Request, options: { enrollment_id: string; activation_token: string; code: string }):
|
|
1454
|
-
Promise<{ enabled: true; backup_codes: string[] }>;
|
|
1455
|
-
unenrollWithCookie(req: Request, options: { code: string }): Promise<{ enabled: false }>;
|
|
1450
|
+
resend(options: { mfa_token: string }): Promise<{ sent: boolean; delivery: SomewhereMfaDelivery; expires_in: number }>;
|
|
1451
|
+
enroll(options: { token: string }): Promise<SomewhereMfaCodeSent | { enrolled: true; method: 'email' }>;
|
|
1452
|
+
verify(options: { token: string; challenge_id: string; code: string }): Promise<{ enrolled: true; method: 'email' }>;
|
|
1453
|
+
unenroll(options: { token: string; password: string }): Promise<SomewhereMfaCodeSent | { enrolled: false }>;
|
|
1454
|
+
unenroll(options: { token: string; challenge_id: string; code: string }): Promise<{ enrolled: false }>;
|
|
1455
|
+
recover(options: { email: string; password: string }): Promise<SomewhereMfaRecovery>;
|
|
1456
|
+
cancelRecovery(options: { token: string }): Promise<{ outcome: 'cancelled' | 'already_closed' | 'completed'; message: string }>;
|
|
1457
|
+
reauthenticate(options: { token: string; enrollment_id: string; method: 'password' | 'email'; password?: string }): Promise<never>;
|
|
1458
|
+
verifyReauthentication(options: { token: string; enrollment_id: string; challenge_id: string; code: string }): Promise<never>;
|
|
1459
|
+
// The same steps for the request's own session (cookie or Bearer). Turning codes on or off ends no session.
|
|
1460
|
+
enrollWithCookie(req: Request): Promise<SomewhereMfaCodeSent | { enrolled: true; method: 'email' }>;
|
|
1461
|
+
verifyWithCookie(req: Request, options: { challenge_id: string; code: string }): Promise<{ enrolled: true; method: 'email' }>;
|
|
1462
|
+
unenrollWithCookie(req: Request, options: { password: string }): Promise<SomewhereMfaCodeSent | { enrolled: false }>;
|
|
1463
|
+
unenrollWithCookie(req: Request, options: { challenge_id: string; code: string }): Promise<{ enrolled: false }>;
|
|
1464
|
+
reauthenticateWithCookie(req: Request, options: { enrollment_id: string; method: 'password' | 'email'; password?: string }): Promise<never>;
|
|
1465
|
+
verifyReauthenticationWithCookie(req: Request, options: { enrollment_id: string; challenge_id: string; code: string }): Promise<never>;
|
|
1456
1466
|
}
|
|
1457
1467
|
interface SomewhereRuntimeAuth {
|
|
1458
1468
|
signup(options: SomewhereAuthSignupOptions): Promise<SomewhereAuthIssuedSession>;
|
|
@@ -4044,6 +4054,7 @@ var require_schema_types = __commonJS({
|
|
|
4044
4054
|
interface Scope { readonly __somewhereScope: unique symbol }
|
|
4045
4055
|
interface OwnerScope extends Scope { readonly __somewhereOwner: unique symbol }
|
|
4046
4056
|
interface MemberScope extends Scope { readonly __somewhereMember: unique symbol }
|
|
4057
|
+
interface GrantScope extends Scope { readonly __somewhereGrant: unique symbol }
|
|
4047
4058
|
interface GroupScope extends Scope { readonly __somewhereGroup: unique symbol }
|
|
4048
4059
|
interface AppRoleScope extends Scope { readonly __somewhereAppRole: unique symbol }
|
|
4049
4060
|
interface AnyOfScope extends Scope { readonly __somewhereAnyOf: unique symbol }
|
|
@@ -4102,8 +4113,9 @@ function removed(): SomewhereSchemaDeclaration.ColumnMarker;
|
|
|
4102
4113
|
function removedTable(): SomewhereSchemaDeclaration.TableMarker;
|
|
4103
4114
|
function exported(): SomewhereSchemaDeclaration.TableMarker;
|
|
4104
4115
|
`;
|
|
4105
|
-
var policySignatures = `function
|
|
4106
|
-
function anyOf(other: SomewhereSchemaDeclaration.
|
|
4116
|
+
var policySignatures = `function grant(options: { table: string; grantee: string; row?: string; operations: readonly ['read'] | readonly ['read', 'update'] }): SomewhereSchemaDeclaration.GrantScope;
|
|
4117
|
+
function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, other: SomewhereSchemaDeclaration.GrantScope | SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope): SomewhereSchemaDeclaration.AnyOfScope;
|
|
4118
|
+
function anyOf(other: SomewhereSchemaDeclaration.GrantScope | SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.AnyOfScope;
|
|
4107
4119
|
function allOf(gate: SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, who: SomewhereSchemaDeclaration.OwnerScope | SomewhereSchemaDeclaration.AnyOfScope): SomewhereSchemaDeclaration.Scope;
|
|
4108
4120
|
function parent(options: { via: string; table?: string }): SomewhereSchemaDeclaration.Scope;
|
|
4109
4121
|
function group(options?: { column?: string; roles?: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.GroupScope;
|
|
@@ -4306,6 +4318,9 @@ function isCanonicalBigintText(value) {
|
|
|
4306
4318
|
}
|
|
4307
4319
|
|
|
4308
4320
|
// worker/src/utils/db-schema-deploy/client-permissions.ts
|
|
4321
|
+
function tableAllowsClientWrites(table) {
|
|
4322
|
+
return Boolean(table.client && (table.client.create !== null || table.client.update !== null || table.client.delete || table.client.intake !== void 0));
|
|
4323
|
+
}
|
|
4309
4324
|
function parseClientPermissions(value, columns, scope) {
|
|
4310
4325
|
const fail = (message) => ({ ok: false, message });
|
|
4311
4326
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -4839,6 +4854,23 @@ function validateBindings(input, tableInfo, errors) {
|
|
|
4839
4854
|
}
|
|
4840
4855
|
}
|
|
4841
4856
|
|
|
4857
|
+
// worker/src/utils/authority/role-domain-map.ts
|
|
4858
|
+
var ROLE_NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
4859
|
+
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]$/;
|
|
4860
|
+
function validRoleDomainMap(value) {
|
|
4861
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.entries(value).every(([role, domain]) => ROLE_NAME_PATTERN.test(role) && typeof domain === "string" && EMAIL_DOMAIN_PATTERN.test(domain));
|
|
4862
|
+
}
|
|
4863
|
+
var ROLE_DOMAIN_MAP_JS = `
|
|
4864
|
+
function __sw_structRoleDomains(value) {
|
|
4865
|
+
if (!value || typeof value !== 'object' || __sw_arrayIsArray(value)) return false;
|
|
4866
|
+
const roles = __sw_keys(value);
|
|
4867
|
+
for (let i = 0; i < roles.length; i++) {
|
|
4868
|
+
const domain = value[roles[i]];
|
|
4869
|
+
if (!__sw_reMatch(${ROLE_NAME_PATTERN}, roles[i]) || typeof domain !== 'string' || !__sw_reMatch(${EMAIL_DOMAIN_PATTERN}, domain)) return false;
|
|
4870
|
+
}
|
|
4871
|
+
return true;
|
|
4872
|
+
}`;
|
|
4873
|
+
|
|
4842
4874
|
// worker/src/utils/authority/scope-enforcement.ts
|
|
4843
4875
|
var SAFE_SCOPE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4844
4876
|
var GROUP_ROLE_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
@@ -4859,7 +4891,9 @@ function validBakedMemberScope(value) {
|
|
|
4859
4891
|
const operations = member.o;
|
|
4860
4892
|
const operationOrder = ["read", "create", "update", "delete"];
|
|
4861
4893
|
const validOperations = operations === void 0 || Array.isArray(operations) && JSON.stringify(operations) === JSON.stringify(operationOrder.filter((operation) => operations.includes(operation))) && (operations.includes("read") || !operations.includes("update") && !operations.includes("delete"));
|
|
4862
|
-
|
|
4894
|
+
const g = member.g, mg = member.mg;
|
|
4895
|
+
const grant = memberKeys === "g,m,mg,o,og,u" && member.og === true && Array.isArray(g) && Array.isArray(mg) && g[0] === "user_id" && mg[0] === "user_id" && (g.length === 1 || g.length === 2 && g[1] !== "user_id" && mg[1] !== "user_id" && mg[1] !== member.u) && member.u !== "user_id" && (JSON.stringify(operations) === '["read"]' || JSON.stringify(operations) === '["read","update"]');
|
|
4896
|
+
return (memberKeys === "g,m,mg,u" || memberKeys === "g,m,mg,o,u" || grant) && validOperations && Array.isArray(member.g) && member.g.length > 0 && member.g.every(ident) && Array.isArray(member.mg) && member.mg.length === member.g.length && member.mg.every(ident) && ident(member.m) && ident(member.u);
|
|
4863
4897
|
}
|
|
4864
4898
|
var MAX_PARENT_EDGES = 4;
|
|
4865
4899
|
function pinShape(policy) {
|
|
@@ -4873,7 +4907,7 @@ function sameAllOfRelation(gate, x) {
|
|
|
4873
4907
|
if (x.k !== "m" || gate.k !== "m") return false;
|
|
4874
4908
|
const a = gate.m;
|
|
4875
4909
|
const b = x.m;
|
|
4876
|
-
return lower(a.m) === lower(b.m) && lower(a.u) === lower(b.u) && a.g.length === b.g.length && a.g.every((column, i) => lower(column) === lower(b.g[i])) && a.mg.length === b.mg.length && a.mg.every((column, i) => lower(column) === lower(b.mg[i]));
|
|
4910
|
+
return !a.og && !b.og && lower(a.m) === lower(b.m) && lower(a.u) === lower(b.u) && a.g.length === b.g.length && a.g.every((column, i) => lower(column) === lower(b.g[i])) && a.mg.length === b.mg.length && a.mg.every((column, i) => lower(column) === lower(b.mg[i]));
|
|
4877
4911
|
}
|
|
4878
4912
|
function validBakedPolicy(value, root = true, edges = 0) {
|
|
4879
4913
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
@@ -4886,7 +4920,7 @@ function validBakedPolicy(value, root = true, edges = 0) {
|
|
|
4886
4920
|
}
|
|
4887
4921
|
if (node.k === "a") {
|
|
4888
4922
|
const second = node.p?.[1]?.k;
|
|
4889
|
-
return exactKeys(node, ["k", "p"]) && Array.isArray(node.p) && node.p.length === 2 && node.p[0]?.k === "o" && validBakedPolicy(node.p[0], false) && (second === "m" ? validBakedPolicy(node.p[1], false) : (second === "g" || second === "r") && validBakedPolicy(node.p[1], true));
|
|
4923
|
+
return exactKeys(node, ["k", "p"]) && Array.isArray(node.p) && node.p.length === 2 && node.p[0]?.k === "o" && validBakedPolicy(node.p[0], false) && (!(node.p[1]?.k === "m" && node.p[1].m?.og) || node.p[0].c === "user_id") && (second === "m" ? validBakedPolicy(node.p[1], false) : (second === "g" || second === "r") && validBakedPolicy(node.p[1], true));
|
|
4890
4924
|
}
|
|
4891
4925
|
if (root && node.k === "n") {
|
|
4892
4926
|
if (!ALLOF_POLICY_ADMITTED || !exactKeys(node, ["k", "g", "w"])) return false;
|
|
@@ -4894,7 +4928,7 @@ function validBakedPolicy(value, root = true, edges = 0) {
|
|
|
4894
4928
|
const who = node.w;
|
|
4895
4929
|
if (!gate || !who) return false;
|
|
4896
4930
|
const gateMember = gate.m;
|
|
4897
|
-
const gateValid = gate.k === "m" ? validBakedPolicy(gate, false) && gateMember?.o !== void 0 : (gate.k === "g" || gate.k === "r") && validBakedPolicy(gate, true);
|
|
4931
|
+
const gateValid = gate.k === "m" ? validBakedPolicy(gate, false) && gateMember?.o !== void 0 && !gateMember.og : (gate.k === "g" || gate.k === "r") && validBakedPolicy(gate, true);
|
|
4898
4932
|
if (!gateValid) return false;
|
|
4899
4933
|
if (who.k === "o") return validBakedPolicy(who, false);
|
|
4900
4934
|
const whoBranches = who.p;
|
|
@@ -4912,7 +4946,9 @@ function validBakedPolicy(value, root = true, edges = 0) {
|
|
|
4912
4946
|
return exactKeys(node, ["k", "c", "roles", "generation"]) && ident(node.c) && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
|
|
4913
4947
|
}
|
|
4914
4948
|
if (root && node.k === "r") {
|
|
4915
|
-
|
|
4949
|
+
const domains = node.d;
|
|
4950
|
+
const validDomains = domains === void 0 || validRoleDomainMap(domains);
|
|
4951
|
+
return (exactKeys(node, ["k", "roles", "generation"]) || exactKeys(node, ["k", "roles", "generation", "d"])) && validDomains && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
|
|
4916
4952
|
}
|
|
4917
4953
|
return false;
|
|
4918
4954
|
}
|
|
@@ -5033,7 +5069,8 @@ function bakedPolicyFromDeclared(value, depth = 0, edges = 0, underParent = fals
|
|
|
5033
5069
|
if (depth !== 0 && !underParent || node.policies[0]?.kind !== "owner" || second !== "member" && second !== "group" && second !== "app_role") return null;
|
|
5034
5070
|
const left = bakedPolicyFromDeclared(node.policies[0], depth + 1);
|
|
5035
5071
|
const right = bakedPolicyFromDeclared(node.policies[1], depth + 1, edges, second !== "member");
|
|
5036
|
-
|
|
5072
|
+
const baked = left && right ? { k: "a", p: [left, right] } : null;
|
|
5073
|
+
return baked && validBakedPolicy(pinShape(baked), true, edges) ? baked : null;
|
|
5037
5074
|
}
|
|
5038
5075
|
if (node.kind === "all_of") {
|
|
5039
5076
|
if (depth !== 0 && !underParent) return null;
|
|
@@ -5125,7 +5162,143 @@ function bakedMemberFromDeclared(scope, explicitOperations = false) {
|
|
|
5125
5162
|
if (JSON.stringify(operations) !== JSON.stringify(allowed.filter((operation) => operations.includes(operation))) || !operations.includes("read") && (operations.includes("update") || operations.includes("delete"))) return null;
|
|
5126
5163
|
o = operations.length === allowed.length && !explicitOperations ? void 0 : [...operations];
|
|
5127
5164
|
}
|
|
5128
|
-
|
|
5165
|
+
if (s.ownerGrant !== void 0 && s.ownerGrant !== true) return null;
|
|
5166
|
+
const member = { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg, ...o !== void 0 ? { o } : {}, ...s.ownerGrant ? { og: true } : {} };
|
|
5167
|
+
return validBakedMemberScope(member) ? member : null;
|
|
5168
|
+
}
|
|
5169
|
+
|
|
5170
|
+
// worker/src/utils/db-schema-deploy/extract-schema-grants.ts
|
|
5171
|
+
function readGrantScope(r, table, line, fail) {
|
|
5172
|
+
r.expectPunct("(", "after grant");
|
|
5173
|
+
r.expectPunct("{", "in grant options");
|
|
5174
|
+
let membership, grantee, row;
|
|
5175
|
+
let operations;
|
|
5176
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5177
|
+
while (!r.tryPunct("}")) {
|
|
5178
|
+
const key = r.readKey("in grant options");
|
|
5179
|
+
if (seen.has(key.name)) fail(key.line, `Duplicate grant option "${key.name}".`);
|
|
5180
|
+
seen.add(key.name);
|
|
5181
|
+
r.expectPunct(":", "after grant option");
|
|
5182
|
+
if (key.name === "table" || key.name === "grantee" || key.name === "row") {
|
|
5183
|
+
const value = r.readLiteral("in grant options");
|
|
5184
|
+
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.");
|
|
5185
|
+
if (key.name === "table") membership = value.toLowerCase();
|
|
5186
|
+
else if (key.name === "grantee") grantee = value.toLowerCase();
|
|
5187
|
+
else row = value.toLowerCase();
|
|
5188
|
+
} else if (key.name === "operations") {
|
|
5189
|
+
const ops = [];
|
|
5190
|
+
r.expectPunct("[", "in grant operations");
|
|
5191
|
+
while (!r.tryPunct("]")) {
|
|
5192
|
+
const op = r.readLiteral("in grant operations");
|
|
5193
|
+
if (op !== "read" && op !== "update") fail(key.line, "grant operations allow only read and update.");
|
|
5194
|
+
if (ops.includes(op)) fail(key.line, "Duplicate grant operation.");
|
|
5195
|
+
ops.push(op);
|
|
5196
|
+
if (!r.tryPunct(",")) {
|
|
5197
|
+
r.expectPunct("]", "closing grant operations");
|
|
5198
|
+
break;
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
if (!ops.includes("read")) fail(key.line, "grant operations require read; update alone is not allowed.");
|
|
5202
|
+
operations = ops.includes("update") ? ["read", "update"] : ["read"];
|
|
5203
|
+
} else fail(key.line, `Unknown grant option "${key.name}". Allowed: table, grantee, row, operations.`);
|
|
5204
|
+
if (!r.tryPunct(",")) {
|
|
5205
|
+
r.expectPunct("}", "closing grant options");
|
|
5206
|
+
break;
|
|
5207
|
+
}
|
|
5208
|
+
}
|
|
5209
|
+
r.tryPunct(",");
|
|
5210
|
+
r.expectPunct(")", "closing grant");
|
|
5211
|
+
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.`);
|
|
5212
|
+
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.`);
|
|
5213
|
+
return { kind: "member", group: ["user_id"], membership, memberUser: grantee, memberGroup: row === void 0 ? ["user_id"] : ["user_id", row], operations, ownerGrant: true };
|
|
5214
|
+
}
|
|
5215
|
+
function resolveGrantRowKeys(scope, key, table, line, fail) {
|
|
5216
|
+
const visit = (node) => {
|
|
5217
|
+
if (node.kind === "member" && node.ownerGrant && node.memberGroup.length === 2 && node.group.length === 1) {
|
|
5218
|
+
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.`);
|
|
5219
|
+
node.group = ["user_id", key.toLowerCase()];
|
|
5220
|
+
} else if (node.kind === "any_of") node.policies.forEach(visit);
|
|
5221
|
+
else if (node.kind === "all_of") {
|
|
5222
|
+
visit(node.gate);
|
|
5223
|
+
visit(node.who);
|
|
5224
|
+
}
|
|
5225
|
+
};
|
|
5226
|
+
if (scope.kind === "policy") visit(scope.policy);
|
|
5227
|
+
}
|
|
5228
|
+
function validateDeclaredMemberships(tables, tableByName, knownColumns, errors) {
|
|
5229
|
+
const memberPolicies = (scope) => {
|
|
5230
|
+
const out = [];
|
|
5231
|
+
const visit = (node) => {
|
|
5232
|
+
if (node.kind === "member") out.push(node);
|
|
5233
|
+
else if (node.kind === "any_of") node.policies.forEach(visit);
|
|
5234
|
+
else if (node.kind === "all_of") {
|
|
5235
|
+
visit(node.gate);
|
|
5236
|
+
visit(node.who);
|
|
5237
|
+
}
|
|
5238
|
+
};
|
|
5239
|
+
if (scope.kind === "member") out.push(scope);
|
|
5240
|
+
else if (scope.kind === "policy") visit(scope.policy);
|
|
5241
|
+
return out;
|
|
5242
|
+
};
|
|
5243
|
+
for (const t of tables) {
|
|
5244
|
+
for (const sc of memberPolicies(t.scope)) {
|
|
5245
|
+
const own = knownColumns(t);
|
|
5246
|
+
for (const g of sc.group) {
|
|
5247
|
+
if (!own.has(g)) {
|
|
5248
|
+
errors.push(
|
|
5249
|
+
`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.`
|
|
5250
|
+
);
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
const membershipTable = tableByName.get(sc.membership);
|
|
5254
|
+
if (!membershipTable) {
|
|
5255
|
+
errors.push(
|
|
5256
|
+
`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.`
|
|
5257
|
+
);
|
|
5258
|
+
continue;
|
|
5259
|
+
}
|
|
5260
|
+
const membershipCols = knownColumns(membershipTable);
|
|
5261
|
+
if (sc.ownerGrant) {
|
|
5262
|
+
if (membershipTable.scope.kind !== "owner" || membershipTable.scope.column !== "user_id" || membershipTable.scope.visitors) {
|
|
5263
|
+
errors.push(`Grant table "${sc.membership}" must use owner() with managed user_id and authenticated owners.`);
|
|
5264
|
+
}
|
|
5265
|
+
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.`);
|
|
5266
|
+
const grantee = membershipTable.columns.find((column) => column.name === sc.memberUser);
|
|
5267
|
+
if (!grantee || grantee.helper !== "text" || grantee.nullable) errors.push(`Grant grantee "${sc.membership}"."${sc.memberUser}" must be a required text column.`);
|
|
5268
|
+
if (sc.memberGroup.length === 2) {
|
|
5269
|
+
const row = membershipTable.columns.find((column) => column.name === sc.memberGroup[1]);
|
|
5270
|
+
const key = t.columns.find((column) => column.name === sc.group[1]);
|
|
5271
|
+
const keyHelper = key?.helper === "id" ? key.uuid ? "text" : "integer" : null;
|
|
5272
|
+
if (!row || row.nullable || row.helper !== keyHelper) {
|
|
5273
|
+
errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" must be a required ${keyHelper ?? "id-typed"} column holding a "${t.name}" id.`);
|
|
5274
|
+
} else if (row.hasDefault || row.references !== t.name || row.onDelete !== "cascade") {
|
|
5275
|
+
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' }).`);
|
|
5276
|
+
}
|
|
5277
|
+
if (membershipTable.client?.update?.includes(sc.memberGroup[1])) {
|
|
5278
|
+
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.`);
|
|
5279
|
+
}
|
|
5280
|
+
const users = tables.filter((other) => memberPolicies(other.scope).some((m) => m.ownerGrant && m.membership === sc.membership));
|
|
5281
|
+
if (users.length !== 1 || memberPolicies(t.scope).filter((m) => m.ownerGrant && m.membership === sc.membership).length !== 1) {
|
|
5282
|
+
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.`);
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
} else if (tableAllowsClientWrites(membershipTable)) {
|
|
5286
|
+
errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
|
|
5287
|
+
}
|
|
5288
|
+
if (!membershipCols.has(sc.memberUser)) {
|
|
5289
|
+
errors.push(
|
|
5290
|
+
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
|
|
5291
|
+
);
|
|
5292
|
+
}
|
|
5293
|
+
for (const mg of sc.ownerGrant ? [] : sc.memberGroup) {
|
|
5294
|
+
if (!membershipCols.has(mg)) {
|
|
5295
|
+
errors.push(
|
|
5296
|
+
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
|
|
5297
|
+
);
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5129
5302
|
}
|
|
5130
5303
|
|
|
5131
5304
|
// worker/src/utils/db-schema-deploy/schema-constants.ts
|
|
@@ -6219,8 +6392,9 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
|
|
|
6219
6392
|
}
|
|
6220
6393
|
|
|
6221
6394
|
// worker/src/utils/db-schema-deploy/extract-schema-roles.ts
|
|
6395
|
+
var EMAIL_DOMAIN = EMAIL_DOMAIN_PATTERN;
|
|
6222
6396
|
var GROUP_KEYS = ["roles", "adminGrantableRoles"];
|
|
6223
|
-
var APP_ROLE_KEYS = ["grantableBy"];
|
|
6397
|
+
var APP_ROLE_KEYS = ["grantableBy", "emailDomain"];
|
|
6224
6398
|
function readStringList2(r, fail, ctx) {
|
|
6225
6399
|
const open = r.peek();
|
|
6226
6400
|
if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted role names, such as ['viewer'].`);
|
|
@@ -6264,7 +6438,7 @@ function readAppRoles(r, fail) {
|
|
|
6264
6438
|
const key = r.readKey("for an app role name");
|
|
6265
6439
|
if (out.some((role2) => role2.name === key.name)) fail(key.line, `app role "${key.name}" is declared twice.`);
|
|
6266
6440
|
r.expectPunct(":", `after app role "${key.name}"`);
|
|
6267
|
-
r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...] }`);
|
|
6441
|
+
r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...], emailDomain: '...' }`);
|
|
6268
6442
|
const role = { name: key.name, grantableBy: [] };
|
|
6269
6443
|
const seen = /* @__PURE__ */ new Set();
|
|
6270
6444
|
while (!r.tryPunct("}")) {
|
|
@@ -6275,7 +6449,16 @@ function readAppRoles(r, fail) {
|
|
|
6275
6449
|
if (seen.has(opt.name)) fail(opt.line, `app role "${key.name}" declares ${opt.name} twice.`);
|
|
6276
6450
|
seen.add(opt.name);
|
|
6277
6451
|
r.expectPunct(":", `after appRoles.${key.name}.${opt.name}`);
|
|
6278
|
-
|
|
6452
|
+
if (opt.name === "emailDomain") {
|
|
6453
|
+
const t = r.next();
|
|
6454
|
+
if (t.kind !== "string") fail(t.line, `appRoles.${key.name}.emailDomain must be one quoted domain, such as 'example.com'.`);
|
|
6455
|
+
if (!EMAIL_DOMAIN.test(t.value)) {
|
|
6456
|
+
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.`);
|
|
6457
|
+
}
|
|
6458
|
+
role.emailDomain = t.value;
|
|
6459
|
+
} else {
|
|
6460
|
+
role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
|
|
6461
|
+
}
|
|
6279
6462
|
if (!r.tryPunct(",")) {
|
|
6280
6463
|
r.expectPunct("}", `closing app role "${key.name}"`);
|
|
6281
6464
|
break;
|
|
@@ -6295,18 +6478,31 @@ function canonicalRoleVocabulary(groups, appRoles) {
|
|
|
6295
6478
|
v: 1,
|
|
6296
6479
|
groupRoles: sorted2(groups?.roles ?? []),
|
|
6297
6480
|
adminGrantableGroupRoles: sorted2(groups?.adminGrantableRoles ?? []),
|
|
6298
|
-
appRoles: [...appRoles ?? []].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((role) => ({ name: role.name, grantableBy: sorted2(role.grantableBy) }))
|
|
6481
|
+
appRoles: [...appRoles ?? []].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((role) => ({ name: role.name, grantableBy: sorted2(role.grantableBy), ...role.emailDomain ? { emailDomain: role.emailDomain } : {} }))
|
|
6299
6482
|
};
|
|
6300
6483
|
}
|
|
6301
6484
|
function validateDeclaredRoles(groups, appRoles) {
|
|
6302
6485
|
const delegation = /* @__PURE__ */ Object.create(null);
|
|
6303
6486
|
for (const role of appRoles ?? []) delegation[role.name] = role.grantableBy;
|
|
6304
|
-
return roleVocabularyErrors({
|
|
6487
|
+
return [...emailDomainRoleErrors(appRoles ?? []), ...roleVocabularyErrors({
|
|
6305
6488
|
groupRoles: groups?.roles ?? [],
|
|
6306
6489
|
adminGrantableGroupRoles: groups?.adminGrantableRoles ?? [],
|
|
6307
6490
|
appRoles: (appRoles ?? []).map((role) => role.name),
|
|
6308
6491
|
appRoleDelegation: delegation
|
|
6309
|
-
});
|
|
6492
|
+
})];
|
|
6493
|
+
}
|
|
6494
|
+
function emailDomainRoleErrors(appRoles) {
|
|
6495
|
+
const errors = [];
|
|
6496
|
+
const byDomain = new Set(appRoles.filter((role) => role.emailDomain !== void 0).map((role) => role.name));
|
|
6497
|
+
for (const role of appRoles) {
|
|
6498
|
+
if (role.emailDomain !== void 0 && !EMAIL_DOMAIN.test(role.emailDomain)) errors.push(`appRoles.${role.name}.emailDomain is not one lowercase domain.`);
|
|
6499
|
+
for (const grantor of role.grantableBy) {
|
|
6500
|
+
if (byDomain.has(grantor)) {
|
|
6501
|
+
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.`);
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
return errors;
|
|
6310
6506
|
}
|
|
6311
6507
|
|
|
6312
6508
|
// worker/src/utils/db-schema-deploy/extract-schema-options.ts
|
|
@@ -6396,6 +6592,7 @@ var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
|
|
|
6396
6592
|
...TABLE_MARKER_HELPERS,
|
|
6397
6593
|
"schema",
|
|
6398
6594
|
"table",
|
|
6595
|
+
"grant",
|
|
6399
6596
|
"removed",
|
|
6400
6597
|
"hasMany",
|
|
6401
6598
|
"belongsTo",
|
|
@@ -6510,10 +6707,10 @@ function tokenize(source) {
|
|
|
6510
6707
|
var Reader = class {
|
|
6511
6708
|
constructor(tokens, options = {}) {
|
|
6512
6709
|
this.tokens = tokens;
|
|
6513
|
-
this.bareIdIsText = !options.legacyIntegerIdDefault;
|
|
6710
|
+
this.bareIdIsText = (table) => !options.legacyIntegerIdDefault && !options.integerKeyTables?.has(table.toLowerCase());
|
|
6514
6711
|
}
|
|
6515
6712
|
pos = 0;
|
|
6516
|
-
/** What a bare id() declares: a random text key, or
|
|
6713
|
+
/** What a bare id() on this table declares: a random text key, or an integer (ExtractSchemaOptions). */
|
|
6517
6714
|
bareIdIsText;
|
|
6518
6715
|
peek() {
|
|
6519
6716
|
return this.tokens[this.pos];
|
|
@@ -6681,7 +6878,7 @@ function readColumn(r, tableName, rawName, line) {
|
|
|
6681
6878
|
const col = {
|
|
6682
6879
|
name,
|
|
6683
6880
|
helper,
|
|
6684
|
-
uuid: helper === "id" && r.bareIdIsText,
|
|
6881
|
+
uuid: helper === "id" && r.bareIdIsText(tableName),
|
|
6685
6882
|
nullable: false,
|
|
6686
6883
|
hasDefault: false,
|
|
6687
6884
|
default: null,
|
|
@@ -6918,7 +7115,7 @@ function readPolicyMember(r, tableName, line) {
|
|
|
6918
7115
|
if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
|
|
6919
7116
|
return scope;
|
|
6920
7117
|
}
|
|
6921
|
-
var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), group() or appRole().";
|
|
7118
|
+
var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), grant(), group() or appRole().";
|
|
6922
7119
|
function readAnyOfBranch(r, tableName, which) {
|
|
6923
7120
|
const tok = r.expectIdent(`for the ${which} policy in anyOf() on table "${tableName}"`);
|
|
6924
7121
|
const failAt = (at, message) => {
|
|
@@ -6926,6 +7123,7 @@ function readAnyOfBranch(r, tableName, which) {
|
|
|
6926
7123
|
};
|
|
6927
7124
|
if (tok.value === "owner") return readPolicyOwner(r, tableName, tok.line);
|
|
6928
7125
|
if (tok.value === "member") return readPolicyMember(r, tableName, tok.line);
|
|
7126
|
+
if (tok.value === "grant") return readGrantScope(r, tableName, tok.line, failAt);
|
|
6929
7127
|
if (tok.value === "group") return readGroupScope(r, tableName, tok.line, failAt);
|
|
6930
7128
|
if (tok.value === "appRole") return readAppRoleScope(r, tableName, tok.line, failAt);
|
|
6931
7129
|
throw new SchemaTsError(`line ${tok.line}: ${ANY_OF_FORMS}`);
|
|
@@ -6941,6 +7139,7 @@ function readAnyOfScope(r, tableName, line) {
|
|
|
6941
7139
|
if (owners.length !== 1) throw new SchemaTsError(`line ${line}: ${ANY_OF_FORMS}`);
|
|
6942
7140
|
const owner = owners[0];
|
|
6943
7141
|
const other = firstPolicy.kind === "owner" ? secondPolicy : firstPolicy;
|
|
7142
|
+
if (other.kind === "member" && other.ownerGrant && owner.column !== DEFAULT_OWNER_COLUMN) throw new SchemaTsError(`line ${line}: grant() requires owner() with managed user_id.`);
|
|
6944
7143
|
return { kind: "policy", policy: { kind: "any_of", policies: [owner, other] } };
|
|
6945
7144
|
}
|
|
6946
7145
|
var ALL_OF_FORMS = "allOf() takes a gate \u2014 member(), group() or appRole() \u2014 and then owner() or anyOf(owner(), ...).";
|
|
@@ -6968,6 +7167,7 @@ function readAllOfScope(r, tableName, line) {
|
|
|
6968
7167
|
r.tryPunct(",");
|
|
6969
7168
|
r.expectPunct(")", "closing allOf()");
|
|
6970
7169
|
const x = who.kind === "any_of" ? who.policies[1] : null;
|
|
7170
|
+
if (x?.kind === "member" && x.ownerGrant) throw new SchemaTsError(`line ${line}: grant() belongs in anyOf(owner(), grant()), not allOf().`);
|
|
6971
7171
|
for (const node of [gate, ...x ? [x] : []]) {
|
|
6972
7172
|
if (node.kind !== "member") continue;
|
|
6973
7173
|
if (!explicitMemberOperations.has(node)) {
|
|
@@ -7203,6 +7403,9 @@ function readTable(r, rawName, nameLine) {
|
|
|
7203
7403
|
if (key.name === "scope") {
|
|
7204
7404
|
if (scope) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "scope" twice.`);
|
|
7205
7405
|
scope = readScope(r, rawName);
|
|
7406
|
+
resolveGrantRowKeys(scope, idCols[0]?.name, rawName, key.line, (at, message) => {
|
|
7407
|
+
throw new SchemaTsError(`line ${at}: ${message}`);
|
|
7408
|
+
});
|
|
7206
7409
|
} else if (key.name === "indexes") {
|
|
7207
7410
|
if (indexEntries) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "indexes" twice.`);
|
|
7208
7411
|
indexEntries = readColumnLists(r, rawName, "indexes");
|
|
@@ -7434,55 +7637,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
7434
7637
|
};
|
|
7435
7638
|
resolveAppRoleScopes(tables, appRoles, errors);
|
|
7436
7639
|
resolveGroupsAndParents(tables, tableByName, privatePolicy, groupRoles, errors);
|
|
7437
|
-
|
|
7438
|
-
const out = [];
|
|
7439
|
-
const visit = (node) => {
|
|
7440
|
-
if (node.kind === "member") out.push(node);
|
|
7441
|
-
else if (node.kind === "any_of") node.policies.forEach(visit);
|
|
7442
|
-
else if (node.kind === "all_of") {
|
|
7443
|
-
visit(node.gate);
|
|
7444
|
-
visit(node.who);
|
|
7445
|
-
}
|
|
7446
|
-
};
|
|
7447
|
-
if (scope.kind === "member") out.push(scope);
|
|
7448
|
-
else if (scope.kind === "policy") visit(scope.policy);
|
|
7449
|
-
return out;
|
|
7450
|
-
};
|
|
7451
|
-
for (const t of tables) {
|
|
7452
|
-
for (const sc of memberPolicies(t.scope)) {
|
|
7453
|
-
const own = knownColumns(t);
|
|
7454
|
-
for (const g of sc.group) {
|
|
7455
|
-
if (!own.has(g)) {
|
|
7456
|
-
errors.push(
|
|
7457
|
-
`Table "${t.name}" is member-scoped on "${g}", but "${g}" is not one of its declared columns. The group-key columns must be real columns on the table.`
|
|
7458
|
-
);
|
|
7459
|
-
}
|
|
7460
|
-
}
|
|
7461
|
-
const membershipTable = tableByName.get(sc.membership);
|
|
7462
|
-
if (!membershipTable) {
|
|
7463
|
-
errors.push(
|
|
7464
|
-
`Table "${t.name}" is member-scoped through the membership table "${sc.membership}", which is not declared in db/schema.ts. The membership table must be a managed table in the same schema.`
|
|
7465
|
-
);
|
|
7466
|
-
continue;
|
|
7467
|
-
}
|
|
7468
|
-
const membershipCols = knownColumns(membershipTable);
|
|
7469
|
-
if (membershipTable.client && (membershipTable.client.create !== null || membershipTable.client.update !== null || membershipTable.client.delete || membershipTable.client.intake !== void 0)) {
|
|
7470
|
-
errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
|
|
7471
|
-
}
|
|
7472
|
-
if (!membershipCols.has(sc.memberUser)) {
|
|
7473
|
-
errors.push(
|
|
7474
|
-
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
|
|
7475
|
-
);
|
|
7476
|
-
}
|
|
7477
|
-
for (const mg of sc.memberGroup) {
|
|
7478
|
-
if (!membershipCols.has(mg)) {
|
|
7479
|
-
errors.push(
|
|
7480
|
-
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
|
|
7481
|
-
);
|
|
7482
|
-
}
|
|
7483
|
-
}
|
|
7484
|
-
}
|
|
7485
|
-
}
|
|
7640
|
+
validateDeclaredMemberships(tables, tableByName, knownColumns, errors);
|
|
7486
7641
|
validateDeclaredRelations(tables, tableByName, knownColumns, (table) => removedTables.includes(table) ? "removedTable()" : exportedTables.includes(table) ? "exported()" : null, errors);
|
|
7487
7642
|
validateRowLinks(tables, tableByName, errors);
|
|
7488
7643
|
validateIntake(tables, tableByName, errors);
|
|
@@ -7490,7 +7645,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
7490
7645
|
errors.push(...fileErrors);
|
|
7491
7646
|
validateFileCollections(fileCollections, (name) => {
|
|
7492
7647
|
const t = tableByName.get(name);
|
|
7493
|
-
return t ? { columns: knownColumns(t), clientWrites:
|
|
7648
|
+
return t ? { columns: knownColumns(t), clientWrites: tableAllowsClientWrites(t), parentTarget: fileParentTarget(t) } : null;
|
|
7494
7649
|
}, errors);
|
|
7495
7650
|
}
|
|
7496
7651
|
if (groupRoles || appRoles) errors.push(...validateDeclaredRoles(groupRoles, appRoles));
|
|
@@ -7601,7 +7756,10 @@ function declaredRolesFromSource(files) {
|
|
|
7601
7756
|
if (source === void 0) return void 0;
|
|
7602
7757
|
const parsed = extractSchemaTs(source);
|
|
7603
7758
|
if (!parsed.ok || !parsed.declaration.groups && !parsed.declaration.appRoles) return void 0;
|
|
7604
|
-
return JSON.stringify(canonicalRoleVocabulary(
|
|
7759
|
+
return JSON.stringify(canonicalRoleVocabulary(
|
|
7760
|
+
parsed.declaration.groups,
|
|
7761
|
+
parsed.declaration.appRoles?.map((role) => ({ name: role.name, grantableBy: role.grantableBy }))
|
|
7762
|
+
));
|
|
7605
7763
|
}
|
|
7606
7764
|
|
|
7607
7765
|
// declared-data-vendor-entry.js
|
|
@@ -7619,8 +7777,8 @@ var checkerContract = __toESM(require_checker_contract());
|
|
|
7619
7777
|
var import_runtime_types = __toESM(require_runtime_types());
|
|
7620
7778
|
var import_generated_declarations = __toESM(require_generated_declarations());
|
|
7621
7779
|
var import_generated_declarations_render = __toESM(require_generated_declarations_render());
|
|
7622
|
-
function referenceCheckDeclaredTables(graphFiles) {
|
|
7623
|
-
const authority = schemaAuthorityFromSource(graphFiles);
|
|
7780
|
+
function referenceCheckDeclaredTables(graphFiles, schemaOptions = {}) {
|
|
7781
|
+
const authority = schemaAuthorityFromSource(graphFiles, schemaOptions);
|
|
7624
7782
|
return authority ? (0, import_runtime_types.declaredTablesDeclaration)(authority) : void 0;
|
|
7625
7783
|
}
|
|
7626
7784
|
function referenceCheckDeclarations(graphFiles) {
|