alchemy 0.76.0 → 0.77.0

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 (107) hide show
  1. package/bin/alchemy.js +111 -30
  2. package/bin/services/execute-alchemy.ts +10 -1
  3. package/lib/aws/ec2/index.d.ts +1 -0
  4. package/lib/aws/ec2/index.d.ts.map +1 -1
  5. package/lib/aws/ec2/index.js +1 -0
  6. package/lib/aws/ec2/index.js.map +1 -1
  7. package/lib/aws/index.d.ts +1 -7
  8. package/lib/aws/index.d.ts.map +1 -1
  9. package/lib/aws/index.js +1 -8
  10. package/lib/aws/index.js.map +1 -1
  11. package/lib/cloudflare/auth.d.ts.map +1 -1
  12. package/lib/cloudflare/auth.js +1 -0
  13. package/lib/cloudflare/auth.js.map +1 -1
  14. package/lib/cloudflare/bucket-custom-domain.d.ts +90 -0
  15. package/lib/cloudflare/bucket-custom-domain.d.ts.map +1 -0
  16. package/lib/cloudflare/bucket-custom-domain.js +81 -0
  17. package/lib/cloudflare/bucket-custom-domain.js.map +1 -0
  18. package/lib/cloudflare/bucket.d.ts +28 -4
  19. package/lib/cloudflare/bucket.d.ts.map +1 -1
  20. package/lib/cloudflare/bucket.js +41 -14
  21. package/lib/cloudflare/bucket.js.map +1 -1
  22. package/lib/cloudflare/cloudflare-env-proxy.d.ts.map +1 -1
  23. package/lib/cloudflare/cloudflare-env-proxy.js +0 -3
  24. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  25. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  26. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  27. package/lib/cloudflare/index.d.ts +1 -0
  28. package/lib/cloudflare/index.d.ts.map +1 -1
  29. package/lib/cloudflare/index.js +1 -0
  30. package/lib/cloudflare/index.js.map +1 -1
  31. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  32. package/lib/cloudflare/miniflare/build-worker-options.js +3 -1
  33. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  34. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
  35. package/lib/cloudflare/miniflare/remote-binding-proxy.js +73 -9
  36. package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
  37. package/lib/cloudflare/react-router/plugin.d.ts +2 -1
  38. package/lib/cloudflare/react-router/plugin.d.ts.map +1 -1
  39. package/lib/cloudflare/react-router/plugin.js +6 -14
  40. package/lib/cloudflare/react-router/plugin.js.map +1 -1
  41. package/lib/cloudflare/vite/plugin.d.ts.map +1 -1
  42. package/lib/cloudflare/vite/plugin.js +0 -3
  43. package/lib/cloudflare/vite/plugin.js.map +1 -1
  44. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  45. package/lib/cloudflare/worker-bundle.js +17 -0
  46. package/lib/cloudflare/worker-bundle.js.map +1 -1
  47. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  48. package/lib/cloudflare/worker-metadata.js +6 -4
  49. package/lib/cloudflare/worker-metadata.js.map +1 -1
  50. package/lib/cloudflare/worker-subdomain.d.ts.map +1 -1
  51. package/lib/cloudflare/worker-subdomain.js +0 -1
  52. package/lib/cloudflare/worker-subdomain.js.map +1 -1
  53. package/lib/cloudflare/worker.d.ts.map +1 -1
  54. package/lib/cloudflare/worker.js +28 -38
  55. package/lib/cloudflare/worker.js.map +1 -1
  56. package/lib/cloudflare/wrangler.json.d.ts +2 -281
  57. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  58. package/lib/cloudflare/wrangler.json.js +23 -11
  59. package/lib/cloudflare/wrangler.json.js.map +1 -1
  60. package/lib/encrypt.d.ts +28 -3
  61. package/lib/encrypt.d.ts.map +1 -1
  62. package/lib/encrypt.js +77 -3
  63. package/lib/encrypt.js.map +1 -1
  64. package/lib/planetscale/role.d.ts +9 -0
  65. package/lib/planetscale/role.d.ts.map +1 -1
  66. package/lib/planetscale/role.js +18 -3
  67. package/lib/planetscale/role.js.map +1 -1
  68. package/lib/prisma-postgres/api.d.ts.map +1 -1
  69. package/lib/prisma-postgres/api.js +1 -0
  70. package/lib/prisma-postgres/api.js.map +1 -1
  71. package/lib/scope.d.ts +5 -0
  72. package/lib/scope.d.ts.map +1 -1
  73. package/lib/scope.js +23 -0
  74. package/lib/scope.js.map +1 -1
  75. package/lib/serde.js +1 -1
  76. package/lib/serde.js.map +1 -1
  77. package/lib/util/telemetry.js +4 -0
  78. package/lib/util/telemetry.js.map +1 -1
  79. package/package.json +12 -10
  80. package/src/aws/ec2/index.ts +1 -0
  81. package/src/aws/index.ts +1 -9
  82. package/src/cloudflare/auth.ts +1 -0
  83. package/src/cloudflare/bucket-custom-domain.ts +281 -0
  84. package/src/cloudflare/bucket.ts +85 -18
  85. package/src/cloudflare/cloudflare-env-proxy.ts +0 -3
  86. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  87. package/src/cloudflare/index.ts +1 -0
  88. package/src/cloudflare/miniflare/build-worker-options.ts +3 -1
  89. package/src/cloudflare/miniflare/remote-binding-proxy.ts +80 -9
  90. package/src/cloudflare/react-router/plugin.ts +9 -21
  91. package/src/cloudflare/vite/plugin.ts +0 -3
  92. package/src/cloudflare/worker-bundle.ts +28 -1
  93. package/src/cloudflare/worker-metadata.ts +8 -4
  94. package/src/cloudflare/worker-subdomain.ts +0 -1
  95. package/src/cloudflare/worker.ts +32 -45
  96. package/src/cloudflare/wrangler.json.ts +62 -397
  97. package/src/encrypt.ts +115 -3
  98. package/src/planetscale/role.ts +30 -3
  99. package/src/prisma-postgres/api.ts +1 -0
  100. package/src/scope.ts +27 -0
  101. package/src/serde.ts +1 -1
  102. package/src/util/telemetry.ts +5 -0
  103. package/workers/cloudflare-state-store.js +4802 -8
  104. package/workers/dofs-state-store.js +632 -2
  105. package/workers/remote-binding-proxy.js +1929 -3
  106. package/workers/tunnel-proxy.js +58 -6
  107. package/workers/tunnel-proxy.ts +1 -1
@@ -1,12 +1,14 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "pathe";
3
+ import type { Unstable_Config as WranglerJsonConfig } from "wrangler";
3
4
  import type { Context } from "../context.ts";
4
5
  import { Resource } from "../resource.ts";
5
6
  import { Scope } from "../scope.ts";
6
7
  import { isSecret } from "../secret.ts";
7
8
  import { assertNever } from "../util/assert-never.ts";
8
- import type { Bindings, WorkerBindingRateLimit } from "./bindings.ts";
9
- import type { R2BucketJurisdiction } from "./bucket.ts";
9
+ import { createCloudflareApi } from "./api.ts";
10
+ import type { Bindings } from "./bindings.ts";
11
+ import { getCloudflareRegistryWithAccountNamespace } from "./container.ts";
10
12
  import type { DurableObjectNamespace } from "./durable-object-namespace.ts";
11
13
  import type { EventSource } from "./event-source.ts";
12
14
  import { isQueueEventSource } from "./event-source.ts";
@@ -14,10 +16,6 @@ import { isQueue } from "./queue.ts";
14
16
  import { unencryptSecrets } from "./util/filter-env-bindings.ts";
15
17
  import { isWorker, type Worker, type WorkerProps } from "./worker.ts";
16
18
 
17
- type WranglerJsonRateLimit = Omit<WorkerBindingRateLimit, "type"> & {
18
- type: "rate_limit";
19
- };
20
-
21
19
  /**
22
20
  * Properties for wrangler.json configuration file
23
21
  */
@@ -162,13 +160,17 @@ export async function WranglerJson(
162
160
  }
163
161
  : undefined,
164
162
  placement: worker.placement,
165
- limits: worker.limits,
163
+ limits: worker.limits
164
+ ? {
165
+ cpu_ms: worker.limits.cpu_ms ?? 30_000,
166
+ }
167
+ : undefined,
166
168
  logpush: worker.logpush,
167
169
  };
168
170
 
169
171
  // Process bindings if they exist
170
172
  if (worker.bindings) {
171
- processBindings(
173
+ await processBindings(
172
174
  spec,
173
175
  worker.bindings,
174
176
  worker.eventSources,
@@ -176,6 +178,8 @@ export async function WranglerJson(
176
178
  cwd,
177
179
  props.secrets ?? false,
178
180
  Scope.current.local && !props.worker.dev?.remote,
181
+ async () =>
182
+ worker.accountId ?? (await createCloudflareApi(worker)).accountId,
179
183
  );
180
184
  }
181
185
 
@@ -197,7 +201,7 @@ export async function WranglerJson(
197
201
  spec.triggers = { crons: worker.crons };
198
202
  }
199
203
 
200
- if (spec.assets) {
204
+ if (spec.assets && spec.assets.directory) {
201
205
  spec.assets.directory = path.relative(dirname, spec.assets.directory);
202
206
  }
203
207
 
@@ -212,7 +216,7 @@ export async function WranglerJson(
212
216
  // but do not modify `finalSpec` so that way secrets aren't written to state unencrypted.
213
217
  const withSecretsUnwrapped = {
214
218
  ...finalSpec,
215
- vars: unencryptSecrets(finalSpec.vars ?? {}),
219
+ vars: unencryptSecrets((finalSpec.vars as Record<string, string>) ?? {}),
216
220
  };
217
221
  await writeJSON(filePath, withSecretsUnwrapped);
218
222
  } else {
@@ -235,317 +239,12 @@ const writeJSON = async (filePath: string, content: any) => {
235
239
  /**
236
240
  * Wrangler.json configuration specification based on Cloudflare's schema
237
241
  */
238
- export interface WranglerJsonSpec {
239
- /**
240
- * The name of the worker
241
- */
242
- name: string;
243
-
244
- /**
245
- * Main entry point for the worker
246
- */
247
- main?: string;
248
-
249
- /**
250
- * A date in the form yyyy-mm-dd used to determine Workers runtime version
251
- */
252
- compatibility_date?: string;
253
-
254
- /**
255
- * A list of flags that enable features from upcoming Workers runtime
256
- */
257
- compatibility_flags?: string[];
258
-
259
- /**
260
- * The placement mode for the worker
261
- */
262
- placement?: {
263
- mode: "smart";
264
- };
265
-
266
- /**
267
- * The CPU time limit for the worker
268
- */
269
- limits?: {
270
- cpu_ms?: number;
271
- };
272
-
273
- /**
274
- * Send Trace Events from this Worker to Workers Logpush.
275
- *
276
- * This will not configure a corresponding Logpush job automatically.
277
- *
278
- * @default false
279
- */
280
- logpush?: boolean;
281
-
282
- /**
283
- * Whether to enable a workers.dev URL for this worker
284
- */
285
- workers_dev?: boolean;
286
-
287
- /**
288
- * Routes to attach to the worker
289
- */
290
- routes?: string[];
291
-
292
- /**
293
- * Scheduled triggers for the worker
294
- */
295
- triggers?: {
296
- crons: string[];
297
- };
298
-
299
- /**
300
- * AI bindings
301
- */
302
- ai?: {
303
- binding: string;
304
- remote?: boolean;
305
- };
306
-
307
- /**
308
- * Browser bindings
309
- */
310
- browser?: {
311
- binding: string;
312
- remote?: boolean;
313
- };
314
-
315
- /**
316
- * Images bindings
317
- */
318
- images?: {
319
- binding: string;
320
- remote?: boolean;
321
- };
322
-
323
- /**
324
- * KV Namespace bindings
325
- */
326
- kv_namespaces?: {
327
- binding: string;
328
- id: string;
329
- /**
330
- * The ID of the KV namespace used during `wrangler dev`
331
- */
332
- preview_id?: string;
333
- remote?: boolean;
334
- }[];
335
-
336
- /**
337
- * Durable Object bindings
338
- */
339
- durable_objects?: {
340
- bindings: {
341
- name: string;
342
- class_name: string;
343
- script_name?: string;
344
- environment?: string;
345
- }[];
346
- };
347
-
348
- /**
349
- * R2 bucket bindings
350
- */
351
- r2_buckets?: {
352
- binding: string;
353
- bucket_name: string;
354
- /**
355
- * The preview name of this R2 bucket at the edge.
356
- */
357
- preview_bucket_name?: string;
358
- remote?: boolean;
359
- }[];
360
-
361
- /**
362
- * Queue bindings
363
- */
364
- queues?: {
365
- producers?: { queue: string; binding: string }[];
366
- consumers?: QueueConsumerWranglerJson[];
367
- };
368
-
369
- /**
370
- * Service bindings
371
- */
372
- services?: {
373
- binding: string;
374
- service: string;
375
- environment?: string;
376
- entrypoint?: string;
377
- }[];
378
-
379
- worker_loaders?: {
380
- binding: string;
381
- }[];
382
-
383
- /**
384
- * Workflow bindings
385
- */
386
- workflows?: {
387
- name: string;
388
- binding: string;
389
- class_name: string;
390
- script_name?: string;
391
- }[];
392
-
393
- /**
394
- * Vectorize index bindings
395
- */
396
- vectorize?: {
397
- binding: string;
398
- index_name: string;
399
- remote?: boolean;
400
- }[];
401
-
402
- /**
403
- * Plain text bindings (vars)
404
- */
405
- vars?: Record<string, string>;
406
-
407
- /**
408
- * D1 database bindings
409
- */
410
- d1_databases?: {
411
- binding: string;
412
- database_id: string;
413
- database_name: string;
414
- migrations_dir?: string;
415
- /**
416
- * The ID of the D1 database used during `wrangler dev`
417
- */
418
- preview_database_id?: string;
419
- remote?: boolean;
420
- }[];
421
-
422
- /**
423
- * Assets bindings
424
- */
425
- assets?: {
426
- directory: string;
427
- binding: string;
428
- not_found_handling?: "none" | "404-page" | "single-page-application";
429
- html_handling?:
430
- | "auto-trailing-slash"
431
- | "drop-trailing-slash"
432
- | "force-trailing-slash"
433
- | "none";
434
- run_worker_first?: boolean | string[];
435
- };
436
-
437
- /**
438
- * Migrations
439
- */
440
- migrations?: {
441
- tag: string;
442
- new_sqlite_classes?: string[];
443
- new_classes?: string[];
444
- }[];
445
-
446
- /**
447
- * Workflow bindings
448
- */
449
- wasm_modules?: Record<string, string>;
450
-
451
- /**
452
- * Safe mode configuration
453
- */
454
- node_compat?: boolean;
455
-
456
- /**
457
- * Whether to minify the worker script
458
- */
459
- minify?: boolean;
460
-
461
- /**
462
- * Analytics Engine datasets
463
- */
464
- analytics_engine_datasets?: { binding: string; dataset: string }[];
465
-
466
- /**
467
- * Hyperdrive bindings
468
- */
469
- hyperdrive?: {
470
- binding: string;
471
- id: string;
472
- localConnectionString?: string;
473
- }[];
474
-
475
- /**
476
- * Pipelines
477
- */
478
- pipelines?: { binding: string; pipeline: string }[];
479
-
480
- /**
481
- * Secrets Store bindings
482
- */
483
- secrets_store_secrets?: {
484
- binding: string;
485
- store_id: string;
486
- secret_name: string;
487
- }[];
488
-
489
- /**
490
- * Version metadata bindings
491
- */
492
- version_metadata?: {
493
- binding: string;
494
- };
495
-
496
- /**
497
- * Dispatch namespace bindings
498
- */
499
- dispatch_namespaces?: {
500
- binding: string;
501
- namespace: string;
502
- remote?: boolean;
503
- }[];
504
-
505
- /**
506
- * Tail consumers that will receive execution logs from this worker
507
- */
508
- tail_consumers?: Array<{ service: string }>;
509
-
510
- /**
511
- * Unsafe bindings section for experimental features
512
- */
513
- unsafe?: {
514
- bindings: WranglerJsonRateLimit[];
515
- };
516
- }
517
-
518
- /**
519
- * Queue consumer bindings for wrangler.json
520
- */
521
- interface QueueConsumerWranglerJson {
522
- queue: string;
523
- /**
524
- * The maximum number of messages allowed in each batch.
525
- */
526
- max_batch_size?: number;
527
- /**
528
- * The maximum number of concurrent consumers allowed to run at once. Leaving this unset will mean that the number of invocations will scale to the currently supported maximum.
529
- */
530
- max_concurrency?: number;
531
- /**
532
- * The maximum number of retries for a message, if it fails or `retryAll()` is invoked.
533
- */
534
- max_retries?: number;
535
- /**
536
- * The maximum number of seconds to wait for messages to fill a batch before the batch is sent to the consumer Worker.
537
- */
538
- max_batch_timeout?: number;
539
- /**
540
- * The number of seconds to delay retried messages for by default, before they are re-delivered to the consumer. This can be overridden on a per-message or per-batch basis when retrying messages.
541
- */
542
- retry_delay?: number;
543
- }
242
+ export interface WranglerJsonSpec extends Partial<WranglerJsonConfig> {}
544
243
 
545
244
  /**
546
245
  * Process worker bindings into wrangler.json format
547
246
  */
548
- function processBindings(
247
+ async function processBindings(
549
248
  spec: WranglerJsonSpec,
550
249
  bindings: Bindings,
551
250
  eventSources: EventSource[] | undefined,
@@ -553,87 +252,37 @@ function processBindings(
553
252
  workerCwd: string,
554
253
  writeSecrets: boolean,
555
254
  local: boolean,
556
- ): void {
255
+ getAccountId: () => Promise<string>,
256
+ ): Promise<void> {
557
257
  // Arrays to collect different binding types
558
- const kvNamespaces: {
559
- binding: string;
560
- id: string;
561
- preview_id: string;
562
- remote?: boolean;
563
- }[] = [];
564
- const durableObjects: {
565
- name: string;
566
- class_name: string;
567
- script_name?: string;
568
- environment?: string;
569
- }[] = [];
570
- const r2Buckets: {
571
- binding: string;
572
- bucket_name: string;
573
- preview_bucket_name: string;
574
- jurisdiction?: R2BucketJurisdiction;
575
- }[] = [];
576
- const services: {
577
- binding: string;
578
- service: string;
579
- environment?: string;
580
- entrypoint?: string;
581
- }[] = [];
258
+ const kvNamespaces: WranglerJsonConfig["kv_namespaces"] = [];
259
+ const durableObjects: WranglerJsonConfig["durable_objects"]["bindings"] = [];
260
+ const r2Buckets: WranglerJsonConfig["r2_buckets"] = [];
261
+ const services: WranglerJsonConfig["services"] = [];
582
262
  const secrets: string[] = [];
583
- const workflows: {
584
- name: string;
585
- binding: string;
586
- class_name: string;
587
- script_name?: string;
588
- }[] = [];
589
- const d1Databases: {
590
- binding: string;
591
- database_id: string;
592
- database_name: string;
593
- migrations_dir?: string;
594
- preview_database_id: string;
595
- remote?: boolean;
596
- }[] = [];
263
+ const workflows: WranglerJsonConfig["workflows"] = [];
264
+ const d1Databases: WranglerJsonConfig["d1_databases"] = [];
597
265
  const queues: {
598
- producers: { queue: string; binding: string }[];
599
- consumers: QueueConsumerWranglerJson[];
266
+ producers: WranglerJsonConfig["queues"]["producers"] & {};
267
+ consumers: WranglerJsonConfig["queues"]["consumers"] & {};
600
268
  } = {
601
269
  producers: [],
602
270
  consumers: [],
603
271
  };
604
-
605
- const new_sqlite_classes: string[] = [];
606
- const new_classes: string[] = [];
607
-
608
- const vectorizeIndexes: {
609
- binding: string;
610
- index_name: string;
611
- remote?: boolean;
612
- }[] = [];
613
- const analyticsEngineDatasets: { binding: string; dataset: string }[] = [];
614
- const hyperdrive: {
615
- binding: string;
616
- id: string;
617
- localConnectionString?: string;
618
- }[] = [];
619
- const pipelines: { binding: string; pipeline: string }[] = [];
620
- const secretsStoreSecrets: {
621
- binding: string;
622
- store_id: string;
623
- secret_name: string;
624
- }[] = [];
625
- const dispatchNamespaces: {
626
- binding: string;
627
- namespace: string;
628
- remote?: boolean;
629
- }[] = [];
630
- const unsafeBindings: WranglerJsonRateLimit[] = [];
631
- const containers: {
632
- class_name: string;
633
- }[] = [];
634
- const workerLoaders: {
635
- binding: string;
636
- }[] = [];
272
+ const new_sqlite_classes: WranglerJsonConfig["migrations"][number]["new_sqlite_classes"] =
273
+ [];
274
+ const new_classes: WranglerJsonConfig["migrations"][number]["new_classes"] =
275
+ [];
276
+ const vectorizeIndexes: WranglerJsonConfig["vectorize"] = [];
277
+ const analyticsEngineDatasets: WranglerJsonConfig["analytics_engine_datasets"] =
278
+ [];
279
+ const hyperdrive: WranglerJsonConfig["hyperdrive"] = [];
280
+ const pipelines: WranglerJsonConfig["pipelines"] = [];
281
+ const secretsStoreSecrets: WranglerJsonConfig["secrets_store_secrets"] = [];
282
+ const dispatchNamespaces: WranglerJsonConfig["dispatch_namespaces"] = [];
283
+ const ratelimits: WranglerJsonConfig["ratelimits"] = [];
284
+ const containers: WranglerJsonConfig["containers"] = [];
285
+ const workerLoaders: WranglerJsonConfig["worker_loaders"] = [];
637
286
 
638
287
  for (const eventSource of eventSources ?? []) {
639
288
  if (isQueueEventSource(eventSource)) {
@@ -838,9 +487,8 @@ function processBindings(
838
487
  remote: true,
839
488
  });
840
489
  } else if (binding.type === "ratelimit") {
841
- unsafeBindings.push({
490
+ ratelimits.push({
842
491
  name: bindingName,
843
- type: "rate_limit",
844
492
  namespace_id: binding.namespace_id.toString(),
845
493
  simple: binding.simple,
846
494
  });
@@ -852,8 +500,23 @@ function processBindings(
852
500
  class_name: binding.className,
853
501
  script_name: binding.scriptName,
854
502
  });
503
+ new_sqlite_classes.push(binding.className);
504
+ // If there are build options, use the local image path, otherwise use the image reference
505
+ const image = binding.image.build
506
+ ? path.resolve(
507
+ workerCwd,
508
+ path.join(
509
+ binding.image.build.context ?? process.cwd(),
510
+ binding.image.build.dockerfile ?? "Dockerfile",
511
+ ),
512
+ )
513
+ : getCloudflareRegistryWithAccountNamespace(
514
+ await getAccountId(),
515
+ binding.image.imageRef,
516
+ );
855
517
  containers.push({
856
518
  class_name: binding.className,
519
+ image,
857
520
  });
858
521
  } else if (binding.type === "worker_loader") {
859
522
  workerLoaders.push({
@@ -933,10 +596,12 @@ function processBindings(
933
596
  spec.dispatch_namespaces = dispatchNamespaces;
934
597
  }
935
598
 
936
- if (unsafeBindings.length > 0) {
937
- spec.unsafe = {
938
- bindings: unsafeBindings,
939
- };
599
+ if (containers.length > 0) {
600
+ spec.containers = containers;
601
+ }
602
+
603
+ if (ratelimits.length > 0) {
604
+ spec.ratelimits = ratelimits;
940
605
  }
941
606
 
942
607
  if (workerLoaders.length > 0) {
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 encrypt(value: string, key: string): Promise<string> {
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 decryptWithKey(
156
+ export async function libsodiumDecrypt(
45
157
  encryptedValue: string,
46
158
  key: string,
47
159
  ): Promise<string> {