@intentius/chant-lexicon-aws 0.44.8 → 0.44.10

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.
@@ -19,12 +19,22 @@
19
19
  *
20
20
  * ## Signing
21
21
  *
22
- * Requests are unsigned, exactly like `awsApply`'s. That is what makes the
23
- * emulator-first lanes work with no credential plumbing, and it is the reason
24
- * this is scoped to the Floci lanes (#1198) rather than announced as a
25
- * real-cloud read path: real AWS rejects an unsigned request. The CLI path
26
- * remains available for a signed read until SigV4 lands here.
22
+ * Requests are signed with SigV4 (#1686) when credentials resolve see
23
+ * `./sigv4.ts`, which holds the implementation so that cedar's AVP client can
24
+ * adopt the same one rather than growing a second. Two cases stay unsigned, and
25
+ * both are deliberate:
26
+ *
27
+ * - **No credentials.** The caller's existing no-credentials path is
28
+ * untouched: the request still goes out carrying only the credential scope
29
+ * of {@link regionScope}, which is what the emulator lanes have always
30
+ * sent.
31
+ * - **An endpoint override.** Floci does not verify signatures, and signing
32
+ * against it would mean every local lane suddenly needs credentials to read
33
+ * what it just deployed. `signEndpointOverride` opts back in for an
34
+ * override that *is* real AWS — a VPC endpoint, a signing proxy.
27
35
  */
36
+ import { type AwsCredentialSource } from "./sigv4.js";
37
+ export type { AwsCredentials, AwsCredentialResolver, AwsCredentialSource } from "./sigv4.js";
28
38
  /** Injectable HTTP, mirroring `AwsHttp` in the applier so tests avoid the network. */
29
39
  export type AwsReadHttp = (url: string, init: {
30
40
  headers: Record<string, string>;
@@ -49,6 +59,18 @@ export interface AwsReadClientOptions {
49
59
  region?: string;
50
60
  http?: AwsReadHttp;
51
61
  signal?: AbortSignal;
62
+ /**
63
+ * What to sign with: literal credentials, or a resolver that decides. Omitted,
64
+ * the environment answers; when it has nothing, the request goes out unsigned
65
+ * exactly as it did before signing existed.
66
+ */
67
+ credentials?: AwsCredentialSource;
68
+ /** Environment the credential fallback reads. Defaults to `process.env`; injectable for tests. */
69
+ env?: Record<string, string | undefined>;
70
+ /** Sign even against an endpoint override — for an override that is real AWS. */
71
+ signEndpointOverride?: boolean;
72
+ /** Signing clock. Injected by tests so a signature is reproducible. */
73
+ now?: Date;
52
74
  }
53
75
  /** Service host for `service`, honouring an endpoint override. */
54
76
  export declare function serviceUrl(service: string, endpoint?: string, region?: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,sFAAsF;AACtF,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACvD,MAAM,CAAC,EAAE,WAAW,KACjB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAO/C,2EAA2E;AAC3E,qBAAa,YAAa,SAAQ,KAAK;IAGnC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,gGAAgG;IAChG,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM;gBAHtB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM;IACvB,gGAAgG;IACvF,IAAI,CAAC,EAAE,MAAM,YAAA;CAKzB;AAED,MAAM,WAAW,oBAAoB;IACnC,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,SAAiB,GAAG,MAAM,CAE9F;AAmCD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAOlE;AAYD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAItF;AAED,0FAA0F;AAC1F,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAIrF;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wEAAwE;AACxE,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,EAAE,CAAC,CAS1B;AAED,gEAAgE;AAChE,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAOjC;AAID,wDAAwD;AACxD,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AA6CD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,uBAAuB,GAAG,IAAI,CAQrF;AAWD,8DAA8D;AAC9D,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAGzC;AAED,kFAAkF;AAClF,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAiBpC"}
1
+ {"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /** SHA-256 of the empty string — the payload hash of every bodyless request. */
2
+ export declare const EMPTY_PAYLOAD_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
3
+ /** A resolved credential set. `sessionToken` is present for STS/role credentials. */
4
+ export interface AwsCredentials {
5
+ accessKeyId: string;
6
+ secretAccessKey: string;
7
+ sessionToken?: string;
8
+ }
9
+ /**
10
+ * The credential injection seam: a function the caller supplies to decide, by
11
+ * whatever means it likes, what to sign with. Returning `undefined` means "this
12
+ * process has no credentials" and is respected as an answer, not overridden.
13
+ */
14
+ export type AwsCredentialResolver = () => AwsCredentials | undefined;
15
+ /** Either literal credentials or a resolver for them. */
16
+ export type AwsCredentialSource = AwsCredentials | AwsCredentialResolver;
17
+ /**
18
+ * Credentials for a request: explicit → environment → absent.
19
+ *
20
+ * A resolver function is authoritative — if one is injected and it declines,
21
+ * the environment is not consulted behind its back, because the point of
22
+ * injecting one is to control the answer. Literal credentials are likewise
23
+ * final. Only the no-source case falls through to `AWS_ACCESS_KEY_ID` /
24
+ * `AWS_SECRET_ACCESS_KEY` (plus `AWS_SESSION_TOKEN` when set), and a half-set
25
+ * environment — a key id with no secret — is absent rather than a signature
26
+ * that cannot verify.
27
+ *
28
+ * Deliberately not implemented: the profile file, IMDS, and the container
29
+ * credential endpoints. Each is a separate transport with its own failure and
30
+ * caching story; a caller that has those can resolve them itself and pass the
31
+ * result in, which is what the resolver seam is for.
32
+ */
33
+ export declare function resolveCredentials(source?: AwsCredentialSource, env?: Record<string, string | undefined>): AwsCredentials | undefined;
34
+ /** One request to sign. `headers` are the caller's; the signer adds its own. */
35
+ export interface SigV4Request {
36
+ method: string;
37
+ /** Absolute URL. Its host is signed and its path/query are canonicalized. */
38
+ url: string;
39
+ headers: Record<string, string>;
40
+ body: string;
41
+ /** Service name as it appears in the credential scope (`cloudformation`, `cloudcontrolapi`, …). */
42
+ service: string;
43
+ region: string;
44
+ credentials: AwsCredentials;
45
+ /** Signing clock. Injected by tests; otherwise now. */
46
+ now?: Date;
47
+ }
48
+ /** `YYYYMMDDTHHMMSSZ` — the `X-Amz-Date` format, which is ISO-8601 basic. */
49
+ export declare function amzDate(date: Date): string;
50
+ /** Lowercase hex SHA-256. */
51
+ export declare function sha256Hex(payload: string): string;
52
+ /**
53
+ * The signing key: four chained HMACs from the secret, so the key on the wire
54
+ * is scoped to one day, one region and one service rather than being the
55
+ * account secret itself.
56
+ */
57
+ export declare function signingKey(secretAccessKey: string, day: string, region: string, service: string): Buffer;
58
+ /**
59
+ * Header names lowercased, values trimmed with internal whitespace runs
60
+ * collapsed, sorted by name. Returns the canonical block and the `;`-joined
61
+ * signed-header list that has to agree with it exactly.
62
+ */
63
+ export declare function canonicalHeaders(headers: Record<string, string>): {
64
+ canonical: string;
65
+ signed: string;
66
+ };
67
+ /** The canonical request, verbatim as it is hashed into the string to sign. */
68
+ export declare function canonicalRequest(method: string, url: URL, headers: Record<string, string>, payloadHash: string): {
69
+ canonical: string;
70
+ signed: string;
71
+ };
72
+ /** The string to sign: algorithm, timestamp, scope, hashed canonical request. */
73
+ export declare function stringToSign(timestamp: string, scope: string, canonical: string): string;
74
+ /**
75
+ * The caller's headers plus everything SigV4 adds: `x-amz-date`,
76
+ * `x-amz-content-sha256`, `x-amz-security-token` when the credentials are
77
+ * temporary, and the `Authorization` header itself.
78
+ *
79
+ * `host` is signed (AWS requires it) but not returned — see the module header.
80
+ */
81
+ export declare function signRequest(request: SigV4Request): Record<string, string>;
82
+ //# sourceMappingURL=sigv4.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.d.ts","sourceRoot":"","sources":["../../src/api/sigv4.ts"],"names":[],"mappings":"AAkCA,gFAAgF;AAChF,eAAO,MAAM,oBAAoB,qEAAqE,CAAC;AASvG,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,cAAc,GAAG,SAAS,CAAC;AAErE,yDAAyD;AACzD,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG,qBAAqB,CAAC;AAEzE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,CAAC,EAAE,mBAAmB,EAC5B,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,cAAc,GAAG,SAAS,CAW5B;AAED,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,mGAAmG;IACnG,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,cAAc,CAAC;IAC5B,uDAAuD;IACvD,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,6EAA6E;AAC7E,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAE1C;AAED,6BAA6B;AAC7B,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAMD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAKxG;AAmCD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CASvG;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,WAAW,EAAE,MAAM,GAClB;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAavC;AAED,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAExF;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA0BzE"}
@@ -0,0 +1,23 @@
1
+ import { type AwsReadClientOptions } from "./api/read-client.js";
2
+ import type { ResourceMetadata } from "@intentius/chant/lexicon";
3
+ /**
4
+ * The Cloud Control identifier the declared props spell, or undefined when the
5
+ * type has no primary identifier on record or any part of it is absent or
6
+ * non-scalar (a Ref, an intrinsic, a server-assigned name). Multi-part
7
+ * identifiers join with `|`, Cloud Control's own separator.
8
+ */
9
+ export declare function declaredIdentifier(entityType: string, props: Record<string, unknown>): string | undefined;
10
+ /**
11
+ * Identity-read every entity the stack did not answer for and whose props
12
+ * spell an identifier. `already` is the stack's answer — an entity the stack
13
+ * DID return is never re-read. The `queried` map records the address each
14
+ * attempted read was issued against (#1620), whatever the verdict.
15
+ */
16
+ export declare function observeByIdentity(entityNames: string[], entities: Map<string, {
17
+ entityType: string;
18
+ props: Record<string, unknown>;
19
+ }> | undefined, already: Record<string, ResourceMetadata>, client: AwsReadClientOptions): Promise<{
20
+ resources: Record<string, ResourceMetadata>;
21
+ queried: Record<string, string>;
22
+ }>;
23
+ //# sourceMappingURL=identity-observe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-observe.d.ts","sourceRoot":"","sources":["../src/identity-observe.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAe,KAAK,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAmBjE;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAazG;AAWD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EAAE,EACrB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAAG,SAAS,EACzF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,CA4B3F"}
package/dist/index.d.ts CHANGED
@@ -14,7 +14,8 @@ export { output, isLexiconOutput } from "@intentius/chant/lexicon-output";
14
14
  export type { LexiconOutput } from "@intentius/chant/lexicon-output";
15
15
  export { awsPlugin } from "./plugin.js";
16
16
  export { observeResourcesDeepAws, awsDeepNormalizationHooks, hasOwnershipMarker, DEEP_READABLE_TYPES, AWS_READ_ONLY_NAMES, AWS_SERVICE_DEFAULTS, } from "./deep-observe.js";
17
- export { describeStackResources, describeStackOutputs, getResource, listResources, parseResourceDescription, AwsReadError, type AwsReadHttp, type AwsReadClientOptions, type CloudControlDescription, type StackResource, } from "./api/read-client.js";
17
+ export { describeStackResources, describeStackOutputs, getResource, listResources, parseResourceDescription, AwsReadError, type AwsReadHttp, type AwsReadClientOptions, type CloudControlDescription, type StackResource, type AwsCredentials, type AwsCredentialResolver, type AwsCredentialSource, } from "./api/read-client.js";
18
+ export { signRequest, resolveCredentials, EMPTY_PAYLOAD_SHA256, type SigV4Request, } from "./api/sigv4.js";
18
19
  export { Sub, Ref, GetAtt, If, Join, Select, Split, Base64, GetAZs, SubIntrinsic, RefIntrinsic, GetAttIntrinsic, IfIntrinsic, JoinIntrinsic, SelectIntrinsic, SplitIntrinsic, Base64Intrinsic, GetAZsIntrinsic, } from "./intrinsics.js";
19
20
  export { AWS, StackName, Region, AccountId, StackId, URLSuffix, NoValue, NotificationARNs, Partition, } from "./pseudo.js";
20
21
  export * from "./generated/index.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "73481436310597fa252541226cf4d8edfa395deb2df9218b7dd62452b79e830a",
4
+ "manifest.json": "ba1ea326a8ff78358dba97679b751f9abf5a923977cb6cc52e759e19cae16b2f",
5
5
  "meta.json": "2e4cae67cbc2eb2962c3dd0aa57008ae96fb725dd0118af7b04b2b44eab7722a",
6
6
  "types/index.d.ts": "6d6b37bd14a9b40fb3ff5c66e1cb49be1ae1032de47a4e167cf5f411c943b7f5",
7
7
  "rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
@@ -62,5 +62,5 @@
62
62
  "skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
63
63
  "skills/chant-aws-carve-terraform.md": "52dc7a8a10156d236266cc8dbf75614fabb6e2fa4caf31c13cca348e167a58b5"
64
64
  },
65
- "composite": "f13bd5fcaa776bd6d798093538c2497aa1a6d44bff8f94e235b4610a0b22e925"
65
+ "composite": "491effd79e8556535ecfaf370d32b01a5aec7540a31a03d6c2b268ec6bf81664"
66
66
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws",
3
- "version": "0.44.8",
3
+ "version": "0.44.10",
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,EAAgL,MAAM,0BAA0B,CAAC;AAqC5O;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAWnD,eAAO,MAAM,SAAS,EAAE,aA05BvB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAgL,MAAM,0BAA0B,CAAC;AAqC5O;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAWnD,eAAO,MAAM,SAAS,EAAE,aAw6BvB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.44.8",
3
+ "version": "0.44.10",
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.44.8",
83
+ "@intentius/chant": "^0.44.10",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -19,6 +19,7 @@ import {
19
19
  xmlError,
20
20
  type AwsReadHttp,
21
21
  } from "./read-client";
22
+ import { EMPTY_PAYLOAD_SHA256 } from "./sigv4";
22
23
 
23
24
  const respond = (text: string, status = 200): ReturnType<AwsReadHttp> => Promise.resolve({ status, text });
24
25
 
@@ -210,25 +211,107 @@ describe("the region an endpoint override cannot carry in its host", () => {
210
211
  // snapshot recorded that region as holding nothing.
211
212
  test("the request names the region in its credential scope", async () => {
212
213
  const { http, calls } = recording(() => respond(stackXml));
213
- await describeStackResources("web", { endpoint: "http://localhost:4566", region: "us-west-1", http });
214
+ await describeStackResources("web", { endpoint: "http://localhost:4566", region: "us-west-1", http, env: {} });
214
215
  expect(calls[0].headers.authorization).toContain("/us-west-1/cloudformation/aws4_request");
215
216
  });
216
217
 
217
218
  test("Cloud Control carries it too, scoped to its own service", async () => {
218
219
  const { http, calls } = recording(() => respond(JSON.stringify({ ResourceDescription: {} })));
219
- await getResource("AWS::EC2::VPC", "vpc-01", { endpoint: "http://localhost:4566", region: "eu-west-1", http });
220
+ await getResource("AWS::EC2::VPC", "vpc-01", { endpoint: "http://localhost:4566", region: "eu-west-1", http, env: {} });
220
221
  expect(calls[0].headers.authorization).toContain("/eu-west-1/cloudcontrolapi/aws4_request");
221
222
  });
222
223
 
223
224
  test("no region, no header — nothing invents one", async () => {
224
225
  const { http, calls } = recording(() => respond(stackXml));
225
- await describeStackResources("web", { endpoint: "http://localhost:4566", http });
226
+ await describeStackResources("web", { endpoint: "http://localhost:4566", http, env: {} });
226
227
  expect(calls[0].headers.authorization).toBeUndefined();
227
228
  });
228
229
 
229
230
  test("it is a scope, not a signature — the placeholder says so", async () => {
230
231
  const { http, calls } = recording(() => respond(stackXml));
231
- await describeStackResources("web", { region: "us-west-1", http });
232
+ await describeStackResources("web", { region: "us-west-1", http, env: {} });
232
233
  expect(calls[0].headers.authorization).toContain("Signature=unsigned");
233
234
  });
234
235
  });
236
+
237
+ /**
238
+ * The signing decision, as the client makes it. `./sigv4.test.ts` proves the
239
+ * signature is correct against AWS's own vectors; these prove the client only
240
+ * produces one when it should.
241
+ */
242
+ describe("SigV4 on the read path", () => {
243
+ const credentials = { accessKeyId: "AKIDEXAMPLE", secretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" };
244
+ const now = new Date("2015-08-30T12:36:00Z");
245
+
246
+ test("credentials and a real host produce a signed request, not the placeholder", async () => {
247
+ const { http, calls } = recording(() => respond(stackXml));
248
+ await describeStackResources("web", { region: "us-west-1", credentials, now, http, env: {} });
249
+
250
+ const auth = calls[0].headers.authorization;
251
+ expect(auth).toContain("Credential=AKIDEXAMPLE/20150830/us-west-1/cloudformation/aws4_request");
252
+ expect(auth).not.toContain("Signature=unsigned");
253
+ expect(auth).toMatch(/Signature=[0-9a-f]{64}$/);
254
+ expect(calls[0].headers["x-amz-date"]).toBe("20150830T123600Z");
255
+ // The body hash, not the empty-payload constant — the Query call has a body.
256
+ expect(calls[0].headers["x-amz-content-sha256"]).toMatch(/^[0-9a-f]{64}$/);
257
+ expect(calls[0].headers["x-amz-content-sha256"]).not.toBe(EMPTY_PAYLOAD_SHA256);
258
+ // `host` is signed but never emitted: fetch computes it and forbids the override.
259
+ expect(calls[0].headers.host).toBeUndefined();
260
+ expect(auth).toContain("SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date");
261
+ });
262
+
263
+ test("an endpoint override is not signed, so the emulator lanes need no credentials", async () => {
264
+ const { http, calls } = recording(() => respond(stackXml));
265
+ await describeStackResources("web", {
266
+ endpoint: "http://localhost:4566",
267
+ region: "us-west-1",
268
+ credentials,
269
+ http,
270
+ env: {},
271
+ });
272
+ expect(calls[0].headers.authorization).toContain("Signature=unsigned");
273
+ expect(calls[0].headers["x-amz-date"]).toBeUndefined();
274
+ });
275
+
276
+ test("signEndpointOverride signs one anyway — for an override that is real AWS", async () => {
277
+ const { http, calls } = recording(() => respond(stackXml));
278
+ await describeStackResources("web", {
279
+ endpoint: "https://vpce-1234.cloudformation.us-west-1.vpce.amazonaws.com",
280
+ region: "us-west-1",
281
+ credentials,
282
+ signEndpointOverride: true,
283
+ now,
284
+ http,
285
+ env: {},
286
+ });
287
+ expect(calls[0].headers.authorization).toMatch(/Signature=[0-9a-f]{64}$/);
288
+ });
289
+
290
+ test("no credentials leaves the old unsigned path exactly as it was", async () => {
291
+ const { http, calls } = recording(() => respond(stackXml));
292
+ await describeStackResources("web", { region: "us-west-1", http, env: {} });
293
+ expect(calls[0].headers.authorization).toContain("Signature=unsigned");
294
+ expect(calls[0].headers["x-amz-date"]).toBeUndefined();
295
+ expect(calls[0].headers["x-amz-content-sha256"]).toBeUndefined();
296
+ });
297
+
298
+ test("the environment is the fallback source, and Cloud Control signs from it too", async () => {
299
+ const { http, calls } = recording(() => respond(JSON.stringify({ ResourceDescription: {} })));
300
+ await getResource("AWS::EC2::VPC", "vpc-01", {
301
+ region: "eu-west-1",
302
+ now,
303
+ http,
304
+ env: { AWS_ACCESS_KEY_ID: "AKIDENV", AWS_SECRET_ACCESS_KEY: "s", AWS_SESSION_TOKEN: "tok" },
305
+ });
306
+ expect(calls[0].headers.authorization).toContain("Credential=AKIDENV/20150830/eu-west-1/cloudcontrolapi/");
307
+ expect(calls[0].headers["x-amz-security-token"]).toBe("tok");
308
+ expect(calls[0].headers["x-amz-target"]).toBe("CloudApiService.GetResource");
309
+ });
310
+
311
+ test("signing without a named region falls back to the same default the host does", async () => {
312
+ const { http, calls } = recording(() => respond(stackXml));
313
+ await describeStackResources("web", { credentials, now, http, env: {} });
314
+ expect(calls[0].url).toBe("https://cloudformation.us-east-1.amazonaws.com/");
315
+ expect(calls[0].headers.authorization).toContain("/20150830/us-east-1/cloudformation/aws4_request");
316
+ });
317
+ });
@@ -19,12 +19,23 @@
19
19
  *
20
20
  * ## Signing
21
21
  *
22
- * Requests are unsigned, exactly like `awsApply`'s. That is what makes the
23
- * emulator-first lanes work with no credential plumbing, and it is the reason
24
- * this is scoped to the Floci lanes (#1198) rather than announced as a
25
- * real-cloud read path: real AWS rejects an unsigned request. The CLI path
26
- * remains available for a signed read until SigV4 lands here.
22
+ * Requests are signed with SigV4 (#1686) when credentials resolve see
23
+ * `./sigv4.ts`, which holds the implementation so that cedar's AVP client can
24
+ * adopt the same one rather than growing a second. Two cases stay unsigned, and
25
+ * both are deliberate:
26
+ *
27
+ * - **No credentials.** The caller's existing no-credentials path is
28
+ * untouched: the request still goes out carrying only the credential scope
29
+ * of {@link regionScope}, which is what the emulator lanes have always
30
+ * sent.
31
+ * - **An endpoint override.** Floci does not verify signatures, and signing
32
+ * against it would mean every local lane suddenly needs credentials to read
33
+ * what it just deployed. `signEndpointOverride` opts back in for an
34
+ * override that *is* real AWS — a VPC endpoint, a signing proxy.
27
35
  */
36
+ import { resolveCredentials, signRequest, type AwsCredentialSource } from "./sigv4";
37
+
38
+ export type { AwsCredentials, AwsCredentialResolver, AwsCredentialSource } from "./sigv4";
28
39
 
29
40
  const DEFAULT_REGION = "us-east-1";
30
41
  const CFN_API_VERSION = "2010-05-15";
@@ -62,6 +73,18 @@ export interface AwsReadClientOptions {
62
73
  region?: string;
63
74
  http?: AwsReadHttp;
64
75
  signal?: AbortSignal;
76
+ /**
77
+ * What to sign with: literal credentials, or a resolver that decides. Omitted,
78
+ * the environment answers; when it has nothing, the request goes out unsigned
79
+ * exactly as it did before signing existed.
80
+ */
81
+ credentials?: AwsCredentialSource;
82
+ /** Environment the credential fallback reads. Defaults to `process.env`; injectable for tests. */
83
+ env?: Record<string, string | undefined>;
84
+ /** Sign even against an endpoint override — for an override that is real AWS. */
85
+ signEndpointOverride?: boolean;
86
+ /** Signing clock. Injected by tests so a signature is reproducible. */
87
+ now?: Date;
65
88
  }
66
89
 
67
90
  /** Service host for `service`, honouring an endpoint override. */
@@ -84,15 +107,19 @@ export function serviceUrl(service: string, endpoint?: string, region = DEFAULT_
84
107
  * an empty observation and the snapshot recorded the region as holding nothing.
85
108
  * A three-region estate snapshotted as one region and nothing said so.
86
109
  *
87
- * This is NOT SigV4. The signature is a placeholder and real AWS rejects it
88
- * as it already rejects every request from this module, which is unsigned by
89
- * design (see the header comment). It carries the scope, nothing more, and it
90
- * must not be mistaken for the signed read path that would replace it.
110
+ * This is NOT SigV4. The signature is a placeholder and real AWS rejects it. It
111
+ * is what an unsigned request still has to carry so an emulator learns the
112
+ * region, and it is only ever sent when no credentials resolved the moment
113
+ * they do, {@link requestHeaders} sends a real signature instead.
91
114
  */
92
- function regionScope(service: string, region?: string): Record<string, string> {
115
+ function regionScope(
116
+ service: string,
117
+ region: string | undefined,
118
+ env: Record<string, string | undefined>,
119
+ ): Record<string, string> {
93
120
  if (!region) return {};
94
121
  const day = new Date().toISOString().slice(0, 10).replace(/-/g, "");
95
- const key = process.env.AWS_ACCESS_KEY_ID || "chant";
122
+ const key = env.AWS_ACCESS_KEY_ID || "chant";
96
123
  return {
97
124
  authorization:
98
125
  `AWS4-HMAC-SHA256 Credential=${key}/${day}/${region}/${service}/aws4_request, ` +
@@ -100,6 +127,38 @@ function regionScope(service: string, region?: string): Record<string, string> {
100
127
  };
101
128
  }
102
129
 
130
+ /**
131
+ * The headers one request goes out with — signed when there is something to
132
+ * sign with and the target is real AWS, scope-only otherwise.
133
+ *
134
+ * Signing needs a region even when the caller named none, because the scope
135
+ * string has a slot for one; it borrows the same `us-east-1` default that
136
+ * {@link serviceUrl} already used to build the host, so the signature agrees
137
+ * with the endpoint it is sent to.
138
+ */
139
+ function requestHeaders(
140
+ service: string,
141
+ url: string,
142
+ body: string,
143
+ base: Record<string, string>,
144
+ options: AwsReadClientOptions,
145
+ ): Record<string, string> {
146
+ const env = options.env ?? process.env;
147
+ const credentials = resolveCredentials(options.credentials, env);
148
+ const signable = credentials && (!options.endpoint || options.signEndpointOverride === true);
149
+ if (!signable) return { ...base, ...regionScope(service, options.region, env) };
150
+ return signRequest({
151
+ method: "POST",
152
+ url,
153
+ headers: base,
154
+ body,
155
+ service,
156
+ region: options.region ?? DEFAULT_REGION,
157
+ credentials,
158
+ ...(options.now ? { now: options.now } : {}),
159
+ });
160
+ }
161
+
103
162
  /* ── CloudFormation Query ─────────────────────────────────────────────────── */
104
163
 
105
164
  /**
@@ -162,10 +221,13 @@ export async function cfnQuery(
162
221
  const res = await http(
163
222
  url,
164
223
  {
165
- headers: {
166
- "content-type": "application/x-www-form-urlencoded",
167
- ...regionScope("cloudformation", options.region),
168
- },
224
+ headers: requestHeaders(
225
+ "cloudformation",
226
+ url,
227
+ body,
228
+ { "content-type": "application/x-www-form-urlencoded" },
229
+ options,
230
+ ),
169
231
  body,
170
232
  },
171
233
  options.signal,
@@ -238,15 +300,21 @@ async function cloudControl(
238
300
  ): Promise<Record<string, unknown>> {
239
301
  const http = options.http ?? defaultHttp;
240
302
  const url = serviceUrl("cloudcontrolapi", options.endpoint, options.region);
303
+ const payloadJson = JSON.stringify(payload);
241
304
  const res = await http(
242
305
  url,
243
306
  {
244
- headers: {
245
- "content-type": "application/x-amz-json-1.0",
246
- "x-amz-target": `${CLOUD_CONTROL_TARGET_PREFIX}.${operation}`,
247
- ...regionScope("cloudcontrolapi", options.region),
248
- },
249
- body: JSON.stringify(payload),
307
+ headers: requestHeaders(
308
+ "cloudcontrolapi",
309
+ url,
310
+ payloadJson,
311
+ {
312
+ "content-type": "application/x-amz-json-1.0",
313
+ "x-amz-target": `${CLOUD_CONTROL_TARGET_PREFIX}.${operation}`,
314
+ },
315
+ options,
316
+ ),
317
+ body: payloadJson,
250
318
  },
251
319
  options.signal,
252
320
  );