@tumbaland/backend-core 1.44.0 → 1.46.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.
Files changed (123) hide show
  1. package/dist/fileService/client.d.ts +54 -0
  2. package/dist/fileService/client.d.ts.map +1 -0
  3. package/dist/fileService/client.js +137 -0
  4. package/dist/fileService/client.js.map +1 -0
  5. package/dist/fileService/index.d.ts +4 -0
  6. package/dist/fileService/index.d.ts.map +1 -0
  7. package/dist/fileService/index.js +40 -0
  8. package/dist/fileService/index.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/middleware/corsMiddleware.d.ts.map +1 -1
  14. package/dist/middleware/corsMiddleware.js +4 -1
  15. package/dist/middleware/corsMiddleware.js.map +1 -1
  16. package/dist/middleware/errorHandler.d.ts.map +1 -1
  17. package/dist/middleware/errorHandler.js +5 -8
  18. package/dist/middleware/errorHandler.js.map +1 -1
  19. package/dist/utils/escapeRegex.d.ts +8 -0
  20. package/dist/utils/escapeRegex.d.ts.map +1 -0
  21. package/dist/utils/escapeRegex.js +12 -0
  22. package/dist/utils/escapeRegex.js.map +1 -0
  23. package/package.json +5 -1
  24. package/.versionrc.json +0 -7
  25. package/__mocks__/uuid.js +0 -8
  26. package/jest.config.js +0 -24
  27. package/src/apiKeys/ApiKey.test.ts +0 -142
  28. package/src/apiKeys/ApiKey.ts +0 -102
  29. package/src/apiKeys/crypto.test.ts +0 -161
  30. package/src/apiKeys/crypto.ts +0 -163
  31. package/src/apiKeys/index.test.ts +0 -72
  32. package/src/apiKeys/index.ts +0 -35
  33. package/src/apiKeys/middleware.test.ts +0 -651
  34. package/src/apiKeys/middleware.ts +0 -341
  35. package/src/apiKeys/service.test.ts +0 -401
  36. package/src/apiKeys/service.ts +0 -168
  37. package/src/apiKeys/types.test.ts +0 -41
  38. package/src/apiKeys/types.ts +0 -124
  39. package/src/app/createBaseApp.test.ts +0 -109
  40. package/src/app/createBaseApp.ts +0 -102
  41. package/src/app/shutdown.test.ts +0 -129
  42. package/src/app/shutdown.ts +0 -81
  43. package/src/audit/AuditEvent.ts +0 -123
  44. package/src/audit/actor.test.ts +0 -95
  45. package/src/audit/actor.ts +0 -68
  46. package/src/audit/context.test.ts +0 -91
  47. package/src/audit/context.ts +0 -83
  48. package/src/audit/index.ts +0 -11
  49. package/src/audit/plugin.test.ts +0 -258
  50. package/src/audit/plugin.ts +0 -254
  51. package/src/audit/reads.test.ts +0 -164
  52. package/src/audit/reads.ts +0 -88
  53. package/src/audit/service.test.ts +0 -115
  54. package/src/audit/service.ts +0 -95
  55. package/src/auth/session.test.ts +0 -89
  56. package/src/auth/session.ts +0 -75
  57. package/src/config/env.test.ts +0 -28
  58. package/src/config/env.ts +0 -13
  59. package/src/database/connection.test.ts +0 -188
  60. package/src/database/connection.ts +0 -90
  61. package/src/entitlements/UsageMeter.ts +0 -49
  62. package/src/entitlements/client.test.ts +0 -200
  63. package/src/entitlements/client.ts +0 -179
  64. package/src/entitlements/definitions.test.ts +0 -161
  65. package/src/entitlements/definitions.ts +0 -268
  66. package/src/entitlements/index.ts +0 -42
  67. package/src/entitlements/middleware.test.ts +0 -196
  68. package/src/entitlements/middleware.ts +0 -150
  69. package/src/entitlements/reconcile.test.ts +0 -333
  70. package/src/entitlements/reconcile.ts +0 -384
  71. package/src/entitlements/types.ts +0 -21
  72. package/src/entitlements/usage.test.ts +0 -314
  73. package/src/entitlements/usage.ts +0 -223
  74. package/src/errors/HttpError.test.ts +0 -76
  75. package/src/errors/HttpError.ts +0 -91
  76. package/src/groups/client.test.ts +0 -215
  77. package/src/groups/client.ts +0 -182
  78. package/src/groups/index.ts +0 -7
  79. package/src/groups/membership.test.ts +0 -84
  80. package/src/groups/membership.ts +0 -133
  81. package/src/groups/subject.test.ts +0 -85
  82. package/src/groups/subject.ts +0 -50
  83. package/src/health/createHealthCheck.test.ts +0 -89
  84. package/src/health/createHealthCheck.ts +0 -67
  85. package/src/health/healthController.test.ts +0 -113
  86. package/src/health/healthController.ts +0 -56
  87. package/src/index.ts +0 -88
  88. package/src/logging/logger.test.ts +0 -91
  89. package/src/logging/logger.ts +0 -103
  90. package/src/metrics/index.test.ts +0 -116
  91. package/src/metrics/index.ts +0 -111
  92. package/src/middleware/authMiddleware.test.ts +0 -275
  93. package/src/middleware/authMiddleware.ts +0 -91
  94. package/src/middleware/corsMiddleware.test.ts +0 -135
  95. package/src/middleware/corsMiddleware.ts +0 -65
  96. package/src/middleware/errorHandler.test.ts +0 -188
  97. package/src/middleware/errorHandler.ts +0 -103
  98. package/src/middleware/internalServiceAuth.test.ts +0 -173
  99. package/src/middleware/internalServiceAuth.ts +0 -96
  100. package/src/middleware/requestLogger.test.ts +0 -81
  101. package/src/middleware/requestLogger.ts +0 -48
  102. package/src/middleware/security.test.ts +0 -45
  103. package/src/middleware/security.ts +0 -43
  104. package/src/middleware/validate.test.ts +0 -72
  105. package/src/middleware/validate.ts +0 -23
  106. package/src/oauth/index.ts +0 -29
  107. package/src/oauth/models.ts +0 -164
  108. package/src/oauth/service.test.ts +0 -432
  109. package/src/oauth/service.ts +0 -299
  110. package/src/oauth/tokens.test.ts +0 -146
  111. package/src/oauth/tokens.ts +0 -186
  112. package/src/testing/serviceTestSetup.ts +0 -17
  113. package/src/tracing/index.test.ts +0 -272
  114. package/src/tracing/index.ts +0 -110
  115. package/src/types/auth.ts +0 -45
  116. package/src/utils/correlation.test.ts +0 -47
  117. package/src/utils/correlation.ts +0 -22
  118. package/src/utils/permissionUtils.test.ts +0 -47
  119. package/src/utils/permissionUtils.ts +0 -68
  120. package/src/utils/response.test.ts +0 -64
  121. package/src/utils/response.ts +0 -60
  122. package/tsconfig.build.json +0 -7
  123. package/tsconfig.json +0 -23
@@ -1,173 +0,0 @@
1
- import { Request, Response } from 'express';
2
-
3
- jest.mock('../logging/logger', () => ({
4
- __esModule: true,
5
- default: { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn(), http: jest.fn() }
6
- }));
7
-
8
- import {
9
- allowUserOrInternalService,
10
- internalServiceTokenBypass,
11
- requireInternalServiceToken,
12
- verifyInternalToken
13
- } from './internalServiceAuth';
14
-
15
- function mockRes(): Response {
16
- const res: Partial<Response> = {};
17
- res.status = jest.fn().mockReturnValue(res);
18
- res.json = jest.fn().mockReturnValue(res);
19
- return res as Response;
20
- }
21
-
22
- function mockReq(headers: Record<string, string> = {}): Request {
23
- return { headers, originalUrl: '/internal/entitlements' } as unknown as Request;
24
- }
25
-
26
- const ORIGINAL_ENV = process.env;
27
-
28
- beforeEach(() => {
29
- process.env = { ...ORIGINAL_ENV, INTERNAL_SERVICE_TOKEN: 'test-internal-token' };
30
- });
31
-
32
- afterAll(() => {
33
- process.env = ORIGINAL_ENV;
34
- });
35
-
36
- describe('verifyInternalToken', () => {
37
- it('accepts only an exact match', () => {
38
- expect(verifyInternalToken('test-internal-token')).toBe(true);
39
- expect(verifyInternalToken('test-internal-tokeN')).toBe(false);
40
- });
41
-
42
- it('rejects a prefix or an extension without throwing on the length mismatch', () => {
43
- expect(verifyInternalToken('test')).toBe(false);
44
- expect(verifyInternalToken('test-internal-token-and-more')).toBe(false);
45
- });
46
-
47
- it('rejects anything that is not a non-empty string', () => {
48
- expect(verifyInternalToken(undefined)).toBe(false);
49
- expect(verifyInternalToken('')).toBe(false);
50
- expect(verifyInternalToken(['test-internal-token'])).toBe(false);
51
- });
52
-
53
- it('rejects everything when no token is configured', () => {
54
- delete process.env.INTERNAL_SERVICE_TOKEN;
55
-
56
- expect(verifyInternalToken('test-internal-token')).toBe(false);
57
- });
58
- });
59
-
60
- describe('requireInternalServiceToken', () => {
61
- it('marks the request internal and continues on a valid token', () => {
62
- const req = mockReq({ 'x-internal-token': 'test-internal-token', 'x-service-id': 'album-service' });
63
- const next = jest.fn();
64
-
65
- requireInternalServiceToken(req, mockRes(), next);
66
-
67
- expect(req.isInternalService).toBe(true);
68
- expect(req.serviceIdentifier).toBe('album-service');
69
- expect(next).toHaveBeenCalledTimes(1);
70
- });
71
-
72
- it('defaults the service identifier when the header is absent', () => {
73
- const req = mockReq({ 'x-internal-token': 'test-internal-token' });
74
-
75
- requireInternalServiceToken(req, mockRes(), jest.fn());
76
-
77
- expect(req.serviceIdentifier).toBe('unknown');
78
- });
79
-
80
- it('rejects a missing token with 401', () => {
81
- const res = mockRes();
82
- const next = jest.fn();
83
-
84
- requireInternalServiceToken(mockReq(), res, next);
85
-
86
- expect(res.status).toHaveBeenCalledWith(401);
87
- expect(next).not.toHaveBeenCalled();
88
- });
89
-
90
- it('rejects a wrong token with 401', () => {
91
- const res = mockRes();
92
- const next = jest.fn();
93
-
94
- requireInternalServiceToken(mockReq({ 'x-internal-token': 'wrong' }), res, next);
95
-
96
- expect(res.status).toHaveBeenCalledWith(401);
97
- expect(next).not.toHaveBeenCalled();
98
- });
99
-
100
- it('fails with 500 rather than open when the deployment has no token configured', () => {
101
- delete process.env.INTERNAL_SERVICE_TOKEN;
102
- const res = mockRes();
103
- const next = jest.fn();
104
-
105
- requireInternalServiceToken(mockReq({ 'x-internal-token': 'anything' }), res, next);
106
-
107
- expect(res.status).toHaveBeenCalledWith(500);
108
- expect(next).not.toHaveBeenCalled();
109
- });
110
- });
111
-
112
- describe('internalServiceTokenBypass', () => {
113
- it('passes a request with no internal header through untouched, for normal user auth', () => {
114
- const req = mockReq();
115
- const next = jest.fn();
116
-
117
- internalServiceTokenBypass(req, mockRes(), next);
118
-
119
- expect(next).toHaveBeenCalledTimes(1);
120
- expect(req.isInternalService).toBeUndefined();
121
- });
122
-
123
- it('rejects a bogus token instead of letting it stand in for authentication', () => {
124
- // The bug this replaces: the header's mere presence used to skip JWT
125
- // verification, so `x-internal-token: anything` reached the controller
126
- // unauthenticated on any route that did not mount a second middleware.
127
- const res = mockRes();
128
- const next = jest.fn();
129
-
130
- internalServiceTokenBypass(mockReq({ 'x-internal-token': 'anything' }), res, next);
131
-
132
- expect(res.status).toHaveBeenCalledWith(401);
133
- expect(next).not.toHaveBeenCalled();
134
- });
135
-
136
- it('marks a valid internal call and continues', () => {
137
- const req = mockReq({ 'x-internal-token': 'test-internal-token' });
138
- const next = jest.fn();
139
-
140
- internalServiceTokenBypass(req, mockRes(), next);
141
-
142
- expect(req.isInternalService).toBe(true);
143
- expect(next).toHaveBeenCalledTimes(1);
144
- });
145
- });
146
-
147
- describe('allowUserOrInternalService', () => {
148
- it('allows an internal service', () => {
149
- const next = jest.fn();
150
-
151
- allowUserOrInternalService({ isInternalService: true } as Request, mockRes(), next);
152
-
153
- expect(next).toHaveBeenCalledTimes(1);
154
- });
155
-
156
- it('allows an authenticated user', () => {
157
- const next = jest.fn();
158
-
159
- allowUserOrInternalService({ user: { email: 'u@example.com' } } as Request, mockRes(), next);
160
-
161
- expect(next).toHaveBeenCalledTimes(1);
162
- });
163
-
164
- it('rejects anyone else with 401', () => {
165
- const res = mockRes();
166
- const next = jest.fn();
167
-
168
- allowUserOrInternalService({} as Request, res, next);
169
-
170
- expect(res.status).toHaveBeenCalledWith(401);
171
- expect(next).not.toHaveBeenCalled();
172
- });
173
- });
@@ -1,96 +0,0 @@
1
- import { Request, RequestHandler } from 'express';
2
- import { timingSafeEqual } from 'crypto';
3
- import logger from '../logging/logger';
4
-
5
- /**
6
- * Fields set by the internal-service middlewares below. Declared globally so
7
- * routers and controllers read `req.isInternalService` off the same `Request`
8
- * type Express itself uses, with no cast at the call site.
9
- */
10
- declare global {
11
- namespace Express {
12
- interface Request {
13
- isInternalService?: boolean;
14
- serviceIdentifier?: string;
15
- }
16
- }
17
- }
18
-
19
- export const INTERNAL_TOKEN_HEADER = 'x-internal-token';
20
- export const SERVICE_ID_HEADER = 'x-service-id';
21
-
22
- /**
23
- * Constant-time comparison of the presented token against the configured one.
24
- *
25
- * `===` on secrets leaks their prefix through timing, and `timingSafeEqual`
26
- * throws on a length mismatch (which is itself a leak), so length is compared
27
- * first and both buffers are hashed to a fixed width — a token of the wrong
28
- * length takes the same path as one of the right length with wrong bytes.
29
- */
30
- export function verifyInternalToken(presented: unknown): boolean {
31
- const expected = process.env.INTERNAL_SERVICE_TOKEN;
32
- if (!expected) return false;
33
- if (typeof presented !== 'string' || presented.length === 0) return false;
34
-
35
- const a = Buffer.from(presented, 'utf8');
36
- const b = Buffer.from(expected, 'utf8');
37
- if (a.length !== b.length) return false;
38
- return timingSafeEqual(a, b);
39
- }
40
-
41
- function markInternal(req: Request): void {
42
- req.isInternalService = true;
43
- req.serviceIdentifier = (req.headers[SERVICE_ID_HEADER] as string) || 'unknown';
44
- }
45
-
46
- /**
47
- * Hard gate for endpoints that are only ever called service-to-service.
48
- * No user-JWT fallback: a missing or wrong token is always a 401.
49
- */
50
- export const requireInternalServiceToken: RequestHandler = (req, res, next) => {
51
- if (!process.env.INTERNAL_SERVICE_TOKEN) {
52
- logger.error('INTERNAL_SERVICE_TOKEN not configured');
53
- res.status(500).json({ success: false, message: 'Internal service authentication not configured' });
54
- return;
55
- }
56
-
57
- if (!verifyInternalToken(req.headers[INTERNAL_TOKEN_HEADER])) {
58
- logger.warn('Rejected internal service request', {
59
- serviceId: (req.headers[SERVICE_ID_HEADER] as string) || 'unknown',
60
- path: req.originalUrl
61
- });
62
- res.status(401).json({ success: false, message: 'Invalid or missing internal service token' });
63
- return;
64
- }
65
-
66
- markInternal(req);
67
- next();
68
- };
69
-
70
- /**
71
- * For routers that serve both browsers and services: when the internal-token
72
- * header is present it is *verified here*, and a bad token is rejected rather
73
- * than passed along.
74
- *
75
- * The bug this replaces: treating the mere presence of the header as grounds to
76
- * skip JWT verification, leaving the real check to a second middleware mounted
77
- * per-router — so any route that forgot to mount the second one was reachable
78
- * by sending `x-internal-token: anything`.
79
- */
80
- export const internalServiceTokenBypass: RequestHandler = (req, res, next) => {
81
- const presented = req.headers[INTERNAL_TOKEN_HEADER];
82
- if (presented === undefined) {
83
- next();
84
- return;
85
- }
86
- requireInternalServiceToken(req, res, next);
87
- };
88
-
89
- /** Allows a request that authenticated as either a user or an internal service. */
90
- export const allowUserOrInternalService: RequestHandler = (req, res, next) => {
91
- if (req.isInternalService || req.user) {
92
- next();
93
- return;
94
- }
95
- res.status(401).json({ success: false, message: 'Authentication required' });
96
- };
@@ -1,81 +0,0 @@
1
- jest.mock('../logging/logger', () => ({
2
- __esModule: true,
3
- default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), http: jest.fn() }
4
- }));
5
-
6
- import logger from '../logging/logger';
7
- import { requestLogger, requestLoggerWithMetrics, simpleRequestLogger } from './requestLogger';
8
-
9
- function mockReqRes() {
10
- const req: any = {
11
- method: 'GET',
12
- url: '/albums/123',
13
- correlationId: 'corr-1',
14
- ip: '127.0.0.1',
15
- get: jest.fn().mockReturnValue('test-agent')
16
- };
17
- const res: any = { statusCode: 200, on: jest.fn() };
18
- return { req, res };
19
- }
20
-
21
- describe('simpleRequestLogger', () => {
22
- beforeEach(() => jest.clearAllMocks());
23
-
24
- it('logs an inbound request line with correlation id, ip, and user agent', () => {
25
- const { req, res } = mockReqRes();
26
- const next = jest.fn();
27
-
28
- simpleRequestLogger(req, res, next);
29
-
30
- expect(logger.info).toHaveBeenCalledWith('→ GET /albums/123', {
31
- correlationId: 'corr-1',
32
- ip: '127.0.0.1',
33
- userAgent: 'test-agent'
34
- });
35
- expect(next).toHaveBeenCalledTimes(1);
36
- });
37
-
38
- it('falls back to "unknown" correlation id when none was set on the request', () => {
39
- const { req, res } = mockReqRes();
40
- delete req.correlationId;
41
- const next = jest.fn();
42
-
43
- simpleRequestLogger(req, res, next);
44
-
45
- expect(logger.info).toHaveBeenCalledWith(
46
- expect.any(String),
47
- expect.objectContaining({ correlationId: 'unknown' })
48
- );
49
- });
50
-
51
- it('logs the outbound response line with status and duration once the response finishes', () => {
52
- const { req, res } = mockReqRes();
53
- const next = jest.fn();
54
-
55
- simpleRequestLogger(req, res, next);
56
-
57
- expect(res.on).toHaveBeenCalledWith('finish', expect.any(Function));
58
- const finishHandler = res.on.mock.calls.find((c: any[]) => c[0] === 'finish')[1];
59
- res.statusCode = 200;
60
- finishHandler();
61
-
62
- expect(logger.info).toHaveBeenCalledWith(
63
- expect.stringMatching(/^← GET \/albums\/123 200 \d+ms$/),
64
- expect.objectContaining({ correlationId: 'corr-1', statusCode: 200 })
65
- );
66
- });
67
- });
68
-
69
- describe('requestLogger', () => {
70
- it('is a Morgan middleware function', () => {
71
- expect(typeof requestLogger).toBe('function');
72
- });
73
- });
74
-
75
- describe('requestLoggerWithMetrics', () => {
76
- it('bundles requestLogger and metricsMiddleware as a two-item array', () => {
77
- expect(requestLoggerWithMetrics).toHaveLength(2);
78
- expect(requestLoggerWithMetrics[0]).toBe(requestLogger);
79
- expect(typeof requestLoggerWithMetrics[1]).toBe('function');
80
- });
81
- });
@@ -1,48 +0,0 @@
1
- import morgan from 'morgan';
2
- import { Request, Response, NextFunction } from 'express';
3
- import logger from '../logging/logger';
4
- import { metricsMiddleware } from '../metrics';
5
-
6
- /**
7
- * Morgan middleware configured to use Winston logger
8
- * Logs HTTP requests in structured format
9
- */
10
- export const requestLogger = morgan('combined', {
11
- stream: {
12
- write: (message: string) => {
13
- logger.http(message.trim());
14
- }
15
- }
16
- });
17
-
18
- /**
19
- * Combined middleware that includes both logging and metrics
20
- * Use this instead of separate requestLogger and metricsMiddleware
21
- */
22
- export const requestLoggerWithMetrics = [requestLogger, metricsMiddleware];
23
-
24
- /**
25
- * Simple request logger for development
26
- * Logs basic request info with correlation ID
27
- */
28
- export const simpleRequestLogger = (req: Request, res: Response, next: NextFunction) => {
29
- const start = Date.now();
30
- const correlationId = req.correlationId || 'unknown';
31
-
32
- logger.info(`→ ${req.method} ${req.url}`, {
33
- correlationId,
34
- ip: req.ip,
35
- userAgent: req.get('User-Agent')
36
- });
37
-
38
- res.on('finish', () => {
39
- const duration = Date.now() - start;
40
- logger.info(`← ${req.method} ${req.url} ${res.statusCode} ${duration}ms`, {
41
- correlationId,
42
- statusCode: res.statusCode,
43
- duration
44
- });
45
- });
46
-
47
- next();
48
- };
@@ -1,45 +0,0 @@
1
- import express from 'express';
2
- import request from 'supertest';
3
- import { securityHeaders, createRateLimiter } from './security';
4
-
5
- describe('securityHeaders', () => {
6
- const app = express();
7
- app.use(securityHeaders);
8
- app.get('/ping', (_req, res) => res.json({ ok: true }));
9
-
10
- it('sets standard helmet protections', async () => {
11
- const res = await request(app).get('/ping');
12
-
13
- expect(res.headers['x-content-type-options']).toBe('nosniff');
14
- expect(res.headers['x-frame-options']).toBe('SAMEORIGIN');
15
- });
16
-
17
- it('relaxes cross-origin-resource-policy so other-origin frontends can read the response', async () => {
18
- const res = await request(app).get('/ping');
19
-
20
- expect(res.headers['cross-origin-resource-policy']).toBe('cross-origin');
21
- });
22
-
23
- it('does not set a Content-Security-Policy header (JSON API, not HTML)', async () => {
24
- const res = await request(app).get('/ping');
25
-
26
- expect(res.headers['content-security-policy']).toBeUndefined();
27
- });
28
- });
29
-
30
- describe('createRateLimiter', () => {
31
- it('allows requests under the limit and blocks once the limit is exceeded', async () => {
32
- const app = express();
33
- app.use(createRateLimiter({ windowMs: 60_000, max: 2 }));
34
- app.get('/ping', (_req, res) => res.json({ ok: true }));
35
-
36
- const first = await request(app).get('/ping');
37
- const second = await request(app).get('/ping');
38
- const third = await request(app).get('/ping');
39
-
40
- expect(first.status).toBe(200);
41
- expect(second.status).toBe(200);
42
- expect(third.status).toBe(429);
43
- expect(third.body).toEqual({ success: false, message: 'Too many requests, please try again later' });
44
- });
45
- });
@@ -1,43 +0,0 @@
1
- import helmet from 'helmet';
2
- import rateLimit, { Options as RateLimitOptions } from 'express-rate-limit';
3
-
4
- /**
5
- * Standard security headers (helmet) for all services.
6
- *
7
- * - `contentSecurityPolicy` is off: these are JSON APIs, not HTML apps, and a
8
- * couple of services serve a static Swagger docs page under `/api/docs`
9
- * whose inline scripts a default CSP would break.
10
- * - `crossOriginResourcePolicy` is relaxed to `cross-origin`: helmet's default
11
- * (`same-origin`) makes browsers refuse to read the response from a
12
- * different origin — which every frontend is, since each front and backend
13
- * service runs on its own port. CORS (see `createCorsMiddleware`) already
14
- * does origin enforcement; this header must not fight it.
15
- */
16
- export const securityHeaders = helmet({
17
- contentSecurityPolicy: false,
18
- crossOriginResourcePolicy: { policy: 'cross-origin' },
19
- crossOriginEmbedderPolicy: false
20
- });
21
-
22
- /**
23
- * Build a rate limiter with sane JSON error output. Counts per client IP.
24
- */
25
- export function createRateLimiter(options: Partial<RateLimitOptions> = {}) {
26
- return rateLimit({
27
- windowMs: 15 * 60 * 1000,
28
- max: 300,
29
- standardHeaders: true,
30
- legacyHeaders: false,
31
- message: { success: false, message: 'Too many requests, please try again later' },
32
- ...options
33
- });
34
- }
35
-
36
- /** Baseline limiter for authenticated/general routes. */
37
- export const standardRateLimiter = createRateLimiter();
38
-
39
- /**
40
- * Tighter limiter for brute-force-prone unauthenticated routes (token
41
- * issuance, public endpoints with no auth in front of them).
42
- */
43
- export const strictRateLimiter = createRateLimiter({ max: 20 });
@@ -1,72 +0,0 @@
1
- import express from 'express';
2
- import request from 'supertest';
3
- import { z } from 'zod';
4
- import { validate } from './validate';
5
- import { errorHandler } from './errorHandler';
6
-
7
- function buildApp(schema: z.ZodType) {
8
- const app = express();
9
- app.use(express.json());
10
- app.post('/things', validate(schema), (req, res) => {
11
- res.json({ success: true, data: req.body });
12
- });
13
- app.use(errorHandler);
14
- return app;
15
- }
16
-
17
- describe('validate', () => {
18
- const schema = z.object({
19
- name: z.string().min(1),
20
- age: z.number().int().positive().optional()
21
- });
22
-
23
- it('calls the handler with the parsed body when validation passes', async () => {
24
- const app = buildApp(schema);
25
-
26
- const res = await request(app).post('/things').send({ name: 'Alice', age: 30 });
27
-
28
- expect(res.status).toBe(200);
29
- expect(res.body).toEqual({ success: true, data: { name: 'Alice', age: 30 } });
30
- });
31
-
32
- it('responds 400 via the shared errorHandler when a required field is missing', async () => {
33
- const app = buildApp(schema);
34
-
35
- const res = await request(app).post('/things').send({});
36
-
37
- expect(res.status).toBe(400);
38
- expect(res.body.success).toBe(false);
39
- expect(res.body.error).toBe('BadRequestError');
40
- expect(res.body.message).toContain('name');
41
- });
42
-
43
- it('responds 400 when a field has the wrong type', async () => {
44
- const app = buildApp(schema);
45
-
46
- const res = await request(app).post('/things').send({ name: 'Bob', age: 'not-a-number' });
47
-
48
- expect(res.status).toBe(400);
49
- expect(res.body.error).toBe('BadRequestError');
50
- });
51
-
52
- it('strips unknown fields not declared in the schema (zod default behavior)', async () => {
53
- const app = buildApp(schema);
54
-
55
- const res = await request(app).post('/things').send({ name: 'Carol', extra: 'nope' });
56
-
57
- expect(res.status).toBe(200);
58
- expect(res.body.data).toEqual({ name: 'Carol' });
59
- });
60
-
61
- it('omits the field prefix when the zod issue has no path (whole-body validation)', async () => {
62
- const arraySchema = z.array(z.string());
63
- const app = buildApp(arraySchema);
64
- const rawResult = arraySchema.safeParse({ not: 'an array' });
65
-
66
- const res = await request(app).post('/things').send({ not: 'an array' });
67
-
68
- expect(res.status).toBe(400);
69
- expect(rawResult.success).toBe(false);
70
- expect(res.body.message).toBe(!rawResult.success ? rawResult.error.issues[0].message : undefined);
71
- });
72
- });
@@ -1,23 +0,0 @@
1
- import { Request, Response, NextFunction, RequestHandler } from 'express';
2
- import { ZodType } from 'zod';
3
- import { BadRequestError } from '../errors/HttpError';
4
-
5
- /**
6
- * Validates `req.body` against a zod schema before the route handler runs.
7
- * On failure, throws a `BadRequestError` (caught by the shared errorHandler)
8
- * instead of letting controllers hand-roll their own field checks. On
9
- * success, `req.body` is replaced with the parsed result so handlers get
10
- * zod's inferred, coerced types rather than raw untyped input.
11
- */
12
- export function validate(schema: ZodType): RequestHandler {
13
- return (req: Request, _res: Response, next: NextFunction): void => {
14
- const result = schema.safeParse(req.body);
15
- if (!result.success) {
16
- const issue = result.error.issues[0];
17
- const field = issue.path.length ? `${issue.path.join('.')}: ` : '';
18
- throw new BadRequestError(`${field}${issue.message}`);
19
- }
20
- req.body = result.data;
21
- next();
22
- };
23
- }
@@ -1,29 +0,0 @@
1
- export { OAuthClient, AuthorizationCode, RefreshToken } from './models';
2
- export type { IOAuthClient, IAuthorizationCode, IRefreshToken } from './models';
3
- export {
4
- registerClient,
5
- findClient,
6
- isRegisteredRedirect,
7
- issueAuthorizationCode,
8
- redeemAuthorizationCode,
9
- issueRefreshToken,
10
- redeemRefreshToken,
11
- revokeRefreshTokensForUser,
12
- listConnections
13
- } from './service';
14
- export type {
15
- RegisterClientInput,
16
- RegisteredClient,
17
- IssueCodeInput,
18
- CodeRedemption,
19
- CodeRejection,
20
- RefreshRedemption
21
- } from './service';
22
- export {
23
- mintAccessToken,
24
- verifyAccessToken,
25
- parseScopes,
26
- isAccessTokenClaims,
27
- ACCESS_TOKEN_TYPE
28
- } from './tokens';
29
- export type { AccessTokenClaims, MintAccessTokenInput, MintedAccessToken, AccessTokenVerification } from './tokens';