@gilhrpenner/convex-files-control 0.1.1 → 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 +243 -207
- package/dist/client/http.d.ts +4 -4
- package/dist/client/http.d.ts.map +1 -1
- package/dist/client/http.js +39 -10
- package/dist/client/http.js.map +1 -1
- package/dist/client/index.d.ts +66 -11
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +136 -41
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +2 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/download.d.ts +2 -0
- package/dist/component/download.d.ts.map +1 -1
- package/dist/component/download.js +33 -12
- package/dist/component/download.js.map +1 -1
- package/dist/component/schema.d.ts +3 -1
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +1 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +10 -6
- package/dist/react/index.js.map +1 -1
- package/package.json +4 -3
- package/src/__tests__/client-extra.test.ts +157 -4
- package/src/__tests__/client.test.ts +572 -46
- package/src/__tests__/download-core.test.ts +70 -0
- package/src/__tests__/entrypoints.test.ts +13 -0
- package/src/__tests__/http.test.ts +34 -0
- package/src/__tests__/react.test.ts +10 -16
- package/src/__tests__/shared.test.ts +0 -5
- package/src/__tests__/transfer.test.ts +103 -0
- package/src/client/http.ts +51 -10
- package/src/client/index.ts +242 -51
- package/src/component/_generated/component.ts +2 -0
- package/src/component/download.ts +35 -14
- package/src/component/schema.ts +1 -0
- package/src/react/index.ts +11 -10
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const findFileByStorageIdMock = vi.fn();
|
|
4
|
+
|
|
5
|
+
vi.mock("../component/lib.js", async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import("../component/lib.js")>();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
findFileByStorageId: findFileByStorageIdMock,
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function getHandler(fn: unknown) {
|
|
14
|
+
return (fn as { _handler: (...args: any[]) => any })._handler;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function makeCtx(grantOverrides: Record<string, unknown> = {}) {
|
|
18
|
+
return {
|
|
19
|
+
db: {
|
|
20
|
+
get: vi.fn(async () => ({
|
|
21
|
+
_id: "grant",
|
|
22
|
+
storageId: "storage",
|
|
23
|
+
maxUses: null,
|
|
24
|
+
useCount: 0,
|
|
25
|
+
shareableLink: true,
|
|
26
|
+
...grantOverrides,
|
|
27
|
+
})),
|
|
28
|
+
delete: vi.fn(async () => undefined),
|
|
29
|
+
},
|
|
30
|
+
} as any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("consumeDownloadGrantForUrl shareable link edge cases", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
findFileByStorageIdMock.mockReset();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("deletes grant when shareable link file is missing", async () => {
|
|
39
|
+
const { consumeDownloadGrantForUrl } = await import("../component/download.js");
|
|
40
|
+
const ctx = makeCtx();
|
|
41
|
+
findFileByStorageIdMock.mockResolvedValueOnce(null);
|
|
42
|
+
|
|
43
|
+
const result = await getHandler(consumeDownloadGrantForUrl)(ctx, {
|
|
44
|
+
downloadToken: "grant",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result).toEqual({ status: "file_missing" });
|
|
48
|
+
expect(ctx.db.delete).toHaveBeenCalledWith("grant");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("handles missing file after initial shareable check", async () => {
|
|
52
|
+
const { consumeDownloadGrantForUrl } = await import("../component/download.js");
|
|
53
|
+
const ctx = makeCtx();
|
|
54
|
+
let calls = 0;
|
|
55
|
+
const thenable = {
|
|
56
|
+
then: (resolve: (value: any) => void) => {
|
|
57
|
+
calls += 1;
|
|
58
|
+
resolve(calls === 1 ? { storageProvider: "convex" } : null);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
findFileByStorageIdMock.mockReturnValueOnce(thenable as any);
|
|
62
|
+
|
|
63
|
+
const result = await getHandler(consumeDownloadGrantForUrl)(ctx, {
|
|
64
|
+
downloadToken: "grant",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result).toEqual({ status: "file_missing" });
|
|
68
|
+
expect(ctx.db.delete).toHaveBeenCalledWith("grant");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("entrypoint modules", () => {
|
|
4
|
+
test("shared entrypoint and schema load from source", async () => {
|
|
5
|
+
const shared = await import(new URL("../shared.ts", import.meta.url).href);
|
|
6
|
+
expect(shared.DEFAULT_PATH_PREFIX).toBe("/files");
|
|
7
|
+
|
|
8
|
+
const schemaModule = await import(
|
|
9
|
+
new URL("../component/schema.ts", import.meta.url).href
|
|
10
|
+
);
|
|
11
|
+
expect(schemaModule.default).toBeTruthy();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { corsHeaders, parseJsonStringArray } from "../client/http.js";
|
|
3
|
+
|
|
4
|
+
describe("http helpers", () => {
|
|
5
|
+
test("corsHeaders sets credentials for specific origins", () => {
|
|
6
|
+
const headers = corsHeaders("https://example.com");
|
|
7
|
+
expect(headers.get("Access-Control-Allow-Origin")).toBe("https://example.com");
|
|
8
|
+
expect(headers.get("Access-Control-Allow-Credentials")).toBe("true");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("corsHeaders includes extra allow headers", () => {
|
|
12
|
+
const headers = corsHeaders(undefined, ["X-Extra"]);
|
|
13
|
+
expect(headers.get("Access-Control-Allow-Headers")).toContain("X-Extra");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("corsHeaders trims and deduplicates allow headers", () => {
|
|
17
|
+
const headers = corsHeaders(undefined, ["", " Authorization ", "authorization"]);
|
|
18
|
+
const allow = headers.get("Access-Control-Allow-Headers") ?? "";
|
|
19
|
+
const matches = allow.split(", ").filter((value) => value === "Authorization");
|
|
20
|
+
expect(matches).toHaveLength(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("parseJsonStringArray returns string arrays", () => {
|
|
24
|
+
expect(parseJsonStringArray('["a","b"]')).toEqual(["a", "b"]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("parseJsonStringArray rejects non-string arrays", () => {
|
|
28
|
+
expect(parseJsonStringArray('["a", 1]')).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("parseJsonStringArray returns null for invalid JSON", () => {
|
|
32
|
+
expect(parseJsonStringArray("not-json")).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -48,7 +48,6 @@ describe("useUploadFile", () => {
|
|
|
48
48
|
const file = new File(["data"], "file.txt", { type: "text/plain" });
|
|
49
49
|
const result = await uploadViaPresignedUrl({
|
|
50
50
|
file,
|
|
51
|
-
accessKeys: ["a"],
|
|
52
51
|
expiresAt: 123,
|
|
53
52
|
});
|
|
54
53
|
|
|
@@ -57,8 +56,8 @@ describe("useUploadFile", () => {
|
|
|
57
56
|
expect(finalizeUpload).toHaveBeenCalledWith({
|
|
58
57
|
uploadToken,
|
|
59
58
|
storageId: "storage",
|
|
60
|
-
accessKeys: ["a"],
|
|
61
59
|
expiresAt: 123,
|
|
60
|
+
fileName: "file.txt",
|
|
62
61
|
});
|
|
63
62
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
64
63
|
uploadUrl,
|
|
@@ -94,7 +93,6 @@ describe("useUploadFile", () => {
|
|
|
94
93
|
const file = new File(["data"], "file.bin");
|
|
95
94
|
const result = await uploadViaPresignedUrl({
|
|
96
95
|
file,
|
|
97
|
-
accessKeys: ["a"],
|
|
98
96
|
provider: "r2",
|
|
99
97
|
});
|
|
100
98
|
|
|
@@ -129,7 +127,6 @@ describe("useUploadFile", () => {
|
|
|
129
127
|
await expect(
|
|
130
128
|
uploadViaPresignedUrl({
|
|
131
129
|
file: new File(["data"], "file.txt"),
|
|
132
|
-
accessKeys: ["a"],
|
|
133
130
|
}),
|
|
134
131
|
).rejects.toThrow("Upload failed: bad");
|
|
135
132
|
|
|
@@ -146,7 +143,6 @@ describe("useUploadFile", () => {
|
|
|
146
143
|
await expect(
|
|
147
144
|
uploadViaPresignedUrl({
|
|
148
145
|
file: new File(["data"], "file.txt"),
|
|
149
|
-
accessKeys: ["a"],
|
|
150
146
|
}),
|
|
151
147
|
).rejects.toThrow("Upload did not return a storageId.");
|
|
152
148
|
});
|
|
@@ -162,14 +158,12 @@ describe("useUploadFile", () => {
|
|
|
162
158
|
await expect(
|
|
163
159
|
uploadViaHttpAction({
|
|
164
160
|
file: new File(["data"], "file.txt"),
|
|
165
|
-
accessKeys: ["a"],
|
|
166
161
|
}),
|
|
167
162
|
).rejects.toThrow("Missing HTTP upload URL");
|
|
168
163
|
|
|
169
164
|
await expect(
|
|
170
165
|
uploadViaHttpAction({
|
|
171
166
|
file: new File(["data"], "file.txt"),
|
|
172
|
-
accessKeys: ["a"],
|
|
173
167
|
http: {},
|
|
174
168
|
}),
|
|
175
169
|
).rejects.toThrow("Missing HTTP upload URL");
|
|
@@ -182,7 +176,6 @@ describe("useUploadFile", () => {
|
|
|
182
176
|
await expect(
|
|
183
177
|
uploadViaHttpAction({
|
|
184
178
|
file: new File(["data"], "file.txt"),
|
|
185
|
-
accessKeys: ["a"],
|
|
186
179
|
http: { uploadUrl: "https://upload.example.com" },
|
|
187
180
|
}),
|
|
188
181
|
).rejects.toThrow("Bad request");
|
|
@@ -195,7 +188,6 @@ describe("useUploadFile", () => {
|
|
|
195
188
|
await expect(
|
|
196
189
|
uploadViaHttpAction({
|
|
197
190
|
file: new File(["data"], "file.txt"),
|
|
198
|
-
accessKeys: ["a"],
|
|
199
191
|
http: { uploadUrl: "https://upload.example.com" },
|
|
200
192
|
}),
|
|
201
193
|
).rejects.toThrow("HTTP upload failed.");
|
|
@@ -219,18 +211,24 @@ describe("useUploadFile", () => {
|
|
|
219
211
|
const fetchMock = vi.fn(async (_url: string, init?: any) => {
|
|
220
212
|
expect(init?.body).toBeInstanceOf(FormData);
|
|
221
213
|
const form = init?.body as FormData;
|
|
222
|
-
|
|
214
|
+
// Note: accessKeys is NOT in form - it's added server-side via checkUploadRequest hook
|
|
223
215
|
expect(form.get(uploadFormFields.provider)).toBe("convex");
|
|
224
216
|
expect(form.get(uploadFormFields.expiresAt)).toBe("null");
|
|
217
|
+
expect(init?.headers).toEqual({
|
|
218
|
+
Authorization: "Bearer token",
|
|
219
|
+
});
|
|
225
220
|
return new Response(JSON.stringify(responsePayload), { status: 200 });
|
|
226
221
|
});
|
|
227
222
|
vi.stubGlobal("fetch", fetchMock);
|
|
228
223
|
|
|
229
224
|
const result = await uploadViaHttpAction({
|
|
230
225
|
file: new File(["data"], "file.txt"),
|
|
231
|
-
accessKeys: ["a"],
|
|
232
226
|
expiresAt: null,
|
|
233
|
-
http: {
|
|
227
|
+
http: {
|
|
228
|
+
baseUrl: "https://example.com",
|
|
229
|
+
pathPrefix: "/files",
|
|
230
|
+
authToken: "token",
|
|
231
|
+
},
|
|
234
232
|
});
|
|
235
233
|
|
|
236
234
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
@@ -265,7 +263,6 @@ describe("useUploadFile", () => {
|
|
|
265
263
|
|
|
266
264
|
await uploadViaHttpAction({
|
|
267
265
|
file: new File(["data"], "file.txt"),
|
|
268
|
-
accessKeys: ["a"],
|
|
269
266
|
expiresAt: 123,
|
|
270
267
|
http: { baseUrl: "https://example.com" },
|
|
271
268
|
});
|
|
@@ -310,14 +307,12 @@ describe("useUploadFile", () => {
|
|
|
310
307
|
|
|
311
308
|
await uploadFile({
|
|
312
309
|
file: new File(["data"], "file.txt"),
|
|
313
|
-
accessKeys: ["a"],
|
|
314
310
|
});
|
|
315
311
|
|
|
316
312
|
expect(fetchMock).toHaveBeenCalled();
|
|
317
313
|
|
|
318
314
|
await uploadFile({
|
|
319
315
|
file: new File(["data"], "file.txt"),
|
|
320
|
-
accessKeys: ["a"],
|
|
321
316
|
method: "presigned",
|
|
322
317
|
});
|
|
323
318
|
|
|
@@ -355,7 +350,6 @@ describe("useUploadFile", () => {
|
|
|
355
350
|
|
|
356
351
|
await uploadFile({
|
|
357
352
|
file: new File(["data"], "file.txt"),
|
|
358
|
-
accessKeys: ["a"],
|
|
359
353
|
});
|
|
360
354
|
|
|
361
355
|
expect(generateUploadUrl).toHaveBeenCalled();
|
|
@@ -198,6 +198,48 @@ describe("transferFile", () => {
|
|
|
198
198
|
expect(ctx.runMutation).toHaveBeenCalled();
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
+
test("transferFile uses r2 download url for r2 sources", async () => {
|
|
202
|
+
const getUrl = vi.fn();
|
|
203
|
+
const ctx = {
|
|
204
|
+
runQuery: vi.fn(async () => ({
|
|
205
|
+
_id: "file",
|
|
206
|
+
storageId: "storage",
|
|
207
|
+
storageProvider: "r2",
|
|
208
|
+
})),
|
|
209
|
+
runMutation: vi.fn(async () => ({
|
|
210
|
+
storageId: "new-storage",
|
|
211
|
+
storageProvider: "convex",
|
|
212
|
+
})),
|
|
213
|
+
storage: {
|
|
214
|
+
getUrl,
|
|
215
|
+
store: vi.fn(async () => "new-storage"),
|
|
216
|
+
},
|
|
217
|
+
} as any;
|
|
218
|
+
|
|
219
|
+
const downloadSpy = vi
|
|
220
|
+
.spyOn(r2, "getR2DownloadUrl")
|
|
221
|
+
.mockResolvedValue("https://r2-download.example.com");
|
|
222
|
+
|
|
223
|
+
vi.stubGlobal(
|
|
224
|
+
"fetch",
|
|
225
|
+
vi.fn(async () =>
|
|
226
|
+
new Response("data", {
|
|
227
|
+
status: 200,
|
|
228
|
+
headers: { "Content-Type": "text/plain" },
|
|
229
|
+
}),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
await getHandler(transferFile)(ctx, {
|
|
234
|
+
storageId: "storage",
|
|
235
|
+
targetProvider: "convex",
|
|
236
|
+
r2Config,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(downloadSpy).toHaveBeenCalledWith(r2Config, "storage");
|
|
240
|
+
expect(getUrl).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
201
243
|
test("transfers convex source to r2 target", async () => {
|
|
202
244
|
const send = vi.fn(async () => ({}));
|
|
203
245
|
vi.spyOn(r2, "createR2Client").mockReturnValue({ send } as any);
|
|
@@ -241,6 +283,26 @@ describe("transferFile", () => {
|
|
|
241
283
|
expect(send.mock.calls[0]?.[0]).toBeInstanceOf(PutObjectCommand);
|
|
242
284
|
});
|
|
243
285
|
|
|
286
|
+
test("throws when source provider is unrecognized", async () => {
|
|
287
|
+
const ctx = {
|
|
288
|
+
runQuery: vi.fn(async () => ({
|
|
289
|
+
_id: "file",
|
|
290
|
+
storageId: "storage",
|
|
291
|
+
storageProvider: "unknown",
|
|
292
|
+
})),
|
|
293
|
+
storage: {
|
|
294
|
+
getUrl: vi.fn(),
|
|
295
|
+
},
|
|
296
|
+
} as any;
|
|
297
|
+
|
|
298
|
+
await expect(
|
|
299
|
+
getHandler(transferFile)(ctx, {
|
|
300
|
+
storageId: "storage",
|
|
301
|
+
targetProvider: "convex",
|
|
302
|
+
}),
|
|
303
|
+
).rejects.toThrow("File not found.");
|
|
304
|
+
});
|
|
305
|
+
|
|
244
306
|
test("transfers with needsR2 false uses undefined r2Config", async () => {
|
|
245
307
|
let providerReads = 0;
|
|
246
308
|
const file = {
|
|
@@ -372,6 +434,47 @@ describe("commitTransfer", () => {
|
|
|
372
434
|
expect(ctx.storage.delete).toHaveBeenCalled();
|
|
373
435
|
});
|
|
374
436
|
|
|
437
|
+
test("commitTransfer invokes index callbacks for access and grants", async () => {
|
|
438
|
+
const file = {
|
|
439
|
+
_id: "file",
|
|
440
|
+
storageId: "storage",
|
|
441
|
+
storageProvider: "convex",
|
|
442
|
+
};
|
|
443
|
+
const queryObj = {
|
|
444
|
+
eq: vi.fn(() => queryObj),
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const ctx = {
|
|
448
|
+
db: {
|
|
449
|
+
query: (table: string) => ({
|
|
450
|
+
withIndex: (_index: string, cb?: (q: typeof queryObj) => void) => {
|
|
451
|
+
if (cb) {
|
|
452
|
+
cb(queryObj);
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
first: async () => (table === "files" ? file : null),
|
|
456
|
+
collect: async () => [],
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
}),
|
|
460
|
+
patch: vi.fn(async () => undefined),
|
|
461
|
+
},
|
|
462
|
+
storage: {
|
|
463
|
+
delete: vi.fn(async () => undefined),
|
|
464
|
+
},
|
|
465
|
+
} as any;
|
|
466
|
+
|
|
467
|
+
await getHandler(commitTransfer)(ctx, {
|
|
468
|
+
storageId: "storage",
|
|
469
|
+
newStorageId: "new",
|
|
470
|
+
targetProvider: "convex",
|
|
471
|
+
sourceProvider: "convex",
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(queryObj.eq).toHaveBeenCalledWith("fileId", "file");
|
|
475
|
+
expect(queryObj.eq).toHaveBeenCalledWith("storageId", "storage");
|
|
476
|
+
});
|
|
477
|
+
|
|
375
478
|
test("updates records and deletes r2 storage", async () => {
|
|
376
479
|
const deleteSpy = vi
|
|
377
480
|
.spyOn(r2, "deleteR2Object")
|
package/src/client/http.ts
CHANGED
|
@@ -1,24 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const DEFAULT_ALLOW_HEADERS = ["Content-Type", "Authorization"];
|
|
2
|
+
|
|
3
|
+
function buildAllowHeaders(extra?: string[]): string {
|
|
4
|
+
const headers: string[] = [];
|
|
5
|
+
const seen = new Set<string>();
|
|
6
|
+
|
|
7
|
+
const addHeader = (value: string) => {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!trimmed) return;
|
|
10
|
+
const key = trimmed.toLowerCase();
|
|
11
|
+
if (seen.has(key)) return;
|
|
12
|
+
seen.add(key);
|
|
13
|
+
headers.push(trimmed);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
for (const header of DEFAULT_ALLOW_HEADERS) {
|
|
17
|
+
addHeader(header);
|
|
18
|
+
}
|
|
19
|
+
for (const header of extra ?? []) {
|
|
20
|
+
addHeader(header);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return headers.join(", ");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function corsHeaders(origin?: string, allowHeaders?: string[]): Headers {
|
|
27
|
+
const headers = new Headers({
|
|
28
|
+
"Access-Control-Allow-Origin": origin || "*",
|
|
4
29
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
5
|
-
"Access-Control-Allow-Headers":
|
|
30
|
+
"Access-Control-Allow-Headers": buildAllowHeaders(allowHeaders),
|
|
6
31
|
});
|
|
32
|
+
if (origin) {
|
|
33
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
34
|
+
}
|
|
35
|
+
return headers;
|
|
7
36
|
}
|
|
8
37
|
|
|
9
|
-
export function corsResponse(): Response {
|
|
10
|
-
return new Response(null, {
|
|
38
|
+
export function corsResponse(origin?: string, allowHeaders?: string[]): Response {
|
|
39
|
+
return new Response(null, {
|
|
40
|
+
status: 204,
|
|
41
|
+
headers: corsHeaders(origin, allowHeaders),
|
|
42
|
+
});
|
|
11
43
|
}
|
|
12
44
|
|
|
13
|
-
export function jsonSuccess(
|
|
14
|
-
|
|
45
|
+
export function jsonSuccess(
|
|
46
|
+
data: unknown,
|
|
47
|
+
origin?: string,
|
|
48
|
+
allowHeaders?: string[],
|
|
49
|
+
): Response {
|
|
50
|
+
const headers = corsHeaders(origin, allowHeaders);
|
|
15
51
|
headers.set("Content-Type", "application/json");
|
|
16
52
|
|
|
17
53
|
return new Response(JSON.stringify(data), { status: 200, headers });
|
|
18
54
|
}
|
|
19
55
|
|
|
20
|
-
export function jsonError(
|
|
21
|
-
|
|
56
|
+
export function jsonError(
|
|
57
|
+
message: string,
|
|
58
|
+
status: number,
|
|
59
|
+
origin?: string,
|
|
60
|
+
allowHeaders?: string[],
|
|
61
|
+
): Response {
|
|
62
|
+
const headers = corsHeaders(origin, allowHeaders);
|
|
22
63
|
headers.set("Content-Type", "application/json");
|
|
23
64
|
|
|
24
65
|
return new Response(JSON.stringify({ error: message }), { status, headers });
|