@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.
- package/LICENSE +373 -0
- package/README.md +47 -0
- package/dist/audit-log.d.ts +77 -0
- package/dist/audit-log.d.ts.map +1 -0
- package/dist/audit-log.js +105 -0
- package/dist/audit-log.js.map +1 -0
- package/dist/auth.d.ts +24 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +15 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +34 -0
- package/dist/bin.js.map +1 -0
- package/dist/blob-route.d.ts +58 -0
- package/dist/blob-route.d.ts.map +1 -0
- package/dist/blob-route.js +132 -0
- package/dist/blob-route.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics.d.ts +56 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +161 -0
- package/dist/metrics.js.map +1 -0
- package/dist/path-locks.d.ts +39 -0
- package/dist/path-locks.d.ts.map +1 -0
- package/dist/path-locks.js +176 -0
- package/dist/path-locks.js.map +1 -0
- package/dist/persistence-redis.d.ts +52 -0
- package/dist/persistence-redis.d.ts.map +1 -0
- package/dist/persistence-redis.js +50 -0
- package/dist/persistence-redis.js.map +1 -0
- package/dist/persistence-s3.d.ts +81 -0
- package/dist/persistence-s3.d.ts.map +1 -0
- package/dist/persistence-s3.js +183 -0
- package/dist/persistence-s3.js.map +1 -0
- package/dist/persistence.d.ts +41 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +108 -0
- package/dist/persistence.js.map +1 -0
- package/dist/rate-limit.d.ts +29 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +36 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/replay-protect.d.ts +51 -0
- package/dist/replay-protect.d.ts.map +1 -0
- package/dist/replay-protect.js +134 -0
- package/dist/replay-protect.js.map +1 -0
- package/dist/retention.d.ts +32 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +112 -0
- package/dist/retention.js.map +1 -0
- package/dist/room-manager.d.ts +138 -0
- package/dist/room-manager.d.ts.map +1 -0
- package/dist/room-manager.js +380 -0
- package/dist/room-manager.js.map +1 -0
- package/dist/secure-bundle.d.ts +15 -0
- package/dist/secure-bundle.d.ts.map +1 -0
- package/dist/secure-bundle.js +49 -0
- package/dist/secure-bundle.js.map +1 -0
- package/dist/secure-server.d.ts +49 -0
- package/dist/secure-server.d.ts.map +1 -0
- package/dist/secure-server.js +86 -0
- package/dist/secure-server.js.map +1 -0
- package/dist/server.d.ts +59 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +196 -0
- package/dist/server.js.map +1 -0
- package/dist/snapshot-worker.d.ts +32 -0
- package/dist/snapshot-worker.d.ts.map +1 -0
- package/dist/snapshot-worker.js +87 -0
- package/dist/snapshot-worker.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLS termination helpers (spec §14, v0.5).
|
|
3
|
+
*
|
|
4
|
+
* In production we recommend terminating TLS at a reverse proxy
|
|
5
|
+
* (nginx, Caddy, ALB, Cloudflare, etc.). For deployments that need
|
|
6
|
+
* TLS in-process — single-binary appliance, edge worker, dev with a
|
|
7
|
+
* self-signed cert — this module ships:
|
|
8
|
+
*
|
|
9
|
+
* - `createSecureHttpServer(opts)` that wraps `node:https`'s
|
|
10
|
+
* `createServer` with strong defaults (TLS 1.2+ minVersion,
|
|
11
|
+
* conservative cipher list, ALPN for HTTP/1.1).
|
|
12
|
+
*
|
|
13
|
+
* - `applySecurityHeaders(res)` that sets the OWASP-recommended
|
|
14
|
+
* baseline response headers (HSTS, no-sniff, frame deny). Drop
|
|
15
|
+
* this into the http handler before writing any response.
|
|
16
|
+
*
|
|
17
|
+
* - `secureHttpHandler(inner)` wrapper that applies the headers and
|
|
18
|
+
* defends against the classic TRACE method.
|
|
19
|
+
*
|
|
20
|
+
* Apps that already terminate TLS upstream can ignore this module —
|
|
21
|
+
* `startCollabServer` accepts a `server` option so they can pass any
|
|
22
|
+
* pre-built `http.Server` (or `https.Server`).
|
|
23
|
+
*/
|
|
24
|
+
import * as https from 'node:https';
|
|
25
|
+
import type * as http from 'node:http';
|
|
26
|
+
export interface SecureHttpServerOptions {
|
|
27
|
+
/** Path to the TLS certificate file (PEM). */
|
|
28
|
+
certPath: string;
|
|
29
|
+
/** Path to the TLS private key file (PEM). */
|
|
30
|
+
keyPath: string;
|
|
31
|
+
/** Optional CA bundle for client certificate verification. */
|
|
32
|
+
caPath?: string;
|
|
33
|
+
/** Reject TLS below this version. Default `'TLSv1.2'`. */
|
|
34
|
+
minVersion?: 'TLSv1.2' | 'TLSv1.3';
|
|
35
|
+
/** Override cipher list. Default modern AES-GCM / ChaCha set. */
|
|
36
|
+
ciphers?: string;
|
|
37
|
+
/** Optional request handler. */
|
|
38
|
+
requestListener?: http.RequestListener;
|
|
39
|
+
}
|
|
40
|
+
/** Build a hardened `https.Server`. */
|
|
41
|
+
export declare function createSecureHttpServer(opts: SecureHttpServerOptions): https.Server;
|
|
42
|
+
/**
|
|
43
|
+
* Apply OWASP-baseline security headers to a response. Idempotent —
|
|
44
|
+
* if headers are already set, they're left alone.
|
|
45
|
+
*/
|
|
46
|
+
export declare function applySecurityHeaders(res: http.ServerResponse): void;
|
|
47
|
+
/** Wrap a request handler with security-header + TRACE-defence. */
|
|
48
|
+
export declare function secureHttpHandler(inner: http.RequestListener): http.RequestListener;
|
|
49
|
+
//# sourceMappingURL=secure-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-server.d.ts","sourceRoot":"","sources":["../src/secure-server.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AAEpC,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AAEvC,MAAM,WAAW,uBAAuB;IACtC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IACnC,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,eAAe,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC;CACxC;AAcD,uCAAuC;AACvC,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,uBAAuB,GAAG,KAAK,CAAC,MAAM,CAWlF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAenE;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,eAAe,CAUnF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
* TLS termination helpers (spec §14, v0.5).
|
|
6
|
+
*
|
|
7
|
+
* In production we recommend terminating TLS at a reverse proxy
|
|
8
|
+
* (nginx, Caddy, ALB, Cloudflare, etc.). For deployments that need
|
|
9
|
+
* TLS in-process — single-binary appliance, edge worker, dev with a
|
|
10
|
+
* self-signed cert — this module ships:
|
|
11
|
+
*
|
|
12
|
+
* - `createSecureHttpServer(opts)` that wraps `node:https`'s
|
|
13
|
+
* `createServer` with strong defaults (TLS 1.2+ minVersion,
|
|
14
|
+
* conservative cipher list, ALPN for HTTP/1.1).
|
|
15
|
+
*
|
|
16
|
+
* - `applySecurityHeaders(res)` that sets the OWASP-recommended
|
|
17
|
+
* baseline response headers (HSTS, no-sniff, frame deny). Drop
|
|
18
|
+
* this into the http handler before writing any response.
|
|
19
|
+
*
|
|
20
|
+
* - `secureHttpHandler(inner)` wrapper that applies the headers and
|
|
21
|
+
* defends against the classic TRACE method.
|
|
22
|
+
*
|
|
23
|
+
* Apps that already terminate TLS upstream can ignore this module —
|
|
24
|
+
* `startCollabServer` accepts a `server` option so they can pass any
|
|
25
|
+
* pre-built `http.Server` (or `https.Server`).
|
|
26
|
+
*/
|
|
27
|
+
import * as https from 'node:https';
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
const DEFAULT_CIPHERS = [
|
|
30
|
+
'TLS_AES_128_GCM_SHA256',
|
|
31
|
+
'TLS_AES_256_GCM_SHA384',
|
|
32
|
+
'TLS_CHACHA20_POLY1305_SHA256',
|
|
33
|
+
'ECDHE-ECDSA-AES128-GCM-SHA256',
|
|
34
|
+
'ECDHE-RSA-AES128-GCM-SHA256',
|
|
35
|
+
'ECDHE-ECDSA-AES256-GCM-SHA384',
|
|
36
|
+
'ECDHE-RSA-AES256-GCM-SHA384',
|
|
37
|
+
'ECDHE-ECDSA-CHACHA20-POLY1305',
|
|
38
|
+
'ECDHE-RSA-CHACHA20-POLY1305',
|
|
39
|
+
].join(':');
|
|
40
|
+
/** Build a hardened `https.Server`. */
|
|
41
|
+
export function createSecureHttpServer(opts) {
|
|
42
|
+
const tlsOpts = {
|
|
43
|
+
cert: fs.readFileSync(opts.certPath),
|
|
44
|
+
key: fs.readFileSync(opts.keyPath),
|
|
45
|
+
minVersion: opts.minVersion ?? 'TLSv1.2',
|
|
46
|
+
ciphers: opts.ciphers ?? DEFAULT_CIPHERS,
|
|
47
|
+
honorCipherOrder: true,
|
|
48
|
+
ALPNProtocols: ['http/1.1'],
|
|
49
|
+
};
|
|
50
|
+
if (opts.caPath)
|
|
51
|
+
tlsOpts.ca = fs.readFileSync(opts.caPath);
|
|
52
|
+
return https.createServer(tlsOpts, opts.requestListener);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Apply OWASP-baseline security headers to a response. Idempotent —
|
|
56
|
+
* if headers are already set, they're left alone.
|
|
57
|
+
*/
|
|
58
|
+
export function applySecurityHeaders(res) {
|
|
59
|
+
if (!res.headersSent) {
|
|
60
|
+
if (!res.hasHeader('strict-transport-security')) {
|
|
61
|
+
res.setHeader('strict-transport-security', 'max-age=31536000; includeSubDomains');
|
|
62
|
+
}
|
|
63
|
+
if (!res.hasHeader('x-content-type-options')) {
|
|
64
|
+
res.setHeader('x-content-type-options', 'nosniff');
|
|
65
|
+
}
|
|
66
|
+
if (!res.hasHeader('x-frame-options')) {
|
|
67
|
+
res.setHeader('x-frame-options', 'DENY');
|
|
68
|
+
}
|
|
69
|
+
if (!res.hasHeader('referrer-policy')) {
|
|
70
|
+
res.setHeader('referrer-policy', 'no-referrer');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Wrap a request handler with security-header + TRACE-defence. */
|
|
75
|
+
export function secureHttpHandler(inner) {
|
|
76
|
+
return (req, res) => {
|
|
77
|
+
if (req.method === 'TRACE' || req.method === 'TRACK') {
|
|
78
|
+
res.writeHead(405);
|
|
79
|
+
res.end();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
applySecurityHeaders(res);
|
|
83
|
+
inner(req, res);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=secure-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-server.js","sourceRoot":"","sources":["../src/secure-server.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAkB9B,MAAM,eAAe,GAAG;IACtB,wBAAwB;IACxB,wBAAwB;IACxB,8BAA8B;IAC9B,+BAA+B;IAC/B,6BAA6B;IAC7B,+BAA+B;IAC/B,6BAA6B;IAC7B,+BAA+B;IAC/B,6BAA6B;CAC9B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEZ,uCAAuC;AACvC,MAAM,UAAU,sBAAsB,CAAC,IAA6B;IAClE,MAAM,OAAO,GAAwB;QACnC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;QACpC,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC;QAClC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,SAAS;QACxC,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,eAAe;QACxC,gBAAgB,EAAE,IAAI;QACtB,aAAa,EAAE,CAAC,UAAU,CAAC;KAC5B,CAAC;IACF,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC3D,OAAO,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAwB;IAC3D,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QACrB,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,2BAA2B,CAAC,EAAE,CAAC;YAChD,GAAG,CAAC,SAAS,CAAC,2BAA2B,EAAE,qCAAqC,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,wBAAwB,CAAC,EAAE,CAAC;YAC7C,GAAG,CAAC,SAAS,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACtC,GAAG,CAAC,SAAS,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACtC,GAAG,CAAC,SAAS,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,iBAAiB,CAAC,KAA2B;IAC3D,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAClB,IAAI,GAAG,CAAC,MAAM,KAAK,OAAO,IAAI,GAAG,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YACrD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,EAAE,CAAC;YACV,OAAO;QACT,CAAC;QACD,oBAAoB,CAAC,GAAG,CAAC,CAAC;QAC1B,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Websocket sync server entry point.
|
|
3
|
+
*/
|
|
4
|
+
import * as http from 'node:http';
|
|
5
|
+
import { WebSocketServer } from 'ws';
|
|
6
|
+
import { RoomManager } from './room-manager.js';
|
|
7
|
+
import { type Persistence } from './persistence.js';
|
|
8
|
+
import { type AuthenticateFn, type Principal } from './auth.js';
|
|
9
|
+
import { type AuditSink } from './audit-log.js';
|
|
10
|
+
import { type RateLimitOptions } from './rate-limit.js';
|
|
11
|
+
import { type VerifyMessageFn } from './room-manager.js';
|
|
12
|
+
import { type ServerBlobStorage } from './blob-route.js';
|
|
13
|
+
import { MetricsRegistry } from './metrics.js';
|
|
14
|
+
export interface StartCollabServerOptions {
|
|
15
|
+
port?: number;
|
|
16
|
+
host?: string;
|
|
17
|
+
persistence?: Persistence;
|
|
18
|
+
authenticate?: AuthenticateFn;
|
|
19
|
+
maxRooms?: number;
|
|
20
|
+
compactEvery?: number;
|
|
21
|
+
/** Pre-built http server to attach to instead of creating one. */
|
|
22
|
+
server?: http.Server;
|
|
23
|
+
/** Append-only audit sink. Default: drop all events. */
|
|
24
|
+
auditSink?: AuditSink;
|
|
25
|
+
/** Per-peer rate limit. Function form lets you tune by role/user. */
|
|
26
|
+
rateLimit?: RateLimitOptions | ((principal: Principal) => RateLimitOptions);
|
|
27
|
+
/**
|
|
28
|
+
* Pluggable blob storage for the `/blobs/...` route. Default:
|
|
29
|
+
* in-memory. Pass a custom `ServerBlobStorage` to back with S3 or
|
|
30
|
+
* filesystem in production.
|
|
31
|
+
*/
|
|
32
|
+
blobStorage?: ServerBlobStorage;
|
|
33
|
+
/** Reject blob PUTs above this size (default 100 MB). */
|
|
34
|
+
blobMaxBytes?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Unload rooms that have had zero peers for this many ms (default
|
|
37
|
+
* disabled). Persistence keeps the durable copy; rehydrate on next
|
|
38
|
+
* connect.
|
|
39
|
+
*/
|
|
40
|
+
idleUnloadMs?: number;
|
|
41
|
+
/** Metrics registry to publish at `/metrics`. Defaults to the package singleton. */
|
|
42
|
+
metrics?: MetricsRegistry;
|
|
43
|
+
/**
|
|
44
|
+
* Optional per-message verifier (anti-replay HMAC etc.). Runs before
|
|
45
|
+
* rate limit / role check. Returning `{ ok: false }` audits as
|
|
46
|
+
* `reject` with the reason and drops the message.
|
|
47
|
+
*/
|
|
48
|
+
verifyMessage?: VerifyMessageFn;
|
|
49
|
+
}
|
|
50
|
+
export interface CollabServerHandle {
|
|
51
|
+
readonly url: string;
|
|
52
|
+
readonly httpServer: http.Server;
|
|
53
|
+
readonly wss: WebSocketServer;
|
|
54
|
+
readonly roomManager: RoomManager;
|
|
55
|
+
stop(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export declare function startCollabServer(opts?: StartCollabServerOptions): Promise<CollabServerHandle>;
|
|
58
|
+
export { FilePersistence, MemoryPersistence } from './persistence.js';
|
|
59
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAIA;;GAEG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,eAAe,EAAkB,MAAM,IAAI,CAAC;AACrD,OAAO,EAAE,WAAW,EAAuB,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAsC,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACxF,OAAO,EAAwB,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AACtF,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAkB,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/D,MAAM,WAAW,wBAAwB;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC;IACrB,wDAAwD;IACxD,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,qEAAqE;IACrE,SAAS,CAAC,EAAE,gBAAgB,GAAG,CAAC,CAAC,SAAS,EAAE,SAAS,KAAK,gBAAgB,CAAC,CAAC;IAC5E;;;;OAIG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oFAAoF;IACpF,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B;;;;OAIG;IACH,aAAa,CAAC,EAAE,eAAe,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC;IACjC,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAID,wBAAsB,iBAAiB,CACrC,IAAI,GAAE,wBAA6B,GAClC,OAAO,CAAC,kBAAkB,CAAC,CAmI7B;AAsED,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
* Websocket sync server entry point.
|
|
6
|
+
*/
|
|
7
|
+
import * as http from 'node:http';
|
|
8
|
+
import { WebSocketServer } from 'ws';
|
|
9
|
+
import { RoomManager } from './room-manager.js';
|
|
10
|
+
import { MemoryPersistence } from './persistence.js';
|
|
11
|
+
import { allowAnonymousEditor } from './auth.js';
|
|
12
|
+
import { handleBlobRequest, InMemoryBlobStorage, } from './blob-route.js';
|
|
13
|
+
import { defaultMetrics } from './metrics.js';
|
|
14
|
+
const PING_INTERVAL_MS = 30_000;
|
|
15
|
+
export async function startCollabServer(opts = {}) {
|
|
16
|
+
const persistence = opts.persistence ?? new MemoryPersistence();
|
|
17
|
+
const authenticate = opts.authenticate ?? allowAnonymousEditor;
|
|
18
|
+
const roomManager = new RoomManager({
|
|
19
|
+
persistence,
|
|
20
|
+
maxRooms: opts.maxRooms,
|
|
21
|
+
compactEvery: opts.compactEvery,
|
|
22
|
+
auditSink: opts.auditSink,
|
|
23
|
+
rateLimit: opts.rateLimit,
|
|
24
|
+
idleUnloadMs: opts.idleUnloadMs,
|
|
25
|
+
verifyMessage: opts.verifyMessage,
|
|
26
|
+
});
|
|
27
|
+
const blobStorage = opts.blobStorage ?? new InMemoryBlobStorage();
|
|
28
|
+
const metrics = opts.metrics ?? defaultMetrics;
|
|
29
|
+
const peersGauge = metrics.gauge('collab_room_peers', 'Currently connected peers per room');
|
|
30
|
+
const roomsGauge = metrics.gauge('collab_rooms', 'Currently loaded rooms');
|
|
31
|
+
const updatesCounter = metrics.counter('collab_updates_total', 'Y updates accepted by the server');
|
|
32
|
+
const rejectsCounter = metrics.counter('collab_rejects_total', 'Update messages rejected (rate-limit / role / replay)');
|
|
33
|
+
const httpServer = opts.server ??
|
|
34
|
+
http.createServer(async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
if (req.url === '/healthz') {
|
|
37
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
38
|
+
res.end(JSON.stringify({ ok: true, rooms: roomManager.list().length }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (req.url === '/metrics') {
|
|
42
|
+
// Refresh derived gauges before rendering so the snapshot
|
|
43
|
+
// reflects live state, not state-at-last-event.
|
|
44
|
+
const stats = await roomManager.stats();
|
|
45
|
+
roomsGauge.set(stats.length);
|
|
46
|
+
for (const s of stats)
|
|
47
|
+
peersGauge.set(s.peerCount, { room: s.roomId });
|
|
48
|
+
res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4' });
|
|
49
|
+
res.end(metrics.render());
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Blob route: PUT / GET / HEAD / DELETE on /blobs/<hash>, GET /blobs.
|
|
53
|
+
if (req.url && req.url.startsWith('/blobs')) {
|
|
54
|
+
const handled = await handleBlobRequest(req, res, {
|
|
55
|
+
storage: blobStorage,
|
|
56
|
+
maxBytes: opts.blobMaxBytes,
|
|
57
|
+
});
|
|
58
|
+
if (handled)
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
res.writeHead(404);
|
|
62
|
+
res.end();
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.error('[collab-server] http handler error:', err);
|
|
67
|
+
if (!res.headersSent) {
|
|
68
|
+
res.writeHead(500);
|
|
69
|
+
res.end();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// Surface the counters on the manager so the room can bump them in
|
|
74
|
+
// its update / reject paths. Done as opaque setters to keep
|
|
75
|
+
// RoomManager from importing the metrics module directly.
|
|
76
|
+
roomManager.setCounters({
|
|
77
|
+
update: () => updatesCounter.inc(),
|
|
78
|
+
reject: (reason) => rejectsCounter.inc(1, { reason }),
|
|
79
|
+
});
|
|
80
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
81
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
82
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
83
|
+
// handleConnection runs async setup (room load, auth callback,
|
|
84
|
+
// persistence open). If it rejects — e.g. RoomManager.getOrCreate
|
|
85
|
+
// throws on room-cap or persistence failure — we previously
|
|
86
|
+
// discarded the promise with `void`, leaving the socket open and
|
|
87
|
+
// surfacing as a process-level unhandledRejection. Catch the error,
|
|
88
|
+
// log it, and close the socket with a non-1000 code so the client
|
|
89
|
+
// sees a deterministic shutdown.
|
|
90
|
+
handleConnection(ws, req, { roomManager, authenticate }).catch((err) => {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.error('[collab-server] connection setup failed:', err);
|
|
93
|
+
try {
|
|
94
|
+
if (ws.readyState === ws.OPEN || ws.readyState === ws.CONNECTING) {
|
|
95
|
+
// 1011 = server error per RFC 6455
|
|
96
|
+
ws.close(1011, 'connection setup failed');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// ignore close errors
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
if (!opts.server) {
|
|
106
|
+
await new Promise((resolve, reject) => {
|
|
107
|
+
httpServer.once('error', reject);
|
|
108
|
+
httpServer.listen(opts.port ?? 1234, opts.host ?? '0.0.0.0', () => {
|
|
109
|
+
httpServer.off('error', reject);
|
|
110
|
+
resolve();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const address = httpServer.address();
|
|
115
|
+
const url = typeof address === 'object' && address
|
|
116
|
+
? `ws://${opts.host ?? '127.0.0.1'}:${address.port}`
|
|
117
|
+
: `ws://${opts.host ?? '127.0.0.1'}:${opts.port ?? 1234}`;
|
|
118
|
+
return {
|
|
119
|
+
url,
|
|
120
|
+
httpServer,
|
|
121
|
+
wss,
|
|
122
|
+
roomManager,
|
|
123
|
+
async stop() {
|
|
124
|
+
await new Promise((resolve) => wss.close(() => resolve()));
|
|
125
|
+
if (!opts.server) {
|
|
126
|
+
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
127
|
+
}
|
|
128
|
+
await roomManager.unloadAll();
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function handleConnection(ws, req, ctx) {
|
|
133
|
+
ws.binaryType = 'arraybuffer';
|
|
134
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
135
|
+
// y-websocket convention: room id is the path (e.g. ws://host/project/model)
|
|
136
|
+
const roomId = decodeURIComponent(url.pathname.replace(/^\/+/, ''));
|
|
137
|
+
const token = url.searchParams.get('token') ?? undefined;
|
|
138
|
+
if (!roomId) {
|
|
139
|
+
ws.close(4400, 'missing-room');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
let principal;
|
|
143
|
+
try {
|
|
144
|
+
principal = await ctx.authenticate(token, roomId);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
|
+
console.error('[collab-server] auth threw:', err);
|
|
149
|
+
ws.close(4500, 'auth-error');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!principal) {
|
|
153
|
+
ws.close(4401, 'unauthorized');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const room = await ctx.roomManager.getOrCreate(roomId);
|
|
157
|
+
const conn = {
|
|
158
|
+
ws,
|
|
159
|
+
principal,
|
|
160
|
+
awarenessClients: new Set(),
|
|
161
|
+
};
|
|
162
|
+
room.addConnection(conn);
|
|
163
|
+
let alive = true;
|
|
164
|
+
const ping = setInterval(() => {
|
|
165
|
+
if (!alive) {
|
|
166
|
+
try {
|
|
167
|
+
ws.terminate();
|
|
168
|
+
}
|
|
169
|
+
catch { /* socket already gone */ }
|
|
170
|
+
clearInterval(ping);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
alive = false;
|
|
174
|
+
try {
|
|
175
|
+
ws.ping();
|
|
176
|
+
}
|
|
177
|
+
catch { /* socket already gone */ }
|
|
178
|
+
}, PING_INTERVAL_MS);
|
|
179
|
+
ws.on('pong', () => { alive = true; });
|
|
180
|
+
ws.on('message', (data) => {
|
|
181
|
+
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data);
|
|
182
|
+
room.handleMessage(conn, bytes);
|
|
183
|
+
});
|
|
184
|
+
const cleanup = () => {
|
|
185
|
+
clearInterval(ping);
|
|
186
|
+
room.removeConnection(conn);
|
|
187
|
+
};
|
|
188
|
+
ws.on('close', cleanup);
|
|
189
|
+
ws.on('error', (err) => {
|
|
190
|
+
// eslint-disable-next-line no-console
|
|
191
|
+
console.error('[collab-server] ws error:', err);
|
|
192
|
+
cleanup();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
export { FilePersistence, MemoryPersistence } from './persistence.js';
|
|
196
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RoomManager } from './room-manager.js';
|
|
2
|
+
export interface SnapshotWorkerOptions {
|
|
3
|
+
roomManager: RoomManager;
|
|
4
|
+
/** Output directory; created if missing. */
|
|
5
|
+
outputDir: string;
|
|
6
|
+
/** How often to run snapshots, ms. Default 5 minutes. */
|
|
7
|
+
intervalMs?: number;
|
|
8
|
+
/** If true, also emit a per-clientID layer file alongside the composed snapshot. */
|
|
9
|
+
emitLayers?: boolean;
|
|
10
|
+
/** Override clock so tests are deterministic. */
|
|
11
|
+
now?: () => number;
|
|
12
|
+
/** If true, snapshot even rooms with zero peers (default false). */
|
|
13
|
+
includeIdle?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface SnapshotResult {
|
|
16
|
+
roomId: string;
|
|
17
|
+
filePath: string;
|
|
18
|
+
byteLength: number;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class SnapshotWorker {
|
|
22
|
+
private timer;
|
|
23
|
+
private inflight;
|
|
24
|
+
private readonly options;
|
|
25
|
+
constructor(opts: SnapshotWorkerOptions);
|
|
26
|
+
start(): void;
|
|
27
|
+
stop(): void;
|
|
28
|
+
/** Force one snapshot pass across all active rooms. Returns per-room results. */
|
|
29
|
+
runOnce(): Promise<SnapshotResult[]>;
|
|
30
|
+
private now;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=snapshot-worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-worker.d.ts","sourceRoot":"","sources":["../src/snapshot-worker.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,WAAW,CAAC;IACzB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iDAAiD;IACjD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,oEAAoE;IACpE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAA0C;IAC1D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAC4D;gBAExE,IAAI,EAAE,qBAAqB;IAYvC,KAAK,IAAI,IAAI;IAWb,IAAI,IAAI,IAAI;IAKZ,iFAAiF;IAC3E,OAAO,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IA+B1C,OAAO,CAAC,GAAG;CAGZ"}
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
* Server-driven IFCX snapshot worker (spec §12.3).
|
|
6
|
+
*
|
|
7
|
+
* Periodically exports each active room's Y.Doc as an `.ifcx` file
|
|
8
|
+
* (composed state), optionally with per-user "git blame" layers. Output
|
|
9
|
+
* lives next to the durable update log so a deployment can ship its
|
|
10
|
+
* audit chain alongside its persistence.
|
|
11
|
+
*
|
|
12
|
+
* Runs as a self-contained class — instantiate with a `RoomManager`
|
|
13
|
+
* handle and call `start()`. The worker is stateless across restarts;
|
|
14
|
+
* the room itself is the source of truth.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { snapshotToIfcx, serializeIfcx } from '@ifc-lite/collab';
|
|
19
|
+
export class SnapshotWorker {
|
|
20
|
+
timer = null;
|
|
21
|
+
inflight = null;
|
|
22
|
+
options;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.options = {
|
|
25
|
+
roomManager: opts.roomManager,
|
|
26
|
+
outputDir: opts.outputDir,
|
|
27
|
+
intervalMs: opts.intervalMs ?? 5 * 60_000,
|
|
28
|
+
emitLayers: opts.emitLayers ?? false,
|
|
29
|
+
now: opts.now,
|
|
30
|
+
includeIdle: opts.includeIdle,
|
|
31
|
+
};
|
|
32
|
+
fs.mkdirSync(this.options.outputDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
start() {
|
|
35
|
+
if (this.timer)
|
|
36
|
+
return;
|
|
37
|
+
this.timer = setInterval(() => {
|
|
38
|
+
void this.runOnce().catch((err) => {
|
|
39
|
+
// eslint-disable-next-line no-console
|
|
40
|
+
console.error('[snapshot-worker] run failed:', err);
|
|
41
|
+
});
|
|
42
|
+
}, this.options.intervalMs);
|
|
43
|
+
this.timer.unref?.();
|
|
44
|
+
}
|
|
45
|
+
stop() {
|
|
46
|
+
if (this.timer)
|
|
47
|
+
clearInterval(this.timer);
|
|
48
|
+
this.timer = null;
|
|
49
|
+
}
|
|
50
|
+
/** Force one snapshot pass across all active rooms. Returns per-room results. */
|
|
51
|
+
async runOnce() {
|
|
52
|
+
if (this.inflight)
|
|
53
|
+
return this.inflight;
|
|
54
|
+
this.inflight = (async () => {
|
|
55
|
+
const stats = await this.options.roomManager.stats();
|
|
56
|
+
const targets = stats.filter((s) => s.peerCount > 0 || this.options.includeIdle);
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const t of targets) {
|
|
59
|
+
const room = await this.options.roomManager.getOrCreate(t.roomId);
|
|
60
|
+
const start = this.now();
|
|
61
|
+
const ifcx = snapshotToIfcx(room.doc);
|
|
62
|
+
const content = serializeIfcx(ifcx, false);
|
|
63
|
+
const stamp = new Date(this.now()).toISOString().replace(/[:.]/g, '-');
|
|
64
|
+
const safe = t.roomId.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
65
|
+
const filePath = path.join(this.options.outputDir, `${safe}.${stamp}.ifcx`);
|
|
66
|
+
await fs.promises.writeFile(filePath, content);
|
|
67
|
+
results.push({
|
|
68
|
+
roomId: t.roomId,
|
|
69
|
+
filePath,
|
|
70
|
+
byteLength: Buffer.byteLength(content, 'utf8'),
|
|
71
|
+
durationMs: this.now() - start,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
})();
|
|
76
|
+
try {
|
|
77
|
+
return await this.inflight;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
this.inflight = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
now() {
|
|
84
|
+
return this.options.now ? this.options.now() : Date.now();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=snapshot-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-worker.js","sourceRoot":"","sources":["../src/snapshot-worker.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ifc-lite/collab-server",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Reference websocket sync server for @ifc-lite/collab",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"ifc-lite-collab-server": "./dist/bin.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./room-manager": {
|
|
17
|
+
"import": "./dist/room-manager.js",
|
|
18
|
+
"types": "./dist/room-manager.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"lib0": "^0.2.99",
|
|
23
|
+
"ws": "^8.18.0",
|
|
24
|
+
"y-protocols": "^1.0.6",
|
|
25
|
+
"yjs": "^13.6.20",
|
|
26
|
+
"@ifc-lite/collab": "^0.2.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^20.0.0",
|
|
30
|
+
"@types/ws": "^8.5.13",
|
|
31
|
+
"typescript": "^5.3.0",
|
|
32
|
+
"vitest": "^1.6.0",
|
|
33
|
+
"y-websocket": "^2.0.4"
|
|
34
|
+
},
|
|
35
|
+
"license": "MPL-2.0",
|
|
36
|
+
"author": "Louis True",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/louistrue/ifc-lite.git",
|
|
40
|
+
"directory": "packages/collab-server"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://louistrue.github.io/ifc-lite/",
|
|
43
|
+
"bugs": "https://github.com/louistrue/ifc-lite/issues",
|
|
44
|
+
"keywords": [
|
|
45
|
+
"ifc",
|
|
46
|
+
"ifcx",
|
|
47
|
+
"bim",
|
|
48
|
+
"collaboration",
|
|
49
|
+
"crdt",
|
|
50
|
+
"yjs",
|
|
51
|
+
"websocket"
|
|
52
|
+
],
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"dist",
|
|
58
|
+
"README.md"
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "pnpm exec tsc",
|
|
62
|
+
"dev": "pnpm exec tsc --watch",
|
|
63
|
+
"start": "node ./dist/bin.js",
|
|
64
|
+
"test": "vitest run"
|
|
65
|
+
}
|
|
66
|
+
}
|