@zintrust/core 0.4.34 → 0.4.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +4 -4
  2. package/src/cache/drivers/RedisDriver.d.ts.map +1 -1
  3. package/src/cache/drivers/RedisDriver.js +1 -1
  4. package/src/cli/OptionalCliExtensions.d.ts.map +1 -1
  5. package/src/cli/OptionalCliExtensions.js +1 -0
  6. package/src/cli/commands/WorkerCommands.d.ts +2 -1
  7. package/src/cli/commands/WorkerCommands.d.ts.map +1 -1
  8. package/src/cli/commands/WorkerCommands.js +111 -62
  9. package/src/cli/services/WorkerStartupDiagnostics.d.ts +41 -0
  10. package/src/cli/services/WorkerStartupDiagnostics.d.ts.map +1 -0
  11. package/src/cli/services/WorkerStartupDiagnostics.js +348 -0
  12. package/src/cli/utils/DistPackager.d.ts.map +1 -1
  13. package/src/cli/utils/DistPackager.js +22 -18
  14. package/src/config/workers.d.ts +2 -1
  15. package/src/config/workers.d.ts.map +1 -1
  16. package/src/config/workers.js +6 -1
  17. package/src/index.d.ts +11 -11
  18. package/src/index.d.ts.map +1 -1
  19. package/src/index.js +13 -13
  20. package/src/proxy/CloudflareProxyShared.d.ts +50 -0
  21. package/src/proxy/CloudflareProxyShared.d.ts.map +1 -0
  22. package/src/proxy/CloudflareProxyShared.js +117 -0
  23. package/src/proxy/d1/ZintrustD1Proxy.d.ts.map +1 -1
  24. package/src/proxy/d1/ZintrustD1Proxy.js +49 -158
  25. package/src/proxy/kv/ZintrustKvProxy.d.ts.map +1 -1
  26. package/src/proxy/kv/ZintrustKvProxy.js +41 -142
  27. package/src/security/CsrfTokenManager.d.ts.map +1 -1
  28. package/src/security/CsrfTokenManager.js +1 -1
  29. package/src/security/JwtSessions.d.ts.map +1 -1
  30. package/src/security/JwtSessions.js +1 -1
  31. package/src/security/TokenRevocation.d.ts.map +1 -1
  32. package/src/security/TokenRevocation.js +1 -1
  33. package/src/tools/queue/LockProvider.d.ts.map +1 -1
  34. package/src/tools/queue/LockProvider.js +1 -1
  35. package/src/tools/redis/RedisTransport.d.ts +34 -0
  36. package/src/tools/redis/RedisTransport.d.ts.map +1 -0
  37. package/src/tools/redis/RedisTransport.js +251 -0
@@ -1,104 +1,9 @@
1
1
  import { isObject, isString } from '../../helper/index.js';
2
- import { ErrorHandler } from '../ErrorHandler.js';
2
+ import { getEnvInt, json, normalizeBindingName, readAndVerifyJson, toErrorResponse, } from '../CloudflareProxyShared.js';
3
3
  import { RequestValidator } from '../RequestValidator.js';
4
- import { SigningService } from '../SigningService.js';
5
4
  const DEFAULT_SIGNING_WINDOW_MS = 60_000;
6
5
  const DEFAULT_MAX_BODY_BYTES = 128 * 1024;
7
6
  const DEFAULT_LIST_LIMIT = 100;
8
- const json = (status, body) => {
9
- return new Response(JSON.stringify(body), {
10
- status,
11
- headers: {
12
- 'Content-Type': 'application/json; charset=utf-8',
13
- 'Cache-Control': 'no-store',
14
- },
15
- });
16
- };
17
- const toErrorResponse = (status, code, message) => {
18
- const error = ErrorHandler.toProxyError(status, code, message);
19
- return json(error.status, error.body);
20
- };
21
- const getEnvInt = (env, name, fallback) => {
22
- const raw = env[name];
23
- if (!isString(raw))
24
- return fallback;
25
- const parsed = Number.parseInt(raw, 10);
26
- return Number.isFinite(parsed) ? parsed : fallback;
27
- };
28
- const normalizeBindingName = (value) => {
29
- if (!isString(value))
30
- return null;
31
- const trimmed = value.trim();
32
- return trimmed === '' ? null : trimmed;
33
- };
34
- const readBodyBytes = async (request, maxBytes) => {
35
- const buf = await request.arrayBuffer();
36
- if (buf.byteLength > maxBytes) {
37
- return {
38
- ok: false,
39
- response: toErrorResponse(413, 'PAYLOAD_TOO_LARGE', 'Body too large'),
40
- };
41
- }
42
- const bytes = new Uint8Array(buf);
43
- const text = new TextDecoder().decode(bytes);
44
- return { ok: true, bytes, text };
45
- };
46
- const parseOptionalJson = (text) => {
47
- if (text.trim() === '')
48
- return { ok: true, payload: null };
49
- const parsed = RequestValidator.parseJson(text);
50
- if (!parsed.ok) {
51
- let message = parsed.error.message;
52
- if (parsed.error.code === 'INVALID_JSON') {
53
- message = 'Invalid JSON body';
54
- }
55
- else if (parsed.error.code === 'VALIDATION_ERROR') {
56
- message = 'Invalid body';
57
- }
58
- return { ok: false, response: toErrorResponse(400, parsed.error.code, message) };
59
- }
60
- return { ok: true, payload: parsed.value };
61
- };
62
- const loadSigningSecret = (env) => {
63
- const direct = isString(env.KV_REMOTE_SECRET) ? env.KV_REMOTE_SECRET.trim() : '';
64
- if (direct !== '')
65
- return direct;
66
- const fallback = isString(env.APP_KEY) ? env.APP_KEY.trim() : '';
67
- if (fallback !== '')
68
- return fallback;
69
- return null;
70
- };
71
- const verifyNonceKv = async (kv, keyId, nonce, ttlMs) => {
72
- const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1000));
73
- const storageKey = `nonce:${keyId}:${nonce}`;
74
- const existing = await kv.get(storageKey);
75
- if (existing !== null)
76
- return false;
77
- await kv.put(storageKey, '1', { expirationTtl: ttlSeconds });
78
- return true;
79
- };
80
- const verifySignedRequest = async (request, env, bodyBytes) => {
81
- const secret = loadSigningSecret(env);
82
- if (secret === null) {
83
- return toErrorResponse(500, 'CONFIG_ERROR', 'Missing signing secret (KV_REMOTE_SECRET or APP_KEY)');
84
- }
85
- const windowMs = getEnvInt(env, 'ZT_PROXY_SIGNING_WINDOW_MS', DEFAULT_SIGNING_WINDOW_MS);
86
- const verifyResult = await SigningService.verifyWithKeyProvider({
87
- method: request.method,
88
- url: request.url,
89
- body: bodyBytes,
90
- headers: request.headers,
91
- windowMs,
92
- getSecretForKeyId: (_keyId) => secret,
93
- verifyNonce: env.ZT_NONCES === undefined
94
- ? undefined
95
- : async (keyId, nonce, ttlMs) => verifyNonceKv(env.ZT_NONCES, keyId, nonce, ttlMs),
96
- });
97
- if (!verifyResult.ok) {
98
- return toErrorResponse(verifyResult.status, verifyResult.code, verifyResult.message);
99
- }
100
- return { ok: true };
101
- };
102
7
  const requireCache = (env) => {
103
8
  if (env.CACHE !== undefined && env.CACHE !== null)
104
9
  return env.CACHE;
@@ -128,18 +33,20 @@ const buildStorageKey = (env, params) => {
128
33
  parts.push(params.key);
129
34
  return parts.join(':');
130
35
  };
131
- const readAndVerifyJson = async (request, env) => {
132
- const maxBodyBytes = getEnvInt(env, 'ZT_MAX_BODY_BYTES', DEFAULT_MAX_BODY_BYTES);
133
- const bodyResult = await readBodyBytes(request, maxBodyBytes);
134
- if (!bodyResult.ok)
135
- return { ok: false, response: bodyResult.response };
136
- const auth = await verifySignedRequest(request, env, bodyResult.bytes);
137
- if (auth instanceof Response)
138
- return { ok: false, response: auth };
139
- const parsed = parseOptionalJson(bodyResult.text);
140
- if (!parsed.ok)
141
- return { ok: false, response: parsed.response };
142
- return { ok: true, payload: parsed.payload, bodyBytes: bodyResult.bytes };
36
+ const resolveCacheRequest = async (request, env) => {
37
+ const check = await readAndVerifyJson(request, env, {
38
+ secretEnvVar: 'KV_REMOTE_SECRET',
39
+ missingSecretStatus: 500,
40
+ missingSecretMessage: 'Missing signing secret (KV_REMOTE_SECRET or APP_KEY)',
41
+ defaultSigningWindowMs: DEFAULT_SIGNING_WINDOW_MS,
42
+ defaultMaxBodyBytes: DEFAULT_MAX_BODY_BYTES,
43
+ });
44
+ if (!check.ok)
45
+ return { ok: false, response: check.response };
46
+ const cache = requireCache(env);
47
+ if (cache instanceof Response)
48
+ return { ok: false, response: cache };
49
+ return { ok: true, cache, payload: check.payload };
143
50
  };
144
51
  const parseGetPayload = (payload) => {
145
52
  if (!isObject(payload)) {
@@ -154,25 +61,22 @@ const parseGetPayload = (payload) => {
154
61
  return { ok: true, namespace: normalizeNamespace(payload['namespace']), key, type: typeValue };
155
62
  };
156
63
  const handleGet = async (request, env) => {
157
- const check = await readAndVerifyJson(request, env);
158
- if (!check.ok)
159
- return check.response;
160
- const cache = requireCache(env);
161
- if (cache instanceof Response)
162
- return cache;
163
- const parsed = parseGetPayload(check.payload);
64
+ const resolved = await resolveCacheRequest(request, env);
65
+ if (!resolved.ok)
66
+ return resolved.response;
67
+ const parsed = parseGetPayload(resolved.payload);
164
68
  if (!parsed.ok)
165
69
  return parsed.response;
166
70
  const storageKey = buildStorageKey(env, { namespace: parsed.namespace, key: parsed.key });
167
71
  if (parsed.type === 'json') {
168
- const value = await cache.get(storageKey, 'json');
72
+ const value = await resolved.cache.get(storageKey, 'json');
169
73
  return json(200, { value: value ?? null });
170
74
  }
171
75
  if (parsed.type === 'arrayBuffer') {
172
- const value = await cache.get(storageKey, 'arrayBuffer');
76
+ const value = await resolved.cache.get(storageKey, 'arrayBuffer');
173
77
  return json(200, { value: value ?? null });
174
78
  }
175
- const value = await cache.get(storageKey);
79
+ const value = await resolved.cache.get(storageKey);
176
80
  return json(200, { value: value ?? null });
177
81
  };
178
82
  const parsePutPayload = (payload) => {
@@ -196,13 +100,10 @@ const parsePutPayload = (payload) => {
196
100
  };
197
101
  };
198
102
  const handlePut = async (request, env) => {
199
- const check = await readAndVerifyJson(request, env);
200
- if (!check.ok)
201
- return check.response;
202
- const cache = requireCache(env);
203
- if (cache instanceof Response)
204
- return cache;
205
- const parsed = parsePutPayload(check.payload);
103
+ const resolved = await resolveCacheRequest(request, env);
104
+ if (!resolved.ok)
105
+ return resolved.response;
106
+ const parsed = parsePutPayload(resolved.payload);
206
107
  if (!parsed.ok)
207
108
  return parsed.response;
208
109
  const storageKey = buildStorageKey(env, { namespace: parsed.namespace, key: parsed.key });
@@ -211,7 +112,7 @@ const handlePut = async (request, env) => {
211
112
  if (parsed.ttlSeconds !== undefined) {
212
113
  options.expirationTtl = Math.floor(parsed.ttlSeconds);
213
114
  }
214
- await cache.put(storageKey, value, options);
115
+ await resolved.cache.put(storageKey, value, options);
215
116
  return json(200, { ok: true });
216
117
  };
217
118
  const parseDeletePayload = (payload) => {
@@ -225,17 +126,14 @@ const parseDeletePayload = (payload) => {
225
126
  return { ok: true, namespace: normalizeNamespace(payload['namespace']), key };
226
127
  };
227
128
  const handleDelete = async (request, env) => {
228
- const check = await readAndVerifyJson(request, env);
229
- if (!check.ok)
230
- return check.response;
231
- const cache = requireCache(env);
232
- if (cache instanceof Response)
233
- return cache;
234
- const parsed = parseDeletePayload(check.payload);
129
+ const resolved = await resolveCacheRequest(request, env);
130
+ if (!resolved.ok)
131
+ return resolved.response;
132
+ const parsed = parseDeletePayload(resolved.payload);
235
133
  if (!parsed.ok)
236
134
  return parsed.response;
237
135
  const storageKey = buildStorageKey(env, { namespace: parsed.namespace, key: parsed.key });
238
- await cache.delete(storageKey);
136
+ await resolved.cache.delete(storageKey);
239
137
  return json(200, { ok: true });
240
138
  };
241
139
  const parseListPayload = (payload) => {
@@ -252,13 +150,10 @@ const parseListPayload = (payload) => {
252
150
  return { ok: true, params: { namespace, prefix, cursor, limit: limitParsed } };
253
151
  };
254
152
  const handleList = async (request, env) => {
255
- const check = await readAndVerifyJson(request, env);
256
- if (!check.ok)
257
- return check.response;
258
- const cache = requireCache(env);
259
- if (cache instanceof Response)
260
- return cache;
261
- const parsed = parseListPayload(check.payload);
153
+ const resolved = await resolveCacheRequest(request, env);
154
+ if (!resolved.ok)
155
+ return resolved.response;
156
+ const parsed = parseListPayload(resolved.payload);
262
157
  if (!parsed.ok)
263
158
  return parsed.response;
264
159
  const envLimit = getEnvInt(env, 'ZT_KV_LIST_LIMIT', DEFAULT_LIST_LIMIT);
@@ -268,7 +163,11 @@ const handleList = async (request, env) => {
268
163
  const namespacePrefix = normalizeNamespace(parsed.params.namespace);
269
164
  const basePrefix = buildStorageKey(env, { namespace: namespacePrefix, key: '' });
270
165
  const fullPrefix = prefixKey === undefined ? basePrefix : `${basePrefix}${prefixKey}`;
271
- const out = await cache.list({ prefix: fullPrefix, limit, cursor: parsed.params.cursor });
166
+ const out = await resolved.cache.list({
167
+ prefix: fullPrefix,
168
+ limit,
169
+ cursor: parsed.params.cursor,
170
+ });
272
171
  return json(200, {
273
172
  keys: out.keys.map((key) => key.name),
274
173
  cursor: out.cursor,
@@ -1 +1 @@
1
- {"version":3,"file":"CsrfTokenManager.d.ts","sourceRoot":"","sources":["../../../src/security/CsrfTokenManager.ts"],"names":[],"mappings":"AACA;;;GAGG;AAQH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC/D,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,iBAAiB,CAAC;CAC9D;AAED,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAyVF;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,oBAE7B,CAAC"}
1
+ {"version":3,"file":"CsrfTokenManager.d.ts","sourceRoot":"","sources":["../../../src/security/CsrfTokenManager.ts"],"names":[],"mappings":"AACA;;;GAGG;AAQH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC/D,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,iBAAiB,CAAC;CAC9D;AAED,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE/C,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AA6VF;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,oBAE7B,CAAC"}
@@ -117,7 +117,7 @@ const createRedisClientFactory = (options) => {
117
117
  port: Env.getInt('REDIS_PORT', ZintrustLang.REDIS_DEFAULT_PORT),
118
118
  password: Env.get('REDIS_PASSWORD'),
119
119
  db: database,
120
- });
120
+ }, 3, { subsystem: 'csrf' });
121
121
  return redisClient;
122
122
  };
123
123
  };
@@ -1 +1 @@
1
- {"version":3,"file":"JwtSessions.d.ts","sourceRoot":"","sources":["../../../src/security/JwtSessions.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,WAAW,CAAC;AAEzF,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;AAqnBhE,eAAO,MAAM,WAAW;oBACA,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;oBAKtB,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;mBAM1B,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;mBAU5C,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;iBAQ9B,qBAAqB;sBAIhB,IAAI;EAItB,CAAC;AAEH,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"JwtSessions.d.ts","sourceRoot":"","sources":["../../../src/security/JwtSessions.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,WAAW,CAAC;AAEzF,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;AAynBhE,eAAO,MAAM,WAAW;oBACA,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;oBAKtB,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;mBAM1B,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;mBAU5C,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;iBAQ9B,qBAAqB;sBAIhB,IAAI;EAItB,CAAC;AAEH,eAAe,WAAW,CAAC"}
@@ -246,7 +246,7 @@ const createRedisStore = (params) => {
246
246
  port: Env.REDIS_PORT,
247
247
  password: Env.REDIS_PASSWORD,
248
248
  db: Env.getInt('JWT_REVOCATION_REDIS_DB', Env.REDIS_DB),
249
- });
249
+ }, 3, { subsystem: 'jwt-sessions' });
250
250
  const indexGet = async (sub) => {
251
251
  const value = await client.get(encodeSubIndexKey(params.keyPrefix, sub));
252
252
  if (value === null)
@@ -1 +1 @@
1
- {"version":3,"file":"TokenRevocation.d.ts","sourceRoot":"","sources":["../../../src/security/TokenRevocation.ts"],"names":[],"mappings":"AAUA,KAAK,mBAAmB,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;AAEzD,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,WAAW,CAAC;AAysB7F,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;mBACkB,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUjE;;OAEG;qBACoB,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMhD;;OAEG;iBACU,yBAAyB;IAItC;;OAEG;sBACe,IAAI;EAItB,CAAC;AAEH,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"TokenRevocation.d.ts","sourceRoot":"","sources":["../../../src/security/TokenRevocation.ts"],"names":[],"mappings":"AAUA,KAAK,mBAAmB,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;AAEzD,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,GAAG,WAAW,CAAC;AA6sB7F,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;mBACkB,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUjE;;OAEG;qBACoB,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMhD;;OAEG;iBACU,yBAAyB;IAItC;;OAEG;sBACe,IAAI;EAItB,CAAC;AAEH,eAAe,eAAe,CAAC"}
@@ -203,7 +203,7 @@ const createRedisStore = (params) => {
203
203
  port: Env.REDIS_PORT,
204
204
  password: Env.REDIS_PASSWORD,
205
205
  db: Env.getInt('JWT_REVOCATION_REDIS_DB', Env.REDIS_DB),
206
- });
206
+ }, 3, { subsystem: 'jwt-revocation' });
207
207
  return {
208
208
  async revoke(key) {
209
209
  const ttlMs = Math.max(0, key.expiresAtMs - Date.now());
@@ -1 +1 @@
1
- {"version":3,"file":"LockProvider.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/LockProvider.ts"],"names":[],"mappings":"AACA;;;GAGG;AAGH,OAAO,KAAK,EAGV,YAAY,EACZ,kBAAkB,EAEnB,MAAM,eAAe,CAAC;AAwBvB;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAUvD;AAkJD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAWhF;AAYD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CA4FjF;AAOD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAG/E;AACD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAS3E"}
1
+ {"version":3,"file":"LockProvider.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/LockProvider.ts"],"names":[],"mappings":"AACA;;;GAGG;AAGH,OAAO,KAAK,EAGV,YAAY,EACZ,kBAAkB,EAEnB,MAAM,eAAe,CAAC;AA4BvB;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAUvD;AAkJD;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAWhF;AAYD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CA4FjF;AAOD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAG/E;AACD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY,CAS3E"}
@@ -19,7 +19,7 @@ function getRedisClient() {
19
19
  port: redisConfig.port,
20
20
  password: redisConfig.password,
21
21
  db: redisConfig.database,
22
- });
22
+ }, 3, { subsystem: 'lock-provider' });
23
23
  }
24
24
  return redisClient;
25
25
  }
@@ -0,0 +1,34 @@
1
+ import type { RedisConfig } from '../../config/type';
2
+ export type RedisTransportMode = 'direct' | 'proxy';
3
+ export type RedisTransportOptions = Readonly<{
4
+ subsystem?: string;
5
+ requireDirect?: boolean;
6
+ }>;
7
+ type RedisProxyConnection = {
8
+ status: 'ready';
9
+ connect: () => Promise<void>;
10
+ quit: () => Promise<'OK'>;
11
+ disconnect: () => void;
12
+ on: (event: string, handler: (...args: unknown[]) => void) => RedisProxyConnection;
13
+ once: (event: string, handler: (...args: unknown[]) => void) => RedisProxyConnection;
14
+ off: (event: string, handler: (...args: unknown[]) => void) => RedisProxyConnection;
15
+ removeListener: (event: string, handler: (...args: unknown[]) => void) => RedisProxyConnection;
16
+ call: (command: string, ...args: unknown[]) => Promise<unknown>;
17
+ pipeline: () => {
18
+ exec: () => Promise<Array<[Error | null, unknown]>>;
19
+ [key: string]: unknown;
20
+ };
21
+ scanStream: (options?: {
22
+ match?: string;
23
+ count?: number;
24
+ }) => {
25
+ on: (event: string, handler: (...args: unknown[]) => void) => unknown;
26
+ once: (event: string, handler: (...args: unknown[]) => void) => unknown;
27
+ off: (event: string, handler: (...args: unknown[]) => void) => unknown;
28
+ };
29
+ };
30
+ export declare const resolveRedisTransportMode: () => RedisTransportMode;
31
+ export declare const createRedisProxyConnection: (config: RedisConfig, options?: RedisTransportOptions) => RedisProxyConnection;
32
+ export declare const ensureRedisTransportMode: (config: RedisConfig, options?: RedisTransportOptions) => RedisTransportMode;
33
+ export {};
34
+ //# sourceMappingURL=RedisTransport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RedisTransport.d.ts","sourceRoot":"","sources":["../../../../src/tools/redis/RedisTransport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpD,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC,CAAC;AASH,KAAK,oBAAoB,GAAG;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,oBAAoB,CAAC;IACnF,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,oBAAoB,CAAC;IACrF,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,oBAAoB,CAAC;IACpF,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,oBAAoB,CAAC;IAC/F,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,QAAQ,EAAE,MAAM;QACd,IAAI,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;QACpD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK;QAC5D,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC;QACtE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC;QACxE,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC;KACxE,CAAC;CACH,CAAC;AA0PF,eAAO,MAAM,yBAAyB,QAAO,kBAE5C,CAAC;AAEF,eAAO,MAAM,0BAA0B,GACrC,QAAQ,WAAW,EACnB,UAAU,qBAAqB,KAC9B,oBAoCF,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,QAAQ,WAAW,EACnB,UAAU,qBAAqB,KAC9B,kBAaF,CAAC"}
@@ -0,0 +1,251 @@
1
+ import { Env } from '../../config/env.js';
2
+ import { Logger } from '../../config/logger.js';
3
+ import { ErrorFactory } from '../../exceptions/ZintrustError.js';
4
+ import { SignedRequest } from '../../security/SignedRequest.js';
5
+ const loggedSelections = new Set();
6
+ const resolveSigningPrefix = (baseUrl) => {
7
+ try {
8
+ const parsed = new URL(baseUrl);
9
+ const path = parsed.pathname.endsWith('/') ? parsed.pathname.slice(0, -1) : parsed.pathname;
10
+ if (path === '' || path === '/')
11
+ return undefined;
12
+ return path;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ };
18
+ const buildRequestUrl = (baseUrl, requestPath) => {
19
+ const url = new URL(baseUrl);
20
+ const basePath = url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname;
21
+ const normalizedPath = requestPath.startsWith('/') ? requestPath : `/${requestPath}`;
22
+ url.pathname = `${basePath}${normalizedPath}`;
23
+ return url;
24
+ };
25
+ const buildSigningUrl = (requestUrl, baseUrl) => {
26
+ const prefix = resolveSigningPrefix(baseUrl);
27
+ if (prefix === undefined)
28
+ return requestUrl;
29
+ if (requestUrl.pathname === prefix || requestUrl.pathname.startsWith(`${prefix}/`)) {
30
+ const signingUrl = new URL(requestUrl.toString());
31
+ const stripped = requestUrl.pathname.slice(prefix.length);
32
+ signingUrl.pathname = stripped.startsWith('/') ? stripped : `/${stripped}`;
33
+ return signingUrl;
34
+ }
35
+ return requestUrl;
36
+ };
37
+ const resolveProxyBaseUrl = () => {
38
+ const explicit = Env.REDIS_PROXY_URL.trim();
39
+ if (explicit !== '')
40
+ return explicit;
41
+ return `http://${Env.REDIS_PROXY_HOST}:${Env.REDIS_PROXY_PORT}`;
42
+ };
43
+ const resolveProxySettings = () => ({
44
+ baseUrl: resolveProxyBaseUrl(),
45
+ keyId: Env.REDIS_PROXY_KEY_ID.trim() === '' ? undefined : Env.REDIS_PROXY_KEY_ID,
46
+ secret: Env.REDIS_PROXY_SECRET.trim() === '' ? undefined : Env.REDIS_PROXY_SECRET,
47
+ timeoutMs: Env.REDIS_PROXY_TIMEOUT_MS,
48
+ });
49
+ const buildHeaders = async (settings, requestUrl, body) => {
50
+ const headers = {
51
+ 'Content-Type': 'application/json',
52
+ };
53
+ if (settings.keyId !== undefined && settings.secret !== undefined) {
54
+ const signingUrl = buildSigningUrl(requestUrl, settings.baseUrl);
55
+ const signed = await SignedRequest.createHeaders({
56
+ method: 'POST',
57
+ url: signingUrl,
58
+ body,
59
+ keyId: settings.keyId,
60
+ secret: settings.secret,
61
+ });
62
+ Object.assign(headers, signed);
63
+ }
64
+ return headers;
65
+ };
66
+ const requestProxyCommand = async (settings, command, args) => {
67
+ if (settings.baseUrl.trim() === '') {
68
+ throw ErrorFactory.createConfigError('Redis proxy URL is missing (REDIS_PROXY_URL)');
69
+ }
70
+ const body = JSON.stringify({ command, args });
71
+ const requestUrl = buildRequestUrl(settings.baseUrl, '/zin/redis/command');
72
+ const headers = await buildHeaders(settings, requestUrl, body);
73
+ const signal = typeof AbortSignal !== 'undefined' && 'timeout' in AbortSignal
74
+ ? AbortSignal.timeout(settings.timeoutMs)
75
+ : undefined;
76
+ const response = await fetch(requestUrl.toString(), {
77
+ method: 'POST',
78
+ headers,
79
+ body,
80
+ signal,
81
+ });
82
+ if (!response.ok) {
83
+ const text = await response.text();
84
+ throw ErrorFactory.createTryCatchError(`Redis proxy request failed (${response.status})`, text);
85
+ }
86
+ const parsed = (await response.json());
87
+ return parsed.result;
88
+ };
89
+ const logTransportSelection = (mode, config, options) => {
90
+ const rawSubsystem = options?.subsystem?.trim();
91
+ const subsystem = rawSubsystem === undefined || rawSubsystem === '' ? 'redis' : rawSubsystem;
92
+ const cacheKey = `${subsystem}:${mode}:${config.db}`;
93
+ if (loggedSelections.has(cacheKey)) {
94
+ return;
95
+ }
96
+ loggedSelections.add(cacheKey);
97
+ Logger.info('[redis][transport] resolved transport', {
98
+ subsystem,
99
+ mode,
100
+ db: config.db,
101
+ host: mode === 'direct' ? config.host : undefined,
102
+ port: mode === 'direct' ? config.port : undefined,
103
+ proxyUrl: mode === 'proxy' ? resolveProxyBaseUrl() : undefined,
104
+ });
105
+ };
106
+ const normalizeScanResponse = (value) => {
107
+ if (!Array.isArray(value) || value.length < 2) {
108
+ return ['0', []];
109
+ }
110
+ const cursor = typeof value[0] === 'string' ? value[0] : String(value[0] ?? '0');
111
+ const batch = Array.isArray(value[1])
112
+ ? value[1].filter((entry) => typeof entry === 'string')
113
+ : [];
114
+ return [cursor, batch];
115
+ };
116
+ const createScanStream = (settings, options) => {
117
+ const handlers = new Map();
118
+ const stream = {
119
+ on(event, handler) {
120
+ const current = handlers.get(event) ?? new Set();
121
+ current.add(handler);
122
+ handlers.set(event, current);
123
+ return stream;
124
+ },
125
+ once(event, handler) {
126
+ const wrapped = (...args) => {
127
+ stream.off(event, wrapped);
128
+ handler(...args);
129
+ };
130
+ return stream.on(event, wrapped);
131
+ },
132
+ off(event, handler) {
133
+ handlers.get(event)?.delete(handler);
134
+ return stream;
135
+ },
136
+ };
137
+ const emit = (event, ...args) => {
138
+ for (const handler of handlers.get(event) ?? []) {
139
+ handler(...args);
140
+ }
141
+ };
142
+ queueMicrotask(() => {
143
+ void (async () => {
144
+ try {
145
+ let cursor = '0';
146
+ do {
147
+ // eslint-disable-next-line no-await-in-loop
148
+ const result = await requestProxyCommand(settings, 'SCAN', [
149
+ cursor,
150
+ 'MATCH',
151
+ options?.match ?? '*',
152
+ 'COUNT',
153
+ String(options?.count ?? 200),
154
+ ]);
155
+ const [nextCursor, batch] = normalizeScanResponse(result);
156
+ cursor = nextCursor;
157
+ if (batch.length > 0) {
158
+ emit('data', batch);
159
+ }
160
+ } while (cursor !== '0');
161
+ emit('end');
162
+ }
163
+ catch (error) {
164
+ emit('error', error);
165
+ }
166
+ })();
167
+ });
168
+ return stream;
169
+ };
170
+ const createPipeline = (settings) => {
171
+ const commands = [];
172
+ const target = {
173
+ async exec() {
174
+ const results = [];
175
+ for (const entry of commands) {
176
+ try {
177
+ // eslint-disable-next-line no-await-in-loop
178
+ const result = await requestProxyCommand(settings, entry.command, entry.args);
179
+ results.push([null, result]);
180
+ }
181
+ catch (error) {
182
+ results.push([
183
+ error instanceof Error
184
+ ? error
185
+ : ErrorFactory.createTryCatchError('Redis pipeline command failed', error),
186
+ null,
187
+ ]);
188
+ }
189
+ }
190
+ return results;
191
+ },
192
+ };
193
+ const pipeline = new Proxy(target, {
194
+ get(obj, prop) {
195
+ if (typeof prop !== 'string')
196
+ return Reflect.get(obj, prop);
197
+ if (prop in obj)
198
+ return Reflect.get(obj, prop);
199
+ return (...args) => {
200
+ commands.push({ command: prop.toUpperCase(), args });
201
+ return pipeline;
202
+ };
203
+ },
204
+ });
205
+ return pipeline;
206
+ };
207
+ export const resolveRedisTransportMode = () => {
208
+ return Env.USE_REDIS_PROXY || Env.REDIS_PROXY_URL.trim() !== '' ? 'proxy' : 'direct';
209
+ };
210
+ export const createRedisProxyConnection = (config, options) => {
211
+ const settings = resolveProxySettings();
212
+ logTransportSelection('proxy', config, options);
213
+ const target = {
214
+ status: 'ready',
215
+ connect: async () => { },
216
+ // eslint-disable-next-line @typescript-eslint/require-await
217
+ quit: async () => 'OK',
218
+ disconnect: () => undefined,
219
+ on: (_event, _handler) => client,
220
+ once: (_event, _handler) => client,
221
+ off: (_event, _handler) => client,
222
+ removeListener: (_event, _handler) => client,
223
+ call: async (command, ...args) => {
224
+ return requestProxyCommand(settings, command, args);
225
+ },
226
+ pipeline: () => createPipeline(settings),
227
+ scanStream: (scanOptions) => createScanStream(settings, scanOptions),
228
+ };
229
+ const client = new Proxy(target, {
230
+ get(obj, prop) {
231
+ if (typeof prop !== 'string')
232
+ return Reflect.get(obj, prop);
233
+ if (prop === 'then')
234
+ return undefined;
235
+ if (prop in obj)
236
+ return Reflect.get(obj, prop);
237
+ return async (...args) => requestProxyCommand(settings, prop.toUpperCase(), args);
238
+ },
239
+ });
240
+ return client;
241
+ };
242
+ export const ensureRedisTransportMode = (config, options) => {
243
+ const mode = resolveRedisTransportMode();
244
+ if (mode === 'proxy' && options?.requireDirect === true) {
245
+ throw ErrorFactory.createConfigError(`Redis subsystem '${options.subsystem ?? 'redis'}' requires a direct Redis connection, but proxy mode is enabled.`);
246
+ }
247
+ if (mode === 'direct') {
248
+ logTransportSelection(mode, config, options);
249
+ }
250
+ return mode;
251
+ };