Compare commits

...

10 Commits

Author SHA1 Message Date
Dean Sheather
279b5702aa Merge pull request #8070 from coder/upgrade-node-checksums
Fix Node 24.22.1 checksums
2026-10-24 04:50:00 +11:00
Dean Sheather
27baccf843 Fix Node 24.22.1 checksums 2026-10-23 17:35:56 +00:00
Dean Sheather
8aab1b0d72 Merge pull request #8068 from coder/upgrade-node
Upgrade Node to 24.22.1
2026-10-24 02:35:35 +11:00
Dean Sheather
777c493a9b Upgrade Node to 24.22.1 2026-10-23 15:06:41 +00:00
Harsh Bhanushali
f07405cba3 Expand ~ in path options from the config file (#8055)
The shell expands ~ in command-line arguments, but values read from
config.yaml were passed to path.resolve as-is, so
`user-data-dir: ~/.config/code-server-data` resolved to
`<cwd>/~/.config/code-server-data`.
2026-10-14 13:47:08 -08:00
dependabot[bot]
c5fee2d2f9 Bump brace-expansion from 1.1.18 to 1.1.21 (#8044) 2026-10-13 17:02:46 -08:00
Harsh Bhanushali
83670e50c6 Fix false booleans in config file being parsed as true (#8053)
Options like `enable-remote-storage: false` in config.yaml are converted to
`--enable-remote-storage=false`, but the parser set any boolean option to
true regardless of its value. Booleans set to false are now ignored, the
same way optional strings set to false already are.
2026-10-13 17:01:10 -08:00
KeuqS
41cf6f7eaf Add Turkish locale (#8050) 2026-10-09 22:50:17 +00:00
cdrci
78aad47ce6 Update Helm chart and changelog with 4.141.0 (#8045) 2026-10-08 02:35:52 -08:00
cdrci
8b41915d25 Update Code to 1.141.0 (#8040)
* Update Code to 1.141.0
* Allow pull_request_target
* Add e2e tests for remote storage
* Fix Typescript extension not loading
* Remove unused imports
2026-10-07 22:54:47 -08:00
32 changed files with 492 additions and 75 deletions

View File

@@ -79,6 +79,9 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
submodules: true
# When an update/ branch is merged, a release is automatically created
# which requires secrets.
allow-unsafe-pr-checkout: true
- run: quilt push -a
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with:
@@ -173,6 +176,9 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
submodules: true
# When an update/ branch is merged, a release is automatically created
# which requires secrets.
allow-unsafe-pr-checkout: true
- run: quilt push -a
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6
with:
@@ -233,6 +239,9 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
submodules: true
# When an update/ branch is merged, a release is automatically created
# which requires secrets.
allow-unsafe-pr-checkout: true
# quilt has no windows build. The patches are ordinary -p1 diffs
# against the repository root, so git applies them in series order.

View File

@@ -1 +1 @@
24.21.0
24.22.1

View File

@@ -22,6 +22,32 @@ Code v99.99.999
## Unreleased
### Security
- Update bundled Node.js to 24.22.1 to fix a remote code execution bug in the
Brotli decoder (CVE-2026-111497).
- Reject compressed request bodies (`Content-Encoding: br`, `gzip`, or
`deflate`) instead of decompressing them.
## [4.141.0](https://github.com/coder/code-server/releases/tag/v4.141.0) - 2026-10-08
Code v1.141.0
### Added
- A new `--enable-remote-storage` flag can be used to make code-server persist
workbench storage on the remote server instead of browser storage. Use this if
you want to share storage between multiple devices.
### Fixed
- The Typescript extension was trying to load as a browser extension even though
we build the remote server version, causing it to fail to activate.
### Changed
- Update to Code 1.141.0
## [4.140.0](https://github.com/coder/code-server/releases/tag/v4.140.0) - 2026-10-02
Code v1.140.0

View File

@@ -58,10 +58,17 @@ main() {
exit 1
fi
# Add the date, our name, links, enable telemetry (this just makes telemetry
# available; telemetry can still be disabled by flag or setting), and
# configure trusted extensions (since some, like github.copilot-chat, never
# ask to be trusted and this is the only way to get auth working).
# Modify the product.json:
# - Add the date
# - Set the code-server name.
# - Add our links
# - Enable telemetry (this just makes telemetry available; telemetry can still
# be disabled by flag or setting)
# - Configure trusted extensions (since some, like github.copilot-chat, never
# ask to be trusted and this is the only way to get auth working).
# - Make Typescript server plugins run in the workspace. We build the remote
# server version, so there is no browser distribution, and this key is not
# added automatically.
#
# This needs to be done before building as Code will read this file and embed
# it into the client-side code.
@@ -103,6 +110,9 @@ main() {
],
"aiConfig": {
"ariaKey": "code-server"
},
"extensionPointExtensionKind": {
"typescriptServerPlugins": ["workspace"]
}
}
EOF

View File

@@ -15,9 +15,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 3.55.0
version: 3.56.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 4.140.0
appVersion: 4.141.0

View File

@@ -6,7 +6,7 @@ replicaCount: 1
image:
repository: codercom/code-server
tag: '4.140.0'
tag: '4.141.0'
pullPolicy: Always
# Specifies one or more secrets to be used when pulling images from a

12
package-lock.json generated
View File

@@ -1055,9 +1055,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1779,9 +1779,9 @@
"license": "BSD-2-Clause"
},
"node_modules/brace-expansion": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
"integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -48,8 +48,8 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench-dev.html
+ <link rel="manifest" href="{{VS_BASE}}/manifest.json" crossorigin="use-credentials" />
<style id="vscode-css-modules" type="text/css" media="screen"></style>
</head>
@@ -41,7 +41,7 @@
<!-- Startup fallback: shown only if the scripts below never run, e.g. because a proxy,
@@ -92,7 +92,7 @@
<!-- Startup (do not modify order of script tags!) -->
<script nonce="{{WORKBENCH_SCRIPT_NONCE}}">
@@ -85,8 +85,8 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.html
+ <link rel="manifest" href="{{VS_BASE}}/manifest.json" crossorigin="use-credentials" />
<link rel="stylesheet" href="{{WORKBENCH_WEB_BASE_URL}}/out/vs/code/browser/workbench/workbench.css">
</head>
@@ -37,7 +37,7 @@
<!-- Startup fallback: shown only if the scripts below never run, e.g. because a proxy,
@@ -88,7 +88,7 @@
<!-- Startup (do not modify order of script tags!) -->
<script nonce="{{WORKBENCH_SCRIPT_NONCE}}">
@@ -281,8 +281,8 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts
-
(function () {
// Find config by checking for DOM
@@ -610,8 +600,8 @@ function readCookie(name: string): strin
// This script running at all means the page scripts were not blocked or rewritten.
@@ -614,8 +604,8 @@ function readCookie(name: string): strin
if (!configElement || !configElementAttribute) {
throw new Error('Missing web configuration element');
}

View File

@@ -272,7 +272,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extens
===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts
+++ code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts
@@ -475,9 +475,6 @@ export class InstallAction extends Exten
@@ -479,9 +479,6 @@ export class InstallAction extends Exten
if (this.extension.isBuiltin) {
return;
}
@@ -282,7 +282,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extens
if (this.extension.state !== ExtensionState.Uninstalled) {
return;
}
@@ -782,7 +779,7 @@ export abstract class InstallInOtherServ
@@ -786,7 +783,7 @@ export abstract class InstallInOtherServ
}
if (isLanguagePackExtension(this.extension.local.manifest)) {
@@ -291,7 +291,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extens
}
// Prefers to run on UI
@@ -2281,17 +2278,6 @@ export class SetLanguageAction extends E
@@ -2285,17 +2282,6 @@ export class SetLanguageAction extends E
update(): void {
this.enabled = false;
this.class = SetLanguageAction.DisabledClass;
@@ -309,7 +309,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extens
}
override async run(): Promise<any> {
@@ -2308,7 +2294,6 @@ export class ClearLanguageAction extends
@@ -2312,7 +2298,6 @@ export class ClearLanguageAction extends
private static readonly DisabledClass = `${this.EnabledClass} disabled`;
constructor(
@@ -317,7 +317,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/extensions/browser/extens
@ILocaleService private readonly localeService: ILocaleService,
) {
super(ClearLanguageAction.ID, ClearLanguageAction.TITLE.value, ClearLanguageAction.DisabledClass, false);
@@ -2318,17 +2303,6 @@ export class ClearLanguageAction extends
@@ -2322,17 +2307,6 @@ export class ClearLanguageAction extends
update(): void {
this.enabled = false;
this.class = ClearLanguageAction.DisabledClass;

View File

@@ -225,7 +225,7 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench-dev.html
+ <link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<style id="vscode-css-modules" type="text/css" media="screen"></style>
</head>
<!-- Startup fallback: shown only if the scripts below never run, e.g. because a proxy,
Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.html
===================================================================
--- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench.html
@@ -251,7 +251,7 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.html
+ <link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<link rel="stylesheet" href="{{WORKBENCH_WEB_BASE_URL}}/out/vs/code/browser/workbench/workbench.css">
</head>
<!-- Startup fallback: shown only if the scripts below never run, e.g. because a proxy,
Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts

33
patches/node-version.diff Normal file
View File

@@ -0,0 +1,33 @@
Bump the bundled Node.js version
Pull in Node.js security fixes (Brotli decoder RCE) ahead of upstream. Drop
this patch once upstream targets this version or newer.
Index: code-server/lib/vscode/remote/.npmrc
===================================================================
--- code-server.orig/lib/vscode/remote/.npmrc
+++ code-server/lib/vscode/remote/.npmrc
@@ -1,5 +1,5 @@
disturl="https://nodejs.org/dist"
-target="24.21.0"
+target="24.22.1"
ms_build_id="474716"
runtime="node"
build_from_source="true"
Index: code-server/lib/vscode/build/checksums/nodejs.txt
===================================================================
--- code-server.orig/lib/vscode/build/checksums/nodejs.txt
+++ code-server/lib/vscode/build/checksums/nodejs.txt
@@ -1,6 +1,6 @@
-bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057 node-v24.21.0-darwin-arm64.tar.gz
-1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097 node-v24.21.0-darwin-x64.tar.gz
-724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5 node-v24.21.0-linux-arm64.tar.gz
-6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff node-v24.21.0-linux-x64.tar.gz
-dff59da18b6ffe1bf1ca99e1d2af4906080c481740619f5b5098c0fca28bd9b7 win-arm64/node.exe
-ba4e6d110e8c1592a1ecd390f6b05f3da124b13871a5be62b341a07a853c6c32 win-x64/node.exe
+89c682593d76cd44233fa53b1563264e7de9cbb189cad169135c5b4fadfbc0a7 node-v24.22.1-darwin-arm64.tar.gz
+d132557e7ee2b85af08b0c8c34338e65c6430f2238d01f727728ef42dc83b035 node-v24.22.1-darwin-x64.tar.gz
+33c37be234ba8bee041961047b19160f4d7d0f164e8e246f78bdd10feff148b3 node-v24.22.1-linux-arm64.tar.gz
+eb6728e77c576a35162c122c45ccadf224d89ff00abc8dae46cc2897c6002d7c node-v24.22.1-linux-x64.tar.gz
+8bea91dc437fe01e7d92ef0efc6c9eaf654369aaba9b2d3e9841552ea7b95a00 win-arm64/node.exe
+d702daa815eabbff763c7d3808e235cf031cf2ca70211b56847bc72f51e240e5 win-x64/node.exe

View File

@@ -110,7 +110,7 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts
import type { IURLCallbackProvider } from '../../../workbench/services/url/browser/urlService.js';
import { create } from '../../../workbench/workbench.web.main.internal.js';
@@ -612,6 +613,44 @@ class WorkspaceProvider implements IWork
@@ -616,6 +617,44 @@ class WorkspaceProvider implements IWork
settingsSyncOptions: config.settingsSyncOptions ? { enabled: config.settingsSyncOptions.enabled, } : undefined,
workspaceProvider: WorkspaceProvider.create(config),
urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute),
@@ -159,7 +159,7 @@ Index: code-server/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExpl
===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts
+++ code-server/lib/vscode/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts
@@ -99,8 +99,8 @@ export class ForwardedPortsView extends
@@ -100,8 +100,8 @@ export class ForwardedPortsView extends
private async enableForwardedPortsFeatures() {
this.contextKeyListener.clear();

View File

@@ -3,10 +3,10 @@ Index: code-server/lib/vscode/src/vs/platform/secrets/common/secrets.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/platform/secrets/common/secrets.ts
+++ code-server/lib/vscode/src/vs/platform/secrets/common/secrets.ts
@@ -139,8 +139,13 @@ export class BaseSecretStorageService ex
@@ -144,8 +144,13 @@ export class BaseSecretStorageService ex
return await readEncryptedSecret(
key,
(fullKey) => this.getValueFromStorage(key, fullKey, storageService),
(fullKey) => this.getValueFromStorage(key, fullKey, storageService),
- // If the storage service is in-memory, we don't need to decrypt
- this._type === 'in-memory' ? (v) => Promise.resolve(v) : (v) => this._encryptionService.decrypt(v),
+ // Don't decrypt if storage is in-memory or if encryption service is not available
@@ -19,10 +19,10 @@ Index: code-server/lib/vscode/src/vs/platform/secrets/common/secrets.ts
this._logService,
);
} catch (e) {
@@ -160,8 +165,13 @@ export class BaseSecretStorageService ex
@@ -168,8 +173,13 @@ export class BaseSecretStorageService ex
key,
value,
(fullKey, encrypted) => this.setValueInStorage(key, fullKey, encrypted, storageService),
(fullKey, encrypted) => this.setValueInStorage(key, fullKey, encrypted, storageService),
- // If the storage service is in-memory, we don't need to encrypt
- this._type === 'in-memory' ? (v) => Promise.resolve(v) : (v) => this._encryptionService.encrypt(v),
+ // Don't encrypt if storage is in-memory or if encryption service is not available
@@ -35,7 +35,7 @@ Index: code-server/lib/vscode/src/vs/platform/secrets/common/secrets.ts
this._logService,
);
} catch (e) {
@@ -194,8 +204,9 @@ export class BaseSecretStorageService ex
@@ -224,8 +234,9 @@ export class BaseSecretStorageService ex
private async initialize(): Promise<IStorageService> {
let storageService;
@@ -47,7 +47,7 @@ Index: code-server/lib/vscode/src/vs/platform/secrets/common/secrets.ts
this._type = 'persisted';
storageService = this._storageService;
} else {
@@ -203,7 +214,7 @@ export class BaseSecretStorageService ex
@@ -233,7 +244,7 @@ export class BaseSecretStorageService ex
if (this._type === 'in-memory') {
return this._storageService;
}
@@ -77,10 +77,14 @@ Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench.ts
+++ code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts
@@ -625,2 +625,2 @@
@@ -655,8 +655,8 @@ class WorkspaceProvider implements IWork
})
}
},
- secretStorageProvider: config.remoteAuthority && !secretStorageKeyPath
- ? undefined /* with a remote without embedder-preferred storage, store on the remote */
+ secretStorageProvider: config.remoteStorageEnabled || (config.remoteAuthority && !secretStorageKeyPath)
+ ? undefined /* with remote storage enabled, or without embedder-prefered storage, store on the remote */
: new LocalStorageSecretStorageProvider(secretStorageCrypto),
});
})();

View File

@@ -1,8 +1,13 @@
Reuse VS Code's Electron storage service for remote persistence
Index: code-server/lib/vscode/src/vs/platform/storage/electron-main/storageMainService.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/platform/storage/electron-main/storageMainService.ts
+++ code-server/lib/vscode/src/vs/platform/storage/electron-main/storageMainService.ts
@@ -14 +14,6 @@
@@ -11,7 +11,12 @@ import { IStorage } from '../../../base/
import { INativeEnvironmentService } from '../../environment/common/environment.js';
import { IFileService } from '../../files/common/files.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
-import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js';
+import type { ILifecycleMainService, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js';
+
@@ -10,44 +15,110 @@ Reuse VS Code's Electron storage service for remote persistence
+// that module imports Electron, which is not present in the code-server server image.
+const LIFECYCLE_MAIN_PHASE_AFTER_WINDOW_OPEN = 3;
+const SHUTDOWN_REASON_KILL = 2;
@@ -101 +101 @@
import { ILogService } from '../../log/common/log.js';
import { AbstractStorageService, isProfileUsingDefaultStorage, IStorageService, StorageScope, StorageTarget } from '../common/storage.js';
import { ApplicationStorageMain, ApplicationSharedStorageMain, ProfileStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain, IStorageChangeEvent } from './storageMain.js';
@@ -92,7 +97,7 @@ export class StorageMainService extends
@ILogService private readonly logService: ILogService,
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@IUserDataProfilesMainService private readonly userDataProfilesService: IUserDataProfilesMainService,
- @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
+ private readonly lifecycleMainService: ILifecycleMainService,
@@ -117 +117 @@
@IFileService private readonly fileService: IFileService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
) {
@@ -114,7 +119,7 @@ export class StorageMainService extends
// Application Storage: Warmup when any window opens
(async () => {
- await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen);
+ await this.lifecycleMainService.when(LIFECYCLE_MAIN_PHASE_AFTER_WINDOW_OPEN);
@@ -256 +256 @@
this.applicationStorage.init();
this.applicationSharedStorage.init();
@@ -253,7 +258,7 @@ export class StorageMainService extends
}
private createProfileStorage(profile: IUserDataProfile): IStorageMain {
- if (this.shutdownReason === ShutdownReason.KILL) {
+ if (this.shutdownReason === SHUTDOWN_REASON_KILL) {
@@ -295 +295 @@
// Workaround for native crashes that we see when
// SQLite DBs are being created even after shutdown
@@ -292,7 +297,7 @@ export class StorageMainService extends
}
private createWorkspaceStorage(workspace: IAnyWorkspaceIdentifier): IStorageMain {
- if (this.shutdownReason === ShutdownReason.KILL) {
+ if (this.shutdownReason === SHUTDOWN_REASON_KILL) {
// Workaround for native crashes that we see when
// SQLite DBs are being created even after shutdown
Index: code-server/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts
+++ code-server/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts
@@ -6 +6 @@
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
+import { IRemoteService } from '../../../../platform/ipc/common/services.js';
@@ -22 +22 @@
import { IStorage, Storage, MigratingStorage } from '../../../../base/parts/storage/common/storage.js';
import { RemoteStorageService } from '../../../../platform/storage/common/storageService.js';
import { FallbackApplicationStorageDatabaseClient, ApplicationSharedStorageDatabaseClient } from '../../../../platform/storage/common/storageIpc.js';
@@ -19,10 +19,10 @@ export class NativeWorkbenchStorageServi
workspace: IAnyWorkspaceIdentifier | undefined,
private readonly userDataProfileService: IUserDataProfileService,
userDataProfilesService: IUserDataProfilesService,
- mainProcessService: IMainProcessService,
+ remoteService: Pick<IRemoteService, 'getChannel'>,
@@ -25 +25 @@
private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService,
) {
- super(workspace, { currentProfile: userDataProfileService.currentProfile, defaultProfile: userDataProfilesService.defaultProfile }, mainProcessService, workbenchEnvironmentService);
+ super(workspace, { currentProfile: userDataProfileService.currentProfile, defaultProfile: userDataProfilesService.defaultProfile }, remoteService as IRemoteService, workbenchEnvironmentService);
this.registerListeners();
}
Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
+++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts
@@ -68,0 +69 @@
@@ -66,6 +66,7 @@ export const serverOptions: OptionDescri
'disable-experiments': OPTIONS['disable-experiments'],
'disable-workspace-trust': OPTIONS['disable-workspace-trust'],
'file-watcher-polling': { type: 'string', deprecates: ['fileWatcherPolling'] },
+ 'enable-remote-storage': { type: 'boolean', cat: 'o', description: nls.localize('enable-remote-storage', 'Persist browser storage (settings, state, etc.) on the server instead of browser IndexedDB. Enables state portability across browsers/devices.') },
@@ -214,0 +216 @@
'log': OPTIONS['log'],
'logsPath': OPTIONS['logsPath'],
'force-disable-user-env': OPTIONS['force-disable-user-env'],
@@ -212,6 +213,7 @@ export interface ServerParsedArgs {
'logsPath'?: string;
'force-disable-user-env'?: boolean;
+ 'enable-remote-storage'?: boolean;
'enable-proposed-api'?: string[];
/* ----- vs code web options ----- */
Index: code-server/lib/vscode/src/vs/server/node/serverServices.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/server/node/serverServices.ts
+++ code-server/lib/vscode/src/vs/server/node/serverServices.ts
@@ -107,0 +108,4 @@
@@ -105,6 +105,10 @@ import { McpManagementChannel } from '..
import { AllowedMcpServersService } from '../../platform/mcp/common/allowedMcpServersService.js';
import { IMcpGalleryManifestService } from '../../platform/mcp/common/mcpGalleryManifest.js';
import { McpGalleryManifestIPCService } from '../../platform/mcp/common/mcpGalleryManifestServiceIpc.js';
+import { StorageDatabaseChannel } from '../../platform/storage/electron-main/storageIpc.js';
+import { StorageMainService } from '../../platform/storage/electron-main/storageMainService.js';
+import type { ILifecycleMainService } from '../../platform/lifecycle/electron-main/lifecycleMainService.js';
+import type { IUserDataProfilesMainService } from '../../platform/userDataProfile/electron-main/userDataProfile.js';
@@ -240,0 +245,15 @@
import { SANDBOX_HELPER_CHANNEL_NAME, SandboxHelperChannel } from '../../platform/sandbox/common/sandboxHelperIpc.js';
import { SandboxHelperService } from '../../platform/sandbox/node/sandboxHelper.js';
@@ -238,6 +242,21 @@ export async function setupServerService
services.set(IMcpGatewayService, new SyncDescriptor(McpGatewayService));
const instantiationService: IInstantiationService = new InstantiationService(services);
+ // Reuse the Electron storage service on the remote. The server has no
+ // windows, so its lifecycle is always ready and never loads a window.
+ const storageMainService = disposables.add(new StorageMainService(
@@ -63,7 +134,10 @@ Reuse VS Code's Electron storage service for remote persistence
+ uriIdentityService,
+ ));
+
@@ -419,6 +439,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken
services.set(ILanguagePackService, instantiationService.createInstance(NativeLanguagePackService));
const ptyHostStarter = instantiationService.createInstance(
@@ -420,6 +439,7 @@ export async function setupServerService
const languagePackChannel = ProxyChannel.fromService<RemoteAgentConnectionContext>(accessor.get(ILanguagePackService), disposables);
socketServer.registerChannel('languagePacks', languagePackChannel);
@@ -71,28 +145,61 @@ Reuse VS Code's Electron storage service for remote persistence
// clean up extensions folder
remoteExtensionsScanner.whenExtensionsReady().then(() => extensionManagementService.cleanUp());
Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts
+++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts
@@ -411 +411,2 @@
@@ -451,7 +451,8 @@ export class WebClientServer {
folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']),
workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']),
productConfiguration,
- callbackRoute: callbackRoute
+ callbackRoute: callbackRoute,
+ remoteStorageEnabled: this._environmentService.args['enable-remote-storage'] ? true : undefined
};
const cookies = cookie.parse(req.headers.cookie || '');
Index: code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts
+++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts
@@ -413,0 +414,5 @@
@@ -411,6 +411,11 @@ export interface IWorkbenchConstructionO
readonly authenticationProviders?: readonly IAuthenticationProvider[];
//#endregion
+ /**
+ * Persist workbench storage on the remote server instead of browser IndexedDB.
+ */
+ readonly remoteStorageEnabled?: boolean;
+
//#region Development options
Index: code-server/lib/vscode/src/vs/workbench/browser/web.main.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.main.ts
+++ code-server/lib/vscode/src/vs/workbench/browser/web.main.ts
@@ -35,0 +36 @@
@@ -33,6 +33,7 @@ import { ISignService } from '../../plat
import { SignService } from '../../platform/sign/browser/signService.js';
import { IWorkbenchConstructionOptions, IWorkbench, IWorkspace, ITunnel } from './web.api.js';
import { BrowserStorageService } from '../services/storage/browser/storageService.js';
+import { NativeWorkbenchStorageService } from '../services/storage/electron-browser/storageService.js';
@@ -479 +480 @@
import { IStorageService } from '../../platform/storage/common/storage.js';
import { toLocalISOString } from '../../base/common/date.js';
import { isWorkspaceToOpen, isFolderToOpen } from '../../platform/window/common/window.js';
@@ -482,7 +483,7 @@ export class BrowserMain extends Disposa
serviceCollection.set(IWorkbenchConfigurationService, service);
return service;
}),
- this.createStorageService(workspace, logService, userDataProfileService).then(service => {
+ this.createStorageService(workspace, environmentService, logService, userDataProfileService, userDataProfilesService, remoteAgentService).then(service => {
@@ -601 +602,15 @@
serviceCollection.set(IStorageService, service);
return service;
})
@@ -604,7 +605,21 @@ export class BrowserMain extends Disposa
}));
}
- protected async createStorageService(workspace: IAnyWorkspaceIdentifier, logService: ILogService, userDataProfileService: IUserDataProfileService): Promise<IStorageService> {
+ protected async createStorageService(workspace: IAnyWorkspaceIdentifier, environmentService: IBrowserWorkbenchEnvironmentService, logService: ILogService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService, remoteAgentService: IRemoteAgentService): Promise<IStorageService> {
+ const connection = remoteAgentService.getConnection();
@@ -109,14 +216,31 @@ Reuse VS Code's Electron storage service for remote persistence
+ }
+ }
+
const storageService = new BrowserStorageService(workspace, userDataProfileService, logService);
try {
Index: code-server/lib/vscode/src/vs/sessions/browser/web.main.ts
===================================================================
--- code-server.orig/lib/vscode/src/vs/sessions/browser/web.main.ts
+++ code-server/lib/vscode/src/vs/sessions/browser/web.main.ts
@@ -42 +42 @@
@@ -39,9 +39,9 @@ export class SessionsBrowserMain extends
_workspace: IAnyWorkspaceIdentifier,
environmentService: IBrowserWorkbenchEnvironmentService,
userDataProfileService: IUserDataProfileService,
- _userDataProfilesService: BrowserUserDataProfilesService,
+ userDataProfilesService: BrowserUserDataProfilesService,
@@ -44 +44 @@
fileService: FileService,
- _remoteAgentService: IRemoteAgentService,
+ remoteAgentService: IRemoteAgentService,
@@ -85 +85 @@
uriIdentityService: IUriIdentityService,
policyService: IPolicyService,
logService: ILogService,
@@ -82,7 +82,7 @@ export class SessionsBrowserMain extends
serviceCollection.set(IWorkbenchConfigurationService, configurationService);
// Storage
- const storageService = await this.createStorageService(workspaceIdentifier, logService, userDataProfileService);
+ const storageService = await this.createStorageService(workspaceIdentifier, environmentService, logService, userDataProfileService, userDataProfilesService, remoteAgentService);
serviceCollection.set(IStorageService, storageService);
// Workspace Trust Service

View File

@@ -24,6 +24,7 @@ trusted-domains.diff
signature-verification.diff
copilot.diff
app-name.diff
csp-hashes.diff
remote-storage.diff
remote-secret-storage.diff
csp-hashes.diff
node-version.diff

View File

@@ -3,7 +3,7 @@ import * as crypto from "crypto"
import { promises as fs } from "fs"
import { load } from "js-yaml"
import * as path from "path"
import { generateCertificate, generatePassword, paths, splitOnFirstEquals } from "./util"
import { expandTilde, generateCertificate, generatePassword, paths, splitOnFirstEquals } from "./util"
import { EditorSessionManagerClient } from "./vscodeSocket"
export enum Feature {
@@ -436,7 +436,11 @@ export const parse = (
const option = options[key]
if (option.type === "boolean") {
;(args[key] as boolean) = true
// Booleans set to false (for example `--flag=false`, which is also how
// false values from the config file are passed in) are ignored.
if (value !== "false") {
;(args[key] as boolean) = true
}
continue
}
@@ -462,7 +466,7 @@ export const parse = (
}
if (option.path) {
value = path.resolve(value)
value = path.resolve(expandTilde(value))
}
switch (option.type) {

View File

@@ -3,6 +3,7 @@ import i18next, { init } from "i18next"
import * as en from "./locales/en.json"
import * as ja from "./locales/ja.json"
import * as th from "./locales/th.json"
import * as tr from "./locales/tr.json"
import * as ur from "./locales/ur.json"
import * as zhCn from "./locales/zh-cn.json"
@@ -16,6 +17,9 @@ const defaultResources = {
th: {
translation: th,
},
tr: {
translation: tr,
},
ja: {
translation: ja,
},

View File

@@ -0,0 +1,13 @@
{
"LOGIN_TITLE": "{{app}} girişi",
"LOGIN_BELOW": "Lütfen aşağıdan giriş yapın.",
"WELCOME": "{{app}} uygulamasına hoş geldiniz",
"LOGIN_PASSWORD": "Parola için yapılandırma dosyasını kontrol edin.",
"LOGIN_USING_ENV_PASSWORD": "Parola $PASSWORD ortam değişkeninden ayarlandı.",
"LOGIN_USING_HASHED_PASSWORD": "Parola $HASHED_PASSWORD ortam değişkeninden ayarlandı.",
"SUBMIT": "GÖNDER",
"PASSWORD_PLACEHOLDER": "PAROLA",
"LOGIN_RATE_LIMIT": "Giriş denemesi sınırına ulaşıldı!",
"MISS_PASSWORD": "Parola eksik",
"INCORRECT_PASSWORD": "Parola yanlış."
}

View File

@@ -132,8 +132,10 @@ export const register = async (
})
})
app.router.use(express.json())
app.router.use(express.urlencoded({ extended: true }))
// Reject compressed request bodies so unauthenticated requests cannot reach
// the zlib/Brotli decoders. Browsers never compress request bodies.
app.router.use(express.json({ inflate: false }))
app.router.use(express.urlencoded({ extended: true, inflate: false }))
app.router.use(
"/_static",

View File

@@ -511,3 +511,18 @@ export function splitOnFirstEquals(str: string): [string, string | undefined] {
const split = str.split(/=(.+)?/, 2)
return [split[0], split[1]]
}
/**
* Expand a leading `~` to the home directory. The shell does this for
* command-line arguments but values from the config file are not expanded.
* `~user` forms are left alone.
*/
export function expandTilde(p: string): string {
if (p === "~") {
return os.homedir()
}
if (p.startsWith("~/") || p.startsWith(`~${path.sep}`)) {
return path.join(os.homedir(), p.slice(2))
}
return p
}

View File

@@ -38,7 +38,7 @@ export async function makeEditorSessionManagerServer(
): Promise<http.Server> {
const router = express()
router.use(express.json())
router.use(express.json({ inflate: false }))
router.get<{}, GetSessionResponse | string | unknown, undefined, { filePath?: string }>(
"/session",

View File

@@ -2,7 +2,8 @@ import * as vscode from "vscode"
export function activate(context: vscode.ExtensionContext) {
vscode.window.showInformationMessage("test extension loaded")
// Test extension
// Prints the current proxy URI into a notification.
context.subscriptions.push(
vscode.commands.registerCommand("codeServerTest.proxyUri", () => {
if (process.env.VSCODE_PROXY_URI) {
@@ -13,7 +14,8 @@ export function activate(context: vscode.ExtensionContext) {
}),
)
// asExternalUri extension
// Asks for a URI, then runs it through asExternalUri and prints the result
// into a notification.
context.subscriptions.push(
vscode.commands.registerCommand("codeServerTest.asExternalUri", async () => {
const input = await vscode.window.showInputBox({
@@ -28,4 +30,29 @@ export function activate(context: vscode.ExtensionContext) {
}
}),
)
const secretKey = "code-server-test-secret"
// Asks for a string, then stores it as a secret.
context.subscriptions.push(
vscode.commands.registerCommand("codeServerTest.setSecret", async () => {
const input = await vscode.window.showInputBox({
prompt: "Secret to store",
})
if (input) {
await context.secrets.store(secretKey, input)
vscode.window.showInformationMessage(`set secret: ${input}`)
} else {
vscode.window.showErrorMessage(`Failed to run test case. No input provided.`)
}
}),
)
// Prints the current secret, if any.
context.subscriptions.push(
vscode.commands.registerCommand("codeServerTest.getSecret", async () => {
vscode.window.showInformationMessage(`get secret: ${await context.secrets.get(secretKey)}`)
}),
)
}

View File

@@ -22,6 +22,16 @@
"command": "codeServerTest.asExternalUri",
"title": "asExternalUri test",
"category": "code-server"
},
{
"command": "codeServerTest.setSecret",
"title": "Set test secret",
"category": "code-server"
},
{
"command": "codeServerTest.getSecret",
"title": "Get test secret",
"category": "code-server"
}
]
},

View File

@@ -363,13 +363,14 @@ export class CodeServerPage {
}
/**
* Checks if the test extension loaded
* Checks if the test extension loaded by waiting for its notification. Once
* it appears, dismiss the notification.
*/
async waitForTestExtensionLoaded(): Promise<void> {
const selector = "text=test extension loaded"
this.codeServer.logger.debug("Waiting for test extension to load...")
await this.page.waitForSelector(selector)
const locator = this.page.getByRole("dialog", { name: "Info: test extension loaded" })
await locator.hover()
await locator.getByLabel("Clear Notification (Del)").click()
}
/**

View File

@@ -1,5 +1,3 @@
import { promises as fs } from "fs"
import * as path from "path"
import { clean } from "../utils/helpers"
import { describe, test, expect } from "./baseFixture"

61
test/e2e/storage.test.ts Normal file
View File

@@ -0,0 +1,61 @@
import { promises as fs } from "fs"
import * as path from "path"
import { describe, test, expect } from "./baseFixture"
const tests = [
{
name: "browser storage",
args: [],
},
{
name: "remote storage",
args: ["--enable-remote-storage"],
},
]
for (const tc of tests) {
const args = ["--disable-workspace-trust", "--extensions-dir", path.join(__dirname, "./extensions"), ...tc.args]
describe(tc.name, args, {}, () => {
test("should store workbench state and secrets", async ({ codeServerPage }) => {
// Create and open a file.
const dir = await codeServerPage.workspaceDir
const file = path.join(dir, "foo")
await fs.writeFile(file, "bar")
await codeServerPage.openFile(file)
// Store a secret.
const secret = "foo-bar-baz"
await codeServerPage.waitForTestExtensionLoaded()
await codeServerPage.executeCommandViaMenus("code-server: Set test secret")
await codeServerPage.page.waitForSelector(".quick-input-widget:focus-within")
await codeServerPage.page.keyboard.type(secret)
await codeServerPage.page.keyboard.press("Enter")
await expect(codeServerPage.page.getByText(`Info: set secret: ${secret}`)).toBeVisible()
// There should be a workspace storage directory.
const userDir = path.join(dir, "User/workspaceStorage")
const dirs = await fs.readdir(userDir)
expect(dirs.length).toBe(1)
const storageFile = path.join(userDir, dirs[0], "state.vscdb")
if (tc.args.includes("--enable-remote-storage")) {
// The sqlite database should exist.
fs.stat(storageFile)
} else {
// But no sqlite database.
expect(() => fs.stat(storageFile)).rejects.toThrow("ENOENT")
}
// Reload using a command so it has a chance to flush storage.
await codeServerPage.executeCommandViaMenus("Developer: Reload Window")
await codeServerPage.waitForTestExtensionLoaded()
// Should see the same file.
await codeServerPage.waitForTab(file)
// Should be able to retrieve the secret.
await codeServerPage.executeCommandViaMenus("code-server: Get test secret")
await expect(codeServerPage.page.getByText(`Info: get secret: ${secret}`)).toBeVisible()
})
})
}

View File

@@ -0,0 +1,19 @@
import { promises as fs } from "fs"
import * as path from "path"
import { describe, test, expect } from "./baseFixture"
const args = ["--disable-workspace-trust"]
describe("typescript extension", args, {}, () => {
test("should load as a workspace extension", async ({ codeServerPage }) => {
const dir = await codeServerPage.workspaceDir
const file = path.join(dir, "foo.ts")
await fs.writeFile(file, 'const n: number = "x"')
await codeServerPage.openFile(file)
await codeServerPage.executeCommandViaMenus("Developer: Show Running Extensions")
await expect(
codeServerPage.page.getByLabel("typescript-language-features").getByText(/localhost:\d+/),
).toBeVisible()
})
})

View File

@@ -1,5 +1,6 @@
import { Level, logger } from "@coder/logger"
import { promises as fs } from "fs"
import * as os from "os"
import * as path from "path"
import {
UserProvidedArgs,
@@ -575,9 +576,33 @@ describe("parser", () => {
"install-extension": ["extension.number1", "extension.number2"],
})
})
it("should expand ~ in path options from the config file", () => {
const configContents = `
user-data-dir: ~/.config/code-server-data
extensions-dir: ~/extensions
`
expect(parseConfigFile(configContents, "/fake-config-path")).toEqual({
config: "/fake-config-path",
"user-data-dir": path.join(os.homedir(), ".config/code-server-data"),
"extensions-dir": path.join(os.homedir(), "extensions"),
})
})
it("should ignore optional strings set to false", async () => {
expect(parse(["--cert=false"])).toEqual({})
})
it("should ignore booleans set to false", async () => {
expect(parse(["--disable-telemetry=false"])).toEqual({})
})
it("should parse booleans set to false in the config file", () => {
const configContents = `
disable-telemetry: false
disable-update-check: true
`
expect(parseConfigFile(configContents, "/fake-config-path")).toEqual({
config: "/fake-config-path",
"disable-update-check": true,
})
})
it("should use last flag", async () => {
expect(parse(["--port", "8081", "--port", "8082"])).toEqual({
port: 8082,

View File

@@ -1,9 +1,13 @@
import { promises as fs } from "fs"
import * as os from "os"
import * as path from "path"
import { loadCustomStrings } from "../../../src/node/i18n"
import i18n, { loadCustomStrings } from "../../../src/node/i18n"
describe("i18n", () => {
it("includes Turkish translations", () => {
expect(i18n.getResource("tr", "translation", "WELCOME")).toBe("{{app}} uygulamasına hoş geldiniz")
})
let tempDir: string
let validJsonFile: string
let invalidJsonFile: string

View File

@@ -75,6 +75,17 @@ describe("login", () => {
expect(htmlContent).toContain("Missing password")
})
it("should reject compressed request bodies", async () => {
for (const encoding of ["br", "gzip", "deflate"]) {
const resp = await codeServer().fetch("/login", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded", "Content-Encoding": encoding },
body: "password=test",
})
expect(resp.status).toBe(415)
}
})
it("should return HTML with 'Incorrect password' message", async () => {
const params = new URLSearchParams()
params.append("password", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")

View File

@@ -1,5 +1,6 @@
import * as cp from "child_process"
import { promises as fs } from "fs"
import * as os from "os"
import * as path from "path"
import { generateUuid } from "../../../src/common/util"
import { tmpdir } from "../../../src/node/constants"
@@ -623,3 +624,18 @@ describe("splitOnFirstEquals", () => {
})
})
})
describe("expandTilde", () => {
it("should expand a bare ~", () => {
expect(util.expandTilde("~")).toBe(os.homedir())
})
it("should expand a leading ~/", () => {
expect(util.expandTilde("~/.config/code-server")).toBe(path.join(os.homedir(), ".config/code-server"))
})
it("should leave other paths alone", () => {
expect(util.expandTilde("/tmp/~/data")).toBe("/tmp/~/data")
expect(util.expandTilde("relative/~")).toBe("relative/~")
expect(util.expandTilde("~user/data")).toBe("~user/data")
expect(util.expandTilde("")).toBe("")
})
})