@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,116 @@
1
+ import { MatchLockPublishPiece } from "@match-lock/shared";
2
+ import { downloadPieceAssets } from "./index";
3
+
4
+ /**
5
+ * Example usage of the new asset-based download system
6
+ */
7
+ export async function exampleDownload() {
8
+ // Example piece with individual assets
9
+ const examplePiece: MatchLockPublishPiece = {
10
+ name: "example-character",
11
+ author: "game-dev",
12
+ published: "2024-01-01T00:00:00Z",
13
+ sha256: "abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234",
14
+ signature: "example-signature",
15
+ signatureVerificationUrl: "https://example.com/verify",
16
+ preview: {
17
+ image: "https://example.com/preview.jpg",
18
+ url: "https://example.com/preview"
19
+ },
20
+ sources: [
21
+ {
22
+ type: "http",
23
+ url: "https://example.com/assets/"
24
+ }
25
+ ],
26
+ pieceDefinition: "character",
27
+ version: {
28
+ logic: "logic-sha256-hash-here",
29
+ media: "media-sha256-hash-here"
30
+ },
31
+ assets: {
32
+ "character.json": {
33
+ assetType: "json",
34
+ sha256: "file1-sha256-hash-here",
35
+ sizeBytes: 1024
36
+ },
37
+ "sprites/idle.png": {
38
+ assetType: "png",
39
+ sha256: "file2-sha256-hash-here",
40
+ sizeBytes: 2048
41
+ },
42
+ "sounds/jump.wav": {
43
+ assetType: "wav",
44
+ sha256: "file3-sha256-hash-here",
45
+ sizeBytes: 4096
46
+ }
47
+ }
48
+ };
49
+
50
+ const targetDirectory = "./downloaded-pieces";
51
+
52
+ try {
53
+ const pieceFolder = await downloadPieceAssets(
54
+ examplePiece,
55
+ targetDirectory,
56
+ (progress) => {
57
+ console.log(`Download progress for ${progress.pieceId}:`);
58
+ console.log(` Completed: ${progress.completedAssets}/${progress.totalAssets}`);
59
+ if (progress.currentAsset) {
60
+ console.log(` Current: ${progress.currentAsset.filePath} (${Math.round(progress.currentAsset.progress * 100)}%)`);
61
+ }
62
+ }
63
+ );
64
+
65
+ console.log(`Successfully downloaded piece to: ${pieceFolder}`);
66
+
67
+ // The downloaded structure will be:
68
+ // ./downloaded-pieces/game-dev/example-character-logic-sh/
69
+ // ├── character.json
70
+ // ├── sprites/
71
+ // │ └── idle.png
72
+ // ├── sounds/
73
+ // │ └── jump.wav
74
+ // └── piece.matchlock.json (metadata)
75
+
76
+ return pieceFolder;
77
+
78
+ } catch (error) {
79
+ console.error("Download failed:", error);
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Example of downloading a single asset
86
+ */
87
+ export async function exampleSingleAssetDownload() {
88
+ const { downloadSingleAsset } = await import("./downloadSingleAsset");
89
+
90
+ const examplePiece: MatchLockPublishPiece = {
91
+ // ... same as above
92
+ } as any;
93
+
94
+ try {
95
+ const result = await downloadSingleAsset({
96
+ piece: examplePiece,
97
+ filePath: "character.json",
98
+ asset: {
99
+ assetType: "json",
100
+ sha256: "file1-sha256-hash-here",
101
+ sizeBytes: 1024
102
+ },
103
+ targetDirectory: "./single-asset-test",
104
+ onProgress: (progress) => {
105
+ console.log(`Downloading ${progress.filePath}: ${Math.round(progress.progress * 100)}%`);
106
+ }
107
+ });
108
+
109
+ console.log(`Downloaded ${result.filePath} to ${result.localPath}`);
110
+ return result;
111
+
112
+ } catch (error) {
113
+ console.error("Single asset download failed:", error);
114
+ throw error;
115
+ }
116
+ }
@@ -0,0 +1,144 @@
1
+ import { join as pathJoin } from "node:path";
2
+ import { mkdir as fsMkdir, writeFile as fsWriteFile } from "node:fs/promises";
3
+
4
+ import { MatchLockPublishPiece } from "@match-lock/shared";
5
+ import { downloadSingleAsset } from "./downloadSingleAsset";
6
+ import { validateAssetFile } from "./validateAsset";
7
+ import {
8
+ ProgressListener,
9
+ PieceDownloadProgress,
10
+ AssetDownloadResult
11
+ } from "./types";
12
+
13
+ export async function downloadPieceAssets(
14
+ piece: MatchLockPublishPiece,
15
+ targetDirectory: string,
16
+ onProgress?: ProgressListener
17
+ ): Promise<string> {
18
+ // Create piece-specific directory using author/name-logicSha pattern
19
+ const logicShaShort = piece.version.logic.substring(0, 8);
20
+ const pieceFolder = pathJoin(targetDirectory, piece.author, `${piece.name}-${logicShaShort}`);
21
+
22
+ // Ensure piece directory exists
23
+ await fsMkdir(pieceFolder, { recursive: true });
24
+
25
+ // Check if piece is already downloaded and up to date
26
+ if (await isPieceUpToDate(piece, pieceFolder)) {
27
+ return pieceFolder;
28
+ }
29
+
30
+ const assetEntries = Object.entries(piece.assets);
31
+ const totalAssets = assetEntries.length;
32
+ let completedAssets = 0;
33
+ const downloadResults: AssetDownloadResult[] = [];
34
+
35
+ // Download each asset
36
+ for (const [filePath, asset] of assetEntries) {
37
+ const targetFilePath = pathJoin(pieceFolder, filePath);
38
+
39
+ // Check if this specific asset is already downloaded and valid
40
+ if (await validateAssetFile(targetFilePath, asset)) {
41
+ completedAssets++;
42
+ downloadResults.push({
43
+ filePath,
44
+ localPath: targetFilePath,
45
+ asset
46
+ });
47
+
48
+ // Report progress
49
+ if (onProgress) {
50
+ onProgress({
51
+ pieceId: piece.name,
52
+ completedAssets,
53
+ totalAssets
54
+ });
55
+ }
56
+ continue;
57
+ }
58
+
59
+ // Download the asset
60
+ try {
61
+ const result = await downloadSingleAsset({
62
+ piece,
63
+ filePath,
64
+ asset,
65
+ targetDirectory: pieceFolder,
66
+ onProgress: (assetProgress) => {
67
+ if (onProgress) {
68
+ onProgress({
69
+ pieceId: piece.name,
70
+ completedAssets,
71
+ totalAssets,
72
+ currentAsset: assetProgress
73
+ });
74
+ }
75
+ }
76
+ });
77
+
78
+ downloadResults.push(result);
79
+ completedAssets++;
80
+
81
+ // Report completion of this asset
82
+ if (onProgress) {
83
+ onProgress({
84
+ pieceId: piece.name,
85
+ completedAssets,
86
+ totalAssets
87
+ });
88
+ }
89
+
90
+ } catch (error) {
91
+ throw new Error(`Failed to download piece ${piece.name}: ${error}`);
92
+ }
93
+ }
94
+
95
+ // Create piece metadata file
96
+ await createPieceMetadata(piece, pieceFolder, downloadResults);
97
+
98
+ return pieceFolder;
99
+ }
100
+
101
+ async function isPieceUpToDate(
102
+ piece: MatchLockPublishPiece,
103
+ pieceFolder: string
104
+ ): Promise<boolean> {
105
+ try {
106
+ // Check if all assets exist and are valid
107
+ for (const [filePath, asset] of Object.entries(piece.assets)) {
108
+ const targetFilePath = pathJoin(pieceFolder, filePath);
109
+ if (!(await validateAssetFile(targetFilePath, asset))) {
110
+ return false;
111
+ }
112
+ }
113
+ return true;
114
+ } catch (error) {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ async function createPieceMetadata(
120
+ piece: MatchLockPublishPiece,
121
+ pieceFolder: string,
122
+ downloadResults: AssetDownloadResult[]
123
+ ): Promise<void> {
124
+ const metadata = {
125
+ pieceId: piece.name,
126
+ author: piece.author,
127
+ version: piece.version,
128
+ downloadedAt: new Date().toISOString(),
129
+ assets: downloadResults.map(result => ({
130
+ filePath: result.filePath,
131
+ localPath: result.localPath,
132
+ sha256: result.asset.sha256,
133
+ sizeBytes: result.asset.sizeBytes,
134
+ assetType: result.asset.assetType
135
+ }))
136
+ };
137
+
138
+ const metadataPath = pathJoin(pieceFolder, "piece.matchlock.json");
139
+ await fsWriteFile(metadataPath, JSON.stringify(metadata, null, 2), "utf8");
140
+ }
141
+
142
+ export * from "./types";
143
+ export { downloadSingleAsset } from "./downloadSingleAsset";
144
+ export { validateAssetFile } from "./validateAsset";
@@ -0,0 +1,32 @@
1
+ import { MatchLockPublishPiece, MatchLockAsset } from "@match-lock/shared";
2
+
3
+ export type AssetDownloadProgress = {
4
+ pieceId: string;
5
+ filePath: string;
6
+ bytesDownloaded: number;
7
+ totalBytes: number;
8
+ progress: number; // 0-1
9
+ };
10
+
11
+ export type AssetDownloadResult = {
12
+ filePath: string;
13
+ localPath: string;
14
+ asset: MatchLockAsset;
15
+ };
16
+
17
+ export type PieceDownloadProgress = {
18
+ pieceId: string;
19
+ completedAssets: number;
20
+ totalAssets: number;
21
+ currentAsset?: AssetDownloadProgress;
22
+ };
23
+
24
+ export type ProgressListener = (progress: PieceDownloadProgress) => void;
25
+
26
+ export type AssetDownloadOptions = {
27
+ piece: MatchLockPublishPiece;
28
+ filePath: string;
29
+ asset: MatchLockAsset;
30
+ targetDirectory: string;
31
+ onProgress?: (progress: AssetDownloadProgress) => void;
32
+ };
@@ -0,0 +1,54 @@
1
+ import { createHash } from "node:crypto";
2
+ import { stat as fsStat, readFile as fsReadFile } from "node:fs/promises";
3
+ import { MatchLockAsset } from "@match-lock/shared";
4
+
5
+
6
+ export async function validateAssetFile(
7
+ filePath: string,
8
+ expectedAsset: MatchLockAsset
9
+ ): Promise<boolean> {
10
+ try {
11
+ const stats = await fsStat(filePath);
12
+
13
+ // Check file size
14
+ if (stats.size !== expectedAsset.sizeBytes) {
15
+ return false;
16
+ }
17
+
18
+ // Check SHA256 hash
19
+ const fileBuffer = await fsReadFile(filePath);
20
+ const hash = createHash("sha256").update(fileBuffer).digest("hex");
21
+
22
+ return hash === expectedAsset.sha256;
23
+ } catch (error) {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ export async function calculateFileHash(filePath: string): Promise<string> {
29
+ const fileBuffer = await fsReadFile(filePath);
30
+ return createHash("sha256").update(fileBuffer).digest("hex");
31
+ }
32
+
33
+ export async function getFileSize(filePath: string): Promise<number> {
34
+ const stats = await fsStat(filePath);
35
+ return stats.size;
36
+ }
37
+
38
+ export function validateAssetIntegrity(
39
+ actualSize: number,
40
+ actualHash: string,
41
+ expectedAsset: MatchLockAsset
42
+ ): void {
43
+ if (actualSize !== expectedAsset.sizeBytes) {
44
+ throw new Error(
45
+ `Asset size mismatch: expected ${expectedAsset.sizeBytes} bytes, got ${actualSize} bytes`
46
+ );
47
+ }
48
+
49
+ if (actualHash !== expectedAsset.sha256) {
50
+ throw new Error(
51
+ `Asset hash mismatch: expected ${expectedAsset.sha256}, got ${actualHash}`
52
+ );
53
+ }
54
+ }
@@ -0,0 +1,5 @@
1
+ // Export the new asset-based download system
2
+ export * from "./downloadPieceAssets";
3
+
4
+ // Export the legacy bundle-based download system for compatibility
5
+ export * from "./availableGamePiece";
@@ -0,0 +1,39 @@
1
+ import { JSON_Unknown } from "@match-lock/shared";
2
+
3
+ export enum MATCHLOCK_SELECTION_STATE {
4
+ failure = "failure",
5
+
6
+ hello = "hello",
7
+ restriction = "restriction",
8
+ selectionEncrypt = "selection-encrypt",
9
+ selectionDecrypt = "selection-decrypt",
10
+ selectionFinal = "selection-final",
11
+ goodbye = "goodbye",
12
+ }
13
+
14
+ export enum MATCHLOCK_DOWNLOAD_STATE {
15
+ failure = "failure",
16
+
17
+ hello = "hello",
18
+ selectionFinal = "selection-final",
19
+ downloadStart = "download-start",
20
+ downloadProgress = "download-progress",
21
+ downloadValidation = "download-validation",
22
+ downloadFinished = "download-finished",
23
+
24
+ downloadFailure = "download-failure",
25
+ downloadAllComplete = "download-all-complete",
26
+ goodbye = "goodbye",
27
+ }
28
+
29
+ export class ExternalUserError extends Error {
30
+ constructor(public userId: string, public rawError: JSON_Unknown){
31
+ super("External User Error");
32
+ }
33
+ }
34
+
35
+ export class UserMessegeError extends Error {
36
+ constructor(public userId: string, public message: string){
37
+ super(message);
38
+ }
39
+ }
@@ -0,0 +1,202 @@
1
+ import { JSON_Unknown, RosterLockV1Config, UserInput } from "@match-lock/shared";
2
+ import { MATCHLOCK_SELECTION_STATE, UserMessegeError } from "./constants";
3
+ import {
4
+ encryptJSON, decryptJSON,
5
+ CastEncryptedMessage,
6
+ } from "./steps/encrypton";
7
+ import {
8
+ validatePlayerSelection, createRandomSeed,
9
+ PlayerSelectionWithSeed
10
+ } from "./steps/player-selection";
11
+ import {
12
+ finalizeSelection,
13
+ FinalSelection,
14
+ CastFinalSelection
15
+ } from "./steps/final-selection";
16
+ import { Room } from "../../../types/room";
17
+ import { deepEqual } from "node:assert";
18
+
19
+ const STATE_ORDER = [
20
+ MATCHLOCK_SELECTION_STATE.hello,
21
+ MATCHLOCK_SELECTION_STATE.restriction,
22
+ MATCHLOCK_SELECTION_STATE.selectionEncrypt,
23
+ MATCHLOCK_SELECTION_STATE.selectionDecrypt,
24
+ MATCHLOCK_SELECTION_STATE.selectionFinal,
25
+ MATCHLOCK_SELECTION_STATE.goodbye,
26
+ ];
27
+
28
+ export async function handleRoomSelections(
29
+ room: Room,
30
+ abortSignal: AbortSignal,
31
+ restriction: RosterLockV1Config,
32
+ selection: UserInput["userSelection"],
33
+ ){
34
+
35
+ const rngSeed = createRandomSeed();
36
+ const [ownEncrypted] = await Promise.all([
37
+ // Prepare own Selections
38
+ encryptJSON({ selection, rngSeed }),
39
+ // Validate our own selection
40
+ validatePlayerSelection(restriction, { selection, rngSeed })
41
+ ])
42
+
43
+ if(abortSignal.aborted){
44
+ return Promise.reject(new Error("Aborted"));
45
+ }
46
+
47
+ const roomState = new RoomState(
48
+ room, restriction, ownEncrypted
49
+ );
50
+
51
+ const { promise, resolve, reject } = Promise.withResolvers<FinalSelection>();
52
+
53
+ // Send our "hello" after we start listening
54
+ Promise.resolve().then(async ()=>{
55
+ room.broadcast(MATCHLOCK_SELECTION_STATE.hello, "hello");
56
+ });
57
+ const stopListeningToRoom = room.listen(async (userId, event, data)=>{
58
+ try {
59
+ if(!(event in roomState.stateHandlers )){
60
+ return;
61
+ }
62
+ // ensure the message wasn't sent twice and event is in order
63
+ const count = roomState.validateMessageState(userId, event as MATCHLOCK_SELECTION_STATE);
64
+ const { message, finished } = roomState.stateHandlers[event];
65
+ // If there is a message handler, run it
66
+ await message?.(userId, data);
67
+ // If we haven't heard from everyone, exit
68
+ if(count < room.numberOfUsers) return;
69
+ // Update the room step
70
+ roomState.updateStep();
71
+ // Run the finished handler which includes broadcasting the next step
72
+ await finished();
73
+
74
+ // If we're in the goodbye state, we're done
75
+ if(event !== MATCHLOCK_SELECTION_STATE.goodbye) return;
76
+ if(!roomState.agreedSelection) throw new Error("Missing Agreed FInal Selection");
77
+ resolve(roomState.agreedSelection!);
78
+ }catch(e){
79
+ reject(e);
80
+ }
81
+ });
82
+
83
+ promise.finally(()=>{
84
+ stopListeningToRoom();
85
+ });
86
+
87
+ abortSignal.addEventListener("abort", ()=>(
88
+ reject(new Error("Aborted"))
89
+ ));
90
+
91
+ return promise;
92
+
93
+ }
94
+
95
+ class RoomState {
96
+ private state: MATCHLOCK_SELECTION_STATE = MATCHLOCK_SELECTION_STATE.hello;
97
+ private recievedMessages = new Map<MATCHLOCK_SELECTION_STATE, Set<string>>();
98
+
99
+ encryptedData = new Map<string, { iv: string, ciphertext: string }>();
100
+ userSelections = new Map<string, PlayerSelectionWithSeed>();
101
+ agreedSelection: FinalSelection | null = null;
102
+
103
+ constructor(
104
+ public room: Room,
105
+ public restriction: RosterLockV1Config,
106
+ public ownEncrypted: Awaited<ReturnType<typeof encryptJSON>>,
107
+ ){}
108
+
109
+ validateMessageState(
110
+ userId: string,
111
+ messageState: MATCHLOCK_SELECTION_STATE
112
+ ){
113
+ if(this.state !== messageState){
114
+ const currentIndex = STATE_ORDER.indexOf(this.state);
115
+ const messageIndex = STATE_ORDER.indexOf(messageState);
116
+ if(messageIndex === currentIndex + 1){
117
+ console.warn(`User Sent ${messageState} Early`, userId);
118
+ console.warn("We'll still process it as other messages may just be late");
119
+ } else {
120
+ throw new UserMessegeError(userId, `Expected ${this.state} message`);
121
+ }
122
+ }
123
+ if(!this.recievedMessages.has(messageState)){
124
+ this.recievedMessages.set(messageState, new Set());
125
+ }
126
+ if(this.recievedMessages.get(messageState)!.has(userId)){
127
+ throw new UserMessegeError(userId, "User Sent Message Twice");
128
+ }
129
+ this.recievedMessages.get(messageState)!.add(userId);
130
+ return this.recievedMessages.get(messageState)!.size;
131
+ }
132
+
133
+ updateStep(){
134
+ const currentIndex = STATE_ORDER.indexOf(this.state);
135
+ const nextIndex = currentIndex + 1;
136
+ if (nextIndex >= STATE_ORDER.length) {
137
+ throw new Error("No next phase after " + this.state);
138
+ }
139
+ this.state = STATE_ORDER[nextIndex];
140
+ }
141
+
142
+ stateHandlers: Record<string, {
143
+ message?(userId: string, data: JSON_Unknown):any,
144
+ finished():any
145
+ }> = {
146
+ [MATCHLOCK_SELECTION_STATE.hello]: {
147
+ finished: ()=>(
148
+ this.room.broadcast(MATCHLOCK_SELECTION_STATE.restriction, this.restriction)
149
+ ),
150
+ },
151
+ [MATCHLOCK_SELECTION_STATE.restriction]: {
152
+ message: (userId, data)=>{
153
+ deepEqual(data, this.restriction, "Restriction Mismatch")
154
+ },
155
+ finished: ()=>(
156
+ this.room.broadcast(MATCHLOCK_SELECTION_STATE.selectionEncrypt, this.ownEncrypted.encrypted)
157
+ )
158
+ },
159
+ [MATCHLOCK_SELECTION_STATE.selectionEncrypt]: {
160
+ message: (userId, data)=>{
161
+ const casted = CastEncryptedMessage.check(data);
162
+ this.encryptedData.set(userId, casted);
163
+ },
164
+ finished: ()=>(
165
+ this.room.broadcast(MATCHLOCK_SELECTION_STATE.selectionDecrypt, this.ownEncrypted.key)
166
+ )
167
+ },
168
+ [MATCHLOCK_SELECTION_STATE.selectionDecrypt]: {
169
+ message: async (userId, data)=>{
170
+ if(typeof data !== "string") throw new Error("Invalid Decryption Key");
171
+ const encrypted = this.encryptedData.get(userId);
172
+ if(!encrypted) throw new Error("Missing Encrypted Data");
173
+ const decrypted = await decryptJSON(data, encrypted);
174
+ const casted = await validatePlayerSelection(this.restriction, decrypted);
175
+ this.userSelections.set(userId, casted);
176
+ },
177
+ finished: async ()=>{
178
+ const ownFinalSelection = await finalizeSelection(
179
+ this.restriction, Object.fromEntries(this.userSelections)
180
+ );
181
+ this.room.broadcast(MATCHLOCK_SELECTION_STATE.selectionFinal, ownFinalSelection)
182
+ }
183
+ },
184
+ [MATCHLOCK_SELECTION_STATE.selectionFinal]: {
185
+ message: (userId, data)=>{
186
+ const casted: FinalSelection = CastFinalSelection.check(data);
187
+ if(this.agreedSelection === null){
188
+ this.agreedSelection = casted;
189
+ } else {
190
+ deepEqual(this.agreedSelection, casted, "Final Selection Mismatch");
191
+ }
192
+ },
193
+ finished: ()=>(
194
+ this.room.broadcast(MATCHLOCK_SELECTION_STATE.goodbye, "goodbye")
195
+ )
196
+ },
197
+ [MATCHLOCK_SELECTION_STATE.goodbye]: {
198
+ finished: ()=>{},
199
+ }
200
+ }
201
+ }
202
+
@@ -0,0 +1,31 @@
1
+ import { RosterLockV1Config, UserInput } from "@match-lock/shared";
2
+
3
+ import { Room } from "../../../types/room";
4
+ import { waitForExternalError, waitForStoppedRoomHeartbeat } from "./roomErrors";
5
+ import { handleRoomSelections } from "./handleRoomSelections";
6
+ import { ExternalUserError, MATCHLOCK_SELECTION_STATE } from "./constants";
7
+
8
+ export async function handleSelection(
9
+ room: Room,
10
+ lockConfig: RosterLockV1Config,
11
+ selection: UserInput["userSelection"]
12
+ ){
13
+ const abortController = new AbortController();
14
+ try {
15
+ await Promise.race([
16
+ handleRoomSelections(room, abortController.signal, lockConfig, selection),
17
+ waitForExternalError(room, abortController.signal),
18
+ waitForStoppedRoomHeartbeat(room, 10_000, abortController.signal),
19
+ ])
20
+ }catch(e){
21
+ console.log("Match To Start Failed", e);
22
+ if(!(e instanceof ExternalUserError) && e instanceof Error){
23
+ room.broadcast(MATCHLOCK_SELECTION_STATE.failure, e.message);
24
+ }
25
+ room.disconnect();
26
+ throw e;
27
+ }finally{
28
+ abortController.abort();
29
+ }
30
+ }
31
+
@@ -0,0 +1,25 @@
1
+
2
+ import { Room } from "../../../types/room";
3
+ import { ExternalUserError, MATCHLOCK_SELECTION_STATE } from "./constants";
4
+
5
+ import { waitForEvent, waitForEventTimeout } from "@match-lock/shared";
6
+ export async function waitForExternalError(room: Room, abortSignal: AbortSignal){
7
+ const [userId, event, message] = await waitForEvent(
8
+ room.listen.filter((userId, event, message)=>{
9
+ return event === MATCHLOCK_SELECTION_STATE.failure;
10
+ }),
11
+ abortSignal
12
+ )
13
+ throw new ExternalUserError(userId, message);
14
+ }
15
+
16
+ export async function waitForStoppedRoomHeartbeat(room: Room, timeout: number, abortSignal: AbortSignal){
17
+ await waitForEventTimeout(
18
+ room.listen.filter((userId, event, message)=>{
19
+ return event !== MATCHLOCK_SELECTION_STATE.failure;
20
+ }),
21
+ timeout,
22
+ abortSignal
23
+ );
24
+ throw new Error("Heartbeat Timeout");
25
+ }