freertc 0.1.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 +246 -0
- package/bin/freertc.mjs +106 -0
- package/package.json +68 -0
- package/public/app.js +2851 -0
- package/public/index.html +821 -0
- package/scripts/d1-schema.sql +44 -0
- package/scripts/dev-server.mjs +129 -0
- package/scripts/non-cloudflare-server.mjs +427 -0
- package/scripts/postinstall-message.mjs +19 -0
- package/scripts/project-bootstrap.mjs +113 -0
- package/scripts/wrangler-install-wizard.mjs +697 -0
- package/src/index.js +690 -0
- package/wrangler.template.jsonc +71 -0
- package/wrangler.workers-dev.jsonc +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# freertc Cloudflare Worker (WebSocket + D1)
|
|
2
|
+
|
|
3
|
+
This project provides a Cloudflare Worker signaling relay for WebRTC peers using the [Peer Signaling Protocol (PSP)](https://github.com/draeder/Peer-Signaling-Protocol-Specification) envelope shape.
|
|
4
|
+
|
|
5
|
+
## Install from npm
|
|
6
|
+
|
|
7
|
+
Local project install:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install freertc
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Global install:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g freertc
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
When you run the CLI from your project directory, `freertc` copies the required worker files into that directory on first run:
|
|
20
|
+
|
|
21
|
+
- `src/index.js`
|
|
22
|
+
- `public/index.html`
|
|
23
|
+
- `public/app.js`
|
|
24
|
+
- `scripts/d1-schema.sql`
|
|
25
|
+
- `wrangler.template.jsonc`
|
|
26
|
+
- `wrangler.workers-dev.jsonc`
|
|
27
|
+
|
|
28
|
+
## What this worker does
|
|
29
|
+
|
|
30
|
+
- Accepts WebSocket client connections at `/ws`.
|
|
31
|
+
- Validates [PSP](https://github.com/draeder/Peer-Signaling-Protocol-Specification) message envelopes (`psp_version`, `type`, `network`, `from`, `message_id`, `timestamp`).
|
|
32
|
+
- Supports discovery, negotiation, control, and extension message types.
|
|
33
|
+
- Stores peer announcements in Cloudflare D1 (`psp_announcements`).
|
|
34
|
+
- Stores directed signaling messages in Cloudflare D1 (`psp_relay`).
|
|
35
|
+
- Exposes a simple relay registry at `/api/v1/relays` when D1 is configured.
|
|
36
|
+
- Delivers queued relay messages when peers reconnect.
|
|
37
|
+
- Serves the browser demo from `public/`.
|
|
38
|
+
|
|
39
|
+
## Runtime scope
|
|
40
|
+
|
|
41
|
+
- The checked-in Cloudflare Worker runtime is `src/index.js` with Cloudflare D1 (`DB` binding).
|
|
42
|
+
- The Rust/WASM worker lives in `src/lib.rs` and is optional; the default template now uses the JS worker path.
|
|
43
|
+
- The built-in browser demo served by the Worker is `public/index.html` + `public/app.js`.
|
|
44
|
+
- `src/kv.js` and `demo/src/*` are legacy/experimental code paths and are not used by `wrangler dev` or `wrangler deploy` in the current setup.
|
|
45
|
+
|
|
46
|
+
## Supported message types
|
|
47
|
+
|
|
48
|
+
- Discovery: `announce`, `withdraw`, `discover`, `peer_list`, `redirect`
|
|
49
|
+
- Negotiation: `connect_request`, `connect_accept`, `connect_reject`, `offer`, `answer`, `ice_candidate`, `ice_end`, `renegotiate`
|
|
50
|
+
- Control: `ping`, `pong`, `bye`, `error`, `ack`
|
|
51
|
+
- Extension: `ext`
|
|
52
|
+
|
|
53
|
+
## Wrangler install wizard (recommended)
|
|
54
|
+
|
|
55
|
+
Use the interactive wizard from the project directory where you want the worker files and Wrangler config to live:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx freertc wizard
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The default command runs full setup mode (`both`):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx freertc
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can also preselect full setup mode explicitly:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx freertc setup
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Global install flow:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
freertc wizard
|
|
77
|
+
freertc
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
After install, freertc prints a quick-start reminder with the exact next command.
|
|
81
|
+
|
|
82
|
+
The wizard can:
|
|
83
|
+
|
|
84
|
+
- Copy the worker runtime files into your current project when they are missing.
|
|
85
|
+
- Verify Wrangler CLI.
|
|
86
|
+
- Create `wrangler.jsonc` from `wrangler.template.jsonc` if needed.
|
|
87
|
+
- Set Worker name automatically to `freertc-<your-domain>` when a domain is provided.
|
|
88
|
+
- Initialize local D1 schema for `wrangler dev`.
|
|
89
|
+
- Initialize remote D1 schema for deploy.
|
|
90
|
+
- Detect Rust build configs and install `worker-build`/WASM target automatically when required.
|
|
91
|
+
- Check existing Wrangler auth and only run `wrangler login` when needed.
|
|
92
|
+
- Optionally run `npm run dev:cf` and `npm run deploy`.
|
|
93
|
+
|
|
94
|
+
## Manual setup
|
|
95
|
+
|
|
96
|
+
### 1. Install dependencies
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If you installed the npm package instead of cloning the repo, use `npx freertc wizard` instead of the repository scripts below.
|
|
103
|
+
|
|
104
|
+
### 2. Configure Wrangler
|
|
105
|
+
|
|
106
|
+
- If needed, copy `wrangler.template.jsonc` to `wrangler.jsonc`.
|
|
107
|
+
- Set your Worker name, route(s), and D1 database values.
|
|
108
|
+
- Ensure `d1_databases[0].binding` is `DB`.
|
|
109
|
+
|
|
110
|
+
### 3. Initialize D1 schema
|
|
111
|
+
|
|
112
|
+
Local (for `wrangler dev`):
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm run d1:init:local
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Remote (for production):
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm run d1:init:remote
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If your database name is not `freertc-signal`, use Wrangler directly:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
wrangler d1 execute <your-db-name> --local --file scripts/d1-schema.sql
|
|
128
|
+
wrangler d1 execute <your-db-name> --remote --file scripts/d1-schema.sql
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Local development
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm run dev
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`npm run dev` now runs the non-Cloudflare local runtime (plain Node.js + WebSocket).
|
|
138
|
+
|
|
139
|
+
Cloudflare/Wrangler runtime:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm run dev:cf
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Shortcut alias (same behavior):
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm run dev:local
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
You can also choose host/port:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
HOST=127.0.0.1 PORT=8788 npm run dev:node
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`npm run dev:cf` checks local Rust Worker prerequisites automatically:
|
|
158
|
+
|
|
159
|
+
- Uses `wrangler.workers-dev.jsonc` automatically when `wrangler.jsonc` is not present.
|
|
160
|
+
- Only installs `worker-build` and the WebAssembly Rust target when the selected Wrangler config uses a `worker-build` command.
|
|
161
|
+
- The checked-in `wrangler.workers-dev.jsonc` now points to `src/index.js`, so standard demo runs do not require Rust/WASM setup.
|
|
162
|
+
|
|
163
|
+
Endpoints:
|
|
164
|
+
|
|
165
|
+
- WebSocket: `ws://127.0.0.1:8788/ws` (`npm run dev`)
|
|
166
|
+
- Health: `http://127.0.0.1:8788/health` (`npm run dev`)
|
|
167
|
+
- Demo UI: `http://127.0.0.1:8788/` (`npm run dev`)
|
|
168
|
+
|
|
169
|
+
Cloudflare/Wrangler endpoints (default):
|
|
170
|
+
|
|
171
|
+
- WebSocket: `ws://127.0.0.1:8787/ws` (`npm run dev:cf`)
|
|
172
|
+
- Health: `http://127.0.0.1:8787/health` (`npm run dev:cf`)
|
|
173
|
+
- Demo UI: `http://127.0.0.1:8787/` (`npm run dev:cf`)
|
|
174
|
+
- Relay registry: `http://127.0.0.1:8787/api/v1/relays` (`GET`/`POST`, when D1 is configured)
|
|
175
|
+
|
|
176
|
+
## Deploy
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npx freertc deploy
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
If you installed freertc globally:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
freertc deploy
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Repository-only scripts:
|
|
189
|
+
|
|
190
|
+
- `npm run build` builds the Rust/WASM worker via `worker-build --release`.
|
|
191
|
+
- `npm run deploy:raw` deploys without `--env production`.
|
|
192
|
+
- `npm run check` runs `cargo check --target wasm32-unknown-unknown` for the Rust worker path.
|
|
193
|
+
- `npm run dev` runs the standalone local relay (non-Cloudflare).
|
|
194
|
+
- `npm run dev:cf` runs Wrangler/Cloudflare local dev.
|
|
195
|
+
- `npm run dev:node` runs a standalone local relay without Cloudflare/Wrangler.
|
|
196
|
+
- `npm run dev:local` is a no-env shortcut for the standalone local relay.
|
|
197
|
+
|
|
198
|
+
## Troubleshooting custom domain deploys
|
|
199
|
+
|
|
200
|
+
If your custom domain returns:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{"error":"API key is missing"}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
that error is usually from Cloudflare routing/auth config, not from this Worker runtime.
|
|
207
|
+
|
|
208
|
+
Quick checks:
|
|
209
|
+
|
|
210
|
+
1. Compare `https://<workers-subdomain>/health` and `https://<custom-domain>/health`.
|
|
211
|
+
2. Confirm your route/custom domain points to this Worker.
|
|
212
|
+
3. Review Cloudflare Access/API Shield/WAF rules on the custom hostname.
|
|
213
|
+
4. If deploying with `--env production`, verify that environment is the one bound to the route.
|
|
214
|
+
|
|
215
|
+
Expected `/health` response includes JSON like:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{"ok":true,"version":"1.0","peers":0}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Auto WebRTC two-tab test
|
|
222
|
+
|
|
223
|
+
The demo defaults to Auto WebRTC and performs real offer/answer + ICE exchange over this Worker.
|
|
224
|
+
|
|
225
|
+
1. Open `http://127.0.0.1:8787/` in two tabs.
|
|
226
|
+
2. Set both tabs to the same `network` and `session_id`.
|
|
227
|
+
3. Set opposite peer IDs using random hex strings:
|
|
228
|
+
- Tab A `from`: `fc2142e44ec5c76f1bd46ccbb1eb2ed48f66f64260a5299c871f37ac742fa0c9`
|
|
229
|
+
- Tab B `from`: `3e45d44c4ce4f9304a53f42b978fd13d23f85df4d97a88f8eb33ec13a2f8f7b1`
|
|
230
|
+
4. Set each tab `to` to the other tab `from`, or leave `to` empty for auto-discovery.
|
|
231
|
+
5. Connect both sockets and click Start Auto Handshake in both tabs.
|
|
232
|
+
6. When DataChannel is open, send chat messages.
|
|
233
|
+
|
|
234
|
+
## Minimal client expectations
|
|
235
|
+
|
|
236
|
+
- Send `announce` first to bind socket identity (`from`, `network`).
|
|
237
|
+
- Include `session_id` for negotiation messages.
|
|
238
|
+
- Include `to` for directed messages.
|
|
239
|
+
- Use periodic `announce` to refresh presence TTL and receive queued relay messages.
|
|
240
|
+
- Use `ping` for liveness (`pong`) and keepalive.
|
|
241
|
+
|
|
242
|
+
## Notes
|
|
243
|
+
|
|
244
|
+
- TTL is enforced using `timestamp + ttl_ms` (default 30 seconds, max 120 seconds).
|
|
245
|
+
- Malformed envelopes produce [PSP](https://github.com/draeder/Peer-Signaling-Protocol-Specification) `error` responses.
|
|
246
|
+
|
package/bin/freertc.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { ensureProjectFiles, resolveProjectRoot, resolveWranglerCommand } from '../scripts/project-bootstrap.mjs';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(__filename), '..');
|
|
11
|
+
const PROJECT_ROOT = resolveProjectRoot(process.cwd());
|
|
12
|
+
|
|
13
|
+
function printHelp() {
|
|
14
|
+
console.log(`freertc CLI
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
freertc
|
|
18
|
+
freertc wizard
|
|
19
|
+
freertc setup
|
|
20
|
+
freertc init
|
|
21
|
+
freertc install
|
|
22
|
+
freertc deploy [-- <deploy-args>]
|
|
23
|
+
freertc dev [-- <dev-args>]
|
|
24
|
+
freertc dev:cf [-- <dev-args>]
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
npx freertc
|
|
28
|
+
npx freertc wizard
|
|
29
|
+
npx freertc setup
|
|
30
|
+
freertc
|
|
31
|
+
npx freertc deploy
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function runInProject(command, args, { bootstrap = false } = {}) {
|
|
36
|
+
if (bootstrap) {
|
|
37
|
+
ensureProjectFiles(PROJECT_ROOT);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = spawnSync(command, args, {
|
|
41
|
+
cwd: PROJECT_ROOT,
|
|
42
|
+
stdio: 'inherit',
|
|
43
|
+
env: {
|
|
44
|
+
...process.env,
|
|
45
|
+
FREERTC_PACKAGE_ROOT: PACKAGE_ROOT
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (typeof result.status === 'number') {
|
|
50
|
+
process.exit(result.status);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function requireWranglerConfig() {
|
|
57
|
+
const configPath = path.join(PROJECT_ROOT, 'wrangler.jsonc');
|
|
58
|
+
if (fs.existsSync(configPath)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.error(`Missing ${configPath}. Run "npx freertc" or "npx freertc wizard" from this project directory first.`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const [, , subcommand, ...rest] = process.argv;
|
|
67
|
+
|
|
68
|
+
if (!subcommand) {
|
|
69
|
+
runInProject(process.execPath, [path.join(PACKAGE_ROOT, 'scripts', 'wrangler-install-wizard.mjs'), '--mode', 'both'], { bootstrap: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
|
|
73
|
+
printHelp();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (subcommand === 'wizard') {
|
|
78
|
+
runInProject(process.execPath, [path.join(PACKAGE_ROOT, 'scripts', 'wrangler-install-wizard.mjs'), ...rest], { bootstrap: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (subcommand === 'setup') {
|
|
82
|
+
runInProject(process.execPath, [path.join(PACKAGE_ROOT, 'scripts', 'wrangler-install-wizard.mjs'), '--mode', 'both', ...rest], { bootstrap: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (subcommand === 'init' || subcommand === 'install') {
|
|
86
|
+
runInProject(process.execPath, [path.join(PACKAGE_ROOT, 'scripts', 'wrangler-install-wizard.mjs'), '--mode', 'both', ...rest], { bootstrap: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (subcommand === 'deploy') {
|
|
90
|
+
ensureProjectFiles(PROJECT_ROOT);
|
|
91
|
+
requireWranglerConfig();
|
|
92
|
+
const wrangler = resolveWranglerCommand(PROJECT_ROOT);
|
|
93
|
+
runInProject(wrangler.command, [...wrangler.baseArgs, 'deploy', '--env', 'production', ...rest]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (subcommand === 'dev') {
|
|
97
|
+
runInProject(process.execPath, [path.join(PACKAGE_ROOT, 'scripts', 'non-cloudflare-server.mjs'), ...rest], { bootstrap: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (subcommand === 'dev:cf' || subcommand === 'dev-cf') {
|
|
101
|
+
runInProject(process.execPath, [path.join(PACKAGE_ROOT, 'scripts', 'dev-server.mjs'), ...rest], { bootstrap: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.error(`Unknown command: ${subcommand}\n`);
|
|
105
|
+
printHelp();
|
|
106
|
+
process.exit(1);
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "freertc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cloudflare Worker signaling relay for WebRTC peers with D1 storage.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"webrtc",
|
|
7
|
+
"signaling",
|
|
8
|
+
"cloudflare-workers",
|
|
9
|
+
"cloudflare-d1",
|
|
10
|
+
"websocket"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/draeder/freertc#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/draeder/freertc/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/draeder/freertc.git"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"freertc": "bin/freertc.mjs"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"README.md",
|
|
30
|
+
"bin/",
|
|
31
|
+
"public/",
|
|
32
|
+
"scripts/d1-schema.sql",
|
|
33
|
+
"scripts/dev-server.mjs",
|
|
34
|
+
"scripts/non-cloudflare-server.mjs",
|
|
35
|
+
"scripts/postinstall-message.mjs",
|
|
36
|
+
"scripts/project-bootstrap.mjs",
|
|
37
|
+
"scripts/wrangler-install-wizard.mjs",
|
|
38
|
+
"src/index.js",
|
|
39
|
+
"wrangler.template.jsonc",
|
|
40
|
+
"wrangler.workers-dev.jsonc"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "worker-build --release",
|
|
44
|
+
"dev": "node scripts/non-cloudflare-server.mjs",
|
|
45
|
+
"dev:cf": "node scripts/dev-server.mjs",
|
|
46
|
+
"dev:node": "node scripts/non-cloudflare-server.mjs",
|
|
47
|
+
"dev:local": "npm run dev:node",
|
|
48
|
+
"deploy": "wrangler deploy --env production",
|
|
49
|
+
"deploy:raw": "wrangler deploy",
|
|
50
|
+
"check": "cargo check --target wasm32-unknown-unknown",
|
|
51
|
+
"d1:init:local": "wrangler d1 execute freertc-signal --local --file scripts/d1-schema.sql",
|
|
52
|
+
"d1:init:remote": "wrangler d1 execute freertc-signal --remote --file scripts/d1-schema.sql",
|
|
53
|
+
"postinstall": "node scripts/postinstall-message.mjs",
|
|
54
|
+
"wizard": "node scripts/wrangler-install-wizard.mjs",
|
|
55
|
+
"config:validate": "node scripts/validate-config.mjs",
|
|
56
|
+
"config:gen": "node scripts/generate-prod-config.mjs",
|
|
57
|
+
"dev:prod": "npm run config:validate && npm run config:gen && node scripts/prod-server.mjs",
|
|
58
|
+
"deploy:local-prod": "npm run config:validate && npm run config:gen && docker-compose -f docker-compose.prod.yml up -d",
|
|
59
|
+
"deploy:local-prod:logs": "docker-compose -f docker-compose.prod.yml logs -f",
|
|
60
|
+
"deploy:local-prod:stop": "docker-compose -f docker-compose.prod.yml down"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"ws": "^8.18.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"wrangler": "^3.114.0"
|
|
67
|
+
}
|
|
68
|
+
}
|