@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
|
@@ -24,13 +24,13 @@ mock.module("../../fetch.js", () => ({
|
|
|
24
24
|
fetchImpl: (...args: Parameters<FetchFn>) => fetchMock(...args),
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
|
-
const {
|
|
28
|
-
await import("./
|
|
29
|
-
const {
|
|
30
|
-
await import("../../
|
|
27
|
+
const { createCloudPushProxyHandler } =
|
|
28
|
+
await import("./cloud-push-proxy.js");
|
|
29
|
+
const { applyCloudIdentityIds, _resetCloudIdentityForTests } =
|
|
30
|
+
await import("../../cloud-identity.js");
|
|
31
31
|
|
|
32
32
|
const PLATFORM_ASSISTANT_ID = "11111111-1111-4111-8111-111111111111";
|
|
33
|
-
const PLATFORM_BASE_URL = "https://
|
|
33
|
+
const PLATFORM_BASE_URL = "https://test-cloud.mycadet.ai";
|
|
34
34
|
const ASSISTANT_API_KEY = "assistant-api-key-123";
|
|
35
35
|
|
|
36
36
|
function makeKeyedCredentials(
|
|
@@ -42,15 +42,14 @@ function makeKeyedCredentials(
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function registeredCredentials(): CredentialCache {
|
|
45
|
-
|
|
45
|
+
applyCloudIdentityIds({
|
|
46
46
|
assistantId: PLATFORM_ASSISTANT_ID,
|
|
47
47
|
organizationId: "",
|
|
48
48
|
userId: "",
|
|
49
49
|
});
|
|
50
|
+
process.env.CADET_CLOUD_URL = PLATFORM_BASE_URL;
|
|
50
51
|
return makeKeyedCredentials({
|
|
51
|
-
[credentialKey("cadet", "platform_base_url")]: PLATFORM_BASE_URL,
|
|
52
52
|
[credentialKey("cadet", "assistant_api_key")]: ASSISTANT_API_KEY,
|
|
53
|
-
[credentialKey("cadet", "platform_assistant_id")]: PLATFORM_ASSISTANT_ID,
|
|
54
53
|
});
|
|
55
54
|
}
|
|
56
55
|
|
|
@@ -84,27 +83,27 @@ function capturedFetch(): {
|
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
describe("platform push proxy", () => {
|
|
87
|
-
const
|
|
86
|
+
const savedCloudUrl = process.env.CADET_CLOUD_URL;
|
|
88
87
|
|
|
89
88
|
beforeAll(() => {
|
|
90
|
-
delete process.env.
|
|
89
|
+
delete process.env.CADET_CLOUD_URL;
|
|
91
90
|
});
|
|
92
91
|
|
|
93
92
|
afterAll(() => {
|
|
94
|
-
if (
|
|
95
|
-
process.env.
|
|
93
|
+
if (savedCloudUrl !== undefined) {
|
|
94
|
+
process.env.CADET_CLOUD_URL = savedCloudUrl;
|
|
96
95
|
} else {
|
|
97
|
-
delete process.env.
|
|
96
|
+
delete process.env.CADET_CLOUD_URL;
|
|
98
97
|
}
|
|
99
98
|
});
|
|
100
99
|
|
|
101
100
|
afterEach(() => {
|
|
102
101
|
fetchMock = mock(async () => new Response());
|
|
103
|
-
|
|
102
|
+
_resetCloudIdentityForTests();
|
|
104
103
|
});
|
|
105
104
|
|
|
106
105
|
test("returns 503 and does not fetch when platform credentials are missing", async () => {
|
|
107
|
-
const handler =
|
|
106
|
+
const handler = createCloudPushProxyHandler(makeKeyedCredentials({}));
|
|
108
107
|
|
|
109
108
|
const res = await handler.handleUpsertPushToken(
|
|
110
109
|
new Request("http://localhost:7830/v1/assistants/self/push-tokens/", {
|
|
@@ -131,7 +130,7 @@ describe("platform push proxy", () => {
|
|
|
131
130
|
headers: { "content-type": "application/json" },
|
|
132
131
|
}),
|
|
133
132
|
);
|
|
134
|
-
const handler =
|
|
133
|
+
const handler = createCloudPushProxyHandler(registeredCredentials());
|
|
135
134
|
const payload = {
|
|
136
135
|
token: "device-token",
|
|
137
136
|
platform: "ios",
|
|
@@ -166,7 +165,7 @@ describe("platform push proxy", () => {
|
|
|
166
165
|
|
|
167
166
|
test("preserves bundle_id on push-token delete and ignores the URL assistant id", async () => {
|
|
168
167
|
fetchMock = mock(async () => new Response(null, { status: 204 }));
|
|
169
|
-
const handler =
|
|
168
|
+
const handler = createCloudPushProxyHandler(registeredCredentials());
|
|
170
169
|
|
|
171
170
|
const res = await handler.handleDeletePushToken(
|
|
172
171
|
new Request(
|
|
@@ -195,7 +194,7 @@ describe("platform push proxy", () => {
|
|
|
195
194
|
headers: { "content-type": "application/json" },
|
|
196
195
|
}),
|
|
197
196
|
);
|
|
198
|
-
const handler =
|
|
197
|
+
const handler = createCloudPushProxyHandler(registeredCredentials());
|
|
199
198
|
|
|
200
199
|
const res = await handler.handleUpsertLiveActivityToken(
|
|
201
200
|
new Request(
|
|
@@ -217,7 +216,7 @@ describe("platform push proxy", () => {
|
|
|
217
216
|
|
|
218
217
|
test("forwards a Live Activity token delete to Django", async () => {
|
|
219
218
|
fetchMock = mock(async () => new Response(null, { status: 204 }));
|
|
220
|
-
const handler =
|
|
219
|
+
const handler = createCloudPushProxyHandler(registeredCredentials());
|
|
221
220
|
|
|
222
221
|
const res = await handler.handleDeleteLiveActivityToken(
|
|
223
222
|
new Request(
|
|
@@ -242,7 +241,7 @@ describe("platform push proxy", () => {
|
|
|
242
241
|
headers: { "content-type": "application/json" },
|
|
243
242
|
}),
|
|
244
243
|
);
|
|
245
|
-
const handler =
|
|
244
|
+
const handler = createCloudPushProxyHandler(registeredCredentials());
|
|
246
245
|
|
|
247
246
|
const res = await handler.handleDeletePushToken(
|
|
248
247
|
new Request(
|
|
@@ -256,11 +255,11 @@ describe("platform push proxy", () => {
|
|
|
256
255
|
expect(await res.json()).toEqual({ detail: "bundle_id is required." });
|
|
257
256
|
});
|
|
258
257
|
|
|
259
|
-
test("returns 502 when the
|
|
258
|
+
test("returns 502 when the cloud request fails", async () => {
|
|
260
259
|
fetchMock = mock(async () => {
|
|
261
260
|
throw new Error("network down");
|
|
262
261
|
});
|
|
263
|
-
const handler =
|
|
262
|
+
const handler = createCloudPushProxyHandler(registeredCredentials());
|
|
264
263
|
|
|
265
264
|
const res = await handler.handleUpsertPushToken(
|
|
266
265
|
new Request("http://localhost:7830/v1/assistants/self/push-tokens/", {
|
|
@@ -16,32 +16,31 @@ import type { CredentialCache } from "../../credential-cache.js";
|
|
|
16
16
|
import { credentialKey } from "../../credential-key.js";
|
|
17
17
|
import { fetchImpl } from "../../fetch.js";
|
|
18
18
|
import { getLogger } from "../../logger.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { resolveCloudAssistantIdOrUndefined } from "../../cloud-identity.js";
|
|
20
|
+
import { getCloudBaseUrl } from "../../cloud-url.js";
|
|
21
21
|
import { errorResponse } from "../loopback-guard.js";
|
|
22
22
|
|
|
23
|
-
const log = getLogger("
|
|
23
|
+
const log = getLogger("cloud-push-proxy");
|
|
24
24
|
|
|
25
25
|
const UPSTREAM_TIMEOUT_MS = 10_000;
|
|
26
26
|
|
|
27
27
|
interface PlatformTarget {
|
|
28
|
-
|
|
28
|
+
cloudBaseUrl: string;
|
|
29
29
|
assistantApiKey: string;
|
|
30
30
|
assistantId: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function
|
|
33
|
+
export function createCloudPushProxyHandler(credentials: CredentialCache) {
|
|
34
34
|
async function resolvePlatformTarget(): Promise<PlatformTarget | Response> {
|
|
35
|
-
const [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
resolvePlatformAssistantIdOrUndefined(),
|
|
40
|
-
]);
|
|
35
|
+
const [assistantApiKeyRaw, assistantId] = await Promise.all([
|
|
36
|
+
credentials.get(credentialKey("cadet", "assistant_api_key")),
|
|
37
|
+
resolveCloudAssistantIdOrUndefined(),
|
|
38
|
+
]);
|
|
41
39
|
|
|
40
|
+
const cloudBaseUrl = getCloudBaseUrl();
|
|
42
41
|
const assistantApiKey = assistantApiKeyRaw?.trim() || undefined;
|
|
43
42
|
|
|
44
|
-
if (!
|
|
43
|
+
if (!cloudBaseUrl || !assistantApiKey || !assistantId) {
|
|
45
44
|
return errorResponse(
|
|
46
45
|
"PLATFORM_UNAVAILABLE",
|
|
47
46
|
"This assistant is not registered with Cadet Cloud, so push notifications are unavailable.",
|
|
@@ -49,7 +48,7 @@ export function createPlatformPushProxyHandler(credentials: CredentialCache) {
|
|
|
49
48
|
);
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
return {
|
|
51
|
+
return { cloudBaseUrl, assistantApiKey, assistantId };
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
async function forward(
|
|
@@ -62,7 +61,7 @@ export function createPlatformPushProxyHandler(credentials: CredentialCache) {
|
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
const search = new URL(req.url).search;
|
|
65
|
-
const upstreamUrl = `${target.
|
|
64
|
+
const upstreamUrl = `${target.cloudBaseUrl}/v1/assistants/${target.assistantId}${resourcePath}${search}`;
|
|
66
65
|
|
|
67
66
|
const headers = new Headers();
|
|
68
67
|
headers.set("Authorization", `Api-Key ${target.assistantApiKey}`);
|
|
@@ -286,11 +286,11 @@ function sharedInviteRegistrationError(
|
|
|
286
286
|
* daemon-owned and layered on by each create transport exactly once: the
|
|
287
287
|
* daemon's create relay composes in-process; the gateway HTTP handler
|
|
288
288
|
* composes via the `invites_compose_presentation` daemon IPC.
|
|
289
|
-
* A `cadet-shared` invite is registered with the
|
|
289
|
+
* A `cadet-shared` invite is registered with the cloud before the row is
|
|
290
290
|
* written (see `registerSharedInvite`).
|
|
291
291
|
*
|
|
292
292
|
* Throws InviteNativeError(404) when the contact is unknown, (400) on invalid
|
|
293
|
-
* voice parameters, (409 / 429 / 502 / 503) when the
|
|
293
|
+
* voice parameters, (409 / 429 / 502 / 503) when the cloud registration
|
|
294
294
|
* does not succeed, and (500) when the gateway write fails.
|
|
295
295
|
*/
|
|
296
296
|
export async function createInviteNative(
|
|
@@ -71,7 +71,6 @@ const baseConfig: GatewayConfig = {
|
|
|
71
71
|
runtimeMaxRetries: 2,
|
|
72
72
|
runtimeProxyRequireAuth: true,
|
|
73
73
|
runtimeTimeoutMs: 30000,
|
|
74
|
-
shutdownDrainMs: 5000,
|
|
75
74
|
trustProxy: false,
|
|
76
75
|
};
|
|
77
76
|
|
|
@@ -381,7 +380,7 @@ describe("email-webhook", () => {
|
|
|
381
380
|
expect(status).toBe(false);
|
|
382
381
|
});
|
|
383
382
|
|
|
384
|
-
// Intercept response-shape pins. The
|
|
383
|
+
// Intercept response-shape pins. The cloud email relay
|
|
385
384
|
// (mycadet-platform django/app/email/views.py) gates only on
|
|
386
385
|
// `denied` in the gateway body and returns everything else verbatim — it
|
|
387
386
|
// reads neither intercept flag nor `replyText`. The flags name which
|
|
@@ -571,7 +570,7 @@ describe("email-webhook", () => {
|
|
|
571
570
|
const res = await handler(postRequest(body));
|
|
572
571
|
expect(res.status).toBe(500);
|
|
573
572
|
expect(handleInboundMock).not.toHaveBeenCalled();
|
|
574
|
-
// Unreserved so the
|
|
573
|
+
// Unreserved so the cloud's retry delivery can be processed.
|
|
575
574
|
expect(dedupCache.reserve("<transient-upload-fail@example.com>")).toBe(
|
|
576
575
|
true,
|
|
577
576
|
);
|
|
@@ -154,7 +154,7 @@ export function createEmailWebhookHandler(
|
|
|
154
154
|
|
|
155
155
|
// Ingest inline base64 attachments into the assistant's attachment store
|
|
156
156
|
// (which stores them in the conversation workspace) and forward the ids.
|
|
157
|
-
// A transient upload failure surfaces as 500 so the
|
|
157
|
+
// A transient upload failure surfaces as 500 so the cloud retries.
|
|
158
158
|
let attachmentIds: string[] | undefined;
|
|
159
159
|
try {
|
|
160
160
|
const ingested = await ingestEmailAttachments(config, attachments, tlog);
|
|
@@ -222,7 +222,7 @@ export function createEmailWebhookHandler(
|
|
|
222
222
|
"Gateway intercept — returning reply text to platform",
|
|
223
223
|
);
|
|
224
224
|
// replyText contract: the gateway cannot send email on this path — the
|
|
225
|
-
// reply rides the HTTP response for the
|
|
225
|
+
// reply rides the HTTP response for the cloud (vembda) to deliver,
|
|
226
226
|
// and delivery is tracked platform-side.
|
|
227
227
|
return Response.json({
|
|
228
228
|
ok: true,
|
|
@@ -269,9 +269,9 @@ export function createEmailWebhookHandler(
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
// Propagate the runtime's full response (including denied/reason/replyText)
|
|
272
|
-
// so the
|
|
272
|
+
// so the cloud can decide whether to persist the email and how to respond
|
|
273
273
|
// to the sender. Same replyText contract as above: the gateway cannot send
|
|
274
|
-
// email here — the
|
|
274
|
+
// email here — the cloud (vembda) delivers any reply and tracks delivery.
|
|
275
275
|
const runtimeBody = result.runtimeResponse ?? {};
|
|
276
276
|
return Response.json({ ok: true, ...runtimeBody });
|
|
277
277
|
} catch (err) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* POST /v1/contacts/guardian/channel — create a guardian channel binding
|
|
3
|
-
* via
|
|
3
|
+
* via cloud auto-verify.
|
|
4
4
|
*
|
|
5
|
-
* This route is called by the
|
|
5
|
+
* This route is called by the cloud's `assistant email register` flow
|
|
6
6
|
* to auto-verify the owner's email channel. It creates a guardian email
|
|
7
7
|
* channel binding directly in both the assistant and gateway databases.
|
|
8
8
|
*
|
|
@@ -138,7 +138,7 @@ export function createGuardianChannelHandler() {
|
|
|
138
138
|
|
|
139
139
|
log.info(
|
|
140
140
|
{ channel: body.type, address: body.address },
|
|
141
|
-
"Auto-verified guardian channel via
|
|
141
|
+
"Auto-verified guardian channel via cloud auto-register",
|
|
142
142
|
);
|
|
143
143
|
|
|
144
144
|
return Response.json({
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* user who traversed tunnel, so it is cross-checked against the bound
|
|
22
22
|
* platform user id by {@link requireManagedGuardian}. A gateway takes
|
|
23
23
|
* this path when {@link acceptsTunnelAttestation} says it has a tunnel
|
|
24
|
-
* at all: a
|
|
24
|
+
* at all: a cloud pod, or a locally hosted assistant whose gateway dialed
|
|
25
25
|
* tunnel so the mobile app can reach it.
|
|
26
26
|
* - **Everything else.** The caller presents an actor edge JWT and its
|
|
27
27
|
* principal is compared against the binding by {@link requireBoundGuardian}.
|
|
@@ -39,7 +39,7 @@ import { authorizeRuntimeAudioStream } from "./runtime-audio-stream.js";
|
|
|
39
39
|
import { findCadetGuardian } from "../../auth/guardian-bootstrap.js";
|
|
40
40
|
import type { GatewayConfig } from "../../config.js";
|
|
41
41
|
import { requestHasTunnelBridgeAuth } from "../../tunnel/bridge-auth.js";
|
|
42
|
-
import {
|
|
42
|
+
import { readStoredCloudUserId } from "../../cloud-user-id.js";
|
|
43
43
|
|
|
44
44
|
const TUNNEL_USER_ID_HEADER = "x-tunnel-user-id";
|
|
45
45
|
const TUNNEL_ORG_ID_HEADER = "x-tunnel-org-id";
|
|
@@ -47,16 +47,16 @@ const TUNNEL_ACTOR_HEADER = "x-tunnel-actor";
|
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* True when the gateway runs in managed/cloud mode (vembda + tunnel ingress).
|
|
50
|
-
* Mirrors the `
|
|
50
|
+
* Mirrors the `IS_CLOUD` check used by the gateway's HTTP edge auth
|
|
51
51
|
* (`src/http/middleware/auth.ts`) and feature-flag resolver.
|
|
52
52
|
*/
|
|
53
53
|
export function isPlatformManaged(): boolean {
|
|
54
|
-
const v = process.env.
|
|
54
|
+
const v = process.env.IS_CLOUD?.trim().toLowerCase();
|
|
55
55
|
return v === "1" || v === "true";
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* True when a tunnel-attested caller can reach this gateway at all: a
|
|
59
|
+
* True when a tunnel-attested caller can reach this gateway at all: a cloud
|
|
60
60
|
* pod, or any gateway started with a tunnel (`TUNNEL_BASE_URL`), which is
|
|
61
61
|
* how a locally hosted assistant is reachable from the mobile app.
|
|
62
62
|
*
|
|
@@ -101,7 +101,7 @@ export function extractTunnelAttestedContext(
|
|
|
101
101
|
/**
|
|
102
102
|
* Managed-mode guardian check: cross-check a tunnel-attested caller's platform
|
|
103
103
|
* user id against the bound platform user id, the same guard the
|
|
104
|
-
* edge-auth middleware applies to guardian routes under the
|
|
104
|
+
* edge-auth middleware applies to guardian routes under the cloud bypass.
|
|
105
105
|
*
|
|
106
106
|
* Returns null when the caller is the guardian, else a 403/503 Response.
|
|
107
107
|
*/
|
|
@@ -112,7 +112,7 @@ export async function requireManagedGuardian(
|
|
|
112
112
|
let storedUserId: string | undefined;
|
|
113
113
|
let unreachable = false;
|
|
114
114
|
try {
|
|
115
|
-
const result = await
|
|
115
|
+
const result = await readStoredCloudUserId();
|
|
116
116
|
storedUserId = result.userId;
|
|
117
117
|
unreachable = result.unreachable;
|
|
118
118
|
} catch (err) {
|
|
@@ -172,8 +172,8 @@ export async function authorizeGuardianStream(
|
|
|
172
172
|
config: GatewayConfig,
|
|
173
173
|
log: Logger,
|
|
174
174
|
): Promise<Response | null> {
|
|
175
|
-
// Checked here as well as in the shared gate, because the
|
|
176
|
-
// below skips that gate entirely: without this, a
|
|
175
|
+
// Checked here as well as in the shared gate, because the cloud path
|
|
176
|
+
// below skips that gate entirely: without this, a cloud caller sending a
|
|
177
177
|
// plain request would fall through to `server.upgrade` and get a 500.
|
|
178
178
|
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
|
|
179
179
|
return new Response("Upgrade Required", { status: 426 });
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* POST /inbound/register — auto-verify the guardian's email channel when
|
|
3
3
|
* an email provider is registered.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
5
|
+
* The cloud provides `guardian_email` in the request body. For BYO
|
|
6
6
|
* providers with an identity API (Mailgun), the gateway cross-verifies
|
|
7
7
|
* the email against the API response. For BYO providers without one
|
|
8
8
|
* (Resend), it validates that the stored API key is functional (proving
|
|
9
|
-
* account ownership) and trusts the provided email. For
|
|
9
|
+
* account ownership) and trusts the provided email. For cloud-managed
|
|
10
10
|
* email (type "cadet"), the route's edge-scoped auth is sufficient.
|
|
11
11
|
*
|
|
12
12
|
* On success, creates a guardian email channel binding directly in both
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { ConfigFileCache } from "../../config-file-cache.js";
|
|
8
|
+
import { handleSaveIngressDomain } from "./ingress-domains.js";
|
|
9
|
+
|
|
10
|
+
// The handler's write path resolves the workspace config through
|
|
11
|
+
// getWorkspaceDir; the mock redirects it into a per-test temp dir.
|
|
12
|
+
let workspaceDir = "";
|
|
13
|
+
|
|
14
|
+
mock.module("../../credential-reader.js", () => ({
|
|
15
|
+
getWorkspaceDir: () => workspaceDir,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const SavedIngressSchema = z.looseObject({
|
|
19
|
+
ingress: z
|
|
20
|
+
.looseObject({
|
|
21
|
+
publicBaseUrl: z.string().optional(),
|
|
22
|
+
publicBaseUrlManagedBy: z.string().optional(),
|
|
23
|
+
})
|
|
24
|
+
.optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let testDir = "";
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
testDir = mkdtempSync(join(tmpdir(), "gateway-ingress-domains-test-"));
|
|
31
|
+
workspaceDir = testDir;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function request(body?: unknown): Request {
|
|
39
|
+
return new Request("http://127.0.0.1/v1/ingress/domains", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "Content-Type": "application/json" },
|
|
42
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readSavedIngress(): z.infer<typeof SavedIngressSchema>["ingress"] {
|
|
47
|
+
const raw = SavedIngressSchema.parse(
|
|
48
|
+
JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8")),
|
|
49
|
+
);
|
|
50
|
+
return raw.ingress;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("POST /v1/ingress/domains", () => {
|
|
54
|
+
test("records the tunnel URL as tunnel-managed in the gateway config", async () => {
|
|
55
|
+
const res = await handleSaveIngressDomain(
|
|
56
|
+
request({ publicBaseUrl: "https://foo.ngrok.app" }),
|
|
57
|
+
"127.0.0.1",
|
|
58
|
+
new ConfigFileCache(),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(res.status).toBe(200);
|
|
62
|
+
expect(readSavedIngress()).toMatchObject({
|
|
63
|
+
publicBaseUrl: "https://foo.ngrok.app",
|
|
64
|
+
publicBaseUrlManagedBy: "tunnel",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("normalizes the URL the same way every other public base URL is", async () => {
|
|
69
|
+
await handleSaveIngressDomain(
|
|
70
|
+
request({ publicBaseUrl: "https://foo.ngrok.app/" }),
|
|
71
|
+
"127.0.0.1",
|
|
72
|
+
new ConfigFileCache(),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(readSavedIngress()?.publicBaseUrl).toBe("https://foo.ngrok.app");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("is a no-op write when the URL is already recorded as tunnel-managed", async () => {
|
|
79
|
+
const configFile = new ConfigFileCache();
|
|
80
|
+
await handleSaveIngressDomain(
|
|
81
|
+
request({ publicBaseUrl: "https://foo.ngrok.app" }),
|
|
82
|
+
"127.0.0.1",
|
|
83
|
+
configFile,
|
|
84
|
+
);
|
|
85
|
+
const afterFirst = readFileSync(join(workspaceDir, "config.json"), "utf-8");
|
|
86
|
+
|
|
87
|
+
await handleSaveIngressDomain(
|
|
88
|
+
request({ publicBaseUrl: "https://foo.ngrok.app" }),
|
|
89
|
+
"127.0.0.1",
|
|
90
|
+
configFile,
|
|
91
|
+
);
|
|
92
|
+
const afterSecond = readFileSync(
|
|
93
|
+
join(workspaceDir, "config.json"),
|
|
94
|
+
"utf-8",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(afterSecond).toBe(afterFirst);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("rejects non-loopback callers", async () => {
|
|
101
|
+
const res = await handleSaveIngressDomain(
|
|
102
|
+
request({ publicBaseUrl: "https://foo.ngrok.app" }),
|
|
103
|
+
"10.0.0.1",
|
|
104
|
+
new ConfigFileCache(),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(res.status).toBe(403);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("rejects a body without a usable public URL", async () => {
|
|
111
|
+
const res = await handleSaveIngressDomain(
|
|
112
|
+
request({}),
|
|
113
|
+
"127.0.0.1",
|
|
114
|
+
new ConfigFileCache(),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(res.status).toBe(400);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("rejects non-POST methods", async () => {
|
|
121
|
+
const res = await handleSaveIngressDomain(
|
|
122
|
+
new Request("http://127.0.0.1/v1/ingress/domains", { method: "GET" }),
|
|
123
|
+
"127.0.0.1",
|
|
124
|
+
new ConfigFileCache(),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(res.status).toBe(405);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loopback-guarded recording of a tunnel's public URL.
|
|
3
|
+
*
|
|
4
|
+
* POST /v1/ingress/domains — the CLI (its ngrok agent, today) tells the
|
|
5
|
+
* gateway the public URL its tunnel published; the gateway writes
|
|
6
|
+
* `ingress.publicBaseUrl` + `publicBaseUrlManagedBy: "tunnel"` in its own
|
|
7
|
+
* config via the same machinery its own tunnel uses, so both publishers
|
|
8
|
+
* record through one path and the gateway's existing loss handling clears
|
|
9
|
+
* it uniformly. Write-only: reads keep flowing through the config the
|
|
10
|
+
* gateway already serves. While we ship one bundle the gateway still hosts
|
|
11
|
+
* the ngrok-related logic; once the bundle splits, the gateway manages its
|
|
12
|
+
* own tunnel (mycadet.ai vs <assistant>.mycadet.bot) and this route is what
|
|
13
|
+
* the CLI-side publishers migrate onto.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
import { normalizeHttpPublicBaseUrlWithoutTrailingSlash } from "@mycadet/service-contracts/ingress";
|
|
19
|
+
|
|
20
|
+
import type { ConfigFileCache } from "../../config-file-cache.js";
|
|
21
|
+
import { writeManagedPublicBaseUrl } from "../../tunnel/client.js";
|
|
22
|
+
import {
|
|
23
|
+
enforceLoopbackOnly,
|
|
24
|
+
errorResponse,
|
|
25
|
+
rejectBrowserOrigin,
|
|
26
|
+
} from "../loopback-guard.js";
|
|
27
|
+
|
|
28
|
+
const SaveIngressDomainRequestSchema = z.object({
|
|
29
|
+
publicBaseUrl: z.string(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export async function handleSaveIngressDomain(
|
|
33
|
+
req: Request,
|
|
34
|
+
clientIp: string,
|
|
35
|
+
configFile: ConfigFileCache,
|
|
36
|
+
): Promise<Response> {
|
|
37
|
+
if (req.method !== "POST") {
|
|
38
|
+
return new Response("method not allowed", {
|
|
39
|
+
status: 405,
|
|
40
|
+
headers: { Allow: "POST" },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const guardError = enforceLoopbackOnly(req, clientIp, "ingress-domains");
|
|
45
|
+
if (guardError) return guardError;
|
|
46
|
+
const originError = rejectBrowserOrigin(req, clientIp, "ingress-domains");
|
|
47
|
+
if (originError) return originError;
|
|
48
|
+
|
|
49
|
+
let body: unknown;
|
|
50
|
+
try {
|
|
51
|
+
body = await req.json();
|
|
52
|
+
} catch {
|
|
53
|
+
body = {};
|
|
54
|
+
}
|
|
55
|
+
const parsed = SaveIngressDomainRequestSchema.safeParse(body);
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
return errorResponse("BAD_REQUEST", "publicBaseUrl is required", 400);
|
|
58
|
+
}
|
|
59
|
+
const publicUrl = normalizeHttpPublicBaseUrlWithoutTrailingSlash(
|
|
60
|
+
parsed.data.publicBaseUrl,
|
|
61
|
+
);
|
|
62
|
+
if (!publicUrl) {
|
|
63
|
+
return errorResponse(
|
|
64
|
+
"BAD_REQUEST",
|
|
65
|
+
"publicBaseUrl must be an absolute http(s) URL",
|
|
66
|
+
400,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// No-op (no write) when the URL is already recorded as tunnel-managed.
|
|
71
|
+
await writeManagedPublicBaseUrl(publicUrl, configFile);
|
|
72
|
+
return Response.json({ ok: true });
|
|
73
|
+
}
|
|
@@ -119,7 +119,7 @@ async function checkLiveVoiceAuth(
|
|
|
119
119
|
// the caller is *a* platform user who traversed tunnel, not that they
|
|
120
120
|
// are THIS assistant's guardian, so cross-check the tunnel user id
|
|
121
121
|
// against the bound platform user id (the same guardian check the
|
|
122
|
-
// edge-auth middleware applies to guardian routes under the
|
|
122
|
+
// edge-auth middleware applies to guardian routes under the cloud
|
|
123
123
|
// bypass). Without it, any tunnel-authorized org user reaching this
|
|
124
124
|
// assistant would be stamped guardian downstream.
|
|
125
125
|
const guardianError = await requireManagedGuardian(
|
|
@@ -127,7 +127,7 @@ async function checkLiveVoiceAuth(
|
|
|
127
127
|
log,
|
|
128
128
|
);
|
|
129
129
|
if (guardianError) return { response: guardianError };
|
|
130
|
-
// Tunnel attests a
|
|
130
|
+
// Tunnel attests a cloud user, not an actor principal, so the
|
|
131
131
|
// principal comes from the gateway's own binding rather than from
|
|
132
132
|
// anything the request carried.
|
|
133
133
|
const guardian = await findCadetGuardian();
|
|
@@ -452,7 +452,7 @@ async function handleSyncBytesImport(
|
|
|
452
452
|
*
|
|
453
453
|
* Returns the job's current status on 200, or 404 if the jobId is unknown
|
|
454
454
|
* (never issued, or pruned after the TTL). The response shape deliberately
|
|
455
|
-
* mirrors the
|
|
455
|
+
* mirrors the cloud's existing `/v1/migrations/import/:jobId/status/`
|
|
456
456
|
* endpoint so `PlatformMigrationClient.pollImportStatus` works against
|
|
457
457
|
* either surface with no changes.
|
|
458
458
|
*/
|
|
@@ -176,7 +176,7 @@ describe("upgrade gate", () => {
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
it("upgrades a cadet-signed declaration nobody approved", async () => {
|
|
179
|
-
// Only a caller holding the
|
|
179
|
+
// Only a caller holding the cloud secret can open it, and the user
|
|
180
180
|
// extended that trust when they connected their account.
|
|
181
181
|
const handle = makeHandler({
|
|
182
182
|
resolve: () =>
|
|
@@ -203,7 +203,7 @@ describe("upgrade gate", () => {
|
|
|
203
203
|
expect(upgrades).toHaveLength(1);
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
-
it("still demands the
|
|
206
|
+
it("still demands the cloud signature on an unapproved cadet route", async () => {
|
|
207
207
|
// Skipping approval did not skip authentication.
|
|
208
208
|
const handle = makeHandler({
|
|
209
209
|
resolve: () =>
|
|
@@ -378,7 +378,7 @@ describe("handshake signature", () => {
|
|
|
378
378
|
expect(upgrades).toEqual([]);
|
|
379
379
|
});
|
|
380
380
|
|
|
381
|
-
it("verifies a cadet-signed route against the
|
|
381
|
+
it("verifies a cadet-signed route against the cloud secret", async () => {
|
|
382
382
|
const handle = makeHandler({
|
|
383
383
|
resolve: () => approvedWith([{ ...ROUTE, signer: "cadet" as const }]),
|
|
384
384
|
});
|