@brutal-ai/server 0.0.0-stage → 0.2.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/README.md +8 -2
- package/cli.js +254 -0
- package/config.d.ts +21 -0
- package/config.js +79 -0
- package/package.json +33 -4
- package/worker/index.js +41540 -0
- package/worker/index.js.map +1 -0
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @brutal-ai/server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Brutal](https://github.com/cldixon/brutal): feedback from your agents, on your own Cloudflare
|
|
4
|
+
account. This package is the prebuilt Worker, the `defineBrutal` config helper and the
|
|
5
|
+
`brutal-server` CLI. Start with:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun create brutal-ai my-brutal
|
|
9
|
+
```
|
package/cli.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// worker/cli/brutal-server.ts
|
|
5
|
+
import { writeFileSync } from "fs";
|
|
6
|
+
import { basename } from "path";
|
|
7
|
+
var {$ } = globalThis.Bun;
|
|
8
|
+
|
|
9
|
+
// worker/config.ts
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { isAbsolute, join } from "path";
|
|
12
|
+
import { bindings, defineConfig } from "cf/config";
|
|
13
|
+
var CONFIG_FILE = "brutal.config.json";
|
|
14
|
+
var DEFAULTS = { worker: "brutal" };
|
|
15
|
+
var ENV = {
|
|
16
|
+
worker: "BRUTAL_WORKER",
|
|
17
|
+
database: "BRUTAL_DATABASE",
|
|
18
|
+
domain: "BRUTAL_DOMAIN"
|
|
19
|
+
};
|
|
20
|
+
function configPath(dir) {
|
|
21
|
+
const named = process.env.BRUTAL_CONFIG;
|
|
22
|
+
return named ? isAbsolute(named) ? named : join(dir, named) : join(dir, CONFIG_FILE);
|
|
23
|
+
}
|
|
24
|
+
function readConfigFile(dir) {
|
|
25
|
+
const path = configPath(dir);
|
|
26
|
+
if (!existsSync(path)) {
|
|
27
|
+
if (process.env.BRUTAL_CONFIG && !process.env.BRUTAL_SETUP) {
|
|
28
|
+
throw new Error(`BRUTAL_CONFIG points at ${path}, which doesn't exist`);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
33
|
+
}
|
|
34
|
+
function loadBrutalConfig(dir, { allowDefaults = false } = {}) {
|
|
35
|
+
const file = readConfigFile(dir);
|
|
36
|
+
const env = {};
|
|
37
|
+
for (const [key, name] of Object.entries(ENV)) {
|
|
38
|
+
const value = process.env[name];
|
|
39
|
+
if (value !== undefined)
|
|
40
|
+
env[key] = value || null;
|
|
41
|
+
}
|
|
42
|
+
if (!file && !env.worker && !allowDefaults && !process.env.BRUTAL_SETUP) {
|
|
43
|
+
throw new Error(`No ${CONFIG_FILE} in ${dir}. Run \`bun run setup\` to make one, ` + "or set BRUTAL_WORKER (and BRUTAL_DOMAIN) in the environment.");
|
|
44
|
+
}
|
|
45
|
+
const config = { ...DEFAULTS, ...file, ...env };
|
|
46
|
+
if (!config.worker)
|
|
47
|
+
throw new Error(`${CONFIG_FILE} needs a worker name`);
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// worker/cli/access.ts
|
|
52
|
+
var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
53
|
+
async function ensureAccess(cf, want, dryRun = false) {
|
|
54
|
+
const emails = [...new Set(want.emails.map((e) => e.trim().toLowerCase()).filter(Boolean))];
|
|
55
|
+
if (emails.length === 0 || emails.some((e) => !e.includes("@"))) {
|
|
56
|
+
throw new Error("Access needs at least one email address to let in.");
|
|
57
|
+
}
|
|
58
|
+
const changes = [];
|
|
59
|
+
const org = await cf(["organization", "get"]);
|
|
60
|
+
const team = org?.auth_domain ?? "";
|
|
61
|
+
if (!team) {
|
|
62
|
+
throw new Error("This Cloudflare account has no Zero Trust organization yet. Open Zero Trust in the " + "Cloudflare dashboard once to create one (the Free plan is enough), then run this again.");
|
|
63
|
+
}
|
|
64
|
+
const idps = await cf(["identity-providers", "list"]) ?? [];
|
|
65
|
+
let otp = idps.find((i) => i.type === "onetimepin");
|
|
66
|
+
if (!otp) {
|
|
67
|
+
changes.push("create the one-time PIN login");
|
|
68
|
+
otp = dryRun ? { id: "(new)", type: "onetimepin" } : await cf(["identity-providers", "create"], {
|
|
69
|
+
name: "One-time PIN",
|
|
70
|
+
type: "onetimepin",
|
|
71
|
+
config: {}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const policyName = `Brutal ${want.worker}: owners`;
|
|
75
|
+
const policyBody = {
|
|
76
|
+
name: policyName,
|
|
77
|
+
decision: "allow",
|
|
78
|
+
include: emails.map((email) => ({ email: { email } })),
|
|
79
|
+
exclude: [],
|
|
80
|
+
require: []
|
|
81
|
+
};
|
|
82
|
+
const policies = await cf(["access", "policies", "list"]) ?? [];
|
|
83
|
+
let policy = policies.find((p) => p.name === policyName);
|
|
84
|
+
if (!policy) {
|
|
85
|
+
changes.push(`create the policy "${policyName}" (allow ${emails.join(", ")})`);
|
|
86
|
+
policy = dryRun ? { id: "(new)", ...policyBody } : await cf(["access", "policies", "create"], policyBody);
|
|
87
|
+
} else if (policy.decision !== "allow" || !same(policy.include, policyBody.include)) {
|
|
88
|
+
changes.push(`correct the policy "${policyName}" (allow ${emails.join(", ")} only)`);
|
|
89
|
+
if (!dryRun) {
|
|
90
|
+
policy = await cf(["access", "policies", "update", policy.id], policyBody);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const domain = `${want.host}/dashboard`;
|
|
94
|
+
const title = `Brutal (${want.host})`;
|
|
95
|
+
const appBody = {
|
|
96
|
+
name: title,
|
|
97
|
+
domain,
|
|
98
|
+
type: "self_hosted",
|
|
99
|
+
session_duration: want.sessionDuration,
|
|
100
|
+
allowed_idps: [otp.id],
|
|
101
|
+
auto_redirect_to_identity: true,
|
|
102
|
+
app_launcher_visible: false,
|
|
103
|
+
policies: [{ id: policy.id, precedence: 1 }]
|
|
104
|
+
};
|
|
105
|
+
const apps = await cf(["access", "applications", "list"]) ?? [];
|
|
106
|
+
let app = apps.find((a) => a.domain === domain);
|
|
107
|
+
if (!app) {
|
|
108
|
+
changes.push(`create the application for ${domain} (${want.sessionDuration} sessions)`);
|
|
109
|
+
if (!dryRun)
|
|
110
|
+
app = await cf(["access", "applications", "create"], appBody);
|
|
111
|
+
} else if (app.name !== title || app.type !== "self_hosted" || app.session_duration !== want.sessionDuration || !same(app.allowed_idps, [otp.id]) || app.auto_redirect_to_identity !== true || !same((app.policies ?? []).map((p) => p.id), [policy.id])) {
|
|
112
|
+
changes.push(`correct the application for ${domain}`);
|
|
113
|
+
if (!dryRun)
|
|
114
|
+
app = await cf(["access", "applications", "update", app.id], appBody);
|
|
115
|
+
}
|
|
116
|
+
return { team, aud: app?.aud ?? "", changes };
|
|
117
|
+
}
|
|
118
|
+
function parseCfJson(stdout) {
|
|
119
|
+
const lines = stdout.split(`
|
|
120
|
+
`);
|
|
121
|
+
const start = lines.findIndex((l) => l.startsWith("[") || l.startsWith("{"));
|
|
122
|
+
if (start === -1)
|
|
123
|
+
return null;
|
|
124
|
+
return JSON.parse(lines.slice(start).join(`
|
|
125
|
+
`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// worker/cli/brutal-server.ts
|
|
129
|
+
var root = process.cwd();
|
|
130
|
+
var [command, ...rest] = process.argv.slice(2);
|
|
131
|
+
process.env.CF_SEND_TELEMETRY ??= "false";
|
|
132
|
+
var commands = { setup, deploy, secrets, dev, access };
|
|
133
|
+
var run = command ? commands[command] : undefined;
|
|
134
|
+
if (!run) {
|
|
135
|
+
console.log("Usage: brutal-server <setup|deploy|secrets|dev|access>");
|
|
136
|
+
process.exit(command ? 1 : 0);
|
|
137
|
+
}
|
|
138
|
+
await run();
|
|
139
|
+
async function requireAuth() {
|
|
140
|
+
const whoami = await $`bunx cf auth whoami`.nothrow().quiet();
|
|
141
|
+
let authenticated = false;
|
|
142
|
+
try {
|
|
143
|
+
authenticated = whoami.exitCode === 0 && JSON.parse(whoami.stdout.toString()).authenticated;
|
|
144
|
+
} catch {}
|
|
145
|
+
if (!authenticated) {
|
|
146
|
+
console.error("Not signed in to Cloudflare. Run: bunx cf auth login");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function setup() {
|
|
151
|
+
console.log(`
|
|
152
|
+
Brutal setup
|
|
153
|
+
`);
|
|
154
|
+
process.env.BRUTAL_SETUP = "1";
|
|
155
|
+
await requireAuth();
|
|
156
|
+
const current = readConfigFile(root) ?? {};
|
|
157
|
+
const worker = prompt(`Worker name [${current.worker ?? DEFAULTS.worker}]:`)?.trim();
|
|
158
|
+
const domainAnswer = prompt(`Custom domain, on a zone in your Cloudflare account (blank for none, "-" to remove) [${current.domain ?? ""}]:`)?.trim();
|
|
159
|
+
const domain = domainAnswer === "-" ? "" : domainAnswer || current.domain || "";
|
|
160
|
+
const config = {
|
|
161
|
+
...current,
|
|
162
|
+
worker: worker || current.worker || DEFAULTS.worker,
|
|
163
|
+
domain: domain || null
|
|
164
|
+
};
|
|
165
|
+
writeFileSync(configPath(root), `${JSON.stringify(config, null, 2)}
|
|
166
|
+
`);
|
|
167
|
+
console.log(`
|
|
168
|
+
\u2713 Wrote ${basename(configPath(root))}`);
|
|
169
|
+
console.log(domain ? `\u2713 ${domain} will route to this Worker (its zone must be on your account)` : "\u2713 No custom domain: it will be served from *.workers.dev");
|
|
170
|
+
console.log(`
|
|
171
|
+
Next: bun run deploy
|
|
172
|
+
`);
|
|
173
|
+
}
|
|
174
|
+
async function workerExists(worker) {
|
|
175
|
+
return (await $`bunx cf workers secrets list --worker ${worker}`.nothrow().quiet()).exitCode === 0;
|
|
176
|
+
}
|
|
177
|
+
async function deploy() {
|
|
178
|
+
const { worker } = loadBrutalConfig(root);
|
|
179
|
+
const existed = await workerExists(worker);
|
|
180
|
+
const code = await interactive(["bunx", "cf", "deploy", ...rest]);
|
|
181
|
+
if (code !== 0)
|
|
182
|
+
process.exit(code);
|
|
183
|
+
if (existed || rest.includes("--dry-run"))
|
|
184
|
+
return;
|
|
185
|
+
if (process.env.CI) {
|
|
186
|
+
console.log(`
|
|
187
|
+
No setup code was set here, since CI logs may be public.`);
|
|
188
|
+
console.log("Run `bun run secrets` on your own machine for one before you claim it.\n");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
await secrets();
|
|
192
|
+
}
|
|
193
|
+
async function secrets() {
|
|
194
|
+
const { worker } = loadBrutalConfig(root);
|
|
195
|
+
const bytes = crypto.getRandomValues(new Uint8Array(9));
|
|
196
|
+
const setupCode = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
197
|
+
await $`bunx cf workers secrets update SETUP_CODE --worker ${worker} --type secret_text --text ${setupCode}`.quiet();
|
|
198
|
+
console.log(`
|
|
199
|
+
\u2713 SETUP_CODE set on ${worker}`);
|
|
200
|
+
console.log(`
|
|
201
|
+
Setup code: ${setupCode}`);
|
|
202
|
+
console.log(`Open /setup on your instance and enter it to make your keys.
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
async function access() {
|
|
206
|
+
const dryRun = rest.includes("--dry-run");
|
|
207
|
+
await requireAuth();
|
|
208
|
+
const current = readConfigFile(root);
|
|
209
|
+
const config = loadBrutalConfig(root);
|
|
210
|
+
if (!config.domain) {
|
|
211
|
+
console.error("Access needs a custom domain: run `bun run setup` and give one, deploy, then run this.");
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
let emails = config.access?.emails ?? [];
|
|
215
|
+
if (emails.length === 0) {
|
|
216
|
+
const answer = prompt("Email(s) allowed to sign in to the dashboard, comma-separated:");
|
|
217
|
+
emails = (answer ?? "").split(",").map((e) => e.trim()).filter(Boolean);
|
|
218
|
+
}
|
|
219
|
+
const cf = async (args, body) => {
|
|
220
|
+
const extra = body === undefined ? [] : ["--body", JSON.stringify(body)];
|
|
221
|
+
const out = await $`bunx cf zero-trust ${args} ${extra}`.nothrow().quiet();
|
|
222
|
+
if (out.exitCode !== 0) {
|
|
223
|
+
throw new Error(`cf zero-trust ${args.join(" ")} failed: ${out.stderr.toString() || out.stdout.toString()}`);
|
|
224
|
+
}
|
|
225
|
+
return parseCfJson(out.stdout.toString());
|
|
226
|
+
};
|
|
227
|
+
console.log(`
|
|
228
|
+
Cloudflare Access for https://${config.domain}/dashboard
|
|
229
|
+
`);
|
|
230
|
+
const ensured = await ensureAccess(cf, { host: config.domain, worker: config.worker, emails, sessionDuration: "24h" }, dryRun);
|
|
231
|
+
for (const change of ensured.changes)
|
|
232
|
+
console.log(`${dryRun ? "Would" : "\u2713"} ${change}`);
|
|
233
|
+
if (ensured.changes.length === 0)
|
|
234
|
+
console.log("\u2713 Access is already as it should be");
|
|
235
|
+
if (dryRun)
|
|
236
|
+
return;
|
|
237
|
+
const next = {
|
|
238
|
+
...current ?? config,
|
|
239
|
+
worker: current?.worker ?? config.worker,
|
|
240
|
+
access: { emails, team: ensured.team, aud: ensured.aud }
|
|
241
|
+
};
|
|
242
|
+
writeFileSync(configPath(root), `${JSON.stringify(next, null, 2)}
|
|
243
|
+
`);
|
|
244
|
+
console.log(`\u2713 Wrote the team and application to ${basename(configPath(root))}`);
|
|
245
|
+
console.log(`
|
|
246
|
+
Next: bun run deploy. The dashboard then signs in with a code mailed to you.
|
|
247
|
+
`);
|
|
248
|
+
}
|
|
249
|
+
async function dev() {
|
|
250
|
+
process.exit(await interactive(["bunx", "cf", "dev", "--port", "8787", ...rest]));
|
|
251
|
+
}
|
|
252
|
+
function interactive(cmd) {
|
|
253
|
+
return Bun.spawn(cmd, { cwd: root, stdio: ["inherit", "inherit", "inherit"] }).exited;
|
|
254
|
+
}
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A Brutal instance's settings, as brutal.config.json holds them. */
|
|
2
|
+
export type BrutalConfig = {
|
|
3
|
+
/** Worker name. Also names the D1 database unless `database` is set. */
|
|
4
|
+
worker: string;
|
|
5
|
+
/** D1 database for reports and keys. Defaults to the Worker name. */
|
|
6
|
+
database?: string;
|
|
7
|
+
/** Custom domain on a zone in your Cloudflare account. Without one, workers.dev serves it. */
|
|
8
|
+
domain?: string | null;
|
|
9
|
+
/** Cloudflare Access in front of /dashboard; `brutal-server access` sets it up. */
|
|
10
|
+
access?: { emails: string[]; team?: string; aud?: string } | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The Brutal Worker's cf config, from the brutal.config.json in `dir`. Use as the default export
|
|
15
|
+
* of cloudflare.config.ts:
|
|
16
|
+
*
|
|
17
|
+
* export default defineBrutal({ dir: import.meta.dirname });
|
|
18
|
+
*/
|
|
19
|
+
export function defineBrutal(options: { dir: string; entrypoint?: string }): unknown;
|
|
20
|
+
|
|
21
|
+
export function loadBrutalConfig(dir: string, options?: { allowDefaults?: boolean }): BrutalConfig;
|
package/config.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// worker/config.ts
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { bindings, defineConfig } from "cf/config";
|
|
5
|
+
var CONFIG_FILE = "brutal.config.json";
|
|
6
|
+
var DEFAULTS = { worker: "brutal" };
|
|
7
|
+
var ENV = {
|
|
8
|
+
worker: "BRUTAL_WORKER",
|
|
9
|
+
database: "BRUTAL_DATABASE",
|
|
10
|
+
domain: "BRUTAL_DOMAIN"
|
|
11
|
+
};
|
|
12
|
+
function configPath(dir) {
|
|
13
|
+
const named = process.env.BRUTAL_CONFIG;
|
|
14
|
+
return named ? isAbsolute(named) ? named : join(dir, named) : join(dir, CONFIG_FILE);
|
|
15
|
+
}
|
|
16
|
+
function readConfigFile(dir) {
|
|
17
|
+
const path = configPath(dir);
|
|
18
|
+
if (!existsSync(path)) {
|
|
19
|
+
if (process.env.BRUTAL_CONFIG && !process.env.BRUTAL_SETUP) {
|
|
20
|
+
throw new Error(`BRUTAL_CONFIG points at ${path}, which doesn't exist`);
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
25
|
+
}
|
|
26
|
+
function loadBrutalConfig(dir, { allowDefaults = false } = {}) {
|
|
27
|
+
const file = readConfigFile(dir);
|
|
28
|
+
const env = {};
|
|
29
|
+
for (const [key, name] of Object.entries(ENV)) {
|
|
30
|
+
const value = process.env[name];
|
|
31
|
+
if (value !== undefined)
|
|
32
|
+
env[key] = value || null;
|
|
33
|
+
}
|
|
34
|
+
if (!file && !env.worker && !allowDefaults && !process.env.BRUTAL_SETUP) {
|
|
35
|
+
throw new Error(`No ${CONFIG_FILE} in ${dir}. Run \`bun run setup\` to make one, ` + "or set BRUTAL_WORKER (and BRUTAL_DOMAIN) in the environment.");
|
|
36
|
+
}
|
|
37
|
+
const config = { ...DEFAULTS, ...file, ...env };
|
|
38
|
+
if (!config.worker)
|
|
39
|
+
throw new Error(`${CONFIG_FILE} needs a worker name`);
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
function defineBrutal({ dir, entrypoint }) {
|
|
43
|
+
return defineConfig(() => {
|
|
44
|
+
const argv = process.argv;
|
|
45
|
+
const brutal = loadBrutalConfig(dir, {
|
|
46
|
+
allowDefaults: argv.includes("dev") || argv.includes("types")
|
|
47
|
+
});
|
|
48
|
+
const domain = brutal.domain || null;
|
|
49
|
+
const access = domain && brutal.access?.team && brutal.access.aud ? brutal.access : null;
|
|
50
|
+
return {
|
|
51
|
+
worker: {
|
|
52
|
+
name: brutal.worker,
|
|
53
|
+
compatibilityDate: "2026-10-01",
|
|
54
|
+
compatibilityFlags: ["nodejs_compat"],
|
|
55
|
+
entrypoint: entrypoint ?? join(import.meta.dirname, "worker", "index.js"),
|
|
56
|
+
observability: { enabled: true },
|
|
57
|
+
...domain && { domains: [domain] },
|
|
58
|
+
workersDev: !domain,
|
|
59
|
+
previewUrls: false,
|
|
60
|
+
env: {
|
|
61
|
+
DB: bindings.d1({ name: brutal.database ?? brutal.worker }),
|
|
62
|
+
...access && {
|
|
63
|
+
ACCESS_TEAM: bindings.text(access.team ?? ""),
|
|
64
|
+
ACCESS_AUD: bindings.text(access.aud ?? ""),
|
|
65
|
+
ACCESS_EMAILS: bindings.text(access.emails.join(","))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
CONFIG_FILE,
|
|
74
|
+
DEFAULTS,
|
|
75
|
+
configPath,
|
|
76
|
+
defineBrutal,
|
|
77
|
+
loadBrutalConfig,
|
|
78
|
+
readConfigFile
|
|
79
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
|
+
"version": "0.2.0",
|
|
3
|
+
"license": "MIT",
|
|
2
4
|
"name": "@brutal-ai/server",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
5
|
+
"description": "Brutal: feedback from your agents, deployed to your own Cloudflare account",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/cldixon/brutal.git",
|
|
10
|
+
"directory": "worker"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"brutal-server": "cli.js"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
"./config": {
|
|
17
|
+
"types": "./config.d.ts",
|
|
18
|
+
"default": "./config.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"cli.js",
|
|
23
|
+
"config.js",
|
|
24
|
+
"config.d.ts",
|
|
25
|
+
"worker",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"cf": "1.0.0-beta.28",
|
|
30
|
+
"wrangler": "^4.149.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"bun": ">=1.2.0"
|
|
34
|
+
}
|
|
35
|
+
}
|