1/**
2 * Generate `packages/alchemy/src/AWS/IAM/actions.generated.ts` — a SHALLOW
3 * string-literal union type `IamAction` built from distilled's per-service
4 * operation names.
5 *
6 * Coordinator-run (like generate-api-reference.ts):
7 *
8 * bun scripts/generate-iam-actions.ts
9 *
10 * Source of truth: `submodules/distilled/packages/aws/src/services/*.ts`. For each
11 * service module we extract
12 *
13 * - the sigv4 signing name (`T.AwsAuthSigv4({ name: "..." })`), which is the
14 * best distilled-local approximation of the IAM action service prefix
15 * (a small override map corrects the known mismatches, e.g.
16 * `monitoring` -> `cloudwatch`), and
17 * - every `operationName: "X"` registered via `API.make`.
18 *
19 * The emitted type is a flat union of fully-expanded literals
20 * (`"s3:GetObject" | "s3:*" | ... | (string & {})`) — deliberately NO
21 * template-literal type computation, per the DSL-track type-level cost
22 * ceiling (processes/AWS/design/dsl-abstractions.md, risk #5). The
23 * `(string & {})` escape hatch means unknown/condition-only IAM actions are
24 * never a hard break; the union exists purely for autocomplete.
25 *
26 * Note: distilled operation names approximate IAM actions but are not 1:1
27 * (IAM has actions with no API operation and vice versa) — see the open
28 * question in dsl-abstractions.md. The escape hatch covers the gap.
29 */
30
31const SERVICES_DIR = new URL(
32 "../submodules/distilled/packages/aws/src/services/",
33 import.meta.url,
34).pathname;
35const OUT_FILE = new URL(
36 "../packages/alchemy/src/AWS/IAM/actions.generated.ts",
37 import.meta.url,
38).pathname;
39
40/**
41 * sigv4 signing name -> IAM action service prefix, for the known cases where
42 * the two differ. Everything else uses the (lowercased) signing name as-is.
43 */
44const PREFIX_OVERRIDES: Record<string, string> = {
45 // CloudWatch metrics sign as "monitoring" but IAM actions are `cloudwatch:*`.
46 monitoring: "cloudwatch",
47 // IoT data planes sign with dedicated names but share the `iot:` prefix.
48 iotdata: "iot",
49 ioteventsdata: "iotevents",
50 IoTSecuredTunneling: "iot",
51 "iot-jobs-data": "iot",
52};
53
54const { readdir } = await import("node:fs/promises");
55const { readFileSync, writeFileSync } = await import("node:fs");
56const { join } = await import("node:path");
57
58const files = (await readdir(SERVICES_DIR)).filter(
59 (f) => f.endsWith(".ts") && f !== "index.ts",
60);
61
62/** IAM action prefix -> operation names */
63const byPrefix = new Map<string, Set<string>>();
64let totalOps = 0;
65const skipped: string[] = [];
66
67for (const file of files.sort()) {
68 const source = readFileSync(join(SERVICES_DIR, file), "utf8");
69 const nameMatch = source.match(/AwsAuthSigv[24]\(\{\s*name:\s*"([^"]+)"/);
70 if (!nameMatch) {
71 skipped.push(file);
72 continue;
73 }
74 const sigv4Name = nameMatch[1]!;
75 const prefix = (PREFIX_OVERRIDES[sigv4Name] ?? sigv4Name).toLowerCase();
76 const ops = byPrefix.get(prefix) ?? new Set<string>();
77 byPrefix.set(prefix, ops);
78 for (const match of source.matchAll(/operationName:\s*"([A-Za-z0-9_]+)"/g)) {
79 if (!ops.has(match[1]!)) {
80 ops.add(match[1]!);
81 totalOps++;
82 }
83 }
84}
85
86const prefixes = [...byPrefix.keys()].sort();
87const lines: string[] = [
88 "// AUTO-GENERATED FILE — DO NOT EDIT.",
89 "//",
90 "// Regenerate with: bun scripts/generate-iam-actions.ts",
91 "//",
92 `// Source: submodules/distilled/packages/aws/src/services/*.ts (${files.length - skipped.length} service modules,`,
93 `// ${prefixes.length} IAM action prefixes, ${totalOps} operations).`,
94 "//",
95 "// A SHALLOW literal union (no template-literal type computation) of",
96 "// `service:Operation` IAM action names, plus `service:*` per service and a",
97 "// `(string & {})` escape hatch so arbitrary action strings always remain",
98 "// assignable. Exists purely for editor autocomplete on",
99 "// `PolicyStatement.Action`.",
100 "",
101 "export type IamAction =",
102];
103for (const prefix of prefixes) {
104 lines.push(` | "${prefix}:*"`);
105 for (const op of [...byPrefix.get(prefix)!].sort()) {
106 lines.push(` | "${prefix}:${op}"`);
107 }
108}
109lines.push(" | (string & {});", "");
110
111writeFileSync(OUT_FILE, lines.join("\n"));
112console.log(
113 `wrote ${OUT_FILE}: ${prefixes.length} prefixes, ${totalOps} operations, ${lines.length} lines`,
114);
115if (skipped.length > 0) {
116 console.log(`skipped (no AwsAuthSigv4 name): ${skipped.join(", ")}`);
117}