@gajae-code/ai 0.1.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.
- package/CHANGELOG.md +2644 -0
- package/README.md +1181 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth-broker/client.d.ts +66 -0
- package/dist/types/auth-broker/index.d.ts +5 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +96 -0
- package/dist/types/auth-broker/server.d.ts +32 -0
- package/dist/types/auth-broker/types.d.ts +105 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
- package/dist/types/auth-gateway/http.d.ts +39 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +17 -0
- package/dist/types/auth-gateway/types.d.ts +115 -0
- package/dist/types/auth-storage.d.ts +641 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/index.d.ts +49 -0
- package/dist/types/model-cache.d.ts +17 -0
- package/dist/types/model-manager.d.ts +62 -0
- package/dist/types/model-thinking.d.ts +71 -0
- package/dist/types/models.d.ts +12 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/provider-models/bundled-references.d.ts +4 -0
- package/dist/types/provider-models/descriptors.d.ts +48 -0
- package/dist/types/provider-models/google.d.ts +20 -0
- package/dist/types/provider-models/index.d.ts +5 -0
- package/dist/types/provider-models/ollama.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +237 -0
- package/dist/types/provider-models/special.d.ts +16 -0
- package/dist/types/providers/amazon-bedrock.d.ts +36 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic.d.ts +188 -0
- package/dist/types/providers/aws-credentials.d.ts +43 -0
- package/dist/types/providers/aws-eventstream.d.ts +38 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +15 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
- package/dist/types/providers/cursor.d.ts +42 -0
- package/dist/types/providers/error-message.d.ts +27 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +24 -0
- package/dist/types/providers/google-gemini-cli.d.ts +72 -0
- package/dist/types/providers/google-gemini-headers.d.ts +18 -0
- package/dist/types/providers/google-shared.d.ts +163 -0
- package/dist/types/providers/google-types.d.ts +138 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +175 -0
- package/dist/types/providers/ollama.d.ts +6 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-codex/constants.d.ts +26 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
- package/dist/types/providers/openai-codex-responses.d.ts +67 -0
- package/dist/types/providers/openai-completions-compat.d.ts +25 -0
- package/dist/types/providers/openai-completions.d.ts +33 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-shared.d.ts +89 -0
- package/dist/types/providers/openai-responses.d.ts +32 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +68 -0
- package/dist/types/providers/register-builtins.d.ts +31 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +12 -0
- package/dist/types/providers/vision-guard.d.ts +8 -0
- package/dist/types/rate-limit-utils.d.ts +19 -0
- package/dist/types/stream.d.ts +24 -0
- package/dist/types/types.d.ts +746 -0
- package/dist/types/usage/claude.d.ts +3 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +2 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +2 -0
- package/dist/types/usage/openai-codex.d.ts +3 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +258 -0
- package/dist/types/utils/abort.d.ts +19 -0
- package/dist/types/utils/anthropic-auth.d.ts +31 -0
- package/dist/types/utils/discovery/antigravity.d.ts +61 -0
- package/dist/types/utils/discovery/codex.d.ts +38 -0
- package/dist/types/utils/discovery/cursor.d.ts +23 -0
- package/dist/types/utils/discovery/gemini.d.ts +25 -0
- package/dist/types/utils/discovery/index.d.ts +4 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
- package/dist/types/utils/event-stream.d.ts +28 -0
- package/dist/types/utils/fireworks-model-id.d.ts +10 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/h2-fetch.d.ts +22 -0
- package/dist/types/utils/http-inspector.d.ts +31 -0
- package/dist/types/utils/idle-iterator.d.ts +67 -0
- package/dist/types/utils/json-parse.d.ts +10 -0
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
- package/dist/types/utils/oauth/anthropic.d.ts +22 -0
- package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
- package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
- package/dist/types/utils/oauth/callback-server.d.ts +57 -0
- package/dist/types/utils/oauth/cerebras.d.ts +1 -0
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/cursor.d.ts +15 -0
- package/dist/types/utils/oauth/deepseek.d.ts +10 -0
- package/dist/types/utils/oauth/firepass.d.ts +1 -0
- package/dist/types/utils/oauth/fireworks.d.ts +1 -0
- package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
- package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
- package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
- package/dist/types/utils/oauth/huggingface.d.ts +19 -0
- package/dist/types/utils/oauth/index.d.ts +38 -0
- package/dist/types/utils/oauth/kagi.d.ts +17 -0
- package/dist/types/utils/oauth/kilo.d.ts +5 -0
- package/dist/types/utils/oauth/kimi.d.ts +21 -0
- package/dist/types/utils/oauth/litellm.d.ts +18 -0
- package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
- package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
- package/dist/types/utils/oauth/moonshot.d.ts +1 -0
- package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
- package/dist/types/utils/oauth/nvidia.d.ts +18 -0
- package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
- package/dist/types/utils/oauth/ollama.d.ts +18 -0
- package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
- package/dist/types/utils/oauth/opencode.d.ts +18 -0
- package/dist/types/utils/oauth/parallel.d.ts +17 -0
- package/dist/types/utils/oauth/perplexity.d.ts +9 -0
- package/dist/types/utils/oauth/pkce.d.ts +8 -0
- package/dist/types/utils/oauth/qianfan.d.ts +17 -0
- package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
- package/dist/types/utils/oauth/synthetic.d.ts +1 -0
- package/dist/types/utils/oauth/tavily.d.ts +17 -0
- package/dist/types/utils/oauth/together.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +44 -0
- package/dist/types/utils/oauth/venice.d.ts +18 -0
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/vllm.d.ts +16 -0
- package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
- package/dist/types/utils/oauth/zai.d.ts +18 -0
- package/dist/types/utils/oauth/zenmux.d.ts +1 -0
- package/dist/types/utils/overflow.d.ts +54 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/retry-after.d.ts +3 -0
- package/dist/types/utils/retry.d.ts +26 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +49 -0
- package/dist/types/utils/schema/index.d.ts +13 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +93 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +25 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/wire.d.ts +54 -0
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
- package/dist/types/utils/sse-debug.d.ts +10 -0
- package/dist/types/utils/tool-call-healing.d.ts +71 -0
- package/dist/types/utils/tool-choice.d.ts +50 -0
- package/dist/types/utils/validation.d.ts +17 -0
- package/dist/types/utils.d.ts +28 -0
- package/package.json +146 -0
- package/src/api-registry.ts +96 -0
- package/src/auth-broker/client.ts +358 -0
- package/src/auth-broker/index.ts +5 -0
- package/src/auth-broker/refresher.ts +127 -0
- package/src/auth-broker/remote-store.ts +623 -0
- package/src/auth-broker/server.ts +644 -0
- package/src/auth-broker/types.ts +127 -0
- package/src/auth-broker/wire-schemas.ts +200 -0
- package/src/auth-gateway/http.ts +194 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +717 -0
- package/src/auth-gateway/types.ts +134 -0
- package/src/auth-storage.ts +4104 -0
- package/src/cli.ts +262 -0
- package/src/index.ts +54 -0
- package/src/model-cache.ts +129 -0
- package/src/model-manager.ts +450 -0
- package/src/model-thinking.ts +691 -0
- package/src/models.json +73853 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +90 -0
- package/src/provider-models/bundled-references.ts +38 -0
- package/src/provider-models/descriptors.ts +308 -0
- package/src/provider-models/google.ts +91 -0
- package/src/provider-models/index.ts +5 -0
- package/src/provider-models/ollama.ts +153 -0
- package/src/provider-models/openai-compat.ts +2275 -0
- package/src/provider-models/special.ts +67 -0
- package/src/providers/amazon-bedrock.ts +849 -0
- package/src/providers/anthropic-messages-server-schema.ts +229 -0
- package/src/providers/anthropic-messages-server.ts +677 -0
- package/src/providers/anthropic.ts +2696 -0
- package/src/providers/aws-credentials.ts +501 -0
- package/src/providers/aws-eventstream.ts +185 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +337 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2561 -0
- package/src/providers/error-message.ts +21 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +372 -0
- package/src/providers/google-auth.ts +252 -0
- package/src/providers/google-gemini-cli.ts +795 -0
- package/src/providers/google-gemini-headers.ts +41 -0
- package/src/providers/google-shared.ts +902 -0
- package/src/providers/google-types.ts +167 -0
- package/src/providers/google-vertex.ts +88 -0
- package/src/providers/google.ts +41 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/mock.ts +500 -0
- package/src/providers/ollama.ts +544 -0
- package/src/providers/openai-anthropic-shim.ts +138 -0
- package/src/providers/openai-chat-server-schema.ts +243 -0
- package/src/providers/openai-chat-server.ts +628 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +161 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2598 -0
- package/src/providers/openai-completions-compat.ts +279 -0
- package/src/providers/openai-completions.ts +1853 -0
- package/src/providers/openai-responses-server-schema.ts +290 -0
- package/src/providers/openai-responses-server.ts +1183 -0
- package/src/providers/openai-responses-shared.ts +800 -0
- package/src/providers/openai-responses.ts +621 -0
- package/src/providers/pi-native-client.ts +228 -0
- package/src/providers/pi-native-server.ts +210 -0
- package/src/providers/register-builtins.ts +412 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +309 -0
- package/src/providers/vision-guard.ts +31 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +895 -0
- package/src/types.ts +884 -0
- package/src/usage/claude.ts +431 -0
- package/src/usage/gemini.ts +250 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +201 -0
- package/src/usage/kimi.ts +271 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +503 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +183 -0
- package/src/utils/abort.ts +51 -0
- package/src/utils/anthropic-auth.ts +87 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +4 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +142 -0
- package/src/utils/fireworks-model-id.ts +30 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/h2-fetch.ts +60 -0
- package/src/utils/http-inspector.ts +176 -0
- package/src/utils/idle-iterator.ts +250 -0
- package/src/utils/json-parse.ts +148 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +200 -0
- package/src/utils/oauth/api-key-login.ts +87 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +16 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/deepseek.ts +53 -0
- package/src/utils/oauth/firepass.ts +24 -0
- package/src/utils/oauth/fireworks.ts +15 -0
- package/src/utils/oauth/github-copilot.ts +362 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +200 -0
- package/src/utils/oauth/google-gemini-cli.ts +256 -0
- package/src/utils/oauth/google-oauth-shared.ts +110 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +444 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +254 -0
- package/src/utils/oauth/litellm.ts +47 -0
- package/src/utils/oauth/lm-studio.ts +38 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +16 -0
- package/src/utils/oauth/nanogpt.ts +15 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama-cloud.ts +28 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +299 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +206 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +16 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +16 -0
- package/src/utils/oauth/types.ts +94 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +137 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +15 -0
- package/src/utils/overflow.ts +137 -0
- package/src/utils/parse-bind.ts +54 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +54 -0
- package/src/utils/schema/CONSTRAINTS.md +164 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +190 -0
- package/src/utils/schema/index.ts +13 -0
- package/src/utils/schema/json-schema-validator.ts +577 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +1588 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +97 -0
- package/src/utils/schema/types.ts +11 -0
- package/src/utils/schema/wire.ts +213 -0
- package/src/utils/schema/zod-decontaminate.ts +331 -0
- package/src/utils/sse-debug.ts +289 -0
- package/src/utils/tool-call-healing.ts +271 -0
- package/src/utils/tool-choice.ts +99 -0
- package/src/utils/validation.ts +1019 -0
- package/src/utils.ts +166 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS credential resolution for the Bedrock provider.
|
|
3
|
+
*
|
|
4
|
+
* Chain (first hit wins):
|
|
5
|
+
* 1. Static credentials from the environment
|
|
6
|
+
* (`AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` [+ `AWS_SESSION_TOKEN`]).
|
|
7
|
+
* 2. Profile in `~/.aws/credentials` (and `~/.aws/config` for SSO):
|
|
8
|
+
* - static `aws_access_key_id` / `aws_secret_access_key` / `aws_session_token`
|
|
9
|
+
* - SSO profile referencing a cached token in `~/.aws/sso/cache/*.json`,
|
|
10
|
+
* which we exchange for short-lived role credentials via
|
|
11
|
+
* `https://portal.sso.{region}.amazonaws.com/federation/credentials`.
|
|
12
|
+
* - `credential_process` — an external command emitting the AWS SDK
|
|
13
|
+
* `Version: 1` JSON envelope on stdout. Used by `aws-vault`, `granted`,
|
|
14
|
+
* in-house brokers, etc.
|
|
15
|
+
* 3. EC2 IMDSv2 (only when `AWS_EC2_METADATA_DISABLED` is unset / falsey and
|
|
16
|
+
* `169.254.169.254` is reachable within a 1 s timeout).
|
|
17
|
+
*
|
|
18
|
+
* Resolved credentials are cached process-wide per profile and refreshed
|
|
19
|
+
* 60 s before `Expiration` to absorb clock skew.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as os from "node:os";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import { $env, isEnoent, logger } from "@gajae-code/utils";
|
|
26
|
+
import type { AwsCredentials } from "./aws-sigv4";
|
|
27
|
+
|
|
28
|
+
export interface ResolvedCredentials extends AwsCredentials {
|
|
29
|
+
/** Absolute expiration timestamp in ms. `undefined` for non-expiring static creds. */
|
|
30
|
+
expiresAt?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CredentialResolveOptions {
|
|
34
|
+
/** Named profile from `~/.aws/credentials` / `~/.aws/config`. */
|
|
35
|
+
profile?: string;
|
|
36
|
+
/** Falls back to env (`AWS_REGION` / `AWS_DEFAULT_REGION`) and finally `us-east-1`. */
|
|
37
|
+
region?: string;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
42
|
+
|
|
43
|
+
interface CacheEntry {
|
|
44
|
+
creds: ResolvedCredentials;
|
|
45
|
+
expiresAt: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const cache: Map<string, CacheEntry> = new Map();
|
|
49
|
+
|
|
50
|
+
export async function resolveAwsCredentials(opts: CredentialResolveOptions = {}): Promise<ResolvedCredentials> {
|
|
51
|
+
const profile = opts.profile || $env.AWS_PROFILE || "default";
|
|
52
|
+
const region = opts.region || $env.AWS_REGION || $env.AWS_DEFAULT_REGION || "us-east-1";
|
|
53
|
+
const cacheKey = `${profile}\x00${region}`;
|
|
54
|
+
|
|
55
|
+
const hit = cache.get(cacheKey);
|
|
56
|
+
if (hit && hit.expiresAt - REFRESH_SKEW_MS > Date.now()) return hit.creds;
|
|
57
|
+
|
|
58
|
+
const creds = await resolveFresh(profile, region, opts.signal);
|
|
59
|
+
cache.set(cacheKey, { creds, expiresAt: creds.expiresAt ?? Number.POSITIVE_INFINITY });
|
|
60
|
+
return creds;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function resolveFresh(profile: string, region: string, signal?: AbortSignal): Promise<ResolvedCredentials> {
|
|
64
|
+
// 1. Environment first — matches the AWS SDK chain order.
|
|
65
|
+
const envCreds = readEnvCredentials();
|
|
66
|
+
if (envCreds) return envCreds;
|
|
67
|
+
|
|
68
|
+
// 2. Profile (static or SSO).
|
|
69
|
+
const profileCreds = await readProfileCredentials(profile, region, signal);
|
|
70
|
+
if (profileCreds) return profileCreds;
|
|
71
|
+
|
|
72
|
+
// 3. EC2 IMDSv2.
|
|
73
|
+
if ($env.AWS_EC2_METADATA_DISABLED?.toLowerCase() !== "true") {
|
|
74
|
+
const imdsCreds = await readImdsCredentials(signal);
|
|
75
|
+
if (imdsCreds) return imdsCreds;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Unable to resolve AWS credentials. Set AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY, ` +
|
|
80
|
+
`or configure profile '${profile}' in ~/.aws/credentials (or ~/.aws/config for SSO).`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readEnvCredentials(): ResolvedCredentials | undefined {
|
|
85
|
+
const ak = $env.AWS_ACCESS_KEY_ID;
|
|
86
|
+
const sk = $env.AWS_SECRET_ACCESS_KEY;
|
|
87
|
+
if (!ak || !sk) return undefined;
|
|
88
|
+
const token = $env.AWS_SESSION_TOKEN;
|
|
89
|
+
return token
|
|
90
|
+
? { accessKeyId: ak, secretAccessKey: sk, sessionToken: token }
|
|
91
|
+
: { accessKeyId: ak, secretAccessKey: sk };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------- INI parsing ----------
|
|
95
|
+
|
|
96
|
+
/** Map of section name -> map of key -> value. Section names are stripped of
|
|
97
|
+
* any leading `profile ` (so `~/.aws/config` aligns with `~/.aws/credentials`). */
|
|
98
|
+
type IniFile = Record<string, Record<string, string>>;
|
|
99
|
+
|
|
100
|
+
function parseIni(text: string): IniFile {
|
|
101
|
+
const out: IniFile = {};
|
|
102
|
+
let current: Record<string, string> | null = null;
|
|
103
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
104
|
+
const line = rawLine.trim();
|
|
105
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
106
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
107
|
+
let name = line.slice(1, -1).trim();
|
|
108
|
+
if (name.startsWith("profile ")) name = name.slice(8).trim();
|
|
109
|
+
if (name.startsWith("sso-session ")) name = `sso-session:${name.slice(12).trim()}`;
|
|
110
|
+
let section = out[name];
|
|
111
|
+
if (!section) {
|
|
112
|
+
section = {};
|
|
113
|
+
out[name] = section;
|
|
114
|
+
}
|
|
115
|
+
current = section;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!current) continue;
|
|
119
|
+
const eq = line.indexOf("=");
|
|
120
|
+
if (eq === -1) continue;
|
|
121
|
+
current[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function readIniFile(p: string): Promise<IniFile | undefined> {
|
|
127
|
+
try {
|
|
128
|
+
const text = await fs.promises.readFile(p, "utf8");
|
|
129
|
+
return parseIni(text);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (isEnoent(err)) return undefined;
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------- Profile / SSO ----------
|
|
137
|
+
|
|
138
|
+
async function readProfileCredentials(
|
|
139
|
+
profile: string,
|
|
140
|
+
region: string,
|
|
141
|
+
signal: AbortSignal | undefined,
|
|
142
|
+
): Promise<ResolvedCredentials | undefined> {
|
|
143
|
+
const home = os.homedir();
|
|
144
|
+
const credentialsPath = $env.AWS_SHARED_CREDENTIALS_FILE || path.join(home, ".aws", "credentials");
|
|
145
|
+
const configPath = $env.AWS_CONFIG_FILE || path.join(home, ".aws", "config");
|
|
146
|
+
|
|
147
|
+
const credentialsIni = await readIniFile(credentialsPath);
|
|
148
|
+
const configIni = await readIniFile(configPath);
|
|
149
|
+
|
|
150
|
+
// Static credentials live in ~/.aws/credentials; SSO config lives in
|
|
151
|
+
// ~/.aws/config under `[profile foo]`. Merge into a single view.
|
|
152
|
+
const merged: Record<string, string> = { ...(configIni?.[profile] ?? {}), ...(credentialsIni?.[profile] ?? {}) };
|
|
153
|
+
if (Object.keys(merged).length === 0) return undefined;
|
|
154
|
+
|
|
155
|
+
if (merged.aws_access_key_id && merged.aws_secret_access_key) {
|
|
156
|
+
const out: ResolvedCredentials = {
|
|
157
|
+
accessKeyId: merged.aws_access_key_id,
|
|
158
|
+
secretAccessKey: merged.aws_secret_access_key,
|
|
159
|
+
};
|
|
160
|
+
if (merged.aws_session_token) out.sessionToken = merged.aws_session_token;
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (merged.sso_account_id && merged.sso_role_name) {
|
|
165
|
+
return readSsoCredentials(merged, configIni, region, signal);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (merged.credential_process) {
|
|
169
|
+
return readCredentialProcess(profile, merged.credential_process, signal);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface SsoCachedToken {
|
|
176
|
+
accessToken?: string;
|
|
177
|
+
expiresAt?: string;
|
|
178
|
+
startUrl?: string;
|
|
179
|
+
region?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function readSsoCredentials(
|
|
183
|
+
profileCfg: Record<string, string>,
|
|
184
|
+
configIni: IniFile | undefined,
|
|
185
|
+
defaultRegion: string,
|
|
186
|
+
signal: AbortSignal | undefined,
|
|
187
|
+
): Promise<ResolvedCredentials | undefined> {
|
|
188
|
+
// Two SSO profile shapes:
|
|
189
|
+
// - legacy: `sso_start_url` + `sso_region` directly on the profile
|
|
190
|
+
// - sso-session: `sso_session = my-session` references a `[sso-session my-session]` block
|
|
191
|
+
let startUrl = profileCfg.sso_start_url;
|
|
192
|
+
let ssoRegion = profileCfg.sso_region;
|
|
193
|
+
const sessionName = profileCfg.sso_session;
|
|
194
|
+
if (sessionName && configIni) {
|
|
195
|
+
const session = configIni[`sso-session:${sessionName}`];
|
|
196
|
+
if (session) {
|
|
197
|
+
startUrl = startUrl || session.sso_start_url;
|
|
198
|
+
ssoRegion = ssoRegion || session.sso_region;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!startUrl || !ssoRegion) return undefined;
|
|
202
|
+
|
|
203
|
+
const token = await loadSsoCachedToken(startUrl, sessionName);
|
|
204
|
+
if (!token?.accessToken) {
|
|
205
|
+
throw new Error(`AWS SSO token for ${startUrl} not found in ~/.aws/sso/cache. Run 'aws sso login' first.`);
|
|
206
|
+
}
|
|
207
|
+
const expiresAt = token.expiresAt ? Date.parse(token.expiresAt) : Number.POSITIVE_INFINITY;
|
|
208
|
+
if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
|
|
209
|
+
throw new Error(`AWS SSO token for ${startUrl} has expired. Run 'aws sso login' to refresh.`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const url =
|
|
213
|
+
`https://portal.sso.${ssoRegion}.amazonaws.com/federation/credentials` +
|
|
214
|
+
`?account_id=${encodeURIComponent(profileCfg.sso_account_id)}` +
|
|
215
|
+
`&role_name=${encodeURIComponent(profileCfg.sso_role_name)}`;
|
|
216
|
+
const response = await fetch(url, {
|
|
217
|
+
method: "GET",
|
|
218
|
+
headers: { "x-amz-sso_bearer_token": token.accessToken },
|
|
219
|
+
signal,
|
|
220
|
+
});
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
const body = await response.text().catch(() => "");
|
|
223
|
+
throw new Error(`AWS SSO GetRoleCredentials failed: ${response.status} ${body.slice(0, 200)}`);
|
|
224
|
+
}
|
|
225
|
+
const json = (await response.json()) as {
|
|
226
|
+
roleCredentials?: { accessKeyId: string; secretAccessKey: string; sessionToken: string; expiration: number };
|
|
227
|
+
};
|
|
228
|
+
const role = json.roleCredentials;
|
|
229
|
+
if (!role) throw new Error("AWS SSO GetRoleCredentials: missing roleCredentials in response");
|
|
230
|
+
|
|
231
|
+
// region is honored at the caller; we only consume defaultRegion to keep the
|
|
232
|
+
// param wired for symmetry with other resolution paths.
|
|
233
|
+
void defaultRegion;
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
accessKeyId: role.accessKeyId,
|
|
237
|
+
secretAccessKey: role.secretAccessKey,
|
|
238
|
+
sessionToken: role.sessionToken,
|
|
239
|
+
expiresAt: role.expiration,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function loadSsoCachedToken(
|
|
244
|
+
startUrl: string,
|
|
245
|
+
sessionName: string | undefined,
|
|
246
|
+
): Promise<SsoCachedToken | undefined> {
|
|
247
|
+
const cacheDir = path.join(os.homedir(), ".aws", "sso", "cache");
|
|
248
|
+
let entries: string[];
|
|
249
|
+
try {
|
|
250
|
+
entries = await fs.promises.readdir(cacheDir);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (isEnoent(err)) return undefined;
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
// Prefer the deterministic hash for legacy `sso_start_url` profiles or the
|
|
256
|
+
// session name for the newer `sso-session` shape; otherwise scan.
|
|
257
|
+
const candidates: string[] = [];
|
|
258
|
+
const hash = await sha1Hex(sessionName || startUrl);
|
|
259
|
+
candidates.push(`${hash}.json`);
|
|
260
|
+
for (const entry of entries) {
|
|
261
|
+
if (entry.endsWith(".json") && !candidates.includes(entry)) candidates.push(entry);
|
|
262
|
+
}
|
|
263
|
+
for (const file of candidates) {
|
|
264
|
+
if (!entries.includes(file)) continue;
|
|
265
|
+
try {
|
|
266
|
+
const text = await fs.promises.readFile(path.join(cacheDir, file), "utf8");
|
|
267
|
+
const parsed = JSON.parse(text) as SsoCachedToken;
|
|
268
|
+
if (parsed.startUrl === startUrl || (sessionName && file === `${hash}.json`)) {
|
|
269
|
+
return parsed;
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
logger.debug("aws-credentials: failed to read SSO cache", { file, err: String(err) });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function sha1Hex(input: string): Promise<string> {
|
|
279
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-1", new TextEncoder().encode(input));
|
|
280
|
+
const bytes = new Uint8Array(digest);
|
|
281
|
+
let out = "";
|
|
282
|
+
for (let i = 0; i < bytes.length; i++) out += bytes[i].toString(16).padStart(2, "0");
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------- credential_process ----------
|
|
287
|
+
|
|
288
|
+
/** JSON envelope emitted by an external credential process. Matches the
|
|
289
|
+
* AWS CLI / SDK contract documented at
|
|
290
|
+
* https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html */
|
|
291
|
+
interface CredentialProcessEnvelope {
|
|
292
|
+
Version?: number;
|
|
293
|
+
AccessKeyId?: string;
|
|
294
|
+
SecretAccessKey?: string;
|
|
295
|
+
SessionToken?: string;
|
|
296
|
+
Expiration?: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function readCredentialProcess(
|
|
300
|
+
profile: string,
|
|
301
|
+
command: string,
|
|
302
|
+
signal: AbortSignal | undefined,
|
|
303
|
+
): Promise<ResolvedCredentials> {
|
|
304
|
+
const argv = buildCredentialProcessArgv(profile, command);
|
|
305
|
+
|
|
306
|
+
const child = Bun.spawn(argv, {
|
|
307
|
+
stdin: "ignore",
|
|
308
|
+
stdout: "pipe",
|
|
309
|
+
stderr: "pipe",
|
|
310
|
+
windowsHide: true,
|
|
311
|
+
signal,
|
|
312
|
+
});
|
|
313
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
314
|
+
new Response(child.stdout).text(),
|
|
315
|
+
new Response(child.stderr).text(),
|
|
316
|
+
child.exited,
|
|
317
|
+
]);
|
|
318
|
+
if (exitCode !== 0) {
|
|
319
|
+
const tail = stderr.trim().slice(-512) || stdout.trim().slice(-512) || "(no output)";
|
|
320
|
+
throw new Error(`AWS credential_process for profile '${profile}' exited ${exitCode}: ${tail}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let parsed: CredentialProcessEnvelope;
|
|
324
|
+
try {
|
|
325
|
+
parsed = JSON.parse(stdout) as CredentialProcessEnvelope;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
throw new Error(`AWS credential_process for profile '${profile}' did not emit valid JSON: ${String(err)}`);
|
|
328
|
+
}
|
|
329
|
+
if (parsed.Version !== 1) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`AWS credential_process for profile '${profile}' returned unsupported Version ${parsed.Version ?? "<missing>"}; expected 1.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (!parsed.AccessKeyId || !parsed.SecretAccessKey) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`AWS credential_process for profile '${profile}' returned envelope without AccessKeyId/SecretAccessKey.`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const out: ResolvedCredentials = {
|
|
341
|
+
accessKeyId: parsed.AccessKeyId,
|
|
342
|
+
secretAccessKey: parsed.SecretAccessKey,
|
|
343
|
+
};
|
|
344
|
+
if (parsed.SessionToken) out.sessionToken = parsed.SessionToken;
|
|
345
|
+
if (parsed.Expiration) {
|
|
346
|
+
const exp = Date.parse(parsed.Expiration);
|
|
347
|
+
if (!Number.isNaN(exp)) out.expiresAt = exp;
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Resolve the argv for `Bun.spawn`. On Windows we route `.cmd`/`.bat` helpers
|
|
353
|
+
* through `cmd.exe /c` because direct execution refuses batch files (mirrors
|
|
354
|
+
* Node's `execFile` policy and avoids surprise no-ops). */
|
|
355
|
+
function buildCredentialProcessArgv(profile: string, command: string): string[] {
|
|
356
|
+
const tokens = tokenizeCredentialProcessCommand(command);
|
|
357
|
+
if (tokens.length === 0) {
|
|
358
|
+
throw new Error(`AWS credential_process for profile '${profile}' is empty.`);
|
|
359
|
+
}
|
|
360
|
+
if (process.platform === "win32" && isBatchScript(tokens[0])) {
|
|
361
|
+
return ["cmd.exe", "/d", "/s", "/c", command];
|
|
362
|
+
}
|
|
363
|
+
return tokens;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isBatchScript(executable: string): boolean {
|
|
367
|
+
const lower = executable.toLowerCase();
|
|
368
|
+
return lower.endsWith(".cmd") || lower.endsWith(".bat");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** POSIX-shell-style tokenizer used by the AWS CLI for `credential_process`.
|
|
372
|
+
*
|
|
373
|
+
* Outside quotes a backslash escapes the next character. Inside single quotes
|
|
374
|
+
* everything is literal (no escapes, cannot contain `'`). Inside double quotes
|
|
375
|
+
* a backslash only escapes `$`, `` ` ``, `"`, and `\` — every other backslash
|
|
376
|
+
* is preserved verbatim, which is what makes Windows paths like
|
|
377
|
+
* `"C:\Program Files\tool\auth.exe"` survive tokenization. */
|
|
378
|
+
export function tokenizeCredentialProcessCommand(cmd: string): string[] {
|
|
379
|
+
const tokens: string[] = [];
|
|
380
|
+
let current = "";
|
|
381
|
+
let hasToken = false;
|
|
382
|
+
let mode: "normal" | "single" | "double" = "normal";
|
|
383
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
384
|
+
const ch = cmd[i];
|
|
385
|
+
if (mode === "normal") {
|
|
386
|
+
if (ch === "'") {
|
|
387
|
+
mode = "single";
|
|
388
|
+
hasToken = true;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (ch === '"') {
|
|
392
|
+
mode = "double";
|
|
393
|
+
hasToken = true;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (ch === "\\" && i + 1 < cmd.length) {
|
|
397
|
+
current += cmd[++i];
|
|
398
|
+
hasToken = true;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
402
|
+
if (hasToken) {
|
|
403
|
+
tokens.push(current);
|
|
404
|
+
current = "";
|
|
405
|
+
hasToken = false;
|
|
406
|
+
}
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
current += ch;
|
|
410
|
+
hasToken = true;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (mode === "single") {
|
|
414
|
+
if (ch === "'") {
|
|
415
|
+
mode = "normal";
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
current += ch;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
// double-quote
|
|
422
|
+
if (ch === '"') {
|
|
423
|
+
mode = "normal";
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (ch === "\\" && i + 1 < cmd.length) {
|
|
427
|
+
const next = cmd[i + 1];
|
|
428
|
+
if (next === "$" || next === "`" || next === '"' || next === "\\") {
|
|
429
|
+
current += next;
|
|
430
|
+
i++;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
// Preserve literal backslash for Windows paths.
|
|
434
|
+
current += ch;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
current += ch;
|
|
438
|
+
}
|
|
439
|
+
if (mode !== "normal") {
|
|
440
|
+
throw new Error("AWS credential_process command has an unterminated quote.");
|
|
441
|
+
}
|
|
442
|
+
if (hasToken) tokens.push(current);
|
|
443
|
+
return tokens;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---------- IMDSv2 ----------
|
|
447
|
+
|
|
448
|
+
const IMDS_HOST = "169.254.169.254";
|
|
449
|
+
const IMDS_TIMEOUT_MS = 1000;
|
|
450
|
+
|
|
451
|
+
async function readImdsCredentials(parentSignal: AbortSignal | undefined): Promise<ResolvedCredentials | undefined> {
|
|
452
|
+
const timeout = AbortSignal.timeout(IMDS_TIMEOUT_MS);
|
|
453
|
+
const signal = parentSignal ? AbortSignal.any([parentSignal, timeout]) : timeout;
|
|
454
|
+
try {
|
|
455
|
+
const tokenRes = await fetch(`http://${IMDS_HOST}/latest/api/token`, {
|
|
456
|
+
method: "PUT",
|
|
457
|
+
headers: { "x-aws-ec2-metadata-token-ttl-seconds": "21600" },
|
|
458
|
+
signal,
|
|
459
|
+
});
|
|
460
|
+
if (!tokenRes.ok) return undefined;
|
|
461
|
+
const token = await tokenRes.text();
|
|
462
|
+
|
|
463
|
+
const roleRes = await fetch(`http://${IMDS_HOST}/latest/meta-data/iam/security-credentials/`, {
|
|
464
|
+
headers: { "x-aws-ec2-metadata-token": token },
|
|
465
|
+
signal,
|
|
466
|
+
});
|
|
467
|
+
if (!roleRes.ok) return undefined;
|
|
468
|
+
const role = (await roleRes.text()).trim();
|
|
469
|
+
if (!role) return undefined;
|
|
470
|
+
|
|
471
|
+
const credsRes = await fetch(
|
|
472
|
+
`http://${IMDS_HOST}/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`,
|
|
473
|
+
{
|
|
474
|
+
headers: { "x-aws-ec2-metadata-token": token },
|
|
475
|
+
signal,
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
if (!credsRes.ok) return undefined;
|
|
479
|
+
const body = (await credsRes.json()) as {
|
|
480
|
+
AccessKeyId?: string;
|
|
481
|
+
SecretAccessKey?: string;
|
|
482
|
+
Token?: string;
|
|
483
|
+
Expiration?: string;
|
|
484
|
+
};
|
|
485
|
+
if (!body.AccessKeyId || !body.SecretAccessKey) return undefined;
|
|
486
|
+
const out: ResolvedCredentials = {
|
|
487
|
+
accessKeyId: body.AccessKeyId,
|
|
488
|
+
secretAccessKey: body.SecretAccessKey,
|
|
489
|
+
};
|
|
490
|
+
if (body.Token) out.sessionToken = body.Token;
|
|
491
|
+
if (body.Expiration) out.expiresAt = Date.parse(body.Expiration);
|
|
492
|
+
return out;
|
|
493
|
+
} catch {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Test/diagnostic helper — drops cached credentials. */
|
|
499
|
+
export function clearAwsCredentialCache(): void {
|
|
500
|
+
cache.clear();
|
|
501
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `application/vnd.amazon.eventstream` decoder.
|
|
3
|
+
*
|
|
4
|
+
* Wire format (all integers big-endian):
|
|
5
|
+
*
|
|
6
|
+
* [total length u32]
|
|
7
|
+
* [headers length u32]
|
|
8
|
+
* [prelude CRC32 u32] <- CRC over the first 8 bytes
|
|
9
|
+
* [headers headers_length]
|
|
10
|
+
* [payload total_length - headers_length - 16]
|
|
11
|
+
* [message CRC32 u32] <- CRC over the entire message minus the trailing 4 bytes
|
|
12
|
+
*
|
|
13
|
+
* Headers: a sequence of `[name_len u8][name utf8][value_type u8][value …]`.
|
|
14
|
+
* We only need the typed values Bedrock emits (boolean true/false, byte, short,
|
|
15
|
+
* integer, long, byte-array, string, timestamp, uuid). All are surfaced as
|
|
16
|
+
* strings for ease of consumption — Bedrock only sets string-valued headers in
|
|
17
|
+
* practice (`:event-type`, `:message-type`, `:content-type`, `:exception-type`).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const PRELUDE_LEN = 8;
|
|
21
|
+
const PRELUDE_CRC_LEN = 4;
|
|
22
|
+
const MESSAGE_CRC_LEN = 4;
|
|
23
|
+
const HEADER_BLOCK_OFFSET = PRELUDE_LEN + PRELUDE_CRC_LEN;
|
|
24
|
+
const MIN_MESSAGE_LEN = HEADER_BLOCK_OFFSET + MESSAGE_CRC_LEN;
|
|
25
|
+
|
|
26
|
+
export interface EventStreamMessage {
|
|
27
|
+
/** Lower-cased copy is *not* applied — Bedrock uses casing like `:event-type` verbatim. */
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
payload: Uint8Array;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** CRC32 (IEEE / zlib polynomial 0xEDB88320), matches `@aws-crypto/crc32`. */
|
|
33
|
+
const CRC_TABLE = (() => {
|
|
34
|
+
const t = new Uint32Array(256);
|
|
35
|
+
for (let i = 0; i < 256; i++) {
|
|
36
|
+
let c = i;
|
|
37
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
38
|
+
t[i] = c >>> 0;
|
|
39
|
+
}
|
|
40
|
+
return t;
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
export function crc32(bytes: Uint8Array, seed = 0): number {
|
|
44
|
+
let c = (seed ^ 0xffffffff) >>> 0;
|
|
45
|
+
for (let i = 0; i < bytes.length; i++) c = (CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)) >>> 0;
|
|
46
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Decode a single, fully buffered eventstream message. Throws if the framing is
|
|
51
|
+
* malformed or either CRC mismatches. Used by both `decodeEventStream` (the
|
|
52
|
+
* streaming entry point) and the unit tests, which exercise it with hand-built
|
|
53
|
+
* frames.
|
|
54
|
+
*/
|
|
55
|
+
export function decodeMessage(frame: Uint8Array): EventStreamMessage {
|
|
56
|
+
if (frame.length < MIN_MESSAGE_LEN) throw new Error("eventstream: frame too short");
|
|
57
|
+
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
58
|
+
const total = view.getUint32(0, false);
|
|
59
|
+
if (total !== frame.length) throw new Error(`eventstream: framed length ${total} != buffer ${frame.length}`);
|
|
60
|
+
const headersLen = view.getUint32(4, false);
|
|
61
|
+
const preludeCrc = view.getUint32(8, false);
|
|
62
|
+
const computedPreludeCrc = crc32(frame.subarray(0, PRELUDE_LEN));
|
|
63
|
+
if (computedPreludeCrc !== preludeCrc) throw new Error("eventstream: prelude CRC mismatch");
|
|
64
|
+
const msgCrc = view.getUint32(total - MESSAGE_CRC_LEN, false);
|
|
65
|
+
const computedMsgCrc = crc32(frame.subarray(0, total - MESSAGE_CRC_LEN));
|
|
66
|
+
if (computedMsgCrc !== msgCrc) throw new Error("eventstream: message CRC mismatch");
|
|
67
|
+
|
|
68
|
+
const headersBytes = frame.subarray(HEADER_BLOCK_OFFSET, HEADER_BLOCK_OFFSET + headersLen);
|
|
69
|
+
const payload = frame.subarray(HEADER_BLOCK_OFFSET + headersLen, total - MESSAGE_CRC_LEN);
|
|
70
|
+
return { headers: parseHeaders(headersBytes), payload };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseHeaders(buf: Uint8Array): Record<string, string> {
|
|
74
|
+
const out: Record<string, string> = {};
|
|
75
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
76
|
+
const decoder = new TextDecoder();
|
|
77
|
+
let p = 0;
|
|
78
|
+
while (p < buf.length) {
|
|
79
|
+
const nameLen = view.getUint8(p);
|
|
80
|
+
p += 1;
|
|
81
|
+
const name = decoder.decode(buf.subarray(p, p + nameLen));
|
|
82
|
+
p += nameLen;
|
|
83
|
+
const type = view.getUint8(p);
|
|
84
|
+
p += 1;
|
|
85
|
+
switch (type) {
|
|
86
|
+
case 0: // bool true
|
|
87
|
+
out[name] = "true";
|
|
88
|
+
break;
|
|
89
|
+
case 1: // bool false
|
|
90
|
+
out[name] = "false";
|
|
91
|
+
break;
|
|
92
|
+
case 2: // byte
|
|
93
|
+
out[name] = String(view.getInt8(p));
|
|
94
|
+
p += 1;
|
|
95
|
+
break;
|
|
96
|
+
case 3: // short
|
|
97
|
+
out[name] = String(view.getInt16(p, false));
|
|
98
|
+
p += 2;
|
|
99
|
+
break;
|
|
100
|
+
case 4: // integer
|
|
101
|
+
out[name] = String(view.getInt32(p, false));
|
|
102
|
+
p += 4;
|
|
103
|
+
break;
|
|
104
|
+
case 5: // long — surface as decimal string to avoid precision loss
|
|
105
|
+
out[name] = bigIntFromBytes(buf.subarray(p, p + 8)).toString();
|
|
106
|
+
p += 8;
|
|
107
|
+
break;
|
|
108
|
+
case 6: {
|
|
109
|
+
// byte array — base64 for safe transport
|
|
110
|
+
const len = view.getUint16(p, false);
|
|
111
|
+
p += 2;
|
|
112
|
+
out[name] = Buffer.from(buf.buffer, buf.byteOffset + p, len).toString("base64");
|
|
113
|
+
p += len;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 7: {
|
|
117
|
+
// string
|
|
118
|
+
const len = view.getUint16(p, false);
|
|
119
|
+
p += 2;
|
|
120
|
+
out[name] = decoder.decode(buf.subarray(p, p + len));
|
|
121
|
+
p += len;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 8: // timestamp (ms since epoch as i64)
|
|
125
|
+
out[name] = new Date(Number(bigIntFromBytes(buf.subarray(p, p + 8)))).toISOString();
|
|
126
|
+
p += 8;
|
|
127
|
+
break;
|
|
128
|
+
case 9: {
|
|
129
|
+
// uuid
|
|
130
|
+
const u = buf.subarray(p, p + 16);
|
|
131
|
+
const hex: string[] = [];
|
|
132
|
+
for (let i = 0; i < 16; i++) hex.push(u[i].toString(16).padStart(2, "0"));
|
|
133
|
+
out[name] =
|
|
134
|
+
`${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
135
|
+
p += 16;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
default:
|
|
139
|
+
throw new Error(`eventstream: unknown header value type ${type}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function bigIntFromBytes(b: Uint8Array): bigint {
|
|
146
|
+
let v = 0n;
|
|
147
|
+
for (let i = 0; i < b.length; i++) v = (v << 8n) | BigInt(b[i]);
|
|
148
|
+
// sign-extend (two's complement)
|
|
149
|
+
if (b.length === 8 && b[0] & 0x80) v -= 1n << 64n;
|
|
150
|
+
return v;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Async generator that consumes a `ReadableStream<Uint8Array>` (e.g. a fetch
|
|
155
|
+
* response body) and yields fully-framed messages. Handles arbitrary chunk
|
|
156
|
+
* boundaries: messages may span multiple chunks, and a single chunk may carry
|
|
157
|
+
* many messages.
|
|
158
|
+
*/
|
|
159
|
+
export async function* decodeEventStream(source: ReadableStream<Uint8Array>): AsyncGenerator<EventStreamMessage> {
|
|
160
|
+
const reader = source.getReader();
|
|
161
|
+
// Single growable buffer; we slide a read cursor along it and compact when a
|
|
162
|
+
// complete prefix has been consumed. Avoids per-message Uint8Array copies.
|
|
163
|
+
let buf: Uint8Array<ArrayBufferLike> = new Uint8Array(0);
|
|
164
|
+
try {
|
|
165
|
+
while (true) {
|
|
166
|
+
const { value, done } = await reader.read();
|
|
167
|
+
if (value && value.length > 0) buf = buf.length === 0 ? value : Buffer.concat([buf, value]);
|
|
168
|
+
let offset = 0;
|
|
169
|
+
while (buf.length - offset >= 4) {
|
|
170
|
+
const dv = new DataView(buf.buffer, buf.byteOffset + offset, buf.length - offset);
|
|
171
|
+
const total = dv.getUint32(0, false);
|
|
172
|
+
if (total < MIN_MESSAGE_LEN) throw new Error(`eventstream: total length ${total} below minimum`);
|
|
173
|
+
if (buf.length - offset < total) break;
|
|
174
|
+
const frame = buf.subarray(offset, offset + total);
|
|
175
|
+
yield decodeMessage(frame);
|
|
176
|
+
offset += total;
|
|
177
|
+
}
|
|
178
|
+
if (offset > 0) buf = buf.slice(offset);
|
|
179
|
+
if (done) break;
|
|
180
|
+
}
|
|
181
|
+
if (buf.length > 0) throw new Error("eventstream: truncated message at end of stream");
|
|
182
|
+
} finally {
|
|
183
|
+
reader.releaseLock();
|
|
184
|
+
}
|
|
185
|
+
}
|