@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.
Files changed (174) hide show
  1. package/LICENSE.md +11 -0
  2. package/dist/authentication.js +53 -0
  3. package/dist/authentication.js.map +1 -0
  4. package/dist/config/constants.js +12 -0
  5. package/dist/config/constants.js.map +1 -0
  6. package/dist/config/findFile.js +32 -0
  7. package/dist/config/findFile.js.map +1 -0
  8. package/dist/config/index.js +29 -0
  9. package/dist/config/index.js.map +1 -0
  10. package/dist/config/manage.js +41 -0
  11. package/dist/config/manage.js.map +1 -0
  12. package/dist/cors.js +20 -0
  13. package/dist/cors.js.map +1 -0
  14. package/dist/editor-support/index.js +247 -0
  15. package/dist/editor-support/index.js.map +1 -0
  16. package/dist/handle-room/version-1/file-routes.js +53 -0
  17. package/dist/handle-room/version-1/file-routes.js.map +1 -0
  18. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js +76 -0
  19. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js.map +1 -0
  20. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js +218 -0
  21. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js.map +1 -0
  22. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js +18 -0
  23. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js.map +1 -0
  24. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js +16 -0
  25. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js.map +1 -0
  26. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js +370 -0
  27. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js.map +1 -0
  28. package/dist/handle-room/version-1/globals/FolderDB/index.js +18 -0
  29. package/dist/handle-room/version-1/globals/FolderDB/index.js.map +1 -0
  30. package/dist/handle-room/version-1/globals/FolderDB/types.js +3 -0
  31. package/dist/handle-room/version-1/globals/FolderDB/types.js.map +1 -0
  32. package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js +95 -0
  33. package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js.map +1 -0
  34. package/dist/handle-room/version-1/globals/Room/index.js +18 -0
  35. package/dist/handle-room/version-1/globals/Room/index.js.map +1 -0
  36. package/dist/handle-room/version-1/globals/Room/room.js +3 -0
  37. package/dist/handle-room/version-1/globals/Room/room.js.map +1 -0
  38. package/dist/handle-room/version-1/globals/index.js +20 -0
  39. package/dist/handle-room/version-1/globals/index.js.map +1 -0
  40. package/dist/handle-room/version-1/globals/types.js +3 -0
  41. package/dist/handle-room/version-1/globals/types.js.map +1 -0
  42. package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js +83 -0
  43. package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js.map +1 -0
  44. package/dist/handle-room/version-1/handleDownloads/index.js +35 -0
  45. package/dist/handle-room/version-1/handleDownloads/index.js.map +1 -0
  46. package/dist/handle-room/version-1/handleDownloads/types.js +3 -0
  47. package/dist/handle-room/version-1/handleDownloads/types.js.map +1 -0
  48. package/dist/handle-room/version-1/handleRoomSelections/encryption.js +21 -0
  49. package/dist/handle-room/version-1/handleRoomSelections/encryption.js.map +1 -0
  50. package/dist/handle-room/version-1/handleRoomSelections/random.js +12 -0
  51. package/dist/handle-room/version-1/handleRoomSelections/random.js.map +1 -0
  52. package/dist/handle-room/version-1/index.js +28 -0
  53. package/dist/handle-room/version-1/index.js.map +1 -0
  54. package/dist/handle-room/version-1/piece-sort.js +71 -0
  55. package/dist/handle-room/version-1/piece-sort.js.map +1 -0
  56. package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js +89 -0
  57. package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js.map +1 -0
  58. package/dist/handle-room/version-1/room-handler-bridge/http-handler.js +19 -0
  59. package/dist/handle-room/version-1/room-handler-bridge/http-handler.js.map +1 -0
  60. package/dist/handle-room/version-1/room-handler-bridge/steps.js +170 -0
  61. package/dist/handle-room/version-1/room-handler-bridge/steps.js.map +1 -0
  62. package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js +28 -0
  63. package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js.map +1 -0
  64. package/dist/handle-room/version-1/schema/count.js +16 -0
  65. package/dist/handle-room/version-1/schema/count.js.map +1 -0
  66. package/dist/handle-room/version-1/schema/lock.js +46 -0
  67. package/dist/handle-room/version-1/schema/lock.js.map +1 -0
  68. package/dist/handle-room/version-1/schema/selected.js +13 -0
  69. package/dist/handle-room/version-1/schema/selected.js.map +1 -0
  70. package/dist/handle-room/version-1/select.js +94 -0
  71. package/dist/handle-room/version-1/select.js.map +1 -0
  72. package/dist/index.js +180 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/server.js +88 -0
  75. package/dist/server.js.map +1 -0
  76. package/dist/utils/fs.js +32 -0
  77. package/dist/utils/fs.js.map +1 -0
  78. package/dist/utils/http-router.js +49 -0
  79. package/dist/utils/http-router.js.map +1 -0
  80. package/dist/utils/pagination.js +23 -0
  81. package/dist/utils/pagination.js.map +1 -0
  82. package/dist/utils/websocket-router.js +15 -0
  83. package/dist/utils/websocket-router.js.map +1 -0
  84. package/package.json +66 -0
  85. package/src/authentication.ts +48 -0
  86. package/src/config/constants.ts +7 -0
  87. package/src/config/findFile.ts +34 -0
  88. package/src/config/index.ts +17 -0
  89. package/src/config/manage.ts +48 -0
  90. package/src/cors.ts +23 -0
  91. package/src/editor-support/index.ts +289 -0
  92. package/src/handle-room/version-0/piece-download/availableGamePiece/index.ts +109 -0
  93. package/src/handle-room/version-0/piece-download/availableGamePiece/isUpToDate.ts +78 -0
  94. package/src/handle-room/version-0/piece-download/downloadPieceAssets/downloadSingleAsset.ts +129 -0
  95. package/src/handle-room/version-0/piece-download/downloadPieceAssets/example.ts +116 -0
  96. package/src/handle-room/version-0/piece-download/downloadPieceAssets/index.ts +144 -0
  97. package/src/handle-room/version-0/piece-download/downloadPieceAssets/types.ts +32 -0
  98. package/src/handle-room/version-0/piece-download/downloadPieceAssets/validateAssetFile.ts +54 -0
  99. package/src/handle-room/version-0/piece-download/index.ts +5 -0
  100. package/src/handle-room/version-0/piece-selection/constants.ts +39 -0
  101. package/src/handle-room/version-0/piece-selection/handleRoomSelections.ts +202 -0
  102. package/src/handle-room/version-0/piece-selection/index.ts +31 -0
  103. package/src/handle-room/version-0/piece-selection/roomErrors.ts +25 -0
  104. package/src/handle-room/version-0/piece-selection/steps/encrypton.ts +70 -0
  105. package/src/handle-room/version-0/piece-selection/steps/final-selection.ts +72 -0
  106. package/src/handle-room/version-0/piece-selection/steps/player-selection.ts +100 -0
  107. package/src/handle-room/version-1/constants.ts +44 -0
  108. package/src/handle-room/version-1/file-routes.ts +68 -0
  109. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.ts +83 -0
  110. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.ts +317 -0
  111. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.ts +31 -0
  112. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.ts +13 -0
  113. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.ts +547 -0
  114. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/types.ts +6 -0
  115. package/src/handle-room/version-1/globals/FolderDB/index.ts +3 -0
  116. package/src/handle-room/version-1/globals/FolderDB/types.ts +72 -0
  117. package/src/handle-room/version-1/globals/Room/WebsocketRoom.ts +118 -0
  118. package/src/handle-room/version-1/globals/Room/index.ts +3 -0
  119. package/src/handle-room/version-1/globals/Room/room.ts +8 -0
  120. package/src/handle-room/version-1/globals/index.ts +4 -0
  121. package/src/handle-room/version-1/globals/types.ts +7 -0
  122. package/src/handle-room/version-1/handleDownloads/DownloadTracker.ts +98 -0
  123. package/src/handle-room/version-1/handleDownloads/index.ts +41 -0
  124. package/src/handle-room/version-1/handleDownloads/types.ts +9 -0
  125. package/src/handle-room/version-1/handleRoomSelections/encryption.ts +29 -0
  126. package/src/handle-room/version-1/handleRoomSelections/random.ts +9 -0
  127. package/src/handle-room/version-1/index.ts +33 -0
  128. package/src/handle-room/version-1/piece-sort.ts +96 -0
  129. package/src/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.ts +125 -0
  130. package/src/handle-room/version-1/room-handler-bridge/http-handler.ts +16 -0
  131. package/src/handle-room/version-1/room-handler-bridge/server.pseudo.ts +85 -0
  132. package/src/handle-room/version-1/room-handler-bridge/steps.ts +255 -0
  133. package/src/handle-room/version-1/room-handler-bridge/ws-handler.ts +30 -0
  134. package/src/handle-room/version-1/room-handler-state/RoomState.ts +132 -0
  135. package/src/handle-room/version-1/room-handler-state/bind-to-websocket.ts +93 -0
  136. package/src/handle-room/version-1/room-handler-state/handleRoomSelections.ts +71 -0
  137. package/src/handle-room/version-1/room-handler-state/handleRoomWithErrors.ts +55 -0
  138. package/src/handle-room/version-1/room-handler-state/roomErrors.ts +35 -0
  139. package/src/handle-room/version-1/room-handler-state/user-selection.ts +98 -0
  140. package/src/handle-room/version-1/schema/count.ts +13 -0
  141. package/src/handle-room/version-1/schema/lock.ts +47 -0
  142. package/src/handle-room/version-1/schema/selected.ts +14 -0
  143. package/src/handle-room/version-1/select.ts +130 -0
  144. package/src/handle-room/version-1/validateUnknown.ts +11 -0
  145. package/src/index.old.ts +71 -0
  146. package/src/index.ts +231 -0
  147. package/src/server.ts +85 -0
  148. package/src/utils/fs.ts +43 -0
  149. package/src/utils/http-router.ts +55 -0
  150. package/src/utils/pagination.ts +17 -0
  151. package/src/utils/websocket-router.ts +23 -0
  152. package/tests/integration/editor-pieces-search.test.ts +147 -0
  153. package/tests/integration/game-complete.test.ts +72 -0
  154. package/tests/integration/handle-full-selection.test.ts +143 -0
  155. package/tests/integration/helpers/bridge.ts +22 -0
  156. package/tests/integration/helpers/fakeFolderDB.ts +37 -0
  157. package/tests/integration/helpers/fakePluginRuntime.ts +46 -0
  158. package/tests/integration/helpers/lockConfig.ts +71 -0
  159. package/tests/integration/helpers/make-tar-gz.ts +41 -0
  160. package/tests/integration/helpers/piece.ts +98 -0
  161. package/tests/integration/helpers/plugin-dir.ts +26 -0
  162. package/tests/integration/helpers/room.ts +42 -0
  163. package/tests/integration/helpers/server.ts +51 -0
  164. package/tests/integration/helpers/validLockConfig.ts +80 -0
  165. package/tests/integration/piece-download-and-list.test.ts +194 -0
  166. package/tests/integration/piece-ensure-ws.test.ts +256 -0
  167. package/tests/integration/piece-files.test.ts +139 -0
  168. package/tests/integration/piece-sort-list.test.ts +113 -0
  169. package/tests/integration/protocol.test.ts +129 -0
  170. package/tests/integration/root-and-auth.test.ts +100 -0
  171. package/tests/integration/sync-dl.test.ts +62 -0
  172. package/tests/tsconfig.json +14 -0
  173. package/tsconfig.json +22 -0
  174. 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
+