@better-forge/runtime 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +8 -2
- package/dist/appLock.d.ts +13 -0
- package/dist/appLock.d.ts.map +1 -0
- package/dist/appLock.js +67 -0
- package/dist/appLock.js.map +1 -0
- package/dist/createBetterApp.d.ts +63 -0
- package/dist/createBetterApp.d.ts.map +1 -0
- package/dist/createBetterApp.js +181 -0
- package/dist/createBetterApp.js.map +1 -0
- package/dist/dataPaths.d.ts +13 -0
- package/dist/dataPaths.d.ts.map +1 -0
- package/dist/dataPaths.js +39 -0
- package/dist/dataPaths.js.map +1 -0
- package/dist/errorHandler.d.ts +8 -0
- package/dist/errorHandler.d.ts.map +1 -0
- package/dist/errorHandler.js +14 -0
- package/dist/errorHandler.js.map +1 -0
- package/dist/health.d.ts +57 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +62 -0
- package/dist/health.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/processLiveness.d.ts +15 -0
- package/dist/processLiveness.d.ts.map +1 -0
- package/dist/processLiveness.js +30 -0
- package/dist/processLiveness.js.map +1 -0
- package/dist/proxy.d.ts +4 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +20 -0
- package/dist/proxy.js.map +1 -0
- package/dist/publicUrl.d.ts +11 -0
- package/dist/publicUrl.d.ts.map +1 -0
- package/dist/publicUrl.js +29 -0
- package/dist/publicUrl.js.map +1 -0
- package/dist/rateLimitKey.d.ts +7 -0
- package/dist/rateLimitKey.d.ts.map +1 -0
- package/dist/rateLimitKey.js +21 -0
- package/dist/rateLimitKey.js.map +1 -0
- package/dist/redactUrl.d.ts +9 -0
- package/dist/redactUrl.d.ts.map +1 -0
- package/dist/redactUrl.js +48 -0
- package/dist/redactUrl.js.map +1 -0
- package/dist/shutdown.d.ts +20 -0
- package/dist/shutdown.d.ts.map +1 -0
- package/dist/shutdown.js +46 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/sse.d.ts +58 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +119 -0
- package/dist/sse.js.map +1 -0
- package/dist/updateCheck.d.ts +26 -0
- package/dist/updateCheck.d.ts.map +1 -0
- package/dist/updateCheck.js +95 -0
- package/dist/updateCheck.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +63 -4
- package/src/appLock.ts +63 -0
- package/src/createBetterApp.ts +236 -0
- package/src/dataPaths.ts +48 -0
- package/src/errorHandler.ts +15 -0
- package/src/health.ts +109 -0
- package/src/index.ts +14 -0
- package/src/processLiveness.ts +31 -0
- package/src/proxy.ts +20 -0
- package/src/publicUrl.ts +26 -0
- package/src/rateLimitKey.ts +20 -0
- package/src/redactUrl.ts +49 -0
- package/src/shutdown.ts +62 -0
- package/src/sse.ts +154 -0
- package/src/updateCheck.ts +107 -0
- package/src/version.ts +7 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { backupDir, dataDir, databasePath, isSetupComplete, setupConfigPath, writeSetupComplete } from './dataPaths.js';
|
|
2
|
+
export { resolveTrustProxy, type TrustProxy } from './proxy.js';
|
|
3
|
+
export { rateLimitKey } from './rateLimitKey.js';
|
|
4
|
+
export { createPublicUrlResolver } from './publicUrl.js';
|
|
5
|
+
export { errorHandler } from './errorHandler.js';
|
|
6
|
+
export { createAppLock } from './appLock.js';
|
|
7
|
+
export { isOwnerAlive, isProcessAlive, currentProcessOwner, PROCESS_STARTED_AT, type ProcessOwner } from './processLiveness.js';
|
|
8
|
+
export { createRuntimeShutdown, type RuntimeShutdown, type ShutdownSteps } from './shutdown.js';
|
|
9
|
+
export { createSse, type ClientInfo, type EventStreamOptions, type SseFilter, type SseLimits } from './sse.js';
|
|
10
|
+
export { createUpdateCheck, isNewerVersion, updateCheckEnabled, type LatestRelease } from './updateCheck.js';
|
|
11
|
+
export { createHealthRoutes, createSystemHealth, type ComponentStatus, type HealthContributor, type HealthSection, type SystemHealthReport } from './health.js';
|
|
12
|
+
export { resolveAppVersion } from './version.js';
|
|
13
|
+
export { createBetterApp, JWT_EXPIRES_IN, resolveJwtSecret, type BetterApp, type BetterAppOptions } from './createBetterApp.js';
|
|
14
|
+
export { redactUrl, type UrlRedaction } from './redactUrl.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACvH,OAAO,EAAE,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAC/H,OAAO,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAA;AAC/F,OAAO,EAAE,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,kBAAkB,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAA;AAC9G,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC5G,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,KAAK,eAAe,EAAE,KAAK,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAC/J,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAC/H,OAAO,EAAE,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { backupDir, dataDir, databasePath, isSetupComplete, setupConfigPath, writeSetupComplete } from './dataPaths.js';
|
|
2
|
+
export { resolveTrustProxy } from './proxy.js';
|
|
3
|
+
export { rateLimitKey } from './rateLimitKey.js';
|
|
4
|
+
export { createPublicUrlResolver } from './publicUrl.js';
|
|
5
|
+
export { errorHandler } from './errorHandler.js';
|
|
6
|
+
export { createAppLock } from './appLock.js';
|
|
7
|
+
export { isOwnerAlive, isProcessAlive, currentProcessOwner, PROCESS_STARTED_AT } from './processLiveness.js';
|
|
8
|
+
export { createRuntimeShutdown } from './shutdown.js';
|
|
9
|
+
export { createSse } from './sse.js';
|
|
10
|
+
export { createUpdateCheck, isNewerVersion, updateCheckEnabled } from './updateCheck.js';
|
|
11
|
+
export { createHealthRoutes, createSystemHealth } from './health.js';
|
|
12
|
+
export { resolveAppVersion } from './version.js';
|
|
13
|
+
export { createBetterApp, JWT_EXPIRES_IN, resolveJwtSecret } from './createBetterApp.js';
|
|
14
|
+
export { redactUrl } from './redactUrl.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACvH,OAAO,EAAE,iBAAiB,EAAmB,MAAM,YAAY,CAAA;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,kBAAkB,EAAqB,MAAM,sBAAsB,CAAA;AAC/H,OAAO,EAAE,qBAAqB,EAA4C,MAAM,eAAe,CAAA;AAC/F,OAAO,EAAE,SAAS,EAA4E,MAAM,UAAU,CAAA;AAC9G,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,kBAAkB,EAAsB,MAAM,kBAAkB,CAAA;AAC5G,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAA6F,MAAM,aAAa,CAAA;AAC/J,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,gBAAgB,EAAyC,MAAM,sBAAsB,CAAA;AAC/H,OAAO,EAAE,SAAS,EAAqB,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Wall-clock start of this process; identical in the main thread and in worker threads. */
|
|
2
|
+
export declare const PROCESS_STARTED_AT: number;
|
|
3
|
+
/** Signal 0 checks existence only; EPERM still means the process exists under another user. */
|
|
4
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
5
|
+
export interface ProcessOwner {
|
|
6
|
+
pid: number;
|
|
7
|
+
processStartedAt: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function currentProcessOwner(): ProcessOwner;
|
|
10
|
+
/**
|
|
11
|
+
* Whether the recorded owner may still be running. The start time distinguishes a crashed
|
|
12
|
+
* earlier incarnation that reused our pid (containers restart as the same pid) from us.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isOwnerAlive(owner: Partial<ProcessOwner>): boolean;
|
|
15
|
+
//# sourceMappingURL=processLiveness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processLiveness.d.ts","sourceRoot":"","sources":["../src/processLiveness.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,QAAmD,CAAA;AAElF,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnD;AAED,MAAM,WAAW,YAAY;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE;AAEvE,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,OAAO,CAMlE"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Wall-clock start of this process; identical in the main thread and in worker threads. */
|
|
2
|
+
export const PROCESS_STARTED_AT = Math.round(Date.now() - process.uptime() * 1000);
|
|
3
|
+
/** Signal 0 checks existence only; EPERM still means the process exists under another user. */
|
|
4
|
+
export function isProcessAlive(pid) {
|
|
5
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
return error.code === 'EPERM';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function currentProcessOwner() {
|
|
16
|
+
return { pid: process.pid, processStartedAt: PROCESS_STARTED_AT };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether the recorded owner may still be running. The start time distinguishes a crashed
|
|
20
|
+
* earlier incarnation that reused our pid (containers restart as the same pid) from us.
|
|
21
|
+
*/
|
|
22
|
+
export function isOwnerAlive(owner) {
|
|
23
|
+
if (typeof owner.pid !== 'number')
|
|
24
|
+
return false;
|
|
25
|
+
if (owner.pid === process.pid) {
|
|
26
|
+
return typeof owner.processStartedAt === 'number' && Math.abs(owner.processStartedAt - PROCESS_STARTED_AT) < 1_000;
|
|
27
|
+
}
|
|
28
|
+
return isProcessAlive(owner.pid);
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=processLiveness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"processLiveness.js","sourceRoot":"","sources":["../src/processLiveness.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAA;AAElF,+FAA+F;AAC/F,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IACpD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAA;IAC1D,CAAC;AACH,CAAC;AAID,MAAM,UAAU,mBAAmB;IACjC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,CAAA;AACnE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAA4B;IACvD,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC/C,IAAI,KAAK,CAAC,GAAG,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC;QAC9B,OAAO,OAAO,KAAK,CAAC,gBAAgB,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,GAAG,kBAAkB,CAAC,GAAG,KAAK,CAAA;IACpH,CAAC;IACD,OAAO,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;AAClC,CAAC"}
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAEnD,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAC,CAAA;AAExE,wBAAgB,iBAAiB,CAAC,GAAG,qBAA6B,GAAG,UAAU,CAe9E"}
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function resolveTrustProxy(raw = process.env['TRUST_PROXY']) {
|
|
2
|
+
const value = raw?.trim();
|
|
3
|
+
if (!value || value.toLowerCase() === 'false')
|
|
4
|
+
return false;
|
|
5
|
+
if (value.toLowerCase() === 'true')
|
|
6
|
+
return true;
|
|
7
|
+
if (/^\d+$/.test(value)) {
|
|
8
|
+
// Hop-count-only trust can't validate the immediate peer, so @fastify/proxy-addr
|
|
9
|
+
// no longer supports it (it always fails closed). Treat a numeric value as "trust a
|
|
10
|
+
// proxy on this host or on a private network": loopback covers a proxy on the same
|
|
11
|
+
// host, uniquelocal (10/8, 172.16/12, 192.168/16, fc00::/7) covers one reaching the
|
|
12
|
+
// app over a Docker bridge network. Use an explicit address/CIDR list to narrow it.
|
|
13
|
+
return ['loopback', 'uniquelocal'];
|
|
14
|
+
}
|
|
15
|
+
const addresses = value.split(',').map((address) => address.trim()).filter(Boolean);
|
|
16
|
+
if (addresses.length === 0)
|
|
17
|
+
return false;
|
|
18
|
+
return addresses.length === 1 ? addresses[0] : addresses;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,iBAAiB,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAChE,MAAM,KAAK,GAAG,GAAG,EAAE,IAAI,EAAE,CAAA;IACzB,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,OAAO;QAAE,OAAO,KAAK,CAAA;IAC3D,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAC/C,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,iFAAiF;QACjF,oFAAoF;QACpF,mFAAmF;QACnF,oFAAoF;QACpF,oFAAoF;QACpF,OAAO,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;IACpC,CAAC;IACD,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACnF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACxC,OAAO,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AAC3D,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The absolute base URL the application is reached at, from the `PUBLIC_URL` environment variable.
|
|
3
|
+
*
|
|
4
|
+
* It is deployment configuration, not an admin setting: links in emails and redirects are built from it, so changing
|
|
5
|
+
* it must require access to the server — an account in the admin console alone must not be able to point those links
|
|
6
|
+
* at another domain.
|
|
7
|
+
*
|
|
8
|
+
* The returned function reports an invalid value once per distinct value, then keeps answering ''.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createPublicUrlResolver(): (env?: NodeJS.ProcessEnv) => string;
|
|
11
|
+
//# sourceMappingURL=publicUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicUrl.d.ts","sourceRoot":"","sources":["../src/publicUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,KAGJ,MAAK,MAAM,CAAC,UAAwB,KAAG,MAAM,CAa/E"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The absolute base URL the application is reached at, from the `PUBLIC_URL` environment variable.
|
|
3
|
+
*
|
|
4
|
+
* It is deployment configuration, not an admin setting: links in emails and redirects are built from it, so changing
|
|
5
|
+
* it must require access to the server — an account in the admin console alone must not be able to point those links
|
|
6
|
+
* at another domain.
|
|
7
|
+
*
|
|
8
|
+
* The returned function reports an invalid value once per distinct value, then keeps answering ''.
|
|
9
|
+
*/
|
|
10
|
+
export function createPublicUrlResolver() {
|
|
11
|
+
let warnedAbout = null;
|
|
12
|
+
return function resolvePublicUrl(env = process.env) {
|
|
13
|
+
const raw = (env['PUBLIC_URL'] ?? '').trim().replace(/\/+$/, '');
|
|
14
|
+
if (!raw)
|
|
15
|
+
return '';
|
|
16
|
+
try {
|
|
17
|
+
const url = new URL(raw);
|
|
18
|
+
if ((url.protocol === 'https:' || url.protocol === 'http:') && !url.search && !url.hash)
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
catch { /* reported below */ }
|
|
22
|
+
if (warnedAbout !== raw) {
|
|
23
|
+
warnedAbout = raw;
|
|
24
|
+
console.warn(`PUBLIC_URL "${raw}" is not an absolute http(s) URL without query or fragment — ignoring it`);
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=publicUrl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicUrl.js","sourceRoot":"","sources":["../src/publicUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB;IACrC,IAAI,WAAW,GAAkB,IAAI,CAAA;IAErC,OAAO,SAAS,gBAAgB,CAAC,MAAyB,OAAO,CAAC,GAAG;QACnE,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QAChE,IAAI,CAAC,GAAG;YAAE,OAAO,EAAE,CAAA;QACnB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;YACxB,IAAI,CAAC,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI;gBAAE,OAAO,GAAG,CAAA;QACrG,CAAC;QAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,CAAC;QAChC,IAAI,WAAW,KAAK,GAAG,EAAE,CAAC;YACxB,WAAW,GAAG,GAAG,CAAA;YACjB,OAAO,CAAC,IAAI,CAAC,eAAe,GAAG,0EAA0E,CAAC,CAAA;QAC5G,CAAC;QACD,OAAO,EAAE,CAAA;IACX,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key a client is rate limited under. An IPv4 address is its own key; an IPv6 address is
|
|
3
|
+
* reduced to its /64, because one subscriber is routinely handed a whole /64 and could otherwise
|
|
4
|
+
* rotate through billions of addresses to dodge a per-address limit.
|
|
5
|
+
*/
|
|
6
|
+
export declare function rateLimitKey(ip: string): string;
|
|
7
|
+
//# sourceMappingURL=rateLimitKey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimitKey.d.ts","sourceRoot":"","sources":["../src/rateLimitKey.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAY/C"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isIPv6 } from 'node:net';
|
|
2
|
+
/**
|
|
3
|
+
* The key a client is rate limited under. An IPv4 address is its own key; an IPv6 address is
|
|
4
|
+
* reduced to its /64, because one subscriber is routinely handed a whole /64 and could otherwise
|
|
5
|
+
* rotate through billions of addresses to dodge a per-address limit.
|
|
6
|
+
*/
|
|
7
|
+
export function rateLimitKey(ip) {
|
|
8
|
+
const address = ip.split('%')[0] ?? ip;
|
|
9
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(address);
|
|
10
|
+
if (mapped)
|
|
11
|
+
return mapped[1];
|
|
12
|
+
if (!isIPv6(address))
|
|
13
|
+
return address;
|
|
14
|
+
const [head = '', tail] = address.split('::');
|
|
15
|
+
const front = head ? head.split(':') : [];
|
|
16
|
+
const back = tail ? tail.split(':') : [];
|
|
17
|
+
const groups = tail === undefined ? front : [...front, ...Array(8 - front.length - back.length).fill('0'), ...back];
|
|
18
|
+
const prefix = groups.slice(0, 4).map((group) => parseInt(group, 16).toString(16));
|
|
19
|
+
return `${prefix.join(':')}::/64`;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=rateLimitKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimitKey.js","sourceRoot":"","sources":["../src/rateLimitKey.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEjC;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,EAAU;IACrC,MAAM,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IACtC,MAAM,MAAM,GAAG,gCAAgC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC7D,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC,CAAC,CAAE,CAAA;IAC7B,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAA;IAEpC,MAAM,CAAC,IAAI,GAAG,EAAE,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACxC,MAAM,MAAM,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,GAAG,KAAK,CAAS,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;IAC3H,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAA;IAClF,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAA;AACnC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface UrlRedaction {
|
|
2
|
+
/** Query parameters whose value is replaced. Names are compared after decoding, ignoring case. */
|
|
3
|
+
queryParams?: readonly string[] | undefined;
|
|
4
|
+
/** Paths whose next segment is a capability token (`/t/` hides `abc` in `/t/abc?x=1`), matched after percent-decoding as the router does. */
|
|
5
|
+
pathTokenPrefixes?: readonly string[] | undefined;
|
|
6
|
+
}
|
|
7
|
+
/** The URL as it may be logged: capability tokens in the path and the values of secret query parameters are hidden. */
|
|
8
|
+
export declare function redactUrl(url: string, rules: UrlRedaction): string;
|
|
9
|
+
//# sourceMappingURL=redactUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redactUrl.d.ts","sourceRoot":"","sources":["../src/redactUrl.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,kGAAkG;IAClG,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;IAC3C,6IAA6I;IAC7I,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CAClD;AAkBD,uHAAuH;AACvH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAwBlE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const REDACTED = '[redacted]';
|
|
2
|
+
function decodeName(name) {
|
|
3
|
+
try {
|
|
4
|
+
return decodeURIComponent(name.replace(/\+/g, ' '));
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return name;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The router decodes the path before it matches a route (`/%68ook/` reaches `/hook/`), but keeps an encoded slash.
|
|
12
|
+
* Each segment is decoded on its own, so one malformed sequence leaves only that segment as it was sent.
|
|
13
|
+
*/
|
|
14
|
+
function decodePath(path) {
|
|
15
|
+
return path.split('/').map((segment) => {
|
|
16
|
+
try {
|
|
17
|
+
return decodeURI(segment);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return segment;
|
|
21
|
+
}
|
|
22
|
+
}).join('/');
|
|
23
|
+
}
|
|
24
|
+
/** The URL as it may be logged: capability tokens in the path and the values of secret query parameters are hidden. */
|
|
25
|
+
export function redactUrl(url, rules) {
|
|
26
|
+
const queryAt = url.indexOf('?');
|
|
27
|
+
let path = queryAt === -1 ? url : url.slice(0, queryAt);
|
|
28
|
+
let query = queryAt === -1 ? '' : url.slice(queryAt + 1);
|
|
29
|
+
const decoded = decodePath(path);
|
|
30
|
+
for (const prefix of rules.pathTokenPrefixes ?? []) {
|
|
31
|
+
// Everything from the token on is dropped: the rest of a path is not decoded text a log should repeat.
|
|
32
|
+
if (decoded.startsWith(prefix) && decoded.length > prefix.length) {
|
|
33
|
+
path = `${prefix}${REDACTED}`;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const names = (rules.queryParams ?? []).map((name) => name.toLowerCase());
|
|
38
|
+
if (names.length > 0 && query) {
|
|
39
|
+
query = query.split('&').map((pair) => {
|
|
40
|
+
const equals = pair.indexOf('=');
|
|
41
|
+
if (equals === -1)
|
|
42
|
+
return pair;
|
|
43
|
+
return names.includes(decodeName(pair.slice(0, equals)).toLowerCase()) ? `${pair.slice(0, equals + 1)}${REDACTED}` : pair;
|
|
44
|
+
}).join('&');
|
|
45
|
+
}
|
|
46
|
+
return queryAt === -1 ? path : `${path}?${query}`;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=redactUrl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redactUrl.js","sourceRoot":"","sources":["../src/redactUrl.ts"],"names":[],"mappings":"AAOA,MAAM,QAAQ,GAAG,YAAY,CAAA;AAE7B,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,CAAC;QAAC,OAAO,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAA;IAAC,CAAC;AACnF,CAAC;AAED;;;GAGG;AACH,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;QACrC,IAAI,CAAC;YAAC,OAAO,SAAS,CAAC,OAAO,CAAC,CAAA;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,OAAO,OAAO,CAAA;QAAC,CAAC;IAC5D,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACd,CAAC;AAED,uHAAuH;AACvH,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,KAAmB;IACxD,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;IACvD,IAAI,KAAK,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAA;IAExD,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IAChC,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,EAAE,EAAE,CAAC;QACnD,uGAAuG;QACvG,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;YACjE,IAAI,GAAG,GAAG,MAAM,GAAG,QAAQ,EAAE,CAAA;YAC7B,MAAK;QACP,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAA;IACzE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,EAAE,CAAC;QAC9B,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACpC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YAChC,IAAI,MAAM,KAAK,CAAC,CAAC;gBAAE,OAAO,IAAI,CAAA;YAC9B,OAAO,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;QAC3H,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACd,CAAC;IAED,OAAO,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,KAAK,EAAE,CAAA;AACnD,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ShutdownSteps {
|
|
2
|
+
/** Stops producing new work and ends long-lived streams (SSE) so the server can drain. */
|
|
3
|
+
stopIntake: () => void;
|
|
4
|
+
/** Stops accepting connections and resolves once in-flight requests have finished. */
|
|
5
|
+
closeServer: () => Promise<void>;
|
|
6
|
+
/** Resolves once background work that started before the stop (a check about to record its result, say) has finished. */
|
|
7
|
+
drain?: () => Promise<void>;
|
|
8
|
+
/** Closes the DB and releases the app lock — only safe once no request or background work can still use them. */
|
|
9
|
+
releaseResources: () => void;
|
|
10
|
+
exit: (code: number) => void;
|
|
11
|
+
forceExitMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface RuntimeShutdown {
|
|
14
|
+
/** Immediate full teardown, for paths where the server never started or has already closed. */
|
|
15
|
+
cleanup: () => void;
|
|
16
|
+
/** Signal handler: drain the server and the background work first, then release shared resources and exit. */
|
|
17
|
+
shutdown: () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare function createRuntimeShutdown(steps: ShutdownSteps): RuntimeShutdown;
|
|
20
|
+
//# sourceMappingURL=shutdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shutdown.d.ts","sourceRoot":"","sources":["../src/shutdown.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,sFAAsF;IACtF,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC,yHAAyH;IACzH,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3B,iHAAiH;IACjH,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,+FAA+F;IAC/F,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,8GAA8G;IAC9G,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,aAAa,GAAG,eAAe,CAyC3E"}
|
package/dist/shutdown.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function createRuntimeShutdown(steps) {
|
|
2
|
+
let intakeStopped = false;
|
|
3
|
+
let released = false;
|
|
4
|
+
let shuttingDown = false;
|
|
5
|
+
let forceExit;
|
|
6
|
+
const stopIntake = () => {
|
|
7
|
+
if (intakeStopped)
|
|
8
|
+
return;
|
|
9
|
+
intakeStopped = true;
|
|
10
|
+
steps.stopIntake();
|
|
11
|
+
};
|
|
12
|
+
const release = () => {
|
|
13
|
+
if (released)
|
|
14
|
+
return;
|
|
15
|
+
released = true;
|
|
16
|
+
steps.releaseResources();
|
|
17
|
+
};
|
|
18
|
+
const cleanup = () => {
|
|
19
|
+
stopIntake();
|
|
20
|
+
// A signal-driven shutdown releases after the drain; the server's close hook fires before it.
|
|
21
|
+
if (!shuttingDown)
|
|
22
|
+
release();
|
|
23
|
+
};
|
|
24
|
+
const finish = (code) => {
|
|
25
|
+
clearTimeout(forceExit);
|
|
26
|
+
release();
|
|
27
|
+
steps.exit(code);
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
cleanup,
|
|
31
|
+
shutdown: () => {
|
|
32
|
+
if (shuttingDown)
|
|
33
|
+
return;
|
|
34
|
+
shuttingDown = true;
|
|
35
|
+
forceExit = setTimeout(() => {
|
|
36
|
+
release();
|
|
37
|
+
steps.exit(1);
|
|
38
|
+
}, steps.forceExitMs ?? 5_000);
|
|
39
|
+
forceExit.unref();
|
|
40
|
+
stopIntake();
|
|
41
|
+
// The forced exit above is the deadline of the drain: work still running by then is left to the next start.
|
|
42
|
+
steps.closeServer().then(() => steps.drain?.()).then(() => finish(0), () => finish(1));
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=shutdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shutdown.js","sourceRoot":"","sources":["../src/shutdown.ts"],"names":[],"mappings":"AAoBA,MAAM,UAAU,qBAAqB,CAAC,KAAoB;IACxD,IAAI,aAAa,GAAG,KAAK,CAAA;IACzB,IAAI,QAAQ,GAAG,KAAK,CAAA;IACpB,IAAI,YAAY,GAAG,KAAK,CAAA;IACxB,IAAI,SAAoD,CAAA;IACxD,MAAM,UAAU,GAAG,GAAG,EAAE;QACtB,IAAI,aAAa;YAAE,OAAM;QACzB,aAAa,GAAG,IAAI,CAAA;QACpB,KAAK,CAAC,UAAU,EAAE,CAAA;IACpB,CAAC,CAAA;IACD,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,QAAQ;YAAE,OAAM;QACpB,QAAQ,GAAG,IAAI,CAAA;QACf,KAAK,CAAC,gBAAgB,EAAE,CAAA;IAC1B,CAAC,CAAA;IACD,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,UAAU,EAAE,CAAA;QACZ,8FAA8F;QAC9F,IAAI,CAAC,YAAY;YAAE,OAAO,EAAE,CAAA;IAC9B,CAAC,CAAA;IACD,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,EAAE;QAC9B,YAAY,CAAC,SAAS,CAAC,CAAA;QACvB,OAAO,EAAE,CAAA;QACT,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAClB,CAAC,CAAA;IAED,OAAO;QACL,OAAO;QACP,QAAQ,EAAE,GAAG,EAAE;YACb,IAAI,YAAY;gBAAE,OAAM;YACxB,YAAY,GAAG,IAAI,CAAA;YACnB,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC1B,OAAO,EAAE,CAAA;gBACT,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACf,CAAC,EAAE,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,CAAA;YAC9B,SAAS,CAAC,KAAK,EAAE,CAAA;YACjB,UAAU,EAAE,CAAA;YACZ,4GAA4G;YAC5G,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;QACxF,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from 'fastify';
|
|
2
|
+
type SseClient = FastifyReply;
|
|
3
|
+
/**
|
|
4
|
+
* Shapes what one client receives: return the data to send (possibly reduced) or `null` to skip the
|
|
5
|
+
* event. Clients without a filter receive everything as broadcast.
|
|
6
|
+
*/
|
|
7
|
+
export type SseFilter = (event: string, data: unknown) => {
|
|
8
|
+
data: unknown;
|
|
9
|
+
} | null;
|
|
10
|
+
export interface ClientInfo {
|
|
11
|
+
filter?: SseFilter | undefined;
|
|
12
|
+
/** Set for signed-in streams, so revoking the session can end them. */
|
|
13
|
+
session?: {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
userId: number;
|
|
16
|
+
} | undefined;
|
|
17
|
+
ip?: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface SseLimits {
|
|
20
|
+
/** Open anonymous streams in total; further ones are refused with 429. Signed-in streams are not counted. */
|
|
21
|
+
maxClients: number;
|
|
22
|
+
/** Open anonymous streams from one address (an IPv6 /64 counts as one); further ones are refused with 429. */
|
|
23
|
+
maxClientsPerIp: number;
|
|
24
|
+
/** A client with more unsent bytes than this is not reading and gets disconnected. */
|
|
25
|
+
maxBufferedBytes: number;
|
|
26
|
+
}
|
|
27
|
+
export interface EventStreamOptions {
|
|
28
|
+
filter?: SseFilter;
|
|
29
|
+
session?: {
|
|
30
|
+
sessionId: string;
|
|
31
|
+
userId: number;
|
|
32
|
+
} | undefined;
|
|
33
|
+
/** Re-checked with every keep-alive ping; the stream ends once it resolves to false. */
|
|
34
|
+
stillAllowed?: () => Promise<boolean>;
|
|
35
|
+
pingIntervalMs?: number;
|
|
36
|
+
}
|
|
37
|
+
/** Live event streams (server-sent events) to browsers, with per-client filters and session-bound streams. All state is in the returned closure. */
|
|
38
|
+
export declare function createSse(limitOptions?: Partial<SseLimits>): {
|
|
39
|
+
sseService: {
|
|
40
|
+
add(client: SseClient, info?: ClientInfo): void;
|
|
41
|
+
remove(client: SseClient): void;
|
|
42
|
+
/** Server-side hook for anything that must react to the same events clients receive. */
|
|
43
|
+
onBroadcast(listener: (event: string) => void): () => void;
|
|
44
|
+
broadcast(event: string, data: unknown): void;
|
|
45
|
+
/** Ends the signed-in streams whose session matches, e.g. after the session was revoked. */
|
|
46
|
+
disconnectSessions(match: (session: {
|
|
47
|
+
sessionId: string;
|
|
48
|
+
userId: number;
|
|
49
|
+
}) => boolean): void;
|
|
50
|
+
/** Ends every stream opened without signing in, e.g. once access is restricted to signed-in users. */
|
|
51
|
+
disconnectAnonymous(): void;
|
|
52
|
+
closeAll(): void;
|
|
53
|
+
clientCount(): number;
|
|
54
|
+
};
|
|
55
|
+
serveEventStream: (req: FastifyRequest, reply: FastifyReply, options?: EventStreamOptions) => Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG3D,KAAK,SAAS,GAAG,YAAY,CAAA;AAE7B;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAAA;AAElF,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC9B,uEAAuE;IACvE,OAAO,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;IAC3D,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,6GAA6G;IAC7G,UAAU,EAAE,MAAM,CAAA;IAClB,8GAA8G;IAC9G,eAAe,EAAE,MAAM,CAAA;IACvB,sFAAsF;IACtF,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAID,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,OAAO,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAA;IAC3D,wFAAwF;IACxF,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,oJAAoJ;AACpJ,wBAAgB,SAAS,CAAC,YAAY,GAAE,OAAO,CAAC,SAAS,CAAM;;oBAuB/C,SAAS,SAAQ,UAAU;uBAIxB,SAAS;QAIxB,wFAAwF;8BAClE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;yBAKzC,MAAM,QAAQ,OAAO;QAWtC,4FAA4F;kCAClE,CAAC,OAAO,EAAE;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO;QAMrF,sGAAsG;;;;;4BAiBnE,cAAc,SAAS,YAAY,YAAW,kBAAkB,KAAQ,OAAO,CAAC,IAAI,CAAC;EA2C3H"}
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { rateLimitKey } from './rateLimitKey.js';
|
|
2
|
+
const DEFAULT_LIMITS = { maxClients: 1000, maxClientsPerIp: 10, maxBufferedBytes: 1024 * 1024 };
|
|
3
|
+
/** Live event streams (server-sent events) to browsers, with per-client filters and session-bound streams. All state is in the returned closure. */
|
|
4
|
+
export function createSse(limitOptions = {}) {
|
|
5
|
+
const limits = { ...DEFAULT_LIMITS, ...limitOptions };
|
|
6
|
+
const clients = new Map();
|
|
7
|
+
const listeners = new Set();
|
|
8
|
+
function end(client) {
|
|
9
|
+
try {
|
|
10
|
+
client.raw.end();
|
|
11
|
+
}
|
|
12
|
+
catch { /* ignore shutdown races */ }
|
|
13
|
+
try {
|
|
14
|
+
client.raw.destroy();
|
|
15
|
+
}
|
|
16
|
+
catch { /* ignore shutdown races */ }
|
|
17
|
+
clients.delete(client);
|
|
18
|
+
}
|
|
19
|
+
/** Writes to one client and drops it when it cannot keep up, so a stalled reader cannot grow the heap. */
|
|
20
|
+
function send(client, chunk) {
|
|
21
|
+
try {
|
|
22
|
+
client.raw.write(chunk);
|
|
23
|
+
if (client.raw.writableLength > limits.maxBufferedBytes)
|
|
24
|
+
end(client);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
clients.delete(client);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const sseService = {
|
|
31
|
+
add(client, info = {}) {
|
|
32
|
+
clients.set(client, info);
|
|
33
|
+
},
|
|
34
|
+
remove(client) {
|
|
35
|
+
clients.delete(client);
|
|
36
|
+
},
|
|
37
|
+
/** Server-side hook for anything that must react to the same events clients receive. */
|
|
38
|
+
onBroadcast(listener) {
|
|
39
|
+
listeners.add(listener);
|
|
40
|
+
return () => { listeners.delete(listener); };
|
|
41
|
+
},
|
|
42
|
+
broadcast(event, data) {
|
|
43
|
+
for (const listener of listeners)
|
|
44
|
+
listener(event);
|
|
45
|
+
const format = (value) => `event: ${event}\ndata: ${JSON.stringify(value)}\n\n`;
|
|
46
|
+
const payload = format(data);
|
|
47
|
+
for (const [client, { filter }] of clients) {
|
|
48
|
+
const shaped = filter ? filter(event, data) : { data };
|
|
49
|
+
if (!shaped)
|
|
50
|
+
continue;
|
|
51
|
+
send(client, shaped.data === data ? payload : format(shaped.data));
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
/** Ends the signed-in streams whose session matches, e.g. after the session was revoked. */
|
|
55
|
+
disconnectSessions(match) {
|
|
56
|
+
for (const [client, { session }] of clients) {
|
|
57
|
+
if (session && match(session))
|
|
58
|
+
end(client);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
/** Ends every stream opened without signing in, e.g. once access is restricted to signed-in users. */
|
|
62
|
+
disconnectAnonymous() {
|
|
63
|
+
for (const [client, { session }] of clients) {
|
|
64
|
+
if (!session)
|
|
65
|
+
end(client);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
closeAll() {
|
|
69
|
+
for (const client of clients.keys())
|
|
70
|
+
end(client);
|
|
71
|
+
},
|
|
72
|
+
clientCount() {
|
|
73
|
+
return clients.size;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
/** Holds the request open as an event stream until the client disconnects or loses access. */
|
|
77
|
+
async function serveEventStream(req, reply, options = {}) {
|
|
78
|
+
// Only anonymous streams are limited: a signed-in one is accountable, and counting it would let
|
|
79
|
+
// visitors fill the budget the admin stream needs.
|
|
80
|
+
const ip = rateLimitKey(req.ip);
|
|
81
|
+
if (!options.session) {
|
|
82
|
+
let total = 0;
|
|
83
|
+
let fromIp = 0;
|
|
84
|
+
for (const client of clients.values()) {
|
|
85
|
+
if (client.session)
|
|
86
|
+
continue;
|
|
87
|
+
total++;
|
|
88
|
+
if (client.ip === ip)
|
|
89
|
+
fromIp++;
|
|
90
|
+
}
|
|
91
|
+
if (total >= limits.maxClients || fromIp >= limits.maxClientsPerIp) {
|
|
92
|
+
return reply.code(429).send({ error: 'Too many open event streams' });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
reply.raw.setHeader('Content-Type', 'text/event-stream');
|
|
96
|
+
reply.raw.setHeader('Cache-Control', 'no-cache');
|
|
97
|
+
reply.raw.setHeader('Connection', 'keep-alive');
|
|
98
|
+
reply.raw.setHeader('X-Accel-Buffering', 'no');
|
|
99
|
+
reply.raw.flushHeaders();
|
|
100
|
+
sseService.add(reply, { filter: options.filter, session: options.session, ip });
|
|
101
|
+
reply.raw.write('event: ping\ndata: {}\n\n');
|
|
102
|
+
const pingInterval = setInterval(() => {
|
|
103
|
+
void (async () => {
|
|
104
|
+
if (options.stillAllowed && !(await options.stillAllowed().catch(() => false))) {
|
|
105
|
+
clearInterval(pingInterval);
|
|
106
|
+
end(reply);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
send(reply, `event: ping\ndata: ${JSON.stringify({ ts: Date.now() })}\n\n`);
|
|
110
|
+
if (!clients.has(reply))
|
|
111
|
+
clearInterval(pingInterval);
|
|
112
|
+
})();
|
|
113
|
+
}, options.pingIntervalMs ?? 30_000);
|
|
114
|
+
req.raw.on('close', () => { clearInterval(pingInterval); sseService.remove(reply); });
|
|
115
|
+
await new Promise((resolve) => { req.raw.on('close', resolve); });
|
|
116
|
+
}
|
|
117
|
+
return { sseService, serveEventStream };
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=sse.js.map
|
package/dist/sse.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface LatestRelease {
|
|
2
|
+
version: string;
|
|
3
|
+
url: string;
|
|
4
|
+
}
|
|
5
|
+
/** `UPDATE_CHECK=false` keeps the instance from contacting GitHub (air-gapped or privacy-minded installs). */
|
|
6
|
+
export declare function updateCheckEnabled(environment?: {
|
|
7
|
+
UPDATE_CHECK?: string | undefined;
|
|
8
|
+
}): boolean;
|
|
9
|
+
/** True when `candidate` is a higher x.y.z than `current`; false when either is not a plain release version. */
|
|
10
|
+
export declare function isNewerVersion(candidate: string, current: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Asks GitHub which release of `repository` ("owner/name") is newest and remembers it, to point out an update to
|
|
13
|
+
* whoever runs an older `version()`. All state is in the returned closure.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createUpdateCheck(options: {
|
|
16
|
+
name: string;
|
|
17
|
+
repository: string;
|
|
18
|
+
version: () => string;
|
|
19
|
+
}): {
|
|
20
|
+
checkForUpdate: () => Promise<boolean>;
|
|
21
|
+
startUpdateCheck: () => void;
|
|
22
|
+
stopUpdateCheck: () => void;
|
|
23
|
+
getAvailableUpdate: () => LatestRelease | null;
|
|
24
|
+
resetUpdateCheck: () => void;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=updateCheck.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"updateCheck.d.ts","sourceRoot":"","sources":["../src/updateCheck.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,8GAA8G;AAC9G,wBAAgB,kBAAkB,CAAC,WAAW,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAgB,GAAG,OAAO,CAE5G;AAOD,gHAAgH;AAChH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ1E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,MAAM,CAAA;CAAE;0BA2BnE,OAAO,CAAC,OAAO,CAAC;4BAUpB,IAAI;2BAaL,IAAI;8BAOD,aAAa,GAAG,IAAI;4BAKtB,IAAI;EAMlC"}
|