@lib-acr/express 0.0.0-stage → 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Application Control Room
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
1
+ # @lib-acr/express
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Express middleware: one `http_request` event per request (method, route template, status, duration, body sizes), X-Request-Id correlation, W3C traceparent joining, and an error handler that reports server-side failures.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @lib-acr/express
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import express from 'express';
15
+ import { ACRMiddleware, ACRErrorHandler, acrTraceHeaders } from '@lib-acr/express';
16
+
17
+ import { ACR } from '@lib-acr/node';
18
+
19
+ const acr = new ACR({ apiKey, apiSecret, endpoint }); // one client for the whole process
20
+ const app = express();
21
+ app.use(ACRMiddleware({ client: acr }));
22
+
23
+ app.get('/orders/:id', (req, res) => {
24
+ // req.acr.traceId / requestId are available for logs; acrTraceHeaders(req) for outgoing calls
25
+ res.json({ id: req.params.id });
26
+ });
27
+
28
+ app.use(ACRErrorHandler({ client: acr }));
29
+ ```
30
+
31
+ Passing `client` is recommended. You can pass `options` instead; one client is created per options object, so give
32
+ both handlers the **same** object (`const cfg = {…}; ACRMiddleware({ options: cfg }); ACRErrorHandler({ options: cfg })`).
33
+ Two different objects create two clients with separate buffers.
34
+
35
+ ## Notes
36
+
37
+ Route templates are recorded (`/orders/:id`), never concrete URLs. Unmatched paths are recorded as `(unmatched)`. Headers, query strings and bodies are never recorded. Health and readiness paths are ignored by default.
38
+
39
+ ## Remote control
40
+
41
+ Opt-in maintenance mode. Register it before your routes:
42
+
43
+ ```ts
44
+ import { maintenanceMiddleware } from '@lib-acr/express';
45
+
46
+ app.use(maintenanceMiddleware({
47
+ client: acr,
48
+ getUserId: (req) => req.user?.id, // users in allowUserIds keep access
49
+ exempt: (req) => req.path.startsWith('/webhooks'),
50
+ }));
51
+ ```
52
+
53
+ While maintenance is on, requests get `503` with `{ "error": "MAINTENANCE", "message": "…", "endsAt": "…" | null }`
54
+ and `Retry-After` (seconds until `endsAt`, otherwise 300). `/health`, `/healthz`, `/ready` and `/readyz` always pass,
55
+ including under a prefix such as `/api/health`.
56
+
57
+ ## Feature flags
58
+
59
+ ```ts
60
+ app.get('/checkout', (req, res) => {
61
+ const variant = acr.flag('new-checkout', false, { userId: req.user?.id }) ? 'new' : 'old';
62
+ res.json({ variant });
63
+ });
64
+ ```
65
+
66
+ ## Wire protocol
67
+
68
+ Every SDK sends the same protocol: `POST {endpoint}/ingest/{events|logs|errors|heartbeat}` with the `X-ACR-Api-Key`
69
+ header (and `X-ACR-Secret-Key` for server keys). The credential determines the app and environment. The body is
70
+ `{ schemaVersion: 1, sdk, items[1..500] }`.
71
+ Each item has a client `id`, which lets the server drop duplicates when a batch is retried.
72
+
73
+ Retries use exponential backoff with jitter for network errors, 429 and 5xx responses. Other 4xx responses are not retried.
74
+ Telemetry is buffered in a bounded in-memory queue. When it is full, the oldest items are dropped and counted in `stats()`.
75
+ SDK code never throws into the host application.
76
+
77
+ Metrics (`increment`, `gauge`, `histogram`) and traces are not yet available.
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.SDK_INFO = exports.resolveClient = exports.maintenanceMiddleware = exports.contextFromRequest = exports.acrTraceHeaders = exports.ACRErrorHandler = exports.ACRMiddleware = void 0;
18
+ var middleware_js_1 = require("./middleware.js");
19
+ Object.defineProperty(exports, "ACRMiddleware", { enumerable: true, get: function () { return middleware_js_1.ACRMiddleware; } });
20
+ Object.defineProperty(exports, "ACRErrorHandler", { enumerable: true, get: function () { return middleware_js_1.ACRErrorHandler; } });
21
+ Object.defineProperty(exports, "acrTraceHeaders", { enumerable: true, get: function () { return middleware_js_1.acrTraceHeaders; } });
22
+ Object.defineProperty(exports, "contextFromRequest", { enumerable: true, get: function () { return middleware_js_1.contextFromRequest; } });
23
+ Object.defineProperty(exports, "maintenanceMiddleware", { enumerable: true, get: function () { return middleware_js_1.maintenanceMiddleware; } });
24
+ Object.defineProperty(exports, "resolveClient", { enumerable: true, get: function () { return middleware_js_1.resolveClient; } });
25
+ __exportStar(require("@lib-acr/node"), exports);
26
+ var sdk_info_js_1 = require("./sdk-info.js");
27
+ Object.defineProperty(exports, "SDK_INFO", { enumerable: true, get: function () { return sdk_info_js_1.SDK_INFO; } });
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,iDAA4I;AAAnI,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AAAE,gHAAA,eAAe,OAAA;AAAE,mHAAA,kBAAkB,OAAA;AAAE,sHAAA,qBAAqB,OAAA;AAAE,8GAAA,aAAa,OAAA;AAElH,gDAA8B;AAC9B,6CAAyC;AAAhC,uGAAA,QAAQ,OAAA"}
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveClient = resolveClient;
4
+ exports.contextFromRequest = contextFromRequest;
5
+ exports.ACRMiddleware = ACRMiddleware;
6
+ exports.ACRErrorHandler = ACRErrorHandler;
7
+ exports.acrTraceHeaders = acrTraceHeaders;
8
+ exports.maintenanceMiddleware = maintenanceMiddleware;
9
+ const core_1 = require("@lib-acr/core");
10
+ const node_1 = require("@lib-acr/node");
11
+ const sdk_info_js_1 = require("./sdk-info.js");
12
+ const DEFAULT_IGNORED = ['/health', '/healthz', '/ready', '/readyz', '/favicon.ico'];
13
+ const REQUEST_ID_RE = /^[\w.-]{1,64}$/;
14
+ /** Clients created from `options`, keyed by the options object, so the middleware and the error handler share one. */
15
+ const clientsByOptions = new WeakMap();
16
+ /**
17
+ * Returns `options.client`, or the client created for `options.options`. The same options object always yields the
18
+ * same client, so ACRMiddleware and ACRErrorHandler configured with it share one buffer and one flush timer.
19
+ */
20
+ function resolveClient(options) {
21
+ if (options.client)
22
+ return options.client;
23
+ if (!options.options)
24
+ throw new Error('[acr] ExpressAcrOptions needs either client or options');
25
+ const existing = clientsByOptions.get(options.options);
26
+ if (existing)
27
+ return existing;
28
+ const created = new node_1.ACR(options.options, sdk_info_js_1.SDK_INFO);
29
+ clientsByOptions.set(options.options, created);
30
+ return created;
31
+ }
32
+ function isIgnored(path, ignored) {
33
+ return ignored.some((p) => (typeof p === 'string' ? p === path : p.test(path)));
34
+ }
35
+ /** Reads trace headers. The rest of the request headers are never kept. */
36
+ function contextFromRequest(req) {
37
+ const parsed = (0, core_1.parseTraceparent)(req.get('traceparent'));
38
+ const incomingRequestId = req.get('x-request-id');
39
+ const requestId = incomingRequestId && REQUEST_ID_RE.test(incomingRequestId) ? incomingRequestId : (0, core_1.generateId)();
40
+ return {
41
+ traceId: parsed?.traceId ?? (0, core_1.generateTraceId)(),
42
+ spanId: (0, core_1.generateSpanId)(),
43
+ parentSpanId: parsed?.spanId ?? null,
44
+ requestId,
45
+ };
46
+ }
47
+ /** Route template only (for example /orders/:id), never the concrete URL, so cardinality stays bounded. */
48
+ function routeOf(req) {
49
+ if (!req.route)
50
+ return '(unmatched)';
51
+ const path = typeof req.route.path === 'string' ? req.route.path : '';
52
+ return `${req.baseUrl ?? ''}${path}` || '/';
53
+ }
54
+ /**
55
+ * Records one `http_request` event per request: method, route template, status, duration and body sizes. It sets
56
+ * X-Request-Id on the response and exposes req.acr for logs and errors. Headers, query strings and bodies are not
57
+ * recorded.
58
+ */
59
+ function ACRMiddleware(options) {
60
+ const client = resolveClient(options);
61
+ const ignored = [...DEFAULT_IGNORED, ...(options.ignorePaths ?? [])];
62
+ const eventName = options.eventName ?? 'http_request';
63
+ return function acrRequestTracing(req, res, next) {
64
+ if (isIgnored(req.path, ignored)) {
65
+ next();
66
+ return;
67
+ }
68
+ const ctx = contextFromRequest(req);
69
+ req.acr = ctx;
70
+ res.locals.acr = ctx;
71
+ res.setHeader('X-Request-Id', ctx.requestId);
72
+ const started = process.hrtime.bigint();
73
+ let recorded = false;
74
+ const record = (aborted) => {
75
+ if (recorded)
76
+ return;
77
+ recorded = true;
78
+ const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
79
+ const status = aborted ? 499 : res.statusCode;
80
+ const requestBytes = Number(req.get('content-length') ?? 0) || 0;
81
+ const responseBytes = Number(res.getHeader('content-length') ?? 0) || 0;
82
+ client.trackEvent(eventName, {
83
+ method: req.method,
84
+ route: routeOf(req),
85
+ status,
86
+ statusClass: `${Math.floor(status / 100)}xx`,
87
+ durationMs: Math.round(durationMs * 100) / 100,
88
+ requestBytes,
89
+ responseBytes,
90
+ aborted,
91
+ parentSpanId: ctx.parentSpanId ?? undefined,
92
+ }, {
93
+ traceId: ctx.traceId,
94
+ spanId: ctx.spanId,
95
+ requestId: ctx.requestId,
96
+ userId: options.getUserId?.(req),
97
+ });
98
+ };
99
+ res.once('finish', () => record(false));
100
+ res.once('close', () => record(!res.writableEnded));
101
+ next();
102
+ };
103
+ }
104
+ /**
105
+ * Error handler to register after your routes. It reports server-side failures (5xx or an error without a status)
106
+ * and always passes the error on, so the normal Express error response is unchanged. Client errors such as 400 or 404
107
+ * are not reported.
108
+ */
109
+ function ACRErrorHandler(options) {
110
+ const client = resolveClient(options);
111
+ return function acrErrorReporter(err, req, _res, next) {
112
+ const status = statusOf(err);
113
+ if (status === null || status >= 500) {
114
+ client.captureError(err, {
115
+ handled: false,
116
+ source: 'backend',
117
+ route: routeOf(req),
118
+ traceId: req.acr?.traceId,
119
+ spanId: req.acr?.spanId,
120
+ requestId: req.acr?.requestId,
121
+ userId: options.getUserId?.(req),
122
+ attributes: { method: req.method, status: status ?? 500, framework: 'express' },
123
+ });
124
+ }
125
+ next(err);
126
+ };
127
+ }
128
+ function statusOf(err) {
129
+ const candidate = err;
130
+ const value = candidate?.status ?? candidate?.statusCode;
131
+ return typeof value === 'number' ? value : null;
132
+ }
133
+ /** Headers to add to outgoing calls so downstream services join the same trace. */
134
+ function acrTraceHeaders(req) {
135
+ const ctx = req.acr;
136
+ if (!ctx)
137
+ return {};
138
+ return {
139
+ traceparent: (0, core_1.formatTraceparent)({ traceId: ctx.traceId, spanId: ctx.spanId }),
140
+ 'x-request-id': ctx.requestId,
141
+ };
142
+ }
143
+ /**
144
+ * Opt-in maintenance mode for APIs. While the control snapshot has maintenance enabled, requests get `503` with
145
+ * `{ error: 'MAINTENANCE', message, endsAt }` and `Retry-After` (seconds until `endsAt`, else 300). Health and
146
+ * readiness probes (/health, /healthz, /ready, /readyz) always pass, as do users in `allowUserIds` (via `getUserId`)
147
+ * and requests for which `exempt` returns true. Register it before your routes.
148
+ */
149
+ function maintenanceMiddleware(options) {
150
+ const client = resolveClient(options);
151
+ return function acrMaintenance(req, res, next) {
152
+ if ((0, core_1.isMaintenanceExemptPath)(req.path)) {
153
+ next();
154
+ return;
155
+ }
156
+ let blocked = null;
157
+ try {
158
+ blocked = (0, core_1.maintenanceResponse)(client.getControlState(), { userId: options.getUserId?.(req) });
159
+ if (blocked && options.exempt?.(req))
160
+ blocked = null;
161
+ }
162
+ catch {
163
+ blocked = null; // a failing exempt/getUserId callback must not take the API down
164
+ }
165
+ if (!blocked) {
166
+ next();
167
+ return;
168
+ }
169
+ res.setHeader('Retry-After', String(blocked.retryAfterSeconds));
170
+ res.setHeader('Cache-Control', 'no-store');
171
+ res.status(blocked.status).json(blocked.body);
172
+ };
173
+ }
174
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_INFO = void 0;
4
+ // Generated by scripts/sync-sdk-info.mjs from package.json. Do not edit by hand.
5
+ exports.SDK_INFO = { name: '@lib-acr/express', version: '0.0.1' };
6
+ //# sourceMappingURL=sdk-info.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-info.js","sourceRoot":"","sources":["../../src/sdk-info.ts"],"names":[],"mappings":";;;AAAA,iFAAiF;AACpE,QAAA,QAAQ,GAAG,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAW,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { ACRMiddleware, ACRErrorHandler, acrTraceHeaders, contextFromRequest, maintenanceMiddleware, resolveClient } from './middleware.js';
2
+ export * from '@lib-acr/node';
3
+ export { SDK_INFO } from './sdk-info.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE5I,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,166 @@
1
+ import { formatTraceparent, generateId, generateSpanId, generateTraceId, isMaintenanceExemptPath, maintenanceResponse, parseTraceparent, } from '@lib-acr/core';
2
+ import { ACR } from '@lib-acr/node';
3
+ import { SDK_INFO } from './sdk-info.js';
4
+ const DEFAULT_IGNORED = ['/health', '/healthz', '/ready', '/readyz', '/favicon.ico'];
5
+ const REQUEST_ID_RE = /^[\w.-]{1,64}$/;
6
+ /** Clients created from `options`, keyed by the options object, so the middleware and the error handler share one. */
7
+ const clientsByOptions = new WeakMap();
8
+ /**
9
+ * Returns `options.client`, or the client created for `options.options`. The same options object always yields the
10
+ * same client, so ACRMiddleware and ACRErrorHandler configured with it share one buffer and one flush timer.
11
+ */
12
+ export function resolveClient(options) {
13
+ if (options.client)
14
+ return options.client;
15
+ if (!options.options)
16
+ throw new Error('[acr] ExpressAcrOptions needs either client or options');
17
+ const existing = clientsByOptions.get(options.options);
18
+ if (existing)
19
+ return existing;
20
+ const created = new ACR(options.options, SDK_INFO);
21
+ clientsByOptions.set(options.options, created);
22
+ return created;
23
+ }
24
+ function isIgnored(path, ignored) {
25
+ return ignored.some((p) => (typeof p === 'string' ? p === path : p.test(path)));
26
+ }
27
+ /** Reads trace headers. The rest of the request headers are never kept. */
28
+ export function contextFromRequest(req) {
29
+ const parsed = parseTraceparent(req.get('traceparent'));
30
+ const incomingRequestId = req.get('x-request-id');
31
+ const requestId = incomingRequestId && REQUEST_ID_RE.test(incomingRequestId) ? incomingRequestId : generateId();
32
+ return {
33
+ traceId: parsed?.traceId ?? generateTraceId(),
34
+ spanId: generateSpanId(),
35
+ parentSpanId: parsed?.spanId ?? null,
36
+ requestId,
37
+ };
38
+ }
39
+ /** Route template only (for example /orders/:id), never the concrete URL, so cardinality stays bounded. */
40
+ function routeOf(req) {
41
+ if (!req.route)
42
+ return '(unmatched)';
43
+ const path = typeof req.route.path === 'string' ? req.route.path : '';
44
+ return `${req.baseUrl ?? ''}${path}` || '/';
45
+ }
46
+ /**
47
+ * Records one `http_request` event per request: method, route template, status, duration and body sizes. It sets
48
+ * X-Request-Id on the response and exposes req.acr for logs and errors. Headers, query strings and bodies are not
49
+ * recorded.
50
+ */
51
+ export function ACRMiddleware(options) {
52
+ const client = resolveClient(options);
53
+ const ignored = [...DEFAULT_IGNORED, ...(options.ignorePaths ?? [])];
54
+ const eventName = options.eventName ?? 'http_request';
55
+ return function acrRequestTracing(req, res, next) {
56
+ if (isIgnored(req.path, ignored)) {
57
+ next();
58
+ return;
59
+ }
60
+ const ctx = contextFromRequest(req);
61
+ req.acr = ctx;
62
+ res.locals.acr = ctx;
63
+ res.setHeader('X-Request-Id', ctx.requestId);
64
+ const started = process.hrtime.bigint();
65
+ let recorded = false;
66
+ const record = (aborted) => {
67
+ if (recorded)
68
+ return;
69
+ recorded = true;
70
+ const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
71
+ const status = aborted ? 499 : res.statusCode;
72
+ const requestBytes = Number(req.get('content-length') ?? 0) || 0;
73
+ const responseBytes = Number(res.getHeader('content-length') ?? 0) || 0;
74
+ client.trackEvent(eventName, {
75
+ method: req.method,
76
+ route: routeOf(req),
77
+ status,
78
+ statusClass: `${Math.floor(status / 100)}xx`,
79
+ durationMs: Math.round(durationMs * 100) / 100,
80
+ requestBytes,
81
+ responseBytes,
82
+ aborted,
83
+ parentSpanId: ctx.parentSpanId ?? undefined,
84
+ }, {
85
+ traceId: ctx.traceId,
86
+ spanId: ctx.spanId,
87
+ requestId: ctx.requestId,
88
+ userId: options.getUserId?.(req),
89
+ });
90
+ };
91
+ res.once('finish', () => record(false));
92
+ res.once('close', () => record(!res.writableEnded));
93
+ next();
94
+ };
95
+ }
96
+ /**
97
+ * Error handler to register after your routes. It reports server-side failures (5xx or an error without a status)
98
+ * and always passes the error on, so the normal Express error response is unchanged. Client errors such as 400 or 404
99
+ * are not reported.
100
+ */
101
+ export function ACRErrorHandler(options) {
102
+ const client = resolveClient(options);
103
+ return function acrErrorReporter(err, req, _res, next) {
104
+ const status = statusOf(err);
105
+ if (status === null || status >= 500) {
106
+ client.captureError(err, {
107
+ handled: false,
108
+ source: 'backend',
109
+ route: routeOf(req),
110
+ traceId: req.acr?.traceId,
111
+ spanId: req.acr?.spanId,
112
+ requestId: req.acr?.requestId,
113
+ userId: options.getUserId?.(req),
114
+ attributes: { method: req.method, status: status ?? 500, framework: 'express' },
115
+ });
116
+ }
117
+ next(err);
118
+ };
119
+ }
120
+ function statusOf(err) {
121
+ const candidate = err;
122
+ const value = candidate?.status ?? candidate?.statusCode;
123
+ return typeof value === 'number' ? value : null;
124
+ }
125
+ /** Headers to add to outgoing calls so downstream services join the same trace. */
126
+ export function acrTraceHeaders(req) {
127
+ const ctx = req.acr;
128
+ if (!ctx)
129
+ return {};
130
+ return {
131
+ traceparent: formatTraceparent({ traceId: ctx.traceId, spanId: ctx.spanId }),
132
+ 'x-request-id': ctx.requestId,
133
+ };
134
+ }
135
+ /**
136
+ * Opt-in maintenance mode for APIs. While the control snapshot has maintenance enabled, requests get `503` with
137
+ * `{ error: 'MAINTENANCE', message, endsAt }` and `Retry-After` (seconds until `endsAt`, else 300). Health and
138
+ * readiness probes (/health, /healthz, /ready, /readyz) always pass, as do users in `allowUserIds` (via `getUserId`)
139
+ * and requests for which `exempt` returns true. Register it before your routes.
140
+ */
141
+ export function maintenanceMiddleware(options) {
142
+ const client = resolveClient(options);
143
+ return function acrMaintenance(req, res, next) {
144
+ if (isMaintenanceExemptPath(req.path)) {
145
+ next();
146
+ return;
147
+ }
148
+ let blocked = null;
149
+ try {
150
+ blocked = maintenanceResponse(client.getControlState(), { userId: options.getUserId?.(req) });
151
+ if (blocked && options.exempt?.(req))
152
+ blocked = null;
153
+ }
154
+ catch {
155
+ blocked = null; // a failing exempt/getUserId callback must not take the API down
156
+ }
157
+ if (!blocked) {
158
+ next();
159
+ return;
160
+ }
161
+ res.setHeader('Retry-After', String(blocked.retryAfterSeconds));
162
+ res.setHeader('Cache-Control', 'no-store');
163
+ res.status(blocked.status).json(blocked.body);
164
+ };
165
+ }
166
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/sync-sdk-info.mjs from package.json. Do not edit by hand.
2
+ export const SDK_INFO = { name: '@lib-acr/express', version: '0.0.1' };
3
+ //# sourceMappingURL=sdk-info.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-info.js","sourceRoot":"","sources":["../../src/sdk-info.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAW,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { ACRMiddleware, ACRErrorHandler, acrTraceHeaders, contextFromRequest, maintenanceMiddleware, resolveClient } from './middleware.js';
2
+ export type { AcrRequestContext, ExpressAcrOptions, MaintenanceMiddlewareOptions } from './middleware.js';
3
+ export * from '@lib-acr/node';
4
+ export { SDK_INFO } from './sdk-info.js';
@@ -0,0 +1,68 @@
1
+ import { AcrClient, type AcrOptions } from '@lib-acr/core';
2
+ import type { NextFunction, Request, Response } from 'express';
3
+ /** Trace and request identifiers for one incoming request. Available as req.acr and res.locals.acr. */
4
+ export interface AcrRequestContext {
5
+ traceId: string;
6
+ /** Span id of this server operation. */
7
+ spanId: string;
8
+ /** Span id of the caller, when a traceparent header arrived. */
9
+ parentSpanId: string | null;
10
+ requestId: string;
11
+ }
12
+ declare global {
13
+ namespace Express {
14
+ interface Request {
15
+ acr?: AcrRequestContext;
16
+ }
17
+ }
18
+ }
19
+ export interface ExpressAcrOptions {
20
+ /**
21
+ * Use an existing client (for example one shared with a NestJS or background-job setup). Recommended: create one
22
+ * `ACR` and pass it to both ACRMiddleware and ACRErrorHandler.
23
+ */
24
+ client?: AcrClient;
25
+ /**
26
+ * Options used to create a client when `client` is not given. One client is created per options object, so pass
27
+ * the same object to ACRMiddleware and ACRErrorHandler to share it.
28
+ */
29
+ options?: AcrOptions;
30
+ /** Exact paths or patterns that are not recorded. Default: health and readiness probes. */
31
+ ignorePaths?: Array<string | RegExp>;
32
+ /** Event name for request telemetry. Default `http_request`. */
33
+ eventName?: string;
34
+ /** Returns the authenticated user id for a request, if any. Keep it an opaque id. */
35
+ getUserId?: (req: Request) => string | undefined;
36
+ }
37
+ /**
38
+ * Returns `options.client`, or the client created for `options.options`. The same options object always yields the
39
+ * same client, so ACRMiddleware and ACRErrorHandler configured with it share one buffer and one flush timer.
40
+ */
41
+ export declare function resolveClient(options: ExpressAcrOptions): AcrClient;
42
+ /** Reads trace headers. The rest of the request headers are never kept. */
43
+ export declare function contextFromRequest(req: Request): AcrRequestContext;
44
+ /**
45
+ * Records one `http_request` event per request: method, route template, status, duration and body sizes. It sets
46
+ * X-Request-Id on the response and exposes req.acr for logs and errors. Headers, query strings and bodies are not
47
+ * recorded.
48
+ */
49
+ export declare function ACRMiddleware(options: ExpressAcrOptions): (req: Request, res: Response, next: NextFunction) => void;
50
+ /**
51
+ * Error handler to register after your routes. It reports server-side failures (5xx or an error without a status)
52
+ * and always passes the error on, so the normal Express error response is unchanged. Client errors such as 400 or 404
53
+ * are not reported.
54
+ */
55
+ export declare function ACRErrorHandler(options: ExpressAcrOptions): (err: unknown, req: Request, _res: Response, next: NextFunction) => void;
56
+ /** Headers to add to outgoing calls so downstream services join the same trace. */
57
+ export declare function acrTraceHeaders(req: Request): Record<string, string>;
58
+ export interface MaintenanceMiddlewareOptions extends Pick<ExpressAcrOptions, 'client' | 'options' | 'getUserId'> {
59
+ /** Return true to let a request through during maintenance (for example admin or webhook routes). */
60
+ exempt?: (req: Request) => boolean;
61
+ }
62
+ /**
63
+ * Opt-in maintenance mode for APIs. While the control snapshot has maintenance enabled, requests get `503` with
64
+ * `{ error: 'MAINTENANCE', message, endsAt }` and `Retry-After` (seconds until `endsAt`, else 300). Health and
65
+ * readiness probes (/health, /healthz, /ready, /readyz) always pass, as do users in `allowUserIds` (via `getUserId`)
66
+ * and requests for which `exempt` returns true. Register it before your routes.
67
+ */
68
+ export declare function maintenanceMiddleware(options: MaintenanceMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,4 @@
1
+ export declare const SDK_INFO: {
2
+ readonly name: "@lib-acr/express";
3
+ readonly version: "0.0.1";
4
+ };
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@lib-acr/express",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "ACR middleware for Express: request events with trace correlation, error capture and W3C trace context.",
5
+ "license": "MIT",
6
+ "main": "./dist/cjs/index.js",
7
+ "module": "./dist/esm/index.js",
8
+ "types": "./dist/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "import": "./dist/esm/index.js",
13
+ "require": "./dist/cjs/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "sideEffects": false,
22
+ "engines": {
23
+ "node": ">=18.17"
24
+ },
25
+ "dependencies": {
26
+ "@lib-acr/core": "0.0.1",
27
+ "@lib-acr/node": "0.0.1"
28
+ },
29
+ "peerDependencies": {
30
+ "express": ">=4.18.0 <6"
31
+ },
32
+ "devDependencies": {
33
+ "express": "^5.1.0",
34
+ "@types/express": "^5.0.6"
35
+ },
36
+ "scripts": {
37
+ "build": "node ../../scripts/build-package.mjs",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "test": "vitest run"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/application-control-room/application.git",
44
+ "directory": "packages/express"
45
+ },
46
+ "homepage": "https://github.com/application-control-room/application/tree/main/packages/express#readme",
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }