@openwop/openwop-conformance 1.144.0 → 1.146.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/README.md
CHANGED
|
@@ -94,7 +94,7 @@ Exit code is non-zero on any failed assertion. `--certify` distinguishes: `0`
|
|
|
94
94
|
|
|
95
95
|
## What's Covered
|
|
96
96
|
|
|
97
|
-
The current suite has
|
|
97
|
+
The current suite has 468 scenario files under `src/scenarios/`.
|
|
98
98
|
- 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.
|
|
99
99
|
- 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.**
|
|
100
100
|
- 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.
|
|
@@ -438,7 +438,7 @@ Server-required (added in 1.7.0):
|
|
|
438
438
|
| ------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
439
439
|
| **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. |
|
|
440
440
|
|
|
441
|
-
Current source tree:
|
|
441
|
+
Current source tree: 468 scenario files. Use [`coverage.md`](./coverage.md) for current grade/gap tracking.
|
|
442
442
|
|
|
443
443
|
## Remaining Gaps
|
|
444
444
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"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.146.0",
|
|
4
|
+
"corpusCommit": "aedaa189e427ef0f62db402c1b97f1d8a3ebb9ca"
|
|
5
5
|
}
|
|
@@ -3695,6 +3695,11 @@
|
|
|
3695
3695
|
"type": "boolean",
|
|
3696
3696
|
"description": "RFC 0159 (auth-profiles.md \u00a7Subject linking). When true, this host advertises BOTH `openwop-auth-saml` and `openwop-auth-scim` AND honours the cross-profile subject-linking obligation: it links the SAML and SCIM principals of one human on an opaque, IdP-stable subject id (SCIM `externalId` \u2194 persistent SAML `NameID`), same-tenant only, and on SCIM deactivation fail-closes the linked SAML identity too (composing RFC 0049 \u00a7C) \u2014 so a provisioned leaver cannot still SSO in. The link key MUST NOT be a mutable/PII attribute (email, userName). Optional; absent/false means the SAML and SCIM lanes are independent and the combined leaver guarantee is NOT claimed. A host MUST NOT set this true unless both profiles are advertised and the obligation holds."
|
|
3697
3697
|
},
|
|
3698
|
+
"subjectLinkKey": {
|
|
3699
|
+
"type": "string",
|
|
3700
|
+
"enum": ["opaque-idp", "oid", "immutable-id"],
|
|
3701
|
+
"description": "RFC 0163 (auth-profiles.md \u00a7Subject linking). The CLASS of opaque, IdP-stable, non-PII identifier this host joins the SAML and SCIM lanes on. A CLOSED enum of ALLOWED classes only: `opaque-idp` (SCIM `externalId` \u2194 persistent-format SAML `NameID`), `oid` (an IdP-asserted immutable object identifier), `immutable-id` (a configured immutable-id attribute asserted by the same IdP). Mutable/PII keys (email, userName, displayName) are INEXPRESSIBLE by construction \u2014 a conforming host cannot name one, which is the witness that converts RFC 0159 \u00a7A.2/\u00a7A.4's negative-existence prohibition into a positive advertisement. REQUIRED (its own MUST in auth-profiles.md \u00a7Subject linking, not a schema `required` row) when `subjectLinking` is true: a host setting `subjectLinking:true` MUST advertise a `subjectLinkKey` in this enum and MUST honour it (advertise only what you honour). Optional at the schema level so the field is additive; absent-with-`subjectLinking:true` is a conformance violation, not a schema-validation error. RFC 0163 also requires the SAML and SCIM lanes to share an IdP trust root before a link may form (SAML assertion issuer corresponds to the SCIM provisioning IdP)."
|
|
3702
|
+
},
|
|
3698
3703
|
"rotation": {
|
|
3699
3704
|
"type": "object",
|
|
3700
3705
|
"description": "API-key rotation advertisement (auth-profiles.md \u00a7`openwop-auth-api-key-rotation`).",
|
|
@@ -119,3 +119,37 @@ describe('resolveFileRecord classifies a corpus scenario as inapplicable, not bl
|
|
|
119
119
|
expect(r.disposition).toBe('executed-pass');
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
describe('the published layout is what makes these rows comparable across hosts', () => {
|
|
124
|
+
// Load-bearing and, until now, tested nowhere.
|
|
125
|
+
//
|
|
126
|
+
// A spec-coherence row is `inapplicable` only when V1_DIR is null, and V1_DIR
|
|
127
|
+
// is null only when the layout resolves to `published`. Two peers established
|
|
128
|
+
// by measurement what the code implies: `resolveLayout()` keys off PKG_ROOT,
|
|
129
|
+
// not the consuming repo, so an npm-installed consumer's parent is always
|
|
130
|
+
// `node_modules/@openwop/` — which never contains `schemas/` no matter where
|
|
131
|
+
// the host's checkout sits on disk. Every npm-consuming host therefore gets
|
|
132
|
+
// the same answer, and the dispositions are comparable BY CONSTRUCTION.
|
|
133
|
+
//
|
|
134
|
+
// One peer had generalised the opposite way — "the disposition is a property
|
|
135
|
+
// of where the bundle was cut" — from a host whose own runner sets
|
|
136
|
+
// OPENWOP_CONFORMANCE_ROOT when it finds a sibling checkout. True of that
|
|
137
|
+
// host, false of the artifact. The distinction only survives if something
|
|
138
|
+
// holds the artifact to it.
|
|
139
|
+
//
|
|
140
|
+
// The thing that would break it is a change that looks HELPFUL: adding
|
|
141
|
+
// `spec` to `files` so "the corpus tests run for consumers too". That would
|
|
142
|
+
// silently give npm consumers V1_DIR, the 28 would execute instead of
|
|
143
|
+
// flipping, and every host's numbers would shift with no failure anywhere.
|
|
144
|
+
it('the published package ships no spec/ — so V1_DIR is null for npm consumers', () => {
|
|
145
|
+
const pkg = JSON.parse(readFileSync(join(SCENARIOS, '../../package.json'), 'utf8')) as { files: string[] };
|
|
146
|
+
expect(
|
|
147
|
+
pkg.files.filter((f) => f === 'spec' || f.startsWith('spec/')),
|
|
148
|
+
'adding spec/ to `files` would give npm consumers a V1_DIR, so the spec-coherence rows would execute '
|
|
149
|
+
+ 'instead of reporting `inapplicable` — changing every host bundle with no test going red',
|
|
150
|
+
).toEqual([]);
|
|
151
|
+
// schemas/ IS shipped, and is what selects the `published` layout. If this
|
|
152
|
+
// ever stops being true the layout resolves to neither branch.
|
|
153
|
+
expect(pkg.files, 'schemas/ is what makes resolveLayout() pick `published`').toContain('schemas');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth-subject-link-key-class — RFC 0163: SCIM ⟷ SAML subject-linking
|
|
3
|
+
* hardening (the declarable, witnessable link-key class + the same-IdP
|
|
4
|
+
* trust-root MUST).
|
|
5
|
+
*
|
|
6
|
+
* Status: DRAFT. RFC 0163 is `Draft` (additive follow-on to RFC 0159, which
|
|
7
|
+
* is `Accepted`). The obligations are documented in `auth-profiles.md`
|
|
8
|
+
* §"Subject linking (SAML ⟷ SCIM)" → "Link-key-class declaration and same-IdP
|
|
9
|
+
* trust root — RFC 0163", and are discoverable via
|
|
10
|
+
* `capabilities.auth.subjectLinkKey` (a closed enum) under the same
|
|
11
|
+
* `capabilities.auth.subjectLinking` opt-in as RFC 0159.
|
|
12
|
+
*
|
|
13
|
+
* TWO LEGS:
|
|
14
|
+
* (§A) ADVERTISEMENT — server-free, runs whenever subjectLinking:true. A
|
|
15
|
+
* subjectLinking:true host MUST advertise a subjectLinkKey that is a
|
|
16
|
+
* member of the CLOSED enum {opaque-idp, oid, immutable-id}. Mutable/PII
|
|
17
|
+
* keys are inexpressible by construction, which is the witness that
|
|
18
|
+
* converts RFC 0159 §A.2/§A.4's negative-existence prohibition into a
|
|
19
|
+
* positive advertisement.
|
|
20
|
+
* (§B) SAME-IDP TRUST ROOT — behavioral. A SAML(IdP-A) + SCIM(IdP-B)
|
|
21
|
+
* identifier collision MUST NOT form a cross-lane link. This leg needs a
|
|
22
|
+
* TWO-TRUST-ROOT fixture (two distinct-entityID synthetic IdPs); it is
|
|
23
|
+
* DECLARED here and PHASED — soft-skips `blocked` until the fixture is
|
|
24
|
+
* engineered (RFC 0163 gap G-fixture), mirroring how RFC 0159 phased its
|
|
25
|
+
* cross-lane behavioral legs. Do not delete this describe block: it is
|
|
26
|
+
* the placeholder that records the requirement as declared-but-unwitnessed.
|
|
27
|
+
*
|
|
28
|
+
* @see RFCS/0163-subject-linking-hardening.md
|
|
29
|
+
* @see spec/v1/auth-profiles.md §"Subject linking (SAML ⟷ SCIM)"
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { describe, it, expect } from 'vitest';
|
|
33
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
34
|
+
import { driver } from '../lib/driver.js';
|
|
35
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
36
|
+
|
|
37
|
+
const SAML_PROFILE = 'openwop-auth-saml';
|
|
38
|
+
const SCIM_PROFILE = 'openwop-auth-scim';
|
|
39
|
+
const SUBJECT_LINK_KEY_CLASSES = ['opaque-idp', 'oid', 'immutable-id'] as const;
|
|
40
|
+
|
|
41
|
+
interface DiscoveryAuth {
|
|
42
|
+
profiles?: string[];
|
|
43
|
+
subjectLinking?: boolean;
|
|
44
|
+
subjectLinkKey?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface DiscoveryDoc {
|
|
48
|
+
capabilities?: { auth?: DiscoveryAuth };
|
|
49
|
+
extensions?: { auth?: DiscoveryAuth };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readAuth(): Promise<DiscoveryAuth | null> {
|
|
53
|
+
const res = await driver.get('/.well-known/openwop');
|
|
54
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
55
|
+
return capabilityFamily<DiscoveryAuth>(body, 'auth') ?? body?.extensions?.auth ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('auth-subject-link-key-class: advertisement shape (RFC 0163 §A)', () => {
|
|
59
|
+
it('subjectLinking:true requires a subjectLinkKey drawn from the closed safe-class enum', async () => {
|
|
60
|
+
const auth = await readAuth();
|
|
61
|
+
if (auth === null || auth.subjectLinking !== true) {
|
|
62
|
+
return softSkip('inapplicable', 'auth.subjectLinking not claimed');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// §A.1 — a subjectLinking:true host MUST advertise a subjectLinkKey.
|
|
66
|
+
expect(
|
|
67
|
+
typeof auth.subjectLinkKey === 'string' && auth.subjectLinkKey.length > 0,
|
|
68
|
+
driver.describe(
|
|
69
|
+
'auth-profiles.md §Subject linking',
|
|
70
|
+
'RFC 0163 §A.1: a host advertising capabilities.auth.subjectLinking:true MUST also advertise capabilities.auth.subjectLinkKey',
|
|
71
|
+
),
|
|
72
|
+
).toBe(true);
|
|
73
|
+
|
|
74
|
+
// §A.3 — the value MUST be a member of the CLOSED enum of allowed classes
|
|
75
|
+
// (opaque-idp | oid | immutable-id). Mutable/PII keys (email, userName,
|
|
76
|
+
// displayName) are absent from the enum by construction, so a conforming
|
|
77
|
+
// host cannot name one.
|
|
78
|
+
expect(
|
|
79
|
+
(SUBJECT_LINK_KEY_CLASSES as readonly string[]).includes(auth.subjectLinkKey ?? ''),
|
|
80
|
+
driver.describe(
|
|
81
|
+
'auth-profiles.md §Subject linking',
|
|
82
|
+
`RFC 0163 §A.3: subjectLinkKey MUST be one of ${SUBJECT_LINK_KEY_CLASSES.join(', ')} — a closed enum of allowed classes only; a mutable/PII key is inexpressible`,
|
|
83
|
+
),
|
|
84
|
+
).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('subjectLinkKey is only claimed alongside both SAML and SCIM profiles (rides the RFC 0159 §B gate)', async () => {
|
|
88
|
+
const auth = await readAuth();
|
|
89
|
+
if (auth === null || auth.subjectLinkKey === undefined) {
|
|
90
|
+
return softSkip('inapplicable', 'auth.subjectLinkKey not claimed');
|
|
91
|
+
}
|
|
92
|
+
const profiles = auth.profiles ?? [];
|
|
93
|
+
expect(
|
|
94
|
+
profiles.includes(SAML_PROFILE) && profiles.includes(SCIM_PROFILE),
|
|
95
|
+
driver.describe(
|
|
96
|
+
'auth-profiles.md §Subject linking',
|
|
97
|
+
'RFC 0163: subjectLinkKey is meaningful only when both openwop-auth-saml and openwop-auth-scim are advertised (it names the class the two lanes are joined on)',
|
|
98
|
+
),
|
|
99
|
+
).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('auth-subject-link-key-class: same-IdP trust root (RFC 0163 §B — PHASED, follow-on fixture)', () => {
|
|
104
|
+
it('a SAML(IdP-A) + SCIM(IdP-B) identifier collision MUST NOT form a cross-lane link', async () => {
|
|
105
|
+
const auth = await readAuth();
|
|
106
|
+
if (auth === null || auth.subjectLinking !== true) {
|
|
107
|
+
return softSkip('inapplicable', 'capability-gated on auth.subjectLinking');
|
|
108
|
+
}
|
|
109
|
+
// The §B behavioral leg needs a TWO-TRUST-ROOT fixture (two distinct-entityID
|
|
110
|
+
// synthetic IdPs minting a colliding identifier). That fixture is a declared
|
|
111
|
+
// follow-on (RFC 0163 gap G-fixture); until it is bundled this leg is
|
|
112
|
+
// `blocked` (RFC 0148 §A — unobservable, not unmet), exactly as RFC 0159
|
|
113
|
+
// phased its cross-lane behavioral legs. The requirement is recorded as
|
|
114
|
+
// declared-but-unwitnessed rather than silently omitted.
|
|
115
|
+
return softSkip('blocked', 'RFC 0163 §B two-trust-root fixture is a declared follow-on (gap G-fixture)');
|
|
116
|
+
});
|
|
117
|
+
});
|