Skip to content

Commit 3b1bcd8

Browse files
committed
Merge remote-tracking branch 'origin/main' into issue-252
2 parents aeadaf4 + 915b00a commit 3b1bcd8

29 files changed

Lines changed: 1316 additions & 423 deletions

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ jobs:
411411
done <<< "$RELEASE_PACKAGE_PATHS_TO_RELEASE"
412412
- name: Create GitHub Release
413413
if: steps.compare_npm.outputs.should_release != 'false' && github.actor != 'github-actions[bot]'
414-
uses: softprops/action-gh-release@v2
414+
uses: softprops/action-gh-release@v3
415415
with:
416416
tag_name: ${{ steps.release_version.outputs.tag }}
417417
generate_release_notes: true

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM ubuntu:24.04
1+
FROM ubuntu:26.04
22

33
ENV DEBIAN_FRONTEND=noninteractive
44
ENV BUN_INSTALL=/opt/bun

bun.lock

Lines changed: 688 additions & 218 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
35.5 KB
Loading

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
"start": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js \"$@\"' --"
4747
},
4848
"devDependencies": {
49-
"@changesets/changelog-github": "^0.6.0",
50-
"@changesets/cli": "^2.30.0",
49+
"@changesets/changelog-github": "^0.7.0",
50+
"@changesets/cli": "^2.31.0",
5151
"@prover-coder-ai/dist-deps-prune": "^1.0.17"
5252
},
5353
"trustedDependencies": [

packages/api/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM ubuntu:24.04
1+
FROM ubuntu:26.04
22

33
ARG DOCKER_GIT_CONTROLLER_REV=unknown
44
ARG UBUNTU_APT_MIRROR=

packages/api/package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
},
2121
"dependencies": {
2222
"@effect-template/lib": "workspace:*",
23-
"@effect/platform": "^0.96.0",
23+
"@effect/platform": "^0.96.1",
2424
"@effect/platform-node": "^0.106.0",
2525
"@effect/schema": "^0.75.5",
26-
"effect": "^3.21.0",
27-
"node-pty": "^1.0.0",
28-
"ws": "^8.18.3"
26+
"effect": "^3.21.2",
27+
"node-pty": "^1.1.0",
28+
"ws": "^8.20.0"
2929
},
3030
"repository": {
3131
"type": "git",
@@ -38,13 +38,13 @@
3838
"devDependencies": {
3939
"@effect/vitest": "^0.29.0",
4040
"@eslint/js": "10.0.1",
41-
"@types/node": "^24.12.0",
41+
"@types/node": "^25.6.2",
4242
"@types/ws": "^8.18.1",
43-
"@typescript-eslint/eslint-plugin": "^8.57.1",
44-
"@typescript-eslint/parser": "^8.57.1",
45-
"eslint": "^10.1.0",
46-
"globals": "^17.4.0",
47-
"typescript": "^5.9.3",
48-
"vitest": "^4.1.0"
43+
"@typescript-eslint/eslint-plugin": "^8.59.2",
44+
"@typescript-eslint/parser": "^8.59.2",
45+
"eslint": "^10.3.0",
46+
"globals": "^17.6.0",
47+
"typescript": "^6.0.3",
48+
"vitest": "^4.1.5"
4949
}
5050
}

packages/api/src/services/terminal-image-fetch-core.ts

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { fileURLToPath } from "node:url"
2+
13
export type TerminalImageFetchPlan =
24
| {
35
readonly _tag: "InvalidTerminalImageFetch"
@@ -23,6 +25,21 @@ const controlCharRange = `${String.fromCodePoint(0)}-${String.fromCodePoint(0x1F
2325
const deleteChar = String.fromCodePoint(0x7F)
2426
const invalidCharacterPattern = new RegExp(String.raw`[\s${controlCharRange}${deleteChar}]`, "u")
2527
const traversalPattern = /(?:^|\/)(?:\.|\.\.)(?=\/|$)/u
28+
const urlSchemePattern = /^[A-Za-z][A-Za-z0-9+.-]*:/u
29+
const fileUrlPattern = /^file:\/\//iu
30+
const encodedPathSeparatorPattern = /%(?:2f|5c)/iu
31+
const fileUrlBackslashPattern = /\\/u
32+
const fileUrlTraversalPattern = /(?:^|[\\/])(?:\.|%2e)(?:(?:\.|%2e))?(?=[\\/]|$)/iu
33+
34+
type TerminalImagePathNormalization =
35+
| {
36+
readonly _tag: "InvalidTerminalImagePath"
37+
readonly message: string
38+
}
39+
| {
40+
readonly _tag: "ValidTerminalImagePath"
41+
readonly path: string
42+
}
2643

2744
const lowercaseExtension = (path: string): string | null => {
2845
const lastDot = path.lastIndexOf(".")
@@ -32,26 +49,85 @@ const lowercaseExtension = (path: string): string | null => {
3249
return path.slice(lastDot + 1).toLowerCase()
3350
}
3451

52+
const rawFileUrlPathname = (path: string): string => {
53+
const withoutScheme = path.slice("file://".length)
54+
const pathStart = withoutScheme.indexOf("/")
55+
if (pathStart < 0) {
56+
return ""
57+
}
58+
const pathAndSuffix = withoutScheme.slice(pathStart)
59+
const queryStart = pathAndSuffix.indexOf("?")
60+
const hashStart = pathAndSuffix.indexOf("#")
61+
if (queryStart < 0 && hashStart < 0) {
62+
return pathAndSuffix
63+
}
64+
if (queryStart < 0) {
65+
return pathAndSuffix.slice(0, hashStart)
66+
}
67+
if (hashStart < 0) {
68+
return pathAndSuffix.slice(0, queryStart)
69+
}
70+
return pathAndSuffix.slice(0, Math.min(queryStart, hashStart))
71+
}
72+
73+
const normalizeTerminalImagePath = (path: string): TerminalImagePathNormalization => {
74+
if (!urlSchemePattern.test(path)) {
75+
return { _tag: "ValidTerminalImagePath", path }
76+
}
77+
if (!fileUrlPattern.test(path)) {
78+
return { _tag: "InvalidTerminalImagePath", message: "Only file:// image URLs are supported." }
79+
}
80+
81+
const rawPathname = rawFileUrlPathname(path)
82+
if (fileUrlTraversalPattern.test(rawPathname)) {
83+
return { _tag: "InvalidTerminalImagePath", message: "Image path must not contain '.' or '..' segments." }
84+
}
85+
if (encodedPathSeparatorPattern.test(rawPathname) || fileUrlBackslashPattern.test(rawPathname)) {
86+
return {
87+
_tag: "InvalidTerminalImagePath",
88+
message: "Image file URL must not contain encoded or backslash path separators."
89+
}
90+
}
91+
92+
try {
93+
const url = new URL(path)
94+
if (url.protocol !== "file:" || (url.hostname !== "" && url.hostname !== "localhost")) {
95+
return { _tag: "InvalidTerminalImagePath", message: "Image file URL must point to a local path." }
96+
}
97+
if (url.search.length > 0 || url.hash.length > 0) {
98+
return { _tag: "InvalidTerminalImagePath", message: "Image file URL must not include query or fragment." }
99+
}
100+
return { _tag: "ValidTerminalImagePath", path: fileURLToPath(url, { windows: false }) }
101+
} catch {
102+
return { _tag: "InvalidTerminalImagePath", message: "Image file URL is invalid." }
103+
}
104+
}
105+
35106
export const planTerminalImageFetch = (path: string): TerminalImageFetchPlan => {
36107
if (typeof path !== "string" || path.length === 0) {
37108
return { _tag: "InvalidTerminalImageFetch", message: "Image path is required." }
38109
}
39-
if (!path.startsWith("/")) {
110+
const normalized = normalizeTerminalImagePath(path)
111+
if (normalized._tag === "InvalidTerminalImagePath") {
112+
return { _tag: "InvalidTerminalImageFetch", message: normalized.message }
113+
}
114+
const containerPath = normalized.path
115+
if (!containerPath.startsWith("/")) {
40116
return { _tag: "InvalidTerminalImageFetch", message: "Image path must be absolute." }
41117
}
42-
if (invalidCharacterPattern.test(path)) {
118+
if (invalidCharacterPattern.test(containerPath)) {
43119
return { _tag: "InvalidTerminalImageFetch", message: "Image path contains invalid characters." }
44120
}
45-
if (traversalPattern.test(path)) {
121+
if (traversalPattern.test(containerPath)) {
46122
return { _tag: "InvalidTerminalImageFetch", message: "Image path must not contain '.' or '..' segments." }
47123
}
48-
const extension = lowercaseExtension(path)
124+
const extension = lowercaseExtension(containerPath)
49125
if (extension === null) {
50126
return { _tag: "InvalidTerminalImageFetch", message: "Image path must include a file extension." }
51127
}
52128
const mediaType = supportedExtensionMediaTypes.get(extension)
53129
if (mediaType === undefined) {
54130
return { _tag: "InvalidTerminalImageFetch", message: `Unsupported image extension: .${extension}` }
55131
}
56-
return { _tag: "ValidTerminalImageFetch", containerPath: path, mediaType }
132+
return { _tag: "ValidTerminalImageFetch", containerPath, mediaType }
57133
}

packages/api/tests/terminal-image-fetch-core.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ import { describe, expect, it } from "@effect/vitest"
33
import { planTerminalImageFetch } from "../src/services/terminal-image-fetch-core.js"
44

55
describe("terminal image fetch core", () => {
6-
it("accepts an absolute path with a supported image extension", () => {
6+
it("continues to accept an absolute path with a supported image extension", () => {
77
expect(planTerminalImageFetch("/tmp/issue232-main.png")).toEqual({
88
_tag: "ValidTerminalImageFetch",
99
containerPath: "/tmp/issue232-main.png",
1010
mediaType: "image/png"
1111
})
1212
})
1313

14+
it("accepts a file URL and normalizes it to an absolute container path", () => {
15+
expect(planTerminalImageFetch("file:///tmp/phantom-e2e.tuhl98/wallet-step-after-password.png")).toEqual({
16+
_tag: "ValidTerminalImageFetch",
17+
containerPath: "/tmp/phantom-e2e.tuhl98/wallet-step-after-password.png",
18+
mediaType: "image/png"
19+
})
20+
})
21+
1422
it("maps each supported extension to its media type", () => {
1523
expect(planTerminalImageFetch("/a.jpg")).toMatchObject({ mediaType: "image/jpeg" })
1624
expect(planTerminalImageFetch("/a.jpeg")).toMatchObject({ mediaType: "image/jpeg" })
@@ -33,6 +41,13 @@ describe("terminal image fetch core", () => {
3341
})
3442
})
3543

44+
it("rejects non-file URLs", () => {
45+
expect(planTerminalImageFetch("https://example.com/tmp/photo.png")).toEqual({
46+
_tag: "InvalidTerminalImageFetch",
47+
message: "Only file:// image URLs are supported."
48+
})
49+
})
50+
3651
it("rejects whitespace and control characters", () => {
3752
expect(planTerminalImageFetch("/tmp/has space.png")).toMatchObject({
3853
_tag: "InvalidTerminalImageFetch"
@@ -51,6 +66,32 @@ describe("terminal image fetch core", () => {
5166
})
5267
})
5368

69+
it("rejects traversal segments in file URLs before URL normalization", () => {
70+
expect(planTerminalImageFetch("file:///tmp/../etc/photo.png")).toMatchObject({
71+
_tag: "InvalidTerminalImageFetch",
72+
message: "Image path must not contain '.' or '..' segments."
73+
})
74+
expect(planTerminalImageFetch("file:///tmp/%2E%2E/etc/photo.png")).toMatchObject({
75+
_tag: "InvalidTerminalImageFetch",
76+
message: "Image path must not contain '.' or '..' segments."
77+
})
78+
})
79+
80+
it("rejects unsafe file URL forms", () => {
81+
expect(planTerminalImageFetch("file://example.com/tmp/photo.png")).toMatchObject({
82+
_tag: "InvalidTerminalImageFetch",
83+
message: "Image file URL must point to a local path."
84+
})
85+
expect(planTerminalImageFetch("file:///tmp/photo.png?download=1")).toMatchObject({
86+
_tag: "InvalidTerminalImageFetch",
87+
message: "Image file URL must not include query or fragment."
88+
})
89+
expect(planTerminalImageFetch("file:///tmp/%2Fetc/photo.png")).toMatchObject({
90+
_tag: "InvalidTerminalImageFetch",
91+
message: "Image file URL must not contain encoded or backslash path separators."
92+
})
93+
})
94+
5495
it("rejects unsupported extensions", () => {
5596
expect(planTerminalImageFetch("/tmp/file.bmp")).toMatchObject({
5697
_tag: "InvalidTerminalImageFetch"

packages/app/package.json

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -62,64 +62,64 @@
6262
"packageManager": "bun@1.3.11",
6363
"dependencies": {
6464
"@prover-coder-ai/docker-git-session-sync": "workspace:*",
65-
"@effect/cli": "^0.75.0",
66-
"@effect/cluster": "^0.58.0",
65+
"@effect/cli": "^0.75.1",
66+
"@effect/cluster": "^0.58.2",
6767
"@effect/experimental": "^0.60.0",
68-
"@effect/platform": "^0.96.0",
68+
"@effect/platform": "^0.96.1",
6969
"@effect/platform-node": "^0.106.0",
7070
"@effect/printer": "^0.49.0",
7171
"@effect/printer-ansi": "^0.49.0",
72-
"@effect/rpc": "^0.75.0",
72+
"@effect/rpc": "^0.75.1",
7373
"@effect/schema": "^0.75.5",
74-
"@effect/sql": "^0.51.0",
74+
"@effect/sql": "^0.51.1",
7575
"@effect/typeclass": "^0.40.0",
76-
"@effect/workflow": "^0.18.0",
77-
"@gridland/bun": "0.2.53",
78-
"@gridland/web": "0.2.53",
79-
"effect": "^3.21.0",
80-
"react": "^19.2.4",
81-
"react-dom": "^19.2.4",
76+
"@effect/workflow": "^0.18.1",
77+
"@gridland/bun": "0.4.3",
78+
"@gridland/web": "0.4.3",
79+
"effect": "^3.21.2",
80+
"react": "^19.2.6",
81+
"react-dom": "^19.2.6",
8282
"react-reconciler": "^0.33.0",
83-
"ts-morph": "^27.0.2",
83+
"ts-morph": "^28.0.0",
8484
"xterm": "^5.3.0",
8585
"xterm-addon-fit": "^0.8.0"
8686
},
8787
"devDependencies": {
88-
"@biomejs/biome": "^2.4.8",
88+
"@biomejs/biome": "^2.4.14",
8989
"@effect-template/lib": "workspace:*",
9090
"@effect/eslint-plugin": "^0.3.2",
9191
"@effect/language-service": "latest",
9292
"@effect/vitest": "^0.29.0",
9393
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.1",
94-
"@eslint/compat": "2.0.3",
94+
"@eslint/compat": "2.1.0",
9595
"@eslint/eslintrc": "3.3.5",
9696
"@eslint/js": "10.0.1",
97-
"@prover-coder-ai/eslint-plugin-suggest-members": "^0.0.25",
97+
"@prover-coder-ai/eslint-plugin-suggest-members": "^0.0.26",
9898
"@ton-ai-core/vibecode-linter": "^1.0.11",
99-
"@types/node": "^24.12.0",
99+
"@types/node": "^25.6.2",
100100
"@types/react": "^19.2.14",
101101
"@types/react-dom": "^19.2.3",
102102
"@types/ws": "^8.18.1",
103-
"@typescript-eslint/eslint-plugin": "^8.57.1",
104-
"@typescript-eslint/parser": "^8.57.1",
103+
"@typescript-eslint/eslint-plugin": "^8.59.2",
104+
"@typescript-eslint/parser": "^8.59.2",
105105
"@vitejs/plugin-react": "^6.0.1",
106-
"@vitest/coverage-v8": "^4.1.0",
107-
"@vitest/eslint-plugin": "^1.6.13",
108-
"biome": "npm:@biomejs/biome@^2.4.8",
109-
"eslint": "^10.1.0",
106+
"@vitest/coverage-v8": "^4.1.5",
107+
"@vitest/eslint-plugin": "^1.6.17",
108+
"biome": "npm:@biomejs/biome@^2.4.14",
109+
"eslint": "^10.3.0",
110110
"eslint-import-resolver-typescript": "^4.4.4",
111111
"eslint-plugin-codegen": "0.34.1",
112112
"eslint-plugin-import": "^2.32.0",
113-
"eslint-plugin-simple-import-sort": "^12.1.1",
114-
"eslint-plugin-sonarjs": "^4.0.2",
113+
"eslint-plugin-simple-import-sort": "^13.0.0",
114+
"eslint-plugin-sonarjs": "^4.0.3",
115115
"eslint-plugin-sort-destructure-keys": "^3.0.0",
116-
"eslint-plugin-unicorn": "^63.0.0",
117-
"globals": "^17.4.0",
118-
"jscpd": "^4.0.8",
119-
"typescript": "^5.9.3",
120-
"typescript-eslint": "^8.57.1",
121-
"vite": "^8.0.1",
122-
"vitest": "^4.1.0",
116+
"eslint-plugin-unicorn": "^64.0.0",
117+
"globals": "^17.6.0",
118+
"jscpd": "^4.0.9",
119+
"typescript": "^6.0.3",
120+
"typescript-eslint": "^8.59.2",
121+
"vite": "^8.0.11",
122+
"vitest": "^4.1.5",
123123
"ws": "^8.20.0"
124124
}
125125
}

0 commit comments

Comments
 (0)