@mycadet/gateway 0.12.7 → 0.12.9

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.
Files changed (179) hide show
  1. package/README.md +3 -3
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/package.json +2 -2
  3. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/{platform-credential.ts → cloud-credential.ts} +9 -9
  4. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  5. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  6. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  7. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  8. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  9. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  10. package/node_modules/@mycadet/ces-client/package.json +1 -1
  11. package/node_modules/@mycadet/ces-client/src/__tests__/ces-client.test.ts +14 -14
  12. package/node_modules/@mycadet/ces-client/src/rpc-client.ts +1 -1
  13. package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  14. package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  15. package/node_modules/@mycadet/environments/src/types.ts +6 -6
  16. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/package.json +2 -2
  17. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/{platform-credential.ts → cloud-credential.ts} +9 -9
  18. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  19. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  20. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  21. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  22. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  23. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  24. package/node_modules/@mycadet/gateway-client/src/binding-strength-contract.ts +5 -5
  25. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +1 -1
  26. package/node_modules/@mycadet/gateway-client/src/inbound-contract.ts +2 -2
  27. package/node_modules/@mycadet/gateway-client/src/outbound-contract.ts +1 -1
  28. package/node_modules/@mycadet/service-contracts/package.json +2 -2
  29. package/node_modules/@mycadet/service-contracts/src/{platform-credential.ts → cloud-credential.ts} +9 -9
  30. package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  31. package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  32. package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  33. package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  34. package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  35. package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  36. package/openapi.json +17 -17
  37. package/package.json +1 -1
  38. package/scripts/generate-openapi.ts +1 -1
  39. package/src/__tests__/actor-token-revocation.test.ts +0 -1
  40. package/src/__tests__/{backup-worker-platform-mode.test.ts → backup-worker-cloud-mode.test.ts} +4 -4
  41. package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
  42. package/src/__tests__/{platform-identity.test.ts → cloud-identity.test.ts} +72 -86
  43. package/src/__tests__/config.test.ts +0 -24
  44. package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -1
  45. package/src/__tests__/credential-requests.test.ts +6 -6
  46. package/src/__tests__/{credential-watcher-managed-bootstrap.test.ts → credential-watcher-cloud-bootstrap.test.ts} +1 -1
  47. package/src/__tests__/edge-auth.test.ts +18 -18
  48. package/src/__tests__/edge-guardian-auth.test.ts +10 -10
  49. package/src/__tests__/guardian-init-lockfile.test.ts +6 -8
  50. package/src/__tests__/guardian-pin.test.ts +8 -8
  51. package/src/__tests__/handle-inbound-admission.test.ts +0 -1
  52. package/src/__tests__/handle-inbound-app-context.test.ts +0 -1
  53. package/src/__tests__/handle-inbound-invite-intercept.test.ts +0 -1
  54. package/src/__tests__/handle-inbound-trust-verdict.test.ts +0 -1
  55. package/src/__tests__/inbound-contact-seed.test.ts +1 -1
  56. package/src/__tests__/live-voice-websocket.test.ts +12 -12
  57. package/src/__tests__/load-guards.test.ts +0 -1
  58. package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -1
  59. package/src/__tests__/oauth-callback.test.ts +0 -1
  60. package/src/__tests__/remote-feature-flag-sync.test.ts +35 -45
  61. package/src/__tests__/resolve-assistant.test.ts +0 -1
  62. package/src/__tests__/route-schema-guard.test.ts +4 -1
  63. package/src/__tests__/runtime-client.test.ts +1 -2
  64. package/src/__tests__/runtime-health-proxy.test.ts +0 -1
  65. package/src/__tests__/runtime-proxy-auth.test.ts +3 -2
  66. package/src/__tests__/runtime-proxy-device-stamp.test.ts +0 -1
  67. package/src/__tests__/runtime-proxy-error-body.test.ts +0 -1
  68. package/src/__tests__/runtime-proxy.test.ts +0 -1
  69. package/src/__tests__/runtime-stream-test-utils.ts +0 -1
  70. package/src/__tests__/shared-invite-registration.test.ts +15 -32
  71. package/src/__tests__/slack-control-plane-proxy.test.ts +0 -1
  72. package/src/__tests__/slack-display-name.test.ts +0 -1
  73. package/src/__tests__/slack-normalize.test.ts +0 -1
  74. package/src/__tests__/slack-reaction-normalize.test.ts +0 -1
  75. package/src/__tests__/slack-socket-mode-catchup.test.ts +0 -1
  76. package/src/__tests__/slack-socket-mode-group-dm.test.ts +0 -1
  77. package/src/__tests__/slack-socket-mode-liveness.test.ts +0 -1
  78. package/src/__tests__/slack-socket-mode-outbound-root.test.ts +0 -1
  79. package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +0 -1
  80. package/src/__tests__/speech-relay-websocket.test.ts +16 -14
  81. package/src/__tests__/stt-stream-websocket.test.ts +5 -5
  82. package/src/__tests__/telegram-control-plane-proxy.test.ts +0 -1
  83. package/src/__tests__/telegram-webhook-drop-visibility.test.ts +0 -1
  84. package/src/__tests__/telegram-webhook-handler.test.ts +0 -1
  85. package/src/__tests__/telegram-webhook-manager.test.ts +79 -80
  86. package/src/__tests__/twilio-media-websocket.test.ts +0 -1
  87. package/src/__tests__/twilio-webhooks.test.ts +6 -7
  88. package/src/__tests__/whatsapp-download.test.ts +0 -1
  89. package/src/__tests__/whatsapp-webhook.test.ts +0 -1
  90. package/src/attachments/ingest.test.ts +0 -1
  91. package/src/auth/guardian-bootstrap.ts +16 -15
  92. package/src/auth/scopes.ts +1 -1
  93. package/src/auth-fallback-reporter.ts +1 -1
  94. package/src/backup/backup-worker.ts +2 -2
  95. package/src/channels/inbound-event.ts +3 -3
  96. package/src/channels/ingress-inbound-vendors.test.ts +2 -2
  97. package/src/channels/ingress-inbound.ts +1 -1
  98. package/src/channels/ingress-verification.test.ts +2 -2
  99. package/src/channels/ingress-verification.ts +4 -4
  100. package/src/channels/plugin-ingress-approvals.test.ts +1 -1
  101. package/src/channels/plugin-ingress-approvals.ts +1 -1
  102. package/src/channels/plugin-ingress.test.ts +3 -3
  103. package/src/channels/plugin-ingress.ts +3 -3
  104. package/src/channels/room-admission.ts +2 -2
  105. package/src/cli/contacts.ts +3 -1
  106. package/src/cli/index.ts +1 -1
  107. package/src/{platform-identity.ts → cloud-identity.ts} +60 -68
  108. package/src/cloud-url.ts +25 -0
  109. package/src/{platform-user-id.ts → cloud-user-id.ts} +4 -4
  110. package/src/config.ts +2 -4
  111. package/src/credential-reader.ts +2 -2
  112. package/src/db/assistant-db-proxy.ts +1 -1
  113. package/src/discord/forward.ts +1 -1
  114. package/src/email/normalize.ts +4 -4
  115. package/src/email/register-callback.test.ts +27 -34
  116. package/src/email/register-callback.ts +21 -26
  117. package/src/feature-flag-registry.json +5 -5
  118. package/src/feature-flag-remote-store.ts +2 -2
  119. package/src/feature-flag-resolver.ts +5 -5
  120. package/src/http/cadet-signature.ts +1 -1
  121. package/src/http/edge-forwarded-header.ts +1 -1
  122. package/src/http/loopback-guard.ts +1 -1
  123. package/src/http/middleware/auth.ts +22 -22
  124. package/src/http/router.ts +1 -1
  125. package/src/http/routes/auto-approve-thresholds-routes.ts +1 -1
  126. package/src/http/routes/cadet-identity.ts +2 -2
  127. package/src/http/routes/channel-admission-policy-routes.ts +2 -2
  128. package/src/http/routes/channel-ingress-routes.ts +3 -3
  129. package/src/http/routes/channel-ingress.test.ts +13 -2
  130. package/src/http/routes/channel-permission-overrides-routes.ts +3 -3
  131. package/src/http/routes/channel-verification-session-proxy.ts +8 -22
  132. package/src/http/routes/{platform-push-proxy.test.ts → cloud-push-proxy.test.ts} +21 -22
  133. package/src/http/routes/{platform-push-proxy.ts → cloud-push-proxy.ts} +13 -14
  134. package/src/http/routes/contacts-control-plane-proxy.ts +3 -3
  135. package/src/http/routes/contacts-control-plane-routes.ts +5 -5
  136. package/src/http/routes/email-webhook.test.ts +2 -3
  137. package/src/http/routes/email-webhook.ts +4 -4
  138. package/src/http/routes/feature-flags.ts +1 -1
  139. package/src/http/routes/guardian-channel-create.ts +3 -3
  140. package/src/http/routes/guardian-pin.ts +9 -9
  141. package/src/http/routes/inbound-register.ts +2 -2
  142. package/src/http/routes/ingress-domains.test.ts +129 -0
  143. package/src/http/routes/ingress-domains.ts +73 -0
  144. package/src/http/routes/live-voice-websocket.ts +2 -2
  145. package/src/http/routes/log-export.test.ts +0 -1
  146. package/src/http/routes/log-tail.test.ts +0 -1
  147. package/src/http/routes/migration-proxy.ts +1 -1
  148. package/src/http/routes/plugin-webhook-websocket.test.ts +3 -3
  149. package/src/http/routes/plugin-webhook.test.ts +9 -8
  150. package/src/http/routes/plugin-webhook.ts +1 -1
  151. package/src/http/routes/speech-relay-websocket.ts +8 -11
  152. package/src/http/routes/stt-stream-websocket.ts +1 -1
  153. package/src/http/routes/telegram-webhook.test.ts +0 -1
  154. package/src/http/routes/trust-rules-routes.ts +2 -2
  155. package/src/http/routes/twilio-voice-verify-callback.ts +3 -3
  156. package/src/http/routes/twilio-voice-webhook.test.ts +3 -4
  157. package/src/http/routes/twilio-voice-webhook.ts +3 -3
  158. package/src/http/routes/whatsapp-webhook.test.ts +0 -1
  159. package/src/index.ts +57 -56
  160. package/src/ipc/credential-request-handlers.ts +4 -4
  161. package/src/ipc/debug-export-handlers.test.ts +0 -1
  162. package/src/ipc/log-tail-handlers.test.ts +0 -1
  163. package/src/ipc/risk-classification-handlers.test.ts +4 -4
  164. package/src/ipc/risk-classification-handlers.ts +2 -2
  165. package/src/remote-feature-flag-sync.ts +17 -19
  166. package/src/risk/command-registry/commands/assistant.ts +2 -2
  167. package/src/risk/skill-risk-classifier.test.ts +24 -24
  168. package/src/risk/skill-risk-classifier.ts +10 -10
  169. package/src/runtime/client.ts +17 -17
  170. package/src/schema.ts +29 -29
  171. package/src/telegram/send.test.ts +0 -1
  172. package/src/telegram/webhook-manager.ts +33 -38
  173. package/src/tunnel/allowed-paths.test.ts +3 -3
  174. package/src/tunnel/allowed-paths.ts +1 -1
  175. package/src/tunnel/client.test.ts +20 -21
  176. package/src/tunnel/client.ts +16 -17
  177. package/src/verification/shared-invite-registration.ts +22 -17
  178. package/src/version.ts +1 -1
  179. package/src/platform-url.ts +0 -24
@@ -43,7 +43,7 @@ export const ROUTES: GatewayRouteDefinition[] = [
43
43
  operationId: "channelPermissionOverridesList",
44
44
  summary: "List channel-permission matrix cells",
45
45
  description:
46
- "Returns every persisted cell (cascade selector × contact-type → RiskThreshold). Unset cells fall through the cascade; the list contains only explicit overrides.",
46
+ "Returns every persisted cell in the cascade selector × contact-type → RiskThreshold map. Unset cells fall through the cascade; the list contains only explicit overrides.",
47
47
  tags: ["channel-permission-overrides"],
48
48
  responseBody: z.object({
49
49
  cells: z.array(ChannelPermissionCellRowSchema),
@@ -66,7 +66,7 @@ export const ROUTES: GatewayRouteDefinition[] = [
66
66
  operationId: "channelPermissionResolve",
67
67
  summary: "Resolve the effective channel-permission threshold",
68
68
  description:
69
- "Read-only cascade resolution for one coordinate: walks channel → channel_type → adapter → workspace for the given selector keys and contact-type, returning the winning cell's threshold and scope, or null when no cell matches (the caller then falls through to the global thresholds). Same resolver the runtime evaluator uses over IPC.",
69
+ "Read-only cascade resolution for one coordinate: walks channel → channel_type → adapter → workspace for the given selector keys and contact-type, returning the winning cell's threshold and scope, or null when no cell matches, in which case the caller falls through to the global thresholds. Same resolver the runtime evaluator uses over IPC.",
70
70
  tags: ["channel-permission-overrides"],
71
71
  requestBody: ResolveChannelPermissionRequestSchema,
72
72
  responseBody: z.object({
@@ -84,7 +84,7 @@ export const ROUTES: GatewayRouteDefinition[] = [
84
84
  operationId: "channelPermissionOverrideDelete",
85
85
  summary: "Delete a channel-permission matrix cell",
86
86
  description:
87
- "Removes one cell by its composite key (selector × contact-type), letting the next cascade tier up win. Returns whether a cell was removed.",
87
+ "Removes one cell by its composite key, selector × contact-type, letting the next cascade tier up win. Returns whether a cell was removed.",
88
88
  tags: ["channel-permission-overrides"],
89
89
  requestBody: ChannelPermissionCellKeySchema,
90
90
  responseBody: z.object({ removed: z.boolean() }),
@@ -19,6 +19,7 @@ import { getLogger } from "../../logger.js";
19
19
  import { isLoopbackAddress } from "../../util/is-loopback-address.js";
20
20
  import { TUNNEL_FORWARDED_HEADER } from "../../tunnel/bridge-utils.js";
21
21
  import { requestArrivedViaEdgeProxy } from "../edge-forwarded-header.js";
22
+ import { getIsCloud } from "../../cloud-url.js";
22
23
 
23
24
  const log = getLogger("channel-verification-session-proxy");
24
25
 
@@ -166,7 +167,7 @@ export function createChannelVerificationSessionProxyHandler(
166
167
  }
167
168
 
168
169
  // Defense-in-depth: reject requests forwarded by the self-hosted nginx
169
- // edge (e.g. the SPA reached over an ngrok tunnel). Every hop in that
170
+ // edge (e.g. the SPA reached over a tunnel). Every hop in that
170
171
  // chain is loopback, so the raw peer IP would otherwise misclassify a
171
172
  // remote caller as local. The edge sets this marker unconditionally and
172
173
  // it cannot be spoofed or stripped by the client. Only the self-hosted
@@ -190,40 +191,25 @@ export function createChannelVerificationSessionProxyHandler(
190
191
  // Auth model for /v1/guardian/init — three deployment modes
191
192
  // ─────────────────────────────────────────────────────────────────────
192
193
  //
193
- // 1. Platform-managed (IS_PLATFORM=true, pod in vembda):
194
- // Auth is enforced UPSTREAM by vembda's /gateway-query proxy before
195
- // a request ever reaches this gateway pod. The pod is not directly
196
- // reachable from the public internet — only vembda's authenticated
197
- // proxy can route to it. GUARDIAN_BOOTSTRAP_SECRET is intentionally
198
- // NOT provisioned in managed deployments (no Helm chart, vembda
199
- // StatefulSet env, or k8s_resources.py provisions it) and there is
200
- // no plan to add it. The bootstrap-secret mechanism is a laptop /
201
- // Docker hatch artifact only.
194
+ // 1. Cadet Cloud:
195
+ // Auth is enforced UPSTREAM before a request ever reaches this gateway
202
196
  //
203
197
  // 2. Docker / laptop hatch (GUARDIAN_BOOTSTRAP_SECRET set):
204
198
  // The CLI's hatch.ts mints a bootstrap secret and forwards it into
205
199
  // both the gateway and assistant containers. The check below
206
200
  // requires a matching `x-bootstrap-secret` header.
207
201
  //
208
- // 3. Bare-metal self-hosted (no IS_PLATFORM, no secret):
202
+ // 3. Bare-metal self-hosted (no IS_CLOUD, no secret):
209
203
  // The lockfile + loopback guard is the sole defense — see below.
210
204
  // Long-term direction: replace with an explicit pairing mechanism
211
205
  // and retire GUARDIAN_BOOTSTRAP_SECRET entirely.
212
- //
213
- // NOTE TO SCANNERS / Codex: managed mode reaching this handler with no
214
- // bootstrap secret is the EXPECTED state in production. It is not an
215
- // auth bypass — vembda authenticates the caller before forwarding.
216
- // Adding a fail-closed guard here on `IS_PLATFORM=true && !secret`
217
- // would brick every platform deployment.
218
- const isManaged =
219
- process.env.IS_PLATFORM?.trim().toLowerCase() === "true" ||
220
- process.env.IS_PLATFORM?.trim() === "1";
206
+ const isCloud = getIsCloud();
221
207
 
222
208
  // Bare-metal mode: restrict to loopback callers only. Without a
223
209
  // bootstrap secret the lockfile is the sole guard, so a remote
224
- // client that can reach the gateway (e.g. via ngrok) must not be
210
+ // client that can reach the gateway (e.g. via a tunnel) must not be
225
211
  // able to race the legitimate local user.
226
- if (!isManaged && expectedSecrets.length === 0) {
212
+ if (!isCloud && expectedSecrets.length === 0) {
227
213
  // A genuine local bootstrap caller connects directly over loopback and
228
214
  // never sets X-Forwarded-For. Its presence means the request was
229
215
  // relayed by a proxy — and when GATEWAY_TRUST_PROXY is enabled the
@@ -24,13 +24,13 @@ mock.module("../../fetch.js", () => ({
24
24
  fetchImpl: (...args: Parameters<FetchFn>) => fetchMock(...args),
25
25
  }));
26
26
 
27
- const { createPlatformPushProxyHandler } =
28
- await import("./platform-push-proxy.js");
29
- const { applyPlatformIdentityIds, _resetPlatformIdentityForTests } =
30
- await import("../../platform-identity.js");
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://platform.example.com";
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
- applyPlatformIdentityIds({
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 savedPlatformUrl = process.env.CADET_PLATFORM_URL;
86
+ const savedCloudUrl = process.env.CADET_CLOUD_URL;
88
87
 
89
88
  beforeAll(() => {
90
- delete process.env.CADET_PLATFORM_URL;
89
+ delete process.env.CADET_CLOUD_URL;
91
90
  });
92
91
 
93
92
  afterAll(() => {
94
- if (savedPlatformUrl !== undefined) {
95
- process.env.CADET_PLATFORM_URL = savedPlatformUrl;
93
+ if (savedCloudUrl !== undefined) {
94
+ process.env.CADET_CLOUD_URL = savedCloudUrl;
96
95
  } else {
97
- delete process.env.CADET_PLATFORM_URL;
96
+ delete process.env.CADET_CLOUD_URL;
98
97
  }
99
98
  });
100
99
 
101
100
  afterEach(() => {
102
101
  fetchMock = mock(async () => new Response());
103
- _resetPlatformIdentityForTests();
102
+ _resetCloudIdentityForTests();
104
103
  });
105
104
 
106
105
  test("returns 503 and does not fetch when platform credentials are missing", async () => {
107
- const handler = createPlatformPushProxyHandler(makeKeyedCredentials({}));
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 = createPlatformPushProxyHandler(registeredCredentials());
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 = createPlatformPushProxyHandler(registeredCredentials());
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 = createPlatformPushProxyHandler(registeredCredentials());
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 = createPlatformPushProxyHandler(registeredCredentials());
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 = createPlatformPushProxyHandler(registeredCredentials());
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 platform request fails", async () => {
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 = createPlatformPushProxyHandler(registeredCredentials());
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 { resolvePlatformAssistantIdOrUndefined } from "../../platform-identity.js";
20
- import { getPlatformBaseUrl } from "../../platform-url.js";
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("platform-push-proxy");
23
+ const log = getLogger("cloud-push-proxy");
24
24
 
25
25
  const UPSTREAM_TIMEOUT_MS = 10_000;
26
26
 
27
27
  interface PlatformTarget {
28
- platformBaseUrl: string;
28
+ cloudBaseUrl: string;
29
29
  assistantApiKey: string;
30
30
  assistantId: string;
31
31
  }
32
32
 
33
- export function createPlatformPushProxyHandler(credentials: CredentialCache) {
33
+ export function createCloudPushProxyHandler(credentials: CredentialCache) {
34
34
  async function resolvePlatformTarget(): Promise<PlatformTarget | Response> {
35
- const [platformBaseUrl, assistantApiKeyRaw, assistantId] =
36
- await Promise.all([
37
- getPlatformBaseUrl(credentials),
38
- credentials.get(credentialKey("cadet", "assistant_api_key")),
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 (!platformBaseUrl || !assistantApiKey || !assistantId) {
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 { platformBaseUrl, assistantApiKey, assistantId };
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.platformBaseUrl}/v1/assistants/${target.assistantId}${resourcePath}${search}`;
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 platform before the row is
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 platform registration
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(
@@ -364,7 +364,7 @@ export async function createInviteNative(
364
364
  }
365
365
  if (!isValidE164(input.expectedExternalUserId)) {
366
366
  throw new InviteNativeError(
367
- "expectedExternalUserId must be in E.164 format (e.g., +15551234567)",
367
+ "expectedExternalUserId must be in E.164 format, e.g. +15551234567",
368
368
  400,
369
369
  "BAD_REQUEST",
370
370
  );
@@ -36,7 +36,7 @@ const ContactChannelPayloadSchema = z.object({
36
36
  externalChatId: z.string().nullable(),
37
37
  externalUserId: z
38
38
  .string()
39
- .describe("Compat alias for address (older macOS clients)"),
39
+ .describe("Compat alias for address, for older macOS clients"),
40
40
  status: z.string().nullable(),
41
41
  policy: z.string().nullable(),
42
42
  verifiedAt: z.number().nullable(),
@@ -69,7 +69,7 @@ const ContactPayloadSchema = z.object({
69
69
  interactionCount: z.number(),
70
70
  lastInteraction: z.number().nullable(),
71
71
  autoApproveThreshold: RiskThresholdSchema.nullable().describe(
72
- "Per-contact auto-approve ceiling. Null means unset (inherit cascade).",
72
+ "Per-contact auto-approve ceiling. Null means unset, inheriting the cascade.",
73
73
  ),
74
74
  assistantMetadata: AssistantContactMetadataSchema.nullable(),
75
75
  channels: z.array(ContactChannelPayloadSchema),
@@ -92,7 +92,7 @@ const UpsertContactChannelInputSchema = z.object({
92
92
  .string()
93
93
  .optional()
94
94
  .describe(
95
- "Delivery chat id. Omit to preserve the stored value. A legacy explicit null is accepted for compatibility and treated as omitted; clearing is not supported (a stored null is indistinguishable from never-learned, so no consumer could tell a clear from a gap).",
95
+ "Delivery chat id. Omit to preserve the stored value. A legacy explicit null is accepted for compatibility and treated as omitted; clearing is not supported: a stored null is indistinguishable from never-learned, so no consumer could tell a clear from a gap.",
96
96
  ),
97
97
  status: z.string().optional(),
98
98
  policy: z.string().optional(),
@@ -200,7 +200,7 @@ export const ROUTES: GatewayRouteDefinition[] = [
200
200
  operationId: "contactsUpsert",
201
201
  summary: "Create or update a contact",
202
202
  description:
203
- "Gateway-native contact upsert (dual-writes the gateway ACL store and the assistant info mirror). Matches by id, then by any provided (type, address) channel, else creates.",
203
+ "Gateway-native contact upsert that dual-writes the gateway ACL store and the assistant info mirror. Matches by id, then by the provided type and address channel, else creates.",
204
204
  tags: ["contacts"],
205
205
  requestBody: UpsertContactRequestSchema,
206
206
  responseBody: z.object({
@@ -265,7 +265,7 @@ export const ROUTES: GatewayRouteDefinition[] = [
265
265
  operationId: "contactChannelVerify",
266
266
  summary: "Manually verify a contact channel",
267
267
  description:
268
- "Guardian-only manual attestation: marks the channel active/verified in the gateway store (source of truth) with a best-effort assistant mirror. Idempotent.",
268
+ "Guardian-only manual attestation: marks the channel active/verified in the gateway store, the source of truth, with a best-effort assistant mirror. Idempotent.",
269
269
  tags: ["contacts"],
270
270
  pathParameters: [
271
271
  { name: "channel_id", description: "The contact-channel id" },
@@ -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 platform email relay
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 platform's retry delivery can be processed.
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 platform retries.
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 platform (vembda) to deliver,
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 platform can decide whether to persist the email and how to respond
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 platform (vembda) delivers any reply and tracks delivery.
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) {
@@ -126,7 +126,7 @@ export function createFeatureFlagsPatchHandler(onFlagChanged?: () => void) {
126
126
  return Response.json(
127
127
  {
128
128
  error:
129
- "Invalid flag key format. Must be a simple kebab-case string (e.g., 'browser', 'contacts')",
129
+ "Invalid flag key format. Must be a simple kebab-case string, e.g. 'browser' or 'contacts'",
130
130
  },
131
131
  { status: 400 },
132
132
  );
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * POST /v1/contacts/guardian/channel — create a guardian channel binding
3
- * via platform auto-verify.
3
+ * via cloud auto-verify.
4
4
  *
5
- * This route is called by the platform's `assistant email register` flow
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 platform auto-register",
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 managed pod, or a locally hosted assistant whose gateway dialed
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 { readStoredPlatformUserId } from "../../platform-user-id.js";
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 `IS_PLATFORM` check used by the gateway's HTTP edge auth
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.IS_PLATFORM?.trim().toLowerCase();
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 managed
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 platform bypass.
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 readStoredPlatformUserId();
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 managed path
176
- // below skips that gate entirely: without this, a managed caller sending 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 platform provides `guardian_email` in the request body. For BYO
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 platform-managed
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
+ });