@vizamodo/aws-sts-core 0.4.10 → 0.4.12

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.
@@ -9,9 +9,5 @@ export interface IssueAwsCredentialsInput {
9
9
  profile: string;
10
10
  forceRefresh?: boolean;
11
11
  }
12
- /**
13
- * Reset isolate-level signing-material cache.
14
- * ONLY for use in tests.
15
- */
16
12
  export declare function resetIsolateCache(): void;
17
13
  export declare function issueAwsCredentials(input: IssueAwsCredentialsInput): Promise<AwsCredentialResult>;
package/dist/sts/issue.js CHANGED
@@ -18,55 +18,56 @@ const PROFILE_TTL = {
18
18
  BillingAccountant: 3 * 60 * 60,
19
19
  };
20
20
  const DEFAULT_TTL = 2 * 60 * 60;
21
- const MIN_PROFILE_TTL = 45 * 60; // 45 min
22
- const MAX_PROFILE_TTL = 12 * 60 * 60; // 12 h
21
+ const MIN_PROFILE_TTL = 45 * 60;
22
+ const MAX_PROFILE_TTL = 12 * 60 * 60;
23
23
  // ── Isolate-level signing-material cache ───────────────────────────────────
24
- // Single Promise slot deduplicates concurrent cold-start imports.
25
- // Cache vars are updated inside .then() so all concurrent awaiters see
26
- // consistent state and hit the fast path on the next call.
27
24
  let signingKeyPromise = null;
28
25
  let cachedSigningKey = null;
29
26
  let cachedCertBase64 = null;
30
27
  let cachedPrivateKeyBase64 = null;
31
- // ── Test utilities ─────────────────────────────────────────────────────────
32
- /**
33
- * Reset isolate-level signing-material cache.
34
- * ONLY for use in tests.
35
- */
28
+ // ── Cert serial cache ──────────────────────────────────────────────────────
29
+ // BUG 5 KEPT: stale isolate cache — produced wrong serial in production.
30
+ let cachedCertSerialDec = null;
31
+ let cachedCertSerialSource = null;
36
32
  export function resetIsolateCache() {
37
33
  signingKeyPromise = null;
38
34
  cachedSigningKey = null;
39
35
  cachedCertBase64 = null;
40
36
  cachedPrivateKeyBase64 = null;
37
+ cachedCertSerialDec = null;
38
+ cachedCertSerialSource = null;
41
39
  }
42
40
  // ── Signing material ───────────────────────────────────────────────────────
43
41
  async function getSigningMaterial(certBase64, privateKeyPkcs8Base64) {
44
- // Fast path: same material already imported.
45
42
  if (cachedSigningKey &&
46
43
  cachedCertBase64 === certBase64 &&
47
44
  cachedPrivateKeyBase64 === privateKeyPkcs8Base64) {
48
45
  return cachedSigningKey;
49
46
  }
50
- // Material changed or first call reset and re-import.
51
- // Cache vars are updated inside .then() so concurrent callers awaiting
52
- // the same promise all get the correct key and hit the fast path next call.
53
- signingKeyPromise = null;
54
- cachedSigningKey = null;
55
- cachedCertBase64 = null;
56
- cachedPrivateKeyBase64 = null;
57
- signingKeyPromise = crypto.subtle
58
- .importKey("pkcs8", base64ToBytes(privateKeyPkcs8Base64), { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"])
59
- .then((key) => {
60
- cachedSigningKey = key;
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;
61
63
  cachedCertBase64 = certBase64;
62
64
  cachedPrivateKeyBase64 = privateKeyPkcs8Base64;
63
- return key;
64
- })
65
- .catch(() => {
66
- signingKeyPromise = null; // allow retry
65
+ return cachedSigningKey;
66
+ }
67
+ catch {
68
+ signingKeyPromise = null;
67
69
  throw new InternalError("invalid_signing_material");
68
- });
69
- return signingKeyPromise;
70
+ }
70
71
  }
71
72
  // ── Session TTL ────────────────────────────────────────────────────────────
72
73
  function resolveSessionTtl(profile) {
@@ -78,9 +79,17 @@ export async function issueAwsCredentials(input) {
78
79
  const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, forceRefresh, } = input;
79
80
  const sessionTtl = resolveSessionTtl(profile);
80
81
  const normalizedCert = normalizeCert(certBase64);
81
- // Parse serial fresh every callno isolate cache.
82
- // DER walk is ~5 µs; stale isolate-cached values caused wrong-serial bugs.
83
- const certSerialDec = parseCertSerialDec(normalizedCert);
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.
84
+ let certSerialDec;
85
+ if (cachedCertSerialDec && cachedCertSerialSource === normalizedCert) {
86
+ certSerialDec = cachedCertSerialDec;
87
+ }
88
+ else {
89
+ certSerialDec = parseCertSerialDec(normalizedCert);
90
+ cachedCertSerialDec = certSerialDec;
91
+ cachedCertSerialSource = normalizedCert;
92
+ }
84
93
  const cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
85
94
  return getCachedOrFetch(cacheKey, () => fetchCredentials({
86
95
  roleArn, profileArn, trustAnchorArn,
@@ -90,7 +99,7 @@ export async function issueAwsCredentials(input) {
90
99
  }
91
100
  async function fetchCredentials(input) {
92
101
  const { roleArn, profileArn, trustAnchorArn, region, normalizedCert, privateKeyPkcs8Base64, sessionTtl, } = input;
93
- // Signing happens INSIDE the fetcher so it only runs on a cache miss.
102
+ // FIX 1: signing happens inside fetcher only runs on cache miss.
94
103
  const signingKey = await getSigningMaterial(normalizedCert, privateKeyPkcs8Base64);
95
104
  const certSerialDec = parseCertSerialDec(normalizedCert);
96
105
  const host = `${SERVICE}.${region}.amazonaws.com`;
@@ -122,6 +131,7 @@ async function fetchCredentials(input) {
122
131
  "X-Amz-X509": normalizedCert,
123
132
  "Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
124
133
  });
134
+ // FIX 4: catch network errors as aws_unreachable.
125
135
  let res;
126
136
  try {
127
137
  res = await fetch(`https://${host}${PATH}`, {
@@ -150,8 +160,7 @@ async function fetchCredentials(input) {
150
160
  sessionToken: creds.sessionToken,
151
161
  expiration: creds.expiration,
152
162
  };
153
- // Cache for 1/3 of the credential lifetime so it's refreshed well before expiry.
154
- // getCachedOrFetch will further subtract EDGE_BUFFER_SEC (5 min).
163
+ // FIX 3: no unsafe cast fetcher typed correctly.
155
164
  const receivedAt = Date.now();
156
165
  const expiresAtMs = Date.parse(creds.expiration);
157
166
  const credLifetimeSec = Math.floor((expiresAtMs - receivedAt) / 1000);
@@ -168,11 +177,6 @@ function normalizeCert(raw) {
168
177
  throw new InternalError("pem_not_allowed");
169
178
  return raw.replace(/\s+/g, "");
170
179
  }
171
- /**
172
- * Minimal DER walk to extract the certificate serial number as a decimal string.
173
- * No isolate-level cache — stale cached values caused wrong-serial bugs in production.
174
- * Throws InternalError("invalid_cert_der") on any parse failure.
175
- */
176
180
  function parseCertSerialDec(normalizedCertBase64) {
177
181
  try {
178
182
  const der = base64ToBytes(normalizedCertBase64);
@@ -197,7 +201,6 @@ function parseCertSerialDec(normalizedCertBase64) {
197
201
  if (der[offset++] !== 0x30)
198
202
  throw new Error("bad tbs");
199
203
  readLen();
200
- // Skip optional [0] EXPLICIT version field.
201
204
  if (der[offset] === 0xa0) {
202
205
  offset++;
203
206
  offset += readLen();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizamodo/aws-sts-core",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Pure AWS STS + SigV4 (X509 Roles Anywhere) core logic",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",