{"record":{"id":"1ec45ac0442d0ea6","repo":"can1357/oh-my-pi","slug":"source-response-has-a-missing-or-invalid-expira","errorCode":null,"errorMessage":"${source} response has a missing or invalid Expiration.","messagePattern":"(.+?) response has a missing or invalid Expiration\\.","errorType":"exception","errorClass":"AwsCredentialsError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/providers/aws-credentials.ts","lineNumber":69,"sourceCode":" * TTL for file-sourced credentials that carry a session token but no expiry.\n * Tools like aws-vault/saml2aws rewrite ~/.aws/credentials with short-lived STS\n * session keys; caching them forever serves stale creds after rotation.\n */\nconst FILE_SESSION_CREDS_TTL_MS = 5 * 60_000;\n/**\n * Bound for the detached (signal-free) shared resolution: a hung\n * credential_process/SSO/IMDS fetch must not pin the inflight slot forever.\n */\nconst SHARED_RESOLVE_TIMEOUT_MS = 30_000;\n\nfunction requireDynamicCredentialExpiration(\n\tvalue: string | undefined,\n\tsource: string,\n\tkind: AIError.AwsCredentialsErrorKind,\n): number {\n\tconst expiresAt = value ? Date.parse(value) : Number.NaN;\n\tif (Number.isFinite(expiresAt)) return expiresAt;\n\tthrow new AIError.AwsCredentialsError(`${source} response has a missing or invalid Expiration.`, kind);\n}\n\n/** Credential-process expiry is optional; missing/malformed values disable caching. */\nfunction dynamicCredentialExpiration(value: string | undefined): number {\n\tif (!value) return Date.now();\n\tconst expiresAt = Date.parse(value);\n\treturn Number.isFinite(expiresAt) ? expiresAt : Date.now();\n}\n\ninterface CacheEntry {\n\tcreds: ResolvedCredentials;\n\texpiresAt: number;\n}\n\nconst cache: Map<string, CacheEntry> = new Map();\nconst inflight: Map<string, Promise<ResolvedCredentials>> = new Map();\n\nfunction credentialCacheKey(profile: string, region: string, loadSharedConfig: boolean): string {","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/aws-credentials.ts#L51-L87","documentation":"AIError.AwsCredentialsError (kind from the caller: container/task credentials or similar) thrown by requireDynamicCredentialExpiration in packages/ai/src/providers/aws-credentials.ts. Some AWS credential sources (ECS container credentials, EC2 IMDS) are required to include an Expiration field; this helper parses it with Date.parse and throws if it is absent or unparseable, because expiry-based caching and refresh cannot work without it.","triggerScenarios":"readContainerCredentials (ECS/HTTP credentials endpoint) or expiresAt receives a credentials response whose Expiration field is missing, empty, or not a parseable ISO-8601 date string, e.g. a mocked/misconfigured AWS_CONTAINER_CREDENTIALS_RELATIVE_URI endpoint returning JSON without Expiration.","commonSituations":"Localstack or a corporate proxy replacing the ECS metadata endpoint returns minimal JSON without Expiration; a custom credentials proxy emits a non-ISO date format; an IMDS/ECS endpoint behind a gateway returns an error page or partial JSON; clock/encoding issues aside, typo'd field name in a hand-rolled credentials server.","solutions":["Fix the credentials endpoint to return a valid Expiration field in ISO-8601 format (e.g. \"2026-08-30T12:00:00Z\")","If using a custom/mock ECS credentials server, add the Expiration field to its JSON response","Bypass the broken container-credentials path by providing static credentials via AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or an AWS profile","Check that AWS_CONTAINER_CREDENTIALS_RELATIVE_URI / AWS_CONTAINER_CREDENTIALS_FULL_URI points at the real ECS/EKS metadata service, not a stub"],"exampleFix":"// before (custom ECS credentials endpoint response)\n{ \"AccessKeyId\": \"AKIA...\", \"SecretAccessKey\": \"...\", \"Token\": \"...\" }\n// after\n{ \"AccessKeyId\": \"AKIA...\", \"SecretAccessKey\": \"...\", \"Token\": \"...\", \"Expiration\": \"2026-08-30T12:00:00Z\" }","handlingStrategy":"fallback","validationCode":"async function hasParseableExpiration(credsUrl: string): Promise<boolean> {\n  try {\n    const json = await fetch(credsUrl).then(r => r.json());\n    return typeof json.Expiration === \"string\" && !Number.isNaN(Date.parse(json.Expiration));\n  } catch {\n    return false;\n  }\n}","typeGuard":"function hasValidExpiration(v: unknown): v is string {\n  return typeof v === \"string\" && Number.isFinite(Date.parse(v));\n}","tryCatchPattern":"try {\n  const result = await session.prompt(bedrockModel, messages);\n} catch (err) {\n  if (err instanceof AIError.AwsCredentialsError && err.message.includes(\"invalid Expiration\")) {\n    // fall back to static env credentials or fix the container credentials endpoint\n    logger.error(\"credentials endpoint returned bad Expiration\", {});\n  }\n  throw err;\n}","preventionTips":["When mocking or proxying ECS/IMDS credentials endpoints, always include an ISO-8601 Expiration field","Point AWS_CONTAINER_CREDENTIALS_* at the real metadata service, not hand-rolled stubs","Prefer static env keys or a profile in local/dev environments where no metadata service exists","Validate a custom credentials proxy's JSON schema before deploying it"],"tags":["aws","credentials","expiration","ecs","metadata"],"backgroundTag":"aws-credentials-expiration-invalid","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}