@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.
- package/package.json +5 -1
- package/.versionrc.json +0 -7
- package/__mocks__/uuid.js +0 -8
- package/jest.config.js +0 -24
- package/src/apiKeys/ApiKey.test.ts +0 -142
- package/src/apiKeys/ApiKey.ts +0 -102
- package/src/apiKeys/crypto.test.ts +0 -161
- package/src/apiKeys/crypto.ts +0 -163
- package/src/apiKeys/index.test.ts +0 -72
- package/src/apiKeys/index.ts +0 -35
- package/src/apiKeys/middleware.test.ts +0 -651
- package/src/apiKeys/middleware.ts +0 -341
- package/src/apiKeys/service.test.ts +0 -401
- package/src/apiKeys/service.ts +0 -168
- package/src/apiKeys/types.test.ts +0 -41
- package/src/apiKeys/types.ts +0 -124
- package/src/app/createBaseApp.test.ts +0 -109
- package/src/app/createBaseApp.ts +0 -102
- package/src/app/shutdown.test.ts +0 -129
- package/src/app/shutdown.ts +0 -81
- package/src/audit/AuditEvent.ts +0 -123
- package/src/audit/actor.test.ts +0 -95
- package/src/audit/actor.ts +0 -68
- package/src/audit/context.test.ts +0 -91
- package/src/audit/context.ts +0 -83
- package/src/audit/index.ts +0 -11
- package/src/audit/plugin.test.ts +0 -258
- package/src/audit/plugin.ts +0 -254
- package/src/audit/reads.test.ts +0 -164
- package/src/audit/reads.ts +0 -88
- package/src/audit/service.test.ts +0 -115
- package/src/audit/service.ts +0 -95
- package/src/auth/session.test.ts +0 -89
- package/src/auth/session.ts +0 -75
- package/src/config/env.test.ts +0 -28
- package/src/config/env.ts +0 -13
- package/src/database/connection.test.ts +0 -188
- package/src/database/connection.ts +0 -90
- package/src/entitlements/UsageMeter.ts +0 -49
- package/src/entitlements/client.test.ts +0 -200
- package/src/entitlements/client.ts +0 -179
- package/src/entitlements/definitions.test.ts +0 -161
- package/src/entitlements/definitions.ts +0 -268
- package/src/entitlements/index.ts +0 -42
- package/src/entitlements/middleware.test.ts +0 -196
- package/src/entitlements/middleware.ts +0 -150
- package/src/entitlements/reconcile.test.ts +0 -333
- package/src/entitlements/reconcile.ts +0 -384
- package/src/entitlements/types.ts +0 -21
- package/src/entitlements/usage.test.ts +0 -314
- package/src/entitlements/usage.ts +0 -223
- package/src/errors/HttpError.test.ts +0 -76
- package/src/errors/HttpError.ts +0 -91
- package/src/groups/client.test.ts +0 -215
- package/src/groups/client.ts +0 -182
- package/src/groups/index.ts +0 -7
- package/src/groups/membership.test.ts +0 -84
- package/src/groups/membership.ts +0 -133
- package/src/groups/subject.test.ts +0 -85
- package/src/groups/subject.ts +0 -50
- package/src/health/createHealthCheck.test.ts +0 -89
- package/src/health/createHealthCheck.ts +0 -67
- package/src/health/healthController.test.ts +0 -113
- package/src/health/healthController.ts +0 -56
- package/src/index.ts +0 -88
- package/src/logging/logger.test.ts +0 -91
- package/src/logging/logger.ts +0 -103
- package/src/metrics/index.test.ts +0 -116
- package/src/metrics/index.ts +0 -111
- package/src/middleware/authMiddleware.test.ts +0 -275
- package/src/middleware/authMiddleware.ts +0 -91
- package/src/middleware/corsMiddleware.test.ts +0 -135
- package/src/middleware/corsMiddleware.ts +0 -65
- package/src/middleware/errorHandler.test.ts +0 -188
- package/src/middleware/errorHandler.ts +0 -103
- package/src/middleware/internalServiceAuth.test.ts +0 -173
- package/src/middleware/internalServiceAuth.ts +0 -96
- package/src/middleware/requestLogger.test.ts +0 -81
- package/src/middleware/requestLogger.ts +0 -48
- package/src/middleware/security.test.ts +0 -45
- package/src/middleware/security.ts +0 -43
- package/src/middleware/validate.test.ts +0 -72
- package/src/middleware/validate.ts +0 -23
- package/src/oauth/index.ts +0 -29
- package/src/oauth/models.ts +0 -164
- package/src/oauth/service.test.ts +0 -432
- package/src/oauth/service.ts +0 -299
- package/src/oauth/tokens.test.ts +0 -146
- package/src/oauth/tokens.ts +0 -186
- package/src/testing/serviceTestSetup.ts +0 -17
- package/src/tracing/index.test.ts +0 -272
- package/src/tracing/index.ts +0 -110
- package/src/types/auth.ts +0 -45
- package/src/utils/correlation.test.ts +0 -47
- package/src/utils/correlation.ts +0 -22
- package/src/utils/permissionUtils.test.ts +0 -47
- package/src/utils/permissionUtils.ts +0 -68
- package/src/utils/response.test.ts +0 -64
- package/src/utils/response.ts +0 -60
- package/tsconfig.build.json +0 -7
- 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
|
-
});
|
package/src/tracing/index.ts
DELETED
|
@@ -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
|
-
});
|
package/src/utils/correlation.ts
DELETED
|
@@ -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
|
-
});
|