auramaxx 0.1.1 → 0.1.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auramaxx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "AuraJS CLI for creating, playing, and publishing JavaScript-first games.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aurajs",
|
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
"src/lib/startBannerQuotes.ts",
|
|
27
27
|
"src/server/cli/commands/onboard.ts",
|
|
28
28
|
"src/server/cli/commands/create.ts",
|
|
29
|
+
"src/server/cli/commands/fork.ts",
|
|
30
|
+
"src/server/cli/commands/make.ts",
|
|
29
31
|
"src/server/cli/commands/play.ts",
|
|
30
32
|
"src/server/cli/commands/publish.ts",
|
|
33
|
+
"src/server/cli/lib/published-game-integrity.ts",
|
|
31
34
|
"src/server/cli/lib/prompt.ts",
|
|
32
35
|
"src/server/cli/lib/theme.ts"
|
|
33
36
|
],
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { rmSync } from 'fs';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
|
|
5
|
+
import { printBanner, printSection, paint, ANSI } from '../lib/theme';
|
|
6
|
+
import {
|
|
7
|
+
buildPublishedGameLaunchEnv,
|
|
8
|
+
installVerifiedGamePackage,
|
|
9
|
+
parseArgs,
|
|
10
|
+
resolveGameBin,
|
|
11
|
+
resolvePackageName,
|
|
12
|
+
} from './play';
|
|
13
|
+
|
|
14
|
+
export interface ForkCommandPlan {
|
|
15
|
+
help: boolean;
|
|
16
|
+
name: string | null;
|
|
17
|
+
gameArgs: string[];
|
|
18
|
+
gameBin: string | null;
|
|
19
|
+
packageName: string | null;
|
|
20
|
+
forwardedArgs: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildForkPlan(argv: string[]): ForkCommandPlan {
|
|
24
|
+
const parsed = parseArgs(argv);
|
|
25
|
+
return {
|
|
26
|
+
...parsed,
|
|
27
|
+
gameBin: parsed.name ? resolveGameBin(parsed.name) : null,
|
|
28
|
+
packageName: parsed.name ? resolvePackageName(parsed.name) : null,
|
|
29
|
+
forwardedArgs: ['fork', ...parsed.gameArgs],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function main(argv: string[] = process.argv.slice(2)) {
|
|
34
|
+
const plan = buildForkPlan(argv);
|
|
35
|
+
|
|
36
|
+
if (plan.help || !plan.name) {
|
|
37
|
+
printBanner('FORK');
|
|
38
|
+
console.log(` ${paint('Usage:', ANSI.bold)} auramaxx fork <game> [destination|wrapper fork options]`);
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(` ${paint('Examples:', ANSI.dim)}`);
|
|
41
|
+
console.log(' auramaxx fork aurasu');
|
|
42
|
+
console.log(' auramaxx fork aurasu ./aurasu-local');
|
|
43
|
+
console.log(' auramaxx fork @auraindustry/chess-dev-cli@1.2.3 --dest ./chess-local');
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(` ${paint('Runs:', ANSI.dim)} verified signed temporary install -> local game wrapper fork`);
|
|
46
|
+
console.log('');
|
|
47
|
+
if (!plan.name && !plan.help) {
|
|
48
|
+
console.error(' Missing game name.');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
printBanner(plan.name.toUpperCase());
|
|
55
|
+
printSection(plan.name, 'Forking editable package into local project...');
|
|
56
|
+
|
|
57
|
+
let install: Awaited<ReturnType<typeof installVerifiedGamePackage>> | null = null;
|
|
58
|
+
try {
|
|
59
|
+
install = await installVerifiedGamePackage(plan);
|
|
60
|
+
const env = buildPublishedGameLaunchEnv(process.env);
|
|
61
|
+
execFileSync(process.execPath, [install.binAbsolutePath, ...plan.forwardedArgs], {
|
|
62
|
+
stdio: 'inherit',
|
|
63
|
+
cwd: install.packageRoot,
|
|
64
|
+
env,
|
|
65
|
+
});
|
|
66
|
+
} finally {
|
|
67
|
+
if (install?.installRoot) {
|
|
68
|
+
rmSync(install.installRoot, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
74
|
+
main().catch((err) => {
|
|
75
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
76
|
+
const status = (err as { status?: number; exitCode?: number })?.status
|
|
77
|
+
?? (err as { status?: number; exitCode?: number })?.exitCode
|
|
78
|
+
?? 1;
|
|
79
|
+
process.exit(Number.isInteger(status) ? status : 1);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { printBanner, paint, ANSI } from '../lib/theme';
|
|
6
|
+
|
|
7
|
+
const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const LOCAL_AURAJS_CLI = path.resolve(
|
|
9
|
+
COMMAND_DIR,
|
|
10
|
+
'../../../../../packages/aurascript/src/cli/src/cli.mjs',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv: string[]) {
|
|
14
|
+
for (const arg of argv) {
|
|
15
|
+
if (arg === '--help' || arg === '-h') {
|
|
16
|
+
return { help: true, passthrough: [] as string[] };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
help: false,
|
|
21
|
+
passthrough: [...argv],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveInvocationCwd(): string {
|
|
26
|
+
const forwardedCwd = process.env.AURA_INVOKE_CWD;
|
|
27
|
+
if (forwardedCwd && path.isAbsolute(forwardedCwd)) {
|
|
28
|
+
return forwardedCwd;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const shellPwd = process.env.PWD;
|
|
32
|
+
if (shellPwd && path.isAbsolute(shellPwd)) {
|
|
33
|
+
return shellPwd;
|
|
34
|
+
}
|
|
35
|
+
return process.cwd();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
40
|
+
|
|
41
|
+
if (parsed.help) {
|
|
42
|
+
printBanner('MAKE');
|
|
43
|
+
console.log(` ${paint('Usage:', ANSI.bold)} auramaxx make [kind] [name] [--role <custom|enemy|pickup|player|world>]`);
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(' Public wrapper around AuraJS project file generation.');
|
|
46
|
+
console.log(' Run it from inside an AuraJS game project.');
|
|
47
|
+
console.log(` ${paint('Examples:', ANSI.dim)}`);
|
|
48
|
+
console.log(' auramaxx make');
|
|
49
|
+
console.log(' auramaxx make scene Scene1');
|
|
50
|
+
console.log(' auramaxx make ui-screen PauseMenu');
|
|
51
|
+
console.log(' auramaxx make prefab EnemyShip --role enemy');
|
|
52
|
+
console.log(' auramaxx make list');
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(' When no arguments are passed in a TTY, AuraJS opens an interactive make flow.');
|
|
55
|
+
console.log('');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const invocationCwd = resolveInvocationCwd();
|
|
60
|
+
const auraArgs = ['make', ...parsed.passthrough];
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
if (existsSync(LOCAL_AURAJS_CLI)) {
|
|
64
|
+
execFileSync(process.execPath, [LOCAL_AURAJS_CLI, ...auraArgs], {
|
|
65
|
+
cwd: invocationCwd,
|
|
66
|
+
stdio: 'inherit',
|
|
67
|
+
env: process.env,
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
execFileSync(
|
|
71
|
+
'npm',
|
|
72
|
+
['exec', '--yes', '--package', '@auraindustry/aurajs', '--', 'aura', ...auraArgs],
|
|
73
|
+
{
|
|
74
|
+
cwd: invocationCwd,
|
|
75
|
+
stdio: 'inherit',
|
|
76
|
+
env: process.env,
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
} catch (error: unknown) {
|
|
81
|
+
const status = (error as { status?: number }).status;
|
|
82
|
+
if (status) process.exit(status);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main().catch((err) => {
|
|
88
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -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, createProgressDisplay } 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,31 @@ 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
|
+
|
|
117
|
+
function resolveInstalledGlobalAuramaxxVersion(): string | null {
|
|
118
|
+
try {
|
|
119
|
+
const root = execFileSync(resolveNpmCommand(), ['root', '-g'], {
|
|
120
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
121
|
+
encoding: 'utf8',
|
|
122
|
+
timeout: 30000,
|
|
123
|
+
}).trim();
|
|
124
|
+
if (!root) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const packageJsonPath = join(root, 'auramaxx', 'package.json');
|
|
128
|
+
if (!existsSync(packageJsonPath)) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { version?: unknown };
|
|
132
|
+
return typeof pkg.version === 'string' && pkg.version.trim().length > 0 ? pkg.version.trim() : null;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
111
138
|
function normalizeRelativePath(pathLike: string): string {
|
|
112
139
|
return String(pathLike || '')
|
|
113
140
|
.trim()
|
|
@@ -180,6 +207,63 @@ function formatInstallError(error: unknown, packageSpec: string): Error {
|
|
|
180
207
|
return new Error(`Refusing to run ${packageSpec}: ${message}`);
|
|
181
208
|
}
|
|
182
209
|
|
|
210
|
+
function isUnsupportedAurajsWrapperError(error: unknown): error is PublishedGameIntegrityError {
|
|
211
|
+
return error instanceof PublishedGameIntegrityError
|
|
212
|
+
&& error.reasonCode === 'published_game_aurajs_version_unsupported';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function promptUpgradeAuramaxx(expectedAurajsVersion: string): Promise<boolean> {
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log(` ${paint('AuraMaxx update required', ANSI.bold)}`);
|
|
218
|
+
console.log(` ${paint(`This game uses @auraindustry/aurajs ${expectedAurajsVersion}.`, ANSI.dim)}`);
|
|
219
|
+
console.log(` ${paint('Your installed AuraMaxx does not recognize that play wrapper yet.', ANSI.dim)}`);
|
|
220
|
+
console.log('');
|
|
221
|
+
|
|
222
|
+
const choice = await promptSelect(
|
|
223
|
+
' Update AuraMaxx now?',
|
|
224
|
+
[
|
|
225
|
+
{ value: 'update', label: 'Yes, update and retry', aliases: ['y', 'yes', '1'] },
|
|
226
|
+
{ value: 'cancel', label: 'No, cancel', aliases: ['n', 'no', '2'] },
|
|
227
|
+
],
|
|
228
|
+
'update',
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return choice === 'update';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function installLatestAuramaxx(): void {
|
|
235
|
+
console.log('');
|
|
236
|
+
console.log(` ${paint('Reinstalling latest AuraMaxx...', ANSI.bold)}`);
|
|
237
|
+
console.log('');
|
|
238
|
+
try {
|
|
239
|
+
execFileSync(resolveNpmCommand(), ['uninstall', '-g', 'auramaxx'], {
|
|
240
|
+
stdio: 'inherit',
|
|
241
|
+
timeout: 120000,
|
|
242
|
+
});
|
|
243
|
+
} catch {
|
|
244
|
+
// Ignore missing-package uninstall failures and continue with a clean install.
|
|
245
|
+
}
|
|
246
|
+
execFileSync(resolveNpmCommand(), ['install', '-g', 'auramaxx@latest', '--foreground-scripts'], {
|
|
247
|
+
stdio: 'inherit',
|
|
248
|
+
timeout: 180000,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const installedVersion = resolveInstalledGlobalAuramaxxVersion();
|
|
252
|
+
if (installedVersion) {
|
|
253
|
+
console.log('');
|
|
254
|
+
console.log(` ${paint(`Installed AuraMaxx ${installedVersion}`, ANSI.dim)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function relaunchWithUpdatedAuramaxx(argv: string[]): never {
|
|
259
|
+
printComplete('AuraMaxx updated. Relaunching play...');
|
|
260
|
+
execFileSync(resolveAuramaxxCommand(), ['play', ...argv], {
|
|
261
|
+
stdio: 'inherit',
|
|
262
|
+
env: process.env,
|
|
263
|
+
});
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
|
|
183
267
|
export async function installVerifiedGamePackage(
|
|
184
268
|
plan: PlayCommandPlan,
|
|
185
269
|
{
|
|
@@ -250,6 +334,9 @@ export async function installVerifiedGamePackage(
|
|
|
250
334
|
};
|
|
251
335
|
} catch (error: unknown) {
|
|
252
336
|
rmSync(installRoot, { recursive: true, force: true });
|
|
337
|
+
if (error instanceof PublishedGameIntegrityError) {
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
253
340
|
throw formatInstallError(error, plan.name);
|
|
254
341
|
}
|
|
255
342
|
}
|
|
@@ -327,7 +414,36 @@ export async function main(argv: string[] = process.argv.slice(2)) {
|
|
|
327
414
|
}
|
|
328
415
|
}
|
|
329
416
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
330
|
-
|
|
417
|
+
const cliArgv = process.argv.slice(2);
|
|
418
|
+
main(cliArgv).catch((err) => {
|
|
419
|
+
if (isUnsupportedAurajsWrapperError(err)) {
|
|
420
|
+
const expectedAurajsVersion = String(err.details.expectedAurajsVersion || '').trim() || '<unknown>';
|
|
421
|
+
(async () => {
|
|
422
|
+
const shouldUpdate = await promptUpgradeAuramaxx(expectedAurajsVersion);
|
|
423
|
+
if (!shouldUpdate) {
|
|
424
|
+
console.error(err.message);
|
|
425
|
+
process.exit(1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
installLatestAuramaxx();
|
|
430
|
+
relaunchWithUpdatedAuramaxx(cliArgv);
|
|
431
|
+
} catch (updateError) {
|
|
432
|
+
console.error('');
|
|
433
|
+
console.error(
|
|
434
|
+
updateError instanceof Error
|
|
435
|
+
? updateError.message
|
|
436
|
+
: String(updateError),
|
|
437
|
+
);
|
|
438
|
+
console.error(' Manual fallback: npm uninstall -g auramaxx && npm install -g auramaxx@latest');
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
})().catch((promptError) => {
|
|
442
|
+
console.error(promptError instanceof Error ? promptError.message : String(promptError));
|
|
443
|
+
process.exit(1);
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
331
447
|
console.error(err instanceof Error ? err.message : String(err));
|
|
332
448
|
const status = (err as { status?: number; exitCode?: number })?.status
|
|
333
449
|
?? (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
|
+
}
|