@chaos-overlords/server 0.1.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 (64) hide show
  1. package/LICENSE +674 -0
  2. package/NOTICE +21 -0
  3. package/README.md +33 -0
  4. package/dist/app.d.ts +7 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +60 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/container.d.ts +61 -0
  9. package/dist/container.d.ts.map +1 -0
  10. package/dist/container.js +17 -0
  11. package/dist/container.js.map +1 -0
  12. package/dist/http/errorHandler.d.ts +5 -0
  13. package/dist/http/errorHandler.d.ts.map +1 -0
  14. package/dist/http/errorHandler.js +49 -0
  15. package/dist/http/errorHandler.js.map +1 -0
  16. package/dist/http/guards.d.ts +12 -0
  17. package/dist/http/guards.d.ts.map +1 -0
  18. package/dist/http/guards.js +17 -0
  19. package/dist/http/guards.js.map +1 -0
  20. package/dist/http/middleware.d.ts +27 -0
  21. package/dist/http/middleware.d.ts.map +1 -0
  22. package/dist/http/middleware.js +70 -0
  23. package/dist/http/middleware.js.map +1 -0
  24. package/dist/http/routes.d.ts +11 -0
  25. package/dist/http/routes.d.ts.map +1 -0
  26. package/dist/http/routes.js +11 -0
  27. package/dist/http/routes.js.map +1 -0
  28. package/dist/http/types.d.ts +10 -0
  29. package/dist/http/types.d.ts.map +1 -0
  30. package/dist/http/types.js +2 -0
  31. package/dist/http/types.js.map +1 -0
  32. package/dist/index.d.ts +7 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +6 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/routes/bugReports.d.ts +12 -0
  37. package/dist/routes/bugReports.d.ts.map +1 -0
  38. package/dist/routes/bugReports.js +23 -0
  39. package/dist/routes/bugReports.js.map +1 -0
  40. package/dist/routes/events.d.ts +5 -0
  41. package/dist/routes/events.d.ts.map +1 -0
  42. package/dist/routes/events.js +39 -0
  43. package/dist/routes/events.js.map +1 -0
  44. package/dist/routes/lobby.d.ts +13 -0
  45. package/dist/routes/lobby.d.ts.map +1 -0
  46. package/dist/routes/lobby.js +63 -0
  47. package/dist/routes/lobby.js.map +1 -0
  48. package/dist/routes/snapshots.d.ts +5 -0
  49. package/dist/routes/snapshots.d.ts.map +1 -0
  50. package/dist/routes/snapshots.js +22 -0
  51. package/dist/routes/snapshots.js.map +1 -0
  52. package/dist/routes/turns.d.ts +5 -0
  53. package/dist/routes/turns.d.ts.map +1 -0
  54. package/dist/routes/turns.js +37 -0
  55. package/dist/routes/turns.js.map +1 -0
  56. package/dist/sse/LocalEventHub.d.ts +22 -0
  57. package/dist/sse/LocalEventHub.d.ts.map +1 -0
  58. package/dist/sse/LocalEventHub.js +43 -0
  59. package/dist/sse/LocalEventHub.js.map +1 -0
  60. package/dist/sse/createSseResponse.d.ts +28 -0
  61. package/dist/sse/createSseResponse.d.ts.map +1 -0
  62. package/dist/sse/createSseResponse.js +126 -0
  63. package/dist/sse/createSseResponse.js.map +1 -0
  64. package/package.json +69 -0
package/NOTICE ADDED
@@ -0,0 +1,21 @@
1
+ Chaos Overlords: New Chrome
2
+ A clean-room MonoGame reimplementation of the 1996 turn-based strategy game.
3
+
4
+ Copyright (C) 2026 kibertoad
5
+
6
+ This program is free software: you can redistribute it and/or modify it under
7
+ the terms of the GNU General Public License as published by the Free Software
8
+ Foundation, version 3 of the License.
9
+
10
+ This program is distributed in the hope that it will be useful, but WITHOUT
11
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along with
15
+ this program in the LICENSE file. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ The GPL covers the original code in this repository. It does not cover or grant
18
+ rights to the original Chaos Overlords assets. Those assets are not distributed
19
+ by this project and must be imported locally from a legally owned copy.
20
+
21
+ Project contact: https://github.com/kibertoad/chaos-overlords-new-chrome/issues
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @chaos-overlords/server
2
+
3
+ HTTP server for the [*Chaos Overlords: New Chrome*](https://github.com/kibertoad/chaos-overlords-new-chrome) multiplayer server.
4
+
5
+ The Hono application both runtimes serve: REST intents, the server-sent events stream, bearer
6
+ auth, rate limiting and the error envelope. It takes every port it needs through a container, so
7
+ hosting it is a matter of supplying storage, a clock and an event hub.
8
+
9
+ ```ts
10
+ import { createApp } from '@chaos-overlords/server'
11
+
12
+ const app = createApp(container)
13
+ ```
14
+
15
+ For a server you can just run, see `@chaos-overlords/node-server` or `@chaos-overlords/worker`.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ npm install @chaos-overlords/server
21
+ ```
22
+
23
+ Requires Node.js 22 or newer.
24
+
25
+ ## Documentation
26
+
27
+ The operator and contributor manual for the whole multiplayer workspace is in
28
+ [`multiplayer/README.md`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/multiplayer/README.md); the design notes are in
29
+ [`docs/MULTIPLAYER.md`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/docs/MULTIPLAYER.md).
30
+
31
+ ## Licence
32
+
33
+ GPL-3.0-only. See [`LICENSE`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/LICENSE) and [`NOTICE`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/NOTICE).
package/dist/app.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { Hono } from 'hono';
2
+ import type { ServerContainer } from './container';
3
+ import type { AppEnv } from './http/types';
4
+ export declare const API_PREFIX = "/api/v1";
5
+ /** The application both runtime facades serve; only the container differs between them. */
6
+ export declare function createApp(container: ServerContainer): Hono<AppEnv>;
7
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AASlD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAO1C,eAAO,MAAM,UAAU,YAAY,CAAA;AAInC,2FAA2F;AAC3F,wBAAgB,SAAS,CAAC,SAAS,EAAE,eAAe,GAAG,IAAI,CAAC,MAAM,CAAC,CAYlE"}
package/dist/app.js ADDED
@@ -0,0 +1,60 @@
1
+ import { BUG_REPORT_LIMITS, LIMITS } from '@chaos-overlords/contracts';
2
+ import { Hono } from 'hono';
3
+ import { bodyLimit } from 'hono/body-limit';
4
+ import { handleError } from './http/errorHandler';
5
+ import { bearerAuth, bugReportRateLimited, memberRateLimited, rateLimited, requestId, } from './http/middleware';
6
+ import { registerBugReportRoutes } from './routes/bugReports';
7
+ import { registerEventRoutes } from './routes/events';
8
+ import { registerMemberLobbyRoutes, registerPublicLobbyRoutes } from './routes/lobby';
9
+ import { registerSnapshotRoutes } from './routes/snapshots';
10
+ import { registerTurnRoutes } from './routes/turns';
11
+ export const API_PREFIX = '/api/v1';
12
+ const SMALL_BODY = 16 * 1024;
13
+ /** The application both runtime facades serve; only the container differs between them. */
14
+ export function createApp(container) {
15
+ const app = new Hono();
16
+ app.onError(handleError);
17
+ app.use('*', async (c, next) => {
18
+ c.set('container', container);
19
+ await next();
20
+ });
21
+ app.use('*', requestId);
22
+ app.get('/health', (c) => c.json({ ok: true }));
23
+ app.route(API_PREFIX, apiRoutes());
24
+ return app;
25
+ }
26
+ /**
27
+ * The API, mounted from the contracts.
28
+ *
29
+ * **Registration order is load-bearing.** Hono composes every matching middleware and the first
30
+ * matching handler in the order they were registered, and a handler ends the chain. The
31
+ * unauthenticated doors are therefore registered before the `/matches/:matchId/*` auth mount, which
32
+ * would otherwise also match `/matches/join` and demand a token for it. Each body limit is
33
+ * registered before the route it guards, so it runs before the contract validator reads the body —
34
+ * a limit checked after the body has been parsed guards nothing.
35
+ */
36
+ function apiRoutes() {
37
+ const api = new Hono();
38
+ // The doors a stranger can knock on, throttled per client address.
39
+ api.use('/matches', rateLimited, bodyLimit({ maxSize: LIMITS.gameSettingsBytes + SMALL_BODY }));
40
+ api.use('/matches/join', rateLimited, bodyLimit({ maxSize: SMALL_BODY }));
41
+ // The third one. It is unauthenticated like the other two and far larger than either, so it gets
42
+ // a budget of its own rather than borrowing the lobby's — see `RateLimiters.bugReport`.
43
+ api.use('/bug-reports', bugReportRateLimited, bodyLimit({ maxSize: BUG_REPORT_LIMITS.stateBase64Bytes + SMALL_BODY }));
44
+ registerPublicLobbyRoutes(api);
45
+ registerBugReportRoutes(api);
46
+ // `/:matchId/*` also matches the bare `/:matchId`, so this is the only mount: a second one for
47
+ // the bare path would authenticate (a token lookup plus a match read) and charge the member's
48
+ // rate limit twice on every request to it.
49
+ api.use('/matches/:matchId/*', bearerAuth, memberRateLimited());
50
+ api.use('/matches/:matchId/turns/:turn/orders', bodyLimit({ maxSize: LIMITS.ordersBytes }));
51
+ api.use('/matches/:matchId/turns/:turn/report', bodyLimit({ maxSize: SMALL_BODY }));
52
+ // A snapshot is a megabyte, so uploads carry their own tighter budget on top of the member one.
53
+ api.use('/matches/:matchId/snapshots', memberRateLimited('upload'), bodyLimit({ maxSize: LIMITS.snapshotBase64Bytes + SMALL_BODY }));
54
+ registerMemberLobbyRoutes(api);
55
+ registerTurnRoutes(api);
56
+ registerSnapshotRoutes(api);
57
+ registerEventRoutes(api);
58
+ return api;
59
+ }
60
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import type { BugReportService } from '@chaos-overlords/bug-reports';
2
+ import type { EventStreamOpener, Kernel, RateLimiter } from '@chaos-overlords/kernel';
3
+ import type { Context } from 'hono';
4
+ import type { AppEnv } from './http/types';
5
+ export interface ServerConfig {
6
+ /** Serve `GET /api/v1/matches` (public lobby browsing). Off by default for self-hosted servers. */
7
+ publicListing: boolean;
8
+ /** Interval between SSE keepalive comments. */
9
+ sseHeartbeatMs: number;
10
+ }
11
+ /**
12
+ * The three doors that need throttling, each with its own budget and key.
13
+ *
14
+ * `anonymous` guards create and join per client address: the only calls a stranger can make, and the
15
+ * only ones a join code can be guessed through. The other two are keyed by player token, because an
16
+ * authenticated member is also a cost: order documents are a quarter of a megabyte each and snapshot
17
+ * uploads four times that, so a buggy client in a retry loop must not be able to write without
18
+ * bound. Windows are per process, which is all a single self-hosted server needs; a public
19
+ * deployment puts its platform's rate limiting in front as the real gate.
20
+ */
21
+ export interface RateLimiters {
22
+ anonymous: RateLimiter;
23
+ member: RateLimiter;
24
+ upload: RateLimiter;
25
+ /**
26
+ * The bug report door, which is the only unauthenticated one that accepts megabytes.
27
+ *
28
+ * Its own tier rather than the anonymous one: a player filing a report and a player retrying a
29
+ * join are nothing alike in cost, and sharing a budget would mean a handful of reports locked
30
+ * somebody out of a lobby (or, the other way round, that a lobby's generous budget also bought
31
+ * thirty multi-megabyte uploads a minute from a stranger).
32
+ */
33
+ bugReport: RateLimiter;
34
+ }
35
+ export interface ServerContainer {
36
+ kernel: Kernel;
37
+ /**
38
+ * Bug report intake, backed by its own database. Absent on a deployment that has not configured
39
+ * one, where the route answers 404 rather than pretending to take reports.
40
+ */
41
+ bugReports?: BugReportService;
42
+ eventStream: EventStreamOpener;
43
+ rateLimiters: RateLimiters;
44
+ config: ServerConfig;
45
+ /** How the runtime identifies a caller for rate limiting; defaults to proxy headers. */
46
+ clientAddress?: (c: Context<AppEnv>) => string;
47
+ }
48
+ export declare const DEFAULT_SERVER_CONFIG: ServerConfig;
49
+ /** Per-minute budgets. Generous for play, far below what a retry loop would spend. */
50
+ export declare const DEFAULT_RATE_LIMITS: {
51
+ readonly anonymousPerMinute: 30;
52
+ readonly memberPerMinute: 240;
53
+ readonly uploadPerMinute: 10;
54
+ /**
55
+ * Five reports a minute per address. A player filing one files one; a player filing a second
56
+ * because the first did not seem to send is the case this has to leave room for, and everything
57
+ * beyond that is a client in a loop uploading whole match journals.
58
+ */
59
+ readonly bugReportPerMinute: 5;
60
+ };
61
+ //# sourceMappingURL=container.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container.d.ts","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,WAAW,YAAY;IAC3B,mGAAmG;IACnG,aAAa,EAAE,OAAO,CAAA;IACtB,+CAA+C;IAC/C,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,WAAW,CAAA;IACtB,MAAM,EAAE,WAAW,CAAA;IACnB,MAAM,EAAE,WAAW,CAAA;IACnB;;;;;;;OAOG;IACH,SAAS,EAAE,WAAW,CAAA;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,YAAY,EAAE,YAAY,CAAA;IAC1B,MAAM,EAAE,YAAY,CAAA;IACpB,wFAAwF;IACxF,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,MAAM,CAAA;CAC/C;AAED,eAAO,MAAM,qBAAqB,EAAE,YAGnC,CAAA;AAED,sFAAsF;AACtF,eAAO,MAAM,mBAAmB;;;;IAI9B;;;;OAIG;;CAEK,CAAA"}
@@ -0,0 +1,17 @@
1
+ export const DEFAULT_SERVER_CONFIG = {
2
+ publicListing: false,
3
+ sseHeartbeatMs: 20_000,
4
+ };
5
+ /** Per-minute budgets. Generous for play, far below what a retry loop would spend. */
6
+ export const DEFAULT_RATE_LIMITS = {
7
+ anonymousPerMinute: 30,
8
+ memberPerMinute: 240,
9
+ uploadPerMinute: 10,
10
+ /**
11
+ * Five reports a minute per address. A player filing one files one; a player filing a second
12
+ * because the first did not seem to send is the case this has to leave room for, and everything
13
+ * beyond that is a client in a loop uploading whole match journals.
14
+ */
15
+ bugReportPerMinute: 5,
16
+ };
17
+ //# sourceMappingURL=container.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container.js","sourceRoot":"","sources":["../src/container.ts"],"names":[],"mappings":"AAmDA,MAAM,CAAC,MAAM,qBAAqB,GAAiB;IACjD,aAAa,EAAE,KAAK;IACpB,cAAc,EAAE,MAAM;CACvB,CAAA;AAED,sFAAsF;AACtF,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,kBAAkB,EAAE,EAAE;IACtB,eAAe,EAAE,GAAG;IACpB,eAAe,EAAE,EAAE;IACnB;;;;OAIG;IACH,kBAAkB,EAAE,CAAC;CACb,CAAA"}
@@ -0,0 +1,5 @@
1
+ import type { Context } from 'hono';
2
+ import type { AppEnv } from './types';
3
+ /** The ONE producer of the error envelope. Controllers throw; nothing else builds `{ error }`. */
4
+ export declare function handleError(error: Error, c: Context<AppEnv>): Response;
5
+ //# sourceMappingURL=errorHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/http/errorHandler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAEnC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAErC,kGAAkG;AAClG,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,QAAQ,CA0BtE"}
@@ -0,0 +1,49 @@
1
+ import { STATUS_BY_CODE } from '@chaos-overlords/contracts';
2
+ import { isDomainError } from '@chaos-overlords/kernel';
3
+ import { SchemaValidationError } from '@toad-contracts/core';
4
+ import { HTTPException } from 'hono/http-exception';
5
+ /** The ONE producer of the error envelope. Controllers throw; nothing else builds `{ error }`. */
6
+ export function handleError(error, c) {
7
+ const requestId = c.get('requestId');
8
+ if (isDomainError(error)) {
9
+ return respond(c, error.code, error.message, error.details, requestId);
10
+ }
11
+ if (error instanceof SchemaValidationError) {
12
+ // The contract validator refused a path param, a query value or the body. The issue paths and
13
+ // messages come straight from valibot rather than being restated per endpoint, so a client is
14
+ // told which field it got wrong.
15
+ return respond(c, 'validation_failed', 'Request failed contract validation', { reason: 'invalid_request', issues: error.issues }, requestId);
16
+ }
17
+ if (error instanceof HTTPException) {
18
+ const code = codeForStatus(error.status);
19
+ return respond(c, code, error.message || code, { reason: `http_${error.status}` }, requestId);
20
+ }
21
+ c.get('container')?.kernel.deps.logger.error('unhandled request error', {
22
+ requestId,
23
+ error: error.stack ?? String(error),
24
+ });
25
+ return respond(c, 'internal', 'Internal server error', { reason: 'internal' }, requestId);
26
+ }
27
+ /**
28
+ * The error code a framework `HTTPException` maps to. Written out rather than derived by scanning
29
+ * `STATUS_BY_CODE` for a matching value: that scan silently picks whichever code was declared first
30
+ * the moment two of them share a status, and the mapping is worth being able to read.
31
+ */
32
+ const CODE_BY_STATUS = {
33
+ 400: 'bad_request',
34
+ 401: 'unauthorized',
35
+ 403: 'forbidden',
36
+ 404: 'not_found',
37
+ 409: 'conflict',
38
+ 413: 'payload_too_large',
39
+ 422: 'validation_failed',
40
+ 429: 'rate_limited',
41
+ };
42
+ function codeForStatus(status) {
43
+ return CODE_BY_STATUS[status] ?? 'internal';
44
+ }
45
+ function respond(c, code, message, details, requestId) {
46
+ const body = { error: { code, message, details, requestId } };
47
+ return c.json(body, STATUS_BY_CODE[code]);
48
+ }
49
+ //# sourceMappingURL=errorHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/http/errorHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAGnD,kGAAkG;AAClG,MAAM,UAAU,WAAW,CAAC,KAAY,EAAE,CAAkB;IAC1D,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IACpC,IAAI,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;IACxE,CAAC;IACD,IAAI,KAAK,YAAY,qBAAqB,EAAE,CAAC;QAC3C,8FAA8F;QAC9F,8FAA8F;QAC9F,iCAAiC;QACjC,OAAO,OAAO,CACZ,CAAC,EACD,mBAAmB,EACnB,oCAAoC,EACpC,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EACnD,SAAS,CACV,CAAA;IACH,CAAC;IACD,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACxC,OAAO,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,SAAS,CAAC,CAAA;IAC/F,CAAC;IACD,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,EAAE;QACtE,SAAS;QACT,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;KACpC,CAAC,CAAA;IACF,OAAO,OAAO,CAAC,CAAC,EAAE,UAAU,EAAE,uBAAuB,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,SAAS,CAAC,CAAA;AAC3F,CAAC;AAED;;;;GAIG;AACH,MAAM,cAAc,GAAwC;IAC1D,GAAG,EAAE,aAAa;IAClB,GAAG,EAAE,cAAc;IACnB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,mBAAmB;IACxB,GAAG,EAAE,mBAAmB;IACxB,GAAG,EAAE,cAAc;CACpB,CAAA;AAED,SAAS,aAAa,CAAC,MAAc;IACnC,OAAO,cAAc,CAAC,MAAM,CAAC,IAAI,UAAU,CAAA;AAC7C,CAAC;AAED,SAAS,OAAO,CACd,CAAkB,EAClB,IAAe,EACf,OAAe,EACf,OAAuD,EACvD,SAAiB;IAEjB,MAAM,IAAI,GAAkB,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,CAAA;IAC5E,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,IAAI,CAAQ,CAAC,CAAA;AAClD,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { Principal } from '@chaos-overlords/kernel';
2
+ /**
3
+ * The principal, checked against the `:matchId` the request named.
4
+ *
5
+ * A token for another match answers 404, never 403, so a player cannot probe which match ids exist.
6
+ *
7
+ * It takes the two values rather than the context because each contract handler's context is typed
8
+ * from its own contract, and Hono's `Context` is invariant in its env: one guard signature could
9
+ * not accept all of them.
10
+ */
11
+ export declare function requireMember(principal: Principal, matchId: string): Principal;
12
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/http/guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAGxD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAK9E"}
@@ -0,0 +1,17 @@
1
+ import { NotFoundError } from '@chaos-overlords/kernel';
2
+ /**
3
+ * The principal, checked against the `:matchId` the request named.
4
+ *
5
+ * A token for another match answers 404, never 403, so a player cannot probe which match ids exist.
6
+ *
7
+ * It takes the two values rather than the context because each contract handler's context is typed
8
+ * from its own contract, and Hono's `Context` is invariant in its env: one guard signature could
9
+ * not accept all of them.
10
+ */
11
+ export function requireMember(principal, matchId) {
12
+ if (principal.match.id !== matchId) {
13
+ throw new NotFoundError('No such match', { reason: 'unknown_match' });
14
+ }
15
+ return principal;
16
+ }
17
+ //# sourceMappingURL=guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/http/guards.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAEvD;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,SAAoB,EAAE,OAAe;IACjE,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,KAAK,OAAO,EAAE,CAAC;QACnC,MAAM,IAAI,aAAa,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAA;IACvE,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { Context, MiddlewareHandler } from 'hono';
2
+ import type { RateLimiters } from '../container';
3
+ import type { AppEnv } from './types';
4
+ /**
5
+ * Mints or adopts `X-Request-Id`; it rides every error envelope so a player can quote it.
6
+ *
7
+ * The header is set before the handler runs, not after: a handler that throws never comes back
8
+ * here, and the response most worth correlating is exactly the one that failed.
9
+ */
10
+ export declare const requestId: MiddlewareHandler<AppEnv>;
11
+ /** Resolves the bearer token to a principal or refuses with 401. */
12
+ export declare const bearerAuth: MiddlewareHandler<AppEnv>;
13
+ /** Fixed-window limiter on the unauthenticated doors, keyed by client address. */
14
+ export declare const rateLimited: MiddlewareHandler<AppEnv>;
15
+ /**
16
+ * Limiter for the bug report door, keyed by client address like the other unauthenticated ones but
17
+ * spending its own budget: a report is a rare, large call and a join is a frequent, tiny one, and
18
+ * one must not be able to exhaust the other.
19
+ */
20
+ export declare const bugReportRateLimited: MiddlewareHandler<AppEnv>;
21
+ /**
22
+ * Limiter for an authenticated member, keyed by player rather than address so one player on a shared
23
+ * address cannot spend another's budget. Must run after `bearerAuth`.
24
+ */
25
+ export declare function memberRateLimited(tier?: keyof RateLimiters): MiddlewareHandler<AppEnv>;
26
+ export declare function defaultClientAddress(c: Context<AppEnv>): string;
27
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/http/middleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAErC;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,iBAAiB,CAAC,MAAM,CAK/C,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,UAAU,EAAE,iBAAiB,CAAC,MAAM,CAUhD,CAAA;AAED,kFAAkF;AAClF,eAAO,MAAM,WAAW,EAAE,iBAAiB,CAAC,MAAM,CAKjD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC,MAAM,CAK1D,CAAA;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,MAAM,YAAuB,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAMhG;AAiBD,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAM/D"}
@@ -0,0 +1,70 @@
1
+ import { RateLimitedError, UnauthorizedError } from '@chaos-overlords/kernel';
2
+ /**
3
+ * Mints or adopts `X-Request-Id`; it rides every error envelope so a player can quote it.
4
+ *
5
+ * The header is set before the handler runs, not after: a handler that throws never comes back
6
+ * here, and the response most worth correlating is exactly the one that failed.
7
+ */
8
+ export const requestId = async (c, next) => {
9
+ const id = c.req.header('x-request-id')?.slice(0, 64) || crypto.randomUUID();
10
+ c.set('requestId', id);
11
+ c.header('X-Request-Id', id);
12
+ await next();
13
+ };
14
+ /** Resolves the bearer token to a principal or refuses with 401. */
15
+ export const bearerAuth = async (c, next) => {
16
+ const header = c.req.header('authorization') ?? '';
17
+ const [scheme, token] = header.split(' ', 2);
18
+ if (scheme?.toLowerCase() !== 'bearer' || !token) {
19
+ throw new UnauthorizedError('Send the player token as a Bearer credential', {
20
+ reason: 'missing_token',
21
+ });
22
+ }
23
+ c.set('principal', await c.get('container').kernel.auth.authenticate(token));
24
+ await next();
25
+ };
26
+ /** Fixed-window limiter on the unauthenticated doors, keyed by client address. */
27
+ export const rateLimited = async (c, next) => {
28
+ const container = c.get('container');
29
+ const key = (container.clientAddress ?? defaultClientAddress)(c);
30
+ enforce(container.rateLimiters, 'anonymous', key, c);
31
+ await next();
32
+ };
33
+ /**
34
+ * Limiter for the bug report door, keyed by client address like the other unauthenticated ones but
35
+ * spending its own budget: a report is a rare, large call and a join is a frequent, tiny one, and
36
+ * one must not be able to exhaust the other.
37
+ */
38
+ export const bugReportRateLimited = async (c, next) => {
39
+ const container = c.get('container');
40
+ const key = (container.clientAddress ?? defaultClientAddress)(c);
41
+ enforce(container.rateLimiters, 'bugReport', key, c);
42
+ await next();
43
+ };
44
+ /**
45
+ * Limiter for an authenticated member, keyed by player rather than address so one player on a shared
46
+ * address cannot spend another's budget. Must run after `bearerAuth`.
47
+ */
48
+ export function memberRateLimited(tier = 'member') {
49
+ return async (c, next) => {
50
+ const container = c.get('container');
51
+ enforce(container.rateLimiters, tier, c.get('principal').player.id, c);
52
+ await next();
53
+ };
54
+ }
55
+ function enforce(limiters, tier, key, c) {
56
+ const retryAfter = limiters[tier].take(`${tier}:${key}`);
57
+ if (retryAfter === null)
58
+ return;
59
+ c.header('Retry-After', String(retryAfter));
60
+ throw new RateLimitedError('Too many attempts; slow down', {
61
+ reason: 'rate_limited',
62
+ retryAfterSeconds: retryAfter,
63
+ });
64
+ }
65
+ export function defaultClientAddress(c) {
66
+ return (c.req.header('cf-connecting-ip') ??
67
+ c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ??
68
+ 'unknown');
69
+ }
70
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/http/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { AppEnv } from './types';
2
+ /**
3
+ * Route registration bound to this app's environment, so a contract handler's `c.get('container')`
4
+ * resolves alongside `c.req.valid(...)`.
5
+ *
6
+ * Every route is mounted from a contract in `@chaos-overlords/contracts`: the method, the path and
7
+ * the validation of each request target come from the one definition the TypeScript client calls
8
+ * through and the C# client's records are generated from. Nothing here restates a path.
9
+ */
10
+ export declare const buildHonoRoute: <const TContract extends import("@toad-contracts/core").ApiContract, TSchema extends import("hono").Schema = import("hono/types").BlankSchema, TBasePath extends string = "/">(app: import("hono").Hono<AppEnv, TSchema, TBasePath>, contract: TContract, handler: import("@toad-contracts/hono").HonoContractHandler<TContract, AppEnv>, options?: import("@toad-contracts/hono").BuildHonoRouteOptions) => import("hono").Hono<AppEnv, TSchema, TBasePath>;
11
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/http/routes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAErC;;;;;;;GAOG;AACH,eAAO,MAAQ,cAAc,8bAAiC,CAAA"}
@@ -0,0 +1,11 @@
1
+ import { honoContractRoutes } from '@toad-contracts/hono';
2
+ /**
3
+ * Route registration bound to this app's environment, so a contract handler's `c.get('container')`
4
+ * resolves alongside `c.req.valid(...)`.
5
+ *
6
+ * Every route is mounted from a contract in `@chaos-overlords/contracts`: the method, the path and
7
+ * the validation of each request target come from the one definition the TypeScript client calls
8
+ * through and the C# client's records are generated from. Nothing here restates a path.
9
+ */
10
+ export const { buildHonoRoute } = honoContractRoutes();
11
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/http/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAGzD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,kBAAkB,EAAU,CAAA"}
@@ -0,0 +1,10 @@
1
+ import type { Principal } from '@chaos-overlords/kernel';
2
+ import type { ServerContainer } from '../container';
3
+ export interface AppEnv {
4
+ Variables: {
5
+ container: ServerContainer;
6
+ requestId: string;
7
+ principal: Principal;
8
+ };
9
+ }
10
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/http/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAEnD,MAAM,WAAW,MAAM;IACrB,SAAS,EAAE;QACT,SAAS,EAAE,eAAe,CAAA;QAC1B,SAAS,EAAE,MAAM,CAAA;QACjB,SAAS,EAAE,SAAS,CAAA;KACrB,CAAA;CACF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/http/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ export { API_PREFIX, createApp } from './app';
2
+ export { DEFAULT_RATE_LIMITS, DEFAULT_SERVER_CONFIG, type RateLimiters, type ServerConfig, type ServerContainer, } from './container';
3
+ export { bugReportRateLimited, defaultClientAddress } from './http/middleware';
4
+ export type { AppEnv } from './http/types';
5
+ export { createSseResponse, type EventStreamSource, formatEvent } from './sse/createSseResponse';
6
+ export { LocalEventHub } from './sse/LocalEventHub';
7
+ //# 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,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAC7C,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC9E,YAAY,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAChG,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { API_PREFIX, createApp } from './app';
2
+ export { DEFAULT_RATE_LIMITS, DEFAULT_SERVER_CONFIG, } from './container';
3
+ export { bugReportRateLimited, defaultClientAddress } from './http/middleware';
4
+ export { createSseResponse, formatEvent } from './sse/createSseResponse';
5
+ export { LocalEventHub } from './sse/LocalEventHub';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAC7C,OAAO,EACL,mBAAmB,EACnB,qBAAqB,GAItB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAE9E,OAAO,EAAE,iBAAiB,EAA0B,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAChG,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA"}
@@ -0,0 +1,12 @@
1
+ import type { Hono } from 'hono';
2
+ import type { AppEnv } from '../http/types';
3
+ /**
4
+ * The bug report door.
5
+ *
6
+ * Unauthenticated, and registered alongside the two lobby doors that are. A deployment that has not
7
+ * configured the intake answers 404 rather than 500: "this server does not take bug reports" is a
8
+ * true and actionable thing to tell a client, and it is the same shape the public lobby listing
9
+ * uses when it is switched off.
10
+ */
11
+ export declare function registerBugReportRoutes(api: Hono<AppEnv>): void;
12
+ //# sourceMappingURL=bugReports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bugReports.d.ts","sourceRoot":"","sources":["../../src/routes/bugReports.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEhC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAU/D"}
@@ -0,0 +1,23 @@
1
+ import { submitBugReportContract } from '@chaos-overlords/contracts';
2
+ import { NotFoundError } from '@chaos-overlords/kernel';
3
+ import { buildHonoRoute } from '../http/routes';
4
+ /**
5
+ * The bug report door.
6
+ *
7
+ * Unauthenticated, and registered alongside the two lobby doors that are. A deployment that has not
8
+ * configured the intake answers 404 rather than 500: "this server does not take bug reports" is a
9
+ * true and actionable thing to tell a client, and it is the same shape the public lobby listing
10
+ * uses when it is switched off.
11
+ */
12
+ export function registerBugReportRoutes(api) {
13
+ buildHonoRoute(api, submitBugReportContract, async (c) => {
14
+ const { bugReports } = c.get('container');
15
+ if (!bugReports) {
16
+ throw new NotFoundError('This server does not accept bug reports', {
17
+ reason: 'bug_reports_disabled',
18
+ });
19
+ }
20
+ return c.json(await bugReports.submit(c.req.valid('json')), 201);
21
+ });
22
+ }
23
+ //# sourceMappingURL=bugReports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bugReports.js","sourceRoot":"","sources":["../../src/routes/bugReports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAA;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAG/C;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CAAC,GAAiB;IACvD,cAAc,CAAC,GAAG,EAAE,uBAAuB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACvD,MAAM,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QACzC,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,aAAa,CAAC,yCAAyC,EAAE;gBACjE,MAAM,EAAE,sBAAsB;aAC/B,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAClE,CAAC,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Hono } from 'hono';
2
+ import type { AppEnv } from '../http/types';
3
+ /** The event log: a paged REST read (the fallback) and the SSE stream over the same log. */
4
+ export declare function registerEventRoutes(api: Hono<AppEnv>): void;
5
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/routes/events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAGhC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAmB3D"}
@@ -0,0 +1,39 @@
1
+ import { listEventsContract, streamEventsContract } from '@chaos-overlords/contracts';
2
+ import { requireMember } from '../http/guards';
3
+ import { buildHonoRoute } from '../http/routes';
4
+ /** The event log: a paged REST read (the fallback) and the SSE stream over the same log. */
5
+ export function registerEventRoutes(api) {
6
+ buildHonoRoute(api, listEventsContract, async (c) => {
7
+ const principal = requireMember(c.get('principal'), c.req.valid('param').matchId);
8
+ const { after, limit } = c.req.valid('query');
9
+ const events = await c
10
+ .get('container')
11
+ .kernel.deps.storage.events.listAfter(principal.match.id, after, limit);
12
+ return c.json({ events }, 200);
13
+ });
14
+ buildHonoRoute(api, streamEventsContract, async (c) => {
15
+ const principal = requireMember(c.get('principal'), c.req.valid('param').matchId);
16
+ const afterSeq = resumePoint(c.req.header('last-event-id'), c.req.query('after'));
17
+ return c.get('container').eventStream.open({
18
+ matchId: principal.match.id,
19
+ afterSeq,
20
+ signal: c.req.raw.signal,
21
+ });
22
+ });
23
+ }
24
+ /**
25
+ * `Last-Event-ID` (what an EventSource resends) wins over `?after=`; both default to 0.
26
+ *
27
+ * The stream reads these itself rather than through a query schema because a browser's own
28
+ * `EventSource` sets the header and nothing else, and a resume point that failed validation should
29
+ * restart the stream from the beginning rather than refuse the connection.
30
+ */
31
+ function resumePoint(lastEventId, after) {
32
+ for (const candidate of [lastEventId, after]) {
33
+ const parsed = Number(candidate);
34
+ if (candidate !== undefined && Number.isInteger(parsed) && parsed >= 0)
35
+ return parsed;
36
+ }
37
+ return 0;
38
+ }
39
+ //# sourceMappingURL=events.js.map