@suss/contract-aws-apigateway 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,557 @@
1
+ // src/http.ts
2
+ import { restBinding as restBinding2 } from "@suss/behavioral-ir";
3
+
4
+ // src/shared.ts
5
+ import { restBinding } from "@suss/behavioral-ir";
6
+ var FRAMEWORK = "apigateway";
7
+ var PROTOCOL = "http";
8
+ function throttleEnforces(throttle) {
9
+ const enforces = (v) => v !== void 0 && v > 0;
10
+ return enforces(throttle.burstLimit) || enforces(throttle.rateLimit);
11
+ }
12
+ function integrationCanTimeOut(integration) {
13
+ return integration.type !== "mock" && integration.type !== "unknown";
14
+ }
15
+ function integrationCanFail(integration) {
16
+ return integration.type !== "mock" && integration.type !== "unknown";
17
+ }
18
+ function buildCorsPreflightSummary(options) {
19
+ const { apiId, path, cors, sourceFile, extraMetadata } = options;
20
+ const ownerKey = `${apiId}:OPTIONS:${path}:cors`;
21
+ const headers = {
22
+ "Access-Control-Allow-Origin": {
23
+ type: "literal",
24
+ value: cors.allowOrigins.join(",")
25
+ },
26
+ "Access-Control-Allow-Methods": {
27
+ type: "literal",
28
+ value: cors.allowMethods.join(",")
29
+ }
30
+ };
31
+ if (cors.allowHeaders !== void 0 && cors.allowHeaders.length > 0) {
32
+ headers["Access-Control-Allow-Headers"] = {
33
+ type: "literal",
34
+ value: cors.allowHeaders.join(",")
35
+ };
36
+ }
37
+ if (cors.exposeHeaders !== void 0 && cors.exposeHeaders.length > 0) {
38
+ headers["Access-Control-Expose-Headers"] = {
39
+ type: "literal",
40
+ value: cors.exposeHeaders.join(",")
41
+ };
42
+ }
43
+ if (cors.allowCredentials === true) {
44
+ headers["Access-Control-Allow-Credentials"] = {
45
+ type: "literal",
46
+ value: "true"
47
+ };
48
+ }
49
+ if (cors.maxAge !== void 0) {
50
+ headers["Access-Control-Max-Age"] = {
51
+ type: "literal",
52
+ value: String(cors.maxAge)
53
+ };
54
+ }
55
+ const transition = {
56
+ id: `${ownerKey}:platform:204`,
57
+ conditions: [
58
+ {
59
+ type: "opaque",
60
+ sourceText: "aws:apigateway:cors-preflight",
61
+ reason: "externalFunction"
62
+ }
63
+ ],
64
+ output: {
65
+ type: "response",
66
+ statusCode: { type: "literal", value: 204 },
67
+ body: null,
68
+ headers
69
+ },
70
+ effects: [],
71
+ location: { start: 0, end: 0 },
72
+ isDefault: false,
73
+ confidence: { source: "derived", level: "high" },
74
+ metadata: {
75
+ source: "aws::apigateway::platform",
76
+ platform: "apiGateway",
77
+ causes: ["cors-preflight"],
78
+ ...cors.configRef !== void 0 ? { configRefs: [cors.configRef] } : {}
79
+ }
80
+ };
81
+ return {
82
+ kind: "handler",
83
+ location: {
84
+ file: `${sourceFile}:${ownerKey}`,
85
+ range: { start: 0, end: 0 },
86
+ exportName: null
87
+ },
88
+ identity: {
89
+ name: ownerKey,
90
+ exportPath: null,
91
+ boundaryBinding: restBinding({
92
+ transport: PROTOCOL,
93
+ method: "OPTIONS",
94
+ path,
95
+ recognition: FRAMEWORK
96
+ })
97
+ },
98
+ inputs: [],
99
+ transitions: [transition],
100
+ gaps: [],
101
+ confidence: { source: "derived", level: "high" },
102
+ metadata: {
103
+ apiId,
104
+ synthetic: "cors-preflight",
105
+ ...extraMetadata ?? {}
106
+ }
107
+ };
108
+ }
109
+
110
+ // src/transitions.ts
111
+ function makeContribution(cause, configRef) {
112
+ if (configRef === void 0) {
113
+ return { cause };
114
+ }
115
+ return { cause, configRef };
116
+ }
117
+ function handlerTransition(args) {
118
+ return {
119
+ id: `${args.ownerKey}:integration:${args.statusCode}`,
120
+ conditions: [],
121
+ output: {
122
+ type: "response",
123
+ statusCode: { type: "literal", value: args.statusCode },
124
+ body: null,
125
+ headers: {}
126
+ },
127
+ effects: [],
128
+ location: { start: 0, end: 0 },
129
+ isDefault: false,
130
+ confidence: { source: "derived", level: "high" },
131
+ metadata: {
132
+ source: args.source,
133
+ ...args.configRef !== void 0 ? { configRef: args.configRef } : {}
134
+ }
135
+ };
136
+ }
137
+ function platformTransition(args) {
138
+ if (args.contributions.length === 0) {
139
+ return null;
140
+ }
141
+ const causes = unique(args.contributions.map((c) => c.cause));
142
+ const refs = args.contributions.map((c) => c.configRef).filter((r) => r !== void 0);
143
+ const predicate = {
144
+ type: "opaque",
145
+ sourceText: `aws:apigateway:status-${args.statusCode}`,
146
+ reason: "externalFunction"
147
+ };
148
+ return {
149
+ id: `${args.ownerKey}:platform:${args.statusCode}`,
150
+ conditions: [predicate],
151
+ output: {
152
+ type: "response",
153
+ statusCode: { type: "literal", value: args.statusCode },
154
+ body: null,
155
+ headers: {}
156
+ },
157
+ effects: [],
158
+ location: { start: 0, end: 0 },
159
+ isDefault: false,
160
+ confidence: { source: "derived", level: "high" },
161
+ metadata: {
162
+ source: "aws::apigateway::platform",
163
+ platform: "apiGateway",
164
+ causes,
165
+ ...refs.length > 0 ? { configRefs: refs } : {}
166
+ }
167
+ };
168
+ }
169
+ function unique(values) {
170
+ return Array.from(new Set(values));
171
+ }
172
+
173
+ // src/http.ts
174
+ function httpApiToSummaries(config) {
175
+ const sourceFile = config.source ?? `aws-apigateway:${config.id}`;
176
+ const summaries = [];
177
+ for (const route of config.routes) {
178
+ const parsed = parseRouteKey(route.routeKey);
179
+ if (parsed === null) {
180
+ continue;
181
+ }
182
+ summaries.push(buildRouteSummary(route, parsed, config, sourceFile));
183
+ }
184
+ if (config.cors !== void 0) {
185
+ for (const path of uniquePaths(config.routes)) {
186
+ summaries.push(
187
+ buildCorsPreflightSummary({
188
+ apiId: config.id,
189
+ path,
190
+ cors: config.cors,
191
+ sourceFile,
192
+ extraMetadata: { apiVersion: "v2" }
193
+ })
194
+ );
195
+ }
196
+ }
197
+ return summaries;
198
+ }
199
+ function parseRouteKey(routeKey) {
200
+ const trimmed = routeKey.trim();
201
+ if (trimmed === "" || trimmed === "$default") {
202
+ return null;
203
+ }
204
+ const space = trimmed.indexOf(" ");
205
+ if (space < 0) {
206
+ return null;
207
+ }
208
+ const method = trimmed.slice(0, space).toUpperCase();
209
+ const path = trimmed.slice(space + 1).trim();
210
+ if (method === "" || path === "") {
211
+ return null;
212
+ }
213
+ return { method, path };
214
+ }
215
+ function buildRouteSummary(route, parsed, api, sourceFile) {
216
+ const ownerKey = route.name ?? `${api.id}:${parsed.method}:${parsed.path}`;
217
+ const transitions = [];
218
+ for (const code of route.integration.statusCodes) {
219
+ transitions.push(
220
+ handlerTransition({
221
+ ownerKey,
222
+ statusCode: code,
223
+ source: `aws::apigateway::integration.${route.integration.type}`,
224
+ configRef: route.integration.configRef
225
+ })
226
+ );
227
+ }
228
+ const platformByStatus = collectPlatformContributions(route, api);
229
+ for (const [status, contributions] of platformByStatus) {
230
+ const transition = platformTransition({
231
+ ownerKey,
232
+ statusCode: status,
233
+ contributions
234
+ });
235
+ if (transition !== null) {
236
+ transitions.push(transition);
237
+ }
238
+ }
239
+ if (transitions.length === 0) {
240
+ transitions.push({
241
+ id: `${ownerKey}:integration:default`,
242
+ conditions: [],
243
+ output: {
244
+ type: "response",
245
+ statusCode: null,
246
+ body: null,
247
+ headers: {}
248
+ },
249
+ effects: [],
250
+ location: { start: 0, end: 0 },
251
+ isDefault: true,
252
+ confidence: { source: "derived", level: "low" },
253
+ metadata: {
254
+ source: `aws::apigateway::integration.${route.integration.type}`
255
+ }
256
+ });
257
+ }
258
+ return {
259
+ kind: "handler",
260
+ location: {
261
+ file: `${sourceFile}:${ownerKey}`,
262
+ range: { start: 0, end: 0 },
263
+ exportName: null
264
+ },
265
+ identity: {
266
+ name: ownerKey,
267
+ exportPath: null,
268
+ boundaryBinding: restBinding2({
269
+ transport: PROTOCOL,
270
+ method: parsed.method,
271
+ path: parsed.path,
272
+ recognition: FRAMEWORK
273
+ })
274
+ },
275
+ inputs: [],
276
+ transitions,
277
+ gaps: [],
278
+ confidence: { source: "derived", level: "high" },
279
+ metadata: {
280
+ apiId: api.id,
281
+ apiVersion: "v2",
282
+ integrationType: route.integration.type,
283
+ http: {
284
+ declaredContract: {
285
+ framework: FRAMEWORK,
286
+ provenance: "independent",
287
+ responses: route.integration.statusCodes.map((statusCode) => ({
288
+ statusCode
289
+ }))
290
+ },
291
+ // Additive pointer to the code that implements this route (SAM
292
+ // Lambda proxy Handler), so a checker can correlate the declared
293
+ // route with the extracted handler summary carrying the same
294
+ // REST binding.
295
+ ...route.implementingHandler !== void 0 ? { implementingHandler: route.implementingHandler } : {}
296
+ }
297
+ }
298
+ };
299
+ }
300
+ function collectPlatformContributions(route, api) {
301
+ const buckets = /* @__PURE__ */ new Map();
302
+ const add = (status, contribution) => {
303
+ const existing = buckets.get(status);
304
+ if (existing === void 0) {
305
+ buckets.set(status, [contribution]);
306
+ } else {
307
+ existing.push(contribution);
308
+ }
309
+ };
310
+ const authorizer = resolveAuthorizer(route, api);
311
+ if (authorizer !== null) {
312
+ if (authorizer.identitySourceRequired !== false) {
313
+ add(401, makeContribution("authorization", authorizer.configRef));
314
+ }
315
+ add(403, makeContribution("authorization", authorizer.configRef));
316
+ }
317
+ const throttle = resolveThrottle(route, api);
318
+ if (throttle !== null && throttleEnforces(throttle)) {
319
+ add(429, makeContribution("throttle", throttle.configRef));
320
+ }
321
+ if (integrationCanTimeOut(route.integration)) {
322
+ add(
323
+ 504,
324
+ makeContribution("integration-timeout", route.integration.configRef)
325
+ );
326
+ }
327
+ if (integrationCanFail(route.integration)) {
328
+ add(
329
+ 502,
330
+ makeContribution("integration-failure", route.integration.configRef)
331
+ );
332
+ }
333
+ return buckets;
334
+ }
335
+ function resolveAuthorizer(route, api) {
336
+ if (route.authorizer === null) {
337
+ return null;
338
+ }
339
+ if (route.authorizer !== void 0) {
340
+ return route.authorizer;
341
+ }
342
+ return api.defaultAuthorizer ?? null;
343
+ }
344
+ function resolveThrottle(route, api) {
345
+ if (route.throttle !== void 0) {
346
+ return route.throttle;
347
+ }
348
+ return api.defaultThrottle ?? null;
349
+ }
350
+ function uniquePaths(routes) {
351
+ const seen = /* @__PURE__ */ new Set();
352
+ const out = [];
353
+ for (const r of routes) {
354
+ const parsed = parseRouteKey(r.routeKey);
355
+ if (parsed === null || seen.has(parsed.path)) {
356
+ continue;
357
+ }
358
+ seen.add(parsed.path);
359
+ out.push(parsed.path);
360
+ }
361
+ return out;
362
+ }
363
+
364
+ // src/rest.ts
365
+ import { restBinding as restBinding3 } from "@suss/behavioral-ir";
366
+ function restApiToSummaries(config) {
367
+ const sourceFile = config.source ?? `aws-apigateway:${config.id}`;
368
+ const summaries = [];
369
+ for (const endpoint of config.endpoints) {
370
+ summaries.push(buildEndpointSummary(endpoint, config, sourceFile));
371
+ }
372
+ if (config.cors !== void 0) {
373
+ for (const endpoint of dedupeByPath(config.endpoints)) {
374
+ summaries.push(
375
+ buildCorsPreflightSummary({
376
+ apiId: config.id,
377
+ path: endpoint.path,
378
+ cors: config.cors,
379
+ sourceFile
380
+ })
381
+ );
382
+ }
383
+ }
384
+ return summaries;
385
+ }
386
+ function buildEndpointSummary(endpoint, api, sourceFile) {
387
+ const method = endpoint.method.toUpperCase();
388
+ const ownerKey = endpoint.name ?? `${api.id}:${method}:${endpoint.path}`;
389
+ const transitions = [];
390
+ for (const code of endpoint.integration.statusCodes) {
391
+ transitions.push(
392
+ handlerTransition({
393
+ ownerKey,
394
+ statusCode: code,
395
+ source: `aws::apigateway::integration.${endpoint.integration.type}`,
396
+ configRef: endpoint.integration.configRef
397
+ })
398
+ );
399
+ }
400
+ const platformByStatus = collectPlatformContributions2(endpoint, api);
401
+ for (const [status, contributions] of platformByStatus) {
402
+ const transition = platformTransition({
403
+ ownerKey,
404
+ statusCode: status,
405
+ contributions
406
+ });
407
+ if (transition !== null) {
408
+ transitions.push(transition);
409
+ }
410
+ }
411
+ if (transitions.length === 0) {
412
+ transitions.push({
413
+ id: `${ownerKey}:integration:default`,
414
+ conditions: [],
415
+ output: {
416
+ type: "response",
417
+ statusCode: null,
418
+ body: null,
419
+ headers: {}
420
+ },
421
+ effects: [],
422
+ location: { start: 0, end: 0 },
423
+ isDefault: true,
424
+ confidence: { source: "derived", level: "low" },
425
+ metadata: {
426
+ source: `aws::apigateway::integration.${endpoint.integration.type}`
427
+ }
428
+ });
429
+ }
430
+ return {
431
+ kind: "handler",
432
+ location: {
433
+ file: `${sourceFile}:${ownerKey}`,
434
+ range: { start: 0, end: 0 },
435
+ exportName: null
436
+ },
437
+ identity: {
438
+ name: ownerKey,
439
+ exportPath: null,
440
+ boundaryBinding: restBinding3({
441
+ transport: PROTOCOL,
442
+ method,
443
+ path: endpoint.path,
444
+ recognition: FRAMEWORK
445
+ })
446
+ },
447
+ inputs: [],
448
+ transitions,
449
+ gaps: [],
450
+ confidence: { source: "derived", level: "high" },
451
+ metadata: {
452
+ apiId: api.id,
453
+ integrationType: endpoint.integration.type,
454
+ http: {
455
+ // The integration's declared status codes (from MethodResponses
456
+ // in the CFN template) are the declared contract here. Platform-
457
+ // injected transitions (authorizer 401/403, throttle 429, etc.)
458
+ // come from SEPARATE config fields and so are independent from
459
+ // this contract — contract-consistency comparison is meaningful
460
+ // and will surface template-internal inconsistencies.
461
+ declaredContract: {
462
+ framework: FRAMEWORK,
463
+ provenance: "independent",
464
+ responses: endpoint.integration.statusCodes.map((statusCode) => ({
465
+ statusCode
466
+ }))
467
+ },
468
+ // Additive pointer to the code that implements this endpoint
469
+ // (SAM Lambda proxy Handler), so a checker can correlate the
470
+ // declared route with the extracted handler summary carrying the
471
+ // same REST binding.
472
+ ...endpoint.implementingHandler !== void 0 ? { implementingHandler: endpoint.implementingHandler } : {}
473
+ }
474
+ }
475
+ };
476
+ }
477
+ function collectPlatformContributions2(endpoint, api) {
478
+ const buckets = /* @__PURE__ */ new Map();
479
+ const add = (status, contribution) => {
480
+ const existing = buckets.get(status);
481
+ if (existing === void 0) {
482
+ buckets.set(status, [contribution]);
483
+ } else {
484
+ existing.push(contribution);
485
+ }
486
+ };
487
+ const authorizer = resolveAuthorizer2(endpoint, api);
488
+ if (authorizer !== null) {
489
+ if (authorizer.identitySourceRequired !== false) {
490
+ add(401, makeContribution("authorization", authorizer.configRef));
491
+ }
492
+ add(403, makeContribution("authorization", authorizer.configRef));
493
+ }
494
+ if (endpoint.apiKeyRequired === true) {
495
+ add(403, makeContribution("api-key", endpoint.configRef));
496
+ }
497
+ if (endpoint.requestValidation !== void 0) {
498
+ const v = endpoint.requestValidation;
499
+ if (v.body === true || v.params === true || v.headers === true) {
500
+ add(
501
+ 400,
502
+ makeContribution(
503
+ "request-validation",
504
+ v.configRef ?? endpoint.configRef
505
+ )
506
+ );
507
+ }
508
+ }
509
+ const throttle = resolveThrottle2(endpoint, api);
510
+ if (throttle !== null && throttleEnforces(throttle)) {
511
+ add(429, makeContribution("throttle", throttle.configRef));
512
+ }
513
+ if (integrationCanTimeOut(endpoint.integration)) {
514
+ add(
515
+ 504,
516
+ makeContribution("integration-timeout", endpoint.integration.configRef)
517
+ );
518
+ }
519
+ if (integrationCanFail(endpoint.integration)) {
520
+ add(
521
+ 502,
522
+ makeContribution("integration-failure", endpoint.integration.configRef)
523
+ );
524
+ }
525
+ return buckets;
526
+ }
527
+ function resolveAuthorizer2(endpoint, api) {
528
+ if (endpoint.authorizer === null) {
529
+ return null;
530
+ }
531
+ if (endpoint.authorizer !== void 0) {
532
+ return endpoint.authorizer;
533
+ }
534
+ return api.defaultAuthorizer ?? null;
535
+ }
536
+ function resolveThrottle2(endpoint, api) {
537
+ if (endpoint.throttle !== void 0) {
538
+ return endpoint.throttle;
539
+ }
540
+ return api.defaultThrottle ?? null;
541
+ }
542
+ function dedupeByPath(endpoints) {
543
+ const seen = /* @__PURE__ */ new Set();
544
+ const out = [];
545
+ for (const e of endpoints) {
546
+ if (seen.has(e.path)) {
547
+ continue;
548
+ }
549
+ seen.add(e.path);
550
+ out.push(e);
551
+ }
552
+ return out;
553
+ }
554
+ export {
555
+ httpApiToSummaries,
556
+ restApiToSummaries
557
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@suss/contract-aws-apigateway",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Generate suss behavioral summaries for AWS API Gateway resources (REST + HTTP API), independent of how they were deployed.",
6
+ "keywords": [
7
+ "suss",
8
+ "behavioral-summary",
9
+ "aws",
10
+ "api-gateway",
11
+ "apigateway"
12
+ ],
13
+ "license": "Apache-2.0",
14
+ "author": "Nimbus Cloud AI LLC",
15
+ "homepage": "https://github.com/nimbuscloud-ai/suss/tree/main/packages/contract/aws-apigateway#readme",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/nimbuscloud-ai/suss.git",
19
+ "directory": "packages/contract/aws-apigateway"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/nimbuscloud-ai/suss/issues"
23
+ },
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "require": "./dist/index.cjs"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "test": "vitest run",
42
+ "test:coverage": "vitest run --coverage",
43
+ "lint": "biome check .",
44
+ "lint:fix": "biome check --write .",
45
+ "typecheck": "tsc --noEmit"
46
+ },
47
+ "dependencies": {
48
+ "@suss/behavioral-ir": "0.0.1"
49
+ },
50
+ "devDependencies": {
51
+ "tsup": "*",
52
+ "typescript": "*",
53
+ "vitest": "*"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }