@openwop/openwop-conformance 1.6.0 → 1.10.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 +18 -0
- package/README.md +2 -2
- package/api/asyncapi.yaml +74 -1
- package/api/openapi.yaml +316 -0
- package/coverage.md +16 -0
- package/fixtures/conformance-run-duration-breach.json +33 -0
- package/fixtures.md +19 -0
- package/package.json +1 -1
- package/schemas/README.md +12 -0
- package/schemas/agent-inventory-response.schema.json +90 -0
- package/schemas/ai-envelope.schema.json +28 -0
- package/schemas/annotation-create.schema.json +37 -0
- package/schemas/annotation.schema.json +56 -0
- package/schemas/artifact-type-pack-manifest.schema.json +160 -0
- package/schemas/capabilities.schema.json +195 -4
- package/schemas/chat-card-pack-manifest.schema.json +158 -0
- package/schemas/envelopes/media.audio.schema.json +38 -0
- package/schemas/envelopes/media.file.schema.json +37 -0
- package/schemas/envelopes/media.image.schema.json +33 -0
- package/schemas/heartbeat-evaluated.schema.json +14 -0
- package/schemas/heartbeat-state-changed.schema.json +14 -0
- package/schemas/node-pack-manifest.schema.json +16 -1
- package/schemas/run-event-payloads.schema.json +96 -5
- package/schemas/run-event.schema.json +4 -0
- package/schemas/workflow-definition.schema.json +5 -0
- package/schemas/workspace-file-create.schema.json +20 -0
- package/schemas/workspace-file.schema.json +39 -0
- package/src/lib/agentLoop.ts +44 -0
- package/src/lib/agentRuntime.ts +45 -0
- package/src/lib/artifactTypes.ts +96 -0
- package/src/lib/cardPacks.ts +52 -0
- package/src/lib/discovery-capabilities.ts +50 -0
- package/src/lib/distillation.ts +38 -0
- package/src/lib/feedback.ts +31 -0
- package/src/lib/heartbeat.ts +31 -0
- package/src/lib/memoryAttribution.ts +48 -0
- package/src/lib/subRunAttestation.ts +35 -0
- package/src/lib/toolHooks.ts +33 -0
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
- package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
- package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
- package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
- package/src/scenarios/ai-envelope-shape.test.ts +14 -18
- package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
- package/src/scenarios/approval-gate-flow.test.ts +4 -6
- package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
- package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
- package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
- package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
- package/src/scenarios/audit-log-integrity.test.ts +3 -2
- package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
- package/src/scenarios/auth-mtls.test.ts +2 -1
- package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
- package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
- package/src/scenarios/auth-saml-profile.test.ts +2 -1
- package/src/scenarios/auth-scim-profile.test.ts +2 -1
- package/src/scenarios/authorization-fail-closed.test.ts +2 -1
- package/src/scenarios/authorization-roles-shape.test.ts +2 -1
- package/src/scenarios/byok-auth-modes.test.ts +141 -0
- package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
- package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
- package/src/scenarios/commitment-fired.test.ts +83 -0
- package/src/scenarios/credential-payload-redaction.test.ts +2 -1
- package/src/scenarios/credentials-capability-shape.test.ts +2 -1
- package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
- package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
- package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
- package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
- package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
- package/src/scenarios/distillation-shape.test.ts +41 -0
- package/src/scenarios/distillation-stable-archive.test.ts +37 -0
- package/src/scenarios/distillation-token-budget.test.ts +45 -0
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
- package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
- package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
- package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
- package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
- package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
- package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
- package/src/scenarios/experimental-tier-shape.test.ts +5 -4
- package/src/scenarios/feedback-capability-shape.test.ts +35 -0
- package/src/scenarios/feedback-correction-redaction.test.ts +35 -0
- package/src/scenarios/feedback-cross-tenant-isolation.test.ts +37 -0
- package/src/scenarios/feedback-fork-not-copied.test.ts +40 -0
- package/src/scenarios/feedback-on-terminal-run.test.ts +32 -0
- package/src/scenarios/feedback-record-and-list.test.ts +32 -0
- package/src/scenarios/feedback-unsupported-501.test.ts +32 -0
- package/src/scenarios/fs-path-traversal.test.ts +2 -1
- package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
- package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
- package/src/scenarios/http-client-ssrf.test.ts +10 -13
- package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
- package/src/scenarios/media-url-inline-cap.test.ts +167 -0
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
- package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
- package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
- package/src/scenarios/memory-attribution-shape.test.ts +28 -0
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
- package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
- package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
- package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
- package/src/scenarios/model-capability-substituted.test.ts +2 -1
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
- package/src/scenarios/multi-region-idempotency.test.ts +10 -10
- package/src/scenarios/oauth-capability-shape.test.ts +2 -1
- package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
- package/src/scenarios/pause-resume.test.ts +3 -3
- package/src/scenarios/production-backpressure.test.ts +2 -2
- package/src/scenarios/production-retention-expiry.test.ts +2 -2
- package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
- package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
- package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
- package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
- package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
- package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
- package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
- package/src/scenarios/prompt-pack-install.test.ts +2 -1
- package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
- package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
- package/src/scenarios/prompt-template-shape.test.ts +2 -1
- package/src/scenarios/provider-usage.test.ts +2 -1
- package/src/scenarios/redaction.test.ts +4 -1
- package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
- package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
- package/src/scenarios/replayDeterminism.test.ts +3 -1
- package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
- package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
- package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
- package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
- package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
- package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
- package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
- package/src/scenarios/spec-corpus-validity.test.ts +4 -1
- package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
- package/src/scenarios/subrun-approval-gate.test.ts +35 -0
- package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
- package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
- package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
- package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
- package/src/scenarios/tool-hooks-shape.test.ts +34 -0
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
- package/src/scenarios/wasm-pack-load.test.ts +2 -2
- package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
- package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
- package/src/scenarios/workspace-behavior.test.ts +134 -0
- package/src/scenarios/workspace-capability-shape.test.ts +73 -0
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded artifact-schema compilation (RFC 0071, `Active`).
|
|
3
|
+
*
|
|
4
|
+
* Always-on, server-free assertion for the SECURITY invariant
|
|
5
|
+
* `artifact-schema-compile-bounded`. Artifact-type packs ship third-party
|
|
6
|
+
* JSON Schemas that the engine compiles (Ajv) at install + validation time;
|
|
7
|
+
* an unbounded compile is a denial-of-service vector (schema bombs:
|
|
8
|
+
* pathological `$ref` recursion, keyword-count explosion, oversized payloads,
|
|
9
|
+
* catastrophic-backtracking `pattern`s). This scenario asserts two things
|
|
10
|
+
* that must hold for every release regardless of which host runs it:
|
|
11
|
+
*
|
|
12
|
+
* PART 1 — contract present. `artifact-type-packs.md` carries the normative
|
|
13
|
+
* bounded-compilation MUST (serialized-size, `$ref`-depth, keyword-count
|
|
14
|
+
* bounds + wall-clock timeout), and `host-capabilities.md` §host.artifactTypes
|
|
15
|
+
* references it. Guards against the requirement being silently dropped.
|
|
16
|
+
*
|
|
17
|
+
* PART 2 — defense is well-defined + implementable. A reference bounding
|
|
18
|
+
* predicate built from representative finite limits rejects three schema
|
|
19
|
+
* bombs and admits a benign artifact schema. The specific numeric limits are
|
|
20
|
+
* host-configurable per the spec (advertised, not protocol-mandated); the
|
|
21
|
+
* point is that *some* finite bound exists and catches the bombs while
|
|
22
|
+
* passing legitimate schemas.
|
|
23
|
+
*
|
|
24
|
+
* The behavioral end-to-end form (a host rejects an over-bounds pack at
|
|
25
|
+
* registry `PUT` with `pack_validation_failed`) is capability-gated on
|
|
26
|
+
* `host.artifactTypes.supported` and is `host-pending` until a reference host
|
|
27
|
+
* lands; this server-free scenario is the always-on floor.
|
|
28
|
+
*
|
|
29
|
+
* @see spec/v1/artifact-type-packs.md §"Bounded schema compilation (normative)"
|
|
30
|
+
* @see SECURITY/threat-model-node-packs.md §"Distributed artifact schemas"
|
|
31
|
+
* @see RFCS/0071-artifact-type-and-chat-card-packs.md
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { describe, it, expect } from 'vitest';
|
|
35
|
+
import { readFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import { V1_DIR } from '../lib/paths.js';
|
|
38
|
+
|
|
39
|
+
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
40
|
+
|
|
41
|
+
describe('artifact-schema-compile-bounded: contract present in the corpus (RFC 0071, server-free)', () => {
|
|
42
|
+
const artifactDoc = V1_DIR ? readFileSync(join(V1_DIR, 'artifact-type-packs.md'), 'utf8') : '';
|
|
43
|
+
const hostCaps = V1_DIR ? readFileSync(join(V1_DIR, 'host-capabilities.md'), 'utf8') : '';
|
|
44
|
+
|
|
45
|
+
it.skipIf(V1_DIR === null)('artifact-type-packs.md declares the bounded-compilation MUST', () => {
|
|
46
|
+
expect(
|
|
47
|
+
/Bounded schema compilation/i.test(artifactDoc),
|
|
48
|
+
why('artifact-type-packs.md', 'a "Bounded schema compilation" section MUST exist'),
|
|
49
|
+
).toBe(true);
|
|
50
|
+
expect(
|
|
51
|
+
/MUST bound/i.test(artifactDoc) && /MUST reject/i.test(artifactDoc),
|
|
52
|
+
why('artifact-type-packs.md §"Bounded schema compilation"', 'host MUST bound + MUST reject over-limit schemas'),
|
|
53
|
+
).toBe(true);
|
|
54
|
+
// The three structural axes + the timeout MUST all be named.
|
|
55
|
+
for (const axis of [/byte size/i, /\$ref/i, /keyword/i, /timeout/i]) {
|
|
56
|
+
expect(axis.test(artifactDoc), why('artifact-type-packs.md', `bound axis ${axis} MUST be named`)).toBe(true);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it.skipIf(V1_DIR === null)('host-capabilities.md §host.artifactTypes references the bound', () => {
|
|
61
|
+
expect(
|
|
62
|
+
/artifact-schema-compile-bounded/.test(hostCaps),
|
|
63
|
+
why('host-capabilities.md §host.artifactTypes', 'MUST reference the bounded-compilation invariant'),
|
|
64
|
+
).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('artifact-schema-compile-bounded: a finite bound catches schema bombs (RFC 0071, server-free)', () => {
|
|
69
|
+
// Representative, host-configurable limits (the spec leaves the exact values
|
|
70
|
+
// to host advertisement; these stand in for "some finite bound").
|
|
71
|
+
const LIMITS = { maxBytes: 64 * 1024, maxRefDepth: 16, maxKeywords: 2000 };
|
|
72
|
+
|
|
73
|
+
function refDepth(node: unknown, seen = 0): number {
|
|
74
|
+
if (node === null || typeof node !== 'object') return seen;
|
|
75
|
+
const obj = node as Record<string, unknown>;
|
|
76
|
+
const here = '$ref' in obj ? seen + 1 : seen;
|
|
77
|
+
let max = here;
|
|
78
|
+
for (const v of Object.values(obj)) max = Math.max(max, refDepth(v, here));
|
|
79
|
+
return max;
|
|
80
|
+
}
|
|
81
|
+
function keywordCount(node: unknown): number {
|
|
82
|
+
if (node === null || typeof node !== 'object') return 0;
|
|
83
|
+
const obj = node as Record<string, unknown>;
|
|
84
|
+
let n = Object.keys(obj).length;
|
|
85
|
+
for (const v of Object.values(obj)) n += keywordCount(v);
|
|
86
|
+
return n;
|
|
87
|
+
}
|
|
88
|
+
/** Reference bound predicate — the shape a conformant host applies at PUT/install. */
|
|
89
|
+
function exceedsBounds(schema: unknown): boolean {
|
|
90
|
+
const bytes = Buffer.byteLength(JSON.stringify(schema), 'utf8');
|
|
91
|
+
if (bytes > LIMITS.maxBytes) return true;
|
|
92
|
+
if (refDepth(schema) > LIMITS.maxRefDepth) return true;
|
|
93
|
+
if (keywordCount(schema) > LIMITS.maxKeywords) return true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
it('admits a benign artifact schema', () => {
|
|
98
|
+
const benign = {
|
|
99
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
100
|
+
$id: 'https://h.example/schemas/artifacts/vendor.acme.cad.model.schema.json',
|
|
101
|
+
type: 'object',
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
required: ['name'],
|
|
104
|
+
properties: { name: { type: 'string' }, dims: { type: 'array', items: { type: 'number' } } },
|
|
105
|
+
};
|
|
106
|
+
expect(exceedsBounds(benign), why('artifact-type-packs.md', 'a legitimate artifact schema MUST NOT be rejected')).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('rejects a $ref-depth bomb', () => {
|
|
110
|
+
// Nest $ref-bearing objects deeper than maxRefDepth so resolution depth accumulates.
|
|
111
|
+
let node: Record<string, unknown> = { type: 'string' };
|
|
112
|
+
for (let i = 0; i < LIMITS.maxRefDepth + 4; i++) node = { $ref: '#/x', properties: { nested: node } };
|
|
113
|
+
expect(exceedsBounds({ type: 'object', properties: { deep: node } }), why('threat-model-node-packs.md', 'a $ref-depth bomb MUST be rejected')).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects a keyword-count bomb', () => {
|
|
117
|
+
const props: Record<string, unknown> = {};
|
|
118
|
+
for (let i = 0; i < LIMITS.maxKeywords + 100; i++) props[`p${i}`] = { type: 'string' };
|
|
119
|
+
expect(exceedsBounds({ type: 'object', properties: props }), why('threat-model-node-packs.md', 'a keyword-count bomb MUST be rejected')).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('rejects an oversized schema', () => {
|
|
123
|
+
const huge = { type: 'object', description: 'x'.repeat(LIMITS.maxBytes + 1) };
|
|
124
|
+
expect(exceedsBounds(huge), why('threat-model-node-packs.md', 'an over-size schema MUST be rejected')).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifact-type-pack-install — RFC 0071 Phase 1 §"Binding the existing artifact
|
|
3
|
+
* surfaces". A host that advertises `host.artifactTypes` installs an
|
|
4
|
+
* artifact-type pack, then produces an artifact of a registered type:
|
|
5
|
+
*
|
|
6
|
+
* - a payload that conforms to the pack schema is stored and surfaces an
|
|
7
|
+
* `artifact.created` with `registered: true` (validated against the pack);
|
|
8
|
+
* - a payload that violates the schema is rejected (not stored, no
|
|
9
|
+
* `registered: true` artifact.created).
|
|
10
|
+
*
|
|
11
|
+
* Gated on `capabilities.host.artifactTypes.supported` + the host-sample
|
|
12
|
+
* install/produce seam; soft-skips when either is absent (`host-pending`
|
|
13
|
+
* until a reference host wires RFC 0071 — see the migration request at
|
|
14
|
+
* docs/openwop-adoption/0071-artifact-type-packs-migration-request.md).
|
|
15
|
+
*
|
|
16
|
+
* @see spec/v1/artifact-type-packs.md §"Binding the existing artifact surfaces"
|
|
17
|
+
* @see RFCS/0071-artifact-type-and-chat-card-packs.md
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from 'vitest';
|
|
21
|
+
import { driver } from '../lib/driver.js';
|
|
22
|
+
import {
|
|
23
|
+
readArtifactTypesCap,
|
|
24
|
+
artifactTypesSupported,
|
|
25
|
+
installArtifactTypePack,
|
|
26
|
+
produceArtifact,
|
|
27
|
+
sampleArtifactTypePack,
|
|
28
|
+
} from '../lib/artifactTypes.js';
|
|
29
|
+
|
|
30
|
+
describe('artifact-type-pack-install: registered artifacts are schema-validated (RFC 0071)', () => {
|
|
31
|
+
it('a conforming payload yields artifact.created { registered: true }', async () => {
|
|
32
|
+
if (!artifactTypesSupported(await readArtifactTypesCap())) return; // unadvertised — soft-skip
|
|
33
|
+
const { artifactTypeId, manifest, schema } = sampleArtifactTypePack();
|
|
34
|
+
|
|
35
|
+
const installed = await installArtifactTypePack(manifest, { [artifactTypeId]: schema });
|
|
36
|
+
if (installed === null) return; // seam absent — soft-skip
|
|
37
|
+
expect(
|
|
38
|
+
installed.status >= 200 && installed.status < 300,
|
|
39
|
+
driver.describe('artifact-type-packs.md §"Pack kind"', 'a valid artifact-type pack MUST install cleanly'),
|
|
40
|
+
).toBe(true);
|
|
41
|
+
|
|
42
|
+
const produced = await produceArtifact(artifactTypeId, { title: 'Hello', body: 'World' });
|
|
43
|
+
if (produced === null) return; // seam absent — soft-skip
|
|
44
|
+
expect(
|
|
45
|
+
produced.json['registered'],
|
|
46
|
+
driver.describe('artifact-type-packs.md §"Binding the existing artifact surfaces"', 'a payload matching a registered artifactTypeId MUST be marked registered'),
|
|
47
|
+
).toBe(true);
|
|
48
|
+
expect(
|
|
49
|
+
produced.json['validated'],
|
|
50
|
+
driver.describe('artifact-type-packs.md', 'the host MUST validate the payload against the pack schema before emitting artifact.created'),
|
|
51
|
+
).toBe(true);
|
|
52
|
+
const evt = produced.json['artifactCreated'] as { registered?: unknown } | undefined;
|
|
53
|
+
if (evt && 'registered' in evt) {
|
|
54
|
+
expect(
|
|
55
|
+
evt.registered,
|
|
56
|
+
driver.describe('run-event-payloads.schema.json §artifactCreated', 'artifact.created.registered MUST be true for a validated registered artifact'),
|
|
57
|
+
).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('a schema-violating payload is rejected (not stored as a validated registered artifact)', async () => {
|
|
62
|
+
if (!artifactTypesSupported(await readArtifactTypesCap())) return;
|
|
63
|
+
const { artifactTypeId, manifest, schema } = sampleArtifactTypePack();
|
|
64
|
+
if ((await installArtifactTypePack(manifest, { [artifactTypeId]: schema })) === null) return;
|
|
65
|
+
|
|
66
|
+
// `body` missing + a foreign key → fails additionalProperties:false + required.
|
|
67
|
+
const produced = await produceArtifact(artifactTypeId, { title: 'Hello', extra: true });
|
|
68
|
+
if (produced === null) return;
|
|
69
|
+
const rejected =
|
|
70
|
+
produced.status >= 400 ||
|
|
71
|
+
produced.json['validated'] === false ||
|
|
72
|
+
produced.json['stored'] === false;
|
|
73
|
+
expect(
|
|
74
|
+
rejected,
|
|
75
|
+
driver.describe('artifact-type-packs.md §"Binding the existing artifact surfaces"', 'a payload that fails the pack schema MUST NOT be emitted as a validated registered artifact'),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Artifact-type pack manifest validation — `artifact-type-packs.md` §"Manifest format"
|
|
3
|
+
* + `schemas/artifact-type-pack-manifest.schema.json` (RFC 0071 Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* Server-free schema-validation scenario. Exercises the new
|
|
6
|
+
* `artifact-type-pack-manifest.schema.json` with a positive sample and the
|
|
7
|
+
* negative samples derived from the RFC's "Examples" section that are
|
|
8
|
+
* expressible at the JSON-Schema layer:
|
|
9
|
+
*
|
|
10
|
+
* 1. Positive: a valid `kind: "artifact-type"` manifest with a single
|
|
11
|
+
* `artifactTypes[]` entry validates cleanly.
|
|
12
|
+
* 2. Negative — kind/contents mismatch: a manifest carrying BOTH
|
|
13
|
+
* `artifactTypes[]` AND `nodes[]` is rejected. Surface-level outcome at
|
|
14
|
+
* the registry HTTP API is `pack_kind_invalid` per the spec;
|
|
15
|
+
* schema-level outcome is an `additionalProperties` violation on
|
|
16
|
+
* `nodes` (this schema does not declare that field).
|
|
17
|
+
* 3. Negative — empty `artifactTypes[]`: rejected with a `minItems`
|
|
18
|
+
* violation (a pack MUST declare at least one artifact type).
|
|
19
|
+
* 4. Negative — invalid `artifactTypeId`: a value that does not match the
|
|
20
|
+
* reverse-DNS pattern (e.g. an uppercase scope) is rejected with a
|
|
21
|
+
* `pattern` violation.
|
|
22
|
+
* 5. Negative — unknown `rendering.display`: a value outside the closed
|
|
23
|
+
* enum (`"3d-viewport"`) is rejected with an `enum` violation
|
|
24
|
+
* (the RenderingHint vocabulary is reused from RFC 0055, `card` excluded).
|
|
25
|
+
* 6. Negative — non-conforming `exportFormats` identifier: an uppercase /
|
|
26
|
+
* unprefixed value is rejected with a `pattern` violation (reserved-core
|
|
27
|
+
* + `vendor.*`/`x-` extension idiom).
|
|
28
|
+
*
|
|
29
|
+
* NOTE: the RFC's "core scope published from a non-core account" negative is a
|
|
30
|
+
* registry-PUT enforcement rule (account ↔ scope binding), NOT a schema
|
|
31
|
+
* constraint — `core.*` is a valid `artifactTypeId` pattern. It is therefore
|
|
32
|
+
* not asserted here; it belongs to the capability-gated publish scenario.
|
|
33
|
+
*
|
|
34
|
+
* Capability-gated end-to-end scenarios (install + validate; store-without-
|
|
35
|
+
* render negotiation) are deferred and gate on `host.artifactTypes.supported`
|
|
36
|
+
* per the RFC; behavior grade is `host-pending` until a reference host lands.
|
|
37
|
+
*
|
|
38
|
+
* @see spec/v1/artifact-type-packs.md
|
|
39
|
+
* @see schemas/artifact-type-pack-manifest.schema.json
|
|
40
|
+
* @see RFCS/0071-artifact-type-and-chat-card-packs.md
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { describe, it, expect } from 'vitest';
|
|
44
|
+
import { readFileSync } from 'node:fs';
|
|
45
|
+
import { join } from 'node:path';
|
|
46
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
47
|
+
import addFormats from 'ajv-formats';
|
|
48
|
+
import type { ErrorObject } from 'ajv';
|
|
49
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
50
|
+
|
|
51
|
+
const SCHEMA_PATH = join(SCHEMAS_DIR, 'artifact-type-pack-manifest.schema.json');
|
|
52
|
+
|
|
53
|
+
function validManifest() {
|
|
54
|
+
return {
|
|
55
|
+
kind: 'artifact-type',
|
|
56
|
+
name: 'vendor.acme.cad',
|
|
57
|
+
version: '1.0.0',
|
|
58
|
+
engines: { openwop: '>=1.1 <2.0.0' },
|
|
59
|
+
artifactTypes: [
|
|
60
|
+
{
|
|
61
|
+
artifactTypeId: 'vendor.acme.cad.model',
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
schemaRef: 'schemas/cad-model.schema.json',
|
|
64
|
+
rendering: { display: 'file', mimeType: 'model/step' },
|
|
65
|
+
exportFormats: ['step', 'stl', 'pdf'],
|
|
66
|
+
syncOn: 'completion',
|
|
67
|
+
supportsCheckpoint: true,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('category: artifact-type-pack manifest validation', () => {
|
|
74
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
75
|
+
addFormats(ajv);
|
|
76
|
+
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
77
|
+
const validate = ajv.compile(schema);
|
|
78
|
+
|
|
79
|
+
const failsWith = (manifest: unknown, keyword: string): ErrorObject[] => {
|
|
80
|
+
const ok = validate(manifest);
|
|
81
|
+
expect(ok).toBe(false);
|
|
82
|
+
const errs = (validate.errors ?? []).filter((e) => e.keyword === keyword);
|
|
83
|
+
return errs;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
it('positive: a valid artifact-type pack manifest validates cleanly', () => {
|
|
87
|
+
const ok = validate(validManifest());
|
|
88
|
+
expect(
|
|
89
|
+
ok,
|
|
90
|
+
`artifact-type-packs.md §"Manifest format": a well-formed kind:"artifact-type" manifest MUST validate. Errors: ${JSON.stringify(validate.errors)}`,
|
|
91
|
+
).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('negative: a manifest mixing artifactTypes[] and nodes[] is rejected (pack_kind_invalid at the registry)', () => {
|
|
95
|
+
const manifest = { ...validManifest(), nodes: [{ typeId: 'vendor.acme.x', version: '1.0.0', category: 'data', role: 'pure' }] };
|
|
96
|
+
const errs = failsWith(manifest, 'additionalProperties');
|
|
97
|
+
expect(
|
|
98
|
+
errs.some((e) => (e.params as { additionalProperty?: string }).additionalProperty === 'nodes'),
|
|
99
|
+
'artifact-type-packs.md §"Pack kind": one kind per pack — a foreign `nodes[]` field MUST be rejected (additionalProperties:false)',
|
|
100
|
+
).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('negative: an empty artifactTypes[] is rejected (a pack MUST declare ≥1 type)', () => {
|
|
104
|
+
const manifest = { ...validManifest(), artifactTypes: [] };
|
|
105
|
+
const errs = failsWith(manifest, 'minItems');
|
|
106
|
+
expect(errs.length, 'artifact-type-pack-manifest.schema.json: artifactTypes minItems:1').toBeGreaterThan(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('negative: an artifactTypeId that is not reverse-DNS scoped is rejected', () => {
|
|
110
|
+
const manifest = validManifest();
|
|
111
|
+
manifest.artifactTypes[0]!.artifactTypeId = 'Vendor.Acme.Model'; // uppercase scope
|
|
112
|
+
const errs = failsWith(manifest, 'pattern');
|
|
113
|
+
expect(errs.length, 'artifact-type-packs.md: artifactTypeId MUST match the reverse-DNS pattern').toBeGreaterThan(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('negative: an unknown rendering.display value is rejected (closed RFC 0055 enum, card excluded)', () => {
|
|
117
|
+
const manifest = validManifest();
|
|
118
|
+
(manifest.artifactTypes[0]!.rendering as { display: string }).display = '3d-viewport';
|
|
119
|
+
const errs = failsWith(manifest, 'enum');
|
|
120
|
+
expect(errs.length, 'artifact-type-packs.md: rendering.display reuses the closed ai-envelope §"Rendering hints" enum').toBeGreaterThan(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('negative: a non-conforming exportFormats identifier is rejected (reserved-core + vendor.*/x- only)', () => {
|
|
124
|
+
const manifest = validManifest();
|
|
125
|
+
manifest.artifactTypes[0]!.exportFormats = ['PPTX']; // uppercase, unprefixed
|
|
126
|
+
const errs = failsWith(manifest, 'pattern');
|
|
127
|
+
expect(errs.length, 'artifact-type-packs.md: exportFormats identifiers are lowercase core ids OR vendor.*/x- extensions').toBeGreaterThan(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('positive: the validation field accepts "open"/"closed" and rejects other values (RFC 0075)', () => {
|
|
131
|
+
for (const v of ['open', 'closed']) {
|
|
132
|
+
const m = validManifest();
|
|
133
|
+
(m.artifactTypes[0] as Record<string, unknown>).validation = v;
|
|
134
|
+
expect(validate(m), `artifact-type-packs.md §validation: "${v}" MUST validate (RFC 0075)`).toBe(true);
|
|
135
|
+
}
|
|
136
|
+
const bad = validManifest();
|
|
137
|
+
(bad.artifactTypes[0] as Record<string, unknown>).validation = 'lenient';
|
|
138
|
+
expect(failsWith(bad, 'enum').length, 'validation MUST be open|closed').toBeGreaterThan(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifact-type-store-without-render — RFC 0071 Phase 1 §host.artifactTypes.
|
|
3
|
+
* The cross-host negotiation guarantee: a host that can STORE an artifact type
|
|
4
|
+
* but cannot RENDER it MUST still accept + store the artifact and MUST NOT fail
|
|
5
|
+
* the run for lack of a renderer. An artifact produced on a richly-rendering
|
|
6
|
+
* host stays storable + forwardable + inspectable on a store-only host.
|
|
7
|
+
*
|
|
8
|
+
* Gated on `host.artifactTypes.supported` AND the advertised facets
|
|
9
|
+
* `store: true, render: false` (a host that renders everything can't exercise
|
|
10
|
+
* this path — it soft-skips), plus the host-sample produce seam. `host-pending`
|
|
11
|
+
* until a reference host advertises a store-without-render posture.
|
|
12
|
+
*
|
|
13
|
+
* @see spec/v1/artifact-type-packs.md §host.artifactTypes
|
|
14
|
+
* @see spec/v1/host-capabilities.md §host.artifactTypes
|
|
15
|
+
* @see RFCS/0071-artifact-type-and-chat-card-packs.md
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest';
|
|
19
|
+
import { driver } from '../lib/driver.js';
|
|
20
|
+
import {
|
|
21
|
+
readArtifactTypesCap,
|
|
22
|
+
artifactTypesSupported,
|
|
23
|
+
installArtifactTypePack,
|
|
24
|
+
produceArtifact,
|
|
25
|
+
sampleArtifactTypePack,
|
|
26
|
+
} from '../lib/artifactTypes.js';
|
|
27
|
+
|
|
28
|
+
describe('artifact-type-store-without-render: store-only hosts must not fail the run (RFC 0071)', () => {
|
|
29
|
+
it('a stored-but-unrendered artifact completes the run', async () => {
|
|
30
|
+
const cap = await readArtifactTypesCap();
|
|
31
|
+
if (!artifactTypesSupported(cap)) return; // unadvertised — soft-skip
|
|
32
|
+
// Only meaningful for a host that stores but does NOT render.
|
|
33
|
+
if (cap?.['store'] !== true || cap?.['render'] !== false) return; // not a store-without-render host — soft-skip
|
|
34
|
+
|
|
35
|
+
const { artifactTypeId, manifest, schema } = sampleArtifactTypePack();
|
|
36
|
+
if ((await installArtifactTypePack(manifest, { [artifactTypeId]: schema })) === null) return;
|
|
37
|
+
|
|
38
|
+
const produced = await produceArtifact(artifactTypeId, { title: 'Stored', body: 'Not rendered here' });
|
|
39
|
+
if (produced === null) return; // seam absent — soft-skip
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
produced.json['stored'],
|
|
43
|
+
driver.describe('artifact-type-packs.md §host.artifactTypes', 'a host advertising store:true MUST persist the artifact'),
|
|
44
|
+
).toBe(true);
|
|
45
|
+
expect(
|
|
46
|
+
produced.json['rendered'],
|
|
47
|
+
driver.describe('artifact-type-packs.md §host.artifactTypes', 'render:false host MUST NOT render'),
|
|
48
|
+
).toBe(false);
|
|
49
|
+
expect(
|
|
50
|
+
produced.json['runStatus'],
|
|
51
|
+
driver.describe('artifact-type-packs.md §host.artifactTypes', 'a host MUST NOT fail the run solely because it lacks a renderer for a stored artifact type'),
|
|
52
|
+
).toBe('completed');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { describe, it, expect } from 'vitest';
|
|
19
19
|
import { driver } from '../lib/driver.js';
|
|
20
20
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
21
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
21
22
|
|
|
22
23
|
interface AuditIntegrityCaps {
|
|
23
24
|
hashChain?: boolean;
|
|
@@ -34,7 +35,7 @@ interface AuthCaps {
|
|
|
34
35
|
|
|
35
36
|
async function isProfileAdvertised(): Promise<boolean> {
|
|
36
37
|
const disco = await driver.get('/.well-known/openwop');
|
|
37
|
-
const auth = (disco.json
|
|
38
|
+
const auth = capabilityFamily<AuthCaps>(disco.json, 'auth') ?? {};
|
|
38
39
|
return Array.isArray(auth.profiles) && auth.profiles.includes('openwop-audit-log-integrity');
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -46,7 +47,7 @@ describe('audit-log-integrity: profile shape', () => {
|
|
|
46
47
|
|
|
47
48
|
const disco = await driver.get('/.well-known/openwop');
|
|
48
49
|
const integrity =
|
|
49
|
-
(disco.json
|
|
50
|
+
capabilityFamily<AuthCaps>(disco.json, 'auth')
|
|
50
51
|
?.auditLogIntegrity ?? {};
|
|
51
52
|
|
|
52
53
|
expect(integrity.hashChain, driver.describe(
|
|
@@ -28,6 +28,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
29
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
30
30
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
31
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
31
32
|
|
|
32
33
|
interface RotationCaps {
|
|
33
34
|
supported?: boolean;
|
|
@@ -45,7 +46,7 @@ const CANARY = 'hk_openwop_canary_d1d2d3d4_NOT_A_REAL_KEY';
|
|
|
45
46
|
|
|
46
47
|
async function readAuthCaps(): Promise<AuthCaps | undefined> {
|
|
47
48
|
const disco = await driver.get('/.well-known/openwop');
|
|
48
|
-
return (disco.json as { capabilities?: { auth?: AuthCaps } })
|
|
49
|
+
return capabilityFamily((disco.json as { capabilities?: { auth?: AuthCaps } }), 'auth');
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function isProfileAdvertised(auth: AuthCaps | undefined): boolean {
|
|
@@ -45,6 +45,7 @@ import { driver } from '../lib/driver.js';
|
|
|
45
45
|
import { loadEnv } from '../lib/env.js';
|
|
46
46
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
47
47
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
48
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
48
49
|
|
|
49
50
|
interface MtlsCaps {
|
|
50
51
|
supported?: boolean;
|
|
@@ -74,7 +75,7 @@ interface HttpsResponse {
|
|
|
74
75
|
|
|
75
76
|
async function readAuthCaps(): Promise<AuthCaps | undefined> {
|
|
76
77
|
const disco = await driver.get('/.well-known/openwop');
|
|
77
|
-
return (disco.json as { capabilities?: { auth?: AuthCaps } })
|
|
78
|
+
return capabilityFamily((disco.json as { capabilities?: { auth?: AuthCaps } }), 'auth');
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function isProfileAdvertised(auth: AuthCaps | undefined): boolean {
|
|
@@ -33,6 +33,7 @@ import { driver } from '../lib/driver.js';
|
|
|
33
33
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
34
34
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
35
35
|
import { createSyntheticOIDCIssuer } from '../lib/oidc-issuer.js';
|
|
36
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
36
37
|
|
|
37
38
|
interface OAuth2Caps {
|
|
38
39
|
supported?: boolean;
|
|
@@ -51,7 +52,7 @@ const FIXTURE = 'conformance-noop';
|
|
|
51
52
|
|
|
52
53
|
async function readAuthCaps(): Promise<AuthCaps | undefined> {
|
|
53
54
|
const disco = await driver.get('/.well-known/openwop');
|
|
54
|
-
return (disco.json as { capabilities?: { auth?: AuthCaps } })
|
|
55
|
+
return capabilityFamily((disco.json as { capabilities?: { auth?: AuthCaps } }), 'auth');
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function isProfileAdvertised(auth: AuthCaps | undefined): boolean {
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
createSyntheticOIDCIssuer,
|
|
48
48
|
type SyntheticOIDCIssuer,
|
|
49
49
|
} from '../lib/oidc-issuer.js';
|
|
50
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
50
51
|
|
|
51
52
|
interface OIDCCaps {
|
|
52
53
|
supported?: boolean;
|
|
@@ -66,7 +67,7 @@ const FIXTURE = 'conformance-noop';
|
|
|
66
67
|
|
|
67
68
|
async function readAuthCaps(): Promise<AuthCaps | undefined> {
|
|
68
69
|
const disco = await driver.get('/.well-known/openwop');
|
|
69
|
-
return (disco.json as { capabilities?: { auth?: AuthCaps } })
|
|
70
|
+
return capabilityFamily((disco.json as { capabilities?: { auth?: AuthCaps } }), 'auth');
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
function isProfileAdvertised(auth: AuthCaps | undefined): boolean {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { describe, it, expect } from 'vitest';
|
|
21
21
|
import { driver } from '../lib/driver.js';
|
|
22
22
|
import { createSyntheticSamlIdp, type SamlVariant } from '../lib/saml-idp.js';
|
|
23
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
23
24
|
|
|
24
25
|
const SAML_PROFILE = 'openwop-auth-saml';
|
|
25
26
|
|
|
@@ -35,7 +36,7 @@ interface DiscoveryDoc {
|
|
|
35
36
|
async function readProfiles(): Promise<string[] | null> {
|
|
36
37
|
const res = await driver.get('/.well-known/openwop');
|
|
37
38
|
const body = res.json as DiscoveryDoc | undefined;
|
|
38
|
-
return body
|
|
39
|
+
return capabilityFamily<{ profiles?: string[] }>(body, 'auth')?.profiles ?? body?.extensions?.auth?.profiles ?? null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
describe('auth-saml-profile: advertisement shape (RFC 0050)', () => {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect } from 'vitest';
|
|
19
19
|
import { driver } from '../lib/driver.js';
|
|
20
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
20
21
|
|
|
21
22
|
const SCIM_PROFILE = 'openwop-auth-scim';
|
|
22
23
|
|
|
@@ -32,7 +33,7 @@ interface DiscoveryDoc {
|
|
|
32
33
|
async function readProfiles(): Promise<string[] | null> {
|
|
33
34
|
const res = await driver.get('/.well-known/openwop');
|
|
34
35
|
const body = res.json as DiscoveryDoc | undefined;
|
|
35
|
-
return body
|
|
36
|
+
return capabilityFamily<{ profiles?: string[] }>(body, 'auth')?.profiles ?? body?.extensions?.auth?.profiles ?? null;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
describe('auth-scim-profile: advertisement shape (RFC 0050)', () => {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { describe, it, expect } from 'vitest';
|
|
26
26
|
import { driver } from '../lib/driver.js';
|
|
27
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
27
28
|
|
|
28
29
|
interface DiscoveryAuthorization {
|
|
29
30
|
supported?: boolean;
|
|
@@ -39,7 +40,7 @@ interface DiscoveryDoc {
|
|
|
39
40
|
async function readAuthorization(): Promise<DiscoveryAuthorization | null> {
|
|
40
41
|
const res = await driver.get('/.well-known/openwop');
|
|
41
42
|
const body = res.json as DiscoveryDoc | undefined;
|
|
42
|
-
return body
|
|
43
|
+
return capabilityFamily(body, 'authorization') ?? null;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
describe('authorization-fail-closed: advertisement shape (RFC 0049 §C)', () => {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
23
24
|
|
|
24
25
|
interface DiscoveryRole {
|
|
25
26
|
role?: string;
|
|
@@ -41,7 +42,7 @@ interface DiscoveryDoc {
|
|
|
41
42
|
async function readAuthorization(): Promise<DiscoveryAuthorization | null> {
|
|
42
43
|
const res = await driver.get('/.well-known/openwop');
|
|
43
44
|
const body = res.json as DiscoveryDoc | undefined;
|
|
44
|
-
return body
|
|
45
|
+
return capabilityFamily(body, 'authorization') ?? null;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
describe('authorization-roles-shape: advertisement shape (RFC 0049 §A)', () => {
|