@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
package/dist/bin.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ /* This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
5
+ /** CLI entry point: `ifc-lite-collab-server`. */
6
+ import { FilePersistence, startCollabServer } from './server.js';
7
+ const port = Number(process.env.COLLAB_PORT ?? 1234);
8
+ const host = process.env.COLLAB_HOST ?? '0.0.0.0';
9
+ const dataDir = process.env.COLLAB_DATA_DIR ?? './.collab-data';
10
+ const maxRooms = Number(process.env.COLLAB_MAX_ROOMS ?? 1024);
11
+ async function main() {
12
+ const handle = await startCollabServer({
13
+ port,
14
+ host,
15
+ persistence: new FilePersistence({ dataDir }),
16
+ maxRooms,
17
+ });
18
+ // eslint-disable-next-line no-console
19
+ console.log(`[collab-server] listening at ${handle.url} (data: ${dataDir})`);
20
+ const shutdown = async () => {
21
+ // eslint-disable-next-line no-console
22
+ console.log('[collab-server] shutting down…');
23
+ await handle.stop();
24
+ process.exit(0);
25
+ };
26
+ process.once('SIGINT', shutdown);
27
+ process.once('SIGTERM', shutdown);
28
+ }
29
+ main().catch((err) => {
30
+ // eslint-disable-next-line no-console
31
+ console.error('[collab-server] fatal:', err);
32
+ process.exit(1);
33
+ });
34
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA;;+DAE+D;AAE/D,iDAAiD;AAEjD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEjE,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC;AACrD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,SAAS,CAAC;AAClD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,gBAAgB,CAAC;AAChE,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,IAAI,CAAC,CAAC;AAE9D,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC;QACrC,IAAI;QACJ,IAAI;QACJ,WAAW,EAAE,IAAI,eAAe,CAAC,EAAE,OAAO,EAAE,CAAC;QAC7C,QAAQ;KACT,CAAC,CAAC;IACH,sCAAsC;IACtC,OAAO,CAAC,GAAG,CAAC,gCAAgC,MAAM,CAAC,GAAG,WAAW,OAAO,GAAG,CAAC,CAAC;IAE7E,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,sCAAsC;QACtC,OAAO,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;QAC9C,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AACpC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAC;IAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * HTTP blob route (spec §11.1 + §11.3).
3
+ *
4
+ * Mounts `/blobs/<hash>` (PUT/GET/HEAD/DELETE) and `/blobs` (GET list)
5
+ * on the supplied http server. Storage is pluggable so deployments can
6
+ * back this with S3, GCS, local disk, or in-memory for tests.
7
+ *
8
+ * Authentication is intentionally NOT included here — the existing
9
+ * `AuthenticateFn` only sees websocket upgrades. Production deployments
10
+ * should layer their own JWT validation in front of this route (e.g. a
11
+ * reverse proxy). For dev/tests we accept anonymous traffic.
12
+ */
13
+ import * as http from 'node:http';
14
+ export interface ServerBlobMeta {
15
+ hash: string;
16
+ byteLength: number;
17
+ contentType?: string;
18
+ uploadedAt: string;
19
+ }
20
+ export interface ServerBlobStorage {
21
+ put(hash: string, bytes: Uint8Array, contentType?: string): Promise<ServerBlobMeta>;
22
+ get(hash: string): Promise<{
23
+ bytes: Uint8Array;
24
+ meta: ServerBlobMeta;
25
+ } | null>;
26
+ has(hash: string): Promise<boolean>;
27
+ delete(hash: string): Promise<boolean>;
28
+ list(): Promise<string[]>;
29
+ }
30
+ /** In-memory storage. Default for tests + dev. */
31
+ export declare class InMemoryBlobStorage implements ServerBlobStorage {
32
+ private readonly blobs;
33
+ put(hash: string, bytes: Uint8Array, contentType?: string): Promise<ServerBlobMeta>;
34
+ get(hash: string): Promise<{
35
+ bytes: Uint8Array<ArrayBuffer>;
36
+ meta: ServerBlobMeta;
37
+ } | null>;
38
+ has(hash: string): Promise<boolean>;
39
+ delete(hash: string): Promise<boolean>;
40
+ list(): Promise<string[]>;
41
+ }
42
+ export interface BlobRouteOptions {
43
+ storage: ServerBlobStorage;
44
+ /** Reject PUTs over this size (default 100 MB). */
45
+ maxBytes?: number;
46
+ /**
47
+ * If set, server recomputes a sha256 of the body and 409s when it
48
+ * doesn't match the URL hash. Defaults to `false` because clients use
49
+ * `fnv128` which is not sha256-compatible.
50
+ */
51
+ verifySha256?: boolean;
52
+ }
53
+ /**
54
+ * Route handler for `/blobs/...` requests. Returns `true` if the
55
+ * request was handled (the caller should not write its own response).
56
+ */
57
+ export declare function handleBlobRequest(req: http.IncomingMessage, res: http.ServerResponse, opts: BlobRouteOptions): Promise<boolean> | boolean;
58
+ //# sourceMappingURL=blob-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob-route.d.ts","sourceRoot":"","sources":["../src/blob-route.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACpF,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,IAAI,EAAE,cAAc,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC/E,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC3B;AAED,kDAAkD;AAClD,qBAAa,mBAAoB,YAAW,iBAAiB;IAC3D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkE;IAElF,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAUnF,GAAG,CAAC,IAAI,EAAE,MAAM;;;;IAIhB,GAAG,CAAC,IAAI,EAAE,MAAM;IAGhB,MAAM,CAAC,IAAI,EAAE,MAAM;IAGnB,IAAI;CAGX;AAKD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAsC5B"}
@@ -0,0 +1,132 @@
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
+ import * as crypto from 'node:crypto';
5
+ /** In-memory storage. Default for tests + dev. */
6
+ export class InMemoryBlobStorage {
7
+ blobs = new Map();
8
+ async put(hash, bytes, contentType) {
9
+ const meta = {
10
+ hash,
11
+ byteLength: bytes.byteLength,
12
+ contentType,
13
+ uploadedAt: new Date().toISOString(),
14
+ };
15
+ this.blobs.set(hash, { bytes: new Uint8Array(bytes), meta });
16
+ return meta;
17
+ }
18
+ async get(hash) {
19
+ const v = this.blobs.get(hash);
20
+ return v ? { bytes: new Uint8Array(v.bytes), meta: v.meta } : null;
21
+ }
22
+ async has(hash) {
23
+ return this.blobs.has(hash);
24
+ }
25
+ async delete(hash) {
26
+ return this.blobs.delete(hash);
27
+ }
28
+ async list() {
29
+ return Array.from(this.blobs.keys());
30
+ }
31
+ }
32
+ /** Match exactly 32 lowercase hex chars (the client's `fnv128` output). */
33
+ const HASH_REGEX = /^[a-f0-9]{32}$/;
34
+ /**
35
+ * Route handler for `/blobs/...` requests. Returns `true` if the
36
+ * request was handled (the caller should not write its own response).
37
+ */
38
+ export function handleBlobRequest(req, res, opts) {
39
+ const url = new URL(req.url ?? '/', 'http://localhost');
40
+ if (!url.pathname.startsWith('/blobs'))
41
+ return false;
42
+ if (url.pathname === '/blobs' || url.pathname === '/blobs/') {
43
+ if (req.method === 'GET') {
44
+ return opts.storage.list().then((hashes) => {
45
+ res.writeHead(200, { 'content-type': 'application/json' });
46
+ res.end(JSON.stringify({ hashes }));
47
+ return true;
48
+ });
49
+ }
50
+ res.writeHead(405);
51
+ res.end();
52
+ return true;
53
+ }
54
+ const hash = url.pathname.slice('/blobs/'.length);
55
+ if (!HASH_REGEX.test(hash)) {
56
+ res.writeHead(400, { 'content-type': 'application/json' });
57
+ res.end(JSON.stringify({ error: 'invalid-hash' }));
58
+ return true;
59
+ }
60
+ switch (req.method) {
61
+ case 'PUT':
62
+ return handlePut(req, res, hash, opts);
63
+ case 'GET':
64
+ return handleGet(res, hash, opts);
65
+ case 'HEAD':
66
+ return handleHead(res, hash, opts);
67
+ case 'DELETE':
68
+ return handleDelete(res, hash, opts);
69
+ default:
70
+ res.writeHead(405);
71
+ res.end();
72
+ return true;
73
+ }
74
+ }
75
+ async function handlePut(req, res, hash, opts) {
76
+ const max = opts.maxBytes ?? 100 * 1024 * 1024;
77
+ const chunks = [];
78
+ let total = 0;
79
+ for await (const chunk of req) {
80
+ const buf = chunk;
81
+ total += buf.byteLength;
82
+ if (total > max) {
83
+ res.writeHead(413, { 'content-type': 'application/json' });
84
+ res.end(JSON.stringify({ error: 'payload-too-large', max }));
85
+ req.destroy();
86
+ return true;
87
+ }
88
+ chunks.push(buf);
89
+ }
90
+ const body = Buffer.concat(chunks);
91
+ if (opts.verifySha256) {
92
+ const sha = crypto.createHash('sha256').update(body).digest('hex');
93
+ if (sha !== hash) {
94
+ res.writeHead(409, { 'content-type': 'application/json' });
95
+ res.end(JSON.stringify({ error: 'hash-mismatch', expected: hash, computed: sha }));
96
+ return true;
97
+ }
98
+ }
99
+ const contentType = req.headers['content-type'] ?? undefined;
100
+ const meta = await opts.storage.put(hash, body, contentType);
101
+ res.writeHead(201, { 'content-type': 'application/json' });
102
+ res.end(JSON.stringify(meta));
103
+ return true;
104
+ }
105
+ async function handleGet(res, hash, opts) {
106
+ const v = await opts.storage.get(hash);
107
+ if (!v) {
108
+ res.writeHead(404);
109
+ res.end();
110
+ return true;
111
+ }
112
+ res.writeHead(200, {
113
+ 'content-type': v.meta.contentType ?? 'application/octet-stream',
114
+ 'content-length': String(v.meta.byteLength),
115
+ 'x-blob-hash': v.meta.hash,
116
+ });
117
+ res.end(Buffer.from(v.bytes));
118
+ return true;
119
+ }
120
+ async function handleHead(res, hash, opts) {
121
+ const exists = await opts.storage.has(hash);
122
+ res.writeHead(exists ? 200 : 404);
123
+ res.end();
124
+ return true;
125
+ }
126
+ async function handleDelete(res, hash, opts) {
127
+ const ok = await opts.storage.delete(hash);
128
+ res.writeHead(ok ? 204 : 404);
129
+ res.end();
130
+ return true;
131
+ }
132
+ //# sourceMappingURL=blob-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob-route.js","sourceRoot":"","sources":["../src/blob-route.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export { startCollabServer, FilePersistence, MemoryPersistence, type StartCollabServerOptions, type CollabServerHandle, } from './server.js';
2
+ export { type Persistence, type FilePersistenceOptions, } from './persistence.js';
3
+ export { S3Persistence, type S3PersistenceOptions, type S3LikeClient, type S3Commands, } from './persistence-s3.js';
4
+ export { RedisPersistence, type RedisPersistenceOptions, type RedisLikeClient, } from './persistence-redis.js';
5
+ export { type AuthenticateFn, type Principal, type Role, allowAnonymousEditor, denyAll, canWrite, } from './auth.js';
6
+ export { Room, RoomManager, type RoomOptions, type RoomManagerOptions, type PeerConnection, } from './room-manager.js';
7
+ export { MemoryAuditSink, JsonlFileAuditSink, noopAuditSink, shortHash, type AuditEntry, type AuditOpType, type AuditSink, type JsonlFileAuditSinkOptions, } from './audit-log.js';
8
+ export { DEFAULT_RETENTION, applyRetention, planRetention, type RetentionDecision, type RetentionPolicy, } from './retention.js';
9
+ export { SnapshotWorker, type SnapshotWorkerOptions, type SnapshotResult, } from './snapshot-worker.js';
10
+ export { MetricsRegistry, defaultMetrics, type LabelValues, } from './metrics.js';
11
+ export { computeHmac, createReplayProtector, decodeSignedFrame, encodeSignedFrame, verifyWithReplayProtector, type ReplayDecision, type ReplayProtector, type ReplayProtectorOptions, type UpdateEnvelope, } from './replay-protect.js';
12
+ export { type VerifyDecision, type VerifyMessageFn, } from './room-manager.js';
13
+ export { applySecurityHeaders, createSecureHttpServer, secureHttpHandler, type SecureHttpServerOptions, } from './secure-server.js';
14
+ export { startSecureCollabServer, type StartSecureCollabServerOptions, } from './secure-bundle.js';
15
+ export { createPathLockRegistry, harvestUpdatePaths, verifyAgainstPathLocks, type PathLock, type PathLockRegistry, } from './path-locks.js';
16
+ export { createRateLimiter, type RateLimitOptions, type RateLimiter, } from './rate-limit.js';
17
+ export { InMemoryBlobStorage, handleBlobRequest, type ServerBlobStorage, type ServerBlobMeta, type BlobRouteOptions, } from './blob-route.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,sBAAsB,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,UAAU,GAChB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,eAAe,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,IAAI,EACT,oBAAoB,EACpB,OAAO,EACP,QAAQ,GACT,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,IAAI,EACJ,WAAW,EACX,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,cAAc,GACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,SAAS,EACT,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,yBAAyB,GAC/B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,cAAc,EACd,KAAK,qBAAqB,EAC1B,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,eAAe,EACf,cAAc,EACd,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,iBAAiB,EACjB,KAAK,uBAAuB,GAC7B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EACvB,KAAK,8BAA8B,GACpC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,QAAQ,EACb,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
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
+ export { startCollabServer, FilePersistence, MemoryPersistence, } from './server.js';
5
+ export { S3Persistence, } from './persistence-s3.js';
6
+ export { RedisPersistence, } from './persistence-redis.js';
7
+ export { allowAnonymousEditor, denyAll, canWrite, } from './auth.js';
8
+ export { Room, RoomManager, } from './room-manager.js';
9
+ export { MemoryAuditSink, JsonlFileAuditSink, noopAuditSink, shortHash, } from './audit-log.js';
10
+ export { DEFAULT_RETENTION, applyRetention, planRetention, } from './retention.js';
11
+ export { SnapshotWorker, } from './snapshot-worker.js';
12
+ export { MetricsRegistry, defaultMetrics, } from './metrics.js';
13
+ export { computeHmac, createReplayProtector, decodeSignedFrame, encodeSignedFrame, verifyWithReplayProtector, } from './replay-protect.js';
14
+ export { applySecurityHeaders, createSecureHttpServer, secureHttpHandler, } from './secure-server.js';
15
+ export { startSecureCollabServer, } from './secure-bundle.js';
16
+ export { createPathLockRegistry, harvestUpdatePaths, verifyAgainstPathLocks, } from './path-locks.js';
17
+ export { createRateLimiter, } from './rate-limit.js';
18
+ export { InMemoryBlobStorage, handleBlobRequest, } from './blob-route.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,iBAAiB,GAGlB,MAAM,aAAa,CAAC;AAKrB,OAAO,EACL,aAAa,GAId,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gBAAgB,GAGjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAIL,oBAAoB,EACpB,OAAO,EACP,QAAQ,GACT,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,IAAI,EACJ,WAAW,GAIZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,SAAS,GAKV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,aAAa,GAGd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,cAAc,GAGf,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,eAAe,EACf,cAAc,GAEf,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,yBAAyB,GAK1B,MAAM,qBAAqB,CAAC;AAK7B,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,GAExB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,sBAAsB,GAGvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iBAAiB,GAGlB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,GAIlB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Lightweight metrics registry (spec §15 §14.4).
3
+ *
4
+ * Prometheus-text-format compatible — apps can scrape `/metrics` and
5
+ * point Grafana / Datadog / etc. at it. Metric types are intentionally
6
+ * minimal:
7
+ * - counters (monotonic)
8
+ * - gauges (set/inc/dec to any value)
9
+ * - histograms via simple sum + count (we don't ship buckets — those
10
+ * come in v0.5+)
11
+ *
12
+ * Built dependency-free so we can swap for prom-client later without an
13
+ * API churn.
14
+ */
15
+ export type LabelValues = Record<string, string>;
16
+ export declare class MetricsRegistry {
17
+ private readonly metrics;
18
+ private ensure;
19
+ /** Register or fetch a counter. Counters are monotonic (only `inc`). */
20
+ counter(name: string, help: string): {
21
+ inc: (n?: number, labels?: LabelValues) => void;
22
+ get: (labels?: LabelValues) => number;
23
+ };
24
+ /** Register or fetch a gauge. Gauges can move in any direction. */
25
+ gauge(name: string, help: string): {
26
+ set: (value: number, labels?: LabelValues) => void;
27
+ inc: (n?: number, labels?: LabelValues) => void;
28
+ dec: (n?: number, labels?: LabelValues) => void;
29
+ get: (labels?: LabelValues) => number;
30
+ };
31
+ /**
32
+ * Tiny histogram: tracks `sum` and `count`. Apps can compute average
33
+ * client-side. Real bucketed histograms are also available via
34
+ * `bucketedHistogram(name, buckets, help)`.
35
+ */
36
+ histogram(name: string, help: string): {
37
+ observe: (value: number, labels?: LabelValues) => void;
38
+ mean: (labels?: LabelValues) => number;
39
+ };
40
+ /**
41
+ * Prometheus-style bucketed histogram. `buckets` are upper bounds
42
+ * (e.g. `[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]`). Each
43
+ * observation increments every bucket whose upper bound it doesn't
44
+ * exceed, plus `sum` and `count`. Render output uses the
45
+ * conventional `<name>_bucket{le="<bound>"}` labels.
46
+ */
47
+ bucketedHistogram(name: string, buckets: readonly number[], help: string): {
48
+ observe: (value: number, labels?: LabelValues) => void;
49
+ buckets: number[];
50
+ };
51
+ /** Render the full registry as Prometheus text format. */
52
+ render(): string;
53
+ }
54
+ /** Default registry shared by the server. Apps can inject their own. */
55
+ export declare const defaultMetrics: MetricsRegistry;
56
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAoBjD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAE1D,OAAO,CAAC,MAAM;IAuBd,wEAAwE;IACxE,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;mCAGT,WAAW;uBAYlB,WAAW;;IAI7B,mEAAmE;IACnE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;qBAGf,MAAM,WAAU,WAAW;mCAGnB,WAAW;mCAIX,WAAW;uBAIlB,WAAW;;IAI7B;;;;OAIG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;yBAMf,MAAM,WAAU,WAAW;wBAK7B,WAAW;;IAS9B;;;;;;OAMG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM;yBAsBnD,MAAM,WAAU,WAAW;;;IAkBhD,0DAA0D;IAC1D,MAAM,IAAI,MAAM;CAsBjB;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,iBAAwB,CAAC"}
@@ -0,0 +1,161 @@
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
+ function labelKey(labels) {
5
+ const keys = Object.keys(labels).sort();
6
+ if (keys.length === 0)
7
+ return '';
8
+ return keys.map((k) => `${k}="${escapeLabel(labels[k])}"`).join(',');
9
+ }
10
+ function escapeLabel(value) {
11
+ return value.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
12
+ }
13
+ export class MetricsRegistry {
14
+ metrics = new Map();
15
+ ensure(name, type, help) {
16
+ let m = this.metrics.get(name);
17
+ if (!m) {
18
+ m = { type, help, values: new Map() };
19
+ this.metrics.set(name, m);
20
+ return m;
21
+ }
22
+ // Silently reusing a name with a different type/help corrupts the
23
+ // metric family (mismatched HELP/TYPE lines on render, ambiguous
24
+ // semantics for consumers). Surface re-registration loudly.
25
+ if (m.type !== type) {
26
+ throw new Error(`@ifc-lite/collab-server: metric "${name}" already registered as ${m.type}, cannot redefine as ${type}`);
27
+ }
28
+ if (m.help !== help) {
29
+ throw new Error(`@ifc-lite/collab-server: metric "${name}" already registered with different help text`);
30
+ }
31
+ return m;
32
+ }
33
+ /** Register or fetch a counter. Counters are monotonic (only `inc`). */
34
+ counter(name, help) {
35
+ const m = this.ensure(name, 'counter', help);
36
+ return {
37
+ inc: (n = 1, labels = {}) => {
38
+ // Counters are monotonic by definition. Negative/NaN/Infinity
39
+ // increments silently corrupt the series and downstream
40
+ // rate(...) calculations.
41
+ if (!Number.isFinite(n) || n < 0) {
42
+ throw new Error(`@ifc-lite/collab-server: counter "${name}" increment must be a finite non-negative number (got ${n})`);
43
+ }
44
+ const key = labelKey(labels);
45
+ m.values.set(key, (m.values.get(key) ?? 0) + n);
46
+ },
47
+ get: (labels = {}) => m.values.get(labelKey(labels)) ?? 0,
48
+ };
49
+ }
50
+ /** Register or fetch a gauge. Gauges can move in any direction. */
51
+ gauge(name, help) {
52
+ const m = this.ensure(name, 'gauge', help);
53
+ return {
54
+ set: (value, labels = {}) => {
55
+ m.values.set(labelKey(labels), value);
56
+ },
57
+ inc: (n = 1, labels = {}) => {
58
+ const key = labelKey(labels);
59
+ m.values.set(key, (m.values.get(key) ?? 0) + n);
60
+ },
61
+ dec: (n = 1, labels = {}) => {
62
+ const key = labelKey(labels);
63
+ m.values.set(key, (m.values.get(key) ?? 0) - n);
64
+ },
65
+ get: (labels = {}) => m.values.get(labelKey(labels)) ?? 0,
66
+ };
67
+ }
68
+ /**
69
+ * Tiny histogram: tracks `sum` and `count`. Apps can compute average
70
+ * client-side. Real bucketed histograms are also available via
71
+ * `bucketedHistogram(name, buckets, help)`.
72
+ */
73
+ histogram(name, help) {
74
+ const sumName = `${name}_sum`;
75
+ const countName = `${name}_count`;
76
+ const sum = this.ensure(sumName, 'histogram-sum', help);
77
+ const count = this.ensure(countName, 'histogram-count', help);
78
+ return {
79
+ observe: (value, labels = {}) => {
80
+ const key = labelKey(labels);
81
+ sum.values.set(key, (sum.values.get(key) ?? 0) + value);
82
+ count.values.set(key, (count.values.get(key) ?? 0) + 1);
83
+ },
84
+ mean: (labels = {}) => {
85
+ const key = labelKey(labels);
86
+ const c = count.values.get(key) ?? 0;
87
+ if (c === 0)
88
+ return 0;
89
+ return (sum.values.get(key) ?? 0) / c;
90
+ },
91
+ };
92
+ }
93
+ /**
94
+ * Prometheus-style bucketed histogram. `buckets` are upper bounds
95
+ * (e.g. `[5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]`). Each
96
+ * observation increments every bucket whose upper bound it doesn't
97
+ * exceed, plus `sum` and `count`. Render output uses the
98
+ * conventional `<name>_bucket{le="<bound>"}` labels.
99
+ */
100
+ bucketedHistogram(name, buckets, help) {
101
+ if (buckets.length === 0) {
102
+ throw new Error(`@ifc-lite/collab-server: bucketedHistogram requires at least one bucket`);
103
+ }
104
+ // Dedupe before sorting — duplicate bounds increment the same `le`
105
+ // bucket series twice per observation, throwing off bucket counts
106
+ // and Prometheus's `histogram_quantile` math.
107
+ const sortedBuckets = [...new Set(buckets)].sort((a, b) => a - b);
108
+ if (sortedBuckets.some((b) => !Number.isFinite(b))) {
109
+ throw new Error(`@ifc-lite/collab-server: bucketedHistogram "${name}" buckets must be finite numbers`);
110
+ }
111
+ const sumName = `${name}_sum`;
112
+ const countName = `${name}_count`;
113
+ const bucketName = `${name}_bucket`;
114
+ const sum = this.ensure(sumName, 'histogram-sum', help);
115
+ const count = this.ensure(countName, 'histogram-count', help);
116
+ const bucketEntry = this.ensure(bucketName, 'histogram-bucket', help);
117
+ bucketEntry.buckets = sortedBuckets;
118
+ return {
119
+ observe: (value, labels = {}) => {
120
+ const key = labelKey(labels);
121
+ sum.values.set(key, (sum.values.get(key) ?? 0) + value);
122
+ count.values.set(key, (count.values.get(key) ?? 0) + 1);
123
+ for (const b of sortedBuckets) {
124
+ if (value <= b) {
125
+ const bucketKey = labelKey({ ...labels, le: String(b) });
126
+ bucketEntry.values.set(bucketKey, (bucketEntry.values.get(bucketKey) ?? 0) + 1);
127
+ }
128
+ }
129
+ // +Inf bucket — every observation counts.
130
+ const infKey = labelKey({ ...labels, le: '+Inf' });
131
+ bucketEntry.values.set(infKey, (bucketEntry.values.get(infKey) ?? 0) + 1);
132
+ },
133
+ buckets: sortedBuckets,
134
+ };
135
+ }
136
+ /** Render the full registry as Prometheus text format. */
137
+ render() {
138
+ const lines = [];
139
+ for (const [name, m] of this.metrics) {
140
+ lines.push(`# HELP ${name} ${m.help}`);
141
+ const renderedType = m.type === 'counter'
142
+ ? 'counter'
143
+ : m.type === 'histogram-bucket'
144
+ ? 'histogram'
145
+ : 'gauge';
146
+ lines.push(`# TYPE ${name} ${renderedType}`);
147
+ if (m.values.size === 0) {
148
+ lines.push(`${name} 0`);
149
+ continue;
150
+ }
151
+ for (const [key, value] of m.values) {
152
+ const renderedKey = key.length > 0 ? `{${key}}` : '';
153
+ lines.push(`${name}${renderedKey} ${value}`);
154
+ }
155
+ }
156
+ return lines.join('\n') + '\n';
157
+ }
158
+ }
159
+ /** Default registry shared by the server. Apps can inject their own. */
160
+ export const defaultMetrics = new MetricsRegistry();
161
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { Principal, Role } from './auth.js';
2
+ import type { VerifyMessageFn } from './room-manager.js';
3
+ export interface PathLock {
4
+ /** Prefix that triggers this lock. Either `entities/<path-prefix>` or `geometry/<id-prefix>` etc. */
5
+ prefix: string;
6
+ /** Optional human-readable label for audit logs. */
7
+ label?: string;
8
+ /** Allow these userIds to bypass the lock. */
9
+ exemptUserIds?: ReadonlySet<string>;
10
+ /** Allow these roles to bypass the lock (e.g. the admin that placed it). */
11
+ exemptRoles?: ReadonlySet<Role>;
12
+ }
13
+ export interface PathLockRegistry {
14
+ /** Snapshot of currently-active locks. */
15
+ list(): readonly PathLock[];
16
+ /** Add a lock. Returns the lock for the caller to keep a reference to. */
17
+ add(lock: PathLock): PathLock;
18
+ /** Remove a previously-added lock. */
19
+ remove(lock: PathLock): boolean;
20
+ /** Drop every lock. */
21
+ clear(): void;
22
+ /** Returns the matching lock if `path` is locked AND `principal` is not exempt. */
23
+ matches(path: string, principal: Principal): PathLock | null;
24
+ }
25
+ export declare function createPathLockRegistry(): PathLockRegistry;
26
+ /**
27
+ * Apply `update` to a throwaway Y.Doc and harvest the touched paths
28
+ * as a list of `<top>/<key>` strings (`entities/wall`, `geometry/g1`,
29
+ * etc.). Each top-level shared type the runtime knows about is
30
+ * inspected.
31
+ */
32
+ export declare function harvestUpdatePaths(update: Uint8Array): string[];
33
+ /**
34
+ * Build a `VerifyMessageFn` that rejects sync-update messages whose
35
+ * harvested paths intersect a locked prefix (and whose principal
36
+ * isn't exempt). Pass-through for non-update frames.
37
+ */
38
+ export declare function verifyAgainstPathLocks(registry: PathLockRegistry): VerifyMessageFn;
39
+ //# sourceMappingURL=path-locks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-locks.d.ts","sourceRoot":"","sources":["../src/path-locks.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAkB,eAAe,EAAkB,MAAM,mBAAmB,CAAC;AAIzF,MAAM,WAAW,QAAQ;IACvB,qGAAqG;IACrG,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,4EAA4E;IAC5E,WAAW,CAAC,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,0CAA0C;IAC1C,IAAI,IAAI,SAAS,QAAQ,EAAE,CAAC;IAC5B,0EAA0E;IAC1E,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC9B,sCAAsC;IACtC,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IAChC,uBAAuB;IACvB,KAAK,IAAI,IAAI,CAAC;IACd,mFAAmF;IACnF,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC;CAC9D;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,CA4BzD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAwC/D;AASD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,eAAe,CAgDlF"}