@zintrust/redis-rpc 2.4.2 → 2.4.4

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/README.md CHANGED
@@ -56,6 +56,43 @@ The server and client read these variables:
56
56
  | `REDIS_RPC_RETRY_MAX` | `2` | Client-side retry count used by integrations. |
57
57
  | `REDIS_RPC_RETRY_DELAY_MS` | `500` | Client-side retry delay used by integrations. |
58
58
 
59
+ ### Custom request headers
60
+
61
+ The client supports injecting extra HTTP headers into every outgoing RPC request. This uses the same environment-variable convention as all other ZinTrust proxy adapters (`REDIS_PROXY_HEADERS_*` for the redis-proxy, `MYSQL_PROXY_HEADERS_*` for MySQL, and so on).
62
+
63
+ **Pattern:** `REDIS_RPC_PROXY_HEADERS_{HEADER_NAME}=value`
64
+
65
+ Underscores in `HEADER_NAME` are converted to hyphens to form the actual header name.
66
+
67
+ | Environment variable | HTTP header sent |
68
+ | --- | --- |
69
+ | `REDIS_RPC_PROXY_HEADERS_X_Tenant_Id=abc` | `x-tenant-id: abc` |
70
+ | `REDIS_RPC_PROXY_HEADERS_Authorization=Bearer t` | `authorization: Bearer t` |
71
+ | `REDIS_RPC_PROXY_HEADERS_X_Trace_Id=xyz` | `x-trace-id: xyz` |
72
+ | `REDIS_RPC_PROXY_HEADERS_X_Custom_Header=foo` | `x-custom-header: foo` |
73
+
74
+ These headers are read once when `createRedisRpcClient()` is called and are sent on every request. They are merged after `x-redis-rpc-secret`, so they cannot accidentally overwrite authentication.
75
+
76
+ You can also pass headers directly in code. Programmatic headers take priority over env-sourced ones when a key collides:
77
+
78
+ ```ts
79
+ const client = createRedisRpcClient({
80
+ baseUrl: process.env.REDIS_RPC_URL,
81
+ secret: process.env.REDIS_RPC_SECRET,
82
+ headers: {
83
+ 'x-tenant-id': 'abc',
84
+ 'x-trace-id': '123',
85
+ },
86
+ });
87
+ ```
88
+
89
+ Both sources are combined: `{ ...rpcClientHeaders(), ...options.headers }`. To inspect what headers were auto-detected from env, call the exported helper directly:
90
+
91
+ ```ts
92
+ import { rpcClientHeaders } from '@zintrust/redis-rpc';
93
+ console.log(rpcClientHeaders()); // { 'x-tenant-id': 'abc', ... } or undefined
94
+ ```
95
+
59
96
  Set both `USE_REDIS_PROXY=true` and `REDIS_RPC_URL` to make supported ZinTrust packages select Redis RPC automatically. `USE_REDIS_PROXY=true` by itself does not enable Redis RPC; it only says the process is allowed to use a Redis proxy transport.
60
97
 
61
98
  ## Running the server
package/dist/client.js CHANGED
@@ -2,7 +2,7 @@ import { ErrorFactory, isUndefinedOrNull } from '@zintrust/core/runtime';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import http from 'node:http';
4
4
  import https from 'node:https';
5
- import { rpcServerOptions } from './env.js';
5
+ import { rpcClientHeaders, rpcServerOptions } from './env.js';
6
6
  const requestJson = (url, body, headers) => {
7
7
  return new Promise((resolve, reject) => {
8
8
  const transport = url.protocol === 'https:' ? https : http;
@@ -48,6 +48,8 @@ export const createRedisRpcClient = (options = {}) => {
48
48
  const settings = rpcServerOptions();
49
49
  const baseUrl = options.baseUrl || `http://${settings.host}:${settings.port}`;
50
50
  const secret = options.secret ?? settings.secret;
51
+ // Env-sourced headers are the baseline; options.headers merges on top (wins on collision).
52
+ const resolvedHeaders = { ...rpcClientHeaders(), ...options.headers };
51
53
  const client = Object.freeze({
52
54
  call: async (service, method, payload = {}) => {
53
55
  const url = new URL('/rpc', baseUrl);
@@ -61,6 +63,7 @@ export const createRedisRpcClient = (options = {}) => {
61
63
  'content-type': 'application/json',
62
64
  connection: 'close',
63
65
  ...(secret ? { 'x-redis-rpc-secret': secret } : {}),
66
+ ...resolvedHeaders,
64
67
  });
65
68
  const parsed = response.body;
66
69
  if (!response.ok || parsed.ok !== true) {
package/dist/env.d.ts CHANGED
@@ -15,3 +15,18 @@ export declare const readString: (key: string, fallback?: string) => string;
15
15
  export declare const readInt: (key: string, fallback: number) => number;
16
16
  export declare const redisConnectionOptions: () => RedisRpcRedisOptions;
17
17
  export declare const rpcServerOptions: () => RedisRpcServerOptions;
18
+ /**
19
+ * Reads custom HTTP headers from environment variables using the same convention
20
+ * as all other ZinTrust proxies (SqlProxyAdapterUtils.parseCustomHeadersFromEnv).
21
+ *
22
+ * Pattern: REDIS_RPC_PROXY_HEADERS_{HEADER_NAME}=value
23
+ * Underscores in HEADER_NAME are converted to hyphens.
24
+ *
25
+ * Examples:
26
+ * REDIS_RPC_PROXY_HEADERS_X_Tenant_Id=abc → x-tenant-id: abc
27
+ * REDIS_RPC_PROXY_HEADERS_Authorization=Bearer t → authorization: Bearer t
28
+ * REDIS_RPC_PROXY_HEADERS_X_Trace_Id=xyz → x-trace-id: xyz
29
+ *
30
+ * Returns undefined when no matching env vars are set (no overhead).
31
+ */
32
+ export declare const rpcClientHeaders: () => Record<string, string> | undefined;
package/dist/env.js CHANGED
@@ -1,9 +1,12 @@
1
- import { isNull } from '@zintrust/core/helper';
2
1
  import { config as loadDotenv } from 'dotenv';
3
2
  loadDotenv({ path: process.env.REDIS_RPC_ENV_FILE || '.env' });
4
3
  export const readString = (key, fallback = '') => {
5
4
  const value = process.env[key];
6
- return typeof value === 'string' && !isNull(value.trim()) ? value.trim() : fallback;
5
+ if (typeof value !== 'string') {
6
+ return fallback;
7
+ }
8
+ const trimmed = value.trim();
9
+ return trimmed === '' ? fallback : trimmed;
7
10
  };
8
11
  export const readInt = (key, fallback) => {
9
12
  const parsed = Number.parseInt(readString(key, String(fallback)), 10);
@@ -22,3 +25,27 @@ export const rpcServerOptions = () => ({
22
25
  secret: readString('REDIS_RPC_SECRET', readString('REDIS_PROXY_SECRET', readString('APP_KEY', ''))),
23
26
  prefix: readString('REDIS_RPC_BULLMQ_PREFIX', readString('BULLMQ_PREFIX', 'bull')),
24
27
  });
28
+ /**
29
+ * Reads custom HTTP headers from environment variables using the same convention
30
+ * as all other ZinTrust proxies (SqlProxyAdapterUtils.parseCustomHeadersFromEnv).
31
+ *
32
+ * Pattern: REDIS_RPC_PROXY_HEADERS_{HEADER_NAME}=value
33
+ * Underscores in HEADER_NAME are converted to hyphens.
34
+ *
35
+ * Examples:
36
+ * REDIS_RPC_PROXY_HEADERS_X_Tenant_Id=abc → x-tenant-id: abc
37
+ * REDIS_RPC_PROXY_HEADERS_Authorization=Bearer t → authorization: Bearer t
38
+ * REDIS_RPC_PROXY_HEADERS_X_Trace_Id=xyz → x-trace-id: xyz
39
+ *
40
+ * Returns undefined when no matching env vars are set (no overhead).
41
+ */
42
+ export const rpcClientHeaders = () => {
43
+ const PREFIX = 'REDIS_RPC_PROXY_HEADERS_';
44
+ const headers = {};
45
+ for (const [key, value] of Object.entries(process.env)) {
46
+ if (key.startsWith(PREFIX) && typeof value === 'string' && value.trim() !== '') {
47
+ headers[key.slice(PREFIX.length).replaceAll('_', '-')] = value.trim();
48
+ }
49
+ }
50
+ return Object.keys(headers).length > 0 ? headers : undefined;
51
+ };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createRedisRpcServer, listenRedisRpcServer } from './server';
2
2
  export { createRedisRpcClient } from './client';
3
+ export { rpcClientHeaders, rpcServerOptions, redisConnectionOptions } from './env';
3
4
  export { createBullMqRpcQueue, createWorkerRpcRuntime, createQueueMonitorRpcDriver, createRedisRpcService, } from './adapters';
4
5
  export { createRedisRpcBackend } from './backend';
5
6
  export type { RpcPayload, RpcRequest, RpcErrorBody, RpcSuccessBody, RedisRpcBackendState, RedisRpcServiceHandler, RedisRpcBackend, CreateRedisRpcBackendOptions, RedisRpcClientOptions, RedisRpcClient, RedisRpcServerInstance, } from './types';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createRedisRpcServer, listenRedisRpcServer } from './server.js';
2
2
  export { createRedisRpcClient } from './client.js';
3
+ export { rpcClientHeaders, rpcServerOptions, redisConnectionOptions } from './env.js';
3
4
  export { createBullMqRpcQueue, createWorkerRpcRuntime, createQueueMonitorRpcDriver, createRedisRpcService, } from './adapters.js';
4
5
  export { createRedisRpcBackend } from './backend.js';
5
6
  /**
package/dist/server.js CHANGED
@@ -18,33 +18,169 @@ const json = (response, status, payload) => {
18
18
  });
19
19
  response.end(JSON.stringify(payload));
20
20
  };
21
+ const getHeaderSecret = (request) => {
22
+ const raw = request.headers['x-redis-rpc-secret'];
23
+ if (Array.isArray(raw)) {
24
+ return raw[0] ?? '';
25
+ }
26
+ return typeof raw === 'string' ? raw : '';
27
+ };
28
+ const previewSecret = (value) => value.slice(0, 5);
29
+ const logStep = (step, details) => {
30
+ Logger.debug(`[redis-rpc][server] ${step}`, details);
31
+ };
32
+ const getRequestContext = (request, settings) => {
33
+ const method = request.method ?? '';
34
+ const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
35
+ const headerSecret = getHeaderSecret(request);
36
+ const settingsSecret = settings.secret || '';
37
+ return {
38
+ method,
39
+ url,
40
+ headerSecret,
41
+ settingsSecret,
42
+ };
43
+ };
44
+ const logRequestReceived = (context) => {
45
+ logStep('request.received', {
46
+ method: context.method,
47
+ path: context.url.pathname,
48
+ host: context.url.host,
49
+ headerSecretPreview: previewSecret(context.headerSecret),
50
+ settingsSecretPreview: previewSecret(context.settingsSecret),
51
+ });
52
+ };
53
+ const isHealthRoute = (context) => context.method === 'GET' && context.url.pathname === '/health';
54
+ const isRpcRoute = (context) => context.method === 'POST' && context.url.pathname === '/rpc';
55
+ const logHealthRoute = (prefix, path) => {
56
+ logStep('route.health', {
57
+ path,
58
+ prefix,
59
+ });
60
+ };
61
+ const logRouteCheck = (context) => {
62
+ logStep('route.check', {
63
+ method: context.method,
64
+ path: context.url.pathname,
65
+ isRpcRoute: isRpcRoute(context),
66
+ });
67
+ };
68
+ const logRouteNotFound = (context) => {
69
+ logStep('route.notFound', {
70
+ method: context.method,
71
+ path: context.url.pathname,
72
+ });
73
+ };
74
+ const validateRequestSecret = (context) => {
75
+ logStep('secret.validate.start', {
76
+ headerSecretPreview: previewSecret(context.headerSecret),
77
+ settingsSecretPreview: previewSecret(context.settingsSecret),
78
+ });
79
+ const secretMatches = context.settingsSecret !== '' && context.headerSecret === context.settingsSecret;
80
+ logStep('secret.validate.result', {
81
+ headerSecretPreview: previewSecret(context.headerSecret),
82
+ settingsSecretPreview: previewSecret(context.settingsSecret),
83
+ secretMatches,
84
+ });
85
+ return secretMatches;
86
+ };
87
+ const logSecretValidationFailure = (context) => {
88
+ logStep('secret.validate.failed', {
89
+ headerSecretPreview: previewSecret(context.headerSecret),
90
+ settingsSecretPreview: previewSecret(context.settingsSecret),
91
+ });
92
+ };
93
+ const readRpcBody = async (request) => {
94
+ logStep('body.read.start', {});
95
+ const bodyText = await readBody(request);
96
+ logStep('body.read.complete', {
97
+ length: bodyText.length,
98
+ isEmpty: bodyText.trim().length === 0,
99
+ });
100
+ try {
101
+ const body = (bodyText.trim().length === 0 ? {} : JSON.parse(bodyText));
102
+ logStep('body.parse.complete', {
103
+ requestId: body.requestId ?? null,
104
+ service: String(body.service || ''),
105
+ method: String(body.method || ''),
106
+ hasPayload: body.payload !== undefined,
107
+ });
108
+ return body;
109
+ }
110
+ catch (error) {
111
+ logStep('body.parse.failed', {
112
+ error: error instanceof Error ? error.message : String(error),
113
+ });
114
+ throw error;
115
+ }
116
+ };
117
+ const dispatchRpcRequest = async (backend, body) => {
118
+ logStep('dispatch.start', {
119
+ requestId: body.requestId ?? null,
120
+ service: String(body.service || ''),
121
+ method: String(body.method || ''),
122
+ });
123
+ const result = await backend.dispatch(String(body.service || ''), String(body.method || ''), body.payload ?? {});
124
+ logStep('dispatch.complete', {
125
+ requestId: body.requestId ?? null,
126
+ service: String(body.service || ''),
127
+ method: String(body.method || ''),
128
+ });
129
+ return result;
130
+ };
131
+ const sendRpcSuccess = (response, requestId, result) => {
132
+ json(response, 200, { ok: true, requestId, result, error: null });
133
+ logStep('response.sent', {
134
+ status: 200,
135
+ requestId,
136
+ });
137
+ };
138
+ const handleRequestError = (response, error) => {
139
+ const payload = toErrorPayload(error);
140
+ logStep('request.error', {
141
+ status: payload.status,
142
+ message: payload.body && typeof payload.body === 'object' && 'message' in payload.body
143
+ ? String(payload.body.message ?? '')
144
+ : '',
145
+ });
146
+ json(response, payload.status, payload.body);
147
+ };
148
+ const handleRpcRequest = async (request, response, context, backend) => {
149
+ logRouteCheck(context);
150
+ if (!isRpcRoute(context)) {
151
+ logRouteNotFound(context);
152
+ throw createRpcNotFoundError('Unknown Redis RPC route');
153
+ }
154
+ if (!validateRequestSecret(context)) {
155
+ logSecretValidationFailure(context);
156
+ throw createRpcUnauthorizedError('Invalid Redis RPC secret');
157
+ }
158
+ const body = await readRpcBody(request);
159
+ const result = await dispatchRpcRequest(backend, body);
160
+ sendRpcSuccess(response, body.requestId ?? null, result);
161
+ };
162
+ const handleHealthRequest = (response, backend) => {
163
+ logHealthRoute(backend.prefix, '/health');
164
+ json(response, 200, { ok: true, service: 'redis-rpc', prefix: backend.prefix });
165
+ };
166
+ const handleIncomingRequest = async (request, response, settings, backend) => {
167
+ const context = getRequestContext(request, settings);
168
+ logRequestReceived(context);
169
+ if (isHealthRoute(context)) {
170
+ handleHealthRequest(response, backend);
171
+ return;
172
+ }
173
+ await handleRpcRequest(request, response, context, backend);
174
+ };
21
175
  export const createRedisRpcServer = (options = {}) => {
22
176
  const settings = { ...rpcServerOptions(), ...options };
23
177
  const backend = isObject(options.backend)
24
178
  ? options.backend
25
179
  : createRedisRpcBackend(settings);
26
- const server = http.createServer(async (request, response) => {
27
- try {
28
- const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
29
- if (request.method === 'GET' && url.pathname === '/health') {
30
- json(response, 200, { ok: true, service: 'redis-rpc', prefix: backend.prefix });
31
- return;
32
- }
33
- if (request.method !== 'POST' || url.pathname !== '/rpc') {
34
- throw createRpcNotFoundError('Unknown Redis RPC route');
35
- }
36
- if (!settings.secret || request.headers['x-redis-rpc-secret'] !== settings.secret) {
37
- throw createRpcUnauthorizedError('Invalid Redis RPC secret');
38
- }
39
- const bodyText = await readBody(request);
40
- const body = (bodyText.trim().length === 0 ? {} : JSON.parse(bodyText));
41
- const result = await backend.dispatch(String(body.service || ''), String(body.method || ''), body.payload ?? {});
42
- json(response, 200, { ok: true, requestId: body.requestId ?? null, result, error: null });
43
- }
44
- catch (error) {
45
- const payload = toErrorPayload(error);
46
- json(response, payload.status, payload.body);
47
- }
180
+ const server = http.createServer((request, response) => {
181
+ void handleIncomingRequest(request, response, settings, backend).catch((error) => {
182
+ handleRequestError(response, error);
183
+ });
48
184
  });
49
185
  return { server, backend, settings };
50
186
  };
package/dist/types.d.ts CHANGED
@@ -49,6 +49,8 @@ export type CreateRedisRpcBackendOptions = Partial<RedisRpcServerOptions> & Read
49
49
  export type RedisRpcClientOptions = Partial<RedisRpcServerOptions> & Readonly<{
50
50
  baseUrl?: string;
51
51
  secret?: string;
52
+ /** Extra HTTP headers merged into every request (same pattern as redis-proxy customHeaders). */
53
+ headers?: Record<string, string>;
52
54
  }>;
53
55
  export type RedisRpcClient = Readonly<{
54
56
  call: <T = unknown>(service: string, method: string, payload?: RpcPayload) => Promise<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/redis-rpc",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "Redis RPC backend for BullMQ queue operations in ZinTrust.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,98 +0,0 @@
1
- {
2
- "name": "@zintrust/redis-rpc",
3
- "version": "2.4.1",
4
- "buildDate": "2026-05-31T11:45:34.243Z",
5
- "buildEnvironment": {
6
- "node": "v22.22.1",
7
- "platform": "darwin",
8
- "arch": "arm64"
9
- },
10
- "git": {
11
- "commit": "e97b7b3d",
12
- "branch": "release"
13
- },
14
- "package": {
15
- "engines": {
16
- "node": ">=20.0.0"
17
- },
18
- "dependencies": [
19
- "@zintrust/core",
20
- "bullmq",
21
- "dotenv",
22
- "ioredis"
23
- ],
24
- "peerDependencies": [
25
- "@zintrust/core"
26
- ]
27
- },
28
- "files": {
29
- "adapters.d.ts": {
30
- "size": 2331,
31
- "sha256": "4080a5f54655beac7aa22f271aae61e03611230abf75491cd269d83278ea07c7"
32
- },
33
- "adapters.js": {
34
- "size": 2817,
35
- "sha256": "f7a732072fa8b1c0a288cdea9e6304f0e884179ed924eb1530a2dbd57e5bb087"
36
- },
37
- "backend.d.ts": {
38
- "size": 183,
39
- "sha256": "c2f1dd71fe2c5a1dc73a7fba855b5e8f8b1700c9f05baeab2c5d9aa8f31f4c60"
40
- },
41
- "backend.js": {
42
- "size": 21160,
43
- "sha256": "85777d42545d97624a0973fccc33b91a0f8e6a6345c940a8581845a4670cce2e"
44
- },
45
- "build-manifest.json": {
46
- "size": 2714,
47
- "sha256": "7b981cbb55c59a29db94c6f110f360299e33e36ed26d54a5f4aef9af54e3a324"
48
- },
49
- "client.d.ts": {
50
- "size": 166,
51
- "sha256": "f58e91ca403a526864cca16eba5caa34a947835d403a20fe9e02e7cdd7ec55ca"
52
- },
53
- "client.js": {
54
- "size": 3528,
55
- "sha256": "79322c1c1265d34a02260f0d51b99e0e549e02978a40208faaf4e6fdf56e92e3"
56
- },
57
- "env.d.ts": {
58
- "size": 575,
59
- "sha256": "640907189918c080ac4bdce3f9a09771ab417ab266d9d5096071ce03e95f36a0"
60
- },
61
- "env.js": {
62
- "size": 1282,
63
- "sha256": "4612b16a360e9a0644718f533c94dd80f2f4e1134c396348cf86814f54387ba7"
64
- },
65
- "errors.d.ts": {
66
- "size": 484,
67
- "sha256": "178c171224b78c62b2347f58922906fa6903cdff27731db14b4f9c88af4e86d1"
68
- },
69
- "errors.js": {
70
- "size": 1202,
71
- "sha256": "eea48bcb9a1fe0f4f065835b9ce517087a5216bd9f78816378c17fac432d7dce"
72
- },
73
- "index.d.ts": {
74
- "size": 775,
75
- "sha256": "69ade1c65e33274c5a4aef0ddddd02a2ad4b8436da40fdb918e352f17a038230"
76
- },
77
- "index.js": {
78
- "size": 745,
79
- "sha256": "554f195a0480b8fddb7778d656aae9d528398b15b1f95748d5ccf77c58884dc4"
80
- },
81
- "server.d.ts": {
82
- "size": 276,
83
- "sha256": "3f9453082da4e8bdef61c904a4e6a8f2c5c24ef6ae8c785ec6671ee57739256c"
84
- },
85
- "server.js": {
86
- "size": 3027,
87
- "sha256": "843b4cb91e438003d474d1f145d30cdc0627f8e099fb90bfe02597eb469be192"
88
- },
89
- "types.d.ts": {
90
- "size": 2348,
91
- "sha256": "90ab4f7122923b219e51cd333688e90a1152def08b6e86891dd066c36f185157"
92
- },
93
- "types.js": {
94
- "size": 11,
95
- "sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"
96
- }
97
- }
98
- }