@vizamodo/aws-sts-core 0.4.6 → 0.4.7
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 +8 -0
- package/dist/sts/issue.js +228 -126
- package/package.json +1 -1
package/dist/sts/issue.d.ts
CHANGED
|
@@ -9,4 +9,12 @@ export interface IssueAwsCredentialsInput {
|
|
|
9
9
|
profile: string;
|
|
10
10
|
forceRefresh?: boolean;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Reset isolate-level signing material and cert serial caches.
|
|
14
|
+
* ONLY for use in tests — forces re-import of signing key on next call.
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resetIsolateCache(): void;
|
|
12
20
|
export declare function issueAwsCredentials(input: IssueAwsCredentialsInput): Promise<AwsCredentialResult>;
|
package/dist/sts/issue.js
CHANGED
|
@@ -9,6 +9,10 @@ import { getCachedOrFetch, wrapResult } from "@vizamodo/edge-cache-core";
|
|
|
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
|
+
*/
|
|
12
16
|
const PROFILE_TTL = {
|
|
13
17
|
Runtime: 45 * 60,
|
|
14
18
|
ConsoleReadOnly: 12 * 60 * 60,
|
|
@@ -18,59 +22,94 @@ const PROFILE_TTL = {
|
|
|
18
22
|
BillingAccountant: 3 * 60 * 60,
|
|
19
23
|
};
|
|
20
24
|
const DEFAULT_SESSION_TTL = 2 * 60 * 60;
|
|
21
|
-
const MIN_SESSION_TTL = 45 * 60;
|
|
22
|
-
const MAX_SESSION_TTL = 12 * 60 * 60;
|
|
23
|
-
// ── Isolate-level
|
|
24
|
-
//
|
|
25
|
+
const MIN_SESSION_TTL = 45 * 60; // 45 min
|
|
26
|
+
const MAX_SESSION_TTL = 12 * 60 * 60; // 12 h
|
|
27
|
+
// ── Isolate-level signing-material cache ───────────────────────────────────
|
|
28
|
+
// Single Promise slot prevents concurrent cold-start import races.
|
|
25
29
|
let signingKeyPromise = null;
|
|
26
30
|
let cachedSigningKey = null;
|
|
27
31
|
let cachedCertBase64 = null;
|
|
28
32
|
let cachedPrivateKeyBase64 = null;
|
|
29
|
-
//
|
|
33
|
+
// ── Isolate-level cert-serial cache ───────────────────────────────────────
|
|
34
|
+
// DER walk is CPU-bound; the cert rarely rotates within an isolate lifetime.
|
|
30
35
|
let cachedCertSerialDec = null;
|
|
31
36
|
let cachedCertSerialSource = null;
|
|
32
|
-
// ──
|
|
37
|
+
// ── Test utilities ─────────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Reset isolate-level signing material and cert serial caches.
|
|
40
|
+
* ONLY for use in tests — forces re-import of signing key on next call.
|
|
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.
|
|
44
|
+
*/
|
|
45
|
+
export function resetIsolateCache() {
|
|
46
|
+
signingKeyPromise = null;
|
|
47
|
+
cachedSigningKey = null;
|
|
48
|
+
cachedCertBase64 = null;
|
|
49
|
+
cachedPrivateKeyBase64 = null;
|
|
50
|
+
cachedCertSerialDec = null;
|
|
51
|
+
cachedCertSerialSource = null;
|
|
52
|
+
}
|
|
53
|
+
// ── Signing material ───────────────────────────────────────────────────────
|
|
54
|
+
async function getSigningMaterial(certBase64, privateKeyPkcs8Base64) {
|
|
55
|
+
// Fast path: same material already imported in this isolate.
|
|
56
|
+
if (cachedSigningKey &&
|
|
57
|
+
cachedCertBase64 === certBase64 &&
|
|
58
|
+
cachedPrivateKeyBase64 === privateKeyPkcs8Base64) {
|
|
59
|
+
return cachedSigningKey;
|
|
60
|
+
}
|
|
61
|
+
// Material changed or first call — reset and re-import.
|
|
62
|
+
// Cache vars are updated inside .then() so concurrent callers
|
|
63
|
+
// awaiting the same promise all see consistent state after resolve.
|
|
64
|
+
signingKeyPromise = null;
|
|
65
|
+
cachedSigningKey = null;
|
|
66
|
+
cachedCertBase64 = null;
|
|
67
|
+
cachedPrivateKeyBase64 = null;
|
|
68
|
+
signingKeyPromise = crypto.subtle
|
|
69
|
+
.importKey("pkcs8", base64ToBytes(privateKeyPkcs8Base64), { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"])
|
|
70
|
+
.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
|
+
cachedSigningKey = key;
|
|
75
|
+
cachedCertBase64 = certBase64;
|
|
76
|
+
cachedPrivateKeyBase64 = privateKeyPkcs8Base64;
|
|
77
|
+
return key;
|
|
78
|
+
})
|
|
79
|
+
.catch(() => {
|
|
80
|
+
signingKeyPromise = null; // allow retry on next call
|
|
81
|
+
throw new InternalError("invalid_signing_material");
|
|
82
|
+
});
|
|
83
|
+
return signingKeyPromise;
|
|
84
|
+
}
|
|
85
|
+
// ── Session TTL ────────────────────────────────────────────────────────────
|
|
86
|
+
function resolveSessionTtl(profile) {
|
|
87
|
+
const ttl = PROFILE_TTL[profile] ?? DEFAULT_SESSION_TTL;
|
|
88
|
+
return Math.min(Math.max(ttl, MIN_SESSION_TTL), MAX_SESSION_TTL);
|
|
89
|
+
}
|
|
90
|
+
// ── Main export ────────────────────────────────────────────────────────────
|
|
33
91
|
export async function issueAwsCredentials(input) {
|
|
34
92
|
const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, forceRefresh, } = input;
|
|
35
93
|
const sessionTtl = resolveSessionTtl(profile);
|
|
36
94
|
const normalizedCert = normalizeCert(certBase64);
|
|
95
|
+
// Cert serial — use isolate cache to avoid redundant DER walks.
|
|
37
96
|
const certSerialDec = getCertSerialDec(normalizedCert);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
sessionTtl,
|
|
45
|
-
certSerialDec
|
|
46
|
-
});
|
|
47
|
-
}, {
|
|
48
|
-
// Cache L2 (Edge) sẽ được refresh khi còn 1/3 thời gian hiệu lực
|
|
49
|
-
// hoặc cưỡng bức refresh qua forceRefresh
|
|
50
|
-
ttlSec: 60,
|
|
51
|
-
forceRefresh
|
|
52
|
-
});
|
|
97
|
+
const cacheKey = `${region}|${roleArn}|${profileArn}|${trustAnchorArn}|${certSerialDec}`;
|
|
98
|
+
return getCachedOrFetch(cacheKey, () => fetchCredentials({
|
|
99
|
+
roleArn, profileArn, trustAnchorArn,
|
|
100
|
+
region, normalizedCert, privateKeyPkcs8Base64,
|
|
101
|
+
sessionTtl,
|
|
102
|
+
}), { ttlSec: 60, forceRefresh });
|
|
53
103
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const { roleArn, profileArn, trustAnchorArn, region, normalizedCert, privateKeyPkcs8Base64, sessionTtl, certSerialDec } = params;
|
|
57
|
-
// 1. Chuẩn bị Signing Material (Isolate-cached)
|
|
104
|
+
async function fetchCredentials(input) {
|
|
105
|
+
const { roleArn, profileArn, trustAnchorArn, region, normalizedCert, privateKeyPkcs8Base64, sessionTtl, } = input;
|
|
58
106
|
const signingKey = await getSigningMaterial(normalizedCert, privateKeyPkcs8Base64);
|
|
59
|
-
// 2. AWS SigV4 Metadata
|
|
60
107
|
const host = `${SERVICE}.${region}.amazonaws.com`;
|
|
61
|
-
const
|
|
62
|
-
const amzDate =
|
|
63
|
-
const dateStamp =
|
|
64
|
-
const
|
|
65
|
-
// 3. Request Body & Payload Hash
|
|
66
|
-
const body = JSON.stringify({
|
|
67
|
-
trustAnchorArn,
|
|
68
|
-
profileArn,
|
|
69
|
-
roleArn,
|
|
70
|
-
durationSeconds: sessionTtl
|
|
71
|
-
});
|
|
108
|
+
const iso = new Date().toISOString();
|
|
109
|
+
const amzDate = isoToAmzDate(iso);
|
|
110
|
+
const dateStamp = iso.slice(0, 4) + iso.slice(5, 7) + iso.slice(8, 10);
|
|
111
|
+
const body = JSON.stringify({ trustAnchorArn, profileArn, roleArn, durationSeconds: sessionTtl });
|
|
72
112
|
const payloadHash = await sha256Hex(body);
|
|
73
|
-
// 4. Canonical Request & StringToSign
|
|
74
113
|
const baseHeaders = {
|
|
75
114
|
"content-type": "application/json",
|
|
76
115
|
"host": host,
|
|
@@ -78,6 +117,7 @@ async function fetchAwsCredentials(params) {
|
|
|
78
117
|
"x-amz-x509": normalizedCert,
|
|
79
118
|
};
|
|
80
119
|
const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
|
|
120
|
+
const credentialScope = `${dateStamp}/${region}/${SERVICE}/aws4_request`;
|
|
81
121
|
const canonicalRequest = buildCanonicalRequest({
|
|
82
122
|
method: "POST",
|
|
83
123
|
canonicalUri: PATH,
|
|
@@ -86,128 +126,190 @@ async function fetchAwsCredentials(params) {
|
|
|
86
126
|
signedHeaders,
|
|
87
127
|
payloadHash,
|
|
88
128
|
});
|
|
129
|
+
const canonicalRequestHash = await sha256Hex(canonicalRequest);
|
|
89
130
|
const stringToSign = buildStringToSign({
|
|
90
131
|
algorithm: ALGORITHM,
|
|
91
132
|
amzDate,
|
|
92
133
|
credentialScope,
|
|
93
|
-
canonicalRequestHash
|
|
134
|
+
canonicalRequestHash,
|
|
94
135
|
});
|
|
95
|
-
// 5. Signature
|
|
96
136
|
const signatureHex = await signStringToSign(stringToSign, signingKey);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
const finalHeaders = new Headers({
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
"X-Amz-Date": amzDate,
|
|
170
|
+
"X-Amz-X509": normalizedCert,
|
|
171
|
+
"Authorization": `${ALGORITHM} Credential=${certSerialDec}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signatureHex}`,
|
|
107
172
|
});
|
|
173
|
+
let res;
|
|
174
|
+
try {
|
|
175
|
+
res = await fetch(`https://${host}${PATH}`, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: finalHeaders,
|
|
178
|
+
body,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.warn("[aws-unreachable]", { region, err });
|
|
183
|
+
throw new InternalError("aws_unreachable");
|
|
184
|
+
}
|
|
108
185
|
if (!res.ok) {
|
|
109
|
-
const
|
|
110
|
-
console.error("[aws-rejected]", {
|
|
186
|
+
const text = await res.text().catch(() => "<no-body>");
|
|
187
|
+
console.error("[aws-rejected]", {
|
|
188
|
+
status: res.status,
|
|
189
|
+
body: text,
|
|
190
|
+
region,
|
|
191
|
+
});
|
|
111
192
|
throw new InternalError("aws_rejected");
|
|
112
193
|
}
|
|
113
|
-
const
|
|
114
|
-
const creds =
|
|
115
|
-
if (!creds?.accessKeyId) {
|
|
194
|
+
const json = await res.json();
|
|
195
|
+
const creds = json?.credentialSet?.[0]?.credentials;
|
|
196
|
+
if (!creds?.accessKeyId || !creds?.secretAccessKey || !creds?.sessionToken) {
|
|
197
|
+
console.warn("[issueAwsCredentials] malformed AWS credential response");
|
|
116
198
|
throw new InternalError("aws_malformed_credentials");
|
|
117
199
|
}
|
|
118
|
-
const
|
|
200
|
+
const value = {
|
|
119
201
|
accessKeyId: creds.accessKeyId,
|
|
120
202
|
secretAccessKey: creds.secretAccessKey,
|
|
121
203
|
sessionToken: creds.sessionToken,
|
|
122
204
|
expiration: creds.expiration,
|
|
123
205
|
};
|
|
124
|
-
//
|
|
125
|
-
//
|
|
206
|
+
// Cache for 1/3 of the credential lifetime so it's refreshed well before expiry.
|
|
207
|
+
// deriveEdgeTtlSec in cache.ts will further subtract EDGE_BUFFER_SEC (5 min).
|
|
126
208
|
const expiresAtMs = Date.parse(creds.expiration);
|
|
127
|
-
const
|
|
128
|
-
|
|
209
|
+
const receivedAt = Date.now();
|
|
210
|
+
const credLifetimeSec = Math.floor((expiresAtMs - receivedAt) / 1000);
|
|
211
|
+
if (Number.isFinite(expiresAtMs) && credLifetimeSec > 0) {
|
|
129
212
|
const cacheTtlSec = Math.floor(credLifetimeSec / 3);
|
|
130
|
-
const cacheExpiry = new Date(
|
|
131
|
-
return wrapResult(
|
|
213
|
+
const cacheExpiry = new Date(receivedAt + cacheTtlSec * 1000).toISOString();
|
|
214
|
+
return wrapResult(value, cacheExpiry);
|
|
132
215
|
}
|
|
133
|
-
return
|
|
216
|
+
return value;
|
|
134
217
|
}
|
|
135
218
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
const ttl = PROFILE_TTL[profile] ?? DEFAULT_SESSION_TTL;
|
|
138
|
-
return Math.min(Math.max(ttl, MIN_SESSION_TTL), MAX_SESSION_TTL);
|
|
139
|
-
}
|
|
219
|
+
/** Strip whitespace; reject PEM-wrapped input. */
|
|
140
220
|
function normalizeCert(raw) {
|
|
141
|
-
if (raw.includes("BEGIN CERTIFICATE"))
|
|
221
|
+
if (raw.includes("BEGIN CERTIFICATE")) {
|
|
142
222
|
throw new InternalError("pem_not_allowed");
|
|
223
|
+
}
|
|
143
224
|
return raw.replace(/\s+/g, "");
|
|
144
225
|
}
|
|
226
|
+
/** Return cert serial as decimal string, using isolate-level cache. */
|
|
145
227
|
function getCertSerialDec(normalizedCert) {
|
|
146
228
|
if (cachedCertSerialDec && cachedCertSerialSource === normalizedCert) {
|
|
147
229
|
return cachedCertSerialDec;
|
|
148
230
|
}
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
const readLen = () => {
|
|
152
|
-
const b = der[offset++];
|
|
153
|
-
if ((b & 0x80) === 0)
|
|
154
|
-
return b;
|
|
155
|
-
let n = b & 0x7f;
|
|
156
|
-
let len = 0;
|
|
157
|
-
while (n--)
|
|
158
|
-
len = (len << 8) | der[offset++];
|
|
159
|
-
return len;
|
|
160
|
-
};
|
|
161
|
-
// Strict DER Walk (Sửa lỗi skip sai INTEGER của bản cũ)
|
|
162
|
-
if (der[offset++] !== 0x30)
|
|
163
|
-
throw new Error("Not a SEQUENCE"); // Cert
|
|
164
|
-
readLen();
|
|
165
|
-
if (der[offset++] !== 0x30)
|
|
166
|
-
throw new Error("Not a SEQUENCE"); // TBS
|
|
167
|
-
readLen();
|
|
168
|
-
if (der[offset] === 0xa0) { // Version tag
|
|
169
|
-
offset++;
|
|
170
|
-
offset += readLen();
|
|
171
|
-
}
|
|
172
|
-
if (der[offset++] !== 0x02)
|
|
173
|
-
throw new Error("Serial not an INTEGER");
|
|
174
|
-
const sLen = readLen();
|
|
175
|
-
let serial = der.slice(offset, offset + sLen);
|
|
176
|
-
// Strip ASN.1 padding byte 0x00
|
|
177
|
-
if (serial.length > 1 && serial[0] === 0x00)
|
|
178
|
-
serial = serial.slice(1);
|
|
179
|
-
let big = 0n;
|
|
180
|
-
for (const b of serial)
|
|
181
|
-
big = (big << 8n) | BigInt(b);
|
|
182
|
-
cachedCertSerialDec = big.toString();
|
|
231
|
+
const serial = parseCertSerialDec(normalizedCert);
|
|
232
|
+
cachedCertSerialDec = serial;
|
|
183
233
|
cachedCertSerialSource = normalizedCert;
|
|
184
|
-
return
|
|
234
|
+
return serial;
|
|
185
235
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Minimal DER walk to extract the certificate serial number as a decimal string.
|
|
238
|
+
* Throws InternalError("invalid_cert_der") on any parse failure.
|
|
239
|
+
*/
|
|
240
|
+
function parseCertSerialDec(normalizedCertBase64) {
|
|
241
|
+
try {
|
|
242
|
+
const der = base64ToBytes(normalizedCertBase64);
|
|
243
|
+
let offset = 0;
|
|
244
|
+
function readLen() {
|
|
245
|
+
if (offset >= der.length)
|
|
246
|
+
throw new Error("DER overflow");
|
|
247
|
+
const b = der[offset++];
|
|
248
|
+
if ((b & 0x80) === 0)
|
|
249
|
+
return b;
|
|
250
|
+
const n = b & 0x7f;
|
|
251
|
+
let len = 0;
|
|
252
|
+
if (offset + n > der.length)
|
|
253
|
+
throw new Error("DER overflow");
|
|
254
|
+
for (let i = 0; i < n; i++)
|
|
255
|
+
len = (len << 8) | der[offset++];
|
|
256
|
+
return len;
|
|
257
|
+
}
|
|
258
|
+
// Certificate ::= SEQUENCE
|
|
259
|
+
if (der[offset++] !== 0x30)
|
|
260
|
+
throw new Error("bad cert");
|
|
261
|
+
readLen();
|
|
262
|
+
// tbsCertificate ::= SEQUENCE
|
|
263
|
+
if (der[offset++] !== 0x30)
|
|
264
|
+
throw new Error("bad tbs");
|
|
265
|
+
readLen();
|
|
266
|
+
// Optional version [0] EXPLICIT
|
|
267
|
+
if (der[offset] === 0xa0) {
|
|
268
|
+
offset++; // tag
|
|
269
|
+
offset += readLen(); // skip content
|
|
270
|
+
}
|
|
271
|
+
// 👉 SERIAL MUST be the next INTEGER after optional version
|
|
272
|
+
if (der[offset++] !== 0x02)
|
|
273
|
+
throw new Error("bad serial tag");
|
|
274
|
+
const len = readLen();
|
|
275
|
+
if (offset + len > der.length)
|
|
276
|
+
throw new Error("DER overflow");
|
|
277
|
+
let serial = der.slice(offset, offset + len);
|
|
278
|
+
// Strip leading 0x00 only if it's padding for signed INTEGER
|
|
279
|
+
if (serial.length > 1 && serial[0] === 0x00) {
|
|
280
|
+
serial = serial.slice(1);
|
|
281
|
+
}
|
|
282
|
+
let serialBig = 0n;
|
|
283
|
+
for (let i = 0; i < serial.length; i++) {
|
|
284
|
+
serialBig = (serialBig << 8n) | BigInt(serial[i]);
|
|
285
|
+
}
|
|
286
|
+
return serialBig.toString();
|
|
189
287
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
288
|
+
catch (e) {
|
|
289
|
+
console.error("[parseCertSerialDec] failed", e);
|
|
290
|
+
throw new InternalError("invalid_cert_der");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Convert ISO-8601 to compact AMZ date-time format.
|
|
295
|
+
* e.g. "2026-03-07T12:00:00.000Z" → "20260307T120000Z"
|
|
296
|
+
*/
|
|
297
|
+
function isoToAmzDate(iso) {
|
|
298
|
+
return (iso.slice(0, 4) +
|
|
299
|
+
iso.slice(5, 7) +
|
|
300
|
+
iso.slice(8, 10) +
|
|
301
|
+
"T" +
|
|
302
|
+
iso.slice(11, 13) +
|
|
303
|
+
iso.slice(14, 16) +
|
|
304
|
+
iso.slice(17, 19) +
|
|
305
|
+
"Z");
|
|
205
306
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
307
|
+
/** base64 → Uint8Array via V8's vectorized atob path. */
|
|
308
|
+
function base64ToBytes(base64) {
|
|
309
|
+
const binary = atob(base64);
|
|
310
|
+
const bytes = new Uint8Array(binary.length);
|
|
311
|
+
for (let i = 0; i < binary.length; i++) {
|
|
312
|
+
bytes[i] = binary.charCodeAt(i);
|
|
211
313
|
}
|
|
212
314
|
return bytes;
|
|
213
315
|
}
|