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.
Files changed (66) hide show
  1. package/lib/alchemize.d.ts +4 -0
  2. package/lib/alchemize.d.ts.map +1 -1
  3. package/lib/alchemize.js +4 -0
  4. package/lib/alchemize.js.map +1 -1
  5. package/lib/apply.d.ts.map +1 -1
  6. package/lib/apply.js +4 -0
  7. package/lib/apply.js.map +1 -1
  8. package/lib/cloudflare/bindings.d.ts +3 -2
  9. package/lib/cloudflare/bindings.d.ts.map +1 -1
  10. package/lib/cloudflare/bound.d.ts +8 -0
  11. package/lib/cloudflare/bound.d.ts.map +1 -0
  12. package/lib/cloudflare/bound.js +2 -0
  13. package/lib/cloudflare/bound.js.map +1 -0
  14. package/lib/cloudflare/bucket.d.ts +3 -3
  15. package/lib/cloudflare/bucket.d.ts.map +1 -1
  16. package/lib/cloudflare/bucket.js +1 -1
  17. package/lib/cloudflare/bucket.js.map +1 -1
  18. package/lib/cloudflare/static-site.js.map +1 -1
  19. package/lib/cloudflare/worker.d.ts +12 -6
  20. package/lib/cloudflare/worker.d.ts.map +1 -1
  21. package/lib/cloudflare/worker.js +14 -2
  22. package/lib/cloudflare/worker.js.map +1 -1
  23. package/lib/encrypt.d.ts +17 -0
  24. package/lib/encrypt.d.ts.map +1 -0
  25. package/lib/encrypt.js +45 -0
  26. package/lib/encrypt.js.map +1 -0
  27. package/lib/github/secret.d.ts +2 -1
  28. package/lib/github/secret.d.ts.map +1 -1
  29. package/lib/github/secret.js +21 -25
  30. package/lib/github/secret.js.map +1 -1
  31. package/lib/index.d.ts +1 -0
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.js +1 -0
  34. package/lib/index.js.map +1 -1
  35. package/lib/input.d.ts +2 -1
  36. package/lib/input.d.ts.map +1 -1
  37. package/lib/output.d.ts +2 -1
  38. package/lib/output.d.ts.map +1 -1
  39. package/lib/output.js.map +1 -1
  40. package/lib/secret.d.ts +12 -0
  41. package/lib/secret.d.ts.map +1 -0
  42. package/lib/secret.js +30 -0
  43. package/lib/secret.js.map +1 -0
  44. package/lib/serde.d.ts +3 -0
  45. package/lib/serde.d.ts.map +1 -0
  46. package/lib/serde.js +37 -0
  47. package/lib/serde.js.map +1 -0
  48. package/lib/state.d.ts.map +1 -1
  49. package/lib/state.js +3 -2
  50. package/lib/state.js.map +1 -1
  51. package/package.json +3 -6
  52. package/src/alchemize.ts +9 -0
  53. package/src/apply.ts +3 -0
  54. package/src/cloudflare/bindings.ts +4 -2
  55. package/src/cloudflare/bound.ts +18 -0
  56. package/src/cloudflare/bucket.ts +3 -3
  57. package/src/cloudflare/static-site.ts +1 -1
  58. package/src/cloudflare/worker.ts +77 -59
  59. package/src/encrypt.ts +79 -0
  60. package/src/github/secret.ts +34 -33
  61. package/src/index.ts +1 -0
  62. package/src/input.ts +13 -10
  63. package/src/output.ts +13 -9
  64. package/src/secret.ts +41 -0
  65. package/src/serde.ts +46 -0
  66. 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
+ }
@@ -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: string;
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 encryptValue(props.value, publicKey.key);
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
@@ -12,4 +12,5 @@ export {
12
12
  type DeleteContext,
13
13
  type UpdateContext,
14
14
  } from "./resource";
15
+ export * from "./secret";
15
16
  export type { FileSystemStateStore as StateStore } from "./state";
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
- | Output<T>
10
- | (T extends any[] ? Inputs<T> : never)
11
- | (T extends string | number | boolean ? Output<T> : never)
12
- | (T extends object
13
- ? {
14
- [k in keyof T]: Input<T[k]>;
15
- }
16
- : never);
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 null
49
- ? O
50
- : O extends any[]
51
- ? ResolveN<O>
52
- : O extends object
53
- ? {
54
- [k in keyof O]: Resolved<O[k]>;
55
- }
56
- : O;
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