@couch-kit/core 0.10.0 → 0.10.2
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/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/dist/index.cjs +42 -29
- package/dist/index.js +42 -29
- package/lib/constants.d.ts +5 -1
- package/lib/constants.d.ts.map +1 -1
- package/lib/middleware.d.ts +2 -1
- package/lib/middleware.d.ts.map +1 -1
- package/lib/replay.d.ts +6 -0
- package/lib/replay.d.ts.map +1 -1
- package/lib/sha256.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +12 -2
- package/src/middleware.ts +16 -2
- package/src/replay.ts +9 -1
- package/src/sha256.ts +2 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @couch-kit/core
|
|
2
2
|
|
|
3
|
+
## 0.10.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#225](https://github.com/faluciano/react-native-couch-kit/pull/225) [`072bc40`](https://github.com/faluciano/react-native-couch-kit/commit/072bc40f923dd9169f2bcb344deb733223402323) Thanks [@faluciano](https://github.com/faluciano)! - **Security:** `actionValidator` drops an action whose validator throws. Before, the middleware error boundary passed it on to the reducer, so a malformed payload (exactly what makes a validator like `(a) => a.payload.points > 0` throw) skipped validation. `isValidSecret` now also rejects secrets longer than 128 hex characters (`MAX_SECRET_HEX_LENGTH`), so a client cannot make the host hash and store an arbitrarily long string.
|
|
8
|
+
|
|
9
|
+
## 0.10.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#208](https://github.com/faluciano/react-native-couch-kit/pull/208) [`99598d6`](https://github.com/faluciano/react-native-couch-kit/commit/99598d6796b0bd3042aa9f3fbd3bf44ee891b890) Thanks [@faluciano](https://github.com/faluciano)! - `replayActions` wraps the reducer it is given with `createGameReducer`. A recording that contains internal actions (`__PLAYER_JOINED__` and the others a host recorder captures) now replays the way it ran. Before, those actions were silently ignored, because a game's own reducer does not handle them. Passing a reducer that is already wrapped is harmless.
|
|
14
|
+
|
|
3
15
|
## 0.10.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ actionLogger({ collapsed: false }); // expanded groups
|
|
|
84
84
|
|
|
85
85
|
#### `actionValidator(schema)`
|
|
86
86
|
|
|
87
|
-
Validates actions against a schema before they reach the reducer. If a validator returns `false
|
|
87
|
+
Validates actions against a schema before they reach the reducer. If a validator returns `false` or throws, the action is dropped and a warning is logged.
|
|
88
88
|
|
|
89
89
|
```typescript
|
|
90
90
|
import { actionValidator, type ActionSchema } from "@couch-kit/core";
|
|
@@ -188,7 +188,7 @@ try {
|
|
|
188
188
|
|
|
189
189
|
#### `isValidSecret(secret: string): boolean`
|
|
190
190
|
|
|
191
|
-
Validates that a string is a valid UUID format (32
|
|
191
|
+
Validates that a string is a valid UUID format (32 to 128 hex characters, with or without dashes).
|
|
192
192
|
|
|
193
193
|
#### `derivePlayerId(secret: string): Promise<string>`
|
|
194
194
|
|
package/dist/index.cjs
CHANGED
|
@@ -39,32 +39,33 @@ var __export = (target, all) => {
|
|
|
39
39
|
// src/index.ts
|
|
40
40
|
var exports_src = {};
|
|
41
41
|
__export(exports_src, {
|
|
42
|
-
|
|
43
|
-
replayActions: () => replayActions,
|
|
44
|
-
isValidSecret: () => isValidSecret,
|
|
45
|
-
generateId: () => generateId,
|
|
46
|
-
derivePlayerIdLegacy: () => derivePlayerIdLegacy,
|
|
47
|
-
derivePlayerId: () => derivePlayerId,
|
|
48
|
-
createGameReducer: () => createGameReducer,
|
|
49
|
-
applyMiddleware: () => applyMiddleware,
|
|
50
|
-
actionValidator: () => actionValidator,
|
|
51
|
-
actionLogger: () => actionLogger,
|
|
52
|
-
SYNC_BACKOFF_FACTOR: () => SYNC_BACKOFF_FACTOR,
|
|
53
|
-
MessageTypes: () => MessageTypes,
|
|
54
|
-
MAX_SYNC_INTERVAL: () => MAX_SYNC_INTERVAL,
|
|
55
|
-
MAX_PENDING_PINGS: () => MAX_PENDING_PINGS,
|
|
56
|
-
MAX_FRAME_SIZE: () => MAX_FRAME_SIZE,
|
|
57
|
-
KEEPALIVE_TIMEOUT: () => KEEPALIVE_TIMEOUT,
|
|
58
|
-
KEEPALIVE_INTERVAL: () => KEEPALIVE_INTERVAL,
|
|
59
|
-
InternalActionTypes: () => InternalActionTypes,
|
|
60
|
-
DEFAULT_WS_PORT_OFFSET: () => DEFAULT_WS_PORT_OFFSET,
|
|
61
|
-
DEFAULT_WS_PATH: () => DEFAULT_WS_PATH,
|
|
62
|
-
DEFAULT_SYNC_INTERVAL: () => DEFAULT_SYNC_INTERVAL,
|
|
63
|
-
DEFAULT_MAX_RETRIES: () => DEFAULT_MAX_RETRIES,
|
|
64
|
-
DEFAULT_MAX_DELAY: () => DEFAULT_MAX_DELAY,
|
|
65
|
-
DEFAULT_HTTP_PORT: () => DEFAULT_HTTP_PORT,
|
|
42
|
+
DEFAULT_BASE_DELAY: () => DEFAULT_BASE_DELAY,
|
|
66
43
|
DEFAULT_DISCONNECT_TIMEOUT: () => DEFAULT_DISCONNECT_TIMEOUT,
|
|
67
|
-
|
|
44
|
+
DEFAULT_HTTP_PORT: () => DEFAULT_HTTP_PORT,
|
|
45
|
+
DEFAULT_MAX_DELAY: () => DEFAULT_MAX_DELAY,
|
|
46
|
+
DEFAULT_MAX_RETRIES: () => DEFAULT_MAX_RETRIES,
|
|
47
|
+
DEFAULT_SYNC_INTERVAL: () => DEFAULT_SYNC_INTERVAL,
|
|
48
|
+
DEFAULT_WS_PATH: () => DEFAULT_WS_PATH,
|
|
49
|
+
DEFAULT_WS_PORT_OFFSET: () => DEFAULT_WS_PORT_OFFSET,
|
|
50
|
+
InternalActionTypes: () => InternalActionTypes,
|
|
51
|
+
KEEPALIVE_INTERVAL: () => KEEPALIVE_INTERVAL,
|
|
52
|
+
KEEPALIVE_TIMEOUT: () => KEEPALIVE_TIMEOUT,
|
|
53
|
+
MAX_FRAME_SIZE: () => MAX_FRAME_SIZE,
|
|
54
|
+
MAX_PENDING_PINGS: () => MAX_PENDING_PINGS,
|
|
55
|
+
MAX_SECRET_HEX_LENGTH: () => MAX_SECRET_HEX_LENGTH,
|
|
56
|
+
MAX_SYNC_INTERVAL: () => MAX_SYNC_INTERVAL,
|
|
57
|
+
MessageTypes: () => MessageTypes,
|
|
58
|
+
SYNC_BACKOFF_FACTOR: () => SYNC_BACKOFF_FACTOR,
|
|
59
|
+
actionLogger: () => actionLogger,
|
|
60
|
+
actionValidator: () => actionValidator,
|
|
61
|
+
applyMiddleware: () => applyMiddleware,
|
|
62
|
+
createGameReducer: () => createGameReducer,
|
|
63
|
+
derivePlayerId: () => derivePlayerId,
|
|
64
|
+
derivePlayerIdLegacy: () => derivePlayerIdLegacy,
|
|
65
|
+
generateId: () => generateId,
|
|
66
|
+
isValidSecret: () => isValidSecret,
|
|
67
|
+
replayActions: () => replayActions,
|
|
68
|
+
toErrorMessage: () => toErrorMessage
|
|
68
69
|
});
|
|
69
70
|
module.exports = __toCommonJS(exports_src);
|
|
70
71
|
|
|
@@ -142,7 +143,15 @@ function actionValidator(schema) {
|
|
|
142
143
|
if (getInternalTypes().has(action.type))
|
|
143
144
|
return next(action);
|
|
144
145
|
const validate = schema[action.type];
|
|
145
|
-
if (
|
|
146
|
+
if (!validate)
|
|
147
|
+
return next(action);
|
|
148
|
+
let valid;
|
|
149
|
+
try {
|
|
150
|
+
valid = validate(action);
|
|
151
|
+
} catch {
|
|
152
|
+
valid = false;
|
|
153
|
+
}
|
|
154
|
+
if (!valid) {
|
|
146
155
|
console.warn(`[couch-kit] Action "${action.type}" failed validation, skipping`);
|
|
147
156
|
return _api.getState();
|
|
148
157
|
}
|
|
@@ -393,15 +402,18 @@ function generateId() {
|
|
|
393
402
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.getRandomValues === "function") {
|
|
394
403
|
const bytes = new Uint8Array(16);
|
|
395
404
|
globalThis.crypto.getRandomValues(bytes);
|
|
396
|
-
return Array.from(bytes).map((
|
|
405
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
397
406
|
}
|
|
398
407
|
const a = Math.random().toString(36).substring(2, 15);
|
|
399
408
|
const b = Math.random().toString(36).substring(2, 10);
|
|
400
409
|
return a + b;
|
|
401
410
|
}
|
|
411
|
+
var MAX_SECRET_HEX_LENGTH = 128;
|
|
402
412
|
function isValidSecret(secret) {
|
|
413
|
+
if (secret.length > MAX_SECRET_HEX_LENGTH * 2)
|
|
414
|
+
return false;
|
|
403
415
|
const hex = secret.replace(/-/g, "");
|
|
404
|
-
return hex.length >= 32 && /^[0-9a-f]+$/i.test(hex);
|
|
416
|
+
return hex.length >= 32 && hex.length <= MAX_SECRET_HEX_LENGTH && /^[0-9a-f]+$/i.test(hex);
|
|
405
417
|
}
|
|
406
418
|
async function derivePlayerId(secret) {
|
|
407
419
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle?.digest === "function") {
|
|
@@ -422,10 +434,11 @@ function toErrorMessage(error) {
|
|
|
422
434
|
// src/replay.ts
|
|
423
435
|
function replayActions(recording, reducer) {
|
|
424
436
|
const snapshots = [];
|
|
437
|
+
const gameReducer = createGameReducer(reducer);
|
|
425
438
|
let currentState = recording.initialState;
|
|
426
439
|
for (let i = 0;i < recording.actions.length; i++) {
|
|
427
440
|
const { action, timestamp } = recording.actions[i];
|
|
428
|
-
currentState =
|
|
441
|
+
currentState = gameReducer(currentState, action);
|
|
429
442
|
snapshots.push({
|
|
430
443
|
state: currentState,
|
|
431
444
|
action,
|
package/dist/index.js
CHANGED
|
@@ -72,7 +72,15 @@ function actionValidator(schema) {
|
|
|
72
72
|
if (getInternalTypes().has(action.type))
|
|
73
73
|
return next(action);
|
|
74
74
|
const validate = schema[action.type];
|
|
75
|
-
if (
|
|
75
|
+
if (!validate)
|
|
76
|
+
return next(action);
|
|
77
|
+
let valid;
|
|
78
|
+
try {
|
|
79
|
+
valid = validate(action);
|
|
80
|
+
} catch {
|
|
81
|
+
valid = false;
|
|
82
|
+
}
|
|
83
|
+
if (!valid) {
|
|
76
84
|
console.warn(`[couch-kit] Action "${action.type}" failed validation, skipping`);
|
|
77
85
|
return _api.getState();
|
|
78
86
|
}
|
|
@@ -323,15 +331,18 @@ function generateId() {
|
|
|
323
331
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.getRandomValues === "function") {
|
|
324
332
|
const bytes = new Uint8Array(16);
|
|
325
333
|
globalThis.crypto.getRandomValues(bytes);
|
|
326
|
-
return Array.from(bytes).map((
|
|
334
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
327
335
|
}
|
|
328
336
|
const a = Math.random().toString(36).substring(2, 15);
|
|
329
337
|
const b = Math.random().toString(36).substring(2, 10);
|
|
330
338
|
return a + b;
|
|
331
339
|
}
|
|
340
|
+
var MAX_SECRET_HEX_LENGTH = 128;
|
|
332
341
|
function isValidSecret(secret) {
|
|
342
|
+
if (secret.length > MAX_SECRET_HEX_LENGTH * 2)
|
|
343
|
+
return false;
|
|
333
344
|
const hex = secret.replace(/-/g, "");
|
|
334
|
-
return hex.length >= 32 && /^[0-9a-f]+$/i.test(hex);
|
|
345
|
+
return hex.length >= 32 && hex.length <= MAX_SECRET_HEX_LENGTH && /^[0-9a-f]+$/i.test(hex);
|
|
335
346
|
}
|
|
336
347
|
async function derivePlayerId(secret) {
|
|
337
348
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle?.digest === "function") {
|
|
@@ -352,10 +363,11 @@ function toErrorMessage(error) {
|
|
|
352
363
|
// src/replay.ts
|
|
353
364
|
function replayActions(recording, reducer) {
|
|
354
365
|
const snapshots = [];
|
|
366
|
+
const gameReducer = createGameReducer(reducer);
|
|
355
367
|
let currentState = recording.initialState;
|
|
356
368
|
for (let i = 0;i < recording.actions.length; i++) {
|
|
357
369
|
const { action, timestamp } = recording.actions[i];
|
|
358
|
-
currentState =
|
|
370
|
+
currentState = gameReducer(currentState, action);
|
|
359
371
|
snapshots.push({
|
|
360
372
|
state: currentState,
|
|
361
373
|
action,
|
|
@@ -372,30 +384,31 @@ function replayActions(recording, reducer) {
|
|
|
372
384
|
};
|
|
373
385
|
}
|
|
374
386
|
export {
|
|
375
|
-
|
|
376
|
-
replayActions,
|
|
377
|
-
isValidSecret,
|
|
378
|
-
generateId,
|
|
379
|
-
derivePlayerIdLegacy,
|
|
380
|
-
derivePlayerId,
|
|
381
|
-
createGameReducer,
|
|
382
|
-
applyMiddleware,
|
|
383
|
-
actionValidator,
|
|
384
|
-
actionLogger,
|
|
385
|
-
SYNC_BACKOFF_FACTOR,
|
|
386
|
-
MessageTypes,
|
|
387
|
-
MAX_SYNC_INTERVAL,
|
|
388
|
-
MAX_PENDING_PINGS,
|
|
389
|
-
MAX_FRAME_SIZE,
|
|
390
|
-
KEEPALIVE_TIMEOUT,
|
|
391
|
-
KEEPALIVE_INTERVAL,
|
|
392
|
-
InternalActionTypes,
|
|
393
|
-
DEFAULT_WS_PORT_OFFSET,
|
|
394
|
-
DEFAULT_WS_PATH,
|
|
395
|
-
DEFAULT_SYNC_INTERVAL,
|
|
396
|
-
DEFAULT_MAX_RETRIES,
|
|
397
|
-
DEFAULT_MAX_DELAY,
|
|
398
|
-
DEFAULT_HTTP_PORT,
|
|
387
|
+
DEFAULT_BASE_DELAY,
|
|
399
388
|
DEFAULT_DISCONNECT_TIMEOUT,
|
|
400
|
-
|
|
389
|
+
DEFAULT_HTTP_PORT,
|
|
390
|
+
DEFAULT_MAX_DELAY,
|
|
391
|
+
DEFAULT_MAX_RETRIES,
|
|
392
|
+
DEFAULT_SYNC_INTERVAL,
|
|
393
|
+
DEFAULT_WS_PATH,
|
|
394
|
+
DEFAULT_WS_PORT_OFFSET,
|
|
395
|
+
InternalActionTypes,
|
|
396
|
+
KEEPALIVE_INTERVAL,
|
|
397
|
+
KEEPALIVE_TIMEOUT,
|
|
398
|
+
MAX_FRAME_SIZE,
|
|
399
|
+
MAX_PENDING_PINGS,
|
|
400
|
+
MAX_SECRET_HEX_LENGTH,
|
|
401
|
+
MAX_SYNC_INTERVAL,
|
|
402
|
+
MessageTypes,
|
|
403
|
+
SYNC_BACKOFF_FACTOR,
|
|
404
|
+
actionLogger,
|
|
405
|
+
actionValidator,
|
|
406
|
+
applyMiddleware,
|
|
407
|
+
createGameReducer,
|
|
408
|
+
derivePlayerId,
|
|
409
|
+
derivePlayerIdLegacy,
|
|
410
|
+
generateId,
|
|
411
|
+
isValidSecret,
|
|
412
|
+
replayActions,
|
|
413
|
+
toErrorMessage
|
|
401
414
|
};
|
package/lib/constants.d.ts
CHANGED
|
@@ -42,8 +42,12 @@ export declare const KEEPALIVE_TIMEOUT = 10000;
|
|
|
42
42
|
* falling back to `crypto.getRandomValues()` for older environments.
|
|
43
43
|
*/
|
|
44
44
|
export declare function generateId(): string;
|
|
45
|
+
/** Longest secret, in hex characters, the host accepts. A UUID is 32. */
|
|
46
|
+
export declare const MAX_SECRET_HEX_LENGTH = 128;
|
|
45
47
|
/**
|
|
46
|
-
* Validates that a string looks like a UUID (with or without dashes, 32
|
|
48
|
+
* Validates that a string looks like a UUID (with or without dashes, 32 to
|
|
49
|
+
* {@link MAX_SECRET_HEX_LENGTH} hex chars). The upper bound keeps a client from
|
|
50
|
+
* making the host hash and store an arbitrarily long string.
|
|
47
51
|
*/
|
|
48
52
|
export declare function isValidSecret(secret: string): boolean;
|
|
49
53
|
/**
|
package/lib/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,wDAAwD;AAIxD,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,0DAA0D;AAC1D,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,8FAA8F;AAC9F,eAAO,MAAM,cAAc,QAAc,CAAC;AAE1C,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,oEAAoE;AACpE,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,0DAA0D;AAC1D,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,+EAA+E;AAC/E,eAAO,MAAM,0BAA0B,QAAgB,CAAC;AAExD,8FAA8F;AAC9F,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,8FAA8F;AAC9F,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAwBnC;AAED
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,wDAAwD;AAIxD,oDAAoD;AACpD,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,0DAA0D;AAC1D,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,8FAA8F;AAC9F,eAAO,MAAM,cAAc,QAAc,CAAC;AAE1C,4DAA4D;AAC5D,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,oEAAoE;AACpE,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC,+DAA+D;AAC/D,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,0DAA0D;AAC1D,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,+EAA+E;AAC/E,eAAO,MAAM,0BAA0B,QAAgB,CAAC;AAExD,8FAA8F;AAC9F,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AAExC,8FAA8F;AAC9F,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAwBnC;AAED,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAQrD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBpE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGrD"}
|
package/lib/middleware.d.ts
CHANGED
|
@@ -53,7 +53,8 @@ export type ActionSchema<A extends IAction> = {
|
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
55
|
* Middleware that validates actions against a schema before they reach the
|
|
56
|
-
* reducer. Invalid actions are
|
|
56
|
+
* reducer. Invalid actions are dropped with a console warning. A validator
|
|
57
|
+
* that throws counts as a failed validation.
|
|
57
58
|
*
|
|
58
59
|
* Internal actions are never validated — they always pass through.
|
|
59
60
|
*
|
package/lib/middleware.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,OAAO,EACP,cAAc,EACd,WAAW,EACZ,MAAM,YAAY,CAAC;AAOpB,oDAAoD;AACpD,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,UAAU;IACjD,QAAQ,EAAE,MAAM,CAAC,CAAC;CACnB;AAED,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,IAAI,CACxE,MAAM,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,KAC1B,CAAC,CAAC;AAEP;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CACpB,CAAC,SAAS,UAAU,GAAG,UAAU,EACjC,CAAC,SAAS,OAAO,GAAG,OAAO,IACzB,CACF,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC,KAClB,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAM9E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACrE,GAAG,WAAW,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GACjC,CACD,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,KAC3C,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAwDzC;AAMD,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EAClE,OAAO,CAAC,EAAE,mBAAmB,GAC5B,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAmBlB;AAeD;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,OAAO,IAAI;KAC3C,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,KAAK,OAAO;CACxD,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,OAAO,EACP,cAAc,EACd,WAAW,EACZ,MAAM,YAAY,CAAC;AAOpB,oDAAoD;AACpD,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,UAAU;IACjD,QAAQ,EAAE,MAAM,CAAC,CAAC;CACnB;AAED,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,IAAI,CACxE,MAAM,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,KAC1B,CAAC,CAAC;AAEP;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,CACpB,CAAC,SAAS,UAAU,GAAG,UAAU,EACjC,CAAC,SAAS,OAAO,GAAG,OAAO,IACzB,CACF,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC,KAClB,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAM9E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACrE,GAAG,WAAW,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GACjC,CACD,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,KAC3C,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAwDzC;AAMD,yDAAyD;AACzD,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EAClE,OAAO,CAAC,EAAE,mBAAmB,GAC5B,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAmBlB;AAeD;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,OAAO,IAAI;KAC3C,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,KAAK,OAAO;CACxD,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACrE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GACtB,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CA6BlB"}
|
package/lib/replay.d.ts
CHANGED
|
@@ -37,6 +37,12 @@ export interface ReplayResult<S extends IGameState = IGameState> {
|
|
|
37
37
|
/**
|
|
38
38
|
* Replays a recording against a reducer, producing the final state
|
|
39
39
|
* and intermediate snapshots for each action applied.
|
|
40
|
+
*
|
|
41
|
+
* Pass the game's own reducer, as given to the host. It is wrapped with
|
|
42
|
+
* {@link createGameReducer} here, so recordings that include internal actions
|
|
43
|
+
* (`__PLAYER_JOINED__` and friends, as a host recorder captures) replay the
|
|
44
|
+
* same way they ran. Wrapping an already-wrapped reducer is harmless: the outer
|
|
45
|
+
* layer handles internal actions and the inner never sees them.
|
|
40
46
|
*/
|
|
41
47
|
export declare function replayActions<S extends IGameState = IGameState, A extends IAction = IAction>(recording: Recording<S, A>, reducer: GameReducer<S, A>): ReplayResult<S>;
|
|
42
48
|
//# sourceMappingURL=replay.d.ts.map
|
package/lib/replay.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO;IACzD,MAAM,EAAE,CAAC,CAAC;IACV,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU;IAC9D,KAAK,EAAE,CAAC,CAAC;IACT,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,SAAS,CACxB,CAAC,SAAS,UAAU,GAAG,UAAU,EACjC,CAAC,SAAS,OAAO,GAAG,OAAO;IAE3B,YAAY,EAAE,CAAC,CAAC;IAChB,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU;IAC7D,UAAU,EAAE,CAAC,CAAC;IACd,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,CAAC,SAAS,UAAU,GAAG,UAAU,EACjC,CAAC,SAAS,OAAO,GAAG,OAAO,EAC3B,SAAS,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CA6BzE"}
|
package/lib/sha256.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sha256.d.ts","sourceRoot":"","sources":["../src/sha256.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAoDH;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"sha256.d.ts","sourceRoot":"","sources":["../src/sha256.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAoDH;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAiGjD"}
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -84,12 +84,22 @@ export function generateId(): string {
|
|
|
84
84
|
return a + b;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Longest secret, in hex characters, the host accepts. A UUID is 32. */
|
|
88
|
+
export const MAX_SECRET_HEX_LENGTH = 128;
|
|
89
|
+
|
|
87
90
|
/**
|
|
88
|
-
* Validates that a string looks like a UUID (with or without dashes, 32
|
|
91
|
+
* Validates that a string looks like a UUID (with or without dashes, 32 to
|
|
92
|
+
* {@link MAX_SECRET_HEX_LENGTH} hex chars). The upper bound keeps a client from
|
|
93
|
+
* making the host hash and store an arbitrarily long string.
|
|
89
94
|
*/
|
|
90
95
|
export function isValidSecret(secret: string): boolean {
|
|
96
|
+
if (secret.length > MAX_SECRET_HEX_LENGTH * 2) return false;
|
|
91
97
|
const hex = secret.replace(/-/g, "");
|
|
92
|
-
return
|
|
98
|
+
return (
|
|
99
|
+
hex.length >= 32 &&
|
|
100
|
+
hex.length <= MAX_SECRET_HEX_LENGTH &&
|
|
101
|
+
/^[0-9a-f]+$/i.test(hex)
|
|
102
|
+
);
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
/**
|
package/src/middleware.ts
CHANGED
|
@@ -178,7 +178,8 @@ export type ActionSchema<A extends IAction> = {
|
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
180
|
* Middleware that validates actions against a schema before they reach the
|
|
181
|
-
* reducer. Invalid actions are
|
|
181
|
+
* reducer. Invalid actions are dropped with a console warning. A validator
|
|
182
|
+
* that throws counts as a failed validation.
|
|
182
183
|
*
|
|
183
184
|
* Internal actions are never validated — they always pass through.
|
|
184
185
|
*
|
|
@@ -196,8 +197,21 @@ export function actionValidator<S extends IGameState, A extends IAction>(
|
|
|
196
197
|
if (getInternalTypes().has(action.type)) return next(action);
|
|
197
198
|
|
|
198
199
|
const validate = schema[action.type as A["type"]];
|
|
200
|
+
if (!validate) return next(action);
|
|
201
|
+
|
|
202
|
+
// A validator that throws has not said yes. Malformed client payloads are
|
|
203
|
+
// exactly what makes a validator like `(a) => a.payload.points > 0` throw,
|
|
204
|
+
// so a throw counts as a failed validation. Letting the middleware error
|
|
205
|
+
// boundary fall through to `next` would hand the reducer the very action
|
|
206
|
+
// the validator exists to stop.
|
|
207
|
+
let valid: boolean;
|
|
208
|
+
try {
|
|
209
|
+
valid = validate(action as A & { type: A["type"] });
|
|
210
|
+
} catch {
|
|
211
|
+
valid = false;
|
|
212
|
+
}
|
|
199
213
|
|
|
200
|
-
if (
|
|
214
|
+
if (!valid) {
|
|
201
215
|
console.warn(
|
|
202
216
|
`[couch-kit] Action "${action.type}" failed validation, skipping`,
|
|
203
217
|
);
|
package/src/replay.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createGameReducer } from "./reducer.js";
|
|
1
2
|
import type { GameReducer, IAction, IGameState } from "./types.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -45,17 +46,24 @@ export interface ReplayResult<S extends IGameState = IGameState> {
|
|
|
45
46
|
/**
|
|
46
47
|
* Replays a recording against a reducer, producing the final state
|
|
47
48
|
* and intermediate snapshots for each action applied.
|
|
49
|
+
*
|
|
50
|
+
* Pass the game's own reducer, as given to the host. It is wrapped with
|
|
51
|
+
* {@link createGameReducer} here, so recordings that include internal actions
|
|
52
|
+
* (`__PLAYER_JOINED__` and friends, as a host recorder captures) replay the
|
|
53
|
+
* same way they ran. Wrapping an already-wrapped reducer is harmless: the outer
|
|
54
|
+
* layer handles internal actions and the inner never sees them.
|
|
48
55
|
*/
|
|
49
56
|
export function replayActions<
|
|
50
57
|
S extends IGameState = IGameState,
|
|
51
58
|
A extends IAction = IAction,
|
|
52
59
|
>(recording: Recording<S, A>, reducer: GameReducer<S, A>): ReplayResult<S> {
|
|
53
60
|
const snapshots: StateSnapshot<S>[] = [];
|
|
61
|
+
const gameReducer = createGameReducer(reducer);
|
|
54
62
|
let currentState = recording.initialState;
|
|
55
63
|
|
|
56
64
|
for (let i = 0; i < recording.actions.length; i++) {
|
|
57
65
|
const { action, timestamp } = recording.actions[i];
|
|
58
|
-
currentState =
|
|
66
|
+
currentState = gameReducer(currentState, action);
|
|
59
67
|
|
|
60
68
|
snapshots.push({
|
|
61
69
|
state: currentState,
|
package/src/sha256.ts
CHANGED
|
@@ -111,10 +111,8 @@ export function sha256Hex(message: string): string {
|
|
|
111
111
|
0;
|
|
112
112
|
}
|
|
113
113
|
for (let i = 16; i < 64; i++) {
|
|
114
|
-
const s0 =
|
|
115
|
-
|
|
116
|
-
const s1 =
|
|
117
|
-
rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
114
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
115
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
118
116
|
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
119
117
|
}
|
|
120
118
|
|