@somewhere-cloud/cli 0.38.16 → 0.38.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/runtime/DECLARED-DATA-VENDOR.json +14 -11
- package/runtime/PLATFORM-SOURCE.json +4 -0
- package/runtime/SOURCE-LINT-VENDOR.json +30 -27
- package/runtime/declared-data.cjs +295 -115
- package/runtime/source-lint.cjs +710 -189
|
@@ -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}`);
|
|
@@ -670,7 +684,7 @@ ${generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INS
|
|
|
670
684
|
class DataError extends Error{constructor(status,payload){super(payload&&typeof payload.message==="string"?payload.message:payload&&typeof payload.error==="string"?payload.error:"Data operation failed");this.name="DataError";this.status=status;this.code=payload&&typeof payload.error==="string"?payload.error:null}}
|
|
671
685
|
const validateListLimit=input=>{if(input.limit!==undefined&&(!Number.isInteger(input.limit)||input.limit<1||input.limit>100))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"limit must be an integer from 1 to 100"})};
|
|
672
686
|
let swReadSession=null,swInFlight=0,swStarts=0;const swReadSessionVoid=()=>{swReadSession=null;swStarts++};${generation >= 3 ? PUBLIC_BOOTSTRAP_CLIENT_RUNTIME : ""}const invoke=async(table,operation,input)=>{if(operation==="list"||operation==="relation_list")validateListLimit(input);${generation >= 3 ? PUBLIC_BOOTSTRAP_INVOKE : ""}const alone=swInFlight===0,start=++swStarts,isRead=operation==="list"||operation==="get"||operation==="aggregate"||operation==="relation_list";swInFlight++;let response;try{response=await fetch("/__sw/data",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json",...(swReadSession?{"X-Sw-Read-Session":swReadSession}:{})},body:JSON.stringify({...input,contract,table,operation})})}catch(error){if(!isRead)swReadSession=null;throw error}finally{swInFlight--}const next=response.headers&&typeof response.headers.get==="function"?response.headers.get("X-Sw-Read-Session"):null;if(!alone||swStarts!==start||swInFlight!==0)swReadSession=null;else if(next&&/^swrs1[.][A-Za-z0-9_-]+[.][A-Za-z0-9_-]+$/.test(next))swReadSession=next;else if(!isRead||!response.ok)swReadSession=null;const payload=await response.json().catch(()=>{throw new DataError(response.status,{error:"INVALID_DATA_RESPONSE",message:"Data operation returned an invalid response"})});if(!response.ok)throw new DataError(response.status,payload);return payload};
|
|
673
|
-
${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : "values
|
|
687
|
+
${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : '(values,...extra)=>{if(extra.length!==0)throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"create takes only the values to insert. Nothing was sent."});return invoke(table,operation,{values})}'}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
|
|
674
688
|
export{data,DataError};
|
|
675
689
|
`;
|
|
676
690
|
return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration, ...insertReceiptCapability === void 0 ? {} : { insert_receipt: insertReceiptCapability } } };
|
|
@@ -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
|
|
|
@@ -949,6 +963,13 @@ interface SomewhereDbPlainInsertOptions { onConflict?: 'ignore' | 'update' | nul
|
|
|
949
963
|
// signed-in user or visitor on a table keyed by id() with no blob() column. Any other insert
|
|
950
964
|
// given a key refuses before anything runs. Eligible inserts without one get a fresh key.
|
|
951
965
|
interface SomewhereDbInsertOptions extends SomewhereDbPlainInsertOptions { idempotencyKey?: string }
|
|
966
|
+
// A caller upsert runs under the table's declared rule: 'update' also needs update on the conflicting
|
|
967
|
+
// row, so a declared table whose rule never grants update (or a per-row shares table) takes 'ignore'
|
|
968
|
+
// only. A key never combines with onConflict.
|
|
969
|
+
type SomewhereDbConflict<T extends string> = __SomewhereDeclared<T, 'conflict', 'ignore' | 'update'>;
|
|
970
|
+
type SomewhereDbCallerInsertOptions<T extends string> =
|
|
971
|
+
| { onConflict?: SomewhereDbConflict<T> | null; idempotencyKey?: never }
|
|
972
|
+
| { onConflict?: null; idempotencyKey?: string };
|
|
952
973
|
interface SomewhereDbReceipt { readonly replayed: boolean; readonly created_at: number; readonly result_until: number; readonly refuse_until: number }
|
|
953
974
|
// Process-local: retry() repeats this exact request with the same key under the current
|
|
954
975
|
// context's authority, until eight days after createdAt. It never runs by itself.
|
|
@@ -966,7 +987,7 @@ interface SomewhereDbRemove { where?: SomewhereDbWhere | null }
|
|
|
966
987
|
// (insert values, update set/increment). With no declared tables every name and
|
|
967
988
|
// value stays accepted, as before. Insert and Set pick caller or server identity.
|
|
968
989
|
type __SomewhereDbIntentFor<T extends string, Insert extends 'insert' | 'serverInsert', Set extends 'set' | 'serverSet'> =
|
|
969
|
-
| { op: 'insert'; table: T; values: __SomewhereDeclared<T, Insert, SomewhereDbValues>; options?: SomewhereDbPlainInsertOptions | null }
|
|
990
|
+
| { op: 'insert'; table: T; values: __SomewhereDeclared<T, Insert, SomewhereDbValues>; options?: (Insert extends 'insert' ? { onConflict?: SomewhereDbConflict<T> | null } : SomewhereDbPlainInsertOptions) | null }
|
|
970
991
|
| ({ op: 'update'; table: T } & SomewhereDbUpdateFor<T, Set>)
|
|
971
992
|
| { op: 'remove'; table: T; where?: SomewhereDbWhere | null };
|
|
972
993
|
// sw.db.server.tx: server authority may name row identity (owner column, shared authorship).
|
|
@@ -1052,7 +1073,7 @@ interface SomewhereServerDb {
|
|
|
1052
1073
|
}
|
|
1053
1074
|
interface SomewhereCallerDb extends Omit<SomewhereServerDb, 'from' | 'count' | 'insert' | 'update' | 'tx'> {
|
|
1054
1075
|
// The platform composes row identity from the verified user.
|
|
1055
|
-
insert<T extends SomewhereDbTable>(table: T, values: __SomewhereDeclared<T, 'insert', SomewhereDbValues>, options?:
|
|
1076
|
+
insert<T extends SomewhereDbTable>(table: T, values: __SomewhereDeclared<T, 'insert', SomewhereDbValues>, options?: SomewhereDbCallerInsertOptions<T> | null): Promise<SomewhereDbInsertResult<SomewhereDbRow<T>>>;
|
|
1056
1077
|
update<T extends SomewhereDbTable>(table: T, spec: SomewhereDbUpdateFor<T, 'set'>): Promise<SomewhereDbResult<SomewhereDbRow<T>>>;
|
|
1057
1078
|
tx(intents: readonly SomewhereDbCallerWriteIntent[]): Promise<SomewhereDbResult[]>;
|
|
1058
1079
|
from<T extends SomewhereDbTable, const C extends readonly SomewhereDbColumn<T>[] = readonly SomewhereDbColumn<T>[], const I extends readonly SomewhereDbRelationName<T>[] = []>(
|
|
@@ -1424,35 +1445,31 @@ interface SomewhereRuntimeAuthModeration {
|
|
|
1424
1445
|
user: { id: string; email: string; role: SomewhereAuthRole };
|
|
1425
1446
|
}>;
|
|
1426
1447
|
}
|
|
1427
|
-
//
|
|
1448
|
+
// Email sign-in codes (tsk_8fe91eb5). Authenticator-app activation (reauthenticate*) answers 501 AUTH_FEATURE_UNAVAILABLE.
|
|
1449
|
+
type SomewhereMfaDelivery = 'sent' | 'pending' | 'unknown';
|
|
1450
|
+
interface SomewhereMfaCodeSent { challenge_id: string; delivery: SomewhereMfaDelivery; expires_in: number }
|
|
1451
|
+
type SomewhereMfaRecovery =
|
|
1452
|
+
| { removal_pending: true; removes_at: string; wait_seconds: number; notice_resent?: boolean }
|
|
1453
|
+
| { enrolled: false };
|
|
1428
1454
|
interface SomewhereRuntimeAuthMfa {
|
|
1429
1455
|
challenge(options: { mfa_token: string; code: string }): Promise<SomewhereAuthSession>;
|
|
1430
1456
|
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 }>;
|
|
1457
|
+
resend(options: { mfa_token: string }): Promise<{ sent: boolean; delivery: SomewhereMfaDelivery; expires_in: number }>;
|
|
1458
|
+
enroll(options: { token: string }): Promise<SomewhereMfaCodeSent | { enrolled: true; method: 'email' }>;
|
|
1459
|
+
verify(options: { token: string; challenge_id: string; code: string }): Promise<{ enrolled: true; method: 'email' }>;
|
|
1460
|
+
unenroll(options: { token: string; password: string }): Promise<SomewhereMfaCodeSent | { enrolled: false }>;
|
|
1461
|
+
unenroll(options: { token: string; challenge_id: string; code: string }): Promise<{ enrolled: false }>;
|
|
1462
|
+
recover(options: { email: string; password: string }): Promise<SomewhereMfaRecovery>;
|
|
1463
|
+
cancelRecovery(options: { token: string }): Promise<{ outcome: 'cancelled' | 'already_closed' | 'completed'; message: string }>;
|
|
1464
|
+
reauthenticate(options: { token: string; enrollment_id: string; method: 'password' | 'email'; password?: string }): Promise<never>;
|
|
1465
|
+
verifyReauthentication(options: { token: string; enrollment_id: string; challenge_id: string; code: string }): Promise<never>;
|
|
1466
|
+
// The same steps for the request's own session (cookie or Bearer). Turning codes on or off ends no session.
|
|
1467
|
+
enrollWithCookie(req: Request): Promise<SomewhereMfaCodeSent | { enrolled: true; method: 'email' }>;
|
|
1468
|
+
verifyWithCookie(req: Request, options: { challenge_id: string; code: string }): Promise<{ enrolled: true; method: 'email' }>;
|
|
1469
|
+
unenrollWithCookie(req: Request, options: { password: string }): Promise<SomewhereMfaCodeSent | { enrolled: false }>;
|
|
1470
|
+
unenrollWithCookie(req: Request, options: { challenge_id: string; code: string }): Promise<{ enrolled: false }>;
|
|
1471
|
+
reauthenticateWithCookie(req: Request, options: { enrollment_id: string; method: 'password' | 'email'; password?: string }): Promise<never>;
|
|
1472
|
+
verifyReauthenticationWithCookie(req: Request, options: { enrollment_id: string; challenge_id: string; code: string }): Promise<never>;
|
|
1456
1473
|
}
|
|
1457
1474
|
interface SomewhereRuntimeAuth {
|
|
1458
1475
|
signup(options: SomewhereAuthSignupOptions): Promise<SomewhereAuthIssuedSession>;
|
|
@@ -3831,11 +3848,26 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
|
|
|
3831
3848
|
const find = (policy) => !policy ? null : policy.k === "o" ? policy.c : policy.k === "a" ? find(policy.p[0]) || find(policy.p[1]) : policy.k === "n" ? find(policy.w) : null;
|
|
3832
3849
|
return find(table.policy);
|
|
3833
3850
|
}
|
|
3851
|
+
function declaredRuleGrants(policy, op) {
|
|
3852
|
+
if (!policy) return true;
|
|
3853
|
+
if (policy.k === "o") return true;
|
|
3854
|
+
if (policy.k === "m") return !policy.m.o || policy.m.o.includes(op);
|
|
3855
|
+
if (policy.k === "a") return declaredRuleGrants(policy.p[0], op) || declaredRuleGrants(policy.p[1], op);
|
|
3856
|
+
if (policy.k === "n") return declaredRuleGrants(policy.g, op) && declaredRuleGrants(policy.w, op) && (op !== "update" || declaredRuleGrants(policy.g, "read") && declaredRuleGrants(policy.w, "read"));
|
|
3857
|
+
if (policy.k === "g" || policy.k === "r") return !!(policy.roles && policy.roles[op] && policy.roles[op].length);
|
|
3858
|
+
if (policy.k === "p") return declaredRuleGrants(policy.p, op);
|
|
3859
|
+
return true;
|
|
3860
|
+
}
|
|
3834
3861
|
function declaredTablesDeclaration3(authority) {
|
|
3835
3862
|
const schema = authority && authority.schema || {};
|
|
3836
3863
|
const scopes = authority && authority.scopes || {};
|
|
3837
3864
|
const names = Object.keys(schema).sort();
|
|
3838
3865
|
if (!names.length) return "";
|
|
3866
|
+
const rowShares = /* @__PURE__ */ new Set();
|
|
3867
|
+
for (const name of names) {
|
|
3868
|
+
const policy = schema[name].policy, branch = policy && policy.k === "a" ? policy.p[1] : null;
|
|
3869
|
+
if (branch && branch.k === "m" && branch.m.og && branch.m.g.length === 2) rowShares.add(String(branch.m.m).toLowerCase());
|
|
3870
|
+
}
|
|
3839
3871
|
const key = JSON.stringify;
|
|
3840
3872
|
const shape = (fields) => fields.length ? "{ " + fields.join("; ") + " }" : "Record<string, never>";
|
|
3841
3873
|
const union = (columns) => columns.length ? columns.map((column) => key(column.n)).join(" | ") : "never";
|
|
@@ -3853,7 +3885,7 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
|
|
|
3853
3885
|
if (owner && !columns.some((column) => column.n === owner)) columns.push({ n: owner, t: "text", optional: false, identity: true, type: "string" });
|
|
3854
3886
|
const declared = columns.filter((column) => !column.identity);
|
|
3855
3887
|
const relations = (table.relations || []).filter((relation) => Object.prototype.hasOwnProperty.call(schema, relation.table)).map((relation) => key(relation.name) + ": SomewhereDeclaredTables[" + key(relation.table) + "]['row']" + (relation.kind === "belongsTo" ? " | null" : "[]"));
|
|
3856
|
-
return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(columns.filter((column) => (!column.identity || column.n === owner) && column.t !== "json" && column.t !== "blob")) + ";\n relations: " + (relations.length ? shape(relations) : "{}") + ";\n };\n";
|
|
3888
|
+
return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(columns.filter((column) => (!column.identity || column.n === owner) && column.t !== "json" && column.t !== "blob")) + ";\n relations: " + (relations.length ? shape(relations) : "{}") + ";\n conflict: " + (rowShares.has(name.toLowerCase()) || !declaredRuleGrants(table.policy || (table.member ? { k: "m", m: table.member } : null), "update") ? "'ignore'" : "'ignore' | 'update'") + ";\n };\n";
|
|
3857
3889
|
});
|
|
3858
3890
|
return "interface SomewhereDeclaredTables {\n" + entries.join("") + "}\n";
|
|
3859
3891
|
}
|
|
@@ -4044,6 +4076,7 @@ var require_schema_types = __commonJS({
|
|
|
4044
4076
|
interface Scope { readonly __somewhereScope: unique symbol }
|
|
4045
4077
|
interface OwnerScope extends Scope { readonly __somewhereOwner: unique symbol }
|
|
4046
4078
|
interface MemberScope extends Scope { readonly __somewhereMember: unique symbol }
|
|
4079
|
+
interface GrantScope extends Scope { readonly __somewhereGrant: unique symbol }
|
|
4047
4080
|
interface GroupScope extends Scope { readonly __somewhereGroup: unique symbol }
|
|
4048
4081
|
interface AppRoleScope extends Scope { readonly __somewhereAppRole: unique symbol }
|
|
4049
4082
|
interface AnyOfScope extends Scope { readonly __somewhereAnyOf: unique symbol }
|
|
@@ -4102,8 +4135,9 @@ function removed(): SomewhereSchemaDeclaration.ColumnMarker;
|
|
|
4102
4135
|
function removedTable(): SomewhereSchemaDeclaration.TableMarker;
|
|
4103
4136
|
function exported(): SomewhereSchemaDeclaration.TableMarker;
|
|
4104
4137
|
`;
|
|
4105
|
-
var policySignatures = `function
|
|
4106
|
-
function anyOf(other: SomewhereSchemaDeclaration.
|
|
4138
|
+
var policySignatures = `function grant(options: { table: string; grantee: string; row?: string; operations: readonly ['read'] | readonly ['read', 'update'] }): SomewhereSchemaDeclaration.GrantScope;
|
|
4139
|
+
function anyOf(owner: SomewhereSchemaDeclaration.OwnerScope, other: SomewhereSchemaDeclaration.GrantScope | SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope): SomewhereSchemaDeclaration.AnyOfScope;
|
|
4140
|
+
function anyOf(other: SomewhereSchemaDeclaration.GrantScope | SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, owner: SomewhereSchemaDeclaration.OwnerScope): SomewhereSchemaDeclaration.AnyOfScope;
|
|
4107
4141
|
function allOf(gate: SomewhereSchemaDeclaration.MemberScope | SomewhereSchemaDeclaration.GroupScope | SomewhereSchemaDeclaration.AppRoleScope, who: SomewhereSchemaDeclaration.OwnerScope | SomewhereSchemaDeclaration.AnyOfScope): SomewhereSchemaDeclaration.Scope;
|
|
4108
4142
|
function parent(options: { via: string; table?: string }): SomewhereSchemaDeclaration.Scope;
|
|
4109
4143
|
function group(options?: { column?: string; roles?: Partial<Record<'read' | 'create' | 'update' | 'delete', string[]>> }): SomewhereSchemaDeclaration.GroupScope;
|
|
@@ -4306,6 +4340,9 @@ function isCanonicalBigintText(value) {
|
|
|
4306
4340
|
}
|
|
4307
4341
|
|
|
4308
4342
|
// worker/src/utils/db-schema-deploy/client-permissions.ts
|
|
4343
|
+
function tableAllowsClientWrites(table) {
|
|
4344
|
+
return Boolean(table.client && (table.client.create !== null || table.client.update !== null || table.client.delete || table.client.intake !== void 0));
|
|
4345
|
+
}
|
|
4309
4346
|
function parseClientPermissions(value, columns, scope) {
|
|
4310
4347
|
const fail = (message) => ({ ok: false, message });
|
|
4311
4348
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -4839,6 +4876,23 @@ function validateBindings(input, tableInfo, errors) {
|
|
|
4839
4876
|
}
|
|
4840
4877
|
}
|
|
4841
4878
|
|
|
4879
|
+
// worker/src/utils/authority/role-domain-map.ts
|
|
4880
|
+
var ROLE_NAME_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
4881
|
+
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]$/;
|
|
4882
|
+
function validRoleDomainMap(value) {
|
|
4883
|
+
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));
|
|
4884
|
+
}
|
|
4885
|
+
var ROLE_DOMAIN_MAP_JS = `
|
|
4886
|
+
function __sw_structRoleDomains(value) {
|
|
4887
|
+
if (!value || typeof value !== 'object' || __sw_arrayIsArray(value)) return false;
|
|
4888
|
+
const roles = __sw_keys(value);
|
|
4889
|
+
for (let i = 0; i < roles.length; i++) {
|
|
4890
|
+
const domain = value[roles[i]];
|
|
4891
|
+
if (!__sw_reMatch(${ROLE_NAME_PATTERN}, roles[i]) || typeof domain !== 'string' || !__sw_reMatch(${EMAIL_DOMAIN_PATTERN}, domain)) return false;
|
|
4892
|
+
}
|
|
4893
|
+
return true;
|
|
4894
|
+
}`;
|
|
4895
|
+
|
|
4842
4896
|
// worker/src/utils/authority/scope-enforcement.ts
|
|
4843
4897
|
var SAFE_SCOPE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4844
4898
|
var GROUP_ROLE_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
@@ -4859,7 +4913,9 @@ function validBakedMemberScope(value) {
|
|
|
4859
4913
|
const operations = member.o;
|
|
4860
4914
|
const operationOrder = ["read", "create", "update", "delete"];
|
|
4861
4915
|
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
|
-
|
|
4916
|
+
const g = member.g, mg = member.mg;
|
|
4917
|
+
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"]');
|
|
4918
|
+
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
4919
|
}
|
|
4864
4920
|
var MAX_PARENT_EDGES = 4;
|
|
4865
4921
|
function pinShape(policy) {
|
|
@@ -4873,7 +4929,7 @@ function sameAllOfRelation(gate, x) {
|
|
|
4873
4929
|
if (x.k !== "m" || gate.k !== "m") return false;
|
|
4874
4930
|
const a = gate.m;
|
|
4875
4931
|
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]));
|
|
4932
|
+
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
4933
|
}
|
|
4878
4934
|
function validBakedPolicy(value, root = true, edges = 0) {
|
|
4879
4935
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
@@ -4886,7 +4942,7 @@ function validBakedPolicy(value, root = true, edges = 0) {
|
|
|
4886
4942
|
}
|
|
4887
4943
|
if (node.k === "a") {
|
|
4888
4944
|
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));
|
|
4945
|
+
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
4946
|
}
|
|
4891
4947
|
if (root && node.k === "n") {
|
|
4892
4948
|
if (!ALLOF_POLICY_ADMITTED || !exactKeys(node, ["k", "g", "w"])) return false;
|
|
@@ -4894,7 +4950,7 @@ function validBakedPolicy(value, root = true, edges = 0) {
|
|
|
4894
4950
|
const who = node.w;
|
|
4895
4951
|
if (!gate || !who) return false;
|
|
4896
4952
|
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);
|
|
4953
|
+
const gateValid = gate.k === "m" ? validBakedPolicy(gate, false) && gateMember?.o !== void 0 && !gateMember.og : (gate.k === "g" || gate.k === "r") && validBakedPolicy(gate, true);
|
|
4898
4954
|
if (!gateValid) return false;
|
|
4899
4955
|
if (who.k === "o") return validBakedPolicy(who, false);
|
|
4900
4956
|
const whoBranches = who.p;
|
|
@@ -4912,7 +4968,9 @@ function validBakedPolicy(value, root = true, edges = 0) {
|
|
|
4912
4968
|
return exactKeys(node, ["k", "c", "roles", "generation"]) && ident(node.c) && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
|
|
4913
4969
|
}
|
|
4914
4970
|
if (root && node.k === "r") {
|
|
4915
|
-
|
|
4971
|
+
const domains = node.d;
|
|
4972
|
+
const validDomains = domains === void 0 || validRoleDomainMap(domains);
|
|
4973
|
+
return (exactKeys(node, ["k", "roles", "generation"]) || exactKeys(node, ["k", "roles", "generation", "d"])) && validDomains && validGroupRoles(node.roles) && Number.isSafeInteger(node.generation) && node.generation >= 1;
|
|
4916
4974
|
}
|
|
4917
4975
|
return false;
|
|
4918
4976
|
}
|
|
@@ -5033,7 +5091,8 @@ function bakedPolicyFromDeclared(value, depth = 0, edges = 0, underParent = fals
|
|
|
5033
5091
|
if (depth !== 0 && !underParent || node.policies[0]?.kind !== "owner" || second !== "member" && second !== "group" && second !== "app_role") return null;
|
|
5034
5092
|
const left = bakedPolicyFromDeclared(node.policies[0], depth + 1);
|
|
5035
5093
|
const right = bakedPolicyFromDeclared(node.policies[1], depth + 1, edges, second !== "member");
|
|
5036
|
-
|
|
5094
|
+
const baked = left && right ? { k: "a", p: [left, right] } : null;
|
|
5095
|
+
return baked && validBakedPolicy(pinShape(baked), true, edges) ? baked : null;
|
|
5037
5096
|
}
|
|
5038
5097
|
if (node.kind === "all_of") {
|
|
5039
5098
|
if (depth !== 0 && !underParent) return null;
|
|
@@ -5125,7 +5184,143 @@ function bakedMemberFromDeclared(scope, explicitOperations = false) {
|
|
|
5125
5184
|
if (JSON.stringify(operations) !== JSON.stringify(allowed.filter((operation) => operations.includes(operation))) || !operations.includes("read") && (operations.includes("update") || operations.includes("delete"))) return null;
|
|
5126
5185
|
o = operations.length === allowed.length && !explicitOperations ? void 0 : [...operations];
|
|
5127
5186
|
}
|
|
5128
|
-
|
|
5187
|
+
if (s.ownerGrant !== void 0 && s.ownerGrant !== true) return null;
|
|
5188
|
+
const member = { g, m: s.membership.toLowerCase(), u: s.memberUser.toLowerCase(), mg, ...o !== void 0 ? { o } : {}, ...s.ownerGrant ? { og: true } : {} };
|
|
5189
|
+
return validBakedMemberScope(member) ? member : null;
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
// worker/src/utils/db-schema-deploy/extract-schema-grants.ts
|
|
5193
|
+
function readGrantScope(r, table, line, fail) {
|
|
5194
|
+
r.expectPunct("(", "after grant");
|
|
5195
|
+
r.expectPunct("{", "in grant options");
|
|
5196
|
+
let membership, grantee, row;
|
|
5197
|
+
let operations;
|
|
5198
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5199
|
+
while (!r.tryPunct("}")) {
|
|
5200
|
+
const key = r.readKey("in grant options");
|
|
5201
|
+
if (seen.has(key.name)) fail(key.line, `Duplicate grant option "${key.name}".`);
|
|
5202
|
+
seen.add(key.name);
|
|
5203
|
+
r.expectPunct(":", "after grant option");
|
|
5204
|
+
if (key.name === "table" || key.name === "grantee" || key.name === "row") {
|
|
5205
|
+
const value = r.readLiteral("in grant options");
|
|
5206
|
+
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.");
|
|
5207
|
+
if (key.name === "table") membership = value.toLowerCase();
|
|
5208
|
+
else if (key.name === "grantee") grantee = value.toLowerCase();
|
|
5209
|
+
else row = value.toLowerCase();
|
|
5210
|
+
} else if (key.name === "operations") {
|
|
5211
|
+
const ops = [];
|
|
5212
|
+
r.expectPunct("[", "in grant operations");
|
|
5213
|
+
while (!r.tryPunct("]")) {
|
|
5214
|
+
const op = r.readLiteral("in grant operations");
|
|
5215
|
+
if (op !== "read" && op !== "update") fail(key.line, "grant operations allow only read and update.");
|
|
5216
|
+
if (ops.includes(op)) fail(key.line, "Duplicate grant operation.");
|
|
5217
|
+
ops.push(op);
|
|
5218
|
+
if (!r.tryPunct(",")) {
|
|
5219
|
+
r.expectPunct("]", "closing grant operations");
|
|
5220
|
+
break;
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
if (!ops.includes("read")) fail(key.line, "grant operations require read; update alone is not allowed.");
|
|
5224
|
+
operations = ops.includes("update") ? ["read", "update"] : ["read"];
|
|
5225
|
+
} else fail(key.line, `Unknown grant option "${key.name}". Allowed: table, grantee, row, operations.`);
|
|
5226
|
+
if (!r.tryPunct(",")) {
|
|
5227
|
+
r.expectPunct("}", "closing grant options");
|
|
5228
|
+
break;
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
r.tryPunct(",");
|
|
5232
|
+
r.expectPunct(")", "closing grant");
|
|
5233
|
+
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.`);
|
|
5234
|
+
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.`);
|
|
5235
|
+
return { kind: "member", group: ["user_id"], membership, memberUser: grantee, memberGroup: row === void 0 ? ["user_id"] : ["user_id", row], operations, ownerGrant: true };
|
|
5236
|
+
}
|
|
5237
|
+
function resolveGrantRowKeys(scope, key, table, line, fail) {
|
|
5238
|
+
const visit = (node) => {
|
|
5239
|
+
if (node.kind === "member" && node.ownerGrant && node.memberGroup.length === 2 && node.group.length === 1) {
|
|
5240
|
+
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.`);
|
|
5241
|
+
node.group = ["user_id", key.toLowerCase()];
|
|
5242
|
+
} else if (node.kind === "any_of") node.policies.forEach(visit);
|
|
5243
|
+
else if (node.kind === "all_of") {
|
|
5244
|
+
visit(node.gate);
|
|
5245
|
+
visit(node.who);
|
|
5246
|
+
}
|
|
5247
|
+
};
|
|
5248
|
+
if (scope.kind === "policy") visit(scope.policy);
|
|
5249
|
+
}
|
|
5250
|
+
function validateDeclaredMemberships(tables, tableByName, knownColumns, errors) {
|
|
5251
|
+
const memberPolicies = (scope) => {
|
|
5252
|
+
const out = [];
|
|
5253
|
+
const visit = (node) => {
|
|
5254
|
+
if (node.kind === "member") out.push(node);
|
|
5255
|
+
else if (node.kind === "any_of") node.policies.forEach(visit);
|
|
5256
|
+
else if (node.kind === "all_of") {
|
|
5257
|
+
visit(node.gate);
|
|
5258
|
+
visit(node.who);
|
|
5259
|
+
}
|
|
5260
|
+
};
|
|
5261
|
+
if (scope.kind === "member") out.push(scope);
|
|
5262
|
+
else if (scope.kind === "policy") visit(scope.policy);
|
|
5263
|
+
return out;
|
|
5264
|
+
};
|
|
5265
|
+
for (const t of tables) {
|
|
5266
|
+
for (const sc of memberPolicies(t.scope)) {
|
|
5267
|
+
const own = knownColumns(t);
|
|
5268
|
+
for (const g of sc.group) {
|
|
5269
|
+
if (!own.has(g)) {
|
|
5270
|
+
errors.push(
|
|
5271
|
+
`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.`
|
|
5272
|
+
);
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
const membershipTable = tableByName.get(sc.membership);
|
|
5276
|
+
if (!membershipTable) {
|
|
5277
|
+
errors.push(
|
|
5278
|
+
`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.`
|
|
5279
|
+
);
|
|
5280
|
+
continue;
|
|
5281
|
+
}
|
|
5282
|
+
const membershipCols = knownColumns(membershipTable);
|
|
5283
|
+
if (sc.ownerGrant) {
|
|
5284
|
+
if (membershipTable.scope.kind !== "owner" || membershipTable.scope.column !== "user_id" || membershipTable.scope.visitors) {
|
|
5285
|
+
errors.push(`Grant table "${sc.membership}" must use owner() with managed user_id and authenticated owners.`);
|
|
5286
|
+
}
|
|
5287
|
+
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.`);
|
|
5288
|
+
const grantee = membershipTable.columns.find((column) => column.name === sc.memberUser);
|
|
5289
|
+
if (!grantee || grantee.helper !== "text" || grantee.nullable) errors.push(`Grant grantee "${sc.membership}"."${sc.memberUser}" must be a required text column.`);
|
|
5290
|
+
if (sc.memberGroup.length === 2) {
|
|
5291
|
+
const row = membershipTable.columns.find((column) => column.name === sc.memberGroup[1]);
|
|
5292
|
+
const key = t.columns.find((column) => column.name === sc.group[1]);
|
|
5293
|
+
const keyHelper = key?.helper === "id" ? key.uuid ? "text" : "integer" : null;
|
|
5294
|
+
if (!row || row.nullable || row.helper !== keyHelper) {
|
|
5295
|
+
errors.push(`Grant row "${sc.membership}"."${sc.memberGroup[1]}" must be a required ${keyHelper ?? "id-typed"} column holding a "${t.name}" id.`);
|
|
5296
|
+
} else if (row.hasDefault || row.references !== t.name || row.onDelete !== "cascade") {
|
|
5297
|
+
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' }).`);
|
|
5298
|
+
}
|
|
5299
|
+
if (membershipTable.client?.update?.includes(sc.memberGroup[1])) {
|
|
5300
|
+
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.`);
|
|
5301
|
+
}
|
|
5302
|
+
const users = tables.filter((other) => memberPolicies(other.scope).some((m) => m.ownerGrant && m.membership === sc.membership));
|
|
5303
|
+
if (users.length !== 1 || memberPolicies(t.scope).filter((m) => m.ownerGrant && m.membership === sc.membership).length !== 1) {
|
|
5304
|
+
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.`);
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
} else if (tableAllowsClientWrites(membershipTable)) {
|
|
5308
|
+
errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
|
|
5309
|
+
}
|
|
5310
|
+
if (!membershipCols.has(sc.memberUser)) {
|
|
5311
|
+
errors.push(
|
|
5312
|
+
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${sc.memberUser}" column named in member({ member_user }).`
|
|
5313
|
+
);
|
|
5314
|
+
}
|
|
5315
|
+
for (const mg of sc.ownerGrant ? [] : sc.memberGroup) {
|
|
5316
|
+
if (!membershipCols.has(mg)) {
|
|
5317
|
+
errors.push(
|
|
5318
|
+
`The membership table "${sc.membership}" (for member-scoped "${t.name}") has no "${mg}" column named in member({ member_group }).`
|
|
5319
|
+
);
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5129
5324
|
}
|
|
5130
5325
|
|
|
5131
5326
|
// worker/src/utils/db-schema-deploy/schema-constants.ts
|
|
@@ -6219,8 +6414,9 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
|
|
|
6219
6414
|
}
|
|
6220
6415
|
|
|
6221
6416
|
// worker/src/utils/db-schema-deploy/extract-schema-roles.ts
|
|
6417
|
+
var EMAIL_DOMAIN = EMAIL_DOMAIN_PATTERN;
|
|
6222
6418
|
var GROUP_KEYS = ["roles", "adminGrantableRoles"];
|
|
6223
|
-
var APP_ROLE_KEYS = ["grantableBy"];
|
|
6419
|
+
var APP_ROLE_KEYS = ["grantableBy", "emailDomain"];
|
|
6224
6420
|
function readStringList2(r, fail, ctx) {
|
|
6225
6421
|
const open = r.peek();
|
|
6226
6422
|
if (open.kind !== "punct" || open.value !== "[") fail(open.line, `${ctx} must be a list of quoted role names, such as ['viewer'].`);
|
|
@@ -6264,7 +6460,7 @@ function readAppRoles(r, fail) {
|
|
|
6264
6460
|
const key = r.readKey("for an app role name");
|
|
6265
6461
|
if (out.some((role2) => role2.name === key.name)) fail(key.line, `app role "${key.name}" is declared twice.`);
|
|
6266
6462
|
r.expectPunct(":", `after app role "${key.name}"`);
|
|
6267
|
-
r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...] }`);
|
|
6463
|
+
r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...], emailDomain: '...' }`);
|
|
6268
6464
|
const role = { name: key.name, grantableBy: [] };
|
|
6269
6465
|
const seen = /* @__PURE__ */ new Set();
|
|
6270
6466
|
while (!r.tryPunct("}")) {
|
|
@@ -6275,7 +6471,16 @@ function readAppRoles(r, fail) {
|
|
|
6275
6471
|
if (seen.has(opt.name)) fail(opt.line, `app role "${key.name}" declares ${opt.name} twice.`);
|
|
6276
6472
|
seen.add(opt.name);
|
|
6277
6473
|
r.expectPunct(":", `after appRoles.${key.name}.${opt.name}`);
|
|
6278
|
-
|
|
6474
|
+
if (opt.name === "emailDomain") {
|
|
6475
|
+
const t = r.next();
|
|
6476
|
+
if (t.kind !== "string") fail(t.line, `appRoles.${key.name}.emailDomain must be one quoted domain, such as 'example.com'.`);
|
|
6477
|
+
if (!EMAIL_DOMAIN.test(t.value)) {
|
|
6478
|
+
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.`);
|
|
6479
|
+
}
|
|
6480
|
+
role.emailDomain = t.value;
|
|
6481
|
+
} else {
|
|
6482
|
+
role.grantableBy = readStringList2(r, fail, `appRoles.${key.name}.grantableBy`);
|
|
6483
|
+
}
|
|
6279
6484
|
if (!r.tryPunct(",")) {
|
|
6280
6485
|
r.expectPunct("}", `closing app role "${key.name}"`);
|
|
6281
6486
|
break;
|
|
@@ -6295,18 +6500,31 @@ function canonicalRoleVocabulary(groups, appRoles) {
|
|
|
6295
6500
|
v: 1,
|
|
6296
6501
|
groupRoles: sorted2(groups?.roles ?? []),
|
|
6297
6502
|
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) }))
|
|
6503
|
+
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
6504
|
};
|
|
6300
6505
|
}
|
|
6301
6506
|
function validateDeclaredRoles(groups, appRoles) {
|
|
6302
6507
|
const delegation = /* @__PURE__ */ Object.create(null);
|
|
6303
6508
|
for (const role of appRoles ?? []) delegation[role.name] = role.grantableBy;
|
|
6304
|
-
return roleVocabularyErrors({
|
|
6509
|
+
return [...emailDomainRoleErrors(appRoles ?? []), ...roleVocabularyErrors({
|
|
6305
6510
|
groupRoles: groups?.roles ?? [],
|
|
6306
6511
|
adminGrantableGroupRoles: groups?.adminGrantableRoles ?? [],
|
|
6307
6512
|
appRoles: (appRoles ?? []).map((role) => role.name),
|
|
6308
6513
|
appRoleDelegation: delegation
|
|
6309
|
-
});
|
|
6514
|
+
})];
|
|
6515
|
+
}
|
|
6516
|
+
function emailDomainRoleErrors(appRoles) {
|
|
6517
|
+
const errors = [];
|
|
6518
|
+
const byDomain = new Set(appRoles.filter((role) => role.emailDomain !== void 0).map((role) => role.name));
|
|
6519
|
+
for (const role of appRoles) {
|
|
6520
|
+
if (role.emailDomain !== void 0 && !EMAIL_DOMAIN.test(role.emailDomain)) errors.push(`appRoles.${role.name}.emailDomain is not one lowercase domain.`);
|
|
6521
|
+
for (const grantor of role.grantableBy) {
|
|
6522
|
+
if (byDomain.has(grantor)) {
|
|
6523
|
+
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.`);
|
|
6524
|
+
}
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
return errors;
|
|
6310
6528
|
}
|
|
6311
6529
|
|
|
6312
6530
|
// worker/src/utils/db-schema-deploy/extract-schema-options.ts
|
|
@@ -6396,6 +6614,7 @@ var SCHEMA_CALLABLE = /* @__PURE__ */ new Set([
|
|
|
6396
6614
|
...TABLE_MARKER_HELPERS,
|
|
6397
6615
|
"schema",
|
|
6398
6616
|
"table",
|
|
6617
|
+
"grant",
|
|
6399
6618
|
"removed",
|
|
6400
6619
|
"hasMany",
|
|
6401
6620
|
"belongsTo",
|
|
@@ -6510,10 +6729,10 @@ function tokenize(source) {
|
|
|
6510
6729
|
var Reader = class {
|
|
6511
6730
|
constructor(tokens, options = {}) {
|
|
6512
6731
|
this.tokens = tokens;
|
|
6513
|
-
this.bareIdIsText = !options.legacyIntegerIdDefault;
|
|
6732
|
+
this.bareIdIsText = (table) => !options.legacyIntegerIdDefault && !options.integerKeyTables?.has(table.toLowerCase());
|
|
6514
6733
|
}
|
|
6515
6734
|
pos = 0;
|
|
6516
|
-
/** What a bare id() declares: a random text key, or
|
|
6735
|
+
/** What a bare id() on this table declares: a random text key, or an integer (ExtractSchemaOptions). */
|
|
6517
6736
|
bareIdIsText;
|
|
6518
6737
|
peek() {
|
|
6519
6738
|
return this.tokens[this.pos];
|
|
@@ -6681,7 +6900,7 @@ function readColumn(r, tableName, rawName, line) {
|
|
|
6681
6900
|
const col = {
|
|
6682
6901
|
name,
|
|
6683
6902
|
helper,
|
|
6684
|
-
uuid: helper === "id" && r.bareIdIsText,
|
|
6903
|
+
uuid: helper === "id" && r.bareIdIsText(tableName),
|
|
6685
6904
|
nullable: false,
|
|
6686
6905
|
hasDefault: false,
|
|
6687
6906
|
default: null,
|
|
@@ -6918,7 +7137,7 @@ function readPolicyMember(r, tableName, line) {
|
|
|
6918
7137
|
if (scope.kind !== "member") throw new SchemaTsError(`line ${line}: invalid member() policy.`);
|
|
6919
7138
|
return scope;
|
|
6920
7139
|
}
|
|
6921
|
-
var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), group() or appRole().";
|
|
7140
|
+
var ANY_OF_FORMS = "anyOf() accepts exactly one owner() and one member(), grant(), group() or appRole().";
|
|
6922
7141
|
function readAnyOfBranch(r, tableName, which) {
|
|
6923
7142
|
const tok = r.expectIdent(`for the ${which} policy in anyOf() on table "${tableName}"`);
|
|
6924
7143
|
const failAt = (at, message) => {
|
|
@@ -6926,6 +7145,7 @@ function readAnyOfBranch(r, tableName, which) {
|
|
|
6926
7145
|
};
|
|
6927
7146
|
if (tok.value === "owner") return readPolicyOwner(r, tableName, tok.line);
|
|
6928
7147
|
if (tok.value === "member") return readPolicyMember(r, tableName, tok.line);
|
|
7148
|
+
if (tok.value === "grant") return readGrantScope(r, tableName, tok.line, failAt);
|
|
6929
7149
|
if (tok.value === "group") return readGroupScope(r, tableName, tok.line, failAt);
|
|
6930
7150
|
if (tok.value === "appRole") return readAppRoleScope(r, tableName, tok.line, failAt);
|
|
6931
7151
|
throw new SchemaTsError(`line ${tok.line}: ${ANY_OF_FORMS}`);
|
|
@@ -6941,6 +7161,7 @@ function readAnyOfScope(r, tableName, line) {
|
|
|
6941
7161
|
if (owners.length !== 1) throw new SchemaTsError(`line ${line}: ${ANY_OF_FORMS}`);
|
|
6942
7162
|
const owner = owners[0];
|
|
6943
7163
|
const other = firstPolicy.kind === "owner" ? secondPolicy : firstPolicy;
|
|
7164
|
+
if (other.kind === "member" && other.ownerGrant && owner.column !== DEFAULT_OWNER_COLUMN) throw new SchemaTsError(`line ${line}: grant() requires owner() with managed user_id.`);
|
|
6944
7165
|
return { kind: "policy", policy: { kind: "any_of", policies: [owner, other] } };
|
|
6945
7166
|
}
|
|
6946
7167
|
var ALL_OF_FORMS = "allOf() takes a gate \u2014 member(), group() or appRole() \u2014 and then owner() or anyOf(owner(), ...).";
|
|
@@ -6968,6 +7189,7 @@ function readAllOfScope(r, tableName, line) {
|
|
|
6968
7189
|
r.tryPunct(",");
|
|
6969
7190
|
r.expectPunct(")", "closing allOf()");
|
|
6970
7191
|
const x = who.kind === "any_of" ? who.policies[1] : null;
|
|
7192
|
+
if (x?.kind === "member" && x.ownerGrant) throw new SchemaTsError(`line ${line}: grant() belongs in anyOf(owner(), grant()), not allOf().`);
|
|
6971
7193
|
for (const node of [gate, ...x ? [x] : []]) {
|
|
6972
7194
|
if (node.kind !== "member") continue;
|
|
6973
7195
|
if (!explicitMemberOperations.has(node)) {
|
|
@@ -7203,6 +7425,9 @@ function readTable(r, rawName, nameLine) {
|
|
|
7203
7425
|
if (key.name === "scope") {
|
|
7204
7426
|
if (scope) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "scope" twice.`);
|
|
7205
7427
|
scope = readScope(r, rawName);
|
|
7428
|
+
resolveGrantRowKeys(scope, idCols[0]?.name, rawName, key.line, (at, message) => {
|
|
7429
|
+
throw new SchemaTsError(`line ${at}: ${message}`);
|
|
7430
|
+
});
|
|
7206
7431
|
} else if (key.name === "indexes") {
|
|
7207
7432
|
if (indexEntries) throw new SchemaTsError(`line ${key.line}: table "${rawName}" declares "indexes" twice.`);
|
|
7208
7433
|
indexEntries = readColumnLists(r, rawName, "indexes");
|
|
@@ -7434,55 +7659,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
7434
7659
|
};
|
|
7435
7660
|
resolveAppRoleScopes(tables, appRoles, errors);
|
|
7436
7661
|
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
|
-
}
|
|
7662
|
+
validateDeclaredMemberships(tables, tableByName, knownColumns, errors);
|
|
7486
7663
|
validateDeclaredRelations(tables, tableByName, knownColumns, (table) => removedTables.includes(table) ? "removedTable()" : exportedTables.includes(table) ? "exported()" : null, errors);
|
|
7487
7664
|
validateRowLinks(tables, tableByName, errors);
|
|
7488
7665
|
validateIntake(tables, tableByName, errors);
|
|
@@ -7490,7 +7667,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
7490
7667
|
errors.push(...fileErrors);
|
|
7491
7668
|
validateFileCollections(fileCollections, (name) => {
|
|
7492
7669
|
const t = tableByName.get(name);
|
|
7493
|
-
return t ? { columns: knownColumns(t), clientWrites:
|
|
7670
|
+
return t ? { columns: knownColumns(t), clientWrites: tableAllowsClientWrites(t), parentTarget: fileParentTarget(t) } : null;
|
|
7494
7671
|
}, errors);
|
|
7495
7672
|
}
|
|
7496
7673
|
if (groupRoles || appRoles) errors.push(...validateDeclaredRoles(groupRoles, appRoles));
|
|
@@ -7601,7 +7778,10 @@ function declaredRolesFromSource(files) {
|
|
|
7601
7778
|
if (source === void 0) return void 0;
|
|
7602
7779
|
const parsed = extractSchemaTs(source);
|
|
7603
7780
|
if (!parsed.ok || !parsed.declaration.groups && !parsed.declaration.appRoles) return void 0;
|
|
7604
|
-
return JSON.stringify(canonicalRoleVocabulary(
|
|
7781
|
+
return JSON.stringify(canonicalRoleVocabulary(
|
|
7782
|
+
parsed.declaration.groups,
|
|
7783
|
+
parsed.declaration.appRoles?.map((role) => ({ name: role.name, grantableBy: role.grantableBy }))
|
|
7784
|
+
));
|
|
7605
7785
|
}
|
|
7606
7786
|
|
|
7607
7787
|
// declared-data-vendor-entry.js
|
|
@@ -7619,8 +7799,8 @@ var checkerContract = __toESM(require_checker_contract());
|
|
|
7619
7799
|
var import_runtime_types = __toESM(require_runtime_types());
|
|
7620
7800
|
var import_generated_declarations = __toESM(require_generated_declarations());
|
|
7621
7801
|
var import_generated_declarations_render = __toESM(require_generated_declarations_render());
|
|
7622
|
-
function referenceCheckDeclaredTables(graphFiles) {
|
|
7623
|
-
const authority = schemaAuthorityFromSource(graphFiles);
|
|
7802
|
+
function referenceCheckDeclaredTables(graphFiles, schemaOptions = {}) {
|
|
7803
|
+
const authority = schemaAuthorityFromSource(graphFiles, schemaOptions);
|
|
7624
7804
|
return authority ? (0, import_runtime_types.declaredTablesDeclaration)(authority) : void 0;
|
|
7625
7805
|
}
|
|
7626
7806
|
function referenceCheckDeclarations(graphFiles) {
|