@vizamodo/aws-sts-core 0.1.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.
@@ -0,0 +1,11 @@
1
+ export declare function buildFederationLoginUrl(input: {
2
+ accessKeyId: string;
3
+ secretAccessKey: string;
4
+ sessionToken: string;
5
+ region: string;
6
+ }): Promise<{
7
+ loginUrl: string;
8
+ shortUrl: string;
9
+ ttlHours: number;
10
+ region: string;
11
+ }>;
@@ -0,0 +1,25 @@
1
+ export async function buildFederationLoginUrl(input) {
2
+ const session = {
3
+ sessionId: input.accessKeyId,
4
+ sessionKey: input.secretAccessKey,
5
+ sessionToken: input.sessionToken
6
+ };
7
+ const sessionJson = JSON.stringify(session);
8
+ const encoded = encodeURIComponent(sessionJson);
9
+ const tokenResp = await fetch(`https://signin.aws.amazon.com/federation?Action=getSigninToken&Session=${encoded}`);
10
+ const { SigninToken } = await tokenResp.json();
11
+ if (!SigninToken)
12
+ throw new Error("federation_failed");
13
+ const loginUrl = `https://signin.aws.amazon.com/federation?Action=login` +
14
+ `&Issuer=viza` +
15
+ `&Destination=https://console.aws.amazon.com/?region=${input.region}` +
16
+ `&SigninToken=${SigninToken}`;
17
+ const shortUrl = loginUrl.slice(0, 40) + "..." +
18
+ loginUrl.slice(-60);
19
+ return {
20
+ loginUrl,
21
+ shortUrl,
22
+ ttlHours: 1,
23
+ region: input.region
24
+ };
25
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export * from "./sts/issue";
3
+ export * from "./federation/login";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./types";
2
+ export * from "./sts/issue";
3
+ export * from "./federation/login";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Build AWS SigV4 Canonical Request
3
+ * This module is PURE and deterministic.
4
+ * It does NOT sign anything.
5
+ */
6
+ export interface CanonicalRequestInput {
7
+ method: string;
8
+ /**
9
+ * Path MUST be URI-encoded and start with '/'
10
+ * Example: '/sessions'
11
+ */
12
+ canonicalUri: string;
13
+ /**
14
+ * Canonicalized query string.
15
+ * Must already be sorted and encoded.
16
+ * This string is used as-is.
17
+ */
18
+ query?: string;
19
+ /**
20
+ * Headers to be signed.
21
+ * This is a fully canonicalized string, including sorting, lowercasing keys,
22
+ * trimming values, and newline termination.
23
+ */
24
+ canonicalHeaders: string;
25
+ /**
26
+ * Lowercase, sorted header names joined by ';'
27
+ */
28
+ signedHeaders: string;
29
+ /**
30
+ * Hex-encoded SHA256 hash of request body.
31
+ * For empty body, use SHA256('') constant.
32
+ */
33
+ payloadHash: string;
34
+ }
35
+ export declare function buildCanonicalRequest(input: CanonicalRequestInput): string;
@@ -0,0 +1,16 @@
1
+ // src/sigv4/canonical.ts
2
+ /**
3
+ * Build AWS SigV4 Canonical Request
4
+ * This module is PURE and deterministic.
5
+ * It does NOT sign anything.
6
+ */
7
+ export function buildCanonicalRequest(input) {
8
+ return [
9
+ input.method.toUpperCase(),
10
+ input.canonicalUri,
11
+ input.query ?? "",
12
+ input.canonicalHeaders, // Bản thân cái này đã có \n cuối cùng rồi
13
+ input.signedHeaders,
14
+ input.payloadHash
15
+ ].join("\n");
16
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Canonicalize HTTP headers for AWS SigV4.
3
+ *
4
+ * Rules (AWS spec):
5
+ * - Header names: lowercase
6
+ * - Trim leading/trailing spaces in values
7
+ * - Collapse sequential spaces into one
8
+ * - Sort headers by name (ASCII)
9
+ * - Join as: `name:value\n`
10
+ *
11
+ * This function is PURE and side‑effect free.
12
+ */
13
+ export interface CanonicalHeadersResult {
14
+ canonicalHeaders: string;
15
+ signedHeaders: string;
16
+ }
17
+ export declare function canonicalizeHeaders(headers: Record<string, string | undefined>): CanonicalHeadersResult;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Canonicalize HTTP headers for AWS SigV4.
3
+ *
4
+ * Rules (AWS spec):
5
+ * - Header names: lowercase
6
+ * - Trim leading/trailing spaces in values
7
+ * - Collapse sequential spaces into one
8
+ * - Sort headers by name (ASCII)
9
+ * - Join as: `name:value\n`
10
+ *
11
+ * This function is PURE and side‑effect free.
12
+ */
13
+ export function canonicalizeHeaders(headers) {
14
+ const normalized = {};
15
+ for (const [key, value] of Object.entries(headers)) {
16
+ if (value === undefined)
17
+ continue;
18
+ const name = key.toLowerCase().trim();
19
+ if (!name)
20
+ continue;
21
+ // AWS requires:
22
+ // - trim
23
+ // - collapse multiple spaces into one
24
+ const cleanedValue = value
25
+ .trim()
26
+ .replace(/\s+/g, " ");
27
+ normalized[name] = cleanedValue;
28
+ }
29
+ const sortedHeaderNames = Object.keys(normalized).sort();
30
+ const canonicalHeaders = sortedHeaderNames
31
+ .map((name) => `${name}:${normalized[name]}\n`)
32
+ .join("");
33
+ const signedHeaders = sortedHeaderNames.join(";");
34
+ return {
35
+ canonicalHeaders,
36
+ signedHeaders,
37
+ };
38
+ }
@@ -0,0 +1,16 @@
1
+ export interface StringToSignInput {
2
+ algorithm: string;
3
+ amzDate: string;
4
+ credentialScope: string;
5
+ canonicalRequestHash: string;
6
+ }
7
+ /**
8
+ * Build SigV4 String-To-Sign
9
+ *
10
+ * Format:
11
+ * AWS4-X509-ECDSA-SHA256
12
+ * 20240101T000000Z
13
+ * 20240101/ap-southeast-1/sts/aws4_request
14
+ * <hex(canonicalRequestHash)>
15
+ */
16
+ export declare function buildStringToSign(input: StringToSignInput): string;
@@ -0,0 +1,24 @@
1
+ // src/sigv4/string-to-sign.ts
2
+ // Build AWS SigV4 String-To-Sign (X.509 / Roles Anywhere compatible)
3
+ // PURE function: no crypto, no env, no side-effects.
4
+ /**
5
+ * Build SigV4 String-To-Sign
6
+ *
7
+ * Format:
8
+ * AWS4-X509-ECDSA-SHA256
9
+ * 20240101T000000Z
10
+ * 20240101/ap-southeast-1/sts/aws4_request
11
+ * <hex(canonicalRequestHash)>
12
+ */
13
+ export function buildStringToSign(input) {
14
+ const { algorithm, amzDate, credentialScope, canonicalRequestHash, } = input;
15
+ if (!algorithm || !amzDate || !credentialScope || !canonicalRequestHash) {
16
+ throw new Error("string_to_sign_missing_fields");
17
+ }
18
+ return [
19
+ algorithm,
20
+ amzDate,
21
+ credentialScope,
22
+ canonicalRequestHash,
23
+ ].join("\n");
24
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Base error type for viza-aws-bridge.
3
+ * This worker is a credential issuer and MUST NOT leak internals.
4
+ */
5
+ export declare abstract class BridgeError extends Error {
6
+ readonly code: string;
7
+ readonly status: number;
8
+ protected constructor(code: string, status: number);
9
+ }
10
+ /**
11
+ * The request is invalid or malformed.
12
+ * This indicates a programmer or protocol error by the caller.
13
+ */
14
+ export declare class InvalidRequestError extends BridgeError {
15
+ constructor(code?: string);
16
+ }
17
+ /**
18
+ * The operation is not permitted.
19
+ * Used when AWS rejects the assume or signing operation.
20
+ */
21
+ export declare class UnauthorizedError extends BridgeError {
22
+ constructor(code?: string);
23
+ }
24
+ /**
25
+ * Internal failure inside the bridge.
26
+ * NEVER expose underlying AWS / crypto / parsing errors.
27
+ */
28
+ export declare class InternalError extends BridgeError {
29
+ constructor(code?: string);
30
+ }
31
+ /**
32
+ * Normalize unknown errors into a safe HTTP response.
33
+ * This is the ONLY place where errors are translated to responses.
34
+ */
35
+ export declare function normalizeError(err: unknown): {
36
+ status: number;
37
+ body: {
38
+ error: string;
39
+ };
40
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Base error type for viza-aws-bridge.
3
+ * This worker is a credential issuer and MUST NOT leak internals.
4
+ */
5
+ export class BridgeError extends Error {
6
+ code;
7
+ status;
8
+ constructor(code, status) {
9
+ super(code);
10
+ this.code = code;
11
+ this.status = status;
12
+ }
13
+ }
14
+ /**
15
+ * The request is invalid or malformed.
16
+ * This indicates a programmer or protocol error by the caller.
17
+ */
18
+ export class InvalidRequestError extends BridgeError {
19
+ constructor(code = "invalid_request") {
20
+ super(code, 400);
21
+ }
22
+ }
23
+ /**
24
+ * The operation is not permitted.
25
+ * Used when AWS rejects the assume or signing operation.
26
+ */
27
+ export class UnauthorizedError extends BridgeError {
28
+ constructor(code = "unauthorized") {
29
+ super(code, 403);
30
+ }
31
+ }
32
+ /**
33
+ * Internal failure inside the bridge.
34
+ * NEVER expose underlying AWS / crypto / parsing errors.
35
+ */
36
+ export class InternalError extends BridgeError {
37
+ constructor(code = "internal_error") {
38
+ super(code, 500);
39
+ }
40
+ }
41
+ /**
42
+ * Normalize unknown errors into a safe HTTP response.
43
+ * This is the ONLY place where errors are translated to responses.
44
+ */
45
+ export function normalizeError(err) {
46
+ if (err instanceof BridgeError) {
47
+ return {
48
+ status: err.status,
49
+ body: { error: err.code },
50
+ };
51
+ }
52
+ // Absolute fallback: never leak
53
+ return {
54
+ status: 500,
55
+ body: { error: "internal_error" },
56
+ };
57
+ }
@@ -0,0 +1,16 @@
1
+ import type { AwsCredentialResult } from "../types";
2
+ /**
3
+ * Issue short-lived AWS credentials via Roles Anywhere.
4
+ * Preserve:
5
+ * - TTL per profile
6
+ * - isolate-level signing material cache
7
+ */
8
+ export declare function issueAwsCredentials(input: {
9
+ roleArn: string;
10
+ profileArn: string;
11
+ trustAnchorArn: string;
12
+ region: string;
13
+ certBase64: string;
14
+ privateKeyPkcs8Base64: string;
15
+ profile: string;
16
+ }): Promise<AwsCredentialResult>;
@@ -0,0 +1,153 @@
1
+ import { canonicalizeHeaders } from "../sigv4/headers";
2
+ import { buildCanonicalRequest } from "../sigv4/canonical";
3
+ import { buildStringToSign } from "../sigv4/string-to-sign";
4
+ import { signStringToSign } from "./signer";
5
+ import { InternalError } from "./errors";
6
+ // ---- isolate-level cached signing material ----
7
+ let cachedSigningKey = null;
8
+ let cachedCertBase64 = null;
9
+ async function getSigningMaterial(input) {
10
+ if (cachedSigningKey && cachedCertBase64) {
11
+ return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
12
+ }
13
+ try {
14
+ cachedCertBase64 = input.certBase64;
15
+ const keyBuffer = base64ToArrayBuffer(input.privateKeyPkcs8Base64);
16
+ cachedSigningKey = await crypto.subtle.importKey("pkcs8", keyBuffer, { name: "ECDSA", namedCurve: "P-256" }, false, ["sign"]);
17
+ return { signingKey: cachedSigningKey, certBase64: cachedCertBase64 };
18
+ }
19
+ catch {
20
+ throw new InternalError("invalid_signing_material");
21
+ }
22
+ }
23
+ function resolveSessionTtlByProfile(profile) {
24
+ switch (profile) {
25
+ case "hub-console-ro":
26
+ return 12 * 60 * 60; // 12h
27
+ case "hub-billing-ro":
28
+ case "hub-billing-admin":
29
+ return 2 * 60 * 60; // 2h
30
+ case "hub-runtime":
31
+ return 90 * 60; // 1h30m
32
+ default:
33
+ // fail-safe
34
+ return 2 * 60 * 60;
35
+ }
36
+ }
37
+ /**
38
+ * Issue short-lived AWS credentials via Roles Anywhere.
39
+ * Preserve:
40
+ * - TTL per profile
41
+ * - isolate-level signing material cache
42
+ */
43
+ export async function issueAwsCredentials(input) {
44
+ const { roleArn, profileArn, trustAnchorArn, region, certBase64, privateKeyPkcs8Base64, profile, } = input;
45
+ if (!roleArn || !profileArn || !trustAnchorArn || !region) {
46
+ throw new InternalError("missing_aws_configuration");
47
+ }
48
+ const sessionTtl = resolveSessionTtlByProfile(profile);
49
+ const signingMaterial = await getSigningMaterial({
50
+ certBase64,
51
+ privateKeyPkcs8Base64,
52
+ });
53
+ const signingKey = signingMaterial.signingKey;
54
+ const method = "POST";
55
+ const service = "rolesanywhere";
56
+ const host = `rolesanywhere.${region}.amazonaws.com`;
57
+ const path = "/sessions";
58
+ const now = new Date();
59
+ const amzDate = now
60
+ .toISOString()
61
+ .replace(/\.\d{3}Z$/, "Z")
62
+ .replace(/[:-]/g, "");
63
+ const dateStamp = amzDate.slice(0, 8);
64
+ const body = JSON.stringify({
65
+ trustAnchorArn,
66
+ profileArn,
67
+ roleArn,
68
+ durationSeconds: sessionTtl,
69
+ });
70
+ const payloadHash = await sha256Hex(body);
71
+ const baseHeaders = {
72
+ host,
73
+ "content-type": "application/json",
74
+ "x-amz-date": amzDate,
75
+ "x-amz-x509-chain": certBase64,
76
+ };
77
+ const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
78
+ const canonicalRequest = buildCanonicalRequest({
79
+ method,
80
+ canonicalUri: path,
81
+ query: "",
82
+ canonicalHeaders,
83
+ signedHeaders,
84
+ payloadHash,
85
+ });
86
+ const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
87
+ const stringToSign = buildStringToSign({
88
+ algorithm: "AWS4-X509-ECDSA-SHA256",
89
+ amzDate,
90
+ credentialScope,
91
+ canonicalRequestHash: await sha256Hex(canonicalRequest),
92
+ });
93
+ const signatureHex = await signStringToSign(stringToSign, signingKey);
94
+ const authorization = `AWS4-X509-ECDSA-SHA256 ` +
95
+ `Credential=${roleArn}/${credentialScope}, ` +
96
+ `SignedHeaders=${signedHeaders}, ` +
97
+ `Signature=${signatureHex}`;
98
+ const headers = {
99
+ ...baseHeaders,
100
+ Authorization: authorization,
101
+ };
102
+ let res;
103
+ try {
104
+ res = await fetch(`https://${host}${path}`, {
105
+ method,
106
+ headers,
107
+ body,
108
+ });
109
+ }
110
+ catch {
111
+ throw new InternalError("aws_unreachable");
112
+ }
113
+ if (!res.ok) {
114
+ throw new InternalError("aws_rejected");
115
+ }
116
+ let json;
117
+ try {
118
+ json = await res.json();
119
+ }
120
+ catch {
121
+ throw new InternalError("invalid_aws_response");
122
+ }
123
+ const creds = json?.credentialSet?.[0]?.credentials;
124
+ if (!creds?.accessKeyId ||
125
+ !creds?.secretAccessKey ||
126
+ !creds?.sessionToken ||
127
+ !creds?.expiration) {
128
+ throw new InternalError("malformed_aws_credentials");
129
+ }
130
+ return {
131
+ accessKeyId: creds.accessKeyId,
132
+ secretAccessKey: creds.secretAccessKey,
133
+ sessionToken: creds.sessionToken,
134
+ expiration: creds.expiration,
135
+ };
136
+ }
137
+ // ---- helpers ----
138
+ async function sha256Hex(input) {
139
+ const data = new TextEncoder().encode(input);
140
+ const hash = await crypto.subtle.digest("SHA-256", data);
141
+ const bytes = new Uint8Array(hash);
142
+ return Array.from(bytes)
143
+ .map((b) => b.toString(16).padStart(2, "0"))
144
+ .join("");
145
+ }
146
+ function base64ToArrayBuffer(base64) {
147
+ const binary = atob(base64);
148
+ const bytes = new Uint8Array(binary.length);
149
+ for (let i = 0; i < binary.length; i++) {
150
+ bytes[i] = binary.charCodeAt(i);
151
+ }
152
+ return bytes.buffer;
153
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Sign String-To-Sign using ECDSA (P-256, SHA-256).
3
+ *
4
+ * IMPORTANT:
5
+ * - Uses AWS4-X509-ECDSA-SHA256
6
+ * - WebCrypto returns DER-encoded signature
7
+ * - We convert DER -> hex (lowercase) as required by AWS
8
+ */
9
+ export declare function signStringToSign(stringToSign: string, signingKey: CryptoKey): Promise<string>;
@@ -0,0 +1,34 @@
1
+ import { InternalError } from "./errors";
2
+ /**
3
+ * Sign String-To-Sign using ECDSA (P-256, SHA-256).
4
+ *
5
+ * IMPORTANT:
6
+ * - Uses AWS4-X509-ECDSA-SHA256
7
+ * - WebCrypto returns DER-encoded signature
8
+ * - We convert DER -> hex (lowercase) as required by AWS
9
+ */
10
+ export async function signStringToSign(stringToSign, signingKey) {
11
+ try {
12
+ const data = new TextEncoder().encode(stringToSign);
13
+ const signature = await crypto.subtle.sign({
14
+ name: "ECDSA",
15
+ hash: "SHA-256",
16
+ }, signingKey, data);
17
+ return arrayBufferToHex(signature);
18
+ }
19
+ catch {
20
+ throw new InternalError("signing_failed");
21
+ }
22
+ }
23
+ /**
24
+ * Convert ArrayBuffer → lowercase hex string.
25
+ * This matches AWS SigV4 expectations.
26
+ */
27
+ function arrayBufferToHex(buf) {
28
+ const bytes = new Uint8Array(buf);
29
+ let hex = "";
30
+ for (let i = 0; i < bytes.length; i++) {
31
+ hex += bytes[i].toString(16).padStart(2, "0");
32
+ }
33
+ return hex;
34
+ }
@@ -0,0 +1,6 @@
1
+ export interface AwsCredentialResult {
2
+ accessKeyId: string;
3
+ secretAccessKey: string;
4
+ sessionToken: string;
5
+ expiration: string;
6
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@vizamodo/aws-sts-core",
3
+ "version": "0.1.10",
4
+ "description": "Pure AWS STS + SigV4 (X509 Roles Anywhere) core logic",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "clean": "rm -rf dist",
17
+ "prepublishOnly": "npm run build",
18
+ "dev": "ts-node bin/viza.ts",
19
+ "release:prod": "rm -rf dist && npx npm-check-updates -u && npm install && git add package.json package-lock.json && git commit -m 'chore(deps): auto update dependencies before release' || echo 'No changes' && node versioning.js && npm login && npm publish --tag latest --access public && git push"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.9.3"
23
+ }
24
+ }