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.
- package/bin/alchemy.js +3797 -3857
- package/bin/trpc.ts +4 -9
- package/lib/alchemy.d.ts +3 -3
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +2 -15
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.js +24 -9
- package/lib/apply.js.map +1 -1
- package/lib/aws/utils.d.ts +2 -1
- package/lib/aws/utils.d.ts.map +1 -1
- package/lib/aws/utils.js.map +1 -1
- package/lib/clickhouse/api/client.gen.d.ts +13 -0
- package/lib/clickhouse/api/client.gen.d.ts.map +1 -0
- package/lib/clickhouse/api/client.gen.js +7 -0
- package/lib/clickhouse/api/client.gen.js.map +1 -0
- package/lib/clickhouse/api/sdk.gen.d.ts +322 -0
- package/lib/clickhouse/api/sdk.gen.d.ts.map +1 -0
- package/lib/clickhouse/api/sdk.gen.js +683 -0
- package/lib/clickhouse/api/sdk.gen.js.map +1 -0
- package/lib/clickhouse/api/types.gen.d.ts +5030 -0
- package/lib/clickhouse/api/types.gen.d.ts.map +1 -0
- package/lib/clickhouse/api/types.gen.js +2 -0
- package/lib/clickhouse/api/types.gen.js.map +1 -0
- package/lib/clickhouse/api-key.d.ts +29 -0
- package/lib/clickhouse/api-key.d.ts.map +1 -0
- package/lib/clickhouse/api-key.js +83 -0
- package/lib/clickhouse/api-key.js.map +1 -0
- package/lib/clickhouse/api.d.ts +36 -0
- package/lib/clickhouse/api.d.ts.map +1 -0
- package/lib/clickhouse/api.js +48 -0
- package/lib/clickhouse/api.js.map +1 -0
- package/lib/clickhouse/index.d.ts +4 -0
- package/lib/clickhouse/index.d.ts.map +1 -0
- package/lib/clickhouse/index.js +4 -0
- package/lib/clickhouse/index.js.map +1 -0
- package/lib/clickhouse/organization.d.ts +16 -0
- package/lib/clickhouse/organization.d.ts.map +1 -0
- package/lib/clickhouse/organization.js +21 -0
- package/lib/clickhouse/organization.js.map +1 -0
- package/lib/clickhouse/service.d.ts +275 -0
- package/lib/clickhouse/service.d.ts.map +1 -0
- package/lib/clickhouse/service.js +274 -0
- package/lib/clickhouse/service.js.map +1 -0
- package/lib/cloudflare/account-api-token.d.ts.map +1 -1
- package/lib/cloudflare/account-api-token.js +1 -1
- package/lib/cloudflare/account-api-token.js.map +1 -1
- package/lib/cloudflare/bun-spa/bun-spa.d.ts +15 -0
- package/lib/cloudflare/bun-spa/bun-spa.d.ts.map +1 -0
- package/lib/cloudflare/bun-spa/bun-spa.js +95 -0
- package/lib/cloudflare/bun-spa/bun-spa.js.map +1 -0
- 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 +11 -1
- package/lib/cloudflare/container.d.ts.map +1 -1
- package/lib/cloudflare/container.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/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +3 -3
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.js +2 -0
- package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +36 -49
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
- package/lib/cloudflare/permission-groups.d.ts +2 -3
- package/lib/cloudflare/permission-groups.d.ts.map +1 -1
- package/lib/cloudflare/permission-groups.js +10 -15
- package/lib/cloudflare/permission-groups.js.map +1 -1
- package/lib/cloudflare/redwood/redwood.d.ts +1 -1
- package/lib/cloudflare/redwood/redwood.js +1 -1
- package/lib/cloudflare/util/filter-env-bindings.d.ts +62 -0
- package/lib/cloudflare/util/filter-env-bindings.d.ts.map +1 -0
- package/lib/cloudflare/util/filter-env-bindings.js +81 -0
- package/lib/cloudflare/util/filter-env-bindings.js.map +1 -0
- package/lib/cloudflare/website.d.ts +2 -2
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +6 -18
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts +1 -0
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +1 -0
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +11 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +8 -0
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +3 -4
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/coinbase/client.d.ts +54 -0
- package/lib/coinbase/client.d.ts.map +1 -0
- package/lib/coinbase/client.js +89 -0
- package/lib/coinbase/client.js.map +1 -0
- package/lib/coinbase/evm-account.d.ts +118 -0
- package/lib/coinbase/evm-account.d.ts.map +1 -0
- package/lib/coinbase/evm-account.js +198 -0
- package/lib/coinbase/evm-account.js.map +1 -0
- package/lib/coinbase/evm-smart-account.d.ts +122 -0
- package/lib/coinbase/evm-smart-account.d.ts.map +1 -0
- package/lib/coinbase/evm-smart-account.js +213 -0
- package/lib/coinbase/evm-smart-account.js.map +1 -0
- package/lib/coinbase/faucet.d.ts +11 -0
- package/lib/coinbase/faucet.d.ts.map +1 -0
- package/lib/coinbase/faucet.js +146 -0
- package/lib/coinbase/faucet.js.map +1 -0
- package/lib/coinbase/index.d.ts +4 -0
- package/lib/coinbase/index.d.ts.map +1 -0
- package/lib/coinbase/index.js +4 -0
- package/lib/coinbase/index.js.map +1 -0
- package/lib/coinbase/types.d.ts +28 -0
- package/lib/coinbase/types.d.ts.map +1 -0
- package/lib/coinbase/types.js +4 -0
- package/lib/{util/telemetry → coinbase}/types.js.map +1 -1
- package/lib/coinbase/utils.d.ts +13 -0
- package/lib/coinbase/utils.d.ts.map +1 -0
- package/lib/coinbase/utils.js +17 -0
- package/lib/coinbase/utils.js.map +1 -0
- package/lib/neon/api/types.gen.d.ts +7 -2
- package/lib/neon/api/types.gen.d.ts.map +1 -1
- package/lib/planetscale/api/types.gen.d.ts +20 -4
- package/lib/planetscale/api/types.gen.d.ts.map +1 -1
- package/lib/planetscale/api.d.ts.map +1 -1
- package/lib/planetscale/api.js +1 -0
- package/lib/planetscale/api.js.map +1 -1
- package/lib/planetscale/password.d.ts.map +1 -1
- package/lib/planetscale/password.js +1 -13
- package/lib/planetscale/password.js.map +1 -1
- package/lib/resource.d.ts.map +1 -1
- package/lib/resource.js +7 -3
- package/lib/resource.js.map +1 -1
- package/lib/scope.d.ts +4 -9
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +16 -37
- package/lib/scope.js.map +1 -1
- package/lib/state/instrumented-state-store.d.ts +1 -3
- package/lib/state/instrumented-state-store.d.ts.map +1 -1
- package/lib/state/instrumented-state-store.js +19 -21
- package/lib/state/instrumented-state-store.js.map +1 -1
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/bun.js +1 -2
- package/lib/test/bun.js.map +1 -1
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +1 -2
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/diff.d.ts +5 -0
- package/lib/util/diff.d.ts.map +1 -0
- package/lib/util/diff.js +14 -0
- package/lib/util/diff.js.map +1 -0
- package/lib/util/telemetry.d.ts +52 -0
- package/lib/util/telemetry.d.ts.map +1 -0
- package/lib/util/telemetry.js +191 -0
- package/lib/util/telemetry.js.map +1 -0
- package/package.json +19 -1
- package/src/alchemy.ts +5 -20
- package/src/apply.ts +29 -12
- package/src/aws/utils.ts +2 -1
- package/src/clickhouse/api/client.gen.ts +28 -0
- package/src/clickhouse/api/sdk.gen.ts +1286 -0
- package/src/clickhouse/api/types.gen.ts +5898 -0
- package/src/clickhouse/api-key.ts +136 -0
- package/src/clickhouse/api.ts +84 -0
- package/src/clickhouse/index.ts +3 -0
- package/src/clickhouse/organization.ts +31 -0
- package/src/clickhouse/service.ts +668 -0
- package/src/cloudflare/account-api-token.ts +1 -4
- package/src/cloudflare/bun-spa/bun-spa.ts +139 -0
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/container.ts +20 -1
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/miniflare/build-worker-options.ts +6 -4
- package/src/cloudflare/miniflare/miniflare-controller.ts +2 -0
- package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +41 -58
- package/src/cloudflare/permission-groups.ts +21 -49
- package/src/cloudflare/redwood/redwood.ts +1 -1
- package/src/cloudflare/util/filter-env-bindings.ts +96 -0
- package/src/cloudflare/website.ts +11 -24
- package/src/cloudflare/worker-metadata.ts +2 -0
- package/src/cloudflare/worker.ts +12 -1
- package/src/cloudflare/wrangler.json.ts +12 -8
- package/src/coinbase/client.ts +146 -0
- package/src/coinbase/evm-account.ts +317 -0
- package/src/coinbase/evm-smart-account.ts +339 -0
- package/src/coinbase/faucet.ts +173 -0
- package/src/coinbase/index.ts +3 -0
- package/src/coinbase/types.ts +33 -0
- package/src/coinbase/utils.ts +19 -0
- package/src/neon/api/types.gen.ts +13 -1
- package/src/planetscale/api/types.gen.ts +22 -2
- package/src/planetscale/api.ts +1 -0
- package/src/planetscale/password.ts +1 -14
- package/src/resource.ts +11 -4
- package/src/scope.ts +22 -45
- package/src/state/instrumented-state-store.ts +27 -26
- package/src/test/bun.ts +1 -2
- package/src/test/vitest.ts +1 -2
- package/src/util/diff.ts +14 -0
- package/src/util/telemetry.ts +297 -0
- package/workers/tunnel-proxy.js +1 -1
- package/lib/util/telemetry/client.d.ts +0 -31
- package/lib/util/telemetry/client.d.ts.map +0 -1
- package/lib/util/telemetry/client.js +0 -121
- package/lib/util/telemetry/client.js.map +0 -1
- package/lib/util/telemetry/constants.d.ts +0 -5
- package/lib/util/telemetry/constants.d.ts.map +0 -1
- package/lib/util/telemetry/constants.js +0 -8
- package/lib/util/telemetry/constants.js.map +0 -1
- package/lib/util/telemetry/context.d.ts +0 -15
- package/lib/util/telemetry/context.d.ts.map +0 -1
- package/lib/util/telemetry/context.js +0 -128
- package/lib/util/telemetry/context.js.map +0 -1
- package/lib/util/telemetry/index.d.ts +0 -3
- package/lib/util/telemetry/index.d.ts.map +0 -1
- package/lib/util/telemetry/index.js +0 -3
- package/lib/util/telemetry/index.js.map +0 -1
- package/lib/util/telemetry/types.d.ts +0 -68
- package/lib/util/telemetry/types.d.ts.map +0 -1
- package/lib/util/telemetry/types.js +0 -1
- package/src/util/telemetry/client.ts +0 -156
- package/src/util/telemetry/constants.ts +0 -13
- package/src/util/telemetry/context.ts +0 -154
- package/src/util/telemetry/index.ts +0 -2
- 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
|
+
}
|
|
@@ -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 =
|
|
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.
|
package/src/cloudflare/index.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
64
|
-
.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
105
|
-
const
|
|
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
|
-
|
|
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
|
|
125
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
301
|
+
const devCommand = typeof dev === "string" ? dev : dev?.command;
|
|
302
|
+
if (devCommand && scope.local) {
|
|
306
303
|
url = await scope.spawn(name, {
|
|
307
|
-
cmd:
|
|
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
|
-
...
|
|
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
|