@vizamodo/aws-sts-core 0.3.3 → 0.3.8

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.
@@ -0,0 +1,2 @@
1
+ export declare function hex(buf: ArrayBuffer | Uint8Array): string;
2
+ export declare function sha256Hex(input: string): Promise<string>;
@@ -0,0 +1,22 @@
1
+ const textEncoder = new TextEncoder();
2
+ const HEX_TABLE = new Array(256);
3
+ for (let i = 0; i < 256; i++) {
4
+ HEX_TABLE[i] = (i < 16 ? "0" : "") + i.toString(16);
5
+ }
6
+ export function hex(buf) {
7
+ const arr = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
8
+ const out = new Array(arr.length);
9
+ for (let i = 0; i < arr.length; i++) {
10
+ out[i] = HEX_TABLE[arr[i]];
11
+ }
12
+ return out.join("");
13
+ }
14
+ export async function sha256Hex(input) {
15
+ const hash = await crypto.subtle.digest("SHA-256", textEncoder.encode(input));
16
+ const bytes = new Uint8Array(hash);
17
+ const hexParts = new Array(32);
18
+ for (let i = 0; i < 32; i++) {
19
+ hexParts[i] = HEX_TABLE[bytes[i]];
20
+ }
21
+ return hexParts.join("");
22
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./types";
2
2
  export * from "./sts/issue";
3
3
  export * from "./federation/login";
4
+ export * from "./sigv4/request";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./types";
2
2
  export * from "./sts/issue";
3
3
  export * from "./federation/login";
4
+ export * from "./sigv4/request";
@@ -0,0 +1,17 @@
1
+ export type AwsCredentials = {
2
+ accessKeyId: string;
3
+ secretAccessKey: string;
4
+ sessionToken?: string;
5
+ };
6
+ export type BuildSignedAwsRequestInput = {
7
+ service: string;
8
+ region: string;
9
+ target: string;
10
+ body: string;
11
+ contentType?: string;
12
+ credentials: AwsCredentials;
13
+ };
14
+ /**
15
+ * Build signed AWS JSON API request
16
+ */
17
+ export declare function buildSignedAwsRequest(input: BuildSignedAwsRequestInput): Promise<RequestInit>;
@@ -0,0 +1,124 @@
1
+ /*
2
+ SigV4 signed AWS JSON request builder.
3
+
4
+ Designed for edge runtimes (Cloudflare Workers, Deno, Bun, Node 20+).
5
+ No AWS SDK dependency.
6
+ */
7
+ import { sha256Hex, hex } from "../crypto/sha256";
8
+ const DEFAULT_CONTENT_TYPE = "application/x-amz-json-1.1";
9
+ const encoder = new TextEncoder();
10
+ const signingKeyCache = new Map();
11
+ const MAX_SIGNING_KEY_CACHE = 64;
12
+ const hmacKeyCache = new Map();
13
+ /**
14
+ * HMAC SHA256
15
+ */
16
+ async function hmac(key, data) {
17
+ const rawKey = key instanceof Uint8Array ? key : new Uint8Array(key);
18
+ const keyId = hex(rawKey);
19
+ let cryptoKey = hmacKeyCache.get(keyId);
20
+ if (!cryptoKey) {
21
+ cryptoKey = await crypto.subtle.importKey("raw", rawKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
22
+ hmacKeyCache.set(keyId, cryptoKey);
23
+ }
24
+ return crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(data));
25
+ }
26
+ function toUint8(buf) {
27
+ return new Uint8Array(buf);
28
+ }
29
+ /**
30
+ * Derive SigV4 signing key
31
+ */
32
+ async function getSignatureKey(secretKey, date, region, service) {
33
+ const kDate = await hmac(new TextEncoder().encode("AWS4" + secretKey), date);
34
+ const kRegion = await hmac(toUint8(kDate), region);
35
+ const kService = await hmac(toUint8(kRegion), service);
36
+ return hmac(toUint8(kService), "aws4_request");
37
+ }
38
+ /**
39
+ * Build signed AWS JSON API request
40
+ */
41
+ export async function buildSignedAwsRequest(input) {
42
+ const { service, region, target, body, credentials } = input;
43
+ const contentType = input.contentType ?? DEFAULT_CONTENT_TYPE;
44
+ const host = `${service}.${region}.amazonaws.com`;
45
+ const now = new Date();
46
+ const iso = now.toISOString();
47
+ const amzDate = iso.slice(0, 4) +
48
+ iso.slice(5, 7) +
49
+ iso.slice(8, 10) +
50
+ "T" +
51
+ iso.slice(11, 13) +
52
+ iso.slice(14, 16) +
53
+ iso.slice(17, 19) +
54
+ "Z";
55
+ const dateStamp = amzDate.slice(0, 8);
56
+ const canonicalHeaders = `content-type:${contentType}\n` +
57
+ `host:${host}\n` +
58
+ `x-amz-date:${amzDate}\n` +
59
+ `x-amz-target:${target}\n` +
60
+ (credentials.sessionToken
61
+ ? `x-amz-security-token:${credentials.sessionToken}\n`
62
+ : "");
63
+ const signedHeaders = credentials.sessionToken
64
+ ? "content-type;host;x-amz-date;x-amz-security-token;x-amz-target"
65
+ : "content-type;host;x-amz-date;x-amz-target";
66
+ const payloadHash = await sha256Hex(body);
67
+ const canonicalRequest = "POST\n" +
68
+ "/\n" +
69
+ "\n" +
70
+ canonicalHeaders +
71
+ "\n" +
72
+ signedHeaders +
73
+ "\n" +
74
+ payloadHash;
75
+ const credentialScope = dateStamp +
76
+ "/" +
77
+ region +
78
+ "/" +
79
+ service +
80
+ "/aws4_request";
81
+ const stringToSign = "AWS4-HMAC-SHA256\n" +
82
+ amzDate +
83
+ "\n" +
84
+ credentialScope +
85
+ "\n" +
86
+ await sha256Hex(canonicalRequest);
87
+ const cacheKey = await sha256Hex(credentials.secretAccessKey +
88
+ "|" +
89
+ dateStamp +
90
+ "|" +
91
+ region +
92
+ "|" +
93
+ service);
94
+ let signingKey = signingKeyCache.get(cacheKey);
95
+ if (!signingKey) {
96
+ signingKey = await getSignatureKey(credentials.secretAccessKey, dateStamp, region, service);
97
+ if (signingKeyCache.size > MAX_SIGNING_KEY_CACHE) {
98
+ const firstKey = signingKeyCache.keys().next().value;
99
+ if (firstKey)
100
+ signingKeyCache.delete(firstKey);
101
+ }
102
+ signingKeyCache.set(cacheKey, signingKey);
103
+ }
104
+ const signature = hex(await hmac(signingKey, stringToSign));
105
+ const authorization = "AWS4-HMAC-SHA256 " +
106
+ `Credential=${credentials.accessKeyId}/${credentialScope}, ` +
107
+ `SignedHeaders=${signedHeaders}, ` +
108
+ `Signature=${signature}`;
109
+ const headers = {
110
+ "Content-Type": contentType,
111
+ "X-Amz-Date": amzDate,
112
+ "X-Amz-Target": target,
113
+ Authorization: authorization,
114
+ Host: host
115
+ };
116
+ if (credentials.sessionToken) {
117
+ headers["X-Amz-Security-Token"] = credentials.sessionToken;
118
+ }
119
+ return {
120
+ method: "POST",
121
+ headers,
122
+ body
123
+ };
124
+ }
package/dist/sts/issue.js CHANGED
@@ -3,12 +3,13 @@ import { buildCanonicalRequest } from "../sigv4/canonical";
3
3
  import { buildStringToSign } from "../sigv4/string-to-sign";
4
4
  import { signStringToSign } from "./signer";
5
5
  import { InternalError } from "./errors";
6
+ import { sha256Hex } from "../crypto/sha256";
6
7
  // ---- constants (cleaner configuration, no runtime cost) ----
7
8
  const ALGORITHM = "AWS4-X509-ECDSA-SHA256";
8
9
  const SERVICE = "rolesanywhere";
9
10
  const PATH = "/sessions";
10
11
  const PROFILE_TTL = {
11
- Runtime: 90 * 60,
12
+ Runtime: 30 * 60,
12
13
  ConsoleReadOnly: 12 * 60 * 60,
13
14
  ConsoleViewOnlyProd: 12 * 60 * 60,
14
15
  BillingReadOnly: 2 * 60 * 60,
@@ -26,13 +27,6 @@ let cachedPrivateKeyBase64 = null;
26
27
  let cachedCertSerialDec = null;
27
28
  let cachedCertSerialSource = null;
28
29
  const stsCredentialCache = new Map();
29
- // ---- shared encoder ----
30
- const textEncoder = new TextEncoder();
31
- // Precomputed hex table for fast byte→hex conversion (no per-byte toString alloc)
32
- const HEX_TABLE = new Array(256);
33
- for (let i = 0; i < 256; i++) {
34
- HEX_TABLE[i] = (i < 16 ? "0" : "") + i.toString(16);
35
- }
36
30
  async function getSigningMaterial(input) {
37
31
  if (cachedSigningKey &&
38
32
  cachedCertBase64 === input.certBase64 &&
@@ -91,11 +85,15 @@ export async function issueAwsCredentials(input) {
91
85
  const der = base64ToBytes(normalizedCert);
92
86
  let offset = 0;
93
87
  function readLen() {
88
+ if (offset >= der.length)
89
+ throw new Error("DER overflow");
94
90
  const b = der[offset++];
95
91
  if ((b & 0x80) === 0)
96
92
  return b;
97
93
  const n = b & 0x7f;
98
94
  let len = 0;
95
+ if (offset + n > der.length)
96
+ throw new Error("DER overflow");
99
97
  for (let i = 0; i < n; i++) {
100
98
  len = (len << 8) | der[offset++];
101
99
  }
@@ -115,6 +113,8 @@ export async function issueAwsCredentials(input) {
115
113
  if (der[offset++] !== 0x02)
116
114
  throw new Error("bad serial tag");
117
115
  const serialLen = readLen();
116
+ if (offset + serialLen > der.length)
117
+ throw new Error("DER overflow");
118
118
  let serial = der.slice(offset, offset + serialLen);
119
119
  if (serial.length > 1 && serial[0] === 0x00) {
120
120
  serial = serial.slice(1);
@@ -134,17 +134,17 @@ export async function issueAwsCredentials(input) {
134
134
  }
135
135
  }
136
136
  // ---- STS credential cache lookup (using certificate serial) ----
137
- cacheKey = `${region}|${roleArn}|${profileArn}|${certSerialDec}`;
137
+ cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
138
138
  const cachedEntry = stsCredentialCache.get(cacheKey);
139
- // Only reuse cached credentials if they still have >1h1m remaining
140
- const MIN_REMAINING_MS = 61 * 60 * 1000;
139
+ // Only reuse cached credentials if they still have >10p remaining
140
+ const MIN_REMAINING_MS = 5 * 60 * 1000;
141
141
  if (cachedEntry) {
142
- // If credentials already resolved and still valid reuse
143
- if (cachedEntry.expiresAt > Date.now() + MIN_REMAINING_MS) {
142
+ // If refresh already in-flight await the same promise
143
+ if (cachedEntry.expiresAt === 0) {
144
144
  return cachedEntry.promise;
145
145
  }
146
- // If a refresh is already in-flight await same promise
147
- if (cachedEntry.expiresAt === 0) {
146
+ // If credentials still valid with safe remaining window reuse
147
+ if (cachedEntry.expiresAt > Date.now() + MIN_REMAINING_MS) {
148
148
  return cachedEntry.promise;
149
149
  }
150
150
  }
@@ -155,9 +155,9 @@ export async function issueAwsCredentials(input) {
155
155
  // 2. Setup constants
156
156
  const host = `${SERVICE}.${region}.amazonaws.com`;
157
157
  const path = PATH;
158
- const now = new Date();
159
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
160
- const dateStamp = amzDate.slice(0, 8);
158
+ const iso = new Date().toISOString(); // e.g. 2026-03-07T12:00:00.000Z
159
+ 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`;
160
+ const dateStamp = iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10);
161
161
  const service = SERVICE;
162
162
  // 3. Chuẩn bị Body & Cert
163
163
  const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
@@ -204,20 +204,17 @@ export async function issueAwsCredentials(input) {
204
204
  body,
205
205
  });
206
206
  if (!res.ok) {
207
- const errorBody = await res.text();
208
- console.error("[aws-rejected] Request details", {
207
+ console.warn("[aws-rejected]", {
209
208
  status: res.status,
210
- response: errorBody,
211
209
  region,
212
- profile,
213
- amzDate
210
+ profile
214
211
  });
215
212
  throw new InternalError("aws_rejected");
216
213
  }
217
214
  const json = await res.json();
218
215
  const creds = json?.credentialSet?.[0]?.credentials;
219
216
  if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
220
- console.error("[issueAwsCredentials] malformed AWS credential response", { json });
217
+ console.warn("[issueAwsCredentials] malformed AWS credential response");
221
218
  throw new InternalError("aws_malformed_credentials");
222
219
  }
223
220
  const result = {
@@ -226,7 +223,8 @@ export async function issueAwsCredentials(input) {
226
223
  sessionToken: creds.sessionToken,
227
224
  expiration: creds.expiration,
228
225
  };
229
- const exp = new Date(creds.expiration).getTime();
226
+ // subtract small clock‑skew safety window (5s)
227
+ const exp = new Date(creds.expiration).getTime() - 5000;
230
228
  // update cache expiration after success
231
229
  const entry = stsCredentialCache.get(cacheKey);
232
230
  if (entry) {
@@ -254,17 +252,8 @@ function normalizeCert(raw) {
254
252
  if (raw.includes("BEGIN CERTIFICATE")) {
255
253
  throw new InternalError("pem_not_allowed");
256
254
  }
257
- return raw.trim();
258
- }
259
- async function sha256Hex(input) {
260
- // Tận dụng textEncoder có sẵn
261
- const hash = await crypto.subtle.digest("SHA-256", textEncoder.encode(input));
262
- const bytes = new Uint8Array(hash);
263
- const hexParts = new Array(32); // SHA-256 luôn là 32 bytes
264
- for (let i = 0; i < 32; i++) {
265
- hexParts[i] = HEX_TABLE[bytes[i]];
266
- }
267
- return hexParts.join("");
255
+ // remove whitespace and newlines to ensure stable cache keys
256
+ return raw.replace(/\s+/g, "");
268
257
  }
269
258
  async function getCanonicalRequestHash(canonicalRequest) {
270
259
  return sha256Hex(canonicalRequest);
@@ -274,7 +263,7 @@ async function getPayloadHash(body) {
274
263
  // so caching adds complexity without measurable benefit.
275
264
  return sha256Hex(body);
276
265
  }
277
- // Faster base64 decode → Uint8Array (avoids extra ArrayBuffer wrapping)
266
+ // Faster base64 decode → Uint8Array (V8 optimized path using built‑in vectorized conversion)
278
267
  function base64ToBytes(base64) {
279
268
  const binary = atob(base64);
280
269
  const len = binary.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizamodo/aws-sts-core",
3
- "version": "0.3.3",
3
+ "version": "0.3.8",
4
4
  "description": "Pure AWS STS + SigV4 (X509 Roles Anywhere) core logic",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",