@intentius/chant-lexicon-aws 0.44.13 → 0.45.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.
Files changed (52) hide show
  1. package/dist/agentcore/trace-fetch.d.ts +4 -1
  2. package/dist/agentcore/trace-fetch.d.ts.map +1 -1
  3. package/dist/api/read-client.d.ts +31 -1
  4. package/dist/api/read-client.d.ts.map +1 -1
  5. package/dist/codegen/docs.d.ts.map +1 -1
  6. package/dist/components/capability-plugin.d.ts.map +1 -1
  7. package/dist/components/cloud-executor.d.ts +9 -0
  8. package/dist/components/cloud-executor.d.ts.map +1 -1
  9. package/dist/composites/agentcore-agent.d.ts +33 -19
  10. package/dist/composites/agentcore-agent.d.ts.map +1 -1
  11. package/dist/composites/index.d.ts +1 -1
  12. package/dist/composites/index.d.ts.map +1 -1
  13. package/dist/deep-observe.d.ts +21 -0
  14. package/dist/deep-observe.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/integrity.json +2 -2
  18. package/dist/manifest.json +1 -1
  19. package/dist/op/activities/aws-apply.d.ts +14 -4
  20. package/dist/op/activities/aws-apply.d.ts.map +1 -1
  21. package/dist/plugin.d.ts.map +1 -1
  22. package/dist/properties.d.ts +4 -3
  23. package/dist/properties.d.ts.map +1 -1
  24. package/dist/spec/fetch.d.ts +12 -1
  25. package/dist/spec/fetch.d.ts.map +1 -1
  26. package/package.json +2 -2
  27. package/src/agentcore/trace-fetch.test.ts +17 -0
  28. package/src/agentcore/trace-fetch.ts +7 -2
  29. package/src/api/read-client.test.ts +87 -0
  30. package/src/api/read-client.ts +57 -1
  31. package/src/codegen/docs-links.test.ts +44 -30
  32. package/src/codegen/docs.ts +2 -1034
  33. package/src/components/capability-plugin.ts +5 -2
  34. package/src/components/cloud-executor.test.ts +29 -1
  35. package/src/components/cloud-executor.ts +23 -5
  36. package/src/composites/agentcore-agent.test.ts +32 -12
  37. package/src/composites/agentcore-agent.ts +43 -23
  38. package/src/composites/index.ts +1 -1
  39. package/src/composites/microvm-app.test.ts +2 -2
  40. package/src/deep-observe.test.ts +94 -0
  41. package/src/deep-observe.ts +58 -2
  42. package/src/import/roundtrip-fixtures.test.ts +1 -1
  43. package/src/index.ts +3 -1
  44. package/src/lifecycle-integration.test.ts +89 -0
  45. package/src/op/activities/aws-apply.test.ts +40 -4
  46. package/src/op/activities/aws-apply.ts +23 -7
  47. package/src/plugin.ts +34 -35
  48. package/src/properties.test.ts +5 -5
  49. package/src/properties.ts +6 -5
  50. package/src/serializer.test.ts +73 -33
  51. package/src/spec/fetch.test.ts +40 -0
  52. package/src/spec/fetch.ts +24 -3
@@ -68,6 +68,9 @@
68
68
  * endpoint override, since an emulator does not verify signatures and requiring
69
69
  * credentials to read a local lane would be a tax with nothing behind it.
70
70
  * `signEndpointOverride: true` opts back in for an override that *is* real AWS.
71
+ * What counts as an override is read-client's `resolveEndpointOverride` rule —
72
+ * the `endpoint` option, else `AWS_ENDPOINT_URL_BEDROCK_AGENTCORE`, else
73
+ * `AWS_ENDPOINT_URL` (#1694).
71
74
  */
72
75
 
73
76
  import { mkdir, writeFile } from "node:fs/promises";
@@ -76,6 +79,7 @@ import {
76
79
  AwsReadError,
77
80
  requestHeaders,
78
81
  serviceUrl,
82
+ withEndpointOverride,
79
83
  type AwsCredentialSource,
80
84
  type AwsReadHttp,
81
85
  } from "../api/read-client";
@@ -506,7 +510,7 @@ const defaultHttp: AwsReadHttp = async (url, init, signal) => {
506
510
 
507
511
  /** Where to read from, how to reach it, and what to sign with. */
508
512
  export interface AgentCoreReadOptions {
509
- /** Endpoint override. Omit for the real regional host. */
513
+ /** Endpoint override. Omitted, `AWS_ENDPOINT_URL[_BEDROCK_AGENTCORE]` answers; with neither, the real regional host. */
510
514
  readonly endpoint?: string;
511
515
  /** Default `us-east-1`. */
512
516
  readonly region?: string;
@@ -527,10 +531,11 @@ export interface AgentCoreReadOptions {
527
531
  async function agentCorePost(
528
532
  path: string,
529
533
  body: Record<string, unknown>,
530
- options: AgentCoreReadOptions,
534
+ readOptions: AgentCoreReadOptions,
531
535
  http: AwsReadHttp,
532
536
  signal?: AbortSignal,
533
537
  ): Promise<Record<string, unknown>> {
538
+ const options = withEndpointOverride(SERVICE, readOptions);
534
539
  const url = `${serviceUrl(SERVICE, options.endpoint, options.region)}${path.replace(/^\//, "")}`;
535
540
  const wire = JSON.stringify(body);
536
541
  const res = await http(
@@ -13,6 +13,8 @@ import {
13
13
  getResource,
14
14
  listResources,
15
15
  parseResourceDescription,
16
+ resolveEndpointOverride,
17
+ serviceEndpointEnvVar,
16
18
  serviceUrl,
17
19
  xmlLeaves,
18
20
  xmlMembers,
@@ -315,3 +317,88 @@ describe("SigV4 on the read path", () => {
315
317
  expect(calls[0].headers.authorization).toContain("/20150830/us-east-1/cloudformation/aws4_request");
316
318
  });
317
319
  });
320
+
321
+ describe("what counts as an endpoint override (#1694)", () => {
322
+ const credentials = { accessKeyId: "AKIDEXAMPLE", secretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" };
323
+ const now = new Date("2015-08-30T12:36:00Z");
324
+
325
+ test("the option, then the service variable, then AWS_ENDPOINT_URL — the SDK's precedence", () => {
326
+ const env = { AWS_ENDPOINT_URL: "http://all:1", AWS_ENDPOINT_URL_CLOUDFORMATION: "http://cfn:2" };
327
+ expect(resolveEndpointOverride("cloudformation", "http://opt:3", env)).toBe("http://opt:3");
328
+ expect(resolveEndpointOverride("cloudformation", undefined, env)).toBe("http://cfn:2");
329
+ expect(resolveEndpointOverride("cloudcontrolapi", undefined, env)).toBe("http://all:1");
330
+ expect(resolveEndpointOverride("cloudformation", undefined, {})).toBeUndefined();
331
+ expect(resolveEndpointOverride("cloudformation", "", { AWS_ENDPOINT_URL: "" })).toBeUndefined();
332
+ });
333
+
334
+ test("the service variable is named by the SDK's service id, not the signing name", () => {
335
+ expect(serviceEndpointEnvVar("cloudformation")).toBe("AWS_ENDPOINT_URL_CLOUDFORMATION");
336
+ expect(serviceEndpointEnvVar("cloudcontrolapi")).toBe("AWS_ENDPOINT_URL_CLOUDCONTROL");
337
+ expect(serviceEndpointEnvVar("bedrock-agentcore")).toBe("AWS_ENDPOINT_URL_BEDROCK_AGENTCORE");
338
+ });
339
+
340
+ test("AWS_ENDPOINT_URL alone retargets the request and leaves it unsigned, like the option", async () => {
341
+ const { http, calls } = recording(() => respond(stackXml));
342
+ await describeStackResources("web", {
343
+ region: "us-west-1",
344
+ credentials,
345
+ http,
346
+ env: { AWS_ENDPOINT_URL: "http://localhost:4566" },
347
+ });
348
+ expect(calls[0].url).toBe("http://localhost:4566/");
349
+ expect(calls[0].headers.authorization).toContain("Signature=unsigned");
350
+ expect(calls[0].headers["x-amz-date"]).toBeUndefined();
351
+ });
352
+
353
+ test("the service-specific variable does the same, for Cloud Control too", async () => {
354
+ const { http, calls } = recording(() => respond(JSON.stringify({ ResourceDescription: {} })));
355
+ await getResource("AWS::EC2::VPC", "vpc-01", {
356
+ region: "eu-west-1",
357
+ credentials,
358
+ http,
359
+ env: { AWS_ENDPOINT_URL_CLOUDCONTROL: "http://localhost:4566" },
360
+ });
361
+ expect(calls[0].url).toBe("http://localhost:4566/");
362
+ expect(calls[0].headers.authorization).toContain("Signature=unsigned");
363
+ });
364
+
365
+ test("the option is the same override — same target, same unsigned headers", async () => {
366
+ const viaEnv = recording(() => respond(stackXml));
367
+ const viaOption = recording(() => respond(stackXml));
368
+ await describeStackResources("web", {
369
+ region: "us-west-1",
370
+ credentials,
371
+ http: viaEnv.http,
372
+ env: { AWS_ENDPOINT_URL: "http://localhost:4566" },
373
+ });
374
+ await describeStackResources("web", {
375
+ endpoint: "http://localhost:4566",
376
+ region: "us-west-1",
377
+ credentials,
378
+ http: viaOption.http,
379
+ env: {},
380
+ });
381
+ expect(viaOption.calls[0]).toEqual(viaEnv.calls[0]);
382
+ });
383
+
384
+ test("signEndpointOverride signs against an override the environment named", async () => {
385
+ const { http, calls } = recording(() => respond(stackXml));
386
+ await describeStackResources("web", {
387
+ region: "us-west-1",
388
+ credentials,
389
+ signEndpointOverride: true,
390
+ now,
391
+ http,
392
+ env: { AWS_ENDPOINT_URL: "https://vpce-1234.cloudformation.us-west-1.vpce.amazonaws.com" },
393
+ });
394
+ expect(calls[0].url).toBe("https://vpce-1234.cloudformation.us-west-1.vpce.amazonaws.com/");
395
+ expect(calls[0].headers.authorization).toMatch(/Signature=[0-9a-f]{64}$/);
396
+ });
397
+
398
+ test("neither option nor variable: the real regional host, signed", async () => {
399
+ const { http, calls } = recording(() => respond(stackXml));
400
+ await describeStackResources("web", { region: "us-west-1", credentials, now, http, env: {} });
401
+ expect(calls[0].url).toBe("https://cloudformation.us-west-1.amazonaws.com/");
402
+ expect(calls[0].headers.authorization).toMatch(/Signature=[0-9a-f]{64}$/);
403
+ });
404
+ });
@@ -32,6 +32,14 @@
32
32
  * against it would mean every local lane suddenly needs credentials to read
33
33
  * what it just deployed. `signEndpointOverride` opts back in for an
34
34
  * override that *is* real AWS — a VPC endpoint, a signing proxy.
35
+ *
36
+ * What counts as an override is one rule, {@link resolveEndpointOverride}: the
37
+ * `endpoint` option, else the ambient `AWS_ENDPOINT_URL_<SERVICE>` the AWS SDK
38
+ * honours per service, else `AWS_ENDPOINT_URL` (#1694). Cedar's AVP client
39
+ * (`lexicons/cedar/src/avp/client.ts`) applies the same rule; it restates it
40
+ * rather than importing it, because cedar does not depend on this lexicon and
41
+ * a lexicon build compiles against the published core, so a helper hoisted
42
+ * into core would not be visible to either until the next core release.
35
43
  */
36
44
  import { resolveCredentials, signRequest, type AwsCredentialSource } from "./sigv4";
37
45
 
@@ -67,7 +75,11 @@ export class AwsReadError extends Error {
67
75
  }
68
76
 
69
77
  export interface AwsReadClientOptions {
70
- /** Endpoint override (Floci `http://localhost:4566`). Omit for real AWS hosts. */
78
+ /**
79
+ * Endpoint override (Floci `http://localhost:4566`). Omitted, the environment
80
+ * answers through {@link resolveEndpointOverride}; when it names nothing
81
+ * either, the target is the real AWS host.
82
+ */
71
83
  endpoint?: string;
72
84
  /** Region for the real-AWS host and the Query `Version` context. */
73
85
  region?: string;
@@ -87,6 +99,45 @@ export interface AwsReadClientOptions {
87
99
  now?: Date;
88
100
  }
89
101
 
102
+ /**
103
+ * The SDK's per-service endpoint variable for a signing-service name:
104
+ * `AWS_ENDPOINT_URL_<SERVICE ID>`, the service id upper-cased with every
105
+ * non-alphanumeric run replaced by `_`. The SDK keys that by the service id of
106
+ * its model, which is the signing name for every service here except Cloud
107
+ * Control (`cloudcontrolapi` signs, `CloudControl` is the id).
108
+ */
109
+ const ENV_SERVICE_ID: Record<string, string> = { cloudcontrolapi: "cloudcontrol" };
110
+
111
+ /** The name of the service-specific endpoint variable the AWS SDK reads for `service`. */
112
+ export function serviceEndpointEnvVar(service: string): string {
113
+ const id = ENV_SERVICE_ID[service] ?? service;
114
+ return `AWS_ENDPOINT_URL_${id.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
115
+ }
116
+
117
+ /**
118
+ * The one rule for what an endpoint override is (#1694): the `endpoint` option
119
+ * when given, else the service-specific `AWS_ENDPOINT_URL_<SERVICE>`, else the
120
+ * ambient `AWS_ENDPOINT_URL` — the same precedence the AWS SDK applies. The
121
+ * result is the target the request goes to and, through {@link requestHeaders},
122
+ * the fact that decides whether it is signed. Returns `undefined` for real AWS.
123
+ */
124
+ export function resolveEndpointOverride(
125
+ service: string,
126
+ endpoint: string | undefined,
127
+ env: Record<string, string | undefined> = process.env,
128
+ ): string | undefined {
129
+ return endpoint || env[serviceEndpointEnvVar(service)] || env.AWS_ENDPOINT_URL || undefined;
130
+ }
131
+
132
+ /**
133
+ * `options` with its endpoint settled by {@link resolveEndpointOverride}, so
134
+ * the URL builder and the signing decision read the same answer.
135
+ */
136
+ export function withEndpointOverride(service: string, options: AwsReadClientOptions): AwsReadClientOptions {
137
+ const endpoint = resolveEndpointOverride(service, options.endpoint, options.env ?? process.env);
138
+ return endpoint ? { ...options, endpoint } : options;
139
+ }
140
+
90
141
  /** Service host for `service`, honouring an endpoint override. */
91
142
  export function serviceUrl(service: string, endpoint?: string, region = DEFAULT_REGION): string {
92
143
  return `${(endpoint ?? `https://${service}.${region}.amazonaws.com`).replace(/\/$/, "")}/`;
@@ -141,6 +192,9 @@ function regionScope(
141
192
  * `agentcore/trace-fetch.ts` reads `bedrock-agentcore` through the same seam,
142
193
  * and a second copy of this would be a second place for the emulator carve-out
143
194
  * to drift.
195
+ *
196
+ * Callers pass options already settled by {@link withEndpointOverride}, so an
197
+ * override the environment named is skipped exactly like one the option did.
144
198
  */
145
199
  export function requestHeaders(
146
200
  service: string,
@@ -221,6 +275,7 @@ export async function cfnQuery(
221
275
  params: Record<string, string>,
222
276
  options: AwsReadClientOptions = {},
223
277
  ): Promise<string> {
278
+ options = withEndpointOverride("cloudformation", options);
224
279
  const http = options.http ?? defaultHttp;
225
280
  const url = serviceUrl("cloudformation", options.endpoint, options.region);
226
281
  const body = new URLSearchParams({ Action: action, Version: CFN_API_VERSION, ...params }).toString();
@@ -304,6 +359,7 @@ async function cloudControl(
304
359
  payload: Record<string, unknown>,
305
360
  options: AwsReadClientOptions = {},
306
361
  ): Promise<Record<string, unknown>> {
362
+ options = withEndpointOverride("cloudcontrolapi", options);
307
363
  const http = options.http ?? defaultHttp;
308
364
  const url = serviceUrl("cloudcontrolapi", options.endpoint, options.region);
309
365
  const payloadJson = JSON.stringify(payload);
@@ -4,6 +4,18 @@ import { join, basename } from "path";
4
4
 
5
5
  const docsDir = join(import.meta.dirname, "..", "..", "docs", "src", "content", "docs");
6
6
  const docsSource = join(import.meta.dirname, "docs.ts");
7
+ const pagesDir = join(import.meta.dirname, "..", "..", "docs", "pages");
8
+
9
+ /** Authored prose lives in docs/pages/ (chant #1731); docs.ts keeps only the overview strings. */
10
+ function authoredSources(): Array<{ name: string; path: string }> {
11
+ const sources = [{ name: "docs.ts", path: docsSource }];
12
+ if (existsSync(pagesDir)) {
13
+ for (const file of readdirSync(pagesDir).sort()) {
14
+ if (file.endsWith(".mdx")) sources.push({ name: `docs/pages/${file}`, path: join(pagesDir, file) });
15
+ }
16
+ }
17
+ return sources;
18
+ }
7
19
  const docsExist = existsSync(docsDir);
8
20
 
9
21
  /**
@@ -108,36 +120,38 @@ describe("docs internal links", () => {
108
120
  });
109
121
  }
110
122
 
111
- // Validate source docs.ts catches broken links before regeneration
112
- test("docs.ts source cross-page links use ../ not ./", () => {
113
- if (!docsExist) return; // needs generated slugs for validation
114
- const content = readFileSync(docsSource, "utf-8");
115
- const links = extractMarkdownLinks(content);
116
- const errors: string[] = [];
117
- for (const link of links) {
118
- const pathPart = link.href.split("#")[0];
119
- // Links in docs.ts extraPages are rendered on non-index pages,
120
- // so they must use ../ to navigate to sibling pages
121
- if (pathPart.startsWith("./") && slugs.has(pathPart.slice(2).replace(/\/$/, ""))) {
122
- errors.push(`line ${link.line}: [${link.text}](${link.href}) — use "../" prefix for cross-page links`);
123
+ // Validate the authored sources — docs.ts and docs/pages/*.mdx so a
124
+ // broken link is caught before regeneration.
125
+ for (const source of authoredSources()) {
126
+ test(`${source.name} cross-page links use ../ not ./`, () => {
127
+ if (!docsExist) return; // needs generated slugs for validation
128
+ const content = readFileSync(source.path, "utf-8");
129
+ const links = extractMarkdownLinks(content);
130
+ const errors: string[] = [];
131
+ for (const link of links) {
132
+ const pathPart = link.href.split("#")[0];
133
+ // Authored pages render as non-index pages, so sibling links must use ../
134
+ if (pathPart.startsWith("./") && slugs.has(pathPart.slice(2).replace(/\/$/, ""))) {
135
+ errors.push(`line ${link.line}: [${link.text}](${link.href}) — use "../" prefix for cross-page links`);
136
+ }
123
137
  }
124
- }
125
- if (errors.length > 0) {
126
- throw new Error(`docs.ts has ./ links that will break on non-index pages:\n${errors.join("\n")}`);
127
- }
128
- });
138
+ if (errors.length > 0) {
139
+ throw new Error(`${source.name} has ./ links that will break on non-index pages:\n${errors.join("\n")}`);
140
+ }
141
+ });
129
142
 
130
- test("docs.ts source — link targets exist as pages", () => {
131
- if (!docsExist) return; // needs generated slugs for validation
132
- const content = readFileSync(docsSource, "utf-8");
133
- const links = extractMarkdownLinks(content);
134
- const errors: string[] = [];
135
- for (const link of links) {
136
- const error = resolveTarget(link.href, slugs);
137
- if (error) errors.push(`line ${link.line}: [${link.text}](${link.href}) — ${error}`);
138
- }
139
- if (errors.length > 0) {
140
- throw new Error(`docs.ts has links to non-existent pages:\n${errors.join("\n")}`);
141
- }
142
- });
143
+ test(`${source.name} — link targets exist as pages`, () => {
144
+ if (!docsExist) return; // needs generated slugs for validation
145
+ const content = readFileSync(source.path, "utf-8");
146
+ const links = extractMarkdownLinks(content);
147
+ const errors: string[] = [];
148
+ for (const link of links) {
149
+ const error = resolveTarget(link.href, slugs);
150
+ if (error) errors.push(`line ${link.line}: [${link.text}](${link.href}) — ${error}`);
151
+ }
152
+ if (errors.length > 0) {
153
+ throw new Error(`${source.name} has links to non-existent pages:\n${errors.join("\n")}`);
154
+ }
155
+ });
156
+ }
143
157
  });