@mpgd/cli 0.54.1 → 0.56.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/capacitor-shell-starter.js +130 -3
- package/dist/devvit-sdk-check.d.ts +14 -0
- package/dist/devvit-sdk-check.js +112 -0
- package/dist/index.js +13 -0
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +14 -5
- package/dist/notifications-starter.d.ts +29 -2
- package/dist/notifications-starter.js +37 -1
- package/package.json +16 -16
- package/templates/lite-overlay/agent/acceptance.md +1 -0
- package/templates/lite-overlay/agent/brief.md +1 -0
- package/templates/notifications-overlay/common/docs/NOTIFICATIONS.md +75 -3
- package/templates/notifications-overlay/common/src/platform/notificationBackend.ts +2 -1
- package/templates/phaser-game/agent/acceptance.md +10 -0
- package/templates/phaser-game/agent/brief.md +12 -0
- package/templates/phaser-game/apps/target-devvit/package.json +10 -9
- package/templates/phaser-game/src/env.d.ts +5 -0
- package/templates/phaser-game/src/platform/developmentServerSession.ts +104 -0
- package/templates/phaser-game/src/platform/serverSession.ts +49 -26
|
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { DOMParser } from '@xmldom/xmldom';
|
|
7
7
|
import { satisfies, valid, validRange } from 'semver';
|
|
8
|
-
import { capacitorGatewayRegistersNotifications, conflictingPushPlugin, notificationsCapacitorGatewayModules, notificationsCapacitorGateways, notificationsCapacitorSupportFiles, notificationsCapacitorTemplateSupportFiles, notificationsGameDependencies, notificationsMessagingFirebaseRange, notificationsMessagingPlugin, notificationsProviderId, notificationsShellPlugins, wireCapacitorGatewayNotifications, } from './notifications-starter.js';
|
|
8
|
+
import { capacitorGatewayRegistersNotifications, conflictingPushPlugin, notificationsCapacitorGatewayModules, notificationsCapacitorGateways, notificationsCapacitorSupportFiles, notificationsCapacitorTemplateSupportFiles, notificationsCapacitorTemplateSupportImporters, notificationsGameDependencies, notificationsMessagingFirebaseRange, notificationsMessagingPlugin, notificationsProviderId, notificationsShellPlugins, sourceReferencesRelativeModule, wireCapacitorGatewayNotifications, } from './notifications-starter.js';
|
|
9
9
|
import { isNonPublicServiceHostname } from './production-target-readiness.js';
|
|
10
10
|
import { assertAndroidSettingsAppProject, assertAndroidSettingsNoAppRemap, assertIosReleaseInfoPlistExpansion, assertIosReleasePackagingSettings, assertIosReleaseProductName, assertNativeShellIdentity, hasAndroidDisplayNameResourceOverride, hasAndroidManifestSourceSetOverride, hasAndroidResourceSourceSetOverride, hasGradleIdentityMutation, hasGradleTaskAction, maskGradleStrings, readIosAppTargetId, readIosReleaseInfoPlist, stripGradleComments, } from './native-shell-identity.js';
|
|
11
11
|
const shellPath = 'apps/mobile-capacitor';
|
|
@@ -2796,9 +2796,29 @@ function planNotificationsProvider(input) {
|
|
|
2796
2796
|
// Base-template modules they import, missing from a game an earlier release created.
|
|
2797
2797
|
if (input.templateRoot !== undefined) {
|
|
2798
2798
|
for (const file of notificationsCapacitorTemplateSupportFiles) {
|
|
2799
|
-
if (readExisting(safeDestination(gameRoot, file))
|
|
2800
|
-
|
|
2799
|
+
if (readExisting(safeDestination(gameRoot, file)) !== undefined) {
|
|
2800
|
+
continue;
|
|
2801
2801
|
}
|
|
2802
|
+
const imported = notificationsCapacitorTemplateSupportImporters[file];
|
|
2803
|
+
const importerSource = imported === undefined
|
|
2804
|
+
? undefined
|
|
2805
|
+
: readExisting(safeDestination(gameRoot, imported.importer));
|
|
2806
|
+
// A game whose own importer does not import the module would never use it.
|
|
2807
|
+
if (imported !== undefined && importerSource !== undefined
|
|
2808
|
+
&& !sourceReferencesRelativeModule(importerSource, path.posix.basename(file, '.ts'))) {
|
|
2809
|
+
warnings.push(imported.warning);
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
// The file imports a kit export an older installed package lacks.
|
|
2813
|
+
const required = imported?.requires;
|
|
2814
|
+
if (required !== undefined
|
|
2815
|
+
&& installedPackageLacksExport(gameRoot, required.packageName, required.exportName)) {
|
|
2816
|
+
const version = installedPackageVersion(gameRoot, required.packageName);
|
|
2817
|
+
throw new Error(`--providers notifications writes ${file}, which imports ${required.exportName} from `
|
|
2818
|
+
+ `${required.packageName}; the game's ${required.packageName} ${version} does not export it. `
|
|
2819
|
+
+ "Upgrade the game's kit packages (mpgd kit upgrade) first.");
|
|
2820
|
+
}
|
|
2821
|
+
files.push({ path: file, content: starterFile(input.templateRoot, file) });
|
|
2802
2822
|
}
|
|
2803
2823
|
}
|
|
2804
2824
|
warnings.push('The notification settings screen is not added to an existing game; render one from '
|
|
@@ -2905,3 +2925,110 @@ function requireString(value, label) {
|
|
|
2905
2925
|
function json(value) {
|
|
2906
2926
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
2907
2927
|
}
|
|
2928
|
+
/** Declaration files the export check reads at most: a guard against a pathological graph. */
|
|
2929
|
+
const maxDeclarationFilesChecked = 500;
|
|
2930
|
+
/** The declaration file names TypeScript pairs with a module specifier. */
|
|
2931
|
+
function declarationCandidates(base) {
|
|
2932
|
+
// A specifier that already names a declaration file is that file.
|
|
2933
|
+
if (/\.d\.[cm]?ts$/u.test(base)) {
|
|
2934
|
+
return [base];
|
|
2935
|
+
}
|
|
2936
|
+
const extension = path.extname(base);
|
|
2937
|
+
const stem = base.slice(0, base.length - extension.length);
|
|
2938
|
+
switch (extension) {
|
|
2939
|
+
case '.js':
|
|
2940
|
+
case '.ts':
|
|
2941
|
+
return [`${stem}.d.ts`];
|
|
2942
|
+
case '.mjs':
|
|
2943
|
+
case '.mts':
|
|
2944
|
+
return [`${stem}.d.mts`];
|
|
2945
|
+
case '.cjs':
|
|
2946
|
+
case '.cts':
|
|
2947
|
+
return [`${stem}.d.cts`];
|
|
2948
|
+
default:
|
|
2949
|
+
return [`${base}.d.ts`, path.join(base, 'index.d.ts')];
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
/**
|
|
2953
|
+
* Whether the game's installed `packageName` provably lacks `exportName`:
|
|
2954
|
+
* its root type declarations, and every declaration file they re-export
|
|
2955
|
+
* from through relative specifiers, at any depth, never name it. Anything
|
|
2956
|
+
* the check cannot follow passes, and the game's build then decides, as for
|
|
2957
|
+
* the adapter's settings opener: an uninstalled or unreadable package, a
|
|
2958
|
+
* module it cannot read, or an `export *` from another package.
|
|
2959
|
+
*/
|
|
2960
|
+
function installedPackageLacksExport(gameRoot, packageName, exportName) {
|
|
2961
|
+
// Only a plain identifier is searched for; anything else is not proof of absence.
|
|
2962
|
+
if (!/^[A-Za-z_$][\w$]*$/u.test(exportName)) {
|
|
2963
|
+
return false;
|
|
2964
|
+
}
|
|
2965
|
+
const packageRoot = path.join(gameRoot, 'node_modules', packageName);
|
|
2966
|
+
const manifest = readInstalledManifest(gameRoot, packageName);
|
|
2967
|
+
if (manifest === undefined) {
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2970
|
+
const rootExport = typeof manifest.exports === 'object' && manifest.exports !== null
|
|
2971
|
+
? manifest.exports['.']
|
|
2972
|
+
: undefined;
|
|
2973
|
+
const rootTypes = typeof rootExport === 'object' && rootExport !== null
|
|
2974
|
+
? rootExport.types
|
|
2975
|
+
: undefined;
|
|
2976
|
+
const typesPath = typeof rootTypes === 'string' ? rootTypes : manifest.types;
|
|
2977
|
+
if (typeof typesPath !== 'string') {
|
|
2978
|
+
return false;
|
|
2979
|
+
}
|
|
2980
|
+
// Every declaration file the root re-exports from, followed through relative
|
|
2981
|
+
// `export … from` specifiers at any depth; the name is searched as text, so a
|
|
2982
|
+
// named re-export of it counts as well.
|
|
2983
|
+
const relativeFrom = /\bexport\b[^;]*?\bfrom\s*['"](\.{1,2}\/[^'"]+)['"]/gu;
|
|
2984
|
+
const packageStar = /\bexport\s*(?:type\s*)?\*\s*(?:as\s+[\w$]+\s*)?from\s*['"](?!\.{1,2}\/)[^'"]+['"]/u;
|
|
2985
|
+
const nameUsed = new RegExp(`(?<![\\w$])${exportName.replace(/\$/gu, '\\$')}(?![\\w$])`, 'u');
|
|
2986
|
+
const pending = [path.join(packageRoot, typesPath)];
|
|
2987
|
+
const visited = new Set();
|
|
2988
|
+
while (pending.length > 0) {
|
|
2989
|
+
const file = pending.pop() ?? '';
|
|
2990
|
+
if (visited.has(file)) {
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
visited.add(file);
|
|
2994
|
+
if (visited.size > maxDeclarationFilesChecked) {
|
|
2995
|
+
return false;
|
|
2996
|
+
}
|
|
2997
|
+
let source;
|
|
2998
|
+
try {
|
|
2999
|
+
source = readExisting(file);
|
|
3000
|
+
}
|
|
3001
|
+
catch {
|
|
3002
|
+
source = undefined;
|
|
3003
|
+
}
|
|
3004
|
+
if (source === undefined || packageStar.test(source)) {
|
|
3005
|
+
// A module the check cannot read, or names from another package: do not refuse on a guess.
|
|
3006
|
+
return false;
|
|
3007
|
+
}
|
|
3008
|
+
if (nameUsed.test(source)) {
|
|
3009
|
+
return false;
|
|
3010
|
+
}
|
|
3011
|
+
for (const match of source.matchAll(relativeFrom)) {
|
|
3012
|
+
const candidates = declarationCandidates(path.join(path.dirname(file), match[1] ?? ''));
|
|
3013
|
+
pending.push(candidates.find((candidate) => existsSync(candidate)) ?? candidates[0] ?? file);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
return true;
|
|
3017
|
+
}
|
|
3018
|
+
/** The game's installed `packageName` manifest, or `undefined` when it is missing or unreadable. */
|
|
3019
|
+
function readInstalledManifest(gameRoot, packageName) {
|
|
3020
|
+
try {
|
|
3021
|
+
const source = readExisting(path.join(gameRoot, 'node_modules', packageName, 'package.json'));
|
|
3022
|
+
return source === undefined
|
|
3023
|
+
? undefined
|
|
3024
|
+
: parseJsonObject(source, `${packageName}/package.json`);
|
|
3025
|
+
}
|
|
3026
|
+
catch {
|
|
3027
|
+
return undefined;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
/** The installed package's version, for a message. */
|
|
3031
|
+
function installedPackageVersion(gameRoot, packageName) {
|
|
3032
|
+
const version = readInstalledManifest(gameRoot, packageName)?.version;
|
|
3033
|
+
return typeof version === 'string' ? version : 'unknown';
|
|
3034
|
+
}
|
|
@@ -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
|
@@ -27,6 +27,7 @@ import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-ge
|
|
|
27
27
|
import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
|
|
28
28
|
import { applyNotificationsStarter, notificationsProviderId, readGameCreateProviders, } from './notifications-starter.js';
|
|
29
29
|
import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
|
|
30
|
+
import { checkDevvitSdk } from './devvit-sdk-check.js';
|
|
30
31
|
import { applyToolingMigration, planToolingMigration } from './tooling-migration.js';
|
|
31
32
|
import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
|
|
32
33
|
import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
|
|
@@ -2525,6 +2526,18 @@ const kitCommand = defineI18n({
|
|
|
2525
2526
|
ko: '현재 mpgd-kit 체크아웃을 점검합니다.',
|
|
2526
2527
|
}),
|
|
2527
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
|
+
}),
|
|
2528
2541
|
upgrade: defineI18n({
|
|
2529
2542
|
name: 'upgrade',
|
|
2530
2543
|
description: 'Plan or apply the latest published Kit package versions for a game.',
|
|
@@ -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;
|
|
@@ -14899,8 +14900,16 @@ var notificationsCapacitorSupportFiles = Object.freeze([
|
|
|
14899
14900
|
"docs/NOTIFICATIONS.md"
|
|
14900
14901
|
]);
|
|
14901
14902
|
var notificationsCapacitorTemplateSupportFiles = Object.freeze([
|
|
14902
|
-
"src/platform/serverSession.ts"
|
|
14903
|
+
"src/platform/serverSession.ts",
|
|
14904
|
+
"src/platform/developmentServerSession.ts"
|
|
14903
14905
|
]);
|
|
14906
|
+
var notificationsCapacitorTemplateSupportImporters = Object.freeze({
|
|
14907
|
+
"src/platform/developmentServerSession.ts": {
|
|
14908
|
+
importer: "src/platform/serverSession.ts",
|
|
14909
|
+
requires: { packageName: "@mpgd/game-services", exportName: "createDevelopmentServerSession" },
|
|
14910
|
+
warning: "src/platform/serverSession.ts predates development notification sessions (VITE_MPGD_DEV_NOTIFICATION_SESSION); to try notifications locally, copy serverSession.ts and developmentServerSession.ts from the starter template."
|
|
14911
|
+
}
|
|
14912
|
+
});
|
|
14904
14913
|
|
|
14905
14914
|
// node_modules/.cache/mpgd-native-build/packages/cli/src/native-shell-identity.js
|
|
14906
14915
|
function assertAndroidSettingsAppProject(source) {
|
|
@@ -61,9 +61,36 @@ export declare const notificationsCapacitorSupportFiles: readonly string[];
|
|
|
61
61
|
/**
|
|
62
62
|
* Files of the base game template the notifications modules import, which a
|
|
63
63
|
* game created by an earlier CLI release may not have: written from the
|
|
64
|
-
* template when missing, kept when the game has one.
|
|
64
|
+
* template when missing, kept when the game has one. A file listed in
|
|
65
|
+
* `notificationsCapacitorTemplateSupportImporters` is written only together
|
|
66
|
+
* with its importer.
|
|
65
67
|
*/
|
|
66
|
-
export declare const notificationsCapacitorTemplateSupportFiles: readonly
|
|
68
|
+
export declare const notificationsCapacitorTemplateSupportFiles: readonly ["src/platform/serverSession.ts", "src/platform/developmentServerSession.ts"];
|
|
69
|
+
/**
|
|
70
|
+
* Template support files only one other support file imports: the importer,
|
|
71
|
+
* and the warning for a game that keeps its own importer without importing
|
|
72
|
+
* the file (an earlier `serverSession.ts`). Such a game would never import the
|
|
73
|
+
* file, and its `@mpgd/game-services` may predate what the file imports, so
|
|
74
|
+
* the file is not written for it. An importer that imports it gets it back.
|
|
75
|
+
*/
|
|
76
|
+
export declare const notificationsCapacitorTemplateSupportImporters: Readonly<Partial<Record<(typeof notificationsCapacitorTemplateSupportFiles)[number], {
|
|
77
|
+
readonly importer: (typeof notificationsCapacitorTemplateSupportFiles)[number];
|
|
78
|
+
readonly warning: string;
|
|
79
|
+
/**
|
|
80
|
+
* A kit export the file imports, which the game's installed package
|
|
81
|
+
* must have before the file is written (an older package lacks it).
|
|
82
|
+
*/
|
|
83
|
+
readonly requires?: {
|
|
84
|
+
readonly packageName: string;
|
|
85
|
+
readonly exportName: string;
|
|
86
|
+
};
|
|
87
|
+
}>>>;
|
|
88
|
+
/**
|
|
89
|
+
* Whether `source` names the module `./<name>` (with or without `.js` or
|
|
90
|
+
* `.ts`) as a string outside comments: any static, side-effect, or dynamic
|
|
91
|
+
* import or re-export of it. A commented-out import does not count.
|
|
92
|
+
*/
|
|
93
|
+
export declare function sourceReferencesRelativeModule(source: string, name: string): boolean;
|
|
67
94
|
/** Whether a Capacitor gateway already registers a notifications provider. */
|
|
68
95
|
export declare function capacitorGatewayRegistersNotifications(source: string): boolean;
|
|
69
96
|
/**
|
|
@@ -479,11 +479,47 @@ export const notificationsCapacitorSupportFiles = Object.freeze([
|
|
|
479
479
|
/**
|
|
480
480
|
* Files of the base game template the notifications modules import, which a
|
|
481
481
|
* game created by an earlier CLI release may not have: written from the
|
|
482
|
-
* template when missing, kept when the game has one.
|
|
482
|
+
* template when missing, kept when the game has one. A file listed in
|
|
483
|
+
* `notificationsCapacitorTemplateSupportImporters` is written only together
|
|
484
|
+
* with its importer.
|
|
483
485
|
*/
|
|
484
486
|
export const notificationsCapacitorTemplateSupportFiles = Object.freeze([
|
|
485
487
|
'src/platform/serverSession.ts',
|
|
488
|
+
'src/platform/developmentServerSession.ts',
|
|
486
489
|
]);
|
|
490
|
+
/**
|
|
491
|
+
* Template support files only one other support file imports: the importer,
|
|
492
|
+
* and the warning for a game that keeps its own importer without importing
|
|
493
|
+
* the file (an earlier `serverSession.ts`). Such a game would never import the
|
|
494
|
+
* file, and its `@mpgd/game-services` may predate what the file imports, so
|
|
495
|
+
* the file is not written for it. An importer that imports it gets it back.
|
|
496
|
+
*/
|
|
497
|
+
export const notificationsCapacitorTemplateSupportImporters = Object.freeze({
|
|
498
|
+
'src/platform/developmentServerSession.ts': {
|
|
499
|
+
importer: 'src/platform/serverSession.ts',
|
|
500
|
+
requires: { packageName: '@mpgd/game-services', exportName: 'createDevelopmentServerSession' },
|
|
501
|
+
warning: 'src/platform/serverSession.ts predates development notification sessions '
|
|
502
|
+
+ '(VITE_MPGD_DEV_NOTIFICATION_SESSION); to try notifications locally, copy serverSession.ts and '
|
|
503
|
+
+ 'developmentServerSession.ts from the starter template.',
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
/**
|
|
507
|
+
* Whether `source` names the module `./<name>` (with or without `.js` or
|
|
508
|
+
* `.ts`) as a string outside comments: any static, side-effect, or dynamic
|
|
509
|
+
* import or re-export of it. A commented-out import does not count.
|
|
510
|
+
*/
|
|
511
|
+
export function sourceReferencesRelativeModule(source, name) {
|
|
512
|
+
const specifiers = new Set([`./${name}`, `./${name}.js`, `./${name}.ts`]);
|
|
513
|
+
let found = false;
|
|
514
|
+
// One pass over strings and comments, as in capacitorGatewayRegistersNotifications.
|
|
515
|
+
source.replace(/'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"|`(?:\\.|[^`\\])*`|\/\/[^\n]*|\/\*[\s\S]*?\*\//gu, (token) => {
|
|
516
|
+
if (!token.startsWith('/') && specifiers.has(token.slice(1, -1))) {
|
|
517
|
+
found = true;
|
|
518
|
+
}
|
|
519
|
+
return token;
|
|
520
|
+
});
|
|
521
|
+
return found;
|
|
522
|
+
}
|
|
487
523
|
/** Whether a Capacitor gateway already registers a notifications provider. */
|
|
488
524
|
export function capacitorGatewayRegistersNotifications(source) {
|
|
489
525
|
// One pass over strings and comments: neither registers anything, and a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.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.
|
|
74
|
-
"@mpgd/platform": "0.27.1",
|
|
73
|
+
"@mpgd/catalog": "0.10.7",
|
|
75
74
|
"@mpgd/phaser-assets": "0.9.1",
|
|
76
|
-
"@mpgd/target-config": "0.23.
|
|
75
|
+
"@mpgd/target-config": "0.23.7",
|
|
76
|
+
"@mpgd/platform": "0.27.2"
|
|
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-
|
|
85
|
-
"@mpgd/adapter-
|
|
86
|
-
"@mpgd/adapter-
|
|
87
|
-
"@mpgd/adapter-
|
|
88
|
-
"@mpgd/
|
|
89
|
-
"@mpgd/analytics": "0.
|
|
90
|
-
"@mpgd/
|
|
91
|
-
"@mpgd/capacitor-play-billing": "0.3.
|
|
92
|
-
"@mpgd/capacitor-storekit": "0.3.
|
|
93
|
-
"@mpgd/game-runtime": "0.
|
|
94
|
-
"@mpgd/
|
|
95
|
-
"@mpgd/
|
|
84
|
+
"@mpgd/adapter-ait": "0.20.4",
|
|
85
|
+
"@mpgd/adapter-browser": "0.11.7",
|
|
86
|
+
"@mpgd/adapter-capacitor": "0.12.4",
|
|
87
|
+
"@mpgd/adapter-devvit": "0.13.6",
|
|
88
|
+
"@mpgd/adapter-verse8": "0.7.8",
|
|
89
|
+
"@mpgd/analytics": "0.5.0",
|
|
90
|
+
"@mpgd/bridge": "0.15.0",
|
|
91
|
+
"@mpgd/capacitor-play-billing": "0.3.8",
|
|
92
|
+
"@mpgd/capacitor-storekit": "0.3.8",
|
|
93
|
+
"@mpgd/game-runtime": "0.8.1",
|
|
94
|
+
"@mpgd/i18n": "0.6.21",
|
|
95
|
+
"@mpgd/game-services": "0.28.0",
|
|
96
96
|
"@mpgd/lite-shell": "0.1.0"
|
|
97
97
|
},
|
|
98
98
|
"main": "./dist/index.js",
|
|
@@ -11,6 +11,7 @@ Preserve the generated JSON and Markdown in `artifacts/acceptance` or the target
|
|
|
11
11
|
- Verify renderer teardown removes its own listeners and never updates a replacement view from an old promise.
|
|
12
12
|
- Purchases and rewarded-ad callbacks never grant locally; verify authoritative ledger grants and durable reconciliation before release.
|
|
13
13
|
- Keep unknown native outcomes and late recovery observers; closing one lease must not release another owner's presentation or audio.
|
|
14
|
+
- Verify the game sets its real server session with `setStarterServerSession` and does not rely on the development fallback (read only with `--providers notifications`): `VITE_MPGD_DEV_NOTIFICATION_SESSION` is local-only (its `mpgd-dev` header needs the Vite dev server, so a `pnpm build` never signs in with it), and deployed acceptance uses the identity service's sessions.
|
|
14
15
|
|
|
15
16
|
## Reddit Devvit
|
|
16
17
|
|
|
@@ -11,6 +11,7 @@ Read `AGENTS.md`, the agent manifest, and the `use-mpgd-kit` router before editi
|
|
|
11
11
|
- Keep recovery above renderer lifetimes; reconcile existing operations on startup and resume without opening SDK UI.
|
|
12
12
|
- Rewarded v2 needs an application-owned durable encrypted journal. An in-memory store is not recovery evidence.
|
|
13
13
|
- Reserve safe-area geometry once and preserve feed scrolling and browser touch gestures.
|
|
14
|
+
- Set the server session with `setStarterServerSession` in `src/main.ts` before `bootstrap()` (a guest backend through `createGuestServerSession`). In a game created with `--providers notifications`, `VITE_MPGD_DEV_NOTIFICATION_SESSION` on the Vite dev server in the `development` mode is a local-only development session for notifications (`src/platform/developmentServerSession.ts`); it never reaches a build, commerce, or a deployed Worker.
|
|
14
15
|
|
|
15
16
|
Run `pnpm dev`, `pnpm check`, `pnpm build`, and `pnpm accept`.
|
|
16
17
|
Devvit inline and expanded entries both use the lite renderer. Keep optional engines behind explicit dynamic import boundaries and verify the inline budget.
|
|
@@ -74,10 +74,22 @@ game has both:
|
|
|
74
74
|
identity service answers to the Worker's `authenticateNotificationSession`
|
|
75
75
|
for this session's header, the same across token refreshes (see the kit's
|
|
76
76
|
`docs/CLOUDFLARE_WORKER_DEPLOY.md`, "Sessions"). Do not pass a per-token
|
|
77
|
-
or rotating id
|
|
78
|
-
|
|
77
|
+
or rotating id. With a guest-session backend, pass
|
|
78
|
+
`createGuestServerSession(coordinator)` from `@mpgd/game-services`: it
|
|
79
|
+
answers the coordinator's `sessionFamilyId` (not a view's `sessionId`,
|
|
80
|
+
which may rotate on refresh), and refreshes the access token as needed
|
|
81
|
+
(an expired one before the request, one about to expire in the
|
|
82
|
+
background).
|
|
79
83
|
|
|
80
|
-
|
|
84
|
+
```ts
|
|
85
|
+
// src/main.ts, before bootstrap()
|
|
86
|
+
const guest = createGuestSessionCoordinator({ backend, credentials, installationId });
|
|
87
|
+
setStarterServerSession(createGuestServerSession(guest));
|
|
88
|
+
void guest.start();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Without either (on the Vite dev server, a development session counts as a
|
|
92
|
+
session; see "Try it locally"), it returns `undefined` and the settings screen asks only
|
|
81
93
|
for the OS or host permission (and withdraws a Devvit opt-in the game
|
|
82
94
|
rendered). With them, the runtime registers the device for the signed-in
|
|
83
95
|
player, keeps its registration state in the platform storage, and
|
|
@@ -102,3 +114,63 @@ Night consent adds to 광고성 정보 수신동의: the screen offers it only w
|
|
|
102
114
|
advertising consent is granted, and turning advertising consent off also
|
|
103
115
|
withdraws a granted night consent in the same call (two records, two result
|
|
104
116
|
notices).
|
|
117
|
+
|
|
118
|
+
## Try it locally
|
|
119
|
+
|
|
120
|
+
You can see the settings switches record consent before you have an
|
|
121
|
+
identity service, against the kit's game-services Worker on your machine:
|
|
122
|
+
|
|
123
|
+
1. Fill in `advertisingSender` in `notifications/deployment.json` (the
|
|
124
|
+
Worker refuses a placeholder). In mpgd-kit, give the Worker this game's
|
|
125
|
+
catalog and deployment and the development sessions flag in
|
|
126
|
+
`apps/game-services-worker/.dev.vars` (local variables, never
|
|
127
|
+
committed), then run it with the memory store, the default in its
|
|
128
|
+
`wrangler.toml`:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
cd <mpgd-kit>
|
|
132
|
+
node - <game>/notifications/topics.json <game>/notifications/deployment.json \
|
|
133
|
+
> apps/game-services-worker/.dev.vars <<'EOF'
|
|
134
|
+
const fs = require('node:fs');
|
|
135
|
+
// One line of JSON in single quotes, with any ' written as \u0027.
|
|
136
|
+
const value = (file) =>
|
|
137
|
+
`'${JSON.stringify(JSON.parse(fs.readFileSync(file, 'utf8'))).replaceAll("'", '\\u0027')}'`;
|
|
138
|
+
console.log('MPGD_ALLOW_INSECURE_DEVELOPMENT_NOTIFICATION_SESSIONS=true');
|
|
139
|
+
console.log(`MPGD_NOTIFICATIONS_CATALOG=${value(process.argv[2])}`);
|
|
140
|
+
console.log(`MPGD_NOTIFICATIONS_DEPLOYMENT=${value(process.argv[3])}`);
|
|
141
|
+
EOF
|
|
142
|
+
pnpm --dir apps/game-services-worker dev --host 127.0.0.1 --port 8787
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The Worker logs once that sessions are not authenticated. To send real
|
|
146
|
+
pushes to a phone or an emulator, follow mpgd-kit's live test runbook
|
|
147
|
+
(`docs/NOTIFICATIONS_LIVE_TEST.md`) instead of adding the key here: it
|
|
148
|
+
runs this same Worker, with `pnpm notifications:live env` writing the
|
|
149
|
+
variables and the Firebase service-account key to a file outside the
|
|
150
|
+
checkout that you link as `.dev.vars`.
|
|
151
|
+
2. Start this game's dev server with the Worker URL and a development
|
|
152
|
+
session, `<playerId>` or `<playerId>.<installationId>` (letters, digits,
|
|
153
|
+
`_`, `-`):
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
VITE_MPGD_GAME_SERVICES_URL=http://127.0.0.1:8787 \
|
|
157
|
+
VITE_MPGD_DEV_NOTIFICATION_SESSION=player-1 \
|
|
158
|
+
pnpm dev
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`src/platform/developmentServerSession.ts` then signs the game in as that
|
|
162
|
+
player with `Authorization: Bearer mpgd-dev.<playerId>.<sessionId>.<installationId>`,
|
|
163
|
+
which the Worker accepts only with the flag. Anyone can write that header,
|
|
164
|
+
so it authenticates nothing: the session exists only on the Vite dev server
|
|
165
|
+
(`import.meta.env.DEV`) in the `development` mode, and the Worker refuses
|
|
166
|
+
the flag with its D1 store, ignores it once `GAME_SERVICES_NOTIFICATIONS_AUTH`
|
|
167
|
+
is bound, and takes development sessions only on a local host (`localhost`,
|
|
168
|
+
`127.0.0.1`, `[::1]`, `*.localhost`, or `10.0.2.2`, which the Android
|
|
169
|
+
emulator uses for your machine). That check reads the request's host, so
|
|
170
|
+
it protects a deployed Worker, not your network: keep the local Worker on
|
|
171
|
+
loopback, its default. Calling `setStarterServerSession`, even with `undefined`, turns it
|
|
172
|
+
off; the first call after the game used it warns, since the notifications
|
|
173
|
+
backend keeps the session it first read.
|
|
174
|
+
The development session is for the notifications backend only: the
|
|
175
|
+
commerce client keeps the session the game set with `setStarterServerSession`
|
|
176
|
+
(or none), so its requests never carry a development header.
|
|
@@ -90,7 +90,8 @@ export function createStarterWorkerNotificationBackend(
|
|
|
90
90
|
if (session === undefined) {
|
|
91
91
|
input.log(
|
|
92
92
|
'[notifications] VITE_MPGD_GAME_SERVICES_URL is set but no server session is: call setStarterServerSession '
|
|
93
|
-
+ '(src/platform/serverSession.ts) before the game starts
|
|
93
|
+
+ '(src/platform/serverSession.ts) before the game starts, or on the dev server set '
|
|
94
|
+
+ 'VITE_MPGD_DEV_NOTIFICATION_SESSION (docs/NOTIFICATIONS.md). The settings screen shows no switch until then.',
|
|
94
95
|
);
|
|
95
96
|
return undefined;
|
|
96
97
|
}
|
|
@@ -64,6 +64,16 @@ Verify safe-area geometry is reserved exactly once: CSS-padded containers must
|
|
|
64
64
|
not also subtract `safeArea.contentBounds`, while full-bleed game surfaces use
|
|
65
65
|
the snapshot bounds for persistent controls.
|
|
66
66
|
|
|
67
|
+
## Server session
|
|
68
|
+
|
|
69
|
+
Verify the game sets its real server-backed session with `setStarterServerSession`
|
|
70
|
+
before `bootstrap()` (a guest backend through `createGuestServerSession`) and does
|
|
71
|
+
not rely on the development fallback, which only a game created with
|
|
72
|
+
`--providers notifications` reads. `VITE_MPGD_DEV_NOTIFICATION_SESSION` is local-only evidence:
|
|
73
|
+
a `pnpm build` never signs in with it (its `mpgd-dev` header needs the Vite dev
|
|
74
|
+
server), and acceptance against a deployed Worker uses the identity service's
|
|
75
|
+
sessions.
|
|
76
|
+
|
|
67
77
|
## Shared native presentation
|
|
68
78
|
|
|
69
79
|
- Purchase, rewarded, and interstitial requests use the same game-owned scope.
|
|
@@ -49,6 +49,18 @@ the game-owned action coordinator and authoritative services, with an
|
|
|
49
49
|
application-owned durable encrypted operation store. Keep recovery above scenes
|
|
50
50
|
and reconcile existing operations on startup and resume without opening SDK UI.
|
|
51
51
|
|
|
52
|
+
## Server session
|
|
53
|
+
|
|
54
|
+
Set the game's server-backed session with `setStarterServerSession`
|
|
55
|
+
(`src/platform/serverSession.ts`) before `bootstrap()`; with a guest backend use
|
|
56
|
+
`createGuestServerSession(coordinator)`, whose `sessionId` is the stable
|
|
57
|
+
session-family id. For local notifications work only (a game created with
|
|
58
|
+
`--providers notifications`), `VITE_MPGD_DEV_NOTIFICATION_SESSION` on the Vite
|
|
59
|
+
dev server in the `development` mode signs in with an unauthenticated
|
|
60
|
+
development session (`src/platform/developmentServerSession.ts`)
|
|
61
|
+
against a local Worker run with `MPGD_ALLOW_INSECURE_DEVELOPMENT_NOTIFICATION_SESSIONS`;
|
|
62
|
+
it never reaches a build, the commerce client, or a deployed Worker.
|
|
63
|
+
|
|
52
64
|
## ONE play server-owned monetization
|
|
53
65
|
|
|
54
66
|
Specify issued placement/product IDs, the authenticated reward and checkout intent routes, durable SSV/intent/ledger stores, an encrypted client operation journal, and reconciliation plus atomic cancellation fencing. Keep monetization disabled until these ports are wired. Purchase/ad callbacks are evidence; backend grants precede consume/acknowledge. Separate confirmed checkout closure from financial completion. Plan real-app acceptance with ONEconsole credentials and hosting headers.
|
|
@@ -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.
|
|
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.
|
|
29
|
+
"@devvit/start": "0.15.0",
|
|
29
30
|
"@ttsc/unplugin": "0.32.1",
|
|
30
31
|
"@types/node": "^24.0.0",
|
|
31
|
-
"devvit": "0.
|
|
32
|
+
"devvit": "0.15.0",
|
|
32
33
|
"ttsc": "0.32.1",
|
|
33
34
|
"typescript": "7.0.2",
|
|
34
35
|
"vite": "7.3.6"
|
|
@@ -19,4 +19,9 @@ interface ImportMetaEnv {
|
|
|
19
19
|
readonly VITE_MPGD_GAME_SERVICES_URL?: string;
|
|
20
20
|
readonly VITE_MPGD_GAME_SERVICES_TARGET?: string;
|
|
21
21
|
readonly VITE_MPGD_GAME_SERVICES_TRANSPORT?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Local development only: `<playerId>` or `<playerId>.<installationId>`
|
|
24
|
+
* for an unauthenticated notifications session (see `serverSession.ts`).
|
|
25
|
+
*/
|
|
26
|
+
readonly VITE_MPGD_DEV_NOTIFICATION_SESSION?: string;
|
|
22
27
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createDevelopmentServerSession, type DevelopmentServerSession } from '@mpgd/game-services';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Local development only: an unauthenticated session for a game-services
|
|
5
|
+
* Worker run with `MPGD_ALLOW_INSECURE_DEVELOPMENT_NOTIFICATION_SESSIONS = "true"`
|
|
6
|
+
* (see docs/NOTIFICATIONS.md, "Try it locally"). On only when the Vite dev
|
|
7
|
+
* server runs (`dev`, `import.meta.env.DEV`), the mode is `development`, and
|
|
8
|
+
* `VITE_MPGD_DEV_NOTIFICATION_SESSION` is `<playerId>` or
|
|
9
|
+
* `<playerId>.<installationId>` (each 1 to 64 letters, digits, `_`, or `-`;
|
|
10
|
+
* the installation defaults to `dev-installation`). The session id is
|
|
11
|
+
* `dev-<installationId>`. Anyone can write such a header, so the Worker
|
|
12
|
+
* refuses the flag with its D1 store. Otherwise `undefined`; an invalid
|
|
13
|
+
* value logs why.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveStarterDevelopmentServerSession(env: {
|
|
16
|
+
readonly dev: boolean;
|
|
17
|
+
readonly mode: string;
|
|
18
|
+
readonly value: string | undefined;
|
|
19
|
+
}): DevelopmentServerSession | undefined {
|
|
20
|
+
const value = env.value?.trim();
|
|
21
|
+
// Only the development mode: a staging or release build that runs with
|
|
22
|
+
// NODE_ENV=development has `dev` set too.
|
|
23
|
+
if (!env.dev || env.mode !== 'development' || value === undefined || value === '') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const [playerId = '', installationId = 'dev-installation', ...rest] = value.split('.');
|
|
27
|
+
try {
|
|
28
|
+
if (rest.length > 0) {
|
|
29
|
+
throw new Error('Expected <playerId> or <playerId>.<installationId>.');
|
|
30
|
+
}
|
|
31
|
+
const session = createDevelopmentServerSession({ mode: env.mode, playerId, installationId });
|
|
32
|
+
console.warn(
|
|
33
|
+
`[game-services] VITE_MPGD_DEV_NOTIFICATION_SESSION: signed in as ${playerId} with an `
|
|
34
|
+
+ 'unauthenticated development session. Local development only.',
|
|
35
|
+
);
|
|
36
|
+
return session;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('[game-services] VITE_MPGD_DEV_NOTIFICATION_SESSION is ignored.', error);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What `serverSession.ts` hands out, kept apart from `import.meta.env` so it
|
|
45
|
+
* can be tested: the game's own session once it called `set` (even with
|
|
46
|
+
* `undefined`), else the development session, read once. `once` marks the
|
|
47
|
+
* reader that keeps what it reads, the commerce client, which only ever
|
|
48
|
+
* gets the game's own session: the development session is for the
|
|
49
|
+
* notifications Worker, and a commerce backend with ingress authentication
|
|
50
|
+
* would refuse its header. A `set` that changes the session after a reader
|
|
51
|
+
* kept the old one warns: after the commerce client read it, or after the
|
|
52
|
+
* development session was handed out (a notifications backend keeps it).
|
|
53
|
+
*/
|
|
54
|
+
export function createStarterServerSessionSlot<Session extends object>(input: {
|
|
55
|
+
/** The development session; called at most once. */
|
|
56
|
+
readonly development: () => Session | undefined;
|
|
57
|
+
readonly warn: (message: string) => void;
|
|
58
|
+
}): {
|
|
59
|
+
resolve(options?: { readonly once?: boolean }): Session | undefined;
|
|
60
|
+
set(session: Session | undefined): void;
|
|
61
|
+
} {
|
|
62
|
+
let session: Session | undefined;
|
|
63
|
+
let setByGame = false;
|
|
64
|
+
let readOnce = false;
|
|
65
|
+
let development: Session | null | undefined;
|
|
66
|
+
let developmentHandedOut = false;
|
|
67
|
+
return {
|
|
68
|
+
resolve(options = {}) {
|
|
69
|
+
if (options.once === true) {
|
|
70
|
+
readOnce = true;
|
|
71
|
+
}
|
|
72
|
+
// The commerce client never gets the development session.
|
|
73
|
+
if (setByGame || options.once === true) {
|
|
74
|
+
return session;
|
|
75
|
+
}
|
|
76
|
+
if (development === undefined) {
|
|
77
|
+
development = input.development() ?? null;
|
|
78
|
+
}
|
|
79
|
+
if (development === null) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
developmentHandedOut = true;
|
|
83
|
+
return development;
|
|
84
|
+
},
|
|
85
|
+
set(next) {
|
|
86
|
+
if (developmentHandedOut && !setByGame) {
|
|
87
|
+
// Even `undefined`: a notifications backend that kept the development session keeps sending its header.
|
|
88
|
+
input.warn(
|
|
89
|
+
'[game-services] setStarterServerSession ran after the development session '
|
|
90
|
+
+ '(VITE_MPGD_DEV_NOTIFICATION_SESSION) was in use; a notifications backend created '
|
|
91
|
+
+ 'already keeps it. Unset the variable, or call setStarterServerSession before bootstrap().',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (readOnce && next !== undefined && next !== session) {
|
|
95
|
+
input.warn(
|
|
96
|
+
'[game-services] setStarterServerSession ran after the commerce client was created; '
|
|
97
|
+
+ "it does not send this session's header. Call it before bootstrap().",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
session = next;
|
|
101
|
+
setByGame = true;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
import type { GameServicesHeaderResolver } from '@mpgd/game-services';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
createStarterServerSessionSlot,
|
|
5
|
+
resolveStarterDevelopmentServerSession,
|
|
6
|
+
} from './developmentServerSession';
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* The game's server-backed session: who is signed in to the game backend,
|
|
5
10
|
* and the `Authorization` header for that session. The starter has none, so
|
|
6
11
|
* `resolveStarterServerSession` returns `undefined`. Set yours with
|
|
7
|
-
* `setStarterServerSession
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* `setStarterServerSession`. The commerce client then sends its header. In
|
|
13
|
+
* a game created with `--providers notifications`, the notifications
|
|
14
|
+
* runtime uses the same session and header for the game-services Worker's
|
|
15
|
+
* notification routes.
|
|
16
|
+
*
|
|
17
|
+
* With a guest-session backend, `createGuestServerSession` from
|
|
18
|
+
* `@mpgd/game-services` reads a `GuestSessionCoordinator` as this session:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const guest = createGuestSessionCoordinator({ backend, credentials, installationId });
|
|
22
|
+
* setStarterServerSession(createGuestServerSession(guest));
|
|
23
|
+
* void guest.start();
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* `sessionId` must be the stable session-family id: the same across token
|
|
27
|
+
* refreshes, and the session your identity service answers to the Worker's
|
|
28
|
+
* `authenticateNotificationSession` for this session's header. Do not pass
|
|
29
|
+
* a per-token or rotating id: `createGuestServerSession` answers the
|
|
30
|
+
* coordinator's `sessionFamilyId`, never a view's `sessionId`, which may
|
|
31
|
+
* rotate on refresh. With another session holder, pass its family id.
|
|
11
32
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* for this session's header, the same across token refreshes. Do not pass a
|
|
15
|
-
* per-token or rotating id: a `GuestSessionCoordinator` view's `sessionId`
|
|
16
|
-
* may rotate on refresh, so it is not that id.
|
|
33
|
+
* For local development without an identity service, see
|
|
34
|
+
* `developmentServerSession.ts`.
|
|
17
35
|
*/
|
|
18
36
|
export interface StarterServerSession {
|
|
19
37
|
/**
|
|
@@ -29,22 +47,31 @@ export interface StarterServerSession {
|
|
|
29
47
|
readonly getHeaders: GameServicesHeaderResolver;
|
|
30
48
|
}
|
|
31
49
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
/** The game's session, or the development one (see `developmentServerSession.ts`). */
|
|
51
|
+
const slot = createStarterServerSessionSlot<StarterServerSession>({
|
|
52
|
+
// `import.meta.env.DEV` is false in a normal `vite build`, which drops this branch.
|
|
53
|
+
development: () =>
|
|
54
|
+
import.meta.env.DEV
|
|
55
|
+
? resolveStarterDevelopmentServerSession({
|
|
56
|
+
dev: import.meta.env.DEV,
|
|
57
|
+
mode: import.meta.env.MODE,
|
|
58
|
+
value: import.meta.env.VITE_MPGD_DEV_NOTIFICATION_SESSION,
|
|
59
|
+
})
|
|
60
|
+
: undefined,
|
|
61
|
+
warn: (message) => console.warn(message),
|
|
62
|
+
});
|
|
35
63
|
|
|
36
64
|
/**
|
|
37
65
|
* The game's server-backed session; `undefined` until the game sets one with
|
|
38
|
-
* `setStarterServerSession
|
|
39
|
-
*
|
|
66
|
+
* `setStarterServerSession`, unless a development session is on (see
|
|
67
|
+
* `developmentServerSession.ts`). `once` marks the reader that keeps what
|
|
68
|
+
* it reads, the commerce client, so a later change warns; it gets only the
|
|
69
|
+
* game's own session, never the development one.
|
|
40
70
|
*/
|
|
41
71
|
export function resolveStarterServerSession(
|
|
42
72
|
options: { readonly once?: boolean } = {},
|
|
43
73
|
): StarterServerSession | undefined {
|
|
44
|
-
|
|
45
|
-
readOnce = true;
|
|
46
|
-
}
|
|
47
|
-
return serverSession;
|
|
74
|
+
return slot.resolve(options);
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
/**
|
|
@@ -53,14 +80,10 @@ export function resolveStarterServerSession(
|
|
|
53
80
|
* notification settings and opens read it until a backend exists. Its
|
|
54
81
|
* `current()` may answer `undefined` until the player signs in. A session
|
|
55
82
|
* set after the commerce client was created does not reach that client,
|
|
56
|
-
* which this warns about.
|
|
83
|
+
* which this warns about. Any call, `undefined` included, turns the
|
|
84
|
+
* development session off; the first call after the development session
|
|
85
|
+
* was handed out warns, `undefined` included.
|
|
57
86
|
*/
|
|
58
87
|
export function setStarterServerSession(session: StarterServerSession | undefined): void {
|
|
59
|
-
|
|
60
|
-
console.warn(
|
|
61
|
-
'[game-services] setStarterServerSession ran after the commerce client was created; '
|
|
62
|
-
+ "it does not send this session's header. Call it before bootstrap().",
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
serverSession = session;
|
|
88
|
+
slot.set(session);
|
|
66
89
|
}
|