@openwop/openwop-conformance 2.45.10 → 2.45.12

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.
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
3
3
  "package": "@openwop/spec-artifacts",
4
- "version": "2.45.10",
5
- "corpusTag": "v2.45.10",
4
+ "version": "2.45.12",
5
+ "corpusTag": "v2.45.12",
6
6
  "files": {
7
7
  "api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
8
8
  "api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
9
9
  "api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
10
10
  "api/openapi.yaml": "ac6de5303872664b78ff419c035915593e01599e02835e777a0123de1baeb398",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "448d60e189059fe6bd4953b8c47f6941f89053707f45621d4e32b8bef0773670",
13
- "api/v2/asyncapi.yaml": "8db5aa748cd4f6481055d48d0a6d66d596f20e712d2004aa86de79b4c34e9bd9",
14
- "api/v2/openapi.yaml": "e3b1ed5954b8e9acd6c68cd2a924603afcebb686d07fb5a26e929b01b7f6599b",
12
+ "api/seams-v2.yaml": "b971877ce1060183e49a3b6c0b9ba1b3bfbf52cceb38482bc03b8d7554231b5f",
13
+ "api/v2/asyncapi.yaml": "af56f052444d3ffa2b8f9c380dcdb91c903b813d8fe7941d030c785ab54cbeac",
14
+ "api/v2/openapi.yaml": "47e3b00c61d4ef6fc7e42b819b52648bb3a1de966f5d6e4a41c6ff4a6c2d80f3",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "bd1c00fae7b6fda506a75aff841bbdc438d4ce092642a72041203bee999b88de",
17
17
  "schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
@@ -117,7 +117,7 @@
117
117
  "schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
118
118
  "schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
119
119
  "schemas/v2/budget-policy.schema.json": "1de68095db0beb5c7a4be629de26e79f62ebbe748a88d46f36e768edfd1f0d68",
120
- "schemas/v2/capabilities.schema.json": "4503d383c2b5efff532a79dabc79d17b5f6494c88d5b385bd3ca5f1a4e3f7647",
120
+ "schemas/v2/capabilities.schema.json": "c7a01b2f569139f99ba5e1a5329a42556fbd82bc743f8fd96963304395fc9ef0",
121
121
  "schemas/v2/certification-bundle.schema.json": "82ce31a1ed6c7e21b0ba88ff62a7b8b768fc3b176b1d06fce023203830b94725",
122
122
  "schemas/v2/channel-presence-payload.schema.json": "4eeb224910e6f7eee3ab5e7c3899559c25c83a26d0022e0ad195c4672611977a",
123
123
  "schemas/v2/channel-written-payload.schema.json": "ccecff3c71a3275ad8db035ac0e04db25e6f8d23cf091ddbcd5a60d1be1cbb6b",
@@ -127,6 +127,7 @@
127
127
  "schemas/v2/compensation-projection.schema.json": "f46977c7076347b5473faa8c4bbe73d8321c33b33c98fff2f26ee7cbada4ed98",
128
128
  "schemas/v2/configurable.schema.json": "33a6884047e2490f723733bf8b74d3b9164e00374cfea6dc3eac5a70dc368861",
129
129
  "schemas/v2/connection-pack-manifest.schema.json": "8d019de6bd0c5b14ea3b227580734ec792521493b396a850f7f9217aa7a6a547",
130
+ "schemas/v2/connection-provider-registry.schema.json": "5b404da328a95c45808929e233ee766ebd635cf0df6f5d15d4c19354dfd7e75d",
130
131
  "schemas/v2/conversation-event.schema.json": "9ad4c57285818edbf38b17816de0c4162f777c835f59e68702d0367a81cbccf6",
131
132
  "schemas/v2/conversation-turn.schema.json": "75bb6a4a03072a958feb5b34e2a1f40471e9e3996a81fa430b65717f0773c9a4",
132
133
  "schemas/v2/credential-provenance.schema.json": "f86b09e94b1c524dec7d1c6353965665ae8891e9517faef6e742d639c5578c99",
@@ -206,13 +207,13 @@
206
207
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
207
208
  "spec/v1/alias-detectors.json": "0b2f28808ffe0b9d98a187775038c3406bb70919747a788a6e4f866e293ae6d3",
208
209
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
209
- "spec/v1/core-standard-manifest.json": "819828fb2f978fa9e4a994652bbe16c797d36a8c4393d57d1432c8a53f2120ab",
210
+ "spec/v1/core-standard-manifest.json": "f138120b544d0e40c9ac05dd9b77a51bb624a3212cfd5a13d97be96fbbe9b3e4",
210
211
  "spec/v1/deprecations.json": "a3293072ea951857395d05f0437264e88965f1e9019ff61c55bacfa9c1aaa17c",
211
212
  "spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
212
213
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
213
214
  "spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
214
215
  "spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
215
- "spec/v1/gaps.json": "7eab313ec80c40285e3290113f5d3fea90ea6f8bc385ef6fd15d415f52540035",
216
+ "spec/v1/gaps.json": "9dd6e21e6122b292c4912028dfcc8368e3df1a8bafc81451c73c90a2a9a8fdc1",
216
217
  "spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
217
218
  "spec/v1/migrations.json": "4c50760be70cb644d02427bcaf5749606700edf4513f423903cf3b1d12f5c300",
218
219
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
@@ -222,13 +223,13 @@
222
223
  "spec/v2/core/artifact-type-packs.md": "f734740cc3963f86c9f273a7dace26291bba2855161ddd3540618131a2c8d13e",
223
224
  "spec/v2/core/capabilities.md": "04feda1747ca8f97443176cbf82d7d69891322a7bbf9a9bed88f519b8fb574fe",
224
225
  "spec/v2/core/conformance.md": "3ca291e63bc70a6ef829ae8759ad0ded3741ea7aa507bbbdffc57de443ab2ae2",
225
- "spec/v2/core/connection-packs.md": "1ee129d792c537bfe82393ccb92555d8e6ddc342b1aa781a13f101b27ec38171",
226
+ "spec/v2/core/connection-packs.md": "c80f95bd6a03698f35c72cdb2b46bd973f49203bbd786c26760e35fa86e6f23d",
226
227
  "spec/v2/core/conversation.md": "354fa57c73d11062e523a44aa75a6353c5c81aefaf306d30c42416a228c82c86",
227
228
  "spec/v2/core/errors.md": "91f46acc9c9a5532a98cf62c69285d68575c100ff6064f0b1bf94731a087e34a",
228
229
  "spec/v2/core/events.md": "c1a20e3aa6736119ea07aa2cf66faa808a1874d2cde4933033b65506d1742883",
229
230
  "spec/v2/core/execution.md": "8a0c9db1bbfb97c008ab9328b7333c1cc3318196e78bfbefb49e781bdf4f9d44",
230
231
  "spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
231
- "spec/v2/core/headers.md": "1e601c31d2c8bcb7628b7efe2b65fb53d9c79db2a60a4235c333cf85290bb0c0",
232
+ "spec/v2/core/headers.md": "60aee0768bb1b15cf8ff783170ba6541c8c106da417c8a066c8f85cb6a5b1ba1",
232
233
  "spec/v2/core/host-services.md": "73f26ea32891044131d63f9861d2cbddccb5d08bf76e29e47afef9bb5b646791",
233
234
  "spec/v2/core/i18n.md": "882904b7d59f992ab1e5f7346167161a54cf665b8fee47e64a3862cf9133a580",
234
235
  "spec/v2/core/idempotency.md": "adbd317a6ebb57c2759c076297b31f67def9e391576bccd0bbea787b13e85872",
@@ -246,12 +247,12 @@
246
247
  "spec/v2/core/security-defaults.md": "887d8c4bc592094e3b931c23f1c2175c18ebbd47528b074016f9d2091bc513b2",
247
248
  "spec/v2/core/storage.md": "dc94b985847c1341c7a064f04a6010f0fdbe6c3ab3e555d4362878c81d764c18",
248
249
  "spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
249
- "spec/v2/core/versioning.md": "b7acb0d8b77425cb53ea700e47ba790bf7506de600c398690e6ec5bb9c7a220e",
250
+ "spec/v2/core/versioning.md": "fe48f4514b9c9f998d5da1930b8faa02ca4de89dcd49b07d95414fb04c67d438",
250
251
  "spec/v2/core/webhooks.md": "daf6ec03092f4b3fde1eb74a5e2e801ec48db5139a13fcc0fd7fb4a004635ae3",
251
252
  "spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
252
253
  "spec/v2/corrections.json": "3a8d93e33865b692d26a3b6bfd3819766e206b2e0c2a69669baa4bf833d05a49",
253
254
  "spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
254
- "spec/v2/declaration.json": "8e5eab46ea448725b806a2395f80f5c36fa582a308f80eb09931a633a595eae1",
255
+ "spec/v2/declaration.json": "e963d3aa73892ee08281227c8a01aa6c06f1b825cdcb26aab029b1ab3f4155bf",
255
256
  "spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
256
257
  "spec/v2/errors.json": "83f048b0a06952b4af6f125fb66c4212acab51056b411390f1010acd6ab9cdc0",
257
258
  "spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
@@ -281,6 +282,7 @@
281
282
  "spec/v2/facets/aiProviders.schema.json": "4eb911c2403b8f618325194855a1326680fbda2549dfbfe86a70dd2743f8fb12",
282
283
  "spec/v2/facets/auditLogIntegrity.schema.json": "9a7f81320c8f940c6f4129bc7dd895a4063231c5458a717a08c2cbf9bb886c46",
283
284
  "spec/v2/facets/auth.schema.json": "7d98bc81a0f7d307460d039eebfebd21388091bfe1d538762583940f94c0bd2a",
285
+ "spec/v2/facets/connections.schema.json": "f998bb206a5814183bb4720cc34fa43b7ab64d61e39a79437b009c589ce8b78e",
284
286
  "spec/v2/facets/envelopeStrictness.schema.json": "28be0a27f880bd45ec1222a16155f5e9167bbca57bf0322970fe1eb2654562aa",
285
287
  "spec/v2/facets/heartbeat.schema.json": "81d7fa1895a8b0241bd07cf00a7e7905bef0fd14eb3427325261c62eee5746a6",
286
288
  "spec/v2/facets/interrupt.schema.json": "2110ef32c3f880bf706475ef4cf8ddf97ff06871b8e6003a5edbcf4439f1fb70",
@@ -297,17 +299,17 @@
297
299
  "spec/v2/generated/error-codes.md": "f171c0ca1be40221f9b4fbfcbce73c728c315a1e8feb8dd6bf738d6ca9e29526",
298
300
  "spec/v2/harness-trust-anchors.json": "6007b92eae16a070fbc3777424efb0abfe865fda89d096963109b96282b91205",
299
301
  "spec/v2/harness-trust-anchors.schema.json": "372b5d92337c893d3449eff3f6bfd009ac09fdac285296d8bdbe0a08fff9e34c",
300
- "spec/v2/id-field-bindings.json": "cc315f9a7aa5ec552860c1a790e105502fb737dc10b57aa5641bdd7405d6b638",
302
+ "spec/v2/id-field-bindings.json": "aa3e7c3590a490a77286e3ae60c1ab7407e7d627e05f981c3e08f221af4bd5dc",
301
303
  "spec/v2/interop-map.json": "02255dafe7eb55f61ca7f9ae7c21f079d1848a6756991343088975ac9d184b8d",
302
304
  "spec/v2/interop-map.schema.json": "0017c882df34ee215e2e0729de051490e09d43aac3d2c949285698ed51832f12",
303
305
  "spec/v2/migrations.json": "651954938a1e154c0b6f1e3dd6e105889d30de3bb65789f21de7f77a12869e9e",
304
306
  "spec/v2/migrations.schema.json": "b91ade6320ad1b1185c9495b9424f3aa63438ed8e412c45171c227d82c0afd86",
305
- "spec/v2/path-manifest.json": "36363105280139dad095dc4f64b5b272121e9759cdcf6de011358dd1c1a90540",
307
+ "spec/v2/path-manifest.json": "b84c7561f50c7df0cb61b202bd6595acad3ee7edca7a45f711949bea3c69bd27",
306
308
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
307
309
  "spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
308
- "spec/v2/release.json": "044530e75b4cdec6747c1bcebe799ebc62f1f6c2a1e722369b35b2854e54049f",
310
+ "spec/v2/release.json": "0966caad45aa4689f63ff476b863590b716c2ba264f2da20c964b0fe7f8773f2",
309
311
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
310
- "spec/v2/surface-baseline.json": "99ddcf9eb0d06967879ce7f6e54c49fe19bfabc9ee50f00f78699b401108bfd5"
312
+ "spec/v2/surface-baseline.json": "a00146b764f3f2f31af25cef62a1cf71f43354c55bb79a77ad38642d0ad460d5"
311
313
  },
312
- "corpusCommit": "8817d7c027047bbf1aa8d9385122137396f1bdca"
314
+ "corpusCommit": "f3c37c0559bd41f77fe89b00f306357b60497fb9"
313
315
  }
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { capabilityFamily } from './discovery-capabilities.js';
22
22
  import { codemapV1toV2 } from './era2-seed.js';
23
+ import { projectBoundId } from './bound-id.js';
23
24
 
24
25
  export interface MajorProfile {
25
26
  readonly major: number;
@@ -49,6 +50,16 @@ export interface MajorProfile {
49
50
  * unread observation and never guesses a name.
50
51
  */
51
52
  eventType(era1Name: string): string | undefined;
53
+ /** The trigger subscription collection, e.g. `/v1/trigger-subscriptions` or `/trigger-subscriptions`. */
54
+ readonly triggerSubscriptionsPath: string;
55
+ /** An id as one URL path segment: percent-encoded at v1, the `~` bound-id projection at v2 (`identity.md` §5). */
56
+ idSegment(id: string): string;
57
+ /**
58
+ * How a read of another tenant's id is refused. `not_found`: as an id never
59
+ * minted (v1, bare ids). `id_tenant_mismatch`: `403`, because a v2 bound id
60
+ * names its tenant and `identity.md` §5 requires the refusal to say so.
61
+ */
62
+ readonly foreignTenantRead: 'not_found' | 'id_tenant_mismatch';
52
63
  }
53
64
 
54
65
  const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
@@ -68,6 +79,9 @@ export const MAJOR_PROFILES: Readonly<Record<number, MajorProfile>> = {
68
79
  // v1 budgets are driven through a host seam, not `createRun`.
69
80
  runBudget: () => null,
70
81
  eventType: (era1Name) => era1Name,
82
+ triggerSubscriptionsPath: '/v1/trigger-subscriptions',
83
+ idSegment: (id) => encodeURIComponent(id),
84
+ foreignTenantRead: 'not_found',
71
85
  },
72
86
  2: {
73
87
  major: 2,
@@ -82,6 +96,9 @@ export const MAJOR_PROFILES: Readonly<Record<number, MajorProfile>> = {
82
96
  },
83
97
  runBudget: (policy) => ({ configurable: { version: 1, budget: { ...policy } } }),
84
98
  eventType: (era1Name) => codemapV1toV2().get(era1Name),
99
+ triggerSubscriptionsPath: '/trigger-subscriptions',
100
+ idSegment: (id) => projectBoundId(id),
101
+ foreignTenantRead: 'id_tenant_mismatch',
85
102
  },
86
103
  };
87
104
 
@@ -277,7 +277,9 @@ export function isMemory(c: DiscoveryPayload): boolean {
277
277
  /**
278
278
  * `openwop-trigger-bridge` predicate (RFC 0083, widened by RFC 0099). Host
279
279
  * composes the durable inbound-work contract: advertises the `triggerBridge`,
280
- * has a `deadLetter` sink for exhausted deliveries, and has at least one
280
+ * has a `deadLetter` run sink (RFC 0053: where a trigger-started run that fails
281
+ * terminally lands; a dead-lettered DELIVERY starts no run and is read through
282
+ * `triggerBridge.deadLetter`, RFC 0232), and has at least one
281
283
  * durable inbound source (queue bus, durable webhooks, scheduling, OR — per
282
284
  * RFC 0099 — externally-ingested `email`/`form` via
283
285
  * `triggerBridge.ingestion.externalSources[]`). Capability families are
@@ -0,0 +1,70 @@
1
+ /**
2
+ * A scratch double for RFC 0233's provider registry reads. Each self-test turns
3
+ * on ONE defect to show `provider-registry-witness.ts` convicts it.
4
+ *
5
+ * A test double, not a host: it executes nothing, and nothing it does is
6
+ * evidence about any implementation.
7
+ */
8
+
9
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
10
+ import type { AddressInfo } from 'node:net';
11
+ import { FIRST_PACK, FIXTURE_ID, RIVAL_PACK } from './provider-registry-witness.js';
12
+
13
+ export type RegistryDefect =
14
+ | 'none' | 'not-installed' | 'duplicate-id' | 'no-refusal' | 'wrong-holder' | 'first-refused'
15
+ | 'rival-resolves' | 'qualified-wrong-pack' | 'leaks-endpoint' | 'not-served';
16
+
17
+ export class ProviderRegistryDouble {
18
+ defect: RegistryDefect = 'none';
19
+ url = '';
20
+ /** The v2 discovery document, so the scenario files themselves can run against the double. */
21
+ discovery: Record<string, unknown> = { connections: { status: 'experimental', since: '2.45', witness: 'witnessable-gated', packsSupported: true, providerRead: true } };
22
+ private server: Server | undefined;
23
+
24
+ async start(): Promise<string> {
25
+ this.server = createServer((req, res) => this.handle(req, res));
26
+ await new Promise<void>((r) => this.server!.listen(0, '127.0.0.1', r));
27
+ this.url = `http://127.0.0.1:${(this.server.address() as AddressInfo).port}`;
28
+ return this.url;
29
+ }
30
+ async stop(): Promise<void> { const s = this.server; if (s) { s.closeAllConnections(); await new Promise<void>((r) => s.close(() => r())); } }
31
+
32
+ private send(res: ServerResponse, status: number, body: unknown): void { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); }
33
+
34
+ private registry(): Record<string, unknown> {
35
+ const d = this.defect;
36
+ const providers: Array<Record<string, unknown>> = [{ id: 'github', source: 'builtin' }];
37
+ if (d !== 'not-installed') {
38
+ providers.push({ id: FIXTURE_ID, source: 'pack', packName: d === 'first-refused' ? RIVAL_PACK : FIRST_PACK, ...(d === 'leaks-endpoint' ? { tokenUrl: 'https://acme.example/token' } : {}) });
39
+ if (d === 'duplicate-id') providers.push({ id: FIXTURE_ID, source: 'pack', packName: RIVAL_PACK });
40
+ }
41
+ const refusals: Array<Record<string, unknown>> = [];
42
+ if (d !== 'not-installed' && d !== 'no-refusal') {
43
+ refusals.push(d === 'first-refused'
44
+ ? { packName: FIRST_PACK, providerId: FIXTURE_ID, code: 'connection_provider_conflict', heldBy: RIVAL_PACK }
45
+ : { packName: RIVAL_PACK, providerId: FIXTURE_ID, code: 'connection_provider_conflict', heldBy: d === 'wrong-holder' ? 'builtin' : FIRST_PACK });
46
+ }
47
+ return { providers, refusals };
48
+ }
49
+
50
+ private handle(req: IncomingMessage, res: ServerResponse): void {
51
+ const u = new URL(req.url ?? '/', 'http://x');
52
+ if (req.method === 'GET' && u.pathname === '/.well-known/openwop') return this.send(res, 200, this.discovery);
53
+ if (this.defect === 'not-served' && u.pathname.startsWith('/connection-providers')) return this.send(res, 404, { error: 'not_found', message: 'no route' });
54
+ if (req.method === 'GET' && u.pathname === '/connection-providers') return this.send(res, 200, this.registry());
55
+ const m = /^\/connection-providers\/([^/]+)$/.exec(u.pathname);
56
+ if (req.method === 'GET' && m) {
57
+ const id = decodeURIComponent(m[1]!);
58
+ const pack = u.searchParams.get('pack');
59
+ const reg = this.registry();
60
+ const defs = (reg['providers'] as Array<Record<string, unknown>>).filter((p) => p['id'] === id);
61
+ let hit: Record<string, unknown> | undefined;
62
+ if (pack === null) hit = defs.length === 1 ? defs[0] : undefined;
63
+ else if (pack === RIVAL_PACK && this.defect === 'rival-resolves') hit = defs[0];
64
+ else if (pack === FIRST_PACK && this.defect === 'qualified-wrong-pack') hit = { id, source: 'builtin' };
65
+ else hit = defs.find((p) => p['packName'] === pack);
66
+ return hit ? this.send(res, 200, hit) : this.send(res, 404, { error: 'connection_provider_unresolved', message: `no definition for ${id}` });
67
+ }
68
+ return this.send(res, 404, { error: 'not_found', message: `no route ${req.method} ${u.pathname}` });
69
+ }
70
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The connection-provider registry witness (RFC 0233): the normative
3
+ * observation path for `connection-packs.md` §Provider identity and §The
4
+ * qualified form at major 2, read through `GET /connection-providers` and
5
+ * `GET /connection-providers/{providerId}`.
6
+ *
7
+ * Installing a pack is no protocol operation, so the suite never causes the
8
+ * conflict here. An operator installs the §D fixture pair
9
+ * (`connection-pack-acme-widgets`, then `connection-pack-acme-widgets-rival`)
10
+ * through the host's own install path, and the registry describes the outcome.
11
+ *
12
+ * Each leg OBSERVES and returns findings; it asserts nothing. The scenario maps
13
+ * findings to requirement ids, and `provider-registry-witness.test.ts` proves
14
+ * each leg against a scratch double with one defect turned on.
15
+ */
16
+
17
+ import { driver } from './driver.js';
18
+ import { readErrorCode } from './error-envelope.js';
19
+
20
+ export const FIRST_PACK = 'core.openwop.connections.acme-widgets';
21
+ export const RIVAL_PACK = 'core.openwop.connections.acme-widgets-rival';
22
+ export const FIXTURE_ID = 'acme-widgets';
23
+
24
+ export interface RegistryFinding { readonly ok: boolean; readonly doc: string; readonly message: string }
25
+ export type RegistryOutcome =
26
+ | { readonly kind: 'skip'; readonly disposition: 'inapplicable' | 'blocked'; readonly reason: string }
27
+ | { readonly kind: 'observed'; readonly findings: readonly RegistryFinding[] };
28
+
29
+ const DOC = 'connection-packs.md §Observation';
30
+ const isRecord = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === 'object' && !Array.isArray(v);
31
+ const f = (ok: boolean, doc: string, message: string): RegistryFinding => ({ ok, doc, message });
32
+
33
+ /** `connections.providerRead` as advertised in a v2 discovery document. */
34
+ export function providerReadAdvertised(connections: Record<string, unknown> | null): boolean {
35
+ return connections !== null && connections['providerRead'] === true;
36
+ }
37
+
38
+ interface Read { readonly status: number; readonly json: unknown }
39
+ async function list(): Promise<Read> {
40
+ const r = await driver.get('/connection-providers');
41
+ return { status: r.status, json: r.json };
42
+ }
43
+ async function resolve(providerId: string, pack?: string): Promise<Read> {
44
+ const q = pack === undefined ? '' : `?pack=${encodeURIComponent(pack)}`;
45
+ const r = await driver.get(`/connection-providers/${encodeURIComponent(providerId)}${q}`);
46
+ return { status: r.status, json: r.json };
47
+ }
48
+
49
+ const providersOf = (json: unknown): Array<Record<string, unknown>> => {
50
+ const p = isRecord(json) ? json['providers'] : undefined;
51
+ return Array.isArray(p) ? p.filter(isRecord) : [];
52
+ };
53
+ const refusalsOf = (json: unknown): Array<Record<string, unknown>> => {
54
+ const r = isRecord(json) ? json['refusals'] : undefined;
55
+ return Array.isArray(r) ? r.filter(isRecord) : [];
56
+ };
57
+
58
+ /** Is the §D fixture installed? The registry says so itself (RFC 0233 §D.2). */
59
+ export function fixtureInstalled(json: unknown): boolean {
60
+ return providersOf(json).some((p) => p['id'] === FIXTURE_ID && p['packName'] === FIRST_PACK);
61
+ }
62
+
63
+ /** Leg 3: a schema-valid, content-free registry with each bare id once. Runs on any host with the facet. */
64
+ export async function uniqueLeg(validate: (doc: unknown) => { ok: boolean; errors: string }): Promise<RegistryOutcome> {
65
+ const r = await list();
66
+ if (r.status !== 200) return { kind: 'observed', findings: [f(false, DOC, `a host advertising connections.providerRead MUST serve GET /connection-providers (got ${r.status})`)] };
67
+ const v = validate(r.json);
68
+ const ids = providersOf(r.json).map((p) => String(p['id']));
69
+ const dupes = [...new Set(ids.filter((id, i) => ids.indexOf(id) !== i))];
70
+ return { kind: 'observed', findings: [
71
+ f(v.ok, DOC, `the registry MUST validate against connection-provider-registry.schema.json (closed: no endpoint, scope catalog or credential)${v.ok ? '' : `: ${v.errors}`}`),
72
+ f(dupes.length === 0, 'connection-packs.md §Provider identity', `a bare provider id MUST appear at most once (duplicated: ${dupes.join(', ') || 'none'})`),
73
+ ] };
74
+ }
75
+
76
+ const NOT_INSTALLED = 'the §D fixture pack connection-pack-acme-widgets is not installed on this host (RFC 0233 §D: operator opt-in), so no conflict has been caused to read';
77
+
78
+ /** Leg 1: the later registration of a bare id was refused, and the id is held once. */
79
+ export async function failClosedLeg(): Promise<RegistryOutcome> {
80
+ const r = await list();
81
+ if (r.status !== 200) return { kind: 'observed', findings: [f(false, DOC, `a host advertising connections.providerRead MUST serve GET /connection-providers (got ${r.status})`)] };
82
+ if (!fixtureInstalled(r.json)) return { kind: 'skip', disposition: 'inapplicable', reason: NOT_INSTALLED };
83
+ const holders = providersOf(r.json).filter((p) => p['id'] === FIXTURE_ID);
84
+ const refused = refusalsOf(r.json).filter((x) => x['providerId'] === FIXTURE_ID && x['code'] === 'connection_provider_conflict');
85
+ return { kind: 'observed', findings: [
86
+ f(holders.length === 1, 'connection-packs.md §Provider identity', `${FIXTURE_ID} MUST be defined exactly once (got ${holders.length})`),
87
+ f(refused.length >= 1, 'connection-packs.md §Provider identity', `a later registration of ${FIXTURE_ID} MUST be refused with connection_provider_conflict and appear in refusals (RFC 0233 §D.1: the rival is attempted after the first pack)`),
88
+ f(refused.every((x) => x['heldBy'] === FIRST_PACK), 'connection-packs.md §Observation', `a conflict refusal MUST name the holder, ${FIRST_PACK} (got ${refused.map((x) => String(x['heldBy'])).join(', ') || 'none'})`),
89
+ f(!refused.some((x) => x['packName'] === FIRST_PACK), 'connection-packs.md §Provider identity', 'the first registration MUST NOT be the one refused'),
90
+ ] };
91
+ }
92
+
93
+ /** Leg 2: the qualified form resolves only to the named pack; bare resolves to the one definition. */
94
+ export async function qualifiedLeg(): Promise<RegistryOutcome> {
95
+ const r = await list();
96
+ if (r.status !== 200) return { kind: 'observed', findings: [f(false, DOC, `a host advertising connections.providerRead MUST serve GET /connection-providers (got ${r.status})`)] };
97
+ if (!fixtureInstalled(r.json)) return { kind: 'skip', disposition: 'inapplicable', reason: NOT_INSTALLED };
98
+ const named = await resolve(FIXTURE_ID, FIRST_PACK);
99
+ const rival = await resolve(FIXTURE_ID, RIVAL_PACK);
100
+ const bare = await resolve(FIXTURE_ID);
101
+ const row = (x: Read): Record<string, unknown> => (isRecord(x.json) ? x.json : {});
102
+ return { kind: 'observed', findings: [
103
+ f(named.status === 200 && row(named)['source'] === 'pack' && row(named)['packName'] === FIRST_PACK, 'connection-packs.md §The qualified form', `${FIXTURE_ID}?pack=${FIRST_PACK} MUST resolve to that pack's definition (got ${named.status} ${JSON.stringify(named.json ?? null).slice(0, 160)})`),
104
+ f(rival.status === 404 && readErrorCode(rival.json) === 'connection_provider_unresolved', 'connection-packs.md §The qualified form', `a qualified reference to a pack that does not define the id MUST answer 404 connection_provider_unresolved (got ${rival.status} ${String(readErrorCode(rival.json))})`),
105
+ f(bare.status === 200 && row(bare)['packName'] === FIRST_PACK, 'connection-packs.md §The qualified form', `the bare id MUST resolve to its one definition, ${FIRST_PACK} (got ${bare.status} ${JSON.stringify(bare.json ?? null).slice(0, 160)})`),
106
+ ] };
107
+ }