@openwop/openwop-conformance 1.154.0 → 1.156.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/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/api/openapi.yaml +17 -1
- package/coverage.md +4 -0
- package/dist/lib/spec-coherence.js +1 -0
- package/package.json +2 -1
- package/requirements.json +280 -20
- package/schemas/CORPUS-STAMP.json +8 -7
- package/schemas/README.md +1 -0
- package/schemas/capabilities.schema.json +7 -0
- package/schemas/run-event-payloads.schema.json +9 -0
- package/schemas/run-snapshot.schema.json +4 -0
- package/schemas/subject.schema.json +56 -0
- package/src/lib/spec-coherence.ts +1 -0
- package/src/scenarios/anonymous-actor-shape.test.ts +6 -1
- package/src/scenarios/auth-subject-link.test.ts +20 -1
- package/src/scenarios/discovery.test.ts +12 -0
- package/src/scenarios/identity-owner-shape.test.ts +7 -3
- package/src/scenarios/owner-subject-echo.test.ts +127 -0
- package/src/scenarios/owner-subject-shape.test.ts +96 -0
- package/src/scenarios/protocol-versions-array.test.ts +103 -0
- package/src/scenarios/webhook-signed-delivery.test.ts +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [1.156.0] — 2026-09-02 — the event codemap as data (v2 charter Phase 1, tranche 5)
|
|
4
|
+
|
|
5
|
+
One new corpus-coherence scenario file (471 → 472), not packed:
|
|
6
|
+
|
|
7
|
+
- `event-codemap-complete.test.ts` — gates on `V1_DIR`; asserts `spec/v1/event-codemap.json` (generated by `scripts/generate-event-codemap.mjs`) has exactly one row per `RunEventType`, every row's payload `$def` exists, and every proposed v2 name is unique, matches the charter's `domain.verb-ed` grammar, and never uses the reserved `core.` prefix. 117 rows; 18 flagged for a C.4 decision (tense, folded segments, `core.` drop). Registered in `SPEC_COHERENCE_SCENARIOS`, `spec-coherence-scenarios.json`, and the `files` negations; the published tarball is unchanged in content.
|
|
8
|
+
|
|
9
|
+
## [1.155.0] — 2026-09-02 — RFC 0165 `Draft → Active`: `protocolVersions[]`, the Subject record, header dual emission
|
|
10
|
+
|
|
11
|
+
Three new scenario files (468 → 471) and four added legs, all additive:
|
|
12
|
+
|
|
13
|
+
- `protocol-versions-array.test.ts` — server-free: the schema declares root `protocolVersions` with the scalar's strict grammar; presence-gated: items match, are unique, contain `protocolVersion`, and profile derivation is unchanged by the array.
|
|
14
|
+
- `owner-subject-shape.test.ts` — server-free: `subject.schema.json` compiles; legacy and workload forms accepted; email/whitespace `subjectId` rejected (invariant `subject-record-opaque`); `keyClass` iff saml/scim; both `owner` sub-objects declare `subject` and the `run.started` echo declares `principalKind`.
|
|
15
|
+
- `owner-subject-echo.test.ts` — gated on a snapshot carrying `owner.subject`: consistency with the owner triple, actor depth ≤ 4, `run.started` echo, fork copies tenant + subject verbatim.
|
|
16
|
+
- Legs: `discovery.test.ts` (standard `ETag` + `If-None-Match` → 304, presence-gated); `webhook-signed-delivery.test.ts` (the `OpenWOP-*` family equals `X-openwop-*` and verifies, presence-gated); `auth-subject-link.test.ts` (legacy subjects never linked — seam-gated, records `blocked`); `identity-owner-shape.test.ts` registers `subject.schema.json`.
|
|
17
|
+
|
|
3
18
|
## [1.154.0] — 2026-09-02 — the tarball carries the suite and nothing else; the vendored contract is digest-checked (v2 charter Phase 1)
|
|
4
19
|
|
|
5
20
|
No new scenario file. Three packed changes:
|
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
|
|
|
103
103
|
|
|
104
104
|
## What's Covered
|
|
105
105
|
|
|
106
|
-
The current suite has
|
|
106
|
+
The current suite has 472 scenario files under `src/scenarios/`.
|
|
107
107
|
- 2026-08-19 (suite `1.137.0 → 1.138.0`): NEW `durability-poison-exhaustion.test.ts` — RFC 0158 §C.8, the FIRST row of that RFC's conformance table to land. Asserts what `failure-path.test.ts` cannot: not just that deterministically failing work reaches terminal, but that attempts STOP — counted on the log, re-counted after a scaled quiet window, asserted unchanged. A host still redelivering records more. Seam-gated on the existing event-log seam (`blocked` = unobservable, not unmet) and outside every profile floor.
|
|
108
108
|
- 2026-08-19 (suite `1.136.15 → 1.137.0`): NEW `replay-fanout-suppression.test.ts` — capability-gated on `webhooks.supported`, **outside every profile floor**; witnesses `replay.md` §"Host-initiated fan-out is an external effect", which was the largest normative MUST NOT on the replay surface with no scenario and no SECURITY invariant. Three legs in ONE `it` against ONE receiver and ONE subscription — a positive control, the MUST NOT, and a `branch` boundary leg — because "no delivery arrived" passes identically when delivery never worked, so absence is asserted only after presence is proven on that exact wiring. A host with an SSRF guard correctly refuses the loopback receiver and records `blocked`: **unobservable, not unmet.**
|
|
109
109
|
- 2026-08-18 (suite `1.136.7 → 1.136.8`, SP-04): NEW `spec-section-citations.test.ts` — server-free; a `<doc>.md §"<Section>"` citation of a checked doc MUST resolve to a heading that exists. `storage-adapters.md §"Claim acquisition"` was cited by four artifacts, including a normative MUST in `production-profile.md` §Durability, and the section did not exist for the life of RFC 0009. Scoped to the docs whose section citations carry the durability contract; a corpus-wide sweep finds ~260 unresolved citations that need triage before they can gate.
|
|
@@ -447,7 +447,7 @@ Server-required (added in 1.7.0):
|
|
|
447
447
|
| ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
448
448
|
| **Redaction** | [`capabilities.md`](../spec/v1/capabilities.md) §"Secrets" + NFR-7 + §"aiProviders" | Vendor-neutral assertions that the server doesn't leak secret material. Three scenario groups: (a) discovery shape contract — `secrets` + `aiProviders` advertisements are well-formed regardless of `secrets.supported`; when `supported === true`, scopes MUST be non-empty + `resolution === 'host-managed'`; `byok ⊆ supported`. (b) bearer-token redaction — invalid Bearer canary in `Authorization` header is not echoed in the 401 response body. (c) credentialRef echo control — gated on `secrets.supported === true`; canary planted in `configurable.ai.credentialRef` MUST NOT appear in any RunEvent payload (poll-based capture; transport-agnostic). Uses runtime-built canary fixtures (`lib/canaries.ts`) that defeat static secret scanners. 6 scenarios. |
|
|
449
449
|
|
|
450
|
-
Current source tree:
|
|
450
|
+
Current source tree: 472 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
|
|
451
451
|
|
|
452
452
|
## Remaining Gaps
|
|
453
453
|
|
package/api/openapi.yaml
CHANGED
|
@@ -109,6 +109,14 @@ paths:
|
|
|
109
109
|
summary: Capability declaration handshake.
|
|
110
110
|
operationId: getCapabilities
|
|
111
111
|
security: []
|
|
112
|
+
parameters:
|
|
113
|
+
- name: If-None-Match
|
|
114
|
+
in: header
|
|
115
|
+
required: false
|
|
116
|
+
schema: { type: string }
|
|
117
|
+
description: >-
|
|
118
|
+
RFC 0165 §C.2. Standard conditional request against the discovery document's `ETag`;
|
|
119
|
+
a matching value SHOULD yield `304 Not Modified`.
|
|
112
120
|
responses:
|
|
113
121
|
'200':
|
|
114
122
|
description: Capabilities object — see `capabilities.md`.
|
|
@@ -116,13 +124,21 @@ paths:
|
|
|
116
124
|
Cache-Control:
|
|
117
125
|
schema: { type: string }
|
|
118
126
|
example: 'public, max-age=300'
|
|
127
|
+
ETag:
|
|
128
|
+
schema: { type: string }
|
|
129
|
+
description: >-
|
|
130
|
+
RFC 0165 §C.2. Standard strong validator for the response bytes (SHOULD be sent).
|
|
131
|
+
Distinct from `Capabilities-Etag`, which validates negotiation safety
|
|
132
|
+
(`capabilities-change-detection.md` §"Cache validators").
|
|
119
133
|
Capabilities-Etag:
|
|
120
134
|
schema: { type: string }
|
|
121
|
-
description: Optional probe handle for mid-session capability change detection.
|
|
135
|
+
description: Optional probe handle for mid-session capability change detection. Deprecated toward v2 (RFC 0165 §C.2).
|
|
122
136
|
content:
|
|
123
137
|
application/json:
|
|
124
138
|
schema:
|
|
125
139
|
$ref: '#/components/schemas/Capabilities'
|
|
140
|
+
'304':
|
|
141
|
+
description: RFC 0165 §C.2 — `If-None-Match` matched the current `ETag`; the body is unchanged.
|
|
126
142
|
'503':
|
|
127
143
|
description: Server unable to compute capabilities (transient).
|
|
128
144
|
content:
|
package/coverage.md
CHANGED
|
@@ -112,6 +112,10 @@ The scenario groups in the table below (one row per group; count the rows — th
|
|
|
112
112
|
| `oauth-authorization-code-roundtrip.test.ts` | `capabilities.oauth` (RFC 0047 §C) + `SECURITY/invariants.yaml` `credential-payload-redaction` | A (capability-gated on `oauth.supported` + `grants` ∋ `authorization_code`; behavioral roundtrip via optional `POST /v1/host/sample/oauth/authorize-code-roundtrip` seam against the one canonical synthetic provider in `fixtures/oauth-providers/synthetic.json` — returns a credential REFERENCE; the authorization code / state / PKCE verifier / acquired access+refresh tokens are absent from every observable surface; `connector.authorized` carries the ref not the token) | `host-pending` | Capability+grant-gated; behavioral probe soft-skips on 404. Closes the RFC 0047 Tier-2 gap — exercises the actual authorization-code dance (shape + redaction scenarios existed; the grant itself was unexercised). |
|
|
113
113
|
| `connector-manifest-validity.test.ts` | `node-pack-manifest.schema.json` §Connector (RFC 0045, `node-packs.md` §Connectors) | Server-free (schema validity of the `connector` block incl. both ConnectorAuth variants + positive/negative round-trip; §B action/trigger typeId-resolution semantics — `connector_action_unresolved` on an unknown typeId) | host-pass (server-free) | Always runs; no host needed. Behavioral idempotency-hint + rate-limit-honored scenarios deferred until a host advertises a connector. |
|
|
114
114
|
| `identity-owner-shape.test.ts` | `run-snapshot.schema.json` properties.owner (RFC 0048 §C, `auth.md` §Identity claims) | Server-free (owner triple schema validity: positive `{tenant}` + full triple; negative missing-tenant + unknown-prop) | host-pass (server-free) | Always runs; no host needed. |
|
|
115
|
+
| `protocol-versions-array.test.ts` | RFC 0165 §A (`version-negotiation.md` §"`protocolVersions[]`") | A (server-free: schema declares the array with the scalar's grammar) + B (presence-gated: items match, unique, contain `protocolVersion`; profile derivation unchanged) | host-pending (presence-gated) | A host that omits the array records `inapplicable`. |
|
|
116
|
+
| `owner-subject-shape.test.ts` | RFC 0165 §B.1–§B.2 (`schemas/subject.schema.json`; both `owner` sub-objects) | Server-free: subject compiles; legacy + workload forms accepted; email/whitespace `subjectId` rejected (invariant `subject-record-opaque`); keyClass iff saml/scim; both owner sub-objects declare `subject` and the echo declares `principalKind` | host-pass (server-free) | Always runs; no host needed. |
|
|
117
|
+
| `owner-subject-echo.test.ts` | RFC 0165 §B.2, §B.4, §B.5 (`auth.md` §"The Subject record", `replay.md` §"Fork ownership") | B (gated on a snapshot carrying `owner.subject`): tenant/subjectId/kind/keyClass consistency, actor depth ≤ 4, `run.started` echo equals the snapshot, fork copies tenant + subject verbatim | host-pending (gated) | A host that does not emit `owner.subject` records `inapplicable`. |
|
|
118
|
+
| `event-codemap-complete.test.ts` | `spec/v1/event-codemap.json` (v2 charter C.4/C.9 — the v1→v2 event-type codemap as data) | Corpus coherence (gates on `V1_DIR`; asserts nothing about a host): every `RunEventType` has one row, every payload `$def` exists, v2 names unique + grammatical + never `core.`-prefixed | inapplicable to hosts | Not packed (SPEC_COHERENCE_SCENARIOS). |
|
|
115
119
|
| `cross-workspace-isolation.test.ts` | RFC 0048 §C/§D (`auth.md` §Identity claims, `rest-endpoints.md` `run_forbidden`) | A (owner-echo shape if a sample run is readable; §D isolation MUST-NOT via optional `POST /v1/host/sample/identity/cross-workspace-read` seam — cross-workspace read fails closed with `run_forbidden`/`not_found`) | `host-pending` | Behavioral probe soft-skips on 404; no host advertises run ownership yet (RFC 0048 `Draft`). |
|
|
116
120
|
| `authorization-roles-shape.test.ts` | `capabilities.authorization` (RFC 0049 §A, `auth.md` §"Role-based authorization") | A (advertisement shape always — `supported` boolean; `failClosed` const true; every `roles[].role` non-empty + `scopes` array) | `host-pending` | Always runs; asserts the block is absent or well-formed. |
|
|
117
121
|
| `authorization-fail-closed.test.ts` | `capabilities.authorization` (RFC 0049 §C) + `SECURITY/invariants.yaml` `authorization-fail-closed` | A (advertisement `failClosed===true` always; fail-closed MUST-NOT via optional `POST /v1/host/sample/authorization/decide` seam — an unseeded-role principal resolves `allowed:false`) | `host-pending` | Capability-gated on `authorization.supported`; behavioral probe soft-skips on 404. Scope-match + denial-audited scenarios deferred to a host. |
|
|
@@ -93,6 +93,7 @@ export const SPEC_COHERENCE_SCENARIOS = new Set([
|
|
|
93
93
|
'effect-identity-composition.test.ts',
|
|
94
94
|
'effect-identity-cross-scope.test.ts',
|
|
95
95
|
'error-envelope-canonical-shape.test.ts',
|
|
96
|
+
'event-codemap-complete.test.ts',
|
|
96
97
|
'form-content-packs.test.ts',
|
|
97
98
|
'multi-region-effect-vocabulary.test.ts',
|
|
98
99
|
'normative-example-extraction.test.ts',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.156.0",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"!src/scenarios/effect-identity-composition.test.ts",
|
|
41
41
|
"!src/scenarios/effect-identity-cross-scope.test.ts",
|
|
42
42
|
"!src/scenarios/error-envelope-canonical-shape.test.ts",
|
|
43
|
+
"!src/scenarios/event-codemap-complete.test.ts",
|
|
43
44
|
"!src/scenarios/form-content-packs.test.ts",
|
|
44
45
|
"!src/scenarios/multi-region-effect-vocabulary.test.ts",
|
|
45
46
|
"!src/scenarios/normative-example-extraction.test.ts",
|
package/requirements.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"$comment": "GENERATED by conformance/scripts/generate-requirement-registry.mjs — do not edit. One record per it()/test() in src/scenarios. Ids: openwop.it.<file-stem>.<title-slug>[~n] (src/lib/requirement-ids.ts). A record with id null has an interpolated title; its run-time row is keyed by the rendered title and maps here by file+line only. Renamed ids need a row in requirement-aliases.json.",
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
|
-
"files":
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
5
|
+
"files": 472,
|
|
6
|
+
"tests": 1971,
|
|
7
|
+
"withStableId": 1948,
|
|
8
8
|
"interpolatedTitles": 23,
|
|
9
9
|
"explicitIds": 0
|
|
10
10
|
},
|
|
@@ -3479,7 +3479,7 @@
|
|
|
3479
3479
|
{
|
|
3480
3480
|
"id": "openwop.it.anonymous-actor-shape.a-content-free-anon-grant-deny-record-validates-against-the-existing-def",
|
|
3481
3481
|
"file": "anonymous-actor-shape.test.ts",
|
|
3482
|
-
"line":
|
|
3482
|
+
"line": 147,
|
|
3483
3483
|
"title": "a content-free anon grant + deny record validates against the existing $def",
|
|
3484
3484
|
"explicitId": null,
|
|
3485
3485
|
"citations": []
|
|
@@ -3487,7 +3487,7 @@
|
|
|
3487
3487
|
{
|
|
3488
3488
|
"id": "openwop.it.anonymous-actor-shape.authorization-decided-is-in-the-runeventtype-enum-the-reused-audit-event",
|
|
3489
3489
|
"file": "anonymous-actor-shape.test.ts",
|
|
3490
|
-
"line":
|
|
3490
|
+
"line": 173,
|
|
3491
3491
|
"title": "authorization.decided is in the RunEventType enum (the reused audit event)",
|
|
3492
3492
|
"explicitId": null,
|
|
3493
3493
|
"citations": []
|
|
@@ -4531,6 +4531,14 @@
|
|
|
4531
4531
|
}
|
|
4532
4532
|
]
|
|
4533
4533
|
},
|
|
4534
|
+
{
|
|
4535
|
+
"id": "openwop.it.auth-subject-link.a-scim-deactivation-does-not-deny-a-saml-login-whose-run-subject-is-legacy-issue",
|
|
4536
|
+
"file": "auth-subject-link.test.ts",
|
|
4537
|
+
"line": 201,
|
|
4538
|
+
"title": "a SCIM deactivation does not deny a SAML login whose run subject is legacy-issued",
|
|
4539
|
+
"explicitId": null,
|
|
4540
|
+
"citations": []
|
|
4541
|
+
},
|
|
4534
4542
|
{
|
|
4535
4543
|
"id": "openwop.it.auth.returns-401-with-canonical-error-envelope-per-auth-md-3",
|
|
4536
4544
|
"file": "auth.test.ts",
|
|
@@ -7998,7 +8006,7 @@
|
|
|
7998
8006
|
{
|
|
7999
8007
|
"id": "openwop.it.discovery.returns-200-with-required-capabilities-fields-per-capabilities-md-2",
|
|
8000
8008
|
"file": "discovery.test.ts",
|
|
8001
|
-
"line":
|
|
8009
|
+
"line": 16,
|
|
8002
8010
|
"title": "returns 200 with required Capabilities fields per capabilities.md §2",
|
|
8003
8011
|
"explicitId": null,
|
|
8004
8012
|
"citations": [
|
|
@@ -8020,7 +8028,7 @@
|
|
|
8020
8028
|
{
|
|
8021
8029
|
"id": "openwop.it.discovery.serves-cache-control-per-capabilities-md-4-caching-guidance",
|
|
8022
8030
|
"file": "discovery.test.ts",
|
|
8023
|
-
"line":
|
|
8031
|
+
"line": 36,
|
|
8024
8032
|
"title": "serves Cache-Control per capabilities.md §4 (caching guidance)",
|
|
8025
8033
|
"explicitId": null,
|
|
8026
8034
|
"citations": [
|
|
@@ -8030,10 +8038,27 @@
|
|
|
8030
8038
|
}
|
|
8031
8039
|
]
|
|
8032
8040
|
},
|
|
8041
|
+
{
|
|
8042
|
+
"id": "openwop.it.discovery.if-a-standard-etag-is-present-a-matching-if-none-match-yields-304-rfc-0165-c-2-p",
|
|
8043
|
+
"file": "discovery.test.ts",
|
|
8044
|
+
"line": 46,
|
|
8045
|
+
"title": "IF a standard ETag is present, a matching If-None-Match yields 304 (RFC 0165 §C.2 — presence-gated)",
|
|
8046
|
+
"explicitId": null,
|
|
8047
|
+
"citations": [
|
|
8048
|
+
{
|
|
8049
|
+
"section": "RFC 0165 §C.2",
|
|
8050
|
+
"requirement": "ETag, when present, MUST be a non-empty validator"
|
|
8051
|
+
},
|
|
8052
|
+
{
|
|
8053
|
+
"section": "capabilities-change-detection.md §\"Cache validators\"",
|
|
8054
|
+
"requirement": "a matching If-None-Match SHOULD yield 304 Not Modified"
|
|
8055
|
+
}
|
|
8056
|
+
]
|
|
8057
|
+
},
|
|
8033
8058
|
{
|
|
8034
8059
|
"id": "openwop.it.discovery.if-capabilities-etag-is-present-it-is-non-empty-and-stable-within-the-cache-wind",
|
|
8035
8060
|
"file": "discovery.test.ts",
|
|
8036
|
-
"line":
|
|
8061
|
+
"line": 57,
|
|
8037
8062
|
"title": "IF Capabilities-Etag is present, it is non-empty and stable within the cache window",
|
|
8038
8063
|
"explicitId": null,
|
|
8039
8064
|
"citations": [
|
|
@@ -8054,7 +8079,7 @@
|
|
|
8054
8079
|
{
|
|
8055
8080
|
"id": "openwop.it.discovery.declares-non-zero-limits-per-capabilities-md-3-capabilitylimiter-shape",
|
|
8056
8081
|
"file": "discovery.test.ts",
|
|
8057
|
-
"line":
|
|
8082
|
+
"line": 83,
|
|
8058
8083
|
"title": "declares non-zero limits per capabilities.md §3 (CapabilityLimiter shape)",
|
|
8059
8084
|
"explicitId": null,
|
|
8060
8085
|
"citations": [
|
|
@@ -8077,7 +8102,7 @@
|
|
|
8077
8102
|
{
|
|
8078
8103
|
"id": "openwop.it.discovery.if-fixtures-is-present-it-must-be-a-string-of-unique-non-empty-entries",
|
|
8079
8104
|
"file": "discovery.test.ts",
|
|
8080
|
-
"line":
|
|
8105
|
+
"line": 107,
|
|
8081
8106
|
"title": "IF fixtures is present, it MUST be a string[] of unique non-empty entries",
|
|
8082
8107
|
"explicitId": null,
|
|
8083
8108
|
"citations": [
|
|
@@ -8102,7 +8127,7 @@
|
|
|
8102
8127
|
{
|
|
8103
8128
|
"id": "openwop.it.discovery.returns-200-with-a-parseable-openapi-3-1-document",
|
|
8104
8129
|
"file": "discovery.test.ts",
|
|
8105
|
-
"line":
|
|
8130
|
+
"line": 147,
|
|
8106
8131
|
"title": "returns 200 with a parseable OpenAPI 3.1 document",
|
|
8107
8132
|
"explicitId": null,
|
|
8108
8133
|
"citations": [
|
|
@@ -8119,7 +8144,7 @@
|
|
|
8119
8144
|
{
|
|
8120
8145
|
"id": "openwop.it.discovery.host-claiming-auth-scoped-discovery-advertises-required-fields",
|
|
8121
8146
|
"file": "discovery.test.ts",
|
|
8122
|
-
"line":
|
|
8147
|
+
"line": 206,
|
|
8123
8148
|
"title": "host claiming auth-scoped discovery advertises required fields",
|
|
8124
8149
|
"explicitId": null,
|
|
8125
8150
|
"citations": [
|
|
@@ -8140,7 +8165,7 @@
|
|
|
8140
8165
|
{
|
|
8141
8166
|
"id": "openwop.it.discovery.authenticated-discovery-preserves-required-capabilities-fields",
|
|
8142
8167
|
"file": "discovery.test.ts",
|
|
8143
|
-
"line":
|
|
8168
|
+
"line": 242,
|
|
8144
8169
|
"title": "authenticated discovery preserves required Capabilities fields",
|
|
8145
8170
|
"explicitId": null,
|
|
8146
8171
|
"citations": [
|
|
@@ -8158,7 +8183,7 @@
|
|
|
8158
8183
|
{
|
|
8159
8184
|
"id": "openwop.it.discovery.unauthorized-key-must-not-reveal-capabilities-outside-its-authorization",
|
|
8160
8185
|
"file": "discovery.test.ts",
|
|
8161
|
-
"line":
|
|
8186
|
+
"line": 283,
|
|
8162
8187
|
"title": "unauthorized key MUST NOT reveal capabilities outside its authorization",
|
|
8163
8188
|
"explicitId": null,
|
|
8164
8189
|
"citations": [
|
|
@@ -9788,6 +9813,30 @@
|
|
|
9788
9813
|
}
|
|
9789
9814
|
]
|
|
9790
9815
|
},
|
|
9816
|
+
{
|
|
9817
|
+
"id": "openwop.it.event-codemap-complete.every-runeventtype-enum-member-has-exactly-one-row-and-every-row-names-an-enum-m",
|
|
9818
|
+
"file": "event-codemap-complete.test.ts",
|
|
9819
|
+
"line": 43,
|
|
9820
|
+
"title": "every RunEventType enum member has exactly one row, and every row names an enum member",
|
|
9821
|
+
"explicitId": null,
|
|
9822
|
+
"citations": []
|
|
9823
|
+
},
|
|
9824
|
+
{
|
|
9825
|
+
"id": "openwop.it.event-codemap-complete.every-payloaddef-exists-in-run-event-payloads-schema-json-defs",
|
|
9826
|
+
"file": "event-codemap-complete.test.ts",
|
|
9827
|
+
"line": 51,
|
|
9828
|
+
"title": "every payloadDef exists in run-event-payloads.schema.json $defs",
|
|
9829
|
+
"explicitId": null,
|
|
9830
|
+
"citations": []
|
|
9831
|
+
},
|
|
9832
|
+
{
|
|
9833
|
+
"id": "openwop.it.event-codemap-complete.every-proposed-v2-name-is-unique-grammatical-two-segments-and-never-under-the-re",
|
|
9834
|
+
"file": "event-codemap-complete.test.ts",
|
|
9835
|
+
"line": 58,
|
|
9836
|
+
"title": "every proposed v2 name is unique, grammatical, two segments, and never under the reserved `core.` prefix",
|
|
9837
|
+
"explicitId": null,
|
|
9838
|
+
"citations": []
|
|
9839
|
+
},
|
|
9791
9840
|
{
|
|
9792
9841
|
"id": "openwop.it.eventOrdering.events-from-a-single-poll-have-non-decreasing-sequence-numbers",
|
|
9793
9842
|
"file": "eventOrdering.test.ts",
|
|
@@ -11581,7 +11630,7 @@
|
|
|
11581
11630
|
{
|
|
11582
11631
|
"id": "openwop.it.identity-owner-shape.run-snapshot-schema-json-defines-an-optional-owner-triple",
|
|
11583
11632
|
"file": "identity-owner-shape.test.ts",
|
|
11584
|
-
"line":
|
|
11633
|
+
"line": 37,
|
|
11585
11634
|
"title": "run-snapshot.schema.json defines an optional owner triple",
|
|
11586
11635
|
"explicitId": null,
|
|
11587
11636
|
"citations": []
|
|
@@ -11589,7 +11638,7 @@
|
|
|
11589
11638
|
{
|
|
11590
11639
|
"id": "openwop.it.identity-owner-shape.positive-tenant-only-owner-validates",
|
|
11591
11640
|
"file": "identity-owner-shape.test.ts",
|
|
11592
|
-
"line":
|
|
11641
|
+
"line": 50,
|
|
11593
11642
|
"title": "positive: tenant-only owner validates",
|
|
11594
11643
|
"explicitId": null,
|
|
11595
11644
|
"citations": []
|
|
@@ -11597,7 +11646,7 @@
|
|
|
11597
11646
|
{
|
|
11598
11647
|
"id": "openwop.it.identity-owner-shape.positive-full-triple-validates",
|
|
11599
11648
|
"file": "identity-owner-shape.test.ts",
|
|
11600
|
-
"line":
|
|
11649
|
+
"line": 54,
|
|
11601
11650
|
"title": "positive: full triple validates",
|
|
11602
11651
|
"explicitId": null,
|
|
11603
11652
|
"citations": []
|
|
@@ -11605,7 +11654,7 @@
|
|
|
11605
11654
|
{
|
|
11606
11655
|
"id": "openwop.it.identity-owner-shape.negative-owner-missing-tenant-is-rejected-tenant-is-required",
|
|
11607
11656
|
"file": "identity-owner-shape.test.ts",
|
|
11608
|
-
"line":
|
|
11657
|
+
"line": 61,
|
|
11609
11658
|
"title": "negative: owner missing tenant is rejected (tenant is required)",
|
|
11610
11659
|
"explicitId": null,
|
|
11611
11660
|
"citations": []
|
|
@@ -11613,7 +11662,7 @@
|
|
|
11613
11662
|
{
|
|
11614
11663
|
"id": "openwop.it.identity-owner-shape.negative-unknown-owner-property-is-rejected-additionalproperties-false",
|
|
11615
11664
|
"file": "identity-owner-shape.test.ts",
|
|
11616
|
-
"line":
|
|
11665
|
+
"line": 65,
|
|
11617
11666
|
"title": "negative: unknown owner property is rejected (additionalProperties:false)",
|
|
11618
11667
|
"explicitId": null,
|
|
11619
11668
|
"citations": []
|
|
@@ -14556,6 +14605,141 @@
|
|
|
14556
14605
|
}
|
|
14557
14606
|
]
|
|
14558
14607
|
},
|
|
14608
|
+
{
|
|
14609
|
+
"id": "openwop.it.owner-subject-echo.subject-is-consistent-with-the-owner-triple-b-2-and-the-actor-chain-is-bounded-b",
|
|
14610
|
+
"file": "owner-subject-echo.test.ts",
|
|
14611
|
+
"line": 77,
|
|
14612
|
+
"title": "subject is consistent with the owner triple (§B.2) and the actor chain is bounded (§B.5)",
|
|
14613
|
+
"explicitId": null,
|
|
14614
|
+
"citations": [
|
|
14615
|
+
{
|
|
14616
|
+
"section": "RFC 0165 §B.2",
|
|
14617
|
+
"requirement": "subject.tenant MUST equal owner.tenant"
|
|
14618
|
+
},
|
|
14619
|
+
{
|
|
14620
|
+
"section": "RFC 0165 §B.2",
|
|
14621
|
+
"requirement": "subject.subjectId MUST equal owner.principal when both are present"
|
|
14622
|
+
},
|
|
14623
|
+
{
|
|
14624
|
+
"section": "RFC 0165 §B.2",
|
|
14625
|
+
"requirement": "subject.kind MUST equal owner.principalKind when both are present"
|
|
14626
|
+
},
|
|
14627
|
+
{
|
|
14628
|
+
"section": "RFC 0165 §B.2",
|
|
14629
|
+
"requirement": "keyClass MUST be present on a linkable lane"
|
|
14630
|
+
},
|
|
14631
|
+
{
|
|
14632
|
+
"section": "RFC 0165 §B.2",
|
|
14633
|
+
"requirement": "keyClass MUST be absent off the linkable lanes"
|
|
14634
|
+
},
|
|
14635
|
+
{
|
|
14636
|
+
"section": "RFC 0165 §B.5",
|
|
14637
|
+
"requirement": "actor chain depth MUST NOT exceed 4"
|
|
14638
|
+
}
|
|
14639
|
+
]
|
|
14640
|
+
},
|
|
14641
|
+
{
|
|
14642
|
+
"id": "openwop.it.owner-subject-echo.run-started-echoes-the-snapshot-subject-verbatim-b-2-rfc-0048-c",
|
|
14643
|
+
"file": "owner-subject-echo.test.ts",
|
|
14644
|
+
"line": 99,
|
|
14645
|
+
"title": "run.started echoes the snapshot subject verbatim (§B.2, RFC 0048 §C)",
|
|
14646
|
+
"explicitId": null,
|
|
14647
|
+
"citations": [
|
|
14648
|
+
{
|
|
14649
|
+
"section": "RFC 0048 §C",
|
|
14650
|
+
"requirement": "run.started MUST be present in the event log"
|
|
14651
|
+
},
|
|
14652
|
+
{
|
|
14653
|
+
"section": "RFC 0165 §B.2",
|
|
14654
|
+
"requirement": "run.started owner.subject MUST equal RunSnapshot.owner.subject"
|
|
14655
|
+
}
|
|
14656
|
+
]
|
|
14657
|
+
},
|
|
14658
|
+
{
|
|
14659
|
+
"id": "openwop.it.owner-subject-echo.a-fork-copies-owner-tenant-and-owner-subject-verbatim-onto-the-child-b-4",
|
|
14660
|
+
"file": "owner-subject-echo.test.ts",
|
|
14661
|
+
"line": 110,
|
|
14662
|
+
"title": "a fork copies owner.tenant and owner.subject verbatim onto the child (§B.4)",
|
|
14663
|
+
"explicitId": null,
|
|
14664
|
+
"citations": [
|
|
14665
|
+
{
|
|
14666
|
+
"section": "replay.md §\"Fork ownership\"",
|
|
14667
|
+
"requirement": ":fork MUST return 201 for a completed noop run"
|
|
14668
|
+
},
|
|
14669
|
+
{
|
|
14670
|
+
"section": "RFC 0165 §B.4",
|
|
14671
|
+
"requirement": "the child owner.tenant MUST equal the source"
|
|
14672
|
+
},
|
|
14673
|
+
{
|
|
14674
|
+
"section": "RFC 0165 §B.4",
|
|
14675
|
+
"requirement": "the child owner.subject MUST be copied verbatim from the source"
|
|
14676
|
+
}
|
|
14677
|
+
]
|
|
14678
|
+
},
|
|
14679
|
+
{
|
|
14680
|
+
"id": "openwop.it.owner-subject-shape.compiles-and-accepts-a-full-saml-subject",
|
|
14681
|
+
"file": "owner-subject-shape.test.ts",
|
|
14682
|
+
"line": 43,
|
|
14683
|
+
"title": "compiles and accepts a full SAML subject",
|
|
14684
|
+
"explicitId": null,
|
|
14685
|
+
"citations": []
|
|
14686
|
+
},
|
|
14687
|
+
{
|
|
14688
|
+
"id": "openwop.it.owner-subject-shape.accepts-the-legacy-issuer-form-rfc-0165-b-3-and-a-workload-subject",
|
|
14689
|
+
"file": "owner-subject-shape.test.ts",
|
|
14690
|
+
"line": 47,
|
|
14691
|
+
"title": "accepts the legacy issuer form (RFC 0165 §B.3) and a workload subject",
|
|
14692
|
+
"explicitId": null,
|
|
14693
|
+
"citations": []
|
|
14694
|
+
},
|
|
14695
|
+
{
|
|
14696
|
+
"id": "openwop.it.owner-subject-shape.rejects-an-email-shaped-or-whitespace-subjectid-invariant-subject-record-opaque",
|
|
14697
|
+
"file": "owner-subject-shape.test.ts",
|
|
14698
|
+
"line": 52,
|
|
14699
|
+
"title": "rejects an email-shaped or whitespace subjectId (invariant subject-record-opaque)",
|
|
14700
|
+
"explicitId": null,
|
|
14701
|
+
"citations": []
|
|
14702
|
+
},
|
|
14703
|
+
{
|
|
14704
|
+
"id": "openwop.it.owner-subject-shape.requires-keyclass-on-the-saml-scim-lanes-and-forbids-it-elsewhere",
|
|
14705
|
+
"file": "owner-subject-shape.test.ts",
|
|
14706
|
+
"line": 57,
|
|
14707
|
+
"title": "requires keyClass on the saml/scim lanes and forbids it elsewhere",
|
|
14708
|
+
"explicitId": null,
|
|
14709
|
+
"citations": []
|
|
14710
|
+
},
|
|
14711
|
+
{
|
|
14712
|
+
"id": "openwop.it.owner-subject-shape.rejects-an-unknown-member-and-a-missing-required-member",
|
|
14713
|
+
"file": "owner-subject-shape.test.ts",
|
|
14714
|
+
"line": 64,
|
|
14715
|
+
"title": "rejects an unknown member and a missing required member",
|
|
14716
|
+
"explicitId": null,
|
|
14717
|
+
"citations": []
|
|
14718
|
+
},
|
|
14719
|
+
{
|
|
14720
|
+
"id": "openwop.it.owner-subject-shape.accepts-a-nested-actor-chain-depth-is-a-host-obligation-rfc-0165-b-5",
|
|
14721
|
+
"file": "owner-subject-shape.test.ts",
|
|
14722
|
+
"line": 70,
|
|
14723
|
+
"title": "accepts a nested actor chain (depth is a host obligation, RFC 0165 §B.5)",
|
|
14724
|
+
"explicitId": null,
|
|
14725
|
+
"citations": []
|
|
14726
|
+
},
|
|
14727
|
+
{
|
|
14728
|
+
"id": "openwop.it.owner-subject-shape.runsnapshot-owner-accepts-a-subject-and-still-rejects-an-undeclared-member",
|
|
14729
|
+
"file": "owner-subject-shape.test.ts",
|
|
14730
|
+
"line": 82,
|
|
14731
|
+
"title": "RunSnapshot.owner accepts a subject and still rejects an undeclared member",
|
|
14732
|
+
"explicitId": null,
|
|
14733
|
+
"citations": []
|
|
14734
|
+
},
|
|
14735
|
+
{
|
|
14736
|
+
"id": "openwop.it.owner-subject-shape.run-started-owner-echo-accepts-the-same-subject-and-principalkind-the-echo-match",
|
|
14737
|
+
"file": "owner-subject-shape.test.ts",
|
|
14738
|
+
"line": 88,
|
|
14739
|
+
"title": "run.started owner echo accepts the same subject AND principalKind (the echo matches the snapshot)",
|
|
14740
|
+
"explicitId": null,
|
|
14741
|
+
"citations": []
|
|
14742
|
+
},
|
|
14559
14743
|
{
|
|
14560
14744
|
"id": "openwop.it.pack-manifest-extension-opacity.leg-1-an-extension-bearing-manifest-installs",
|
|
14561
14745
|
"file": "pack-manifest-extension-opacity.test.ts",
|
|
@@ -16816,6 +17000,73 @@
|
|
|
16816
17000
|
"explicitId": null,
|
|
16817
17001
|
"citations": []
|
|
16818
17002
|
},
|
|
17003
|
+
{
|
|
17004
|
+
"id": "openwop.it.protocol-versions-array.declares-root-protocolversions-as-a-unique-non-empty-string-array",
|
|
17005
|
+
"file": "protocol-versions-array.test.ts",
|
|
17006
|
+
"line": 44,
|
|
17007
|
+
"title": "declares root `protocolVersions` as a unique, non-empty string array",
|
|
17008
|
+
"explicitId": null,
|
|
17009
|
+
"citations": []
|
|
17010
|
+
},
|
|
17011
|
+
{
|
|
17012
|
+
"id": "openwop.it.protocol-versions-array.item-grammar-equals-the-scalar-grammar-one-axis-one-grammar",
|
|
17013
|
+
"file": "protocol-versions-array.test.ts",
|
|
17014
|
+
"line": 52,
|
|
17015
|
+
"title": "item grammar equals the scalar grammar (one axis, one grammar)",
|
|
17016
|
+
"explicitId": null,
|
|
17017
|
+
"citations": []
|
|
17018
|
+
},
|
|
17019
|
+
{
|
|
17020
|
+
"id": "openwop.it.protocol-versions-array.accepts-1-11-2-0-rejects-01-0-1-0-0-duplicates-and-empty",
|
|
17021
|
+
"file": "protocol-versions-array.test.ts",
|
|
17022
|
+
"line": 59,
|
|
17023
|
+
"title": "accepts [\"1.11\", \"2.0\"], rejects \"01.0\", \"1.0.0\", duplicates, and empty",
|
|
17024
|
+
"explicitId": null,
|
|
17025
|
+
"citations": []
|
|
17026
|
+
},
|
|
17027
|
+
{
|
|
17028
|
+
"id": "openwop.it.protocol-versions-array.when-advertised-every-item-matches-the-grammar-items-are-unique-and-the-array-co",
|
|
17029
|
+
"file": "protocol-versions-array.test.ts",
|
|
17030
|
+
"line": 73,
|
|
17031
|
+
"title": "when advertised, every item matches the grammar, items are unique, and the array contains protocolVersion",
|
|
17032
|
+
"explicitId": null,
|
|
17033
|
+
"citations": [
|
|
17034
|
+
{
|
|
17035
|
+
"section": "RFC 0165 §A.1",
|
|
17036
|
+
"requirement": "protocolVersions MUST be an array"
|
|
17037
|
+
},
|
|
17038
|
+
{
|
|
17039
|
+
"section": "RFC 0165 §A.1",
|
|
17040
|
+
"requirement": "protocolVersions MUST be non-empty"
|
|
17041
|
+
},
|
|
17042
|
+
{
|
|
17043
|
+
"section": "RFC 0165 §A.1",
|
|
17044
|
+
"requirement": null,
|
|
17045
|
+
"interpolated": true
|
|
17046
|
+
},
|
|
17047
|
+
{
|
|
17048
|
+
"section": "RFC 0165 §A.1",
|
|
17049
|
+
"requirement": "protocolVersions items MUST be unique"
|
|
17050
|
+
},
|
|
17051
|
+
{
|
|
17052
|
+
"section": "RFC 0165 §A.2",
|
|
17053
|
+
"requirement": "protocolVersions MUST contain the value of protocolVersion"
|
|
17054
|
+
}
|
|
17055
|
+
]
|
|
17056
|
+
},
|
|
17057
|
+
{
|
|
17058
|
+
"id": "openwop.it.protocol-versions-array.profile-derivation-is-unchanged-by-the-array-iscore-reads-the-scalar-only",
|
|
17059
|
+
"file": "protocol-versions-array.test.ts",
|
|
17060
|
+
"line": 90,
|
|
17061
|
+
"title": "profile derivation is unchanged by the array (isCore reads the scalar only)",
|
|
17062
|
+
"explicitId": null,
|
|
17063
|
+
"citations": [
|
|
17064
|
+
{
|
|
17065
|
+
"section": "RFC 0165 §A.2",
|
|
17066
|
+
"requirement": "openwop-discovery-core derivation MUST NOT depend on protocolVersions in v1.x"
|
|
17067
|
+
}
|
|
17068
|
+
]
|
|
17069
|
+
},
|
|
16819
17070
|
{
|
|
16820
17071
|
"id": "openwop.it.provider-usage.capabilities-providerusage-is-either-absent-or-a-well-formed-object",
|
|
16821
17072
|
"file": "provider-usage.test.ts",
|
|
@@ -22365,13 +22616,22 @@
|
|
|
22365
22616
|
{
|
|
22366
22617
|
"section": "webhooks.md §\"Delivery headers\"",
|
|
22367
22618
|
"requirement": "X-openwop-Signature MUST be sha256=HMAC-SHA256(secret, `${X-openwop-Timestamp}.${rawBody}`)"
|
|
22619
|
+
},
|
|
22620
|
+
{
|
|
22621
|
+
"section": "RFC 0165 §C.1",
|
|
22622
|
+
"requirement": null,
|
|
22623
|
+
"interpolated": true
|
|
22624
|
+
},
|
|
22625
|
+
{
|
|
22626
|
+
"section": "RFC 0165 §C.1",
|
|
22627
|
+
"requirement": "OpenWOP-Signature MUST verify over the same bytes as X-openwop-Signature"
|
|
22368
22628
|
}
|
|
22369
22629
|
]
|
|
22370
22630
|
},
|
|
22371
22631
|
{
|
|
22372
22632
|
"id": "openwop.it.webhook-signed-delivery.control-with-a-tunnel-wired-the-host-still-refuses-the-loopback-receiver",
|
|
22373
22633
|
"file": "webhook-signed-delivery.test.ts",
|
|
22374
|
-
"line":
|
|
22634
|
+
"line": 378,
|
|
22375
22635
|
"title": "control: with a tunnel wired, the host still refuses the loopback receiver",
|
|
22376
22636
|
"explicitId": null,
|
|
22377
22637
|
"citations": [
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md §\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
|
|
3
|
-
"suiteVersion": "1.
|
|
4
|
-
"corpusCommit": "
|
|
3
|
+
"suiteVersion": "1.156.0",
|
|
4
|
+
"corpusCommit": "de4bba1f255a49995789dfd12ed7fe4c4a1dab90",
|
|
5
5
|
"files": {
|
|
6
6
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
7
7
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
8
8
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
9
|
-
"api/openapi.yaml": "
|
|
9
|
+
"api/openapi.yaml": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
|
|
10
10
|
"api/redocly.yaml": "9f2374405b9ca58994ae076ada9fbe4dfb8265b77e4bf65331d1493192c0bd3a",
|
|
11
|
-
"schemas/README.md": "
|
|
11
|
+
"schemas/README.md": "3769a5bcdc2d8f02c819d20eecf34e0c50666b47ac922380a9b0e0ec13cfc9e2",
|
|
12
12
|
"schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
|
|
13
13
|
"schemas/a2ui-surface-delta-frame.schema.json": "970d434e4fdcb925de64aaace91eb83bfbcfd8f76757a9ce9f339eda5fb3f4e8",
|
|
14
14
|
"schemas/agent-deployment-transition.schema.json": "1ac987fed0f380fa5b4e9a40e8e13316e5ae42d03a0ff42db4be6273e1bfe9f4",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"schemas/artifact-type-pack-manifest.schema.json": "13932ea58e819e01b3a0cc7a65590b70683dd5fe9208fc06c64c339943d3c4ce",
|
|
27
27
|
"schemas/audit-verify-result.schema.json": "e8fd013fb610ba5db27ab0113ec2eb18e7da5f9310dd3de2dd3dc87a259c21fe",
|
|
28
28
|
"schemas/budget-policy.schema.json": "3d4fe30ae129fe136ab9ece395f2a2a42a76360786b1b9a1d5f1cbc753eb5b39",
|
|
29
|
-
"schemas/capabilities.schema.json": "
|
|
29
|
+
"schemas/capabilities.schema.json": "72811aa52360e9b84988e7989e4cad519acf0db096f4e3894574f7d022e37e7a",
|
|
30
30
|
"schemas/certification-bundle-v2.schema.json": "3540232676086869ae78d24f3b9c2154f9c4a8a5eeb976512d6a7409f8cf19bb",
|
|
31
31
|
"schemas/channel-presence-payload.schema.json": "f9d0ef4219814c2b765baf3e68cbe5eea2cb42379f516a93a0459dcd5f7db47e",
|
|
32
32
|
"schemas/channel-written-payload.schema.json": "54723a56bc5a35232f64544ca686ac1ac959a5fb05b32b2401facca6b3ef51a3",
|
|
@@ -77,15 +77,16 @@
|
|
|
77
77
|
"schemas/residency.schema.json": "b45fdc91ae88091251914dbd6787a1e2ce687580b91d31a759217b7a1f0010c0",
|
|
78
78
|
"schemas/run-ancestry-response.schema.json": "963a5763b03216aa72fc2854560c778e53994f8cb06ea9a4053e541273c87f5c",
|
|
79
79
|
"schemas/run-diff-response.schema.json": "d9d196730b5d5d8a04a591d21a8fc6b44be7fa134f11827ff7e4f1a86d47e83d",
|
|
80
|
-
"schemas/run-event-payloads.schema.json": "
|
|
80
|
+
"schemas/run-event-payloads.schema.json": "37cdbdb5038e148056c104a49b8d28a9668479e06d401309d354a8b0a70921a3",
|
|
81
81
|
"schemas/run-event.schema.json": "c70c06932b9831295042836213e617c8e946f20ddd49cf4e889b74d89d2175a8",
|
|
82
82
|
"schemas/run-options.schema.json": "f8241ece6ddc6527de9160d811d42c231b4f77b935f2e107b3c05608545c59ab",
|
|
83
83
|
"schemas/run-orchestrator-decided-event.schema.json": "17a43a3c44680a24d411141357a867b3caed6107d56de88fe2e062eaf93fa2c1",
|
|
84
|
-
"schemas/run-snapshot.schema.json": "
|
|
84
|
+
"schemas/run-snapshot.schema.json": "8a45f997beec0a2fa596f4631129a869cfbc37c9ad38583530ed11655301d2ab",
|
|
85
85
|
"schemas/security-advisory.schema.json": "9c550f0bf10a3193dcca4a127081fb66fe5dc1b6f729c4c17bfaea3ba3ce05fc",
|
|
86
86
|
"schemas/self-hosted-runner-dispatch-frame.schema.json": "3f1d7edd572c6747657299525d8e85329b93b6fadf670e3a19c74d53e725adec",
|
|
87
87
|
"schemas/self-hosted-runner-registration.schema.json": "6c781adf1bb915a9e47796494f4a527353cbbf72b480e5a05be4aac65a6d0c6e",
|
|
88
88
|
"schemas/self-hosted-runner-result-frame.schema.json": "16f2f711b6071c797d8d4065f6f5ff39102ff03890fefce7a88a669831566296",
|
|
89
|
+
"schemas/subject.schema.json": "89679e35c7ec586adf6dd4adaff0bfa5453648d067fb2a43d7a2f7b2d016e10c",
|
|
89
90
|
"schemas/suspend-request.schema.json": "1aa39f2a947934bedc9f9696bf45fcb6119395c580d5ca282efb9443bd94a7bb",
|
|
90
91
|
"schemas/tool-descriptor.schema.json": "7f6adbae63003c1f1311f6a436f7f286542be62aa19a27b46730f2c2810857f7",
|
|
91
92
|
"schemas/trigger-event.schema.json": "59d63b79dca3593f45d281776cbfe43ae4d85ee25519b82ad59f162a9efb592c",
|
package/schemas/README.md
CHANGED
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
| `run-options.schema.json` | `run-options.md` | Per-run input overlay (configurable + tags + metadata) on `POST /v1/runs` |
|
|
72
72
|
| `run-orchestrator-decided-event.schema.json` | orchestrator RFC + `observability.md` | Event payload for orchestrator decisions |
|
|
73
73
|
| `run-snapshot.schema.json` | `rest-endpoints.md` §RunSnapshot | Projected run state from `GET /v1/runs/{runId}` |
|
|
74
|
+
| `subject.schema.json` | `auth.md` §"The Subject record" + RFC 0165 | DRAFT — issuer-scoped, lane-typed identity record carried as `owner.subject` on `RunSnapshot` and the `run.started` echo; opaque `subjectId` (no `@`/whitespace), `keyClass` iff saml/scim, optional bounded `actor` chain. Optional in v1.x; the v2 major requires it. |
|
|
74
75
|
| `residency.schema.json` | RFC 0129 + `capabilities.md` §dataResidency | OPTIONAL data-residency `{region}` constraint on `POST /v1/runs` (honor-or-reject admission) |
|
|
75
76
|
| `credential-provenance.schema.json` | `host-capabilities.md` §"Credential provenance + egress policy" (RFC 0079) | Metadata about a host-issued credential at the tool/egress boundary — `credentialId`/`issuer`/`audiences`(+scopes/expiry/redaction/audit-correlation). Secret-free (SR-1); the §C audience-binding MUST is evaluated against `audiences`. |
|
|
76
77
|
| `security-advisory.schema.json` | `registry-operations.md` + INCIDENT-RESPONSE runbook | Registry-owned CVE advisory record at `registry/security/advisories.json`. One entry per disclosed vulnerability — id, severity, affected pack-name + SemVer range, optional fixedIn/advisoryUrl/credits. Enforced by `check-advisories.mjs` in `.github/workflows/registry-publish.yml`. |
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
"description": "RFC 0149 \u00a7C \u2014 openwop protocol version the server speaks, as ASCII `<major>.<minor>` with no leading zero except zero itself. `1.0` and `1.12` are valid; `1`, `1.0.0`, `v1.0`, and `01.0` are not. The integer major is the hard compatibility boundary and the integer minor is the additive contract level; PATCH belongs to suite and SDK versions, not the spec version. Previously constrained only by `minLength: 1`, which admitted `v1.0`, `1.0.0`, and `banana` \u2014 a negotiation the wire could not decide, since neither integer can be extracted from an unconstrained string. Independent of `engineVersion` (which gates persisted-doc compatibility per version-negotiation.md).",
|
|
17
17
|
"pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
|
|
18
18
|
},
|
|
19
|
+
"protocolVersions": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"minItems": 1,
|
|
22
|
+
"uniqueItems": true,
|
|
23
|
+
"items": { "type": "string", "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$" },
|
|
24
|
+
"description": "RFC 0165 \u00a7A. Every protocol `<major>.<minor>` this host speaks, newest first by convention. OPTIONAL in v1.x; when present it MUST contain the value of `protocolVersion` and MUST NOT name a major the host does not serve. Same grammar as `protocolVersion` (RFC 0149 \u00a7C), not the looser A2A item pattern. Consumers that do not understand it ignore it; profile derivation reads `protocolVersion` only. Reserved for v2 major negotiation."
|
|
25
|
+
},
|
|
19
26
|
"contractProvenance": {
|
|
20
27
|
"type": "object",
|
|
21
28
|
"description": "RFC 0146 (`Active`). Which corpus revision this host's contract handling corresponds to \u2014 the copy it validates and serves against. OPTIONAL; absent \u21d2 UNSPECIFIED provenance, neither current nor stale, and a consumer MUST NOT infer a version from its absence. ADVISORY: a consumer MUST NOT reject a request, refuse interop, or fail a run solely because this differs from its own \u2014 v1.x corpus revisions are additive, so a host on an older revision is conformant and this field detects drift rather than making drift an error. Nothing else on the wire answers this: `protocolVersion` is `1.0` across the entire v1 line (the drift happens inside that value) and `schemaVersions` is per-envelope-type. NOT an integrity check \u2014 `corpusCommit` says WHICH contract, never whether the copy was modified.",
|
|
@@ -1019,6 +1019,15 @@
|
|
|
1019
1019
|
"principal": {
|
|
1020
1020
|
"type": "string",
|
|
1021
1021
|
"minLength": 1
|
|
1022
|
+
},
|
|
1023
|
+
"principalKind": {
|
|
1024
|
+
"type": "string",
|
|
1025
|
+
"enum": ["user", "agent", "anonymous"],
|
|
1026
|
+
"description": "RFC 0132 (declared here by RFC 0165): the echo carries the same optional kind as `RunSnapshot.owner.principalKind`, so a host that echoes its own snapshot owner emits a valid payload."
|
|
1027
|
+
},
|
|
1028
|
+
"subject": {
|
|
1029
|
+
"$ref": "subject.schema.json",
|
|
1030
|
+
"description": "RFC 0165 \u00a7B. The run's Subject record, echoed verbatim from `RunSnapshot.owner.subject` when present."
|
|
1022
1031
|
}
|
|
1023
1032
|
},
|
|
1024
1033
|
"additionalProperties": false
|
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"tenant": { "type": "string", "minLength": 1, "description": "Top-level isolation boundary." },
|
|
47
47
|
"workspace": { "type": "string", "minLength": 1, "description": "Optional sub-tenant within the tenant (RFC 0048 workspace)." },
|
|
48
48
|
"principal": { "type": "string", "minLength": 1, "description": "Acting identity (user or agent) — opaque id, never PII." },
|
|
49
|
+
"subject": {
|
|
50
|
+
"$ref": "subject.schema.json",
|
|
51
|
+
"description": "RFC 0165 \u00a7B. OPTIONAL issuer-scoped Subject record for the acting identity. When present: `subject.tenant` MUST equal `tenant`, `subject.subjectId` MUST equal `principal` when both are present, `subject.kind` MUST equal `principalKind` when both are present. A host that emits it on new runs synthesizes `issuer: \"urn:openwop:legacy\"` for runs that predate emission (\u00a7B.3) and copies it verbatim onto forks (\u00a7B.4)."
|
|
52
|
+
},
|
|
49
53
|
"principalKind": {
|
|
50
54
|
"type": "string",
|
|
51
55
|
"enum": ["user", "agent", "anonymous"],
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://openwop.dev/spec/v1/subject.schema.json",
|
|
4
|
+
"title": "OpenWOP Subject",
|
|
5
|
+
"description": "RFC 0165 §B. An issuer-scoped, lane-typed identity record carried as `owner.subject` on RunSnapshot and on the run.started owner echo. With `issuer` in the key, two trust roots issuing the same identifier are distinct by construction. Optional in v1.x; the v2 major requires it and removes the bare `owner.principal` string.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["issuer", "subjectId", "tenant", "lane", "kind"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"issuer": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"maxLength": 1024,
|
|
14
|
+
"pattern": "^\\S+$",
|
|
15
|
+
"description": "Trust root that asserted this subject: the SAML IdP entityID, the OIDC `iss`, the mTLS CA subject, the API-key realm, the SCIM connection id, or `urn:openwop:legacy` for a run that predates subject emission (RFC 0165 §B.3)."
|
|
16
|
+
},
|
|
17
|
+
"subjectId": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"minLength": 1,
|
|
20
|
+
"maxLength": 512,
|
|
21
|
+
"pattern": "^[^@\\s]+$",
|
|
22
|
+
"description": "Opaque, stable, issuer-scoped identifier. MUST NOT be an email address, a display name, or credential material (SECURITY invariant `subject-record-opaque`). Equals `owner.principal` when both are present (RFC 0165 §B.2)."
|
|
23
|
+
},
|
|
24
|
+
"tenant": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"minLength": 1,
|
|
27
|
+
"description": "MUST equal the enclosing `owner.tenant` (RFC 0165 §B.2)."
|
|
28
|
+
},
|
|
29
|
+
"lane": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"enum": ["api-key", "oauth2", "oidc", "mtls", "saml", "scim", "ldap", "workload"],
|
|
32
|
+
"description": "The authentication lane that minted the subject (auth-profiles.md). `workload` corresponds to RFC 0154's workload identity; the RFC 0154 `scheme` is carried in `issuer`."
|
|
33
|
+
},
|
|
34
|
+
"kind": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": ["user", "agent", "anonymous", "workload"],
|
|
37
|
+
"description": "Superset of `owner.principalKind` (RFC 0132) plus `workload` (RFC 0154). Equals `principalKind` when both are present."
|
|
38
|
+
},
|
|
39
|
+
"keyClass": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"enum": ["opaque-idp", "configured-immutable"],
|
|
42
|
+
"description": "How `subjectId` was derived when the lane is linkable (RFC 0163 §A). Present iff `lane` is `saml` or `scim`; MUST equal `capabilities.auth.subjectLinkKey` when both identity profiles are advertised (RFC 0164 §A.3)."
|
|
43
|
+
},
|
|
44
|
+
"actor": {
|
|
45
|
+
"$ref": "#",
|
|
46
|
+
"description": "Who acted on this subject's behalf (delegation; RFC 0154 §B inverted to the run's point of view). Provenance, not authorization. Chain depth MUST NOT exceed 4; a host refuses rather than truncates (RFC 0165 §B.5)."
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"allOf": [
|
|
50
|
+
{
|
|
51
|
+
"if": { "properties": { "lane": { "enum": ["saml", "scim"] } } },
|
|
52
|
+
"then": { "required": ["keyClass"] },
|
|
53
|
+
"else": { "not": { "required": ["keyClass"] } }
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -94,6 +94,7 @@ export const SPEC_COHERENCE_SCENARIOS: ReadonlySet<string> = new Set([
|
|
|
94
94
|
'effect-identity-composition.test.ts',
|
|
95
95
|
'effect-identity-cross-scope.test.ts',
|
|
96
96
|
'error-envelope-canonical-shape.test.ts',
|
|
97
|
+
'event-codemap-complete.test.ts',
|
|
97
98
|
'form-content-packs.test.ts',
|
|
98
99
|
'multi-region-effect-vocabulary.test.ts',
|
|
99
100
|
'normative-example-extraction.test.ts',
|
|
@@ -112,9 +112,13 @@ describe('anonymous-actor-shape: capability advertisement (RFC 0132 §B, server-
|
|
|
112
112
|
describe('anonymous-actor-shape: owner.principalKind (RFC 0132 §A, server-free)', () => {
|
|
113
113
|
it('run-snapshot owner accepts principalKind "anonymous" and rejects "guest"', () => {
|
|
114
114
|
const snap = loadSchema('run-snapshot.schema.json');
|
|
115
|
-
|
|
115
|
+
// RFC 0165 added an optional `subject` member ($ref subject.schema.json);
|
|
116
|
+
// register it and give the extracted sub-schema its parent's `$id` as the
|
|
117
|
+
// base the relative $ref resolves against.
|
|
118
|
+
const owner = { $id: snap.$id as string, ...((snap.properties as Record<string, unknown>).owner as Record<string, unknown>) };
|
|
116
119
|
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
117
120
|
addFormats(ajv);
|
|
121
|
+
ajv.addSchema(loadSchema('subject.schema.json'));
|
|
118
122
|
const validate = ajv.compile(owner);
|
|
119
123
|
expect(
|
|
120
124
|
validate({ tenant: 'acme', principal: 'anon:sess-3f9c', principalKind: 'anonymous' }),
|
|
@@ -136,6 +140,7 @@ describe('anonymous-actor-shape: audit reuses authorization.decided (RFC 0132 §
|
|
|
136
140
|
const payloads = loadSchema('run-event-payloads.schema.json');
|
|
137
141
|
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
138
142
|
addFormats(ajv);
|
|
143
|
+
ajv.addSchema(loadSchema('subject.schema.json'));
|
|
139
144
|
ajv.addSchema(payloads, 'payloads');
|
|
140
145
|
const decided = ajv.getSchema('payloads#/$defs/authorizationDecided');
|
|
141
146
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { describe, it, expect } from 'vitest';
|
|
27
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
27
|
+
import { softSkip, seamAbsent } from '../lib/soft-skip.js';
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
29
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
30
30
|
|
|
@@ -190,3 +190,22 @@ describe('auth-subject-link: link-key hygiene (RFC 0159 §A.2 — opt-in)', () =
|
|
|
190
190
|
).toBe(false);
|
|
191
191
|
});
|
|
192
192
|
});
|
|
193
|
+
|
|
194
|
+
describe('auth-subject-link: legacy subjects are never linked (RFC 0165 §B.3 — seam-gated)', () => {
|
|
195
|
+
// Invariant `subject-legacy-not-linkable` (witness class: negative-existence,
|
|
196
|
+
// seam-gated). A run that predates `owner.subject` emission carries a synthesized
|
|
197
|
+
// `issuer: "urn:openwop:legacy"`; a SCIM deactivation MUST NOT deny it. The only
|
|
198
|
+
// causable observation is a host seam that mints a legacy-issuer subject and a
|
|
199
|
+
// linked SCIM record; no such seam is specified yet, so this leg records
|
|
200
|
+
// `blocked` honestly rather than a pass (RFC 0148 §A).
|
|
201
|
+
it('a SCIM deactivation does not deny a SAML login whose run subject is legacy-issued', async () => {
|
|
202
|
+
const res = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
203
|
+
const auth = ((res.json as Record<string, unknown>)['auth'] ?? (res.json as { capabilities?: { auth?: unknown } }).capabilities?.auth) as { profiles?: string[] } | undefined;
|
|
204
|
+
const both = Array.isArray(auth?.profiles) && auth.profiles.includes('openwop-auth-saml') && auth.profiles.includes('openwop-auth-scim');
|
|
205
|
+
if (!both) {
|
|
206
|
+
return softSkip('inapplicable', 'host does not advertise both identity profiles (RFC 0164) — the legacy-link rule has no linked pair to protect');
|
|
207
|
+
}
|
|
208
|
+
return seamAbsent('no seam mints a legacy-issuer subject bound to a SCIM record (RFC 0165 §B.3); the negative cannot be caused from outside the host');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
@@ -10,6 +10,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
10
10
|
import { driver } from '../lib/driver.js';
|
|
11
11
|
import { discoveryFamilies } from '../lib/discovery-capabilities.js';
|
|
12
12
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
13
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
13
14
|
|
|
14
15
|
describe('discovery: /.well-known/openwop', () => {
|
|
15
16
|
it('returns 200 with required Capabilities fields per capabilities.md §2', async () => {
|
|
@@ -42,6 +43,17 @@ describe('discovery: /.well-known/openwop', () => {
|
|
|
42
43
|
)).toBeTruthy();
|
|
43
44
|
});
|
|
44
45
|
|
|
46
|
+
it('IF a standard ETag is present, a matching If-None-Match yields 304 (RFC 0165 §C.2 — presence-gated)', async () => {
|
|
47
|
+
const first = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
48
|
+
const etag = first.headers.get('etag');
|
|
49
|
+
if (etag === null) {
|
|
50
|
+
return softSkip('inapplicable', 'host does not send a standard ETag on the discovery document (RFC 0165 §C.2 — SHOULD, presence-gated)');
|
|
51
|
+
}
|
|
52
|
+
expect(etag.trim().length, driver.describe('RFC 0165 §C.2', 'ETag, when present, MUST be a non-empty validator')).toBeGreaterThan(0);
|
|
53
|
+
const second = await driver.get('/.well-known/openwop', { authenticated: false, headers: { 'If-None-Match': etag } });
|
|
54
|
+
expect(second.status, driver.describe('capabilities-change-detection.md §"Cache validators"', 'a matching If-None-Match SHOULD yield 304 Not Modified')).toBe(304);
|
|
55
|
+
});
|
|
56
|
+
|
|
45
57
|
it('IF Capabilities-Etag is present, it is non-empty and stable within the cache window', async () => {
|
|
46
58
|
const first = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
47
59
|
const firstEtag = first.headers.get('capabilities-etag');
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* - Positive: `{ tenant }` and `{ tenant, workspace, principal }` validate.
|
|
10
10
|
* - Negative: missing `tenant` (required), or an unknown property, is rejected.
|
|
11
11
|
*
|
|
12
|
-
* The owner subschema
|
|
13
|
-
*
|
|
12
|
+
* The owner subschema referenced only itself until RFC 0165 added an optional
|
|
13
|
+
* `subject` member ($ref subject.schema.json); that schema is registered on the
|
|
14
|
+
* ajv instance so the owner subschema still compiles standalone.
|
|
14
15
|
*
|
|
15
16
|
* @see RFCS/0048-tenant-workspace-principal-identity-model.md
|
|
16
17
|
* @see schemas/run-snapshot.schema.json properties.owner
|
|
@@ -24,6 +25,7 @@ import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
|
24
25
|
|
|
25
26
|
interface SnapshotSchema {
|
|
26
27
|
$schema: string;
|
|
28
|
+
$id: string;
|
|
27
29
|
properties: { owner?: Record<string, unknown> };
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -40,7 +42,9 @@ describe('category: identity owner-triple shape (RFC 0048 §C)', () => {
|
|
|
40
42
|
});
|
|
41
43
|
|
|
42
44
|
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
43
|
-
|
|
45
|
+
ajv.addSchema(JSON.parse(readFileSync(join(SCHEMAS_DIR, 'subject.schema.json'), 'utf8')) as Record<string, unknown>);
|
|
46
|
+
// `$id` is the base the relative `subject.schema.json` $ref (RFC 0165) resolves against.
|
|
47
|
+
const ownerSchema = { $schema: snapshot.$schema, $id: snapshot.$id, ...(snapshot.properties.owner as Record<string, unknown>) };
|
|
44
48
|
const validate = ajv.compile(ownerSchema);
|
|
45
49
|
|
|
46
50
|
it('positive: tenant-only owner validates', () => {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* owner-subject-echo — RFC 0165 §B.2, §B.4 verification (host-facing, gated).
|
|
3
|
+
*
|
|
4
|
+
* A host that emits `owner.subject` on a new run MUST: keep it consistent with
|
|
5
|
+
* the owner triple (§B.2), echo it verbatim on `run.started` (§B.2; RFC 0048
|
|
6
|
+
* §C), and copy it verbatim onto a fork (§B.4 — `replay.md` §"Fork
|
|
7
|
+
* ownership"). This file drives the `conformance-noop` fixture, reads the
|
|
8
|
+
* snapshot, and gates every leg on the snapshot actually carrying a subject:
|
|
9
|
+
* a host that does not emit one is conformant in v1.x (`inapplicable`), and a
|
|
10
|
+
* host that emits one is held to the rules.
|
|
11
|
+
*
|
|
12
|
+
* @see RFCS/0165-v2-preparation-wire-shapes.md §B
|
|
13
|
+
* @see spec/v1/auth.md §"The Subject record"
|
|
14
|
+
* @see spec/v1/replay.md §"Fork ownership"
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import { driver } from '../lib/driver.js';
|
|
19
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
20
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
21
|
+
|
|
22
|
+
const NOOP_WORKFLOW_ID = 'conformance-noop';
|
|
23
|
+
const SKIP_NO_NOOP = !isFixtureAdvertised(NOOP_WORKFLOW_ID);
|
|
24
|
+
|
|
25
|
+
interface Subject {
|
|
26
|
+
issuer: string;
|
|
27
|
+
subjectId: string;
|
|
28
|
+
tenant: string;
|
|
29
|
+
lane: string;
|
|
30
|
+
kind: string;
|
|
31
|
+
keyClass?: string;
|
|
32
|
+
actor?: Subject;
|
|
33
|
+
}
|
|
34
|
+
interface Owner {
|
|
35
|
+
tenant?: string;
|
|
36
|
+
workspace?: string;
|
|
37
|
+
principal?: string;
|
|
38
|
+
principalKind?: string;
|
|
39
|
+
subject?: Subject;
|
|
40
|
+
}
|
|
41
|
+
interface Snapshot {
|
|
42
|
+
runId?: string;
|
|
43
|
+
status?: string;
|
|
44
|
+
owner?: Owner;
|
|
45
|
+
}
|
|
46
|
+
interface RawEvent {
|
|
47
|
+
type?: string;
|
|
48
|
+
sequence?: number;
|
|
49
|
+
payload?: { owner?: Owner };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function createRun(): Promise<string> {
|
|
53
|
+
const create = await driver.post('/v1/runs', { workflowId: NOOP_WORKFLOW_ID });
|
|
54
|
+
if (create.status !== 201) throw new Error(`Failed to start ${NOOP_WORKFLOW_ID}: ${create.status}`);
|
|
55
|
+
return (create.json as { runId: string }).runId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function snapshot(runId: string): Promise<Snapshot> {
|
|
59
|
+
const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
|
|
60
|
+
if (res.status !== 200) throw new Error(`GET /v1/runs/${runId}: ${res.status}`);
|
|
61
|
+
return res.json as Snapshot;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function events(runId: string): Promise<readonly RawEvent[]> {
|
|
65
|
+
const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll?lastSequence=0&timeout=1`);
|
|
66
|
+
if (res.status !== 200) throw new Error(`events for ${runId}: ${res.status}`);
|
|
67
|
+
return (res.json as { events?: RawEvent[] })?.events ?? [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function actorDepth(s: Subject | undefined): number {
|
|
71
|
+
let d = 0;
|
|
72
|
+
for (let cur = s?.actor; cur !== undefined; cur = cur.actor) d++;
|
|
73
|
+
return d;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe.skipIf(SKIP_NO_NOOP)('owner-subject-echo: consistency, echo, fork (RFC 0165 §B — gated on a host that emits owner.subject)', () => {
|
|
77
|
+
it('subject is consistent with the owner triple (§B.2) and the actor chain is bounded (§B.5)', async () => {
|
|
78
|
+
const runId = await createRun();
|
|
79
|
+
const snap = await snapshot(runId);
|
|
80
|
+
const subject = snap.owner?.subject;
|
|
81
|
+
if (subject === undefined) {
|
|
82
|
+
return softSkip('inapplicable', 'host does not emit owner.subject on new runs (RFC 0165 §B — optional in v1.x)');
|
|
83
|
+
}
|
|
84
|
+
expect(subject.tenant, driver.describe('RFC 0165 §B.2', 'subject.tenant MUST equal owner.tenant')).toBe(snap.owner?.tenant);
|
|
85
|
+
if (snap.owner?.principal !== undefined) {
|
|
86
|
+
expect(subject.subjectId, driver.describe('RFC 0165 §B.2', 'subject.subjectId MUST equal owner.principal when both are present')).toBe(snap.owner.principal);
|
|
87
|
+
}
|
|
88
|
+
if (snap.owner?.principalKind !== undefined) {
|
|
89
|
+
expect(subject.kind, driver.describe('RFC 0165 §B.2', 'subject.kind MUST equal owner.principalKind when both are present')).toBe(snap.owner.principalKind);
|
|
90
|
+
}
|
|
91
|
+
if (subject.lane === 'saml' || subject.lane === 'scim') {
|
|
92
|
+
expect(subject.keyClass, driver.describe('RFC 0165 §B.2', 'keyClass MUST be present on a linkable lane')).toBeDefined();
|
|
93
|
+
} else {
|
|
94
|
+
expect(subject.keyClass, driver.describe('RFC 0165 §B.2', 'keyClass MUST be absent off the linkable lanes')).toBeUndefined();
|
|
95
|
+
}
|
|
96
|
+
expect(actorDepth(subject), driver.describe('RFC 0165 §B.5', 'actor chain depth MUST NOT exceed 4')).toBeLessThanOrEqual(4);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('run.started echoes the snapshot subject verbatim (§B.2, RFC 0048 §C)', async () => {
|
|
100
|
+
const runId = await createRun();
|
|
101
|
+
const snap = await snapshot(runId);
|
|
102
|
+
if (snap.owner?.subject === undefined) {
|
|
103
|
+
return softSkip('inapplicable', 'host does not emit owner.subject on new runs (RFC 0165 §B — optional in v1.x)');
|
|
104
|
+
}
|
|
105
|
+
const started = (await events(runId)).find((e) => e.type === 'run.started');
|
|
106
|
+
expect(started, driver.describe('RFC 0048 §C', 'run.started MUST be present in the event log')).toBeDefined();
|
|
107
|
+
expect(started?.payload?.owner?.subject, driver.describe('RFC 0165 §B.2', 'run.started owner.subject MUST equal RunSnapshot.owner.subject')).toEqual(snap.owner.subject);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('a fork copies owner.tenant and owner.subject verbatim onto the child (§B.4)', async () => {
|
|
111
|
+
const sourceRunId = await createRun();
|
|
112
|
+
const source = await snapshot(sourceRunId);
|
|
113
|
+
if (source.owner?.subject === undefined) {
|
|
114
|
+
return softSkip('inapplicable', 'host does not emit owner.subject on new runs (RFC 0165 §B — optional in v1.x)');
|
|
115
|
+
}
|
|
116
|
+
const fork = await driver.post(`/v1/runs/${encodeURIComponent(sourceRunId)}:fork`, { fromSeq: 0, mode: 'replay' });
|
|
117
|
+
if (fork.status === 404 || fork.status === 501 || fork.status === 403) {
|
|
118
|
+
return softSkip('inapplicable', `host does not offer :fork on this run (${fork.status}); the fork-copy rule has no fork to check`);
|
|
119
|
+
}
|
|
120
|
+
expect(fork.status, driver.describe('replay.md §"Fork ownership"', ':fork MUST return 201 for a completed noop run')).toBe(201);
|
|
121
|
+
const childId = (fork.json as { runId?: string }).runId;
|
|
122
|
+
expect(typeof childId, 'fork response MUST include the child runId').toBe('string');
|
|
123
|
+
const child = await snapshot(childId as string);
|
|
124
|
+
expect(child.owner?.tenant, driver.describe('RFC 0165 §B.4', 'the child owner.tenant MUST equal the source')).toBe(source.owner.tenant);
|
|
125
|
+
expect(child.owner?.subject, driver.describe('RFC 0165 §B.4', 'the child owner.subject MUST be copied verbatim from the source')).toEqual(source.owner.subject);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* owner-subject-shape — RFC 0165 §B.1–§B.2 verification (server-free).
|
|
3
|
+
*
|
|
4
|
+
* `schemas/subject.schema.json` is the OPTIONAL issuer-scoped identity record
|
|
5
|
+
* carried as `owner.subject` on `RunSnapshot` and on the `run.started` owner
|
|
6
|
+
* echo. This file proves the SCHEMA half of §B: the record compiles standalone,
|
|
7
|
+
* both owner sub-objects reference it as a declared optional member (they are
|
|
8
|
+
* `additionalProperties: false`, so an undeclared `subject` would be rejected —
|
|
9
|
+
* the live `principalKind` divergence RFC 0165 §Alternatives 3 names), and the
|
|
10
|
+
* opacity + key-class conditionals hold. Behaviour (echo, fork copy, legacy
|
|
11
|
+
* synthesis) is `owner-subject-echo.test.ts`, gated on a host that emits it.
|
|
12
|
+
*
|
|
13
|
+
* SECURITY invariant `subject-record-opaque` (witness class: claims-check —
|
|
14
|
+
* the schema pattern is what forbids an email-shaped or whitespace subjectId;
|
|
15
|
+
* no probe can prove a host never mints PII, only that the wire refuses it).
|
|
16
|
+
*
|
|
17
|
+
* @see RFCS/0165-v2-preparation-wire-shapes.md §B
|
|
18
|
+
* @see schemas/subject.schema.json
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
25
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
26
|
+
|
|
27
|
+
const read = (f: string): Record<string, unknown> => JSON.parse(readFileSync(join(SCHEMAS_DIR, f), 'utf8')) as Record<string, unknown>;
|
|
28
|
+
const subject = read('subject.schema.json');
|
|
29
|
+
const snapshot = read('run-snapshot.schema.json') as { $schema: string; $id: string; properties: { owner: Record<string, unknown> } };
|
|
30
|
+
const payloads = read('run-event-payloads.schema.json') as { $schema: string; $id: string; $defs: { runStarted: { properties: { owner: Record<string, unknown> } } } };
|
|
31
|
+
|
|
32
|
+
function ajvWithSubject(): Ajv2020 {
|
|
33
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
34
|
+
ajv.addSchema(subject);
|
|
35
|
+
return ajv;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const VALID = { issuer: 'https://idp.example.com/entity', subjectId: 'idp-op-8f3a', tenant: 'acme', lane: 'saml', kind: 'user', keyClass: 'opaque-idp' };
|
|
39
|
+
|
|
40
|
+
describe('owner-subject-shape: subject.schema.json (RFC 0165 §B.1)', () => {
|
|
41
|
+
const validate = ajvWithSubject().compile(subject);
|
|
42
|
+
|
|
43
|
+
it('compiles and accepts a full SAML subject', () => {
|
|
44
|
+
expect(validate(VALID), JSON.stringify(validate.errors)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('accepts the legacy issuer form (RFC 0165 §B.3) and a workload subject', () => {
|
|
48
|
+
expect(validate({ issuer: 'urn:openwop:legacy', subjectId: 'user_42', tenant: 'acme', lane: 'api-key', kind: 'user' }), JSON.stringify(validate.errors)).toBe(true);
|
|
49
|
+
expect(validate({ issuer: 'spiffe://trust.example', subjectId: 'spiffe://trust.example/ns/a/sa/b', tenant: 'acme', lane: 'workload', kind: 'workload' }), JSON.stringify(validate.errors)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects an email-shaped or whitespace subjectId (invariant subject-record-opaque)', () => {
|
|
53
|
+
expect(validate({ ...VALID, subjectId: 'alice@example.com' }), 'RFC 0165 §B.2: subjectId MUST NOT be an email address').toBe(false);
|
|
54
|
+
expect(validate({ ...VALID, subjectId: 'Alice Example' }), 'RFC 0165 §B.2: subjectId MUST NOT contain whitespace (a display name)').toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('requires keyClass on the saml/scim lanes and forbids it elsewhere', () => {
|
|
58
|
+
const { keyClass: _k, ...noKey } = VALID;
|
|
59
|
+
expect(validate(noKey), 'RFC 0165 §B.2: keyClass MUST be present when lane is saml or scim').toBe(false);
|
|
60
|
+
expect(validate({ ...VALID, lane: 'oidc' }), 'RFC 0165 §B.2: keyClass MUST be absent when the lane is not linkable').toBe(false);
|
|
61
|
+
expect(validate({ ...noKey, lane: 'oidc' }), JSON.stringify(validate.errors)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('rejects an unknown member and a missing required member', () => {
|
|
65
|
+
expect(validate({ ...VALID, email: 'x' })).toBe(false);
|
|
66
|
+
const { tenant: _t, ...noTenant } = VALID;
|
|
67
|
+
expect(validate(noTenant)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('accepts a nested actor chain (depth is a host obligation, RFC 0165 §B.5)', () => {
|
|
71
|
+
const peer = { issuer: 'https://peer.example', subjectId: 'peer-1', tenant: 'acme', lane: 'workload', kind: 'workload' };
|
|
72
|
+
const { keyClass: _k, ...anon } = { ...VALID, kind: 'anonymous', lane: 'api-key', actor: peer };
|
|
73
|
+
expect(validate(anon), JSON.stringify(validate.errors)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('owner-subject-shape: both owner sub-objects declare `subject` (RFC 0165 §B.1)', () => {
|
|
78
|
+
// The extracted sub-schema needs its parent's `$id` as the base for the relative `subject.schema.json` $ref.
|
|
79
|
+
const ownerSnap = { $schema: snapshot.$schema, $id: snapshot.$id, ...snapshot.properties.owner };
|
|
80
|
+
const ownerEcho = { $schema: payloads.$schema, $id: payloads.$id, ...payloads.$defs.runStarted.properties.owner };
|
|
81
|
+
|
|
82
|
+
it('RunSnapshot.owner accepts a subject and still rejects an undeclared member', () => {
|
|
83
|
+
const v = ajvWithSubject().compile(ownerSnap);
|
|
84
|
+
expect(v({ tenant: 'acme', principal: 'idp-op-8f3a', principalKind: 'user', subject: VALID }), JSON.stringify(v.errors)).toBe(true);
|
|
85
|
+
expect(v({ tenant: 'acme', role: 'admin' })).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('run.started owner echo accepts the same subject AND principalKind (the echo matches the snapshot)', () => {
|
|
89
|
+
const v = ajvWithSubject().compile(ownerEcho);
|
|
90
|
+
expect(
|
|
91
|
+
v({ tenant: 'acme', principal: 'idp-op-8f3a', principalKind: 'user', subject: VALID }),
|
|
92
|
+
'RFC 0165 §B.1: run.started owner MUST declare subject and principalKind so a host can echo its snapshot owner verbatim',
|
|
93
|
+
).toBe(true);
|
|
94
|
+
expect(v({ tenant: 'acme', subject: { ...VALID, subjectId: 'a@b' } })).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* protocol-versions-array — RFC 0165 §A verification.
|
|
3
|
+
*
|
|
4
|
+
* `protocolVersions: string[]` is the OPTIONAL root advertisement of every
|
|
5
|
+
* `<major>.<minor>` a host serves, reserved for v2 major negotiation
|
|
6
|
+
* (`version-negotiation.md` §"`protocolVersions[]`"). Three things are
|
|
7
|
+
* checkable today:
|
|
8
|
+
*
|
|
9
|
+
* 1. Server-free: the schema declares the array with the SAME strict grammar
|
|
10
|
+
* as the scalar (RFC 0149 §C), not the looser A2A item pattern — one axis,
|
|
11
|
+
* one grammar; and the Levenshtein-1 neighbour of `protocolVersion` is a
|
|
12
|
+
* real property, so the root-layout typo lint cannot mistake it.
|
|
13
|
+
* 2. Host, presence-gated: when advertised, every item matches, items are
|
|
14
|
+
* unique, and the array contains `protocolVersion`'s value.
|
|
15
|
+
* 3. Host, presence-gated: profile derivation is unchanged by the array —
|
|
16
|
+
* `isCore` reads the scalar only (RFC 0165 §A.2, §Alternatives 6).
|
|
17
|
+
*
|
|
18
|
+
* A host that omits the array is conformant (`inapplicable`, not a pass).
|
|
19
|
+
*
|
|
20
|
+
* @see RFCS/0165-v2-preparation-wire-shapes.md §A
|
|
21
|
+
* @see spec/v1/version-negotiation.md §"Protocol version grammar"
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest';
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
28
|
+
import { driver } from '../lib/driver.js';
|
|
29
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
30
|
+
import { isCore, type DiscoveryPayload } from '../lib/profiles.js';
|
|
31
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
32
|
+
|
|
33
|
+
interface CapsSchema {
|
|
34
|
+
$schema: string;
|
|
35
|
+
properties: {
|
|
36
|
+
protocolVersion?: { pattern?: string };
|
|
37
|
+
protocolVersions?: { type?: string; minItems?: number; uniqueItems?: boolean; items?: { pattern?: string } };
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const caps = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'capabilities.schema.json'), 'utf8')) as CapsSchema;
|
|
42
|
+
|
|
43
|
+
describe('protocol-versions-array: schema shape (RFC 0165 §A.1, server-free)', () => {
|
|
44
|
+
it('declares root `protocolVersions` as a unique, non-empty string array', () => {
|
|
45
|
+
const p = caps.properties.protocolVersions;
|
|
46
|
+
expect(p, 'RFC 0165 §A.1: capabilities.schema.json MUST declare root `protocolVersions`').toBeDefined();
|
|
47
|
+
expect(p?.type).toBe('array');
|
|
48
|
+
expect(p?.minItems).toBe(1);
|
|
49
|
+
expect(p?.uniqueItems).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('item grammar equals the scalar grammar (one axis, one grammar)', () => {
|
|
53
|
+
expect(
|
|
54
|
+
caps.properties.protocolVersions?.items?.pattern,
|
|
55
|
+
'RFC 0165 §A.1: protocolVersions items MUST use the RFC 0149 §C grammar, not the A2A item pattern',
|
|
56
|
+
).toBe(caps.properties.protocolVersion?.pattern);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('accepts ["1.11", "2.0"], rejects "01.0", "1.0.0", duplicates, and empty', () => {
|
|
60
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
61
|
+
const validate = ajv.compile({ $schema: caps.$schema, ...(caps.properties.protocolVersions as Record<string, unknown>) });
|
|
62
|
+
expect(validate(['1.11', '2.0'])).toBe(true);
|
|
63
|
+
expect(validate(['01.0'])).toBe(false);
|
|
64
|
+
expect(validate(['1.0.0'])).toBe(false);
|
|
65
|
+
expect(validate(['1.0', '1.0'])).toBe(false);
|
|
66
|
+
expect(validate([])).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('protocol-versions-array: advertisement (RFC 0165 §A.2 — presence-gated)', () => {
|
|
71
|
+
const GRAMMAR = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/;
|
|
72
|
+
|
|
73
|
+
it('when advertised, every item matches the grammar, items are unique, and the array contains protocolVersion', async () => {
|
|
74
|
+
const res = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
75
|
+
const doc = res.json as Record<string, unknown>;
|
|
76
|
+
const arr = doc['protocolVersions'];
|
|
77
|
+
if (arr === undefined) {
|
|
78
|
+
return softSkip('inapplicable', 'host does not advertise protocolVersions (RFC 0165 §A — optional in v1.x)');
|
|
79
|
+
}
|
|
80
|
+
expect(Array.isArray(arr), driver.describe('RFC 0165 §A.1', 'protocolVersions MUST be an array')).toBe(true);
|
|
81
|
+
const items = arr as unknown[];
|
|
82
|
+
expect(items.length, driver.describe('RFC 0165 §A.1', 'protocolVersions MUST be non-empty')).toBeGreaterThan(0);
|
|
83
|
+
for (const v of items) {
|
|
84
|
+
expect(typeof v === 'string' && GRAMMAR.test(v), driver.describe('RFC 0165 §A.1', `protocolVersions item ${JSON.stringify(v)} MUST match the RFC 0149 §C grammar`)).toBe(true);
|
|
85
|
+
}
|
|
86
|
+
expect(new Set(items).size, driver.describe('RFC 0165 §A.1', 'protocolVersions items MUST be unique')).toBe(items.length);
|
|
87
|
+
expect(items, driver.describe('RFC 0165 §A.2', 'protocolVersions MUST contain the value of protocolVersion')).toContain(doc['protocolVersion']);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('profile derivation is unchanged by the array (isCore reads the scalar only)', async () => {
|
|
91
|
+
const res = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
92
|
+
const doc = res.json as DiscoveryPayload & Record<string, unknown>;
|
|
93
|
+
if (doc['protocolVersions'] === undefined) {
|
|
94
|
+
return softSkip('inapplicable', 'host does not advertise protocolVersions (RFC 0165 §A — optional in v1.x)');
|
|
95
|
+
}
|
|
96
|
+
const without = { ...doc } as Record<string, unknown>;
|
|
97
|
+
delete without['protocolVersions'];
|
|
98
|
+
expect(
|
|
99
|
+
isCore(doc),
|
|
100
|
+
driver.describe('RFC 0165 §A.2', 'openwop-discovery-core derivation MUST NOT depend on protocolVersions in v1.x'),
|
|
101
|
+
).toBe(isCore(without as DiscoveryPayload));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -323,6 +323,28 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
|
|
|
323
323
|
driver.describe('webhooks.md §"Delivery headers"', 'X-openwop-Signature MUST be sha256=HMAC-SHA256(secret, `${X-openwop-Timestamp}.${rawBody}`)'),
|
|
324
324
|
).toBe(expected);
|
|
325
325
|
|
|
326
|
+
// RFC 0165 §C.1 — dual emission of the `OpenWOP-*` family. SHOULD-level and
|
|
327
|
+
// presence-gated: when the new family is present, every value MUST equal its
|
|
328
|
+
// `X-openwop-*` counterpart and the signature verifies reading either.
|
|
329
|
+
const dual = first.headers['openwop-signature'];
|
|
330
|
+
if (dual === undefined) {
|
|
331
|
+
softSkip('inapplicable', 'host does not yet dual-emit the OpenWOP-* webhook header family (RFC 0165 §C.1 — SHOULD)');
|
|
332
|
+
} else {
|
|
333
|
+
for (const [neu, old] of [
|
|
334
|
+
['openwop-webhook-id', 'x-openwop-webhook-id'],
|
|
335
|
+
['openwop-event-type', 'x-openwop-event-type'],
|
|
336
|
+
['openwop-timestamp', 'x-openwop-timestamp'],
|
|
337
|
+
['openwop-signature', 'x-openwop-signature'],
|
|
338
|
+
['openwop-signature-algorithm', 'x-openwop-signature-algorithm'],
|
|
339
|
+
] as const) {
|
|
340
|
+
expect(first.headers[neu], driver.describe('RFC 0165 §C.1', `${neu} MUST equal ${old} when the OpenWOP-* family is emitted`)).toBe(first.headers[old]);
|
|
341
|
+
}
|
|
342
|
+
expect(
|
|
343
|
+
String(dual).replace('sha256=', ''),
|
|
344
|
+
driver.describe('RFC 0165 §C.1', 'OpenWOP-Signature MUST verify over the same bytes as X-openwop-Signature'),
|
|
345
|
+
).toBe(expected);
|
|
346
|
+
}
|
|
347
|
+
|
|
326
348
|
// Body should parse as JSON with a run event shape.
|
|
327
349
|
const event = JSON.parse(first.body) as { type?: unknown; runId?: unknown };
|
|
328
350
|
expect(typeof event.type).toBe('string');
|