@intentius/chant-lexicon-aws 0.52.2 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -169,6 +169,25 @@ export interface StackDetail {
169
169
  export declare function describeStackDetail(stackName: string, options?: AwsReadClientOptions): Promise<StackDetail>;
170
170
  /** `DescribeStacks` outputs for one stack, as `key → value`. */
171
171
  export declare function describeStackOutputs(stackName: string, options?: AwsReadClientOptions): Promise<Record<string, string>>;
172
+ /** What `sts:GetCallerIdentity` answers with. No credential is in the response. */
173
+ export interface CallerIdentity {
174
+ /** The principal's ARN — an IAM user, or an assumed-role session. */
175
+ arn: string;
176
+ /** The 12-digit account the credentials belong to. */
177
+ account: string;
178
+ }
179
+ /**
180
+ * `sts:GetCallerIdentity` — who these credentials are, per the service itself
181
+ * (#1982). Query protocol like CloudFormation, so it rides the same transport,
182
+ * the same endpoint-override rule and the same signing decision as every other
183
+ * read, which is what keeps `chant lifecycle whoami` and `describeResources`
184
+ * from resolving different targets.
185
+ *
186
+ * The response carries an ARN, an account and a `UserId`. Only the first two
187
+ * are returned: `UserId` adds nothing a principal ARN does not already say,
188
+ * and the narrower the surface, the fewer ways a credential reaches a report.
189
+ */
190
+ export declare function getCallerIdentity(options?: AwsReadClientOptions): Promise<CallerIdentity>;
172
191
  /** One live resource, as Cloud Control describes it. */
173
192
  export interface CloudControlDescription {
174
193
  identifier: string;
@@ -1 +1 @@
1
- {"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Who chant would act as in AWS — the lexicon half of `chant lifecycle whoami`
3
+ * (chant #1982).
4
+ *
5
+ * `sts:GetCallerIdentity` on the same transport every read uses, so the answer
6
+ * cannot describe a target the read does not reach: the endpoint override, the
7
+ * region and the signing decision are resolved by `read-client.ts` once, for
8
+ * both. That parity is the point of the command — a whoami that reports real
9
+ * AWS while `describeResources` reads Floci is worse than none — and a test
10
+ * pins it.
11
+ *
12
+ * ## What the region actually is
13
+ *
14
+ * The native read transport takes its region from the stack being observed
15
+ * (`stacks[].region`, #1261) and falls back to `us-east-1` inside `serviceUrl`.
16
+ * It does NOT read `AWS_REGION`. So that is what is reported, including the
17
+ * awkward case: an operator whose shell exports `AWS_REGION=eu-west-1` and
18
+ * whose project declares no stack region is reading us-east-1, and has had no
19
+ * way to find that out short of a wrong answer. `source` names it.
20
+ *
21
+ * ## Credentials
22
+ *
23
+ * `resolveCredentials` (./api/sigv4.ts) reads `AWS_ACCESS_KEY_ID` /
24
+ * `AWS_SECRET_ACCESS_KEY` and nothing else — no profile file, no IMDS, no
25
+ * container endpoint, deliberately. `AWS_PROFILE` alone therefore signs
26
+ * nothing, and against real AWS the call comes back unauthorized. That is
27
+ * reported as `no-credentials` naming the profile, which is a far better
28
+ * answer than an empty stack read an hour later.
29
+ *
30
+ * No credential value is ever returned. The identity is the principal ARN the
31
+ * service itself answers with, and the scope is an account and a region.
32
+ */
33
+ import type { DescribeIdentityOptions, DescribeIdentityResult } from "@intentius/chant/lexicon";
34
+ import { type AwsReadClientOptions } from "./api/read-client.js";
35
+ /** Options this module needs beyond the contract — the injectable transport, for tests. */
36
+ export interface CallerIdentityOptions extends DescribeIdentityOptions {
37
+ client?: AwsReadClientOptions;
38
+ }
39
+ /**
40
+ * The AWS `describeIdentity`. Returns the STS principal and the account+region
41
+ * scope, or a typed refusal — never a guessed identity and never an empty one.
42
+ */
43
+ export declare function describeIdentity(options: CallerIdentityOptions): Promise<DescribeIdentityResult>;
44
+ //# sourceMappingURL=caller-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caller-identity.d.ts","sourceRoot":"","sources":["../src/caller-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAChG,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAgC3B,2FAA2F;AAC3F,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IACpE,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAmEtG"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "2bc626775d04c8e8b162da0a076ac5efab827834fc0ea83ae1871b82479cf7ec",
4
+ "manifest.json": "704d8f5e7e5f494108a64890a4340747bd01ae7b716c4ac56d2af24f7baa4b17",
5
5
  "meta.json": "49cbb2ca9b454391a7644e14ee9cfd91bcf906c6af683bf46a182aa85047ecc4",
6
6
  "types/index.d.ts": "6d6b37bd14a9b40fb3ff5c66e1cb49be1ae1032de47a4e167cf5f411c943b7f5",
7
7
  "rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
@@ -72,5 +72,5 @@
72
72
  "skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
73
73
  "skills/chant-aws-carve-terraform.md": "0ae4376590db47979be144d3ac6cdcfd56c9640d0536421aacf7330459d38e52"
74
74
  },
75
- "composite": "d3fda61a74692e9473bb1aeeb31723128b926b4c9a0014ba2d399dfab05473bb"
75
+ "composite": "127eb4df938dced05503394592d2e12632ca9209b32de6d7531c6a867dca53fc"
76
76
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws",
3
- "version": "0.52.2",
3
+ "version": "0.53.1",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "AWS",
6
6
  "intrinsics": [
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAoN,MAAM,0BAA0B,CAAC;AAuChR;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aAm/BvB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAqQ,MAAM,0BAA0B,CAAC;AAuCjU;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aA8/BvB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.52.2",
3
+ "version": "0.53.1",
4
4
  "description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -80,7 +80,7 @@
80
80
  "typescript": "^5.9.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@intentius/chant": "^0.52.2",
83
+ "@intentius/chant": "^0.53.1",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -364,6 +364,54 @@ export async function describeStackOutputs(
364
364
  return (await describeStackDetail(stackName, options)).outputs;
365
365
  }
366
366
 
367
+ /* ── STS ──────────────────────────────────────────────────────────────────── */
368
+
369
+ const STS_API_VERSION = "2011-06-15";
370
+
371
+ /** What `sts:GetCallerIdentity` answers with. No credential is in the response. */
372
+ export interface CallerIdentity {
373
+ /** The principal's ARN — an IAM user, or an assumed-role session. */
374
+ arn: string;
375
+ /** The 12-digit account the credentials belong to. */
376
+ account: string;
377
+ }
378
+
379
+ /**
380
+ * `sts:GetCallerIdentity` — who these credentials are, per the service itself
381
+ * (#1982). Query protocol like CloudFormation, so it rides the same transport,
382
+ * the same endpoint-override rule and the same signing decision as every other
383
+ * read, which is what keeps `chant lifecycle whoami` and `describeResources`
384
+ * from resolving different targets.
385
+ *
386
+ * The response carries an ARN, an account and a `UserId`. Only the first two
387
+ * are returned: `UserId` adds nothing a principal ARN does not already say,
388
+ * and the narrower the surface, the fewer ways a credential reaches a report.
389
+ */
390
+ export async function getCallerIdentity(options: AwsReadClientOptions = {}): Promise<CallerIdentity> {
391
+ options = withEndpointOverride("sts", options);
392
+ const http = options.http ?? defaultHttp;
393
+ const url = serviceUrl("sts", options.endpoint, options.region);
394
+ const body = new URLSearchParams({ Action: "GetCallerIdentity", Version: STS_API_VERSION }).toString();
395
+ const res = await http(
396
+ url,
397
+ {
398
+ headers: requestHeaders("sts", url, body, { "content-type": "application/x-www-form-urlencoded" }, options),
399
+ body,
400
+ },
401
+ options.signal,
402
+ );
403
+ const err = xmlError(res.text);
404
+ if (err || res.status >= 400) {
405
+ throw new AwsReadError(
406
+ err?.message ?? `GetCallerIdentity failed with HTTP ${res.status}`,
407
+ res.status,
408
+ err?.code,
409
+ );
410
+ }
411
+ const leaves = xmlLeaves(res.text);
412
+ return { arn: leaves.Arn ?? "", account: leaves.Account ?? "" };
413
+ }
414
+
367
415
  /* ── Cloud Control ────────────────────────────────────────────────────────── */
368
416
 
369
417
  /** One live resource, as Cloud Control describes it. */
@@ -0,0 +1,191 @@
1
+ /**
2
+ * `chant lifecycle whoami` for AWS (chant #1982).
3
+ *
4
+ * The transport is injected everywhere here, so nothing reaches a network and
5
+ * no real credential is involved. The load-bearing test is the last one: the
6
+ * identity query and the declared-entity read must resolve the same target,
7
+ * because a whoami that names a binding the read does not use is worse than no
8
+ * whoami at all.
9
+ */
10
+ import { describe, test, expect } from "vitest";
11
+ import { describeIdentity } from "./caller-identity";
12
+ import { describeStackResources, type AwsReadHttp } from "./api/read-client";
13
+
14
+ const CALLER_IDENTITY_XML = `
15
+ <GetCallerIdentityResponse><GetCallerIdentityResult>
16
+ <Arn>arn:aws:sts::491500000000:assumed-role/deploy/ci</Arn>
17
+ <UserId>AROAEXAMPLEID123456:ci</UserId>
18
+ <Account>491500000000</Account>
19
+ </GetCallerIdentityResult></GetCallerIdentityResponse>`;
20
+
21
+ const STACK_XML = `
22
+ <DescribeStackResourcesResponse><DescribeStackResourcesResult><StackResources>
23
+ <member>
24
+ <LogicalResourceId>bucket</LogicalResourceId>
25
+ <ResourceType>AWS::S3::Bucket</ResourceType>
26
+ </member>
27
+ </StackResources></DescribeStackResourcesResult></DescribeStackResourcesResponse>`;
28
+
29
+ /** Records every request so a test can assert what went on the wire. */
30
+ function recording(text: string, status = 200) {
31
+ const calls: Array<{ url: string; headers: Record<string, string>; body: string }> = [];
32
+ const http: AwsReadHttp = (url, init) => {
33
+ calls.push({ url, headers: init.headers, body: init.body });
34
+ return Promise.resolve({ status, text });
35
+ };
36
+ return { http, calls };
37
+ }
38
+
39
+ /** Credentials that exist only in this record — never read from the process. */
40
+ const CREDS = { AWS_ACCESS_KEY_ID: "AKIAEXAMPLEKEYID0000", AWS_SECRET_ACCESS_KEY: "notarealsecret" };
41
+
42
+ describe("aws describeIdentity (#1982)", () => {
43
+ test("reports the STS principal, the account+region scope, and where the binding came from", async () => {
44
+ const { http, calls } = recording(CALLER_IDENTITY_XML);
45
+ const result = await describeIdentity({
46
+ environment: "prod",
47
+ region: "eu-west-1",
48
+ client: { http, env: CREDS },
49
+ });
50
+
51
+ expect(result).toEqual({
52
+ identity: "arn:aws:sts::491500000000:assumed-role/deploy/ci",
53
+ scope: "491500000000 eu-west-1",
54
+ source: "env AWS_ACCESS_KEY_ID; stacks[].region",
55
+ endpoint: "https://sts.eu-west-1.amazonaws.com/",
56
+ });
57
+ expect(calls).toHaveLength(1);
58
+ expect(calls[0].body).toContain("Action=GetCallerIdentity");
59
+ // The principal id adds nothing an ARN does not already say, and the
60
+ // narrower the surface the fewer ways a credential reaches a report.
61
+ expect(JSON.stringify(result)).not.toContain("AROAEXAMPLEID123456");
62
+ });
63
+
64
+ test("no environment credentials against real AWS is no-credentials, and the call is never made", async () => {
65
+ const { http, calls } = recording(CALLER_IDENTITY_XML);
66
+ const result = await describeIdentity({ environment: "prod", client: { http, env: {} } });
67
+ expect(result).toEqual({
68
+ unresolved: {
69
+ reason: "no-credentials",
70
+ detail: "no AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in the environment",
71
+ },
72
+ });
73
+ expect(calls).toHaveLength(0);
74
+ });
75
+
76
+ test("AWS_PROFILE alone is reported as no-credentials, naming the profile that does not sign", async () => {
77
+ const { http } = recording(CALLER_IDENTITY_XML);
78
+ const result = await describeIdentity({
79
+ environment: "prod",
80
+ client: { http, env: { AWS_PROFILE: "acme-ci" } },
81
+ });
82
+ expect(result).toMatchObject({ unresolved: { reason: "no-credentials" } });
83
+ const detail = (result as { unresolved: { detail: string } }).unresolved.detail;
84
+ expect(detail).toContain("AWS_PROFILE=acme-ci");
85
+ expect(detail).toContain("not the profile file");
86
+ });
87
+
88
+ test("an unsigned emulator read still resolves an identity, and says it was unsigned", async () => {
89
+ const { http, calls } = recording(CALLER_IDENTITY_XML);
90
+ const result = await describeIdentity({
91
+ environment: "floci",
92
+ client: { http, env: { AWS_ENDPOINT_URL: "http://localhost:4566" } },
93
+ });
94
+ expect(result).toMatchObject({
95
+ identity: "arn:aws:sts::491500000000:assumed-role/deploy/ci",
96
+ endpoint: "http://localhost:4566/",
97
+ });
98
+ expect((result as { source: string }).source).toContain("unsigned (endpoint override)");
99
+ expect((result as { source: string }).source).toContain("endpoint override http://localhost:4566");
100
+ expect(calls[0].url).toBe("http://localhost:4566/");
101
+ });
102
+
103
+ test("a refused call is no-credentials; a broken one is read-failed", async () => {
104
+ const denied = recording(
105
+ "<ErrorResponse><Error><Code>InvalidClientTokenId</Code><Message>bad token</Message></Error></ErrorResponse>",
106
+ 403,
107
+ );
108
+ expect(
109
+ await describeIdentity({ environment: "prod", client: { http: denied.http, env: CREDS } }),
110
+ ).toMatchObject({ unresolved: { reason: "no-credentials" } });
111
+
112
+ const broken = recording(
113
+ "<ErrorResponse><Error><Code>ServiceUnavailable</Code><Message>try later</Message></Error></ErrorResponse>",
114
+ 503,
115
+ );
116
+ const result = await describeIdentity({ environment: "prod", client: { http: broken.http, env: CREDS } });
117
+ expect(result).toMatchObject({ unresolved: { reason: "read-failed" } });
118
+ expect((result as { unresolved: { detail: string } }).unresolved.detail).toContain("ServiceUnavailable");
119
+ });
120
+
121
+ test("an answer with no Arn is read-failed, never an empty identity", async () => {
122
+ const { http } = recording("<GetCallerIdentityResponse><GetCallerIdentityResult/></GetCallerIdentityResponse>");
123
+ const result = await describeIdentity({ environment: "prod", client: { http, env: CREDS } });
124
+ expect(result).toMatchObject({ unresolved: { reason: "read-failed" } });
125
+ });
126
+
127
+ test("the region an undeclared project actually reads is reported, not the one AWS_REGION suggests", async () => {
128
+ // The read transport takes its region from `stacks[].region` and defaults
129
+ // to us-east-1; it does not consult AWS_REGION. Reporting the shell's var
130
+ // here would describe a target no read uses.
131
+ const { http, calls } = recording(CALLER_IDENTITY_XML);
132
+ const result = await describeIdentity({
133
+ environment: "prod",
134
+ client: { http, env: { ...CREDS, AWS_REGION: "ap-south-1" } },
135
+ });
136
+ expect((result as { scope: string }).scope).toBe("491500000000 us-east-1");
137
+ expect((result as { source: string }).source).toContain("AWS_REGION=ap-south-1 is not read by this transport");
138
+ expect(calls[0].url).toBe("https://sts.us-east-1.amazonaws.com/");
139
+ });
140
+
141
+ test("no credential material appears anywhere in the reported row", async () => {
142
+ const { http } = recording(CALLER_IDENTITY_XML);
143
+ const result = await describeIdentity({ environment: "prod", client: { http, env: CREDS } });
144
+ const serialized = JSON.stringify(result);
145
+ expect(serialized).not.toContain(CREDS.AWS_SECRET_ACCESS_KEY);
146
+ expect(serialized).not.toContain(CREDS.AWS_ACCESS_KEY_ID);
147
+ });
148
+ });
149
+
150
+ describe("whoami and the live read resolve the same target (#1982 acceptance)", () => {
151
+ test("an endpoint override sends both to the same origin", async () => {
152
+ const env = { ...CREDS, AWS_ENDPOINT_URL: "http://localhost:4566" };
153
+
154
+ const identityHttp = recording(CALLER_IDENTITY_XML);
155
+ const identity = await describeIdentity({
156
+ environment: "floci",
157
+ region: "eu-west-1",
158
+ client: { http: identityHttp.http, env },
159
+ });
160
+
161
+ const readHttp = recording(STACK_XML);
162
+ await describeStackResources("floci", { http: readHttp.http, env, region: "eu-west-1" });
163
+
164
+ const origin = (url: string): string => new URL(url).origin;
165
+ expect(origin(identityHttp.calls[0].url)).toBe(origin(readHttp.calls[0].url));
166
+ // And the row names it, so the operator sees the target rather than
167
+ // inferring it.
168
+ expect((identity as { endpoint: string }).endpoint).toBe(readHttp.calls[0].url);
169
+ });
170
+
171
+ test("against real AWS both resolve the same region, on each service's own host", async () => {
172
+ const identityHttp = recording(CALLER_IDENTITY_XML);
173
+ await describeIdentity({
174
+ environment: "prod",
175
+ region: "eu-west-1",
176
+ client: { http: identityHttp.http, env: CREDS },
177
+ });
178
+
179
+ const readHttp = recording(STACK_XML);
180
+ await describeStackResources("prod", { http: readHttp.http, env: CREDS, region: "eu-west-1" });
181
+
182
+ const host = (url: string): string => new URL(url).host;
183
+ expect(host(identityHttp.calls[0].url)).toBe("sts.eu-west-1.amazonaws.com");
184
+ expect(host(readHttp.calls[0].url)).toBe("cloudformation.eu-west-1.amazonaws.com");
185
+ // The credential scope both signatures carry is the same region — which is
186
+ // the part a wrong answer would get wrong.
187
+ for (const calls of [identityHttp.calls, readHttp.calls]) {
188
+ expect(calls[0].headers.authorization ?? calls[0].headers.Authorization).toContain("/eu-west-1/");
189
+ }
190
+ });
191
+ });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Who chant would act as in AWS — the lexicon half of `chant lifecycle whoami`
3
+ * (chant #1982).
4
+ *
5
+ * `sts:GetCallerIdentity` on the same transport every read uses, so the answer
6
+ * cannot describe a target the read does not reach: the endpoint override, the
7
+ * region and the signing decision are resolved by `read-client.ts` once, for
8
+ * both. That parity is the point of the command — a whoami that reports real
9
+ * AWS while `describeResources` reads Floci is worse than none — and a test
10
+ * pins it.
11
+ *
12
+ * ## What the region actually is
13
+ *
14
+ * The native read transport takes its region from the stack being observed
15
+ * (`stacks[].region`, #1261) and falls back to `us-east-1` inside `serviceUrl`.
16
+ * It does NOT read `AWS_REGION`. So that is what is reported, including the
17
+ * awkward case: an operator whose shell exports `AWS_REGION=eu-west-1` and
18
+ * whose project declares no stack region is reading us-east-1, and has had no
19
+ * way to find that out short of a wrong answer. `source` names it.
20
+ *
21
+ * ## Credentials
22
+ *
23
+ * `resolveCredentials` (./api/sigv4.ts) reads `AWS_ACCESS_KEY_ID` /
24
+ * `AWS_SECRET_ACCESS_KEY` and nothing else — no profile file, no IMDS, no
25
+ * container endpoint, deliberately. `AWS_PROFILE` alone therefore signs
26
+ * nothing, and against real AWS the call comes back unauthorized. That is
27
+ * reported as `no-credentials` naming the profile, which is a far better
28
+ * answer than an empty stack read an hour later.
29
+ *
30
+ * No credential value is ever returned. The identity is the principal ARN the
31
+ * service itself answers with, and the scope is an account and a region.
32
+ */
33
+
34
+ import type { DescribeIdentityOptions, DescribeIdentityResult } from "@intentius/chant/lexicon";
35
+ import {
36
+ AwsReadError,
37
+ getCallerIdentity,
38
+ resolveEndpointOverride,
39
+ serviceUrl,
40
+ type AwsReadClientOptions,
41
+ } from "./api/read-client";
42
+ import { resolveCredentials } from "./api/sigv4";
43
+
44
+ /** The region the read transport falls back to when no stack declares one. */
45
+ const DEFAULT_REGION = "us-east-1";
46
+
47
+ /** True when the failure says the call was refused rather than broken. */
48
+ function isAuthFailure(err: AwsReadError): boolean {
49
+ if (err.status === 401 || err.status === 403) return true;
50
+ return /credential|token|expired|AccessDenied|not authorized|Unauthorized|InvalidClientTokenId|SignatureDoesNotMatch/i.test(
51
+ `${err.code ?? ""} ${err.message}`,
52
+ );
53
+ }
54
+
55
+ /** Where the region came from, said plainly enough to act on. */
56
+ function regionSource(declared: string | undefined, env: Record<string, string | undefined>): string {
57
+ if (declared) return `stacks[].region`;
58
+ const ambient = env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
59
+ if (ambient && ambient !== DEFAULT_REGION) {
60
+ return `chant default (no stacks[].region; AWS_REGION=${ambient} is not read by this transport)`;
61
+ }
62
+ return "chant default (no stacks[].region)";
63
+ }
64
+
65
+ /** Where the signature came from, without any part of the credential itself. */
66
+ function credentialSource(env: Record<string, string | undefined>): string | undefined {
67
+ if (!resolveCredentials(undefined, env)) return undefined;
68
+ return env.AWS_SESSION_TOKEN
69
+ ? "env AWS_ACCESS_KEY_ID + AWS_SESSION_TOKEN"
70
+ : "env AWS_ACCESS_KEY_ID";
71
+ }
72
+
73
+ /** Options this module needs beyond the contract — the injectable transport, for tests. */
74
+ export interface CallerIdentityOptions extends DescribeIdentityOptions {
75
+ client?: AwsReadClientOptions;
76
+ }
77
+
78
+ /**
79
+ * The AWS `describeIdentity`. Returns the STS principal and the account+region
80
+ * scope, or a typed refusal — never a guessed identity and never an empty one.
81
+ */
82
+ export async function describeIdentity(options: CallerIdentityOptions): Promise<DescribeIdentityResult> {
83
+ const client: AwsReadClientOptions = {
84
+ ...options.client,
85
+ ...(options.region ? { region: options.region } : {}),
86
+ };
87
+ const env = client.env ?? process.env;
88
+ const override = resolveEndpointOverride("sts", client.endpoint, env);
89
+ // The region the transport resolves, not the one an operator might assume:
90
+ // `serviceUrl` defaults it, so this is the host the read is actually sent to.
91
+ const region = client.region ?? DEFAULT_REGION;
92
+ const endpoint = serviceUrl("sts", override, client.region);
93
+
94
+ const credential = credentialSource(env);
95
+ // Against an emulator the call is unsigned by design (Floci verifies no
96
+ // signature), so an absent credential is not yet a refusal — the emulator
97
+ // answers with the identity it models. Against real AWS it is.
98
+ if (!credential && !override) {
99
+ return {
100
+ unresolved: {
101
+ reason: "no-credentials",
102
+ detail: env.AWS_PROFILE
103
+ ? `no AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in the environment; AWS_PROFILE=${env.AWS_PROFILE} is set, but chant's native transport reads only the environment credentials, not the profile file`
104
+ : "no AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in the environment",
105
+ },
106
+ };
107
+ }
108
+
109
+ let identity;
110
+ try {
111
+ identity = await getCallerIdentity(client);
112
+ } catch (err) {
113
+ if (err instanceof AwsReadError) {
114
+ return {
115
+ unresolved: {
116
+ reason: isAuthFailure(err) ? "no-credentials" : "read-failed",
117
+ detail: `sts:GetCallerIdentity against ${endpoint} failed: ${err.code ? `${err.code}: ` : ""}${err.message}`,
118
+ },
119
+ };
120
+ }
121
+ return {
122
+ unresolved: {
123
+ reason: "read-failed",
124
+ detail: `sts:GetCallerIdentity against ${endpoint} failed: ${err instanceof Error ? err.message : String(err)}`,
125
+ },
126
+ };
127
+ }
128
+
129
+ if (!identity.arn) {
130
+ return {
131
+ unresolved: {
132
+ reason: "read-failed",
133
+ detail: `sts:GetCallerIdentity against ${endpoint} answered without an Arn`,
134
+ },
135
+ };
136
+ }
137
+
138
+ const sources = [
139
+ credential ?? "unsigned (endpoint override)",
140
+ regionSource(client.region, env),
141
+ ...(override ? [`endpoint override ${override}`] : []),
142
+ ];
143
+ return {
144
+ identity: identity.arn,
145
+ scope: identity.account ? `${identity.account} ${region}` : region,
146
+ source: sources.join("; "),
147
+ endpoint,
148
+ };
149
+ }
package/src/plugin.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "module";
2
2
  import { detectTemplate } from "./detect";
3
- import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, DependencyObservation, DisruptionQuery, DisruptionVerdict, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
3
+ import type { LexiconPlugin, IntrinsicDef, ObservationResult, DeepObservationResult, DependencyObservation, DescribeIdentityOptions, DescribeIdentityResult, DisruptionQuery, DisruptionVerdict, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
4
4
  const require = createRequire(import.meta.url);
5
5
  import type { LintRule } from "@intentius/chant/lint/rule";
6
6
  import type { TemplateParser } from "@intentius/chant/import/parser";
@@ -800,6 +800,17 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
800
800
  return awsDisruption(options);
801
801
  },
802
802
 
803
+ /**
804
+ * Who chant would act as here, before it acts (#1982) —
805
+ * `sts:GetCallerIdentity` on the same transport, endpoint override and
806
+ * region resolution `describeResources` uses, so the principal reported and
807
+ * the account read are the same one. Implementation in ./caller-identity.ts.
808
+ */
809
+ async describeIdentity(options: DescribeIdentityOptions): Promise<DescribeIdentityResult> {
810
+ const { describeIdentity } = await import("./caller-identity");
811
+ return describeIdentity(options);
812
+ },
813
+
803
814
  /**
804
815
  * Resources of a managed kind that exist without being declared or
805
816
  * referenced (#1278) — the account's default security groups, an unattached