@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.
- package/dist/sts/issue.d.ts +3 -4
- package/dist/sts/issue.js +77 -74
- package/package.json +1 -1
package/dist/sts/issue.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AwsCredentialResult } from "../types";
|
|
2
|
-
export
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
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
|
-
// ──
|
|
73
|
-
function
|
|
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 =
|
|
79
|
+
const sessionTtl = resolveSessionTtlByProfile(profile);
|
|
81
80
|
const normalizedCert = normalizeCert(certBase64);
|
|
82
|
-
// BUG 5 KEPT: isolate-level cert serial cache
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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) {
|