session-collab-mcp 0.3.0 → 0.4.2
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/package.json +2 -6
- package/src/auth/handlers.ts +1 -1
- package/src/auth/middleware.ts +1 -1
- package/src/auth/password.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/db/auth-queries.ts +1 -1
- package/src/db/queries.ts +1 -1
- package/src/mcp/server.ts +35 -2
- package/src/mcp/tools/claim.ts +1 -1
- package/src/mcp/tools/decision.ts +1 -1
- package/src/mcp/tools/message.ts +1 -1
- package/src/mcp/tools/session.ts +1 -1
- package/src/tokens/handlers.ts +1 -1
- package/src/frontend/app.ts +0 -1181
- package/src/index.ts +0 -438
package/src/index.ts
DELETED
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
// Session Collaboration MCP - Cloudflare Worker Entry Point
|
|
2
|
-
// Implements MCP SSE transport for Claude Code integration
|
|
3
|
-
|
|
4
|
-
import type { D1Database } from '@cloudflare/workers-types';
|
|
5
|
-
import { McpServer, parseRequest } from './mcp/server';
|
|
6
|
-
import { createErrorResponse, MCP_ERROR_CODES, type JsonRpcResponse } from './mcp/protocol';
|
|
7
|
-
import { validateAuth, unauthorizedResponse, type Env as AuthEnv } from './auth/middleware';
|
|
8
|
-
import type { AuthContext } from './auth/types';
|
|
9
|
-
import {
|
|
10
|
-
handleRegister,
|
|
11
|
-
handleLogin,
|
|
12
|
-
handleRefresh,
|
|
13
|
-
handleLogout,
|
|
14
|
-
handleGetMe,
|
|
15
|
-
handleUpdateMe,
|
|
16
|
-
handleChangePassword,
|
|
17
|
-
} from './auth/handlers';
|
|
18
|
-
import { handleCreateToken, handleListTokens, handleRevokeToken } from './tokens/handlers';
|
|
19
|
-
import { generateAppHtml } from './frontend/app';
|
|
20
|
-
|
|
21
|
-
export interface Env extends AuthEnv {
|
|
22
|
-
DB: D1Database;
|
|
23
|
-
API_TOKEN?: string;
|
|
24
|
-
JWT_SECRET?: string;
|
|
25
|
-
ENVIRONMENT: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// CORS headers for cross-origin requests
|
|
29
|
-
const corsHeaders = {
|
|
30
|
-
'Access-Control-Allow-Origin': '*',
|
|
31
|
-
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
32
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// Handle MCP SSE endpoint
|
|
36
|
-
async function handleMcpRequest(request: Request, env: Env, authContext: AuthContext | null): Promise<Response> {
|
|
37
|
-
// Validate auth - require authentication if JWT_SECRET or API_TOKEN is configured
|
|
38
|
-
if ((env.JWT_SECRET || env.API_TOKEN) && !authContext) {
|
|
39
|
-
return unauthorizedResponse();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const server = new McpServer(env.DB, authContext ?? undefined);
|
|
43
|
-
|
|
44
|
-
// Handle SSE GET request (for establishing connection)
|
|
45
|
-
if (request.method === 'GET') {
|
|
46
|
-
return new Response(
|
|
47
|
-
JSON.stringify({
|
|
48
|
-
type: 'sse',
|
|
49
|
-
endpoint: '/mcp',
|
|
50
|
-
description: 'Session Collaboration MCP Server',
|
|
51
|
-
instructions: 'POST JSON-RPC requests to this endpoint',
|
|
52
|
-
}),
|
|
53
|
-
{
|
|
54
|
-
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
55
|
-
}
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Handle POST request (MCP message)
|
|
60
|
-
if (request.method === 'POST') {
|
|
61
|
-
const body = await request.text();
|
|
62
|
-
const jsonRpcRequest = parseRequest(body);
|
|
63
|
-
|
|
64
|
-
let response: JsonRpcResponse;
|
|
65
|
-
|
|
66
|
-
if (!jsonRpcRequest) {
|
|
67
|
-
response = createErrorResponse(undefined, MCP_ERROR_CODES.PARSE_ERROR, 'Invalid JSON-RPC request');
|
|
68
|
-
} else {
|
|
69
|
-
response = await server.handleRequest(jsonRpcRequest);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return new Response(JSON.stringify(response), {
|
|
73
|
-
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return new Response('Method not allowed', { status: 405 });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Handle auth routes
|
|
81
|
-
async function handleAuthRoute(request: Request, env: Env, pathname: string, authContext: AuthContext | null): Promise<Response> {
|
|
82
|
-
const ctx = { db: env.DB, jwtSecret: env.JWT_SECRET ?? '', request };
|
|
83
|
-
|
|
84
|
-
// Public auth routes (no auth required)
|
|
85
|
-
if (request.method === 'POST') {
|
|
86
|
-
if (pathname === '/auth/register') {
|
|
87
|
-
return handleRegister(ctx);
|
|
88
|
-
}
|
|
89
|
-
if (pathname === '/auth/login') {
|
|
90
|
-
return handleLogin(ctx);
|
|
91
|
-
}
|
|
92
|
-
if (pathname === '/auth/refresh') {
|
|
93
|
-
return handleRefresh(ctx);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Protected auth routes (auth required)
|
|
98
|
-
if (!authContext) {
|
|
99
|
-
return unauthorizedResponse();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (request.method === 'POST' && pathname === '/auth/logout') {
|
|
103
|
-
return handleLogout(ctx, authContext);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (pathname === '/auth/me') {
|
|
107
|
-
if (request.method === 'GET') {
|
|
108
|
-
return handleGetMe(ctx, authContext);
|
|
109
|
-
}
|
|
110
|
-
if (request.method === 'PUT') {
|
|
111
|
-
return handleUpdateMe(ctx, authContext);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (request.method === 'PUT' && pathname === '/auth/password') {
|
|
116
|
-
return handleChangePassword(ctx, authContext);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return new Response('Not found', { status: 404 });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Handle token routes
|
|
123
|
-
async function handleTokenRoute(request: Request, env: Env, pathname: string, authContext: AuthContext | null): Promise<Response> {
|
|
124
|
-
if (!authContext) {
|
|
125
|
-
return unauthorizedResponse();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const ctx = { db: env.DB, request };
|
|
129
|
-
|
|
130
|
-
// POST /tokens - create token
|
|
131
|
-
if (request.method === 'POST' && pathname === '/tokens') {
|
|
132
|
-
return handleCreateToken(ctx, authContext);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// GET /tokens - list tokens
|
|
136
|
-
if (request.method === 'GET' && pathname === '/tokens') {
|
|
137
|
-
return handleListTokens(ctx, authContext);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// DELETE /tokens/:id - revoke token
|
|
141
|
-
const deleteMatch = pathname.match(/^\/tokens\/([a-f0-9-]+)$/);
|
|
142
|
-
if (request.method === 'DELETE' && deleteMatch) {
|
|
143
|
-
const tokenId = deleteMatch[1];
|
|
144
|
-
return handleRevokeToken(ctx, authContext, tokenId);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return new Response('Not found', { status: 404 });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Health check endpoint (JSON)
|
|
151
|
-
function handleHealthCheck(env: Env): Response {
|
|
152
|
-
return new Response(
|
|
153
|
-
JSON.stringify({
|
|
154
|
-
status: 'healthy',
|
|
155
|
-
service: 'session-collab-mcp',
|
|
156
|
-
version: '0.2.0',
|
|
157
|
-
environment: env.ENVIRONMENT,
|
|
158
|
-
auth_enabled: !!(env.JWT_SECRET || env.API_TOKEN),
|
|
159
|
-
}),
|
|
160
|
-
{
|
|
161
|
-
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
162
|
-
}
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Homepage - MCP service info and quick start
|
|
167
|
-
function handleHomepage(env: Env, request: Request): Response {
|
|
168
|
-
const url = new URL(request.url);
|
|
169
|
-
const origin = url.origin;
|
|
170
|
-
const html = `<!DOCTYPE html>
|
|
171
|
-
<html lang="en">
|
|
172
|
-
<head>
|
|
173
|
-
<meta charset="UTF-8">
|
|
174
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
175
|
-
<title>Session Collab MCP</title>
|
|
176
|
-
<style>
|
|
177
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
178
|
-
body {
|
|
179
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
180
|
-
background: #0f172a;
|
|
181
|
-
color: #e2e8f0;
|
|
182
|
-
min-height: 100vh;
|
|
183
|
-
padding: 2rem;
|
|
184
|
-
}
|
|
185
|
-
.container { max-width: 720px; margin: 0 auto; }
|
|
186
|
-
header { text-align: center; margin-bottom: 2rem; }
|
|
187
|
-
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.5rem; }
|
|
188
|
-
.status {
|
|
189
|
-
display: inline-flex;
|
|
190
|
-
align-items: center;
|
|
191
|
-
gap: 0.5rem;
|
|
192
|
-
background: rgba(34, 197, 94, 0.1);
|
|
193
|
-
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
194
|
-
color: #22c55e;
|
|
195
|
-
padding: 0.25rem 0.75rem;
|
|
196
|
-
border-radius: 9999px;
|
|
197
|
-
font-size: 0.75rem;
|
|
198
|
-
}
|
|
199
|
-
.dot {
|
|
200
|
-
width: 6px; height: 6px;
|
|
201
|
-
background: #22c55e;
|
|
202
|
-
border-radius: 50%;
|
|
203
|
-
}
|
|
204
|
-
.version { color: #64748b; font-size: 0.75rem; margin-top: 0.5rem; }
|
|
205
|
-
.card {
|
|
206
|
-
background: rgba(255,255,255,0.03);
|
|
207
|
-
border: 1px solid rgba(255,255,255,0.08);
|
|
208
|
-
border-radius: 0.75rem;
|
|
209
|
-
padding: 1.25rem;
|
|
210
|
-
margin-bottom: 1rem;
|
|
211
|
-
}
|
|
212
|
-
h2 { font-size: 1rem; color: #94a3b8; margin-bottom: 1rem; font-weight: 500; }
|
|
213
|
-
.tools {
|
|
214
|
-
display: grid;
|
|
215
|
-
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
216
|
-
gap: 0.5rem;
|
|
217
|
-
}
|
|
218
|
-
.tool {
|
|
219
|
-
background: rgba(99, 102, 241, 0.1);
|
|
220
|
-
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
221
|
-
border-radius: 0.5rem;
|
|
222
|
-
padding: 0.5rem 0.75rem;
|
|
223
|
-
font-size: 0.8rem;
|
|
224
|
-
}
|
|
225
|
-
.tool code { color: #818cf8; }
|
|
226
|
-
.tool span { color: #64748b; font-size: 0.7rem; display: block; margin-top: 0.125rem; }
|
|
227
|
-
.step { display: flex; gap: 0.75rem; margin-bottom: 1rem; }
|
|
228
|
-
.step:last-child { margin-bottom: 0; }
|
|
229
|
-
.num {
|
|
230
|
-
flex-shrink: 0;
|
|
231
|
-
width: 1.5rem; height: 1.5rem;
|
|
232
|
-
background: #3b82f6;
|
|
233
|
-
border-radius: 50%;
|
|
234
|
-
display: flex;
|
|
235
|
-
align-items: center;
|
|
236
|
-
justify-content: center;
|
|
237
|
-
font-size: 0.75rem;
|
|
238
|
-
font-weight: 600;
|
|
239
|
-
}
|
|
240
|
-
.step-content { flex: 1; }
|
|
241
|
-
.step-content h3 { font-size: 0.875rem; margin-bottom: 0.375rem; font-weight: 500; }
|
|
242
|
-
pre {
|
|
243
|
-
background: #020617;
|
|
244
|
-
border: 1px solid rgba(255,255,255,0.08);
|
|
245
|
-
border-radius: 0.375rem;
|
|
246
|
-
padding: 0.75rem;
|
|
247
|
-
font-size: 0.7rem;
|
|
248
|
-
overflow-x: auto;
|
|
249
|
-
line-height: 1.5;
|
|
250
|
-
}
|
|
251
|
-
code { color: #7dd3fc; }
|
|
252
|
-
.copy-btn {
|
|
253
|
-
background: rgba(59, 130, 246, 0.2);
|
|
254
|
-
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
255
|
-
color: #60a5fa;
|
|
256
|
-
padding: 0.2rem 0.5rem;
|
|
257
|
-
border-radius: 0.25rem;
|
|
258
|
-
cursor: pointer;
|
|
259
|
-
font-size: 0.65rem;
|
|
260
|
-
margin-top: 0.375rem;
|
|
261
|
-
}
|
|
262
|
-
.copy-btn:hover { background: rgba(59, 130, 246, 0.3); }
|
|
263
|
-
footer { text-align: center; color: #475569; font-size: 0.7rem; margin-top: 1.5rem; }
|
|
264
|
-
.login-btn {
|
|
265
|
-
display: inline-block;
|
|
266
|
-
margin-top: 1rem;
|
|
267
|
-
padding: 0.5rem 1.5rem;
|
|
268
|
-
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
|
269
|
-
color: #fff;
|
|
270
|
-
text-decoration: none;
|
|
271
|
-
border-radius: 0.5rem;
|
|
272
|
-
font-size: 0.875rem;
|
|
273
|
-
font-weight: 500;
|
|
274
|
-
transition: opacity 0.2s;
|
|
275
|
-
}
|
|
276
|
-
.login-btn:hover { opacity: 0.9; }
|
|
277
|
-
</style>
|
|
278
|
-
</head>
|
|
279
|
-
<body>
|
|
280
|
-
<div class="container">
|
|
281
|
-
<header>
|
|
282
|
-
<h1>Session Collab MCP</h1>
|
|
283
|
-
<div class="status"><span class="dot"></span>Operational</div>
|
|
284
|
-
<p class="version">v0.2.0 · ${env.ENVIRONMENT}</p>
|
|
285
|
-
<a href="/app" class="login-btn">Login</a>
|
|
286
|
-
</header>
|
|
287
|
-
|
|
288
|
-
<div class="card">
|
|
289
|
-
<h2>Quick Start</h2>
|
|
290
|
-
|
|
291
|
-
<div class="step">
|
|
292
|
-
<div class="num">1</div>
|
|
293
|
-
<div class="step-content">
|
|
294
|
-
<h3>Register & Login</h3>
|
|
295
|
-
<pre id="s1">curl -X POST ${origin}/auth/register -H "Content-Type: application/json" \\
|
|
296
|
-
-d '{"email": "you@example.com", "password": "YourPass123"}'</pre>
|
|
297
|
-
<button class="copy-btn" onclick="copy('s1')">Copy</button>
|
|
298
|
-
</div>
|
|
299
|
-
</div>
|
|
300
|
-
|
|
301
|
-
<div class="step">
|
|
302
|
-
<div class="num">2</div>
|
|
303
|
-
<div class="step-content">
|
|
304
|
-
<h3>Create API Token</h3>
|
|
305
|
-
<pre id="s2">curl -X POST ${origin}/tokens -H "Content-Type: application/json" \\
|
|
306
|
-
-H "Authorization: Bearer ACCESS_TOKEN" -d '{"name": "My Machine"}'</pre>
|
|
307
|
-
<button class="copy-btn" onclick="copy('s2')">Copy</button>
|
|
308
|
-
</div>
|
|
309
|
-
</div>
|
|
310
|
-
|
|
311
|
-
<div class="step">
|
|
312
|
-
<div class="num">3</div>
|
|
313
|
-
<div class="step-content">
|
|
314
|
-
<h3>Configure Claude Code</h3>
|
|
315
|
-
<pre id="s3">{
|
|
316
|
-
"mcpServers": {
|
|
317
|
-
"session-collab": {
|
|
318
|
-
"type": "http",
|
|
319
|
-
"url": "${origin}/mcp",
|
|
320
|
-
"headers": {
|
|
321
|
-
"Authorization": "Bearer mcp_YOUR_TOKEN"
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}</pre>
|
|
326
|
-
<button class="copy-btn" onclick="copy('s3')">Copy</button>
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
</div>
|
|
330
|
-
|
|
331
|
-
<div class="card">
|
|
332
|
-
<h2>MCP Tools</h2>
|
|
333
|
-
<div class="tools">
|
|
334
|
-
<div class="tool"><code>collab_session_start</code><span>Start a session</span></div>
|
|
335
|
-
<div class="tool"><code>collab_session_end</code><span>End a session</span></div>
|
|
336
|
-
<div class="tool"><code>collab_session_list</code><span>List sessions</span></div>
|
|
337
|
-
<div class="tool"><code>collab_session_heartbeat</code><span>Update heartbeat</span></div>
|
|
338
|
-
<div class="tool"><code>collab_claim</code><span>Claim files</span></div>
|
|
339
|
-
<div class="tool"><code>collab_check</code><span>Check conflicts</span></div>
|
|
340
|
-
<div class="tool"><code>collab_release</code><span>Release claim</span></div>
|
|
341
|
-
<div class="tool"><code>collab_claims_list</code><span>List all claims</span></div>
|
|
342
|
-
<div class="tool"><code>collab_message_send</code><span>Send message</span></div>
|
|
343
|
-
<div class="tool"><code>collab_message_list</code><span>Read messages</span></div>
|
|
344
|
-
<div class="tool"><code>collab_decision_add</code><span>Record decision</span></div>
|
|
345
|
-
<div class="tool"><code>collab_decision_list</code><span>List decisions</span></div>
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
348
|
-
|
|
349
|
-
<footer>Powered by Cloudflare Workers + D1</footer>
|
|
350
|
-
</div>
|
|
351
|
-
<script>
|
|
352
|
-
function copy(id) {
|
|
353
|
-
navigator.clipboard.writeText(document.getElementById(id).textContent);
|
|
354
|
-
event.target.textContent = 'Copied!';
|
|
355
|
-
setTimeout(() => event.target.textContent = 'Copy', 1500);
|
|
356
|
-
}
|
|
357
|
-
</script>
|
|
358
|
-
</body>
|
|
359
|
-
</html>`;
|
|
360
|
-
|
|
361
|
-
return new Response(html, {
|
|
362
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8', ...corsHeaders },
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Main request handler
|
|
367
|
-
export default {
|
|
368
|
-
async fetch(request: Request, env: Env): Promise<Response> {
|
|
369
|
-
const url = new URL(request.url);
|
|
370
|
-
|
|
371
|
-
// Handle CORS preflight
|
|
372
|
-
if (request.method === 'OPTIONS') {
|
|
373
|
-
return new Response(null, {
|
|
374
|
-
status: 204,
|
|
375
|
-
headers: corsHeaders,
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Validate auth once for all routes
|
|
380
|
-
const authContext = await validateAuth(request, env);
|
|
381
|
-
|
|
382
|
-
// Route requests
|
|
383
|
-
const pathname = url.pathname;
|
|
384
|
-
|
|
385
|
-
// Homepage
|
|
386
|
-
if (pathname === '/') {
|
|
387
|
-
return handleHomepage(env, request);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Dashboard App
|
|
391
|
-
if (pathname === '/app' || pathname === '/dashboard') {
|
|
392
|
-
const html = generateAppHtml(url.origin);
|
|
393
|
-
return new Response(html, {
|
|
394
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8', ...corsHeaders },
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Health check
|
|
399
|
-
if (pathname === '/health') {
|
|
400
|
-
return handleHealthCheck(env);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Auth routes
|
|
404
|
-
if (pathname.startsWith('/auth/')) {
|
|
405
|
-
return handleAuthRoute(request, env, pathname, authContext);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Token routes
|
|
409
|
-
if (pathname.startsWith('/tokens')) {
|
|
410
|
-
return handleTokenRoute(request, env, pathname, authContext);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// MCP routes
|
|
414
|
-
if (pathname === '/mcp' || pathname === '/mcp/' || pathname === '/sse' || pathname === '/sse/') {
|
|
415
|
-
return handleMcpRequest(request, env, authContext);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Handle OAuth discovery - indicate this server uses Bearer token auth, not OAuth
|
|
419
|
-
if (pathname === '/.well-known/oauth-authorization-server') {
|
|
420
|
-
return new Response(
|
|
421
|
-
JSON.stringify({
|
|
422
|
-
error: 'oauth_not_supported',
|
|
423
|
-
error_description: 'This server uses Bearer token authentication, not OAuth. Include your API token in the Authorization header.',
|
|
424
|
-
}),
|
|
425
|
-
{
|
|
426
|
-
status: 404,
|
|
427
|
-
headers: { 'Content-Type': 'application/json', ...corsHeaders },
|
|
428
|
-
}
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Return JSON 404 for all other routes
|
|
433
|
-
return new Response(
|
|
434
|
-
JSON.stringify({ error: 'not_found', message: 'The requested resource was not found' }),
|
|
435
|
-
{ status: 404, headers: { 'Content-Type': 'application/json', ...corsHeaders } }
|
|
436
|
-
);
|
|
437
|
-
},
|
|
438
|
-
};
|