apiwatcher-cli 0.1.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 (109) hide show
  1. package/changesets/stripe/2025-09-30.clover__2026-08-26.dahlia.json +4576 -0
  2. package/changesets/stripe/index.json +16 -0
  3. package/data/stripe/events.json +271 -0
  4. package/data/stripe/method-map.json +5718 -0
  5. package/dist/changeset/index-file.d.ts +30 -0
  6. package/dist/changeset/index-file.d.ts.map +1 -0
  7. package/dist/changeset/index-file.js +34 -0
  8. package/dist/changeset/index-file.js.map +1 -0
  9. package/dist/changeset/load.d.ts +21 -0
  10. package/dist/changeset/load.d.ts.map +1 -0
  11. package/dist/changeset/load.js +113 -0
  12. package/dist/changeset/load.js.map +1 -0
  13. package/dist/changeset/types.d.ts +97 -0
  14. package/dist/changeset/types.d.ts.map +1 -0
  15. package/dist/changeset/types.js +18 -0
  16. package/dist/changeset/types.js.map +1 -0
  17. package/dist/changeset/version.d.ts +17 -0
  18. package/dist/changeset/version.d.ts.map +1 -0
  19. package/dist/changeset/version.js +33 -0
  20. package/dist/changeset/version.js.map +1 -0
  21. package/dist/cli/args.d.ts +15 -0
  22. package/dist/cli/args.d.ts.map +1 -0
  23. package/dist/cli/args.js +82 -0
  24. package/dist/cli/args.js.map +1 -0
  25. package/dist/cli/index.d.ts +5 -0
  26. package/dist/cli/index.d.ts.map +1 -0
  27. package/dist/cli/index.js +333 -0
  28. package/dist/cli/index.js.map +1 -0
  29. package/dist/config.d.ts +26 -0
  30. package/dist/config.d.ts.map +1 -0
  31. package/dist/config.js +92 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/index.d.ts +14 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +14 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/report/impact.d.ts +40 -0
  38. package/dist/report/impact.d.ts.map +1 -0
  39. package/dist/report/impact.js +331 -0
  40. package/dist/report/impact.js.map +1 -0
  41. package/dist/report/impact.test.d.ts +2 -0
  42. package/dist/report/impact.test.d.ts.map +1 -0
  43. package/dist/report/impact.test.js +247 -0
  44. package/dist/report/impact.test.js.map +1 -0
  45. package/dist/report/render.d.ts +9 -0
  46. package/dist/report/render.d.ts.map +1 -0
  47. package/dist/report/render.js +211 -0
  48. package/dist/report/render.js.map +1 -0
  49. package/dist/scanner/ast.d.ts +74 -0
  50. package/dist/scanner/ast.d.ts.map +1 -0
  51. package/dist/scanner/ast.js +930 -0
  52. package/dist/scanner/ast.js.map +1 -0
  53. package/dist/scanner/paths.d.ts +42 -0
  54. package/dist/scanner/paths.d.ts.map +1 -0
  55. package/dist/scanner/paths.js +179 -0
  56. package/dist/scanner/paths.js.map +1 -0
  57. package/dist/scanner/scan.d.ts +11 -0
  58. package/dist/scanner/scan.d.ts.map +1 -0
  59. package/dist/scanner/scan.js +268 -0
  60. package/dist/scanner/scan.js.map +1 -0
  61. package/dist/scanner/scan.test.d.ts +2 -0
  62. package/dist/scanner/scan.test.d.ts.map +1 -0
  63. package/dist/scanner/scan.test.js +134 -0
  64. package/dist/scanner/scan.test.js.map +1 -0
  65. package/dist/scanner/types.d.ts +74 -0
  66. package/dist/scanner/types.d.ts.map +1 -0
  67. package/dist/scanner/types.js +7 -0
  68. package/dist/scanner/types.js.map +1 -0
  69. package/dist/scanner/version.d.ts +23 -0
  70. package/dist/scanner/version.d.ts.map +1 -0
  71. package/dist/scanner/version.js +155 -0
  72. package/dist/scanner/version.js.map +1 -0
  73. package/dist/scanner/walk.d.ts +26 -0
  74. package/dist/scanner/walk.d.ts.map +1 -0
  75. package/dist/scanner/walk.js +117 -0
  76. package/dist/scanner/walk.js.map +1 -0
  77. package/dist/scanner/wrapper.test.d.ts +2 -0
  78. package/dist/scanner/wrapper.test.d.ts.map +1 -0
  79. package/dist/scanner/wrapper.test.js +92 -0
  80. package/dist/scanner/wrapper.test.js.map +1 -0
  81. package/dist/specdiff/build-method-map.d.ts +6 -0
  82. package/dist/specdiff/build-method-map.d.ts.map +1 -0
  83. package/dist/specdiff/build-method-map.js +231 -0
  84. package/dist/specdiff/build-method-map.js.map +1 -0
  85. package/dist/specdiff/compat.d.ts +20 -0
  86. package/dist/specdiff/compat.d.ts.map +1 -0
  87. package/dist/specdiff/compat.js +58 -0
  88. package/dist/specdiff/compat.js.map +1 -0
  89. package/dist/specdiff/diff.d.ts +28 -0
  90. package/dist/specdiff/diff.d.ts.map +1 -0
  91. package/dist/specdiff/diff.js +529 -0
  92. package/dist/specdiff/diff.js.map +1 -0
  93. package/dist/specdiff/diff.test.d.ts +2 -0
  94. package/dist/specdiff/diff.test.d.ts.map +1 -0
  95. package/dist/specdiff/diff.test.js +260 -0
  96. package/dist/specdiff/diff.test.js.map +1 -0
  97. package/dist/specdiff/fetch.d.ts +28 -0
  98. package/dist/specdiff/fetch.d.ts.map +1 -0
  99. package/dist/specdiff/fetch.js +68 -0
  100. package/dist/specdiff/fetch.js.map +1 -0
  101. package/dist/specdiff/methodmap.d.ts +45 -0
  102. package/dist/specdiff/methodmap.d.ts.map +1 -0
  103. package/dist/specdiff/methodmap.js +109 -0
  104. package/dist/specdiff/methodmap.js.map +1 -0
  105. package/dist/specdiff/openapi.d.ts +166 -0
  106. package/dist/specdiff/openapi.d.ts.map +1 -0
  107. package/dist/specdiff/openapi.js +282 -0
  108. package/dist/specdiff/openapi.js.map +1 -0
  109. package/package.json +33 -0
@@ -0,0 +1,930 @@
1
+ import ts from 'typescript';
2
+ import { callKey, normalizePath } from '../specdiff/methodmap.js';
3
+ const SNIPPET_MAX = 160;
4
+ /** Names that read as a Stripe client even when we cannot prove the binding. */
5
+ const CLIENT_NAME_HINT = /^(stripe|stripeClient|stripeApi|stripeSdk|_stripe)$/i;
6
+ const STRIPE_HOSTS = ['api.stripe.com', 'files.stripe.com'];
7
+ /** `invoice.payment_failed`, `customer.subscription.deleted`, ... */
8
+ const EVENT_SHAPE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$/;
9
+ export function parseSource(file) {
10
+ const jsx = file.relative.endsWith('x');
11
+ return ts.createSourceFile(file.relative, file.text, ts.ScriptTarget.Latest,
12
+ /* setParentNodes */ true, jsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
13
+ }
14
+ function evidenceAt(sf, file, node) {
15
+ const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
16
+ const lines = file.text.split('\n');
17
+ const raw = (lines[line] ?? '').trim();
18
+ return {
19
+ file: file.relative,
20
+ line: line + 1,
21
+ column: character + 1,
22
+ snippet: raw.length > SNIPPET_MAX ? `${raw.slice(0, SNIPPET_MAX - 1)}…` : raw,
23
+ };
24
+ }
25
+ /** Unwrap `await x`, `(x)`, `x!`, `x as T` to reach the interesting expression. */
26
+ function unwrap(node) {
27
+ let current = node;
28
+ for (;;) {
29
+ if (ts.isAwaitExpression(current))
30
+ current = current.expression;
31
+ else if (ts.isParenthesizedExpression(current))
32
+ current = current.expression;
33
+ else if (ts.isNonNullExpression(current))
34
+ current = current.expression;
35
+ else if (ts.isAsExpression(current) || ts.isSatisfiesExpression(current))
36
+ current = current.expression;
37
+ else
38
+ return current;
39
+ }
40
+ }
41
+ /** Decompose `a.b.c.d` (or `this.b.c`) into root + segments. */
42
+ function memberChain(expr) {
43
+ const segments = [];
44
+ let current = unwrap(expr);
45
+ for (;;) {
46
+ if (ts.isPropertyAccessExpression(current)) {
47
+ segments.unshift(current.name.text);
48
+ current = unwrap(current.expression);
49
+ continue;
50
+ }
51
+ if (ts.isElementAccessExpression(current)) {
52
+ const arg = current.argumentExpression;
53
+ if (arg && ts.isStringLiteral(arg)) {
54
+ segments.unshift(arg.text);
55
+ current = unwrap(current.expression);
56
+ continue;
57
+ }
58
+ return null;
59
+ }
60
+ break;
61
+ }
62
+ if (ts.isIdentifier(current))
63
+ return { root: current, rootName: current.text, segments };
64
+ if (current.kind === ts.SyntaxKind.ThisKeyword) {
65
+ return { root: current, rootName: 'this', segments };
66
+ }
67
+ return null;
68
+ }
69
+ /** Array methods whose callback receives one element of the collection. */
70
+ const ITERATOR_METHODS = new Set([
71
+ 'map',
72
+ 'forEach',
73
+ 'filter',
74
+ 'find',
75
+ 'flatMap',
76
+ 'some',
77
+ 'every',
78
+ 'sort',
79
+ ]);
80
+ /**
81
+ * Match a call chain against the method map.
82
+ *
83
+ * Tries the longest namespace first and walks inward, so `payments.client.
84
+ * checkout.sessions.create` still resolves to `checkout.sessions.create`. The
85
+ * map is the oracle here: a chain that matches a real Stripe endpoint is strong
86
+ * evidence on its own, which is how wrapped clients get unmasked without
87
+ * needing a model.
88
+ */
89
+ export function resolveCall(map, segments) {
90
+ if (segments.length < 2)
91
+ return null;
92
+ const method = segments[segments.length - 1];
93
+ if (method === undefined)
94
+ return null;
95
+ for (let start = 0; start < segments.length - 1; start++) {
96
+ const namespace = segments.slice(start, -1).join('.');
97
+ const hit = map.byCall[callKey(namespace, method)];
98
+ if (hit) {
99
+ return { namespace, method, httpMethod: hit.method, path: hit.path, droppedPrefix: start };
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ /**
105
+ * How much to trust an SDK call we matched by shape.
106
+ *
107
+ * A resolved client binding is proof. Without one, a multi-segment namespace
108
+ * (`checkout.sessions`) is distinctive enough to be near-certain, while a bare
109
+ * `customers.list` could plausibly be someone's ORM.
110
+ */
111
+ function callConfidence(resolved, isKnownClient, nameHint, stripeInManifest) {
112
+ if (isKnownClient)
113
+ return 1;
114
+ if (nameHint && stripeInManifest)
115
+ return 0.95;
116
+ const distinctive = resolved.namespace.includes('.');
117
+ if (distinctive)
118
+ return stripeInManifest ? 0.85 : 0.7;
119
+ return stripeInManifest ? 0.6 : 0.4;
120
+ }
121
+ export function scanFile(input, parsed) {
122
+ const { file, methodMap, resolveClientExports, stripeInManifest } = input;
123
+ const sf = parsed ?? parseSource(file);
124
+ const usages = [];
125
+ const clients = [];
126
+ const exportedClients = new Set();
127
+ const exportedClientFactories = new Set();
128
+ /** Local identifiers that return a client when called. */
129
+ const factoryNames = new Set();
130
+ /** Identifiers in this file that hold a Stripe client. */
131
+ const clientNames = new Set();
132
+ /** Identifiers bound to the `stripe` package's default export (the constructor). */
133
+ const constructorNames = new Set();
134
+ /**
135
+ * Variable name -> what it holds. `prefix` is the dotted path from the
136
+ * response root, so a `.map` callback parameter can carry `['data', '[]']`
137
+ * and have its field reads reported at the right depth.
138
+ */
139
+ const responseBindings = new Map();
140
+ let importsStripePackage = false;
141
+ // --- pass 1: imports and client construction -----------------------------
142
+ const collectBindings = (node) => {
143
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
144
+ const spec = node.moduleSpecifier.text;
145
+ const clause = node.importClause;
146
+ if (spec === 'stripe') {
147
+ importsStripePackage = true;
148
+ if (clause?.name)
149
+ constructorNames.add(clause.name.text);
150
+ const named = clause?.namedBindings;
151
+ if (named && ts.isNamespaceImport(named))
152
+ constructorNames.add(named.name.text);
153
+ if (named && ts.isNamedImports(named)) {
154
+ for (const el of named.elements) {
155
+ if (el.propertyName?.text === 'Stripe' || el.name.text === 'Stripe') {
156
+ constructorNames.add(el.name.text);
157
+ }
158
+ }
159
+ }
160
+ }
161
+ const exported = resolveClientExports(spec);
162
+ if (exported) {
163
+ if (clause?.name && exported.values.has('default'))
164
+ clientNames.add(clause.name.text);
165
+ if (clause?.name && exported.factories.has('default'))
166
+ factoryNames.add(clause.name.text);
167
+ const named = clause?.namedBindings;
168
+ if (named && ts.isNamedImports(named)) {
169
+ for (const el of named.elements) {
170
+ const original = el.propertyName?.text ?? el.name.text;
171
+ if (exported.values.has(original)) {
172
+ clientNames.add(el.name.text);
173
+ clients.push({
174
+ name: el.name.text,
175
+ file: file.relative,
176
+ line: evidenceAt(sf, file, el).line,
177
+ reason: 'importedDefault',
178
+ });
179
+ }
180
+ else if (exported.factories.has(original)) {
181
+ // `const stripe = getStripeClient()` binds on call, below.
182
+ factoryNames.add(el.name.text);
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ // const Stripe = require('stripe') / const { stripe } = require('./lib')
189
+ if (ts.isVariableDeclaration(node) && node.initializer) {
190
+ const init = unwrap(node.initializer);
191
+ if (ts.isCallExpression(init) &&
192
+ ts.isIdentifier(init.expression) &&
193
+ init.expression.text === 'require' &&
194
+ init.arguments.length === 1) {
195
+ const arg = init.arguments[0];
196
+ if (arg && ts.isStringLiteral(arg)) {
197
+ if (arg.text === 'stripe') {
198
+ importsStripePackage = true;
199
+ if (ts.isIdentifier(node.name))
200
+ constructorNames.add(node.name.text);
201
+ }
202
+ const exported = resolveClientExports(arg.text);
203
+ if (exported) {
204
+ if (ts.isIdentifier(node.name))
205
+ clientNames.add(node.name.text);
206
+ else if (ts.isObjectBindingPattern(node.name)) {
207
+ for (const el of node.name.elements) {
208
+ const original = el.propertyName && ts.isIdentifier(el.propertyName)
209
+ ? el.propertyName.text
210
+ : ts.isIdentifier(el.name)
211
+ ? el.name.text
212
+ : null;
213
+ if (!original || !ts.isIdentifier(el.name))
214
+ continue;
215
+ if (exported.values.has(original))
216
+ clientNames.add(el.name.text);
217
+ else if (exported.factories.has(original))
218
+ factoryNames.add(el.name.text);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ ts.forEachChild(node, collectBindings);
226
+ };
227
+ collectBindings(sf);
228
+ // `new Stripe(...)` — record the client and any pinned apiVersion.
229
+ const collectConstructions = (node) => {
230
+ if (ts.isNewExpression(node)) {
231
+ const chain = memberChain(node.expression);
232
+ const calleeName = chain
233
+ ? chain.segments.length > 0
234
+ ? chain.segments[chain.segments.length - 1]
235
+ : chain.rootName
236
+ : null;
237
+ const isStripeCtor = calleeName !== null &&
238
+ (constructorNames.has(calleeName) ||
239
+ (calleeName === 'Stripe' && (importsStripePackage || stripeInManifest)));
240
+ if (isStripeCtor) {
241
+ const ev = evidenceAt(sf, file, node);
242
+ const apiVersion = readApiVersionArg(node, input.versionConstants);
243
+ usages.push({
244
+ kind: 'clientInit',
245
+ evidence: ev,
246
+ confidence: 1,
247
+ ...(apiVersion ? { field: 'apiVersion', event: apiVersion } : {}),
248
+ });
249
+ // Bind the variable (or class property) it is assigned to.
250
+ const owner = assignmentTarget(node);
251
+ if (owner) {
252
+ clientNames.add(owner.name);
253
+ clients.push({ name: owner.name, file: file.relative, line: ev.line, reason: 'newStripe' });
254
+ if (owner.exported)
255
+ exportedClients.add(owner.exportName ?? owner.name);
256
+ }
257
+ }
258
+ }
259
+ ts.forEachChild(node, collectConstructions);
260
+ };
261
+ collectConstructions(sf);
262
+ // A binding annotated `Stripe` is a client by declaration — the strongest
263
+ // signal available, and the only thing that identifies a client arriving as a
264
+ // function parameter.
265
+ collectTypedClients(sf, constructorNames, clientNames, clients, (node) => evidenceAt(sf, file, node).line, file.relative);
266
+ // Functions that hand back a client, e.g. `export const getStripeClient = () =>
267
+ // new Stripe(key)`. Recommended practice in serverless frameworks, so worth
268
+ // resolving rather than guessing by name.
269
+ collectClientFactories(sf, clientNames, factoryNames, exportedClientFactories);
270
+ // `const stripe = getStripeClient()` — bind the result to a client.
271
+ bindFactoryCalls(sf, factoryNames, clientNames, clients, (node) => evidenceAt(sf, file, node).line, file.relative);
272
+ // `const client = stripeClient` — follow plain aliases so a local rename does
273
+ // not drop every call site behind it to a name-based guess.
274
+ propagateAliases(sf, clientNames);
275
+ // --- pass 2: usages ------------------------------------------------------
276
+ const visit = (node) => {
277
+ if (ts.isCallExpression(node)) {
278
+ handleCall(node);
279
+ // Must run before descending, so the callback parameter is bound by the
280
+ // time its body is visited.
281
+ handleIteratorCallback(node);
282
+ }
283
+ if (ts.isStringLiteralLike(node)) {
284
+ handleString(node, node.text);
285
+ }
286
+ // `fetch(\`https://api.stripe.com/v1/payment_intents/${id}/cancel\`)` — the
287
+ // usual shape for a hand-rolled REST call, so interpolation must be handled.
288
+ if (ts.isTemplateExpression(node)) {
289
+ handleString(node, templateToPath(node));
290
+ }
291
+ if (ts.isPropertyAccessExpression(node)) {
292
+ handleResponseFieldAccess(node);
293
+ }
294
+ ts.forEachChild(node, visit);
295
+ };
296
+ function handleCall(node) {
297
+ const chain = memberChain(node.expression);
298
+ if (!chain)
299
+ return;
300
+ const resolved = resolveCall(methodMap, chain.segments);
301
+ if (!resolved)
302
+ return;
303
+ const isKnownClient = clientNames.has(chain.rootName) ||
304
+ // `this.stripe.customers.create` inside a wrapper class.
305
+ (chain.rootName === 'this' && resolved.droppedPrefix > 0);
306
+ const nameHint = CLIENT_NAME_HINT.test(chain.rootName) ||
307
+ chain.segments.slice(0, resolved.droppedPrefix).some((s) => CLIENT_NAME_HINT.test(s));
308
+ const confidence = callConfidence(resolved, isKnownClient, nameHint, stripeInManifest);
309
+ // Below this, a match is more likely to be someone's ORM than Stripe.
310
+ if (confidence < 0.4)
311
+ return;
312
+ const via = [chain.rootName, ...chain.segments.slice(0, resolved.droppedPrefix)].join('.');
313
+ const ev = evidenceAt(sf, file, node.expression);
314
+ const callId = `${ev.file}:${ev.line}:${ev.column}`;
315
+ usages.push({
316
+ kind: 'sdkCall',
317
+ evidence: ev,
318
+ confidence,
319
+ namespace: resolved.namespace,
320
+ method: resolved.method,
321
+ httpMethod: resolved.httpMethod,
322
+ path: resolved.path,
323
+ via,
324
+ callId,
325
+ });
326
+ recordRequestParams(node, resolved, confidence, callId);
327
+ bindResponse(node, resolved, callId);
328
+ }
329
+ /** Top-level and one-level-nested keys of the params object literal. */
330
+ function recordRequestParams(node, resolved, confidence, callId) {
331
+ const objectArg = node.arguments.find((a) => ts.isObjectLiteralExpression(unwrap(a)));
332
+ if (!objectArg)
333
+ return;
334
+ const obj = unwrap(objectArg);
335
+ if (!ts.isObjectLiteralExpression(obj))
336
+ return;
337
+ const walkObject = (literal, prefix, depth) => {
338
+ if (depth > 2)
339
+ return;
340
+ for (const prop of literal.properties) {
341
+ const name = prop.name && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))
342
+ ? prop.name.text
343
+ : null;
344
+ if (name === null)
345
+ continue;
346
+ const fieldPath = prefix === '' ? name : `${prefix}.${name}`;
347
+ usages.push({
348
+ kind: 'requestParam',
349
+ evidence: evidenceAt(sf, file, prop),
350
+ confidence,
351
+ namespace: resolved.namespace,
352
+ method: resolved.method,
353
+ httpMethod: resolved.httpMethod,
354
+ path: resolved.path,
355
+ field: fieldPath,
356
+ callId,
357
+ });
358
+ if (ts.isPropertyAssignment(prop)) {
359
+ const value = unwrap(prop.initializer);
360
+ if (ts.isObjectLiteralExpression(value))
361
+ walkObject(value, fieldPath, depth + 1);
362
+ }
363
+ }
364
+ };
365
+ walkObject(obj, '', 0);
366
+ }
367
+ /** Remember which variable holds this call's response, plus destructured fields. */
368
+ function bindResponse(node, resolved, callId) {
369
+ const decl = enclosingVariableDeclaration(node);
370
+ if (!decl)
371
+ return;
372
+ if (ts.isIdentifier(decl.name)) {
373
+ responseBindings.set(decl.name.text, { call: resolved, prefix: [], callId });
374
+ return;
375
+ }
376
+ if (ts.isObjectBindingPattern(decl.name)) {
377
+ for (const el of decl.name.elements) {
378
+ const original = el.propertyName && (ts.isIdentifier(el.propertyName) || ts.isStringLiteral(el.propertyName))
379
+ ? el.propertyName.text
380
+ : ts.isIdentifier(el.name)
381
+ ? el.name.text
382
+ : null;
383
+ if (original === null)
384
+ continue;
385
+ usages.push({
386
+ kind: 'responseField',
387
+ evidence: evidenceAt(sf, file, el),
388
+ confidence: 0.95,
389
+ namespace: resolved.namespace,
390
+ method: resolved.method,
391
+ httpMethod: resolved.httpMethod,
392
+ path: resolved.path,
393
+ field: original,
394
+ callId,
395
+ });
396
+ }
397
+ }
398
+ }
399
+ function handleResponseFieldAccess(node) {
400
+ const chain = memberChain(node);
401
+ if (!chain || chain.segments.length === 0)
402
+ return;
403
+ const binding = responseBindings.get(chain.rootName);
404
+ if (!binding)
405
+ return;
406
+ // Skip the chain that is itself a call expression target (handled elsewhere).
407
+ if (node.parent && ts.isCallExpression(node.parent) && node.parent.expression === node)
408
+ return;
409
+ const field = [...binding.prefix, ...chain.segments].join('.');
410
+ usages.push({
411
+ kind: 'responseField',
412
+ evidence: evidenceAt(sf, file, node),
413
+ confidence: 0.9,
414
+ namespace: binding.call.namespace,
415
+ method: binding.call.method,
416
+ httpMethod: binding.call.httpMethod,
417
+ path: binding.call.path,
418
+ field,
419
+ via: chain.rootName,
420
+ ...(binding.callId ? { callId: binding.callId } : {}),
421
+ });
422
+ }
423
+ /**
424
+ * `page.data.map((item) => item.amount)` — bind the callback parameter to the
425
+ * element it iterates, so fields read inside the callback are attributed to the
426
+ * right endpoint. List iteration is how most Stripe collections get consumed,
427
+ * so without this the response side misses the common case.
428
+ */
429
+ function handleIteratorCallback(node) {
430
+ const chain = memberChain(node.expression);
431
+ if (!chain || chain.segments.length === 0)
432
+ return;
433
+ const methodName = chain.segments[chain.segments.length - 1];
434
+ if (methodName === undefined || !ITERATOR_METHODS.has(methodName))
435
+ return;
436
+ const binding = responseBindings.get(chain.rootName);
437
+ if (!binding)
438
+ return;
439
+ // The property path being iterated, e.g. `data` in `page.data.map(...)`.
440
+ const iterated = chain.segments.slice(0, -1);
441
+ if (iterated.length === 0)
442
+ return;
443
+ const callback = node.arguments.find((a) => {
444
+ const fn = unwrap(a);
445
+ return ts.isArrowFunction(fn) || ts.isFunctionExpression(fn);
446
+ });
447
+ if (!callback)
448
+ return;
449
+ const fn = unwrap(callback);
450
+ if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))
451
+ return;
452
+ const param = fn.parameters[0];
453
+ if (!param)
454
+ return;
455
+ // The array marker belongs on the iterated segment (`data[]`), not as a
456
+ // segment of its own — `data.[]` would not match a spec path of `data[]`.
457
+ const segments = [...binding.prefix, ...iterated];
458
+ const last = segments[segments.length - 1];
459
+ if (last === undefined)
460
+ return;
461
+ const elementPrefix = [...segments.slice(0, -1), `${last}[]`];
462
+ if (ts.isIdentifier(param.name)) {
463
+ responseBindings.set(param.name.text, {
464
+ call: binding.call,
465
+ prefix: elementPrefix,
466
+ ...(binding.callId ? { callId: binding.callId } : {}),
467
+ });
468
+ return;
469
+ }
470
+ // `.map(({ id, amount }) => ...)` reads those fields directly.
471
+ if (ts.isObjectBindingPattern(param.name)) {
472
+ for (const el of param.name.elements) {
473
+ const original = el.propertyName && (ts.isIdentifier(el.propertyName) || ts.isStringLiteral(el.propertyName))
474
+ ? el.propertyName.text
475
+ : ts.isIdentifier(el.name)
476
+ ? el.name.text
477
+ : null;
478
+ if (original === null)
479
+ continue;
480
+ usages.push({
481
+ kind: 'responseField',
482
+ evidence: evidenceAt(sf, file, el),
483
+ confidence: 0.9,
484
+ namespace: binding.call.namespace,
485
+ method: binding.call.method,
486
+ httpMethod: binding.call.httpMethod,
487
+ path: binding.call.path,
488
+ field: [...elementPrefix, original].join('.'),
489
+ ...(binding.callId ? { callId: binding.callId } : {}),
490
+ });
491
+ }
492
+ }
493
+ }
494
+ function handleString(node, text) {
495
+ // Raw REST calls: full URL or a bare /v1/ path.
496
+ const pathMatch = extractStripePath(text);
497
+ if (pathMatch) {
498
+ const verb = inferHttpMethod(node);
499
+ usages.push({
500
+ kind: 'rawUrl',
501
+ evidence: evidenceAt(sf, file, node),
502
+ confidence: pathMatch.viaHost ? 1 : stripeInManifest ? 0.8 : 0.5,
503
+ path: pathMatch.path,
504
+ ...(verb ? { httpMethod: verb } : {}),
505
+ });
506
+ return;
507
+ }
508
+ // Webhook event types.
509
+ if (EVENT_SHAPE.test(text) && looksLikeEventContext(node, input.knownEvents, text)) {
510
+ usages.push({
511
+ kind: 'webhookEvent',
512
+ evidence: evidenceAt(sf, file, node),
513
+ confidence: input.knownEvents?.has(text) ? 1 : 0.6,
514
+ event: text,
515
+ });
516
+ }
517
+ }
518
+ visit(sf);
519
+ // A file that constructs a client and exports it via `export { stripe }`.
520
+ collectNamedExports(sf, clientNames, exportedClients);
521
+ return { usages, clients, exportedClients, exportedClientFactories, importsStripePackage };
522
+ }
523
+ // --------------------------------------------------------------------------
524
+ /**
525
+ * Read the pinned `apiVersion` out of a `new Stripe(...)` call.
526
+ *
527
+ * Accepts a literal or a constant reference — `apiVersion: STRIPE_API_VERSION` is
528
+ * how most repos do it, and the pinned version decides which changesets apply, so
529
+ * failing to read it is the difference between a targeted report and a guess.
530
+ */
531
+ function readApiVersionArg(node, versionConstants) {
532
+ for (const arg of node.arguments ?? []) {
533
+ const obj = unwrap(arg);
534
+ if (!ts.isObjectLiteralExpression(obj))
535
+ continue;
536
+ for (const prop of obj.properties) {
537
+ if (!ts.isPropertyAssignment(prop))
538
+ continue;
539
+ const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null;
540
+ if (name !== 'apiVersion')
541
+ continue;
542
+ const value = unwrap(prop.initializer);
543
+ if (ts.isStringLiteralLike(value))
544
+ return value.text;
545
+ if (ts.isIdentifier(value))
546
+ return versionConstants?.get(value.text) ?? null;
547
+ // `STRIPE_API_VERSION as Stripe.LatestApiVersion` is unwrapped already; a
548
+ // property access like `constants.STRIPE_API_VERSION` resolves by leaf name.
549
+ if (ts.isPropertyAccessExpression(value)) {
550
+ return versionConstants?.get(value.name.text) ?? null;
551
+ }
552
+ }
553
+ }
554
+ return null;
555
+ }
556
+ /**
557
+ * Find what a `new Stripe(...)` expression is assigned to.
558
+ *
559
+ * Climbs through expression forms that pass the value along, including the
560
+ * env-guarded shapes that show up constantly in real code:
561
+ *
562
+ * const stripe = key ? new Stripe(key) : null
563
+ * const stripe = cached ?? (cached = new Stripe(key))
564
+ *
565
+ * Stopping at the conditional would leave the client unbound and drop every call
566
+ * site in the repo to a name-based guess.
567
+ */
568
+ function assignmentTarget(node) {
569
+ let current = node.parent;
570
+ let previous = node;
571
+ while (current) {
572
+ if (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current)) {
573
+ previous = current;
574
+ current = current.parent;
575
+ continue;
576
+ }
577
+ // `cond ? new Stripe() : null` — only the branches carry the value.
578
+ if (ts.isConditionalExpression(current) && current.condition !== previous) {
579
+ previous = current;
580
+ current = current.parent;
581
+ continue;
582
+ }
583
+ // `x ?? new Stripe()`, `x || new Stripe()`, `x && new Stripe()`
584
+ if (ts.isBinaryExpression(current) &&
585
+ (current.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
586
+ current.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
587
+ current.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken)) {
588
+ previous = current;
589
+ current = current.parent;
590
+ continue;
591
+ }
592
+ break;
593
+ }
594
+ if (!current)
595
+ return null;
596
+ if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {
597
+ const statement = current.parent?.parent;
598
+ const exported = statement !== undefined &&
599
+ ts.isVariableStatement(statement) &&
600
+ (statement.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
601
+ return { name: current.name.text, exported };
602
+ }
603
+ if (ts.isPropertyDeclaration(current) && ts.isIdentifier(current.name)) {
604
+ return { name: current.name.text, exported: false };
605
+ }
606
+ // `this.stripe = new Stripe(...)` / `module.exports = new Stripe(...)`
607
+ if (ts.isBinaryExpression(current) && current.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
608
+ const chain = memberChain(current.left);
609
+ if (chain) {
610
+ const last = chain.segments[chain.segments.length - 1];
611
+ const name = last ?? chain.rootName;
612
+ const isModuleExports = chain.rootName === 'module' && chain.segments[0] === 'exports';
613
+ return {
614
+ name,
615
+ exported: isModuleExports || chain.rootName === 'exports',
616
+ ...(isModuleExports ? { exportName: 'default' } : {}),
617
+ };
618
+ }
619
+ }
620
+ if (ts.isExportAssignment(current))
621
+ return { name: 'default', exported: true, exportName: 'default' };
622
+ return null;
623
+ }
624
+ function enclosingVariableDeclaration(node) {
625
+ let current = node.parent;
626
+ // Only climb through wrappers that preserve the value.
627
+ while (current &&
628
+ (ts.isAwaitExpression(current) ||
629
+ ts.isParenthesizedExpression(current) ||
630
+ ts.isNonNullExpression(current) ||
631
+ ts.isAsExpression(current))) {
632
+ current = current.parent;
633
+ }
634
+ if (current && ts.isVariableDeclaration(current))
635
+ return current;
636
+ return null;
637
+ }
638
+ /**
639
+ * Bindings whose declared type is `Stripe`.
640
+ *
641
+ * A wrapper that takes the client as an argument — `async function handle(stripe:
642
+ * Stripe, event: Stripe.Event)` — gives no other way to know what `stripe` is.
643
+ * The annotation is a declaration, not a heuristic, so these are full confidence.
644
+ */
645
+ function collectTypedClients(sf, constructorNames, clientNames, clients, lineOf, file) {
646
+ if (constructorNames.size === 0)
647
+ return;
648
+ /** Is this type annotation the Stripe client type? */
649
+ const isClientType = (type) => {
650
+ if (!type)
651
+ return false;
652
+ // `Stripe | null`, `Stripe | undefined`
653
+ if (ts.isUnionTypeNode(type))
654
+ return type.types.some(isClientType);
655
+ if (!ts.isTypeReferenceNode(type))
656
+ return false;
657
+ const name = type.typeName;
658
+ if (ts.isIdentifier(name))
659
+ return constructorNames.has(name.text);
660
+ // `Stripe.Stripe` — the namespace re-export of the client class.
661
+ if (ts.isQualifiedName(name) && ts.isIdentifier(name.left)) {
662
+ return constructorNames.has(name.left.text) && name.right.text === 'Stripe';
663
+ }
664
+ return false;
665
+ };
666
+ const visit = (node) => {
667
+ if ((ts.isParameter(node) || ts.isVariableDeclaration(node) || ts.isPropertyDeclaration(node)) &&
668
+ ts.isIdentifier(node.name) &&
669
+ isClientType(node.type)) {
670
+ clientNames.add(node.name.text);
671
+ clients.push({ name: node.name.text, file, line: lineOf(node), reason: 'wrapperProperty' });
672
+ }
673
+ ts.forEachChild(node, visit);
674
+ };
675
+ visit(sf);
676
+ }
677
+ /**
678
+ * Follow plain aliases: `const client = stripeClient`.
679
+ *
680
+ * Iterated to a fixed point because an alias can be assigned before the name it
681
+ * copies has itself been recognised. The bound is small; chains longer than a few
682
+ * hops do not occur in practice.
683
+ */
684
+ function propagateAliases(sf, clientNames) {
685
+ const pairs = [];
686
+ const collect = (node) => {
687
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
688
+ const init = unwrap(node.initializer);
689
+ if (ts.isIdentifier(init))
690
+ pairs.push({ target: node.name.text, source: init.text });
691
+ }
692
+ if (ts.isBinaryExpression(node) &&
693
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
694
+ ts.isPropertyAccessExpression(node.left)) {
695
+ const init = unwrap(node.right);
696
+ if (ts.isIdentifier(init))
697
+ pairs.push({ target: node.left.name.text, source: init.text });
698
+ }
699
+ ts.forEachChild(node, collect);
700
+ };
701
+ collect(sf);
702
+ if (pairs.length === 0)
703
+ return;
704
+ for (let round = 0; round < 4; round++) {
705
+ let changed = false;
706
+ for (const { target, source } of pairs) {
707
+ if (clientNames.has(source) && !clientNames.has(target)) {
708
+ clientNames.add(target);
709
+ changed = true;
710
+ }
711
+ }
712
+ if (!changed)
713
+ return;
714
+ }
715
+ }
716
+ /**
717
+ * Find functions that hand back a Stripe client.
718
+ *
719
+ * Lazy init behind a getter is the recommended pattern wherever module-level env
720
+ * access is a problem (Next.js, Workers, Lambda), so a repo can have every call
721
+ * site go through `getStripeClient()` and never name Stripe at the call site.
722
+ */
723
+ function collectClientFactories(sf, clientNames, factoryNames, exportedFactories) {
724
+ /** Does this function body return something we know is a client? */
725
+ const returnsClient = (body) => {
726
+ let found = false;
727
+ const walk = (node) => {
728
+ if (found)
729
+ return;
730
+ // Do not descend into nested functions: their returns are not ours.
731
+ if (node !== body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)))
732
+ return;
733
+ if (ts.isReturnStatement(node) && node.expression) {
734
+ if (expressionIsClient(node.expression, clientNames, factoryNames))
735
+ found = true;
736
+ }
737
+ ts.forEachChild(node, walk);
738
+ };
739
+ // A concise arrow body is itself the returned expression.
740
+ if (ts.isExpression(body))
741
+ return expressionIsClient(body, clientNames, factoryNames);
742
+ walk(body);
743
+ return found;
744
+ };
745
+ const isExported = (node) => ts.canHaveModifiers(node) &&
746
+ (ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
747
+ const visit = (node) => {
748
+ if (ts.isFunctionDeclaration(node) && node.name && node.body && returnsClient(node.body)) {
749
+ factoryNames.add(node.name.text);
750
+ if (isExported(node))
751
+ exportedFactories.add(node.name.text);
752
+ }
753
+ if (ts.isVariableStatement(node)) {
754
+ const exported = isExported(node);
755
+ for (const decl of node.declarationList.declarations) {
756
+ if (!ts.isIdentifier(decl.name) || !decl.initializer)
757
+ continue;
758
+ const init = unwrap(decl.initializer);
759
+ if (!ts.isArrowFunction(init) && !ts.isFunctionExpression(init))
760
+ continue;
761
+ if (!returnsClient(init.body))
762
+ continue;
763
+ factoryNames.add(decl.name.text);
764
+ if (exported)
765
+ exportedFactories.add(decl.name.text);
766
+ }
767
+ }
768
+ ts.forEachChild(node, visit);
769
+ };
770
+ visit(sf);
771
+ }
772
+ /** Is this expression (possibly guarded) a known client or factory call? */
773
+ function expressionIsClient(expr, clientNames, factoryNames) {
774
+ const node = unwrap(expr);
775
+ if (ts.isIdentifier(node))
776
+ return clientNames.has(node.text);
777
+ if (ts.isNewExpression(node)) {
778
+ const chain = memberChain(node.expression);
779
+ const name = chain?.segments[chain.segments.length - 1] ?? chain?.rootName;
780
+ return name === 'Stripe';
781
+ }
782
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
783
+ return factoryNames.has(node.expression.text);
784
+ }
785
+ // `key ? new Stripe(key) : null`, `cached ?? new Stripe(key)`
786
+ if (ts.isConditionalExpression(node)) {
787
+ return (expressionIsClient(node.whenTrue, clientNames, factoryNames) ||
788
+ expressionIsClient(node.whenFalse, clientNames, factoryNames));
789
+ }
790
+ if (ts.isBinaryExpression(node) &&
791
+ (node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
792
+ node.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
793
+ return (expressionIsClient(node.left, clientNames, factoryNames) ||
794
+ expressionIsClient(node.right, clientNames, factoryNames));
795
+ }
796
+ return false;
797
+ }
798
+ /** `const stripe = getStripeClient()` — the result is a client. */
799
+ function bindFactoryCalls(sf, factoryNames, clientNames, clients, lineOf, file) {
800
+ if (factoryNames.size === 0)
801
+ return;
802
+ const visit = (node) => {
803
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
804
+ const init = unwrap(node.initializer);
805
+ if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && factoryNames.has(init.expression.text)) {
806
+ clientNames.add(node.name.text);
807
+ clients.push({ name: node.name.text, file, line: lineOf(node), reason: 'wrapperProperty' });
808
+ }
809
+ }
810
+ // `this.stripe = getStripeClient()` inside a service class.
811
+ if (ts.isBinaryExpression(node) &&
812
+ node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
813
+ ts.isPropertyAccessExpression(node.left)) {
814
+ const init = unwrap(node.right);
815
+ if (ts.isCallExpression(init) && ts.isIdentifier(init.expression) && factoryNames.has(init.expression.text)) {
816
+ clientNames.add(node.left.name.text);
817
+ clients.push({ name: node.left.name.text, file, line: lineOf(node), reason: 'wrapperProperty' });
818
+ }
819
+ }
820
+ ts.forEachChild(node, visit);
821
+ };
822
+ visit(sf);
823
+ }
824
+ function collectNamedExports(sf, clientNames, out) {
825
+ const visit = (node) => {
826
+ if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
827
+ for (const el of node.exportClause.elements) {
828
+ const local = el.propertyName?.text ?? el.name.text;
829
+ if (clientNames.has(local))
830
+ out.add(el.name.text);
831
+ }
832
+ }
833
+ ts.forEachChild(node, visit);
834
+ };
835
+ visit(sf);
836
+ }
837
+ /**
838
+ * Render a template literal as a path, with each interpolation as `{}`.
839
+ *
840
+ * That is the same normalized form `normalizePath` produces for spec paths, so
841
+ * `/v1/payment_intents/${id}/cancel` lines up with
842
+ * `/v1/payment_intents/{intent}/cancel`.
843
+ */
844
+ function templateToPath(node) {
845
+ let out = node.head.text;
846
+ for (const span of node.templateSpans) {
847
+ out += `{}${span.literal.text}`;
848
+ }
849
+ return out;
850
+ }
851
+ /** Pull a Stripe REST path out of a string, whether it is a full URL or not. */
852
+ export function extractStripePath(text) {
853
+ for (const host of STRIPE_HOSTS) {
854
+ const idx = text.indexOf(host);
855
+ if (idx !== -1) {
856
+ const rest = text.slice(idx + host.length);
857
+ const m = /^(\/v\d+\/[A-Za-z0-9_\-./{}$:]*)/.exec(rest);
858
+ return { path: m?.[1] ? trimPath(m[1]) : '/', viaHost: true };
859
+ }
860
+ }
861
+ // A bare `/v1/...` path only counts if it looks like a Stripe resource path.
862
+ const bare = /^\/v1\/[a-z][a-z0-9_]*(\/[A-Za-z0-9_\-.{}$:]*)*$/.exec(text);
863
+ if (bare)
864
+ return { path: trimPath(text), viaHost: false };
865
+ return null;
866
+ }
867
+ function trimPath(path) {
868
+ const clean = path.replace(/[?#].*$/, '').replace(/\/+$/, '');
869
+ return clean === '' ? '/' : clean;
870
+ }
871
+ /** Best-effort HTTP verb for a raw URL, read from a nearby `method:` property. */
872
+ function inferHttpMethod(node) {
873
+ let current = node.parent;
874
+ for (let i = 0; i < 4 && current; i++) {
875
+ if (ts.isCallExpression(current)) {
876
+ for (const arg of current.arguments) {
877
+ const obj = unwrap(arg);
878
+ if (!ts.isObjectLiteralExpression(obj))
879
+ continue;
880
+ for (const prop of obj.properties) {
881
+ if (!ts.isPropertyAssignment(prop))
882
+ continue;
883
+ const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null;
884
+ if (name !== 'method')
885
+ continue;
886
+ const value = unwrap(prop.initializer);
887
+ if (ts.isStringLiteralLike(value))
888
+ return value.text.toLowerCase();
889
+ }
890
+ }
891
+ const chain = memberChain(current.expression);
892
+ const last = chain?.segments[chain.segments.length - 1];
893
+ if (last && ['get', 'post', 'put', 'patch', 'delete'].includes(last.toLowerCase())) {
894
+ return last.toLowerCase();
895
+ }
896
+ }
897
+ current = current.parent;
898
+ }
899
+ return null;
900
+ }
901
+ /**
902
+ * Decide whether an event-shaped string is really a webhook event type.
903
+ *
904
+ * Known-event membership is the strongest signal. Failing that, require the
905
+ * literal to sit somewhere events actually appear: a comparison against a
906
+ * `.type` property, a `switch` case, or an array of event names.
907
+ */
908
+ function looksLikeEventContext(node, knownEvents, text) {
909
+ if (knownEvents && knownEvents.has(text))
910
+ return true;
911
+ if (knownEvents && knownEvents.size > 0)
912
+ return false;
913
+ let current = node.parent;
914
+ for (let i = 0; i < 4 && current; i++) {
915
+ if (ts.isCaseClause(current))
916
+ return true;
917
+ if (ts.isBinaryExpression(current)) {
918
+ const other = current.left === node ? current.right : current.left;
919
+ const chain = ts.isExpression(other) ? memberChain(other) : null;
920
+ if (chain && chain.segments[chain.segments.length - 1] === 'type')
921
+ return true;
922
+ }
923
+ if (ts.isArrayLiteralExpression(current))
924
+ return true;
925
+ current = current.parent;
926
+ }
927
+ return false;
928
+ }
929
+ export { normalizePath };
930
+ //# sourceMappingURL=ast.js.map