@ifc-lite/collab-server 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +47 -0
  3. package/dist/audit-log.d.ts +77 -0
  4. package/dist/audit-log.d.ts.map +1 -0
  5. package/dist/audit-log.js +105 -0
  6. package/dist/audit-log.js.map +1 -0
  7. package/dist/auth.d.ts +24 -0
  8. package/dist/auth.d.ts.map +1 -0
  9. package/dist/auth.js +15 -0
  10. package/dist/auth.js.map +1 -0
  11. package/dist/bin.d.ts +3 -0
  12. package/dist/bin.d.ts.map +1 -0
  13. package/dist/bin.js +34 -0
  14. package/dist/bin.js.map +1 -0
  15. package/dist/blob-route.d.ts +58 -0
  16. package/dist/blob-route.d.ts.map +1 -0
  17. package/dist/blob-route.js +132 -0
  18. package/dist/blob-route.js.map +1 -0
  19. package/dist/index.d.ts +18 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +19 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/metrics.d.ts +56 -0
  24. package/dist/metrics.d.ts.map +1 -0
  25. package/dist/metrics.js +161 -0
  26. package/dist/metrics.js.map +1 -0
  27. package/dist/path-locks.d.ts +39 -0
  28. package/dist/path-locks.d.ts.map +1 -0
  29. package/dist/path-locks.js +176 -0
  30. package/dist/path-locks.js.map +1 -0
  31. package/dist/persistence-redis.d.ts +52 -0
  32. package/dist/persistence-redis.d.ts.map +1 -0
  33. package/dist/persistence-redis.js +50 -0
  34. package/dist/persistence-redis.js.map +1 -0
  35. package/dist/persistence-s3.d.ts +81 -0
  36. package/dist/persistence-s3.d.ts.map +1 -0
  37. package/dist/persistence-s3.js +183 -0
  38. package/dist/persistence-s3.js.map +1 -0
  39. package/dist/persistence.d.ts +41 -0
  40. package/dist/persistence.d.ts.map +1 -0
  41. package/dist/persistence.js +108 -0
  42. package/dist/persistence.js.map +1 -0
  43. package/dist/rate-limit.d.ts +29 -0
  44. package/dist/rate-limit.d.ts.map +1 -0
  45. package/dist/rate-limit.js +36 -0
  46. package/dist/rate-limit.js.map +1 -0
  47. package/dist/replay-protect.d.ts +51 -0
  48. package/dist/replay-protect.d.ts.map +1 -0
  49. package/dist/replay-protect.js +134 -0
  50. package/dist/replay-protect.js.map +1 -0
  51. package/dist/retention.d.ts +32 -0
  52. package/dist/retention.d.ts.map +1 -0
  53. package/dist/retention.js +112 -0
  54. package/dist/retention.js.map +1 -0
  55. package/dist/room-manager.d.ts +138 -0
  56. package/dist/room-manager.d.ts.map +1 -0
  57. package/dist/room-manager.js +380 -0
  58. package/dist/room-manager.js.map +1 -0
  59. package/dist/secure-bundle.d.ts +15 -0
  60. package/dist/secure-bundle.d.ts.map +1 -0
  61. package/dist/secure-bundle.js +49 -0
  62. package/dist/secure-bundle.js.map +1 -0
  63. package/dist/secure-server.d.ts +49 -0
  64. package/dist/secure-server.d.ts.map +1 -0
  65. package/dist/secure-server.js +86 -0
  66. package/dist/secure-server.js.map +1 -0
  67. package/dist/server.d.ts +59 -0
  68. package/dist/server.d.ts.map +1 -0
  69. package/dist/server.js +196 -0
  70. package/dist/server.js.map +1 -0
  71. package/dist/snapshot-worker.d.ts +32 -0
  72. package/dist/snapshot-worker.d.ts.map +1 -0
  73. package/dist/snapshot-worker.js +87 -0
  74. package/dist/snapshot-worker.js.map +1 -0
  75. package/package.json +66 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Room manager.
3
+ *
4
+ * Each room owns:
5
+ * - one in-memory Y.Doc that all peers in the room sync against
6
+ * - a set of WebSocket connections
7
+ * - an Awareness instance forwarded over the same connections
8
+ *
9
+ * Updates are persisted via the supplied `Persistence`. Compaction kicks
10
+ * in every `compactEvery` updates per spec §12.2.
11
+ */
12
+ import { Awareness } from 'y-protocols/awareness';
13
+ import * as Y from 'yjs';
14
+ import type { WebSocket } from 'ws';
15
+ import type { Persistence } from './persistence.js';
16
+ import type { Principal } from './auth.js';
17
+ import { type AuditOpType, type AuditSink } from './audit-log.js';
18
+ import { type RateLimitOptions, type RateLimiter } from './rate-limit.js';
19
+ export interface VerifyDecision {
20
+ ok: boolean;
21
+ /** Audit-friendly reason string when ok=false. */
22
+ reason?: string;
23
+ }
24
+ export type VerifyMessageFn = (msg: Uint8Array, conn: PeerConnection) => VerifyDecision;
25
+ export interface RoomOptions {
26
+ persistence: Persistence;
27
+ /** Compact the persisted log every N updates (default 1000). */
28
+ compactEvery?: number;
29
+ /** Idle timeout before a room is unloaded (default 60s). */
30
+ idleUnloadMs?: number;
31
+ /** Audit sink for connect/update/awareness events (default = no-op). */
32
+ auditSink?: AuditSink;
33
+ /**
34
+ * Per-peer rate-limit knobs. Applied per connection. Service accounts
35
+ * (e.g. MCP agents) typically get a tighter budget than humans.
36
+ */
37
+ rateLimit?: RateLimitOptions | ((principal: Principal) => RateLimitOptions);
38
+ /**
39
+ * Optional per-message verifier. Runs BEFORE rate limit / role check.
40
+ * Consumers using the anti-replay protector pass a wrapper here that
41
+ * decodes their envelope and calls `replayProtector.verify(...)`.
42
+ * Returning `{ ok: false }` audits as `reject` with `reason`.
43
+ */
44
+ verifyMessage?: VerifyMessageFn;
45
+ /** Internal: metric counters injected by the manager. */
46
+ counters?: {
47
+ update?: () => void;
48
+ reject?: (reason: string) => void;
49
+ };
50
+ }
51
+ export interface PeerConnection {
52
+ ws: WebSocket;
53
+ principal: Principal;
54
+ /** Subscribed clientIDs that this peer's awareness has reported (for cleanup). */
55
+ awarenessClients: Set<number>;
56
+ /** Per-connection rate limiter. */
57
+ limiter?: RateLimiter;
58
+ }
59
+ export declare class Room {
60
+ readonly id: string;
61
+ readonly doc: Y.Doc;
62
+ readonly awareness: Awareness;
63
+ private readonly conns;
64
+ private readonly persistence;
65
+ private updatesSinceCompact;
66
+ private readonly compactEvery;
67
+ private readonly auditSink;
68
+ private readonly rateLimitFor;
69
+ private readonly verifyMessage?;
70
+ private counters;
71
+ private destroyed;
72
+ constructor(id: string, opts: RoomOptions);
73
+ setCounters(counters: {
74
+ update?: () => void;
75
+ reject?: (reason: string) => void;
76
+ }): void;
77
+ /**
78
+ * Append an audit-log entry for this room.
79
+ * Public so the http upgrade handler can log connect/disconnect.
80
+ */
81
+ audit(principal: Principal, opType: AuditOpType, opHash: string, detail?: Record<string, unknown>): void;
82
+ loadFromDisk(): Promise<void>;
83
+ /** Number of currently connected peers. */
84
+ get peerCount(): number;
85
+ addConnection(conn: PeerConnection): void;
86
+ removeConnection(conn: PeerConnection): void;
87
+ /** Receive a binary message from a peer and dispatch it. */
88
+ handleMessage(conn: PeerConnection, msg: Uint8Array): void;
89
+ /** Forward Y updates to every other connected peer and persist. */
90
+ private onDocUpdate;
91
+ private persistAndMaybeCompact;
92
+ /**
93
+ * Forward an awareness update to every other peer. Tracks which client
94
+ * IDs the connection contributed so we can clean up on disconnect.
95
+ */
96
+ private onAwarenessUpdate;
97
+ destroy(): Promise<void>;
98
+ }
99
+ export interface RoomManagerOptions extends RoomOptions {
100
+ /** Soft cap on simultaneous rooms (default 1024). */
101
+ maxRooms?: number;
102
+ }
103
+ export interface RoomCounters {
104
+ update?: () => void;
105
+ reject?: (reason: string) => void;
106
+ }
107
+ export declare class RoomManager {
108
+ private readonly rooms;
109
+ /** Wall-clock ms when a room last had >0 peers, or 0 if never observed. */
110
+ private readonly lastActiveAt;
111
+ private readonly options;
112
+ private idleTimer;
113
+ private counters;
114
+ constructor(options: RoomManagerOptions);
115
+ getOrCreate(roomId: string): Promise<Room>;
116
+ /**
117
+ * Inject counters used by `Room.handleMessage` to bump metric values.
118
+ * The manager forwards them to every newly-loaded room.
119
+ */
120
+ setCounters(counters: RoomCounters): void;
121
+ list(): string[];
122
+ /** Snapshot of `(roomId, peerCount)` for diagnostics / tests. */
123
+ stats(): Promise<Array<{
124
+ roomId: string;
125
+ peerCount: number;
126
+ idleMs: number;
127
+ }>>;
128
+ unload(roomId: string): Promise<void>;
129
+ unloadAll(): Promise<void>;
130
+ /**
131
+ * Unload any room with zero peers that has been idle longer than
132
+ * `idleUnloadMs`. The persistence layer holds the durable copy, so
133
+ * unloading is non-destructive — the room is rehydrated on next
134
+ * connect.
135
+ */
136
+ sweepIdle(): Promise<string[]>;
137
+ }
138
+ //# sourceMappingURL=room-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-manager.d.ts","sourceRoot":"","sources":["../src/room-manager.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,SAAS,EAAsE,MAAM,uBAAuB,CAAC;AAItH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAqB,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAK7F,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,KAAK,cAAc,CAAC;AAExF,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,SAAS,CAAC,EAAE,gBAAgB,GAAG,CAAC,CAAC,SAAS,EAAE,SAAS,KAAK,gBAAgB,CAAC,CAAC;IAC5E;;;;;OAKG;IACH,aAAa,CAAC,EAAE,eAAe,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;CACvE;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,SAAS,CAAC;IACd,SAAS,EAAE,SAAS,CAAC;IACrB,kFAAkF;IAClF,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,mCAAmC;IACnC,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,qBAAa,IAAI;IACf,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6C;IAC1E,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAkB;IACjD,OAAO,CAAC,QAAQ,CAA6D;IAC7E,OAAO,CAAC,SAAS,CAAS;gBAEd,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW;IAgBzC,WAAW,CAAC,QAAQ,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,GAAG,IAAI;IAIvF;;;OAGG;IACH,KAAK,CACH,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,IAAI;IAiBD,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAOnC,2CAA2C;IAC3C,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAwBzC,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IAQ5C,4DAA4D;IAC5D,aAAa,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI;IAkE1D,mEAAmE;IACnE,OAAO,CAAC,WAAW,CAWjB;YAEY,sBAAsB;IAkBpC;;;OAGG;IACH,OAAO,CAAC,iBAAiB,CAmBvB;IAEI,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAkB/B;AAED,MAAM,WAAW,kBAAmB,SAAQ,WAAW;IACrD,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAC1D,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,QAAQ,CAAoB;gBAExB,OAAO,EAAE,kBAAkB;IAajC,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBhD;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAQzC,IAAI,IAAI,MAAM,EAAE;IAIhB,iEAAiE;IAC3D,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAc9E,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASrC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAMhC;;;;;OAKG;IACG,SAAS,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;CAkBrC"}
@@ -0,0 +1,380 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Room manager.
6
+ *
7
+ * Each room owns:
8
+ * - one in-memory Y.Doc that all peers in the room sync against
9
+ * - a set of WebSocket connections
10
+ * - an Awareness instance forwarded over the same connections
11
+ *
12
+ * Updates are persisted via the supplied `Persistence`. Compaction kicks
13
+ * in every `compactEvery` updates per spec §12.2.
14
+ */
15
+ import { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness';
16
+ import * as syncProtocol from 'y-protocols/sync';
17
+ import * as encoding from 'lib0/encoding';
18
+ import * as decoding from 'lib0/decoding';
19
+ import * as Y from 'yjs';
20
+ import { canWrite } from './auth.js';
21
+ import { noopAuditSink, shortHash, } from './audit-log.js';
22
+ import { createRateLimiter } from './rate-limit.js';
23
+ const MESSAGE_SYNC = 0;
24
+ const MESSAGE_AWARENESS = 1;
25
+ export class Room {
26
+ id;
27
+ doc;
28
+ awareness;
29
+ conns = new Set();
30
+ persistence;
31
+ updatesSinceCompact = 0;
32
+ compactEvery;
33
+ auditSink;
34
+ rateLimitFor;
35
+ verifyMessage;
36
+ counters;
37
+ destroyed = false;
38
+ constructor(id, opts) {
39
+ this.id = id;
40
+ this.doc = new Y.Doc();
41
+ this.awareness = new Awareness(this.doc);
42
+ this.persistence = opts.persistence;
43
+ this.compactEvery = opts.compactEvery ?? 1000;
44
+ this.auditSink = opts.auditSink ?? noopAuditSink;
45
+ const rl = opts.rateLimit;
46
+ this.rateLimitFor = typeof rl === 'function' ? rl : () => rl ?? {};
47
+ this.verifyMessage = opts.verifyMessage;
48
+ this.counters = opts.counters ?? {};
49
+ this.doc.on('update', this.onDocUpdate);
50
+ this.awareness.on('update', this.onAwarenessUpdate);
51
+ }
52
+ setCounters(counters) {
53
+ this.counters = counters;
54
+ }
55
+ /**
56
+ * Append an audit-log entry for this room.
57
+ * Public so the http upgrade handler can log connect/disconnect.
58
+ */
59
+ audit(principal, opType, opHash, detail) {
60
+ void Promise.resolve(this.auditSink.append({
61
+ timestamp: new Date().toISOString(),
62
+ userId: principal.userId,
63
+ role: principal.role,
64
+ roomId: this.id,
65
+ opType,
66
+ opHash,
67
+ detail,
68
+ })).catch((err) => {
69
+ // eslint-disable-next-line no-console
70
+ console.error(`[collab-server] audit append error:`, err);
71
+ });
72
+ }
73
+ async loadFromDisk() {
74
+ const saved = await this.persistence.load(this.id);
75
+ if (saved && saved.byteLength > 0) {
76
+ Y.applyUpdate(this.doc, saved, 'load-from-disk');
77
+ }
78
+ }
79
+ /** Number of currently connected peers. */
80
+ get peerCount() {
81
+ return this.conns.size;
82
+ }
83
+ addConnection(conn) {
84
+ if (!conn.limiter) {
85
+ conn.limiter = createRateLimiter(this.rateLimitFor(conn.principal));
86
+ }
87
+ this.conns.add(conn);
88
+ this.audit(conn.principal, 'connect', '');
89
+ // Step 1 of the y-protocols sync handshake: send our state vector.
90
+ const enc = encoding.createEncoder();
91
+ encoding.writeVarUint(enc, MESSAGE_SYNC);
92
+ syncProtocol.writeSyncStep1(enc, this.doc);
93
+ safeSend(conn.ws, encoding.toUint8Array(enc));
94
+ // Send our current awareness snapshot.
95
+ const states = this.awareness.getStates();
96
+ if (states.size > 0) {
97
+ const aenc = encoding.createEncoder();
98
+ encoding.writeVarUint(aenc, MESSAGE_AWARENESS);
99
+ encoding.writeVarUint8Array(aenc, encodeAwarenessUpdate(this.awareness, Array.from(states.keys())));
100
+ safeSend(conn.ws, encoding.toUint8Array(aenc));
101
+ }
102
+ }
103
+ removeConnection(conn) {
104
+ this.conns.delete(conn);
105
+ if (conn.awarenessClients.size > 0) {
106
+ removeAwarenessStates(this.awareness, Array.from(conn.awarenessClients), 'connection-closed');
107
+ }
108
+ this.audit(conn.principal, 'disconnect', '');
109
+ }
110
+ /** Receive a binary message from a peer and dispatch it. */
111
+ handleMessage(conn, msg) {
112
+ if (this.destroyed)
113
+ return;
114
+ if (this.verifyMessage) {
115
+ const decision = this.verifyMessage(msg, conn);
116
+ if (!decision.ok) {
117
+ const reason = decision.reason ?? 'verify-failed';
118
+ this.audit(conn.principal, 'reject', shortHash(msg), { reason });
119
+ this.counters.reject?.(reason);
120
+ return;
121
+ }
122
+ }
123
+ const decoder = decoding.createDecoder(msg);
124
+ const messageType = decoding.readVarUint(decoder);
125
+ switch (messageType) {
126
+ case MESSAGE_SYNC: {
127
+ // Peek the inner sync subtype BEFORE calling readSyncMessage —
128
+ // y-protocols/sync applies the wire payload to the doc as a side
129
+ // effect for both messageYjsSyncStep2 and messageYjsUpdate. Doing
130
+ // the capability/rate-limit check after readSyncMessage means a
131
+ // viewer can mutate state (and have it broadcast and persisted by
132
+ // onDocUpdate) before this branch returns reject — the auth
133
+ // gate would be cosmetic.
134
+ const subtype = decoding.peekVarUint(decoder);
135
+ const isWriteFrame = subtype === syncProtocol.messageYjsUpdate ||
136
+ subtype === syncProtocol.messageYjsSyncStep2;
137
+ if (isWriteFrame) {
138
+ if (!canWrite(conn.principal)) {
139
+ this.audit(conn.principal, 'reject', shortHash(msg), { reason: 'role' });
140
+ this.counters.reject?.('role');
141
+ return;
142
+ }
143
+ if (conn.limiter && !conn.limiter.tryConsume(1)) {
144
+ this.audit(conn.principal, 'reject', shortHash(msg), { reason: 'rate-limit' });
145
+ this.counters.reject?.('rate-limit');
146
+ return;
147
+ }
148
+ }
149
+ const enc = encoding.createEncoder();
150
+ encoding.writeVarUint(enc, MESSAGE_SYNC);
151
+ const replyType = syncProtocol.readSyncMessage(decoder, enc, this.doc, conn);
152
+ if (replyType === syncProtocol.messageYjsUpdate) {
153
+ this.audit(conn.principal, 'update', shortHash(msg), { bytes: msg.byteLength });
154
+ this.counters.update?.();
155
+ }
156
+ else if (replyType === syncProtocol.messageYjsSyncStep1) {
157
+ this.audit(conn.principal, 'sync-step1', '');
158
+ }
159
+ else if (replyType === syncProtocol.messageYjsSyncStep2) {
160
+ this.audit(conn.principal, 'sync-step2', '');
161
+ }
162
+ if (encoding.length(enc) > 1) {
163
+ safeSend(conn.ws, encoding.toUint8Array(enc));
164
+ }
165
+ break;
166
+ }
167
+ case MESSAGE_AWARENESS: {
168
+ const update = decoding.readVarUint8Array(decoder);
169
+ applyAwarenessUpdate(this.awareness, update, conn);
170
+ this.audit(conn.principal, 'awareness', shortHash(update));
171
+ break;
172
+ }
173
+ default:
174
+ // Unknown frame; ignore.
175
+ break;
176
+ }
177
+ }
178
+ /** Forward Y updates to every other connected peer and persist. */
179
+ onDocUpdate = (update, origin) => {
180
+ if (origin === 'load-from-disk')
181
+ return;
182
+ const enc = encoding.createEncoder();
183
+ encoding.writeVarUint(enc, MESSAGE_SYNC);
184
+ syncProtocol.writeUpdate(enc, update);
185
+ const message = encoding.toUint8Array(enc);
186
+ for (const conn of this.conns) {
187
+ if (conn === origin)
188
+ continue;
189
+ safeSend(conn.ws, message);
190
+ }
191
+ void this.persistAndMaybeCompact(update);
192
+ };
193
+ async persistAndMaybeCompact(update) {
194
+ try {
195
+ await this.persistence.append(this.id, update);
196
+ this.updatesSinceCompact++;
197
+ if (this.updatesSinceCompact >= this.compactEvery) {
198
+ const merged = Y.encodeStateAsUpdate(this.doc);
199
+ await this.persistence.compact(this.id, merged);
200
+ this.updatesSinceCompact = 0;
201
+ }
202
+ }
203
+ catch (err) {
204
+ // Persistence failures are logged but never block sync — the
205
+ // in-memory state is still consistent across peers, and the next
206
+ // successful append/compact catches up.
207
+ // eslint-disable-next-line no-console
208
+ console.error(`[collab-server] persistence error for room ${this.id}:`, err);
209
+ }
210
+ }
211
+ /**
212
+ * Forward an awareness update to every other peer. Tracks which client
213
+ * IDs the connection contributed so we can clean up on disconnect.
214
+ */
215
+ onAwarenessUpdate = (changes, origin) => {
216
+ const all = [...changes.added, ...changes.updated, ...changes.removed];
217
+ if (origin && origin.ws) {
218
+ const conn = origin;
219
+ for (const client of changes.added)
220
+ conn.awarenessClients.add(client);
221
+ for (const client of changes.removed)
222
+ conn.awarenessClients.delete(client);
223
+ }
224
+ if (all.length === 0)
225
+ return;
226
+ const enc = encoding.createEncoder();
227
+ encoding.writeVarUint(enc, MESSAGE_AWARENESS);
228
+ encoding.writeVarUint8Array(enc, encodeAwarenessUpdate(this.awareness, all));
229
+ const message = encoding.toUint8Array(enc);
230
+ for (const conn of this.conns) {
231
+ if (conn === origin)
232
+ continue;
233
+ safeSend(conn.ws, message);
234
+ }
235
+ };
236
+ async destroy() {
237
+ this.destroyed = true;
238
+ for (const conn of this.conns) {
239
+ try {
240
+ conn.ws.close();
241
+ }
242
+ catch { /* socket may already be torn down */ }
243
+ }
244
+ this.conns.clear();
245
+ this.doc.off('update', this.onDocUpdate);
246
+ this.awareness.off('update', this.onAwarenessUpdate);
247
+ // Final compaction so the next load picks up the freshest state.
248
+ try {
249
+ await this.persistence.compact(this.id, Y.encodeStateAsUpdate(this.doc));
250
+ }
251
+ catch (err) {
252
+ // eslint-disable-next-line no-console
253
+ console.error(`[collab-server] compact-on-destroy error for ${this.id}:`, err);
254
+ }
255
+ this.awareness.destroy();
256
+ this.doc.destroy();
257
+ }
258
+ }
259
+ export class RoomManager {
260
+ rooms = new Map();
261
+ /** Wall-clock ms when a room last had >0 peers, or 0 if never observed. */
262
+ lastActiveAt = new Map();
263
+ options;
264
+ idleTimer = null;
265
+ counters = {};
266
+ constructor(options) {
267
+ this.options = options;
268
+ if (options.idleUnloadMs && options.idleUnloadMs > 0) {
269
+ // Sweep at half the idle window so eviction granularity is reasonable.
270
+ const sweepMs = Math.max(1_000, Math.floor(options.idleUnloadMs / 2));
271
+ this.idleTimer = setInterval(() => {
272
+ void this.sweepIdle();
273
+ }, sweepMs);
274
+ // Don't keep the Node event loop alive solely for this timer.
275
+ this.idleTimer.unref?.();
276
+ }
277
+ }
278
+ async getOrCreate(roomId) {
279
+ let pending = this.rooms.get(roomId);
280
+ if (pending) {
281
+ this.lastActiveAt.set(roomId, Date.now());
282
+ return pending;
283
+ }
284
+ const max = this.options.maxRooms ?? 1024;
285
+ if (this.rooms.size >= max) {
286
+ throw new Error(`@ifc-lite/collab-server: room limit (${max}) reached`);
287
+ }
288
+ const counters = this.counters;
289
+ const verifyMessage = this.options.verifyMessage;
290
+ pending = (async () => {
291
+ const room = new Room(roomId, { ...this.options, counters, verifyMessage });
292
+ await room.loadFromDisk();
293
+ return room;
294
+ })();
295
+ this.rooms.set(roomId, pending);
296
+ this.lastActiveAt.set(roomId, Date.now());
297
+ return pending;
298
+ }
299
+ /**
300
+ * Inject counters used by `Room.handleMessage` to bump metric values.
301
+ * The manager forwards them to every newly-loaded room.
302
+ */
303
+ setCounters(counters) {
304
+ this.counters = counters;
305
+ // Apply to already-loaded rooms via their internal hook.
306
+ for (const pending of this.rooms.values()) {
307
+ void pending.then((room) => room.setCounters(counters));
308
+ }
309
+ }
310
+ list() {
311
+ return Array.from(this.rooms.keys());
312
+ }
313
+ /** Snapshot of `(roomId, peerCount)` for diagnostics / tests. */
314
+ async stats() {
315
+ const out = [];
316
+ const now = Date.now();
317
+ for (const [roomId, pending] of this.rooms) {
318
+ const room = await pending;
319
+ out.push({
320
+ roomId,
321
+ peerCount: room.peerCount,
322
+ idleMs: now - (this.lastActiveAt.get(roomId) ?? now),
323
+ });
324
+ }
325
+ return out;
326
+ }
327
+ async unload(roomId) {
328
+ const pending = this.rooms.get(roomId);
329
+ if (!pending)
330
+ return;
331
+ this.rooms.delete(roomId);
332
+ this.lastActiveAt.delete(roomId);
333
+ const room = await pending;
334
+ await room.destroy();
335
+ }
336
+ async unloadAll() {
337
+ if (this.idleTimer)
338
+ clearInterval(this.idleTimer);
339
+ this.idleTimer = null;
340
+ await Promise.all(Array.from(this.rooms.keys()).map((id) => this.unload(id)));
341
+ }
342
+ /**
343
+ * Unload any room with zero peers that has been idle longer than
344
+ * `idleUnloadMs`. The persistence layer holds the durable copy, so
345
+ * unloading is non-destructive — the room is rehydrated on next
346
+ * connect.
347
+ */
348
+ async sweepIdle() {
349
+ const idleMs = this.options.idleUnloadMs;
350
+ if (!idleMs || idleMs <= 0)
351
+ return [];
352
+ const now = Date.now();
353
+ const candidates = [];
354
+ for (const [roomId, pending] of this.rooms) {
355
+ const room = await pending;
356
+ if (room.peerCount > 0) {
357
+ // Active rooms reset the idle clock.
358
+ this.lastActiveAt.set(roomId, now);
359
+ continue;
360
+ }
361
+ const lastActive = this.lastActiveAt.get(roomId) ?? now;
362
+ if (now - lastActive >= idleMs)
363
+ candidates.push(roomId);
364
+ }
365
+ for (const roomId of candidates)
366
+ await this.unload(roomId);
367
+ return candidates;
368
+ }
369
+ }
370
+ function safeSend(ws, data) {
371
+ try {
372
+ if (ws.readyState === ws.OPEN)
373
+ ws.send(data);
374
+ }
375
+ catch (err) {
376
+ // eslint-disable-next-line no-console
377
+ console.error('[collab-server] ws send error:', err);
378
+ }
379
+ }
380
+ //# sourceMappingURL=room-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"room-manager.js","sourceRoot":"","sources":["../src/room-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * One-call helper that bundles `createSecureHttpServer` + the
3
+ * security-header wrapper + `startCollabServer`. Useful when the
4
+ * deployer wants TLS in-process without writing the wiring.
5
+ *
6
+ * Production deployments terminating TLS at a reverse proxy keep
7
+ * using `startCollabServer` directly.
8
+ */
9
+ import { type SecureHttpServerOptions } from './secure-server.js';
10
+ import { type CollabServerHandle, type StartCollabServerOptions } from './server.js';
11
+ export interface StartSecureCollabServerOptions extends Omit<StartCollabServerOptions, 'server'> {
12
+ tls: SecureHttpServerOptions;
13
+ }
14
+ export declare function startSecureCollabServer(opts: StartSecureCollabServerOptions): Promise<CollabServerHandle>;
15
+ //# sourceMappingURL=secure-bundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure-bundle.d.ts","sourceRoot":"","sources":["../src/secure-bundle.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AAEH,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,EAC9B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,8BACf,SAAQ,IAAI,CAAC,wBAAwB,EAAE,QAAQ,CAAC;IAChD,GAAG,EAAE,uBAAuB,CAAC;CAC9B;AAED,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,8BAA8B,GACnC,OAAO,CAAC,kBAAkB,CAAC,CAwC7B"}
@@ -0,0 +1,49 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * One-call helper that bundles `createSecureHttpServer` + the
6
+ * security-header wrapper + `startCollabServer`. Useful when the
7
+ * deployer wants TLS in-process without writing the wiring.
8
+ *
9
+ * Production deployments terminating TLS at a reverse proxy keep
10
+ * using `startCollabServer` directly.
11
+ */
12
+ import { createSecureHttpServer, secureHttpHandler, } from './secure-server.js';
13
+ import { startCollabServer, } from './server.js';
14
+ export async function startSecureCollabServer(opts) {
15
+ // Build the underlying https.Server with hardened defaults. The
16
+ // collab server attaches its request listener inside startCollabServer.
17
+ const httpsServer = createSecureHttpServer({ ...opts.tls });
18
+ const handle = await startCollabServer({
19
+ ...opts,
20
+ server: httpsServer,
21
+ });
22
+ // After startCollabServer has attached its listeners, wrap each one
23
+ // with the OWASP security-header wrapper so every response gets
24
+ // hardened headers without each route opting in. This is more robust
25
+ // than patching `emit` (which has a window where listeners can fire
26
+ // before the wrapper is in place) and works even if multiple
27
+ // listeners were attached.
28
+ const requestListeners = httpsServer.listeners('request');
29
+ if (requestListeners.length > 0) {
30
+ httpsServer.removeAllListeners('request');
31
+ for (const listener of requestListeners) {
32
+ httpsServer.on('request', secureHttpHandler(listener));
33
+ }
34
+ }
35
+ // startCollabServer skips listening whenever a server is supplied,
36
+ // so this helper always has to bind. The previous gate
37
+ // `if (!opts.port && !opts.host)` skipped listen() any time the
38
+ // caller passed only one of the two (e.g. just `port: 4000`),
39
+ // returning a handle for an unbound server.
40
+ await new Promise((resolve, reject) => {
41
+ httpsServer.once('error', reject);
42
+ httpsServer.listen(opts.port ?? 1234, opts.host ?? '0.0.0.0', () => {
43
+ httpsServer.off('error', reject);
44
+ resolve();
45
+ });
46
+ });
47
+ return handle;
48
+ }
49
+ //# sourceMappingURL=secure-bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure-bundle.js","sourceRoot":"","sources":["../src/secure-bundle.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;GAOG;AAEH,OAAO,EACL,sBAAsB,EACtB,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,iBAAiB,GAGlB,MAAM,aAAa,CAAC;AAOrB,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,IAAoC;IAEpC,gEAAgE;IAChE,wEAAwE;IACxE,MAAM,WAAW,GAAG,sBAAsB,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAE5D,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC;QACrC,GAAG,IAAI;QACP,MAAM,EAAE,WAAW;KACpB,CAAC,CAAC;IAEH,oEAAoE;IACpE,gEAAgE;IAChE,qEAAqE;IACrE,oEAAoE;IACpE,6DAA6D;IAC7D,2BAA2B;IAC3B,MAAM,gBAAgB,GAAG,WAAW,CAAC,SAAS,CAAC,SAAS,CAEvD,CAAC;IACF,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,WAAW,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;QAC1C,KAAK,MAAM,QAAQ,IAAI,gBAAgB,EAAE,CAAC;YACxC,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED,mEAAmE;IACnE,uDAAuD;IACvD,gEAAgE;IAChE,8DAA8D;IAC9D,4CAA4C;IAC5C,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAClC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,SAAS,EAAE,GAAG,EAAE;YACjE,WAAW,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACjC,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}