@yolk-sdk/conformance 0.1.0-canary.96
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/LICENSE +21 -0
- package/README.md +316 -0
- package/dist/case.d.mts +89 -0
- package/dist/case.d.mts.map +1 -0
- package/dist/case.mjs +101 -0
- package/dist/case.mjs.map +1 -0
- package/dist/fixture.d.mts +432 -0
- package/dist/fixture.d.mts.map +1 -0
- package/dist/fixture.mjs +391 -0
- package/dist/fixture.mjs.map +1 -0
- package/dist/record.d.mts +96 -0
- package/dist/record.d.mts.map +1 -0
- package/dist/record.mjs +212 -0
- package/dist/record.mjs.map +1 -0
- package/dist/replay.d.mts +316 -0
- package/dist/replay.d.mts.map +1 -0
- package/dist/replay.mjs +272 -0
- package/dist/replay.mjs.map +1 -0
- package/dist/runner.d.mts +163 -0
- package/dist/runner.d.mts.map +1 -0
- package/dist/runner.mjs +264 -0
- package/dist/runner.mjs.map +1 -0
- package/dist/wire-internal.d.mts +90 -0
- package/dist/wire-internal.d.mts.map +1 -0
- package/dist/wire-internal.mjs +212 -0
- package/dist/wire-internal.mjs.map +1 -0
- package/package.json +75 -0
- package/src/case.ts +153 -0
- package/src/fixture.ts +625 -0
- package/src/record.ts +377 -0
- package/src/replay.ts +617 -0
- package/src/runner.ts +559 -0
- package/src/wire-internal.ts +382 -0
package/dist/fixture.mjs
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { ConformanceObservation } from "./case.mjs";
|
|
2
|
+
import { apiKeyPatterns, bearerPattern, credentialFieldPattern, credentialParamPattern, decodeBase64Bytes, hasLiveCredentialParam, isCredentialHeaderName, syntheticPortCredentialParams } from "./wire-internal.mjs";
|
|
3
|
+
import { Option, Predicate } from "effect";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
//#region src/fixture.ts
|
|
6
|
+
/**
|
|
7
|
+
* Fixture data models: recorded HTTP exchanges with outside services (`WireFixture`) and recorded
|
|
8
|
+
* calls through a host-provided port that is not HTTP (`PortFixture`, for example an email client
|
|
9
|
+
* port).
|
|
10
|
+
*
|
|
11
|
+
* Fixtures are plain, serializable data. They must contain synthetic or
|
|
12
|
+
* scrubbed content only: never credentials, cookies, or customer data. Use
|
|
13
|
+
* `scanFixtureForSecrets` / `scanPortFixtureForSecrets` before committing a fixture.
|
|
14
|
+
*
|
|
15
|
+
* @experimental
|
|
16
|
+
*/
|
|
17
|
+
/** `verified` = recorded from a live service; `unverified` = synthetic placeholder. */
|
|
18
|
+
const WireFixtureEvidence = Schema.Literals(["verified", "unverified"]);
|
|
19
|
+
/** Lowercase header name to value. Request headers are allowlisted and never credentials. */
|
|
20
|
+
const WireHeaders = Schema.Record(Schema.String, Schema.String);
|
|
21
|
+
const HttpStatus = Schema.Int.check(Schema.isBetween({
|
|
22
|
+
minimum: 100,
|
|
23
|
+
maximum: 599
|
|
24
|
+
}));
|
|
25
|
+
const WireRequest = Schema.Struct({
|
|
26
|
+
method: Schema.NonEmptyString,
|
|
27
|
+
/** Absolute URL including any query string. */
|
|
28
|
+
url: Schema.NonEmptyString,
|
|
29
|
+
headers: Schema.optionalKey(WireHeaders),
|
|
30
|
+
/** Parsed JSON request body, or the raw text when it is not JSON (for example a form body). */
|
|
31
|
+
body: Schema.optionalKey(Schema.Json)
|
|
32
|
+
});
|
|
33
|
+
const Base64String = Schema.String.check(Schema.isBase64());
|
|
34
|
+
/**
|
|
35
|
+
* One recorded network chunk. A chunk that is valid UTF-8 on its own is stored
|
|
36
|
+
* as readable text (empty chunks as `""`); any other chunk is stored as
|
|
37
|
+
* `{ base64 }` holding its exact bytes. Replay emits exactly these bytes.
|
|
38
|
+
*/
|
|
39
|
+
const WireChunk = Schema.Union([Schema.String, Schema.Struct({ base64: Base64String })]);
|
|
40
|
+
const absent = Schema.optionalKey(Schema.Never);
|
|
41
|
+
/** A response whose whole body is valid UTF-8, stored as one string. */
|
|
42
|
+
const WireTextBodyResponse = Schema.Struct({
|
|
43
|
+
status: HttpStatus,
|
|
44
|
+
headers: WireHeaders,
|
|
45
|
+
body: Schema.String,
|
|
46
|
+
bodyBase64: absent,
|
|
47
|
+
chunks: absent
|
|
48
|
+
});
|
|
49
|
+
/** A response whose whole body is not valid UTF-8 (for example a PDF), stored as base64 bytes. */
|
|
50
|
+
const WireBase64BodyResponse = Schema.Struct({
|
|
51
|
+
status: HttpStatus,
|
|
52
|
+
headers: WireHeaders,
|
|
53
|
+
bodyBase64: Base64String,
|
|
54
|
+
body: absent,
|
|
55
|
+
chunks: absent
|
|
56
|
+
});
|
|
57
|
+
/** A response recorded as one whole body: exactly one of `body` or `bodyBase64`. */
|
|
58
|
+
const WireBodyResponse = Schema.Union([WireTextBodyResponse, WireBase64BodyResponse]);
|
|
59
|
+
/**
|
|
60
|
+
* A streamed response (for example `text/event-stream`). Each entry is one
|
|
61
|
+
* network chunk (see `WireChunk`); chunk boundaries and bytes are preserved on
|
|
62
|
+
* replay.
|
|
63
|
+
*/
|
|
64
|
+
const WireStreamResponse = Schema.Struct({
|
|
65
|
+
status: HttpStatus,
|
|
66
|
+
headers: WireHeaders,
|
|
67
|
+
chunks: Schema.Array(WireChunk),
|
|
68
|
+
body: absent,
|
|
69
|
+
bodyBase64: absent
|
|
70
|
+
});
|
|
71
|
+
const WireResponse = Schema.Union([
|
|
72
|
+
WireTextBodyResponse,
|
|
73
|
+
WireBase64BodyResponse,
|
|
74
|
+
WireStreamResponse
|
|
75
|
+
]);
|
|
76
|
+
const WireExchange = Schema.Struct({
|
|
77
|
+
request: WireRequest,
|
|
78
|
+
response: WireResponse
|
|
79
|
+
});
|
|
80
|
+
const RecordedAtDate = Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/));
|
|
81
|
+
const WireFixture = Schema.Struct({
|
|
82
|
+
id: Schema.NonEmptyString,
|
|
83
|
+
caseId: Schema.NonEmptyString,
|
|
84
|
+
evidence: WireFixtureEvidence,
|
|
85
|
+
/** Calendar date of the recording (`YYYY-MM-DD`, UTC). */
|
|
86
|
+
recordedAt: RecordedAtDate,
|
|
87
|
+
/** Synthetic account label, for example `synthetic`; never a real account name. */
|
|
88
|
+
account: Schema.NonEmptyString,
|
|
89
|
+
endpoint: Schema.NonEmptyString,
|
|
90
|
+
model: Schema.optionalKey(Schema.String),
|
|
91
|
+
note: Schema.optionalKey(Schema.String),
|
|
92
|
+
exchanges: Schema.NonEmptyArray(WireExchange)
|
|
93
|
+
});
|
|
94
|
+
/** Decode unknown input (for example a JSON file) into a `WireFixture`. */
|
|
95
|
+
const decodeWireFixture = Schema.decodeUnknownEffect(WireFixture);
|
|
96
|
+
const isWireStreamResponse = (response) => Predicate.hasProperty(response, "chunks");
|
|
97
|
+
const isWireBase64BodyResponse = (response) => Predicate.hasProperty(response, "bodyBase64");
|
|
98
|
+
const millisPerDay = 864e5;
|
|
99
|
+
const recordedAtPattern = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
100
|
+
const recordedAtMillis = (recordedAt) => {
|
|
101
|
+
const match = recordedAtPattern.exec(recordedAt);
|
|
102
|
+
if (match === null) return NaN;
|
|
103
|
+
return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Whole days between `recordedAt` (UTC midnight) and `now`. `NaN` when
|
|
107
|
+
* `recordedAt` is not a `YYYY-MM-DD` date.
|
|
108
|
+
*/
|
|
109
|
+
const fixtureAgeDays = (fixture, now) => Math.floor((now.getTime() - recordedAtMillis(fixture.recordedAt)) / millisPerDay);
|
|
110
|
+
/**
|
|
111
|
+
* True when the fixture is older than `maxAgeDays` (default 30). Fixtures with
|
|
112
|
+
* an unreadable `recordedAt` are treated as stale.
|
|
113
|
+
*/
|
|
114
|
+
const isFixtureStale = (fixture, now, maxAgeDays = 30) => {
|
|
115
|
+
const age = fixtureAgeDays(fixture, now);
|
|
116
|
+
return Number.isNaN(age) || age > maxAgeDays;
|
|
117
|
+
};
|
|
118
|
+
const scanText = (text, location, issues) => {
|
|
119
|
+
if (bearerPattern.test(text)) issues.push({
|
|
120
|
+
kind: "bearer_token",
|
|
121
|
+
location
|
|
122
|
+
});
|
|
123
|
+
if (apiKeyPatterns.some((pattern) => pattern.test(text))) issues.push({
|
|
124
|
+
kind: "api_key",
|
|
125
|
+
location
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
const scanHeaders = (headers, location, issues) => {
|
|
129
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
130
|
+
const headerLocation = `${location}.${name}`;
|
|
131
|
+
if (isCredentialHeaderName(name)) issues.push({
|
|
132
|
+
kind: "credential_header",
|
|
133
|
+
location: headerLocation
|
|
134
|
+
});
|
|
135
|
+
scanText(value, headerLocation, issues);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const scanUrl = (url, location, issues) => {
|
|
139
|
+
if (credentialParamPattern.test(url)) issues.push({
|
|
140
|
+
kind: "credential_query_param",
|
|
141
|
+
location
|
|
142
|
+
});
|
|
143
|
+
scanText(url, location, issues);
|
|
144
|
+
};
|
|
145
|
+
const scanJson = (value, location, issues) => {
|
|
146
|
+
if (Predicate.isString(value)) {
|
|
147
|
+
scanText(value, location, issues);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
value.forEach((item, index) => scanJson(item, `${location}[${index}]`, issues));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (value !== null && !Predicate.isNumber(value) && !Predicate.isBoolean(value)) for (const [key, item] of Object.entries(value)) {
|
|
155
|
+
const itemLocation = `${location}.${key}`;
|
|
156
|
+
if (credentialFieldPattern.test(key) && Predicate.isString(item) && item.length > 0) issues.push({
|
|
157
|
+
kind: "credential_field",
|
|
158
|
+
location: itemLocation
|
|
159
|
+
});
|
|
160
|
+
scanJson(item, itemLocation, issues);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const parseJsonOption = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
|
|
164
|
+
const sseDataPayloads = (text) => text.replace(/\r\n?/g, "\n").split("\n\n").flatMap((event) => {
|
|
165
|
+
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, ""));
|
|
166
|
+
return data.length > 0 ? [data.join("\n")] : [];
|
|
167
|
+
});
|
|
168
|
+
/**
|
|
169
|
+
* Scan a whole payload (request/response body or reassembled stream): token
|
|
170
|
+
* patterns, form-encoded credential parameters, and credential fields in the
|
|
171
|
+
* payload itself when it is JSON, or in each SSE `data:` payload that is JSON
|
|
172
|
+
* (located as `<location>.events[n]`).
|
|
173
|
+
*/
|
|
174
|
+
const scanPayload = (text, location, issues) => {
|
|
175
|
+
scanText(text, location, issues);
|
|
176
|
+
if (credentialParamPattern.test(text)) issues.push({
|
|
177
|
+
kind: "credential_query_param",
|
|
178
|
+
location
|
|
179
|
+
});
|
|
180
|
+
const json = parseJsonOption(text);
|
|
181
|
+
if (Option.isSome(json)) {
|
|
182
|
+
scanJson(json.value, location, issues);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
sseDataPayloads(text).forEach((payload, index) => {
|
|
186
|
+
const event = parseJsonOption(payload);
|
|
187
|
+
if (Option.isSome(event)) scanJson(event.value, `${location}.events[${index}]`, issues);
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
const lossyText = (bytes) => new TextDecoder().decode(bytes);
|
|
191
|
+
const chunkBytes = (chunk) => Predicate.isString(chunk) ? new TextEncoder().encode(chunk) : Option.getOrElse(decodeBase64Bytes(chunk.base64), () => new Uint8Array());
|
|
192
|
+
const concatBytes = (parts) => {
|
|
193
|
+
const joined = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
|
|
194
|
+
let offset = 0;
|
|
195
|
+
for (const part of parts) {
|
|
196
|
+
joined.set(part, offset);
|
|
197
|
+
offset += part.length;
|
|
198
|
+
}
|
|
199
|
+
return joined;
|
|
200
|
+
};
|
|
201
|
+
const scanStream = (chunks, location, issues) => {
|
|
202
|
+
const parts = chunks.map(chunkBytes);
|
|
203
|
+
parts.forEach((bytes, index) => scanText(lossyText(bytes), `${location}[${index}]`, issues));
|
|
204
|
+
scanPayload(lossyText(concatBytes(parts)), location, issues);
|
|
205
|
+
};
|
|
206
|
+
const uniqueIssues = (issues) => {
|
|
207
|
+
const seen = /* @__PURE__ */ new Set();
|
|
208
|
+
return issues.filter((issue) => {
|
|
209
|
+
const key = `${issue.kind} ${issue.location}`;
|
|
210
|
+
if (seen.has(key)) return false;
|
|
211
|
+
seen.add(key);
|
|
212
|
+
return true;
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Pure secret scan. Flags credential headers, bearer tokens, common API-key
|
|
217
|
+
* prefixes, JWTs, private keys, credential query/form parameters, and
|
|
218
|
+
* credential JSON fields anywhere in the fixture: metadata, URLs, headers,
|
|
219
|
+
* request bodies, response bodies (text or decodable base64), each stream
|
|
220
|
+
* chunk, and the reassembled stream (so a secret split across chunks is still
|
|
221
|
+
* found). JSON bodies and SSE `data:` payloads get the credential-field scan.
|
|
222
|
+
* Issues name locations only. Returns an empty array when clean.
|
|
223
|
+
*/
|
|
224
|
+
const scanFixtureForSecrets = (fixture) => {
|
|
225
|
+
const issues = [];
|
|
226
|
+
scanText(fixture.id, "id", issues);
|
|
227
|
+
scanText(fixture.caseId, "caseId", issues);
|
|
228
|
+
scanText(fixture.account, "account", issues);
|
|
229
|
+
scanUrl(fixture.endpoint, "endpoint", issues);
|
|
230
|
+
if (fixture.model !== void 0) scanText(fixture.model, "model", issues);
|
|
231
|
+
if (fixture.note !== void 0) scanText(fixture.note, "note", issues);
|
|
232
|
+
fixture.exchanges.forEach((exchange, index) => {
|
|
233
|
+
const base = `exchanges[${index}]`;
|
|
234
|
+
const requestBody = exchange.request.body;
|
|
235
|
+
scanUrl(exchange.request.url, `${base}.request.url`, issues);
|
|
236
|
+
scanHeaders(exchange.request.headers, `${base}.request.headers`, issues);
|
|
237
|
+
if (Predicate.isString(requestBody)) scanPayload(requestBody, `${base}.request.body`, issues);
|
|
238
|
+
else if (requestBody !== void 0) scanJson(requestBody, `${base}.request.body`, issues);
|
|
239
|
+
const response = exchange.response;
|
|
240
|
+
scanHeaders(response.headers, `${base}.response.headers`, issues);
|
|
241
|
+
if (isWireStreamResponse(response)) scanStream(response.chunks, `${base}.response.chunks`, issues);
|
|
242
|
+
else if (isWireBase64BodyResponse(response)) {
|
|
243
|
+
const bytes = decodeBase64Bytes(response.bodyBase64);
|
|
244
|
+
if (Option.isSome(bytes)) scanPayload(lossyText(bytes.value), `${base}.response.bodyBase64`, issues);
|
|
245
|
+
} else scanPayload(response.body, `${base}.response.body`, issues);
|
|
246
|
+
});
|
|
247
|
+
return uniqueIssues(issues);
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* A failure a port answered instead of a value. `expected` is the port's value-level failure (for
|
|
251
|
+
* example a connector `ActionResult.failure`); `error` is the port's typed error channel. `code`
|
|
252
|
+
* is a sanitized classification, never raw provider text.
|
|
253
|
+
*/
|
|
254
|
+
const PortFailure = Schema.Struct({
|
|
255
|
+
kind: Schema.Literals(["expected", "error"]),
|
|
256
|
+
code: Schema.NonEmptyString,
|
|
257
|
+
message: Schema.String,
|
|
258
|
+
status: Schema.optionalKey(Schema.Int)
|
|
259
|
+
});
|
|
260
|
+
const portFixtureFields = {
|
|
261
|
+
id: Schema.NonEmptyString,
|
|
262
|
+
/** Port name, for example `EmailClient`. */
|
|
263
|
+
port: Schema.NonEmptyString,
|
|
264
|
+
/** Port method, for example `listMessages`. */
|
|
265
|
+
method: Schema.NonEmptyString,
|
|
266
|
+
/** The request as plain JSON, with every credential field removed (`redactPortPayload`). */
|
|
267
|
+
request: Schema.Json,
|
|
268
|
+
/**
|
|
269
|
+
* The live observation this call was recorded from. Absent means a synthetic placeholder
|
|
270
|
+
* (`unverified`); present means `verified` as of `observed.date`.
|
|
271
|
+
*/
|
|
272
|
+
observed: Schema.optionalKey(ConformanceObservation),
|
|
273
|
+
note: Schema.optionalKey(Schema.String)
|
|
274
|
+
};
|
|
275
|
+
/** A port call answered with a value (`response`, plain JSON). */
|
|
276
|
+
const PortValueFixture = Schema.Struct({
|
|
277
|
+
...portFixtureFields,
|
|
278
|
+
response: Schema.Json,
|
|
279
|
+
failure: absent
|
|
280
|
+
});
|
|
281
|
+
/** A port call answered with a failure. */
|
|
282
|
+
const PortFailureFixture = Schema.Struct({
|
|
283
|
+
...portFixtureFields,
|
|
284
|
+
failure: PortFailure,
|
|
285
|
+
response: absent
|
|
286
|
+
});
|
|
287
|
+
/**
|
|
288
|
+
* One recorded call through a host-provided port: the port, the method, the credential-free JSON
|
|
289
|
+
* request, and exactly one of `response` or `failure`. Plain JSON only, so one fixture backs
|
|
290
|
+
* replay, an emulator, and parity checks alike.
|
|
291
|
+
*/
|
|
292
|
+
const PortFixture = Schema.Union([PortValueFixture, PortFailureFixture]);
|
|
293
|
+
/** Decode unknown input (for example a JSON file) into a `PortFixture`. */
|
|
294
|
+
const decodePortFixture = Schema.decodeUnknownEffect(PortFixture);
|
|
295
|
+
const isPortFixture = (fixture) => !Predicate.hasProperty(fixture, "exchanges");
|
|
296
|
+
const isPortFailureFixture = (fixture) => Predicate.hasProperty(fixture, "failure") && fixture.failure !== void 0;
|
|
297
|
+
/**
|
|
298
|
+
* Evidence and date of any fixture: a `WireFixture`'s `evidence` and `recordedAt`; a
|
|
299
|
+
* `PortFixture` is `verified` as of `observed.date` when observed, else `unverified` without a
|
|
300
|
+
* date.
|
|
301
|
+
*/
|
|
302
|
+
const conformanceFixtureEvidence = (fixture) => {
|
|
303
|
+
if (!isPortFixture(fixture)) return {
|
|
304
|
+
evidence: fixture.evidence,
|
|
305
|
+
date: fixture.recordedAt
|
|
306
|
+
};
|
|
307
|
+
return fixture.observed === void 0 ? { evidence: "unverified" } : {
|
|
308
|
+
evidence: "verified",
|
|
309
|
+
date: fixture.observed.date
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
const portCredentialKeyPattern = /^credentials?$/i;
|
|
313
|
+
const isJsonRecord = (value) => value !== null && Predicate.isObject(value) && !Array.isArray(value);
|
|
314
|
+
/** True for a port payload key that holds a credential (`credential`, `password`, `token`, ...). */
|
|
315
|
+
const isPortCredentialKey = (key) => portCredentialKeyPattern.test(key) || credentialFieldPattern.test(key);
|
|
316
|
+
/**
|
|
317
|
+
* A copy of a port payload without credential fields (see `isPortCredentialKey`: `credential(s)`
|
|
318
|
+
* plus the credential field names, AWS-style `accessKeyId` / `secretAccessKey` / `sessionToken`
|
|
319
|
+
* and their snake_case forms included), at any depth.
|
|
320
|
+
* Port requests often carry the resolved credential; record and compare requests only after this.
|
|
321
|
+
*/
|
|
322
|
+
const redactPortPayload = (value) => {
|
|
323
|
+
if (Array.isArray(value)) return value.map(redactPortPayload);
|
|
324
|
+
if (!isJsonRecord(value)) return value;
|
|
325
|
+
const copy = {};
|
|
326
|
+
for (const [key, item] of Object.entries(value)) if (!isPortCredentialKey(key)) copy[key] = redactPortPayload(item);
|
|
327
|
+
return copy;
|
|
328
|
+
};
|
|
329
|
+
/** A port string: the token patterns plus credential query or form parameters (a signed URL). */
|
|
330
|
+
const scanPortText = (text, location, issues) => {
|
|
331
|
+
scanText(text, location, issues);
|
|
332
|
+
if (hasLiveCredentialParam(text)) issues.push({
|
|
333
|
+
kind: "credential_query_param",
|
|
334
|
+
location
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
const scanPortJson = (value, location, issues) => {
|
|
338
|
+
scanJson(value, location, issues);
|
|
339
|
+
const visit = (item, itemLocation) => {
|
|
340
|
+
if (Predicate.isString(item)) {
|
|
341
|
+
if (hasLiveCredentialParam(item)) issues.push({
|
|
342
|
+
kind: "credential_query_param",
|
|
343
|
+
location: itemLocation
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (Array.isArray(item)) {
|
|
348
|
+
item.forEach((entry, index) => visit(entry, `${itemLocation}[${index}]`));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!isJsonRecord(item)) return;
|
|
352
|
+
for (const [key, entry] of Object.entries(item)) {
|
|
353
|
+
if (portCredentialKeyPattern.test(key) && entry !== null) issues.push({
|
|
354
|
+
kind: "credential_field",
|
|
355
|
+
location: `${itemLocation}.${key}`
|
|
356
|
+
});
|
|
357
|
+
visit(entry, `${itemLocation}.${key}`);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
visit(value, location);
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* Pure secret scan of a `PortFixture`: the same token patterns and credential JSON fields as
|
|
364
|
+
* `scanFixtureForSecrets`, plus any non-null `credential` / `credentials` field (port requests must
|
|
365
|
+
* be recorded through `redactPortPayload`), plus credential query or form parameters inside every
|
|
366
|
+
* JSON string value, `note`, and `failure.message` (a signed URL a port answered, such as an S3
|
|
367
|
+
* presigned URL's `X-Amz-Signature` / `X-Amz-Credential` / `X-Amz-Security-Token`), each `name=`
|
|
368
|
+
* found and judged on its own. The only exemption: the raw value, up to `&`, `#`, whitespace, `"`,
|
|
369
|
+
* `<`, `>`, or the end (never `?` or `'`), percent-decoded, exactly equals that name's entry in
|
|
370
|
+
* `syntheticPortCredentialParams`. Parameters with escaped names (`&`, percent-encoded) are not
|
|
371
|
+
* found. Covers metadata, the request, the response, and the failure. Issues name locations only.
|
|
372
|
+
* Returns an empty array when clean.
|
|
373
|
+
*/
|
|
374
|
+
const scanPortFixtureForSecrets = (fixture) => {
|
|
375
|
+
const issues = [];
|
|
376
|
+
scanText(fixture.id, "id", issues);
|
|
377
|
+
scanText(fixture.port, "port", issues);
|
|
378
|
+
scanText(fixture.method, "method", issues);
|
|
379
|
+
if (fixture.observed !== void 0) scanText(fixture.observed.account, "observed.account", issues);
|
|
380
|
+
if (fixture.note !== void 0) scanPortText(fixture.note, "note", issues);
|
|
381
|
+
scanPortJson(fixture.request, "request", issues);
|
|
382
|
+
if (isPortFailureFixture(fixture)) {
|
|
383
|
+
scanText(fixture.failure.code, "failure.code", issues);
|
|
384
|
+
scanPortText(fixture.failure.message, "failure.message", issues);
|
|
385
|
+
} else scanPortJson(fixture.response, "response", issues);
|
|
386
|
+
return uniqueIssues(issues);
|
|
387
|
+
};
|
|
388
|
+
//#endregion
|
|
389
|
+
export { PortFailure, PortFailureFixture, PortFixture, PortValueFixture, WireBase64BodyResponse, WireBodyResponse, WireChunk, WireExchange, WireFixture, WireFixtureEvidence, WireHeaders, WireRequest, WireResponse, WireStreamResponse, WireTextBodyResponse, conformanceFixtureEvidence, decodePortFixture, decodeWireFixture, fixtureAgeDays, isFixtureStale, isPortCredentialKey, isPortFailureFixture, isPortFixture, isWireBase64BodyResponse, isWireStreamResponse, redactPortPayload, scanFixtureForSecrets, scanPortFixtureForSecrets, syntheticPortCredentialParams };
|
|
390
|
+
|
|
391
|
+
//# sourceMappingURL=fixture.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.mjs","names":[],"sources":["../src/fixture.ts"],"sourcesContent":["/**\n * Fixture data models: recorded HTTP exchanges with outside services (`WireFixture`) and recorded\n * calls through a host-provided port that is not HTTP (`PortFixture`, for example an email client\n * port).\n *\n * Fixtures are plain, serializable data. They must contain synthetic or\n * scrubbed content only: never credentials, cookies, or customer data. Use\n * `scanFixtureForSecrets` / `scanPortFixtureForSecrets` before committing a fixture.\n *\n * @experimental\n */\nimport { Option, Predicate } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport { ConformanceObservation } from './case.ts'\nimport {\n apiKeyPatterns,\n bearerPattern,\n credentialFieldPattern,\n credentialParamPattern,\n decodeBase64Bytes,\n hasLiveCredentialParam,\n isCredentialHeaderName\n} from './wire-internal.ts'\n\nexport { syntheticPortCredentialParams } from './wire-internal.ts'\n\n/** `verified` = recorded from a live service; `unverified` = synthetic placeholder. */\nexport const WireFixtureEvidence = Schema.Literals(['verified', 'unverified'])\n\nexport type WireFixtureEvidence = typeof WireFixtureEvidence.Type\n\n/** Lowercase header name to value. Request headers are allowlisted and never credentials. */\nexport const WireHeaders = Schema.Record(Schema.String, Schema.String)\n\nexport type WireHeaders = typeof WireHeaders.Type\n\nconst HttpStatus = Schema.Int.check(Schema.isBetween({ minimum: 100, maximum: 599 }))\n\nexport const WireRequest = Schema.Struct({\n method: Schema.NonEmptyString,\n /** Absolute URL including any query string. */\n url: Schema.NonEmptyString,\n headers: Schema.optionalKey(WireHeaders),\n /** Parsed JSON request body, or the raw text when it is not JSON (for example a form body). */\n body: Schema.optionalKey(Schema.Json)\n})\n\nexport type WireRequest = typeof WireRequest.Type\n\nconst Base64String = Schema.String.check(Schema.isBase64())\n\n/**\n * One recorded network chunk. A chunk that is valid UTF-8 on its own is stored\n * as readable text (empty chunks as `\"\"`); any other chunk is stored as\n * `{ base64 }` holding its exact bytes. Replay emits exactly these bytes.\n */\nexport const WireChunk = Schema.Union([Schema.String, Schema.Struct({ base64: Base64String })])\n\nexport type WireChunk = typeof WireChunk.Type\n\n// `Never` keys keep the response shapes mutually exclusive when decoding: a\n// response carries exactly one of `body`, `bodyBase64`, or `chunks`.\nconst absent = Schema.optionalKey(Schema.Never)\n\n/** A response whose whole body is valid UTF-8, stored as one string. */\nexport const WireTextBodyResponse = Schema.Struct({\n status: HttpStatus,\n headers: WireHeaders,\n body: Schema.String,\n bodyBase64: absent,\n chunks: absent\n})\n\nexport type WireTextBodyResponse = typeof WireTextBodyResponse.Type\n\n/** A response whose whole body is not valid UTF-8 (for example a PDF), stored as base64 bytes. */\nexport const WireBase64BodyResponse = Schema.Struct({\n status: HttpStatus,\n headers: WireHeaders,\n bodyBase64: Base64String,\n body: absent,\n chunks: absent\n})\n\nexport type WireBase64BodyResponse = typeof WireBase64BodyResponse.Type\n\n/** A response recorded as one whole body: exactly one of `body` or `bodyBase64`. */\nexport const WireBodyResponse = Schema.Union([WireTextBodyResponse, WireBase64BodyResponse])\n\nexport type WireBodyResponse = typeof WireBodyResponse.Type\n\n/**\n * A streamed response (for example `text/event-stream`). Each entry is one\n * network chunk (see `WireChunk`); chunk boundaries and bytes are preserved on\n * replay.\n */\nexport const WireStreamResponse = Schema.Struct({\n status: HttpStatus,\n headers: WireHeaders,\n chunks: Schema.Array(WireChunk),\n body: absent,\n bodyBase64: absent\n})\n\nexport type WireStreamResponse = typeof WireStreamResponse.Type\n\nexport const WireResponse = Schema.Union([\n WireTextBodyResponse,\n WireBase64BodyResponse,\n WireStreamResponse\n])\n\nexport type WireResponse = typeof WireResponse.Type\n\nexport const WireExchange = Schema.Struct({\n request: WireRequest,\n response: WireResponse\n})\n\nexport type WireExchange = typeof WireExchange.Type\n\nconst RecordedAtDate = Schema.String.check(Schema.isPattern(/^\\d{4}-\\d{2}-\\d{2}$/))\n\nexport const WireFixture = Schema.Struct({\n id: Schema.NonEmptyString,\n caseId: Schema.NonEmptyString,\n evidence: WireFixtureEvidence,\n /** Calendar date of the recording (`YYYY-MM-DD`, UTC). */\n recordedAt: RecordedAtDate,\n /** Synthetic account label, for example `synthetic`; never a real account name. */\n account: Schema.NonEmptyString,\n endpoint: Schema.NonEmptyString,\n model: Schema.optionalKey(Schema.String),\n note: Schema.optionalKey(Schema.String),\n exchanges: Schema.NonEmptyArray(WireExchange)\n})\n\nexport type WireFixture = typeof WireFixture.Type\n\n/** Decode unknown input (for example a JSON file) into a `WireFixture`. */\nexport const decodeWireFixture = Schema.decodeUnknownEffect(WireFixture)\n\nexport const isWireStreamResponse = (response: WireResponse): response is WireStreamResponse =>\n Predicate.hasProperty(response, 'chunks')\n\nexport const isWireBase64BodyResponse = (\n response: WireResponse\n): response is WireBase64BodyResponse => Predicate.hasProperty(response, 'bodyBase64')\n\nconst millisPerDay = 86_400_000\n\nconst recordedAtPattern = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\nconst recordedAtMillis = (recordedAt: string): number => {\n const match = recordedAtPattern.exec(recordedAt)\n\n if (match === null) {\n return Number.NaN\n }\n\n return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))\n}\n\n/**\n * Whole days between `recordedAt` (UTC midnight) and `now`. `NaN` when\n * `recordedAt` is not a `YYYY-MM-DD` date.\n */\nexport const fixtureAgeDays = (fixture: Pick<WireFixture, 'recordedAt'>, now: Date): number =>\n Math.floor((now.getTime() - recordedAtMillis(fixture.recordedAt)) / millisPerDay)\n\n/**\n * True when the fixture is older than `maxAgeDays` (default 30). Fixtures with\n * an unreadable `recordedAt` are treated as stale.\n */\nexport const isFixtureStale = (\n fixture: Pick<WireFixture, 'recordedAt'>,\n now: Date,\n maxAgeDays = 30\n): boolean => {\n const age = fixtureAgeDays(fixture, now)\n\n return Number.isNaN(age) || age > maxAgeDays\n}\n\nexport type FixtureSecretIssueKind =\n | 'credential_header'\n | 'bearer_token'\n | 'api_key'\n | 'credential_query_param'\n | 'credential_field'\n\n/** A secret-scan finding. `location` is a path into the fixture; the secret itself is never echoed. */\nexport type FixtureSecretIssue = {\n readonly kind: FixtureSecretIssueKind\n readonly location: string\n}\n\ntype IssueSink = Array<FixtureSecretIssue>\n\nconst scanText = (text: string, location: string, issues: IssueSink): void => {\n if (bearerPattern.test(text)) {\n issues.push({ kind: 'bearer_token', location })\n }\n\n if (apiKeyPatterns.some(pattern => pattern.test(text))) {\n issues.push({ kind: 'api_key', location })\n }\n}\n\nconst scanHeaders = (\n headers: WireHeaders | undefined,\n location: string,\n issues: IssueSink\n): void => {\n for (const [name, value] of Object.entries(headers ?? {})) {\n const headerLocation = `${location}.${name}`\n\n if (isCredentialHeaderName(name)) {\n issues.push({ kind: 'credential_header', location: headerLocation })\n }\n\n scanText(value, headerLocation, issues)\n }\n}\n\nconst scanUrl = (url: string, location: string, issues: IssueSink): void => {\n if (credentialParamPattern.test(url)) {\n issues.push({ kind: 'credential_query_param', location })\n }\n\n scanText(url, location, issues)\n}\n\nconst scanJson = (value: Schema.Json, location: string, issues: IssueSink): void => {\n if (Predicate.isString(value)) {\n scanText(value, location, issues)\n\n return\n }\n\n if (Array.isArray(value)) {\n value.forEach((item, index) => scanJson(item, `${location}[${index}]`, issues))\n\n return\n }\n\n if (value !== null && !Predicate.isNumber(value) && !Predicate.isBoolean(value)) {\n for (const [key, item] of Object.entries(value)) {\n const itemLocation = `${location}.${key}`\n\n if (credentialFieldPattern.test(key) && Predicate.isString(item) && item.length > 0) {\n issues.push({ kind: 'credential_field', location: itemLocation })\n }\n\n scanJson(item, itemLocation, issues)\n }\n }\n}\n\nconst parseJsonOption = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json))\n\n// `data:` payloads of each server-sent event, multi-line data joined with `\\n`.\n// SSE allows CRLF, LF, or bare CR line endings; normalize before splitting events.\nconst sseDataPayloads = (text: string): ReadonlyArray<string> =>\n text\n .replace(/\\r\\n?/g, '\\n')\n .split('\\n\\n')\n .flatMap(event => {\n const data = event\n .split('\\n')\n .filter(line => line.startsWith('data:'))\n .map(line => line.slice('data:'.length).replace(/^ /, ''))\n\n return data.length > 0 ? [data.join('\\n')] : []\n })\n\n/**\n * Scan a whole payload (request/response body or reassembled stream): token\n * patterns, form-encoded credential parameters, and credential fields in the\n * payload itself when it is JSON, or in each SSE `data:` payload that is JSON\n * (located as `<location>.events[n]`).\n */\nconst scanPayload = (text: string, location: string, issues: IssueSink): void => {\n scanText(text, location, issues)\n\n if (credentialParamPattern.test(text)) {\n issues.push({ kind: 'credential_query_param', location })\n }\n\n const json = parseJsonOption(text)\n\n if (Option.isSome(json)) {\n scanJson(json.value, location, issues)\n\n return\n }\n\n sseDataPayloads(text).forEach((payload, index) => {\n const event = parseJsonOption(payload)\n\n if (Option.isSome(event)) {\n scanJson(event.value, `${location}.events[${index}]`, issues)\n }\n })\n}\n\nconst lossyText = (bytes: Uint8Array): string => new TextDecoder().decode(bytes)\n\nconst chunkBytes = (chunk: WireChunk): Uint8Array =>\n Predicate.isString(chunk)\n ? new TextEncoder().encode(chunk)\n : Option.getOrElse(decodeBase64Bytes(chunk.base64), () => new Uint8Array())\n\nconst concatBytes = (parts: ReadonlyArray<Uint8Array>): Uint8Array => {\n const joined = new Uint8Array(parts.reduce((total, part) => total + part.length, 0))\n let offset = 0\n\n for (const part of parts) {\n joined.set(part, offset)\n offset += part.length\n }\n\n return joined\n}\n\nconst scanStream = (\n chunks: ReadonlyArray<WireChunk>,\n location: string,\n issues: IssueSink\n): void => {\n const parts = chunks.map(chunkBytes)\n\n parts.forEach((bytes, index) => scanText(lossyText(bytes), `${location}[${index}]`, issues))\n\n // The reassembled stream catches secrets split across chunks and JSON\n // credential fields inside SSE events. Bytes are decoded non-fatally.\n scanPayload(lossyText(concatBytes(parts)), location, issues)\n}\n\nconst uniqueIssues = (issues: IssueSink): ReadonlyArray<FixtureSecretIssue> => {\n const seen = new Set<string>()\n\n return issues.filter(issue => {\n const key = `${issue.kind} ${issue.location}`\n\n if (seen.has(key)) {\n return false\n }\n\n seen.add(key)\n\n return true\n })\n}\n\n/**\n * Pure secret scan. Flags credential headers, bearer tokens, common API-key\n * prefixes, JWTs, private keys, credential query/form parameters, and\n * credential JSON fields anywhere in the fixture: metadata, URLs, headers,\n * request bodies, response bodies (text or decodable base64), each stream\n * chunk, and the reassembled stream (so a secret split across chunks is still\n * found). JSON bodies and SSE `data:` payloads get the credential-field scan.\n * Issues name locations only. Returns an empty array when clean.\n */\nexport const scanFixtureForSecrets = (fixture: WireFixture): ReadonlyArray<FixtureSecretIssue> => {\n const issues: IssueSink = []\n\n scanText(fixture.id, 'id', issues)\n scanText(fixture.caseId, 'caseId', issues)\n scanText(fixture.account, 'account', issues)\n scanUrl(fixture.endpoint, 'endpoint', issues)\n\n if (fixture.model !== undefined) {\n scanText(fixture.model, 'model', issues)\n }\n\n if (fixture.note !== undefined) {\n scanText(fixture.note, 'note', issues)\n }\n\n fixture.exchanges.forEach((exchange, index) => {\n const base = `exchanges[${index}]`\n const requestBody = exchange.request.body\n\n scanUrl(exchange.request.url, `${base}.request.url`, issues)\n scanHeaders(exchange.request.headers, `${base}.request.headers`, issues)\n\n if (Predicate.isString(requestBody)) {\n scanPayload(requestBody, `${base}.request.body`, issues)\n } else if (requestBody !== undefined) {\n scanJson(requestBody, `${base}.request.body`, issues)\n }\n\n const response = exchange.response\n\n scanHeaders(response.headers, `${base}.response.headers`, issues)\n\n if (isWireStreamResponse(response)) {\n scanStream(response.chunks, `${base}.response.chunks`, issues)\n } else if (isWireBase64BodyResponse(response)) {\n const bytes = decodeBase64Bytes(response.bodyBase64)\n\n if (Option.isSome(bytes)) {\n scanPayload(lossyText(bytes.value), `${base}.response.bodyBase64`, issues)\n }\n } else {\n scanPayload(response.body, `${base}.response.body`, issues)\n }\n })\n\n return uniqueIssues(issues)\n}\n\n// Port fixtures: one recorded call through a host-provided port that is not HTTP.\n\n/**\n * A failure a port answered instead of a value. `expected` is the port's value-level failure (for\n * example a connector `ActionResult.failure`); `error` is the port's typed error channel. `code`\n * is a sanitized classification, never raw provider text.\n */\nexport const PortFailure = Schema.Struct({\n kind: Schema.Literals(['expected', 'error']),\n code: Schema.NonEmptyString,\n message: Schema.String,\n status: Schema.optionalKey(Schema.Int)\n})\n\nexport type PortFailure = typeof PortFailure.Type\n\nconst portFixtureFields = {\n id: Schema.NonEmptyString,\n /** Port name, for example `EmailClient`. */\n port: Schema.NonEmptyString,\n /** Port method, for example `listMessages`. */\n method: Schema.NonEmptyString,\n /** The request as plain JSON, with every credential field removed (`redactPortPayload`). */\n request: Schema.Json,\n /**\n * The live observation this call was recorded from. Absent means a synthetic placeholder\n * (`unverified`); present means `verified` as of `observed.date`.\n */\n observed: Schema.optionalKey(ConformanceObservation),\n note: Schema.optionalKey(Schema.String)\n}\n\n/** A port call answered with a value (`response`, plain JSON). */\nexport const PortValueFixture = Schema.Struct({\n ...portFixtureFields,\n response: Schema.Json,\n failure: absent\n})\n\nexport type PortValueFixture = typeof PortValueFixture.Type\n\n/** A port call answered with a failure. */\nexport const PortFailureFixture = Schema.Struct({\n ...portFixtureFields,\n failure: PortFailure,\n response: absent\n})\n\nexport type PortFailureFixture = typeof PortFailureFixture.Type\n\n/**\n * One recorded call through a host-provided port: the port, the method, the credential-free JSON\n * request, and exactly one of `response` or `failure`. Plain JSON only, so one fixture backs\n * replay, an emulator, and parity checks alike.\n */\nexport const PortFixture = Schema.Union([PortValueFixture, PortFailureFixture])\n\nexport type PortFixture = typeof PortFixture.Type\n\n/** Decode unknown input (for example a JSON file) into a `PortFixture`. */\nexport const decodePortFixture = Schema.decodeUnknownEffect(PortFixture)\n\n/** Any fixture a conformance case may cite: an HTTP `WireFixture` or a `PortFixture`. */\nexport type ConformanceFixture = WireFixture | PortFixture\n\nexport const isPortFixture = (fixture: ConformanceFixture): fixture is PortFixture =>\n !Predicate.hasProperty(fixture, 'exchanges')\n\nexport const isPortFailureFixture = (fixture: PortFixture): fixture is PortFailureFixture =>\n Predicate.hasProperty(fixture, 'failure') && fixture.failure !== undefined\n\n/** Evidence and (when known) date of a fixture. */\nexport type ConformanceFixtureEvidence = {\n readonly evidence: WireFixtureEvidence\n readonly date?: string\n}\n\n/**\n * Evidence and date of any fixture: a `WireFixture`'s `evidence` and `recordedAt`; a\n * `PortFixture` is `verified` as of `observed.date` when observed, else `unverified` without a\n * date.\n */\nexport const conformanceFixtureEvidence = (\n fixture: ConformanceFixture\n): ConformanceFixtureEvidence => {\n if (!isPortFixture(fixture)) {\n return { evidence: fixture.evidence, date: fixture.recordedAt }\n }\n\n return fixture.observed === undefined\n ? { evidence: 'unverified' }\n : { evidence: 'verified', date: fixture.observed.date }\n}\n\nconst portCredentialKeyPattern = /^credentials?$/i\n\nconst isJsonRecord = (value: Schema.Json): value is Schema.JsonObject =>\n value !== null && Predicate.isObject(value) && !Array.isArray(value)\n\n/** True for a port payload key that holds a credential (`credential`, `password`, `token`, ...). */\nexport const isPortCredentialKey = (key: string): boolean =>\n portCredentialKeyPattern.test(key) || credentialFieldPattern.test(key)\n\n/**\n * A copy of a port payload without credential fields (see `isPortCredentialKey`: `credential(s)`\n * plus the credential field names, AWS-style `accessKeyId` / `secretAccessKey` / `sessionToken`\n * and their snake_case forms included), at any depth.\n * Port requests often carry the resolved credential; record and compare requests only after this.\n */\nexport const redactPortPayload = (value: Schema.Json): Schema.Json => {\n if (Array.isArray(value)) {\n return value.map(redactPortPayload)\n }\n\n if (!isJsonRecord(value)) {\n return value\n }\n\n const copy: Record<string, Schema.Json> = {}\n\n for (const [key, item] of Object.entries(value)) {\n if (!isPortCredentialKey(key)) {\n copy[key] = redactPortPayload(item)\n }\n }\n\n return copy\n}\n\n/** A port string: the token patterns plus credential query or form parameters (a signed URL). */\nconst scanPortText = (text: string, location: string, issues: IssueSink): void => {\n scanText(text, location, issues)\n\n if (hasLiveCredentialParam(text)) {\n issues.push({ kind: 'credential_query_param', location })\n }\n}\n\nconst scanPortJson = (value: Schema.Json, location: string, issues: IssueSink): void => {\n scanJson(value, location, issues)\n\n const visit = (item: Schema.Json, itemLocation: string): void => {\n if (Predicate.isString(item)) {\n if (hasLiveCredentialParam(item)) {\n issues.push({ kind: 'credential_query_param', location: itemLocation })\n }\n\n return\n }\n\n if (Array.isArray(item)) {\n item.forEach((entry, index) => visit(entry, `${itemLocation}[${index}]`))\n\n return\n }\n\n if (!isJsonRecord(item)) {\n return\n }\n\n for (const [key, entry] of Object.entries(item)) {\n if (portCredentialKeyPattern.test(key) && entry !== null) {\n issues.push({ kind: 'credential_field', location: `${itemLocation}.${key}` })\n }\n\n visit(entry, `${itemLocation}.${key}`)\n }\n }\n\n visit(value, location)\n}\n\n/**\n * Pure secret scan of a `PortFixture`: the same token patterns and credential JSON fields as\n * `scanFixtureForSecrets`, plus any non-null `credential` / `credentials` field (port requests must\n * be recorded through `redactPortPayload`), plus credential query or form parameters inside every\n * JSON string value, `note`, and `failure.message` (a signed URL a port answered, such as an S3\n * presigned URL's `X-Amz-Signature` / `X-Amz-Credential` / `X-Amz-Security-Token`), each `name=`\n * found and judged on its own. The only exemption: the raw value, up to `&`, `#`, whitespace, `\"`,\n * `<`, `>`, or the end (never `?` or `'`), percent-decoded, exactly equals that name's entry in\n * `syntheticPortCredentialParams`. Parameters with escaped names (`&`, percent-encoded) are not\n * found. Covers metadata, the request, the response, and the failure. Issues name locations only.\n * Returns an empty array when clean.\n */\nexport const scanPortFixtureForSecrets = (\n fixture: PortFixture\n): ReadonlyArray<FixtureSecretIssue> => {\n const issues: IssueSink = []\n\n scanText(fixture.id, 'id', issues)\n scanText(fixture.port, 'port', issues)\n scanText(fixture.method, 'method', issues)\n\n if (fixture.observed !== undefined) {\n scanText(fixture.observed.account, 'observed.account', issues)\n }\n\n if (fixture.note !== undefined) {\n scanPortText(fixture.note, 'note', issues)\n }\n\n scanPortJson(fixture.request, 'request', issues)\n\n if (isPortFailureFixture(fixture)) {\n scanText(fixture.failure.code, 'failure.code', issues)\n scanPortText(fixture.failure.message, 'failure.message', issues)\n } else {\n scanPortJson(fixture.response, 'response', issues)\n }\n\n return uniqueIssues(issues)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { FixtureSecretIssue, WireExchange, WireFixture } from "./fixture.mjs";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
import { HttpClient } from "effect/unstable/http";
|
|
4
|
+
|
|
5
|
+
//#region src/record.d.ts
|
|
6
|
+
/** Default allowlisted request headers. Credential headers are always dropped. */
|
|
7
|
+
declare const defaultRecordedRequestHeaders: ReadonlyArray<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Default allowlisted response headers. Entries ending in `*` are prefixes.
|
|
10
|
+
* Credential headers (including `set-cookie`) are always dropped.
|
|
11
|
+
*/
|
|
12
|
+
declare const defaultRecordedResponseHeaders: ReadonlyArray<string>;
|
|
13
|
+
/** Response media types recorded as `chunks` instead of a single `body`. */
|
|
14
|
+
declare const defaultStreamMediaTypes: ReadonlyArray<string>;
|
|
15
|
+
type WireRecorderOptions = {
|
|
16
|
+
readonly requestHeaders?: ReadonlyArray<string>;
|
|
17
|
+
readonly responseHeaders?: ReadonlyArray<string>;
|
|
18
|
+
readonly streamMediaTypes?: ReadonlyArray<string>;
|
|
19
|
+
};
|
|
20
|
+
declare const WireRecordingIncomplete_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
21
|
+
readonly _tag: "WireRecordingIncomplete";
|
|
22
|
+
} & Readonly<A>;
|
|
23
|
+
/** Raised by `drain` when a recorded request failed or its body was not read to the end. */
|
|
24
|
+
declare class WireRecordingIncomplete extends WireRecordingIncomplete_base<{
|
|
25
|
+
/** `METHOD url` (query string removed) of each incomplete exchange. */readonly requests: ReadonlyArray<string>;
|
|
26
|
+
}> {
|
|
27
|
+
get message(): string;
|
|
28
|
+
}
|
|
29
|
+
type WireRecorderApi = {
|
|
30
|
+
/**
|
|
31
|
+
* Take every recorded exchange, in request order, and clear the recorder.
|
|
32
|
+
* Fails with `WireRecordingIncomplete` (and discards the drained entries)
|
|
33
|
+
* when any request failed or its body was not fully read.
|
|
34
|
+
*
|
|
35
|
+
* A request still pending at drain time is reported in that drain's
|
|
36
|
+
* `WireRecordingIncomplete` and is then dropped: if its body finishes later,
|
|
37
|
+
* the result is ignored and never appears in, or overwrites an entry of, a
|
|
38
|
+
* later drain.
|
|
39
|
+
*/
|
|
40
|
+
readonly drain: Effect.Effect<ReadonlyArray<WireExchange>, WireRecordingIncomplete>;
|
|
41
|
+
};
|
|
42
|
+
declare const WireRecorder_base: Context.ServiceClass<WireRecorder, "@yolk-sdk/conformance/WireRecorder", WireRecorderApi>;
|
|
43
|
+
declare class WireRecorder extends WireRecorder_base {
|
|
44
|
+
/**
|
|
45
|
+
* Wrap the `HttpClient` already in context and provide the recording client
|
|
46
|
+
* plus its `WireRecorder`. Provide the real client below this layer.
|
|
47
|
+
*/
|
|
48
|
+
static layer: (options?: WireRecorderOptions) => Layer.Layer<HttpClient.HttpClient | WireRecorder, never, HttpClient.HttpClient>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Wrap a host-provided `HttpClient` so every exchange is recorded losslessly.
|
|
52
|
+
* Streamed responses (see `streamMediaTypes`) are teed chunk by chunk with
|
|
53
|
+
* network boundaries preserved: each chunk is stored as text when it is valid
|
|
54
|
+
* UTF-8 on its own (empty chunks as `""`), otherwise as `{ base64 }` of its
|
|
55
|
+
* exact bytes. Other responses are recorded as one `body` (valid UTF-8) or
|
|
56
|
+
* `bodyBase64` (anything else). The caller receives the same status, headers,
|
|
57
|
+
* and bytes. Body read failures still reach the caller as `HttpClientError`s
|
|
58
|
+
* (the upstream error is kept as the cause), matching how `FetchHttpClient`
|
|
59
|
+
* reports them.
|
|
60
|
+
*/
|
|
61
|
+
declare const makeRecordingHttpClient: (upstream: HttpClient.HttpClient, options?: WireRecorderOptions) => Effect.Effect<{
|
|
62
|
+
readonly client: HttpClient.HttpClient;
|
|
63
|
+
readonly recorder: WireRecorderApi;
|
|
64
|
+
}>;
|
|
65
|
+
declare const WireFixtureInvalid_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
66
|
+
readonly _tag: "WireFixtureInvalid";
|
|
67
|
+
} & Readonly<A>;
|
|
68
|
+
/** A fixture failed schema validation (for example an empty exchange list or bad date). */
|
|
69
|
+
declare class WireFixtureInvalid extends WireFixtureInvalid_base<{
|
|
70
|
+
readonly fixtureId: string;
|
|
71
|
+
readonly cause: unknown;
|
|
72
|
+
}> {
|
|
73
|
+
get message(): string;
|
|
74
|
+
}
|
|
75
|
+
declare const WireFixtureSecretsFound_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => import("effect/Cause").YieldableError & {
|
|
76
|
+
readonly _tag: "WireFixtureSecretsFound";
|
|
77
|
+
} & Readonly<A>;
|
|
78
|
+
/** The secret scan found credentials or credential-like values in a fixture. */
|
|
79
|
+
declare class WireFixtureSecretsFound extends WireFixtureSecretsFound_base<{
|
|
80
|
+
readonly fixtureId: string;
|
|
81
|
+
readonly issues: ReadonlyArray<FixtureSecretIssue>;
|
|
82
|
+
}> {
|
|
83
|
+
get message(): string;
|
|
84
|
+
}
|
|
85
|
+
type WireFixtureInput = Omit<WireFixture, 'exchanges'> & {
|
|
86
|
+
readonly exchanges: ReadonlyArray<WireExchange>;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Validate a fixture and run `scanFixtureForSecrets`. Fails with
|
|
90
|
+
* `WireFixtureInvalid` or `WireFixtureSecretsFound`; never returns a fixture
|
|
91
|
+
* with scan issues.
|
|
92
|
+
*/
|
|
93
|
+
declare const makeWireFixture: (input: WireFixtureInput) => Effect.Effect<WireFixture, WireFixtureInvalid | WireFixtureSecretsFound>;
|
|
94
|
+
//#endregion
|
|
95
|
+
export { WireFixtureInput, WireFixtureInvalid, WireFixtureSecretsFound, WireRecorder, WireRecorderApi, WireRecorderOptions, WireRecordingIncomplete, defaultRecordedRequestHeaders, defaultRecordedResponseHeaders, defaultStreamMediaTypes, makeRecordingHttpClient, makeWireFixture };
|
|
96
|
+
//# sourceMappingURL=record.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.d.mts","names":[],"sources":["../src/record.ts"],"mappings":";;;;;;cAuCa,6BAAA,EAA+B,aAAa;AAMC;AAU1D;;;AAV0D,cAA7C,8BAAA,EAAgC,aAAa;AAUP;AAAA,cAAtC,uBAAA,EAAyB,aAAa;AAAA,KAEvC,mBAAA;EAAA,SACD,cAAA,GAAiB,aAAA;EAAA,SACjB,eAAA,GAAkB,aAAA;EAAA,SAClB,gBAAA,GAAmB,aAAA;AAAA;AAAA,cAC7B,4BAAA;;;;cAGY,uBAAA,SAAgC,4BAAA;EALhB,gFAOlB,QAAA,EAAU,aAAA;AAAA;EAAA,IAEN,OAAA,CAAA;AAAA;AAAA,KAKH,eAAA;;;;;;;;;;;WAWD,KAAA,EAAO,MAAA,CAAO,MAAA,CAAO,aAAA,CAAc,YAAA,GAAe,uBAAA;AAAA;AAAA,cAC5D,iBAAA;cAEY,YAAA,SAAqB,iBAAA;;;;;SAOzB,KAAA,GACL,OAAA,GAAS,mBAAA,KACR,KAAA,CAAM,KAAA,CAAM,UAAA,CAAW,UAAA,GAAa,YAAA,SAAqB,UAAA,CAAW,UAAA;AAAA;;;;;;;;;AAhCzE;;;cAqJa,uBAAA,GACX,QAAA,EAAU,UAAA,CAAW,UAAA,EACrB,OAAA,GAAS,mBAAA,KACR,MAAA,CAAO,MAAA;EAAA,SAAkB,MAAA,EAAQ,UAAA,CAAW,UAAA;EAAA,SAAqB,QAAA,EAAU,eAAA;AAAA;AAAA,cAkH1E,uBAAA;;;AAjQJ;AAAA,cAoQa,kBAAA,SAA2B,uBAAA;EAAA,SAC7B,SAAA;EAAA,SACA,KAAA;AAAA;EAAA,IAEI,OAAA,CAAA;AAAA;AAAA,cAGd,4BAAA;;;;cAGY,uBAAA,SAAgC,4BAAA;EAAA,SAClC,SAAA;EAAA,SACA,MAAA,EAAQ,aAAA,CAAc,kBAAA;AAAA;EAAA,IAElB,OAAA,CAAA;AAAA;AAAA,KAKH,gBAAA,GAAmB,IAAA,CAAK,WAAA;EAAA,SACzB,SAAA,EAAW,aAAA,CAAc,YAAA;AAAA;;;;;;cAQvB,eAAA,GACX,KAAA,EAAO,gBAAA,KACN,MAAA,CAAO,MAAA,CAAO,WAAA,EAAa,kBAAA,GAAqB,uBAAA"}
|