diff --git a/bun.lock b/bun.lock index c52d9e6b176..3ea6f31f5dd 100644 --- a/bun.lock +++ b/bun.lock @@ -960,6 +960,20 @@ "solid-js": "^1.9.0", }, }, + "packages/updates": { + "name": "@opencode-ai/updates", + "version": "1.18.4", + "dependencies": { + "jose": "6.0.11", + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:", + "@tsconfig/node22": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + "wrangler": "4.110.0", + }, + }, "packages/util": { "name": "@opencode-ai/util", "version": "1.18.3", @@ -2119,6 +2133,8 @@ "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], + "@opencode-ai/updates": ["@opencode-ai/updates@workspace:packages/updates"], + "@opencode-ai/util": ["@opencode-ai/util@workspace:packages/util"], "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"], @@ -6587,6 +6603,8 @@ "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], + "@opencode-ai/updates/wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="], + "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], "@opencode-ai/www/@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.44.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "4.20260708.1", "unenv": "2.0.0-rc.24", "wrangler": "4.110.0", "ws": "8.21.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" }, "bin": { "cf-vite": "bin/cf-vite" } }, "sha512-8wGGunqRcs34o4GRq0Rurp7GZg30xtLJeRGUU81a49r9zQRjlp3xIlsWr3nFlSCso4eE3cjZfiKC/2y116M4TQ=="], @@ -7857,6 +7875,16 @@ "@opencode-ai/desktop/@actions/artifact/@actions/http-client": ["@actions/http-client@2.2.3", "", { "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA=="], + "@opencode-ai/updates/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + + "@opencode-ai/updates/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], + + "@opencode-ai/updates/wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "@opencode-ai/updates/wrangler/miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="], + + "@opencode-ai/updates/wrangler/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], + "@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="], "@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="], @@ -8705,6 +8733,72 @@ "@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@opencode-ai/updates/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@opencode-ai/updates/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "@opencode-ai/updates/wrangler/miniflare/undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + + "@opencode-ai/updates/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="], + + "@opencode-ai/updates/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="], + + "@opencode-ai/updates/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="], + + "@opencode-ai/updates/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="], + + "@opencode-ai/updates/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], + "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], @@ -8989,6 +9083,44 @@ "@opencode-ai/desktop/@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="], "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="], @@ -9139,6 +9271,8 @@ "@mintlify/common/sucrase/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@opencode-ai/updates/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + "@opencode-ai/www/@cloudflare/vite-plugin/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], "@opencode-ai/www/wrangler/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], diff --git a/packages/cli/script/publish.ts b/packages/cli/script/publish.ts index fd0f5cdb181..7312483dc9a 100755 --- a/packages/cli/script/publish.ts +++ b/packages/cli/script/publish.ts @@ -3,6 +3,7 @@ import { $ } from "bun" import pkg from "../package.json" import { Script } from "@opencode-ai/script" import { fileURLToPath } from "url" +import { UpdateArtifact } from "../../../script/update-artifact" const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) @@ -81,3 +82,10 @@ await publishDistribution({ binary: "opencode2-node", packagePrefix: "@opencode-ai/cli-node-", }) +await UpdateArtifact.publish({ + channel: Script.channel, + name: "cli", + distribution: "npm", + version: Script.version, + metadata: {}, +}) diff --git a/packages/updates/README.md b/packages/updates/README.md new file mode 100644 index 00000000000..05366d56e31 --- /dev/null +++ b/packages/updates/README.md @@ -0,0 +1,24 @@ +# OpenCode Updates + +The updates Worker serves all selected artifacts for a channel. + +```sh +curl 'https://update.opencode.ai/api/latest' +``` + +The `/admin*` route must be protected by a Cloudflare Access self-hosted application. Configure the application with: + +- Public hostname: `update.opencode.ai` +- Path: `admin*` +- Policy: allow the OpenCode team identity group + +The Worker has `workers_dev` and preview URLs disabled so the custom hostname is its only public entry point. + +GitHub Actions publishes artifacts through `POST /api/publish` using a short-lived OIDC token with audience `https://update.opencode.ai`. The Worker accepts only tokens signed by GitHub for repository ID `975734319`, owner ID `66570915`, and `.github/workflows/publish.yml` on configured publishing refs. + +Apply migrations and deploy from this directory: + +```sh +bun run db:migrate +bun run deploy +``` diff --git a/packages/updates/migrations/0001_artifact.sql b/packages/updates/migrations/0001_artifact.sql new file mode 100644 index 00000000000..a501c8583e4 --- /dev/null +++ b/packages/updates/migrations/0001_artifact.sql @@ -0,0 +1,14 @@ +CREATE TABLE artifact ( + channel TEXT NOT NULL, + name TEXT NOT NULL, + distribution TEXT NOT NULL, + version TEXT NOT NULL, + metadata TEXT NOT NULL, + active INTEGER NOT NULL DEFAULT 0 CHECK (active IN (0, 1)), + time_updated INTEGER NOT NULL, + PRIMARY KEY (channel, name, distribution, version) +) WITHOUT ROWID; + +CREATE UNIQUE INDEX artifact_active +ON artifact (channel, name, distribution) +WHERE active = 1; diff --git a/packages/updates/package.json b/packages/updates/package.json new file mode 100644 index 00000000000..5225434eb72 --- /dev/null +++ b/packages/updates/package.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@opencode-ai/updates", + "version": "1.18.4", + "private": true, + "type": "module", + "scripts": { + "deploy": "wrangler deploy", + "dev": "wrangler dev", + "db:migrate": "wrangler d1 migrations apply opencode-updates --remote", + "test": "bun test", + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "jose": "6.0.11" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:", + "@tsconfig/node22": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + "wrangler": "4.110.0" + } +} diff --git a/packages/updates/src/index.test.ts b/packages/updates/src/index.test.ts new file mode 100644 index 00000000000..029785c9274 --- /dev/null +++ b/packages/updates/src/index.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test" +import { channelsForRef, validGitHubClaims } from "./index" + +const claims = { + repository: "anomalyco/opencode", + repository_id: "975734319", + repository_owner_id: "66570915", + workflow_ref: "anomalyco/opencode/.github/workflows/publish.yml@refs/heads/dev", + ref: "refs/heads/dev", + sha: "abc123", + run_id: "123", + run_attempt: "1", + actor: "opencode-agent", +} + +describe("GitHub publish authorization", () => { + test("allows the publish workflow from the repository", () => { + expect(validGitHubClaims(claims)).toBe(true) + expect(channelsForRef(claims.ref)).toEqual(["dev", "latest"]) + }) + + test("rejects another repository or workflow", () => { + expect(validGitHubClaims({ ...claims, repository_id: "1" })).toBe(false) + expect(validGitHubClaims({ ...claims, workflow_ref: "anomalyco/opencode/.github/workflows/other.yml@refs/heads/dev" })).toBe(false) + }) + + test("rejects unconfigured refs", () => { + const ref = "refs/heads/untrusted" + expect(validGitHubClaims({ ...claims, ref, workflow_ref: `anomalyco/opencode/.github/workflows/publish.yml@${ref}` })).toBe(false) + }) +}) diff --git a/packages/updates/src/index.ts b/packages/updates/src/index.ts new file mode 100644 index 00000000000..796837b143d --- /dev/null +++ b/packages/updates/src/index.ts @@ -0,0 +1,369 @@ +import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose" + +interface Env { + DB: D1Database +} + +type ArtifactRow = { + channel: string + name: string + distribution: string + version: string + metadata: string + active: number + time_updated: number +} + +type Artifact = Omit & { + metadata: unknown + active: boolean +} + +type ArtifactInput = Pick & { + metadata: unknown +} + +const identifier = /^[a-zA-Z0-9._-]{1,64}$/ +const version = /^[a-zA-Z0-9.+_-]{1,128}$/ +const select = "SELECT channel, name, distribution, version, metadata, active, time_updated FROM artifact" +const audience = "https://update.opencode.ai" +const githubKeys = createRemoteJWKSet(new URL("https://token.actions.githubusercontent.com/.well-known/jwks")) + +export default { + async fetch(request, env): Promise { + const url = new URL(request.url) + + if (url.pathname === "/") return json({ service: "opencode-updates" }) + if (url.pathname === "/admin" && request.method === "GET") return admin(request, env) + if (url.pathname === "/admin/artifact" && request.method === "POST") return registerArtifact(request, env) + if (url.pathname === "/admin/activate" && request.method === "POST") return activateArtifact(request, env) + if (url.pathname === "/api/publish" && request.method === "POST") return publishArtifact(request, env) + if (request.method !== "GET") return new Response("Method not allowed", { status: 405 }) + + const segments = url.pathname.split("/").filter(Boolean) + if (segments.length === 2 && segments[0] === "api" && validIdentifier(segments[1])) { + return channel(env.DB, segments[1]) + } + return new Response("Not found", { status: 404 }) + }, +} satisfies ExportedHandler + +async function channel(db: D1Database, channel: string) { + const result = await db + .prepare(`${select} WHERE channel = ? AND active = 1 ORDER BY name, distribution`) + .bind(channel) + .all() + if (!result.results.length) return json({ error: "Channel not found" }, 404) + return cached({ channel, artifacts: result.results.map(decodeArtifact) }) +} + +async function admin(request: Request, env: Env) { + const result = await env.DB.prepare(`${select} ORDER BY channel, name, distribution, active DESC, time_updated DESC`).all() + const rows = result.results + .map( + (artifact) => ` + ${escape(artifact.channel)} + ${escape(artifact.name)} + ${escape(artifact.distribution)} + ${escape(artifact.version)} + ${artifact.active ? 'Active' : 'History'} + ${new Date(artifact.time_updated).toISOString()} + + ${ + artifact.active + ? "" + : `
+ + + + + +
` + } + + `, + ) + .join("") + + return new Response( + ` + + + + + OpenCode Updates + + + + +
+
+

Release control

Artifacts

+ ${escape(request.headers.get("Cf-Access-Authenticated-User-Email") ?? "Cloudflare Access pending")} +
+
+

Published artifacts

Activate any successfully published version without changing its metadata.

+
+ + + ${rows || ''} +
ChannelNameDistributionVersionStatusTime updated
No artifacts published yet.
+
+
+
+

Publish artifact

Publishing stores the metadata and activates this version for its distribution.

+
+
+ ${field("Channel", '')} + ${field("Name", '')} + ${field("Distribution", '')} + ${field("Version", '')} + +
+
+
+
+
+ +`, + { headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" } }, + ) +} + +async function registerArtifact(request: Request, env: Env) { + const invalid = validMutation(request) + if (invalid) return invalid + const form = await request.formData() + const artifact = parseArtifact({ + channel: form.get("channel"), + name: form.get("name"), + distribution: form.get("distribution"), + version: form.get("version"), + metadata: form.get("metadata"), + }) + if (artifact instanceof Response) return artifact + await activate(env.DB, [artifact]) + return Response.redirect(new URL("/admin", request.url), 303) +} + +async function publishArtifact(request: Request, env: Env) { + const claims = await verifyGitHub(request) + if (claims instanceof Response) return claims + const input: unknown = await request.json().catch(() => undefined) + const artifact = parseArtifact(isRecord(input) ? input : {}) + if (artifact instanceof Response) return artifact + if (!channelsForRef(claims.ref).includes(artifact.channel)) return json({ error: "Channel is not allowed" }, 403) + if (!isRecord(artifact.metadata)) return json({ error: "Metadata must be an object" }, 400) + await activate(env.DB, [ + { + ...artifact, + metadata: { + ...artifact.metadata, + github: { + sha: claims.sha, + run_id: claims.run_id, + run_attempt: claims.run_attempt, + actor: claims.actor, + ref: claims.ref, + }, + }, + }, + ]) + return json({ published: true }) +} + +async function activateArtifact(request: Request, env: Env) { + const invalid = validMutation(request) + if (invalid) return invalid + const form = await request.formData() + const key = parseKey({ + channel: form.get("channel"), + name: form.get("name"), + distribution: form.get("distribution"), + version: form.get("version"), + }) + if (key instanceof Response) return key + const exists = await env.DB.prepare( + "SELECT 1 FROM artifact WHERE channel = ? AND name = ? AND distribution = ? AND version = ?", + ) + .bind(key.channel, key.name, key.distribution, key.version) + .first() + if (!exists) return json({ error: "Artifact not found" }, 404) + await env.DB.batch([ + deactivateStatement(env.DB, key), + env.DB + .prepare( + "UPDATE artifact SET active = 1, time_updated = ? WHERE channel = ? AND name = ? AND distribution = ? AND version = ?", + ) + .bind(Date.now(), key.channel, key.name, key.distribution, key.version), + ]) + return Response.redirect(new URL("/admin", request.url), 303) +} + +function activate(db: D1Database, artifacts: ArtifactInput[]) { + return db.batch( + artifacts.flatMap((artifact) => [ + deactivateStatement(db, artifact), + db + .prepare( + `INSERT INTO artifact (channel, name, distribution, version, metadata, active, time_updated) + VALUES (?, ?, ?, ?, ?, 1, ?) + ON CONFLICT (channel, name, distribution, version) DO UPDATE SET + metadata = excluded.metadata, active = 1, time_updated = excluded.time_updated`, + ) + .bind( + artifact.channel, + artifact.name, + artifact.distribution, + artifact.version, + JSON.stringify(artifact.metadata), + Date.now(), + ), + ]), + ) +} + +function deactivateStatement(db: D1Database, artifact: Pick) { + return db + .prepare("UPDATE artifact SET active = 0 WHERE channel = ? AND name = ? AND distribution = ? AND active = 1") + .bind(artifact.channel, artifact.name, artifact.distribution) +} + +function parseArtifact(input: Record): ArtifactInput | Response { + const key = parseKey(input) + if (key instanceof Response) return key + const metadata = typeof input.metadata === "string" ? parseMetadata(input.metadata) : input.metadata + if (metadata === undefined) return json({ error: "Metadata must be valid JSON" }, 400) + return { ...key, metadata } +} + +function parseKey(input: Record): Omit | Response { + if ( + !validIdentifier(input.channel) || + !validIdentifier(input.name) || + !validIdentifier(input.distribution) || + !validVersion(input.version) + ) + return json({ error: "Invalid artifact" }, 400) + return { + channel: input.channel, + name: input.name, + distribution: input.distribution, + version: input.version, + } +} + +function decodeArtifact(row: ArtifactRow): Artifact { + return { ...row, metadata: decodeMetadata(row.metadata), active: row.active === 1 } +} + +function decodeMetadata(input: string) { + return parseMetadata(input) ?? null +} + +function parseMetadata(input: string): unknown | undefined { + try { + return JSON.parse(input) + } catch { + return + } +} + +async function verifyGitHub(request: Request) { + const authorization = request.headers.get("Authorization") + if (!authorization?.startsWith("Bearer ")) return json({ error: "Unauthorized" }, 401) + const result = await jwtVerify(authorization.slice("Bearer ".length), githubKeys, { + issuer: "https://token.actions.githubusercontent.com", + audience, + }).catch(() => undefined) + if (!result || !validGitHubClaims(result.payload)) return json({ error: "Unauthorized" }, 401) + return result.payload +} + +type GitHubClaims = JWTPayload & { + repository: string + repository_id: string + repository_owner_id: string + workflow_ref: string + ref: string + sha: string + run_id: string + run_attempt: string + actor: string +} + +export function validGitHubClaims(claims: JWTPayload): claims is GitHubClaims { + if (claims.repository !== "anomalyco/opencode") return false + if (claims.repository_id !== "975734319") return false + if (claims.repository_owner_id !== "66570915") return false + if (typeof claims.workflow_ref !== "string" || typeof claims.ref !== "string") return false + if (claims.workflow_ref !== `anomalyco/opencode/.github/workflows/publish.yml@${claims.ref}`) return false + if (!channelsForRef(claims.ref).length) return false + return [claims.sha, claims.run_id, claims.run_attempt, claims.actor].every((value) => typeof value === "string") +} + +export function channelsForRef(ref: string) { + if (ref === "refs/heads/dev") return ["dev", "latest"] + if (ref === "refs/heads/v2") return ["next"] + if (ref === "refs/heads/beta") return ["beta"] + if (ref === "refs/heads/ci") return ["ci"] + if (ref === "refs/heads/fix/npm-native-binary-install") return ["fix/npm-native-binary-install"] + const snapshot = ref.match(/^refs\/heads\/(snapshot-[a-zA-Z0-9._-]+)$/)?.[1] + return snapshot ? [snapshot] : [] +} + +function validMutation(request: Request) { + const origin = request.headers.get("Origin") + if (origin && origin !== new URL(request.url).origin) return json({ error: "Invalid origin" }, 403) +} + +function validIdentifier(input: unknown): input is string { + return typeof input === "string" && identifier.test(input) +} + +function validVersion(input: unknown): input is string { + return typeof input === "string" && version.test(input) +} + +function isRecord(input: unknown): input is Record { + return typeof input === "object" && input !== null && !Array.isArray(input) +} + +function cached(value: unknown) { + return json(value, 200, { "Cache-Control": "public, max-age=60" }) +} + +function json(value: unknown, status = 200, headers?: HeadersInit) { + return Response.json(value, { status, headers }) +} + +function field(label: string, input: string) { + return `
${input}
` +} + +function escape(value: string) { + return value.replace(/[&<>"']/g, (character) => { + if (character === "&") return "&" + if (character === "<") return "<" + if (character === ">") return ">" + if (character === '"') return """ + return "'" + }) +} diff --git a/packages/updates/tsconfig.json b/packages/updates/tsconfig.json new file mode 100644 index 00000000000..8cdcc84d7f8 --- /dev/null +++ b/packages/updates/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "lib": ["ES2023", "WebWorker"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "types": ["@cloudflare/workers-types", "bun"] + }, + "include": ["src"] +} diff --git a/packages/updates/wrangler.jsonc b/packages/updates/wrangler.jsonc new file mode 100644 index 00000000000..988ea80282a --- /dev/null +++ b/packages/updates/wrangler.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "opencode-updates", + "main": "src/index.ts", + "compatibility_date": "2026-07-21", + "workers_dev": false, + "preview_urls": false, + "routes": [ + { + "pattern": "update.opencode.ai", + "custom_domain": true + } + ], + "d1_databases": [ + { + "binding": "DB", + "database_name": "opencode-updates", + "database_id": "058debd8-1572-4c4a-8781-2c5a0e33cb46", + "migrations_dir": "migrations" + } + ], + "observability": { + "enabled": true + } +} diff --git a/script/publish.ts b/script/publish.ts index cfb3aace137..3d74fd68121 100755 --- a/script/publish.ts +++ b/script/publish.ts @@ -3,6 +3,7 @@ import { Script } from "@opencode-ai/script" import { $ } from "bun" import { fileURLToPath } from "url" +import { UpdateArtifact } from "./update-artifact" console.log("=== publishing ===\n") @@ -82,4 +83,13 @@ if (Script.release && !Script.preview) { if (Script.release) { await $`gh release edit ${tag} --draft=false --repo ${process.env.GH_REPO}` + const repo = process.env.GH_REPO + if (!repo) throw new Error("GH_REPO is required") + await UpdateArtifact.publish({ + channel: Script.channel, + name: "desktop", + distribution: "github", + version: Script.version, + metadata: await UpdateArtifact.desktopMetadata(Script.version, repo), + }) } diff --git a/script/update-artifact.ts b/script/update-artifact.ts new file mode 100644 index 00000000000..89a50837eb4 --- /dev/null +++ b/script/update-artifact.ts @@ -0,0 +1,104 @@ +type Artifact = { + channel: string + name: string + distribution: string + version: string + metadata: Record +} + +type DesktopFile = { + url: string + sha512: string + size: number + blockMapSize?: number +} + +export namespace UpdateArtifact { + export async function publish(artifact: Artifact) { + if (process.env.GITHUB_ACTIONS !== "true") { + console.log("skipped update artifact publication outside GitHub Actions") + return + } + const requestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL + const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN + if (!requestURL || !requestToken) throw new Error("GitHub Actions OIDC is unavailable") + + const url = new URL(requestURL) + url.searchParams.set("audience", "https://update.opencode.ai") + const tokenResponse = await fetch(url, { headers: { Authorization: `Bearer ${requestToken}` } }) + if (!tokenResponse.ok) throw new Error(`Failed to request GitHub OIDC token: ${tokenResponse.status}`) + const token: unknown = await tokenResponse.json() + if (!isRecord(token) || typeof token.value !== "string") throw new Error("GitHub OIDC response did not include a token") + + const response = await fetch("https://update.opencode.ai/api/publish", { + method: "POST", + headers: { + Authorization: `Bearer ${token.value}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(artifact), + }) + if (response.ok) return + throw new Error(`Failed to publish update artifact: ${response.status} ${await response.text()}`) + } + + export async function desktopMetadata(version: string, repo: string) { + const directory = process.env.RUNNER_TEMP ?? "/tmp" + const entries = await Promise.all( + [ + ["desktop.yml", "latest.yml"], + ["desktop-mac.yml", "latest-mac.yml"], + ["desktop-linux.yml", "latest-linux.yml"], + ["desktop-linux-arm64.yml", "latest-linux-arm64.yml"], + ].map(async ([name, source]) => { + const file = Bun.file(`${directory}/${source}`) + if (!(await file.exists())) return + return [name, parseDesktop(await file.text(), version, repo)] as const + }), + ) + const manifests = Object.fromEntries(entries.filter((entry) => entry !== undefined)) + if (!Object.keys(manifests).length) throw new Error("No desktop update metadata found") + return { manifests } + } +} + +function parseDesktop(content: string, version: string, repo: string) { + const lines = content.split("\n") + const found = lines.find((line) => line.startsWith("version:"))?.slice("version:".length).trim() + if (found !== version) throw new Error(`Desktop metadata version mismatch: expected ${version}, got ${found}`) + const releaseDate = lines + .find((line) => line.startsWith("releaseDate:")) + ?.slice("releaseDate:".length) + .trim() + .replace(/^['"]|['"]$/g, "") + if (!releaseDate) throw new Error("Desktop metadata did not include a release date") + + const files: DesktopFile[] = [] + lines.forEach((line) => { + const value = line.trim() + if (value.startsWith("- url:")) { + const name = value.slice("- url:".length).trim() + files.push({ + url: name.startsWith("http") + ? name + : `https://github.com/${repo}/releases/download/v${version}/${encodeURIComponent(name)}`, + sha512: "", + size: 0, + }) + return + } + const current = files.at(-1) + if (!current) return + if (value.startsWith("sha512:")) current.sha512 = value.slice("sha512:".length).trim() + if (value.startsWith("size:")) current.size = Number(value.slice("size:".length).trim()) + if (value.startsWith("blockMapSize:")) current.blockMapSize = Number(value.slice("blockMapSize:".length).trim()) + }) + if (!files.length || files.some((file) => !file.sha512 || !file.size)) { + throw new Error("Desktop metadata contained an incomplete file") + } + return { files, releaseDate } +} + +function isRecord(input: unknown): input is Record { + return typeof input === "object" && input !== null && !Array.isArray(input) +}