@onlineapps/service-wrapper 2.1.59 → 2.1.60

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.
@@ -45,6 +45,12 @@
45
45
  },
46
46
  "validation": {
47
47
  "enabled": true
48
+ },
49
+ "accountContext": {
50
+ "enabled": true,
51
+ "headerName": "account-id",
52
+ "requirePathPrefixes": ["/api/"],
53
+ "excludePathPrefixes": ["/api/v1/specification"]
48
54
  }
49
55
  }
50
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "2.1.59",
3
+ "version": "2.1.60",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Create an Express middleware that enforces required account context for business APIs.
5
+ *
6
+ * Rules:
7
+ * - Applies only for paths that match `requirePathPrefixes` and do NOT match `excludePathPrefixes`.
8
+ * - Requires header `headerName` to be a positive integer.
9
+ * - Sets `req.account_id` as integer.
10
+ *
11
+ * @param {Object} options
12
+ * @param {string} options.serviceName
13
+ * @param {Object} options.accountContext
14
+ * @param {boolean} options.accountContext.enabled
15
+ * @param {string} options.accountContext.headerName
16
+ * @param {string[]} options.accountContext.requirePathPrefixes
17
+ * @param {string[]} options.accountContext.excludePathPrefixes
18
+ * @returns {Function} Express middleware
19
+ */
20
+ function createAccountContextMiddleware(options = {}) {
21
+ const serviceName = options.serviceName;
22
+ const accountContext = options.accountContext || {};
23
+
24
+ if (!serviceName || typeof serviceName !== 'string') {
25
+ throw new Error('[service-wrapper][AccountContext] Missing dependency - Expected serviceName (string)');
26
+ }
27
+ if (typeof accountContext !== 'object' || Array.isArray(accountContext)) {
28
+ throw new Error('[service-wrapper][AccountContext] Invalid configuration - Expected accountContext to be an object');
29
+ }
30
+
31
+ const {
32
+ enabled,
33
+ headerName,
34
+ requirePathPrefixes,
35
+ excludePathPrefixes
36
+ } = accountContext;
37
+
38
+ if (typeof enabled !== 'boolean') {
39
+ throw new Error(`[service-wrapper][AccountContext] Invalid configuration - wrapper.accountContext.enabled must be boolean, got: ${typeof enabled}`);
40
+ }
41
+ if (typeof headerName !== 'string' || headerName.trim() === '') {
42
+ throw new Error(`[service-wrapper][AccountContext] Invalid configuration - wrapper.accountContext.headerName must be non-empty string, got: ${headerName}`);
43
+ }
44
+ if (!Array.isArray(requirePathPrefixes) || requirePathPrefixes.some(p => typeof p !== 'string')) {
45
+ throw new Error('[service-wrapper][AccountContext] Invalid configuration - wrapper.accountContext.requirePathPrefixes must be string[]');
46
+ }
47
+ if (!Array.isArray(excludePathPrefixes) || excludePathPrefixes.some(p => typeof p !== 'string')) {
48
+ throw new Error('[service-wrapper][AccountContext] Invalid configuration - wrapper.accountContext.excludePathPrefixes must be string[]');
49
+ }
50
+
51
+ const requirePrefixes = requirePathPrefixes;
52
+ const excludePrefixes = excludePathPrefixes;
53
+
54
+ return function accountContextMiddleware(req, res, next) {
55
+ if (!enabled) {
56
+ return next();
57
+ }
58
+
59
+ const path = req && typeof req.path === 'string' ? req.path : '';
60
+ if (!requirePrefixes.some(prefix => path.startsWith(prefix))) {
61
+ return next();
62
+ }
63
+ if (excludePrefixes.some(prefix => path.startsWith(prefix))) {
64
+ return next();
65
+ }
66
+
67
+ const rawAccountId = req.headers ? req.headers[headerName] : undefined;
68
+ if (!rawAccountId) {
69
+ return res.status(400).json({
70
+ error: `[${serviceName}][Multitenancy] Missing account context - Expected request header '${headerName}' (INT)`
71
+ });
72
+ }
73
+
74
+ const accountId = Number.parseInt(String(rawAccountId), 10);
75
+ if (!Number.isInteger(accountId) || accountId <= 0) {
76
+ return res.status(400).json({
77
+ error: `[${serviceName}][Multitenancy] Invalid account context - Expected '${headerName}' to be a positive integer`
78
+ });
79
+ }
80
+
81
+ req.account_id = accountId;
82
+ return next();
83
+ };
84
+ }
85
+
86
+ module.exports = { createAccountContextMiddleware };
87
+
88
+
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ const ServiceWrapper = require('./ServiceWrapper');
16
16
  const { ConfigLoader } = require('./ConfigLoader');
17
17
  const runtimeCfg = require('./config');
18
18
  const pkg = require('../package.json');
19
+ const { createAccountContextMiddleware } = require('./createAccountContextMiddleware');
19
20
 
20
21
  // Note: WorkflowProcessor and ApiCaller functionality has been moved to connectors:
21
22
  // - WorkflowProcessor -> @onlineapps/conn-orch-orchestrator
@@ -58,6 +59,14 @@ async function bootstrap(serviceRoot, options = {}) {
58
59
 
59
60
  console.log(`Starting ${config.service.name} v${config.service.version}...`);
60
61
 
62
+ // 0. Apply shared request context middleware (account context for multitenancy)
63
+ // Enforced centrally for all business services: /api/** must have account-id (except /api/v1/specification).
64
+ const accountContextMw = createAccountContextMiddleware({
65
+ serviceName: config.service.name,
66
+ accountContext: config.wrapper?.accountContext
67
+ });
68
+ app.use(accountContextMw);
69
+
61
70
  // 1. Start HTTP server
62
71
  const PORT = config.service.port;
63
72
  const server = app.listen(PORT, () => {