@onlineapps/service-wrapper 3.3.5 → 3.4.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # Changelog — @onlineapps/service-wrapper
2
+
3
+ All notable changes to this package. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format and semantic versioning within the internal-monorepo convention (all biz-service consumers ship matching changes in the same session).
4
+
5
+ ## [3.4.1] — 2026-08-17
6
+
7
+ **Patch: drop stale port/url/specificationEndpoint guards left over from A2.2.**
8
+
9
+ Under 3.4.0 the wrapper still refused to start when `config.service.port`, `config.service.url`, or `config.service.specificationEndpoint` were absent — legacy checks from `_ensureValidationProof` and `_initializeRegistry` that predated ADR 0005 and were not removed together with the Express init in A2.2. Live rollout hit the failure on first biz restart (biz-property Phase 0.2: "Service port is required for validation").
10
+
11
+ Removed:
12
+ - Port + URL guard in `_ensureValidationProof` (before Tier-1 validation runs).
13
+ - Port + URL guard in `_initializeRegistry` (before Registry registration).
14
+ - specificationEndpoint guard in `_initializeRegistry`.
15
+
16
+ Behaviour changes:
17
+ - `serviceInfo.url` sent to Registry is now the empty string (was: required non-empty).
18
+ - `specificationEndpoint` sent to RegistryClient is `''` when absent (was: throw).
19
+ - Consumers: none — Registry stores the ops spec directly from the register payload; the URL and endpoint fields have had no live reader since 3.4.0 A2.
20
+
21
+ Fully compatible with 3.4.0 tests (98/98 GREEN) and with 3.4.0 biz configs that still declare `port`/`url`/`specificationEndpoint` in `config/service/config.json` — they're just no longer required.
22
+
23
+ ## [3.4.0] — 2026-08-17
24
+
25
+ **Zero-HTTP shape for biz services** (ADR 0005 landed). See `api/docs/biz/80-decisions/0005-no-http-in-biz-containers.md`.
26
+
27
+ ### BREAKING
28
+
29
+ - **Constructor:** `options.app` and `options.server` no longer required. Passed values are accepted for transitional callers but IGNORED — the wrapper never binds an Express app or HTTP server.
30
+ - **`bootstrap()`:** signature `bootstrap(serviceRoot, options)` no longer reads `options.app`, no longer resolves `require('./src/app')`, no longer calls `app.listen(PORT)`. Return value shape changed from `{wrapper, server}` to `{wrapper}` (no server exists).
31
+ - **Removed HTTP endpoints:** `GET /`, `GET /health`, `GET /info`, `GET /status`, `GET /specification`. These no longer mount on any Express app. Consumers relying on HTTP-polling `/health` (e.g. Registry `businessReadinessChecker` pre-3.4.0) MUST switch to reading `infrastructureHealthTracker` state populated from the MQ heartbeat.
32
+ - **Removed methods** (were private, listed for grep-completeness): `_setupHealthChecks`, `_setupInfoEndpoint`, `_getValidationInfo`, `_getStandardLevel`, `_getInfraFingerprint`, `_extractExpressRoutes`.
33
+ - **Removed test suites**: `tests/unit/ServiceWrapper.healthDependencies.test.js` (whole file tested the retired HTTP `/health` handler). `tests/unit/ServiceWrapper.test.js` describes `/info endpoint` and `Operations-Routes Alignment` deleted (tested removed methods).
34
+
35
+ ### Added
36
+
37
+ - **`wrapper.startHealthHeartbeat()`** — new public method. Wires the biz service into the existing MQ heartbeat mechanism (`createBaseClientAdapter` from `@onlineapps/infrastructure-tools`). First heartbeat is emitted synchronously by publisher `start()`.
38
+ - **`wrapper.stopHealthHeartbeat()`** — cleanup counterpart; called from `_cleanupBeforeRestart`.
39
+ - **`wrapper.refreshDependencyHealth()`** — periodic refresh of cached `_lastDbHealth` / `_lastRedisHealth` (was a side effect of `_setupHealthChecks`; now standalone).
40
+ - **`wrapper._collectHeartbeatComponents()`** — private helper returning `{mq, redis, db, cache} → 'healthy'|'unhealthy'|'unknown'` map, matching the shape `infrastructureHealth.listener` validates.
41
+ - **Bootstrap phase 0.10** — Health Heartbeat Publisher startup (documented in `api/docs/biz/50-lifecycle/tier1-validation.md`).
42
+ - **New standard level v1.3** — Zero-HTTP Shape (documented in `api/docs/biz/00-model/service-shape.md`).
43
+
44
+ ### Dependencies
45
+
46
+ - New: `@onlineapps/infrastructure-tools@1.2.4` (exact, provides the `createHealthPublisher` factory this version wires into).
47
+
48
+ ### Migration for biz services
49
+
50
+ Every biz service under `api_biz/*/` has also been updated in the same session:
51
+ - `docker-compose.yml` / `docker-compose.production.yml`: removed `ports:` section
52
+ - `Dockerfile`: removed `EXPOSE` and `HEALTHCHECK`
53
+ - No changes needed to handlers (they never touched Express directly)
54
+ - `src/app.js` remains in each biz repo for now (bootstrap doesn't read it; deletion is a follow-up cleanup)
55
+
56
+ Rollout order:
57
+ 1. `npm publish @onlineapps/service-wrapper@3.4.0`
58
+ 2. In each `api_biz/*/package.json`: exact `"@onlineapps/service-wrapper": "3.4.0"` + `npm install`
59
+ 3. Restart containers; verify `docker port <biz>` empty and `redis-cli KEYS 'infrastructure:health:*'` shows entries.
60
+
61
+ ### Rationale
62
+
63
+ - Removes the last HTTP surface in biz containers. `_probeServiceHealth` (Registry) was the sole automated consumer; replaced by tracker reads.
64
+ - Reduces service-wrapper code by ~950 lines net.
65
+ - Unifies biz + infra service liveness path (same queue, same tracker, same Redis projection).
66
+
67
+ ## [3.3.5] — Pre-3.4.0 baseline
68
+
69
+ Last version with Express-mounted health/info/status endpoints. No changelog for versions prior to 3.4.0.
package/README.md CHANGED
@@ -246,7 +246,7 @@ Returns runtime information:
246
246
  - **Implementation standard level** — highest cumulative level satisfied (v1.0, v1.1, v1.2, ...)
247
247
  - **Revalidation state** — current backoff cycle status
248
248
 
249
- Standard levels are determined by `ServiceStructureValidator` from `@onlineapps/conn-orch-validator`. See [Implementation Standard Levels](/docs/standards/SERVICE_ENDPOINTS.md#implementation-standard-levels) and [Validator README](/shared/connector/conn-orch-validator/README.md#implementation-standard-levels).
249
+ Standard levels are determined by `ServiceStructureValidator` from `@onlineapps/conn-orch-validator`. See [Implementation Standard Levels](/docs/biz/00-model/service-shape.md#implementation-standard-levels) and [Validator README](/shared/connector/conn-orch-validator/README.md#implementation-standard-levels).
250
250
 
251
251
  ---
252
252
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/service-wrapper",
3
- "version": "3.3.5",
3
+ "version": "3.4.1",
4
4
  "description": "Thin orchestration layer for microservices - delegates all infrastructure concerns to specialized connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -34,6 +34,7 @@
34
34
  "@onlineapps/conn-orch-orchestrator": "2.1.3",
35
35
  "@onlineapps/conn-orch-registry": "1.2.2",
36
36
  "@onlineapps/conn-orch-validator": "3.2.3",
37
+ "@onlineapps/infrastructure-tools": "1.2.4",
37
38
  "@onlineapps/monitoring-core": "1.0.24",
38
39
  "@onlineapps/runtime-config": "1.0.2",
39
40
  "@onlineapps/service-common": "1.1.3",
@@ -46,6 +47,7 @@
46
47
  "jsdoc": "^4.0.2",
47
48
  "jsdoc-to-markdown": "^8.0.0"
48
49
  },
50
+ "//_express_devDep": "Express remains in devDependencies solely because a few legacy unit tests (ServiceWrapper.test.js, orchestrator-contract-v1.test.js) still construct Express apps for tests that were written before ADR 0005. Runtime never imports Express. The dep will be dropped when those tests migrate.",
49
51
  "engines": {
50
52
  "node": ">=14.0.0"
51
53
  }
@@ -99,20 +99,22 @@ class ServiceWrapper {
99
99
  /**
100
100
  * Create a new ServiceWrapper instance
101
101
  * @param {Object} options - Configuration options
102
- * @param {Object} options.app - Express application instance
103
- * @param {Object} options.server - HTTP server instance
104
102
  * @param {Object} options.config - Service and wrapper configuration
105
103
  * @param {Object} options.operations - Operations schema
106
104
  * @param {string} [options.serviceRoot] - Service root directory (for validation)
107
105
  * @param {Object} [options.validationProof=null] - Optional validation proof {hash, data}
108
106
  * @param {Object} [options._validationOrchestrator=null] - Optional ValidationOrchestrator for testing
107
+ * @param {Object} [options.app] - IGNORED (transitional; ADR 0005 removed Express)
108
+ * @param {Object} [options.server] - IGNORED (transitional; ADR 0005 removed Express)
109
109
  */
110
110
  constructor(options = {}) {
111
111
  this._validateOptions(options);
112
112
 
113
113
  // Store configuration
114
- this.app = options.app;
115
- this.server = options.server;
114
+ // Zero-HTTP shape (ADR 0005): biz services do not own an Express app
115
+ // or HTTP server. `options.app` / `options.server` are accepted for
116
+ // transitional compatibility with callers still passing them, but the
117
+ // wrapper does not read them anywhere.
116
118
  this.config = this._processConfig(options.config);
117
119
  this.operations = options.operations;
118
120
  this.serviceRoot = options.serviceRoot;
@@ -137,7 +139,7 @@ class ServiceWrapper {
137
139
  this._errorMapper = null;
138
140
 
139
141
  // Fail-fast: enforce naming conventions and workspaceScoped invariants.
140
- // See api/docs/standards/OPERATIONS.md and biz-service-onboarding.md §3.1 / §4.
142
+ // See api/docs/biz/30-operations/schema-v3.md and biz-service-onboarding.md §3.1 / §4.
141
143
  // Rejection here keeps the contract authoritative at the earliest possible
142
144
  // point (before MQ listeners, registry client, or any I/O is touched).
143
145
  this._validateNamingAndWorkspaceScoped();
@@ -309,12 +311,10 @@ class ServiceWrapper {
309
311
  * @private
310
312
  */
311
313
  _validateOptions(options) {
312
- if (!options.app) {
313
- throw new Error('Express app instance is required');
314
- }
315
- if (!options.server) {
316
- throw new Error('HTTP server instance is required');
317
- }
314
+ // Zero-HTTP shape (ADR 0005): biz services no longer own an Express app
315
+ // or HTTP server. `app` and `server` options are accepted but no longer
316
+ // required — they exist only as a transitional hint for callers still
317
+ // passing them. They MUST NOT be used inside the wrapper.
318
318
  if (!options.config) {
319
319
  throw new Error('Configuration is required');
320
320
  }
@@ -354,7 +354,7 @@ class ServiceWrapper {
354
354
  : null;
355
355
  if (!opsMap || typeof opsMap !== 'object') {
356
356
  throw new Error(
357
- `[ServiceWrapper][${serviceName}] operations schema is invalid - expected object with 'operations' map of operation definitions; see api/docs/standards/OPERATIONS.md`
357
+ `[ServiceWrapper][${serviceName}] operations schema is invalid - expected object with 'operations' map of operation definitions; see api/docs/biz/30-operations/schema-v3.md`
358
358
  );
359
359
  }
360
360
  const offenders = Object.keys(opsMap).filter((key) => !kebabRegex.test(key));
@@ -362,7 +362,7 @@ class ServiceWrapper {
362
362
  throw new Error(
363
363
  `[ServiceWrapper][${serviceName}] operation key(s) violate kebab-case convention: ` +
364
364
  `${offenders.map((k) => `'${k}'`).join(', ')} - must match /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/. ` +
365
- `See api/docs/standards/OPERATIONS.md §Best Practices - Naming Convention. ` +
365
+ `See api/docs/biz/30-operations/schema-v3.md §Best Practices - Naming Convention. ` +
366
366
  `Rename the key(s) in operations.json plus every cookbook/test/Admin UI literal; there is no alias layer.`
367
367
  );
368
368
  }
@@ -372,7 +372,7 @@ class ServiceWrapper {
372
372
  if (typeof ws !== 'boolean') {
373
373
  throw new Error(
374
374
  `[ServiceWrapper][${serviceName}] service.workspaceScoped is required and MUST be an explicit boolean in config.json (got ${ws === undefined ? 'undefined' : JSON.stringify(ws)}). ` +
375
- `See api/docs/standards/biz-service-onboarding.md §3.1. ` +
375
+ `See api/docs/biz/60-templates/onboarding-checklist.md §3.1. ` +
376
376
  `true = service stores data keyed by workspace_id and implements list-workspaces operation; ` +
377
377
  `false = stateless or cross-workspace.`
378
378
  );
@@ -384,14 +384,14 @@ class ServiceWrapper {
384
384
  throw new Error(
385
385
  `[ServiceWrapper][${serviceName}] service.workspaceScoped is true but 'list-workspaces' operation is not declared. ` +
386
386
  `Workspace-scoped services MUST expose list-workspaces (workspace discovery convention). ` +
387
- `See api/docs/standards/biz-service-onboarding.md §4 and OPERATIONS.md §Reserved operation names.`
387
+ `See api/docs/biz/60-templates/onboarding-checklist.md §4 and OPERATIONS.md §Reserved operation names.`
388
388
  );
389
389
  }
390
390
  if (ws === false && hasListWorkspaces) {
391
391
  throw new Error(
392
392
  `[ServiceWrapper][${serviceName}] service.workspaceScoped is false but 'list-workspaces' operation is declared. ` +
393
393
  `Stateless / cross-workspace services MUST NOT declare list-workspaces - the flag is the authoritative signal. ` +
394
- `See api/docs/standards/biz-service-onboarding.md §4.`
394
+ `See api/docs/biz/60-templates/onboarding-checklist.md §4.`
395
395
  );
396
396
  }
397
397
  }
@@ -650,8 +650,11 @@ class ServiceWrapper {
650
650
  async _cleanupBeforeRestart() {
651
651
  const serviceName = this.config.service?.name || 'unnamed-service';
652
652
  console.log(`[CLEANUP] Starting cleanup for ${serviceName} before restart...`);
653
-
653
+
654
654
  try {
655
+ // 0. Stop heartbeat publisher (Phase 0.10)
656
+ this.stopHealthHeartbeat();
657
+
655
658
  // 1. Cancel all consumers
656
659
  if (this.registryClient?.queueManager?.channel) {
657
660
  try {
@@ -919,10 +922,8 @@ class ServiceWrapper {
919
922
  // cache, monitoring, mq) and BEFORE MQ messages are dispatched.
920
923
  this._initializeInvocationPipeline();
921
924
 
922
- // Setup health checks
923
- if (this.config.wrapper?.health?.enabled !== false) {
924
- this._setupHealthChecks();
925
- }
925
+ // Legacy HTTP /health and /info endpoints — RETIRED per ADR 0005.
926
+ // Health surface flows via Phase 0.10 heartbeat below. No HTTP mount.
926
927
 
927
928
  // Initialize orchestrator for workflow processing
928
929
  // NOTE: Orchestrator is prepared but workflow listeners will be started
@@ -931,6 +932,26 @@ class ServiceWrapper {
931
932
  await this._initializeOrchestrator();
932
933
  }
933
934
 
935
+ // FÁZE 0.10: Health heartbeat publisher (ADR 0005 — replaces HTTP /health).
936
+ // Wires biz service into the existing infrastructure heartbeat mechanism
937
+ // used by gateway / validator / delivery_endpoint. First heartbeat is
938
+ // emitted synchronously by createHealthPublisher.start() so Registry can
939
+ // flip readiness immediately on boot.
940
+ if (this.mqClient && this.config.wrapper?.heartbeat?.enabled !== false) {
941
+ const heartbeatStartTime = Date.now();
942
+ try {
943
+ await this.startHealthHeartbeat();
944
+ this._logPhase('0.10', 'Health Heartbeat Publisher', 'PASSED', null, Date.now() - heartbeatStartTime);
945
+ } catch (error) {
946
+ // Transient: heartbeat failure should not block service operation.
947
+ // Registry will mark the service stale until heartbeats arrive.
948
+ this._logPhase('0.10', 'Health Heartbeat Publisher', 'FAILED', error, Date.now() - heartbeatStartTime);
949
+ this.logger?.warn('[ServiceWrapper] Heartbeat publisher failed to start; service continues without heartbeat', {
950
+ error: error.message
951
+ });
952
+ }
953
+ }
954
+
934
955
  this.isInitialized = true;
935
956
  this._logPhase('INIT', 'ServiceWrapper Initialization', 'PASSED', null, Date.now() - startTime);
936
957
  this.logger?.info(`ServiceWrapper initialized successfully for ${serviceName}`);
@@ -1213,15 +1234,11 @@ class ServiceWrapper {
1213
1234
  throw new Error('[ServiceWrapper] Missing configuration - service.name is required');
1214
1235
  }
1215
1236
 
1216
- const servicePort = this.config.service?.port;
1217
- if (!servicePort) {
1218
- throw new Error('[ServiceWrapper] Missing configuration - service.port is required');
1219
- }
1220
-
1221
- const serviceUrl = this.config.service?.url;
1222
- if (!serviceUrl) {
1223
- throw new Error('[ServiceWrapper] Missing configuration - service.url is required');
1224
- }
1237
+ // ADR 0005: biz has no HTTP — no port, no URL. Kept in serviceInfo
1238
+ // payload as empty string for Registry-side backward compat with
1239
+ // pre-3.4.1 consumers that may still expect the field.
1240
+ const servicePort = null;
1241
+ const serviceUrl = '';
1225
1242
 
1226
1243
  const mqUrl = this.config.wrapper?.mq?.url;
1227
1244
  if (!mqUrl) {
@@ -1232,14 +1249,11 @@ class ServiceWrapper {
1232
1249
  // FÁZE 0.5 a 0.6: Vytvoření front a spuštění konzumerů se provádí v registryClient.init()
1233
1250
  // FÁZE 0.7: Registrace u Registry
1234
1251
 
1235
- // FAIL-FAST: specificationEndpoint MUST be in config, no fallbacks
1236
- const specificationEndpoint = this.config.service?.specificationEndpoint;
1237
- if (!specificationEndpoint) {
1238
- throw new Error(
1239
- `[ServiceWrapper] Missing required configuration - service.specificationEndpoint is required. ` +
1240
- `Fix: Add "specificationEndpoint": "/api/v1/specification" to conn-config/config.json under "service" section.`
1241
- );
1242
- }
1252
+ // ADR 0005: no HTTP surface specificationEndpoint has no consumer.
1253
+ // Registry receives full operations spec in the register payload
1254
+ // itself. Field kept optional for pre-3.4.1 config files that may
1255
+ // still declare it; ignored if absent.
1256
+ const specificationEndpoint = this.config.service?.specificationEndpoint || '';
1243
1257
 
1244
1258
  // Logger may not be initialized yet at this point - RegistryClient handles null gracefully
1245
1259
  this.registryClient = new RegistryConnector.ServiceRegistryClient({
@@ -1507,236 +1521,180 @@ class ServiceWrapper {
1507
1521
  }
1508
1522
 
1509
1523
  /**
1510
- * Setup health check endpoint
1511
- * @private
1524
+ * Refresh cached component health signals for the heartbeat.
1525
+ *
1526
+ * The heartbeat itself is fire-and-forget on a short interval; we do not
1527
+ * probe DB/Redis on every heartbeat. Instead, this helper is invoked
1528
+ * periodically (or on demand) to keep `_lastDbHealth` / `_lastRedisHealth`
1529
+ * fresh so the next heartbeat carries meaningful status.
1530
+ *
1531
+ * Ties into `_checkDatabaseDependency` / `_checkRedisDependency` which
1532
+ * remain the single source of truth for what "healthy" means.
1533
+ *
1534
+ * @returns {Promise<void>}
1512
1535
  */
1513
- _setupHealthChecks() {
1514
- const healthEndpoint = this.config.wrapper?.health?.endpoint || '/health';
1515
-
1516
- const healthHandler = async (req, res) => {
1517
- const dependencyIssues = [];
1518
- const health = {
1519
- status: 'healthy',
1520
- service: this.config.service?.name || 'unnamed-service',
1521
- timestamp: new Date().toISOString(),
1522
- requiredConnectors: this.requiredConnectors || null,
1523
- components: {
1524
- http: 'healthy',
1525
- mq: 'disabled',
1526
- registry: this.registryClient ? 'healthy' : 'disabled',
1527
- cache: this.cacheConnector ? 'healthy' : 'disabled',
1528
- state: this.stateConnector ? 'healthy' : 'disabled',
1529
- database: this.requiredConnectors?.db === true ? 'unknown' : 'not_required',
1530
- redis: this.requiredConnectors?.redis === true ? 'unknown' : 'not_required'
1531
- }
1532
- };
1533
-
1534
- if (this.mqClient) {
1535
- const transport = this.mqClient._transport;
1536
- if (transport && typeof transport.performHealthCheck === 'function') {
1537
- try {
1538
- const mqHealth = await transport.performHealthCheck();
1539
- health.components.mq = mqHealth.healthy ? 'healthy' : 'unhealthy';
1540
- if (!mqHealth.healthy) {
1541
- health.mqIssues = mqHealth.issues;
1542
- }
1543
- } catch (e) {
1544
- health.components.mq = 'unhealthy';
1545
- health.mqIssues = [e.message];
1546
- }
1547
- } else {
1548
- health.components.mq = this.mqClient.isConnected() ? 'healthy' : 'unhealthy';
1549
- }
1550
- }
1551
-
1552
- if (!this.requiredConnectors) {
1553
- health.components.database = 'unhealthy';
1554
- health.components.redis = 'unhealthy';
1555
- dependencyIssues.push(
1556
- 'Integration contract was not loaded - expected config/service/integration-contract.json during initialization'
1557
- );
1558
- } else {
1536
+ async refreshDependencyHealth() {
1537
+ if (!this.requiredConnectors) {
1538
+ // Integration contract not loaded — dependency state is unknown.
1539
+ this._lastDbHealth = 'unknown';
1540
+ this._lastRedisHealth = 'unknown';
1541
+ return;
1542
+ }
1543
+ if (this.requiredConnectors.db === true) {
1544
+ try {
1559
1545
  const dbDependency = await this._checkDatabaseDependency();
1560
- health.components.database = dbDependency.status;
1561
- if (dbDependency.status === 'unhealthy' && dbDependency.reason) {
1562
- dependencyIssues.push(`[db] ${dbDependency.reason}`);
1563
- }
1564
-
1565
- const redisDependency = await this._checkRedisDependency();
1566
- health.components.redis = redisDependency.status;
1567
- if (redisDependency.status === 'unhealthy' && redisDependency.reason) {
1568
- dependencyIssues.push(`[redis] ${redisDependency.reason}`);
1569
- }
1546
+ this._lastDbHealth = dbDependency.status;
1547
+ } catch (_) {
1548
+ this._lastDbHealth = 'unhealthy';
1570
1549
  }
1571
-
1572
- if (dependencyIssues.length > 0) {
1573
- health.dependencyIssues = dependencyIssues;
1574
- }
1575
-
1576
- const statuses = Object.values(health.components);
1577
- if (statuses.includes('unhealthy')) {
1578
- health.status = 'unhealthy';
1579
- res.status(503);
1580
- } else {
1581
- res.status(200);
1550
+ }
1551
+ if (this.requiredConnectors.redis === true) {
1552
+ try {
1553
+ const redisDependency = await this._checkRedisDependency();
1554
+ this._lastRedisHealth = redisDependency.status;
1555
+ } catch (_) {
1556
+ this._lastRedisHealth = 'unhealthy';
1582
1557
  }
1583
-
1584
- res.json(health);
1585
- };
1586
-
1587
- // Health route must be registered BEFORE catch-all 404/error handlers.
1588
- // Services register routes (including 404) in app.js before ServiceWrapper initializes.
1589
- // We use _healthHandler reference that bootstrap() registered early in the stack.
1590
- if (this._healthRouteRegistered) {
1591
- this._healthImpl = healthHandler;
1592
- } else {
1593
- this.app.get(healthEndpoint, healthHandler);
1594
1558
  }
1595
-
1596
- this.logger?.info(`Health check endpoint registered at ${healthEndpoint}`);
1597
-
1598
- this._setupInfoEndpoint();
1599
1559
  }
1600
1560
 
1601
1561
  /**
1602
- * Setup /info endpoint exposing versions, fingerprint, and validation status.
1603
- * Registered alongside /health — always available once wrapper is initialized.
1604
- * @private
1562
+ * Start the health heartbeat publisher (ADR 0005, Phase 0.10).
1563
+ *
1564
+ * Wires this biz service into the existing infrastructure heartbeat
1565
+ * mechanism at `@onlineapps/infrastructure-tools` — the same mechanism
1566
+ * already used in production by api_gateway, api_services_validator,
1567
+ * api_delivery_endpoint. Registry's `infrastructureHealth.listener`
1568
+ * consumes the messages and `infrastructureHealthTracker` maintains
1569
+ * the state.
1570
+ *
1571
+ * Payload shape follows the validated infra format:
1572
+ * { serviceName, version, status, timestamp,
1573
+ * components: { mq, redis, db, cache }, correlationId }
1574
+ *
1575
+ * First heartbeat is emitted synchronously by publisher.start().
1576
+ * On publish failure, the shared publisher logs and continues — the
1577
+ * service is not crashed by a heartbeat glitch.
1578
+ *
1579
+ * @returns {Promise<void>}
1605
1580
  */
1606
- _setupInfoEndpoint() {
1607
- const fs = require('fs');
1608
- const path = require('path');
1609
- const wrapperPkg = require('../package.json');
1610
-
1611
- this.app.get('/info', async (req, res) => {
1612
- const serviceName = this.config.service?.name || 'unnamed-service';
1613
- const serviceVersion = this.config.service?.version || 'unknown';
1614
-
1615
- const info = {
1616
- service: serviceName,
1617
- serviceVersion,
1618
- serviceWrapper: wrapperPkg.version,
1619
- timestamp: new Date().toISOString()
1620
- };
1621
-
1622
- // Collect @onlineapps/* dependency versions from service package.json
1623
- info.dependencies = {};
1624
- if (this.serviceRoot) {
1625
- try {
1626
- const svcPkgPath = path.join(this.serviceRoot, 'package.json');
1627
- const svcPkg = JSON.parse(fs.readFileSync(svcPkgPath, 'utf8'));
1628
- const deps = svcPkg.dependencies || {};
1629
- for (const [name, version] of Object.entries(deps)) {
1630
- if (name.startsWith('@onlineapps/')) {
1631
- info.dependencies[name] = version;
1632
- }
1633
- }
1634
- } catch (e) {
1635
- info.dependencies._error = e.message;
1636
- }
1637
- }
1638
-
1639
- // Validation proof and contract fingerprint
1640
- info.validation = this._getValidationInfo();
1581
+ async startHealthHeartbeat() {
1582
+ if (this._heartbeatPublisher) {
1583
+ this.logger?.warn('[ServiceWrapper] Heartbeat publisher already started');
1584
+ return;
1585
+ }
1586
+ if (!this.mqClient) {
1587
+ throw new Error('[ServiceWrapper] mqClient is required to start heartbeat publisher');
1588
+ }
1641
1589
 
1642
- // Infrastructure fingerprint from Redis (if cache available)
1643
- info.infraFingerprint = await this._getInfraFingerprint();
1590
+ const { createBaseClientAdapter } = require('@onlineapps/infrastructure-tools');
1591
+ const serviceName = this.config.service?.name || 'unnamed-service';
1592
+ const serviceVersion = this.config.service?.version || '1.0.0';
1644
1593
 
1645
- // Standard level (determined from filesystem, not from validation proof)
1646
- info.standardLevel = this._getStandardLevel();
1594
+ const getHealthData = () => this._collectHeartbeatComponents();
1647
1595
 
1648
- // Revalidation state
1649
- info.revalidation = {
1650
- state: this._revalidationState,
1651
- attempt: this._revalidationAttempt
1652
- };
1596
+ // createBaseClientAdapter builds a publisher that uses BaseClient.publish;
1597
+ // our mqClient exposes .publish so this is a direct fit.
1598
+ this._heartbeatPublisher = createBaseClientAdapter(
1599
+ this.mqClient,
1600
+ serviceName,
1601
+ getHealthData,
1602
+ { ...(this.config.wrapper?.heartbeat || {}) },
1603
+ this.logger
1604
+ );
1653
1605
 
1654
- res.json(info);
1655
- });
1606
+ // Some downstream consumers of createBaseClientAdapter attach the
1607
+ // service version onto messages via the publisher constructor;
1608
+ // createHealthPublisher accepts version only via options — we can
1609
+ // rely on the tracker's `version: 'unknown'` default when absent.
1610
+ void serviceVersion;
1611
+
1612
+ // Prime the cached dependency signals so the first heartbeat has
1613
+ // meaningful component state instead of 'unknown' placeholders.
1614
+ await this.refreshDependencyHealth();
1615
+
1616
+ // Periodic dependency refresh so subsequent heartbeats stay fresh
1617
+ // without probing DB/Redis inside the sync getHealthData callback.
1618
+ const refreshInterval = this.config.wrapper?.heartbeat?.dependencyRefreshMs || 10000;
1619
+ this._heartbeatRefreshInterval = setInterval(() => {
1620
+ this.refreshDependencyHealth().catch((e) => {
1621
+ this.logger?.warn('[ServiceWrapper] Dependency refresh failed', { error: e.message });
1622
+ });
1623
+ }, refreshInterval);
1656
1624
 
1657
- this.logger?.info('Info endpoint registered at /info');
1625
+ await this._heartbeatPublisher.start();
1626
+ this.logger?.info(`[ServiceWrapper] Heartbeat publisher started for ${serviceName}`);
1658
1627
  }
1659
1628
 
1660
1629
  /**
1661
- * Collect validation proof info for /info endpoint.
1630
+ * Snapshot of connector-oriented component health for the heartbeat
1631
+ * `components` map. Values are one of 'healthy' | 'unhealthy' | 'unknown',
1632
+ * matching what `infrastructureHealth.listener` accepts.
1633
+ *
1634
+ * The connector-orientation (mq/redis/db/cache) matches what the legacy
1635
+ * HTTP /health handler already computes via _checkDatabaseDependency /
1636
+ * _checkRedisDependency; here we produce the same signals in the shape
1637
+ * the shared listener validates.
1638
+ *
1662
1639
  * @private
1663
- * @returns {Object} Validation status summary
1640
+ * @returns {Object<string, 'healthy'|'unhealthy'|'unknown'>}
1664
1641
  */
1665
- _getValidationInfo() {
1666
- const fs = require('fs');
1667
- const path = require('path');
1642
+ _collectHeartbeatComponents() {
1643
+ const components = {
1644
+ mq: 'unknown',
1645
+ redis: 'unknown',
1646
+ db: 'unknown',
1647
+ cache: 'unknown'
1648
+ };
1668
1649
 
1669
- if (!this.serviceRoot) {
1670
- return { status: 'no_service_root' };
1650
+ // MQ — mqClient is the transport; if isConnected() returns true it is up.
1651
+ if (this.mqClient && typeof this.mqClient.isConnected === 'function') {
1652
+ try {
1653
+ components.mq = this.mqClient.isConnected() ? 'healthy' : 'unhealthy';
1654
+ } catch (_) {
1655
+ components.mq = 'unhealthy';
1656
+ }
1657
+ } else if (this.mqClient) {
1658
+ components.mq = 'healthy';
1671
1659
  }
1672
1660
 
1673
- const proofPath = path.join(this.serviceRoot, PROOF_RELATIVE_PATH);
1674
-
1675
- if (!fs.existsSync(proofPath)) {
1676
- return { status: 'no_proof' };
1661
+ // Redis / DB / cache — heartbeat is fire-and-forget so we only sample
1662
+ // the last known state; the full async probe belongs to the periodic
1663
+ // dependency checks, not to every 5-second heartbeat.
1664
+ if (!this.requiredConnectors) {
1665
+ // Integration contract not loaded — treat DB/redis as not required.
1666
+ components.db = 'unknown';
1667
+ components.redis = 'unknown';
1668
+ } else {
1669
+ components.db = this.requiredConnectors.db === true
1670
+ ? (this._lastDbHealth || 'unknown')
1671
+ : 'unknown';
1672
+ components.redis = this.requiredConnectors.redis === true
1673
+ ? (this._lastRedisHealth || 'unknown')
1674
+ : 'unknown';
1677
1675
  }
1678
1676
 
1679
- try {
1680
- const raw = JSON.parse(fs.readFileSync(proofPath, 'utf8'));
1681
- const data = raw.validationData || {};
1677
+ components.cache = this.cacheConnector ? 'healthy' : 'unknown';
1682
1678
 
1683
- const validatedAt = data.validatedAt ? new Date(data.validatedAt) : null;
1684
- const ageMs = validatedAt ? Date.now() - validatedAt.getTime() : null;
1685
-
1686
- return {
1687
- status: 'valid',
1688
- contractFingerprint: data.contractFingerprint || null,
1689
- validatedAt: data.validatedAt || null,
1690
- proofAge: ageMs !== null ? `${Math.round(ageMs / 60000)}m` : null,
1691
- validatorVersion: data.validatorVersion || null,
1692
- testsRun: data.testsRun ?? null,
1693
- testsPassed: data.testsPassed ?? null
1694
- };
1695
- } catch (e) {
1696
- return { status: 'error', error: e.message };
1697
- }
1679
+ return components;
1698
1680
  }
1699
1681
 
1700
1682
  /**
1701
- * Determine the implementation standard level from the filesystem.
1702
- * Uses ServiceStructureValidator's standard level checks.
1703
- * @private
1704
- * @returns {{ level: string|null, details: Array }}
1683
+ * Stop the heartbeat publisher (used in cleanup paths).
1684
+ * @returns {void}
1705
1685
  */
1706
- _getStandardLevel() {
1707
- if (!this.serviceRoot) {
1708
- return { level: null, details: [] };
1686
+ stopHealthHeartbeat() {
1687
+ if (this._heartbeatRefreshInterval) {
1688
+ clearInterval(this._heartbeatRefreshInterval);
1689
+ this._heartbeatRefreshInterval = null;
1709
1690
  }
1710
-
1711
- try {
1712
- const { ServiceStructureValidator } = require('@onlineapps/conn-orch-validator/src/validators/ServiceStructureValidator');
1713
- const validator = new ServiceStructureValidator(this.serviceRoot);
1714
- return validator.determineStandardLevel();
1715
- } catch (e) {
1716
- return { level: null, error: e.message };
1717
- }
1718
- }
1719
-
1720
- /**
1721
- * Read infra:fingerprint from Redis (if cache connector is available).
1722
- * @private
1723
- * @returns {Promise<Object|null>}
1724
- */
1725
- async _getInfraFingerprint() {
1726
- if (!this.cacheConnector) {
1727
- return null;
1728
- }
1729
-
1730
- try {
1731
- const raw = await this.cacheConnector.get('infra:fingerprint');
1732
- if (!raw) return null;
1733
- const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
1734
- return {
1735
- hash: parsed.hash || null,
1736
- timestamp: parsed.timestamp || null
1737
- };
1738
- } catch (e) {
1739
- return { error: e.message };
1691
+ if (this._heartbeatPublisher) {
1692
+ try {
1693
+ this._heartbeatPublisher.stop();
1694
+ } catch (e) {
1695
+ this.logger?.warn('[ServiceWrapper] Heartbeat stop() threw', { error: e.message });
1696
+ }
1697
+ this._heartbeatPublisher = null;
1740
1698
  }
1741
1699
  }
1742
1700
 
@@ -2162,49 +2120,6 @@ class ServiceWrapper {
2162
2120
  throw err;
2163
2121
  }
2164
2122
 
2165
- /**
2166
- * Extract all registered Express routes from the app router stack.
2167
- * Handles nested Router instances (e.g. app.use('/api', router)).
2168
- * Compatible with Express 4 (app._router) and Express 5 (app.router).
2169
- * @private
2170
- * @returns {Array<{method: string, path: string}>}
2171
- */
2172
- _extractExpressRoutes() {
2173
- const routes = [];
2174
- const stack = this.app._router?.stack || this.app.router?.stack || [];
2175
-
2176
- const extractFromStack = (layers, prefix) => {
2177
- for (const layer of layers) {
2178
- if (layer.route) {
2179
- for (const method of Object.keys(layer.route.methods)) {
2180
- routes.push({ method: method.toUpperCase(), path: `${prefix}${layer.route.path}` });
2181
- }
2182
- } else if (layer.name === 'router' && layer.handle?.stack) {
2183
- let mountPath = '';
2184
-
2185
- // Express 4: extract prefix from regexp
2186
- // Multi-segment paths like /api/v1 produce regexp source ^\/api\/v1\/?...
2187
- // Each path segment is escaped as \/segment, so we capture all segments.
2188
- if (layer.regexp?.source) {
2189
- const match = layer.regexp.source.match(/^\^((?:\\\/[a-zA-Z0-9_.~-]+)+)/);
2190
- if (match) mountPath = match[1].replace(/\\\//g, '/');
2191
- }
2192
-
2193
- // Express 5: probe the matcher to discover mount prefix
2194
- if (!mountPath && layer.matchers?.[0]) {
2195
- const probe = layer.matchers[0]('/api');
2196
- if (probe && probe.path) mountPath = probe.path;
2197
- }
2198
-
2199
- extractFromStack(layer.handle.stack, `${prefix}${mountPath}`);
2200
- }
2201
- }
2202
- };
2203
-
2204
- extractFromStack(stack, '');
2205
- return routes;
2206
- }
2207
-
2208
2123
  /**
2209
2124
  * Instantiate the v3 invocation pipeline (HandlerRegistry, SchemaValidator,
2210
2125
  * ContextBuilder, ErrorMapper, HandlerLoader). Called from initialize()
@@ -2699,16 +2614,11 @@ class ServiceWrapper {
2699
2614
  throw new Error('Service version is required for validation');
2700
2615
  }
2701
2616
 
2702
- const { name: serviceName, version: serviceVersion, port: servicePort } = this.config.service;
2703
-
2704
- if (!servicePort) {
2705
- throw new Error('Service port is required for validation (config.service.port)');
2706
- }
2617
+ const { name: serviceName, version: serviceVersion } = this.config.service;
2707
2618
 
2708
- const serviceUrl = this.config.service?.url;
2709
- if (!serviceUrl) {
2710
- throw new Error('[ServiceWrapper] Missing configuration - config.service.url is required for validation (set SERVICE_URL in conn-config/config.json placeholder)');
2711
- }
2619
+ // ADR 0005: biz services have no HTTP surface — no port to bind, no
2620
+ // URL to advertise. Prior versions of this method required both;
2621
+ // dropped in 3.4.1 because they had no consumer post-A2.
2712
2622
 
2713
2623
  // Phase 5: Check restart-aware failure tracking
2714
2624
  const failureData = this._readValidationFailureFile();
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- // See: docs/standards/tenant-context-contract.md
3
+ // See: docs/biz/20-tenancy/tenant-context.md
4
4
  /**
5
5
  * Tenant context middleware — requires x-tenant-id + x-workspace-id headers.
6
6
  *
package/src/index.js CHANGED
@@ -22,136 +22,60 @@ const { createTenantContextMiddleware } = require('./createTenantContextMiddlewa
22
22
  // @onlineapps/conn-orch-orchestrator. The former ApiCaller /
23
23
  // @onlineapps/conn-orch-api-mapper was retired 2026-04-21 (ADR-0001);
24
24
  // per-operation dispatch happens in-process via HandlerRegistry.
25
-
26
- // Note: HTTP logging middleware is now in each service's middlewares/httpLogging.js
27
- // It uses lazy logger access via lib/logger.js (set after wrapper.initialize())
25
+ //
26
+ // ADR 0005 (2026-08-17): biz containers run zero HTTP. bootstrap() does
27
+ // not create or listen on any Express app. Status flows via the MQ
28
+ // heartbeat mechanism at @onlineapps/infrastructure-tools, consumed by
29
+ // Registry's infrastructureHealth.listener. See docs/biz/80-decisions/0005-.
28
30
 
29
31
  /**
30
- * Bootstrap a business service with standard configuration
31
- * Eliminates boilerplate code in service index.js
32
- *
32
+ * Bootstrap a business service with standard configuration.
33
+ * Eliminates boilerplate code in service index.js.
34
+ *
35
+ * Zero-HTTP shape (ADR 0005): no Express app is created, no port is bound.
36
+ * Business operations flow via MQ + in-process HandlerRegistry dispatch.
37
+ * Health flows via MQ heartbeat (Phase 0.10 inside wrapper.initialize()).
38
+ *
33
39
  * @param {string} serviceRoot - Service root directory (__dirname from index.js)
34
40
  * @param {Object} [options] - Optional overrides
35
- * @param {Object} [options.app] - Express app (default: require('./src/app'))
36
41
  * @param {Object} [options.config] - Config (default: require('./src/config'))
37
42
  * @param {Function} [options.setLogger] - Logger setter (default: require('./src/lib/logger').setLogger)
38
- * @returns {Promise<{wrapper: ServiceWrapper, server: Object}>}
39
- *
43
+ * @param {string} [options.serviceBaseDir] - Handler base dir (default: <serviceRoot>/src)
44
+ * @returns {Promise<{wrapper: ServiceWrapper}>}
45
+ *
40
46
  * @example
41
47
  * // index.js (minimal)
42
48
  * require('@onlineapps/service-wrapper').bootstrap(__dirname);
43
49
  */
44
50
  async function bootstrap(serviceRoot, options = {}) {
45
- // Load app, config, and logger from service
46
- const app = options.app || require(path.join(serviceRoot, 'src', 'app'));
47
51
  const config = options.config || require(path.join(serviceRoot, 'src', 'config'));
48
-
49
- // Logger setter is optional - some services may not have it
52
+
53
+ // Logger setter is optional some services may not have one.
50
54
  let setLogger = options.setLogger;
51
55
  if (!setLogger) {
52
56
  try {
53
57
  const loggerModule = require(path.join(serviceRoot, 'src', 'lib', 'logger'));
54
58
  setLogger = loggerModule.setLogger;
55
59
  } catch (e) {
56
- // No logger module - that's OK
57
60
  setLogger = () => {};
58
61
  }
59
62
  }
60
63
 
61
64
  console.log(`Starting ${config.service.name} v${config.service.version}...`);
62
65
 
63
- // 0. Apply shared tenant context middleware (x-tenant-id + x-workspace-id + x-person-id)
64
- const tenantContextMw = createTenantContextMiddleware({
65
- serviceName: config.service.name,
66
- tenantContext: config.wrapper?.tenantContext
67
- });
68
- app.use(tenantContextMw);
69
-
70
- // Express routes are processed in registration order.
71
- // Services usually register routes inside src/app BEFORE bootstrap runs, so app.use() here would be too late.
72
- // We must enforce tenant context BEFORE routes: move our middleware to the beginning of the router stack.
73
- // Fail-fast if we cannot guarantee correct order.
74
- if (!app || !app._router || !Array.isArray(app._router.stack) || app._router.stack.length === 0) {
75
- throw new Error(
76
- `[service-wrapper][TenantContext] Cannot enforce tenant context - Express router stack not available. ` +
77
- `Fix: ensure the service exports an Express app instance with registered routes before bootstrap.`
78
- );
79
- }
80
-
81
- const stack = app._router.stack;
82
- const lastLayer = stack[stack.length - 1];
83
- const isOurMiddleware =
84
- lastLayer &&
85
- lastLayer.handle &&
86
- typeof lastLayer.handle === 'function' &&
87
- lastLayer.handle.name === tenantContextMw.name;
88
-
89
- if (!isOurMiddleware) {
90
- throw new Error(
91
- `[service-wrapper][TenantContext] Cannot enforce tenant context - middleware placement is not deterministic. ` +
92
- `Fix: ensure bootstrap registers tenant context middleware before routes, or update service to export app factory.`
93
- );
94
- }
95
-
96
- stack.pop();
97
- // Insert AFTER expressInit (which sets up res.status/res.json via setPrototypeOf).
98
- // Position 0 = query, 1 = expressInit. Tenant context must run after both.
99
- const expressInitIdx = stack.findIndex(l => l.name === 'expressInit');
100
- if (expressInitIdx === -1) {
101
- throw new Error(
102
- '[service-wrapper][TenantContext] Express stack missing expressInit layer - ' +
103
- 'cannot guarantee tenant context runs after Express initialization'
104
- );
105
- }
106
- stack.splice(expressInitIdx + 1, 0, lastLayer);
107
-
108
- // Register /health route BEFORE service's 404 handler.
109
- // ServiceWrapper._setupHealthChecks() will later set _healthImpl with actual MQ checks.
110
- // Until then, this returns a basic healthy response (service is starting up).
111
- let wrapperRef = null;
112
- const healthEndpoint = config.wrapper?.health?.endpoint || '/health';
113
- app.get(healthEndpoint, async (req, res) => {
114
- if (wrapperRef && wrapperRef._healthImpl) {
115
- return wrapperRef._healthImpl(req, res);
116
- }
117
- res.json({ status: 'starting', service: config.service.name, timestamp: new Date().toISOString() });
118
- });
119
-
120
- // Move health route before 404/error handlers (same stack technique as tenant context)
121
- const healthLayer = stack.pop();
122
- if (healthLayer) {
123
- const catchAllIdx = stack.findIndex((layer, i) =>
124
- i > 0 && !layer.route && !['query', 'expressInit', 'tenantContextMiddleware', 'jsonParser', 'urlencodedParser'].includes(layer.name)
125
- );
126
- if (catchAllIdx > 0) {
127
- stack.splice(catchAllIdx, 0, healthLayer);
128
- } else {
129
- stack.push(healthLayer);
130
- }
131
- }
132
-
133
- // 1. Start HTTP server
134
- const PORT = config.service.port;
135
- const server = app.listen(PORT, () => {
136
- console.log(`✓ HTTP server listening on port ${PORT}`);
137
- console.log(`✓ Health check: /health`);
138
- });
139
-
140
- // 2. Initialize Service Wrapper (MQ, Registry, Monitoring, etc.)
141
- // v3 invocation model: HandlerLoader requires the service's src/ as base
142
- // dir for resolving handler refs (RFC §5.9). Biz services may override by
143
- // passing options.serviceBaseDir via bootstrap().
66
+ // v3 invocation model: HandlerLoader requires the service's src/ as the
67
+ // base dir for resolving handler refs (RFC §5.9). Biz services may
68
+ // override via options.serviceBaseDir.
144
69
  const serviceBaseDir = options.serviceBaseDir || path.join(serviceRoot, 'src');
70
+
145
71
  const wrapper = new ServiceWrapper({
146
- app,
147
- server,
148
72
  serviceRoot,
149
73
  serviceBaseDir,
150
74
  config: {
151
75
  service: {
152
76
  name: config.service.name,
153
77
  version: config.service.version,
154
- port: config.service.port,
78
+ // `port` intentionally omitted — no HTTP surface in biz containers.
155
79
  url: config.service.url,
156
80
  specificationEndpoint: config.service.specificationEndpoint,
157
81
  description: config.service.description,
@@ -164,33 +88,31 @@ async function bootstrap(serviceRoot, options = {}) {
164
88
  validationProof: config.validationProof
165
89
  });
166
90
 
167
- wrapper._healthRouteRegistered = true;
168
91
  await wrapper.initialize();
169
- wrapperRef = wrapper;
170
92
 
171
- // Initialize centralized logger for use throughout the service
172
93
  if (setLogger && wrapper.logger) {
173
94
  setLogger(wrapper.logger);
174
95
  }
175
96
 
176
- console.log(`✓ Service Wrapper initialized (MQ, Registry, Monitoring)`);
97
+ console.log(`✓ Service Wrapper initialized (MQ, Registry, Monitoring, Heartbeat)`);
177
98
  console.log(`✓ Local logs: logs/app.{date}.log`);
178
99
  console.log(`✓ ${config.service.name} ready\n`);
179
100
 
180
- // 3. Setup graceful shutdown
101
+ // Graceful shutdown no HTTP server to close, just tear down the wrapper.
181
102
  const shutdown = async () => {
182
103
  console.log('\nShutting down gracefully...');
183
- await wrapper.shutdown();
184
- server.close(() => {
185
- console.log('Server closed');
186
- process.exit(0);
187
- });
104
+ if (typeof wrapper.shutdown === 'function') {
105
+ await wrapper.shutdown();
106
+ } else if (typeof wrapper.stopHealthHeartbeat === 'function') {
107
+ wrapper.stopHealthHeartbeat();
108
+ }
109
+ process.exit(0);
188
110
  };
189
111
 
190
112
  process.on('SIGTERM', shutdown);
191
113
  process.on('SIGINT', shutdown);
192
114
 
193
- return { wrapper, server };
115
+ return { wrapper };
194
116
  }
195
117
 
196
118
  // v3 invocation-model components (RFC §5.9): exported for biz services
@@ -225,4 +147,4 @@ module.exports.ErrorMapper = ErrorMapper;
225
147
  module.exports.ValidationError = ValidationError;
226
148
  module.exports.UnknownOperationError = UnknownOperationError;
227
149
  module.exports.BusinessError = BusinessError;
228
- module.exports.AbortError = AbortError;
150
+ module.exports.AbortError = AbortError;