@contractkit/plugin-typescript 0.29.0 → 0.31.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/dist/index.js CHANGED
@@ -1105,7 +1105,7 @@ function pascalToDotCase(name) {
1105
1105
  __name(pascalToDotCase, "pascalToDotCase");
1106
1106
 
1107
1107
  // src/codegen-operation.ts
1108
- import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType } from "@contractkit/core";
1108
+ import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType, emittedResponses } from "@contractkit/core";
1109
1109
  import { basename, dirname as dirname2, relative as relative2 } from "path";
1110
1110
  function bodyParserToken(contentType) {
1111
1111
  switch (classifyContentType(contentType)) {
@@ -1182,17 +1182,59 @@ function generateOp(root, options = {}) {
1182
1182
  const types = collectTypes(root, options.modelsWithInput, options.modelsWithOutput);
1183
1183
  const services = collectServices(root);
1184
1184
  const routerName = deriveRouterName(root.file);
1185
- const needsParseAndValidate = routeNeedsValidation(root);
1185
+ const lines = [];
1186
+ lines.push("");
1187
+ lines.push("/**");
1188
+ const relFile = options.outPath ? relative2(dirname2(options.outPath), root.file) : root.file;
1189
+ lines.push(` * generated from [${basename(root.file)}](file://./${relFile})`);
1190
+ lines.push("*/");
1191
+ lines.push(`export const ${routerName} = ServerKitRouter();`);
1192
+ lines.push("");
1193
+ const includeInternal = options.includeInternal ?? true;
1194
+ for (const route of root.routes) {
1195
+ for (const op of route.operations) {
1196
+ if (!includeInternal && resolveModifiers(route, op).includes("internal")) continue;
1197
+ lines.push(...generateHandler(route, op, root, options));
1198
+ lines.push("");
1199
+ }
1200
+ }
1201
+ const handlerBody = lines.join("\n");
1202
+ const references = /* @__PURE__ */ __name((symbol) => new RegExp(`\\b${symbol}\\b`).test(handlerBody), "references");
1203
+ const helpers = [];
1204
+ if (references("_ZodBinary")) {
1205
+ helpers.push(`const _ZodBinary = z.custom<Buffer>((val) => Buffer.isBuffer(val), { error: 'Must be binary data' });`);
1206
+ }
1207
+ if (references("_ZodDatetime")) {
1208
+ helpers.push(`const _ZodDatetime = z.preprocess((val) => typeof val === 'string' ? DateTime.fromISO(val) : val, z.custom<DateTime>((val) => val instanceof DateTime && val.isValid, { message: 'Must be in ISO 8601 format' }));`);
1209
+ }
1210
+ if (references("_ZodInterval")) {
1211
+ helpers.push(`const _ZodInterval = z.preprocess((val) => typeof val === 'string' ? Interval.fromISO(val) : val, z.custom<Interval>((val) => val instanceof Interval && val.isValid, { message: 'Must be an ISO 8601 interval' })).transform(val => val.toISO()!);`);
1212
+ }
1213
+ const needsZodJson = references("_ZodJson");
1214
+ if (needsZodJson || references("_JsonValue")) {
1215
+ helpers.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
1216
+ }
1217
+ if (needsZodJson) {
1218
+ helpers.push(`const _ZodJson: z.ZodType<_JsonValue> = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(_ZodJson), z.record(z.string(), _ZodJson)]));`);
1219
+ }
1220
+ const generated = [
1221
+ ...helpers.length ? [
1222
+ "",
1223
+ ...helpers
1224
+ ] : [],
1225
+ ...lines
1226
+ ].join("\n");
1227
+ const uses = /* @__PURE__ */ __name((symbol) => new RegExp(`\\b${symbol}\\b`).test(generated), "uses");
1186
1228
  const body = [];
1187
- const needsSignature = fileNeedsSignature(root);
1188
- const needsPolicy = fileNeedsPolicy(root);
1189
1229
  const koaImports = [
1190
1230
  "ServerKitRouter",
1191
- "bodyParserMiddleware"
1192
- ];
1193
- if (needsPolicy) koaImports.push("requirePolicy");
1194
- if (needsSignature) koaImports.push("requireSignature");
1195
- body.push(`import { ${koaImports.join(", ")} } from '@maroonedsoftware/koa';`);
1231
+ "bodyParserMiddleware",
1232
+ "requirePolicy",
1233
+ "requireSignature"
1234
+ ].filter(uses);
1235
+ if (koaImports.length > 0) {
1236
+ body.push(`import { ${koaImports.join(", ")} } from '@maroonedsoftware/koa';`);
1237
+ }
1196
1238
  for (const svc of services) {
1197
1239
  const modulePath = root.services?.[svc] ?? root.meta[svc] ?? deriveModulePath(svc, options.servicePathTemplate);
1198
1240
  body.push(`import { ${svc} } from '${modulePath}';`);
@@ -1200,49 +1242,20 @@ function generateOp(root, options = {}) {
1200
1242
  if (types.length > 0) {
1201
1243
  body.push(...generateTypeImports(types, root.file, options));
1202
1244
  }
1203
- const luxonImports = [];
1204
- if (opNeedsDateTime(root)) luxonImports.push("DateTime");
1205
- if (opNeedsScalar(root, "duration")) luxonImports.push("Duration");
1206
- if (opNeedsScalar(root, "interval")) luxonImports.push("Interval");
1245
+ const luxonImports = [
1246
+ "DateTime",
1247
+ "Duration",
1248
+ "Interval"
1249
+ ].filter(uses);
1207
1250
  if (luxonImports.length > 0) {
1208
1251
  body.push(`import { ${luxonImports.join(", ")} } from 'luxon';`);
1209
1252
  }
1210
- if (needsParseAndValidate) {
1253
+ if (uses("parseAndValidate")) {
1211
1254
  body.push(`import { parseAndValidate } from '@maroonedsoftware/zod';`);
1212
1255
  }
1213
- if (fileUsesMultipart(root)) {
1256
+ if (uses("MultipartBody")) {
1214
1257
  body.push(`import { MultipartBody } from '@maroonedsoftware/multipart';`);
1215
1258
  }
1216
- const helpers = [];
1217
- if (opNeedsScalar(root, "binary")) {
1218
- helpers.push(`const _ZodBinary = z.custom<Buffer>((val) => Buffer.isBuffer(val), { error: 'Must be binary data' });`);
1219
- }
1220
- if (opNeedsScalar(root, "datetime")) {
1221
- helpers.push(`const _ZodDatetime = z.preprocess((val) => typeof val === 'string' ? DateTime.fromISO(val) : val, z.custom<DateTime>((val) => val instanceof DateTime && val.isValid, { message: 'Must be in ISO 8601 format' }));`);
1222
- }
1223
- if (opNeedsScalar(root, "interval")) {
1224
- helpers.push(`const _ZodInterval = z.preprocess((val) => typeof val === 'string' ? Interval.fromISO(val) : val, z.custom<Interval>((val) => val instanceof Interval && val.isValid, { message: 'Must be an ISO 8601 interval' })).transform(val => val.toISO()!);`);
1225
- }
1226
- if (opNeedsScalar(root, "json")) {
1227
- helpers.push(`type _JsonValue = string | number | boolean | null | _JsonValue[] | { [key: string]: _JsonValue };`);
1228
- helpers.push(`const _ZodJson: z.ZodType<_JsonValue> = z.lazy(() => z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(_ZodJson), z.record(z.string(), _ZodJson)]));`);
1229
- }
1230
- const lines = [];
1231
- lines.push("");
1232
- lines.push("/**");
1233
- const relFile = options.outPath ? relative2(dirname2(options.outPath), root.file) : root.file;
1234
- lines.push(` * generated from [${basename(root.file)}](file://./${relFile})`);
1235
- lines.push("*/");
1236
- lines.push(`export const ${routerName} = ServerKitRouter();`);
1237
- lines.push("");
1238
- const includeInternal = options.includeInternal ?? true;
1239
- for (const route of root.routes) {
1240
- for (const op of route.operations) {
1241
- if (!includeInternal && resolveModifiers(route, op).includes("internal")) continue;
1242
- lines.push(...generateHandler(route, op, root, options));
1243
- lines.push("");
1244
- }
1245
- }
1246
1259
  const allContent = [
1247
1260
  ...body,
1248
1261
  ...helpers.length ? [
@@ -1328,50 +1341,161 @@ function generateHandler(route, op, root, options) {
1328
1341
  lines.push("");
1329
1342
  }
1330
1343
  }
1331
- const primaryResponse2 = op.responses.find((r) => r.bodyType) ?? op.responses[0];
1344
+ const emitted = emittedResponses(op);
1332
1345
  const serviceParts = inferService(op, route, file);
1333
- const respHeaders = primaryResponse2?.headers ?? [];
1346
+ const call = `await service.${serviceParts.methodName}(${buildArgs(route, op)})`;
1347
+ if (emitted.length > 1) {
1348
+ lines.push(...generateMultiStatusResult(emitted, serviceParts.className, call, options));
1349
+ } else {
1350
+ lines.push(...generateSingleStatusResult(emitted[0], op, serviceParts.className, call, options));
1351
+ }
1352
+ lines.push(`});`);
1353
+ return lines;
1354
+ }
1355
+ __name(generateHandler, "generateHandler");
1356
+ function generateSingleStatusResult(resp, op, className, call, options) {
1357
+ const lines = [];
1358
+ const bodies = resp ? resp.bodies : [];
1359
+ const respHeaders = resp?.headers ?? [];
1334
1360
  const hasRespHeaders = respHeaders.length > 0;
1335
- const headersAnnotation = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, options.modelsWithOutput, "server")}`).join("; ")} }` : "";
1336
- if (primaryResponse2?.bodyType) {
1337
- const { annotation, prelude } = formatTypeAnnotation(primaryResponse2.bodyType, options.modelsWithOutput);
1338
- if (prelude) {
1339
- lines.push(` ${prelude}`);
1340
- }
1341
- lines.push(` const service = ctx.container.get(${serviceParts.className});`);
1361
+ const headersAnnotation = hasRespHeaders ? renderHeadersAnnotation(respHeaders, options.modelsWithOutput) : "";
1362
+ if (bodies.length === 1) {
1363
+ const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput);
1364
+ if (prelude) lines.push(` ${prelude}`);
1365
+ lines.push(` const service = ctx.container.get(${className});`);
1342
1366
  if (hasRespHeaders) {
1343
- lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } = await service.${serviceParts.methodName}(${buildArgs(route, op)});`);
1367
+ lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } = ${call};`);
1344
1368
  } else {
1345
- lines.push(` const result: ${annotation} = await service.${serviceParts.methodName}(${buildArgs(route, op)});`);
1369
+ lines.push(` const result: ${annotation} = ${call};`);
1346
1370
  }
1371
+ } else if (bodies.length > 1) {
1372
+ const { members, preludes } = renderResponseMembers(resp, options, {
1373
+ includeStatus: false,
1374
+ varPrefix: "result"
1375
+ });
1376
+ for (const prelude of preludes) lines.push(` ${prelude}`);
1377
+ lines.push(` const service = ctx.container.get(${className});`);
1378
+ lines.push(` const result: ${members.join(" | ")} = ${call};`);
1347
1379
  } else {
1348
- lines.push(` const service = ctx.container.get(${serviceParts.className});`);
1380
+ lines.push(` const service = ctx.container.get(${className});`);
1349
1381
  if (hasRespHeaders) {
1350
- lines.push(` const result: { headers: ${headersAnnotation} } = await service.${serviceParts.methodName}(${buildArgs(route, op)});`);
1382
+ lines.push(` const result: { headers: ${headersAnnotation} } = ${call};`);
1351
1383
  } else {
1352
- lines.push(` await service.${serviceParts.methodName}(${buildArgs(route, op)});`);
1384
+ lines.push(` ${call};`);
1353
1385
  }
1354
1386
  }
1355
1387
  lines.push("");
1356
- lines.push(` ctx.status = ${primaryResponse2?.statusCode ?? 200};`);
1357
- if (hasRespHeaders) {
1358
- for (const h of respHeaders) {
1359
- const accessor = `result.headers[${JSON.stringify(headerNameToProperty(h.name))}]`;
1360
- if (h.optional) {
1361
- lines.push(` if (${accessor} !== undefined) ctx.set('${h.name}', String(${accessor}));`);
1362
- } else {
1363
- lines.push(` ctx.set('${h.name}', String(${accessor}));`);
1364
- }
1365
- }
1366
- }
1367
- if (primaryResponse2?.bodyType && primaryResponse2.contentType) {
1368
- lines.push(` ctx.type = '${primaryResponse2.contentType}';`);
1388
+ lines.push(` ctx.status = ${resp?.statusCode ?? op.responses[0]?.statusCode ?? 200};`);
1389
+ lines.push(...headerSetLines(respHeaders, " "));
1390
+ if (bodies.length === 1) {
1391
+ lines.push(` ctx.type = '${bodies[0].contentType}';`);
1369
1392
  lines.push(` ctx.body = ${hasRespHeaders ? "result.body" : "result"};`);
1393
+ } else if (bodies.length > 1) {
1394
+ lines.push(` ctx.type = result.contentType;`);
1395
+ lines.push(` ctx.body = result.body;`);
1370
1396
  }
1371
- lines.push(`});`);
1372
1397
  return lines;
1373
1398
  }
1374
- __name(generateHandler, "generateHandler");
1399
+ __name(generateSingleStatusResult, "generateSingleStatusResult");
1400
+ function generateMultiStatusResult(emitted, className, call, options) {
1401
+ const lines = [];
1402
+ const members = [];
1403
+ const preludes = [];
1404
+ for (const resp of emitted) {
1405
+ const rendered = renderResponseMembers(resp, options, {
1406
+ includeStatus: true,
1407
+ varPrefix: `result${resp.statusCode}`
1408
+ });
1409
+ members.push(...rendered.members);
1410
+ preludes.push(...rendered.preludes);
1411
+ }
1412
+ for (const prelude of preludes) lines.push(` ${prelude}`);
1413
+ lines.push(` const service = ctx.container.get(${className});`);
1414
+ lines.push(` const result:`);
1415
+ for (const member of members) lines.push(` | ${member}`);
1416
+ lines.push(` = ${call};`);
1417
+ lines.push("");
1418
+ lines.push(` ctx.status = result.status;`);
1419
+ lines.push(` switch (result.status) {`);
1420
+ for (const resp of emitted) {
1421
+ lines.push(` case ${resp.statusCode}:`);
1422
+ lines.push(...headerSetLines(resp.headers ?? [], " "));
1423
+ if (resp.bodies.length > 0) {
1424
+ lines.push(` ctx.type = result.contentType;`);
1425
+ lines.push(` ctx.body = result.body;`);
1426
+ }
1427
+ lines.push(` break;`);
1428
+ }
1429
+ lines.push(` }`);
1430
+ return lines;
1431
+ }
1432
+ __name(generateMultiStatusResult, "generateMultiStatusResult");
1433
+ function renderResponseMembers(resp, options, opts) {
1434
+ const bodies = resp.bodies;
1435
+ const headers = resp.headers ?? [];
1436
+ const leading = opts.includeStatus ? [
1437
+ `status: ${resp.statusCode}`
1438
+ ] : [];
1439
+ const trailing = headers.length > 0 ? [
1440
+ `headers: ${renderHeadersAnnotation(headers, options.modelsWithOutput)}`
1441
+ ] : [];
1442
+ const preludes = [];
1443
+ if (bodies.length === 0) {
1444
+ return {
1445
+ members: [
1446
+ `{ ${[
1447
+ ...leading,
1448
+ ...trailing
1449
+ ].join("; ")} }`
1450
+ ],
1451
+ preludes
1452
+ };
1453
+ }
1454
+ const uniform = bodies.every((b) => bodyTypesStructurallyEqual(b.bodyType, bodies[0].bodyType));
1455
+ if (uniform) {
1456
+ const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput, `${opts.varPrefix}Type`);
1457
+ if (prelude) preludes.push(prelude);
1458
+ const contentType = bodies.map((b) => `'${b.contentType}'`).join(" | ");
1459
+ return {
1460
+ members: [
1461
+ `{ ${[
1462
+ ...leading,
1463
+ `contentType: ${contentType}`,
1464
+ `body: ${annotation}`,
1465
+ ...trailing
1466
+ ].join("; ")} }`
1467
+ ],
1468
+ preludes
1469
+ };
1470
+ }
1471
+ const members = bodies.map((b, i) => {
1472
+ const { annotation, prelude } = formatTypeAnnotation(b.bodyType, options.modelsWithOutput, `${opts.varPrefix}Type${i}`);
1473
+ if (prelude) preludes.push(prelude);
1474
+ return `{ ${[
1475
+ ...leading,
1476
+ `contentType: '${b.contentType}'`,
1477
+ `body: ${annotation}`,
1478
+ ...trailing
1479
+ ].join("; ")} }`;
1480
+ });
1481
+ return {
1482
+ members,
1483
+ preludes
1484
+ };
1485
+ }
1486
+ __name(renderResponseMembers, "renderResponseMembers");
1487
+ function renderHeadersAnnotation(headers, modelsWithOutput) {
1488
+ const fields = headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput, "server")}`);
1489
+ return `{ ${fields.join("; ")} }`;
1490
+ }
1491
+ __name(renderHeadersAnnotation, "renderHeadersAnnotation");
1492
+ function headerSetLines(headers, indent) {
1493
+ return headers.map((h) => {
1494
+ const accessor = `result.headers[${JSON.stringify(headerNameToProperty(h.name))}]`;
1495
+ return h.optional ? `${indent}if (${accessor} !== undefined) ctx.set('${h.name}', String(${accessor}));` : `${indent}ctx.set('${h.name}', String(${accessor}));`;
1496
+ });
1497
+ }
1498
+ __name(headerSetLines, "headerSetLines");
1375
1499
  function inferService(op, route, file) {
1376
1500
  if (op.service) {
1377
1501
  const [cls = "", method] = op.service.split(".");
@@ -1465,9 +1589,9 @@ function serverTsScalar(name) {
1465
1589
  }
1466
1590
  }
1467
1591
  __name(serverTsScalar, "serverTsScalar");
1468
- function formatTypeAnnotation(bodyType, modelsWithOutput) {
1592
+ function formatTypeAnnotation(bodyType, modelsWithOutput, varName = "resultType") {
1469
1593
  if (bodyType.kind === "array") {
1470
- const inner = formatTypeAnnotation(bodyType.item, modelsWithOutput);
1594
+ const inner = formatTypeAnnotation(bodyType.item, modelsWithOutput, varName);
1471
1595
  return {
1472
1596
  annotation: `${inner.annotation}[]`,
1473
1597
  prelude: inner.prelude
@@ -1484,8 +1608,8 @@ function formatTypeAnnotation(bodyType, modelsWithOutput) {
1484
1608
  };
1485
1609
  const schema = renderType(bodyType);
1486
1610
  return {
1487
- annotation: "z.infer<typeof resultType>",
1488
- prelude: `const resultType = ${schema};`
1611
+ annotation: `z.infer<typeof ${varName}>`,
1612
+ prelude: `const ${varName} = ${schema};`
1489
1613
  };
1490
1614
  }
1491
1615
  __name(formatTypeAnnotation, "formatTypeAnnotation");
@@ -1571,9 +1695,9 @@ function collectTypes(root, modelsWithInput, modelsWithOutput) {
1571
1695
  }
1572
1696
  }
1573
1697
  for (const resp of op.responses) {
1574
- if (resp.bodyType) {
1575
- collectTypeNodeRefs(resp.bodyType, types);
1576
- collectOutputTypeNodeRefs(resp.bodyType, types, modelsWithOutput);
1698
+ for (const body of resp.bodies) {
1699
+ collectTypeNodeRefs(body.bodyType, types);
1700
+ collectOutputTypeNodeRefs(body.bodyType, types, modelsWithOutput);
1577
1701
  }
1578
1702
  if (resp.headers) {
1579
1703
  for (const h of resp.headers) {
@@ -1716,28 +1840,6 @@ function collectTypeNodeRefs(type, out) {
1716
1840
  }
1717
1841
  }
1718
1842
  __name(collectTypeNodeRefs, "collectTypeNodeRefs");
1719
- function paramSourceNeedsDateTime(source) {
1720
- if (!source) return false;
1721
- if (source.kind === "ref") return false;
1722
- if (source.kind === "params") return source.nodes.some((p) => typeNeedsDateTime(p.type));
1723
- return typeNeedsDateTime(source.node);
1724
- }
1725
- __name(paramSourceNeedsDateTime, "paramSourceNeedsDateTime");
1726
- function opNeedsDateTime(root) {
1727
- return root.routes.some((route) => paramSourceNeedsDateTime(route.params) || route.operations.some((op) => !!op.request?.bodies.some((b) => typeNeedsDateTime(b.bodyType)) || op.responses.some((r) => r.bodyType && typeNeedsDateTime(r.bodyType)) || paramSourceNeedsDateTime(op.query) || paramSourceNeedsDateTime(op.headers)));
1728
- }
1729
- __name(opNeedsDateTime, "opNeedsDateTime");
1730
- function paramSourceNeedsScalar(source, name) {
1731
- if (!source) return false;
1732
- if (source.kind === "ref") return false;
1733
- if (source.kind === "params") return source.nodes.some((p) => typeNeedsScalar(p.type, name));
1734
- return typeNeedsScalar(source.node, name);
1735
- }
1736
- __name(paramSourceNeedsScalar, "paramSourceNeedsScalar");
1737
- function opNeedsScalar(root, name) {
1738
- return root.routes.some((route) => paramSourceNeedsScalar(route.params, name) || route.operations.some((op) => !!op.request?.bodies.some((b) => typeNeedsScalar(b.bodyType, name)) || op.responses.some((r) => r.bodyType && typeNeedsScalar(r.bodyType, name)) || paramSourceNeedsScalar(op.query, name) || paramSourceNeedsScalar(op.headers, name)));
1739
- }
1740
- __name(opNeedsScalar, "opNeedsScalar");
1741
1843
  function collectServices(root) {
1742
1844
  const services = /* @__PURE__ */ new Set();
1743
1845
  const inferredService = `${deriveBaseName(root.file)}Service`;
@@ -1755,29 +1857,6 @@ function collectServices(root) {
1755
1857
  ].sort();
1756
1858
  }
1757
1859
  __name(collectServices, "collectServices");
1758
- function hasParamSource(source) {
1759
- if (!source) return false;
1760
- if (source.kind === "ref") return true;
1761
- if (source.kind === "params") return source.nodes.length > 0;
1762
- return true;
1763
- }
1764
- __name(hasParamSource, "hasParamSource");
1765
- function routeNeedsValidation(root) {
1766
- return root.routes.some((r) => hasParamSource(r.params) || r.operations.some((op) => !!op.request || hasParamSource(op.query) || hasParamSource(op.headers)));
1767
- }
1768
- __name(routeNeedsValidation, "routeNeedsValidation");
1769
- function fileNeedsPolicy(root) {
1770
- return root.routes.some((route) => route.operations.some((op) => resolveSecurity(route, op, root) !== SECURITY_NONE));
1771
- }
1772
- __name(fileNeedsPolicy, "fileNeedsPolicy");
1773
- function fileNeedsSignature(root) {
1774
- return root.routes.some((route) => route.operations.some((op) => !!op.signature));
1775
- }
1776
- __name(fileNeedsSignature, "fileNeedsSignature");
1777
- function fileUsesMultipart(root) {
1778
- return root.routes.some((route) => route.operations.some((op) => (op.request?.bodies ?? []).some((b) => b.contentType === "multipart/form-data")));
1779
- }
1780
- __name(fileUsesMultipart, "fileUsesMultipart");
1781
1860
  function isValidIdentifier2(name) {
1782
1861
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
1783
1862
  }
@@ -1814,7 +1893,7 @@ __name(deriveTypeImportPath, "deriveTypeImportPath");
1814
1893
  import { runIncrementalCodegen, parseIncrementalManifest, emptyIncrementalManifest, serializeIncrementalManifest, hashFingerprint, collectTransitiveModelRefs } from "@contractkit/core";
1815
1894
 
1816
1895
  // src/codegen-sdk.ts
1817
- import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2 } from "@contractkit/core";
1896
+ import { resolveModifiers as resolveModifiers2, isJsonMime, classifyContentType as classifyContentType2, observableResponses, thrownResponses } from "@contractkit/core";
1818
1897
  import { basename as basename2, dirname as dirname3, relative as relative3 } from "path";
1819
1898
  function jsonOrFormSerialize(varName, contentType) {
1820
1899
  if (contentType === "application/x-www-form-urlencoded") {
@@ -1891,16 +1970,17 @@ function generateSdk(root, options = {}) {
1891
1970
  if (sdkNeedsBigIntReplacer(root, includeInternal)) valueImports.push("bigIntReplacer");
1892
1971
  if (sdkNeedsBigIntReviver(root, includeInternal)) valueImports.push("parseJson");
1893
1972
  if (sdkNeedsQueryString(root, includeInternal)) valueImports.push("buildQueryString");
1973
+ if (sdkNeedsReadContentType(root, includeInternal)) valueImports.push("readContentType");
1894
1974
  if (valueImports.length > 0) {
1895
1975
  lines.push(`import { ${valueImports.join(", ")} } from '${rel}';`);
1896
1976
  }
1897
1977
  } else {
1898
1978
  lines.push("");
1899
- lines.push("export class SdkError extends Error {");
1979
+ lines.push("export class SdkError<TBody = unknown> extends Error {");
1900
1980
  lines.push(" constructor(");
1901
1981
  lines.push(" public readonly status: number,");
1902
1982
  lines.push(" public readonly statusText: string,");
1903
- lines.push(" public readonly body: unknown,");
1983
+ lines.push(" public readonly body: TBody,");
1904
1984
  lines.push(" public readonly headers: Headers,");
1905
1985
  lines.push(" ) {");
1906
1986
  lines.push(" super(`${status} ${statusText}`);");
@@ -1908,7 +1988,16 @@ function generateSdk(root, options = {}) {
1908
1988
  lines.push(" }");
1909
1989
  lines.push("}");
1910
1990
  lines.push("");
1911
- lines.push("export type SdkFetch = (url: string, init: RequestInit) => Promise<Response>;");
1991
+ lines.push("export interface SdkRequestInit extends RequestInit {");
1992
+ lines.push(" /**");
1993
+ lines.push(" * Statuses this operation declares as values rather than errors \u2014 a 304 from");
1994
+ lines.push(" * conditional-GET middleware, or an error status the service returns deliberately.");
1995
+ lines.push(" * Anything else at or above 400 still throws SdkError.");
1996
+ lines.push(" */");
1997
+ lines.push(" expectStatuses?: number[];");
1998
+ lines.push("}");
1999
+ lines.push("");
2000
+ lines.push("export type SdkFetch = (url: string, init: SdkRequestInit) => Promise<Response>;");
1912
2001
  lines.push("");
1913
2002
  lines.push("export interface SdkOptions {");
1914
2003
  lines.push(" baseUrl: string;");
@@ -1918,9 +2007,13 @@ function generateSdk(root, options = {}) {
1918
2007
  lines.push(" requestIdFactory?: () => string;");
1919
2008
  lines.push("}");
1920
2009
  lines.push("");
2010
+ lines.push("export function readContentType(res: Response): string {");
2011
+ lines.push(" return res.headers.get('content-type')?.split(';')[0]?.trim() ?? '';");
2012
+ lines.push("}");
2013
+ lines.push("");
1921
2014
  lines.push("export function createSdkFetch(options: SdkOptions): SdkFetch {");
1922
2015
  lines.push(" const getRequestId = options.requestIdFactory ?? (() => crypto.randomUUID());");
1923
- lines.push(" return async (url: string, init: RequestInit): Promise<Response> => {");
2016
+ lines.push(" return async (url: string, init: SdkRequestInit): Promise<Response> => {");
1924
2017
  lines.push(" const baseHeaders = typeof options.headers === 'function'");
1925
2018
  lines.push(" ? await options.headers()");
1926
2019
  lines.push(" : options.headers ?? {};");
@@ -1928,7 +2021,7 @@ function generateSdk(root, options = {}) {
1928
2021
  lines.push(" ...init,");
1929
2022
  lines.push(" headers: { ...baseHeaders, 'X-Request-ID': getRequestId(), ...init.headers as Record<string, string> },");
1930
2023
  lines.push(" });");
1931
- lines.push(" if (!res.ok) {");
2024
+ lines.push(" if (!res.ok && !(init.expectStatuses ?? []).includes(res.status)) {");
1932
2025
  lines.push(" const text = await res.text();");
1933
2026
  lines.push(" let body: unknown;");
1934
2027
  lines.push(" try { body = JSON.parse(text); } catch { body = text; }");
@@ -1959,6 +2052,11 @@ function generateSdk(root, options = {}) {
1959
2052
  lines.push(JSON_VALUE_TYPE_DECL);
1960
2053
  }
1961
2054
  lines.push("");
2055
+ const errorAliases = generateErrorBodyAliases(root, options);
2056
+ if (errorAliases.length > 0) {
2057
+ lines.push(...errorAliases);
2058
+ lines.push("");
2059
+ }
1962
2060
  lines.push("/**");
1963
2061
  const relFile = options.outPath ? relative3(dirname3(options.outPath), root.file) : root.file;
1964
2062
  lines.push(` * generated from [${basename2(root.file)}](file://./${relFile})`);
@@ -2006,19 +2104,37 @@ function generateMethod(route, op, file, options) {
2006
2104
  const { modelsWithInput, modelsWithOutput } = options;
2007
2105
  const params = buildMethodParams(route, op, modelsWithInput);
2008
2106
  const paramStr = params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ");
2009
- const primaryResponse2 = op.responses.find((r) => r.bodyType) ?? op.responses[0];
2010
- const isVoid = !primaryResponse2?.bodyType;
2011
- const respCategory = primaryResponse2?.contentType ? classifyContentType2(primaryResponse2.contentType) : "json";
2012
- const dataType = isVoid ? "void" : respCategory === "text" ? "string" : respCategory === "binary" ? "Blob" : renderOutputTsType(primaryResponse2.bodyType, modelsWithOutput);
2013
- const respHeaders = primaryResponse2?.headers ?? [];
2107
+ const observable = observableResponses(op);
2108
+ const thrown = thrownResponses(op);
2109
+ const isMultiStatus = observable.length > 1;
2110
+ const primaryResponse = observable[0];
2111
+ const primaryBodies = primaryResponse ? primaryResponse.bodies : [];
2112
+ const isVoid = primaryBodies.length === 0;
2113
+ const respHeaders = primaryResponse?.headers ?? [];
2014
2114
  const hasRespHeaders = respHeaders.length > 0;
2015
- const headersShape = hasRespHeaders ? `{ ${respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`).join("; ")} }` : "";
2016
- const returnType = hasRespHeaders ? isVoid ? `{ headers: ${headersShape} }` : `{ data: ${dataType}; headers: ${headersShape} }` : dataType;
2115
+ const headersShape = hasRespHeaders ? renderSdkHeadersShape(respHeaders, modelsWithOutput) : "";
2116
+ let returnMembers;
2117
+ let returnType = "";
2118
+ if (isMultiStatus) {
2119
+ returnMembers = observable.flatMap((r) => sdkResponseMembers(r, modelsWithOutput, true));
2120
+ } else if (primaryBodies.length > 1) {
2121
+ returnMembers = sdkResponseMembers(primaryResponse, modelsWithOutput, false);
2122
+ } else {
2123
+ const dataType = isVoid ? "void" : sdkDataType(primaryBodies[0], modelsWithOutput);
2124
+ returnType = hasRespHeaders ? isVoid ? `{ headers: ${headersShape} }` : `{ data: ${dataType}; headers: ${headersShape} }` : dataType;
2125
+ }
2126
+ if (returnMembers?.length === 1) {
2127
+ returnType = returnMembers[0];
2128
+ returnMembers = void 0;
2129
+ }
2130
+ const expectStatuses = observable.filter((r) => r.statusCode < 200 || r.statusCode >= 300).map((r) => r.statusCode);
2017
2131
  const desc = op.description ?? route.description;
2018
- if (op.name || desc) {
2132
+ const errorBodyName = thrown.some((r) => r.bodies.length > 0) ? errorBodyTypeName(route, op) : void 0;
2133
+ if (op.name || desc || errorBodyName) {
2019
2134
  const tags = [];
2020
2135
  if (op.name) tags.push(`@name ${op.name}`);
2021
2136
  if (desc) tags.push(`@description ${desc}`);
2137
+ if (errorBodyName) tags.push(`@throws {SdkError<${errorBodyName}>} on ${thrown.map((r) => r.statusCode).join(", ")}`);
2022
2138
  const contentLines = tags.flatMap((t) => escapeJsDocLines(t));
2023
2139
  if (contentLines.length === 1) {
2024
2140
  lines.push(` /** ${contentLines[0]} */`);
@@ -2028,7 +2144,13 @@ function generateMethod(route, op, file, options) {
2028
2144
  lines.push(` */`);
2029
2145
  }
2030
2146
  }
2031
- lines.push(` async ${methodName}(${paramStr}): Promise<${returnType}> {`);
2147
+ if (returnMembers) {
2148
+ lines.push(` async ${methodName}(${paramStr}): Promise<`);
2149
+ for (const member of returnMembers) lines.push(` | ${member}`);
2150
+ lines.push(` > {`);
2151
+ } else {
2152
+ lines.push(` async ${methodName}(${paramStr}): Promise<${returnType}> {`);
2153
+ }
2032
2154
  const urlExpr = buildUrlExpression(route.path, route.params);
2033
2155
  const hasQuery = !!op.query;
2034
2156
  let fetchUrl = urlExpr;
@@ -2088,7 +2210,9 @@ function generateMethod(route, op, file, options) {
2088
2210
  fetchArgs.push("headers: customHeaders");
2089
2211
  }
2090
2212
  }
2091
- const resultPrefix = isVoid && !hasRespHeaders ? "" : "const result = ";
2213
+ if (expectStatuses.length > 0) fetchArgs.push(`expectStatuses: [${expectStatuses.join(", ")}]`);
2214
+ const needsResult = isMultiStatus || !isVoid || hasRespHeaders;
2215
+ const resultPrefix = needsResult ? "const result = " : "";
2092
2216
  if (fetchArgs.length === 2 && !hasBody && !hasOpHeaders && !hasQuery) {
2093
2217
  lines.push(` ${resultPrefix}await this.fetch(\`${fetchUrl}\`, { method: '${httpMethod}' });`);
2094
2218
  } else {
@@ -2098,22 +2222,185 @@ function generateMethod(route, op, file, options) {
2098
2222
  }
2099
2223
  lines.push(` });`);
2100
2224
  }
2101
- const readBodyExpr = respCategory === "text" ? `await result.text()` : respCategory === "binary" ? `await result.blob()` : `await parseJson<${dataType}>(result)`;
2102
- if (hasRespHeaders) {
2103
- const headerEntries = respHeaders.map((h) => `${quoteKey(headerNameToProperty(h.name))}: result.headers.get('${h.name}') ?? undefined`).join(", ");
2225
+ if (isMultiStatus) {
2226
+ const [fallback, ...rest] = observable;
2227
+ lines.push(` switch (result.status) {`);
2228
+ for (const resp of rest) {
2229
+ lines.push(` case ${resp.statusCode}:`);
2230
+ lines.push(...sdkReturnLines(resp, modelsWithOutput, " ", true));
2231
+ }
2232
+ lines.push(` default:`);
2233
+ lines.push(...sdkReturnLines(fallback, modelsWithOutput, " ", true));
2234
+ lines.push(` }`);
2235
+ } else if (primaryBodies.length > 1) {
2236
+ lines.push(...sdkReturnLines(primaryResponse, modelsWithOutput, " ", false));
2237
+ } else if (hasRespHeaders) {
2238
+ const headerEntries = sdkHeaderEntries(respHeaders);
2104
2239
  if (isVoid) {
2105
2240
  lines.push(` return { headers: { ${headerEntries} } };`);
2106
2241
  } else {
2107
- lines.push(` const data = ${readBodyExpr};`);
2242
+ lines.push(` const data = ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
2108
2243
  lines.push(` return { data, headers: { ${headerEntries} } };`);
2109
2244
  }
2110
2245
  } else if (!isVoid) {
2111
- lines.push(` return ${readBodyExpr};`);
2246
+ lines.push(` return ${sdkReadExpr(primaryBodies[0], modelsWithOutput)};`);
2112
2247
  }
2113
2248
  lines.push(" }");
2114
2249
  return lines;
2115
2250
  }
2116
2251
  __name(generateMethod, "generateMethod");
2252
+ function sdkDataType(body, modelsWithOutput) {
2253
+ const category = classifyContentType2(body.contentType);
2254
+ if (category === "text") return "string";
2255
+ if (category === "binary") return "Blob";
2256
+ return renderOutputTsType(body.bodyType, modelsWithOutput);
2257
+ }
2258
+ __name(sdkDataType, "sdkDataType");
2259
+ function sdkReadExpr(body, modelsWithOutput) {
2260
+ const category = classifyContentType2(body.contentType);
2261
+ if (category === "text") return "await result.text()";
2262
+ if (category === "binary") return "await result.blob()";
2263
+ return `await parseJson<${renderOutputTsType(body.bodyType, modelsWithOutput)}>(result)`;
2264
+ }
2265
+ __name(sdkReadExpr, "sdkReadExpr");
2266
+ function renderSdkHeadersShape(headers, modelsWithOutput) {
2267
+ const fields = headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}${h.optional ? "?" : ""}: ${renderOutputTsType(h.type, modelsWithOutput)}`);
2268
+ return `{ ${fields.join("; ")} }`;
2269
+ }
2270
+ __name(renderSdkHeadersShape, "renderSdkHeadersShape");
2271
+ function sdkHeaderEntries(headers) {
2272
+ return headers.map((h) => `${quoteKey(headerNameToProperty(h.name))}: result.headers.get('${h.name}') ?? undefined`).join(", ");
2273
+ }
2274
+ __name(sdkHeaderEntries, "sdkHeaderEntries");
2275
+ function sdkResponseMembers(resp, modelsWithOutput, includeStatus) {
2276
+ const bodies = resp.bodies;
2277
+ const headers = resp.headers ?? [];
2278
+ const leading = includeStatus ? [
2279
+ `status: ${resp.statusCode}`
2280
+ ] : [];
2281
+ const trailing = headers.length > 0 ? [
2282
+ `headers: ${renderSdkHeadersShape(headers, modelsWithOutput)}`
2283
+ ] : [];
2284
+ if (bodies.length === 0) {
2285
+ return [
2286
+ `{ ${[
2287
+ ...leading,
2288
+ ...trailing
2289
+ ].join("; ")} }`
2290
+ ];
2291
+ }
2292
+ const dataTypes = bodies.map((b) => sdkDataType(b, modelsWithOutput));
2293
+ if (dataTypes.every((t) => t === dataTypes[0])) {
2294
+ const contentType = bodies.map((b) => `'${b.contentType}'`).join(" | ");
2295
+ return [
2296
+ `{ ${[
2297
+ ...leading,
2298
+ `contentType: ${contentType}`,
2299
+ `data: ${dataTypes[0]}`,
2300
+ ...trailing
2301
+ ].join("; ")} }`
2302
+ ];
2303
+ }
2304
+ return bodies.map((b, i) => `{ ${[
2305
+ ...leading,
2306
+ `contentType: '${b.contentType}'`,
2307
+ `data: ${dataTypes[i]}`,
2308
+ ...trailing
2309
+ ].join("; ")} }`);
2310
+ }
2311
+ __name(sdkResponseMembers, "sdkResponseMembers");
2312
+ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus) {
2313
+ const bodies = resp.bodies;
2314
+ const headers = resp.headers ?? [];
2315
+ const leading = includeStatus ? [
2316
+ `status: ${resp.statusCode}`
2317
+ ] : [];
2318
+ const trailing = headers.length > 0 ? [
2319
+ `headers: { ${sdkHeaderEntries(headers)} }`
2320
+ ] : [];
2321
+ if (bodies.length === 0) {
2322
+ return [
2323
+ `${indent}return { ${[
2324
+ ...leading,
2325
+ ...trailing
2326
+ ].join(", ")} };`
2327
+ ];
2328
+ }
2329
+ if (bodies.length === 1) {
2330
+ const fields = [
2331
+ ...leading,
2332
+ `contentType: '${bodies[0].contentType}'`,
2333
+ `data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
2334
+ ...trailing
2335
+ ];
2336
+ return [
2337
+ `${indent}return { ${fields.join(", ")} };`
2338
+ ];
2339
+ }
2340
+ const dataTypes = bodies.map((b) => sdkDataType(b, modelsWithOutput));
2341
+ if (dataTypes.every((t) => t === dataTypes[0])) {
2342
+ const cast = bodies.map((b) => `'${b.contentType}'`).join(" | ");
2343
+ const fields = [
2344
+ ...leading,
2345
+ `contentType: readContentType(result) as ${cast}`,
2346
+ `data: ${sdkReadExpr(bodies[0], modelsWithOutput)}`,
2347
+ ...trailing
2348
+ ];
2349
+ return [
2350
+ `${indent}return { ${fields.join(", ")} };`
2351
+ ];
2352
+ }
2353
+ const lines = [
2354
+ `${indent}switch (readContentType(result)) {`
2355
+ ];
2356
+ for (const body of bodies.slice(1)) {
2357
+ const fields = [
2358
+ ...leading,
2359
+ `contentType: '${body.contentType}'`,
2360
+ `data: ${sdkReadExpr(body, modelsWithOutput)}`,
2361
+ ...trailing
2362
+ ];
2363
+ lines.push(`${indent} case '${body.contentType}':`);
2364
+ lines.push(`${indent} return { ${fields.join(", ")} };`);
2365
+ }
2366
+ const first = bodies[0];
2367
+ const fallbackFields = [
2368
+ ...leading,
2369
+ `contentType: '${first.contentType}'`,
2370
+ `data: ${sdkReadExpr(first, modelsWithOutput)}`,
2371
+ ...trailing
2372
+ ];
2373
+ lines.push(`${indent} default:`);
2374
+ lines.push(`${indent} return { ${fallbackFields.join(", ")} };`);
2375
+ lines.push(`${indent}}`);
2376
+ return lines;
2377
+ }
2378
+ __name(sdkReturnLines, "sdkReturnLines");
2379
+ function errorBodyTypeName(route, op) {
2380
+ const method = deriveMethodName(op, route);
2381
+ return `${method.charAt(0).toUpperCase()}${method.slice(1)}ErrorBody`;
2382
+ }
2383
+ __name(errorBodyTypeName, "errorBodyTypeName");
2384
+ function generateErrorBodyAliases(root, options) {
2385
+ const includeInternal = options.includeInternal ?? false;
2386
+ const lines = [];
2387
+ for (const route of root.routes) {
2388
+ for (const op of route.operations) {
2389
+ const mods = resolveModifiers2(route, op);
2390
+ if (!includeInternal && mods.includes("internal")) continue;
2391
+ const types = /* @__PURE__ */ new Set();
2392
+ for (const resp of thrownResponses(op)) {
2393
+ for (const body of resp.bodies) types.add(sdkDataType(body, options.modelsWithOutput));
2394
+ }
2395
+ if (types.size === 0) continue;
2396
+ lines.push(`export type ${errorBodyTypeName(route, op)} = ${[
2397
+ ...types
2398
+ ].join(" | ")};`);
2399
+ }
2400
+ }
2401
+ return lines;
2402
+ }
2403
+ __name(generateErrorBodyAliases, "generateErrorBodyAliases");
2117
2404
  function buildUrlExpression(path, _) {
2118
2405
  return path.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_match, name) => {
2119
2406
  return `\${encodeURIComponent(${name})}`;
@@ -2350,9 +2637,9 @@ function collectTypes2(root, modelsWithInput, modelsWithOutput, includeInternal
2350
2637
  }
2351
2638
  }
2352
2639
  for (const resp of op.responses) {
2353
- if (resp.bodyType) {
2354
- collectTypeNodeRefs2(resp.bodyType, types);
2355
- collectOutputTypeNodeRefs2(resp.bodyType, types, modelsWithOutput);
2640
+ for (const body of resp.bodies) {
2641
+ collectTypeNodeRefs2(body.bodyType, types);
2642
+ collectOutputTypeNodeRefs2(body.bodyType, types, modelsWithOutput);
2356
2643
  }
2357
2644
  if (resp.headers) {
2358
2645
  for (const h of resp.headers) {
@@ -2454,6 +2741,16 @@ function sdkNeedsQueryString(root, includeInternal = false) {
2454
2741
  return false;
2455
2742
  }
2456
2743
  __name(sdkNeedsQueryString, "sdkNeedsQueryString");
2744
+ function sdkNeedsReadContentType(root, includeInternal = false) {
2745
+ for (const route of root.routes) {
2746
+ for (const op of route.operations) {
2747
+ if (!includeInternal && resolveModifiers2(route, op).includes("internal")) continue;
2748
+ if (observableResponses(op).some((r) => r.bodies.length > 1)) return true;
2749
+ }
2750
+ }
2751
+ return false;
2752
+ }
2753
+ __name(sdkNeedsReadContentType, "sdkNeedsReadContentType");
2457
2754
  function sdkNeedsBigIntReplacer(root, includeInternal = false) {
2458
2755
  for (const route of root.routes) {
2459
2756
  for (const op of route.operations) {
@@ -2468,10 +2765,7 @@ function sdkNeedsBigIntReviver(root, includeInternal = false) {
2468
2765
  for (const route of root.routes) {
2469
2766
  for (const op of route.operations) {
2470
2767
  if (!includeInternal && resolveModifiers2(route, op).includes("internal")) continue;
2471
- if (op.responses.some((r) => {
2472
- if (!r.bodyType) return false;
2473
- return !r.contentType || classifyContentType2(r.contentType) === "json";
2474
- })) {
2768
+ if (op.responses.some((r) => r.bodies.some((b) => classifyContentType2(b.contentType) === "json"))) {
2475
2769
  return true;
2476
2770
  }
2477
2771
  }
@@ -2488,7 +2782,7 @@ function sdkNeedsJson(root, includeInternal = false) {
2488
2782
  if (src.kind === "params") return src.nodes.some((p) => typeNeedsScalar(p.type, "json"));
2489
2783
  return typeNeedsScalar(src.node, "json");
2490
2784
  }, "check");
2491
- if (!!op.request?.bodies.some((b) => typeNeedsScalar(b.bodyType, "json")) || op.responses.some((r) => r.bodyType && typeNeedsScalar(r.bodyType, "json")) || check(op.query) || check(op.headers) || check(route.params)) return true;
2785
+ if (!!op.request?.bodies.some((b) => typeNeedsScalar(b.bodyType, "json")) || op.responses.some((r) => r.bodies.some((b) => typeNeedsScalar(b.bodyType, "json"))) || check(op.query) || check(op.headers) || check(route.params)) return true;
2492
2786
  }
2493
2787
  }
2494
2788
  return false;
@@ -2572,11 +2866,11 @@ function deriveTypeImportPath2(file, template) {
2572
2866
  __name(deriveTypeImportPath2, "deriveTypeImportPath");
2573
2867
  function generateSdkOptions() {
2574
2868
  return [
2575
- "export class SdkError extends Error {",
2869
+ "export class SdkError<TBody = unknown> extends Error {",
2576
2870
  " constructor(",
2577
2871
  " public readonly status: number,",
2578
2872
  " public readonly statusText: string,",
2579
- " public readonly body: unknown,",
2873
+ " public readonly body: TBody,",
2580
2874
  " public readonly headers: Headers,",
2581
2875
  " ) {",
2582
2876
  " super(`${status} ${statusText}`);",
@@ -2584,7 +2878,16 @@ function generateSdkOptions() {
2584
2878
  " }",
2585
2879
  "}",
2586
2880
  "",
2587
- "export type SdkFetch = (url: string, init: RequestInit) => Promise<Response>;",
2881
+ "export interface SdkRequestInit extends RequestInit {",
2882
+ " /**",
2883
+ " * Statuses this operation declares as values rather than errors \u2014 a 304 from",
2884
+ " * conditional-GET middleware, or an error status the service returns deliberately.",
2885
+ " * Anything else at or above 400 still throws SdkError.",
2886
+ " */",
2887
+ " expectStatuses?: number[];",
2888
+ "}",
2889
+ "",
2890
+ "export type SdkFetch = (url: string, init: SdkRequestInit) => Promise<Response>;",
2588
2891
  "",
2589
2892
  "export interface SdkOptions {",
2590
2893
  " baseUrl: string;",
@@ -2610,9 +2913,13 @@ function generateSdkOptions() {
2610
2913
  "",
2611
2914
  JSON_VALUE_TYPE_DECL,
2612
2915
  "",
2916
+ "export function readContentType(res: Response): string {",
2917
+ " return res.headers.get('content-type')?.split(';')[0]?.trim() ?? '';",
2918
+ "}",
2919
+ "",
2613
2920
  "export function createSdkFetch(options: SdkOptions): SdkFetch {",
2614
2921
  " const getRequestId = options.requestIdFactory ?? (() => crypto.randomUUID());",
2615
- " return async (url: string, init: RequestInit): Promise<Response> => {",
2922
+ " return async (url: string, init: SdkRequestInit): Promise<Response> => {",
2616
2923
  " const baseHeaders = typeof options.headers === 'function'",
2617
2924
  " ? await options.headers()",
2618
2925
  " : options.headers ?? {};",
@@ -2620,7 +2927,7 @@ function generateSdkOptions() {
2620
2927
  " ...init,",
2621
2928
  " headers: { ...baseHeaders, 'X-Request-ID': getRequestId(), ...init.headers as Record<string, string> },",
2622
2929
  " });",
2623
- " if (!res.ok) {",
2930
+ " if (!res.ok && !(init.expectStatuses ?? []).includes(res.status)) {",
2624
2931
  " const text = await res.text();",
2625
2932
  " let body: unknown;",
2626
2933
  " try { body = JSON.parse(text); } catch { body = text; }",
@@ -2719,6 +3026,7 @@ function generateAreaClient(input) {
2719
3026
  const { area, outPath, inlineFiles, subareaClients, sdkOptionsPath } = input;
2720
3027
  const className = deriveAreaClientClassName(area);
2721
3028
  const collectedMethodLines = [];
3029
+ const collectedErrorAliases = /* @__PURE__ */ new Set();
2722
3030
  const seenMethods = /* @__PURE__ */ new Set();
2723
3031
  const typesByImportPath = /* @__PURE__ */ new Map();
2724
3032
  const unresolvedTypes = /* @__PURE__ */ new Set();
@@ -2726,6 +3034,7 @@ function generateAreaClient(input) {
2726
3034
  let needsBigIntReplacer = false;
2727
3035
  let needsBigIntReviver = false;
2728
3036
  let needsQueryString = false;
3037
+ let needsReadContentType = false;
2729
3038
  for (const inline of inlineFiles) {
2730
3039
  const includeInternal = inline.codegenOptions.includeInternal ?? false;
2731
3040
  const { lines: methodLines, methodNames } = generateClientMethods(inline.root, inline.codegenOptions);
@@ -2736,10 +3045,12 @@ function generateAreaClient(input) {
2736
3045
  seenMethods.add(name);
2737
3046
  }
2738
3047
  collectedMethodLines.push(...methodLines);
3048
+ for (const alias of generateErrorBodyAliases(inline.root, inline.codegenOptions)) collectedErrorAliases.add(alias);
2739
3049
  if (sdkNeedsJson(inline.root, includeInternal)) needsJson = true;
2740
3050
  if (sdkNeedsBigIntReplacer(inline.root, includeInternal)) needsBigIntReplacer = true;
2741
3051
  if (sdkNeedsBigIntReviver(inline.root, includeInternal)) needsBigIntReviver = true;
2742
3052
  if (sdkNeedsQueryString(inline.root, includeInternal)) needsQueryString = true;
3053
+ if (sdkNeedsReadContentType(inline.root, includeInternal)) needsReadContentType = true;
2743
3054
  const typesForFile = collectTypes2(inline.root, inline.codegenOptions.modelsWithInput, inline.codegenOptions.modelsWithOutput, includeInternal);
2744
3055
  const { modelOutPaths } = inline.codegenOptions;
2745
3056
  if (modelOutPaths) {
@@ -2767,6 +3078,7 @@ function generateAreaClient(input) {
2767
3078
  if (needsBigIntReplacer) valueImports.push("bigIntReplacer");
2768
3079
  if (needsBigIntReviver) valueImports.push("parseJson");
2769
3080
  if (needsQueryString) valueImports.push("buildQueryString");
3081
+ if (needsReadContentType) valueImports.push("readContentType");
2770
3082
  if (valueImports.length > 0) {
2771
3083
  lines.push(`import { ${valueImports.join(", ")} } from '${sdkOptionsRel}';`);
2772
3084
  }
@@ -2791,6 +3103,10 @@ function generateAreaClient(input) {
2791
3103
  lines.push(`import { ${sc.client.className} } from '${sc.client.importPath}';`);
2792
3104
  }
2793
3105
  lines.push("");
3106
+ if (collectedErrorAliases.size > 0) {
3107
+ lines.push(...collectedErrorAliases);
3108
+ lines.push("");
3109
+ }
2794
3110
  lines.push(`export class ${className} {`);
2795
3111
  for (const sc of subareaClients) {
2796
3112
  lines.push(` readonly ${sc.propertyName}: ${sc.client.className};`);
@@ -3087,7 +3403,7 @@ function renderOutputField(field, outputCase, modelsWithOutput, target) {
3087
3403
  __name(renderOutputField, "renderOutputField");
3088
3404
 
3089
3405
  // src/codegen-mcp.ts
3090
- import { resolveModifiers as resolveModifiers3 } from "@contractkit/core";
3406
+ import { resolveModifiers as resolveModifiers3, emittedResponses as emittedResponses2 } from "@contractkit/core";
3091
3407
  import { basename as basename3, dirname as dirname5, relative as relative5 } from "path";
3092
3408
  function mcpConfig(op) {
3093
3409
  return op.mcp && typeof op.mcp === "object" ? op.mcp : void 0;
@@ -3213,12 +3529,15 @@ function argsSchemaExpr(props) {
3213
3529
  return `z.object({ ${fields} })`;
3214
3530
  }
3215
3531
  __name(argsSchemaExpr, "argsSchemaExpr");
3216
- function primaryResponse(op) {
3217
- return op.responses.find((r) => r.bodyType) ?? op.responses[0];
3532
+ function primaryResponseBody(op) {
3533
+ for (const resp of emittedResponses2(op)) {
3534
+ if (resp.bodies[0]) return resp.bodies[0].bodyType;
3535
+ }
3536
+ return void 0;
3218
3537
  }
3219
- __name(primaryResponse, "primaryResponse");
3538
+ __name(primaryResponseBody, "primaryResponseBody");
3220
3539
  function outputSchemaExpr(op) {
3221
- const body = primaryResponse(op)?.bodyType;
3540
+ const body = primaryResponseBody(op);
3222
3541
  if (!body) return void 0;
3223
3542
  if (body.kind === "ref") return body.name;
3224
3543
  if (body.kind === "inlineObject") return renderType(body);
@@ -3287,7 +3606,7 @@ function collectSchemaIds(ops, modelsWithInput) {
3287
3606
  }
3288
3607
  walkSourceRefs(op.query, ids, modelsWithInput);
3289
3608
  walkSourceRefs(op.headers, ids, modelsWithInput);
3290
- const body = primaryResponse(op)?.bodyType;
3609
+ const body = primaryResponseBody(op);
3291
3610
  if (body && (body.kind === "ref" || body.kind === "inlineObject")) walkTypeRefs(body, ids, "read");
3292
3611
  }
3293
3612
  return ids;
@@ -3391,7 +3710,7 @@ function renderToolClass(plan, file, options) {
3391
3710
  const props = buildArgsProps(route, op, options.modelsWithInput);
3392
3711
  const destructure = props.map((p) => p.key);
3393
3712
  const callArgs = buildArgs(route, op);
3394
- const isVoid = !primaryResponse(op)?.bodyType;
3713
+ const isVoid = !primaryResponseBody(op);
3395
3714
  const structured = !!outExpr;
3396
3715
  lines.push(" async handle(args: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {");
3397
3716
  if (destructure.length > 0) {
@@ -3803,7 +4122,7 @@ function collectOpRootRefs(root, modelMap) {
3803
4122
  for (const body of op.request.bodies) seeds.push(body.bodyType);
3804
4123
  }
3805
4124
  for (const resp of op.responses) {
3806
- if (resp.bodyType) seeds.push(resp.bodyType);
4125
+ for (const body of resp.bodies) seeds.push(body.bodyType);
3807
4126
  if (resp.headers) {
3808
4127
  for (const h of resp.headers) seeds.push(h.type);
3809
4128
  }