@suss/manifest-aws 0.3.2 → 0.4.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/dist/index.cjs CHANGED
@@ -39,7 +39,13 @@ __export(index_exports, {
39
39
  qualifiedLogicalId: () => qualifiedLogicalId,
40
40
  readAppSyncResolvers: () => readAppSyncResolvers,
41
41
  readServerlessFunctions: () => readServerlessFunctions,
42
+ reduceEventPattern: () => reduceEventPattern,
42
43
  refTarget: () => refTarget,
44
+ resolveBucketChannel: () => resolveBucketChannel,
45
+ resolveEventBusToken: () => resolveEventBusToken,
46
+ resolveQueueChannel: () => resolveQueueChannel,
47
+ resolveResourceChannel: () => resolveResourceChannel,
48
+ resolveTopicChannel: () => resolveTopicChannel,
43
49
  resourcesWithGlobals: () => resourcesWithGlobals,
44
50
  unfollowedStackMessage: () => unfollowedStackMessage
45
51
  });
@@ -63,7 +69,11 @@ var getAttParts = (value) => {
63
69
  return value.map(String);
64
70
  }
65
71
  const text = String(value);
66
- return text.includes(".") ? text.split(".") : [text];
72
+ const dot = text.indexOf(".");
73
+ if (dot === -1) {
74
+ return [text];
75
+ }
76
+ return [text.slice(0, dot), text.slice(dot + 1)];
67
77
  };
68
78
  var CLOUDFORMATION_YAML_TAGS = [
69
79
  ...everyNodeKind("!Ref", (value) => ({ Ref: value })),
@@ -115,8 +125,7 @@ function refTarget(value) {
115
125
  return getAtt[0];
116
126
  }
117
127
  if (typeof getAtt === "string") {
118
- const dot = getAtt.indexOf(".");
119
- return dot === -1 ? getAtt : getAtt.slice(0, dot);
128
+ return getAttParts(getAtt)[0] ?? null;
120
129
  }
121
130
  return null;
122
131
  }
@@ -280,6 +289,102 @@ function stringOf(value) {
280
289
  return typeof value === "string" ? value : null;
281
290
  }
282
291
 
292
+ // src/arn.ts
293
+ function resolveResourceChannel(value, service) {
294
+ if (value === null || value === void 0) {
295
+ return null;
296
+ }
297
+ if (typeof value === "string") {
298
+ const resource = resolveArnResource(value, service);
299
+ return resource ?? value;
300
+ }
301
+ return refTarget(value);
302
+ }
303
+ function resolveArnResource(value, service) {
304
+ const parts = value.split(":");
305
+ if (parts[0] !== "arn" || parts[2] !== service) {
306
+ return null;
307
+ }
308
+ const region = parts[3];
309
+ const account = parts[4];
310
+ const resource = parts.slice(5).join(":");
311
+ if (service === "s3") {
312
+ if (region !== "" || account !== "" || resource === "") {
313
+ return null;
314
+ }
315
+ const slash = resource.indexOf("/");
316
+ const bucket = slash === -1 ? resource : resource.slice(0, slash);
317
+ return bucket === "" ? null : bucket;
318
+ }
319
+ if (!region || !account || resource === "") {
320
+ return null;
321
+ }
322
+ return resource;
323
+ }
324
+ function resolveQueueChannel(value) {
325
+ return resolveResourceChannel(value, "sqs");
326
+ }
327
+ function resolveTopicChannel(value) {
328
+ return resolveResourceChannel(value, "sns");
329
+ }
330
+ function resolveBucketChannel(value) {
331
+ return resolveResourceChannel(value, "s3");
332
+ }
333
+
334
+ // src/eventPattern.ts
335
+ function reduceEventPattern(pattern) {
336
+ if (pattern === null || typeof pattern !== "object") {
337
+ return {
338
+ kind: "unresolvable",
339
+ reason: "rule declares no EventPattern"
340
+ };
341
+ }
342
+ const detailType = pattern["detail-type"];
343
+ if (detailType === void 0) {
344
+ return {
345
+ kind: "unresolvable",
346
+ reason: "EventPattern has no detail-type; v0 reduces routing to exact detail-type match only"
347
+ };
348
+ }
349
+ if (!Array.isArray(detailType)) {
350
+ return {
351
+ kind: "unresolvable",
352
+ reason: "detail-type is not a plain array of string literals"
353
+ };
354
+ }
355
+ const detailTypes = [];
356
+ for (const entry of detailType) {
357
+ if (typeof entry !== "string") {
358
+ return {
359
+ kind: "unresolvable",
360
+ reason: "detail-type contains a content filter (prefix / anything-but / etc.); pattern subsumption is out of v0 scope"
361
+ };
362
+ }
363
+ detailTypes.push(entry);
364
+ }
365
+ if (detailTypes.length === 0) {
366
+ return { kind: "unresolvable", reason: "detail-type array is empty" };
367
+ }
368
+ return { kind: "exact", detailTypes };
369
+ }
370
+ function resolveEventBusToken(value) {
371
+ if (value === null || value === void 0) {
372
+ return "default";
373
+ }
374
+ if (typeof value === "object") {
375
+ const logicalId = refTarget(value);
376
+ return logicalId ?? "default";
377
+ }
378
+ if (typeof value === "string") {
379
+ const arnMatch = value.match(/:event-bus\/(.+)$/);
380
+ if (arnMatch !== null) {
381
+ return arnMatch[1];
382
+ }
383
+ return value;
384
+ }
385
+ return "default";
386
+ }
387
+
283
388
  // src/globals.ts
284
389
  var GLOBALS_SECTION_TYPES = {
285
390
  Api: "AWS::Serverless::Api",
@@ -594,7 +699,13 @@ function readServerlessFunctions(template) {
594
699
  qualifiedLogicalId,
595
700
  readAppSyncResolvers,
596
701
  readServerlessFunctions,
702
+ reduceEventPattern,
597
703
  refTarget,
704
+ resolveBucketChannel,
705
+ resolveEventBusToken,
706
+ resolveQueueChannel,
707
+ resolveResourceChannel,
708
+ resolveTopicChannel,
598
709
  resourcesWithGlobals,
599
710
  unfollowedStackMessage
600
711
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,15 @@
1
1
  import YAML from 'yaml';
2
2
 
3
+ /**
4
+ * templateLoader.ts parses a CloudFormation or SAM template file into a
5
+ * plain object, resolving the shorthand YAML tags CFN uses for intrinsics.
6
+ *
7
+ * This is the one place that turns a template on disk into data. The
8
+ * summary-generation paths and the code-side handler pairing both read
9
+ * through it, so the YAML and JSON parsing, along with the handling of
10
+ * intrinsic tags, lives here instead of being repeated per consumer.
11
+ */
12
+
3
13
  interface CloudFormationResource {
4
14
  Type?: string;
5
15
  Properties?: Record<string, unknown>;
@@ -8,7 +18,7 @@ interface CloudFormationResource {
8
18
  interface CloudFormationTemplate {
9
19
  /**
10
20
  * SAM `Globals` block. Section keys (`Function`, `Api`, `HttpApi`)
11
- * hold defaults that individual resources inherit unless they
21
+ * contain defaults that individual resources inherit unless they
12
22
  * override the property themselves.
13
23
  */
14
24
  Globals?: Record<string, Record<string, unknown>>;
@@ -16,29 +26,44 @@ interface CloudFormationTemplate {
16
26
  }
17
27
  declare const CLOUDFORMATION_YAML_TAGS: YAML.CollectionTag[];
18
28
  /**
19
- * Load a CloudFormation / SAM template from disk. Format is detected by
20
- * extension; `.json` parses as JSON, everything else (including
21
- * `.yaml` / `.yml` / `.template`) goes through the YAML parser with the
29
+ * Load a CloudFormation or SAM template from disk. The format comes from
30
+ * the extension: `.json` parses as JSON, and everything else (including
31
+ * `.yaml`, `.yml`, and `.template`) goes through the YAML parser with the
22
32
  * intrinsic tags registered.
23
33
  *
24
- * Throws when the file is missing or the parsed value isn't an object —
25
- * a malformed manifest is a load-time error, not a silent empty result.
34
+ * Throws when the file is missing or the parsed value isn't an object. A
35
+ * malformed manifest is a load-time error rather than an empty result.
26
36
  */
27
37
  declare function loadCloudFormationTemplate(templatePath: string): CloudFormationTemplate;
28
38
  /**
29
- * CloudFormation references show up in four shapes after parsing:
39
+ * CloudFormation references come out of the parser in four forms:
30
40
  * - { Ref: "LogicalId" }
31
41
  * - { "Fn::GetAtt": ["LogicalId", "Attr"] }
32
42
  * - { "Fn::GetAtt": "LogicalId.Attr" }, the full-form spelling of the
33
43
  * `!GetAtt` short form, which templates written by hand still use
34
44
  * - the bare logical id when the parser doesn't recognise the YAML tag
35
45
  *
36
- * Every CloudFormation property that names another resource accepts all
37
- * of them, so anything reading such a property should come through here
38
- * rather than matching one shape.
46
+ * Every CloudFormation property that points at another resource accepts
47
+ * all four, so anything reading such a property should go through here
48
+ * instead of matching a single form.
39
49
  */
40
50
  declare function refTarget(value: unknown): string | null;
41
51
 
52
+ /**
53
+ * appsyncResolvers.ts works out which GraphQL field each Lambda serves.
54
+ *
55
+ * AppSync does not wire a field to a handler in code. The template does it,
56
+ * over three hops: a resolver points at a pipeline function, the function
57
+ * points at a data source, and the data source points at a Lambda. Both ways
58
+ * of writing that are covered, the raw AWS::AppSync::* resources and the SAM
59
+ * AWS::Serverless::GraphQLApi shorthand whose DataSources, Functions, and
60
+ * Resolvers blocks the transform expands into those same resources.
61
+ *
62
+ * Like the rest of this package, this reports only what the template says.
63
+ * Whether a field has an implementation, and whether that implementation
64
+ * matches the schema, is for a reader above to decide.
65
+ */
66
+
42
67
  /** One GraphQL field and the Lambdas behind it. */
43
68
  interface AppSyncResolverBinding {
44
69
  typeName: string;
@@ -46,10 +71,10 @@ interface AppSyncResolverBinding {
46
71
  /**
47
72
  * Every Lambda a request for this field runs, in pipeline order.
48
73
  * Usually one. A pipeline can chain several, and nothing in the
49
- * template says which of them is the one that produces the value:
50
- * the first is as often an auth step as it is the resolver, and the
51
- * last is as often a formatter. So all of them are reported, and a
52
- * reader that needs to pick says so itself.
74
+ * template says which of them produces the value: the first is as
75
+ * often an auth step as it is the resolver, and the last is as often
76
+ * a formatter. So all of them are reported, and a reader that needs
77
+ * to pick one decides for itself.
53
78
  *
54
79
  * Empty when the field is served by something other than a Lambda.
55
80
  */
@@ -57,21 +82,125 @@ interface AppSyncResolverBinding {
57
82
  }
58
83
  declare function readAppSyncResolvers(template: CloudFormationTemplate): AppSyncResolverBinding[];
59
84
 
85
+ /**
86
+ * arn.ts resolves the references AWS manifests use to point at a queue, a
87
+ * topic, or a bucket, down to the channel string suss keys the boundary on.
88
+ *
89
+ * Every manifest language that targets AWS writes a reference one of three
90
+ * ways: as a CFN intrinsic (`!Ref X`, `!GetAtt X.Arn`), as a plain ARN
91
+ * string, or as a bare name. The CloudFormation reader and the Serverless
92
+ * Framework reader both resolve them here, so a queue written two different
93
+ * ways in two manifest languages still ends up on one channel.
94
+ */
95
+ /**
96
+ * Resolve a reference (`!Ref X`, `!GetAtt X.Arn`, plain string ARN) to
97
+ * the referenced resource's channel string. `service` is the ARN
98
+ * segment a plain string is checked against (`"sqs"`, `"sns"`, `"s3"`),
99
+ * so a queue ARN, a topic ARN, and a bucket ARN all resolve through the
100
+ * same code.
101
+ *
102
+ * Returns null when the reference is dynamic (a parameter, an import,
103
+ * or an Fn::Join that points at nothing this template declares); those
104
+ * need cross-stack resolution that's out of scope for v0.
105
+ */
106
+ declare function resolveResourceChannel(value: unknown, service: string): string | null;
107
+ /**
108
+ * Resolve a Queue reference (`!Ref X`, `!GetAtt X.Arn`, plain string
109
+ * ARN) to the queue's channel string.
110
+ */
111
+ declare function resolveQueueChannel(value: unknown): string | null;
112
+ /**
113
+ * Resolve a Topic reference (`!Ref X`, `!GetAtt X.Arn`, plain string
114
+ * ARN) to the topic's channel string.
115
+ */
116
+ declare function resolveTopicChannel(value: unknown): string | null;
117
+ /**
118
+ * Resolve a Bucket reference (`!Ref X`, `!GetAtt X.Arn`, plain string
119
+ * ARN) to the bucket's channel string.
120
+ */
121
+ declare function resolveBucketChannel(value: unknown): string | null;
122
+
123
+ /**
124
+ * eventPattern.ts reads what an EventBridge rule says about the events it
125
+ * matches, and which bus those events arrive on.
126
+ *
127
+ * A rule's EventPattern and its EventBusName are written the same way in a
128
+ * CloudFormation template and in a serverless.yml eventBridge event, so
129
+ * both readers reduce them here.
130
+ */
131
+ /**
132
+ * How far a rule's EventPattern reduced: either to the exact
133
+ * detail-types it lists, or not at all, along with a reason someone can
134
+ * read.
135
+ */
136
+ type PatternReduction = {
137
+ kind: "exact";
138
+ detailTypes: string[];
139
+ } | {
140
+ kind: "unresolvable";
141
+ reason: string;
142
+ };
143
+ /**
144
+ * Reduce a rule's EventPattern to the exact set of DetailTypes it
145
+ * matches. v0 handles only a literal `detail-type` array; anything else
146
+ * (absent detail-type, content-filter objects, empty array) is
147
+ * unresolvable, surfaced by the checker, never silently dropped.
148
+ */
149
+ declare function reduceEventPattern(pattern: unknown): PatternReduction;
150
+ /**
151
+ * Resolve a rule's EventBusName to the channel's bus token: the CFN
152
+ * logical id when it's a Ref/GetAtt, the name segment of an event-bus
153
+ * ARN, the literal string otherwise, or "default" when it's left out
154
+ * (the EventBridge default event bus).
155
+ */
156
+ declare function resolveEventBusToken(value: unknown): string;
157
+
158
+ /**
159
+ * globals.ts folds the SAM `Globals` section into the resources that
160
+ * inherit from it, so every reader sees one set of properties per resource
161
+ * and none of them has to know that `Globals` exists.
162
+ *
163
+ * SAM applies a section's properties to each resource of the matching type,
164
+ * and the resource's own value wins where both declare one, per
165
+ * https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy-globals.html
166
+ *
167
+ * That page has the combining rules too. A map is merged key by key, a list
168
+ * becomes the section's entries followed by the resource's, and anything
169
+ * else is replaced outright. Merging keys is what makes a function that sets
170
+ * one environment variable of its own still get the section's others.
171
+ */
172
+
60
173
  /**
61
174
  * The template's resources with each `Globals` section already applied
62
- * to the resources it covers. A resource of a type no section names, or
63
- * a template with no `Globals`, comes back untouched.
175
+ * to the resources it covers. A resource whose type no section covers,
176
+ * or a template with no `Globals`, comes back untouched.
64
177
  */
65
178
  declare function resourcesWithGlobals(template: CloudFormationTemplate): Record<string, CloudFormationResource>;
66
179
  /**
67
180
  * Per function logical id, the environment variables the `Function`
68
181
  * section supplies that the function does not declare for itself. A
69
182
  * consumer that treats a function's environment as its own contract
70
- * needs the distinction: a name every function in the document receives
71
- * says something about the document, not about any one function.
183
+ * needs the difference, because a variable every function in the
184
+ * document receives tells you about the document, not about one
185
+ * function.
72
186
  */
73
187
  declare function inheritedEnvVars(template: CloudFormationTemplate): Record<string, string[]>;
74
188
 
189
+ /**
190
+ * nestedStacks.ts reads a template and every child template it embeds, as a
191
+ * list of documents.
192
+ *
193
+ * A stack resource points at another template, and CloudFormation deploys that
194
+ * template's resources alongside the parent's. Each document keeps its own
195
+ * namespace, its own SAM `Globals` section, and its own directory, so the tree
196
+ * stays a list of documents. Merging them would apply one document's defaults
197
+ * to another's resources, and let two logical ids spelled the same way collide.
198
+ *
199
+ * A child's reference to one of its own parameters is not followed to what the
200
+ * parent bound, and a `Fn::GetAtt` on a stack resource is not followed to the
201
+ * child's output. Both point at nothing, like any reference we can't resolve.
202
+ */
203
+
75
204
  /**
76
205
  * How deep the reader follows children. CloudFormation itself allows
77
206
  * deeper nesting; a template chain this long in a repository is far
@@ -79,7 +208,7 @@ declare function inheritedEnvVars(template: CloudFormationTemplate): Record<stri
79
208
  * rather than applied silently.
80
209
  */
81
210
  declare const MAX_STACK_DEPTH = 10;
82
- /** One template file, and where it sits in the tree that reached it. */
211
+ /** One template file, and where it lives in the tree that reached it. */
83
212
  interface TemplateDocument {
84
213
  /** Absolute path of the file this document was read from. */
85
214
  path: string;
@@ -92,8 +221,8 @@ interface TemplateDocument {
92
221
  }
93
222
  /**
94
223
  * Why a child template was not read. Every value is a limit on what the
95
- * reader could reach, so a consumer should report it as reading that
96
- * stopped rather than treat the child as a template holding nothing.
224
+ * reader could reach, so a consumer should report it as a read that
225
+ * stopped rather than treat the child as an empty template.
97
226
  */
98
227
  type UnfollowedReason = "remoteUrl" | "notALiteralPath" | "fileMissing" | "unreadable" | "cycle" | "depthLimit";
99
228
  /** A child template the reader could not open, and what stopped it. */
@@ -105,7 +234,7 @@ interface UnfollowedStack {
105
234
  /** The template location as the document writes it, when it is a string. */
106
235
  templateUrl: string | null;
107
236
  reason: UnfollowedReason;
108
- /** One phrase naming what stopped the read, for a message to a person. */
237
+ /** One phrase saying what stopped the read, for a message to a person. */
109
238
  detail: string;
110
239
  }
111
240
  interface TemplateTree {
@@ -125,14 +254,27 @@ interface TemplateTree {
125
254
  declare function loadTemplateTree(rootPath: string): TemplateTree;
126
255
  /**
127
256
  * A logical id qualified by the stack path that reaches it, so two
128
- * documents that both declare `HandlerFunction` name two different
257
+ * documents that both declare `HandlerFunction` refer to two different
129
258
  * deployed things. A resource in the root document keeps its bare id,
130
259
  * which is also what CloudFormation shows for it.
131
260
  */
132
261
  declare function qualifiedLogicalId(stackPath: string[], logicalId: string): string;
133
- /** A line naming one child the reader could not open, and why. */
262
+ /** A line saying which child the reader could not open, and why. */
134
263
  declare function unfollowedStackMessage(stack: UnfollowedStack): string;
135
264
 
265
+ /**
266
+ * serverlessFunctions.ts reads AWS::Serverless::Function resources into a
267
+ * code-facing view: which module and export each function points at, and
268
+ * which route and non-route Events it declares.
269
+ *
270
+ * The summary-generation paths expand SAM Events into API Gateway route
271
+ * summaries. This reader covers the other question the code-side pairing
272
+ * needs: given the template, where is each handler's implementation, and
273
+ * what routes is it bound to? The manifest semantics (how a Handler string
274
+ * is laid out, how CodeUri is inherited from Globals, Api versus HttpApi
275
+ * Events) stay here rather than being reconstructed in a framework pack.
276
+ */
277
+
136
278
  /** A route-bearing SAM Event (`Type: Api` or `Type: HttpApi`). */
137
279
  interface ServerlessHttpRoute {
138
280
  /** Key of the event in the function's `Events` map. */
@@ -147,9 +289,10 @@ interface ServerlessHttpRoute {
147
289
  apiId: string | null;
148
290
  }
149
291
  /**
150
- * A recognized SAM Event that isn't an HTTP route — SQS / Schedule /
151
- * SNS / S3 / etc. Surfaced (not dropped) so the code side can account
152
- * for handlers it recognizes but deliberately doesn't extract as HTTP.
292
+ * A recognized SAM Event that isn't an HTTP route (SQS, Schedule, SNS,
293
+ * S3, and so on). These are reported rather than dropped, so the code
294
+ * side can account for handlers it recognizes but deliberately doesn't
295
+ * extract as HTTP.
153
296
  */
154
297
  interface ServerlessNonHttpEvent {
155
298
  eventId: string;
@@ -163,7 +306,7 @@ interface ServerlessFunctionInfo {
163
306
  handler: string;
164
307
  /** Module-path portion of the handler (everything before the final dot). */
165
308
  modulePath: string;
166
- /** Exported symbol the handler names (everything after the final dot). */
309
+ /** Exported symbol the handler points at (everything after the final dot). */
167
310
  exportName: string;
168
311
  /**
169
312
  * CodeUri base directory the handler resolves against: the function's
@@ -181,16 +324,16 @@ interface ParsedHandler {
181
324
  * Split a SAM Handler string into its module path and exported symbol.
182
325
  * The final dot separates them: `"src/handlers/confirmToken.handler"` →
183
326
  * `{ modulePath: "src/handlers/confirmToken", exportName: "handler" }`.
184
- * Returns null when there's no dot (nothing names an export to bind to).
327
+ * Returns null when there's no dot, since there's no export to bind to.
185
328
  */
186
329
  declare function parseHandler(handler: string): ParsedHandler | null;
187
330
  /**
188
331
  * Read every AWS::Serverless::Function resource in the template into a
189
332
  * code-facing `ServerlessFunctionInfo`. Functions without a parseable
190
- * Handler are skipped (there's no export to bind). Functions with no
191
- * Events still appear (empty route + non-route lists) so a consumer can
192
- * see them.
333
+ * Handler are skipped, since there's no export to bind. Functions with
334
+ * no Events still appear, with both event lists empty, so a consumer
335
+ * can see them.
193
336
  */
194
337
  declare function readServerlessFunctions(template: CloudFormationTemplate): ServerlessFunctionInfo[];
195
338
 
196
- export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, MAX_STACK_DEPTH, type ParsedHandler, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, inheritedEnvVars, loadCloudFormationTemplate, loadTemplateTree, parseHandler, qualifiedLogicalId, readAppSyncResolvers, readServerlessFunctions, refTarget, resourcesWithGlobals, unfollowedStackMessage };
339
+ export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, MAX_STACK_DEPTH, type ParsedHandler, type PatternReduction, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, inheritedEnvVars, loadCloudFormationTemplate, loadTemplateTree, parseHandler, qualifiedLogicalId, readAppSyncResolvers, readServerlessFunctions, reduceEventPattern, refTarget, resolveBucketChannel, resolveEventBusToken, resolveQueueChannel, resolveResourceChannel, resolveTopicChannel, resourcesWithGlobals, unfollowedStackMessage };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import YAML from 'yaml';
2
2
 
3
+ /**
4
+ * templateLoader.ts parses a CloudFormation or SAM template file into a
5
+ * plain object, resolving the shorthand YAML tags CFN uses for intrinsics.
6
+ *
7
+ * This is the one place that turns a template on disk into data. The
8
+ * summary-generation paths and the code-side handler pairing both read
9
+ * through it, so the YAML and JSON parsing, along with the handling of
10
+ * intrinsic tags, lives here instead of being repeated per consumer.
11
+ */
12
+
3
13
  interface CloudFormationResource {
4
14
  Type?: string;
5
15
  Properties?: Record<string, unknown>;
@@ -8,7 +18,7 @@ interface CloudFormationResource {
8
18
  interface CloudFormationTemplate {
9
19
  /**
10
20
  * SAM `Globals` block. Section keys (`Function`, `Api`, `HttpApi`)
11
- * hold defaults that individual resources inherit unless they
21
+ * contain defaults that individual resources inherit unless they
12
22
  * override the property themselves.
13
23
  */
14
24
  Globals?: Record<string, Record<string, unknown>>;
@@ -16,29 +26,44 @@ interface CloudFormationTemplate {
16
26
  }
17
27
  declare const CLOUDFORMATION_YAML_TAGS: YAML.CollectionTag[];
18
28
  /**
19
- * Load a CloudFormation / SAM template from disk. Format is detected by
20
- * extension; `.json` parses as JSON, everything else (including
21
- * `.yaml` / `.yml` / `.template`) goes through the YAML parser with the
29
+ * Load a CloudFormation or SAM template from disk. The format comes from
30
+ * the extension: `.json` parses as JSON, and everything else (including
31
+ * `.yaml`, `.yml`, and `.template`) goes through the YAML parser with the
22
32
  * intrinsic tags registered.
23
33
  *
24
- * Throws when the file is missing or the parsed value isn't an object —
25
- * a malformed manifest is a load-time error, not a silent empty result.
34
+ * Throws when the file is missing or the parsed value isn't an object. A
35
+ * malformed manifest is a load-time error rather than an empty result.
26
36
  */
27
37
  declare function loadCloudFormationTemplate(templatePath: string): CloudFormationTemplate;
28
38
  /**
29
- * CloudFormation references show up in four shapes after parsing:
39
+ * CloudFormation references come out of the parser in four forms:
30
40
  * - { Ref: "LogicalId" }
31
41
  * - { "Fn::GetAtt": ["LogicalId", "Attr"] }
32
42
  * - { "Fn::GetAtt": "LogicalId.Attr" }, the full-form spelling of the
33
43
  * `!GetAtt` short form, which templates written by hand still use
34
44
  * - the bare logical id when the parser doesn't recognise the YAML tag
35
45
  *
36
- * Every CloudFormation property that names another resource accepts all
37
- * of them, so anything reading such a property should come through here
38
- * rather than matching one shape.
46
+ * Every CloudFormation property that points at another resource accepts
47
+ * all four, so anything reading such a property should go through here
48
+ * instead of matching a single form.
39
49
  */
40
50
  declare function refTarget(value: unknown): string | null;
41
51
 
52
+ /**
53
+ * appsyncResolvers.ts works out which GraphQL field each Lambda serves.
54
+ *
55
+ * AppSync does not wire a field to a handler in code. The template does it,
56
+ * over three hops: a resolver points at a pipeline function, the function
57
+ * points at a data source, and the data source points at a Lambda. Both ways
58
+ * of writing that are covered, the raw AWS::AppSync::* resources and the SAM
59
+ * AWS::Serverless::GraphQLApi shorthand whose DataSources, Functions, and
60
+ * Resolvers blocks the transform expands into those same resources.
61
+ *
62
+ * Like the rest of this package, this reports only what the template says.
63
+ * Whether a field has an implementation, and whether that implementation
64
+ * matches the schema, is for a reader above to decide.
65
+ */
66
+
42
67
  /** One GraphQL field and the Lambdas behind it. */
43
68
  interface AppSyncResolverBinding {
44
69
  typeName: string;
@@ -46,10 +71,10 @@ interface AppSyncResolverBinding {
46
71
  /**
47
72
  * Every Lambda a request for this field runs, in pipeline order.
48
73
  * Usually one. A pipeline can chain several, and nothing in the
49
- * template says which of them is the one that produces the value:
50
- * the first is as often an auth step as it is the resolver, and the
51
- * last is as often a formatter. So all of them are reported, and a
52
- * reader that needs to pick says so itself.
74
+ * template says which of them produces the value: the first is as
75
+ * often an auth step as it is the resolver, and the last is as often
76
+ * a formatter. So all of them are reported, and a reader that needs
77
+ * to pick one decides for itself.
53
78
  *
54
79
  * Empty when the field is served by something other than a Lambda.
55
80
  */
@@ -57,21 +82,125 @@ interface AppSyncResolverBinding {
57
82
  }
58
83
  declare function readAppSyncResolvers(template: CloudFormationTemplate): AppSyncResolverBinding[];
59
84
 
85
+ /**
86
+ * arn.ts resolves the references AWS manifests use to point at a queue, a
87
+ * topic, or a bucket, down to the channel string suss keys the boundary on.
88
+ *
89
+ * Every manifest language that targets AWS writes a reference one of three
90
+ * ways: as a CFN intrinsic (`!Ref X`, `!GetAtt X.Arn`), as a plain ARN
91
+ * string, or as a bare name. The CloudFormation reader and the Serverless
92
+ * Framework reader both resolve them here, so a queue written two different
93
+ * ways in two manifest languages still ends up on one channel.
94
+ */
95
+ /**
96
+ * Resolve a reference (`!Ref X`, `!GetAtt X.Arn`, plain string ARN) to
97
+ * the referenced resource's channel string. `service` is the ARN
98
+ * segment a plain string is checked against (`"sqs"`, `"sns"`, `"s3"`),
99
+ * so a queue ARN, a topic ARN, and a bucket ARN all resolve through the
100
+ * same code.
101
+ *
102
+ * Returns null when the reference is dynamic (a parameter, an import,
103
+ * or an Fn::Join that points at nothing this template declares); those
104
+ * need cross-stack resolution that's out of scope for v0.
105
+ */
106
+ declare function resolveResourceChannel(value: unknown, service: string): string | null;
107
+ /**
108
+ * Resolve a Queue reference (`!Ref X`, `!GetAtt X.Arn`, plain string
109
+ * ARN) to the queue's channel string.
110
+ */
111
+ declare function resolveQueueChannel(value: unknown): string | null;
112
+ /**
113
+ * Resolve a Topic reference (`!Ref X`, `!GetAtt X.Arn`, plain string
114
+ * ARN) to the topic's channel string.
115
+ */
116
+ declare function resolveTopicChannel(value: unknown): string | null;
117
+ /**
118
+ * Resolve a Bucket reference (`!Ref X`, `!GetAtt X.Arn`, plain string
119
+ * ARN) to the bucket's channel string.
120
+ */
121
+ declare function resolveBucketChannel(value: unknown): string | null;
122
+
123
+ /**
124
+ * eventPattern.ts reads what an EventBridge rule says about the events it
125
+ * matches, and which bus those events arrive on.
126
+ *
127
+ * A rule's EventPattern and its EventBusName are written the same way in a
128
+ * CloudFormation template and in a serverless.yml eventBridge event, so
129
+ * both readers reduce them here.
130
+ */
131
+ /**
132
+ * How far a rule's EventPattern reduced: either to the exact
133
+ * detail-types it lists, or not at all, along with a reason someone can
134
+ * read.
135
+ */
136
+ type PatternReduction = {
137
+ kind: "exact";
138
+ detailTypes: string[];
139
+ } | {
140
+ kind: "unresolvable";
141
+ reason: string;
142
+ };
143
+ /**
144
+ * Reduce a rule's EventPattern to the exact set of DetailTypes it
145
+ * matches. v0 handles only a literal `detail-type` array; anything else
146
+ * (absent detail-type, content-filter objects, empty array) is
147
+ * unresolvable, surfaced by the checker, never silently dropped.
148
+ */
149
+ declare function reduceEventPattern(pattern: unknown): PatternReduction;
150
+ /**
151
+ * Resolve a rule's EventBusName to the channel's bus token: the CFN
152
+ * logical id when it's a Ref/GetAtt, the name segment of an event-bus
153
+ * ARN, the literal string otherwise, or "default" when it's left out
154
+ * (the EventBridge default event bus).
155
+ */
156
+ declare function resolveEventBusToken(value: unknown): string;
157
+
158
+ /**
159
+ * globals.ts folds the SAM `Globals` section into the resources that
160
+ * inherit from it, so every reader sees one set of properties per resource
161
+ * and none of them has to know that `Globals` exists.
162
+ *
163
+ * SAM applies a section's properties to each resource of the matching type,
164
+ * and the resource's own value wins where both declare one, per
165
+ * https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy-globals.html
166
+ *
167
+ * That page has the combining rules too. A map is merged key by key, a list
168
+ * becomes the section's entries followed by the resource's, and anything
169
+ * else is replaced outright. Merging keys is what makes a function that sets
170
+ * one environment variable of its own still get the section's others.
171
+ */
172
+
60
173
  /**
61
174
  * The template's resources with each `Globals` section already applied
62
- * to the resources it covers. A resource of a type no section names, or
63
- * a template with no `Globals`, comes back untouched.
175
+ * to the resources it covers. A resource whose type no section covers,
176
+ * or a template with no `Globals`, comes back untouched.
64
177
  */
65
178
  declare function resourcesWithGlobals(template: CloudFormationTemplate): Record<string, CloudFormationResource>;
66
179
  /**
67
180
  * Per function logical id, the environment variables the `Function`
68
181
  * section supplies that the function does not declare for itself. A
69
182
  * consumer that treats a function's environment as its own contract
70
- * needs the distinction: a name every function in the document receives
71
- * says something about the document, not about any one function.
183
+ * needs the difference, because a variable every function in the
184
+ * document receives tells you about the document, not about one
185
+ * function.
72
186
  */
73
187
  declare function inheritedEnvVars(template: CloudFormationTemplate): Record<string, string[]>;
74
188
 
189
+ /**
190
+ * nestedStacks.ts reads a template and every child template it embeds, as a
191
+ * list of documents.
192
+ *
193
+ * A stack resource points at another template, and CloudFormation deploys that
194
+ * template's resources alongside the parent's. Each document keeps its own
195
+ * namespace, its own SAM `Globals` section, and its own directory, so the tree
196
+ * stays a list of documents. Merging them would apply one document's defaults
197
+ * to another's resources, and let two logical ids spelled the same way collide.
198
+ *
199
+ * A child's reference to one of its own parameters is not followed to what the
200
+ * parent bound, and a `Fn::GetAtt` on a stack resource is not followed to the
201
+ * child's output. Both point at nothing, like any reference we can't resolve.
202
+ */
203
+
75
204
  /**
76
205
  * How deep the reader follows children. CloudFormation itself allows
77
206
  * deeper nesting; a template chain this long in a repository is far
@@ -79,7 +208,7 @@ declare function inheritedEnvVars(template: CloudFormationTemplate): Record<stri
79
208
  * rather than applied silently.
80
209
  */
81
210
  declare const MAX_STACK_DEPTH = 10;
82
- /** One template file, and where it sits in the tree that reached it. */
211
+ /** One template file, and where it lives in the tree that reached it. */
83
212
  interface TemplateDocument {
84
213
  /** Absolute path of the file this document was read from. */
85
214
  path: string;
@@ -92,8 +221,8 @@ interface TemplateDocument {
92
221
  }
93
222
  /**
94
223
  * Why a child template was not read. Every value is a limit on what the
95
- * reader could reach, so a consumer should report it as reading that
96
- * stopped rather than treat the child as a template holding nothing.
224
+ * reader could reach, so a consumer should report it as a read that
225
+ * stopped rather than treat the child as an empty template.
97
226
  */
98
227
  type UnfollowedReason = "remoteUrl" | "notALiteralPath" | "fileMissing" | "unreadable" | "cycle" | "depthLimit";
99
228
  /** A child template the reader could not open, and what stopped it. */
@@ -105,7 +234,7 @@ interface UnfollowedStack {
105
234
  /** The template location as the document writes it, when it is a string. */
106
235
  templateUrl: string | null;
107
236
  reason: UnfollowedReason;
108
- /** One phrase naming what stopped the read, for a message to a person. */
237
+ /** One phrase saying what stopped the read, for a message to a person. */
109
238
  detail: string;
110
239
  }
111
240
  interface TemplateTree {
@@ -125,14 +254,27 @@ interface TemplateTree {
125
254
  declare function loadTemplateTree(rootPath: string): TemplateTree;
126
255
  /**
127
256
  * A logical id qualified by the stack path that reaches it, so two
128
- * documents that both declare `HandlerFunction` name two different
257
+ * documents that both declare `HandlerFunction` refer to two different
129
258
  * deployed things. A resource in the root document keeps its bare id,
130
259
  * which is also what CloudFormation shows for it.
131
260
  */
132
261
  declare function qualifiedLogicalId(stackPath: string[], logicalId: string): string;
133
- /** A line naming one child the reader could not open, and why. */
262
+ /** A line saying which child the reader could not open, and why. */
134
263
  declare function unfollowedStackMessage(stack: UnfollowedStack): string;
135
264
 
265
+ /**
266
+ * serverlessFunctions.ts reads AWS::Serverless::Function resources into a
267
+ * code-facing view: which module and export each function points at, and
268
+ * which route and non-route Events it declares.
269
+ *
270
+ * The summary-generation paths expand SAM Events into API Gateway route
271
+ * summaries. This reader covers the other question the code-side pairing
272
+ * needs: given the template, where is each handler's implementation, and
273
+ * what routes is it bound to? The manifest semantics (how a Handler string
274
+ * is laid out, how CodeUri is inherited from Globals, Api versus HttpApi
275
+ * Events) stay here rather than being reconstructed in a framework pack.
276
+ */
277
+
136
278
  /** A route-bearing SAM Event (`Type: Api` or `Type: HttpApi`). */
137
279
  interface ServerlessHttpRoute {
138
280
  /** Key of the event in the function's `Events` map. */
@@ -147,9 +289,10 @@ interface ServerlessHttpRoute {
147
289
  apiId: string | null;
148
290
  }
149
291
  /**
150
- * A recognized SAM Event that isn't an HTTP route — SQS / Schedule /
151
- * SNS / S3 / etc. Surfaced (not dropped) so the code side can account
152
- * for handlers it recognizes but deliberately doesn't extract as HTTP.
292
+ * A recognized SAM Event that isn't an HTTP route (SQS, Schedule, SNS,
293
+ * S3, and so on). These are reported rather than dropped, so the code
294
+ * side can account for handlers it recognizes but deliberately doesn't
295
+ * extract as HTTP.
153
296
  */
154
297
  interface ServerlessNonHttpEvent {
155
298
  eventId: string;
@@ -163,7 +306,7 @@ interface ServerlessFunctionInfo {
163
306
  handler: string;
164
307
  /** Module-path portion of the handler (everything before the final dot). */
165
308
  modulePath: string;
166
- /** Exported symbol the handler names (everything after the final dot). */
309
+ /** Exported symbol the handler points at (everything after the final dot). */
167
310
  exportName: string;
168
311
  /**
169
312
  * CodeUri base directory the handler resolves against: the function's
@@ -181,16 +324,16 @@ interface ParsedHandler {
181
324
  * Split a SAM Handler string into its module path and exported symbol.
182
325
  * The final dot separates them: `"src/handlers/confirmToken.handler"` →
183
326
  * `{ modulePath: "src/handlers/confirmToken", exportName: "handler" }`.
184
- * Returns null when there's no dot (nothing names an export to bind to).
327
+ * Returns null when there's no dot, since there's no export to bind to.
185
328
  */
186
329
  declare function parseHandler(handler: string): ParsedHandler | null;
187
330
  /**
188
331
  * Read every AWS::Serverless::Function resource in the template into a
189
332
  * code-facing `ServerlessFunctionInfo`. Functions without a parseable
190
- * Handler are skipped (there's no export to bind). Functions with no
191
- * Events still appear (empty route + non-route lists) so a consumer can
192
- * see them.
333
+ * Handler are skipped, since there's no export to bind. Functions with
334
+ * no Events still appear, with both event lists empty, so a consumer
335
+ * can see them.
193
336
  */
194
337
  declare function readServerlessFunctions(template: CloudFormationTemplate): ServerlessFunctionInfo[];
195
338
 
196
- export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, MAX_STACK_DEPTH, type ParsedHandler, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, inheritedEnvVars, loadCloudFormationTemplate, loadTemplateTree, parseHandler, qualifiedLogicalId, readAppSyncResolvers, readServerlessFunctions, refTarget, resourcesWithGlobals, unfollowedStackMessage };
339
+ export { type AppSyncResolverBinding, CLOUDFORMATION_YAML_TAGS, type CloudFormationResource, type CloudFormationTemplate, MAX_STACK_DEPTH, type ParsedHandler, type PatternReduction, type ServerlessFunctionInfo, type ServerlessHttpRoute, type ServerlessNonHttpEvent, type TemplateDocument, type TemplateTree, type UnfollowedReason, type UnfollowedStack, inheritedEnvVars, loadCloudFormationTemplate, loadTemplateTree, parseHandler, qualifiedLogicalId, readAppSyncResolvers, readServerlessFunctions, reduceEventPattern, refTarget, resolveBucketChannel, resolveEventBusToken, resolveQueueChannel, resolveResourceChannel, resolveTopicChannel, resourcesWithGlobals, unfollowedStackMessage };
package/dist/index.js CHANGED
@@ -16,7 +16,11 @@ var getAttParts = (value) => {
16
16
  return value.map(String);
17
17
  }
18
18
  const text = String(value);
19
- return text.includes(".") ? text.split(".") : [text];
19
+ const dot = text.indexOf(".");
20
+ if (dot === -1) {
21
+ return [text];
22
+ }
23
+ return [text.slice(0, dot), text.slice(dot + 1)];
20
24
  };
21
25
  var CLOUDFORMATION_YAML_TAGS = [
22
26
  ...everyNodeKind("!Ref", (value) => ({ Ref: value })),
@@ -68,8 +72,7 @@ function refTarget(value) {
68
72
  return getAtt[0];
69
73
  }
70
74
  if (typeof getAtt === "string") {
71
- const dot = getAtt.indexOf(".");
72
- return dot === -1 ? getAtt : getAtt.slice(0, dot);
75
+ return getAttParts(getAtt)[0] ?? null;
73
76
  }
74
77
  return null;
75
78
  }
@@ -233,6 +236,102 @@ function stringOf(value) {
233
236
  return typeof value === "string" ? value : null;
234
237
  }
235
238
 
239
+ // src/arn.ts
240
+ function resolveResourceChannel(value, service) {
241
+ if (value === null || value === void 0) {
242
+ return null;
243
+ }
244
+ if (typeof value === "string") {
245
+ const resource = resolveArnResource(value, service);
246
+ return resource ?? value;
247
+ }
248
+ return refTarget(value);
249
+ }
250
+ function resolveArnResource(value, service) {
251
+ const parts = value.split(":");
252
+ if (parts[0] !== "arn" || parts[2] !== service) {
253
+ return null;
254
+ }
255
+ const region = parts[3];
256
+ const account = parts[4];
257
+ const resource = parts.slice(5).join(":");
258
+ if (service === "s3") {
259
+ if (region !== "" || account !== "" || resource === "") {
260
+ return null;
261
+ }
262
+ const slash = resource.indexOf("/");
263
+ const bucket = slash === -1 ? resource : resource.slice(0, slash);
264
+ return bucket === "" ? null : bucket;
265
+ }
266
+ if (!region || !account || resource === "") {
267
+ return null;
268
+ }
269
+ return resource;
270
+ }
271
+ function resolveQueueChannel(value) {
272
+ return resolveResourceChannel(value, "sqs");
273
+ }
274
+ function resolveTopicChannel(value) {
275
+ return resolveResourceChannel(value, "sns");
276
+ }
277
+ function resolveBucketChannel(value) {
278
+ return resolveResourceChannel(value, "s3");
279
+ }
280
+
281
+ // src/eventPattern.ts
282
+ function reduceEventPattern(pattern) {
283
+ if (pattern === null || typeof pattern !== "object") {
284
+ return {
285
+ kind: "unresolvable",
286
+ reason: "rule declares no EventPattern"
287
+ };
288
+ }
289
+ const detailType = pattern["detail-type"];
290
+ if (detailType === void 0) {
291
+ return {
292
+ kind: "unresolvable",
293
+ reason: "EventPattern has no detail-type; v0 reduces routing to exact detail-type match only"
294
+ };
295
+ }
296
+ if (!Array.isArray(detailType)) {
297
+ return {
298
+ kind: "unresolvable",
299
+ reason: "detail-type is not a plain array of string literals"
300
+ };
301
+ }
302
+ const detailTypes = [];
303
+ for (const entry of detailType) {
304
+ if (typeof entry !== "string") {
305
+ return {
306
+ kind: "unresolvable",
307
+ reason: "detail-type contains a content filter (prefix / anything-but / etc.); pattern subsumption is out of v0 scope"
308
+ };
309
+ }
310
+ detailTypes.push(entry);
311
+ }
312
+ if (detailTypes.length === 0) {
313
+ return { kind: "unresolvable", reason: "detail-type array is empty" };
314
+ }
315
+ return { kind: "exact", detailTypes };
316
+ }
317
+ function resolveEventBusToken(value) {
318
+ if (value === null || value === void 0) {
319
+ return "default";
320
+ }
321
+ if (typeof value === "object") {
322
+ const logicalId = refTarget(value);
323
+ return logicalId ?? "default";
324
+ }
325
+ if (typeof value === "string") {
326
+ const arnMatch = value.match(/:event-bus\/(.+)$/);
327
+ if (arnMatch !== null) {
328
+ return arnMatch[1];
329
+ }
330
+ return value;
331
+ }
332
+ return "default";
333
+ }
334
+
236
335
  // src/globals.ts
237
336
  var GLOBALS_SECTION_TYPES = {
238
337
  Api: "AWS::Serverless::Api",
@@ -546,7 +645,13 @@ export {
546
645
  qualifiedLogicalId,
547
646
  readAppSyncResolvers,
548
647
  readServerlessFunctions,
648
+ reduceEventPattern,
549
649
  refTarget,
650
+ resolveBucketChannel,
651
+ resolveEventBusToken,
652
+ resolveQueueChannel,
653
+ resolveResourceChannel,
654
+ resolveTopicChannel,
550
655
  resourcesWithGlobals,
551
656
  unfollowedStackMessage
552
657
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/manifest-aws",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Parse AWS CloudFormation / SAM templates into plain data — the shared facts layer contract readers and manifest-driven framework packs both consume.",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@suss/ir-core": "0.3.2",
48
+ "@suss/ir-core": "0.4.0",
49
49
  "yaml": "^2.4.0"
50
50
  },
51
51
  "devDependencies": {