fix(server): sync permission check (#15123)

fix #15121



#### PR Dependency Tree


* **PR #15123** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Security Improvements**
* Enforced document-level `Doc.Read`/`Doc.Update` checks for key sync
websocket operations, including filtering workspace doc timestamp
results to only readable documents.
* Improved remote permission handling: once a remote denies access,
syncing stops for the affected document and retry behavior is
suppressed.
* **Improvements**
* `delete-doc` now relies on server acknowledgment and returns an
explicit `{ success: true }`.
* Websocket acknowledgment errors are now normalized for consistent
error details.
* **Tests**
* Expanded permission-denied and websocket error-handling coverage,
including timestamp filtering and no-retry behavior after permission
denial.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-06-18 02:43:25 +08:00
committed by GitHub
parent da7781a751
commit 1256d66938
6 changed files with 666 additions and 42 deletions
+19 -8
View File
@@ -22,6 +22,12 @@ interface CloudDocStorageOptions extends DocStorageOptions {
type: SpaceType;
}
function createWebsocketError(error: { name: string; message: string }) {
const err = new Error(error.message);
err.name = error.name;
return err;
}
export class CloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
static readonly identifier = 'CloudDocStorage';
@@ -88,7 +94,7 @@ export class CloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
return null;
}
// TODO: use [UserFriendlyError]
throw new Error(response.error.message);
throw createWebsocketError(response.error);
}
return {
@@ -111,7 +117,7 @@ export class CloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
return null;
}
// TODO: use [UserFriendlyError]
throw new Error(response.error.message);
throw createWebsocketError(response.error);
}
return {
@@ -132,7 +138,7 @@ export class CloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
if ('error' in response) {
// TODO(@forehalo): use [UserFriendlyError]
throw new Error(response.error.message);
throw createWebsocketError(response.error);
}
return {
@@ -153,7 +159,7 @@ export class CloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
if ('error' in response) {
// TODO: use [UserFriendlyError]
throw new Error(response.error.message);
throw createWebsocketError(response.error);
}
return {
@@ -174,7 +180,7 @@ export class CloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
if ('error' in response) {
// TODO(@forehalo): use [UserFriendlyError]
throw new Error(response.error.message);
throw createWebsocketError(response.error);
}
return Object.entries(response.data).reduce((ret, [docId, timestamp]) => {
@@ -184,11 +190,16 @@ export class CloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
}
override async deleteDoc(docId: string) {
this.socket.emit('space:delete-doc', {
const response = await this.socket.emitWithAck('space:delete-doc', {
spaceType: this.spaceType,
spaceId: this.spaceId,
docId: this.idConverter.newIdToOldId(docId),
});
if ('error' in response) {
// TODO(@forehalo): use [UserFriendlyError]
throw createWebsocketError(response.error);
}
}
protected async setDocSnapshot() {
@@ -224,7 +235,7 @@ class CloudDocStorageConnection extends SocketConnection {
});
if ('error' in res) {
throw new Error(res.error.message);
throw createWebsocketError(res.error);
}
if (!this.idConverter) {
@@ -272,7 +283,7 @@ class CloudDocStorageConnection extends SocketConnection {
return null;
}
// TODO: use [UserFriendlyError]
throw new Error(response.error.message);
throw createWebsocketError(response.error);
}
return base64ToUint8Array(response.data.missing);
@@ -121,7 +121,10 @@ interface ClientEvents {
timestamp: number;
},
];
'space:delete-doc': { spaceType: string; spaceId: string; docId: string };
'space:delete-doc': [
{ spaceType: string; spaceId: string; docId: string },
{ success?: true },
];
'telemetry:batch': [TelemetryBatch, TelemetryAck];