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