@glubean/sdk 0.1.39 → 0.2.1

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.
Files changed (78) hide show
  1. package/dist/configure.d.ts +4 -21
  2. package/dist/configure.d.ts.map +1 -1
  3. package/dist/configure.js +19 -145
  4. package/dist/configure.js.map +1 -1
  5. package/dist/contract-core.d.ts +98 -0
  6. package/dist/contract-core.d.ts.map +1 -0
  7. package/dist/contract-core.js +749 -0
  8. package/dist/contract-core.js.map +1 -0
  9. package/dist/contract-http/adapter.d.ts +52 -0
  10. package/dist/contract-http/adapter.d.ts.map +1 -0
  11. package/dist/contract-http/adapter.js +650 -0
  12. package/dist/contract-http/adapter.js.map +1 -0
  13. package/dist/contract-http/factory.d.ts +32 -0
  14. package/dist/contract-http/factory.d.ts.map +1 -0
  15. package/dist/contract-http/factory.js +83 -0
  16. package/dist/contract-http/factory.js.map +1 -0
  17. package/dist/contract-http/flow-helpers.d.ts +12 -0
  18. package/dist/contract-http/flow-helpers.d.ts.map +1 -0
  19. package/dist/contract-http/flow-helpers.js +34 -0
  20. package/dist/contract-http/flow-helpers.js.map +1 -0
  21. package/dist/contract-http/index.d.ts +16 -0
  22. package/dist/contract-http/index.d.ts.map +1 -0
  23. package/dist/contract-http/index.js +15 -0
  24. package/dist/contract-http/index.js.map +1 -0
  25. package/dist/contract-http/markdown.d.ts +10 -0
  26. package/dist/contract-http/markdown.d.ts.map +1 -0
  27. package/dist/contract-http/markdown.js +21 -0
  28. package/dist/contract-http/markdown.js.map +1 -0
  29. package/dist/contract-http/openapi.d.ts +15 -0
  30. package/dist/contract-http/openapi.d.ts.map +1 -0
  31. package/dist/contract-http/openapi.js +38 -0
  32. package/dist/contract-http/openapi.js.map +1 -0
  33. package/dist/contract-http/types.d.ts +252 -0
  34. package/dist/contract-http/types.d.ts.map +1 -0
  35. package/dist/contract-http/types.js +13 -0
  36. package/dist/contract-http/types.js.map +1 -0
  37. package/dist/contract-types.d.ts +420 -467
  38. package/dist/contract-types.d.ts.map +1 -1
  39. package/dist/contract-types.js +16 -4
  40. package/dist/contract-types.js.map +1 -1
  41. package/dist/each-builder.d.ts +244 -0
  42. package/dist/each-builder.d.ts.map +1 -0
  43. package/dist/each-builder.js +268 -0
  44. package/dist/each-builder.js.map +1 -0
  45. package/dist/index.d.ts +30 -513
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +33 -826
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal.d.ts +1 -0
  50. package/dist/internal.d.ts.map +1 -1
  51. package/dist/internal.js +1 -0
  52. package/dist/internal.js.map +1 -1
  53. package/dist/runtime-carrier.d.ts +142 -0
  54. package/dist/runtime-carrier.d.ts.map +1 -0
  55. package/dist/runtime-carrier.js +148 -0
  56. package/dist/runtime-carrier.js.map +1 -0
  57. package/dist/session.d.ts.map +1 -1
  58. package/dist/session.js +2 -1
  59. package/dist/session.js.map +1 -1
  60. package/dist/test-builder.d.ts +249 -0
  61. package/dist/test-builder.d.ts.map +1 -0
  62. package/dist/test-builder.js +265 -0
  63. package/dist/test-builder.js.map +1 -0
  64. package/dist/test-extend.d.ts +59 -0
  65. package/dist/test-extend.d.ts.map +1 -0
  66. package/dist/test-extend.js +111 -0
  67. package/dist/test-extend.js.map +1 -0
  68. package/dist/test-utils.d.ts +39 -0
  69. package/dist/test-utils.d.ts.map +1 -0
  70. package/dist/test-utils.js +91 -0
  71. package/dist/test-utils.js.map +1 -0
  72. package/dist/types.d.ts +41 -122
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +1 -1
  75. package/dist/contract.d.ts +0 -64
  76. package/dist/contract.d.ts.map +0 -1
  77. package/dist/contract.js +0 -793
  78. package/dist/contract.js.map +0 -1
@@ -0,0 +1,650 @@
1
+ /**
2
+ * HTTP adapter — built-in implementation of ContractProtocolAdapter for HTTP.
3
+ *
4
+ * Shipped inside @glubean/sdk (zero-config UX for the common case) but uses
5
+ * the same adapter interface as future plugin protocols (gRPC / GraphQL /
6
+ * Kafka). Registered at SDK load time from `../index.ts`.
7
+ *
8
+ * Responsibilities:
9
+ * - execute: run a case's request + expect + verify lifecycle
10
+ * - project: produce Runtime ContractProjection<HttpPayloadSchemas>
11
+ * - normalize: convert Runtime → ExtractedContractProjection<HttpSafeSchemas>
12
+ * - executeCaseInFlow: deep-merge lens resolvedInputs over case spec,
13
+ * run case with Rule 1 try/finally teardown
14
+ * - classifyFailure: map HTTP status → FailureKind
15
+ * - describePayload: summary for index views
16
+ * - toOpenApi / toMarkdown: delegate to ./openapi.ts / ./markdown.ts
17
+ *
18
+ * .case() fail-fast: rejects cases with function-valued input fields
19
+ * (body/params/query/headers) because function fields reference case-local
20
+ * setup state which is not available in flow mode.
21
+ */
22
+ import { mergeSlot } from "./flow-helpers.js";
23
+ import { buildOpenApiForHttp } from "./openapi.js";
24
+ import { renderMarkdownForHttp } from "./markdown.js";
25
+ // =============================================================================
26
+ // Helpers — endpoint, params, request body, response headers
27
+ // =============================================================================
28
+ export function parseEndpoint(endpoint) {
29
+ const spaceIdx = endpoint.indexOf(" ");
30
+ if (spaceIdx === -1)
31
+ return { method: "GET", path: endpoint };
32
+ return {
33
+ method: endpoint.slice(0, spaceIdx).toUpperCase(),
34
+ path: endpoint.slice(spaceIdx + 1),
35
+ };
36
+ }
37
+ function extractParamValue(v) {
38
+ if (typeof v === "string")
39
+ return v;
40
+ if (v && typeof v === "object" && "value" in v) {
41
+ return String(v.value);
42
+ }
43
+ return String(v);
44
+ }
45
+ export function flattenParamValues(params) {
46
+ if (!params)
47
+ return undefined;
48
+ const result = {};
49
+ for (const [key, value] of Object.entries(params)) {
50
+ result[key] = extractParamValue(value);
51
+ }
52
+ return result;
53
+ }
54
+ function resolveParams(path, params) {
55
+ if (!params)
56
+ return path;
57
+ let resolved = path;
58
+ for (const [key, value] of Object.entries(params)) {
59
+ resolved = resolved.replace(`:${key}`, encodeURIComponent(value));
60
+ }
61
+ return resolved;
62
+ }
63
+ const STRUCTURED_REQUEST_FIELDS = [
64
+ "body",
65
+ "contentType",
66
+ "headers",
67
+ "example",
68
+ "examples",
69
+ ];
70
+ export function normalizeRequest(req) {
71
+ if (!req || typeof req !== "object")
72
+ return undefined;
73
+ const hasStructuredField = STRUCTURED_REQUEST_FIELDS.some((f) => f in req);
74
+ if (hasStructuredField)
75
+ return req;
76
+ return { body: req };
77
+ }
78
+ function buildRequestBodyOptions(body, contentType) {
79
+ if (body === undefined)
80
+ return {};
81
+ const ct = (contentType ?? "application/json").toLowerCase();
82
+ if (ct.startsWith("application/json")) {
83
+ return { json: body };
84
+ }
85
+ if (ct.startsWith("multipart/form-data")) {
86
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
87
+ return { body };
88
+ }
89
+ if (body && typeof body === "object") {
90
+ const fd = new FormData();
91
+ for (const [k, v] of Object.entries(body)) {
92
+ if (v instanceof Blob || v instanceof File)
93
+ fd.append(k, v);
94
+ else
95
+ fd.append(k, String(v));
96
+ }
97
+ return { body: fd };
98
+ }
99
+ return { body };
100
+ }
101
+ if (ct.startsWith("application/x-www-form-urlencoded")) {
102
+ if (body instanceof URLSearchParams)
103
+ return { body };
104
+ if (body && typeof body === "object") {
105
+ const params = new URLSearchParams();
106
+ for (const [k, v] of Object.entries(body)) {
107
+ params.append(k, String(v));
108
+ }
109
+ return { body: params };
110
+ }
111
+ return { body };
112
+ }
113
+ return { body };
114
+ }
115
+ function normalizeResponseHeaders(headers) {
116
+ const result = {};
117
+ if (!headers)
118
+ return result;
119
+ if (typeof headers.forEach === "function" &&
120
+ typeof headers.get === "function") {
121
+ headers.forEach((value, key) => {
122
+ const lowerKey = key.toLowerCase();
123
+ if (lowerKey === "set-cookie") {
124
+ const existing = result[lowerKey];
125
+ const newValue = typeof existing === "string"
126
+ ? [existing, value]
127
+ : [...(existing ?? []), value];
128
+ result[lowerKey] = newValue;
129
+ }
130
+ else {
131
+ result[lowerKey] = value;
132
+ }
133
+ });
134
+ return result;
135
+ }
136
+ if (typeof headers === "object") {
137
+ for (const [key, value] of Object.entries(headers)) {
138
+ const lowerKey = key.toLowerCase();
139
+ if (Array.isArray(value))
140
+ result[lowerKey] = value.map(String);
141
+ else if (value != null)
142
+ result[lowerKey] = String(value);
143
+ }
144
+ }
145
+ return result;
146
+ }
147
+ // =============================================================================
148
+ // Schema → JSON Schema (normalize helper)
149
+ // =============================================================================
150
+ /**
151
+ * Try to convert a SchemaLike (Zod v4, Valibot, etc.) to JSON Schema.
152
+ * Uses the schema's own `toJSONSchema` method if present. Falls back to
153
+ * passing through as-is if already plain or unrecognized.
154
+ */
155
+ export function schemaToJsonSchema(schema) {
156
+ if (schema == null)
157
+ return null;
158
+ if (typeof schema !== "object")
159
+ return schema;
160
+ // Already plain JSON Schema-ish
161
+ if ("type" in schema || "$ref" in schema) {
162
+ return schema;
163
+ }
164
+ // Zod v4 instance method
165
+ const toJSONSchema = schema.toJSONSchema;
166
+ if (typeof toJSONSchema === "function") {
167
+ try {
168
+ return toJSONSchema.call(schema);
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ // =============================================================================
177
+ // Extract per-param metadata from case spec (for OpenAPI)
178
+ // =============================================================================
179
+ function extractParamMetaSchemas(params) {
180
+ if (!params || typeof params === "function")
181
+ return undefined;
182
+ const result = {};
183
+ let hasAny = false;
184
+ for (const [key, val] of Object.entries(params)) {
185
+ if (val && typeof val === "object" && !Array.isArray(val) && "value" in val) {
186
+ const pv = val;
187
+ if (pv.schema !== undefined ||
188
+ pv.description !== undefined ||
189
+ pv.required !== undefined ||
190
+ pv.deprecated !== undefined) {
191
+ result[key] = {
192
+ schema: pv.schema,
193
+ description: pv.description,
194
+ required: pv.required,
195
+ deprecated: pv.deprecated,
196
+ };
197
+ hasAny = true;
198
+ }
199
+ }
200
+ }
201
+ return hasAny ? result : undefined;
202
+ }
203
+ // =============================================================================
204
+ // Case execution (standalone mode)
205
+ // =============================================================================
206
+ /**
207
+ * Run a single case. Called by the dispatcher in contract-core.ts (via
208
+ * adapter.execute). Full lifecycle: setup → request → assert → verify →
209
+ * teardown (finally).
210
+ */
211
+ async function executeCase(ctx, caseSpec, spec) {
212
+ const { method, path } = parseEndpoint(spec.endpoint);
213
+ // Setup
214
+ const state = caseSpec.setup
215
+ ? await caseSpec.setup(ctx)
216
+ : undefined;
217
+ try {
218
+ const client = (caseSpec.client ?? spec.client);
219
+ if (!client) {
220
+ throw new Error(`No HTTP client provided for case. Set "client" on the case or contract spec.`);
221
+ }
222
+ // Resolve params/query/body/headers
223
+ const rawParams = typeof caseSpec.params === "function"
224
+ ? caseSpec.params(state)
225
+ : caseSpec.params;
226
+ const params = flattenParamValues(rawParams);
227
+ const resolvedPath = resolveParams(path, params);
228
+ const requestOptions = {};
229
+ const body = typeof caseSpec.body === "function"
230
+ ? caseSpec.body(state)
231
+ : caseSpec.body;
232
+ const normalizedReq = normalizeRequest(spec.request);
233
+ const effectiveContentType = caseSpec.contentType ?? normalizedReq?.contentType ?? "application/json";
234
+ if (body !== undefined) {
235
+ Object.assign(requestOptions, buildRequestBodyOptions(body, effectiveContentType));
236
+ }
237
+ const headers = typeof caseSpec.headers === "function"
238
+ ? caseSpec.headers(state)
239
+ : caseSpec.headers;
240
+ if (headers)
241
+ requestOptions.headers = headers;
242
+ if (caseSpec.query) {
243
+ const rawQuery = typeof caseSpec.query === "function"
244
+ ? caseSpec.query(state)
245
+ : caseSpec.query;
246
+ requestOptions.searchParams = flattenParamValues(rawQuery);
247
+ }
248
+ requestOptions.throwHttpErrors = false;
249
+ const methodLower = method.toLowerCase();
250
+ let res;
251
+ try {
252
+ res = await client[methodLower](resolvedPath, requestOptions);
253
+ }
254
+ catch (err) {
255
+ if (err instanceof Error && err.name === "TimeoutError") {
256
+ const timeoutMs = client._configuredTimeout ?? 10000;
257
+ throw new Error(`${err.message} (timeout: ${timeoutMs}ms)`);
258
+ }
259
+ throw err;
260
+ }
261
+ // Assertions
262
+ ctx.expect(res).toHaveStatus(caseSpec.expect.status);
263
+ if (caseSpec.expect.headers) {
264
+ const normalizedHeaders = normalizeResponseHeaders(res.headers);
265
+ ctx.validate(normalizedHeaders, caseSpec.expect.headers, `response headers`);
266
+ }
267
+ let parsed;
268
+ if (caseSpec.expect.schema) {
269
+ const jsonBody = await res.json();
270
+ const validated = ctx.validate(jsonBody, caseSpec.expect.schema, `response body`);
271
+ parsed = (validated !== undefined ? validated : jsonBody);
272
+ }
273
+ else if (caseSpec.verify) {
274
+ parsed = (await res.json());
275
+ }
276
+ else {
277
+ parsed = undefined;
278
+ }
279
+ if (caseSpec.verify) {
280
+ await caseSpec.verify(ctx, parsed);
281
+ }
282
+ }
283
+ finally {
284
+ if (caseSpec.teardown) {
285
+ await caseSpec.teardown(ctx, state);
286
+ }
287
+ }
288
+ }
289
+ // =============================================================================
290
+ // project(): HttpContractSpec → ContractProjection<HttpPayloadSchemas>
291
+ // =============================================================================
292
+ function projectHttp(spec) {
293
+ const { method, path } = parseEndpoint(spec.endpoint);
294
+ const normalizedReq = normalizeRequest(spec.request);
295
+ return {
296
+ protocol: "http",
297
+ target: spec.endpoint,
298
+ description: spec.description,
299
+ feature: spec.feature,
300
+ tags: spec.tags,
301
+ extensions: spec.extensions,
302
+ deprecated: spec.deprecated,
303
+ meta: { method, path },
304
+ schemas: {
305
+ request: normalizedReq
306
+ ? {
307
+ body: normalizedReq.body,
308
+ contentType: normalizedReq.contentType,
309
+ headers: normalizedReq.headers,
310
+ example: normalizedReq.example,
311
+ examples: normalizedReq.examples,
312
+ }
313
+ : undefined,
314
+ },
315
+ cases: Object.entries(spec.cases).map(([key, c]) => {
316
+ const effectiveDeprecated = c.deprecated ?? spec.deprecated;
317
+ const lifecycle = effectiveDeprecated
318
+ ? "deprecated"
319
+ : c.deferred
320
+ ? "deferred"
321
+ : "active";
322
+ const paramSchemas = extractParamMetaSchemas(c.params);
323
+ const querySchemas = extractParamMetaSchemas(c.query);
324
+ return {
325
+ key,
326
+ description: c.description,
327
+ lifecycle,
328
+ severity: c.severity ?? "warning",
329
+ deferredReason: c.deferred,
330
+ deprecatedReason: effectiveDeprecated,
331
+ requires: c.requires,
332
+ defaultRun: c.defaultRun,
333
+ tags: c.tags,
334
+ extensions: c.extensions,
335
+ schemas: {
336
+ request: undefined,
337
+ response: {
338
+ status: c.expect.status,
339
+ body: c.expect.schema,
340
+ contentType: c.expect.contentType,
341
+ headers: c.expect.headers,
342
+ example: c.expect.example,
343
+ examples: c.expect.examples,
344
+ },
345
+ params: paramSchemas,
346
+ query: querySchemas,
347
+ },
348
+ };
349
+ }),
350
+ };
351
+ }
352
+ // =============================================================================
353
+ // normalize(): Runtime → Safe (Zod → JSON Schema)
354
+ // =============================================================================
355
+ function normalizeHttp(projection) {
356
+ const safeContractSchemas = projection.schemas
357
+ ? {
358
+ request: projection.schemas.request
359
+ ? {
360
+ body: schemaToJsonSchema(projection.schemas.request.body) ?? undefined,
361
+ contentType: projection.schemas.request.contentType,
362
+ headers: schemaToJsonSchema(projection.schemas.request.headers) ?? undefined,
363
+ example: projection.schemas.request.example,
364
+ examples: projection.schemas.request.examples,
365
+ }
366
+ : undefined,
367
+ // Contract-level security (set by the scoped factory via
368
+ // `contract.http.with("name", { security })`). Must survive
369
+ // normalize so downstream tools (toOpenApi, Cloud views) see it.
370
+ security: projection.schemas.security,
371
+ }
372
+ : undefined;
373
+ return {
374
+ id: projection.id,
375
+ protocol: projection.protocol,
376
+ target: projection.target,
377
+ description: projection.description,
378
+ feature: projection.feature,
379
+ instanceName: projection.instanceName,
380
+ tags: projection.tags,
381
+ extensions: projection.extensions,
382
+ deprecated: projection.deprecated,
383
+ schemas: safeContractSchemas,
384
+ meta: projection.meta,
385
+ cases: projection.cases.map((c) => ({
386
+ key: c.key,
387
+ description: c.description,
388
+ lifecycle: c.lifecycle,
389
+ severity: c.severity,
390
+ deferredReason: c.deferredReason,
391
+ deprecatedReason: c.deprecatedReason,
392
+ requires: c.requires,
393
+ defaultRun: c.defaultRun,
394
+ tags: c.tags,
395
+ extensions: c.extensions,
396
+ meta: c.meta,
397
+ schemas: c.schemas
398
+ ? {
399
+ response: c.schemas.response
400
+ ? {
401
+ status: c.schemas.response.status,
402
+ body: schemaToJsonSchema(c.schemas.response.body) ?? undefined,
403
+ contentType: c.schemas.response.contentType,
404
+ headers: schemaToJsonSchema(c.schemas.response.headers) ?? undefined,
405
+ example: c.schemas.response.example,
406
+ examples: c.schemas.response.examples,
407
+ }
408
+ : undefined,
409
+ params: c.schemas.params
410
+ ? normalizeParamMetaRecord(c.schemas.params)
411
+ : undefined,
412
+ query: c.schemas.query
413
+ ? normalizeParamMetaRecord(c.schemas.query)
414
+ : undefined,
415
+ security: c.schemas.security,
416
+ }
417
+ : undefined,
418
+ })),
419
+ };
420
+ }
421
+ function normalizeParamMetaRecord(raw) {
422
+ const out = {};
423
+ for (const [key, meta] of Object.entries(raw)) {
424
+ out[key] = {
425
+ schema: schemaToJsonSchema(meta.schema) ?? undefined,
426
+ description: meta.description,
427
+ required: meta.required,
428
+ deprecated: meta.deprecated,
429
+ };
430
+ }
431
+ return out;
432
+ }
433
+ // =============================================================================
434
+ // executeCaseInFlow(): deep-merge lens inputs + Rule 1 teardown
435
+ // =============================================================================
436
+ async function executeCaseInFlowHttp(input) {
437
+ const { ctx, contract, caseKey, resolvedInputs } = input;
438
+ const spec = contract._spec;
439
+ const caseSpec = spec.cases[caseKey];
440
+ if (!caseSpec) {
441
+ throw new Error(`case "${caseKey}" not in contract "${contract._projection.id}"`);
442
+ }
443
+ // Note: function-valued fields are rejected at .case() time (§5.1.1).
444
+ // In flow mode we assume all input slots are static (possibly undefined).
445
+ // Compute effective body/params/query/headers via deep-merge
446
+ const patch = (resolvedInputs ?? {});
447
+ const effectiveBody = mergeSlot(caseSpec.body, patch.body);
448
+ const effectiveParams = mergeSlot(caseSpec.params, patch.params);
449
+ const effectiveQuery = mergeSlot(caseSpec.query, patch.query);
450
+ const effectiveHeaders = mergeSlot(caseSpec.headers, patch.headers);
451
+ // Rule 1: case setup throw → teardown does NOT run. Track whether setup
452
+ // SUCCEEDED via a separate flag, not via state-is-undefined — a setup may
453
+ // legitimately return `undefined` and we still owe it a teardown call.
454
+ let setupRan = false;
455
+ let caseState = undefined;
456
+ if (caseSpec.setup) {
457
+ caseState = await caseSpec.setup(ctx);
458
+ setupRan = true;
459
+ }
460
+ try {
461
+ const { method, path } = parseEndpoint(spec.endpoint);
462
+ const client = (caseSpec.client ?? spec.client);
463
+ if (!client) {
464
+ throw new Error(`No HTTP client provided for case "${caseKey}" in contract "${contract._projection.id}". ` +
465
+ `Set "client" on the case or contract spec.`);
466
+ }
467
+ const resolvedPath = resolveParams(path, flattenParamValues(effectiveParams));
468
+ const normalizedReq = normalizeRequest(spec.request);
469
+ const effectiveContentType = caseSpec.contentType ?? normalizedReq?.contentType ?? "application/json";
470
+ const requestOptions = { throwHttpErrors: false };
471
+ if (effectiveBody !== undefined) {
472
+ Object.assign(requestOptions, buildRequestBodyOptions(effectiveBody, effectiveContentType));
473
+ }
474
+ if (effectiveHeaders)
475
+ requestOptions.headers = effectiveHeaders;
476
+ if (effectiveQuery) {
477
+ requestOptions.searchParams = flattenParamValues(effectiveQuery);
478
+ }
479
+ const methodLower = method.toLowerCase();
480
+ let res;
481
+ try {
482
+ res = await client[methodLower](resolvedPath, requestOptions);
483
+ }
484
+ catch (err) {
485
+ if (err instanceof Error && err.name === "TimeoutError") {
486
+ const timeoutMs = client._configuredTimeout ?? 10000;
487
+ throw new Error(`${err.message} (timeout: ${timeoutMs}ms)`);
488
+ }
489
+ throw err;
490
+ }
491
+ ctx.expect(res).toHaveStatus(caseSpec.expect.status);
492
+ const responseHeaders = normalizeResponseHeaders(res.headers);
493
+ if (caseSpec.expect.headers) {
494
+ ctx.validate(responseHeaders, caseSpec.expect.headers, `response headers`);
495
+ }
496
+ let body;
497
+ if (caseSpec.expect.schema) {
498
+ const jsonBody = await res.json();
499
+ const validated = ctx.validate(jsonBody, caseSpec.expect.schema, `response body`);
500
+ body = validated !== undefined ? validated : jsonBody;
501
+ }
502
+ else if (caseSpec.verify) {
503
+ body = await res.json();
504
+ }
505
+ else {
506
+ // Try to pull body anyway for downstream lens
507
+ try {
508
+ body = await res.json();
509
+ }
510
+ catch {
511
+ body = undefined;
512
+ }
513
+ }
514
+ if (caseSpec.verify) {
515
+ await caseSpec.verify(ctx, body);
516
+ }
517
+ return { status: res.status, headers: responseHeaders, body };
518
+ }
519
+ finally {
520
+ // Rule 1: case.teardown runs in finally whenever setup succeeded,
521
+ // regardless of downstream outcome AND regardless of the state value
522
+ // returned by setup (undefined is a valid return). Teardown errors are
523
+ // logged but MUST NOT mask the primary exception.
524
+ if (caseSpec.teardown && setupRan) {
525
+ try {
526
+ await caseSpec.teardown(ctx, caseState);
527
+ }
528
+ catch (tdErr) {
529
+ ctx.log?.(`case.teardown("${caseKey}") failed: ${String(tdErr)}`);
530
+ }
531
+ }
532
+ }
533
+ }
534
+ // =============================================================================
535
+ // classifyFailure(): HTTP status → FailureKind
536
+ // =============================================================================
537
+ function classifyHttpFailure(input) {
538
+ // Scan recent HTTP trace events for the response status
539
+ for (let i = input.events.length - 1; i >= 0; i--) {
540
+ const ev = input.events[i];
541
+ if (ev.type === "http:response" || ev.type === "http:trace") {
542
+ const status = ev.data.status;
543
+ if (typeof status === "number") {
544
+ return statusToClassification(status);
545
+ }
546
+ }
547
+ }
548
+ // Fallback: inspect error
549
+ if (input.error instanceof Error) {
550
+ if (input.error.name === "TimeoutError") {
551
+ return { kind: "timeout", source: "trace", retryable: true };
552
+ }
553
+ if (/ECONNREFUSED|ENOTFOUND|fetch failed/.test(input.error.message)) {
554
+ return { kind: "transport", source: "trace", retryable: true };
555
+ }
556
+ }
557
+ return undefined;
558
+ }
559
+ function statusToClassification(status) {
560
+ if (status === 401)
561
+ return { kind: "auth", source: "trace" };
562
+ if (status === 403)
563
+ return { kind: "permission", source: "trace" };
564
+ if (status === 404)
565
+ return { kind: "not-found", source: "trace" };
566
+ if (status === 429)
567
+ return { kind: "rate-limit", source: "trace", retryable: true };
568
+ if (status >= 500)
569
+ return { kind: "transport", source: "trace", retryable: true };
570
+ return { kind: "business-rule", source: "trace" };
571
+ }
572
+ // =============================================================================
573
+ // describePayload(): payload overview from HttpSafeSchemas
574
+ // =============================================================================
575
+ function describeHttpPayload(schemas) {
576
+ if (!schemas)
577
+ return undefined;
578
+ return {
579
+ hasRequest: !!schemas.request?.body,
580
+ hasResponse: !!schemas.response?.body,
581
+ responseStatus: schemas.response?.status,
582
+ responseContentType: schemas.response?.contentType,
583
+ requestContentType: schemas.request?.contentType,
584
+ };
585
+ }
586
+ // =============================================================================
587
+ // The HTTP adapter
588
+ // =============================================================================
589
+ export const httpAdapter = {
590
+ async execute(ctx, caseSpec, spec) {
591
+ await executeCase(ctx, caseSpec, spec);
592
+ },
593
+ project(spec) {
594
+ return projectHttp(spec);
595
+ },
596
+ normalize(projection) {
597
+ return normalizeHttp(projection);
598
+ },
599
+ executeCaseInFlow(input) {
600
+ return executeCaseInFlowHttp(input);
601
+ },
602
+ classifyFailure(input) {
603
+ return classifyHttpFailure(input);
604
+ },
605
+ describePayload(schemas) {
606
+ return describeHttpPayload(schemas);
607
+ },
608
+ toOpenApi(projection) {
609
+ return buildOpenApiForHttp(projection);
610
+ },
611
+ toMarkdown(projection) {
612
+ return renderMarkdownForHttp(projection);
613
+ },
614
+ renderTarget(target) {
615
+ return target; // HTTP "POST /users" is already human-readable
616
+ },
617
+ validateCaseForFlow(spec, caseKey, contractId) {
618
+ const caseSpec = spec.cases[caseKey];
619
+ if (!caseSpec) {
620
+ throw new Error(`Case "${caseKey}" not in contract "${contractId}"`);
621
+ }
622
+ validateHttpCaseForFlow(contractId, caseKey, caseSpec);
623
+ },
624
+ };
625
+ // =============================================================================
626
+ // .case(key) fail-fast for function-valued input fields
627
+ // =============================================================================
628
+ /**
629
+ * Validate that an HTTP case can be referenced from a flow. Called from
630
+ * ProtocolContract.case() — throws if the case has function-valued inputs.
631
+ * See contract-flow v9 §5.1.1.
632
+ */
633
+ export function validateHttpCaseForFlow(contractId, caseKey, caseSpec) {
634
+ const functionFields = [];
635
+ if (typeof caseSpec.body === "function")
636
+ functionFields.push("body");
637
+ if (typeof caseSpec.params === "function")
638
+ functionFields.push("params");
639
+ if (typeof caseSpec.query === "function")
640
+ functionFields.push("query");
641
+ if (typeof caseSpec.headers === "function")
642
+ functionFields.push("headers");
643
+ if (functionFields.length > 0) {
644
+ throw new Error(`Contract "${contractId}" case "${caseKey}" has function-valued field(s): ` +
645
+ `${functionFields.join(", ")}. Function fields reference case-local setup state, ` +
646
+ `which is not available in flow mode. ` +
647
+ `Fix: split into a new case with static values, or convert the field to a static value.`);
648
+ }
649
+ }
650
+ //# sourceMappingURL=adapter.js.map