@synapsor/runner 1.6.3 → 1.6.4
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/AGENTS.md +2 -2
- package/CHANGELOG.md +72 -8
- package/CONTRIBUTING.md +10 -3
- package/README.md +61 -93
- package/SECURITY.md +7 -3
- package/THREAT_MODEL.md +37 -0
- package/dist/authoring.mjs +98 -13
- package/dist/cli.d.ts +12 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts +60 -0
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +7321 -2000
- package/dist/runtime.mjs +528 -51
- package/docs/README.md +14 -0
- package/docs/agent-guided-setup.md +7 -7
- package/docs/aggregate-reads.md +14 -5
- package/docs/app-owned-executors.md +5 -6
- package/docs/auto-boundary-and-scoped-explore.md +58 -6
- package/docs/capability-authoring.md +2 -2
- package/docs/client-recipes.md +1 -1
- package/docs/cloud-mode.md +7 -7
- package/docs/conformance.md +6 -3
- package/docs/current-scope.md +13 -2
- package/docs/cursor-plugin.md +3 -3
- package/docs/database-enforced-scope.md +6 -0
- package/docs/dsl-reference.md +12 -1
- package/docs/fresh-developer-usability.md +6 -6
- package/docs/getting-started-own-database.md +41 -41
- package/docs/guided-onboarding.md +98 -30
- package/docs/limitations.md +34 -14
- package/docs/local-mode.md +20 -20
- package/docs/mcp-audit.md +14 -14
- package/docs/mcp-client-setup.md +11 -14
- package/docs/mcp-clients.md +1 -1
- package/docs/migrating-to-synapsor-spec.md +5 -1
- package/docs/openai-agents-sdk.md +2 -2
- package/docs/proposal-evidence-freshness.md +7 -7
- package/docs/recipes.md +6 -6
- package/docs/release-notes.md +86 -10
- package/docs/release-policy.md +20 -7
- package/docs/reversible-change-sets.md +1 -1
- package/docs/reviewed-database-views.md +132 -0
- package/docs/reviewed-relationships.md +245 -0
- package/docs/rfcs/006-reviewed-reversible-change-sets.md +1 -1
- package/docs/runner-bundles.md +7 -7
- package/docs/schema-api-candidates.md +1 -1
- package/docs/security-boundary.md +30 -0
- package/docs/troubleshooting-first-run.md +52 -12
- package/docs/use-your-own-database.md +6 -4
- package/docs/workbench-ask.md +279 -0
- package/docs/writeback-executors.md +7 -7
- package/examples/auto-boundary-churn/README.md +1 -1
- package/examples/community-solar-clean-room/README.md +7 -0
- package/examples/community-solar-clean-room/docker-compose.yml +16 -0
- package/examples/community-solar-clean-room/package.json +15 -0
- package/examples/community-solar-clean-room/prisma/schema.prisma +171 -0
- package/examples/community-solar-clean-room/seed/postgres.sql +571 -0
- package/examples/fitflow-guided-onboarding/README.md +2 -2
- package/examples/openai-agents-http/Makefile +1 -1
- package/examples/openai-agents-http/README.md +1 -1
- package/examples/openai-agents-stdio/Makefile +1 -1
- package/examples/openai-agents-stdio/README.md +2 -2
- package/examples/openai-agents-stdio/agent.py +1 -4
- package/examples/reference-support-billing-app/README.md +16 -16
- package/examples/retail-clean-room/README.md +7 -0
- package/examples/retail-clean-room/docker-compose.yml +16 -0
- package/examples/retail-clean-room/package.json +9 -0
- package/examples/retail-clean-room/prisma/schema.prisma +239 -0
- package/examples/retail-clean-room/seed/postgres.sql +781 -0
- package/examples/retail-clean-room/view-recipe/average-retained-revenue.synapsor.sql +21 -0
- package/examples/retail-clean-room/view-recipe/synapsor.runner.json +25 -0
- package/examples/support-plan-credit/mcp-client-examples/claude-code.sh +1 -4
- package/examples/support-plan-credit/mcp-client-examples/claude-desktop.json +1 -4
- package/examples/support-plan-credit/mcp-client-examples/codex.config.toml +1 -4
- package/examples/support-plan-credit/mcp-client-examples/cursor-global.mcp.json +1 -4
- package/examples/support-plan-credit/mcp-client-examples/cursor-project.mcp.json +1 -4
- package/examples/support-plan-credit/mcp-client-examples/generic-stdio.json +1 -4
- package/examples/support-plan-credit/mcp-client-examples/generic-stdio.mjs +1 -1
- package/examples/support-plan-credit/mcp-client-examples/google-adk.py +1 -1
- package/examples/support-plan-credit/mcp-client-examples/langchain.mjs +1 -1
- package/examples/support-plan-credit/mcp-client-examples/llamaindex.py +1 -1
- package/examples/support-plan-credit/mcp-client-examples/openai-agents-stdio.ts +1 -1
- package/examples/support-plan-credit/mcp-client-examples/vscode.mcp.json +1 -4
- package/fixtures/compatibility/published-1.6.3/manifest.json +76 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/dsl/examples/aggregate-read.synapsor.sql +21 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/dsl/examples/billing-late-fee.synapsor.sql +56 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/dsl/examples/bounded-set-multi-term.synapsor.sql +30 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/dsl/examples/principal-row-scope.synapsor.sql +23 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/aggregate-read/contract.json +119 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/approval-quorum/contract.json +44 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/bounded-set-threats/contract.json +115 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/principal-row-scope/contract.json +78 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/proposal-capability/contract.json +101 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/reversible-change-sets/contract.json +98 -0
- package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/valid/basic-read.contract.json +60 -0
- package/llms.txt +10 -5
- package/package.json +8 -2
package/dist/runtime.mjs
CHANGED
|
@@ -184,6 +184,7 @@ var PROTECTED_READ_KEYS = /* @__PURE__ */ new Set([
|
|
|
184
184
|
"generation_lock_fingerprint",
|
|
185
185
|
"predicates",
|
|
186
186
|
"relationship",
|
|
187
|
+
"relationships",
|
|
187
188
|
"row_order_by",
|
|
188
189
|
"aggregate",
|
|
189
190
|
"limits"
|
|
@@ -202,6 +203,19 @@ var PROTECTED_RELATIONSHIP_KEYS = /* @__PURE__ */ new Set([
|
|
|
202
203
|
"cardinality",
|
|
203
204
|
"max_fan_out"
|
|
204
205
|
]);
|
|
206
|
+
var PROTECTED_RELATIONSHIP_PATH_KEYS = /* @__PURE__ */ new Set(["name", "links"]);
|
|
207
|
+
var PROTECTED_RELATIONSHIP_LINK_KEYS = /* @__PURE__ */ new Set([
|
|
208
|
+
"schema",
|
|
209
|
+
"table",
|
|
210
|
+
"primary_key",
|
|
211
|
+
"tenant_key",
|
|
212
|
+
"principal_scope_key",
|
|
213
|
+
"local_key",
|
|
214
|
+
"target_key",
|
|
215
|
+
"cardinality",
|
|
216
|
+
"max_fan_out",
|
|
217
|
+
"unmatched_rows"
|
|
218
|
+
]);
|
|
205
219
|
var PROTECTED_ROW_ORDER_KEYS = /* @__PURE__ */ new Set(["field", "direction"]);
|
|
206
220
|
var PROTECTED_AGGREGATE_KEYS = /* @__PURE__ */ new Set([
|
|
207
221
|
"counted_entity",
|
|
@@ -666,7 +680,18 @@ function validateProtectedRead(capability, path3, errors) {
|
|
|
666
680
|
const keptOut = new Set(Array.isArray(capability.kept_out_fields) ? capability.kept_out_fields.filter((field) => typeof field === "string") : []);
|
|
667
681
|
const subject = isRecord(capability.subject) ? capability.subject : {};
|
|
668
682
|
const trustedScopeFields = new Set([subject.tenant_key, subject.principal_scope_key].filter((field) => isSafeIdentifier(field)));
|
|
669
|
-
const
|
|
683
|
+
const relationshipNames = /* @__PURE__ */ new Set();
|
|
684
|
+
const legacyRelationshipName = validateProtectedRelationship(protectedRead.relationship, keptOut, trustedScopeFields, `${path3}.protected_read.relationship`, errors);
|
|
685
|
+
if (legacyRelationshipName)
|
|
686
|
+
relationshipNames.add(legacyRelationshipName);
|
|
687
|
+
if (protectedRead.relationship !== void 0 && protectedRead.relationships !== void 0) {
|
|
688
|
+
errors.push({
|
|
689
|
+
path: `${path3}.protected_read`,
|
|
690
|
+
code: "PROTECTED_RELATIONSHIP_FORMS_CONFLICT",
|
|
691
|
+
message: "protected_read must use either legacy relationship or reviewed relationships, not both."
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
validateProtectedRelationshipPaths(protectedRead.relationships, keptOut, trustedScopeFields, `${path3}.protected_read.relationships`, relationshipNames, errors);
|
|
670
695
|
if (!isRecord(capability.evidence) || capability.evidence.required !== true || capability.evidence.query_audit !== true) {
|
|
671
696
|
errors.push({
|
|
672
697
|
path: `${path3}.evidence`,
|
|
@@ -685,7 +710,7 @@ function validateProtectedRead(capability, path3, errors) {
|
|
|
685
710
|
return;
|
|
686
711
|
}
|
|
687
712
|
checkUnknownKeys(predicate, PROTECTED_PREDICATE_KEYS, predicatePath, errors);
|
|
688
|
-
validateProtectedFieldReference(predicate.field, predicate.relationship,
|
|
713
|
+
validateProtectedFieldReference(predicate.field, predicate.relationship, relationshipNames, keptOut, trustedScopeFields, predicatePath, errors);
|
|
689
714
|
if (!["eq", "neq", "lt", "lte", "gt", "gte", "in"].includes(String(predicate.operator))) {
|
|
690
715
|
errors.push({ path: `${predicatePath}.operator`, code: "INVALID_PROTECTED_PREDICATE_OPERATOR", message: "protected predicate operator must be eq, neq, lt, lte, gt, gte, or in." });
|
|
691
716
|
}
|
|
@@ -745,7 +770,7 @@ function validateProtectedRead(capability, path3, errors) {
|
|
|
745
770
|
validateProtectedAggregate(protectedRead.aggregate, {
|
|
746
771
|
args,
|
|
747
772
|
referencedArgs,
|
|
748
|
-
|
|
773
|
+
relationshipNames,
|
|
749
774
|
keptOut,
|
|
750
775
|
trustedScopeFields,
|
|
751
776
|
maxGroups: isRecord(protectedRead.limits) ? protectedRead.limits.max_groups : void 0,
|
|
@@ -788,6 +813,65 @@ function validateProtectedRelationship(value, keptOut, trustedScopeFields, path3
|
|
|
788
813
|
}
|
|
789
814
|
return isSafeIdentifier(value.name) ? value.name : void 0;
|
|
790
815
|
}
|
|
816
|
+
function validateProtectedRelationshipPaths(value, keptOut, trustedScopeFields, path3, relationshipNames, errors) {
|
|
817
|
+
if (value === void 0)
|
|
818
|
+
return;
|
|
819
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 3) {
|
|
820
|
+
errors.push({
|
|
821
|
+
path: path3,
|
|
822
|
+
code: "INVALID_PROTECTED_RELATIONSHIP_PATHS",
|
|
823
|
+
message: "protected relationships must contain 1 through 3 reviewed paths."
|
|
824
|
+
});
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
value.forEach((candidate, pathIndex) => {
|
|
828
|
+
const candidatePath = `${path3}[${pathIndex}]`;
|
|
829
|
+
if (!isRecord(candidate)) {
|
|
830
|
+
errors.push({ path: candidatePath, code: "PROTECTED_RELATIONSHIP_PATH_NOT_OBJECT", message: "protected relationship path must be an object." });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
checkUnknownKeys(candidate, PROTECTED_RELATIONSHIP_PATH_KEYS, candidatePath, errors);
|
|
834
|
+
if (!isSafeIdentifier(candidate.name)) {
|
|
835
|
+
errors.push({ path: `${candidatePath}.name`, code: "INVALID_PROTECTED_RELATIONSHIP_IDENTIFIER", message: "relationship path name must be a fixed safe identifier." });
|
|
836
|
+
} else if (relationshipNames.has(candidate.name)) {
|
|
837
|
+
errors.push({ path: `${candidatePath}.name`, code: "DUPLICATE_PROTECTED_RELATIONSHIP", message: "protected relationship names must be unique." });
|
|
838
|
+
} else {
|
|
839
|
+
relationshipNames.add(candidate.name);
|
|
840
|
+
}
|
|
841
|
+
if (!Array.isArray(candidate.links) || candidate.links.length < 1 || candidate.links.length > 2) {
|
|
842
|
+
errors.push({ path: `${candidatePath}.links`, code: "INVALID_PROTECTED_RELATIONSHIP_LINKS", message: "a protected relationship path requires one or two reviewed many-to-one links." });
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
candidate.links.forEach((link, linkIndex) => {
|
|
846
|
+
const linkPath = `${candidatePath}.links[${linkIndex}]`;
|
|
847
|
+
if (!isRecord(link)) {
|
|
848
|
+
errors.push({ path: linkPath, code: "PROTECTED_RELATIONSHIP_LINK_NOT_OBJECT", message: "protected relationship link must be an object." });
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
checkUnknownKeys(link, PROTECTED_RELATIONSHIP_LINK_KEYS, linkPath, errors);
|
|
852
|
+
for (const key of ["schema", "table", "primary_key", "tenant_key", "local_key", "target_key"]) {
|
|
853
|
+
if (!isSafeIdentifier(link[key]))
|
|
854
|
+
errors.push({ path: `${linkPath}.${key}`, code: "INVALID_PROTECTED_RELATIONSHIP_IDENTIFIER", message: `${key} must be a fixed safe identifier.` });
|
|
855
|
+
}
|
|
856
|
+
if (link.principal_scope_key !== void 0 && !isSafeIdentifier(link.principal_scope_key)) {
|
|
857
|
+
errors.push({ path: `${linkPath}.principal_scope_key`, code: "INVALID_PROTECTED_RELATIONSHIP_IDENTIFIER", message: "principal_scope_key must be a fixed safe identifier." });
|
|
858
|
+
}
|
|
859
|
+
if (link.cardinality !== "many_to_one" || link.max_fan_out !== 1) {
|
|
860
|
+
errors.push({ path: linkPath, code: "PROTECTED_RELATIONSHIP_CARDINALITY_FORBIDDEN", message: "every protected relationship link must be reviewed many-to-one with max_fan_out 1." });
|
|
861
|
+
}
|
|
862
|
+
if (link.unmatched_rows !== "exclude" && link.unmatched_rows !== "keep_null") {
|
|
863
|
+
errors.push({ path: `${linkPath}.unmatched_rows`, code: "PROTECTED_RELATIONSHIP_NULL_SEMANTICS_REQUIRED", message: "every protected relationship link must freeze exclude or keep_null unmatched-row semantics." });
|
|
864
|
+
}
|
|
865
|
+
if (linkIndex === 0 && isSafeIdentifier(link.local_key) && (keptOut.has(link.local_key) || trustedScopeFields.has(link.local_key))) {
|
|
866
|
+
errors.push({
|
|
867
|
+
path: `${linkPath}.local_key`,
|
|
868
|
+
code: "PROTECTED_RELATIONSHIP_LOCAL_KEY_FORBIDDEN",
|
|
869
|
+
message: "the reviewed relationship cannot join through a kept-out or trusted-scope subject field."
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
}
|
|
791
875
|
function validateProtectedAggregate(value, input) {
|
|
792
876
|
const { path: path3, errors } = input;
|
|
793
877
|
if (!isRecord(value)) {
|
|
@@ -811,14 +895,14 @@ function validateProtectedAggregate(value, input) {
|
|
|
811
895
|
validateUniqueIdentifier(measure.name, measureNames, `${measurePath}.name`, "DUPLICATE_PROTECTED_MEASURE_NAME", errors);
|
|
812
896
|
if (!["count", "count_distinct", "sum", "avg"].includes(String(measure.function)))
|
|
813
897
|
errors.push({ path: `${measurePath}.function`, code: "INVALID_PROTECTED_MEASURE_FUNCTION", message: "measure function must be count, count_distinct, sum, or avg." });
|
|
814
|
-
validateProtectedRelationshipReference(measure.relationship, input.
|
|
898
|
+
validateProtectedRelationshipReference(measure.relationship, input.relationshipNames, `${measurePath}.relationship`, errors);
|
|
815
899
|
if (measure.function === "count") {
|
|
816
900
|
if (measure.field !== void 0)
|
|
817
901
|
errors.push({ path: `${measurePath}.field`, code: "PROTECTED_COUNT_FIELD_FORBIDDEN", message: "count measures count scoped subject rows and must not declare a field." });
|
|
818
902
|
if (measure.relationship !== void 0)
|
|
819
903
|
errors.push({ path: `${measurePath}.relationship`, code: "PROTECTED_COUNT_RELATIONSHIP_FORBIDDEN", message: "count measures count the scoped subject entity, not a joined relation." });
|
|
820
904
|
} else {
|
|
821
|
-
validateProtectedFieldReference(measure.field, measure.relationship, input.
|
|
905
|
+
validateProtectedFieldReference(measure.field, measure.relationship, input.relationshipNames, input.keptOut, input.trustedScopeFields, measurePath, errors);
|
|
822
906
|
}
|
|
823
907
|
});
|
|
824
908
|
}
|
|
@@ -835,7 +919,7 @@ function validateProtectedAggregate(value, input) {
|
|
|
835
919
|
}
|
|
836
920
|
checkUnknownKeys(dimension, PROTECTED_DIMENSION_KEYS, dimensionPath, errors);
|
|
837
921
|
validateUniqueIdentifier(dimension.name, dimensionNames, `${dimensionPath}.name`, "DUPLICATE_PROTECTED_DIMENSION_NAME", errors);
|
|
838
|
-
validateProtectedFieldReference(dimension.field, dimension.relationship, input.
|
|
922
|
+
validateProtectedFieldReference(dimension.field, dimension.relationship, input.relationshipNames, input.keptOut, input.trustedScopeFields, dimensionPath, errors);
|
|
839
923
|
});
|
|
840
924
|
}
|
|
841
925
|
}
|
|
@@ -849,7 +933,7 @@ function validateProtectedAggregate(value, input) {
|
|
|
849
933
|
errors.push({ path: `${bucketPath}.name`, code: "INVALID_PROTECTED_TIME_BUCKET_NAME", message: "time bucket name must be a safe identifier." });
|
|
850
934
|
if (value.time_bucket.bucket !== "day" && value.time_bucket.bucket !== "week" && value.time_bucket.bucket !== "month")
|
|
851
935
|
errors.push({ path: `${bucketPath}.bucket`, code: "INVALID_PROTECTED_TIME_BUCKET", message: "time bucket must be day, week, or month." });
|
|
852
|
-
validateProtectedFieldReference(value.time_bucket.field, value.time_bucket.relationship, input.
|
|
936
|
+
validateProtectedFieldReference(value.time_bucket.field, value.time_bucket.relationship, input.relationshipNames, input.keptOut, input.trustedScopeFields, bucketPath, errors);
|
|
853
937
|
}
|
|
854
938
|
}
|
|
855
939
|
if (value.comparison !== void 0) {
|
|
@@ -858,7 +942,7 @@ function validateProtectedAggregate(value, input) {
|
|
|
858
942
|
errors.push({ path: comparisonPath, code: "PROTECTED_COMPARISON_NOT_OBJECT", message: "comparison must be an object." });
|
|
859
943
|
} else {
|
|
860
944
|
checkUnknownKeys(value.comparison, PROTECTED_COMPARISON_KEYS, comparisonPath, errors);
|
|
861
|
-
validateProtectedFieldReference(value.comparison.field, value.comparison.relationship, input.
|
|
945
|
+
validateProtectedFieldReference(value.comparison.field, value.comparison.relationship, input.relationshipNames, input.keptOut, input.trustedScopeFields, comparisonPath, errors);
|
|
862
946
|
if (!Array.isArray(value.comparison.ranges) || value.comparison.ranges.length < 1 || value.comparison.ranges.length > 2) {
|
|
863
947
|
errors.push({ path: `${comparisonPath}.ranges`, code: "INVALID_PROTECTED_COMPARISON_RANGES", message: "comparison requires one or two bounded time ranges." });
|
|
864
948
|
} else {
|
|
@@ -949,19 +1033,20 @@ function validateProtectedValue(value, args, referencedArgs, path3, errors) {
|
|
|
949
1033
|
}
|
|
950
1034
|
}
|
|
951
1035
|
}
|
|
952
|
-
function validateProtectedFieldReference(field, relationship,
|
|
1036
|
+
function validateProtectedFieldReference(field, relationship, relationshipNames, keptOut, trustedScopeFields, path3, errors) {
|
|
953
1037
|
if (!isSafeIdentifier(field)) {
|
|
954
1038
|
errors.push({ path: `${path3}.field`, code: "INVALID_PROTECTED_FIELD", message: "protected field must be a fixed safe identifier." });
|
|
955
1039
|
} else if (relationship === void 0 && (keptOut.has(field) || trustedScopeFields.has(field))) {
|
|
956
1040
|
errors.push({ path: `${path3}.field`, code: "PROTECTED_FIELD_FORBIDDEN", message: "kept-out and trusted-scope fields cannot be selected, filtered, grouped, sorted, joined, aggregated, or counted distinctly." });
|
|
957
1041
|
}
|
|
958
|
-
validateProtectedRelationshipReference(relationship,
|
|
1042
|
+
validateProtectedRelationshipReference(relationship, relationshipNames, `${path3}.relationship`, errors);
|
|
959
1043
|
}
|
|
960
|
-
function validateProtectedRelationshipReference(value,
|
|
1044
|
+
function validateProtectedRelationshipReference(value, relationshipNames, path3, errors) {
|
|
961
1045
|
if (value === void 0)
|
|
962
1046
|
return;
|
|
963
|
-
if (!isSafeIdentifier(value) || value
|
|
964
|
-
errors.push({ path: path3, code: "UNKNOWN_PROTECTED_RELATIONSHIP", message: "relationship must reference the capability's
|
|
1047
|
+
if (!isSafeIdentifier(value) || !relationshipNames.has(value)) {
|
|
1048
|
+
errors.push({ path: path3, code: "UNKNOWN_PROTECTED_RELATIONSHIP", message: "relationship must reference one of the capability's reviewed many-to-one paths." });
|
|
1049
|
+
}
|
|
965
1050
|
}
|
|
966
1051
|
function validateUniqueIdentifier(value, seen, path3, code, errors) {
|
|
967
1052
|
if (!isSafeIdentifier(value)) {
|
|
@@ -2191,7 +2276,7 @@ function validateRunnerCapabilityConfig(input) {
|
|
|
2191
2276
|
);
|
|
2192
2277
|
validateNotifications(input.notifications, strict, errors);
|
|
2193
2278
|
validateSupervisedWorker(input.supervised_worker, input, strict, errors);
|
|
2194
|
-
validateProposalFreshness(input.proposal_freshness, input.capabilities, strict, errors);
|
|
2279
|
+
validateProposalFreshness(input.proposal_freshness, input.capabilities, strict, errors, hasContracts);
|
|
2195
2280
|
validateEffectiveContextCompatibility(input.trusted_context, input.contexts, input.capabilities, errors);
|
|
2196
2281
|
validateApprovalPolicyReferences(input.capabilities, input.policies, errors);
|
|
2197
2282
|
validateWritebackReadiness(input.sources, input.capabilities, input.mode, errors, warnings);
|
|
@@ -3597,7 +3682,7 @@ function validateCapabilities2(value, sources, contexts, executors, mode, strict
|
|
|
3597
3682
|
capabilityNames.set(capability.name, index);
|
|
3598
3683
|
});
|
|
3599
3684
|
}
|
|
3600
|
-
function validateProposalFreshness(value, capabilitiesValue, strict, errors) {
|
|
3685
|
+
function validateProposalFreshness(value, capabilitiesValue, strict, errors, hasContracts = false) {
|
|
3601
3686
|
if (value === void 0) return;
|
|
3602
3687
|
if (!isRecord2(value)) {
|
|
3603
3688
|
errors.push({
|
|
@@ -3625,7 +3710,9 @@ function validateProposalFreshness(value, capabilitiesValue, strict, errors) {
|
|
|
3625
3710
|
}
|
|
3626
3711
|
const proposal = byName.get(proposalName);
|
|
3627
3712
|
if (!proposal) {
|
|
3628
|
-
|
|
3713
|
+
if (!hasContracts) {
|
|
3714
|
+
errors.push({ path: path3, code: "FRESHNESS_PROPOSAL_UNKNOWN", message: `Freshness policy references unknown proposal capability ${proposalName}.` });
|
|
3715
|
+
}
|
|
3629
3716
|
continue;
|
|
3630
3717
|
}
|
|
3631
3718
|
if (proposal.kind !== "proposal") {
|
|
@@ -3675,7 +3762,9 @@ function validateProposalFreshness(value, capabilitiesValue, strict, errors) {
|
|
|
3675
3762
|
}
|
|
3676
3763
|
const dependency = byName.get(String(rawDependency.capability));
|
|
3677
3764
|
if (!dependency) {
|
|
3678
|
-
|
|
3765
|
+
if (!hasContracts) {
|
|
3766
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_UNKNOWN", message: `Unknown supporting capability ${String(rawDependency.capability)}.` });
|
|
3767
|
+
}
|
|
3679
3768
|
continue;
|
|
3680
3769
|
}
|
|
3681
3770
|
if (dependency.kind !== "read" || dependency.protected_read !== void 0 || dependency.aggregate !== void 0) {
|
|
@@ -6246,10 +6335,16 @@ function quotePostgresIdentifier(identifier) {
|
|
|
6246
6335
|
return `"${identifier}"`;
|
|
6247
6336
|
}
|
|
6248
6337
|
async function inspectPostgresRlsTarget(client, input) {
|
|
6338
|
+
return inspectPostgresRlsTargetInternal(client, input, /* @__PURE__ */ new Set());
|
|
6339
|
+
}
|
|
6340
|
+
async function inspectPostgresRlsTargetInternal(client, input, ancestors) {
|
|
6341
|
+
const targetKey = `${input.schema}.${input.table}`;
|
|
6249
6342
|
const metadata = await client.query(
|
|
6250
6343
|
`SELECT
|
|
6251
6344
|
current_user AS role,
|
|
6252
6345
|
c.oid::text AS table_oid,
|
|
6346
|
+
c.relkind AS relation_kind,
|
|
6347
|
+
c.reloptions AS relation_options,
|
|
6253
6348
|
c.relrowsecurity AS row_security_enabled,
|
|
6254
6349
|
c.relforcerowsecurity AS force_row_security,
|
|
6255
6350
|
owner.rolname = current_user AS role_is_table_owner,
|
|
@@ -6259,19 +6354,26 @@ async function inspectPostgresRlsTarget(client, input) {
|
|
|
6259
6354
|
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
6260
6355
|
JOIN pg_catalog.pg_roles owner ON owner.oid = c.relowner
|
|
6261
6356
|
JOIN pg_catalog.pg_roles current_role_info ON current_role_info.rolname = current_user
|
|
6262
|
-
WHERE n.nspname = $1 AND c.relname = $2 AND c.relkind IN ('r', 'p')
|
|
6357
|
+
WHERE n.nspname = $1 AND c.relname = $2 AND c.relkind IN ('r', 'p', 'v')
|
|
6263
6358
|
LIMIT 1`,
|
|
6264
6359
|
[input.schema, input.table]
|
|
6265
6360
|
);
|
|
6266
6361
|
const row = metadata.rows[0];
|
|
6362
|
+
const relationKind = row?.relation_kind === "v" ? "view" : row?.relation_kind === "p" ? "partitioned_table" : "table";
|
|
6363
|
+
const relationOptions = Array.isArray(row?.relation_options) ? row.relation_options.filter((value) => typeof value === "string") : [];
|
|
6267
6364
|
const requiredOperations = [...new Set(input.operations)].sort();
|
|
6268
6365
|
const report = {
|
|
6269
6366
|
schema: input.schema,
|
|
6270
6367
|
table: input.table,
|
|
6368
|
+
...row ? { target_kind: relationKind } : {},
|
|
6271
6369
|
role: typeof row?.role === "string" ? row.role : "unknown",
|
|
6272
6370
|
table_exists: Boolean(row),
|
|
6273
6371
|
row_security_enabled: row?.row_security_enabled === true,
|
|
6274
6372
|
force_row_security: row?.force_row_security === true,
|
|
6373
|
+
...relationKind === "view" && row ? {
|
|
6374
|
+
security_invoker: relationOptions.includes("security_invoker=true"),
|
|
6375
|
+
security_barrier: relationOptions.includes("security_barrier=true")
|
|
6376
|
+
} : {},
|
|
6275
6377
|
role_is_superuser: row?.role_is_superuser === true,
|
|
6276
6378
|
role_has_bypassrls: row?.role_has_bypassrls === true,
|
|
6277
6379
|
role_is_table_owner: row?.role_is_table_owner === true,
|
|
@@ -6286,6 +6388,70 @@ async function inspectPostgresRlsTarget(client, input) {
|
|
|
6286
6388
|
report.errors.push("POSTGRES_RLS_TARGET_NOT_FOUND");
|
|
6287
6389
|
return report;
|
|
6288
6390
|
}
|
|
6391
|
+
if (relationKind === "view") {
|
|
6392
|
+
if (ancestors.has(targetKey)) {
|
|
6393
|
+
report.errors.push("POSTGRES_RLS_VIEW_DEPENDENCY_CYCLE");
|
|
6394
|
+
return report;
|
|
6395
|
+
}
|
|
6396
|
+
if (requiredOperations.some((operation) => operation !== "SELECT")) {
|
|
6397
|
+
report.errors.push("POSTGRES_RLS_VIEW_READ_ONLY_REQUIRED");
|
|
6398
|
+
}
|
|
6399
|
+
if (!report.security_invoker) report.errors.push("POSTGRES_RLS_VIEW_SECURITY_INVOKER_REQUIRED");
|
|
6400
|
+
if (!report.security_barrier) report.errors.push("POSTGRES_RLS_VIEW_SECURITY_BARRIER_REQUIRED");
|
|
6401
|
+
if (report.role_is_superuser) report.errors.push("POSTGRES_RLS_ROLE_SUPERUSER");
|
|
6402
|
+
if (report.role_has_bypassrls) report.errors.push("POSTGRES_RLS_ROLE_BYPASSRLS");
|
|
6403
|
+
if (report.role_is_table_owner) report.errors.push("POSTGRES_RLS_ROLE_TABLE_OWNER");
|
|
6404
|
+
const dependencies = await client.query(
|
|
6405
|
+
`SELECT DISTINCT
|
|
6406
|
+
dependency_namespace.nspname AS schema_name,
|
|
6407
|
+
dependency_relation.relname AS relation_name,
|
|
6408
|
+
dependency_relation.relkind AS relation_kind
|
|
6409
|
+
FROM pg_catalog.pg_rewrite rewrite
|
|
6410
|
+
JOIN pg_catalog.pg_depend dependency
|
|
6411
|
+
ON dependency.classid = 'pg_rewrite'::regclass
|
|
6412
|
+
AND dependency.objid = rewrite.oid
|
|
6413
|
+
AND dependency.refclassid = 'pg_class'::regclass
|
|
6414
|
+
JOIN pg_catalog.pg_class dependency_relation
|
|
6415
|
+
ON dependency_relation.oid = dependency.refobjid
|
|
6416
|
+
JOIN pg_catalog.pg_namespace dependency_namespace
|
|
6417
|
+
ON dependency_namespace.oid = dependency_relation.relnamespace
|
|
6418
|
+
WHERE rewrite.ev_class = $1::oid
|
|
6419
|
+
AND dependency.deptype = 'n'
|
|
6420
|
+
AND dependency.refobjid <> rewrite.ev_class
|
|
6421
|
+
AND dependency_relation.relkind IN ('r', 'p', 'v')
|
|
6422
|
+
ORDER BY dependency_namespace.nspname, dependency_relation.relname`,
|
|
6423
|
+
[row.table_oid]
|
|
6424
|
+
);
|
|
6425
|
+
if (!dependencies.rows.length) report.errors.push("POSTGRES_RLS_VIEW_DEPENDENCIES_UNVERIFIED");
|
|
6426
|
+
const nextAncestors = new Set(ancestors);
|
|
6427
|
+
nextAncestors.add(targetKey);
|
|
6428
|
+
report.referenced_relations = [];
|
|
6429
|
+
for (const dependency of dependencies.rows) {
|
|
6430
|
+
if (typeof dependency.schema_name !== "string" || typeof dependency.relation_name !== "string") {
|
|
6431
|
+
report.errors.push("POSTGRES_RLS_VIEW_DEPENDENCIES_UNVERIFIED");
|
|
6432
|
+
continue;
|
|
6433
|
+
}
|
|
6434
|
+
const dependencyReport = await inspectPostgresRlsTargetInternal(client, {
|
|
6435
|
+
schema: dependency.schema_name,
|
|
6436
|
+
table: dependency.relation_name,
|
|
6437
|
+
scope: input.scope,
|
|
6438
|
+
operations: ["SELECT"]
|
|
6439
|
+
}, nextAncestors);
|
|
6440
|
+
report.referenced_relations.push({
|
|
6441
|
+
schema: dependencyReport.schema,
|
|
6442
|
+
table: dependencyReport.table,
|
|
6443
|
+
...dependencyReport.target_kind ? { target_kind: dependencyReport.target_kind } : {},
|
|
6444
|
+
ok: dependencyReport.ok,
|
|
6445
|
+
errors: dependencyReport.errors
|
|
6446
|
+
});
|
|
6447
|
+
for (const error of dependencyReport.errors) {
|
|
6448
|
+
report.errors.push(`POSTGRES_RLS_VIEW_DEPENDENCY_FAILED:${dependencyReport.schema}.${dependencyReport.table}:${error}`);
|
|
6449
|
+
}
|
|
6450
|
+
}
|
|
6451
|
+
report.errors = [...new Set(report.errors)];
|
|
6452
|
+
report.ok = report.errors.length === 0;
|
|
6453
|
+
return report;
|
|
6454
|
+
}
|
|
6289
6455
|
const policies = await client.query(
|
|
6290
6456
|
`SELECT
|
|
6291
6457
|
p.polname AS name,
|
|
@@ -13435,10 +13601,22 @@ var HIGH_CONFIDENCE_RULES = [
|
|
|
13435
13601
|
"paymentmethod",
|
|
13436
13602
|
"paymentinstrument",
|
|
13437
13603
|
"paymentdetails",
|
|
13604
|
+
"paymentcard",
|
|
13605
|
+
"cardonfile",
|
|
13606
|
+
"cardholder",
|
|
13607
|
+
"cardholdername",
|
|
13608
|
+
"cardexpiry",
|
|
13609
|
+
"cardexpiration",
|
|
13438
13610
|
"cardnumber",
|
|
13611
|
+
"fullpan",
|
|
13439
13612
|
"creditcard",
|
|
13440
13613
|
"debitcard",
|
|
13441
13614
|
"cardtoken",
|
|
13615
|
+
"card",
|
|
13616
|
+
"cc",
|
|
13617
|
+
"ccnumber",
|
|
13618
|
+
"ccnum",
|
|
13619
|
+
"pan",
|
|
13442
13620
|
"cvv",
|
|
13443
13621
|
"cvc",
|
|
13444
13622
|
"bankaccount",
|
|
@@ -13493,6 +13671,7 @@ var HIGH_CONFIDENCE_RULES = [
|
|
|
13493
13671
|
"phone",
|
|
13494
13672
|
"phonenumber",
|
|
13495
13673
|
"telephone",
|
|
13674
|
+
"address",
|
|
13496
13675
|
"streetaddress",
|
|
13497
13676
|
"homeaddress",
|
|
13498
13677
|
"mailingaddress",
|
|
@@ -13596,7 +13775,15 @@ function rule(code, reason, patterns) {
|
|
|
13596
13775
|
return {
|
|
13597
13776
|
code,
|
|
13598
13777
|
reason,
|
|
13599
|
-
matches: (value) => patterns.some((pattern) =>
|
|
13778
|
+
matches: (value) => patterns.some((pattern) => {
|
|
13779
|
+
if (pattern === "pan" || pattern === "cc" || pattern === "card") {
|
|
13780
|
+
return value.compact === pattern || value.descriptionTokens.has(pattern);
|
|
13781
|
+
}
|
|
13782
|
+
if (pattern.length <= 4) {
|
|
13783
|
+
return value.compact === pattern || value.tokens.has(pattern) || value.descriptionTokens.has(pattern);
|
|
13784
|
+
}
|
|
13785
|
+
return value.compact.includes(pattern) || value.descriptionCompact.includes(pattern);
|
|
13786
|
+
})
|
|
13600
13787
|
};
|
|
13601
13788
|
}
|
|
13602
13789
|
function normalizeEvidence(input) {
|
|
@@ -13606,6 +13793,7 @@ function normalizeEvidence(input) {
|
|
|
13606
13793
|
compact: identifier.replaceAll("_", ""),
|
|
13607
13794
|
tokens: new Set(identifier.split("_").filter(Boolean)),
|
|
13608
13795
|
descriptionCompact: description.replaceAll("_", ""),
|
|
13796
|
+
descriptionTokens: new Set(description.split("_").filter(Boolean)),
|
|
13609
13797
|
dataType: String(input.dataType ?? "").trim().toLowerCase()
|
|
13610
13798
|
};
|
|
13611
13799
|
}
|
|
@@ -13669,7 +13857,22 @@ function rolePostureFingerprint(inspection) {
|
|
|
13669
13857
|
}))
|
|
13670
13858
|
});
|
|
13671
13859
|
}
|
|
13672
|
-
var TENANT_COLUMNS = /* @__PURE__ */ new Set([
|
|
13860
|
+
var TENANT_COLUMNS = /* @__PURE__ */ new Set([
|
|
13861
|
+
"tenant_id",
|
|
13862
|
+
"account_id",
|
|
13863
|
+
"organization_id",
|
|
13864
|
+
"org_id",
|
|
13865
|
+
"workspace_id",
|
|
13866
|
+
"customer_id",
|
|
13867
|
+
"merchant_id",
|
|
13868
|
+
"store_id",
|
|
13869
|
+
"company_id",
|
|
13870
|
+
"team_id",
|
|
13871
|
+
"facility_id",
|
|
13872
|
+
"property_id",
|
|
13873
|
+
"clinic_id",
|
|
13874
|
+
"project_id"
|
|
13875
|
+
]);
|
|
13673
13876
|
var CONFLICT_COLUMNS = /* @__PURE__ */ new Set(["updated_at", "modified_at", "row_version", "version", "lock_version", "etag"]);
|
|
13674
13877
|
var IMMUTABLE_COLUMNS = /* @__PURE__ */ new Set(["id", "uuid", "created_at", "created_by"]);
|
|
13675
13878
|
var LARGE_OR_BINARY_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -13906,7 +14109,7 @@ async function inspectMysql(options) {
|
|
|
13906
14109
|
const [versionRows] = await connection.query("SELECT VERSION() AS version, CURRENT_USER() AS `current_user`");
|
|
13907
14110
|
const schemaParam = options.schema ?? null;
|
|
13908
14111
|
const [schemaRows] = await connection.query(
|
|
13909
|
-
`SELECT schema_name FROM information_schema.schemata
|
|
14112
|
+
`SELECT schema_name AS schema_name FROM information_schema.schemata
|
|
13910
14113
|
WHERE schema_name NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
|
|
13911
14114
|
ORDER BY schema_name`
|
|
13912
14115
|
);
|
|
@@ -13922,8 +14125,8 @@ async function inspectMysql(options) {
|
|
|
13922
14125
|
);
|
|
13923
14126
|
const [columnRows] = await connection.query(
|
|
13924
14127
|
`SELECT table_schema AS \`schema\`, table_name AS table_name, column_name AS name,
|
|
13925
|
-
data_type, column_type AS udt_name, is_nullable,
|
|
13926
|
-
column_default, extra AS is_generated, extra AS is_identity,
|
|
14128
|
+
data_type AS data_type, column_type AS udt_name, is_nullable AS is_nullable,
|
|
14129
|
+
column_default AS column_default, extra AS is_generated, extra AS is_identity,
|
|
13927
14130
|
ordinal_position AS ordinal_position, column_comment AS comment
|
|
13928
14131
|
FROM information_schema.columns
|
|
13929
14132
|
WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
|
|
@@ -17430,8 +17633,8 @@ async function preflightPostgresDatabaseScope(inputConfig, env = process.env, cr
|
|
|
17430
17633
|
}
|
|
17431
17634
|
}
|
|
17432
17635
|
}
|
|
17433
|
-
var SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS = /* @__PURE__ */ new Set(["1.6.0", "1.6.3"]);
|
|
17434
|
-
var SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS = /* @__PURE__ */ new Set(["1.5.0", "1.5.1", "1.6.0"]);
|
|
17636
|
+
var SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS = /* @__PURE__ */ new Set(["1.6.0", "1.6.3", "1.6.4"]);
|
|
17637
|
+
var SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS = /* @__PURE__ */ new Set(["1.5.0", "1.5.1", "1.6.0", "1.7.0"]);
|
|
17435
17638
|
async function preflightGeneratedAuthority(inputConfig, env = process.env, inspect = inspectDatabase) {
|
|
17436
17639
|
const config = resolveRuntimeConfig(inputConfig);
|
|
17437
17640
|
const protectedCapabilities = localCapabilities(config).filter((capability) => capability.protected_read);
|
|
@@ -17472,20 +17675,37 @@ async function preflightGeneratedAuthority(inputConfig, env = process.env, inspe
|
|
|
17472
17675
|
const inspection = await inspect({
|
|
17473
17676
|
engine: lock.engine,
|
|
17474
17677
|
databaseUrlEnv: lock.source_env,
|
|
17678
|
+
...lock.inspected_schema ? { schema: lock.inspected_schema } : {},
|
|
17475
17679
|
statementTimeoutMs: Math.min(...protectedCapabilities.map((capability) => capability.protected_read.limits.statement_timeout_ms)),
|
|
17476
17680
|
env
|
|
17477
17681
|
});
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17682
|
+
if (lock.authority_dependencies) {
|
|
17683
|
+
if (credentialPostureFingerprintForGeneratedAuthority(inspection) !== lock.authority_dependencies.credential_posture_fingerprint) {
|
|
17684
|
+
throw new McpRuntimeError(
|
|
17685
|
+
"GENERATED_AUTHORITY_DRIFT",
|
|
17686
|
+
"Generated protected authority is stale because the database credential posture changed. Rescan, review, and activate a new digest."
|
|
17687
|
+
);
|
|
17688
|
+
}
|
|
17689
|
+
for (const capability of protectedCapabilities) {
|
|
17690
|
+
assertProtectedCapabilityDependenciesCurrent(
|
|
17691
|
+
capability,
|
|
17692
|
+
lock.authority_dependencies,
|
|
17693
|
+
inspection
|
|
17694
|
+
);
|
|
17695
|
+
}
|
|
17696
|
+
} else {
|
|
17697
|
+
const schemaFingerprint = schemaFingerprintForInspection(inspection);
|
|
17698
|
+
const postureFingerprint = rolePostureFingerprint(inspection);
|
|
17699
|
+
const changes = [
|
|
17700
|
+
...schemaFingerprint !== lock.schema_fingerprint ? ["schema metadata"] : [],
|
|
17701
|
+
...postureFingerprint !== lock.role_posture_fingerprint ? ["database role, grants, ownership, or RLS posture"] : []
|
|
17702
|
+
];
|
|
17703
|
+
if (changes.length > 0) {
|
|
17704
|
+
throw new McpRuntimeError(
|
|
17705
|
+
"GENERATED_AUTHORITY_DRIFT",
|
|
17706
|
+
`Generated protected authority is stale because ${changes.join(" and ")} changed. Rescan, review the semantic diff, regenerate, and activate a new digest.`
|
|
17707
|
+
);
|
|
17708
|
+
}
|
|
17489
17709
|
}
|
|
17490
17710
|
const role = inspection.role_posture;
|
|
17491
17711
|
if (!role?.verified || !role.read_only || role.superuser !== false || role.bypass_rls !== false || role.writable_relations.length > 0 || role.owned_relations.length > 0) {
|
|
@@ -17497,13 +17717,194 @@ async function preflightGeneratedAuthority(inputConfig, env = process.env, inspe
|
|
|
17497
17717
|
}
|
|
17498
17718
|
function assertGeneratedAuthorityLockShape(value) {
|
|
17499
17719
|
const digest = /^sha256:[a-f0-9]{64}$/;
|
|
17500
|
-
if (!value || value.schema_version !== "synapsor.generation-lock.v1" || !SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS.has(value.compiler_version) || !SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS.has(value.spec_version) || value.engine !== "postgres" && value.engine !== "mysql" || !/^[A-Z_][A-Z0-9_]*$/.test(value.source_env) || !digest.test(value.schema_fingerprint) || !digest.test(value.role_posture_fingerprint) || !digest.test(value.evidence_fingerprint) || !digest.test(value.generated_contract_digest) || !digest.test(value.reviewed_overrides_digest) || !Array.isArray(value.protected_authority) || value.protected_authority.some((item) => typeof item !== "string")) {
|
|
17720
|
+
if (!value || value.schema_version !== "synapsor.generation-lock.v1" || !SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS.has(value.compiler_version) || !SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS.has(value.spec_version) || value.engine !== "postgres" && value.engine !== "mysql" || !/^[A-Z_][A-Z0-9_]*$/.test(value.source_env) || value.inspected_schema !== void 0 && (typeof value.inspected_schema !== "string" || !value.inspected_schema) || !digest.test(value.schema_fingerprint) || !digest.test(value.role_posture_fingerprint) || !digest.test(value.evidence_fingerprint) || !digest.test(value.generated_contract_digest) || !digest.test(value.reviewed_overrides_digest) || !Array.isArray(value.protected_authority) || value.protected_authority.some((item) => typeof item !== "string") || value.authority_dependencies !== void 0 && !generatedAuthorityDependenciesValid(value.authority_dependencies, digest)) {
|
|
17501
17721
|
throw new McpRuntimeError(
|
|
17502
17722
|
"GENERATION_LOCK_INVALID",
|
|
17503
17723
|
"The generated-authority lock is malformed or belongs to an unsupported compiler/spec version."
|
|
17504
17724
|
);
|
|
17505
17725
|
}
|
|
17506
17726
|
}
|
|
17727
|
+
function generatedAuthorityDependenciesValid(value, digest) {
|
|
17728
|
+
if (!value || value.schema_version !== "synapsor.authority-dependencies.v1" || !digest.test(value.credential_posture_fingerprint) || !isRecord5(value.resources) || !isRecord5(value.relationships)) {
|
|
17729
|
+
return false;
|
|
17730
|
+
}
|
|
17731
|
+
for (const dependency of Object.values(value.resources)) {
|
|
17732
|
+
if (!dependency || typeof dependency.schema !== "string" || typeof dependency.table !== "string" || !Array.isArray(dependency.fields) || dependency.fields.some((field) => typeof field !== "string") || !digest.test(dependency.fingerprint)) {
|
|
17733
|
+
return false;
|
|
17734
|
+
}
|
|
17735
|
+
}
|
|
17736
|
+
for (const dependency of Object.values(value.relationships)) {
|
|
17737
|
+
if (!dependency || typeof dependency.root_resource !== "string" || typeof dependency.relationship_id !== "string" || !Array.isArray(dependency.links) || dependency.links.length < 1 || dependency.links.length > 2 || !digest.test(dependency.proof_digest) || canonicalJsonDigest(dependency.links) !== dependency.proof_digest) {
|
|
17738
|
+
return false;
|
|
17739
|
+
}
|
|
17740
|
+
}
|
|
17741
|
+
return true;
|
|
17742
|
+
}
|
|
17743
|
+
function credentialPostureFingerprintForGeneratedAuthority(inspection) {
|
|
17744
|
+
const role = inspection.role_posture;
|
|
17745
|
+
return canonicalJsonDigest({
|
|
17746
|
+
engine: inspection.engine,
|
|
17747
|
+
current_user: inspection.current_user,
|
|
17748
|
+
role: role ? {
|
|
17749
|
+
verified: role.verified,
|
|
17750
|
+
superuser: role.superuser,
|
|
17751
|
+
bypass_rls: role.bypass_rls,
|
|
17752
|
+
read_only: role.read_only,
|
|
17753
|
+
writable_relations: [...role.writable_relations].sort(),
|
|
17754
|
+
owned_relations: [...role.owned_relations].sort()
|
|
17755
|
+
} : null
|
|
17756
|
+
});
|
|
17757
|
+
}
|
|
17758
|
+
function assertProtectedCapabilityDependenciesCurrent(capability, dependencies, inspection) {
|
|
17759
|
+
const root = `${capability.target.schema}.${capability.target.table}`;
|
|
17760
|
+
const resourceIds = /* @__PURE__ */ new Set([root]);
|
|
17761
|
+
const protectedRead = capability.protected_read;
|
|
17762
|
+
const paths = [
|
|
17763
|
+
...protectedRead.relationship ? [{
|
|
17764
|
+
name: protectedRead.relationship.name,
|
|
17765
|
+
links: [{
|
|
17766
|
+
schema: protectedRead.relationship.schema,
|
|
17767
|
+
table: protectedRead.relationship.table,
|
|
17768
|
+
local_key: protectedRead.relationship.local_key,
|
|
17769
|
+
target_key: protectedRead.relationship.target_key,
|
|
17770
|
+
cardinality: protectedRead.relationship.cardinality,
|
|
17771
|
+
max_fan_out: protectedRead.relationship.max_fan_out
|
|
17772
|
+
}]
|
|
17773
|
+
}] : [],
|
|
17774
|
+
...(protectedRead.relationships ?? []).map((path3) => ({
|
|
17775
|
+
name: path3.name,
|
|
17776
|
+
links: path3.links.map((link) => ({
|
|
17777
|
+
schema: link.schema,
|
|
17778
|
+
table: link.table,
|
|
17779
|
+
local_key: link.local_key,
|
|
17780
|
+
target_key: link.target_key,
|
|
17781
|
+
cardinality: link.cardinality,
|
|
17782
|
+
max_fan_out: link.max_fan_out
|
|
17783
|
+
}))
|
|
17784
|
+
}))
|
|
17785
|
+
];
|
|
17786
|
+
for (const path3 of paths) {
|
|
17787
|
+
const dependency = dependencies.relationships[`${root}::${path3.name}`];
|
|
17788
|
+
if (!dependency || dependency.root_resource !== root || dependency.relationship_id !== path3.name || path3.links.length !== dependency.links.length) {
|
|
17789
|
+
throw generatedDependencyDrift(capability, `relationship ${path3.name} is not bound to its generation-lock proof`);
|
|
17790
|
+
}
|
|
17791
|
+
let source = root;
|
|
17792
|
+
for (const [index, link] of path3.links.entries()) {
|
|
17793
|
+
const expected = dependency.links[index];
|
|
17794
|
+
const target = `${link.schema}.${link.table}`;
|
|
17795
|
+
if (expected.source_resource !== source || expected.target_resource !== target || expected.source_columns.length !== 1 || expected.source_columns[0] !== link.local_key || expected.target_columns.length !== 1 || expected.target_columns[0] !== link.target_key || link.cardinality !== "many_to_one" || link.max_fan_out !== 1) {
|
|
17796
|
+
throw generatedDependencyDrift(capability, `relationship ${path3.name} no longer matches its reviewed structural path`);
|
|
17797
|
+
}
|
|
17798
|
+
resourceIds.add(source);
|
|
17799
|
+
resourceIds.add(target);
|
|
17800
|
+
source = target;
|
|
17801
|
+
}
|
|
17802
|
+
if (relationshipDependencyFingerprintForGeneratedAuthority(dependency, inspection) !== dependency.proof_digest) {
|
|
17803
|
+
throw generatedDependencyDrift(capability, `relationship ${path3.name} foreign-key or uniqueness proof changed`);
|
|
17804
|
+
}
|
|
17805
|
+
}
|
|
17806
|
+
for (const resourceId of resourceIds) {
|
|
17807
|
+
const dependency = dependencies.resources[resourceId];
|
|
17808
|
+
if (!dependency || resourceDependencyFingerprintForGeneratedAuthority(dependency, inspection) !== dependency.fingerprint) {
|
|
17809
|
+
throw generatedDependencyDrift(capability, `resource ${resourceId} authority-bearing schema or RLS metadata changed`);
|
|
17810
|
+
}
|
|
17811
|
+
}
|
|
17812
|
+
}
|
|
17813
|
+
function generatedDependencyDrift(capability, reason) {
|
|
17814
|
+
return new McpRuntimeError(
|
|
17815
|
+
"GENERATED_AUTHORITY_DRIFT",
|
|
17816
|
+
`Generated protected capability ${capability.name} is stale because ${reason}. Rescan, review, and activate a new digest.`
|
|
17817
|
+
);
|
|
17818
|
+
}
|
|
17819
|
+
function resourceDependencyFingerprintForGeneratedAuthority(dependency, inspection) {
|
|
17820
|
+
const table = inspection.tables.find((candidate) => candidate.schema === dependency.schema && candidate.name === dependency.table);
|
|
17821
|
+
if (!table) return void 0;
|
|
17822
|
+
const columns = new Map(table.columns.map((column) => [column.name, column]));
|
|
17823
|
+
const selectedColumns = dependency.fields.map((name) => {
|
|
17824
|
+
const column = columns.get(name);
|
|
17825
|
+
if (!column) return void 0;
|
|
17826
|
+
return {
|
|
17827
|
+
name: column.name,
|
|
17828
|
+
data_type: column.data_type,
|
|
17829
|
+
nullable: column.nullable,
|
|
17830
|
+
default: column.default ?? null,
|
|
17831
|
+
generated: column.generated,
|
|
17832
|
+
identity: column.identity ?? false,
|
|
17833
|
+
enum_values: [...column.enum_values ?? []].sort()
|
|
17834
|
+
};
|
|
17835
|
+
});
|
|
17836
|
+
if (selectedColumns.some((column) => !column)) return void 0;
|
|
17837
|
+
return canonicalJsonDigest({
|
|
17838
|
+
engine: inspection.engine,
|
|
17839
|
+
schema: table.schema,
|
|
17840
|
+
table: table.name,
|
|
17841
|
+
type: table.type,
|
|
17842
|
+
primary_key: [...table.primary_key],
|
|
17843
|
+
columns: selectedColumns,
|
|
17844
|
+
row_level_security: table.row_level_security ?? "unknown",
|
|
17845
|
+
row_level_security_policies: [...table.row_level_security_policies ?? []].map((policy) => ({
|
|
17846
|
+
name: policy.name,
|
|
17847
|
+
command: policy.command,
|
|
17848
|
+
permissive: policy.permissive,
|
|
17849
|
+
roles: [...policy.roles].sort(),
|
|
17850
|
+
using_expression: policy.using_expression ?? null,
|
|
17851
|
+
check_expression: policy.check_expression ?? null
|
|
17852
|
+
})).sort((left, right) => left.name.localeCompare(right.name)),
|
|
17853
|
+
role_posture: table.role_posture ? {
|
|
17854
|
+
owner: table.role_posture.owner,
|
|
17855
|
+
current_role_is_owner: table.role_posture.current_role_is_owner,
|
|
17856
|
+
current_role_can_assume_owner: table.role_posture.current_role_can_assume_owner,
|
|
17857
|
+
privileges: table.role_posture.privileges,
|
|
17858
|
+
row_security_forced: table.role_posture.row_security_forced,
|
|
17859
|
+
row_security_effective_for_current_role: table.role_posture.row_security_effective_for_current_role
|
|
17860
|
+
} : null
|
|
17861
|
+
});
|
|
17862
|
+
}
|
|
17863
|
+
function relationshipDependencyFingerprintForGeneratedAuthority(dependency, inspection) {
|
|
17864
|
+
const tables = new Map(inspection.tables.map((table) => [`${table.schema}.${table.name}`, table]));
|
|
17865
|
+
const links = [];
|
|
17866
|
+
for (const expected of dependency.links) {
|
|
17867
|
+
const source = tables.get(expected.source_resource);
|
|
17868
|
+
const target = tables.get(expected.target_resource);
|
|
17869
|
+
if (!source || !target) return void 0;
|
|
17870
|
+
const foreignKey = source.foreign_keys.find((candidate) => candidate.name === expected.constraint_name && candidate.referenced_schema === target.schema && candidate.referenced_table === target.name && sameGeneratedColumns(candidate.columns, expected.source_columns) && sameGeneratedColumns(candidate.referenced_columns, expected.target_columns));
|
|
17871
|
+
if (!foreignKey) return void 0;
|
|
17872
|
+
const uniqueness = generatedRelationshipTargetUniqueness(target, expected.target_columns);
|
|
17873
|
+
if (!uniqueness || uniqueness.kind !== expected.target_uniqueness.kind || uniqueness.name !== expected.target_uniqueness.name || !sameGeneratedColumns(uniqueness.columns, expected.target_uniqueness.columns)) {
|
|
17874
|
+
return void 0;
|
|
17875
|
+
}
|
|
17876
|
+
links.push({
|
|
17877
|
+
constraint_name: foreignKey.name,
|
|
17878
|
+
source_resource: expected.source_resource,
|
|
17879
|
+
target_resource: expected.target_resource,
|
|
17880
|
+
source_columns: [...foreignKey.columns],
|
|
17881
|
+
target_columns: [...foreignKey.referenced_columns],
|
|
17882
|
+
target_uniqueness: uniqueness,
|
|
17883
|
+
nullable: foreignKey.columns.some((name) => source.columns.find((column) => column.name === name)?.nullable !== false),
|
|
17884
|
+
cardinality: "many_to_one",
|
|
17885
|
+
max_fan_out: 1
|
|
17886
|
+
});
|
|
17887
|
+
}
|
|
17888
|
+
return canonicalJsonDigest(links);
|
|
17889
|
+
}
|
|
17890
|
+
function generatedRelationshipTargetUniqueness(target, columns) {
|
|
17891
|
+
const same = (candidate) => sameGeneratedColumns(candidate, columns);
|
|
17892
|
+
if (same(target.primary_key)) {
|
|
17893
|
+
const named = target.unique_constraints.find((constraint2) => same(constraint2.columns)) ?? target.indexes.find((index2) => index2.unique === true && same(index2.columns ?? []));
|
|
17894
|
+
return {
|
|
17895
|
+
kind: "primary_key",
|
|
17896
|
+
name: named?.name ?? `${target.schema}.${target.name}.primary_key`,
|
|
17897
|
+
columns: [...columns]
|
|
17898
|
+
};
|
|
17899
|
+
}
|
|
17900
|
+
const constraint = target.unique_constraints.find((candidate) => same(candidate.columns));
|
|
17901
|
+
if (constraint) return { kind: "unique_constraint", name: constraint.name, columns: [...columns] };
|
|
17902
|
+
const index = target.indexes.find((candidate) => candidate.unique === true && same(candidate.columns ?? []));
|
|
17903
|
+
return index ? { kind: "unique_index", name: index.name, columns: [...columns] } : void 0;
|
|
17904
|
+
}
|
|
17905
|
+
function sameGeneratedColumns(left, right) {
|
|
17906
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
17907
|
+
}
|
|
17507
17908
|
function createDefaultRuntimeStore(config, env, storePath) {
|
|
17508
17909
|
const sharedPostgres = config.storage?.shared_postgres;
|
|
17509
17910
|
if (sharedPostgres?.mode === "runtime_store") {
|
|
@@ -20965,14 +21366,14 @@ var RuntimeDatabasePools = class {
|
|
|
20965
21366
|
if (timeoutMs) await client.query(`SET LOCAL statement_timeout = ${timeoutMs}`);
|
|
20966
21367
|
if (input.source.database_scope?.mode === "postgres_rls") {
|
|
20967
21368
|
for (const target of protectedReadTargets(input.capability)) {
|
|
20968
|
-
const preflightKey = `${poolKey}\0${target.schema}\0${target.table}\0SELECT`;
|
|
21369
|
+
const preflightKey = `${poolKey}\0${target.schema}\0${target.table}\0SELECT\0${target.principalScoped ? "tenant+principal" : "tenant"}`;
|
|
20969
21370
|
if (!this.postgresRlsPreflight.has(preflightKey)) {
|
|
20970
21371
|
await assertPostgresRlsTarget(client, {
|
|
20971
21372
|
schema: target.schema,
|
|
20972
21373
|
table: target.table,
|
|
20973
21374
|
scope: {
|
|
20974
21375
|
tenantSetting: input.source.database_scope.tenant_setting,
|
|
20975
|
-
principalSetting: input.source.database_scope.principal_setting
|
|
21376
|
+
...target.principalScoped && input.source.database_scope.principal_setting ? { principalSetting: input.source.database_scope.principal_setting } : {}
|
|
20976
21377
|
},
|
|
20977
21378
|
operations: ["SELECT"]
|
|
20978
21379
|
});
|
|
@@ -21153,27 +21554,51 @@ function runtimeReadQuery(capability, placeholderStyle, args, context) {
|
|
|
21153
21554
|
function buildProtectedReadQuery(capability, placeholderStyle, args, context) {
|
|
21154
21555
|
const protectedRead = capability.protected_read;
|
|
21155
21556
|
if (!protectedRead) throw new McpRuntimeError("PROTECTED_READ_REQUIRED", "Protected read authority is missing.");
|
|
21156
|
-
const
|
|
21557
|
+
const relationships = protectedRelationshipPaths(protectedRead);
|
|
21157
21558
|
const values = [];
|
|
21158
21559
|
const bind = (value) => {
|
|
21159
21560
|
values.push(value);
|
|
21160
21561
|
return placeholderStyle === "$" ? `$${values.length}` : "?";
|
|
21161
21562
|
};
|
|
21563
|
+
const relationshipAliases = /* @__PURE__ */ new Map();
|
|
21564
|
+
for (const [pathIndex, relationship] of relationships.entries()) {
|
|
21565
|
+
relationshipAliases.set(
|
|
21566
|
+
relationship.name,
|
|
21567
|
+
protectedRelationshipAlias(pathIndex, relationship.links.length - 1)
|
|
21568
|
+
);
|
|
21569
|
+
}
|
|
21162
21570
|
const field = (name, relationshipName) => {
|
|
21163
|
-
|
|
21571
|
+
const alias = relationshipName === void 0 ? "t0" : relationshipAliases.get(relationshipName);
|
|
21572
|
+
if (!alias) {
|
|
21164
21573
|
throw new McpRuntimeError("PROTECTED_RELATIONSHIP_INVALID", "Protected field references an unreviewed relationship.");
|
|
21165
21574
|
}
|
|
21166
|
-
return `${
|
|
21575
|
+
return `${alias}.${quoteIdentifier(name, placeholderStyle)}`;
|
|
21576
|
+
};
|
|
21577
|
+
const fromClause = () => {
|
|
21578
|
+
const joins = [];
|
|
21579
|
+
for (const [pathIndex, relationship] of relationships.entries()) {
|
|
21580
|
+
let sourceAlias = "t0";
|
|
21581
|
+
let preserveUnmatched = false;
|
|
21582
|
+
for (const [linkIndex, link] of relationship.links.entries()) {
|
|
21583
|
+
const alias = protectedRelationshipAlias(pathIndex, linkIndex);
|
|
21584
|
+
preserveUnmatched ||= link.unmatched_rows === "keep_null";
|
|
21585
|
+
const conditions = [
|
|
21586
|
+
`${sourceAlias}.${quoteIdentifier(link.local_key, placeholderStyle)} = ${alias}.${quoteIdentifier(link.target_key, placeholderStyle)}`,
|
|
21587
|
+
`${alias}.${quoteIdentifier(link.tenant_key, placeholderStyle)} = ${bind(context.tenant_id)}`,
|
|
21588
|
+
...link.principal_scope_key ? [`${alias}.${quoteIdentifier(link.principal_scope_key, placeholderStyle)} = ${bind(context.principal)}`] : []
|
|
21589
|
+
];
|
|
21590
|
+
joins.push(
|
|
21591
|
+
` ${preserveUnmatched ? "LEFT JOIN" : "JOIN"} ${quoteIdentifier(link.schema, placeholderStyle)}.${quoteIdentifier(link.table, placeholderStyle)} ${alias} ON ${conditions.join(" AND ")}`
|
|
21592
|
+
);
|
|
21593
|
+
sourceAlias = alias;
|
|
21594
|
+
}
|
|
21595
|
+
}
|
|
21596
|
+
return `${quoteIdentifier(capability.target.schema, placeholderStyle)}.${quoteIdentifier(capability.target.table, placeholderStyle)} t0${joins.join("")}`;
|
|
21167
21597
|
};
|
|
21168
|
-
const join = relationship ? ` JOIN ${quoteIdentifier(relationship.schema, placeholderStyle)}.${quoteIdentifier(relationship.table, placeholderStyle)} t1 ON t0.${quoteIdentifier(relationship.local_key, placeholderStyle)} = t1.${quoteIdentifier(relationship.target_key, placeholderStyle)}` : "";
|
|
21169
21598
|
const scopedWhere = () => {
|
|
21170
21599
|
const where = [];
|
|
21171
21600
|
if (capability.target.tenant_key) where.push(`t0.${quoteIdentifier(capability.target.tenant_key, placeholderStyle)} = ${bind(context.tenant_id)}`);
|
|
21172
21601
|
if (capability.target.principal_scope_key) where.push(`t0.${quoteIdentifier(capability.target.principal_scope_key, placeholderStyle)} = ${bind(context.principal)}`);
|
|
21173
|
-
if (relationship) {
|
|
21174
|
-
where.push(`t1.${quoteIdentifier(relationship.tenant_key, placeholderStyle)} = ${bind(context.tenant_id)}`);
|
|
21175
|
-
if (relationship.principal_scope_key) where.push(`t1.${quoteIdentifier(relationship.principal_scope_key, placeholderStyle)} = ${bind(context.principal)}`);
|
|
21176
|
-
}
|
|
21177
21602
|
for (const predicate of protectedRead.predicates ?? []) {
|
|
21178
21603
|
const reference = field(predicate.field, predicate.relationship);
|
|
21179
21604
|
if (predicate.operator === "in") {
|
|
@@ -21200,8 +21625,8 @@ function buildProtectedReadQuery(capability, placeholderStyle, args, context) {
|
|
|
21200
21625
|
}
|
|
21201
21626
|
return where;
|
|
21202
21627
|
};
|
|
21203
|
-
const from = `${quoteIdentifier(capability.target.schema, placeholderStyle)}.${quoteIdentifier(capability.target.table, placeholderStyle)} t0${join}`;
|
|
21204
21628
|
if (protectedRead.mode === "rows") {
|
|
21629
|
+
const from = fromClause();
|
|
21205
21630
|
const columns = capability.visible_columns.map((column) => `t0.${quoteIdentifier(column, placeholderStyle)} AS ${quoteIdentifier(column, placeholderStyle)}`);
|
|
21206
21631
|
const where = scopedWhere();
|
|
21207
21632
|
const order = protectedRead.row_order_by?.length ? ` ORDER BY ${protectedRead.row_order_by.map((item) => `${field(item.field)} ${item.direction.toUpperCase()}`).join(", ")}` : ` ORDER BY t0.${quoteIdentifier(capability.target.primary_key, placeholderStyle)} ASC`;
|
|
@@ -21213,6 +21638,7 @@ function buildProtectedReadQuery(capability, placeholderStyle, args, context) {
|
|
|
21213
21638
|
const aggregate = protectedRead.aggregate;
|
|
21214
21639
|
if (!aggregate) throw new McpRuntimeError("PROTECTED_AGGREGATE_REQUIRED", "Protected aggregate authority is missing.");
|
|
21215
21640
|
const aggregateQuery = (range, period) => {
|
|
21641
|
+
const from = fromClause();
|
|
21216
21642
|
const where = scopedWhere();
|
|
21217
21643
|
if (range && aggregate.comparison) {
|
|
21218
21644
|
const reference = field(aggregate.comparison.field, aggregate.comparison.relationship);
|
|
@@ -21273,10 +21699,61 @@ function protectedStatementTimeout(capability, sourceTimeout) {
|
|
|
21273
21699
|
return sourceTimeout === void 0 ? protectedTimeout : Math.min(protectedTimeout, sourceTimeout);
|
|
21274
21700
|
}
|
|
21275
21701
|
function protectedReadTargets(capability) {
|
|
21276
|
-
const targets =
|
|
21702
|
+
const targets = /* @__PURE__ */ new Map();
|
|
21703
|
+
const addTarget = (target) => {
|
|
21704
|
+
const key = `${target.schema}\0${target.table}`;
|
|
21705
|
+
const existing = targets.get(key);
|
|
21706
|
+
targets.set(key, {
|
|
21707
|
+
...target,
|
|
21708
|
+
principalScoped: target.principalScoped || Boolean(existing?.principalScoped)
|
|
21709
|
+
});
|
|
21710
|
+
};
|
|
21711
|
+
addTarget({
|
|
21712
|
+
schema: capability.target.schema,
|
|
21713
|
+
table: capability.target.table,
|
|
21714
|
+
principalScoped: Boolean(capability.target.principal_scope_key)
|
|
21715
|
+
});
|
|
21277
21716
|
const relationship = capability.protected_read?.relationship;
|
|
21278
|
-
if (relationship)
|
|
21279
|
-
|
|
21717
|
+
if (relationship) {
|
|
21718
|
+
addTarget({
|
|
21719
|
+
schema: relationship.schema,
|
|
21720
|
+
table: relationship.table,
|
|
21721
|
+
principalScoped: Boolean(relationship.principal_scope_key)
|
|
21722
|
+
});
|
|
21723
|
+
}
|
|
21724
|
+
for (const path3 of capability.protected_read?.relationships ?? []) {
|
|
21725
|
+
for (const link of path3.links) {
|
|
21726
|
+
addTarget({
|
|
21727
|
+
schema: link.schema,
|
|
21728
|
+
table: link.table,
|
|
21729
|
+
principalScoped: Boolean(link.principal_scope_key)
|
|
21730
|
+
});
|
|
21731
|
+
}
|
|
21732
|
+
}
|
|
21733
|
+
return [...targets.values()];
|
|
21734
|
+
}
|
|
21735
|
+
function protectedRelationshipPaths(protectedRead) {
|
|
21736
|
+
if (protectedRead.relationships?.length) return protectedRead.relationships;
|
|
21737
|
+
const relationship = protectedRead.relationship;
|
|
21738
|
+
if (!relationship) return [];
|
|
21739
|
+
return [{
|
|
21740
|
+
name: relationship.name,
|
|
21741
|
+
links: [{
|
|
21742
|
+
schema: relationship.schema,
|
|
21743
|
+
table: relationship.table,
|
|
21744
|
+
primary_key: relationship.primary_key,
|
|
21745
|
+
tenant_key: relationship.tenant_key,
|
|
21746
|
+
...relationship.principal_scope_key ? { principal_scope_key: relationship.principal_scope_key } : {},
|
|
21747
|
+
local_key: relationship.local_key,
|
|
21748
|
+
target_key: relationship.target_key,
|
|
21749
|
+
cardinality: "many_to_one",
|
|
21750
|
+
max_fan_out: 1,
|
|
21751
|
+
unmatched_rows: "exclude"
|
|
21752
|
+
}]
|
|
21753
|
+
}];
|
|
21754
|
+
}
|
|
21755
|
+
function protectedRelationshipAlias(pathIndex, linkIndex) {
|
|
21756
|
+
return `r${pathIndex + 1}_${linkIndex + 1}`;
|
|
21280
21757
|
}
|
|
21281
21758
|
function queryValues(capability, args, context) {
|
|
21282
21759
|
if (capability.kind === "aggregate_read") return [
|