@nmakarov/cli-toolkit 0.32.0 → 0.33.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/README.md +17 -0
- package/dist/aws.cjs +61 -0
- package/dist/aws.cjs.map +1 -1
- package/dist/aws.js +61 -0
- package/dist/aws.js.map +1 -1
- package/dist/index.cjs +61 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/scripts/aws/discover.js +60 -0
package/README.md
CHANGED
|
@@ -51,6 +51,23 @@ Shipped as TypeScript sources under `scripts/ssm/` (included in the npm package)
|
|
|
51
51
|
|
|
52
52
|
From a clone of this repo: `npm run ssm:list -- --prefix /your/prefix/`, `npm run ssm:pull -- --path /your/prefix/`.
|
|
53
53
|
|
|
54
|
+
## AWS discovery (`cli-aws-discover`)
|
|
55
|
+
|
|
56
|
+
Read-only peek at the account behind your credentials (caller identity, Route53
|
|
57
|
+
hosted zones, VPCs, latest Ubuntu AMI) — handy for filling a `terraform.tfvars`:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx cli-aws-discover
|
|
61
|
+
npx cli-aws-discover --awsRegion=ca-central-1
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Credentials resolve from `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` /
|
|
65
|
+
`AWS_REGION` (env or `.env`), an `AWS_PROFILE`, or an instance role. If none are
|
|
66
|
+
found — or AWS rejects them — it prints exactly how to mint a read-only key,
|
|
67
|
+
**no stack trace**. The same checks are available programmatically:
|
|
68
|
+
`aws.checkCredentials()`, `Aws.isAuthError(err)`, `Aws.credentialsHelp(region)`
|
|
69
|
+
(see `@nmakarov/cli-toolkit/aws`).
|
|
70
|
+
|
|
54
71
|
## Deploy ecosystem (`cli-deploy`)
|
|
55
72
|
|
|
56
73
|
Manifest-driven release deploys to EC2: clone on the host, serve versioned
|
package/dist/aws.cjs
CHANGED
|
@@ -66,6 +66,7 @@ var Aws = class _Aws {
|
|
|
66
66
|
secretAccessKey: config.secretAccessKey,
|
|
67
67
|
...config.sessionToken ? { sessionToken: config.sessionToken } : {}
|
|
68
68
|
} : void 0;
|
|
69
|
+
this.hasExplicitCredentials = !!this._credentials;
|
|
69
70
|
process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= "true";
|
|
70
71
|
this._clients = {};
|
|
71
72
|
}
|
|
@@ -88,6 +89,66 @@ var Aws = class _Aws {
|
|
|
88
89
|
getRegion() {
|
|
89
90
|
return this.region;
|
|
90
91
|
}
|
|
92
|
+
// ── credentials ─────────────────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Are any credentials available *before* hitting AWS? Returns
|
|
95
|
+
* { ok, source } — explicit keys, or a resolvable default chain (profile/SSO/
|
|
96
|
+
* instance role). { ok:false } means there's nothing to even try with.
|
|
97
|
+
* Note: "ok" only means creds were *found*, not that AWS will accept them.
|
|
98
|
+
*/
|
|
99
|
+
async checkCredentials() {
|
|
100
|
+
if (this.hasExplicitCredentials) {
|
|
101
|
+
return { ok: true, source: "explicit keys (env/.env/CLI)" };
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const provider = this._sts().config.credentials;
|
|
105
|
+
const resolved = typeof provider === "function" ? await provider() : provider;
|
|
106
|
+
if (resolved?.accessKeyId) {
|
|
107
|
+
return { ok: true, source: "default credential chain (profile/SSO/role)" };
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
return { ok: false };
|
|
112
|
+
}
|
|
113
|
+
/** True for "bad/missing credentials" style errors (vs. real failures). */
|
|
114
|
+
static isAuthError(err) {
|
|
115
|
+
const name = err?.name || err?.Code || err?.__type || "";
|
|
116
|
+
return [
|
|
117
|
+
"CredentialsProviderError",
|
|
118
|
+
"InvalidClientTokenId",
|
|
119
|
+
"UnrecognizedClientException",
|
|
120
|
+
"AuthFailure",
|
|
121
|
+
"AccessDenied",
|
|
122
|
+
"AccessDeniedException",
|
|
123
|
+
"ExpiredToken",
|
|
124
|
+
"ExpiredTokenException",
|
|
125
|
+
"SignatureDoesNotMatch",
|
|
126
|
+
"MissingAuthenticationToken"
|
|
127
|
+
].includes(name);
|
|
128
|
+
}
|
|
129
|
+
/** Short, precise instructions for getting AWS credentials into .env. */
|
|
130
|
+
static credentialsHelp(region = DEFAULT_REGION) {
|
|
131
|
+
return [
|
|
132
|
+
"No usable AWS credentials were found (or AWS rejected them).",
|
|
133
|
+
"",
|
|
134
|
+
"Put a read-only access key in the project's .env:",
|
|
135
|
+
"",
|
|
136
|
+
" AWS_ACCESS_KEY_ID=AKIA...",
|
|
137
|
+
" AWS_SECRET_ACCESS_KEY=...",
|
|
138
|
+
` AWS_REGION=${region} # optional (default ${DEFAULT_REGION})`,
|
|
139
|
+
"",
|
|
140
|
+
"Get a key from the AWS console (~2 min):",
|
|
141
|
+
" 1. IAM \u2192 Users \u2192 create or pick a user (console sign-in not needed).",
|
|
142
|
+
' 2. Attach a policy \u2014 "ReadOnlyAccess" (AWS managed) is enough for discovery.',
|
|
143
|
+
' 3. The user \u2192 "Security credentials" \u2192 "Create access key" \u2192 "CLI".',
|
|
144
|
+
" 4. Copy the Access key ID + Secret access key (the secret shows only once).",
|
|
145
|
+
" 5. Paste both into .env, then re-run.",
|
|
146
|
+
" Direct link: https://console.aws.amazon.com/iam/home#/users",
|
|
147
|
+
"",
|
|
148
|
+
"Prefer a named profile or an EC2 instance role? Re-run with AWS_PROFILE=<name>",
|
|
149
|
+
"set (or on the instance) and credentials resolve automatically."
|
|
150
|
+
].join("\n");
|
|
151
|
+
}
|
|
91
152
|
// ── identity ──────────────────────────────────────────────────────────────
|
|
92
153
|
/** { account, arn, userId } — confirm which account/identity the keys belong to. */
|
|
93
154
|
async whoAmI() {
|
package/dist/aws.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/aws/index.js"],"sourcesContent":["/**\n * AWS discovery component\n *\n * A thin, read-only wrapper over a few AWS SDK v3 clients (STS, EC2, Route53)\n * with cli-toolkit conventions:\n *\n * const aws = await Aws.init(context);\n * const me = await aws.whoAmI();\n * const zones = await aws.listHostedZones();\n *\n * The whole point: given just an access key + secret (usually from .env), it can\n * *discover* the boring-to-look-up bits of an AWS account (account id, regions,\n * VPCs, subnets, security groups, key pairs, Route53 hosted zones, the latest\n * Ubuntu AMI) so scripts can pre-fill config (e.g. a terraform.tfvars) instead of\n * making you hunt through the console.\n *\n * Credentials + region are resolved via context.params, so they come from CLI,\n * env, or .env transparently:\n * awsAccessKeyId -> AWS_ACCESS_KEY_ID\n * awsSecretAccessKey -> AWS_SECRET_ACCESS_KEY\n * awsSessionToken -> AWS_SESSION_TOKEN (optional, for temp creds)\n * awsRegion -> AWS_REGION (default us-east-1)\n *\n * If no explicit keys are found, the SDK's default credential chain is used\n * (shared config/profile, instance role, etc.) — so AWS_PROFILE also works.\n *\n * Everything here is read-only (Describe / List / Get calls). It never mutates AWS.\n */\n\nimport {\n STSClient,\n GetCallerIdentityCommand,\n} from \"@aws-sdk/client-sts\";\nimport {\n EC2Client,\n DescribeRegionsCommand,\n DescribeAvailabilityZonesCommand,\n DescribeVpcsCommand,\n DescribeSubnetsCommand,\n DescribeSecurityGroupsCommand,\n DescribeKeyPairsCommand,\n DescribeImagesCommand,\n} from \"@aws-sdk/client-ec2\";\nimport {\n Route53Client,\n ListHostedZonesCommand,\n} from \"@aws-sdk/client-route-53\";\n\nconst DEFAULT_REGION = \"us-east-1\";\nconst CANONICAL_OWNER_ID = \"099720109477\"; // Ubuntu images\nconst DEFAULT_UBUNTU_PATTERN = \"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*\";\n\nfunction nameTag(tags) {\n const t = (tags ?? []).find(x => x.Key === \"Name\");\n return t?.Value ?? null;\n}\n\nexport class Aws {\n /**\n * Resolve credentials/region from params and return an Aws instance.\n * Does not call AWS until you invoke a method (clients are lazy).\n */\n static async init(context, options = {}) {\n // Tolerate blank/missing values (e.g. an env var present but empty, like\n // a half-filled .env): treat them as \"not set\" rather than failing param\n // validation, so we fall back cleanly to the SDK default credential chain.\n const getOpt = async (k) => {\n try {\n const v = await context?.params?.get?.(k, \"string\");\n return v != null && String(v).trim() !== \"\" ? v : undefined;\n } catch {\n return undefined;\n }\n };\n\n const config = {\n region: options.region ?? (await getOpt(\"awsRegion\")) ?? DEFAULT_REGION,\n accessKeyId: options.accessKeyId ?? (await getOpt(\"awsAccessKeyId\")),\n secretAccessKey: options.secretAccessKey ?? (await getOpt(\"awsSecretAccessKey\")),\n sessionToken: options.sessionToken ?? (await getOpt(\"awsSessionToken\")),\n };\n\n const aws = new Aws(context, config);\n context?.logger?.debug?.(\n `[aws] region=${config.region} credentials=${config.accessKeyId ? \"explicit\" : \"default-chain\"}`,\n );\n return aws;\n }\n\n constructor(context, config) {\n this.logger = context?.logger ?? console;\n this.region = config.region || DEFAULT_REGION;\n\n // Build an explicit credentials object only when keys were supplied;\n // otherwise leave undefined so the SDK uses its default chain (profile,\n // instance role, env, …).\n this._credentials = config.accessKeyId && config.secretAccessKey\n ? {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n ...(config.sessionToken ? { sessionToken: config.sessionToken } : {}),\n }\n : undefined;\n\n // Quiet the AWS SDK v3 \"node >=22 required after Jan 2027\" notice.\n process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= \"true\";\n\n this._clients = {};\n }\n\n // ── clients (lazy, optionally region-scoped) ──────────────────────────────\n _clientConfig(region) {\n const cfg = { region: region || this.region };\n if (this._credentials) cfg.credentials = this._credentials;\n return cfg;\n }\n\n _sts() {\n return (this._clients.sts ??= new STSClient(this._clientConfig()));\n }\n\n _ec2(region) {\n const key = `ec2:${region || this.region}`;\n return (this._clients[key] ??= new EC2Client(this._clientConfig(region)));\n }\n\n _route53() {\n // Route53 is global; region is irrelevant but the client still wants one.\n return (this._clients.route53 ??= new Route53Client(this._clientConfig()));\n }\n\n getRegion() { return this.region; }\n\n // ── identity ──────────────────────────────────────────────────────────────\n /** { account, arn, userId } — confirm which account/identity the keys belong to. */\n async whoAmI() {\n const out = await this._sts().send(new GetCallerIdentityCommand({}));\n return { account: out.Account, arn: out.Arn, userId: out.UserId };\n }\n\n // ── regions / AZs ──────────────────────────────────────────────────────────\n /** Enabled region names, sorted. */\n async listRegions({ allRegions = false } = {}) {\n const out = await this._ec2().send(new DescribeRegionsCommand({ AllRegions: allRegions }));\n return (out.Regions ?? []).map(r => r.RegionName).sort();\n }\n\n /** Availability zone names for a region (defaults to the instance region). */\n async listAvailabilityZones(region) {\n const out = await this._ec2(region).send(new DescribeAvailabilityZonesCommand({}));\n return (out.AvailabilityZones ?? [])\n .filter(z => z.State === \"available\")\n .map(z => z.ZoneName)\n .sort();\n }\n\n // ── VPC / subnets / SGs / key pairs ────────────────────────────────────────\n /** [{ id, cidr, isDefault, name }] in a region. */\n async listVpcs(region) {\n const out = await this._ec2(region).send(new DescribeVpcsCommand({}));\n return (out.Vpcs ?? []).map(v => ({\n id: v.VpcId,\n cidr: v.CidrBlock,\n isDefault: !!v.IsDefault,\n name: nameTag(v.Tags),\n }));\n }\n\n /** [{ id, vpcId, az, cidr, public, name }]; pass vpcId to scope. */\n async listSubnets({ vpcId, region } = {}) {\n const Filters = vpcId ? [{ Name: \"vpc-id\", Values: [vpcId] }] : undefined;\n const out = await this._ec2(region).send(new DescribeSubnetsCommand({ Filters }));\n return (out.Subnets ?? []).map(s => ({\n id: s.SubnetId,\n vpcId: s.VpcId,\n az: s.AvailabilityZone,\n cidr: s.CidrBlock,\n public: !!s.MapPublicIpOnLaunch,\n name: nameTag(s.Tags),\n }));\n }\n\n /** [{ id, name, vpcId, description }]; pass vpcId to scope. */\n async listSecurityGroups({ vpcId, region } = {}) {\n const Filters = vpcId ? [{ Name: \"vpc-id\", Values: [vpcId] }] : undefined;\n const out = await this._ec2(region).send(new DescribeSecurityGroupsCommand({ Filters }));\n return (out.SecurityGroups ?? []).map(g => ({\n id: g.GroupId,\n name: g.GroupName,\n vpcId: g.VpcId,\n description: g.Description,\n }));\n }\n\n /** [{ name, fingerprint }] EC2 key pairs in a region. */\n async listKeyPairs(region) {\n const out = await this._ec2(region).send(new DescribeKeyPairsCommand({}));\n return (out.KeyPairs ?? []).map(k => ({ name: k.KeyName, fingerprint: k.KeyFingerprint }));\n }\n\n // ── Route53 ────────────────────────────────────────────────────────────────\n /** [{ id, name, private, recordCount }] — id is the bare zone id (no /hostedzone/). */\n async listHostedZones() {\n const zones = [];\n let marker;\n do {\n const out = await this._route53().send(new ListHostedZonesCommand({ Marker: marker }));\n for (const z of out.HostedZones ?? []) {\n zones.push({\n id: (z.Id ?? \"\").replace(\"/hostedzone/\", \"\"),\n name: (z.Name ?? \"\").replace(/\\.$/, \"\"), // strip trailing dot\n private: !!z.Config?.PrivateZone,\n recordCount: z.ResourceRecordSetCount,\n });\n }\n marker = out.IsTruncated ? out.NextMarker : undefined;\n } while (marker);\n return zones;\n }\n\n /**\n * Best hosted zone for a domain: exact match, else the longest zone name that\n * is a suffix of the domain (so \"api.foo.com\" matches the \"foo.com\" zone).\n * Returns the zone object or null.\n */\n async findHostedZoneForDomain(domain) {\n const target = String(domain ?? \"\").replace(/\\.$/, \"\").toLowerCase();\n if (!target) return null;\n const zones = await this.listHostedZones();\n const exact = zones.find(z => z.name.toLowerCase() === target);\n if (exact) return exact;\n return zones\n .filter(z => target.endsWith(`.${z.name.toLowerCase()}`))\n .sort((a, b) => b.name.length - a.name.length)[0] ?? null;\n }\n\n // ── AMI lookup ──────────────────────────────────────────────────────────────\n /**\n * Latest Ubuntu AMI matching a name pattern in a region.\n * Returns { id, name, creationDate, architecture } or null.\n */\n async findLatestUbuntuAmi({ region, pattern = DEFAULT_UBUNTU_PATTERN, architecture = \"x86_64\" } = {}) {\n const out = await this._ec2(region).send(new DescribeImagesCommand({\n Owners: [CANONICAL_OWNER_ID],\n Filters: [\n { Name: \"name\", Values: [pattern] },\n { Name: \"virtualization-type\", Values: [\"hvm\"] },\n { Name: \"state\", Values: [\"available\"] },\n ...(architecture ? [{ Name: \"architecture\", Values: [architecture] }] : []),\n ],\n }));\n const newest = (out.Images ?? [])\n .sort((a, b) => String(b.CreationDate).localeCompare(String(a.CreationDate)))[0];\n if (!newest) return null;\n return {\n id: newest.ImageId,\n name: newest.Name,\n creationDate: newest.CreationDate,\n architecture: newest.Architecture,\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA6BA,wBAGO;AACP,wBASO;AACP,6BAGO;AAEP,IAAM,iBAAiB;AACvB,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAE/B,SAAS,QAAQ,MAAM;AACnB,QAAM,KAAK,QAAQ,CAAC,GAAG,KAAK,OAAK,EAAE,QAAQ,MAAM;AACjD,SAAO,GAAG,SAAS;AACvB;AAEO,IAAM,MAAN,MAAM,KAAI;AAAA;AAAA;AAAA;AAAA;AAAA,EAKb,aAAa,KAAK,SAAS,UAAU,CAAC,GAAG;AAIrC,UAAM,SAAS,OAAO,MAAM;AACxB,UAAI;AACA,cAAM,IAAI,MAAM,SAAS,QAAQ,MAAM,GAAG,QAAQ;AAClD,eAAO,KAAK,QAAQ,OAAO,CAAC,EAAE,KAAK,MAAM,KAAK,IAAI;AAAA,MACtD,QAAQ;AACJ,eAAO;AAAA,MACX;AAAA,IACJ;AAEA,UAAM,SAAS;AAAA,MACX,QAAQ,QAAQ,UAAW,MAAM,OAAO,WAAW,KAAM;AAAA,MACzD,aAAa,QAAQ,eAAgB,MAAM,OAAO,gBAAgB;AAAA,MAClE,iBAAiB,QAAQ,mBAAoB,MAAM,OAAO,oBAAoB;AAAA,MAC9E,cAAc,QAAQ,gBAAiB,MAAM,OAAO,iBAAiB;AAAA,IACzE;AAEA,UAAM,MAAM,IAAI,KAAI,SAAS,MAAM;AACnC,aAAS,QAAQ;AAAA,MACb,gBAAgB,OAAO,MAAM,gBAAgB,OAAO,cAAc,aAAa,eAAe;AAAA,IAClG;AACA,WAAO;AAAA,EACX;AAAA,EAEA,YAAY,SAAS,QAAQ;AACzB,SAAK,SAAS,SAAS,UAAU;AACjC,SAAK,SAAS,OAAO,UAAU;AAK/B,SAAK,eAAe,OAAO,eAAe,OAAO,kBAC3C;AAAA,MACE,aAAa,OAAO;AAAA,MACpB,iBAAiB,OAAO;AAAA,MACxB,GAAI,OAAO,eAAe,EAAE,cAAc,OAAO,aAAa,IAAI,CAAC;AAAA,IACvE,IACE;AAGN,YAAQ,IAAI,qDAAqD;AAEjE,SAAK,WAAW,CAAC;AAAA,EACrB;AAAA;AAAA,EAGA,cAAc,QAAQ;AAClB,UAAM,MAAM,EAAE,QAAQ,UAAU,KAAK,OAAO;AAC5C,QAAI,KAAK,aAAc,KAAI,cAAc,KAAK;AAC9C,WAAO;AAAA,EACX;AAAA,EAEA,OAAO;AACH,WAAQ,KAAK,SAAS,QAAQ,IAAI,4BAAU,KAAK,cAAc,CAAC;AAAA,EACpE;AAAA,EAEA,KAAK,QAAQ;AACT,UAAM,MAAM,OAAO,UAAU,KAAK,MAAM;AACxC,WAAQ,KAAK,SAAS,GAAG,MAAM,IAAI,4BAAU,KAAK,cAAc,MAAM,CAAC;AAAA,EAC3E;AAAA,EAEA,WAAW;AAEP,WAAQ,KAAK,SAAS,YAAY,IAAI,qCAAc,KAAK,cAAc,CAAC;AAAA,EAC5E;AAAA,EAEA,YAAY;AAAE,WAAO,KAAK;AAAA,EAAQ;AAAA;AAAA;AAAA,EAIlC,MAAM,SAAS;AACX,UAAM,MAAM,MAAM,KAAK,KAAK,EAAE,KAAK,IAAI,2CAAyB,CAAC,CAAC,CAAC;AACnE,WAAO,EAAE,SAAS,IAAI,SAAS,KAAK,IAAI,KAAK,QAAQ,IAAI,OAAO;AAAA,EACpE;AAAA;AAAA;AAAA,EAIA,MAAM,YAAY,EAAE,aAAa,MAAM,IAAI,CAAC,GAAG;AAC3C,UAAM,MAAM,MAAM,KAAK,KAAK,EAAE,KAAK,IAAI,yCAAuB,EAAE,YAAY,WAAW,CAAC,CAAC;AACzF,YAAQ,IAAI,WAAW,CAAC,GAAG,IAAI,OAAK,EAAE,UAAU,EAAE,KAAK;AAAA,EAC3D;AAAA;AAAA,EAGA,MAAM,sBAAsB,QAAQ;AAChC,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,mDAAiC,CAAC,CAAC,CAAC;AACjF,YAAQ,IAAI,qBAAqB,CAAC,GAC7B,OAAO,OAAK,EAAE,UAAU,WAAW,EACnC,IAAI,OAAK,EAAE,QAAQ,EACnB,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAIA,MAAM,SAAS,QAAQ;AACnB,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,sCAAoB,CAAC,CAAC,CAAC;AACpE,YAAQ,IAAI,QAAQ,CAAC,GAAG,IAAI,QAAM;AAAA,MAC9B,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,WAAW,CAAC,CAAC,EAAE;AAAA,MACf,MAAM,QAAQ,EAAE,IAAI;AAAA,IACxB,EAAE;AAAA,EACN;AAAA;AAAA,EAGA,MAAM,YAAY,EAAE,OAAO,OAAO,IAAI,CAAC,GAAG;AACtC,UAAM,UAAU,QAAQ,CAAC,EAAE,MAAM,UAAU,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI;AAChE,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,yCAAuB,EAAE,QAAQ,CAAC,CAAC;AAChF,YAAQ,IAAI,WAAW,CAAC,GAAG,IAAI,QAAM;AAAA,MACjC,IAAI,EAAE;AAAA,MACN,OAAO,EAAE;AAAA,MACT,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,QAAQ,CAAC,CAAC,EAAE;AAAA,MACZ,MAAM,QAAQ,EAAE,IAAI;AAAA,IACxB,EAAE;AAAA,EACN;AAAA;AAAA,EAGA,MAAM,mBAAmB,EAAE,OAAO,OAAO,IAAI,CAAC,GAAG;AAC7C,UAAM,UAAU,QAAQ,CAAC,EAAE,MAAM,UAAU,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI;AAChE,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,gDAA8B,EAAE,QAAQ,CAAC,CAAC;AACvF,YAAQ,IAAI,kBAAkB,CAAC,GAAG,IAAI,QAAM;AAAA,MACxC,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,OAAO,EAAE;AAAA,MACT,aAAa,EAAE;AAAA,IACnB,EAAE;AAAA,EACN;AAAA;AAAA,EAGA,MAAM,aAAa,QAAQ;AACvB,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,0CAAwB,CAAC,CAAC,CAAC;AACxE,YAAQ,IAAI,YAAY,CAAC,GAAG,IAAI,QAAM,EAAE,MAAM,EAAE,SAAS,aAAa,EAAE,eAAe,EAAE;AAAA,EAC7F;AAAA;AAAA;AAAA,EAIA,MAAM,kBAAkB;AACpB,UAAM,QAAQ,CAAC;AACf,QAAI;AACJ,OAAG;AACC,YAAM,MAAM,MAAM,KAAK,SAAS,EAAE,KAAK,IAAI,8CAAuB,EAAE,QAAQ,OAAO,CAAC,CAAC;AACrF,iBAAW,KAAK,IAAI,eAAe,CAAC,GAAG;AACnC,cAAM,KAAK;AAAA,UACP,KAAK,EAAE,MAAM,IAAI,QAAQ,gBAAgB,EAAE;AAAA,UAC3C,OAAO,EAAE,QAAQ,IAAI,QAAQ,OAAO,EAAE;AAAA;AAAA,UACtC,SAAS,CAAC,CAAC,EAAE,QAAQ;AAAA,UACrB,aAAa,EAAE;AAAA,QACnB,CAAC;AAAA,MACL;AACA,eAAS,IAAI,cAAc,IAAI,aAAa;AAAA,IAChD,SAAS;AACT,WAAO;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,wBAAwB,QAAQ;AAClC,UAAM,SAAS,OAAO,UAAU,EAAE,EAAE,QAAQ,OAAO,EAAE,EAAE,YAAY;AACnE,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,QAAQ,MAAM,KAAK,gBAAgB;AACzC,UAAM,QAAQ,MAAM,KAAK,OAAK,EAAE,KAAK,YAAY,MAAM,MAAM;AAC7D,QAAI,MAAO,QAAO;AAClB,WAAO,MACF,OAAO,OAAK,OAAO,SAAS,IAAI,EAAE,KAAK,YAAY,CAAC,EAAE,CAAC,EACvD,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,CAAC,KAAK;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,oBAAoB,EAAE,QAAQ,UAAU,wBAAwB,eAAe,SAAS,IAAI,CAAC,GAAG;AAClG,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,wCAAsB;AAAA,MAC/D,QAAQ,CAAC,kBAAkB;AAAA,MAC3B,SAAS;AAAA,QACL,EAAE,MAAM,QAAQ,QAAQ,CAAC,OAAO,EAAE;AAAA,QAClC,EAAE,MAAM,uBAAuB,QAAQ,CAAC,KAAK,EAAE;AAAA,QAC/C,EAAE,MAAM,SAAS,QAAQ,CAAC,WAAW,EAAE;AAAA,QACvC,GAAI,eAAe,CAAC,EAAE,MAAM,gBAAgB,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,CAAC;AAAA,MAC7E;AAAA,IACJ,CAAC,CAAC;AACF,UAAM,UAAU,IAAI,UAAU,CAAC,GAC1B,KAAK,CAAC,GAAG,MAAM,OAAO,EAAE,YAAY,EAAE,cAAc,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC;AACnF,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO;AAAA,MACH,IAAI,OAAO;AAAA,MACX,MAAM,OAAO;AAAA,MACb,cAAc,OAAO;AAAA,MACrB,cAAc,OAAO;AAAA,IACzB;AAAA,EACJ;AACJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/aws/index.js"],"sourcesContent":["/**\n * AWS discovery component\n *\n * A thin, read-only wrapper over a few AWS SDK v3 clients (STS, EC2, Route53)\n * with cli-toolkit conventions:\n *\n * const aws = await Aws.init(context);\n * const me = await aws.whoAmI();\n * const zones = await aws.listHostedZones();\n *\n * The whole point: given just an access key + secret (usually from .env), it can\n * *discover* the boring-to-look-up bits of an AWS account (account id, regions,\n * VPCs, subnets, security groups, key pairs, Route53 hosted zones, the latest\n * Ubuntu AMI) so scripts can pre-fill config (e.g. a terraform.tfvars) instead of\n * making you hunt through the console.\n *\n * Credentials + region are resolved via context.params, so they come from CLI,\n * env, or .env transparently:\n * awsAccessKeyId -> AWS_ACCESS_KEY_ID\n * awsSecretAccessKey -> AWS_SECRET_ACCESS_KEY\n * awsSessionToken -> AWS_SESSION_TOKEN (optional, for temp creds)\n * awsRegion -> AWS_REGION (default us-east-1)\n *\n * If no explicit keys are found, the SDK's default credential chain is used\n * (shared config/profile, instance role, etc.) — so AWS_PROFILE also works.\n *\n * Everything here is read-only (Describe / List / Get calls). It never mutates AWS.\n */\n\nimport {\n STSClient,\n GetCallerIdentityCommand,\n} from \"@aws-sdk/client-sts\";\nimport {\n EC2Client,\n DescribeRegionsCommand,\n DescribeAvailabilityZonesCommand,\n DescribeVpcsCommand,\n DescribeSubnetsCommand,\n DescribeSecurityGroupsCommand,\n DescribeKeyPairsCommand,\n DescribeImagesCommand,\n} from \"@aws-sdk/client-ec2\";\nimport {\n Route53Client,\n ListHostedZonesCommand,\n} from \"@aws-sdk/client-route-53\";\n\nconst DEFAULT_REGION = \"us-east-1\";\nconst CANONICAL_OWNER_ID = \"099720109477\"; // Ubuntu images\nconst DEFAULT_UBUNTU_PATTERN = \"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*\";\n\nfunction nameTag(tags) {\n const t = (tags ?? []).find(x => x.Key === \"Name\");\n return t?.Value ?? null;\n}\n\nexport class Aws {\n /**\n * Resolve credentials/region from params and return an Aws instance.\n * Does not call AWS until you invoke a method (clients are lazy).\n */\n static async init(context, options = {}) {\n // Tolerate blank/missing values (e.g. an env var present but empty, like\n // a half-filled .env): treat them as \"not set\" rather than failing param\n // validation, so we fall back cleanly to the SDK default credential chain.\n const getOpt = async (k) => {\n try {\n const v = await context?.params?.get?.(k, \"string\");\n return v != null && String(v).trim() !== \"\" ? v : undefined;\n } catch {\n return undefined;\n }\n };\n\n const config = {\n region: options.region ?? (await getOpt(\"awsRegion\")) ?? DEFAULT_REGION,\n accessKeyId: options.accessKeyId ?? (await getOpt(\"awsAccessKeyId\")),\n secretAccessKey: options.secretAccessKey ?? (await getOpt(\"awsSecretAccessKey\")),\n sessionToken: options.sessionToken ?? (await getOpt(\"awsSessionToken\")),\n };\n\n const aws = new Aws(context, config);\n context?.logger?.debug?.(\n `[aws] region=${config.region} credentials=${config.accessKeyId ? \"explicit\" : \"default-chain\"}`,\n );\n return aws;\n }\n\n constructor(context, config) {\n this.logger = context?.logger ?? console;\n this.region = config.region || DEFAULT_REGION;\n\n // Build an explicit credentials object only when keys were supplied;\n // otherwise leave undefined so the SDK uses its default chain (profile,\n // instance role, env, …).\n this._credentials = config.accessKeyId && config.secretAccessKey\n ? {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n ...(config.sessionToken ? { sessionToken: config.sessionToken } : {}),\n }\n : undefined;\n\n /** True when explicit access key + secret were supplied (env/.env/CLI). */\n this.hasExplicitCredentials = !!this._credentials;\n\n // Quiet the AWS SDK v3 \"node >=22 required after Jan 2027\" notice.\n process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= \"true\";\n\n this._clients = {};\n }\n\n // ── clients (lazy, optionally region-scoped) ──────────────────────────────\n _clientConfig(region) {\n const cfg = { region: region || this.region };\n if (this._credentials) cfg.credentials = this._credentials;\n return cfg;\n }\n\n _sts() {\n return (this._clients.sts ??= new STSClient(this._clientConfig()));\n }\n\n _ec2(region) {\n const key = `ec2:${region || this.region}`;\n return (this._clients[key] ??= new EC2Client(this._clientConfig(region)));\n }\n\n _route53() {\n // Route53 is global; region is irrelevant but the client still wants one.\n return (this._clients.route53 ??= new Route53Client(this._clientConfig()));\n }\n\n getRegion() { return this.region; }\n\n // ── credentials ─────────────────────────────────────────────────────────────\n /**\n * Are any credentials available *before* hitting AWS? Returns\n * { ok, source } — explicit keys, or a resolvable default chain (profile/SSO/\n * instance role). { ok:false } means there's nothing to even try with.\n * Note: \"ok\" only means creds were *found*, not that AWS will accept them.\n */\n async checkCredentials() {\n if (this.hasExplicitCredentials) {\n return { ok: true, source: \"explicit keys (env/.env/CLI)\" };\n }\n try {\n const provider = this._sts().config.credentials;\n const resolved = typeof provider === \"function\" ? await provider() : provider;\n if (resolved?.accessKeyId) {\n return { ok: true, source: \"default credential chain (profile/SSO/role)\" };\n }\n } catch {\n // nothing resolvable\n }\n return { ok: false };\n }\n\n /** True for \"bad/missing credentials\" style errors (vs. real failures). */\n static isAuthError(err) {\n const name = err?.name || err?.Code || err?.__type || \"\";\n return [\n \"CredentialsProviderError\",\n \"InvalidClientTokenId\",\n \"UnrecognizedClientException\",\n \"AuthFailure\",\n \"AccessDenied\",\n \"AccessDeniedException\",\n \"ExpiredToken\",\n \"ExpiredTokenException\",\n \"SignatureDoesNotMatch\",\n \"MissingAuthenticationToken\",\n ].includes(name);\n }\n\n /** Short, precise instructions for getting AWS credentials into .env. */\n static credentialsHelp(region = DEFAULT_REGION) {\n return [\n \"No usable AWS credentials were found (or AWS rejected them).\",\n \"\",\n \"Put a read-only access key in the project's .env:\",\n \"\",\n \" AWS_ACCESS_KEY_ID=AKIA...\",\n \" AWS_SECRET_ACCESS_KEY=...\",\n ` AWS_REGION=${region} # optional (default ${DEFAULT_REGION})`,\n \"\",\n \"Get a key from the AWS console (~2 min):\",\n \" 1. IAM → Users → create or pick a user (console sign-in not needed).\",\n ' 2. Attach a policy — \"ReadOnlyAccess\" (AWS managed) is enough for discovery.',\n ' 3. The user → \"Security credentials\" → \"Create access key\" → \"CLI\".',\n \" 4. Copy the Access key ID + Secret access key (the secret shows only once).\",\n \" 5. Paste both into .env, then re-run.\",\n \" Direct link: https://console.aws.amazon.com/iam/home#/users\",\n \"\",\n \"Prefer a named profile or an EC2 instance role? Re-run with AWS_PROFILE=<name>\",\n \"set (or on the instance) and credentials resolve automatically.\",\n ].join(\"\\n\");\n }\n\n // ── identity ──────────────────────────────────────────────────────────────\n /** { account, arn, userId } — confirm which account/identity the keys belong to. */\n async whoAmI() {\n const out = await this._sts().send(new GetCallerIdentityCommand({}));\n return { account: out.Account, arn: out.Arn, userId: out.UserId };\n }\n\n // ── regions / AZs ──────────────────────────────────────────────────────────\n /** Enabled region names, sorted. */\n async listRegions({ allRegions = false } = {}) {\n const out = await this._ec2().send(new DescribeRegionsCommand({ AllRegions: allRegions }));\n return (out.Regions ?? []).map(r => r.RegionName).sort();\n }\n\n /** Availability zone names for a region (defaults to the instance region). */\n async listAvailabilityZones(region) {\n const out = await this._ec2(region).send(new DescribeAvailabilityZonesCommand({}));\n return (out.AvailabilityZones ?? [])\n .filter(z => z.State === \"available\")\n .map(z => z.ZoneName)\n .sort();\n }\n\n // ── VPC / subnets / SGs / key pairs ────────────────────────────────────────\n /** [{ id, cidr, isDefault, name }] in a region. */\n async listVpcs(region) {\n const out = await this._ec2(region).send(new DescribeVpcsCommand({}));\n return (out.Vpcs ?? []).map(v => ({\n id: v.VpcId,\n cidr: v.CidrBlock,\n isDefault: !!v.IsDefault,\n name: nameTag(v.Tags),\n }));\n }\n\n /** [{ id, vpcId, az, cidr, public, name }]; pass vpcId to scope. */\n async listSubnets({ vpcId, region } = {}) {\n const Filters = vpcId ? [{ Name: \"vpc-id\", Values: [vpcId] }] : undefined;\n const out = await this._ec2(region).send(new DescribeSubnetsCommand({ Filters }));\n return (out.Subnets ?? []).map(s => ({\n id: s.SubnetId,\n vpcId: s.VpcId,\n az: s.AvailabilityZone,\n cidr: s.CidrBlock,\n public: !!s.MapPublicIpOnLaunch,\n name: nameTag(s.Tags),\n }));\n }\n\n /** [{ id, name, vpcId, description }]; pass vpcId to scope. */\n async listSecurityGroups({ vpcId, region } = {}) {\n const Filters = vpcId ? [{ Name: \"vpc-id\", Values: [vpcId] }] : undefined;\n const out = await this._ec2(region).send(new DescribeSecurityGroupsCommand({ Filters }));\n return (out.SecurityGroups ?? []).map(g => ({\n id: g.GroupId,\n name: g.GroupName,\n vpcId: g.VpcId,\n description: g.Description,\n }));\n }\n\n /** [{ name, fingerprint }] EC2 key pairs in a region. */\n async listKeyPairs(region) {\n const out = await this._ec2(region).send(new DescribeKeyPairsCommand({}));\n return (out.KeyPairs ?? []).map(k => ({ name: k.KeyName, fingerprint: k.KeyFingerprint }));\n }\n\n // ── Route53 ────────────────────────────────────────────────────────────────\n /** [{ id, name, private, recordCount }] — id is the bare zone id (no /hostedzone/). */\n async listHostedZones() {\n const zones = [];\n let marker;\n do {\n const out = await this._route53().send(new ListHostedZonesCommand({ Marker: marker }));\n for (const z of out.HostedZones ?? []) {\n zones.push({\n id: (z.Id ?? \"\").replace(\"/hostedzone/\", \"\"),\n name: (z.Name ?? \"\").replace(/\\.$/, \"\"), // strip trailing dot\n private: !!z.Config?.PrivateZone,\n recordCount: z.ResourceRecordSetCount,\n });\n }\n marker = out.IsTruncated ? out.NextMarker : undefined;\n } while (marker);\n return zones;\n }\n\n /**\n * Best hosted zone for a domain: exact match, else the longest zone name that\n * is a suffix of the domain (so \"api.foo.com\" matches the \"foo.com\" zone).\n * Returns the zone object or null.\n */\n async findHostedZoneForDomain(domain) {\n const target = String(domain ?? \"\").replace(/\\.$/, \"\").toLowerCase();\n if (!target) return null;\n const zones = await this.listHostedZones();\n const exact = zones.find(z => z.name.toLowerCase() === target);\n if (exact) return exact;\n return zones\n .filter(z => target.endsWith(`.${z.name.toLowerCase()}`))\n .sort((a, b) => b.name.length - a.name.length)[0] ?? null;\n }\n\n // ── AMI lookup ──────────────────────────────────────────────────────────────\n /**\n * Latest Ubuntu AMI matching a name pattern in a region.\n * Returns { id, name, creationDate, architecture } or null.\n */\n async findLatestUbuntuAmi({ region, pattern = DEFAULT_UBUNTU_PATTERN, architecture = \"x86_64\" } = {}) {\n const out = await this._ec2(region).send(new DescribeImagesCommand({\n Owners: [CANONICAL_OWNER_ID],\n Filters: [\n { Name: \"name\", Values: [pattern] },\n { Name: \"virtualization-type\", Values: [\"hvm\"] },\n { Name: \"state\", Values: [\"available\"] },\n ...(architecture ? [{ Name: \"architecture\", Values: [architecture] }] : []),\n ],\n }));\n const newest = (out.Images ?? [])\n .sort((a, b) => String(b.CreationDate).localeCompare(String(a.CreationDate)))[0];\n if (!newest) return null;\n return {\n id: newest.ImageId,\n name: newest.Name,\n creationDate: newest.CreationDate,\n architecture: newest.Architecture,\n };\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/aws.js
CHANGED
|
@@ -58,6 +58,7 @@ var Aws = class _Aws {
|
|
|
58
58
|
secretAccessKey: config.secretAccessKey,
|
|
59
59
|
...config.sessionToken ? { sessionToken: config.sessionToken } : {}
|
|
60
60
|
} : void 0;
|
|
61
|
+
this.hasExplicitCredentials = !!this._credentials;
|
|
61
62
|
process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= "true";
|
|
62
63
|
this._clients = {};
|
|
63
64
|
}
|
|
@@ -80,6 +81,66 @@ var Aws = class _Aws {
|
|
|
80
81
|
getRegion() {
|
|
81
82
|
return this.region;
|
|
82
83
|
}
|
|
84
|
+
// ── credentials ─────────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Are any credentials available *before* hitting AWS? Returns
|
|
87
|
+
* { ok, source } — explicit keys, or a resolvable default chain (profile/SSO/
|
|
88
|
+
* instance role). { ok:false } means there's nothing to even try with.
|
|
89
|
+
* Note: "ok" only means creds were *found*, not that AWS will accept them.
|
|
90
|
+
*/
|
|
91
|
+
async checkCredentials() {
|
|
92
|
+
if (this.hasExplicitCredentials) {
|
|
93
|
+
return { ok: true, source: "explicit keys (env/.env/CLI)" };
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const provider = this._sts().config.credentials;
|
|
97
|
+
const resolved = typeof provider === "function" ? await provider() : provider;
|
|
98
|
+
if (resolved?.accessKeyId) {
|
|
99
|
+
return { ok: true, source: "default credential chain (profile/SSO/role)" };
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
return { ok: false };
|
|
104
|
+
}
|
|
105
|
+
/** True for "bad/missing credentials" style errors (vs. real failures). */
|
|
106
|
+
static isAuthError(err) {
|
|
107
|
+
const name = err?.name || err?.Code || err?.__type || "";
|
|
108
|
+
return [
|
|
109
|
+
"CredentialsProviderError",
|
|
110
|
+
"InvalidClientTokenId",
|
|
111
|
+
"UnrecognizedClientException",
|
|
112
|
+
"AuthFailure",
|
|
113
|
+
"AccessDenied",
|
|
114
|
+
"AccessDeniedException",
|
|
115
|
+
"ExpiredToken",
|
|
116
|
+
"ExpiredTokenException",
|
|
117
|
+
"SignatureDoesNotMatch",
|
|
118
|
+
"MissingAuthenticationToken"
|
|
119
|
+
].includes(name);
|
|
120
|
+
}
|
|
121
|
+
/** Short, precise instructions for getting AWS credentials into .env. */
|
|
122
|
+
static credentialsHelp(region = DEFAULT_REGION) {
|
|
123
|
+
return [
|
|
124
|
+
"No usable AWS credentials were found (or AWS rejected them).",
|
|
125
|
+
"",
|
|
126
|
+
"Put a read-only access key in the project's .env:",
|
|
127
|
+
"",
|
|
128
|
+
" AWS_ACCESS_KEY_ID=AKIA...",
|
|
129
|
+
" AWS_SECRET_ACCESS_KEY=...",
|
|
130
|
+
` AWS_REGION=${region} # optional (default ${DEFAULT_REGION})`,
|
|
131
|
+
"",
|
|
132
|
+
"Get a key from the AWS console (~2 min):",
|
|
133
|
+
" 1. IAM \u2192 Users \u2192 create or pick a user (console sign-in not needed).",
|
|
134
|
+
' 2. Attach a policy \u2014 "ReadOnlyAccess" (AWS managed) is enough for discovery.',
|
|
135
|
+
' 3. The user \u2192 "Security credentials" \u2192 "Create access key" \u2192 "CLI".',
|
|
136
|
+
" 4. Copy the Access key ID + Secret access key (the secret shows only once).",
|
|
137
|
+
" 5. Paste both into .env, then re-run.",
|
|
138
|
+
" Direct link: https://console.aws.amazon.com/iam/home#/users",
|
|
139
|
+
"",
|
|
140
|
+
"Prefer a named profile or an EC2 instance role? Re-run with AWS_PROFILE=<name>",
|
|
141
|
+
"set (or on the instance) and credentials resolve automatically."
|
|
142
|
+
].join("\n");
|
|
143
|
+
}
|
|
83
144
|
// ── identity ──────────────────────────────────────────────────────────────
|
|
84
145
|
/** { account, arn, userId } — confirm which account/identity the keys belong to. */
|
|
85
146
|
async whoAmI() {
|
package/dist/aws.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/aws/index.js"],"sourcesContent":["/**\n * AWS discovery component\n *\n * A thin, read-only wrapper over a few AWS SDK v3 clients (STS, EC2, Route53)\n * with cli-toolkit conventions:\n *\n * const aws = await Aws.init(context);\n * const me = await aws.whoAmI();\n * const zones = await aws.listHostedZones();\n *\n * The whole point: given just an access key + secret (usually from .env), it can\n * *discover* the boring-to-look-up bits of an AWS account (account id, regions,\n * VPCs, subnets, security groups, key pairs, Route53 hosted zones, the latest\n * Ubuntu AMI) so scripts can pre-fill config (e.g. a terraform.tfvars) instead of\n * making you hunt through the console.\n *\n * Credentials + region are resolved via context.params, so they come from CLI,\n * env, or .env transparently:\n * awsAccessKeyId -> AWS_ACCESS_KEY_ID\n * awsSecretAccessKey -> AWS_SECRET_ACCESS_KEY\n * awsSessionToken -> AWS_SESSION_TOKEN (optional, for temp creds)\n * awsRegion -> AWS_REGION (default us-east-1)\n *\n * If no explicit keys are found, the SDK's default credential chain is used\n * (shared config/profile, instance role, etc.) — so AWS_PROFILE also works.\n *\n * Everything here is read-only (Describe / List / Get calls). It never mutates AWS.\n */\n\nimport {\n STSClient,\n GetCallerIdentityCommand,\n} from \"@aws-sdk/client-sts\";\nimport {\n EC2Client,\n DescribeRegionsCommand,\n DescribeAvailabilityZonesCommand,\n DescribeVpcsCommand,\n DescribeSubnetsCommand,\n DescribeSecurityGroupsCommand,\n DescribeKeyPairsCommand,\n DescribeImagesCommand,\n} from \"@aws-sdk/client-ec2\";\nimport {\n Route53Client,\n ListHostedZonesCommand,\n} from \"@aws-sdk/client-route-53\";\n\nconst DEFAULT_REGION = \"us-east-1\";\nconst CANONICAL_OWNER_ID = \"099720109477\"; // Ubuntu images\nconst DEFAULT_UBUNTU_PATTERN = \"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*\";\n\nfunction nameTag(tags) {\n const t = (tags ?? []).find(x => x.Key === \"Name\");\n return t?.Value ?? null;\n}\n\nexport class Aws {\n /**\n * Resolve credentials/region from params and return an Aws instance.\n * Does not call AWS until you invoke a method (clients are lazy).\n */\n static async init(context, options = {}) {\n // Tolerate blank/missing values (e.g. an env var present but empty, like\n // a half-filled .env): treat them as \"not set\" rather than failing param\n // validation, so we fall back cleanly to the SDK default credential chain.\n const getOpt = async (k) => {\n try {\n const v = await context?.params?.get?.(k, \"string\");\n return v != null && String(v).trim() !== \"\" ? v : undefined;\n } catch {\n return undefined;\n }\n };\n\n const config = {\n region: options.region ?? (await getOpt(\"awsRegion\")) ?? DEFAULT_REGION,\n accessKeyId: options.accessKeyId ?? (await getOpt(\"awsAccessKeyId\")),\n secretAccessKey: options.secretAccessKey ?? (await getOpt(\"awsSecretAccessKey\")),\n sessionToken: options.sessionToken ?? (await getOpt(\"awsSessionToken\")),\n };\n\n const aws = new Aws(context, config);\n context?.logger?.debug?.(\n `[aws] region=${config.region} credentials=${config.accessKeyId ? \"explicit\" : \"default-chain\"}`,\n );\n return aws;\n }\n\n constructor(context, config) {\n this.logger = context?.logger ?? console;\n this.region = config.region || DEFAULT_REGION;\n\n // Build an explicit credentials object only when keys were supplied;\n // otherwise leave undefined so the SDK uses its default chain (profile,\n // instance role, env, …).\n this._credentials = config.accessKeyId && config.secretAccessKey\n ? {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n ...(config.sessionToken ? { sessionToken: config.sessionToken } : {}),\n }\n : undefined;\n\n // Quiet the AWS SDK v3 \"node >=22 required after Jan 2027\" notice.\n process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= \"true\";\n\n this._clients = {};\n }\n\n // ── clients (lazy, optionally region-scoped) ──────────────────────────────\n _clientConfig(region) {\n const cfg = { region: region || this.region };\n if (this._credentials) cfg.credentials = this._credentials;\n return cfg;\n }\n\n _sts() {\n return (this._clients.sts ??= new STSClient(this._clientConfig()));\n }\n\n _ec2(region) {\n const key = `ec2:${region || this.region}`;\n return (this._clients[key] ??= new EC2Client(this._clientConfig(region)));\n }\n\n _route53() {\n // Route53 is global; region is irrelevant but the client still wants one.\n return (this._clients.route53 ??= new Route53Client(this._clientConfig()));\n }\n\n getRegion() { return this.region; }\n\n // ── identity ──────────────────────────────────────────────────────────────\n /** { account, arn, userId } — confirm which account/identity the keys belong to. */\n async whoAmI() {\n const out = await this._sts().send(new GetCallerIdentityCommand({}));\n return { account: out.Account, arn: out.Arn, userId: out.UserId };\n }\n\n // ── regions / AZs ──────────────────────────────────────────────────────────\n /** Enabled region names, sorted. */\n async listRegions({ allRegions = false } = {}) {\n const out = await this._ec2().send(new DescribeRegionsCommand({ AllRegions: allRegions }));\n return (out.Regions ?? []).map(r => r.RegionName).sort();\n }\n\n /** Availability zone names for a region (defaults to the instance region). */\n async listAvailabilityZones(region) {\n const out = await this._ec2(region).send(new DescribeAvailabilityZonesCommand({}));\n return (out.AvailabilityZones ?? [])\n .filter(z => z.State === \"available\")\n .map(z => z.ZoneName)\n .sort();\n }\n\n // ── VPC / subnets / SGs / key pairs ────────────────────────────────────────\n /** [{ id, cidr, isDefault, name }] in a region. */\n async listVpcs(region) {\n const out = await this._ec2(region).send(new DescribeVpcsCommand({}));\n return (out.Vpcs ?? []).map(v => ({\n id: v.VpcId,\n cidr: v.CidrBlock,\n isDefault: !!v.IsDefault,\n name: nameTag(v.Tags),\n }));\n }\n\n /** [{ id, vpcId, az, cidr, public, name }]; pass vpcId to scope. */\n async listSubnets({ vpcId, region } = {}) {\n const Filters = vpcId ? [{ Name: \"vpc-id\", Values: [vpcId] }] : undefined;\n const out = await this._ec2(region).send(new DescribeSubnetsCommand({ Filters }));\n return (out.Subnets ?? []).map(s => ({\n id: s.SubnetId,\n vpcId: s.VpcId,\n az: s.AvailabilityZone,\n cidr: s.CidrBlock,\n public: !!s.MapPublicIpOnLaunch,\n name: nameTag(s.Tags),\n }));\n }\n\n /** [{ id, name, vpcId, description }]; pass vpcId to scope. */\n async listSecurityGroups({ vpcId, region } = {}) {\n const Filters = vpcId ? [{ Name: \"vpc-id\", Values: [vpcId] }] : undefined;\n const out = await this._ec2(region).send(new DescribeSecurityGroupsCommand({ Filters }));\n return (out.SecurityGroups ?? []).map(g => ({\n id: g.GroupId,\n name: g.GroupName,\n vpcId: g.VpcId,\n description: g.Description,\n }));\n }\n\n /** [{ name, fingerprint }] EC2 key pairs in a region. */\n async listKeyPairs(region) {\n const out = await this._ec2(region).send(new DescribeKeyPairsCommand({}));\n return (out.KeyPairs ?? []).map(k => ({ name: k.KeyName, fingerprint: k.KeyFingerprint }));\n }\n\n // ── Route53 ────────────────────────────────────────────────────────────────\n /** [{ id, name, private, recordCount }] — id is the bare zone id (no /hostedzone/). */\n async listHostedZones() {\n const zones = [];\n let marker;\n do {\n const out = await this._route53().send(new ListHostedZonesCommand({ Marker: marker }));\n for (const z of out.HostedZones ?? []) {\n zones.push({\n id: (z.Id ?? \"\").replace(\"/hostedzone/\", \"\"),\n name: (z.Name ?? \"\").replace(/\\.$/, \"\"), // strip trailing dot\n private: !!z.Config?.PrivateZone,\n recordCount: z.ResourceRecordSetCount,\n });\n }\n marker = out.IsTruncated ? out.NextMarker : undefined;\n } while (marker);\n return zones;\n }\n\n /**\n * Best hosted zone for a domain: exact match, else the longest zone name that\n * is a suffix of the domain (so \"api.foo.com\" matches the \"foo.com\" zone).\n * Returns the zone object or null.\n */\n async findHostedZoneForDomain(domain) {\n const target = String(domain ?? \"\").replace(/\\.$/, \"\").toLowerCase();\n if (!target) return null;\n const zones = await this.listHostedZones();\n const exact = zones.find(z => z.name.toLowerCase() === target);\n if (exact) return exact;\n return zones\n .filter(z => target.endsWith(`.${z.name.toLowerCase()}`))\n .sort((a, b) => b.name.length - a.name.length)[0] ?? null;\n }\n\n // ── AMI lookup ──────────────────────────────────────────────────────────────\n /**\n * Latest Ubuntu AMI matching a name pattern in a region.\n * Returns { id, name, creationDate, architecture } or null.\n */\n async findLatestUbuntuAmi({ region, pattern = DEFAULT_UBUNTU_PATTERN, architecture = \"x86_64\" } = {}) {\n const out = await this._ec2(region).send(new DescribeImagesCommand({\n Owners: [CANONICAL_OWNER_ID],\n Filters: [\n { Name: \"name\", Values: [pattern] },\n { Name: \"virtualization-type\", Values: [\"hvm\"] },\n { Name: \"state\", Values: [\"available\"] },\n ...(architecture ? [{ Name: \"architecture\", Values: [architecture] }] : []),\n ],\n }));\n const newest = (out.Images ?? [])\n .sort((a, b) => String(b.CreationDate).localeCompare(String(a.CreationDate)))[0];\n if (!newest) return null;\n return {\n id: newest.ImageId,\n name: newest.Name,\n creationDate: newest.CreationDate,\n architecture: newest.Architecture,\n };\n }\n}\n"],"mappings":";AA6BA;AAAA,EACI;AAAA,EACA;AAAA,OACG;AACP;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EACI;AAAA,EACA;AAAA,OACG;AAEP,IAAM,iBAAiB;AACvB,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAE/B,SAAS,QAAQ,MAAM;AACnB,QAAM,KAAK,QAAQ,CAAC,GAAG,KAAK,OAAK,EAAE,QAAQ,MAAM;AACjD,SAAO,GAAG,SAAS;AACvB;AAEO,IAAM,MAAN,MAAM,KAAI;AAAA;AAAA;AAAA;AAAA;AAAA,EAKb,aAAa,KAAK,SAAS,UAAU,CAAC,GAAG;AAIrC,UAAM,SAAS,OAAO,MAAM;AACxB,UAAI;AACA,cAAM,IAAI,MAAM,SAAS,QAAQ,MAAM,GAAG,QAAQ;AAClD,eAAO,KAAK,QAAQ,OAAO,CAAC,EAAE,KAAK,MAAM,KAAK,IAAI;AAAA,MACtD,QAAQ;AACJ,eAAO;AAAA,MACX;AAAA,IACJ;AAEA,UAAM,SAAS;AAAA,MACX,QAAQ,QAAQ,UAAW,MAAM,OAAO,WAAW,KAAM;AAAA,MACzD,aAAa,QAAQ,eAAgB,MAAM,OAAO,gBAAgB;AAAA,MAClE,iBAAiB,QAAQ,mBAAoB,MAAM,OAAO,oBAAoB;AAAA,MAC9E,cAAc,QAAQ,gBAAiB,MAAM,OAAO,iBAAiB;AAAA,IACzE;AAEA,UAAM,MAAM,IAAI,KAAI,SAAS,MAAM;AACnC,aAAS,QAAQ;AAAA,MACb,gBAAgB,OAAO,MAAM,gBAAgB,OAAO,cAAc,aAAa,eAAe;AAAA,IAClG;AACA,WAAO;AAAA,EACX;AAAA,EAEA,YAAY,SAAS,QAAQ;AACzB,SAAK,SAAS,SAAS,UAAU;AACjC,SAAK,SAAS,OAAO,UAAU;AAK/B,SAAK,eAAe,OAAO,eAAe,OAAO,kBAC3C;AAAA,MACE,aAAa,OAAO;AAAA,MACpB,iBAAiB,OAAO;AAAA,MACxB,GAAI,OAAO,eAAe,EAAE,cAAc,OAAO,aAAa,IAAI,CAAC;AAAA,IACvE,IACE;AAGN,YAAQ,IAAI,qDAAqD;AAEjE,SAAK,WAAW,CAAC;AAAA,EACrB;AAAA;AAAA,EAGA,cAAc,QAAQ;AAClB,UAAM,MAAM,EAAE,QAAQ,UAAU,KAAK,OAAO;AAC5C,QAAI,KAAK,aAAc,KAAI,cAAc,KAAK;AAC9C,WAAO;AAAA,EACX;AAAA,EAEA,OAAO;AACH,WAAQ,KAAK,SAAS,QAAQ,IAAI,UAAU,KAAK,cAAc,CAAC;AAAA,EACpE;AAAA,EAEA,KAAK,QAAQ;AACT,UAAM,MAAM,OAAO,UAAU,KAAK,MAAM;AACxC,WAAQ,KAAK,SAAS,GAAG,MAAM,IAAI,UAAU,KAAK,cAAc,MAAM,CAAC;AAAA,EAC3E;AAAA,EAEA,WAAW;AAEP,WAAQ,KAAK,SAAS,YAAY,IAAI,cAAc,KAAK,cAAc,CAAC;AAAA,EAC5E;AAAA,EAEA,YAAY;AAAE,WAAO,KAAK;AAAA,EAAQ;AAAA;AAAA;AAAA,EAIlC,MAAM,SAAS;AACX,UAAM,MAAM,MAAM,KAAK,KAAK,EAAE,KAAK,IAAI,yBAAyB,CAAC,CAAC,CAAC;AACnE,WAAO,EAAE,SAAS,IAAI,SAAS,KAAK,IAAI,KAAK,QAAQ,IAAI,OAAO;AAAA,EACpE;AAAA;AAAA;AAAA,EAIA,MAAM,YAAY,EAAE,aAAa,MAAM,IAAI,CAAC,GAAG;AAC3C,UAAM,MAAM,MAAM,KAAK,KAAK,EAAE,KAAK,IAAI,uBAAuB,EAAE,YAAY,WAAW,CAAC,CAAC;AACzF,YAAQ,IAAI,WAAW,CAAC,GAAG,IAAI,OAAK,EAAE,UAAU,EAAE,KAAK;AAAA,EAC3D;AAAA;AAAA,EAGA,MAAM,sBAAsB,QAAQ;AAChC,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,iCAAiC,CAAC,CAAC,CAAC;AACjF,YAAQ,IAAI,qBAAqB,CAAC,GAC7B,OAAO,OAAK,EAAE,UAAU,WAAW,EACnC,IAAI,OAAK,EAAE,QAAQ,EACnB,KAAK;AAAA,EACd;AAAA;AAAA;AAAA,EAIA,MAAM,SAAS,QAAQ;AACnB,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,oBAAoB,CAAC,CAAC,CAAC;AACpE,YAAQ,IAAI,QAAQ,CAAC,GAAG,IAAI,QAAM;AAAA,MAC9B,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,WAAW,CAAC,CAAC,EAAE;AAAA,MACf,MAAM,QAAQ,EAAE,IAAI;AAAA,IACxB,EAAE;AAAA,EACN;AAAA;AAAA,EAGA,MAAM,YAAY,EAAE,OAAO,OAAO,IAAI,CAAC,GAAG;AACtC,UAAM,UAAU,QAAQ,CAAC,EAAE,MAAM,UAAU,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI;AAChE,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,uBAAuB,EAAE,QAAQ,CAAC,CAAC;AAChF,YAAQ,IAAI,WAAW,CAAC,GAAG,IAAI,QAAM;AAAA,MACjC,IAAI,EAAE;AAAA,MACN,OAAO,EAAE;AAAA,MACT,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,QAAQ,CAAC,CAAC,EAAE;AAAA,MACZ,MAAM,QAAQ,EAAE,IAAI;AAAA,IACxB,EAAE;AAAA,EACN;AAAA;AAAA,EAGA,MAAM,mBAAmB,EAAE,OAAO,OAAO,IAAI,CAAC,GAAG;AAC7C,UAAM,UAAU,QAAQ,CAAC,EAAE,MAAM,UAAU,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI;AAChE,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,8BAA8B,EAAE,QAAQ,CAAC,CAAC;AACvF,YAAQ,IAAI,kBAAkB,CAAC,GAAG,IAAI,QAAM;AAAA,MACxC,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,OAAO,EAAE;AAAA,MACT,aAAa,EAAE;AAAA,IACnB,EAAE;AAAA,EACN;AAAA;AAAA,EAGA,MAAM,aAAa,QAAQ;AACvB,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,wBAAwB,CAAC,CAAC,CAAC;AACxE,YAAQ,IAAI,YAAY,CAAC,GAAG,IAAI,QAAM,EAAE,MAAM,EAAE,SAAS,aAAa,EAAE,eAAe,EAAE;AAAA,EAC7F;AAAA;AAAA;AAAA,EAIA,MAAM,kBAAkB;AACpB,UAAM,QAAQ,CAAC;AACf,QAAI;AACJ,OAAG;AACC,YAAM,MAAM,MAAM,KAAK,SAAS,EAAE,KAAK,IAAI,uBAAuB,EAAE,QAAQ,OAAO,CAAC,CAAC;AACrF,iBAAW,KAAK,IAAI,eAAe,CAAC,GAAG;AACnC,cAAM,KAAK;AAAA,UACP,KAAK,EAAE,MAAM,IAAI,QAAQ,gBAAgB,EAAE;AAAA,UAC3C,OAAO,EAAE,QAAQ,IAAI,QAAQ,OAAO,EAAE;AAAA;AAAA,UACtC,SAAS,CAAC,CAAC,EAAE,QAAQ;AAAA,UACrB,aAAa,EAAE;AAAA,QACnB,CAAC;AAAA,MACL;AACA,eAAS,IAAI,cAAc,IAAI,aAAa;AAAA,IAChD,SAAS;AACT,WAAO;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,wBAAwB,QAAQ;AAClC,UAAM,SAAS,OAAO,UAAU,EAAE,EAAE,QAAQ,OAAO,EAAE,EAAE,YAAY;AACnE,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,QAAQ,MAAM,KAAK,gBAAgB;AACzC,UAAM,QAAQ,MAAM,KAAK,OAAK,EAAE,KAAK,YAAY,MAAM,MAAM;AAC7D,QAAI,MAAO,QAAO;AAClB,WAAO,MACF,OAAO,OAAK,OAAO,SAAS,IAAI,EAAE,KAAK,YAAY,CAAC,EAAE,CAAC,EACvD,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,CAAC,KAAK;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,oBAAoB,EAAE,QAAQ,UAAU,wBAAwB,eAAe,SAAS,IAAI,CAAC,GAAG;AAClG,UAAM,MAAM,MAAM,KAAK,KAAK,MAAM,EAAE,KAAK,IAAI,sBAAsB;AAAA,MAC/D,QAAQ,CAAC,kBAAkB;AAAA,MAC3B,SAAS;AAAA,QACL,EAAE,MAAM,QAAQ,QAAQ,CAAC,OAAO,EAAE;AAAA,QAClC,EAAE,MAAM,uBAAuB,QAAQ,CAAC,KAAK,EAAE;AAAA,QAC/C,EAAE,MAAM,SAAS,QAAQ,CAAC,WAAW,EAAE;AAAA,QACvC,GAAI,eAAe,CAAC,EAAE,MAAM,gBAAgB,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,CAAC;AAAA,MAC7E;AAAA,IACJ,CAAC,CAAC;AACF,UAAM,UAAU,IAAI,UAAU,CAAC,GAC1B,KAAK,CAAC,GAAG,MAAM,OAAO,EAAE,YAAY,EAAE,cAAc,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC;AACnF,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO;AAAA,MACH,IAAI,OAAO;AAAA,MACX,MAAM,OAAO;AAAA,MACb,cAAc,OAAO;AAAA,MACrB,cAAc,OAAO;AAAA,IACzB;AAAA,EACJ;AACJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/aws/index.js"],"sourcesContent":["/**\n * AWS discovery component\n *\n * A thin, read-only wrapper over a few AWS SDK v3 clients (STS, EC2, Route53)\n * with cli-toolkit conventions:\n *\n * const aws = await Aws.init(context);\n * const me = await aws.whoAmI();\n * const zones = await aws.listHostedZones();\n *\n * The whole point: given just an access key + secret (usually from .env), it can\n * *discover* the boring-to-look-up bits of an AWS account (account id, regions,\n * VPCs, subnets, security groups, key pairs, Route53 hosted zones, the latest\n * Ubuntu AMI) so scripts can pre-fill config (e.g. a terraform.tfvars) instead of\n * making you hunt through the console.\n *\n * Credentials + region are resolved via context.params, so they come from CLI,\n * env, or .env transparently:\n * awsAccessKeyId -> AWS_ACCESS_KEY_ID\n * awsSecretAccessKey -> AWS_SECRET_ACCESS_KEY\n * awsSessionToken -> AWS_SESSION_TOKEN (optional, for temp creds)\n * awsRegion -> AWS_REGION (default us-east-1)\n *\n * If no explicit keys are found, the SDK's default credential chain is used\n * (shared config/profile, instance role, etc.) — so AWS_PROFILE also works.\n *\n * Everything here is read-only (Describe / List / Get calls). It never mutates AWS.\n */\n\nimport {\n STSClient,\n GetCallerIdentityCommand,\n} from \"@aws-sdk/client-sts\";\nimport {\n EC2Client,\n DescribeRegionsCommand,\n DescribeAvailabilityZonesCommand,\n DescribeVpcsCommand,\n DescribeSubnetsCommand,\n DescribeSecurityGroupsCommand,\n DescribeKeyPairsCommand,\n DescribeImagesCommand,\n} from \"@aws-sdk/client-ec2\";\nimport {\n Route53Client,\n ListHostedZonesCommand,\n} from \"@aws-sdk/client-route-53\";\n\nconst DEFAULT_REGION = \"us-east-1\";\nconst CANONICAL_OWNER_ID = \"099720109477\"; // Ubuntu images\nconst DEFAULT_UBUNTU_PATTERN = \"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*\";\n\nfunction nameTag(tags) {\n const t = (tags ?? []).find(x => x.Key === \"Name\");\n return t?.Value ?? null;\n}\n\nexport class Aws {\n /**\n * Resolve credentials/region from params and return an Aws instance.\n * Does not call AWS until you invoke a method (clients are lazy).\n */\n static async init(context, options = {}) {\n // Tolerate blank/missing values (e.g. an env var present but empty, like\n // a half-filled .env): treat them as \"not set\" rather than failing param\n // validation, so we fall back cleanly to the SDK default credential chain.\n const getOpt = async (k) => {\n try {\n const v = await context?.params?.get?.(k, \"string\");\n return v != null && String(v).trim() !== \"\" ? v : undefined;\n } catch {\n return undefined;\n }\n };\n\n const config = {\n region: options.region ?? (await getOpt(\"awsRegion\")) ?? DEFAULT_REGION,\n accessKeyId: options.accessKeyId ?? (await getOpt(\"awsAccessKeyId\")),\n secretAccessKey: options.secretAccessKey ?? (await getOpt(\"awsSecretAccessKey\")),\n sessionToken: options.sessionToken ?? (await getOpt(\"awsSessionToken\")),\n };\n\n const aws = new Aws(context, config);\n context?.logger?.debug?.(\n `[aws] region=${config.region} credentials=${config.accessKeyId ? \"explicit\" : \"default-chain\"}`,\n );\n return aws;\n }\n\n constructor(context, config) {\n this.logger = context?.logger ?? console;\n this.region = config.region || DEFAULT_REGION;\n\n // Build an explicit credentials object only when keys were supplied;\n // otherwise leave undefined so the SDK uses its default chain (profile,\n // instance role, env, …).\n this._credentials = config.accessKeyId && config.secretAccessKey\n ? {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n ...(config.sessionToken ? { sessionToken: config.sessionToken } : {}),\n }\n : undefined;\n\n /** True when explicit access key + secret were supplied (env/.env/CLI). */\n this.hasExplicitCredentials = !!this._credentials;\n\n // Quiet the AWS SDK v3 \"node >=22 required after Jan 2027\" notice.\n process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= \"true\";\n\n this._clients = {};\n }\n\n // ── clients (lazy, optionally region-scoped) ──────────────────────────────\n _clientConfig(region) {\n const cfg = { region: region || this.region };\n if (this._credentials) cfg.credentials = this._credentials;\n return cfg;\n }\n\n _sts() {\n return (this._clients.sts ??= new STSClient(this._clientConfig()));\n }\n\n _ec2(region) {\n const key = `ec2:${region || this.region}`;\n return (this._clients[key] ??= new EC2Client(this._clientConfig(region)));\n }\n\n _route53() {\n // Route53 is global; region is irrelevant but the client still wants one.\n return (this._clients.route53 ??= new Route53Client(this._clientConfig()));\n }\n\n getRegion() { return this.region; }\n\n // ── credentials ─────────────────────────────────────────────────────────────\n /**\n * Are any credentials available *before* hitting AWS? Returns\n * { ok, source } — explicit keys, or a resolvable default chain (profile/SSO/\n * instance role). { ok:false } means there's nothing to even try with.\n * Note: \"ok\" only means creds were *found*, not that AWS will accept them.\n */\n async checkCredentials() {\n if (this.hasExplicitCredentials) {\n return { ok: true, source: \"explicit keys (env/.env/CLI)\" };\n }\n try {\n const provider = this._sts().config.credentials;\n const resolved = typeof provider === \"function\" ? await provider() : provider;\n if (resolved?.accessKeyId) {\n return { ok: true, source: \"default credential chain (profile/SSO/role)\" };\n }\n } catch {\n // nothing resolvable\n }\n return { ok: false };\n }\n\n /** True for \"bad/missing credentials\" style errors (vs. real failures). */\n static isAuthError(err) {\n const name = err?.name || err?.Code || err?.__type || \"\";\n return [\n \"CredentialsProviderError\",\n \"InvalidClientTokenId\",\n \"UnrecognizedClientException\",\n \"AuthFailure\",\n \"AccessDenied\",\n \"AccessDeniedException\",\n \"ExpiredToken\",\n \"ExpiredTokenException\",\n \"SignatureDoesNotMatch\",\n \"MissingAuthenticationToken\",\n ].includes(name);\n }\n\n /** Short, precise instructions for getting AWS credentials into .env. */\n static credentialsHelp(region = DEFAULT_REGION) {\n return [\n \"No usable AWS credentials were found (or AWS rejected them).\",\n \"\",\n \"Put a read-only access key in the project's .env:\",\n \"\",\n \" AWS_ACCESS_KEY_ID=AKIA...\",\n \" AWS_SECRET_ACCESS_KEY=...\",\n ` AWS_REGION=${region} # optional (default ${DEFAULT_REGION})`,\n \"\",\n \"Get a key from the AWS console (~2 min):\",\n \" 1. IAM → Users → create or pick a user (console sign-in not needed).\",\n ' 2. Attach a policy — \"ReadOnlyAccess\" (AWS managed) is enough for discovery.',\n ' 3. The user → \"Security credentials\" → \"Create access key\" → \"CLI\".',\n \" 4. Copy the Access key ID + Secret access key (the secret shows only once).\",\n \" 5. Paste both into .env, then re-run.\",\n \" Direct link: https://console.aws.amazon.com/iam/home#/users\",\n \"\",\n \"Prefer a named profile or an EC2 instance role? Re-run with AWS_PROFILE=<name>\",\n \"set (or on the instance) and credentials resolve automatically.\",\n ].join(\"\\n\");\n }\n\n // ── identity ──────────────────────────────────────────────────────────────\n /** { account, arn, userId } — confirm which account/identity the keys belong to. */\n async whoAmI() {\n const out = await this._sts().send(new GetCallerIdentityCommand({}));\n return { account: out.Account, arn: out.Arn, userId: out.UserId };\n }\n\n // ── regions / AZs ──────────────────────────────────────────────────────────\n /** Enabled region names, sorted. */\n async listRegions({ allRegions = false } = {}) {\n const out = await this._ec2().send(new DescribeRegionsCommand({ AllRegions: allRegions }));\n return (out.Regions ?? []).map(r => r.RegionName).sort();\n }\n\n /** Availability zone names for a region (defaults to the instance region). */\n async listAvailabilityZones(region) {\n const out = await this._ec2(region).send(new DescribeAvailabilityZonesCommand({}));\n return (out.AvailabilityZones ?? [])\n .filter(z => z.State === \"available\")\n .map(z => z.ZoneName)\n .sort();\n }\n\n // ── VPC / subnets / SGs / key pairs ────────────────────────────────────────\n /** [{ id, cidr, isDefault, name }] in a region. */\n async listVpcs(region) {\n const out = await this._ec2(region).send(new DescribeVpcsCommand({}));\n return (out.Vpcs ?? []).map(v => ({\n id: v.VpcId,\n cidr: v.CidrBlock,\n isDefault: !!v.IsDefault,\n name: nameTag(v.Tags),\n }));\n }\n\n /** [{ id, vpcId, az, cidr, public, name }]; pass vpcId to scope. */\n async listSubnets({ vpcId, region } = {}) {\n const Filters = vpcId ? [{ Name: \"vpc-id\", Values: [vpcId] }] : undefined;\n const out = await this._ec2(region).send(new DescribeSubnetsCommand({ Filters }));\n return (out.Subnets ?? []).map(s => ({\n id: s.SubnetId,\n vpcId: s.VpcId,\n az: s.AvailabilityZone,\n cidr: s.CidrBlock,\n public: !!s.MapPublicIpOnLaunch,\n name: nameTag(s.Tags),\n }));\n }\n\n /** [{ id, name, vpcId, description }]; pass vpcId to scope. */\n async listSecurityGroups({ vpcId, region } = {}) {\n const Filters = vpcId ? [{ Name: \"vpc-id\", Values: [vpcId] }] : undefined;\n const out = await this._ec2(region).send(new DescribeSecurityGroupsCommand({ Filters }));\n return (out.SecurityGroups ?? []).map(g => ({\n id: g.GroupId,\n name: g.GroupName,\n vpcId: g.VpcId,\n description: g.Description,\n }));\n }\n\n /** [{ name, fingerprint }] EC2 key pairs in a region. */\n async listKeyPairs(region) {\n const out = await this._ec2(region).send(new DescribeKeyPairsCommand({}));\n return (out.KeyPairs ?? []).map(k => ({ name: k.KeyName, fingerprint: k.KeyFingerprint }));\n }\n\n // ── Route53 ────────────────────────────────────────────────────────────────\n /** [{ id, name, private, recordCount }] — id is the bare zone id (no /hostedzone/). */\n async listHostedZones() {\n const zones = [];\n let marker;\n do {\n const out = await this._route53().send(new ListHostedZonesCommand({ Marker: marker }));\n for (const z of out.HostedZones ?? []) {\n zones.push({\n id: (z.Id ?? \"\").replace(\"/hostedzone/\", \"\"),\n name: (z.Name ?? \"\").replace(/\\.$/, \"\"), // strip trailing dot\n private: !!z.Config?.PrivateZone,\n recordCount: z.ResourceRecordSetCount,\n });\n }\n marker = out.IsTruncated ? out.NextMarker : undefined;\n } while (marker);\n return zones;\n }\n\n /**\n * Best hosted zone for a domain: exact match, else the longest zone name that\n * is a suffix of the domain (so \"api.foo.com\" matches the \"foo.com\" zone).\n * Returns the zone object or null.\n */\n async findHostedZoneForDomain(domain) {\n const target = String(domain ?? \"\").replace(/\\.$/, \"\").toLowerCase();\n if (!target) return null;\n const zones = await this.listHostedZones();\n const exact = zones.find(z => z.name.toLowerCase() === target);\n if (exact) return exact;\n return zones\n .filter(z => target.endsWith(`.${z.name.toLowerCase()}`))\n .sort((a, b) => b.name.length - a.name.length)[0] ?? null;\n }\n\n // ── AMI lookup ──────────────────────────────────────────────────────────────\n /**\n * Latest Ubuntu AMI matching a name pattern in a region.\n * Returns { id, name, creationDate, architecture } or null.\n */\n async findLatestUbuntuAmi({ region, pattern = DEFAULT_UBUNTU_PATTERN, architecture = \"x86_64\" } = {}) {\n const out = await this._ec2(region).send(new DescribeImagesCommand({\n Owners: [CANONICAL_OWNER_ID],\n Filters: [\n { Name: \"name\", Values: [pattern] },\n { Name: \"virtualization-type\", Values: [\"hvm\"] },\n { Name: \"state\", Values: [\"available\"] },\n ...(architecture ? [{ Name: \"architecture\", Values: [architecture] }] : []),\n ],\n }));\n const newest = (out.Images ?? [])\n .sort((a, b) => String(b.CreationDate).localeCompare(String(a.CreationDate)))[0];\n if (!newest) return null;\n return {\n id: newest.ImageId,\n name: newest.Name,\n creationDate: newest.CreationDate,\n architecture: newest.Architecture,\n };\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -4173,6 +4173,7 @@ var Aws = class _Aws {
|
|
|
4173
4173
|
secretAccessKey: config2.secretAccessKey,
|
|
4174
4174
|
...config2.sessionToken ? { sessionToken: config2.sessionToken } : {}
|
|
4175
4175
|
} : void 0;
|
|
4176
|
+
this.hasExplicitCredentials = !!this._credentials;
|
|
4176
4177
|
process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED ??= "true";
|
|
4177
4178
|
this._clients = {};
|
|
4178
4179
|
}
|
|
@@ -4195,6 +4196,66 @@ var Aws = class _Aws {
|
|
|
4195
4196
|
getRegion() {
|
|
4196
4197
|
return this.region;
|
|
4197
4198
|
}
|
|
4199
|
+
// ── credentials ─────────────────────────────────────────────────────────────
|
|
4200
|
+
/**
|
|
4201
|
+
* Are any credentials available *before* hitting AWS? Returns
|
|
4202
|
+
* { ok, source } — explicit keys, or a resolvable default chain (profile/SSO/
|
|
4203
|
+
* instance role). { ok:false } means there's nothing to even try with.
|
|
4204
|
+
* Note: "ok" only means creds were *found*, not that AWS will accept them.
|
|
4205
|
+
*/
|
|
4206
|
+
async checkCredentials() {
|
|
4207
|
+
if (this.hasExplicitCredentials) {
|
|
4208
|
+
return { ok: true, source: "explicit keys (env/.env/CLI)" };
|
|
4209
|
+
}
|
|
4210
|
+
try {
|
|
4211
|
+
const provider = this._sts().config.credentials;
|
|
4212
|
+
const resolved = typeof provider === "function" ? await provider() : provider;
|
|
4213
|
+
if (resolved?.accessKeyId) {
|
|
4214
|
+
return { ok: true, source: "default credential chain (profile/SSO/role)" };
|
|
4215
|
+
}
|
|
4216
|
+
} catch {
|
|
4217
|
+
}
|
|
4218
|
+
return { ok: false };
|
|
4219
|
+
}
|
|
4220
|
+
/** True for "bad/missing credentials" style errors (vs. real failures). */
|
|
4221
|
+
static isAuthError(err) {
|
|
4222
|
+
const name = err?.name || err?.Code || err?.__type || "";
|
|
4223
|
+
return [
|
|
4224
|
+
"CredentialsProviderError",
|
|
4225
|
+
"InvalidClientTokenId",
|
|
4226
|
+
"UnrecognizedClientException",
|
|
4227
|
+
"AuthFailure",
|
|
4228
|
+
"AccessDenied",
|
|
4229
|
+
"AccessDeniedException",
|
|
4230
|
+
"ExpiredToken",
|
|
4231
|
+
"ExpiredTokenException",
|
|
4232
|
+
"SignatureDoesNotMatch",
|
|
4233
|
+
"MissingAuthenticationToken"
|
|
4234
|
+
].includes(name);
|
|
4235
|
+
}
|
|
4236
|
+
/** Short, precise instructions for getting AWS credentials into .env. */
|
|
4237
|
+
static credentialsHelp(region = DEFAULT_REGION) {
|
|
4238
|
+
return [
|
|
4239
|
+
"No usable AWS credentials were found (or AWS rejected them).",
|
|
4240
|
+
"",
|
|
4241
|
+
"Put a read-only access key in the project's .env:",
|
|
4242
|
+
"",
|
|
4243
|
+
" AWS_ACCESS_KEY_ID=AKIA...",
|
|
4244
|
+
" AWS_SECRET_ACCESS_KEY=...",
|
|
4245
|
+
` AWS_REGION=${region} # optional (default ${DEFAULT_REGION})`,
|
|
4246
|
+
"",
|
|
4247
|
+
"Get a key from the AWS console (~2 min):",
|
|
4248
|
+
" 1. IAM \u2192 Users \u2192 create or pick a user (console sign-in not needed).",
|
|
4249
|
+
' 2. Attach a policy \u2014 "ReadOnlyAccess" (AWS managed) is enough for discovery.',
|
|
4250
|
+
' 3. The user \u2192 "Security credentials" \u2192 "Create access key" \u2192 "CLI".',
|
|
4251
|
+
" 4. Copy the Access key ID + Secret access key (the secret shows only once).",
|
|
4252
|
+
" 5. Paste both into .env, then re-run.",
|
|
4253
|
+
" Direct link: https://console.aws.amazon.com/iam/home#/users",
|
|
4254
|
+
"",
|
|
4255
|
+
"Prefer a named profile or an EC2 instance role? Re-run with AWS_PROFILE=<name>",
|
|
4256
|
+
"set (or on the instance) and credentials resolve automatically."
|
|
4257
|
+
].join("\n");
|
|
4258
|
+
}
|
|
4198
4259
|
// ── identity ──────────────────────────────────────────────────────────────
|
|
4199
4260
|
/** { account, arn, userId } — confirm which account/identity the keys belong to. */
|
|
4200
4261
|
async whoAmI() {
|