@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
|
@@ -34,13 +34,12 @@ mock.module("../fetch.js", () => ({
|
|
|
34
34
|
let testAssistantDb: Database | null = null;
|
|
35
35
|
|
|
36
36
|
mock.module("../db/assistant-db-proxy.js", () => ({
|
|
37
|
-
|
|
38
37
|
async assistantDbQuery(sql: string, bind?: any[]) {
|
|
39
38
|
if (!testAssistantDb) throw new Error("test assistant DB not initialized");
|
|
40
39
|
const stmt = testAssistantDb.prepare(sql);
|
|
41
40
|
return bind ? stmt.all(...bind) : stmt.all();
|
|
42
41
|
},
|
|
43
|
-
|
|
42
|
+
|
|
44
43
|
async assistantDbRun(sql: string, bind?: any[]) {
|
|
45
44
|
if (!testAssistantDb) throw new Error("test assistant DB not initialized");
|
|
46
45
|
const stmt = testAssistantDb.prepare(sql);
|
|
@@ -159,7 +158,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
159
158
|
routingEntries: [],
|
|
160
159
|
port: 7830,
|
|
161
160
|
runtimeProxyRequireAuth: true,
|
|
162
|
-
shutdownDrainMs: 5000,
|
|
163
161
|
runtimeTimeoutMs: 30000,
|
|
164
162
|
runtimeMaxRetries: 2,
|
|
165
163
|
runtimeInitialBackoffMs: 500,
|
|
@@ -738,7 +736,7 @@ describe("guardian/reset-bootstrap", () => {
|
|
|
738
736
|
describe("guardian/init bare-metal loopback gating", () => {
|
|
739
737
|
test("rejects non-loopback clients in bare-metal mode", async () => {
|
|
740
738
|
delete process.env.GUARDIAN_BOOTSTRAP_SECRET;
|
|
741
|
-
delete process.env.
|
|
739
|
+
delete process.env.IS_CLOUD;
|
|
742
740
|
const handler = createChannelVerificationSessionProxyHandler(makeConfig());
|
|
743
741
|
const res = await handler.handleGuardianInit(
|
|
744
742
|
new Request("http://localhost:7830/v1/guardian/init", {
|
|
@@ -756,7 +754,7 @@ describe("guardian/init bare-metal loopback gating", () => {
|
|
|
756
754
|
|
|
757
755
|
test("allows loopback clients in bare-metal mode", async () => {
|
|
758
756
|
delete process.env.GUARDIAN_BOOTSTRAP_SECRET;
|
|
759
|
-
delete process.env.
|
|
757
|
+
delete process.env.IS_CLOUD;
|
|
760
758
|
const handler = createChannelVerificationSessionProxyHandler(makeConfig());
|
|
761
759
|
const res = await handler.handleGuardianInit(
|
|
762
760
|
new Request("http://localhost:7830/v1/guardian/init", {
|
|
@@ -792,9 +790,9 @@ describe("guardian/init bare-metal loopback gating", () => {
|
|
|
792
790
|
expect(body.accessToken).toBeTruthy();
|
|
793
791
|
});
|
|
794
792
|
|
|
795
|
-
test("skips loopback check in
|
|
793
|
+
test("skips loopback check in cloud-managed mode (IS_CLOUD=true)", async () => {
|
|
796
794
|
delete process.env.GUARDIAN_BOOTSTRAP_SECRET;
|
|
797
|
-
process.env.
|
|
795
|
+
process.env.IS_CLOUD = "true";
|
|
798
796
|
try {
|
|
799
797
|
const handler =
|
|
800
798
|
createChannelVerificationSessionProxyHandler(makeConfig());
|
|
@@ -814,7 +812,7 @@ describe("guardian/init bare-metal loopback gating", () => {
|
|
|
814
812
|
const body = await res.json();
|
|
815
813
|
expect(body.accessToken).toBeTruthy();
|
|
816
814
|
} finally {
|
|
817
|
-
delete process.env.
|
|
815
|
+
delete process.env.IS_CLOUD;
|
|
818
816
|
}
|
|
819
817
|
});
|
|
820
818
|
});
|
|
@@ -23,8 +23,8 @@ mock.module("../auth/guardian-bootstrap.js", () => ({
|
|
|
23
23
|
let mockReadCredential = mock(
|
|
24
24
|
async (_key: string): Promise<string | undefined> => TUNNEL_USER_ID,
|
|
25
25
|
);
|
|
26
|
-
mock.module("../
|
|
27
|
-
|
|
26
|
+
mock.module("../cloud-user-id.js", () => ({
|
|
27
|
+
readStoredCloudUserId: async () => ({
|
|
28
28
|
userId: await mockReadCredential("cadet:platform_user_id"),
|
|
29
29
|
unreachable: false,
|
|
30
30
|
}),
|
|
@@ -44,7 +44,7 @@ beforeEach(() => {
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
afterEach(() => {
|
|
47
|
-
delete process.env.
|
|
47
|
+
delete process.env.IS_CLOUD;
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -130,7 +130,7 @@ describe("authorizeGuardianStream: the actor token path", () => {
|
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
* The
|
|
133
|
+
* The cloud path, where tunnel validated the browser's token and injected the
|
|
134
134
|
* caller. The attestation proves the caller is *a* platform user who traversed
|
|
135
135
|
* tunnel, not that they are this assistant's guardian, so it is cross-checked
|
|
136
136
|
* against the stored `platform_user_id`.
|
|
@@ -158,9 +158,9 @@ describe("authorizeGuardianStream: the tunnel-attested managed path", () => {
|
|
|
158
158
|
tunnelBaseUrl?: string;
|
|
159
159
|
}) => {
|
|
160
160
|
if (managed) {
|
|
161
|
-
process.env.
|
|
161
|
+
process.env.IS_CLOUD = "true";
|
|
162
162
|
} else {
|
|
163
|
-
delete process.env.
|
|
163
|
+
delete process.env.IS_CLOUD;
|
|
164
164
|
}
|
|
165
165
|
const headers = new Headers({
|
|
166
166
|
"x-tunnel-user-id": userId,
|
|
@@ -205,7 +205,7 @@ describe("authorizeGuardianStream: the tunnel-attested managed path", () => {
|
|
|
205
205
|
expect(res!.status).toBe(403);
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
-
test("answers 503 when the
|
|
208
|
+
test("answers 503 when the cloud user lookup fails", async () => {
|
|
209
209
|
mockReadCredential = mock(async () => {
|
|
210
210
|
throw new Error("credential store down");
|
|
211
211
|
});
|
|
@@ -280,7 +280,7 @@ describe("authorizeGuardianStream: the tunnel-attested managed path", () => {
|
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
/**
|
|
283
|
-
* The
|
|
283
|
+
* The cloud path skips the shared gate, so the upgrade header is checked
|
|
284
284
|
* before it: a plain managed request must get 426, not a failed upgrade.
|
|
285
285
|
*/
|
|
286
286
|
test("answers 426 for a plain request even when tunnel attests the guardian", async () => {
|
|
@@ -237,7 +237,7 @@ describe("re-seen identity", () => {
|
|
|
237
237
|
status: "unverified",
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
-
// The
|
|
240
|
+
// The cloud profile says "Alice Example", a DM supplies the chat id.
|
|
241
241
|
await upsertContactChannel({
|
|
242
242
|
sourceChannel: "slack",
|
|
243
243
|
externalUserId: SLACK_USER,
|
|
@@ -36,7 +36,7 @@ mock.module("../auth/guardian-bootstrap.js", () => ({
|
|
|
36
36
|
findCadetGuardian: () => mockFindCadetGuardian(),
|
|
37
37
|
}));
|
|
38
38
|
|
|
39
|
-
// The
|
|
39
|
+
// The cloud/tunnel path cross-checks the attested caller against the stored
|
|
40
40
|
// `platform_user_id`. Default the credential to `TUNNEL_USER_ID` so the tunnel
|
|
41
41
|
// success cases match; negative cases override this mock.
|
|
42
42
|
let mockReadCredential = mock(
|
|
@@ -45,8 +45,8 @@ let mockReadCredential = mock(
|
|
|
45
45
|
mock.module("../credential-reader.js", () => ({
|
|
46
46
|
readCredential: (key: string) => mockReadCredential(key),
|
|
47
47
|
}));
|
|
48
|
-
mock.module("../
|
|
49
|
-
|
|
48
|
+
mock.module("../cloud-user-id.js", () => ({
|
|
49
|
+
readStoredCloudUserId: async () => {
|
|
50
50
|
const userId = await mockReadCredential("cadet:platform_user_id");
|
|
51
51
|
return { userId, unreachable: false };
|
|
52
52
|
},
|
|
@@ -273,13 +273,13 @@ describe("createLiveVoiceWebsocketHandler — tunnel-attested managed auth", ()
|
|
|
273
273
|
const TEST_TOKEN = mintEdgeToken();
|
|
274
274
|
// TUNNEL_USER_ID is declared at module scope (shared with the credential mock).
|
|
275
275
|
const TUNNEL_ORG_ID = "22222222-2222-2222-2222-222222222222";
|
|
276
|
-
const originalIsPlatform = process.env.
|
|
276
|
+
const originalIsPlatform = process.env.IS_CLOUD;
|
|
277
277
|
|
|
278
278
|
function setPlatform(on: boolean) {
|
|
279
279
|
if (on) {
|
|
280
|
-
process.env.
|
|
280
|
+
process.env.IS_CLOUD = "true";
|
|
281
281
|
} else {
|
|
282
|
-
delete process.env.
|
|
282
|
+
delete process.env.IS_CLOUD;
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
|
|
@@ -302,9 +302,9 @@ describe("createLiveVoiceWebsocketHandler — tunnel-attested managed auth", ()
|
|
|
302
302
|
|
|
303
303
|
afterEach(() => {
|
|
304
304
|
if (originalIsPlatform === undefined) {
|
|
305
|
-
delete process.env.
|
|
305
|
+
delete process.env.IS_CLOUD;
|
|
306
306
|
} else {
|
|
307
|
-
process.env.
|
|
307
|
+
process.env.IS_CLOUD = originalIsPlatform;
|
|
308
308
|
}
|
|
309
309
|
});
|
|
310
310
|
|
|
@@ -319,9 +319,9 @@ describe("createLiveVoiceWebsocketHandler — tunnel-attested managed auth", ()
|
|
|
319
319
|
});
|
|
320
320
|
|
|
321
321
|
/**
|
|
322
|
-
* Tunnel attests a
|
|
322
|
+
* Tunnel attests a cloud user, not an actor principal, so this path has
|
|
323
323
|
* no principal in the request at all. The binding supplies it, which is
|
|
324
|
-
* what lets the
|
|
324
|
+
* what lets the cloud deployment carry an identity downstream rather than
|
|
325
325
|
* leaving the daemon to resolve one of its own.
|
|
326
326
|
*/
|
|
327
327
|
test("managed mode carries the bound guardian's principal on the socket", async () => {
|
|
@@ -455,8 +455,8 @@ describe("createLiveVoiceWebsocketHandler — tunnel-attested managed auth", ()
|
|
|
455
455
|
|
|
456
456
|
/**
|
|
457
457
|
* A locally hosted assistant reaches the mobile app through a tunnel
|
|
458
|
-
* with
|
|
459
|
-
* as on a
|
|
458
|
+
* with IS_CLOUD unset. The attestation arrives through the same bridge
|
|
459
|
+
* as on a cloud pod, so the tunnel, not the env var, is what admits it.
|
|
460
460
|
*/
|
|
461
461
|
test("a locally hosted gateway with a tunnel trusts an attested caller with bridge proof", async () => {
|
|
462
462
|
setPlatform(false);
|
|
@@ -35,9 +35,8 @@ const { readRemoteFeatureFlags, clearRemoteFeatureFlagStoreCache } =
|
|
|
35
35
|
await import("../feature-flag-remote-store.js");
|
|
36
36
|
const { resetFeatureFlagDefaultsCache, _setRegistryCandidateOverrides } =
|
|
37
37
|
await import("../feature-flag-defaults.js");
|
|
38
|
-
const { resetEnvOverridesCache } =
|
|
39
|
-
"../feature-flag-env-overrides.js"
|
|
40
|
-
);
|
|
38
|
+
const { resetEnvOverridesCache } =
|
|
39
|
+
await import("../feature-flag-env-overrides.js");
|
|
41
40
|
|
|
42
41
|
// ---------------------------------------------------------------------------
|
|
43
42
|
// Test-local registry with a GA flag (defaultEnabled: true) for the
|
|
@@ -110,7 +109,6 @@ function fakeCredentialCache(
|
|
|
110
109
|
|
|
111
110
|
function defaultCredentials(): Record<string, string> {
|
|
112
111
|
return {
|
|
113
|
-
"credential/cadet/platform_base_url": "https://cloud.mycadet.ai",
|
|
114
112
|
"credential/cadet/assistant_api_key": "test-api-key",
|
|
115
113
|
};
|
|
116
114
|
}
|
|
@@ -118,16 +116,16 @@ function defaultCredentials(): Record<string, string> {
|
|
|
118
116
|
// ---------------------------------------------------------------------------
|
|
119
117
|
// Setup / teardown
|
|
120
118
|
// ---------------------------------------------------------------------------
|
|
121
|
-
const
|
|
119
|
+
const savedCadetCloudUrl = process.env.CADET_CLOUD_URL;
|
|
122
120
|
const savedAssistantCredential = process.env.ASSISTANT_API_KEY;
|
|
123
|
-
const savedDisablePlatform = process.env.
|
|
121
|
+
const savedDisablePlatform = process.env.CADET_DISABLE_CLOUD;
|
|
124
122
|
|
|
125
123
|
beforeEach(() => {
|
|
126
124
|
// Clear env vars that the production code falls back to, so tests remain
|
|
127
125
|
// deterministic unless they explicitly set them.
|
|
128
|
-
|
|
126
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
129
127
|
delete process.env.ASSISTANT_API_KEY;
|
|
130
|
-
delete process.env.
|
|
128
|
+
delete process.env.CADET_DISABLE_CLOUD;
|
|
131
129
|
mkdirSync(protectedDir, { recursive: true });
|
|
132
130
|
// Write the test registry and point resolution at it
|
|
133
131
|
writeFileSync(testRegistryPath, JSON.stringify(TEST_REGISTRY, null, 2));
|
|
@@ -147,9 +145,9 @@ afterEach(() => {
|
|
|
147
145
|
process.env[key] = saved;
|
|
148
146
|
}
|
|
149
147
|
};
|
|
150
|
-
restoreEnv("
|
|
148
|
+
restoreEnv("CADET_CLOUD_URL", savedCadetCloudUrl);
|
|
151
149
|
restoreEnv("ASSISTANT_API_KEY", savedAssistantCredential);
|
|
152
|
-
restoreEnv("
|
|
150
|
+
restoreEnv("CADET_DISABLE_CLOUD", savedDisablePlatform);
|
|
153
151
|
try {
|
|
154
152
|
rmSync(protectedDir, { recursive: true, force: true });
|
|
155
153
|
mkdirSync(protectedDir, { recursive: true });
|
|
@@ -168,7 +166,7 @@ afterEach(() => {
|
|
|
168
166
|
describe("RemoteFeatureFlagSync", () => {
|
|
169
167
|
test("skips sync when platform features are disabled", async () => {
|
|
170
168
|
fetchMock = mock(async () => Response.json({ flags: { ff1: true } }));
|
|
171
|
-
process.env.
|
|
169
|
+
process.env.CADET_DISABLE_CLOUD = "true";
|
|
172
170
|
|
|
173
171
|
const sync = new RemoteFeatureFlagSync({
|
|
174
172
|
credentials: fakeCredentialCache(defaultCredentials()),
|
|
@@ -179,12 +177,11 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
179
177
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
180
178
|
});
|
|
181
179
|
|
|
182
|
-
test("skips sync when no platform URL is available from
|
|
180
|
+
test("skips sync when no platform URL is available from env", async () => {
|
|
183
181
|
fetchMock = mock(async () => Response.json({ flags: { ff1: true } }));
|
|
184
182
|
|
|
185
183
|
const creds = defaultCredentials();
|
|
186
|
-
delete
|
|
187
|
-
delete process.env.CADET_PLATFORM_URL;
|
|
184
|
+
delete process.env.CADET_CLOUD_URL;
|
|
188
185
|
|
|
189
186
|
const sync = new RemoteFeatureFlagSync({
|
|
190
187
|
credentials: fakeCredentialCache(creds),
|
|
@@ -196,12 +193,11 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
196
193
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
197
194
|
});
|
|
198
195
|
|
|
199
|
-
test("
|
|
196
|
+
test("uses the CADET_CLOUD_URL env var", async () => {
|
|
200
197
|
fetchMock = mock(async () => Response.json({ flags: {} }));
|
|
201
|
-
process.env.
|
|
198
|
+
process.env.CADET_CLOUD_URL = "https://env-platform.example.com";
|
|
202
199
|
|
|
203
200
|
const creds = defaultCredentials();
|
|
204
|
-
delete creds["credential/cadet/platform_base_url"];
|
|
205
201
|
|
|
206
202
|
const sync = new RemoteFeatureFlagSync({
|
|
207
203
|
credentials: fakeCredentialCache(creds),
|
|
@@ -235,11 +231,11 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
235
231
|
expect(headers.Accept).toBe("application/json");
|
|
236
232
|
});
|
|
237
233
|
|
|
238
|
-
test("syncs when only
|
|
234
|
+
test("syncs when only cloudUrl and assistantApiKey are present", async () => {
|
|
239
235
|
fetchMock = mock(async () => Response.json({ flags: { ff1: true } }));
|
|
240
236
|
|
|
237
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
241
238
|
const creds = {
|
|
242
|
-
"credential/cadet/platform_base_url": "https://platform.example.com",
|
|
243
239
|
"credential/cadet/assistant_api_key": "test-api-key",
|
|
244
240
|
};
|
|
245
241
|
|
|
@@ -256,9 +252,8 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
256
252
|
fetchMock = mock(async () => Response.json({ flags: { ff1: true } }));
|
|
257
253
|
process.env.ASSISTANT_API_KEY = "env-key";
|
|
258
254
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
};
|
|
255
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
256
|
+
const creds = {};
|
|
262
257
|
|
|
263
258
|
const sync = new RemoteFeatureFlagSync({
|
|
264
259
|
credentials: fakeCredentialCache(creds),
|
|
@@ -299,8 +294,8 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
299
294
|
// with the NEW api key and overwrite the cached per-assistant flag values
|
|
300
295
|
// immediately, rather than serving the previous identity's (or registry
|
|
301
296
|
// default) values until the next ~5-min poll.
|
|
297
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
302
298
|
const cache = fakeCredentialCache({
|
|
303
|
-
"credential/cadet/platform_base_url": "https://platform.example.com",
|
|
304
299
|
"credential/cadet/assistant_api_key": "old-assistant-key",
|
|
305
300
|
});
|
|
306
301
|
|
|
@@ -317,8 +312,8 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
317
312
|
|
|
318
313
|
// Warm-pool claim: credentials now belong to the newly-assigned assistant,
|
|
319
314
|
// whose flag evaluation returns self-intro-greeting ON.
|
|
315
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
320
316
|
cache._setValues({
|
|
321
|
-
"credential/cadet/platform_base_url": "https://platform.example.com",
|
|
322
317
|
"credential/cadet/assistant_api_key": "new-assistant-key",
|
|
323
318
|
});
|
|
324
319
|
fetchMock = mock(async () =>
|
|
@@ -346,8 +341,8 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
346
341
|
// second syncNow() arriving while it is in flight must NOT be dropped by
|
|
347
342
|
// the re-entrancy guard — it must trigger one more fetch so the final
|
|
348
343
|
// identity's flags are cached, not the stale ones.
|
|
344
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
349
345
|
const cache = fakeCredentialCache({
|
|
350
|
-
"credential/cadet/platform_base_url": "https://platform.example.com",
|
|
351
346
|
"credential/cadet/assistant_api_key": "old-assistant-key",
|
|
352
347
|
});
|
|
353
348
|
|
|
@@ -375,8 +370,8 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
375
370
|
|
|
376
371
|
// Warm-pool claim: identity changes, then a second syncNow arrives while
|
|
377
372
|
// the first fetch is still in flight.
|
|
373
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
378
374
|
cache._setValues({
|
|
379
|
-
"credential/cadet/platform_base_url": "https://platform.example.com",
|
|
380
375
|
"credential/cadet/assistant_api_key": "new-assistant-key",
|
|
381
376
|
});
|
|
382
377
|
await sync.syncNow(); // returns immediately, records pendingResync
|
|
@@ -491,10 +486,8 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
491
486
|
test("constructs correct URL from platform base URL", async () => {
|
|
492
487
|
fetchMock = mock(async () => Response.json({ flags: {} }));
|
|
493
488
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
"credential/cadet/platform_base_url": "https://platform.example.com",
|
|
497
|
-
};
|
|
489
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
490
|
+
const creds = { ...defaultCredentials() };
|
|
498
491
|
const sync = new RemoteFeatureFlagSync({
|
|
499
492
|
credentials: fakeCredentialCache(creds),
|
|
500
493
|
});
|
|
@@ -504,7 +497,7 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
504
497
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
505
498
|
const [url] = fetchMock.mock.calls[0];
|
|
506
499
|
expect(url).toBe(
|
|
507
|
-
"https://
|
|
500
|
+
"https://test-cloud.mycadet.ai/v1/feature-flags/assistant-flag-values/",
|
|
508
501
|
);
|
|
509
502
|
});
|
|
510
503
|
|
|
@@ -572,10 +565,8 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
572
565
|
test("strips trailing slashes from platform URL", async () => {
|
|
573
566
|
fetchMock = mock(async () => Response.json({ flags: {} }));
|
|
574
567
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
"credential/cadet/platform_base_url": "https://platform.example.com///",
|
|
578
|
-
};
|
|
568
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai///";
|
|
569
|
+
const creds = { ...defaultCredentials() };
|
|
579
570
|
const sync = new RemoteFeatureFlagSync({
|
|
580
571
|
credentials: fakeCredentialCache(creds),
|
|
581
572
|
});
|
|
@@ -585,12 +576,12 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
585
576
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
586
577
|
const [url] = fetchMock.mock.calls[0];
|
|
587
578
|
expect(url).toBe(
|
|
588
|
-
"https://
|
|
579
|
+
"https://test-cloud.mycadet.ai/v1/feature-flags/assistant-flag-values/",
|
|
589
580
|
);
|
|
590
581
|
});
|
|
591
582
|
|
|
592
583
|
test("normalizes remote false for GA flags (defaultEnabled: true in registry)", async () => {
|
|
593
|
-
// The
|
|
584
|
+
// The cloud sends false for all flags it knows about (blanket-deny).
|
|
594
585
|
// GA flags (defaultEnabled: true in the registry) should not be disabled
|
|
595
586
|
// by remote overrides — only local persisted overrides can do that.
|
|
596
587
|
// Uses the test-local registry which defines test-ga-flag as GA
|
|
@@ -725,8 +716,8 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
725
716
|
test("trims whitespace from credential values", async () => {
|
|
726
717
|
fetchMock = mock(async () => Response.json({ flags: {} }));
|
|
727
718
|
|
|
719
|
+
process.env.CADET_CLOUD_URL = " https://test-cloud.mycadet.ai ";
|
|
728
720
|
const creds = {
|
|
729
|
-
"credential/cadet/platform_base_url": " https://platform.example.com ",
|
|
730
721
|
"credential/cadet/assistant_api_key": " trimmed-key ",
|
|
731
722
|
};
|
|
732
723
|
const sync = new RemoteFeatureFlagSync({
|
|
@@ -738,7 +729,7 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
738
729
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
739
730
|
const [url, init] = fetchMock.mock.calls[0];
|
|
740
731
|
expect(url).toBe(
|
|
741
|
-
"https://
|
|
732
|
+
"https://test-cloud.mycadet.ai/v1/feature-flags/assistant-flag-values/",
|
|
742
733
|
);
|
|
743
734
|
const headers = init?.headers as Record<string, string>;
|
|
744
735
|
expect(headers.Authorization).toBe("Api-Key trimmed-key");
|
|
@@ -905,17 +896,18 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
905
896
|
credentials: creds,
|
|
906
897
|
initialPollIntervalMs: 50,
|
|
907
898
|
});
|
|
899
|
+
// Missing platform URL from the start — polling should pause
|
|
900
|
+
delete process.env.CADET_CLOUD_URL;
|
|
908
901
|
await sync.start();
|
|
909
902
|
|
|
910
|
-
// No fetch calls — missing platform URL, polling should be paused
|
|
911
903
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
912
904
|
|
|
913
905
|
// Wait well past the initial poll interval — should still not poll
|
|
914
906
|
await new Promise((r) => setTimeout(r, 200));
|
|
915
907
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
916
908
|
|
|
917
|
-
// Simulate
|
|
918
|
-
|
|
909
|
+
// Simulate the cloud URL becoming available
|
|
910
|
+
process.env.CADET_CLOUD_URL = "https://test-cloud.mycadet.ai";
|
|
919
911
|
creds._invalidate();
|
|
920
912
|
|
|
921
913
|
// Wait for syncNow to complete (async, fire-and-forget from callback)
|
|
@@ -949,9 +941,7 @@ describe("RemoteFeatureFlagSync", () => {
|
|
|
949
941
|
expect(readRemoteFeatureFlags()).toEqual({ "per-assistant-flag": true });
|
|
950
942
|
|
|
951
943
|
// Simulate transient API key loss (CES hiccup) — platform URL still present
|
|
952
|
-
cache._setValues({
|
|
953
|
-
"credential/cadet/platform_base_url": "https://cloud.mycadet.ai",
|
|
954
|
-
});
|
|
944
|
+
cache._setValues({});
|
|
955
945
|
fetchMock = mock(async () =>
|
|
956
946
|
Response.json({ flags: { "per-assistant-flag": false } }),
|
|
957
947
|
);
|
|
@@ -210,7 +210,7 @@ const EXCLUDED_FROM_SCHEMA = new Set([
|
|
|
210
210
|
"/v1/remote-web/pairing-requests",
|
|
211
211
|
"/v1/remote-web/pairing-requests/approve",
|
|
212
212
|
"/v1/remote-web/pairing-requests/deny",
|
|
213
|
-
// Shared-invite redemption, called by the
|
|
213
|
+
// Shared-invite redemption, called by the cloud on an invitee's behalf;
|
|
214
214
|
// not part of the public gateway API
|
|
215
215
|
"/v1/shared/invites/redeem",
|
|
216
216
|
// Loopback-only device management — not part of the public gateway API
|
|
@@ -227,6 +227,9 @@ const EXCLUDED_FROM_SCHEMA = new Set([
|
|
|
227
227
|
"/v1/speech/stt/stream",
|
|
228
228
|
"/v2/speech/stt/stream",
|
|
229
229
|
"/v1/speech/tts/stream",
|
|
230
|
+
// Loopback-only ingress domain recording — the local CLI reports its
|
|
231
|
+
// tunnel's public URL (bearer-authed); not part of the public gateway API
|
|
232
|
+
"/v1/ingress/domains",
|
|
230
233
|
// Plugin-declared webhooks — which paths exist is decided at runtime by the
|
|
231
234
|
// installed plugins and the guardian's approvals, so there is no fixed set
|
|
232
235
|
// to document. The declaration schema is the contract, not this path.
|
|
@@ -42,7 +42,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
42
42
|
routingEntries: [],
|
|
43
43
|
port: 7830,
|
|
44
44
|
runtimeProxyRequireAuth: true,
|
|
45
|
-
shutdownDrainMs: 5000,
|
|
46
45
|
runtimeTimeoutMs: 30000,
|
|
47
46
|
runtimeMaxRetries: 2,
|
|
48
47
|
runtimeInitialBackoffMs: 500,
|
|
@@ -340,7 +339,7 @@ describe("resolvePublicHttpBaseUrl", () => {
|
|
|
340
339
|
expect(result).toBe("https://assistant.example.com");
|
|
341
340
|
});
|
|
342
341
|
|
|
343
|
-
test("does not read the
|
|
342
|
+
test("does not read the cloud assistant id when the config URL resolves", async () => {
|
|
344
343
|
const platformIdGetter = getId("assistant-123");
|
|
345
344
|
await resolvePublicHttpBaseUrl(
|
|
346
345
|
makeConfig({ tunnelBaseUrl: "https://tunnel-dev.mycadet.ai" }),
|
|
@@ -45,7 +45,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
45
45
|
routingEntries: [],
|
|
46
46
|
port: 7830,
|
|
47
47
|
runtimeProxyRequireAuth: true,
|
|
48
|
-
shutdownDrainMs: 5000,
|
|
49
48
|
runtimeTimeoutMs: 30000,
|
|
50
49
|
runtimeMaxRetries: 2,
|
|
51
50
|
runtimeInitialBackoffMs: 500,
|
|
@@ -412,7 +411,9 @@ describe("runtime proxy OAuth grant path rewrite", () => {
|
|
|
412
411
|
},
|
|
413
412
|
);
|
|
414
413
|
|
|
415
|
-
expect(url).toBe(
|
|
414
|
+
expect(url).toBe(
|
|
415
|
+
"http://localhost:7821/v1/oauth/proxy/stripe_link/v1/charges",
|
|
416
|
+
);
|
|
416
417
|
});
|
|
417
418
|
|
|
418
419
|
test("does not rewrite a host-absolute path without a proxy grant", async () => {
|
|
@@ -161,7 +161,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
161
161
|
routingEntries: [],
|
|
162
162
|
port: 7830,
|
|
163
163
|
runtimeProxyRequireAuth: true,
|
|
164
|
-
shutdownDrainMs: 5000,
|
|
165
164
|
runtimeTimeoutMs: 30000,
|
|
166
165
|
runtimeMaxRetries: 2,
|
|
167
166
|
runtimeInitialBackoffMs: 500,
|