@vizamodo/aws-sts-core 0.3.23 → 0.3.26

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.
@@ -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;
@@ -1,28 +1,36 @@
1
1
  import { sha256Hex } from "../crypto/sha256";
2
- import { getEdgeCache, setEdgeCache } from "../runtime/edge-cache";
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
- let SigninToken;
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
- SigninToken = json.SigninToken;
21
- if (!SigninToken)
19
+ const token = json.SigninToken;
20
+ if (!token)
22
21
  throw new Error("federation_failed");
23
- // cache SigninToken (AWS token ~15 min cache 10 min)
24
- await setEdgeCache(cacheKey, { SigninToken }, 10 * 60).catch(() => { });
25
- }
22
+ // SigninToken TTL ~15 minutes (AWS behavior)
23
+ const SIGNIN_TOKEN_TTL_SEC = 15 * 60;
24
+ // derive effective TTL = min(tokenTTL, credentialTTL)
25
+ const credRemainingSec = Math.floor((Date.parse(input.expiration) - Date.now()) / 1000);
26
+ const effectiveTtlSec = Math.min(SIGNIN_TOKEN_TTL_SEC, credRemainingSec);
27
+ // if credential too close to expiry → skip caching
28
+ if (effectiveTtlSec <= 0) {
29
+ return token;
30
+ }
31
+ return wrapResult(token, new Date(Date.now() + effectiveTtlSec * 1000).toISOString());
32
+ }, { ttlSec: 60 } // fallback only if expiration invalid
33
+ );
26
34
  const baseLogin = `https://signin.aws.amazon.com/federation?Action=login` +
27
35
  `&Issuer=viza` +
28
36
  `&SigninToken=${SigninToken}`;
@@ -52,6 +60,7 @@ export async function buildFederationLoginUrl(input) {
52
60
  const shortUrl = loginUrl.slice(0, 40) + "..." +
53
61
  loginUrl.slice(-60);
54
62
  return {
63
+ ok: true,
55
64
  loginUrl,
56
65
  shortUrl,
57
66
  region: input.region
package/dist/index.d.ts CHANGED
@@ -2,4 +2,3 @@ export * from "./types";
2
2
  export * from "./sts/issue";
3
3
  export * from "./federation/login";
4
4
  export * from "./sigv4/request";
5
- export * from "./runtime/cache";
package/dist/index.js CHANGED
@@ -2,4 +2,3 @@ export * from "./types";
2
2
  export * from "./sts/issue";
3
3
  export * from "./federation/login";
4
4
  export * from "./sigv4/request";
5
- export * from "./runtime/cache";
@@ -1,7 +1,4 @@
1
1
  import type { AwsCredentialResult } from "../types";
2
- /**
3
- * Issue short-lived AWS credentials via Roles Anywhere.
4
- */
5
2
  export declare function issueAwsCredentials(input: {
6
3
  roleArn: string;
7
4
  profileArn: string;
package/dist/sts/issue.js CHANGED
@@ -1,11 +1,11 @@
1
- import { getEdgeCache, setEdgeCache } from "../runtime/edge-cache";
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 (cleaner configuration, no runtime cost) ----
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
- // ---- isolate-level cached signing material ----
22
- // Promise-based cache to avoid cold-start race during concurrent imports
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
- // ---- cached certificate serial (DER parse is expensive, cert rarely changes) ----
29
+ // ---- certificate serial cache (DER walk is CPU-bound, cert rarely rotates) ----
28
30
  let cachedCertSerialDec = null;
29
31
  let cachedCertSerialSource = null;
30
- const stsCredentialCache = new Map();
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
- // Reset cache only after a signing key has been resolved and material actually changed
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 after failure
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
- * Issue short-lived AWS credentials via Roles Anywhere.
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
- let cacheKey = "";
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
- try {
91
- const der = base64ToBytes(normalizedCert);
92
- let offset = 0;
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 path = PATH;
175
- const iso = new Date().toISOString(); // e.g. 2026-03-07T12:00:00.000Z
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 getPayloadHash(body);
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}/${service}/aws4_request`;
110
+ const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
193
111
  const canonicalRequest = buildCanonicalRequest({
194
112
  method: "POST",
195
- canonicalUri: path,
113
+ canonicalUri: PATH,
196
114
  query: "",
197
115
  canonicalHeaders,
198
116
  signedHeaders,
199
117
  payloadHash,
200
118
  });
201
- const canonicalRequestHash = await getCanonicalRequestHash(canonicalRequest);
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": `AWS4-X509-ECDSA-SHA256 Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`
131
+ "Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
215
132
  });
216
- const refreshPromise = (async () => {
217
- try {
218
- const res = await fetch(`https://${host}${path}`, {
219
- method: "POST",
220
- headers: finalHeaders,
221
- body,
222
- });
223
- if (!res.ok) {
224
- console.warn("[aws-rejected]", {
225
- status: res.status,
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
- catch (e) {
265
- // remove broken cache entry so next call can retry
266
- stsCredentialCache.delete(cacheKey);
267
- if (e instanceof InternalError)
268
- throw e;
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
- // store promise immediately to deduplicate concurrent refresh
273
- stsCredentialCache.set(cacheKey, {
274
- promise: refreshPromise,
275
- expiresAt: 0,
276
- });
277
- return refreshPromise;
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
- // ---- helpers ----
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
- async function getCanonicalRequestHash(canonicalRequest) {
288
- return sha256Hex(canonicalRequest);
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
- async function getPayloadHash(body) {
291
- // Payload changes are rare but SHA‑256 here is extremely cheap (~tens of µs),
292
- // so caching adds complexity without measurable benefit.
293
- return sha256Hex(body);
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
- // Faster base64 decode → Uint8Array (V8 optimized path using built‑in vectorized conversion)
247
+ /** Faster base64 → Uint8Array using V8's vectorized atob path. */
296
248
  function base64ToBytes(base64) {
297
249
  const binary = atob(base64);
298
- const len = binary.length;
299
- const bytes = new Uint8Array(len);
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.23",
3
+ "version": "0.3.26",
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
  }
@@ -1,6 +0,0 @@
1
- type Options = {
2
- ttlSec?: number;
3
- forceRefresh?: boolean;
4
- };
5
- export declare function getCachedOrFetch<T>(key: string, fetcher: () => Promise<T>, options?: Options): Promise<T>;
6
- export {};
@@ -1,22 +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 (skip if forceRefresh)
5
- if (!options?.forceRefresh && memory.has(key)) {
6
- return memory.get(key);
7
- }
8
- // L2: edge cache (skip if forceRefresh)
9
- if (!options?.forceRefresh) {
10
- const edge = await getEdgeCache(key);
11
- if (edge) {
12
- memory.set(key, edge);
13
- return edge;
14
- }
15
- }
16
- // L3: fetch
17
- const value = await fetcher();
18
- // write back
19
- memory.set(key, value);
20
- await setEdgeCache(key, value, options?.ttlSec ?? 3000);
21
- return value;
22
- }
@@ -1,8 +0,0 @@
1
- /**
2
- * Generic edge cache GET helper
3
- */
4
- export declare function getEdgeCache<T>(key: string): Promise<T | null>;
5
- /**
6
- * Generic edge cache SET helper
7
- */
8
- export declare function setEdgeCache(key: string, value: unknown, ttlSec: number): Promise<void>;
@@ -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
- }