@vellumai/vellum-gateway 0.7.0 → 0.7.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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +67 -25
- package/Dockerfile +2 -0
- package/README.md +50 -13
- package/bun.lock +16 -2
- package/knip.json +3 -1
- package/package.json +3 -1
- package/src/__tests__/auto-approve-thresholds.test.ts +49 -22
- package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
- package/src/__tests__/config-file-watcher.test.ts +181 -0
- package/src/__tests__/config.test.ts +0 -1
- package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/credential-watcher-managed-bootstrap.test.ts +10 -2
- package/src/__tests__/credential-watcher.test.ts +30 -2
- package/src/__tests__/db-connection-isolation.test.ts +157 -0
- package/src/__tests__/fake-assistant-ipc.ts +39 -0
- package/src/__tests__/feature-flags-route.test.ts +8 -8
- package/src/__tests__/guardian-init-lockfile.test.ts +30 -4
- package/src/__tests__/ipc-feature-flag-routes.test.ts +1 -1
- package/src/__tests__/live-voice-websocket.test.ts +0 -1
- package/src/__tests__/load-guards.test.ts +0 -1
- package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -1
- package/src/__tests__/oauth-callback.test.ts +0 -1
- package/src/__tests__/pair-origin-allowlist.test.ts +155 -0
- package/src/__tests__/rate-limit-loopback.test.ts +1 -1
- package/src/__tests__/remote-feature-flag-sync.test.ts +47 -7
- package/src/__tests__/resolve-assistant.test.ts +0 -1
- package/src/__tests__/route-schema-guard.test.ts +42 -6
- package/src/__tests__/runtime-client.test.ts +0 -1
- package/src/__tests__/runtime-health-proxy.test.ts +0 -1
- package/src/__tests__/runtime-proxy-auth.test.ts +0 -1
- package/src/__tests__/runtime-proxy.test.ts +0 -1
- package/src/__tests__/slack-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/slack-display-name.test.ts +66 -1
- package/src/__tests__/slack-normalize.test.ts +158 -4
- package/src/__tests__/slack-reaction-normalize.test.ts +0 -1
- package/src/__tests__/slack-socket-mode-catchup.test.ts +857 -0
- package/src/__tests__/slack-socket-mode-scopes.test.ts +52 -0
- package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +654 -0
- package/src/__tests__/stt-stream-websocket.test.ts +0 -1
- package/src/__tests__/telegram-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/telegram-send-attachments.test.ts +0 -1
- package/src/__tests__/telegram-webhook-handler.test.ts +0 -1
- package/src/__tests__/text-verification-helpers.test.ts +136 -0
- package/src/__tests__/twilio-media-websocket.test.ts +0 -1
- package/src/__tests__/twilio-relay-websocket.test.ts +0 -1
- package/src/__tests__/twilio-webhooks.test.ts +220 -3
- package/src/__tests__/upstream-transport.test.ts +0 -36
- package/src/__tests__/whatsapp-download.test.ts +0 -1
- package/src/__tests__/whatsapp-webhook.test.ts +0 -1
- package/src/auth/guardian-refresh.ts +4 -18
- package/src/auth/ipc-route-policy.ts +217 -0
- package/src/backup/backup-key.ts +138 -0
- package/src/backup/backup-routes.ts +159 -0
- package/src/backup/backup-worker.ts +374 -0
- package/src/backup/list-snapshots.ts +97 -0
- package/src/backup/local-writer.ts +87 -0
- package/src/backup/offsite-writer.ts +182 -0
- package/src/backup/paths.ts +123 -0
- package/src/backup/stream-crypt.ts +258 -0
- package/src/chrome-extension-origins.ts +28 -0
- package/src/cli/enable-proxy.ts +0 -1
- package/src/config-file-cache.ts +3 -19
- package/src/config-file-utils.ts +124 -0
- package/src/config-file-watcher.ts +57 -25
- package/src/config.ts +4 -7
- package/src/db/connection.ts +65 -3
- package/src/db/contact-store.ts +30 -1
- package/src/db/data-migrations/index.ts +2 -0
- package/src/db/data-migrations/m0003-recover-backup-key.ts +71 -0
- package/src/db/schema.ts +92 -0
- package/src/db/slack-store.ts +144 -11
- package/src/feature-flag-registry.json +40 -152
- package/src/handlers/handle-inbound.ts +123 -0
- package/src/http/middleware/auth.ts +44 -1
- package/src/http/middleware/cors.ts +84 -0
- package/src/http/middleware/rate-limit.ts +6 -8
- package/src/http/routes/auto-approve-thresholds.ts +17 -1
- package/src/http/routes/brain-graph-proxy.ts +1 -1
- package/src/http/routes/channel-readiness-proxy.ts +2 -2
- package/src/http/routes/channel-verification-session-proxy.ts +19 -37
- package/src/http/routes/contact-prompt.ts +149 -0
- package/src/http/routes/contacts-control-plane-proxy.ts +2 -2
- package/src/http/routes/email-webhook.test.ts +0 -1
- package/src/http/routes/ipc-runtime-proxy.test.ts +197 -1
- package/src/http/routes/ipc-runtime-proxy.ts +95 -0
- package/src/http/routes/log-export.test.ts +0 -1
- package/src/http/routes/log-tail.test.ts +336 -0
- package/src/http/routes/log-tail.ts +87 -0
- package/src/http/routes/migration-proxy.ts +1 -2
- package/src/http/routes/oauth-apps-proxy.ts +2 -2
- package/src/http/routes/oauth-providers-proxy.ts +2 -2
- package/src/http/routes/pair.ts +322 -0
- package/src/http/routes/privacy-config.ts +65 -79
- package/src/http/routes/runtime-health-proxy.ts +2 -2
- package/src/http/routes/runtime-proxy.ts +3 -1
- package/src/http/routes/slack-control-plane-proxy.ts +3 -20
- package/src/http/routes/stt-stream-websocket.ts +2 -3
- package/src/http/routes/telegram-control-plane-proxy.ts +2 -2
- package/src/http/routes/telegram-webhook.test.ts +0 -1
- package/src/http/routes/telegram-webhook.ts +6 -0
- package/src/http/routes/trust-rules.suggest.test.ts +25 -0
- package/src/http/routes/trust-rules.ts +7 -0
- package/src/http/routes/twilio-control-plane-proxy.ts +2 -2
- package/src/http/routes/twilio-media-websocket.ts +5 -5
- package/src/http/routes/twilio-voice-verify-callback.ts +310 -0
- package/src/http/routes/twilio-voice-webhook.test.ts +65 -1
- package/src/http/routes/twilio-voice-webhook.ts +45 -1
- package/src/http/routes/whatsapp-webhook.test.ts +0 -1
- package/src/index.ts +357 -278
- package/src/ipc/assistant-client.ts +8 -4
- package/src/ipc/contact-handlers.ts +88 -3
- package/src/ipc/threshold-handlers.ts +2 -0
- package/src/post-assistant-ready.ts +5 -3
- package/src/risk/bash-risk-classifier.test.ts +35 -27
- package/src/risk/bash-risk-classifier.ts +44 -14
- package/src/risk/command-registry/commands/assistant.ts +8 -19
- package/src/risk/command-registry.test.ts +0 -15
- package/src/risk/risk-classifier-parity.test.ts +1 -3
- package/src/runtime/client.ts +58 -3
- package/src/schema.ts +277 -104
- package/src/slack/normalize.test.ts +98 -0
- package/src/slack/normalize.ts +107 -32
- package/src/slack/slack-web.ts +213 -0
- package/src/slack/socket-mode.ts +701 -39
- package/src/telegram/send.test.ts +0 -1
- package/src/twilio/validate-webhook.ts +53 -14
- package/src/twilio/webhook-sync-trigger.ts +58 -0
- package/src/twilio/webhook-sync.test.ts +286 -0
- package/src/twilio/webhook-sync.ts +84 -0
- package/src/util/is-loopback-address.ts +27 -0
- package/src/velay/bridge-utils.ts +228 -0
- package/src/velay/client.test.ts +939 -0
- package/src/velay/client.ts +555 -0
- package/src/velay/http-bridge.test.ts +217 -0
- package/src/velay/http-bridge.ts +83 -0
- package/src/velay/protocol.ts +178 -0
- package/src/velay/test-fake-websocket.ts +69 -0
- package/src/velay/websocket-bridge.test.ts +367 -0
- package/src/velay/websocket-bridge.ts +324 -0
- package/src/verification/binding-helpers.ts +107 -0
- package/src/verification/code-parsing.ts +44 -0
- package/src/verification/contact-helpers.ts +342 -0
- package/src/verification/identity-match.ts +68 -0
- package/src/verification/identity.ts +61 -0
- package/src/verification/rate-limit-helpers.ts +205 -0
- package/src/verification/reply-delivery.ts +109 -0
- package/src/verification/session-helpers.ts +164 -0
- package/src/verification/text-verification.ts +372 -0
- package/src/version.ts +35 -0
- package/src/voice/verification.ts +456 -0
- package/src/webhook-pipeline.ts +4 -0
- package/src/__tests__/browser-relay-websocket.test.ts +0 -698
- package/src/__tests__/telegram-only-default.test.ts +0 -133
- package/src/auth/capability-tokens.ts +0 -248
- package/src/http/routes/browser-extension-pair.ts +0 -455
- package/src/http/routes/browser-relay-websocket.ts +0 -381
- package/src/http/routes/config-file-utils.ts +0 -73
- package/src/ipc/capability-token-handlers.ts +0 -30
- package/src/pairing/approved-devices-store.ts +0 -110
- package/src/pairing/pairing-routes.ts +0 -379
- package/src/pairing/pairing-store.ts +0 -218
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway-owned text-channel verification intercept.
|
|
3
|
+
*
|
|
4
|
+
* Called from handleInbound before forwardToRuntime. When a message is a
|
|
5
|
+
* bare verification code AND there is a pending/active session for this
|
|
6
|
+
* channel, the gateway handles the entire flow:
|
|
7
|
+
*
|
|
8
|
+
* 1. Parse code from message content
|
|
9
|
+
* 2. Check rate limits
|
|
10
|
+
* 3. Hash + find matching session
|
|
11
|
+
* 4. Verify identity binding (outbound sessions)
|
|
12
|
+
* 5. Consume session (dual-write, atomic status guard)
|
|
13
|
+
* 6. Apply side effects (guardian binding OR trusted contact upsert)
|
|
14
|
+
* 7. Deliver deterministic reply
|
|
15
|
+
*
|
|
16
|
+
* The assistant NEVER sees verification code messages. Both success and
|
|
17
|
+
* failure are short-circuited at the gateway.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createGuardianBinding } from "../auth/guardian-bootstrap.js";
|
|
21
|
+
import { getLogger } from "../logger.js";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
getExistingGuardianBinding,
|
|
25
|
+
resolveCanonicalPrincipal,
|
|
26
|
+
revokeExistingChannelGuardian,
|
|
27
|
+
} from "./binding-helpers.js";
|
|
28
|
+
import { parseVerificationCode, hashVerificationSecret } from "./code-parsing.js";
|
|
29
|
+
import {
|
|
30
|
+
findContactChannelByExternalUserId,
|
|
31
|
+
upsertVerifiedContactChannel,
|
|
32
|
+
} from "./contact-helpers.js";
|
|
33
|
+
import { canonicalizeInboundIdentity } from "./identity.js";
|
|
34
|
+
import { checkIdentityMatch } from "./identity-match.js";
|
|
35
|
+
import {
|
|
36
|
+
isRateLimited,
|
|
37
|
+
recordInvalidAttempt,
|
|
38
|
+
resetRateLimit,
|
|
39
|
+
} from "./rate-limit-helpers.js";
|
|
40
|
+
import {
|
|
41
|
+
composeVerificationFailureReply,
|
|
42
|
+
composeVerificationSuccessReply,
|
|
43
|
+
deliverVerificationReply,
|
|
44
|
+
} from "./reply-delivery.js";
|
|
45
|
+
import {
|
|
46
|
+
consumeSession,
|
|
47
|
+
findSessionByHash,
|
|
48
|
+
hasPendingOrActiveSession,
|
|
49
|
+
} from "./session-helpers.js";
|
|
50
|
+
|
|
51
|
+
const log = getLogger("text-verification");
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Types
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export interface TextVerificationInterceptParams {
|
|
58
|
+
sourceChannel: string;
|
|
59
|
+
messageContent: string;
|
|
60
|
+
actorExternalUserId: string;
|
|
61
|
+
actorChatId: string;
|
|
62
|
+
actorDisplayName?: string;
|
|
63
|
+
actorUsername?: string;
|
|
64
|
+
replyCallbackUrl?: string;
|
|
65
|
+
assistantId?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type TextVerificationResult =
|
|
69
|
+
| { intercepted: false }
|
|
70
|
+
| {
|
|
71
|
+
intercepted: true;
|
|
72
|
+
outcome: "verified" | "failed";
|
|
73
|
+
trustClass: "guardian" | "trusted_contact";
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Main intercept
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export async function tryTextVerificationIntercept(
|
|
81
|
+
params: TextVerificationInterceptParams,
|
|
82
|
+
): Promise<TextVerificationResult> {
|
|
83
|
+
const {
|
|
84
|
+
sourceChannel,
|
|
85
|
+
messageContent,
|
|
86
|
+
actorExternalUserId,
|
|
87
|
+
actorChatId,
|
|
88
|
+
actorDisplayName,
|
|
89
|
+
actorUsername,
|
|
90
|
+
replyCallbackUrl,
|
|
91
|
+
assistantId,
|
|
92
|
+
} = params;
|
|
93
|
+
|
|
94
|
+
// 1. Parse — only bare 6-digit numeric or 64-char hex codes are intercepted
|
|
95
|
+
const code = parseVerificationCode(messageContent);
|
|
96
|
+
if (code === undefined) {
|
|
97
|
+
return { intercepted: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. Fast guard — is there any pending session for this channel?
|
|
101
|
+
const hasSessions = await hasPendingOrActiveSession(sourceChannel);
|
|
102
|
+
if (!hasSessions) {
|
|
103
|
+
return { intercepted: false };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const canonicalUserId =
|
|
107
|
+
canonicalizeInboundIdentity(sourceChannel, actorExternalUserId) ??
|
|
108
|
+
actorExternalUserId;
|
|
109
|
+
|
|
110
|
+
// 3. Rate limit check
|
|
111
|
+
if (isRateLimited(sourceChannel, canonicalUserId, actorChatId)) {
|
|
112
|
+
log.info(
|
|
113
|
+
{ sourceChannel, actorExternalUserId: canonicalUserId },
|
|
114
|
+
"Verification attempt rate-limited",
|
|
115
|
+
);
|
|
116
|
+
await replyWithFailure(
|
|
117
|
+
replyCallbackUrl,
|
|
118
|
+
actorChatId,
|
|
119
|
+
assistantId,
|
|
120
|
+
"The verification code is invalid or has expired.",
|
|
121
|
+
);
|
|
122
|
+
return {
|
|
123
|
+
intercepted: true,
|
|
124
|
+
outcome: "failed",
|
|
125
|
+
trustClass: "guardian",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 4. Hash + find session
|
|
130
|
+
const challengeHash = hashVerificationSecret(code);
|
|
131
|
+
const session = await findSessionByHash(sourceChannel, challengeHash);
|
|
132
|
+
|
|
133
|
+
if (!session) {
|
|
134
|
+
await recordInvalidAttempt(sourceChannel, canonicalUserId, actorChatId);
|
|
135
|
+
log.info(
|
|
136
|
+
{ sourceChannel, actorExternalUserId: canonicalUserId },
|
|
137
|
+
"Verification code did not match any pending session",
|
|
138
|
+
);
|
|
139
|
+
await replyWithFailure(
|
|
140
|
+
replyCallbackUrl,
|
|
141
|
+
actorChatId,
|
|
142
|
+
assistantId,
|
|
143
|
+
"The verification code is invalid or has expired.",
|
|
144
|
+
);
|
|
145
|
+
return {
|
|
146
|
+
intercepted: true,
|
|
147
|
+
outcome: "failed",
|
|
148
|
+
trustClass: "guardian",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 5. Identity binding check (outbound sessions)
|
|
153
|
+
if (!checkIdentityMatch(session, canonicalUserId, actorChatId)) {
|
|
154
|
+
await recordInvalidAttempt(sourceChannel, canonicalUserId, actorChatId);
|
|
155
|
+
log.info(
|
|
156
|
+
{ sourceChannel, sessionId: session.id },
|
|
157
|
+
"Verification identity mismatch (anti-oracle: same error as invalid code)",
|
|
158
|
+
);
|
|
159
|
+
await replyWithFailure(
|
|
160
|
+
replyCallbackUrl,
|
|
161
|
+
actorChatId,
|
|
162
|
+
assistantId,
|
|
163
|
+
"The verification code is invalid or has expired.",
|
|
164
|
+
);
|
|
165
|
+
return {
|
|
166
|
+
intercepted: true,
|
|
167
|
+
outcome: "failed",
|
|
168
|
+
trustClass: session.verificationPurpose === "trusted_contact"
|
|
169
|
+
? "trusted_contact"
|
|
170
|
+
: "guardian",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 6. Consume session (atomic — only the first consumer wins)
|
|
175
|
+
const consumed = await consumeSession(
|
|
176
|
+
session.id,
|
|
177
|
+
canonicalUserId,
|
|
178
|
+
actorChatId,
|
|
179
|
+
);
|
|
180
|
+
if (!consumed) {
|
|
181
|
+
log.warn(
|
|
182
|
+
{ sessionId: session.id },
|
|
183
|
+
"Session already consumed by concurrent request",
|
|
184
|
+
);
|
|
185
|
+
await replyWithFailure(
|
|
186
|
+
replyCallbackUrl,
|
|
187
|
+
actorChatId,
|
|
188
|
+
assistantId,
|
|
189
|
+
"The verification code is invalid or has expired.",
|
|
190
|
+
);
|
|
191
|
+
return {
|
|
192
|
+
intercepted: true,
|
|
193
|
+
outcome: "failed",
|
|
194
|
+
trustClass: session.verificationPurpose === "trusted_contact"
|
|
195
|
+
? "trusted_contact"
|
|
196
|
+
: "guardian",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Reset rate limits on success
|
|
201
|
+
await resetRateLimit(sourceChannel, canonicalUserId, actorChatId);
|
|
202
|
+
|
|
203
|
+
const trustClass: "guardian" | "trusted_contact" =
|
|
204
|
+
session.verificationPurpose === "trusted_contact"
|
|
205
|
+
? "trusted_contact"
|
|
206
|
+
: "guardian";
|
|
207
|
+
|
|
208
|
+
// 7. Apply side effects
|
|
209
|
+
if (trustClass === "guardian") {
|
|
210
|
+
await applyGuardianSideEffects({
|
|
211
|
+
sourceChannel,
|
|
212
|
+
canonicalUserId,
|
|
213
|
+
actorChatId,
|
|
214
|
+
actorDisplayName,
|
|
215
|
+
actorUsername,
|
|
216
|
+
});
|
|
217
|
+
} else {
|
|
218
|
+
await applyTrustedContactSideEffects({
|
|
219
|
+
sourceChannel,
|
|
220
|
+
canonicalUserId,
|
|
221
|
+
actorChatId,
|
|
222
|
+
actorDisplayName,
|
|
223
|
+
actorUsername,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 8. Deliver success reply
|
|
228
|
+
if (replyCallbackUrl) {
|
|
229
|
+
const replyText = composeVerificationSuccessReply(trustClass);
|
|
230
|
+
await deliverVerificationReply({
|
|
231
|
+
replyCallbackUrl,
|
|
232
|
+
chatId: actorChatId,
|
|
233
|
+
text: replyText,
|
|
234
|
+
assistantId,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
log.info(
|
|
239
|
+
{
|
|
240
|
+
sourceChannel,
|
|
241
|
+
actorExternalUserId: canonicalUserId,
|
|
242
|
+
trustClass,
|
|
243
|
+
sessionId: session.id,
|
|
244
|
+
},
|
|
245
|
+
"Text verification succeeded",
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return { intercepted: true, outcome: "verified", trustClass };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Side effects
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
async function applyGuardianSideEffects(params: {
|
|
256
|
+
sourceChannel: string;
|
|
257
|
+
canonicalUserId: string;
|
|
258
|
+
actorChatId: string;
|
|
259
|
+
actorDisplayName?: string;
|
|
260
|
+
actorUsername?: string;
|
|
261
|
+
}): Promise<void> {
|
|
262
|
+
const {
|
|
263
|
+
sourceChannel,
|
|
264
|
+
canonicalUserId,
|
|
265
|
+
actorChatId,
|
|
266
|
+
actorDisplayName,
|
|
267
|
+
actorUsername,
|
|
268
|
+
} = params;
|
|
269
|
+
|
|
270
|
+
// Check for binding conflict — another user already holds guardian
|
|
271
|
+
const existing = await getExistingGuardianBinding(sourceChannel);
|
|
272
|
+
if (existing?.externalUserId && existing.externalUserId !== canonicalUserId) {
|
|
273
|
+
log.warn(
|
|
274
|
+
{
|
|
275
|
+
sourceChannel,
|
|
276
|
+
existingGuardian: existing.externalUserId,
|
|
277
|
+
newActor: canonicalUserId,
|
|
278
|
+
},
|
|
279
|
+
"Guardian binding conflict: another user already holds this channel",
|
|
280
|
+
);
|
|
281
|
+
// Still upsert the contact channel so the sender is a known contact,
|
|
282
|
+
// but skip guardian binding creation.
|
|
283
|
+
await upsertVerifiedContactChannel({
|
|
284
|
+
sourceChannel,
|
|
285
|
+
externalUserId: canonicalUserId,
|
|
286
|
+
externalChatId: actorChatId,
|
|
287
|
+
displayName: actorDisplayName,
|
|
288
|
+
username: actorUsername,
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Revoke existing binding (same-user re-verification)
|
|
294
|
+
await revokeExistingChannelGuardian(sourceChannel);
|
|
295
|
+
|
|
296
|
+
// Resolve canonical principal — unify all channel bindings
|
|
297
|
+
const canonicalPrincipal = await resolveCanonicalPrincipal(canonicalUserId);
|
|
298
|
+
|
|
299
|
+
// Determine display name — preserve existing if user is re-verifying
|
|
300
|
+
const existingContact = await findContactChannelByExternalUserId(
|
|
301
|
+
sourceChannel,
|
|
302
|
+
canonicalUserId,
|
|
303
|
+
);
|
|
304
|
+
const displayName =
|
|
305
|
+
existingContact?.displayName?.trim().length
|
|
306
|
+
? existingContact.displayName
|
|
307
|
+
: actorDisplayName ?? actorUsername ?? canonicalUserId;
|
|
308
|
+
|
|
309
|
+
// Create guardian binding (dual-writes to both DBs)
|
|
310
|
+
await createGuardianBinding({
|
|
311
|
+
channel: sourceChannel,
|
|
312
|
+
externalUserId: canonicalUserId,
|
|
313
|
+
deliveryChatId: actorChatId,
|
|
314
|
+
guardianPrincipalId: canonicalPrincipal,
|
|
315
|
+
displayName,
|
|
316
|
+
verifiedVia: "challenge",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function applyTrustedContactSideEffects(params: {
|
|
321
|
+
sourceChannel: string;
|
|
322
|
+
canonicalUserId: string;
|
|
323
|
+
actorChatId: string;
|
|
324
|
+
actorDisplayName?: string;
|
|
325
|
+
actorUsername?: string;
|
|
326
|
+
}): Promise<void> {
|
|
327
|
+
const {
|
|
328
|
+
sourceChannel,
|
|
329
|
+
canonicalUserId,
|
|
330
|
+
actorChatId,
|
|
331
|
+
actorDisplayName,
|
|
332
|
+
actorUsername,
|
|
333
|
+
} = params;
|
|
334
|
+
|
|
335
|
+
// Preserve existing display name if available
|
|
336
|
+
const existingContact = await findContactChannelByExternalUserId(
|
|
337
|
+
sourceChannel,
|
|
338
|
+
canonicalUserId,
|
|
339
|
+
);
|
|
340
|
+
const displayName =
|
|
341
|
+
existingContact?.displayName?.trim().length
|
|
342
|
+
? existingContact.displayName
|
|
343
|
+
: actorDisplayName ?? actorUsername ?? canonicalUserId;
|
|
344
|
+
|
|
345
|
+
await upsertVerifiedContactChannel({
|
|
346
|
+
sourceChannel,
|
|
347
|
+
externalUserId: canonicalUserId,
|
|
348
|
+
externalChatId: actorChatId,
|
|
349
|
+
displayName,
|
|
350
|
+
username: actorUsername,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// Reply helpers
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
async function replyWithFailure(
|
|
359
|
+
replyCallbackUrl: string | undefined,
|
|
360
|
+
chatId: string,
|
|
361
|
+
assistantId: string | undefined,
|
|
362
|
+
reason: string,
|
|
363
|
+
): Promise<void> {
|
|
364
|
+
if (!replyCallbackUrl) return;
|
|
365
|
+
const text = composeVerificationFailureReply(reason);
|
|
366
|
+
await deliverVerificationReply({
|
|
367
|
+
replyCallbackUrl,
|
|
368
|
+
chatId,
|
|
369
|
+
text,
|
|
370
|
+
assistantId,
|
|
371
|
+
});
|
|
372
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEV_VERSION_SENTINEL = "0.0.0-dev";
|
|
5
|
+
|
|
6
|
+
function readPackageVersion(): string | undefined {
|
|
7
|
+
try {
|
|
8
|
+
const pkgPath = join(
|
|
9
|
+
import.meta.dirname ?? __dirname,
|
|
10
|
+
"..",
|
|
11
|
+
"package.json",
|
|
12
|
+
);
|
|
13
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
14
|
+
if (pkg.version && typeof pkg.version === "string") return pkg.version;
|
|
15
|
+
} catch {
|
|
16
|
+
// package.json missing or unreadable
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveVersion(): string {
|
|
22
|
+
const envVersion = process.env.APP_VERSION;
|
|
23
|
+
if (envVersion && envVersion !== DEV_VERSION_SENTINEL) return envVersion;
|
|
24
|
+
return readPackageVersion() ?? DEV_VERSION_SENTINEL;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const APP_VERSION: string = resolveVersion();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Header name for the assistant version returned in every gateway response.
|
|
31
|
+
* Allows the platform to trace which build handled a request.
|
|
32
|
+
*/
|
|
33
|
+
export const VERSION_HEADER_NAME = "X-Vellum-Assistant-Version";
|
|
34
|
+
|
|
35
|
+
export const VERSION_HEADER_VALUE: string = APP_VERSION;
|