alchemy 0.69.1 → 0.70.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 (227) hide show
  1. package/bin/alchemy.js +3797 -3857
  2. package/bin/trpc.ts +4 -9
  3. package/lib/alchemy.d.ts +3 -3
  4. package/lib/alchemy.d.ts.map +1 -1
  5. package/lib/alchemy.js +2 -15
  6. package/lib/alchemy.js.map +1 -1
  7. package/lib/apply.js +24 -9
  8. package/lib/apply.js.map +1 -1
  9. package/lib/aws/utils.d.ts +2 -1
  10. package/lib/aws/utils.d.ts.map +1 -1
  11. package/lib/aws/utils.js.map +1 -1
  12. package/lib/clickhouse/api/client.gen.d.ts +13 -0
  13. package/lib/clickhouse/api/client.gen.d.ts.map +1 -0
  14. package/lib/clickhouse/api/client.gen.js +7 -0
  15. package/lib/clickhouse/api/client.gen.js.map +1 -0
  16. package/lib/clickhouse/api/sdk.gen.d.ts +322 -0
  17. package/lib/clickhouse/api/sdk.gen.d.ts.map +1 -0
  18. package/lib/clickhouse/api/sdk.gen.js +683 -0
  19. package/lib/clickhouse/api/sdk.gen.js.map +1 -0
  20. package/lib/clickhouse/api/types.gen.d.ts +5030 -0
  21. package/lib/clickhouse/api/types.gen.d.ts.map +1 -0
  22. package/lib/clickhouse/api/types.gen.js +2 -0
  23. package/lib/clickhouse/api/types.gen.js.map +1 -0
  24. package/lib/clickhouse/api-key.d.ts +29 -0
  25. package/lib/clickhouse/api-key.d.ts.map +1 -0
  26. package/lib/clickhouse/api-key.js +83 -0
  27. package/lib/clickhouse/api-key.js.map +1 -0
  28. package/lib/clickhouse/api.d.ts +36 -0
  29. package/lib/clickhouse/api.d.ts.map +1 -0
  30. package/lib/clickhouse/api.js +48 -0
  31. package/lib/clickhouse/api.js.map +1 -0
  32. package/lib/clickhouse/index.d.ts +4 -0
  33. package/lib/clickhouse/index.d.ts.map +1 -0
  34. package/lib/clickhouse/index.js +4 -0
  35. package/lib/clickhouse/index.js.map +1 -0
  36. package/lib/clickhouse/organization.d.ts +16 -0
  37. package/lib/clickhouse/organization.d.ts.map +1 -0
  38. package/lib/clickhouse/organization.js +21 -0
  39. package/lib/clickhouse/organization.js.map +1 -0
  40. package/lib/clickhouse/service.d.ts +275 -0
  41. package/lib/clickhouse/service.d.ts.map +1 -0
  42. package/lib/clickhouse/service.js +274 -0
  43. package/lib/clickhouse/service.js.map +1 -0
  44. package/lib/cloudflare/account-api-token.d.ts.map +1 -1
  45. package/lib/cloudflare/account-api-token.js +1 -1
  46. package/lib/cloudflare/account-api-token.js.map +1 -1
  47. package/lib/cloudflare/bun-spa/bun-spa.d.ts +15 -0
  48. package/lib/cloudflare/bun-spa/bun-spa.d.ts.map +1 -0
  49. package/lib/cloudflare/bun-spa/bun-spa.js +95 -0
  50. package/lib/cloudflare/bun-spa/bun-spa.js.map +1 -0
  51. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  52. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  53. package/lib/cloudflare/container.d.ts +11 -1
  54. package/lib/cloudflare/container.d.ts.map +1 -1
  55. package/lib/cloudflare/container.js.map +1 -1
  56. package/lib/cloudflare/index.d.ts +1 -0
  57. package/lib/cloudflare/index.d.ts.map +1 -1
  58. package/lib/cloudflare/index.js +1 -0
  59. package/lib/cloudflare/index.js.map +1 -1
  60. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  61. package/lib/cloudflare/miniflare/build-worker-options.js +3 -3
  62. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  63. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  64. package/lib/cloudflare/miniflare/miniflare-controller.js +2 -0
  65. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  66. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +1 -1
  67. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  68. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +36 -49
  69. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  70. package/lib/cloudflare/permission-groups.d.ts +2 -3
  71. package/lib/cloudflare/permission-groups.d.ts.map +1 -1
  72. package/lib/cloudflare/permission-groups.js +10 -15
  73. package/lib/cloudflare/permission-groups.js.map +1 -1
  74. package/lib/cloudflare/redwood/redwood.d.ts +1 -1
  75. package/lib/cloudflare/redwood/redwood.js +1 -1
  76. package/lib/cloudflare/util/filter-env-bindings.d.ts +62 -0
  77. package/lib/cloudflare/util/filter-env-bindings.d.ts.map +1 -0
  78. package/lib/cloudflare/util/filter-env-bindings.js +81 -0
  79. package/lib/cloudflare/util/filter-env-bindings.js.map +1 -0
  80. package/lib/cloudflare/website.d.ts +2 -2
  81. package/lib/cloudflare/website.d.ts.map +1 -1
  82. package/lib/cloudflare/website.js +6 -18
  83. package/lib/cloudflare/website.js.map +1 -1
  84. package/lib/cloudflare/worker-metadata.d.ts +1 -0
  85. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  86. package/lib/cloudflare/worker-metadata.js +1 -0
  87. package/lib/cloudflare/worker-metadata.js.map +1 -1
  88. package/lib/cloudflare/worker.d.ts +11 -1
  89. package/lib/cloudflare/worker.d.ts.map +1 -1
  90. package/lib/cloudflare/worker.js.map +1 -1
  91. package/lib/cloudflare/wrangler.json.d.ts +8 -0
  92. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  93. package/lib/cloudflare/wrangler.json.js +3 -4
  94. package/lib/cloudflare/wrangler.json.js.map +1 -1
  95. package/lib/coinbase/client.d.ts +54 -0
  96. package/lib/coinbase/client.d.ts.map +1 -0
  97. package/lib/coinbase/client.js +89 -0
  98. package/lib/coinbase/client.js.map +1 -0
  99. package/lib/coinbase/evm-account.d.ts +118 -0
  100. package/lib/coinbase/evm-account.d.ts.map +1 -0
  101. package/lib/coinbase/evm-account.js +198 -0
  102. package/lib/coinbase/evm-account.js.map +1 -0
  103. package/lib/coinbase/evm-smart-account.d.ts +122 -0
  104. package/lib/coinbase/evm-smart-account.d.ts.map +1 -0
  105. package/lib/coinbase/evm-smart-account.js +213 -0
  106. package/lib/coinbase/evm-smart-account.js.map +1 -0
  107. package/lib/coinbase/faucet.d.ts +11 -0
  108. package/lib/coinbase/faucet.d.ts.map +1 -0
  109. package/lib/coinbase/faucet.js +146 -0
  110. package/lib/coinbase/faucet.js.map +1 -0
  111. package/lib/coinbase/index.d.ts +4 -0
  112. package/lib/coinbase/index.d.ts.map +1 -0
  113. package/lib/coinbase/index.js +4 -0
  114. package/lib/coinbase/index.js.map +1 -0
  115. package/lib/coinbase/types.d.ts +28 -0
  116. package/lib/coinbase/types.d.ts.map +1 -0
  117. package/lib/coinbase/types.js +4 -0
  118. package/lib/{util/telemetry → coinbase}/types.js.map +1 -1
  119. package/lib/coinbase/utils.d.ts +13 -0
  120. package/lib/coinbase/utils.d.ts.map +1 -0
  121. package/lib/coinbase/utils.js +17 -0
  122. package/lib/coinbase/utils.js.map +1 -0
  123. package/lib/neon/api/types.gen.d.ts +7 -2
  124. package/lib/neon/api/types.gen.d.ts.map +1 -1
  125. package/lib/planetscale/api/types.gen.d.ts +20 -4
  126. package/lib/planetscale/api/types.gen.d.ts.map +1 -1
  127. package/lib/planetscale/api.d.ts.map +1 -1
  128. package/lib/planetscale/api.js +1 -0
  129. package/lib/planetscale/api.js.map +1 -1
  130. package/lib/planetscale/password.d.ts.map +1 -1
  131. package/lib/planetscale/password.js +1 -13
  132. package/lib/planetscale/password.js.map +1 -1
  133. package/lib/resource.d.ts.map +1 -1
  134. package/lib/resource.js +7 -3
  135. package/lib/resource.js.map +1 -1
  136. package/lib/scope.d.ts +4 -9
  137. package/lib/scope.d.ts.map +1 -1
  138. package/lib/scope.js +16 -37
  139. package/lib/scope.js.map +1 -1
  140. package/lib/state/instrumented-state-store.d.ts +1 -3
  141. package/lib/state/instrumented-state-store.d.ts.map +1 -1
  142. package/lib/state/instrumented-state-store.js +19 -21
  143. package/lib/state/instrumented-state-store.js.map +1 -1
  144. package/lib/test/bun.d.ts.map +1 -1
  145. package/lib/test/bun.js +1 -2
  146. package/lib/test/bun.js.map +1 -1
  147. package/lib/test/vitest.d.ts.map +1 -1
  148. package/lib/test/vitest.js +1 -2
  149. package/lib/test/vitest.js.map +1 -1
  150. package/lib/util/diff.d.ts +5 -0
  151. package/lib/util/diff.d.ts.map +1 -0
  152. package/lib/util/diff.js +14 -0
  153. package/lib/util/diff.js.map +1 -0
  154. package/lib/util/telemetry.d.ts +52 -0
  155. package/lib/util/telemetry.d.ts.map +1 -0
  156. package/lib/util/telemetry.js +191 -0
  157. package/lib/util/telemetry.js.map +1 -0
  158. package/package.json +19 -1
  159. package/src/alchemy.ts +5 -20
  160. package/src/apply.ts +29 -12
  161. package/src/aws/utils.ts +2 -1
  162. package/src/clickhouse/api/client.gen.ts +28 -0
  163. package/src/clickhouse/api/sdk.gen.ts +1286 -0
  164. package/src/clickhouse/api/types.gen.ts +5898 -0
  165. package/src/clickhouse/api-key.ts +136 -0
  166. package/src/clickhouse/api.ts +84 -0
  167. package/src/clickhouse/index.ts +3 -0
  168. package/src/clickhouse/organization.ts +31 -0
  169. package/src/clickhouse/service.ts +668 -0
  170. package/src/cloudflare/account-api-token.ts +1 -4
  171. package/src/cloudflare/bun-spa/bun-spa.ts +139 -0
  172. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  173. package/src/cloudflare/container.ts +20 -1
  174. package/src/cloudflare/index.ts +1 -0
  175. package/src/cloudflare/miniflare/build-worker-options.ts +6 -4
  176. package/src/cloudflare/miniflare/miniflare-controller.ts +2 -0
  177. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +41 -58
  178. package/src/cloudflare/permission-groups.ts +21 -49
  179. package/src/cloudflare/redwood/redwood.ts +1 -1
  180. package/src/cloudflare/util/filter-env-bindings.ts +96 -0
  181. package/src/cloudflare/website.ts +11 -24
  182. package/src/cloudflare/worker-metadata.ts +2 -0
  183. package/src/cloudflare/worker.ts +12 -1
  184. package/src/cloudflare/wrangler.json.ts +12 -8
  185. package/src/coinbase/client.ts +146 -0
  186. package/src/coinbase/evm-account.ts +317 -0
  187. package/src/coinbase/evm-smart-account.ts +339 -0
  188. package/src/coinbase/faucet.ts +173 -0
  189. package/src/coinbase/index.ts +3 -0
  190. package/src/coinbase/types.ts +33 -0
  191. package/src/coinbase/utils.ts +19 -0
  192. package/src/neon/api/types.gen.ts +13 -1
  193. package/src/planetscale/api/types.gen.ts +22 -2
  194. package/src/planetscale/api.ts +1 -0
  195. package/src/planetscale/password.ts +1 -14
  196. package/src/resource.ts +11 -4
  197. package/src/scope.ts +22 -45
  198. package/src/state/instrumented-state-store.ts +27 -26
  199. package/src/test/bun.ts +1 -2
  200. package/src/test/vitest.ts +1 -2
  201. package/src/util/diff.ts +14 -0
  202. package/src/util/telemetry.ts +297 -0
  203. package/workers/tunnel-proxy.js +1 -1
  204. package/lib/util/telemetry/client.d.ts +0 -31
  205. package/lib/util/telemetry/client.d.ts.map +0 -1
  206. package/lib/util/telemetry/client.js +0 -121
  207. package/lib/util/telemetry/client.js.map +0 -1
  208. package/lib/util/telemetry/constants.d.ts +0 -5
  209. package/lib/util/telemetry/constants.d.ts.map +0 -1
  210. package/lib/util/telemetry/constants.js +0 -8
  211. package/lib/util/telemetry/constants.js.map +0 -1
  212. package/lib/util/telemetry/context.d.ts +0 -15
  213. package/lib/util/telemetry/context.d.ts.map +0 -1
  214. package/lib/util/telemetry/context.js +0 -128
  215. package/lib/util/telemetry/context.js.map +0 -1
  216. package/lib/util/telemetry/index.d.ts +0 -3
  217. package/lib/util/telemetry/index.d.ts.map +0 -1
  218. package/lib/util/telemetry/index.js +0 -3
  219. package/lib/util/telemetry/index.js.map +0 -1
  220. package/lib/util/telemetry/types.d.ts +0 -68
  221. package/lib/util/telemetry/types.d.ts.map +0 -1
  222. package/lib/util/telemetry/types.js +0 -1
  223. package/src/util/telemetry/client.ts +0 -156
  224. package/src/util/telemetry/constants.ts +0 -13
  225. package/src/util/telemetry/context.ts +0 -154
  226. package/src/util/telemetry/index.ts +0 -2
  227. package/src/util/telemetry/types.ts +0 -105
@@ -0,0 +1,139 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Scope } from "../../scope.ts";
4
+ import { exists } from "../../util/exists.ts";
5
+ import type { Assets } from "../assets.ts";
6
+ import type { Bindings } from "../bindings.ts";
7
+ import {
8
+ extractStringAndSecretBindings,
9
+ unencryptSecrets,
10
+ } from "../util/filter-env-bindings.ts";
11
+ import {
12
+ spreadBuildProps,
13
+ spreadDevProps,
14
+ Website,
15
+ type WebsiteProps,
16
+ } from "../website.ts";
17
+ import type { Worker } from "../worker.ts";
18
+
19
+ export interface BunSPAProps<B extends Bindings> extends WebsiteProps<B> {
20
+ frontend: string;
21
+ outDir?: string;
22
+ }
23
+
24
+ export type BunSPA<B extends Bindings> = B extends { ASSETS: any }
25
+ ? never
26
+ : Worker<B & { ASSETS: Assets }>;
27
+
28
+ export async function BunSPA<B extends Bindings>(
29
+ id: string,
30
+ props: BunSPAProps<B>,
31
+ ): Promise<BunSPA<B>> {
32
+ const frontendPath = path.resolve(props.frontend);
33
+ if (!(await exists(frontendPath))) {
34
+ throw new Error(`Frontend path ${frontendPath} does not exist`);
35
+ }
36
+
37
+ const stats = await fs.stat(frontendPath);
38
+ if (!stats.isFile()) {
39
+ throw new Error(`Frontend path ${frontendPath} is not a file`);
40
+ }
41
+ const outDir = path.resolve(props.outDir ?? "dist/client");
42
+
43
+ if (props.assets) {
44
+ throw new Error("assets are not supported in BunSPA");
45
+ }
46
+
47
+ const scope = Scope.current;
48
+ console.log("creating website", outDir);
49
+ const website = await Website(id, {
50
+ spa: true,
51
+ ...props,
52
+ bindings: {
53
+ ...props.bindings,
54
+ // set NODE_ENV in worker appropriately if not already set
55
+ NODE_ENV:
56
+ (props.bindings?.NODE_ENV ?? scope.local)
57
+ ? "development"
58
+ : "production",
59
+ } as unknown as B,
60
+ assets: {
61
+ directory: path.resolve(outDir),
62
+ },
63
+ build: spreadBuildProps(
64
+ props,
65
+ `bun build '${frontendPath}' --outdir ${outDir}`,
66
+ ),
67
+ });
68
+
69
+ // in dev
70
+ if (scope.local) {
71
+ const cwd = props.cwd ?? process.cwd();
72
+ await validateBunfigToml(cwd);
73
+ const dev = spreadDevProps(
74
+ props,
75
+ `bun '${path.relative(cwd, frontendPath)}'`,
76
+ );
77
+ console.log("backend url", website.url);
78
+ const secrets = props.wrangler?.secrets ?? !props.wrangler?.path;
79
+ const env = {
80
+ ...(process.env ?? {}),
81
+ ...(props.env ?? {}),
82
+ ...extractStringAndSecretBindings(props.bindings ?? {}, secrets),
83
+ };
84
+ website.url = await scope.spawn(website.name, {
85
+ cmd: typeof dev === "string" ? dev : dev.command!,
86
+ cwd,
87
+ extract: (line) => {
88
+ const URL_REGEX =
89
+ /http:\/\/(localhost|0\.0\.0\.0|127\.0\.0\.1|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d+\/?/;
90
+ const match = line
91
+ .replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
92
+ .match(URL_REGEX);
93
+ if (match) {
94
+ return match[0];
95
+ }
96
+ },
97
+ env: {
98
+ ...unencryptSecrets(env ?? {}),
99
+ ...(typeof dev === "object" ? dev.env : {}),
100
+ FORCE_COLOR: "1",
101
+ ...process.env,
102
+ NODE_ENV: "development",
103
+ ALCHEMY_ROOT: Scope.current.rootDir,
104
+ PUBLIC_BACKEND_URL: website.url!,
105
+ },
106
+ });
107
+ }
108
+ return website;
109
+ }
110
+
111
+ async function validateBunfigToml(cwd: string): Promise<void> {
112
+ const bunfigPath = path.join(cwd, "bunfig.toml");
113
+
114
+ if (!(await exists(bunfigPath))) {
115
+ throw new Error(
116
+ "bunfig.toml is required for BunSPA to work correctly.\n\n" +
117
+ `Create ${bunfigPath} with the following content:\n\n` +
118
+ "[serve.static]\n" +
119
+ `env='PUBLIC_*'\n\n` +
120
+ "This allows Bun to expose PUBLIC_* environment variables to the frontend during development.",
121
+ );
122
+ }
123
+
124
+ const content = await fs.readFile(bunfigPath, "utf-8");
125
+ const config = Bun.TOML.parse(content) as Record<string, any>;
126
+
127
+ const hasServeStatic = config.serve?.static;
128
+ const hasEnvConfig = hasServeStatic && config.serve.static.env === "PUBLIC_*";
129
+
130
+ if (!hasServeStatic || !hasEnvConfig) {
131
+ throw new Error(
132
+ "bunfig.toml is missing required configuration for BunSPA.\n\n" +
133
+ `Add the following section to ${bunfigPath}:\n\n` +
134
+ "[serve.static]\n" +
135
+ `env='PUBLIC_*'\n\n` +
136
+ "This allows Bun to expose PUBLIC_* environment variables to the frontend during development.",
137
+ );
138
+ }
139
+ }
@@ -4,4 +4,4 @@
4
4
  /**
5
5
  * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
6
6
  */
7
- export const DEFAULT_COMPATIBILITY_DATE = "2025-09-27";
7
+ export const DEFAULT_COMPATIBILITY_DATE = "2025-10-01";
@@ -93,9 +93,28 @@ export interface ContainerProps
93
93
  * - `basic`: Basic production instances with standard resources
94
94
  * - `standard`: Standard production instances with enhanced resources
95
95
  *
96
+ * | Instance Type | vCPU | Memory (Min) | Memory (Max) |
97
+ * |---------------|------|--------------|--------------|
98
+ * | lite | 1/16 | 256 MiB | 2 GB |
99
+ * | basic | 1/4 | 1 GiB | 4 GB |
100
+ * | standard-1 | 1/2 | 4 GiB | 8 GB |
101
+ * | standard-2 | 1 | 6 GiB | 12 GB |
102
+ * | standard-3 | 2 | 8 GiB | 16 GB |
103
+ * | standard-4 | 4 | 12 GiB | 20 GB |
104
+ *
96
105
  * @see https://developers.cloudflare.com/containers/pricing/
106
+ * @see https://developers.cloudflare.com/changelog/2025-10-01-new-container-instance-types/
97
107
  */
98
- export type InstanceType = "dev" | "basic" | "standard" | (string & {});
108
+ export type InstanceType =
109
+ | "lite"
110
+ | "dev"
111
+ | "basic"
112
+ | "standard"
113
+ | "standard-1"
114
+ | "standard-2"
115
+ | "standard-3"
116
+ | "standard-4"
117
+ | (string & {});
99
118
 
100
119
  /**
101
120
  * Type guard to check if a binding is a Container binding.
@@ -15,6 +15,7 @@ export * from "./bound.ts";
15
15
  export * from "./browser-rendering.ts";
16
16
  export * from "./bucket-object.ts";
17
17
  export * from "./bucket.ts";
18
+ export * from "./bun-spa/bun-spa.ts";
18
19
  export * from "./certificate-pack.ts";
19
20
  export * from "./cloudflare-env-proxy.ts";
20
21
  export * from "./compatibility-date.gen.ts";
@@ -52,7 +52,9 @@ type RemoteBinding =
52
52
  | WorkerBindingService;
53
53
 
54
54
  type BaseWorkerOptions = {
55
- [K in keyof miniflare.WorkerOptions]: K extends "compatibilityFlags"
55
+ [K in keyof miniflare.WorkerOptions]: K extends
56
+ | "compatibilityFlags"
57
+ | "routes"
56
58
  ? miniflare.WorkerOptions[K]
57
59
  : Exclude<miniflare.WorkerOptions[K], string[]>;
58
60
  };
@@ -70,11 +72,9 @@ export const buildWorkerOptions = async (
70
72
  compatibilityFlags: input.compatibilityFlags,
71
73
  unsafeDirectSockets: [
72
74
  // This matches the Wrangler configuration by exposing the default handler (e.g. `export default { fetch }`).
73
- // However, unlike Wrangler, we set `proxy: false` to avoid the following error when connecting via a websocket:
74
- // workerd/io/worker.c++:2164: info: uncaught exception; source = Uncaught (in promise); stack = TypeError: Invalid URL string.
75
75
  {
76
76
  entrypoint: "default",
77
- proxy: false,
77
+ proxy: true,
78
78
  },
79
79
  ],
80
80
  containerEngine: {
@@ -85,6 +85,8 @@ export const buildWorkerOptions = async (
85
85
  : "unix:///var/run/docker.sock",
86
86
  },
87
87
  },
88
+ // This exposes the worker as a route that can be accessed by setting the MF-Route-Override header.
89
+ routes: [input.name],
88
90
  };
89
91
  for (const [key, binding] of Object.entries(input.bindings ?? {})) {
90
92
  if (typeof binding === "string") {
@@ -104,6 +104,8 @@ export class MiniflareController {
104
104
  // This is required to allow websites and other separate processes
105
105
  // to detect Alchemy-managed Durable Objects via the Wrangler dev registry.
106
106
  unsafeDevRegistryDurableObjectProxy: true,
107
+ // This exposes other handlers like `scheduled` and `email` via HTTP.
108
+ unsafeTriggerHandlers: true,
107
109
  };
108
110
  for (const worker of this.options.values()) {
109
111
  options.workers.push(worker);
@@ -13,7 +13,7 @@ export interface MiniflareWorkerProxy {
13
13
  export async function createMiniflareWorkerProxy(options: {
14
14
  port: number;
15
15
  transformRequest?: (request: RequestInfo) => void;
16
- getWorkerName: (request: miniflare.Request) => string;
16
+ getWorkerName: (request: RequestInfo) => string;
17
17
  miniflare: miniflare.Miniflare;
18
18
  mode: "local" | "remote";
19
19
  }) {
@@ -45,71 +45,50 @@ export async function createMiniflareWorkerProxy(options: {
45
45
  const handleFetch = async (
46
46
  req: http.IncomingMessage,
47
47
  ): Promise<miniflare.Response> => {
48
- const { request, url } = toMiniflareRequest(req);
49
- const name = options.getWorkerName(request);
50
- const worker = await options.miniflare.getWorker(name);
48
+ const info = parseIncomingMessage(req);
49
+ options.transformRequest?.(info);
50
+
51
+ const name = options.getWorkerName(info);
52
+ info.headers.set("MF-Route-Override", name);
51
53
 
52
- // Handle scheduled events
54
+ // Handle scheduled events.
55
+ // Wrangler exposes this as /__scheduled, but Miniflare exposes it as /cdn-cgi/handler/scheduled.
53
56
  // https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/#background
54
57
  // https://github.com/cloudflare/workers-sdk/blob/7d53b9ab6b370944b7934ad51ebef43160c3c775/packages/wrangler/templates/middleware/middleware-scheduled.ts#L6
55
- if (url.pathname === "/__scheduled") {
56
- await worker.scheduled({
57
- scheduledTime: new Date(),
58
- cron: url.searchParams.get("cron") ?? undefined,
59
- });
60
- return new miniflare.Response("Ran scheduled function");
58
+ if (info.url.pathname === "/__scheduled") {
59
+ info.url.pathname = "/cdn-cgi/handler/scheduled";
61
60
  }
62
61
 
63
- const result = await worker
64
- .fetch(request)
65
- .then((response) => ({ success: true, response }) as const)
66
- .catch((error) => ({ success: false, error }) as const);
67
-
68
- // If you open the `/__scheduled` page in a browser, the browser will automatically make a request to `/favicon.ico`.
69
- // For scheduled Workers _without_ a fetch handler, this will result in an unhelpful error that we don't need to log.
70
- // To avoid this, inject a 404 response to favicon.ico loads on the `/__scheduled` page
71
- if (
72
- request.headers.get("referer")?.endsWith("/__scheduled") &&
73
- url.pathname === "/favicon.ico" &&
74
- !result.success
75
- ) {
76
- return new miniflare.Response(null, { status: 404 });
77
- }
78
-
79
- if (!result.success) {
80
- throw result.error;
81
- }
82
-
83
- return result.response;
84
- };
85
-
86
- const toMiniflareRequest = (
87
- req: http.IncomingMessage,
88
- ): { request: miniflare.Request; url: URL } => {
89
- const info = parseIncomingMessage(req);
90
- options.transformRequest?.(info);
91
- return {
92
- request: new miniflare.Request(info.url, {
93
- method: info.method,
94
- headers: info.headers,
95
- body: info.body,
96
- redirect: info.redirect,
97
- duplex: info.duplex,
98
- }),
99
- url: info.url,
100
- };
62
+ const request = new miniflare.Request(info.url, {
63
+ method: info.method,
64
+ headers: info.headers,
65
+ body: info.body,
66
+ redirect: info.redirect,
67
+ duplex: info.duplex,
68
+ });
69
+ return await options.miniflare.dispatchFetch(request);
101
70
  };
102
71
 
103
72
  const createServerWebSocket = async (req: http.IncomingMessage) => {
104
- const { request } = toMiniflareRequest(req);
105
- const name = options.getWorkerName(request);
106
- const target = await options.miniflare.unsafeGetDirectURL(name);
73
+ // Rewrite to Miniflare entry URL
74
+ const target = await options.miniflare.ready;
107
75
  const url = new URL(req.url ?? "/", target);
108
76
  url.protocol = url.protocol.replace("http", "ws");
77
+
109
78
  const protocols = req.headers["sec-websocket-protocol"]
110
79
  ?.split(",")
111
80
  .map((p) => p.trim());
112
- const server = new WebSocket(url, protocols);
81
+
82
+ // Get worker name to set MF-Route-Override header
83
+ const info = parseIncomingMessage(req);
84
+ options.transformRequest?.(info);
85
+ const name = options.getWorkerName(info);
86
+
87
+ const server = new WebSocket(url, protocols, {
88
+ headers: {
89
+ "MF-Route-Override": name,
90
+ },
91
+ });
113
92
  await once(server, "open");
114
93
  return server;
115
94
  };
@@ -121,10 +100,8 @@ export async function createMiniflareWorkerProxy(options: {
121
100
  return {
122
101
  url,
123
102
  close: async () => {
124
- await Promise.all([
125
- new Promise((resolve) => server.close(resolve)),
126
- new Promise((resolve) => wss.close(resolve)),
127
- ]);
103
+ // If we await this while the `wss` server is open, the server will hang.
104
+ server.close();
128
105
  },
129
106
  };
130
107
  }
@@ -172,7 +149,13 @@ const writeMiniflareResponseToNode = (
172
149
  ) => {
173
150
  out.statusCode = res.status;
174
151
  res.headers.forEach((value, key) => {
175
- out.setHeader(key, value);
152
+ // The `transfer-encoding` header prevents Cloudflare Tunnels from working because of the following error:
153
+ // Request failed error="Unable to reach the origin service. The service may be down or it may not be
154
+ // responding to traffic from cloudflared: net/http: HTTP/1.x transport connection broken: too many
155
+ // transfer encodings: [\"chunked\" \"chunked\"]"
156
+ if (key !== "transfer-encoding") {
157
+ out.setHeader(key, value);
158
+ }
176
159
  });
177
160
 
178
161
  if (res.body) {
@@ -1,5 +1,5 @@
1
- import type { Context } from "../context.ts";
2
1
  import { Resource } from "../resource.ts";
2
+ import { extractCloudflareResult } from "./api-response.ts";
3
3
  import { createCloudflareApi, type CloudflareApiOptions } from "./api.ts";
4
4
 
5
5
  /**
@@ -22,16 +22,6 @@ export interface PermissionGroup {
22
22
  scopes: string[];
23
23
  }
24
24
 
25
- /**
26
- * Response from the Cloudflare permission groups API
27
- */
28
- interface PermissionGroupsResponse {
29
- result: PermissionGroup[];
30
- success: boolean;
31
- errors: any[];
32
- messages: any[];
33
- }
34
-
35
25
  export type PermissionGroupName = R2PermissionGroups | (string & {});
36
26
 
37
27
  export type R2PermissionGroups =
@@ -86,7 +76,7 @@ export type PermissionGroups = {
86
76
  *
87
77
  * @example
88
78
  * // Get all permission groups including those for R2
89
- * const permissions = await PermissionGroups("cloudflare-permissions");
79
+ * const permissions = await PermissionGroups();
90
80
  *
91
81
  * // Use with AccountApiToken to create a token with proper permissions
92
82
  * const token = await AccountApiToken("r2-token", {
@@ -106,42 +96,24 @@ export type PermissionGroups = {
106
96
  * ]
107
97
  * });
108
98
  */
109
- export const PermissionGroups = Resource(
110
- "cloudflare::PermissionGroups",
111
- async function (
112
- this: Context<PermissionGroups>,
113
- _id: string,
114
- options: CloudflareApiOptions = {},
115
- ): Promise<PermissionGroups> {
116
- // Only create and update phases are supported
117
- if (this.phase === "delete") {
118
- return this.destroy();
119
- }
120
-
121
- // Initialize API client
122
- const api = await createCloudflareApi(options);
123
-
124
- // Fetch permission groups from Cloudflare API
125
- const response = await api.get(
126
- `/accounts/${api.accountId}/tokens/permission_groups`,
127
- );
128
-
129
- if (!response.ok) {
130
- throw new Error(
131
- `Failed to fetch permission groups: ${response.statusText}`,
132
- );
133
- }
134
-
135
- const data = (await response.json()) as PermissionGroupsResponse;
99
+ export async function PermissionGroups(
100
+ options: CloudflareApiOptions = {},
101
+ ): Promise<PermissionGroups> {
102
+ const api = await createCloudflareApi(options);
103
+ const result = await extractCloudflareResult<PermissionGroup[]>(
104
+ "fetch permission groups",
105
+ api.get(`/accounts/${api.accountId}/tokens/permission_groups`),
106
+ );
107
+ return Object.fromEntries(
108
+ result.map((group) => [group.name, group]),
109
+ ) as PermissionGroups;
110
+ }
136
111
 
137
- if (!data.success || !data.result) {
138
- throw new Error(
139
- `API returned error: ${data.errors?.[0]?.message || "Unknown error"}`,
140
- );
141
- }
112
+ // we are deprecating the PermissionGroups resource (it is now just a function)
113
+ Resource("cloudflare::PermissionGroups", async function (this) {
114
+ if (this.phase === "delete") {
115
+ return this.destroy();
116
+ }
142
117
 
143
- return Object.fromEntries(
144
- data.result.map((group) => [group.name, group]),
145
- ) as PermissionGroups;
146
- },
147
- );
118
+ throw new Error("Not implemented");
119
+ });
@@ -12,7 +12,7 @@ export type Redwood<B extends Bindings> = B extends { ASSETS: any }
12
12
  : Worker<B & { ASSETS: Assets }>;
13
13
 
14
14
  /**
15
- * Deploy a RedwoodJS application to Cloudflare Pages with automatically configured defaults.
15
+ * Deploy a RedwoodJS application to Cloudflare Workers with automatically configured defaults.
16
16
  *
17
17
  * This resource handles the deployment of RedwoodJS applications with optimized settings for
18
18
  * Cloudflare Workers, including proper build commands and compatibility flags.
@@ -0,0 +1,96 @@
1
+ import { isSecret, type Secret } from "../../secret.ts";
2
+ import type { Bindings } from "../bindings.ts";
3
+
4
+ /**
5
+ * Extract string and Secret bindings from a Bindings object
6
+ *
7
+ * This utility filters a Bindings object to return only string values
8
+ * and optionally Secret objects. All other binding types (workers, KV, R2, etc.)
9
+ * are excluded from the result.
10
+ *
11
+ * @param bindings - The bindings object to filter
12
+ * @param includeSecrets - Whether to include Secret objects in the result
13
+ * @returns A record containing only string values and Secret objects (if includeSecrets is true)
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const filtered = extractStringAndSecretBindings(props.bindings, true);
18
+ * // Returns: { API_KEY: Secret(...), DEBUG: "true" }
19
+ * // Excludes: { CACHE: KVNamespace, STORAGE: R2Bucket }
20
+ * ```
21
+ */
22
+ export function extractStringAndSecretBindings(
23
+ bindings: Bindings,
24
+ includeSecrets: boolean,
25
+ ): Record<string, string | Secret> {
26
+ return Object.fromEntries(
27
+ Object.entries(bindings ?? {}).flatMap(([key, value]) => {
28
+ if (typeof value === "string" || (isSecret(value) && includeSecrets)) {
29
+ return [[key, value]];
30
+ }
31
+ return [];
32
+ }),
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Unencrypt all Secret values in an environment object
38
+ *
39
+ * Takes a record that may contain Secret objects and returns a new record
40
+ * with all Secret values replaced by their unencrypted string values.
41
+ * Non-Secret values are passed through unchanged. Undefined values are filtered out.
42
+ *
43
+ * @param env - The environment object that may contain Secret values
44
+ * @returns A new record with all Secrets replaced by their unencrypted values
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const env = { API_KEY: Secret("abc123"), DEBUG: "true" };
49
+ * const unencrypted = unencryptSecrets(env);
50
+ * // Returns: { API_KEY: "abc123", DEBUG: "true" }
51
+ * ```
52
+ */
53
+ export function unencryptSecrets(
54
+ env: Record<string, string | Secret | undefined>,
55
+ ): Record<string, string> {
56
+ return Object.fromEntries(
57
+ Object.entries(env ?? {}).flatMap(([key, value]) => {
58
+ if (isSecret(value)) {
59
+ return [[key, value.unencrypted]];
60
+ }
61
+ if (typeof value === "string") {
62
+ return [[key, value]];
63
+ }
64
+ return [];
65
+ }),
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Filter bindings to strings and unencrypt secrets in one operation
71
+ *
72
+ * This is a convenience function that combines extractStringAndSecretBindings
73
+ * and unencryptSecrets. It filters bindings to strings/secrets and immediately
74
+ * unencrypts any Secret values.
75
+ *
76
+ * Useful for preparing environment variables for dev/build commands where
77
+ * secrets need to be decrypted and all resource bindings need to be excluded.
78
+ *
79
+ * @param bindings - The bindings object to filter
80
+ * @param includeSecrets - Whether to include (and unencrypt) Secret objects
81
+ * @returns A record containing only string values with all secrets unencrypted
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const env = filterStringBindings(props.bindings, true);
86
+ * // Input: { API_KEY: Secret("abc"), DEBUG: "true", CACHE: KVNamespace }
87
+ * // Returns: { API_KEY: "abc", DEBUG: "true" }
88
+ * ```
89
+ */
90
+ export function filterStringBindings(
91
+ bindings: Bindings,
92
+ includeSecrets: boolean,
93
+ ): Record<string, string> {
94
+ const extracted = extractStringAndSecretBindings(bindings, includeSecrets);
95
+ return unencryptSecrets(extracted);
96
+ }
@@ -3,7 +3,6 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { Exec } from "../os/index.ts";
5
5
  import { Scope } from "../scope.ts";
6
- import { isSecret } from "../secret.ts";
7
6
  import { dedent } from "../util/dedent.ts";
8
7
  import { logger } from "../util/logger.ts";
9
8
  import { Assets } from "./assets.ts";
@@ -11,6 +10,10 @@ import type { Bindings } from "./bindings.ts";
11
10
  import { DEFAULT_COMPATIBILITY_DATE } from "./compatibility-date.gen.ts";
12
11
  import { unionCompatibilityFlags } from "./compatibility-presets.ts";
13
12
  import { writeMiniflareSymlink } from "./miniflare/symlink-miniflare-state.ts";
13
+ import {
14
+ extractStringAndSecretBindings,
15
+ unencryptSecrets,
16
+ } from "./util/filter-env-bindings.ts";
14
17
  import { type AssetsConfig, Worker, type WorkerProps } from "./worker.ts";
15
18
  import { WranglerJson, type WranglerJsonSpec } from "./wrangler.json.ts";
16
19
 
@@ -67,7 +70,7 @@ export interface WebsiteProps<B extends Bindings>
67
70
  /**
68
71
  * The command to run to start the dev server
69
72
  */
70
- command: string;
73
+ command?: string;
71
74
  /**
72
75
  * Additional environment variables to set when running the dev command
73
76
  */
@@ -226,14 +229,7 @@ export async function Website<B extends Bindings>(
226
229
  const env = {
227
230
  ...(process.env ?? {}),
228
231
  ...(props.env ?? {}),
229
- ...Object.fromEntries(
230
- Object.entries(props.bindings ?? {}).flatMap(([key, value]) => {
231
- if (typeof value === "string" || (isSecret(value) && secrets)) {
232
- return [[key, value]];
233
- }
234
- return [];
235
- }),
236
- ),
232
+ ...extractStringAndSecretBindings(props.bindings ?? {}, secrets),
237
233
  };
238
234
  const worker = {
239
235
  ...workerProps,
@@ -302,9 +298,10 @@ export async function Website<B extends Bindings>(
302
298
  }
303
299
 
304
300
  let url: string | undefined;
305
- if (dev && scope.local) {
301
+ const devCommand = typeof dev === "string" ? dev : dev?.command;
302
+ if (devCommand && scope.local) {
306
303
  url = await scope.spawn(name, {
307
- cmd: typeof dev === "string" ? dev : dev.command,
304
+ cmd: devCommand,
308
305
  cwd: paths.cwd,
309
306
  extract: (line) => {
310
307
  const URL_REGEX =
@@ -317,17 +314,7 @@ export async function Website<B extends Bindings>(
317
314
  }
318
315
  },
319
316
  env: {
320
- ...Object.fromEntries(
321
- Object.entries(env ?? {}).flatMap(([key, value]) => {
322
- if (isSecret(value)) {
323
- return [[key, value.unencrypted]];
324
- }
325
- if (typeof value === "string") {
326
- return [[key, value]];
327
- }
328
- return [];
329
- }),
330
- ),
317
+ ...unencryptSecrets(env ?? {}),
331
318
  ...(typeof dev === "object" ? dev.env : {}),
332
319
  FORCE_COLOR: "1",
333
320
  ...process.env,
@@ -371,7 +358,7 @@ export const spreadBuildProps = (
371
358
  export const spreadDevProps = (
372
359
  props: { dev?: WebsiteProps<Bindings>["dev"] } | undefined,
373
360
  defaultCommand: string,
374
- ): WebsiteProps<Bindings>["dev"] => {
361
+ ): Exclude<WebsiteProps<Bindings>["dev"], undefined> => {
375
362
  if (typeof props?.dev === "object") {
376
363
  return {
377
364
  ...props.dev,
@@ -183,6 +183,7 @@ export interface WorkerMetadata {
183
183
  observability: {
184
184
  enabled: boolean;
185
185
  };
186
+ logpush?: boolean;
186
187
  migrations?: SingleStepMigration;
187
188
  main_module?: string;
188
189
  body_part?: string;
@@ -325,6 +326,7 @@ export async function prepareWorkerMetadata(
325
326
  observability: {
326
327
  enabled: props.observability?.enabled !== false,
327
328
  },
329
+ logpush: props.logpush ?? false,
328
330
  // TODO(sam): base64 encode instead? 0 collision risk vs readability.
329
331
  tags: [
330
332
  // encode a mapping table of Durable Object stable ID -> binding name