shapi-cli 0.0.0-stage → 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 (61) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +49 -2
  3. package/advisories/anthropic-api-version-2026-10-21-sampling-params-400.json +35 -0
  4. package/advisories/github-workflow-dispatch-204-to-200.json +26 -0
  5. package/advisories/google-ads-api-v22-sunset.json +27 -0
  6. package/advisories/google-custom-search-json-api-shutdown.json +27 -0
  7. package/advisories/google-gemini-2-0-flash-shutdown.json +27 -0
  8. package/advisories/google-gemini-2-5-flash-image-shutdown.json +27 -0
  9. package/advisories/google-gemini-2-5-preview-tts-shutdown.json +27 -0
  10. package/advisories/hubspot-contact-lists-v1-sunset.json +28 -0
  11. package/advisories/hubspot-contacts-v1-list-memberships-removed.json +30 -0
  12. package/advisories/hubspot-marketing-emails-folderid-removed.json +39 -0
  13. package/advisories/hubspot-marketing-emails-v1-sunset.json +24 -0
  14. package/advisories/hubspot-pipelines-v1-sunset.json +44 -0
  15. package/advisories/meta-graph-commerce-order-management-removed.json +26 -0
  16. package/advisories/meta-graph-date-format-param-removed.json +30 -0
  17. package/advisories/meta-graph-delivery-estimate-fields-removed.json +30 -0
  18. package/advisories/meta-graph-instagram-oembed-fields-removed.json +33 -0
  19. package/advisories/meta-graph-marketing-api-v24-sunset.json +34 -0
  20. package/advisories/meta-graph-marketing-api-v25-sunset.json +35 -0
  21. package/advisories/meta-graph-page-insights-impressions-fans-removed.json +51 -0
  22. package/advisories/meta-graph-page-insights-reach-video-views-removed.json +57 -0
  23. package/advisories/meta-graph-root-ids-lookup-removed.json +30 -0
  24. package/advisories/meta-graph-v20-sunset.json +33 -0
  25. package/advisories/meta-graph-v21-sunset.json +33 -0
  26. package/advisories/openai-assistants-api-shutdown.json +27 -0
  27. package/advisories/openai-audio-translations-endpoint-shutdown.json +27 -0
  28. package/advisories/openai-evals-api-shutdown.json +27 -0
  29. package/advisories/quickbooks-companyinfo-neoenabled-removed.json +29 -0
  30. package/advisories/quickbooks-minorversion-below-75-ignored.json +40 -0
  31. package/advisories/quickbooks-refresh-token-five-year-hard-expiry.json +29 -0
  32. package/advisories/reddit-logged-out-json-403.json +25 -0
  33. package/advisories/salesforce-soap-login-broad-ip-range-token-required.json +30 -0
  34. package/advisories/salesforce-soap-login-disabled-when-unused.json +30 -0
  35. package/advisories/salesforce-soap-login-use-any-api-auth-required.json +31 -0
  36. package/advisories/shopify-admin-api-2026-01-unsupported.json +33 -0
  37. package/advisories/shopify-admin-api-2026-04-unsupported.json +33 -0
  38. package/advisories/shopify-global-catalog-rest-api-shutdown.json +24 -0
  39. package/advisories/shopify-non-expiring-offline-tokens-rejected.json +27 -0
  40. package/advisories/shopify-script-tags-stop-running.json +27 -0
  41. package/advisories/slack-files-upload-retired.json +28 -0
  42. package/advisories/stripe-checkout-session-ui-mode-values-renamed.json +53 -0
  43. package/advisories/twilio-conference-list-default-in-progress.json +27 -0
  44. package/advisories/twilio-notify-end-of-life.json +28 -0
  45. package/advisories/twilio-rcs-undelivered-error-code-30046.json +27 -0
  46. package/advisories/twilio-voice-insights-conference-participant-removed.json +28 -0
  47. package/advisories/x-activity-api-pascalcase-event-types-removed.json +31 -0
  48. package/advisories/x-self-serve-follow-writes-removed.json +27 -0
  49. package/advisories/x-self-serve-like-writes-removed.json +27 -0
  50. package/advisories/xero-accounting-transactions-scope-split.json +27 -0
  51. package/advisories/xero-allocations-contact-mismatch-rejected.json +27 -0
  52. package/advisories/xero-au-payroll-isqualifyingearnings-required.json +28 -0
  53. package/advisories/xero-finance-accounting-activities-decommissioned.json +28 -0
  54. package/advisories/xero-invoices-email-daily-limit-429.json +32 -0
  55. package/advisories/xero-nz-payroll-employee-tax-kiwisaverstatus-required.json +27 -0
  56. package/advisories/xero-organisation-class-ultra-enum.json +31 -0
  57. package/bin/shapi.js +4 -0
  58. package/dist/index.js +4206 -0
  59. package/dist/tree-sitter-python.wasm +0 -0
  60. package/package.json +52 -4
  61. package/recordings/github-workflow-dispatch-204-to-200.json +46 -0
package/dist/index.js ADDED
@@ -0,0 +1,4206 @@
1
+ // packages/cli/src/index.ts
2
+ import { Command as Command2, CommanderError as CommanderError2, Option } from "commander";
3
+
4
+ // packages/cli/src/fix.ts
5
+ import { execFileSync as execFileSync3 } from "child_process";
6
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync3, readdirSync as readdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
7
+ import os3 from "os";
8
+ import path12 from "path";
9
+
10
+ // packages/core/src/providers.ts
11
+ import { z } from "zod";
12
+
13
+ // packages/core/providers.json
14
+ var providers_default = [
15
+ {
16
+ id: "github",
17
+ name: "GitHub",
18
+ hosts: ["api.github.com", "uploads.github.com"],
19
+ sdkPackages: ["@octokit/rest", "@octokit/core", "octokit", "@octokit/action", "@actions/github"],
20
+ sdkMethods: {
21
+ "actions.createWorkflowDispatch": "POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches",
22
+ "actions.listWorkflowRuns": "GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs",
23
+ "actions.getWorkflowRun": "GET /repos/{owner}/{repo}/actions/runs/{run_id}",
24
+ "repos.get": "GET /repos/{owner}/{repo}",
25
+ "repos.getContent": "GET /repos/{owner}/{repo}/contents/{path}",
26
+ "repos.createDispatchEvent": "POST /repos/{owner}/{repo}/dispatches",
27
+ "pulls.create": "POST /repos/{owner}/{repo}/pulls",
28
+ "pulls.get": "GET /repos/{owner}/{repo}/pulls/{pull_number}",
29
+ "pulls.merge": "PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge",
30
+ "issues.create": "POST /repos/{owner}/{repo}/issues",
31
+ "issues.createComment": "POST /repos/{owner}/{repo}/issues/{issue_number}/comments",
32
+ "checks.create": "POST /repos/{owner}/{repo}/check-runs"
33
+ },
34
+ docsUrl: "https://docs.github.com/en/rest",
35
+ changelogUrl: "https://github.blog/changelog/"
36
+ },
37
+ {
38
+ id: "stripe",
39
+ name: "Stripe",
40
+ hosts: ["api.stripe.com", "files.stripe.com"],
41
+ sdkPackages: ["stripe"],
42
+ sdkMethods: {
43
+ "customers.create": "POST /v1/customers",
44
+ "customers.retrieve": "GET /v1/customers/{id}",
45
+ "customers.update": "POST /v1/customers/{id}",
46
+ "customers.list": "GET /v1/customers",
47
+ "paymentIntents.create": "POST /v1/payment_intents",
48
+ "paymentIntents.retrieve": "GET /v1/payment_intents/{id}",
49
+ "paymentIntents.confirm": "POST /v1/payment_intents/{id}/confirm",
50
+ "subscriptions.create": "POST /v1/subscriptions",
51
+ "subscriptions.update": "POST /v1/subscriptions/{id}",
52
+ "invoices.create": "POST /v1/invoices",
53
+ "checkout.sessions.create": "POST /v1/checkout/sessions",
54
+ "checkout.sessions.retrieve": "GET /v1/checkout/sessions/{session}",
55
+ "refunds.create": "POST /v1/refunds"
56
+ },
57
+ docsUrl: "https://docs.stripe.com/api",
58
+ changelogUrl: "https://docs.stripe.com/changelog",
59
+ python: {
60
+ packages: [
61
+ { pypi: "stripe", module: "stripe" }
62
+ ],
63
+ methods: {
64
+ "checkout.Session.create": "POST /v1/checkout/sessions",
65
+ "checkout.Session.retrieve": "GET /v1/checkout/sessions/{session}",
66
+ "PaymentIntent.create": "POST /v1/payment_intents",
67
+ "PaymentIntent.retrieve": "GET /v1/payment_intents/{intent}",
68
+ "PaymentIntent.confirm": "POST /v1/payment_intents/{intent}/confirm",
69
+ "SetupIntent.create": "POST /v1/setup_intents",
70
+ "Customer.create": "POST /v1/customers",
71
+ "Customer.list": "GET /v1/customers",
72
+ "Subscription.create": "POST /v1/subscriptions",
73
+ "Balance.retrieve": "GET /v1/balance",
74
+ "checkout.sessions.create": "POST /v1/checkout/sessions",
75
+ "checkout.sessions.retrieve": "GET /v1/checkout/sessions/{session}",
76
+ "payment_intents.create": "POST /v1/payment_intents",
77
+ "payment_intents.retrieve": "GET /v1/payment_intents/{intent}",
78
+ "customers.create": "POST /v1/customers",
79
+ "customers.list": "GET /v1/customers"
80
+ }
81
+ }
82
+ },
83
+ {
84
+ id: "twilio",
85
+ name: "Twilio",
86
+ hosts: ["api.twilio.com", "*.twilio.com"],
87
+ sdkPackages: ["twilio"],
88
+ sdkMethods: {
89
+ "messages.create": "POST /2010-04-01/Accounts/{AccountSid}/Messages.json",
90
+ "calls.create": "POST /2010-04-01/Accounts/{AccountSid}/Calls.json"
91
+ },
92
+ docsUrl: "https://www.twilio.com/docs/usage/api",
93
+ changelogUrl: "https://www.twilio.com/en-us/changelog",
94
+ python: {
95
+ packages: [
96
+ { pypi: "twilio", module: "twilio" }
97
+ ],
98
+ methods: {
99
+ "messages.create": "POST /2010-04-01/Accounts/{AccountSid}/Messages.json",
100
+ "messages.list": "GET /2010-04-01/Accounts/{AccountSid}/Messages.json",
101
+ "calls.create": "POST /2010-04-01/Accounts/{AccountSid}/Calls.json",
102
+ "conferences.list": "GET /2010-04-01/Accounts/{AccountSid}/Conferences.json"
103
+ }
104
+ }
105
+ },
106
+ {
107
+ id: "shopify",
108
+ name: "Shopify",
109
+ hosts: ["*.myshopify.com", "discover.shopifyapps.com"],
110
+ sdkPackages: ["@shopify/shopify-api", "@shopify/admin-api-client", "shopify-api-node"],
111
+ docsUrl: "https://shopify.dev/docs/api/admin",
112
+ changelogUrl: "https://shopify.dev/changelog"
113
+ },
114
+ {
115
+ id: "quickbooks",
116
+ name: "QuickBooks Online",
117
+ hosts: ["quickbooks.api.intuit.com", "sandbox-quickbooks.api.intuit.com", "oauth.platform.intuit.com"],
118
+ sdkPackages: ["node-quickbooks", "intuit-oauth"],
119
+ docsUrl: "https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account",
120
+ changelogUrl: "https://blogs.intuit.com/category/intuit-developer/"
121
+ },
122
+ {
123
+ id: "xero",
124
+ name: "Xero",
125
+ hosts: ["api.xero.com", "identity.xero.com", "login.xero.com"],
126
+ sdkPackages: ["xero-node"],
127
+ docsUrl: "https://developer.xero.com/documentation/api/accounting/overview",
128
+ changelogUrl: "https://developer.xero.com/changelog"
129
+ },
130
+ {
131
+ id: "hubspot",
132
+ name: "HubSpot",
133
+ hosts: ["api.hubapi.com", "api.hubspot.com"],
134
+ sdkPackages: ["@hubspot/api-client"],
135
+ docsUrl: "https://developers.hubspot.com/docs/api/overview",
136
+ changelogUrl: "https://developers.hubspot.com/changelog",
137
+ python: {
138
+ packages: [
139
+ { pypi: "hubspot-api-client", module: "hubspot" }
140
+ ],
141
+ methods: {
142
+ "crm.contacts.basic_api.get_page": "GET /crm/v3/objects/contacts",
143
+ "crm.contacts.basic_api.create": "POST /crm/v3/objects/contacts",
144
+ "crm.deals.basic_api.get_page": "GET /crm/v3/objects/deals",
145
+ "crm.pipelines.pipelines_api.get_all": "GET /crm/v3/pipelines/{objectType}"
146
+ }
147
+ }
148
+ },
149
+ {
150
+ id: "slack",
151
+ name: "Slack",
152
+ hosts: ["slack.com", "hooks.slack.com"],
153
+ sdkPackages: ["@slack/web-api", "@slack/bolt", "@slack/webhook"],
154
+ sdkMethods: {
155
+ "chat.postMessage": "POST /api/chat.postMessage",
156
+ "chat.update": "POST /api/chat.update",
157
+ "conversations.list": "POST /api/conversations.list",
158
+ "conversations.history": "POST /api/conversations.history",
159
+ "users.info": "POST /api/users.info",
160
+ "files.uploadV2": "POST /api/files.uploadV2"
161
+ },
162
+ docsUrl: "https://api.slack.com/methods",
163
+ changelogUrl: "https://api.slack.com/changelog",
164
+ python: {
165
+ packages: [
166
+ { pypi: "slack-sdk", module: "slack_sdk" }
167
+ ]
168
+ }
169
+ },
170
+ {
171
+ id: "openai",
172
+ name: "OpenAI",
173
+ hosts: ["api.openai.com"],
174
+ sdkPackages: ["openai"],
175
+ sdkMethods: {
176
+ "chat.completions.create": "POST /v1/chat/completions",
177
+ "responses.create": "POST /v1/responses",
178
+ "embeddings.create": "POST /v1/embeddings",
179
+ "images.generate": "POST /v1/images/generations",
180
+ "beta.assistants.create": "POST /v1/assistants",
181
+ "beta.threads.create": "POST /v1/threads",
182
+ "beta.threads.runs.create": "POST /v1/threads/{thread_id}/runs",
183
+ "files.create": "POST /v1/files"
184
+ },
185
+ docsUrl: "https://platform.openai.com/docs/api-reference",
186
+ changelogUrl: "https://platform.openai.com/docs/changelog",
187
+ python: {
188
+ packages: [
189
+ { pypi: "openai", module: "openai" }
190
+ ],
191
+ methods: {
192
+ "chat.completions.create": "POST /v1/chat/completions",
193
+ "responses.create": "POST /v1/responses",
194
+ "models.list": "GET /v1/models",
195
+ "embeddings.create": "POST /v1/embeddings",
196
+ "audio.translations.create": "POST /v1/audio/translations",
197
+ "audio.transcriptions.create": "POST /v1/audio/transcriptions",
198
+ "images.generate": "POST /v1/images/generations",
199
+ "beta.assistants.create": "POST /v1/assistants",
200
+ "beta.assistants.list": "GET /v1/assistants",
201
+ "beta.threads.create": "POST /v1/threads",
202
+ "beta.threads.runs.create": "POST /v1/threads/{thread_id}/runs",
203
+ "evals.create": "POST /v1/evals",
204
+ "evals.list": "GET /v1/evals"
205
+ }
206
+ }
207
+ },
208
+ {
209
+ id: "anthropic",
210
+ name: "Anthropic",
211
+ hosts: ["api.anthropic.com"],
212
+ sdkPackages: ["@anthropic-ai/sdk"],
213
+ sdkMethods: {
214
+ "messages.create": "POST /v1/messages",
215
+ "messages.stream": "POST /v1/messages",
216
+ "messages.countTokens": "POST /v1/messages/count_tokens",
217
+ "models.list": "GET /v1/models"
218
+ },
219
+ docsUrl: "https://docs.claude.com/en/api",
220
+ changelogUrl: "https://docs.claude.com/en/release-notes/api",
221
+ python: {
222
+ packages: [
223
+ { pypi: "anthropic", module: "anthropic" }
224
+ ],
225
+ methods: {
226
+ "messages.create": "POST /v1/messages",
227
+ "messages.stream": "POST /v1/messages",
228
+ "messages.count_tokens": "POST /v1/messages/count_tokens",
229
+ "messages.batches.create": "POST /v1/messages/batches",
230
+ "models.list": "GET /v1/models"
231
+ }
232
+ }
233
+ },
234
+ {
235
+ id: "salesforce",
236
+ name: "Salesforce",
237
+ hosts: ["*.salesforce.com", "*.force.com"],
238
+ sdkPackages: ["jsforce", "@jsforce/jsforce-node"],
239
+ docsUrl: "https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/",
240
+ changelogUrl: "https://help.salesforce.com/s/articleView?id=release-notes.salesforce_release_notes.htm"
241
+ },
242
+ {
243
+ id: "zendesk",
244
+ name: "Zendesk",
245
+ hosts: ["*.zendesk.com"],
246
+ sdkPackages: ["node-zendesk"],
247
+ docsUrl: "https://developer.zendesk.com/api-reference/",
248
+ changelogUrl: "https://developer.zendesk.com/api-reference/changelog/changelog/"
249
+ },
250
+ {
251
+ id: "intercom",
252
+ name: "Intercom",
253
+ hosts: ["api.intercom.io", "api.eu.intercom.io", "api.au.intercom.io"],
254
+ sdkPackages: ["intercom-client"],
255
+ docsUrl: "https://developers.intercom.com/docs/references/rest-api/api.intercom.io",
256
+ changelogUrl: "https://developers.intercom.com/docs/build-an-integration/learn-more/rest-apis/api-changelog"
257
+ },
258
+ {
259
+ id: "sendgrid",
260
+ name: "SendGrid",
261
+ hosts: ["api.sendgrid.com"],
262
+ sdkPackages: ["@sendgrid/mail", "@sendgrid/client"],
263
+ sdkMethods: {
264
+ send: "POST /v3/mail/send"
265
+ },
266
+ docsUrl: "https://www.twilio.com/docs/sendgrid/api-reference"
267
+ },
268
+ {
269
+ id: "mailchimp",
270
+ name: "Mailchimp",
271
+ hosts: ["*.api.mailchimp.com"],
272
+ sdkPackages: ["@mailchimp/mailchimp_marketing", "@mailchimp/mailchimp_transactional"],
273
+ docsUrl: "https://mailchimp.com/developer/marketing/api/"
274
+ },
275
+ {
276
+ id: "plaid",
277
+ name: "Plaid",
278
+ hosts: ["production.plaid.com", "sandbox.plaid.com", "development.plaid.com"],
279
+ sdkPackages: ["plaid"],
280
+ docsUrl: "https://plaid.com/docs/api/",
281
+ changelogUrl: "https://plaid.com/docs/changelog/"
282
+ },
283
+ {
284
+ id: "square",
285
+ name: "Square",
286
+ hosts: ["connect.squareup.com", "connect.squareupsandbox.com"],
287
+ sdkPackages: ["square"],
288
+ docsUrl: "https://developer.squareup.com/reference/square",
289
+ changelogUrl: "https://developer.squareup.com/docs/changelog/connect"
290
+ },
291
+ {
292
+ id: "paypal",
293
+ name: "PayPal",
294
+ hosts: ["api-m.paypal.com", "api-m.sandbox.paypal.com", "api.paypal.com", "api.sandbox.paypal.com"],
295
+ sdkPackages: ["@paypal/paypal-server-sdk", "@paypal/checkout-server-sdk"],
296
+ docsUrl: "https://developer.paypal.com/api/rest/"
297
+ },
298
+ {
299
+ id: "google-gemini",
300
+ name: "Google Gemini API",
301
+ hosts: ["generativelanguage.googleapis.com"],
302
+ sdkPackages: ["@google/genai", "@google/generative-ai"],
303
+ docsUrl: "https://ai.google.dev/api",
304
+ changelogUrl: "https://ai.google.dev/gemini-api/docs/changelog"
305
+ },
306
+ {
307
+ id: "google",
308
+ name: "Google APIs",
309
+ hosts: ["*.googleapis.com"],
310
+ sdkPackages: ["googleapis"],
311
+ docsUrl: "https://developers.google.com/apis-explorer"
312
+ },
313
+ {
314
+ id: "microsoft-graph",
315
+ name: "Microsoft Graph",
316
+ hosts: ["graph.microsoft.com"],
317
+ sdkPackages: ["@microsoft/microsoft-graph-client", "@microsoft/msgraph-sdk"],
318
+ docsUrl: "https://learn.microsoft.com/en-us/graph/api/overview",
319
+ changelogUrl: "https://developer.microsoft.com/en-us/graph/changelog"
320
+ },
321
+ {
322
+ id: "meta-graph",
323
+ name: "Meta Graph API",
324
+ hosts: ["graph.facebook.com", "graph.instagram.com"],
325
+ sdkPackages: ["facebook-nodejs-business-sdk"],
326
+ docsUrl: "https://developers.facebook.com/docs/graph-api",
327
+ changelogUrl: "https://developers.facebook.com/docs/graph-api/changelog"
328
+ },
329
+ {
330
+ id: "x",
331
+ name: "X (Twitter)",
332
+ hosts: ["api.x.com", "api.twitter.com", "upload.twitter.com"],
333
+ sdkPackages: ["twitter-api-v2"],
334
+ docsUrl: "https://docs.x.com/x-api",
335
+ changelogUrl: "https://docs.x.com/changelog"
336
+ },
337
+ {
338
+ id: "reddit",
339
+ name: "Reddit",
340
+ hosts: ["reddit.com", "www.reddit.com", "oauth.reddit.com", "old.reddit.com"],
341
+ sdkPackages: ["snoowrap"],
342
+ docsUrl: "https://www.reddit.com/dev/api/"
343
+ },
344
+ {
345
+ id: "linkedin",
346
+ name: "LinkedIn",
347
+ hosts: ["api.linkedin.com"],
348
+ sdkPackages: ["linkedin-api-client"],
349
+ docsUrl: "https://learn.microsoft.com/en-us/linkedin/",
350
+ changelogUrl: "https://learn.microsoft.com/en-us/linkedin/marketing/integrations/recent-changes"
351
+ },
352
+ {
353
+ id: "notion",
354
+ name: "Notion",
355
+ hosts: ["api.notion.com"],
356
+ sdkPackages: ["@notionhq/client"],
357
+ docsUrl: "https://developers.notion.com/reference",
358
+ changelogUrl: "https://developers.notion.com/page/changelog"
359
+ },
360
+ {
361
+ id: "airtable",
362
+ name: "Airtable",
363
+ hosts: ["api.airtable.com"],
364
+ sdkPackages: ["airtable"],
365
+ docsUrl: "https://airtable.com/developers/web/api/introduction"
366
+ },
367
+ {
368
+ id: "atlassian",
369
+ name: "Atlassian (Jira, Confluence)",
370
+ hosts: ["*.atlassian.net", "api.atlassian.com"],
371
+ sdkPackages: ["jira.js", "jira-client", "confluence.js"],
372
+ docsUrl: "https://developer.atlassian.com/cloud/jira/platform/rest/v3/",
373
+ changelogUrl: "https://developer.atlassian.com/changelog/"
374
+ },
375
+ {
376
+ id: "linear",
377
+ name: "Linear",
378
+ hosts: ["api.linear.app"],
379
+ sdkPackages: ["@linear/sdk"],
380
+ docsUrl: "https://linear.app/developers"
381
+ },
382
+ {
383
+ id: "discord",
384
+ name: "Discord",
385
+ hosts: ["discord.com", "discordapp.com"],
386
+ sdkPackages: ["discord.js", "@discordjs/rest"],
387
+ docsUrl: "https://discord.com/developers/docs/reference",
388
+ changelogUrl: "https://discord.com/developers/docs/change-log"
389
+ },
390
+ {
391
+ id: "zoom",
392
+ name: "Zoom",
393
+ hosts: ["api.zoom.us", "zoom.us"],
394
+ sdkPackages: [],
395
+ docsUrl: "https://developers.zoom.us/docs/api/",
396
+ changelogUrl: "https://developers.zoom.us/changelog/"
397
+ },
398
+ {
399
+ id: "dropbox",
400
+ name: "Dropbox",
401
+ hosts: ["api.dropboxapi.com", "content.dropboxapi.com"],
402
+ sdkPackages: ["dropbox"],
403
+ docsUrl: "https://www.dropbox.com/developers/documentation/http/documentation"
404
+ },
405
+ {
406
+ id: "pinterest",
407
+ name: "Pinterest",
408
+ hosts: ["api.pinterest.com"],
409
+ sdkPackages: [],
410
+ docsUrl: "https://developers.pinterest.com/docs/api/v5/",
411
+ changelogUrl: "https://developers.pinterest.com/docs/getting-started/changelog/"
412
+ },
413
+ {
414
+ id: "resend",
415
+ name: "Resend",
416
+ hosts: ["api.resend.com"],
417
+ sdkPackages: ["resend"],
418
+ docsUrl: "https://resend.com/docs/api-reference/introduction"
419
+ },
420
+ {
421
+ id: "cloudflare",
422
+ name: "Cloudflare",
423
+ hosts: ["api.cloudflare.com"],
424
+ sdkPackages: ["cloudflare"],
425
+ docsUrl: "https://developers.cloudflare.com/api/",
426
+ changelogUrl: "https://developers.cloudflare.com/changelog/"
427
+ }
428
+ ];
429
+
430
+ // packages/core/src/providers.ts
431
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
432
+ var EndpointRefSchema = z.string().regex(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) \/\S*$/);
433
+ var ProviderSchema = z.object({
434
+ id: z.string().regex(/^[a-z0-9-]+$/),
435
+ name: z.string().min(1),
436
+ /** Exact hostnames, or "*.example.com" for any subdomain of example.com. */
437
+ hosts: z.array(z.string().regex(/^(\*\.)?[a-z0-9.-]+$/)).min(1),
438
+ /** npm packages that are this provider's SDKs. */
439
+ sdkPackages: z.array(z.string().min(1)),
440
+ /** SDK method chain (after the client, without a leading "rest.") to the HTTP endpoint it calls. */
441
+ sdkMethods: z.record(z.string(), EndpointRefSchema).optional(),
442
+ docsUrl: z.url(),
443
+ changelogUrl: z.url().optional(),
444
+ /** The provider's Python SDK: PyPI package and import module, and SDK method chain → HTTP endpoint. */
445
+ python: z.object({
446
+ packages: z.array(z.object({ pypi: z.string().min(1), module: z.string().min(1) })),
447
+ methods: z.record(z.string(), EndpointRefSchema).optional()
448
+ }).optional()
449
+ });
450
+ var ProviderRegistrySchema = z.array(ProviderSchema).refine((ps) => new Set(ps.map((p) => p.id)).size === ps.length, "provider ids must be unique");
451
+ var cached;
452
+ function loadProviders() {
453
+ cached ??= ProviderRegistrySchema.parse(providers_default);
454
+ return cached;
455
+ }
456
+ function getProvider(id) {
457
+ return loadProviders().find((p) => p.id === id);
458
+ }
459
+ function providerForHost(host) {
460
+ const h = host.toLowerCase().replace(/:\d+$/, "");
461
+ let best;
462
+ for (const provider of loadProviders()) {
463
+ for (const pattern of provider.hosts) {
464
+ let score = -1;
465
+ if (pattern === h) score = 1e4;
466
+ else if (pattern.startsWith("*.") && h.endsWith(pattern.slice(1))) score = pattern.length;
467
+ if (score > (best?.score ?? -1)) best = { provider, score };
468
+ }
469
+ }
470
+ return best?.provider;
471
+ }
472
+ function providerForPackage(name) {
473
+ return loadProviders().find((p) => p.sdkPackages.includes(name));
474
+ }
475
+ function providerForPythonModule(module) {
476
+ const top = module.split(".")[0];
477
+ return loadProviders().find((p) => p.python?.packages.some((pkg) => pkg.module === top));
478
+ }
479
+ function providerForPythonPackage(name) {
480
+ const norm = (s) => s.toLowerCase().replace(/[-_.]+/g, "-");
481
+ return loadProviders().find((p) => p.python?.packages.some((pkg) => norm(pkg.pypi) === norm(name)));
482
+ }
483
+ function sdkEndpoint(provider, chain) {
484
+ const methods = provider.sdkMethods ?? {};
485
+ const key = chain.replace(/^rest\./, "");
486
+ const ref = methods[key];
487
+ if (!ref) return void 0;
488
+ const [method, path13] = ref.split(" ");
489
+ return { method, path: path13 };
490
+ }
491
+
492
+ // packages/core/src/lockfile.ts
493
+ import { createHash } from "crypto";
494
+ import { mkdirSync, writeFileSync } from "fs";
495
+ import path from "path";
496
+ import { z as z2 } from "zod";
497
+ var NameValueSchema = z2.object({
498
+ name: z2.string(),
499
+ /** Statically known value, with unknown parts as {name}; null when unknown. */
500
+ value: z2.string().nullable()
501
+ });
502
+ var SurfaceSchema = z2.object({
503
+ /** Response body field paths read, dot-separated; array elements as "[]". */
504
+ fields: z2.array(z2.string()),
505
+ /** Status codes the code compares against. */
506
+ statusCodes: z2.array(z2.number().int()),
507
+ /** Branches on `response.ok`. */
508
+ checksOk: z2.boolean(),
509
+ /** String values response fields are compared against. */
510
+ enums: z2.array(z2.object({ field: z2.string(), values: z2.array(z2.string()) })),
511
+ /** Request headers set, names lowercased (API version pins live here). */
512
+ headers: z2.array(NameValueSchema),
513
+ /** Request query parameters. */
514
+ query: z2.array(NameValueSchema),
515
+ /** Response headers read, names lowercased. */
516
+ responseHeaders: z2.array(z2.string())
517
+ });
518
+ var CallSiteSchema = z2.object({
519
+ /** Repo-relative POSIX path. */
520
+ file: z2.string(),
521
+ line: z2.number().int().positive(),
522
+ column: z2.number().int().positive(),
523
+ provider: z2.string().nullable(),
524
+ kind: z2.enum(["http", "sdk"]),
525
+ /** HTTP library (fetch, axios, got, undici, ky, node-fetch) or SDK package name. */
526
+ client: z2.string(),
527
+ /** SDK method chain, for example "rest.actions.createWorkflowDispatch". */
528
+ operation: z2.string().optional(),
529
+ method: z2.enum(HTTP_METHODS).nullable(),
530
+ host: z2.string().nullable(),
531
+ /** Path with dynamic segments as {name} or {}, query string removed. */
532
+ endpoint: z2.string().nullable(),
533
+ /** True when provider, method and endpoint are all known. */
534
+ resolved: z2.boolean(),
535
+ surface: SurfaceSchema
536
+ });
537
+ var LockedCallSiteSchema = z2.object({ id: z2.string().regex(/^[0-9a-f]{12}$/) }).extend(CallSiteSchema.shape);
538
+ var ApiLockSchema = z2.object({
539
+ lockfileVersion: z2.literal(1),
540
+ generator: z2.string(),
541
+ providers: z2.array(
542
+ z2.object({
543
+ id: z2.string(),
544
+ callSites: z2.number().int().nonnegative(),
545
+ /** SDK packages in package.json that point at this provider. */
546
+ declaredBy: z2.array(z2.string())
547
+ })
548
+ ),
549
+ callSites: z2.array(LockedCallSiteSchema)
550
+ });
551
+ var LOCKFILE_VERSION = 1;
552
+ function callSiteId(site) {
553
+ return createHash("sha256").update(`${site.file}:${site.line}:${site.column}:${site.method ?? "?"}:${site.endpoint ?? "?"}`).digest("hex").slice(0, 12);
554
+ }
555
+ function createApiLock(input, generator) {
556
+ const callSites = [...input.callSites].sort((a, b) => cmp(a.file, b.file) || a.line - b.line || a.column - b.column).map((site) => ({ id: callSiteId(site), ...site }));
557
+ const providers = /* @__PURE__ */ new Map();
558
+ const entry = (id) => {
559
+ let e = providers.get(id);
560
+ if (!e) providers.set(id, e = { callSites: 0, declaredBy: /* @__PURE__ */ new Set() });
561
+ return e;
562
+ };
563
+ for (const site of callSites) if (site.provider) entry(site.provider).callSites++;
564
+ for (const d of input.declared) entry(d.provider).declaredBy.add(d.package);
565
+ return ApiLockSchema.parse({
566
+ lockfileVersion: LOCKFILE_VERSION,
567
+ generator,
568
+ providers: [...providers].sort(([a], [b]) => cmp(a, b)).map(([id, e]) => ({ id, callSites: e.callSites, declaredBy: [...e.declaredBy].sort() })),
569
+ callSites
570
+ });
571
+ }
572
+ function serializeApiLock(lock) {
573
+ return JSON.stringify(ApiLockSchema.parse(lock), null, 2) + "\n";
574
+ }
575
+ function writeApiLock(file, lock) {
576
+ mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
577
+ writeFileSync(file, serializeApiLock(lock));
578
+ }
579
+ function cmp(a, b) {
580
+ return a < b ? -1 : a > b ? 1 : 0;
581
+ }
582
+
583
+ // packages/core/src/advisory.ts
584
+ import { readdirSync, readFileSync } from "fs";
585
+ import path2 from "path";
586
+ import { z as z3 } from "zod";
587
+ var CHANGE_CLASSES = [
588
+ "status-code",
589
+ "field-rename",
590
+ "enum-added",
591
+ "required-field",
592
+ "version-sunset",
593
+ "scope-change",
594
+ "auth"
595
+ ];
596
+ var SEVERITIES = ["critical", "high", "medium", "low"];
597
+ var IsoDate = z3.iso.date();
598
+ var EndpointPatternSchema = z3.object({
599
+ method: z3.union([z3.enum(HTTP_METHODS), z3.literal("*")]),
600
+ path: z3.string().startsWith("/"),
601
+ /** Limit to these hosts (for providers whose hosts behave differently). */
602
+ hosts: z3.array(z3.string()).optional()
603
+ });
604
+ var TriggerSchema = z3.object({
605
+ /** Affected when the code compares the status against one of these (the old codes). */
606
+ statusCodes: z3.array(z3.number().int()).optional(),
607
+ /**
608
+ * The codes the provider answers with now. Code that already compares
609
+ * against all of them handles the change and is not affected.
610
+ */
611
+ newStatusCodes: z3.array(z3.number().int()).optional(),
612
+ /** Affected when the code reads one of these body fields (or a field beneath it). */
613
+ fields: z3.array(z3.string()).optional(),
614
+ /** Affected when the code compares this field against string values. */
615
+ enums: z3.array(z3.object({ field: z3.string() })).optional(),
616
+ /** Affected when the request sets this header (optionally to one of these values). */
617
+ headers: z3.array(z3.object({ name: z3.string(), values: z3.array(z3.string()).optional() })).optional(),
618
+ /**
619
+ * Affected when the request sets this query parameter: optionally to a number below `below`, or to
620
+ * one of `values` (a comma-separated value such as `metric=a,b` matches when any item does).
621
+ */
622
+ query: z3.array(z3.object({ name: z3.string(), below: z3.number().optional(), values: z3.array(z3.string()).optional() })).optional(),
623
+ /** Every call to the endpoint is affected (sunsets, auth and scope changes). */
624
+ any: z3.boolean().optional()
625
+ }).refine((t) => Object.values(t).some((v) => v !== void 0 && v !== false), "trigger needs at least one condition");
626
+ var FixSchema = z3.object({
627
+ /** Replace this part of the request path, for example "/v21.0/" with "/v26.0/". */
628
+ path: z3.object({ from: z3.string().min(1), to: z3.string().min(1) }).optional(),
629
+ /** Send this query parameter with this value, for example minorversion=75. */
630
+ query: z3.object({ name: z3.string().min(1), value: z3.string() }).optional(),
631
+ /** Send this request header with this value, for example Stripe-Version. */
632
+ header: z3.object({ name: z3.string().min(1), value: z3.string() }).optional(),
633
+ /** Response fields that were renamed: the code reads the new name, falling back to the old one. */
634
+ fields: z3.array(z3.object({ from: z3.string().min(1), to: z3.string().min(1) })).optional(),
635
+ /** Enum values that were renamed: comparisons with an old value also accept the new one. */
636
+ enums: z3.array(z3.object({ field: z3.string().min(1), values: z3.array(z3.object({ from: z3.string(), to: z3.string() })).min(1) })).optional()
637
+ }).refine((f) => Object.values(f).some((v) => v !== void 0), "fix needs at least one change");
638
+ var AdvisorySchema = z3.object({
639
+ id: z3.string().regex(/^[a-z0-9-]+$/),
640
+ provider: z3.string(),
641
+ title: z3.string().min(1),
642
+ summary: z3.string().min(1),
643
+ endpoint: EndpointPatternSchema,
644
+ changeClass: z3.enum(CHANGE_CLASSES),
645
+ trigger: TriggerSchema,
646
+ /** When the changed behavior was first seen in the wild. */
647
+ firstObserved: IsoDate.nullable(),
648
+ /** The provider's own notice, if there was one. */
649
+ publishedNotice: z3.object({ date: IsoDate.nullable(), url: z3.url() }).nullable(),
650
+ /** When the change takes (or took) effect. */
651
+ effectiveDate: IsoDate,
652
+ severity: z3.enum(SEVERITIES),
653
+ migrationNote: z3.string().min(1),
654
+ recipeId: z3.string().optional(),
655
+ /** What `recipeId: "apply-fix"` changes. */
656
+ fix: FixSchema.optional(),
657
+ sources: z3.array(z3.url()).min(1),
658
+ /** "verified": checked against a primary or two independent sources. "unverified": secondary reports only. */
659
+ status: z3.enum(["verified", "unverified"]),
660
+ /** Observations behind a machine-drafted advisory: what SHAPI saw before and after the change. */
661
+ evidence: z3.object({
662
+ source: z3.string(),
663
+ before: z3.object({ observedAt: z3.iso.datetime(), status: z3.number().int(), shapeHash: z3.string() }),
664
+ after: z3.object({ observedAt: z3.iso.datetime(), status: z3.number().int(), shapeHash: z3.string() }),
665
+ changes: z3.array(z3.string())
666
+ }).optional()
667
+ });
668
+ function loadAdvisories(dir) {
669
+ const advisories = [];
670
+ const seen = /* @__PURE__ */ new Set();
671
+ for (const name of readdirSync(dir).filter((f) => f.endsWith(".json")).sort()) {
672
+ const file = path2.join(dir, name);
673
+ const parsed = AdvisorySchema.safeParse(JSON.parse(readFileSync(file, "utf8")));
674
+ if (!parsed.success) throw new Error(`invalid advisory ${name}: ${z3.prettifyError(parsed.error)}`);
675
+ const advisory = parsed.data;
676
+ if (`${advisory.id}.json` !== name) throw new Error(`advisory ${name} must be named ${advisory.id}.json`);
677
+ if (seen.has(advisory.id)) throw new Error(`duplicate advisory id ${advisory.id}`);
678
+ seen.add(advisory.id);
679
+ advisories.push(advisory);
680
+ }
681
+ return advisories.sort((a, b) => a.id < b.id ? -1 : 1);
682
+ }
683
+ var SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
684
+
685
+ // packages/core/src/matcher.ts
686
+ var DAY_MS = 864e5;
687
+ function matchAdvisories(lock, advisories, options = {}) {
688
+ const now = options.now ?? /* @__PURE__ */ new Date();
689
+ const horizon = options.horizonDays ?? 90;
690
+ const affected = [];
691
+ const exposed = [];
692
+ const hitsPerAdvisory = /* @__PURE__ */ new Map();
693
+ for (const advisory of advisories) {
694
+ for (const callSite of lock.callSites) {
695
+ if (!endpointMatches(callSite, advisory)) continue;
696
+ hitsPerAdvisory.set(advisory.id, (hitsPerAdvisory.get(advisory.id) ?? 0) + 1);
697
+ const reasons = triggerReasons(callSite, advisory.trigger);
698
+ if (reasons.length > 0) affected.push({ advisory, callSite, reasons });
699
+ else exposed.push({ advisory, callSite, reasons: [`calls ${describe(callSite)} but does not depend on the changed behavior`] });
700
+ }
701
+ }
702
+ const present = new Set(lock.providers.map((p) => p.id));
703
+ const today = Math.floor(now.getTime() / DAY_MS) * DAY_MS;
704
+ const upcoming = advisories.map((advisory) => ({
705
+ advisory,
706
+ daysUntil: Math.round((Date.parse(advisory.effectiveDate) - today) / DAY_MS),
707
+ callSites: hitsPerAdvisory.get(advisory.id) ?? 0
708
+ })).filter((u) => present.has(u.advisory.provider) && u.daysUntil >= 0 && u.daysUntil <= horizon).sort((a, b) => a.daysUntil - b.daysUntil || cmp2(a.advisory.id, b.advisory.id));
709
+ return { affected: affected.sort(byFinding), exposed: exposed.sort(byFinding), upcoming };
710
+ }
711
+ function endpointMatches(site, advisory) {
712
+ const pattern = advisory.endpoint;
713
+ if (site.provider !== advisory.provider || site.endpoint === null) return false;
714
+ if (pattern.method !== "*" && site.method !== pattern.method) return false;
715
+ if (pattern.hosts && !(site.host && pattern.hosts.includes(site.host))) return false;
716
+ return pathMatches(site.endpoint, pattern);
717
+ }
718
+ function pathMatches(endpoint, pattern) {
719
+ const sample = endpoint.replace(/\{[^}]*\}/g, "\0");
720
+ let re = "";
721
+ const src = pattern.path;
722
+ for (let i = 0; i < src.length; ) {
723
+ if (src.startsWith("/**", i)) {
724
+ re += "(?:/.*)?";
725
+ i += 3;
726
+ } else if (src.startsWith("**", i)) {
727
+ re += ".*";
728
+ i += 2;
729
+ } else if (src[i] === "*") {
730
+ re += "[^/]*";
731
+ i += 1;
732
+ } else if (src[i] === "{") {
733
+ const end = src.indexOf("}", i);
734
+ re += "[^/]+";
735
+ i = end < 0 ? src.length : end + 1;
736
+ } else {
737
+ re += src[i].replace(/[.+?^$()|[\]\\]/g, "\\$&");
738
+ i += 1;
739
+ }
740
+ }
741
+ return new RegExp(`^${re}/?$`).test(sample);
742
+ }
743
+ function triggerReasons(site, trigger) {
744
+ const s = site.surface;
745
+ const reasons = [];
746
+ const handlesNew = (trigger.newStatusCodes ?? []).length > 0 && trigger.newStatusCodes.every((c) => s.statusCodes.includes(c));
747
+ for (const code of trigger.statusCodes ?? []) {
748
+ if (s.statusCodes.includes(code) && !handlesNew) reasons.push(`compares the response status to ${code}`);
749
+ }
750
+ for (const field of trigger.fields ?? []) {
751
+ const read = s.fields.filter((f) => f === field || f.startsWith(`${field}.`) || f.startsWith(`${field}[]`));
752
+ if (read.length) reasons.push(`reads ${read.join(", ")}`);
753
+ }
754
+ for (const { field } of trigger.enums ?? []) {
755
+ const e = s.enums.find((x) => x.field === field);
756
+ if (e) reasons.push(`compares ${field} to ${e.values.map((v) => JSON.stringify(v)).join(", ")}`);
757
+ }
758
+ for (const h of trigger.headers ?? []) {
759
+ const set = s.headers.find((x) => x.name === h.name.toLowerCase());
760
+ if (set && (!h.values || set.value !== null && h.values.includes(set.value))) reasons.push(`sends ${set.name}: ${set.value ?? "(dynamic)"}`);
761
+ }
762
+ for (const q of trigger.query ?? []) {
763
+ const p = s.query.find((x) => x.name === q.name);
764
+ if (!p) continue;
765
+ if (q.below !== void 0) {
766
+ if (p.value !== null && Number(p.value) < q.below) reasons.push(`sends ${p.name}=${p.value} (below ${q.below})`);
767
+ } else if (q.values) {
768
+ const hit = p.value === null ? [] : p.value.split(",").map((v) => v.trim()).filter((v) => q.values.includes(v));
769
+ if (hit.length) reasons.push(`sends ${p.name}=${hit.join(",")}`);
770
+ } else reasons.push(`sends ${p.name}=${p.value ?? "(dynamic)"}`);
771
+ }
772
+ if (trigger.any) reasons.push(`calls ${describe(site)}`);
773
+ return reasons;
774
+ }
775
+ function describe(site) {
776
+ return `${site.method ?? "?"} ${site.endpoint ?? "?"}`;
777
+ }
778
+ function byFinding(a, b) {
779
+ return SEVERITY_RANK[a.advisory.severity] - SEVERITY_RANK[b.advisory.severity] || cmp2(a.callSite.file, b.callSite.file) || a.callSite.line - b.callSite.line || cmp2(a.advisory.id, b.advisory.id);
780
+ }
781
+ function cmp2(a, b) {
782
+ return a < b ? -1 : a > b ? 1 : 0;
783
+ }
784
+
785
+ // packages/extractor/src/index.ts
786
+ import path4 from "path";
787
+ import { readFileSync as readFileSync3 } from "fs";
788
+ import { Project, ScriptKind } from "ts-morph";
789
+
790
+ // packages/extractor/src/callsites.ts
791
+ import { Node, SyntaxKind } from "ts-morph";
792
+ function emptySurface() {
793
+ return { fields: [], statusCodes: [], checksOk: false, enums: [], headers: [], query: [], responseHeaders: [] };
794
+ }
795
+ var HTTP_MODULES = {
796
+ "node-fetch": "node-fetch",
797
+ "cross-fetch": "node-fetch",
798
+ "isomorphic-fetch": "node-fetch",
799
+ axios: "axios",
800
+ got: "got",
801
+ ky: "ky",
802
+ undici: "undici"
803
+ };
804
+ var VERBS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
805
+ function extractFromFile(sf, file) {
806
+ const ctx = { sf, imports: /* @__PURE__ */ new Map(), consts: /* @__PURE__ */ new Map(), http: /* @__PURE__ */ new Map(), sdk: /* @__PURE__ */ new Map() };
807
+ collectImports(ctx);
808
+ collectConsts(ctx);
809
+ collectClients(ctx);
810
+ const found = [];
811
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
812
+ const hit = matchHttp(ctx, call) ?? matchSdk(ctx, call);
813
+ if (!hit) continue;
814
+ const { line, column } = sf.getLineAndColumnAtPos(call.getStart());
815
+ hit.site.file = file;
816
+ hit.site.line = line;
817
+ hit.site.column = column;
818
+ found.push(hit);
819
+ }
820
+ return found;
821
+ }
822
+ function collectImports(ctx) {
823
+ for (const decl of ctx.sf.getImportDeclarations()) {
824
+ const module = decl.getModuleSpecifierValue();
825
+ const def = decl.getDefaultImport();
826
+ if (def) ctx.imports.set(def.getText(), { module, imported: "default" });
827
+ const ns = decl.getNamespaceImport();
828
+ if (ns) ctx.imports.set(ns.getText(), { module, imported: "*" });
829
+ for (const named of decl.getNamedImports()) {
830
+ const local = named.getAliasNode()?.getText() ?? named.getName();
831
+ ctx.imports.set(local, { module, imported: named.getName() });
832
+ }
833
+ }
834
+ for (const decl of ctx.sf.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
835
+ const module = requiredModule(decl.getInitializer());
836
+ if (!module) continue;
837
+ const name = decl.getNameNode();
838
+ if (Node.isIdentifier(name)) ctx.imports.set(name.getText(), { module, imported: "default" });
839
+ else if (Node.isObjectBindingPattern(name)) {
840
+ for (const el of name.getElements()) {
841
+ const imported = el.getPropertyNameNode()?.getText() ?? el.getName();
842
+ ctx.imports.set(el.getName(), { module, imported });
843
+ }
844
+ }
845
+ }
846
+ }
847
+ function requiredModule(node) {
848
+ if (!node || !Node.isCallExpression(node)) return void 0;
849
+ const callee = node.getExpression();
850
+ const arg = node.getArguments()[0];
851
+ if (Node.isIdentifier(callee) && callee.getText() === "require" && arg && Node.isStringLiteral(arg)) {
852
+ return arg.getLiteralValue();
853
+ }
854
+ return void 0;
855
+ }
856
+ function collectConsts(ctx) {
857
+ for (const decl of ctx.sf.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
858
+ const list = decl.getVariableStatement()?.getDeclarationKind();
859
+ const init = decl.getInitializer();
860
+ if (list === "const" && init && Node.isIdentifier(decl.getNameNode())) ctx.consts.set(decl.getName(), init);
861
+ }
862
+ }
863
+ function collectClients(ctx) {
864
+ const bind = (key, init) => {
865
+ if (!init) return;
866
+ const expr = unwrap(init);
867
+ if (!Node.isNewExpression(expr) && !Node.isCallExpression(expr)) return;
868
+ const callee = expr.getExpression();
869
+ const args = expr.getArguments();
870
+ if (Node.isPropertyAccessExpression(callee)) {
871
+ const lib = httpLibOf(ctx, callee.getExpression());
872
+ if (lib && ["create", "extend"].includes(callee.getName())) {
873
+ const opts = args[0];
874
+ const base = Node.isObjectLiteralExpression(opts) ? evalString(ctx, propertyValue(opts, "baseURL")) ?? evalString(ctx, propertyValue(opts, "prefixUrl")) : void 0;
875
+ ctx.http.set(key, { lib, baseURL: base });
876
+ return;
877
+ }
878
+ }
879
+ const module = moduleOfCallee(ctx, callee);
880
+ const provider = module ? providerForPackage(module) : void 0;
881
+ if (module && provider) {
882
+ const options = [...args].reverse().find((a) => Node.isObjectLiteralExpression(a));
883
+ ctx.sdk.set(key, { provider, pkg: module, options });
884
+ }
885
+ };
886
+ for (const decl of ctx.sf.getDescendantsOfKind(SyntaxKind.VariableDeclaration)) {
887
+ if (Node.isIdentifier(decl.getNameNode())) bind(decl.getName(), decl.getInitializer());
888
+ }
889
+ for (const prop of ctx.sf.getDescendantsOfKind(SyntaxKind.PropertyDeclaration)) {
890
+ bind(`this.${prop.getName()}`, prop.getInitializer());
891
+ }
892
+ for (const bin of ctx.sf.getDescendantsOfKind(SyntaxKind.BinaryExpression)) {
893
+ if (bin.getOperatorToken().getKind() !== SyntaxKind.EqualsToken) continue;
894
+ const left = bin.getLeft();
895
+ if (Node.isPropertyAccessExpression(left) && left.getExpression().getKind() === SyntaxKind.ThisKeyword) {
896
+ bind(`this.${left.getName()}`, bin.getRight());
897
+ } else if (Node.isIdentifier(left)) {
898
+ bind(left.getText(), bin.getRight());
899
+ }
900
+ }
901
+ }
902
+ function moduleOfCallee(ctx, callee) {
903
+ const root = chainOf(callee)?.root;
904
+ if (root) return ctx.imports.get(root)?.module;
905
+ return requiredModule(callee);
906
+ }
907
+ function httpLibOf(ctx, node) {
908
+ if (!Node.isIdentifier(node)) return void 0;
909
+ const ref = ctx.imports.get(node.getText());
910
+ if (ref) {
911
+ const lib = HTTP_MODULES[ref.module];
912
+ if (lib === "undici") return ref.imported === "*" ? "undici" : void 0;
913
+ return lib && (ref.imported === "default" || ref.imported === "*") ? lib : void 0;
914
+ }
915
+ return void 0;
916
+ }
917
+ function matchHttp(ctx, call) {
918
+ const callee = call.getExpression();
919
+ const args = call.getArguments();
920
+ if (Node.isIdentifier(callee)) {
921
+ const name = callee.getText();
922
+ const ref = ctx.imports.get(name);
923
+ if (!ref && name === "fetch") return fetchStyle(ctx, call, "fetch", args[0], args[1]);
924
+ if (ref) {
925
+ const lib = HTTP_MODULES[ref.module];
926
+ if (lib === "node-fetch") return fetchStyle(ctx, call, lib, args[0], args[1]);
927
+ if (lib === "undici" && ref.imported === "fetch") return fetchStyle(ctx, call, lib, args[0], args[1]);
928
+ if (lib === "undici" && ref.imported === "request") return fetchStyle(ctx, call, lib, args[0], args[1]);
929
+ if (lib === "axios" && ref.imported === "default") return axiosStyle(ctx, call, void 0, void 0);
930
+ if ((lib === "got" || lib === "ky") && ref.imported === "default") return fetchStyle(ctx, call, lib, args[0], args[1]);
931
+ }
932
+ const inst = ctx.http.get(name);
933
+ if (inst?.lib === "axios") return axiosStyle(ctx, call, void 0, inst.baseURL);
934
+ if (inst) return fetchStyle(ctx, call, inst.lib, args[0], args[1], void 0, inst.baseURL);
935
+ return void 0;
936
+ }
937
+ if (Node.isPropertyAccessExpression(callee)) {
938
+ const target = callee.getExpression();
939
+ const member = callee.getName();
940
+ const lib = httpLibOf(ctx, target);
941
+ const inst = Node.isIdentifier(target) ? ctx.http.get(target.getText()) : void 0;
942
+ const effective = lib ?? inst?.lib;
943
+ if (!effective) return void 0;
944
+ const base = inst?.baseURL;
945
+ if (effective === "undici") {
946
+ return member === "fetch" || member === "request" ? fetchStyle(ctx, call, "undici", args[0], args[1]) : void 0;
947
+ }
948
+ if (effective === "axios") {
949
+ if (member === "request") return axiosStyle(ctx, call, void 0, base);
950
+ if (VERBS.has(member)) return axiosStyle(ctx, call, member.toUpperCase(), base);
951
+ return void 0;
952
+ }
953
+ if (VERBS.has(member)) {
954
+ return fetchStyle(ctx, call, effective, args[0], args[1], member.toUpperCase(), base);
955
+ }
956
+ }
957
+ return void 0;
958
+ }
959
+ function fetchStyle(ctx, call, lib, urlArg, optsArg, verb, baseURL) {
960
+ const opts = resolveObject(ctx, optsArg);
961
+ let method = verb ?? "GET";
962
+ if (!verb && optsArg) {
963
+ if (opts) method = methodFrom(ctx, propertyValue(opts, "method")) ?? (propertyValue(opts, "method") ? null : "GET");
964
+ else method = null;
965
+ }
966
+ const params = opts && (lib === "got" || lib === "ky") ? resolveObject(ctx, propertyValue(opts, "searchParams")) : void 0;
967
+ return httpSite(ctx, call, lib, joinUrl(baseURL, evalString(ctx, urlArg)), method, opts ? optsArg : void 0, params);
968
+ }
969
+ function axiosStyle(ctx, call, verb, baseURL) {
970
+ const args = call.getArguments();
971
+ const first = args[0];
972
+ const firstObj = resolveObject(ctx, first);
973
+ if (!verb && firstObj && !isStringish(first)) {
974
+ const url = evalString(ctx, propertyValue(firstObj, "url"));
975
+ const base = evalString(ctx, propertyValue(firstObj, "baseURL")) ?? baseURL;
976
+ const method2 = methodFrom(ctx, propertyValue(firstObj, "method")) ?? (propertyValue(firstObj, "method") ? null : "GET");
977
+ const params2 = resolveObject(ctx, propertyValue(firstObj, "params"));
978
+ return httpSite(ctx, call, "axios", joinUrl(base, url), method2, first, params2);
979
+ }
980
+ const hasBody = verb === "POST" || verb === "PUT" || verb === "PATCH";
981
+ const config = hasBody ? args[2] : args[1];
982
+ const cfg = resolveObject(ctx, config);
983
+ let method = verb ?? "GET";
984
+ if (!verb && cfg) method = methodFrom(ctx, propertyValue(cfg, "method")) ?? "GET";
985
+ const params = cfg ? resolveObject(ctx, propertyValue(cfg, "params")) : void 0;
986
+ return httpSite(ctx, call, "axios", joinUrl(baseURL, evalString(ctx, first)), method, config, params);
987
+ }
988
+ function httpSite(ctx, call, lib, url, method, options, params) {
989
+ const { host, path: path13, query } = parseUrl(url);
990
+ const surface = emptySurface();
991
+ surface.headers = headersOf(ctx, options);
992
+ surface.query = [...query, ...objectEntries(ctx, params)];
993
+ const provider = host ? providerForHost(host)?.id ?? null : null;
994
+ return {
995
+ call,
996
+ options,
997
+ site: {
998
+ file: "",
999
+ line: 0,
1000
+ column: 0,
1001
+ provider,
1002
+ kind: "http",
1003
+ client: lib,
1004
+ method,
1005
+ host,
1006
+ endpoint: path13,
1007
+ resolved: Boolean(provider && path13 && method),
1008
+ surface
1009
+ }
1010
+ };
1011
+ }
1012
+ function matchSdk(ctx, call) {
1013
+ const chain = chainOf(call.getExpression());
1014
+ if (!chain || chain.names.length === 0) return void 0;
1015
+ let client = ctx.sdk.get(chain.root);
1016
+ if (!client) {
1017
+ const ref = ctx.imports.get(chain.root);
1018
+ const provider = ref ? providerForPackage(ref.module) : void 0;
1019
+ if (!ref || !provider || ref.imported !== "default") return void 0;
1020
+ client = { provider, pkg: ref.module };
1021
+ }
1022
+ const operation = chain.names.join(".");
1023
+ const args = call.getArguments();
1024
+ let method = null;
1025
+ let endpoint = null;
1026
+ let options = args[0];
1027
+ const route = operation === "request" ? evalString(ctx, args[0]) : void 0;
1028
+ if (route) {
1029
+ const m = /^([A-Z]+)\s+(\S+)$/.exec(route.trim());
1030
+ if (m && HTTP_METHODS.includes(m[1])) {
1031
+ method = m[1];
1032
+ endpoint = parseUrl(m[2]).path;
1033
+ } else if (route.startsWith("/")) {
1034
+ method = "GET";
1035
+ endpoint = parseUrl(route).path;
1036
+ }
1037
+ options = args[1];
1038
+ } else if (operation === "graphql") {
1039
+ method = "POST";
1040
+ endpoint = "/graphql";
1041
+ } else {
1042
+ const ep = sdkEndpoint(client.provider, operation);
1043
+ if (ep) {
1044
+ method = ep.method;
1045
+ endpoint = ep.path;
1046
+ }
1047
+ }
1048
+ const host = client.provider.hosts.find((h) => !h.startsWith("*.")) ?? null;
1049
+ const surface = emptySurface();
1050
+ surface.headers = [...headersOf(ctx, client.options, "defaultHeaders"), ...headersOf(ctx, options)];
1051
+ const apiVersion = client.options ? propertyValue(client.options, "apiVersion") : void 0;
1052
+ if (apiVersion) surface.headers.push({ name: `${client.provider.id}-version`, value: evalString(ctx, apiVersion) ?? null });
1053
+ surface.headers.sort((a, b) => a.name.localeCompare(b.name));
1054
+ return {
1055
+ call,
1056
+ options,
1057
+ clientOptions: client.options,
1058
+ site: {
1059
+ file: "",
1060
+ line: 0,
1061
+ column: 0,
1062
+ provider: client.provider.id,
1063
+ kind: "sdk",
1064
+ client: client.pkg,
1065
+ operation,
1066
+ method,
1067
+ host,
1068
+ endpoint,
1069
+ resolved: Boolean(method && endpoint),
1070
+ surface
1071
+ }
1072
+ };
1073
+ }
1074
+ function chainOf(node) {
1075
+ const names = [];
1076
+ let cur = node;
1077
+ while (Node.isPropertyAccessExpression(cur)) {
1078
+ names.unshift(cur.getName());
1079
+ const inner = cur.getExpression();
1080
+ if (inner.getKind() === SyntaxKind.ThisKeyword) {
1081
+ const first = names.shift();
1082
+ return { root: `this.${first}`, names };
1083
+ }
1084
+ cur = inner;
1085
+ }
1086
+ if (Node.isIdentifier(cur)) return { root: cur.getText(), names };
1087
+ return void 0;
1088
+ }
1089
+ function unwrap(node) {
1090
+ let cur = node;
1091
+ for (; ; ) {
1092
+ if (Node.isAwaitExpression(cur) || Node.isParenthesizedExpression(cur) || Node.isAsExpression(cur) || Node.isNonNullExpression(cur)) {
1093
+ cur = cur.getExpression();
1094
+ } else if (Node.isSatisfiesExpression(cur)) {
1095
+ cur = cur.getExpression();
1096
+ } else {
1097
+ return cur;
1098
+ }
1099
+ }
1100
+ }
1101
+ function propertyValue(obj, name) {
1102
+ if (!Node.isObjectLiteralExpression(obj)) return void 0;
1103
+ for (const prop of obj.getProperties()) {
1104
+ if (Node.isPropertyAssignment(prop) && propName(prop.getNameNode()) === name) return prop.getInitializer();
1105
+ if (Node.isShorthandPropertyAssignment(prop) && prop.getName() === name) return prop.getNameNode();
1106
+ }
1107
+ return void 0;
1108
+ }
1109
+ function propName(node) {
1110
+ if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) return node.getLiteralValue();
1111
+ return node.getText();
1112
+ }
1113
+ function headersOf(ctx, options, key = "headers") {
1114
+ const obj = resolveObject(ctx, options);
1115
+ return obj ? objectEntries(ctx, resolveObject(ctx, propertyValue(obj, key))).map((h) => ({ ...h, name: h.name.toLowerCase() })) : [];
1116
+ }
1117
+ function objectEntries(ctx, obj) {
1118
+ if (!obj || !Node.isObjectLiteralExpression(obj)) return [];
1119
+ const out = [];
1120
+ for (const prop of obj.getProperties()) {
1121
+ if (Node.isPropertyAssignment(prop)) {
1122
+ out.push({ name: propName(prop.getNameNode()), value: evalString(ctx, prop.getInitializer()) ?? null });
1123
+ } else if (Node.isShorthandPropertyAssignment(prop)) {
1124
+ out.push({ name: prop.getName(), value: evalString(ctx, prop.getNameNode()) ?? null });
1125
+ }
1126
+ }
1127
+ return out;
1128
+ }
1129
+ function resolveObject(ctx, node) {
1130
+ if (!node) return void 0;
1131
+ const expr = unwrap(node);
1132
+ if (Node.isObjectLiteralExpression(expr)) return expr;
1133
+ if (Node.isIdentifier(expr)) {
1134
+ const init = ctx.consts.get(expr.getText());
1135
+ if (init && Node.isObjectLiteralExpression(unwrap(init))) return unwrap(init);
1136
+ }
1137
+ return void 0;
1138
+ }
1139
+ function isStringish(node) {
1140
+ if (!node) return false;
1141
+ const expr = unwrap(node);
1142
+ return Node.isStringLiteral(expr) || Node.isTemplateExpression(expr) || Node.isNoSubstitutionTemplateLiteral(expr);
1143
+ }
1144
+ function methodFrom(ctx, node) {
1145
+ const value2 = evalString(ctx, node)?.toUpperCase();
1146
+ return value2 && HTTP_METHODS.includes(value2) ? value2 : void 0;
1147
+ }
1148
+ function evalString(ctx, node, depth = 0) {
1149
+ if (!node || depth > 8) return void 0;
1150
+ const expr = unwrap(node);
1151
+ if (Node.isStringLiteral(expr) || Node.isNoSubstitutionTemplateLiteral(expr)) return expr.getLiteralValue();
1152
+ if (Node.isNumericLiteral(expr)) return expr.getLiteralText();
1153
+ if (Node.isTemplateExpression(expr)) {
1154
+ let out = expr.getHead().getLiteralText();
1155
+ for (const span of expr.getTemplateSpans()) {
1156
+ const inner = span.getExpression();
1157
+ out += evalString(ctx, inner, depth + 1) ?? placeholder(inner);
1158
+ out += span.getLiteral().getLiteralText();
1159
+ }
1160
+ return out;
1161
+ }
1162
+ if (Node.isBinaryExpression(expr)) {
1163
+ const op = expr.getOperatorToken().getKind();
1164
+ if (op === SyntaxKind.PlusToken) {
1165
+ const l = evalString(ctx, expr.getLeft(), depth + 1);
1166
+ const r = evalString(ctx, expr.getRight(), depth + 1);
1167
+ if (l === void 0 && r === void 0) return void 0;
1168
+ return (l ?? placeholder(expr.getLeft())) + (r ?? placeholder(expr.getRight()));
1169
+ }
1170
+ if (op === SyntaxKind.QuestionQuestionToken || op === SyntaxKind.BarBarToken) {
1171
+ return evalString(ctx, expr.getLeft(), depth + 1) ?? evalString(ctx, expr.getRight(), depth + 1);
1172
+ }
1173
+ }
1174
+ if (Node.isIdentifier(expr)) {
1175
+ const init = ctx.consts.get(expr.getText());
1176
+ return init && init !== node ? evalString(ctx, init, depth + 1) : void 0;
1177
+ }
1178
+ if (Node.isNewExpression(expr) && expr.getExpression().getText() === "URL") {
1179
+ const [p, base] = expr.getArguments();
1180
+ const pathPart = evalString(ctx, p, depth + 1);
1181
+ const basePart = evalString(ctx, base, depth + 1);
1182
+ return pathPart !== void 0 && basePart !== void 0 ? joinUrl(basePart, pathPart) : pathPart ?? void 0;
1183
+ }
1184
+ return void 0;
1185
+ }
1186
+ function placeholder(node) {
1187
+ const expr = unwrap(node);
1188
+ if (Node.isIdentifier(expr)) return `{${expr.getText()}}`;
1189
+ if (Node.isPropertyAccessExpression(expr)) return `{${expr.getName()}}`;
1190
+ if (Node.isCallExpression(expr) && expr.getArguments().length === 1) return placeholder(expr.getArguments()[0]);
1191
+ return "{}";
1192
+ }
1193
+ function joinUrl(base, url) {
1194
+ if (url === void 0) return void 0;
1195
+ if (!base || /^[a-z]+:\/\//i.test(url)) return url;
1196
+ return base.replace(/\/+$/, "") + "/" + url.replace(/^\/+/, "");
1197
+ }
1198
+ function parseUrl(url) {
1199
+ if (url === void 0) return { host: null, path: null, query: [] };
1200
+ let host = null;
1201
+ let rest = url;
1202
+ const m = /^(?:[a-z]+:)?\/\/([^/?#]+)(.*)$/i.exec(url);
1203
+ if (m) {
1204
+ host = m[1].includes("{") ? null : m[1].toLowerCase();
1205
+ rest = m[2] || "/";
1206
+ } else if (!url.startsWith("/")) {
1207
+ const slash = url.indexOf("/");
1208
+ rest = slash >= 0 ? url.slice(slash) : "/";
1209
+ if (slash < 0) return { host: null, path: null, query: [] };
1210
+ }
1211
+ let path13 = rest.split(/[?#]/)[0].replace(/\/{2,}/g, "/");
1212
+ if (path13.length > 1) path13 = path13.replace(/\/$/, "");
1213
+ const search = /\?([^#]*)/.exec(rest)?.[1] ?? "";
1214
+ const query = search.split("&").filter(Boolean).map((pair) => {
1215
+ const [name, ...value2] = pair.split("=");
1216
+ return { name, value: value2.length ? value2.join("=") : null };
1217
+ });
1218
+ return { host, path: path13 || "/", query };
1219
+ }
1220
+
1221
+ // packages/extractor/src/surface.ts
1222
+ import { Node as Node2, SyntaxKind as SyntaxKind2 } from "ts-morph";
1223
+ var NOT_FIELDS = /* @__PURE__ */ new Set([
1224
+ "length",
1225
+ "map",
1226
+ "filter",
1227
+ "forEach",
1228
+ "find",
1229
+ "findIndex",
1230
+ "some",
1231
+ "every",
1232
+ "includes",
1233
+ "indexOf",
1234
+ "reduce",
1235
+ "slice",
1236
+ "join",
1237
+ "concat",
1238
+ "flatMap",
1239
+ "sort",
1240
+ "keys",
1241
+ "values",
1242
+ "entries",
1243
+ "at",
1244
+ "push",
1245
+ "toString",
1246
+ "toLowerCase",
1247
+ "toUpperCase",
1248
+ "trim",
1249
+ "split",
1250
+ "startsWith",
1251
+ "endsWith",
1252
+ "replace",
1253
+ "match",
1254
+ "then",
1255
+ "catch",
1256
+ "finally",
1257
+ "valueOf",
1258
+ "toJSON"
1259
+ ]);
1260
+ var ITEM_CALLBACKS = { map: 0, flatMap: 0, filter: 0, forEach: 0, find: 0, findIndex: 0, some: 0, every: 0, reduce: 1 };
1261
+ var MAX_DEPTH = 4;
1262
+ var Collector = class {
1263
+ fields = /* @__PURE__ */ new Set();
1264
+ statusCodes = /* @__PURE__ */ new Set();
1265
+ checksOk = false;
1266
+ enums = /* @__PURE__ */ new Map();
1267
+ responseHeaders = /* @__PURE__ */ new Set();
1268
+ addEnum(field, value2) {
1269
+ const set = this.enums.get(field) ?? /* @__PURE__ */ new Set();
1270
+ set.add(value2);
1271
+ this.enums.set(field, set);
1272
+ }
1273
+ };
1274
+ function addResponseSurface(found) {
1275
+ const c = new Collector();
1276
+ bindFrom(found.call, { space: "response", path: [] }, envelopeOf(found), c, 0);
1277
+ const surface = found.site.surface;
1278
+ surface.fields = [...c.fields].sort();
1279
+ surface.statusCodes = [...c.statusCodes].sort((a, b) => a - b);
1280
+ surface.checksOk = c.checksOk;
1281
+ surface.enums = [...c.enums].map(([field, values]) => ({ field, values: [...values].sort() })).sort((a, b) => a.field.localeCompare(b.field));
1282
+ surface.responseHeaders = [...c.responseHeaders].sort();
1283
+ }
1284
+ function envelopeOf({ site, call }) {
1285
+ if (site.kind === "sdk") return site.provider === "github" ? "axios" : "body";
1286
+ switch (site.client) {
1287
+ case "axios":
1288
+ return "axios";
1289
+ case "got":
1290
+ return "got";
1291
+ case "undici":
1292
+ return /request$/.test(call.getExpression().getText()) ? "undici" : "fetch";
1293
+ default:
1294
+ return "fetch";
1295
+ }
1296
+ }
1297
+ function bindFrom(expr, loc, env, c, depth) {
1298
+ if (depth > MAX_DEPTH) return;
1299
+ let cur = expr;
1300
+ for (; ; ) {
1301
+ const p = cur.getParent();
1302
+ if (p && (Node2.isAwaitExpression(p) || Node2.isParenthesizedExpression(p) || Node2.isAsExpression(p) || Node2.isNonNullExpression(p) || Node2.isSatisfiesExpression(p))) {
1303
+ cur = p;
1304
+ } else break;
1305
+ }
1306
+ const parent = cur.getParent();
1307
+ if (!parent) return;
1308
+ if (Node2.isPropertyAccessExpression(parent) && parent.getExpression() === cur && parent.getName() === "then") {
1309
+ const thenCall = parent.getParent();
1310
+ const cb = Node2.isCallExpression(thenCall) ? thenCall.getArguments()[0] : void 0;
1311
+ if (cb && (Node2.isArrowFunction(cb) || Node2.isFunctionExpression(cb))) {
1312
+ const param = cb.getParameters()[0];
1313
+ if (param) bindName(param.getNameNode(), loc, param, env, c, depth);
1314
+ }
1315
+ return;
1316
+ }
1317
+ if (Node2.isVariableDeclaration(parent) && parent.getInitializer() === cur) {
1318
+ bindName(parent.getNameNode(), loc, parent, env, c, depth);
1319
+ return;
1320
+ }
1321
+ if (Node2.isBinaryExpression(parent) && parent.getRight() === cur && parent.getOperatorToken().getKind() === SyntaxKind2.EqualsToken) {
1322
+ const left = parent.getLeft();
1323
+ if (Node2.isIdentifier(left)) bindName(left, loc, parent, env, c, depth);
1324
+ return;
1325
+ }
1326
+ use(cur, loc, env, c, depth);
1327
+ }
1328
+ function bindName(name, loc, decl, env, c, depth) {
1329
+ if (Node2.isIdentifier(name)) {
1330
+ for (const ref of referencesOf(name.getText(), decl)) use(ref, loc, env, c, depth);
1331
+ } else if (Node2.isObjectBindingPattern(name)) {
1332
+ for (const el of name.getElements()) {
1333
+ const prop = el.getPropertyNameNode()?.getText() ?? el.getName();
1334
+ bindName(el.getNameNode(), { ...loc, path: [...loc.path, prop] }, decl, env, c, depth);
1335
+ interpret(env, { ...loc, path: [...loc.path, prop] }, [], el, c, depth, false);
1336
+ }
1337
+ }
1338
+ }
1339
+ function referencesOf(name, decl) {
1340
+ const scope = decl.getFirstAncestor(
1341
+ (n) => Node2.isBlock(n) || Node2.isSourceFile(n) || Node2.isArrowFunction(n) || Node2.isFunctionExpression(n) || Node2.isFunctionDeclaration(n) || Node2.isMethodDeclaration(n) || Node2.isCaseClause(n)
1342
+ ) ?? decl.getSourceFile();
1343
+ const after = decl.getEnd();
1344
+ return scope.getDescendantsOfKind(SyntaxKind2.Identifier).filter((id) => {
1345
+ if (id.getText() !== name || id.getStart() < after) return false;
1346
+ const p = id.getParent();
1347
+ if (Node2.isPropertyAccessExpression(p) && p.getNameNode() === id) return false;
1348
+ if (Node2.isPropertyAssignment(p) && p.getNameNode() === id) return false;
1349
+ if (Node2.isBindingElement(p) || Node2.isVariableDeclaration(p) || Node2.isParameterDeclaration(p)) return false;
1350
+ return true;
1351
+ });
1352
+ }
1353
+ function use(ref, loc, env, c, depth) {
1354
+ const access = [];
1355
+ let cur = ref;
1356
+ for (; ; ) {
1357
+ const p = cur.getParent();
1358
+ if (Node2.isPropertyAccessExpression(p) && p.getExpression() === cur) {
1359
+ access.push(p.getName());
1360
+ cur = p;
1361
+ } else if (Node2.isElementAccessExpression(p) && p.getExpression() === cur) {
1362
+ const arg = p.getArgumentExpression();
1363
+ if (arg && (Node2.isStringLiteral(arg) || Node2.isNoSubstitutionTemplateLiteral(arg))) access.push(arg.getLiteralValue());
1364
+ else access.push("[]");
1365
+ cur = p;
1366
+ } else if (p && (Node2.isNonNullExpression(p) || Node2.isParenthesizedExpression(p))) {
1367
+ cur = p;
1368
+ } else break;
1369
+ }
1370
+ interpret(env, loc, access, cur, c, depth, true);
1371
+ }
1372
+ function interpret(env, loc, access, top, c, depth, compare) {
1373
+ const full = [...loc.path, ...access];
1374
+ if (loc.space === "body" || env === "body") {
1375
+ body(full, top, c, compare, env, depth);
1376
+ return;
1377
+ }
1378
+ const [first, ...rest] = full;
1379
+ if (first === void 0) return;
1380
+ const isCall = () => {
1381
+ const p = top.getParent();
1382
+ return Node2.isCallExpression(p) && p.getExpression() === top;
1383
+ };
1384
+ const statusKey = env === "got" || env === "undici" ? "statusCode" : "status";
1385
+ if (first === statusKey && rest.length === 0) {
1386
+ if (compare) for (const n of comparedLiterals(top).nums) c.statusCodes.add(n);
1387
+ return;
1388
+ }
1389
+ if (first === "ok" && rest.length === 0) {
1390
+ c.checksOk = true;
1391
+ return;
1392
+ }
1393
+ if (first === "headers") {
1394
+ const [h] = rest;
1395
+ if (h === "get" && isCall()) {
1396
+ const arg = top.getParent().getArguments()[0];
1397
+ if (arg && Node2.isStringLiteral(arg)) c.responseHeaders.add(arg.getLiteralValue().toLowerCase());
1398
+ } else if (h && h !== "get") c.responseHeaders.add(h.toLowerCase());
1399
+ return;
1400
+ }
1401
+ if (env === "axios" && first === "data") return body(rest, top, c, compare, env, depth);
1402
+ if (env === "got" && first === "body") return body(rest, top, c, compare, env, depth);
1403
+ const bodyCall = (env === "fetch" || env === "got") && rest.length === 0 && (first === "json" || first === "text") || env === "undici" && first === "body" && rest.length === 1 && (rest[0] === "json" || rest[0] === "text");
1404
+ if (bodyCall && isCall()) {
1405
+ bindFrom(top.getParent(), { space: "body", path: [] }, env, c, depth + 1);
1406
+ }
1407
+ }
1408
+ function body(path13, top, c, compare, env, depth) {
1409
+ const cut = path13.findIndex((p) => NOT_FIELDS.has(p));
1410
+ const fieldPath = cut >= 0 ? path13.slice(0, cut) : path13;
1411
+ if (fieldPath.length === 0) return;
1412
+ const field = fieldPath.join(".").replace(/\.\[\]/g, "[]");
1413
+ c.fields.add(field);
1414
+ if (compare && cut < 0) for (const s of comparedLiterals(top).strs) c.addEnum(field, s);
1415
+ if (depth >= MAX_DEPTH) return;
1416
+ const items = { space: "body", path: [...fieldPath, "[]"] };
1417
+ const method = cut >= 0 && cut === path13.length - 1 ? path13[cut] : void 0;
1418
+ const call = top.getParent();
1419
+ if (method && method in ITEM_CALLBACKS && Node2.isCallExpression(call) && call.getExpression() === top) {
1420
+ const cb = call.getArguments()[0];
1421
+ const param = cb && (Node2.isArrowFunction(cb) || Node2.isFunctionExpression(cb)) ? cb.getParameters()[ITEM_CALLBACKS[method]] : void 0;
1422
+ if (param) bindName(param.getNameNode(), items, param, env, c, depth + 1);
1423
+ if (method === "find") bindFrom(call, items, env, c, depth + 1);
1424
+ return;
1425
+ }
1426
+ const loop = top.getParent();
1427
+ if (cut < 0 && Node2.isForOfStatement(loop) && loop.getExpression() === top) {
1428
+ const list = loop.getInitializer();
1429
+ const decl = Node2.isVariableDeclarationList(list) ? list.getDeclarations()[0] : void 0;
1430
+ if (decl) bindName(decl.getNameNode(), items, decl, env, c, depth + 1);
1431
+ }
1432
+ }
1433
+ function comparedLiterals(node) {
1434
+ const nums = [];
1435
+ const strs = [];
1436
+ const take = (n) => {
1437
+ if (!n) return;
1438
+ if (Node2.isNumericLiteral(n)) nums.push(Number(n.getLiteralValue()));
1439
+ else if (Node2.isStringLiteral(n) || Node2.isNoSubstitutionTemplateLiteral(n)) strs.push(n.getLiteralValue());
1440
+ };
1441
+ let cur = node;
1442
+ while (Node2.isParenthesizedExpression(cur.getParent())) cur = cur.getParent();
1443
+ const p = cur.getParent();
1444
+ if (Node2.isBinaryExpression(p)) {
1445
+ const op = p.getOperatorToken().getKind();
1446
+ if (op === SyntaxKind2.EqualsEqualsEqualsToken || op === SyntaxKind2.ExclamationEqualsEqualsToken || op === SyntaxKind2.EqualsEqualsToken || op === SyntaxKind2.ExclamationEqualsToken) {
1447
+ take(p.getLeft() === cur ? p.getRight() : p.getLeft());
1448
+ }
1449
+ } else if (Node2.isSwitchStatement(p) && p.getExpression() === cur) {
1450
+ for (const clause of p.getClauses()) if (Node2.isCaseClause(clause)) take(clause.getExpression());
1451
+ } else if (Node2.isCallExpression(p) && p.getArguments()[0] === cur) {
1452
+ const callee = p.getExpression();
1453
+ if (Node2.isPropertyAccessExpression(callee) && callee.getName() === "includes") {
1454
+ const arr = callee.getExpression();
1455
+ if (Node2.isArrayLiteralExpression(arr)) arr.getElements().forEach(take);
1456
+ }
1457
+ }
1458
+ return { nums, strs };
1459
+ }
1460
+
1461
+ // packages/extractor/src/files.ts
1462
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1463
+ import path3 from "path";
1464
+ var SOURCE_FILE = /\.(c|m)?[jt]sx?$/;
1465
+ var PYTHON_FILE = /\.py$/;
1466
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "vendor", "bower_components", "venv", "__pycache__", "site-packages"]);
1467
+ var PYTHON_MANIFEST = /^(requirements[\w.-]*\.txt|pyproject\.toml)$/;
1468
+ function listRepoFiles(root) {
1469
+ const sources = [];
1470
+ const manifests = [];
1471
+ const python = [];
1472
+ const pythonManifests = [];
1473
+ const walk2 = (dir) => {
1474
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
1475
+ if (entry.name.startsWith(".")) continue;
1476
+ const full = path3.join(dir, entry.name);
1477
+ if (entry.isDirectory()) {
1478
+ if (!SKIP_DIRS.has(entry.name)) walk2(full);
1479
+ } else if (entry.isFile()) {
1480
+ if (entry.name === "package.json") manifests.push(full);
1481
+ else if (SOURCE_FILE.test(entry.name) && !entry.name.endsWith(".d.ts")) sources.push(full);
1482
+ else if (PYTHON_FILE.test(entry.name)) python.push(full);
1483
+ else if (PYTHON_MANIFEST.test(entry.name)) pythonManifests.push(full);
1484
+ }
1485
+ }
1486
+ };
1487
+ walk2(root);
1488
+ return { sources: sources.sort(), manifests: manifests.sort(), python: python.sort(), pythonManifests: pythonManifests.sort() };
1489
+ }
1490
+ function declaredProviders(root, manifests) {
1491
+ const found = [];
1492
+ for (const manifest of manifests) {
1493
+ let pkg;
1494
+ try {
1495
+ pkg = JSON.parse(readFileSync2(manifest, "utf8"));
1496
+ } catch {
1497
+ continue;
1498
+ }
1499
+ const names = /* @__PURE__ */ new Set();
1500
+ for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
1501
+ const deps = pkg[field];
1502
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
1503
+ }
1504
+ for (const name of [...names].sort()) {
1505
+ const provider = providerForPackage(name);
1506
+ if (provider) found.push({ provider: provider.id, package: name, manifest: toPosix(path3.relative(root, manifest)) });
1507
+ }
1508
+ }
1509
+ return found;
1510
+ }
1511
+ function toPosix(p) {
1512
+ return p.split(path3.sep).join("/");
1513
+ }
1514
+ function declaredPythonProviders(root, manifests) {
1515
+ const found = [];
1516
+ for (const manifest of manifests) {
1517
+ let text;
1518
+ try {
1519
+ text = readFileSync2(manifest, "utf8");
1520
+ } catch {
1521
+ continue;
1522
+ }
1523
+ const names = /* @__PURE__ */ new Set();
1524
+ if (manifest.endsWith(".txt")) {
1525
+ for (const line of text.split(/\r?\n/)) {
1526
+ const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(line.replace(/#.*/, ""));
1527
+ if (m && !line.trim().startsWith("-")) names.add(m[1]);
1528
+ }
1529
+ } else {
1530
+ let inTable = false;
1531
+ let inList = false;
1532
+ for (const line of text.split(/\r?\n/)) {
1533
+ const t = line.replace(/#.*/, "").trim();
1534
+ if (/^\[/.test(t)) {
1535
+ inTable = /dependencies\]$/.test(t);
1536
+ inList = false;
1537
+ continue;
1538
+ }
1539
+ if (/^(dependencies|optional-dependencies|dev-dependencies)\s*=\s*\[/.test(t) || /^[\w-]+\s*=\s*\[/.test(t)) inList = true;
1540
+ if (inList) for (const m of t.matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)/g)) names.add(m[1]);
1541
+ if (inList && t.includes("]")) inList = false;
1542
+ const kv = inTable ? /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/.exec(t) : null;
1543
+ if (kv) names.add(kv[1]);
1544
+ }
1545
+ }
1546
+ for (const name of [...names].sort()) {
1547
+ const provider = providerForPythonPackage(name);
1548
+ if (provider) found.push({ provider: provider.id, package: name, manifest: toPosix(path3.relative(root, manifest)) });
1549
+ }
1550
+ }
1551
+ return found;
1552
+ }
1553
+
1554
+ // packages/extractor/src/python.ts
1555
+ import { existsSync } from "fs";
1556
+ import { createRequire } from "module";
1557
+ import { fileURLToPath } from "url";
1558
+ import { Language, Parser } from "web-tree-sitter";
1559
+ var parser;
1560
+ async function loadPythonParser() {
1561
+ if (parser) return;
1562
+ await Parser.init();
1563
+ const bundled = fileURLToPath(new URL("./tree-sitter-python.wasm", import.meta.url));
1564
+ const wasm = existsSync(bundled) ? bundled : createRequire(import.meta.url).resolve("tree-sitter-python/tree-sitter-python.wasm");
1565
+ const language = await Language.load(wasm);
1566
+ const p = new Parser();
1567
+ p.setLanguage(language);
1568
+ parser = p;
1569
+ }
1570
+ function pythonParserLoaded() {
1571
+ return parser !== void 0;
1572
+ }
1573
+ var HTTP_LIBS = /* @__PURE__ */ new Set(["requests", "httpx", "aiohttp"]);
1574
+ var VERBS2 = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
1575
+ function extractPython(source, file) {
1576
+ if (!parser) throw new Error("call loadPythonParser() before extracting Python");
1577
+ const tree = parser.parse(source);
1578
+ if (!tree) return [];
1579
+ try {
1580
+ const root = tree.rootNode;
1581
+ const ctx = { names: /* @__PURE__ */ new Map(), consts: /* @__PURE__ */ new Map() };
1582
+ collectImports2(root, ctx);
1583
+ collectConsts2(root, ctx);
1584
+ collectClients2(root, ctx);
1585
+ const sites = [];
1586
+ for (const call of root.descendantsOfType("call")) {
1587
+ if (!call) continue;
1588
+ const site = httpCall(ctx, call) ?? sdkCall(ctx, call);
1589
+ if (!site) continue;
1590
+ site.file = file;
1591
+ site.line = call.startPosition.row + 1;
1592
+ site.column = call.startPosition.column + 1;
1593
+ addResponseSurface2(call, site);
1594
+ sites.push(site);
1595
+ }
1596
+ return sites;
1597
+ } finally {
1598
+ tree.delete();
1599
+ }
1600
+ }
1601
+ function collectImports2(root, ctx) {
1602
+ for (const st of root.descendantsOfType(["import_statement", "import_from_statement"])) {
1603
+ if (!st) continue;
1604
+ if (st.type === "import_statement") {
1605
+ for (const item of st.namedChildren) {
1606
+ if (!item) continue;
1607
+ const dotted = item.type === "aliased_import" ? item.childForFieldName("name") : item;
1608
+ const alias = item.type === "aliased_import" ? item.childForFieldName("alias")?.text : dotted?.text.split(".")[0];
1609
+ if (!dotted || !alias) continue;
1610
+ bindModule(ctx, alias, dotted.text);
1611
+ }
1612
+ } else {
1613
+ const module = st.childForFieldName("module_name")?.text;
1614
+ if (!module) continue;
1615
+ for (const item of st.childrenForFieldName("name")) {
1616
+ if (!item) continue;
1617
+ const name = item.type === "aliased_import" ? item.childForFieldName("name")?.text : item.text;
1618
+ const alias = item.type === "aliased_import" ? item.childForFieldName("alias")?.text : item.text;
1619
+ if (name && alias) bindModule(ctx, alias, `${module}.${name}`);
1620
+ }
1621
+ }
1622
+ }
1623
+ }
1624
+ function bindModule(ctx, alias, dotted) {
1625
+ const top = dotted.split(".")[0];
1626
+ if (HTTP_LIBS.has(top)) {
1627
+ ctx.names.set(alias, { kind: "http-module", lib: top });
1628
+ return;
1629
+ }
1630
+ const provider = providerForPythonModule(top);
1631
+ if (provider) ctx.names.set(alias, { kind: "sdk-module", provider, module: dotted });
1632
+ }
1633
+ function collectConsts2(root, ctx) {
1634
+ for (const st of root.namedChildren) {
1635
+ const assign = st?.type === "expression_statement" ? st.namedChildren[0] : void 0;
1636
+ if (assign?.type !== "assignment") continue;
1637
+ const left = assign.childForFieldName("left");
1638
+ const right = assign.childForFieldName("right");
1639
+ if (left?.type === "identifier" && right) ctx.consts.set(left.text, right);
1640
+ }
1641
+ }
1642
+ function collectClients2(root, ctx) {
1643
+ const visit = (target, value2) => {
1644
+ const call = unwrap2(value2);
1645
+ if (call.type !== "call") return;
1646
+ const fn = call.childForFieldName("function");
1647
+ const chain = fn ? chainOf2(fn) : void 0;
1648
+ if (!chain) return;
1649
+ const base = ctx.names.get(chain.root);
1650
+ if (!base) return;
1651
+ if (base.kind === "http-module" || base.kind === "http-client") {
1652
+ const ctor = chain.names.at(-1);
1653
+ if (!ctor || !/^(Session|Client|AsyncClient|ClientSession)$/.test(ctor)) return;
1654
+ const args = call.childForFieldName("arguments");
1655
+ const baseUrl = evalString2(ctx, keyword(args, "base_url"));
1656
+ ctx.names.set(target, { kind: "http-client", lib: base.lib, ...baseUrl ? { baseUrl } : {}, headers: dictEntries(ctx, keyword(args, "headers")) });
1657
+ return;
1658
+ }
1659
+ if (base.kind === "sdk-module" && !sdkEndpoint2(base.provider, [...modulePath(base), ...chain.names])) {
1660
+ ctx.names.set(target, { kind: "sdk-client", provider: base.provider, module: base.module });
1661
+ }
1662
+ };
1663
+ for (const assign of root.descendantsOfType("assignment")) {
1664
+ const left = assign?.childForFieldName("left");
1665
+ const right = assign?.childForFieldName("right");
1666
+ if (left && right && (left.type === "identifier" || left.type === "attribute")) visit(left.text, right);
1667
+ }
1668
+ for (const item of root.descendantsOfType("as_pattern")) {
1669
+ const value2 = item?.namedChildren[0];
1670
+ const alias = item?.childForFieldName("alias")?.namedChildren[0] ?? item?.namedChildren.at(-1);
1671
+ if (value2 && alias && alias.type === "identifier") visit(alias.text, value2);
1672
+ }
1673
+ }
1674
+ function httpCall(ctx, call) {
1675
+ const fn = call.childForFieldName("function");
1676
+ const chain = fn ? chainOf2(fn) : void 0;
1677
+ if (!chain || chain.names.length === 0) {
1678
+ return void 0;
1679
+ }
1680
+ const binding = ctx.names.get(chain.root) ?? ctx.names.get(chain.prefix(chain.names.length - 1));
1681
+ const member = chain.names.at(-1);
1682
+ if (!binding || binding.kind !== "http-module" && binding.kind !== "http-client") return void 0;
1683
+ if (binding.kind === "http-module" && chain.names.length !== 1) return void 0;
1684
+ const args = call.childForFieldName("arguments");
1685
+ const positional = (args?.namedChildren ?? []).filter((a) => a && a.type !== "keyword_argument");
1686
+ let method;
1687
+ let urlNode;
1688
+ if (VERBS2.has(member)) {
1689
+ method = member.toUpperCase();
1690
+ urlNode = positional[0] ?? keyword(args, "url");
1691
+ } else if (member === "request") {
1692
+ const m = evalString2(ctx, positional[0] ?? keyword(args, "method"))?.toUpperCase();
1693
+ method = m && HTTP_METHODS.includes(m) ? m : null;
1694
+ urlNode = positional[1] ?? keyword(args, "url");
1695
+ } else return void 0;
1696
+ const base = binding.kind === "http-client" ? binding.baseUrl : void 0;
1697
+ const { host, path: path13, query } = parseUrl(joinUrl2(base, evalString2(ctx, urlNode)));
1698
+ const surface = emptySurface2();
1699
+ surface.headers = [...binding.kind === "http-client" ? binding.headers : [], ...dictEntries(ctx, keyword(args, "headers"))].map((h) => ({ ...h, name: h.name.toLowerCase() }));
1700
+ surface.query = [...query, ...dictEntries(ctx, keyword(args, "params"))];
1701
+ const provider = host ? providerForHost(host)?.id ?? null : null;
1702
+ return { file: "", line: 0, column: 0, provider, kind: "http", client: binding.lib, method, host, endpoint: path13, resolved: Boolean(provider && path13 && method), surface };
1703
+ }
1704
+ function sdkCall(ctx, call) {
1705
+ const fn = call.childForFieldName("function");
1706
+ const chain = fn ? chainOf2(fn) : void 0;
1707
+ if (!chain || chain.names.length === 0) return void 0;
1708
+ const binding = ctx.names.get(chain.root);
1709
+ if (!binding || binding.kind !== "sdk-module" && binding.kind !== "sdk-client") return void 0;
1710
+ const names = binding.kind === "sdk-module" ? [...modulePath(binding), ...chain.names] : chain.names;
1711
+ const ep = sdkEndpoint2(binding.provider, names);
1712
+ if (!ep && (binding.kind === "sdk-module" || names.length < 2)) return void 0;
1713
+ const surface = emptySurface2();
1714
+ const host = binding.provider.hosts.find((h) => !h.startsWith("*.")) ?? null;
1715
+ return {
1716
+ file: "",
1717
+ line: 0,
1718
+ column: 0,
1719
+ provider: binding.provider.id,
1720
+ kind: "sdk",
1721
+ client: binding.module.split(".")[0],
1722
+ operation: names.join("."),
1723
+ method: ep?.method ?? null,
1724
+ host,
1725
+ endpoint: ep?.path ?? null,
1726
+ resolved: Boolean(ep),
1727
+ surface
1728
+ };
1729
+ }
1730
+ function modulePath(b) {
1731
+ return b.module.split(".").slice(1);
1732
+ }
1733
+ function sdkEndpoint2(provider, names) {
1734
+ let op = names.join(".");
1735
+ if (provider.id === "stripe") op = op.replace(/^v1\./, "");
1736
+ if (provider.id === "slack") {
1737
+ const last = names.at(-1);
1738
+ if (names.length !== 1 || !last || !/^[a-z]+(_[a-zA-Z0-9]+)+$/.test(last)) return void 0;
1739
+ if (last === "files_upload_v2") return { method: "POST", path: "/api/files.completeUploadExternal" };
1740
+ return { method: "POST", path: `/api/${last.replace(/_/g, ".")}` };
1741
+ }
1742
+ const ref = getProvider(provider.id)?.python?.methods?.[op];
1743
+ if (!ref) return void 0;
1744
+ const [method, path13] = ref.split(" ");
1745
+ return { method, path: path13 };
1746
+ }
1747
+ var NOT_FIELDS2 = /* @__PURE__ */ new Set([
1748
+ "keys",
1749
+ "values",
1750
+ "items",
1751
+ "append",
1752
+ "extend",
1753
+ "lower",
1754
+ "upper",
1755
+ "strip",
1756
+ "split",
1757
+ "startswith",
1758
+ "endswith",
1759
+ "replace",
1760
+ "format",
1761
+ "encode",
1762
+ "decode",
1763
+ "count",
1764
+ "index",
1765
+ "copy",
1766
+ "pop",
1767
+ "update",
1768
+ "sort"
1769
+ ]);
1770
+ var SAME_DATA = /* @__PURE__ */ new Set(["to_dict", "to_dict_recursive", "model_dump", "dict"]);
1771
+ var MAX_DEPTH2 = 4;
1772
+ var Collector2 = class {
1773
+ fields = /* @__PURE__ */ new Set();
1774
+ statusCodes = /* @__PURE__ */ new Set();
1775
+ checksOk = false;
1776
+ enums = /* @__PURE__ */ new Map();
1777
+ responseHeaders = /* @__PURE__ */ new Set();
1778
+ };
1779
+ function addResponseSurface2(call, site) {
1780
+ const c = new Collector2();
1781
+ const envelope = site.kind === "sdk" ? "body" : "response";
1782
+ bindFrom2(call, { space: envelope, path: [] }, site.client, c, 0);
1783
+ const s = site.surface;
1784
+ s.fields = [...c.fields].sort();
1785
+ s.statusCodes = [...c.statusCodes].sort((a, b) => a - b);
1786
+ s.checksOk = c.checksOk;
1787
+ s.enums = [...c.enums].map(([field, values]) => ({ field, values: [...values].sort() })).sort((a, b) => a.field.localeCompare(b.field));
1788
+ s.responseHeaders = [...c.responseHeaders].sort();
1789
+ }
1790
+ function bindFrom2(expr, loc, lib, c, depth) {
1791
+ if (depth > MAX_DEPTH2) return;
1792
+ let cur = expr;
1793
+ while (cur.parent && (cur.parent.type === "await" || cur.parent.type === "parenthesized_expression")) cur = cur.parent;
1794
+ const parent = cur.parent;
1795
+ if (!parent) return;
1796
+ if (parent.type === "assignment" && parent.childForFieldName("right")?.id === cur.id) {
1797
+ const left = parent.childForFieldName("left");
1798
+ if (left?.type === "identifier") bindName2(left.text, parent, loc, lib, c, depth);
1799
+ return;
1800
+ }
1801
+ if (parent.type === "as_pattern" && parent.namedChildren[0]?.id === cur.id) {
1802
+ const alias = parent.childForFieldName("alias")?.namedChildren[0] ?? parent.namedChildren.at(-1);
1803
+ if (alias?.type === "identifier") bindName2(alias.text, parent, loc, lib, c, depth);
1804
+ return;
1805
+ }
1806
+ use2(cur, loc, lib, c, depth);
1807
+ }
1808
+ function bindName2(name, decl, loc, lib, c, depth) {
1809
+ for (const ref of referencesOf2(name, decl)) use2(ref, loc, lib, c, depth);
1810
+ }
1811
+ function referencesOf2(name, decl) {
1812
+ let scope = decl.parent;
1813
+ while (scope && scope.type !== "function_definition" && scope.type !== "lambda" && scope.type !== "module") scope = scope.parent;
1814
+ if (!scope) return [];
1815
+ return scope.descendantsOfType("identifier").filter((id) => {
1816
+ if (!id || id.text !== name || id.startIndex < decl.endIndex) return false;
1817
+ const p = id.parent;
1818
+ if (p?.type === "attribute" && p.childForFieldName("attribute")?.id === id.id) return false;
1819
+ if (p?.type === "keyword_argument" && p.childForFieldName("name")?.id === id.id) return false;
1820
+ if (p?.type === "assignment" && p.childForFieldName("left")?.id === id.id) return false;
1821
+ return true;
1822
+ });
1823
+ }
1824
+ function use2(ref, loc, lib, c, depth) {
1825
+ const access = [];
1826
+ let cur = ref;
1827
+ for (; ; ) {
1828
+ const p = cur.parent;
1829
+ if (!p) break;
1830
+ if (p.type === "attribute" && p.childForFieldName("object")?.id === cur.id) {
1831
+ const name = p.childForFieldName("attribute")?.text ?? "";
1832
+ const call = p.parent?.type === "call" && p.parent.childForFieldName("function")?.id === p.id ? p.parent : void 0;
1833
+ if (call && name === "get" && (loc.space === "body" || access.length > 0)) {
1834
+ const key = stringValue(call.childForFieldName("arguments")?.namedChildren[0] ?? void 0);
1835
+ if (key === void 0) break;
1836
+ access.push(key);
1837
+ cur = call;
1838
+ continue;
1839
+ }
1840
+ if (call && SAME_DATA.has(name) && loc.space === "body") {
1841
+ cur = call;
1842
+ continue;
1843
+ }
1844
+ access.push(name);
1845
+ cur = p;
1846
+ } else if (p.type === "subscript" && p.childForFieldName("value")?.id === cur.id) {
1847
+ const key = stringValue(p.childForFieldName("subscript") ?? void 0);
1848
+ access.push(key ?? "[]");
1849
+ cur = p;
1850
+ } else if (p.type === "parenthesized_expression" || p.type === "await") {
1851
+ cur = p;
1852
+ } else break;
1853
+ }
1854
+ interpret2(loc, access, cur, lib, c, depth);
1855
+ }
1856
+ function interpret2(loc, access, top, lib, c, depth) {
1857
+ const full = [...loc.path, ...access];
1858
+ if (loc.space === "body") return body2(full, top, lib, c, depth);
1859
+ const [first, ...rest] = full;
1860
+ if (first === void 0) return;
1861
+ const isCall = top.parent?.type === "call" && top.parent.childForFieldName("function")?.id === top.id;
1862
+ const statusKey = lib === "aiohttp" ? "status" : "status_code";
1863
+ if (first === statusKey && rest.length === 0) {
1864
+ for (const n of comparedLiterals2(top).nums) c.statusCodes.add(n);
1865
+ return;
1866
+ }
1867
+ if (first === "ok" && rest.length === 0 || first === "raise_for_status" && isCall) {
1868
+ c.checksOk = true;
1869
+ return;
1870
+ }
1871
+ if (first === "headers") {
1872
+ if (rest[0]) c.responseHeaders.add(rest[0].toLowerCase());
1873
+ return;
1874
+ }
1875
+ if (first === "json" && rest.length === 0 && isCall) bindFrom2(top.parent, { space: "body", path: [] }, lib, c, depth + 1);
1876
+ }
1877
+ function body2(path13, top, lib, c, depth) {
1878
+ const cut = path13.findIndex((p) => NOT_FIELDS2.has(p));
1879
+ const fieldPath = cut >= 0 ? path13.slice(0, cut) : path13;
1880
+ if (fieldPath.length === 0) {
1881
+ if (path13.length === 0 && depth < MAX_DEPTH2) iterate(top, { space: "body", path: [] }, lib, c, depth);
1882
+ return;
1883
+ }
1884
+ const field = fieldPath.join(".").replace(/\.\[\]/g, "[]");
1885
+ c.fields.add(field);
1886
+ if (cut < 0) {
1887
+ for (const s of comparedLiterals2(top).strs) {
1888
+ const set = c.enums.get(field) ?? /* @__PURE__ */ new Set();
1889
+ set.add(s);
1890
+ c.enums.set(field, set);
1891
+ }
1892
+ if (depth < MAX_DEPTH2) {
1893
+ const parent = top.parent;
1894
+ if (parent?.type === "assignment" && parent.childForFieldName("right")?.id === top.id) {
1895
+ const left = parent.childForFieldName("left");
1896
+ if (left?.type === "identifier") bindName2(left.text, parent, { space: "body", path: fieldPath }, lib, c, depth + 1);
1897
+ }
1898
+ iterate(top, { space: "body", path: fieldPath }, lib, c, depth);
1899
+ }
1900
+ }
1901
+ }
1902
+ function iterate(top, loc, lib, c, depth) {
1903
+ const p = top.parent;
1904
+ const items = { space: "body", path: [...loc.path, "[]"] };
1905
+ if (p?.type === "for_statement" && p.childForFieldName("right")?.id === top.id) {
1906
+ const left = p.childForFieldName("left");
1907
+ if (left?.type === "identifier") bindName2(left.text, left, items, lib, c, depth + 1);
1908
+ } else if (p?.type === "for_in_clause" && p.childForFieldName("right")?.id === top.id) {
1909
+ const left = p.childForFieldName("left");
1910
+ const comp = p.parent;
1911
+ if (left?.type === "identifier" && comp) {
1912
+ for (const id of comp.descendantsOfType("identifier")) {
1913
+ if (id && id.text === left.text && id.id !== left.id) use2(id, items, lib, c, depth + 1);
1914
+ }
1915
+ }
1916
+ }
1917
+ }
1918
+ function comparedLiterals2(node) {
1919
+ const nums = [];
1920
+ const strs = [];
1921
+ let cur = node;
1922
+ while (cur.parent?.type === "parenthesized_expression") cur = cur.parent;
1923
+ const p = cur.parent;
1924
+ if (p?.type !== "comparison_operator") return { nums, strs };
1925
+ const take = (n) => {
1926
+ if (!n) return;
1927
+ if (n.type === "integer") nums.push(Number(n.text));
1928
+ else if (n.type === "string") {
1929
+ const v = stringValue(n);
1930
+ if (v !== void 0) strs.push(v);
1931
+ } else if (n.type === "tuple" || n.type === "list" || n.type === "set") n.namedChildren.forEach(take);
1932
+ };
1933
+ for (const operand of p.namedChildren) if (operand && operand.id !== cur.id) take(operand);
1934
+ return { nums, strs };
1935
+ }
1936
+ function chainOf2(node) {
1937
+ const names = [];
1938
+ let cur = node;
1939
+ while (cur.type === "attribute") {
1940
+ names.unshift(cur.childForFieldName("attribute").text);
1941
+ cur = cur.childForFieldName("object");
1942
+ }
1943
+ if (cur.type !== "identifier") return void 0;
1944
+ const root = cur.text;
1945
+ return { root, names, prefix: (n) => [root, ...names.slice(0, n)].join(".") };
1946
+ }
1947
+ function unwrap2(n) {
1948
+ let cur = n;
1949
+ while (cur.type === "await" || cur.type === "parenthesized_expression") cur = cur.namedChildren[0] ?? cur;
1950
+ return cur;
1951
+ }
1952
+ function keyword(args, name) {
1953
+ for (const a of args?.namedChildren ?? []) {
1954
+ if (a?.type === "keyword_argument" && a.childForFieldName("name")?.text === name) return a.childForFieldName("value") ?? void 0;
1955
+ }
1956
+ return void 0;
1957
+ }
1958
+ function stringValue(n) {
1959
+ if (!n || n.type !== "string") return void 0;
1960
+ if (n.namedChildren.some((c) => c?.type === "interpolation")) return void 0;
1961
+ return n.namedChildren.filter((c) => c?.type === "string_content" || c?.type === "escape_sequence").map((c) => c.text).join("");
1962
+ }
1963
+ function evalString2(ctx, node, depth = 0) {
1964
+ if (!node || depth > 8) return void 0;
1965
+ const n = unwrap2(node);
1966
+ if (n.type === "string") {
1967
+ let out = "";
1968
+ for (const part of n.namedChildren) {
1969
+ if (!part) continue;
1970
+ if (part.type === "string_content" || part.type === "escape_sequence") out += part.text;
1971
+ else if (part.type === "interpolation") {
1972
+ const inner = part.childForFieldName("expression") ?? part.namedChildren[0];
1973
+ out += (inner && evalString2(ctx, inner, depth + 1)) ?? placeholder2(inner ?? void 0);
1974
+ }
1975
+ }
1976
+ return out;
1977
+ }
1978
+ if (n.type === "concatenated_string") return n.namedChildren.map((s) => evalString2(ctx, s ?? void 0, depth + 1) ?? "").join("");
1979
+ if (n.type === "binary_operator" && n.childForFieldName("operator")?.text === "+") {
1980
+ const l = evalString2(ctx, n.childForFieldName("left") ?? void 0, depth + 1);
1981
+ const r = evalString2(ctx, n.childForFieldName("right") ?? void 0, depth + 1);
1982
+ if (l === void 0 && r === void 0) return void 0;
1983
+ return (l ?? placeholder2(n.childForFieldName("left") ?? void 0)) + (r ?? placeholder2(n.childForFieldName("right") ?? void 0));
1984
+ }
1985
+ if (n.type === "identifier") {
1986
+ const init = ctx.consts.get(n.text);
1987
+ return init && init.id !== node.id ? evalString2(ctx, init, depth + 1) : void 0;
1988
+ }
1989
+ if (n.type === "call") {
1990
+ const fn = n.childForFieldName("function")?.text;
1991
+ const args = (n.childForFieldName("arguments")?.namedChildren ?? []).filter(Boolean);
1992
+ if ((fn === "os.getenv" || fn === "getenv" || fn === "os.environ.get") && args[1]) return evalString2(ctx, args[1], depth + 1);
1993
+ }
1994
+ return void 0;
1995
+ }
1996
+ function placeholder2(n) {
1997
+ if (!n) return "{}";
1998
+ const e = unwrap2(n);
1999
+ if (e.type === "identifier") return `{${e.text}}`;
2000
+ if (e.type === "attribute") return `{${e.childForFieldName("attribute")?.text ?? ""}}`;
2001
+ const args = e.type === "call" ? (e.childForFieldName("arguments")?.namedChildren ?? []).filter(Boolean) : [];
2002
+ if (args.length === 1) return placeholder2(args[0]);
2003
+ return "{}";
2004
+ }
2005
+ function dictEntries(ctx, node, depth = 0) {
2006
+ if (!node || depth > 3) return [];
2007
+ const n = unwrap2(node);
2008
+ if (n.type === "identifier") {
2009
+ const init = ctx.consts.get(n.text);
2010
+ return init ? dictEntries(ctx, init, depth + 1) : [];
2011
+ }
2012
+ if (n.type === "call" && n.childForFieldName("function")?.text === "dict") {
2013
+ return (n.childForFieldName("arguments")?.namedChildren ?? []).filter((a) => a?.type === "keyword_argument").map((a) => ({ name: a.childForFieldName("name").text, value: literal(ctx, a.childForFieldName("value") ?? void 0) }));
2014
+ }
2015
+ if (n.type !== "dictionary") return [];
2016
+ const out = [];
2017
+ for (const pair of n.namedChildren) {
2018
+ if (pair?.type !== "pair") continue;
2019
+ const key = stringValue(pair.childForFieldName("key") ?? void 0);
2020
+ if (key !== void 0) out.push({ name: key, value: literal(ctx, pair.childForFieldName("value") ?? void 0) });
2021
+ }
2022
+ return out;
2023
+ }
2024
+ function literal(ctx, n) {
2025
+ if (!n) return null;
2026
+ if (n.type === "integer" || n.type === "float") return n.text;
2027
+ if (n.type === "true" || n.type === "false") return n.text.toLowerCase();
2028
+ return evalString2(ctx, n) ?? null;
2029
+ }
2030
+ function joinUrl2(base, url) {
2031
+ if (url === void 0) return base;
2032
+ if (!base || /^[a-z]+:\/\//i.test(url)) return url;
2033
+ return base.replace(/\/+$/, "") + "/" + url.replace(/^\/+/, "");
2034
+ }
2035
+ function emptySurface2() {
2036
+ return { fields: [], statusCodes: [], checksOk: false, enums: [], headers: [], query: [], responseHeaders: [] };
2037
+ }
2038
+
2039
+ // packages/extractor/src/index.ts
2040
+ var SCRIPT_KINDS = {
2041
+ ".ts": ScriptKind.TS,
2042
+ ".mts": ScriptKind.TS,
2043
+ ".cts": ScriptKind.TS,
2044
+ ".tsx": ScriptKind.TSX,
2045
+ ".js": ScriptKind.JS,
2046
+ ".mjs": ScriptKind.JS,
2047
+ ".cjs": ScriptKind.JS,
2048
+ ".jsx": ScriptKind.JSX
2049
+ };
2050
+ function extractCallSites(rootDir) {
2051
+ const root = path4.resolve(rootDir);
2052
+ const { sources, manifests, python, pythonManifests } = listRepoFiles(root);
2053
+ const project = new Project({
2054
+ useInMemoryFileSystem: true,
2055
+ skipFileDependencyResolution: true,
2056
+ compilerOptions: { allowJs: true }
2057
+ });
2058
+ const found = [];
2059
+ for (const abs of sources) {
2060
+ const rel = toPosix(path4.relative(root, abs));
2061
+ const sf = project.createSourceFile(`/${rel}`, readFileSync3(abs, "utf8"), {
2062
+ overwrite: true,
2063
+ scriptKind: SCRIPT_KINDS[path4.extname(abs)] ?? ScriptKind.TS
2064
+ });
2065
+ for (const f of extractFromFile(sf, rel)) {
2066
+ addResponseSurface(f);
2067
+ found.push(f);
2068
+ }
2069
+ }
2070
+ found.sort((a, b) => a.site.file.localeCompare(b.site.file) || a.site.line - b.site.line || a.site.column - b.site.column);
2071
+ const skipped = [];
2072
+ const pythonSites = [];
2073
+ for (const abs of python) {
2074
+ const rel = toPosix(path4.relative(root, abs));
2075
+ if (!pythonParserLoaded()) {
2076
+ skipped.push({ file: rel, reason: "the Python parser was not loaded" });
2077
+ continue;
2078
+ }
2079
+ try {
2080
+ pythonSites.push(...extractPython(readFileSync3(abs, "utf8"), rel));
2081
+ } catch (err) {
2082
+ skipped.push({ file: rel, reason: err instanceof Error ? err.message : String(err) });
2083
+ }
2084
+ }
2085
+ const callSites = [...found.map((f) => f.site), ...pythonSites].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
2086
+ return {
2087
+ root,
2088
+ filesScanned: sources.length + python.length - skipped.length,
2089
+ callSites,
2090
+ declared: [...declaredProviders(root, manifests), ...declaredPythonProviders(root, pythonManifests)],
2091
+ found,
2092
+ skipped
2093
+ };
2094
+ }
2095
+
2096
+ // packages/shapes/src/shape.ts
2097
+ import { createHash as createHash2, createHmac } from "crypto";
2098
+ import { z as z4 } from "zod";
2099
+ var JSON_TYPES = ["array", "boolean", "integer", "null", "number", "object", "string"];
2100
+ var VALUE_CLASSES = [
2101
+ "datetime",
2102
+ "date",
2103
+ "uuid",
2104
+ "url",
2105
+ "email",
2106
+ "id",
2107
+ "token",
2108
+ "text",
2109
+ "positive",
2110
+ "negative",
2111
+ "zero",
2112
+ "true",
2113
+ "false"
2114
+ ];
2115
+ var MAX_ENUM_VALUES = 50;
2116
+ var FieldShapeSchema = z4.object({
2117
+ /** Field path, dot-separated, array items as "[]". Omitted when the shape is redacted. */
2118
+ path: z4.string().optional(),
2119
+ /** HMAC of the path with the provider salt; stable across observations. */
2120
+ pathHash: z4.string(),
2121
+ types: z4.array(z4.enum(JSON_TYPES)),
2122
+ classes: z4.array(z4.enum(VALUE_CLASSES)),
2123
+ /** How many times the field appeared (more than once inside arrays). */
2124
+ occurrences: z4.number().int().nonnegative(),
2125
+ /** Share of occurrences that were null. */
2126
+ nullRate: z4.number().min(0).max(1),
2127
+ /** Array lengths seen, for array fields. */
2128
+ arrayLength: z4.object({ min: z4.number().int(), max: z4.number().int() }).optional(),
2129
+ /** HMACs of distinct token values (enum candidates), never the values themselves. */
2130
+ valueHashes: z4.array(z4.string()).optional()
2131
+ });
2132
+ var ResponseShapeSchema = z4.object({
2133
+ status: z4.number().int(),
2134
+ /** Allowlisted headers whose values describe the contract, not the payload. */
2135
+ headers: z4.record(z4.string(), z4.string()),
2136
+ /** Names of the other non-volatile headers present. */
2137
+ headerNames: z4.array(z4.string()),
2138
+ body: z4.object({
2139
+ kind: z4.enum(["empty", "json", "text"]),
2140
+ rootType: z4.enum(JSON_TYPES).optional(),
2141
+ fields: z4.array(FieldShapeSchema)
2142
+ })
2143
+ });
2144
+ var VALUE_HEADERS = /* @__PURE__ */ new Set([
2145
+ "content-type",
2146
+ "deprecation",
2147
+ "sunset",
2148
+ "link-deprecation",
2149
+ "x-github-api-version-selected",
2150
+ "stripe-version",
2151
+ "x-api-version",
2152
+ "api-version",
2153
+ "openai-version",
2154
+ "anthropic-version",
2155
+ "intuit-minor-version"
2156
+ ]);
2157
+ var VOLATILE_HEADERS = /* @__PURE__ */ new Set([
2158
+ "date",
2159
+ "age",
2160
+ "etag",
2161
+ "last-modified",
2162
+ "expires",
2163
+ "set-cookie",
2164
+ "cf-ray",
2165
+ "server-timing",
2166
+ "x-request-id",
2167
+ "request-id",
2168
+ "x-github-request-id",
2169
+ "x-runtime",
2170
+ "x-served-by",
2171
+ "x-cache",
2172
+ "x-cache-hits",
2173
+ "x-timer",
2174
+ "content-length",
2175
+ "connection",
2176
+ "keep-alive",
2177
+ "via",
2178
+ "report-to",
2179
+ "nel",
2180
+ "idempotency-key",
2181
+ "original-request",
2182
+ // transport and caching: vary with chunking, compression and proxies, not with the API contract
2183
+ "transfer-encoding",
2184
+ "accept-ranges",
2185
+ "content-encoding",
2186
+ "vary",
2187
+ "alt-svc",
2188
+ "cache-control",
2189
+ // token metadata: present only on authenticated calls, value changes per token
2190
+ "github-authentication-token-expiration"
2191
+ ]);
2192
+ function providerSalt(provider, secret) {
2193
+ return createHmac("sha256", secret).update(provider).digest("hex");
2194
+ }
2195
+ function hmac(salt, value2) {
2196
+ return createHmac("sha256", salt).update(value2).digest("hex").slice(0, 16);
2197
+ }
2198
+ function inferShape(res, opts) {
2199
+ const headers = {};
2200
+ const headerNames = [];
2201
+ const entries = res.headers instanceof Headers ? [...res.headers.entries()] : Object.entries(res.headers);
2202
+ for (const [rawName, value2] of entries) {
2203
+ const name = rawName.toLowerCase();
2204
+ if (VALUE_HEADERS.has(name)) headers[name] = name === "content-type" ? value2.split(";")[0].trim() : value2;
2205
+ else if (!VOLATILE_HEADERS.has(name) && !name.startsWith("x-ratelimit") && !name.startsWith("ratelimit")) headerNames.push(name);
2206
+ }
2207
+ const text = res.body ?? "";
2208
+ let body3;
2209
+ if (text.trim() === "") {
2210
+ body3 = { kind: "empty", fields: [] };
2211
+ } else {
2212
+ let parsed;
2213
+ let isJson = true;
2214
+ try {
2215
+ parsed = JSON.parse(text);
2216
+ } catch {
2217
+ isJson = false;
2218
+ }
2219
+ if (isJson) {
2220
+ const acc = /* @__PURE__ */ new Map();
2221
+ walk(parsed, "", acc);
2222
+ body3 = { kind: "json", rootType: typeOf(parsed), fields: finish(acc, opts.salt) };
2223
+ } else {
2224
+ body3 = { kind: "text", fields: [] };
2225
+ }
2226
+ }
2227
+ return {
2228
+ status: res.status,
2229
+ headers: Object.fromEntries(Object.entries(headers).sort(([a], [b]) => cmp3(a, b))),
2230
+ headerNames: [...new Set(headerNames)].sort(),
2231
+ body: body3
2232
+ };
2233
+ }
2234
+ function walk(value2, path13, acc) {
2235
+ if (path13 !== "") record(value2, path13, acc);
2236
+ if (Array.isArray(value2)) {
2237
+ for (const item of value2) walk(item, `${path13}[]`, acc);
2238
+ } else if (value2 !== null && typeof value2 === "object") {
2239
+ for (const [key, child] of Object.entries(value2)) walk(child, path13 === "" ? key : `${path13}.${key}`, acc);
2240
+ }
2241
+ }
2242
+ function record(value2, path13, acc) {
2243
+ let a = acc.get(path13);
2244
+ if (!a) acc.set(path13, a = { types: /* @__PURE__ */ new Set(), classes: /* @__PURE__ */ new Set(), occurrences: 0, nulls: 0, tokens: /* @__PURE__ */ new Set(), textValues: 0 });
2245
+ a.occurrences++;
2246
+ const type = typeOf(value2);
2247
+ a.types.add(type);
2248
+ if (type === "null") a.nulls++;
2249
+ if (Array.isArray(value2)) {
2250
+ a.lengths = a.lengths ? { min: Math.min(a.lengths.min, value2.length), max: Math.max(a.lengths.max, value2.length) } : { min: value2.length, max: value2.length };
2251
+ }
2252
+ const cls = classify(value2);
2253
+ if (cls) a.classes.add(cls);
2254
+ if (cls === "token" && typeof value2 === "string") a.tokens.add(value2);
2255
+ }
2256
+ function finish(acc, salt) {
2257
+ return [...acc.entries()].sort(([a], [b]) => cmp3(a, b)).map(([path13, a]) => {
2258
+ const field = {
2259
+ path: path13,
2260
+ pathHash: hmac(salt, path13),
2261
+ types: [...a.types].sort(),
2262
+ classes: [...a.classes].sort(),
2263
+ occurrences: a.occurrences,
2264
+ nullRate: round(a.nulls / a.occurrences)
2265
+ };
2266
+ if (a.lengths) field.arrayLength = a.lengths;
2267
+ if (a.tokens.size > 0 && a.tokens.size <= MAX_ENUM_VALUES) {
2268
+ field.valueHashes = [...a.tokens].map((v) => hmac(salt, `${path13}=${v}`)).sort();
2269
+ }
2270
+ return field;
2271
+ });
2272
+ }
2273
+ function typeOf(value2) {
2274
+ if (value2 === null) return "null";
2275
+ if (Array.isArray(value2)) return "array";
2276
+ if (typeof value2 === "number") return Number.isInteger(value2) ? "integer" : "number";
2277
+ if (typeof value2 === "boolean") return "boolean";
2278
+ if (typeof value2 === "string") return "string";
2279
+ return "object";
2280
+ }
2281
+ function classify(value2) {
2282
+ if (typeof value2 === "boolean") return value2 ? "true" : "false";
2283
+ if (typeof value2 === "number") return value2 > 0 ? "positive" : value2 < 0 ? "negative" : "zero";
2284
+ if (typeof value2 !== "string") return void 0;
2285
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/.test(value2)) return "datetime";
2286
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value2)) return "date";
2287
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value2)) return "uuid";
2288
+ if (/^https?:\/\//i.test(value2)) return "url";
2289
+ if (/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value2)) return "email";
2290
+ if (/^[a-z]{2,8}_(?=[A-Za-z0-9]*\d)[A-Za-z0-9]{8,}$/.test(value2) || /^\d{5,}$/.test(value2) || /^[0-9a-f]{20,}$/i.test(value2)) return "id";
2291
+ if (/^[A-Za-z][A-Za-z0-9_.-]{0,39}$/.test(value2)) return "token";
2292
+ return "text";
2293
+ }
2294
+ function shapeHash(shape) {
2295
+ const structural = {
2296
+ status: shape.status,
2297
+ headers: shape.headers,
2298
+ headerNames: shape.headerNames,
2299
+ body: {
2300
+ kind: shape.body.kind,
2301
+ rootType: shape.body.rootType ?? null,
2302
+ fields: shape.body.fields.map((f) => [f.pathHash, f.types, f.classes, f.nullRate > 0, f.valueHashes ?? null])
2303
+ }
2304
+ };
2305
+ return createHash2("sha256").update(JSON.stringify(structural)).digest("hex");
2306
+ }
2307
+ function round(n) {
2308
+ return Math.round(n * 1e3) / 1e3;
2309
+ }
2310
+ function cmp3(a, b) {
2311
+ return a < b ? -1 : a > b ? 1 : 0;
2312
+ }
2313
+
2314
+ // packages/shapes/src/store.ts
2315
+ import { mkdirSync as mkdirSync2 } from "fs";
2316
+ import path5 from "path";
2317
+ import Database from "better-sqlite3";
2318
+ var SCHEMA_VERSION = 1;
2319
+ var ShapeStore = class _ShapeStore {
2320
+ constructor(db) {
2321
+ this.db = db;
2322
+ }
2323
+ db;
2324
+ /** Open (and create or migrate) a store. Use ":memory:" for tests. */
2325
+ static open(file) {
2326
+ if (file !== ":memory:") mkdirSync2(path5.dirname(path5.resolve(file)), { recursive: true });
2327
+ const db = new Database(file);
2328
+ if (file !== ":memory:") db.pragma("journal_mode = WAL");
2329
+ const version = db.pragma("user_version", { simple: true });
2330
+ if (version < SCHEMA_VERSION) {
2331
+ db.exec(`
2332
+ CREATE TABLE IF NOT EXISTS observations (
2333
+ id INTEGER PRIMARY KEY,
2334
+ provider TEXT NOT NULL,
2335
+ method TEXT NOT NULL,
2336
+ endpoint TEXT NOT NULL,
2337
+ observed_at TEXT NOT NULL,
2338
+ source TEXT NOT NULL,
2339
+ shape_hash TEXT NOT NULL,
2340
+ shape_json TEXT NOT NULL
2341
+ );
2342
+ CREATE INDEX IF NOT EXISTS observations_endpoint
2343
+ ON observations (provider, method, endpoint, observed_at);
2344
+ `);
2345
+ db.pragma(`user_version = ${SCHEMA_VERSION}`);
2346
+ }
2347
+ return new _ShapeStore(db);
2348
+ }
2349
+ record(obs) {
2350
+ const shape = ResponseShapeSchema.parse(obs.shape);
2351
+ const hash = shapeHash(shape);
2352
+ const info = this.db.prepare(
2353
+ `INSERT INTO observations (provider, method, endpoint, observed_at, source, shape_hash, shape_json)
2354
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
2355
+ ).run(obs.provider, obs.method, obs.endpoint, obs.observedAt, obs.source, hash, JSON.stringify(shape));
2356
+ return { ...obs, shape, id: Number(info.lastInsertRowid), shapeHash: hash };
2357
+ }
2358
+ /**
2359
+ * Observations of one endpoint, newest first. Pass `source` to stay within one
2360
+ * vantage point: the same endpoint answers differently with and without a token,
2361
+ * so observations from different sources must not be compared.
2362
+ */
2363
+ history(provider, method, endpoint, opts = {}) {
2364
+ const rows = this.db.prepare(
2365
+ `SELECT * FROM observations WHERE provider = ? AND method = ? AND endpoint = ? AND (? IS NULL OR source = ?)
2366
+ ORDER BY observed_at DESC, id DESC LIMIT ?`
2367
+ ).all(provider, method, endpoint, opts.source ?? null, opts.source ?? null, opts.limit ?? 100);
2368
+ return rows.map(fromRow);
2369
+ }
2370
+ latest(provider, method, endpoint, source) {
2371
+ return this.history(provider, method, endpoint, { limit: 1, source })[0];
2372
+ }
2373
+ /** The most recent observation whose shape differs from the latest one: the "before" side of the last change. */
2374
+ previousDistinct(provider, method, endpoint, source) {
2375
+ const latest = this.latest(provider, method, endpoint, source);
2376
+ if (!latest) return void 0;
2377
+ const row = this.db.prepare(
2378
+ `SELECT * FROM observations WHERE provider = ? AND method = ? AND endpoint = ? AND (? IS NULL OR source = ?)
2379
+ AND shape_hash != ?
2380
+ ORDER BY observed_at DESC, id DESC LIMIT 1`
2381
+ ).get(provider, method, endpoint, source ?? null, source ?? null, latest.shapeHash);
2382
+ return row ? fromRow(row) : void 0;
2383
+ }
2384
+ /** One row per endpoint and source. */
2385
+ endpoints() {
2386
+ return this.db.prepare(
2387
+ `SELECT provider, method, endpoint, source, COUNT(*) AS observations, COUNT(DISTINCT shape_hash) AS distinctShapes,
2388
+ MAX(observed_at) AS lastObservedAt
2389
+ FROM observations GROUP BY provider, method, endpoint, source ORDER BY provider, endpoint, method, source`
2390
+ ).all();
2391
+ }
2392
+ close() {
2393
+ this.db.close();
2394
+ }
2395
+ };
2396
+ function fromRow(row) {
2397
+ return {
2398
+ id: row.id,
2399
+ provider: row.provider,
2400
+ method: row.method,
2401
+ endpoint: row.endpoint,
2402
+ observedAt: row.observed_at,
2403
+ source: row.source,
2404
+ shapeHash: row.shape_hash,
2405
+ shape: ResponseShapeSchema.parse(JSON.parse(row.shape_json))
2406
+ };
2407
+ }
2408
+
2409
+ // packages/shapes/src/diff.ts
2410
+ var IMPACT_ORDER = { breaking: 0, "non-breaking": 1, informational: 2 };
2411
+ var CONTRACT_HEADERS = /* @__PURE__ */ new Set(["content-type"]);
2412
+ var SUNSET_HEADERS = /* @__PURE__ */ new Set(["deprecation", "sunset", "link-deprecation"]);
2413
+ function diffShapes(before, after) {
2414
+ const changes = [];
2415
+ const ok = (s) => s >= 200 && s < 300;
2416
+ if (before.status !== after.status) changes.push(statusChange(before.status, after.status));
2417
+ if (before.body.kind !== after.body.kind) {
2418
+ const lost = before.body.kind === "json" && after.body.kind !== "json";
2419
+ changes.push({
2420
+ kind: "body-kind",
2421
+ impact: lost && ok(after.status) ? "breaking" : "informational",
2422
+ ...lost && ok(after.status) ? { changeClass: "field-rename" } : {},
2423
+ from: before.body.kind,
2424
+ to: after.body.kind,
2425
+ detail: `body ${before.body.kind} \u2192 ${after.body.kind}`
2426
+ });
2427
+ }
2428
+ if (ok(before.status) === ok(after.status) && before.body.kind === "json" && after.body.kind === "json") {
2429
+ changes.push(...fieldChanges(before.body.fields, after.body.fields));
2430
+ }
2431
+ changes.push(...headerChanges(before, after));
2432
+ changes.sort((a, b) => IMPACT_ORDER[a.impact] - IMPACT_ORDER[b.impact] || (a.path ?? "").localeCompare(b.path ?? ""));
2433
+ return { changes, breaking: changes.some((c) => c.impact === "breaking") };
2434
+ }
2435
+ function statusChange(from, to) {
2436
+ const ok = (s) => s >= 200 && s < 300;
2437
+ const base = { kind: "status-code", from, to, detail: `status ${from} \u2192 ${to}` };
2438
+ if (ok(from) && ok(to)) return { ...base, impact: "breaking", changeClass: "status-code" };
2439
+ if (ok(from) && (to === 401 || to === 403)) return { ...base, impact: "breaking", changeClass: "auth" };
2440
+ if (ok(from) && (to === 404 || to === 410)) return { ...base, impact: "breaking", changeClass: "version-sunset" };
2441
+ return { ...base, impact: "informational" };
2442
+ }
2443
+ function label(f) {
2444
+ return f.path ?? `#${f.pathHash}`;
2445
+ }
2446
+ function fieldChanges(before, after) {
2447
+ const out = [];
2448
+ const prev = new Map(before.map((f) => [f.pathHash, f]));
2449
+ const next = new Map(after.map((f) => [f.pathHash, f]));
2450
+ const removedPaths = [];
2451
+ const addedPaths = [];
2452
+ const under = (path13, roots) => path13 !== void 0 && roots.some((r) => path13.startsWith(`${r}.`) || path13.startsWith(`${r}[]`));
2453
+ for (const f of before) {
2454
+ if (next.has(f.pathHash)) continue;
2455
+ if (under(f.path, removedPaths)) continue;
2456
+ if (f.path) removedPaths.push(f.path);
2457
+ out.push({
2458
+ kind: "field-removed",
2459
+ impact: "breaking",
2460
+ changeClass: "field-rename",
2461
+ path: f.path,
2462
+ pathHash: f.pathHash,
2463
+ from: f.types,
2464
+ detail: `field ${label(f)} removed`
2465
+ });
2466
+ }
2467
+ for (const f of after) {
2468
+ const old = prev.get(f.pathHash);
2469
+ if (!old) {
2470
+ if (under(f.path, addedPaths)) continue;
2471
+ if (f.path) addedPaths.push(f.path);
2472
+ out.push({
2473
+ kind: "field-added",
2474
+ impact: "non-breaking",
2475
+ path: f.path,
2476
+ pathHash: f.pathHash,
2477
+ to: f.types,
2478
+ detail: `field ${label(f)} added (${f.types.join(" | ")})`
2479
+ });
2480
+ continue;
2481
+ }
2482
+ const where = { path: f.path ?? old.path, pathHash: f.pathHash };
2483
+ const name = f.path ?? old.path ?? `#${f.pathHash}`;
2484
+ const newTypes = f.types.filter((t) => !old.types.includes(t));
2485
+ const goneTypes = old.types.filter((t) => !f.types.includes(t));
2486
+ const nonNullNew = newTypes.filter((t) => t !== "null");
2487
+ if (nonNullNew.length > 0) {
2488
+ out.push({
2489
+ kind: "type-changed",
2490
+ impact: "breaking",
2491
+ changeClass: "field-rename",
2492
+ ...where,
2493
+ from: old.types,
2494
+ to: f.types,
2495
+ detail: `field ${name} type ${old.types.join(" | ")} \u2192 ${f.types.join(" | ")}`
2496
+ });
2497
+ } else if (newTypes.includes("null")) {
2498
+ out.push({
2499
+ kind: "became-nullable",
2500
+ impact: "breaking",
2501
+ changeClass: "required-field",
2502
+ ...where,
2503
+ from: old.types,
2504
+ to: f.types,
2505
+ detail: `field ${name} can now be null`
2506
+ });
2507
+ } else if (goneTypes.includes("null") && goneTypes.length === 1) {
2508
+ out.push({ kind: "no-longer-nullable", impact: "informational", ...where, detail: `field ${name} no longer seen as null` });
2509
+ }
2510
+ const added = (f.valueHashes ?? []).filter((h) => !(old.valueHashes ?? []).includes(h));
2511
+ if (enumLike(old) && added.length > 0) {
2512
+ out.push({
2513
+ kind: "enum-value-added",
2514
+ impact: "breaking",
2515
+ changeClass: "enum-added",
2516
+ ...where,
2517
+ to: added,
2518
+ detail: `field ${name} has ${added.length} value${added.length === 1 ? "" : "s"} not seen before`
2519
+ });
2520
+ }
2521
+ const newClasses = f.classes.filter((c) => !old.classes.includes(c));
2522
+ if (nonNullNew.length === 0 && newClasses.length > 0 && old.classes.length > 0 && !newClasses.every((c) => ["positive", "negative", "zero", "true", "false"].includes(c))) {
2523
+ out.push({
2524
+ kind: "class-changed",
2525
+ impact: "informational",
2526
+ ...where,
2527
+ from: old.classes,
2528
+ to: f.classes,
2529
+ detail: `field ${name} values look like ${f.classes.join(", ")} (were ${old.classes.join(", ")})`
2530
+ });
2531
+ }
2532
+ }
2533
+ return out;
2534
+ }
2535
+ function enumLike(f) {
2536
+ const distinct = f.valueHashes?.length ?? 0;
2537
+ return distinct > 0 && f.occurrences >= 2 * distinct;
2538
+ }
2539
+ function headerChanges(before, after) {
2540
+ const out = [];
2541
+ const names = /* @__PURE__ */ new Set([...Object.keys(before.headers), ...Object.keys(after.headers)]);
2542
+ for (const name of [...names].sort()) {
2543
+ const from = before.headers[name];
2544
+ const to = after.headers[name];
2545
+ if (from === to) continue;
2546
+ if (SUNSET_HEADERS.has(name) && from === void 0) {
2547
+ out.push({ kind: "header-added", impact: "informational", changeClass: "version-sunset", path: name, to, detail: `response now carries ${name}: ${to}` });
2548
+ } else if (from === void 0 || to === void 0) {
2549
+ out.push({
2550
+ kind: from === void 0 ? "header-added" : "header-removed",
2551
+ impact: "informational",
2552
+ path: name,
2553
+ ...from !== void 0 ? { from } : {},
2554
+ ...to !== void 0 ? { to } : {},
2555
+ detail: from === void 0 ? `header ${name}: ${to} added` : `header ${name} removed`
2556
+ });
2557
+ } else {
2558
+ const contract = CONTRACT_HEADERS.has(name) && before.status === after.status;
2559
+ out.push({
2560
+ kind: "header-changed",
2561
+ impact: contract ? "breaking" : "informational",
2562
+ ...contract ? { changeClass: "field-rename" } : {},
2563
+ path: name,
2564
+ from,
2565
+ to,
2566
+ detail: `header ${name}: ${from} \u2192 ${to}`
2567
+ });
2568
+ }
2569
+ }
2570
+ const prevNames = new Set(before.headerNames);
2571
+ const nextNames = new Set(after.headerNames);
2572
+ for (const name of after.headerNames) {
2573
+ if (!prevNames.has(name)) out.push({ kind: "header-added", impact: "informational", path: name, detail: `header ${name} added` });
2574
+ }
2575
+ for (const name of before.headerNames) {
2576
+ if (!nextNames.has(name)) out.push({ kind: "header-removed", impact: "informational", path: name, detail: `header ${name} removed` });
2577
+ }
2578
+ return out;
2579
+ }
2580
+
2581
+ // packages/canary/src/index.ts
2582
+ import { Command, CommanderError } from "commander";
2583
+
2584
+ // packages/canary/src/draft.ts
2585
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
2586
+ import path6 from "path";
2587
+
2588
+ // packages/canary/src/replay.ts
2589
+ import { readFileSync as readFileSync4 } from "fs";
2590
+ import { z as z5 } from "zod";
2591
+ var RecordingSchema = z5.object({
2592
+ provider: z5.string(),
2593
+ method: z5.enum(HTTP_METHODS),
2594
+ endpoint: z5.string().startsWith("/"),
2595
+ note: z5.string().optional(),
2596
+ sources: z5.array(z5.url()).default([]),
2597
+ observations: z5.array(
2598
+ z5.object({
2599
+ observedAt: z5.iso.datetime(),
2600
+ status: z5.number().int(),
2601
+ headers: z5.record(z5.string(), z5.string()).default({}),
2602
+ body: z5.string().default("")
2603
+ })
2604
+ ).min(2)
2605
+ });
2606
+ function loadRecording(file) {
2607
+ return RecordingSchema.parse(JSON.parse(readFileSync4(file, "utf8")));
2608
+ }
2609
+ function replay(recording, store, secret) {
2610
+ const salt = providerSalt(recording.provider, secret);
2611
+ const { provider, method, endpoint } = recording;
2612
+ const observations = recording.observations.map(
2613
+ (o) => store.record({
2614
+ provider,
2615
+ method,
2616
+ endpoint,
2617
+ observedAt: o.observedAt,
2618
+ source: "replay",
2619
+ shape: inferShape({ status: o.status, headers: o.headers, body: o.body }, { salt })
2620
+ })
2621
+ );
2622
+ const after = store.latest(provider, method, endpoint, "replay");
2623
+ const before = store.previousDistinct(provider, method, endpoint, "replay");
2624
+ return { recording, observations, after, before, ...before ? { diff: diffShapes(before.shape, after.shape) } : {} };
2625
+ }
2626
+
2627
+ // packages/repair/src/recipes/apply-fix.ts
2628
+ import { Node as Node3, SyntaxKind as SyntaxKind3 } from "ts-morph";
2629
+ var applyFix = {
2630
+ id: "apply-fix",
2631
+ changeClass: "version-sunset",
2632
+ description: "Apply the advisory's fix: the new path, query value or header, and the renamed fields and enum values",
2633
+ edits({ sourceFile, call, advisory }) {
2634
+ const fix2 = advisory.fix;
2635
+ if (!fix2) return [];
2636
+ const where = (n) => `${sourceFile.getFilePath().slice(1)}:${n.getStartLineNumber()}`;
2637
+ const edits = [...requestEdits(call, sourceFile, fix2, where), ...headerEdits(call, fix2, where)];
2638
+ const scope = enclosingFunction(call) ?? sourceFile;
2639
+ edits.push(...fieldEdits(scope, fix2, where, edits), ...enumEdits(scope, fix2, where));
2640
+ return edits;
2641
+ }
2642
+ };
2643
+ var TEXT_KINDS = [SyntaxKind3.StringLiteral, SyntaxKind3.NoSubstitutionTemplateLiteral, SyntaxKind3.TemplateHead, SyntaxKind3.TemplateMiddle, SyntaxKind3.TemplateTail];
2644
+ function requestEdits(call, sf, fix2, where) {
2645
+ if (!fix2.path && !fix2.query) return [];
2646
+ const edits = [];
2647
+ for (const node of urlTextNodes(call, sf)) {
2648
+ const before = node.getText();
2649
+ let after = before;
2650
+ if (fix2.path) {
2651
+ after = after.split(fix2.path.from).join(fix2.path.to);
2652
+ const from = fix2.path.from.replace(/\/$/, "");
2653
+ const to = fix2.path.to.replace(/\/$/, "");
2654
+ if (from !== fix2.path.from) after = after.replace(new RegExp(`${escapeRe(from)}(?=["'\`]|\\$\\{)`, "g"), to);
2655
+ }
2656
+ if (fix2.query) {
2657
+ const re = new RegExp(`([?&])${escapeRe(fix2.query.name)}=([^&#\`'"$]+)`, "g");
2658
+ after = after.replace(re, (_m, sep) => `${sep}${fix2.query.name}=${fix2.query.value}`);
2659
+ }
2660
+ if (after !== before) edits.push({ start: node.getStart(), end: node.getEnd(), text: after, note: `${where(node)} \`${before}\` \u2192 \`${after}\`` });
2661
+ }
2662
+ if (fix2.query) {
2663
+ for (const prop of [...objectProperties(call, "params"), ...objectProperties(call, "searchParams")]) {
2664
+ if (propName2(prop) !== fix2.query.name || !Node3.isPropertyAssignment(prop)) continue;
2665
+ const init = prop.getInitializer();
2666
+ if (!init || !(Node3.isNumericLiteral(init) || Node3.isStringLiteral(init))) continue;
2667
+ const text = /^\d+(\.\d+)?$/.test(fix2.query.value) && Node3.isNumericLiteral(init) ? fix2.query.value : JSON.stringify(fix2.query.value);
2668
+ if (init.getText() !== text) edits.push({ start: init.getStart(), end: init.getEnd(), text, note: `${where(init)} ${fix2.query.name}: \`${init.getText()}\` \u2192 \`${text}\`` });
2669
+ }
2670
+ }
2671
+ return edits;
2672
+ }
2673
+ function headerEdits(call, fix2, where) {
2674
+ if (!fix2.header) return [];
2675
+ const edits = [];
2676
+ const text = JSON.stringify(fix2.header.value);
2677
+ for (const prop of objectProperties(call, "headers")) {
2678
+ if (propName2(prop)?.toLowerCase() !== fix2.header.name.toLowerCase() || !Node3.isPropertyAssignment(prop)) continue;
2679
+ const init = prop.getInitializer();
2680
+ if (!init || !(Node3.isStringLiteral(init) || Node3.isNoSubstitutionTemplateLiteral(init)) || init.getLiteralText() === fix2.header.value) continue;
2681
+ edits.push({ start: init.getStart(), end: init.getEnd(), text, note: `${where(init)} ${fix2.header.name}: \`${init.getText()}\` \u2192 \`${text}\`` });
2682
+ }
2683
+ return edits;
2684
+ }
2685
+ function fieldEdits(scope, fix2, where, taken) {
2686
+ const edits = [];
2687
+ for (const { from, to } of fix2.fields ?? []) {
2688
+ const oldName = leaf(from);
2689
+ const newName = leaf(to);
2690
+ if (oldName === newName) continue;
2691
+ for (const access of scope.getDescendantsOfKind(SyntaxKind3.PropertyAccessExpression)) {
2692
+ if (access.getName() !== oldName || isWrite(access) || overlaps(access, [...taken, ...edits])) continue;
2693
+ const target = access.getExpression().getText();
2694
+ const dot = access.hasQuestionDotToken() ? "?." : ".";
2695
+ const text = `(${target}${dot}${newName} ?? ${target}${dot}${oldName})`;
2696
+ edits.push({ start: access.getStart(), end: access.getEnd(), text, note: `${where(access)} \`${access.getText()}\` \u2192 \`${text}\`` });
2697
+ }
2698
+ }
2699
+ return edits;
2700
+ }
2701
+ function enumEdits(scope, fix2, where) {
2702
+ const edits = [];
2703
+ for (const { field, values } of fix2.enums ?? []) {
2704
+ const name = leaf(field);
2705
+ const renamed = new Map(values.map((v) => [v.from, v.to]));
2706
+ const isField = (n) => Node3.isPropertyAccessExpression(n) && n.getName() === name || Node3.isIdentifier(n) && n.getText() === name;
2707
+ for (const bin of scope.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
2708
+ const op = bin.getOperatorToken().getKind();
2709
+ const negated = op === SyntaxKind3.ExclamationEqualsEqualsToken || op === SyntaxKind3.ExclamationEqualsToken;
2710
+ if (!negated && op !== SyntaxKind3.EqualsEqualsEqualsToken && op !== SyntaxKind3.EqualsEqualsToken) continue;
2711
+ const [lit, other] = Node3.isStringLiteral(bin.getRight()) ? [bin.getRight(), bin.getLeft()] : [bin.getLeft(), bin.getRight()];
2712
+ if (!Node3.isStringLiteral(lit) || !isField(other)) continue;
2713
+ const to = renamed.get(lit.getLiteralText());
2714
+ if (to === void 0) continue;
2715
+ const text = `${negated ? "!" : ""}${JSON.stringify([lit.getLiteralText(), to])}.includes(${other.getText()})`;
2716
+ edits.push({ start: bin.getStart(), end: bin.getEnd(), text, note: `${where(bin)} \`${bin.getText()}\` \u2192 \`${text}\`` });
2717
+ }
2718
+ for (const sw of scope.getDescendantsOfKind(SyntaxKind3.SwitchStatement)) {
2719
+ if (!isField(sw.getExpression())) continue;
2720
+ const cases = sw.getClauses().filter(Node3.isCaseClause);
2721
+ const present = new Set(cases.map((c) => c.getExpression()).filter(Node3.isStringLiteral).map((l) => l.getLiteralText()));
2722
+ for (const c of cases) {
2723
+ const e = c.getExpression();
2724
+ const to = Node3.isStringLiteral(e) ? renamed.get(e.getLiteralText()) : void 0;
2725
+ if (to === void 0 || present.has(to)) continue;
2726
+ present.add(to);
2727
+ const indent = " ".repeat(c.getSourceFile().getLineAndColumnAtPos(c.getStart()).column - 1);
2728
+ edits.push({ start: c.getStart(), end: c.getStart(), text: `case ${JSON.stringify(to)}:
2729
+ ${indent}`, note: `${where(c)} add case ${JSON.stringify(to)}` });
2730
+ }
2731
+ }
2732
+ for (const inc of scope.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
2733
+ const callee = inc.getExpression();
2734
+ if (!Node3.isPropertyAccessExpression(callee) || callee.getName() !== "includes" || !isField(inc.getArguments()[0] ?? inc)) continue;
2735
+ const arr = callee.getExpression();
2736
+ if (!Node3.isArrayLiteralExpression(arr)) continue;
2737
+ const items = arr.getElements().filter(Node3.isStringLiteral).map((l) => l.getLiteralText());
2738
+ const add = items.map((v) => renamed.get(v)).filter((v) => v !== void 0 && !items.includes(v));
2739
+ if (!add.length) continue;
2740
+ const text = JSON.stringify([...items, ...new Set(add)]);
2741
+ edits.push({ start: arr.getStart(), end: arr.getEnd(), text, note: `${where(arr)} \`${arr.getText()}\` \u2192 \`${text}\`` });
2742
+ }
2743
+ }
2744
+ return edits;
2745
+ }
2746
+ function urlTextNodes(call, sf) {
2747
+ const out = /* @__PURE__ */ new Set();
2748
+ const seen = /* @__PURE__ */ new Set();
2749
+ const visit = (n, depth) => {
2750
+ if (!n || depth > 4 || seen.has(n)) return;
2751
+ seen.add(n);
2752
+ if (TEXT_KINDS.includes(n.getKind())) out.add(n);
2753
+ if (Node3.isIdentifier(n)) {
2754
+ const decl = sf.getVariableDeclaration(n.getText());
2755
+ if (decl?.getVariableStatement()?.getDeclarationKind() === "const") visit(decl.getInitializer(), depth + 1);
2756
+ return;
2757
+ }
2758
+ n.forEachChild((c) => visit(c, depth));
2759
+ };
2760
+ const [first] = call.getArguments();
2761
+ if (first && Node3.isObjectLiteralExpression(first)) {
2762
+ const url = first.getProperty("url");
2763
+ if (url && Node3.isPropertyAssignment(url)) visit(url.getInitializer(), 0);
2764
+ } else visit(first, 0);
2765
+ for (const prop of sf.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) if (propName2(prop) === "baseURL") visit(prop.getInitializer(), 0);
2766
+ return [...out];
2767
+ }
2768
+ function objectProperties(call, name) {
2769
+ const out = [];
2770
+ for (const arg of call.getArguments()) {
2771
+ if (!Node3.isObjectLiteralExpression(arg)) continue;
2772
+ const prop = arg.getProperties().find((p) => propName2(p) === name);
2773
+ const value2 = prop && Node3.isPropertyAssignment(prop) ? prop.getInitializer() : void 0;
2774
+ if (value2 && Node3.isObjectLiteralExpression(value2)) out.push(...value2.getProperties());
2775
+ }
2776
+ return out;
2777
+ }
2778
+ function propName2(p) {
2779
+ if (!(Node3.isPropertyAssignment(p) || Node3.isShorthandPropertyAssignment(p))) return void 0;
2780
+ const n = p.getNameNode();
2781
+ return Node3.isStringLiteral(n) || Node3.isNoSubstitutionTemplateLiteral(n) ? n.getLiteralText() : n.getText();
2782
+ }
2783
+ function enclosingFunction(call) {
2784
+ return call.getFirstAncestor((n) => Node3.isFunctionDeclaration(n) || Node3.isArrowFunction(n) || Node3.isFunctionExpression(n) || Node3.isMethodDeclaration(n));
2785
+ }
2786
+ function isWrite(access) {
2787
+ const p = access.getParent();
2788
+ return Node3.isBinaryExpression(p) && p.getLeft() === access && p.getOperatorToken().getKind() === SyntaxKind3.EqualsToken;
2789
+ }
2790
+ function overlaps(n, edits) {
2791
+ return edits.some((e) => n.getStart() < e.end && e.start < n.getEnd());
2792
+ }
2793
+ function leaf(path13) {
2794
+ return path13.split(".").pop().replace(/\[\]/g, "");
2795
+ }
2796
+ function escapeRe(s) {
2797
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2798
+ }
2799
+
2800
+ // packages/repair/src/recipes/status-code-accept-set.ts
2801
+ import { Node as Node4, SyntaxKind as SyntaxKind4 } from "ts-morph";
2802
+ var STATUS_PROPS = /* @__PURE__ */ new Set(["status", "statusCode"]);
2803
+ var statusCodeAcceptSet = {
2804
+ id: "status-code-accept-set",
2805
+ changeClass: "status-code",
2806
+ description: "Accept the new status codes alongside the old ones wherever the response status is compared",
2807
+ edits({ sourceFile, call, advisory }) {
2808
+ const oldCodes = advisory.trigger.statusCodes ?? [];
2809
+ const accept = [.../* @__PURE__ */ new Set([...oldCodes, ...advisory.trigger.newStatusCodes ?? []])].sort((a, b) => a - b);
2810
+ if (oldCodes.length === 0 || accept.length === oldCodes.length) return [];
2811
+ const list = `[${accept.join(", ")}]`;
2812
+ const edits = [];
2813
+ const where = (n) => `${sourceFile.getFilePath().slice(1)}:${n.getStartLineNumber()}`;
2814
+ for (const status of statusExpressions(call)) {
2815
+ let cur = status;
2816
+ while (Node4.isParenthesizedExpression(cur.getParent())) cur = cur.getParent();
2817
+ const parent = cur.getParent();
2818
+ if (Node4.isBinaryExpression(parent)) {
2819
+ const op = parent.getOperatorToken().getKind();
2820
+ const other = parent.getLeft() === cur ? parent.getRight() : parent.getLeft();
2821
+ if (!Node4.isNumericLiteral(other) || !oldCodes.includes(Number(other.getLiteralValue()))) continue;
2822
+ const negated = op === SyntaxKind4.ExclamationEqualsEqualsToken || op === SyntaxKind4.ExclamationEqualsToken;
2823
+ const equal = op === SyntaxKind4.EqualsEqualsEqualsToken || op === SyntaxKind4.EqualsEqualsToken;
2824
+ if (!negated && !equal) continue;
2825
+ const text = `${negated ? "!" : ""}${list}.includes(${status.getText()})`;
2826
+ edits.push({ start: parent.getStart(), end: parent.getEnd(), text, note: `${where(parent)} \`${parent.getText()}\` \u2192 \`${text}\`` });
2827
+ } else if (Node4.isSwitchStatement(parent) && parent.getExpression() === cur) {
2828
+ const cases = parent.getClauses().filter(Node4.isCaseClause);
2829
+ const present = new Set(cases.map((c) => c.getExpression()).filter(Node4.isNumericLiteral).map((l) => Number(l.getLiteralValue())));
2830
+ const anchor = cases.find((c) => {
2831
+ const e = c.getExpression();
2832
+ return Node4.isNumericLiteral(e) && oldCodes.includes(Number(e.getLiteralValue()));
2833
+ });
2834
+ const missing = accept.filter((c) => !present.has(c));
2835
+ if (!anchor || missing.length === 0) continue;
2836
+ const indent = " ".repeat(sourceFile.getLineAndColumnAtPos(anchor.getStart()).column - 1);
2837
+ const text = missing.map((c) => `case ${c}:
2838
+ ${indent}`).join("");
2839
+ edits.push({ start: anchor.getStart(), end: anchor.getStart(), text, note: `${where(anchor)} add ${missing.map((c) => `case ${c}`).join(", ")}` });
2840
+ } else if (Node4.isCallExpression(parent) && parent.getArguments()[0] === cur) {
2841
+ const callee = parent.getExpression();
2842
+ const arr = Node4.isPropertyAccessExpression(callee) && callee.getName() === "includes" ? callee.getExpression() : void 0;
2843
+ if (!arr || !Node4.isArrayLiteralExpression(arr)) continue;
2844
+ const values = arr.getElements().filter(Node4.isNumericLiteral).map((l) => Number(l.getLiteralValue()));
2845
+ if (!values.some((v) => oldCodes.includes(v))) continue;
2846
+ const merged = [.../* @__PURE__ */ new Set([...values, ...accept])].sort((a, b) => a - b);
2847
+ if (merged.length === values.length) continue;
2848
+ const text = `[${merged.join(", ")}]`;
2849
+ edits.push({ start: arr.getStart(), end: arr.getEnd(), text, note: `${where(arr)} \`${arr.getText()}\` \u2192 \`${text}\`` });
2850
+ }
2851
+ }
2852
+ return edits;
2853
+ }
2854
+ };
2855
+ function statusExpressions(call) {
2856
+ const out = [];
2857
+ let cur = call;
2858
+ for (; ; ) {
2859
+ const p = cur.getParent();
2860
+ if (p && (Node4.isAwaitExpression(p) || Node4.isParenthesizedExpression(p) || Node4.isAsExpression(p) || Node4.isNonNullExpression(p))) cur = p;
2861
+ else break;
2862
+ }
2863
+ const parent = cur.getParent();
2864
+ const sf = call.getSourceFile();
2865
+ if (Node4.isPropertyAccessExpression(parent) && parent.getExpression() === cur && STATUS_PROPS.has(parent.getName())) out.push(parent);
2866
+ if (Node4.isPropertyAccessExpression(parent) && parent.getName() === "then") {
2867
+ const cb = Node4.isCallExpression(parent.getParent()) ? parent.getParent().getArguments()[0] : void 0;
2868
+ const param = cb && (Node4.isArrowFunction(cb) || Node4.isFunctionExpression(cb)) ? cb.getParameters()[0] : void 0;
2869
+ if (param) out.push(...fromBinding(param.getNameNode(), param, sf));
2870
+ }
2871
+ if (Node4.isVariableDeclaration(parent) && parent.getInitializer() === cur) out.push(...fromBinding(parent.getNameNode(), parent, sf));
2872
+ return out;
2873
+ }
2874
+ function fromBinding(name, decl, sf) {
2875
+ const out = [];
2876
+ if (Node4.isIdentifier(name)) {
2877
+ for (const ref of references(name.getText(), decl, sf)) {
2878
+ const p = ref.getParent();
2879
+ if (Node4.isPropertyAccessExpression(p) && p.getExpression() === ref && STATUS_PROPS.has(p.getName())) out.push(p);
2880
+ }
2881
+ } else if (Node4.isObjectBindingPattern(name)) {
2882
+ for (const el of name.getElements()) {
2883
+ const prop = el.getPropertyNameNode()?.getText() ?? el.getName();
2884
+ if (STATUS_PROPS.has(prop) && Node4.isIdentifier(el.getNameNode())) out.push(...references(el.getName(), decl, sf));
2885
+ }
2886
+ }
2887
+ return out;
2888
+ }
2889
+ function references(name, decl, sf) {
2890
+ const scope = decl.getFirstAncestor(
2891
+ (n) => Node4.isBlock(n) || Node4.isArrowFunction(n) || Node4.isFunctionExpression(n) || Node4.isFunctionDeclaration(n) || Node4.isMethodDeclaration(n) || Node4.isCaseClause(n)
2892
+ ) ?? sf;
2893
+ return scope.getDescendantsOfKind(SyntaxKind4.Identifier).filter((id) => {
2894
+ if (id.getText() !== name || id.getStart() < decl.getEnd()) return false;
2895
+ const p = id.getParent();
2896
+ if (Node4.isPropertyAccessExpression(p) && p.getNameNode() === id) return false;
2897
+ if (Node4.isPropertyAssignment(p) && p.getNameNode() === id) return false;
2898
+ return !(Node4.isBindingElement(p) || Node4.isVariableDeclaration(p) || Node4.isParameterDeclaration(p));
2899
+ });
2900
+ }
2901
+
2902
+ // packages/repair/src/recipe.ts
2903
+ import { Project as Project2, ScriptKind as ScriptKind2, SyntaxKind as SyntaxKind5 } from "ts-morph";
2904
+ function findCall(sf, site) {
2905
+ return sf.getDescendantsOfKind(SyntaxKind5.CallExpression).filter((c) => {
2906
+ const pos = sf.getLineAndColumnAtPos(c.getStart());
2907
+ return pos.line === site.line && pos.column === site.column;
2908
+ }).sort((a, b) => a.getWidth() - b.getWidth())[0];
2909
+ }
2910
+ function applyEdits(text, edits) {
2911
+ let out = text;
2912
+ for (const e of [...edits].sort((a, b) => b.start - a.start)) out = out.slice(0, e.start) + e.text + out.slice(e.end);
2913
+ return out;
2914
+ }
2915
+ function runRecipe(recipes, advisory, callSite, text) {
2916
+ const recipe = advisory.recipeId ? recipes[advisory.recipeId] : void 0;
2917
+ if (!recipe) return { status: "no-recipe", reason: advisory.recipeId ? `unknown recipe ${advisory.recipeId}` : "advisory names no recipe" };
2918
+ const project = new Project2({ useInMemoryFileSystem: true, skipFileDependencyResolution: true, compilerOptions: { allowJs: true } });
2919
+ const kind = /\.(c|m)?jsx?$/.test(callSite.file) ? ScriptKind2.JS : callSite.file.endsWith("x") ? ScriptKind2.TSX : ScriptKind2.TS;
2920
+ const sourceFile = project.createSourceFile(`/${callSite.file}`, text, { scriptKind: kind });
2921
+ const call = findCall(sourceFile, callSite);
2922
+ if (!call) return { status: "call-not-found", recipe: recipe.id, reason: `no call at ${callSite.file}:${callSite.line}:${callSite.column}` };
2923
+ const edits = recipe.edits({ sourceFile, call, callSite, advisory });
2924
+ if (edits.length === 0) return { status: "no-change", recipe: recipe.id, reason: "recipe found nothing to change" };
2925
+ return {
2926
+ status: "applied",
2927
+ recipe: recipe.id,
2928
+ change: { file: callSite.file, before: text, after: applyEdits(text, edits), notes: edits.map((e) => e.note) }
2929
+ };
2930
+ }
2931
+
2932
+ // packages/repair/src/contract.ts
2933
+ import path7 from "path";
2934
+ import { Node as Node5, Project as Project3, ScriptKind as ScriptKind3, SyntaxKind as SyntaxKind6 } from "ts-morph";
2935
+
2936
+ // packages/repair/src/synth.ts
2937
+ function synthesizeBody(shape) {
2938
+ if (shape.body.kind !== "json") return null;
2939
+ const root = { children: /* @__PURE__ */ new Map() };
2940
+ for (const field of shape.body.fields) {
2941
+ if (field.path === void 0) throw new Error("cannot synthesize from a redacted shape (field paths removed)");
2942
+ let node = root;
2943
+ for (const token of tokens(field.path)) {
2944
+ if (token === "[]") node = node.item ??= { children: /* @__PURE__ */ new Map() };
2945
+ else {
2946
+ let child = node.children.get(token);
2947
+ if (!child) node.children.set(token, child = { children: /* @__PURE__ */ new Map() });
2948
+ node = child;
2949
+ }
2950
+ }
2951
+ node.field = field;
2952
+ }
2953
+ return value(root, shape.body.rootType === "array" ? "array" : "object");
2954
+ }
2955
+ function tokens(path13) {
2956
+ const out = [];
2957
+ for (const part of path13.split(".")) {
2958
+ const m = /^([^[]*)((?:\[\])*)$/.exec(part);
2959
+ if (m[1]) out.push(m[1]);
2960
+ for (let i = 0; i < m[2].length / 2; i++) out.push("[]");
2961
+ }
2962
+ return out;
2963
+ }
2964
+ function value(node, forced) {
2965
+ const types = node.field?.types.filter((t) => t !== "null") ?? [];
2966
+ const kind = forced ?? (node.item || types.includes("array") ? "array" : node.children.size > 0 || types.includes("object") ? "object" : types[0]);
2967
+ if (kind === "array") return node.item ? [value(node.item)] : [];
2968
+ if (kind === "object") return Object.fromEntries([...node.children].map(([k, child]) => [k, value(child)]));
2969
+ const classes = node.field?.classes ?? [];
2970
+ switch (kind) {
2971
+ case "integer":
2972
+ return classes.includes("negative") ? -1 : classes.includes("zero") && !classes.includes("positive") ? 0 : 1;
2973
+ case "number":
2974
+ return classes.includes("negative") ? -1.5 : 1.5;
2975
+ case "boolean":
2976
+ return !classes.includes("false") || classes.includes("true");
2977
+ case "string":
2978
+ return sampleString(classes);
2979
+ default:
2980
+ return null;
2981
+ }
2982
+ }
2983
+ function sampleString(classes) {
2984
+ if (classes.includes("datetime")) return "2026-01-01T00:00:00Z";
2985
+ if (classes.includes("date")) return "2026-01-01";
2986
+ if (classes.includes("uuid")) return "00000000-0000-4000-8000-000000000000";
2987
+ if (classes.includes("url")) return "https://example.com/shapi";
2988
+ if (classes.includes("email")) return "shapi@example.com";
2989
+ if (classes.includes("id")) return "id_shapi00000001";
2990
+ if (classes.includes("token")) return "shapi_value";
2991
+ return "shapi text";
2992
+ }
2993
+ function bodyFromPaths(paths, values = {}) {
2994
+ const all = [.../* @__PURE__ */ new Set([...paths, ...Object.keys(values)])];
2995
+ const isLeaf = (p) => !all.some((o) => o !== p && (o.startsWith(`${p}.`) || o.startsWith(`${p}[]`)));
2996
+ const shape = {
2997
+ status: 200,
2998
+ headers: {},
2999
+ headerNames: [],
3000
+ body: {
3001
+ kind: "json",
3002
+ rootType: all.some((p) => p.startsWith("[]")) ? "array" : "object",
3003
+ fields: all.map((path13) => ({ path: path13, pathHash: path13, types: isLeaf(path13) ? ["string"] : [], classes: [], occurrences: 1, nullRate: 0 }))
3004
+ }
3005
+ };
3006
+ const body3 = synthesizeBody(shape);
3007
+ for (const [path13, value2] of Object.entries(values)) setPath(body3, tokens(path13), value2);
3008
+ return body3;
3009
+ }
3010
+ function setPath(target, path13, value2) {
3011
+ let cur = target;
3012
+ for (let i = 0; i < path13.length; i++) {
3013
+ const t = path13[i];
3014
+ const last = i === path13.length - 1;
3015
+ if (t === "[]") {
3016
+ const arr = cur;
3017
+ if (last) arr[0] = value2;
3018
+ else cur = arr[0];
3019
+ } else {
3020
+ const obj = cur;
3021
+ if (last) obj[t] = value2;
3022
+ else cur = obj[t];
3023
+ }
3024
+ if (cur === void 0 || cur === null) return;
3025
+ }
3026
+ }
3027
+
3028
+ // packages/repair/src/contract.ts
3029
+ var CONTRACT_DIR = "test/shapi";
3030
+ var RETIRED_STATUS = 410;
3031
+ function generateContractTest(input) {
3032
+ const { callSite, advisory, before, after } = input;
3033
+ const fix2 = advisory.fix;
3034
+ const project = new Project3({ useInMemoryFileSystem: true, skipFileDependencyResolution: true, compilerOptions: { allowJs: true } });
3035
+ const sf = project.createSourceFile(`/${callSite.file}`, input.sourceText, {
3036
+ scriptKind: /\.(c|m)?jsx?$/.test(callSite.file) ? ScriptKind3.JS : ScriptKind3.TS
3037
+ });
3038
+ const call = findCall(sf, callSite);
3039
+ if (!call) return { status: "unsupported", reason: `no call at ${callSite.file}:${callSite.line}:${callSite.column}` };
3040
+ const transport = transportFor(callSite, sf);
3041
+ if ("reason" in transport) return { status: "unsupported", reason: transport.reason };
3042
+ const fn = exportedFunction(call);
3043
+ if (!fn) return { status: "unsupported", reason: "the call site is not inside an exported function" };
3044
+ const file = input.file ?? `${CONTRACT_DIR}/${advisory.id}.test.ts`;
3045
+ const importPath = toImport(path7.posix.relative(path7.posix.dirname(file), callSite.file));
3046
+ const parts = {
3047
+ header: `// Generated by SHAPI for advisory ${advisory.id}: ${advisory.title}
3048
+ // Call site: ${callSite.file}:${callSite.line} (${callSite.method} ${callSite.endpoint}, via ${callSite.client})`,
3049
+ fn: fn.name,
3050
+ importPath,
3051
+ args: fn.params.map((p) => argument(p, sf)).join(", "),
3052
+ advisory,
3053
+ transport
3054
+ };
3055
+ if (fix2 && (fix2.path || fix2.query || fix2.header)) {
3056
+ if (callSite.kind === "sdk") return { status: "unsupported", reason: `the fix changes the request, which ${callSite.client} builds itself` };
3057
+ return { status: "generated", file, content: requestTest({ ...parts, fix: fix2, body: renamePair(callSite, fix2).after }), functionName: fn.name };
3058
+ }
3059
+ if (fix2 && (fix2.fields || fix2.enums)) {
3060
+ return { status: "generated", file, content: sameResultTest({ ...parts, ...renamePair(callSite, fix2) }), functionName: fn.name };
3061
+ }
3062
+ if (!before || !after) return { status: "unsupported", reason: "no before/after responses for this endpoint, and the advisory has no fix to build a test from" };
3063
+ try {
3064
+ return { status: "generated", file, content: recordedTest({ ...parts, before, after }), functionName: fn.name };
3065
+ } catch (err) {
3066
+ return { status: "unsupported", reason: err instanceof Error ? err.message : String(err) };
3067
+ }
3068
+ }
3069
+ function transportFor(site, sf) {
3070
+ if (site.kind === "http") {
3071
+ if (site.client === "fetch" || site.client === "ky") return { mock: "", stubFetch: true, name: "fetch" };
3072
+ if (site.client === "node-fetch") {
3073
+ return { mock: `vi.mock("node-fetch", () => ({ default: (input: RequestInfo | URL, init?: RequestInit) => globalThis.fetch(input, init) }));
3074
+ `, stubFetch: true, name: "node-fetch" };
3075
+ }
3076
+ if (site.client === "axios") return { mock: AXIOS_FAKE, stubFetch: false, name: "axios" };
3077
+ if (site.client === "got") return { mock: GOT_FAKE, stubFetch: false, name: "got" };
3078
+ return { reason: `contract tests support fetch, node-fetch, ky, axios, got and SDK call sites; this one uses ${site.client}` };
3079
+ }
3080
+ const decl = sf.getImportDeclarations().find((d) => d.getModuleSpecifierValue() === site.client);
3081
+ if (!decl) return { reason: `${site.file} does not import ${site.client} with an import statement` };
3082
+ const names = decl.getNamedImports().map((n) => n.getName());
3083
+ const exports = [`default: client`, ...names.map((n) => `${JSON.stringify(n)}: client`)].join(", ");
3084
+ const url = `https://${site.host ?? "sdk.invalid"}${site.endpoint ?? "/"}`;
3085
+ const envelope = site.provider === "github" ? `{ status: res.status, data: res.body, headers: res.headers }` : `res.body`;
3086
+ return {
3087
+ mock: `vi.mock(${JSON.stringify(site.client)}, () => {
3088
+ const call = async () => {
3089
+ const res = shapi.respond({ method: ${JSON.stringify(site.method ?? "GET")}, url: ${JSON.stringify(url)}, headers: {} });
3090
+ if (res.status >= 400) throw Object.assign(new Error(\`${site.client} request failed with \${res.status}\`), { status: res.status, statusCode: res.status, response: { status: res.status, data: res.body } });
3091
+ return ${envelope};
3092
+ };
3093
+ const client: any = new Proxy(function () {}, {
3094
+ get: (_t, prop) => (prop === "then" || typeof prop === "symbol" ? undefined : client),
3095
+ apply: () => call(),
3096
+ construct: () => client,
3097
+ });
3098
+ return { ${exports} };
3099
+ });
3100
+ `,
3101
+ stubFetch: false,
3102
+ name: `a fake ${site.client} client`
3103
+ };
3104
+ }
3105
+ function preamble(p) {
3106
+ return `import { afterEach, describe, expect, it, vi } from "vitest";
3107
+
3108
+ type ShapiRequest = { method: string; url: string; headers: Record<string, string> };
3109
+ type ShapiResponse = { status: number; body: unknown; headers: Record<string, string> };
3110
+ const shapi = vi.hoisted(() => ({
3111
+ respond: (_req: { method: string; url: string; headers: Record<string, string> }): { status: number; body: unknown; headers: Record<string, string> } => ({ status: 200, body: null, headers: {} }),
3112
+ lower: (h: unknown): Record<string, string> =>
3113
+ Object.fromEntries(Object.entries((h ?? {}) as Record<string, unknown>).filter(([, v]) => v !== undefined).map(([k, v]) => [k.toLowerCase(), String(v)])),
3114
+ }));
3115
+ ${p.transport.mock}
3116
+ import { ${p.fn} } from "${p.importPath}";
3117
+ ${p.transport.stubFetch ? `
3118
+ const shapiFetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
3119
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3120
+ const headers = Object.fromEntries(new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)).entries());
3121
+ const res = shapi.respond({ method: (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase(), url, headers });
3122
+ return new Response(res.body === null ? null : JSON.stringify(res.body), { status: res.status, headers: res.headers });
3123
+ });
3124
+ ` : ""}
3125
+ afterEach(() => {
3126
+ vi.unstubAllGlobals();
3127
+ });
3128
+
3129
+ async function run(respond: (req: ShapiRequest) => ShapiResponse): Promise<unknown> {
3130
+ shapi.respond = respond;${p.transport.stubFetch ? `
3131
+ vi.stubGlobal("fetch", shapiFetch);` : ""}
3132
+ return ${p.fn}(${p.args});
3133
+ }
3134
+ `;
3135
+ }
3136
+ var JSON_HEADERS = `{ "content-type": "application/json" }`;
3137
+ function requestTest(p) {
3138
+ const checks = [];
3139
+ if (p.fix.path) checks.push(`url.href.includes(${JSON.stringify(p.fix.path.from)})`);
3140
+ if (p.fix.query) {
3141
+ const name = JSON.stringify(p.fix.query.name);
3142
+ const below = p.advisory.trigger.query?.find((q) => q.name === p.fix.query.name)?.below;
3143
+ checks.push(
3144
+ below === void 0 ? `(url.searchParams.has(${name}) && url.searchParams.get(${name}) !== ${JSON.stringify(p.fix.query.value)})` : `(url.searchParams.has(${name}) && Number(url.searchParams.get(${name})) < ${below})`
3145
+ );
3146
+ }
3147
+ if (p.fix.header) {
3148
+ const name = JSON.stringify(p.fix.header.name.toLowerCase());
3149
+ checks.push(`(${name} in req.headers && req.headers[${name}] !== ${JSON.stringify(p.fix.header.value)})`);
3150
+ }
3151
+ return `${p.header}
3152
+ // The provider no longer accepts the old request: ${p.transport.name} is replaced by a stub that answers it
3153
+ // with ${RETIRED_STATUS}, and answers anything else with a body holding the fields the code reads. The test
3154
+ // fails until ${p.fn}() sends the new request (${describeFix(p.fix)}) and still works.
3155
+ ${preamble(p)}
3156
+ const body = ${JSON.stringify(p.body)};
3157
+
3158
+ function retired(req: ShapiRequest): boolean {
3159
+ const url = new URL(req.url, "https://shapi.invalid");
3160
+ return ${checks.join(" || ")};
3161
+ }
3162
+
3163
+ describe("SHAPI contract: ${p.advisory.id}", () => {
3164
+ it("${p.fn} sends the new request and handles the response", async () => {
3165
+ const sent: boolean[] = [];
3166
+ await run((req) => {
3167
+ const old = retired(req);
3168
+ sent.push(old);
3169
+ return old ? { status: ${RETIRED_STATUS}, body: { error: "retired request (SHAPI contract test)" }, headers: ${JSON_HEADERS} } : { status: 200, body, headers: ${JSON_HEADERS} };
3170
+ });
3171
+ expect(sent.length).toBeGreaterThan(0);
3172
+ expect(sent).not.toContain(true);
3173
+ });
3174
+ });
3175
+ `;
3176
+ }
3177
+ function sameResultTest(p) {
3178
+ return `${p.header}
3179
+ // The provider renamed fields or enum values. This test calls ${p.fn}() once with the old response and
3180
+ // once with the new one (${p.transport.name} replaced by a stub), and expects the same result: it fails
3181
+ // while the code only reads the old names.
3182
+ ${preamble(p)}
3183
+ const responses = [
3184
+ { name: "old", body: ${JSON.stringify(p.before)} },
3185
+ { name: "new", body: ${JSON.stringify(p.after)} },
3186
+ ];
3187
+
3188
+ describe("SHAPI contract: ${p.advisory.id}", () => {
3189
+ it("${p.fn} returns the same result for the old and the new response", async () => {
3190
+ const results: unknown[] = [];
3191
+ for (const r of responses) results.push(await run(() => ({ status: 200, body: r.body, headers: ${JSON_HEADERS} })));
3192
+ expect(results[1]).toEqual(results[0]);
3193
+ });
3194
+ });
3195
+ `;
3196
+ }
3197
+ function recordedTest(p) {
3198
+ const responses = [response(`before (${p.before.status})`, p.before), response(`after (${p.after.status})`, p.after)].join(",\n");
3199
+ return `${p.header}
3200
+ // Calls ${p.fn}() with ${p.transport.name} returning the response the code was written for and the one
3201
+ // ${p.advisory.provider} sends now. It fails while the code accepts only the old response.
3202
+ ${preamble(p)}
3203
+ const responses = [
3204
+ ${responses},
3205
+ ];
3206
+
3207
+ describe("SHAPI contract: ${p.advisory.id}", () => {
3208
+ for (const r of responses) {
3209
+ it(\`${p.fn} handles the \${r.name} response\`, async () => {
3210
+ const seen: ShapiRequest[] = [];
3211
+ await run((req) => (seen.push(req), { status: r.status, body: r.body, headers: r.headers }));
3212
+ expect(seen.length).toBeGreaterThan(0);
3213
+ });
3214
+ }
3215
+ });
3216
+ `;
3217
+ }
3218
+ function renamePair(site, fix2) {
3219
+ const paths = site.surface.fields;
3220
+ const rename = (p) => {
3221
+ for (const { from, to } of fix2.fields ?? []) {
3222
+ if (!/[.[]/.test(from)) {
3223
+ const segs = p.split(".");
3224
+ const i = segs.findIndex((seg) => seg.replace(/\[\]/g, "") === from);
3225
+ if (i >= 0) return [...segs.slice(0, i), segs[i].replace(from, to), ...segs.slice(i + 1)].join(".");
3226
+ } else if (p === from || p.startsWith(`${from}.`) || p.startsWith(`${from}[]`)) return to + p.slice(from.length);
3227
+ }
3228
+ return p;
3229
+ };
3230
+ const oldValues = {};
3231
+ const newValues = {};
3232
+ for (const { field, values } of fix2.enums ?? []) {
3233
+ const compared = site.surface.enums.find((e) => e.field === field)?.values ?? [];
3234
+ const pick = values.find((v) => compared.includes(v.from)) ?? values[0];
3235
+ oldValues[field] = pick.from;
3236
+ newValues[rename(field)] = pick.to;
3237
+ }
3238
+ return { before: bodyFromPaths(paths, oldValues), after: bodyFromPaths(paths.map(rename), newValues) };
3239
+ }
3240
+ function describeFix(fix2) {
3241
+ const parts = [];
3242
+ if (fix2.path) parts.push(`${fix2.path.to} instead of ${fix2.path.from}`);
3243
+ if (fix2.query) parts.push(`${fix2.query.name}=${fix2.query.value}`);
3244
+ if (fix2.header) parts.push(`${fix2.header.name}: ${fix2.header.value}`);
3245
+ return parts.join(", ");
3246
+ }
3247
+ var AXIOS_FAKE = `vi.mock("axios", () => {
3248
+ class AxiosError extends Error {
3249
+ isAxiosError = true;
3250
+ constructor(message: string, readonly response?: unknown, readonly status?: number) {
3251
+ super(message);
3252
+ }
3253
+ }
3254
+ const build = (defaults: any = {}): any => {
3255
+ const send = async (config: any) => {
3256
+ const c = { ...defaults, ...config, headers: { ...defaults.headers, ...config.headers }, params: { ...defaults.params, ...config.params } };
3257
+ let url = String(c.url ?? "");
3258
+ if (c.baseURL && !/^[a-z]+:\\/\\//i.test(url)) url = String(c.baseURL).replace(/\\/+$/, "") + "/" + url.replace(/^\\/+/, "");
3259
+ const qs = new URLSearchParams(Object.entries(c.params).filter(([, v]) => v !== undefined && v !== null).map(([k, v]) => [k, String(v)])).toString();
3260
+ if (qs) url += (url.includes("?") ? "&" : "?") + qs;
3261
+ const res = shapi.respond({ method: String(c.method ?? "get").toUpperCase(), url, headers: shapi.lower(c.headers) });
3262
+ const response = { status: res.status, statusText: "", headers: res.headers, data: res.body, config: c };
3263
+ if (res.status < 200 || res.status >= 300) throw new AxiosError(\`Request failed with status code \${res.status}\`, response, res.status);
3264
+ return response;
3265
+ };
3266
+ const instance: any = (a: any, b?: any) => send(typeof a === "string" ? { ...b, url: a } : a);
3267
+ instance.request = (c: any) => send(c);
3268
+ for (const m of ["get", "delete", "head", "options"]) instance[m] = (url: string, c: any = {}) => send({ ...c, url, method: m });
3269
+ for (const m of ["post", "put", "patch"]) instance[m] = (url: string, data: unknown, c: any = {}) => send({ ...c, url, data, method: m });
3270
+ instance.create = (d: any = {}) => build({ ...defaults, ...d, headers: { ...defaults.headers, ...d.headers } });
3271
+ instance.defaults = defaults;
3272
+ instance.interceptors = { request: { use: () => 0, eject: () => {} }, response: { use: () => 0, eject: () => {} } };
3273
+ instance.isAxiosError = (e: any) => Boolean(e?.isAxiosError);
3274
+ return instance;
3275
+ };
3276
+ const axios = build();
3277
+ return { default: axios, create: axios.create, isAxiosError: axios.isAxiosError, AxiosError };
3278
+ });
3279
+ `;
3280
+ var GOT_FAKE = `vi.mock("got", () => {
3281
+ class HTTPError extends Error {
3282
+ constructor(readonly response: any) {
3283
+ super(\`Response code \${response.statusCode}\`);
3284
+ }
3285
+ }
3286
+ const build = (defaults: any = {}): any => {
3287
+ const call = (input: any, options: any = {}) => {
3288
+ const fromObject = typeof input === "object" && !(input instanceof URL);
3289
+ const o = { ...defaults, ...(fromObject ? input : options), headers: { ...defaults.headers, ...(fromObject ? input.headers : options.headers) } };
3290
+ let url = fromObject ? String(o.url ?? "") : String(input);
3291
+ if (o.prefixUrl) url = String(o.prefixUrl).replace(/\\/+$/, "") + "/" + url.replace(/^\\/+/, "");
3292
+ const sp = o.searchParams;
3293
+ const qs = sp ? new URLSearchParams(typeof sp === "string" || sp instanceof URLSearchParams ? sp : Object.entries(sp).map(([k, v]) => [k, String(v)])).toString() : "";
3294
+ if (qs) url += (url.includes("?") ? "&" : "?") + qs;
3295
+ const res = shapi.respond({ method: String(o.method ?? "GET").toUpperCase(), url, headers: shapi.lower(o.headers) });
3296
+ const text = res.body === null ? "" : JSON.stringify(res.body);
3297
+ const response = { statusCode: res.status, headers: res.headers, body: o.responseType === "json" ? res.body : text, url };
3298
+ const failed = (o.throwHttpErrors ?? true) && res.status >= 400;
3299
+ const settle = <T,>(value: T): Promise<T> => (failed ? Promise.reject(new HTTPError(response)) : Promise.resolve(value));
3300
+ const promise: any = settle(response);
3301
+ promise.catch(() => {});
3302
+ promise.json = () => settle(res.body);
3303
+ promise.text = () => settle(text);
3304
+ promise.buffer = () => settle(Buffer.from(text));
3305
+ return promise;
3306
+ };
3307
+ const got: any = (input: any, options?: any) => call(input, options);
3308
+ for (const m of ["get", "post", "put", "patch", "delete", "head"]) got[m] = (input: any, options: any = {}) => call(input, { ...options, method: m.toUpperCase() });
3309
+ got.extend = (...opts: any[]) => build(Object.assign({}, defaults, ...opts));
3310
+ return got;
3311
+ };
3312
+ return { default: build(), HTTPError };
3313
+ });
3314
+ `;
3315
+ function response(name, shape) {
3316
+ const body3 = synthesizeBody(shape);
3317
+ const headers = shape.headers["content-type"] ? { "content-type": shape.headers["content-type"] } : {};
3318
+ return ` { name: ${JSON.stringify(name)}, status: ${shape.status}, headers: ${JSON.stringify(headers)}, body: ${JSON.stringify(body3)} }`;
3319
+ }
3320
+ function exportedFunction(call) {
3321
+ for (const anc of call.getAncestors()) {
3322
+ if (Node5.isFunctionDeclaration(anc) && anc.isExported() && anc.getName()) return { name: anc.getName(), params: anc.getParameters() };
3323
+ if (Node5.isArrowFunction(anc) || Node5.isFunctionExpression(anc)) {
3324
+ const decl = anc.getParent();
3325
+ if (Node5.isVariableDeclaration(decl) && decl.getVariableStatement()?.isExported()) return { name: decl.getName(), params: anc.getParameters() };
3326
+ }
3327
+ }
3328
+ return void 0;
3329
+ }
3330
+ function argument(p, sf) {
3331
+ const type = p.getTypeNode();
3332
+ return type ? valueFor(type, p.getName(), sf, 0) : JSON.stringify(`shapi-${p.getName()}`);
3333
+ }
3334
+ function valueFor(type, name, sf, depth) {
3335
+ if (depth > 4) return "undefined as never";
3336
+ switch (type.getKind()) {
3337
+ case SyntaxKind6.StringKeyword:
3338
+ return JSON.stringify(`shapi-${name}`);
3339
+ case SyntaxKind6.NumberKeyword:
3340
+ return "1";
3341
+ case SyntaxKind6.BooleanKeyword:
3342
+ return "true";
3343
+ }
3344
+ if (Node5.isTypeLiteral(type)) return objectFrom(type.getProperties(), sf, depth);
3345
+ if (Node5.isTypeReference(type)) {
3346
+ const ref = type.getTypeName().getText();
3347
+ const iface = sf.getInterface(ref);
3348
+ if (iface) return objectFrom(iface.getProperties(), sf, depth);
3349
+ const alias = sf.getTypeAlias(ref)?.getTypeNode();
3350
+ if (alias) return valueFor(alias, name, sf, depth + 1);
3351
+ }
3352
+ if (Node5.isArrayTypeNode(type)) return "[]";
3353
+ return "undefined as never";
3354
+ }
3355
+ function objectFrom(props, sf, depth) {
3356
+ const entries = props.filter((p) => !p.hasQuestionToken()).map((p) => {
3357
+ const t = p.getTypeNode();
3358
+ return `${JSON.stringify(p.getName())}: ${t ? valueFor(t, p.getName(), sf, depth + 1) : "undefined as never"}`;
3359
+ });
3360
+ return `{ ${entries.join(", ")} }`;
3361
+ }
3362
+ function toImport(rel) {
3363
+ const noExt = rel.replace(/\.(c|m)?[jt]sx?$/, "");
3364
+ return noExt.startsWith(".") ? noExt : `./${noExt}`;
3365
+ }
3366
+
3367
+ // packages/repair/src/agent.ts
3368
+ import path10 from "path";
3369
+
3370
+ // packages/repair/src/runner.ts
3371
+ import { spawnSync } from "child_process";
3372
+ import { existsSync as existsSync2 } from "fs";
3373
+ import { createRequire as createRequire2 } from "module";
3374
+ import path8 from "path";
3375
+ function vitestCli(dir) {
3376
+ const local = path8.join(dir, "node_modules", "vitest", "vitest.mjs");
3377
+ if (existsSync2(local)) return local;
3378
+ const require2 = createRequire2(import.meta.url);
3379
+ return path8.join(path8.dirname(require2.resolve("vitest/package.json")), "vitest.mjs");
3380
+ }
3381
+ function runTestFile(dir, file, timeoutMs = 12e4) {
3382
+ const res = spawnSync(process.execPath, [vitestCli(dir), "run", file, "--root", dir, "--reporter", "verbose", "--no-color"], {
3383
+ cwd: dir,
3384
+ encoding: "utf8",
3385
+ timeout: timeoutMs,
3386
+ env: { ...process.env, FORCE_COLOR: "0", CI: "1" }
3387
+ });
3388
+ const output = `${res.stdout ?? ""}${res.stderr ?? ""}${res.error ? `
3389
+ ${res.error.message}` : ""}`;
3390
+ const exitCode = res.status ?? 1;
3391
+ return { passed: exitCode === 0, exitCode, output };
3392
+ }
3393
+ function testsRan(output) {
3394
+ return /^\s*Tests\s+.*\d+ (?:failed|passed)/m.test(output);
3395
+ }
3396
+ function runFailure(output) {
3397
+ const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
3398
+ return lines.find((l) => /\b\w*Error: |\berror: /i.test(l)) ?? lines.find((l) => /Cannot find|not found|Error/.test(l)) ?? lines[0] ?? "no output";
3399
+ }
3400
+
3401
+ // packages/repair/src/workspace.ts
3402
+ import { cpSync, existsSync as existsSync3, mkdirSync as mkdirSync4, mkdtempSync, readFileSync as readFileSync5, symlinkSync, writeFileSync as writeFileSync3 } from "fs";
3403
+ import os from "os";
3404
+ import path9 from "path";
3405
+ var COPY_SKIP = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "coverage", ".shapi"]);
3406
+ function localWorkspace(repoDir) {
3407
+ const dir = mkdtempSync(path9.join(os.tmpdir(), "shapi-repair-"));
3408
+ cpSync(repoDir, dir, {
3409
+ recursive: true,
3410
+ filter: (src) => !COPY_SKIP.has(path9.basename(src)) || path9.resolve(src) === path9.resolve(repoDir)
3411
+ });
3412
+ const modules = path9.join(repoDir, "node_modules");
3413
+ if (existsSync3(modules)) symlinkSync(modules, path9.join(dir, "node_modules"), "junction");
3414
+ return {
3415
+ location: dir,
3416
+ async read(file) {
3417
+ const abs = path9.join(dir, file);
3418
+ return existsSync3(abs) ? readFileSync5(abs, "utf8") : void 0;
3419
+ },
3420
+ async write(file, content) {
3421
+ const abs = path9.join(dir, file);
3422
+ mkdirSync4(path9.dirname(abs), { recursive: true });
3423
+ writeFileSync3(abs, content);
3424
+ },
3425
+ async runTest(file) {
3426
+ return runTestFile(dir, file);
3427
+ }
3428
+ };
3429
+ }
3430
+
3431
+ // packages/repair/src/agent.ts
3432
+ var PROTECTED = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "coverage", ".shapi"]);
3433
+ async function repairCallSite(input) {
3434
+ const { advisory, callSite } = input;
3435
+ if (!input.workspace && !input.repoDir) throw new Error("repairCallSite needs a workspace or a repoDir");
3436
+ if (!/\.(c|m)?[jt]sx?$/.test(callSite.file)) {
3437
+ return { status: "unsupported", changes: [], attempts: [], reason: `fixes support TypeScript and JavaScript; ${callSite.file} is scanned only` };
3438
+ }
3439
+ const ws = input.workspace ?? localWorkspace(input.repoDir);
3440
+ const original = await ws.read(callSite.file);
3441
+ if (original === void 0) return { status: "unsupported", sandbox: ws.location, changes: [], attempts: [], reason: `${callSite.file} is not in the repository` };
3442
+ const test = generateContractTest({ callSite, advisory, before: input.before, after: input.after, sourceText: original, file: input.testFile });
3443
+ if (test.status !== "generated") return { status: "unsupported", changes: [], attempts: [], reason: test.reason };
3444
+ await ws.write(test.file, test.content);
3445
+ const testChange = { file: test.file, before: "", after: test.content, notes: ["generated contract test"] };
3446
+ const baseline = await ws.runTest(test.file);
3447
+ if (!baseline.passed && !testsRan(baseline.output)) {
3448
+ return {
3449
+ status: "unsupported",
3450
+ sandbox: ws.location,
3451
+ changes: [],
3452
+ test: { file: test.file, content: test.content },
3453
+ baseline,
3454
+ attempts: [],
3455
+ reason: `the contract test could not run: ${runFailure(baseline.output)}`
3456
+ };
3457
+ }
3458
+ if (baseline.passed) {
3459
+ return {
3460
+ status: "already-handled",
3461
+ sandbox: ws.location,
3462
+ changes: [],
3463
+ test: { file: test.file, content: test.content },
3464
+ baseline,
3465
+ attempts: [],
3466
+ reason: "the contract test already passes against the new response"
3467
+ };
3468
+ }
3469
+ const attempts = [];
3470
+ const recipe = runRecipe(input.recipes, advisory, callSite, original);
3471
+ if (recipe.status === "applied") {
3472
+ await ws.write(callSite.file, recipe.change.after);
3473
+ const run3 = await ws.runTest(test.file);
3474
+ attempts.push({ method: "recipe", outcome: run3.passed ? "passed" : "failed", detail: recipe.recipe, run: run3 });
3475
+ if (run3.passed) {
3476
+ return { status: "fixed", method: "recipe", sandbox: ws.location, changes: [recipe.change, testChange], test: { file: test.file, content: test.content }, baseline, attempts };
3477
+ }
3478
+ await ws.write(callSite.file, original);
3479
+ } else {
3480
+ attempts.push({ method: "recipe", outcome: "skipped", detail: recipe.reason });
3481
+ }
3482
+ if (!input.llm) {
3483
+ attempts.push({ method: "llm", outcome: "skipped", detail: "no LLM fixer configured" });
3484
+ return rejected(ws, test, baseline, attempts);
3485
+ }
3486
+ const proposal = await input.llm.fix({
3487
+ advisory,
3488
+ callSite,
3489
+ file: { path: callSite.file, content: original },
3490
+ test: { path: test.file, content: test.content },
3491
+ failingOutput: baseline.output
3492
+ });
3493
+ if (proposal.status !== "proposed") {
3494
+ attempts.push({ method: "llm", outcome: "skipped", detail: proposal.reason });
3495
+ return rejected(ws, test, baseline, attempts);
3496
+ }
3497
+ const changes = [];
3498
+ for (const f of proposal.files) {
3499
+ const rel = path10.posix.normalize(f.path.replaceAll("\\", "/"));
3500
+ if (rel.startsWith("..") || path10.posix.isAbsolute(rel) || rel === test.file || rel.split("/").some((s) => PROTECTED.has(s))) {
3501
+ attempts.push({ method: "llm", outcome: "failed", detail: `refused to write ${f.path}: outside the repo, the test, or a protected folder` });
3502
+ await restore(ws, changes);
3503
+ return rejected(ws, test, baseline, attempts);
3504
+ }
3505
+ const before = await ws.read(rel) ?? "";
3506
+ await ws.write(rel, f.content);
3507
+ changes.push({ file: rel, before, after: f.content, notes: [proposal.explanation] });
3508
+ }
3509
+ const run2 = await ws.runTest(test.file);
3510
+ attempts.push({ method: "llm", outcome: run2.passed ? "passed" : "failed", detail: `${proposal.model}: ${proposal.explanation}`, run: run2 });
3511
+ if (run2.passed) {
3512
+ return { status: "fixed", method: "llm", sandbox: ws.location, changes: [...changes, testChange], test: { file: test.file, content: test.content }, baseline, attempts };
3513
+ }
3514
+ await restore(ws, changes);
3515
+ return rejected(ws, test, baseline, attempts);
3516
+ }
3517
+ function rejected(ws, test, baseline, attempts) {
3518
+ return {
3519
+ status: "rejected",
3520
+ sandbox: ws.location,
3521
+ changes: [],
3522
+ test: { file: test.file, content: test.content },
3523
+ baseline,
3524
+ attempts,
3525
+ reason: "no change made the contract test pass"
3526
+ };
3527
+ }
3528
+ async function restore(ws, changes) {
3529
+ for (const c of changes) await ws.write(c.file, c.before);
3530
+ }
3531
+ async function repairAdvisory(input) {
3532
+ if (!input.workspace && !input.repoDir) throw new Error("repairAdvisory needs a workspace or a repoDir");
3533
+ const ws = input.workspace ?? localWorkspace(input.repoDir);
3534
+ const seen = /* @__PURE__ */ new Set();
3535
+ const order = input.callSites.filter((s) => {
3536
+ const key = `${s.file}:${s.line}:${s.column}`;
3537
+ return !seen.has(key) && Boolean(seen.add(key));
3538
+ }).sort((a, b) => a.file.localeCompare(b.file) || b.line - a.line || b.column - a.column);
3539
+ const originals = /* @__PURE__ */ new Map();
3540
+ const sites = [];
3541
+ for (const [i, callSite] of order.entries()) {
3542
+ if (!originals.has(callSite.file)) originals.set(callSite.file, await ws.read(callSite.file) ?? "");
3543
+ const testFile = `test/shapi/${input.advisory.id}${i === 0 ? "" : `.${i + 1}`}.test.ts`;
3544
+ const result = await repairCallSite({ ...input, workspace: ws, callSite, testFile });
3545
+ for (const c of result.changes) if (c.before && !originals.has(c.file)) originals.set(c.file, c.before);
3546
+ sites.push({ callSite, result });
3547
+ }
3548
+ const fixed = sites.filter((s) => s.result.status === "fixed");
3549
+ const needed = sites.filter((s) => s.result.status !== "already-handled");
3550
+ const tests = new Set(fixed.map((s) => s.result.test?.file));
3551
+ const sourceFiles = [...new Set(fixed.flatMap((s) => s.result.changes.filter((c) => !tests.has(c.file)).map((c) => c.file)))].sort();
3552
+ const changes = [];
3553
+ for (const file of sourceFiles) {
3554
+ const notes = fixed.flatMap((s) => s.result.changes.filter((c) => c.file === file).flatMap((c) => c.notes));
3555
+ changes.push({ file, before: originals.get(file) ?? "", after: await ws.read(file) ?? "", notes });
3556
+ }
3557
+ for (const s of fixed) if (s.result.test) changes.push({ file: s.result.test.file, before: "", after: s.result.test.content, notes: ["generated contract test"] });
3558
+ const methods = new Set(fixed.map((s) => s.result.method));
3559
+ return {
3560
+ status: fixed.length === 0 ? "none" : fixed.length === needed.length ? "fixed" : "partial",
3561
+ sites,
3562
+ changes,
3563
+ ...fixed.length ? { method: methods.has("llm") ? "llm" : "recipe" } : {},
3564
+ sandbox: ws.location
3565
+ };
3566
+ }
3567
+
3568
+ // packages/repair/src/llm.ts
3569
+ import Anthropic from "@anthropic-ai/sdk";
3570
+ import { z as z6 } from "zod";
3571
+ var DEFAULT_REPAIR_MODEL = "claude-fable-5-1";
3572
+ var ProposalSchema = z6.object({
3573
+ files: z6.array(z6.object({ path: z6.string().min(1), content: z6.string() })).min(1),
3574
+ explanation: z6.string()
3575
+ });
3576
+ var SUBMIT_FIX = {
3577
+ name: "submit_fix",
3578
+ description: "Submit the corrected files. Each entry is a repo-relative path and the file's complete new content (not a diff). Include only files you changed. Never include the contract test.",
3579
+ strict: true,
3580
+ input_schema: {
3581
+ type: "object",
3582
+ properties: {
3583
+ files: {
3584
+ type: "array",
3585
+ items: {
3586
+ type: "object",
3587
+ properties: { path: { type: "string" }, content: { type: "string" } },
3588
+ required: ["path", "content"],
3589
+ additionalProperties: false
3590
+ }
3591
+ },
3592
+ explanation: { type: "string", description: "One or two sentences on what changed and why, for the pull request." }
3593
+ },
3594
+ required: ["files", "explanation"],
3595
+ additionalProperties: false
3596
+ }
3597
+ };
3598
+ var SYSTEM = `You repair third-party API integrations after the provider changed its behavior.
3599
+ You get a breaking-change advisory, the call site that depends on the old behavior, the file around it, and a contract test that calls the code with the old response and with the new one. The test currently fails on the new response.
3600
+ Make the smallest change that lets the code handle both responses. Keep its behavior for the old response, keep the file's style, and do not touch unrelated code. Do not edit the test.
3601
+ When done, call submit_fix with the complete content of each file you changed. If no safe change exists, explain why in text and do not call the tool.`;
3602
+ function claudeFixer(opts = {}) {
3603
+ const client = opts.client ?? new Anthropic();
3604
+ const model = opts.model ?? process.env.SHAPI_REPAIR_MODEL ?? DEFAULT_REPAIR_MODEL;
3605
+ return {
3606
+ async fix(req) {
3607
+ let response2;
3608
+ try {
3609
+ response2 = await client.beta.messages.create({
3610
+ model,
3611
+ max_tokens: 16e3,
3612
+ // Thinking is always on for this model; effort sets its depth.
3613
+ output_config: { effort: "high" },
3614
+ // On a safety-classifier decline, retry server-side on Anthropic's recommended fallback model.
3615
+ betas: ["server-side-fallback-2026-07-01"],
3616
+ fallbacks: "default",
3617
+ system: SYSTEM,
3618
+ tools: [SUBMIT_FIX],
3619
+ tool_choice: { type: "auto" },
3620
+ messages: [{ role: "user", content: prompt(req) }]
3621
+ });
3622
+ } catch (err) {
3623
+ if (err instanceof Anthropic.AuthenticationError) return { status: "declined", reason: "Claude API authentication failed (set ANTHROPIC_API_KEY)" };
3624
+ if (err instanceof Anthropic.RateLimitError) return { status: "declined", reason: "Claude API rate limit reached; retry later" };
3625
+ if (err instanceof Anthropic.APIError) return { status: "declined", reason: `Claude API error ${err.status ?? ""}: ${err.message}`.trim() };
3626
+ throw err;
3627
+ }
3628
+ if (response2.stop_reason === "refusal") {
3629
+ return { status: "declined", reason: `model declined (${response2.stop_details?.category ?? "no category"})` };
3630
+ }
3631
+ if (response2.stop_reason === "max_tokens") return { status: "declined", reason: "response hit max_tokens before finishing" };
3632
+ const call = response2.content.find((b) => b.type === "tool_use" && b.name === "submit_fix");
3633
+ if (!call) {
3634
+ const text = response2.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
3635
+ return { status: "declined", reason: text.trim() || "model did not submit a fix" };
3636
+ }
3637
+ const parsed = ProposalSchema.safeParse(call.input);
3638
+ if (!parsed.success) return { status: "declined", reason: `fix did not match the submit_fix schema: ${z6.prettifyError(parsed.error)}` };
3639
+ return { status: "proposed", ...parsed.data, model: response2.model };
3640
+ }
3641
+ };
3642
+ }
3643
+ function prompt(req) {
3644
+ const { advisory, callSite } = req;
3645
+ return `<advisory>
3646
+ ${JSON.stringify(
3647
+ {
3648
+ id: advisory.id,
3649
+ provider: advisory.provider,
3650
+ title: advisory.title,
3651
+ summary: advisory.summary,
3652
+ endpoint: advisory.endpoint,
3653
+ changeClass: advisory.changeClass,
3654
+ trigger: advisory.trigger,
3655
+ migrationNote: advisory.migrationNote
3656
+ },
3657
+ null,
3658
+ 2
3659
+ )}
3660
+ </advisory>
3661
+
3662
+ <call_site>${callSite.file}:${callSite.line}:${callSite.column} ${callSite.method ?? "?"} ${callSite.endpoint ?? "?"}</call_site>
3663
+
3664
+ <file path="${req.file.path}">
3665
+ ${req.file.content}
3666
+ </file>
3667
+
3668
+ <contract_test path="${req.test.path}">
3669
+ ${req.test.content}
3670
+ </contract_test>
3671
+
3672
+ <failing_test_output>
3673
+ ${req.failingOutput.slice(-6e3)}
3674
+ </failing_test_output>`;
3675
+ }
3676
+
3677
+ // packages/repair/src/index.ts
3678
+ var RECIPES = {
3679
+ [statusCodeAcceptSet.id]: statusCodeAcceptSet,
3680
+ [applyFix.id]: applyFix
3681
+ };
3682
+
3683
+ // packages/github-app/src/pr.ts
3684
+ import { execFileSync } from "child_process";
3685
+ import { readFileSync as readFileSync6 } from "fs";
3686
+ import { createAppAuth } from "@octokit/auth-app";
3687
+ import { Octokit } from "@octokit/rest";
3688
+
3689
+ // packages/github-app/src/policy.ts
3690
+ var STOP_LABEL = "shapi:stop";
3691
+
3692
+ // packages/github-app/src/pr.ts
3693
+ function octokitFor(auth, opts = {}) {
3694
+ const request = opts.fetch ? { fetch: opts.fetch } : void 0;
3695
+ if ("token" in auth) return new Octokit({ auth: auth.token, userAgent: "shapi", request });
3696
+ return new Octokit({
3697
+ authStrategy: createAppAuth,
3698
+ auth: { appId: auth.appId, privateKey: auth.privateKey, installationId: auth.installationId },
3699
+ userAgent: "shapi",
3700
+ request
3701
+ });
3702
+ }
3703
+ function authFromEnv(env = process.env) {
3704
+ const key = env.SHAPI_GITHUB_APP_PRIVATE_KEY ?? (env.SHAPI_GITHUB_APP_PRIVATE_KEY_PATH ? readFileSync6(env.SHAPI_GITHUB_APP_PRIVATE_KEY_PATH, "utf8") : void 0);
3705
+ if (env.SHAPI_GITHUB_APP_ID && env.SHAPI_GITHUB_APP_INSTALLATION_ID && key) {
3706
+ return { appId: env.SHAPI_GITHUB_APP_ID, privateKey: key, installationId: Number(env.SHAPI_GITHUB_APP_INSTALLATION_ID) };
3707
+ }
3708
+ const token = env.GITHUB_TOKEN ?? env.GH_TOKEN;
3709
+ if (token) return { token };
3710
+ try {
3711
+ const fromGh = execFileSync("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
3712
+ return fromGh ? { token: fromGh } : void 0;
3713
+ } catch {
3714
+ return void 0;
3715
+ }
3716
+ }
3717
+ var FIX_TRAILER = "SHAPI-Branch";
3718
+ async function upsertFixPr(octokit, pr) {
3719
+ const { owner, repo } = splitRepo(pr.repo);
3720
+ const message = `${pr.commitMessage.trimEnd()}
3721
+
3722
+ ${FIX_TRAILER}: ${pr.branch}`;
3723
+ const { data: prs } = await octokit.rest.pulls.list({ owner, repo, head: `${owner}:${pr.branch}`, state: "all", sort: "created", direction: "desc", per_page: 10 });
3724
+ const openPr = prs.find((p) => p.state === "open");
3725
+ const latest = prs[0];
3726
+ const closedByMaintainer = !openPr && latest && !latest.merged_at;
3727
+ if (closedByMaintainer && !pr.retryClosed) {
3728
+ return { action: "skipped", reason: "closed without merging", branch: pr.branch, url: latest.html_url, number: latest.number, state: "closed" };
3729
+ }
3730
+ const head = await branchHead(octokit, owner, repo, pr.branch);
3731
+ const ours = head ? (await octokit.rest.git.getCommit({ owner, repo, commit_sha: head })).data.message.includes(`${FIX_TRAILER}: ${pr.branch}`) : true;
3732
+ if (openPr) {
3733
+ const found = { branch: pr.branch, url: openPr.html_url, number: openPr.number, state: "open" };
3734
+ if (openPr.labels.some((l) => l.name === STOP_LABEL)) return { action: "skipped", reason: `labeled ${STOP_LABEL}`, ...found };
3735
+ if (!ours) return { action: "skipped", reason: "someone else pushed to the branch", ...found };
3736
+ const conflicted = (await octokit.rest.pulls.get({ owner, repo, pull_number: openPr.number })).data.mergeable === false;
3737
+ if (head && !conflicted && await filesMatch(octokit, owner, repo, head, pr.files)) {
3738
+ if (openPr.title !== pr.title || (openPr.body ?? "") !== pr.body) {
3739
+ await octokit.rest.pulls.update({ owner, repo, pull_number: openPr.number, title: pr.title, body: pr.body });
3740
+ }
3741
+ return { action: "unchanged", url: openPr.html_url, number: openPr.number, branch: pr.branch, commit: head };
3742
+ }
3743
+ const commit2 = await commitOnBase(octokit, owner, repo, openPr.base.ref, pr.files, message);
3744
+ await octokit.rest.git.updateRef({ owner, repo, ref: `heads/${pr.branch}`, sha: commit2, force: true });
3745
+ await octokit.rest.pulls.update({ owner, repo, pull_number: openPr.number, title: pr.title, body: pr.body });
3746
+ return { action: "updated", url: openPr.html_url, number: openPr.number, branch: pr.branch, commit: commit2 };
3747
+ }
3748
+ if (head && !ours && !latest?.merged_at && !closedByMaintainer) return { action: "skipped", reason: "a branch with this name exists and is not SHAPI's", branch: pr.branch };
3749
+ const base = pr.base ?? (await octokit.rest.repos.get({ owner, repo })).data.default_branch;
3750
+ const commit = await commitOnBase(octokit, owner, repo, base, pr.files, message);
3751
+ if (head) await octokit.rest.git.updateRef({ owner, repo, ref: `heads/${pr.branch}`, sha: commit, force: true });
3752
+ else await octokit.rest.git.createRef({ owner, repo, ref: `refs/heads/${pr.branch}`, sha: commit });
3753
+ const opened = await octokit.rest.pulls.create({ owner, repo, head: pr.branch, base, title: pr.title, body: pr.body });
3754
+ if (pr.labels?.length) await octokit.rest.issues.addLabels({ owner, repo, issue_number: opened.data.number, labels: pr.labels });
3755
+ return { action: "created", url: opened.data.html_url, number: opened.data.number, branch: pr.branch, commit };
3756
+ }
3757
+ function splitRepo(full) {
3758
+ const [owner, repo] = full.split("/");
3759
+ if (!owner || !repo) throw new Error(`repo must be owner/name, got ${full}`);
3760
+ return { owner, repo };
3761
+ }
3762
+ async function branchHead(octokit, owner, repo, branch) {
3763
+ const { data } = await octokit.rest.git.listMatchingRefs({ owner, repo, ref: `heads/${branch}` });
3764
+ return data.find((r) => r.ref === `refs/heads/${branch}`)?.object.sha;
3765
+ }
3766
+ async function commitOnBase(octokit, owner, repo, base, files, message) {
3767
+ const baseSha = (await octokit.rest.git.getRef({ owner, repo, ref: `heads/${base}` })).data.object.sha;
3768
+ const baseTree = (await octokit.rest.git.getCommit({ owner, repo, commit_sha: baseSha })).data.tree.sha;
3769
+ const existing = new Set(
3770
+ (await octokit.rest.git.getTree({ owner, repo, tree_sha: baseTree, recursive: "true" })).data.tree.map((e) => e.path)
3771
+ );
3772
+ const contents = await Promise.all(
3773
+ files.map(async (f) => ({
3774
+ ...f,
3775
+ content: existing.has(f.path) ? await matchLineEndings(octokit, owner, repo, base, f.path, f.content) : f.content.replace(/\r\n/g, "\n")
3776
+ }))
3777
+ );
3778
+ const tree = await octokit.rest.git.createTree({
3779
+ owner,
3780
+ repo,
3781
+ base_tree: baseTree,
3782
+ tree: contents.map((f) => ({ path: f.path, mode: "100644", type: "blob", content: f.content }))
3783
+ });
3784
+ const commit = await octokit.rest.git.createCommit({ owner, repo, message, tree: tree.data.sha, parents: [baseSha] });
3785
+ return commit.data.sha;
3786
+ }
3787
+ async function readFile(octokit, owner, repo, ref, filePath) {
3788
+ try {
3789
+ const res = await octokit.rest.repos.getContent({ owner, repo, path: filePath, ref });
3790
+ return !Array.isArray(res.data) && res.data.type === "file" && "content" in res.data ? Buffer.from(res.data.content, "base64").toString("utf8") : void 0;
3791
+ } catch (err) {
3792
+ if (err.status === 404) return void 0;
3793
+ throw err;
3794
+ }
3795
+ }
3796
+ async function filesMatch(octokit, owner, repo, ref, files) {
3797
+ const lf = (s) => s.replace(/\r\n/g, "\n");
3798
+ for (const f of files) {
3799
+ const current = await readFile(octokit, owner, repo, ref, f.path);
3800
+ if (current === void 0 || lf(current) !== lf(f.content)) return false;
3801
+ }
3802
+ return true;
3803
+ }
3804
+ async function matchLineEndings(octokit, owner, repo, ref, filePath, content) {
3805
+ const current = await readFile(octokit, owner, repo, ref, filePath) ?? "";
3806
+ const lf = content.replace(/\r\n/g, "\n");
3807
+ return current.includes("\r\n") ? lf.replace(/\n/g, "\r\n") : lf;
3808
+ }
3809
+ function repoFromUrl(url) {
3810
+ const m = /github\.com[/:]([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(url.trim());
3811
+ return m ? `${m[1]}/${m[2]}` : void 0;
3812
+ }
3813
+
3814
+ // packages/github-app/src/body.ts
3815
+ function prTitle(advisory) {
3816
+ return `SHAPI: ${advisory.title}`;
3817
+ }
3818
+ function prBranch(advisory) {
3819
+ return `shapi/${advisory.id}`;
3820
+ }
3821
+ function renderPrBody({ advisory, repair, shapeSource }) {
3822
+ const fixed = repair.sites.filter((s) => s.result.status === "fixed");
3823
+ const tests = new Set(fixed.map((s) => s.result.test?.file));
3824
+ const notes = repair.changes.filter((c) => !tests.has(c.file)).flatMap((c) => (c.notes.length ? c.notes : [`\`${c.file}\``]).map((n) => `- ${n}`));
3825
+ const method = repair.method === "llm" ? "Claude, verified by the contract tests" : `deterministic recipe \`${advisory.recipeId}\``;
3826
+ const evidence = advisory.evidence;
3827
+ const where = (s) => `\`${s.callSite.file}:${s.callSite.line}\` (${s.callSite.method} ${s.callSite.endpoint})`;
3828
+ const siteLine = (s) => s.result.status === "fixed" ? `- [x] ${where(s)}: fixed, test \`${s.result.test?.file}\`` : s.result.status === "already-handled" ? `- [x] ${where(s)}: already handles the change` : `- [ ] ${where(s)}: not fixed here (${s.result.reason ?? s.result.status}); needs a manual change`;
3829
+ const lines = [
3830
+ `**${advisory.provider}** changed \`${advisory.endpoint.method} ${advisory.endpoint.path}\`: ${advisory.title}.`,
3831
+ "",
3832
+ advisory.summary,
3833
+ "",
3834
+ `### Affected call sites (${fixed.length} of ${repair.sites.length} fixed here)`,
3835
+ ...repair.sites.map(siteLine),
3836
+ "",
3837
+ `### Change (${method})`,
3838
+ ...notes,
3839
+ "",
3840
+ `### Contract test${fixed.length === 1 ? "" : "s"}`,
3841
+ "Each test calls the code with the response it was written for and the one the provider sends now. Each failed before this change and passes after it.",
3842
+ "",
3843
+ "| Test | Before this change | After this change |",
3844
+ "|---|---|---|",
3845
+ ...fixed.map((s) => `| \`${s.result.test?.file}\` | ${summary(s.result.baseline)} | ${summary(s.result.attempts.find((a) => a.outcome === "passed")?.run)} |`),
3846
+ "",
3847
+ "### Evidence",
3848
+ "| | |",
3849
+ "|---|---|",
3850
+ `| Advisory | \`${advisory.id}\` (${advisory.status}) |`,
3851
+ `| Severity | ${advisory.severity} |`,
3852
+ `| Change class | ${advisory.changeClass} |`,
3853
+ `| First observed | ${advisory.firstObserved ?? "unknown"} |`,
3854
+ `| Provider notice | ${advisory.publishedNotice ? `[${advisory.publishedNotice.date ?? "link"}](${advisory.publishedNotice.url})` : "none (undocumented change)"} |`,
3855
+ `| Shapes from | ${shapeSource} |`,
3856
+ ...evidence ? [`| Observed | ${evidence.before.status} at ${evidence.before.observedAt} \u2192 ${evidence.after.status} at ${evidence.after.observedAt} (${evidence.source}) |`] : [],
3857
+ `| Blast radius | ${repair.sites.length} affected call site${repair.sites.length === 1 ? "" : "s"} in this repo |`,
3858
+ `| Sources | ${advisory.sources.map((s, i) => `[${i + 1}](${s})`).join(" ")} |`,
3859
+ "",
3860
+ `Migration note: ${advisory.migrationNote}`,
3861
+ "",
3862
+ "---",
3863
+ "Opened by [SHAPI](https://github.com/SriVarshini09/YC). A change is kept only when its contract test failed before it and passes after it."
3864
+ ];
3865
+ return lines.join("\n") + "\n";
3866
+ }
3867
+ function summary(run2) {
3868
+ if (!run2) return "not run";
3869
+ const m = /Tests\s+([^\n]+?)\s*\(\d+\)/.exec(run2.output);
3870
+ return `${run2.passed ? "pass" : "fail"}${m ? ` (${m[1].trim()})` : ""}`;
3871
+ }
3872
+
3873
+ // packages/github-app/src/app.ts
3874
+ import { createHmac as createHmac2, timingSafeEqual } from "crypto";
3875
+ import { createAppAuth as createAppAuth2 } from "@octokit/auth-app";
3876
+
3877
+ // packages/github-app/src/client.ts
3878
+ import { retry } from "@octokit/plugin-retry";
3879
+ import { throttling } from "@octokit/plugin-throttling";
3880
+ import { Octokit as Octokit2 } from "@octokit/rest";
3881
+ var ResilientOctokit = Octokit2.plugin(throttling, retry);
3882
+
3883
+ // packages/github-app/src/snapshot.ts
3884
+ import { Parser as Parser2 } from "tar";
3885
+
3886
+ // packages/cli/src/scan.ts
3887
+ import { execFileSync as execFileSync2 } from "child_process";
3888
+ import { mkdtempSync as mkdtempSync2, rmSync } from "fs";
3889
+ import os2 from "os";
3890
+ import path11 from "path";
3891
+
3892
+ // packages/cli/src/paths.ts
3893
+ import { existsSync as existsSync4 } from "fs";
3894
+ import { fileURLToPath as fileURLToPath2 } from "url";
3895
+ function bundledDir(published, monorepo) {
3896
+ const candidates = [new URL(`../${published}`, import.meta.url), new URL(`../../../${monorepo}`, import.meta.url)].map((u) => fileURLToPath2(u));
3897
+ return candidates.find((p) => existsSync4(p)) ?? candidates[candidates.length - 1];
3898
+ }
3899
+
3900
+ // packages/cli/src/scan.ts
3901
+ var VERSION = "0.2.0";
3902
+ var DEFAULT_ADVISORIES = bundledDir("advisories", "advisories");
3903
+ async function preparePython() {
3904
+ await loadPythonParser().catch(() => void 0);
3905
+ }
3906
+ function scan(target, opts) {
3907
+ const started = performance.now();
3908
+ const failOn = parseFailOn(opts.failOn ?? "critical");
3909
+ const now = opts.now ? new Date(opts.now) : /* @__PURE__ */ new Date();
3910
+ if (Number.isNaN(now.getTime())) throw new Error(`--now: not a date: ${opts.now}`);
3911
+ let dir = path11.resolve(target ?? ".");
3912
+ let label2 = target ?? ".";
3913
+ let cleanup;
3914
+ if (opts.repo) {
3915
+ if (opts.repo.startsWith("-")) throw new Error(`--repo: not a repository URL: ${opts.repo}`);
3916
+ cleanup = mkdtempSync2(path11.join(os2.tmpdir(), "shapi-scan-"));
3917
+ execFileSync2("git", ["-c", "core.autocrlf=false", "clone", "--depth", "1", "--quiet", "--", opts.repo, cleanup], { stdio: ["ignore", "ignore", "pipe"] });
3918
+ dir = cleanup;
3919
+ label2 = opts.repo;
3920
+ }
3921
+ try {
3922
+ const extracted = extractCallSites(dir);
3923
+ const lock = createApiLock(extracted, `shapi ${VERSION}`);
3924
+ if (opts.out) writeApiLock(opts.out, lock);
3925
+ const advisories = loadAdvisories(opts.advisories ?? DEFAULT_ADVISORIES);
3926
+ const match = matchAdvisories(lock, advisories, { now });
3927
+ const failing = failOn === "none" ? [] : match.affected.filter((f) => SEVERITY_RANK[f.advisory.severity] <= SEVERITY_RANK[failOn]);
3928
+ return {
3929
+ label: label2,
3930
+ filesScanned: extracted.filesScanned,
3931
+ skipped: extracted.skipped,
3932
+ lock,
3933
+ match,
3934
+ elapsedMs: Math.round(performance.now() - started),
3935
+ exitCode: failing.length > 0 ? 1 : 0
3936
+ };
3937
+ } finally {
3938
+ if (cleanup) rmSync(cleanup, { recursive: true, force: true });
3939
+ }
3940
+ }
3941
+ function parseFailOn(value2) {
3942
+ if (value2 === "none" || SEVERITIES.includes(value2)) return value2;
3943
+ throw new Error(`--fail-on: expected one of ${[...SEVERITIES, "none"].join(", ")}, got ${value2}`);
3944
+ }
3945
+ function toJsonReport(o) {
3946
+ const finding = (f) => ({
3947
+ advisory: f.advisory.id,
3948
+ severity: f.advisory.severity,
3949
+ title: f.advisory.title,
3950
+ callSite: f.callSite.id,
3951
+ file: f.callSite.file,
3952
+ line: f.callSite.line,
3953
+ method: f.callSite.method,
3954
+ endpoint: f.callSite.endpoint,
3955
+ reasons: f.reasons,
3956
+ migrationNote: f.advisory.migrationNote
3957
+ });
3958
+ return {
3959
+ target: o.label,
3960
+ filesScanned: o.filesScanned,
3961
+ affected: o.match.affected.map(finding),
3962
+ exposed: o.match.exposed.map(finding),
3963
+ upcoming: o.match.upcoming.map((u) => ({
3964
+ advisory: u.advisory.id,
3965
+ provider: u.advisory.provider,
3966
+ title: u.advisory.title,
3967
+ effectiveDate: u.advisory.effectiveDate,
3968
+ daysUntil: u.daysUntil,
3969
+ callSites: u.callSites
3970
+ })),
3971
+ lock: o.lock
3972
+ };
3973
+ }
3974
+
3975
+ // packages/cli/src/fix.ts
3976
+ var DEFAULT_RECORDINGS = bundledDir("recordings", "fixtures/recordings");
3977
+ async function fix(target, opts, deps) {
3978
+ let dir = path12.resolve(target ?? ".");
3979
+ let cleanup;
3980
+ let remote = opts.repo ? repoFromUrl(opts.repo) : void 0;
3981
+ if (opts.repo) {
3982
+ if (opts.repo.startsWith("-")) throw new Error(`--repo: not a repository URL: ${opts.repo}`);
3983
+ cleanup = mkdtempSync3(path12.join(os3.tmpdir(), "shapi-fix-"));
3984
+ execFileSync3("git", ["-c", "core.autocrlf=false", "clone", "--depth", "1", "--quiet", "--", opts.repo, cleanup], { stdio: ["ignore", "ignore", "pipe"] });
3985
+ dir = cleanup;
3986
+ } else if (opts.pr) {
3987
+ remote = originRepo(dir);
3988
+ }
3989
+ if (opts.pr && !remote) throw new Error("--pr needs a GitHub repository: pass --repo <github url> or run inside a clone whose origin is on GitHub");
3990
+ try {
3991
+ const advisories = loadAdvisories(opts.advisories ?? DEFAULT_ADVISORIES).filter((a) => !opts.advisory || a.id === opts.advisory);
3992
+ const lock = createApiLock(extractCallSites(dir), "shapi fix");
3993
+ const now = opts.now ? new Date(opts.now) : /* @__PURE__ */ new Date();
3994
+ const { affected } = matchAdvisories(lock, advisories, { now });
3995
+ if (affected.length === 0) deps.log("No affected call sites: nothing to fix.");
3996
+ const llm = opts.llm ? claudeFixer() : void 0;
3997
+ const outcomes = [];
3998
+ const byAdvisory = /* @__PURE__ */ new Map();
3999
+ for (const f of affected) byAdvisory.set(f.advisory.id, [...byAdvisory.get(f.advisory.id) ?? [], f]);
4000
+ for (const findings of byAdvisory.values()) {
4001
+ const advisory = findings[0].advisory;
4002
+ const shapes = advisory.fix ? { source: FIX_SOURCE } : findShapes(advisory, opts);
4003
+ if (!shapes) {
4004
+ const skipped = "no before/after shapes for this endpoint (record them with the canary, or add a recording)";
4005
+ for (const finding of findings) {
4006
+ deps.log(`SKIP ${finding.callSite.file}:${finding.callSite.line} ${advisory.id}: ${skipped}`);
4007
+ outcomes.push({ finding, skipped });
4008
+ }
4009
+ continue;
4010
+ }
4011
+ const repair = await repairAdvisory({ workspace: localWorkspace(dir), advisory, callSites: findings.map((f) => f.callSite), before: shapes.before, after: shapes.after, recipes: RECIPES, llm });
4012
+ const mine = [];
4013
+ for (const { callSite, result } of repair.sites) {
4014
+ const finding = findings.find((f) => f.callSite.file === callSite.file && f.callSite.line === callSite.line && f.callSite.column === callSite.column);
4015
+ mine.push({ finding, shapeSource: shapes.source, result });
4016
+ deps.log(`${result.status.toUpperCase().padEnd(15)} ${callSite.file}:${callSite.line} ${advisory.id}${result.method ? ` via ${result.method}` : ""}${result.reason ? `: ${result.reason}` : ""}`);
4017
+ for (const a of result.attempts) deps.log(` ${a.method}: ${a.outcome} (${a.detail})`);
4018
+ }
4019
+ outcomes.push(...mine);
4020
+ if (!repair.changes.length) continue;
4021
+ for (const c of repair.changes) deps.log(` ${c.before ? "M" : "A"} ${c.file}`);
4022
+ const files = repair.changes.map((c) => ({ path: c.file, content: c.after }));
4023
+ if (opts.write) {
4024
+ for (const f of files) {
4025
+ mkdirSync5(path12.dirname(path12.join(dir, f.path)), { recursive: true });
4026
+ writeFileSync4(path12.join(dir, f.path), f.content);
4027
+ }
4028
+ deps.log(` wrote ${files.length} files into ${dir}`);
4029
+ }
4030
+ if (opts.pr && remote) {
4031
+ const fixed = repair.sites.filter((s) => s.result.status === "fixed");
4032
+ const meta = {
4033
+ title: prTitle(advisory),
4034
+ branch: prBranch(advisory),
4035
+ commitMessage: `fix: ${advisory.title}
4036
+
4037
+ Advisory ${advisory.id}, ${fixed.length} of ${repair.sites.length} call sites. Contract tests ${fixed.map((s) => s.result.test?.file).join(", ")} fail before this change and pass after.`,
4038
+ body: renderPrBody({ advisory, repair, shapeSource: shapes.source })
4039
+ };
4040
+ const pr = await (deps.deliver ?? deliverToGitHub)(remote, files, meta);
4041
+ for (const o of mine) if (o.result?.status === "fixed") o.pr = pr;
4042
+ deps.log(` pull request: ${pr.url}`);
4043
+ }
4044
+ }
4045
+ return outcomes;
4046
+ } finally {
4047
+ if (cleanup) rmSync2(cleanup, { recursive: true, force: true });
4048
+ }
4049
+ }
4050
+ async function deliverToGitHub(repo, files, meta) {
4051
+ const auth = authFromEnv();
4052
+ if (!auth) throw new Error("no GitHub credentials: set GITHUB_TOKEN, log in with `gh auth login`, or configure the SHAPI GitHub App variables");
4053
+ const outcome = await upsertFixPr(octokitFor(auth), { repo, files, ...meta, labels: ["shapi"] });
4054
+ if (outcome.action === "skipped") throw new Error(`not delivered: ${outcome.reason}${outcome.url ? ` (${outcome.url})` : ""}`);
4055
+ return { url: outcome.url, number: outcome.number, branch: outcome.branch, commit: outcome.commit };
4056
+ }
4057
+ var FIX_SOURCE = "built from the advisory's fix and the fields the code reads";
4058
+ function findShapes(advisory, opts) {
4059
+ if (opts.db && existsSync5(opts.db)) {
4060
+ const store = ShapeStore.open(opts.db);
4061
+ try {
4062
+ for (const e of store.endpoints()) {
4063
+ if (e.provider !== advisory.provider || !pathMatches(e.endpoint, advisory.endpoint)) continue;
4064
+ if (advisory.endpoint.method !== "*" && e.method !== advisory.endpoint.method) continue;
4065
+ const after = store.latest(e.provider, e.method, e.endpoint, e.source);
4066
+ const before = store.previousDistinct(e.provider, e.method, e.endpoint, e.source);
4067
+ if (before && after) return { before: before.shape, after: after.shape, source: `${e.source} (${opts.db})` };
4068
+ }
4069
+ } finally {
4070
+ store.close();
4071
+ }
4072
+ }
4073
+ const dir = opts.recordings ?? DEFAULT_RECORDINGS;
4074
+ if (!existsSync5(dir)) return void 0;
4075
+ for (const name of readdirSync3(dir).filter((f) => f.endsWith(".json")).sort()) {
4076
+ const recording = loadRecording(path12.join(dir, name));
4077
+ if (recording.provider !== advisory.provider || !pathMatches(recording.endpoint, advisory.endpoint)) continue;
4078
+ if (advisory.endpoint.method !== "*" && recording.method !== advisory.endpoint.method) continue;
4079
+ const store = ShapeStore.open(":memory:");
4080
+ const replayed = replay(recording, store, process.env.SHAPI_SHAPE_SECRET ?? "shapi-dev-secret");
4081
+ store.close();
4082
+ if (replayed.before) return { before: replayed.before.shape, after: replayed.after.shape, source: `recording ${name}` };
4083
+ }
4084
+ return void 0;
4085
+ }
4086
+ function originRepo(dir) {
4087
+ try {
4088
+ return repoFromUrl(execFileSync3("git", ["remote", "get-url", "origin"], { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }));
4089
+ } catch {
4090
+ return void 0;
4091
+ }
4092
+ }
4093
+
4094
+ // packages/cli/src/report.ts
4095
+ import { styleText } from "util";
4096
+ function renderText(o, color) {
4097
+ const paint = (style, s) => color ? styleText(style, s) : s;
4098
+ const out = [];
4099
+ const { lock, match } = o;
4100
+ const providers = lock.providers;
4101
+ const unresolved = lock.callSites.filter((c) => !c.resolved).length;
4102
+ out.push(`${paint("bold", "SHAPI scan")} ${o.label}`);
4103
+ out.push(
4104
+ paint(
4105
+ "dim",
4106
+ [plural(o.filesScanned, "file"), plural(lock.callSites.length, "call site"), plural(providers.length, "provider"), `${o.elapsedMs} ms`].join(" \xB7 ")
4107
+ )
4108
+ );
4109
+ if (o.skipped.length) out.push(paint("dim", `${plural(o.skipped.length, "file")} not scanned: ${[...new Set(o.skipped.map((s) => s.reason))].join("; ")}`));
4110
+ out.push("");
4111
+ out.push(paint("bold", "Providers"));
4112
+ if (providers.length === 0) out.push(" none found");
4113
+ const idWidth = Math.max(0, ...providers.map((p) => p.id.length));
4114
+ for (const p of providers) {
4115
+ const sites = plural(p.callSites, "call site");
4116
+ out.push(` ${p.id.padEnd(idWidth)} ${sites.padEnd(13)} ${paint("dim", p.declaredBy.join(", "))}`.trimEnd());
4117
+ }
4118
+ out.push("");
4119
+ out.push(paint("bold", `Affected call sites (${match.affected.length})`));
4120
+ if (match.affected.length === 0) out.push(" none");
4121
+ for (const f of match.affected) out.push(...renderFinding(f, paint));
4122
+ out.push("");
4123
+ if (match.exposed.length > 0) {
4124
+ out.push(paint("bold", `Exposed, not affected (${match.exposed.length})`));
4125
+ for (const f of match.exposed) {
4126
+ out.push(` ${f.callSite.file}:${f.callSite.line} ${f.advisory.id} ${paint("dim", f.reasons.join("; "))}`);
4127
+ }
4128
+ out.push("");
4129
+ }
4130
+ out.push(paint("bold", "Deadlines in the next 90 days"));
4131
+ if (match.upcoming.length === 0) out.push(" none for the providers in this repo");
4132
+ for (const u of match.upcoming) {
4133
+ const where = u.callSites > 0 ? plural(u.callSites, "call site") : "declared dependency";
4134
+ out.push(` ${u.advisory.effectiveDate} (${u.daysUntil} days) ${u.advisory.provider} ${u.advisory.title} ${paint("dim", where)}`);
4135
+ }
4136
+ if (unresolved > 0) {
4137
+ out.push("");
4138
+ out.push(paint("dim", `${plural(unresolved, "call site")} could not be fully resolved (see --json or --out api.lock).`));
4139
+ }
4140
+ return out.join("\n") + "\n";
4141
+ }
4142
+ var SEVERITY_STYLE = { critical: "red", high: "yellow", medium: "cyan", low: "gray" };
4143
+ function renderFinding(f, paint) {
4144
+ const sev = f.advisory.severity.toUpperCase().padEnd(8);
4145
+ const site = f.callSite;
4146
+ const call = `${site.method ?? "?"} ${site.endpoint ?? "?"}`;
4147
+ const indent = " ".repeat(12);
4148
+ const lines = [
4149
+ ` ${paint(SEVERITY_STYLE[f.advisory.severity] ?? "white", sev)} ${paint("bold", `${site.file}:${site.line}`)} ${call}`,
4150
+ `${indent}${f.advisory.title} ${paint("dim", `[${f.advisory.id}]`)}`,
4151
+ `${indent}why: ${f.reasons.join("; ")}`,
4152
+ `${indent}fix: ${f.advisory.migrationNote}`
4153
+ ];
4154
+ if (f.advisory.status === "unverified") lines.push(`${indent}${paint("dim", "note: unverified advisory (secondary reports only)")}`);
4155
+ return lines;
4156
+ }
4157
+ function plural(n, noun) {
4158
+ return `${n} ${noun}${n === 1 ? "" : "s"}`;
4159
+ }
4160
+
4161
+ // packages/cli/src/index.ts
4162
+ var processIo = {
4163
+ stdout: (s) => process.stdout.write(s),
4164
+ stderr: (s) => process.stderr.write(s),
4165
+ isTTY: process.stdout.isTTY
4166
+ };
4167
+ async function run(argv, io = processIo, fixDeps = {}) {
4168
+ let exitCode = 0;
4169
+ await preparePython();
4170
+ const program = new Command2().name("shapi").description("Map the third-party APIs a codebase depends on and flag the breaking changes that hit it.").version(VERSION).exitOverride().configureOutput({ writeOut: io.stdout, writeErr: io.stderr });
4171
+ program.command("scan").description("Scan a repository and report affected call sites").argument("[path]", "directory to scan", ".").option("--json", "print a JSON report (includes the api.lock)").option("--out <file>", "write the api.lock to this file").option("--repo <url>", "clone a git repository (shallow) into a temp dir and scan it").option("--advisories <dir>", "advisory directory (default: the bundled advisories/)").option("--fail-on <severity>", "exit 1 when an affected call site is at least this severe (critical|high|medium|low|none)", "critical").option("--no-color", "disable color").addOption(new Option("--now <date>", "evaluate deadlines as of this date").hideHelp()).action((target, opts) => {
4172
+ const outcome = scan(opts.repo ? void 0 : target, opts);
4173
+ if (opts.json) io.stdout(JSON.stringify(toJsonReport(outcome), null, 2) + "\n");
4174
+ else io.stdout(renderText(outcome, opts.color !== false && Boolean(io.isTTY)));
4175
+ exitCode = outcome.exitCode;
4176
+ });
4177
+ program.command("fix").description("Repair affected call sites test-first: generate a contract test, apply the recipe (or Claude), keep the change only if the test passes").argument("[path]", "repository to fix", ".").option("--repo <url>", "clone a GitHub repository (shallow) and fix it; with --pr the PR goes to that repository").option("--advisory <id>", "only this advisory").option("--advisories <dir>", "advisory directory (default: the bundled advisories/)").option("--db <file>", "shape store with canary observations (checked before recordings)", ".shapi/shapes.db").option("--recordings <dir>", "recorded response sequences (default: the bundled fixtures/recordings/)").option("--llm", "ask Claude when no recipe fixes the call site (needs ANTHROPIC_API_KEY)").option("--write", "write the fix and the contract test into the local repository").option("--pr", "open a pull request with the fix and the contract test").addOption(new Option("--now <date>", "evaluate deadlines as of this date").hideHelp()).action(async (target, opts) => {
4178
+ const outcomes = await fix(opts.repo ? void 0 : target, opts, { log: (l) => io.stdout(`${l}
4179
+ `), ...fixDeps });
4180
+ exitCode = outcomes.every((o) => o.result?.status === "fixed" || o.result?.status === "already-handled") ? 0 : 1;
4181
+ });
4182
+ try {
4183
+ await program.parseAsync(argv);
4184
+ return exitCode;
4185
+ } catch (err) {
4186
+ if (err instanceof CommanderError2) return err.exitCode;
4187
+ io.stderr(`shapi: ${err instanceof Error ? err.message : String(err)}
4188
+ `);
4189
+ return 2;
4190
+ }
4191
+ }
4192
+ async function main(argv) {
4193
+ return run(argv);
4194
+ }
4195
+ export {
4196
+ DEFAULT_ADVISORIES,
4197
+ DEFAULT_RECORDINGS,
4198
+ VERSION,
4199
+ fix,
4200
+ main,
4201
+ preparePython,
4202
+ renderText,
4203
+ run,
4204
+ scan,
4205
+ toJsonReport
4206
+ };