@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
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
resetMockFetch,
|
|
9
9
|
} from "../__tests__/mock-fetch.js";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "../
|
|
11
|
+
applyCloudIdentityIds,
|
|
12
|
+
_resetCloudIdentityForTests,
|
|
13
|
+
} from "../cloud-identity.js";
|
|
14
14
|
import {
|
|
15
15
|
registerEmailCallbackRoute,
|
|
16
16
|
EMAIL_CALLBACK_PATH,
|
|
@@ -18,9 +18,9 @@ import {
|
|
|
18
18
|
|
|
19
19
|
afterEach(() => {
|
|
20
20
|
resetMockFetch();
|
|
21
|
-
delete process.env.
|
|
21
|
+
delete process.env.CADET_CLOUD_URL;
|
|
22
22
|
delete process.env.ASSISTANT_API_KEY;
|
|
23
|
-
|
|
23
|
+
_resetCloudIdentityForTests();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
function makeConfigFile(
|
|
@@ -34,29 +34,22 @@ function makeConfigFile(
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function makeCaches(opts: {
|
|
37
|
-
|
|
37
|
+
cloudBaseUrl?: string;
|
|
38
38
|
assistantApiKey?: string;
|
|
39
|
-
|
|
39
|
+
cloudAssistantId?: string;
|
|
40
40
|
ingressUrl?: string;
|
|
41
41
|
}): { credentials: CredentialCache; configFile?: ConfigFileCache } {
|
|
42
42
|
const store = new Map<string, string>();
|
|
43
|
-
if (opts.
|
|
44
|
-
|
|
45
|
-
credentialKey("cadet", "platform_base_url"),
|
|
46
|
-
opts.platformBaseUrl,
|
|
47
|
-
);
|
|
43
|
+
if (opts.cloudBaseUrl)
|
|
44
|
+
process.env.CADET_CLOUD_URL = opts.cloudBaseUrl;
|
|
48
45
|
if (opts.assistantApiKey)
|
|
49
46
|
store.set(
|
|
50
47
|
credentialKey("cadet", "assistant_api_key"),
|
|
51
48
|
opts.assistantApiKey,
|
|
52
49
|
);
|
|
53
|
-
if (opts.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
opts.platformAssistantId,
|
|
57
|
-
);
|
|
58
|
-
applyPlatformIdentityIds({
|
|
59
|
-
assistantId: opts.platformAssistantId,
|
|
50
|
+
if (opts.cloudAssistantId) {
|
|
51
|
+
applyCloudIdentityIds({
|
|
52
|
+
assistantId: opts.cloudAssistantId,
|
|
60
53
|
organizationId: "org-abc",
|
|
61
54
|
userId: "user-123",
|
|
62
55
|
});
|
|
@@ -95,13 +88,13 @@ describe("registerEmailCallbackRoute", () => {
|
|
|
95
88
|
|
|
96
89
|
test("registers callback route with platform via credential cache", async () => {
|
|
97
90
|
const caches = makeCaches({
|
|
98
|
-
|
|
91
|
+
cloudBaseUrl: "https://test-cloud.mycadet.ai",
|
|
99
92
|
assistantApiKey: "test-api-key",
|
|
100
|
-
|
|
93
|
+
cloudAssistantId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
|
|
101
94
|
});
|
|
102
95
|
|
|
103
96
|
const callbackUrl =
|
|
104
|
-
"https://
|
|
97
|
+
"https://test-cloud.mycadet.ai/v1/gateway/callbacks/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/webhooks/email/";
|
|
105
98
|
|
|
106
99
|
mockFetch(
|
|
107
100
|
"callback-routes/register",
|
|
@@ -130,7 +123,7 @@ describe("registerEmailCallbackRoute", () => {
|
|
|
130
123
|
});
|
|
131
124
|
|
|
132
125
|
test("uses env assistant key with credential cache for assistant ID", async () => {
|
|
133
|
-
process.env.
|
|
126
|
+
process.env.CADET_CLOUD_URL = "https://env-platform.example.com";
|
|
134
127
|
process.env.ASSISTANT_API_KEY = "env-key";
|
|
135
128
|
|
|
136
129
|
const callbackUrl =
|
|
@@ -146,7 +139,7 @@ describe("registerEmailCallbackRoute", () => {
|
|
|
146
139
|
);
|
|
147
140
|
|
|
148
141
|
const caches = makeCaches({
|
|
149
|
-
|
|
142
|
+
cloudAssistantId: "11111111-2222-3333-4444-555555555555",
|
|
150
143
|
});
|
|
151
144
|
|
|
152
145
|
const result = await registerEmailCallbackRoute(caches);
|
|
@@ -161,9 +154,9 @@ describe("registerEmailCallbackRoute", () => {
|
|
|
161
154
|
|
|
162
155
|
test("throws on non-ok response", async () => {
|
|
163
156
|
const caches = makeCaches({
|
|
164
|
-
|
|
157
|
+
cloudBaseUrl: "https://test-cloud.mycadet.ai",
|
|
165
158
|
assistantApiKey: "test-api-key",
|
|
166
|
-
|
|
159
|
+
cloudAssistantId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
|
|
167
160
|
});
|
|
168
161
|
|
|
169
162
|
mockFetch(
|
|
@@ -179,9 +172,9 @@ describe("registerEmailCallbackRoute", () => {
|
|
|
179
172
|
|
|
180
173
|
test("throws when response has no callback_url", async () => {
|
|
181
174
|
const caches = makeCaches({
|
|
182
|
-
|
|
175
|
+
cloudBaseUrl: "https://test-cloud.mycadet.ai",
|
|
183
176
|
assistantApiKey: "test-api-key",
|
|
184
|
-
|
|
177
|
+
cloudAssistantId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
|
|
185
178
|
});
|
|
186
179
|
|
|
187
180
|
mockFetch(
|
|
@@ -200,9 +193,9 @@ describe("registerEmailCallbackRoute", () => {
|
|
|
200
193
|
|
|
201
194
|
test("sends callback_base_url when ingress URL is configured (self-hosted)", async () => {
|
|
202
195
|
const caches = makeCaches({
|
|
203
|
-
|
|
196
|
+
cloudBaseUrl: "https://test-cloud.mycadet.ai",
|
|
204
197
|
assistantApiKey: "vak_selfhosted",
|
|
205
|
-
|
|
198
|
+
cloudAssistantId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
|
|
206
199
|
ingressUrl: "https://my-assistant.example.com",
|
|
207
200
|
});
|
|
208
201
|
|
|
@@ -233,15 +226,15 @@ describe("registerEmailCallbackRoute", () => {
|
|
|
233
226
|
});
|
|
234
227
|
});
|
|
235
228
|
|
|
236
|
-
test("omits callback_base_url when no ingress URL is configured (
|
|
229
|
+
test("omits callback_base_url when no ingress URL is configured (cloud-managed)", async () => {
|
|
237
230
|
const caches = makeCaches({
|
|
238
|
-
|
|
231
|
+
cloudBaseUrl: "https://test-cloud.mycadet.ai",
|
|
239
232
|
assistantApiKey: "vak_managed",
|
|
240
|
-
|
|
233
|
+
cloudAssistantId: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
|
|
241
234
|
});
|
|
242
235
|
|
|
243
236
|
const callbackUrl =
|
|
244
|
-
"https://
|
|
237
|
+
"https://test-cloud.mycadet.ai/v1/gateway/callbacks/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/webhooks/email/";
|
|
245
238
|
|
|
246
239
|
mockFetch(
|
|
247
240
|
"callback-routes/register",
|
|
@@ -2,14 +2,14 @@ import type { ConfigFileCache } from "../config-file-cache.js";
|
|
|
2
2
|
import type { CredentialCache } from "../credential-cache.js";
|
|
3
3
|
import { credentialKey } from "../credential-key.js";
|
|
4
4
|
import { fetchImpl } from "../fetch.js";
|
|
5
|
-
import {
|
|
5
|
+
import { areCloudFeaturesEnabled } from "../feature-flag-resolver.js";
|
|
6
6
|
import { getLogger } from "../logger.js";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveCloudAssistantIdOrUndefined } from "../cloud-identity.js";
|
|
8
8
|
|
|
9
9
|
const log = getLogger("email-callback");
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* The callback path registered with the
|
|
12
|
+
* The callback path registered with the cloud for inbound email webhooks.
|
|
13
13
|
* Must match the gateway route path in index.ts ("/webhooks/email").
|
|
14
14
|
*/
|
|
15
15
|
export const EMAIL_CALLBACK_PATH = "webhooks/email";
|
|
@@ -26,39 +26,34 @@ interface PlatformCallbackRouteResponse {
|
|
|
26
26
|
* Follows the same pattern as Telegram's managed callback route registration
|
|
27
27
|
* in `telegram/webhook-manager.ts`. Requires cloud credentials (base URL,
|
|
28
28
|
* assistant API key, assistant ID). The base URL can fall back to
|
|
29
|
-
* `
|
|
29
|
+
* `CADET_CLOUD_URL`; the key can fall back to `ASSISTANT_API_KEY`.
|
|
30
30
|
*
|
|
31
31
|
* Self-hosted assistants with a configured ``ingress.publicBaseUrl`` send
|
|
32
32
|
* their own base URL so the callback route points directly at the gateway
|
|
33
|
-
* rather than through the
|
|
33
|
+
* rather than through the cloud proxy.
|
|
34
34
|
*
|
|
35
|
-
* Returns the
|
|
35
|
+
* Returns the cloud-assigned callback URL on success, or `undefined` if
|
|
36
36
|
* credentials are not available.
|
|
37
37
|
*/
|
|
38
38
|
export async function registerEmailCallbackRoute(caches?: {
|
|
39
39
|
credentials?: CredentialCache;
|
|
40
40
|
configFile?: ConfigFileCache;
|
|
41
41
|
}): Promise<string | undefined> {
|
|
42
|
-
if (!
|
|
42
|
+
if (!areCloudFeaturesEnabled()) {
|
|
43
43
|
log.debug(
|
|
44
44
|
"Platform features disabled — skipping email callback registration",
|
|
45
45
|
);
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const
|
|
50
|
-
? await
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// matching the daemon's resolvePlatformCallbackRegistrationContext().
|
|
58
|
-
const platformBaseUrl = (
|
|
59
|
-
platformBaseUrlRaw?.trim() ||
|
|
60
|
-
process.env.CADET_PLATFORM_URL?.trim() ||
|
|
61
|
-
""
|
|
49
|
+
const assistantApiKeyRaw = caches?.credentials
|
|
50
|
+
? await caches.credentials.get(credentialKey("cadet", "assistant_api_key"))
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
// The base URL resolves from the environment only — the credential store
|
|
54
|
+
// never carries it.
|
|
55
|
+
const cloudBaseUrl = (
|
|
56
|
+
process.env.CADET_CLOUD_URL?.trim() || ""
|
|
62
57
|
).replace(/\/+$/, "");
|
|
63
58
|
|
|
64
59
|
const assistantCredential =
|
|
@@ -66,12 +61,12 @@ export async function registerEmailCallbackRoute(caches?: {
|
|
|
66
61
|
process.env.ASSISTANT_API_KEY?.trim() ||
|
|
67
62
|
undefined;
|
|
68
63
|
|
|
69
|
-
const assistantId = await
|
|
64
|
+
const assistantId = await resolveCloudAssistantIdOrUndefined();
|
|
70
65
|
|
|
71
|
-
if (!
|
|
66
|
+
if (!cloudBaseUrl || !assistantCredential || !assistantId) {
|
|
72
67
|
log.debug(
|
|
73
68
|
{
|
|
74
|
-
hasPlatformBaseUrl: !!
|
|
69
|
+
hasPlatformBaseUrl: !!cloudBaseUrl,
|
|
75
70
|
hasApiKey: !!assistantCredential,
|
|
76
71
|
hasAssistantId: !!assistantId,
|
|
77
72
|
},
|
|
@@ -80,9 +75,9 @@ export async function registerEmailCallbackRoute(caches?: {
|
|
|
80
75
|
return undefined;
|
|
81
76
|
}
|
|
82
77
|
|
|
83
|
-
// Self-hosted assistants send their public ingress URL so the
|
|
78
|
+
// Self-hosted assistants send their public ingress URL so the cloud
|
|
84
79
|
// registers a callback pointing directly at the gateway rather than
|
|
85
|
-
// routing through the
|
|
80
|
+
// routing through the cloud proxy (matching Telegram's two-tier
|
|
86
81
|
// pattern in telegram/webhook-manager.ts).
|
|
87
82
|
const ingressUrl = caches?.configFile
|
|
88
83
|
?.getString("ingress", "publicBaseUrl")
|
|
@@ -106,7 +101,7 @@ export async function registerEmailCallbackRoute(caches?: {
|
|
|
106
101
|
}
|
|
107
102
|
|
|
108
103
|
const response = await fetchImpl(
|
|
109
|
-
`${
|
|
104
|
+
`${cloudBaseUrl}/v1/internal/gateway/callback-routes/register/`,
|
|
110
105
|
{
|
|
111
106
|
method: "POST",
|
|
112
107
|
headers: {
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"scope": "client",
|
|
83
83
|
"key": "multi-platform-assistant",
|
|
84
84
|
"label": "Multi-Platform Assistant Switcher",
|
|
85
|
-
"description": "Enable the menu-bar assistant switcher for managing multiple
|
|
85
|
+
"description": "Enable the menu-bar assistant switcher for managing multiple cloud-hosted assistants (new/switch/retire)",
|
|
86
86
|
"defaultEnabled": false
|
|
87
87
|
},
|
|
88
88
|
{
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"scope": "client",
|
|
139
139
|
"key": "fork-from-message",
|
|
140
140
|
"label": "Internal Thread Actions (legacy key)",
|
|
141
|
-
"description": "Legacy key for `internal-thread-actions`. The client grants the internal thread affordances when either key is enabled, so the
|
|
141
|
+
"description": "Legacy key for `internal-thread-actions`. The client grants the internal thread affordances when either key is enabled, so the cloud may serve the gate under either name. Declared here so the flag mapper keeps the value instead of dropping it as undeclared.",
|
|
142
142
|
"defaultEnabled": false
|
|
143
143
|
},
|
|
144
144
|
{
|
|
@@ -378,7 +378,7 @@
|
|
|
378
378
|
"scope": "client",
|
|
379
379
|
"key": "push-avatar-sender",
|
|
380
380
|
"label": "Push Avatar Sender",
|
|
381
|
-
"description": "Native notifications show the assistant's avatar as the icon with the assistant's name on line one. Same key the
|
|
381
|
+
"description": "Native notifications show the assistant's avatar as the icon with the assistant's name on line one. Same key the cloud reads for APNs/FCM sender blocks.",
|
|
382
382
|
"defaultEnabled": false
|
|
383
383
|
},
|
|
384
384
|
{
|
|
@@ -450,7 +450,7 @@
|
|
|
450
450
|
"scope": "assistant",
|
|
451
451
|
"key": "tunnel-webhooks",
|
|
452
452
|
"label": "Tunnel Webhooks",
|
|
453
|
-
"description": "
|
|
453
|
+
"description": "Cloud pods resolve webhook callback URLs from the Tunnel-published ingress URL instead of registering platform callback routes. Falls back to platform callback registration while no tunnel URL is published.",
|
|
454
454
|
"defaultEnabled": false
|
|
455
455
|
},
|
|
456
456
|
{
|
|
@@ -483,7 +483,7 @@
|
|
|
483
483
|
"scope": "assistant",
|
|
484
484
|
"key": "assistant-desktop",
|
|
485
485
|
"label": "Virtual desktop",
|
|
486
|
-
"description": "Streams an interactive virtual desktop from a
|
|
486
|
+
"description": "Streams an interactive virtual desktop from a cloud-hosted, containerized assistant over /v1/desktop/stream. Requires platform hosting; self-hosted assistants are excluded. Off: the virtual desktop is hidden. On: the chat header offers its viewer and controls.",
|
|
487
487
|
"defaultEnabled": false
|
|
488
488
|
},
|
|
489
489
|
{
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gateway-side remote feature flag store — file-backed persistence of
|
|
3
|
-
* feature flag values pushed from the
|
|
3
|
+
* feature flag values pushed from the cloud.
|
|
4
4
|
*
|
|
5
5
|
* Mirrors the feature-flag-store.ts pattern: file path resolution via
|
|
6
6
|
* getGatewaySecurityDir(), atomic writes (temp file + rename), 0o600
|
|
7
7
|
* permissions, and module-level caching with manual invalidation.
|
|
8
8
|
*
|
|
9
9
|
* Unlike the local override store, writes replace the *entire* value map at
|
|
10
|
-
* once (the
|
|
10
|
+
* once (the cloud pushes a complete snapshot) and immediately update the
|
|
11
11
|
* in-memory cache so no file watcher is needed.
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -40,13 +40,13 @@ export function isFeatureFlagEnabled(key: string): boolean {
|
|
|
40
40
|
return !!getFeatureFlagValue(key);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function
|
|
44
|
-
const v = process.env.
|
|
43
|
+
export function isCloudServedBuild(): boolean {
|
|
44
|
+
const v = process.env.IS_CLOUD?.trim().toLowerCase();
|
|
45
45
|
return v === "true" || v === "1";
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function
|
|
49
|
-
if (
|
|
50
|
-
const v = process.env.
|
|
48
|
+
export function areCloudFeaturesEnabled(): boolean {
|
|
49
|
+
if (isCloudServedBuild()) return true;
|
|
50
|
+
const v = process.env.CADET_DISABLE_CLOUD?.trim().toLowerCase();
|
|
51
51
|
return !(v === "true" || v === "1");
|
|
52
52
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Verification for the `Cadet-Signature` header.
|
|
3
3
|
*
|
|
4
4
|
* The signer computes `HMAC-SHA256(secret, rawBody)` and sends it as
|
|
5
|
-
* `Cadet-Signature: sha256=<hex-digest>`. The scheme is the
|
|
5
|
+
* `Cadet-Signature: sha256=<hex-digest>`. The scheme is the cloud's, but
|
|
6
6
|
* nothing about it is platform-specific — plugin webhook routes verify the
|
|
7
7
|
* same header against the plugin's own secret.
|
|
8
8
|
*/
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Loopback-only endpoints (`/v1/guardian/init`, `/v1/pair`) treat its presence
|
|
14
14
|
* as proof that the request did NOT arrive directly from a local loopback
|
|
15
15
|
* client: it was forwarded by the edge proxy, possibly from a remote browser
|
|
16
|
-
* over a tunnel (e.g.
|
|
16
|
+
* over a tunnel (e.g. a third-party tunnel). Because every hop in that chain is loopback
|
|
17
17
|
* (browser → tunnel agent → nginx@127.0.0.1 → gateway@127.0.0.1), the raw TCP
|
|
18
18
|
* peer is 127.0.0.1 and a peer-IP loopback check alone would misclassify the
|
|
19
19
|
* caller as local. This marker is the reliable signal instead — and crucially
|
|
@@ -91,7 +91,7 @@ export function enforceLoopbackOnly(
|
|
|
91
91
|
return errorResponse("FORBIDDEN", "endpoint is local-only", 403);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// The self-hosted nginx edge (e.g. the SPA over
|
|
94
|
+
// The self-hosted nginx edge (e.g. the SPA over a tunnel) sets this
|
|
95
95
|
// marker unconditionally; it cannot be spoofed or stripped by the client.
|
|
96
96
|
if (requestArrivedViaEdgeProxy(req)) {
|
|
97
97
|
auditDeny(req, clientIp, auditTag, "edge_proxy_forwarded");
|
|
@@ -13,7 +13,7 @@ import { AuthFallbackCountTracker } from "../../auth-fallback-count-tracker.js";
|
|
|
13
13
|
import { AuthFallbackLogThrottle } from "../../auth-fallback-log-throttle.js";
|
|
14
14
|
import type { AuthRateLimiter } from "../../auth-rate-limiter.js";
|
|
15
15
|
import { getLogger } from "../../logger.js";
|
|
16
|
-
import {
|
|
16
|
+
import { readStoredCloudUserId } from "../../cloud-user-id.js";
|
|
17
17
|
import { isLoopbackPeer } from "../../util/is-loopback-address.js";
|
|
18
18
|
import { requestArrivedViaEdgeProxy } from "../edge-forwarded-header.js";
|
|
19
19
|
|
|
@@ -34,34 +34,34 @@ export const loopbackFallbackCountTracker = new AuthFallbackCountTracker();
|
|
|
34
34
|
type GetClientIp = () => string;
|
|
35
35
|
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
|
-
// Platform-managed auth bypass — DISABLE_HTTP_AUTH +
|
|
37
|
+
// Platform-managed auth bypass — DISABLE_HTTP_AUTH + IS_CLOUD
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
39
|
//
|
|
40
40
|
// Both flags must be set together to disable JWT validation. DISABLE_HTTP_AUTH
|
|
41
41
|
// alone is insufficient — it closes the accidental misconfig case where the
|
|
42
|
-
// flag gets set on a non-
|
|
43
|
-
// public host). When the bypass IS active, the
|
|
42
|
+
// flag gets set on a non-cloud deployment (e.g. a leaked dev env var on a
|
|
43
|
+
// public host). When the bypass IS active, the cloud vembda sidecar is
|
|
44
44
|
// expected to forward `X-Cadet-User-Id`; the gateway cross-checks that
|
|
45
45
|
// against the bound platform user id. This means reaching the gateway
|
|
46
46
|
// sidecar's port directly (without going through vembda) still requires
|
|
47
|
-
// knowing the bound user id. The
|
|
47
|
+
// knowing the bound user id. The cloud header alone is not a free-pass.
|
|
48
48
|
|
|
49
49
|
/** True when DISABLE_HTTP_AUTH=true. */
|
|
50
50
|
export function isHttpAuthDisabled(): boolean {
|
|
51
51
|
return process.env.DISABLE_HTTP_AUTH?.trim().toLowerCase() === "true";
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/** True when
|
|
54
|
+
/** True when IS_CLOUD is set (vembda-managed deployment). */
|
|
55
55
|
function isPlatformManaged(): boolean {
|
|
56
|
-
const v = process.env.
|
|
56
|
+
const v = process.env.IS_CLOUD?.trim();
|
|
57
57
|
return v === "1" || v?.toLowerCase() === "true";
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* True when the
|
|
61
|
+
* True when the cloud-managed auth bypass is in effect — both flags set.
|
|
62
62
|
* Either alone leaves JWT validation in place.
|
|
63
63
|
*/
|
|
64
|
-
function
|
|
64
|
+
function isCloudAuthBypassActive(): boolean {
|
|
65
65
|
return isHttpAuthDisabled() && isPlatformManaged();
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -75,14 +75,14 @@ export function logAuthBypassState(): void {
|
|
|
75
75
|
}
|
|
76
76
|
if (isPlatformManaged()) {
|
|
77
77
|
log.info(
|
|
78
|
-
"DISABLE_HTTP_AUTH +
|
|
78
|
+
"DISABLE_HTTP_AUTH + IS_CLOUD both set: JWT validation bypassed; " +
|
|
79
79
|
"X-Cadet-User-Id is cross-checked against the bound platform user id",
|
|
80
80
|
);
|
|
81
81
|
} else {
|
|
82
82
|
log.warn(
|
|
83
|
-
"DISABLE_HTTP_AUTH is set but
|
|
84
|
-
"JWT validation runs as normal. Set
|
|
85
|
-
"
|
|
83
|
+
"DISABLE_HTTP_AUTH is set but IS_CLOUD is NOT — bypass is INACTIVE. " +
|
|
84
|
+
"JWT validation runs as normal. Set IS_CLOUD=true to opt into the " +
|
|
85
|
+
"cloud-managed auth model.",
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -94,14 +94,14 @@ export function logAuthBypassState(): void {
|
|
|
94
94
|
* clients. Each fallback endpoint and failure kind logs once per cooldown
|
|
95
95
|
* window (see `loopbackFallbackLogThrottle`) so callers still missing or
|
|
96
96
|
* failing auth are visible during rollout without per-request log noise. The
|
|
97
|
-
*
|
|
97
|
+
* cloud-managed bypass is checked first, so this fallback only applies in
|
|
98
98
|
* default mode.
|
|
99
99
|
*
|
|
100
100
|
* - `requireEdgeAuth` — validates a JWT bearer token (aud=cadet-gateway)
|
|
101
101
|
* OR (when bypass is active) cross-checks X-Cadet-User-Id against the
|
|
102
102
|
* bound platform user id.
|
|
103
103
|
* - `requireEdgeAuthWithScope` — same, plus a scope-profile check on the
|
|
104
|
-
* decoded JWT. Under the
|
|
104
|
+
* decoded JWT. Under the cloud bypass, scope is enforced upstream by
|
|
105
105
|
* vembda; the gateway only verifies the cross-checked user id.
|
|
106
106
|
* - `requireEdgeGuardianAuth` — same pattern, additionally requires the
|
|
107
107
|
* authenticated principal to match the bound guardian.
|
|
@@ -117,7 +117,7 @@ export function createAuthMiddleware(
|
|
|
117
117
|
) {
|
|
118
118
|
/**
|
|
119
119
|
* Cross-check `X-Cadet-User-Id` against the bound platform user id.
|
|
120
|
-
* Used by all three guards under the
|
|
120
|
+
* Used by all three guards under the cloud-managed bypass. Returns
|
|
121
121
|
* null on success, or a 4xx/5xx Response.
|
|
122
122
|
*/
|
|
123
123
|
async function requirePlatformUserHeader(
|
|
@@ -134,7 +134,7 @@ export function createAuthMiddleware(
|
|
|
134
134
|
let storedUserId: string | undefined;
|
|
135
135
|
let unreachable = false;
|
|
136
136
|
try {
|
|
137
|
-
const result = await
|
|
137
|
+
const result = await readStoredCloudUserId();
|
|
138
138
|
storedUserId = result.userId;
|
|
139
139
|
unreachable = result.unreachable;
|
|
140
140
|
} catch (err) {
|
|
@@ -177,7 +177,7 @@ export function createAuthMiddleware(
|
|
|
177
177
|
req: Request,
|
|
178
178
|
server?: Server<unknown>,
|
|
179
179
|
): Promise<Response | null> {
|
|
180
|
-
if (
|
|
180
|
+
if (isCloudAuthBypassActive()) {
|
|
181
181
|
return requirePlatformUserHeader(req);
|
|
182
182
|
}
|
|
183
183
|
const hasAuthHeader = hasAuthorizationHeader(req);
|
|
@@ -194,7 +194,7 @@ export function createAuthMiddleware(
|
|
|
194
194
|
/**
|
|
195
195
|
* Validate a JWT bearer token and check that its scope profile includes the
|
|
196
196
|
* required scope. Loopback peers fall back when auth is missing, malformed,
|
|
197
|
-
* invalid, or under-scoped. Under the
|
|
197
|
+
* invalid, or under-scoped. Under the cloud bypass, scope is enforced
|
|
198
198
|
* upstream by vembda — the gateway only confirms the user id.
|
|
199
199
|
*/
|
|
200
200
|
async function requireEdgeAuthWithScope(
|
|
@@ -202,7 +202,7 @@ export function createAuthMiddleware(
|
|
|
202
202
|
scope: Scope,
|
|
203
203
|
server?: Server<unknown>,
|
|
204
204
|
): Promise<Response | null> {
|
|
205
|
-
if (
|
|
205
|
+
if (isCloudAuthBypassActive()) {
|
|
206
206
|
return requirePlatformUserHeader(req);
|
|
207
207
|
}
|
|
208
208
|
const hasAuthHeader = hasAuthorizationHeader(req);
|
|
@@ -233,7 +233,7 @@ export function createAuthMiddleware(
|
|
|
233
233
|
*
|
|
234
234
|
* Two auth modes:
|
|
235
235
|
*
|
|
236
|
-
* 1. Platform-managed (DISABLE_HTTP_AUTH +
|
|
236
|
+
* 1. Platform-managed (DISABLE_HTTP_AUTH + IS_CLOUD): caller's identity
|
|
237
237
|
* is asserted via X-Cadet-User-Id cross-checked against the bound
|
|
238
238
|
* platform user id.
|
|
239
239
|
* 2. Default: validate the edge JWT, require an actor principal, assert it
|
|
@@ -246,7 +246,7 @@ export function createAuthMiddleware(
|
|
|
246
246
|
req: Request,
|
|
247
247
|
server?: Server<unknown>,
|
|
248
248
|
): Promise<Response | null> {
|
|
249
|
-
if (
|
|
249
|
+
if (isCloudAuthBypassActive()) {
|
|
250
250
|
return requirePlatformUserHeader(req);
|
|
251
251
|
}
|
|
252
252
|
return requireEdgeGuardianAuthByActorPrincipal(req, server);
|
package/src/http/router.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type GetRawPeerIp = () => string;
|
|
|
22
22
|
* - "edge-guardian" — caller must be the bound cadet guardian. In the default
|
|
23
23
|
* path, edge JWT is validated and the caller's actor
|
|
24
24
|
* principal must match the guardian's principal id.
|
|
25
|
-
* When DISABLE_HTTP_AUTH=true (
|
|
25
|
+
* When DISABLE_HTTP_AUTH=true (cloud-managed), the
|
|
26
26
|
* caller is asserted via X-Cadet-User-Id forwarded by
|
|
27
27
|
* vembda, cross-referenced with the bound platform
|
|
28
28
|
* user id.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cadet identity validation —
|
|
2
|
+
* Cadet identity validation — cloud-managed email.
|
|
3
3
|
*
|
|
4
|
-
* The route's edge-scoped auth already authenticates the
|
|
4
|
+
* The route's edge-scoped auth already authenticates the cloud,
|
|
5
5
|
* so no external API key validation is needed.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ import type { GatewayRouteDefinition } from "./types.js";
|
|
|
9
9
|
* scripts/generate-openapi.ts). Unlike the neighbouring channel APIs there
|
|
10
10
|
* are no assistant-scoped `/v1/assistants/{assistant_id}/...` variants: the
|
|
11
11
|
* guardian reaches these flat paths directly rather than through the
|
|
12
|
-
*
|
|
12
|
+
* cloud proxy.
|
|
13
13
|
*
|
|
14
14
|
* The handlers live in `channel-ingress.ts`; that module imports
|
|
15
15
|
* `ApproveChannelIngressRequestSchema` so wire validation and the published
|
|
@@ -273,7 +273,18 @@ describe("approve", () => {
|
|
|
273
273
|
refresh: () => {},
|
|
274
274
|
pollIntervalMs: 10,
|
|
275
275
|
});
|
|
276
|
-
|
|
276
|
+
// The watcher polls every 10ms, so the retry normally lands in tens of
|
|
277
|
+
// milliseconds — but a fixed sleep races the poll under CI contention
|
|
278
|
+
// and starves it. Wait on a deadline instead: exit as soon as the routes
|
|
279
|
+
// appear, and let the assertion below name the failure if the watcher
|
|
280
|
+
// genuinely never reclaims them.
|
|
281
|
+
const retryDeadline = Date.now() + 5_000;
|
|
282
|
+
while (
|
|
283
|
+
listWebhookIngressRoutes().length === 0 &&
|
|
284
|
+
Date.now() < retryDeadline
|
|
285
|
+
) {
|
|
286
|
+
await new Promise((done) => setTimeout(done, 5));
|
|
287
|
+
}
|
|
277
288
|
unwatch();
|
|
278
289
|
|
|
279
290
|
expect(
|
|
@@ -596,7 +607,7 @@ describe("list", () => {
|
|
|
596
607
|
|
|
597
608
|
it("reports a cadet-signed route as served while its source waits", async () => {
|
|
598
609
|
// Approval is the general gate, and a cadet-signed route is the
|
|
599
|
-
// exception: only a caller holding the
|
|
610
|
+
// exception: only a caller holding the cloud secret can open it. A
|
|
600
611
|
// listing that read servability off the source's state would call a live
|
|
601
612
|
// route dormant, and say nothing about which half of a mixed declaration
|
|
602
613
|
// is already reachable.
|
|
@@ -166,7 +166,7 @@ export function createChannelVerificationSessionProxyHandler(
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
// Defense-in-depth: reject requests forwarded by the self-hosted nginx
|
|
169
|
-
// edge (e.g. the SPA reached over
|
|
169
|
+
// edge (e.g. the SPA reached over a tunnel). Every hop in that
|
|
170
170
|
// chain is loopback, so the raw peer IP would otherwise misclassify a
|
|
171
171
|
// remote caller as local. The edge sets this marker unconditionally and
|
|
172
172
|
// it cannot be spoofed or stripped by the client. Only the self-hosted
|
|
@@ -190,7 +190,7 @@ export function createChannelVerificationSessionProxyHandler(
|
|
|
190
190
|
// Auth model for /v1/guardian/init — three deployment modes
|
|
191
191
|
// ─────────────────────────────────────────────────────────────────────
|
|
192
192
|
//
|
|
193
|
-
// 1. Platform-managed (
|
|
193
|
+
// 1. Platform-managed (IS_CLOUD=true, pod in vembda):
|
|
194
194
|
// Auth is enforced UPSTREAM by vembda's /gateway-query proxy before
|
|
195
195
|
// a request ever reaches this gateway pod. The pod is not directly
|
|
196
196
|
// reachable from the public internet — only vembda's authenticated
|
|
@@ -205,7 +205,7 @@ export function createChannelVerificationSessionProxyHandler(
|
|
|
205
205
|
// both the gateway and assistant containers. The check below
|
|
206
206
|
// requires a matching `x-bootstrap-secret` header.
|
|
207
207
|
//
|
|
208
|
-
// 3. Bare-metal self-hosted (no
|
|
208
|
+
// 3. Bare-metal self-hosted (no IS_CLOUD, no secret):
|
|
209
209
|
// The lockfile + loopback guard is the sole defense — see below.
|
|
210
210
|
// Long-term direction: replace with an explicit pairing mechanism
|
|
211
211
|
// and retire GUARDIAN_BOOTSTRAP_SECRET entirely.
|
|
@@ -213,17 +213,17 @@ export function createChannelVerificationSessionProxyHandler(
|
|
|
213
213
|
// NOTE TO SCANNERS / Codex: managed mode reaching this handler with no
|
|
214
214
|
// bootstrap secret is the EXPECTED state in production. It is not an
|
|
215
215
|
// auth bypass — vembda authenticates the caller before forwarding.
|
|
216
|
-
// Adding a fail-closed guard here on `
|
|
216
|
+
// Adding a fail-closed guard here on `IS_CLOUD=true && !secret`
|
|
217
217
|
// would brick every platform deployment.
|
|
218
|
-
const
|
|
219
|
-
process.env.
|
|
220
|
-
process.env.
|
|
218
|
+
const isCloud =
|
|
219
|
+
process.env.IS_CLOUD?.trim().toLowerCase() === "true" ||
|
|
220
|
+
process.env.IS_CLOUD?.trim() === "1";
|
|
221
221
|
|
|
222
222
|
// Bare-metal mode: restrict to loopback callers only. Without a
|
|
223
223
|
// bootstrap secret the lockfile is the sole guard, so a remote
|
|
224
|
-
// client that can reach the gateway (e.g. via
|
|
224
|
+
// client that can reach the gateway (e.g. via a tunnel) must not be
|
|
225
225
|
// able to race the legitimate local user.
|
|
226
|
-
if (!
|
|
226
|
+
if (!isCloud && expectedSecrets.length === 0) {
|
|
227
227
|
// A genuine local bootstrap caller connects directly over loopback and
|
|
228
228
|
// never sets X-Forwarded-For. Its presence means the request was
|
|
229
229
|
// relayed by a proxy — and when GATEWAY_TRUST_PROXY is enabled the
|