Skip to content

Commit dcdd8c1

Browse files
committed
feat(api): support Skiller Web localhost links
1 parent 670a168 commit dcdd8c1

4 files changed

Lines changed: 114 additions & 14 deletions

File tree

packages/api/src/http.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -611,22 +611,31 @@ const resolveRequestOrigin = (request: HttpServerRequest.HttpServerRequest): str
611611
const resolveSkillerBackendUrl = (request: HttpServerRequest.HttpServerRequest): string =>
612612
resolveDockerGitSkillerBackendUrl(process.env, resolveRequestOrigin(request))
613613

614+
const isPrivateNetworkCorsRequest = (
615+
request: HttpServerRequest.HttpServerRequest
616+
): boolean =>
617+
readHeader(request, "access-control-request-private-network")?.toLowerCase() === "true"
618+
614619
const skillerCorsHeaders = (
615620
request: HttpServerRequest.HttpServerRequest
616621
): Record<string, string> => {
617622
const origin = readHeader(request, "origin")
618623
if (origin === undefined || !isSkillerWebCorsOriginAllowed(origin, process.env)) {
619624
return {}
620625
}
626+
const privateNetworkHeaders = isPrivateNetworkCorsRequest(request)
627+
? { "access-control-allow-private-network": "true" }
628+
: {}
621629
return {
630+
...privateNetworkHeaders,
622631
"access-control-allow-credentials": "true",
623632
"access-control-allow-headers": readHeader(request, "access-control-request-headers") ??
624633
"content-type,trpc-accept,x-trpc-source",
625634
"access-control-allow-methods": "GET,POST,OPTIONS",
626635
"access-control-allow-origin": origin,
627636
"access-control-max-age": "600",
628637
"access-control-expose-headers": "content-type",
629-
vary: "origin"
638+
vary: "origin, access-control-request-private-network"
630639
}
631640
}
632641

@@ -657,7 +666,9 @@ const skillerErrorResponse = (
657666

658667
const isSkillerCorsPath = (pathname: string): boolean => {
659668
const normalized = pathname.startsWith("/api/") ? pathname.slice("/api".length) : pathname
660-
return normalized === "/skiller/connect" || parseSkillerRoute(pathname) !== null
669+
return normalized === "/skiller/connect" ||
670+
/^\/projects\/by-key\/[^/]+(?:\/terminal-sessions\/[^/]+)?\/skiller\/context$/u.test(normalized) ||
671+
parseSkillerRoute(pathname) !== null
661672
}
662673

663674
const skillerCorsPreflightResponse = (
@@ -987,19 +998,29 @@ export const makeRouter = () => {
987998
),
988999
HttpRouter.get(
9891000
"/projects/by-key/:projectKey/skiller/context",
990-
projectKeyParams.pipe(
991-
Effect.flatMap(({ projectKey }) => readSkillerProjectContext(projectKey, null)),
992-
Effect.flatMap((context) => jsonResponse({ ok: true, ...context }, 200)),
993-
Effect.catchAll(errorResponse)
994-
)
1001+
Effect.gen(function*(_) {
1002+
const request = yield* _(HttpServerRequest.HttpServerRequest)
1003+
return yield* _(
1004+
projectKeyParams.pipe(
1005+
Effect.flatMap(({ projectKey }) => readSkillerProjectContext(projectKey, null)),
1006+
Effect.flatMap((context) => skillerJsonResponse(request, { ok: true, ...context }, 200)),
1007+
Effect.catchAll((error) => skillerErrorResponse(request, error))
1008+
)
1009+
)
1010+
})
9951011
),
9961012
HttpRouter.get(
9971013
"/projects/by-key/:projectKey/terminal-sessions/:sessionId/skiller/context",
998-
terminalSessionByProjectKeyParams.pipe(
999-
Effect.flatMap(({ projectKey, sessionId }) => readSkillerProjectContext(projectKey, sessionId)),
1000-
Effect.flatMap((context) => jsonResponse({ ok: true, ...context }, 200)),
1001-
Effect.catchAll(errorResponse)
1002-
)
1014+
Effect.gen(function*(_) {
1015+
const request = yield* _(HttpServerRequest.HttpServerRequest)
1016+
return yield* _(
1017+
terminalSessionByProjectKeyParams.pipe(
1018+
Effect.flatMap(({ projectKey, sessionId }) => readSkillerProjectContext(projectKey, sessionId)),
1019+
Effect.flatMap((context) => skillerJsonResponse(request, { ok: true, ...context }, 200)),
1020+
Effect.catchAll((error) => skillerErrorResponse(request, error))
1021+
)
1022+
)
1023+
})
10031024
),
10041025
HttpRouter.get(
10051026
"/cloudflare-tunnels/panel",

packages/api/src/services/skiller-core.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ const configuredOrigin = (raw: string): string | null => {
6565
const uniqueOrigins = (origins: ReadonlyArray<string>): ReadonlyArray<string> =>
6666
[...new Set(origins)]
6767

68+
const defaultSkillerWebCorsOrigins = [
69+
"https://skiller-web-henna.vercel.app",
70+
"http://localhost:5180",
71+
"http://127.0.0.1:5180"
72+
] as const
73+
6874
export const resolveConfiguredSkillerWebUrl = (
6975
env: Record<string, string | undefined>
7076
): ConfiguredSkillerWebUrl => {
@@ -111,8 +117,7 @@ export const configuredSkillerWebCorsOrigins = (
111117
return uniqueOrigins([
112118
...fromWeb,
113119
...fromAllowed,
114-
"http://localhost:5180",
115-
"http://127.0.0.1:5180"
120+
...defaultSkillerWebCorsOrigins
116121
])
117122
}
118123

packages/api/tests/skiller-core.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ describe("skiller container filesystem mapping", () => {
6565
expect(configuredSkillerWebCorsOrigins(env)).toEqual([
6666
"https://skiller.example",
6767
"https://preview.example",
68+
"https://skiller-web-henna.vercel.app",
6869
"http://localhost:5180",
6970
"http://127.0.0.1:5180"
7071
])
7172
expect(isSkillerWebCorsOriginAllowed("https://skiller.example", env)).toBe(true)
7273
expect(isSkillerWebCorsOriginAllowed("https://preview.example", env)).toBe(true)
74+
expect(isSkillerWebCorsOriginAllowed("https://skiller-web-henna.vercel.app", env)).toBe(true)
7375
expect(isSkillerWebCorsOriginAllowed("https://preview.example.evil", env)).toBe(false)
7476
expect(isSkillerWebCorsOriginAllowed(undefined, env)).toBe(false)
7577
})
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as HttpApp from "@effect/platform/HttpApp"
2+
import * as HttpRouter from "@effect/platform/HttpRouter"
3+
import { NodeContext } from "@effect/platform-node"
4+
import { describe, expect, it } from "@effect/vitest"
5+
import { Effect } from "effect"
6+
7+
import { makeRouter } from "../src/http.js"
8+
9+
const SKILLER_WEB_PRODUCTION_ORIGIN = "https://skiller-web-henna.vercel.app"
10+
11+
const apiHandler = HttpApp.toWebHandler(
12+
Effect.provide(Effect.flatten(HttpRouter.toHttpApp(makeRouter())), NodeContext.layer)
13+
)
14+
15+
const requestApiRoute = (path: string, init: RequestInit) =>
16+
Effect.tryPromise({
17+
try: () => apiHandler(new Request(`http://127.0.0.1${path}`, init)),
18+
catch: (cause) => new Error(String(cause))
19+
})
20+
21+
const skillerPrivateNetworkPreflight = (path: string) =>
22+
requestApiRoute(path, {
23+
method: "OPTIONS",
24+
headers: {
25+
"access-control-request-method": "POST",
26+
"access-control-request-private-network": "true",
27+
origin: SKILLER_WEB_PRODUCTION_ORIGIN
28+
}
29+
})
30+
31+
describe("skiller web CORS", () => {
32+
it("allows production Skiller Web private-network preflight requests", () =>
33+
Effect.runPromise(
34+
Effect.gen(function*(_) {
35+
const paths = [
36+
"/skiller/connect",
37+
"/api/skiller/connect",
38+
"/skiller/trpc/list_projects",
39+
"/skiller/events",
40+
"/projects/by-key/project-proof/skiller/context",
41+
"/projects/by-key/project-proof/terminal-sessions/session-proof/skiller/context"
42+
] as const
43+
44+
for (const path of paths) {
45+
const response = yield* _(skillerPrivateNetworkPreflight(path))
46+
47+
expect(response.status).toBe(204)
48+
expect(response.headers.get("access-control-allow-origin")).toBe(SKILLER_WEB_PRODUCTION_ORIGIN)
49+
expect(response.headers.get("access-control-allow-private-network")).toBe("true")
50+
expect(response.headers.get("vary")).toContain("access-control-request-private-network")
51+
}
52+
})
53+
))
54+
55+
it("rejects private-network preflight requests from unknown origins", () =>
56+
Effect.runPromise(
57+
Effect.gen(function*(_) {
58+
const response = yield* _(requestApiRoute("/skiller/connect", {
59+
method: "OPTIONS",
60+
headers: {
61+
"access-control-request-method": "POST",
62+
"access-control-request-private-network": "true",
63+
origin: "https://skiller-web-henna.vercel.app.evil.example"
64+
}
65+
}))
66+
67+
expect(response.status).toBe(403)
68+
expect(response.headers.get("access-control-allow-private-network")).toBeNull()
69+
expect(response.headers.get("access-control-allow-origin")).toBeNull()
70+
})
71+
))
72+
})

0 commit comments

Comments
 (0)