alchemy 0.1.17 → 0.1.18
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/lib/alchemize.d.ts +4 -0
- package/lib/alchemize.d.ts.map +1 -1
- package/lib/alchemize.js +4 -0
- package/lib/alchemize.js.map +1 -1
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +4 -0
- package/lib/apply.js.map +1 -1
- package/lib/cloudflare/bindings.d.ts +3 -2
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bound.d.ts +8 -0
- package/lib/cloudflare/bound.d.ts.map +1 -0
- package/lib/cloudflare/bound.js +2 -0
- package/lib/cloudflare/bound.js.map +1 -0
- package/lib/cloudflare/bucket.d.ts +3 -3
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +1 -1
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/static-site.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +12 -6
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +14 -2
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/encrypt.d.ts +17 -0
- package/lib/encrypt.d.ts.map +1 -0
- package/lib/encrypt.js +45 -0
- package/lib/encrypt.js.map +1 -0
- package/lib/github/secret.d.ts +2 -1
- package/lib/github/secret.d.ts.map +1 -1
- package/lib/github/secret.js +21 -25
- package/lib/github/secret.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/input.d.ts +2 -1
- package/lib/input.d.ts.map +1 -1
- package/lib/output.d.ts +2 -1
- package/lib/output.d.ts.map +1 -1
- package/lib/output.js.map +1 -1
- package/lib/secret.d.ts +12 -0
- package/lib/secret.d.ts.map +1 -0
- package/lib/secret.js +30 -0
- package/lib/secret.js.map +1 -0
- package/lib/serde.d.ts +3 -0
- package/lib/serde.d.ts.map +1 -0
- package/lib/serde.js +37 -0
- package/lib/serde.js.map +1 -0
- package/lib/state.d.ts.map +1 -1
- package/lib/state.js +3 -2
- package/lib/state.js.map +1 -1
- package/package.json +3 -6
- package/src/alchemize.ts +9 -0
- package/src/apply.ts +3 -0
- package/src/cloudflare/bindings.ts +4 -2
- package/src/cloudflare/bound.ts +18 -0
- package/src/cloudflare/bucket.ts +3 -3
- package/src/cloudflare/static-site.ts +1 -1
- package/src/cloudflare/worker.ts +77 -59
- package/src/encrypt.ts +79 -0
- package/src/github/secret.ts +34 -33
- package/src/index.ts +1 -0
- package/src/input.ts +13 -10
- package/src/output.ts +13 -9
- package/src/secret.ts +41 -0
- package/src/serde.ts +46 -0
- package/src/state.ts +3 -2
package/src/encrypt.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import sodium from "libsodium-wrappers";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Encrypt a value with a symmetric key using libsodium
|
|
5
|
+
*
|
|
6
|
+
* @param value - The value to encrypt
|
|
7
|
+
* @param key - The encryption key
|
|
8
|
+
* @returns The base64-encoded encrypted value with nonce
|
|
9
|
+
*/
|
|
10
|
+
export async function encryptWithKey(
|
|
11
|
+
value: string,
|
|
12
|
+
key: string,
|
|
13
|
+
): Promise<string> {
|
|
14
|
+
// Initialize libsodium
|
|
15
|
+
await sodium.ready;
|
|
16
|
+
|
|
17
|
+
// Derive a key from the passphrase
|
|
18
|
+
const cryptoKey = sodium.crypto_generichash(
|
|
19
|
+
sodium.crypto_secretbox_KEYBYTES,
|
|
20
|
+
sodium.from_string(key),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Generate a random nonce
|
|
24
|
+
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
|
25
|
+
|
|
26
|
+
// Encrypt the message
|
|
27
|
+
const encryptedBin = sodium.crypto_secretbox_easy(
|
|
28
|
+
sodium.from_string(value),
|
|
29
|
+
nonce,
|
|
30
|
+
cryptoKey,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Combine nonce and ciphertext, then encode to base64
|
|
34
|
+
const combined = new Uint8Array(nonce.length + encryptedBin.length);
|
|
35
|
+
combined.set(nonce);
|
|
36
|
+
combined.set(encryptedBin, nonce.length);
|
|
37
|
+
|
|
38
|
+
return sodium.to_base64(combined, sodium.base64_variants.ORIGINAL);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Decrypt a value encrypted with a symmetric key
|
|
43
|
+
*
|
|
44
|
+
* @param encryptedValue - The base64-encoded encrypted value with nonce
|
|
45
|
+
* @param key - The decryption key
|
|
46
|
+
* @returns The decrypted string
|
|
47
|
+
*/
|
|
48
|
+
export async function decryptWithKey(
|
|
49
|
+
encryptedValue: string,
|
|
50
|
+
key: string,
|
|
51
|
+
): Promise<string> {
|
|
52
|
+
// Initialize libsodium
|
|
53
|
+
await sodium.ready;
|
|
54
|
+
|
|
55
|
+
// Derive a key from the passphrase
|
|
56
|
+
const cryptoKey = sodium.crypto_generichash(
|
|
57
|
+
sodium.crypto_secretbox_KEYBYTES,
|
|
58
|
+
sodium.from_string(key),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Decode the base64 combined value
|
|
62
|
+
const combined = sodium.from_base64(
|
|
63
|
+
encryptedValue,
|
|
64
|
+
sodium.base64_variants.ORIGINAL,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Extract nonce and ciphertext
|
|
68
|
+
const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES);
|
|
69
|
+
const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES);
|
|
70
|
+
|
|
71
|
+
// Decrypt the message
|
|
72
|
+
const decryptedBin = sodium.crypto_secretbox_open_easy(
|
|
73
|
+
ciphertext,
|
|
74
|
+
nonce,
|
|
75
|
+
cryptoKey,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return sodium.to_string(decryptedBin);
|
|
79
|
+
}
|
package/src/github/secret.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import sodium from "libsodium-wrappers";
|
|
2
2
|
import { type Context, Resource } from "../resource";
|
|
3
|
+
import type { Secret } from "../secret";
|
|
3
4
|
import { createGitHubClient, verifyGitHubAuth } from "./client";
|
|
4
|
-
|
|
5
5
|
/**
|
|
6
6
|
* Properties for creating or updating a GitHub Secret
|
|
7
7
|
*/
|
|
@@ -24,7 +24,7 @@ export interface GitHubSecretProps {
|
|
|
24
24
|
/**
|
|
25
25
|
* Secret value (will be stored securely on GitHub)
|
|
26
26
|
*/
|
|
27
|
-
value:
|
|
27
|
+
value: Secret;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Optional GitHub API token (overrides environment variable)
|
|
@@ -47,31 +47,6 @@ export interface GitHubSecretOutput extends Omit<GitHubSecretProps, "value"> {
|
|
|
47
47
|
updatedAt: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
* Encrypt a value for GitHub using libsodium
|
|
52
|
-
* Based on GitHub's documentation for encrypting secrets
|
|
53
|
-
*
|
|
54
|
-
* @param value - The secret value to encrypt
|
|
55
|
-
* @param publicKey - The base64-encoded public key from GitHub
|
|
56
|
-
* @returns The base64-encoded encrypted value
|
|
57
|
-
*/
|
|
58
|
-
async function encryptValue(value: string, publicKey: string): Promise<string> {
|
|
59
|
-
// Initialize libsodium
|
|
60
|
-
await sodium.ready;
|
|
61
|
-
|
|
62
|
-
// Convert the public key from base64 to binary
|
|
63
|
-
const binKey = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
|
|
64
|
-
|
|
65
|
-
// Convert the message to a Uint8Array
|
|
66
|
-
const binMessage = sodium.from_string(value);
|
|
67
|
-
|
|
68
|
-
// Encrypt the message with the public key using libsodium's sealed box
|
|
69
|
-
const encryptedBin = sodium.crypto_box_seal(binMessage, binKey);
|
|
70
|
-
|
|
71
|
-
// Convert the encrypted message to base64
|
|
72
|
-
return sodium.to_base64(encryptedBin, sodium.base64_variants.ORIGINAL);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
50
|
/**
|
|
76
51
|
* Resource for managing GitHub repository secrets
|
|
77
52
|
*/
|
|
@@ -121,7 +96,10 @@ export class GitHubSecret extends Resource(
|
|
|
121
96
|
);
|
|
122
97
|
|
|
123
98
|
// Encrypt the secret value using libsodium
|
|
124
|
-
const encryptedValue = await
|
|
99
|
+
const encryptedValue = await encryptString(
|
|
100
|
+
props.value.unencrypted,
|
|
101
|
+
publicKey.key,
|
|
102
|
+
);
|
|
125
103
|
|
|
126
104
|
// Create or update the secret with the encrypted value and key_id
|
|
127
105
|
const response = await octokit.rest.actions.createOrUpdateRepoSecret({
|
|
@@ -132,11 +110,6 @@ export class GitHubSecret extends Resource(
|
|
|
132
110
|
key_id: publicKey.key_id,
|
|
133
111
|
});
|
|
134
112
|
|
|
135
|
-
// Log success
|
|
136
|
-
if (!ctx.quiet) {
|
|
137
|
-
console.log(`Successfully created/updated secret: ${props.name}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
113
|
// GitHub doesn't return the secret details on create/update, so we need to construct it
|
|
141
114
|
const output: GitHubSecretOutput = {
|
|
142
115
|
id: `${props.owner}/${props.repository}/${props.name}`,
|
|
@@ -170,3 +143,31 @@ export class GitHubSecret extends Resource(
|
|
|
170
143
|
}
|
|
171
144
|
},
|
|
172
145
|
) {}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Encrypt a value for GitHub using libsodium
|
|
149
|
+
* Based on GitHub's documentation for encrypting secrets
|
|
150
|
+
*
|
|
151
|
+
* @param value - The secret value to encrypt
|
|
152
|
+
* @param publicKey - The base64-encoded public key from GitHub
|
|
153
|
+
* @returns The base64-encoded encrypted value
|
|
154
|
+
*/
|
|
155
|
+
async function encryptString(
|
|
156
|
+
value: string,
|
|
157
|
+
publicKey: string,
|
|
158
|
+
): Promise<string> {
|
|
159
|
+
// Initialize libsodium
|
|
160
|
+
await sodium.ready;
|
|
161
|
+
|
|
162
|
+
// Convert the public key from base64 to binary
|
|
163
|
+
const binKey = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
|
|
164
|
+
|
|
165
|
+
// Convert the message to a Uint8Array
|
|
166
|
+
const binMessage = sodium.from_string(value);
|
|
167
|
+
|
|
168
|
+
// Encrypt the message with the public key using libsodium's sealed box
|
|
169
|
+
const encryptedBin = sodium.crypto_box_seal(binMessage, binKey);
|
|
170
|
+
|
|
171
|
+
// Convert the encrypted message to base64
|
|
172
|
+
return sodium.to_base64(encryptedBin, sodium.base64_variants.ORIGINAL);
|
|
173
|
+
}
|
package/src/index.ts
CHANGED
package/src/input.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { Output } from "./output";
|
|
2
|
+
import type { Secret } from "./secret";
|
|
2
3
|
|
|
3
4
|
export type Inputs<P extends any[]> = P extends [infer First, ...infer Rest]
|
|
4
5
|
? [Input<First>, ...Inputs<Rest>]
|
|
5
6
|
: [];
|
|
6
7
|
|
|
7
|
-
export type Input<T = any> =
|
|
8
|
-
| T
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
?
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
export type Input<T = any> = T extends Secret
|
|
9
|
+
? T | Output<T>
|
|
10
|
+
:
|
|
11
|
+
| T
|
|
12
|
+
| Output<T>
|
|
13
|
+
| (T extends any[] ? Inputs<T> : never)
|
|
14
|
+
| (T extends string | number | boolean ? Output<T> : never)
|
|
15
|
+
| (T extends object
|
|
16
|
+
? {
|
|
17
|
+
[k in keyof T]: Input<T[k]>;
|
|
18
|
+
}
|
|
19
|
+
: never);
|
package/src/output.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Secret } from "./secret";
|
|
2
|
+
|
|
1
3
|
export function isOutput<T>(value: any): value is Output<T> {
|
|
2
4
|
return (
|
|
3
5
|
value && typeof value === "object" && typeof value.apply === "function"
|
|
@@ -45,15 +47,17 @@ export class OutputChain<T, U> {
|
|
|
45
47
|
|
|
46
48
|
export type Resolved<O> = O extends Output<infer U>
|
|
47
49
|
? U
|
|
48
|
-
: O extends
|
|
49
|
-
?
|
|
50
|
-
: O extends
|
|
51
|
-
?
|
|
52
|
-
: O extends
|
|
53
|
-
?
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
: O extends Secret
|
|
51
|
+
? Secret
|
|
52
|
+
: O extends null
|
|
53
|
+
? O
|
|
54
|
+
: O extends any[]
|
|
55
|
+
? ResolveN<O>
|
|
56
|
+
: O extends object
|
|
57
|
+
? {
|
|
58
|
+
[k in keyof O]: Resolved<O[k]>;
|
|
59
|
+
}
|
|
60
|
+
: O;
|
|
57
61
|
|
|
58
62
|
type ResolveN<O extends any[]> = O extends [infer First, ...infer Rest]
|
|
59
63
|
? [Resolved<First>, ...ResolveN<Rest>]
|
package/src/secret.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { Input } from "./input";
|
|
3
|
+
import { type Output, isOutput } from "./output";
|
|
4
|
+
|
|
5
|
+
const passphraseContext = new AsyncLocalStorage<string>();
|
|
6
|
+
|
|
7
|
+
export function secret<S extends Input<string> | undefined>(
|
|
8
|
+
unencrypted: S,
|
|
9
|
+
): S extends undefined
|
|
10
|
+
? never
|
|
11
|
+
: S extends Output<string>
|
|
12
|
+
? Output<Secret>
|
|
13
|
+
: Secret {
|
|
14
|
+
if (unencrypted === undefined) {
|
|
15
|
+
throw new Error("Secret cannot be undefined");
|
|
16
|
+
}
|
|
17
|
+
return (
|
|
18
|
+
isOutput(unencrypted) ? unencrypted.apply(secret) : new Secret(unencrypted)
|
|
19
|
+
) as any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class Secret {
|
|
23
|
+
public readonly type = "secret";
|
|
24
|
+
constructor(readonly unencrypted: string) {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function setSecretPassphrase(passphrase: string) {
|
|
28
|
+
passphraseContext.enterWith(passphrase);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getSecretPassphrase(): string {
|
|
32
|
+
const passphrase = passphraseContext.getStore();
|
|
33
|
+
if (!passphrase) {
|
|
34
|
+
throw new Error("No passphrase set");
|
|
35
|
+
}
|
|
36
|
+
return passphrase;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function tryGetSecretPassphrase(): string | undefined {
|
|
40
|
+
return passphraseContext.getStore();
|
|
41
|
+
}
|
package/src/serde.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { decryptWithKey, encryptWithKey } from "./encrypt";
|
|
2
|
+
import { Secret, getSecretPassphrase } from "./secret";
|
|
3
|
+
|
|
4
|
+
export async function serialize(value: any): Promise<any> {
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
return Promise.all(value.map(serialize));
|
|
7
|
+
} else if (value instanceof Secret) {
|
|
8
|
+
return {
|
|
9
|
+
"@secret": await encryptWithKey(value.unencrypted, getSecretPassphrase()),
|
|
10
|
+
};
|
|
11
|
+
} else if (value && typeof value === "object") {
|
|
12
|
+
return Object.fromEntries(
|
|
13
|
+
await Promise.all(
|
|
14
|
+
Object.entries(value).map(async ([key, value]) => [
|
|
15
|
+
key,
|
|
16
|
+
await serialize(value),
|
|
17
|
+
]),
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function deserialize(value: any): Promise<any> {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return await Promise.all(
|
|
27
|
+
value.map(async (item) => await deserialize(item)),
|
|
28
|
+
);
|
|
29
|
+
} else if (value && typeof value === "object") {
|
|
30
|
+
if (typeof value["@secret"] === "string") {
|
|
31
|
+
return new Secret(
|
|
32
|
+
await decryptWithKey(value["@secret"], getSecretPassphrase()),
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
return Object.fromEntries(
|
|
36
|
+
await Promise.all(
|
|
37
|
+
Object.entries(value).map(async ([key, value]) => [
|
|
38
|
+
key,
|
|
39
|
+
await deserialize(value),
|
|
40
|
+
]),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
package/src/state.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ignore } from "./error";
|
|
4
|
+
import { deserialize, serialize } from "./serde";
|
|
4
5
|
|
|
5
6
|
export interface State {
|
|
6
7
|
status:
|
|
@@ -79,7 +80,7 @@ export class FileSystemStateStore implements StateStore {
|
|
|
79
80
|
await this.getPath(key),
|
|
80
81
|
"utf8",
|
|
81
82
|
);
|
|
82
|
-
return JSON.parse(content) as State;
|
|
83
|
+
return (await deserialize(JSON.parse(content))) as State;
|
|
83
84
|
} catch (error: any) {
|
|
84
85
|
if (error.code === "ENOENT") {
|
|
85
86
|
return undefined;
|
|
@@ -91,7 +92,7 @@ export class FileSystemStateStore implements StateStore {
|
|
|
91
92
|
async set(key: string, value: State): Promise<void> {
|
|
92
93
|
return fs.promises.writeFile(
|
|
93
94
|
await this.getPath(key),
|
|
94
|
-
JSON.stringify(value, null, 2),
|
|
95
|
+
JSON.stringify(await serialize(value), null, 2),
|
|
95
96
|
);
|
|
96
97
|
}
|
|
97
98
|
|