@lensmcp/cluster 1.0.0 → 1.2.0
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/executors/gateway/gateway.lib.d.ts +32 -0
- package/executors/gateway/gateway.lib.d.ts.map +1 -1
- package/executors/gateway/gateway.lib.js +147 -1
- package/executors/gateway/health-check.d.ts +53 -0
- package/executors/gateway/health-check.d.ts.map +1 -0
- package/executors/gateway/health-check.js +66 -0
- package/executors/gateway/main.prod-gateway.js +219 -8
- package/executors/gateway/manifest.d.ts +7 -2
- package/executors/gateway/manifest.d.ts.map +1 -1
- package/executors/gateway/manifest.js +22 -9
- package/executors/gateway/metrics.d.ts +37 -0
- package/executors/gateway/metrics.d.ts.map +1 -0
- package/executors/gateway/metrics.js +56 -0
- package/executors/gateway/otel-tracing.d.ts +47 -0
- package/executors/gateway/otel-tracing.d.ts.map +1 -0
- package/executors/gateway/otel-tracing.js +73 -0
- package/executors/gateway/prod-gateway.lib.d.ts +87 -1
- package/executors/gateway/prod-gateway.lib.d.ts.map +1 -1
- package/executors/gateway/prod-gateway.lib.js +309 -31
- package/executors/gateway/providers-prod.d.ts.map +1 -1
- package/executors/gateway/providers-prod.js +46 -13
- package/executors/gateway/rate-limit.d.ts +66 -0
- package/executors/gateway/rate-limit.d.ts.map +1 -0
- package/executors/gateway/rate-limit.js +91 -0
- package/executors/gateway/schema.d.ts +8 -0
- package/executors/gateway/schema.json +19 -0
- package/executors.json +8 -8
- package/main.devserver.js +5 -4
- package/package.json +23 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory token-bucket rate limiter — OPT-IN, per-instance, per-key.
|
|
3
|
+
*
|
|
4
|
+
* Defense-in-depth, NOT the primary limiter: Cloudflare / Cloud Armor remain the
|
|
5
|
+
* global edge throttle. This caps abuse per gateway instance, keyed by the real
|
|
6
|
+
* client IP (resolved through the same trusted-proxy logic as ABAC), and only on
|
|
7
|
+
* the proxied catch-all — the ops endpoints (/livez, /readyz, /statusz) are
|
|
8
|
+
* registered routes and are never subject to it.
|
|
9
|
+
*
|
|
10
|
+
* Token bucket: `limit` tokens refill smoothly over `windowMs`; each request
|
|
11
|
+
* spends one. Empty bucket → denied with the ms until the next token. Idle
|
|
12
|
+
* (refilled-to-full) buckets are swept so the key map can't grow unbounded.
|
|
13
|
+
*/
|
|
14
|
+
export interface RateLimiterOptions {
|
|
15
|
+
/** Tokens (requests) per window. */
|
|
16
|
+
limit: number;
|
|
17
|
+
/** Window length the `limit` refills over (ms). */
|
|
18
|
+
windowMs: number;
|
|
19
|
+
/** Injectable clock (tests). Defaults to Date.now. */
|
|
20
|
+
now?: () => number;
|
|
21
|
+
}
|
|
22
|
+
export interface RateDecision {
|
|
23
|
+
allowed: boolean;
|
|
24
|
+
remaining: number;
|
|
25
|
+
retryAfterMs: number;
|
|
26
|
+
}
|
|
27
|
+
export interface RateLimiter {
|
|
28
|
+
/** Spend one token for `key`. In-memory returns synchronously; the Redis-backed
|
|
29
|
+
* limiter returns a Promise (one round-trip) — the gateway `await`s either. */
|
|
30
|
+
check(key: string): RateDecision | Promise<RateDecision>;
|
|
31
|
+
stop(): void;
|
|
32
|
+
}
|
|
33
|
+
export declare function createRateLimiter(opts: RateLimiterOptions): RateLimiter;
|
|
34
|
+
/**
|
|
35
|
+
* Redis-backed token-bucket rate limiter — ONE GLOBAL budget shared by every
|
|
36
|
+
* gateway container/instance (so a per-IP limit is N-container-independent, unlike
|
|
37
|
+
* the in-memory limiter whose limit silently multiplies by instance count).
|
|
38
|
+
*
|
|
39
|
+
* The check is a single atomic Lua EVAL (no GET-then-SET race), using Redis's own
|
|
40
|
+
* clock (`TIME`) so instances agree on refill regardless of host clock skew. The
|
|
41
|
+
* bucket key TTLs out shortly after a full refill, so idle clients leave no residue.
|
|
42
|
+
*
|
|
43
|
+
* Hot-path cost: one Redis round-trip per request. It is bounded by `timeoutMs` and
|
|
44
|
+
* **fails open** by default — a slow/unreachable Redis must not 429 all traffic and
|
|
45
|
+
* take the gateway down (it's defense-in-depth; Cloudflare/Cloud Armor stay the
|
|
46
|
+
* primary edge throttle). Set `failOpen: false` only if you'd rather shed load than
|
|
47
|
+
* risk exceeding the budget during a Redis outage.
|
|
48
|
+
*/
|
|
49
|
+
export interface RedisRateLimiterOptions {
|
|
50
|
+
/** Tokens (requests) per window. */
|
|
51
|
+
limit: number;
|
|
52
|
+
/** Window the `limit` refills over (ms). */
|
|
53
|
+
windowMs: number;
|
|
54
|
+
/** Key namespace — the Redis key is `<prefix><key>`. Default 'lensmcp:gw:rl:'. */
|
|
55
|
+
prefix?: string;
|
|
56
|
+
/** Max ms to wait on the Redis round-trip before applying the fail policy. Default 100. */
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
/** On Redis error/timeout: true → allow (default), false → deny. */
|
|
59
|
+
failOpen?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** The single ioredis method the limiter needs — an atomic script eval. */
|
|
62
|
+
export interface RateLimitRedis {
|
|
63
|
+
eval(script: string, numKeys: number, ...args: (string | number)[]): Promise<unknown>;
|
|
64
|
+
}
|
|
65
|
+
export declare function createRedisRateLimiter(client: RateLimitRedis, opts: RedisRateLimiterOptions): RateLimiter;
|
|
66
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,kBAAkB;IACjC,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAAG,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE;AAE3F,MAAM,WAAW,WAAW;IAC1B;oFACgF;IAChF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACzD,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,WAAW,CA6BvE;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,uBAAuB;IACtC,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACvF;AA2BD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,uBAAuB,GAAG,WAAW,CAgCzG"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRateLimiter = createRateLimiter;
|
|
4
|
+
exports.createRedisRateLimiter = createRedisRateLimiter;
|
|
5
|
+
function createRateLimiter(opts) {
|
|
6
|
+
const limit = Math.max(1, opts.limit);
|
|
7
|
+
const windowMs = Math.max(1, opts.windowMs);
|
|
8
|
+
const now = opts.now ?? Date.now;
|
|
9
|
+
const refillPerMs = limit / windowMs;
|
|
10
|
+
const buckets = new Map();
|
|
11
|
+
const refilled = (b, t) => Math.min(limit, b.tokens + (t - b.last) * refillPerMs);
|
|
12
|
+
// Evict buckets that have refilled to full (idle) so the map stays bounded.
|
|
13
|
+
const sweep = setInterval(() => {
|
|
14
|
+
const t = now();
|
|
15
|
+
for (const [k, b] of buckets)
|
|
16
|
+
if (refilled(b, t) >= limit)
|
|
17
|
+
buckets.delete(k);
|
|
18
|
+
}, windowMs);
|
|
19
|
+
sweep.unref?.();
|
|
20
|
+
return {
|
|
21
|
+
check(key) {
|
|
22
|
+
const t = now();
|
|
23
|
+
let b = buckets.get(key);
|
|
24
|
+
if (!b) {
|
|
25
|
+
b = { tokens: limit, last: t };
|
|
26
|
+
buckets.set(key, b);
|
|
27
|
+
}
|
|
28
|
+
b.tokens = refilled(b, t);
|
|
29
|
+
b.last = t;
|
|
30
|
+
if (b.tokens >= 1) {
|
|
31
|
+
b.tokens -= 1;
|
|
32
|
+
return { allowed: true, remaining: Math.floor(b.tokens), retryAfterMs: 0 };
|
|
33
|
+
}
|
|
34
|
+
return { allowed: false, remaining: 0, retryAfterMs: Math.ceil((1 - b.tokens) / refillPerMs) };
|
|
35
|
+
},
|
|
36
|
+
stop() { clearInterval(sweep); },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Atomic token bucket in one round-trip. Stores fractional tokens (Redis keeps the
|
|
40
|
+
// string form) so refill is exact across calls; only the returned remaining/retry
|
|
41
|
+
// are integer-coerced by the Lua→RESP boundary, which is fine.
|
|
42
|
+
const BUCKET_LUA = `
|
|
43
|
+
local b = redis.call('HMGET', KEYS[1], 't', 's')
|
|
44
|
+
local cap = tonumber(ARGV[1])
|
|
45
|
+
local refill = tonumber(ARGV[2])
|
|
46
|
+
local ttl = tonumber(ARGV[3])
|
|
47
|
+
local tm = redis.call('TIME')
|
|
48
|
+
local now = tm[1] * 1000 + tm[2] / 1000
|
|
49
|
+
local tokens = tonumber(b[1])
|
|
50
|
+
local ts = tonumber(b[2])
|
|
51
|
+
if tokens == nil then tokens = cap; ts = now end
|
|
52
|
+
tokens = math.min(cap, tokens + (now - ts) * refill)
|
|
53
|
+
local ok = 0
|
|
54
|
+
if tokens >= 1 then tokens = tokens - 1; ok = 1 end
|
|
55
|
+
redis.call('HSET', KEYS[1], 't', tokens, 's', now)
|
|
56
|
+
redis.call('PEXPIRE', KEYS[1], ttl)
|
|
57
|
+
local retry = 0
|
|
58
|
+
if ok == 0 then retry = math.ceil((1 - tokens) / refill) end
|
|
59
|
+
return {ok, math.floor(tokens), retry}`;
|
|
60
|
+
const RL_TIMEOUT = Symbol('rl-timeout');
|
|
61
|
+
const RL_FAIL = Symbol('rl-fail');
|
|
62
|
+
function createRedisRateLimiter(client, opts) {
|
|
63
|
+
const limit = Math.max(1, opts.limit);
|
|
64
|
+
const windowMs = Math.max(1, opts.windowMs);
|
|
65
|
+
const prefix = opts.prefix ?? 'lensmcp:gw:rl:';
|
|
66
|
+
const timeoutMs = Math.max(1, opts.timeoutMs ?? 100);
|
|
67
|
+
const failOpen = opts.failOpen !== false;
|
|
68
|
+
const refillPerMs = limit / windowMs;
|
|
69
|
+
const ttlMs = Math.ceil(windowMs) + 1000; // evict idle buckets shortly after a full refill
|
|
70
|
+
const onFail = () => failOpen
|
|
71
|
+
? { allowed: true, remaining: limit, retryAfterMs: 0 }
|
|
72
|
+
: { allowed: false, remaining: 0, retryAfterMs: windowMs };
|
|
73
|
+
return {
|
|
74
|
+
async check(key) {
|
|
75
|
+
let timer;
|
|
76
|
+
// The eval branch is mapped so it can never reject (→ RL_FAIL) — that keeps a
|
|
77
|
+
// late rejection (after a timeout already won the race) from going unhandled.
|
|
78
|
+
const evalP = Promise.resolve(client.eval(BUCKET_LUA, 1, `${prefix}${key}`, limit, refillPerMs, ttlMs)).then((v) => v, () => RL_FAIL);
|
|
79
|
+
const timeoutP = new Promise((res) => { timer = setTimeout(() => res(RL_TIMEOUT), timeoutMs); });
|
|
80
|
+
const res = await Promise.race([evalP, timeoutP]);
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
if (res === RL_TIMEOUT || res === RL_FAIL)
|
|
83
|
+
return onFail();
|
|
84
|
+
const [ok, remaining, retry] = res;
|
|
85
|
+
return ok === 1
|
|
86
|
+
? { allowed: true, remaining: Number(remaining) || 0, retryAfterMs: 0 }
|
|
87
|
+
: { allowed: false, remaining: 0, retryAfterMs: Number(retry) || windowMs };
|
|
88
|
+
},
|
|
89
|
+
stop() { },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -7,4 +7,12 @@ export interface GatewayExecutorSchema {
|
|
|
7
7
|
trafficFlushMs?: number;
|
|
8
8
|
/** Workspace-relative path to a per-request middleware module (the JWT seam). */
|
|
9
9
|
middleware?: string;
|
|
10
|
+
/** Spawn the lens dashboard as a gateway-managed child + route lens.<baseDomain>. Default true. */
|
|
11
|
+
dashboard?: boolean;
|
|
12
|
+
/** Spawn the single MCP server as a gateway-managed child when a `lens:true` app exists. Default true. */
|
|
13
|
+
mcp?: boolean;
|
|
14
|
+
/** Port the managed dashboard listens on (proxied behind lens.<baseDomain>). Default 4321. */
|
|
15
|
+
dashboardPort?: number;
|
|
16
|
+
/** Override the dashboard hostname. Default lens.<baseDomain>. */
|
|
17
|
+
lensHost?: string;
|
|
10
18
|
}
|
|
@@ -24,6 +24,25 @@
|
|
|
24
24
|
"trafficFlushMs": {
|
|
25
25
|
"type": "number",
|
|
26
26
|
"description": "Traffic-edge aggregation window in ms before flushing to the LensMCP bus. Default 5000; lower (e.g. 1000) for near-realtime canvases."
|
|
27
|
+
},
|
|
28
|
+
"dashboard": {
|
|
29
|
+
"type": "boolean",
|
|
30
|
+
"default": true,
|
|
31
|
+
"description": "Spawn the LensMCP dashboard (the human web view of the lens) as a gateway-managed child and route it at lens.<baseDomain>. The gateway also owns the single MCP server, so `lens:true` frontend apps run only their Vite + instrumentation. Set false to skip (e.g. when a separate `lensmcp dashboard` is already running)."
|
|
32
|
+
},
|
|
33
|
+
"mcp": {
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"default": true,
|
|
36
|
+
"description": "Spawn the single LensMCP MCP server (the agent surface) as a gateway-managed child when any `lens:true` frontend app is declared. Set false if an external `lensmcp mcp` / agent-dev already owns it."
|
|
37
|
+
},
|
|
38
|
+
"dashboardPort": {
|
|
39
|
+
"type": "number",
|
|
40
|
+
"default": 4321,
|
|
41
|
+
"description": "Port the gateway-managed dashboard listens on (proxied behind lens.<baseDomain>). Default 4321."
|
|
42
|
+
},
|
|
43
|
+
"lensHost": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Override the dashboard's dev hostname. Default lens.<baseDomain>, where <baseDomain> is the longest common dotted suffix of the declared cluster hosts (e.g. lens.tetros.ai.local)."
|
|
27
46
|
}
|
|
28
47
|
},
|
|
29
48
|
"additionalProperties": false
|
package/executors.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"executors": {
|
|
3
3
|
"build": {
|
|
4
|
-
"implementation": "./
|
|
5
|
-
"schema": "./
|
|
4
|
+
"implementation": "./executors/build/build.impl",
|
|
5
|
+
"schema": "./executors/build/schema.json",
|
|
6
6
|
"description": "Production webpack build for a NestJS application."
|
|
7
7
|
},
|
|
8
8
|
"serve": {
|
|
9
|
-
"implementation": "./
|
|
10
|
-
"schema": "./
|
|
9
|
+
"implementation": "./executors/serve/serve.impl",
|
|
10
|
+
"schema": "./executors/serve/schema.json",
|
|
11
11
|
"description": "Webpack watch mode with integrated devserver for a NestJS application."
|
|
12
12
|
},
|
|
13
13
|
"trust": {
|
|
14
|
-
"implementation": "./
|
|
15
|
-
"schema": "./
|
|
14
|
+
"implementation": "./executors/trust/trust.impl",
|
|
15
|
+
"schema": "./executors/trust/schema.json",
|
|
16
16
|
"description": "One-time HTTPS dev setup: trust the davnx local CA + write /etc/hosts entries for gateway hostnames (idempotent, sudo prompts inline)."
|
|
17
17
|
},
|
|
18
18
|
"gateway": {
|
|
19
|
-
"implementation": "./
|
|
20
|
-
"schema": "./
|
|
19
|
+
"implementation": "./executors/gateway/gateway.impl",
|
|
20
|
+
"schema": "./executors/gateway/schema.json",
|
|
21
21
|
"description": "Workspace https front door: reads every project davnx declaration (host/port/prefix), routes by hostname, serves the local-CA cert with all SANs."
|
|
22
22
|
}
|
|
23
23
|
}
|
package/main.devserver.js
CHANGED
|
@@ -119,7 +119,7 @@ function cleanupSock(sockPath) {
|
|
|
119
119
|
try {
|
|
120
120
|
fs.unlinkSync(sockPath);
|
|
121
121
|
}
|
|
122
|
-
catch { }
|
|
122
|
+
catch { /* best-effort: socket already gone */ }
|
|
123
123
|
}
|
|
124
124
|
// delete from CJS cache + require fresh
|
|
125
125
|
async function importFresh(spec) {
|
|
@@ -281,6 +281,7 @@ if (process.env.APP_RUNNER === '1') {
|
|
|
281
281
|
inspector?.close?.();
|
|
282
282
|
}
|
|
283
283
|
catch {
|
|
284
|
+
/* inspector may already be detached */
|
|
284
285
|
}
|
|
285
286
|
});
|
|
286
287
|
const shutdown = async (sig) => {
|
|
@@ -546,7 +547,7 @@ else {
|
|
|
546
547
|
try {
|
|
547
548
|
w.proc.kill('SIGTERM');
|
|
548
549
|
}
|
|
549
|
-
catch { }
|
|
550
|
+
catch { /* worker already exited */ }
|
|
550
551
|
}
|
|
551
552
|
workerProcesses.length = 0;
|
|
552
553
|
for (const name of WORKER_NAMES) {
|
|
@@ -789,7 +790,7 @@ else {
|
|
|
789
790
|
try {
|
|
790
791
|
w.proc.kill('SIGTERM');
|
|
791
792
|
}
|
|
792
|
-
catch { }
|
|
793
|
+
catch { /* worker already exited */ }
|
|
793
794
|
}
|
|
794
795
|
for (const c of children) {
|
|
795
796
|
try {
|
|
@@ -804,7 +805,7 @@ else {
|
|
|
804
805
|
try {
|
|
805
806
|
fs.rmdirSync(SOCK_DIR);
|
|
806
807
|
}
|
|
807
|
-
catch { }
|
|
808
|
+
catch { /* best-effort: dir missing or non-empty */ }
|
|
808
809
|
setTimeout(() => process.exit(0), 700).unref();
|
|
809
810
|
};
|
|
810
811
|
['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGUSR2'].forEach(sig => process.on(sig, () => void shutdown(sig)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -31,7 +31,12 @@
|
|
|
31
31
|
"webpack": "^5.0.0",
|
|
32
32
|
"fastify": "^5.0.0",
|
|
33
33
|
"@fastify/reply-from": "^12.0.0",
|
|
34
|
-
"ioredis": "^5.0.0"
|
|
34
|
+
"ioredis": "^5.0.0",
|
|
35
|
+
"@opentelemetry/api": "^1.9.0",
|
|
36
|
+
"@opentelemetry/core": "^1.30.0",
|
|
37
|
+
"@opentelemetry/resources": "^1.30.0",
|
|
38
|
+
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
39
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0"
|
|
35
40
|
},
|
|
36
41
|
"peerDependenciesMeta": {
|
|
37
42
|
"fastify": {
|
|
@@ -42,6 +47,21 @@
|
|
|
42
47
|
},
|
|
43
48
|
"ioredis": {
|
|
44
49
|
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@opentelemetry/api": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"@opentelemetry/core": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@opentelemetry/resources": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"@opentelemetry/sdk-trace-base": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@opentelemetry/exporter-trace-otlp-http": {
|
|
64
|
+
"optional": true
|
|
45
65
|
}
|
|
46
66
|
},
|
|
47
67
|
"dependencies": {
|
|
@@ -53,6 +73,7 @@
|
|
|
53
73
|
"node-forge": "^1.3.1",
|
|
54
74
|
"pino-pretty": "^13.0.0",
|
|
55
75
|
"terser": "^5.0.0",
|
|
76
|
+
"tslib": "^2.3.0",
|
|
56
77
|
"webpack-node-externals": "^3.0.0"
|
|
57
78
|
},
|
|
58
79
|
"devDependencies": {
|