@tumbaland/backend-core 1.44.0 → 1.45.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 (101) hide show
  1. package/package.json +5 -1
  2. package/.versionrc.json +0 -7
  3. package/__mocks__/uuid.js +0 -8
  4. package/jest.config.js +0 -24
  5. package/src/apiKeys/ApiKey.test.ts +0 -142
  6. package/src/apiKeys/ApiKey.ts +0 -102
  7. package/src/apiKeys/crypto.test.ts +0 -161
  8. package/src/apiKeys/crypto.ts +0 -163
  9. package/src/apiKeys/index.test.ts +0 -72
  10. package/src/apiKeys/index.ts +0 -35
  11. package/src/apiKeys/middleware.test.ts +0 -651
  12. package/src/apiKeys/middleware.ts +0 -341
  13. package/src/apiKeys/service.test.ts +0 -401
  14. package/src/apiKeys/service.ts +0 -168
  15. package/src/apiKeys/types.test.ts +0 -41
  16. package/src/apiKeys/types.ts +0 -124
  17. package/src/app/createBaseApp.test.ts +0 -109
  18. package/src/app/createBaseApp.ts +0 -102
  19. package/src/app/shutdown.test.ts +0 -129
  20. package/src/app/shutdown.ts +0 -81
  21. package/src/audit/AuditEvent.ts +0 -123
  22. package/src/audit/actor.test.ts +0 -95
  23. package/src/audit/actor.ts +0 -68
  24. package/src/audit/context.test.ts +0 -91
  25. package/src/audit/context.ts +0 -83
  26. package/src/audit/index.ts +0 -11
  27. package/src/audit/plugin.test.ts +0 -258
  28. package/src/audit/plugin.ts +0 -254
  29. package/src/audit/reads.test.ts +0 -164
  30. package/src/audit/reads.ts +0 -88
  31. package/src/audit/service.test.ts +0 -115
  32. package/src/audit/service.ts +0 -95
  33. package/src/auth/session.test.ts +0 -89
  34. package/src/auth/session.ts +0 -75
  35. package/src/config/env.test.ts +0 -28
  36. package/src/config/env.ts +0 -13
  37. package/src/database/connection.test.ts +0 -188
  38. package/src/database/connection.ts +0 -90
  39. package/src/entitlements/UsageMeter.ts +0 -49
  40. package/src/entitlements/client.test.ts +0 -200
  41. package/src/entitlements/client.ts +0 -179
  42. package/src/entitlements/definitions.test.ts +0 -161
  43. package/src/entitlements/definitions.ts +0 -268
  44. package/src/entitlements/index.ts +0 -42
  45. package/src/entitlements/middleware.test.ts +0 -196
  46. package/src/entitlements/middleware.ts +0 -150
  47. package/src/entitlements/reconcile.test.ts +0 -333
  48. package/src/entitlements/reconcile.ts +0 -384
  49. package/src/entitlements/types.ts +0 -21
  50. package/src/entitlements/usage.test.ts +0 -314
  51. package/src/entitlements/usage.ts +0 -223
  52. package/src/errors/HttpError.test.ts +0 -76
  53. package/src/errors/HttpError.ts +0 -91
  54. package/src/groups/client.test.ts +0 -215
  55. package/src/groups/client.ts +0 -182
  56. package/src/groups/index.ts +0 -7
  57. package/src/groups/membership.test.ts +0 -84
  58. package/src/groups/membership.ts +0 -133
  59. package/src/groups/subject.test.ts +0 -85
  60. package/src/groups/subject.ts +0 -50
  61. package/src/health/createHealthCheck.test.ts +0 -89
  62. package/src/health/createHealthCheck.ts +0 -67
  63. package/src/health/healthController.test.ts +0 -113
  64. package/src/health/healthController.ts +0 -56
  65. package/src/index.ts +0 -88
  66. package/src/logging/logger.test.ts +0 -91
  67. package/src/logging/logger.ts +0 -103
  68. package/src/metrics/index.test.ts +0 -116
  69. package/src/metrics/index.ts +0 -111
  70. package/src/middleware/authMiddleware.test.ts +0 -275
  71. package/src/middleware/authMiddleware.ts +0 -91
  72. package/src/middleware/corsMiddleware.test.ts +0 -135
  73. package/src/middleware/corsMiddleware.ts +0 -65
  74. package/src/middleware/errorHandler.test.ts +0 -188
  75. package/src/middleware/errorHandler.ts +0 -103
  76. package/src/middleware/internalServiceAuth.test.ts +0 -173
  77. package/src/middleware/internalServiceAuth.ts +0 -96
  78. package/src/middleware/requestLogger.test.ts +0 -81
  79. package/src/middleware/requestLogger.ts +0 -48
  80. package/src/middleware/security.test.ts +0 -45
  81. package/src/middleware/security.ts +0 -43
  82. package/src/middleware/validate.test.ts +0 -72
  83. package/src/middleware/validate.ts +0 -23
  84. package/src/oauth/index.ts +0 -29
  85. package/src/oauth/models.ts +0 -164
  86. package/src/oauth/service.test.ts +0 -432
  87. package/src/oauth/service.ts +0 -299
  88. package/src/oauth/tokens.test.ts +0 -146
  89. package/src/oauth/tokens.ts +0 -186
  90. package/src/testing/serviceTestSetup.ts +0 -17
  91. package/src/tracing/index.test.ts +0 -272
  92. package/src/tracing/index.ts +0 -110
  93. package/src/types/auth.ts +0 -45
  94. package/src/utils/correlation.test.ts +0 -47
  95. package/src/utils/correlation.ts +0 -22
  96. package/src/utils/permissionUtils.test.ts +0 -47
  97. package/src/utils/permissionUtils.ts +0 -68
  98. package/src/utils/response.test.ts +0 -64
  99. package/src/utils/response.ts +0 -60
  100. package/tsconfig.build.json +0 -7
  101. package/tsconfig.json +0 -23
@@ -1,272 +0,0 @@
1
- /**
2
- * The real OpenTelemetry SDK/exporter/provider are mocked out — they're
3
- * network- and global-registry-heavy, and createBaseApp.test.ts already
4
- * exercises the real tracingMiddleware happy path end-to-end via supertest.
5
- * This file focuses on this module's own branching logic (env-var
6
- * resolution, parent-span-vs-context handling, the helper wrappers), using
7
- * the real (side-effect-free) @opentelemetry/api for trace/context/propagation.
8
- */
9
- // A partial span: only the members these tests exercise are stubbed, cast to
10
- // the full Span type so it satisfies the now-typed tracing helpers. The jest
11
- // mocks remain reachable at runtime for the assertions below.
12
- const mockSpan = () =>
13
- ({
14
- setAttribute: jest.fn(),
15
- addEvent: jest.fn(),
16
- end: jest.fn(),
17
- spanContext: jest.fn(() => ({ traceId: 't', spanId: 's', traceFlags: 1 }))
18
- }) as unknown as Span;
19
-
20
- const mockTracer = { startSpan: jest.fn(() => mockSpan()) };
21
- const registerMock = jest.fn();
22
- const getTracerMock = jest.fn(() => mockTracer);
23
- const providerCtorMock = jest.fn().mockImplementation((opts: unknown) => ({
24
- __opts: opts,
25
- register: registerMock,
26
- getTracer: getTracerMock
27
- }));
28
- const exporterCtorMock = jest.fn().mockImplementation((opts: unknown) => ({ __opts: opts }));
29
- const spanProcessorCtorMock = jest.fn();
30
- const resourceFromAttributesMock = jest.fn((attrs: unknown) => attrs);
31
-
32
- jest.mock('@opentelemetry/sdk-trace-node', () => ({
33
- NodeTracerProvider: providerCtorMock,
34
- BatchSpanProcessor: spanProcessorCtorMock
35
- }));
36
-
37
- jest.mock('@opentelemetry/exporter-trace-otlp-http', () => ({
38
- OTLPTraceExporter: exporterCtorMock
39
- }));
40
-
41
- jest.mock('@opentelemetry/resources', () => ({
42
- resourceFromAttributes: resourceFromAttributesMock
43
- }));
44
-
45
- import { context, Span } from '@opentelemetry/api';
46
- import { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
47
-
48
- async function loadTracing() {
49
- let tracing!: typeof import('./index');
50
- await jest.isolateModulesAsync(async () => {
51
- tracing = await import('./index');
52
- });
53
- return tracing;
54
- }
55
-
56
- beforeEach(() => {
57
- jest.clearAllMocks();
58
- });
59
-
60
- describe('initTracer', () => {
61
- const ORIGINAL_ENV = process.env;
62
-
63
- beforeEach(() => {
64
- process.env = { ...ORIGINAL_ENV };
65
- delete process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
66
- delete process.env.JAEGER_ENDPOINT;
67
- });
68
-
69
- afterAll(() => {
70
- process.env = ORIGINAL_ENV;
71
- });
72
-
73
- it('defaults to the in-cluster collector endpoint when no env var is set', async () => {
74
- const { initTracer } = await loadTracing();
75
-
76
- initTracer('album-service');
77
-
78
- expect(exporterCtorMock).toHaveBeenCalledWith({ url: 'http://jaeger-collector:4318/v1/traces' });
79
- });
80
-
81
- it('prefers OTEL_EXPORTER_OTLP_TRACES_ENDPOINT over JAEGER_ENDPOINT', async () => {
82
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://otel-endpoint/v1/traces';
83
- process.env.JAEGER_ENDPOINT = 'http://jaeger-endpoint/v1/traces';
84
- const { initTracer } = await loadTracing();
85
-
86
- initTracer('album-service');
87
-
88
- expect(exporterCtorMock).toHaveBeenCalledWith({ url: 'http://otel-endpoint/v1/traces' });
89
- });
90
-
91
- it('falls back to JAEGER_ENDPOINT when OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is unset', async () => {
92
- process.env.JAEGER_ENDPOINT = 'http://jaeger-endpoint/v1/traces';
93
- const { initTracer } = await loadTracing();
94
-
95
- initTracer('album-service');
96
-
97
- expect(exporterCtorMock).toHaveBeenCalledWith({ url: 'http://jaeger-endpoint/v1/traces' });
98
- });
99
-
100
- it('falls back to version 1.0.0 and environment "development" when their env vars are unset', async () => {
101
- const merged: Record<string, string | undefined> = { ...process.env };
102
- delete merged.npm_package_version;
103
- delete merged.NODE_ENV;
104
- process.env = merged as NodeJS.ProcessEnv;
105
-
106
- const { initTracer } = await loadTracing();
107
- initTracer('album-service');
108
-
109
- expect(resourceFromAttributesMock).toHaveBeenCalledWith(
110
- expect.objectContaining({
111
- [ATTR_SERVICE_VERSION]: '1.0.0',
112
- 'deployment.environment': 'development'
113
- })
114
- );
115
- });
116
-
117
- it('registers the provider and returns a tracer scoped to the given service name', async () => {
118
- const { initTracer } = await loadTracing();
119
-
120
- const tracer = initTracer('album-service');
121
-
122
- expect(registerMock).toHaveBeenCalledTimes(1);
123
- expect(getTracerMock).toHaveBeenCalledWith('album-service');
124
- expect(tracer).toBe(mockTracer);
125
- });
126
- });
127
-
128
- describe('getTracer', () => {
129
- it('lazily initializes on first call using SERVICE_NAME (or "unknown-service")', async () => {
130
- const { getTracer } = await loadTracing();
131
-
132
- getTracer();
133
-
134
- expect(getTracerMock).toHaveBeenCalledWith('unknown-service');
135
- });
136
-
137
- it('caches the tracer across calls instead of re-initializing', async () => {
138
- const { getTracer } = await loadTracing();
139
-
140
- getTracer();
141
- getTracer();
142
-
143
- expect(providerCtorMock).toHaveBeenCalledTimes(1);
144
- });
145
- });
146
-
147
- describe('startSpan', () => {
148
- it('starts a root span when no parent is given', async () => {
149
- const { startSpan } = await loadTracing();
150
-
151
- startSpan('op');
152
-
153
- expect(mockTracer.startSpan).toHaveBeenCalledWith('op');
154
- });
155
-
156
- it('starts a child span from a real Span parent (has spanContext()), deriving a parent context from it', async () => {
157
- const { startSpan } = await loadTracing();
158
- const parentSpan = mockSpan();
159
-
160
- startSpan('op', parentSpan);
161
-
162
- const [name, undef, parentContext] = mockTracer.startSpan.mock.calls[0] as unknown as [string, undefined, any];
163
- expect(name).toBe('op');
164
- expect(undef).toBeUndefined();
165
- // trace.setSpan(context.active(), parentSpan) returns a real Context, not the bare parentSpan.
166
- expect(parentContext).not.toBe(parentSpan);
167
- expect(typeof parentContext.getValue).toBe('function');
168
- });
169
-
170
- it('starts a span from a bare Context parent (no spanContext())', async () => {
171
- const { startSpan } = await loadTracing();
172
- const parentContext = context.active();
173
-
174
- startSpan('op', parentContext);
175
-
176
- expect(mockTracer.startSpan).toHaveBeenCalledWith('op', undefined, parentContext);
177
- });
178
- });
179
-
180
- describe('createChildSpan', () => {
181
- it('delegates to startSpan with the given parent', async () => {
182
- const { createChildSpan } = await loadTracing();
183
- const parentSpan = mockSpan();
184
-
185
- createChildSpan('child-op', parentSpan);
186
-
187
- expect(mockTracer.startSpan).toHaveBeenCalledWith('child-op', undefined, expect.anything());
188
- });
189
- });
190
-
191
- describe('logToSpan', () => {
192
- it('adds an event with data to the given span', async () => {
193
- const { logToSpan } = await loadTracing();
194
- const span = mockSpan();
195
-
196
- logToSpan(span, 'cache-miss', { key: 'abc' });
197
-
198
- expect(span.addEvent).toHaveBeenCalledWith('cache-miss', { key: 'abc' });
199
- });
200
- });
201
-
202
- describe('setSpanTag', () => {
203
- it('sets an attribute on the given span', async () => {
204
- const { setSpanTag } = await loadTracing();
205
- const span = mockSpan();
206
-
207
- setSpanTag(span, 'user.id', '42');
208
-
209
- expect(span.setAttribute).toHaveBeenCalledWith('user.id', '42');
210
- });
211
- });
212
-
213
- describe('injectHeaders', () => {
214
- it('returns a headers object without throwing', async () => {
215
- const { injectHeaders } = await loadTracing();
216
- const span = mockSpan();
217
-
218
- const headers = injectHeaders(span);
219
-
220
- expect(typeof headers).toBe('object');
221
- });
222
- });
223
-
224
- describe('extractSpanContext', () => {
225
- it('returns a Context derived from the given headers', async () => {
226
- const { extractSpanContext } = await loadTracing();
227
-
228
- const extracted = extractSpanContext({ traceparent: '00-abc-def-01' });
229
-
230
- expect(typeof (extracted as any).getValue).toBe('function');
231
- });
232
- });
233
-
234
- describe('tracingMiddleware', () => {
235
- function mockReqRes() {
236
- const req: any = { method: 'GET', path: '/albums', url: '/albums?x=1', headers: {} };
237
- const handlers: Record<string, () => void> = {};
238
- const res: any = {
239
- statusCode: 200,
240
- on: jest.fn((event: string, handler: () => void) => {
241
- handlers[event] = handler;
242
- })
243
- };
244
- return { req, res, handlers };
245
- }
246
-
247
- it('starts a server span, attaches it to the request, and calls next', async () => {
248
- const { tracingMiddleware } = await loadTracing();
249
- const { req, res } = mockReqRes();
250
- const next = jest.fn();
251
-
252
- tracingMiddleware(req, res, next);
253
-
254
- expect(req.span).toBeDefined();
255
- expect(req.span.setAttribute).toHaveBeenCalledWith('http.method', 'GET');
256
- expect(req.span.setAttribute).toHaveBeenCalledWith('http.url', '/albums?x=1');
257
- expect(next).toHaveBeenCalledTimes(1);
258
- });
259
-
260
- it('records the response status code and ends the span on finish', async () => {
261
- const { tracingMiddleware } = await loadTracing();
262
- const { req, res, handlers } = mockReqRes();
263
- const next = jest.fn();
264
-
265
- tracingMiddleware(req, res, next);
266
- res.statusCode = 404;
267
- handlers.finish();
268
-
269
- expect(req.span.setAttribute).toHaveBeenCalledWith('http.status_code', 404);
270
- expect(req.span.end).toHaveBeenCalledTimes(1);
271
- });
272
- });
@@ -1,110 +0,0 @@
1
- import {
2
- context,
3
- propagation,
4
- trace,
5
- SpanKind,
6
- Tracer,
7
- Span,
8
- Context,
9
- Attributes,
10
- AttributeValue,
11
- } from '@opentelemetry/api';
12
- import { Request, Response, NextFunction } from 'express';
13
- import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
14
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
15
- import { resourceFromAttributes } from '@opentelemetry/resources';
16
- import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
17
-
18
- // Initialize OpenTelemetry tracer provider, exporting via OTLP/HTTP.
19
- // Endpoint resolution: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT > JAEGER_ENDPOINT > in-cluster collector default.
20
- export const initTracer = (serviceName: string): Tracer => {
21
- const url =
22
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
23
- process.env.JAEGER_ENDPOINT ||
24
- 'http://jaeger-collector:4318/v1/traces';
25
-
26
- const provider = new NodeTracerProvider({
27
- resource: resourceFromAttributes({
28
- [ATTR_SERVICE_NAME]: serviceName,
29
- [ATTR_SERVICE_VERSION]: process.env.npm_package_version || '1.0.0',
30
- 'deployment.environment': process.env.NODE_ENV || 'development',
31
- }),
32
- spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url }))],
33
- });
34
- provider.register();
35
-
36
- return provider.getTracer(serviceName);
37
- };
38
-
39
- // Global tracer instance
40
- let tracer: Tracer | null = null;
41
-
42
- export const getTracer = (): Tracer => {
43
- if (!tracer) {
44
- tracer = initTracer(process.env.SERVICE_NAME || 'unknown-service');
45
- }
46
- return tracer;
47
- };
48
-
49
- // Start a new span. `parent` may be a Span or a Context (e.g. from extractSpanContext).
50
- export const startSpan = (operationName: string, parent?: Span | Context) => {
51
- if (!parent) {
52
- return getTracer().startSpan(operationName);
53
- }
54
- // A Span exposes `spanContext()`; a Context does not — use that to decide
55
- // whether we need to wrap the parent span into an active context first.
56
- const parentContext =
57
- 'spanContext' in parent ? trace.setSpan(context.active(), parent) : parent;
58
- return getTracer().startSpan(operationName, undefined, parentContext);
59
- };
60
-
61
- // Middleware for Express to create spans for requests
62
- export const tracingMiddleware = (req: Request, res: Response, next: NextFunction) => {
63
- const parentContext = propagation.extract(context.active(), req.headers);
64
- const span = getTracer().startSpan(
65
- `${req.method} ${req.path}`,
66
- { kind: SpanKind.SERVER },
67
- parentContext
68
- );
69
- span.setAttribute('http.method', req.method);
70
- span.setAttribute('http.url', req.url);
71
-
72
- // Add span to request for use in handlers
73
- req.span = span;
74
-
75
- res.on('finish', () => {
76
- span.setAttribute('http.status_code', res.statusCode);
77
- span.end();
78
- });
79
-
80
- next();
81
- };
82
-
83
- // Helper to create child spans
84
- export const createChildSpan = (operationName: string, parent: Span | Context) => {
85
- return startSpan(operationName, parent);
86
- };
87
-
88
- // Helper to log events to spans
89
- export const logToSpan = (span: Span, event: string, data?: Attributes) => {
90
- span.addEvent(event, data);
91
- };
92
-
93
- // Helper to set tags on spans
94
- export const setSpanTag = (span: Span, key: string, value: AttributeValue) => {
95
- span.setAttribute(key, value);
96
- };
97
-
98
- // Helper to inject tracing headers (W3C traceparent) for downstream calls
99
- export const injectHeaders = (span: Span) => {
100
- const headers: { [key: string]: string } = {};
101
- propagation.inject(trace.setSpan(context.active(), span), headers);
102
- return headers;
103
- };
104
-
105
- // Helper to extract span context from incoming headers; pass the result to startSpan as parent
106
- export const extractSpanContext = (
107
- headers: Record<string, string | string[] | undefined>
108
- ): Context => {
109
- return propagation.extract(context.active(), headers);
110
- };
package/src/types/auth.ts DELETED
@@ -1,45 +0,0 @@
1
- import type { Span } from '@opentelemetry/api';
2
-
3
- export interface UserPayload {
4
- id: string;
5
- email: string;
6
- name: string;
7
- picture?: string;
8
- groups?: string[];
9
- roles?: string[];
10
- tokenVersion?: number;
11
- /** The auth-service session (user_sessions) this token belongs to; absent on pre-sessions tokens. */
12
- sid?: string;
13
- }
14
-
15
- /**
16
- * Attaches `user`/`userGroups` to every Express `Request` across all
17
- * services, replacing the ad-hoc `(req as any).user` casts and each
18
- * service's own divergent local augmentation. Individual services may still
19
- * carry additional request-scoped fields (e.g. auth-service's `dbUser`) —
20
- * those stay local since they aren't meaningful outside that service.
21
- *
22
- * `correlationId` is set by `correlationMiddleware`, `span` by
23
- * `tracingMiddleware`; both are declared here so the observability middleware
24
- * and downstream handlers can read them without a `(req as any)` cast.
25
- */
26
- declare global {
27
- namespace Express {
28
- interface Request {
29
- user?: UserPayload;
30
- userGroups?: string[];
31
- correlationId?: string;
32
- span?: Span;
33
- }
34
- }
35
- }
36
-
37
- export interface ServiceHealth {
38
- status: 'ok' | 'error';
39
- service: string;
40
- type: 'backend' | 'frontend';
41
- timestamp: string;
42
- version: string;
43
- description: string;
44
- dependencies?: Record<string, unknown>;
45
- }
@@ -1,47 +0,0 @@
1
- import { Request, Response } from 'express';
2
- import { generateCorrelationId, correlationMiddleware } from './correlation';
3
-
4
- function mockReq(headers: Record<string, string> = {}): Request {
5
- return { headers } as unknown as Request;
6
- }
7
-
8
- function mockRes(): Response {
9
- const res: Partial<Response> = {};
10
- res.setHeader = jest.fn().mockReturnValue(res);
11
- return res as Response;
12
- }
13
-
14
- describe('generateCorrelationId', () => {
15
- it('returns a unique UUID each call', () => {
16
- const a = generateCorrelationId();
17
- const b = generateCorrelationId();
18
- expect(a).not.toBe(b);
19
- expect(a).toMatch(/^[0-9a-f-]{36}$/i);
20
- });
21
- });
22
-
23
- describe('correlationMiddleware', () => {
24
- it('generates a new correlation id when none is provided', () => {
25
- const req = mockReq();
26
- const res = mockRes();
27
- const next = jest.fn();
28
-
29
- correlationMiddleware(req, res, next);
30
-
31
- expect((req as any).correlationId).toMatch(/^[0-9a-f-]{36}$/i);
32
- expect(res.setHeader).toHaveBeenCalledWith('x-correlation-id', (req as any).correlationId);
33
- expect(next).toHaveBeenCalledTimes(1);
34
- });
35
-
36
- it('reuses an incoming x-correlation-id header', () => {
37
- const req = mockReq({ 'x-correlation-id': 'existing-id' });
38
- const res = mockRes();
39
- const next = jest.fn();
40
-
41
- correlationMiddleware(req, res, next);
42
-
43
- expect((req as any).correlationId).toBe('existing-id');
44
- expect(res.setHeader).toHaveBeenCalledWith('x-correlation-id', 'existing-id');
45
- expect(next).toHaveBeenCalledTimes(1);
46
- });
47
- });
@@ -1,22 +0,0 @@
1
- import { Request, Response, NextFunction, RequestHandler } from 'express';
2
- import { v4 as uuidv4 } from 'uuid';
3
-
4
- /**
5
- * Generate a unique correlation ID for request tracing
6
- */
7
- export function generateCorrelationId(): string {
8
- return uuidv4();
9
- }
10
-
11
- /**
12
- * Middleware to add correlation ID to requests
13
- * Adds correlation ID to request object and response headers
14
- */
15
- export const correlationMiddleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
16
- const correlationId = req.headers['x-correlation-id'] as string || generateCorrelationId();
17
-
18
- req.correlationId = correlationId;
19
- res.setHeader('x-correlation-id', correlationId);
20
-
21
- next();
22
- };
@@ -1,47 +0,0 @@
1
- import { buildAccessQuery, canAccessResource } from './permissionUtils';
2
-
3
- describe('buildAccessQuery', () => {
4
- it('always includes the user\'s own resources', () => {
5
- expect(buildAccessQuery('user-1')).toEqual({ $or: [{ userId: 'user-1' }] });
6
- });
7
-
8
- it('adds a group-membership condition when groupIds are provided', () => {
9
- expect(buildAccessQuery('user-1', ['g1', 'g2'])).toEqual({
10
- $or: [{ userId: 'user-1' }, { groupId: { $in: ['g1', 'g2'] } }]
11
- });
12
- });
13
-
14
- it('adds a public-resource condition when includePublic is true', () => {
15
- expect(buildAccessQuery('user-1', [], true)).toEqual({
16
- $or: [{ userId: 'user-1' }, { isPublic: true }]
17
- });
18
- });
19
-
20
- it('combines group and public conditions', () => {
21
- expect(buildAccessQuery('user-1', ['g1'], true)).toEqual({
22
- $or: [{ userId: 'user-1' }, { groupId: { $in: ['g1'] } }, { isPublic: true }]
23
- });
24
- });
25
- });
26
-
27
- describe('canAccessResource', () => {
28
- it('allows the owner', () => {
29
- expect(canAccessResource('user-1', 'user-1')).toBe(true);
30
- });
31
-
32
- it('allows anyone when the resource is public', () => {
33
- expect(canAccessResource('user-1', 'user-2', undefined, [], true)).toBe(true);
34
- });
35
-
36
- it('allows a member of the resource\'s group', () => {
37
- expect(canAccessResource('user-1', 'user-2', 'g1', ['g1'])).toBe(true);
38
- });
39
-
40
- it('denies a non-member with no public flag', () => {
41
- expect(canAccessResource('user-1', 'user-2', 'g1', ['g2'])).toBe(false);
42
- });
43
-
44
- it('denies when the resource has no group and belongs to someone else', () => {
45
- expect(canAccessResource('user-1', 'user-2')).toBe(false);
46
- });
47
- });
@@ -1,68 +0,0 @@
1
- /**
2
- * Simple permission helper that builds query filters for group-based access
3
- * This avoids external API calls and can be used in database queries directly
4
- */
5
-
6
- /**
7
- * Build a MongoDB query for resources accessible by a user
8
- * @param userId - The user's ID
9
- * @param groupIds - Array of group IDs the user belongs to (optional)
10
- * @param includePublic - Whether to include public resources
11
- */
12
- export function buildAccessQuery(
13
- userId: string,
14
- groupIds: string[] = [],
15
- includePublic: boolean = false
16
- ) {
17
- const conditions: Array<
18
- { userId: string } | { groupId: { $in: string[] } } | { isPublic: boolean }
19
- > = [
20
- { userId } // User's own resources
21
- ];
22
-
23
- // Add group-based access
24
- if (groupIds.length > 0) {
25
- conditions.push({ groupId: { $in: groupIds } });
26
- }
27
-
28
- // Add public resources if requested
29
- if (includePublic) {
30
- conditions.push({ isPublic: true });
31
- }
32
-
33
- return { $or: conditions };
34
- }
35
-
36
- /**
37
- * Check if a user can access a specific resource
38
- * @param userId - The requesting user's ID
39
- * @param resourceUserId - The resource owner's ID
40
- * @param resourceGroupId - The resource's group ID (if any)
41
- * @param userGroupIds - Array of group IDs the user belongs to
42
- * @param isResourcePublic - Whether the resource is public
43
- */
44
- export function canAccessResource(
45
- userId: string,
46
- resourceUserId: string,
47
- resourceGroupId?: string,
48
- userGroupIds: string[] = [],
49
- isResourcePublic: boolean = false
50
- ): boolean {
51
- // Owner can always access their own resources
52
- if (userId === resourceUserId) {
53
- return true;
54
- }
55
-
56
- // Public resources are accessible to anyone
57
- if (isResourcePublic) {
58
- return true;
59
- }
60
-
61
- // If resource is associated with a group, check if user is in that group
62
- if (resourceGroupId && userGroupIds.includes(resourceGroupId)) {
63
- return true;
64
- }
65
-
66
- // No access
67
- return false;
68
- }
@@ -1,64 +0,0 @@
1
- import { Response } from 'express';
2
- import { createApiResponse, sendSuccess, sendError } from './response';
3
-
4
- function mockRes(): Response {
5
- const res: Partial<Response> = {};
6
- res.status = jest.fn().mockReturnValue(res);
7
- res.json = jest.fn().mockReturnValue(res);
8
- return res as Response;
9
- }
10
-
11
- describe('createApiResponse', () => {
12
- it('omits undefined optional fields', () => {
13
- expect(createApiResponse(true)).toEqual({ success: true });
14
- });
15
-
16
- it('includes provided fields', () => {
17
- expect(createApiResponse(true, { id: 1 }, 'ok', undefined, 'corr-1')).toEqual({
18
- success: true,
19
- data: { id: 1 },
20
- message: 'ok',
21
- correlationId: 'corr-1'
22
- });
23
- });
24
-
25
- it('keeps data even when falsy but defined (e.g. 0 or empty array)', () => {
26
- expect(createApiResponse(true, 0)).toEqual({ success: true, data: 0 });
27
- expect(createApiResponse(true, [])).toEqual({ success: true, data: [] });
28
- });
29
- });
30
-
31
- describe('sendSuccess', () => {
32
- it('defaults to status 200 with a success envelope', () => {
33
- const res = mockRes();
34
- sendSuccess(res, { id: 1 }, 'created');
35
-
36
- expect(res.status).toHaveBeenCalledWith(200);
37
- expect(res.json).toHaveBeenCalledWith({ success: true, data: { id: 1 }, message: 'created' });
38
- });
39
-
40
- it('honors a custom status code', () => {
41
- const res = mockRes();
42
- sendSuccess(res, { id: 1 }, 'created', 201);
43
-
44
- expect(res.status).toHaveBeenCalledWith(201);
45
- });
46
- });
47
-
48
- describe('sendError', () => {
49
- it('defaults to status 500 with an error envelope', () => {
50
- const res = mockRes();
51
- sendError(res, 'Something broke');
52
-
53
- expect(res.status).toHaveBeenCalledWith(500);
54
- expect(res.json).toHaveBeenCalledWith({ success: false, message: 'Something broke' });
55
- });
56
-
57
- it('honors a custom status code and error code', () => {
58
- const res = mockRes();
59
- sendError(res, 'Not found', 404, 'NOT_FOUND');
60
-
61
- expect(res.status).toHaveBeenCalledWith(404);
62
- expect(res.json).toHaveBeenCalledWith({ success: false, message: 'Not found', error: 'NOT_FOUND' });
63
- });
64
- });