@vizamodo/pkg-runtime-primitives 1.0.99
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/README.md +8 -0
- package/dist/aws/console-login.d.ts +17 -0
- package/dist/aws/console-login.js +36 -0
- package/dist/aws/load-backup-from-ssm.d.ts +9 -0
- package/dist/aws/load-backup-from-ssm.js +48 -0
- package/dist/crypto/age.d.ts +22 -0
- package/dist/crypto/age.js +78 -0
- package/dist/github/github-app-token.d.ts +4 -0
- package/dist/github/github-app-token.js +74 -0
- package/dist/github/github-env.d.ts +2 -0
- package/dist/github/github-env.js +13 -0
- package/dist/github/github-headers.d.ts +1 -0
- package/dist/github/github-headers.js +8 -0
- package/dist/github/github-owner-token.d.ts +7 -0
- package/dist/github/github-owner-token.js +20 -0
- package/dist/github/list-workflow-runs.d.ts +9 -0
- package/dist/github/list-workflow-runs.js +36 -0
- package/dist/github/put-secret.d.ts +8 -0
- package/dist/github/put-secret.js +45 -0
- package/dist/github/put-var.d.ts +1 -0
- package/dist/github/put-var.js +27 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +20 -0
- package/dist/runtime/retry-once.d.ts +20 -0
- package/dist/runtime/retry-once.js +25 -0
- package/dist/types/github.d.ts +13 -0
- package/dist/types/github.js +1 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface AwsConsoleLoginConfig {
|
|
2
|
+
profile: string;
|
|
3
|
+
region: string;
|
|
4
|
+
trustAnchorArn: string;
|
|
5
|
+
roleArn: string;
|
|
6
|
+
profileArn: string;
|
|
7
|
+
certBase64: string;
|
|
8
|
+
privateKeyBase64: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function issueAwsSessionCredentials(config: AwsConsoleLoginConfig): Promise<import("@vizamodo/aws-sts-core").AwsCredentialResult>;
|
|
11
|
+
export declare function createAwsFederatedLogin(params: AwsConsoleLoginConfig & {
|
|
12
|
+
intent?: "console" | "billing" | "dynamodb" | "ssm";
|
|
13
|
+
}): Promise<{
|
|
14
|
+
loginUrl: string;
|
|
15
|
+
shortUrl: string;
|
|
16
|
+
ttlHours: number;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { issueAwsCredentials, buildFederationLoginUrl, } from "@vizamodo/aws-sts-core";
|
|
2
|
+
import { retryOnce } from "../runtime/retry-once";
|
|
3
|
+
export async function issueAwsSessionCredentials(config) {
|
|
4
|
+
const { profile, region, trustAnchorArn, roleArn, profileArn, certBase64, privateKeyBase64, } = config;
|
|
5
|
+
return await retryOnce((ctx) => issueAwsCredentials({
|
|
6
|
+
trustAnchorArn,
|
|
7
|
+
roleArn,
|
|
8
|
+
profileArn,
|
|
9
|
+
region,
|
|
10
|
+
certBase64,
|
|
11
|
+
privateKeyPkcs8Base64: privateKeyBase64,
|
|
12
|
+
profile,
|
|
13
|
+
...ctx,
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
export async function createAwsFederatedLogin(params) {
|
|
17
|
+
const { intent, ...config } = params;
|
|
18
|
+
const creds = await issueAwsSessionCredentials(config);
|
|
19
|
+
const { loginUrl, shortUrl } = await retryOnce((ctx) => buildFederationLoginUrl({
|
|
20
|
+
accessKeyId: creds.accessKeyId,
|
|
21
|
+
secretAccessKey: creds.secretAccessKey,
|
|
22
|
+
sessionToken: creds.sessionToken,
|
|
23
|
+
region: config.region,
|
|
24
|
+
expiration: creds.expiration,
|
|
25
|
+
...(intent ? { intent } : {}),
|
|
26
|
+
...ctx,
|
|
27
|
+
}));
|
|
28
|
+
const ttlMs = new Date(creds.expiration).getTime() - Date.now();
|
|
29
|
+
const ttlHoursRaw = ttlMs / (1000 * 60 * 60);
|
|
30
|
+
const ttlHours = Math.max(0, Math.round(ttlHoursRaw * 10) / 10);
|
|
31
|
+
return {
|
|
32
|
+
loginUrl,
|
|
33
|
+
shortUrl,
|
|
34
|
+
ttlHours
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AwsCredentials } from "@vizamodo/aws-sts-core";
|
|
2
|
+
export declare function loadBackupFromSSM(params: {
|
|
3
|
+
region: string;
|
|
4
|
+
credentials: AwsCredentials;
|
|
5
|
+
path: string;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
vars: Record<string, string>;
|
|
8
|
+
secrets: Record<string, string>;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { buildSignedAwsRequest } from "@vizamodo/aws-sts-core";
|
|
2
|
+
export async function loadBackupFromSSM(params) {
|
|
3
|
+
const { region, credentials, path } = params;
|
|
4
|
+
const vars = {};
|
|
5
|
+
const secrets = {};
|
|
6
|
+
let nextToken = undefined;
|
|
7
|
+
do {
|
|
8
|
+
const body = {
|
|
9
|
+
Path: path,
|
|
10
|
+
Recursive: true,
|
|
11
|
+
WithDecryption: true,
|
|
12
|
+
MaxResults: 10
|
|
13
|
+
};
|
|
14
|
+
if (nextToken) {
|
|
15
|
+
body.NextToken = nextToken;
|
|
16
|
+
}
|
|
17
|
+
const resp = await fetch(`https://ssm.${region}.amazonaws.com/`, await buildSignedAwsRequest({
|
|
18
|
+
service: "ssm",
|
|
19
|
+
region,
|
|
20
|
+
target: "AmazonSSM.GetParametersByPath",
|
|
21
|
+
contentType: "application/x-amz-json-1.1",
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
credentials
|
|
24
|
+
}));
|
|
25
|
+
if (!resp.ok) {
|
|
26
|
+
const text = await resp.text().catch(() => "");
|
|
27
|
+
throw new Error(`SSM request failed: ${resp.status} ${text}`);
|
|
28
|
+
}
|
|
29
|
+
const json = await resp.json();
|
|
30
|
+
const parameters = json.Parameters ?? [];
|
|
31
|
+
for (const p of parameters) {
|
|
32
|
+
const name = p.Name;
|
|
33
|
+
const value = p.Value;
|
|
34
|
+
// Extract key using last slash (faster than split)
|
|
35
|
+
const i = name.lastIndexOf("/");
|
|
36
|
+
const key = name.substring(i + 1);
|
|
37
|
+
// Fast path prefix checks (avoid multiple scans)
|
|
38
|
+
if (name.startsWith("/modo/github/vars/")) {
|
|
39
|
+
vars[key] = value;
|
|
40
|
+
}
|
|
41
|
+
else if (name.startsWith("/modo/github/secrets/")) {
|
|
42
|
+
secrets[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
nextToken = json.NextToken;
|
|
46
|
+
} while (nextToken);
|
|
47
|
+
return { vars, secrets };
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypt plaintext using age recipient.
|
|
3
|
+
* Returns armored ciphertext string (safe to store in SSM / JSON).
|
|
4
|
+
*/
|
|
5
|
+
export declare function encryptWithAge(plaintext: string, recipient: string): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Decrypt armored age ciphertext using identity key.
|
|
8
|
+
*/
|
|
9
|
+
export declare function decryptWithAge(ciphertext: string, identity: string): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Encrypt multiple values in parallel with a concurrency limit.
|
|
12
|
+
* Useful when encrypting many secrets to avoid Worker memory spikes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function encryptManyWithAge(items: Record<string, string>, recipient: string, concurrency?: number): Promise<Record<string, string>>;
|
|
15
|
+
/**
|
|
16
|
+
* Decrypt multiple values in parallel with a concurrency limit.
|
|
17
|
+
* Protects Worker memory (128MB) by limiting active decrypt operations.
|
|
18
|
+
*/
|
|
19
|
+
export declare function decryptManyWithAge(items: Record<string, string>, identity: string, concurrency?: number): Promise<{
|
|
20
|
+
data: Record<string, string>;
|
|
21
|
+
warnings: string[];
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as age from "age-encryption";
|
|
2
|
+
/**
|
|
3
|
+
* Encrypt plaintext using age recipient.
|
|
4
|
+
* Returns armored ciphertext string (safe to store in SSM / JSON).
|
|
5
|
+
*/
|
|
6
|
+
export async function encryptWithAge(plaintext, recipient) {
|
|
7
|
+
const e = new age.Encrypter();
|
|
8
|
+
e.addRecipient(recipient);
|
|
9
|
+
const ciphertext = await e.encrypt(plaintext);
|
|
10
|
+
// convert binary age file to ASCII armor (safe for SSM / JSON)
|
|
11
|
+
return age.armor.encode(ciphertext);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Decrypt armored age ciphertext using identity key.
|
|
15
|
+
*/
|
|
16
|
+
export async function decryptWithAge(ciphertext, identity) {
|
|
17
|
+
try {
|
|
18
|
+
const d = new age.Decrypter();
|
|
19
|
+
d.addIdentity(identity);
|
|
20
|
+
const decoded = age.armor.decode(ciphertext);
|
|
21
|
+
const out = await d.decrypt(decoded, "text");
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new Error("age decryption failed: invalid identity key or corrupted ciphertext", { cause: error });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Encrypt multiple values in parallel with a concurrency limit.
|
|
30
|
+
* Useful when encrypting many secrets to avoid Worker memory spikes.
|
|
31
|
+
*/
|
|
32
|
+
export async function encryptManyWithAge(items, recipient, concurrency = 5) {
|
|
33
|
+
const entries = Object.entries(items);
|
|
34
|
+
const result = {};
|
|
35
|
+
let index = 0;
|
|
36
|
+
async function worker() {
|
|
37
|
+
while (index < entries.length) {
|
|
38
|
+
const current = index++;
|
|
39
|
+
const entry = entries[current];
|
|
40
|
+
if (!entry)
|
|
41
|
+
break;
|
|
42
|
+
const [key, value] = entry;
|
|
43
|
+
result[key] = await encryptWithAge(value, recipient);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const workers = Array.from({ length: concurrency }, () => worker());
|
|
47
|
+
await Promise.all(workers);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Decrypt multiple values in parallel with a concurrency limit.
|
|
52
|
+
* Protects Worker memory (128MB) by limiting active decrypt operations.
|
|
53
|
+
*/
|
|
54
|
+
export async function decryptManyWithAge(items, identity, concurrency = 5) {
|
|
55
|
+
const entries = Object.entries(items);
|
|
56
|
+
const result = {};
|
|
57
|
+
const warnings = [];
|
|
58
|
+
let index = 0;
|
|
59
|
+
async function worker() {
|
|
60
|
+
while (index < entries.length) {
|
|
61
|
+
const current = index++;
|
|
62
|
+
const entry = entries[current];
|
|
63
|
+
if (!entry)
|
|
64
|
+
break;
|
|
65
|
+
const [key, value] = entry;
|
|
66
|
+
try {
|
|
67
|
+
result[key] = await decryptWithAge(value, identity);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const msg = `[age.decrypt] skipped key=${key}: ${err.message}`;
|
|
71
|
+
warnings.push(msg);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const workers = Array.from({ length: concurrency }, () => worker());
|
|
76
|
+
await Promise.all(workers);
|
|
77
|
+
return { data: result, warnings };
|
|
78
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
async function createGithubAppJwt(appId, privateKeyPem) {
|
|
2
|
+
// Convert PEM → ArrayBuffer
|
|
3
|
+
const pem = privateKeyPem
|
|
4
|
+
.replace("-----BEGIN PRIVATE KEY-----", "")
|
|
5
|
+
.replace("-----END PRIVATE KEY-----", "")
|
|
6
|
+
.replace(/\n/g, "");
|
|
7
|
+
const binary = atob(pem);
|
|
8
|
+
const bytes = new Uint8Array(binary.length);
|
|
9
|
+
for (let i = 0; i < binary.length; i++)
|
|
10
|
+
bytes[i] = binary.charCodeAt(i);
|
|
11
|
+
const key = await crypto.subtle.importKey("pkcs8", bytes.buffer, {
|
|
12
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
13
|
+
hash: "SHA-256",
|
|
14
|
+
}, false, ["sign"]);
|
|
15
|
+
const now = Math.floor(Date.now() / 1000);
|
|
16
|
+
const header = {
|
|
17
|
+
alg: "RS256",
|
|
18
|
+
typ: "JWT",
|
|
19
|
+
};
|
|
20
|
+
const payload = {
|
|
21
|
+
iat: now - 60,
|
|
22
|
+
exp: now + 540,
|
|
23
|
+
iss: appId,
|
|
24
|
+
};
|
|
25
|
+
const encode = (obj) => btoa(JSON.stringify(obj))
|
|
26
|
+
.replace(/=/g, "")
|
|
27
|
+
.replace(/\+/g, "-")
|
|
28
|
+
.replace(/\//g, "_");
|
|
29
|
+
const headerB64 = encode(header);
|
|
30
|
+
const payloadB64 = encode(payload);
|
|
31
|
+
const data = `${headerB64}.${payloadB64}`;
|
|
32
|
+
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(data));
|
|
33
|
+
const sigBytes = new Uint8Array(signature);
|
|
34
|
+
let binarySig = "";
|
|
35
|
+
for (let i = 0; i < sigBytes.length; i++)
|
|
36
|
+
binarySig += String.fromCharCode(sigBytes[i]);
|
|
37
|
+
const sigB64 = btoa(binarySig)
|
|
38
|
+
.replace(/=/g, "")
|
|
39
|
+
.replace(/\+/g, "-")
|
|
40
|
+
.replace(/\//g, "_");
|
|
41
|
+
return `${data}.${sigB64}`;
|
|
42
|
+
}
|
|
43
|
+
export async function getGithubInstallationToken(installationId, privateKey, appId) {
|
|
44
|
+
const jwt = await createGithubAppJwt(appId, privateKey);
|
|
45
|
+
const res = await fetch(`https://api.github.com/app/installations/${installationId}/access_tokens`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Bearer ${jwt}`,
|
|
49
|
+
Accept: "application/vnd.github+json",
|
|
50
|
+
"User-Agent": "viza-command-hub",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const text = await res.text();
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new Error(`GitHub installation token request failed (${res.status}): ${text}`);
|
|
56
|
+
}
|
|
57
|
+
let json;
|
|
58
|
+
try {
|
|
59
|
+
json = JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error("Invalid JSON from GitHub installation token response");
|
|
63
|
+
}
|
|
64
|
+
if (!json.token) {
|
|
65
|
+
throw new Error("GitHub installation token response missing token");
|
|
66
|
+
}
|
|
67
|
+
if (!json.expires_at) {
|
|
68
|
+
throw new Error("GitHub installation token response missing expires_at");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
token: json.token,
|
|
72
|
+
expiresAt: json.expires_at,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function repoUrl(repo, environment) {
|
|
2
|
+
return `https://api.github.com/repos/${repo}/environments/${environment}`;
|
|
3
|
+
}
|
|
4
|
+
export async function ensureEnvironment(repo, environment, headers) {
|
|
5
|
+
const res = await fetch(repoUrl(repo, environment), {
|
|
6
|
+
method: "PUT",
|
|
7
|
+
headers
|
|
8
|
+
});
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
const text = await res.text();
|
|
11
|
+
throw new Error(`Failed to ensure environment ${repo}:${environment}: ${text}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function githubHeaders(token: string): Record<string, string>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getGithubInstallationToken } from "./github-app-token";
|
|
2
|
+
import { getCachedOrFetch, wrapResult } from "@vizamodo/edge-cache-core";
|
|
3
|
+
export async function getTokenForRepo(params) {
|
|
4
|
+
const { repo, installationId, appPrivateKey, appId, forceRefresh } = params;
|
|
5
|
+
const [owner] = repo.split("/");
|
|
6
|
+
if (!owner) {
|
|
7
|
+
throw new Error(`invalid_repo_format: ${repo}`);
|
|
8
|
+
}
|
|
9
|
+
const key = `gh-token:${owner}`;
|
|
10
|
+
const token = await getCachedOrFetch(key, async () => {
|
|
11
|
+
const { token, expiresAt } = await getGithubInstallationToken(installationId, appPrivateKey, appId);
|
|
12
|
+
// use wrapResult pattern to let cache derive TTL from expiresAt
|
|
13
|
+
return wrapResult(token, expiresAt);
|
|
14
|
+
}, {
|
|
15
|
+
ttlSec: 60,
|
|
16
|
+
...(forceRefresh !== undefined ? { forceRefresh } : {})
|
|
17
|
+
} // allow caller-controlled retry
|
|
18
|
+
);
|
|
19
|
+
return token;
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export async function listWorkflowRuns(params) {
|
|
2
|
+
const { token, repo, workflow, limit: inputLimit = 20, baseUrl = "https://api.github.com", fetchFn = fetch } = params;
|
|
3
|
+
let limit = inputLimit;
|
|
4
|
+
if (!limit || limit <= 0)
|
|
5
|
+
limit = 20;
|
|
6
|
+
if (limit > 100)
|
|
7
|
+
limit = 100;
|
|
8
|
+
const res = await fetchFn(`${baseUrl}/repos/${repo}/actions/workflows/${workflow}/runs?per_page=${limit}`, {
|
|
9
|
+
headers: {
|
|
10
|
+
Authorization: `Bearer ${token}`,
|
|
11
|
+
Accept: "application/vnd.github+json",
|
|
12
|
+
"User-Agent": "viza-command-hub",
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
if (!res.ok) {
|
|
16
|
+
throw new Error(`GitHub API error: ${await res.text()}`);
|
|
17
|
+
}
|
|
18
|
+
const json = (await res.json());
|
|
19
|
+
return json.workflow_runs.map((r) => ({
|
|
20
|
+
id: r.id,
|
|
21
|
+
status: r.status,
|
|
22
|
+
conclusion: r.conclusion ?? null,
|
|
23
|
+
actor: r.actor?.login ?? "unknown",
|
|
24
|
+
attempt: r.run_attempt ?? 1,
|
|
25
|
+
createdAt: r.created_at,
|
|
26
|
+
updatedAt: r.updated_at,
|
|
27
|
+
...(r.head_commit?.committer
|
|
28
|
+
? {
|
|
29
|
+
committer: {
|
|
30
|
+
name: r.head_commit.committer.name,
|
|
31
|
+
email: r.head_commit.committer.email,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
: {}),
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function base64ToBytes(b64: string): Uint8Array;
|
|
2
|
+
export declare function bytesToBase64(bytes: Uint8Array): string;
|
|
3
|
+
export declare function encryptSecret(recipientPub: Uint8Array, secret: string): string;
|
|
4
|
+
export declare function getPublicKey(repo: string, environment: string, headers: Record<string, string>): Promise<{
|
|
5
|
+
key_id: string;
|
|
6
|
+
key: string;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function putSecret(repo: string, environment: string, name: string, encryptedValue: string, keyId: string, headers: Record<string, string>): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import sealedbox from "tweetnacl-sealedbox-js";
|
|
2
|
+
import { repoUrl } from "./github-env";
|
|
3
|
+
// ─────────────────────────────────────────────
|
|
4
|
+
// Crypto
|
|
5
|
+
// ─────────────────────────────────────────────
|
|
6
|
+
export function base64ToBytes(b64) {
|
|
7
|
+
const bin = atob(b64);
|
|
8
|
+
const bytes = new Uint8Array(bin.length);
|
|
9
|
+
for (let i = 0; i < bin.length; i++)
|
|
10
|
+
bytes[i] = bin.charCodeAt(i);
|
|
11
|
+
return bytes;
|
|
12
|
+
}
|
|
13
|
+
export function bytesToBase64(bytes) {
|
|
14
|
+
const chunk = 0x8000;
|
|
15
|
+
const parts = [];
|
|
16
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
17
|
+
parts.push(String.fromCharCode(...bytes.subarray(i, i + chunk)));
|
|
18
|
+
}
|
|
19
|
+
return btoa(parts.join(""));
|
|
20
|
+
}
|
|
21
|
+
export function encryptSecret(recipientPub, secret) {
|
|
22
|
+
const messageBytes = new TextEncoder().encode(secret);
|
|
23
|
+
const sealed = sealedbox.seal(messageBytes, recipientPub);
|
|
24
|
+
return bytesToBase64(sealed);
|
|
25
|
+
}
|
|
26
|
+
// ─────────────────────────────────────────────
|
|
27
|
+
// GitHub API
|
|
28
|
+
// ─────────────────────────────────────────────
|
|
29
|
+
export async function getPublicKey(repo, environment, headers) {
|
|
30
|
+
const res = await fetch(`${repoUrl(repo, environment)}/secrets/public-key`, { headers });
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`Failed to get public key for ${environment}: ${await res.text()}`);
|
|
33
|
+
}
|
|
34
|
+
return res.json();
|
|
35
|
+
}
|
|
36
|
+
export async function putSecret(repo, environment, name, encryptedValue, keyId, headers) {
|
|
37
|
+
const res = await fetch(`${repoUrl(repo, environment)}/secrets/${name}`, {
|
|
38
|
+
method: "PUT",
|
|
39
|
+
headers,
|
|
40
|
+
body: JSON.stringify({ encrypted_value: encryptedValue, key_id: keyId }),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok && res.status !== 204) {
|
|
43
|
+
throw new Error(`Failed to put secret ${name}: ${await res.text()}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function putVar(repo: string, environment: string, name: string, value: string, headers: Record<string, string>): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────
|
|
2
|
+
// GitHub API
|
|
3
|
+
// ─────────────────────────────────────────────
|
|
4
|
+
import { repoUrl } from "./github-env";
|
|
5
|
+
export async function putVar(repo, environment, name, value, headers) {
|
|
6
|
+
const baseUrl = `${repoUrl(repo, environment)}/variables`;
|
|
7
|
+
// PATCH nếu đã tồn tại, POST nếu chưa có
|
|
8
|
+
const patchRes = await fetch(`${baseUrl}/${name}`, {
|
|
9
|
+
method: "PATCH",
|
|
10
|
+
headers,
|
|
11
|
+
body: JSON.stringify({ name, value }),
|
|
12
|
+
});
|
|
13
|
+
if (patchRes.ok || patchRes.status === 204)
|
|
14
|
+
return;
|
|
15
|
+
if (patchRes.status === 404) {
|
|
16
|
+
const postRes = await fetch(baseUrl, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers,
|
|
19
|
+
body: JSON.stringify({ name, value }),
|
|
20
|
+
});
|
|
21
|
+
if (!postRes.ok) {
|
|
22
|
+
throw new Error(`Failed to put variable ${name}: ${await postRes.text()}`);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Failed to put variable ${name}: ${await patchRes.text()}`);
|
|
27
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/****
|
|
2
|
+
* Root export for runtime primitives
|
|
3
|
+
* Organized by domain to keep imports explicit and tree-shake friendly
|
|
4
|
+
*/
|
|
5
|
+
export * from "./aws/console-login";
|
|
6
|
+
export * from "./aws/load-backup-from-ssm";
|
|
7
|
+
export * from "./crypto/age";
|
|
8
|
+
export * from "./github/github-app-token";
|
|
9
|
+
export * from "./github/github-owner-token";
|
|
10
|
+
export * from "./github/list-workflow-runs";
|
|
11
|
+
export * from "./github/put-secret";
|
|
12
|
+
export * from "./github/put-var";
|
|
13
|
+
export * from "./github/github-headers";
|
|
14
|
+
export * from "./runtime/retry-once";
|
|
15
|
+
export * from "./types/github";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/****
|
|
2
|
+
* Root export for runtime primitives
|
|
3
|
+
* Organized by domain to keep imports explicit and tree-shake friendly
|
|
4
|
+
*/
|
|
5
|
+
// AWS
|
|
6
|
+
export * from "./aws/console-login";
|
|
7
|
+
export * from "./aws/load-backup-from-ssm";
|
|
8
|
+
// Crypto
|
|
9
|
+
export * from "./crypto/age";
|
|
10
|
+
// GitHub
|
|
11
|
+
export * from "./github/github-app-token";
|
|
12
|
+
export * from "./github/github-owner-token";
|
|
13
|
+
export * from "./github/list-workflow-runs";
|
|
14
|
+
export * from "./github/put-secret";
|
|
15
|
+
export * from "./github/put-var";
|
|
16
|
+
export * from "./github/github-headers";
|
|
17
|
+
// Runtime
|
|
18
|
+
export * from "./runtime/retry-once";
|
|
19
|
+
// Types
|
|
20
|
+
export * from "./types/github";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type RetryOptions = {
|
|
2
|
+
shouldRetry?: (err: unknown) => boolean;
|
|
3
|
+
onRetry?: (err: unknown) => void;
|
|
4
|
+
};
|
|
5
|
+
type RetryCtx = {
|
|
6
|
+
forceRefresh?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* retryOnce
|
|
10
|
+
*
|
|
11
|
+
* - First attempt: normal execution (cache allowed)
|
|
12
|
+
* - Second attempt: forceRefresh = true (bypass cache)
|
|
13
|
+
*
|
|
14
|
+
* This ensures:
|
|
15
|
+
* - cache remains best-effort
|
|
16
|
+
* - failures are recoverable
|
|
17
|
+
* - no infinite retry loop
|
|
18
|
+
*/
|
|
19
|
+
export declare function retryOnce<T>(fn: (ctx: RetryCtx) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* retryOnce
|
|
3
|
+
*
|
|
4
|
+
* - First attempt: normal execution (cache allowed)
|
|
5
|
+
* - Second attempt: forceRefresh = true (bypass cache)
|
|
6
|
+
*
|
|
7
|
+
* This ensures:
|
|
8
|
+
* - cache remains best-effort
|
|
9
|
+
* - failures are recoverable
|
|
10
|
+
* - no infinite retry loop
|
|
11
|
+
*/
|
|
12
|
+
export async function retryOnce(fn, options) {
|
|
13
|
+
try {
|
|
14
|
+
return await fn({ forceRefresh: false });
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
if (options?.shouldRetry && !options.shouldRetry(err)) {
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
if (options?.onRetry) {
|
|
21
|
+
options.onRetry(err);
|
|
22
|
+
}
|
|
23
|
+
return await fn({ forceRefresh: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type RunSummary = {
|
|
2
|
+
id: number;
|
|
3
|
+
status: "queued" | "in_progress" | "completed";
|
|
4
|
+
conclusion: string | null;
|
|
5
|
+
actor: string;
|
|
6
|
+
attempt: number;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
committer?: {
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vizamodo/pkg-runtime-primitives",
|
|
3
|
+
"version": "1.0.99",
|
|
4
|
+
"description": "Edge-compatible runtime primitives for AWS, GitHub, crypto, and caching used across Viza services",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"clean": "rm -rf dist",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
22
|
+
"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 publish --tag latest --access public && git push",
|
|
23
|
+
"release:full": "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"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@vizamodo/aws-sts-core": "^0.4.36",
|
|
27
|
+
"@vizamodo/edge-cache-core": "^0.3.41",
|
|
28
|
+
"age-encryption": "^0.3.0",
|
|
29
|
+
"tweetnacl-sealedbox-js": "^1.2.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^25.5.0",
|
|
33
|
+
"typescript": "^6.0.2"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"registry": "https://registry.npmjs.org/",
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"author": "Viza Team",
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|