convex-helpers 0.1.94 → 0.1.95

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 CHANGED
@@ -33,7 +33,7 @@ Table of contents:
33
33
  - [What can you do with triggers?](#what-can-you-do-with-triggers)
34
34
  - [Trigger semantics](#trigger-semantics)
35
35
  - [CORS support for HttpRouter](#cors-support-for-httprouter)
36
- - [Standard Schema support](#standard-schema)
36
+ - [Standard Schema](#standard-schema)
37
37
 
38
38
  ## Custom Functions
39
39
 
@@ -1180,12 +1180,16 @@ const cors = corsRouter(
1180
1180
  http,
1181
1181
  // Optional configuration, can be omitted entirely
1182
1182
  {
1183
+ // allowedOrigins can also be a function
1184
+ // allowedOrigins: (req: Request) => Promise<string[]>
1183
1185
  allowedOrigins: ["http://localhost:8080"], // Default: ["*"]
1184
1186
  allowedMethods: ["GET", "POST"], // Defaults to route spec method
1185
1187
  allowedHeaders: ["Content-Type"], // Default: ["Content-Type"]
1186
1188
  exposedHeaders: ["Custom-Header"], // Default: ["Content-Range", "Accept-Ranges"]
1187
1189
  allowCredentials: true, // Default: false
1188
1190
  browserCacheMaxAge: 60, // Default: 86400 (1 day)
1191
+ // returns a 403 if the origin is not allowed
1192
+ enforceAllowOrigins: true, // Default: false
1189
1193
  debug: true, // Default: false
1190
1194
  },
1191
1195
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "convex-helpers",
3
- "version": "0.1.94",
3
+ "version": "0.1.95",
4
4
  "description": "A collection of useful code to complement the official convex package.",
5
5
  "type": "module",
6
6
  "bin": {
package/server/cors.d.ts CHANGED
@@ -31,7 +31,7 @@ export type CorsConfig = {
31
31
  * - https://example.com
32
32
  * @default ["*"]
33
33
  */
34
- allowedOrigins?: string[];
34
+ allowedOrigins?: string[] | ((req: Request) => Promise<string[]>);
35
35
  /**
36
36
  * An array of allowed headers: what headers are allowed to be sent in
37
37
  * the request.
@@ -52,6 +52,11 @@ export type CorsConfig = {
52
52
  * @default 86400 (1 day)
53
53
  */
54
54
  browserCacheMaxAge?: number;
55
+ /**
56
+ * Whether to block requests from origins that are not in the allowedOrigins list.
57
+ * @default false
58
+ */
59
+ enforceAllowOrigins?: boolean;
55
60
  /**
56
61
  * Whether to log debugging information about CORS requests.
57
62
  * @default false
@@ -1 +1 @@
1
- {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["cors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAIL,UAAU,EAIV,KAAK,SAAS,EAGf,MAAM,eAAe,CAAC;AAEvB,eAAO,MAAM,uBAAuB,UAInC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,iBAAiB,GAAG,SAAS,GAAG,UAAU,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,UAAU,EAAE,aAAa,UAAU;;uBAK7C,iBAAiB,KAAG,IAAI;CAsD9C,CAAC;AA+DF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["cors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAIL,UAAU,EAIV,KAAK,SAAS,EAGf,MAAM,eAAe,CAAC;AAEvB,eAAO,MAAM,uBAAuB,UAInC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;;OASG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAClE;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,iBAAiB,GAAG,SAAS,GAAG,UAAU,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,UAAU,EAAE,aAAa,UAAU;;uBAK7C,iBAAiB,KAAG,IAAI;CAsD9C,CAAC;AA+DF,eAAe,UAAU,CAAC"}
package/server/cors.js CHANGED
@@ -146,7 +146,7 @@ const SECONDS_IN_A_DAY = 60 * 60 * 24;
146
146
  * - "https://example1.com, https://example2.com" (allow multiple specific domains)
147
147
  * - "null" (allow requests from data URLs or local files)
148
148
  */
149
- const handleCors = ({ originalHandler, allowedMethods = ["OPTIONS"], allowedOrigins = ["*"], allowedHeaders = ["Content-Type"], exposedHeaders = DEFAULT_EXPOSED_HEADERS, allowCredentials = false, browserCacheMaxAge = SECONDS_IN_A_DAY, debug = false, }) => {
149
+ const handleCors = ({ originalHandler, allowedMethods = ["OPTIONS"], allowedOrigins = ["*"], allowedHeaders = ["Content-Type"], exposedHeaders = DEFAULT_EXPOSED_HEADERS, allowCredentials = false, browserCacheMaxAge = SECONDS_IN_A_DAY, enforceAllowOrigins = false, debug = false, }) => {
150
150
  const uniqueMethods = Array.from(new Set(allowedMethods.map((method) => method.toUpperCase())));
151
151
  const filteredMethods = uniqueMethods.filter((method) => ROUTABLE_HTTP_METHODS.includes(method));
152
152
  if (filteredMethods.length === 0) {
@@ -171,9 +171,17 @@ const handleCors = ({ originalHandler, allowedMethods = ["OPTIONS"], allowedOrig
171
171
  if (exposedHeaders.length > 0) {
172
172
  commonHeaders["Access-Control-Expose-Headers"] = exposedHeaders.join(", ");
173
173
  }
174
+ async function parseAllowedOrigins(request) {
175
+ return Array.isArray(allowedOrigins)
176
+ ? allowedOrigins
177
+ : await allowedOrigins(request);
178
+ }
174
179
  // Helper function to check if origin is allowed (including wildcard subdomain matching)
175
- function isAllowedOrigin(requestOrigin) {
176
- return allowedOrigins.some((allowed) => {
180
+ async function isAllowedOrigin(request) {
181
+ const requestOrigin = request.headers.get("origin");
182
+ if (!requestOrigin)
183
+ return false;
184
+ return (await parseAllowedOrigins(request)).some((allowed) => {
177
185
  if (allowed === "*")
178
186
  return true;
179
187
  if (allowed === requestOrigin)
@@ -208,36 +216,48 @@ const handleCors = ({ originalHandler, allowedMethods = ["OPTIONS"], allowedOrig
208
216
  });
209
217
  }
210
218
  const requestOrigin = request.headers.get("origin");
219
+ const parsedAllowedOrigins = await parseAllowedOrigins(request);
220
+ if (debug) {
221
+ console.log("allowed origins", parsedAllowedOrigins);
222
+ }
211
223
  // Handle origin matching
212
224
  let allowOrigins = null;
213
- if (allowedOrigins.includes("*") && !allowCredentials) {
214
- allowOrigins = "*";
225
+ if (parsedAllowedOrigins.includes("*") &&
226
+ requestOrigin &&
227
+ !allowCredentials) {
228
+ allowOrigins = requestOrigin;
215
229
  }
216
230
  else if (requestOrigin) {
217
231
  // Check if the request origin matches any of the allowed origins
218
232
  // (including wildcard subdomain matching if configured)
219
- if (isAllowedOrigin(requestOrigin)) {
233
+ if (await isAllowedOrigin(request)) {
220
234
  allowOrigins = requestOrigin;
221
235
  }
222
236
  }
223
- if (!allowOrigins) {
237
+ if (enforceAllowOrigins && !allowOrigins) {
224
238
  // Origin not allowed
225
- console.error(`Request from origin ${requestOrigin} blocked, missing from allowed origins: ${allowedOrigins.join()}`);
239
+ console.error(`Request from origin ${requestOrigin} blocked, missing from allowed origins: ${parsedAllowedOrigins.join()}`);
226
240
  return new Response(null, { status: 403 });
227
241
  }
228
242
  /**
229
243
  * OPTIONS has no handler and just returns headers
230
244
  */
231
245
  if (request.method === "OPTIONS") {
246
+ const responseHeaders = new Headers({
247
+ ...commonHeaders,
248
+ ...(allowOrigins
249
+ ? { "Access-Control-Allow-Origin": allowOrigins }
250
+ : {}),
251
+ "Access-Control-Allow-Methods": allowMethods,
252
+ "Access-Control-Allow-Headers": allowedHeaders.join(", "),
253
+ "Access-Control-Max-Age": browserCacheMaxAge.toString(),
254
+ });
255
+ if (debug) {
256
+ console.log("CORS OPTIONS response headers", responseHeaders);
257
+ }
232
258
  return new Response(null, {
233
259
  status: 204,
234
- headers: new Headers({
235
- ...commonHeaders,
236
- "Access-Control-Allow-Origin": allowOrigins,
237
- "Access-Control-Allow-Methods": allowMethods,
238
- "Access-Control-Allow-Headers": allowedHeaders.join(", "),
239
- "Access-Control-Max-Age": browserCacheMaxAge.toString(),
240
- }),
260
+ headers: responseHeaders,
241
261
  });
242
262
  }
243
263
  /**
@@ -254,16 +274,22 @@ const handleCors = ({ originalHandler, allowedMethods = ["OPTIONS"], allowedOrig
254
274
  : originalHandler);
255
275
  const originalResponse = await innerHandler(ctx, request);
256
276
  /**
257
- * Second, get a copy of the original response's headers
277
+ * Second, get a copy of the original response's headers and add the
278
+ * allow origin header if it's allowed
258
279
  */
259
280
  const newHeaders = new Headers(originalResponse.headers);
260
- newHeaders.set("Access-Control-Allow-Origin", allowOrigins);
281
+ if (allowOrigins) {
282
+ newHeaders.set("Access-Control-Allow-Origin", allowOrigins);
283
+ }
261
284
  /**
262
- * Third, add or update our CORS headers
285
+ * Third, add or update our other CORS headers
263
286
  */
264
287
  Object.entries(commonHeaders).forEach(([key, value]) => {
265
288
  newHeaders.set(key, value);
266
289
  });
290
+ if (debug) {
291
+ console.log("CORS response headers", newHeaders);
292
+ }
267
293
  /**
268
294
  * Fourth, return the modified Response.
269
295
  * A Response object is immutable, so we create a new one to return here.
package/server/cors.ts CHANGED
@@ -48,7 +48,7 @@ export type CorsConfig = {
48
48
  * - https://example.com
49
49
  * @default ["*"]
50
50
  */
51
- allowedOrigins?: string[];
51
+ allowedOrigins?: string[] | ((req: Request) => Promise<string[]>);
52
52
  /**
53
53
  * An array of allowed headers: what headers are allowed to be sent in
54
54
  * the request.
@@ -69,6 +69,11 @@ export type CorsConfig = {
69
69
  * @default 86400 (1 day)
70
70
  */
71
71
  browserCacheMaxAge?: number;
72
+ /**
73
+ * Whether to block requests from origins that are not in the allowedOrigins list.
74
+ * @default false
75
+ */
76
+ enforceAllowOrigins?: boolean;
72
77
  /**
73
78
  * Whether to log debugging information about CORS requests.
74
79
  * @default false
@@ -240,6 +245,7 @@ const handleCors = ({
240
245
  exposedHeaders = DEFAULT_EXPOSED_HEADERS,
241
246
  allowCredentials = false,
242
247
  browserCacheMaxAge = SECONDS_IN_A_DAY,
248
+ enforceAllowOrigins = false,
243
249
  debug = false,
244
250
  }: {
245
251
  originalHandler?: PublicHttpAction;
@@ -279,9 +285,17 @@ const handleCors = ({
279
285
  commonHeaders["Access-Control-Expose-Headers"] = exposedHeaders.join(", ");
280
286
  }
281
287
 
288
+ async function parseAllowedOrigins(request: Request): Promise<string[]> {
289
+ return Array.isArray(allowedOrigins)
290
+ ? allowedOrigins
291
+ : await allowedOrigins(request);
292
+ }
293
+
282
294
  // Helper function to check if origin is allowed (including wildcard subdomain matching)
283
- function isAllowedOrigin(requestOrigin: string): boolean {
284
- return allowedOrigins.some((allowed) => {
295
+ async function isAllowedOrigin(request: Request): Promise<boolean> {
296
+ const requestOrigin = request.headers.get("origin");
297
+ if (!requestOrigin) return false;
298
+ return (await parseAllowedOrigins(request)).some((allowed) => {
285
299
  if (allowed === "*") return true;
286
300
  if (allowed === requestOrigin) return true;
287
301
  if (allowed.startsWith("*.")) {
@@ -317,23 +331,32 @@ const handleCors = ({
317
331
  });
318
332
  }
319
333
  const requestOrigin = request.headers.get("origin");
334
+ const parsedAllowedOrigins = await parseAllowedOrigins(request);
335
+
336
+ if (debug) {
337
+ console.log("allowed origins", parsedAllowedOrigins);
338
+ }
320
339
 
321
340
  // Handle origin matching
322
341
  let allowOrigins: string | null = null;
323
- if (allowedOrigins.includes("*") && !allowCredentials) {
324
- allowOrigins = "*";
342
+ if (
343
+ parsedAllowedOrigins.includes("*") &&
344
+ requestOrigin &&
345
+ !allowCredentials
346
+ ) {
347
+ allowOrigins = requestOrigin;
325
348
  } else if (requestOrigin) {
326
349
  // Check if the request origin matches any of the allowed origins
327
350
  // (including wildcard subdomain matching if configured)
328
- if (isAllowedOrigin(requestOrigin)) {
351
+ if (await isAllowedOrigin(request)) {
329
352
  allowOrigins = requestOrigin;
330
353
  }
331
354
  }
332
355
 
333
- if (!allowOrigins) {
356
+ if (enforceAllowOrigins && !allowOrigins) {
334
357
  // Origin not allowed
335
358
  console.error(
336
- `Request from origin ${requestOrigin} blocked, missing from allowed origins: ${allowedOrigins.join()}`,
359
+ `Request from origin ${requestOrigin} blocked, missing from allowed origins: ${parsedAllowedOrigins.join()}`,
337
360
  );
338
361
  return new Response(null, { status: 403 });
339
362
  }
@@ -341,15 +364,21 @@ const handleCors = ({
341
364
  * OPTIONS has no handler and just returns headers
342
365
  */
343
366
  if (request.method === "OPTIONS") {
367
+ const responseHeaders = new Headers({
368
+ ...commonHeaders,
369
+ ...(allowOrigins
370
+ ? { "Access-Control-Allow-Origin": allowOrigins }
371
+ : {}),
372
+ "Access-Control-Allow-Methods": allowMethods,
373
+ "Access-Control-Allow-Headers": allowedHeaders.join(", "),
374
+ "Access-Control-Max-Age": browserCacheMaxAge.toString(),
375
+ });
376
+ if (debug) {
377
+ console.log("CORS OPTIONS response headers", responseHeaders);
378
+ }
344
379
  return new Response(null, {
345
380
  status: 204,
346
- headers: new Headers({
347
- ...commonHeaders,
348
- "Access-Control-Allow-Origin": allowOrigins,
349
- "Access-Control-Allow-Methods": allowMethods,
350
- "Access-Control-Allow-Headers": allowedHeaders.join(", "),
351
- "Access-Control-Max-Age": browserCacheMaxAge.toString(),
352
- }),
381
+ headers: responseHeaders,
353
382
  });
354
383
  }
355
384
 
@@ -372,18 +401,25 @@ const handleCors = ({
372
401
  const originalResponse = await innerHandler(ctx, request);
373
402
 
374
403
  /**
375
- * Second, get a copy of the original response's headers
404
+ * Second, get a copy of the original response's headers and add the
405
+ * allow origin header if it's allowed
376
406
  */
377
407
  const newHeaders = new Headers(originalResponse.headers);
378
- newHeaders.set("Access-Control-Allow-Origin", allowOrigins);
408
+ if (allowOrigins) {
409
+ newHeaders.set("Access-Control-Allow-Origin", allowOrigins);
410
+ }
379
411
 
380
412
  /**
381
- * Third, add or update our CORS headers
413
+ * Third, add or update our other CORS headers
382
414
  */
383
415
  Object.entries(commonHeaders).forEach(([key, value]) => {
384
416
  newHeaders.set(key, value);
385
417
  });
386
418
 
419
+ if (debug) {
420
+ console.log("CORS response headers", newHeaders);
421
+ }
422
+
387
423
  /**
388
424
  * Fourth, return the modified Response.
389
425
  * A Response object is immutable, so we create a new one to return here.