@couch-kit/core 0.10.1 → 0.10.3
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 +21 -4
- package/dist/index.js +21 -4
- 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/reducer.d.ts.map +1 -1
- package/lib/types.d.ts +3 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +12 -2
- package/src/middleware.ts +16 -2
- package/src/reducer.ts +10 -2
- package/src/types.ts +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @couch-kit/core
|
|
2
2
|
|
|
3
|
+
## 0.10.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#227](https://github.com/faluciano/react-native-couch-kit/pull/227) [`8ef31ad`](https://github.com/faluciano/react-native-couch-kit/commit/8ef31adc53dc7342f1d81f8ba2e4acb6c8c1433e) Thanks [@faluciano](https://github.com/faluciano)! - A player who rejoins under a new name or avatar is renamed. `__PLAYER_RECONNECTED__` now carries the rejoining JOIN's `name` and `avatar`, and the reducer applies them. Before, a reconnect kept the old name, so "change name" in a controller did nothing. Recordings without them replay as before.
|
|
8
|
+
|
|
9
|
+
## 0.10.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#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.
|
|
14
|
+
|
|
3
15
|
## 0.10.1
|
|
4
16
|
|
|
5
17
|
### Patch 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
|
@@ -52,6 +52,7 @@ __export(exports_src, {
|
|
|
52
52
|
KEEPALIVE_TIMEOUT: () => KEEPALIVE_TIMEOUT,
|
|
53
53
|
MAX_FRAME_SIZE: () => MAX_FRAME_SIZE,
|
|
54
54
|
MAX_PENDING_PINGS: () => MAX_PENDING_PINGS,
|
|
55
|
+
MAX_SECRET_HEX_LENGTH: () => MAX_SECRET_HEX_LENGTH,
|
|
55
56
|
MAX_SYNC_INTERVAL: () => MAX_SYNC_INTERVAL,
|
|
56
57
|
MessageTypes: () => MessageTypes,
|
|
57
58
|
SYNC_BACKOFF_FACTOR: () => SYNC_BACKOFF_FACTOR,
|
|
@@ -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
|
}
|
|
@@ -186,7 +195,7 @@ function createGameReducer(reducer, options) {
|
|
|
186
195
|
};
|
|
187
196
|
}
|
|
188
197
|
case InternalActionTypes.PLAYER_RECONNECTED: {
|
|
189
|
-
const { playerId } = action.payload;
|
|
198
|
+
const { playerId, name, avatar } = action.payload;
|
|
190
199
|
const player = state.players[playerId];
|
|
191
200
|
if (!player)
|
|
192
201
|
return state;
|
|
@@ -194,7 +203,12 @@ function createGameReducer(reducer, options) {
|
|
|
194
203
|
...state,
|
|
195
204
|
players: {
|
|
196
205
|
...state.players,
|
|
197
|
-
[playerId]: {
|
|
206
|
+
[playerId]: {
|
|
207
|
+
...player,
|
|
208
|
+
connected: true,
|
|
209
|
+
...name !== undefined ? { name } : {},
|
|
210
|
+
...avatar !== undefined ? { avatar } : {}
|
|
211
|
+
}
|
|
198
212
|
}
|
|
199
213
|
};
|
|
200
214
|
}
|
|
@@ -399,9 +413,12 @@ function generateId() {
|
|
|
399
413
|
const b = Math.random().toString(36).substring(2, 10);
|
|
400
414
|
return a + b;
|
|
401
415
|
}
|
|
416
|
+
var MAX_SECRET_HEX_LENGTH = 128;
|
|
402
417
|
function isValidSecret(secret) {
|
|
418
|
+
if (secret.length > MAX_SECRET_HEX_LENGTH * 2)
|
|
419
|
+
return false;
|
|
403
420
|
const hex = secret.replace(/-/g, "");
|
|
404
|
-
return hex.length >= 32 && /^[0-9a-f]+$/i.test(hex);
|
|
421
|
+
return hex.length >= 32 && hex.length <= MAX_SECRET_HEX_LENGTH && /^[0-9a-f]+$/i.test(hex);
|
|
405
422
|
}
|
|
406
423
|
async function derivePlayerId(secret) {
|
|
407
424
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle?.digest === "function") {
|
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
|
}
|
|
@@ -116,7 +124,7 @@ function createGameReducer(reducer, options) {
|
|
|
116
124
|
};
|
|
117
125
|
}
|
|
118
126
|
case InternalActionTypes.PLAYER_RECONNECTED: {
|
|
119
|
-
const { playerId } = action.payload;
|
|
127
|
+
const { playerId, name, avatar } = action.payload;
|
|
120
128
|
const player = state.players[playerId];
|
|
121
129
|
if (!player)
|
|
122
130
|
return state;
|
|
@@ -124,7 +132,12 @@ function createGameReducer(reducer, options) {
|
|
|
124
132
|
...state,
|
|
125
133
|
players: {
|
|
126
134
|
...state.players,
|
|
127
|
-
[playerId]: {
|
|
135
|
+
[playerId]: {
|
|
136
|
+
...player,
|
|
137
|
+
connected: true,
|
|
138
|
+
...name !== undefined ? { name } : {},
|
|
139
|
+
...avatar !== undefined ? { avatar } : {}
|
|
140
|
+
}
|
|
128
141
|
}
|
|
129
142
|
};
|
|
130
143
|
}
|
|
@@ -329,9 +342,12 @@ function generateId() {
|
|
|
329
342
|
const b = Math.random().toString(36).substring(2, 10);
|
|
330
343
|
return a + b;
|
|
331
344
|
}
|
|
345
|
+
var MAX_SECRET_HEX_LENGTH = 128;
|
|
332
346
|
function isValidSecret(secret) {
|
|
347
|
+
if (secret.length > MAX_SECRET_HEX_LENGTH * 2)
|
|
348
|
+
return false;
|
|
333
349
|
const hex = secret.replace(/-/g, "");
|
|
334
|
-
return hex.length >= 32 && /^[0-9a-f]+$/i.test(hex);
|
|
350
|
+
return hex.length >= 32 && hex.length <= MAX_SECRET_HEX_LENGTH && /^[0-9a-f]+$/i.test(hex);
|
|
335
351
|
}
|
|
336
352
|
async function derivePlayerId(secret) {
|
|
337
353
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle?.digest === "function") {
|
|
@@ -386,6 +402,7 @@ export {
|
|
|
386
402
|
KEEPALIVE_TIMEOUT,
|
|
387
403
|
MAX_FRAME_SIZE,
|
|
388
404
|
MAX_PENDING_PINGS,
|
|
405
|
+
MAX_SECRET_HEX_LENGTH,
|
|
389
406
|
MAX_SYNC_INTERVAL,
|
|
390
407
|
MessageTypes,
|
|
391
408
|
SYNC_BACKOFF_FACTOR,
|
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/reducer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/reducer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EACV,UAAU,EACV,OAAO,EACP,cAAc,EACd,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,uCAAuC;AACvC,MAAM,WAAW,wBAAwB,CACvC,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,OAAO;IAEjB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;CACjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACvE,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,OAAO,CAAC,EAAE,wBAAwB,CAAC,CAAC,EAAE,CAAC,CAAC,GACvC,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/reducer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EACV,UAAU,EACV,OAAO,EACP,cAAc,EACd,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,uCAAuC;AACvC,MAAM,WAAW,wBAAwB,CACvC,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,OAAO;IAEjB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;CACjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EACvE,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,EAC1B,OAAO,CAAC,EAAE,wBAAwB,CAAC,CAAC,EAAE,CAAC,CAAC,GACvC,WAAW,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAwFvC"}
|
package/lib/types.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface IAction {
|
|
|
40
40
|
* - `__HYDRATE__` -- Replaces state wholesale (server-to-client sync).
|
|
41
41
|
* - `__PLAYER_JOINED__` -- Adds a player to `state.players`.
|
|
42
42
|
* - `__PLAYER_LEFT__` -- Marks a player as disconnected (`connected: false`).
|
|
43
|
-
* - `__PLAYER_RECONNECTED__` -- Restores a returning player (`connected: true`), preserving existing data.
|
|
43
|
+
* - `__PLAYER_RECONNECTED__` -- Restores a returning player (`connected: true`), preserving existing data. A `name` or `avatar` in the payload replaces the stored one, so a player who rejoins under a new name is renamed.
|
|
44
44
|
* - `__PLAYER_REMOVED__` -- Permanently removes a timed-out disconnected player from `state.players`.
|
|
45
45
|
*/
|
|
46
46
|
export type InternalAction<S extends IGameState = IGameState> = {
|
|
@@ -62,6 +62,8 @@ export type InternalAction<S extends IGameState = IGameState> = {
|
|
|
62
62
|
type: "__PLAYER_RECONNECTED__";
|
|
63
63
|
payload: {
|
|
64
64
|
playerId: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
avatar?: string;
|
|
65
67
|
};
|
|
66
68
|
} | {
|
|
67
69
|
type: "__PLAYER_REMOVED__";
|
package/lib/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,IACxD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,CAAC,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxD,GACD;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAC1D;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,IACxD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,CAAC,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxD,GACD;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GAC1D;IACE,IAAI,EAAE,wBAAwB,CAAC;IAC/B,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/D,GACD;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAElE,+CAA+C;AAC/C,eAAO,MAAM,mBAAmB;aAC9B,OAAO,EAAE,aAAa;aACtB,aAAa,EAAE,mBAAmB;aAClC,WAAW,EAAE,iBAAiB;aAC9B,kBAAkB,EAAE,wBAAwB;aAC5C,cAAc,EAAE,oBAAoB;CAC5B,CAAC;AAEX;;;;;GAKG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,IAAI,CACjE,KAAK,EAAE,CAAC,EACR,MAAM,EAAE,CAAC,KACN,CAAC,CAAC"}
|
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/reducer.ts
CHANGED
|
@@ -87,16 +87,24 @@ export function createGameReducer<S extends IGameState, A extends IAction>(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
case InternalActionTypes.PLAYER_RECONNECTED: {
|
|
90
|
-
const { playerId } = (
|
|
90
|
+
const { playerId, name, avatar } = (
|
|
91
91
|
action as InternalAction<S> & { type: "__PLAYER_RECONNECTED__" }
|
|
92
92
|
).payload;
|
|
93
93
|
const player = state.players[playerId];
|
|
94
94
|
if (!player) return state;
|
|
95
|
+
// Name and avatar come from the rejoining JOIN, so a player who changed
|
|
96
|
+
// their name and rejoined is renamed. Absent (older recordings), the
|
|
97
|
+
// stored values stand.
|
|
95
98
|
return {
|
|
96
99
|
...state,
|
|
97
100
|
players: {
|
|
98
101
|
...state.players,
|
|
99
|
-
[playerId]: {
|
|
102
|
+
[playerId]: {
|
|
103
|
+
...player,
|
|
104
|
+
connected: true,
|
|
105
|
+
...(name !== undefined ? { name } : {}),
|
|
106
|
+
...(avatar !== undefined ? { avatar } : {}),
|
|
107
|
+
},
|
|
100
108
|
},
|
|
101
109
|
};
|
|
102
110
|
}
|
package/src/types.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface IAction {
|
|
|
43
43
|
* - `__HYDRATE__` -- Replaces state wholesale (server-to-client sync).
|
|
44
44
|
* - `__PLAYER_JOINED__` -- Adds a player to `state.players`.
|
|
45
45
|
* - `__PLAYER_LEFT__` -- Marks a player as disconnected (`connected: false`).
|
|
46
|
-
* - `__PLAYER_RECONNECTED__` -- Restores a returning player (`connected: true`), preserving existing data.
|
|
46
|
+
* - `__PLAYER_RECONNECTED__` -- Restores a returning player (`connected: true`), preserving existing data. A `name` or `avatar` in the payload replaces the stored one, so a player who rejoins under a new name is renamed.
|
|
47
47
|
* - `__PLAYER_REMOVED__` -- Permanently removes a timed-out disconnected player from `state.players`.
|
|
48
48
|
*/
|
|
49
49
|
export type InternalAction<S extends IGameState = IGameState> =
|
|
@@ -53,7 +53,10 @@ export type InternalAction<S extends IGameState = IGameState> =
|
|
|
53
53
|
payload: { id: string; name: string; avatar?: string };
|
|
54
54
|
}
|
|
55
55
|
| { type: "__PLAYER_LEFT__"; payload: { playerId: string } }
|
|
56
|
-
| {
|
|
56
|
+
| {
|
|
57
|
+
type: "__PLAYER_RECONNECTED__";
|
|
58
|
+
payload: { playerId: string; name?: string; avatar?: string };
|
|
59
|
+
}
|
|
57
60
|
| { type: "__PLAYER_REMOVED__"; payload: { playerId: string } };
|
|
58
61
|
|
|
59
62
|
/** Well-known internal action type strings. */
|