@vizamodo/aws-sts-core 0.4.7 → 0.4.10
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 +2 -5
- package/dist/sts/issue.js +40 -120
- package/package.json +1 -1
package/dist/sts/issue.d.ts
CHANGED
|
@@ -10,11 +10,8 @@ export interface IssueAwsCredentialsInput {
|
|
|
10
10
|
forceRefresh?: boolean;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Reset isolate-level signing
|
|
14
|
-
* ONLY for use in tests
|
|
15
|
-
*
|
|
16
|
-
* L1 cache is bypassed in tests via `forceRefresh: true` on each call —
|
|
17
|
-
* there is no need to expose a cache-clear API from the core library.
|
|
13
|
+
* Reset isolate-level signing-material cache.
|
|
14
|
+
* ONLY for use in tests.
|
|
18
15
|
*/
|
|
19
16
|
export declare function resetIsolateCache(): void;
|
|
20
17
|
export declare function issueAwsCredentials(input: IssueAwsCredentialsInput): Promise<AwsCredentialResult>;
|
package/dist/sts/issue.js
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
+
import { getCachedOrFetch, wrapResult } from "@vizamodo/edge-cache-core";
|
|
1
2
|
import { canonicalizeHeaders } from "../sigv4/headers";
|
|
2
3
|
import { buildCanonicalRequest } from "../sigv4/canonical";
|
|
3
4
|
import { buildStringToSign } from "../sigv4/string-to-sign";
|
|
4
5
|
import { signStringToSign } from "./signer";
|
|
5
6
|
import { InternalError } from "./errors";
|
|
6
7
|
import { sha256Hex } from "../crypto/sha256";
|
|
7
|
-
import { getCachedOrFetch, wrapResult } from "@vizamodo/edge-cache-core";
|
|
8
8
|
// ── Constants ──────────────────────────────────────────────────────────────
|
|
9
9
|
const ALGORITHM = "AWS4-X509-ECDSA-SHA256";
|
|
10
10
|
const SERVICE = "rolesanywhere";
|
|
11
11
|
const PATH = "/sessions";
|
|
12
|
-
/**
|
|
13
|
-
* Per-profile AWS session duration (seconds).
|
|
14
|
-
* Clamped to [MIN_SESSION_TTL, MAX_SESSION_TTL] at resolution time.
|
|
15
|
-
*/
|
|
16
12
|
const PROFILE_TTL = {
|
|
17
13
|
Runtime: 45 * 60,
|
|
18
14
|
ConsoleReadOnly: 12 * 60 * 60,
|
|
@@ -21,46 +17,39 @@ const PROFILE_TTL = {
|
|
|
21
17
|
BillingAdmin: 3 * 60 * 60,
|
|
22
18
|
BillingAccountant: 3 * 60 * 60,
|
|
23
19
|
};
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
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
|
|
27
23
|
// ── Isolate-level signing-material cache ───────────────────────────────────
|
|
28
|
-
// Single Promise slot
|
|
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.
|
|
29
27
|
let signingKeyPromise = null;
|
|
30
28
|
let cachedSigningKey = null;
|
|
31
29
|
let cachedCertBase64 = null;
|
|
32
30
|
let cachedPrivateKeyBase64 = null;
|
|
33
|
-
// ── Isolate-level cert-serial cache ───────────────────────────────────────
|
|
34
|
-
// DER walk is CPU-bound; the cert rarely rotates within an isolate lifetime.
|
|
35
|
-
let cachedCertSerialDec = null;
|
|
36
|
-
let cachedCertSerialSource = null;
|
|
37
31
|
// ── Test utilities ─────────────────────────────────────────────────────────
|
|
38
32
|
/**
|
|
39
|
-
* Reset isolate-level signing
|
|
40
|
-
* ONLY for use in tests
|
|
41
|
-
*
|
|
42
|
-
* L1 cache is bypassed in tests via `forceRefresh: true` on each call —
|
|
43
|
-
* there is no need to expose a cache-clear API from the core library.
|
|
33
|
+
* Reset isolate-level signing-material cache.
|
|
34
|
+
* ONLY for use in tests.
|
|
44
35
|
*/
|
|
45
36
|
export function resetIsolateCache() {
|
|
46
37
|
signingKeyPromise = null;
|
|
47
38
|
cachedSigningKey = null;
|
|
48
39
|
cachedCertBase64 = null;
|
|
49
40
|
cachedPrivateKeyBase64 = null;
|
|
50
|
-
cachedCertSerialDec = null;
|
|
51
|
-
cachedCertSerialSource = null;
|
|
52
41
|
}
|
|
53
42
|
// ── Signing material ───────────────────────────────────────────────────────
|
|
54
43
|
async function getSigningMaterial(certBase64, privateKeyPkcs8Base64) {
|
|
55
|
-
// Fast path: same material already imported
|
|
44
|
+
// Fast path: same material already imported.
|
|
56
45
|
if (cachedSigningKey &&
|
|
57
46
|
cachedCertBase64 === certBase64 &&
|
|
58
47
|
cachedPrivateKeyBase64 === privateKeyPkcs8Base64) {
|
|
59
48
|
return cachedSigningKey;
|
|
60
49
|
}
|
|
61
50
|
// Material changed or first call — reset and re-import.
|
|
62
|
-
// Cache vars are updated inside .then() so concurrent callers
|
|
63
|
-
//
|
|
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.
|
|
64
53
|
signingKeyPromise = null;
|
|
65
54
|
cachedSigningKey = null;
|
|
66
55
|
cachedCertBase64 = null;
|
|
@@ -68,32 +57,30 @@ async function getSigningMaterial(certBase64, privateKeyPkcs8Base64) {
|
|
|
68
57
|
signingKeyPromise = crypto.subtle
|
|
69
58
|
.importKey("pkcs8", base64ToBytes(privateKeyPkcs8Base64), { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"])
|
|
70
59
|
.then((key) => {
|
|
71
|
-
// Update cache atomically after successful import.
|
|
72
|
-
// All concurrent callers awaiting this promise will see
|
|
73
|
-
// consistent state and hit the fast path on next call.
|
|
74
60
|
cachedSigningKey = key;
|
|
75
61
|
cachedCertBase64 = certBase64;
|
|
76
62
|
cachedPrivateKeyBase64 = privateKeyPkcs8Base64;
|
|
77
63
|
return key;
|
|
78
64
|
})
|
|
79
65
|
.catch(() => {
|
|
80
|
-
signingKeyPromise = null; // allow retry
|
|
66
|
+
signingKeyPromise = null; // allow retry
|
|
81
67
|
throw new InternalError("invalid_signing_material");
|
|
82
68
|
});
|
|
83
69
|
return signingKeyPromise;
|
|
84
70
|
}
|
|
85
71
|
// ── Session TTL ────────────────────────────────────────────────────────────
|
|
86
72
|
function resolveSessionTtl(profile) {
|
|
87
|
-
const ttl = PROFILE_TTL[profile] ??
|
|
88
|
-
return Math.min(Math.max(ttl,
|
|
73
|
+
const ttl = PROFILE_TTL[profile] ?? DEFAULT_TTL;
|
|
74
|
+
return Math.min(Math.max(ttl, MIN_PROFILE_TTL), MAX_PROFILE_TTL);
|
|
89
75
|
}
|
|
90
76
|
// ── Main export ────────────────────────────────────────────────────────────
|
|
91
77
|
export async function issueAwsCredentials(input) {
|
|
92
78
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, forceRefresh, } = input;
|
|
93
79
|
const sessionTtl = resolveSessionTtl(profile);
|
|
94
80
|
const normalizedCert = normalizeCert(certBase64);
|
|
95
|
-
//
|
|
96
|
-
|
|
81
|
+
// Parse serial fresh every call — no isolate cache.
|
|
82
|
+
// DER walk is ~5 µs; stale isolate-cached values caused wrong-serial bugs.
|
|
83
|
+
const certSerialDec = parseCertSerialDec(normalizedCert);
|
|
97
84
|
const cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
98
85
|
return getCachedOrFetch(cacheKey, () => fetchCredentials({
|
|
99
86
|
roleArn, profileArn, trustAnchorArn,
|
|
@@ -103,7 +90,9 @@ export async function issueAwsCredentials(input) {
|
|
|
103
90
|
}
|
|
104
91
|
async function fetchCredentials(input) {
|
|
105
92
|
const { roleArn, profileArn, trustAnchorArn, region, normalizedCert, privateKeyPkcs8Base64, sessionTtl, } = input;
|
|
93
|
+
// Signing happens INSIDE the fetcher so it only runs on a cache miss.
|
|
106
94
|
const signingKey = await getSigningMaterial(normalizedCert, privateKeyPkcs8Base64);
|
|
95
|
+
const certSerialDec = parseCertSerialDec(normalizedCert);
|
|
107
96
|
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
108
97
|
const iso = new Date().toISOString();
|
|
109
98
|
const amzDate = isoToAmzDate(iso);
|
|
@@ -119,51 +108,14 @@ async function fetchCredentials(input) {
|
|
|
119
108
|
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
120
109
|
const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
|
|
121
110
|
const canonicalRequest = buildCanonicalRequest({
|
|
122
|
-
method: "POST",
|
|
123
|
-
|
|
124
|
-
query: "",
|
|
125
|
-
canonicalHeaders,
|
|
126
|
-
signedHeaders,
|
|
127
|
-
payloadHash,
|
|
111
|
+
method: "POST", canonicalUri: PATH, query: "",
|
|
112
|
+
canonicalHeaders, signedHeaders, payloadHash,
|
|
128
113
|
});
|
|
129
|
-
const canonicalRequestHash = await sha256Hex(canonicalRequest);
|
|
130
114
|
const stringToSign = buildStringToSign({
|
|
131
|
-
algorithm: ALGORITHM,
|
|
132
|
-
|
|
133
|
-
credentialScope,
|
|
134
|
-
canonicalRequestHash,
|
|
115
|
+
algorithm: ALGORITHM, amzDate, credentialScope,
|
|
116
|
+
canonicalRequestHash: await sha256Hex(canonicalRequest),
|
|
135
117
|
});
|
|
136
118
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
137
|
-
const certSerialDec = getCertSerialDec(normalizedCert);
|
|
138
|
-
// 🔍 Deep debug helper (compare with working version)
|
|
139
|
-
function debugAwsSigning() {
|
|
140
|
-
try {
|
|
141
|
-
console.debug("[aws-debug][input]", {
|
|
142
|
-
roleArn,
|
|
143
|
-
profileArn,
|
|
144
|
-
trustAnchorArn,
|
|
145
|
-
region,
|
|
146
|
-
sessionTtl,
|
|
147
|
-
});
|
|
148
|
-
console.debug("[aws-debug][cert]", {
|
|
149
|
-
length: normalizedCert.length,
|
|
150
|
-
preview: normalizedCert.slice(0, 30),
|
|
151
|
-
});
|
|
152
|
-
console.debug("[aws-debug][serial]", {
|
|
153
|
-
serialDec: certSerialDec,
|
|
154
|
-
serialHex: BigInt(certSerialDec).toString(16),
|
|
155
|
-
});
|
|
156
|
-
console.debug("[aws-debug][headers]", baseHeaders);
|
|
157
|
-
console.debug("[aws-debug][signedHeaders]", signedHeaders);
|
|
158
|
-
console.debug("[aws-debug][canonicalRequest]", canonicalRequest);
|
|
159
|
-
console.debug("[aws-debug][stringToSign]", stringToSign);
|
|
160
|
-
console.debug("[aws-debug][signatureHex]", signatureHex);
|
|
161
|
-
}
|
|
162
|
-
catch (e) {
|
|
163
|
-
console.warn("[aws-debug][error]", e);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
debugAwsSigning();
|
|
167
119
|
const finalHeaders = new Headers({
|
|
168
120
|
"Content-Type": "application/json",
|
|
169
121
|
"X-Amz-Date": amzDate,
|
|
@@ -183,12 +135,7 @@ async function fetchCredentials(input) {
|
|
|
183
135
|
throw new InternalError("aws_unreachable");
|
|
184
136
|
}
|
|
185
137
|
if (!res.ok) {
|
|
186
|
-
|
|
187
|
-
console.error("[aws-rejected]", {
|
|
188
|
-
status: res.status,
|
|
189
|
-
body: text,
|
|
190
|
-
region,
|
|
191
|
-
});
|
|
138
|
+
console.warn("[aws-rejected]", { status: res.status, region });
|
|
192
139
|
throw new InternalError("aws_rejected");
|
|
193
140
|
}
|
|
194
141
|
const json = await res.json();
|
|
@@ -204,9 +151,9 @@ async function fetchCredentials(input) {
|
|
|
204
151
|
expiration: creds.expiration,
|
|
205
152
|
};
|
|
206
153
|
// Cache for 1/3 of the credential lifetime so it's refreshed well before expiry.
|
|
207
|
-
//
|
|
208
|
-
const expiresAtMs = Date.parse(creds.expiration);
|
|
154
|
+
// getCachedOrFetch will further subtract EDGE_BUFFER_SEC (5 min).
|
|
209
155
|
const receivedAt = Date.now();
|
|
156
|
+
const expiresAtMs = Date.parse(creds.expiration);
|
|
210
157
|
const credLifetimeSec = Math.floor((expiresAtMs - receivedAt) / 1000);
|
|
211
158
|
if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
|
|
212
159
|
const cacheTtlSec = Math.floor(credLifetimeSec / 3);
|
|
@@ -216,25 +163,14 @@ async function fetchCredentials(input) {
|
|
|
216
163
|
return value;
|
|
217
164
|
}
|
|
218
165
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
219
|
-
/** Strip whitespace; reject PEM-wrapped input. */
|
|
220
166
|
function normalizeCert(raw) {
|
|
221
|
-
if (raw.includes("BEGIN CERTIFICATE"))
|
|
167
|
+
if (raw.includes("BEGIN CERTIFICATE"))
|
|
222
168
|
throw new InternalError("pem_not_allowed");
|
|
223
|
-
}
|
|
224
169
|
return raw.replace(/\s+/g, "");
|
|
225
170
|
}
|
|
226
|
-
/** Return cert serial as decimal string, using isolate-level cache. */
|
|
227
|
-
function getCertSerialDec(normalizedCert) {
|
|
228
|
-
if (cachedCertSerialDec && cachedCertSerialSource === normalizedCert) {
|
|
229
|
-
return cachedCertSerialDec;
|
|
230
|
-
}
|
|
231
|
-
const serial = parseCertSerialDec(normalizedCert);
|
|
232
|
-
cachedCertSerialDec = serial;
|
|
233
|
-
cachedCertSerialSource = normalizedCert;
|
|
234
|
-
return serial;
|
|
235
|
-
}
|
|
236
171
|
/**
|
|
237
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.
|
|
238
174
|
* Throws InternalError("invalid_cert_der") on any parse failure.
|
|
239
175
|
*/
|
|
240
176
|
function parseCertSerialDec(normalizedCertBase64) {
|
|
@@ -255,34 +191,28 @@ function parseCertSerialDec(normalizedCertBase64) {
|
|
|
255
191
|
len = (len << 8) | der[offset++];
|
|
256
192
|
return len;
|
|
257
193
|
}
|
|
258
|
-
// Certificate ::= SEQUENCE
|
|
259
194
|
if (der[offset++] !== 0x30)
|
|
260
195
|
throw new Error("bad cert");
|
|
261
196
|
readLen();
|
|
262
|
-
// tbsCertificate ::= SEQUENCE
|
|
263
197
|
if (der[offset++] !== 0x30)
|
|
264
198
|
throw new Error("bad tbs");
|
|
265
199
|
readLen();
|
|
266
|
-
//
|
|
200
|
+
// Skip optional [0] EXPLICIT version field.
|
|
267
201
|
if (der[offset] === 0xa0) {
|
|
268
|
-
offset++;
|
|
269
|
-
offset += readLen();
|
|
202
|
+
offset++;
|
|
203
|
+
offset += readLen();
|
|
270
204
|
}
|
|
271
|
-
// 👉 SERIAL MUST be the next INTEGER after optional version
|
|
272
205
|
if (der[offset++] !== 0x02)
|
|
273
206
|
throw new Error("bad serial tag");
|
|
274
|
-
const
|
|
275
|
-
if (offset +
|
|
207
|
+
const serialLen = readLen();
|
|
208
|
+
if (offset + serialLen > der.length)
|
|
276
209
|
throw new Error("DER overflow");
|
|
277
|
-
let serial = der.slice(offset, offset +
|
|
278
|
-
|
|
279
|
-
if (serial.length > 1 && serial[0] === 0x00) {
|
|
210
|
+
let serial = der.slice(offset, offset + serialLen);
|
|
211
|
+
if (serial.length > 1 && serial[0] === 0x00)
|
|
280
212
|
serial = serial.slice(1);
|
|
281
|
-
}
|
|
282
213
|
let serialBig = 0n;
|
|
283
|
-
for (let i = 0; i < serial.length; i++)
|
|
214
|
+
for (let i = 0; i < serial.length; i++)
|
|
284
215
|
serialBig = (serialBig << 8n) | BigInt(serial[i]);
|
|
285
|
-
}
|
|
286
216
|
return serialBig.toString();
|
|
287
217
|
}
|
|
288
218
|
catch (e) {
|
|
@@ -290,26 +220,16 @@ function parseCertSerialDec(normalizedCertBase64) {
|
|
|
290
220
|
throw new InternalError("invalid_cert_der");
|
|
291
221
|
}
|
|
292
222
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Convert ISO-8601 to compact AMZ date-time format.
|
|
295
|
-
* e.g. "2026-03-07T12:00:00.000Z" → "20260307T120000Z"
|
|
296
|
-
*/
|
|
297
223
|
function isoToAmzDate(iso) {
|
|
298
|
-
return (iso.slice(0, 4) +
|
|
299
|
-
iso.slice(5, 7) +
|
|
300
|
-
iso.slice(8, 10) +
|
|
224
|
+
return (iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10) +
|
|
301
225
|
"T" +
|
|
302
|
-
iso.slice(11, 13) +
|
|
303
|
-
iso.slice(14, 16) +
|
|
304
|
-
iso.slice(17, 19) +
|
|
226
|
+
iso.slice(11, 13) + iso.slice(14, 16) + iso.slice(17, 19) +
|
|
305
227
|
"Z");
|
|
306
228
|
}
|
|
307
|
-
/** base64 → Uint8Array via V8's vectorized atob path. */
|
|
308
229
|
function base64ToBytes(base64) {
|
|
309
230
|
const binary = atob(base64);
|
|
310
231
|
const bytes = new Uint8Array(binary.length);
|
|
311
|
-
for (let i = 0; i < binary.length; i++)
|
|
232
|
+
for (let i = 0; i < binary.length; i++)
|
|
312
233
|
bytes[i] = binary.charCodeAt(i);
|
|
313
|
-
}
|
|
314
234
|
return bytes;
|
|
315
235
|
}
|