forked from bai/curriculum-project-hub
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53998d2651 | |||
| 5b55cf18a8 | |||
| b0d691d53f | |||
| 1f48c5b707 | |||
| 12a2f3117f | |||
| 2ee84d9543 | |||
| 3087132083 | |||
| 63c86322de | |||
| ebf870249f | |||
| f065f9f978 |
@@ -41,7 +41,9 @@ Everything else is platform-managed or derived: instance/Organization id,
|
|||||||
server, SSH settings, release, resource ceilings, database coordinates and
|
server, SSH settings, release, resource ceilings, database coordinates and
|
||||||
generated password, domain, port, workspace, provider/base URL, model/role,
|
generated password, domain, port, workspace, provider/base URL, model/role,
|
||||||
curated skills, concurrency, request/file limits and the managed Mihomo proxy
|
curated skills, concurrency, request/file limits and the managed Mihomo proxy
|
||||||
environment.
|
environment. `NODE_USE_ENV_PROXY=1` is required on Node.js 24 so Hub's built-in
|
||||||
|
`fetch` actually uses that proxy; merely setting `HTTP_PROXY`/`HTTPS_PROXY` is
|
||||||
|
not sufficient.
|
||||||
|
|
||||||
The Feishu app is scoped to this Silo. OAuth users authenticated by that app are
|
The Feishu app is scoped to this Silo. OAuth users authenticated by that app are
|
||||||
automatically admitted to this Organization; OWNER remains the initial
|
automatically admitted to this Organization; OWNER remains the initial
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ const platformEnv = [
|
|||||||
envLine("HTTPS_PROXY", "http://127.0.0.1:7890"),
|
envLine("HTTPS_PROXY", "http://127.0.0.1:7890"),
|
||||||
envLine("ALL_PROXY", "socks5h://127.0.0.1:7890"),
|
envLine("ALL_PROXY", "socks5h://127.0.0.1:7890"),
|
||||||
envLine("NO_PROXY", "127.0.0.1,localhost,::1"),
|
envLine("NO_PROXY", "127.0.0.1,localhost,::1"),
|
||||||
|
envLine("NODE_USE_ENV_PROXY", "1"),
|
||||||
|
envLine("ANTHROPIC_DEFAULT_SONNET_MODEL", "anthropic/claude-sonnet-5"),
|
||||||
envLine("CPH_SANDBOX_EXTRA_DENY_READ", `${envPath}:${keyringPath}`),
|
envLine("CPH_SANDBOX_EXTRA_DENY_READ", `${envPath}:${keyringPath}`),
|
||||||
"",
|
"",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@paradigm/hub",
|
"name": "@paradigm/hub",
|
||||||
"version": "0.0.15",
|
"version": "0.0.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@paradigm/hub",
|
"name": "@paradigm/hub",
|
||||||
"version": "0.0.15",
|
"version": "0.0.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.3.202",
|
"@anthropic-ai/claude-agent-sdk": "^0.3.202",
|
||||||
"@fastify/cookie": "^11.0.2",
|
"@fastify/cookie": "^11.0.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@paradigm/hub",
|
"name": "@paradigm/hub",
|
||||||
"version": "0.0.15",
|
"version": "0.0.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ async function forwardProviderRequest(
|
|||||||
upstream: ProviderUpstreamCredential,
|
upstream: ProviderUpstreamCredential,
|
||||||
options: ProviderProxyOptions,
|
options: ProviderProxyOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!authorized(request.headers.authorization, header(request.headers["x-api-key"]), capability)) {
|
if (!authorized(request.headers.authorization, capability)) {
|
||||||
options.onDiagnostic?.({ code: "provider_proxy_unauthorized", category: "authorization" });
|
options.onDiagnostic?.({ code: "provider_proxy_unauthorized", category: "authorization" });
|
||||||
response.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
|
response.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
|
||||||
response.end("unauthorized");
|
response.end("unauthorized");
|
||||||
@@ -165,24 +165,13 @@ async function forwardProviderRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function authorized(
|
function authorized(value: string | undefined, capability: string): boolean {
|
||||||
authorization: string | undefined,
|
if (value === undefined || !value.startsWith("Bearer ")) return false;
|
||||||
apiKey: string | undefined,
|
const supplied = Buffer.from(value.slice("Bearer ".length));
|
||||||
capability: string,
|
|
||||||
): boolean {
|
|
||||||
const value = authorization?.startsWith("Bearer ")
|
|
||||||
? authorization.slice("Bearer ".length)
|
|
||||||
: apiKey;
|
|
||||||
if (value === undefined) return false;
|
|
||||||
const supplied = Buffer.from(value);
|
|
||||||
const expected = Buffer.from(capability);
|
const expected = Buffer.from(capability);
|
||||||
return supplied.byteLength === expected.byteLength && timingSafeEqual(supplied, expected);
|
return supplied.byteLength === expected.byteLength && timingSafeEqual(supplied, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
function header(value: string | string[] | undefined): string | undefined {
|
|
||||||
return Array.isArray(value) ? value[0] : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function forwardedRequestHeaders(source: IncomingHttpHeaders): Headers {
|
function forwardedRequestHeaders(source: IncomingHttpHeaders): Headers {
|
||||||
const result = new Headers();
|
const result = new Headers();
|
||||||
for (const [name, value] of Object.entries(source)) {
|
for (const [name, value] of Object.entries(source)) {
|
||||||
|
|||||||
@@ -107,33 +107,6 @@ describe("run-scoped provider proxy", () => {
|
|||||||
expect(diagnostics).toEqual([{ code: "provider_proxy_unauthorized", category: "authorization" }]);
|
expect(diagnostics).toEqual([{ code: "provider_proxy_unauthorized", category: "authorization" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts the run capability in the Anthropic x-api-key header", async () => {
|
|
||||||
let upstreamRequests = 0;
|
|
||||||
const upstream = createServer((_request, response) => {
|
|
||||||
upstreamRequests += 1;
|
|
||||||
response.writeHead(200, { "content-type": "application/json" });
|
|
||||||
response.end(JSON.stringify({ ok: true }));
|
|
||||||
});
|
|
||||||
upstreamServers.push(upstream);
|
|
||||||
upstream.listen(0, "127.0.0.1");
|
|
||||||
await once(upstream, "listening");
|
|
||||||
const address = upstream.address();
|
|
||||||
if (address === null || typeof address === "string") throw new Error("expected upstream TCP address");
|
|
||||||
const lease = await openProviderProxyLease({
|
|
||||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
||||||
authToken: "customer-secret",
|
|
||||||
anthropicApiKey: "",
|
|
||||||
});
|
|
||||||
leases.push(lease);
|
|
||||||
|
|
||||||
const response = await fetch(`${lease.sdkEnv.ANTHROPIC_BASE_URL}/v1/messages`, {
|
|
||||||
headers: { "x-api-key": lease.sdkEnv.ANTHROPIC_AUTH_TOKEN ?? "" },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(upstreamRequests).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not forward provider credentials across an upstream redirect", async () => {
|
it("does not forward provider credentials across an upstream redirect", async () => {
|
||||||
let redirectedRequests = 0;
|
let redirectedRequests = 0;
|
||||||
const diagnostics: unknown[] = [];
|
const diagnostics: unknown[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user