@vellumai/credential-executor 0.11.10-dev.202609110043.1ff55a1 → 0.11.10-staging.2
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 +1 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +0 -31
- package/node_modules/@vellumai/service-contracts/src/channels.ts +3 -28
- package/node_modules/@vellumai/service-contracts/src/index.ts +0 -1
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +5 -85
- package/package.json +1 -1
- package/src/__tests__/transport.test.ts +0 -4
- package/src/main.ts +2 -11
- package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +0 -51
- package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +0 -66
- package/src/http/loopback-address.test.ts +0 -35
- package/src/http/loopback-address.ts +0 -25
|
@@ -26,8 +26,7 @@
|
|
|
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"
|
|
30
|
-
"./plan-credit": "./src/plan-credit.ts"
|
|
29
|
+
"./platform-credential": "./src/platform-credential.ts"
|
|
31
30
|
},
|
|
32
31
|
"scripts": {
|
|
33
32
|
"typecheck": "bunx tsc --noEmit",
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
botProviderForChannel,
|
|
5
4
|
CHANNEL_BOT_PROVIDER,
|
|
6
5
|
CHANNEL_IDS,
|
|
7
|
-
channelForBotProvider,
|
|
8
6
|
isChannelBotProvider,
|
|
9
7
|
isChannelUserIntegration,
|
|
10
8
|
isChannelId,
|
|
@@ -43,35 +41,6 @@ describe("isChannelId", () => {
|
|
|
43
41
|
});
|
|
44
42
|
});
|
|
45
43
|
|
|
46
|
-
describe("botProviderForChannel and channelForBotProvider", () => {
|
|
47
|
-
test("are inverses over every entry of the map", () => {
|
|
48
|
-
for (const [channelId, botProviderKey] of Object.entries(
|
|
49
|
-
CHANNEL_BOT_PROVIDER,
|
|
50
|
-
)) {
|
|
51
|
-
expect(botProviderForChannel(channelId)).toBe(botProviderKey);
|
|
52
|
-
expect(channelForBotProvider(botProviderKey)).toBe(
|
|
53
|
-
channelId as keyof typeof CHANNEL_BOT_PROVIDER,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("answer nothing for a channel without a bot, a person's grant, or an unknown key", () => {
|
|
59
|
-
expect(botProviderForChannel("phone")).toBeUndefined();
|
|
60
|
-
expect(botProviderForChannel("vellum")).toBeUndefined();
|
|
61
|
-
expect(botProviderForChannel("not-a-channel")).toBeUndefined();
|
|
62
|
-
// `slack` is the person's integration standing beside the bot's key.
|
|
63
|
-
expect(channelForBotProvider("slack")).toBeUndefined();
|
|
64
|
-
expect(channelForBotProvider("google")).toBeUndefined();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("never resolve an inherited object property as a channel or a key", () => {
|
|
68
|
-
for (const name of ["constructor", "toString", "__proto__"]) {
|
|
69
|
-
expect(botProviderForChannel(name)).toBeUndefined();
|
|
70
|
-
expect(channelForBotProvider(name)).toBeUndefined();
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
44
|
describe("isChannelUserIntegration", () => {
|
|
76
45
|
test("names the grant standing beside a bot of the same brand", () => {
|
|
77
46
|
expect(isChannelUserIntegration("slack")).toBe(true);
|
|
@@ -93,34 +93,9 @@ export const CHANNEL_BOT_PROVIDER = {
|
|
|
93
93
|
* connected" question, for any provider key.
|
|
94
94
|
*/
|
|
95
95
|
export function isChannelBotProvider(providerKey: string): boolean {
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* The provider key holding a channel's bot credential, or `undefined` when
|
|
101
|
-
* the channel has none, or is not a channel at all. Walks the map's own
|
|
102
|
-
* entries, so an inherited object property name is never a channel.
|
|
103
|
-
*/
|
|
104
|
-
export function botProviderForChannel(channelId: string): string | undefined {
|
|
105
|
-
return Object.entries(CHANNEL_BOT_PROVIDER).find(
|
|
106
|
-
([id]) => id === channelId,
|
|
107
|
-
)?.[1];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* The channel whose bot credential a provider key holds, or `undefined` when
|
|
112
|
-
* the key is not a bot's. The inverse of `botProviderForChannel`, for a
|
|
113
|
-
* surface that has only the provider key in hand and needs to speak in the
|
|
114
|
-
* channel's vocabulary.
|
|
115
|
-
*/
|
|
116
|
-
export function channelForBotProvider(
|
|
117
|
-
providerKey: string,
|
|
118
|
-
): keyof typeof CHANNEL_BOT_PROVIDER | undefined {
|
|
119
|
-
return (
|
|
120
|
-
Object.entries(CHANNEL_BOT_PROVIDER) as Array<
|
|
121
|
-
[keyof typeof CHANNEL_BOT_PROVIDER, string]
|
|
122
|
-
>
|
|
123
|
-
).find(([, key]) => key === providerKey)?.[0];
|
|
96
|
+
return (Object.values(CHANNEL_BOT_PROVIDER) as readonly string[]).includes(
|
|
97
|
+
providerKey,
|
|
98
|
+
);
|
|
124
99
|
}
|
|
125
100
|
|
|
126
101
|
/**
|
|
@@ -28,7 +28,6 @@ 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";
|
|
32
31
|
export * from "./platform-credential.js";
|
|
33
32
|
export * from "./remote-web-pairing.js";
|
|
34
33
|
export * from "./twilio-ingress.js";
|
|
@@ -7,13 +7,11 @@
|
|
|
7
7
|
* is spelled. Modelled on Zulip's `reaction_type`, the one surveyed system
|
|
8
8
|
* that separates the namespace from the name.
|
|
9
9
|
*
|
|
10
|
-
* `shortcode` is a name in a channel's own namespace
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* a stand-in for an unknown one, and nothing past an adapter needs a
|
|
16
|
-
* channel's naming to read it.
|
|
10
|
+
* `shortcode` is a name in a channel's own namespace whose kind the channel
|
|
11
|
+
* does not disclose: Slack sends `+1` for the standard emoji and `blob_wave`
|
|
12
|
+
* for a workspace upload with nothing to tell them apart, and only the
|
|
13
|
+
* workspace token can resolve the second. It is a distinct kind from
|
|
14
|
+
* `unicode`, not a stand-in for an unknown one.
|
|
17
15
|
*/
|
|
18
16
|
import { z } from "zod";
|
|
19
17
|
|
|
@@ -41,12 +39,6 @@ export const ReactionEmojiFieldsSchema = z.object({
|
|
|
41
39
|
});
|
|
42
40
|
export type ReactionEmojiFields = z.infer<typeof ReactionEmojiFieldsSchema>;
|
|
43
41
|
|
|
44
|
-
/** The typed fields with the kind and name known. */
|
|
45
|
-
export type ReactionEmojiIdentity = ReactionEmojiFields & {
|
|
46
|
-
emojiKind: ReactionEmojiKind;
|
|
47
|
-
emojiName: string;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
42
|
/**
|
|
51
43
|
* The typed emoji fields a source actually carries, with undefined ones
|
|
52
44
|
* omitted: an absent key and a present-but-undefined one serialize alike, but the
|
|
@@ -66,75 +58,3 @@ export function pickReactionEmojiFields(
|
|
|
66
58
|
: {}),
|
|
67
59
|
};
|
|
68
60
|
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Parse Discord's custom-emoji mention form. The form travels the wire as a
|
|
72
|
-
* reaction's spelling, so more than one package reads it: the daemon rebuilds
|
|
73
|
-
* a REST path from it when the assistant reacts, and a row carrying only the
|
|
74
|
-
* spelling recovers its kind from it. One parser, so the two cannot disagree
|
|
75
|
-
* about what counts as one.
|
|
76
|
-
*
|
|
77
|
-
* `animated` reports whether the spelling carries the `a` marker. Discord's
|
|
78
|
-
* normalizer never writes that marker, so it is read only from spellings that
|
|
79
|
-
* arrive from elsewhere: a row written by another producer, or a value the
|
|
80
|
-
* model hands back as it received it.
|
|
81
|
-
*/
|
|
82
|
-
export function parseDiscordEmojiMention(
|
|
83
|
-
emoji: string,
|
|
84
|
-
): { name: string; id: string; animated: boolean } | null {
|
|
85
|
-
const match = /^<(a?):([^:>]+):(\d+)>$/.exec(emoji);
|
|
86
|
-
return match
|
|
87
|
-
? { name: match[2]!, id: match[3]!, animated: match[1] === "a" }
|
|
88
|
-
: null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Recover an emoji's kind from its spelling alone. This is the one inference
|
|
93
|
-
* the design permits, reserved for a value that carries the string and no
|
|
94
|
-
* typed fields: a persisted row or a replayed retry payload. A value that
|
|
95
|
-
* declares its kind never reaches this; `reactionEmojiIdentity` makes that
|
|
96
|
-
* choice for every reader.
|
|
97
|
-
*
|
|
98
|
-
* A mention form is unambiguous. Past that the two remaining kinds are told
|
|
99
|
-
* apart by whether the string is a name at all: a channel's shortcode is
|
|
100
|
-
* ASCII word characters, optionally carrying Slack's `::skin-tone-N`
|
|
101
|
-
* suffix, and anything else is the character itself.
|
|
102
|
-
*/
|
|
103
|
-
export function classifyReactionEmojiSpelling(
|
|
104
|
-
emoji: string,
|
|
105
|
-
): ReactionEmojiIdentity {
|
|
106
|
-
const custom = parseDiscordEmojiMention(emoji);
|
|
107
|
-
if (custom) {
|
|
108
|
-
// The plain `<:name:id>` form says nothing about animation: the
|
|
109
|
-
// normalizer spells every custom emoji that way and reports animation
|
|
110
|
-
// in the typed field instead, so a spelling without the `a` marker is
|
|
111
|
-
// "unrecorded", not "not animated". Only the `<a:` form asserts it.
|
|
112
|
-
return {
|
|
113
|
-
emojiKind: "custom",
|
|
114
|
-
emojiName: custom.name,
|
|
115
|
-
emojiId: custom.id,
|
|
116
|
-
...(custom.animated ? { emojiAnimated: true } : {}),
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
return /^[\w+-]+(::skin-tone-[2-6])?$/.test(emoji)
|
|
120
|
-
? { emojiKind: "shortcode", emojiName: emoji }
|
|
121
|
-
: { emojiKind: "unicode", emojiName: emoji };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* The typed identity of a reaction's emoji: the fields it declares when it
|
|
126
|
-
* declares a kind, otherwise the kind recovered from its spelling. Every
|
|
127
|
-
* reader that must know what an emoji is goes through this, so the wire
|
|
128
|
-
* contract and the web agree on when the spelling is consulted.
|
|
129
|
-
*/
|
|
130
|
-
export function reactionEmojiIdentity(
|
|
131
|
-
reaction: { emoji: string } & ReactionEmojiFields,
|
|
132
|
-
): ReactionEmojiIdentity {
|
|
133
|
-
return reaction.emojiKind !== undefined && reaction.emojiName !== undefined
|
|
134
|
-
? {
|
|
135
|
-
...pickReactionEmojiFields(reaction),
|
|
136
|
-
emojiKind: reaction.emojiKind,
|
|
137
|
-
emojiName: reaction.emojiName,
|
|
138
|
-
}
|
|
139
|
-
: classifyReactionEmojiSpelling(reaction.emoji);
|
|
140
|
-
}
|
package/package.json
CHANGED
|
@@ -163,10 +163,6 @@ 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\(/);
|
|
170
166
|
});
|
|
171
167
|
|
|
172
168
|
test("getHealthPort defaults to 8090", () => {
|
package/src/main.ts
CHANGED
|
@@ -51,7 +51,6 @@ 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";
|
|
55
54
|
import { handleMetadataRoute } from "./http/metadata-routes.js";
|
|
56
55
|
import { CES_MIGRATIONS } from "./migrations/registry.js";
|
|
57
56
|
import { runCesMigrations } from "./migrations/runner.js";
|
|
@@ -247,12 +246,9 @@ function startHealthServer(
|
|
|
247
246
|
signal: AbortSignal,
|
|
248
247
|
credentialDeps: CredentialRouteDeps | null,
|
|
249
248
|
): 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>.
|
|
253
249
|
const server = Bun.serve({
|
|
254
250
|
port,
|
|
255
|
-
async fetch(req
|
|
251
|
+
async fetch(req) {
|
|
256
252
|
const url = new URL(req.url);
|
|
257
253
|
if (url.pathname === "/healthz") {
|
|
258
254
|
return new Response(JSON.stringify({ status: "ok" }), {
|
|
@@ -260,7 +256,7 @@ function startHealthServer(
|
|
|
260
256
|
});
|
|
261
257
|
}
|
|
262
258
|
if (url.pathname === "/readyz") {
|
|
263
|
-
// Always return 200
|
|
259
|
+
// Always return 200 — pod readiness must not depend on whether the
|
|
264
260
|
// assistant has connected. When the CES feature flag is off the
|
|
265
261
|
// assistant never connects, and a 503 here would block pod
|
|
266
262
|
// scheduling during dark-launch. The sidecar can't do useful work
|
|
@@ -279,11 +275,6 @@ function startHealthServer(
|
|
|
279
275
|
);
|
|
280
276
|
}
|
|
281
277
|
|
|
282
|
-
const peer = httpServer.requestIP(req)?.address;
|
|
283
|
-
if (!peer || !isLoopbackAddress(peer)) {
|
|
284
|
-
return new Response("Not Found", { status: 404 });
|
|
285
|
-
}
|
|
286
|
-
|
|
287
278
|
// Credential CRUD routes (only if service token is configured)
|
|
288
279
|
if (credentialDeps) {
|
|
289
280
|
const metadataResponse = await handleMetadataRoute(req);
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
}
|