@gate-forge/core 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 +202 -0
- package/dist/baselines/adoption.d.ts +76 -0
- package/dist/baselines/adoption.d.ts.map +1 -0
- package/dist/baselines/adoption.js +163 -0
- package/dist/baselines/adoption.js.map +1 -0
- package/dist/baselines/index.d.ts +102 -0
- package/dist/baselines/index.d.ts.map +1 -0
- package/dist/baselines/index.js +186 -0
- package/dist/baselines/index.js.map +1 -0
- package/dist/canonical-json.d.ts +51 -0
- package/dist/canonical-json.d.ts.map +1 -0
- package/dist/canonical-json.js +112 -0
- package/dist/canonical-json.js.map +1 -0
- package/dist/classifier/bind.d.ts +64 -0
- package/dist/classifier/bind.d.ts.map +1 -0
- package/dist/classifier/bind.js +188 -0
- package/dist/classifier/bind.js.map +1 -0
- package/dist/classifier/classify.d.ts +154 -0
- package/dist/classifier/classify.d.ts.map +1 -0
- package/dist/classifier/classify.js +1207 -0
- package/dist/classifier/classify.js.map +1 -0
- package/dist/classifier/glob.d.ts +48 -0
- package/dist/classifier/glob.d.ts.map +1 -0
- package/dist/classifier/glob.js +104 -0
- package/dist/classifier/glob.js.map +1 -0
- package/dist/classifier/index.d.ts +10 -0
- package/dist/classifier/index.d.ts.map +1 -0
- package/dist/classifier/index.js +21 -0
- package/dist/classifier/index.js.map +1 -0
- package/dist/classifier/schema.d.ts +150 -0
- package/dist/classifier/schema.d.ts.map +1 -0
- package/dist/classifier/schema.js +139 -0
- package/dist/classifier/schema.js.map +1 -0
- package/dist/config/index.d.ts +259 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +523 -0
- package/dist/config/index.js.map +1 -0
- package/dist/fingerprints.d.ts +56 -0
- package/dist/fingerprints.d.ts.map +1 -0
- package/dist/fingerprints.js +59 -0
- package/dist/fingerprints.js.map +1 -0
- package/dist/graph/build.d.ts +16 -0
- package/dist/graph/build.d.ts.map +1 -0
- package/dist/graph/build.js +423 -0
- package/dist/graph/build.js.map +1 -0
- package/dist/graph/index.d.ts +12 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +10 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/schema.d.ts +448 -0
- package/dist/graph/schema.d.ts.map +1 -0
- package/dist/graph/schema.js +237 -0
- package/dist/graph/schema.js.map +1 -0
- package/dist/graph/symbols.d.ts +129 -0
- package/dist/graph/symbols.d.ts.map +1 -0
- package/dist/graph/symbols.js +177 -0
- package/dist/graph/symbols.js.map +1 -0
- package/dist/graph/util.d.ts +40 -0
- package/dist/graph/util.d.ts.map +1 -0
- package/dist/graph/util.js +41 -0
- package/dist/graph/util.js.map +1 -0
- package/dist/index.d.ts +683 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +563 -0
- package/dist/index.js.map +1 -0
- package/dist/mapping/resolve.d.ts +177 -0
- package/dist/mapping/resolve.d.ts.map +1 -0
- package/dist/mapping/resolve.js +523 -0
- package/dist/mapping/resolve.js.map +1 -0
- package/dist/policy/coverage.d.ts +79 -0
- package/dist/policy/coverage.d.ts.map +1 -0
- package/dist/policy/coverage.js +119 -0
- package/dist/policy/coverage.js.map +1 -0
- package/dist/policy/evaluate.d.ts +275 -0
- package/dist/policy/evaluate.d.ts.map +1 -0
- package/dist/policy/evaluate.js +382 -0
- package/dist/policy/evaluate.js.map +1 -0
- package/dist/policy/index.d.ts +15 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +14 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/trusted.d.ts +59 -0
- package/dist/policy/trusted.d.ts.map +1 -0
- package/dist/policy/trusted.js +96 -0
- package/dist/policy/trusted.js.map +1 -0
- package/dist/provenance.d.ts +170 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +306 -0
- package/dist/provenance.js.map +1 -0
- package/dist/receipt/index.d.ts +74 -0
- package/dist/receipt/index.d.ts.map +1 -0
- package/dist/receipt/index.js +154 -0
- package/dist/receipt/index.js.map +1 -0
- package/dist/report/index.d.ts +100 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +355 -0
- package/dist/report/index.js.map +1 -0
- package/dist/schemas/adoption.d.ts +40 -0
- package/dist/schemas/adoption.d.ts.map +1 -0
- package/dist/schemas/adoption.js +87 -0
- package/dist/schemas/adoption.js.map +1 -0
- package/dist/schemas/baseline.d.ts +19 -0
- package/dist/schemas/baseline.d.ts.map +1 -0
- package/dist/schemas/baseline.js +45 -0
- package/dist/schemas/baseline.js.map +1 -0
- package/dist/schemas/claim.d.ts +32 -0
- package/dist/schemas/claim.d.ts.map +1 -0
- package/dist/schemas/claim.js +34 -0
- package/dist/schemas/claim.js.map +1 -0
- package/dist/schemas/classification-policy.d.ts +84 -0
- package/dist/schemas/classification-policy.d.ts.map +1 -0
- package/dist/schemas/classification-policy.js +130 -0
- package/dist/schemas/classification-policy.js.map +1 -0
- package/dist/schemas/classification-signal.d.ts +125 -0
- package/dist/schemas/classification-signal.d.ts.map +1 -0
- package/dist/schemas/classification-signal.js +126 -0
- package/dist/schemas/classification-signal.js.map +1 -0
- package/dist/schemas/classification.d.ts +120 -0
- package/dist/schemas/classification.d.ts.map +1 -0
- package/dist/schemas/classification.js +154 -0
- package/dist/schemas/classification.js.map +1 -0
- package/dist/schemas/common.d.ts +73 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +60 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/coverage-policy.d.ts +89 -0
- package/dist/schemas/coverage-policy.d.ts.map +1 -0
- package/dist/schemas/coverage-policy.js +81 -0
- package/dist/schemas/coverage-policy.js.map +1 -0
- package/dist/schemas/evidence.d.ts +65 -0
- package/dist/schemas/evidence.d.ts.map +1 -0
- package/dist/schemas/evidence.js +64 -0
- package/dist/schemas/evidence.js.map +1 -0
- package/dist/schemas/execution-result.d.ts +214 -0
- package/dist/schemas/execution-result.d.ts.map +1 -0
- package/dist/schemas/execution-result.js +240 -0
- package/dist/schemas/execution-result.js.map +1 -0
- package/dist/schemas/gate-receipt.d.ts +86 -0
- package/dist/schemas/gate-receipt.d.ts.map +1 -0
- package/dist/schemas/gate-receipt.js +193 -0
- package/dist/schemas/gate-receipt.js.map +1 -0
- package/dist/schemas/index.d.ts +22 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +22 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/obligation.d.ts +37 -0
- package/dist/schemas/obligation.d.ts.map +1 -0
- package/dist/schemas/obligation.js +46 -0
- package/dist/schemas/obligation.js.map +1 -0
- package/dist/schemas/plugin.d.ts +19 -0
- package/dist/schemas/plugin.d.ts.map +1 -0
- package/dist/schemas/plugin.js +20 -0
- package/dist/schemas/plugin.js.map +1 -0
- package/dist/schemas/policy.d.ts +73 -0
- package/dist/schemas/policy.d.ts.map +1 -0
- package/dist/schemas/policy.js +46 -0
- package/dist/schemas/policy.js.map +1 -0
- package/dist/schemas/resource.d.ts +26 -0
- package/dist/schemas/resource.d.ts.map +1 -0
- package/dist/schemas/resource.js +29 -0
- package/dist/schemas/resource.js.map +1 -0
- package/dist/schemas/run-manifest.d.ts +72 -0
- package/dist/schemas/run-manifest.d.ts.map +1 -0
- package/dist/schemas/run-manifest.js +125 -0
- package/dist/schemas/run-manifest.js.map +1 -0
- package/dist/schemas/runner-adapter.d.ts +169 -0
- package/dist/schemas/runner-adapter.d.ts.map +1 -0
- package/dist/schemas/runner-adapter.js +2 -0
- package/dist/schemas/runner-adapter.js.map +1 -0
- package/dist/schemas/test-catalog.d.ts +472 -0
- package/dist/schemas/test-catalog.d.ts.map +1 -0
- package/dist/schemas/test-catalog.js +311 -0
- package/dist/schemas/test-catalog.js.map +1 -0
- package/dist/schemas/test-map.d.ts +99 -0
- package/dist/schemas/test-map.d.ts.map +1 -0
- package/dist/schemas/test-map.js +103 -0
- package/dist/schemas/test-map.js.map +1 -0
- package/dist/schemas/verdict.d.ts +73 -0
- package/dist/schemas/verdict.d.ts.map +1 -0
- package/dist/schemas/verdict.js +92 -0
- package/dist/schemas/verdict.js.map +1 -0
- package/dist/schemas/waiver.d.ts +36 -0
- package/dist/schemas/waiver.d.ts.map +1 -0
- package/dist/schemas/waiver.js +42 -0
- package/dist/schemas/waiver.js.map +1 -0
- package/dist/supervision/index.d.ts +170 -0
- package/dist/supervision/index.d.ts.map +1 -0
- package/dist/supervision/index.js +354 -0
- package/dist/supervision/index.js.map +1 -0
- package/dist/testing/clock.d.ts +50 -0
- package/dist/testing/clock.d.ts.map +1 -0
- package/dist/testing/clock.js +75 -0
- package/dist/testing/clock.js.map +1 -0
- package/dist/testing/env.d.ts +66 -0
- package/dist/testing/env.d.ts.map +1 -0
- package/dist/testing/env.js +106 -0
- package/dist/testing/env.js.map +1 -0
- package/dist/testing/gate-runner.d.ts +141 -0
- package/dist/testing/gate-runner.d.ts.map +1 -0
- package/dist/testing/gate-runner.js +173 -0
- package/dist/testing/gate-runner.js.map +1 -0
- package/dist/testing/gf19.d.ts +106 -0
- package/dist/testing/gf19.d.ts.map +1 -0
- package/dist/testing/gf19.js +282 -0
- package/dist/testing/gf19.js.map +1 -0
- package/dist/testing/index.d.ts +20 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +14 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/red-probe.d.ts +138 -0
- package/dist/testing/red-probe.d.ts.map +1 -0
- package/dist/testing/red-probe.js +251 -0
- package/dist/testing/red-probe.js.map +1 -0
- package/dist/testing/temp-repo.d.ts +126 -0
- package/dist/testing/temp-repo.d.ts.map +1 -0
- package/dist/testing/temp-repo.js +214 -0
- package/dist/testing/temp-repo.js.map +1 -0
- package/dist/verdict/cause.d.ts +99 -0
- package/dist/verdict/cause.d.ts.map +1 -0
- package/dist/verdict/cause.js +206 -0
- package/dist/verdict/cause.js.map +1 -0
- package/dist/verdict/evaluate.d.ts +135 -0
- package/dist/verdict/evaluate.d.ts.map +1 -0
- package/dist/verdict/evaluate.js +1513 -0
- package/dist/verdict/evaluate.js.map +1 -0
- package/dist/verdict/index.d.ts +21 -0
- package/dist/verdict/index.d.ts.map +1 -0
- package/dist/verdict/index.js +20 -0
- package/dist/verdict/index.js.map +1 -0
- package/dist/verdict/pack-verifiers.d.ts +97 -0
- package/dist/verdict/pack-verifiers.d.ts.map +1 -0
- package/dist/verdict/pack-verifiers.js +763 -0
- package/dist/verdict/pack-verifiers.js.map +1 -0
- package/dist/verdict/registry.d.ts +180 -0
- package/dist/verdict/registry.d.ts.map +1 -0
- package/dist/verdict/registry.js +67 -0
- package/dist/verdict/registry.js.map +1 -0
- package/dist/waivers/index.d.ts +103 -0
- package/dist/waivers/index.d.ts.map +1 -0
- package/dist/waivers/index.js +192 -0
- package/dist/waivers/index.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in semantic verifiers for the pack contract namespaces (ADR 0004
|
|
3
|
+
* D8, plan phase 5). Registered once at module init; the registry rejects
|
|
4
|
+
* re-registration, so no pack can override another namespace.
|
|
5
|
+
*
|
|
6
|
+
* Trust model (invariant, ADR 0001): ONLY witnessed records satisfy.
|
|
7
|
+
* Suite-submitted records are claimed-tier at issuance. Fabricated
|
|
8
|
+
* provenance is rejected before verifiers run (the engine re-computes
|
|
9
|
+
* record hashes).
|
|
10
|
+
*
|
|
11
|
+
* HTTP namespace (plan §8 / D1, honest transport semantics):
|
|
12
|
+
* `http:frontend-request-observed` remains REGISTERED BUT UNAVAILABLE:
|
|
13
|
+
* the supervised session channel (plan Phase 1) now attributes exchanges
|
|
14
|
+
* to a test session's dedicated proxy port inside witness-kept action
|
|
15
|
+
* intervals, but that attribution is by ORIGIN, not by browser — any
|
|
16
|
+
* process holding the session credential can send traffic through the
|
|
17
|
+
* port — so it still does not prove WHICH browser (or that any browser)
|
|
18
|
+
* produced an exchange, and enabling the contract on it would silently
|
|
19
|
+
* change its meaning (a Node-side direct request through the session
|
|
20
|
+
* port would satisfy "frontend request observed"). It always grades
|
|
21
|
+
* blocking `missing` before any evidence is examined, naming this gap.
|
|
22
|
+
* `http:request-observed` requires a
|
|
23
|
+
* witness-observed `http.request` exchange in the bound run PLUS a
|
|
24
|
+
* provenanced claimed `ui.action` anchor from the declaring test; a
|
|
25
|
+
* suite-submitted network record can never satisfy
|
|
26
|
+
* (`HTTP_OBSERVATION_UNTRUSTED`). `http:response-status-ok` is the same
|
|
27
|
+
* transport proof additionally requiring a 2xx status. A witness-observed
|
|
28
|
+
* exchange proves only that the witness observed an HTTP exchange —
|
|
29
|
+
* test attribution is suite-claimed, never independently verified.
|
|
30
|
+
*
|
|
31
|
+
* Domain namespaces (`auth:*`, `task:*`, `validation:*`, `webhook:*`,
|
|
32
|
+
* `workflow:*`): FAIL-CLOSED, unconditionally, for EVERY contract of the
|
|
33
|
+
* namespace. Proving these behaviors requires an engine-owned observer
|
|
34
|
+
* over application state — audit logs, FSM/state observation,
|
|
35
|
+
* identity/role material (plan §6) — and no such producer exists yet.
|
|
36
|
+
* Grading them from a witnessed check record whose witness-derived
|
|
37
|
+
* outcome is merely the observed HTTP status class (2xx → accepted, 4xx
|
|
38
|
+
* → rejected) is forged green: any 2xx would "prove" `audit-emitted` or
|
|
39
|
+
* `persisted-final-state`, any 4xx would "prove" `tenant-isolated` or
|
|
40
|
+
* `denied-no-side-effect`, and a response hash proves nothing about
|
|
41
|
+
* `error-message-explicit`. Every contract of these namespaces therefore
|
|
42
|
+
* grades `missing` with a reason naming the missing channel — never
|
|
43
|
+
* `satisfied`, never `invalid`, whatever evidence arrives.
|
|
44
|
+
*/
|
|
45
|
+
import { isProvenancedRecord } from '../provenance.js';
|
|
46
|
+
import { compareStrings } from '../graph/util.js';
|
|
47
|
+
/**
|
|
48
|
+
* Stable typed code for untrusted runtime observations. Mirrors
|
|
49
|
+
* `HTTP_OBSERVATION_UNTRUSTED` in `@gate-forge/http-contract` (core
|
|
50
|
+
* cannot depend on it); keep the two in lockstep.
|
|
51
|
+
*/
|
|
52
|
+
const HTTP_OBSERVATION_UNTRUSTED = 'HTTP_OBSERVATION_UNTRUSTED';
|
|
53
|
+
import { registerContractCapabilities, registerContractVerifier, } from './registry.js';
|
|
54
|
+
/** Record kinds the witness and suites exchange. */
|
|
55
|
+
const UI_ACTION_KIND = 'ui.action';
|
|
56
|
+
const HTTP_REQUEST_KIND = 'http.request';
|
|
57
|
+
/**
|
|
58
|
+
* The only HTTP contracts the verifier knows (plan §7): a namespace
|
|
59
|
+
* registration never implies every operation in it. Any other `http:*`
|
|
60
|
+
* name fails closed through {@link unknownContract} before anchor or
|
|
61
|
+
* record checks run.
|
|
62
|
+
*/
|
|
63
|
+
const SUPPORTED_HTTP_CONTRACTS = [
|
|
64
|
+
'http:frontend-request-observed',
|
|
65
|
+
'http:request-observed',
|
|
66
|
+
'http:response-status-ok',
|
|
67
|
+
];
|
|
68
|
+
function payloadOf(record) {
|
|
69
|
+
const payload = record.payload;
|
|
70
|
+
return payload !== null && typeof payload === 'object' ? payload : null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The provenanced claimed `ui.action` anchor: a suite assertion that the
|
|
74
|
+
* declaring test drove the UI (hash-verified issuance only — the hash
|
|
75
|
+
* proves the witness issued the anchor record, not that a UI action
|
|
76
|
+
* occurred). Satisfaction weight lives in the witnessed records; test
|
|
77
|
+
* attribution throughout is suite-claimed.
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* Blocking reason for `http:frontend-request-observed` (plan §8 / D1):
|
|
81
|
+
* returned BEFORE anchor or record examination. DECISION (Phase 1
|
|
82
|
+
* review, 2026-09-13): the supervised session channel provides
|
|
83
|
+
* browser-SESSION-bound exchange observation (dedicated proxy port,
|
|
84
|
+
* witness-kept action intervals), but the attribution is by port
|
|
85
|
+
* ORIGIN, not by browser — a hostile test retains its own process and
|
|
86
|
+
* can drive its session port from Node inside an interval — so enabling
|
|
87
|
+
* the contract on that channel would silently change its meaning. It
|
|
88
|
+
* stays unavailable; the UI-semantic `crud:*` contracts carry the
|
|
89
|
+
* session-channel proof instead, backed by the persistence echo that a
|
|
90
|
+
* bare transport claim has no equivalent of.
|
|
91
|
+
*/
|
|
92
|
+
function frontendProofUnavailable(input) {
|
|
93
|
+
return {
|
|
94
|
+
status: 'missing',
|
|
95
|
+
reason: `'${input.obligation.id}': contract 'http:frontend-request-observed' has no independent ` +
|
|
96
|
+
'browser/test observation channel: the witness observes HTTP exchanges but cannot prove ' +
|
|
97
|
+
'which browser, UI action, or test produced an exchange; test attribution is suite-claimed. ' +
|
|
98
|
+
'The supervised session channel (plan Phase 1) attributes exchanges to a test session\'s ' +
|
|
99
|
+
'dedicated proxy port inside witness-kept action intervals, but that attribution is by ' +
|
|
100
|
+
'ORIGIN, not by browser — any process holding the session credential can send traffic ' +
|
|
101
|
+
'through the port — so enabling this contract on it would silently change its meaning. ' +
|
|
102
|
+
'Use the UI-semantic crud contracts for browser proof over the session channel, or the ' +
|
|
103
|
+
"narrower transport contract 'http:request-observed' when a witness-observed " +
|
|
104
|
+
'HTTP exchange suffices',
|
|
105
|
+
recordIds: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Deduplicates string ids into a codepoint-sorted array (local: the
|
|
110
|
+
* sibling helper in `evaluate.ts` is not importable here without
|
|
111
|
+
* widening module coupling; kept in lockstep semantics).
|
|
112
|
+
*
|
|
113
|
+
* Args:
|
|
114
|
+
* ids: candidate record ids (empty entries dropped).
|
|
115
|
+
*
|
|
116
|
+
* Returns:
|
|
117
|
+
* string[]: sorted unique ids.
|
|
118
|
+
*/
|
|
119
|
+
function sortedUniqueIds(ids) {
|
|
120
|
+
const seen = {};
|
|
121
|
+
const unique = [];
|
|
122
|
+
for (const id of ids) {
|
|
123
|
+
if (id.length === 0 || id in seen)
|
|
124
|
+
continue;
|
|
125
|
+
seen[id] = true;
|
|
126
|
+
unique.push(id);
|
|
127
|
+
}
|
|
128
|
+
return unique.sort(compareStrings);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Validates the required suite anchor once and selects it
|
|
132
|
+
* deterministically: when several provenanced anchors qualify, the
|
|
133
|
+
* codepoint-smallest record id wins so input order never matters.
|
|
134
|
+
*
|
|
135
|
+
* Args:
|
|
136
|
+
* input: the claim plus its attributed evidence and obligation.
|
|
137
|
+
*
|
|
138
|
+
* Returns:
|
|
139
|
+
* `{ok: true, anchorId}` with the selected anchor, or `{ok: false,
|
|
140
|
+
* outcome}` with the blocking missing result (wording unchanged).
|
|
141
|
+
*/
|
|
142
|
+
function selectTransportAnchor(input) {
|
|
143
|
+
const actions = input.evidence.filter((entry) => entry.record.kind === UI_ACTION_KIND);
|
|
144
|
+
const provenanced = actions.filter((entry) => isProvenancedRecord(entry.record));
|
|
145
|
+
if (provenanced.length === 0) {
|
|
146
|
+
const detail = actions.length === 0
|
|
147
|
+
? `no '${UI_ACTION_KIND}' anchor from the declaring test`
|
|
148
|
+
: `'${UI_ACTION_KIND}' records exist but none verifies its provenance`;
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
outcome: {
|
|
152
|
+
status: 'missing',
|
|
153
|
+
reason: `'${input.obligation.id}': ${detail}`,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const ids = provenanced.map((entry) => String(entry.record.recordId));
|
|
158
|
+
const selected = sortedUniqueIds(ids)[0];
|
|
159
|
+
return { ok: true, anchorId: selected };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Concrete methods a route candidate or observation may carry. `ANY`
|
|
163
|
+
* (exposure-only evidence) is deliberately absent: an unknown/dynamic
|
|
164
|
+
* method can never establish uniqueness (plan §9 step 9).
|
|
165
|
+
*/
|
|
166
|
+
const CONCRETE_HTTP_METHODS = new Set([
|
|
167
|
+
'GET',
|
|
168
|
+
'HEAD',
|
|
169
|
+
'POST',
|
|
170
|
+
'PUT',
|
|
171
|
+
'PATCH',
|
|
172
|
+
'DELETE',
|
|
173
|
+
'OPTIONS',
|
|
174
|
+
]);
|
|
175
|
+
/**
|
|
176
|
+
* Single deterministic path interpretation for runtime observations
|
|
177
|
+
* (plan §9 steps 1-3). Conservative and lockstep with the witness
|
|
178
|
+
* proxy storage (`normalizeObservedPath` in
|
|
179
|
+
* `@gate-forge/pack-playwright`'s witness/server.ts — same rules, no
|
|
180
|
+
* collapsing, no decoding on either side):
|
|
181
|
+
* - query (`?...`) and fragment (`#...`) are stripped;
|
|
182
|
+
* - one leading slash is required (a missing one is added);
|
|
183
|
+
* - trailing slashes are dropped (root `/` stays `/`) — routers treat
|
|
184
|
+
* these as the same resource;
|
|
185
|
+
* - case and segment boundaries are PRESERVED: duplicate slashes are
|
|
186
|
+
* NOT collapsed (collapsing would let `//` masquerade across segment
|
|
187
|
+
* boundaries), and percent-encodings are NEVER decoded (`%2F` stays a
|
|
188
|
+
* literal part of its segment, never a separator).
|
|
189
|
+
*
|
|
190
|
+
* Noncanonical input whose routing meaning is uncertain (duplicate
|
|
191
|
+
* slashes, an encoded slash, a non-path-absolute value) does NOT match
|
|
192
|
+
* a different endpoint — it fails with a reason so the verifier blocks
|
|
193
|
+
* instead of substituting a route.
|
|
194
|
+
*
|
|
195
|
+
* Local on purpose: core must not depend on `@gate-forge/http-contract`.
|
|
196
|
+
*
|
|
197
|
+
* Args:
|
|
198
|
+
* rawUrl: the observed URL carried by the witnessed record.
|
|
199
|
+
*
|
|
200
|
+
* Returns:
|
|
201
|
+
* `{ok: true, path}` with the interpreted path, or `{ok: false,
|
|
202
|
+
* reason}` naming the noncanonical input.
|
|
203
|
+
*/
|
|
204
|
+
export function interpretObservedPath(rawUrl) {
|
|
205
|
+
if (typeof rawUrl !== 'string' || rawUrl.length === 0) {
|
|
206
|
+
return { ok: false, reason: 'observed url is empty or not a string' };
|
|
207
|
+
}
|
|
208
|
+
let path = rawUrl.split('?')[0]?.split('#')[0] ?? '/';
|
|
209
|
+
if (!path.startsWith('/'))
|
|
210
|
+
path = `/${path}`;
|
|
211
|
+
if (path.includes('//')) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
reason: `observed path '${rawUrl}' contains a duplicate slash whose routing meaning is uncertain; it cannot attribute any endpoint`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (/%2f/i.test(path)) {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
reason: `observed path '${rawUrl}' contains an encoded slash that is never decoded into a separator; it cannot attribute any endpoint`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (path.length > 1)
|
|
224
|
+
path = path.replace(/\/+$/, '');
|
|
225
|
+
if (path.length === 0)
|
|
226
|
+
path = '/';
|
|
227
|
+
return { ok: true, path };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Whether a candidate's canonical path is a supported route shape for
|
|
231
|
+
* runtime attribution (plan §9 steps 4/9): path-absolute, no duplicate
|
|
232
|
+
* slashes, no `..` escape segments, and at most one `{*}` wildcard in
|
|
233
|
+
* trailing position. Anything else (notably a non-trailing wildcard,
|
|
234
|
+
* which the positional matcher can never match) makes the inventory
|
|
235
|
+
* incomplete — uniqueness cannot be established against it.
|
|
236
|
+
*
|
|
237
|
+
* Args:
|
|
238
|
+
* canonicalPath: the candidate's compiled canonical path.
|
|
239
|
+
*
|
|
240
|
+
* Returns:
|
|
241
|
+
* boolean: true only for attributable shapes.
|
|
242
|
+
*/
|
|
243
|
+
function isSupportedRouteShape(canonicalPath) {
|
|
244
|
+
if (typeof canonicalPath !== 'string' || !canonicalPath.startsWith('/'))
|
|
245
|
+
return false;
|
|
246
|
+
if (canonicalPath.includes('//'))
|
|
247
|
+
return false;
|
|
248
|
+
const segments = canonicalPath.split('/').filter((segment) => segment.length > 0);
|
|
249
|
+
let wildcards = 0;
|
|
250
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
251
|
+
const segment = segments[index] ?? '';
|
|
252
|
+
if (segment === '{*}') {
|
|
253
|
+
wildcards += 1;
|
|
254
|
+
if (index !== segments.length - 1)
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
else if (segment === '..') {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return wildcards <= 1;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Whether a candidate carries a concrete attributable method (plan §9
|
|
265
|
+
* step 9): uppercase and in the concrete set. `ANY`, lowercase, empty,
|
|
266
|
+
* and unknown methods make the inventory incomplete.
|
|
267
|
+
*/
|
|
268
|
+
function isConcreteRouteMethod(method) {
|
|
269
|
+
return typeof method === 'string' && method.length > 0 && CONCRETE_HTTP_METHODS.has(method);
|
|
270
|
+
}
|
|
271
|
+
/** Canonical identity text for one candidate (`METHOD path (resourceId)`). */
|
|
272
|
+
function candidateIdentityText(candidate) {
|
|
273
|
+
return `${candidate.method} ${candidate.canonicalPath} (${candidate.resourceId})`;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Deterministic runtime route attribution over the COMPLETE candidate
|
|
277
|
+
* set (plan §9 steps 4-9, D2). No literal-precedence shortcut: when
|
|
278
|
+
* both `/accounts/export` and `/accounts/{}` match the observation,
|
|
279
|
+
* the transport status is known but handler attribution is ambiguous
|
|
280
|
+
* and the claim blocks.
|
|
281
|
+
*
|
|
282
|
+
* Args:
|
|
283
|
+
* observedMethod: the witnessed record's method (any case).
|
|
284
|
+
* observedPath: the interpreted observed path (from
|
|
285
|
+
* `interpretObservedPath`).
|
|
286
|
+
* candidates: the complete host-derived route inventory.
|
|
287
|
+
* obligationResourceId: the obligation's own resource id.
|
|
288
|
+
*
|
|
289
|
+
* Returns:
|
|
290
|
+
* - `{status: 'incomplete', reason}` when any candidate carries an
|
|
291
|
+
* unknown/dynamic method or an unsupported shape (uniqueness
|
|
292
|
+
* cannot be established);
|
|
293
|
+
* - `{status: 'nomatch', reason}` when zero candidates match;
|
|
294
|
+
* - `{status: 'ambiguous', candidates}` with the sorted identity
|
|
295
|
+
* texts when more than one distinct resource matches;
|
|
296
|
+
* - `{status: 'mismatch', matched}` when exactly one candidate
|
|
297
|
+
* matches but it is a different endpoint;
|
|
298
|
+
* - `{status: 'match', matched}` when the unique match is the
|
|
299
|
+
* obligation's own endpoint.
|
|
300
|
+
*/
|
|
301
|
+
export function resolveHttpRoute(observedMethod, observedPath, candidates, obligationResourceId) {
|
|
302
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
303
|
+
const candidate = candidates[index];
|
|
304
|
+
if (candidate === undefined ||
|
|
305
|
+
typeof candidate.resourceId !== 'string' ||
|
|
306
|
+
candidate.resourceId.length === 0 ||
|
|
307
|
+
!isConcreteRouteMethod(candidate.method) ||
|
|
308
|
+
!isSupportedRouteShape(candidate.canonicalPath)) {
|
|
309
|
+
return {
|
|
310
|
+
status: 'incomplete',
|
|
311
|
+
reason: `route inventory entry ${index} is not attributable ` +
|
|
312
|
+
`(unknown/dynamic method or unsupported shape); uniqueness cannot be established ` +
|
|
313
|
+
`against an incomplete inventory, so '${obligationResourceId}' stays blocking`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const upperMethod = observedMethod.toUpperCase();
|
|
318
|
+
const matched = candidates.filter((candidate) => candidate.method === upperMethod && pathMatchesShape(observedPath, candidate.canonicalPath));
|
|
319
|
+
// Deduplicate repeated facts for the same resource identity only —
|
|
320
|
+
// never merge different resource IDs or planes.
|
|
321
|
+
const seen = {};
|
|
322
|
+
const distinct = [];
|
|
323
|
+
for (const candidate of matched) {
|
|
324
|
+
if (candidate.resourceId in seen)
|
|
325
|
+
continue;
|
|
326
|
+
seen[candidate.resourceId] = true;
|
|
327
|
+
distinct.push(candidate);
|
|
328
|
+
}
|
|
329
|
+
if (distinct.length === 0) {
|
|
330
|
+
return {
|
|
331
|
+
status: 'nomatch',
|
|
332
|
+
reason: `observed ${upperMethod} ${observedPath} matches none of the ` +
|
|
333
|
+
`${candidates.length} inventoried routes; evidence from a different endpoint can never ` +
|
|
334
|
+
`satisfy '${obligationResourceId}'`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (distinct.length > 1) {
|
|
338
|
+
return {
|
|
339
|
+
status: 'ambiguous',
|
|
340
|
+
candidates: distinct.map(candidateIdentityText).sort(compareStrings),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const only = distinct[0];
|
|
344
|
+
if (only.resourceId !== obligationResourceId) {
|
|
345
|
+
return { status: 'mismatch', matched: only };
|
|
346
|
+
}
|
|
347
|
+
return { status: 'match', matched: only };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Positional match of a concrete observed path against a compiled
|
|
351
|
+
* canonical shape (ADR 0004 D2/D3 semantics): literal segments must be
|
|
352
|
+
* equal, `{}` matches any single non-empty segment, and a TRAILING `{*}`
|
|
353
|
+
* matches one or more trailing segments. Non-trailing wildcards and any
|
|
354
|
+
* other shape never match. Case-sensitive.
|
|
355
|
+
*
|
|
356
|
+
* Args:
|
|
357
|
+
* observedPath: the concrete observed path (query already stripped).
|
|
358
|
+
* canonicalPath: the endpoint's compiled canonical shape.
|
|
359
|
+
*
|
|
360
|
+
* Returns:
|
|
361
|
+
* boolean: true only when the observed path instantiates the shape.
|
|
362
|
+
*/
|
|
363
|
+
export function pathMatchesShape(observedPath, canonicalPath) {
|
|
364
|
+
const observed = observedPath.split('/').filter((segment) => segment.length > 0);
|
|
365
|
+
const shape = canonicalPath.split('/').filter((segment) => segment.length > 0);
|
|
366
|
+
const wildcardIndex = shape.indexOf('{*}');
|
|
367
|
+
// Fail closed: only a TRAILING `{*}` is a wildcard — a shape that
|
|
368
|
+
// carries one anywhere else (or more than once) never matches.
|
|
369
|
+
if (wildcardIndex !== -1 && wildcardIndex !== shape.length - 1)
|
|
370
|
+
return false;
|
|
371
|
+
if (wildcardIndex !== -1) {
|
|
372
|
+
// The wildcard consumes one or more trailing segments, so the
|
|
373
|
+
// observed path needs at least the shape's leading literals.
|
|
374
|
+
if (observed.length < shape.length)
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
else if (observed.length !== shape.length) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
const literalPositions = wildcardIndex === -1 ? shape.length : wildcardIndex;
|
|
381
|
+
for (let i = 0; i < literalPositions; i++) {
|
|
382
|
+
const pattern = shape[i];
|
|
383
|
+
// `{}` matches any single (non-empty — empties were dropped)
|
|
384
|
+
// segment; anything else must be literally equal. Case-sensitive.
|
|
385
|
+
if (pattern !== '{}' && pattern !== observed[i])
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Grades the explicit transport contracts (`http:request-observed`,
|
|
392
|
+
* `http:response-status-ok`): one shared path, no divergent verifier.
|
|
393
|
+
* Proves only that the witness observed an HTTP exchange in the bound
|
|
394
|
+
* run; test attribution is suite-claimed.
|
|
395
|
+
*
|
|
396
|
+
* Args:
|
|
397
|
+
* input: the claim plus its attributed evidence and obligation.
|
|
398
|
+
*
|
|
399
|
+
* Returns:
|
|
400
|
+
* ClaimOutcome: satisfied only for a witnessed engine-observed
|
|
401
|
+
* exchange matching the endpoint shape (plus 2xx for status-ok).
|
|
402
|
+
*/
|
|
403
|
+
/**
|
|
404
|
+
* Scans the route inventory once for a blocking incomplete reason
|
|
405
|
+
* (plan §10 step 2): a null context or any unattributable entry makes
|
|
406
|
+
* every eligible record `missing`, never satisfied. Computed once so
|
|
407
|
+
* per-record grading stays a pure function of the record.
|
|
408
|
+
*
|
|
409
|
+
* Args:
|
|
410
|
+
* input: the claim plus its attributed evidence and obligation.
|
|
411
|
+
*
|
|
412
|
+
* Returns:
|
|
413
|
+
* string | null: the blocking reason, or null when the inventory is
|
|
414
|
+
* usable for attribution.
|
|
415
|
+
*/
|
|
416
|
+
function transportInventoryBlock(input) {
|
|
417
|
+
const candidates = input.httpRoutes;
|
|
418
|
+
if (candidates === null || candidates === undefined) {
|
|
419
|
+
return (`'${input.obligation.id}': no route inventory context for ` +
|
|
420
|
+
`'${input.obligation.contract}'; HTTP satisfaction requires the complete host-derived ` +
|
|
421
|
+
'route inventory (every applicable http.endpoint resource, including routes with no ' +
|
|
422
|
+
'consumer and no obligation) — without it no endpoint-specific pass is authoritative, ' +
|
|
423
|
+
`so '${input.obligation.id}' stays blocking`);
|
|
424
|
+
}
|
|
425
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
426
|
+
const candidate = candidates[index];
|
|
427
|
+
if (candidate === undefined ||
|
|
428
|
+
typeof candidate.resourceId !== 'string' ||
|
|
429
|
+
candidate.resourceId.length === 0 ||
|
|
430
|
+
!isConcreteRouteMethod(candidate.method) ||
|
|
431
|
+
!isSupportedRouteShape(candidate.canonicalPath)) {
|
|
432
|
+
return (`'${input.obligation.id}': route inventory entry ${index} is not attributable ` +
|
|
433
|
+
`(unknown/dynamic method or unsupported shape); uniqueness cannot be established ` +
|
|
434
|
+
`against an incomplete inventory, so '${input.obligation.resourceId}' stays blocking`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Grades ONE `http.request` record independently for origin, trust,
|
|
441
|
+
* provenance, payload, route identity, and required status (plan §10
|
|
442
|
+
* step 3). Never returns from the outer verifier; the caller
|
|
443
|
+
* aggregates every grade. All reason wordings are verbatim from the
|
|
444
|
+
* single-record grader.
|
|
445
|
+
*
|
|
446
|
+
* Args:
|
|
447
|
+
* record: the lenient record view to grade.
|
|
448
|
+
* trust: the engine-derived trust tier for the record.
|
|
449
|
+
* input: the claim plus its obligation (for ids/contract/status rule).
|
|
450
|
+
* inventoryBlock: the precomputed inventory reason, or null when usable.
|
|
451
|
+
* candidates: the usable route inventory (ignored when inventoryBlock
|
|
452
|
+
* is set).
|
|
453
|
+
*
|
|
454
|
+
* Returns:
|
|
455
|
+
* TransportRecordGrade: the independent grade for this record.
|
|
456
|
+
*/
|
|
457
|
+
function gradeTransportRecord(record, trust, input, inventoryBlock, candidates) {
|
|
458
|
+
const label = String(record.recordId);
|
|
459
|
+
if (record.origin !== 'engine-observed') {
|
|
460
|
+
return {
|
|
461
|
+
status: 'invalid',
|
|
462
|
+
reason: `'${input.obligation.id}': suite-submitted network record ` +
|
|
463
|
+
`'${label}' cannot satisfy an HTTP runtime contract ` +
|
|
464
|
+
`(${HTTP_OBSERVATION_UNTRUSTED}); only a witness-observed HTTP exchange proves ` +
|
|
465
|
+
'transport, and test attribution is suite-claimed',
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
if (trust !== 'witnessed' || !isProvenancedRecord(record)) {
|
|
469
|
+
return {
|
|
470
|
+
status: 'missing',
|
|
471
|
+
reason: `'${input.obligation.id}': observed requests exist but none carries witnessed ` +
|
|
472
|
+
'provenance bound to this run',
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const payload = payloadOf(record);
|
|
476
|
+
if (payload === null || typeof payload['method'] !== 'string' || typeof payload['url'] !== 'string') {
|
|
477
|
+
return {
|
|
478
|
+
status: 'invalid',
|
|
479
|
+
reason: `'${input.obligation.id}': witnessed '${HTTP_REQUEST_KIND}' record ` +
|
|
480
|
+
`'${label}' carries no method/url pair`,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (inventoryBlock !== null) {
|
|
484
|
+
return { status: 'missing', reason: inventoryBlock };
|
|
485
|
+
}
|
|
486
|
+
const observedMethod = payload['method'];
|
|
487
|
+
const interpreted = interpretObservedPath(payload['url']);
|
|
488
|
+
if (interpreted.ok === false) {
|
|
489
|
+
return {
|
|
490
|
+
status: 'invalid',
|
|
491
|
+
reason: `'${input.obligation.id}': witnessed '${HTTP_REQUEST_KIND}' record ` +
|
|
492
|
+
`'${label}' carries a noncanonical observed path: ` +
|
|
493
|
+
`${interpreted.reason}`,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
// Identity match (plan §9, D2 — fail closed, no any-endpoint
|
|
497
|
+
// fallback): the witnessed observation must attribute to EXACTLY the
|
|
498
|
+
// obligation's endpoint within the host-derived COMPLETE route
|
|
499
|
+
// inventory. Missing/incomplete context blocks satisfaction: without
|
|
500
|
+
// every applicable `http.endpoint` resource a literal-vs-parameter
|
|
501
|
+
// overlap (or an unconsumed sibling) could silently steal credit.
|
|
502
|
+
const resolution = resolveHttpRoute(observedMethod, interpreted.path, candidates, input.obligation.resourceId);
|
|
503
|
+
if (resolution.status === 'incomplete') {
|
|
504
|
+
return {
|
|
505
|
+
status: 'missing',
|
|
506
|
+
reason: `'${input.obligation.id}': ${resolution.reason}`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
if (resolution.status === 'nomatch') {
|
|
510
|
+
return {
|
|
511
|
+
status: 'invalid',
|
|
512
|
+
reason: `'${input.obligation.id}': witnessed '${HTTP_REQUEST_KIND}' record ` +
|
|
513
|
+
`'${label}' ${resolution.reason}`,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (resolution.status === 'ambiguous') {
|
|
517
|
+
return {
|
|
518
|
+
status: 'invalid',
|
|
519
|
+
reason: `'${input.obligation.id}': ambiguous route attribution: observed ` +
|
|
520
|
+
`${observedMethod.toUpperCase()} ${interpreted.path} matches ${resolution.candidates.length} ` +
|
|
521
|
+
`distinct routes [${resolution.candidates.join('; ')}]; the transport status is known ` +
|
|
522
|
+
'but handler attribution is not, so no endpoint-specific claim passes until ' +
|
|
523
|
+
'engine-owned handler proof resolves the overlap',
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
if (resolution.status === 'mismatch') {
|
|
527
|
+
return {
|
|
528
|
+
status: 'invalid',
|
|
529
|
+
reason: `'${input.obligation.id}': witnessed '${HTTP_REQUEST_KIND}' record ` +
|
|
530
|
+
`'${label}' observed ${observedMethod.toUpperCase()} ` +
|
|
531
|
+
`${interpreted.path} uniquely matches route ${candidateIdentityText(resolution.matched)} ` +
|
|
532
|
+
`but the obligation requires endpoint '${input.obligation.resourceId}'; evidence from ` +
|
|
533
|
+
'a different endpoint can never satisfy it',
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (input.obligation.contract === 'http:response-status-ok') {
|
|
537
|
+
const status = payload['status'];
|
|
538
|
+
if (typeof status !== 'number' || !Number.isInteger(status) || status < 200 || status > 299) {
|
|
539
|
+
return {
|
|
540
|
+
status: 'invalid',
|
|
541
|
+
reason: `'${input.obligation.id}': observed status ` +
|
|
542
|
+
`'${String(status)}' is not a 2xx response`,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return { status: 'satisfied', recordId: label };
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Grades the explicit transport contracts (`http:request-observed`,
|
|
550
|
+
* `http:response-status-ok`): one shared path, no divergent verifier.
|
|
551
|
+
* Proves only that the witness observed an HTTP exchange in the bound
|
|
552
|
+
* run; test attribution is suite-claimed.
|
|
553
|
+
*
|
|
554
|
+
* Deterministic aggregation (plan §10 steps 1-8): the exact contract
|
|
555
|
+
* and the suite anchor are validated once (codepoint-smallest anchor
|
|
556
|
+
* when several qualify); the route inventory is scanned once; then
|
|
557
|
+
* EVERY `http.request` record is graded independently with NO early
|
|
558
|
+
* return on the first bad record. `satisfied > invalid > missing`
|
|
559
|
+
* decides; ≥1 satisfying record selects the codepoint-smallest
|
|
560
|
+
* record id joined with the anchor — identical for every input
|
|
561
|
+
* permutation. Otherwise the codepoint-smallest invalid reason wins,
|
|
562
|
+
* else the smallest missing reason. Untrusted records never satisfy;
|
|
563
|
+
* status-ok still means ≥1 eligible 2xx (no new success rule).
|
|
564
|
+
*
|
|
565
|
+
* Args:
|
|
566
|
+
* input: the claim plus its attributed evidence and obligation.
|
|
567
|
+
*
|
|
568
|
+
* Returns:
|
|
569
|
+
* ClaimOutcome: satisfied only for a witnessed engine-observed
|
|
570
|
+
* exchange matching the endpoint shape (plus 2xx for status-ok).
|
|
571
|
+
*/
|
|
572
|
+
function gradeTransportObservation(input) {
|
|
573
|
+
const anchor = selectTransportAnchor(input);
|
|
574
|
+
if (anchor.ok === false)
|
|
575
|
+
return anchor.outcome;
|
|
576
|
+
const requests = input.evidence.filter((entry) => entry.record.kind === HTTP_REQUEST_KIND);
|
|
577
|
+
if (requests.length === 0) {
|
|
578
|
+
return {
|
|
579
|
+
status: 'missing',
|
|
580
|
+
reason: `'${input.obligation.id}': no '${HTTP_REQUEST_KIND}' record; the witness observed no ` +
|
|
581
|
+
'matching HTTP exchange in the bound run; test attribution is suite-claimed',
|
|
582
|
+
recordIds: [],
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const inventoryBlock = transportInventoryBlock(input);
|
|
586
|
+
const candidates = (input.httpRoutes ?? []);
|
|
587
|
+
const satisfied = [];
|
|
588
|
+
const invalidReasons = [];
|
|
589
|
+
const missingReasons = [];
|
|
590
|
+
for (const entry of requests) {
|
|
591
|
+
const grade = gradeTransportRecord(entry.record, entry.trust, input, inventoryBlock, candidates);
|
|
592
|
+
if (grade.status === 'satisfied')
|
|
593
|
+
satisfied.push(grade.recordId);
|
|
594
|
+
else if (grade.status === 'invalid')
|
|
595
|
+
invalidReasons.push(grade.reason);
|
|
596
|
+
else
|
|
597
|
+
missingReasons.push(grade.reason);
|
|
598
|
+
}
|
|
599
|
+
if (satisfied.length > 0) {
|
|
600
|
+
const selected = sortedUniqueIds(satisfied)[0];
|
|
601
|
+
return {
|
|
602
|
+
status: 'satisfied',
|
|
603
|
+
recordIds: sortedUniqueIds([anchor.anchorId, selected]),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
if (invalidReasons.length > 0) {
|
|
607
|
+
const reason = invalidReasons.sort(compareStrings)[0];
|
|
608
|
+
return { status: 'invalid', reason };
|
|
609
|
+
}
|
|
610
|
+
const reason = missingReasons.sort(compareStrings)[0];
|
|
611
|
+
return { status: 'missing', reason };
|
|
612
|
+
}
|
|
613
|
+
/** Grades the HTTP namespace: exact dispatch, frontend fail-closed, shared transport path. */
|
|
614
|
+
function httpVerifier(input) {
|
|
615
|
+
// Exact contract dispatch first (plan §7): an unknown `http:*` name is
|
|
616
|
+
// never waived, never persistence-graded, and never examined for
|
|
617
|
+
// anchors or records — it stays blocking `missing` naming the contract.
|
|
618
|
+
if (!SUPPORTED_HTTP_CONTRACTS.includes(input.obligation.contract)) {
|
|
619
|
+
return unknownContract(input);
|
|
620
|
+
}
|
|
621
|
+
// Decision (pinned by tests, 2026-09-13 Phase 1 review): the frontend
|
|
622
|
+
// contract stays unavailable — the session channel binds exchanges to a
|
|
623
|
+
// session's proxy ORIGIN, not to a browser — so return blocking
|
|
624
|
+
// `missing` BEFORE anchor or record examination. No evidence can
|
|
625
|
+
// satisfy it; see {@link frontendProofUnavailable}.
|
|
626
|
+
if (input.obligation.contract === 'http:frontend-request-observed') {
|
|
627
|
+
return frontendProofUnavailable(input);
|
|
628
|
+
}
|
|
629
|
+
return gradeTransportObservation(input);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* The honest evidence channel each domain namespace would need. Wording
|
|
633
|
+
* is per-namespace on purpose: the fail-closed reason must name WHAT is
|
|
634
|
+
* missing, not a generic unsupported hole.
|
|
635
|
+
*/
|
|
636
|
+
const DOMAIN_NAMESPACES = [
|
|
637
|
+
{
|
|
638
|
+
namespace: 'auth',
|
|
639
|
+
channel: 'identity/role material and tenant-scoped application state',
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
namespace: 'task',
|
|
643
|
+
channel: 'queue/job delivery state',
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
namespace: 'validation',
|
|
647
|
+
channel: 'boundary semantics over application state and the response envelope',
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
namespace: 'webhook',
|
|
651
|
+
channel: 'signature/replay verification over application-received deliveries',
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
namespace: 'workflow',
|
|
655
|
+
channel: 'the workflow state machine and its audit log',
|
|
656
|
+
},
|
|
657
|
+
];
|
|
658
|
+
/**
|
|
659
|
+
* Builds the honest fail-closed reason for one domain contract: it names
|
|
660
|
+
* the contract, the behavior to prove, the missing engine-owned channel,
|
|
661
|
+
* and why transport evidence can never substitute for it.
|
|
662
|
+
*/
|
|
663
|
+
function failClosedReason(namespace, channel, input) {
|
|
664
|
+
const verb = input.obligation.contract.slice(namespace.length + 1);
|
|
665
|
+
const behavior = verb.length > 0 ? verb : input.obligation.contract;
|
|
666
|
+
return (`contract '${input.obligation.contract}' has no honest evidence channel: proving '${behavior}' ` +
|
|
667
|
+
`requires an engine-owned observer over application state (${channel} per plan §6), and no such ` +
|
|
668
|
+
'producer exists yet; transport exchanges (status codes, response bytes) cannot prove these ' +
|
|
669
|
+
`semantics, so '${input.obligation.id}' stays blocking. Do not add this contract to policies ` +
|
|
670
|
+
'until its pack ships a state-observing producer.');
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* The domain namespaces' verifier: fail-closed for EVERY contract of the
|
|
674
|
+
* namespace, whatever evidence arrives — old-shape check records,
|
|
675
|
+
* claimed or witnessed, perfectly formed. It never returns `satisfied`
|
|
676
|
+
* and never `invalid`: no existing record can honestly evidence these
|
|
677
|
+
* semantics, and hostile evidence deserves no sharper verdict than the
|
|
678
|
+
* honest-channel reason.
|
|
679
|
+
*/
|
|
680
|
+
function failClosedVerifier(namespace, channel) {
|
|
681
|
+
return (input) => {
|
|
682
|
+
// A contract string that does not parse into this namespace is
|
|
683
|
+
// genuinely unknown, not merely unproducible.
|
|
684
|
+
if (!input.obligation.contract.startsWith(`${namespace}:`)) {
|
|
685
|
+
return unknownContract(input);
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
status: 'missing',
|
|
689
|
+
reason: failClosedReason(namespace, channel, input),
|
|
690
|
+
recordIds: [],
|
|
691
|
+
};
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/** A contract the namespace's spec does not know: fail closed. */
|
|
695
|
+
function unknownContract(input) {
|
|
696
|
+
return {
|
|
697
|
+
status: 'missing',
|
|
698
|
+
reason: `no semantic verifier is registered for contract '${input.obligation.contract}'; ` +
|
|
699
|
+
`'${input.obligation.id}' stays blocking`,
|
|
700
|
+
recordIds: [],
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
/** True once registrations have run (idempotent across imports). */
|
|
704
|
+
let registered = false;
|
|
705
|
+
/**
|
|
706
|
+
* The http namespace's capability record (plan Phase 0 item 3): the two
|
|
707
|
+
* transport contracts are implemented over the witness HTTP proxy
|
|
708
|
+
* channel; `http:frontend-request-observed` stays registered but
|
|
709
|
+
* unavailable — the supervised session channel (plan Phase 1) binds
|
|
710
|
+
* exchanges to a session's proxy ORIGIN, not to a browser, so the
|
|
711
|
+
* independent browser/test observation the contract names still does not
|
|
712
|
+
* exist and enabling it would silently change its meaning.
|
|
713
|
+
*/
|
|
714
|
+
const HTTP_CAPABILITY = {
|
|
715
|
+
namespace: 'http',
|
|
716
|
+
contracts: ['http:request-observed', 'http:response-status-ok'],
|
|
717
|
+
unavailableContracts: [
|
|
718
|
+
{
|
|
719
|
+
contract: 'http:frontend-request-observed',
|
|
720
|
+
reason: 'no independent browser/test observation channel exists: the witness observes HTTP ' +
|
|
721
|
+
'exchanges but cannot prove which browser, UI action, or test produced an exchange ' +
|
|
722
|
+
'(test attribution is suite-claimed). The supervised session channel (plan Phase 1) ' +
|
|
723
|
+
'attributes exchanges to a test session\'s dedicated proxy port inside witness-kept ' +
|
|
724
|
+
'action intervals, but that attribution is by ORIGIN, not by browser — a hostile test ' +
|
|
725
|
+
'retains its own process and can drive the port directly — so the contract stays ' +
|
|
726
|
+
'fail-closed rather than silently change meaning',
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
observer: 'witness HTTP proxy channel: a witness-observed http.request exchange bound to the run ' +
|
|
730
|
+
'(origin engine-observed) plus a provenanced claimed ui.action anchor from the declaring test',
|
|
731
|
+
testKinds: ['browser-e2e', 'api-e2e'],
|
|
732
|
+
availability: { status: 'available' },
|
|
733
|
+
};
|
|
734
|
+
/** Builds the fail-closed capability record for one domain namespace. */
|
|
735
|
+
function domainCapability(namespace, channel) {
|
|
736
|
+
return {
|
|
737
|
+
namespace,
|
|
738
|
+
// none — fail-closed: every contract of the namespace blocks.
|
|
739
|
+
contracts: [],
|
|
740
|
+
unavailableContracts: [],
|
|
741
|
+
observer: `engine-owned observer over application state (${channel})`,
|
|
742
|
+
testKinds: [],
|
|
743
|
+
availability: {
|
|
744
|
+
status: 'unavailable',
|
|
745
|
+
reason: `no engine-owned state-observing producer exists for ${channel}; every contract of ` +
|
|
746
|
+
"the namespace fails closed (transport exchanges cannot prove these semantics), so " +
|
|
747
|
+
"the namespace advertises none — fail-closed",
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/** Registers every pack namespace + the http namespace. Idempotent. */
|
|
752
|
+
export function registerPackVerifiers() {
|
|
753
|
+
if (registered)
|
|
754
|
+
return;
|
|
755
|
+
registered = true;
|
|
756
|
+
registerContractVerifier('http', httpVerifier);
|
|
757
|
+
registerContractCapabilities(HTTP_CAPABILITY);
|
|
758
|
+
for (const { namespace, channel } of DOMAIN_NAMESPACES) {
|
|
759
|
+
registerContractVerifier(namespace, failClosedVerifier(namespace, channel));
|
|
760
|
+
registerContractCapabilities(domainCapability(namespace, channel));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
//# sourceMappingURL=pack-verifiers.js.map
|