autotel-pact 0.2.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 +385 -0
- package/dist/audit.cjs +412 -0
- package/dist/audit.cjs.map +1 -0
- package/dist/audit.d.cts +25 -0
- package/dist/audit.d.ts +25 -0
- package/dist/audit.js +403 -0
- package/dist/audit.js.map +1 -0
- package/dist/auto-wrap.cjs +255 -0
- package/dist/auto-wrap.cjs.map +1 -0
- package/dist/auto-wrap.d.cts +57 -0
- package/dist/auto-wrap.d.ts +57 -0
- package/dist/auto-wrap.js +248 -0
- package/dist/auto-wrap.js.map +1 -0
- package/dist/broker.cjs +84 -0
- package/dist/broker.cjs.map +1 -0
- package/dist/broker.d.cts +23 -0
- package/dist/broker.d.ts +23 -0
- package/dist/broker.js +80 -0
- package/dist/broker.js.map +1 -0
- package/dist/cli.cjs +662 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +656 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +967 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +200 -0
- package/dist/index.d.ts +200 -0
- package/dist/index.js +932 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger-BuBmfWNc.d.ts +22 -0
- package/dist/ledger-D88TzN1c.d.cts +22 -0
- package/dist/processor.cjs +200 -0
- package/dist/processor.cjs.map +1 -0
- package/dist/processor.d.cts +58 -0
- package/dist/processor.d.ts +58 -0
- package/dist/processor.js +193 -0
- package/dist/processor.js.map +1 -0
- package/dist/provider.cjs +219 -0
- package/dist/provider.cjs.map +1 -0
- package/dist/provider.d.cts +41 -0
- package/dist/provider.d.ts +41 -0
- package/dist/provider.js +213 -0
- package/dist/provider.js.map +1 -0
- package/dist/tag.cjs +50 -0
- package/dist/tag.cjs.map +1 -0
- package/dist/tag.d.cts +9 -0
- package/dist/tag.d.ts +9 -0
- package/dist/tag.js +48 -0
- package/dist/tag.js.map +1 -0
- package/dist/types-BHGiwqcp.d.cts +157 -0
- package/dist/types-BHGiwqcp.d.ts +157 -0
- package/package.json +108 -0
- package/schemas/README.md +24 -0
- package/schemas/audit-matrix-v0.2.0.json +78 -0
- package/schemas/ledger-entry-v0.2.0.json +77 -0
- package/src/attrs.test.ts +35 -0
- package/src/attrs.ts +53 -0
- package/src/audit.test.ts +189 -0
- package/src/audit.ts +251 -0
- package/src/auto-wrap.test.ts +149 -0
- package/src/auto-wrap.ts +283 -0
- package/src/broker.test.ts +175 -0
- package/src/broker.ts +118 -0
- package/src/cli.test.ts +148 -0
- package/src/cli.ts +287 -0
- package/src/index.ts +94 -0
- package/src/labels.ts +25 -0
- package/src/ledger-normalize.test.ts +141 -0
- package/src/ledger-normalize.ts +82 -0
- package/src/ledger.test.ts +92 -0
- package/src/ledger.ts +156 -0
- package/src/pact-file.test.ts +124 -0
- package/src/pact-file.ts +65 -0
- package/src/processor.test.ts +90 -0
- package/src/processor.ts +191 -0
- package/src/tag.test.ts +72 -0
- package/src/tag.ts +21 -0
- package/src/types.ts +169 -0
- package/src/wrapper-http.test.ts +133 -0
- package/src/wrapper-http.ts +194 -0
- package/src/wrapper-provider.test.ts +132 -0
- package/src/wrapper-provider.ts +163 -0
- package/src/wrapper.test.ts +176 -0
- package/src/wrapper.ts +221 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var autotel = require('autotel');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var promises = require('fs/promises');
|
|
6
|
+
var path = require('path');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
11
|
+
|
|
12
|
+
// src/wrapper.ts
|
|
13
|
+
|
|
14
|
+
// src/attrs.ts
|
|
15
|
+
var PACT_ATTRS = {
|
|
16
|
+
CONSUMER: "pact.consumer",
|
|
17
|
+
PROVIDER: "pact.provider",
|
|
18
|
+
KIND: "pact.kind",
|
|
19
|
+
INTERACTION_DESCRIPTION: "pact.interaction.description",
|
|
20
|
+
INTERACTION_ID: "pact.interaction.id",
|
|
21
|
+
INTERACTION_STATES: "pact.interaction.states",
|
|
22
|
+
CONTRACT_FILE: "pact.contract.file",
|
|
23
|
+
OUTCOME: "pact.outcome"
|
|
24
|
+
};
|
|
25
|
+
function buildPactAttributes(meta, opts = {}) {
|
|
26
|
+
const attrs = {
|
|
27
|
+
[PACT_ATTRS.CONSUMER]: meta.consumer,
|
|
28
|
+
[PACT_ATTRS.PROVIDER]: meta.provider,
|
|
29
|
+
[PACT_ATTRS.KIND]: meta.kind,
|
|
30
|
+
[PACT_ATTRS.INTERACTION_DESCRIPTION]: meta.description,
|
|
31
|
+
[PACT_ATTRS.INTERACTION_STATES]: meta.states
|
|
32
|
+
};
|
|
33
|
+
if (opts.contractFile) {
|
|
34
|
+
attrs[PACT_ATTRS.CONTRACT_FILE] = opts.contractFile;
|
|
35
|
+
}
|
|
36
|
+
if (meta.interactionId) {
|
|
37
|
+
attrs[PACT_ATTRS.INTERACTION_ID] = meta.interactionId;
|
|
38
|
+
}
|
|
39
|
+
return attrs;
|
|
40
|
+
}
|
|
41
|
+
function outcomeAttribute(outcome) {
|
|
42
|
+
return { [PACT_ATTRS.OUTCOME]: outcome };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/types.ts
|
|
46
|
+
var LEDGER_ENTRY_SPEC = "autotel-pact-ledger-entry/v0.2.0";
|
|
47
|
+
var AUDIT_MATRIX_SPEC = "autotel-pact-audit-matrix/v0.2.0";
|
|
48
|
+
function isInteractionLedgerEntry(entry) {
|
|
49
|
+
return entry.type !== "provider_verification_run";
|
|
50
|
+
}
|
|
51
|
+
function isProviderVerificationRun(entry) {
|
|
52
|
+
return entry.type === "provider_verification_run";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/ledger-normalize.ts
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return typeof value === "object" && value !== null;
|
|
58
|
+
}
|
|
59
|
+
function normalizeLedgerRecord(parsed) {
|
|
60
|
+
if (!isRecord(parsed) || parsed.spec !== LEDGER_ENTRY_SPEC) return null;
|
|
61
|
+
const { consumer, provider, observed_at } = parsed;
|
|
62
|
+
if (typeof consumer !== "string" || typeof provider !== "string" || typeof observed_at !== "string") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (parsed.type === "provider_verification_run") {
|
|
66
|
+
if (typeof parsed.error !== "string") return null;
|
|
67
|
+
const entry2 = {
|
|
68
|
+
type: "provider_verification_run",
|
|
69
|
+
spec: LEDGER_ENTRY_SPEC,
|
|
70
|
+
consumer,
|
|
71
|
+
provider,
|
|
72
|
+
outcome: "failed",
|
|
73
|
+
source: parsed.source === "production" ? "production" : "test",
|
|
74
|
+
role: "provider",
|
|
75
|
+
observed_at,
|
|
76
|
+
error: parsed.error
|
|
77
|
+
};
|
|
78
|
+
if (typeof parsed.run_id === "string") entry2.run_id = parsed.run_id;
|
|
79
|
+
if (typeof parsed.git_sha === "string") entry2.git_sha = parsed.git_sha;
|
|
80
|
+
if (typeof parsed.trace_id === "string") entry2.trace_id = parsed.trace_id;
|
|
81
|
+
if (typeof parsed.span_id === "string") entry2.span_id = parsed.span_id;
|
|
82
|
+
return entry2;
|
|
83
|
+
}
|
|
84
|
+
if (typeof parsed.interaction !== "string") return null;
|
|
85
|
+
const states = Array.isArray(parsed.states) ? parsed.states.filter((s) => typeof s === "string") : [];
|
|
86
|
+
const entry = {
|
|
87
|
+
type: "interaction",
|
|
88
|
+
spec: LEDGER_ENTRY_SPEC,
|
|
89
|
+
consumer,
|
|
90
|
+
provider,
|
|
91
|
+
interaction: parsed.interaction,
|
|
92
|
+
states,
|
|
93
|
+
kind: parsed.kind === "http" ? "http" : "message",
|
|
94
|
+
outcome: parsed.outcome === "failed" ? "failed" : "passed",
|
|
95
|
+
source: parsed.source === "production" ? "production" : "test",
|
|
96
|
+
role: parsed.role === "provider" ? "provider" : "consumer",
|
|
97
|
+
duration_ms: typeof parsed.duration_ms === "number" && parsed.duration_ms >= 0 ? parsed.duration_ms : 0,
|
|
98
|
+
observed_at
|
|
99
|
+
};
|
|
100
|
+
if (typeof parsed.interaction_id === "string" && parsed.interaction_id.length > 0) {
|
|
101
|
+
entry.interaction_id = parsed.interaction_id;
|
|
102
|
+
}
|
|
103
|
+
if (typeof parsed.trace_id === "string") entry.trace_id = parsed.trace_id;
|
|
104
|
+
if (typeof parsed.span_id === "string") entry.span_id = parsed.span_id;
|
|
105
|
+
if (typeof parsed.run_id === "string") entry.run_id = parsed.run_id;
|
|
106
|
+
if (typeof parsed.git_sha === "string") entry.git_sha = parsed.git_sha;
|
|
107
|
+
if (typeof parsed.error === "string") entry.error = parsed.error;
|
|
108
|
+
return entry;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/ledger.ts
|
|
112
|
+
var DEFAULT_DIR = ".autotel-pact";
|
|
113
|
+
function resolveLedgerDir(opts = {}) {
|
|
114
|
+
return path__default.default.resolve(process.cwd(), opts.dir ?? process.env.AUTOTEL_PACT_LEDGER_DIR ?? DEFAULT_DIR);
|
|
115
|
+
}
|
|
116
|
+
function resolveRunId(opts = {}) {
|
|
117
|
+
const explicit = opts.runId ?? process.env.AUTOTEL_PACT_RUN_ID;
|
|
118
|
+
if (explicit) return explicit;
|
|
119
|
+
return `local-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(/[:.]/g, "-")}`;
|
|
120
|
+
}
|
|
121
|
+
function ledgerPath(opts = {}) {
|
|
122
|
+
const dir = resolveLedgerDir(opts);
|
|
123
|
+
return path__default.default.join(dir, `ledger-${resolveRunId(opts)}.jsonl`);
|
|
124
|
+
}
|
|
125
|
+
function writeLine(filePath, entry) {
|
|
126
|
+
fs.mkdirSync(path__default.default.dirname(filePath), { recursive: true });
|
|
127
|
+
fs.appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf8");
|
|
128
|
+
}
|
|
129
|
+
function appendLedgerEntry(entry, opts = {}) {
|
|
130
|
+
const filePath = ledgerPath(opts);
|
|
131
|
+
const normalized = entry.type === "provider_verification_run" ? entry : { ...entry, spec: LEDGER_ENTRY_SPEC, type: "interaction" };
|
|
132
|
+
writeLine(filePath, normalized);
|
|
133
|
+
}
|
|
134
|
+
function appendProviderVerificationFailure(entry, opts = {}) {
|
|
135
|
+
appendLedgerEntry(
|
|
136
|
+
{
|
|
137
|
+
type: "provider_verification_run",
|
|
138
|
+
spec: LEDGER_ENTRY_SPEC,
|
|
139
|
+
outcome: "failed",
|
|
140
|
+
role: "provider",
|
|
141
|
+
source: entry.source ?? "test",
|
|
142
|
+
consumer: entry.consumer,
|
|
143
|
+
provider: entry.provider,
|
|
144
|
+
observed_at: entry.observed_at,
|
|
145
|
+
error: entry.error,
|
|
146
|
+
run_id: entry.run_id,
|
|
147
|
+
git_sha: entry.git_sha,
|
|
148
|
+
trace_id: entry.trace_id,
|
|
149
|
+
span_id: entry.span_id
|
|
150
|
+
},
|
|
151
|
+
opts
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
function readLedger(opts = {}) {
|
|
155
|
+
const dir = resolveLedgerDir(opts);
|
|
156
|
+
if (!fs.existsSync(dir)) return [];
|
|
157
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
158
|
+
const entries = [];
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
const text = fs.readFileSync(path__default.default.join(dir, file), "utf8");
|
|
161
|
+
for (const line of text.split("\n")) {
|
|
162
|
+
if (!line.trim()) continue;
|
|
163
|
+
try {
|
|
164
|
+
const normalized = normalizeLedgerRecord(JSON.parse(line));
|
|
165
|
+
if (normalized) entries.push(normalized);
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return entries;
|
|
171
|
+
}
|
|
172
|
+
var MAX_PENDING_WRITES = 4096;
|
|
173
|
+
var asyncWriteChain = Promise.resolve();
|
|
174
|
+
var pendingWrites = 0;
|
|
175
|
+
async function appendLedgerEntryAsync(entry, opts = {}) {
|
|
176
|
+
if (pendingWrites >= MAX_PENDING_WRITES) {
|
|
177
|
+
await asyncWriteChain;
|
|
178
|
+
}
|
|
179
|
+
const filePath = ledgerPath(opts);
|
|
180
|
+
const normalized = entry.type === "provider_verification_run" ? entry : { ...entry, spec: LEDGER_ENTRY_SPEC, type: "interaction" };
|
|
181
|
+
const line = JSON.stringify(normalized);
|
|
182
|
+
pendingWrites++;
|
|
183
|
+
const run = asyncWriteChain.then(async () => {
|
|
184
|
+
try {
|
|
185
|
+
await promises.mkdir(path__default.default.dirname(filePath), { recursive: true });
|
|
186
|
+
await promises.appendFile(filePath, line + "\n", "utf8");
|
|
187
|
+
} finally {
|
|
188
|
+
pendingWrites--;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
asyncWriteChain = run.catch(() => {
|
|
192
|
+
});
|
|
193
|
+
return run;
|
|
194
|
+
}
|
|
195
|
+
async function flushLedgerWrites() {
|
|
196
|
+
await asyncWriteChain;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/wrapper.ts
|
|
200
|
+
function resolveParticipants(pact, opts) {
|
|
201
|
+
const cfg = pact.config;
|
|
202
|
+
const consumer = opts.consumer ?? cfg?.consumer;
|
|
203
|
+
const provider = opts.provider ?? cfg?.provider;
|
|
204
|
+
if (!consumer || !provider) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"autotel-pact: could not resolve consumer/provider from the Pact instance. Pass `{ consumer, provider }` in the options object."
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return { consumer, provider };
|
|
210
|
+
}
|
|
211
|
+
async function withPactInteraction(pact, handler, opts = {}) {
|
|
212
|
+
const start = process.hrtime.bigint();
|
|
213
|
+
const spanName = opts.spanName ?? "pact.interaction";
|
|
214
|
+
const kind = "message";
|
|
215
|
+
const { consumer, provider } = resolveParticipants(pact, opts);
|
|
216
|
+
if (opts.interactionId && typeof pact.withMetadata === "function") {
|
|
217
|
+
pact.withMetadata({ interactionId: opts.interactionId });
|
|
218
|
+
}
|
|
219
|
+
let captured;
|
|
220
|
+
let handlerResult;
|
|
221
|
+
return autotel.span(spanName, async (span) => {
|
|
222
|
+
span.setAttributes({
|
|
223
|
+
"pact.consumer": consumer,
|
|
224
|
+
"pact.provider": provider,
|
|
225
|
+
"pact.kind": kind
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
await pact.verify(async (reified) => {
|
|
229
|
+
captured = reified;
|
|
230
|
+
const meta = {
|
|
231
|
+
consumer,
|
|
232
|
+
provider,
|
|
233
|
+
description: reified.description ?? "<unknown>",
|
|
234
|
+
states: (reified.providerStates ?? []).map((s) => s.name),
|
|
235
|
+
kind,
|
|
236
|
+
interactionId: opts.interactionId
|
|
237
|
+
};
|
|
238
|
+
span.setAttributes(buildPactAttributes(meta, { contractFile: opts.contractFile }));
|
|
239
|
+
handlerResult = await handler(reified);
|
|
240
|
+
return handlerResult;
|
|
241
|
+
});
|
|
242
|
+
span.setAttributes(outcomeAttribute("passed"));
|
|
243
|
+
writeLedgerForOutcome({
|
|
244
|
+
consumer,
|
|
245
|
+
provider,
|
|
246
|
+
captured,
|
|
247
|
+
kind,
|
|
248
|
+
outcome: "passed",
|
|
249
|
+
start,
|
|
250
|
+
opts
|
|
251
|
+
});
|
|
252
|
+
return handlerResult;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
span.setAttributes(outcomeAttribute("failed"));
|
|
255
|
+
writeLedgerForOutcome({
|
|
256
|
+
consumer,
|
|
257
|
+
provider,
|
|
258
|
+
captured,
|
|
259
|
+
kind,
|
|
260
|
+
outcome: "failed",
|
|
261
|
+
start,
|
|
262
|
+
opts,
|
|
263
|
+
error: error instanceof Error ? error.message : String(error)
|
|
264
|
+
});
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function writeLedgerForOutcome(args) {
|
|
270
|
+
const { consumer, provider, captured, kind, outcome, start, opts, error } = args;
|
|
271
|
+
const ctx = autotel.getActiveSpan()?.spanContext();
|
|
272
|
+
const entry = {
|
|
273
|
+
type: "interaction",
|
|
274
|
+
spec: LEDGER_ENTRY_SPEC,
|
|
275
|
+
consumer,
|
|
276
|
+
provider,
|
|
277
|
+
interaction: captured?.description ?? "<unknown>",
|
|
278
|
+
interaction_id: opts.interactionId,
|
|
279
|
+
states: (captured?.providerStates ?? []).map((s) => s.name),
|
|
280
|
+
kind,
|
|
281
|
+
source: "test",
|
|
282
|
+
role: "consumer",
|
|
283
|
+
outcome,
|
|
284
|
+
duration_ms: Number(process.hrtime.bigint() - start) / 1e6,
|
|
285
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
286
|
+
trace_id: ctx?.traceId,
|
|
287
|
+
span_id: ctx?.spanId,
|
|
288
|
+
run_id: process.env.AUTOTEL_PACT_RUN_ID,
|
|
289
|
+
git_sha: process.env.GIT_SHA ?? process.env.GITHUB_SHA,
|
|
290
|
+
error
|
|
291
|
+
};
|
|
292
|
+
appendLedgerEntry(entry, opts);
|
|
293
|
+
}
|
|
294
|
+
function resolveHttpParticipants(pact, opts) {
|
|
295
|
+
const fromOpts = pact.opts;
|
|
296
|
+
const consumer = opts.consumer ?? fromOpts?.consumer;
|
|
297
|
+
const provider = opts.provider ?? fromOpts?.provider;
|
|
298
|
+
if (!consumer || !provider) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
"autotel-pact: could not resolve consumer/provider from the PactV3 instance. Pass `{ consumer, provider }` in the options object."
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return { consumer, provider };
|
|
304
|
+
}
|
|
305
|
+
async function withHttpPactInteraction(pact, interaction, testFn, opts = {}) {
|
|
306
|
+
if (opts.interactionId !== void 0) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
"autotel-pact: `interactionId` is not yet supported for HTTP Pact. PactV3 interactions have no metadata channel to persist the id; support arrives in v0.2 via PactV4 comments. Use the description as the stable identity for HTTP interactions in v0.1."
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const start = process.hrtime.bigint();
|
|
312
|
+
const spanName = opts.spanName ?? "pact.interaction";
|
|
313
|
+
const kind = "http";
|
|
314
|
+
const { consumer, provider } = resolveHttpParticipants(pact, opts);
|
|
315
|
+
const meta = {
|
|
316
|
+
consumer,
|
|
317
|
+
provider,
|
|
318
|
+
description: interaction.uponReceiving,
|
|
319
|
+
states: (interaction.states ?? []).map((s) => s.description),
|
|
320
|
+
kind,
|
|
321
|
+
interactionId: opts.interactionId
|
|
322
|
+
};
|
|
323
|
+
pact.addInteraction(interaction);
|
|
324
|
+
return autotel.span(spanName, async (span) => {
|
|
325
|
+
span.setAttributes(buildPactAttributes(meta, { contractFile: opts.contractFile }));
|
|
326
|
+
try {
|
|
327
|
+
const result = await pact.executeTest(testFn);
|
|
328
|
+
span.setAttributes(outcomeAttribute("passed"));
|
|
329
|
+
writeHttpLedgerEntry({ meta, outcome: "passed", start, opts });
|
|
330
|
+
return result;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
span.setAttributes(outcomeAttribute("failed"));
|
|
333
|
+
writeHttpLedgerEntry({
|
|
334
|
+
meta,
|
|
335
|
+
outcome: "failed",
|
|
336
|
+
start,
|
|
337
|
+
opts,
|
|
338
|
+
error: error instanceof Error ? error.message : String(error)
|
|
339
|
+
});
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function writeHttpLedgerEntry(args) {
|
|
345
|
+
const { meta, outcome, start, opts, error } = args;
|
|
346
|
+
const ctx = autotel.getActiveSpan()?.spanContext();
|
|
347
|
+
const entry = {
|
|
348
|
+
type: "interaction",
|
|
349
|
+
spec: LEDGER_ENTRY_SPEC,
|
|
350
|
+
consumer: meta.consumer,
|
|
351
|
+
provider: meta.provider,
|
|
352
|
+
interaction: meta.description,
|
|
353
|
+
interaction_id: meta.interactionId,
|
|
354
|
+
states: meta.states,
|
|
355
|
+
kind: "http",
|
|
356
|
+
source: "test",
|
|
357
|
+
role: "consumer",
|
|
358
|
+
outcome,
|
|
359
|
+
duration_ms: Number(process.hrtime.bigint() - start) / 1e6,
|
|
360
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
361
|
+
trace_id: ctx?.traceId,
|
|
362
|
+
span_id: ctx?.spanId,
|
|
363
|
+
run_id: process.env.AUTOTEL_PACT_RUN_ID,
|
|
364
|
+
git_sha: process.env.GIT_SHA ?? process.env.GITHUB_SHA,
|
|
365
|
+
error
|
|
366
|
+
};
|
|
367
|
+
appendLedgerEntry(entry, opts);
|
|
368
|
+
}
|
|
369
|
+
function listPactFiles(dir) {
|
|
370
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
371
|
+
const out = [];
|
|
372
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
373
|
+
const full = path__default.default.join(dir, entry.name);
|
|
374
|
+
if (entry.isDirectory()) {
|
|
375
|
+
out.push(...listPactFiles(full));
|
|
376
|
+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
377
|
+
out.push(full);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
function extractInteractionId(metadata) {
|
|
383
|
+
if (!metadata) return void 0;
|
|
384
|
+
const id = metadata.interactionId ?? metadata.interaction_id;
|
|
385
|
+
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
386
|
+
}
|
|
387
|
+
function interactionsFromPactFile(pact) {
|
|
388
|
+
const consumer = pact.consumer?.name;
|
|
389
|
+
const provider = pact.provider?.name;
|
|
390
|
+
if (!consumer || !provider) return [];
|
|
391
|
+
const keys = [];
|
|
392
|
+
for (const m of pact.messages ?? []) {
|
|
393
|
+
keys.push({
|
|
394
|
+
consumer,
|
|
395
|
+
provider,
|
|
396
|
+
interaction: m.description,
|
|
397
|
+
kind: "message",
|
|
398
|
+
interactionId: extractInteractionId(m.metadata)
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
for (const i of pact.interactions ?? []) {
|
|
402
|
+
keys.push({
|
|
403
|
+
consumer,
|
|
404
|
+
provider,
|
|
405
|
+
interaction: i.description,
|
|
406
|
+
kind: "http",
|
|
407
|
+
interactionId: extractInteractionId(i.metadata)
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return keys;
|
|
411
|
+
}
|
|
412
|
+
function parsePactFile(filePath) {
|
|
413
|
+
try {
|
|
414
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/wrapper-provider.ts
|
|
421
|
+
function resolvePactPaths(opts) {
|
|
422
|
+
const urls = opts.pactUrls ?? [];
|
|
423
|
+
return urls.map((u) => path__default.default.resolve(process.cwd(), u));
|
|
424
|
+
}
|
|
425
|
+
function inferConsumerFromPacts(pactPaths, fallback) {
|
|
426
|
+
for (const filePath of pactPaths) {
|
|
427
|
+
const pact = parsePactFile(filePath);
|
|
428
|
+
if (pact?.consumer?.name) return pact.consumer.name;
|
|
429
|
+
}
|
|
430
|
+
if (fallback) return fallback;
|
|
431
|
+
throw new Error(
|
|
432
|
+
"autotel-pact: could not infer consumer from pact files. Pass `consumer` in options."
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
function kindForPactFile(filePath) {
|
|
436
|
+
const pact = parsePactFile(filePath);
|
|
437
|
+
if (!pact) return "message";
|
|
438
|
+
if ((pact.interactions?.length ?? 0) > 0) return "http";
|
|
439
|
+
return "message";
|
|
440
|
+
}
|
|
441
|
+
async function loadVerifier(VerifierClass) {
|
|
442
|
+
if (VerifierClass) return VerifierClass;
|
|
443
|
+
const mod = await import('@pact-foundation/pact');
|
|
444
|
+
const Verifier = mod.Verifier;
|
|
445
|
+
if (!Verifier) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
"autotel-pact: @pact-foundation/pact Verifier not found. Install the peer dependency."
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
return Verifier;
|
|
451
|
+
}
|
|
452
|
+
async function withProviderVerification(verifierOpts, wrapOpts = {}) {
|
|
453
|
+
const pactPaths = resolvePactPaths(verifierOpts);
|
|
454
|
+
const provider = verifierOpts.provider;
|
|
455
|
+
const consumer = inferConsumerFromPacts(pactPaths, wrapOpts.consumer);
|
|
456
|
+
const spanName = wrapOpts.spanName ?? "pact.verification";
|
|
457
|
+
const start = process.hrtime.bigint();
|
|
458
|
+
const Verifier = wrapOpts.skipVerifier ? void 0 : await loadVerifier(wrapOpts.Verifier);
|
|
459
|
+
return autotel.span(spanName, async () => {
|
|
460
|
+
const span = autotel.getActiveSpan();
|
|
461
|
+
span?.setAttributes({
|
|
462
|
+
[PACT_ATTRS.CONSUMER]: consumer,
|
|
463
|
+
[PACT_ATTRS.PROVIDER]: provider,
|
|
464
|
+
[PACT_ATTRS.KIND]: pactPaths.length === 1 ? kindForPactFile(pactPaths[0]) : "message",
|
|
465
|
+
"pact.role": "provider"
|
|
466
|
+
});
|
|
467
|
+
try {
|
|
468
|
+
if (Verifier) {
|
|
469
|
+
await new Verifier(verifierOpts).verifyProvider();
|
|
470
|
+
}
|
|
471
|
+
span?.setAttributes({ [PACT_ATTRS.OUTCOME]: "passed" });
|
|
472
|
+
const ctx = span?.spanContext();
|
|
473
|
+
const base = {
|
|
474
|
+
source: "test",
|
|
475
|
+
role: "provider",
|
|
476
|
+
outcome: "passed",
|
|
477
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
478
|
+
trace_id: ctx?.traceId,
|
|
479
|
+
span_id: ctx?.spanId,
|
|
480
|
+
run_id: process.env.AUTOTEL_PACT_RUN_ID,
|
|
481
|
+
git_sha: process.env.GIT_SHA ?? process.env.GITHUB_SHA
|
|
482
|
+
};
|
|
483
|
+
for (const filePath of pactPaths) {
|
|
484
|
+
const pact = parsePactFile(filePath);
|
|
485
|
+
if (!pact) continue;
|
|
486
|
+
const interactions = interactionsFromPactFile(pact);
|
|
487
|
+
for (const i of interactions) {
|
|
488
|
+
const entry = {
|
|
489
|
+
type: "interaction",
|
|
490
|
+
spec: LEDGER_ENTRY_SPEC,
|
|
491
|
+
consumer: i.consumer,
|
|
492
|
+
provider: i.provider,
|
|
493
|
+
interaction: i.interaction,
|
|
494
|
+
interaction_id: i.interactionId,
|
|
495
|
+
states: [],
|
|
496
|
+
kind: i.kind,
|
|
497
|
+
duration_ms: Number(process.hrtime.bigint() - start) / 1e6,
|
|
498
|
+
...base
|
|
499
|
+
};
|
|
500
|
+
appendLedgerEntry(entry, wrapOpts);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
span?.setAttributes({ [PACT_ATTRS.OUTCOME]: "failed" });
|
|
505
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
506
|
+
const ctx = span?.spanContext();
|
|
507
|
+
appendProviderVerificationFailure(
|
|
508
|
+
{
|
|
509
|
+
consumer,
|
|
510
|
+
provider,
|
|
511
|
+
source: "test",
|
|
512
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
513
|
+
error: message,
|
|
514
|
+
trace_id: ctx?.traceId,
|
|
515
|
+
span_id: ctx?.spanId,
|
|
516
|
+
run_id: process.env.AUTOTEL_PACT_RUN_ID,
|
|
517
|
+
git_sha: process.env.GIT_SHA ?? process.env.GITHUB_SHA
|
|
518
|
+
},
|
|
519
|
+
wrapOpts
|
|
520
|
+
);
|
|
521
|
+
throw error;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
function tagPactInteraction(meta) {
|
|
526
|
+
const span = autotel.getActiveSpan();
|
|
527
|
+
if (!span) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
"autotel-pact: tagPactInteraction requires an active span. Wrap the handler with trace() first."
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const attrs = buildPactAttributes(meta);
|
|
533
|
+
span.setAttributes(attrs);
|
|
534
|
+
if (meta.interactionId) {
|
|
535
|
+
span.setAttribute(PACT_ATTRS.INTERACTION_ID, meta.interactionId);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/processor.ts
|
|
540
|
+
var DEFAULT_MAX_QUEUE = 1024;
|
|
541
|
+
var WARN_INTERVAL_MS = 6e4;
|
|
542
|
+
function attrString(attrs, key) {
|
|
543
|
+
const v = attrs[key];
|
|
544
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
545
|
+
return void 0;
|
|
546
|
+
}
|
|
547
|
+
function attrStates(attrs) {
|
|
548
|
+
const v = attrs[PACT_ATTRS.INTERACTION_STATES];
|
|
549
|
+
if (Array.isArray(v)) {
|
|
550
|
+
return v.filter((s) => typeof s === "string");
|
|
551
|
+
}
|
|
552
|
+
return [];
|
|
553
|
+
}
|
|
554
|
+
function ledgerEntryFromSpan(span) {
|
|
555
|
+
const attrs = span.attributes;
|
|
556
|
+
const consumer = attrString(attrs, PACT_ATTRS.CONSUMER);
|
|
557
|
+
const provider = attrString(attrs, PACT_ATTRS.PROVIDER);
|
|
558
|
+
const description = attrString(attrs, PACT_ATTRS.INTERACTION_DESCRIPTION);
|
|
559
|
+
const interactionId = attrString(attrs, PACT_ATTRS.INTERACTION_ID);
|
|
560
|
+
if (!consumer || !provider || !description && !interactionId) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
const kindRaw = attrString(attrs, PACT_ATTRS.KIND);
|
|
564
|
+
const kind = kindRaw === "http" ? "http" : "message";
|
|
565
|
+
const ctx = span.spanContext();
|
|
566
|
+
const errored = span.status?.code === 2;
|
|
567
|
+
const entry = {
|
|
568
|
+
type: "interaction",
|
|
569
|
+
spec: LEDGER_ENTRY_SPEC,
|
|
570
|
+
consumer,
|
|
571
|
+
provider,
|
|
572
|
+
interaction: description ?? interactionId,
|
|
573
|
+
interaction_id: interactionId,
|
|
574
|
+
states: attrStates(attrs),
|
|
575
|
+
kind,
|
|
576
|
+
outcome: errored ? "failed" : "passed",
|
|
577
|
+
source: "production",
|
|
578
|
+
role: "consumer",
|
|
579
|
+
duration_ms: 0,
|
|
580
|
+
observed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
581
|
+
trace_id: ctx.traceId,
|
|
582
|
+
span_id: ctx.spanId,
|
|
583
|
+
run_id: process.env.AUTOTEL_PACT_RUN_ID,
|
|
584
|
+
git_sha: process.env.GIT_SHA ?? process.env.GITHUB_SHA
|
|
585
|
+
};
|
|
586
|
+
if (errored && span.status?.message) {
|
|
587
|
+
entry.error = span.status.message;
|
|
588
|
+
}
|
|
589
|
+
return entry;
|
|
590
|
+
}
|
|
591
|
+
var PactLedgerSpanProcessor = class {
|
|
592
|
+
opts;
|
|
593
|
+
maxQueue;
|
|
594
|
+
pending = [];
|
|
595
|
+
flushing = false;
|
|
596
|
+
drops = 0;
|
|
597
|
+
lastWarnAt = 0;
|
|
598
|
+
constructor(opts = {}) {
|
|
599
|
+
this.opts = opts;
|
|
600
|
+
this.maxQueue = opts.maxQueueSize ?? DEFAULT_MAX_QUEUE;
|
|
601
|
+
}
|
|
602
|
+
onStart(_span, _parentContext) {
|
|
603
|
+
}
|
|
604
|
+
onEnd(span) {
|
|
605
|
+
try {
|
|
606
|
+
const entry = ledgerEntryFromSpan(span);
|
|
607
|
+
if (!entry) return;
|
|
608
|
+
if (this.pending.length >= this.maxQueue) {
|
|
609
|
+
this.pending.shift();
|
|
610
|
+
this.drops++;
|
|
611
|
+
this.opts.onDrop?.("queue_full");
|
|
612
|
+
this.maybeWarn(
|
|
613
|
+
`autotel-pact: dropped oldest queued ledger entry (queue full, max ${this.maxQueue}). ${this.drops} total drops.`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
this.pending.push({ entry, opts: this.opts });
|
|
617
|
+
queueMicrotask(() => {
|
|
618
|
+
void this.flush();
|
|
619
|
+
});
|
|
620
|
+
} catch (error) {
|
|
621
|
+
this.opts.onWriteError?.(error);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
maybeWarn(message) {
|
|
625
|
+
const now = Date.now();
|
|
626
|
+
if (now - this.lastWarnAt < WARN_INTERVAL_MS) return;
|
|
627
|
+
this.lastWarnAt = now;
|
|
628
|
+
if (this.opts.onWarn) {
|
|
629
|
+
this.opts.onWarn(message);
|
|
630
|
+
} else {
|
|
631
|
+
console.warn(message);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async flush() {
|
|
635
|
+
if (this.flushing) return;
|
|
636
|
+
this.flushing = true;
|
|
637
|
+
try {
|
|
638
|
+
while (this.pending.length > 0) {
|
|
639
|
+
const item = this.pending.shift();
|
|
640
|
+
try {
|
|
641
|
+
await appendLedgerEntryAsync(item.entry, item.opts);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
this.opts.onWriteError?.(error);
|
|
644
|
+
this.maybeWarn(
|
|
645
|
+
`autotel-pact: ledger write failed (fail-open): ${error instanceof Error ? error.message : String(error)}`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} finally {
|
|
650
|
+
this.flushing = false;
|
|
651
|
+
if (this.pending.length > 0) {
|
|
652
|
+
queueMicrotask(() => {
|
|
653
|
+
void this.flush();
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async forceFlush() {
|
|
659
|
+
await this.flush();
|
|
660
|
+
await flushLedgerWrites();
|
|
661
|
+
}
|
|
662
|
+
async shutdown() {
|
|
663
|
+
await this.forceFlush();
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
function createPactLedgerProcessor(opts = {}) {
|
|
667
|
+
return new PactLedgerSpanProcessor(opts);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/broker.ts
|
|
671
|
+
function authHeaders(config) {
|
|
672
|
+
const headers = {
|
|
673
|
+
Accept: "application/json"
|
|
674
|
+
};
|
|
675
|
+
if (config.token) {
|
|
676
|
+
headers.Authorization = `Bearer ${config.token}`;
|
|
677
|
+
} else if (config.username && config.password) {
|
|
678
|
+
const encoded = Buffer.from(`${config.username}:${config.password}`).toString("base64");
|
|
679
|
+
headers.Authorization = `Basic ${encoded}`;
|
|
680
|
+
}
|
|
681
|
+
return headers;
|
|
682
|
+
}
|
|
683
|
+
function trimBaseUrl(url) {
|
|
684
|
+
return url.replace(/\/$/, "");
|
|
685
|
+
}
|
|
686
|
+
function parseBrokerVerificationResult(consumer, provider, json) {
|
|
687
|
+
if (!json || typeof json !== "object") return null;
|
|
688
|
+
const body = json;
|
|
689
|
+
const success = body.success === true || body.success === void 0 && body.result === "success";
|
|
690
|
+
const verifiedAt = typeof body.verifiedAt === "string" ? body.verifiedAt : typeof body.verified_at === "string" ? body.verified_at : typeof body.createdAt === "string" ? body.createdAt : void 0;
|
|
691
|
+
return {
|
|
692
|
+
consumer,
|
|
693
|
+
provider,
|
|
694
|
+
success: !!success,
|
|
695
|
+
verifiedAt
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
async function fetchBrokerVerifications(config, pairs) {
|
|
699
|
+
const base = trimBaseUrl(config.baseUrl);
|
|
700
|
+
const headers = authHeaders(config);
|
|
701
|
+
const results = [];
|
|
702
|
+
for (const { consumer, provider } of pairs) {
|
|
703
|
+
const url = `${base}/pacts/provider/${encodeURIComponent(provider)}/consumer/${encodeURIComponent(consumer)}/latest/verification-results`;
|
|
704
|
+
try {
|
|
705
|
+
const res = await fetch(url, { headers });
|
|
706
|
+
if (!res.ok) {
|
|
707
|
+
results.push({
|
|
708
|
+
consumer,
|
|
709
|
+
provider,
|
|
710
|
+
success: false,
|
|
711
|
+
error: `HTTP ${res.status} ${res.statusText}`.trim()
|
|
712
|
+
});
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const json = await res.json();
|
|
716
|
+
const parsed = parseBrokerVerificationResult(consumer, provider, json);
|
|
717
|
+
results.push(
|
|
718
|
+
parsed ?? {
|
|
719
|
+
consumer,
|
|
720
|
+
provider,
|
|
721
|
+
success: false,
|
|
722
|
+
error: "Unparseable broker response"
|
|
723
|
+
}
|
|
724
|
+
);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
results.push({
|
|
727
|
+
consumer,
|
|
728
|
+
provider,
|
|
729
|
+
success: false,
|
|
730
|
+
error: error instanceof Error ? error.message : String(error)
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return results;
|
|
735
|
+
}
|
|
736
|
+
function brokerConfigFromEnv() {
|
|
737
|
+
const baseUrl = process.env.PACT_BROKER_BASE_URL;
|
|
738
|
+
if (!baseUrl) return void 0;
|
|
739
|
+
return {
|
|
740
|
+
baseUrl,
|
|
741
|
+
token: process.env.PACT_BROKER_TOKEN,
|
|
742
|
+
username: process.env.PACT_BROKER_USERNAME,
|
|
743
|
+
password: process.env.PACT_BROKER_PASSWORD
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/audit.ts
|
|
748
|
+
var DEFAULT_PACTS_DIR = "./pacts";
|
|
749
|
+
var DEFAULT_WINDOW_DAYS = 14;
|
|
750
|
+
function keyOf(k) {
|
|
751
|
+
const identity = k.interactionId ?? k.interaction;
|
|
752
|
+
return `${k.consumer}::${k.provider}::${k.kind}::${identity}`;
|
|
753
|
+
}
|
|
754
|
+
function pairKey(consumer, provider) {
|
|
755
|
+
return `${consumer}::${provider}`;
|
|
756
|
+
}
|
|
757
|
+
function inWindow(observedAt, cutoff) {
|
|
758
|
+
const t = Date.parse(observedAt);
|
|
759
|
+
return Number.isFinite(t) && t >= cutoff;
|
|
760
|
+
}
|
|
761
|
+
function computeAuditMatrix(input) {
|
|
762
|
+
const windowDays = input.windowDays ?? DEFAULT_WINDOW_DAYS;
|
|
763
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
764
|
+
const cutoff = now.getTime() - windowDays * 24 * 60 * 60 * 1e3;
|
|
765
|
+
const verificationFailures = [];
|
|
766
|
+
const recentInteractions = [];
|
|
767
|
+
for (const entry of input.ledger) {
|
|
768
|
+
if (!inWindow(entry.observed_at, cutoff)) continue;
|
|
769
|
+
if (isProviderVerificationRun(entry)) {
|
|
770
|
+
verificationFailures.push(entry);
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (isInteractionLedgerEntry(entry)) {
|
|
774
|
+
recentInteractions.push(entry);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const brokerByPair = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const b of input.brokerVerifications ?? []) {
|
|
779
|
+
brokerByPair.set(pairKey(b.consumer, b.provider), b);
|
|
780
|
+
}
|
|
781
|
+
const testSeenByKey = /* @__PURE__ */ new Map();
|
|
782
|
+
const prodSeenByKey = /* @__PURE__ */ new Map();
|
|
783
|
+
const providerVerifiedByKey = /* @__PURE__ */ new Map();
|
|
784
|
+
const anyObservedByKey = /* @__PURE__ */ new Map();
|
|
785
|
+
for (const entry of recentInteractions) {
|
|
786
|
+
const k = keyOf({
|
|
787
|
+
consumer: entry.consumer,
|
|
788
|
+
provider: entry.provider,
|
|
789
|
+
interaction: entry.interaction,
|
|
790
|
+
kind: entry.kind,
|
|
791
|
+
interactionId: entry.interaction_id
|
|
792
|
+
});
|
|
793
|
+
const push = (map) => {
|
|
794
|
+
const arr = map.get(k) ?? [];
|
|
795
|
+
arr.push(entry);
|
|
796
|
+
map.set(k, arr);
|
|
797
|
+
};
|
|
798
|
+
push(anyObservedByKey);
|
|
799
|
+
if (entry.source === "test" && entry.role === "consumer") {
|
|
800
|
+
push(testSeenByKey);
|
|
801
|
+
}
|
|
802
|
+
if (entry.source === "production") {
|
|
803
|
+
push(prodSeenByKey);
|
|
804
|
+
}
|
|
805
|
+
if (entry.role === "provider" && entry.outcome === "passed") {
|
|
806
|
+
push(providerVerifiedByKey);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const contractedByKey = /* @__PURE__ */ new Map();
|
|
810
|
+
for (const c of input.contracted) {
|
|
811
|
+
contractedByKey.set(keyOf(c), c);
|
|
812
|
+
}
|
|
813
|
+
const rows = [];
|
|
814
|
+
function pushRow(parts, k, isContracted) {
|
|
815
|
+
const testObs = testSeenByKey.get(k) ?? [];
|
|
816
|
+
const prodObs = prodSeenByKey.get(k) ?? [];
|
|
817
|
+
const providerObs = providerVerifiedByKey.get(k) ?? [];
|
|
818
|
+
const allObs = anyObservedByKey.get(k) ?? [];
|
|
819
|
+
const latest = allObs.toSorted(
|
|
820
|
+
(a, b) => b.observed_at.localeCompare(a.observed_at)
|
|
821
|
+
)[0];
|
|
822
|
+
const broker = brokerByPair.get(pairKey(parts.consumer, parts.provider));
|
|
823
|
+
rows.push({
|
|
824
|
+
consumer: parts.consumer,
|
|
825
|
+
provider: parts.provider,
|
|
826
|
+
interaction: parts.interaction,
|
|
827
|
+
interaction_id: parts.interactionId ?? latest?.interaction_id,
|
|
828
|
+
kind: parts.kind,
|
|
829
|
+
contracted: isContracted,
|
|
830
|
+
observed: testObs.length > 0 || prodObs.length > 0,
|
|
831
|
+
test_seen: testObs.length > 0,
|
|
832
|
+
prod_seen: prodObs.length > 0,
|
|
833
|
+
provider_verified: providerObs.length > 0,
|
|
834
|
+
broker_verified: broker?.success === true,
|
|
835
|
+
broker_verified_at: broker?.verifiedAt,
|
|
836
|
+
broker_error: broker?.error,
|
|
837
|
+
last_observed_at: latest?.observed_at,
|
|
838
|
+
last_outcome: latest?.outcome
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
for (const [k, contracted] of contractedByKey) {
|
|
842
|
+
pushRow(contracted, k, true);
|
|
843
|
+
}
|
|
844
|
+
for (const [k, observations] of anyObservedByKey) {
|
|
845
|
+
if (contractedByKey.has(k)) continue;
|
|
846
|
+
const first = observations[0];
|
|
847
|
+
pushRow(
|
|
848
|
+
{
|
|
849
|
+
consumer: first.consumer,
|
|
850
|
+
provider: first.provider,
|
|
851
|
+
interaction: first.interaction,
|
|
852
|
+
kind: first.kind,
|
|
853
|
+
interactionId: first.interaction_id
|
|
854
|
+
},
|
|
855
|
+
k,
|
|
856
|
+
false
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
rows.sort(
|
|
860
|
+
(a, b) => a.consumer.localeCompare(b.consumer) || a.provider.localeCompare(b.provider) || a.interaction.localeCompare(b.interaction)
|
|
861
|
+
);
|
|
862
|
+
const counts = {
|
|
863
|
+
total: rows.length,
|
|
864
|
+
contracted: rows.filter((r) => r.contracted).length,
|
|
865
|
+
observed: rows.filter((r) => r.observed).length,
|
|
866
|
+
contracted_and_test_seen: rows.filter((r) => r.contracted && r.test_seen).length,
|
|
867
|
+
contracted_not_test_seen: rows.filter((r) => r.contracted && !r.test_seen).length,
|
|
868
|
+
test_or_prod_seen_not_contracted: rows.filter(
|
|
869
|
+
(r) => !r.contracted && (r.test_seen || r.prod_seen)
|
|
870
|
+
).length,
|
|
871
|
+
test_seen: rows.filter((r) => r.test_seen).length,
|
|
872
|
+
prod_seen: rows.filter((r) => r.prod_seen).length,
|
|
873
|
+
provider_verified: rows.filter((r) => r.provider_verified).length,
|
|
874
|
+
broker_verified: rows.filter((r) => r.broker_verified).length
|
|
875
|
+
};
|
|
876
|
+
const matrix = {
|
|
877
|
+
spec: AUDIT_MATRIX_SPEC,
|
|
878
|
+
rows,
|
|
879
|
+
counts,
|
|
880
|
+
window_days: windowDays,
|
|
881
|
+
generated_at: now.toISOString()
|
|
882
|
+
};
|
|
883
|
+
if (verificationFailures.length > 0) {
|
|
884
|
+
matrix.verification_failures = verificationFailures;
|
|
885
|
+
}
|
|
886
|
+
return matrix;
|
|
887
|
+
}
|
|
888
|
+
async function runAudit(opts = {}) {
|
|
889
|
+
const pactsDir = path__default.default.resolve(process.cwd(), opts.pactsDir ?? DEFAULT_PACTS_DIR);
|
|
890
|
+
const contracted = [];
|
|
891
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
892
|
+
for (const file of listPactFiles(pactsDir)) {
|
|
893
|
+
const pact = parsePactFile(file);
|
|
894
|
+
if (!pact) continue;
|
|
895
|
+
const interactions = interactionsFromPactFile(pact);
|
|
896
|
+
contracted.push(...interactions);
|
|
897
|
+
const consumer = pact.consumer?.name;
|
|
898
|
+
const provider = pact.provider?.name;
|
|
899
|
+
if (consumer && provider) {
|
|
900
|
+
pairs.add(pairKey(consumer, provider));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const ledger = readLedger(opts);
|
|
904
|
+
let brokerVerifications;
|
|
905
|
+
if (opts.broker) {
|
|
906
|
+
brokerVerifications = await fetchBrokerVerifications(opts.broker, [...pairs].map((p) => {
|
|
907
|
+
const [consumer, provider] = p.split("::");
|
|
908
|
+
return { consumer, provider };
|
|
909
|
+
}));
|
|
910
|
+
}
|
|
911
|
+
return computeAuditMatrix({
|
|
912
|
+
contracted,
|
|
913
|
+
ledger,
|
|
914
|
+
brokerVerifications,
|
|
915
|
+
windowDays: opts.windowDays
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
function runAuditSync(opts = {}) {
|
|
919
|
+
const pactsDir = path__default.default.resolve(process.cwd(), opts.pactsDir ?? DEFAULT_PACTS_DIR);
|
|
920
|
+
const contracted = [];
|
|
921
|
+
for (const file of listPactFiles(pactsDir)) {
|
|
922
|
+
const pact = parsePactFile(file);
|
|
923
|
+
if (pact) contracted.push(...interactionsFromPactFile(pact));
|
|
924
|
+
}
|
|
925
|
+
const ledger = readLedger(opts);
|
|
926
|
+
return computeAuditMatrix({
|
|
927
|
+
contracted,
|
|
928
|
+
ledger,
|
|
929
|
+
windowDays: opts.windowDays
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/labels.ts
|
|
934
|
+
var BROKER_GRANULARITY_WARNING = "Broker verification proves the latest pact between a consumer and provider was verified. It does not prove each interaction was individually observed by autotel-pact.";
|
|
935
|
+
|
|
936
|
+
exports.AUDIT_MATRIX_SPEC = AUDIT_MATRIX_SPEC;
|
|
937
|
+
exports.BROKER_GRANULARITY_WARNING = BROKER_GRANULARITY_WARNING;
|
|
938
|
+
exports.LEDGER_ENTRY_SPEC = LEDGER_ENTRY_SPEC;
|
|
939
|
+
exports.PACT_ATTRS = PACT_ATTRS;
|
|
940
|
+
exports.PactLedgerSpanProcessor = PactLedgerSpanProcessor;
|
|
941
|
+
exports.appendLedgerEntry = appendLedgerEntry;
|
|
942
|
+
exports.appendLedgerEntryAsync = appendLedgerEntryAsync;
|
|
943
|
+
exports.appendProviderVerificationFailure = appendProviderVerificationFailure;
|
|
944
|
+
exports.brokerConfigFromEnv = brokerConfigFromEnv;
|
|
945
|
+
exports.buildPactAttributes = buildPactAttributes;
|
|
946
|
+
exports.computeAuditMatrix = computeAuditMatrix;
|
|
947
|
+
exports.createPactLedgerProcessor = createPactLedgerProcessor;
|
|
948
|
+
exports.fetchBrokerVerifications = fetchBrokerVerifications;
|
|
949
|
+
exports.flushLedgerWrites = flushLedgerWrites;
|
|
950
|
+
exports.interactionsFromPactFile = interactionsFromPactFile;
|
|
951
|
+
exports.isInteractionLedgerEntry = isInteractionLedgerEntry;
|
|
952
|
+
exports.isProviderVerificationRun = isProviderVerificationRun;
|
|
953
|
+
exports.keyOf = keyOf;
|
|
954
|
+
exports.ledgerPath = ledgerPath;
|
|
955
|
+
exports.listPactFiles = listPactFiles;
|
|
956
|
+
exports.outcomeAttribute = outcomeAttribute;
|
|
957
|
+
exports.parseBrokerVerificationResult = parseBrokerVerificationResult;
|
|
958
|
+
exports.parsePactFile = parsePactFile;
|
|
959
|
+
exports.readLedger = readLedger;
|
|
960
|
+
exports.runAudit = runAudit;
|
|
961
|
+
exports.runAuditSync = runAuditSync;
|
|
962
|
+
exports.tagPactInteraction = tagPactInteraction;
|
|
963
|
+
exports.withHttpPactInteraction = withHttpPactInteraction;
|
|
964
|
+
exports.withPactInteraction = withPactInteraction;
|
|
965
|
+
exports.withProviderVerification = withProviderVerification;
|
|
966
|
+
//# sourceMappingURL=index.cjs.map
|
|
967
|
+
//# sourceMappingURL=index.cjs.map
|