@tumbaland/backend-core 1.17.0 → 1.19.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 (53) hide show
  1. package/README.md +52 -0
  2. package/dist/app/shutdown.d.ts +31 -0
  3. package/dist/app/shutdown.d.ts.map +1 -0
  4. package/dist/app/shutdown.js +58 -0
  5. package/dist/app/shutdown.js.map +1 -0
  6. package/dist/health/createHealthCheck.d.ts +32 -0
  7. package/dist/health/createHealthCheck.d.ts.map +1 -0
  8. package/dist/health/createHealthCheck.js +50 -0
  9. package/dist/health/createHealthCheck.js.map +1 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +9 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/logging/logger.js +1 -1
  15. package/dist/logging/logger.js.map +1 -1
  16. package/dist/metrics/index.d.ts +3 -2
  17. package/dist/metrics/index.d.ts.map +1 -1
  18. package/dist/metrics/index.js +1 -1
  19. package/dist/metrics/index.js.map +1 -1
  20. package/dist/middleware/errorHandler.js.map +1 -1
  21. package/dist/middleware/requestLogger.d.ts +3 -2
  22. package/dist/middleware/requestLogger.d.ts.map +1 -1
  23. package/dist/middleware/requestLogger.js.map +1 -1
  24. package/dist/tracing/index.d.ts +9 -8
  25. package/dist/tracing/index.d.ts.map +1 -1
  26. package/dist/tracing/index.js +7 -7
  27. package/dist/tracing/index.js.map +1 -1
  28. package/dist/types/auth.d.ts +9 -1
  29. package/dist/types/auth.d.ts.map +1 -1
  30. package/dist/utils/correlation.js.map +1 -1
  31. package/dist/utils/permissionUtils.d.ts +31 -0
  32. package/dist/utils/permissionUtils.d.ts.map +1 -0
  33. package/dist/utils/permissionUtils.js +53 -0
  34. package/dist/utils/permissionUtils.js.map +1 -0
  35. package/dist/utils/response.d.ts +3 -3
  36. package/dist/utils/response.d.ts.map +1 -1
  37. package/package.json +3 -3
  38. package/src/app/shutdown.test.ts +129 -0
  39. package/src/app/shutdown.ts +81 -0
  40. package/src/health/createHealthCheck.test.ts +89 -0
  41. package/src/health/createHealthCheck.ts +67 -0
  42. package/src/index.ts +5 -0
  43. package/src/logging/logger.ts +1 -1
  44. package/src/metrics/index.ts +3 -2
  45. package/src/middleware/errorHandler.ts +2 -2
  46. package/src/middleware/requestLogger.ts +2 -1
  47. package/src/tracing/index.test.ts +11 -7
  48. package/src/tracing/index.ts +26 -13
  49. package/src/types/auth.ts +10 -1
  50. package/src/utils/correlation.ts +1 -1
  51. package/src/utils/permissionUtils.test.ts +47 -0
  52. package/src/utils/permissionUtils.ts +68 -0
  53. package/src/utils/response.ts +3 -3
@@ -1,4 +1,15 @@
1
- import { context, propagation, trace, SpanKind, Tracer } from '@opentelemetry/api';
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';
2
13
  import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
3
14
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
4
15
  import { resourceFromAttributes } from '@opentelemetry/resources';
@@ -36,19 +47,19 @@ export const getTracer = (): Tracer => {
36
47
  };
37
48
 
38
49
  // Start a new span. `parent` may be a Span or a Context (e.g. from extractSpanContext).
39
- export const startSpan = (operationName: string, parentSpan?: any) => {
40
- if (!parentSpan) {
50
+ export const startSpan = (operationName: string, parent?: Span | Context) => {
51
+ if (!parent) {
41
52
  return getTracer().startSpan(operationName);
42
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.
43
56
  const parentContext =
44
- typeof parentSpan.spanContext === 'function'
45
- ? trace.setSpan(context.active(), parentSpan)
46
- : parentSpan;
57
+ 'spanContext' in parent ? trace.setSpan(context.active(), parent) : parent;
47
58
  return getTracer().startSpan(operationName, undefined, parentContext);
48
59
  };
49
60
 
50
61
  // Middleware for Express to create spans for requests
51
- export const tracingMiddleware = (req: any, res: any, next: any) => {
62
+ export const tracingMiddleware = (req: Request, res: Response, next: NextFunction) => {
52
63
  const parentContext = propagation.extract(context.active(), req.headers);
53
64
  const span = getTracer().startSpan(
54
65
  `${req.method} ${req.path}`,
@@ -70,28 +81,30 @@ export const tracingMiddleware = (req: any, res: any, next: any) => {
70
81
  };
71
82
 
72
83
  // Helper to create child spans
73
- export const createChildSpan = (operationName: string, parentSpan: any) => {
74
- return startSpan(operationName, parentSpan);
84
+ export const createChildSpan = (operationName: string, parent: Span | Context) => {
85
+ return startSpan(operationName, parent);
75
86
  };
76
87
 
77
88
  // Helper to log events to spans
78
- export const logToSpan = (span: any, event: string, data?: any) => {
89
+ export const logToSpan = (span: Span, event: string, data?: Attributes) => {
79
90
  span.addEvent(event, data);
80
91
  };
81
92
 
82
93
  // Helper to set tags on spans
83
- export const setSpanTag = (span: any, key: string, value: any) => {
94
+ export const setSpanTag = (span: Span, key: string, value: AttributeValue) => {
84
95
  span.setAttribute(key, value);
85
96
  };
86
97
 
87
98
  // Helper to inject tracing headers (W3C traceparent) for downstream calls
88
- export const injectHeaders = (span: any) => {
99
+ export const injectHeaders = (span: Span) => {
89
100
  const headers: { [key: string]: string } = {};
90
101
  propagation.inject(trace.setSpan(context.active(), span), headers);
91
102
  return headers;
92
103
  };
93
104
 
94
105
  // Helper to extract span context from incoming headers; pass the result to startSpan as parent
95
- export const extractSpanContext = (headers: any) => {
106
+ export const extractSpanContext = (
107
+ headers: Record<string, string | string[] | undefined>
108
+ ): Context => {
96
109
  return propagation.extract(context.active(), headers);
97
110
  };
package/src/types/auth.ts CHANGED
@@ -1,9 +1,12 @@
1
+ import type { Span } from '@opentelemetry/api';
2
+
1
3
  export interface UserPayload {
2
4
  id: string;
3
5
  email: string;
4
6
  name: string;
5
7
  picture?: string;
6
8
  groups?: string[];
9
+ roles?: string[];
7
10
  }
8
11
 
9
12
  /**
@@ -12,12 +15,18 @@ export interface UserPayload {
12
15
  * service's own divergent local augmentation. Individual services may still
13
16
  * carry additional request-scoped fields (e.g. auth-service's `dbUser`) —
14
17
  * those stay local since they aren't meaningful outside that service.
18
+ *
19
+ * `correlationId` is set by `correlationMiddleware`, `span` by
20
+ * `tracingMiddleware`; both are declared here so the observability middleware
21
+ * and downstream handlers can read them without a `(req as any)` cast.
15
22
  */
16
23
  declare global {
17
24
  namespace Express {
18
25
  interface Request {
19
26
  user?: UserPayload;
20
27
  userGroups?: string[];
28
+ correlationId?: string;
29
+ span?: Span;
21
30
  }
22
31
  }
23
32
  }
@@ -29,5 +38,5 @@ export interface ServiceHealth {
29
38
  timestamp: string;
30
39
  version: string;
31
40
  description: string;
32
- dependencies?: Record<string, any>;
41
+ dependencies?: Record<string, unknown>;
33
42
  }
@@ -15,7 +15,7 @@ export function generateCorrelationId(): string {
15
15
  export const correlationMiddleware: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
16
16
  const correlationId = req.headers['x-correlation-id'] as string || generateCorrelationId();
17
17
 
18
- (req as any).correlationId = correlationId;
18
+ req.correlationId = correlationId;
19
19
  res.setHeader('x-correlation-id', correlationId);
20
20
 
21
21
  next();
@@ -0,0 +1,47 @@
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
+ });
@@ -0,0 +1,68 @@
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
+ }
@@ -3,7 +3,7 @@ import { Response } from 'express';
3
3
  /**
4
4
  * Standard API response format
5
5
  */
6
- export interface ApiResponse<T = any> {
6
+ export interface ApiResponse<T = unknown> {
7
7
  success: boolean;
8
8
  data?: T;
9
9
  message?: string;
@@ -14,7 +14,7 @@ export interface ApiResponse<T = any> {
14
14
  /**
15
15
  * Create a standardized API response
16
16
  */
17
- export function createApiResponse<T = any>(
17
+ export function createApiResponse<T = unknown>(
18
18
  success: boolean,
19
19
  data?: T,
20
20
  message?: string,
@@ -34,7 +34,7 @@ export function createApiResponse<T = any>(
34
34
  /**
35
35
  * Send a success response
36
36
  */
37
- export function sendSuccess<T = any>(
37
+ export function sendSuccess<T = unknown>(
38
38
  res: Response,
39
39
  data?: T,
40
40
  message?: string,