@mpgd/cli 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.js +500 -11
  3. package/dist/microsoft-store-package-acceptance.d.ts +54 -0
  4. package/dist/microsoft-store-package-acceptance.js +787 -0
  5. package/dist/microsoft-store-package-generation-contract.d.ts +129 -0
  6. package/dist/microsoft-store-package-generation-contract.js +3 -0
  7. package/dist/microsoft-store-package-generation-download.d.ts +22 -0
  8. package/dist/microsoft-store-package-generation-download.js +453 -0
  9. package/dist/microsoft-store-package-generation-input.d.ts +3 -0
  10. package/dist/microsoft-store-package-generation-input.js +384 -0
  11. package/dist/microsoft-store-package-generation-integrity.d.ts +5 -0
  12. package/dist/microsoft-store-package-generation-integrity.js +100 -0
  13. package/dist/microsoft-store-package-generation-manifest.d.ts +3 -0
  14. package/dist/microsoft-store-package-generation-manifest.js +67 -0
  15. package/dist/microsoft-store-package-generation-zip.d.ts +1 -0
  16. package/dist/microsoft-store-package-generation-zip.js +180 -0
  17. package/dist/microsoft-store-package-generation.d.ts +24 -0
  18. package/dist/microsoft-store-package-generation.js +430 -0
  19. package/dist/microsoft-store-starter.d.ts +28 -0
  20. package/dist/microsoft-store-starter.js +655 -0
  21. package/dist/microsoft-store-submission.d.ts +79 -0
  22. package/dist/microsoft-store-submission.js +952 -0
  23. package/dist/microsoft-store-supported-locales.d.ts +1 -0
  24. package/dist/microsoft-store-supported-locales.js +33 -0
  25. package/dist/production-target-readiness.js +3 -1
  26. package/package.json +9 -8
  27. package/templates/phaser-game/.agents/skills/release-microsoft-store/SKILL.md +60 -0
  28. package/templates/phaser-game/.agents/skills/release-microsoft-store/agents/openai.yaml +4 -0
  29. package/templates/phaser-game/.agents/skills/use-mpgd-kit/SKILL.md +45 -0
  30. package/templates/phaser-game/.agents/skills/use-mpgd-kit/agents/openai.yaml +4 -0
  31. package/templates/phaser-game/AGENTS.md +34 -0
  32. package/templates/phaser-game/README.md +88 -25
  33. package/templates/phaser-game/agent/acceptance.md +8 -0
  34. package/templates/phaser-game/agent/brief.md +13 -1
  35. package/templates/phaser-game/agent/game-manifest.json +7 -0
  36. package/templates/phaser-game/apps/target-ait/granite.config.ts +35 -0
  37. package/templates/phaser-game/apps/target-ait/index.html +15 -0
  38. package/templates/phaser-game/apps/target-ait/package.json +33 -0
  39. package/templates/phaser-game/apps/target-ait/src/main.ts +34 -0
  40. package/templates/phaser-game/apps/target-ait/src/styles.css +33 -0
  41. package/templates/phaser-game/apps/target-ait/tsconfig.json +8 -0
  42. package/templates/phaser-game/apps/target-ait/vite.config.ts +53 -0
  43. package/templates/phaser-game/docs/MPGD_KIT_WORKFLOWS.md +128 -0
  44. package/templates/phaser-game/gitignore +7 -0
  45. package/templates/phaser-game/mpgd.microsoft-store.json +29 -0
  46. package/templates/phaser-game/mpgd.targets.json +9 -2
  47. package/templates/phaser-game/package.json +11 -9
  48. package/templates/phaser-game/vite.shared.ts +13 -0
@@ -0,0 +1,129 @@
1
+ export declare const microsoftStorePackageGenerationSchemaVersion: 1;
2
+ export declare const microsoftStorePackageGeneratorEndpoint: 'https://pwabuilder-windows-docker.azurewebsites.net/msix/generatezip';
3
+ export declare const microsoftStorePackageGeneratorSourceRevision: 'ded7914e84d1509c901d2899a3f654f5d44ef08f';
4
+ export interface MicrosoftStorePackageGenerationEvidence {
5
+ readonly schemaVersion: typeof microsoftStorePackageGenerationSchemaVersion;
6
+ readonly target: 'microsoft-store';
7
+ readonly pwaUrl: string;
8
+ readonly modernVersion: string;
9
+ readonly classicVersion: string;
10
+ readonly submissionEvidenceFile: string;
11
+ readonly submissionEvidenceSha256: string;
12
+ readonly productIdentity: MicrosoftStoreProductIdentity;
13
+ readonly manifest: {
14
+ readonly file: string;
15
+ readonly url: string;
16
+ readonly sha256: string;
17
+ readonly pinnedInGeneratorRequest: true;
18
+ readonly icons: {
19
+ readonly count: number;
20
+ readonly verification: 'before-and-after-generator';
21
+ readonly entries: readonly {
22
+ readonly file: string;
23
+ readonly url: string;
24
+ readonly sha256: string;
25
+ readonly width: number;
26
+ readonly height: number;
27
+ }[];
28
+ };
29
+ };
30
+ readonly generator: {
31
+ readonly endpoint: typeof microsoftStorePackageGeneratorEndpoint;
32
+ readonly sourceRevision: typeof microsoftStorePackageGeneratorSourceRevision;
33
+ readonly contract: 'unversioned-best-effort';
34
+ readonly requestSha256: string;
35
+ };
36
+ readonly archive: {
37
+ readonly file: string;
38
+ readonly sizeBytes: number;
39
+ readonly sha256: string;
40
+ readonly contentType: 'application/zip';
41
+ };
42
+ readonly packageInspectionRequired: true;
43
+ }
44
+ export interface RunMicrosoftStorePackageGenerationInput {
45
+ readonly gameRoot: string;
46
+ readonly submissionEvidenceFile: string;
47
+ readonly pwaUrl: string;
48
+ readonly manifestUrl: string;
49
+ readonly modernVersion: string;
50
+ readonly classicVersion: string;
51
+ readonly outputFile: string;
52
+ readonly jsonFile: string;
53
+ readonly markdownFile: string;
54
+ }
55
+ export interface MicrosoftStorePackageGenerationRuntime {
56
+ readonly fetch: typeof fetch;
57
+ }
58
+ export interface CreateMicrosoftStorePackageGenerationRuntimeInput {
59
+ readonly fetch?: typeof fetch;
60
+ readonly resolveAddresses?: MicrosoftStoreAddressResolver;
61
+ }
62
+ export type MicrosoftStoreAddressResolver = (hostname: string) => Promise<readonly MicrosoftStoreResolvedAddress[]>;
63
+ export interface MicrosoftStoreResolvedAddress {
64
+ readonly address: string;
65
+ readonly family: number;
66
+ }
67
+ export interface MicrosoftStoreProductIdentity {
68
+ readonly packageId: string;
69
+ readonly publisherId: string;
70
+ readonly publisherDisplayName: string;
71
+ readonly reservedName: string;
72
+ }
73
+ export interface MicrosoftStoreFileSnapshot {
74
+ readonly sizeBytes: number;
75
+ readonly sha256: string;
76
+ }
77
+ export interface MicrosoftStoreSubmissionEvidenceInput {
78
+ readonly identity: MicrosoftStoreProductIdentity;
79
+ readonly manifestFile: string;
80
+ readonly manifestSha256: string;
81
+ readonly manifest: Readonly<Record<string, unknown>>;
82
+ readonly manifestIcons: readonly MicrosoftStoreManifestIconInput[];
83
+ readonly resourceLanguage: string;
84
+ }
85
+ export interface MicrosoftStoreManifestIconInput {
86
+ readonly file: string;
87
+ readonly url: string;
88
+ readonly snapshot: MicrosoftStoreFileSnapshot;
89
+ readonly width: number;
90
+ readonly height: number;
91
+ }
92
+ export interface PreparedMicrosoftStorePackageGenerationInput {
93
+ readonly gameRoot: string;
94
+ readonly submissionEvidenceFile: string;
95
+ readonly submissionBefore: MicrosoftStoreFileSnapshot;
96
+ readonly submission: MicrosoftStoreSubmissionEvidenceInput;
97
+ readonly manifestBefore: MicrosoftStoreFileSnapshot;
98
+ readonly pwaUrl: string;
99
+ readonly manifestUrl: string;
100
+ readonly modernVersion: string;
101
+ readonly classicVersion: string;
102
+ readonly outputFile: string;
103
+ readonly jsonFile: string;
104
+ readonly markdownFile: string;
105
+ }
106
+ export interface MicrosoftStorePackageGeneratorRequest {
107
+ readonly name: string;
108
+ readonly packageId: string;
109
+ readonly applicationId: 'App';
110
+ readonly url: string;
111
+ readonly version: string;
112
+ readonly allowSigning: true;
113
+ readonly publisher: {
114
+ readonly displayName: string;
115
+ readonly commonName: string;
116
+ };
117
+ readonly generateModernPackage: true;
118
+ readonly classicPackage: {
119
+ readonly generate: true;
120
+ readonly version: string;
121
+ readonly url: string;
122
+ };
123
+ readonly edgeChannel: 'stable';
124
+ readonly manifestUrl: string;
125
+ readonly manifest: Readonly<Record<string, unknown>>;
126
+ readonly resourceLanguage: string;
127
+ readonly targetDeviceFamilies: readonly ['Desktop'];
128
+ readonly usePwaBuilderWithCustomManifest: true;
129
+ }
@@ -0,0 +1,3 @@
1
+ export const microsoftStorePackageGenerationSchemaVersion = 1;
2
+ export const microsoftStorePackageGeneratorEndpoint = 'https://pwabuilder-windows-docker.azurewebsites.net/msix/generatezip';
3
+ export const microsoftStorePackageGeneratorSourceRevision = 'ded7914e84d1509c901d2899a3f654f5d44ef08f';
@@ -0,0 +1,22 @@
1
+ import { type Dispatcher } from 'undici';
2
+ import { type CreateMicrosoftStorePackageGenerationRuntimeInput, type MicrosoftStoreAddressResolver, type MicrosoftStoreFileSnapshot, type MicrosoftStoreManifestIconInput, type MicrosoftStorePackageGenerationRuntime, type MicrosoftStoreResolvedAddress } from './microsoft-store-package-generation-contract.js';
3
+ interface WithMicrosoftStorePackageArchiveInput {
4
+ readonly runtime: MicrosoftStorePackageGenerationRuntime;
5
+ readonly manifestUrl: string;
6
+ readonly manifestSha256: string;
7
+ readonly manifestIcons: readonly MicrosoftStoreManifestIconInput[];
8
+ readonly requestBody: string;
9
+ readonly outputFile: string;
10
+ readonly assertInputsUnchanged: () => void;
11
+ readonly afterPlacement?: (file: string) => void;
12
+ }
13
+ interface MicrosoftStorePublishedFileIdentity extends MicrosoftStoreFileSnapshot {
14
+ readonly dev: number;
15
+ readonly ino: number;
16
+ }
17
+ export declare function createMicrosoftStorePackageGenerationRuntime(input?: CreateMicrosoftStorePackageGenerationRuntimeInput): MicrosoftStorePackageGenerationRuntime;
18
+ export declare function createMicrosoftStoreDispatcherFetch(dispatcher: Dispatcher): typeof fetch;
19
+ export declare function resolveMicrosoftStorePublicAddresses(hostname: string, resolver?: MicrosoftStoreAddressResolver): Promise<readonly MicrosoftStoreResolvedAddress[]>;
20
+ export declare function withMicrosoftStorePackageArchive<Result>(input: WithMicrosoftStorePackageArchiveInput, consume: (archive: MicrosoftStoreFileSnapshot) => Result | Promise<Result>): Promise<Result>;
21
+ export declare function removeMicrosoftStorePackageZipIfOwned(file: string, expected: MicrosoftStorePublishedFileIdentity): void;
22
+ export {};
@@ -0,0 +1,453 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { lookup as lookupDns } from 'node:dns/promises';
3
+ import { closeSync, existsSync, linkSync, lstatSync, openSync, unlinkSync, writeSync, } from 'node:fs';
4
+ import { BlockList } from 'node:net';
5
+ import path from 'node:path';
6
+ import { Agent, fetch as undiciFetch, Request as UndiciRequest, } from 'undici';
7
+ import { formatError } from './evidence-io.js';
8
+ import { microsoftStorePackageGeneratorEndpoint, } from './microsoft-store-package-generation-contract.js';
9
+ import { hashMicrosoftStoreBytes, hashMicrosoftStoreFileSnapshot, } from './microsoft-store-package-generation-integrity.js';
10
+ import { assertMicrosoftStorePackageZip } from './microsoft-store-package-generation-zip.js';
11
+ const maximumManifestBytes = 1024 * 1024;
12
+ const maximumManifestIconBytes = 2 * 1024 * 1024;
13
+ const maximumArchiveBytes = 512 * 1024 * 1024;
14
+ const manifestRequestTimeoutMs = 30000;
15
+ const manifestIconRequestTimeoutMs = 30000;
16
+ const packageRequestTimeoutMs = 10 * 60 * 1000;
17
+ const blockedIpv4Addresses = createBlockedIpv4Addresses();
18
+ const blockedIpv6Addresses = createBlockedIpv6Addresses();
19
+ const allowedIpv6GlobalUnicastAddresses = createAllowedIpv6GlobalUnicastAddresses();
20
+ export function createMicrosoftStorePackageGenerationRuntime(input = {}) {
21
+ const fetcher = input.fetch ?? createPublicOnlyFetch(input.resolveAddresses);
22
+ if (typeof fetcher !== 'function') {
23
+ throw new Error('Microsoft Store package generation requires a Fetch API implementation.');
24
+ }
25
+ return { fetch: fetcher };
26
+ }
27
+ function createPublicOnlyFetch(resolveAddresses) {
28
+ const dispatcher = new Agent({
29
+ connect: {
30
+ lookup: createPublicOnlyLookup((hostname) => resolveMicrosoftStorePublicAddresses(hostname, resolveAddresses)),
31
+ },
32
+ });
33
+ return createMicrosoftStoreDispatcherFetch(dispatcher);
34
+ }
35
+ export function createMicrosoftStoreDispatcherFetch(dispatcher) {
36
+ return (async (input, init = {}) => {
37
+ const response = await undiciFetch(toUndiciRequestInfo(input), {
38
+ ...init,
39
+ dispatcher,
40
+ });
41
+ // Undici implements Node's global Fetch API, but publishes a distinct Response type.
42
+ return response;
43
+ });
44
+ }
45
+ function toUndiciRequestInfo(input) {
46
+ if (typeof input === 'string' || input instanceof URL) {
47
+ return input;
48
+ }
49
+ return new UndiciRequest(input.url, {
50
+ cache: input.cache,
51
+ credentials: input.credentials,
52
+ headers: Object.fromEntries(input.headers),
53
+ integrity: input.integrity,
54
+ keepalive: input.keepalive,
55
+ method: input.method,
56
+ mode: input.mode,
57
+ redirect: input.redirect,
58
+ referrer: input.referrer,
59
+ referrerPolicy: input.referrerPolicy,
60
+ signal: input.signal,
61
+ ...(input.body === null
62
+ ? {}
63
+ : {
64
+ body: input.body,
65
+ duplex: 'half',
66
+ }),
67
+ });
68
+ }
69
+ function createPublicOnlyLookup(resolveAddresses) {
70
+ return (hostname, options, callback) => {
71
+ void resolveAddresses(hostname).then((addresses) => {
72
+ if (options.all === true) {
73
+ callback(null, [...addresses]);
74
+ return;
75
+ }
76
+ let family = options.family;
77
+ if (family === 'IPv4') {
78
+ family = 4;
79
+ }
80
+ else if (family === 'IPv6') {
81
+ family = 6;
82
+ }
83
+ const selected = addresses.find((address) => family === undefined || family === 0 || address.family === family);
84
+ if (selected === undefined) {
85
+ callback(new Error(`No public address matched the requested family for ${hostname}.`), []);
86
+ return;
87
+ }
88
+ callback(null, selected.address, selected.family);
89
+ }, (error) => {
90
+ callback(error instanceof Error
91
+ ? error
92
+ : new Error(`Failed to resolve a public address for ${hostname}.`), []);
93
+ });
94
+ };
95
+ }
96
+ export async function resolveMicrosoftStorePublicAddresses(hostname, resolver = async (value) => lookupDns(value, {
97
+ all: true,
98
+ order: 'verbatim',
99
+ })) {
100
+ const addresses = await resolver(hostname);
101
+ if (addresses.length === 0) {
102
+ throw new Error(`Microsoft Store package generation host did not resolve: ${hostname}.`);
103
+ }
104
+ for (const address of addresses) {
105
+ const family = address.family === 4 ? 'ipv4' : address.family === 6 ? 'ipv6' : undefined;
106
+ if (family === undefined
107
+ || (family === 'ipv4' && blockedIpv4Addresses.check(address.address, family))
108
+ || (family === 'ipv6'
109
+ && (!allowedIpv6GlobalUnicastAddresses.check(address.address, family)
110
+ || blockedIpv6Addresses.check(address.address, family)))) {
111
+ throw new Error(`Microsoft Store package generation host must resolve only to public addresses: ${hostname} resolved to ${address.address}.`);
112
+ }
113
+ }
114
+ return addresses;
115
+ }
116
+ function createBlockedIpv4Addresses() {
117
+ const blockList = new BlockList();
118
+ // IANA special-purpose ranges, including private, loopback, link-local,
119
+ // documentation, benchmarking, multicast, and reserved space (RFC 6890).
120
+ for (const [network, prefix] of [
121
+ ['0.0.0.0', 8],
122
+ ['10.0.0.0', 8],
123
+ ['100.64.0.0', 10],
124
+ ['127.0.0.0', 8],
125
+ ['169.254.0.0', 16],
126
+ ['172.16.0.0', 12],
127
+ ['192.0.0.0', 24],
128
+ ['192.0.2.0', 24],
129
+ ['192.88.99.0', 24],
130
+ ['192.168.0.0', 16],
131
+ ['198.18.0.0', 15],
132
+ ['198.51.100.0', 24],
133
+ ['203.0.113.0', 24],
134
+ ['224.0.0.0', 4],
135
+ ['240.0.0.0', 4],
136
+ ]) {
137
+ blockList.addSubnet(network, prefix, 'ipv4');
138
+ }
139
+ return blockList;
140
+ }
141
+ function createBlockedIpv6Addresses() {
142
+ const blockList = new BlockList();
143
+ // IANA special-purpose ranges, including mapped/translated IPv4, loopback,
144
+ // documentation, unique-local, link-local, multicast, and reserved space.
145
+ for (const [network, prefix] of [
146
+ ['::', 96],
147
+ ['::ffff:0:0', 96],
148
+ ['64:ff9b::', 96],
149
+ ['64:ff9b:1::', 48],
150
+ ['100::', 64],
151
+ ['2001::', 23],
152
+ ['2001:db8::', 32],
153
+ ['2002::', 16],
154
+ ['3fff::', 20],
155
+ ['fc00::', 7],
156
+ ['fe80::', 10],
157
+ ['fec0::', 10],
158
+ ['ff00::', 8],
159
+ ]) {
160
+ blockList.addSubnet(network, prefix, 'ipv6');
161
+ }
162
+ return blockList;
163
+ }
164
+ function createAllowedIpv6GlobalUnicastAddresses() {
165
+ const blockList = new BlockList();
166
+ // IANA currently assigns public IPv6 global unicast space from 2000::/3.
167
+ // Special-purpose subnets inside it remain denied by blockedIpv6Addresses.
168
+ blockList.addSubnet('2000::', 3, 'ipv6');
169
+ return blockList;
170
+ }
171
+ export async function withMicrosoftStorePackageArchive(input, consume) {
172
+ await assertRemoteManifestMatches(input.manifestUrl, input.manifestSha256, input.runtime, 'before package generation');
173
+ await assertRemoteManifestIconsMatch(input.manifestIcons, input.runtime, 'before package generation');
174
+ const temporaryFile = path.join(path.dirname(input.outputFile), `.${path.basename(input.outputFile)}.${process.pid}.${randomUUID()}.tmp`);
175
+ let temporaryFileExists = false;
176
+ let publishedOutputIdentity;
177
+ let completed = false;
178
+ try {
179
+ const response = await fetchResponse(input.runtime, microsoftStorePackageGeneratorEndpoint, {
180
+ method: 'POST',
181
+ body: input.requestBody,
182
+ headers: {
183
+ accept: 'application/zip',
184
+ 'accept-encoding': 'identity',
185
+ 'content-type': 'application/json',
186
+ },
187
+ redirect: 'manual',
188
+ signal: AbortSignal.timeout(packageRequestTimeoutMs),
189
+ }, 'PWABuilder package generator');
190
+ await assertSuccessfulResponse(response, 'PWABuilder package generator');
191
+ const contentType = response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase();
192
+ if (contentType !== 'application/zip') {
193
+ await cancelResponse(response);
194
+ throw new Error(`PWABuilder package generator must return application/zip, received ${contentType ?? 'missing content type'}.`);
195
+ }
196
+ const declaredLength = await readContentLengthSafely(response, 'PWABuilder package generator');
197
+ if (declaredLength !== undefined && declaredLength > maximumArchiveBytes) {
198
+ await cancelResponse(response);
199
+ throw new Error('PWABuilder package archive exceeds the 512 MiB size limit.');
200
+ }
201
+ temporaryFileExists = true;
202
+ const archive = await writeBoundedResponseToFile(response, temporaryFile, maximumArchiveBytes, 'PWABuilder package archive');
203
+ if (declaredLength !== undefined && archive.sizeBytes !== declaredLength) {
204
+ throw new Error(`PWABuilder package archive length mismatch: expected ${declaredLength}, received ${archive.sizeBytes}.`);
205
+ }
206
+ assertMicrosoftStorePackageZip(temporaryFile, archive.sizeBytes);
207
+ input.assertInputsUnchanged();
208
+ await assertRemoteManifestMatches(input.manifestUrl, input.manifestSha256, input.runtime, 'after package generation');
209
+ await assertRemoteManifestIconsMatch(input.manifestIcons, input.runtime, 'after package generation');
210
+ input.assertInputsUnchanged();
211
+ const temporaryMetadata = lstatSync(temporaryFile);
212
+ try {
213
+ linkSync(temporaryFile, input.outputFile);
214
+ }
215
+ catch (error) {
216
+ if (hasErrorCode(error, 'EEXIST')) {
217
+ throw new Error(`Microsoft Store package ZIP appeared during generation: ${input.outputFile}`);
218
+ }
219
+ throw new Error(`Failed to publish Microsoft Store package ZIP: ${input.outputFile} (${formatError(error)})`);
220
+ }
221
+ publishedOutputIdentity = {
222
+ dev: temporaryMetadata.dev,
223
+ ino: temporaryMetadata.ino,
224
+ sizeBytes: archive.sizeBytes,
225
+ sha256: archive.sha256,
226
+ };
227
+ input.afterPlacement?.(input.outputFile);
228
+ unlinkSync(temporaryFile);
229
+ temporaryFileExists = false;
230
+ const outputSnapshot = hashMicrosoftStoreFileSnapshot(input.outputFile, 'Microsoft Store package ZIP');
231
+ if (outputSnapshot.sizeBytes !== archive.sizeBytes
232
+ || outputSnapshot.sha256 !== archive.sha256) {
233
+ throw new Error('Microsoft Store package ZIP changed during atomic publication.');
234
+ }
235
+ const result = await consume(outputSnapshot);
236
+ completed = true;
237
+ return result;
238
+ }
239
+ finally {
240
+ if (temporaryFileExists && existsSync(temporaryFile)) {
241
+ try {
242
+ unlinkSync(temporaryFile);
243
+ }
244
+ catch {
245
+ // Best-effort cleanup must not mask the package generation failure.
246
+ }
247
+ }
248
+ if (publishedOutputIdentity !== undefined && !completed) {
249
+ try {
250
+ removeMicrosoftStorePackageZipIfOwned(input.outputFile, publishedOutputIdentity);
251
+ }
252
+ catch {
253
+ // Best-effort cleanup must not mask the package generation failure.
254
+ }
255
+ }
256
+ }
257
+ }
258
+ async function assertRemoteManifestIconsMatch(icons, runtime, phase) {
259
+ for (const [index, icon] of icons.entries()) {
260
+ const label = `deployed Microsoft Store manifest icon[${index}] ${phase}`;
261
+ const response = await fetchResponse(runtime, icon.url, {
262
+ method: 'GET',
263
+ headers: { accept: 'image/png', 'accept-encoding': 'identity' },
264
+ redirect: 'manual',
265
+ signal: AbortSignal.timeout(manifestIconRequestTimeoutMs),
266
+ }, label);
267
+ await assertSuccessfulResponse(response, label);
268
+ const bytes = await readBoundedResponse(response, maximumManifestIconBytes, label);
269
+ const actualSha256 = hashMicrosoftStoreBytes(bytes);
270
+ if (actualSha256 !== icon.snapshot.sha256) {
271
+ throw new Error(`Deployed Microsoft Store manifest icon[${index}] SHA-256 must match submission evidence ${phase}: expected ${icon.snapshot.sha256}, received ${actualSha256}.`);
272
+ }
273
+ }
274
+ }
275
+ async function assertRemoteManifestMatches(manifestUrl, expectedSha256, runtime, phase) {
276
+ const response = await fetchResponse(runtime, manifestUrl, {
277
+ method: 'GET',
278
+ headers: {
279
+ accept: 'application/manifest+json, application/json',
280
+ 'accept-encoding': 'identity',
281
+ },
282
+ redirect: 'manual',
283
+ signal: AbortSignal.timeout(manifestRequestTimeoutMs),
284
+ }, `deployed Microsoft Store manifest ${phase}`);
285
+ await assertSuccessfulResponse(response, `deployed Microsoft Store manifest ${phase}`);
286
+ const bytes = await readBoundedResponse(response, maximumManifestBytes, `deployed Microsoft Store manifest ${phase}`);
287
+ const actualSha256 = hashMicrosoftStoreBytes(bytes);
288
+ if (actualSha256 !== expectedSha256) {
289
+ throw new Error(`Deployed Microsoft Store manifest SHA-256 must match submission evidence ${phase}: expected ${expectedSha256}, received ${actualSha256}.`);
290
+ }
291
+ }
292
+ async function fetchResponse(runtime, url, init, label) {
293
+ try {
294
+ return await runtime.fetch(url, init);
295
+ }
296
+ catch (error) {
297
+ throw new Error(`${label} request failed: ${formatError(error)}`);
298
+ }
299
+ }
300
+ async function assertSuccessfulResponse(response, label) {
301
+ if (response.status < 200 || response.status >= 300) {
302
+ await cancelResponse(response);
303
+ throw new Error(`${label} must return a 2xx response, received ${response.status}.`);
304
+ }
305
+ }
306
+ async function readBoundedResponse(response, maximumBytes, label) {
307
+ const declaredLength = await readContentLengthSafely(response, label);
308
+ if (declaredLength !== undefined && declaredLength > maximumBytes) {
309
+ await cancelResponse(response);
310
+ throw new Error(`${label} exceeds the ${maximumBytes}-byte size limit.`);
311
+ }
312
+ if (response.body === null) {
313
+ throw new Error(`${label} response body is missing.`);
314
+ }
315
+ const reader = response.body.getReader();
316
+ const chunks = [];
317
+ let sizeBytes = 0;
318
+ try {
319
+ while (true) {
320
+ const result = await reader.read();
321
+ if (result.done) {
322
+ break;
323
+ }
324
+ const chunk = Buffer.from(result.value);
325
+ sizeBytes += chunk.length;
326
+ if (sizeBytes > maximumBytes) {
327
+ await reader.cancel();
328
+ throw new Error(`${label} exceeds the ${maximumBytes}-byte size limit.`);
329
+ }
330
+ chunks.push(chunk);
331
+ }
332
+ }
333
+ finally {
334
+ reader.releaseLock();
335
+ }
336
+ if (declaredLength !== undefined && declaredLength !== sizeBytes) {
337
+ throw new Error(`${label} length mismatch: expected ${declaredLength}, received ${sizeBytes}.`);
338
+ }
339
+ return Buffer.concat(chunks, sizeBytes);
340
+ }
341
+ async function writeBoundedResponseToFile(response, file, maximumBytes, label) {
342
+ if (response.body === null) {
343
+ throw new Error(`${label} response body is missing.`);
344
+ }
345
+ const descriptor = openSync(file, 'wx', 0o600);
346
+ const reader = response.body.getReader();
347
+ const hash = createHash('sha256');
348
+ let sizeBytes = 0;
349
+ let completed = false;
350
+ try {
351
+ while (true) {
352
+ const result = await reader.read();
353
+ if (result.done) {
354
+ break;
355
+ }
356
+ const chunk = Buffer.from(result.value);
357
+ sizeBytes += chunk.length;
358
+ if (sizeBytes > maximumBytes) {
359
+ await reader.cancel();
360
+ throw new Error(`${label} exceeds the ${maximumBytes}-byte size limit.`);
361
+ }
362
+ hash.update(chunk);
363
+ writeAll(descriptor, chunk);
364
+ }
365
+ completed = true;
366
+ }
367
+ finally {
368
+ if (!completed) {
369
+ try {
370
+ await reader.cancel();
371
+ }
372
+ catch {
373
+ // A bounded response may already be cancelled after crossing its limit.
374
+ }
375
+ }
376
+ reader.releaseLock();
377
+ closeSync(descriptor);
378
+ if (!completed && existsSync(file)) {
379
+ unlinkSync(file);
380
+ }
381
+ }
382
+ return { sizeBytes, sha256: hash.digest('hex') };
383
+ }
384
+ function writeAll(descriptor, bytes) {
385
+ let offset = 0;
386
+ while (offset < bytes.length) {
387
+ offset += writeSync(descriptor, bytes, offset, bytes.length - offset);
388
+ }
389
+ }
390
+ function readContentLength(response, label) {
391
+ const contentEncoding = response.headers.get('content-encoding');
392
+ if (contentEncoding !== null && contentEncoding.trim().toLowerCase() !== 'identity') {
393
+ return undefined;
394
+ }
395
+ const value = response.headers.get('content-length');
396
+ if (value === null) {
397
+ return undefined;
398
+ }
399
+ if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) {
400
+ throw new Error(`${label} returned an invalid Content-Length header.`);
401
+ }
402
+ const length = Number(value);
403
+ if (!Number.isSafeInteger(length)) {
404
+ throw new Error(`${label} returned an unsupported Content-Length header.`);
405
+ }
406
+ return length;
407
+ }
408
+ export function removeMicrosoftStorePackageZipIfOwned(file, expected) {
409
+ let metadata;
410
+ try {
411
+ metadata = lstatSync(file);
412
+ }
413
+ catch (error) {
414
+ if (hasErrorCode(error, 'ENOENT')) {
415
+ return;
416
+ }
417
+ throw error;
418
+ }
419
+ if (expected.ino !== 0
420
+ && metadata.dev === expected.dev
421
+ && metadata.ino === expected.ino) {
422
+ unlinkSync(file);
423
+ return;
424
+ }
425
+ if (expected.ino !== 0 || !metadata.isFile() || metadata.dev !== expected.dev) {
426
+ return;
427
+ }
428
+ const actual = hashMicrosoftStoreFileSnapshot(file, 'published Microsoft Store package ZIP cleanup');
429
+ if (actual.sizeBytes === expected.sizeBytes
430
+ && actual.sha256 === expected.sha256) {
431
+ unlinkSync(file);
432
+ }
433
+ }
434
+ function hasErrorCode(error, code) {
435
+ return error instanceof Error && 'code' in error && error.code === code;
436
+ }
437
+ async function readContentLengthSafely(response, label) {
438
+ try {
439
+ return readContentLength(response, label);
440
+ }
441
+ catch (error) {
442
+ await cancelResponse(response);
443
+ throw error;
444
+ }
445
+ }
446
+ async function cancelResponse(response) {
447
+ try {
448
+ await response.body?.cancel();
449
+ }
450
+ catch {
451
+ // A response body may already be closed after an HTTP or validation failure.
452
+ }
453
+ }
@@ -0,0 +1,3 @@
1
+ import { type PreparedMicrosoftStorePackageGenerationInput, type RunMicrosoftStorePackageGenerationInput } from './microsoft-store-package-generation-contract.js';
2
+ export declare function prepareMicrosoftStorePackageGenerationInput(input: RunMicrosoftStorePackageGenerationInput): PreparedMicrosoftStorePackageGenerationInput;
3
+ export declare function assertMicrosoftStorePackageGenerationInputUnchanged(input: PreparedMicrosoftStorePackageGenerationInput): void;