@vellumai/vellum-gateway 0.4.40 → 0.4.42
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/package.json +1 -1
- package/src/config-file-mappings.ts +107 -0
- package/src/config.ts +21 -38
- package/src/credential-mappings.ts +102 -0
- package/src/credential-watcher.ts +25 -86
- package/src/index.ts +36 -113
package/package.json
CHANGED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { GatewayConfig } from "./config.js";
|
|
2
|
+
|
|
3
|
+
type ConfigFileMapping =
|
|
4
|
+
| {
|
|
5
|
+
key: string;
|
|
6
|
+
field: string;
|
|
7
|
+
configField: keyof GatewayConfig;
|
|
8
|
+
type: "string";
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
key: string;
|
|
12
|
+
field: string;
|
|
13
|
+
configField: keyof GatewayConfig;
|
|
14
|
+
type: "record";
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
key: string;
|
|
18
|
+
field: string;
|
|
19
|
+
configField: keyof GatewayConfig;
|
|
20
|
+
type: "normalized-record";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const CONFIG_FILE_MAPPINGS: ConfigFileMapping[] = [
|
|
24
|
+
{
|
|
25
|
+
key: "sms",
|
|
26
|
+
field: "phoneNumber",
|
|
27
|
+
configField: "twilioPhoneNumber",
|
|
28
|
+
type: "string",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: "sms",
|
|
32
|
+
field: "assistantPhoneNumbers",
|
|
33
|
+
configField: "assistantPhoneNumbers",
|
|
34
|
+
type: "normalized-record",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: "email",
|
|
38
|
+
field: "address",
|
|
39
|
+
configField: "assistantEmail",
|
|
40
|
+
type: "string",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "twilio",
|
|
44
|
+
field: "accountSid",
|
|
45
|
+
configField: "twilioAccountSid",
|
|
46
|
+
type: "string",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "ingress",
|
|
50
|
+
field: "publicBaseUrl",
|
|
51
|
+
configField: "ingressPublicBaseUrl",
|
|
52
|
+
type: "string",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** Iterate entries and keep only those whose value is a non-empty, non-whitespace string. */
|
|
57
|
+
function normalizeRecord(raw: unknown): Record<string, string> | undefined {
|
|
58
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
59
|
+
const result: Record<string, string> = {};
|
|
60
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
61
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
62
|
+
result[k] = v;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveRawValue(mapping: ConfigFileMapping, raw: unknown): unknown {
|
|
69
|
+
switch (mapping.type) {
|
|
70
|
+
case "string":
|
|
71
|
+
return typeof raw === "string" ? raw || undefined : undefined;
|
|
72
|
+
case "record":
|
|
73
|
+
return raw && typeof raw === "object" && !Array.isArray(raw)
|
|
74
|
+
? raw
|
|
75
|
+
: undefined;
|
|
76
|
+
case "normalized-record":
|
|
77
|
+
return normalizeRecord(raw);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function applyConfigFileMappings(
|
|
82
|
+
data: Record<string, unknown>,
|
|
83
|
+
changedKeys: Set<string>,
|
|
84
|
+
config: GatewayConfig,
|
|
85
|
+
): void {
|
|
86
|
+
for (const mapping of CONFIG_FILE_MAPPINGS) {
|
|
87
|
+
if (!changedKeys.has(mapping.key)) continue;
|
|
88
|
+
const section = data[mapping.key] as Record<string, unknown> | undefined;
|
|
89
|
+
const raw = section?.[mapping.field];
|
|
90
|
+
(config as Record<string, unknown>)[mapping.configField] = resolveRawValue(
|
|
91
|
+
mapping,
|
|
92
|
+
raw,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function readConfigFileDefaults(
|
|
98
|
+
data: Record<string, unknown>,
|
|
99
|
+
): Partial<Record<keyof GatewayConfig, unknown>> {
|
|
100
|
+
const defaults: Partial<Record<keyof GatewayConfig, unknown>> = {};
|
|
101
|
+
for (const mapping of CONFIG_FILE_MAPPINGS) {
|
|
102
|
+
const section = data[mapping.key] as Record<string, unknown> | undefined;
|
|
103
|
+
const raw = section?.[mapping.field];
|
|
104
|
+
defaults[mapping.configField] = resolveRawValue(mapping, raw);
|
|
105
|
+
}
|
|
106
|
+
return defaults;
|
|
107
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getLogger, type LogFileConfig } from "./logger.js";
|
|
4
|
+
import { readConfigFileDefaults } from "./config-file-mappings.js";
|
|
4
5
|
import {
|
|
5
6
|
getRootDir,
|
|
6
7
|
readCredential,
|
|
@@ -311,51 +312,31 @@ export async function loadConfig(): Promise<GatewayConfig> {
|
|
|
311
312
|
let twilioAccountSid =
|
|
312
313
|
process.env.TWILIO_ACCOUNT_SID || twilioCreds?.accountSid || undefined;
|
|
313
314
|
|
|
314
|
-
//
|
|
315
|
-
let
|
|
316
|
-
process.env.TWILIO_PHONE_NUMBER || undefined;
|
|
317
|
-
let assistantPhoneNumbers: Record<string, string> | undefined;
|
|
318
|
-
let assistantEmail: string | undefined;
|
|
315
|
+
// Read config.json defaults for fields that can come from the config file
|
|
316
|
+
let configFileData: Record<string, unknown> = {};
|
|
319
317
|
try {
|
|
320
318
|
const cfgPath = join(getRootDir(), "workspace", "config.json");
|
|
321
319
|
const raw = readFileSync(cfgPath, "utf-8");
|
|
322
320
|
const data = JSON.parse(raw);
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
data?.sms?.phoneNumber &&
|
|
326
|
-
typeof data.sms.phoneNumber === "string"
|
|
327
|
-
) {
|
|
328
|
-
twilioPhoneNumber = data.sms.phoneNumber;
|
|
329
|
-
}
|
|
330
|
-
if (
|
|
331
|
-
!twilioAccountSid &&
|
|
332
|
-
data?.twilio?.accountSid &&
|
|
333
|
-
typeof data.twilio.accountSid === "string"
|
|
334
|
-
) {
|
|
335
|
-
twilioAccountSid = data.twilio.accountSid;
|
|
336
|
-
}
|
|
337
|
-
const rawMapping = data?.sms?.assistantPhoneNumbers;
|
|
338
|
-
if (
|
|
339
|
-
rawMapping &&
|
|
340
|
-
typeof rawMapping === "object" &&
|
|
341
|
-
!Array.isArray(rawMapping)
|
|
342
|
-
) {
|
|
343
|
-
const normalized: Record<string, string> = {};
|
|
344
|
-
for (const [assistantId, phoneNumber] of Object.entries(
|
|
345
|
-
rawMapping as Record<string, unknown>,
|
|
346
|
-
)) {
|
|
347
|
-
if (typeof phoneNumber === "string" && phoneNumber.trim().length > 0) {
|
|
348
|
-
normalized[assistantId] = phoneNumber;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
assistantPhoneNumbers = normalized;
|
|
352
|
-
}
|
|
353
|
-
if (data?.email?.address && typeof data.email.address === "string") {
|
|
354
|
-
assistantEmail = data.email.address;
|
|
321
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
322
|
+
configFileData = data as Record<string, unknown>;
|
|
355
323
|
}
|
|
356
324
|
} catch {
|
|
357
325
|
// config file may not exist yet
|
|
358
326
|
}
|
|
327
|
+
const configDefaults = readConfigFileDefaults(configFileData);
|
|
328
|
+
|
|
329
|
+
// Phone number: env var > config file > credential store
|
|
330
|
+
let twilioPhoneNumber: string | undefined =
|
|
331
|
+
process.env.TWILIO_PHONE_NUMBER ||
|
|
332
|
+
(configDefaults.twilioPhoneNumber as string | undefined);
|
|
333
|
+
if (!twilioAccountSid) {
|
|
334
|
+
twilioAccountSid = configDefaults.twilioAccountSid as string | undefined;
|
|
335
|
+
}
|
|
336
|
+
const assistantPhoneNumbers = configDefaults.assistantPhoneNumbers as
|
|
337
|
+
| Record<string, string>
|
|
338
|
+
| undefined;
|
|
339
|
+
const assistantEmail = configDefaults.assistantEmail as string | undefined;
|
|
359
340
|
if (!twilioPhoneNumber) {
|
|
360
341
|
twilioPhoneNumber =
|
|
361
342
|
(await readCredential("credential:twilio:phone_number")) || undefined;
|
|
@@ -503,7 +484,9 @@ export async function loadConfig(): Promise<GatewayConfig> {
|
|
|
503
484
|
}
|
|
504
485
|
const trustProxy = trustProxyRaw === "true";
|
|
505
486
|
|
|
506
|
-
const ingressPublicBaseUrl =
|
|
487
|
+
const ingressPublicBaseUrl =
|
|
488
|
+
process.env.INGRESS_PUBLIC_BASE_URL ||
|
|
489
|
+
(configDefaults.ingressPublicBaseUrl as string | undefined);
|
|
507
490
|
|
|
508
491
|
const logFileDir = process.env.GATEWAY_LOG_DIR || undefined;
|
|
509
492
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { GatewayConfig } from "./config.js";
|
|
2
|
+
import type { CredentialChangeEvent } from "./credential-watcher.js";
|
|
3
|
+
|
|
4
|
+
type CredentialServiceMapping = {
|
|
5
|
+
/** Key on CredentialChangeEvent indicating whether this service changed. */
|
|
6
|
+
changedKey: keyof CredentialChangeEvent & `${string}Changed`;
|
|
7
|
+
/** Key on CredentialChangeEvent containing the credentials object. */
|
|
8
|
+
credentialsKey: keyof CredentialChangeEvent & `${string}Credentials`;
|
|
9
|
+
/** Whether to skip updates when env vars provide these credentials. */
|
|
10
|
+
envGuarded: boolean;
|
|
11
|
+
/** Human-friendly name used in log messages (e.g. "Slack channel"). Falls back to capitalizing changedKey. */
|
|
12
|
+
displayName?: string;
|
|
13
|
+
/** Maps credential object fields to GatewayConfig fields. */
|
|
14
|
+
fields: Array<{
|
|
15
|
+
credField: string;
|
|
16
|
+
configField: keyof GatewayConfig;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function buildCredentialServiceMappings(opts: {
|
|
21
|
+
telegramFromEnv: boolean;
|
|
22
|
+
slackFromEnv: boolean;
|
|
23
|
+
}): CredentialServiceMapping[] {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
changedKey: "telegramChanged",
|
|
27
|
+
credentialsKey: "telegramCredentials",
|
|
28
|
+
envGuarded: opts.telegramFromEnv,
|
|
29
|
+
fields: [
|
|
30
|
+
{ credField: "botToken", configField: "telegramBotToken" },
|
|
31
|
+
{ credField: "webhookSecret", configField: "telegramWebhookSecret" },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
changedKey: "twilioChanged",
|
|
36
|
+
credentialsKey: "twilioCredentials",
|
|
37
|
+
envGuarded: false,
|
|
38
|
+
fields: [
|
|
39
|
+
{ credField: "accountSid", configField: "twilioAccountSid" },
|
|
40
|
+
{ credField: "authToken", configField: "twilioAuthToken" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
changedKey: "whatsappChanged",
|
|
45
|
+
credentialsKey: "whatsappCredentials",
|
|
46
|
+
envGuarded: false,
|
|
47
|
+
fields: [
|
|
48
|
+
{ credField: "phoneNumberId", configField: "whatsappPhoneNumberId" },
|
|
49
|
+
{ credField: "accessToken", configField: "whatsappAccessToken" },
|
|
50
|
+
{ credField: "appSecret", configField: "whatsappAppSecret" },
|
|
51
|
+
{
|
|
52
|
+
credField: "webhookVerifyToken",
|
|
53
|
+
configField: "whatsappWebhookVerifyToken",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
changedKey: "slackChannelChanged",
|
|
59
|
+
credentialsKey: "slackChannelCredentials",
|
|
60
|
+
envGuarded: opts.slackFromEnv,
|
|
61
|
+
displayName: "Slack channel",
|
|
62
|
+
fields: [
|
|
63
|
+
{ credField: "botToken", configField: "slackChannelBotToken" },
|
|
64
|
+
{ credField: "appToken", configField: "slackChannelAppToken" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function applyCredentialChanges(
|
|
71
|
+
event: CredentialChangeEvent,
|
|
72
|
+
config: GatewayConfig,
|
|
73
|
+
mappings: CredentialServiceMapping[],
|
|
74
|
+
log: { info: (msg: string) => void },
|
|
75
|
+
): Set<string> {
|
|
76
|
+
const changedServices = new Set<string>();
|
|
77
|
+
for (const mapping of mappings) {
|
|
78
|
+
if (!event[mapping.changedKey]) continue;
|
|
79
|
+
if (mapping.envGuarded) continue;
|
|
80
|
+
const creds = event[mapping.credentialsKey] as Record<
|
|
81
|
+
string,
|
|
82
|
+
unknown
|
|
83
|
+
> | null;
|
|
84
|
+
for (const { credField, configField } of mapping.fields) {
|
|
85
|
+
(config as Record<string, unknown>)[configField] = creds
|
|
86
|
+
? (creds as Record<string, unknown>)[credField]
|
|
87
|
+
: undefined;
|
|
88
|
+
}
|
|
89
|
+
// Extract a human-friendly service name from the changedKey (e.g. "telegramChanged" -> "Telegram")
|
|
90
|
+
const serviceName = mapping.changedKey.replace("Changed", "");
|
|
91
|
+
const capitalizedName =
|
|
92
|
+
serviceName.charAt(0).toUpperCase() + serviceName.slice(1);
|
|
93
|
+
const logName = mapping.displayName ?? capitalizedName;
|
|
94
|
+
log.info(
|
|
95
|
+
creds
|
|
96
|
+
? `${logName} credentials loaded from credential vault`
|
|
97
|
+
: `${logName} credentials cleared`,
|
|
98
|
+
);
|
|
99
|
+
changedServices.add(serviceName);
|
|
100
|
+
}
|
|
101
|
+
return changedServices;
|
|
102
|
+
}
|
|
@@ -43,16 +43,7 @@ export class CredentialWatcher {
|
|
|
43
43
|
private watcher: FSWatcher | null = null;
|
|
44
44
|
private watchingDirectory = false;
|
|
45
45
|
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
46
|
-
private
|
|
47
|
-
private lastWebhookSecret: string | undefined;
|
|
48
|
-
private lastTwilioAccountSid: string | undefined;
|
|
49
|
-
private lastTwilioAuthToken: string | undefined;
|
|
50
|
-
private lastWhatsAppPhoneNumberId: string | undefined;
|
|
51
|
-
private lastWhatsAppAccessToken: string | undefined;
|
|
52
|
-
private lastWhatsAppAppSecret: string | undefined;
|
|
53
|
-
private lastWhatsAppWebhookVerifyToken: string | undefined;
|
|
54
|
-
private lastSlackChannelBotToken: string | undefined;
|
|
55
|
-
private lastSlackChannelAppToken: string | undefined;
|
|
46
|
+
private lastSerialized: Map<string, string> = new Map();
|
|
56
47
|
private polling = false;
|
|
57
48
|
private pendingPoll = false;
|
|
58
49
|
private callback: CredentialChangeCallback;
|
|
@@ -157,90 +148,38 @@ export class CredentialWatcher {
|
|
|
157
148
|
const whatsappCredentials = await readWhatsAppCredentials();
|
|
158
149
|
const slackChannelCredentials = await readSlackChannelCredentials();
|
|
159
150
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const whatsappChanged =
|
|
181
|
-
newWhatsAppPhoneNumberId !== this.lastWhatsAppPhoneNumberId ||
|
|
182
|
-
newWhatsAppAccessToken !== this.lastWhatsAppAccessToken ||
|
|
183
|
-
newWhatsAppAppSecret !== this.lastWhatsAppAppSecret ||
|
|
184
|
-
newWhatsAppWebhookVerifyToken !== this.lastWhatsAppWebhookVerifyToken;
|
|
185
|
-
|
|
186
|
-
const slackChannelChanged =
|
|
187
|
-
newSlackChannelBotToken !== this.lastSlackChannelBotToken ||
|
|
188
|
-
newSlackChannelAppToken !== this.lastSlackChannelAppToken;
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
!telegramChanged &&
|
|
192
|
-
!twilioChanged &&
|
|
193
|
-
!whatsappChanged &&
|
|
194
|
-
!slackChannelChanged
|
|
195
|
-
) {
|
|
196
|
-
return;
|
|
151
|
+
const services = {
|
|
152
|
+
telegram: { creds: telegramCredentials, key: "telegram" },
|
|
153
|
+
twilio: { creds: twilioCredentials, key: "twilio" },
|
|
154
|
+
whatsapp: { creds: whatsappCredentials, key: "whatsapp" },
|
|
155
|
+
slackChannel: { creds: slackChannelCredentials, key: "slackChannel" },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const changedServices = new Set<string>();
|
|
159
|
+
for (const [name, { creds }] of Object.entries(services)) {
|
|
160
|
+
const newVal = creds ? JSON.stringify(creds) : undefined;
|
|
161
|
+
const oldVal = this.lastSerialized.get(name);
|
|
162
|
+
if (newVal !== oldVal) {
|
|
163
|
+
changedServices.add(name);
|
|
164
|
+
if (newVal !== undefined) {
|
|
165
|
+
this.lastSerialized.set(name, newVal);
|
|
166
|
+
} else {
|
|
167
|
+
this.lastSerialized.delete(name);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
197
170
|
}
|
|
198
171
|
|
|
199
|
-
|
|
200
|
-
this.lastWebhookSecret = newWebhookSecret;
|
|
201
|
-
this.lastTwilioAccountSid = newTwilioAccountSid;
|
|
202
|
-
this.lastTwilioAuthToken = newTwilioAuthToken;
|
|
203
|
-
this.lastWhatsAppPhoneNumberId = newWhatsAppPhoneNumberId;
|
|
204
|
-
this.lastWhatsAppAccessToken = newWhatsAppAccessToken;
|
|
205
|
-
this.lastWhatsAppAppSecret = newWhatsAppAppSecret;
|
|
206
|
-
this.lastWhatsAppWebhookVerifyToken = newWhatsAppWebhookVerifyToken;
|
|
207
|
-
this.lastSlackChannelBotToken = newSlackChannelBotToken;
|
|
208
|
-
this.lastSlackChannelAppToken = newSlackChannelAppToken;
|
|
209
|
-
|
|
210
|
-
if (telegramChanged) {
|
|
211
|
-
log.info(
|
|
212
|
-
{ hasCredentials: !!telegramCredentials },
|
|
213
|
-
"Telegram credentials changed",
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
if (twilioChanged) {
|
|
217
|
-
log.info(
|
|
218
|
-
{ hasCredentials: !!twilioCredentials },
|
|
219
|
-
"Twilio credentials changed",
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
if (whatsappChanged) {
|
|
223
|
-
log.info(
|
|
224
|
-
{ hasCredentials: !!whatsappCredentials },
|
|
225
|
-
"WhatsApp credentials changed",
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
if (slackChannelChanged) {
|
|
229
|
-
log.info(
|
|
230
|
-
{ hasCredentials: !!slackChannelCredentials },
|
|
231
|
-
"Slack channel credentials changed",
|
|
232
|
-
);
|
|
233
|
-
}
|
|
172
|
+
if (changedServices.size === 0) return;
|
|
234
173
|
|
|
235
174
|
this.callback({
|
|
236
175
|
telegramCredentials,
|
|
237
|
-
telegramChanged,
|
|
176
|
+
telegramChanged: changedServices.has("telegram"),
|
|
238
177
|
twilioCredentials,
|
|
239
|
-
twilioChanged,
|
|
178
|
+
twilioChanged: changedServices.has("twilio"),
|
|
240
179
|
whatsappCredentials,
|
|
241
|
-
whatsappChanged,
|
|
180
|
+
whatsappChanged: changedServices.has("whatsapp"),
|
|
242
181
|
slackChannelCredentials,
|
|
243
|
-
slackChannelChanged,
|
|
182
|
+
slackChannelChanged: changedServices.has("slackChannel"),
|
|
244
183
|
});
|
|
245
184
|
} finally {
|
|
246
185
|
this.polling = false;
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
} from "./auth/token-exchange.js";
|
|
13
13
|
import { ConfigFileWatcher } from "./config-file-watcher.js";
|
|
14
14
|
import { loadConfig, isSlackChannelConfigured } from "./config.js";
|
|
15
|
+
import {
|
|
16
|
+
buildCredentialServiceMappings,
|
|
17
|
+
applyCredentialChanges,
|
|
18
|
+
} from "./credential-mappings.js";
|
|
15
19
|
import { CredentialWatcher } from "./credential-watcher.js";
|
|
16
20
|
import { createRuntimeProxyHandler } from "./http/routes/runtime-proxy.js";
|
|
17
21
|
import {
|
|
@@ -63,6 +67,7 @@ import {
|
|
|
63
67
|
type RouteDefinition,
|
|
64
68
|
type GetClientIp,
|
|
65
69
|
} from "./http/router.js";
|
|
70
|
+
import { applyConfigFileMappings } from "./config-file-mappings.js";
|
|
66
71
|
import { callTelegramApi } from "./telegram/api.js";
|
|
67
72
|
import { reconcileTelegramWebhook } from "./telegram/webhook-manager.js";
|
|
68
73
|
|
|
@@ -872,65 +877,30 @@ async function main() {
|
|
|
872
877
|
process.env.SLACK_CHANNEL_BOT_TOKEN && process.env.SLACK_CHANNEL_APP_TOKEN
|
|
873
878
|
);
|
|
874
879
|
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
config.telegramWebhookSecret = event.telegramCredentials.webhookSecret;
|
|
880
|
-
log.info("Telegram credentials loaded from credential vault");
|
|
881
|
-
registerTelegramCommands();
|
|
882
|
-
reconcileTelegramWebhook(config).catch((err) => {
|
|
883
|
-
log.error(
|
|
884
|
-
{ err },
|
|
885
|
-
"Failed to reconcile Telegram webhook after credential change",
|
|
886
|
-
);
|
|
887
|
-
});
|
|
888
|
-
} else {
|
|
889
|
-
config.telegramBotToken = undefined;
|
|
890
|
-
config.telegramWebhookSecret = undefined;
|
|
891
|
-
log.info("Telegram credentials cleared");
|
|
892
|
-
}
|
|
893
|
-
}
|
|
880
|
+
const credentialMappings = buildCredentialServiceMappings({
|
|
881
|
+
telegramFromEnv,
|
|
882
|
+
slackFromEnv,
|
|
883
|
+
});
|
|
894
884
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
config.twilioAuthToken = undefined;
|
|
903
|
-
log.info("Twilio credentials cleared");
|
|
904
|
-
}
|
|
905
|
-
}
|
|
885
|
+
const credentialWatcher = new CredentialWatcher((event) => {
|
|
886
|
+
const changed = applyCredentialChanges(
|
|
887
|
+
event,
|
|
888
|
+
config,
|
|
889
|
+
credentialMappings,
|
|
890
|
+
log,
|
|
891
|
+
);
|
|
906
892
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
916
|
-
config.whatsappPhoneNumberId = undefined;
|
|
917
|
-
config.whatsappAccessToken = undefined;
|
|
918
|
-
config.whatsappAppSecret = undefined;
|
|
919
|
-
config.whatsappWebhookVerifyToken = undefined;
|
|
920
|
-
log.info("WhatsApp credentials cleared");
|
|
921
|
-
}
|
|
893
|
+
// Side effects keyed by service name
|
|
894
|
+
if (changed.has("telegram") && isTelegramConfigured()) {
|
|
895
|
+
registerTelegramCommands();
|
|
896
|
+
reconcileTelegramWebhook(config).catch((err) => {
|
|
897
|
+
log.error(
|
|
898
|
+
{ err },
|
|
899
|
+
"Failed to reconcile Telegram webhook after credential change",
|
|
900
|
+
);
|
|
901
|
+
});
|
|
922
902
|
}
|
|
923
|
-
|
|
924
|
-
if (event.slackChannelChanged && !slackFromEnv) {
|
|
925
|
-
if (event.slackChannelCredentials) {
|
|
926
|
-
config.slackChannelBotToken = event.slackChannelCredentials.botToken;
|
|
927
|
-
config.slackChannelAppToken = event.slackChannelCredentials.appToken;
|
|
928
|
-
log.info("Slack channel credentials loaded from credential vault");
|
|
929
|
-
} else {
|
|
930
|
-
config.slackChannelBotToken = undefined;
|
|
931
|
-
config.slackChannelAppToken = undefined;
|
|
932
|
-
log.info("Slack channel credentials cleared");
|
|
933
|
-
}
|
|
903
|
+
if (changed.has("slackChannel")) {
|
|
934
904
|
startSlackSocket();
|
|
935
905
|
}
|
|
936
906
|
});
|
|
@@ -938,63 +908,16 @@ async function main() {
|
|
|
938
908
|
credentialWatcher.start();
|
|
939
909
|
|
|
940
910
|
const configFileWatcher = new ConfigFileWatcher((event) => {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
: undefined;
|
|
952
|
-
if (
|
|
953
|
-
sms?.assistantPhoneNumbers &&
|
|
954
|
-
typeof sms.assistantPhoneNumbers === "object" &&
|
|
955
|
-
!Array.isArray(sms.assistantPhoneNumbers)
|
|
956
|
-
) {
|
|
957
|
-
config.assistantPhoneNumbers = sms.assistantPhoneNumbers as Record<
|
|
958
|
-
string,
|
|
959
|
-
string
|
|
960
|
-
>;
|
|
961
|
-
} else {
|
|
962
|
-
config.assistantPhoneNumbers = undefined;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
if (event.changedKeys.has("email")) {
|
|
967
|
-
const email = event.data.email as { address?: string } | undefined;
|
|
968
|
-
config.assistantEmail =
|
|
969
|
-
typeof email?.address === "string"
|
|
970
|
-
? email.address || undefined
|
|
971
|
-
: undefined;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
if (event.changedKeys.has("twilio")) {
|
|
975
|
-
const twilio = event.data.twilio as { accountSid?: string } | undefined;
|
|
976
|
-
config.twilioAccountSid =
|
|
977
|
-
typeof twilio?.accountSid === "string"
|
|
978
|
-
? twilio.accountSid || undefined
|
|
979
|
-
: undefined;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (event.changedKeys.has("ingress")) {
|
|
983
|
-
const ingress = event.data.ingress as
|
|
984
|
-
| { publicBaseUrl?: string }
|
|
985
|
-
| undefined;
|
|
986
|
-
config.ingressPublicBaseUrl =
|
|
987
|
-
typeof ingress?.publicBaseUrl === "string"
|
|
988
|
-
? ingress.publicBaseUrl || undefined
|
|
989
|
-
: undefined;
|
|
990
|
-
if (isTelegramConfigured()) {
|
|
991
|
-
reconcileTelegramWebhook(config).catch((err) => {
|
|
992
|
-
log.error(
|
|
993
|
-
{ err },
|
|
994
|
-
"Failed to reconcile Telegram webhook after ingress URL change",
|
|
995
|
-
);
|
|
996
|
-
});
|
|
997
|
-
}
|
|
911
|
+
applyConfigFileMappings(event.data, event.changedKeys, config);
|
|
912
|
+
|
|
913
|
+
// Side effect: reconcile Telegram webhook when ingress URL changes
|
|
914
|
+
if (event.changedKeys.has("ingress") && isTelegramConfigured()) {
|
|
915
|
+
reconcileTelegramWebhook(config).catch((err) => {
|
|
916
|
+
log.error(
|
|
917
|
+
{ err },
|
|
918
|
+
"Failed to reconcile Telegram webhook after ingress URL change",
|
|
919
|
+
);
|
|
920
|
+
});
|
|
998
921
|
}
|
|
999
922
|
});
|
|
1000
923
|
|