eval-quality 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 +190 -0
- package/README.md +355 -0
- package/corpus/dev/README.md +36 -0
- package/corpus/dev/compile-seal-example/brief.json +1 -0
- package/corpus/dev/compile-seal-example/contract.json +1 -0
- package/corpus/dev/contracts/absent-collection-locations.json +1 -0
- package/corpus/dev/contracts/absent-sibling-groups.json +1 -0
- package/corpus/dev/contracts/absent-success-indicator.json +1 -0
- package/corpus/dev/contracts/empty-channel-roles.json +1 -0
- package/corpus/dev/contracts/empty-collection-locations.json +1 -0
- package/corpus/dev/contracts/empty-request-shapes.json +1 -0
- package/corpus/dev/contracts/empty-sibling-groups.json +1 -0
- package/corpus/dev/contracts/no-collection-quantifier.json +1 -0
- package/corpus/dev/contracts/no-operation-inventory.json +1 -0
- package/corpus/dev/contracts/no-read-back-relation.json +1 -0
- package/corpus/dev/contracts/no-state-change-marker.json +1 -0
- package/corpus/dev/contracts/no-type-violating-step.json +1 -0
- package/corpus/dev/contracts/per-key-split-oracles.json +1 -0
- package/corpus/dev/contracts/satisfied-declarations.json +1 -0
- package/corpus/dev/contracts/single-required-response-key.json +1 -0
- package/corpus/dev/contracts/split-indicator-oracle.json +1 -0
- package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -0
- package/corpus/dev/contracts/unnamed-reference-set.json +1 -0
- package/corpus/dev/contracts/wrong-cardinality-form.json +1 -0
- package/corpus/dev/index.json +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/local-corpus-adapter.d.ts +7 -0
- package/dist/adapters/local-corpus-adapter.js +78 -0
- package/dist/adapters/node-file-system-adapter.d.ts +7 -0
- package/dist/adapters/node-file-system-adapter.js +39 -0
- package/dist/adapters/port-boundary.d.ts +18 -0
- package/dist/adapters/port-boundary.js +83 -0
- package/dist/adapters/system-clock-adapter.d.ts +10 -0
- package/dist/adapters/system-clock-adapter.js +18 -0
- package/dist/application/compile.d.ts +4 -0
- package/dist/application/compile.js +24 -0
- package/dist/application/diagnostics.d.ts +22 -0
- package/dist/application/diagnostics.js +18 -0
- package/dist/application/index.d.ts +23 -0
- package/dist/application/index.js +17 -0
- package/dist/application/invoke-port.d.ts +2 -0
- package/dist/application/invoke-port.js +45 -0
- package/dist/application/preflight.d.ts +27 -0
- package/dist/application/preflight.js +131 -0
- package/dist/application/seal.d.ts +4 -0
- package/dist/application/seal.js +28 -0
- package/dist/application/serialize.d.ts +6 -0
- package/dist/application/serialize.js +9 -0
- package/dist/cli/arguments.d.ts +28 -0
- package/dist/cli/arguments.js +167 -0
- package/dist/cli/exit-codes.d.ts +36 -0
- package/dist/cli/exit-codes.js +39 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +143 -0
- package/dist/cli/render.d.ts +26 -0
- package/dist/cli/render.js +53 -0
- package/dist/cli/run.d.ts +41 -0
- package/dist/cli/run.js +203 -0
- package/dist/core/canonical/canonicalize.d.ts +1 -0
- package/dist/core/canonical/canonicalize.js +113 -0
- package/dist/core/canonical/digest.d.ts +7 -0
- package/dist/core/canonical/digest.js +81 -0
- package/dist/core/canonical/scan-json.d.ts +1 -0
- package/dist/core/canonical/scan-json.js +298 -0
- package/dist/core/canonical/value-domain.d.ts +4 -0
- package/dist/core/canonical/value-domain.js +113 -0
- package/dist/core/compile/compile.d.ts +24 -0
- package/dist/core/compile/compile.js +67 -0
- package/dist/core/compile/declarations.d.ts +5 -0
- package/dist/core/compile/declarations.js +22 -0
- package/dist/core/compile/expression-legality.d.ts +14 -0
- package/dist/core/compile/expression-legality.js +346 -0
- package/dist/core/compile/forbidden-inputs.d.ts +5 -0
- package/dist/core/compile/forbidden-inputs.js +19 -0
- package/dist/core/compile/interface-inventory.d.ts +10 -0
- package/dist/core/compile/interface-inventory.js +55 -0
- package/dist/core/compile/oracle-alignment.d.ts +14 -0
- package/dist/core/compile/oracle-alignment.js +93 -0
- package/dist/core/compile/reachability.d.ts +17 -0
- package/dist/core/compile/reachability.js +177 -0
- package/dist/core/compile/rubrics.d.ts +55 -0
- package/dist/core/compile/rubrics.js +198 -0
- package/dist/core/compile/scripting-bound.d.ts +5 -0
- package/dist/core/compile/scripting-bound.js +151 -0
- package/dist/core/compile/sensitivity-witness.d.ts +73 -0
- package/dist/core/compile/sensitivity-witness.js +232 -0
- package/dist/core/compile/waivers.d.ts +3 -0
- package/dist/core/compile/waivers.js +21 -0
- package/dist/core/coverage/coverage.d.ts +18 -0
- package/dist/core/coverage/coverage.js +47 -0
- package/dist/core/coverage/relevance.d.ts +75 -0
- package/dist/core/coverage/relevance.js +186 -0
- package/dist/core/coverage/rules.d.ts +7 -0
- package/dist/core/coverage/rules.js +18 -0
- package/dist/core/coverage/satisfaction.d.ts +107 -0
- package/dist/core/coverage/satisfaction.js +415 -0
- package/dist/core/coverage/table.d.ts +26 -0
- package/dist/core/coverage/table.js +222 -0
- package/dist/core/declared-inputs.d.ts +16 -0
- package/dist/core/declared-inputs.js +18 -0
- package/dist/core/evaluate/evidence-resolution.d.ts +54 -0
- package/dist/core/evaluate/evidence-resolution.js +153 -0
- package/dist/core/evaluate/operators.d.ts +87 -0
- package/dist/core/evaluate/operators.js +350 -0
- package/dist/core/evaluate/resolution.d.ts +40 -0
- package/dist/core/evaluate/resolution.js +336 -0
- package/dist/core/evaluate/resolved-value.d.ts +9 -0
- package/dist/core/evaluate/resolved-value.js +6 -0
- package/dist/core/failure-codes.d.ts +18 -0
- package/dist/core/failure-codes.js +50 -0
- package/dist/core/lineage/chain.d.ts +49 -0
- package/dist/core/lineage/chain.js +210 -0
- package/dist/core/lineage/freeze.d.ts +12 -0
- package/dist/core/lineage/freeze.js +35 -0
- package/dist/core/lineage/stage-table.d.ts +43 -0
- package/dist/core/lineage/stage-table.js +106 -0
- package/dist/core/preflight/plan.d.ts +67 -0
- package/dist/core/preflight/plan.js +313 -0
- package/dist/core/preflight/projection.d.ts +40 -0
- package/dist/core/preflight/projection.js +96 -0
- package/dist/core/preflight/reduce.d.ts +8 -0
- package/dist/core/preflight/reduce.js +180 -0
- package/dist/core/preflight/witness-evidence.d.ts +42 -0
- package/dist/core/preflight/witness-evidence.js +88 -0
- package/dist/core/probe/target-policy.d.ts +63 -0
- package/dist/core/probe/target-policy.js +328 -0
- package/dist/core/schemas/artifact-reference.d.ts +19 -0
- package/dist/core/schemas/artifact-reference.js +30 -0
- package/dist/core/schemas/artifact.d.ts +1153 -0
- package/dist/core/schemas/artifact.js +84 -0
- package/dist/core/schemas/constraint-ledger.d.ts +57 -0
- package/dist/core/schemas/constraint-ledger.js +143 -0
- package/dist/core/schemas/eval-contract.d.ts +402 -0
- package/dist/core/schemas/eval-contract.js +147 -0
- package/dist/core/schemas/evaluator-configuration.d.ts +37 -0
- package/dist/core/schemas/evaluator-configuration.js +43 -0
- package/dist/core/schemas/evidence-artifact.d.ts +471 -0
- package/dist/core/schemas/evidence-artifact.js +232 -0
- package/dist/core/schemas/expression.d.ts +197 -0
- package/dist/core/schemas/expression.js +284 -0
- package/dist/core/schemas/faults.d.ts +9 -0
- package/dist/core/schemas/faults.js +32 -0
- package/dist/core/schemas/interface.d.ts +417 -0
- package/dist/core/schemas/interface.js +115 -0
- package/dist/core/schemas/isolation-manifest.d.ts +136 -0
- package/dist/core/schemas/isolation-manifest.js +92 -0
- package/dist/core/schemas/lineage.d.ts +21 -0
- package/dist/core/schemas/lineage.js +28 -0
- package/dist/core/schemas/oracle.d.ts +72 -0
- package/dist/core/schemas/oracle.js +35 -0
- package/dist/core/schemas/plan.d.ts +115 -0
- package/dist/core/schemas/plan.js +58 -0
- package/dist/core/schemas/pointer.d.ts +63 -0
- package/dist/core/schemas/pointer.js +94 -0
- package/dist/core/schemas/port-messages.d.ts +103 -0
- package/dist/core/schemas/port-messages.js +90 -0
- package/dist/core/schemas/preflight-verdict.d.ts +60 -0
- package/dist/core/schemas/preflight-verdict.js +38 -0
- package/dist/core/schemas/primitives.d.ts +78 -0
- package/dist/core/schemas/primitives.js +121 -0
- package/dist/core/schemas/private-artifact-manifest.d.ts +56 -0
- package/dist/core/schemas/private-artifact-manifest.js +46 -0
- package/dist/core/schemas/probe-body.d.ts +25 -0
- package/dist/core/schemas/probe-body.js +23 -0
- package/dist/core/schemas/probe-policy.d.ts +76 -0
- package/dist/core/schemas/probe-policy.js +33 -0
- package/dist/core/schemas/probe.d.ts +189 -0
- package/dist/core/schemas/probe.js +82 -0
- package/dist/core/schemas/publish.d.ts +30 -0
- package/dist/core/schemas/publish.js +135 -0
- package/dist/core/schemas/reference-set.d.ts +18 -0
- package/dist/core/schemas/reference-set.js +27 -0
- package/dist/core/schemas/rubric.d.ts +62 -0
- package/dist/core/schemas/rubric.js +68 -0
- package/dist/core/schemas/scoring-policy.d.ts +28 -0
- package/dist/core/schemas/scoring-policy.js +51 -0
- package/dist/core/schemas/sealed-evaluator-brief.d.ts +80 -0
- package/dist/core/schemas/sealed-evaluator-brief.js +59 -0
- package/dist/core/schemas/sealed-run-record.d.ts +382 -0
- package/dist/core/schemas/sealed-run-record.js +215 -0
- package/dist/core/schemas/sensitivity-witness.d.ts +138 -0
- package/dist/core/schemas/sensitivity-witness.js +86 -0
- package/dist/core/schemas/verdict.d.ts +25 -0
- package/dist/core/schemas/verdict.js +20 -0
- package/dist/core/schemas/waiver.d.ts +17 -0
- package/dist/core/schemas/waiver.js +24 -0
- package/dist/core/seal/derived-reference.d.ts +36 -0
- package/dist/core/seal/derived-reference.js +383 -0
- package/dist/core/seal/direction-prose.d.ts +13 -0
- package/dist/core/seal/direction-prose.js +120 -0
- package/dist/core/seal/plan-index.d.ts +63 -0
- package/dist/core/seal/plan-index.js +155 -0
- package/dist/core/seal/scripting-audit.d.ts +15 -0
- package/dist/core/seal/scripting-audit.js +51 -0
- package/dist/core/seal/seal.d.ts +18 -0
- package/dist/core/seal/seal.js +155 -0
- package/dist/core/stage-contracts.d.ts +30 -0
- package/dist/core/stage-contracts.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +22 -0
- package/dist/ports/clock-port.d.ts +13 -0
- package/dist/ports/clock-port.js +7 -0
- package/dist/ports/corpus-port.d.ts +16 -0
- package/dist/ports/corpus-port.js +7 -0
- package/dist/ports/environment-probe-port.d.ts +76 -0
- package/dist/ports/environment-probe-port.js +32 -0
- package/dist/ports/file-system-port.d.ts +27 -0
- package/dist/ports/file-system-port.js +11 -0
- package/dist/ports/port.d.ts +33 -0
- package/dist/ports/port.js +14 -0
- package/dist/testing/conformance.d.ts +88 -0
- package/dist/testing/conformance.js +281 -0
- package/dist/testing/index.d.ts +26 -0
- package/dist/testing/index.js +18 -0
- package/dist/testing/probe-conformance.d.ts +45 -0
- package/dist/testing/probe-conformance.js +186 -0
- package/package.json +130 -0
- package/schemas/artifact-reference.schema.json +64 -0
- package/schemas/eval-contract.schema.json +2145 -0
- package/schemas/evaluator-configuration.schema.json +199 -0
- package/schemas/evidence-artifact.schema.json +1229 -0
- package/schemas/isolation-manifest.schema.json +394 -0
- package/schemas/preflight-verdict.schema.json +114 -0
- package/schemas/private-artifact-manifest.schema.json +103 -0
- package/schemas/probe.schema.json +1151 -0
- package/schemas/rubric.schema.json +148 -0
- package/schemas/scoring-policy.schema.json +90 -0
- package/schemas/sealed-evaluator-brief.schema.json +268 -0
- package/schemas/sealed-run-record.schema.json +875 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter between a probe leg and AD-4's resolver. A witness relation is an
|
|
3
|
+
* ordinary `Expression`, so `resolveCheck` and `makeResolveOperand` are reused
|
|
4
|
+
* unchanged. This module supplies the `Observation` shape they read and a
|
|
5
|
+
* collection predicate scoped to one operation.
|
|
6
|
+
*/
|
|
7
|
+
import { type PointerDenotesCollection } from '../evaluate/resolution.ts';
|
|
8
|
+
import type { EvalContract } from '../schemas/eval-contract.ts';
|
|
9
|
+
import type { CheckResolutionValue } from '../schemas/evidence-artifact.ts';
|
|
10
|
+
import type { Expression } from '../schemas/expression.ts';
|
|
11
|
+
import type { Operation } from '../schemas/interface.ts';
|
|
12
|
+
import type { ProbeObservation } from '../schemas/port-messages.ts';
|
|
13
|
+
import type { JsonValue } from '../schemas/primitives.ts';
|
|
14
|
+
import type { Observation } from '../schemas/sealed-run-record.ts';
|
|
15
|
+
import type { WitnessInputs } from '../schemas/sensitivity-witness.ts';
|
|
16
|
+
import type { ProjectedObservation } from './projection.ts';
|
|
17
|
+
/**
|
|
18
|
+
* A module constant. A scoring policy is a score-side artifact and AD-38 closes
|
|
19
|
+
* stage one's requirement list against citing one, so pre-flight cannot read
|
|
20
|
+
* its budget from there. The value mirrors the published default policy's.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PREFLIGHT_REGEX_MATCH_STEP_BUDGET = 1000000;
|
|
23
|
+
/**
|
|
24
|
+
* One leg as the `Observation` `makeResolveOperand` takes. The relation reads the
|
|
25
|
+
* **projected** body, which is AD-10's "evaluated over that operation's response
|
|
26
|
+
* descriptor after excluding the volatile pointers", and the raw headers, which
|
|
27
|
+
* the projection does not carry. `provenance` is `baseline`: a pre-flight leg is
|
|
28
|
+
* pre-canned by definition.
|
|
29
|
+
*/
|
|
30
|
+
export declare function evidenceOf(projected: ProjectedObservation, observation: ProbeObservation, inputs: WitnessInputs): Observation;
|
|
31
|
+
/** the contract's declared reference sets in the shape the resolver wants. */
|
|
32
|
+
export declare function referenceSetMembers(contract: EvalContract): Readonly<Record<string, JsonValue[]>>;
|
|
33
|
+
/**
|
|
34
|
+
* Answers `true` only for a `response-body` pointer whose tail names a declared
|
|
35
|
+
* collection location of **this** operation. `makePointerDenotesCollection` does
|
|
36
|
+
* not work here: it resolves through the interaction plan, where a witness leg
|
|
37
|
+
* never appears, and fabricating step objects for a one-operation lookup is more
|
|
38
|
+
* machinery than the predicate.
|
|
39
|
+
*/
|
|
40
|
+
export declare function makeWitnessPointerDenotesCollection(operation: Operation): PointerDenotesCollection;
|
|
41
|
+
/** Resolves one witness relation over the legs it addresses. */
|
|
42
|
+
export declare function resolveWitnessRelation(relation: Expression, legEvidence: Readonly<Record<string, Observation>>, operation: Operation, referenceSets: Readonly<Record<string, JsonValue[]>>, artifactPath: string): CheckResolutionValue;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter between a probe leg and AD-4's resolver. A witness relation is an
|
|
3
|
+
* ordinary `Expression`, so `resolveCheck` and `makeResolveOperand` are reused
|
|
4
|
+
* unchanged. This module supplies the `Observation` shape they read and a
|
|
5
|
+
* collection predicate scoped to one operation.
|
|
6
|
+
*/
|
|
7
|
+
import { makeResolveOperand } from '../evaluate/evidence-resolution.js';
|
|
8
|
+
import { resolveCheck, } from '../evaluate/resolution.js';
|
|
9
|
+
import { decodeTail, parseEvidenceTarget } from '../seal/plan-index.js';
|
|
10
|
+
/**
|
|
11
|
+
* A module constant. A scoring policy is a score-side artifact and AD-38 closes
|
|
12
|
+
* stage one's requirement list against citing one, so pre-flight cannot read
|
|
13
|
+
* its budget from there. The value mirrors the published default policy's.
|
|
14
|
+
*/
|
|
15
|
+
export const PREFLIGHT_REGEX_MATCH_STEP_BUDGET = 1_000_000;
|
|
16
|
+
const asJsonObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: null;
|
|
19
|
+
/**
|
|
20
|
+
* `ObservedCallInputs` is narrower than `WitnessInputs`, so `body` loses
|
|
21
|
+
* information here: an absent body and a non-object JSON body both map to
|
|
22
|
+
* `null`, and a relation addressing `/interactions/{legId}/call-inputs/body` on
|
|
23
|
+
* such a leg resolves `ABSENT`.
|
|
24
|
+
*/
|
|
25
|
+
const callInputsOf = (inputs) => ({
|
|
26
|
+
path: inputs.path,
|
|
27
|
+
query: inputs.query,
|
|
28
|
+
header: inputs.header,
|
|
29
|
+
body: inputs.body.kind === 'json' ? asJsonObject(inputs.body.value) : null,
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* One leg as the `Observation` `makeResolveOperand` takes. The relation reads the
|
|
33
|
+
* **projected** body, which is AD-10's "evaluated over that operation's response
|
|
34
|
+
* descriptor after excluding the volatile pointers", and the raw headers, which
|
|
35
|
+
* the projection does not carry. `provenance` is `baseline`: a pre-flight leg is
|
|
36
|
+
* pre-canned by definition.
|
|
37
|
+
*/
|
|
38
|
+
export function evidenceOf(projected, observation, inputs) {
|
|
39
|
+
const { body } = projected;
|
|
40
|
+
return {
|
|
41
|
+
observationId: projected.legId,
|
|
42
|
+
operationId: projected.operationId,
|
|
43
|
+
provenance: 'baseline',
|
|
44
|
+
callInputs: callInputsOf(inputs),
|
|
45
|
+
responseBody: body.kind === 'json'
|
|
46
|
+
? body.value
|
|
47
|
+
: body.kind === 'text'
|
|
48
|
+
? body.value
|
|
49
|
+
: null,
|
|
50
|
+
responseHeaders: observation.headers,
|
|
51
|
+
responseStatus: observation.status,
|
|
52
|
+
stdout: null,
|
|
53
|
+
stderr: null,
|
|
54
|
+
exitCode: null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** the contract's declared reference sets in the shape the resolver wants. */
|
|
58
|
+
export function referenceSetMembers(contract) {
|
|
59
|
+
return Object.fromEntries(Object.entries(contract.referenceSets ?? {}).map(([id, set]) => [
|
|
60
|
+
id,
|
|
61
|
+
set.members,
|
|
62
|
+
]));
|
|
63
|
+
}
|
|
64
|
+
const tokensEqual = (a, b) => a.length === b.length && a.every((token, index) => token === b[index]);
|
|
65
|
+
/**
|
|
66
|
+
* Answers `true` only for a `response-body` pointer whose tail names a declared
|
|
67
|
+
* collection location of **this** operation. `makePointerDenotesCollection` does
|
|
68
|
+
* not work here: it resolves through the interaction plan, where a witness leg
|
|
69
|
+
* never appears, and fabricating step objects for a one-operation lookup is more
|
|
70
|
+
* machinery than the predicate.
|
|
71
|
+
*/
|
|
72
|
+
export function makeWitnessPointerDenotesCollection(operation) {
|
|
73
|
+
const { collectionLocations } = operation.responseDescriptor;
|
|
74
|
+
return (pointer) => {
|
|
75
|
+
if (pointer.startsWith('@'))
|
|
76
|
+
return false;
|
|
77
|
+
const target = parseEvidenceTarget(pointer);
|
|
78
|
+
if (target.channel !== 'response-body')
|
|
79
|
+
return false;
|
|
80
|
+
if (collectionLocations === null)
|
|
81
|
+
return false;
|
|
82
|
+
return collectionLocations.some((location) => tokensEqual(decodeTail(location.pointer), target.tail));
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Resolves one witness relation over the legs it addresses. */
|
|
86
|
+
export function resolveWitnessRelation(relation, legEvidence, operation, referenceSets, artifactPath) {
|
|
87
|
+
return resolveCheck(relation, makeResolveOperand(legEvidence, referenceSets), makeWitnessPointerDenotesCollection(operation), PREFLIGHT_REGEX_MATCH_STEP_BUDGET, artifactPath);
|
|
88
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** AD-35's default-deny decision over a resolved target, as a pure function. */
|
|
2
|
+
import type { ProbeTargetAuthorization, ProbeTargetPolicy } from '../schemas/probe-policy.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The classes AD-35 names, plus `public` for everything else and
|
|
5
|
+
* `unparseable`. `unparseable` denies: an address the parser cannot read is
|
|
6
|
+
* one it cannot prove is outside the denied classes.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ADDRESS_CLASSES: readonly ['loopback', 'private', 'link-local', 'metadata', 'public', 'unparseable'];
|
|
9
|
+
export type AddressClass = (typeof ADDRESS_CLASSES)[number];
|
|
10
|
+
/**
|
|
11
|
+
* Why a target was denied. Detail carried in the message; every one is thrown
|
|
12
|
+
* as the single AD-28 `forbidden-target` fault, so `check:ad28-registry`
|
|
13
|
+
* stays at ten.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DENIAL_REASONS: readonly ['interface-not-authorized', 'scheme-not-authorized', 'host-not-authorized', 'port-not-authorized', 'address-not-authorized', 'address-unparseable', 'method-not-authorized'];
|
|
16
|
+
export type DenialReason = (typeof DENIAL_REASONS)[number];
|
|
17
|
+
export type ParsedAddress = {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly family: 4 | 6;
|
|
20
|
+
/** the one spelling both sides of every comparison are reduced to. */
|
|
21
|
+
readonly canonical: string;
|
|
22
|
+
readonly addressClass: AddressClass;
|
|
23
|
+
} | {
|
|
24
|
+
readonly ok: false;
|
|
25
|
+
};
|
|
26
|
+
export type ResolvedTarget = {
|
|
27
|
+
readonly interfaceId: string;
|
|
28
|
+
readonly scheme: string;
|
|
29
|
+
readonly host: string;
|
|
30
|
+
readonly port: number;
|
|
31
|
+
readonly address: string;
|
|
32
|
+
readonly method: string;
|
|
33
|
+
};
|
|
34
|
+
export type PolicyDecision = {
|
|
35
|
+
readonly allowed: true;
|
|
36
|
+
readonly authorization: ProbeTargetAuthorization;
|
|
37
|
+
readonly addressClass: AddressClass;
|
|
38
|
+
readonly canonicalAddress: string;
|
|
39
|
+
} | {
|
|
40
|
+
readonly allowed: false;
|
|
41
|
+
readonly reason: DenialReason;
|
|
42
|
+
readonly detail: string;
|
|
43
|
+
readonly addressClass: AddressClass;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Reduces every spelling of one address to a single `canonical` form and
|
|
47
|
+
* classifies it. Three wrappers come off first, in order: a surrounding
|
|
48
|
+
* bracket pair (what `new URL('http://[::1]/').hostname` returns), a `%zone`
|
|
49
|
+
* suffix, then an `::ffff:` prefix, which unwraps to the embedded IPv4 address
|
|
50
|
+
* and is classified as that address in both the dotted and the hex spelling.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseAddress(address: string): ParsedAddress;
|
|
53
|
+
export declare function classifyAddress(address: string): AddressClass;
|
|
54
|
+
/**
|
|
55
|
+
* AD-35's default-deny evaluation. The interface check runs first, so an
|
|
56
|
+
* unmapped interface never reaches address arithmetic. Where several
|
|
57
|
+
* authorizations name one interface, each is tried in declaration order and
|
|
58
|
+
* the first that allows wins; if none allows, the reported denial is the first
|
|
59
|
+
* one's, so the reason names a target the mapping actually declares.
|
|
60
|
+
*/
|
|
61
|
+
export declare function evaluateTarget(policy: ProbeTargetPolicy, target: ResolvedTarget): PolicyDecision;
|
|
62
|
+
/** AD-35 scopes a differential body-sensitivity probe to the methods the mapping marks safe. An empty `safeMethods` means none of them. */
|
|
63
|
+
export declare function isSafeMethod(authorization: ProbeTargetAuthorization, method: string): boolean;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The classes AD-35 names, plus `public` for everything else and
|
|
3
|
+
* `unparseable`. `unparseable` denies: an address the parser cannot read is
|
|
4
|
+
* one it cannot prove is outside the denied classes.
|
|
5
|
+
*/
|
|
6
|
+
export const ADDRESS_CLASSES = [
|
|
7
|
+
'loopback',
|
|
8
|
+
'private',
|
|
9
|
+
'link-local',
|
|
10
|
+
'metadata',
|
|
11
|
+
'public',
|
|
12
|
+
'unparseable',
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Why a target was denied. Detail carried in the message; every one is thrown
|
|
16
|
+
* as the single AD-28 `forbidden-target` fault, so `check:ad28-registry`
|
|
17
|
+
* stays at ten.
|
|
18
|
+
*/
|
|
19
|
+
export const DENIAL_REASONS = [
|
|
20
|
+
'interface-not-authorized',
|
|
21
|
+
'scheme-not-authorized',
|
|
22
|
+
'host-not-authorized',
|
|
23
|
+
'port-not-authorized',
|
|
24
|
+
'address-not-authorized',
|
|
25
|
+
'address-unparseable',
|
|
26
|
+
'method-not-authorized',
|
|
27
|
+
];
|
|
28
|
+
const UNPARSEABLE = { ok: false };
|
|
29
|
+
// Decimal only, and a leading zero is refused. `inet_aton` reads `010.0.0.1`
|
|
30
|
+
// as octal 8.0.0.1 while a naive parse reads 10.0.0.1; one string with two
|
|
31
|
+
// readings is the disagreement AD-28 exists to stop. Refusing it makes the
|
|
32
|
+
// spelling `unparseable`, which denies.
|
|
33
|
+
const IPV4_OCTET_PATTERN = /^(?:0|[1-9][0-9]{0,2})$/;
|
|
34
|
+
const IPV6_GROUP_PATTERN = /^[0-9a-fA-F]{1,4}$/;
|
|
35
|
+
/** `fd00:ec2::254` fully expanded, the one IPv6 metadata endpoint AD-35 names. */
|
|
36
|
+
const IPV6_METADATA = 'fd00:0ec2:0000:0000:0000:0000:0000:0254';
|
|
37
|
+
function parseIpv4Octets(text) {
|
|
38
|
+
const parts = text.split('.');
|
|
39
|
+
if (parts.length !== 4)
|
|
40
|
+
return undefined;
|
|
41
|
+
const octets = [];
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
if (!IPV4_OCTET_PATTERN.test(part))
|
|
44
|
+
return undefined;
|
|
45
|
+
const value = Number.parseInt(part, 10);
|
|
46
|
+
if (value > 255)
|
|
47
|
+
return undefined;
|
|
48
|
+
octets.push(value);
|
|
49
|
+
}
|
|
50
|
+
return octets;
|
|
51
|
+
}
|
|
52
|
+
/** IPv6 text pieces to 16-bit groups. A dotted quad is legal in the last piece only, where it spells the low 32 bits. */
|
|
53
|
+
function toGroups(pieces, allowEmbeddedIpv4) {
|
|
54
|
+
const groups = [];
|
|
55
|
+
for (let index = 0; index < pieces.length; index++) {
|
|
56
|
+
const piece = pieces[index] ?? '';
|
|
57
|
+
if (allowEmbeddedIpv4 &&
|
|
58
|
+
index === pieces.length - 1 &&
|
|
59
|
+
piece.includes('.')) {
|
|
60
|
+
const octets = parseIpv4Octets(piece);
|
|
61
|
+
if (octets === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
groups.push(((octets[0] ?? 0) << 8) | (octets[1] ?? 0));
|
|
64
|
+
groups.push(((octets[2] ?? 0) << 8) | (octets[3] ?? 0));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!IPV6_GROUP_PATTERN.test(piece))
|
|
68
|
+
return undefined;
|
|
69
|
+
groups.push(Number.parseInt(piece, 16));
|
|
70
|
+
}
|
|
71
|
+
return groups;
|
|
72
|
+
}
|
|
73
|
+
function parseIpv6Groups(text) {
|
|
74
|
+
const halves = text.split('::');
|
|
75
|
+
if (halves.length > 2)
|
|
76
|
+
return undefined;
|
|
77
|
+
if (halves.length === 1) {
|
|
78
|
+
const groups = toGroups(text.split(':'), true);
|
|
79
|
+
return groups?.length === 8 ? groups : undefined;
|
|
80
|
+
}
|
|
81
|
+
const headText = halves[0] ?? '';
|
|
82
|
+
const tailText = halves[1] ?? '';
|
|
83
|
+
const head = toGroups(headText === '' ? [] : headText.split(':'), false);
|
|
84
|
+
const tail = toGroups(tailText === '' ? [] : tailText.split(':'), true);
|
|
85
|
+
if (head === undefined || tail === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
// `::` stands for at least one zero group, so a full eight groups either
|
|
88
|
+
// side of it is malformed.
|
|
89
|
+
const fill = 8 - head.length - tail.length;
|
|
90
|
+
if (fill < 1)
|
|
91
|
+
return undefined;
|
|
92
|
+
const groups = [...head];
|
|
93
|
+
for (let index = 0; index < fill; index++)
|
|
94
|
+
groups.push(0);
|
|
95
|
+
groups.push(...tail);
|
|
96
|
+
return groups;
|
|
97
|
+
}
|
|
98
|
+
/** The low 32 bits read as IPv4 octets. */
|
|
99
|
+
function lowOctets(groups) {
|
|
100
|
+
const high = groups[6] ?? 0;
|
|
101
|
+
const low = groups[7] ?? 0;
|
|
102
|
+
return [high >> 8, high & 0xff, low >> 8, low & 0xff];
|
|
103
|
+
}
|
|
104
|
+
/** The embedded IPv4 octets of an `::ffff:0:0/96` address, or `undefined` for any other IPv6 address. */
|
|
105
|
+
function mappedIpv4Octets(groups) {
|
|
106
|
+
for (let index = 0; index < 5; index++) {
|
|
107
|
+
if (groups[index] !== 0)
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
if (groups[5] !== 0xffff)
|
|
111
|
+
return undefined;
|
|
112
|
+
return lowOctets(groups);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The embedded IPv4 octets of the other two well-known prefixes that carry one
|
|
116
|
+
* in their low 32 bits: `::/96`, RFC 4291's deprecated IPv4-compatible form,
|
|
117
|
+
* and `64:ff9b::/96`, RFC 6052's NAT64 prefix. Both reach the embedded address
|
|
118
|
+
* on a stack that translates them, so `64:ff9b::169.254.169.254` reported as
|
|
119
|
+
* `public` is the same wrong answer the metadata ordering exists to prevent.
|
|
120
|
+
*
|
|
121
|
+
* Classification only. The canonical form stays the expanded IPv6 string, so
|
|
122
|
+
* an authorization naming `127.0.0.1` does not permit `::127.0.0.1`: only
|
|
123
|
+
* `::ffff:0:0/96` rewrites the canonical form, because only that rewrite is
|
|
124
|
+
* required (fixture 34) and any other widening of an exact allowlist should
|
|
125
|
+
* fail closed.
|
|
126
|
+
*/
|
|
127
|
+
function translatedIpv4Octets(groups) {
|
|
128
|
+
const nat64 = groups[0] === 0x0064 &&
|
|
129
|
+
groups[1] === 0xff9b &&
|
|
130
|
+
groups[2] === 0 &&
|
|
131
|
+
groups[3] === 0 &&
|
|
132
|
+
groups[4] === 0 &&
|
|
133
|
+
groups[5] === 0;
|
|
134
|
+
const compatible = groups.slice(0, 6).every((group) => group === 0) &&
|
|
135
|
+
!(groups[6] === 0 && (groups[7] === 0 || groups[7] === 1));
|
|
136
|
+
return nat64 || compatible ? lowOctets(groups) : undefined;
|
|
137
|
+
}
|
|
138
|
+
function classifyIpv4(octets) {
|
|
139
|
+
const first = octets[0] ?? 0;
|
|
140
|
+
const second = octets[1] ?? 0;
|
|
141
|
+
const third = octets[2] ?? 0;
|
|
142
|
+
const fourth = octets[3] ?? 0;
|
|
143
|
+
// Metadata precedes link-local: 169.254.169.254 sits inside 169.254.0.0/16,
|
|
144
|
+
// and the other ordering reports a cloud metadata endpoint as something
|
|
145
|
+
// milder on the one denial an operator has to read.
|
|
146
|
+
if (first === 169 && second === 254 && third === 169 && fourth === 254) {
|
|
147
|
+
return 'metadata';
|
|
148
|
+
}
|
|
149
|
+
if (first === 127)
|
|
150
|
+
return 'loopback';
|
|
151
|
+
// The unspecified address routes to local on every stack this package runs
|
|
152
|
+
// on, so it counts as loopback.
|
|
153
|
+
if (first === 0 && second === 0 && third === 0 && fourth === 0) {
|
|
154
|
+
return 'loopback';
|
|
155
|
+
}
|
|
156
|
+
if (first === 169 && second === 254)
|
|
157
|
+
return 'link-local';
|
|
158
|
+
if (first === 10)
|
|
159
|
+
return 'private';
|
|
160
|
+
if (first === 172 && second >= 16 && second <= 31)
|
|
161
|
+
return 'private';
|
|
162
|
+
if (first === 192 && second === 168)
|
|
163
|
+
return 'private';
|
|
164
|
+
if (first === 100 && second >= 64 && second <= 127)
|
|
165
|
+
return 'private';
|
|
166
|
+
return 'public';
|
|
167
|
+
}
|
|
168
|
+
function classifyIpv6(groups, canonical) {
|
|
169
|
+
// Metadata precedes private for the same reason it precedes link-local in
|
|
170
|
+
// IPv4: fd00:ec2::254 sits inside fc00::/7.
|
|
171
|
+
if (canonical === IPV6_METADATA)
|
|
172
|
+
return 'metadata';
|
|
173
|
+
const translated = translatedIpv4Octets(groups);
|
|
174
|
+
if (translated !== undefined)
|
|
175
|
+
return classifyIpv4(translated);
|
|
176
|
+
const allButLastAreZero = groups
|
|
177
|
+
.slice(0, 7)
|
|
178
|
+
.every((group) => group === 0);
|
|
179
|
+
const last = groups[7] ?? 0;
|
|
180
|
+
if (allButLastAreZero && (last === 0 || last === 1))
|
|
181
|
+
return 'loopback';
|
|
182
|
+
if (((groups[0] ?? 0) & 0xffc0) === 0xfe80)
|
|
183
|
+
return 'link-local';
|
|
184
|
+
if (((groups[0] ?? 0) & 0xfe00) === 0xfc00)
|
|
185
|
+
return 'private';
|
|
186
|
+
return 'public';
|
|
187
|
+
}
|
|
188
|
+
function ipv4Address(octets) {
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
family: 4,
|
|
192
|
+
canonical: octets.join('.'),
|
|
193
|
+
addressClass: classifyIpv4(octets),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Reduces every spelling of one address to a single `canonical` form and
|
|
198
|
+
* classifies it. Three wrappers come off first, in order: a surrounding
|
|
199
|
+
* bracket pair (what `new URL('http://[::1]/').hostname` returns), a `%zone`
|
|
200
|
+
* suffix, then an `::ffff:` prefix, which unwraps to the embedded IPv4 address
|
|
201
|
+
* and is classified as that address in both the dotted and the hex spelling.
|
|
202
|
+
*/
|
|
203
|
+
export function parseAddress(address) {
|
|
204
|
+
const unbracketed = address.startsWith('[') && address.endsWith(']') && address.length >= 2
|
|
205
|
+
? address.slice(1, -1)
|
|
206
|
+
: address;
|
|
207
|
+
const zoneAt = unbracketed.indexOf('%');
|
|
208
|
+
const zoned = zoneAt !== -1;
|
|
209
|
+
const bare = zoned ? unbracketed.slice(0, zoneAt) : unbracketed;
|
|
210
|
+
if (zoned) {
|
|
211
|
+
// An empty zone (`fe80::1%`) and a repeated one (`fe80::1%eth0%extra`)
|
|
212
|
+
// are malformed. Dropping either would let a spelling no stack accepts
|
|
213
|
+
// match an authorization naming the bare address.
|
|
214
|
+
const zone = unbracketed.slice(zoneAt + 1);
|
|
215
|
+
if (zone.length === 0 || zone.includes('%'))
|
|
216
|
+
return UNPARSEABLE;
|
|
217
|
+
}
|
|
218
|
+
if (bare.length === 0)
|
|
219
|
+
return UNPARSEABLE;
|
|
220
|
+
if (!bare.includes(':')) {
|
|
221
|
+
// A zone identifier on an IPv4 literal is not an address on any stack,
|
|
222
|
+
// so `127.0.0.1%eth0` is refused instead of quietly becoming
|
|
223
|
+
// `127.0.0.1`.
|
|
224
|
+
if (zoned)
|
|
225
|
+
return UNPARSEABLE;
|
|
226
|
+
const octets = parseIpv4Octets(bare);
|
|
227
|
+
return octets === undefined ? UNPARSEABLE : ipv4Address(octets);
|
|
228
|
+
}
|
|
229
|
+
const groups = parseIpv6Groups(bare);
|
|
230
|
+
if (groups === undefined)
|
|
231
|
+
return UNPARSEABLE;
|
|
232
|
+
const mapped = mappedIpv4Octets(groups);
|
|
233
|
+
if (mapped !== undefined) {
|
|
234
|
+
return zoned ? UNPARSEABLE : ipv4Address(mapped);
|
|
235
|
+
}
|
|
236
|
+
const canonical = groups
|
|
237
|
+
.map((group) => group.toString(16).padStart(4, '0'))
|
|
238
|
+
.join(':');
|
|
239
|
+
const addressClass = classifyIpv6(groups, canonical);
|
|
240
|
+
// A zone scopes an address to one interface, and only a link-local address
|
|
241
|
+
// needs that. Elsewhere the `%` is refused, so a zone cannot ride along on
|
|
242
|
+
// an address whose class does not use it. Two zones of one link-local
|
|
243
|
+
// address still share a canonical form: fixture 35 requires an
|
|
244
|
+
// authorization naming `fe80::1` to permit `[fe80:0:0:0:0:0:0:1%eth0]`, so
|
|
245
|
+
// zone-insensitive matching inside `fe80::/10` is the mandated behaviour
|
|
246
|
+
// and `%eth0` and `%eth1` are one entry.
|
|
247
|
+
if (zoned && addressClass !== 'link-local')
|
|
248
|
+
return UNPARSEABLE;
|
|
249
|
+
return { ok: true, family: 6, canonical, addressClass };
|
|
250
|
+
}
|
|
251
|
+
export function classifyAddress(address) {
|
|
252
|
+
const parsed = parseAddress(address);
|
|
253
|
+
return parsed.ok ? parsed.addressClass : 'unparseable';
|
|
254
|
+
}
|
|
255
|
+
// DNS is case-insensitive and `example.test.` and `example.test` are one name,
|
|
256
|
+
// so a mixed-case or dot-suffixed redirect target would otherwise walk past
|
|
257
|
+
// the host check. One trailing dot only: `example.test..` spells nothing.
|
|
258
|
+
function normalizeHost(host) {
|
|
259
|
+
const lowered = host.toLowerCase();
|
|
260
|
+
return lowered.endsWith('.') ? lowered.slice(0, -1) : lowered;
|
|
261
|
+
}
|
|
262
|
+
function deny(reason, detail, addressClass) {
|
|
263
|
+
return { allowed: false, reason, detail, addressClass };
|
|
264
|
+
}
|
|
265
|
+
function evaluateAgainst(authorization, target, addressClass) {
|
|
266
|
+
if (authorization.scheme !== target.scheme) {
|
|
267
|
+
return deny('scheme-not-authorized', `scheme "${target.scheme}" is not the authorized "${authorization.scheme}"`, addressClass);
|
|
268
|
+
}
|
|
269
|
+
if (normalizeHost(authorization.host) !== normalizeHost(target.host)) {
|
|
270
|
+
return deny('host-not-authorized', `host "${target.host}" is not the authorized "${authorization.host}"`, addressClass);
|
|
271
|
+
}
|
|
272
|
+
if (authorization.port !== target.port) {
|
|
273
|
+
return deny('port-not-authorized', `port ${target.port} is not the authorized ${authorization.port}`, addressClass);
|
|
274
|
+
}
|
|
275
|
+
const parsed = parseAddress(target.address);
|
|
276
|
+
if (!parsed.ok) {
|
|
277
|
+
return deny('address-unparseable', `address "${target.address}" could not be parsed, so it cannot be proven outside a denied class`, 'unparseable');
|
|
278
|
+
}
|
|
279
|
+
// Parsed on both sides. An authorization naming `127.0.0.1` matches
|
|
280
|
+
// `::ffff:127.0.0.1` and `[::ffff:127.0.0.1]`; a string comparison sees
|
|
281
|
+
// three different addresses.
|
|
282
|
+
const named = authorization.addresses.some((entry) => {
|
|
283
|
+
const parsedEntry = parseAddress(entry);
|
|
284
|
+
return parsedEntry.ok && parsedEntry.canonical === parsed.canonical;
|
|
285
|
+
});
|
|
286
|
+
if (!named) {
|
|
287
|
+
return deny('address-not-authorized', `address "${target.address}" (${parsed.canonical}, class ${parsed.addressClass}) is named by no authorized address`, parsed.addressClass);
|
|
288
|
+
}
|
|
289
|
+
if (!authorization.methods.some((method) => method === target.method)) {
|
|
290
|
+
return deny('method-not-authorized', `method "${target.method}" is not among the authorized methods`, parsed.addressClass);
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
allowed: true,
|
|
294
|
+
authorization,
|
|
295
|
+
addressClass: parsed.addressClass,
|
|
296
|
+
canonicalAddress: parsed.canonical,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* AD-35's default-deny evaluation. The interface check runs first, so an
|
|
301
|
+
* unmapped interface never reaches address arithmetic. Where several
|
|
302
|
+
* authorizations name one interface, each is tried in declaration order and
|
|
303
|
+
* the first that allows wins; if none allows, the reported denial is the first
|
|
304
|
+
* one's, so the reason names a target the mapping actually declares.
|
|
305
|
+
*/
|
|
306
|
+
export function evaluateTarget(policy, target) {
|
|
307
|
+
const addressClass = classifyAddress(target.address);
|
|
308
|
+
let firstDenial;
|
|
309
|
+
for (const authorization of policy.authorizations) {
|
|
310
|
+
if (authorization.interfaceId !== target.interfaceId)
|
|
311
|
+
continue;
|
|
312
|
+
const decision = evaluateAgainst(authorization, target, addressClass);
|
|
313
|
+
if (decision.allowed)
|
|
314
|
+
return decision;
|
|
315
|
+
firstDenial ??= decision;
|
|
316
|
+
}
|
|
317
|
+
return (firstDenial ??
|
|
318
|
+
deny('interface-not-authorized', `no authorization names interface "${target.interfaceId}"`, addressClass));
|
|
319
|
+
}
|
|
320
|
+
/** AD-35 scopes a differential body-sensitivity probe to the methods the mapping marks safe. An empty `safeMethods` means none of them. */
|
|
321
|
+
export function isSafeMethod(authorization, method) {
|
|
322
|
+
// Membership in both lists. `safeMethods` is declared without a subset
|
|
323
|
+
// refinement, so a mapping may mark a method safe that it never authorized;
|
|
324
|
+
// answering `true` there would let a differential select a method
|
|
325
|
+
// `evaluateTarget` goes on to deny.
|
|
326
|
+
return (authorization.methods.some((allowed) => allowed === method) &&
|
|
327
|
+
authorization.safeMethods.some((safe) => safe === method));
|
|
328
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** the four-field reference every artifact points at evidence with. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/**
|
|
4
|
+
* Both branches keep every prior-art key so the correspondence stays readable;
|
|
5
|
+
* the inapplicable member is `z.null()` rather than omitted, following the
|
|
6
|
+
* explicit-`null` convention for a branch where the key has no value.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ArtifactReference: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
9
|
+
storage: z.ZodLiteral<"public">;
|
|
10
|
+
path: z.ZodString;
|
|
11
|
+
privateRef: z.ZodNull;
|
|
12
|
+
digest: z.ZodString;
|
|
13
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
14
|
+
storage: z.ZodLiteral<"private">;
|
|
15
|
+
path: z.ZodNull;
|
|
16
|
+
privateRef: z.ZodString;
|
|
17
|
+
digest: z.ZodString;
|
|
18
|
+
}, z.core.$strict>], "storage">;
|
|
19
|
+
export type ArtifactReference = z.infer<typeof ArtifactReference>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** the four-field reference every artifact points at evidence with. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { Digest } from './primitives.js';
|
|
4
|
+
/**
|
|
5
|
+
* Both branches keep every prior-art key so the correspondence stays readable;
|
|
6
|
+
* the inapplicable member is `z.null()` rather than omitted, following the
|
|
7
|
+
* explicit-`null` convention for a branch where the key has no value.
|
|
8
|
+
*/
|
|
9
|
+
export const ArtifactReference = z
|
|
10
|
+
.discriminatedUnion('storage', [
|
|
11
|
+
z.strictObject({
|
|
12
|
+
storage: z.literal('public'),
|
|
13
|
+
path: z.string().min(1),
|
|
14
|
+
privateRef: z.null(),
|
|
15
|
+
digest: Digest,
|
|
16
|
+
}),
|
|
17
|
+
z.strictObject({
|
|
18
|
+
storage: z.literal('private'),
|
|
19
|
+
path: z.null(),
|
|
20
|
+
privateRef: z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1)
|
|
23
|
+
.describe('An opaque handle resolved through the corpus port under AD-8, never a filesystem path and never a URL. AD-18 forbids a private path from entering any artifact, and this is the field a caller would otherwise reach for.'),
|
|
24
|
+
digest: Digest,
|
|
25
|
+
}),
|
|
26
|
+
])
|
|
27
|
+
.meta({
|
|
28
|
+
id: 'ArtifactReference',
|
|
29
|
+
description: "A reference to an artifact, public by path or private by opaque handle. Succeeds the prior-art `artifact-ref` schema per AD-24, with its two `if`/`then` branches re-expressed as a discriminated union per AD-13 and its `sha256` field renamed to `digest` against the shared AD-27 primitive. Alone among the twelve interchange artifacts this one carries NO `schemaVersion` and NO AD-29 lineage: it is a reference shape embedded inside other artifacts rather than one that crosses the package boundary alone, so versioning it would add a key to every finding and every manifest entry for no reader. It is in the inventory because it is published, not because it is exchanged; the exemption is asserted by a test against the registry's `carriesLineage` flag.",
|
|
30
|
+
});
|