@gearbox-protocol/cli-utils 9.2.0 → 9.2.2
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/dist/index.d.ts +249 -229
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1061 -1298
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +58 -56
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +61 -22
- package/dist/schema.js.map +1 -1
- package/package.json +16 -20
- package/dist/chunk-GGX3YGGH.js +0 -63
- package/dist/chunk-GGX3YGGH.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1384 +1,1147 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
clientWhitelistItemSchema,
|
|
6
|
-
optionalAddressArrayLike,
|
|
7
|
-
optionalStringArrayLike,
|
|
8
|
-
stringArrayLike,
|
|
9
|
-
whitelistItemSchema,
|
|
10
|
-
whitelistItemsSchema
|
|
11
|
-
} from "./chunk-GGX3YGGH.js";
|
|
12
|
-
|
|
13
|
-
// src/CensoredString.ts
|
|
14
|
-
var CensoredString = class _CensoredString {
|
|
15
|
-
#value;
|
|
16
|
-
#censored;
|
|
17
|
-
static transform(value) {
|
|
18
|
-
if (value instanceof _CensoredString) {
|
|
19
|
-
return value;
|
|
20
|
-
}
|
|
21
|
-
return new _CensoredString(value);
|
|
22
|
-
}
|
|
23
|
-
constructor(value) {
|
|
24
|
-
this.#value = value;
|
|
25
|
-
const visibleLen = Math.min(4, Math.floor(0.2 * value.length));
|
|
26
|
-
if (visibleLen < 1) {
|
|
27
|
-
this.#censored = "*".repeat(Math.min(8, value.length));
|
|
28
|
-
} else {
|
|
29
|
-
this.#censored = "*".repeat(Math.min(8, value.length - visibleLen)) + value.slice(-1 * visibleLen);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
get value() {
|
|
33
|
-
return this.#value;
|
|
34
|
-
}
|
|
35
|
-
toString() {
|
|
36
|
-
return this.#censored;
|
|
37
|
-
}
|
|
38
|
-
toJSON() {
|
|
39
|
-
return this.toString();
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// src/CensoredURL.ts
|
|
44
|
-
var CensoredURL = class _CensoredURL {
|
|
45
|
-
#value;
|
|
46
|
-
#censored;
|
|
47
|
-
static transform(value) {
|
|
48
|
-
if (value instanceof _CensoredURL) {
|
|
49
|
-
return value;
|
|
50
|
-
}
|
|
51
|
-
return new _CensoredURL(value);
|
|
52
|
-
}
|
|
53
|
-
constructor(url) {
|
|
54
|
-
this.#value = url;
|
|
55
|
-
if (url.includes("127.0.0.1") || url.includes("localhost") || url.includes("host.docker.internal")) {
|
|
56
|
-
this.#censored = url;
|
|
57
|
-
} else {
|
|
58
|
-
try {
|
|
59
|
-
const u = new URL(url);
|
|
60
|
-
if (u.username || u.password) {
|
|
61
|
-
u.username = u.username ? "****" : "";
|
|
62
|
-
u.password = u.password ? "****" : "";
|
|
63
|
-
}
|
|
64
|
-
u.pathname = "/";
|
|
65
|
-
u.search = "";
|
|
66
|
-
this.#censored = `${u.href}******`;
|
|
67
|
-
} catch {
|
|
68
|
-
const visibleLen = Math.min(4, Math.floor(0.2 * url.length));
|
|
69
|
-
if (visibleLen < 1) {
|
|
70
|
-
this.#censored = "*".repeat(Math.min(8, url.length));
|
|
71
|
-
} else {
|
|
72
|
-
this.#censored = "*".repeat(Math.min(8, url.length - visibleLen)) + url.slice(-1 * visibleLen);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
get value() {
|
|
78
|
-
return this.#value;
|
|
79
|
-
}
|
|
80
|
-
toString() {
|
|
81
|
-
return this.#censored;
|
|
82
|
-
}
|
|
83
|
-
toJSON() {
|
|
84
|
-
return this.toString();
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// src/compressedConfig.ts
|
|
89
|
-
import { deflateSync, inflateSync } from "zlib";
|
|
90
|
-
|
|
91
|
-
// src/resolveYamlFiles.ts
|
|
92
|
-
import { readFile } from "fs/promises";
|
|
93
|
-
import { dirname, resolve as pathResolve } from "path";
|
|
1
|
+
import { addressArrayLike, addressLike, boolLike, clientWhitelistItemSchema, optionalAddressArrayLike, optionalStringArrayLike, stringArrayLike, whitelistItemSchema, whitelistItemsSchema } from "./schema.js";
|
|
2
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
94
5
|
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
95
6
|
import YAML, { isPair, isScalar, isSeq } from "yaml";
|
|
96
|
-
|
|
97
|
-
|
|
7
|
+
import { ADDRESS_0X0, AddressMap, SUPPORTED_NETWORKS, chains, getChain } from "@gearbox-protocol/sdk";
|
|
8
|
+
import { prettifyError } from "zod";
|
|
9
|
+
import { z } from "zod/v4";
|
|
10
|
+
import { withRetry } from "viem";
|
|
11
|
+
import { md } from "@vlad-yakovlev/telegram-md";
|
|
12
|
+
import Queue from "p-queue";
|
|
13
|
+
import { LRUCache } from "lru-cache";
|
|
14
|
+
import { Command, Option } from "@commander-js/extra-typings";
|
|
15
|
+
import * as z4 from "zod/v4/core";
|
|
16
|
+
import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
|
|
17
|
+
import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
|
|
18
|
+
//#region src/CensoredString.ts
|
|
19
|
+
var CensoredString = class CensoredString {
|
|
20
|
+
#value;
|
|
21
|
+
#censored;
|
|
22
|
+
static transform(value) {
|
|
23
|
+
if (value instanceof CensoredString) return value;
|
|
24
|
+
return new CensoredString(value);
|
|
25
|
+
}
|
|
26
|
+
constructor(value) {
|
|
27
|
+
this.#value = value;
|
|
28
|
+
const visibleLen = Math.min(4, Math.floor(.2 * value.length));
|
|
29
|
+
if (visibleLen < 1) this.#censored = "*".repeat(Math.min(8, value.length));
|
|
30
|
+
else this.#censored = "*".repeat(Math.min(8, value.length - visibleLen)) + value.slice(-1 * visibleLen);
|
|
31
|
+
}
|
|
32
|
+
get value() {
|
|
33
|
+
return this.#value;
|
|
34
|
+
}
|
|
35
|
+
toString() {
|
|
36
|
+
return this.#censored;
|
|
37
|
+
}
|
|
38
|
+
toJSON() {
|
|
39
|
+
return this.toString();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/CensoredURL.ts
|
|
44
|
+
/**
|
|
45
|
+
* Assumes that host is always visible, and the secret part is in path or query
|
|
46
|
+
*/
|
|
47
|
+
var CensoredURL = class CensoredURL {
|
|
48
|
+
#value;
|
|
49
|
+
#censored;
|
|
50
|
+
static transform(value) {
|
|
51
|
+
if (value instanceof CensoredURL) return value;
|
|
52
|
+
return new CensoredURL(value);
|
|
53
|
+
}
|
|
54
|
+
constructor(url) {
|
|
55
|
+
this.#value = url;
|
|
56
|
+
if (url.includes("127.0.0.1") || url.includes("localhost") || url.includes("host.docker.internal")) this.#censored = url;
|
|
57
|
+
else try {
|
|
58
|
+
const u = new URL(url);
|
|
59
|
+
if (u.username || u.password) {
|
|
60
|
+
u.username = u.username ? "****" : "";
|
|
61
|
+
u.password = u.password ? "****" : "";
|
|
62
|
+
}
|
|
63
|
+
u.pathname = "/";
|
|
64
|
+
u.search = "";
|
|
65
|
+
this.#censored = `${u.href}******`;
|
|
66
|
+
} catch {
|
|
67
|
+
const visibleLen = Math.min(4, Math.floor(.2 * url.length));
|
|
68
|
+
if (visibleLen < 1) this.#censored = "*".repeat(Math.min(8, url.length));
|
|
69
|
+
else this.#censored = "*".repeat(Math.min(8, url.length - visibleLen)) + url.slice(-1 * visibleLen);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
get value() {
|
|
73
|
+
return this.#value;
|
|
74
|
+
}
|
|
75
|
+
toString() {
|
|
76
|
+
return this.#censored;
|
|
77
|
+
}
|
|
78
|
+
toJSON() {
|
|
79
|
+
return this.toString();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/getPromisePath.ts
|
|
84
|
+
/**
|
|
85
|
+
* Similar to lodash.get, but supports promises
|
|
86
|
+
* @param object
|
|
87
|
+
* @param keys
|
|
88
|
+
* @returns
|
|
89
|
+
*/
|
|
98
90
|
async function getPromisePath(object, path) {
|
|
99
|
-
|
|
91
|
+
return getPromised(object, path.split("."));
|
|
100
92
|
}
|
|
101
93
|
async function getPromised(object, keys) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const value = object[key];
|
|
107
|
-
const resolved = await Promise.resolve(value);
|
|
108
|
-
return getPromised(resolved, rest);
|
|
94
|
+
if (keys.length === 0) return Promise.resolve(object);
|
|
95
|
+
const [key, ...rest] = keys;
|
|
96
|
+
const value = object[key];
|
|
97
|
+
return getPromised(await Promise.resolve(value), rest);
|
|
109
98
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/resolveYamlFiles.ts
|
|
101
|
+
/**
|
|
102
|
+
* Template key regexp
|
|
103
|
+
* Consists of alphanumeric+underscore parts separated by dots, all wrappped in env-style brackets
|
|
104
|
+
* e.g. ${KEY} or ${KEY.SUBKEY}
|
|
105
|
+
*
|
|
106
|
+
* Optionally supports a bash-style default value via `:-`, e.g. `${KEY:-fallback}`.
|
|
107
|
+
* The default is used when the resolved value is undefined, null, or an empty string.
|
|
108
|
+
* Capture groups: 1 = key path, 2 = optional default literal (no `}` allowed).
|
|
109
|
+
*/
|
|
110
|
+
const templateKeyRegex = /\$\{([\w-]+(?:\.[\w-]+)*)(?::-([^}]*))?\}/g;
|
|
111
|
+
/**
|
|
112
|
+
* Replaces all `${...}` template tokens in a string using the provided template data.
|
|
113
|
+
* Supports bash-style `${KEY:-default}` fallback when the resolved value is missing
|
|
114
|
+
* (undefined, null, or empty string). The default is a literal and is not further
|
|
115
|
+
* interpolated.
|
|
116
|
+
*/
|
|
113
117
|
async function resolveScalarValue(val, templateData) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return val;
|
|
118
|
+
const matches = val.matchAll(templateKeyRegex);
|
|
119
|
+
for (const match of matches) {
|
|
120
|
+
const [whole, key, defaultValue] = match;
|
|
121
|
+
const resolved = await getPromisePath(templateData, key);
|
|
122
|
+
if (resolved !== void 0 && resolved !== null && resolved !== "") val = val.replace(whole, String(resolved));
|
|
123
|
+
else if (defaultValue !== void 0) val = val.replace(whole, defaultValue);
|
|
124
|
+
}
|
|
125
|
+
return val;
|
|
125
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Merges yaml files, respecting .extends references.
|
|
129
|
+
* Performs env variable substitution on the config.
|
|
130
|
+
* Respects YAML anchors and aliases, and merge keys - so that they can be shared across two files
|
|
131
|
+
*
|
|
132
|
+
* x-* anchors are removed from resulting document (a-la docker compose yaml)
|
|
133
|
+
*
|
|
134
|
+
* @param file
|
|
135
|
+
* @param templateData
|
|
136
|
+
* @returns
|
|
137
|
+
*/
|
|
126
138
|
async function resolveYamlFiles(file, templateData) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return Object.fromEntries(
|
|
130
|
-
Object.entries(obj).filter(([k]) => !k.startsWith("x-"))
|
|
131
|
-
);
|
|
139
|
+
const obj = (await resolveYamlDoc(file, templateData)).toJSON();
|
|
140
|
+
return Object.fromEntries(Object.entries(obj).filter(([k]) => !k.startsWith("x-")));
|
|
132
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Merges yaml documents, respecting .extends references.
|
|
144
|
+
* Optionally, performs env-style variable substitution on the config.
|
|
145
|
+
* Respects YAML anchors and aliases, and merge keys - so that they can be shared across two files
|
|
146
|
+
* Supports s3 links for file
|
|
147
|
+
*
|
|
148
|
+
* @param file
|
|
149
|
+
* @param templateData
|
|
150
|
+
* @returns
|
|
151
|
+
*/
|
|
133
152
|
async function resolveYamlDoc(file, templateData) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
await YAML.visitAsync(cfgRaw, {
|
|
157
|
-
Scalar: async (_, node) => {
|
|
158
|
-
if (typeof node.value === "string") {
|
|
159
|
-
node.value = await resolveScalarValue(node.value, templateData);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
return cfgRaw;
|
|
153
|
+
const content = await loadFile(file);
|
|
154
|
+
let cfgRaw = YAML.parseDocument(content, { merge: true });
|
|
155
|
+
if (cfgRaw.has("extends")) {
|
|
156
|
+
const baseFile = cfgRaw.get("extends");
|
|
157
|
+
const extended = cfgRaw;
|
|
158
|
+
cfgRaw = await resolveYamlDoc(getRelativePath(file, baseFile), templateData);
|
|
159
|
+
YAML.visit(extended, { Pair(_, pair, rawPath) {
|
|
160
|
+
const path = getNodesPath(rawPath);
|
|
161
|
+
if (isScalar(pair.key) && (isScalar(pair.value) || isSeq(pair.value))) {
|
|
162
|
+
if (pair.key.value === "extends") return;
|
|
163
|
+
cfgRaw.setIn([...path, pair.key.value], pair.value);
|
|
164
|
+
return YAML.visit.SKIP;
|
|
165
|
+
}
|
|
166
|
+
} });
|
|
167
|
+
}
|
|
168
|
+
await YAML.visitAsync(cfgRaw, { Scalar: async (_, node) => {
|
|
169
|
+
if (typeof node.value === "string") node.value = await resolveScalarValue(node.value, templateData);
|
|
170
|
+
} });
|
|
171
|
+
return cfgRaw;
|
|
164
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Helper function that gets object with templated values
|
|
175
|
+
* Just because async ast already exists in yaml, but not in json reviver
|
|
176
|
+
* @param json
|
|
177
|
+
* @param templateData
|
|
178
|
+
* @returns
|
|
179
|
+
*/
|
|
165
180
|
async function resolveFromObject(json, templateData) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
return cfgRaw.toJSON();
|
|
181
|
+
const content = YAML.stringify(json);
|
|
182
|
+
const cfgRaw = YAML.parseDocument(content, { merge: true });
|
|
183
|
+
await YAML.visitAsync(cfgRaw, { Scalar: async (_, node) => {
|
|
184
|
+
if (typeof node.value === "string") node.value = await resolveScalarValue(node.value, templateData);
|
|
185
|
+
} });
|
|
186
|
+
return cfgRaw.toJSON();
|
|
176
187
|
}
|
|
177
188
|
function getNodesPath(nodes) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return "";
|
|
183
|
-
}).filter(Boolean);
|
|
189
|
+
return nodes.map((n) => {
|
|
190
|
+
if (isPair(n) && isScalar(n.key)) return n.key.value;
|
|
191
|
+
return "";
|
|
192
|
+
}).filter(Boolean);
|
|
184
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Loads file from s3 or local file system
|
|
196
|
+
* @param file - s3 link or local file path
|
|
197
|
+
* @returns file content
|
|
198
|
+
*/
|
|
185
199
|
async function loadFile(file) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
return await readFile(file, "utf-8");
|
|
200
|
+
if (file.startsWith("s3://")) return await loadS3File(file);
|
|
201
|
+
return await readFile(file, "utf-8");
|
|
190
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Loads file from s3
|
|
205
|
+
* @param file - s3 link like s3://bucket/path/to/file.yaml
|
|
206
|
+
* @returns file content
|
|
207
|
+
*/
|
|
191
208
|
async function loadS3File(file) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
202
|
-
if (!response.Body) {
|
|
203
|
-
throw new Error(`file ${file} not found`);
|
|
204
|
-
}
|
|
205
|
-
return response.Body.transformToString("utf-8");
|
|
209
|
+
const url = new URL(file);
|
|
210
|
+
const bucket = url.hostname;
|
|
211
|
+
const key = url.pathname.slice(1);
|
|
212
|
+
const response = await new S3Client({}).send(new GetObjectCommand({
|
|
213
|
+
Bucket: bucket,
|
|
214
|
+
Key: key
|
|
215
|
+
}));
|
|
216
|
+
if (!response.Body) throw new Error(`file ${file} not found`);
|
|
217
|
+
return response.Body.transformToString("utf-8");
|
|
206
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Given file and a relative file path, returns full path of the relative file
|
|
221
|
+
* Supports s3 links for base file
|
|
222
|
+
* @param file
|
|
223
|
+
* @param relativeFile
|
|
224
|
+
*/
|
|
207
225
|
function getRelativePath(baseFile, relativeFile) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return pathResolve(dirname(baseFile), relativeFile);
|
|
226
|
+
if (baseFile.startsWith("s3://")) {
|
|
227
|
+
const url = new URL(baseFile);
|
|
228
|
+
url.pathname = resolve(dirname(url.pathname), relativeFile);
|
|
229
|
+
return url.toString();
|
|
230
|
+
}
|
|
231
|
+
return resolve(dirname(baseFile), relativeFile);
|
|
215
232
|
}
|
|
216
|
-
|
|
217
|
-
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/compressedConfig.ts
|
|
235
|
+
/**
|
|
236
|
+
* Compress a plain JS object into a base64url string
|
|
237
|
+
* suitable for env vars.
|
|
238
|
+
*/
|
|
218
239
|
function compressObject(obj) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return compressed.toString("base64url");
|
|
240
|
+
const json = JSON.stringify(obj);
|
|
241
|
+
return deflateSync(Buffer.from(json, "utf8")).toString("base64url");
|
|
222
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Decompress a base64url string back into a JS object
|
|
245
|
+
* Allows to pass template data to resolve promises/env
|
|
246
|
+
*/
|
|
223
247
|
async function decompressObject(value, templateData) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const json = JSON.parse(str);
|
|
227
|
-
const result = await resolveFromObject(json, templateData);
|
|
228
|
-
return result;
|
|
248
|
+
const str = inflateSync(Buffer.from(value, "base64url")).toString("utf8");
|
|
249
|
+
return await resolveFromObject(JSON.parse(str), templateData);
|
|
229
250
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/createPinoCensor.ts
|
|
253
|
+
/**
|
|
254
|
+
* Raw values shorter than this are not censored, to avoid masking unrelated
|
|
255
|
+
* substrings throughout the log output.
|
|
256
|
+
*/
|
|
257
|
+
const MIN_SECRET_LENGTH = 4;
|
|
233
258
|
function escapeRegExp(value) {
|
|
234
|
-
|
|
259
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
235
260
|
}
|
|
236
261
|
function collectSecrets(value, out) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
if (Array.isArray(value)) {
|
|
251
|
-
for (const item of value) {
|
|
252
|
-
collectSecrets(item, out);
|
|
253
|
-
}
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
for (const item of Object.values(value)) {
|
|
257
|
-
collectSecrets(item, out);
|
|
258
|
-
}
|
|
262
|
+
if (value == null) return;
|
|
263
|
+
if (value instanceof CensoredString || value instanceof CensoredURL) {
|
|
264
|
+
const raw = value.value;
|
|
265
|
+
if (raw.length >= MIN_SECRET_LENGTH) out.set(raw, value.toString());
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (typeof value !== "object") return;
|
|
269
|
+
if (Array.isArray(value)) {
|
|
270
|
+
for (const item of value) collectSecrets(item, out);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
for (const item of Object.values(value)) collectSecrets(item, out);
|
|
259
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Gathers known secrets from a config and returns a function that replaces each raw secret with its masked representation.
|
|
277
|
+
* Intended for pino's `hooks.streamWrite`
|
|
278
|
+
*/
|
|
260
279
|
function createPinoCensor(config) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const re = new RegExp(pattern, "g");
|
|
268
|
-
return (s) => s.replace(re, (m) => map.get(m) ?? m);
|
|
280
|
+
const map = /* @__PURE__ */ new Map();
|
|
281
|
+
collectSecrets(config, map);
|
|
282
|
+
if (map.size === 0) return (s) => s;
|
|
283
|
+
const pattern = [...map.keys()].sort((a, b) => b.length - a.length).map(escapeRegExp).join("|");
|
|
284
|
+
const re = new RegExp(pattern, "g");
|
|
285
|
+
return (s) => s.replace(re, (m) => map.get(m) ?? m);
|
|
269
286
|
}
|
|
270
|
-
|
|
271
|
-
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/sleep.ts
|
|
272
289
|
async function sleep(ms) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
setTimeout(resolve, ms);
|
|
292
|
+
});
|
|
276
293
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/fetchRetry.ts
|
|
296
|
+
const DEFAULT_RETRIES = 5;
|
|
297
|
+
const DEFAULT_RETRY_INTERVAL$2 = 1e3;
|
|
281
298
|
async function fetchRetry(input, init = {}) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
retryInterval,
|
|
299
|
-
validateResponse
|
|
300
|
-
});
|
|
301
|
-
} else {
|
|
302
|
-
throw e;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
299
|
+
const { retries = DEFAULT_RETRIES, retryInterval = DEFAULT_RETRY_INTERVAL$2, validateResponse, ...options } = init;
|
|
300
|
+
try {
|
|
301
|
+
const response = await fetch(input, options);
|
|
302
|
+
validateResponse?.(response);
|
|
303
|
+
return response;
|
|
304
|
+
} catch (e) {
|
|
305
|
+
if (retries > 0) {
|
|
306
|
+
await sleep(retryInterval);
|
|
307
|
+
return fetchRetry(input, {
|
|
308
|
+
...options,
|
|
309
|
+
retries: retries - 1,
|
|
310
|
+
retryInterval,
|
|
311
|
+
validateResponse
|
|
312
|
+
});
|
|
313
|
+
} else throw e;
|
|
314
|
+
}
|
|
305
315
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
import { chains, getChain } from "@gearbox-protocol/sdk";
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/providers.ts
|
|
309
318
|
function getAlchemyUrl(network, apiKey, protocol = "http") {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
return `${protocol}s://${alchemyDomain}.g.alchemy.com/v2/${apiKey}`;
|
|
319
|
+
const alchemyDomain = ALCHEMY_DOMAINS[network];
|
|
320
|
+
if (!alchemyDomain) return;
|
|
321
|
+
return `${protocol}s://${alchemyDomain}.g.alchemy.com/v2/${apiKey}`;
|
|
315
322
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
323
|
+
const DRPC_NETS = {
|
|
324
|
+
Arbitrum: "arbitrum",
|
|
325
|
+
Base: "base",
|
|
326
|
+
BNB: "bsc",
|
|
327
|
+
Mainnet: "ethereum",
|
|
328
|
+
Optimism: "optimism",
|
|
329
|
+
Sonic: "sonic",
|
|
330
|
+
WorldChain: "worldchain",
|
|
331
|
+
Berachain: "berachain",
|
|
332
|
+
Avalanche: "avalanche",
|
|
333
|
+
Monad: "monad",
|
|
334
|
+
Hemi: "hemi",
|
|
335
|
+
Lisk: "lisk",
|
|
336
|
+
MegaETH: null,
|
|
337
|
+
Etherlink: null,
|
|
338
|
+
Plasma: "plasma",
|
|
339
|
+
Somnia: null
|
|
333
340
|
};
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
341
|
+
const ALCHEMY_DOMAINS = {
|
|
342
|
+
Mainnet: "eth-mainnet",
|
|
343
|
+
Arbitrum: "arb-mainnet",
|
|
344
|
+
Optimism: "opt-mainnet",
|
|
345
|
+
Base: "base-mainnet",
|
|
346
|
+
Sonic: "sonic-mainnet",
|
|
347
|
+
Monad: "monad-mainnet",
|
|
348
|
+
Berachain: "berachain-mainnet",
|
|
349
|
+
Avalanche: "avax-mainnet",
|
|
350
|
+
BNB: "bnb-mainnet",
|
|
351
|
+
WorldChain: "worldchain-mainnet",
|
|
352
|
+
MegaETH: null,
|
|
353
|
+
Etherlink: null,
|
|
354
|
+
Hemi: null,
|
|
355
|
+
Lisk: null,
|
|
356
|
+
Plasma: "plasma-mainnet",
|
|
357
|
+
Somnia: null
|
|
351
358
|
};
|
|
352
359
|
function getDrpcUrl(network, apiKey, protocol = "http") {
|
|
353
|
-
|
|
354
|
-
|
|
360
|
+
const net = DRPC_NETS[network];
|
|
361
|
+
return net ? `${protocol}s://lb.drpc.org/${net}/${apiKey}` : void 0;
|
|
355
362
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
363
|
+
const ANKR_DOMAINS = {
|
|
364
|
+
Arbitrum: "arbitrum",
|
|
365
|
+
Avalanche: "avalanche",
|
|
366
|
+
Base: "base",
|
|
367
|
+
Berachain: null,
|
|
368
|
+
BNB: "bsc",
|
|
369
|
+
Etherlink: "etherlink_mainnet",
|
|
370
|
+
Hemi: null,
|
|
371
|
+
Lisk: null,
|
|
372
|
+
Mainnet: "eth",
|
|
373
|
+
MegaETH: null,
|
|
374
|
+
Monad: null,
|
|
375
|
+
Optimism: "optimism",
|
|
376
|
+
Plasma: null,
|
|
377
|
+
Sonic: "sonic_mainnet",
|
|
378
|
+
WorldChain: null,
|
|
379
|
+
Somnia: "somnia_mainnet"
|
|
373
380
|
};
|
|
374
381
|
function getAnkrUrl(network, apiKey, protocol = "http") {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return net ? `${protocol}s://rpc.ankr.com/${net}${sep}${apiKey}` : void 0;
|
|
382
|
+
const net = ANKR_DOMAINS[network];
|
|
383
|
+
return net ? `${protocol}s://rpc.ankr.com/${net}${protocol === "ws" ? "/ws/" : "/"}${apiKey}` : void 0;
|
|
378
384
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
385
|
+
const THIRDWEB_DOMAINS = {
|
|
386
|
+
Arbitrum: chains.Arbitrum.id.toString(),
|
|
387
|
+
Avalanche: chains.Avalanche.id.toString(),
|
|
388
|
+
Base: chains.Base.id.toString(),
|
|
389
|
+
Berachain: chains.Berachain.id.toString(),
|
|
390
|
+
BNB: chains.BNB.id.toString(),
|
|
391
|
+
Etherlink: chains.Etherlink.id.toString(),
|
|
392
|
+
Hemi: chains.Hemi.id.toString(),
|
|
393
|
+
Lisk: chains.Lisk.id.toString(),
|
|
394
|
+
Mainnet: chains.Mainnet.id.toString(),
|
|
395
|
+
MegaETH: chains.MegaETH.id.toString(),
|
|
396
|
+
Monad: chains.Monad.id.toString(),
|
|
397
|
+
Optimism: chains.Optimism.id.toString(),
|
|
398
|
+
Plasma: chains.Plasma.id.toString(),
|
|
399
|
+
Sonic: chains.Sonic.id.toString(),
|
|
400
|
+
WorldChain: chains.WorldChain.id.toString(),
|
|
401
|
+
Somnia: chains.Somnia.id.toString()
|
|
396
402
|
};
|
|
397
403
|
function getThirdWebUrl(network, apiKey, protocol = "http") {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
const net = THIRDWEB_DOMAINS[network];
|
|
402
|
-
return `https://${net}.rpc.thirdweb.com/${apiKey}`;
|
|
404
|
+
if (protocol === "ws") return;
|
|
405
|
+
return `https://${THIRDWEB_DOMAINS[network]}.rpc.thirdweb.com/${apiKey}`;
|
|
403
406
|
}
|
|
404
407
|
function getErpcKey(network, projectId, urlBase) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
const chain = getChain(network);
|
|
409
|
-
return `${urlBase}/${projectId}/evm/${chain.id}`;
|
|
408
|
+
if (!projectId || !urlBase) return;
|
|
409
|
+
return `${urlBase}/${projectId}/evm/${getChain(network).id}`;
|
|
410
410
|
}
|
|
411
|
-
|
|
412
|
-
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/getProviders.ts
|
|
413
413
|
function getProviders(config, logger) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
};
|
|
494
|
-
})
|
|
495
|
-
);
|
|
496
|
-
break;
|
|
497
|
-
case "erpc":
|
|
498
|
-
if (erpcProjectId && erpcUrl) {
|
|
499
|
-
result.push({
|
|
500
|
-
name: "erpc",
|
|
501
|
-
url: getErpcKey(network, erpcProjectId, erpcUrl)
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (logger) {
|
|
508
|
-
const providers = result.filter((p) => !!p.url).map((p) => ({
|
|
509
|
-
name: p.name,
|
|
510
|
-
url: new CensoredURL(p.url)
|
|
511
|
-
}));
|
|
512
|
-
logger.debug({ providers, network }, "gathered providers");
|
|
513
|
-
}
|
|
514
|
-
return result.map((p) => ({
|
|
515
|
-
...p,
|
|
516
|
-
httpClientOptions: {
|
|
517
|
-
...p.httpTransportOptions,
|
|
518
|
-
methods: { exclude: ["eth_fillTransaction"] }
|
|
519
|
-
}
|
|
520
|
-
})).filter((p) => !!p.url);
|
|
414
|
+
const { network, enabledProviders = [
|
|
415
|
+
"erpc",
|
|
416
|
+
"custom",
|
|
417
|
+
"drpc",
|
|
418
|
+
"alchemy",
|
|
419
|
+
"ankr",
|
|
420
|
+
"thirdweb"
|
|
421
|
+
], alchemyKeys = [], ankrKeys = [], drpcKeys = [], thirdwebKeys = [], jsonRpcProviders = [], erpcProjectId, erpcUrl } = config;
|
|
422
|
+
const result = [];
|
|
423
|
+
for (const p of enabledProviders) switch (p) {
|
|
424
|
+
case "alchemy":
|
|
425
|
+
result.push(...alchemyKeys.map((k) => ({
|
|
426
|
+
name: "alchemy",
|
|
427
|
+
url: getAlchemyUrl(network, k.value)
|
|
428
|
+
})));
|
|
429
|
+
break;
|
|
430
|
+
case "drpc":
|
|
431
|
+
result.push(...drpcKeys.map((k) => ({
|
|
432
|
+
name: "drpc",
|
|
433
|
+
url: getDrpcUrl(network, k.value)
|
|
434
|
+
})));
|
|
435
|
+
break;
|
|
436
|
+
case "ankr":
|
|
437
|
+
result.push(...ankrKeys.map((k) => ({
|
|
438
|
+
name: "ankr",
|
|
439
|
+
url: getAnkrUrl(network, k.value)
|
|
440
|
+
})));
|
|
441
|
+
break;
|
|
442
|
+
case "thirdweb":
|
|
443
|
+
result.push(...thirdwebKeys.map((k) => ({
|
|
444
|
+
name: "thirdweb",
|
|
445
|
+
url: getThirdWebUrl(network, k.value)
|
|
446
|
+
})));
|
|
447
|
+
break;
|
|
448
|
+
case "custom":
|
|
449
|
+
result.push(...jsonRpcProviders.map((url) => {
|
|
450
|
+
const urlObj = new URL(url.value);
|
|
451
|
+
const hasCredentials = urlObj.username || urlObj.password;
|
|
452
|
+
let cleanUrl = url.value;
|
|
453
|
+
let httpTransportOptions = {};
|
|
454
|
+
if (hasCredentials) {
|
|
455
|
+
const cleanUrlObj = new URL(url.value);
|
|
456
|
+
cleanUrlObj.username = "";
|
|
457
|
+
cleanUrlObj.password = "";
|
|
458
|
+
cleanUrl = cleanUrlObj.toString();
|
|
459
|
+
const credentials = `${urlObj.username}:${urlObj.password}`;
|
|
460
|
+
httpTransportOptions = { fetchOptions: { headers: { Authorization: `Basic ${Buffer.from(credentials).toString("base64")}` } } };
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
name: getTopLevelDomain(urlObj),
|
|
464
|
+
url: cleanUrl,
|
|
465
|
+
httpTransportOptions
|
|
466
|
+
};
|
|
467
|
+
}));
|
|
468
|
+
break;
|
|
469
|
+
case "erpc":
|
|
470
|
+
if (erpcProjectId && erpcUrl) result.push({
|
|
471
|
+
name: "erpc",
|
|
472
|
+
url: getErpcKey(network, erpcProjectId, erpcUrl)
|
|
473
|
+
});
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
if (logger) {
|
|
477
|
+
const providers = result.filter((p) => !!p.url).map((p) => ({
|
|
478
|
+
name: p.name,
|
|
479
|
+
url: new CensoredURL(p.url)
|
|
480
|
+
}));
|
|
481
|
+
logger.debug({
|
|
482
|
+
providers,
|
|
483
|
+
network
|
|
484
|
+
}, "gathered providers");
|
|
485
|
+
}
|
|
486
|
+
return result.map((p) => ({
|
|
487
|
+
...p,
|
|
488
|
+
httpClientOptions: {
|
|
489
|
+
...p.httpTransportOptions,
|
|
490
|
+
methods: { exclude: ["eth_fillTransaction"] }
|
|
491
|
+
}
|
|
492
|
+
})).filter((p) => !!p.url);
|
|
521
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* Take the last two parts for top-level domain (e.g., "google.com" from "a.b.c.google.com")
|
|
496
|
+
**/
|
|
522
497
|
function getTopLevelDomain({ hostname }) {
|
|
523
|
-
|
|
524
|
-
|
|
498
|
+
const parts = hostname.split(".");
|
|
499
|
+
return parts.length >= 2 ? parts.slice(-2).join(".") : hostname;
|
|
525
500
|
}
|
|
526
|
-
|
|
527
|
-
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region src/getFirstHttpTransportOptions.ts
|
|
528
503
|
function getFirstHttpTransportOptions(config) {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
config: {
|
|
536
|
-
...provider.httpTransportOptions
|
|
537
|
-
}
|
|
538
|
-
};
|
|
504
|
+
const [provider] = getProviders(config);
|
|
505
|
+
if (!provider) throw new Error("No provider found");
|
|
506
|
+
return {
|
|
507
|
+
url: provider.url,
|
|
508
|
+
config: { ...provider.httpTransportOptions }
|
|
509
|
+
};
|
|
539
510
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
import { prettifyError } from "zod";
|
|
543
|
-
|
|
544
|
-
// src/notifications/SlackNotifier.ts
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region src/notifications/SlackNotifier.ts
|
|
545
513
|
var SlackNotifier = class {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
this.#logger?.debug("Slack message sent successfully");
|
|
574
|
-
} catch (error) {
|
|
575
|
-
this.#logger?.error("Error sending Slack message:", error);
|
|
576
|
-
throw error;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
514
|
+
#config;
|
|
515
|
+
#logger;
|
|
516
|
+
constructor(config, logger) {
|
|
517
|
+
this.#config = config;
|
|
518
|
+
this.#logger = logger;
|
|
519
|
+
}
|
|
520
|
+
notify(message, severity) {
|
|
521
|
+
if (this.#config.alertsOnly && severity === "notification") return;
|
|
522
|
+
const emoji = severity === "alert" ? "🚨" : "ℹ️";
|
|
523
|
+
const messageText = typeof message === "string" ? message : message.plain;
|
|
524
|
+
this.#sendToSlack(`${emoji} Alert: ${messageText}`);
|
|
525
|
+
}
|
|
526
|
+
async #sendToSlack(text) {
|
|
527
|
+
try {
|
|
528
|
+
this.#logger?.debug(`Sending message to Slack: ${text}`);
|
|
529
|
+
const response = await fetch(this.#config.webhookUrl.value, {
|
|
530
|
+
method: "POST",
|
|
531
|
+
headers: { "Content-Type": "application/json" },
|
|
532
|
+
body: JSON.stringify({ text })
|
|
533
|
+
});
|
|
534
|
+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
535
|
+
this.#logger?.debug("Slack message sent successfully");
|
|
536
|
+
} catch (error) {
|
|
537
|
+
this.#logger?.error("Error sending Slack message:", error);
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
579
541
|
};
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
prefix: z.string().optional()
|
|
542
|
+
//#endregion
|
|
543
|
+
//#region src/notifications/schema.ts
|
|
544
|
+
const TelegramConfig = z.object({
|
|
545
|
+
type: z.literal("telegram"),
|
|
546
|
+
token: z.union([z.string(), z.instanceof(CensoredString)]).transform(CensoredString.transform),
|
|
547
|
+
alertsChannel: z.union([z.string().startsWith("-"), z.instanceof(CensoredString)]).transform(CensoredString.transform),
|
|
548
|
+
/**
|
|
549
|
+
* Optional channel to send notifications to
|
|
550
|
+
* If not set, notifications will not be sent
|
|
551
|
+
*/
|
|
552
|
+
notificationsChannel: z.union([z.string().startsWith("-"), z.instanceof(CensoredString)]).transform(CensoredString.transform).optional(),
|
|
553
|
+
prefix: z.string().optional()
|
|
593
554
|
});
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
555
|
+
const SlackConfig = z.object({
|
|
556
|
+
type: z.literal("slack"),
|
|
557
|
+
/**
|
|
558
|
+
* Slack webhook URL for sending notifications
|
|
559
|
+
*/
|
|
560
|
+
webhookUrl: z.union([z.string(), z.instanceof(CensoredString)]).transform(CensoredString.transform),
|
|
561
|
+
/**
|
|
562
|
+
* If true, only alerts will be sent. Informational notifications are suppressed
|
|
563
|
+
*/
|
|
564
|
+
alertsOnly: boolLike().optional()
|
|
604
565
|
});
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
])
|
|
609
|
-
var SignlConfig = z.object({
|
|
610
|
-
apiKey: z.union([z.string(), z.instanceof(CensoredString)]).transform(CensoredString.transform),
|
|
611
|
-
teamId: z.union([z.string(), z.instanceof(CensoredString)]).transform(CensoredString.transform)
|
|
566
|
+
const NotificationConfig = z.discriminatedUnion("type", [TelegramConfig, SlackConfig]);
|
|
567
|
+
const SignlConfig = z.object({
|
|
568
|
+
apiKey: z.union([z.string(), z.instanceof(CensoredString)]).transform(CensoredString.transform),
|
|
569
|
+
teamId: z.union([z.string(), z.instanceof(CensoredString)]).transform(CensoredString.transform)
|
|
612
570
|
});
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
import { md } from "@vlad-yakovlev/telegram-md";
|
|
616
|
-
import Queue from "p-queue";
|
|
617
|
-
import { withRetry } from "viem";
|
|
618
|
-
|
|
619
|
-
// src/notifications/ThrottleManager.ts
|
|
620
|
-
import { LRUCache } from "lru-cache";
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/notifications/ThrottleManager.ts
|
|
621
573
|
function defaultBackoffFn(prevWait) {
|
|
622
|
-
|
|
623
|
-
prevWait ? prevWait * 2 : 10 * 60 * 1e3,
|
|
624
|
-
// 10 minutes
|
|
625
|
-
24 * 60 * 60 * 1e3
|
|
626
|
-
// 24 hours
|
|
627
|
-
);
|
|
574
|
+
return Math.min(prevWait ? prevWait * 2 : 600 * 1e3, 1440 * 60 * 1e3);
|
|
628
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Manages per-feed throttling with exponential backoff.
|
|
578
|
+
* Each dedupe key has its own independent throttle timer.
|
|
579
|
+
*/
|
|
629
580
|
var ThrottleManager = class {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return this.#state.get(dedupeKey) ?? { lastAlertTime: 0, currentWait: 0 };
|
|
658
|
-
}
|
|
581
|
+
#state = new LRUCache({ max: 5e3 });
|
|
582
|
+
/**
|
|
583
|
+
* Checks if it's permitted to send a message for the given dedupe key
|
|
584
|
+
* @returns
|
|
585
|
+
*/
|
|
586
|
+
canSend(dedupeKey) {
|
|
587
|
+
if (!dedupeKey) return true;
|
|
588
|
+
const state = this.#getState(dedupeKey);
|
|
589
|
+
return Date.now() - state.lastAlertTime >= state.currentWait;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Records that notification was sent for the given dedupe key
|
|
593
|
+
*/
|
|
594
|
+
onSent(dedupeKey, throttleFn = defaultBackoffFn) {
|
|
595
|
+
if (!dedupeKey) return;
|
|
596
|
+
const state = this.#getState(dedupeKey);
|
|
597
|
+
this.#state.set(dedupeKey, {
|
|
598
|
+
lastAlertTime: Date.now(),
|
|
599
|
+
currentWait: throttleFn(state.currentWait)
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
#getState(dedupeKey) {
|
|
603
|
+
return this.#state.get(dedupeKey) ?? {
|
|
604
|
+
lastAlertTime: 0,
|
|
605
|
+
currentWait: 0
|
|
606
|
+
};
|
|
607
|
+
}
|
|
659
608
|
};
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/notifications/TelegramNotifier.ts
|
|
611
|
+
const DEFAULT_RETRY_INTERVAL$1 = 1e3;
|
|
612
|
+
const DEFAULT_RETRY_COUNT$1 = 5;
|
|
613
|
+
const DEFAULT_MAX_CHUNK_SIZE = 4096;
|
|
665
614
|
var TelegramNotifier = class {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
return this.#slicePlainText(plain);
|
|
764
|
-
}
|
|
765
|
-
return [{ text: mdText, parse_mode: "MarkdownV2" }];
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* Telegram has a limit on message size (default 4096 characters).
|
|
769
|
-
* This function slices the text into chunks respecting the limit.
|
|
770
|
-
* Each chunk is prefixed with the prefix (if set), ensuring the total length <= maxChunkSize.
|
|
771
|
-
* @param text - The text to slice.
|
|
772
|
-
* @returns An array of texts.
|
|
773
|
-
*/
|
|
774
|
-
#slicePlainText(text) {
|
|
775
|
-
const chunks = [];
|
|
776
|
-
const prefix = this.#prefix ? `${this.#prefix} ` : "";
|
|
777
|
-
const maxChunkSize = this.#maxChunkSize - prefix.length;
|
|
778
|
-
for (let i = 0; i < text.length; i += maxChunkSize) {
|
|
779
|
-
const chunkText = text.slice(i, i + maxChunkSize);
|
|
780
|
-
chunks.push({ text: `${prefix}${chunkText}` });
|
|
781
|
-
}
|
|
782
|
-
return chunks;
|
|
783
|
-
}
|
|
615
|
+
#logger;
|
|
616
|
+
#alertsChannel;
|
|
617
|
+
#notificationsChannel;
|
|
618
|
+
#botToken;
|
|
619
|
+
#prefix;
|
|
620
|
+
#retryInterval;
|
|
621
|
+
#retryCount;
|
|
622
|
+
#maxChunkSize;
|
|
623
|
+
#fetchFn;
|
|
624
|
+
#queue = new Queue({
|
|
625
|
+
concurrency: 1,
|
|
626
|
+
interval: 6e4,
|
|
627
|
+
intervalCap: 20,
|
|
628
|
+
strict: true
|
|
629
|
+
});
|
|
630
|
+
#throttleManager = new ThrottleManager();
|
|
631
|
+
constructor(opts, logger) {
|
|
632
|
+
this.#logger = logger;
|
|
633
|
+
this.#alertsChannel = opts.alertsChannel.value;
|
|
634
|
+
this.#botToken = opts.token.value;
|
|
635
|
+
this.#notificationsChannel = opts.notificationsChannel?.value;
|
|
636
|
+
this.#prefix = opts.prefix;
|
|
637
|
+
this.#retryInterval = opts.retryInterval ?? DEFAULT_RETRY_INTERVAL$1;
|
|
638
|
+
this.#retryCount = opts.retryCount ?? DEFAULT_RETRY_COUNT$1;
|
|
639
|
+
this.#maxChunkSize = opts.maxChunkSize ?? DEFAULT_MAX_CHUNK_SIZE;
|
|
640
|
+
this.#fetchFn = opts.fetchFn ?? fetch;
|
|
641
|
+
}
|
|
642
|
+
notify(message, severity) {
|
|
643
|
+
const dest = severity === "alert" ? this.#alertsChannel : this.#notificationsChannel;
|
|
644
|
+
if (!dest) return;
|
|
645
|
+
this.#enqueueTelegram(message, dest, severity === "alert");
|
|
646
|
+
}
|
|
647
|
+
async #enqueueTelegram(message, channelId, severe = false) {
|
|
648
|
+
if (!this.#throttleManager.canSend(message.dedupeKey)) return;
|
|
649
|
+
const payloads = this.#toTelegramPayloads(message);
|
|
650
|
+
for (const payload of payloads) this.#queue.add(() => this.#sendToTelegram(payload, channelId, severe).catch((e) => this.#logger?.error(e)), {
|
|
651
|
+
priority: severe ? 100 : 0,
|
|
652
|
+
timeout: this.#retryInterval * this.#retryCount * 2
|
|
653
|
+
});
|
|
654
|
+
this.#throttleManager.onSent(message.dedupeKey, message.throttleFn);
|
|
655
|
+
}
|
|
656
|
+
async #sendToTelegram(payload, channelId, severe = false) {
|
|
657
|
+
const severity = severe ? "alert" : "notification";
|
|
658
|
+
this.#logger?.debug(`sending telegram ${severity} to channel ${channelId}...`);
|
|
659
|
+
const url = `https://api.telegram.org/bot${this.#botToken}/sendMessage`;
|
|
660
|
+
try {
|
|
661
|
+
await withRetry(async () => {
|
|
662
|
+
const resp = await this.#fetchFn(url, {
|
|
663
|
+
method: "POST",
|
|
664
|
+
headers: { "Content-Type": "application/json" },
|
|
665
|
+
body: JSON.stringify({
|
|
666
|
+
...payload,
|
|
667
|
+
chat_id: channelId
|
|
668
|
+
})
|
|
669
|
+
});
|
|
670
|
+
if (resp.ok) this.#logger?.debug(`telegram ${severity} sent successfully`);
|
|
671
|
+
else {
|
|
672
|
+
const txt = await resp.text();
|
|
673
|
+
throw new Error(`api error ${resp.status}: ${txt}`);
|
|
674
|
+
}
|
|
675
|
+
}, {
|
|
676
|
+
delay: this.#retryInterval,
|
|
677
|
+
retryCount: this.#retryCount
|
|
678
|
+
});
|
|
679
|
+
} catch (e) {
|
|
680
|
+
this.#logger?.error(`cannot send telegram ${severity}: ${e}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
#toTelegramPayloads(message) {
|
|
684
|
+
const { md: mdMessage, plain } = message;
|
|
685
|
+
if (!mdMessage) return this.#slicePlainText(plain);
|
|
686
|
+
let text = mdMessage;
|
|
687
|
+
if (this.#prefix) text = md.join([md.bold(this.#prefix), text], " ");
|
|
688
|
+
const mdText = text.toString();
|
|
689
|
+
if (mdText.length > this.#maxChunkSize) return this.#slicePlainText(plain);
|
|
690
|
+
return [{
|
|
691
|
+
text: mdText,
|
|
692
|
+
parse_mode: "MarkdownV2"
|
|
693
|
+
}];
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Telegram has a limit on message size (default 4096 characters).
|
|
697
|
+
* This function slices the text into chunks respecting the limit.
|
|
698
|
+
* Each chunk is prefixed with the prefix (if set), ensuring the total length <= maxChunkSize.
|
|
699
|
+
* @param text - The text to slice.
|
|
700
|
+
* @returns An array of texts.
|
|
701
|
+
*/
|
|
702
|
+
#slicePlainText(text) {
|
|
703
|
+
const chunks = [];
|
|
704
|
+
const prefix = this.#prefix ? `${this.#prefix} ` : "";
|
|
705
|
+
const maxChunkSize = this.#maxChunkSize - prefix.length;
|
|
706
|
+
for (let i = 0; i < text.length; i += maxChunkSize) {
|
|
707
|
+
const chunkText = text.slice(i, i + maxChunkSize);
|
|
708
|
+
chunks.push({ text: `${prefix}${chunkText}` });
|
|
709
|
+
}
|
|
710
|
+
return chunks;
|
|
711
|
+
}
|
|
784
712
|
};
|
|
785
|
-
|
|
786
|
-
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region src/notifications/createNotifier.ts
|
|
787
715
|
function createNotifier(config, logger) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
return new SlackNotifier(parsed.data, logger);
|
|
799
|
-
}
|
|
716
|
+
const parsed = NotificationConfig.safeParse(config);
|
|
717
|
+
if (!parsed.success) {
|
|
718
|
+
console.log(">>>>", parsed.error);
|
|
719
|
+
logger?.warn(`invalid notification config: ${prettifyError(parsed.error)}`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
switch (parsed.data.type) {
|
|
723
|
+
case "telegram": return new TelegramNotifier(parsed.data, logger);
|
|
724
|
+
case "slack": return new SlackNotifier(parsed.data, logger);
|
|
725
|
+
}
|
|
800
726
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
// src/notifications/SignlNotifier.ts
|
|
806
|
-
import { withRetry as withRetry2 } from "viem";
|
|
807
|
-
var DEFAULT_RETRY_INTERVAL3 = 1e3;
|
|
808
|
-
var DEFAULT_RETRY_COUNT2 = 5;
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/notifications/SignlNotifier.ts
|
|
729
|
+
const DEFAULT_RETRY_INTERVAL = 1e3;
|
|
730
|
+
const DEFAULT_RETRY_COUNT = 5;
|
|
809
731
|
var SignlNotifier = class {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
this.#logger?.info(`signl alert response: ${JSON.stringify(data)}`);
|
|
857
|
-
} else {
|
|
858
|
-
throw new Error(`signl api error ${resp.status}: ${resp.statusText}`);
|
|
859
|
-
}
|
|
860
|
-
},
|
|
861
|
-
{ delay: this.#retryInterval, retryCount: this.#retryCount }
|
|
862
|
-
);
|
|
863
|
-
if (dedupeKey) {
|
|
864
|
-
this.#throttleManager.onSent(dedupeKey, this.#throttleFn);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
732
|
+
#logger;
|
|
733
|
+
#apiKey;
|
|
734
|
+
#teamId;
|
|
735
|
+
#retryInterval;
|
|
736
|
+
#retryCount;
|
|
737
|
+
#throttleManager = new ThrottleManager();
|
|
738
|
+
#throttleFn;
|
|
739
|
+
constructor(opts, logger) {
|
|
740
|
+
this.#apiKey = opts.apiKey;
|
|
741
|
+
this.#teamId = opts.teamId;
|
|
742
|
+
this.#retryInterval = opts.retryInterval ?? DEFAULT_RETRY_INTERVAL;
|
|
743
|
+
this.#retryCount = opts.retryCount ?? DEFAULT_RETRY_COUNT;
|
|
744
|
+
this.#logger = logger;
|
|
745
|
+
this.#throttleFn = opts.throttleFn;
|
|
746
|
+
this.#logger?.info(opts, "signl notifier configured");
|
|
747
|
+
}
|
|
748
|
+
alert(message, dedupeKey) {
|
|
749
|
+
this.#sendAlert(message, dedupeKey).catch((e) => this.#logger?.error(e));
|
|
750
|
+
}
|
|
751
|
+
async #sendAlert(message, dedupeKey) {
|
|
752
|
+
if (dedupeKey && !this.#throttleManager.canSend(dedupeKey)) return;
|
|
753
|
+
const text = message.slice(0, 64);
|
|
754
|
+
this.#logger?.debug("sending signl alert");
|
|
755
|
+
await withRetry(async () => {
|
|
756
|
+
const resp = await fetch(`https://connect.signl4.com/api/v2/alerts?x-s4-api-key=${this.#apiKey.value}`, {
|
|
757
|
+
method: "POST",
|
|
758
|
+
headers: { "Content-Type": "application/json" },
|
|
759
|
+
body: JSON.stringify({
|
|
760
|
+
category: "emergency",
|
|
761
|
+
severity: 0,
|
|
762
|
+
teamId: this.#teamId.value,
|
|
763
|
+
text,
|
|
764
|
+
title: "GEARBOX ALERT"
|
|
765
|
+
})
|
|
766
|
+
});
|
|
767
|
+
if (resp.ok) {
|
|
768
|
+
this.#logger?.warn("signl alert sent");
|
|
769
|
+
const data = await resp.json();
|
|
770
|
+
this.#logger?.info(`signl alert response: ${JSON.stringify(data)}`);
|
|
771
|
+
} else throw new Error(`signl api error ${resp.status}: ${resp.statusText}`);
|
|
772
|
+
}, {
|
|
773
|
+
delay: this.#retryInterval,
|
|
774
|
+
retryCount: this.#retryCount
|
|
775
|
+
});
|
|
776
|
+
if (dedupeKey) this.#throttleManager.onSent(dedupeKey, this.#throttleFn);
|
|
777
|
+
}
|
|
867
778
|
};
|
|
868
|
-
|
|
869
|
-
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/notifications/NotificationsService.ts
|
|
870
781
|
var NotificationsService = class {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
this.#logger?.debug(notification.plain);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
#toNotification(message) {
|
|
925
|
-
if (typeof message === "string" || !("messageFor" in message)) {
|
|
926
|
-
return {
|
|
927
|
-
messageFor: (recipient) => recipient ? void 0 : message
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
return message;
|
|
931
|
-
}
|
|
932
|
-
#toDedupableNotification(msg) {
|
|
933
|
-
if (typeof msg === "string") {
|
|
934
|
-
return {
|
|
935
|
-
plain: msg
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
return msg;
|
|
939
|
-
}
|
|
782
|
+
#logger;
|
|
783
|
+
#notifiers = new AddressMap();
|
|
784
|
+
#signlNotifier;
|
|
785
|
+
constructor(options, logger) {
|
|
786
|
+
this.#logger = logger;
|
|
787
|
+
for (const n of options.notifications) {
|
|
788
|
+
const recipient = n.recipient ?? ADDRESS_0X0;
|
|
789
|
+
const notifier = createNotifier(n, logger);
|
|
790
|
+
if (notifier) this.#notifiers.upsert(recipient, notifier);
|
|
791
|
+
}
|
|
792
|
+
if (options.signl) this.#signlNotifier = new SignlNotifier(options.signl);
|
|
793
|
+
else this.#logger?.warn("signl notifier not configured");
|
|
794
|
+
}
|
|
795
|
+
registerRecipient(address, options) {
|
|
796
|
+
const notifier = createNotifier(options, this.#logger);
|
|
797
|
+
if (notifier) this.#notifiers.upsert(address, notifier);
|
|
798
|
+
}
|
|
799
|
+
signl(message, dedupeKey) {
|
|
800
|
+
this.#signlNotifier?.alert(message, dedupeKey);
|
|
801
|
+
}
|
|
802
|
+
alert(msg) {
|
|
803
|
+
this.#notify(msg, "alert");
|
|
804
|
+
}
|
|
805
|
+
notify(msg) {
|
|
806
|
+
this.#notify(msg, "notification");
|
|
807
|
+
}
|
|
808
|
+
#notify(msg, severity) {
|
|
809
|
+
const message = this.#toNotification(msg);
|
|
810
|
+
for (const [addr, notifier] of this.#notifiers.entries()) {
|
|
811
|
+
const recipient = addr === ADDRESS_0X0 ? void 0 : addr;
|
|
812
|
+
const recipientNotification = message.messageFor(recipient);
|
|
813
|
+
if (recipientNotification) {
|
|
814
|
+
const notification = this.#toDedupableNotification(recipientNotification);
|
|
815
|
+
notifier.notify(notification, severity);
|
|
816
|
+
if (addr === ADDRESS_0X0) if (severity === "alert") this.#logger?.warn(notification.plain);
|
|
817
|
+
else this.#logger?.debug(notification.plain);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
#toNotification(message) {
|
|
822
|
+
if (typeof message === "string" || !("messageFor" in message)) return { messageFor: (recipient) => recipient ? void 0 : message };
|
|
823
|
+
return message;
|
|
824
|
+
}
|
|
825
|
+
#toDedupableNotification(msg) {
|
|
826
|
+
if (typeof msg === "string") return { plain: msg };
|
|
827
|
+
return msg;
|
|
828
|
+
}
|
|
940
829
|
};
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
import { z as z2 } from "zod/v4";
|
|
945
|
-
|
|
946
|
-
// src/Zommand.ts
|
|
947
|
-
import { Command, Option } from "@commander-js/extra-typings";
|
|
948
|
-
import * as z4 from "zod/v4/core";
|
|
949
|
-
var zommandRegistry = z4.registry();
|
|
830
|
+
//#endregion
|
|
831
|
+
//#region src/Zommand.ts
|
|
832
|
+
const zommandRegistry = z4.registry();
|
|
950
833
|
var Zommand = class extends Command {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
let fromYaml = {};
|
|
1016
|
-
if (config) {
|
|
1017
|
-
fromYaml = await resolveYamlFiles(config, {
|
|
1018
|
-
...process.env,
|
|
1019
|
-
...this.#templatedData
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
let fromInlineConfig = {};
|
|
1023
|
-
if (inlineConfig) {
|
|
1024
|
-
fromInlineConfig = await decompressObject(
|
|
1025
|
-
inlineConfig,
|
|
1026
|
-
this.#templatedData
|
|
1027
|
-
);
|
|
1028
|
-
} else if (process.env.INLINE_CONFIG) {
|
|
1029
|
-
fromInlineConfig = await decompressObject(
|
|
1030
|
-
process.env.INLINE_CONFIG,
|
|
1031
|
-
this.#templatedData
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
|
-
const schemaFromEnv = {};
|
|
1035
|
-
for (const [env, key] of Object.entries(this.#envToObjectKeys)) {
|
|
1036
|
-
if (process.env[env]) {
|
|
1037
|
-
schemaFromEnv[key] = process.env[env];
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
const combined = {
|
|
1041
|
-
...schemaFromEnv,
|
|
1042
|
-
...fromYaml,
|
|
1043
|
-
...fromInlineConfig,
|
|
1044
|
-
...opts
|
|
1045
|
-
};
|
|
1046
|
-
const data = z4.parse(this.#schema, combined);
|
|
1047
|
-
await fn.apply(this, [...argz, data, this]);
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
834
|
+
#schema;
|
|
835
|
+
#templatedData;
|
|
836
|
+
#envToObjectKeys = {};
|
|
837
|
+
constructor(name, opts) {
|
|
838
|
+
super(name);
|
|
839
|
+
const { schema, templateData, configFile = true, inlineConfig = true } = opts;
|
|
840
|
+
this.#schema = schema;
|
|
841
|
+
this.#templatedData = templateData;
|
|
842
|
+
const def = this.#schema._zod.def;
|
|
843
|
+
if (def.type === "object") this.#addOptionsFromObject(def);
|
|
844
|
+
else if (def.type === "union") for (const o of def.options) {
|
|
845
|
+
const opt = o;
|
|
846
|
+
if (opt._zod.def.type === "object") this.#addOptionsFromObject(opt._zod.def);
|
|
847
|
+
}
|
|
848
|
+
else throw new Error(`expected object schema, got ${this.#schema._zod.def.type}`);
|
|
849
|
+
if (configFile) {
|
|
850
|
+
let opt = new Option("--config [file]", "config file (or s3 link");
|
|
851
|
+
if (typeof configFile === "string") opt = opt.default(configFile);
|
|
852
|
+
this.addOption(opt);
|
|
853
|
+
}
|
|
854
|
+
if (inlineConfig) this.addOption(new Option("--inline-config [config]", "inline config"));
|
|
855
|
+
}
|
|
856
|
+
#addOptionsFromObject(def) {
|
|
857
|
+
const shape = def.shape;
|
|
858
|
+
for (const [key, optionSchema] of Object.entries(shape)) {
|
|
859
|
+
const meta = zommandRegistry.get(optionSchema);
|
|
860
|
+
if (!meta) continue;
|
|
861
|
+
if (!this.options.find((o) => o.flags === meta.flags)) {
|
|
862
|
+
let description = meta.description;
|
|
863
|
+
if (meta.env) description = `${description} (env variable ${meta.env})`;
|
|
864
|
+
this.addOption(new Option(meta.flags, description));
|
|
865
|
+
if (meta.env) this.#envToObjectKeys[meta.env] = key;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
action(fn) {
|
|
870
|
+
return super.action(async (...args) => {
|
|
871
|
+
const argz = args;
|
|
872
|
+
argz.splice(-1);
|
|
873
|
+
const [{ config, inlineConfig, ...opts }] = argz.splice(-1);
|
|
874
|
+
let fromYaml = {};
|
|
875
|
+
if (config) fromYaml = await resolveYamlFiles(config, {
|
|
876
|
+
...process.env,
|
|
877
|
+
...this.#templatedData
|
|
878
|
+
});
|
|
879
|
+
let fromInlineConfig = {};
|
|
880
|
+
if (inlineConfig) fromInlineConfig = await decompressObject(inlineConfig, this.#templatedData);
|
|
881
|
+
else if (process.env.INLINE_CONFIG) fromInlineConfig = await decompressObject(process.env.INLINE_CONFIG, this.#templatedData);
|
|
882
|
+
const schemaFromEnv = {};
|
|
883
|
+
for (const [env, key] of Object.entries(this.#envToObjectKeys)) if (process.env[env]) schemaFromEnv[key] = process.env[env];
|
|
884
|
+
const combined = {
|
|
885
|
+
...schemaFromEnv,
|
|
886
|
+
...fromYaml,
|
|
887
|
+
...fromInlineConfig,
|
|
888
|
+
...opts
|
|
889
|
+
};
|
|
890
|
+
const data = z4.parse(this.#schema, combined);
|
|
891
|
+
await fn.apply(this, [
|
|
892
|
+
...argz,
|
|
893
|
+
data,
|
|
894
|
+
this
|
|
895
|
+
]);
|
|
896
|
+
});
|
|
897
|
+
}
|
|
1050
898
|
};
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region src/providers-schema.ts
|
|
901
|
+
const RpcProvider = z.enum([
|
|
902
|
+
"alchemy",
|
|
903
|
+
"drpc",
|
|
904
|
+
"ankr",
|
|
905
|
+
"thirdweb",
|
|
906
|
+
"custom",
|
|
907
|
+
"erpc"
|
|
1060
908
|
]);
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
909
|
+
const ProvidersSchema = z.object({
|
|
910
|
+
network: z.enum(SUPPORTED_NETWORKS).register(zommandRegistry, {
|
|
911
|
+
flags: "--network <network>",
|
|
912
|
+
description: "Gearbox-supported network",
|
|
913
|
+
env: "NETWORK"
|
|
914
|
+
}),
|
|
915
|
+
/**
|
|
916
|
+
* RPC providers to use
|
|
917
|
+
*/
|
|
918
|
+
jsonRpcProviders: stringArrayLike().pipe(z.array(z.url().transform(CensoredURL.transform))).transform((a) => a.length ? a : void 0).optional().register(zommandRegistry, {
|
|
919
|
+
flags: "--json-rpc-providers <urls...>",
|
|
920
|
+
description: "RPC providers to use, comma separated full http urls.",
|
|
921
|
+
env: "JSON_RPC_PROVIDERS"
|
|
922
|
+
}),
|
|
923
|
+
/**
|
|
924
|
+
* RPC providers to use with their keys
|
|
925
|
+
* Order matters
|
|
926
|
+
*/
|
|
927
|
+
enabledProviders: stringArrayLike().pipe(z.array(RpcProvider)).optional().register(zommandRegistry, {
|
|
928
|
+
flags: "--enabled-providers <providers...>",
|
|
929
|
+
description: "Keyed RPC providers to use, comma separated, order matters. Possible values: custom (for full json rpc urls)/alchemy/drpc/ankr/thirdweb",
|
|
930
|
+
env: "ENABLED_PROVIDERS"
|
|
931
|
+
}),
|
|
932
|
+
/**
|
|
933
|
+
* Alchemy API keys to use
|
|
934
|
+
*/
|
|
935
|
+
alchemyKeys: stringArrayLike().pipe(z.array(z.string().transform(CensoredString.transform))).transform((a) => a.length ? a : void 0).optional().register(zommandRegistry, {
|
|
936
|
+
flags: "--alchemy-keys <keys...>",
|
|
937
|
+
description: "Alchemy API keys to use, comma separated",
|
|
938
|
+
env: "ALCHEMY_KEYS"
|
|
939
|
+
}),
|
|
940
|
+
/**
|
|
941
|
+
* DRPC API keys to use
|
|
942
|
+
*/
|
|
943
|
+
drpcKeys: stringArrayLike().pipe(z.array(z.string().transform(CensoredString.transform))).transform((a) => a.length ? a : void 0).optional().register(zommandRegistry, {
|
|
944
|
+
flags: "--drpc-keys <keys...>",
|
|
945
|
+
description: "DRPC API keys to use, comma separated",
|
|
946
|
+
env: "DRPC_KEYS"
|
|
947
|
+
}),
|
|
948
|
+
/**
|
|
949
|
+
* Ankr API keys to use
|
|
950
|
+
*/
|
|
951
|
+
ankrKeys: stringArrayLike().pipe(z.array(z.string().transform(CensoredString.transform))).transform((a) => a.length ? a : void 0).optional().register(zommandRegistry, {
|
|
952
|
+
flags: "--ankr-keys <keys...>",
|
|
953
|
+
description: "Ankr API keys to use, comma separated",
|
|
954
|
+
env: "ANKR_KEYS"
|
|
955
|
+
}),
|
|
956
|
+
/**
|
|
957
|
+
* Thirdweb API keys to use
|
|
958
|
+
*/
|
|
959
|
+
thirdwebKeys: stringArrayLike().pipe(z.array(z.string().transform(CensoredString.transform))).transform((a) => a.length ? a : void 0).optional().register(zommandRegistry, {
|
|
960
|
+
flags: "--thirdweb-keys <keys...>",
|
|
961
|
+
description: "Thirdweb API keys to use, comma separated",
|
|
962
|
+
env: "THIRDWEB_KEYS"
|
|
963
|
+
}),
|
|
964
|
+
/**
|
|
965
|
+
* If set, eRPC will be added as first provider
|
|
966
|
+
*/
|
|
967
|
+
erpcProjectId: z.string().optional().register(zommandRegistry, {
|
|
968
|
+
flags: "--erpc-project-id <project-id>",
|
|
969
|
+
description: "If set, eRPC will be added as first provider",
|
|
970
|
+
env: "ERPC_PROJECT_ID"
|
|
971
|
+
}),
|
|
972
|
+
/**
|
|
973
|
+
* eRPC URL to use
|
|
974
|
+
*/
|
|
975
|
+
erpcUrl: z.url().default("http://erpc.local").register(zommandRegistry, {
|
|
976
|
+
flags: "--erpc-url <url>",
|
|
977
|
+
description: "eRPC URL to use",
|
|
978
|
+
env: "ERPC_URL"
|
|
979
|
+
}),
|
|
980
|
+
/**
|
|
981
|
+
* Timeout for JSON RPC requests, in milliseconds
|
|
982
|
+
*/
|
|
983
|
+
jsonRpcTimeout: z.coerce.number().int().positive().optional().register(zommandRegistry, {
|
|
984
|
+
flags: "--json-rpc-timeout <timeout>",
|
|
985
|
+
description: "Timeout for JSON RPC requests, in milliseconds",
|
|
986
|
+
env: "JSON_RPC_TIMEOUT"
|
|
987
|
+
})
|
|
1140
988
|
});
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
import {
|
|
1144
|
-
GetSecretValueCommand,
|
|
1145
|
-
SecretsManagerClient
|
|
1146
|
-
} from "@aws-sdk/client-secrets-manager";
|
|
989
|
+
//#endregion
|
|
990
|
+
//#region src/SecretsManager.ts
|
|
1147
991
|
var SecretsManager = class {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
return this.#secrets.get(SecretId);
|
|
1176
|
-
}
|
|
1177
|
-
if (!this.#smClient) {
|
|
1178
|
-
this.#smClient = new SecretsManagerClient({});
|
|
1179
|
-
}
|
|
1180
|
-
const { SecretString } = await this.#smClient.send(
|
|
1181
|
-
new GetSecretValueCommand({ SecretId })
|
|
1182
|
-
);
|
|
1183
|
-
if (!SecretString) {
|
|
1184
|
-
throw new Error(`secret '${SecretId}' not found`);
|
|
1185
|
-
}
|
|
1186
|
-
this.#secrets.set(SecretId, SecretString);
|
|
1187
|
-
return SecretString;
|
|
1188
|
-
}
|
|
992
|
+
#smClient;
|
|
993
|
+
#secrets = /* @__PURE__ */ new Map();
|
|
994
|
+
async secret(key) {
|
|
995
|
+
const [SecretId, ...jsonPath] = key.split(".");
|
|
996
|
+
const secretStr = await this.#getSecret(SecretId);
|
|
997
|
+
if (!jsonPath.length) return secretStr;
|
|
998
|
+
let v = JSON.parse(secretStr);
|
|
999
|
+
for (const p of jsonPath) v = v[p];
|
|
1000
|
+
if (!v || typeof v !== "string") throw new Error(`cannot find secret '${key}'`);
|
|
1001
|
+
return v;
|
|
1002
|
+
}
|
|
1003
|
+
async secretRaw(key) {
|
|
1004
|
+
const val = await this.#getSecret(key);
|
|
1005
|
+
try {
|
|
1006
|
+
return JSON.parse(val);
|
|
1007
|
+
} catch {
|
|
1008
|
+
return val;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async #getSecret(SecretId) {
|
|
1012
|
+
if (this.#secrets.has(SecretId)) return this.#secrets.get(SecretId);
|
|
1013
|
+
if (!this.#smClient) this.#smClient = new SecretsManagerClient({});
|
|
1014
|
+
const { SecretString } = await this.#smClient.send(new GetSecretValueCommand({ SecretId }));
|
|
1015
|
+
if (!SecretString) throw new Error(`secret '${SecretId}' not found`);
|
|
1016
|
+
this.#secrets.set(SecretId, SecretString);
|
|
1017
|
+
return SecretString;
|
|
1018
|
+
}
|
|
1189
1019
|
};
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1020
|
+
const secretsManagerProxy = (mgr) => new Proxy(mgr ?? new SecretsManager(), { get(target, prop) {
|
|
1021
|
+
if (typeof prop === "string" && ![
|
|
1022
|
+
"secret",
|
|
1023
|
+
"secretRaw",
|
|
1024
|
+
"then",
|
|
1025
|
+
"catch",
|
|
1026
|
+
"finally"
|
|
1027
|
+
].includes(prop)) return target.secretRaw(prop);
|
|
1028
|
+
} });
|
|
1029
|
+
//#endregion
|
|
1030
|
+
//#region src/SSMManager.ts
|
|
1200
1031
|
var SSMManager = class {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
new GetParameterCommand({
|
|
1212
|
-
Name: name
|
|
1213
|
-
})
|
|
1214
|
-
);
|
|
1215
|
-
const value = response.Parameter?.Value;
|
|
1216
|
-
if (!value) {
|
|
1217
|
-
throw new Error(`parameter ${name} value not found`);
|
|
1218
|
-
}
|
|
1219
|
-
this.#parameters.set(name, value);
|
|
1220
|
-
return value;
|
|
1221
|
-
}
|
|
1032
|
+
#ssmClient;
|
|
1033
|
+
#parameters = /* @__PURE__ */ new Map();
|
|
1034
|
+
async parameter(name) {
|
|
1035
|
+
if (this.#parameters.has(name)) return this.#parameters.get(name);
|
|
1036
|
+
if (!this.#ssmClient) this.#ssmClient = new SSMClient({});
|
|
1037
|
+
const value = (await this.#ssmClient.send(new GetParameterCommand({ Name: name }))).Parameter?.Value;
|
|
1038
|
+
if (!value) throw new Error(`parameter ${name} value not found`);
|
|
1039
|
+
this.#parameters.set(name, value);
|
|
1040
|
+
return value;
|
|
1041
|
+
}
|
|
1222
1042
|
};
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1043
|
+
const ssmManagerProxy = (mgr) => new Proxy(mgr ?? new SSMManager(), { get(target, prop) {
|
|
1044
|
+
if (typeof prop === "string" && ![
|
|
1045
|
+
"parameter",
|
|
1046
|
+
"then",
|
|
1047
|
+
"catch",
|
|
1048
|
+
"finally"
|
|
1049
|
+
].includes(prop)) return target.parameter(prop);
|
|
1050
|
+
} });
|
|
1051
|
+
//#endregion
|
|
1052
|
+
//#region src/uncensorMap.ts
|
|
1232
1053
|
function uncensorMap(map) {
|
|
1233
|
-
|
|
1054
|
+
return Object.fromEntries(Object.entries(map).map(([k, v]) => [k, v.value]));
|
|
1234
1055
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1056
|
+
//#endregion
|
|
1057
|
+
//#region src/Whitelist.ts
|
|
1058
|
+
const REFRESH_INTERVAL_MS = 600 * 1e3;
|
|
1059
|
+
const whitelistResponseSchema = z.array(whitelistItemSchema);
|
|
1060
|
+
/**
|
|
1061
|
+
* Holds two whitelists:
|
|
1062
|
+
* - a `hard` whitelist built once from static config, always available;
|
|
1063
|
+
* - a `soft` whitelist fetched from the backend and kept in
|
|
1064
|
+
* memory with a periodic refresh.
|
|
1065
|
+
*
|
|
1066
|
+
* On soft fetch failure the previous in-memory copy is retained; if no
|
|
1067
|
+
* successful fetch has occurred yet the soft whitelist is considered empty.
|
|
1068
|
+
*/
|
|
1241
1069
|
var Whitelist = class {
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
`loaded ${items.length} soft whitelist entries for ${this.#network}`
|
|
1316
|
-
);
|
|
1317
|
-
} catch (e) {
|
|
1318
|
-
this.#logger?.warn(
|
|
1319
|
-
`failed to refresh whitelist from ${this.#url}, keeping previous version: ${e}`
|
|
1320
|
-
);
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
/**
|
|
1324
|
-
* Returns the matching whitelist entry for the given address in the
|
|
1325
|
-
* requested mode, or `undefined` if absent or expired.
|
|
1326
|
-
*/
|
|
1327
|
-
has(address, mode) {
|
|
1328
|
-
const list = mode === "hard" ? this.#hard : this.#soft;
|
|
1329
|
-
const item = list.get(address);
|
|
1330
|
-
if (!item) {
|
|
1331
|
-
return void 0;
|
|
1332
|
-
}
|
|
1333
|
-
if (item.expiresAt !== void 0 && Date.now() / 1e3 >= item.expiresAt) {
|
|
1334
|
-
return void 0;
|
|
1335
|
-
}
|
|
1336
|
-
return item;
|
|
1337
|
-
}
|
|
1338
|
-
};
|
|
1339
|
-
export {
|
|
1340
|
-
CensoredString,
|
|
1341
|
-
CensoredURL,
|
|
1342
|
-
NotificationConfig,
|
|
1343
|
-
NotificationsService,
|
|
1344
|
-
ProvidersSchema,
|
|
1345
|
-
RpcProvider,
|
|
1346
|
-
SSMManager,
|
|
1347
|
-
SecretsManager,
|
|
1348
|
-
SignlConfig,
|
|
1349
|
-
SlackConfig,
|
|
1350
|
-
TelegramConfig,
|
|
1351
|
-
Whitelist,
|
|
1352
|
-
Zommand,
|
|
1353
|
-
addressArrayLike,
|
|
1354
|
-
addressLike,
|
|
1355
|
-
boolLike,
|
|
1356
|
-
clientWhitelistItemSchema,
|
|
1357
|
-
compressObject,
|
|
1358
|
-
createNotifier,
|
|
1359
|
-
createPinoCensor,
|
|
1360
|
-
decompressObject,
|
|
1361
|
-
fetchRetry,
|
|
1362
|
-
getAlchemyUrl,
|
|
1363
|
-
getAnkrUrl,
|
|
1364
|
-
getDrpcUrl,
|
|
1365
|
-
getErpcKey,
|
|
1366
|
-
getFirstHttpTransportOptions,
|
|
1367
|
-
getProviders,
|
|
1368
|
-
getRelativePath,
|
|
1369
|
-
getThirdWebUrl,
|
|
1370
|
-
optionalAddressArrayLike,
|
|
1371
|
-
optionalStringArrayLike,
|
|
1372
|
-
resolveFromObject,
|
|
1373
|
-
resolveYamlDoc,
|
|
1374
|
-
resolveYamlFiles,
|
|
1375
|
-
secretsManagerProxy,
|
|
1376
|
-
sleep,
|
|
1377
|
-
ssmManagerProxy,
|
|
1378
|
-
stringArrayLike,
|
|
1379
|
-
uncensorMap,
|
|
1380
|
-
whitelistItemSchema,
|
|
1381
|
-
whitelistItemsSchema,
|
|
1382
|
-
zommandRegistry
|
|
1070
|
+
#url;
|
|
1071
|
+
#network;
|
|
1072
|
+
#logger;
|
|
1073
|
+
#soft = new AddressMap();
|
|
1074
|
+
#hard;
|
|
1075
|
+
#refreshInterval;
|
|
1076
|
+
constructor({ url, network, hard = [], logger }) {
|
|
1077
|
+
this.#url = url;
|
|
1078
|
+
this.#network = network;
|
|
1079
|
+
this.#logger = logger?.child?.({ name: "whitelist" }) ?? logger;
|
|
1080
|
+
this.#hard = new AddressMap();
|
|
1081
|
+
for (const entry of hard) this.#hard.upsert(entry.address, entry);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Performs the initial soft fetch and starts the periodic refresh timer.
|
|
1085
|
+
* Failure of the initial fetch is non-fatal: the soft whitelist stays empty.
|
|
1086
|
+
* When no `url` is configured this is a no-op (only the hard whitelist is used).
|
|
1087
|
+
*/
|
|
1088
|
+
async start() {
|
|
1089
|
+
if (!this.#url) {
|
|
1090
|
+
this.#logger?.debug(`no whitelist url configured, using ${this.#hard.size} hard whitelist entries for ${this.#network}`);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
this.#logger?.info(`loading ${this.#network} whitelist from ${this.#url}, refresh every ${REFRESH_INTERVAL_MS / 1e3}s`);
|
|
1094
|
+
await this.#refresh();
|
|
1095
|
+
this.#refreshInterval = setInterval(() => {
|
|
1096
|
+
this.#refresh().catch((e) => {
|
|
1097
|
+
this.#logger?.warn(`unexpected whitelist refresh error: ${e}`);
|
|
1098
|
+
});
|
|
1099
|
+
}, REFRESH_INTERVAL_MS);
|
|
1100
|
+
}
|
|
1101
|
+
/** Stops the refresh timer. */
|
|
1102
|
+
stop() {
|
|
1103
|
+
if (this.#refreshInterval) {
|
|
1104
|
+
clearInterval(this.#refreshInterval);
|
|
1105
|
+
this.#refreshInterval = void 0;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Fetches the soft whitelist from the backend, parses it and atomically
|
|
1110
|
+
* replaces the in-memory map. On any error logs a warning and retains
|
|
1111
|
+
* the previous map.
|
|
1112
|
+
*/
|
|
1113
|
+
async #refresh() {
|
|
1114
|
+
if (!this.#url) return;
|
|
1115
|
+
try {
|
|
1116
|
+
const resp = await fetchRetry(this.#url);
|
|
1117
|
+
if (!resp.ok) throw new Error(`whitelist fetch failed: ${resp.status} ${resp.statusText}`);
|
|
1118
|
+
const data = await resp.json();
|
|
1119
|
+
const items = whitelistResponseSchema.parse(data).filter((i) => i.network === this.#network);
|
|
1120
|
+
const next = new AddressMap();
|
|
1121
|
+
for (const item of items) next.upsert(item.address, {
|
|
1122
|
+
address: item.address,
|
|
1123
|
+
reason: item.reason,
|
|
1124
|
+
expiresAt: item.expiresAt,
|
|
1125
|
+
contractType: item.contractType
|
|
1126
|
+
});
|
|
1127
|
+
this.#soft = next;
|
|
1128
|
+
this.#logger?.debug(`loaded ${items.length} soft whitelist entries for ${this.#network}`);
|
|
1129
|
+
} catch (e) {
|
|
1130
|
+
this.#logger?.warn(`failed to refresh whitelist from ${this.#url}, keeping previous version: ${e}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Returns the matching whitelist entry for the given address in the
|
|
1135
|
+
* requested mode, or `undefined` if absent or expired.
|
|
1136
|
+
*/
|
|
1137
|
+
has(address, mode) {
|
|
1138
|
+
const item = (mode === "hard" ? this.#hard : this.#soft).get(address);
|
|
1139
|
+
if (!item) return;
|
|
1140
|
+
if (item.expiresAt !== void 0 && Date.now() / 1e3 >= item.expiresAt) return;
|
|
1141
|
+
return item;
|
|
1142
|
+
}
|
|
1383
1143
|
};
|
|
1144
|
+
//#endregion
|
|
1145
|
+
export { CensoredString, CensoredURL, NotificationConfig, NotificationsService, ProvidersSchema, RpcProvider, SSMManager, SecretsManager, SignlConfig, SlackConfig, TelegramConfig, Whitelist, Zommand, addressArrayLike, addressLike, boolLike, clientWhitelistItemSchema, compressObject, createNotifier, createPinoCensor, decompressObject, fetchRetry, getAlchemyUrl, getAnkrUrl, getDrpcUrl, getErpcKey, getFirstHttpTransportOptions, getProviders, getRelativePath, getThirdWebUrl, optionalAddressArrayLike, optionalStringArrayLike, resolveFromObject, resolveYamlDoc, resolveYamlFiles, secretsManagerProxy, sleep, ssmManagerProxy, stringArrayLike, uncensorMap, whitelistItemSchema, whitelistItemsSchema, zommandRegistry };
|
|
1146
|
+
|
|
1384
1147
|
//# sourceMappingURL=index.js.map
|