@roster-lock/match-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +11 -0
- package/dist/authentication.js +53 -0
- package/dist/authentication.js.map +1 -0
- package/dist/config/constants.js +12 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/findFile.js +32 -0
- package/dist/config/findFile.js.map +1 -0
- package/dist/config/index.js +29 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/manage.js +41 -0
- package/dist/config/manage.js.map +1 -0
- package/dist/cors.js +20 -0
- package/dist/cors.js.map +1 -0
- package/dist/editor-support/index.js +247 -0
- package/dist/editor-support/index.js.map +1 -0
- package/dist/handle-room/version-1/file-routes.js +53 -0
- package/dist/handle-room/version-1/file-routes.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js +76 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js +218 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js +18 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js +16 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js +370 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/index.js +18 -0
- package/dist/handle-room/version-1/globals/FolderDB/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/types.js +3 -0
- package/dist/handle-room/version-1/globals/FolderDB/types.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js +95 -0
- package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/index.js +18 -0
- package/dist/handle-room/version-1/globals/Room/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/room.js +3 -0
- package/dist/handle-room/version-1/globals/Room/room.js.map +1 -0
- package/dist/handle-room/version-1/globals/index.js +20 -0
- package/dist/handle-room/version-1/globals/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/types.js +3 -0
- package/dist/handle-room/version-1/globals/types.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js +83 -0
- package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/index.js +35 -0
- package/dist/handle-room/version-1/handleDownloads/index.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/types.js +3 -0
- package/dist/handle-room/version-1/handleDownloads/types.js.map +1 -0
- package/dist/handle-room/version-1/handleRoomSelections/encryption.js +21 -0
- package/dist/handle-room/version-1/handleRoomSelections/encryption.js.map +1 -0
- package/dist/handle-room/version-1/handleRoomSelections/random.js +12 -0
- package/dist/handle-room/version-1/handleRoomSelections/random.js.map +1 -0
- package/dist/handle-room/version-1/index.js +28 -0
- package/dist/handle-room/version-1/index.js.map +1 -0
- package/dist/handle-room/version-1/piece-sort.js +71 -0
- package/dist/handle-room/version-1/piece-sort.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js +89 -0
- package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/http-handler.js +19 -0
- package/dist/handle-room/version-1/room-handler-bridge/http-handler.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/steps.js +170 -0
- package/dist/handle-room/version-1/room-handler-bridge/steps.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js +28 -0
- package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js.map +1 -0
- package/dist/handle-room/version-1/schema/count.js +16 -0
- package/dist/handle-room/version-1/schema/count.js.map +1 -0
- package/dist/handle-room/version-1/schema/lock.js +46 -0
- package/dist/handle-room/version-1/schema/lock.js.map +1 -0
- package/dist/handle-room/version-1/schema/selected.js +13 -0
- package/dist/handle-room/version-1/schema/selected.js.map +1 -0
- package/dist/handle-room/version-1/select.js +94 -0
- package/dist/handle-room/version-1/select.js.map +1 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +1 -0
- package/dist/server.js +88 -0
- package/dist/server.js.map +1 -0
- package/dist/utils/fs.js +32 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/http-router.js +49 -0
- package/dist/utils/http-router.js.map +1 -0
- package/dist/utils/pagination.js +23 -0
- package/dist/utils/pagination.js.map +1 -0
- package/dist/utils/websocket-router.js +15 -0
- package/dist/utils/websocket-router.js.map +1 -0
- package/package.json +66 -0
- package/src/authentication.ts +48 -0
- package/src/config/constants.ts +7 -0
- package/src/config/findFile.ts +34 -0
- package/src/config/index.ts +17 -0
- package/src/config/manage.ts +48 -0
- package/src/cors.ts +23 -0
- package/src/editor-support/index.ts +289 -0
- package/src/handle-room/version-0/piece-download/availableGamePiece/index.ts +109 -0
- package/src/handle-room/version-0/piece-download/availableGamePiece/isUpToDate.ts +78 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/downloadSingleAsset.ts +129 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/example.ts +116 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/index.ts +144 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/types.ts +32 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/validateAssetFile.ts +54 -0
- package/src/handle-room/version-0/piece-download/index.ts +5 -0
- package/src/handle-room/version-0/piece-selection/constants.ts +39 -0
- package/src/handle-room/version-0/piece-selection/handleRoomSelections.ts +202 -0
- package/src/handle-room/version-0/piece-selection/index.ts +31 -0
- package/src/handle-room/version-0/piece-selection/roomErrors.ts +25 -0
- package/src/handle-room/version-0/piece-selection/steps/encrypton.ts +70 -0
- package/src/handle-room/version-0/piece-selection/steps/final-selection.ts +72 -0
- package/src/handle-room/version-0/piece-selection/steps/player-selection.ts +100 -0
- package/src/handle-room/version-1/constants.ts +44 -0
- package/src/handle-room/version-1/file-routes.ts +68 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.ts +83 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.ts +317 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.ts +31 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.ts +13 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.ts +547 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/types.ts +6 -0
- package/src/handle-room/version-1/globals/FolderDB/index.ts +3 -0
- package/src/handle-room/version-1/globals/FolderDB/types.ts +72 -0
- package/src/handle-room/version-1/globals/Room/WebsocketRoom.ts +118 -0
- package/src/handle-room/version-1/globals/Room/index.ts +3 -0
- package/src/handle-room/version-1/globals/Room/room.ts +8 -0
- package/src/handle-room/version-1/globals/index.ts +4 -0
- package/src/handle-room/version-1/globals/types.ts +7 -0
- package/src/handle-room/version-1/handleDownloads/DownloadTracker.ts +98 -0
- package/src/handle-room/version-1/handleDownloads/index.ts +41 -0
- package/src/handle-room/version-1/handleDownloads/types.ts +9 -0
- package/src/handle-room/version-1/handleRoomSelections/encryption.ts +29 -0
- package/src/handle-room/version-1/handleRoomSelections/random.ts +9 -0
- package/src/handle-room/version-1/index.ts +33 -0
- package/src/handle-room/version-1/piece-sort.ts +96 -0
- package/src/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.ts +125 -0
- package/src/handle-room/version-1/room-handler-bridge/http-handler.ts +16 -0
- package/src/handle-room/version-1/room-handler-bridge/server.pseudo.ts +85 -0
- package/src/handle-room/version-1/room-handler-bridge/steps.ts +255 -0
- package/src/handle-room/version-1/room-handler-bridge/ws-handler.ts +30 -0
- package/src/handle-room/version-1/room-handler-state/RoomState.ts +132 -0
- package/src/handle-room/version-1/room-handler-state/bind-to-websocket.ts +93 -0
- package/src/handle-room/version-1/room-handler-state/handleRoomSelections.ts +71 -0
- package/src/handle-room/version-1/room-handler-state/handleRoomWithErrors.ts +55 -0
- package/src/handle-room/version-1/room-handler-state/roomErrors.ts +35 -0
- package/src/handle-room/version-1/room-handler-state/user-selection.ts +98 -0
- package/src/handle-room/version-1/schema/count.ts +13 -0
- package/src/handle-room/version-1/schema/lock.ts +47 -0
- package/src/handle-room/version-1/schema/selected.ts +14 -0
- package/src/handle-room/version-1/select.ts +130 -0
- package/src/handle-room/version-1/validateUnknown.ts +11 -0
- package/src/index.old.ts +71 -0
- package/src/index.ts +231 -0
- package/src/server.ts +85 -0
- package/src/utils/fs.ts +43 -0
- package/src/utils/http-router.ts +55 -0
- package/src/utils/pagination.ts +17 -0
- package/src/utils/websocket-router.ts +23 -0
- package/tests/integration/editor-pieces-search.test.ts +147 -0
- package/tests/integration/game-complete.test.ts +72 -0
- package/tests/integration/handle-full-selection.test.ts +143 -0
- package/tests/integration/helpers/bridge.ts +22 -0
- package/tests/integration/helpers/fakeFolderDB.ts +37 -0
- package/tests/integration/helpers/fakePluginRuntime.ts +46 -0
- package/tests/integration/helpers/lockConfig.ts +71 -0
- package/tests/integration/helpers/make-tar-gz.ts +41 -0
- package/tests/integration/helpers/piece.ts +98 -0
- package/tests/integration/helpers/plugin-dir.ts +26 -0
- package/tests/integration/helpers/room.ts +42 -0
- package/tests/integration/helpers/server.ts +51 -0
- package/tests/integration/helpers/validLockConfig.ts +80 -0
- package/tests/integration/piece-download-and-list.test.ts +194 -0
- package/tests/integration/piece-ensure-ws.test.ts +256 -0
- package/tests/integration/piece-files.test.ts +139 -0
- package/tests/integration/piece-sort-list.test.ts +113 -0
- package/tests/integration/protocol.test.ts +129 -0
- package/tests/integration/root-and-auth.test.ts +100 -0
- package/tests/integration/sync-dl.test.ts +62 -0
- package/tests/tsconfig.json +14 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
|
|
2
|
+
import { JSON_Unknown } from "@match-lock/shared";
|
|
3
|
+
import {
|
|
4
|
+
Object as CastObject,
|
|
5
|
+
String as CastString,
|
|
6
|
+
} from "runtypes";
|
|
7
|
+
|
|
8
|
+
import { webcrypto as crypto } from "crypto";
|
|
9
|
+
// Generate a random AES-GCM key (256-bit)
|
|
10
|
+
|
|
11
|
+
export async function encryptJSON(
|
|
12
|
+
originalValue: JSON_Unknown
|
|
13
|
+
): Promise<{ key: string, encrypted: { iv: string, ciphertext: string } }> {
|
|
14
|
+
const key = await crypto.subtle.generateKey(
|
|
15
|
+
{
|
|
16
|
+
name: "AES-GCM",
|
|
17
|
+
length: 256,
|
|
18
|
+
},
|
|
19
|
+
true, // extractable
|
|
20
|
+
["encrypt", "decrypt"]
|
|
21
|
+
);
|
|
22
|
+
const text = JSON.stringify(originalValue);
|
|
23
|
+
const iv = crypto.getRandomValues(new Uint8Array(12)); // 12-byte IV is recommended for AES-GCM
|
|
24
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
25
|
+
{
|
|
26
|
+
name: "AES-GCM",
|
|
27
|
+
iv,
|
|
28
|
+
},
|
|
29
|
+
key,
|
|
30
|
+
new TextEncoder().encode(text)
|
|
31
|
+
);
|
|
32
|
+
return {
|
|
33
|
+
key: Buffer.from(await crypto.subtle.exportKey("raw", key)).toString("base64"),
|
|
34
|
+
encrypted: {
|
|
35
|
+
iv: Buffer.from(iv).toString("base64"),
|
|
36
|
+
ciphertext: Buffer.from(ciphertext).toString("base64"),
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const CastEncryptedMessage = (
|
|
42
|
+
CastObject({
|
|
43
|
+
iv: CastString,
|
|
44
|
+
ciphertext: CastString,
|
|
45
|
+
}).conform<{ iv: string, ciphertext: string }>()
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export async function decryptJSON(
|
|
49
|
+
keyString: string, { iv: ivString, ciphertext: ciphertextString }: { iv: string, ciphertext: string }
|
|
50
|
+
): Promise<JSON_Unknown> {
|
|
51
|
+
const key = await crypto.subtle.importKey(
|
|
52
|
+
"raw",
|
|
53
|
+
Buffer.from(keyString, "base64").buffer,
|
|
54
|
+
"AES-GCM",
|
|
55
|
+
true,
|
|
56
|
+
["encrypt", "decrypt"]
|
|
57
|
+
);
|
|
58
|
+
const iv = new Uint8Array(Buffer.from(ivString, "base64"));
|
|
59
|
+
const ciphertext = Buffer.from(ciphertextString, "base64").buffer;
|
|
60
|
+
const plaintextBuffer = await crypto.subtle.decrypt(
|
|
61
|
+
{
|
|
62
|
+
name: "AES-GCM",
|
|
63
|
+
iv,
|
|
64
|
+
},
|
|
65
|
+
key,
|
|
66
|
+
ciphertext
|
|
67
|
+
);
|
|
68
|
+
const text = new TextDecoder().decode(plaintextBuffer);
|
|
69
|
+
return JSON.parse(text);
|
|
70
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CollectionId, MatchLockRestrictionConfig, PieceId, UserId } from "@match-lock/shared";
|
|
2
|
+
import { PlayerSelectionWithSeed } from "./player-selection";
|
|
3
|
+
import { runScriptInFork } from "../script-vm";
|
|
4
|
+
|
|
5
|
+
export type FinalSelection = Record<CollectionId, Array<PieceId> | Record<UserId, Array<PieceId>>>;
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Union as CastUnion,
|
|
9
|
+
Array as CastArray,
|
|
10
|
+
Record as CastRecord,
|
|
11
|
+
String as CastString,
|
|
12
|
+
} from "runtypes";
|
|
13
|
+
|
|
14
|
+
const CastGlobalResult = CastArray(CastString);
|
|
15
|
+
const CastPlayerResult = CastRecord(CastString, CastArray(CastString));
|
|
16
|
+
|
|
17
|
+
export const CastFinalSelection = (
|
|
18
|
+
CastRecord(CastString, CastUnion(
|
|
19
|
+
CastGlobalResult,
|
|
20
|
+
CastPlayerResult,
|
|
21
|
+
)).conform<FinalSelection>()
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export async function finalizeSelection(
|
|
25
|
+
restriction: MatchLockRestrictionConfig,
|
|
26
|
+
players: Record<UserId, PlayerSelectionWithSeed>
|
|
27
|
+
): Promise<FinalSelection>{
|
|
28
|
+
const seeds: Record<UserId, string> = {};
|
|
29
|
+
for(const [userId, player] of Object.entries(players)){
|
|
30
|
+
seeds[userId] = player.rngSeed;
|
|
31
|
+
}
|
|
32
|
+
const finalSelection: Record<CollectionId, Array<PieceId> | Record<UserId, Array<PieceId>>> = {};
|
|
33
|
+
await Promise.all(Object.entries(restriction.pieces).map(async ([collectionId, collection])=>{
|
|
34
|
+
if(collection.selectionConfig.type === "mandatory") return;
|
|
35
|
+
if(collection.selectionConfig.type === "on-demand") return;
|
|
36
|
+
if(collection.selectionConfig.type === "agreed") return;
|
|
37
|
+
finalSelection[collectionId] = await handleCollectionSelections(collectionId, collection, players, seeds);
|
|
38
|
+
}));
|
|
39
|
+
return finalSelection;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async function handleCollectionSelections(
|
|
44
|
+
collectionId: string,
|
|
45
|
+
collection: MatchLockRestrictionConfig["pieces"][string],
|
|
46
|
+
players: Record<UserId, PlayerSelectionWithSeed>,
|
|
47
|
+
seeds: Record<UserId, string>
|
|
48
|
+
){
|
|
49
|
+
const pieces: Record<UserId, Array<PieceId>> = {};
|
|
50
|
+
for(const [userId, player] of Object.entries(players)){
|
|
51
|
+
pieces[userId] = player.selection[collectionId];
|
|
52
|
+
}
|
|
53
|
+
if(!("algorithm" in collection.selectionConfig) || !collection.selectionConfig.algorithm){
|
|
54
|
+
return pieces
|
|
55
|
+
}
|
|
56
|
+
const algorithmResult = await runScriptInFork({
|
|
57
|
+
type: collection.selectionConfig.type,
|
|
58
|
+
scriptConfig: collection.selectionConfig.algorithm,
|
|
59
|
+
args: {
|
|
60
|
+
pieceIds: collection.pieces.map(p=>p.id),
|
|
61
|
+
playerChoices: pieces,
|
|
62
|
+
playerSeeds: seeds,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if(collection.selectionConfig.type === "global-choices"){
|
|
66
|
+
return CastGlobalResult.check(algorithmResult);
|
|
67
|
+
}
|
|
68
|
+
if(collection.selectionConfig.type === "player-choices"){
|
|
69
|
+
return CastPlayerResult.check(algorithmResult);
|
|
70
|
+
}
|
|
71
|
+
throw new Error("Invalid Collection Type");
|
|
72
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
|
|
2
|
+
import { CollectionId, JSON_Unknown, RosterLockV1Config, ROSTERLOCK_V1_CASTER_JSONSCHEMA, UserInput, PieceId } from "@match-lock/shared";
|
|
3
|
+
import {
|
|
4
|
+
Object as CastObject,
|
|
5
|
+
Record as CastRecord,
|
|
6
|
+
String as CastString,
|
|
7
|
+
Array as CastArray,
|
|
8
|
+
} from "runtypes";
|
|
9
|
+
import { runScriptInFork } from "../script-vm";
|
|
10
|
+
|
|
11
|
+
export type PlayerSelection = Record<CollectionId, Array<PieceId>>;
|
|
12
|
+
export const CastPlayerSelection = (
|
|
13
|
+
CastRecord(CastString, CastArray(CastString)).conform<PlayerSelection>()
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
export type PlayerSelectionWithSeed = {
|
|
17
|
+
selection: PlayerSelection;
|
|
18
|
+
rngSeed: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CastPlayerSelectionWithSeed = (
|
|
22
|
+
CastObject({
|
|
23
|
+
selection: CastPlayerSelection,
|
|
24
|
+
rngSeed: CastString,
|
|
25
|
+
}).conform<PlayerSelectionWithSeed>()
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export function createRandomSeed(){
|
|
29
|
+
const array = new Uint8Array(16); // 16 bytes = 128 bits = 32 hex characters
|
|
30
|
+
crypto.getRandomValues(array);
|
|
31
|
+
const randHex = Array.from(array)
|
|
32
|
+
.map(b => b.toString(16).padStart(2, "0"))
|
|
33
|
+
.join(""); // 32-character hex string
|
|
34
|
+
return randHex;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function validatePlayerSelection(
|
|
38
|
+
restriction: RosterLockV1Config,
|
|
39
|
+
data: JSON_Unknown
|
|
40
|
+
): Promise<UserInput> {
|
|
41
|
+
const casted: PlayerSelectionWithSeed = CastPlayerSelectionWithSeed.check(data);
|
|
42
|
+
if(casted.rngSeed.length !== 32){
|
|
43
|
+
throw new Error("Invalid RNG Seed");
|
|
44
|
+
}
|
|
45
|
+
await Promise.all(Object.entries(restriction.pieces).map(async ([collectionId, collection])=>{
|
|
46
|
+
return runCollectionValidation(collectionId, collection, casted);
|
|
47
|
+
}));
|
|
48
|
+
return casted;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runCollectionValidation(
|
|
52
|
+
collectionId: string,
|
|
53
|
+
collection: MatchLockRestrictionConfig["pieces"][string],
|
|
54
|
+
casted: PlayerSelectionWithSeed
|
|
55
|
+
){
|
|
56
|
+
if(collection.selectionConfig.type === "mandatory") return;
|
|
57
|
+
if(collection.selectionConfig.type === "on-demand") return;
|
|
58
|
+
if(collection.selectionConfig.type === "agreed") return;
|
|
59
|
+
const pieces = casted.selection[collectionId];
|
|
60
|
+
const validation = collection.selectionConfig.validation;
|
|
61
|
+
if(!pieces){
|
|
62
|
+
throw new Error(`Missing Collection ${collectionId}`);
|
|
63
|
+
}
|
|
64
|
+
if(!validation) return;
|
|
65
|
+
if(validation.unique && new Set(pieces).size !== pieces.length){
|
|
66
|
+
throw new Error(`Duplicate Piece in Collection ${collectionId}`);
|
|
67
|
+
}
|
|
68
|
+
validateCount(collectionId, pieces, validation.count);
|
|
69
|
+
|
|
70
|
+
for(const pieceId of pieces){
|
|
71
|
+
if(!collection.pieces.find(p=>p.id === pieceId)){
|
|
72
|
+
throw new Error(`Piece ${pieceId} not found in collection ${collectionId}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Promise.all(validation.customValidation.map(async (script)=>(
|
|
77
|
+
await runScriptInFork({
|
|
78
|
+
type: "validation",
|
|
79
|
+
scriptConfig: script,
|
|
80
|
+
args: { pieceIds: pieces },
|
|
81
|
+
})
|
|
82
|
+
)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateCount(collectionId: string, pieces: Array<string>, countConfig: number | [number, number]){
|
|
86
|
+
if(!Array.isArray(countConfig)){
|
|
87
|
+
if(pieces.length !== countConfig){
|
|
88
|
+
throw new Error(`Invalid Piece Count in Selection ${collectionId}`);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const [min, max] = countConfig;
|
|
93
|
+
if(pieces.length < min){
|
|
94
|
+
throw new Error(`Not Enough Pieces in Selection ${collectionId}`);
|
|
95
|
+
}
|
|
96
|
+
if(pieces.length > max){
|
|
97
|
+
throw new Error(`Too Many Pieces in Selection ${collectionId}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { JSON_Unknown } from "@roster-lock/utils";
|
|
2
|
+
|
|
3
|
+
export enum ROOM_EVENT {
|
|
4
|
+
roomFull = "room-full",
|
|
5
|
+
userLeft = "user-left",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export enum MATCHLOCK_SELECTION_STATE {
|
|
9
|
+
failure = "failure",
|
|
10
|
+
|
|
11
|
+
hello = "selection-hello",
|
|
12
|
+
lockConfig = "selection-lock",
|
|
13
|
+
selectionEncrypt = "selection-encrypt",
|
|
14
|
+
selectionDecrypt = "selection-decrypt",
|
|
15
|
+
selectionFinal = "selection-final",
|
|
16
|
+
goodbye = "selection-goodbye",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export enum MATCHLOCK_DOWNLOAD_STATE {
|
|
20
|
+
failure = "failure",
|
|
21
|
+
|
|
22
|
+
hello = "download-hello",
|
|
23
|
+
downloadStart = "download-start",
|
|
24
|
+
downloadProgress = "download-progress",
|
|
25
|
+
downloadValidation = "download-validation",
|
|
26
|
+
downloadFinished = "download-finished",
|
|
27
|
+
downloadFailure = "download-failure",
|
|
28
|
+
|
|
29
|
+
downloadAllComplete = "download-all-complete",
|
|
30
|
+
downloadFullFailure = "download-full-fail",
|
|
31
|
+
goodbye = "download-goodbye",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ExternalUserError extends Error {
|
|
35
|
+
constructor(public userId: string, public rawError: JSON_Unknown){
|
|
36
|
+
super("External User Error");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class UserMessegeError extends Error {
|
|
41
|
+
constructor(public userId: string, public message: string){
|
|
42
|
+
super(message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
|
|
2
|
+
import { RosterLockV1Config, RosterLockPiece } from "@roster-lock/types";
|
|
3
|
+
import { jsonBody, HTTPRequestHandler, HTTPError } from "../../utils/http-router";
|
|
4
|
+
import { V1Env } from "./globals/types";
|
|
5
|
+
import z, { ZodType } from "zod";
|
|
6
|
+
|
|
7
|
+
import { engineCaster, pieceFileInfoCaster } from "./schema/lock";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { mimeTypeFor } from "../../utils/fs";
|
|
10
|
+
|
|
11
|
+
type GetPieceInfo = {
|
|
12
|
+
engine: RosterLockV1Config["engine"]
|
|
13
|
+
pieceType: string
|
|
14
|
+
piece: Pick<RosterLockPiece, "version" | "pathVariables">
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getAssetSchema: ZodType<(
|
|
18
|
+
& GetPieceInfo
|
|
19
|
+
& { assetName: string }
|
|
20
|
+
)> = z.object({
|
|
21
|
+
engine: engineCaster,
|
|
22
|
+
pieceType: z.string(),
|
|
23
|
+
piece: pieceFileInfoCaster,
|
|
24
|
+
assetName: z.string(),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export const getFilesOfAsset: HTTPRequestHandler = async function (this: V1Env, { req, res }){
|
|
28
|
+
const { fileDB } = this;
|
|
29
|
+
const body = await jsonBody(req)
|
|
30
|
+
const parsedValue = getAssetSchema.safeParse(body);
|
|
31
|
+
if(!parsedValue.success){
|
|
32
|
+
throw new HTTPError(400, "Bad Form", parsedValue.error);
|
|
33
|
+
}
|
|
34
|
+
const { engine, pieceType, piece, assetName } = parsedValue.data;
|
|
35
|
+
const files = [];
|
|
36
|
+
for await (const filePath of fileDB.getFilesofAsset(
|
|
37
|
+
engine, pieceType, piece, assetName
|
|
38
|
+
)){
|
|
39
|
+
files.push(filePath)
|
|
40
|
+
}
|
|
41
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
42
|
+
res.end(JSON.stringify(files));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const getContentsSchema: ZodType<(
|
|
47
|
+
& GetPieceInfo
|
|
48
|
+
& { filePath: string }
|
|
49
|
+
)> = z.object({
|
|
50
|
+
engine: engineCaster,
|
|
51
|
+
pieceType: z.string(),
|
|
52
|
+
piece: pieceFileInfoCaster,
|
|
53
|
+
filePath: z.string(),
|
|
54
|
+
})
|
|
55
|
+
export const getPieceFileContents: HTTPRequestHandler = async function (this: V1Env, { req, res }){
|
|
56
|
+
const { fileDB } = this;
|
|
57
|
+
const body = await jsonBody(req)
|
|
58
|
+
const parsedValue = getContentsSchema.safeParse(body);
|
|
59
|
+
if(!parsedValue.success){
|
|
60
|
+
throw new HTTPError(400, "Bad Form", parsedValue.error);
|
|
61
|
+
}
|
|
62
|
+
const { engine, pieceType, piece, filePath } = parsedValue.data;
|
|
63
|
+
const stream = await fileDB.getPieceFileContents(
|
|
64
|
+
engine, pieceType, piece, filePath
|
|
65
|
+
);
|
|
66
|
+
res.setHeader("Content-Type", mimeTypeFor(filePath));
|
|
67
|
+
await pipeline(stream, res);
|
|
68
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
|
|
2
|
+
import { ProgressHandlers } from "../../../handleDownloads/types";
|
|
3
|
+
|
|
4
|
+
export class MultiAbortSignal {
|
|
5
|
+
private abortController = new AbortController();
|
|
6
|
+
private handlers = new Map<ProgressHandlers, ()=>void>();
|
|
7
|
+
get abortSignal(){
|
|
8
|
+
return this.abortController.signal;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
constructor(signals: ProgressHandlers[] = []){
|
|
12
|
+
for(const signal of signals){
|
|
13
|
+
this.addSignal(signal);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
emitEvent(
|
|
18
|
+
event: Parameters<ProgressHandlers["onProgress"]>[0]
|
|
19
|
+
){
|
|
20
|
+
for(const handler of this.handlers.keys()){
|
|
21
|
+
try {
|
|
22
|
+
handler.onProgress(event);
|
|
23
|
+
}catch(e){
|
|
24
|
+
console.error("Error Emitting Event", e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
addSignal(newHandler: ProgressHandlers){
|
|
30
|
+
if(this.abortController.signal.aborted) return;
|
|
31
|
+
if(newHandler.abortSignal.aborted) return;
|
|
32
|
+
if(this.handlers.has(newHandler)) return;
|
|
33
|
+
const listener = ()=>{
|
|
34
|
+
this.removeSignal(newHandler);
|
|
35
|
+
if(this.handlers.size === 0) this.abort();
|
|
36
|
+
};
|
|
37
|
+
this.handlers.set(newHandler, listener);
|
|
38
|
+
newHandler.abortSignal.addEventListener("abort", listener);
|
|
39
|
+
}
|
|
40
|
+
removeSignal(oldHandler: ProgressHandlers){
|
|
41
|
+
const listener = this.handlers.get(oldHandler);
|
|
42
|
+
if(!listener) return;
|
|
43
|
+
oldHandler.abortSignal.removeEventListener("abort", listener);
|
|
44
|
+
this.handlers.delete(oldHandler);
|
|
45
|
+
}
|
|
46
|
+
clear(){
|
|
47
|
+
for(const [handler, listener] of this.handlers.entries()) {
|
|
48
|
+
handler.abortSignal.removeEventListener("abort", listener);
|
|
49
|
+
}
|
|
50
|
+
this.handlers.clear();
|
|
51
|
+
}
|
|
52
|
+
abort(){
|
|
53
|
+
this.clear();
|
|
54
|
+
this.abortController.abort();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function raceWithAbort<T>(
|
|
59
|
+
promise: Promise<T>,
|
|
60
|
+
abortSignal: AbortSignal
|
|
61
|
+
): Promise<T> {
|
|
62
|
+
if(abortSignal.aborted) throw new Error('Download aborted by caller');
|
|
63
|
+
|
|
64
|
+
const { promise: abortPromise, reject } = Promise.withResolvers<T>();
|
|
65
|
+
|
|
66
|
+
const listener = ()=>{
|
|
67
|
+
reject(new Error('Download aborted by caller'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
abortSignal.addEventListener("abort", listener, { once: true });
|
|
71
|
+
|
|
72
|
+
// promise is also awaited via Promise.race below; catch here too so this
|
|
73
|
+
// second attached continuation doesn't count as an unhandled rejection.
|
|
74
|
+
promise.finally(()=>{
|
|
75
|
+
abortSignal.removeEventListener("abort", listener);
|
|
76
|
+
}).catch(()=>{});
|
|
77
|
+
|
|
78
|
+
return Promise.race([
|
|
79
|
+
promise,
|
|
80
|
+
abortPromise
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|