@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.
- package/dist/agentcore/trace-fetch.d.ts +4 -1
- package/dist/agentcore/trace-fetch.d.ts.map +1 -1
- package/dist/api/read-client.d.ts +31 -1
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/cloud-executor.d.ts +9 -0
- package/dist/components/cloud-executor.d.ts.map +1 -1
- package/dist/composites/agentcore-agent.d.ts +33 -19
- package/dist/composites/agentcore-agent.d.ts.map +1 -1
- package/dist/composites/index.d.ts +1 -1
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/deep-observe.d.ts +21 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/aws-apply.d.ts +14 -4
- package/dist/op/activities/aws-apply.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/properties.d.ts +4 -3
- package/dist/properties.d.ts.map +1 -1
- package/dist/spec/fetch.d.ts +12 -1
- package/dist/spec/fetch.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/agentcore/trace-fetch.test.ts +17 -0
- package/src/agentcore/trace-fetch.ts +7 -2
- package/src/api/read-client.test.ts +87 -0
- package/src/api/read-client.ts +57 -1
- package/src/codegen/docs-links.test.ts +44 -30
- package/src/codegen/docs.ts +2 -1034
- package/src/components/capability-plugin.ts +5 -2
- package/src/components/cloud-executor.test.ts +29 -1
- package/src/components/cloud-executor.ts +23 -5
- package/src/composites/agentcore-agent.test.ts +32 -12
- package/src/composites/agentcore-agent.ts +43 -23
- package/src/composites/index.ts +1 -1
- package/src/composites/microvm-app.test.ts +2 -2
- package/src/deep-observe.test.ts +94 -0
- package/src/deep-observe.ts +58 -2
- package/src/import/roundtrip-fixtures.test.ts +1 -1
- package/src/index.ts +3 -1
- package/src/lifecycle-integration.test.ts +89 -0
- package/src/op/activities/aws-apply.test.ts +40 -4
- package/src/op/activities/aws-apply.ts +23 -7
- package/src/plugin.ts +34 -35
- package/src/properties.test.ts +5 -5
- package/src/properties.ts +6 -5
- package/src/serializer.test.ts +73 -33
- package/src/spec/fetch.test.ts +40 -0
- 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.
|
|
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
|
-
|
|
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
|
+
});
|
package/src/api/read-client.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
});
|