auramaxx 0.1.0 → 0.1.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/README.md
CHANGED
|
@@ -27,15 +27,34 @@ npx auramaxx
|
|
|
27
27
|
Inside a scaffolded game, use the shorter `auramaxx` alias or the generated
|
|
28
28
|
`npm run ...` scripts for local engine commands.
|
|
29
29
|
|
|
30
|
-
Play an
|
|
30
|
+
## Play an Example
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
|
|
33
|
+
npm install -g auramaxx
|
|
34
|
+
auramaxx play auracraft
|
|
34
35
|
|
|
35
36
|
# or run a published game wrapper directly
|
|
36
|
-
npx
|
|
37
|
+
npx auracraft play
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Fork a Game
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g auramaxx
|
|
44
|
+
auramaxx fork auramon
|
|
37
45
|
```
|
|
38
46
|
|
|
47
|
+
## Working With AI Agent
|
|
48
|
+
|
|
49
|
+
Inside your codebase:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd <your-codebase>
|
|
53
|
+
npx -y skills add Aura-Industry/auramaxx
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then use <https://www.aurajs.gg/docs> for engine docs and API references.
|
|
57
|
+
|
|
39
58
|
## Multiplayer TL;DR
|
|
40
59
|
|
|
41
60
|
```bash
|
|
@@ -52,7 +71,7 @@ Keep that same flow and add internet-backed rooms with either:
|
|
|
52
71
|
- `aura.config.json -> multiplayer.relay = "relay.aurajs.gg"`
|
|
53
72
|
- or `AURA_MULTIPLAYER_RELAY_HOST=relay.aurajs.gg`
|
|
54
73
|
|
|
55
|
-
Use [
|
|
74
|
+
Use [Multiplayer Quickstart](https://www.aurajs.gg/docs/multiplayer-quickstart) for the short multiplayer doc, or [Multiplayer Party Example](./packages/aurascript/examples/multiplayer-party/README.md) for the same-project example with diagnostics and room chat.
|
|
56
75
|
|
|
57
76
|
## Feature Set
|
|
58
77
|
|
|
@@ -80,13 +99,13 @@ Use [`packages/aurascript/docs/external/game-dev-api/multiplayer-quickstart.md`]
|
|
|
80
99
|
|
|
81
100
|
## Docs
|
|
82
101
|
|
|
83
|
-
- [
|
|
84
|
-
- [
|
|
85
|
-
- [
|
|
86
|
-
- [
|
|
87
|
-
- [
|
|
88
|
-
- [
|
|
89
|
-
- [
|
|
102
|
+
- [AuraJS README](https://www.aurajs.gg/docs) - AuraJS overview and public docs entrypoint
|
|
103
|
+
- [Game Developer Handbook](https://www.aurajs.gg/docs/handbook) - game developer handbook
|
|
104
|
+
- [Multiplayer Quickstart](https://www.aurajs.gg/docs/multiplayer-quickstart) - shortest multiplayer setup path
|
|
105
|
+
- [Core API Contract](https://www.aurajs.gg/docs/api-contract) - frozen core API contract
|
|
106
|
+
- [3D API Contract](https://www.aurajs.gg/docs/api-contract-3d) - frozen 3D API contract
|
|
107
|
+
- [Exact Reference Index](https://www.aurajs.gg/docs/reference) - exact reference index
|
|
108
|
+
- [Combined Public Reference](https://www.aurajs.gg/docs/reference) - combined public reference
|
|
90
109
|
|
|
91
110
|
Live docs:
|
|
92
111
|
|
package/bin/auramaxx.js
CHANGED
|
@@ -48,7 +48,7 @@ const COMMANDS = {
|
|
|
48
48
|
'shell-hook': 'Auto-load .aura env vars on cd (like direnv)',
|
|
49
49
|
play: 'Play an AuraJS game from npm (auramaxx play <game>)',
|
|
50
50
|
fork: 'Fork a published AuraJS game into a local editable project (auramaxx fork <game>)',
|
|
51
|
-
create: 'Scaffold a new AuraJS game (2d/3d/
|
|
51
|
+
create: 'Scaffold a new AuraJS game (2d/3d/multiplayer) via AuraJS CLI',
|
|
52
52
|
make: 'Generate authored AuraJS project files via AuraJS CLI',
|
|
53
53
|
publish: 'Publish current AuraJS game package to npm with guided prompts',
|
|
54
54
|
// diary: 'Append daily diary entries via authenticated CLI path', // Temporarily disabled at root CLI.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auramaxx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "AuraJS CLI for creating, playing, and publishing JavaScript-first games.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aurajs",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"src/server/cli/commands/create.ts",
|
|
29
29
|
"src/server/cli/commands/play.ts",
|
|
30
30
|
"src/server/cli/commands/publish.ts",
|
|
31
|
+
"src/server/cli/lib/published-game-integrity.ts",
|
|
31
32
|
"src/server/cli/lib/prompt.ts",
|
|
32
33
|
"src/server/cli/lib/theme.ts"
|
|
33
34
|
],
|
|
@@ -6,23 +6,21 @@ import { printBanner, printSection, paint, ANSI } from '../lib/theme';
|
|
|
6
6
|
import { promptInput, promptSelect } from '../lib/prompt';
|
|
7
7
|
|
|
8
8
|
const TEMPLATE_OPTIONS = [
|
|
9
|
-
{ value: '2d
|
|
10
|
-
{ value: '3d
|
|
11
|
-
{ value: '
|
|
9
|
+
{ value: '2d', label: '[2D] Adventure', aliases: ['1', '2', '2d', '2d-adventure', 'adventure-2d'] },
|
|
10
|
+
{ value: '3d', label: '[3D] Adventure', aliases: ['3', '3d', '3d-adventure', 'adventure-3d'] },
|
|
11
|
+
{ value: 'multiplayer', label: '[MP] Multiplayer', aliases: ['4', 'mp', 'multiplayer', 'room', 'room-code', 'local-multiplayer'] },
|
|
12
12
|
];
|
|
13
|
-
|
|
14
|
-
const VALID_TEMPLATES = new Set(TEMPLATE_OPTIONS.map((option) => option.value));
|
|
15
13
|
const TEMPLATE_ALIASES: Record<string, string> = {
|
|
16
|
-
'2d': '2d
|
|
17
|
-
'
|
|
18
|
-
'2d
|
|
19
|
-
'
|
|
20
|
-
'3d': '3d
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
14
|
+
'2d': '2d',
|
|
15
|
+
'2d-adventure': '2d',
|
|
16
|
+
'adventure-2d': '2d',
|
|
17
|
+
'3d': '3d',
|
|
18
|
+
'3d-adventure': '3d',
|
|
19
|
+
'adventure-3d': '3d',
|
|
20
|
+
'multiplayer': 'multiplayer',
|
|
21
|
+
'local-multiplayer': 'multiplayer',
|
|
22
|
+
'room-code': 'multiplayer',
|
|
23
|
+
'room': 'multiplayer',
|
|
26
24
|
};
|
|
27
25
|
const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
28
26
|
const LOCAL_AURAJS_CLI = path.resolve(
|
|
@@ -98,15 +96,10 @@ async function resolveGameName(initialName: string | null): Promise<string> {
|
|
|
98
96
|
|
|
99
97
|
async function resolveTemplate(initialTemplate: string | null): Promise<string> {
|
|
100
98
|
const normalized = normalizeTemplate(initialTemplate);
|
|
101
|
-
if (normalized
|
|
99
|
+
if (normalized) {
|
|
102
100
|
return normalized;
|
|
103
101
|
}
|
|
104
|
-
|
|
105
|
-
if (normalized && !VALID_TEMPLATES.has(normalized)) {
|
|
106
|
-
console.log(` Unknown template "${normalized}"; choose one below.`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return promptSelect(' Starter template', TEMPLATE_OPTIONS, '2d-shooter');
|
|
102
|
+
return promptSelect(' Starter template', TEMPLATE_OPTIONS, '2d');
|
|
110
103
|
}
|
|
111
104
|
|
|
112
105
|
async function main() {
|
|
@@ -114,10 +107,13 @@ async function main() {
|
|
|
114
107
|
|
|
115
108
|
if (parsed.help) {
|
|
116
109
|
printBanner('CREATE');
|
|
117
|
-
console.log(` ${paint('Usage:', ANSI.bold)} auramaxx create [name] [--template <2d
|
|
110
|
+
console.log(` ${paint('Usage:', ANSI.bold)} auramaxx create [name] [--template <2d|3d|multiplayer>] [--skip-install]`);
|
|
118
111
|
console.log('');
|
|
119
112
|
console.log(' Styled wrapper around AuraJS create scaffolding.');
|
|
120
|
-
console.log(` ${paint('
|
|
113
|
+
console.log(` ${paint('Examples:', ANSI.dim)}`);
|
|
114
|
+
console.log(' auramaxx create my-game --template 2d');
|
|
115
|
+
console.log(' auramaxx create my-game --template 3d');
|
|
116
|
+
console.log(' auramaxx create my-room-game --template multiplayer');
|
|
121
117
|
console.log('');
|
|
122
118
|
return;
|
|
123
119
|
}
|
|
@@ -3,7 +3,9 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'fs';
|
|
|
3
3
|
import { homedir, tmpdir } from 'os';
|
|
4
4
|
import { join, resolve } from 'path';
|
|
5
5
|
import { pathToFileURL } from 'url';
|
|
6
|
-
import { printBanner, printSection, paint, ANSI } from '../lib/theme';
|
|
6
|
+
import { printBanner, printSection, paint, ANSI, createProgressDisplay, printComplete } from '../lib/theme';
|
|
7
|
+
import { promptSelect } from '../lib/prompt';
|
|
8
|
+
import { PublishedGameIntegrityError } from '../lib/published-game-integrity';
|
|
7
9
|
import {
|
|
8
10
|
assertPublishedGameBinIntegrity,
|
|
9
11
|
buildPublishedGameLaunchEnv,
|
|
@@ -108,6 +110,10 @@ function resolveNpmCommand(): string {
|
|
|
108
110
|
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
function resolveAuramaxxCommand(): string {
|
|
114
|
+
return process.platform === 'win32' ? 'auramaxx.cmd' : 'auramaxx';
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
function normalizeRelativePath(pathLike: string): string {
|
|
112
118
|
return String(pathLike || '')
|
|
113
119
|
.trim()
|
|
@@ -180,22 +186,69 @@ function formatInstallError(error: unknown, packageSpec: string): Error {
|
|
|
180
186
|
return new Error(`Refusing to run ${packageSpec}: ${message}`);
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
function isUnsupportedAurajsWrapperError(error: unknown): error is PublishedGameIntegrityError {
|
|
190
|
+
return error instanceof PublishedGameIntegrityError
|
|
191
|
+
&& error.reasonCode === 'published_game_aurajs_version_unsupported';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function promptUpgradeAuramaxx(expectedAurajsVersion: string): Promise<boolean> {
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log(` ${paint('AuraMaxx update required', ANSI.bold)}`);
|
|
197
|
+
console.log(` ${paint(`This game uses @auraindustry/aurajs ${expectedAurajsVersion}.`, ANSI.dim)}`);
|
|
198
|
+
console.log(` ${paint('Your installed AuraMaxx does not recognize that play wrapper yet.', ANSI.dim)}`);
|
|
199
|
+
console.log('');
|
|
200
|
+
|
|
201
|
+
const choice = await promptSelect(
|
|
202
|
+
' Update AuraMaxx now?',
|
|
203
|
+
[
|
|
204
|
+
{ value: 'update', label: 'Yes, update and retry', aliases: ['y', 'yes', '1'] },
|
|
205
|
+
{ value: 'cancel', label: 'No, cancel', aliases: ['n', 'no', '2'] },
|
|
206
|
+
],
|
|
207
|
+
'update',
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return choice === 'update';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function installLatestAuramaxx(): void {
|
|
214
|
+
console.log('');
|
|
215
|
+
console.log(` ${paint('Installing latest AuraMaxx...', ANSI.bold)}`);
|
|
216
|
+
console.log('');
|
|
217
|
+
execFileSync(resolveNpmCommand(), ['install', '-g', 'auramaxx@latest', '--foreground-scripts'], {
|
|
218
|
+
stdio: 'inherit',
|
|
219
|
+
timeout: 120000,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function relaunchWithUpdatedAuramaxx(argv: string[]): never {
|
|
224
|
+
printComplete('AuraMaxx updated. Relaunching play...');
|
|
225
|
+
execFileSync(resolveAuramaxxCommand(), ['play', ...argv], {
|
|
226
|
+
stdio: 'inherit',
|
|
227
|
+
env: process.env,
|
|
228
|
+
});
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
|
|
183
232
|
export async function installVerifiedGamePackage(
|
|
184
233
|
plan: PlayCommandPlan,
|
|
185
234
|
{
|
|
186
235
|
execFileSyncImpl = execFileSync,
|
|
187
236
|
env = process.env,
|
|
237
|
+
onProgress,
|
|
188
238
|
}: {
|
|
189
239
|
execFileSyncImpl?: typeof execFileSync;
|
|
190
240
|
env?: NodeJS.ProcessEnv;
|
|
241
|
+
onProgress?: (step: number, label: string, detail?: string) => void;
|
|
191
242
|
} = {},
|
|
192
243
|
) {
|
|
193
244
|
if (!plan.name || !plan.packageName) {
|
|
194
245
|
throw new Error('installVerifiedGamePackage requires a resolved package spec.');
|
|
195
246
|
}
|
|
196
247
|
|
|
248
|
+
onProgress?.(1, 'Resolving package', plan.packageName);
|
|
197
249
|
const installRoot = mkdtempSync(join(tmpdir(), 'auramaxx-play-'));
|
|
198
250
|
try {
|
|
251
|
+
onProgress?.(2, 'Installing verified wrapper', plan.name);
|
|
199
252
|
execFileSyncImpl(resolveNpmCommand(), buildSecureInstallArgs(plan.name, installRoot), {
|
|
200
253
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
201
254
|
env: {
|
|
@@ -212,6 +265,7 @@ export async function installVerifiedGamePackage(
|
|
|
212
265
|
|
|
213
266
|
const gamePackage = readInstalledPackageJson(packageRoot);
|
|
214
267
|
const dependencySpec = String(gamePackage?.dependencies?.['@auraindustry/aurajs'] || '').trim();
|
|
268
|
+
onProgress?.(3, 'Verifying package integrity', dependencySpec || plan.packageName);
|
|
215
269
|
const integrity = assertPublishedGameBinIntegrity({
|
|
216
270
|
packageRoot,
|
|
217
271
|
projectPackage: gamePackage,
|
|
@@ -245,6 +299,9 @@ export async function installVerifiedGamePackage(
|
|
|
245
299
|
};
|
|
246
300
|
} catch (error: unknown) {
|
|
247
301
|
rmSync(installRoot, { recursive: true, force: true });
|
|
302
|
+
if (error instanceof PublishedGameIntegrityError) {
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
248
305
|
throw formatInstallError(error, plan.name);
|
|
249
306
|
}
|
|
250
307
|
}
|
|
@@ -296,14 +353,20 @@ export async function main(argv: string[] = process.argv.slice(2)) {
|
|
|
296
353
|
|
|
297
354
|
printBanner(plan.name.toUpperCase());
|
|
298
355
|
printSection(plan.name, describeForwardedCommand(plan.forwardedArgs[0] || 'play'));
|
|
356
|
+
const progress = createProgressDisplay(4);
|
|
299
357
|
|
|
300
358
|
let install: Awaited<ReturnType<typeof installVerifiedGamePackage>> | null = null;
|
|
301
359
|
try {
|
|
302
|
-
install = await installVerifiedGamePackage(plan
|
|
360
|
+
install = await installVerifiedGamePackage(plan, {
|
|
361
|
+
onProgress(step, label, detail) {
|
|
362
|
+
progress.update(step, label, detail);
|
|
363
|
+
},
|
|
364
|
+
});
|
|
303
365
|
const env = buildPublishedGameLaunchEnv(
|
|
304
366
|
process.env,
|
|
305
367
|
plan.forwardedArgs[0] === 'join' ? { AURA_GAME_JOIN_MODE: 'play' } : {},
|
|
306
368
|
);
|
|
369
|
+
progress.update(4, 'Launching game', plan.forwardedArgs[0] || 'play');
|
|
307
370
|
execFileSync(process.execPath, [install.binAbsolutePath, ...plan.forwardedArgs], {
|
|
308
371
|
stdio: 'inherit',
|
|
309
372
|
cwd: install.packageRoot,
|
|
@@ -316,7 +379,36 @@ export async function main(argv: string[] = process.argv.slice(2)) {
|
|
|
316
379
|
}
|
|
317
380
|
}
|
|
318
381
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
319
|
-
|
|
382
|
+
const cliArgv = process.argv.slice(2);
|
|
383
|
+
main(cliArgv).catch((err) => {
|
|
384
|
+
if (isUnsupportedAurajsWrapperError(err)) {
|
|
385
|
+
const expectedAurajsVersion = String(err.details.expectedAurajsVersion || '').trim() || '<unknown>';
|
|
386
|
+
(async () => {
|
|
387
|
+
const shouldUpdate = await promptUpgradeAuramaxx(expectedAurajsVersion);
|
|
388
|
+
if (!shouldUpdate) {
|
|
389
|
+
console.error(err.message);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
installLatestAuramaxx();
|
|
395
|
+
relaunchWithUpdatedAuramaxx(cliArgv);
|
|
396
|
+
} catch (updateError) {
|
|
397
|
+
console.error('');
|
|
398
|
+
console.error(
|
|
399
|
+
updateError instanceof Error
|
|
400
|
+
? updateError.message
|
|
401
|
+
: String(updateError),
|
|
402
|
+
);
|
|
403
|
+
console.error(' Manual fallback: npm install -g auramaxx@latest');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
})().catch((promptError) => {
|
|
407
|
+
console.error(promptError instanceof Error ? promptError.message : String(promptError));
|
|
408
|
+
process.exit(1);
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
320
412
|
console.error(err instanceof Error ? err.message : String(err));
|
|
321
413
|
const status = (err as { status?: number; exitCode?: number })?.status
|
|
322
414
|
?? (err as { status?: number; exitCode?: number })?.exitCode
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify } from 'crypto';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
const PACKAGE_INTEGRITY_SCHEMA = 'aurajs.package-integrity.v1';
|
|
6
|
+
const PACKAGE_INTEGRITY_MANIFEST_PATH = 'aura.package-integrity.json';
|
|
7
|
+
const PACKAGE_INTEGRITY_SIGNATURE_PATH = 'aura.package-integrity.sig';
|
|
8
|
+
const PACKAGE_SIGNER_TRUST_STORE_PATH = 'published-game-signers.json';
|
|
9
|
+
const EXACT_SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
10
|
+
const PLAY_WRAPPER_REQUIRED_MARKERS = [
|
|
11
|
+
'#!/usr/bin/env node',
|
|
12
|
+
'const fallbackAuraPackage =',
|
|
13
|
+
"const MINIMAL_COMMANDS = ['dev', 'join', 'play', 'fork', 'publish', 'session'];",
|
|
14
|
+
"const ALL_COMMANDS = ['dev', 'join', 'play', 'fork', 'publish', 'session', 'state', 'inspect', 'action'];",
|
|
15
|
+
"args: ['exec', '--yes', '--package', fallbackAuraPackage, '--', 'aura', ...commandArgs]",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export class PublishedGameIntegrityError extends Error {
|
|
19
|
+
reasonCode: string;
|
|
20
|
+
details: Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
constructor(reasonCode: string, message: string, details: Record<string, unknown> = {}) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'PublishedGameIntegrityError';
|
|
25
|
+
this.reasonCode = reasonCode;
|
|
26
|
+
this.details = details;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeRelativePath(pathLike: string): string {
|
|
31
|
+
return String(pathLike || '')
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/^[.][\\/]/, '')
|
|
34
|
+
.replaceAll('\\', '/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeText(value: string): string {
|
|
38
|
+
return String(value || '').replace(/\r\n?/g, '\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeString(value: unknown): string | null {
|
|
42
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
43
|
+
? value.trim()
|
|
44
|
+
: null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sha256Buffer(buffer: Buffer): string {
|
|
48
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sha256Text(value: string): string {
|
|
52
|
+
return sha256Buffer(Buffer.from(normalizeText(value), 'utf8'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readJsonFile(path: string): Record<string, unknown> {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new PublishedGameIntegrityError(
|
|
60
|
+
'published_game_json_invalid',
|
|
61
|
+
`Failed to parse JSON at ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
62
|
+
{ path },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function listRelativeFiles(root: string, current = root, acc: string[] = []): string[] {
|
|
68
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
69
|
+
const fullPath = join(current, entry.name);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
listRelativeFiles(root, fullPath, acc);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (entry.isFile()) {
|
|
75
|
+
acc.push(normalizeRelativePath(relative(root, fullPath)));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return acc;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function listHashedPackageFiles(root: string, current = root, acc: Array<{ path: string; size: number; sha256: string }> = []) {
|
|
82
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
83
|
+
const fullPath = join(current, entry.name);
|
|
84
|
+
const relativePath = normalizeRelativePath(relative(root, fullPath));
|
|
85
|
+
|
|
86
|
+
if (relativePath === PACKAGE_INTEGRITY_MANIFEST_PATH || relativePath === PACKAGE_INTEGRITY_SIGNATURE_PATH) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (entry.isSymbolicLink()) {
|
|
90
|
+
throw new PublishedGameIntegrityError(
|
|
91
|
+
'published_game_symlink_not_allowed',
|
|
92
|
+
`Published game package may not contain symlinks: ${relativePath}`,
|
|
93
|
+
{ path: relativePath },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (entry.isDirectory()) {
|
|
97
|
+
listHashedPackageFiles(root, fullPath, acc);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!entry.isFile()) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const bytes = readFileSync(fullPath);
|
|
105
|
+
acc.push({
|
|
106
|
+
path: relativePath,
|
|
107
|
+
size: bytes.length,
|
|
108
|
+
sha256: sha256Buffer(bytes),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
acc.sort((left, right) => left.path.localeCompare(right.path));
|
|
113
|
+
return acc;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sortKeysDeep(value: unknown): unknown {
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
return value.map((entry) => sortKeysDeep(entry));
|
|
119
|
+
}
|
|
120
|
+
if (!value || typeof value !== 'object') {
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const sorted: Record<string, unknown> = {};
|
|
125
|
+
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
|
|
126
|
+
sorted[key] = sortKeysDeep((value as Record<string, unknown>)[key]);
|
|
127
|
+
}
|
|
128
|
+
return sorted;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function stableSerialize(value: unknown): string {
|
|
132
|
+
return JSON.stringify(sortKeysDeep(value));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeBinMap(projectPackage: Record<string, unknown>): Record<string, string> {
|
|
136
|
+
if (typeof projectPackage?.bin === 'string' && projectPackage.bin.trim().length > 0) {
|
|
137
|
+
return {
|
|
138
|
+
[String(projectPackage?.name || 'game').split('/').pop() || 'game']: normalizeRelativePath(projectPackage.bin),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const normalized: Record<string, string> = {};
|
|
143
|
+
for (const key of Object.keys((projectPackage?.bin || {}) as Record<string, unknown>).sort()) {
|
|
144
|
+
const value = (projectPackage.bin as Record<string, unknown>)[key];
|
|
145
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
normalized[key] = normalizeRelativePath(value);
|
|
149
|
+
}
|
|
150
|
+
return normalized;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveProjectBinEntries(projectPackage: Record<string, unknown>, packageName: string | null) {
|
|
154
|
+
const normalizedPackageName = normalizeString(packageName) || normalizeString(projectPackage?.name) || 'game';
|
|
155
|
+
if (typeof projectPackage?.bin === 'string' && projectPackage.bin.trim().length > 0) {
|
|
156
|
+
return [{
|
|
157
|
+
name: normalizedPackageName.split('/').pop() || normalizedPackageName,
|
|
158
|
+
relativePath: normalizeRelativePath(projectPackage.bin),
|
|
159
|
+
}];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return Object.entries((projectPackage?.bin || {}) as Record<string, unknown>)
|
|
163
|
+
.filter(([, value]) => typeof value === 'string' && value.trim().length > 0)
|
|
164
|
+
.map(([name, value]) => ({
|
|
165
|
+
name,
|
|
166
|
+
relativePath: normalizeRelativePath(String(value)),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function assertPlayWrapperContract(relativePath: string, wrapperText: string): { markers: string[] } {
|
|
171
|
+
const normalized = normalizeText(wrapperText);
|
|
172
|
+
const missingMarkers = PLAY_WRAPPER_REQUIRED_MARKERS.filter((marker) => !normalized.includes(marker));
|
|
173
|
+
if (missingMarkers.length > 0) {
|
|
174
|
+
throw new PublishedGameIntegrityError(
|
|
175
|
+
'published_game_bin_wrapper_contract_invalid',
|
|
176
|
+
`Bin target "${relativePath}" does not satisfy the AuraJS play wrapper contract.`,
|
|
177
|
+
{
|
|
178
|
+
relativePath,
|
|
179
|
+
missingMarkers,
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
markers: [...PLAY_WRAPPER_REQUIRED_MARKERS],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeAuthoredMetadata(packageRoot: string) {
|
|
190
|
+
const configPath = resolve(packageRoot, 'aura.config.json');
|
|
191
|
+
if (!existsSync(configPath)) {
|
|
192
|
+
return {
|
|
193
|
+
identity: null,
|
|
194
|
+
window: null,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const config = readJsonFile(configPath);
|
|
199
|
+
const iconPath = normalizeString((config.identity as Record<string, unknown> | undefined)?.icon);
|
|
200
|
+
const normalizedIconPath = iconPath ? normalizeRelativePath(iconPath) : null;
|
|
201
|
+
const iconAbsolutePath = normalizedIconPath ? resolve(packageRoot, normalizedIconPath) : null;
|
|
202
|
+
const iconBytes = iconAbsolutePath && existsSync(iconAbsolutePath) && lstatSync(iconAbsolutePath).isFile()
|
|
203
|
+
? readFileSync(iconAbsolutePath)
|
|
204
|
+
: null;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
identity: {
|
|
208
|
+
name: normalizeString((config.identity as Record<string, unknown> | undefined)?.name),
|
|
209
|
+
version: normalizeString((config.identity as Record<string, unknown> | undefined)?.version),
|
|
210
|
+
executable: normalizeString((config.identity as Record<string, unknown> | undefined)?.executable),
|
|
211
|
+
icon: normalizedIconPath,
|
|
212
|
+
iconAsset: {
|
|
213
|
+
path: normalizedIconPath,
|
|
214
|
+
exists: Boolean(iconBytes),
|
|
215
|
+
size: iconBytes ? iconBytes.length : null,
|
|
216
|
+
sha256: iconBytes ? sha256Buffer(iconBytes) : null,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
window: {
|
|
220
|
+
title: normalizeString((config.window as Record<string, unknown> | undefined)?.title),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function updateSignerTrustStore(trustRoot: string | null, packageName: string, packageVersion: string | null, signerFingerprint: string) {
|
|
226
|
+
if (!trustRoot) {
|
|
227
|
+
return {
|
|
228
|
+
status: 'unchecked',
|
|
229
|
+
storePath: null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const storePath = resolve(trustRoot, PACKAGE_SIGNER_TRUST_STORE_PATH);
|
|
234
|
+
const existingEntries = existsSync(storePath) ? readJsonFile(storePath) : {};
|
|
235
|
+
const existing = (existingEntries[packageName] as Record<string, unknown> | undefined) || null;
|
|
236
|
+
const existingFingerprint = normalizeString(existing?.signerFingerprint);
|
|
237
|
+
if (existingFingerprint && existingFingerprint !== signerFingerprint) {
|
|
238
|
+
throw new PublishedGameIntegrityError(
|
|
239
|
+
'published_game_signer_changed',
|
|
240
|
+
`Published game signer changed for ${packageName}: expected ${existingFingerprint}, got ${signerFingerprint}.`,
|
|
241
|
+
{
|
|
242
|
+
packageName,
|
|
243
|
+
packageVersion,
|
|
244
|
+
expectedFingerprint: existingFingerprint,
|
|
245
|
+
actualFingerprint: signerFingerprint,
|
|
246
|
+
storePath,
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const now = new Date().toISOString();
|
|
252
|
+
const nextEntries = {
|
|
253
|
+
...existingEntries,
|
|
254
|
+
[packageName]: {
|
|
255
|
+
signerFingerprint,
|
|
256
|
+
firstSeenAt: existing?.firstSeenAt || now,
|
|
257
|
+
lastSeenAt: now,
|
|
258
|
+
lastVersion: packageVersion,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
mkdirSync(dirname(storePath), { recursive: true });
|
|
263
|
+
writeFileSync(storePath, `${JSON.stringify(nextEntries, null, 2)}\n`, 'utf8');
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
status: existing ? 'trusted' : 'trusted_first_use',
|
|
267
|
+
storePath,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function buildPublishedGameLaunchEnv(
|
|
272
|
+
source: NodeJS.ProcessEnv,
|
|
273
|
+
explicit: Record<string, string> = {},
|
|
274
|
+
): NodeJS.ProcessEnv {
|
|
275
|
+
return {
|
|
276
|
+
...(source || {}),
|
|
277
|
+
...explicit,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function assertPublishedGameBinIntegrity(input: {
|
|
282
|
+
packageRoot: string;
|
|
283
|
+
projectPackage?: Record<string, unknown> | null;
|
|
284
|
+
packageName?: string | null;
|
|
285
|
+
expectedAurajsVersion?: string | null;
|
|
286
|
+
}) {
|
|
287
|
+
const packageRoot = resolve(input.packageRoot);
|
|
288
|
+
const packageJsonPath = resolve(packageRoot, 'package.json');
|
|
289
|
+
if (!existsSync(packageJsonPath)) {
|
|
290
|
+
throw new PublishedGameIntegrityError(
|
|
291
|
+
'published_game_package_json_missing',
|
|
292
|
+
`Could not read installed package metadata at ${packageJsonPath}.`,
|
|
293
|
+
{ packageJsonPath },
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const projectPackage = input.projectPackage || readJsonFile(packageJsonPath);
|
|
298
|
+
const packageName = normalizeString(input.packageName) || normalizeString(projectPackage?.name);
|
|
299
|
+
const dependencySpec = normalizeString((projectPackage.dependencies as Record<string, unknown> | undefined)?.['@auraindustry/aurajs']);
|
|
300
|
+
if (!dependencySpec) {
|
|
301
|
+
throw new PublishedGameIntegrityError(
|
|
302
|
+
'published_game_aurajs_dependency_missing',
|
|
303
|
+
'Published AuraJS games must depend on @auraindustry/aurajs.',
|
|
304
|
+
{ packageJsonPath },
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (!EXACT_SEMVER_PATTERN.test(dependencySpec)) {
|
|
308
|
+
throw new PublishedGameIntegrityError(
|
|
309
|
+
'published_game_aurajs_dependency_unpinned',
|
|
310
|
+
'Published AuraJS games must pin @auraindustry/aurajs to an exact version.',
|
|
311
|
+
{ dependencySpec, packageJsonPath },
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (normalizeString(input.expectedAurajsVersion) && dependencySpec !== input.expectedAurajsVersion) {
|
|
315
|
+
throw new PublishedGameIntegrityError(
|
|
316
|
+
'published_game_aurajs_dependency_version_mismatch',
|
|
317
|
+
`Expected @auraindustry/aurajs version ${input.expectedAurajsVersion}, found ${dependencySpec}.`,
|
|
318
|
+
{
|
|
319
|
+
dependencySpec,
|
|
320
|
+
expectedAurajsVersion: input.expectedAurajsVersion,
|
|
321
|
+
packageJsonPath,
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const binEntries = resolveProjectBinEntries(projectPackage, packageName);
|
|
327
|
+
if (binEntries.length === 0) {
|
|
328
|
+
throw new PublishedGameIntegrityError(
|
|
329
|
+
'published_game_bin_missing',
|
|
330
|
+
'Published AuraJS games must declare a generated wrapper entrypoint in package.json -> bin.',
|
|
331
|
+
{ packageJsonPath },
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const entry of binEntries) {
|
|
336
|
+
if (!entry.relativePath.startsWith('bin/')) {
|
|
337
|
+
throw new PublishedGameIntegrityError(
|
|
338
|
+
'published_game_bin_path_outside_bin_dir',
|
|
339
|
+
`Bin target "${entry.relativePath}" must live under bin/.`,
|
|
340
|
+
{ entry },
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const binDir = resolve(packageRoot, 'bin');
|
|
346
|
+
if (!existsSync(binDir)) {
|
|
347
|
+
throw new PublishedGameIntegrityError(
|
|
348
|
+
'published_game_bin_dir_missing',
|
|
349
|
+
'Published AuraJS games must include a bin/ directory.',
|
|
350
|
+
{ binDir },
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const actualBinFiles = listRelativeFiles(binDir).map((entry) => normalizeRelativePath(join('bin', entry)));
|
|
355
|
+
const declaredBinPaths = [...new Set(binEntries.map((entry) => entry.relativePath))];
|
|
356
|
+
const declaredBinSet = new Set(declaredBinPaths);
|
|
357
|
+
const unexpectedFiles = actualBinFiles.filter((entry) => !declaredBinSet.has(entry));
|
|
358
|
+
if (unexpectedFiles.length > 0) {
|
|
359
|
+
throw new PublishedGameIntegrityError(
|
|
360
|
+
'published_game_bin_unexpected_file',
|
|
361
|
+
`Published AuraJS games may not ship extra bin/ files: ${unexpectedFiles.join(', ')}`,
|
|
362
|
+
{
|
|
363
|
+
declaredBinPaths,
|
|
364
|
+
unexpectedFiles,
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const verifiedFiles = [];
|
|
370
|
+
for (const relativePath of declaredBinPaths) {
|
|
371
|
+
const absolutePath = resolve(packageRoot, relativePath);
|
|
372
|
+
if (!existsSync(absolutePath)) {
|
|
373
|
+
throw new PublishedGameIntegrityError(
|
|
374
|
+
'published_game_bin_target_missing',
|
|
375
|
+
`Published game bin target "${relativePath}" is missing from the installed package.`,
|
|
376
|
+
{ relativePath, absolutePath },
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const wrapperText = readFileSync(absolutePath, 'utf8');
|
|
381
|
+
const contract = assertPlayWrapperContract(relativePath, wrapperText);
|
|
382
|
+
|
|
383
|
+
verifiedFiles.push({
|
|
384
|
+
relativePath,
|
|
385
|
+
absolutePath,
|
|
386
|
+
hash: sha256Text(wrapperText),
|
|
387
|
+
contractMarkers: contract.markers,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
reasonCode: 'published_game_bin_integrity_ok',
|
|
393
|
+
packageName,
|
|
394
|
+
aurajsDependency: {
|
|
395
|
+
spec: dependencySpec,
|
|
396
|
+
wrapperContract: 'aurajs.play-wrapper.v1',
|
|
397
|
+
},
|
|
398
|
+
bin: {
|
|
399
|
+
entries: binEntries,
|
|
400
|
+
declaredFiles: declaredBinPaths,
|
|
401
|
+
actualFiles: actualBinFiles,
|
|
402
|
+
verifiedFiles,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function verifyPublishedGamePackageIntegrity(input: {
|
|
408
|
+
packageRoot: string;
|
|
409
|
+
expectedPackageName?: string | null;
|
|
410
|
+
trustRoot?: string | null;
|
|
411
|
+
}) {
|
|
412
|
+
const packageRoot = resolve(input.packageRoot);
|
|
413
|
+
const manifestPath = resolve(packageRoot, PACKAGE_INTEGRITY_MANIFEST_PATH);
|
|
414
|
+
const signaturePath = resolve(packageRoot, PACKAGE_INTEGRITY_SIGNATURE_PATH);
|
|
415
|
+
if (!existsSync(manifestPath) || !existsSync(signaturePath)) {
|
|
416
|
+
throw new PublishedGameIntegrityError(
|
|
417
|
+
'published_game_integrity_artifacts_missing',
|
|
418
|
+
`Published game package must include ${PACKAGE_INTEGRITY_MANIFEST_PATH} and ${PACKAGE_INTEGRITY_SIGNATURE_PATH}.`,
|
|
419
|
+
{ manifestPath, signaturePath },
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const manifest = readJsonFile(manifestPath);
|
|
424
|
+
const signature = String(readFileSync(signaturePath, 'utf8') || '').trim();
|
|
425
|
+
if (normalizeString(manifest?.schema) !== PACKAGE_INTEGRITY_SCHEMA) {
|
|
426
|
+
throw new PublishedGameIntegrityError(
|
|
427
|
+
'published_game_integrity_schema_invalid',
|
|
428
|
+
`Expected ${PACKAGE_INTEGRITY_SCHEMA}, found ${manifest?.schema || '<missing>'}.`,
|
|
429
|
+
{ schema: manifest?.schema || null },
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (!signature) {
|
|
433
|
+
throw new PublishedGameIntegrityError(
|
|
434
|
+
'published_game_integrity_signature_missing',
|
|
435
|
+
`${PACKAGE_INTEGRITY_SIGNATURE_PATH} is empty.`,
|
|
436
|
+
{ signaturePath },
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const publicKeyPem = normalizeString((manifest.signer as Record<string, unknown> | undefined)?.publicKeyPem);
|
|
441
|
+
const signerFingerprint = normalizeString((manifest.signer as Record<string, unknown> | undefined)?.fingerprint);
|
|
442
|
+
if (!publicKeyPem || !signerFingerprint) {
|
|
443
|
+
throw new PublishedGameIntegrityError(
|
|
444
|
+
'published_game_signer_missing',
|
|
445
|
+
'Package integrity manifest is missing signer metadata.',
|
|
446
|
+
{},
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const actualFingerprint = sha256Buffer(
|
|
451
|
+
createPublicKey(publicKeyPem).export({ format: 'der', type: 'spki' }) as Buffer,
|
|
452
|
+
);
|
|
453
|
+
if (actualFingerprint !== signerFingerprint) {
|
|
454
|
+
throw new PublishedGameIntegrityError(
|
|
455
|
+
'published_game_signer_fingerprint_mismatch',
|
|
456
|
+
'Package integrity signer fingerprint does not match the embedded public key.',
|
|
457
|
+
{
|
|
458
|
+
expectedFingerprint: signerFingerprint,
|
|
459
|
+
actualFingerprint,
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const signatureOk = verify(
|
|
465
|
+
null,
|
|
466
|
+
Buffer.from(stableSerialize(manifest)),
|
|
467
|
+
publicKeyPem,
|
|
468
|
+
Buffer.from(signature, 'base64'),
|
|
469
|
+
);
|
|
470
|
+
if (!signatureOk) {
|
|
471
|
+
throw new PublishedGameIntegrityError(
|
|
472
|
+
'published_game_integrity_signature_invalid',
|
|
473
|
+
'Package integrity signature verification failed.',
|
|
474
|
+
{ signerFingerprint },
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (normalizeString(input.expectedPackageName) && normalizeString((manifest.package as Record<string, unknown> | undefined)?.name) !== input.expectedPackageName) {
|
|
479
|
+
throw new PublishedGameIntegrityError(
|
|
480
|
+
'published_game_package_name_mismatch',
|
|
481
|
+
`Expected published package ${input.expectedPackageName}, found ${(manifest.package as Record<string, unknown> | undefined)?.name || '<missing>'}.`,
|
|
482
|
+
{
|
|
483
|
+
expectedPackageName: input.expectedPackageName,
|
|
484
|
+
actualPackageName: (manifest.package as Record<string, unknown> | undefined)?.name || null,
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const actualFiles = listHashedPackageFiles(packageRoot);
|
|
490
|
+
const expectedFiles = Array.isArray(manifest.files) ? manifest.files as Array<Record<string, unknown>> : [];
|
|
491
|
+
const expectedByPath = new Map(expectedFiles.map((entry) => [normalizeRelativePath(String(entry.path || '')), entry]));
|
|
492
|
+
const actualByPath = new Map(actualFiles.map((entry) => [entry.path, entry]));
|
|
493
|
+
const missing = [];
|
|
494
|
+
const mismatched = [];
|
|
495
|
+
for (const [path, expected] of expectedByPath) {
|
|
496
|
+
const actual = actualByPath.get(path);
|
|
497
|
+
if (!actual) {
|
|
498
|
+
missing.push(path);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (actual.sha256 !== expected.sha256 || actual.size !== expected.size) {
|
|
502
|
+
mismatched.push({
|
|
503
|
+
path,
|
|
504
|
+
expected,
|
|
505
|
+
actual,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const extra = actualFiles.filter((entry) => !expectedByPath.has(entry.path)).map((entry) => entry.path);
|
|
510
|
+
if (missing.length > 0 || mismatched.length > 0 || extra.length > 0) {
|
|
511
|
+
throw new PublishedGameIntegrityError(
|
|
512
|
+
'published_game_file_mismatch',
|
|
513
|
+
'Published game package contents do not match the signed integrity manifest.',
|
|
514
|
+
{
|
|
515
|
+
missing,
|
|
516
|
+
extra,
|
|
517
|
+
mismatched,
|
|
518
|
+
},
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const packageJson = readJsonFile(resolve(packageRoot, 'package.json'));
|
|
523
|
+
const actualPackageMetadata = {
|
|
524
|
+
name: normalizeString(packageJson?.name),
|
|
525
|
+
version: normalizeString(packageJson?.version),
|
|
526
|
+
description: normalizeString(packageJson?.description),
|
|
527
|
+
type: normalizeString(packageJson?.type),
|
|
528
|
+
aurajsVersion: normalizeString((packageJson.dependencies as Record<string, unknown> | undefined)?.['@auraindustry/aurajs']),
|
|
529
|
+
bin: normalizeBinMap(packageJson),
|
|
530
|
+
};
|
|
531
|
+
if (stableSerialize(actualPackageMetadata) !== stableSerialize(manifest.package || {})) {
|
|
532
|
+
throw new PublishedGameIntegrityError(
|
|
533
|
+
'published_game_package_metadata_mismatch',
|
|
534
|
+
'Installed package.json metadata does not match the signed integrity manifest.',
|
|
535
|
+
{
|
|
536
|
+
expected: manifest.package || null,
|
|
537
|
+
actual: actualPackageMetadata,
|
|
538
|
+
},
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const actualAuthoredMetadata = normalizeAuthoredMetadata(packageRoot);
|
|
543
|
+
if (stableSerialize(actualAuthoredMetadata) !== stableSerialize((manifest.publishedMetadata as Record<string, unknown> | undefined)?.authored || {})) {
|
|
544
|
+
throw new PublishedGameIntegrityError(
|
|
545
|
+
'published_game_authored_metadata_mismatch',
|
|
546
|
+
'Installed authored game metadata does not match the signed integrity manifest.',
|
|
547
|
+
{
|
|
548
|
+
expected: (manifest.publishedMetadata as Record<string, unknown> | undefined)?.authored || null,
|
|
549
|
+
actual: actualAuthoredMetadata,
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const trust = updateSignerTrustStore(
|
|
555
|
+
normalizeString(input.trustRoot),
|
|
556
|
+
normalizeString(actualPackageMetadata.name) || '<unknown>',
|
|
557
|
+
normalizeString(actualPackageMetadata.version),
|
|
558
|
+
signerFingerprint,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
reasonCode: 'published_game_package_integrity_ok',
|
|
563
|
+
manifestPath,
|
|
564
|
+
signaturePath,
|
|
565
|
+
packageName: actualPackageMetadata.name,
|
|
566
|
+
packageVersion: actualPackageMetadata.version,
|
|
567
|
+
signerFingerprint,
|
|
568
|
+
trust,
|
|
569
|
+
fileCount: expectedFiles.length,
|
|
570
|
+
publishedMetadata: manifest.publishedMetadata || null,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
@@ -221,6 +221,64 @@ export function printComplete(message: string): void {
|
|
|
221
221
|
console.log('');
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
function stripAnsi(text: string): string {
|
|
225
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function formatProgressBar(current: number, total: number, width: number): string {
|
|
229
|
+
const safeTotal = Math.max(1, total);
|
|
230
|
+
const clamped = Math.max(0, Math.min(current, safeTotal));
|
|
231
|
+
const filled = Math.round((clamped / safeTotal) * width);
|
|
232
|
+
const active = paint('/'.repeat(filled), ANSI.fgAccent, ANSI.bold);
|
|
233
|
+
const pending = paint('-'.repeat(Math.max(0, width - filled)), ANSI.dim);
|
|
234
|
+
return `[${active}${pending}]`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface ProgressDisplay {
|
|
238
|
+
update(step: number, label: string, detail?: string): void;
|
|
239
|
+
complete(label?: string, detail?: string): void;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function createProgressDisplay(totalSteps: number, width = 24): ProgressDisplay {
|
|
243
|
+
const stdout = process.stdout;
|
|
244
|
+
const interactive = stdout.isTTY;
|
|
245
|
+
let renderedLineCount = 0;
|
|
246
|
+
|
|
247
|
+
function draw(step: number, label: string, detail?: string) {
|
|
248
|
+
const safeStep = Math.max(0, Math.min(step, totalSteps));
|
|
249
|
+
const titleLine = ` ${paint('//', ANSI.fgAccent, ANSI.bold)} ${paint(label, ANSI.bold)} ${paint(`${safeStep}/${totalSteps}`, ANSI.dim)} ${formatProgressBar(safeStep, totalSteps, width)}`;
|
|
250
|
+
const lines = detail ? [titleLine, ` ${paint(detail, ANSI.dim)}`] : [titleLine];
|
|
251
|
+
|
|
252
|
+
if (interactive && renderedLineCount > 0) {
|
|
253
|
+
stdout.write(`\u001b[${renderedLineCount}A`);
|
|
254
|
+
for (let index = 0; index < renderedLineCount; index += 1) {
|
|
255
|
+
stdout.write('\u001b[2K');
|
|
256
|
+
stdout.write('\u001b[1B');
|
|
257
|
+
}
|
|
258
|
+
stdout.write(`\u001b[${renderedLineCount}A`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const line of lines) {
|
|
262
|
+
if (interactive) {
|
|
263
|
+
stdout.write('\u001b[2K');
|
|
264
|
+
stdout.write(`${line}\n`);
|
|
265
|
+
} else {
|
|
266
|
+
console.log(stripAnsi(line));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
renderedLineCount = lines.length;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
update(step: number, label: string, detail?: string) {
|
|
274
|
+
draw(step, label, detail);
|
|
275
|
+
},
|
|
276
|
+
complete(label = 'Ready', detail?: string) {
|
|
277
|
+
draw(totalSteps, label, detail);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
224
282
|
// ── Seed phrase box (security-critical) ──────────────────────
|
|
225
283
|
|
|
226
284
|
/**
|