@mondaydotcomorg/atp-compiler 0.22.2 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/unit/api-call-analyzer.test.ts +387 -0
- package/dist/api-call-analyzer.d.ts +45 -0
- package/dist/api-call-analyzer.d.ts.map +1 -0
- package/dist/api-call-analyzer.js +192 -0
- package/dist/api-call-analyzer.js.map +1 -0
- package/dist/index.cjs +144 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +144 -13
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/api-call-analyzer.ts +232 -0
- package/src/index.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { describe, test, expect } from '@jest/globals';
|
|
2
|
+
import { analyzeApiCalls } from '../../src/api-call-analyzer';
|
|
3
|
+
|
|
4
|
+
describe('analyzeApiCalls', () => {
|
|
5
|
+
test('extracts a single api.<group>.<op>(...) call', () => {
|
|
6
|
+
const r = analyzeApiCalls(`return api.calendar.events_list({ calendarId: 'primary' });`);
|
|
7
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
8
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('extracts nested await + multiple calls in the same group', () => {
|
|
12
|
+
const code = `
|
|
13
|
+
const c = await api.calendar.calendars_get({ calendarId: 'primary' });
|
|
14
|
+
return await api.calendar.events_list({ calendarId: c.id, maxResults: 10 });
|
|
15
|
+
`;
|
|
16
|
+
const r = analyzeApiCalls(code);
|
|
17
|
+
const sorted = r.apiCalls.slice().sort((a, b) => a.operationId.localeCompare(b.operationId));
|
|
18
|
+
expect(sorted).toEqual([
|
|
19
|
+
{ apiGroup: 'calendar', operationId: 'calendars_get' },
|
|
20
|
+
{ apiGroup: 'calendar', operationId: 'events_list' },
|
|
21
|
+
]);
|
|
22
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('extracts cross-group calls', () => {
|
|
26
|
+
const r = analyzeApiCalls(`
|
|
27
|
+
await api.calendar.events_list({});
|
|
28
|
+
await api.gmail.messages_list({});
|
|
29
|
+
`);
|
|
30
|
+
expect(r.apiCalls).toEqual(
|
|
31
|
+
expect.arrayContaining([
|
|
32
|
+
{ apiGroup: 'calendar', operationId: 'events_list' },
|
|
33
|
+
{ apiGroup: 'gmail', operationId: 'messages_list' },
|
|
34
|
+
])
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('deduplicates repeated calls to the same api.<group>.<op>', () => {
|
|
39
|
+
const r = analyzeApiCalls(`
|
|
40
|
+
await api.calendar.events_list({ maxResults: 1 });
|
|
41
|
+
await api.calendar.events_list({ maxResults: 2 });
|
|
42
|
+
`);
|
|
43
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('flags dynamic dispatch via computed operation member', () => {
|
|
47
|
+
const r = analyzeApiCalls(`return api.calendar[fn]({});`);
|
|
48
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('flags dynamic dispatch via computed group member', () => {
|
|
52
|
+
const r = analyzeApiCalls(`return api[g].events_list({});`);
|
|
53
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('flags destructured api: const { calendar } = api', () => {
|
|
57
|
+
const r = analyzeApiCalls(`
|
|
58
|
+
const { calendar } = api;
|
|
59
|
+
return calendar.events_list({});
|
|
60
|
+
`);
|
|
61
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('flags destructure-with-rename: const { calendar: c } = api', () => {
|
|
65
|
+
const r = analyzeApiCalls(`
|
|
66
|
+
const { calendar: c } = api;
|
|
67
|
+
return c.events_list({});
|
|
68
|
+
`);
|
|
69
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('flags aliasing: const x = api.calendar', () => {
|
|
73
|
+
const r = analyzeApiCalls(`
|
|
74
|
+
const x = api.calendar;
|
|
75
|
+
return x.events_list({});
|
|
76
|
+
`);
|
|
77
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('flags aliasing: const x = api', () => {
|
|
81
|
+
const r = analyzeApiCalls(`
|
|
82
|
+
const x = api;
|
|
83
|
+
return x.calendar.events_list({});
|
|
84
|
+
`);
|
|
85
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('returns empty calls + no dynamic flag for trivial code', () => {
|
|
89
|
+
const r = analyzeApiCalls(`return 42;`);
|
|
90
|
+
expect(r.apiCalls).toEqual([]);
|
|
91
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('fails closed with dynamicCallsDetected=true on syntax error', () => {
|
|
95
|
+
const r = analyzeApiCalls(`return api.calendar.events_list(`);
|
|
96
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
97
|
+
expect(r.apiCalls).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('handles empty string input', () => {
|
|
101
|
+
const r = analyzeApiCalls('');
|
|
102
|
+
expect(r.apiCalls).toEqual([]);
|
|
103
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// Dedup — same (group, op) through different syntactic paths
|
|
108
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe('dedup', () => {
|
|
111
|
+
test('same op called in both branches of a ternary', () => {
|
|
112
|
+
const r = analyzeApiCalls(`
|
|
113
|
+
return flag
|
|
114
|
+
? api.calendar.events_list({ a: 1 })
|
|
115
|
+
: api.calendar.events_list({ a: 2 });
|
|
116
|
+
`);
|
|
117
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
118
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('same op called inside a loop is reported once', () => {
|
|
122
|
+
const r = analyzeApiCalls(`
|
|
123
|
+
for (const id of ids) {
|
|
124
|
+
await api.calendar.events_list({ calendarId: id });
|
|
125
|
+
}
|
|
126
|
+
`);
|
|
127
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('same op across try/catch/finally reports once', () => {
|
|
131
|
+
const r = analyzeApiCalls(`
|
|
132
|
+
try {
|
|
133
|
+
await api.calendar.events_list({});
|
|
134
|
+
} catch (e) {
|
|
135
|
+
await api.calendar.events_list({ retry: true });
|
|
136
|
+
} finally {
|
|
137
|
+
api.calendar.events_list({ cleanup: true });
|
|
138
|
+
}
|
|
139
|
+
`);
|
|
140
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('same op across multiple statements is reported once', () => {
|
|
144
|
+
const r = analyzeApiCalls(`
|
|
145
|
+
const a = await api.calendar.events_list({ maxResults: 1 });
|
|
146
|
+
const b = await api.calendar.events_list({ maxResults: 2 });
|
|
147
|
+
const c = await api.calendar.events_list({ maxResults: 3 });
|
|
148
|
+
`);
|
|
149
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
154
|
+
// Complex control-flow / realistic code shapes
|
|
155
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
describe('complex control flow', () => {
|
|
158
|
+
test('IIFE (immediately-invoked async function)', () => {
|
|
159
|
+
const r = analyzeApiCalls(`
|
|
160
|
+
return (async () => await api.calendar.events_list({}))();
|
|
161
|
+
`);
|
|
162
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
163
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('Promise.all with multiple awaits', () => {
|
|
167
|
+
const r = analyzeApiCalls(`
|
|
168
|
+
return await Promise.all([
|
|
169
|
+
api.calendar.events_list({}),
|
|
170
|
+
api.gmail.messages_list({}),
|
|
171
|
+
api.drive.files_list({}),
|
|
172
|
+
]);
|
|
173
|
+
`);
|
|
174
|
+
const sorted = r.apiCalls
|
|
175
|
+
.slice()
|
|
176
|
+
.sort((a, b) => (a.apiGroup + '.' + a.operationId).localeCompare(b.apiGroup + '.' + b.operationId));
|
|
177
|
+
expect(sorted).toEqual([
|
|
178
|
+
{ apiGroup: 'calendar', operationId: 'events_list' },
|
|
179
|
+
{ apiGroup: 'drive', operationId: 'files_list' },
|
|
180
|
+
{ apiGroup: 'gmail', operationId: 'messages_list' },
|
|
181
|
+
]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('arrow inside .then() chain', () => {
|
|
185
|
+
const r = analyzeApiCalls(`
|
|
186
|
+
return api.calendar.events_list({})
|
|
187
|
+
.then((list) => api.calendar.events_get({ eventId: list.items[0].id }))
|
|
188
|
+
.then((ev) => api.calendar.calendars_get({ calendarId: ev.calendarId }));
|
|
189
|
+
`);
|
|
190
|
+
expect(r.apiCalls.sort((a, b) => a.operationId.localeCompare(b.operationId))).toEqual([
|
|
191
|
+
{ apiGroup: 'calendar', operationId: 'calendars_get' },
|
|
192
|
+
{ apiGroup: 'calendar', operationId: 'events_get' },
|
|
193
|
+
{ apiGroup: 'calendar', operationId: 'events_list' },
|
|
194
|
+
]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('class method body', () => {
|
|
198
|
+
const r = analyzeApiCalls(`
|
|
199
|
+
class Scheduler {
|
|
200
|
+
async listEvents() {
|
|
201
|
+
return await api.calendar.events_list({ calendarId: 'primary' });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return new Scheduler().listEvents();
|
|
205
|
+
`);
|
|
206
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('nested try/catch/finally across multiple groups', () => {
|
|
210
|
+
const r = analyzeApiCalls(`
|
|
211
|
+
try {
|
|
212
|
+
await api.calendar.events_list({});
|
|
213
|
+
} catch (e) {
|
|
214
|
+
await api.gmail.messages_list({ label: 'errors' });
|
|
215
|
+
} finally {
|
|
216
|
+
api.drive.files_list({});
|
|
217
|
+
}
|
|
218
|
+
`);
|
|
219
|
+
expect(r.apiCalls.length).toBe(3);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('switch statement with api calls per case', () => {
|
|
223
|
+
const r = analyzeApiCalls(`
|
|
224
|
+
switch (kind) {
|
|
225
|
+
case 'a': return api.calendar.events_list({});
|
|
226
|
+
case 'b': return api.calendar.events_get({ eventId: x });
|
|
227
|
+
default: return api.calendar.calendars_get({ calendarId: 'primary' });
|
|
228
|
+
}
|
|
229
|
+
`);
|
|
230
|
+
expect(r.apiCalls.length).toBe(3);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
235
|
+
// Multi-group realism — many sources + many ops
|
|
236
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
describe('multi-group', () => {
|
|
239
|
+
test('realistic Google Suite workload (6 groups, 11 ops)', () => {
|
|
240
|
+
const r = analyzeApiCalls(`
|
|
241
|
+
const events = await api.calendar.events_list({ calendarId: 'primary' });
|
|
242
|
+
const cal = await api.calendar.calendars_get({ calendarId: 'primary' });
|
|
243
|
+
const messages = await api.gmail.messages_list({});
|
|
244
|
+
const thread = await api.gmail.threads_get({ id: '1' });
|
|
245
|
+
const sheet = await api.sheets.spreadsheets_get({ spreadsheetId: 'x' });
|
|
246
|
+
const values = await api.sheets.spreadsheets_values_get({ spreadsheetId: 'x', range: 'A1' });
|
|
247
|
+
const drive = await api.drive.files_list({});
|
|
248
|
+
const file = await api.drive.files_get({ fileId: 'x' });
|
|
249
|
+
const deck = await api.slides.presentations_get({ presentationId: 'x' });
|
|
250
|
+
const doc = await api.docs.documents_get({ documentId: 'x' });
|
|
251
|
+
const batch = await api.sheets.spreadsheets_batchUpdate({ spreadsheetId: 'x' });
|
|
252
|
+
return { events, cal, messages, thread, sheet, values, drive, file, deck, doc, batch };
|
|
253
|
+
`);
|
|
254
|
+
|
|
255
|
+
const groupsTouched = new Set(r.apiCalls.map((c) => c.apiGroup));
|
|
256
|
+
expect(groupsTouched).toEqual(new Set(['calendar', 'gmail', 'sheets', 'drive', 'slides', 'docs']));
|
|
257
|
+
expect(r.apiCalls).toHaveLength(11);
|
|
258
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('mixed groups with some dedup', () => {
|
|
262
|
+
const r = analyzeApiCalls(`
|
|
263
|
+
await api.calendar.events_list({});
|
|
264
|
+
await api.gmail.messages_list({});
|
|
265
|
+
await api.calendar.events_list({}); // dup
|
|
266
|
+
await api.gmail.messages_list({}); // dup
|
|
267
|
+
await api.calendar.events_get({ eventId: 'x' });
|
|
268
|
+
`);
|
|
269
|
+
expect(r.apiCalls).toHaveLength(3);
|
|
270
|
+
expect(r.apiCalls.map((c) => c.apiGroup + '.' + c.operationId).sort()).toEqual([
|
|
271
|
+
'calendar.events_get',
|
|
272
|
+
'calendar.events_list',
|
|
273
|
+
'gmail.messages_list',
|
|
274
|
+
]);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
279
|
+
// Additional dynamic-dispatch escape patterns
|
|
280
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
describe('dynamic dispatch — additional escapes', () => {
|
|
283
|
+
test('flags optional-chained operation member', () => {
|
|
284
|
+
const r = analyzeApiCalls(`return api.calendar?.events_list?.({});`);
|
|
285
|
+
// Detected statically OR dynamic — either outcome denies a grant
|
|
286
|
+
// that doesn't cover the target. This implementation DETECTS
|
|
287
|
+
// because the static path is resolvable; no dynamic flag needed.
|
|
288
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('flags .call() redirection as a regular static call', () => {
|
|
292
|
+
const r = analyzeApiCalls(`api.calendar.events_list.call(null, {});`);
|
|
293
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('flags .apply() redirection', () => {
|
|
297
|
+
const r = analyzeApiCalls(`api.calendar.events_list.apply(null, [{}]);`);
|
|
298
|
+
expect(r.apiCalls).toEqual([{ apiGroup: 'calendar', operationId: 'events_list' }]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('flags .bind() + later call', () => {
|
|
302
|
+
const r = analyzeApiCalls(`
|
|
303
|
+
const boundList = api.calendar.events_list.bind(null);
|
|
304
|
+
return boundList({});
|
|
305
|
+
`);
|
|
306
|
+
// .bind still surfaces the underlying call target.
|
|
307
|
+
expect(r.apiCalls).toContainEqual({ apiGroup: 'calendar', operationId: 'events_list' });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('flags Object.values(api) as dynamic', () => {
|
|
311
|
+
const r = analyzeApiCalls(`
|
|
312
|
+
const groups = Object.values(api);
|
|
313
|
+
return groups.map((g) => Object.keys(g));
|
|
314
|
+
`);
|
|
315
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('flags Object.keys(api) as dynamic', () => {
|
|
319
|
+
const r = analyzeApiCalls(`return Object.keys(api);`);
|
|
320
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('flags spread {...api} as dynamic', () => {
|
|
324
|
+
const r = analyzeApiCalls(`const x = { ...api }; return x;`);
|
|
325
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('flags passing api as function argument', () => {
|
|
329
|
+
const r = analyzeApiCalls(`
|
|
330
|
+
const use = (a) => a.calendar.events_list({});
|
|
331
|
+
return use(api);
|
|
332
|
+
`);
|
|
333
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('flags returning bare api', () => {
|
|
337
|
+
const r = analyzeApiCalls(`return api;`);
|
|
338
|
+
expect(r.dynamicCallsDetected).toBe(true);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
343
|
+
// Not-false-positive — identifiers named "api" that aren't the global
|
|
344
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
describe('does not false-positive on unrelated "api" identifiers', () => {
|
|
347
|
+
test('this.api.foo.bar(...) is ignored (not the global api)', () => {
|
|
348
|
+
const r = analyzeApiCalls(`return this.api.foo.bar({});`);
|
|
349
|
+
expect(r.apiCalls).toEqual([]);
|
|
350
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('someObj.api.foo.bar(...) is ignored', () => {
|
|
354
|
+
const r = analyzeApiCalls(`
|
|
355
|
+
const wrapper = { api: null };
|
|
356
|
+
return wrapper.api?.foo?.bar?.({});
|
|
357
|
+
`);
|
|
358
|
+
expect(r.apiCalls).toEqual([]);
|
|
359
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('deep api.x.y.z(...) — not valid ATP syntax — silently ignored', () => {
|
|
363
|
+
// Sandbox's runtime namespace would throw on this; static analyzer
|
|
364
|
+
// ignores it so we don't over-flag invalid code as dynamic.
|
|
365
|
+
const r = analyzeApiCalls(`return api.x.y.z({});`);
|
|
366
|
+
expect(r.apiCalls).toEqual([]);
|
|
367
|
+
expect(r.dynamicCallsDetected).toBe(false);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
372
|
+
// Idempotency — deterministic output across calls
|
|
373
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
describe('idempotency', () => {
|
|
376
|
+
test('calling analyzeApiCalls twice on same code yields identical output', () => {
|
|
377
|
+
const code = `
|
|
378
|
+
await api.calendar.events_list({});
|
|
379
|
+
await api.gmail.messages_list({});
|
|
380
|
+
const { calendar } = api;
|
|
381
|
+
`;
|
|
382
|
+
const a = analyzeApiCalls(code);
|
|
383
|
+
const b = analyzeApiCalls(code);
|
|
384
|
+
expect(a).toEqual(b);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-dispatch static analysis of ATP agent code.
|
|
3
|
+
*
|
|
4
|
+
* Extracts every `api.<group>.<op>(...)` call chain from submitted code
|
|
5
|
+
* and flags patterns that defeat static analysis (dynamic dispatch,
|
|
6
|
+
* aliasing, destructuring).
|
|
7
|
+
*
|
|
8
|
+
* Intended caller: governance layers that need to know WHICH api groups
|
|
9
|
+
* and operations code will touch BEFORE dispatching it to the sandbox.
|
|
10
|
+
* Paired with runtime `filterApiGroups` enforcement in atp-server for
|
|
11
|
+
* defense-in-depth; the static pass catches unauthorized references
|
|
12
|
+
* up-front without paying sandbox startup cost.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { apiCalls, dynamicCallsDetected } = analyzeApiCalls(code);
|
|
16
|
+
* for (const call of apiCalls) {
|
|
17
|
+
* // check (call.apiGroup, call.operationId) against a grant
|
|
18
|
+
* }
|
|
19
|
+
* if (dynamicCallsDetected) {
|
|
20
|
+
* // deny unless the grant explicitly allows dynamic dispatch
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
export interface DetectedApiCall {
|
|
24
|
+
apiGroup: string;
|
|
25
|
+
operationId: string;
|
|
26
|
+
}
|
|
27
|
+
export interface AnalysisResult {
|
|
28
|
+
/** Unique `(apiGroup, operationId)` pairs statically visible in the code. */
|
|
29
|
+
apiCalls: DetectedApiCall[];
|
|
30
|
+
/**
|
|
31
|
+
* True iff the code contains patterns we cannot statically resolve to a
|
|
32
|
+
* concrete `(apiGroup, operationId)` — e.g. `api[varName].fn(...)`,
|
|
33
|
+
* destructuring (`const { calendar } = api`), or aliasing
|
|
34
|
+
* (`const x = api.calendar`). Governance layers should fail-closed on
|
|
35
|
+
* this flag unless the caller's policy opts into dynamic dispatch.
|
|
36
|
+
*/
|
|
37
|
+
dynamicCallsDetected: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Analyze agent code and return its statically-visible api.* call set plus a
|
|
41
|
+
* dynamic-dispatch flag. Pure function, no I/O, fail-closed on parse errors
|
|
42
|
+
* (returns `{ apiCalls: [], dynamicCallsDetected: true }`).
|
|
43
|
+
*/
|
|
44
|
+
export declare function analyzeApiCalls(code: string): AnalysisResult;
|
|
45
|
+
//# sourceMappingURL=api-call-analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-call-analyzer.d.ts","sourceRoot":"","sources":["../src/api-call-analyzer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAWH,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC9B,6EAA6E;IAC7E,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B;;;;;;OAMG;IACH,oBAAoB,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAgL5D"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-dispatch static analysis of ATP agent code.
|
|
3
|
+
*
|
|
4
|
+
* Extracts every `api.<group>.<op>(...)` call chain from submitted code
|
|
5
|
+
* and flags patterns that defeat static analysis (dynamic dispatch,
|
|
6
|
+
* aliasing, destructuring).
|
|
7
|
+
*
|
|
8
|
+
* Intended caller: governance layers that need to know WHICH api groups
|
|
9
|
+
* and operations code will touch BEFORE dispatching it to the sandbox.
|
|
10
|
+
* Paired with runtime `filterApiGroups` enforcement in atp-server for
|
|
11
|
+
* defense-in-depth; the static pass catches unauthorized references
|
|
12
|
+
* up-front without paying sandbox startup cost.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { apiCalls, dynamicCallsDetected } = analyzeApiCalls(code);
|
|
16
|
+
* for (const call of apiCalls) {
|
|
17
|
+
* // check (call.apiGroup, call.operationId) against a grant
|
|
18
|
+
* }
|
|
19
|
+
* if (dynamicCallsDetected) {
|
|
20
|
+
* // deny unless the grant explicitly allows dynamic dispatch
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
import { parse } from '@babel/parser';
|
|
24
|
+
// @babel/traverse exports default; interop handles the CJS/ESM quirk
|
|
25
|
+
// (see https://github.com/babel/babel/issues/13855).
|
|
26
|
+
import _traverse from '@babel/traverse';
|
|
27
|
+
import * as t from '@babel/types';
|
|
28
|
+
const traverse = (_traverse.default ??
|
|
29
|
+
_traverse);
|
|
30
|
+
/**
|
|
31
|
+
* Analyze agent code and return its statically-visible api.* call set plus a
|
|
32
|
+
* dynamic-dispatch flag. Pure function, no I/O, fail-closed on parse errors
|
|
33
|
+
* (returns `{ apiCalls: [], dynamicCallsDetected: true }`).
|
|
34
|
+
*/
|
|
35
|
+
export function analyzeApiCalls(code) {
|
|
36
|
+
const calls = [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
let dynamicCallsDetected = false;
|
|
39
|
+
let ast;
|
|
40
|
+
try {
|
|
41
|
+
ast = parse(code, {
|
|
42
|
+
sourceType: 'module',
|
|
43
|
+
allowReturnOutsideFunction: true,
|
|
44
|
+
plugins: ['typescript'],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Fail-closed: syntax error → treat as dynamic so governance denies.
|
|
49
|
+
return { apiCalls: [], dynamicCallsDetected: true };
|
|
50
|
+
}
|
|
51
|
+
// Helper records a static api.<group>.<op>(...) call, or flips
|
|
52
|
+
// dynamicCallsDetected when the call expression escapes static resolution.
|
|
53
|
+
const tryRecordCall = (calleeNode) => {
|
|
54
|
+
let callee = calleeNode;
|
|
55
|
+
// Unwrap one `.call` / `.apply` / `.bind` redirection:
|
|
56
|
+
// api.calendar.events_list.call(null, {...})
|
|
57
|
+
// has callee = MemberExpression { object: api.calendar.events_list, property: 'call' }
|
|
58
|
+
if ((t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
|
59
|
+
!callee.computed &&
|
|
60
|
+
t.isIdentifier(callee.property) &&
|
|
61
|
+
(callee.property.name === 'call' ||
|
|
62
|
+
callee.property.name === 'apply' ||
|
|
63
|
+
callee.property.name === 'bind')) {
|
|
64
|
+
callee = callee.object;
|
|
65
|
+
}
|
|
66
|
+
if (!t.isMemberExpression(callee) && !t.isOptionalMemberExpression(callee))
|
|
67
|
+
return;
|
|
68
|
+
const groupExpr = callee.object;
|
|
69
|
+
if (!t.isMemberExpression(groupExpr) && !t.isOptionalMemberExpression(groupExpr))
|
|
70
|
+
return;
|
|
71
|
+
if (!t.isIdentifier(groupExpr.object, { name: 'api' }))
|
|
72
|
+
return;
|
|
73
|
+
// api[groupVar].op(...) or api.group[fnVar](...) → dynamic
|
|
74
|
+
if (groupExpr.computed || callee.computed) {
|
|
75
|
+
dynamicCallsDetected = true;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const groupNode = groupExpr.property;
|
|
79
|
+
const opNode = callee.property;
|
|
80
|
+
if (!t.isIdentifier(groupNode) || !t.isIdentifier(opNode)) {
|
|
81
|
+
dynamicCallsDetected = true;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const key = `${groupNode.name}.${opNode.name}`;
|
|
85
|
+
if (!seen.has(key)) {
|
|
86
|
+
seen.add(key);
|
|
87
|
+
calls.push({ apiGroup: groupNode.name, operationId: opNode.name });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
traverse(ast, {
|
|
92
|
+
// Assignments / destructures that alias `api` or `api.<group>` — any of
|
|
93
|
+
// these lets the code reach an api group via an opaque identifier later.
|
|
94
|
+
VariableDeclarator(path) {
|
|
95
|
+
const init = path.node.init;
|
|
96
|
+
if (!init)
|
|
97
|
+
return;
|
|
98
|
+
// const { calendar } = api
|
|
99
|
+
if (t.isObjectPattern(path.node.id) && t.isIdentifier(init, { name: 'api' })) {
|
|
100
|
+
dynamicCallsDetected = true;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// const x = api
|
|
104
|
+
if (t.isIdentifier(path.node.id) && t.isIdentifier(init, { name: 'api' })) {
|
|
105
|
+
dynamicCallsDetected = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// const x = api.<group> OR const x = api['<group>']
|
|
109
|
+
if (t.isIdentifier(path.node.id) &&
|
|
110
|
+
t.isMemberExpression(init) &&
|
|
111
|
+
t.isIdentifier(init.object, { name: 'api' })) {
|
|
112
|
+
dynamicCallsDetected = true;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
// Any other mention of `api` that hands it off to an opaque consumer:
|
|
116
|
+
// fn(api) — alias escape via function argument
|
|
117
|
+
// Object.values(api) / keys(…) — enumeration
|
|
118
|
+
// { ...api } / [ ...api ] — spread
|
|
119
|
+
// return api — caller gets the alias
|
|
120
|
+
// api = x (reassignment) — later reads hit a different object
|
|
121
|
+
//
|
|
122
|
+
// The api.<group>.<op>(...) pattern is recognised by the CallExpression
|
|
123
|
+
// visitor below; skip it here via parent-shape whitelisting.
|
|
124
|
+
Identifier(path) {
|
|
125
|
+
if (path.node.name !== 'api')
|
|
126
|
+
return;
|
|
127
|
+
// Skip the PROPERTY position of a member expression — e.g.
|
|
128
|
+
// `this.api`, `window.api`, `someObj.api`. That's not the
|
|
129
|
+
// global `api` binding we care about.
|
|
130
|
+
if ((t.isMemberExpression(path.parent) || t.isOptionalMemberExpression(path.parent)) &&
|
|
131
|
+
path.parent.property === path.node &&
|
|
132
|
+
!path.parent.computed) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// `api.<group>` (non-computed member) — safe, handled by CallExpression.
|
|
136
|
+
if ((t.isMemberExpression(path.parent) || t.isOptionalMemberExpression(path.parent)) &&
|
|
137
|
+
path.parent.object === path.node &&
|
|
138
|
+
!path.parent.computed) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Left-hand side of `const x = api` / `const { calendar } = api`
|
|
142
|
+
// — handled by VariableDeclarator above (dynamic flag set there).
|
|
143
|
+
if (t.isVariableDeclarator(path.parent) && path.parent.init === path.node) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Skip declaration positions where `api` is a local binding name,
|
|
147
|
+
// not a reference:
|
|
148
|
+
// { api: value } — object property key
|
|
149
|
+
// function f(api) { ... } — param name
|
|
150
|
+
// class { api() {...} } — method name
|
|
151
|
+
// function api() {} — function name
|
|
152
|
+
// class api {} — class name
|
|
153
|
+
if ((t.isObjectProperty(path.parent) || t.isObjectMethod(path.parent)) &&
|
|
154
|
+
path.parent.key === path.node &&
|
|
155
|
+
!path.parent.computed) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (t.isClassMethod(path.parent) && path.parent.key === path.node && !path.parent.computed) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if ((t.isFunctionDeclaration(path.parent) ||
|
|
162
|
+
t.isFunctionExpression(path.parent) ||
|
|
163
|
+
t.isClassDeclaration(path.parent) ||
|
|
164
|
+
t.isClassExpression(path.parent)) &&
|
|
165
|
+
path.parent.id === path.node) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (path.parentPath?.isFunction() && path.listKey === 'params') {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (t.isImportSpecifier(path.parent) || t.isImportDefaultSpecifier(path.parent) || t.isImportNamespaceSpecifier(path.parent)) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Anything else (`Object.values(api)`, `fn(api)`, `{ ...api }`,
|
|
175
|
+
// `return api`, `api = ...`) escapes static resolution.
|
|
176
|
+
dynamicCallsDetected = true;
|
|
177
|
+
},
|
|
178
|
+
CallExpression(path) {
|
|
179
|
+
tryRecordCall(path.node.callee);
|
|
180
|
+
},
|
|
181
|
+
OptionalCallExpression(path) {
|
|
182
|
+
tryRecordCall(path.node.callee);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Visitor error → fail-closed (should be unreachable under our plugin set).
|
|
188
|
+
return { apiCalls: [], dynamicCallsDetected: true };
|
|
189
|
+
}
|
|
190
|
+
return { apiCalls: calls, dynamicCallsDetected };
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=api-call-analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-call-analyzer.js","sourceRoot":"","sources":["../src/api-call-analyzer.ts"],"names":[],"mappings":"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"}
|