@velum-labs/routekit-daemon 1.3.2 → 1.5.0
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/dist/account-enroll-service.d.ts +1 -3
- package/dist/account-enroll-service.js +262 -262
- package/dist/account-mutation-service.d.ts +1 -3
- package/dist/account-mutation-service.js +302 -283
- package/dist/account-query-service.d.ts +1 -3
- package/dist/account-query-service.js +88 -86
- package/dist/call-attribution-store.js +544 -1
- package/dist/control-dispatch.js +9 -15
- package/dist/daemon-bootstrap-preflight.js +1 -1
- package/dist/daemon-control-handlers.d.ts +1 -1
- package/dist/daemon-control-handlers.js +24 -27
- package/dist/daemon-generations.d.ts +14 -28
- package/dist/daemon-generations.js +114 -53
- package/dist/daemon-lifecycle-service.d.ts +1 -3
- package/dist/daemon-lifecycle-service.js +88 -94
- package/dist/daemon-maintenance.d.ts +27 -49
- package/dist/daemon-maintenance.js +5 -1
- package/dist/daemon-options.d.ts +3 -2
- package/dist/daemon-policy-context.d.ts +1 -0
- package/dist/daemon-policy-context.js +1 -0
- package/dist/daemon-runtime-state.d.ts +1 -1
- package/dist/daemon-runtime-state.js +2 -3
- package/dist/daemon-state-context.d.ts +1 -1
- package/dist/daemon-state-context.js +1 -38
- package/dist/doctor-service.d.ts +1 -3
- package/dist/doctor-service.js +87 -91
- package/dist/effect/daemon-live.d.ts +7 -6
- package/dist/effect/daemon-live.js +57 -135
- package/dist/eval-routing-service.d.ts +1 -3
- package/dist/eval-routing-service.js +45 -69
- package/dist/eval-session-execution-store.d.ts +24 -0
- package/dist/eval-session-execution-store.js +253 -0
- package/dist/host-protocol.d.ts +1 -0
- package/dist/host.js +3 -0
- package/dist/internal-providers/provider-anthropic/index.d.ts +23 -0
- package/dist/internal-providers/provider-anthropic/index.js +42 -0
- package/dist/internal-providers/provider-anthropic/test/provider.test.js +33 -0
- package/dist/internal-providers/provider-azure/index.d.ts +25 -0
- package/dist/internal-providers/provider-azure/index.js +101 -0
- package/dist/internal-providers/provider-azure/test/provider.test.js +83 -0
- package/dist/internal-providers/provider-bedrock/codec.d.ts +11 -0
- package/dist/internal-providers/provider-bedrock/codec.js +484 -0
- package/dist/internal-providers/provider-bedrock/connection-settings.d.ts +6 -0
- package/dist/internal-providers/provider-bedrock/connection-settings.js +41 -0
- package/dist/internal-providers/provider-bedrock/connection.d.ts +17 -0
- package/dist/internal-providers/provider-bedrock/connection.js +143 -0
- package/dist/internal-providers/provider-bedrock/index.d.ts +27 -0
- package/dist/internal-providers/provider-bedrock/index.js +47 -0
- package/dist/internal-providers/provider-bedrock/source.d.ts +39 -0
- package/dist/internal-providers/provider-bedrock/source.js +454 -0
- package/dist/internal-providers/provider-bedrock/test/bedrock-source.test.d.ts +1 -0
- package/dist/internal-providers/provider-bedrock/test/bedrock-source.test.js +786 -0
- package/dist/internal-providers/provider-bedrock/test/connection.test.d.ts +1 -0
- package/dist/internal-providers/provider-bedrock/test/connection.test.js +249 -0
- package/dist/internal-providers/provider-bedrock/test/workload-credentials.test.d.ts +1 -0
- package/dist/internal-providers/provider-bedrock/test/workload-credentials.test.js +29 -0
- package/dist/internal-providers/provider-bedrock/workload-credentials.d.ts +5 -0
- package/dist/internal-providers/provider-bedrock/workload-credentials.js +14 -0
- package/dist/internal-providers/provider-claude-code/index.d.ts +6 -0
- package/dist/internal-providers/provider-claude-code/index.js +4 -0
- package/dist/internal-providers/provider-claude-code/module-api.d.ts +25 -0
- package/dist/internal-providers/provider-claude-code/module-api.js +31 -0
- package/dist/internal-providers/provider-claude-code/relay.d.ts +20 -0
- package/dist/internal-providers/provider-claude-code/relay.js +115 -0
- package/dist/internal-providers/provider-claude-code/subscription.d.ts +2 -0
- package/dist/internal-providers/provider-claude-code/subscription.js +194 -0
- package/dist/internal-providers/provider-claude-code/test/http-submission.test.d.ts +1 -0
- package/dist/internal-providers/provider-claude-code/test/http-submission.test.js +140 -0
- package/dist/internal-providers/provider-claude-code/test/relay.test.d.ts +1 -0
- package/dist/internal-providers/provider-claude-code/test/relay.test.js +379 -0
- package/dist/internal-providers/provider-claude-code/test/subscription.test.d.ts +1 -0
- package/dist/internal-providers/provider-claude-code/test/subscription.test.js +256 -0
- package/dist/internal-providers/provider-cliproxy/index.d.ts +9 -0
- package/dist/internal-providers/provider-cliproxy/index.js +15 -0
- package/dist/internal-providers/provider-codex/index.d.ts +6 -0
- package/dist/internal-providers/provider-codex/index.js +7 -0
- package/dist/internal-providers/provider-codex/module-api.d.ts +25 -0
- package/dist/internal-providers/provider-codex/module-api.js +31 -0
- package/dist/internal-providers/provider-codex/relay.d.ts +123 -0
- package/dist/internal-providers/provider-codex/relay.js +261 -0
- package/dist/internal-providers/provider-codex/subscription.d.ts +2 -0
- package/dist/internal-providers/provider-codex/subscription.js +555 -0
- package/dist/internal-providers/provider-codex/test/http-submission.test.d.ts +1 -0
- package/dist/internal-providers/provider-codex/test/http-submission.test.js +142 -0
- package/dist/internal-providers/provider-codex/test/relay.test.d.ts +1 -0
- package/dist/internal-providers/provider-codex/test/relay.test.js +600 -0
- package/dist/internal-providers/provider-codex/test/subscription.test.d.ts +1 -0
- package/dist/internal-providers/provider-codex/test/subscription.test.js +70 -0
- package/dist/internal-providers/provider-codex/test/translated-content.test.d.ts +1 -0
- package/dist/internal-providers/provider-codex/test/translated-content.test.js +91 -0
- package/dist/internal-providers/provider-custom/index.d.ts +9 -0
- package/dist/internal-providers/provider-custom/index.js +15 -0
- package/dist/internal-providers/provider-google/index.d.ts +23 -0
- package/dist/internal-providers/provider-google/index.js +43 -0
- package/dist/internal-providers/provider-google/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-google/test/provider.test.js +34 -0
- package/dist/internal-providers/provider-google-vertex/index.d.ts +28 -0
- package/dist/internal-providers/provider-google-vertex/index.js +137 -0
- package/dist/internal-providers/provider-google-vertex/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-google-vertex/test/provider.test.js +85 -0
- package/dist/internal-providers/provider-openai/documented-reasoning.d.ts +9 -0
- package/dist/internal-providers/provider-openai/documented-reasoning.js +26 -0
- package/dist/internal-providers/provider-openai/index.d.ts +23 -0
- package/dist/internal-providers/provider-openai/index.js +74 -0
- package/dist/internal-providers/provider-openai/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-openai/test/provider.test.js +268 -0
- package/dist/internal-providers/provider-openai-compatible/index.d.ts +9 -0
- package/dist/internal-providers/provider-openai-compatible/index.js +15 -0
- package/dist/internal-providers/provider-openai-compatible/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-openai-compatible/test/provider.test.js +40 -0
- package/dist/internal-providers/provider-openrouter/connection.d.ts +5 -0
- package/dist/internal-providers/provider-openrouter/connection.js +143 -0
- package/dist/internal-providers/provider-openrouter/index.d.ts +28 -0
- package/dist/internal-providers/provider-openrouter/index.js +42 -0
- package/dist/internal-providers/provider-openrouter/pricing.d.ts +4 -0
- package/dist/internal-providers/provider-openrouter/pricing.js +108 -0
- package/dist/internal-providers/provider-openrouter/test/connection.test.d.ts +1 -0
- package/dist/internal-providers/provider-openrouter/test/connection.test.js +71 -0
- package/dist/internal-providers/provider-openrouter/test/pricing.test.d.ts +1 -0
- package/dist/internal-providers/provider-openrouter/test/pricing.test.js +45 -0
- package/dist/internal-providers/provider-openrouter/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-openrouter/test/provider.test.js +141 -0
- package/dist/internal-providers/provider-openrouter/test/validation.test.d.ts +1 -0
- package/dist/internal-providers/provider-openrouter/test/validation.test.js +225 -0
- package/dist/launcher-service.d.ts +3 -7
- package/dist/launcher-service.js +144 -133
- package/dist/provider-modules.d.ts +6 -0
- package/dist/provider-modules.js +55 -0
- package/dist/provider-query-service.d.ts +1 -3
- package/dist/provider-query-service.js +140 -143
- package/dist/router-generation-service.d.ts +1 -3
- package/dist/router-generation-service.js +83 -78
- package/dist/services/eval-session/service.d.ts +20 -8
- package/dist/services/eval-session/service.js +410 -40
- package/dist/services/gateway-generation/service.d.ts +7 -3
- package/dist/services/gateway-generation/service.js +126 -63
- package/dist/services/generations/service.d.ts +3 -3
- package/dist/services/generations/service.js +5 -1
- package/dist/telemetry-application-service.d.ts +1 -3
- package/dist/telemetry-application-service.js +41 -45
- package/dist/test/call-attribution-store.test.js +186 -0
- package/dist/test/daemon-accounts.test.js +2 -2
- package/dist/test/daemon-data-plane.test.js +24 -45
- package/dist/test/daemon-fixtures.js +1 -1
- package/dist/test/daemon-generation-transaction.test.js +66 -2
- package/dist/test/daemon-runtime-state.test.js +28 -1
- package/dist/test/effect-daemon.test.js +38 -0
- package/dist/test/eval-routing-application-service.test.js +6 -6
- package/dist/test/eval-session-execution.test.d.ts +1 -0
- package/dist/test/eval-session-execution.test.js +356 -0
- package/dist/test/eval-session-service.test.js +70 -1
- package/dist/test/host-worker-session.test.js +1 -0
- package/dist/test/launcher-session.test.d.ts +1 -0
- package/dist/test/launcher-session.test.js +87 -0
- package/dist/test/leaderboard.test.js +25 -1
- package/dist/test/provider-modules.test.d.ts +1 -0
- package/dist/test/provider-modules.test.js +65 -0
- package/dist/test/routekit-upstream-sources.test.d.ts +1 -0
- package/dist/test/routekit-upstream-sources.test.js +40 -0
- package/dist/test/runtime-composition-types.test.js +11 -0
- package/dist/token-application-service.d.ts +1 -3
- package/dist/token-application-service.js +38 -44
- package/dist/worker.js +5 -3
- package/package.json +35 -11
- package/dist/application-services.d.ts +0 -10
- package/dist/application-services.js +0 -22
- package/dist/daemon-lifecycle.d.ts +0 -54
- package/dist/daemon-lifecycle.js +0 -177
- package/dist/test/application-services.test.js +0 -135
- package/dist/test/daemon-lifecycle.test.js +0 -182
- /package/dist/{test/application-services.test.d.ts → internal-providers/provider-anthropic/test/provider.test.d.ts} +0 -0
- /package/dist/{test/daemon-lifecycle.test.d.ts → internal-providers/provider-azure/test/provider.test.d.ts} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
4
|
+
import { bedrockRoleCredentials, bedrockRoleEnrollment, makeBedrockRoleCredentials } from "../connection.js";
|
|
5
|
+
import { BEDROCK_MASTER_CREDENTIALS_DISABLED, BEDROCK_MASTER_CREDENTIALS_RESOURCE } from "../workload-credentials.js";
|
|
6
|
+
const context = {
|
|
7
|
+
enrollmentId: "00000000-0000-4000-8000-000000000001", sourceId: "production",
|
|
8
|
+
nonce: "x".repeat(43), callbackUrl: "https://cloud.example/callback",
|
|
9
|
+
settings: {
|
|
10
|
+
rolePrincipalArn: "arn:aws:iam::123456789012:role/RouteKitCloud",
|
|
11
|
+
roleTemplateUrl: "https://routekit-templates.s3.us-east-1.amazonaws.com/connection-role.yaml"
|
|
12
|
+
},
|
|
13
|
+
fetch: async () => { assert.fail("Role setup must not make HTTP requests"); }
|
|
14
|
+
};
|
|
15
|
+
test("role onboarding uses an enrollment-specific external ID and stores a renewable role binding", async () => {
|
|
16
|
+
const start = await runRouteKitEffect(bedrockRoleEnrollment.start(context, { region: "us-east-1" }));
|
|
17
|
+
if (start.kind !== "action" || start.action.kind !== "external")
|
|
18
|
+
throw new Error("Missing setup action");
|
|
19
|
+
const url = new URL(start.action.url);
|
|
20
|
+
assert.equal(url.origin, "https://us-east-1.console.aws.amazon.com");
|
|
21
|
+
assert.match(url.hash, /^#\/stacks\/create\/review\?/u);
|
|
22
|
+
const params = new URLSearchParams(url.hash.split("?")[1]);
|
|
23
|
+
assert.equal(params.get("param_ExternalId"), context.nonce);
|
|
24
|
+
assert.equal(params.get("param_RouteKitPrincipalArn"), context.settings.rolePrincipalArn);
|
|
25
|
+
assert.match(params.get("stackName") ?? "", /^RouteKit-[A-Za-z0-9-]+-[a-f0-9]{24}$/u);
|
|
26
|
+
const form = await runRouteKitEffect(bedrockRoleEnrollment.resume(context, start.state, { kind: "submit", stepId: "setup", values: {} }));
|
|
27
|
+
if (form.kind !== "action" || form.action.kind !== "form")
|
|
28
|
+
throw new Error("Missing role form");
|
|
29
|
+
const complete = await runRouteKitEffect(bedrockRoleEnrollment.resume(context, form.state, {
|
|
30
|
+
kind: "submit",
|
|
31
|
+
stepId: "role",
|
|
32
|
+
values: {
|
|
33
|
+
"role-arn": "arn:aws:iam::999999999999:role/routekit-cloud/CustomerConnection"
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
if (complete.kind !== "complete")
|
|
37
|
+
throw new Error("Missing binding");
|
|
38
|
+
assert.deepEqual(complete.connection.environment, {});
|
|
39
|
+
assert.equal(complete.connection.credential?.kind, "assume-role");
|
|
40
|
+
const first = await runRouteKitEffect(bedrockRoleCredentials.resolve(complete.connection.credential, { sourceId: "one" }));
|
|
41
|
+
const second = await runRouteKitEffect(bedrockRoleCredentials.resolve(complete.connection.credential, { sourceId: "two" }));
|
|
42
|
+
assert.equal(first.environment?.AWS_REGION, "us-east-1");
|
|
43
|
+
assert.equal(first.environment?.AWS_ACCESS_KEY_ID, undefined);
|
|
44
|
+
assert.equal(typeof first.resources?.bedrockCredentials, "function");
|
|
45
|
+
assert.notEqual(first.resources?.bedrockCredentials, second.resources?.bedrockCredentials);
|
|
46
|
+
});
|
|
47
|
+
test("role connection availability requires deployment configuration and rejects malformed bindings", async () => {
|
|
48
|
+
assert.equal(bedrockRoleEnrollment.available?.({}), false);
|
|
49
|
+
assert.equal(bedrockRoleEnrollment.available?.(context.settings), true);
|
|
50
|
+
for (const roleTemplateUrl of [
|
|
51
|
+
"http://localhost/role.yaml",
|
|
52
|
+
"https://routekit-templates.s3.us-east-1.amazonaws.com/",
|
|
53
|
+
"https://s3.us-east-1.amazonaws.com/routekit-templates",
|
|
54
|
+
"https://routekit-templates.s3-website-us-east-1.amazonaws.com/role.yaml",
|
|
55
|
+
"https://routekit-templates.s3-accelerate.amazonaws.com/role.yaml",
|
|
56
|
+
"https://access:secret@routekit-templates.s3.us-east-1.amazonaws.com/role.yaml",
|
|
57
|
+
"https://routekit-templates.s3.us-east-1.amazonaws.com/role.yaml?versionId=one",
|
|
58
|
+
"https://routekit-templates.s3.us-east-1.amazonaws.com/role.yaml#fragment",
|
|
59
|
+
"https://routekit-templates.s3.us-east-1.amazonaws.com.example/role.yaml"
|
|
60
|
+
]) {
|
|
61
|
+
assert.equal(bedrockRoleEnrollment.available?.({
|
|
62
|
+
...context.settings,
|
|
63
|
+
roleTemplateUrl
|
|
64
|
+
}), false, roleTemplateUrl);
|
|
65
|
+
}
|
|
66
|
+
await assert.rejects(runRouteKitEffect(bedrockRoleCredentials.resolve({
|
|
67
|
+
version: 1, kind: "assume-role", payload: { roleArn: "bad", region: "us-east-1", externalId: "short" }
|
|
68
|
+
}, { sourceId: "one" })));
|
|
69
|
+
await assert.rejects(runRouteKitEffect(bedrockRoleEnrollment.resume(context, { version: 2, data: {} }, { kind: "submit", stepId: "role", values: {} })));
|
|
70
|
+
});
|
|
71
|
+
test("role onboarding rejects out-of-scope roles and malformed enrollment nonces", async () => {
|
|
72
|
+
const started = await runRouteKitEffect(bedrockRoleEnrollment.start(context, { region: "us-east-1" }));
|
|
73
|
+
if (started.kind !== "action")
|
|
74
|
+
throw new Error("Missing setup action");
|
|
75
|
+
for (const roleArn of [
|
|
76
|
+
"arn:aws:iam::999999999999:role/CustomerConnection",
|
|
77
|
+
"arn:aws:iam::999999999999:role/routekit-cloudish/CustomerConnection",
|
|
78
|
+
"arn:aws:iam::999999999999:role/routekit-cloud/",
|
|
79
|
+
"arn:aws:iam::99999999999:role/routekit-cloud/CustomerConnection",
|
|
80
|
+
"arn:aws-us-gov:iam::999999999999:role/routekit-cloud/CustomerConnection",
|
|
81
|
+
`arn:aws:iam::999999999999:role/routekit-cloud/${"x".repeat(2049)}`
|
|
82
|
+
]) {
|
|
83
|
+
await assert.rejects(runRouteKitEffect(bedrockRoleEnrollment.resume(context, started.state, {
|
|
84
|
+
kind: "submit",
|
|
85
|
+
stepId: "role",
|
|
86
|
+
values: { "role-arn": roleArn }
|
|
87
|
+
})), /Enter the RoleArn/);
|
|
88
|
+
}
|
|
89
|
+
for (const nonce of [
|
|
90
|
+
"x".repeat(42),
|
|
91
|
+
"x".repeat(44),
|
|
92
|
+
"x".repeat(42) + "=",
|
|
93
|
+
"x".repeat(42) + "/"
|
|
94
|
+
]) {
|
|
95
|
+
await assert.rejects(runRouteKitEffect(bedrockRoleEnrollment.start({ ...context, nonce }, { region: "us-east-1" })), /Choose a valid AWS region/);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
test("role onboarding creates deterministic collision-resistant legal stack names", async () => {
|
|
99
|
+
const enrollmentIds = [
|
|
100
|
+
"12345678-0000-4000-8000-000000000001",
|
|
101
|
+
"12345678-0000-4000-8000-000000000002",
|
|
102
|
+
`${"long/id with unsafe characters ".repeat(12)}one`,
|
|
103
|
+
`${"long/id with unsafe characters ".repeat(12)}two`
|
|
104
|
+
];
|
|
105
|
+
const names = [];
|
|
106
|
+
for (const enrollmentId of enrollmentIds) {
|
|
107
|
+
const started = await runRouteKitEffect(bedrockRoleEnrollment.start({ ...context, enrollmentId }, { region: "us-east-1" }));
|
|
108
|
+
if (started.kind !== "action" || started.action.kind !== "external") {
|
|
109
|
+
throw new Error("Missing setup action");
|
|
110
|
+
}
|
|
111
|
+
const url = new URL(started.action.url);
|
|
112
|
+
const parameters = new URLSearchParams(url.hash.split("?")[1]);
|
|
113
|
+
const stackName = parameters.get("stackName");
|
|
114
|
+
assert.ok(stackName);
|
|
115
|
+
assert.match(stackName, /^[A-Za-z][A-Za-z0-9-]*$/u);
|
|
116
|
+
assert.ok(stackName.length <= 128);
|
|
117
|
+
names.push(stackName);
|
|
118
|
+
const replay = await runRouteKitEffect(bedrockRoleEnrollment.start({ ...context, enrollmentId }, { region: "us-east-1" }));
|
|
119
|
+
if (replay.kind !== "action" || replay.action.kind !== "external") {
|
|
120
|
+
throw new Error("Missing replay setup action");
|
|
121
|
+
}
|
|
122
|
+
const replayParameters = new URLSearchParams(new URL(replay.action.url).hash.split("?")[1]);
|
|
123
|
+
assert.equal(replayParameters.get("stackName"), stackName);
|
|
124
|
+
}
|
|
125
|
+
assert.equal(new Set(names).size, enrollmentIds.length);
|
|
126
|
+
});
|
|
127
|
+
test("role credentials prefer an injected renewable master provider and isolate every binding", async () => {
|
|
128
|
+
const injectedMaster = async () => ({
|
|
129
|
+
accessKeyId: "platform-temporary",
|
|
130
|
+
secretAccessKey: "platform-secret",
|
|
131
|
+
sessionToken: "platform-token"
|
|
132
|
+
});
|
|
133
|
+
let nodeProviderCalls = 0;
|
|
134
|
+
const calls = [];
|
|
135
|
+
const adapter = makeBedrockRoleCredentials({
|
|
136
|
+
nodeProviderChain: () => {
|
|
137
|
+
nodeProviderCalls += 1;
|
|
138
|
+
return async () => ({
|
|
139
|
+
accessKeyId: "node-temporary",
|
|
140
|
+
secretAccessKey: "node-secret"
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
temporaryCredentials: ((input) => {
|
|
144
|
+
calls.push(input);
|
|
145
|
+
return async () => ({
|
|
146
|
+
accessKeyId: `customer-${calls.length}`,
|
|
147
|
+
secretAccessKey: "customer-secret",
|
|
148
|
+
sessionToken: "customer-token"
|
|
149
|
+
});
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
const firstBinding = {
|
|
153
|
+
version: 1,
|
|
154
|
+
kind: "assume-role",
|
|
155
|
+
payload: {
|
|
156
|
+
roleArn: "arn:aws:iam::111111111111:role/routekit-cloud/First",
|
|
157
|
+
externalId: "a".repeat(43),
|
|
158
|
+
region: "us-east-1"
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const secondBinding = {
|
|
162
|
+
version: 1,
|
|
163
|
+
kind: "assume-role",
|
|
164
|
+
payload: {
|
|
165
|
+
roleArn: "arn:aws:iam::222222222222:role/routekit-cloud/Second",
|
|
166
|
+
externalId: "b".repeat(43),
|
|
167
|
+
region: "us-west-2"
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const resources = { [BEDROCK_MASTER_CREDENTIALS_RESOURCE]: injectedMaster };
|
|
171
|
+
const first = await runRouteKitEffect(adapter.resolve(firstBinding, { sourceId: "first", resources }));
|
|
172
|
+
const second = await runRouteKitEffect(adapter.resolve(secondBinding, { sourceId: "second", resources }));
|
|
173
|
+
assert.equal(nodeProviderCalls, 0);
|
|
174
|
+
assert.equal(calls.length, 2);
|
|
175
|
+
assert.equal(calls[0]?.masterCredentials, injectedMaster);
|
|
176
|
+
assert.equal(calls[1]?.masterCredentials, injectedMaster);
|
|
177
|
+
assert.deepEqual(calls[0]?.params, {
|
|
178
|
+
RoleArn: firstBinding.payload.roleArn,
|
|
179
|
+
ExternalId: firstBinding.payload.externalId,
|
|
180
|
+
RoleSessionName: "routekit-cloud"
|
|
181
|
+
});
|
|
182
|
+
assert.deepEqual(calls[1]?.params, {
|
|
183
|
+
RoleArn: secondBinding.payload.roleArn,
|
|
184
|
+
ExternalId: secondBinding.payload.externalId,
|
|
185
|
+
RoleSessionName: "routekit-cloud"
|
|
186
|
+
});
|
|
187
|
+
assert.notEqual(first.resources?.bedrockCredentials, second.resources?.bedrockCredentials);
|
|
188
|
+
assert.doesNotMatch(JSON.stringify(firstBinding), /platform-temporary|customer-temporary/u);
|
|
189
|
+
});
|
|
190
|
+
test("role credentials use the Node chain only when the master resource is absent", async () => {
|
|
191
|
+
const nodeMaster = async () => ({
|
|
192
|
+
accessKeyId: "node-temporary",
|
|
193
|
+
secretAccessKey: "node-secret"
|
|
194
|
+
});
|
|
195
|
+
let nodeProviderCalls = 0;
|
|
196
|
+
let capturedMaster;
|
|
197
|
+
const adapter = makeBedrockRoleCredentials({
|
|
198
|
+
nodeProviderChain: () => {
|
|
199
|
+
nodeProviderCalls += 1;
|
|
200
|
+
return nodeMaster;
|
|
201
|
+
},
|
|
202
|
+
temporaryCredentials: ((input) => {
|
|
203
|
+
capturedMaster = input.masterCredentials;
|
|
204
|
+
return async () => ({
|
|
205
|
+
accessKeyId: "customer-temporary",
|
|
206
|
+
secretAccessKey: "customer-secret"
|
|
207
|
+
});
|
|
208
|
+
})
|
|
209
|
+
});
|
|
210
|
+
await runRouteKitEffect(adapter.resolve({
|
|
211
|
+
version: 1,
|
|
212
|
+
kind: "assume-role",
|
|
213
|
+
payload: {
|
|
214
|
+
roleArn: "arn:aws:iam::111111111111:role/Customer",
|
|
215
|
+
externalId: "x".repeat(43),
|
|
216
|
+
region: "us-east-1"
|
|
217
|
+
}
|
|
218
|
+
}, { sourceId: "fallback" }));
|
|
219
|
+
assert.equal(nodeProviderCalls, 1);
|
|
220
|
+
assert.equal(capturedMaster, nodeMaster);
|
|
221
|
+
await assert.rejects(runRouteKitEffect(adapter.resolve({
|
|
222
|
+
version: 1,
|
|
223
|
+
kind: "assume-role",
|
|
224
|
+
payload: {
|
|
225
|
+
roleArn: "arn:aws:iam::111111111111:role/Customer",
|
|
226
|
+
externalId: "x".repeat(43),
|
|
227
|
+
region: "us-east-1"
|
|
228
|
+
}
|
|
229
|
+
}, {
|
|
230
|
+
sourceId: "disabled",
|
|
231
|
+
resources: {
|
|
232
|
+
[BEDROCK_MASTER_CREDENTIALS_RESOURCE]: BEDROCK_MASTER_CREDENTIALS_DISABLED
|
|
233
|
+
}
|
|
234
|
+
})), /stored AWS role connection is invalid/u);
|
|
235
|
+
assert.equal(nodeProviderCalls, 1);
|
|
236
|
+
await assert.rejects(runRouteKitEffect(adapter.resolve({
|
|
237
|
+
version: 1,
|
|
238
|
+
kind: "assume-role",
|
|
239
|
+
payload: {
|
|
240
|
+
roleArn: "arn:aws:iam::111111111111:role/Customer",
|
|
241
|
+
externalId: "x".repeat(43),
|
|
242
|
+
region: "us-east-1"
|
|
243
|
+
}
|
|
244
|
+
}, {
|
|
245
|
+
sourceId: "invalid",
|
|
246
|
+
resources: { [BEDROCK_MASTER_CREDENTIALS_RESOURCE]: "not-a-provider" }
|
|
247
|
+
})), /stored AWS role connection is invalid/u);
|
|
248
|
+
assert.equal(nodeProviderCalls, 1);
|
|
249
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { BEDROCK_MASTER_CREDENTIALS_DISABLED, BEDROCK_MASTER_CREDENTIALS_RESOURCE, bedrockMasterCredentialsFromResources } from "../workload-credentials.js";
|
|
4
|
+
test("Bedrock master credential resources are optional renewable providers", async () => {
|
|
5
|
+
assert.equal(bedrockMasterCredentialsFromResources(undefined, "bedrock"), undefined);
|
|
6
|
+
assert.equal(bedrockMasterCredentialsFromResources({}, "bedrock"), undefined);
|
|
7
|
+
let calls = 0;
|
|
8
|
+
const provider = async () => {
|
|
9
|
+
calls += 1;
|
|
10
|
+
return {
|
|
11
|
+
accessKeyId: `temporary-${calls}`,
|
|
12
|
+
secretAccessKey: "temporary-secret",
|
|
13
|
+
sessionToken: "temporary-token"
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
const resolved = bedrockMasterCredentialsFromResources({ [BEDROCK_MASTER_CREDENTIALS_RESOURCE]: provider }, "bedrock");
|
|
17
|
+
assert.equal(resolved, provider);
|
|
18
|
+
assert.equal(calls, 0, "resource validation must not eagerly obtain credentials");
|
|
19
|
+
assert.equal((await resolved?.()).accessKeyId, "temporary-1");
|
|
20
|
+
assert.equal((await resolved?.()).accessKeyId, "temporary-2");
|
|
21
|
+
});
|
|
22
|
+
test("Bedrock master credential resources fail closed when present but invalid", () => {
|
|
23
|
+
assert.throws(() => bedrockMasterCredentialsFromResources({
|
|
24
|
+
[BEDROCK_MASTER_CREDENTIALS_RESOURCE]: BEDROCK_MASTER_CREDENTIALS_DISABLED
|
|
25
|
+
}, "customer"), /host connector is disabled/);
|
|
26
|
+
for (const invalid of [null, {}, [], "credential", 1, true]) {
|
|
27
|
+
assert.throws(() => bedrockMasterCredentialsFromResources({ [BEDROCK_MASTER_CREDENTIALS_RESOURCE]: invalid }, "customer"), /invalid "bedrockMasterCredentials" resource/);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BedrockClientConfig } from "@aws-sdk/client-bedrock";
|
|
2
|
+
export declare const BEDROCK_MASTER_CREDENTIALS_RESOURCE = "bedrockMasterCredentials";
|
|
3
|
+
export declare const BEDROCK_MASTER_CREDENTIALS_DISABLED: unique symbol;
|
|
4
|
+
export type BedrockMasterCredentialsProvider = Extract<NonNullable<BedrockClientConfig["credentials"]>, (...arguments_: never[]) => unknown>;
|
|
5
|
+
export declare function bedrockMasterCredentialsFromResources(resources: Readonly<Record<string, unknown>> | undefined, sourceId: string): BedrockMasterCredentialsProvider | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const BEDROCK_MASTER_CREDENTIALS_RESOURCE = "bedrockMasterCredentials";
|
|
2
|
+
export const BEDROCK_MASTER_CREDENTIALS_DISABLED = Symbol.for("@velum-labs/routekit-provider-bedrock/master-credentials-disabled");
|
|
3
|
+
export function bedrockMasterCredentialsFromResources(resources, sourceId) {
|
|
4
|
+
const resource = resources?.[BEDROCK_MASTER_CREDENTIALS_RESOURCE];
|
|
5
|
+
if (resource === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (resource === BEDROCK_MASTER_CREDENTIALS_DISABLED) {
|
|
8
|
+
throw new Error(`provider source "${sourceId}" cannot use "${BEDROCK_MASTER_CREDENTIALS_RESOURCE}" while the host connector is disabled`);
|
|
9
|
+
}
|
|
10
|
+
if (typeof resource !== "function") {
|
|
11
|
+
throw new Error(`provider source "${sourceId}" received an invalid "${BEDROCK_MASTER_CREDENTIALS_RESOURCE}" resource`);
|
|
12
|
+
}
|
|
13
|
+
return resource;
|
|
14
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AnthropicBackendRelay } from "./relay.js";
|
|
2
|
+
import type { SubscriptionAccountSet } from "@velum-labs/routekit-accounts";
|
|
3
|
+
export { anthropicProvider, claudeCode, claudeCodeSubscriptionProvider } from "./module-api.js";
|
|
4
|
+
export type { AnthropicRelayOptions } from "./relay.js";
|
|
5
|
+
export { AnthropicBackendRelay } from "./relay.js";
|
|
6
|
+
export declare const claudeCodeRelayFactory: (accounts: SubscriptionAccountSet) => AnthropicBackendRelay;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AnthropicBackendRelay } from "./relay.js";
|
|
2
|
+
export { anthropicProvider, claudeCode, claudeCodeSubscriptionProvider } from "./module-api.js";
|
|
3
|
+
export { AnthropicBackendRelay } from "./relay.js";
|
|
4
|
+
export const claudeCodeRelayFactory = (accounts) => new AnthropicBackendRelay({ accounts });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SubscriptionAccountBackend } from "@velum-labs/routekit-accounts/provider";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
export declare const claudeCode: {
|
|
4
|
+
readonly id: "claude-code";
|
|
5
|
+
readonly protocol: "anthropic";
|
|
6
|
+
readonly accountClass: "subscription";
|
|
7
|
+
readonly billingMode: "subscription";
|
|
8
|
+
readonly connection: Readonly<{
|
|
9
|
+
validate?: (context: Readonly<{
|
|
10
|
+
sourceId: string;
|
|
11
|
+
config: import("@velum-labs/routekit-config-core").SourceConfig;
|
|
12
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
13
|
+
resources?: Readonly<Record<string, unknown>>;
|
|
14
|
+
}>) => Effect.Effect<void, Error, import("@velum-labs/routekit-runtime/effect").RouteKitPlatform>;
|
|
15
|
+
credentialValidation?: "registry-key-probe" | "registry-key-probe-if-configured-models";
|
|
16
|
+
manifest: import("@velum-labs/routekit-provider-core").ProviderConnectionManifest;
|
|
17
|
+
prepare?: (input: import("@velum-labs/routekit-provider-core").PrepareProviderConnectionInput) => import("@velum-labs/routekit-provider-core").PreparedProviderConnection;
|
|
18
|
+
enrollment?: Readonly<Record<string, import("@velum-labs/routekit-provider-core").ProviderEnrollmentAdapter>>;
|
|
19
|
+
credentials?: import("@velum-labs/routekit-provider-core").ProviderCredentialAdapter;
|
|
20
|
+
subscriptionMode?: import("@velum-labs/routekit-registry").SubscriptionMode;
|
|
21
|
+
}>;
|
|
22
|
+
readonly createSource: (context: import("@velum-labs/routekit-provider-core").ProviderModuleContext) => Effect.Effect<SubscriptionAccountBackend, never, never>;
|
|
23
|
+
};
|
|
24
|
+
export { anthropicProvider } from "./subscription.js";
|
|
25
|
+
export declare const claudeCodeSubscriptionProvider: import("@velum-labs/routekit-accounts").SubscriptionProvider<"claude-code">;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SubscriptionAccountBackend } from "@velum-labs/routekit-accounts/provider";
|
|
2
|
+
import { AnthropicBackend, defineProviderConnection, defineProvider } from "@velum-labs/routekit-provider-core";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { anthropicProvider } from "./subscription.js";
|
|
5
|
+
export const claudeCode = defineProvider({
|
|
6
|
+
id: "claude-code",
|
|
7
|
+
protocol: "anthropic",
|
|
8
|
+
accountClass: "subscription",
|
|
9
|
+
billingMode: "subscription",
|
|
10
|
+
connection: defineProviderConnection({
|
|
11
|
+
subscriptionMode: "claude-code",
|
|
12
|
+
manifest: {
|
|
13
|
+
version: 1, id: "claude-code", displayName: "Claude Code", category: "subscription",
|
|
14
|
+
description: "Connect a Claude subscription through the Claude Code OAuth flow.",
|
|
15
|
+
methods: [{ id: "oauth", kind: "oauth", label: "Sign in with Claude", schemaRevision: 1, fields: [] }]
|
|
16
|
+
}
|
|
17
|
+
}),
|
|
18
|
+
createSource: (context) => Effect.sync(() => {
|
|
19
|
+
const accountSet = context.resources?.accountSet;
|
|
20
|
+
if (accountSet === undefined || accountSet.mode !== "claude-code") {
|
|
21
|
+
throw new Error('provider module "claude-code" requires its daemon-owned account set');
|
|
22
|
+
}
|
|
23
|
+
return new SubscriptionAccountBackend({
|
|
24
|
+
sourceId: context.sourceId,
|
|
25
|
+
accountSet,
|
|
26
|
+
backendFactory: (_mode, backendOptions) => new AnthropicBackend(backendOptions)
|
|
27
|
+
});
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
export { anthropicProvider } from "./subscription.js";
|
|
31
|
+
export const claudeCodeSubscriptionProvider = anthropicProvider();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
2
|
+
import { type SubscriptionAccountSet, type SubscriptionAccountSetSnapshot, type SubscriptionAnthropicRequest, type SubscriptionGatewayRequestRelay, type SubscriptionRelay, type SubscriptionResponsesRequest } from "@velum-labs/routekit-accounts";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
export type AnthropicRelayOptions = {
|
|
5
|
+
accounts: SubscriptionAccountSet;
|
|
6
|
+
backendUrl?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class AnthropicBackendRelay implements SubscriptionRelay {
|
|
9
|
+
#private;
|
|
10
|
+
readonly kind: "request";
|
|
11
|
+
readonly dialect: "anthropic";
|
|
12
|
+
readonly httpSubmission: SubscriptionGatewayRequestRelay["httpSubmission"];
|
|
13
|
+
constructor(options: AnthropicRelayOptions);
|
|
14
|
+
shouldRelay(_headers: IncomingHttpHeaders, model: string | undefined, servesLocally: (model: string) => boolean): boolean;
|
|
15
|
+
relay(headers: IncomingHttpHeaders, body: SubscriptionAnthropicRequest | SubscriptionResponsesRequest, signal?: AbortSignal, options?: Parameters<SubscriptionGatewayRequestRelay["relay"]>[3]): Effect.Effect<Response, Error | import("effect/PlatformError").PlatformError, any>;
|
|
16
|
+
models(headers: IncomingHttpHeaders, search: string, signal?: AbortSignal): Effect.Effect<Response, Error | import("effect/PlatformError").PlatformError, any>;
|
|
17
|
+
countTokens(headers: IncomingHttpHeaders, body: SubscriptionAnthropicRequest, signal?: AbortSignal): Effect.Effect<Response, Error | import("effect/PlatformError").PlatformError, any>;
|
|
18
|
+
snapshot(): SubscriptionAccountSetSnapshot;
|
|
19
|
+
close(): Effect.Effect<void, import("@velum-labs/routekit-runtime/effect").RouteKitFailure, never>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { forwardRelayHeaders } from "@velum-labs/routekit-accounts";
|
|
3
|
+
import { providerDefaultBaseUrl, subscriptionInfo } from "@velum-labs/routekit-registry";
|
|
4
|
+
import { executeInferenceRequest, executeWebRequest, routeKitError } from "@velum-labs/routekit-runtime/effect";
|
|
5
|
+
import { trimTrailingSlashes } from "@velum-labs/routekit-runtime/network";
|
|
6
|
+
import { Effect } from "effect";
|
|
7
|
+
function mergeHeaderTokens(current, required) {
|
|
8
|
+
const tokens = new Set([current, required]
|
|
9
|
+
.flatMap((value) => value?.split(",") ?? [])
|
|
10
|
+
.map((value) => value.trim())
|
|
11
|
+
.filter((value) => value.length > 0));
|
|
12
|
+
return [...tokens].join(",");
|
|
13
|
+
}
|
|
14
|
+
function withAnthropicAccount(body, accountId) {
|
|
15
|
+
if (accountId === undefined)
|
|
16
|
+
return body;
|
|
17
|
+
const request = body;
|
|
18
|
+
const userId = request.metadata?.user_id;
|
|
19
|
+
if (typeof userId !== "string")
|
|
20
|
+
return body;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(userId);
|
|
23
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
24
|
+
return body;
|
|
25
|
+
return {
|
|
26
|
+
...body,
|
|
27
|
+
metadata: {
|
|
28
|
+
...request.metadata,
|
|
29
|
+
user_id: JSON.stringify({
|
|
30
|
+
...parsed,
|
|
31
|
+
account_uuid: accountId
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class AnthropicBackendRelay {
|
|
41
|
+
kind = "request";
|
|
42
|
+
dialect = "anthropic";
|
|
43
|
+
httpSubmission;
|
|
44
|
+
#accounts;
|
|
45
|
+
#backendUrl;
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.#accounts = options.accounts;
|
|
48
|
+
this.#backendUrl = trimTrailingSlashes(options.backendUrl ?? providerDefaultBaseUrl("anthropic") ?? "https://api.anthropic.com");
|
|
49
|
+
this.httpSubmission =
|
|
50
|
+
this.#accounts.mode === "claude-code" &&
|
|
51
|
+
this.#backendUrl === trimTrailingSlashes(providerDefaultBaseUrl("anthropic") ?? "")
|
|
52
|
+
? "credential-owner-http-v1"
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
shouldRelay(_headers, model, servesLocally) {
|
|
56
|
+
return model === undefined || model.length === 0 || !servesLocally(model);
|
|
57
|
+
}
|
|
58
|
+
relay(headers, body, signal, options) {
|
|
59
|
+
const operationId = randomUUID();
|
|
60
|
+
return this.#accounts.execute(body.model, (credential) => {
|
|
61
|
+
const upstreamHeaders = this.#upstreamHeaders(headers, credential.accessToken);
|
|
62
|
+
return executeInferenceRequest(`${this.#backendUrl}/v1/messages`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: upstreamHeaders,
|
|
65
|
+
body: JSON.stringify(withAnthropicAccount(body, credential.accountId)),
|
|
66
|
+
...(signal !== undefined ? { signal } : {})
|
|
67
|
+
}).pipe(Effect.mapError((error) => routeKitError(error)));
|
|
68
|
+
}, signal, {
|
|
69
|
+
responseMode: options?.responseMode,
|
|
70
|
+
onAttempt: (account) => options?.onAttribution?.({
|
|
71
|
+
accountAttempt: {
|
|
72
|
+
operationId,
|
|
73
|
+
seat: account.seat,
|
|
74
|
+
credentialId: account.credentialId
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
77
|
+
onRetriedAttempt: (attempt) => options?.onAttribution?.({
|
|
78
|
+
accountAttemptResult: { operationId, ...attempt }
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
models(headers, search, signal) {
|
|
83
|
+
return this.#accounts.execute(undefined, (credential) => executeWebRequest(`${this.#backendUrl}/v1/models${search}`, {
|
|
84
|
+
headers: this.#upstreamHeaders(headers, credential.accessToken),
|
|
85
|
+
...(signal !== undefined ? { signal } : {})
|
|
86
|
+
}).pipe(Effect.mapError((error) => routeKitError(error))));
|
|
87
|
+
}
|
|
88
|
+
countTokens(headers, body, signal) {
|
|
89
|
+
return this.#accounts.execute(body.model, (credential) => executeWebRequest(`${this.#backendUrl}/v1/messages/count_tokens`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: this.#upstreamHeaders(headers, credential.accessToken),
|
|
92
|
+
body: JSON.stringify(withAnthropicAccount(body, credential.accountId)),
|
|
93
|
+
...(signal !== undefined ? { signal } : {})
|
|
94
|
+
}).pipe(Effect.mapError((error) => routeKitError(error))));
|
|
95
|
+
}
|
|
96
|
+
snapshot() {
|
|
97
|
+
return this.#accounts.statusSnapshot();
|
|
98
|
+
}
|
|
99
|
+
close() {
|
|
100
|
+
return this.#accounts.close();
|
|
101
|
+
}
|
|
102
|
+
#upstreamHeaders(headers, accessToken) {
|
|
103
|
+
const forwarded = forwardRelayHeaders(headers);
|
|
104
|
+
delete forwarded.authorization;
|
|
105
|
+
delete forwarded.Authorization;
|
|
106
|
+
delete forwarded["x-api-key"];
|
|
107
|
+
const oauthBeta = subscriptionInfo("claude-code").oauthBetaHeader ?? "oauth-2025-04-20";
|
|
108
|
+
Object.assign(forwarded, {
|
|
109
|
+
authorization: `Bearer ${accessToken}`,
|
|
110
|
+
"anthropic-beta": mergeHeaderTokens(forwarded["anthropic-beta"], oauthBeta),
|
|
111
|
+
"content-type": "application/json"
|
|
112
|
+
});
|
|
113
|
+
return forwarded;
|
|
114
|
+
}
|
|
115
|
+
}
|