snapshot of https://github.com/ueberdosis/hocuspocus.git @ cf3cfa355b4d3bb61c6b6c80071f71783e32470e (2026-09-15)
Some checks failed
build / lint (22) (push) Failing after 17s
build / test (22) (push) Failing after 8s
build / lint (23) (push) Failing after 2s
build / build (22) (push) Has been skipped
build / build (23) (push) Has been skipped
build / test (23) (push) Failing after 4s
docsearch / docsearch (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 17:21:35 +05:00
commit e0c588ca1c
265 changed files with 55259 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
# @hocuspocus/extension-throttle
[![Version](https://img.shields.io/npm/v/@hocuspocus/extension-throttle.svg?label=version)](https://www.npmjs.com/package/@hocuspocus/extension-throttle)
[![Downloads](https://img.shields.io/npm/dm/@hocuspocus/extension-throttle.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@hocuspocus/extension-throttle.svg)](https://www.npmjs.com/package/@hocuspocus/extension-throttle)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
Rate-limit and throttle connections to a [Hocuspocus](https://github.com/ueberdosis/hocuspocus) server by IP. Protects your collaboration backend from abuse, accidental reconnect loops, and misbehaving clients.
## Installation
```bash
npm install @hocuspocus/extension-throttle
```
## Usage
With default thresholds (15 connection attempts per 60 seconds, 5 minute ban):
```js
import { Server } from "@hocuspocus/server"
import { Throttle } from "@hocuspocus/extension-throttle"
const server = new Server({
extensions: [new Throttle()],
})
server.listen()
```
### Tune the limits
```js
new Throttle({
throttle: 30, // allowed connection attempts per window
consideredSeconds: 60, // window length in seconds
banTime: 5, // ban duration in minutes
})
```
Pass `throttle: false` (or `null`) to effectively disable the limiter without removing the extension.
## Documentation
Full options reference: [tiptap.dev/docs/hocuspocus/server/extensions/throttle](https://tiptap.dev/docs/hocuspocus/server/extensions/throttle).
## License
MIT — see [LICENSE.md](https://github.com/ueberdosis/hocuspocus/blob/main/LICENSE.md).

View File

@@ -0,0 +1,40 @@
{
"name": "@hocuspocus/extension-throttle",
"version": "4.7.0",
"description": "hocuspocus throttle extension",
"homepage": "https://hocuspocus.dev",
"keywords": [
"hocuspocus",
"throttle",
"yjs"
],
"license": "MIT",
"type": "module",
"main": "dist/hocuspocus-throttle.cjs",
"module": "dist/hocuspocus-throttle.esm.js",
"types": "dist/index.d.ts",
"exports": {
"source": {
"import": "./src/index.ts"
},
"default": {
"import": "./dist/hocuspocus-throttle.esm.js",
"require": "./dist/hocuspocus-throttle.cjs",
"types": "./dist/index.d.ts"
}
},
"files": [
"src",
"dist"
],
"dependencies": {
"@hocuspocus/server": "workspace:^"
},
"gitHead": "b3454a4ca289a84ddfb7fa5607a2d4b8d5c37e9d",
"repository": {
"url": "https://github.com/ueberdosis/hocuspocus"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,126 @@
import type { Extension, onConnectPayload } from "@hocuspocus/server";
export interface ThrottleConfiguration {
throttle: number | null | false; // how many requests within `consideredSeconds` until we're rejecting requests (setting this to 15 means the 16th request will be rejected)
consideredSeconds: number; // how many seconds to consider (default is last 60 seconds from the current connection attempt)
banTime: number; // for how long to ban after receiving too many requests (in minutes!)
cleanupInterval: number; // how often to clean up the records of IPs (this won't delete ips that are still blocked or recent enough by `consideredSeconds`)
}
export class Throttle implements Extension {
configuration: ThrottleConfiguration = {
throttle: 15,
banTime: 5,
consideredSeconds: 60,
cleanupInterval: 90,
};
connectionsByIp: Map<string, Array<number>> = new Map();
bannedIps: Map<string, number> = new Map();
cleanupInterval?: NodeJS.Timeout;
/**
* Constructor
*/
constructor(configuration?: Partial<ThrottleConfiguration>) {
this.configuration = {
...this.configuration,
...configuration,
};
this.cleanupInterval = setInterval(
this.clearMaps.bind(this),
this.configuration.cleanupInterval * 1000,
);
}
onDestroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
return Promise.resolve();
}
public clearMaps() {
this.connectionsByIp.forEach((value, key) => {
const filteredValue = value.filter(
(timestamp) =>
timestamp + this.configuration.consideredSeconds * 1000 > Date.now(),
);
if (filteredValue.length) {
this.connectionsByIp.set(key, filteredValue);
} else {
this.connectionsByIp.delete(key);
}
});
this.bannedIps.forEach((value, key) => {
if (!this.isBanned(key)) {
this.bannedIps.delete(key);
}
});
}
isBanned(ip: string) {
const bannedAt = this.bannedIps.get(ip) || 0;
return Date.now() < bannedAt + this.configuration.banTime * 60 * 1000;
}
/**
* Throttle requests
* @private
*/
private throttle(ip: string): boolean {
if (!this.configuration.throttle) {
return false;
}
if (this.isBanned(ip)) return true;
this.bannedIps.delete(ip);
// add this connection try to the list of previous connections
const previousConnections = this.connectionsByIp.get(ip) || [];
previousConnections.push(Date.now());
// calculate the previous connections in the last considered time interval
const previousConnectionsInTheConsideredInterval =
previousConnections.filter(
(timestamp) =>
timestamp + this.configuration.consideredSeconds * 1000 > Date.now(),
);
this.connectionsByIp.set(ip, previousConnectionsInTheConsideredInterval);
if (
previousConnectionsInTheConsideredInterval.length >
this.configuration.throttle
) {
this.bannedIps.set(ip, Date.now());
return true;
}
return false;
}
/**
* onConnect hook
* @param data
*/
onConnect(data: onConnectPayload): Promise<any> {
const { request } = data;
// get the remote ip address from headers (web standard Request doesn't have socket)
const ip =
request.headers.get("x-real-ip") ||
request.headers.get("x-forwarded-for") ||
"";
// throttle the connection
return this.throttle(<string>ip) ? Promise.reject() : Promise.resolve();
}
}