@synapsor/spec 0.1.0
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/LICENSE +201 -0
- package/README.md +125 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +67 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +8 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/load.d.ts +3 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +6 -0
- package/dist/normalize.d.ts +3 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +16 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +522 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/examples/external-action.contract.json +114 -0
- package/examples/guarded-writeback.contract.json +171 -0
- package/examples/read-only-analytics.contract.json +94 -0
- package/examples/support-refund.contract.json +168 -0
- package/fixtures/conformance/kept-out-fields/contract.json +47 -0
- package/fixtures/conformance/kept-out-fields/expected.redaction.json +11 -0
- package/fixtures/conformance/kept-out-fields/scenario.json +9 -0
- package/fixtures/conformance/manual-approval/contract.json +66 -0
- package/fixtures/conformance/manual-approval/expected.proposal.json +9 -0
- package/fixtures/conformance/manual-approval/scenario.json +5 -0
- package/fixtures/conformance/proposal-capability/contract.json +101 -0
- package/fixtures/conformance/proposal-capability/expected.proposal.json +25 -0
- package/fixtures/conformance/proposal-capability/expected.replay.json +14 -0
- package/fixtures/conformance/proposal-capability/scenario.json +21 -0
- package/fixtures/conformance/read-capability/contract.json +73 -0
- package/fixtures/conformance/read-capability/expected.evidence.json +11 -0
- package/fixtures/conformance/read-capability/expected.replay.json +13 -0
- package/fixtures/conformance/read-capability/scenario.json +19 -0
- package/fixtures/invalid/kept-out-visible.contract.json +46 -0
- package/fixtures/invalid/model-controlled-tenant.contract.json +45 -0
- package/fixtures/valid/basic-read.contract.json +60 -0
- package/package.json +37 -0
- package/schemas/capability.schema.json +6 -0
- package/schemas/context.schema.json +6 -0
- package/schemas/envelope-v2.schema.json +20 -0
- package/schemas/evidence.schema.json +6 -0
- package/schemas/external-action.schema.json +6 -0
- package/schemas/policy.schema.json +6 -0
- package/schemas/proposal.schema.json +6 -0
- package/schemas/receipt.schema.json +6 -0
- package/schemas/replay.schema.json +6 -0
- package/schemas/synapsor-contract.schema.json +371 -0
- package/schemas/workflow.schema.json +6 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SynapsorContract, ValidationResult } from "./types.js";
|
|
2
|
+
export declare function validateContract(input: unknown): ValidationResult;
|
|
3
|
+
export declare function assertValidContract(input: unknown): asserts input is SynapsorContract;
|
|
4
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA2DtF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAiCjE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,gBAAgB,CAGrF"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { SPEC_VERSION } from "./version.js";
|
|
2
|
+
import { SynapsorSpecValidationError } from "./errors.js";
|
|
3
|
+
const TOP_LEVEL_KEYS = new Set([
|
|
4
|
+
"spec_version",
|
|
5
|
+
"kind",
|
|
6
|
+
"metadata",
|
|
7
|
+
"resources",
|
|
8
|
+
"contexts",
|
|
9
|
+
"capabilities",
|
|
10
|
+
"workflows",
|
|
11
|
+
"policies",
|
|
12
|
+
"evidence",
|
|
13
|
+
"proposals",
|
|
14
|
+
"receipts",
|
|
15
|
+
"replay",
|
|
16
|
+
"external_actions",
|
|
17
|
+
]);
|
|
18
|
+
const METADATA_KEYS = new Set(["name", "description", "version", "tags"]);
|
|
19
|
+
const RESOURCE_KEYS = new Set(["name", "engine", "schema", "table", "type", "primary_key", "tenant_key", "conflict_key", "single_tenant_dev"]);
|
|
20
|
+
const CONTEXT_KEYS = new Set(["name", "description", "bindings", "tenant_binding", "principal_binding"]);
|
|
21
|
+
const BINDING_KEYS = new Set(["name", "source", "key", "required"]);
|
|
22
|
+
const CAPABILITY_KEYS = new Set(["name", "description", "kind", "context", "source", "subject", "args", "lookup", "visible_fields", "kept_out_fields", "evidence", "max_rows", "proposal"]);
|
|
23
|
+
const SUBJECT_KEYS = new Set(["resource", "schema", "table", "primary_key", "tenant_key", "conflict_key", "single_tenant_dev"]);
|
|
24
|
+
const ARG_KEYS = new Set(["type", "description", "required", "max_length", "minimum", "maximum", "enum"]);
|
|
25
|
+
const LOOKUP_KEYS = new Set(["id_from_arg"]);
|
|
26
|
+
const EVIDENCE_KEYS = new Set(["required", "sources", "query_audit", "handle_prefix"]);
|
|
27
|
+
const PROPOSAL_KEYS = new Set(["action", "allowed_fields", "patch", "conflict_guard", "approval", "writeback"]);
|
|
28
|
+
const PATCH_KEYS = new Set(["fixed", "from_arg"]);
|
|
29
|
+
const CONFLICT_GUARD_KEYS = new Set(["column", "weak_guard_ack"]);
|
|
30
|
+
const APPROVAL_KEYS = new Set(["mode", "required_role"]);
|
|
31
|
+
const WRITEBACK_KEYS = new Set(["mode", "executor", "idempotency_key"]);
|
|
32
|
+
const WORKFLOW_KEYS = new Set(["name", "description", "context", "allowed_capabilities", "required_evidence", "approval", "settlement", "replay"]);
|
|
33
|
+
const POLICY_KEYS = new Set(["name", "kind", "mode", "rules"]);
|
|
34
|
+
const EVIDENCE_RECORD_KEYS = new Set(["handle", "capability", "query_fingerprint", "items"]);
|
|
35
|
+
const PROPOSAL_RECORD_KEYS = new Set(["id", "capability", "subject", "status", "diff", "evidence_handle"]);
|
|
36
|
+
const RECEIPT_KEYS = new Set(["id", "proposal_id", "status", "idempotency_key", "source_database_mutated", "rows_affected"]);
|
|
37
|
+
const REPLAY_KEYS = new Set(["id", "proposal_id", "run_id", "events"]);
|
|
38
|
+
const EXTERNAL_ACTION_KEYS = new Set(["id", "action", "handler", "idempotency_key", "receipt"]);
|
|
39
|
+
const TRUSTED_ARG_NAMES = new Set([
|
|
40
|
+
"tenant_id",
|
|
41
|
+
"tenantId",
|
|
42
|
+
"principal",
|
|
43
|
+
"principal_id",
|
|
44
|
+
"principalId",
|
|
45
|
+
"schema",
|
|
46
|
+
"table",
|
|
47
|
+
"column",
|
|
48
|
+
"columns",
|
|
49
|
+
"database_url",
|
|
50
|
+
"write_url",
|
|
51
|
+
"read_url",
|
|
52
|
+
"expected_version",
|
|
53
|
+
"row_version",
|
|
54
|
+
]);
|
|
55
|
+
export function validateContract(input) {
|
|
56
|
+
const errors = [];
|
|
57
|
+
const warnings = [];
|
|
58
|
+
if (!isRecord(input)) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
errors: [{ path: "$", code: "CONTRACT_NOT_OBJECT", message: "Contract must be a JSON object." }],
|
|
62
|
+
warnings,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
checkUnknownKeys(input, TOP_LEVEL_KEYS, "$", errors);
|
|
66
|
+
if (input.spec_version !== SPEC_VERSION) {
|
|
67
|
+
errors.push({ path: "$.spec_version", code: "UNSUPPORTED_SPEC_VERSION", message: `spec_version must be ${SPEC_VERSION}.` });
|
|
68
|
+
}
|
|
69
|
+
if (input.kind !== "SynapsorContract") {
|
|
70
|
+
errors.push({ path: "$.kind", code: "INVALID_KIND", message: "kind must be SynapsorContract." });
|
|
71
|
+
}
|
|
72
|
+
validateMetadata(input.metadata, "$.metadata", errors);
|
|
73
|
+
const resourceNames = validateResources(input.resources, errors, warnings);
|
|
74
|
+
const contextNames = validateContexts(input.contexts, errors, warnings);
|
|
75
|
+
const capabilityNames = validateCapabilities(input.capabilities, contextNames, resourceNames, errors, warnings);
|
|
76
|
+
validateWorkflows(input.workflows, contextNames, capabilityNames, errors);
|
|
77
|
+
validatePolicies(input.policies, errors);
|
|
78
|
+
validateEvidenceRecords(input.evidence, errors);
|
|
79
|
+
validateProposalRecords(input.proposals, capabilityNames, errors);
|
|
80
|
+
validateReceiptRecords(input.receipts, errors);
|
|
81
|
+
validateReplay(input.replay, errors);
|
|
82
|
+
validateExternalActions(input.external_actions, errors);
|
|
83
|
+
scanForInlineSecrets(input, "$", errors);
|
|
84
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
85
|
+
}
|
|
86
|
+
export function assertValidContract(input) {
|
|
87
|
+
const result = validateContract(input);
|
|
88
|
+
if (!result.ok)
|
|
89
|
+
throw new SynapsorSpecValidationError(result.errors);
|
|
90
|
+
}
|
|
91
|
+
function validateMetadata(value, path, errors) {
|
|
92
|
+
if (value === undefined)
|
|
93
|
+
return;
|
|
94
|
+
if (!isRecord(value)) {
|
|
95
|
+
errors.push({ path, code: "METADATA_NOT_OBJECT", message: "metadata must be an object." });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
checkUnknownKeys(value, METADATA_KEYS, path, errors);
|
|
99
|
+
if (value.name !== undefined && !isNonEmptyString(value.name))
|
|
100
|
+
errors.push({ path: `${path}.name`, code: "INVALID_METADATA_NAME", message: "metadata.name must be a non-empty string." });
|
|
101
|
+
if (value.tags !== undefined && (!Array.isArray(value.tags) || value.tags.some((tag) => !isNonEmptyString(tag)))) {
|
|
102
|
+
errors.push({ path: `${path}.tags`, code: "INVALID_METADATA_TAGS", message: "metadata.tags must be an array of non-empty strings." });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function validateResources(value, errors, warnings) {
|
|
106
|
+
const names = new Set();
|
|
107
|
+
if (value === undefined)
|
|
108
|
+
return names;
|
|
109
|
+
if (!Array.isArray(value)) {
|
|
110
|
+
errors.push({ path: "$.resources", code: "RESOURCES_NOT_ARRAY", message: "resources must be an array." });
|
|
111
|
+
return names;
|
|
112
|
+
}
|
|
113
|
+
value.forEach((resource, index) => {
|
|
114
|
+
const path = `$.resources[${index}]`;
|
|
115
|
+
if (!isRecord(resource)) {
|
|
116
|
+
errors.push({ path, code: "RESOURCE_NOT_OBJECT", message: "resource must be an object." });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
checkUnknownKeys(resource, RESOURCE_KEYS, path, errors);
|
|
120
|
+
if (!isQualifiedOrSafeName(resource.name))
|
|
121
|
+
errors.push({ path: `${path}.name`, code: "INVALID_RESOURCE_NAME", message: "resource name must be a safe identifier or qualified name." });
|
|
122
|
+
else
|
|
123
|
+
addUnique(names, resource.name, `${path}.name`, "DUPLICATE_RESOURCE_NAME", errors);
|
|
124
|
+
if (!["postgres", "mysql", "synapsor"].includes(String(resource.engine)))
|
|
125
|
+
errors.push({ path: `${path}.engine`, code: "INVALID_RESOURCE_ENGINE", message: "engine must be postgres, mysql, or synapsor." });
|
|
126
|
+
for (const key of ["schema", "table", "primary_key"]) {
|
|
127
|
+
if (!isSafeIdentifier(resource[key]))
|
|
128
|
+
errors.push({ path: `${path}.${key}`, code: "INVALID_RESOURCE_IDENTIFIER", message: `${key} must be a fixed safe identifier.` });
|
|
129
|
+
}
|
|
130
|
+
if (resource.tenant_key !== undefined && !isSafeIdentifier(resource.tenant_key))
|
|
131
|
+
errors.push({ path: `${path}.tenant_key`, code: "INVALID_TENANT_KEY", message: "tenant_key must be a fixed safe identifier." });
|
|
132
|
+
if (!isSafeIdentifier(resource.tenant_key) && resource.single_tenant_dev !== true) {
|
|
133
|
+
errors.push({ path: `${path}.tenant_key`, code: "TENANT_KEY_REQUIRED", message: "tenant_key is required unless single_tenant_dev is explicitly true." });
|
|
134
|
+
}
|
|
135
|
+
if (resource.single_tenant_dev === true)
|
|
136
|
+
warnings.push({ path: `${path}.single_tenant_dev`, code: "SINGLE_TENANT_DEV", message: "single_tenant_dev is only for local demos and must not be used for shared tenant data." });
|
|
137
|
+
if (resource.conflict_key !== undefined && !isSafeIdentifier(resource.conflict_key))
|
|
138
|
+
errors.push({ path: `${path}.conflict_key`, code: "INVALID_CONFLICT_KEY", message: "conflict_key must be a fixed safe identifier." });
|
|
139
|
+
});
|
|
140
|
+
return names;
|
|
141
|
+
}
|
|
142
|
+
function validateContexts(value, errors, warnings) {
|
|
143
|
+
const names = new Set();
|
|
144
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
145
|
+
errors.push({ path: "$.contexts", code: "CONTEXTS_REQUIRED", message: "At least one context is required." });
|
|
146
|
+
return names;
|
|
147
|
+
}
|
|
148
|
+
value.forEach((context, index) => {
|
|
149
|
+
const path = `$.contexts[${index}]`;
|
|
150
|
+
if (!isRecord(context)) {
|
|
151
|
+
errors.push({ path, code: "CONTEXT_NOT_OBJECT", message: "context must be an object." });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
checkUnknownKeys(context, CONTEXT_KEYS, path, errors);
|
|
155
|
+
if (!isQualifiedOrSafeName(context.name))
|
|
156
|
+
errors.push({ path: `${path}.name`, code: "INVALID_CONTEXT_NAME", message: "context name must be a safe identifier or qualified name." });
|
|
157
|
+
else
|
|
158
|
+
addUnique(names, context.name, `${path}.name`, "DUPLICATE_CONTEXT_NAME", errors);
|
|
159
|
+
if (!Array.isArray(context.bindings) || context.bindings.length === 0) {
|
|
160
|
+
errors.push({ path: `${path}.bindings`, code: "BINDINGS_REQUIRED", message: "context.bindings must contain trusted/session bindings." });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const bindingNames = new Set();
|
|
164
|
+
context.bindings.forEach((binding, bindingIndex) => {
|
|
165
|
+
const bindingPath = `${path}.bindings[${bindingIndex}]`;
|
|
166
|
+
if (!isRecord(binding)) {
|
|
167
|
+
errors.push({ path: bindingPath, code: "BINDING_NOT_OBJECT", message: "binding must be an object." });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
checkUnknownKeys(binding, BINDING_KEYS, bindingPath, errors);
|
|
171
|
+
if (!isSafeIdentifier(binding.name))
|
|
172
|
+
errors.push({ path: `${bindingPath}.name`, code: "INVALID_BINDING_NAME", message: "binding name must be a safe identifier." });
|
|
173
|
+
else
|
|
174
|
+
addUnique(bindingNames, binding.name, `${bindingPath}.name`, "DUPLICATE_BINDING_NAME", errors);
|
|
175
|
+
if (!["session", "environment", "cloud_session", "static_dev", "http_claim"].includes(String(binding.source)))
|
|
176
|
+
errors.push({ path: `${bindingPath}.source`, code: "INVALID_BINDING_SOURCE", message: "binding source must be session, environment, cloud_session, static_dev, or http_claim." });
|
|
177
|
+
if (!isNonEmptyString(binding.key))
|
|
178
|
+
errors.push({ path: `${bindingPath}.key`, code: "INVALID_BINDING_KEY", message: "binding key must be a non-empty string." });
|
|
179
|
+
if (binding.source === "static_dev")
|
|
180
|
+
warnings.push({ path: `${bindingPath}.source`, code: "STATIC_DEV_BINDING", message: "static_dev bindings are for local demos only." });
|
|
181
|
+
});
|
|
182
|
+
for (const key of ["tenant_binding", "principal_binding"]) {
|
|
183
|
+
if (context[key] !== undefined && (!isSafeIdentifier(context[key]) || !bindingNames.has(String(context[key])))) {
|
|
184
|
+
errors.push({ path: `${path}.${key}`, code: "UNKNOWN_CONTEXT_BINDING", message: `${key} must reference a binding name.` });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
return names;
|
|
189
|
+
}
|
|
190
|
+
function validateCapabilities(value, contextNames, resourceNames, errors, warnings) {
|
|
191
|
+
const names = new Set();
|
|
192
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
193
|
+
errors.push({ path: "$.capabilities", code: "CAPABILITIES_REQUIRED", message: "At least one capability is required." });
|
|
194
|
+
return names;
|
|
195
|
+
}
|
|
196
|
+
value.forEach((capability, index) => {
|
|
197
|
+
const path = `$.capabilities[${index}]`;
|
|
198
|
+
if (!isRecord(capability)) {
|
|
199
|
+
errors.push({ path, code: "CAPABILITY_NOT_OBJECT", message: "capability must be an object." });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
checkUnknownKeys(capability, CAPABILITY_KEYS, path, errors);
|
|
203
|
+
if (!isQualifiedName(capability.name))
|
|
204
|
+
errors.push({ path: `${path}.name`, code: "INVALID_CAPABILITY_NAME", message: "capability name must be namespace.name." });
|
|
205
|
+
else
|
|
206
|
+
addUnique(names, capability.name, `${path}.name`, "DUPLICATE_CAPABILITY_NAME", errors);
|
|
207
|
+
if (!["read", "proposal", "external_action", "answer_with_evidence"].includes(String(capability.kind)))
|
|
208
|
+
errors.push({ path: `${path}.kind`, code: "INVALID_CAPABILITY_KIND", message: "kind must be read, proposal, external_action, or answer_with_evidence." });
|
|
209
|
+
if (!isNonEmptyString(capability.context) || !contextNames.has(capability.context))
|
|
210
|
+
errors.push({ path: `${path}.context`, code: "UNKNOWN_CONTEXT", message: "capability.context must reference a declared context." });
|
|
211
|
+
validateSubject(capability.subject, `${path}.subject`, resourceNames, errors, warnings);
|
|
212
|
+
validateArgs(capability.args, `${path}.args`, errors);
|
|
213
|
+
if (capability.lookup !== undefined)
|
|
214
|
+
validateLookup(capability.lookup, `${path}.lookup`, capability.args, errors);
|
|
215
|
+
validateFieldList(capability.visible_fields, `${path}.visible_fields`, "VISIBLE_FIELDS_REQUIRED", errors);
|
|
216
|
+
if (capability.kept_out_fields !== undefined)
|
|
217
|
+
validateFieldList(capability.kept_out_fields, `${path}.kept_out_fields`, "INVALID_KEPT_OUT_FIELDS", errors, true);
|
|
218
|
+
validateKeptOutExclusion(capability.visible_fields, capability.kept_out_fields, path, errors);
|
|
219
|
+
if (capability.evidence !== undefined)
|
|
220
|
+
validateEvidenceRequirement(capability.evidence, `${path}.evidence`, errors);
|
|
221
|
+
if (capability.max_rows !== undefined && !isPositiveInteger(capability.max_rows))
|
|
222
|
+
errors.push({ path: `${path}.max_rows`, code: "INVALID_MAX_ROWS", message: "max_rows must be a positive integer." });
|
|
223
|
+
if (capability.kind === "proposal")
|
|
224
|
+
validateProposalAction(capability.proposal, `${path}.proposal`, errors);
|
|
225
|
+
if (capability.kind !== "proposal" && capability.proposal !== undefined)
|
|
226
|
+
errors.push({ path: `${path}.proposal`, code: "PROPOSAL_ONLY_FOR_PROPOSAL_KIND", message: "proposal is only valid for proposal capabilities." });
|
|
227
|
+
});
|
|
228
|
+
return names;
|
|
229
|
+
}
|
|
230
|
+
function validateSubject(value, path, resourceNames, errors, warnings) {
|
|
231
|
+
if (!isRecord(value)) {
|
|
232
|
+
errors.push({ path, code: "SUBJECT_REQUIRED", message: "subject must be an object." });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
checkUnknownKeys(value, SUBJECT_KEYS, path, errors);
|
|
236
|
+
if (value.resource !== undefined && (!isQualifiedOrSafeName(value.resource) || (resourceNames.size > 0 && !resourceNames.has(String(value.resource))))) {
|
|
237
|
+
errors.push({ path: `${path}.resource`, code: "UNKNOWN_RESOURCE", message: "subject.resource must reference a declared resource." });
|
|
238
|
+
}
|
|
239
|
+
if (value.resource === undefined) {
|
|
240
|
+
for (const key of ["schema", "table", "primary_key"]) {
|
|
241
|
+
if (!isSafeIdentifier(value[key]))
|
|
242
|
+
errors.push({ path: `${path}.${key}`, code: "INVALID_SUBJECT_IDENTIFIER", message: `${key} must be provided as a fixed safe identifier when resource is omitted.` });
|
|
243
|
+
}
|
|
244
|
+
if (!isSafeIdentifier(value.tenant_key) && value.single_tenant_dev !== true) {
|
|
245
|
+
errors.push({ path: `${path}.tenant_key`, code: "TENANT_KEY_REQUIRED", message: "tenant_key is required unless single_tenant_dev is explicitly true." });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (value.single_tenant_dev === true)
|
|
249
|
+
warnings.push({ path: `${path}.single_tenant_dev`, code: "SINGLE_TENANT_DEV", message: "single_tenant_dev is only for local demos and must not be used for shared tenant data." });
|
|
250
|
+
}
|
|
251
|
+
function validateArgs(value, path, errors) {
|
|
252
|
+
if (!isRecord(value) || Object.keys(value).length === 0) {
|
|
253
|
+
errors.push({ path, code: "ARGS_REQUIRED", message: "args must define at least one model-facing business argument." });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
for (const [name, arg] of Object.entries(value)) {
|
|
257
|
+
const argPath = `${path}.${name}`;
|
|
258
|
+
if (!isSafeIdentifier(name))
|
|
259
|
+
errors.push({ path: argPath, code: "INVALID_ARG_NAME", message: "arg names must be safe identifiers." });
|
|
260
|
+
if (TRUSTED_ARG_NAMES.has(name))
|
|
261
|
+
errors.push({ path: argPath, code: "MODEL_CONTROLLED_TRUST_ARG", message: "model-facing args cannot include trust scope, dynamic identifiers, or version authority." });
|
|
262
|
+
if (!isRecord(arg)) {
|
|
263
|
+
errors.push({ path: argPath, code: "ARG_NOT_OBJECT", message: "arg definition must be an object." });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
checkUnknownKeys(arg, ARG_KEYS, argPath, errors);
|
|
267
|
+
if (!["string", "number", "boolean"].includes(String(arg.type)))
|
|
268
|
+
errors.push({ path: `${argPath}.type`, code: "INVALID_ARG_TYPE", message: "arg type must be string, number, or boolean." });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function validateLookup(value, path, args, errors) {
|
|
272
|
+
if (!isRecord(value)) {
|
|
273
|
+
errors.push({ path, code: "LOOKUP_NOT_OBJECT", message: "lookup must be an object." });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
checkUnknownKeys(value, LOOKUP_KEYS, path, errors);
|
|
277
|
+
if (!isSafeIdentifier(value.id_from_arg) || !isRecord(args) || !Object.prototype.hasOwnProperty.call(args, String(value.id_from_arg))) {
|
|
278
|
+
errors.push({ path: `${path}.id_from_arg`, code: "UNKNOWN_LOOKUP_ARG", message: "lookup.id_from_arg must reference a model-facing arg." });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function validateEvidenceRequirement(value, path, errors) {
|
|
282
|
+
if (!isRecord(value)) {
|
|
283
|
+
errors.push({ path, code: "EVIDENCE_NOT_OBJECT", message: "evidence must be an object." });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
checkUnknownKeys(value, EVIDENCE_KEYS, path, errors);
|
|
287
|
+
if (typeof value.required !== "boolean")
|
|
288
|
+
errors.push({ path: `${path}.required`, code: "INVALID_EVIDENCE_REQUIRED", message: "evidence.required must be boolean." });
|
|
289
|
+
if (value.sources !== undefined && (!Array.isArray(value.sources) || value.sources.some((source) => !isNonEmptyString(source))))
|
|
290
|
+
errors.push({ path: `${path}.sources`, code: "INVALID_EVIDENCE_SOURCES", message: "evidence.sources must be non-empty strings." });
|
|
291
|
+
}
|
|
292
|
+
function validateProposalAction(value, path, errors) {
|
|
293
|
+
if (!isRecord(value)) {
|
|
294
|
+
errors.push({ path, code: "PROPOSAL_REQUIRED", message: "proposal capabilities must define proposal action semantics." });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
checkUnknownKeys(value, PROPOSAL_KEYS, path, errors);
|
|
298
|
+
if (!isQualifiedOrSafeName(value.action))
|
|
299
|
+
errors.push({ path: `${path}.action`, code: "INVALID_PROPOSAL_ACTION", message: "proposal.action must be a safe action name." });
|
|
300
|
+
validateFieldList(value.allowed_fields, `${path}.allowed_fields`, "ALLOWED_FIELDS_REQUIRED", errors);
|
|
301
|
+
if (!isRecord(value.patch) || Object.keys(value.patch).length === 0) {
|
|
302
|
+
errors.push({ path: `${path}.patch`, code: "PATCH_REQUIRED", message: "proposal.patch must map allowed fields to fixed or arg values." });
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
for (const [field, patch] of Object.entries(value.patch)) {
|
|
306
|
+
const patchPath = `${path}.patch.${field}`;
|
|
307
|
+
if (!isSafeIdentifier(field))
|
|
308
|
+
errors.push({ path: patchPath, code: "INVALID_PATCH_FIELD", message: "patch field must be a safe identifier." });
|
|
309
|
+
if (Array.isArray(value.allowed_fields) && !value.allowed_fields.includes(field))
|
|
310
|
+
errors.push({ path: patchPath, code: "PATCH_FIELD_NOT_ALLOWED", message: "patch field must be listed in allowed_fields." });
|
|
311
|
+
if (!isRecord(patch)) {
|
|
312
|
+
errors.push({ path: patchPath, code: "PATCH_BINDING_NOT_OBJECT", message: "patch binding must be an object." });
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
checkUnknownKeys(patch, PATCH_KEYS, patchPath, errors);
|
|
316
|
+
if (patch.fixed === undefined && !isSafeIdentifier(patch.from_arg))
|
|
317
|
+
errors.push({ path: patchPath, code: "PATCH_BINDING_REQUIRED", message: "patch binding must include fixed or from_arg." });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (value.conflict_guard !== undefined) {
|
|
321
|
+
if (!isRecord(value.conflict_guard))
|
|
322
|
+
errors.push({ path: `${path}.conflict_guard`, code: "CONFLICT_GUARD_NOT_OBJECT", message: "conflict_guard must be an object." });
|
|
323
|
+
else {
|
|
324
|
+
checkUnknownKeys(value.conflict_guard, CONFLICT_GUARD_KEYS, `${path}.conflict_guard`, errors);
|
|
325
|
+
if (value.conflict_guard.column !== undefined && !isSafeIdentifier(value.conflict_guard.column))
|
|
326
|
+
errors.push({ path: `${path}.conflict_guard.column`, code: "INVALID_CONFLICT_COLUMN", message: "conflict_guard.column must be a safe identifier." });
|
|
327
|
+
if (value.conflict_guard.column === undefined && value.conflict_guard.weak_guard_ack !== true)
|
|
328
|
+
errors.push({ path: `${path}.conflict_guard`, code: "CONFLICT_GUARD_REQUIRED", message: "proposal needs conflict_guard.column unless weak_guard_ack is explicit." });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (value.approval !== undefined)
|
|
332
|
+
validateNestedObject(value.approval, APPROVAL_KEYS, `${path}.approval`, errors);
|
|
333
|
+
if (value.writeback !== undefined) {
|
|
334
|
+
validateNestedObject(value.writeback, WRITEBACK_KEYS, `${path}.writeback`, errors);
|
|
335
|
+
if (isRecord(value.writeback) && !["direct_sql", "app_handler", "cloud_worker", "none"].includes(String(value.writeback.mode)))
|
|
336
|
+
errors.push({ path: `${path}.writeback.mode`, code: "INVALID_WRITEBACK_MODE", message: "writeback.mode must be direct_sql, app_handler, cloud_worker, or none." });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function validateWorkflows(value, contextNames, capabilityNames, errors) {
|
|
340
|
+
if (value === undefined)
|
|
341
|
+
return;
|
|
342
|
+
if (!Array.isArray(value)) {
|
|
343
|
+
errors.push({ path: "$.workflows", code: "WORKFLOWS_NOT_ARRAY", message: "workflows must be an array." });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const names = new Set();
|
|
347
|
+
value.forEach((workflow, index) => {
|
|
348
|
+
const path = `$.workflows[${index}]`;
|
|
349
|
+
if (!isRecord(workflow)) {
|
|
350
|
+
errors.push({ path, code: "WORKFLOW_NOT_OBJECT", message: "workflow must be an object." });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
checkUnknownKeys(workflow, WORKFLOW_KEYS, path, errors);
|
|
354
|
+
if (!isQualifiedName(workflow.name))
|
|
355
|
+
errors.push({ path: `${path}.name`, code: "INVALID_WORKFLOW_NAME", message: "workflow name must be namespace.name." });
|
|
356
|
+
else
|
|
357
|
+
addUnique(names, workflow.name, `${path}.name`, "DUPLICATE_WORKFLOW_NAME", errors);
|
|
358
|
+
if (!isNonEmptyString(workflow.context) || !contextNames.has(workflow.context))
|
|
359
|
+
errors.push({ path: `${path}.context`, code: "UNKNOWN_CONTEXT", message: "workflow.context must reference a declared context." });
|
|
360
|
+
if (!Array.isArray(workflow.allowed_capabilities) || workflow.allowed_capabilities.length === 0) {
|
|
361
|
+
errors.push({ path: `${path}.allowed_capabilities`, code: "WORKFLOW_CAPABILITIES_REQUIRED", message: "workflow.allowed_capabilities must list allowed capabilities." });
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
workflow.allowed_capabilities.forEach((name, allowedIndex) => {
|
|
365
|
+
if (!isNonEmptyString(name) || !capabilityNames.has(name))
|
|
366
|
+
errors.push({ path: `${path}.allowed_capabilities[${allowedIndex}]`, code: "UNKNOWN_CAPABILITY", message: "workflow allowed capability must reference a declared capability." });
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function validatePolicies(value, errors) {
|
|
372
|
+
if (value === undefined)
|
|
373
|
+
return;
|
|
374
|
+
validateArrayRecords(value, "$.policies", POLICY_KEYS, "POLICY", errors);
|
|
375
|
+
}
|
|
376
|
+
function validateEvidenceRecords(value, errors) {
|
|
377
|
+
if (value === undefined)
|
|
378
|
+
return;
|
|
379
|
+
validateArrayRecords(value, "$.evidence", EVIDENCE_RECORD_KEYS, "EVIDENCE", errors);
|
|
380
|
+
}
|
|
381
|
+
function validateProposalRecords(value, capabilityNames, errors) {
|
|
382
|
+
if (value === undefined)
|
|
383
|
+
return;
|
|
384
|
+
if (!Array.isArray(value)) {
|
|
385
|
+
errors.push({ path: "$.proposals", code: "PROPOSALS_NOT_ARRAY", message: "proposals must be an array." });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
value.forEach((proposal, index) => {
|
|
389
|
+
const path = `$.proposals[${index}]`;
|
|
390
|
+
if (!isRecord(proposal)) {
|
|
391
|
+
errors.push({ path, code: "PROPOSAL_NOT_OBJECT", message: "proposal must be an object." });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
checkUnknownKeys(proposal, PROPOSAL_RECORD_KEYS, path, errors);
|
|
395
|
+
if (!isNonEmptyString(proposal.id))
|
|
396
|
+
errors.push({ path: `${path}.id`, code: "INVALID_PROPOSAL_ID", message: "proposal.id must be non-empty." });
|
|
397
|
+
if (!isNonEmptyString(proposal.capability) || (capabilityNames.size > 0 && !capabilityNames.has(String(proposal.capability))))
|
|
398
|
+
errors.push({ path: `${path}.capability`, code: "UNKNOWN_CAPABILITY", message: "proposal.capability must reference a declared capability." });
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function validateReceiptRecords(value, errors) {
|
|
402
|
+
if (value === undefined)
|
|
403
|
+
return;
|
|
404
|
+
validateArrayRecords(value, "$.receipts", RECEIPT_KEYS, "RECEIPT", errors);
|
|
405
|
+
}
|
|
406
|
+
function validateReplay(value, errors) {
|
|
407
|
+
if (value === undefined)
|
|
408
|
+
return;
|
|
409
|
+
if (!Array.isArray(value)) {
|
|
410
|
+
errors.push({ path: "$.replay", code: "REPLAY_NOT_ARRAY", message: "replay must be an array." });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
value.forEach((record, index) => {
|
|
414
|
+
const path = `$.replay[${index}]`;
|
|
415
|
+
if (!isRecord(record)) {
|
|
416
|
+
errors.push({ path, code: "REPLAY_RECORD_NOT_OBJECT", message: "replay record must be an object." });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
checkUnknownKeys(record, REPLAY_KEYS, path, errors);
|
|
420
|
+
if (!isNonEmptyString(record.id))
|
|
421
|
+
errors.push({ path: `${path}.id`, code: "INVALID_REPLAY_ID", message: "replay id must be non-empty." });
|
|
422
|
+
if (!Array.isArray(record.events))
|
|
423
|
+
errors.push({ path: `${path}.events`, code: "REPLAY_EVENTS_REQUIRED", message: "replay.events must be an array." });
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function validateExternalActions(value, errors) {
|
|
427
|
+
if (value === undefined)
|
|
428
|
+
return;
|
|
429
|
+
validateArrayRecords(value, "$.external_actions", EXTERNAL_ACTION_KEYS, "EXTERNAL_ACTION", errors);
|
|
430
|
+
}
|
|
431
|
+
function validateArrayRecords(value, path, keys, label, errors) {
|
|
432
|
+
if (!Array.isArray(value)) {
|
|
433
|
+
errors.push({ path, code: `${label}_NOT_ARRAY`, message: `${path} must be an array.` });
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
value.forEach((record, index) => {
|
|
437
|
+
const itemPath = `${path}[${index}]`;
|
|
438
|
+
if (!isRecord(record)) {
|
|
439
|
+
errors.push({ path: itemPath, code: `${label}_NOT_OBJECT`, message: `${label.toLowerCase()} entry must be an object.` });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
checkUnknownKeys(record, keys, itemPath, errors);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
function validateFieldList(value, path, code, errors, allowEmpty = false) {
|
|
446
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) {
|
|
447
|
+
errors.push({ path, code, message: "field list must be a non-empty array of safe identifiers." });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const seen = new Set();
|
|
451
|
+
value.forEach((field, index) => {
|
|
452
|
+
if (!isSafeIdentifier(field))
|
|
453
|
+
errors.push({ path: `${path}[${index}]`, code: "INVALID_FIELD_IDENTIFIER", message: "field names must be fixed safe identifiers." });
|
|
454
|
+
else
|
|
455
|
+
addUnique(seen, field, `${path}[${index}]`, "DUPLICATE_FIELD", errors);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function validateKeptOutExclusion(visible, keptOut, path, errors) {
|
|
459
|
+
if (!Array.isArray(visible) || !Array.isArray(keptOut))
|
|
460
|
+
return;
|
|
461
|
+
const visibleSet = new Set(visible);
|
|
462
|
+
for (const field of keptOut) {
|
|
463
|
+
if (visibleSet.has(field))
|
|
464
|
+
errors.push({ path: `${path}.kept_out_fields`, code: "KEPT_OUT_FIELD_VISIBLE", message: `kept-out field must not also be visible: ${String(field)}` });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function validateNestedObject(value, keys, path, errors) {
|
|
468
|
+
if (!isRecord(value)) {
|
|
469
|
+
errors.push({ path, code: "NESTED_VALUE_NOT_OBJECT", message: "nested value must be an object." });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
checkUnknownKeys(value, keys, path, errors);
|
|
473
|
+
}
|
|
474
|
+
function scanForInlineSecrets(value, path, errors) {
|
|
475
|
+
if (Array.isArray(value)) {
|
|
476
|
+
value.forEach((item, index) => scanForInlineSecrets(item, `${path}[${index}]`, errors));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (!isRecord(value))
|
|
480
|
+
return;
|
|
481
|
+
for (const [key, child] of Object.entries(value)) {
|
|
482
|
+
const childPath = `${path}.${key}`;
|
|
483
|
+
if (/database_url|read_url|write_url|connection_string|password|secret|token/i.test(key) && typeof child === "string" && /(postgres(?:ql)?:\/\/|mysql:\/\/|Bearer\s+|[A-Za-z0-9+/=]{24,})/i.test(child)) {
|
|
484
|
+
errors.push({ path: childPath, code: "INLINE_SECRET_OR_URL", message: "Contracts must not contain database URLs, passwords, bearer tokens, or secrets. Put runtime wiring in runner config/env vars." });
|
|
485
|
+
}
|
|
486
|
+
scanForInlineSecrets(child, childPath, errors);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function checkUnknownKeys(value, allowed, path, errors) {
|
|
490
|
+
for (const key of Object.keys(value)) {
|
|
491
|
+
if (allowed.has(key) || isExtensionKey(key))
|
|
492
|
+
continue;
|
|
493
|
+
errors.push({ path: `${path}.${key}`, code: "UNKNOWN_CORE_FIELD", message: `Unknown core field ${key}. Use x-cloud-*, x-runner-*, or x-experimental-* for extensions.` });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function addUnique(seen, name, path, code, errors) {
|
|
497
|
+
const value = String(name);
|
|
498
|
+
if (seen.has(value))
|
|
499
|
+
errors.push({ path, code, message: `Duplicate name: ${value}` });
|
|
500
|
+
seen.add(value);
|
|
501
|
+
}
|
|
502
|
+
function isExtensionKey(key) {
|
|
503
|
+
return /^x-(cloud|runner|experimental)-[A-Za-z0-9_.-]+$/.test(key);
|
|
504
|
+
}
|
|
505
|
+
function isRecord(value) {
|
|
506
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
507
|
+
}
|
|
508
|
+
function isNonEmptyString(value) {
|
|
509
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
510
|
+
}
|
|
511
|
+
function isSafeIdentifier(value) {
|
|
512
|
+
return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
513
|
+
}
|
|
514
|
+
function isQualifiedName(value) {
|
|
515
|
+
return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
516
|
+
}
|
|
517
|
+
function isQualifiedOrSafeName(value) {
|
|
518
|
+
return isSafeIdentifier(value) || isQualifiedName(value);
|
|
519
|
+
}
|
|
520
|
+
function isPositiveInteger(value) {
|
|
521
|
+
return Number.isInteger(value) && Number(value) > 0;
|
|
522
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,EAAG,KAAc,CAAC;AAC3C,eAAO,MAAM,uBAAuB,kBAA0B,CAAC"}
|
package/dist/version.js
ADDED