@mpgd/cli 0.54.0 → 0.55.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.
@@ -0,0 +1,14 @@
1
+ export interface DevvitSdkVersion {
2
+ readonly name: string;
3
+ readonly declared: string;
4
+ readonly installed: string;
5
+ readonly latest: string;
6
+ readonly source?: 'adapter';
7
+ }
8
+ export interface CheckDevvitSdkOptions {
9
+ /** Test seam. Production looks up the npm `latest` dist-tag. */
10
+ readonly lookupLatest?: (name: string) => Promise<string>;
11
+ }
12
+ export declare function assertLatestDevvitSdk(entries: readonly DevvitSdkVersion[]): void;
13
+ /** Read-only pre-upload gate. Never install a new SDK between verified build and upload. */
14
+ export declare function checkDevvitSdk(project: string, options?: CheckDevvitSdkOptions): Promise<readonly DevvitSdkVersion[]>;
@@ -0,0 +1,112 @@
1
+ import { execFile as execFileCallback } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { readFile, realpath } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ const execFile = promisify(execFileCallback);
6
+ const stable = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
7
+ function isFirstPartyDevvitPackage(name) {
8
+ return name === 'devvit' || name.startsWith('@devvit/');
9
+ }
10
+ function devvitPackageNames(dependencies) {
11
+ return Object.keys(dependencies).filter(isFirstPartyDevvitPackage);
12
+ }
13
+ function record(value) {
14
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
15
+ throw new Error('Invalid package metadata.');
16
+ }
17
+ return value;
18
+ }
19
+ async function npmLatestStable(name, cwd) {
20
+ const { stdout } = await execFile(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['view', `${name}@latest`, 'version', '--json', '--registry=https://registry.npmjs.org'], {
21
+ cwd,
22
+ encoding: 'utf8',
23
+ timeout: 20_000,
24
+ maxBuffer: 16_384,
25
+ shell: process.platform === 'win32',
26
+ });
27
+ return requireStableVersion(name, JSON.parse(stdout));
28
+ }
29
+ function requireStableVersion(name, latest) {
30
+ if (typeof latest !== 'string' || !stable.test(latest)) {
31
+ throw new Error('Registry did not return a stable Devvit version: ' + name);
32
+ }
33
+ return latest;
34
+ }
35
+ export function assertLatestDevvitSdk(entries) {
36
+ if (!entries.some((e) => e.name === 'devvit' || e.name === '@devvit/cli') || !entries.some((e) => e.name === '@devvit/web' || e.name === '@devvit/public-api')) {
37
+ throw new Error('Devvit upload needs both an installed CLI and SDK in this project.');
38
+ }
39
+ for (const entry of entries) {
40
+ if (!stable.test(entry.latest) || (entry.source !== 'adapter' && entry.declared !== entry.latest) || entry.installed !== entry.latest) {
41
+ throw new Error(`${entry.name}: declared=${entry.declared}, installed=${entry.installed}, latest=${entry.latest}. Update exact SDK/CLI pins and lockfile, rebuild and test, then retry upload.`);
42
+ }
43
+ }
44
+ }
45
+ /** Read-only pre-upload gate. Never install a new SDK between verified build and upload. */
46
+ export async function checkDevvitSdk(project, options = {}) {
47
+ const manifestPath = path.resolve(project, 'package.json');
48
+ const manifest = record(JSON.parse(await readFile(manifestPath, 'utf8')));
49
+ const dependencies = {
50
+ ...(manifest.dependencies === undefined ? {} : record(manifest.dependencies)),
51
+ ...(manifest.devDependencies === undefined ? {} : record(manifest.devDependencies)),
52
+ };
53
+ async function installedPackage(from, name) {
54
+ let directory = from;
55
+ for (;;) {
56
+ const file = path.join(directory, 'node_modules', name, 'package.json');
57
+ try {
58
+ const metadata = record(JSON.parse(await readFile(file, 'utf8')));
59
+ if (metadata.name !== name || typeof metadata.version !== 'string') {
60
+ throw new Error('Invalid installed package: ' + name);
61
+ }
62
+ return { metadata, directory: path.dirname(await realpath(file)) };
63
+ }
64
+ catch (error) {
65
+ if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
66
+ throw error;
67
+ }
68
+ }
69
+ const parent = path.dirname(directory);
70
+ if (parent === directory) {
71
+ throw new Error('Cannot resolve installed Devvit package: ' + name);
72
+ }
73
+ directory = parent;
74
+ }
75
+ }
76
+ const targetDirectory = path.dirname(manifestPath);
77
+ const subjects = devvitPackageNames(dependencies).map((name) => ({
78
+ name,
79
+ declared: dependencies[name],
80
+ directory: targetDirectory,
81
+ transitive: false,
82
+ }));
83
+ if (Object.hasOwn(dependencies, '@mpgd/adapter-devvit')) {
84
+ const adapter = await installedPackage(targetDirectory, '@mpgd/adapter-devvit');
85
+ const adapterDependencies = record(adapter.metadata.dependencies);
86
+ for (const name of devvitPackageNames(adapterDependencies)) {
87
+ subjects.push({
88
+ name,
89
+ declared: adapterDependencies[name],
90
+ directory: adapter.directory,
91
+ transitive: true,
92
+ });
93
+ }
94
+ }
95
+ const entries = await Promise.all(subjects.map(async ({ name, declared, directory, transitive }) => {
96
+ if (typeof declared !== 'string') {
97
+ throw new Error('Invalid Devvit dependency: ' + name);
98
+ }
99
+ const resolved = await installedPackage(directory, name);
100
+ const installed = resolved.metadata.version;
101
+ if (typeof installed !== 'string') {
102
+ throw new Error('Missing installed Devvit version: ' + name);
103
+ }
104
+ const latest = options.lookupLatest
105
+ ? requireStableVersion(name, await options.lookupLatest(name))
106
+ : await npmLatestStable(name, path.dirname(manifestPath));
107
+ // A consumer may use a reviewed exact override until the adapter release catches up.
108
+ return { name, declared, installed, latest, ...(transitive ? { source: 'adapter' } : {}) };
109
+ }));
110
+ assertLatestDevvitSdk(entries);
111
+ return entries;
112
+ }
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { buildAssetPacks } from './asset-pack-build.js';
12
12
  import { verifyAssetPackDelivery } from './asset-pack-verify.js';
13
13
  import { buildAssetPackTarget } from './asset-pack-target.js';
14
14
  import { publishAssetPacks } from './asset-pack-publish.js';
15
+ import { assertLegalProjectPath, publishLegalSiteFiles } from './legal-site-output.js';
15
16
  import { assertAssetPackTargetPolicy } from '@mpgd/target-config/asset-packs';
16
17
  import { materializeCapacitorShellStarter, planCapacitorShellStarter, } from './capacitor-shell-starter.js';
17
18
  import { doctorNativeDeployment, initializeDeployConfig, parseDeployTargets, planNativeDeployment, readNativeDeploymentPlan, writeNativeDeploymentPlan, } from './deploy-planning.js';
@@ -26,6 +27,7 @@ import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-ge
26
27
  import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
27
28
  import { applyNotificationsStarter, notificationsProviderId, readGameCreateProviders, } from './notifications-starter.js';
28
29
  import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
30
+ import { checkDevvitSdk } from './devvit-sdk-check.js';
29
31
  import { applyToolingMigration, planToolingMigration } from './tooling-migration.js';
30
32
  import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
31
33
  import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
@@ -1818,7 +1820,10 @@ const targetCommand = defineI18n({
1818
1820
  action: 'build',
1819
1821
  target,
1820
1822
  profile,
1821
- env: createTargetCommandEnv(ctx.values, { nativeBuildTarget: target }),
1823
+ env: createTargetCommandEnv(ctx.values, {
1824
+ nativeBuildTarget: target,
1825
+ requireSourceRevision: true,
1826
+ }),
1822
1827
  ...(variant === undefined ? {} : { variant }),
1823
1828
  });
1824
1829
  },
@@ -2448,7 +2453,7 @@ const targetCommand = defineI18n({
2448
2453
  run: (ctx) => {
2449
2454
  const aitVariant = readOptionalString(ctx.values['ait-variant']);
2450
2455
  const iosVariant = readOptionalString(ctx.values['ios-variant']);
2451
- const env = createTargetCommandEnv(ctx.values);
2456
+ const env = createTargetCommandEnv(ctx.values, { requireSourceRevision: true });
2452
2457
  runTargetMatrix({
2453
2458
  action: 'build',
2454
2459
  targets: parseTargetList(readOptionalString(ctx.values.targets), env),
@@ -2521,6 +2526,18 @@ const kitCommand = defineI18n({
2521
2526
  ko: '현재 mpgd-kit 체크아웃을 점검합니다.',
2522
2527
  }),
2523
2528
  subCommands: {
2529
+ 'devvit-check': defineI18n({
2530
+ name: 'devvit-check',
2531
+ description: 'Require exact installed latest stable Devvit SDK and CLI before upload.',
2532
+ resource: commandResource({ en: 'Check latest stable Devvit SDK before upload.', ko: '업로드 전 Devvit 최신 안정 SDK를 확인합니다.' }),
2533
+ args: { project: { type: 'string', default: '.', description: 'Devvit target directory.' } },
2534
+ run: async (ctx) => {
2535
+ const entries = await checkDevvitSdk(readOptionalString(ctx.values.project) ?? '.');
2536
+ for (const entry of entries) {
2537
+ console.info(entry.name + '@' + entry.installed + ' (latest stable)');
2538
+ }
2539
+ },
2540
+ }),
2524
2541
  upgrade: defineI18n({
2525
2542
  name: 'upgrade',
2526
2543
  description: 'Plan or apply the latest published Kit package versions for a game.',
@@ -2774,9 +2791,13 @@ function resolveLegalCommandOptions(values) {
2774
2791
  function buildLegalSite(input) {
2775
2792
  const legalDir = path.resolve(process.cwd(), input.legalDir);
2776
2793
  const outDir = path.resolve(process.cwd(), input.outDir);
2794
+ assertLegalProjectPath('legal-dir', legalDir);
2795
+ assertLegalProjectPath('out-dir', outDir);
2777
2796
  const outputs = createLegalSiteOutputs(legalDir, outDir);
2778
2797
  if (input.check) {
2798
+ assertLegalProjectPath('out-dir', outDir);
2779
2799
  const stale = outputs.filter((output) => {
2800
+ assertLegalProjectPath('out-dir', output.file);
2780
2801
  if (!existsSync(output.file)) {
2781
2802
  return true;
2782
2803
  }
@@ -2792,29 +2813,24 @@ function buildLegalSite(input) {
2792
2813
  outDir,
2793
2814
  };
2794
2815
  }
2795
- for (const output of outputs) {
2796
- mkdirSync(path.dirname(output.file), { recursive: true });
2797
- writeFileSync(output.file, output.content);
2798
- }
2816
+ publishLegalSiteFiles(outDir, outputs);
2799
2817
  return {
2800
2818
  outDir,
2801
2819
  };
2802
2820
  }
2803
2821
  function createLegalSiteOutputs(legalDir, outDir) {
2804
- const relativeLegalDir = path.relative(process.cwd(), legalDir);
2805
- const relativeOutDir = path.relative(process.cwd(), outDir);
2806
- if (relativeLegalDir.startsWith('..') || path.isAbsolute(relativeLegalDir)) {
2807
- throw new Error(`legal-dir must be inside the project root: ${legalDir}`);
2808
- }
2809
- if (relativeOutDir.startsWith('..') || path.isAbsolute(relativeOutDir)) {
2810
- throw new Error(`out-dir must be inside the project root: ${outDir}`);
2811
- }
2812
- const sourceRoot = toTemplatePath(relativeLegalDir || '.');
2822
+ assertLegalProjectPath('legal-dir', legalDir);
2823
+ assertLegalProjectPath('out-dir', outDir);
2824
+ const sourceRoot = toTemplatePath(path.relative(process.cwd(), legalDir) || '.');
2813
2825
  const outputs = legalPageSlugs.map((slug) => {
2814
2826
  const sourceFile = path.join(legalDir, `${slug}.html`);
2827
+ // existsSync and readFileSync follow a page symlink. Check the file
2828
+ // itself, not only the legal directory, immediately before reading it.
2829
+ assertLegalProjectPath('legal-dir', sourceFile);
2815
2830
  if (!existsSync(sourceFile)) {
2816
2831
  throw new Error(`Missing legal page source: ${sourceFile}`);
2817
2832
  }
2833
+ assertLegalProjectPath('legal-dir', sourceFile);
2818
2834
  return {
2819
2835
  file: path.join(outDir, slug, 'index.html'),
2820
2836
  content: normalizeLegalHtml(readFileSync(sourceFile, 'utf8'), sourceFile),
@@ -3347,7 +3363,11 @@ function createTargetCommandEnv(values, options = {}) {
3347
3363
  });
3348
3364
  const env = {
3349
3365
  ...process.env,
3350
- ...createGameOwnedReleaseEnv(gameRoot, process.env),
3366
+ // Release provenance is recorded only by build commands. Smoke, doctor,
3367
+ // icons, and submission preflight must still run on an exported game tree.
3368
+ ...createGameOwnedReleaseEnv(gameRoot, process.env, {
3369
+ requireSourceRevision: options.requireSourceRevision === true,
3370
+ }),
3351
3371
  MPGD_PLATFORM_TARGETS_FILE: preparedTargetsFile,
3352
3372
  ...(targetConfigExtensionsFile === undefined
3353
3373
  ? {}
@@ -3450,10 +3470,9 @@ function resolveTargetConfigExtensionsFile(gameRoot, configuredFile) {
3450
3470
  const gameOwnedFile = path.join(gameRoot, 'mpgd.target-config.json');
3451
3471
  return existsSync(gameOwnedFile) ? gameOwnedFile : undefined;
3452
3472
  }
3453
- function createGameOwnedReleaseEnv(gameRoot, baseEnv) {
3473
+ function createGameOwnedReleaseEnv(gameRoot, baseEnv, options) {
3454
3474
  const monetizationFiles = resolveGameOwnedMonetizationFiles(gameRoot, baseEnv);
3455
- const sourceGitSha = readOptionalString(baseEnv.MPGD_SOURCE_GIT_SHA)
3456
- ?? readSourceGitSha(gameRoot);
3475
+ const sourceGitSha = resolveCommandSourceGitSha(gameRoot, baseEnv, options.requireSourceRevision);
3457
3476
  const appVersion = readOptionalString(baseEnv.APP_VERSION)
3458
3477
  ?? readGamePackageVersion(gameRoot);
3459
3478
  const notificationTopicsFile = readConfiguredPath(baseEnv.MPGD_NOTIFICATION_TOPICS_FILE);
@@ -3464,7 +3483,7 @@ function createGameOwnedReleaseEnv(gameRoot, baseEnv) {
3464
3483
  ...(notificationTopicsFile === undefined
3465
3484
  ? {}
3466
3485
  : { MPGD_NOTIFICATION_TOPICS_FILE: path.resolve(gameRoot, notificationTopicsFile) }),
3467
- MPGD_SOURCE_GIT_SHA: sourceGitSha,
3486
+ ...(sourceGitSha === undefined ? {} : { MPGD_SOURCE_GIT_SHA: sourceGitSha }),
3468
3487
  ...(appVersion === undefined ? {} : { APP_VERSION: appVersion }),
3469
3488
  };
3470
3489
  }
@@ -3499,15 +3518,48 @@ function readConfiguredPath(value) {
3499
3518
  const normalized = value?.trim();
3500
3519
  return normalized === undefined || normalized.length === 0 ? undefined : normalized;
3501
3520
  }
3521
+ function resolveCommandSourceGitSha(gameRoot, baseEnv, requireSourceRevision) {
3522
+ const configuredSourceGitSha = readOptionalString(baseEnv.MPGD_SOURCE_GIT_SHA);
3523
+ if (requireSourceRevision) {
3524
+ return configuredSourceGitSha === undefined
3525
+ ? readSourceGitSha(gameRoot)
3526
+ : requireSourceGitSha(configuredSourceGitSha);
3527
+ }
3528
+ if (configuredSourceGitSha !== undefined) {
3529
+ return isSourceGitSha(configuredSourceGitSha) ? configuredSourceGitSha : undefined;
3530
+ }
3531
+ const detectedSourceGitSha = readGitHead(gameRoot);
3532
+ return detectedSourceGitSha !== undefined && isSourceGitSha(detectedSourceGitSha)
3533
+ ? detectedSourceGitSha
3534
+ : undefined;
3535
+ }
3502
3536
  function readSourceGitSha(gameRoot) {
3537
+ const head = readGitHead(gameRoot);
3538
+ if (head === undefined) {
3539
+ throw new Error(`Failed to resolve the game source Git revision for ${gameRoot}. `
3540
+ + 'Set MPGD_SOURCE_GIT_SHA to its lowercase 40-character commit SHA.');
3541
+ }
3542
+ return requireSourceGitSha(head);
3543
+ }
3544
+ function readGitHead(gameRoot) {
3503
3545
  const result = spawnSync('git', ['-C', gameRoot, 'rev-parse', 'HEAD'], {
3504
3546
  encoding: 'utf8',
3505
3547
  stdio: ['ignore', 'pipe', 'ignore'],
3506
3548
  });
3507
3549
  if (result.error !== undefined || result.status !== 0) {
3508
- return 'uncommitted';
3550
+ return undefined;
3551
+ }
3552
+ const head = result.stdout.trim();
3553
+ return head.length === 0 ? undefined : head;
3554
+ }
3555
+ function requireSourceGitSha(value) {
3556
+ if (!isSourceGitSha(value)) {
3557
+ throw new Error('MPGD_SOURCE_GIT_SHA must be a lowercase 40-character game source SHA.');
3509
3558
  }
3510
- return result.stdout.trim() || 'uncommitted';
3559
+ return value;
3560
+ }
3561
+ function isSourceGitSha(value) {
3562
+ return /^[0-9a-f]{40}$/u.test(value);
3511
3563
  }
3512
3564
  function readGamePackageVersion(gameRoot) {
3513
3565
  const packageFile = path.join(gameRoot, 'package.json');
@@ -1,6 +1,6 @@
1
1
  import { execFile as execFileCallback, spawnSync } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
3
- import { existsSync, lstatSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { existsSync, lstatSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { compare, intersects, minVersion, satisfies, valid, validRange } from 'semver';
@@ -154,6 +154,9 @@ export function applyKitUpgrade(plan, runLockfileUpdate = updatePnpmLockfile) {
154
154
  ...plan.manifestDigests,
155
155
  ...plan.lockfileDigests,
156
156
  })) {
157
+ // Re-check links before the digest read. readFileSync follows a symlink,
158
+ // so a same-bytes swap would otherwise look unchanged.
159
+ assertOwnedUpgradePath(upgradeWriteAnchor(plan, file), file);
157
160
  const current = readFileSync(file, 'utf8');
158
161
  if (digest(current) !== expected) {
159
162
  throw new Error('File changed after upgrade planning: ' + file);
@@ -176,9 +179,11 @@ export function applyKitUpgrade(plan, runLockfileUpdate = updatePnpmLockfile) {
176
179
  section[update.packageName] = update.next;
177
180
  parsed[update.section] = section;
178
181
  }
179
- writeFileSync(manifest, stringifyLike(source, parsed));
182
+ writeUpgradeFile(plan, manifest, stringifyLike(source, parsed));
180
183
  }
181
184
  for (const root of plan.workspaceRoots) {
185
+ const lockfile = path.join(root, 'pnpm-lock.yaml');
186
+ assertOwnedUpgradePath(upgradeWriteAnchor(plan, lockfile), lockfile);
182
187
  runLockfileUpdate(root);
183
188
  }
184
189
  }
@@ -186,7 +191,7 @@ export function applyKitUpgrade(plan, runLockfileUpdate = updatePnpmLockfile) {
186
191
  const restoreFailures = [];
187
192
  for (const [file, original] of snapshots) {
188
193
  try {
189
- writeFileSync(file, original);
194
+ writeUpgradeFile(plan, file, original);
190
195
  }
191
196
  catch (restoreError) {
192
197
  restoreFailures.push(file + ': ' + formatError(restoreError));
@@ -473,6 +478,100 @@ function stringifyLike(original, value) {
473
478
  return JSON.stringify(value, null, indent).replaceAll('\n', newline)
474
479
  + (original.endsWith('\n') ? newline : '');
475
480
  }
481
+ function upgradeWriteAnchor(plan, file) {
482
+ const resolved = path.resolve(file);
483
+ let anchor = plan.gameRoot;
484
+ while (!inside(anchor, resolved)) {
485
+ const parent = path.dirname(anchor);
486
+ if (parent === anchor) {
487
+ throw new Error('Refusing to write outside the planned upgrade directory: ' + file);
488
+ }
489
+ anchor = parent;
490
+ }
491
+ // In-game files used to return plan.gameRoot before this check. A game
492
+ // directory or parent swapped for a symlink after planning then skipped
493
+ // lstat/realpath, and assertOwnedUpgradePath only inspects children.
494
+ let anchorStat;
495
+ try {
496
+ anchorStat = lstatSync(anchor);
497
+ }
498
+ catch (error) {
499
+ throw new Error('Refusing to write through a symbolic link: ' + anchor, { cause: error });
500
+ }
501
+ if (anchorStat.isSymbolicLink()) {
502
+ throw new Error('Refusing to write through a symbolic link: ' + anchor);
503
+ }
504
+ const anchorReal = realpathSync(anchor);
505
+ // Compare with the game root captured at planning time. A fresh realpath of
506
+ // both sides would follow a parent swapped for a symlink and look consistent.
507
+ if (!inside(anchorReal, plan.gameRoot)
508
+ && path.resolve(anchorReal) !== path.resolve(plan.gameRoot)) {
509
+ throw new Error('Refusing to write through a symbolic link: ' + anchor);
510
+ }
511
+ return anchor;
512
+ }
513
+ function assertOwnedUpgradePath(anchor, file) {
514
+ const resolved = path.resolve(file);
515
+ if (!inside(anchor, resolved)) {
516
+ throw new Error('Refusing to write outside the planned upgrade directory: ' + file);
517
+ }
518
+ const relative = path.relative(anchor, resolved);
519
+ const parts = relative.split(path.sep).filter((part) => part !== '' && part !== '.');
520
+ let current = anchor;
521
+ for (let index = 0; index < parts.length; index += 1) {
522
+ const part = parts[index];
523
+ if (part === undefined) {
524
+ continue;
525
+ }
526
+ current = path.join(current, part);
527
+ const isFinal = index === parts.length - 1;
528
+ let stat;
529
+ try {
530
+ stat = lstatSync(current);
531
+ }
532
+ catch (error) {
533
+ if (isErrno(error, 'ENOENT') && isFinal) {
534
+ return;
535
+ }
536
+ throw new Error('Upgrade file disappeared before write: ' + file, { cause: error });
537
+ }
538
+ if (stat.isSymbolicLink()) {
539
+ throw new Error('Refusing to write through a symbolic link: ' + current);
540
+ }
541
+ }
542
+ if (!inside(realpathSync(anchor), realpathSync(resolved))) {
543
+ throw new Error('Refusing to write through a symbolic link: ' + file);
544
+ }
545
+ }
546
+ function writeUpgradeFile(plan, file, source) {
547
+ const anchor = upgradeWriteAnchor(plan, file);
548
+ assertOwnedUpgradePath(anchor, file);
549
+ const temporary = `${file}.mpgd-${randomUUID()}.tmp`;
550
+ try {
551
+ let mode = 0o644;
552
+ try {
553
+ const stat = lstatSync(file);
554
+ if (stat.isSymbolicLink()) {
555
+ throw new Error('Refusing to write through a symbolic link: ' + file);
556
+ }
557
+ mode = stat.mode & 0o777;
558
+ }
559
+ catch (error) {
560
+ if (!isErrno(error, 'ENOENT')) {
561
+ throw error;
562
+ }
563
+ }
564
+ writeFileSync(temporary, source, { flag: 'wx', mode });
565
+ assertOwnedUpgradePath(anchor, file);
566
+ renameSync(temporary, file);
567
+ }
568
+ finally {
569
+ rmSync(temporary, { force: true });
570
+ }
571
+ }
572
+ function isErrno(error, code) {
573
+ return error !== null && typeof error === 'object' && 'code' in error && error.code === code;
574
+ }
476
575
  function inside(root, candidate) {
477
576
  const relative = path.relative(root, candidate);
478
577
  return relative === '' || (!relative.startsWith('..' + path.sep) && relative !== '..'
@@ -0,0 +1,12 @@
1
+ export interface LegalSiteFile {
2
+ readonly file: string;
3
+ readonly content: string;
4
+ }
5
+ /**
6
+ * Legal-site paths are contained only after the nearest existing ancestor is
7
+ * canonicalized. A lexical `path.relative` check treats a symlinked `artifacts`
8
+ * directory as inside the project while later writes follow the link.
9
+ */
10
+ export declare function assertLegalProjectPath(label: string, resolved: string): void;
11
+ /** Stage the site beside the destination, then publish with rename. */
12
+ export declare function publishLegalSiteFiles(outDir: string, outputs: readonly LegalSiteFile[]): void;
@@ -0,0 +1,226 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { lstatSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ /**
5
+ * Legal-site paths are contained only after the nearest existing ancestor is
6
+ * canonicalized. A lexical `path.relative` check treats a symlinked `artifacts`
7
+ * directory as inside the project while later writes follow the link.
8
+ */
9
+ export function assertLegalProjectPath(label, resolved) {
10
+ const lexicalRoot = process.cwd();
11
+ const lexical = path.relative(lexicalRoot, resolved);
12
+ if (lexical.startsWith('..') || path.isAbsolute(lexical)) {
13
+ throw new Error(`${label} must be inside the project root: ${resolved}`);
14
+ }
15
+ assertNoLegalSymlink(lexicalRoot, resolved, label);
16
+ const canonicalRoot = realpathSync(lexicalRoot);
17
+ const canonical = realpathBestEffort(resolved);
18
+ const canonicalRelative = path.relative(canonicalRoot, canonical);
19
+ if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
20
+ throw new Error(`${label} must be inside the project root: ${resolved}`);
21
+ }
22
+ }
23
+ /** Stage the site beside the destination, then publish with rename. */
24
+ export function publishLegalSiteFiles(outDir, outputs) {
25
+ assertLegalProjectPath('out-dir', outDir);
26
+ // A temp dir on the project root is a different filesystem when out-dir is a
27
+ // mount or sits under a bind-mounted artifacts directory. Rename then fails
28
+ // with EXDEV. Stage on the destination filesystem instead.
29
+ const staging = path.join(legalStagingParent(outDir), `.mpgd-legal-${randomUUID()}`);
30
+ let publishedDirectory = false;
31
+ try {
32
+ for (const output of outputs) {
33
+ const relative = path.relative(outDir, output.file);
34
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
35
+ throw new Error(`Legal site output escapes the output directory: ${output.file}`);
36
+ }
37
+ const staged = path.join(staging, relative);
38
+ mkdirSync(path.dirname(staged), { recursive: true });
39
+ writeFileSync(staged, output.content, { flag: 'wx' });
40
+ }
41
+ assertLegalProjectPath('out-dir', outDir);
42
+ let existing;
43
+ try {
44
+ existing = lstatSync(outDir);
45
+ }
46
+ catch (error) {
47
+ if (!isErrno(error, 'ENOENT')) {
48
+ throw error;
49
+ }
50
+ }
51
+ if (existing?.isSymbolicLink() === true) {
52
+ throw new Error(`Refusing to write legal site output through a symbolic link: ${outDir}`);
53
+ }
54
+ if (existing === undefined) {
55
+ ensureLegalDirectory(path.dirname(outDir));
56
+ assertLegalProjectPath('out-dir', outDir);
57
+ renameSync(staging, outDir);
58
+ publishedDirectory = true;
59
+ return;
60
+ }
61
+ if (!existing.isDirectory()) {
62
+ throw new Error(`Legal site output path is not a directory: ${outDir}`);
63
+ }
64
+ for (const output of outputs) {
65
+ assertNoLegalSymlink(process.cwd(), output.file, 'out-dir');
66
+ ensureLegalDirectory(path.dirname(output.file));
67
+ assertNoLegalSymlink(process.cwd(), output.file, 'out-dir');
68
+ renameSync(path.join(staging, path.relative(outDir, output.file)), output.file);
69
+ }
70
+ }
71
+ finally {
72
+ if (!publishedDirectory) {
73
+ rmSync(staging, { recursive: true, force: true });
74
+ }
75
+ }
76
+ }
77
+ /**
78
+ * Parent for the staging directory. An existing out-dir may itself be a mount,
79
+ * so stage inside it. A missing out-dir is created on its nearest existing
80
+ * ancestor, which is the filesystem the later rename must stay on.
81
+ */
82
+ function legalStagingParent(outDir) {
83
+ const existing = lstatIfPresent(outDir);
84
+ if (existing !== undefined) {
85
+ if (existing.isSymbolicLink()) {
86
+ throw new Error(`Refusing to write legal site output through a symbolic link: ${outDir}`);
87
+ }
88
+ if (!existing.isDirectory()) {
89
+ throw new Error(`Legal site output path is not a directory: ${outDir}`);
90
+ }
91
+ return outDir;
92
+ }
93
+ let current = path.dirname(outDir);
94
+ for (;;) {
95
+ const stat = lstatIfPresent(current);
96
+ if (stat !== undefined) {
97
+ if (stat.isSymbolicLink()) {
98
+ throw new Error(`Refusing to write legal site output through a symbolic link: ${current}`);
99
+ }
100
+ if (!stat.isDirectory()) {
101
+ throw new Error(`Legal site output path is not a directory: ${current}`);
102
+ }
103
+ return current;
104
+ }
105
+ const parent = path.dirname(current);
106
+ if (parent === current) {
107
+ throw new Error(`out-dir must be inside the project root: ${outDir}`);
108
+ }
109
+ current = parent;
110
+ }
111
+ }
112
+ function lstatIfPresent(target) {
113
+ try {
114
+ return lstatSync(target);
115
+ }
116
+ catch (error) {
117
+ if (isErrno(error, 'ENOENT')) {
118
+ return undefined;
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+ /** Resolve symlinks for the longest existing ancestor, keeping the remainder. */
124
+ function realpathBestEffort(target) {
125
+ // lstat-based existence stops the walk at dangling symlinks, so realpath
126
+ // fails closed on them instead of lexically skipping the link.
127
+ const exists = (candidate) => {
128
+ try {
129
+ lstatSync(candidate);
130
+ return true;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ };
136
+ let existing = target;
137
+ while (!exists(existing)) {
138
+ const parent = path.dirname(existing);
139
+ if (parent === existing) {
140
+ return existing;
141
+ }
142
+ existing = parent;
143
+ }
144
+ let real;
145
+ try {
146
+ real = realpathSync(existing);
147
+ }
148
+ catch (error) {
149
+ throw new Error(`Legal site path resolves through a broken symbolic link: ${target}`, {
150
+ cause: error,
151
+ });
152
+ }
153
+ return existing === target ? real : path.resolve(real, path.relative(existing, target));
154
+ }
155
+ function assertNoLegalSymlink(root, target, label) {
156
+ const relative = path.relative(root, target);
157
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
158
+ throw new Error(`${label} must be inside the project root: ${target}`);
159
+ }
160
+ const verb = label === 'legal-dir' ? 'read legal sources' : 'write legal site output';
161
+ let current = root;
162
+ for (const part of relative.split(path.sep)) {
163
+ if (part === '' || part === '.') {
164
+ continue;
165
+ }
166
+ current = path.join(current, part);
167
+ let stat;
168
+ try {
169
+ stat = lstatSync(current);
170
+ }
171
+ catch (error) {
172
+ if (isErrno(error, 'ENOENT')) {
173
+ return;
174
+ }
175
+ throw error;
176
+ }
177
+ if (stat.isSymbolicLink()) {
178
+ throw new Error(`Refusing to ${verb} through a symbolic link: ${current}`);
179
+ }
180
+ }
181
+ }
182
+ function ensureLegalDirectory(directory) {
183
+ const root = process.cwd();
184
+ const relative = path.relative(root, directory);
185
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
186
+ throw new Error(`out-dir must be inside the project root: ${directory}`);
187
+ }
188
+ if (relative === '') {
189
+ return;
190
+ }
191
+ let current = root;
192
+ for (const part of relative.split(path.sep)) {
193
+ if (part === '' || part === '.') {
194
+ continue;
195
+ }
196
+ current = path.join(current, part);
197
+ let stat;
198
+ try {
199
+ stat = lstatSync(current);
200
+ }
201
+ catch (error) {
202
+ if (!isErrno(error, 'ENOENT')) {
203
+ throw error;
204
+ }
205
+ try {
206
+ mkdirSync(current);
207
+ continue;
208
+ }
209
+ catch (mkdirError) {
210
+ if (!isErrno(mkdirError, 'EEXIST')) {
211
+ throw mkdirError;
212
+ }
213
+ stat = lstatSync(current);
214
+ }
215
+ }
216
+ if (stat.isSymbolicLink()) {
217
+ throw new Error(`Refusing to write legal site output through a symbolic link: ${current}`);
218
+ }
219
+ if (!stat.isDirectory()) {
220
+ throw new Error(`Legal site output path is not a directory: ${current}`);
221
+ }
222
+ }
223
+ }
224
+ function isErrno(error, code) {
225
+ return error !== null && typeof error === 'object' && 'code' in error && error.code === code;
226
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "95ea69d3e9755256b27a3b0ea230812a632ed03b",
2
+ "kitGitSha": "5e470aab672ae58128c6834786aadce5b41b484c",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.54.0"
4
+ "packageVersion": "0.55.0"
5
5
  }
@@ -6990,9 +6990,6 @@ var releaseBackoffMs = 60 * secondMs;
6990
6990
  // node_modules/.cache/mpgd-native-build/adapters/devvit/src/server/redis-notification-store.js
6991
6991
  var defaultPurgeLimit = Number.POSITIVE_INFINITY;
6992
6992
 
6993
- // node_modules/.cache/mpgd-native-build/adapters/devvit/src/notifications-contract.js
6994
- var devvitHostCodeCharacters = "[A-Za-z0-9_.:-]{1,128}";
6995
-
6996
6993
  // node_modules/.cache/mpgd-native-build/adapters/devvit/src/server/notifications-delivery.js
6997
6994
  var devvitNotificationProviderCodes = Object.freeze({
6998
6995
  deviceRecipient: "DEVVIT_DEVICE_RECIPIENT",
@@ -7023,13 +7020,17 @@ var devvitNotificationSchedulerTasks = Object.freeze({
7023
7020
  })
7024
7021
  });
7025
7022
 
7023
+ // node_modules/.cache/mpgd-native-build/packages/bridge/src/notifications.js
7024
+ var hostCodeCharacters = "[A-Za-z0-9_.:-]{1,128}";
7025
+ var hostCodeValue = new RegExp(`^${hostCodeCharacters}$`, "u");
7026
+ var hostCodeSuffix = new RegExp(`\\(host code (${hostCodeCharacters})\\)$`, "u");
7027
+
7026
7028
  // node_modules/.cache/mpgd-native-build/adapters/devvit/src/server/notifications-bridge.js
7027
7029
  var devvitNotificationHostCodes = Object.freeze({
7028
7030
  optInRefused: "DEVVIT_OPT_IN_REFUSED",
7029
7031
  optOutRefused: "DEVVIT_OPT_OUT_REFUSED",
7030
7032
  pluginFailed: "DEVVIT_NOTIFICATIONS_PLUGIN_FAILED"
7031
7033
  });
7032
- var hostCodeValue = new RegExp(`^${devvitHostCodeCharacters}$`, "u");
7033
7034
 
7034
7035
  // node_modules/.cache/mpgd-native-build/packages/cli/src/target-name.js
7035
7036
  var deploymentTargetNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
@@ -9502,9 +9503,21 @@ var assertReleaseManifestStructure = /* @__PURE__ */ (() => {
9502
9503
  };
9503
9504
  })();
9504
9505
  var fullGitShaPattern = /^[0-9a-f]{40}$/u;
9506
+ var sha256DigestPattern = /^[0-9a-f]{64}$/u;
9505
9507
  function assertReleaseManifest(input) {
9506
9508
  const manifest = assertReleaseManifestStructure(input);
9509
+ if (!fullGitShaPattern.test(manifest.gitSha)) {
9510
+ throw new TypeError("Release manifest gitSha must be a lowercase 40-character SHA.");
9511
+ }
9512
+ if (!fullGitShaPattern.test(manifest.kitGitSha)) {
9513
+ throw new TypeError("Release manifest kitGitSha must be a lowercase 40-character SHA.");
9514
+ }
9507
9515
  for (const [target2, entry] of Object.entries(manifest.targets)) {
9516
+ assertSha256Digest(entry.effectiveConfig.digest, `Release manifest ${target2} effectiveConfig.digest`);
9517
+ assertSha256Digest(entry.iconManifest.digest, `Release manifest ${target2} iconManifest.digest`);
9518
+ assertSha256Digest(entry.iconManifest.sourceSha256, `Release manifest ${target2} iconManifest.sourceSha256`);
9519
+ assertSha256Digest(entry.iconManifest.sharedConfigSha256, `Release manifest ${target2} iconManifest.sharedConfigSha256`);
9520
+ assertSha256Digest(entry.iconManifest.renderConfigSha256, `Release manifest ${target2} iconManifest.renderConfigSha256`);
9508
9521
  const delivery = entry.nativeDelivery;
9509
9522
  if (delivery === void 0) {
9510
9523
  continue;
@@ -9519,9 +9532,6 @@ function assertReleaseManifest(input) {
9519
9532
  throw new TypeError(`Release manifest production native delivery is unsigned: ${target2}.`);
9520
9533
  }
9521
9534
  }
9522
- if (!fullGitShaPattern.test(manifest.kitGitSha)) {
9523
- throw new TypeError("Release manifest kitGitSha must be a lowercase 40-character SHA.");
9524
- }
9525
9535
  if (manifest.releaseIdentity !== void 0) {
9526
9536
  const releaseIdentity = createMpgdReleaseIdentity({
9527
9537
  gameVersion: manifest.releaseIdentity.gameVersion,
@@ -9543,6 +9553,11 @@ function assertReleaseManifest(input) {
9543
9553
  }
9544
9554
  return manifest;
9545
9555
  }
9556
+ function assertSha256Digest(value, label) {
9557
+ if (!sha256DigestPattern.test(value)) {
9558
+ throw new TypeError(`${label} must be a lowercase 64-character SHA-256 digest.`);
9559
+ }
9560
+ }
9546
9561
 
9547
9562
  // node_modules/.cache/mpgd-native-build/tools/target/release-manifest-lock.js
9548
9563
  import { randomUUID } from "node:crypto";
@@ -9641,7 +9656,7 @@ function canRecoverLock(lockFile) {
9641
9656
  // node_modules/.cache/mpgd-native-build/tools/target/generate-release-manifest.js
9642
9657
  var __typia_transform__accessExpressionAsString6 = _accessExpressionAsString_110._accessExpressionAsString;
9643
9658
  var assertProductCatalog = /* @__PURE__ */ (() => {
9644
- const _ae0 = '({ readonly type: "currency"; readonly currency: string & MinLength<1>; readonly amount: number; } | { readonly type: "entitlement"; readonly entitlement: string; } | { readonly type: "resource"; readonly resource: string & MinLength<1>; readonly amount: number & ExclusiveMinimum<0> & Maximum<1.7976931348623157e+308>; })';
9659
+ const _ae0 = '({ readonly type: "currency"; readonly currency: string & MinLength<1>; readonly amount: PositiveLedgerAmount; } | { readonly type: "entitlement"; readonly entitlement: string; } | { readonly type: "resource"; readonly resource: string & MinLength<1>; readonly amount: number & ExclusiveMinimum<0> & Maximum<1.7976931348623157e+308>; })';
9645
9660
  const _ao0 = (input, _path, _exceptionable = true) => ("string" === typeof input.version || _assertGuard_19._assertGuard(_exceptionable, {
9646
9661
  method: "typia.createAssert",
9647
9662
  path: _path + ".version",
@@ -9739,10 +9754,25 @@ var assertProductCatalog = /* @__PURE__ */ (() => {
9739
9754
  path: _path + ".currency",
9740
9755
  expected: "(string & MinLength<1>)",
9741
9756
  value: input.currency
9742
- }, _errorFactory)) && ("number" === typeof input.amount || _assertGuard_19._assertGuard(_exceptionable, {
9757
+ }, _errorFactory)) && ("number" === typeof input.amount && (1 <= input.amount || _assertGuard_19._assertGuard(_exceptionable, {
9743
9758
  method: "typia.createAssert",
9744
9759
  path: _path + ".amount",
9745
- expected: "number",
9760
+ expected: "number & Minimum<1>",
9761
+ value: input.amount
9762
+ }, _errorFactory)) && (input.amount <= 9007199254740991 || _assertGuard_19._assertGuard(_exceptionable, {
9763
+ method: "typia.createAssert",
9764
+ path: _path + ".amount",
9765
+ expected: "number & Maximum<9007199254740991>",
9766
+ value: input.amount
9767
+ }, _errorFactory)) && (input.amount % 1 === 0 || _assertGuard_19._assertGuard(_exceptionable, {
9768
+ method: "typia.createAssert",
9769
+ path: _path + ".amount",
9770
+ expected: "number & MultipleOf<1>",
9771
+ value: input.amount
9772
+ }, _errorFactory)) || _assertGuard_19._assertGuard(_exceptionable, {
9773
+ method: "typia.createAssert",
9774
+ path: _path + ".amount",
9775
+ expected: "(number & Minimum<1> & Maximum<9007199254740991> & MultipleOf<1>)",
9746
9776
  value: input.amount
9747
9777
  }, _errorFactory));
9748
9778
  const _ao3 = (input, _path, _exceptionable = true) => ("entitlement" === input.type || _assertGuard_19._assertGuard(_exceptionable, {
@@ -9815,7 +9845,7 @@ var assertProductCatalog = /* @__PURE__ */ (() => {
9815
9845
  })();
9816
9846
  const _io0 = (input) => "string" === typeof input.version && (void 0 === input.currencies || Array.isArray(input.currencies) && (1 <= input.currencies.length && input.currencies.every((elem) => "string" === typeof elem && 1 <= elem.length))) && (Array.isArray(input.products) && input.products.every((elem) => "object" === typeof elem && null !== elem && _io1(elem)));
9817
9847
  const _io1 = (input) => "string" === typeof input.id && ("consumable" === input.type || "non_consumable" === input.type || "subscription" === input.type) && ("object" === typeof input.grant && null !== input.grant && _iu0(input.grant)) && ("object" === typeof input.platformProductIds && null !== input.platformProductIds && false === Array.isArray(input.platformProductIds) && _io5(input.platformProductIds));
9818
- const _io2 = (input) => "currency" === input.type && ("string" === typeof input.currency && 1 <= input.currency.length) && "number" === typeof input.amount;
9848
+ const _io2 = (input) => "currency" === input.type && ("string" === typeof input.currency && 1 <= input.currency.length) && ("number" === typeof input.amount && (1 <= input.amount && input.amount <= 9007199254740991 && input.amount % 1 === 0));
9819
9849
  const _io3 = (input) => "entitlement" === input.type && "string" === typeof input.entitlement;
9820
9850
  const _io4 = (input) => "resource" === input.type && ("string" === typeof input.resource && 1 <= input.resource.length) && ("number" === typeof input.amount && (0 < input.amount && input.amount <= 17976931348623157e292));
9821
9851
  const _io5 = (input) => Object.keys(input).every((key) => {
@@ -9855,16 +9885,31 @@ var assertProductCatalog = /* @__PURE__ */ (() => {
9855
9885
  };
9856
9886
  })();
9857
9887
  var assertAdPlacements = /* @__PURE__ */ (() => {
9858
- const _ip0 = (input) => "number" === typeof input["amount"];
9888
+ const _ip0 = (input) => "number" === typeof input["amount"] && (1 <= input["amount"] && input["amount"] <= 9007199254740991 && input["amount"] % 1 === 0);
9859
9889
  const _ae0 = '(AdPlacementEntryBase & { readonly type: "banner" | "interstitial"; readonly reward?: never; } | AdPlacementEntryBase & { readonly type: "rewarded"; readonly reward?: AdReward; })';
9860
- const _ae1 = '(undefined | { readonly type: "continue"; readonly amount: number; } | { readonly type: "currency"; readonly amount: number; readonly currency: string & MinLength<1>; })';
9861
- const _ap0 = (input, _path, _exceptionable = true) => "number" === typeof input["amount"] || _assertGuard_19._assertGuard(_exceptionable, {
9890
+ const _ae1 = '(undefined | { readonly type: "continue"; readonly amount: PositiveLedgerAmount; } | { readonly type: "currency"; readonly amount: PositiveLedgerAmount; readonly currency: string & MinLength<1>; })';
9891
+ const _ap0 = (input, _path, _exceptionable = true) => "number" === typeof input["amount"] && (1 <= input["amount"] || _assertGuard_19._assertGuard(_exceptionable, {
9862
9892
  method: "typia.createAssert",
9863
9893
  path: _path + ".amount",
9864
- expected: "number",
9894
+ expected: "number & Minimum<1>",
9895
+ value: input["amount"]
9896
+ }, _errorFactory)) && (input["amount"] <= 9007199254740991 || _assertGuard_19._assertGuard(_exceptionable, {
9897
+ method: "typia.createAssert",
9898
+ path: _path + ".amount",
9899
+ expected: "number & Maximum<9007199254740991>",
9900
+ value: input["amount"]
9901
+ }, _errorFactory)) && (input["amount"] % 1 === 0 || _assertGuard_19._assertGuard(_exceptionable, {
9902
+ method: "typia.createAssert",
9903
+ path: _path + ".amount",
9904
+ expected: "number & MultipleOf<1>",
9905
+ value: input["amount"]
9906
+ }, _errorFactory)) || _assertGuard_19._assertGuard(_exceptionable, {
9907
+ method: "typia.createAssert",
9908
+ path: _path + ".amount",
9909
+ expected: "(number & Minimum<1> & Maximum<9007199254740991> & MultipleOf<1>)",
9865
9910
  value: input["amount"]
9866
9911
  }, _errorFactory);
9867
- const _ae2 = '({ readonly type: "continue"; readonly amount: number; } | { readonly type: "currency"; readonly amount: number; readonly currency: string & MinLength<1>; })';
9912
+ const _ae2 = '({ readonly type: "continue"; readonly amount: PositiveLedgerAmount; } | { readonly type: "currency"; readonly amount: PositiveLedgerAmount; readonly currency: string & MinLength<1>; })';
9868
9913
  const _ao0 = (input, _path, _exceptionable = true) => ("string" === typeof input.version || _assertGuard_19._assertGuard(_exceptionable, {
9869
9914
  method: "typia.createAssert",
9870
9915
  path: _path + ".version",
@@ -10373,9 +10418,47 @@ function readSdkMajor(envValue, metadataValue) {
10373
10418
  function getSourceGitSha(kitGitSha) {
10374
10419
  const configuredGitSha = readOptionalString2(process.env.MPGD_SOURCE_GIT_SHA);
10375
10420
  if (configuredGitSha !== void 0) {
10376
- return configuredGitSha;
10421
+ return requireFullGitSha(configuredGitSha, "MPGD_SOURCE_GIT_SHA");
10377
10422
  }
10378
- return kitGitSha;
10423
+ if (isInRepoKitBuild()) {
10424
+ return inRepoKitSourceGitSha(kitGitSha);
10425
+ }
10426
+ throw new Error("MPGD_SOURCE_GIT_SHA is required for a downstream game build and must be a lowercase 40-character SHA. Refusing to record the kit revision as the game revision.");
10427
+ }
10428
+ function inRepoKitSourceGitSha(kitGitSha) {
10429
+ return requireFullGitSha(kitGitSha, "in-repo kit source Git SHA");
10430
+ }
10431
+ function isInRepoKitBuild() {
10432
+ if (process.env.MPGD_NATIVE_PACKAGED_BUILD === "1") {
10433
+ return false;
10434
+ }
10435
+ const targetsFile = readOptionalString2(process.env.MPGD_PLATFORM_TARGETS_FILE);
10436
+ if (targetsFile === void 0) {
10437
+ return true;
10438
+ }
10439
+ return isInsideDirectory(resolveKitRoot(), targetsFile);
10440
+ }
10441
+ function isInsideDirectory(root, candidate) {
10442
+ let resolvedRoot;
10443
+ try {
10444
+ resolvedRoot = realpathSync6(root);
10445
+ } catch {
10446
+ return false;
10447
+ }
10448
+ const resolvedCandidate = resolve12(candidate);
10449
+ let comparable = resolvedCandidate;
10450
+ try {
10451
+ comparable = realpathSync6(resolvedCandidate);
10452
+ } catch {
10453
+ }
10454
+ const rel = relative9(resolvedRoot, comparable);
10455
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute8(rel);
10456
+ }
10457
+ function requireFullGitSha(value, label) {
10458
+ if (!/^[0-9a-f]{40}$/u.test(value)) {
10459
+ throw new Error(`${label} must be a lowercase 40-character SHA.`);
10460
+ }
10461
+ return value;
10379
10462
  }
10380
10463
  function resolveKitGitSha() {
10381
10464
  if (process.env.MPGD_NATIVE_PACKAGED_BUILD === "1") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,10 +70,10 @@
70
70
  "parse5": "7.3.0",
71
71
  "postcss": "8.5.28",
72
72
  "postcss-value-parser": "4.2.0",
73
- "@mpgd/catalog": "0.10.5",
74
- "@mpgd/platform": "0.27.0",
75
- "@mpgd/phaser-assets": "0.9.1",
76
- "@mpgd/target-config": "0.23.5"
73
+ "@mpgd/catalog": "0.10.7",
74
+ "@mpgd/target-config": "0.23.7",
75
+ "@mpgd/platform": "0.27.2",
76
+ "@mpgd/phaser-assets": "0.9.1"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/node": "^24.0.0",
@@ -81,18 +81,18 @@
81
81
  "@types/yauzl": "3.4.0",
82
82
  "ttsc": "0.32.1",
83
83
  "typescript": "7.0.2",
84
- "@mpgd/adapter-ait": "0.20.1",
85
- "@mpgd/adapter-browser": "0.11.5",
86
- "@mpgd/adapter-devvit": "0.13.3",
87
- "@mpgd/adapter-verse8": "0.7.5",
88
- "@mpgd/bridge": "0.14.2",
89
- "@mpgd/adapter-capacitor": "0.12.1",
90
- "@mpgd/analytics": "0.4.14",
91
- "@mpgd/capacitor-play-billing": "0.3.5",
92
- "@mpgd/capacitor-storekit": "0.3.5",
93
- "@mpgd/game-services": "0.27.0",
94
- "@mpgd/i18n": "0.6.19",
95
- "@mpgd/game-runtime": "0.7.5",
84
+ "@mpgd/adapter-ait": "0.20.3",
85
+ "@mpgd/adapter-browser": "0.11.7",
86
+ "@mpgd/adapter-capacitor": "0.12.3",
87
+ "@mpgd/adapter-devvit": "0.13.5",
88
+ "@mpgd/adapter-verse8": "0.7.7",
89
+ "@mpgd/analytics": "0.4.16",
90
+ "@mpgd/capacitor-play-billing": "0.3.7",
91
+ "@mpgd/bridge": "0.15.0",
92
+ "@mpgd/capacitor-storekit": "0.3.7",
93
+ "@mpgd/game-runtime": "0.8.0",
94
+ "@mpgd/game-services": "0.27.2",
95
+ "@mpgd/i18n": "0.6.21",
96
96
  "@mpgd/lite-shell": "0.1.0"
97
97
  },
98
98
  "main": "./dist/index.js",
@@ -6,29 +6,30 @@
6
6
  "scripts": {
7
7
  "prepare:icon": "pnpm --dir ../.. icons:generate:devvit",
8
8
  "prepare:icon:production": "pnpm --dir ../.. icons:generate:devvit:production",
9
- "init": "pnpm run prepare:icon:production && devvit upload --config devvit.json",
10
- "init:copy-paste": "pnpm run prepare:icon:production && devvit upload --copy-paste --config devvit.json",
11
- "dev": "pnpm run prepare:icon && devvit playtest --config devvit.json",
9
+ "init": "pnpm run check:sdk && pnpm run prepare:icon:production && devvit upload --config devvit.json",
10
+ "init:copy-paste": "pnpm run check:sdk && pnpm run prepare:icon:production && devvit upload --copy-paste --config devvit.json",
11
+ "dev": "pnpm run check:sdk && pnpm run prepare:icon && devvit playtest --config devvit.json",
12
12
  "build": "pnpm --dir ../.. build:devvit",
13
13
  "whoami": "devvit whoami",
14
- "upload": "pnpm run prepare:icon:production && devvit upload --config devvit.json",
15
- "publish": "pnpm run prepare:icon:production && devvit publish --config devvit.json",
14
+ "upload": "pnpm run check:sdk && pnpm run prepare:icon:production && devvit upload --config devvit.json",
15
+ "publish": "pnpm run check:sdk && pnpm run prepare:icon:production && devvit publish --config devvit.json",
16
16
  "check": "ttsc --noEmit -p tsconfig.json",
17
17
  "lint": "ttsc --noEmit -p tsconfig.json",
18
18
  "format": "ttsc format",
19
- "fix": "ttsc fix"
19
+ "fix": "ttsc fix",
20
+ "check:sdk": "pnpm exec mpgd kit devvit-check --project ."
20
21
  },
21
22
  "dependencies": {
22
- "@devvit/web": "0.14.1",
23
+ "@devvit/web": "0.15.0",
23
24
  "@mpgd/adapter-devvit": "__MPGD_DEPENDENCY_VERSION_ADAPTER_DEVVIT__",
24
25
  "@mpgd/bridge": "__MPGD_DEPENDENCY_VERSION_BRIDGE__",
25
26
  "@mpgd/catalog": "__MPGD_DEPENDENCY_VERSION_CATALOG__"
26
27
  },
27
28
  "devDependencies": {
28
- "@devvit/start": "0.14.1",
29
+ "@devvit/start": "0.15.0",
29
30
  "@ttsc/unplugin": "0.32.1",
30
31
  "@types/node": "^24.0.0",
31
- "devvit": "0.14.1",
32
+ "devvit": "0.15.0",
32
33
  "ttsc": "0.32.1",
33
34
  "typescript": "7.0.2",
34
35
  "vite": "7.3.6"