@chaos-overlords/kernel 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.
- package/LICENSE +674 -0
- package/NOTICE +21 -0
- package/README.md +28 -0
- package/dist/domain/entities.d.ts +91 -0
- package/dist/domain/entities.d.ts.map +1 -0
- package/dist/domain/entities.js +9 -0
- package/dist/domain/entities.js.map +1 -0
- package/dist/domain/errors.d.ts +30 -0
- package/dist/domain/errors.d.ts.map +1 -0
- package/dist/domain/errors.js +45 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/logic/canonical-json.d.ts +17 -0
- package/dist/logic/canonical-json.d.ts.map +1 -0
- package/dist/logic/canonical-json.js +44 -0
- package/dist/logic/canonical-json.js.map +1 -0
- package/dist/logic/crypto.d.ts +29 -0
- package/dist/logic/crypto.d.ts.map +1 -0
- package/dist/logic/crypto.js +76 -0
- package/dist/logic/crypto.js.map +1 -0
- package/dist/logic/index.d.ts +5 -0
- package/dist/logic/index.d.ts.map +1 -0
- package/dist/logic/index.js +5 -0
- package/dist/logic/index.js.map +1 -0
- package/dist/logic/password.d.ts +4 -0
- package/dist/logic/password.d.ts.map +1 -0
- package/dist/logic/password.js +38 -0
- package/dist/logic/password.js.map +1 -0
- package/dist/logic/turn-logic.d.ts +52 -0
- package/dist/logic/turn-logic.d.ts.map +1 -0
- package/dist/logic/turn-logic.js +101 -0
- package/dist/logic/turn-logic.js.map +1 -0
- package/dist/ports/index.d.ts +3 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +3 -0
- package/dist/ports/index.js.map +1 -0
- package/dist/ports/runtime.d.ts +38 -0
- package/dist/ports/runtime.d.ts.map +1 -0
- package/dist/ports/runtime.js +7 -0
- package/dist/ports/runtime.js.map +1 -0
- package/dist/ports/storage.d.ts +141 -0
- package/dist/ports/storage.d.ts.map +1 -0
- package/dist/ports/storage.js +2 -0
- package/dist/ports/storage.js.map +1 -0
- package/dist/services/AuthService.d.ts +13 -0
- package/dist/services/AuthService.d.ts.map +1 -0
- package/dist/services/AuthService.js +24 -0
- package/dist/services/AuthService.js.map +1 -0
- package/dist/services/EventPublisher.d.ts +20 -0
- package/dist/services/EventPublisher.d.ts.map +1 -0
- package/dist/services/EventPublisher.js +37 -0
- package/dist/services/EventPublisher.js.map +1 -0
- package/dist/services/LobbyService.d.ts +45 -0
- package/dist/services/LobbyService.d.ts.map +1 -0
- package/dist/services/LobbyService.js +366 -0
- package/dist/services/LobbyService.js.map +1 -0
- package/dist/services/MatchQueryService.d.ts +24 -0
- package/dist/services/MatchQueryService.d.ts.map +1 -0
- package/dist/services/MatchQueryService.js +99 -0
- package/dist/services/MatchQueryService.js.map +1 -0
- package/dist/services/RateLimiter.d.ts +28 -0
- package/dist/services/RateLimiter.d.ts.map +1 -0
- package/dist/services/RateLimiter.js +48 -0
- package/dist/services/RateLimiter.js.map +1 -0
- package/dist/services/RetentionService.d.ts +23 -0
- package/dist/services/RetentionService.d.ts.map +1 -0
- package/dist/services/RetentionService.js +36 -0
- package/dist/services/RetentionService.js.map +1 -0
- package/dist/services/SnapshotService.d.ts +40 -0
- package/dist/services/SnapshotService.d.ts.map +1 -0
- package/dist/services/SnapshotService.js +137 -0
- package/dist/services/SnapshotService.js.map +1 -0
- package/dist/services/TurnService.d.ts +63 -0
- package/dist/services/TurnService.d.ts.map +1 -0
- package/dist/services/TurnService.js +392 -0
- package/dist/services/TurnService.js.map +1 -0
- package/dist/services/createKernel.d.ts +23 -0
- package/dist/services/createKernel.d.ts.map +1 -0
- package/dist/services/createKernel.js +22 -0
- package/dist/services/createKernel.js.map +1 -0
- package/dist/services/deps.d.ts +11 -0
- package/dist/services/deps.d.ts.map +1 -0
- package/dist/services/deps.js +2 -0
- package/dist/services/deps.js.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/index.js.map +1 -0
- package/dist/testing/InMemoryStorage.d.ts +28 -0
- package/dist/testing/InMemoryStorage.d.ts.map +1 -0
- package/dist/testing/InMemoryStorage.js +279 -0
- package/dist/testing/InMemoryStorage.js.map +1 -0
- package/dist/testing/fakes.d.ts +38 -0
- package/dist/testing/fakes.d.ts.map +1 -0
- package/dist/testing/fakes.js +44 -0
- package/dist/testing/fakes.js.map +1 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +68 -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,28 @@
|
|
|
1
|
+
# @chaos-overlords/kernel
|
|
2
|
+
|
|
3
|
+
Domain kernel for the [*Chaos Overlords: New Chrome*](https://github.com/kibertoad/chaos-overlords-new-chrome) multiplayer server.
|
|
4
|
+
|
|
5
|
+
The runtime-neutral half of the multiplayer server: entities, the storage and runtime ports,
|
|
6
|
+
pure turn logic, and the lobby, turn and snapshot services. It knows nothing about HTTP, SQL or the
|
|
7
|
+
runtime it is hosted in — those arrive as ports, which is what lets the same domain run on Node.js
|
|
8
|
+
and on Cloudflare Workers.
|
|
9
|
+
|
|
10
|
+
`@chaos-overlords/kernel/testing` carries an in-memory storage for hermetic tests.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @chaos-overlords/kernel
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires Node.js 22 or newer.
|
|
19
|
+
|
|
20
|
+
## Documentation
|
|
21
|
+
|
|
22
|
+
The operator and contributor manual for the whole multiplayer workspace is in
|
|
23
|
+
[`multiplayer/README.md`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/multiplayer/README.md); the design notes are in
|
|
24
|
+
[`docs/MULTIPLAYER.md`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/docs/MULTIPLAYER.md).
|
|
25
|
+
|
|
26
|
+
## Licence
|
|
27
|
+
|
|
28
|
+
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).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { MatchEvent, MatchSettings, MatchStatus, OrderDocument, PlayerStatus, TurnStatus } from '@chaos-overlords/contracts';
|
|
2
|
+
export interface Match {
|
|
3
|
+
id: string;
|
|
4
|
+
status: MatchStatus;
|
|
5
|
+
settings: MatchSettings;
|
|
6
|
+
hostPlayerId: string;
|
|
7
|
+
joinCode: string;
|
|
8
|
+
passwordHash: string | null;
|
|
9
|
+
seed: number | null;
|
|
10
|
+
/** 0 in the lobby; the open turn number once running. */
|
|
11
|
+
currentTurn: number;
|
|
12
|
+
/** Seats taken in the lobby; capacity is enforced on this counter atomically. */
|
|
13
|
+
seatCount: number;
|
|
14
|
+
/**
|
|
15
|
+
* Monotonic count of seats ever claimed. It never decreases, so the value handed to each player
|
|
16
|
+
* as `joinOrder` is a stable total order even after someone leaves and a newcomer takes the seat.
|
|
17
|
+
*/
|
|
18
|
+
joinCounter: number;
|
|
19
|
+
createdAt: Date;
|
|
20
|
+
updatedAt: Date;
|
|
21
|
+
}
|
|
22
|
+
export interface Player {
|
|
23
|
+
id: string;
|
|
24
|
+
matchId: string;
|
|
25
|
+
/** Assigned at match start; -1 while in the lobby. */
|
|
26
|
+
slot: number;
|
|
27
|
+
/** Position in the match's monotonic join sequence; the host is always 0. */
|
|
28
|
+
joinOrder: number;
|
|
29
|
+
displayName: string;
|
|
30
|
+
/**
|
|
31
|
+
* SHA-256 of the player's bearer token, or null once the membership is revoked (left or kicked
|
|
32
|
+
* from a running match). A null hash matches no token, so revocation needs no extra check.
|
|
33
|
+
*/
|
|
34
|
+
tokenHash: string | null;
|
|
35
|
+
status: PlayerStatus;
|
|
36
|
+
joinedAt: Date;
|
|
37
|
+
}
|
|
38
|
+
/** One participant of a sealed turn: the slot its orders were hashed under. */
|
|
39
|
+
export interface SealedSlot {
|
|
40
|
+
playerId: string;
|
|
41
|
+
slot: number;
|
|
42
|
+
}
|
|
43
|
+
export interface Turn {
|
|
44
|
+
matchId: string;
|
|
45
|
+
number: number;
|
|
46
|
+
status: TurnStatus;
|
|
47
|
+
openedAt: Date;
|
|
48
|
+
deadlineAt: Date | null;
|
|
49
|
+
sealedAt: Date | null;
|
|
50
|
+
orderSetHash: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* The participants whose orders the seal folded into `orderSetHash`, frozen at seal time and
|
|
53
|
+
* never recomputed. Readers of the sealed set serve exactly these rows, so the set a client
|
|
54
|
+
* fetches always re-hashes to the digest that was announced, no matter who leaves afterwards.
|
|
55
|
+
*/
|
|
56
|
+
sealedSlots: readonly SealedSlot[] | null;
|
|
57
|
+
}
|
|
58
|
+
/** One player's row for a turn. Rows are pre-created when the turn opens (see TurnRepository). */
|
|
59
|
+
export interface TurnOrders {
|
|
60
|
+
matchId: string;
|
|
61
|
+
turn: number;
|
|
62
|
+
playerId: string;
|
|
63
|
+
orders: OrderDocument | null;
|
|
64
|
+
ordersHash: string | null;
|
|
65
|
+
ready: boolean;
|
|
66
|
+
submittedAt: Date | null;
|
|
67
|
+
}
|
|
68
|
+
export interface TurnReport {
|
|
69
|
+
matchId: string;
|
|
70
|
+
turn: number;
|
|
71
|
+
playerId: string;
|
|
72
|
+
stateHash: string;
|
|
73
|
+
finished: boolean;
|
|
74
|
+
reportedAt: Date;
|
|
75
|
+
}
|
|
76
|
+
export interface Snapshot {
|
|
77
|
+
matchId: string;
|
|
78
|
+
turn: number;
|
|
79
|
+
formatVersion: number;
|
|
80
|
+
stateHash: string;
|
|
81
|
+
uploadedByPlayerId: string;
|
|
82
|
+
uploadedAt: Date;
|
|
83
|
+
/** Base64 of the client's native snapshot bytes; the server never decodes it. */
|
|
84
|
+
body: string;
|
|
85
|
+
}
|
|
86
|
+
export type PersistedEvent = MatchEvent;
|
|
87
|
+
export declare const ACTIVE_PLAYER: PlayerStatus;
|
|
88
|
+
export declare function activePlayers(players: readonly Player[]): Player[];
|
|
89
|
+
/** Human-controlled seats, including an absent player the lobby elected to keep waiting for. */
|
|
90
|
+
export declare function humanParticipants(players: readonly Player[]): Player[];
|
|
91
|
+
//# sourceMappingURL=entities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entities.d.ts","sourceRoot":"","sources":["../../src/domain/entities.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const ACTIVE_PLAYER = 'active';
|
|
2
|
+
export function activePlayers(players) {
|
|
3
|
+
return players.filter((player) => player.status === ACTIVE_PLAYER);
|
|
4
|
+
}
|
|
5
|
+
/** Human-controlled seats, including an absent player the lobby elected to keep waiting for. */
|
|
6
|
+
export function humanParticipants(players) {
|
|
7
|
+
return players.filter((player) => player.status === 'active' || player.status === 'takeoverPending');
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=entities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entities.js","sourceRoot":"","sources":["../../src/domain/entities.ts"],"names":[],"mappings":"AAsGA,MAAM,CAAC,MAAM,aAAa,GAAiB,QAAQ,CAAA;AAEnD,MAAM,UAAU,aAAa,CAAC,OAA0B;IACtD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,aAAa,CAAC,CAAA;AACpE,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,iBAAiB,CAAC,OAA0B;IAC1D,OAAO,OAAO,CAAC,MAAM,CACnB,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,iBAAiB,CAC9E,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ErrorCode } from '@chaos-overlords/contracts';
|
|
2
|
+
export type ErrorDetails = Record<string, unknown> & {
|
|
3
|
+
reason?: string;
|
|
4
|
+
};
|
|
5
|
+
/** A refusal. Controllers throw these; the HTTP layer renders the one wire envelope. */
|
|
6
|
+
export declare class DomainError extends Error {
|
|
7
|
+
readonly code: ErrorCode;
|
|
8
|
+
readonly details: ErrorDetails;
|
|
9
|
+
constructor(code: ErrorCode, message: string, details?: ErrorDetails);
|
|
10
|
+
}
|
|
11
|
+
export declare class NotFoundError extends DomainError {
|
|
12
|
+
constructor(message: string, details?: ErrorDetails);
|
|
13
|
+
}
|
|
14
|
+
export declare class UnauthorizedError extends DomainError {
|
|
15
|
+
constructor(message: string, details?: ErrorDetails);
|
|
16
|
+
}
|
|
17
|
+
export declare class ForbiddenError extends DomainError {
|
|
18
|
+
constructor(message: string, details?: ErrorDetails);
|
|
19
|
+
}
|
|
20
|
+
export declare class ConflictError extends DomainError {
|
|
21
|
+
constructor(message: string, details?: ErrorDetails);
|
|
22
|
+
}
|
|
23
|
+
export declare class ValidationError extends DomainError {
|
|
24
|
+
constructor(message: string, details?: ErrorDetails);
|
|
25
|
+
}
|
|
26
|
+
export declare class RateLimitedError extends DomainError {
|
|
27
|
+
constructor(message: string, details?: ErrorDetails);
|
|
28
|
+
}
|
|
29
|
+
export declare function isDomainError(error: unknown): error is DomainError;
|
|
30
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/domain/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAE3D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAExE,wFAAwF;AACxF,qBAAa,WAAY,SAAQ,KAAK;IAElC,QAAQ,CAAC,IAAI,EAAE,SAAS;IAExB,QAAQ,CAAC,OAAO,EAAE,YAAY;gBAFrB,IAAI,EAAE,SAAS,EACxB,OAAO,EAAE,MAAM,EACN,OAAO,GAAE,YAAiB;CAKtC;AAED,qBAAa,aAAc,SAAQ,WAAW;gBAChC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB;CAGxD;AACD,qBAAa,iBAAkB,SAAQ,WAAW;gBACpC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB;CAGxD;AACD,qBAAa,cAAe,SAAQ,WAAW;gBACjC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB;CAGxD;AACD,qBAAa,aAAc,SAAQ,WAAW;gBAChC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB;CAGxD;AACD,qBAAa,eAAgB,SAAQ,WAAW;gBAClC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB;CAGxD;AACD,qBAAa,gBAAiB,SAAQ,WAAW;gBACnC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB;CAGxD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAElE"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** A refusal. Controllers throw these; the HTTP layer renders the one wire envelope. */
|
|
2
|
+
export class DomainError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, details = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
this.name = new.target.name;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class NotFoundError extends DomainError {
|
|
13
|
+
constructor(message, details = {}) {
|
|
14
|
+
super('not_found', message, details);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class UnauthorizedError extends DomainError {
|
|
18
|
+
constructor(message, details = {}) {
|
|
19
|
+
super('unauthorized', message, details);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class ForbiddenError extends DomainError {
|
|
23
|
+
constructor(message, details = {}) {
|
|
24
|
+
super('forbidden', message, details);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class ConflictError extends DomainError {
|
|
28
|
+
constructor(message, details = {}) {
|
|
29
|
+
super('conflict', message, details);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class ValidationError extends DomainError {
|
|
33
|
+
constructor(message, details = {}) {
|
|
34
|
+
super('validation_failed', message, details);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class RateLimitedError extends DomainError {
|
|
38
|
+
constructor(message, details = {}) {
|
|
39
|
+
super('rate_limited', message, details);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function isDomainError(error) {
|
|
43
|
+
return error instanceof DomainError;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/domain/errors.ts"],"names":[],"mappings":"AAIA,wFAAwF;AACxF,MAAM,OAAO,WAAY,SAAQ,KAAK;IAEzB;IAEA;IAHX,YACW,IAAe,EACxB,OAAe,EACN,UAAwB,EAAE;QAEnC,KAAK,CAAC,OAAO,CAAC,CAAA;QAJL,SAAI,GAAJ,IAAI,CAAW;QAEf,YAAO,GAAP,OAAO,CAAmB;QAGnC,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAA;IAC7B,CAAC;CACF;AAED,MAAM,OAAO,aAAc,SAAQ,WAAW;IAC5C,YAAY,OAAe,EAAE,UAAwB,EAAE;QACrD,KAAK,CAAC,WAAW,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACtC,CAAC;CACF;AACD,MAAM,OAAO,iBAAkB,SAAQ,WAAW;IAChD,YAAY,OAAe,EAAE,UAAwB,EAAE;QACrD,KAAK,CAAC,cAAc,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACzC,CAAC;CACF;AACD,MAAM,OAAO,cAAe,SAAQ,WAAW;IAC7C,YAAY,OAAe,EAAE,UAAwB,EAAE;QACrD,KAAK,CAAC,WAAW,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACtC,CAAC;CACF;AACD,MAAM,OAAO,aAAc,SAAQ,WAAW;IAC5C,YAAY,OAAe,EAAE,UAAwB,EAAE;QACrD,KAAK,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACrC,CAAC;CACF;AACD,MAAM,OAAO,eAAgB,SAAQ,WAAW;IAC9C,YAAY,OAAe,EAAE,UAAwB,EAAE;QACrD,KAAK,CAAC,mBAAmB,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IAC9C,CAAC;CACF;AACD,MAAM,OAAO,gBAAiB,SAAQ,WAAW;IAC/C,YAAY,OAAe,EAAE,UAAwB,EAAE;QACrD,KAAK,CAAC,cAAc,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IACzC,CAAC;CACF;AAED,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,KAAK,YAAY,WAAW,CAAA;AACrC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA;AACvB,cAAc,YAAY,CAAA;AAC1B,cAAc,yBAAyB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,SAAS,CAAA;AACvB,cAAc,YAAY,CAAA;AAC1B,cAAc,yBAAyB,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic JSON, the form every order digest is taken over: object keys sorted by UTF-16 code
|
|
3
|
+
* unit (JavaScript's default string order, .NET's `StringComparer.Ordinal`), no whitespace, and
|
|
4
|
+
* numbers restricted to safe integers.
|
|
5
|
+
*
|
|
6
|
+
* The restriction is the point: a client written in another language has to reproduce this text
|
|
7
|
+
* byte for byte to verify `ordersHash`, and floating-point shortest round-trip text is not portable
|
|
8
|
+
* (`1e+21` here, `1E+21` in .NET). Integers, booleans, null and JSON-escaped strings are. Anything
|
|
9
|
+
* outside that is a programming error, not a runtime condition, so it throws rather than hashing
|
|
10
|
+
* something a peer cannot reproduce.
|
|
11
|
+
*/
|
|
12
|
+
export declare function canonicalJson(value: unknown): string;
|
|
13
|
+
export declare class NonCanonicalValueError extends Error {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
constructor(path: string, detail: string);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=canonical-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical-json.d.ts","sourceRoot":"","sources":["../../src/logic/canonical-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAE7C,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM;CAKjB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic JSON, the form every order digest is taken over: object keys sorted by UTF-16 code
|
|
3
|
+
* unit (JavaScript's default string order, .NET's `StringComparer.Ordinal`), no whitespace, and
|
|
4
|
+
* numbers restricted to safe integers.
|
|
5
|
+
*
|
|
6
|
+
* The restriction is the point: a client written in another language has to reproduce this text
|
|
7
|
+
* byte for byte to verify `ordersHash`, and floating-point shortest round-trip text is not portable
|
|
8
|
+
* (`1e+21` here, `1E+21` in .NET). Integers, booleans, null and JSON-escaped strings are. Anything
|
|
9
|
+
* outside that is a programming error, not a runtime condition, so it throws rather than hashing
|
|
10
|
+
* something a peer cannot reproduce.
|
|
11
|
+
*/
|
|
12
|
+
export function canonicalJson(value) {
|
|
13
|
+
return JSON.stringify(canonicalize(value));
|
|
14
|
+
}
|
|
15
|
+
export class NonCanonicalValueError extends Error {
|
|
16
|
+
path;
|
|
17
|
+
constructor(path, detail) {
|
|
18
|
+
super(`cannot canonicalize ${path || 'value'}: ${detail}`);
|
|
19
|
+
this.path = path;
|
|
20
|
+
this.name = 'NonCanonicalValueError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function canonicalize(value, path = '') {
|
|
24
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string')
|
|
25
|
+
return value;
|
|
26
|
+
if (typeof value === 'number') {
|
|
27
|
+
if (!Number.isSafeInteger(value) || Object.is(value, -0)) {
|
|
28
|
+
throw new NonCanonicalValueError(path, `${value} is not a safe non-negative-zero integer`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return value.map((item, index) => canonicalize(item, `${path}[${index}]`));
|
|
34
|
+
if (typeof value === 'object') {
|
|
35
|
+
const source = value;
|
|
36
|
+
const sorted = {};
|
|
37
|
+
for (const key of Object.keys(source).sort()) {
|
|
38
|
+
sorted[key] = canonicalize(source[key], path ? `${path}.${key}` : key);
|
|
39
|
+
}
|
|
40
|
+
return sorted;
|
|
41
|
+
}
|
|
42
|
+
throw new NonCanonicalValueError(path, `unsupported type ${typeof value}`);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=canonical-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical-json.js","sourceRoot":"","sources":["../../src/logic/canonical-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAA;AAC5C,CAAC;AAED,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAEpC;IADX,YACW,IAAY,EACrB,MAAc;QAEd,KAAK,CAAC,uBAAuB,IAAI,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC,CAAA;QAHjD,SAAI,GAAJ,IAAI,CAAQ;QAIrB,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAA;IACtC,CAAC;CACF;AAED,SAAS,YAAY,CAAC,KAAc,EAAE,IAAI,GAAG,EAAE;IAC7C,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3F,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzD,MAAM,IAAI,sBAAsB,CAAC,IAAI,EAAE,GAAG,KAAK,0CAA0C,CAAC,CAAA;QAC5F,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACtB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC,CAAC,CAAA;IAC5E,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,KAAgC,CAAA;QAC/C,MAAM,MAAM,GAA4B,EAAE,CAAA;QAC1C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC7C,MAAM,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;QACxE,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC;IACD,MAAM,IAAI,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,OAAO,KAAK,EAAE,CAAC,CAAA;AAC5E,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare function sha256Hex(input: string | Uint8Array): Promise<string>;
|
|
2
|
+
export declare function toHex(bytes: Uint8Array): string;
|
|
3
|
+
export declare function randomBytes(length: number): Uint8Array;
|
|
4
|
+
/** A bearer token: 256 random bits, prefixed so a leaked one is recognisable in logs. */
|
|
5
|
+
export declare const TOKEN_PREFIX = "cop_";
|
|
6
|
+
export declare function generateToken(): string;
|
|
7
|
+
export declare function hashToken(token: string): Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* A join code of uniformly distributed glyphs. The alphabet's size does not divide 256, so bytes
|
|
10
|
+
* above the largest whole multiple are rejected and redrawn rather than folded with `%`, which
|
|
11
|
+
* would make the first glyphs ~3% likelier and cost entropy on a code that guards a lobby.
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateJoinCode(length: number): string;
|
|
14
|
+
/**
|
|
15
|
+
* A seed for the deterministic game core, in the range the core can actually hold.
|
|
16
|
+
*
|
|
17
|
+
* `MatchSetup.InitialSeed` is a C# `int`, so the wire value has to be a SIGNED 32-bit integer:
|
|
18
|
+
* drawing 32 unsigned bits would put half of all seeds above `int.MaxValue`, where the client's
|
|
19
|
+
* deserializer refuses them and the match never starts. `Int32Array` reinterprets the same four
|
|
20
|
+
* random bytes as the signed value the core will read, which keeps the full 32 bits of entropy.
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateSeed(): number;
|
|
23
|
+
export declare function hashOrderDocument(orders: unknown): Promise<string>;
|
|
24
|
+
/** The digest every client can recompute from the sealed set it fetched, to verify integrity. */
|
|
25
|
+
export declare function hashOrderSet(entries: ReadonlyArray<{
|
|
26
|
+
slot: number;
|
|
27
|
+
ordersHash: string;
|
|
28
|
+
}>): Promise<string>;
|
|
29
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/logic/crypto.ts"],"names":[],"mappings":"AAIA,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAI3E;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAI/C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAItD;AAQD,yFAAyF;AACzF,eAAO,MAAM,YAAY,SAAS,CAAA;AAElC,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAExD;AAKD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWvD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAElE;AAED,iGAAiG;AACjG,wBAAgB,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GAC3D,OAAO,CAAC,MAAM,CAAC,CAKjB"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { canonicalJson } from './canonical-json';
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
export async function sha256Hex(input) {
|
|
4
|
+
const bytes = typeof input === 'string' ? encoder.encode(input) : input;
|
|
5
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
6
|
+
return toHex(new Uint8Array(digest));
|
|
7
|
+
}
|
|
8
|
+
export function toHex(bytes) {
|
|
9
|
+
let out = '';
|
|
10
|
+
for (const byte of bytes)
|
|
11
|
+
out += byte.toString(16).padStart(2, '0');
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
export function randomBytes(length) {
|
|
15
|
+
const bytes = new Uint8Array(length);
|
|
16
|
+
crypto.getRandomValues(bytes);
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
function toBase64Url(bytes) {
|
|
20
|
+
let binary = '';
|
|
21
|
+
for (const byte of bytes)
|
|
22
|
+
binary += String.fromCharCode(byte);
|
|
23
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
|
|
24
|
+
}
|
|
25
|
+
/** A bearer token: 256 random bits, prefixed so a leaked one is recognisable in logs. */
|
|
26
|
+
export const TOKEN_PREFIX = 'cop_';
|
|
27
|
+
export function generateToken() {
|
|
28
|
+
return `${TOKEN_PREFIX}${toBase64Url(randomBytes(32))}`;
|
|
29
|
+
}
|
|
30
|
+
export function hashToken(token) {
|
|
31
|
+
return sha256Hex(token);
|
|
32
|
+
}
|
|
33
|
+
/** Crockford-ish alphabet without the glyphs players misread over voice chat (0/O, 1/I/L). */
|
|
34
|
+
const JOIN_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
|
35
|
+
/**
|
|
36
|
+
* A join code of uniformly distributed glyphs. The alphabet's size does not divide 256, so bytes
|
|
37
|
+
* above the largest whole multiple are rejected and redrawn rather than folded with `%`, which
|
|
38
|
+
* would make the first glyphs ~3% likelier and cost entropy on a code that guards a lobby.
|
|
39
|
+
*/
|
|
40
|
+
export function generateJoinCode(length) {
|
|
41
|
+
const limit = Math.floor(256 / JOIN_CODE_ALPHABET.length) * JOIN_CODE_ALPHABET.length;
|
|
42
|
+
let code = '';
|
|
43
|
+
while (code.length < length) {
|
|
44
|
+
for (const byte of randomBytes(length)) {
|
|
45
|
+
if (byte >= limit)
|
|
46
|
+
continue;
|
|
47
|
+
code += JOIN_CODE_ALPHABET[byte % JOIN_CODE_ALPHABET.length];
|
|
48
|
+
if (code.length === length)
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return code;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A seed for the deterministic game core, in the range the core can actually hold.
|
|
56
|
+
*
|
|
57
|
+
* `MatchSetup.InitialSeed` is a C# `int`, so the wire value has to be a SIGNED 32-bit integer:
|
|
58
|
+
* drawing 32 unsigned bits would put half of all seeds above `int.MaxValue`, where the client's
|
|
59
|
+
* deserializer refuses them and the match never starts. `Int32Array` reinterprets the same four
|
|
60
|
+
* random bytes as the signed value the core will read, which keeps the full 32 bits of entropy.
|
|
61
|
+
*/
|
|
62
|
+
export function generateSeed() {
|
|
63
|
+
const [value] = new Int32Array(randomBytes(4).buffer);
|
|
64
|
+
return value ?? 0;
|
|
65
|
+
}
|
|
66
|
+
export function hashOrderDocument(orders) {
|
|
67
|
+
return sha256Hex(canonicalJson(orders));
|
|
68
|
+
}
|
|
69
|
+
/** The digest every client can recompute from the sealed set it fetched, to verify integrity. */
|
|
70
|
+
export function hashOrderSet(entries) {
|
|
71
|
+
const lines = [...entries]
|
|
72
|
+
.sort((a, b) => a.slot - b.slot)
|
|
73
|
+
.map((entry) => `${entry.slot}:${entry.ordersHash}`);
|
|
74
|
+
return sha256Hex(lines.join('\n'));
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/logic/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/logic/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/logic/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** PBKDF2-SHA256 over WebCrypto, so the same code runs on Node and workerd. */
|
|
2
|
+
export declare function hashPassword(password: string): Promise<string>;
|
|
3
|
+
export declare function verifyPassword(password: string, stored: string): Promise<boolean>;
|
|
4
|
+
//# sourceMappingURL=password.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../src/logic/password.ts"],"names":[],"mappings":"AAKA,+EAA+E;AAC/E,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpE;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAKvF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { randomBytes, toHex } from './crypto';
|
|
2
|
+
const ITERATIONS = 120_000;
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
/** PBKDF2-SHA256 over WebCrypto, so the same code runs on Node and workerd. */
|
|
5
|
+
export async function hashPassword(password) {
|
|
6
|
+
const salt = randomBytes(16);
|
|
7
|
+
const derived = await derive(password, salt);
|
|
8
|
+
return `pbkdf2$${ITERATIONS}$${toHex(salt)}$${toHex(derived)}`;
|
|
9
|
+
}
|
|
10
|
+
export async function verifyPassword(password, stored) {
|
|
11
|
+
const [scheme, iterationsText, saltHex, hashHex] = stored.split('$');
|
|
12
|
+
if (scheme !== 'pbkdf2' || !iterationsText || !saltHex || !hashHex)
|
|
13
|
+
return false;
|
|
14
|
+
const derived = await derive(password, fromHex(saltHex), Number(iterationsText));
|
|
15
|
+
return constantTimeEqual(toHex(derived), hashHex);
|
|
16
|
+
}
|
|
17
|
+
async function derive(password, salt, iterations = ITERATIONS) {
|
|
18
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, [
|
|
19
|
+
'deriveBits',
|
|
20
|
+
]);
|
|
21
|
+
const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt: salt, iterations }, key, 256);
|
|
22
|
+
return new Uint8Array(bits);
|
|
23
|
+
}
|
|
24
|
+
function fromHex(hex) {
|
|
25
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
26
|
+
for (let i = 0; i < bytes.length; i += 1)
|
|
27
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
28
|
+
return bytes;
|
|
29
|
+
}
|
|
30
|
+
function constantTimeEqual(a, b) {
|
|
31
|
+
if (a.length !== b.length)
|
|
32
|
+
return false;
|
|
33
|
+
let diff = 0;
|
|
34
|
+
for (let i = 0; i < a.length; i += 1)
|
|
35
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
36
|
+
return diff === 0;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/logic/password.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAE7C,MAAM,UAAU,GAAG,OAAO,CAAA;AAC1B,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAEjC,+EAA+E;AAC/E,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,MAAM,IAAI,GAAG,WAAW,CAAC,EAAE,CAAC,CAAA;IAC5B,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC5C,OAAO,UAAU,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAA;AAChE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAAgB,EAAE,MAAc;IACnE,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACpE,IAAI,MAAM,KAAK,QAAQ,IAAI,CAAC,cAAc,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAA;IAChF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC,CAAC,CAAA;IAChF,OAAO,iBAAiB,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAA;AACnD,CAAC;AAED,KAAK,UAAU,MAAM,CACnB,QAAgB,EAChB,IAAgB,EAChB,UAAU,GAAG,UAAU;IAEvB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE;QAC1F,YAAY;KACb,CAAC,CAAA;IACF,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,UAAU,CACzC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAoB,EAAE,UAAU,EAAE,EAC3E,GAAG,EACH,GAAG,CACJ,CAAA;IACD,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAA;AAC7B,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC;QACtC,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC7D,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,iBAAiB,CAAC,CAAS,EAAE,CAAS;IAC7C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACvC,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC;QAAE,IAAI,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAC/E,OAAO,IAAI,KAAK,CAAC,CAAA;AACnB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Player, TurnOrders, TurnReport } from '../domain/entities';
|
|
2
|
+
/** Every human seat still being waited on has marked ready. An empty roster is never "ready". */
|
|
3
|
+
export declare function allActiveReady(players: readonly Player[], orders: readonly TurnOrders[]): boolean;
|
|
4
|
+
export type Consensus = {
|
|
5
|
+
kind: 'pending';
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'confirmed';
|
|
8
|
+
stateHash: string;
|
|
9
|
+
finished: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'desynced';
|
|
12
|
+
reports: Array<{
|
|
13
|
+
playerId: string;
|
|
14
|
+
stateHash: string;
|
|
15
|
+
}>;
|
|
16
|
+
/** The hashes tied for the most reports; what a recovery snapshot may claim. */
|
|
17
|
+
candidateStateHashes: string[];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The state hashes that the most active players reported, tied if more than one.
|
|
21
|
+
*
|
|
22
|
+
* This is the corroboration a recovery snapshot is held to. Letting the host name any hash at all
|
|
23
|
+
* would make the host's client authoritative over a disagreement it is itself a party to: upload a
|
|
24
|
+
* doctored snapshot after a deliberate desync and every other client is told to adopt it. A hash
|
|
25
|
+
* that more players than any other already computed independently cannot be minted by one of them.
|
|
26
|
+
*
|
|
27
|
+
* A genuine tie (most of all, the 1-1 split of a two-player match) leaves nothing to count, and
|
|
28
|
+
* there is no third party to ask, so the host breaks it. Lockstep of three or more is where this
|
|
29
|
+
* bites, and that is the case worth defending: consistency is what matters, so converging on the
|
|
30
|
+
* majority's state is right even when the host's own client happens to be the correct one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function authoritativeCandidates(players: readonly Player[], reports: readonly TurnReport[]): string[];
|
|
33
|
+
/**
|
|
34
|
+
* Compare the post-turn state hashes the active players reported.
|
|
35
|
+
*
|
|
36
|
+
* Without an authoritative hash: once everyone has reported, unanimous agreement confirms the
|
|
37
|
+
* turn and any disagreement flags a desync. With one (the host uploaded a snapshot for this
|
|
38
|
+
* turn), a report is counted only when it matches it, so stragglers reloading the snapshot can
|
|
39
|
+
* converge without tripping a second desync.
|
|
40
|
+
*/
|
|
41
|
+
export declare function evaluateConsensus(players: readonly Player[], reports: readonly TurnReport[], authoritativeHash: string | null): Consensus;
|
|
42
|
+
/**
|
|
43
|
+
* Host takes slot 0; everyone else follows the match's monotonic join sequence. The sequence, not
|
|
44
|
+
* the join timestamp, is the key: two players seated in the same millisecond would otherwise be
|
|
45
|
+
* ordered by their random ids, and the slot order decides resolution order on every client.
|
|
46
|
+
*/
|
|
47
|
+
export declare function assignSlots(players: readonly Player[], hostPlayerId: string): Array<{
|
|
48
|
+
playerId: string;
|
|
49
|
+
slot: number;
|
|
50
|
+
}>;
|
|
51
|
+
export declare function turnDeadline(openedAt: Date, turnTimerSeconds: number): Date | null;
|
|
52
|
+
//# sourceMappingURL=turn-logic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-logic.d.ts","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAExE,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAOjG;AAED,MAAM,MAAM,SAAS,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAC3D;IACE,IAAI,EAAE,UAAU,CAAA;IAChB,OAAO,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACvD,gFAAgF;IAChF,oBAAoB,EAAE,MAAM,EAAE,CAAA;CAC/B,CAAA;AAEL;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,SAAS,UAAU,EAAE,GAC7B,MAAM,EAAE,CAaV;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,SAAS,UAAU,EAAE,EAC9B,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAC/B,SAAS,CAgCX;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,YAAY,EAAE,MAAM,GACnB,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAU3C;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAElF"}
|