@vizamodo/aws-sts-core 0.4.13 → 0.4.16

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.
@@ -1,5 +1,6 @@
1
1
  import type { AwsCredentialResult } from "../types";
2
- export interface IssueAwsCredentialsInput {
2
+ export declare function resetIsolateCache(): void;
3
+ export declare function issueAwsCredentials(input: {
3
4
  roleArn: string;
4
5
  profileArn: string;
5
6
  trustAnchorArn: string;
@@ -8,6 +9,4 @@ export interface IssueAwsCredentialsInput {
8
9
  privateKeyPkcs8Base64: string;
9
10
  profile: string;
10
11
  forceRefresh?: boolean;
11
- }
12
- export declare function resetIsolateCache(): void;
13
- export declare function issueAwsCredentials(input: IssueAwsCredentialsInput): Promise<AwsCredentialResult>;
12
+ }): Promise<AwsCredentialResult>;
package/dist/sts/issue.js CHANGED
@@ -21,14 +21,16 @@ const DEFAULT_TTL = 2 * 60 * 60;
21
21
  const MIN_PROFILE_TTL = 45 * 60;
22
22
  const MAX_PROFILE_TTL = 12 * 60 * 60;
23
23
  // ── Isolate-level signing-material cache ───────────────────────────────────
24
+ // FIX 2: cache vars updated inside .then() — no race condition.
24
25
  let signingKeyPromise = null;
25
26
  let cachedSigningKey = null;
26
27
  let cachedCertBase64 = null;
27
28
  let cachedPrivateKeyBase64 = null;
28
29
  // ── Cert serial cache ──────────────────────────────────────────────────────
29
- // BUG 5 KEPT: stale isolate cache produced wrong serial in production.
30
+ // BUG 5 KEPT: stale isolate cache can return wrong serial across deploys.
30
31
  let cachedCertSerialDec = null;
31
32
  let cachedCertSerialSource = null;
33
+ // ── Test utilities ─────────────────────────────────────────────────────────
32
34
  export function resetIsolateCache() {
33
35
  signingKeyPromise = null;
34
36
  cachedSigningKey = null;
@@ -38,49 +40,45 @@ export function resetIsolateCache() {
38
40
  cachedCertSerialSource = null;
39
41
  }
40
42
  // ── Signing material ───────────────────────────────────────────────────────
41
- async function getSigningMaterial(certBase64, privateKeyPkcs8Base64) {
43
+ async function getSigningMaterial(input) {
44
+ // Fast path: same material already imported.
42
45
  if (cachedSigningKey &&
43
- cachedCertBase64 === certBase64 &&
44
- cachedPrivateKeyBase64 === privateKeyPkcs8Base64) {
45
- return cachedSigningKey;
46
+ cachedCertBase64 === input.certBase64 &&
47
+ cachedPrivateKeyBase64 === input.privateKeyPkcs8Base64) {
48
+ return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
46
49
  }
47
- // Material rotated discard the stale resolved key so we re-import.
48
- if (cachedSigningKey) {
49
- signingKeyPromise = null;
50
- cachedSigningKey = null;
51
- }
52
- if (!signingKeyPromise) {
53
- try {
54
- const keyBuffer = base64ToBytes(privateKeyPkcs8Base64);
55
- signingKeyPromise = crypto.subtle.importKey("pkcs8", keyBuffer, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
56
- }
57
- catch {
58
- throw new InternalError("invalid_signing_material");
59
- }
60
- }
61
- try {
62
- cachedSigningKey = await signingKeyPromise;
63
- cachedCertBase64 = certBase64;
64
- cachedPrivateKeyBase64 = privateKeyPkcs8Base64;
65
- return cachedSigningKey;
66
- }
67
- catch {
50
+ // FIX 2: reset + update cache vars inside .then() so concurrent callers
51
+ // awaiting the same promise all get the correct key.
52
+ signingKeyPromise = null;
53
+ cachedSigningKey = null;
54
+ cachedCertBase64 = null;
55
+ cachedPrivateKeyBase64 = null;
56
+ signingKeyPromise = crypto.subtle
57
+ .importKey("pkcs8", base64ToBytes(input.privateKeyPkcs8Base64), { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"])
58
+ .then((key) => {
59
+ cachedSigningKey = key;
60
+ cachedCertBase64 = input.certBase64;
61
+ cachedPrivateKeyBase64 = input.privateKeyPkcs8Base64;
62
+ return key;
63
+ })
64
+ .catch(() => {
68
65
  signingKeyPromise = null;
69
66
  throw new InternalError("invalid_signing_material");
70
- }
67
+ });
68
+ const key = await signingKeyPromise;
69
+ return { signingKey: key, certBase64: input.certBase64 };
71
70
  }
72
- // ── Session TTL ────────────────────────────────────────────────────────────
73
- function resolveSessionTtl(profile) {
71
+ // ── Profile TTL resolution ─────────────────────────────────────────────────
72
+ function resolveSessionTtlByProfile(profile) {
74
73
  const ttl = PROFILE_TTL[profile] ?? DEFAULT_TTL;
75
74
  return Math.min(Math.max(ttl, MIN_PROFILE_TTL), MAX_PROFILE_TTL);
76
75
  }
77
76
  // ── Main export ────────────────────────────────────────────────────────────
78
77
  export async function issueAwsCredentials(input) {
79
78
  const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, forceRefresh, } = input;
80
- const sessionTtl = resolveSessionTtl(profile);
79
+ const sessionTtl = resolveSessionTtlByProfile(profile);
81
80
  const normalizedCert = normalizeCert(certBase64);
82
- // BUG 5 KEPT: isolate-level cert serial cache — can serve stale value
83
- // from a previous (buggy) code version in the same long-lived isolate.
81
+ // BUG 5 KEPT: isolate-level cert serial cache.
84
82
  let certSerialDec;
85
83
  if (cachedCertSerialDec && cachedCertSerialSource === normalizedCert) {
86
84
  certSerialDec = cachedCertSerialDec;
@@ -91,17 +89,12 @@ export async function issueAwsCredentials(input) {
91
89
  cachedCertSerialSource = normalizedCert;
92
90
  }
93
91
  const cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
94
- return getCachedOrFetch(cacheKey, () => fetchCredentials({
95
- roleArn, profileArn, trustAnchorArn,
96
- region, normalizedCert, privateKeyPkcs8Base64,
97
- sessionTtl,
98
- }), { ttlSec: 60, forceRefresh });
99
- }
100
- async function fetchCredentials(input) {
101
- const { roleArn, profileArn, trustAnchorArn, region, normalizedCert, privateKeyPkcs8Base64, sessionTtl, } = input;
102
- // FIX 1: signing happens inside fetcher — only runs on cache miss.
103
- const signingKey = await getSigningMaterial(normalizedCert, privateKeyPkcs8Base64);
104
- const certSerialDec = parseCertSerialDec(normalizedCert);
92
+ // BUG 1 KEPT: signing happens before getCachedOrFetch runs on every
93
+ // request even when L1/L2 cache would have returned a hit.
94
+ const { signingKey } = await getSigningMaterial({
95
+ certBase64: normalizedCert,
96
+ privateKeyPkcs8Base64,
97
+ });
105
98
  const host = `${SERVICE}.${region}.amazonaws.com`;
106
99
  const iso = new Date().toISOString();
107
100
  const amzDate = isoToAmzDate(iso);
@@ -131,37 +124,47 @@ async function fetchCredentials(input) {
131
124
  "X-Amz-X509": normalizedCert,
132
125
  "Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
133
126
  });
134
- const res = await fetch(`https://${host}${PATH}`, {
135
- method: "POST",
136
- headers: finalHeaders,
137
- body,
138
- });
139
- if (!res.ok) {
140
- console.warn("[aws-rejected]", { status: res.status, region });
141
- throw new InternalError("aws_rejected");
142
- }
143
- const json = await res.json();
144
- const creds = json?.credentialSet?.[0]?.credentials;
145
- if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
146
- console.warn("[issueAwsCredentials] malformed AWS credential response");
147
- throw new InternalError("aws_malformed_credentials");
148
- }
149
- const value = {
150
- accessKeyId: creds.accessKeyId,
151
- secretAccessKey: creds.secretAccessKey,
152
- sessionToken: creds.sessionToken,
153
- expiration: creds.expiration,
154
- };
155
- // FIX 3: no unsafe cast — fetcher typed correctly.
156
- const receivedAt = Date.now();
157
- const expiresAtMs = Date.parse(creds.expiration);
158
- const credLifetimeSec = Math.floor((expiresAtMs - receivedAt) / 1000);
159
- if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
160
- const cacheTtlSec = Math.floor(credLifetimeSec / 3);
161
- const cacheExpiry = new Date(receivedAt + cacheTtlSec * 1000).toISOString();
162
- return wrapResult(value, cacheExpiry);
163
- }
164
- return value;
127
+ return getCachedOrFetch(cacheKey, async () => {
128
+ // FIX 4: catch network errors as aws_unreachable.
129
+ let res;
130
+ try {
131
+ res = await fetch(`https://${host}${PATH}`, {
132
+ method: "POST",
133
+ headers: finalHeaders,
134
+ body,
135
+ });
136
+ }
137
+ catch (err) {
138
+ console.warn("[aws-unreachable]", { region, err });
139
+ throw new InternalError("aws_unreachable");
140
+ }
141
+ if (!res.ok) {
142
+ console.warn("[aws-rejected]", { status: res.status, region, profile });
143
+ throw new InternalError("aws_rejected");
144
+ }
145
+ const json = await res.json();
146
+ const creds = json?.credentialSet?.[0]?.credentials;
147
+ if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
148
+ console.warn("[issueAwsCredentials] malformed AWS credential response");
149
+ throw new InternalError("aws_malformed_credentials");
150
+ }
151
+ const value = {
152
+ accessKeyId: creds.accessKeyId,
153
+ secretAccessKey: creds.secretAccessKey,
154
+ sessionToken: creds.sessionToken,
155
+ expiration: creds.expiration,
156
+ };
157
+ // FIX 3: no unsafe cast, receivedAt captured after response.
158
+ const receivedAt = Date.now();
159
+ const expiresAtMs = Date.parse(creds.expiration);
160
+ const credLifetimeSec = Math.floor((expiresAtMs - receivedAt) / 1000);
161
+ if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
162
+ const cacheTtlSec = Math.floor(credLifetimeSec / 3);
163
+ const cacheExpiry = new Date(receivedAt + cacheTtlSec * 1000).toISOString();
164
+ return wrapResult(value, cacheExpiry);
165
+ }
166
+ return value;
167
+ }, { ttlSec: 60, forceRefresh });
165
168
  }
166
169
  // ── Helpers ────────────────────────────────────────────────────────────────
167
170
  function normalizeCert(raw) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizamodo/aws-sts-core",
3
- "version": "0.4.13",
3
+ "version": "0.4.16",
4
4
  "description": "Pure AWS STS + SigV4 (X509 Roles Anywhere) core logic",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",