@couch-kit/core 0.9.0 → 0.9.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 +23 -0
- package/README.md +2 -2
- package/dist/index.cjs +152 -9
- package/dist/index.js +152 -9
- package/lib/constants.d.ts +5 -2
- package/lib/constants.d.ts.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/reducer.d.ts +32 -0
- package/lib/reducer.d.ts.map +1 -0
- package/lib/sha256.d.ts +20 -0
- package/lib/sha256.d.ts.map +1 -0
- package/lib/types.d.ts +0 -30
- package/lib/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/constants.ts +12 -4
- package/src/index.ts +1 -0
- package/src/reducer.ts +126 -0
- package/src/sha256.ts +169 -0
- package/src/types.ts +0 -120
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @couch-kit/core
|
|
2
2
|
|
|
3
|
+
## 0.9.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#85](https://github.com/faluciano/react-native-couch-kit/pull/85) [`0064a42`](https://github.com/faluciano/react-native-couch-kit/commit/0064a42e0593d2ed2734c27926c5df686dcf3b5f) Thanks [@faluciano](https://github.com/faluciano)! - **Security:** Derive public player IDs with a real SHA-256 on React Native / Hermes.
|
|
8
|
+
|
|
9
|
+
`derivePlayerId` hashes the session secret with SHA-256 via the Web Crypto API,
|
|
10
|
+
but Hermes (where the host runs) does not expose `crypto.subtle`. The previous
|
|
11
|
+
fallback used `derivePlayerIdLegacy`, which simply truncated the secret —
|
|
12
|
+
exposing the first half of the raw session token as the public `playerId`
|
|
13
|
+
broadcast in game state.
|
|
14
|
+
|
|
15
|
+
The fallback now uses a dependency-free pure-JS SHA-256 that produces the exact
|
|
16
|
+
same digest as `crypto.subtle`, so player IDs are secure and identical across
|
|
17
|
+
all host runtimes. `derivePlayerIdLegacy` is retained only for reconnect
|
|
18
|
+
migration of players who joined under the old scheme.
|
|
19
|
+
|
|
20
|
+
## 0.9.1
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- [#54](https://github.com/faluciano/react-native-couch-kit/pull/54) [`6860811`](https://github.com/faluciano/react-native-couch-kit/commit/6860811ec2fc0648fd9fd9d676c1b6ebcb4d14d0) Thanks [@faluciano](https://github.com/faluciano)! - Move `createGameReducer` and `CreateGameReducerOptions` from `types.ts` into a dedicated `reducer.ts` module. This is an internal refactor — the public API is unchanged (both remain exported from `@couch-kit/core`) and runtime behaviour is identical. It also removes a circular import between `types.ts` and `middleware.ts`.
|
|
25
|
+
|
|
3
26
|
## 0.9.0
|
|
4
27
|
|
|
5
28
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -190,9 +190,9 @@ try {
|
|
|
190
190
|
|
|
191
191
|
Validates that a string is a valid UUID format (32+ hex characters, with or without dashes).
|
|
192
192
|
|
|
193
|
-
#### `derivePlayerId(secret: string): string
|
|
193
|
+
#### `derivePlayerId(secret: string): Promise<string>`
|
|
194
194
|
|
|
195
|
-
Derives a stable, public player ID from a secret UUID
|
|
195
|
+
Derives a stable, public player ID from a secret UUID using SHA-256, taking the first 16 hex characters of the hash. Async because it uses the Web Crypto `crypto.subtle.digest` API.
|
|
196
196
|
|
|
197
197
|
### Constants
|
|
198
198
|
|
package/dist/index.cjs
CHANGED
|
@@ -56,6 +56,14 @@ __export(exports_src, {
|
|
|
56
56
|
});
|
|
57
57
|
module.exports = __toCommonJS(exports_src);
|
|
58
58
|
|
|
59
|
+
// src/types.ts
|
|
60
|
+
var InternalActionTypes = {
|
|
61
|
+
HYDRATE: "__HYDRATE__",
|
|
62
|
+
PLAYER_JOINED: "__PLAYER_JOINED__",
|
|
63
|
+
PLAYER_LEFT: "__PLAYER_LEFT__",
|
|
64
|
+
PLAYER_RECONNECTED: "__PLAYER_RECONNECTED__",
|
|
65
|
+
PLAYER_REMOVED: "__PLAYER_REMOVED__"
|
|
66
|
+
};
|
|
59
67
|
// src/middleware.ts
|
|
60
68
|
function applyMiddleware(...middlewares) {
|
|
61
69
|
return (reducer) => {
|
|
@@ -130,14 +138,7 @@ function actionValidator(schema) {
|
|
|
130
138
|
};
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
// src/
|
|
134
|
-
var InternalActionTypes = {
|
|
135
|
-
HYDRATE: "__HYDRATE__",
|
|
136
|
-
PLAYER_JOINED: "__PLAYER_JOINED__",
|
|
137
|
-
PLAYER_LEFT: "__PLAYER_LEFT__",
|
|
138
|
-
PLAYER_RECONNECTED: "__PLAYER_RECONNECTED__",
|
|
139
|
-
PLAYER_REMOVED: "__PLAYER_REMOVED__"
|
|
140
|
-
};
|
|
141
|
+
// src/reducer.ts
|
|
141
142
|
function createGameReducer(reducer, options) {
|
|
142
143
|
const baseReducer = (state, action) => {
|
|
143
144
|
switch (action.type) {
|
|
@@ -216,6 +217,148 @@ var MessageTypes = {
|
|
|
216
217
|
RECONNECTED: "RECONNECTED",
|
|
217
218
|
ERROR: "ERROR"
|
|
218
219
|
};
|
|
220
|
+
// src/sha256.ts
|
|
221
|
+
var K = new Uint32Array([
|
|
222
|
+
1116352408,
|
|
223
|
+
1899447441,
|
|
224
|
+
3049323471,
|
|
225
|
+
3921009573,
|
|
226
|
+
961987163,
|
|
227
|
+
1508970993,
|
|
228
|
+
2453635748,
|
|
229
|
+
2870763221,
|
|
230
|
+
3624381080,
|
|
231
|
+
310598401,
|
|
232
|
+
607225278,
|
|
233
|
+
1426881987,
|
|
234
|
+
1925078388,
|
|
235
|
+
2162078206,
|
|
236
|
+
2614888103,
|
|
237
|
+
3248222580,
|
|
238
|
+
3835390401,
|
|
239
|
+
4022224774,
|
|
240
|
+
264347078,
|
|
241
|
+
604807628,
|
|
242
|
+
770255983,
|
|
243
|
+
1249150122,
|
|
244
|
+
1555081692,
|
|
245
|
+
1996064986,
|
|
246
|
+
2554220882,
|
|
247
|
+
2821834349,
|
|
248
|
+
2952996808,
|
|
249
|
+
3210313671,
|
|
250
|
+
3336571891,
|
|
251
|
+
3584528711,
|
|
252
|
+
113926993,
|
|
253
|
+
338241895,
|
|
254
|
+
666307205,
|
|
255
|
+
773529912,
|
|
256
|
+
1294757372,
|
|
257
|
+
1396182291,
|
|
258
|
+
1695183700,
|
|
259
|
+
1986661051,
|
|
260
|
+
2177026350,
|
|
261
|
+
2456956037,
|
|
262
|
+
2730485921,
|
|
263
|
+
2820302411,
|
|
264
|
+
3259730800,
|
|
265
|
+
3345764771,
|
|
266
|
+
3516065817,
|
|
267
|
+
3600352804,
|
|
268
|
+
4094571909,
|
|
269
|
+
275423344,
|
|
270
|
+
430227734,
|
|
271
|
+
506948616,
|
|
272
|
+
659060556,
|
|
273
|
+
883997877,
|
|
274
|
+
958139571,
|
|
275
|
+
1322822218,
|
|
276
|
+
1537002063,
|
|
277
|
+
1747873779,
|
|
278
|
+
1955562222,
|
|
279
|
+
2024104815,
|
|
280
|
+
2227730452,
|
|
281
|
+
2361852424,
|
|
282
|
+
2428436474,
|
|
283
|
+
2756734187,
|
|
284
|
+
3204031479,
|
|
285
|
+
3329325298
|
|
286
|
+
]);
|
|
287
|
+
function utf8Bytes(str) {
|
|
288
|
+
const bytes = [];
|
|
289
|
+
for (let i = 0;i < str.length; i++) {
|
|
290
|
+
let code = str.charCodeAt(i);
|
|
291
|
+
if (code >= 55296 && code <= 56319 && i + 1 < str.length) {
|
|
292
|
+
const next = str.charCodeAt(i + 1);
|
|
293
|
+
if (next >= 56320 && next <= 57343) {
|
|
294
|
+
code = 65536 + (code - 55296 << 10) + (next - 56320);
|
|
295
|
+
i++;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (code < 128) {
|
|
299
|
+
bytes.push(code);
|
|
300
|
+
} else if (code < 2048) {
|
|
301
|
+
bytes.push(192 | code >> 6, 128 | code & 63);
|
|
302
|
+
} else if (code < 65536) {
|
|
303
|
+
bytes.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
|
|
304
|
+
} else {
|
|
305
|
+
bytes.push(240 | code >> 18, 128 | code >> 12 & 63, 128 | code >> 6 & 63, 128 | code & 63);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return bytes;
|
|
309
|
+
}
|
|
310
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
311
|
+
function sha256Hex(message) {
|
|
312
|
+
const bytes = utf8Bytes(message);
|
|
313
|
+
const bitLen = bytes.length * 8;
|
|
314
|
+
bytes.push(128);
|
|
315
|
+
while (bytes.length % 64 !== 56)
|
|
316
|
+
bytes.push(0);
|
|
317
|
+
const hi = Math.floor(bitLen / 4294967296);
|
|
318
|
+
const lo = bitLen >>> 0;
|
|
319
|
+
bytes.push(hi >>> 24 & 255, hi >>> 16 & 255, hi >>> 8 & 255, hi & 255, lo >>> 24 & 255, lo >>> 16 & 255, lo >>> 8 & 255, lo & 255);
|
|
320
|
+
let h0 = 1779033703, h1 = 3144134277, h2 = 1013904242, h3 = 2773480762, h4 = 1359893119, h5 = 2600822924, h6 = 528734635, h7 = 1541459225;
|
|
321
|
+
const w = new Uint32Array(64);
|
|
322
|
+
for (let offset = 0;offset < bytes.length; offset += 64) {
|
|
323
|
+
for (let i = 0;i < 16; i++) {
|
|
324
|
+
const j = offset + i * 4;
|
|
325
|
+
w[i] = (bytes[j] << 24 | bytes[j + 1] << 16 | bytes[j + 2] << 8 | bytes[j + 3]) >>> 0;
|
|
326
|
+
}
|
|
327
|
+
for (let i = 16;i < 64; i++) {
|
|
328
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
|
|
329
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
|
|
330
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
331
|
+
}
|
|
332
|
+
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
|
333
|
+
for (let i = 0;i < 64; i++) {
|
|
334
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
335
|
+
const ch = e & f ^ ~e & g;
|
|
336
|
+
const temp1 = h + S1 + ch + K[i] + w[i] >>> 0;
|
|
337
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
338
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
339
|
+
const temp2 = S0 + maj >>> 0;
|
|
340
|
+
h = g;
|
|
341
|
+
g = f;
|
|
342
|
+
f = e;
|
|
343
|
+
e = d + temp1 >>> 0;
|
|
344
|
+
d = c;
|
|
345
|
+
c = b;
|
|
346
|
+
b = a;
|
|
347
|
+
a = temp1 + temp2 >>> 0;
|
|
348
|
+
}
|
|
349
|
+
h0 = h0 + a >>> 0;
|
|
350
|
+
h1 = h1 + b >>> 0;
|
|
351
|
+
h2 = h2 + c >>> 0;
|
|
352
|
+
h3 = h3 + d >>> 0;
|
|
353
|
+
h4 = h4 + e >>> 0;
|
|
354
|
+
h5 = h5 + f >>> 0;
|
|
355
|
+
h6 = h6 + g >>> 0;
|
|
356
|
+
h7 = h7 + h >>> 0;
|
|
357
|
+
}
|
|
358
|
+
const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
|
|
359
|
+
return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7);
|
|
360
|
+
}
|
|
361
|
+
|
|
219
362
|
// src/constants.ts
|
|
220
363
|
var DEFAULT_HTTP_PORT = 8080;
|
|
221
364
|
var DEFAULT_WS_PORT_OFFSET = 2;
|
|
@@ -252,7 +395,7 @@ async function derivePlayerId(secret) {
|
|
|
252
395
|
const hash = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
253
396
|
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
|
|
254
397
|
}
|
|
255
|
-
return
|
|
398
|
+
return sha256Hex(secret).slice(0, 16);
|
|
256
399
|
}
|
|
257
400
|
function derivePlayerIdLegacy(secret) {
|
|
258
401
|
return secret.replace(/-/g, "").slice(0, 16);
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var InternalActionTypes = {
|
|
3
|
+
HYDRATE: "__HYDRATE__",
|
|
4
|
+
PLAYER_JOINED: "__PLAYER_JOINED__",
|
|
5
|
+
PLAYER_LEFT: "__PLAYER_LEFT__",
|
|
6
|
+
PLAYER_RECONNECTED: "__PLAYER_RECONNECTED__",
|
|
7
|
+
PLAYER_REMOVED: "__PLAYER_REMOVED__"
|
|
8
|
+
};
|
|
1
9
|
// src/middleware.ts
|
|
2
10
|
function applyMiddleware(...middlewares) {
|
|
3
11
|
return (reducer) => {
|
|
@@ -72,14 +80,7 @@ function actionValidator(schema) {
|
|
|
72
80
|
};
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
// src/
|
|
76
|
-
var InternalActionTypes = {
|
|
77
|
-
HYDRATE: "__HYDRATE__",
|
|
78
|
-
PLAYER_JOINED: "__PLAYER_JOINED__",
|
|
79
|
-
PLAYER_LEFT: "__PLAYER_LEFT__",
|
|
80
|
-
PLAYER_RECONNECTED: "__PLAYER_RECONNECTED__",
|
|
81
|
-
PLAYER_REMOVED: "__PLAYER_REMOVED__"
|
|
82
|
-
};
|
|
83
|
+
// src/reducer.ts
|
|
83
84
|
function createGameReducer(reducer, options) {
|
|
84
85
|
const baseReducer = (state, action) => {
|
|
85
86
|
switch (action.type) {
|
|
@@ -158,6 +159,148 @@ var MessageTypes = {
|
|
|
158
159
|
RECONNECTED: "RECONNECTED",
|
|
159
160
|
ERROR: "ERROR"
|
|
160
161
|
};
|
|
162
|
+
// src/sha256.ts
|
|
163
|
+
var K = new Uint32Array([
|
|
164
|
+
1116352408,
|
|
165
|
+
1899447441,
|
|
166
|
+
3049323471,
|
|
167
|
+
3921009573,
|
|
168
|
+
961987163,
|
|
169
|
+
1508970993,
|
|
170
|
+
2453635748,
|
|
171
|
+
2870763221,
|
|
172
|
+
3624381080,
|
|
173
|
+
310598401,
|
|
174
|
+
607225278,
|
|
175
|
+
1426881987,
|
|
176
|
+
1925078388,
|
|
177
|
+
2162078206,
|
|
178
|
+
2614888103,
|
|
179
|
+
3248222580,
|
|
180
|
+
3835390401,
|
|
181
|
+
4022224774,
|
|
182
|
+
264347078,
|
|
183
|
+
604807628,
|
|
184
|
+
770255983,
|
|
185
|
+
1249150122,
|
|
186
|
+
1555081692,
|
|
187
|
+
1996064986,
|
|
188
|
+
2554220882,
|
|
189
|
+
2821834349,
|
|
190
|
+
2952996808,
|
|
191
|
+
3210313671,
|
|
192
|
+
3336571891,
|
|
193
|
+
3584528711,
|
|
194
|
+
113926993,
|
|
195
|
+
338241895,
|
|
196
|
+
666307205,
|
|
197
|
+
773529912,
|
|
198
|
+
1294757372,
|
|
199
|
+
1396182291,
|
|
200
|
+
1695183700,
|
|
201
|
+
1986661051,
|
|
202
|
+
2177026350,
|
|
203
|
+
2456956037,
|
|
204
|
+
2730485921,
|
|
205
|
+
2820302411,
|
|
206
|
+
3259730800,
|
|
207
|
+
3345764771,
|
|
208
|
+
3516065817,
|
|
209
|
+
3600352804,
|
|
210
|
+
4094571909,
|
|
211
|
+
275423344,
|
|
212
|
+
430227734,
|
|
213
|
+
506948616,
|
|
214
|
+
659060556,
|
|
215
|
+
883997877,
|
|
216
|
+
958139571,
|
|
217
|
+
1322822218,
|
|
218
|
+
1537002063,
|
|
219
|
+
1747873779,
|
|
220
|
+
1955562222,
|
|
221
|
+
2024104815,
|
|
222
|
+
2227730452,
|
|
223
|
+
2361852424,
|
|
224
|
+
2428436474,
|
|
225
|
+
2756734187,
|
|
226
|
+
3204031479,
|
|
227
|
+
3329325298
|
|
228
|
+
]);
|
|
229
|
+
function utf8Bytes(str) {
|
|
230
|
+
const bytes = [];
|
|
231
|
+
for (let i = 0;i < str.length; i++) {
|
|
232
|
+
let code = str.charCodeAt(i);
|
|
233
|
+
if (code >= 55296 && code <= 56319 && i + 1 < str.length) {
|
|
234
|
+
const next = str.charCodeAt(i + 1);
|
|
235
|
+
if (next >= 56320 && next <= 57343) {
|
|
236
|
+
code = 65536 + (code - 55296 << 10) + (next - 56320);
|
|
237
|
+
i++;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (code < 128) {
|
|
241
|
+
bytes.push(code);
|
|
242
|
+
} else if (code < 2048) {
|
|
243
|
+
bytes.push(192 | code >> 6, 128 | code & 63);
|
|
244
|
+
} else if (code < 65536) {
|
|
245
|
+
bytes.push(224 | code >> 12, 128 | code >> 6 & 63, 128 | code & 63);
|
|
246
|
+
} else {
|
|
247
|
+
bytes.push(240 | code >> 18, 128 | code >> 12 & 63, 128 | code >> 6 & 63, 128 | code & 63);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return bytes;
|
|
251
|
+
}
|
|
252
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
253
|
+
function sha256Hex(message) {
|
|
254
|
+
const bytes = utf8Bytes(message);
|
|
255
|
+
const bitLen = bytes.length * 8;
|
|
256
|
+
bytes.push(128);
|
|
257
|
+
while (bytes.length % 64 !== 56)
|
|
258
|
+
bytes.push(0);
|
|
259
|
+
const hi = Math.floor(bitLen / 4294967296);
|
|
260
|
+
const lo = bitLen >>> 0;
|
|
261
|
+
bytes.push(hi >>> 24 & 255, hi >>> 16 & 255, hi >>> 8 & 255, hi & 255, lo >>> 24 & 255, lo >>> 16 & 255, lo >>> 8 & 255, lo & 255);
|
|
262
|
+
let h0 = 1779033703, h1 = 3144134277, h2 = 1013904242, h3 = 2773480762, h4 = 1359893119, h5 = 2600822924, h6 = 528734635, h7 = 1541459225;
|
|
263
|
+
const w = new Uint32Array(64);
|
|
264
|
+
for (let offset = 0;offset < bytes.length; offset += 64) {
|
|
265
|
+
for (let i = 0;i < 16; i++) {
|
|
266
|
+
const j = offset + i * 4;
|
|
267
|
+
w[i] = (bytes[j] << 24 | bytes[j + 1] << 16 | bytes[j + 2] << 8 | bytes[j + 3]) >>> 0;
|
|
268
|
+
}
|
|
269
|
+
for (let i = 16;i < 64; i++) {
|
|
270
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
|
|
271
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
|
|
272
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
273
|
+
}
|
|
274
|
+
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
|
275
|
+
for (let i = 0;i < 64; i++) {
|
|
276
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
277
|
+
const ch = e & f ^ ~e & g;
|
|
278
|
+
const temp1 = h + S1 + ch + K[i] + w[i] >>> 0;
|
|
279
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
280
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
281
|
+
const temp2 = S0 + maj >>> 0;
|
|
282
|
+
h = g;
|
|
283
|
+
g = f;
|
|
284
|
+
f = e;
|
|
285
|
+
e = d + temp1 >>> 0;
|
|
286
|
+
d = c;
|
|
287
|
+
c = b;
|
|
288
|
+
b = a;
|
|
289
|
+
a = temp1 + temp2 >>> 0;
|
|
290
|
+
}
|
|
291
|
+
h0 = h0 + a >>> 0;
|
|
292
|
+
h1 = h1 + b >>> 0;
|
|
293
|
+
h2 = h2 + c >>> 0;
|
|
294
|
+
h3 = h3 + d >>> 0;
|
|
295
|
+
h4 = h4 + e >>> 0;
|
|
296
|
+
h5 = h5 + f >>> 0;
|
|
297
|
+
h6 = h6 + g >>> 0;
|
|
298
|
+
h7 = h7 + h >>> 0;
|
|
299
|
+
}
|
|
300
|
+
const toHex = (n) => (n >>> 0).toString(16).padStart(8, "0");
|
|
301
|
+
return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7);
|
|
302
|
+
}
|
|
303
|
+
|
|
161
304
|
// src/constants.ts
|
|
162
305
|
var DEFAULT_HTTP_PORT = 8080;
|
|
163
306
|
var DEFAULT_WS_PORT_OFFSET = 2;
|
|
@@ -194,7 +337,7 @@ async function derivePlayerId(secret) {
|
|
|
194
337
|
const hash = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
195
338
|
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
|
|
196
339
|
}
|
|
197
|
-
return
|
|
340
|
+
return sha256Hex(secret).slice(0, 16);
|
|
198
341
|
}
|
|
199
342
|
function derivePlayerIdLegacy(secret) {
|
|
200
343
|
return secret.replace(/-/g, "").slice(0, 16);
|
package/lib/constants.d.ts
CHANGED
|
@@ -40,8 +40,11 @@ export declare function isValidSecret(secret: string): boolean;
|
|
|
40
40
|
* Takes the first 16 hex characters of the SHA-256 hash, producing
|
|
41
41
|
* a deterministic ID that cannot be reversed to recover the secret.
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
43
|
+
* Uses the Web Crypto API (`crypto.subtle`) when available (browser / Node),
|
|
44
|
+
* and a dependency-free pure-JS SHA-256 fallback otherwise (e.g. React Native
|
|
45
|
+
* / Hermes, which does not expose `crypto.subtle`). Both paths produce the
|
|
46
|
+
* same digest, so the derived ID is identical across host runtimes and never
|
|
47
|
+
* leaks any part of the secret.
|
|
45
48
|
*/
|
|
46
49
|
export declare function derivePlayerId(secret: string): Promise<string>;
|
|
47
50
|
/**
|
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;
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,wDAAwD;AAKxD,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,4CAA4C;AAC5C,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C,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;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGrD;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/index.d.ts
CHANGED
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC"}
|
package/lib/reducer.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Middleware } from "./middleware";
|
|
2
|
+
import type { IGameState, IAction, InternalAction, GameReducer } from "./types";
|
|
3
|
+
/** Options for `createGameReducer`. */
|
|
4
|
+
export interface CreateGameReducerOptions<S extends IGameState, A extends IAction> {
|
|
5
|
+
/**
|
|
6
|
+
* Optional middleware stack. Middlewares are applied in order — the first
|
|
7
|
+
* middleware in the array is the outermost layer and sees every action first.
|
|
8
|
+
*
|
|
9
|
+
* Middleware can observe **and** transform both user actions and internal
|
|
10
|
+
* actions. Custom middleware should avoid blocking internal actions (types
|
|
11
|
+
* prefixed with `__`) to prevent breaking framework behaviour.
|
|
12
|
+
*/
|
|
13
|
+
middleware?: Middleware<S, A>[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a user-provided reducer with automatic handling of internal actions:
|
|
17
|
+
*
|
|
18
|
+
* - `__HYDRATE__` -- Replaces state wholesale (used for server→client state sync).
|
|
19
|
+
* - `__PLAYER_JOINED__` -- Adds a player to `state.players`.
|
|
20
|
+
* - `__PLAYER_LEFT__` -- Marks a player as disconnected in `state.players`.
|
|
21
|
+
* - `__PLAYER_RECONNECTED__` -- Restores a returning player's connection status, preserving all existing data.
|
|
22
|
+
* - `__PLAYER_REMOVED__` -- Permanently removes a timed-out disconnected player from `state.players`.
|
|
23
|
+
*
|
|
24
|
+
* The wrapped reducer accepts both `A` (user actions) and `InternalAction<S>`.
|
|
25
|
+
* User reducers only need to handle their own action types.
|
|
26
|
+
*
|
|
27
|
+
* When `options.middleware` is provided, the middleware stack wraps around the
|
|
28
|
+
* entire reducer (including internal-action handling). Middleware can observe
|
|
29
|
+
* all actions flowing through the reducer.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createGameReducer<S extends IGameState, A extends IAction>(reducer: GameReducer<S, A>, options?: CreateGameReducerOptions<S, A>): GameReducer<S, A | InternalAction<S>>;
|
|
32
|
+
//# sourceMappingURL=reducer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reducer.d.ts","sourceRoot":"","sources":["../src/reducer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EACV,UAAU,EACV,OAAO,EACP,cAAc,EACd,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,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,CAgFvC"}
|
package/lib/sha256.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free SHA-256 implementation.
|
|
3
|
+
*
|
|
4
|
+
* Couch Kit derives public player IDs from a secret using SHA-256. Browsers and
|
|
5
|
+
* Node expose this via the Web Crypto API (`crypto.subtle.digest`), but React
|
|
6
|
+
* Native / Hermes — where the host actually runs — does **not** provide
|
|
7
|
+
* `crypto.subtle` by default. This pure-JS implementation is used as the
|
|
8
|
+
* fallback so the host still produces a proper one-way hash instead of leaking
|
|
9
|
+
* part of the secret. It is byte-for-byte compatible with
|
|
10
|
+
* `crypto.subtle.digest("SHA-256", ...)`, so player IDs are stable regardless
|
|
11
|
+
* of which runtime the host uses.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Compute the SHA-256 digest of a UTF-8 string.
|
|
15
|
+
*
|
|
16
|
+
* @param message - The string to hash.
|
|
17
|
+
* @returns Lowercase hex-encoded 256-bit digest (64 characters).
|
|
18
|
+
*/
|
|
19
|
+
export declare function sha256Hex(message: string): string;
|
|
20
|
+
//# sourceMappingURL=sha256.d.ts.map
|
|
@@ -0,0 +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,CAmGjD"}
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Middleware } from "./middleware";
|
|
2
1
|
/**
|
|
3
2
|
* Represents a player connected to the game session.
|
|
4
3
|
*
|
|
@@ -85,33 +84,4 @@ export declare const InternalActionTypes: {
|
|
|
85
84
|
* Used by both `GameHostProvider` and `useGameClient` to define game logic.
|
|
86
85
|
*/
|
|
87
86
|
export type GameReducer<S extends IGameState, A extends IAction> = (state: S, action: A) => S;
|
|
88
|
-
/** Options for `createGameReducer`. */
|
|
89
|
-
export interface CreateGameReducerOptions<S extends IGameState, A extends IAction> {
|
|
90
|
-
/**
|
|
91
|
-
* Optional middleware stack. Middlewares are applied in order — the first
|
|
92
|
-
* middleware in the array is the outermost layer and sees every action first.
|
|
93
|
-
*
|
|
94
|
-
* Middleware can observe **and** transform both user actions and internal
|
|
95
|
-
* actions. Custom middleware should avoid blocking internal actions (types
|
|
96
|
-
* prefixed with `__`) to prevent breaking framework behaviour.
|
|
97
|
-
*/
|
|
98
|
-
middleware?: Middleware<S, A>[];
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Wraps a user-provided reducer with automatic handling of internal actions:
|
|
102
|
-
*
|
|
103
|
-
* - `__HYDRATE__` -- Replaces state wholesale (used for server→client state sync).
|
|
104
|
-
* - `__PLAYER_JOINED__` -- Adds a player to `state.players`.
|
|
105
|
-
* - `__PLAYER_LEFT__` -- Marks a player as disconnected in `state.players`.
|
|
106
|
-
* - `__PLAYER_RECONNECTED__` -- Restores a returning player's connection status, preserving all existing data.
|
|
107
|
-
* - `__PLAYER_REMOVED__` -- Permanently removes a timed-out disconnected player from `state.players`.
|
|
108
|
-
*
|
|
109
|
-
* The wrapped reducer accepts both `A` (user actions) and `InternalAction<S>`.
|
|
110
|
-
* User reducers only need to handle their own action types.
|
|
111
|
-
*
|
|
112
|
-
* When `options.middleware` is provided, the middleware stack wraps around the
|
|
113
|
-
* entire reducer (including internal-action handling). Middleware can observe
|
|
114
|
-
* all actions flowing through the reducer.
|
|
115
|
-
*/
|
|
116
|
-
export declare function createGameReducer<S extends IGameState, A extends IAction>(reducer: GameReducer<S, A>, options?: CreateGameReducerOptions<S, A>): GameReducer<S, A | InternalAction<S>>;
|
|
117
87
|
//# sourceMappingURL=types.d.ts.map
|
package/lib/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
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;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACjE;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;;;;;;CAMtB,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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@couch-kit/core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"provenance": true
|
|
@@ -50,6 +50,6 @@
|
|
|
50
50
|
"clean": "rm -rf dist lib"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"typescript": "^
|
|
53
|
+
"typescript": "^6.0.0"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Shared constants for Couch Kit protocol defaults. */
|
|
2
2
|
|
|
3
|
+
import { sha256Hex } from "./sha256";
|
|
4
|
+
|
|
5
|
+
|
|
3
6
|
/** Default HTTP port for the static file server. */
|
|
4
7
|
export const DEFAULT_HTTP_PORT = 8080;
|
|
5
8
|
|
|
@@ -82,8 +85,11 @@ export function isValidSecret(secret: string): boolean {
|
|
|
82
85
|
* Takes the first 16 hex characters of the SHA-256 hash, producing
|
|
83
86
|
* a deterministic ID that cannot be reversed to recover the secret.
|
|
84
87
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
88
|
+
* Uses the Web Crypto API (`crypto.subtle`) when available (browser / Node),
|
|
89
|
+
* and a dependency-free pure-JS SHA-256 fallback otherwise (e.g. React Native
|
|
90
|
+
* / Hermes, which does not expose `crypto.subtle`). Both paths produce the
|
|
91
|
+
* same digest, so the derived ID is identical across host runtimes and never
|
|
92
|
+
* leaks any part of the secret.
|
|
87
93
|
*/
|
|
88
94
|
export async function derivePlayerId(secret: string): Promise<string> {
|
|
89
95
|
if (
|
|
@@ -98,8 +104,10 @@ export async function derivePlayerId(secret: string): Promise<string> {
|
|
|
98
104
|
.slice(0, 16);
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
// Web Crypto unavailable (React Native / Hermes) —
|
|
102
|
-
|
|
107
|
+
// Web Crypto unavailable (React Native / Hermes) — use the pure-JS SHA-256
|
|
108
|
+
// fallback. This still produces a secure one-way hash (unlike the deprecated
|
|
109
|
+
// legacy derivation, which exposed the first half of the secret).
|
|
110
|
+
return sha256Hex(secret).slice(0, 16);
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
/**
|
package/src/index.ts
CHANGED
package/src/reducer.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { applyMiddleware } from "./middleware";
|
|
2
|
+
import type { Middleware } from "./middleware";
|
|
3
|
+
import { InternalActionTypes } from "./types";
|
|
4
|
+
import type {
|
|
5
|
+
IGameState,
|
|
6
|
+
IAction,
|
|
7
|
+
InternalAction,
|
|
8
|
+
GameReducer,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
/** Options for `createGameReducer`. */
|
|
12
|
+
export interface CreateGameReducerOptions<
|
|
13
|
+
S extends IGameState,
|
|
14
|
+
A extends IAction,
|
|
15
|
+
> {
|
|
16
|
+
/**
|
|
17
|
+
* Optional middleware stack. Middlewares are applied in order — the first
|
|
18
|
+
* middleware in the array is the outermost layer and sees every action first.
|
|
19
|
+
*
|
|
20
|
+
* Middleware can observe **and** transform both user actions and internal
|
|
21
|
+
* actions. Custom middleware should avoid blocking internal actions (types
|
|
22
|
+
* prefixed with `__`) to prevent breaking framework behaviour.
|
|
23
|
+
*/
|
|
24
|
+
middleware?: Middleware<S, A>[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wraps a user-provided reducer with automatic handling of internal actions:
|
|
29
|
+
*
|
|
30
|
+
* - `__HYDRATE__` -- Replaces state wholesale (used for server→client state sync).
|
|
31
|
+
* - `__PLAYER_JOINED__` -- Adds a player to `state.players`.
|
|
32
|
+
* - `__PLAYER_LEFT__` -- Marks a player as disconnected in `state.players`.
|
|
33
|
+
* - `__PLAYER_RECONNECTED__` -- Restores a returning player's connection status, preserving all existing data.
|
|
34
|
+
* - `__PLAYER_REMOVED__` -- Permanently removes a timed-out disconnected player from `state.players`.
|
|
35
|
+
*
|
|
36
|
+
* The wrapped reducer accepts both `A` (user actions) and `InternalAction<S>`.
|
|
37
|
+
* User reducers only need to handle their own action types.
|
|
38
|
+
*
|
|
39
|
+
* When `options.middleware` is provided, the middleware stack wraps around the
|
|
40
|
+
* entire reducer (including internal-action handling). Middleware can observe
|
|
41
|
+
* all actions flowing through the reducer.
|
|
42
|
+
*/
|
|
43
|
+
export function createGameReducer<S extends IGameState, A extends IAction>(
|
|
44
|
+
reducer: GameReducer<S, A>,
|
|
45
|
+
options?: CreateGameReducerOptions<S, A>,
|
|
46
|
+
): GameReducer<S, A | InternalAction<S>> {
|
|
47
|
+
const baseReducer: GameReducer<S, A | InternalAction<S>> = (
|
|
48
|
+
state: S,
|
|
49
|
+
action: A | InternalAction<S>,
|
|
50
|
+
): S => {
|
|
51
|
+
switch (action.type) {
|
|
52
|
+
case InternalActionTypes.HYDRATE:
|
|
53
|
+
return (action as InternalAction<S> & { type: "__HYDRATE__" }).payload;
|
|
54
|
+
|
|
55
|
+
case InternalActionTypes.PLAYER_JOINED: {
|
|
56
|
+
const { id, name, avatar } = (
|
|
57
|
+
action as InternalAction<S> & { type: "__PLAYER_JOINED__" }
|
|
58
|
+
).payload;
|
|
59
|
+
return {
|
|
60
|
+
...state,
|
|
61
|
+
players: {
|
|
62
|
+
...state.players,
|
|
63
|
+
[id]: {
|
|
64
|
+
id,
|
|
65
|
+
name,
|
|
66
|
+
avatar,
|
|
67
|
+
isHost: false,
|
|
68
|
+
connected: true,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case InternalActionTypes.PLAYER_LEFT: {
|
|
75
|
+
const { playerId } = (
|
|
76
|
+
action as InternalAction<S> & { type: "__PLAYER_LEFT__" }
|
|
77
|
+
).payload;
|
|
78
|
+
const player = state.players[playerId];
|
|
79
|
+
if (!player) return state;
|
|
80
|
+
return {
|
|
81
|
+
...state,
|
|
82
|
+
players: {
|
|
83
|
+
...state.players,
|
|
84
|
+
[playerId]: { ...player, connected: false },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
case InternalActionTypes.PLAYER_RECONNECTED: {
|
|
90
|
+
const { playerId } = (
|
|
91
|
+
action as InternalAction<S> & { type: "__PLAYER_RECONNECTED__" }
|
|
92
|
+
).payload;
|
|
93
|
+
const player = state.players[playerId];
|
|
94
|
+
if (!player) return state;
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
players: {
|
|
98
|
+
...state.players,
|
|
99
|
+
[playerId]: { ...player, connected: true },
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case InternalActionTypes.PLAYER_REMOVED: {
|
|
105
|
+
const { playerId } = (
|
|
106
|
+
action as InternalAction<S> & { type: "__PLAYER_REMOVED__" }
|
|
107
|
+
).payload;
|
|
108
|
+
if (!state.players[playerId]) return state;
|
|
109
|
+
const { [playerId]: _, ...remainingPlayers } = state.players;
|
|
110
|
+
return {
|
|
111
|
+
...state,
|
|
112
|
+
players: remainingPlayers,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
default:
|
|
117
|
+
return reducer(state, action as A);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const middlewares = options?.middleware;
|
|
122
|
+
|
|
123
|
+
if (!middlewares || middlewares.length === 0) return baseReducer;
|
|
124
|
+
|
|
125
|
+
return applyMiddleware<S, A>(...middlewares)(baseReducer);
|
|
126
|
+
}
|
package/src/sha256.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free SHA-256 implementation.
|
|
3
|
+
*
|
|
4
|
+
* Couch Kit derives public player IDs from a secret using SHA-256. Browsers and
|
|
5
|
+
* Node expose this via the Web Crypto API (`crypto.subtle.digest`), but React
|
|
6
|
+
* Native / Hermes — where the host actually runs — does **not** provide
|
|
7
|
+
* `crypto.subtle` by default. This pure-JS implementation is used as the
|
|
8
|
+
* fallback so the host still produces a proper one-way hash instead of leaking
|
|
9
|
+
* part of the secret. It is byte-for-byte compatible with
|
|
10
|
+
* `crypto.subtle.digest("SHA-256", ...)`, so player IDs are stable regardless
|
|
11
|
+
* of which runtime the host uses.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Round constants (first 32 bits of the fractional parts of the cube roots of the first 64 primes). */
|
|
15
|
+
// prettier-ignore
|
|
16
|
+
const K = new Uint32Array([
|
|
17
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
18
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
19
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
20
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
21
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
22
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
23
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
24
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/** Encode a JS string as UTF-8 bytes without relying on the global `TextEncoder`. */
|
|
28
|
+
function utf8Bytes(str: string): number[] {
|
|
29
|
+
const bytes: number[] = [];
|
|
30
|
+
for (let i = 0; i < str.length; i++) {
|
|
31
|
+
let code = str.charCodeAt(i);
|
|
32
|
+
// Combine surrogate pairs into a single code point.
|
|
33
|
+
if (code >= 0xd800 && code <= 0xdbff && i + 1 < str.length) {
|
|
34
|
+
const next = str.charCodeAt(i + 1);
|
|
35
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
36
|
+
code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00);
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (code < 0x80) {
|
|
41
|
+
bytes.push(code);
|
|
42
|
+
} else if (code < 0x800) {
|
|
43
|
+
bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
|
44
|
+
} else if (code < 0x10000) {
|
|
45
|
+
bytes.push(
|
|
46
|
+
0xe0 | (code >> 12),
|
|
47
|
+
0x80 | ((code >> 6) & 0x3f),
|
|
48
|
+
0x80 | (code & 0x3f),
|
|
49
|
+
);
|
|
50
|
+
} else {
|
|
51
|
+
bytes.push(
|
|
52
|
+
0xf0 | (code >> 18),
|
|
53
|
+
0x80 | ((code >> 12) & 0x3f),
|
|
54
|
+
0x80 | ((code >> 6) & 0x3f),
|
|
55
|
+
0x80 | (code & 0x3f),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return bytes;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rotr = (x: number, n: number): number => (x >>> n) | (x << (32 - n));
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compute the SHA-256 digest of a UTF-8 string.
|
|
66
|
+
*
|
|
67
|
+
* @param message - The string to hash.
|
|
68
|
+
* @returns Lowercase hex-encoded 256-bit digest (64 characters).
|
|
69
|
+
*/
|
|
70
|
+
export function sha256Hex(message: string): string {
|
|
71
|
+
const bytes = utf8Bytes(message);
|
|
72
|
+
const bitLen = bytes.length * 8;
|
|
73
|
+
|
|
74
|
+
// Padding: append 0x80, then zeros, then the 64-bit big-endian length.
|
|
75
|
+
bytes.push(0x80);
|
|
76
|
+
while (bytes.length % 64 !== 56) bytes.push(0);
|
|
77
|
+
// 64-bit length. JS bitwise ops are 32-bit, so split high/low words.
|
|
78
|
+
const hi = Math.floor(bitLen / 0x100000000);
|
|
79
|
+
const lo = bitLen >>> 0;
|
|
80
|
+
bytes.push(
|
|
81
|
+
(hi >>> 24) & 0xff,
|
|
82
|
+
(hi >>> 16) & 0xff,
|
|
83
|
+
(hi >>> 8) & 0xff,
|
|
84
|
+
hi & 0xff,
|
|
85
|
+
(lo >>> 24) & 0xff,
|
|
86
|
+
(lo >>> 16) & 0xff,
|
|
87
|
+
(lo >>> 8) & 0xff,
|
|
88
|
+
lo & 0xff,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Initial hash values (fractional parts of square roots of first 8 primes).
|
|
92
|
+
let h0 = 0x6a09e667,
|
|
93
|
+
h1 = 0xbb67ae85,
|
|
94
|
+
h2 = 0x3c6ef372,
|
|
95
|
+
h3 = 0xa54ff53a,
|
|
96
|
+
h4 = 0x510e527f,
|
|
97
|
+
h5 = 0x9b05688c,
|
|
98
|
+
h6 = 0x1f83d9ab,
|
|
99
|
+
h7 = 0x5be0cd19;
|
|
100
|
+
|
|
101
|
+
const w = new Uint32Array(64);
|
|
102
|
+
|
|
103
|
+
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
104
|
+
for (let i = 0; i < 16; i++) {
|
|
105
|
+
const j = offset + i * 4;
|
|
106
|
+
w[i] =
|
|
107
|
+
((bytes[j] << 24) |
|
|
108
|
+
(bytes[j + 1] << 16) |
|
|
109
|
+
(bytes[j + 2] << 8) |
|
|
110
|
+
bytes[j + 3]) >>>
|
|
111
|
+
0;
|
|
112
|
+
}
|
|
113
|
+
for (let i = 16; i < 64; i++) {
|
|
114
|
+
const s0 =
|
|
115
|
+
rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
|
|
116
|
+
const s1 =
|
|
117
|
+
rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
|
|
118
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let a = h0,
|
|
122
|
+
b = h1,
|
|
123
|
+
c = h2,
|
|
124
|
+
d = h3,
|
|
125
|
+
e = h4,
|
|
126
|
+
f = h5,
|
|
127
|
+
g = h6,
|
|
128
|
+
h = h7;
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < 64; i++) {
|
|
131
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
132
|
+
const ch = (e & f) ^ (~e & g);
|
|
133
|
+
const temp1 = (h + S1 + ch + K[i] + w[i]) >>> 0;
|
|
134
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
135
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
136
|
+
const temp2 = (S0 + maj) >>> 0;
|
|
137
|
+
|
|
138
|
+
h = g;
|
|
139
|
+
g = f;
|
|
140
|
+
f = e;
|
|
141
|
+
e = (d + temp1) >>> 0;
|
|
142
|
+
d = c;
|
|
143
|
+
c = b;
|
|
144
|
+
b = a;
|
|
145
|
+
a = (temp1 + temp2) >>> 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
h0 = (h0 + a) >>> 0;
|
|
149
|
+
h1 = (h1 + b) >>> 0;
|
|
150
|
+
h2 = (h2 + c) >>> 0;
|
|
151
|
+
h3 = (h3 + d) >>> 0;
|
|
152
|
+
h4 = (h4 + e) >>> 0;
|
|
153
|
+
h5 = (h5 + f) >>> 0;
|
|
154
|
+
h6 = (h6 + g) >>> 0;
|
|
155
|
+
h7 = (h7 + h) >>> 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const toHex = (n: number): string => (n >>> 0).toString(16).padStart(8, "0");
|
|
159
|
+
return (
|
|
160
|
+
toHex(h0) +
|
|
161
|
+
toHex(h1) +
|
|
162
|
+
toHex(h2) +
|
|
163
|
+
toHex(h3) +
|
|
164
|
+
toHex(h4) +
|
|
165
|
+
toHex(h5) +
|
|
166
|
+
toHex(h6) +
|
|
167
|
+
toHex(h7)
|
|
168
|
+
);
|
|
169
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { applyMiddleware } from "./middleware";
|
|
2
|
-
import type { Middleware } from "./middleware";
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* Represents a player connected to the game session.
|
|
6
3
|
*
|
|
@@ -78,120 +75,3 @@ export type GameReducer<S extends IGameState, A extends IAction> = (
|
|
|
78
75
|
state: S,
|
|
79
76
|
action: A,
|
|
80
77
|
) => S;
|
|
81
|
-
|
|
82
|
-
/** Options for `createGameReducer`. */
|
|
83
|
-
export interface CreateGameReducerOptions<
|
|
84
|
-
S extends IGameState,
|
|
85
|
-
A extends IAction,
|
|
86
|
-
> {
|
|
87
|
-
/**
|
|
88
|
-
* Optional middleware stack. Middlewares are applied in order — the first
|
|
89
|
-
* middleware in the array is the outermost layer and sees every action first.
|
|
90
|
-
*
|
|
91
|
-
* Middleware can observe **and** transform both user actions and internal
|
|
92
|
-
* actions. Custom middleware should avoid blocking internal actions (types
|
|
93
|
-
* prefixed with `__`) to prevent breaking framework behaviour.
|
|
94
|
-
*/
|
|
95
|
-
middleware?: Middleware<S, A>[];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Wraps a user-provided reducer with automatic handling of internal actions:
|
|
100
|
-
*
|
|
101
|
-
* - `__HYDRATE__` -- Replaces state wholesale (used for server→client state sync).
|
|
102
|
-
* - `__PLAYER_JOINED__` -- Adds a player to `state.players`.
|
|
103
|
-
* - `__PLAYER_LEFT__` -- Marks a player as disconnected in `state.players`.
|
|
104
|
-
* - `__PLAYER_RECONNECTED__` -- Restores a returning player's connection status, preserving all existing data.
|
|
105
|
-
* - `__PLAYER_REMOVED__` -- Permanently removes a timed-out disconnected player from `state.players`.
|
|
106
|
-
*
|
|
107
|
-
* The wrapped reducer accepts both `A` (user actions) and `InternalAction<S>`.
|
|
108
|
-
* User reducers only need to handle their own action types.
|
|
109
|
-
*
|
|
110
|
-
* When `options.middleware` is provided, the middleware stack wraps around the
|
|
111
|
-
* entire reducer (including internal-action handling). Middleware can observe
|
|
112
|
-
* all actions flowing through the reducer.
|
|
113
|
-
*/
|
|
114
|
-
export function createGameReducer<S extends IGameState, A extends IAction>(
|
|
115
|
-
reducer: GameReducer<S, A>,
|
|
116
|
-
options?: CreateGameReducerOptions<S, A>,
|
|
117
|
-
): GameReducer<S, A | InternalAction<S>> {
|
|
118
|
-
const baseReducer: GameReducer<S, A | InternalAction<S>> = (
|
|
119
|
-
state: S,
|
|
120
|
-
action: A | InternalAction<S>,
|
|
121
|
-
): S => {
|
|
122
|
-
switch (action.type) {
|
|
123
|
-
case InternalActionTypes.HYDRATE:
|
|
124
|
-
return (action as InternalAction<S> & { type: "__HYDRATE__" }).payload;
|
|
125
|
-
|
|
126
|
-
case InternalActionTypes.PLAYER_JOINED: {
|
|
127
|
-
const { id, name, avatar } = (
|
|
128
|
-
action as InternalAction<S> & { type: "__PLAYER_JOINED__" }
|
|
129
|
-
).payload;
|
|
130
|
-
return {
|
|
131
|
-
...state,
|
|
132
|
-
players: {
|
|
133
|
-
...state.players,
|
|
134
|
-
[id]: {
|
|
135
|
-
id,
|
|
136
|
-
name,
|
|
137
|
-
avatar,
|
|
138
|
-
isHost: false,
|
|
139
|
-
connected: true,
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
case InternalActionTypes.PLAYER_LEFT: {
|
|
146
|
-
const { playerId } = (
|
|
147
|
-
action as InternalAction<S> & { type: "__PLAYER_LEFT__" }
|
|
148
|
-
).payload;
|
|
149
|
-
const player = state.players[playerId];
|
|
150
|
-
if (!player) return state;
|
|
151
|
-
return {
|
|
152
|
-
...state,
|
|
153
|
-
players: {
|
|
154
|
-
...state.players,
|
|
155
|
-
[playerId]: { ...player, connected: false },
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
case InternalActionTypes.PLAYER_RECONNECTED: {
|
|
161
|
-
const { playerId } = (
|
|
162
|
-
action as InternalAction<S> & { type: "__PLAYER_RECONNECTED__" }
|
|
163
|
-
).payload;
|
|
164
|
-
const player = state.players[playerId];
|
|
165
|
-
if (!player) return state;
|
|
166
|
-
return {
|
|
167
|
-
...state,
|
|
168
|
-
players: {
|
|
169
|
-
...state.players,
|
|
170
|
-
[playerId]: { ...player, connected: true },
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
case InternalActionTypes.PLAYER_REMOVED: {
|
|
176
|
-
const { playerId } = (
|
|
177
|
-
action as InternalAction<S> & { type: "__PLAYER_REMOVED__" }
|
|
178
|
-
).payload;
|
|
179
|
-
if (!state.players[playerId]) return state;
|
|
180
|
-
const { [playerId]: _, ...remainingPlayers } = state.players;
|
|
181
|
-
return {
|
|
182
|
-
...state,
|
|
183
|
-
players: remainingPlayers,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
default:
|
|
188
|
-
return reducer(state, action as A);
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const middlewares = options?.middleware;
|
|
193
|
-
|
|
194
|
-
if (!middlewares || middlewares.length === 0) return baseReducer;
|
|
195
|
-
|
|
196
|
-
return applyMiddleware<S, A>(...middlewares)(baseReducer);
|
|
197
|
-
}
|