@mycadet/gateway 0.12.7 → 0.12.8
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/README.md +3 -3
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/{platform-credential.ts → cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/ces-client/src/rpc-client.ts +1 -1
- package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/{platform-credential.ts → cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/gateway-client/src/binding-strength-contract.ts +5 -5
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +1 -1
- package/node_modules/@mycadet/gateway-client/src/inbound-contract.ts +2 -2
- package/node_modules/@mycadet/gateway-client/src/outbound-contract.ts +1 -1
- package/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/service-contracts/src/{platform-credential.ts → cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/package.json +1 -1
- package/src/__tests__/actor-token-revocation.test.ts +0 -1
- package/src/__tests__/{backup-worker-platform-mode.test.ts → backup-worker-cloud-mode.test.ts} +4 -4
- package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
- package/src/__tests__/{platform-identity.test.ts → cloud-identity.test.ts} +72 -86
- package/src/__tests__/config.test.ts +0 -24
- package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/credential-requests.test.ts +6 -6
- package/src/__tests__/{credential-watcher-managed-bootstrap.test.ts → credential-watcher-cloud-bootstrap.test.ts} +1 -1
- package/src/__tests__/edge-auth.test.ts +18 -18
- package/src/__tests__/edge-guardian-auth.test.ts +10 -10
- package/src/__tests__/guardian-init-lockfile.test.ts +6 -8
- package/src/__tests__/guardian-pin.test.ts +8 -8
- package/src/__tests__/handle-inbound-admission.test.ts +0 -1
- package/src/__tests__/handle-inbound-app-context.test.ts +0 -1
- package/src/__tests__/handle-inbound-invite-intercept.test.ts +0 -1
- package/src/__tests__/handle-inbound-trust-verdict.test.ts +0 -1
- package/src/__tests__/inbound-contact-seed.test.ts +1 -1
- package/src/__tests__/live-voice-websocket.test.ts +12 -12
- 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__/remote-feature-flag-sync.test.ts +35 -45
- package/src/__tests__/resolve-assistant.test.ts +0 -1
- package/src/__tests__/route-schema-guard.test.ts +4 -1
- package/src/__tests__/runtime-client.test.ts +1 -2
- package/src/__tests__/runtime-health-proxy.test.ts +0 -1
- package/src/__tests__/runtime-proxy-auth.test.ts +3 -2
- package/src/__tests__/runtime-proxy-device-stamp.test.ts +0 -1
- package/src/__tests__/runtime-proxy-error-body.test.ts +0 -1
- package/src/__tests__/runtime-proxy.test.ts +0 -1
- package/src/__tests__/runtime-stream-test-utils.ts +0 -1
- package/src/__tests__/shared-invite-registration.test.ts +15 -32
- package/src/__tests__/slack-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/slack-display-name.test.ts +0 -1
- package/src/__tests__/slack-normalize.test.ts +0 -1
- package/src/__tests__/slack-reaction-normalize.test.ts +0 -1
- package/src/__tests__/slack-socket-mode-catchup.test.ts +0 -1
- package/src/__tests__/slack-socket-mode-group-dm.test.ts +0 -1
- package/src/__tests__/slack-socket-mode-liveness.test.ts +0 -1
- package/src/__tests__/slack-socket-mode-outbound-root.test.ts +0 -1
- package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +0 -1
- package/src/__tests__/speech-relay-websocket.test.ts +16 -14
- package/src/__tests__/stt-stream-websocket.test.ts +5 -5
- package/src/__tests__/telegram-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/telegram-webhook-drop-visibility.test.ts +0 -1
- package/src/__tests__/telegram-webhook-handler.test.ts +0 -1
- package/src/__tests__/telegram-webhook-manager.test.ts +79 -80
- package/src/__tests__/twilio-media-websocket.test.ts +0 -1
- package/src/__tests__/twilio-webhooks.test.ts +6 -7
- package/src/__tests__/whatsapp-download.test.ts +0 -1
- package/src/__tests__/whatsapp-webhook.test.ts +0 -1
- package/src/attachments/ingest.test.ts +0 -1
- package/src/auth/guardian-bootstrap.ts +17 -15
- package/src/auth/scopes.ts +1 -1
- package/src/auth-fallback-reporter.ts +1 -1
- package/src/backup/backup-worker.ts +2 -2
- package/src/channels/inbound-event.ts +3 -3
- package/src/channels/ingress-inbound-vendors.test.ts +2 -2
- package/src/channels/ingress-inbound.ts +1 -1
- package/src/channels/ingress-verification.test.ts +2 -2
- package/src/channels/ingress-verification.ts +4 -4
- package/src/channels/plugin-ingress-approvals.test.ts +1 -1
- package/src/channels/plugin-ingress-approvals.ts +1 -1
- package/src/channels/plugin-ingress.test.ts +3 -3
- package/src/channels/plugin-ingress.ts +3 -3
- package/src/channels/room-admission.ts +2 -2
- package/src/{platform-identity.ts → cloud-identity.ts} +60 -68
- package/src/cloud-url.ts +14 -0
- package/src/{platform-user-id.ts → cloud-user-id.ts} +4 -4
- package/src/config.ts +2 -4
- package/src/credential-reader.ts +2 -2
- package/src/db/assistant-db-proxy.ts +1 -1
- package/src/discord/forward.ts +1 -1
- package/src/email/normalize.ts +4 -4
- package/src/email/register-callback.test.ts +27 -34
- package/src/email/register-callback.ts +21 -26
- package/src/feature-flag-registry.json +5 -5
- package/src/feature-flag-remote-store.ts +2 -2
- package/src/feature-flag-resolver.ts +5 -5
- package/src/http/cadet-signature.ts +1 -1
- package/src/http/edge-forwarded-header.ts +1 -1
- package/src/http/loopback-guard.ts +1 -1
- package/src/http/middleware/auth.ts +22 -22
- package/src/http/router.ts +1 -1
- package/src/http/routes/cadet-identity.ts +2 -2
- package/src/http/routes/channel-ingress-routes.ts +1 -1
- package/src/http/routes/channel-ingress.test.ts +13 -2
- package/src/http/routes/channel-verification-session-proxy.ts +9 -9
- package/src/http/routes/{platform-push-proxy.test.ts → cloud-push-proxy.test.ts} +21 -22
- package/src/http/routes/{platform-push-proxy.ts → cloud-push-proxy.ts} +13 -14
- package/src/http/routes/contacts-control-plane-proxy.ts +2 -2
- package/src/http/routes/email-webhook.test.ts +2 -3
- package/src/http/routes/email-webhook.ts +4 -4
- package/src/http/routes/guardian-channel-create.ts +3 -3
- package/src/http/routes/guardian-pin.ts +9 -9
- package/src/http/routes/inbound-register.ts +2 -2
- package/src/http/routes/ingress-domains.test.ts +129 -0
- package/src/http/routes/ingress-domains.ts +73 -0
- package/src/http/routes/live-voice-websocket.ts +2 -2
- package/src/http/routes/log-export.test.ts +0 -1
- package/src/http/routes/log-tail.test.ts +0 -1
- package/src/http/routes/migration-proxy.ts +1 -1
- package/src/http/routes/plugin-webhook-websocket.test.ts +3 -3
- package/src/http/routes/plugin-webhook.test.ts +9 -8
- package/src/http/routes/plugin-webhook.ts +1 -1
- package/src/http/routes/speech-relay-websocket.ts +8 -11
- package/src/http/routes/stt-stream-websocket.ts +1 -1
- package/src/http/routes/telegram-webhook.test.ts +0 -1
- package/src/http/routes/twilio-voice-verify-callback.ts +3 -3
- package/src/http/routes/twilio-voice-webhook.test.ts +3 -4
- package/src/http/routes/twilio-voice-webhook.ts +3 -3
- package/src/http/routes/whatsapp-webhook.test.ts +0 -1
- package/src/index.ts +57 -56
- package/src/ipc/credential-request-handlers.ts +4 -4
- package/src/ipc/debug-export-handlers.test.ts +0 -1
- package/src/ipc/log-tail-handlers.test.ts +0 -1
- package/src/ipc/risk-classification-handlers.test.ts +4 -4
- package/src/ipc/risk-classification-handlers.ts +2 -2
- package/src/remote-feature-flag-sync.ts +16 -18
- package/src/risk/command-registry/commands/assistant.ts +2 -2
- package/src/risk/skill-risk-classifier.test.ts +24 -24
- package/src/risk/skill-risk-classifier.ts +10 -10
- package/src/runtime/client.ts +17 -17
- package/src/schema.ts +5 -5
- package/src/telegram/send.test.ts +0 -1
- package/src/telegram/webhook-manager.ts +33 -38
- package/src/tunnel/allowed-paths.test.ts +3 -3
- package/src/tunnel/allowed-paths.ts +1 -1
- package/src/tunnel/client.test.ts +20 -21
- package/src/tunnel/client.ts +17 -17
- package/src/verification/shared-invite-registration.ts +22 -17
- package/src/version.ts +1 -1
- package/src/platform-url.ts +0 -24
|
@@ -7,13 +7,13 @@ import { credentialKey } from "../credential-key.js";
|
|
|
7
7
|
import { registerWebhookIngressRoute } from "../db/webhook-ingress-route-store.js";
|
|
8
8
|
import { fetchImpl } from "../fetch.js";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
areCloudFeaturesEnabled,
|
|
11
11
|
isFeatureFlagEnabled,
|
|
12
|
-
|
|
12
|
+
isCloudServedBuild,
|
|
13
13
|
} from "../feature-flag-resolver.js";
|
|
14
14
|
import { callTelegramApi } from "./api.js";
|
|
15
15
|
import { getLogger } from "../logger.js";
|
|
16
|
-
import {
|
|
16
|
+
import { resolveCloudAssistantIdOrUndefined } from "../cloud-identity.js";
|
|
17
17
|
|
|
18
18
|
const log = getLogger("webhook-manager");
|
|
19
19
|
const TELEGRAM_CALLBACK_PATH = "webhooks/telegram";
|
|
@@ -46,26 +46,21 @@ interface PlatformCallbackRouteResponse {
|
|
|
46
46
|
async function registerManagedTelegramCallbackRoute(
|
|
47
47
|
caches?: WebhookManagerCaches,
|
|
48
48
|
): Promise<string | undefined> {
|
|
49
|
-
if (!
|
|
49
|
+
if (!areCloudFeaturesEnabled()) {
|
|
50
50
|
log.debug(
|
|
51
51
|
"Platform features disabled — skipping managed Telegram callback registration",
|
|
52
52
|
);
|
|
53
53
|
return undefined;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const
|
|
57
|
-
? await
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// matching the daemon's resolvePlatformCallbackRegistrationContext().
|
|
65
|
-
const platformBaseUrl = (
|
|
66
|
-
platformBaseUrlRaw?.trim() ||
|
|
67
|
-
process.env.CADET_PLATFORM_URL?.trim() ||
|
|
68
|
-
""
|
|
56
|
+
const assistantApiKeyRaw = caches?.credentials
|
|
57
|
+
? await caches.credentials.get(credentialKey("cadet", "assistant_api_key"))
|
|
58
|
+
: undefined;
|
|
59
|
+
|
|
60
|
+
// The base URL resolves from the environment only — the credential store
|
|
61
|
+
// never carries it.
|
|
62
|
+
const cloudBaseUrl = (
|
|
63
|
+
process.env.CADET_CLOUD_URL?.trim() || ""
|
|
69
64
|
).replace(/\/+$/, "");
|
|
70
65
|
|
|
71
66
|
const assistantCredential =
|
|
@@ -73,12 +68,12 @@ async function registerManagedTelegramCallbackRoute(
|
|
|
73
68
|
process.env.ASSISTANT_API_KEY?.trim() ||
|
|
74
69
|
undefined;
|
|
75
70
|
|
|
76
|
-
const assistantId = await
|
|
71
|
+
const assistantId = await resolveCloudAssistantIdOrUndefined();
|
|
77
72
|
|
|
78
|
-
if (!
|
|
73
|
+
if (!cloudBaseUrl || !assistantCredential || !assistantId) {
|
|
79
74
|
log.debug(
|
|
80
75
|
{
|
|
81
|
-
hasPlatformBaseUrl: !!
|
|
76
|
+
hasPlatformBaseUrl: !!cloudBaseUrl,
|
|
82
77
|
hasApiKey: !!assistantCredential,
|
|
83
78
|
hasAssistantId: !!assistantId,
|
|
84
79
|
},
|
|
@@ -104,8 +99,8 @@ async function registerManagedTelegramCallbackRoute(
|
|
|
104
99
|
log.debug("Could not resolve Telegram bot username for source_identifier");
|
|
105
100
|
}
|
|
106
101
|
|
|
107
|
-
// Self-hosted assistants send their public ingress URL so the
|
|
108
|
-
// can register a callback that points at this gateway.
|
|
102
|
+
// Self-hosted assistants send their public ingress URL so the cloud
|
|
103
|
+
// can register a callback that points at this gateway. Cloud pods may
|
|
109
104
|
// also include it; Django ignores a client-provided base for those.
|
|
110
105
|
const ingressUrl = readIngressBaseUrl(caches);
|
|
111
106
|
|
|
@@ -122,7 +117,7 @@ async function registerManagedTelegramCallbackRoute(
|
|
|
122
117
|
}
|
|
123
118
|
|
|
124
119
|
const response = await fetchImpl(
|
|
125
|
-
`${
|
|
120
|
+
`${cloudBaseUrl}/v1/internal/gateway/callback-routes/register/`,
|
|
126
121
|
{
|
|
127
122
|
method: "POST",
|
|
128
123
|
headers: {
|
|
@@ -177,7 +172,7 @@ function readIngressBaseUrl(caches?: WebhookManagerCaches): string | undefined {
|
|
|
177
172
|
* path is unclaimed would register a webhook that every delivery bounces off.
|
|
178
173
|
*
|
|
179
174
|
* Returns undefined when the tunnel has published no URL yet or the claim
|
|
180
|
-
* fails, which leaves the caller on the
|
|
175
|
+
* fails, which leaves the caller on the cloud callback route.
|
|
181
176
|
*/
|
|
182
177
|
function resolveTunnelTelegramWebhookUrl(
|
|
183
178
|
caches?: WebhookManagerCaches,
|
|
@@ -195,7 +190,7 @@ function resolveTunnelTelegramWebhookUrl(
|
|
|
195
190
|
} catch (err) {
|
|
196
191
|
log.error(
|
|
197
192
|
{ err },
|
|
198
|
-
"Could not claim the Telegram webhook path in the ingress registry; falling back to the
|
|
193
|
+
"Could not claim the Telegram webhook path in the ingress registry; falling back to the cloud callback route",
|
|
199
194
|
);
|
|
200
195
|
return undefined;
|
|
201
196
|
}
|
|
@@ -213,33 +208,33 @@ async function resolveExpectedTelegramWebhookUrl(
|
|
|
213
208
|
// resolver declines makes setup report success while setWebhook never runs
|
|
214
209
|
// (LUM-2899).
|
|
215
210
|
//
|
|
216
|
-
// 1.
|
|
217
|
-
// the
|
|
211
|
+
// 1. Cloud pods (`IS_CLOUD`) with `tunnel-webhooks` off always use
|
|
212
|
+
// the cloud callback route and never consult ingress at all. A pod's
|
|
218
213
|
// `ingress.publicBaseUrl` is written by the Tunnel client, not by
|
|
219
214
|
// a user, and that address is only live while the tunnel is:
|
|
220
215
|
// `clearManagedPublicBaseUrl` wipes the key when the tunnel drops, but
|
|
221
216
|
// Telegram keeps delivering to whatever was last registered, so a pod
|
|
222
217
|
// that resolved through the address alone would be pointed at a dead
|
|
223
|
-
// one. The
|
|
218
|
+
// one. The cloud callback route is the pod's stable inbound address.
|
|
224
219
|
// 2. With the flag on, a pod points Telegram at its published Tunnel URL
|
|
225
220
|
// and claims that path in the ingress registry first, so the tunnel
|
|
226
221
|
// edge and the bridge both admit it. A pod with no published URL, or a
|
|
227
|
-
// claim that fails, still falls back to the
|
|
222
|
+
// claim that fails, still falls back to the cloud callback route.
|
|
228
223
|
// Both the tunnel publishing a URL and the flag flipping re-run this
|
|
229
224
|
// reconciliation, so a pod moves between the two addresses in either
|
|
230
225
|
// direction without a restart.
|
|
231
226
|
// 3. An explicit `ingress.enabled: false` is a decision not to accept
|
|
232
227
|
// inbound webhooks at all; it precedes both tiers below and actively
|
|
233
228
|
// deregisters, so `reconcileTelegramWebhook` handles it before calling
|
|
234
|
-
// this resolver.
|
|
229
|
+
// this resolver. Cloud pods are exempt (see the comment there).
|
|
235
230
|
// 4. A configured public ingress URL wins (a self-hosted tunnel, or the
|
|
236
231
|
// Tunnel-published URL while the tunnel is registered).
|
|
237
232
|
// 5. Platform-connected local assistants holding cadet credentials fall
|
|
238
|
-
// back to a
|
|
233
|
+
// back to a cloud platform callback route.
|
|
239
234
|
// `registerManagedTelegramCallbackRoute` self-gates on platform
|
|
240
235
|
// features and credential presence, so a gateway with no platform
|
|
241
236
|
// context resolves to undefined and reconciliation skips.
|
|
242
|
-
if (
|
|
237
|
+
if (isCloudServedBuild()) {
|
|
243
238
|
if (isFeatureFlagEnabled("tunnel-webhooks")) {
|
|
244
239
|
const tunnelUrl = resolveTunnelTelegramWebhookUrl(caches);
|
|
245
240
|
if (tunnelUrl) {
|
|
@@ -269,7 +264,7 @@ let reconcileDrain: Promise<void> | undefined;
|
|
|
269
264
|
* resolves depends on config that moves underneath it. A Tunnel rules refresh
|
|
270
265
|
* clears `ingress.publicBaseUrl` and republishes it moments later, and each
|
|
271
266
|
* write fires a config change. Run concurrently, the clear-time reconciliation
|
|
272
|
-
* resolves the
|
|
267
|
+
* resolves the cloud callback route while the republish-time one resolves
|
|
273
268
|
* Tunnel, and whichever setWebhook lands last decides where Telegram delivers.
|
|
274
269
|
* A stale winner leaves the pod on the fallback address until some later
|
|
275
270
|
* trigger happens to correct it.
|
|
@@ -375,13 +370,13 @@ async function reconcileTelegramWebhookNow(
|
|
|
375
370
|
// idempotent, and pending updates are deliberately kept so re-enabling
|
|
376
371
|
// ingress does not lose queued messages.
|
|
377
372
|
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
373
|
+
// Cloud pods are exempt: `hasWebhookRoutingConfigured` resolves the
|
|
374
|
+
// IS_CLOUD tier ahead of the explicit-disable check because a pod has no
|
|
380
375
|
// self-owned ingress to disable, so honoring a stray flag here would delete
|
|
381
376
|
// the pod's only delivery path while the daemon still reports the channel
|
|
382
377
|
// as configured (the LUM-2899 divergence in mirror image).
|
|
383
378
|
if (
|
|
384
|
-
!
|
|
379
|
+
!isCloudServedBuild() &&
|
|
385
380
|
caches?.configFile?.getBoolean("ingress", "enabled") === false
|
|
386
381
|
) {
|
|
387
382
|
await callTelegramApi("deleteWebhook", {}, apiOpts);
|
|
@@ -399,8 +394,8 @@ async function reconcileTelegramWebhookNow(
|
|
|
399
394
|
try {
|
|
400
395
|
expectedUrl = await resolveExpectedTelegramWebhookUrl(caches);
|
|
401
396
|
} catch (err) {
|
|
402
|
-
// Managed callback route registration failed — this is a
|
|
403
|
-
// issue. Do not suggest
|
|
397
|
+
// Managed callback route registration failed — this is a cloud-side
|
|
398
|
+
// issue. Do not suggest third-party tunnel options; they are not
|
|
404
399
|
// usable in containerized deployments.
|
|
405
400
|
const detail = err instanceof Error ? err.message : String(err);
|
|
406
401
|
log.error(
|
|
@@ -28,7 +28,7 @@ beforeEach(() => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
describe("TUNNEL_ALLOWED_PATHS", () => {
|
|
31
|
-
it("matches the
|
|
31
|
+
it("matches the cloud-side header name (must stay in sync with mycadet-platform RegistrationAllowedPathsHeader)", () => {
|
|
32
32
|
expect(TUNNEL_ALLOWED_PATHS_HEADER).toBe("X-Cadet-Tunnel-Allowed-Paths");
|
|
33
33
|
});
|
|
34
34
|
|
|
@@ -57,7 +57,7 @@ describe("TUNNEL_ALLOWED_PATHS", () => {
|
|
|
57
57
|
for (const pattern of patterns) {
|
|
58
58
|
expect(() => new RegExp(pattern)).not.toThrow();
|
|
59
59
|
// RE2-incompatible features that should never appear: lookahead,
|
|
60
|
-
// lookbehind, backreferences. A simple guard is enough — the
|
|
60
|
+
// lookbehind, backreferences. A simple guard is enough — the cloud
|
|
61
61
|
// side will reject anything Go's regexp.Compile can't parse.
|
|
62
62
|
expect(pattern).not.toMatch(/\(\?[=!<]/); // lookaround
|
|
63
63
|
expect(pattern).not.toMatch(/\\[1-9]/); // backreferences
|
|
@@ -85,7 +85,7 @@ describe("TUNNEL_ALLOWED_PATHS", () => {
|
|
|
85
85
|
"/v1/audio/some-uuid.mp3": true,
|
|
86
86
|
"/v1/live-voice": true,
|
|
87
87
|
"/v1/stt/stream": true,
|
|
88
|
-
// Watch sessions on a
|
|
88
|
+
// Watch sessions on a cloud assistant ride the tunnel, the same shape
|
|
89
89
|
// as live voice: tunnel validates the browser's minted token on this path
|
|
90
90
|
// and injects the attested caller, and the gateway's handler admits only
|
|
91
91
|
// the guardian on it. Self-hosted assistants bypass tunnel entirely.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tunnel path allowlist sent to Tunnel on the WS upgrade request via the
|
|
3
3
|
* {@link TUNNEL_ALLOWED_PATHS_HEADER} HTTP header. Tunnel parses the JSON-encoded
|
|
4
|
-
* regex array on the
|
|
4
|
+
* regex array on the cloud side
|
|
5
5
|
* ({@link
|
|
6
6
|
* https://github.com/vargasjr-dev/mycadet-platform/blob/main/tunnel/internal/tunnel/protocol.go
|
|
7
7
|
* `RegistrationAllowedPathsHeader`})
|
|
@@ -81,8 +81,8 @@ const {
|
|
|
81
81
|
} = await import("./allowed-paths.js");
|
|
82
82
|
const { TunnelClient, createTunnelClient, enablePublicIngress } =
|
|
83
83
|
await import("./client.js");
|
|
84
|
-
const {
|
|
85
|
-
await import("../
|
|
84
|
+
const { applyCloudIdentityIds, _resetCloudIdentityForTests } =
|
|
85
|
+
await import("../cloud-identity.js");
|
|
86
86
|
|
|
87
87
|
const WS_OPEN = WebSocket.OPEN;
|
|
88
88
|
const WS_CLOSED = WebSocket.CLOSED;
|
|
@@ -91,7 +91,7 @@ function makeCredentials(values: Record<string, string | undefined>) {
|
|
|
91
91
|
const assistantId =
|
|
92
92
|
values[credentialKey("cadet", "platform_assistant_id")]?.trim();
|
|
93
93
|
if (assistantId) {
|
|
94
|
-
|
|
94
|
+
applyCloudIdentityIds({
|
|
95
95
|
assistantId,
|
|
96
96
|
organizationId: "",
|
|
97
97
|
userId: "",
|
|
@@ -178,7 +178,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
178
178
|
runtimeMaxRetries: 0,
|
|
179
179
|
runtimeProxyRequireAuth: true,
|
|
180
180
|
runtimeTimeoutMs: 1,
|
|
181
|
-
shutdownDrainMs: 1,
|
|
182
181
|
trustProxy: false,
|
|
183
182
|
...overrides,
|
|
184
183
|
};
|
|
@@ -270,10 +269,10 @@ async function flushPromises(): Promise<void> {
|
|
|
270
269
|
}
|
|
271
270
|
|
|
272
271
|
const originalAssistantApiKey = process.env.ASSISTANT_API_KEY;
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
const
|
|
272
|
+
const originalCloudUrl = process.env.CADET_CLOUD_URL;
|
|
273
|
+
const originalCloudAssistantId = process.env.CLOUD_ASSISTANT_ID;
|
|
274
|
+
const originalCloudOrgId = process.env.CLOUD_ORGANIZATION_ID;
|
|
275
|
+
const originalCloudUserId = process.env.CLOUD_USER_ID;
|
|
277
276
|
|
|
278
277
|
function restoreEnv(name: string, value: string | undefined): void {
|
|
279
278
|
if (value === undefined) {
|
|
@@ -291,21 +290,21 @@ beforeEach(() => {
|
|
|
291
290
|
webhookRouteReads = 0;
|
|
292
291
|
fetchImplFn = async () => new Response("not found", { status: 404 });
|
|
293
292
|
delete process.env.ASSISTANT_API_KEY;
|
|
294
|
-
delete process.env.
|
|
295
|
-
delete process.env.
|
|
296
|
-
delete process.env.
|
|
297
|
-
delete process.env.
|
|
298
|
-
|
|
293
|
+
delete process.env.CADET_CLOUD_URL;
|
|
294
|
+
delete process.env.CLOUD_ASSISTANT_ID;
|
|
295
|
+
delete process.env.CLOUD_ORGANIZATION_ID;
|
|
296
|
+
delete process.env.CLOUD_USER_ID;
|
|
297
|
+
_resetCloudIdentityForTests();
|
|
299
298
|
});
|
|
300
299
|
|
|
301
300
|
afterEach(() => {
|
|
302
301
|
rmSync(workspaceDir, { recursive: true, force: true });
|
|
303
302
|
restoreEnv("ASSISTANT_API_KEY", originalAssistantApiKey);
|
|
304
|
-
restoreEnv("
|
|
305
|
-
restoreEnv("
|
|
306
|
-
restoreEnv("
|
|
307
|
-
restoreEnv("
|
|
308
|
-
|
|
303
|
+
restoreEnv("CADET_CLOUD_URL", originalCloudUrl);
|
|
304
|
+
restoreEnv("CLOUD_ASSISTANT_ID", originalCloudAssistantId);
|
|
305
|
+
restoreEnv("CLOUD_ORGANIZATION_ID", originalCloudOrgId);
|
|
306
|
+
restoreEnv("CLOUD_USER_ID", originalCloudUserId);
|
|
307
|
+
_resetCloudIdentityForTests();
|
|
309
308
|
});
|
|
310
309
|
|
|
311
310
|
describe("enablePublicIngress", () => {
|
|
@@ -368,7 +367,7 @@ describe("TunnelClient", () => {
|
|
|
368
367
|
await client.stop();
|
|
369
368
|
});
|
|
370
369
|
|
|
371
|
-
test("opens a socket and registers when the
|
|
370
|
+
test("opens a socket and registers when the cloud assistant ID is missing", async () => {
|
|
372
371
|
const sockets: FakeWebSocket[] = [];
|
|
373
372
|
const reconnectDelays: number[] = [];
|
|
374
373
|
writeConfig({
|
|
@@ -629,7 +628,7 @@ describe("TunnelClient", () => {
|
|
|
629
628
|
test("awaits validate before rejecting a mismatched registration", async () => {
|
|
630
629
|
const sockets: FakeWebSocket[] = [];
|
|
631
630
|
process.env.ASSISTANT_API_KEY = "api-key-123";
|
|
632
|
-
process.env.
|
|
631
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
633
632
|
fetchImplFn = async () =>
|
|
634
633
|
new Response(
|
|
635
634
|
JSON.stringify({
|
|
@@ -785,7 +784,7 @@ describe("TunnelClient", () => {
|
|
|
785
784
|
resolveFirstApiKeyRead = resolve;
|
|
786
785
|
});
|
|
787
786
|
let useFreshCredentials = false;
|
|
788
|
-
|
|
787
|
+
applyCloudIdentityIds({
|
|
789
788
|
assistantId: "asst-123",
|
|
790
789
|
organizationId: "",
|
|
791
790
|
userId: "",
|
package/src/tunnel/client.ts
CHANGED
|
@@ -9,9 +9,9 @@ import { credentialKey } from "../credential-key.js";
|
|
|
9
9
|
import { mutateConfigFile } from "../config-file-utils.js";
|
|
10
10
|
import { getLogger } from "../logger.js";
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "../
|
|
12
|
+
ensureCloudIdentityIds,
|
|
13
|
+
peekCloudAssistantId,
|
|
14
|
+
} from "../cloud-identity.js";
|
|
15
15
|
import { ExponentialBackoff } from "../util/exponential-backoff.js";
|
|
16
16
|
import { listWebhookIngressRoutes } from "../db/webhook-ingress-route-store.js";
|
|
17
17
|
import {
|
|
@@ -335,8 +335,8 @@ export class TunnelClient {
|
|
|
335
335
|
this.scheduleReconnect();
|
|
336
336
|
return;
|
|
337
337
|
}
|
|
338
|
-
await
|
|
339
|
-
const expectedAssistantId =
|
|
338
|
+
await ensureCloudIdentityIds();
|
|
339
|
+
const expectedAssistantId = peekCloudAssistantId();
|
|
340
340
|
|
|
341
341
|
let registerUrl: string;
|
|
342
342
|
try {
|
|
@@ -360,7 +360,7 @@ export class TunnelClient {
|
|
|
360
360
|
// Declares the path allowlist Tunnel enforces for inbound proxied
|
|
361
361
|
// traffic on this tunnel. Read per attempt so a reconnect picks up
|
|
362
362
|
// webhook routes registered since the last one. See
|
|
363
|
-
// ./allowed-paths.ts for the route inventory and the
|
|
363
|
+
// ./allowed-paths.ts for the route inventory and the cloud-side
|
|
364
364
|
// enforcement (ATL-402).
|
|
365
365
|
[TUNNEL_ALLOWED_PATHS_HEADER]: buildTunnelAllowedPathsHeaderValue(
|
|
366
366
|
() => this.readRegisteredWebhookPaths(),
|
|
@@ -435,7 +435,7 @@ export class TunnelClient {
|
|
|
435
435
|
private async handleMessage(
|
|
436
436
|
data: unknown,
|
|
437
437
|
originWs: WebSocket,
|
|
438
|
-
|
|
438
|
+
cloudAssistantId: string | undefined,
|
|
439
439
|
): Promise<void> {
|
|
440
440
|
if (this.ws !== originWs || !this.running) return;
|
|
441
441
|
|
|
@@ -458,7 +458,7 @@ export class TunnelClient {
|
|
|
458
458
|
|
|
459
459
|
switch (frame.type) {
|
|
460
460
|
case TUNNEL_FRAME_TYPES.registered:
|
|
461
|
-
await this.handleRegisteredFrame(frame, originWs,
|
|
461
|
+
await this.handleRegisteredFrame(frame, originWs, cloudAssistantId);
|
|
462
462
|
return;
|
|
463
463
|
case TUNNEL_FRAME_TYPES.httpRequest:
|
|
464
464
|
await this.handleHttpRequestFrame(frame, originWs);
|
|
@@ -479,12 +479,12 @@ export class TunnelClient {
|
|
|
479
479
|
private async handleRegisteredFrame(
|
|
480
480
|
frame: TunnelRegisteredFrame,
|
|
481
481
|
originWs: WebSocket,
|
|
482
|
-
|
|
482
|
+
cloudAssistantId: string | undefined,
|
|
483
483
|
): Promise<void> {
|
|
484
|
-
if (
|
|
484
|
+
if (cloudAssistantId && frame.assistant_id !== cloudAssistantId) {
|
|
485
485
|
log.error(
|
|
486
486
|
{
|
|
487
|
-
expectedAssistantId:
|
|
487
|
+
expectedAssistantId: cloudAssistantId,
|
|
488
488
|
receivedAssistantId: frame.assistant_id,
|
|
489
489
|
},
|
|
490
490
|
"Tunnel registered assistant ID mismatch",
|
|
@@ -793,12 +793,12 @@ export function createTunnelClient(
|
|
|
793
793
|
},
|
|
794
794
|
): TunnelClient | undefined {
|
|
795
795
|
if (!config.tunnelBaseUrl) {
|
|
796
|
-
const
|
|
797
|
-
process.env.
|
|
798
|
-
process.env.
|
|
799
|
-
if (
|
|
796
|
+
const isCloud =
|
|
797
|
+
process.env.IS_CLOUD?.trim().toLowerCase() === "true" ||
|
|
798
|
+
process.env.IS_CLOUD?.trim() === "1";
|
|
799
|
+
if (isCloud) {
|
|
800
800
|
log.warn(
|
|
801
|
-
"TUNNEL_BASE_URL is not configured on a
|
|
801
|
+
"TUNNEL_BASE_URL is not configured on a cloud pod — the assistant tunnel will not be established and inbound webhook delivery will fail",
|
|
802
802
|
);
|
|
803
803
|
}
|
|
804
804
|
void clearManagedPublicBaseUrl(deps.configFile).catch((err) => {
|
|
@@ -845,7 +845,7 @@ async function mutateGatewayConfigFile(
|
|
|
845
845
|
|
|
846
846
|
const TUNNEL_MANAGED_BY = "tunnel";
|
|
847
847
|
|
|
848
|
-
async function writeManagedPublicBaseUrl(
|
|
848
|
+
export async function writeManagedPublicBaseUrl(
|
|
849
849
|
publicUrl: string,
|
|
850
850
|
configFile: ConfigFileCache,
|
|
851
851
|
): Promise<void> {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Registers a `cadet-shared` invite with the
|
|
3
|
-
* invitee's redemption back to this assistant. The
|
|
2
|
+
* Registers a `cadet-shared` invite with the cloud, which routes the
|
|
3
|
+
* invitee's redemption back to this assistant. The cloud receives only the
|
|
4
4
|
* link-token hash and the invite's expiry, never the token itself.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ import { credentialKey } from "../credential-key.js";
|
|
|
10
10
|
import { readCredentialResult } from "../credential-reader.js";
|
|
11
11
|
import { fetchImpl } from "../fetch.js";
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
areCloudFeaturesEnabled,
|
|
14
14
|
isFeatureFlagEnabled,
|
|
15
15
|
} from "../feature-flag-resolver.js";
|
|
16
16
|
import { getLogger } from "../logger.js";
|
|
@@ -26,9 +26,9 @@ const REGISTRATION_TIMEOUT_MS = 10_000;
|
|
|
26
26
|
export type SharedInviteRegistrationResult =
|
|
27
27
|
/** Registered, or nothing to register for this invite or gateway. */
|
|
28
28
|
| { status: "registered" | "skipped" }
|
|
29
|
-
/** The
|
|
29
|
+
/** The cloud already holds this hash. */
|
|
30
30
|
| { status: "duplicate" }
|
|
31
|
-
/** The
|
|
31
|
+
/** The cloud refused another outstanding invite. */
|
|
32
32
|
| { status: "limit_reached" }
|
|
33
33
|
/** The credential store could not be read. */
|
|
34
34
|
| { status: "unavailable" }
|
|
@@ -39,8 +39,8 @@ export type SharedInviteRegistrationResult =
|
|
|
39
39
|
* Store value when present, else the environment fallback. `unreachable` is
|
|
40
40
|
* set only when the store could not be read and no fallback exists.
|
|
41
41
|
*/
|
|
42
|
-
async function
|
|
43
|
-
field: "
|
|
42
|
+
async function readCloudCredential(
|
|
43
|
+
field: "assistant_api_key",
|
|
44
44
|
envName: string,
|
|
45
45
|
): Promise<{ value: string; unreachable: boolean }> {
|
|
46
46
|
const stored = await readCredentialResult(credentialKey("cadet", field));
|
|
@@ -49,7 +49,7 @@ async function readPlatformCredential(
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Register the invite's link-token hash and expiry with the
|
|
52
|
+
* Register the invite's link-token hash and expiry with the cloud.
|
|
53
53
|
*
|
|
54
54
|
* Skipped for any channel other than `cadet-shared`, while the
|
|
55
55
|
* trusted-contacts flag is off, when platform features are disabled, and when
|
|
@@ -63,28 +63,33 @@ export async function registerSharedInvite(params: {
|
|
|
63
63
|
if (
|
|
64
64
|
params.sourceChannel !== SHARED_INVITE_CHANNEL ||
|
|
65
65
|
!isFeatureFlagEnabled(TRUSTED_CONTACTS_FLAG) ||
|
|
66
|
-
!
|
|
66
|
+
!areCloudFeaturesEnabled()
|
|
67
67
|
) {
|
|
68
68
|
return { status: "skipped" };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const [
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const [cloudBaseUrl, apiKey] = await Promise.all([
|
|
72
|
+
Promise.resolve(
|
|
73
|
+
(process.env.CADET_CLOUD_URL?.trim() || "").replace(/\/+$/, ""),
|
|
74
|
+
),
|
|
75
|
+
readCloudCredential("assistant_api_key", "ASSISTANT_API_KEY"),
|
|
74
76
|
]);
|
|
75
|
-
if (
|
|
77
|
+
if (apiKey.unreachable) {
|
|
76
78
|
log.warn("Shared invite registration: credential store unreachable");
|
|
77
79
|
return { status: "unavailable" };
|
|
78
80
|
}
|
|
79
|
-
if (!
|
|
81
|
+
if (!cloudBaseUrl || !apiKey.value) {
|
|
80
82
|
log.debug(
|
|
81
|
-
{
|
|
83
|
+
{
|
|
84
|
+
hasPlatformBaseUrl: !!cloudBaseUrl,
|
|
85
|
+
hasApiKey: !!apiKey.value,
|
|
86
|
+
},
|
|
82
87
|
"Shared invite registration skipped: no cloud credentials",
|
|
83
88
|
);
|
|
84
89
|
return { status: "skipped" };
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
const url = `${
|
|
92
|
+
const url = `${cloudBaseUrl}${SHARED_INVITE_REGISTRATION_PATH}`;
|
|
88
93
|
let response: Response;
|
|
89
94
|
try {
|
|
90
95
|
response = await fetchImpl(url, {
|
|
@@ -109,7 +114,7 @@ export async function registerSharedInvite(params: {
|
|
|
109
114
|
}
|
|
110
115
|
log.warn(
|
|
111
116
|
{ status: response.status },
|
|
112
|
-
"Shared invite registration refused by the
|
|
117
|
+
"Shared invite registration refused by the cloud",
|
|
113
118
|
);
|
|
114
119
|
if (response.status === 409) {
|
|
115
120
|
return { status: "duplicate" };
|
package/src/version.ts
CHANGED
|
@@ -28,7 +28,7 @@ export const APP_VERSION: string = resolveVersion();
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Header name for the assistant version returned in every gateway response.
|
|
31
|
-
* Allows the
|
|
31
|
+
* Allows the cloud to trace which build handled a request.
|
|
32
32
|
*/
|
|
33
33
|
export const VERSION_HEADER_NAME = "X-Cadet-Assistant-Version";
|
|
34
34
|
|
package/src/platform-url.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { CredentialCache } from "./credential-cache.js";
|
|
2
|
-
import { credentialKey } from "./credential-key.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolve the platform base URL from the credential cache, falling back to
|
|
6
|
-
* the `CADET_PLATFORM_URL` environment variable. Returns `undefined` when
|
|
7
|
-
* neither source provides a value.
|
|
8
|
-
*
|
|
9
|
-
* The returned URL is trimmed and has any trailing slashes removed so callers
|
|
10
|
-
* can safely append path segments.
|
|
11
|
-
*/
|
|
12
|
-
export async function getPlatformBaseUrl(
|
|
13
|
-
credentials: CredentialCache,
|
|
14
|
-
): Promise<string | undefined> {
|
|
15
|
-
const raw = await credentials.get(
|
|
16
|
-
credentialKey("cadet", "platform_base_url"),
|
|
17
|
-
);
|
|
18
|
-
const url = (
|
|
19
|
-
raw?.trim() ||
|
|
20
|
-
process.env.CADET_PLATFORM_URL?.trim() ||
|
|
21
|
-
""
|
|
22
|
-
).replace(/\/+$/, "");
|
|
23
|
-
return url || undefined;
|
|
24
|
-
}
|