alchemy 0.76.1 → 0.77.1
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/bin/alchemy.js +109 -29
- package/bin/services/execute-alchemy.ts +10 -1
- package/lib/aws/ec2/index.d.ts +1 -0
- package/lib/aws/ec2/index.d.ts.map +1 -1
- package/lib/aws/ec2/index.js +1 -0
- package/lib/aws/ec2/index.js.map +1 -1
- package/lib/aws/index.d.ts +1 -7
- package/lib/aws/index.d.ts.map +1 -1
- package/lib/aws/index.js +1 -8
- package/lib/aws/index.js.map +1 -1
- package/lib/cloudflare/bucket-custom-domain.d.ts +90 -0
- package/lib/cloudflare/bucket-custom-domain.d.ts.map +1 -0
- package/lib/cloudflare/bucket-custom-domain.js +81 -0
- package/lib/cloudflare/bucket-custom-domain.js.map +1 -0
- package/lib/cloudflare/bucket.d.ts +28 -4
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +41 -14
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/cloudflare-env-proxy.d.ts.map +1 -1
- package/lib/cloudflare/cloudflare-env-proxy.js +0 -3
- package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/container.d.ts.map +1 -1
- package/lib/cloudflare/container.js +8 -5
- package/lib/cloudflare/container.js.map +1 -1
- package/lib/cloudflare/d1-database.d.ts +13 -3
- package/lib/cloudflare/d1-database.d.ts.map +1 -1
- package/lib/cloudflare/d1-database.js +31 -15
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.js +73 -9
- package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
- package/lib/cloudflare/react-router/plugin.d.ts +2 -1
- package/lib/cloudflare/react-router/plugin.d.ts.map +1 -1
- package/lib/cloudflare/react-router/plugin.js +6 -14
- package/lib/cloudflare/react-router/plugin.js.map +1 -1
- package/lib/cloudflare/vite/plugin.d.ts.map +1 -1
- package/lib/cloudflare/vite/plugin.js +0 -3
- package/lib/cloudflare/vite/plugin.js.map +1 -1
- package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
- package/lib/cloudflare/worker-bundle.js +17 -0
- package/lib/cloudflare/worker-bundle.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +6 -4
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker-subdomain.d.ts.map +1 -1
- package/lib/cloudflare/worker-subdomain.js +0 -1
- package/lib/cloudflare/worker-subdomain.js.map +1 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +28 -38
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +2 -281
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +23 -11
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/docker/api.d.ts +4 -0
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +3 -0
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/image.d.ts +50 -12
- package/lib/docker/image.d.ts.map +1 -1
- package/lib/docker/image.js +86 -58
- package/lib/docker/image.js.map +1 -1
- package/lib/docker/remote-image.d.ts +1 -0
- package/lib/docker/remote-image.d.ts.map +1 -1
- package/lib/docker/remote-image.js +2 -0
- package/lib/docker/remote-image.js.map +1 -1
- package/lib/encrypt.d.ts +28 -3
- package/lib/encrypt.d.ts.map +1 -1
- package/lib/encrypt.js +77 -3
- package/lib/encrypt.js.map +1 -1
- package/lib/planetscale/role.d.ts +9 -0
- package/lib/planetscale/role.d.ts.map +1 -1
- package/lib/planetscale/role.js +18 -3
- package/lib/planetscale/role.js.map +1 -1
- package/lib/prisma-postgres/api.d.ts.map +1 -1
- package/lib/prisma-postgres/api.js +1 -0
- package/lib/prisma-postgres/api.js.map +1 -1
- package/lib/scope.d.ts +5 -0
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +23 -0
- package/lib/scope.js.map +1 -1
- package/lib/serde.js +1 -1
- package/lib/serde.js.map +1 -1
- package/lib/util/telemetry.js +4 -0
- package/lib/util/telemetry.js.map +1 -1
- package/package.json +12 -10
- package/src/aws/ec2/index.ts +1 -0
- package/src/aws/index.ts +1 -9
- package/src/cloudflare/bucket-custom-domain.ts +281 -0
- package/src/cloudflare/bucket.ts +85 -18
- package/src/cloudflare/cloudflare-env-proxy.ts +0 -3
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/container.ts +10 -7
- package/src/cloudflare/d1-database.ts +44 -6
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/miniflare/remote-binding-proxy.ts +80 -9
- package/src/cloudflare/react-router/plugin.ts +9 -21
- package/src/cloudflare/vite/plugin.ts +0 -3
- package/src/cloudflare/worker-bundle.ts +28 -1
- package/src/cloudflare/worker-metadata.ts +8 -4
- package/src/cloudflare/worker-subdomain.ts +0 -1
- package/src/cloudflare/worker.ts +32 -45
- package/src/cloudflare/wrangler.json.ts +62 -397
- package/src/docker/api.ts +7 -0
- package/src/docker/image.ts +164 -89
- package/src/docker/remote-image.ts +3 -0
- package/src/encrypt.ts +115 -3
- package/src/planetscale/role.ts +30 -3
- package/src/prisma-postgres/api.ts +1 -0
- package/src/scope.ts +27 -0
- package/src/serde.ts +1 -1
- package/src/util/telemetry.ts +5 -0
- package/workers/cloudflare-state-store.js +4802 -8
- package/workers/dofs-state-store.js +632 -2
- package/workers/remote-binding-proxy.js +1929 -3
- package/workers/tunnel-proxy.js +58 -6
- package/workers/tunnel-proxy.ts +1 -1
package/src/docker/image.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Context } from "../context.ts";
|
|
|
5
5
|
import { Resource } from "../resource.ts";
|
|
6
6
|
import type { Secret } from "../secret.ts";
|
|
7
7
|
import { DockerApi } from "./api.ts";
|
|
8
|
+
import type { RemoteImage } from "./remote-image.ts";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Options for building a Docker image
|
|
@@ -40,9 +41,28 @@ export interface DockerBuildOptions {
|
|
|
40
41
|
target?: string;
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
+
* Use an external cache source for a build
|
|
45
|
+
*
|
|
46
|
+
* @see https://docs.docker.com/reference/cli/docker/buildx/build/#cache-from
|
|
47
|
+
*
|
|
44
48
|
*/
|
|
45
49
|
cacheFrom?: string[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Export build cache to an external cache destination
|
|
53
|
+
*
|
|
54
|
+
* @see https://docs.docker.com/reference/cli/docker/buildx/build/#cache-to
|
|
55
|
+
*
|
|
56
|
+
*/
|
|
57
|
+
cacheTo?: string[];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Additional options to pass to the Docker build command. This serves as an escape hatch for any additional options that are not supported by the other properties.
|
|
61
|
+
*
|
|
62
|
+
* @see https://docs.docker.com/reference/cli/docker/buildx/build/#options
|
|
63
|
+
*
|
|
64
|
+
*/
|
|
65
|
+
options?: string[];
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
export interface ImageRegistry {
|
|
@@ -54,22 +74,12 @@ export interface ImageRegistry {
|
|
|
54
74
|
/**
|
|
55
75
|
* Properties for creating a Docker image
|
|
56
76
|
*/
|
|
57
|
-
export
|
|
58
|
-
/**
|
|
59
|
-
* Repository name for the image (e.g., "username/image")
|
|
60
|
-
*/
|
|
61
|
-
name?: string;
|
|
62
|
-
|
|
77
|
+
export type ImageProps = {
|
|
63
78
|
/**
|
|
64
79
|
* Tag for the image (e.g., "latest")
|
|
65
80
|
*/
|
|
66
81
|
tag?: string;
|
|
67
82
|
|
|
68
|
-
/**
|
|
69
|
-
* Build configuration
|
|
70
|
-
*/
|
|
71
|
-
build?: DockerBuildOptions;
|
|
72
|
-
|
|
73
83
|
/**
|
|
74
84
|
* Registry credentials
|
|
75
85
|
*/
|
|
@@ -79,12 +89,36 @@ export interface ImageProps {
|
|
|
79
89
|
* Whether to skip pushing the image to registry
|
|
80
90
|
*/
|
|
81
91
|
skipPush?: boolean;
|
|
82
|
-
}
|
|
92
|
+
} & (
|
|
93
|
+
| {
|
|
94
|
+
/**
|
|
95
|
+
* Image name or reference (e.g., "nginx:alpine")
|
|
96
|
+
*/
|
|
97
|
+
image: string | Image | RemoteImage;
|
|
98
|
+
build?: never;
|
|
99
|
+
name?: never;
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
/**
|
|
103
|
+
* Repository name for the image (e.g., "username/image")
|
|
104
|
+
*
|
|
105
|
+
* @default - the id
|
|
106
|
+
*/
|
|
107
|
+
name?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Build configuration
|
|
110
|
+
*/
|
|
111
|
+
build: DockerBuildOptions;
|
|
112
|
+
|
|
113
|
+
image?: never;
|
|
114
|
+
}
|
|
115
|
+
);
|
|
83
116
|
|
|
84
117
|
/**
|
|
85
118
|
* Docker Image resource
|
|
86
119
|
*/
|
|
87
|
-
export interface Image
|
|
120
|
+
export interface Image {
|
|
121
|
+
kind: "Image";
|
|
88
122
|
/**
|
|
89
123
|
* Image name
|
|
90
124
|
*/
|
|
@@ -109,6 +143,15 @@ export interface Image extends ImageProps {
|
|
|
109
143
|
* Time when the image was built
|
|
110
144
|
*/
|
|
111
145
|
builtAt: number;
|
|
146
|
+
/**
|
|
147
|
+
* Tag for the image
|
|
148
|
+
*/
|
|
149
|
+
tag: string;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build configuration
|
|
153
|
+
*/
|
|
154
|
+
build: DockerBuildOptions | undefined;
|
|
112
155
|
}
|
|
113
156
|
|
|
114
157
|
/**
|
|
@@ -142,12 +185,32 @@ export const Image = Resource(
|
|
|
142
185
|
// No action needed for delete as Docker images aren't automatically removed
|
|
143
186
|
// This is intentional as other resources might depend on the same image
|
|
144
187
|
return this.destroy();
|
|
145
|
-
}
|
|
146
|
-
// Normalize properties
|
|
147
|
-
const tag = props.tag || "latest";
|
|
148
|
-
const name = props.name || id;
|
|
149
|
-
const imageRef = `${name}:${tag}`;
|
|
188
|
+
}
|
|
150
189
|
|
|
190
|
+
const tag = props.tag || "latest";
|
|
191
|
+
const name =
|
|
192
|
+
props.name ||
|
|
193
|
+
(typeof props.image === "string"
|
|
194
|
+
? props.image
|
|
195
|
+
: props.image?.name
|
|
196
|
+
)?.split(":")[0] ||
|
|
197
|
+
id;
|
|
198
|
+
const imageRef = `${name}:${tag}`;
|
|
199
|
+
let imageId: string | undefined;
|
|
200
|
+
if (props.image) {
|
|
201
|
+
const image =
|
|
202
|
+
typeof props.image === "string" ? props.image : props.image.imageRef;
|
|
203
|
+
|
|
204
|
+
const kind =
|
|
205
|
+
typeof props.image === "object" && props.image.kind === "Image"
|
|
206
|
+
? "local"
|
|
207
|
+
: "remote";
|
|
208
|
+
if (kind === "remote") {
|
|
209
|
+
await api.pullImage(image);
|
|
210
|
+
}
|
|
211
|
+
await api.tagImage(image, imageRef);
|
|
212
|
+
// TODO: Extract image ID from pull output if available
|
|
213
|
+
} else {
|
|
151
214
|
let context: string;
|
|
152
215
|
let dockerfile: string;
|
|
153
216
|
if (props.build?.dockerfile && props.build?.context) {
|
|
@@ -170,7 +233,7 @@ export const Image = Resource(
|
|
|
170
233
|
const buildOptions: Record<string, string> = props.build?.args || {};
|
|
171
234
|
|
|
172
235
|
// Add platform if specified
|
|
173
|
-
|
|
236
|
+
const buildArgs = ["build", "-t", imageRef];
|
|
174
237
|
|
|
175
238
|
if (props.build?.platform) {
|
|
176
239
|
buildArgs.push("--platform", props.build.platform);
|
|
@@ -188,11 +251,23 @@ export const Image = Resource(
|
|
|
188
251
|
}
|
|
189
252
|
}
|
|
190
253
|
|
|
254
|
+
// Add cache destinations if specified
|
|
255
|
+
if (props.build?.cacheTo && props.build.cacheTo.length > 0) {
|
|
256
|
+
for (const cacheTarget of props.build.cacheTo) {
|
|
257
|
+
buildArgs.push("--cache-to", cacheTarget);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
191
261
|
// Add build arguments
|
|
192
262
|
for (const [key, value] of Object.entries(buildOptions)) {
|
|
193
263
|
buildArgs.push("--build-arg", `${key}=${value}`);
|
|
194
264
|
}
|
|
195
265
|
|
|
266
|
+
// Add build options if specified
|
|
267
|
+
if (props.build?.options && props.build.options.length > 0) {
|
|
268
|
+
buildArgs.push(...props.build.options);
|
|
269
|
+
}
|
|
270
|
+
|
|
196
271
|
buildArgs.push("-f", dockerfile);
|
|
197
272
|
|
|
198
273
|
// Add context path
|
|
@@ -203,77 +278,77 @@ export const Image = Resource(
|
|
|
203
278
|
|
|
204
279
|
// Extract image ID from build output if available
|
|
205
280
|
const imageIdMatch = /Successfully built ([a-f0-9]+)/.exec(stdout);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Push the image
|
|
245
|
-
const { stdout: pushOut } = await api.exec(["push", targetImage]);
|
|
246
|
-
|
|
247
|
-
// Attempt to extract the repo digest from push output
|
|
248
|
-
const digestMatch = /digest:\s+([a-z0-9]+:[a-f0-9]{64})/.exec(
|
|
249
|
-
pushOut,
|
|
250
|
-
);
|
|
251
|
-
if (digestMatch) {
|
|
252
|
-
const digestHash = digestMatch[1];
|
|
253
|
-
// Strip tag (anything after last :) to build image@digest reference
|
|
254
|
-
const [repoWithoutTag] =
|
|
255
|
-
targetImage.split(":").length > 2
|
|
256
|
-
? [targetImage] // unlikely but safety
|
|
257
|
-
: [targetImage.substring(0, targetImage.lastIndexOf(":"))];
|
|
258
|
-
repoDigest = `${repoWithoutTag}@${digestHash}`;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Update the final image reference to point at the pushed image
|
|
262
|
-
finalImageRef = targetImage;
|
|
263
|
-
} finally {
|
|
264
|
-
// Clean up credentials from the isolated config
|
|
265
|
-
await api.logout(registryHost);
|
|
281
|
+
imageId = imageIdMatch ? imageIdMatch[1] : undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Handle push if required
|
|
285
|
+
let repoDigest: string | undefined;
|
|
286
|
+
let finalImageRef = imageRef;
|
|
287
|
+
if (props.registry && !props.skipPush) {
|
|
288
|
+
const { server, username, password } = props.registry;
|
|
289
|
+
|
|
290
|
+
// Ensure the registry server does not have trailing slash
|
|
291
|
+
const registryHost = server.replace(/\/$/, "");
|
|
292
|
+
|
|
293
|
+
// Determine if the built image already includes a registry host (e.g. ghcr.io/user/repo)
|
|
294
|
+
const firstSegment = imageRef.split("/")[0];
|
|
295
|
+
const hasRegistryPrefix = firstSegment.includes(".");
|
|
296
|
+
|
|
297
|
+
// Compose the target image reference that will be pushed
|
|
298
|
+
const targetImage = hasRegistryPrefix
|
|
299
|
+
? imageRef // already fully-qualified
|
|
300
|
+
: `${registryHost}/${imageRef}`;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Create a temporary directory that will act as an isolated Docker config
|
|
304
|
+
// (credentials) directory. This prevents race-conditions when multiple
|
|
305
|
+
// concurrent tests perform `docker login` / `logout` by ensuring each
|
|
306
|
+
// Image operation has its own credential store.
|
|
307
|
+
const tempConfigDir = await fs.mkdtemp(
|
|
308
|
+
path.join(os.tmpdir(), "docker-config-"),
|
|
309
|
+
);
|
|
310
|
+
const api = new DockerApi({ configDir: tempConfigDir });
|
|
311
|
+
|
|
312
|
+
// Authenticate to registry using the isolated config directory
|
|
313
|
+
await api.login(registryHost, username, password.unencrypted);
|
|
314
|
+
|
|
315
|
+
// Tag local image with fully qualified name if necessary
|
|
316
|
+
if (targetImage !== imageRef) {
|
|
317
|
+
await api.exec(["tag", imageRef, targetImage]);
|
|
266
318
|
}
|
|
267
|
-
}
|
|
268
319
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
320
|
+
// Push the image
|
|
321
|
+
const { stdout: pushOut } = await api.exec(["push", targetImage]);
|
|
322
|
+
|
|
323
|
+
// Attempt to extract the repo digest from push output
|
|
324
|
+
const digestMatch = /digest:\s+([a-z0-9]+:[a-f0-9]{64})/.exec(pushOut);
|
|
325
|
+
if (digestMatch) {
|
|
326
|
+
const digestHash = digestMatch[1];
|
|
327
|
+
// Strip tag (anything after last :) to build image@digest reference
|
|
328
|
+
const [repoWithoutTag] =
|
|
329
|
+
targetImage.split(":").length > 2
|
|
330
|
+
? [targetImage] // unlikely but safety
|
|
331
|
+
: [targetImage.substring(0, targetImage.lastIndexOf(":"))];
|
|
332
|
+
repoDigest = `${repoWithoutTag}@${digestHash}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Update the final image reference to point at the pushed image
|
|
336
|
+
finalImageRef = targetImage;
|
|
337
|
+
} finally {
|
|
338
|
+
// Clean up credentials from the isolated config
|
|
339
|
+
await api.logout(registryHost);
|
|
340
|
+
}
|
|
277
341
|
}
|
|
342
|
+
return {
|
|
343
|
+
kind: "Image",
|
|
344
|
+
...props,
|
|
345
|
+
tag,
|
|
346
|
+
name,
|
|
347
|
+
imageRef: finalImageRef,
|
|
348
|
+
imageId,
|
|
349
|
+
repoDigest,
|
|
350
|
+
builtAt: Date.now(),
|
|
351
|
+
build: props.build,
|
|
352
|
+
};
|
|
278
353
|
},
|
|
279
354
|
);
|
|
@@ -26,6 +26,7 @@ export interface RemoteImageProps {
|
|
|
26
26
|
* Docker Remote Image resource
|
|
27
27
|
*/
|
|
28
28
|
export interface RemoteImage extends RemoteImageProps {
|
|
29
|
+
kind: "RemoteImage";
|
|
29
30
|
/**
|
|
30
31
|
* Full image reference (name:tag)
|
|
31
32
|
*/
|
|
@@ -71,8 +72,10 @@ export const RemoteImage = Resource(
|
|
|
71
72
|
await api.pullImage(imageRef);
|
|
72
73
|
|
|
73
74
|
return {
|
|
75
|
+
kind: "RemoteImage",
|
|
74
76
|
...props,
|
|
75
77
|
imageRef,
|
|
78
|
+
tag,
|
|
76
79
|
createdAt: Date.now(),
|
|
77
80
|
};
|
|
78
81
|
}
|
package/src/encrypt.ts
CHANGED
|
@@ -1,11 +1,122 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const KEY_LEN = 32;
|
|
4
|
+
const SCRYPT_N = 16384;
|
|
5
|
+
const SCRYPT_R = 8;
|
|
6
|
+
const SCRYPT_P = 1;
|
|
7
|
+
const SALT_LEN = 16;
|
|
8
|
+
const IV_LEN = 12;
|
|
9
|
+
const ALGO = "aes-256-gcm";
|
|
10
|
+
|
|
11
|
+
interface Encrypted {
|
|
12
|
+
version: "v1";
|
|
13
|
+
ciphertext: string; // base64
|
|
14
|
+
iv: string; // base64
|
|
15
|
+
salt: string; // base64
|
|
16
|
+
tag: string; // base64
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Decrypts a value encrypted with a symmetric key.
|
|
21
|
+
* For backwards compatibility, uses the legacy libsodium implementation if the value is a string.
|
|
22
|
+
* Otherwise, uses the AES-256-GCM implementation with scrypt key derivation.
|
|
23
|
+
*/
|
|
24
|
+
export function decryptWithKey(
|
|
25
|
+
value: string | Encrypted,
|
|
26
|
+
key: string,
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
return libsodiumDecrypt(value, key);
|
|
30
|
+
}
|
|
31
|
+
return aes256Decrypt(value, key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Encrypt a value with a symmetric key using AES-256-GCM with scrypt key derivation.
|
|
36
|
+
*/
|
|
37
|
+
export async function encrypt(
|
|
38
|
+
value: string,
|
|
39
|
+
passphrase: string,
|
|
40
|
+
): Promise<Encrypted> {
|
|
41
|
+
const salt = crypto.randomBytes(SALT_LEN);
|
|
42
|
+
const key = await deriveScryptKey(passphrase, salt);
|
|
43
|
+
const iv = crypto.randomBytes(IV_LEN);
|
|
44
|
+
|
|
45
|
+
const cipher = crypto.createCipheriv(ALGO, key, iv);
|
|
46
|
+
const ciphertext = Buffer.concat([
|
|
47
|
+
cipher.update(value, "utf8"),
|
|
48
|
+
cipher.final(),
|
|
49
|
+
]);
|
|
50
|
+
const tag = cipher.getAuthTag();
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
version: "v1",
|
|
54
|
+
ciphertext: ciphertext.toString("base64"),
|
|
55
|
+
iv: iv.toString("base64"),
|
|
56
|
+
salt: salt.toString("base64"),
|
|
57
|
+
tag: tag.toString("base64"),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Decrypt a value encrypted with a symmetric key using AES-256-GCM with scrypt key derivation.
|
|
63
|
+
* @internal - Exposed for testing. Use `decryptWithKey` instead.
|
|
64
|
+
*/
|
|
65
|
+
export async function aes256Decrypt(
|
|
66
|
+
parts: Encrypted,
|
|
67
|
+
passphrase: string,
|
|
68
|
+
): Promise<string> {
|
|
69
|
+
const salt = Buffer.from(parts.salt, "base64");
|
|
70
|
+
const iv = Buffer.from(parts.iv, "base64");
|
|
71
|
+
const ciphertext = Buffer.from(parts.ciphertext, "base64");
|
|
72
|
+
const tag = Buffer.from(parts.tag, "base64");
|
|
73
|
+
|
|
74
|
+
const key = await deriveScryptKey(passphrase, salt);
|
|
75
|
+
|
|
76
|
+
const decipher = crypto.createDecipheriv(ALGO, key, iv);
|
|
77
|
+
decipher.setAuthTag(tag);
|
|
78
|
+
|
|
79
|
+
const plaintext = Buffer.concat([
|
|
80
|
+
decipher.update(ciphertext),
|
|
81
|
+
decipher.final(),
|
|
82
|
+
]);
|
|
83
|
+
return plaintext.toString("utf8");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function deriveScryptKey(
|
|
87
|
+
passphrase: string,
|
|
88
|
+
salt: Buffer,
|
|
89
|
+
): Promise<Buffer> {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
crypto.scrypt(
|
|
92
|
+
passphrase,
|
|
93
|
+
salt,
|
|
94
|
+
KEY_LEN,
|
|
95
|
+
{
|
|
96
|
+
N: SCRYPT_N,
|
|
97
|
+
r: SCRYPT_R,
|
|
98
|
+
p: SCRYPT_P,
|
|
99
|
+
},
|
|
100
|
+
(err, derivedKey) => {
|
|
101
|
+
if (err) reject(err);
|
|
102
|
+
else resolve(derivedKey);
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
1
108
|
/**
|
|
2
109
|
* Encrypt a value with a symmetric key using libsodium
|
|
110
|
+
* @internal - Exposed to test backwards compatibility. Use `encrypt` instead.
|
|
3
111
|
*
|
|
4
112
|
* @param value - The value to encrypt
|
|
5
113
|
* @param key - The encryption key
|
|
6
114
|
* @returns The base64-encoded encrypted value with nonce
|
|
7
115
|
*/
|
|
8
|
-
export async function
|
|
116
|
+
export async function libsodiumEncrypt(
|
|
117
|
+
value: string,
|
|
118
|
+
key: string,
|
|
119
|
+
): Promise<string> {
|
|
9
120
|
const sodium = (await import("libsodium-wrappers")).default;
|
|
10
121
|
// Initialize libsodium
|
|
11
122
|
await sodium.ready;
|
|
@@ -35,13 +146,14 @@ export async function encrypt(value: string, key: string): Promise<string> {
|
|
|
35
146
|
}
|
|
36
147
|
|
|
37
148
|
/**
|
|
38
|
-
* Decrypt a value encrypted with a symmetric key
|
|
149
|
+
* Decrypt a value encrypted with a symmetric key using libsodium.
|
|
150
|
+
* @internal - Exposed to test backwards compatibility. Use `decryptWithKey` instead.
|
|
39
151
|
*
|
|
40
152
|
* @param encryptedValue - The base64-encoded encrypted value with nonce
|
|
41
153
|
* @param key - The decryption key
|
|
42
154
|
* @returns The decrypted string
|
|
43
155
|
*/
|
|
44
|
-
export async function
|
|
156
|
+
export async function libsodiumDecrypt(
|
|
45
157
|
encryptedValue: string,
|
|
46
158
|
key: string,
|
|
47
159
|
): Promise<string> {
|
package/src/planetscale/role.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface RoleProps extends PlanetScaleProps {
|
|
|
51
51
|
* @default true
|
|
52
52
|
*/
|
|
53
53
|
delete?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* successor role
|
|
57
|
+
* @default postgres
|
|
58
|
+
*/
|
|
59
|
+
successor?: string | Role;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
/**
|
|
@@ -123,6 +129,11 @@ export interface Role extends Omit<RoleProps, "inheritedRoles"> {
|
|
|
123
129
|
* The roles that this role inherits from.
|
|
124
130
|
*/
|
|
125
131
|
inheritedRoles: InheritedRole[];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The successor role
|
|
135
|
+
*/
|
|
136
|
+
successor: string;
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
/**
|
|
@@ -198,10 +209,18 @@ export const Role = Resource(
|
|
|
198
209
|
: props.inheritedRoles.inheritedRoles;
|
|
199
210
|
const shouldDelete = props.delete ?? false;
|
|
200
211
|
|
|
212
|
+
const successorRole =
|
|
213
|
+
typeof props.successor === "string"
|
|
214
|
+
? props.successor
|
|
215
|
+
: (props.successor?.name ?? "postgres");
|
|
216
|
+
|
|
201
217
|
switch (this.phase) {
|
|
202
218
|
case "delete": {
|
|
203
219
|
if (shouldDelete && this.output?.id) {
|
|
204
220
|
const res = await api.deleteRole({
|
|
221
|
+
body: {
|
|
222
|
+
successor: successorRole,
|
|
223
|
+
},
|
|
205
224
|
path: {
|
|
206
225
|
organization,
|
|
207
226
|
database,
|
|
@@ -275,6 +294,7 @@ export const Role = Resource(
|
|
|
275
294
|
expiresAt: data.expires_at,
|
|
276
295
|
databaseName: data.database_name,
|
|
277
296
|
inheritedRoles,
|
|
297
|
+
successor: successorRole,
|
|
278
298
|
connectionUrl: alchemy.secret(
|
|
279
299
|
`postgresql://${data.username}:${data.password}@${data.access_host_url}:5432/${data.database_name}?sslmode=verify-full`,
|
|
280
300
|
),
|
|
@@ -284,9 +304,16 @@ export const Role = Resource(
|
|
|
284
304
|
};
|
|
285
305
|
}
|
|
286
306
|
case "update": {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
307
|
+
if (successorRole !== this.output.successor) {
|
|
308
|
+
// According to the types, the only property that can be updated is the name.
|
|
309
|
+
// However, I was getting 500 errors when trying to update the name, so we'll just replace.
|
|
310
|
+
return this.replace();
|
|
311
|
+
} else {
|
|
312
|
+
return {
|
|
313
|
+
...this.output,
|
|
314
|
+
successor: successorRole,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
290
317
|
}
|
|
291
318
|
}
|
|
292
319
|
},
|
package/src/scope.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
2
5
|
import util from "node:util";
|
|
3
6
|
import path from "pathe";
|
|
4
7
|
import pc from "picocolors";
|
|
@@ -357,6 +360,30 @@ export class Scope {
|
|
|
357
360
|
return result.extracted as any;
|
|
358
361
|
}
|
|
359
362
|
|
|
363
|
+
public async exec(
|
|
364
|
+
id: string,
|
|
365
|
+
command: string,
|
|
366
|
+
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
|
367
|
+
const logFilePath = path.join(this.dotAlchemy, "logs", `${id}.log`);
|
|
368
|
+
const [cmd, ...args] = command.split(/\s+/);
|
|
369
|
+
const child = spawn(cmd, args, { stdio: "pipe" });
|
|
370
|
+
await once(child, "spawn"); // throws if command not found
|
|
371
|
+
await fs.mkdir(path.dirname(logFilePath), { recursive: true });
|
|
372
|
+
const logFile = await fs.open(logFilePath, "w");
|
|
373
|
+
let stdout = "";
|
|
374
|
+
let stderr = "";
|
|
375
|
+
child.stdout.on("data", (chunk) => {
|
|
376
|
+
stdout += chunk.toString();
|
|
377
|
+
logFile.write(chunk);
|
|
378
|
+
});
|
|
379
|
+
child.stderr.on("data", (chunk) => {
|
|
380
|
+
stderr += chunk.toString();
|
|
381
|
+
logFile.write(chunk);
|
|
382
|
+
});
|
|
383
|
+
const [exitCode] = (await once(child, "exit")) as [number];
|
|
384
|
+
return { exitCode, stdout, stderr };
|
|
385
|
+
}
|
|
386
|
+
|
|
360
387
|
/**
|
|
361
388
|
* @internal
|
|
362
389
|
*/
|
package/src/serde.ts
CHANGED
|
@@ -183,7 +183,7 @@ export async function deserialize(
|
|
|
183
183
|
"See: https://alchemy.run/concepts/secret/#encryption-password",
|
|
184
184
|
);
|
|
185
185
|
}
|
|
186
|
-
if (typeof value["@secret"] === "object") {
|
|
186
|
+
if (typeof value["@secret"] === "object" && "data" in value["@secret"]) {
|
|
187
187
|
return new Secret(
|
|
188
188
|
JSON.parse(
|
|
189
189
|
await decryptWithKey(value["@secret"].data, scope.password),
|
package/src/util/telemetry.ts
CHANGED
|
@@ -354,6 +354,7 @@ async function fetchNoResponse(
|
|
|
354
354
|
body?: string;
|
|
355
355
|
headers?: Record<string, string>;
|
|
356
356
|
method?: string;
|
|
357
|
+
timeout?: number;
|
|
357
358
|
} = {},
|
|
358
359
|
) {
|
|
359
360
|
const parsedUrl = new URL(url);
|
|
@@ -372,12 +373,16 @@ async function fetchNoResponse(
|
|
|
372
373
|
path: parsedUrl.pathname + parsedUrl.search,
|
|
373
374
|
method: options.method || "GET",
|
|
374
375
|
headers,
|
|
376
|
+
signal: AbortSignal.timeout(options?.timeout ?? 3000),
|
|
375
377
|
};
|
|
376
378
|
|
|
377
379
|
await new Promise<void>((resolve, reject) => {
|
|
378
380
|
const req = client.request(requestOptions);
|
|
379
381
|
|
|
380
382
|
req.on("error", (error) => {
|
|
383
|
+
if (error.name === "AbortError" && !SUPPRESS_TELEMETRY_ERRORS) {
|
|
384
|
+
logger.warn("Telemetry request timed out");
|
|
385
|
+
}
|
|
381
386
|
reject(error);
|
|
382
387
|
});
|
|
383
388
|
|