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.
Files changed (230) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +355 -0
  3. package/corpus/dev/README.md +36 -0
  4. package/corpus/dev/compile-seal-example/brief.json +1 -0
  5. package/corpus/dev/compile-seal-example/contract.json +1 -0
  6. package/corpus/dev/contracts/absent-collection-locations.json +1 -0
  7. package/corpus/dev/contracts/absent-sibling-groups.json +1 -0
  8. package/corpus/dev/contracts/absent-success-indicator.json +1 -0
  9. package/corpus/dev/contracts/empty-channel-roles.json +1 -0
  10. package/corpus/dev/contracts/empty-collection-locations.json +1 -0
  11. package/corpus/dev/contracts/empty-request-shapes.json +1 -0
  12. package/corpus/dev/contracts/empty-sibling-groups.json +1 -0
  13. package/corpus/dev/contracts/no-collection-quantifier.json +1 -0
  14. package/corpus/dev/contracts/no-operation-inventory.json +1 -0
  15. package/corpus/dev/contracts/no-read-back-relation.json +1 -0
  16. package/corpus/dev/contracts/no-state-change-marker.json +1 -0
  17. package/corpus/dev/contracts/no-type-violating-step.json +1 -0
  18. package/corpus/dev/contracts/per-key-split-oracles.json +1 -0
  19. package/corpus/dev/contracts/satisfied-declarations.json +1 -0
  20. package/corpus/dev/contracts/single-required-response-key.json +1 -0
  21. package/corpus/dev/contracts/split-indicator-oracle.json +1 -0
  22. package/corpus/dev/contracts/unaddressed-parameter-sibling.json +1 -0
  23. package/corpus/dev/contracts/unnamed-reference-set.json +1 -0
  24. package/corpus/dev/contracts/wrong-cardinality-form.json +1 -0
  25. package/corpus/dev/index.json +1 -0
  26. package/dist/adapters/index.d.ts +12 -0
  27. package/dist/adapters/index.js +3 -0
  28. package/dist/adapters/local-corpus-adapter.d.ts +7 -0
  29. package/dist/adapters/local-corpus-adapter.js +78 -0
  30. package/dist/adapters/node-file-system-adapter.d.ts +7 -0
  31. package/dist/adapters/node-file-system-adapter.js +39 -0
  32. package/dist/adapters/port-boundary.d.ts +18 -0
  33. package/dist/adapters/port-boundary.js +83 -0
  34. package/dist/adapters/system-clock-adapter.d.ts +10 -0
  35. package/dist/adapters/system-clock-adapter.js +18 -0
  36. package/dist/application/compile.d.ts +4 -0
  37. package/dist/application/compile.js +24 -0
  38. package/dist/application/diagnostics.d.ts +22 -0
  39. package/dist/application/diagnostics.js +18 -0
  40. package/dist/application/index.d.ts +23 -0
  41. package/dist/application/index.js +17 -0
  42. package/dist/application/invoke-port.d.ts +2 -0
  43. package/dist/application/invoke-port.js +45 -0
  44. package/dist/application/preflight.d.ts +27 -0
  45. package/dist/application/preflight.js +131 -0
  46. package/dist/application/seal.d.ts +4 -0
  47. package/dist/application/seal.js +28 -0
  48. package/dist/application/serialize.d.ts +6 -0
  49. package/dist/application/serialize.js +9 -0
  50. package/dist/cli/arguments.d.ts +28 -0
  51. package/dist/cli/arguments.js +167 -0
  52. package/dist/cli/exit-codes.d.ts +36 -0
  53. package/dist/cli/exit-codes.js +39 -0
  54. package/dist/cli/main.d.ts +2 -0
  55. package/dist/cli/main.js +143 -0
  56. package/dist/cli/render.d.ts +26 -0
  57. package/dist/cli/render.js +53 -0
  58. package/dist/cli/run.d.ts +41 -0
  59. package/dist/cli/run.js +203 -0
  60. package/dist/core/canonical/canonicalize.d.ts +1 -0
  61. package/dist/core/canonical/canonicalize.js +113 -0
  62. package/dist/core/canonical/digest.d.ts +7 -0
  63. package/dist/core/canonical/digest.js +81 -0
  64. package/dist/core/canonical/scan-json.d.ts +1 -0
  65. package/dist/core/canonical/scan-json.js +298 -0
  66. package/dist/core/canonical/value-domain.d.ts +4 -0
  67. package/dist/core/canonical/value-domain.js +113 -0
  68. package/dist/core/compile/compile.d.ts +24 -0
  69. package/dist/core/compile/compile.js +67 -0
  70. package/dist/core/compile/declarations.d.ts +5 -0
  71. package/dist/core/compile/declarations.js +22 -0
  72. package/dist/core/compile/expression-legality.d.ts +14 -0
  73. package/dist/core/compile/expression-legality.js +346 -0
  74. package/dist/core/compile/forbidden-inputs.d.ts +5 -0
  75. package/dist/core/compile/forbidden-inputs.js +19 -0
  76. package/dist/core/compile/interface-inventory.d.ts +10 -0
  77. package/dist/core/compile/interface-inventory.js +55 -0
  78. package/dist/core/compile/oracle-alignment.d.ts +14 -0
  79. package/dist/core/compile/oracle-alignment.js +93 -0
  80. package/dist/core/compile/reachability.d.ts +17 -0
  81. package/dist/core/compile/reachability.js +177 -0
  82. package/dist/core/compile/rubrics.d.ts +55 -0
  83. package/dist/core/compile/rubrics.js +198 -0
  84. package/dist/core/compile/scripting-bound.d.ts +5 -0
  85. package/dist/core/compile/scripting-bound.js +151 -0
  86. package/dist/core/compile/sensitivity-witness.d.ts +73 -0
  87. package/dist/core/compile/sensitivity-witness.js +232 -0
  88. package/dist/core/compile/waivers.d.ts +3 -0
  89. package/dist/core/compile/waivers.js +21 -0
  90. package/dist/core/coverage/coverage.d.ts +18 -0
  91. package/dist/core/coverage/coverage.js +47 -0
  92. package/dist/core/coverage/relevance.d.ts +75 -0
  93. package/dist/core/coverage/relevance.js +186 -0
  94. package/dist/core/coverage/rules.d.ts +7 -0
  95. package/dist/core/coverage/rules.js +18 -0
  96. package/dist/core/coverage/satisfaction.d.ts +107 -0
  97. package/dist/core/coverage/satisfaction.js +415 -0
  98. package/dist/core/coverage/table.d.ts +26 -0
  99. package/dist/core/coverage/table.js +222 -0
  100. package/dist/core/declared-inputs.d.ts +16 -0
  101. package/dist/core/declared-inputs.js +18 -0
  102. package/dist/core/evaluate/evidence-resolution.d.ts +54 -0
  103. package/dist/core/evaluate/evidence-resolution.js +153 -0
  104. package/dist/core/evaluate/operators.d.ts +87 -0
  105. package/dist/core/evaluate/operators.js +350 -0
  106. package/dist/core/evaluate/resolution.d.ts +40 -0
  107. package/dist/core/evaluate/resolution.js +336 -0
  108. package/dist/core/evaluate/resolved-value.d.ts +9 -0
  109. package/dist/core/evaluate/resolved-value.js +6 -0
  110. package/dist/core/failure-codes.d.ts +18 -0
  111. package/dist/core/failure-codes.js +50 -0
  112. package/dist/core/lineage/chain.d.ts +49 -0
  113. package/dist/core/lineage/chain.js +210 -0
  114. package/dist/core/lineage/freeze.d.ts +12 -0
  115. package/dist/core/lineage/freeze.js +35 -0
  116. package/dist/core/lineage/stage-table.d.ts +43 -0
  117. package/dist/core/lineage/stage-table.js +106 -0
  118. package/dist/core/preflight/plan.d.ts +67 -0
  119. package/dist/core/preflight/plan.js +313 -0
  120. package/dist/core/preflight/projection.d.ts +40 -0
  121. package/dist/core/preflight/projection.js +96 -0
  122. package/dist/core/preflight/reduce.d.ts +8 -0
  123. package/dist/core/preflight/reduce.js +180 -0
  124. package/dist/core/preflight/witness-evidence.d.ts +42 -0
  125. package/dist/core/preflight/witness-evidence.js +88 -0
  126. package/dist/core/probe/target-policy.d.ts +63 -0
  127. package/dist/core/probe/target-policy.js +328 -0
  128. package/dist/core/schemas/artifact-reference.d.ts +19 -0
  129. package/dist/core/schemas/artifact-reference.js +30 -0
  130. package/dist/core/schemas/artifact.d.ts +1153 -0
  131. package/dist/core/schemas/artifact.js +84 -0
  132. package/dist/core/schemas/constraint-ledger.d.ts +57 -0
  133. package/dist/core/schemas/constraint-ledger.js +143 -0
  134. package/dist/core/schemas/eval-contract.d.ts +402 -0
  135. package/dist/core/schemas/eval-contract.js +147 -0
  136. package/dist/core/schemas/evaluator-configuration.d.ts +37 -0
  137. package/dist/core/schemas/evaluator-configuration.js +43 -0
  138. package/dist/core/schemas/evidence-artifact.d.ts +471 -0
  139. package/dist/core/schemas/evidence-artifact.js +232 -0
  140. package/dist/core/schemas/expression.d.ts +197 -0
  141. package/dist/core/schemas/expression.js +284 -0
  142. package/dist/core/schemas/faults.d.ts +9 -0
  143. package/dist/core/schemas/faults.js +32 -0
  144. package/dist/core/schemas/interface.d.ts +417 -0
  145. package/dist/core/schemas/interface.js +115 -0
  146. package/dist/core/schemas/isolation-manifest.d.ts +136 -0
  147. package/dist/core/schemas/isolation-manifest.js +92 -0
  148. package/dist/core/schemas/lineage.d.ts +21 -0
  149. package/dist/core/schemas/lineage.js +28 -0
  150. package/dist/core/schemas/oracle.d.ts +72 -0
  151. package/dist/core/schemas/oracle.js +35 -0
  152. package/dist/core/schemas/plan.d.ts +115 -0
  153. package/dist/core/schemas/plan.js +58 -0
  154. package/dist/core/schemas/pointer.d.ts +63 -0
  155. package/dist/core/schemas/pointer.js +94 -0
  156. package/dist/core/schemas/port-messages.d.ts +103 -0
  157. package/dist/core/schemas/port-messages.js +90 -0
  158. package/dist/core/schemas/preflight-verdict.d.ts +60 -0
  159. package/dist/core/schemas/preflight-verdict.js +38 -0
  160. package/dist/core/schemas/primitives.d.ts +78 -0
  161. package/dist/core/schemas/primitives.js +121 -0
  162. package/dist/core/schemas/private-artifact-manifest.d.ts +56 -0
  163. package/dist/core/schemas/private-artifact-manifest.js +46 -0
  164. package/dist/core/schemas/probe-body.d.ts +25 -0
  165. package/dist/core/schemas/probe-body.js +23 -0
  166. package/dist/core/schemas/probe-policy.d.ts +76 -0
  167. package/dist/core/schemas/probe-policy.js +33 -0
  168. package/dist/core/schemas/probe.d.ts +189 -0
  169. package/dist/core/schemas/probe.js +82 -0
  170. package/dist/core/schemas/publish.d.ts +30 -0
  171. package/dist/core/schemas/publish.js +135 -0
  172. package/dist/core/schemas/reference-set.d.ts +18 -0
  173. package/dist/core/schemas/reference-set.js +27 -0
  174. package/dist/core/schemas/rubric.d.ts +62 -0
  175. package/dist/core/schemas/rubric.js +68 -0
  176. package/dist/core/schemas/scoring-policy.d.ts +28 -0
  177. package/dist/core/schemas/scoring-policy.js +51 -0
  178. package/dist/core/schemas/sealed-evaluator-brief.d.ts +80 -0
  179. package/dist/core/schemas/sealed-evaluator-brief.js +59 -0
  180. package/dist/core/schemas/sealed-run-record.d.ts +382 -0
  181. package/dist/core/schemas/sealed-run-record.js +215 -0
  182. package/dist/core/schemas/sensitivity-witness.d.ts +138 -0
  183. package/dist/core/schemas/sensitivity-witness.js +86 -0
  184. package/dist/core/schemas/verdict.d.ts +25 -0
  185. package/dist/core/schemas/verdict.js +20 -0
  186. package/dist/core/schemas/waiver.d.ts +17 -0
  187. package/dist/core/schemas/waiver.js +24 -0
  188. package/dist/core/seal/derived-reference.d.ts +36 -0
  189. package/dist/core/seal/derived-reference.js +383 -0
  190. package/dist/core/seal/direction-prose.d.ts +13 -0
  191. package/dist/core/seal/direction-prose.js +120 -0
  192. package/dist/core/seal/plan-index.d.ts +63 -0
  193. package/dist/core/seal/plan-index.js +155 -0
  194. package/dist/core/seal/scripting-audit.d.ts +15 -0
  195. package/dist/core/seal/scripting-audit.js +51 -0
  196. package/dist/core/seal/seal.d.ts +18 -0
  197. package/dist/core/seal/seal.js +155 -0
  198. package/dist/core/stage-contracts.d.ts +30 -0
  199. package/dist/core/stage-contracts.js +1 -0
  200. package/dist/index.d.ts +15 -0
  201. package/dist/index.js +22 -0
  202. package/dist/ports/clock-port.d.ts +13 -0
  203. package/dist/ports/clock-port.js +7 -0
  204. package/dist/ports/corpus-port.d.ts +16 -0
  205. package/dist/ports/corpus-port.js +7 -0
  206. package/dist/ports/environment-probe-port.d.ts +76 -0
  207. package/dist/ports/environment-probe-port.js +32 -0
  208. package/dist/ports/file-system-port.d.ts +27 -0
  209. package/dist/ports/file-system-port.js +11 -0
  210. package/dist/ports/port.d.ts +33 -0
  211. package/dist/ports/port.js +14 -0
  212. package/dist/testing/conformance.d.ts +88 -0
  213. package/dist/testing/conformance.js +281 -0
  214. package/dist/testing/index.d.ts +26 -0
  215. package/dist/testing/index.js +18 -0
  216. package/dist/testing/probe-conformance.d.ts +45 -0
  217. package/dist/testing/probe-conformance.js +186 -0
  218. package/package.json +130 -0
  219. package/schemas/artifact-reference.schema.json +64 -0
  220. package/schemas/eval-contract.schema.json +2145 -0
  221. package/schemas/evaluator-configuration.schema.json +199 -0
  222. package/schemas/evidence-artifact.schema.json +1229 -0
  223. package/schemas/isolation-manifest.schema.json +394 -0
  224. package/schemas/preflight-verdict.schema.json +114 -0
  225. package/schemas/private-artifact-manifest.schema.json +103 -0
  226. package/schemas/probe.schema.json +1151 -0
  227. package/schemas/rubric.schema.json +148 -0
  228. package/schemas/scoring-policy.schema.json +90 -0
  229. package/schemas/sealed-evaluator-brief.schema.json +268 -0
  230. 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
+ });