@vizamodo/aws-sts-core 0.3.21 → 0.3.25
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/federation/login.d.ts +2 -0
- package/dist/federation/login.js +14 -12
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/sts/issue.d.ts +0 -3
- package/dist/sts/issue.js +139 -188
- package/package.json +4 -1
- package/dist/runtime/cache.d.ts +0 -5
- package/dist/runtime/cache.js +0 -20
- package/dist/runtime/edge-cache.d.ts +0 -8
- package/dist/runtime/edge-cache.js +0 -46
|
@@ -3,8 +3,10 @@ export declare function buildFederationLoginUrl(input: {
|
|
|
3
3
|
secretAccessKey: string;
|
|
4
4
|
sessionToken: string;
|
|
5
5
|
region: string;
|
|
6
|
+
expiration: string;
|
|
6
7
|
intent?: "console" | "billing" | "dynamodb" | "ssm";
|
|
7
8
|
}): Promise<{
|
|
9
|
+
ok: true;
|
|
8
10
|
loginUrl: string;
|
|
9
11
|
shortUrl: string;
|
|
10
12
|
region: string;
|
package/dist/federation/login.js
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
import { sha256Hex } from "../crypto/sha256";
|
|
2
|
-
import {
|
|
2
|
+
import { getCachedOrFetch, wrapResult } from "@vizamodo/edge-cache-core";
|
|
3
3
|
export async function buildFederationLoginUrl(input) {
|
|
4
4
|
const session = {
|
|
5
5
|
sessionId: input.accessKeyId,
|
|
6
6
|
sessionKey: input.secretAccessKey,
|
|
7
7
|
sessionToken: input.sessionToken
|
|
8
8
|
};
|
|
9
|
+
const expiresAtMs = Date.parse(input.expiration);
|
|
10
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
|
|
11
|
+
throw new Error("[federation] invalid or expired credentials");
|
|
12
|
+
}
|
|
9
13
|
const cacheKey = `aws-signin:${await sha256Hex(input.sessionToken)}`;
|
|
10
14
|
const sessionJson = JSON.stringify(session);
|
|
11
15
|
const encoded = encodeURIComponent(sessionJson);
|
|
12
|
-
|
|
13
|
-
const cached = await getEdgeCache(cacheKey);
|
|
14
|
-
if (cached) {
|
|
15
|
-
SigninToken = cached.SigninToken;
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
16
|
+
const SigninToken = await getCachedOrFetch(cacheKey, async () => {
|
|
18
17
|
const tokenResp = await fetch(`https://signin.aws.amazon.com/federation?Action=getSigninToken&Session=${encoded}`);
|
|
19
18
|
const json = await tokenResp.json();
|
|
20
|
-
|
|
21
|
-
if (!
|
|
19
|
+
const token = json.SigninToken;
|
|
20
|
+
if (!token)
|
|
22
21
|
throw new Error("federation_failed");
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// SigninToken TTL ~15 minutes (AWS behavior)
|
|
23
|
+
const SIGNIN_TOKEN_TTL_SEC = 15 * 60;
|
|
24
|
+
return wrapResult(token, new Date(Date.now() + SIGNIN_TOKEN_TTL_SEC * 1000).toISOString());
|
|
25
|
+
}, { ttlSec: 60 } // fallback only if expiration invalid
|
|
26
|
+
);
|
|
26
27
|
const baseLogin = `https://signin.aws.amazon.com/federation?Action=login` +
|
|
27
28
|
`&Issuer=viza` +
|
|
28
29
|
`&SigninToken=${SigninToken}`;
|
|
@@ -52,6 +53,7 @@ export async function buildFederationLoginUrl(input) {
|
|
|
52
53
|
const shortUrl = loginUrl.slice(0, 40) + "..." +
|
|
53
54
|
loginUrl.slice(-60);
|
|
54
55
|
return {
|
|
56
|
+
ok: true,
|
|
55
57
|
loginUrl,
|
|
56
58
|
shortUrl,
|
|
57
59
|
region: input.region
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/sts/issue.d.ts
CHANGED
package/dist/sts/issue.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getCachedOrFetch, wrapResult } from "@vizamodo/edge-cache-core";
|
|
2
2
|
import { canonicalizeHeaders } from "../sigv4/headers";
|
|
3
3
|
import { buildCanonicalRequest } from "../sigv4/canonical";
|
|
4
4
|
import { buildStringToSign } from "../sigv4/string-to-sign";
|
|
5
5
|
import { signStringToSign } from "./signer";
|
|
6
6
|
import { InternalError } from "./errors";
|
|
7
7
|
import { sha256Hex } from "../crypto/sha256";
|
|
8
|
-
// ---- constants
|
|
8
|
+
// ---- constants ----
|
|
9
9
|
const ALGORITHM = "AWS4-X509-ECDSA-SHA256";
|
|
10
10
|
const SERVICE = "rolesanywhere";
|
|
11
11
|
const PATH = "/sessions";
|
|
@@ -18,26 +18,29 @@ const PROFILE_TTL = {
|
|
|
18
18
|
BillingAccountant: 3 * 60 * 60,
|
|
19
19
|
};
|
|
20
20
|
const DEFAULT_TTL = 2 * 60 * 60;
|
|
21
|
-
//
|
|
22
|
-
|
|
21
|
+
const MIN_PROFILE_TTL = 45 * 60; // 45 min — lower guard
|
|
22
|
+
const MAX_PROFILE_TTL = 12 * 60 * 60; // 12 h — upper guard
|
|
23
|
+
// ---- isolate-level signing material cache ----
|
|
24
|
+
// Single Promise-based slot avoids concurrent cold-start races.
|
|
23
25
|
let signingKeyPromise = null;
|
|
24
26
|
let cachedSigningKey = null;
|
|
25
27
|
let cachedCertBase64 = null;
|
|
26
28
|
let cachedPrivateKeyBase64 = null;
|
|
27
|
-
// ----
|
|
29
|
+
// ---- certificate serial cache (DER walk is CPU-bound, cert rarely rotates) ----
|
|
28
30
|
let cachedCertSerialDec = null;
|
|
29
31
|
let cachedCertSerialSource = null;
|
|
30
|
-
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Signing material
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
31
35
|
async function getSigningMaterial(input) {
|
|
36
|
+
// Fast path: same material already imported.
|
|
32
37
|
if (cachedSigningKey &&
|
|
33
38
|
cachedCertBase64 === input.certBase64 &&
|
|
34
39
|
cachedPrivateKeyBase64 === input.privateKeyPkcs8Base64) {
|
|
35
40
|
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
36
41
|
}
|
|
37
|
-
//
|
|
38
|
-
if (cachedSigningKey
|
|
39
|
-
(cachedCertBase64 !== input.certBase64 ||
|
|
40
|
-
cachedPrivateKeyBase64 !== input.privateKeyPkcs8Base64)) {
|
|
42
|
+
// Material rotated — discard the stale resolved key so we re-import.
|
|
43
|
+
if (cachedSigningKey) {
|
|
41
44
|
signingKeyPromise = null;
|
|
42
45
|
cachedSigningKey = null;
|
|
43
46
|
}
|
|
@@ -57,131 +60,46 @@ async function getSigningMaterial(input) {
|
|
|
57
60
|
return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
|
|
58
61
|
}
|
|
59
62
|
catch {
|
|
60
|
-
signingKeyPromise = null; // allow retry
|
|
63
|
+
signingKeyPromise = null; // allow retry on next call
|
|
61
64
|
throw new InternalError("invalid_signing_material");
|
|
62
65
|
}
|
|
63
66
|
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Profile TTL resolution
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
64
70
|
function resolveSessionTtlByProfile(profile) {
|
|
65
71
|
const ttl = PROFILE_TTL[profile] ?? DEFAULT_TTL;
|
|
66
|
-
// Guard rails
|
|
67
|
-
const MIN_PROFILE_TTL = 45 * 60; // 45 minutes
|
|
68
|
-
const MAX_PROFILE_TTL = 12 * 60 * 60; // 12 hours
|
|
69
|
-
// Clamp TTL to safe range
|
|
70
72
|
return Math.min(Math.max(ttl, MIN_PROFILE_TTL), MAX_PROFILE_TTL);
|
|
71
73
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Main export
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
75
77
|
export async function issueAwsCredentials(input) {
|
|
76
78
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, } = input;
|
|
77
|
-
// 1. Kiểm tra đầu vào & Fix TTL an toàn
|
|
78
79
|
const sessionTtl = resolveSessionTtlByProfile(profile);
|
|
79
|
-
// Normalize certificate once so every subsystem (cache, DER parse, headers)
|
|
80
|
-
// uses the exact same canonical string
|
|
81
80
|
const normalizedCert = normalizeCert(certBase64);
|
|
82
|
-
|
|
83
|
-
// --- REFACTOR: Parser ASN.1 an toàn để lấy Serial Number (DECIMAL) ---
|
|
84
|
-
// --- MINIMAL DER WALK: extract serial number ---
|
|
81
|
+
// ---- DER serial extraction (with isolate-level cache) ----
|
|
85
82
|
let certSerialDec;
|
|
86
83
|
if (cachedCertSerialDec && cachedCertSerialSource === normalizedCert) {
|
|
87
84
|
certSerialDec = cachedCertSerialDec;
|
|
88
85
|
}
|
|
89
86
|
else {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
function readLen() {
|
|
94
|
-
if (offset >= der.length)
|
|
95
|
-
throw new Error("DER overflow");
|
|
96
|
-
const b = der[offset++];
|
|
97
|
-
if ((b & 0x80) === 0)
|
|
98
|
-
return b;
|
|
99
|
-
const n = b & 0x7f;
|
|
100
|
-
let len = 0;
|
|
101
|
-
if (offset + n > der.length)
|
|
102
|
-
throw new Error("DER overflow");
|
|
103
|
-
for (let i = 0; i < n; i++) {
|
|
104
|
-
len = (len << 8) | der[offset++];
|
|
105
|
-
}
|
|
106
|
-
return len;
|
|
107
|
-
}
|
|
108
|
-
if (der[offset++] !== 0x30)
|
|
109
|
-
throw new Error("bad cert");
|
|
110
|
-
readLen();
|
|
111
|
-
if (der[offset++] !== 0x30)
|
|
112
|
-
throw new Error("bad tbs");
|
|
113
|
-
readLen();
|
|
114
|
-
if (der[offset] === 0xa0) {
|
|
115
|
-
offset++;
|
|
116
|
-
const vLen = readLen();
|
|
117
|
-
offset += vLen;
|
|
118
|
-
}
|
|
119
|
-
if (der[offset++] !== 0x02)
|
|
120
|
-
throw new Error("bad serial tag");
|
|
121
|
-
const serialLen = readLen();
|
|
122
|
-
if (offset + serialLen > der.length)
|
|
123
|
-
throw new Error("DER overflow");
|
|
124
|
-
let serial = der.slice(offset, offset + serialLen);
|
|
125
|
-
if (serial.length > 1 && serial[0] === 0x00) {
|
|
126
|
-
serial = serial.slice(1);
|
|
127
|
-
}
|
|
128
|
-
// Build BigInt directly from bytes (avoids hex string allocation)
|
|
129
|
-
let serialBig = 0n;
|
|
130
|
-
for (let i = 0; i < serial.length; i++) {
|
|
131
|
-
serialBig = (serialBig << 8n) | BigInt(serial[i]);
|
|
132
|
-
}
|
|
133
|
-
certSerialDec = serialBig.toString();
|
|
134
|
-
cachedCertSerialDec = certSerialDec;
|
|
135
|
-
cachedCertSerialSource = normalizedCert;
|
|
136
|
-
}
|
|
137
|
-
catch (e) {
|
|
138
|
-
console.error("[issueAwsCredentials] Failed to parse cert serial", e);
|
|
139
|
-
throw new InternalError("invalid_cert_der");
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// ---- cross-request cache lookup (Cloudflare Cache API) ----
|
|
143
|
-
cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
144
|
-
const externalCached = await getEdgeCache(cacheKey);
|
|
145
|
-
if (externalCached?.expiration) {
|
|
146
|
-
const exp = Date.parse(externalCached.expiration);
|
|
147
|
-
// Ensure cached credentials still have at least 2/3 of session TTL remaining
|
|
148
|
-
const MIN_REMAINING_MS = Math.floor((sessionTtl * 2) / 3) * 1000;
|
|
149
|
-
if (Number.isFinite(exp) && exp > Date.now() + MIN_REMAINING_MS) {
|
|
150
|
-
return externalCached;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// ---- isolate-level cache lookup (dedupe concurrent refresh within isolate) ----
|
|
154
|
-
const cachedEntry = stsCredentialCache.get(cacheKey);
|
|
155
|
-
// Ensure remaining TTL is at least 2/3 of the profile session TTL
|
|
156
|
-
// so console sessions (e.g. 12h) don't receive credentials close to expiry
|
|
157
|
-
const MIN_REMAINING_MS = Math.floor((sessionTtl * 2) / 3) * 1000;
|
|
158
|
-
if (cachedEntry) {
|
|
159
|
-
// If refresh already in-flight → await the same promise
|
|
160
|
-
if (cachedEntry.expiresAt === 0) {
|
|
161
|
-
return cachedEntry.promise;
|
|
162
|
-
}
|
|
163
|
-
// If credentials still valid with safe remaining window → reuse
|
|
164
|
-
if (cachedEntry.expiresAt > Date.now() + MIN_REMAINING_MS) {
|
|
165
|
-
return cachedEntry.promise;
|
|
166
|
-
}
|
|
87
|
+
certSerialDec = parseCertSerialDec(normalizedCert); // throws InternalError on bad DER
|
|
88
|
+
cachedCertSerialDec = certSerialDec;
|
|
89
|
+
cachedCertSerialSource = normalizedCert;
|
|
167
90
|
}
|
|
91
|
+
const cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
92
|
+
// ---- Build SigV4 request ----
|
|
168
93
|
const { signingKey } = await getSigningMaterial({
|
|
169
94
|
certBase64: normalizedCert,
|
|
170
95
|
privateKeyPkcs8Base64,
|
|
171
96
|
});
|
|
172
|
-
// 2. Setup constants
|
|
173
97
|
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const amzDate = `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}T${iso.slice(11, 13)}${iso.slice(14, 16)}${iso.slice(17, 19)}Z`;
|
|
98
|
+
const iso = new Date().toISOString();
|
|
99
|
+
const amzDate = isoToAmzDate(iso);
|
|
177
100
|
const dateStamp = iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10);
|
|
178
|
-
const service = SERVICE;
|
|
179
|
-
// 3. Chuẩn bị Body & Cert
|
|
180
101
|
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
181
|
-
const payloadHash = await
|
|
182
|
-
// --- REFACTOR: Làm sạch chứng chỉ chặt chẽ ---
|
|
183
|
-
// const normalizedCert = normalizeCert(certBase64);
|
|
184
|
-
// 4. Tính toán Signature
|
|
102
|
+
const payloadHash = await sha256Hex(body);
|
|
185
103
|
const baseHeaders = {
|
|
186
104
|
"content-type": "application/json",
|
|
187
105
|
"host": host,
|
|
@@ -189,16 +107,16 @@ export async function issueAwsCredentials(input) {
|
|
|
189
107
|
"x-amz-x509": normalizedCert,
|
|
190
108
|
};
|
|
191
109
|
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
192
|
-
const credentialScope = `${dateStamp}/${region}/${
|
|
110
|
+
const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
|
|
193
111
|
const canonicalRequest = buildCanonicalRequest({
|
|
194
112
|
method: "POST",
|
|
195
|
-
canonicalUri:
|
|
113
|
+
canonicalUri: PATH,
|
|
196
114
|
query: "",
|
|
197
115
|
canonicalHeaders,
|
|
198
116
|
signedHeaders,
|
|
199
117
|
payloadHash,
|
|
200
118
|
});
|
|
201
|
-
const canonicalRequestHash = await
|
|
119
|
+
const canonicalRequestHash = await sha256Hex(canonicalRequest);
|
|
202
120
|
const stringToSign = buildStringToSign({
|
|
203
121
|
algorithm: ALGORITHM,
|
|
204
122
|
amzDate,
|
|
@@ -206,98 +124,131 @@ export async function issueAwsCredentials(input) {
|
|
|
206
124
|
canonicalRequestHash,
|
|
207
125
|
});
|
|
208
126
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
209
|
-
// 5. Build Authorization Header với số Serial (DECIMAL)
|
|
210
127
|
const finalHeaders = new Headers({
|
|
211
128
|
"Content-Type": "application/json",
|
|
212
129
|
"X-Amz-Date": amzDate,
|
|
213
130
|
"X-Amz-X509": normalizedCert,
|
|
214
|
-
"Authorization":
|
|
131
|
+
"Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
|
|
215
132
|
});
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
region,
|
|
227
|
-
profile
|
|
228
|
-
});
|
|
229
|
-
throw new InternalError("aws_rejected");
|
|
230
|
-
}
|
|
231
|
-
const json = await res.json();
|
|
232
|
-
const creds = json?.credentialSet?.[0]?.credentials;
|
|
233
|
-
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
234
|
-
console.warn("[issueAwsCredentials] malformed AWS credential response");
|
|
235
|
-
throw new InternalError("aws_malformed_credentials");
|
|
236
|
-
}
|
|
237
|
-
const result = {
|
|
238
|
-
accessKeyId: creds.accessKeyId,
|
|
239
|
-
secretAccessKey: creds.secretAccessKey,
|
|
240
|
-
sessionToken: creds.sessionToken,
|
|
241
|
-
expiration: creds.expiration,
|
|
242
|
-
};
|
|
243
|
-
// persist credential in edge cache (best effort)
|
|
244
|
-
if (result.expiration) {
|
|
245
|
-
const expiresAt = Date.parse(result.expiration);
|
|
246
|
-
if (Number.isFinite(expiresAt)) {
|
|
247
|
-
const remainingSec = Math.floor((expiresAt - Date.now()) / 1000);
|
|
248
|
-
// Cache lifetime policy: only cache for 1/3 of requested session TTL
|
|
249
|
-
const desiredTtl = Math.floor(sessionTtl / 3);
|
|
250
|
-
// Ensure we never cache longer than the actual credential lifetime
|
|
251
|
-
const ttlSec = Math.max(0, Math.min(desiredTtl, remainingSec));
|
|
252
|
-
setEdgeCache(cacheKey, result, ttlSec).catch(() => { });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
// subtract small clock‑skew safety window (5s)
|
|
256
|
-
const exp = new Date(creds.expiration).getTime() - 5000;
|
|
257
|
-
// update cache expiration after success
|
|
258
|
-
const entry = stsCredentialCache.get(cacheKey);
|
|
259
|
-
if (entry) {
|
|
260
|
-
entry.expiresAt = exp;
|
|
261
|
-
}
|
|
262
|
-
return result;
|
|
133
|
+
const issuedAt = Date.now(); // snapshot before the network round-trip
|
|
134
|
+
return getCachedOrFetch(cacheKey, async () => {
|
|
135
|
+
const res = await fetch(`https://${host}${PATH}`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: finalHeaders,
|
|
138
|
+
body,
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
console.warn("[aws-rejected]", { status: res.status, region, profile });
|
|
142
|
+
throw new InternalError("aws_rejected");
|
|
263
143
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
throw new InternalError("aws_unreachable");
|
|
144
|
+
const json = await res.json();
|
|
145
|
+
const creds = json?.credentialSet?.[0]?.credentials;
|
|
146
|
+
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
147
|
+
console.warn("[issueAwsCredentials] malformed AWS credential response");
|
|
148
|
+
throw new InternalError("aws_malformed_credentials");
|
|
270
149
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
150
|
+
const value = {
|
|
151
|
+
accessKeyId: creds.accessKeyId,
|
|
152
|
+
secretAccessKey: creds.secretAccessKey,
|
|
153
|
+
sessionToken: creds.sessionToken,
|
|
154
|
+
expiration: creds.expiration,
|
|
155
|
+
};
|
|
156
|
+
// derive TTL = 1/3 lifetime
|
|
157
|
+
const expiresAtMs = Date.parse(creds.expiration);
|
|
158
|
+
const credLifetimeSec = Math.floor((expiresAtMs - issuedAt) / 1000);
|
|
159
|
+
if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
|
|
160
|
+
const edgeCacheTtlSec = Math.floor(credLifetimeSec / 3);
|
|
161
|
+
const edgeCacheExpiry = new Date(issuedAt + edgeCacheTtlSec * 1000).toISOString();
|
|
162
|
+
return wrapResult(value, edgeCacheExpiry);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}, { ttlSec: 60 });
|
|
278
166
|
}
|
|
279
|
-
//
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Helpers
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/** Strip whitespace and reject PEM-wrapped input. */
|
|
280
171
|
function normalizeCert(raw) {
|
|
281
172
|
if (raw.includes("BEGIN CERTIFICATE")) {
|
|
282
173
|
throw new InternalError("pem_not_allowed");
|
|
283
174
|
}
|
|
284
|
-
// remove whitespace and newlines to ensure stable cache keys
|
|
285
175
|
return raw.replace(/\s+/g, "");
|
|
286
176
|
}
|
|
287
|
-
|
|
288
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Minimal DER walk to extract the certificate serial number as a decimal string.
|
|
179
|
+
* Throws InternalError("invalid_cert_der") on any parse failure.
|
|
180
|
+
*/
|
|
181
|
+
function parseCertSerialDec(normalizedCertBase64) {
|
|
182
|
+
try {
|
|
183
|
+
const der = base64ToBytes(normalizedCertBase64);
|
|
184
|
+
let offset = 0;
|
|
185
|
+
function readLen() {
|
|
186
|
+
if (offset >= der.length)
|
|
187
|
+
throw new Error("DER overflow");
|
|
188
|
+
const b = der[offset++];
|
|
189
|
+
if ((b & 0x80) === 0)
|
|
190
|
+
return b;
|
|
191
|
+
const n = b & 0x7f;
|
|
192
|
+
let len = 0;
|
|
193
|
+
if (offset + n > der.length)
|
|
194
|
+
throw new Error("DER overflow");
|
|
195
|
+
for (let i = 0; i < n; i++)
|
|
196
|
+
len = (len << 8) | der[offset++];
|
|
197
|
+
return len;
|
|
198
|
+
}
|
|
199
|
+
if (der[offset++] !== 0x30)
|
|
200
|
+
throw new Error("bad cert");
|
|
201
|
+
readLen();
|
|
202
|
+
if (der[offset++] !== 0x30)
|
|
203
|
+
throw new Error("bad tbs");
|
|
204
|
+
readLen();
|
|
205
|
+
// Skip optional [0] EXPLICIT version field.
|
|
206
|
+
if (der[offset] === 0xa0) {
|
|
207
|
+
offset++;
|
|
208
|
+
const vLen = readLen();
|
|
209
|
+
offset += vLen;
|
|
210
|
+
}
|
|
211
|
+
if (der[offset++] !== 0x02)
|
|
212
|
+
throw new Error("bad serial tag");
|
|
213
|
+
const serialLen = readLen();
|
|
214
|
+
if (offset + serialLen > der.length)
|
|
215
|
+
throw new Error("DER overflow");
|
|
216
|
+
let serial = der.slice(offset, offset + serialLen);
|
|
217
|
+
// Strip ASN.1 sign-extension padding byte.
|
|
218
|
+
if (serial.length > 1 && serial[0] === 0x00) {
|
|
219
|
+
serial = serial.slice(1);
|
|
220
|
+
}
|
|
221
|
+
// Accumulate directly to BigInt — avoids intermediate hex string allocation.
|
|
222
|
+
let serialBig = 0n;
|
|
223
|
+
for (let i = 0; i < serial.length; i++) {
|
|
224
|
+
serialBig = (serialBig << 8n) | BigInt(serial[i]);
|
|
225
|
+
}
|
|
226
|
+
return serialBig.toString();
|
|
227
|
+
}
|
|
228
|
+
catch (e) {
|
|
229
|
+
console.error("[parseCertSerialDec] failed", e);
|
|
230
|
+
throw new InternalError("invalid_cert_der");
|
|
231
|
+
}
|
|
289
232
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Convert an ISO-8601 timestamp to the compact AMZ date-time format.
|
|
235
|
+
* e.g. "2026-03-07T12:00:00.000Z" → "20260307T120000Z"
|
|
236
|
+
*/
|
|
237
|
+
function isoToAmzDate(iso) {
|
|
238
|
+
return (iso.slice(0, 4) +
|
|
239
|
+
iso.slice(5, 7) +
|
|
240
|
+
iso.slice(8, 10) +
|
|
241
|
+
"T" +
|
|
242
|
+
iso.slice(11, 13) +
|
|
243
|
+
iso.slice(14, 16) +
|
|
244
|
+
iso.slice(17, 19) +
|
|
245
|
+
"Z");
|
|
294
246
|
}
|
|
295
|
-
|
|
247
|
+
/** Faster base64 → Uint8Array using V8's vectorized atob path. */
|
|
296
248
|
function base64ToBytes(base64) {
|
|
297
249
|
const binary = atob(base64);
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
for (let i = 0; i < len; i++) {
|
|
250
|
+
const bytes = new Uint8Array(binary.length);
|
|
251
|
+
for (let i = 0; i < binary.length; i++) {
|
|
301
252
|
bytes[i] = binary.charCodeAt(i);
|
|
302
253
|
}
|
|
303
254
|
return bytes;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizamodo/aws-sts-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.25",
|
|
4
4
|
"description": "Pure AWS STS + SigV4 (X509 Roles Anywhere) core logic",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
"@vitest/coverage-v8": "^4.1.0",
|
|
27
27
|
"typescript": "^5.9.3",
|
|
28
28
|
"vitest": "^4.1.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@vizamodo/edge-cache-core": "^0.3.29"
|
|
29
32
|
}
|
|
30
33
|
}
|
package/dist/runtime/cache.d.ts
DELETED
package/dist/runtime/cache.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { getEdgeCache, setEdgeCache } from "./edge-cache";
|
|
2
|
-
const memory = new Map();
|
|
3
|
-
export async function getCachedOrFetch(key, fetcher, options) {
|
|
4
|
-
// L1: memory
|
|
5
|
-
if (memory.has(key)) {
|
|
6
|
-
return memory.get(key);
|
|
7
|
-
}
|
|
8
|
-
// L2: edge cache
|
|
9
|
-
const edge = await getEdgeCache(key);
|
|
10
|
-
if (edge) {
|
|
11
|
-
memory.set(key, edge);
|
|
12
|
-
return edge;
|
|
13
|
-
}
|
|
14
|
-
// L3: fetch
|
|
15
|
-
const value = await fetcher();
|
|
16
|
-
// write back
|
|
17
|
-
memory.set(key, value);
|
|
18
|
-
await setEdgeCache(key, value, options?.ttlSec ?? 3000);
|
|
19
|
-
return value;
|
|
20
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
const CACHE_KEY_PREFIX = "https://edge-cache.internal/";
|
|
2
|
-
/**
|
|
3
|
-
* Generic edge cache GET helper
|
|
4
|
-
*/
|
|
5
|
-
export async function getEdgeCache(key) {
|
|
6
|
-
try {
|
|
7
|
-
const cache = caches.default;
|
|
8
|
-
const req = new Request(CACHE_KEY_PREFIX + key);
|
|
9
|
-
const res = await cache.match(req);
|
|
10
|
-
if (!res)
|
|
11
|
-
return null;
|
|
12
|
-
try {
|
|
13
|
-
return (await res.json());
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
// corrupted cache entry → ignore
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
// cache API failure should not break runtime
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Generic edge cache SET helper
|
|
27
|
-
*/
|
|
28
|
-
export async function setEdgeCache(key, value, ttlSec) {
|
|
29
|
-
if (ttlSec <= 0)
|
|
30
|
-
return;
|
|
31
|
-
const cache = caches.default;
|
|
32
|
-
const req = new Request(CACHE_KEY_PREFIX + key);
|
|
33
|
-
const body = JSON.stringify(value);
|
|
34
|
-
const res = new Response(body, {
|
|
35
|
-
headers: {
|
|
36
|
-
"Content-Type": "application/json",
|
|
37
|
-
"Cache-Control": `max-age=${ttlSec}`,
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
try {
|
|
41
|
-
await cache.put(req, res);
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
// cache write failure is non-fatal
|
|
45
|
-
}
|
|
46
|
-
}
|