codex-grok-mcp 0.2.0-beta.1
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/CONTRIBUTING.md +67 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +109 -0
- package/dist/bridge-companion.d.ts +61 -0
- package/dist/bridge-companion.js +436 -0
- package/dist/bridge-companion.js.map +1 -0
- package/dist/bridge-pairing.d.ts +30 -0
- package/dist/bridge-pairing.js +364 -0
- package/dist/bridge-pairing.js.map +1 -0
- package/dist/bridge-protocol.d.ts +187 -0
- package/dist/bridge-protocol.js +360 -0
- package/dist/bridge-protocol.js.map +1 -0
- package/dist/bridge-replay.d.ts +10 -0
- package/dist/bridge-replay.js +107 -0
- package/dist/bridge-replay.js.map +1 -0
- package/dist/bridge-runtime.d.ts +11 -0
- package/dist/bridge-runtime.js +259 -0
- package/dist/bridge-runtime.js.map +1 -0
- package/dist/direct-gateway-transport.d.ts +7 -0
- package/dist/direct-gateway-transport.js +241 -0
- package/dist/direct-gateway-transport.js.map +1 -0
- package/dist/grok-bot-client.d.ts +93 -0
- package/dist/grok-bot-client.js +475 -0
- package/dist/grok-bot-client.js.map +1 -0
- package/dist/grok-bot-gateway.d.ts +204 -0
- package/dist/grok-bot-gateway.js +741 -0
- package/dist/grok-bot-gateway.js.map +1 -0
- package/dist/grok-cli.d.ts +31 -0
- package/dist/grok-cli.js +338 -0
- package/dist/grok-cli.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +228 -0
- package/dist/index.js.map +1 -0
- package/dist/relay-transport.d.ts +9 -0
- package/dist/relay-transport.js +278 -0
- package/dist/relay-transport.js.map +1 -0
- package/dist/schema.d.ts +16 -0
- package/dist/schema.js +22 -0
- package/dist/schema.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/package.json +71 -0
- package/plugins/codex-grok-mcp/assets/icon.png +0 -0
- package/relay/.dev.vars.example +2 -0
- package/relay/README.md +26 -0
- package/relay/package-lock.json +3291 -0
- package/relay/package.json +17 -0
- package/relay/src/index.ts +182 -0
- package/relay/test/relay.test.ts +127 -0
- package/relay/test/tsconfig.json +13 -0
- package/relay/tsconfig.json +15 -0
- package/relay/vitest.config.ts +15 -0
- package/relay/worker-configuration.d.ts +15362 -0
- package/relay/wrangler.jsonc +33 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codex-grok-mcp/relay",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"check": "tsc --noEmit && tsc --noEmit -p test/tsconfig.json",
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"types": "wrangler types"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@cloudflare/vitest-plugin": "1.1.3",
|
|
13
|
+
"typescript": "7.0.2",
|
|
14
|
+
"vitest": "4.1.11",
|
|
15
|
+
"wrangler": "4.128.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
|
|
3
|
+
const CHANNEL_PATTERN = /^[A-Za-z0-9_-]{21}[AQgw]$/;
|
|
4
|
+
const MAX_FRAME_BYTES = 128 * 1024;
|
|
5
|
+
const INVALID_FRAME_CLOSE_CODE = 4400;
|
|
6
|
+
const PEER_UNAVAILABLE_CLOSE_CODE = 4404;
|
|
7
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
8
|
+
|
|
9
|
+
type Role = "codex" | "bridge";
|
|
10
|
+
type RelayEnv = Env & { RELAY_ACCESS_TOKEN: string };
|
|
11
|
+
|
|
12
|
+
function isRole(value: string | null): value is Role {
|
|
13
|
+
return value === "codex" || value === "bridge";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function close(socket: WebSocket, code: number, reason: string): void {
|
|
17
|
+
try {
|
|
18
|
+
socket.close(code, reason);
|
|
19
|
+
} catch {
|
|
20
|
+
// The peer may already be disconnected.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function frameSize(message: string | ArrayBuffer): number {
|
|
25
|
+
return typeof message === "string"
|
|
26
|
+
? TEXT_ENCODER.encode(message).byteLength
|
|
27
|
+
: message.byteLength;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function encodeBase64Url(bytes: Uint8Array): string {
|
|
31
|
+
return btoa(String.fromCharCode(...bytes))
|
|
32
|
+
.replaceAll("+", "-")
|
|
33
|
+
.replaceAll("/", "_")
|
|
34
|
+
.replace(/=+$/, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function decodeMasterToken(value: unknown): Uint8Array | undefined {
|
|
38
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value)) return undefined;
|
|
39
|
+
try {
|
|
40
|
+
const padded = `${value.replaceAll("-", "+").replaceAll("_", "/")}${"=".repeat(
|
|
41
|
+
(4 - (value.length % 4)) % 4,
|
|
42
|
+
)}`;
|
|
43
|
+
const bytes = Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
|
|
44
|
+
return bytes.byteLength === 32 && encodeBase64Url(bytes) === value ? bytes : undefined;
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function channelAccessToken(master: Uint8Array, channel: string): Promise<string> {
|
|
51
|
+
const key = await crypto.subtle.importKey(
|
|
52
|
+
"raw",
|
|
53
|
+
master,
|
|
54
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
55
|
+
false,
|
|
56
|
+
["sign"],
|
|
57
|
+
);
|
|
58
|
+
const signature = await crypto.subtle.sign(
|
|
59
|
+
"HMAC",
|
|
60
|
+
key,
|
|
61
|
+
TEXT_ENCODER.encode(`codex-grok-mcp-relay:v1\0${channel}`),
|
|
62
|
+
);
|
|
63
|
+
return encodeBase64Url(new Uint8Array(signature));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function isAuthorized(
|
|
67
|
+
request: Request,
|
|
68
|
+
master: Uint8Array,
|
|
69
|
+
channel: string,
|
|
70
|
+
): Promise<boolean> {
|
|
71
|
+
const accessToken = await channelAccessToken(master, channel);
|
|
72
|
+
const [provided, expected] = await Promise.all([
|
|
73
|
+
crypto.subtle.digest("SHA-256", TEXT_ENCODER.encode(request.headers.get("Authorization") ?? "")),
|
|
74
|
+
crypto.subtle.digest("SHA-256", TEXT_ENCODER.encode(`Bearer ${accessToken}`)),
|
|
75
|
+
]);
|
|
76
|
+
return crypto.subtle.timingSafeEqual(provided, expected);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class Relay extends DurableObject<RelayEnv> {
|
|
80
|
+
fetch(request: Request): Response {
|
|
81
|
+
const role = new URL(request.url).searchParams.get("role");
|
|
82
|
+
if (!isRole(role) || request.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
|
|
83
|
+
return new Response(null, { status: 400 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const socket of this.ctx.getWebSockets(role)) {
|
|
87
|
+
close(socket, 4000, "replaced");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const [client, server] = Object.values(new WebSocketPair());
|
|
91
|
+
this.ctx.acceptWebSocket(server, [role]);
|
|
92
|
+
server.serializeAttachment(role);
|
|
93
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
webSocketMessage(socket: WebSocket, message: string | ArrayBuffer): void {
|
|
97
|
+
const role: unknown = socket.deserializeAttachment();
|
|
98
|
+
if (!isRole(typeof role === "string" ? role : null)) {
|
|
99
|
+
close(socket, 1011, "invalid connection state");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const peerRole: Role = role === "codex" ? "bridge" : "codex";
|
|
104
|
+
const peer = this.ctx
|
|
105
|
+
.getWebSockets(peerRole)
|
|
106
|
+
.find((candidate) => candidate.readyState === WebSocket.OPEN);
|
|
107
|
+
|
|
108
|
+
if (!peer) {
|
|
109
|
+
close(socket, PEER_UNAVAILABLE_CLOSE_CODE, "peer unavailable");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (frameSize(message) > MAX_FRAME_BYTES) {
|
|
114
|
+
close(socket, 1009, "frame too large");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
peer.send(message);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
webSocketClose(socket: WebSocket, code: number, _reason: string, _wasClean: boolean): void {
|
|
122
|
+
const role: unknown = socket.deserializeAttachment();
|
|
123
|
+
if (role !== "bridge") return;
|
|
124
|
+
|
|
125
|
+
const peer = this.ctx
|
|
126
|
+
.getWebSockets("codex")
|
|
127
|
+
.find((candidate) => candidate.readyState === WebSocket.OPEN);
|
|
128
|
+
if (!peer) return;
|
|
129
|
+
|
|
130
|
+
const rejectedFrame = code === INVALID_FRAME_CLOSE_CODE;
|
|
131
|
+
close(
|
|
132
|
+
peer,
|
|
133
|
+
rejectedFrame ? INVALID_FRAME_CLOSE_CODE : PEER_UNAVAILABLE_CLOSE_CODE,
|
|
134
|
+
rejectedFrame ? "peer rejected frame" : "peer unavailable",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
webSocketError(socket: WebSocket, _error: unknown): void {
|
|
139
|
+
close(socket, 1011, "websocket error");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default {
|
|
144
|
+
async fetch(request: Request, env: RelayEnv): Promise<Response> {
|
|
145
|
+
if (request.headers.has("Origin")) {
|
|
146
|
+
return new Response(null, { status: 403 });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const url = new URL(request.url);
|
|
150
|
+
const match = /^\/v1\/connect\/([^/]+)$/.exec(url.pathname);
|
|
151
|
+
|
|
152
|
+
if (!match) {
|
|
153
|
+
return new Response(null, { status: 404 });
|
|
154
|
+
}
|
|
155
|
+
if (request.method !== "GET") {
|
|
156
|
+
return new Response(null, { status: 405 });
|
|
157
|
+
}
|
|
158
|
+
if (!CHANNEL_PATTERN.test(match[1])) {
|
|
159
|
+
return new Response(null, { status: 400 });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const roles = url.searchParams.getAll("role");
|
|
163
|
+
if (roles.length !== 1 || !isRole(roles[0])) {
|
|
164
|
+
return new Response(null, { status: 400 });
|
|
165
|
+
}
|
|
166
|
+
const master = decodeMasterToken(env.RELAY_ACCESS_TOKEN);
|
|
167
|
+
if (master === undefined) {
|
|
168
|
+
return new Response(null, { status: 503 });
|
|
169
|
+
}
|
|
170
|
+
if (!(await isAuthorized(request, master, match[1]))) {
|
|
171
|
+
return new Response(null, {
|
|
172
|
+
status: 401,
|
|
173
|
+
headers: { "WWW-Authenticate": 'Bearer realm="codex-grok-relay"' },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (request.headers.get("Upgrade")?.toLowerCase() !== "websocket") {
|
|
177
|
+
return new Response(null, { status: 426 });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return env.RELAY.getByName(match[1]).fetch(request);
|
|
181
|
+
},
|
|
182
|
+
} satisfies ExportedHandler<RelayEnv>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { env, exports } from "cloudflare:workers";
|
|
2
|
+
import { listDurableObjectIds } from "cloudflare:test";
|
|
3
|
+
import { expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
const CHANNEL = "AAAAAAAAAAAAAAAAAAAAAA";
|
|
6
|
+
const UNAVAILABLE_CHANNEL = "AAAAAAAAAAAAAAAAAAAAAQ";
|
|
7
|
+
const CAP_CHANNEL = "AAAAAAAAAAAAAAAAAAAABA";
|
|
8
|
+
const AUTHORIZATIONS: Record<string, string> = {
|
|
9
|
+
[CHANNEL]: "Bearer oCZGsv3_Wa-CwwkEYxnF9Zvl9nzxLC3mTUDx4R23H2E",
|
|
10
|
+
[UNAVAILABLE_CHANNEL]: "Bearer chjihmYNxBTJTqJG2OdkC2-BY78eR8KObXxeN1rmwQc",
|
|
11
|
+
[CAP_CHANNEL]: "Bearer NhP4BGrMM7x-D5Rf7GSLEj3uIJenewrPcW_Vv3JC02w",
|
|
12
|
+
};
|
|
13
|
+
const AUTHORIZATION = AUTHORIZATIONS[CHANNEL] ?? "";
|
|
14
|
+
|
|
15
|
+
function event(socket: WebSocket, type: "close"): Promise<CloseEvent>;
|
|
16
|
+
function event(socket: WebSocket, type: "message"): Promise<MessageEvent>;
|
|
17
|
+
function event(socket: WebSocket, type: "close" | "message"): Promise<CloseEvent | MessageEvent> {
|
|
18
|
+
return new Promise((resolve) => socket.addEventListener(type, resolve, { once: true }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function connect(channel: string, role: "codex" | "bridge"): Promise<WebSocket> {
|
|
22
|
+
const response = await exports.default.fetch(`https://relay.test/v1/connect/${channel}?role=${role}`, {
|
|
23
|
+
headers: { Authorization: AUTHORIZATIONS[channel] ?? "", Upgrade: "websocket" },
|
|
24
|
+
});
|
|
25
|
+
expect(response.status).toBe(101);
|
|
26
|
+
expect(response.webSocket).not.toBeNull();
|
|
27
|
+
const socket = response.webSocket;
|
|
28
|
+
if (!socket) throw new Error("missing WebSocket");
|
|
29
|
+
socket.accept();
|
|
30
|
+
socket.binaryType = "arraybuffer";
|
|
31
|
+
return socket;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it("rejects browser and unauthenticated requests before creating a Durable Object", async () => {
|
|
35
|
+
const url = `https://relay.test/v1/connect/${CHANNEL}?role=codex`;
|
|
36
|
+
|
|
37
|
+
const missing = await exports.default.fetch(url, { headers: { Upgrade: "websocket" } });
|
|
38
|
+
expect(missing.status).toBe(401);
|
|
39
|
+
expect(missing.headers.get("WWW-Authenticate")).toBe('Bearer realm="codex-grok-relay"');
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
(await exports.default.fetch(url, {
|
|
43
|
+
headers: { Authorization: "Bearer wrong-token", Upgrade: "websocket" },
|
|
44
|
+
})).status,
|
|
45
|
+
).toBe(401);
|
|
46
|
+
expect(
|
|
47
|
+
(await exports.default.fetch(url, {
|
|
48
|
+
headers: { Authorization: AUTHORIZATION, Origin: "https://example.test", Upgrade: "websocket" },
|
|
49
|
+
})).status,
|
|
50
|
+
).toBe(403);
|
|
51
|
+
expect(
|
|
52
|
+
(await exports.default.fetch(
|
|
53
|
+
`https://relay.test/v1/connect/${UNAVAILABLE_CHANNEL}?role=codex`,
|
|
54
|
+
{ headers: { Authorization: AUTHORIZATION, Upgrade: "websocket" } },
|
|
55
|
+
)).status,
|
|
56
|
+
).toBe(401);
|
|
57
|
+
expect(await listDurableObjectIds(env.RELAY)).toHaveLength(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("validates, routes opaque frames, caps frames, reports availability, and replaces roles", async () => {
|
|
61
|
+
expect((await exports.default.fetch("https://relay.test/nope")).status).toBe(404);
|
|
62
|
+
expect(
|
|
63
|
+
(await exports.default.fetch("https://relay.test/v1/connect/not-a-channel?role=codex", {
|
|
64
|
+
headers: { Authorization: AUTHORIZATION, Upgrade: "websocket" },
|
|
65
|
+
})).status,
|
|
66
|
+
).toBe(400);
|
|
67
|
+
expect(
|
|
68
|
+
(await exports.default.fetch("https://relay.test/v1/connect/AAAAAAAAAAAAAAAAAAAAAB?role=codex", {
|
|
69
|
+
headers: { Authorization: AUTHORIZATION, Upgrade: "websocket" },
|
|
70
|
+
})).status,
|
|
71
|
+
).toBe(400);
|
|
72
|
+
expect(
|
|
73
|
+
(await exports.default.fetch(`https://relay.test/v1/connect/${CHANNEL}?role=nope`, {
|
|
74
|
+
headers: { Authorization: AUTHORIZATION, Upgrade: "websocket" },
|
|
75
|
+
})).status,
|
|
76
|
+
).toBe(400);
|
|
77
|
+
expect(
|
|
78
|
+
(await exports.default.fetch(`https://relay.test/v1/connect/${CHANNEL}?role=codex`, {
|
|
79
|
+
headers: { Authorization: AUTHORIZATION },
|
|
80
|
+
})).status,
|
|
81
|
+
).toBe(426);
|
|
82
|
+
|
|
83
|
+
const codex = await connect(CHANNEL, "codex");
|
|
84
|
+
const bridge = await connect(CHANNEL, "bridge");
|
|
85
|
+
|
|
86
|
+
const stringMessage = event(bridge, "message");
|
|
87
|
+
codex.send("opaque");
|
|
88
|
+
expect((await stringMessage).data).toBe("opaque");
|
|
89
|
+
|
|
90
|
+
const binaryMessage = event(codex, "message");
|
|
91
|
+
bridge.send(new Uint8Array([1, 2, 3]).buffer);
|
|
92
|
+
expect(Array.from(new Uint8Array((await binaryMessage).data as ArrayBuffer))).toEqual([1, 2, 3]);
|
|
93
|
+
|
|
94
|
+
const replaced = event(codex, "close");
|
|
95
|
+
const replacement = await connect(CHANNEL, "codex");
|
|
96
|
+
expect((await replaced).code).toBe(4000);
|
|
97
|
+
|
|
98
|
+
const replacementMessage = event(replacement, "message");
|
|
99
|
+
bridge.send("new socket");
|
|
100
|
+
expect((await replacementMessage).data).toBe("new socket");
|
|
101
|
+
|
|
102
|
+
const unavailable = await connect(UNAVAILABLE_CHANNEL, "codex");
|
|
103
|
+
const unavailableClose = event(unavailable, "close");
|
|
104
|
+
unavailable.send(new Uint8Array(128 * 1024 + 1));
|
|
105
|
+
const unavailableEvent = await unavailableClose;
|
|
106
|
+
expect(unavailableEvent.code).toBe(4404);
|
|
107
|
+
expect(unavailableEvent.reason).toBe("peer unavailable");
|
|
108
|
+
|
|
109
|
+
const cappedSender = await connect(CAP_CHANNEL, "codex");
|
|
110
|
+
const cappedPeer = await connect(CAP_CHANNEL, "bridge");
|
|
111
|
+
const cappedClose = event(cappedSender, "close");
|
|
112
|
+
let oversizedForwarded = false;
|
|
113
|
+
cappedPeer.addEventListener("message", () => {
|
|
114
|
+
oversizedForwarded = true;
|
|
115
|
+
});
|
|
116
|
+
cappedSender.send(new Uint8Array(128 * 1024 + 1));
|
|
117
|
+
expect((await cappedClose).code).toBe(1009);
|
|
118
|
+
expect(oversizedForwarded).toBe(false);
|
|
119
|
+
|
|
120
|
+
const rejectedByCompanion = event(replacement, "close");
|
|
121
|
+
bridge.close(4400, "untrusted detail");
|
|
122
|
+
const rejection = await rejectedByCompanion;
|
|
123
|
+
expect(rejection.code).toBe(4400);
|
|
124
|
+
expect(rejection.reason).toBe("peer rejected frame");
|
|
125
|
+
|
|
126
|
+
cappedPeer.close(1000, "done");
|
|
127
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"moduleResolution": "Bundler",
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"types": ["@cloudflare/vitest-plugin/types"]
|
|
7
|
+
},
|
|
8
|
+
"include": [
|
|
9
|
+
"./**/*.ts",
|
|
10
|
+
"../vitest.config.ts",
|
|
11
|
+
"../worker-configuration.d.ts"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"types": ["./worker-configuration.d.ts"]
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.ts",
|
|
13
|
+
"worker-configuration.d.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cloudflareTest } from "@cloudflare/vitest-plugin";
|
|
2
|
+
import { defineConfig } from "vitest/config";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [
|
|
6
|
+
cloudflareTest({
|
|
7
|
+
miniflare: {
|
|
8
|
+
bindings: {
|
|
9
|
+
RELAY_ACCESS_TOKEN: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
wrangler: { configPath: "./wrangler.jsonc" },
|
|
13
|
+
}),
|
|
14
|
+
],
|
|
15
|
+
});
|