@mpgd/cli 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/asset-pack-build.d.ts +32 -0
- package/dist/asset-pack-build.js +411 -0
- package/dist/asset-pack-verify.d.ts +76 -0
- package/dist/asset-pack-verify.js +922 -0
- package/dist/asset-pack-zip.d.ts +19 -0
- package/dist/asset-pack-zip.js +81 -0
- package/dist/bin.js +6 -1
- package/dist/cloudflare-pages-static.js +1 -1
- package/dist/evidence-io.js +1 -1
- package/dist/game-acceptance.js +2 -2
- package/dist/gameplay-e2e.js +5 -5
- package/dist/hosted-pwa-deployment.js +2 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +296 -5
- package/dist/microsoft-store-package-acceptance.js +1 -1
- package/dist/microsoft-store-package-generation-download.js +3 -3
- package/dist/microsoft-store-package-generation-input.js +1 -1
- package/dist/microsoft-store-package-generation-zip.js +1 -1
- package/dist/microsoft-store-submission.js +4 -4
- package/dist/offline-playtest.js +5 -5
- package/package.json +16 -16
- package/templates/phaser-game/apps/target-ait/package.json +4 -5
- package/templates/phaser-game/apps/target-ait/vite.config.ts +2 -7
- package/templates/phaser-game/apps/target-cloudflare-pages/package.json +2 -2
- package/templates/phaser-game/apps/target-devvit/package.json +2 -2
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +6 -12
- package/templates/phaser-game/package.json +3 -3
- package/templates/phaser-game/pnpm-workspace.yaml +0 -4
- package/templates/phaser-game/src/entry.ts +1 -4
- package/templates/phaser-game/src/gameEntry.ts +1 -4
- package/templates/phaser-game/src/platform/buildGateways/redditSandbox.ts +1 -4
- package/templates/phaser-game/src/platform/buildGateways/verse8.ts +1 -4
- package/templates/phaser-game/src/platform/devvitEntrypoint.ts +1 -4
- package/templates/phaser-game/tsconfig.json +1 -0
- package/templates/phaser-game/vite.runtime-target-config.ts +2 -2
- package/templates/phaser-game/vite.shared.ts +1 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic ZIP writer for pack delivery archives. The builder targets
|
|
3
|
+
* this limited format only: STORE and DEFLATE entries, no ZIP64, no split
|
|
4
|
+
* archives, no encryption, no directory or symlink entries, no comments.
|
|
5
|
+
* Fixed metadata: DOS timestamp 1980-01-01 00:00:00, UNIX permissions 0644,
|
|
6
|
+
* UTF-8 name flag, no extra fields. Given the same entries in the same order
|
|
7
|
+
* and the same Node/zlib build, the produced bytes are identical.
|
|
8
|
+
*/
|
|
9
|
+
export interface DeterministicZipEntry {
|
|
10
|
+
/** Normalized relative entry path; validated by the pack format module. */
|
|
11
|
+
readonly path: string;
|
|
12
|
+
/** Uncompressed entry bytes. */
|
|
13
|
+
readonly data: Buffer;
|
|
14
|
+
readonly method: 'store' | 'deflate';
|
|
15
|
+
/** Precomputed stored bytes for the entry method, when the builder already
|
|
16
|
+
* produced them (for example while choosing the method). */
|
|
17
|
+
readonly stored?: Buffer | undefined;
|
|
18
|
+
}
|
|
19
|
+
export declare function createDeterministicZip(entries: readonly DeterministicZipEntry[]): Buffer;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { crc32, deflateRawSync } from 'node:zlib';
|
|
2
|
+
const DOS_TIME = 0;
|
|
3
|
+
const DOS_DATE = 0x0021;
|
|
4
|
+
const VERSION_NEEDED = 20;
|
|
5
|
+
const VERSION_MADE_BY = 0x0300;
|
|
6
|
+
const UTF8_FLAG = 0x0800;
|
|
7
|
+
const EXTERNAL_ATTRIBUTES = (0o100644 << 16) >>> 0;
|
|
8
|
+
const MAX_ENTRIES = 0xffff;
|
|
9
|
+
const MAX_ENTRY_BYTES = 0xffffffff;
|
|
10
|
+
export function createDeterministicZip(entries) {
|
|
11
|
+
if (entries.length === 0 || entries.length > MAX_ENTRIES) {
|
|
12
|
+
throw new Error(`Deterministic ZIP requires 1..${MAX_ENTRIES} entries`);
|
|
13
|
+
}
|
|
14
|
+
const localChunks = [];
|
|
15
|
+
const centralChunks = [];
|
|
16
|
+
let offset = 0;
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const name = Buffer.from(entry.path, 'utf8');
|
|
19
|
+
if (name.length > MAX_ENTRIES) {
|
|
20
|
+
throw new Error(`Deterministic ZIP entry name exceeds 65535 bytes: ${entry.path}`);
|
|
21
|
+
}
|
|
22
|
+
if (entry.data.length > MAX_ENTRY_BYTES) {
|
|
23
|
+
throw new Error(`Deterministic ZIP entry exceeds 4 GiB: ${entry.path}`);
|
|
24
|
+
}
|
|
25
|
+
const compressed = entry.method === 'deflate'
|
|
26
|
+
? (entry.stored ?? deflateRawSync(entry.data, { level: 9 }))
|
|
27
|
+
: entry.data;
|
|
28
|
+
if (compressed.length > MAX_ENTRY_BYTES) {
|
|
29
|
+
throw new Error(`Deterministic ZIP entry exceeds 4 GiB: ${entry.path}`);
|
|
30
|
+
}
|
|
31
|
+
const methodCode = entry.method === 'deflate' ? 8 : 0;
|
|
32
|
+
const digest = crc32(entry.data) >>> 0;
|
|
33
|
+
const local = Buffer.alloc(30);
|
|
34
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
35
|
+
local.writeUInt16LE(VERSION_NEEDED, 4);
|
|
36
|
+
local.writeUInt16LE(UTF8_FLAG, 6);
|
|
37
|
+
local.writeUInt16LE(methodCode, 8);
|
|
38
|
+
local.writeUInt16LE(DOS_TIME, 10);
|
|
39
|
+
local.writeUInt16LE(DOS_DATE, 12);
|
|
40
|
+
local.writeUInt32LE(digest, 14);
|
|
41
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
42
|
+
local.writeUInt32LE(entry.data.length, 22);
|
|
43
|
+
local.writeUInt16LE(name.length, 26);
|
|
44
|
+
local.writeUInt16LE(0, 28);
|
|
45
|
+
localChunks.push(local, name, compressed);
|
|
46
|
+
const central = Buffer.alloc(46);
|
|
47
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
48
|
+
central.writeUInt16LE(VERSION_MADE_BY, 4);
|
|
49
|
+
central.writeUInt16LE(VERSION_NEEDED, 6);
|
|
50
|
+
central.writeUInt16LE(UTF8_FLAG, 8);
|
|
51
|
+
central.writeUInt16LE(methodCode, 10);
|
|
52
|
+
central.writeUInt16LE(DOS_TIME, 12);
|
|
53
|
+
central.writeUInt16LE(DOS_DATE, 14);
|
|
54
|
+
central.writeUInt32LE(digest, 16);
|
|
55
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
56
|
+
central.writeUInt32LE(entry.data.length, 24);
|
|
57
|
+
central.writeUInt16LE(name.length, 28);
|
|
58
|
+
central.writeUInt16LE(0, 30);
|
|
59
|
+
central.writeUInt16LE(0, 32);
|
|
60
|
+
central.writeUInt16LE(0, 34);
|
|
61
|
+
central.writeUInt16LE(0, 36);
|
|
62
|
+
central.writeUInt32LE(EXTERNAL_ATTRIBUTES, 38);
|
|
63
|
+
central.writeUInt32LE(offset, 42);
|
|
64
|
+
centralChunks.push(central, name);
|
|
65
|
+
offset += local.length + name.length + compressed.length;
|
|
66
|
+
}
|
|
67
|
+
const centralDirectory = Buffer.concat(centralChunks);
|
|
68
|
+
if (offset > MAX_ENTRY_BYTES || centralDirectory.length > MAX_ENTRY_BYTES) {
|
|
69
|
+
throw new Error('Deterministic ZIP archive exceeds 4 GiB');
|
|
70
|
+
}
|
|
71
|
+
const end = Buffer.alloc(22);
|
|
72
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
73
|
+
end.writeUInt16LE(0, 4);
|
|
74
|
+
end.writeUInt16LE(0, 6);
|
|
75
|
+
end.writeUInt16LE(entries.length, 8);
|
|
76
|
+
end.writeUInt16LE(entries.length, 10);
|
|
77
|
+
end.writeUInt32LE(centralDirectory.length, 12);
|
|
78
|
+
end.writeUInt32LE(offset, 16);
|
|
79
|
+
end.writeUInt16LE(0, 20);
|
|
80
|
+
return Buffer.concat([...localChunks, centralDirectory, end]);
|
|
81
|
+
}
|
package/dist/bin.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readCliArgs, runMpgdCli } from './index.js';
|
|
2
|
+
import { mpgdCliNeedsForcedExit, readCliArgs, runMpgdCli } from './index.js';
|
|
3
3
|
await runMpgdCli(readCliArgs());
|
|
4
|
+
if (mpgdCliNeedsForcedExit()) {
|
|
5
|
+
// Stalled threadpool filesystem work cannot be cancelled; only the
|
|
6
|
+
// standalone binary may terminate the process to outrun it.
|
|
7
|
+
process.exit(process.exitCode ?? 1);
|
|
8
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* being silently ignored.
|
|
10
10
|
*/
|
|
11
11
|
const maximumHeaderBlocks = 100;
|
|
12
|
-
const maximumLineLength =
|
|
12
|
+
const maximumLineLength = 2_000;
|
|
13
13
|
const supportedRedirectCodes = new Set([301, 302, 303, 307, 308]);
|
|
14
14
|
export function parseCloudflarePagesHeaders(input) {
|
|
15
15
|
const lines = input.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n');
|
package/dist/evidence-io.js
CHANGED
|
@@ -38,7 +38,7 @@ export function escapeMarkdownInline(value) {
|
|
|
38
38
|
.replaceAll('\n', ' ');
|
|
39
39
|
}
|
|
40
40
|
export function formatDuration(durationMs) {
|
|
41
|
-
return durationMs <
|
|
41
|
+
return durationMs < 1_000 ? `${durationMs}ms` : `${(durationMs / 1_000).toFixed(1)}s`;
|
|
42
42
|
}
|
|
43
43
|
export function formatError(error) {
|
|
44
44
|
return error instanceof Error ? error.message : String(error);
|
package/dist/game-acceptance.js
CHANGED
|
@@ -3,9 +3,9 @@ import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { escapeMarkdownInline, escapeMarkdownTable, formatDuration, formatError, readBoundedUtf8File, relativeOrAbsolute, writeEvidenceReportFiles, } from './evidence-io.js';
|
|
5
5
|
import { collectGameplayE2EPathEvidence, maximumGameplayE2EStates, readGameplayE2EPlan, resolveGameplayE2EPathInsideGameRoot, } from './gameplay-e2e.js';
|
|
6
|
-
export const defaultGameAcceptanceCommandTimeoutMs = 30 * 60 *
|
|
6
|
+
export const defaultGameAcceptanceCommandTimeoutMs = 30 * 60 * 1_000;
|
|
7
7
|
const defaultGameAcceptanceReleaseManifestFile = 'artifacts/release-manifest.json';
|
|
8
|
-
export const maximumGameplayE2EReportBytes =
|
|
8
|
+
export const maximumGameplayE2EReportBytes = 1_024 * 1_024;
|
|
9
9
|
export function resolveGameAcceptanceReleaseManifestFile(gameRoot, env = process.env) {
|
|
10
10
|
const configuredFile = env.MPGD_RELEASE_MANIFEST_FILE;
|
|
11
11
|
return path.resolve(gameRoot, configuredFile === undefined || configuredFile.length === 0
|
package/dist/gameplay-e2e.js
CHANGED
|
@@ -7,13 +7,13 @@ const gameplayStateIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
|
7
7
|
const gameplayKeyControlCharacterPattern = /[\u0000-\u001f\u007f]/u;
|
|
8
8
|
export const maximumGameplayE2EStates = 50;
|
|
9
9
|
const maximumActionsPerState = 100;
|
|
10
|
-
const maximumWaitMs =
|
|
11
|
-
const maximumBackgroundMs = 5 *
|
|
12
|
-
const maximumHashReadChunkBytes = 64 *
|
|
10
|
+
const maximumWaitMs = 60_000;
|
|
11
|
+
const maximumBackgroundMs = 5 * 60_000;
|
|
12
|
+
const maximumHashReadChunkBytes = 64 * 1_024;
|
|
13
13
|
const defaultGameplayE2EHashLimits = {
|
|
14
14
|
maximumDepth: 128,
|
|
15
|
-
maximumEntries:
|
|
16
|
-
maximumTotalFileBytes: 4 *
|
|
15
|
+
maximumEntries: 100_000,
|
|
16
|
+
maximumTotalFileBytes: 4 * 1_024 * 1_024 * 1_024,
|
|
17
17
|
};
|
|
18
18
|
class GameplayE2EAggregatedError extends Error {
|
|
19
19
|
errors;
|
|
@@ -966,7 +966,7 @@ function decodeCssEscapes(value) {
|
|
|
966
966
|
return plain ?? '';
|
|
967
967
|
}
|
|
968
968
|
const codePoint = Number.parseInt(hex, 16);
|
|
969
|
-
return codePoint >
|
|
969
|
+
return codePoint > 0x10_ffff || Number.isNaN(codePoint)
|
|
970
970
|
? '\uFFFD'
|
|
971
971
|
: String.fromCodePoint(codePoint);
|
|
972
972
|
});
|
|
@@ -1161,9 +1161,7 @@ function verifyCloudflarePagesHeaders(deploymentRoot, sourceFiles, deploymentFil
|
|
|
1161
1161
|
if (sourcePaths.has(file.slice(1))) {
|
|
1162
1162
|
requirements.push({
|
|
1163
1163
|
requestPath: file,
|
|
1164
|
-
expected: noStoreCacheControlFiles.has(file)
|
|
1165
|
-
? noStoreCacheControl
|
|
1166
|
-
: freshCacheControl,
|
|
1164
|
+
expected: noStoreCacheControlFiles.has(file) ? noStoreCacheControl : freshCacheControl,
|
|
1167
1165
|
label: file.slice(1),
|
|
1168
1166
|
});
|
|
1169
1167
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { createMicrosoftStorePackageAcceptanceRuntime, microsoftStorePackageAcce
|
|
|
8
8
|
export { createMicrosoftStorePackageGenerationRuntime, microsoftStorePackageGeneratorEndpoint, microsoftStorePackageGeneratorSourceRevision, microsoftStorePackageGenerationSchemaVersion, renderMicrosoftStorePackageGenerationMarkdown, runMicrosoftStorePackageGeneration, type CreateMicrosoftStorePackageGenerationRuntimeInput, type MicrosoftStoreAddressResolver, type MicrosoftStorePackageGenerationEvidence, type MicrosoftStorePackageGenerationRuntime, type MicrosoftStoreResolvedAddress, type RunMicrosoftStorePackageGenerationInput, } from './microsoft-store-package-generation.js';
|
|
9
9
|
export { microsoftStoreSubmissionSchemaVersion, parseMicrosoftStoreSubmissionConfig, renderMicrosoftStoreSubmissionMarkdown, runMicrosoftStoreSubmissionPreflight, type MicrosoftStoreSubmissionConfig, type MicrosoftStoreSubmissionCommerce, type MicrosoftStoreSubmissionCommerceProduct, type MicrosoftStoreSubmissionEvidence, type RunMicrosoftStoreSubmissionPreflightInput, } from './microsoft-store-submission.js';
|
|
10
10
|
export { assertProductionTargetReadiness, type ProductionTargetReadinessInput, } from './production-target-readiness.js';
|
|
11
|
+
export declare function mpgdCliNeedsForcedExit(): boolean;
|
|
11
12
|
export declare function runMpgdCli(args: readonly string[]): Promise<void>;
|
|
12
13
|
export declare function runCreateGameCli(args: readonly string[]): Promise<void>;
|
|
13
14
|
export declare function readCliArgs(): readonly string[];
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import completion from '@gunshi/plugin-completion';
|
|
|
6
6
|
import i18n, { defineI18n } from '@gunshi/plugin-i18n';
|
|
7
7
|
import resources from '@gunshi/resources';
|
|
8
8
|
import { cli } from 'gunshi';
|
|
9
|
+
import { buildAssetPacks } from './asset-pack-build.js';
|
|
10
|
+
import { verifyAssetPackDelivery } from './asset-pack-verify.js';
|
|
9
11
|
import { normalizeConfiguredBuildTargets, normalizeBuildTarget as normalizeConfiguredTargetName, supportedBuildTargets, } from './build-targets.js';
|
|
10
12
|
import { escapeMarkdownInline as escapeEvidenceMarkdownInline, writeEvidenceReportFiles, } from './evidence-io.js';
|
|
11
13
|
import { defaultGameAcceptanceCommandTimeoutMs, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, } from './game-acceptance.js';
|
|
@@ -85,11 +87,71 @@ const acceptanceStepIds = {
|
|
|
85
87
|
targetSmoke: 'target-smoke',
|
|
86
88
|
gameplayE2E: 'gameplay-e2e',
|
|
87
89
|
};
|
|
90
|
+
/** Whether the last run reported a deadline failure that leaves
|
|
91
|
+
* uncancellable filesystem work pending: the standalone binary forces its
|
|
92
|
+
* exit (see bin.ts); in-process library callers handle it themselves. */
|
|
93
|
+
let forcedExitPending = false;
|
|
94
|
+
/** A blocked pipe must not prevent the standalone binary from reaching its
|
|
95
|
+
* forced-exit path after a deadline report. The callback may remain queued
|
|
96
|
+
* behind a stalled reader, so the drain is best effort and bounded. */
|
|
97
|
+
const CLI_OUTPUT_DRAIN_TIMEOUT_MS = 250;
|
|
98
|
+
const drainOutput = (stream) => new Promise((resolve) => {
|
|
99
|
+
let settled = false;
|
|
100
|
+
const finish = () => {
|
|
101
|
+
if (settled) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
settled = true;
|
|
105
|
+
resolve();
|
|
106
|
+
};
|
|
107
|
+
const timer = setTimeout(finish, CLI_OUTPUT_DRAIN_TIMEOUT_MS);
|
|
108
|
+
try {
|
|
109
|
+
stream.write('', () => {
|
|
110
|
+
clearTimeout(timer);
|
|
111
|
+
finish();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
finish();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
export function mpgdCliNeedsForcedExit() {
|
|
120
|
+
return forcedExitPending;
|
|
121
|
+
}
|
|
88
122
|
export async function runMpgdCli(args) {
|
|
123
|
+
forcedExitPending = false;
|
|
89
124
|
await cli([...args], entryCommand, {
|
|
90
125
|
name: 'mpgd',
|
|
91
126
|
version: cliVersion,
|
|
127
|
+
// Machine-readable mode contract: `assets verify-delivery --json`
|
|
128
|
+
// owns stdout entirely — exactly one JSON document, parseable with
|
|
129
|
+
// JSON.parse(stdout) and nothing else. The framework's banner is
|
|
130
|
+
// suppressed for that command, and framework-level argument errors
|
|
131
|
+
// (bad value types, missing required arguments — rejected before the
|
|
132
|
+
// command handler runs) move to stderr so stdout never carries a
|
|
133
|
+
// partial document. Every other command keeps the default rendering.
|
|
134
|
+
renderHeader: async (ctx) => {
|
|
135
|
+
const values = ctx.values;
|
|
136
|
+
if (ctx.name === 'verify-delivery' && values.json === true) {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
const title = ctx.env.description || ctx.env.name || '';
|
|
140
|
+
return title
|
|
141
|
+
? `${title} (${ctx.env.name || ''}${ctx.env.version ? ` v${ctx.env.version}` : ''})`
|
|
142
|
+
: title;
|
|
143
|
+
},
|
|
144
|
+
renderValidationErrors: async (ctx, error) => {
|
|
145
|
+
const messages = error.errors.map((entry) => String(entry.message)).join('\n');
|
|
146
|
+
const values = ctx.values;
|
|
147
|
+
if (ctx.name === 'verify-delivery' && values.json === true) {
|
|
148
|
+
process.stderr.write(`${messages}\n`);
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
return messages;
|
|
152
|
+
},
|
|
92
153
|
subCommands: {
|
|
154
|
+
assets: assetsCommand,
|
|
93
155
|
game: gameCommand,
|
|
94
156
|
legal: legalCommand,
|
|
95
157
|
target: targetCommand,
|
|
@@ -143,7 +205,7 @@ const entryCommand = defineI18n({
|
|
|
143
205
|
ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
|
|
144
206
|
}),
|
|
145
207
|
run: () => {
|
|
146
|
-
console.info('Use a sub-command: game, target, kit.');
|
|
208
|
+
console.info('Use a sub-command: assets, game, legal, target, kit.');
|
|
147
209
|
console.info('Run "pnpm mpgd --help" for available commands.');
|
|
148
210
|
},
|
|
149
211
|
});
|
|
@@ -774,6 +836,226 @@ function readPositiveInteger(value, label) {
|
|
|
774
836
|
}
|
|
775
837
|
return parsed;
|
|
776
838
|
}
|
|
839
|
+
function assetPackVerifyArgs() {
|
|
840
|
+
return {
|
|
841
|
+
manifest: {
|
|
842
|
+
type: 'string',
|
|
843
|
+
required: true,
|
|
844
|
+
description: 'Path to the delivery manifest JSON document.',
|
|
845
|
+
},
|
|
846
|
+
root: {
|
|
847
|
+
type: 'string',
|
|
848
|
+
required: true,
|
|
849
|
+
description: 'Deployment root the manifest artifact paths resolve against.',
|
|
850
|
+
},
|
|
851
|
+
json: {
|
|
852
|
+
type: 'boolean',
|
|
853
|
+
required: false,
|
|
854
|
+
description: 'Print a single machine-readable JSON report to stdout.',
|
|
855
|
+
},
|
|
856
|
+
'max-object-bytes': {
|
|
857
|
+
type: 'number',
|
|
858
|
+
required: false,
|
|
859
|
+
description: 'Optional static-host limit: largest allowed deployed object in bytes.',
|
|
860
|
+
},
|
|
861
|
+
'max-files': {
|
|
862
|
+
type: 'number',
|
|
863
|
+
required: false,
|
|
864
|
+
description: 'Optional static-host limit: total regular files allowed under the root.',
|
|
865
|
+
},
|
|
866
|
+
'max-total-bytes': {
|
|
867
|
+
type: 'number',
|
|
868
|
+
required: false,
|
|
869
|
+
description: 'Optional static-host limit: total deployed bytes allowed under the root.',
|
|
870
|
+
},
|
|
871
|
+
'max-archive-bytes': {
|
|
872
|
+
type: 'number',
|
|
873
|
+
required: false,
|
|
874
|
+
description: 'Optional cap: largest zip archive read for verification, in bytes.',
|
|
875
|
+
},
|
|
876
|
+
'max-entry-bytes': {
|
|
877
|
+
type: 'number',
|
|
878
|
+
required: false,
|
|
879
|
+
description: 'Optional cap: largest expanded zip entry allowed, in bytes.',
|
|
880
|
+
},
|
|
881
|
+
'max-expanded-bytes': {
|
|
882
|
+
type: 'number',
|
|
883
|
+
required: false,
|
|
884
|
+
description: 'Optional cap: total expanded bytes allowed per archive, in bytes.',
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function assetPackBuildArgs() {
|
|
889
|
+
return {
|
|
890
|
+
config: {
|
|
891
|
+
type: 'string',
|
|
892
|
+
required: true,
|
|
893
|
+
description: 'Path to the asset pack build config JSON file.',
|
|
894
|
+
},
|
|
895
|
+
out: {
|
|
896
|
+
type: 'string',
|
|
897
|
+
required: true,
|
|
898
|
+
description: 'Output directory for delivery artifacts, outside the source root.',
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
const assetsCommand = defineI18n({
|
|
903
|
+
name: 'assets',
|
|
904
|
+
description: 'Build deterministic asset pack delivery artifacts.',
|
|
905
|
+
resource: commandResource({
|
|
906
|
+
en: 'Build deterministic asset pack delivery artifacts.',
|
|
907
|
+
ko: '결정적인 에셋 팩 전달 산출물을 빌드합니다.',
|
|
908
|
+
}),
|
|
909
|
+
subCommands: {
|
|
910
|
+
'build-packs': defineI18n({
|
|
911
|
+
name: 'build-packs',
|
|
912
|
+
description: 'Build files or ZIP delivery artifacts for configured packs.',
|
|
913
|
+
resource: commandResource({
|
|
914
|
+
en: 'Build files or ZIP delivery artifacts for configured packs.',
|
|
915
|
+
ko: '설정된 팩의 files 또는 ZIP 전달 산출물을 빌드합니다.',
|
|
916
|
+
}, {
|
|
917
|
+
config: {
|
|
918
|
+
en: 'Path to the asset pack build config JSON file.',
|
|
919
|
+
ko: '에셋 팩 빌드 설정 JSON 파일 경로.',
|
|
920
|
+
},
|
|
921
|
+
out: {
|
|
922
|
+
en: 'Output directory for delivery artifacts, outside the source root.',
|
|
923
|
+
ko: '전달 산출물 출력 디렉터리. 소스 루트 밖이어야 합니다.',
|
|
924
|
+
},
|
|
925
|
+
}),
|
|
926
|
+
args: assetPackBuildArgs(),
|
|
927
|
+
run: (ctx) => {
|
|
928
|
+
const report = buildAssetPacks({
|
|
929
|
+
configPath: ctx.values.config,
|
|
930
|
+
outDir: ctx.values.out,
|
|
931
|
+
});
|
|
932
|
+
for (const output of report.outputs) {
|
|
933
|
+
console.info(`${output.action === 'written' ? 'wrote' : 'unchanged'} ${output.path} `
|
|
934
|
+
+ `(${output.bytes} bytes, sha256:${output.sha256})`);
|
|
935
|
+
}
|
|
936
|
+
for (const archive of report.archives) {
|
|
937
|
+
console.info(`archive ${archive.path}: ${archive.entryCount} entries `
|
|
938
|
+
+ `(${archive.storeEntries} store, ${archive.deflateEntries} deflate), `
|
|
939
|
+
+ `sources ${archive.sourceBytes} bytes, archive ${archive.archiveBytes} bytes, `
|
|
940
|
+
+ `sha256:${archive.sha256}`);
|
|
941
|
+
}
|
|
942
|
+
console.info(`Asset pack delivery manifest: ${report.manifestPath}`);
|
|
943
|
+
},
|
|
944
|
+
}),
|
|
945
|
+
'verify-delivery': defineI18n({
|
|
946
|
+
name: 'verify-delivery',
|
|
947
|
+
description: 'Verify delivery artifacts read-only against the manifest.',
|
|
948
|
+
resource: commandResource({
|
|
949
|
+
en: 'Verify delivery artifacts read-only against the manifest.',
|
|
950
|
+
ko: '전달 산출물을 manifest 기준으로 읽기 전용으로 검증합니다.',
|
|
951
|
+
}, {
|
|
952
|
+
manifest: {
|
|
953
|
+
en: 'Path to the delivery manifest JSON document.',
|
|
954
|
+
ko: '전달 manifest JSON 문서 경로.',
|
|
955
|
+
},
|
|
956
|
+
root: {
|
|
957
|
+
en: 'Deployment root the manifest artifact paths resolve against.',
|
|
958
|
+
ko: 'manifest 산출물 경로의 기준이 되는 배포 루트 디렉터리.',
|
|
959
|
+
},
|
|
960
|
+
json: {
|
|
961
|
+
en: 'Print a single machine-readable JSON report to stdout.',
|
|
962
|
+
ko: '기계 판독 가능한 JSON 보고서 하나를 stdout에 출력합니다.',
|
|
963
|
+
},
|
|
964
|
+
'max-object-bytes': {
|
|
965
|
+
en: 'Optional static-host limit: largest allowed deployed object in bytes.',
|
|
966
|
+
ko: '선택적 정적 호스트 한도: 허용되는 최대 배포 object 바이트 수.',
|
|
967
|
+
},
|
|
968
|
+
'max-files': {
|
|
969
|
+
en: 'Optional static-host limit: total regular files allowed under the root.',
|
|
970
|
+
ko: '선택적 정적 호스트 한도: 루트 아래 허용되는 일반 파일 총 개수.',
|
|
971
|
+
},
|
|
972
|
+
'max-total-bytes': {
|
|
973
|
+
en: 'Optional static-host limit: total deployed bytes allowed under the root.',
|
|
974
|
+
ko: '선택적 정적 호스트 한도: 루트 아래 허용되는 총 배포 바이트 수.',
|
|
975
|
+
},
|
|
976
|
+
'max-archive-bytes': {
|
|
977
|
+
en: 'Optional cap: largest zip archive read for verification, in bytes.',
|
|
978
|
+
ko: '선택적 상한: 검증 시 읽는 zip 아카이브의 최대 바이트 수.',
|
|
979
|
+
},
|
|
980
|
+
'max-entry-bytes': {
|
|
981
|
+
en: 'Optional cap: largest expanded zip entry allowed, in bytes.',
|
|
982
|
+
ko: '선택적 상한: 허용되는 확장(zip 해제) 엔트리 최대 바이트 수.',
|
|
983
|
+
},
|
|
984
|
+
'max-expanded-bytes': {
|
|
985
|
+
en: 'Optional cap: total expanded bytes allowed per archive, in bytes.',
|
|
986
|
+
ko: '선택적 상한: 아카이브별 허용되는 총 확장 바이트 수.',
|
|
987
|
+
},
|
|
988
|
+
}),
|
|
989
|
+
args: assetPackVerifyArgs(),
|
|
990
|
+
run: async (ctx) => {
|
|
991
|
+
const hostLimits = {
|
|
992
|
+
...(ctx.values['max-object-bytes'] === undefined
|
|
993
|
+
? {}
|
|
994
|
+
: { maxObjectBytes: ctx.values['max-object-bytes'] }),
|
|
995
|
+
...(ctx.values['max-files'] === undefined
|
|
996
|
+
? {}
|
|
997
|
+
: { maxFiles: ctx.values['max-files'] }),
|
|
998
|
+
...(ctx.values['max-total-bytes'] === undefined
|
|
999
|
+
? {}
|
|
1000
|
+
: { maxTotalBytes: ctx.values['max-total-bytes'] }),
|
|
1001
|
+
};
|
|
1002
|
+
const report = await verifyAssetPackDelivery({
|
|
1003
|
+
manifestPath: ctx.values.manifest,
|
|
1004
|
+
root: ctx.values.root,
|
|
1005
|
+
...(Object.keys(hostLimits).length > 0 ? { hostLimits } : {}),
|
|
1006
|
+
...(ctx.values['max-archive-bytes'] === undefined
|
|
1007
|
+
? {}
|
|
1008
|
+
: { maxArchiveBytes: ctx.values['max-archive-bytes'] }),
|
|
1009
|
+
...(ctx.values['max-entry-bytes'] === undefined
|
|
1010
|
+
? {}
|
|
1011
|
+
: { maxEntryBytes: ctx.values['max-entry-bytes'] }),
|
|
1012
|
+
...(ctx.values['max-expanded-bytes'] === undefined
|
|
1013
|
+
? {}
|
|
1014
|
+
: { maxExpandedBytes: ctx.values['max-expanded-bytes'] }),
|
|
1015
|
+
});
|
|
1016
|
+
if (ctx.values.json === true) {
|
|
1017
|
+
console.info(JSON.stringify(report, null, 2));
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
for (const archive of report.archives) {
|
|
1021
|
+
console.info(`verified archive ${archive.packId}: ${archive.entries} entries, ${archive.expandedBytes} expanded bytes`);
|
|
1022
|
+
}
|
|
1023
|
+
console.info(`manifest ${report.manifest.sha256.slice(0, 12)}… (${report.manifest.bytes} bytes, ${report.manifest.packs} packs)`);
|
|
1024
|
+
console.info(`referenced objects: ${report.referenced.files} files, ${report.referenced.bytes} bytes`);
|
|
1025
|
+
if (report.inventory !== undefined) {
|
|
1026
|
+
console.info(`root inventory: ${report.inventory.files} files, ${report.inventory.bytes} bytes${report.inventory.truncated ? ' (truncated)' : ''}`);
|
|
1027
|
+
}
|
|
1028
|
+
for (const failure of report.failures) {
|
|
1029
|
+
console.error(`[${failure.stage}/${failure.code}] ${failure.message}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (report.ok) {
|
|
1032
|
+
console.info('Delivery verification passed.');
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
console.error('Delivery verification failed; see the failures above.');
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (report.failures.some((failure) => failure.code === 'deadline')) {
|
|
1039
|
+
// Piped console output is asynchronous; drain both streams so a
|
|
1040
|
+
// large report is not truncated by the binary's forced exit.
|
|
1041
|
+
await Promise.all([drainOutput(process.stdout), drainOutput(process.stderr)]);
|
|
1042
|
+
// Stalled filesystem requests cannot be cancelled once the
|
|
1043
|
+
// deadline fires; the standalone binary forces its exit so a
|
|
1044
|
+
// pending threadpool call cannot keep the process (and its
|
|
1045
|
+
// leaked handle) alive past the reported budget. In-process
|
|
1046
|
+
// callers of runMpgdCli observe mpgdCliNeedsForcedExit instead.
|
|
1047
|
+
forcedExitPending = true;
|
|
1048
|
+
}
|
|
1049
|
+
if (!report.ok) {
|
|
1050
|
+
process.exitCode = 1;
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
}),
|
|
1054
|
+
},
|
|
1055
|
+
run: () => {
|
|
1056
|
+
console.info('Use "mpgd assets build-packs" or "mpgd assets verify-delivery".');
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
777
1059
|
const legalCommand = defineI18n({
|
|
778
1060
|
name: 'legal',
|
|
779
1061
|
description: 'Build static legal/support pages for store and release evidence.',
|
|
@@ -1910,9 +2192,7 @@ function createGameApp(input) {
|
|
|
1910
2192
|
relativePath: file.relativePath,
|
|
1911
2193
|
content: file.content,
|
|
1912
2194
|
});
|
|
1913
|
-
return prepared === undefined
|
|
1914
|
-
? []
|
|
1915
|
-
: [{ ...file, content: renderTemplate(prepared, context) }];
|
|
2195
|
+
return prepared === undefined ? [] : [{ ...file, content: renderTemplate(prepared, context) }];
|
|
1916
2196
|
});
|
|
1917
2197
|
if (input.dryRun) {
|
|
1918
2198
|
console.info(`Would create ${path.relative(process.cwd(), appDir) || appDir}:`);
|
|
@@ -2003,8 +2283,17 @@ function createTemplateContext(input) {
|
|
|
2003
2283
|
if (input.workspace && kitPath === undefined) {
|
|
2004
2284
|
throw new Error('Could not detect an mpgd-kit checkout for --workspace. Pass --kit-path or set MPGD_KIT_PATH.');
|
|
2005
2285
|
}
|
|
2006
|
-
const
|
|
2286
|
+
const relativeKitPath = path.relative(input.appDir, kitPath ?? input.appDir);
|
|
2287
|
+
if (input.workspace && path.isAbsolute(relativeKitPath)) {
|
|
2288
|
+
throw new Error('--workspace requires the game and mpgd-kit to share a filesystem root.');
|
|
2289
|
+
}
|
|
2290
|
+
const workspaceRoot = toTemplatePath(relativeKitPath || '.');
|
|
2007
2291
|
const workspacePrefix = workspaceRoot;
|
|
2292
|
+
// The normalized relative path's parent hops reach the common source root.
|
|
2293
|
+
// Do not inherit the kit's narrower root when the game is a sibling checkout.
|
|
2294
|
+
const tsconfigRootDir = input.workspace
|
|
2295
|
+
? workspacePrefix.split('/').filter((segment) => segment === '..').join('/') || '.'
|
|
2296
|
+
: '.';
|
|
2008
2297
|
const mpgdDependencyVersionReplacements = resolveMpgdDependencyVersionReplacements({
|
|
2009
2298
|
...(input.dependencyVersion === undefined
|
|
2010
2299
|
? {}
|
|
@@ -2025,6 +2314,7 @@ function createTemplateContext(input) {
|
|
|
2025
2314
|
tsconfigExtendsLine: input.workspace
|
|
2026
2315
|
? ` "extends": "${workspacePrefix}/tsconfig.base.json",`
|
|
2027
2316
|
: '',
|
|
2317
|
+
tsconfigRootDir,
|
|
2028
2318
|
tsconfigWorkspaceIncludes: input.workspace
|
|
2029
2319
|
? [
|
|
2030
2320
|
`,\n "${workspacePrefix}/packages/**/*.ts"`,
|
|
@@ -2142,6 +2432,7 @@ function renderTemplate(content, context) {
|
|
|
2142
2432
|
.replaceAll('__PACKAGE_NAME__', context.packageName)
|
|
2143
2433
|
.replaceAll('__MPGD_DEPENDENCY_VERSION__', context.defaultDependencyVersion)
|
|
2144
2434
|
.replaceAll('__TSCONFIG_EXTENDS_LINE__', context.tsconfigExtendsLine)
|
|
2435
|
+
.replaceAll('__TSCONFIG_ROOT_DIR__', context.tsconfigRootDir)
|
|
2145
2436
|
.replaceAll('__TSCONFIG_WORKSPACE_INCLUDES__', context.tsconfigWorkspaceIncludes)
|
|
2146
2437
|
.replaceAll('__TSCONFIG_WORKSPACE_EXCLUDES__', context.tsconfigWorkspaceExcludes)
|
|
2147
2438
|
.replaceAll('__WORKSPACE_I18N_BUILD_PREFIX__', context.workspaceI18nBuildPrefix)
|
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsolute, } from './evidence-io.js';
|
|
7
7
|
export const microsoftStorePackageAcceptanceSchemaVersion = 1;
|
|
8
8
|
const supportedPackageExtensions = ['.appx', '.appxbundle', '.msix', '.msixbundle'];
|
|
9
|
-
const maximumWindowsPackageCommandDurationMs = 60 * 60 *
|
|
9
|
+
const maximumWindowsPackageCommandDurationMs = 60 * 60 * 1_000;
|
|
10
10
|
const maximumWindowsPackageCommandOutputBytes = 16 * 1024 * 1024;
|
|
11
11
|
const maximumWindowsPackageCommandDiagnosticCharacters = 8 * 1024;
|
|
12
12
|
const xmlNamedEntities = {
|
|
@@ -11,9 +11,9 @@ import { assertMicrosoftStorePackageZip } from './microsoft-store-package-genera
|
|
|
11
11
|
const maximumManifestBytes = 1024 * 1024;
|
|
12
12
|
const maximumManifestIconBytes = 2 * 1024 * 1024;
|
|
13
13
|
const maximumArchiveBytes = 512 * 1024 * 1024;
|
|
14
|
-
const manifestRequestTimeoutMs =
|
|
15
|
-
const manifestIconRequestTimeoutMs =
|
|
16
|
-
const packageRequestTimeoutMs = 10 * 60 *
|
|
14
|
+
const manifestRequestTimeoutMs = 30_000;
|
|
15
|
+
const manifestIconRequestTimeoutMs = 30_000;
|
|
16
|
+
const packageRequestTimeoutMs = 10 * 60 * 1_000;
|
|
17
17
|
const blockedIpv4Addresses = createBlockedIpv4Addresses();
|
|
18
18
|
const blockedIpv6Addresses = createBlockedIpv6Addresses();
|
|
19
19
|
const allowedIpv6GlobalUnicastAddresses = createAllowedIpv6GlobalUnicastAddresses();
|
|
@@ -261,7 +261,7 @@ function requireStorePackageVersion(input, label) {
|
|
|
261
261
|
}
|
|
262
262
|
const numbers = parts.map(Number);
|
|
263
263
|
if (numbers[0] === 0
|
|
264
|
-
|| numbers.some((part) => part >
|
|
264
|
+
|| numbers.some((part) => part > 65_535)
|
|
265
265
|
|| (numbers[3] ?? 0) !== 0) {
|
|
266
266
|
throw new Error(`${label} must start from 1, keep every part at or below 65535, and end in .0.`);
|
|
267
267
|
}
|
|
@@ -10,7 +10,7 @@ export function assertMicrosoftStorePackageZip(file, sizeBytes) {
|
|
|
10
10
|
if (header.readUInt32LE(0) !== 0x04034b50) {
|
|
11
11
|
throw new Error('PWABuilder package archive must start with a ZIP local file header.');
|
|
12
12
|
}
|
|
13
|
-
const tailLength = Math.min(sizeBytes,
|
|
13
|
+
const tailLength = Math.min(sizeBytes, 65_557);
|
|
14
14
|
const tail = Buffer.allocUnsafe(tailLength);
|
|
15
15
|
readExactly(descriptor, tail, sizeBytes - tailLength);
|
|
16
16
|
const endOffset = findEndOfCentralDirectory(tail);
|
|
@@ -7,7 +7,7 @@ import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsol
|
|
|
7
7
|
import { isMicrosoftStoreSupportedListingLocale } from './microsoft-store-supported-locales.js';
|
|
8
8
|
export const microsoftStoreSubmissionSchemaVersion = 1;
|
|
9
9
|
// Microsoft Store listing and package identity limits documented by Microsoft Learn.
|
|
10
|
-
const maximumStoreDescriptionCharacters =
|
|
10
|
+
const maximumStoreDescriptionCharacters = 10_000;
|
|
11
11
|
const maximumStoreDesktopScreenshots = 10;
|
|
12
12
|
const maximumStoreManifestIcons = 32;
|
|
13
13
|
const maximumStoreScreenshotBytes = 50 * 1024 * 1024;
|
|
@@ -907,20 +907,20 @@ function pngPassLength(size, start, step) {
|
|
|
907
907
|
return size <= start ? 0 : Math.ceil((size - start) / step);
|
|
908
908
|
}
|
|
909
909
|
function crc32Png(parts) {
|
|
910
|
-
let crc =
|
|
910
|
+
let crc = 0xffff_ffff;
|
|
911
911
|
for (const part of parts) {
|
|
912
912
|
for (const byte of part) {
|
|
913
913
|
crc = (crc >>> 8) ^ (pngCrc32Table[(crc ^ byte) & 0xff] ?? 0);
|
|
914
914
|
}
|
|
915
915
|
}
|
|
916
|
-
return (crc ^
|
|
916
|
+
return (crc ^ 0xffff_ffff) >>> 0;
|
|
917
917
|
}
|
|
918
918
|
function createPngCrc32Table() {
|
|
919
919
|
const table = new Uint32Array(256);
|
|
920
920
|
for (let value = 0; value < table.length; value += 1) {
|
|
921
921
|
let crc = value;
|
|
922
922
|
for (let bit = 0; bit < 8; bit += 1) {
|
|
923
|
-
crc = (crc >>> 1) ^ (crc & 1 ?
|
|
923
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb8_8320 : 0);
|
|
924
924
|
}
|
|
925
925
|
table[value] = crc >>> 0;
|
|
926
926
|
}
|