@vellumai/credential-executor 0.11.10 → 0.11.11-staging.1
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/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
- package/package.json +1 -1
- package/src/__tests__/transport.test.ts +4 -0
- package/src/http/loopback-address.test.ts +35 -0
- package/src/http/loopback-address.ts +25 -0
- package/src/main.ts +11 -2
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"./error": "./src/error.ts",
|
|
27
27
|
"./reactions": "./src/reactions.ts",
|
|
28
28
|
"./guardian-requests": "./src/guardian-requests.ts",
|
|
29
|
-
"./platform-credential": "./src/platform-credential.ts"
|
|
29
|
+
"./platform-credential": "./src/platform-credential.ts",
|
|
30
|
+
"./plan-credit": "./src/plan-credit.ts"
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
32
33
|
"typecheck": "bunx tsc --noEmit",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extraCreditUsd,
|
|
5
|
+
planCreditUsedFraction,
|
|
6
|
+
usageGrantRatio,
|
|
7
|
+
} from "../plan-credit.js";
|
|
8
|
+
|
|
9
|
+
describe("usageGrantRatio", () => {
|
|
10
|
+
test("reads the used share and clamps to 0..1", () => {
|
|
11
|
+
expect(usageGrantRatio(20, 9.1)).toBeCloseTo(0.545);
|
|
12
|
+
expect(usageGrantRatio(20, 0)).toBe(1);
|
|
13
|
+
expect(usageGrantRatio(20, 25)).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("has no reading without both figures or with nothing granted", () => {
|
|
17
|
+
expect(usageGrantRatio(null, 5)).toBeNull();
|
|
18
|
+
expect(usageGrantRatio(20, null)).toBeNull();
|
|
19
|
+
expect(usageGrantRatio(0, 0)).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("planCreditUsedFraction", () => {
|
|
24
|
+
test("prefers the ratio whatever the plan", () => {
|
|
25
|
+
expect(planCreditUsedFraction(20, 9.1, "base")).toBeCloseTo(0.545);
|
|
26
|
+
expect(planCreditUsedFraction(20, 9.1, null)).toBeCloseTo(0.545);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("treats a Pro plan with nothing granted as fully spent", () => {
|
|
30
|
+
expect(planCreditUsedFraction(0, 0, "pro")).toBe(1);
|
|
31
|
+
expect(planCreditUsedFraction(0, null, "pro")).toBe(1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("gives a base or unknown plan with nothing granted no reading", () => {
|
|
35
|
+
expect(planCreditUsedFraction(0, 0, "base")).toBeNull();
|
|
36
|
+
expect(planCreditUsedFraction(0, 0, null)).toBeNull();
|
|
37
|
+
expect(planCreditUsedFraction(null, null, "pro")).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("extraCreditUsd", () => {
|
|
42
|
+
test("nets unused grant credit out of the balance to the cent", () => {
|
|
43
|
+
expect(extraCreditUsd(42.17, 9.1)).toBe(33.07);
|
|
44
|
+
expect(extraCreditUsd(34.65, 9.1)).toBe(25.55);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("never goes below zero", () => {
|
|
48
|
+
expect(extraCreditUsd(5, 9.1)).toBe(0);
|
|
49
|
+
expect(extraCreditUsd(-2.5, 0)).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -28,6 +28,7 @@ export * from "./rpc.js";
|
|
|
28
28
|
export * from "./trust-rules.js";
|
|
29
29
|
export * from "./ingress.js";
|
|
30
30
|
export * from "./no-response.js";
|
|
31
|
+
export * from "./plan-credit.js";
|
|
31
32
|
export * from "./platform-credential.js";
|
|
32
33
|
export * from "./remote-web-pairing.js";
|
|
33
34
|
export * from "./twilio-ingress.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-credit derivations shared by the assistant runtime's `platform credits`
|
|
3
|
+
* route and the web client's usage meter, so both read the billing summary's
|
|
4
|
+
* grant figures the same way and can never quote different readings.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type BillingPlanId = "base" | "pro";
|
|
8
|
+
|
|
9
|
+
function clamp01(value: number): number {
|
|
10
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
return value > 1 ? 1 : value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* How much of the usage credit an account was granted it has already used:
|
|
18
|
+
* the granted total less what is still unused, over that total. The initial
|
|
19
|
+
* $5 grant burns 0 to 100% as it is spent, and a further grant grows the total
|
|
20
|
+
* so the bar drops back. Null when nothing was ever granted, or when the
|
|
21
|
+
* platform reports neither figure, which has no honest reading rather than a
|
|
22
|
+
* full or empty bar.
|
|
23
|
+
*/
|
|
24
|
+
export function usageGrantRatio(
|
|
25
|
+
totalUsd: number | null,
|
|
26
|
+
availableUsd: number | null,
|
|
27
|
+
): number | null {
|
|
28
|
+
if (totalUsd == null || availableUsd == null || totalUsd <= 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return clamp01((totalUsd - availableUsd) / totalUsd);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The plan-credit reading with the plan's fallback applied. The grant figures
|
|
36
|
+
* count only unexpired grants, so a Pro subscription whose grants total
|
|
37
|
+
* nothing has used or expired everything it was granted (a full bar, with
|
|
38
|
+
* nothing scheduled to refill it), while a base plan in that position was
|
|
39
|
+
* never granted anything (no reading). An unknown plan gets the ratio when
|
|
40
|
+
* the figures allow one and no fallback otherwise.
|
|
41
|
+
*/
|
|
42
|
+
export function planCreditUsedFraction(
|
|
43
|
+
totalUsd: number | null,
|
|
44
|
+
availableUsd: number | null,
|
|
45
|
+
planId: BillingPlanId | null,
|
|
46
|
+
): number | null {
|
|
47
|
+
const ratio = usageGrantRatio(totalUsd, availableUsd);
|
|
48
|
+
if (ratio != null) {
|
|
49
|
+
return ratio;
|
|
50
|
+
}
|
|
51
|
+
if (planId === "pro" && totalUsd != null && totalUsd <= 0) {
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Credit bought or earned on top of the plan-included grants: the balance less
|
|
59
|
+
* whatever is still unused on those grants, never below zero, to the cent.
|
|
60
|
+
*/
|
|
61
|
+
export function extraCreditUsd(
|
|
62
|
+
balanceUsd: number,
|
|
63
|
+
availableUsd: number,
|
|
64
|
+
): number {
|
|
65
|
+
return Math.round(Math.max(0, balanceUsd - availableUsd) * 100) / 100;
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -163,6 +163,10 @@ describe("health probes", () => {
|
|
|
163
163
|
expect(src).toMatch(/\/readyz/);
|
|
164
164
|
// Health server uses Bun.serve on a dedicated port, not the socket
|
|
165
165
|
expect(src).toMatch(/startHealthServer\(\s*healthPort/);
|
|
166
|
+
// Kubelet httpGet targets the pod IP, so the probe listener must not
|
|
167
|
+
// bind loopback-only. Credential routes are gated with isLoopbackAddress.
|
|
168
|
+
expect(src).not.toMatch(/hostname:\s*"127\.0\.0\.1"/);
|
|
169
|
+
expect(src).toMatch(/isLoopbackAddress\(/);
|
|
166
170
|
});
|
|
167
171
|
|
|
168
172
|
test("getHealthPort defaults to 8090", () => {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isCesHttpProbePath,
|
|
5
|
+
isLoopbackAddress,
|
|
6
|
+
} from "./loopback-address.js";
|
|
7
|
+
|
|
8
|
+
describe("isLoopbackAddress", () => {
|
|
9
|
+
test("accepts IPv4 loopback", () => {
|
|
10
|
+
expect(isLoopbackAddress("127.0.0.1")).toBe(true);
|
|
11
|
+
expect(isLoopbackAddress("127.0.0.2")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("accepts IPv6 loopback and IPv4-mapped loopback", () => {
|
|
15
|
+
expect(isLoopbackAddress("::1")).toBe(true);
|
|
16
|
+
expect(isLoopbackAddress("::ffff:127.0.0.1")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("rejects non-loopback addresses", () => {
|
|
20
|
+
expect(isLoopbackAddress("10.0.0.1")).toBe(false);
|
|
21
|
+
expect(isLoopbackAddress("192.168.1.10")).toBe(false);
|
|
22
|
+
expect(isLoopbackAddress("8.8.8.8")).toBe(false);
|
|
23
|
+
expect(isLoopbackAddress("::ffff:10.0.0.1")).toBe(false);
|
|
24
|
+
expect(isLoopbackAddress("2001:db8::1")).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("isCesHttpProbePath", () => {
|
|
29
|
+
test("matches kubelet probe paths only", () => {
|
|
30
|
+
expect(isCesHttpProbePath("/healthz")).toBe(true);
|
|
31
|
+
expect(isCesHttpProbePath("/readyz")).toBe(true);
|
|
32
|
+
expect(isCesHttpProbePath("/v1/credentials")).toBe(false);
|
|
33
|
+
expect(isCesHttpProbePath("/v1/logs/export")).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict loopback check for CES HTTP peers: 127.0.0.0/8 and ::1.
|
|
3
|
+
* IPv4-mapped IPv6 (`::ffff:127.0.0.1`) is treated as IPv4.
|
|
4
|
+
*/
|
|
5
|
+
export function isLoopbackAddress(addr: string): boolean {
|
|
6
|
+
const v4Mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
7
|
+
const normalized = v4Mapped ? v4Mapped[1] : addr;
|
|
8
|
+
|
|
9
|
+
if (normalized.includes(".")) {
|
|
10
|
+
const parts = normalized.split(".").map(Number);
|
|
11
|
+
if (
|
|
12
|
+
parts.length !== 4 ||
|
|
13
|
+
parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)
|
|
14
|
+
) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return parts[0] === 127;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return normalized.toLowerCase() === "::1";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isCesHttpProbePath(pathname: string): boolean {
|
|
24
|
+
return pathname === "/healthz" || pathname === "/readyz";
|
|
25
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
type CredentialRouteDeps,
|
|
52
52
|
} from "./http/credential-routes.js";
|
|
53
53
|
import { handleLogExportRoute } from "./http/log-export-routes.js";
|
|
54
|
+
import { isLoopbackAddress } from "./http/loopback-address.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";
|
|
@@ -246,9 +247,12 @@ function startHealthServer(
|
|
|
246
247
|
signal: AbortSignal,
|
|
247
248
|
credentialDeps: CredentialRouteDeps | null,
|
|
248
249
|
): ReturnType<typeof Bun.serve> {
|
|
250
|
+
// Listen on every interface so kubelet httpGet probes can hit the pod IP.
|
|
251
|
+
// Credential CRUD, metadata, and log export stay loopback-only: assistant
|
|
252
|
+
// and gateway in this pod call http://localhost:<CES_HEALTH_PORT>.
|
|
249
253
|
const server = Bun.serve({
|
|
250
254
|
port,
|
|
251
|
-
async fetch(req) {
|
|
255
|
+
async fetch(req, httpServer) {
|
|
252
256
|
const url = new URL(req.url);
|
|
253
257
|
if (url.pathname === "/healthz") {
|
|
254
258
|
return new Response(JSON.stringify({ status: "ok" }), {
|
|
@@ -256,7 +260,7 @@ function startHealthServer(
|
|
|
256
260
|
});
|
|
257
261
|
}
|
|
258
262
|
if (url.pathname === "/readyz") {
|
|
259
|
-
// Always return 200
|
|
263
|
+
// Always return 200: pod readiness must not depend on whether the
|
|
260
264
|
// assistant has connected. When the CES feature flag is off the
|
|
261
265
|
// assistant never connects, and a 503 here would block pod
|
|
262
266
|
// scheduling during dark-launch. The sidecar can't do useful work
|
|
@@ -275,6 +279,11 @@ function startHealthServer(
|
|
|
275
279
|
);
|
|
276
280
|
}
|
|
277
281
|
|
|
282
|
+
const peer = httpServer.requestIP(req)?.address;
|
|
283
|
+
if (!peer || !isLoopbackAddress(peer)) {
|
|
284
|
+
return new Response("Not Found", { status: 404 });
|
|
285
|
+
}
|
|
286
|
+
|
|
278
287
|
// Credential CRUD routes (only if service token is configured)
|
|
279
288
|
if (credentialDeps) {
|
|
280
289
|
const metadataResponse = await handleMetadataRoute(req);
|