@mycadet/credential-executor 0.12.10 → 0.13.0-dev.202610171617.9ea363e
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/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/package.json +5 -1
- package/src/http/request-errors.test.ts +29 -0
- package/src/http/request-errors.ts +34 -0
- package/src/main.ts +28 -21
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
trimmedNonEmptyString,
|
|
11
11
|
TUNNEL_PROVIDERS,
|
|
12
12
|
type TunnelRecord,
|
|
13
|
-
|
|
13
|
+
tunnelHostForCloudHost,
|
|
14
14
|
} from "../ingress.js";
|
|
15
15
|
import {
|
|
16
16
|
buildTwilioMediaStreamUrl,
|
|
@@ -19,27 +19,27 @@ import {
|
|
|
19
19
|
resolveTwilioPublicBaseUrl,
|
|
20
20
|
} from "../twilio-ingress.js";
|
|
21
21
|
|
|
22
|
-
describe("
|
|
22
|
+
describe("tunnelHostForCloudHost", () => {
|
|
23
23
|
test("maps the prod platform host to the prod tunnel", () => {
|
|
24
|
-
expect(
|
|
24
|
+
expect(tunnelHostForCloudHost("cloud.mycadet.ai")).toBe(
|
|
25
25
|
"tunnel.mycadet.ai",
|
|
26
26
|
);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
test("maps env-prefixed platform hosts to their env tunnel", () => {
|
|
30
|
-
expect(
|
|
31
|
-
"tunnel
|
|
30
|
+
expect(tunnelHostForCloudHost("staging-cloud.mycadet.ai")).toBe(
|
|
31
|
+
"staging-tunnel.mycadet.ai",
|
|
32
32
|
);
|
|
33
|
-
expect(
|
|
34
|
-
"tunnel
|
|
33
|
+
expect(tunnelHostForCloudHost("dev-cloud.mycadet.ai")).toBe(
|
|
34
|
+
"dev-tunnel.mycadet.ai",
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
test("returns null for hosts outside the deployment convention", () => {
|
|
39
|
-
expect(
|
|
40
|
-
expect(
|
|
39
|
+
expect(tunnelHostForCloudHost("localhost")).toBeNull();
|
|
40
|
+
expect(tunnelHostForCloudHost("platform.example.com")).toBeNull();
|
|
41
41
|
expect(
|
|
42
|
-
|
|
42
|
+
tunnelHostForCloudHost("staging-cloud.mycadet.ai.evil.com"),
|
|
43
43
|
).toBeNull();
|
|
44
44
|
});
|
|
45
45
|
});
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Map a Cadet Cloud host onto its environment's tunnel host, following the
|
|
3
3
|
* deployment naming convention (`cloud.mycadet.ai` → `tunnel.mycadet.ai`,
|
|
4
|
-
* `{env}-cloud.mycadet.ai` → `
|
|
4
|
+
* `{env}-cloud.mycadet.ai` → `{env}-tunnel.mycadet.ai`). Returns null for
|
|
5
5
|
* hosts outside that convention (localhost, custom domains). Shared by the
|
|
6
6
|
* gateway speech relay and the web live-voice client so the two ends of the
|
|
7
7
|
* cloud-speech transport can't drift.
|
|
8
8
|
*/
|
|
9
|
-
export function
|
|
9
|
+
export function tunnelHostForCloudHost(host: string): string | null {
|
|
10
10
|
if (host === "cloud.mycadet.ai") {
|
|
11
11
|
return "tunnel.mycadet.ai";
|
|
12
12
|
}
|
|
13
13
|
const match = /^([a-z0-9-]+)-cloud\.mycadet\.ai$/.exec(host);
|
|
14
|
-
return match ?
|
|
14
|
+
return match ? `${match[1]}-tunnel.mycadet.ai` : null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function normalizePublicBaseUrl(value: unknown): string | undefined {
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mycadet/credential-executor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0-dev.202610171617.9ea363e",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/vargasjr-dev/mycadet-temp.git"
|
|
7
|
+
},
|
|
4
8
|
"license": "MIT",
|
|
5
9
|
"type": "module",
|
|
6
10
|
"exports": {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { withCesHttpErrorLogging } from "./request-errors.js";
|
|
3
|
+
|
|
4
|
+
test("CES HTTP failures log the exception and return a safe correlation id", async () => {
|
|
5
|
+
const captured: Array<Record<string, unknown>> = [];
|
|
6
|
+
const failure = new Error("backend unavailable");
|
|
7
|
+
const response = await withCesHttpErrorLogging(
|
|
8
|
+
new Request("http://127.0.0.1:8090/v1/credentials", {
|
|
9
|
+
headers: { Authorization: "Bearer fixture-secret" },
|
|
10
|
+
}),
|
|
11
|
+
async () => {
|
|
12
|
+
throw failure;
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
error: (context) => {
|
|
16
|
+
captured.push(context);
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
const body = await response.json();
|
|
21
|
+
expect(response.status).toBe(500);
|
|
22
|
+
expect(body.error).toBe("Internal server error");
|
|
23
|
+
expect(body.request_id).toBe(response.headers.get("x-ces-error-id"));
|
|
24
|
+
expect(captured[0]?.err).toBe(failure);
|
|
25
|
+
expect(captured[0]?.method).toBe("GET");
|
|
26
|
+
expect(captured[0]?.path).toBe("/v1/credentials");
|
|
27
|
+
expect(JSON.stringify(body)).not.toContain("fixture-secret");
|
|
28
|
+
expect(JSON.stringify(captured)).not.toContain("fixture-secret");
|
|
29
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
type RequestLogger = {
|
|
4
|
+
error(context: Record<string, unknown>, message: string): void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** Keep exception diagnostics in CES logs and expose only a correlation id. */
|
|
8
|
+
export async function withCesHttpErrorLogging(
|
|
9
|
+
request: Request,
|
|
10
|
+
handle: () => Promise<Response>,
|
|
11
|
+
logger: RequestLogger,
|
|
12
|
+
): Promise<Response> {
|
|
13
|
+
try {
|
|
14
|
+
return await handle();
|
|
15
|
+
} catch (err) {
|
|
16
|
+
const requestId = randomUUID();
|
|
17
|
+
logger.error(
|
|
18
|
+
{
|
|
19
|
+
err,
|
|
20
|
+
requestId,
|
|
21
|
+
method: request.method,
|
|
22
|
+
path: new URL(request.url).pathname,
|
|
23
|
+
},
|
|
24
|
+
"CES HTTP request failed",
|
|
25
|
+
);
|
|
26
|
+
return Response.json(
|
|
27
|
+
{ error: "Internal server error", request_id: requestId },
|
|
28
|
+
{
|
|
29
|
+
status: 500,
|
|
30
|
+
headers: { "x-ces-error-id": requestId },
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from "./http/credential-routes.js";
|
|
52
52
|
import { handleLogExportRoute } from "./http/log-export-routes.js";
|
|
53
53
|
import { isLoopbackAddress } from "./http/loopback-address.js";
|
|
54
|
+
import { withCesHttpErrorLogging } from "./http/request-errors.js";
|
|
54
55
|
import { handleMetadataRoute } from "./http/metadata-routes.js";
|
|
55
56
|
import { CES_MIGRATIONS } from "./migrations/registry.js";
|
|
56
57
|
import { runCesMigrations } from "./migrations/runner.js";
|
|
@@ -283,29 +284,35 @@ function startHealthServer(
|
|
|
283
284
|
return new Response("Not Found", { status: 404 });
|
|
284
285
|
}
|
|
285
286
|
|
|
286
|
-
|
|
287
|
-
if (credentialDeps) {
|
|
288
|
-
const metadataResponse = await handleMetadataRoute(req);
|
|
289
|
-
if (metadataResponse) {
|
|
290
|
-
return metadataResponse;
|
|
291
|
-
}
|
|
292
|
-
const credentialResponse = await handleCredentialRoute(
|
|
293
|
-
req,
|
|
294
|
-
credentialDeps,
|
|
295
|
-
);
|
|
296
|
-
if (credentialResponse) {
|
|
297
|
-
return credentialResponse;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Log export route
|
|
302
|
-
const logExportResponse = await handleLogExportRoute(
|
|
287
|
+
return withCesHttpErrorLogging(
|
|
303
288
|
req,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
289
|
+
async () => {
|
|
290
|
+
// Credential CRUD routes (only if service token is configured)
|
|
291
|
+
if (credentialDeps) {
|
|
292
|
+
const metadataResponse = await handleMetadataRoute(req);
|
|
293
|
+
if (metadataResponse) {
|
|
294
|
+
return metadataResponse;
|
|
295
|
+
}
|
|
296
|
+
const credentialResponse = await handleCredentialRoute(
|
|
297
|
+
req,
|
|
298
|
+
credentialDeps,
|
|
299
|
+
);
|
|
300
|
+
if (credentialResponse) {
|
|
301
|
+
return credentialResponse;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
307
304
|
|
|
308
|
-
|
|
305
|
+
// Log export route
|
|
306
|
+
const logExportResponse = await handleLogExportRoute(
|
|
307
|
+
req,
|
|
308
|
+
getCesLogDir("managed"),
|
|
309
|
+
);
|
|
310
|
+
if (logExportResponse) return logExportResponse;
|
|
311
|
+
|
|
312
|
+
return new Response("Not Found", { status: 404 });
|
|
313
|
+
},
|
|
314
|
+
log,
|
|
315
|
+
);
|
|
309
316
|
},
|
|
310
317
|
});
|
|
311
318
|
|