@yiaany/ghostapi 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CONTRIBUTING.md +132 -119
  3. package/LICENSE +21 -21
  4. package/PROVENANCE.md +13 -0
  5. package/README.md +78 -84
  6. package/SECURITY.md +56 -53
  7. package/dist/actions/gateway.js +320 -72
  8. package/dist/actions/gateway.js.map +1 -1
  9. package/dist/actions/index.d.ts +2 -2
  10. package/dist/actions/index.js +1 -1
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/agents/agentPrompt.js +7 -5
  13. package/dist/agents/agentPrompt.js.map +1 -1
  14. package/dist/ai/aiClient.js +5 -5
  15. package/dist/ai/aiClient.js.map +1 -1
  16. package/dist/ai/aiGenerator.js +66 -26
  17. package/dist/ai/aiGenerator.js.map +1 -1
  18. package/dist/ai/genericInference.js +151 -36
  19. package/dist/ai/genericInference.js.map +1 -1
  20. package/dist/ai/prompts.js +55 -49
  21. package/dist/ai/prompts.js.map +1 -1
  22. package/dist/approvals/approvalInbox.js +649 -133
  23. package/dist/approvals/approvalInbox.js.map +1 -1
  24. package/dist/approvals/index.d.ts +2 -2
  25. package/dist/approvals/index.js +1 -1
  26. package/dist/approvals/index.js.map +1 -1
  27. package/dist/behavior/behaviorStore.js +34 -14
  28. package/dist/behavior/behaviorStore.js.map +1 -1
  29. package/dist/cache/index.js +14 -5
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +226 -93
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.js +17 -3
  34. package/dist/cli/openUrl.js.map +1 -1
  35. package/dist/cli/parser.js +79 -18
  36. package/dist/cli/parser.js.map +1 -1
  37. package/dist/config/dataPaths.js +4 -2
  38. package/dist/config/dataPaths.js.map +1 -1
  39. package/dist/config/localConfig.js +5 -2
  40. package/dist/config/localConfig.js.map +1 -1
  41. package/dist/config/serverConfig.js +11 -5
  42. package/dist/config/serverConfig.js.map +1 -1
  43. package/dist/contracts/contract.js +311 -83
  44. package/dist/contracts/contract.js.map +1 -1
  45. package/dist/contracts/index.d.ts +2 -2
  46. package/dist/contracts/index.js +1 -1
  47. package/dist/contracts/index.js.map +1 -1
  48. package/dist/credentials/credentialBroker.js +391 -79
  49. package/dist/credentials/credentialBroker.js.map +1 -1
  50. package/dist/credentials/index.d.ts +2 -2
  51. package/dist/credentials/index.js +1 -1
  52. package/dist/credentials/index.js.map +1 -1
  53. package/dist/dashboard/app.js +823 -685
  54. package/dist/dashboard/dashboard.js.map +1 -1
  55. package/dist/dashboard/index.html +704 -267
  56. package/dist/dashboard/styles.css +1040 -349
  57. package/dist/egress/capabilities.js +55 -25
  58. package/dist/egress/capabilities.js.map +1 -1
  59. package/dist/egress/linuxBootstrap.js +47 -11
  60. package/dist/egress/linuxBootstrap.js.map +1 -1
  61. package/dist/egress/run.js +170 -31
  62. package/dist/egress/run.js.map +1 -1
  63. package/dist/errors/errorEngine.js +15 -3
  64. package/dist/errors/errorEngine.js.map +1 -1
  65. package/dist/errors/providerErrors.js.map +1 -1
  66. package/dist/evals/evals.js +304 -78
  67. package/dist/evals/evals.js.map +1 -1
  68. package/dist/evals/index.d.ts +2 -2
  69. package/dist/evals/index.js +1 -1
  70. package/dist/evals/index.js.map +1 -1
  71. package/dist/evidence/index.d.ts +2 -2
  72. package/dist/evidence/index.js +1 -1
  73. package/dist/evidence/index.js.map +1 -1
  74. package/dist/evidence/report.d.ts +1 -1
  75. package/dist/evidence/report.js +263 -79
  76. package/dist/evidence/report.js.map +1 -1
  77. package/dist/fault/faultLab.js +24 -9
  78. package/dist/fault/faultLab.js.map +1 -1
  79. package/dist/index.d.ts +42 -42
  80. package/dist/index.js +21 -21
  81. package/dist/index.js.map +1 -1
  82. package/dist/inventory/analysis.d.ts +1 -1
  83. package/dist/inventory/analysis.js +174 -50
  84. package/dist/inventory/analysis.js.map +1 -1
  85. package/dist/inventory/index.d.ts +4 -4
  86. package/dist/inventory/index.js +3 -3
  87. package/dist/inventory/index.js.map +1 -1
  88. package/dist/inventory/inventoryController.js +362 -115
  89. package/dist/inventory/inventoryController.js.map +1 -1
  90. package/dist/inventory/types.js +680 -140
  91. package/dist/inventory/types.js.map +1 -1
  92. package/dist/ledger/actionLedger.js +355 -78
  93. package/dist/ledger/actionLedger.js.map +1 -1
  94. package/dist/ledger/index.d.ts +2 -2
  95. package/dist/ledger/index.js +1 -1
  96. package/dist/ledger/index.js.map +1 -1
  97. package/dist/mcp/server.js +26 -11
  98. package/dist/mcp/server.js.map +1 -1
  99. package/dist/policy/index.d.ts +2 -2
  100. package/dist/policy/index.js +1 -1
  101. package/dist/policy/index.js.map +1 -1
  102. package/dist/policy/policy.js +112 -32
  103. package/dist/policy/policy.js.map +1 -1
  104. package/dist/productTelemetry/index.js +71 -20
  105. package/dist/productTelemetry/index.js.map +1 -1
  106. package/dist/providers/conformance.js +10 -5
  107. package/dist/providers/conformance.js.map +1 -1
  108. package/dist/providers/discord.js +2 -2
  109. package/dist/providers/generic.js +3 -3
  110. package/dist/providers/github.js +2 -2
  111. package/dist/providers/index.d.ts +3 -3
  112. package/dist/providers/index.js +2 -2
  113. package/dist/providers/index.js.map +1 -1
  114. package/dist/providers/openai.js +3 -3
  115. package/dist/providers/packs/resendPack.js +51 -25
  116. package/dist/providers/packs/resendPack.js.map +1 -1
  117. package/dist/providers/packs/stripePack.js +590 -121
  118. package/dist/providers/packs/stripePack.js.map +1 -1
  119. package/dist/providers/registry.js +4 -4
  120. package/dist/providers/registry.js.map +1 -1
  121. package/dist/providers/runtime.js +7 -6
  122. package/dist/providers/runtime.js.map +1 -1
  123. package/dist/providers/stripeWebhook.js +39 -10
  124. package/dist/providers/stripeWebhook.js.map +1 -1
  125. package/dist/providers/twilio.js +2 -2
  126. package/dist/proxy/cacheKey.js +9 -2
  127. package/dist/proxy/cacheKey.js.map +1 -1
  128. package/dist/proxy/providerDetector.js +22 -9
  129. package/dist/proxy/providerDetector.js.map +1 -1
  130. package/dist/proxy/proxyHandler.js +57 -22
  131. package/dist/proxy/proxyHandler.js.map +1 -1
  132. package/dist/proxy/requestNormalizer.js +4 -2
  133. package/dist/proxy/requestNormalizer.js.map +1 -1
  134. package/dist/reliability/costGovernance.js +211 -49
  135. package/dist/reliability/costGovernance.js.map +1 -1
  136. package/dist/reliability/index.d.ts +8 -8
  137. package/dist/reliability/index.js +4 -4
  138. package/dist/reliability/index.js.map +1 -1
  139. package/dist/reliability/reconciliation.js +274 -65
  140. package/dist/reliability/reconciliation.js.map +1 -1
  141. package/dist/reliability/runtimeHealth.js +127 -35
  142. package/dist/reliability/runtimeHealth.js.map +1 -1
  143. package/dist/reliability/slo.js +257 -50
  144. package/dist/reliability/slo.js.map +1 -1
  145. package/dist/report/safetyReport.d.ts +6 -0
  146. package/dist/report/safetyReport.js +56 -12
  147. package/dist/report/safetyReport.js.map +1 -1
  148. package/dist/rules/aiRules.js +5 -5
  149. package/dist/rules/aiRules.js.map +1 -1
  150. package/dist/safety/index.d.ts +2 -2
  151. package/dist/safety/index.js +1 -1
  152. package/dist/safety/index.js.map +1 -1
  153. package/dist/safety/safetyController.js +675 -165
  154. package/dist/safety/safetyController.js.map +1 -1
  155. package/dist/scenarios/scenarioBundle.js +204 -71
  156. package/dist/scenarios/scenarioBundle.js.map +1 -1
  157. package/dist/scenarios/scenarioStore.js +88 -29
  158. package/dist/scenarios/scenarioStore.js.map +1 -1
  159. package/dist/security/headerSanitizer.js +52 -10
  160. package/dist/security/headerSanitizer.js.map +1 -1
  161. package/dist/security/secrets.js +10 -5
  162. package/dist/security/secrets.js.map +1 -1
  163. package/dist/server/accessControl.js +37 -10
  164. package/dist/server/accessControl.js.map +1 -1
  165. package/dist/server/createServer.js +1 -1
  166. package/dist/server/createServer.js.map +1 -1
  167. package/dist/server/eventsStore.js +15 -5
  168. package/dist/server/eventsStore.js.map +1 -1
  169. package/dist/server/routes.js +61 -19
  170. package/dist/server/routes.js.map +1 -1
  171. package/dist/server/sse.js +1 -1
  172. package/dist/server/sse.js.map +1 -1
  173. package/dist/setup/sdkDetector.js +7 -2
  174. package/dist/setup/sdkDetector.js.map +1 -1
  175. package/dist/setup/setupGenerator.js +136 -48
  176. package/dist/setup/setupGenerator.js.map +1 -1
  177. package/dist/state/stateExtractor.js +3 -1
  178. package/dist/state/stateExtractor.js.map +1 -1
  179. package/dist/state/stateResolver.js +10 -7
  180. package/dist/state/stateResolver.js.map +1 -1
  181. package/dist/state/stateStore.js +13 -4
  182. package/dist/state/stateStore.js.map +1 -1
  183. package/dist/storage/fileStore.js +12 -4
  184. package/dist/storage/fileStore.js.map +1 -1
  185. package/dist/teamControl/controlPlane.js +846 -174
  186. package/dist/teamControl/controlPlane.js.map +1 -1
  187. package/dist/teamControl/deployment.js +19 -5
  188. package/dist/teamControl/deployment.js.map +1 -1
  189. package/dist/teamControl/index.d.ts +4 -4
  190. package/dist/teamControl/index.js +2 -2
  191. package/dist/teamControl/index.js.map +1 -1
  192. package/dist/tests/testGenerator.js +15 -7
  193. package/dist/tests/testGenerator.js.map +1 -1
  194. package/dist/trust/index.d.ts +2 -2
  195. package/dist/trust/index.js +1 -1
  196. package/dist/trust/index.js.map +1 -1
  197. package/dist/trust/trustLadder.js +309 -63
  198. package/dist/trust/trustLadder.js.map +1 -1
  199. package/dist/worlds/index.d.ts +2 -2
  200. package/dist/worlds/index.js +1 -1
  201. package/dist/worlds/index.js.map +1 -1
  202. package/dist/worlds/worlds.js +364 -65
  203. package/dist/worlds/worlds.js.map +1 -1
  204. package/docs/assets/dashboard.png +0 -0
  205. package/docs/ci.md +49 -49
  206. package/docs/github-actions.md +84 -84
  207. package/docs/mcp.md +58 -52
  208. package/docs/policy.md +63 -61
  209. package/docs/providers/authoring-packs.md +180 -175
  210. package/docs/providers/stripe-core-pack.md +84 -82
  211. package/docs/releases/README.md +13 -0
  212. package/docs/security/action-gateway-threat-model.md +34 -34
  213. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
  214. package/docs/security/approval-inbox-threat-model.md +35 -35
  215. package/docs/security/credential-broker-threat-model.md +52 -52
  216. package/docs/security/egress-threat-model.md +126 -126
  217. package/docs/security/inventory-threat-model.md +104 -104
  218. package/docs/security/kill-switch-budgets-threat-model.md +37 -37
  219. package/docs/security/reliability-threat-model.md +111 -111
  220. package/docs/security/trust-ladder-threat-model.md +33 -33
  221. package/docs/telemetry.md +14 -0
  222. package/docs/usage.md +680 -571
  223. package/examples/README.md +10 -10
  224. package/examples/agent-instructions/README.md +39 -39
  225. package/examples/ci-smoke/README.md +21 -21
  226. package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
  227. package/examples/ci-smoke/package.json +9 -9
  228. package/examples/ci-smoke/production-egress.mjs +4 -2
  229. package/examples/ci-smoke/safe.mjs +21 -17
  230. package/examples/evals/README.md +24 -24
  231. package/examples/evals/retry-after.eval.json +71 -52
  232. package/examples/generic-rest/README.md +27 -27
  233. package/examples/github-fetch/README.md +38 -35
  234. package/examples/openai-streaming/README.md +16 -16
  235. package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
  236. package/examples/policy/ghostapi.policy.yaml +27 -27
  237. package/examples/record-replay/README.md +19 -19
  238. package/examples/record-replay/replay-requests.json +22 -22
  239. package/examples/record-replay/stripe-checkout.har.json +30 -30
  240. package/examples/resend-node/README.md +22 -22
  241. package/examples/stripe-node/README.md +35 -35
  242. package/examples/stripe-node/checkout-flow.mjs +87 -61
  243. package/examples/twilio-node/README.md +21 -21
  244. package/examples/worlds/README.md +19 -19
  245. package/examples/worlds/subscription-recovery.mjs +21 -12
  246. package/package.json +28 -6
  247. package/dist/landing/assets/index-BXR41fAD.css +0 -1
  248. package/dist/landing/assets/index-CxWsopzP.css +0 -1
  249. package/dist/landing/assets/index-D8_ggMRE.js +0 -55
  250. package/dist/landing/assets/index-DZlpyhqd.js +0 -55
  251. package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
  252. package/dist/landing/index.html +0 -15
  253. package/dist/landing/landing.d.ts +0 -3
  254. package/dist/landing/landing.js +0 -12
  255. package/dist/landing/landing.js.map +0 -1
  256. package/docs/commercial/README.md +0 -29
  257. package/docs/commercial/data-inventory.md +0 -36
  258. package/docs/commercial/manual-invoicing.md +0 -44
  259. package/docs/commercial/metrics.md +0 -51
  260. package/docs/commercial/pricing.md +0 -103
  261. package/docs/commercial/privacy-policy-draft.md +0 -41
  262. package/docs/commercial/terms-topics-for-counsel.md +0 -51
  263. package/docs/design-partners/README.md +0 -52
  264. package/docs/design-partners/demo-narratives.md +0 -33
  265. package/docs/design-partners/design-partner-offer.md +0 -39
  266. package/docs/design-partners/discovery-questions.md +0 -28
  267. package/docs/design-partners/feedback-capture-template.md +0 -48
  268. package/docs/design-partners/icp-one-pager.md +0 -43
  269. package/docs/design-partners/onboarding-checklist.md +0 -33
  270. package/docs/design-partners/pilot-success-criteria.md +0 -27
  271. package/docs/design-partners/pricing-interview-script.md +0 -18
  272. package/docs/design-partners/telemetry-plan.md +0 -48
  273. package/docs/development/baseline.md +0 -114
  274. package/docs/development/onboarding-smoke.md +0 -37
  275. package/docs/development/verification-0.1.8.md +0 -25
  276. package/docs/enterprise-product-roadmap-ru.md +0 -1879
  277. package/docs/fundraising/README.md +0 -87
  278. package/docs/fundraising/data-room-checklist.md +0 -72
  279. package/docs/fundraising/demo-script.md +0 -52
  280. package/docs/fundraising/design-partners-50.md +0 -74
  281. package/docs/fundraising/launch-posts.md +0 -37
  282. package/docs/fundraising/metrics-and-evidence.md +0 -30
  283. package/docs/fundraising/roadmap-12-month.md +0 -25
  284. package/docs/fundraising/technical-due-diligence-index.md +0 -37
  285. package/docs/fundraising/yc-application.md +0 -25
  286. package/docs/hosted-pilot.md +0 -111
  287. package/docs/operations/disaster-recovery-runbook.md +0 -81
  288. package/docs/operations/kill-switch-runbook.md +0 -32
  289. package/docs/release-checklist.md +0 -83
  290. package/docs/release-migration-and-rollback.md +0 -63
  291. package/docs/release-readiness.md +0 -39
  292. package/docs/team-control-plane.md +0 -72
@@ -1,18 +1,26 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, mkdir, readFile, realpath } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
4
4
  import { getDataPaths } from "../config/dataPaths.js";
5
5
  import { getProviderManifests } from "../providers/registry.js";
6
6
  import { atomicWriteJson } from "../storage/fileStore.js";
7
7
  import { sanitizeSecretString } from "../security/secrets.js";
8
- import { prepareScenarioRecording } from "../scenarios/scenarioBundle.js";
8
+ import { prepareScenarioRecording, } from "../scenarios/scenarioBundle.js";
9
9
  const MAX_INPUT_BYTES = 1024 * 1024;
10
10
  const MAX_CONTRACT_BYTES = 512 * 1024;
11
11
  const MAX_PATHS = 200;
12
12
  const MAX_OPERATIONS = 400;
13
13
  const MAX_SCHEMA_DEPTH = 20;
14
14
  const MAX_PROPERTIES = 100;
15
- const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
15
+ const HTTP_METHODS = new Set([
16
+ "GET",
17
+ "POST",
18
+ "PUT",
19
+ "PATCH",
20
+ "DELETE",
21
+ "HEAD",
22
+ "OPTIONS",
23
+ ]);
16
24
  export class ContractError extends Error {
17
25
  constructor(message) {
18
26
  super(message);
@@ -24,7 +32,8 @@ export function importOpenApiContract(input, options = {}) {
24
32
  const document = objectAt(input, "OpenAPI document must be an object.");
25
33
  assertExactKeys(document, ["openapi", "info", "paths", "servers"], "OpenAPI document");
26
34
  assertNoUnsupportedOpenApiFeatures(document, "OpenAPI document");
27
- if (typeof document.openapi !== "string" || !/^3\.0\.\d+(?:[-+].*)?$/.test(document.openapi)) {
35
+ if (typeof document.openapi !== "string" ||
36
+ !/^3\.0\.\d+(?:[-+].*)?$/.test(document.openapi)) {
28
37
  throw new ContractError("Only OpenAPI 3.0.x is supported. Convert Swagger/OpenAPI 2.x or OpenAPI 3.1 before import.");
29
38
  }
30
39
  if (document.servers !== undefined)
@@ -52,18 +61,27 @@ export function importOpenApiContract(input, options = {}) {
52
61
  }
53
62
  if (operations.length === 0)
54
63
  throw new ContractError("OpenAPI document does not contain any supported operations.");
55
- const infoTitle = document.info === undefined ? undefined : objectAt(document.info, "OpenAPI info must be an object.").title;
64
+ const infoTitle = document.info === undefined
65
+ ? undefined
66
+ : objectAt(document.info, "OpenAPI info must be an object.").title;
56
67
  return validateContract({
57
68
  schemaVersion: 1,
58
69
  kind: "ghostapi.contract",
59
- metadata: { title: normalizeTitle(options.title ?? infoTitle ?? "OpenAPI import"), source: "openapi", importedAt: options.importedAt ?? new Date().toISOString() },
70
+ metadata: {
71
+ title: normalizeTitle(options.title ?? infoTitle ?? "OpenAPI import"),
72
+ source: "openapi",
73
+ importedAt: options.importedAt ?? new Date().toISOString(),
74
+ },
60
75
  operations,
61
- providerCapabilities: currentProviderCapabilities()
76
+ providerCapabilities: currentProviderCapabilities(),
62
77
  });
63
78
  }
64
79
  export function importHarContract(input, options) {
65
80
  const bundle = prepareScenarioRecording(input, options);
66
- return { bundle, contract: contractFromScenarioBundle(bundle, options.title, options.importedAt) };
81
+ return {
82
+ bundle,
83
+ contract: contractFromScenarioBundle(bundle, options.title, options.importedAt),
84
+ };
67
85
  }
68
86
  export function contractFromScenarioBundle(bundle, title = bundle.metadata.title, importedAt = bundle.metadata.recordedAt) {
69
87
  const operations = new Map();
@@ -76,11 +94,12 @@ export function contractFromScenarioBundle(bundle, title = bundle.metadata.title
76
94
  method: interaction.request.method,
77
95
  path: pathWithoutQuery(interaction.request.path),
78
96
  request: inferSchema(interaction.request.body),
79
- responses: { [String(interaction.response.status)]: responseSchema }
97
+ responses: { [String(interaction.response.status)]: responseSchema },
80
98
  });
81
99
  continue;
82
100
  }
83
- existing.responses[String(interaction.response.status)] = mergeSchemas(existing.responses[String(interaction.response.status)], responseSchema) ?? null;
101
+ existing.responses[String(interaction.response.status)] =
102
+ mergeSchemas(existing.responses[String(interaction.response.status)], responseSchema) ?? null;
84
103
  existing.request = mergeSchemas(existing.request, inferSchema(interaction.request.body));
85
104
  }
86
105
  return validateContract({
@@ -88,7 +107,7 @@ export function contractFromScenarioBundle(bundle, title = bundle.metadata.title
88
107
  kind: "ghostapi.contract",
89
108
  metadata: { title: normalizeTitle(title), source: "har", importedAt },
90
109
  operations: [...operations.values()].sort(compareOperations),
91
- providerCapabilities: currentProviderCapabilities()
110
+ providerCapabilities: currentProviderCapabilities(),
92
111
  });
93
112
  }
94
113
  export async function importOpenApiContractFromFile(path, options = {}, projectRoot = process.cwd()) {
@@ -99,7 +118,9 @@ export async function importHarContractFromFile(path, options, projectRoot = pro
99
118
  }
100
119
  export async function writeContract(contract, outPath, projectRoot = process.cwd()) {
101
120
  const valid = validateContract(contract);
102
- const target = outPath === undefined ? join(getDataPaths().contracts, `${slugify(valid.metadata.title)}.contract.json`) : await resolveOutputPath(outPath, projectRoot);
121
+ const target = outPath === undefined
122
+ ? join(getDataPaths().contracts, `${slugify(valid.metadata.title)}.contract.json`)
123
+ : await resolveOutputPath(outPath, projectRoot);
103
124
  const serialized = JSON.stringify(valid);
104
125
  if (Buffer.byteLength(serialized, "utf8") > MAX_CONTRACT_BYTES)
105
126
  throw new ContractError(`Contract exceeds ${MAX_CONTRACT_BYTES} bytes.`);
@@ -120,9 +141,13 @@ export function validateContract(input) {
120
141
  throw new ContractError("Contract metadata source must be openapi or har.");
121
142
  if (!isTimestamp(metadata.importedAt))
122
143
  throw new ContractError("Contract metadata importedAt must be an ISO timestamp.");
123
- if (!Array.isArray(root.operations) || root.operations.length === 0 || root.operations.length > MAX_OPERATIONS)
144
+ if (!Array.isArray(root.operations) ||
145
+ root.operations.length === 0 ||
146
+ root.operations.length > MAX_OPERATIONS)
124
147
  throw new ContractError(`Contract operations must contain 1-${MAX_OPERATIONS} entries.`);
125
- const operations = root.operations.map((operation) => validateOperation(operation)).sort(compareOperations);
148
+ const operations = root.operations
149
+ .map((operation) => validateOperation(operation))
150
+ .sort(compareOperations);
126
151
  const keys = new Set();
127
152
  for (const operation of operations) {
128
153
  const key = `${operation.method} ${operation.path}`;
@@ -130,23 +155,36 @@ export function validateContract(input) {
130
155
  throw new ContractError(`Contract operation is duplicated: ${key}`);
131
156
  keys.add(key);
132
157
  }
133
- if (!Array.isArray(root.providerCapabilities) || root.providerCapabilities.length > 20)
158
+ if (!Array.isArray(root.providerCapabilities) ||
159
+ root.providerCapabilities.length > 20)
134
160
  throw new ContractError("Contract provider capabilities are invalid.");
135
- const providerCapabilities = root.providerCapabilities.map(validateCapability).sort((left, right) => left.name.localeCompare(right.name));
161
+ const providerCapabilities = root.providerCapabilities
162
+ .map(validateCapability)
163
+ .sort((left, right) => left.name.localeCompare(right.name));
136
164
  return {
137
165
  schemaVersion: 1,
138
166
  kind: "ghostapi.contract",
139
- metadata: { title: normalizeTitle(metadata.title), source: metadata.source, importedAt: metadata.importedAt },
167
+ metadata: {
168
+ title: normalizeTitle(metadata.title),
169
+ source: metadata.source,
170
+ importedAt: metadata.importedAt,
171
+ },
140
172
  operations,
141
- providerCapabilities
173
+ providerCapabilities,
142
174
  };
143
175
  }
144
176
  export function diffContracts(baselineInput, candidateInput) {
145
177
  const baseline = validateContract(baselineInput);
146
178
  const candidate = validateContract(candidateInput);
147
179
  const findings = [];
148
- const baselineOperations = new Map(baseline.operations.map((operation) => [operationKey(operation), operation]));
149
- const candidateOperations = new Map(candidate.operations.map((operation) => [operationKey(operation), operation]));
180
+ const baselineOperations = new Map(baseline.operations.map((operation) => [
181
+ operationKey(operation),
182
+ operation,
183
+ ]));
184
+ const candidateOperations = new Map(candidate.operations.map((operation) => [
185
+ operationKey(operation),
186
+ operation,
187
+ ]));
150
188
  for (const key of [...baselineOperations.keys()].sort()) {
151
189
  if (!candidateOperations.has(key))
152
190
  findings.push(finding("endpoint-removed", key, "breaking", `Endpoint removed: ${key}. Existing callers can no longer rely on this operation.`));
@@ -155,17 +193,25 @@ export function diffContracts(baselineInput, candidateInput) {
155
193
  if (!baselineOperations.has(key))
156
194
  findings.push(finding("endpoint-added", key, "non-breaking", `Endpoint added: ${key}.`));
157
195
  }
158
- for (const key of [...baselineOperations.keys()].filter((entry) => candidateOperations.has(entry)).sort()) {
196
+ for (const key of [...baselineOperations.keys()]
197
+ .filter((entry) => candidateOperations.has(entry))
198
+ .sort()) {
159
199
  const before = baselineOperations.get(key);
160
200
  const after = candidateOperations.get(key);
161
201
  diffSchema(before.request, after.request, `${key} request`, "request", findings);
162
202
  const beforeStatuses = new Set(Object.keys(before.responses));
163
203
  const afterStatuses = new Set(Object.keys(after.responses));
164
- for (const status of [...beforeStatuses].filter((value) => !afterStatuses.has(value)).sort())
204
+ for (const status of [...beforeStatuses]
205
+ .filter((value) => !afterStatuses.has(value))
206
+ .sort())
165
207
  findings.push(finding("response-status-removed", `${key} ${status}`, "breaking", `Response status ${status} removed from ${key}.`));
166
- for (const status of [...afterStatuses].filter((value) => !beforeStatuses.has(value)).sort())
208
+ for (const status of [...afterStatuses]
209
+ .filter((value) => !beforeStatuses.has(value))
210
+ .sort())
167
211
  findings.push(finding("response-status-added", `${key} ${status}`, "uncertain", `Response status ${status} added to ${key}; client handling is unknown.`));
168
- for (const status of [...beforeStatuses].filter((value) => afterStatuses.has(value)).sort())
212
+ for (const status of [...beforeStatuses]
213
+ .filter((value) => afterStatuses.has(value))
214
+ .sort())
169
215
  diffSchema(before.responses[status] ?? undefined, after.responses[status] ?? undefined, `${key} response ${status}`, "response", findings);
170
216
  }
171
217
  diffProviderCapabilities(baseline.providerCapabilities, candidate.providerCapabilities, findings);
@@ -178,9 +224,11 @@ export function diffContracts(baselineInput, candidateInput) {
178
224
  findings: sorted,
179
225
  summary: {
180
226
  breaking: sorted.filter((entry) => entry.severity === "breaking").length,
181
- nonBreaking: sorted.filter((entry) => entry.severity === "non-breaking").length,
182
- uncertain: sorted.filter((entry) => entry.severity === "uncertain").length
183
- }
227
+ nonBreaking: sorted.filter((entry) => entry.severity === "non-breaking")
228
+ .length,
229
+ uncertain: sorted.filter((entry) => entry.severity === "uncertain")
230
+ .length,
231
+ },
184
232
  };
185
233
  }
186
234
  export function formatContractDiff(diff) {
@@ -189,18 +237,29 @@ export function formatContractDiff(diff) {
189
237
  `Baseline: ${diff.baselineHash}`,
190
238
  `Candidate: ${diff.candidateHash}`,
191
239
  `Findings: ${diff.summary.breaking} breaking, ${diff.summary.nonBreaking} non-breaking, ${diff.summary.uncertain} uncertain`,
192
- ...diff.findings.map((entry) => ` ${entry.severity.toUpperCase()} ${entry.message}`)
240
+ ...diff.findings.map((entry) => ` ${entry.severity.toUpperCase()} ${entry.message}`),
193
241
  ].join("\n");
194
242
  }
195
243
  export function contractHash(contract) {
196
- return createHash("sha256").update(stableStringify(validateContract(contract)), "utf8").digest("hex");
244
+ return createHash("sha256")
245
+ .update(stableStringify(validateContract(contract)), "utf8")
246
+ .digest("hex");
197
247
  }
198
248
  function parseOpenApiOperation(path, method, value) {
199
249
  const operation = objectAt(value, `OpenAPI ${method} ${path} must be an object.`);
200
250
  assertNoUnsupportedOpenApiFeatures(operation, `OpenAPI ${method} ${path}`);
201
- const allowed = ["summary", "description", "operationId", "tags", "requestBody", "responses"];
251
+ const allowed = [
252
+ "summary",
253
+ "description",
254
+ "operationId",
255
+ "tags",
256
+ "requestBody",
257
+ "responses",
258
+ ];
202
259
  assertExactKeys(operation, allowed, `OpenAPI ${method} ${path}`);
203
- const request = operation.requestBody === undefined ? undefined : parseRequestBody(operation.requestBody, `OpenAPI ${method} ${path} requestBody`);
260
+ const request = operation.requestBody === undefined
261
+ ? undefined
262
+ : parseRequestBody(operation.requestBody, `OpenAPI ${method} ${path} requestBody`);
204
263
  const responsesRecord = objectAt(operation.responses, `OpenAPI ${method} ${path} must declare responses.`);
205
264
  const responseEntries = Object.entries(responsesRecord);
206
265
  if (responseEntries.length === 0 || responseEntries.length > 20)
@@ -211,7 +270,12 @@ function parseOpenApiOperation(path, method, value) {
211
270
  throw new ContractError(`OpenAPI ${method} ${path} has unsupported response status: ${status}.`);
212
271
  responses[status] = parseResponse(response, `OpenAPI ${method} ${path} response ${status}`);
213
272
  }
214
- return { method, path, ...(request === undefined ? {} : { request }), responses };
273
+ return {
274
+ method,
275
+ path,
276
+ ...(request === undefined ? {} : { request }),
277
+ responses,
278
+ };
215
279
  }
216
280
  function parseRequestBody(value, label) {
217
281
  const body = objectAt(value, `${label} must be an object.`);
@@ -225,39 +289,73 @@ function parseResponse(value, label) {
225
289
  assertExactKeys(response, ["description", "content", "headers"], label);
226
290
  if (response.headers !== undefined)
227
291
  throw new ContractError(`${label} headers are unsupported; import JSON response schemas only.`);
228
- return response.content === undefined ? null : parseContent(response.content, label) ?? null;
292
+ return response.content === undefined
293
+ ? null
294
+ : (parseContent(response.content, label) ?? null);
229
295
  }
230
296
  function parseContent(value, label) {
231
297
  const content = objectAt(value, `${label} content must be an object.`);
232
298
  const entries = Object.entries(content);
233
- if (entries.length !== 1 || entries[0][0].toLowerCase() !== "application/json")
299
+ if (entries.length !== 1 ||
300
+ entries[0][0].toLowerCase() !== "application/json")
234
301
  throw new ContractError(`${label} supports exactly one application/json content entry.`);
235
302
  const media = objectAt(entries[0][1], `${label} application/json media type must be an object.`);
236
303
  assertNoUnsupportedOpenApiFeatures(media, `${label} application/json media type`);
237
304
  assertExactKeys(media, ["schema"], `${label} application/json media type`);
238
- return media.schema === undefined ? undefined : parseOpenApiSchema(media.schema, `${label} schema`);
305
+ return media.schema === undefined
306
+ ? undefined
307
+ : parseOpenApiSchema(media.schema, `${label} schema`);
239
308
  }
240
309
  function parseOpenApiSchema(value, label, depth = 0) {
241
310
  if (depth > MAX_SCHEMA_DEPTH)
242
311
  throw new ContractError(`${label} exceeds schema recursion depth ${MAX_SCHEMA_DEPTH}.`);
243
312
  const schema = objectAt(value, `${label} must be an object.`);
244
313
  assertNoUnsupportedOpenApiFeatures(schema, label);
245
- assertExactKeys(schema, ["type", "enum", "required", "properties", "items", "nullable", "description", "format", "title", "example", "default", "minLength", "maxLength", "minimum", "maximum", "minItems", "maxItems"], label);
314
+ assertExactKeys(schema, [
315
+ "type",
316
+ "enum",
317
+ "required",
318
+ "properties",
319
+ "items",
320
+ "nullable",
321
+ "description",
322
+ "format",
323
+ "title",
324
+ "example",
325
+ "default",
326
+ "minLength",
327
+ "maxLength",
328
+ "minimum",
329
+ "maximum",
330
+ "minItems",
331
+ "maxItems",
332
+ ], label);
246
333
  if (schema.nullable === true)
247
334
  throw new ContractError(`${label} nullable is unsupported; use an explicit oneOf outside this supported subset.`);
248
335
  const result = {};
249
336
  if (schema.type !== undefined) {
250
- if (schema.type !== "string" && schema.type !== "number" && schema.type !== "integer" && schema.type !== "boolean" && schema.type !== "array" && schema.type !== "object")
337
+ if (schema.type !== "string" &&
338
+ schema.type !== "number" &&
339
+ schema.type !== "integer" &&
340
+ schema.type !== "boolean" &&
341
+ schema.type !== "array" &&
342
+ schema.type !== "object")
251
343
  throw new ContractError(`${label} has unsupported schema type.`);
252
344
  result.type = schema.type;
253
345
  }
254
346
  if (schema.enum !== undefined) {
255
- if (!Array.isArray(schema.enum) || schema.enum.length === 0 || schema.enum.length > 100 || schema.enum.some((entry) => !["string", "number", "boolean"].includes(typeof entry) && entry !== null))
347
+ if (!Array.isArray(schema.enum) ||
348
+ schema.enum.length === 0 ||
349
+ schema.enum.length > 100 ||
350
+ schema.enum.some((entry) => !["string", "number", "boolean"].includes(typeof entry) &&
351
+ entry !== null))
256
352
  throw new ContractError(`${label} enum must contain 1-100 scalar JSON values.`);
257
353
  result.enum = [...schema.enum];
258
354
  }
259
355
  if (schema.required !== undefined) {
260
- if (!Array.isArray(schema.required) || schema.required.some((entry) => typeof entry !== "string" || !isFieldName(entry)) || new Set(schema.required).size !== schema.required.length)
356
+ if (!Array.isArray(schema.required) ||
357
+ schema.required.some((entry) => typeof entry !== "string" || !isFieldName(entry)) ||
358
+ new Set(schema.required).size !== schema.required.length)
261
359
  throw new ContractError(`${label} required must be unique field names.`);
262
360
  result.required = [...schema.required].sort();
263
361
  }
@@ -266,9 +364,15 @@ function parseOpenApiSchema(value, label, depth = 0) {
266
364
  throw new ContractError(`${label} properties requires type object.`);
267
365
  const properties = objectAt(schema.properties, `${label} properties must be an object.`);
268
366
  const entries = Object.entries(properties);
269
- if (entries.length > MAX_PROPERTIES || entries.some(([name]) => !isFieldName(name)))
367
+ if (entries.length > MAX_PROPERTIES ||
368
+ entries.some(([name]) => !isFieldName(name)))
270
369
  throw new ContractError(`${label} properties exceed supported limits.`);
271
- result.properties = Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)).map(([name, child]) => [name, parseOpenApiSchema(child, `${label}.properties.${name}`, depth + 1)]));
370
+ result.properties = Object.fromEntries(entries
371
+ .sort(([left], [right]) => left.localeCompare(right))
372
+ .map(([name, child]) => [
373
+ name,
374
+ parseOpenApiSchema(child, `${label}.properties.${name}`, depth + 1),
375
+ ]));
272
376
  }
273
377
  if (schema.items !== undefined) {
274
378
  if (result.type !== "array")
@@ -282,7 +386,21 @@ function parseOpenApiSchema(value, label, depth = 0) {
282
386
  return result;
283
387
  }
284
388
  function assertNoUnsupportedOpenApiFeatures(value, label) {
285
- for (const key of ["$ref", "$dynamicRef", "$recursiveRef", "allOf", "anyOf", "oneOf", "not", "discriminator", "callbacks", "links", "security", "externalDocs", "webhooks"]) {
389
+ for (const key of [
390
+ "$ref",
391
+ "$dynamicRef",
392
+ "$recursiveRef",
393
+ "allOf",
394
+ "anyOf",
395
+ "oneOf",
396
+ "not",
397
+ "discriminator",
398
+ "callbacks",
399
+ "links",
400
+ "security",
401
+ "externalDocs",
402
+ "webhooks",
403
+ ]) {
286
404
  if (value[key] !== undefined)
287
405
  throw new ContractError(`${label} uses unsupported OpenAPI feature ${key}. External or recursive references are never fetched or resolved.`);
288
406
  }
@@ -293,7 +411,11 @@ function inferSchema(value, depth = 0) {
293
411
  if (value === null)
294
412
  return { type: "null" };
295
413
  if (Array.isArray(value)) {
296
- const items = value.length === 0 ? undefined : value.slice(1).reduce((current, entry) => mergeSchemas(current, inferSchema(entry, depth + 1)) ?? {}, inferSchema(value[0], depth + 1));
414
+ const items = value.length === 0
415
+ ? undefined
416
+ : value
417
+ .slice(1)
418
+ .reduce((current, entry) => mergeSchemas(current, inferSchema(entry, depth + 1)) ?? {}, inferSchema(value[0], depth + 1));
297
419
  return { type: "array", ...(items === undefined ? {} : { items }) };
298
420
  }
299
421
  if (typeof value === "string")
@@ -305,7 +427,13 @@ function inferSchema(value, depth = 0) {
305
427
  const entries = Object.entries(objectAt(value, "HAR body must be a JSON value."));
306
428
  if (entries.length > MAX_PROPERTIES)
307
429
  throw new ContractError(`HAR body object exceeds ${MAX_PROPERTIES} properties.`);
308
- return { type: "object", required: entries.map(([key]) => key).sort(), properties: Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, inferSchema(entry, depth + 1)])) };
430
+ return {
431
+ type: "object",
432
+ required: entries.map(([key]) => key).sort(),
433
+ properties: Object.fromEntries(entries
434
+ .sort(([left], [right]) => left.localeCompare(right))
435
+ .map(([key, entry]) => [key, inferSchema(entry, depth + 1)])),
436
+ };
309
437
  }
310
438
  function mergeSchemas(left, right) {
311
439
  if (left === undefined || left === null)
@@ -314,7 +442,9 @@ function mergeSchemas(left, right) {
314
442
  return left;
315
443
  if (left.type !== right.type)
316
444
  return {};
317
- const result = { ...(left.type === undefined ? {} : { type: left.type }) };
445
+ const result = {
446
+ ...(left.type === undefined ? {} : { type: left.type }),
447
+ };
318
448
  if (left.enum !== undefined && right.enum !== undefined) {
319
449
  const intersection = left.enum.filter((entry) => right.enum.some((candidate) => stableStringify(candidate) === stableStringify(entry)));
320
450
  if (intersection.length > 0)
@@ -323,16 +453,27 @@ function mergeSchemas(left, right) {
323
453
  if (left.type === "object" && right.type === "object") {
324
454
  const leftProperties = left.properties ?? {};
325
455
  const rightProperties = right.properties ?? {};
326
- const common = Object.keys(leftProperties).filter((key) => Object.hasOwn(rightProperties, key)).sort();
327
- result.properties = Object.fromEntries(common.map((key) => [key, mergeSchemas(leftProperties[key], rightProperties[key]) ?? {}]));
328
- result.required = (left.required ?? []).filter((key) => (right.required ?? []).includes(key) && Object.hasOwn(result.properties ?? {}, key)).sort();
456
+ const common = Object.keys(leftProperties)
457
+ .filter((key) => Object.hasOwn(rightProperties, key))
458
+ .sort();
459
+ result.properties = Object.fromEntries(common.map((key) => [
460
+ key,
461
+ mergeSchemas(leftProperties[key], rightProperties[key]) ?? {},
462
+ ]));
463
+ result.required = (left.required ?? [])
464
+ .filter((key) => (right.required ?? []).includes(key) &&
465
+ Object.hasOwn(result.properties ?? {}, key))
466
+ .sort();
329
467
  }
330
468
  if (left.type === "array")
331
469
  result.items = mergeSchemas(left.items, right.items);
332
470
  return result;
333
471
  }
334
472
  function diffSchema(before, after, location, context, findings) {
335
- if (before === undefined || before === null || after === undefined || after === null) {
473
+ if (before === undefined ||
474
+ before === null ||
475
+ after === undefined ||
476
+ after === null) {
336
477
  if (before !== after)
337
478
  findings.push(finding("schema-presence-changed", location, "uncertain", `${location} schema presence changed; compatibility cannot be proven.`));
338
479
  return;
@@ -344,19 +485,29 @@ function diffSchema(before, after, location, context, findings) {
344
485
  diffEnum(before.enum, after.enum, location, context, findings);
345
486
  const beforeRequired = new Set(before.required ?? []);
346
487
  const afterRequired = new Set(after.required ?? []);
347
- for (const field of [...afterRequired].filter((field) => !beforeRequired.has(field)).sort()) {
488
+ for (const field of [...afterRequired]
489
+ .filter((field) => !beforeRequired.has(field))
490
+ .sort()) {
348
491
  findings.push(finding("required-field-added", `${location}.${field}`, context === "request" ? "breaking" : "uncertain", `${location} required field added: ${field}.${context === "response" ? " Client response tolerance is unknown." : ""}`));
349
492
  }
350
- for (const field of [...beforeRequired].filter((field) => !afterRequired.has(field)).sort()) {
493
+ for (const field of [...beforeRequired]
494
+ .filter((field) => !afterRequired.has(field))
495
+ .sort()) {
351
496
  findings.push(finding("required-field-removed", `${location}.${field}`, context === "response" ? "breaking" : "non-breaking", `${location} required field removed: ${field}.${context === "response" ? " Existing clients may depend on it." : ""}`));
352
497
  }
353
498
  const beforeProperties = before.properties ?? {};
354
499
  const afterProperties = after.properties ?? {};
355
- for (const field of Object.keys(beforeProperties).filter((field) => !Object.hasOwn(afterProperties, field)).sort())
500
+ for (const field of Object.keys(beforeProperties)
501
+ .filter((field) => !Object.hasOwn(afterProperties, field))
502
+ .sort())
356
503
  findings.push(finding("property-removed", `${location}.${field}`, context === "response" ? "breaking" : "uncertain", `${location} property removed: ${field}.${context === "request" ? " Acceptance of this field is not modeled by the supported subset." : ""}`));
357
- for (const field of Object.keys(afterProperties).filter((field) => !Object.hasOwn(beforeProperties, field)).sort())
504
+ for (const field of Object.keys(afterProperties)
505
+ .filter((field) => !Object.hasOwn(beforeProperties, field))
506
+ .sort())
358
507
  findings.push(finding("property-added", `${location}.${field}`, context === "request" ? "non-breaking" : "uncertain", `${location} property added: ${field}.${context === "response" ? " Client response tolerance is unknown." : ""}`));
359
- for (const field of Object.keys(beforeProperties).filter((field) => Object.hasOwn(afterProperties, field)).sort())
508
+ for (const field of Object.keys(beforeProperties)
509
+ .filter((field) => Object.hasOwn(afterProperties, field))
510
+ .sort())
360
511
  diffSchema(beforeProperties[field], afterProperties[field], `${location}.${field}`, context, findings);
361
512
  if (before.items !== undefined || after.items !== undefined)
362
513
  diffSchema(before.items, after.items, `${location}[]`, context, findings);
@@ -369,9 +520,13 @@ function diffEnum(before, after, location, context, findings) {
369
520
  }
370
521
  const beforeValues = new Set(before.map(stableStringify));
371
522
  const afterValues = new Set(after.map(stableStringify));
372
- for (const value of [...beforeValues].filter((value) => !afterValues.has(value)).sort())
523
+ for (const value of [...beforeValues]
524
+ .filter((value) => !afterValues.has(value))
525
+ .sort())
373
526
  findings.push(finding("enum-value-removed", `${location}.${value}`, "breaking", `${location} enum value removed: ${value}.`));
374
- for (const value of [...afterValues].filter((value) => !beforeValues.has(value)).sort())
527
+ for (const value of [...afterValues]
528
+ .filter((value) => !beforeValues.has(value))
529
+ .sort())
375
530
  findings.push(finding("enum-value-added", `${location}.${value}`, context === "request" ? "non-breaking" : "uncertain", `${location} enum value added: ${value}.${context === "response" ? " Client handling is unknown." : ""}`));
376
531
  }
377
532
  function diffProviderCapabilities(before, after, findings) {
@@ -385,12 +540,19 @@ function diffProviderCapabilities(before, after, findings) {
385
540
  if (!baseline.has(name))
386
541
  findings.push(finding("provider-pack-added", name, "non-breaking", `Provider pack capability snapshot added: ${name}.`));
387
542
  }
388
- for (const name of [...baseline.keys()].filter((name) => candidate.has(name)).sort()) {
543
+ for (const name of [...baseline.keys()]
544
+ .filter((name) => candidate.has(name))
545
+ .sort()) {
389
546
  const left = baseline.get(name);
390
547
  const right = candidate.get(name);
391
548
  if (left.packVersion !== right.packVersion)
392
549
  findings.push(finding("provider-pack-version-changed", name, "uncertain", `Provider pack ${name} version changed from ${left.packVersion ?? "legacy"} to ${right.packVersion ?? "legacy"}; semantic compatibility is not inferred from version text.`));
393
- for (const capability of [...new Set([...Object.keys(left.capabilities), ...Object.keys(right.capabilities)])].sort()) {
550
+ for (const capability of [
551
+ ...new Set([
552
+ ...Object.keys(left.capabilities),
553
+ ...Object.keys(right.capabilities),
554
+ ]),
555
+ ].sort()) {
394
556
  const previous = left.capabilities[capability] ?? false;
395
557
  const current = right.capabilities[capability] ?? false;
396
558
  if (previous === current)
@@ -402,7 +564,9 @@ function diffProviderCapabilities(before, after, findings) {
402
564
  function validateOperation(value) {
403
565
  const operation = objectAt(value, "Contract operation is invalid.");
404
566
  assertExactKeys(operation, ["method", "path", "request", "responses"], "Contract operation");
405
- const method = typeof operation.method === "string" && HTTP_METHODS.has(operation.method) ? operation.method : null;
567
+ const method = typeof operation.method === "string" && HTTP_METHODS.has(operation.method)
568
+ ? operation.method
569
+ : null;
406
570
  if (method === null)
407
571
  throw new ContractError("Contract operation method is invalid.");
408
572
  validateOpenApiPath(operation.path);
@@ -413,12 +577,23 @@ function validateOperation(value) {
413
577
  return {
414
578
  method,
415
579
  path: operation.path,
416
- ...(operation.request === undefined ? {} : { request: validateSchema(operation.request, "Contract request schema") }),
417
- responses: Object.fromEntries(responseEntries.sort(([left], [right]) => left.localeCompare(right)).map(([status, schema]) => {
580
+ ...(operation.request === undefined
581
+ ? {}
582
+ : {
583
+ request: validateSchema(operation.request, "Contract request schema"),
584
+ }),
585
+ responses: Object.fromEntries(responseEntries
586
+ .sort(([left], [right]) => left.localeCompare(right))
587
+ .map(([status, schema]) => {
418
588
  if (!/^(?:[1-5][0-9]{2}|default)$/.test(status))
419
589
  throw new ContractError("Contract response status is invalid.");
420
- return [status, schema === null ? null : validateSchema(schema, `Contract response ${status} schema`)];
421
- }))
590
+ return [
591
+ status,
592
+ schema === null
593
+ ? null
594
+ : validateSchema(schema, `Contract response ${status} schema`),
595
+ ];
596
+ })),
422
597
  };
423
598
  }
424
599
  function validateSchema(value, label, depth = 0) {
@@ -428,17 +603,31 @@ function validateSchema(value, label, depth = 0) {
428
603
  assertExactKeys(schema, ["type", "enum", "required", "properties", "items"], label);
429
604
  const result = {};
430
605
  if (schema.type !== undefined) {
431
- if (!["string", "number", "integer", "boolean", "array", "object", "null"].includes(schema.type))
606
+ if (![
607
+ "string",
608
+ "number",
609
+ "integer",
610
+ "boolean",
611
+ "array",
612
+ "object",
613
+ "null",
614
+ ].includes(schema.type))
432
615
  throw new ContractError(`${label} type is invalid.`);
433
616
  result.type = schema.type;
434
617
  }
435
618
  if (schema.enum !== undefined) {
436
- if (!Array.isArray(schema.enum) || schema.enum.length === 0 || schema.enum.length > 100 || schema.enum.some((entry) => !["string", "number", "boolean"].includes(typeof entry) && entry !== null))
619
+ if (!Array.isArray(schema.enum) ||
620
+ schema.enum.length === 0 ||
621
+ schema.enum.length > 100 ||
622
+ schema.enum.some((entry) => !["string", "number", "boolean"].includes(typeof entry) &&
623
+ entry !== null))
437
624
  throw new ContractError(`${label} enum is invalid.`);
438
625
  result.enum = [...schema.enum];
439
626
  }
440
627
  if (schema.required !== undefined) {
441
- if (!Array.isArray(schema.required) || schema.required.some((entry) => typeof entry !== "string" || !isFieldName(entry)) || new Set(schema.required).size !== schema.required.length)
628
+ if (!Array.isArray(schema.required) ||
629
+ schema.required.some((entry) => typeof entry !== "string" || !isFieldName(entry)) ||
630
+ new Set(schema.required).size !== schema.required.length)
442
631
  throw new ContractError(`${label} required is invalid.`);
443
632
  result.required = [...schema.required].sort();
444
633
  }
@@ -447,9 +636,15 @@ function validateSchema(value, label, depth = 0) {
447
636
  throw new ContractError(`${label} properties requires type object.`);
448
637
  const properties = objectAt(schema.properties, `${label} properties are invalid.`);
449
638
  const entries = Object.entries(properties);
450
- if (entries.length > MAX_PROPERTIES || entries.some(([key]) => !isFieldName(key)))
639
+ if (entries.length > MAX_PROPERTIES ||
640
+ entries.some(([key]) => !isFieldName(key)))
451
641
  throw new ContractError(`${label} properties are invalid.`);
452
- result.properties = Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, validateSchema(entry, `${label}.${key}`, depth + 1)]));
642
+ result.properties = Object.fromEntries(entries
643
+ .sort(([left], [right]) => left.localeCompare(right))
644
+ .map(([key, entry]) => [
645
+ key,
646
+ validateSchema(entry, `${label}.${key}`, depth + 1),
647
+ ]));
453
648
  }
454
649
  if (schema.items !== undefined) {
455
650
  if (result.type !== "array")
@@ -465,20 +660,37 @@ function validateSchema(value, label, depth = 0) {
465
660
  function validateCapability(value) {
466
661
  const capability = objectAt(value, "Contract provider capability is invalid.");
467
662
  assertExactKeys(capability, ["name", "packVersion", "capabilities"], "Contract provider capability");
468
- if (typeof capability.name !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(capability.name))
663
+ if (typeof capability.name !== "string" ||
664
+ !/^[a-z][a-z0-9-]{0,63}$/.test(capability.name))
469
665
  throw new ContractError("Contract provider capability name is invalid.");
470
- if (capability.packVersion !== null && typeof capability.packVersion !== "string")
666
+ if (capability.packVersion !== null &&
667
+ typeof capability.packVersion !== "string")
471
668
  throw new ContractError("Contract provider pack version is invalid.");
472
669
  const flags = objectAt(capability.capabilities, "Contract provider capabilities are invalid.");
473
- if (Object.keys(flags).length > 30 || Object.values(flags).some((value) => typeof value !== "boolean"))
670
+ if (Object.keys(flags).length > 30 ||
671
+ Object.values(flags).some((value) => typeof value !== "boolean"))
474
672
  throw new ContractError("Contract provider capabilities are invalid.");
475
- return { name: capability.name, packVersion: capability.packVersion, capabilities: Object.fromEntries(Object.entries(flags).sort(([left], [right]) => left.localeCompare(right))) };
673
+ return {
674
+ name: capability.name,
675
+ packVersion: capability.packVersion,
676
+ capabilities: Object.fromEntries(Object.entries(flags).sort(([left], [right]) => left.localeCompare(right))),
677
+ };
476
678
  }
477
679
  function currentProviderCapabilities() {
478
- return getProviderManifests().map((manifest) => ({ name: manifest.name, packVersion: manifest.packVersion, capabilities: Object.fromEntries(Object.entries(manifest.capabilities).sort(([left], [right]) => left.localeCompare(right))) })).sort((left, right) => left.name.localeCompare(right.name));
680
+ return getProviderManifests()
681
+ .map((manifest) => ({
682
+ name: manifest.name,
683
+ packVersion: manifest.packVersion,
684
+ capabilities: Object.fromEntries(Object.entries(manifest.capabilities).sort(([left], [right]) => left.localeCompare(right))),
685
+ }))
686
+ .sort((left, right) => left.name.localeCompare(right.name));
479
687
  }
480
688
  function validateOpenApiPath(value) {
481
- if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.length > 1_000 || /[\r\n?#{}]/.test(value))
689
+ if (typeof value !== "string" ||
690
+ !value.startsWith("/") ||
691
+ value.startsWith("//") ||
692
+ value.length > 1_000 ||
693
+ /[\r\n?#{}]/.test(value))
482
694
  throw new ContractError("OpenAPI path must be a bounded relative path without query, fragment, path parameters, or line breaks.");
483
695
  }
484
696
  function pathWithoutQuery(path) {
@@ -494,7 +706,9 @@ function finding(kind, subject, severity, message) {
494
706
  return { id: `${kind}:${subject}`, severity, message };
495
707
  }
496
708
  function normalizeTitle(value) {
497
- if (typeof value !== "string" || value.trim() === "" || value.trim().length > 120)
709
+ if (typeof value !== "string" ||
710
+ value.trim() === "" ||
711
+ value.trim().length > 120)
498
712
  throw new ContractError("Contract title must be 1-120 characters.");
499
713
  const title = value.trim();
500
714
  if (sanitizeSecretString(title) !== title)
@@ -505,7 +719,9 @@ function isFieldName(value) {
505
719
  return value.length > 0 && value.length <= 128 && !/[\r\n]/.test(value);
506
720
  }
507
721
  function isTimestamp(value) {
508
- return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value) && Number.isFinite(Date.parse(value));
722
+ return (typeof value === "string" &&
723
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value) &&
724
+ Number.isFinite(Date.parse(value)));
509
725
  }
510
726
  function objectAt(value, message) {
511
727
  if (value === null || typeof value !== "object" || Array.isArray(value))
@@ -543,7 +759,8 @@ async function readBoundedJson(path, projectRoot, label, limit = MAX_INPUT_BYTES
543
759
  }
544
760
  }
545
761
  function isArchive(source) {
546
- return (source[0] === 0x50 && source[1] === 0x4b) || (source[0] === 0x1f && source[1] === 0x8b);
762
+ return ((source[0] === 0x50 && source[1] === 0x4b) ||
763
+ (source[0] === 0x1f && source[1] === 0x8b));
547
764
  }
548
765
  async function resolveExistingPath(path, projectRoot, label) {
549
766
  const target = isAbsolute(path) ? resolve(path) : resolve(projectRoot, path);
@@ -551,7 +768,7 @@ async function resolveExistingPath(path, projectRoot, label) {
551
768
  const info = await lstat(target);
552
769
  if (!info.isFile() || info.isSymbolicLink())
553
770
  throw new ContractError(`${label} must be a regular non-symlink file.`);
554
- if (!await isRealPathInsideAllowedRoots(target, projectRoot))
771
+ if (!(await isRealPathInsideAllowedRoots(target, projectRoot)))
555
772
  throw new ContractError(`${label} resolves outside the project root or GHOSTAPI_DATA_DIR through a symlink.`);
556
773
  return target;
557
774
  }
@@ -562,7 +779,7 @@ async function resolveOutputPath(path, projectRoot) {
562
779
  const parent = await lstat(dirname(target));
563
780
  if (!parent.isDirectory() || parent.isSymbolicLink())
564
781
  throw new ContractError("Contract output parent must be a real directory, not a symlink.");
565
- if (!await isRealPathInsideAllowedRoots(dirname(target), projectRoot))
782
+ if (!(await isRealPathInsideAllowedRoots(dirname(target), projectRoot)))
566
783
  throw new ContractError("Contract output parent resolves outside the project root or GHOSTAPI_DATA_DIR through a symlink.");
567
784
  const existing = await lstat(target).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
568
785
  if (existing?.isSymbolicLink())
@@ -577,16 +794,23 @@ function assertPathInsideAllowedRoots(projectRoot, target, label) {
577
794
  }
578
795
  function isInside(root, target) {
579
796
  const relativePath = relative(resolve(root), target);
580
- return relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
797
+ return (relativePath !== ".." &&
798
+ !relativePath.startsWith(`..${sep}`) &&
799
+ !isAbsolute(relativePath));
581
800
  }
582
801
  async function isRealPathInsideAllowedRoots(target, projectRoot) {
583
802
  const realTarget = await realpath(target);
584
803
  const realProjectRoot = await realpath(projectRoot);
585
804
  const dataRoot = await realpath(getDataPaths().root).catch(() => null);
586
- return isInside(realProjectRoot, realTarget) || (dataRoot !== null && isInside(dataRoot, realTarget));
805
+ return (isInside(realProjectRoot, realTarget) ||
806
+ (dataRoot !== null && isInside(dataRoot, realTarget)));
587
807
  }
588
808
  function slugify(value) {
589
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 80) || "contract";
809
+ return (value
810
+ .toLowerCase()
811
+ .replace(/[^a-z0-9]+/g, "-")
812
+ .replace(/^-|-$/g, "")
813
+ .slice(0, 80) || "contract");
590
814
  }
591
815
  function stableStringify(value) {
592
816
  if (value === undefined)
@@ -595,7 +819,11 @@ function stableStringify(value) {
595
819
  return JSON.stringify(value);
596
820
  if (Array.isArray(value))
597
821
  return `[${value.map(stableStringify).join(",")}]`;
598
- return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
822
+ return `{${Object.entries(value)
823
+ .filter(([, entry]) => entry !== undefined)
824
+ .sort(([left], [right]) => left.localeCompare(right))
825
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`)
826
+ .join(",")}}`;
599
827
  }
600
828
  function isErrorCode(error, code) {
601
829
  return error instanceof Error && "code" in error && error.code === code;