@mpgd/cli 0.13.0 → 0.14.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/index.d.ts +3 -0
- package/dist/index.js +363 -3
- package/dist/microsoft-store-package-acceptance.d.ts +54 -0
- package/dist/microsoft-store-package-acceptance.js +787 -0
- package/dist/microsoft-store-package-generation-contract.d.ts +129 -0
- package/dist/microsoft-store-package-generation-contract.js +3 -0
- package/dist/microsoft-store-package-generation-download.d.ts +22 -0
- package/dist/microsoft-store-package-generation-download.js +453 -0
- package/dist/microsoft-store-package-generation-input.d.ts +3 -0
- package/dist/microsoft-store-package-generation-input.js +384 -0
- package/dist/microsoft-store-package-generation-integrity.d.ts +5 -0
- package/dist/microsoft-store-package-generation-integrity.js +100 -0
- package/dist/microsoft-store-package-generation-manifest.d.ts +3 -0
- package/dist/microsoft-store-package-generation-manifest.js +67 -0
- package/dist/microsoft-store-package-generation-zip.d.ts +1 -0
- package/dist/microsoft-store-package-generation-zip.js +180 -0
- package/dist/microsoft-store-package-generation.d.ts +24 -0
- package/dist/microsoft-store-package-generation.js +430 -0
- package/dist/microsoft-store-submission.d.ts +79 -0
- package/dist/microsoft-store-submission.js +952 -0
- package/dist/microsoft-store-supported-locales.d.ts +1 -0
- package/dist/microsoft-store-supported-locales.js +33 -0
- package/package.json +6 -5
- package/templates/phaser-game/README.md +38 -0
- package/templates/phaser-game/gitignore +1 -0
- package/templates/phaser-game/mpgd.microsoft-store.json +29 -0
- package/templates/phaser-game/package.json +2 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync } from 'node:fs';
|
|
2
|
+
export function assertMicrosoftStorePackageZip(file, sizeBytes) {
|
|
3
|
+
if (sizeBytes < 22) {
|
|
4
|
+
throw new Error('PWABuilder package archive is truncated.');
|
|
5
|
+
}
|
|
6
|
+
const descriptor = openSync(file, 'r');
|
|
7
|
+
try {
|
|
8
|
+
const header = Buffer.allocUnsafe(4);
|
|
9
|
+
readExactly(descriptor, header, 0);
|
|
10
|
+
if (header.readUInt32LE(0) !== 0x04034b50) {
|
|
11
|
+
throw new Error('PWABuilder package archive must start with a ZIP local file header.');
|
|
12
|
+
}
|
|
13
|
+
const tailLength = Math.min(sizeBytes, 65557);
|
|
14
|
+
const tail = Buffer.allocUnsafe(tailLength);
|
|
15
|
+
readExactly(descriptor, tail, sizeBytes - tailLength);
|
|
16
|
+
const endOffset = findEndOfCentralDirectory(tail);
|
|
17
|
+
if (endOffset < 0) {
|
|
18
|
+
throw new Error('PWABuilder package archive is missing its ZIP end record.');
|
|
19
|
+
}
|
|
20
|
+
const commentLength = tail.readUInt16LE(endOffset + 20);
|
|
21
|
+
if (endOffset + 22 + commentLength !== tail.length) {
|
|
22
|
+
throw new Error('PWABuilder package archive has a truncated or trailing ZIP end record.');
|
|
23
|
+
}
|
|
24
|
+
const diskNumber = tail.readUInt16LE(endOffset + 4);
|
|
25
|
+
const centralDirectoryDisk = tail.readUInt16LE(endOffset + 6);
|
|
26
|
+
const entriesOnDisk = tail.readUInt16LE(endOffset + 8);
|
|
27
|
+
const totalEntries = tail.readUInt16LE(endOffset + 10);
|
|
28
|
+
const centralDirectorySize = tail.readUInt32LE(endOffset + 12);
|
|
29
|
+
const centralDirectoryOffset = tail.readUInt32LE(endOffset + 16);
|
|
30
|
+
const absoluteEndOffset = sizeBytes - tailLength + endOffset;
|
|
31
|
+
if (diskNumber === 0xffff
|
|
32
|
+
|| centralDirectoryDisk === 0xffff
|
|
33
|
+
|| entriesOnDisk === 0xffff
|
|
34
|
+
|| totalEntries === 0xffff
|
|
35
|
+
|| centralDirectorySize === 0xffffffff
|
|
36
|
+
|| centralDirectoryOffset === 0xffffffff) {
|
|
37
|
+
throw new Error('PWABuilder package archive must not use ZIP64 extensions.');
|
|
38
|
+
}
|
|
39
|
+
if (diskNumber !== 0
|
|
40
|
+
|| centralDirectoryDisk !== 0
|
|
41
|
+
|| entriesOnDisk === 0
|
|
42
|
+
|| entriesOnDisk !== totalEntries
|
|
43
|
+
|| centralDirectoryOffset + centralDirectorySize !== absoluteEndOffset) {
|
|
44
|
+
throw new Error('PWABuilder package archive has an unsupported ZIP directory layout.');
|
|
45
|
+
}
|
|
46
|
+
assertSafeZipEntries(descriptor, centralDirectoryOffset, absoluteEndOffset, totalEntries);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
closeSync(descriptor);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function assertSafeZipEntries(descriptor, centralDirectoryOffset, centralDirectoryEnd, totalEntries) {
|
|
53
|
+
let cursor = centralDirectoryOffset;
|
|
54
|
+
const seenEntryNames = new Set();
|
|
55
|
+
for (let index = 0; index < totalEntries; index += 1) {
|
|
56
|
+
const header = Buffer.allocUnsafe(46);
|
|
57
|
+
if (cursor + header.length > centralDirectoryEnd) {
|
|
58
|
+
throw new Error('PWABuilder package archive has a truncated ZIP central directory.');
|
|
59
|
+
}
|
|
60
|
+
readExactly(descriptor, header, cursor);
|
|
61
|
+
if (header.readUInt32LE(0) !== 0x02014b50) {
|
|
62
|
+
throw new Error('PWABuilder package archive has an invalid ZIP central directory.');
|
|
63
|
+
}
|
|
64
|
+
const fileNameLength = header.readUInt16LE(28);
|
|
65
|
+
const extraLength = header.readUInt16LE(30);
|
|
66
|
+
const commentLength = header.readUInt16LE(32);
|
|
67
|
+
const diskStart = header.readUInt16LE(34);
|
|
68
|
+
const flags = header.readUInt16LE(8);
|
|
69
|
+
const compressionMethod = header.readUInt16LE(10);
|
|
70
|
+
const compressedSize = header.readUInt32LE(20);
|
|
71
|
+
const uncompressedSize = header.readUInt32LE(24);
|
|
72
|
+
const localHeaderOffset = header.readUInt32LE(42);
|
|
73
|
+
const entryLength = header.length + fileNameLength + extraLength + commentLength;
|
|
74
|
+
if (diskStart === 0xffff
|
|
75
|
+
|| compressedSize === 0xffffffff
|
|
76
|
+
|| uncompressedSize === 0xffffffff
|
|
77
|
+
|| localHeaderOffset === 0xffffffff) {
|
|
78
|
+
throw new Error('PWABuilder package archive must not use ZIP64 extensions.');
|
|
79
|
+
}
|
|
80
|
+
if (diskStart !== 0) {
|
|
81
|
+
throw new Error('PWABuilder package archive must keep every entry on disk 0.');
|
|
82
|
+
}
|
|
83
|
+
if (cursor + entryLength > centralDirectoryEnd) {
|
|
84
|
+
throw new Error('PWABuilder package archive has a truncated ZIP central directory entry.');
|
|
85
|
+
}
|
|
86
|
+
const fileNameBytes = Buffer.allocUnsafe(fileNameLength);
|
|
87
|
+
readExactly(descriptor, fileNameBytes, cursor + header.length);
|
|
88
|
+
const fileName = fileNameBytes.toString('utf8');
|
|
89
|
+
assertSafeZipEntryName(fileName);
|
|
90
|
+
const canonicalName = canonicalizeZipEntryName(fileName);
|
|
91
|
+
if (seenEntryNames.has(canonicalName)) {
|
|
92
|
+
throw new Error(`PWABuilder package archive contains a duplicate or case-colliding ZIP entry: ${fileName}`);
|
|
93
|
+
}
|
|
94
|
+
seenEntryNames.add(canonicalName);
|
|
95
|
+
const unixMode = (header.readUInt32LE(38) >>> 16) & 0o170000;
|
|
96
|
+
if (unixMode === 0o120000) {
|
|
97
|
+
throw new Error('PWABuilder package archive must not contain symbolic links.');
|
|
98
|
+
}
|
|
99
|
+
if (localHeaderOffset + 30 > centralDirectoryOffset) {
|
|
100
|
+
throw new Error('PWABuilder package archive has an invalid ZIP local header offset.');
|
|
101
|
+
}
|
|
102
|
+
const localHeader = Buffer.allocUnsafe(30);
|
|
103
|
+
readExactly(descriptor, localHeader, localHeaderOffset);
|
|
104
|
+
if (localHeader.readUInt32LE(0) !== 0x04034b50) {
|
|
105
|
+
throw new Error('PWABuilder package archive has an invalid ZIP local file header.');
|
|
106
|
+
}
|
|
107
|
+
const localFileNameLength = localHeader.readUInt16LE(26);
|
|
108
|
+
const localExtraLength = localHeader.readUInt16LE(28);
|
|
109
|
+
const localFlags = localHeader.readUInt16LE(6);
|
|
110
|
+
const localCompressionMethod = localHeader.readUInt16LE(8);
|
|
111
|
+
const localCompressedSize = localHeader.readUInt32LE(18);
|
|
112
|
+
const localUncompressedSize = localHeader.readUInt32LE(22);
|
|
113
|
+
const localMetadataEnd = localHeaderOffset + localHeader.length
|
|
114
|
+
+ localFileNameLength
|
|
115
|
+
+ localExtraLength;
|
|
116
|
+
if (localCompressedSize === 0xffffffff || localUncompressedSize === 0xffffffff) {
|
|
117
|
+
throw new Error('PWABuilder package archive must not use ZIP64 extensions.');
|
|
118
|
+
}
|
|
119
|
+
const usesDataDescriptor = (flags & 0x0008) !== 0;
|
|
120
|
+
if (localFlags !== flags
|
|
121
|
+
|| localCompressionMethod !== compressionMethod
|
|
122
|
+
|| (!usesDataDescriptor
|
|
123
|
+
&& (localCompressedSize !== compressedSize
|
|
124
|
+
|| localUncompressedSize !== uncompressedSize))
|
|
125
|
+
|| (usesDataDescriptor
|
|
126
|
+
&& ((localCompressedSize !== 0 && localCompressedSize !== compressedSize)
|
|
127
|
+
|| (localUncompressedSize !== 0 && localUncompressedSize !== uncompressedSize)))) {
|
|
128
|
+
throw new Error('PWABuilder package archive local metadata must match its central directory entry.');
|
|
129
|
+
}
|
|
130
|
+
if (localMetadataEnd > centralDirectoryOffset
|
|
131
|
+
|| localMetadataEnd + compressedSize > centralDirectoryOffset) {
|
|
132
|
+
throw new Error('PWABuilder package archive has a truncated ZIP local file entry.');
|
|
133
|
+
}
|
|
134
|
+
const localFileNameBytes = Buffer.allocUnsafe(localFileNameLength);
|
|
135
|
+
readExactly(descriptor, localFileNameBytes, localHeaderOffset + localHeader.length);
|
|
136
|
+
assertSafeZipEntryName(localFileNameBytes.toString('utf8'));
|
|
137
|
+
if (!localFileNameBytes.equals(fileNameBytes)) {
|
|
138
|
+
throw new Error('PWABuilder package archive local filename must match its central directory entry.');
|
|
139
|
+
}
|
|
140
|
+
cursor += entryLength;
|
|
141
|
+
}
|
|
142
|
+
if (cursor !== centralDirectoryEnd) {
|
|
143
|
+
throw new Error('PWABuilder package archive has an inconsistent ZIP central directory.');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function canonicalizeZipEntryName(input) {
|
|
147
|
+
return input.replaceAll('\\', '/').replace(/\/+$/u, '').normalize('NFC').toLowerCase();
|
|
148
|
+
}
|
|
149
|
+
function assertSafeZipEntryName(input) {
|
|
150
|
+
const normalized = input.replaceAll('\\', '/');
|
|
151
|
+
const components = normalized.split('/');
|
|
152
|
+
if (input.length === 0
|
|
153
|
+
|| input.includes('\0')
|
|
154
|
+
|| normalized.startsWith('/')
|
|
155
|
+
|| /^[A-Za-z]:/u.test(normalized)
|
|
156
|
+
|| components.some((component) => component.includes(':'))
|
|
157
|
+
|| components.some((component, index) => (component === '.'
|
|
158
|
+
|| component === '..'
|
|
159
|
+
|| (component.length === 0 && index !== components.length - 1)))) {
|
|
160
|
+
throw new Error(`PWABuilder package archive contains an unsafe ZIP entry path: ${input}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function findEndOfCentralDirectory(input) {
|
|
164
|
+
for (let offset = input.length - 22; offset >= 0; offset -= 1) {
|
|
165
|
+
if (input.readUInt32LE(offset) === 0x06054b50) {
|
|
166
|
+
return offset;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return -1;
|
|
170
|
+
}
|
|
171
|
+
function readExactly(descriptor, output, position) {
|
|
172
|
+
let offset = 0;
|
|
173
|
+
while (offset < output.length) {
|
|
174
|
+
const bytesRead = readSync(descriptor, output, offset, output.length - offset, position + offset);
|
|
175
|
+
if (bytesRead === 0) {
|
|
176
|
+
throw new Error('PWABuilder package archive is truncated.');
|
|
177
|
+
}
|
|
178
|
+
offset += bytesRead;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type MicrosoftStoreFileSnapshot, type MicrosoftStorePackageGenerationEvidence, type MicrosoftStorePackageGenerationRuntime, type RunMicrosoftStorePackageGenerationInput } from './microsoft-store-package-generation-contract.js';
|
|
2
|
+
export { microsoftStorePackageGenerationSchemaVersion, microsoftStorePackageGeneratorEndpoint, microsoftStorePackageGeneratorSourceRevision, type CreateMicrosoftStorePackageGenerationRuntimeInput, type MicrosoftStoreAddressResolver, type MicrosoftStorePackageGenerationEvidence, type MicrosoftStorePackageGenerationRuntime, type MicrosoftStoreResolvedAddress, type RunMicrosoftStorePackageGenerationInput, } from './microsoft-store-package-generation-contract.js';
|
|
3
|
+
export { createMicrosoftStorePackageGenerationRuntime } from './microsoft-store-package-generation-download.js';
|
|
4
|
+
export declare function runMicrosoftStorePackageGeneration(input: RunMicrosoftStorePackageGenerationInput, runtime?: MicrosoftStorePackageGenerationRuntime): Promise<MicrosoftStorePackageGenerationEvidence>;
|
|
5
|
+
interface MicrosoftStoreFileNodeIdentity {
|
|
6
|
+
readonly dev: number;
|
|
7
|
+
readonly ino: number;
|
|
8
|
+
}
|
|
9
|
+
interface MicrosoftStoreEvidenceFileIdentity extends MicrosoftStoreFileNodeIdentity, MicrosoftStoreFileSnapshot {
|
|
10
|
+
readonly mtimeMs: number;
|
|
11
|
+
readonly ctimeMs: number;
|
|
12
|
+
}
|
|
13
|
+
interface MicrosoftStoreEvidencePublicationHooks {
|
|
14
|
+
readonly afterPlacement?: (file: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function writeMicrosoftStorePackageGenerationEvidenceFiles(input: {
|
|
17
|
+
readonly jsonFile: string;
|
|
18
|
+
readonly markdownFile: string;
|
|
19
|
+
readonly report: unknown;
|
|
20
|
+
readonly markdown: string;
|
|
21
|
+
}, hooks?: MicrosoftStoreEvidencePublicationHooks): void;
|
|
22
|
+
export declare function microsoftStoreEvidenceFileMatchesIdentity(file: string, expected: MicrosoftStoreEvidenceFileIdentity): boolean;
|
|
23
|
+
export declare function removeMicrosoftStoreEvidenceLockIfOwned(file: string, expected: MicrosoftStoreFileNodeIdentity | undefined, token: string | undefined): void;
|
|
24
|
+
export declare function renderMicrosoftStorePackageGenerationMarkdown(evidence: MicrosoftStorePackageGenerationEvidence): string;
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, copyFileSync, existsSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsolute, } from './evidence-io.js';
|
|
5
|
+
import { microsoftStorePackageGenerationSchemaVersion, microsoftStorePackageGeneratorEndpoint, microsoftStorePackageGeneratorSourceRevision, } from './microsoft-store-package-generation-contract.js';
|
|
6
|
+
import { createMicrosoftStorePackageGenerationRuntime, withMicrosoftStorePackageArchive, } from './microsoft-store-package-generation-download.js';
|
|
7
|
+
import { assertMicrosoftStorePackageGenerationInputUnchanged, prepareMicrosoftStorePackageGenerationInput, } from './microsoft-store-package-generation-input.js';
|
|
8
|
+
import { hashMicrosoftStoreBytes, hashMicrosoftStoreFileSnapshot, } from './microsoft-store-package-generation-integrity.js';
|
|
9
|
+
export { microsoftStorePackageGenerationSchemaVersion, microsoftStorePackageGeneratorEndpoint, microsoftStorePackageGeneratorSourceRevision, } from './microsoft-store-package-generation-contract.js';
|
|
10
|
+
export { createMicrosoftStorePackageGenerationRuntime } from './microsoft-store-package-generation-download.js';
|
|
11
|
+
export async function runMicrosoftStorePackageGeneration(input, runtime = createMicrosoftStorePackageGenerationRuntime()) {
|
|
12
|
+
const prepared = prepareMicrosoftStorePackageGenerationInput(input);
|
|
13
|
+
const pinnedManifest = createMicrosoftStoreGeneratorManifest(prepared.submission.manifest, prepared.submission.manifestIcons.map((icon) => icon.url));
|
|
14
|
+
const request = {
|
|
15
|
+
name: prepared.submission.identity.reservedName,
|
|
16
|
+
packageId: prepared.submission.identity.packageId,
|
|
17
|
+
applicationId: 'App',
|
|
18
|
+
url: prepared.pwaUrl,
|
|
19
|
+
version: prepared.modernVersion,
|
|
20
|
+
allowSigning: true,
|
|
21
|
+
publisher: {
|
|
22
|
+
displayName: prepared.submission.identity.publisherDisplayName,
|
|
23
|
+
commonName: prepared.submission.identity.publisherId,
|
|
24
|
+
},
|
|
25
|
+
generateModernPackage: true,
|
|
26
|
+
classicPackage: {
|
|
27
|
+
generate: true,
|
|
28
|
+
version: prepared.classicVersion,
|
|
29
|
+
url: prepared.pwaUrl,
|
|
30
|
+
},
|
|
31
|
+
edgeChannel: 'stable',
|
|
32
|
+
manifestUrl: prepared.manifestUrl,
|
|
33
|
+
manifest: pinnedManifest,
|
|
34
|
+
resourceLanguage: prepared.submission.resourceLanguage,
|
|
35
|
+
targetDeviceFamilies: ['Desktop'],
|
|
36
|
+
usePwaBuilderWithCustomManifest: true,
|
|
37
|
+
};
|
|
38
|
+
const requestBody = JSON.stringify(request);
|
|
39
|
+
const requestSha256 = hashMicrosoftStoreBytes(Buffer.from(requestBody));
|
|
40
|
+
return withMicrosoftStorePackageArchive({
|
|
41
|
+
runtime,
|
|
42
|
+
manifestUrl: prepared.manifestUrl,
|
|
43
|
+
manifestSha256: prepared.submission.manifestSha256,
|
|
44
|
+
manifestIcons: prepared.submission.manifestIcons,
|
|
45
|
+
requestBody,
|
|
46
|
+
outputFile: prepared.outputFile,
|
|
47
|
+
assertInputsUnchanged: () => {
|
|
48
|
+
assertMicrosoftStorePackageGenerationInputUnchanged(prepared);
|
|
49
|
+
},
|
|
50
|
+
}, (archive) => {
|
|
51
|
+
const evidence = {
|
|
52
|
+
schemaVersion: microsoftStorePackageGenerationSchemaVersion,
|
|
53
|
+
target: 'microsoft-store',
|
|
54
|
+
pwaUrl: prepared.pwaUrl,
|
|
55
|
+
modernVersion: prepared.modernVersion,
|
|
56
|
+
classicVersion: prepared.classicVersion,
|
|
57
|
+
submissionEvidenceFile: relativeOrAbsolute(prepared.gameRoot, prepared.submissionEvidenceFile),
|
|
58
|
+
submissionEvidenceSha256: prepared.submissionBefore.sha256,
|
|
59
|
+
productIdentity: prepared.submission.identity,
|
|
60
|
+
manifest: {
|
|
61
|
+
file: relativeOrAbsolute(prepared.gameRoot, prepared.submission.manifestFile),
|
|
62
|
+
url: prepared.manifestUrl,
|
|
63
|
+
sha256: prepared.submission.manifestSha256,
|
|
64
|
+
pinnedInGeneratorRequest: true,
|
|
65
|
+
icons: {
|
|
66
|
+
count: prepared.submission.manifestIcons.length,
|
|
67
|
+
verification: 'before-and-after-generator',
|
|
68
|
+
entries: prepared.submission.manifestIcons.map((icon) => ({
|
|
69
|
+
file: relativeOrAbsolute(prepared.gameRoot, icon.file),
|
|
70
|
+
url: icon.url,
|
|
71
|
+
sha256: icon.snapshot.sha256,
|
|
72
|
+
width: icon.width,
|
|
73
|
+
height: icon.height,
|
|
74
|
+
})),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
generator: {
|
|
78
|
+
endpoint: microsoftStorePackageGeneratorEndpoint,
|
|
79
|
+
sourceRevision: microsoftStorePackageGeneratorSourceRevision,
|
|
80
|
+
contract: 'unversioned-best-effort',
|
|
81
|
+
requestSha256,
|
|
82
|
+
},
|
|
83
|
+
archive: {
|
|
84
|
+
file: relativeOrAbsolute(prepared.gameRoot, prepared.outputFile),
|
|
85
|
+
sizeBytes: archive.sizeBytes,
|
|
86
|
+
sha256: archive.sha256,
|
|
87
|
+
contentType: 'application/zip',
|
|
88
|
+
},
|
|
89
|
+
packageInspectionRequired: true,
|
|
90
|
+
};
|
|
91
|
+
mkdirSync(path.dirname(prepared.jsonFile), { recursive: true });
|
|
92
|
+
mkdirSync(path.dirname(prepared.markdownFile), { recursive: true });
|
|
93
|
+
writeMicrosoftStorePackageGenerationEvidenceFiles({
|
|
94
|
+
jsonFile: prepared.jsonFile,
|
|
95
|
+
markdownFile: prepared.markdownFile,
|
|
96
|
+
report: evidence,
|
|
97
|
+
markdown: renderMicrosoftStorePackageGenerationMarkdown(evidence),
|
|
98
|
+
});
|
|
99
|
+
return evidence;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function createMicrosoftStoreGeneratorManifest(manifest, iconUrls) {
|
|
103
|
+
const icons = manifest.icons;
|
|
104
|
+
if (!Array.isArray(icons) || icons.length !== iconUrls.length) {
|
|
105
|
+
throw new Error('Microsoft Store generator manifest icons must match verified icon URLs.');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
...manifest,
|
|
109
|
+
icons: icons.map((icon, index) => {
|
|
110
|
+
if (icon === null || typeof icon !== 'object' || Array.isArray(icon)) {
|
|
111
|
+
throw new Error(`Microsoft Store generator manifest icon[${index}] must be an object.`);
|
|
112
|
+
}
|
|
113
|
+
const url = iconUrls[index];
|
|
114
|
+
if (url === undefined) {
|
|
115
|
+
throw new Error(`Microsoft Store generator manifest icon[${index}] URL is missing.`);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
...icon,
|
|
119
|
+
src: url,
|
|
120
|
+
};
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function writeMicrosoftStorePackageGenerationEvidenceFiles(input, hooks = {}) {
|
|
125
|
+
const transactionId = `${process.pid}.${randomUUID()}`;
|
|
126
|
+
const files = [
|
|
127
|
+
createEvidenceFilePublication(input.jsonFile, `${JSON.stringify(input.report, null, 2)}\n`, transactionId),
|
|
128
|
+
createEvidenceFilePublication(input.markdownFile, input.markdown, transactionId),
|
|
129
|
+
];
|
|
130
|
+
let completed = false;
|
|
131
|
+
try {
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
acquireEvidenceFileLock(file, transactionId);
|
|
134
|
+
}
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
const descriptor = openSync(file.temporaryFile, 'wx', 0o600);
|
|
137
|
+
file.temporaryExists = true;
|
|
138
|
+
try {
|
|
139
|
+
writeFileSync(descriptor, file.contents);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
closeSync(descriptor);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
const metadata = lstatIfExists(file.finalFile);
|
|
147
|
+
if (metadata === undefined) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
151
|
+
throw new Error(`Microsoft Store package generation evidence must be a regular file: ${file.finalFile}`);
|
|
152
|
+
}
|
|
153
|
+
copyFileSync(file.finalFile, file.backupFile, fsConstants.COPYFILE_EXCL);
|
|
154
|
+
file.backupExists = true;
|
|
155
|
+
const backupSnapshot = hashMicrosoftStoreFileSnapshot(file.backupFile, 'Microsoft Store package generation evidence backup');
|
|
156
|
+
file.previousIdentity = evidenceFileIdentity(metadata, backupSnapshot);
|
|
157
|
+
if (!microsoftStoreEvidenceFileMatchesIdentity(file.finalFile, file.previousIdentity)) {
|
|
158
|
+
throw new Error(`Microsoft Store package generation evidence changed while it was backed up: ${file.finalFile}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
const publishedIdentity = publishedEvidenceFileIdentity(lstatSync(file.temporaryFile), file.contentsSnapshot);
|
|
163
|
+
if (file.previousIdentity === undefined) {
|
|
164
|
+
linkSync(file.temporaryFile, file.finalFile);
|
|
165
|
+
file.publishedIdentity = publishedIdentity;
|
|
166
|
+
hooks.afterPlacement?.(file.finalFile);
|
|
167
|
+
unlinkSync(file.temporaryFile);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
if (!microsoftStoreEvidenceFileMatchesIdentity(file.finalFile, file.previousIdentity)) {
|
|
171
|
+
throw new Error(`Microsoft Store package generation evidence changed before publication: ${file.finalFile}`);
|
|
172
|
+
}
|
|
173
|
+
renameSync(file.temporaryFile, file.finalFile);
|
|
174
|
+
file.publishedIdentity = publishedIdentity;
|
|
175
|
+
hooks.afterPlacement?.(file.finalFile);
|
|
176
|
+
}
|
|
177
|
+
file.temporaryExists = false;
|
|
178
|
+
}
|
|
179
|
+
completed = true;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
throw new Error(`Failed to write Microsoft Store package generation evidence: ${formatError(error)}`, { cause: error });
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
if (!completed) {
|
|
186
|
+
for (const file of [...files].reverse()) {
|
|
187
|
+
rollbackEvidenceFilePublication(file);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
unlinkIfPresent(file.temporaryFile, file.temporaryExists);
|
|
192
|
+
unlinkIfPresent(file.backupFile, file.backupExists);
|
|
193
|
+
removeMicrosoftStoreEvidenceLockIfOwned(file.lockFile, file.lockIdentity, file.lockToken);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function createEvidenceFilePublication(finalFile, contents, transactionId) {
|
|
198
|
+
const prefix = `.${path.basename(finalFile)}.${transactionId}`;
|
|
199
|
+
const contentsBytes = Buffer.from(contents);
|
|
200
|
+
return {
|
|
201
|
+
finalFile,
|
|
202
|
+
temporaryFile: path.join(path.dirname(finalFile), `${prefix}.tmp`),
|
|
203
|
+
backupFile: path.join(path.dirname(finalFile), `${prefix}.bak`),
|
|
204
|
+
lockFile: path.join(path.dirname(finalFile), `.${path.basename(finalFile)}.mpgd-package-generation.lock`),
|
|
205
|
+
contents,
|
|
206
|
+
contentsSnapshot: {
|
|
207
|
+
sizeBytes: contentsBytes.length,
|
|
208
|
+
sha256: hashMicrosoftStoreBytes(contentsBytes),
|
|
209
|
+
},
|
|
210
|
+
temporaryExists: false,
|
|
211
|
+
backupExists: false,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function acquireEvidenceFileLock(file, transactionId) {
|
|
215
|
+
let descriptor;
|
|
216
|
+
try {
|
|
217
|
+
descriptor = openSync(file.lockFile, 'wx', 0o600);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (hasErrorCode(error, 'EEXIST')) {
|
|
221
|
+
throw new Error(`Microsoft Store package generation evidence is already being written: ${file.finalFile}`);
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
file.lockToken = transactionId;
|
|
226
|
+
let initialized = false;
|
|
227
|
+
try {
|
|
228
|
+
file.lockIdentity = fileNodeIdentity(fstatSync(descriptor));
|
|
229
|
+
writeFileSync(descriptor, `${transactionId}\n`);
|
|
230
|
+
initialized = true;
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
try {
|
|
234
|
+
closeSync(descriptor);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
if (!initialized && unlinkImmediately(file.lockFile)) {
|
|
238
|
+
delete file.lockIdentity;
|
|
239
|
+
delete file.lockToken;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function rollbackEvidenceFilePublication(file) {
|
|
245
|
+
if (file.publishedIdentity === undefined) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
let matchesIdentity = false;
|
|
249
|
+
try {
|
|
250
|
+
matchesIdentity = microsoftStorePublishedEvidenceFileMatchesIdentity(file.finalFile, file.publishedIdentity);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Preserve the backup for manual recovery when rollback inspection fails.
|
|
254
|
+
file.backupExists = false;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!matchesIdentity) {
|
|
258
|
+
// A concurrent writer replaced the final file; preserve the backup for manual recovery.
|
|
259
|
+
file.backupExists = false;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (file.backupExists) {
|
|
263
|
+
try {
|
|
264
|
+
renameSync(file.backupFile, file.finalFile);
|
|
265
|
+
file.backupExists = false;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// Preserve the backup for manual recovery when an atomic rollback cannot complete.
|
|
269
|
+
file.backupExists = false;
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
unlinkSync(file.finalFile);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Do not mask the original evidence publication failure.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function microsoftStorePublishedEvidenceFileMatchesIdentity(file, expected) {
|
|
281
|
+
const metadata = lstatIfExists(file);
|
|
282
|
+
if (metadata === undefined
|
|
283
|
+
|| !metadata.isFile()
|
|
284
|
+
|| metadata.dev !== expected.dev
|
|
285
|
+
|| (expected.ino !== 0 && metadata.ino !== expected.ino)) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const actual = hashMicrosoftStoreFileSnapshot(file, 'published Microsoft Store package generation evidence ownership check');
|
|
289
|
+
return actual.sizeBytes === expected.sizeBytes
|
|
290
|
+
&& actual.sha256 === expected.sha256;
|
|
291
|
+
}
|
|
292
|
+
export function microsoftStoreEvidenceFileMatchesIdentity(file, expected) {
|
|
293
|
+
const metadata = lstatIfExists(file);
|
|
294
|
+
if (metadata === undefined || !metadata.isFile() || metadata.dev !== expected.dev) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (expected.ino !== 0) {
|
|
298
|
+
return metadata.ino === expected.ino
|
|
299
|
+
&& metadata.size === expected.sizeBytes
|
|
300
|
+
&& metadata.mtimeMs === expected.mtimeMs
|
|
301
|
+
&& metadata.ctimeMs === expected.ctimeMs;
|
|
302
|
+
}
|
|
303
|
+
const actual = hashMicrosoftStoreFileSnapshot(file, 'Microsoft Store package generation evidence ownership check');
|
|
304
|
+
return actual.sizeBytes === expected.sizeBytes
|
|
305
|
+
&& actual.sha256 === expected.sha256;
|
|
306
|
+
}
|
|
307
|
+
function fileNodeIdentity(metadata) {
|
|
308
|
+
return { dev: metadata.dev, ino: metadata.ino };
|
|
309
|
+
}
|
|
310
|
+
function metadataMatchesNodeIdentity(metadata, expected) {
|
|
311
|
+
return expected.ino !== 0
|
|
312
|
+
&& metadata.dev === expected.dev
|
|
313
|
+
&& metadata.ino === expected.ino;
|
|
314
|
+
}
|
|
315
|
+
function evidenceFileIdentity(metadata, snapshot) {
|
|
316
|
+
return {
|
|
317
|
+
dev: metadata.dev,
|
|
318
|
+
ino: metadata.ino,
|
|
319
|
+
...snapshot,
|
|
320
|
+
mtimeMs: metadata.mtimeMs,
|
|
321
|
+
ctimeMs: metadata.ctimeMs,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function publishedEvidenceFileIdentity(metadata, snapshot) {
|
|
325
|
+
if (metadata.size !== snapshot.sizeBytes) {
|
|
326
|
+
throw new Error('Microsoft Store package generation temporary evidence size changed.');
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
dev: metadata.dev,
|
|
330
|
+
ino: metadata.ino,
|
|
331
|
+
...snapshot,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function lstatIfExists(file) {
|
|
335
|
+
try {
|
|
336
|
+
return lstatSync(file);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (hasErrorCode(error, 'ENOENT')) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function unlinkIfPresent(file, expectedToExist) {
|
|
346
|
+
if (!expectedToExist || !existsSync(file)) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
unlinkSync(file);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Temporary cleanup must not invalidate otherwise consistent evidence outputs.
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function unlinkImmediately(file) {
|
|
357
|
+
try {
|
|
358
|
+
unlinkSync(file);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
export function removeMicrosoftStoreEvidenceLockIfOwned(file, expected, token) {
|
|
366
|
+
if (expected === undefined || token === undefined) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const metadata = lstatIfExists(file);
|
|
371
|
+
if (metadata !== undefined
|
|
372
|
+
&& metadata.isFile()
|
|
373
|
+
&& (metadataMatchesNodeIdentity(metadata, expected)
|
|
374
|
+
|| (expected.ino === 0
|
|
375
|
+
&& microsoftStoreEvidenceLockMatchesToken(file, token)))) {
|
|
376
|
+
unlinkSync(file);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
// Do not remove a lock whose ownership can no longer be proven.
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function microsoftStoreEvidenceLockMatchesToken(file, token) {
|
|
384
|
+
const expected = Buffer.from(`${token}\n`);
|
|
385
|
+
const descriptor = openSync(file, 'r');
|
|
386
|
+
try {
|
|
387
|
+
const metadata = fstatSync(descriptor);
|
|
388
|
+
if (!metadata.isFile() || metadata.size !== expected.length) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
const contents = Buffer.allocUnsafe(expected.length);
|
|
392
|
+
let offset = 0;
|
|
393
|
+
while (offset < contents.length) {
|
|
394
|
+
const bytesRead = readSync(descriptor, contents, offset, contents.length - offset, offset);
|
|
395
|
+
if (bytesRead === 0) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
offset += bytesRead;
|
|
399
|
+
}
|
|
400
|
+
return contents.equals(expected);
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
closeSync(descriptor);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function hasErrorCode(error, code) {
|
|
407
|
+
return error instanceof Error && 'code' in error && error.code === code;
|
|
408
|
+
}
|
|
409
|
+
export function renderMicrosoftStorePackageGenerationMarkdown(evidence) {
|
|
410
|
+
return `# Microsoft Store Package Generation
|
|
411
|
+
|
|
412
|
+
- PWA URL: ${escapeMarkdownInline(evidence.pwaUrl)}
|
|
413
|
+
- Manifest URL: ${escapeMarkdownInline(evidence.manifest.url)}
|
|
414
|
+
- Manifest SHA-256: \`${evidence.manifest.sha256}\`
|
|
415
|
+
- Manifest pinned in generator request: yes
|
|
416
|
+
- Manifest icons verified before and after generator: ${evidence.manifest.icons.count}
|
|
417
|
+
- Modern version: \`${evidence.modernVersion}\`
|
|
418
|
+
- Classic version: \`${evidence.classicVersion}\`
|
|
419
|
+
- Package ID: ${escapeMarkdownInline(evidence.productIdentity.packageId)}
|
|
420
|
+
- Publisher ID: ${escapeMarkdownInline(evidence.productIdentity.publisherId)}
|
|
421
|
+
- Generator endpoint: ${escapeMarkdownInline(evidence.generator.endpoint)}
|
|
422
|
+
- Generator contract: ${evidence.generator.contract}
|
|
423
|
+
- Generator request SHA-256: \`${evidence.generator.requestSha256}\`
|
|
424
|
+
- Package inspection: required before submission
|
|
425
|
+
|
|
426
|
+
| Archive | Bytes | SHA-256 |
|
|
427
|
+
| --- | ---: | --- |
|
|
428
|
+
| ${escapeMarkdownTable(evidence.archive.file)} | ${evidence.archive.sizeBytes} | \`${evidence.archive.sha256}\` |
|
|
429
|
+
`;
|
|
430
|
+
}
|