@mycadet/gateway 0.12.7-dev.202610111049.d2b52a8 → 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.
Files changed (61) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
  2. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
  3. package/node_modules/@mycadet/service-contracts/src/ingress.ts +10 -4
  4. package/package.json +1 -1
  5. package/src/__tests__/actor-token-revocation.test.ts +0 -1
  6. package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
  7. package/src/__tests__/config.test.ts +0 -24
  8. package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -1
  9. package/src/__tests__/guardian-init-lockfile.test.ts +0 -1
  10. package/src/__tests__/handle-inbound-admission.test.ts +0 -1
  11. package/src/__tests__/handle-inbound-app-context.test.ts +0 -1
  12. package/src/__tests__/handle-inbound-invite-intercept.test.ts +0 -1
  13. package/src/__tests__/handle-inbound-trust-verdict.test.ts +0 -1
  14. package/src/__tests__/load-guards.test.ts +0 -1
  15. package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -1
  16. package/src/__tests__/oauth-callback.test.ts +0 -1
  17. package/src/__tests__/resolve-assistant.test.ts +0 -1
  18. package/src/__tests__/route-schema-guard.test.ts +3 -0
  19. package/src/__tests__/runtime-client.test.ts +0 -1
  20. package/src/__tests__/runtime-health-proxy.test.ts +0 -1
  21. package/src/__tests__/runtime-proxy-auth.test.ts +3 -2
  22. package/src/__tests__/runtime-proxy-device-stamp.test.ts +0 -1
  23. package/src/__tests__/runtime-proxy-error-body.test.ts +0 -1
  24. package/src/__tests__/runtime-proxy.test.ts +0 -1
  25. package/src/__tests__/runtime-stream-test-utils.ts +0 -1
  26. package/src/__tests__/slack-control-plane-proxy.test.ts +0 -1
  27. package/src/__tests__/slack-display-name.test.ts +0 -1
  28. package/src/__tests__/slack-normalize.test.ts +0 -1
  29. package/src/__tests__/slack-reaction-normalize.test.ts +0 -1
  30. package/src/__tests__/slack-socket-mode-catchup.test.ts +0 -1
  31. package/src/__tests__/slack-socket-mode-group-dm.test.ts +0 -1
  32. package/src/__tests__/slack-socket-mode-liveness.test.ts +0 -1
  33. package/src/__tests__/slack-socket-mode-outbound-root.test.ts +0 -1
  34. package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +0 -1
  35. package/src/__tests__/telegram-control-plane-proxy.test.ts +0 -1
  36. package/src/__tests__/telegram-webhook-drop-visibility.test.ts +0 -1
  37. package/src/__tests__/telegram-webhook-handler.test.ts +0 -1
  38. package/src/__tests__/twilio-media-websocket.test.ts +0 -1
  39. package/src/__tests__/twilio-webhooks.test.ts +0 -1
  40. package/src/__tests__/whatsapp-download.test.ts +0 -1
  41. package/src/__tests__/whatsapp-webhook.test.ts +0 -1
  42. package/src/attachments/ingest.test.ts +0 -1
  43. package/src/config.ts +0 -2
  44. package/src/http/edge-forwarded-header.ts +1 -1
  45. package/src/http/loopback-guard.ts +1 -1
  46. package/src/http/routes/channel-verification-session-proxy.ts +2 -2
  47. package/src/http/routes/email-webhook.test.ts +0 -1
  48. package/src/http/routes/ingress-domains.test.ts +129 -0
  49. package/src/http/routes/ingress-domains.ts +73 -0
  50. package/src/http/routes/log-export.test.ts +0 -1
  51. package/src/http/routes/log-tail.test.ts +0 -1
  52. package/src/http/routes/telegram-webhook.test.ts +0 -1
  53. package/src/http/routes/twilio-voice-webhook.test.ts +0 -1
  54. package/src/http/routes/whatsapp-webhook.test.ts +0 -1
  55. package/src/index.ts +12 -4
  56. package/src/ipc/debug-export-handlers.test.ts +0 -1
  57. package/src/ipc/log-tail-handlers.test.ts +0 -1
  58. package/src/telegram/send.test.ts +0 -1
  59. package/src/telegram/webhook-manager.ts +1 -1
  60. package/src/tunnel/client.test.ts +0 -1
  61. package/src/tunnel/client.ts +1 -1
@@ -100,11 +100,17 @@ export const INGRESS_PAIRING_TUNNEL_KEY = "pairingTunnel";
100
100
  export const INGRESS_ASSISTANT_ID_KEY = "assistantId";
101
101
 
102
102
  /**
103
- * The local tunnel providers the CLI can start, in the order they are offered.
104
- * `cadet tunnel`'s accepted `--provider` values and the persisted-record
105
- * validation below both derive from it.
103
+ * The tunnel providers the CLI offers, in the order they are presented —
104
+ * the managed `cadet` tunnel first (listed, not yet implemented), then the
105
+ * local ones. `cadet tunnel`'s accepted `--provider` values and the
106
+ * persisted-record validation below both derive from it.
106
107
  */
107
- export const TUNNEL_PROVIDERS = ["ngrok", "cloudflare", "tailscale"] as const;
108
+ export const TUNNEL_PROVIDERS = [
109
+ "cadet",
110
+ "ngrok",
111
+ "cloudflare",
112
+ "tailscale",
113
+ ] as const;
108
114
 
109
115
  export type TunnelProviderName = (typeof TUNNEL_PROVIDERS)[number];
110
116
 
@@ -100,11 +100,17 @@ export const INGRESS_PAIRING_TUNNEL_KEY = "pairingTunnel";
100
100
  export const INGRESS_ASSISTANT_ID_KEY = "assistantId";
101
101
 
102
102
  /**
103
- * The local tunnel providers the CLI can start, in the order they are offered.
104
- * `cadet tunnel`'s accepted `--provider` values and the persisted-record
105
- * validation below both derive from it.
103
+ * The tunnel providers the CLI offers, in the order they are presented —
104
+ * the managed `cadet` tunnel first (listed, not yet implemented), then the
105
+ * local ones. `cadet tunnel`'s accepted `--provider` values and the
106
+ * persisted-record validation below both derive from it.
106
107
  */
107
- export const TUNNEL_PROVIDERS = ["ngrok", "cloudflare", "tailscale"] as const;
108
+ export const TUNNEL_PROVIDERS = [
109
+ "cadet",
110
+ "ngrok",
111
+ "cloudflare",
112
+ "tailscale",
113
+ ] as const;
108
114
 
109
115
  export type TunnelProviderName = (typeof TUNNEL_PROVIDERS)[number];
110
116
 
@@ -100,11 +100,17 @@ export const INGRESS_PAIRING_TUNNEL_KEY = "pairingTunnel";
100
100
  export const INGRESS_ASSISTANT_ID_KEY = "assistantId";
101
101
 
102
102
  /**
103
- * The local tunnel providers the CLI can start, in the order they are offered.
104
- * `cadet tunnel`'s accepted `--provider` values and the persisted-record
105
- * validation below both derive from it.
103
+ * The tunnel providers the CLI offers, in the order they are presented —
104
+ * the managed `cadet` tunnel first (listed, not yet implemented), then the
105
+ * local ones. `cadet tunnel`'s accepted `--provider` values and the
106
+ * persisted-record validation below both derive from it.
106
107
  */
107
- export const TUNNEL_PROVIDERS = ["ngrok", "cloudflare", "tailscale"] as const;
108
+ export const TUNNEL_PROVIDERS = [
109
+ "cadet",
110
+ "ngrok",
111
+ "cloudflare",
112
+ "tailscale",
113
+ ] as const;
108
114
 
109
115
  export type TunnelProviderName = (typeof TUNNEL_PROVIDERS)[number];
110
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycadet/gateway",
3
- "version": "0.12.7-dev.202610111049.d2b52a8",
3
+ "version": "0.12.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -251,7 +251,6 @@ describe("runtime proxy enforcement", () => {
251
251
  routingEntries: [],
252
252
  port: 7830,
253
253
  runtimeProxyRequireAuth: true,
254
- shutdownDrainMs: 5000,
255
254
  runtimeTimeoutMs: 30000,
256
255
  runtimeMaxRetries: 2,
257
256
  runtimeInitialBackoffMs: 500,
@@ -26,7 +26,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
26
26
  routingEntries: [],
27
27
  port: 7830,
28
28
  runtimeProxyRequireAuth: true,
29
- shutdownDrainMs: 5000,
30
29
  runtimeTimeoutMs: 30000,
31
30
  runtimeMaxRetries: 2,
32
31
  runtimeInitialBackoffMs: 500,
@@ -7,30 +7,6 @@ import { testWorkspaceDir } from "./test-preload.js";
7
7
  import { loadConfig } from "../config.js";
8
8
 
9
9
  describe("config: hardcoded defaults", () => {
10
- test("returns expected hardcoded values", () => {
11
- const config = loadConfig();
12
- expect(config.shutdownDrainMs).toBe(5000);
13
- expect(config.runtimeTimeoutMs).toBe(30000);
14
- expect(config.runtimeMaxRetries).toBe(2);
15
- expect(config.runtimeInitialBackoffMs).toBe(500);
16
- expect(config.maxWebhookPayloadBytes).toBe(1024 * 1024);
17
- expect(config.maxEmailWebhookPayloadBytes).toBe(350 * 1024 * 1024);
18
- expect(config.maxAttachmentBytes).toEqual({
19
- telegram: 20 * 1024 * 1024,
20
- slack: 100 * 1024 * 1024,
21
- whatsapp: 16 * 1024 * 1024,
22
- discord: 100 * 1024 * 1024,
23
- email: 25 * 1024 * 1024,
24
- default: 100 * 1024 * 1024,
25
- });
26
- expect(config.maxAttachmentConcurrency).toBe(3);
27
- expect(config.runtimeProxyRequireAuth).toBe(true);
28
- expect(config.trustProxy).toBe(false);
29
- expect(config.routingEntries).toEqual([]);
30
- expect(config.logFile.dir).toMatch(/logs$/);
31
- expect(config.logFile.retentionDays).toBe(30);
32
- });
33
-
34
10
  test("GATEWAY_PORT defaults to 7830", () => {
35
11
  const saved = process.env.GATEWAY_PORT;
36
12
  delete process.env.GATEWAY_PORT;
@@ -425,7 +425,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
425
425
  routingEntries: [],
426
426
  port: 7830,
427
427
  runtimeProxyRequireAuth: true,
428
- shutdownDrainMs: 5000,
429
428
  runtimeTimeoutMs: 30000,
430
429
  runtimeMaxRetries: 2,
431
430
  runtimeInitialBackoffMs: 500,
@@ -158,7 +158,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
158
158
  routingEntries: [],
159
159
  port: 7830,
160
160
  runtimeProxyRequireAuth: true,
161
- shutdownDrainMs: 5000,
162
161
  runtimeTimeoutMs: 30000,
163
162
  runtimeMaxRetries: 2,
164
163
  runtimeInitialBackoffMs: 500,
@@ -90,7 +90,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
90
90
  runtimeMaxRetries: 2,
91
91
  runtimeProxyRequireAuth: false,
92
92
  runtimeTimeoutMs: 30000,
93
- shutdownDrainMs: 5000,
94
93
  trustProxy: false,
95
94
  ...overrides,
96
95
  } as GatewayConfig;
@@ -86,7 +86,6 @@ function makeConfig(): GatewayConfig {
86
86
  runtimeMaxRetries: 2,
87
87
  runtimeProxyRequireAuth: false,
88
88
  runtimeTimeoutMs: 30000,
89
- shutdownDrainMs: 5000,
90
89
  trustProxy: false,
91
90
  } as unknown as GatewayConfig;
92
91
  }
@@ -180,7 +180,6 @@ function makeConfig(): GatewayConfig {
180
180
  runtimeMaxRetries: 2,
181
181
  runtimeProxyRequireAuth: false,
182
182
  runtimeTimeoutMs: 30000,
183
- shutdownDrainMs: 5000,
184
183
  trustProxy: false,
185
184
  } as unknown as GatewayConfig;
186
185
  }
@@ -155,7 +155,6 @@ function makeConfig(): GatewayConfig {
155
155
  runtimeMaxRetries: 2,
156
156
  runtimeProxyRequireAuth: false,
157
157
  runtimeTimeoutMs: 30000,
158
- shutdownDrainMs: 5000,
159
158
  trustProxy: false,
160
159
  } as unknown as GatewayConfig;
161
160
  }
@@ -21,7 +21,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
21
21
  routingEntries: [],
22
22
  port: 7830,
23
23
  runtimeProxyRequireAuth: true,
24
- shutdownDrainMs: 5000,
25
24
  runtimeTimeoutMs: 30000,
26
25
  runtimeMaxRetries: 2,
27
26
  runtimeInitialBackoffMs: 500,
@@ -29,7 +29,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
29
29
  routingEntries: [],
30
30
  port: 7830,
31
31
  runtimeProxyRequireAuth: true,
32
- shutdownDrainMs: 5000,
33
32
  runtimeTimeoutMs: 30000,
34
33
  runtimeMaxRetries: 2,
35
34
  runtimeInitialBackoffMs: 500,
@@ -33,7 +33,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
33
33
  routingEntries: [],
34
34
  port: 7830,
35
35
  runtimeProxyRequireAuth: true,
36
- shutdownDrainMs: 5000,
37
36
  runtimeTimeoutMs: 30000,
38
37
  runtimeMaxRetries: 2,
39
38
  runtimeInitialBackoffMs: 500,
@@ -9,7 +9,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
9
9
  routingEntries: [],
10
10
  port: 7830,
11
11
  runtimeProxyRequireAuth: true,
12
- shutdownDrainMs: 5000,
13
12
  runtimeTimeoutMs: 30000,
14
13
  runtimeMaxRetries: 2,
15
14
  runtimeInitialBackoffMs: 500,
@@ -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,
@@ -26,7 +26,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
26
26
  routingEntries: [],
27
27
  port: 7830,
28
28
  runtimeProxyRequireAuth: true,
29
- shutdownDrainMs: 5000,
30
29
  runtimeTimeoutMs: 30000,
31
30
  runtimeMaxRetries: 2,
32
31
  runtimeInitialBackoffMs: 500,
@@ -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("http://localhost:7821/v1/oauth/proxy/stripe_link/v1/charges");
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,
@@ -75,7 +75,6 @@ function makeConfig(): GatewayConfig {
75
75
  routingEntries: [],
76
76
  port: 7830,
77
77
  runtimeProxyRequireAuth: true,
78
- shutdownDrainMs: 5000,
79
78
  runtimeTimeoutMs: 30000,
80
79
  runtimeMaxRetries: 2,
81
80
  runtimeInitialBackoffMs: 500,
@@ -26,7 +26,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
26
26
  routingEntries: [],
27
27
  port: 7830,
28
28
  runtimeProxyRequireAuth: false,
29
- shutdownDrainMs: 5000,
30
29
  runtimeTimeoutMs: 30000,
31
30
  runtimeMaxRetries: 2,
32
31
  runtimeInitialBackoffMs: 500,
@@ -50,7 +50,6 @@ export function makeConfig(
50
50
  routingEntries: [],
51
51
  port: 7830,
52
52
  runtimeProxyRequireAuth: true,
53
- shutdownDrainMs: 5000,
54
53
  runtimeTimeoutMs: 30000,
55
54
  runtimeMaxRetries: 2,
56
55
  runtimeInitialBackoffMs: 500,
@@ -26,7 +26,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
26
26
  routingEntries: [],
27
27
  port: 7830,
28
28
  runtimeProxyRequireAuth: true,
29
- shutdownDrainMs: 5000,
30
29
  runtimeTimeoutMs: 30000,
31
30
  runtimeMaxRetries: 2,
32
31
  runtimeInitialBackoffMs: 500,
@@ -77,7 +77,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
77
77
  runtimeMaxRetries: 2,
78
78
  runtimeProxyRequireAuth: false,
79
79
  runtimeTimeoutMs: 30000,
80
- shutdownDrainMs: 5000,
81
80
  trustProxy: false,
82
81
  ...overrides,
83
82
  } as GatewayConfig;
@@ -33,7 +33,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
33
33
  runtimeMaxRetries: 2,
34
34
  runtimeProxyRequireAuth: false,
35
35
  runtimeTimeoutMs: 30000,
36
- shutdownDrainMs: 5000,
37
36
  trustProxy: false,
38
37
  ...overrides,
39
38
  } as GatewayConfig;
@@ -22,7 +22,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
22
22
  runtimeMaxRetries: 2,
23
23
  runtimeProxyRequireAuth: false,
24
24
  runtimeTimeoutMs: 30000,
25
- shutdownDrainMs: 5000,
26
25
  trustProxy: false,
27
26
  ...overrides,
28
27
  } as GatewayConfig;
@@ -62,7 +62,6 @@ function makeConfig(): GatewayConfig {
62
62
  runtimeMaxRetries: 2,
63
63
  runtimeProxyRequireAuth: false,
64
64
  runtimeTimeoutMs: 30000,
65
- shutdownDrainMs: 5000,
66
65
  trustProxy: false,
67
66
  };
68
67
  }
@@ -121,7 +121,6 @@ function makeConfig(): GatewayConfig {
121
121
  runtimeMaxRetries: 2,
122
122
  runtimeProxyRequireAuth: false,
123
123
  runtimeTimeoutMs: 30000,
124
- shutdownDrainMs: 5000,
125
124
  trustProxy: false,
126
125
  };
127
126
  }
@@ -178,7 +178,6 @@ function makeGatewayConfig(): GatewayConfig {
178
178
  runtimeMaxRetries: 2,
179
179
  runtimeProxyRequireAuth: false,
180
180
  runtimeTimeoutMs: 30000,
181
- shutdownDrainMs: 5000,
182
181
  trustProxy: false,
183
182
  } as unknown as GatewayConfig;
184
183
  }
@@ -134,7 +134,6 @@ function makeConfig(): GatewayConfig {
134
134
  runtimeMaxRetries: 2,
135
135
  runtimeProxyRequireAuth: false,
136
136
  runtimeTimeoutMs: 30000,
137
- shutdownDrainMs: 5000,
138
137
  trustProxy: false,
139
138
  };
140
139
  }
@@ -121,7 +121,6 @@ function makeConfig(): GatewayConfig {
121
121
  runtimeMaxRetries: 2,
122
122
  runtimeProxyRequireAuth: false,
123
123
  runtimeTimeoutMs: 30000,
124
- shutdownDrainMs: 5000,
125
124
  trustProxy: false,
126
125
  };
127
126
  }
@@ -26,7 +26,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
26
26
  routingEntries: [],
27
27
  port: 7830,
28
28
  runtimeProxyRequireAuth: true,
29
- shutdownDrainMs: 5000,
30
29
  runtimeTimeoutMs: 30000,
31
30
  runtimeMaxRetries: 2,
32
31
  runtimeInitialBackoffMs: 500,
@@ -79,7 +79,6 @@ function makeConfig(): GatewayConfig {
79
79
  routingEntries: [],
80
80
  port: 7830,
81
81
  runtimeProxyRequireAuth: false,
82
- shutdownDrainMs: 5000,
83
82
  runtimeTimeoutMs: 30000,
84
83
  runtimeMaxRetries: 2,
85
84
  runtimeInitialBackoffMs: 500,
@@ -33,7 +33,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
33
33
  routingEntries: [],
34
34
  port: 7830,
35
35
  runtimeProxyRequireAuth: false,
36
- shutdownDrainMs: 5000,
37
36
  runtimeTimeoutMs: 30000,
38
37
  runtimeMaxRetries: 2,
39
38
  runtimeInitialBackoffMs: 500,
@@ -61,7 +61,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
61
61
  routingEntries: [],
62
62
  port: 7830,
63
63
  runtimeProxyRequireAuth: true,
64
- shutdownDrainMs: 5000,
65
64
  runtimeTimeoutMs: 30000,
66
65
  runtimeMaxRetries: 2,
67
66
  runtimeInitialBackoffMs: 500,
@@ -135,7 +135,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
135
135
  routingEntries: [],
136
136
  port: 7830,
137
137
  runtimeProxyRequireAuth: true,
138
- shutdownDrainMs: 5000,
139
138
  runtimeTimeoutMs: 30000,
140
139
  runtimeMaxRetries: 2,
141
140
  runtimeInitialBackoffMs: 500,
@@ -25,7 +25,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
25
25
  routingEntries: [],
26
26
  port: 7830,
27
27
  runtimeProxyRequireAuth: true,
28
- shutdownDrainMs: 5000,
29
28
  runtimeTimeoutMs: 30000,
30
29
  runtimeMaxRetries: 2,
31
30
  runtimeInitialBackoffMs: 500,
@@ -52,7 +52,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
52
52
  routingEntries: [],
53
53
  port: 7830,
54
54
  runtimeProxyRequireAuth: true,
55
- shutdownDrainMs: 5000,
56
55
  runtimeTimeoutMs: 30000,
57
56
  runtimeMaxRetries: 2,
58
57
  runtimeInitialBackoffMs: 500,
@@ -29,7 +29,6 @@ function config(overrides?: Partial<GatewayConfig>): GatewayConfig {
29
29
  runtimeMaxRetries: 0,
30
30
  runtimeProxyRequireAuth: false,
31
31
  runtimeTimeoutMs: 1000,
32
- shutdownDrainMs: 1000,
33
32
  trustProxy: false,
34
33
  };
35
34
  return { ...base, ...overrides };
package/src/config.ts CHANGED
@@ -36,7 +36,6 @@ export type GatewayConfig = {
36
36
  runtimeMaxRetries: number;
37
37
  runtimeProxyRequireAuth: boolean;
38
38
  runtimeTimeoutMs: number;
39
- shutdownDrainMs: number;
40
39
  /** When true, trust X-Forwarded-For for client IP resolution (set when behind a reverse proxy). */
41
40
  trustProxy: boolean;
42
41
  };
@@ -226,7 +225,6 @@ export function loadConfig(): GatewayConfig {
226
225
  runtimeMaxRetries: 2,
227
226
  runtimeProxyRequireAuth,
228
227
  runtimeTimeoutMs,
229
- shutdownDrainMs: 5000,
230
228
  trustProxy,
231
229
  };
232
230
  }
@@ -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. ngrok). Because every hop in that chain is loopback
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 an ngrok tunnel) sets this
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");
@@ -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 an ngrok tunnel). Every hop in that
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
@@ -221,7 +221,7 @@ export function createChannelVerificationSessionProxyHandler(
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 ngrok) must not be
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
226
  if (!isCloud && expectedSecrets.length === 0) {
227
227
  // A genuine local bootstrap caller connects directly over loopback and
@@ -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
 
@@ -0,0 +1,129 @@
1
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
5
+ import { z } from "zod";
6
+
7
+ import { ConfigFileCache } from "../../config-file-cache.js";
8
+ import { handleSaveIngressDomain } from "./ingress-domains.js";
9
+
10
+ // The handler's write path resolves the workspace config through
11
+ // getWorkspaceDir; the mock redirects it into a per-test temp dir.
12
+ let workspaceDir = "";
13
+
14
+ mock.module("../../credential-reader.js", () => ({
15
+ getWorkspaceDir: () => workspaceDir,
16
+ }));
17
+
18
+ const SavedIngressSchema = z.looseObject({
19
+ ingress: z
20
+ .looseObject({
21
+ publicBaseUrl: z.string().optional(),
22
+ publicBaseUrlManagedBy: z.string().optional(),
23
+ })
24
+ .optional(),
25
+ });
26
+
27
+ let testDir = "";
28
+
29
+ beforeEach(() => {
30
+ testDir = mkdtempSync(join(tmpdir(), "gateway-ingress-domains-test-"));
31
+ workspaceDir = testDir;
32
+ });
33
+
34
+ afterEach(() => {
35
+ rmSync(testDir, { recursive: true, force: true });
36
+ });
37
+
38
+ function request(body?: unknown): Request {
39
+ return new Request("http://127.0.0.1/v1/ingress/domains", {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: body === undefined ? undefined : JSON.stringify(body),
43
+ });
44
+ }
45
+
46
+ function readSavedIngress(): z.infer<typeof SavedIngressSchema>["ingress"] {
47
+ const raw = SavedIngressSchema.parse(
48
+ JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8")),
49
+ );
50
+ return raw.ingress;
51
+ }
52
+
53
+ describe("POST /v1/ingress/domains", () => {
54
+ test("records the tunnel URL as tunnel-managed in the gateway config", async () => {
55
+ const res = await handleSaveIngressDomain(
56
+ request({ publicBaseUrl: "https://foo.ngrok.app" }),
57
+ "127.0.0.1",
58
+ new ConfigFileCache(),
59
+ );
60
+
61
+ expect(res.status).toBe(200);
62
+ expect(readSavedIngress()).toMatchObject({
63
+ publicBaseUrl: "https://foo.ngrok.app",
64
+ publicBaseUrlManagedBy: "tunnel",
65
+ });
66
+ });
67
+
68
+ test("normalizes the URL the same way every other public base URL is", async () => {
69
+ await handleSaveIngressDomain(
70
+ request({ publicBaseUrl: "https://foo.ngrok.app/" }),
71
+ "127.0.0.1",
72
+ new ConfigFileCache(),
73
+ );
74
+
75
+ expect(readSavedIngress()?.publicBaseUrl).toBe("https://foo.ngrok.app");
76
+ });
77
+
78
+ test("is a no-op write when the URL is already recorded as tunnel-managed", async () => {
79
+ const configFile = new ConfigFileCache();
80
+ await handleSaveIngressDomain(
81
+ request({ publicBaseUrl: "https://foo.ngrok.app" }),
82
+ "127.0.0.1",
83
+ configFile,
84
+ );
85
+ const afterFirst = readFileSync(join(workspaceDir, "config.json"), "utf-8");
86
+
87
+ await handleSaveIngressDomain(
88
+ request({ publicBaseUrl: "https://foo.ngrok.app" }),
89
+ "127.0.0.1",
90
+ configFile,
91
+ );
92
+ const afterSecond = readFileSync(
93
+ join(workspaceDir, "config.json"),
94
+ "utf-8",
95
+ );
96
+
97
+ expect(afterSecond).toBe(afterFirst);
98
+ });
99
+
100
+ test("rejects non-loopback callers", async () => {
101
+ const res = await handleSaveIngressDomain(
102
+ request({ publicBaseUrl: "https://foo.ngrok.app" }),
103
+ "10.0.0.1",
104
+ new ConfigFileCache(),
105
+ );
106
+
107
+ expect(res.status).toBe(403);
108
+ });
109
+
110
+ test("rejects a body without a usable public URL", async () => {
111
+ const res = await handleSaveIngressDomain(
112
+ request({}),
113
+ "127.0.0.1",
114
+ new ConfigFileCache(),
115
+ );
116
+
117
+ expect(res.status).toBe(400);
118
+ });
119
+
120
+ test("rejects non-POST methods", async () => {
121
+ const res = await handleSaveIngressDomain(
122
+ new Request("http://127.0.0.1/v1/ingress/domains", { method: "GET" }),
123
+ "127.0.0.1",
124
+ new ConfigFileCache(),
125
+ );
126
+
127
+ expect(res.status).toBe(405);
128
+ });
129
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Loopback-guarded recording of a tunnel's public URL.
3
+ *
4
+ * POST /v1/ingress/domains — the CLI (its ngrok agent, today) tells the
5
+ * gateway the public URL its tunnel published; the gateway writes
6
+ * `ingress.publicBaseUrl` + `publicBaseUrlManagedBy: "tunnel"` in its own
7
+ * config via the same machinery its own tunnel uses, so both publishers
8
+ * record through one path and the gateway's existing loss handling clears
9
+ * it uniformly. Write-only: reads keep flowing through the config the
10
+ * gateway already serves. While we ship one bundle the gateway still hosts
11
+ * the ngrok-related logic; once the bundle splits, the gateway manages its
12
+ * own tunnel (mycadet.ai vs <assistant>.mycadet.bot) and this route is what
13
+ * the CLI-side publishers migrate onto.
14
+ */
15
+
16
+ import { z } from "zod";
17
+
18
+ import { normalizeHttpPublicBaseUrlWithoutTrailingSlash } from "@mycadet/service-contracts/ingress";
19
+
20
+ import type { ConfigFileCache } from "../../config-file-cache.js";
21
+ import { writeManagedPublicBaseUrl } from "../../tunnel/client.js";
22
+ import {
23
+ enforceLoopbackOnly,
24
+ errorResponse,
25
+ rejectBrowserOrigin,
26
+ } from "../loopback-guard.js";
27
+
28
+ const SaveIngressDomainRequestSchema = z.object({
29
+ publicBaseUrl: z.string(),
30
+ });
31
+
32
+ export async function handleSaveIngressDomain(
33
+ req: Request,
34
+ clientIp: string,
35
+ configFile: ConfigFileCache,
36
+ ): Promise<Response> {
37
+ if (req.method !== "POST") {
38
+ return new Response("method not allowed", {
39
+ status: 405,
40
+ headers: { Allow: "POST" },
41
+ });
42
+ }
43
+
44
+ const guardError = enforceLoopbackOnly(req, clientIp, "ingress-domains");
45
+ if (guardError) return guardError;
46
+ const originError = rejectBrowserOrigin(req, clientIp, "ingress-domains");
47
+ if (originError) return originError;
48
+
49
+ let body: unknown;
50
+ try {
51
+ body = await req.json();
52
+ } catch {
53
+ body = {};
54
+ }
55
+ const parsed = SaveIngressDomainRequestSchema.safeParse(body);
56
+ if (!parsed.success) {
57
+ return errorResponse("BAD_REQUEST", "publicBaseUrl is required", 400);
58
+ }
59
+ const publicUrl = normalizeHttpPublicBaseUrlWithoutTrailingSlash(
60
+ parsed.data.publicBaseUrl,
61
+ );
62
+ if (!publicUrl) {
63
+ return errorResponse(
64
+ "BAD_REQUEST",
65
+ "publicBaseUrl must be an absolute http(s) URL",
66
+ 400,
67
+ );
68
+ }
69
+
70
+ // No-op (no write) when the URL is already recorded as tunnel-managed.
71
+ await writeManagedPublicBaseUrl(publicUrl, configFile);
72
+ return Response.json({ ok: true });
73
+ }
@@ -112,7 +112,6 @@ const baseConfig: GatewayConfig = {
112
112
  runtimeMaxRetries: 2,
113
113
  runtimeProxyRequireAuth: true,
114
114
  runtimeTimeoutMs: 30000,
115
- shutdownDrainMs: 5000,
116
115
  trustProxy: false,
117
116
  };
118
117
 
@@ -79,7 +79,6 @@ function makeConfig(dir: string | undefined): GatewayConfig {
79
79
  runtimeMaxRetries: 2,
80
80
  runtimeProxyRequireAuth: true,
81
81
  runtimeTimeoutMs: 30000,
82
- shutdownDrainMs: 5000,
83
82
  trustProxy: false,
84
83
  } as GatewayConfig;
85
84
  }
@@ -72,7 +72,6 @@ const baseConfig: GatewayConfig = {
72
72
  runtimeMaxRetries: 2,
73
73
  runtimeProxyRequireAuth: true,
74
74
  runtimeTimeoutMs: 30000,
75
- shutdownDrainMs: 5000,
76
75
  trustProxy: false,
77
76
  };
78
77
 
@@ -112,7 +112,6 @@ const baseConfig: GatewayConfig = {
112
112
  runtimeMaxRetries: 2,
113
113
  runtimeProxyRequireAuth: true,
114
114
  runtimeTimeoutMs: 30000,
115
- shutdownDrainMs: 5000,
116
115
  trustProxy: false,
117
116
  };
118
117
 
@@ -91,7 +91,6 @@ const baseConfig: GatewayConfig = {
91
91
  runtimeMaxRetries: 2,
92
92
  runtimeProxyRequireAuth: true,
93
93
  runtimeTimeoutMs: 30000,
94
- shutdownDrainMs: 5000,
95
94
  trustProxy: false,
96
95
  };
97
96
 
package/src/index.ts CHANGED
@@ -104,6 +104,7 @@ import {
104
104
  handleListDevices,
105
105
  handleRevokeDevice,
106
106
  } from "./http/routes/devices.js";
107
+ import { handleSaveIngressDomain } from "./http/routes/ingress-domains.js";
107
108
  import { handlePair } from "./http/routes/pair.js";
108
109
  import { handleCredentialEntryPage } from "./http/routes/credential-entry-page.js";
109
110
  import {
@@ -888,6 +889,15 @@ async function main() {
888
889
  handler: () => psHandler.handlePs(),
889
890
  },
890
891
 
892
+ // ── Ingress domain recording (localhost CLI only, bearer-authed) ──
893
+ {
894
+ path: "/v1/ingress/domains",
895
+ method: "POST",
896
+ auth: "edge",
897
+ handler: (req, _params, getClientIp) =>
898
+ handleSaveIngressDomain(req, getClientIp(), configFileCache),
899
+ },
900
+
891
901
  // ── Tunnel status ──
892
902
  {
893
903
  path: "/v1/tunnel/status",
@@ -3171,7 +3181,7 @@ async function main() {
3171
3181
  // Force-reconnect Slack WebSocket (may be half-open after sleep)
3172
3182
  slackSocketClient?.forceReconnect();
3173
3183
 
3174
- // Invalidate caches so next read picks up any config changes (e.g. new ngrok URL)
3184
+ // Invalidate caches so next read picks up any config changes (e.g. a new tunnel URL)
3175
3185
  configFileCache.invalidate();
3176
3186
  credentialCache.invalidate();
3177
3187
 
@@ -3190,8 +3200,6 @@ async function main() {
3190
3200
  });
3191
3201
  sleepWakeDetector.start();
3192
3202
 
3193
- const drainMs = config.shutdownDrainMs;
3194
-
3195
3203
  process.on("SIGTERM", () => {
3196
3204
  log.info("SIGTERM received, starting graceful shutdown");
3197
3205
  draining = true;
@@ -3225,7 +3233,7 @@ async function main() {
3225
3233
  server.stop(true);
3226
3234
  process.exit(0);
3227
3235
  });
3228
- }, drainMs);
3236
+ }, 5_000);
3229
3237
  });
3230
3238
  }
3231
3239
 
@@ -40,7 +40,6 @@ function makeConfig(logDir: string | undefined): GatewayConfig {
40
40
  runtimeMaxRetries: 0,
41
41
  runtimeProxyRequireAuth: true,
42
42
  runtimeTimeoutMs: 1000,
43
- shutdownDrainMs: 1,
44
43
  trustProxy: false,
45
44
  } as GatewayConfig;
46
45
  }
@@ -43,7 +43,6 @@ function makeConfig(): GatewayConfig {
43
43
  runtimeMaxRetries: 2,
44
44
  runtimeProxyRequireAuth: true,
45
45
  runtimeTimeoutMs: 30000,
46
- shutdownDrainMs: 5000,
47
46
  trustProxy: false,
48
47
  } as GatewayConfig;
49
48
  }
@@ -39,7 +39,6 @@ const baseConfig: GatewayConfig = {
39
39
  runtimeMaxRetries: 2,
40
40
  runtimeProxyRequireAuth: true,
41
41
  runtimeTimeoutMs: 30000,
42
- shutdownDrainMs: 5000,
43
42
  trustProxy: false,
44
43
  };
45
44
 
@@ -395,7 +395,7 @@ async function reconcileTelegramWebhookNow(
395
395
  expectedUrl = await resolveExpectedTelegramWebhookUrl(caches);
396
396
  } catch (err) {
397
397
  // Managed callback route registration failed — this is a cloud-side
398
- // issue. Do not suggest ngrok or other tunnel options; they are not
398
+ // issue. Do not suggest third-party tunnel options; they are not
399
399
  // usable in containerized deployments.
400
400
  const detail = err instanceof Error ? err.message : String(err);
401
401
  log.error(
@@ -178,7 +178,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
178
178
  runtimeMaxRetries: 0,
179
179
  runtimeProxyRequireAuth: true,
180
180
  runtimeTimeoutMs: 1,
181
- shutdownDrainMs: 1,
182
181
  trustProxy: false,
183
182
  ...overrides,
184
183
  };
@@ -845,7 +845,7 @@ async function mutateGatewayConfigFile(
845
845
 
846
846
  const TUNNEL_MANAGED_BY = "tunnel";
847
847
 
848
- async function writeManagedPublicBaseUrl(
848
+ export async function writeManagedPublicBaseUrl(
849
849
  publicUrl: string,
850
850
  configFile: ConfigFileCache,
851
851
  ): Promise<void> {