@tumbaland/backend-core 1.34.0 → 1.35.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.
@@ -1,4 +1,4 @@
1
- import cors from 'cors';
1
+ import type { RequestHandler } from 'express';
2
2
  export interface CorsMiddlewareOptions {
3
3
  /**
4
4
  * Comma-separated origin whitelist. Defaults to process.env.CORS_ORIGIN,
@@ -6,15 +6,26 @@ export interface CorsMiddlewareOptions {
6
6
  */
7
7
  origins?: string;
8
8
  exposedHeaders?: string[];
9
+ /**
10
+ * Path prefixes that skip the origin check entirely.
11
+ *
12
+ * For endpoints the browser *navigates* to rather than fetches — an OAuth
13
+ * authorize page and the consent form it posts back. CORS governs
14
+ * script-initiated cross-origin reads and has nothing to say about a
15
+ * top-level form submission, but it still blocks one: Firefox sends a
16
+ * literal `Origin: null` after a cross-site redirect chain, which is truthy,
17
+ * matches no whitelist entry, and turns a legitimate consent into a 500.
18
+ *
19
+ * Nothing is weakened by skipping them. These routes carry no XHR-readable
20
+ * data, and what actually protects the consent POST is its signed consent
21
+ * token, not the origin header.
22
+ */
23
+ bypassPaths?: string[];
9
24
  }
10
25
  /**
11
26
  * Shared CORS middleware with an explicit origin whitelist.
12
27
  * Requests without an Origin header (server-to-server, curl, health checks)
13
28
  * are always allowed; browser requests must match the whitelist.
14
29
  */
15
- export declare function createCorsMiddleware(options?: CorsMiddlewareOptions): (req: cors.CorsRequest, res: {
16
- statusCode?: number | undefined;
17
- setHeader(key: string, value: string): any;
18
- end(): any;
19
- }, next: (err?: any) => any) => void;
30
+ export declare function createCorsMiddleware(options?: CorsMiddlewareOptions): RequestHandler;
20
31
  //# sourceMappingURL=corsMiddleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"corsMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/corsMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B;cAmBlE,CAAC;;;aACI,CAAC,uBAEX"}
1
+ {"version":3,"file":"corsMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/corsMiddleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,cAAc,CA+BxF"}
@@ -12,7 +12,8 @@ const logger_1 = __importDefault(require("../logging/logger"));
12
12
  * are always allowed; browser requests must match the whitelist.
13
13
  */
14
14
  function createCorsMiddleware(options = {}) {
15
- return (0, cors_1.default)({
15
+ const bypassPaths = options.bypassPaths ?? [];
16
+ const corsHandler = (0, cors_1.default)({
16
17
  origin: (origin, callback) => {
17
18
  const allowedOrigins = (options.origins ?? process.env.CORS_ORIGIN ?? '')
18
19
  .split(',')
@@ -33,5 +34,12 @@ function createCorsMiddleware(options = {}) {
33
34
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-correlation-id', 'x-session-id'],
34
35
  exposedHeaders: options.exposedHeaders
35
36
  });
37
+ if (bypassPaths.length === 0)
38
+ return corsHandler;
39
+ return (req, res, next) => {
40
+ if (bypassPaths.some((prefix) => req.path.startsWith(prefix)))
41
+ return next();
42
+ return corsHandler(req, res, next);
43
+ };
36
44
  }
37
45
  //# sourceMappingURL=corsMiddleware.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"corsMiddleware.js","sourceRoot":"","sources":["../../src/middleware/corsMiddleware.ts"],"names":[],"mappings":";;;;;AAiBA,oDAsBC;AAvCD,gDAAwB;AACxB,+DAAuC;AAWvC;;;;GAIG;AACH,SAAgB,oBAAoB,CAAC,UAAiC,EAAE;IACtE,OAAO,IAAA,cAAI,EAAC;QACV,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;YAC3B,MAAM,cAAc,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC;iBACtE,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBAClB,MAAM,CAAC,OAAO,CAAC,CAAC;YACnB,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC/C,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC9B,CAAC;YACD,gBAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/C,OAAO,QAAQ,CAAC,IAAI,KAAK,CAAC,uBAAuB,GAAG,MAAM,CAAC,CAAC,CAAC;QAC/D,CAAC;QACD,WAAW,EAAE,IAAI;QACjB,uEAAuE;QACvE,uEAAuE;QACvE,wEAAwE;QACxE,kDAAkD;QAClD,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC;QAC7D,cAAc,EAAE,CAAC,cAAc,EAAE,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,cAAc,CAAC;QACzG,cAAc,EAAE,OAAO,CAAC,cAAc;KACvC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"corsMiddleware.js","sourceRoot":"","sources":["../../src/middleware/corsMiddleware.ts"],"names":[],"mappings":";;;;;AAiCA,oDA+BC;AAhED,gDAAwB;AAExB,+DAAuC;AA0BvC;;;;GAIG;AACH,SAAgB,oBAAoB,CAAC,UAAiC,EAAE;IACtE,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC;IAE9C,MAAM,WAAW,GAAG,IAAA,cAAI,EAAC;QACvB,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;YAC3B,MAAM,cAAc,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC;iBACtE,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBAClB,MAAM,CAAC,OAAO,CAAC,CAAC;YACnB,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC/C,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC9B,CAAC;YACD,gBAAM,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/C,OAAO,QAAQ,CAAC,IAAI,KAAK,CAAC,uBAAuB,GAAG,MAAM,CAAC,CAAC,CAAC;QAC/D,CAAC;QACD,WAAW,EAAE,IAAI;QACjB,uEAAuE;QACvE,uEAAuE;QACvE,wEAAwE;QACxE,kDAAkD;QAClD,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC;QAC7D,cAAc,EAAE,CAAC,cAAc,EAAE,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,cAAc,CAAC;QACzG,cAAc,EAAE,OAAO,CAAC,cAAc;KACvC,CAAC,CAAC;IAEH,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,WAA6B,CAAC;IAEnE,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QAC7E,OAAQ,WAA8B,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACzD,CAAC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tumbaland/backend-core",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "description": "Core shared functionality for Tumbaland backend services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -94,3 +94,42 @@ describe('createCorsMiddleware', () => {
94
94
  process.env = ORIGINAL_ENV;
95
95
  });
96
96
  });
97
+
98
+ describe('bypassPaths', () => {
99
+ const run = (middleware: ReturnType<typeof createCorsMiddleware>, path: string, origin?: string) => {
100
+ const req = { path, method: 'POST', headers: origin ? { origin } : {} } as never;
101
+ const res = { setHeader: jest.fn(), getHeader: jest.fn(), end: jest.fn() } as never;
102
+ const next = jest.fn();
103
+ middleware(req, res, next);
104
+ return next;
105
+ };
106
+
107
+ it('lets a bypassed path through with an origin that would otherwise be blocked', () => {
108
+ // Firefox sends a literal `Origin: null` on the consent form POST after the
109
+ // login redirect. It is truthy, matches no whitelist entry, and turned an
110
+ // approved consent into a 500.
111
+ const middleware = createCorsMiddleware({ origins: 'https://app.example', bypassPaths: ['/oauth'] });
112
+
113
+ const next = run(middleware, '/oauth/authorize', 'null');
114
+
115
+ expect(next).toHaveBeenCalledWith();
116
+ });
117
+
118
+ it('still checks the origin on every other path', () => {
119
+ const middleware = createCorsMiddleware({ origins: 'https://app.example', bypassPaths: ['/oauth'] });
120
+
121
+ const next = run(middleware, '/auth/profile', 'null');
122
+
123
+ expect(next).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining('CORS') }));
124
+ });
125
+
126
+ it('matches by prefix, so nested oauth routes are covered too', () => {
127
+ const middleware = createCorsMiddleware({ origins: 'https://app.example', bypassPaths: ['/oauth'] });
128
+ expect(run(middleware, '/oauth/token', 'null')).toHaveBeenCalledWith();
129
+ });
130
+
131
+ it('changes nothing when no bypass is configured', () => {
132
+ const middleware = createCorsMiddleware({ origins: 'https://app.example' });
133
+ expect(run(middleware, '/oauth/authorize', 'null')).toHaveBeenCalledWith(expect.any(Error));
134
+ });
135
+ });
@@ -1,4 +1,5 @@
1
1
  import cors from 'cors';
2
+ import type { RequestHandler } from 'express';
2
3
  import logger from '../logging/logger';
3
4
 
4
5
  export interface CorsMiddlewareOptions {
@@ -8,6 +9,21 @@ export interface CorsMiddlewareOptions {
8
9
  */
9
10
  origins?: string;
10
11
  exposedHeaders?: string[];
12
+ /**
13
+ * Path prefixes that skip the origin check entirely.
14
+ *
15
+ * For endpoints the browser *navigates* to rather than fetches — an OAuth
16
+ * authorize page and the consent form it posts back. CORS governs
17
+ * script-initiated cross-origin reads and has nothing to say about a
18
+ * top-level form submission, but it still blocks one: Firefox sends a
19
+ * literal `Origin: null` after a cross-site redirect chain, which is truthy,
20
+ * matches no whitelist entry, and turns a legitimate consent into a 500.
21
+ *
22
+ * Nothing is weakened by skipping them. These routes carry no XHR-readable
23
+ * data, and what actually protects the consent POST is its signed consent
24
+ * token, not the origin header.
25
+ */
26
+ bypassPaths?: string[];
11
27
  }
12
28
 
13
29
  /**
@@ -15,8 +31,10 @@ export interface CorsMiddlewareOptions {
15
31
  * Requests without an Origin header (server-to-server, curl, health checks)
16
32
  * are always allowed; browser requests must match the whitelist.
17
33
  */
18
- export function createCorsMiddleware(options: CorsMiddlewareOptions = {}) {
19
- return cors({
34
+ export function createCorsMiddleware(options: CorsMiddlewareOptions = {}): RequestHandler {
35
+ const bypassPaths = options.bypassPaths ?? [];
36
+
37
+ const corsHandler = cors({
20
38
  origin: (origin, callback) => {
21
39
  const allowedOrigins = (options.origins ?? process.env.CORS_ORIGIN ?? '')
22
40
  .split(',')
@@ -37,4 +55,11 @@ export function createCorsMiddleware(options: CorsMiddlewareOptions = {}) {
37
55
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'x-correlation-id', 'x-session-id'],
38
56
  exposedHeaders: options.exposedHeaders
39
57
  });
58
+
59
+ if (bypassPaths.length === 0) return corsHandler as RequestHandler;
60
+
61
+ return (req, res, next) => {
62
+ if (bypassPaths.some((prefix) => req.path.startsWith(prefix))) return next();
63
+ return (corsHandler as RequestHandler)(req, res, next);
64
+ };
40
65
  }