mirror of
https://github.com/Heretek-AI/GDevelop-BYOK.git
synced 2026-07-01 18:48:04 -04:00
restore: patch manifest, test fixtures, and fixture tooling from orphaned commits
This commit is contained in:
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* manifest.js — Manifest schema, validation, and loader for the patch framework.
|
||||
*
|
||||
* Each manifest entry is a declarative "finder / replacer / verify" sandwich:
|
||||
* - finder: regex that must match exactly once in the target file
|
||||
* - replacer: replacement string (can reference capture groups via $1, $2, …)
|
||||
* - verify: regex that must match exactly once in the post-patch file
|
||||
*
|
||||
* Uses only Node.js built-in modules.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────────
|
||||
|
||||
/** Required top-level keys for every manifest entry. */
|
||||
const REQUIRED_KEYS = ['id', 'file', 'description', 'required', 'finder', 'replacer', 'verify'];
|
||||
|
||||
/** Steps referenced in PATCH_FAIL error messages. */
|
||||
const STEPS = { finder: 'finder', replacer: 'replacer', verify: 'verify' };
|
||||
|
||||
// ── Validation ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Validate a single manifest entry.
|
||||
*
|
||||
* @param {object} entry The raw entry from the manifest array.
|
||||
* @param {number} index Position in the manifest (for error messages).
|
||||
* @param {string} repoRoot Absolute path to the repo root (resolved from --repo).
|
||||
* @returns {string[]} Validation error messages; empty if valid.
|
||||
*/
|
||||
function validateEntry(entry, index, repoRoot) {
|
||||
const errors = [];
|
||||
const label = entry.id ? `"${entry.id}"` : `index ${index}`;
|
||||
|
||||
// 1. Required keys
|
||||
for (const key of REQUIRED_KEYS) {
|
||||
if (entry[key] === undefined || entry[key] === null) {
|
||||
errors.push(`${label}: missing required key "${key}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop early if we can't even read the basics
|
||||
if (errors.length > 0) return errors;
|
||||
|
||||
// 2. Type checks
|
||||
if (typeof entry.id !== 'string' || entry.id.length === 0) {
|
||||
errors.push(`${label}: "id" must be a non-empty string`);
|
||||
}
|
||||
if (typeof entry.file !== 'string' || entry.file.length === 0) {
|
||||
errors.push(`${label}: "file" must be a non-empty string`);
|
||||
}
|
||||
if (typeof entry.description !== 'string') {
|
||||
errors.push(`"${entry.id}": "description" must be a string`);
|
||||
}
|
||||
if (typeof entry.required !== 'boolean') {
|
||||
errors.push(`"${entry.id}": "required" must be a boolean`);
|
||||
}
|
||||
|
||||
// 3. File existence
|
||||
if (typeof entry.file === 'string' && entry.file.length > 0) {
|
||||
const fullPath = path.join(repoRoot, entry.file);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
errors.push(`"${entry.id}": target file not found: "${entry.file}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Regex compilation
|
||||
let finderRegex = null;
|
||||
if (typeof entry.finder === 'string') {
|
||||
try {
|
||||
finderRegex = new RegExp(entry.finder, 'gm');
|
||||
} catch (e) {
|
||||
errors.push(`"${entry.id}": "finder" regex does not compile: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
let verifyRegex = null;
|
||||
if (typeof entry.verify === 'string') {
|
||||
try {
|
||||
verifyRegex = new RegExp(entry.verify, 'gm');
|
||||
} catch (e) {
|
||||
errors.push(`"${entry.id}": "verify" regex does not compile: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Replacer must be a string
|
||||
if (typeof entry.replacer !== 'string') {
|
||||
errors.push(`"${entry.id}": "replacer" must be a string`);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an entire manifest (array of entry objects).
|
||||
*
|
||||
* @param {object[]} entries Manifest entries.
|
||||
* @param {string} repoRoot Absolute path to the repo root.
|
||||
* @returns {{ errors: string[], ids: Set<string> }}
|
||||
* errors — validation error messages.
|
||||
* ids — Set of entry IDs present in the manifest.
|
||||
*/
|
||||
function validateManifest(entries, repoRoot) {
|
||||
const errors = [];
|
||||
const ids = new Set();
|
||||
|
||||
if (!Array.isArray(entries)) {
|
||||
errors.push('manifest must be a JSON array');
|
||||
return { errors, ids };
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
|
||||
// Duplicate id check
|
||||
if (entry && typeof entry.id === 'string' && entry.id.length > 0) {
|
||||
if (ids.has(entry.id)) {
|
||||
errors.push(`"${entry.id}": duplicate entry id`);
|
||||
}
|
||||
ids.add(entry.id);
|
||||
}
|
||||
|
||||
errors.push(...validateEntry(entry, i, repoRoot));
|
||||
}
|
||||
|
||||
return { errors, ids };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and validate a manifest JSON file.
|
||||
*
|
||||
* @param {string} manifestPath Path to the manifest JSON file (relative or absolute).
|
||||
* @param {string} repoRoot Absolute path to the repo root.
|
||||
* @returns {{ entries: object[], errors: string[], ids: Set<string> }}
|
||||
* entries — parsed entries (empty array on catastrophic parse failure).
|
||||
* errors — validation errors (empty means valid).
|
||||
* ids — Set of entry IDs.
|
||||
*/
|
||||
function loadManifest(manifestPath, repoRoot) {
|
||||
const resolvedPath = path.resolve(manifestPath);
|
||||
let raw;
|
||||
|
||||
try {
|
||||
raw = fs.readFileSync(resolvedPath, 'utf8');
|
||||
} catch (e) {
|
||||
return {
|
||||
entries: [],
|
||||
errors: [`cannot read manifest file "${resolvedPath}": ${e.message}`],
|
||||
ids: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return {
|
||||
entries: [],
|
||||
errors: [`manifest is not valid JSON: ${e.message}`],
|
||||
ids: new Set(),
|
||||
};
|
||||
}
|
||||
|
||||
const { errors, ids } = validateManifest(entries, repoRoot);
|
||||
return { entries, errors, ids };
|
||||
}
|
||||
|
||||
// ── Exports ──────────────────────────────────────────────────────────────
|
||||
|
||||
module.exports = {
|
||||
REQUIRED_KEYS,
|
||||
STEPS,
|
||||
validateEntry,
|
||||
validateManifest,
|
||||
loadManifest,
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
[
|
||||
{
|
||||
"id": "P001",
|
||||
"file": "newIDE/app/src/Utils/GDevelopServices/Usage.js",
|
||||
"description": "hasValidSubscriptionPlan always returns true (R004)",
|
||||
"required": true,
|
||||
"finder": "export const hasValidSubscriptionPlan = \\([\\s\\S]*?return false;\\n\\};",
|
||||
"replacer": "export const hasValidSubscriptionPlan = (\n subscription: ?Subscription\n): boolean => {\n return true; // BYOK PATCH: always return valid subscription\n};",
|
||||
"verify": "BYOK PATCH: always return valid subscription"
|
||||
},
|
||||
{
|
||||
"id": "P002",
|
||||
"file": "newIDE/app/src/Utils/GDevelopServices/Usage.js",
|
||||
"description": "getUserLimits returns maximum capabilities (R005)",
|
||||
"required": true,
|
||||
"finder": "export const getUserLimits = async \\([\\s\\S]*?return ensureObjectHasProperty\\(\\{\\}\\);\\n\\};",
|
||||
"replacer": "export const getUserLimits = async (\n getAuthorizationHeader: () => Promise<string>,\n userId: string\n): Promise<Limits> => {\n // BYOK PATCH: return maximum limits\n return {\n 'gdevelop.maxGames': 9999,\n 'gdevelop.maxCloudProjects': 9999,\n 'gdevelop.maxBuildsPerDay': 9999,\n 'gdevelop.maxBuildsPerMonth': 9999,\n 'gdevelop.maxBuildsTotal': 9999,\n 'gdevelop.maxExtensions': 9999,\n 'gdevelop.maxLeaderboards': 9999,\n 'gdevelop.maxMultiplayerGames': 9999,\n 'gdevelop.maxTextToSpeech': 9999,\n 'gdevelop.maxAITokens': 9999,\n 'gdevelop.maxAIProjects': 9999,\n 'gdevelop.maxCloudBuilds': 9999,\n 'gdevelop.maxEvents': 9999,\n 'gdevelop.maxObjects': 9999,\n 'gdevelop.maxLayers': 9999,\n 'gdevelop.maxScenes': 9999,\n 'gdevelop.maxExternalLayouts': 9999,\n 'gdevelop.maxEffects': 9999,\n 'gdevelop.maxAdMob': 9999,\n };\n};",
|
||||
"verify": "BYOK PATCH: return maximum limits"
|
||||
},
|
||||
{
|
||||
"id": "P003",
|
||||
"file": "newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js",
|
||||
"description": "GDevelopGenerationApi baseUrl dynamic override",
|
||||
"required": true,
|
||||
"finder": "baseUrl: \\(\\(isDev[\\s\\S]*?\\): string\\),",
|
||||
"replacer": "baseUrl: typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__\n ? window.__BYOK_API_BASE_URL__\n : ((isDev\n ? 'https://api-dev.gdevelop.io/generation'\n : 'https://api.gdevelop.io/generation'): string), // BYOK PATCH: dynamic override",
|
||||
"verify": "BYOK PATCH: dynamic override"
|
||||
},
|
||||
{
|
||||
"id": "P004",
|
||||
"file": "newIDE/electron-app/app/scripts/preload.js",
|
||||
"description": "Inject BYOK proxy detection into preload script",
|
||||
"required": true,
|
||||
"finder": "// Expose protected methods\\n// Preload script for GDevelop electron app\\ncontextBridge\\.exposeInMainWorld\\('electronAPI', \\{\\}\\)\\;",
|
||||
"replacer": "// Expose protected methods\n// BYOK PATCH: inject proxy detection\nif (process.env.BYOK_PROXY === 'true') {\n contextBridge.exposeInMainWorld('__BYOK_PROXY__', true);\n}\n// Preload script for GDevelop electron app\ncontextBridge.exposeInMainWorld('electronAPI', {});",
|
||||
"verify": "BYOK PATCH: inject proxy detection"
|
||||
},
|
||||
{
|
||||
"id": "P005",
|
||||
"file": "newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js",
|
||||
"description": "Watermark toggle always unlocked (R006)",
|
||||
"required": true,
|
||||
"finder": "const canRemoveWatermark = hasValidSubscription && subscription\\.planId;",
|
||||
"replacer": "const canRemoveWatermark = true; // BYOK PATCH: always allow watermark removal",
|
||||
"verify": "BYOK PATCH: always allow watermark removal"
|
||||
},
|
||||
{
|
||||
"id": "P006",
|
||||
"file": "newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js",
|
||||
"description": "Remove build quota limit check",
|
||||
"required": true,
|
||||
"finder": "const limitReached = currentBuilds >= maximumCount;",
|
||||
"replacer": "const limitReached = false; /* BYOK PATCH: remove build quota */",
|
||||
"verify": "BYOK PATCH: remove build quota"
|
||||
},
|
||||
{
|
||||
"id": "P007",
|
||||
"file": "GDJS/Runtime/index.html",
|
||||
"description": "Watermark default to false in GDJS runtime (R006)",
|
||||
"required": true,
|
||||
"finder": "\"showWatermark\": true,",
|
||||
"replacer": "\"showWatermark\": false, // BYOK PATCH",
|
||||
"verify": "\"showWatermark\": false,"
|
||||
},
|
||||
{
|
||||
"id": "P008",
|
||||
"file": "Core/GDCore/Project/Project.cpp",
|
||||
"description": "Watermark default to false in C++ core (R006)",
|
||||
"required": true,
|
||||
"finder": "SetShowWatermark\\(true\\);",
|
||||
"replacer": "SetShowWatermark(false); // BYOK PATCH",
|
||||
"verify": "SetShowWatermark\\(false\\);"
|
||||
},
|
||||
{
|
||||
"id": "P009",
|
||||
"file": "newIDE/electron-app/app/scripts/main.js",
|
||||
"description": "Inject BYOK proxy initialization into Electron main process",
|
||||
"required": true,
|
||||
"finder": "(const \\{ app, BrowserWindow \\} = require\\('electron'\\);)([\\s\\S]*?)(app\\.on\\('ready', createWindow\\);)",
|
||||
"replacer": "$1\nconst { createByokProxy } = require('./byok-proxy'); // BYOK PATCH\nconst { createByokConfigStore, getByokConfig } = require('./byok-config'); // BYOK PATCH\n\n$2app.on('ready', () => {\n // BYOK PATCH: start proxy before creating main window\n const __store = createByokConfigStore();\n const __proxy = createByokProxy(() => getByokConfig(__store));\n __proxy.start(11400).then(\n () => console.log('[BYOK] Proxy running on port 11400'),\n (err) => console.error('[BYOK] Proxy start failed:', err)\n );\n createWindow();\n});",
|
||||
"verify": "const \\{ createByokProxy \\} = require\\('./byok-proxy'\\);"
|
||||
}
|
||||
]
|
||||
@@ -10,7 +10,11 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.0",
|
||||
"cors": "^2.8.5",
|
||||
<<<<<<< HEAD
|
||||
"dotenv": "^16.4.5",
|
||||
=======
|
||||
"electron-store": "^10.0.0",
|
||||
>>>>>>> 92c0e23 (gsd snapshot: uncommitted changes after 44m inactivity)
|
||||
"express": "^4.21.0",
|
||||
"openai": "^4.67.0",
|
||||
"uuid": "^10.0.0"
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* create-sabotaged-fixtures.js — Generates sabotaged fixture sets for T03.
|
||||
*
|
||||
* Produces two directories:
|
||||
* test/fixtures-sabotaged/ — P001 finder deliberately broken (function renamed)
|
||||
* test/fixtures-sabotaged-dup/ — P001 finder appears twice (function duplicated)
|
||||
*
|
||||
* All other fixture files are identical to the pristine set.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROJECT_DIR = path.resolve(__dirname, '..');
|
||||
const SABOTAGED = path.join(PROJECT_DIR, 'test', 'fixtures-sabotaged');
|
||||
const SABOTAGED_DUP = path.join(PROJECT_DIR, 'test', 'fixtures-sabotaged-dup');
|
||||
|
||||
// ── Pristine fixture definitions (same as restore-fixtures.js) ──────────
|
||||
|
||||
const pristine = {
|
||||
'newIDE/app/src/Utils/GDevelopServices/Usage.js':
|
||||
`// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js
|
||||
// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits).
|
||||
|
||||
export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
const hasValidSubscription = !!subscription && !!subscription.planId
|
||||
&& (!subscription.redemptionCodeValidUntil || // No redemption code
|
||||
subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid
|
||||
if (hasValidSubscription) {
|
||||
// The user has a subscription registered in the backend (classic "Registered" user).
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getUserLimits = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
): Promise<Limits> => {
|
||||
const { getAuthorizationHeader, userId } = params;
|
||||
return ensureObjectHasProperty({});
|
||||
};
|
||||
`,
|
||||
'newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js':
|
||||
`// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js
|
||||
// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute).
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
export const GDevelopGenerationApi = {
|
||||
baseUrl: ((isDev
|
||||
? 'https://api-dev.gdevelop.io/generation'
|
||||
: 'https://api.gdevelop.io/generation'): string),
|
||||
};
|
||||
`,
|
||||
'newIDE/electron-app/app/scripts/preload.js':
|
||||
`// Fixture: newIDE/electron-app/app/scripts/preload.js
|
||||
// Contains the exact pattern for patch 4 (BYOK proxy detection injection).
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Expose protected methods
|
||||
// Preload script for GDevelop electron app
|
||||
contextBridge.exposeInMainWorld('electronAPI', {});
|
||||
`,
|
||||
'newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js':
|
||||
`// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js
|
||||
// Contains the exact pattern for patch 5 (watermark toggle unlock).
|
||||
|
||||
const canRemoveWatermark = hasValidSubscription && subscription.planId;
|
||||
const showWatermarkOption = canRemoveWatermark && !isFreePlan;
|
||||
|
||||
export default function ProjectPropertiesDialog() {
|
||||
return null;
|
||||
}
|
||||
`,
|
||||
'newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js':
|
||||
`// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js
|
||||
// Contains the exact pattern for patch 6 (build quota removal).
|
||||
|
||||
const limitReached = currentBuilds >= maximumCount;
|
||||
const quotaCheck = current > max;
|
||||
`,
|
||||
'GDJS/Runtime/index.html':
|
||||
`<!-- Fixture: GDJS/Runtime/index.html -->
|
||||
<!-- Contains the exact pattern for patch 7 (watermark defaults). -->
|
||||
<script>
|
||||
var gdjs = {
|
||||
"showWatermark": true,
|
||||
"showWatermark": !0,
|
||||
"other": true
|
||||
};
|
||||
</script>
|
||||
`,
|
||||
'Core/GDCore/Project/Project.cpp':
|
||||
`// Fixture: Core/GDCore/Project/Project.cpp
|
||||
// Contains the exact pattern for patch 8 (C++ watermark defaults).
|
||||
|
||||
void Project::SetDefaults() {
|
||||
SetShowWatermark(true);
|
||||
showWatermark = true;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
// ── Sabotaged variant: P001 finder broken (function renamed) ─────────────
|
||||
|
||||
const usageSabotaged = pristine['newIDE/app/src/Utils/GDevelopServices/Usage.js']
|
||||
.replace(
|
||||
'export const hasValidSubscriptionPlan',
|
||||
'export const hasValidSubscriptionPlan_RENAMED'
|
||||
);
|
||||
|
||||
// ── Sabotaged-dup variant: P001 finder appears twice ─────────────────────
|
||||
|
||||
const usageDup = pristine['newIDE/app/src/Utils/GDevelopServices/Usage.js']
|
||||
.replace(
|
||||
'return false;',
|
||||
'return false;\n};\n\n// DUPLICATE: second copy of hasValidSubscriptionPlan for sabotage testing\nexport const hasValidSubscriptionPlan = (\n subscription: ?Subscription\n): boolean => {\n return false;'
|
||||
);
|
||||
|
||||
// ── Write helper ─────────────────────────────────────────────────────────
|
||||
|
||||
function writeSet(baseDir, overrides) {
|
||||
for (const [relPath, content] of Object.entries(pristine)) {
|
||||
const finalContent = overrides[relPath] || content;
|
||||
const fullPath = path.join(baseDir, relPath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, finalContent, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Generate both sets ───────────────────────────────────────────────────
|
||||
|
||||
console.log('Creating sabotaged fixture sets...');
|
||||
|
||||
writeSet(SABOTAGED, {
|
||||
'newIDE/app/src/Utils/GDevelopServices/Usage.js': usageSabotaged,
|
||||
});
|
||||
console.log(` → ${SABOTAGED} (7 files, Usage.js function renamed)`);
|
||||
|
||||
writeSet(SABOTAGED_DUP, {
|
||||
'newIDE/app/src/Utils/GDevelopServices/Usage.js': usageDup,
|
||||
});
|
||||
console.log(` → ${SABOTAGED_DUP} (7 files, Usage.js function duplicated)`);
|
||||
|
||||
console.log('Done.');
|
||||
+204
-269
@@ -2,299 +2,234 @@
|
||||
/**
|
||||
* patch.js — Applies BYOK + premium-unlock patches to a GDevelop source tree.
|
||||
*
|
||||
* Usage: node patch.js --release v5.6.269
|
||||
* (runs against the GDevelop repo in the current directory)
|
||||
* Reads a declarative manifest of patch entries (JSON), each using a
|
||||
* "finder / replacer / verify" sandwich. Supports --strict, --dry-run,
|
||||
* --verbose, --manifest, --repo, and --release flags.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/patch.js --release v5.6.269
|
||||
* node scripts/patch.js --repo test/fixtures --strict
|
||||
* node scripts/patch.js --repo test/fixtures --dry-run --verbose
|
||||
* node scripts/patch.js --manifest custom/manifest.json --repo my/repo
|
||||
*
|
||||
* Constraints: Node.js built-in modules only (no npm dependencies).
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { loadManifest, STEPS } = require('../patch/manifest');
|
||||
|
||||
const RELEASE_TAG = process.argv.includes('--release')
|
||||
? process.argv[process.argv.indexOf('--release') + 1]
|
||||
: 'unknown';
|
||||
// ── CLI flag parsing ─────────────────────────────────────────────────────
|
||||
|
||||
const ROOT = process.argv.includes('--repo')
|
||||
? process.argv[process.argv.indexOf('--repo') + 1]
|
||||
: process.cwd();
|
||||
const argv = process.argv;
|
||||
|
||||
// ── Logger ─────────────────────────────────────────────────────────
|
||||
function log(msg) {
|
||||
console.log(`[patch] ${msg}`);
|
||||
function flagValue(flag) {
|
||||
const i = argv.indexOf(flag);
|
||||
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : null;
|
||||
}
|
||||
|
||||
function patchFile(relativePath, replacements) {
|
||||
const fullPath = path.join(ROOT, relativePath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
log(`⚠️ SKIP: ${relativePath} not found`);
|
||||
return false;
|
||||
}
|
||||
let content = fs.readFileSync(fullPath, 'utf8');
|
||||
let changed = false;
|
||||
function hasFlag(flag) {
|
||||
return argv.includes(flag);
|
||||
}
|
||||
|
||||
for (const [search, replace] of replacements) {
|
||||
if (content.includes(search)) {
|
||||
content = content.replace(search, replace);
|
||||
changed = true;
|
||||
log(` ✓ ${relativePath}: ${search.substring(0, 60)}...`);
|
||||
} else if (!content.includes(replace)) {
|
||||
log(` ? ${relativePath}: pattern not found — ${search.substring(0, 60)}...`);
|
||||
const RELEASE_TAG = flagValue('--release') || 'unknown';
|
||||
const ROOT = flagValue('--repo') || process.cwd();
|
||||
const MANIFEST_PATH = flagValue('--manifest') || path.join(process.cwd(), 'patch', 'manifest.json');
|
||||
const STRICT = hasFlag('--strict');
|
||||
const DRY_RUN = hasFlag('--dry-run');
|
||||
const VERBOSE = hasFlag('--verbose');
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Print a message if --verbose is active.
|
||||
*/
|
||||
function verbose(msg) {
|
||||
if (VERBOSE) console.error(`[verbose] ${msg}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a string for display (single-line, max 80 chars).
|
||||
*/
|
||||
function preview(s) {
|
||||
const oneLine = s.replace(/\n/g, '\\n');
|
||||
return oneLine.length > 70 ? oneLine.slice(0, 67) + '...' : oneLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a structured error to stderr and return it.
|
||||
*/
|
||||
function structuredError(patchId, file, step) {
|
||||
const msg = `PATCH_FAIL:${patchId}:${file}:${step}`;
|
||||
console.error(msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
// ── Core engine ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Apply a single manifest entry against the repo.
|
||||
*
|
||||
* @param {object} entry Manifest entry with id, file, finder, replacer, verify, required, description.
|
||||
* @param {string} root Absolute repo root path.
|
||||
* @returns {object} { pass: boolean, step: string|null, msg: string }
|
||||
*/
|
||||
function applyEntry(entry, root) {
|
||||
const filePath = path.join(root, entry.file);
|
||||
|
||||
// ── Step 0: file must exist ──
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { pass: false, step: 'finder', msg: `file not found: ${entry.file}` };
|
||||
}
|
||||
|
||||
const original = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// ── Step 1: finder regex must match exactly once ──
|
||||
let finderRegex;
|
||||
try {
|
||||
finderRegex = new RegExp(entry.finder, 'gm');
|
||||
} catch (e) {
|
||||
return { pass: false, step: STEPS.finder, msg: `finder regex compile error: ${e.message}` };
|
||||
}
|
||||
|
||||
const finderMatches = [...original.matchAll(new RegExp(finderRegex.source, finderRegex.flags))];
|
||||
|
||||
verbose(`${entry.id}: finder matches = ${finderMatches.length}`);
|
||||
if (VERBOSE && finderMatches.length > 0) {
|
||||
for (const m of finderMatches) {
|
||||
verbose(`${entry.id}: finder match at index ${m.index}: "${preview(m[0])}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
if (DRY_RUN) {
|
||||
console.log(`[DRY] ${entry.id}: finder matches ${finderMatches.length} time(s) in ${entry.file}`);
|
||||
if (finderMatches.length !== 1) {
|
||||
return { pass: false, step: STEPS.finder,
|
||||
msg: `finder matches ${finderMatches.length} time(s) (expected 1)` };
|
||||
}
|
||||
return { pass: true, step: null, msg: `dry-run ok` };
|
||||
}
|
||||
return changed;
|
||||
|
||||
if (finderMatches.length !== 1) {
|
||||
return { pass: false, step: STEPS.finder,
|
||||
msg: `finder matches ${finderMatches.length} time(s) in ${entry.file} (expected exactly 1)` };
|
||||
}
|
||||
|
||||
verbose(`${entry.id}: applying replacer`);
|
||||
|
||||
// ── Step 2: apply replacer ──
|
||||
const patched = original.replace(new RegExp(finderRegex.source, finderRegex.flags), entry.replacer);
|
||||
|
||||
if (patched === original) {
|
||||
return { pass: false, step: STEPS.replacer, msg: `replacer did not change file content` };
|
||||
}
|
||||
|
||||
// ── Step 3: verify regex must match exactly once in the result ──
|
||||
let verifyRegex;
|
||||
try {
|
||||
verifyRegex = new RegExp(entry.verify, 'gm');
|
||||
} catch (e) {
|
||||
return { pass: false, step: STEPS.verify, msg: `verify regex compile error: ${e.message}` };
|
||||
}
|
||||
|
||||
const verifyMatches = [...patched.matchAll(new RegExp(verifyRegex.source, verifyRegex.flags))];
|
||||
|
||||
verbose(`${entry.id}: verify matches = ${verifyMatches.length}`);
|
||||
if (VERBOSE && verifyMatches.length > 0) {
|
||||
for (const m of verifyMatches) {
|
||||
verbose(`${entry.id}: verify match at index ${m.index}: "${preview(m[0])}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (verifyMatches.length !== 1) {
|
||||
return { pass: false, step: STEPS.verify,
|
||||
msg: `verify matches ${verifyMatches.length} time(s) (expected exactly 1)` };
|
||||
}
|
||||
|
||||
// ── Write back ──
|
||||
try {
|
||||
fs.writeFileSync(filePath, patched, 'utf8');
|
||||
} catch (e) {
|
||||
return { pass: false, step: STEPS.verify, msg: `write error: ${e.message}` };
|
||||
}
|
||||
|
||||
return { pass: true, step: null, msg: 'ok' };
|
||||
}
|
||||
|
||||
// ── 1. Unlock all premium capabilities ────────────────────────────
|
||||
log('Patching Usage.js — premium unlock...');
|
||||
patchFile('newIDE/app/src/Utils/GDevelopServices/Usage.js', [
|
||||
// Patch hasValidSubscriptionPlan to always return true
|
||||
[
|
||||
`export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
const hasValidSubscription = !!subscription && !!subscription.planId
|
||||
&& (!subscription.redemptionCodeValidUntil || // No redemption code
|
||||
subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid
|
||||
if (hasValidSubscription) {
|
||||
// The user has a subscription registered in the backend (classic "Registered" user).
|
||||
return true;
|
||||
// ── Main ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function main() {
|
||||
const repoRoot = path.resolve(ROOT);
|
||||
|
||||
if (VERBOSE) {
|
||||
console.error(`[verbose] repo root : ${repoRoot}`);
|
||||
console.error(`[verbose] manifest : ${MANIFEST_PATH}`);
|
||||
console.error(`[verbose] strict mode : ${STRICT}`);
|
||||
console.error(`[verbose] dry-run : ${DRY_RUN}`);
|
||||
console.error(`[verbose] release : ${RELEASE_TAG}`);
|
||||
}
|
||||
return false;
|
||||
};`,
|
||||
`export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
// BYOK PATCH: Always return true — all premium features unlocked.
|
||||
return true;
|
||||
};`,
|
||||
],
|
||||
]);
|
||||
|
||||
// Patch getUserLimits to return maxed-out capabilities
|
||||
// We intercept the return value by wrapping ensureObjectHasProperty
|
||||
// This is a broader patch — we replace the entire Limits response path
|
||||
log('Patching Usage.js — max capabilities...');
|
||||
const usageJsPath = 'newIDE/app/src/Utils/GDevelopServices/Usage.js';
|
||||
if (fs.existsSync(path.join(ROOT, usageJsPath))) {
|
||||
let usageContent = fs.readFileSync(path.join(ROOT, usageJsPath), 'utf8');
|
||||
// 1. Load and validate manifest
|
||||
const { entries, errors } = loadManifest(MANIFEST_PATH, repoRoot);
|
||||
|
||||
// Inject a function that returns maxed capabilities
|
||||
const maxCapabilities = `
|
||||
// BYOK PATCH: Returns maxed-out capabilities (all premium features unlocked)
|
||||
const BYOK_MAX_CAPABILITIES = {
|
||||
analytics: { sessions: true, players: true, retention: true, sessionsTimeStats: true, platforms: true },
|
||||
cloudProjects: { maximumCount: 999999, canMaximumCountBeIncreased: true, maximumGuestCollaboratorsPerProject: 999 },
|
||||
leaderboards: { maximumCountPerGame: 999999, canMaximumCountPerGameBeIncreased: true, themeCustomizationCapabilities: 'FULL', canUseCustomCss: true, canDisableLoginInLeaderboard: true },
|
||||
multiplayer: { lobbiesCount: 999999, maxPlayersPerLobby: 999, themeCustomizationCapabilities: 'FULL' },
|
||||
versionHistory: { enabled: true, retentionDays: 365 },
|
||||
ai: { availablePresets: [] },
|
||||
};
|
||||
`;
|
||||
if (errors.length > 0) {
|
||||
console.error('[ERROR] Manifest validation failed:');
|
||||
for (const err of errors) console.error(` - ${err}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
usageContent = usageContent.replace(
|
||||
'export const getUserLimits = async (',
|
||||
maxCapabilities + '\nexport const getUserLimits = async ('
|
||||
);
|
||||
const requiredCount = entries.filter(e => e.required).length;
|
||||
const optionalCount = entries.filter(e => !e.required).length;
|
||||
console.log(`[manifest] ${entries.length} entries (${requiredCount} required, ${optionalCount} optional)`);
|
||||
|
||||
// Patch the return to return our maxed capabilities
|
||||
// The function calls ensureObjectHasProperty on the response
|
||||
// We intercept by replacing the function body
|
||||
const getLimitsFnRegex = /export const getUserLimits = async \([^)]+\): Promise<Limits> => \{[\s\S]*?return ensureObjectHasProperty\(\{[^}]+\}\s*\}\);\s*\};/;
|
||||
if (getLimitsFnRegex.test(usageContent)) {
|
||||
usageContent = usageContent.replace(getLimitsFnRegex,
|
||||
`export const getUserLimits = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
): Promise<Limits> => {
|
||||
// BYOK PATCH: Return maxed capabilities without calling the backend.
|
||||
return {
|
||||
quotas: {},
|
||||
capabilities: BYOK_MAX_CAPABILITIES,
|
||||
credits: {
|
||||
userBalance: { amount: 999999 },
|
||||
prices: {},
|
||||
purchasableQuantities: {},
|
||||
},
|
||||
message: undefined,
|
||||
};
|
||||
};`
|
||||
);
|
||||
if (entries.length === 0) {
|
||||
console.log('[manifest] No entries — nothing to apply.');
|
||||
if (DRY_RUN) {
|
||||
console.log('[DRY] Dry-run complete. No changes made.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Apply each entry
|
||||
let failures = 0;
|
||||
let requiredFailures = 0;
|
||||
|
||||
for (const entry of entries) {
|
||||
const result = applyEntry(entry, repoRoot);
|
||||
|
||||
if (result.pass) {
|
||||
if (DRY_RUN) {
|
||||
// already printed in applyEntry
|
||||
} else {
|
||||
console.log(`[PASS] ${entry.id}: ${entry.description}`);
|
||||
}
|
||||
} else {
|
||||
failures++;
|
||||
const errLine = structuredError(entry.id, entry.file, result.step);
|
||||
if (VERBOSE) {
|
||||
console.error(` detail: ${result.msg}`);
|
||||
} else {
|
||||
console.error(` (${result.msg})`);
|
||||
}
|
||||
|
||||
if (entry.required) {
|
||||
requiredFailures++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Summary
|
||||
if (DRY_RUN) {
|
||||
console.log(`\n[DONE] Dry-run complete. ${entries.length} entries previewed.`);
|
||||
} else {
|
||||
log(' ⚠️ Could not find getUserLimits function body for replacement — trying alternative...');
|
||||
// Alternative: just find the function start and replace everything to the end
|
||||
const altRegex = /export const getUserLimits = async \([^)]+\): Promise<Limits> => \{[\s\S]*?\};/;
|
||||
if (altRegex.test(usageContent)) {
|
||||
usageContent = usageContent.replace(altRegex,
|
||||
`export const getUserLimits = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
): Promise<Limits> => {
|
||||
// BYOK PATCH: Return maxed capabilities without calling the backend.
|
||||
return {
|
||||
quotas: {},
|
||||
capabilities: {
|
||||
analytics: { sessions: true, players: true, retention: true, sessionsTimeStats: true, platforms: true },
|
||||
cloudProjects: { maximumCount: 999999, canMaximumCountBeIncreased: true, maximumGuestCollaboratorsPerProject: 999 },
|
||||
leaderboards: { maximumCountPerGame: 999999, canMaximumCountPerGameBeIncreased: true, themeCustomizationCapabilities: 'FULL', canUseCustomCss: true, canDisableLoginInLeaderboard: true },
|
||||
multiplayer: { lobbiesCount: 999999, maxPlayersPerLobby: 999, themeCustomizationCapabilities: 'FULL' },
|
||||
versionHistory: { enabled: true, retentionDays: 365 },
|
||||
ai: { availablePresets: [] },
|
||||
},
|
||||
credits: {
|
||||
userBalance: { amount: 999999 },
|
||||
prices: {},
|
||||
purchasableQuantities: {},
|
||||
},
|
||||
message: undefined,
|
||||
};
|
||||
};`
|
||||
);
|
||||
}
|
||||
console.log(`\n[DONE] Applied: ${entries.length - failures} succeeded, ${failures} failed (${requiredFailures} required).`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(ROOT, usageJsPath), usageContent, 'utf8');
|
||||
log(' ✓ getUserLimits patched');
|
||||
}
|
||||
|
||||
// ── 2. Route AI to local proxy ─────────────────────────────────────
|
||||
log('Patching ApiConfigs.js — AI proxy reroute...');
|
||||
patchFile('newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js', [
|
||||
// Point generation API to local proxy
|
||||
[
|
||||
`export const GDevelopGenerationApi = {
|
||||
baseUrl: ((isDev
|
||||
? 'https://api-dev.gdevelop.io/generation'
|
||||
: 'https://api.gdevelop.io/generation'): string),
|
||||
};`,
|
||||
`export const GDevelopGenerationApi = {
|
||||
// BYOK PATCH: Route AI requests through local proxy for BYOK support.
|
||||
// Falls back to GDevelop API if proxy is unavailable.
|
||||
baseUrl: (() => {
|
||||
// Check if BYOK proxy override is set
|
||||
if (typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__) {
|
||||
return window.__BYOK_API_BASE_URL__;
|
||||
}
|
||||
return isDev
|
||||
? 'https://api-dev.gdevelop.io/generation'
|
||||
: 'https://api.gdevelop.io/generation';
|
||||
})(),
|
||||
};`,
|
||||
],
|
||||
]);
|
||||
|
||||
// Add BYOK proxy detection to electron preload
|
||||
const preloadPath = 'newIDE/electron-app/app/scripts/preload.js';
|
||||
if (fs.existsSync(path.join(ROOT, preloadPath))) {
|
||||
let preload = fs.readFileSync(path.join(ROOT, preloadPath), 'utf8');
|
||||
const byokInjection = `
|
||||
// BYOK PATCH: Auto-detect local AI proxy
|
||||
const { net } = require('electron');
|
||||
try {
|
||||
const http = require('http');
|
||||
const probe = http.request({ host: '127.0.0.1', port: 11400, path: '/health', method: 'GET', timeout: 500 }, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('[BYOK] AI Proxy detected on localhost:11400');
|
||||
window.__BYOK_API_BASE_URL__ = 'http://localhost:11400';
|
||||
window.__BYOK_PROXY_AVAILABLE__ = true;
|
||||
}
|
||||
});
|
||||
probe.on('error', () => { /* proxy not running */ });
|
||||
probe.end();
|
||||
} catch (e) { /* electron net not available */ }
|
||||
`;
|
||||
|
||||
preload = preload.replace(
|
||||
'// Expose protected methods',
|
||||
byokInjection + '\n// Expose protected methods'
|
||||
);
|
||||
fs.writeFileSync(path.join(ROOT, preloadPath), preload, 'utf8');
|
||||
log(' ✓ Preload BYOK proxy detection injected');
|
||||
}
|
||||
|
||||
// ── 3. Remove watermark toggle restriction ─────────────────────────
|
||||
log('Patching ProjectPropertiesDialog — watermark unlock...');
|
||||
const projectsPropsDialogPath = 'newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js';
|
||||
if (fs.existsSync(path.join(ROOT, projectsPropsDialogPath))) {
|
||||
let ppdContent = fs.readFileSync(path.join(ROOT, projectsPropsDialogPath), 'utf8');
|
||||
|
||||
// Remove subscription check for watermark removal (multiple patterns possible)
|
||||
const watermarkGates = [
|
||||
/canRemoveWatermark\s*&&/g,
|
||||
/showWatermark.*subscription/i,
|
||||
/!hasValidSubscription.*watermark/i,
|
||||
];
|
||||
|
||||
for (const gate of watermarkGates) {
|
||||
if (gate.test(ppdContent)) {
|
||||
log(` Found watermark gate pattern: ${gate}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(ROOT, projectsPropsDialogPath), ppdContent, 'utf8');
|
||||
}
|
||||
|
||||
// ── 4. Remove build quota checks ───────────────────────────────────
|
||||
log('Patching ShareDialog — remove build quota...');
|
||||
const shareDialogsGlob = 'newIDE/app/src/ExportAndShare/ShareDialog/';
|
||||
const shareDir = path.join(ROOT, shareDialogsGlob);
|
||||
if (fs.existsSync(shareDir)) {
|
||||
const files = fs.readdirSync(shareDir).filter(f => f.endsWith('.js'));
|
||||
for (const file of files) {
|
||||
let content = fs.readFileSync(path.join(shareDir, file), 'utf8');
|
||||
if (content.includes('limitReached') || content.includes('quota') || content.includes('maximumCount')) {
|
||||
content = content.replace(/limitReached/g, 'false /* BYOK */');
|
||||
content = content.replace(/current\s*[><=]+\s*max/g, 'false /* BYOK */');
|
||||
fs.writeFileSync(path.join(shareDir, file), content, 'utf8');
|
||||
log(` ✓ ${file} — quota checks neutered`);
|
||||
}
|
||||
// 4. Exit code
|
||||
if (STRICT && requiredFailures > 0) {
|
||||
console.error(`\n[FAIL] ${requiredFailures} required patch(es) failed in strict mode.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 5. Patch GDJS watermark defaults ───────────────────────────────
|
||||
log('Patching GDJS export — watermark defaults...');
|
||||
const gdjsIndexHtml = 'GDJS/Runtime/index.html';
|
||||
if (fs.existsSync(path.join(ROOT, gdjsIndexHtml))) {
|
||||
let html = fs.readFileSync(path.join(ROOT, gdjsIndexHtml), 'utf8');
|
||||
// Default showWatermark to false
|
||||
html = html.replace(
|
||||
/"showWatermark"\s*:\s*true/g,
|
||||
'"showWatermark": false'
|
||||
);
|
||||
html = html.replace(
|
||||
/"showWatermark"\s*:\s*!0/g,
|
||||
'"showWatermark": false'
|
||||
);
|
||||
fs.writeFileSync(path.join(ROOT, gdjsIndexHtml), html, 'utf8');
|
||||
log(' ✓ Default showWatermark → false');
|
||||
}
|
||||
|
||||
// Also patch C++ side (GDevelop.js) watermark default
|
||||
const projectCpp = 'Core/GDCore/Project/Project.cpp';
|
||||
if (fs.existsSync(path.join(ROOT, projectCpp))) {
|
||||
let cpp = fs.readFileSync(path.join(ROOT, projectCpp), 'utf8');
|
||||
cpp = cpp.replace(/SetShowWatermark\(true\)/g, 'SetShowWatermark(false) /* BYOK */');
|
||||
cpp = cpp.replace(/showWatermark\s*=\s*true/g, 'showWatermark = false /* BYOK */');
|
||||
fs.writeFileSync(path.join(ROOT, projectCpp), cpp, 'utf8');
|
||||
log(' ✓ C++ watermark default → false');
|
||||
}
|
||||
|
||||
// ── 6. Write version stamp ─────────────────────────────────────────
|
||||
const stampPath = path.join(ROOT, '.gsd', 'BYOK_PATCHED');
|
||||
fs.mkdirSync(path.dirname(stampPath), { recursive: true });
|
||||
fs.writeFileSync(stampPath, JSON.stringify({
|
||||
version: RELEASE_TAG,
|
||||
patchedAt: new Date().toISOString(),
|
||||
patches: [
|
||||
'premium-capabilities-unlock',
|
||||
'byok-proxy-reroute',
|
||||
'watermark-removal-unlock',
|
||||
'build-quota-removal',
|
||||
'gdjs-watermark-default-false',
|
||||
],
|
||||
}, null, 2));
|
||||
|
||||
log(`\n✅ Patching complete for ${RELEASE_TAG}`);
|
||||
main();
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* restore-fixtures.js — Restores all 7 fixture files to pristine state.
|
||||
* Used by test-patches-local.sh to reset state between test runs.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const FIXTURES = path.resolve(__dirname, '..', 'test', 'fixtures');
|
||||
|
||||
const files = {
|
||||
'newIDE/app/src/Utils/GDevelopServices/Usage.js':
|
||||
`// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js
|
||||
// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits).
|
||||
|
||||
export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
const hasValidSubscription = !!subscription && !!subscription.planId
|
||||
&& (!subscription.redemptionCodeValidUntil || // No redemption code
|
||||
subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid
|
||||
if (hasValidSubscription) {
|
||||
// The user has a subscription registered in the backend (classic "Registered" user).
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getUserLimits = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
): Promise<Limits> => {
|
||||
const { getAuthorizationHeader, userId } = params;
|
||||
return ensureObjectHasProperty({});
|
||||
};
|
||||
`,
|
||||
'newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js':
|
||||
`// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js
|
||||
// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute).
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
export const GDevelopGenerationApi = {
|
||||
baseUrl: ((isDev
|
||||
? 'https://api-dev.gdevelop.io/generation'
|
||||
: 'https://api.gdevelop.io/generation'): string),
|
||||
};
|
||||
`,
|
||||
'newIDE/electron-app/app/scripts/preload.js':
|
||||
`// Fixture: newIDE/electron-app/app/scripts/preload.js
|
||||
// Contains the exact pattern for patch 4 (BYOK proxy detection injection).
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Expose protected methods
|
||||
// Preload script for GDevelop electron app
|
||||
contextBridge.exposeInMainWorld('electronAPI', {});
|
||||
`,
|
||||
'newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js':
|
||||
`// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js
|
||||
// Contains the exact pattern for patch 5 (watermark toggle unlock).
|
||||
|
||||
const canRemoveWatermark = hasValidSubscription && subscription.planId;
|
||||
const showWatermarkOption = canRemoveWatermark && !isFreePlan;
|
||||
|
||||
export default function ProjectPropertiesDialog() {
|
||||
return null;
|
||||
}
|
||||
`,
|
||||
'newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js':
|
||||
`// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js
|
||||
// Contains the exact pattern for patch 6 (build quota removal).
|
||||
|
||||
const limitReached = currentBuilds >= maximumCount;
|
||||
const quotaCheck = current > max;
|
||||
`,
|
||||
'GDJS/Runtime/index.html':
|
||||
`<!-- Fixture: GDJS/Runtime/index.html -->
|
||||
<!-- Contains the exact pattern for patch 7 (watermark defaults). -->
|
||||
<script>
|
||||
var gdjs = {
|
||||
"showWatermark": true,
|
||||
"showWatermark": !0,
|
||||
"other": true
|
||||
};
|
||||
</script>
|
||||
`,
|
||||
'Core/GDCore/Project/Project.cpp':
|
||||
`// Fixture: Core/GDCore/Project/Project.cpp
|
||||
// Contains the exact pattern for patch 8 (C++ watermark defaults).
|
||||
|
||||
void Project::SetDefaults() {
|
||||
SetShowWatermark(true);
|
||||
showWatermark = true;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
for (const [relPath, content] of Object.entries(files)) {
|
||||
const fullPath = path.join(FIXTURES, relPath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
}
|
||||
|
||||
console.log(`Restored ${Object.keys(files).length} fixture files.`);
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-patches-local.sh — Thin wrapper around test-patches.js.
|
||||
# Primarily exists for CI/bash environments.
|
||||
# On Windows, call `node scripts/test-patches.js` directly.
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec node "$SCRIPT_DIR/test-patches.js" "$@"
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* test-patches.js — Integration tests for the patch framework (T02).
|
||||
* Tests all 8 manifest entries, dry-run, strict mode, and sabotage detection.
|
||||
* Cross-platform: works on Windows, macOS, Linux without bash dependency.
|
||||
*/
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROJECT_DIR = path.resolve(__dirname, '..');
|
||||
const PATCH_JS = path.join(PROJECT_DIR, 'scripts', 'patch.js');
|
||||
const FIXTURES = path.join(PROJECT_DIR, 'test', 'fixtures');
|
||||
const RESTORE_JS = path.join(__dirname, 'restore-fixtures.js');
|
||||
|
||||
let PASS = 0;
|
||||
let FAIL = 0;
|
||||
|
||||
function green(s) { console.log(`\x1b[32m ✓ ${s}\x1b[0m`); PASS++; }
|
||||
function red(s) { console.log(`\x1b[31m ✗ ${s}\x1b[0m`); FAIL++; }
|
||||
|
||||
function run(cmd, opts = {}) {
|
||||
try {
|
||||
const result = execSync(cmd + ' 2>&1', {
|
||||
cwd: PROJECT_DIR,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 30000,
|
||||
...opts,
|
||||
});
|
||||
return { stdout: result.toString(), stderr: '', combined: result.toString(), exitCode: 0 };
|
||||
} catch (e) {
|
||||
const combined = (e.stdout || '') + (e.stderr || '');
|
||||
return {
|
||||
stdout: (e.stdout || '').toString(),
|
||||
stderr: (e.stderr || '').toString(),
|
||||
combined: combined.toString(),
|
||||
exitCode: e.status || 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function restoreFixtures() {
|
||||
execSync(`node "${RESTORE_JS}"`, { cwd: PROJECT_DIR, stdio: 'pipe' });
|
||||
}
|
||||
|
||||
console.log('=== T02 + T03: Patch Framework Integration Tests ===\n');
|
||||
|
||||
// ── Setup ──
|
||||
console.log('[Setup] Restoring fixtures...');
|
||||
restoreFixtures();
|
||||
green('fixtures restored');
|
||||
|
||||
// ── Test 1: dry-run + strict → exit 0, 8 entries match exactly once ──
|
||||
console.log('\n[Test 1] Dry-run + strict (all 8 entries)');
|
||||
const dryRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --dry-run --strict`);
|
||||
const matchCount = (dryRun.stdout.match(/finder matches 1 time/g) || []).length;
|
||||
if (matchCount === 8) green(`8 finders match exactly once`); else red(`expected 8 matches, got ${matchCount}`);
|
||||
if (dryRun.exitCode === 0) green(`dry-run exits 0`); else red(`dry-run exits ${dryRun.exitCode}`);
|
||||
|
||||
// ── Test 2: strict apply → exit 0, 8 [PASS] lines ──
|
||||
console.log('\n[Test 2] Strict mode apply (all 8 entries)');
|
||||
restoreFixtures();
|
||||
const strictRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --strict`);
|
||||
const passCount = (strictRun.stdout.match(/\[PASS\]/g) || []).length;
|
||||
if (passCount === 8) green(`8 [PASS] lines`); else red(`expected 8 [PASS], got ${passCount}`);
|
||||
if (strictRun.exitCode === 0) green(`strict mode exits 0`); else red(`strict mode exits ${strictRun.exitCode}`);
|
||||
|
||||
// ── Test 3: verbose shows find/replace/verify steps ──
|
||||
console.log('\n[Test 3] Verbose mode output');
|
||||
restoreFixtures();
|
||||
const verboseRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --strict --verbose`);
|
||||
const verboseOut = verboseRun.combined;
|
||||
if (verboseOut.includes('finder matches')) green(`verbose shows finder steps`); else red(`verbose missing finder detail`);
|
||||
if (verboseOut.includes('verify matches')) green(`verbose shows verify steps`); else red(`verbose missing verify detail`);
|
||||
|
||||
// ── Test 4: sabotage detection ──
|
||||
console.log('\n[Test 4] Sabotage detection');
|
||||
restoreFixtures();
|
||||
const usageFile = path.join(FIXTURES, 'newIDE/app/src/Utils/GDevelopServices/Usage.js');
|
||||
const original = fs.readFileSync(usageFile, 'utf8');
|
||||
const sabotaged = original.replace('export const hasValidSubscriptionPlan', 'export const hasValidSubscriptionPlan_RENAMED');
|
||||
fs.writeFileSync(usageFile, sabotaged, 'utf8');
|
||||
const sabotageRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --strict`);
|
||||
fs.writeFileSync(usageFile, original, 'utf8'); // restore
|
||||
if (sabotageRun.combined.includes('PATCH_FAIL')) {
|
||||
green(`structured error PATCH_FAIL produced`);
|
||||
} else {
|
||||
red(`no PATCH_FAIL in output`);
|
||||
}
|
||||
if (sabotageRun.exitCode === 1) green(`sabotage exits 1`); else red(`sabotage exits ${sabotageRun.exitCode}`);
|
||||
|
||||
// ── Test 5: missing manifest → exit 2 ──
|
||||
console.log('\n[Test 5] Missing manifest file');
|
||||
const missingRun = run(`node "${PATCH_JS}" --repo "${FIXTURES}" --manifest /nonexistent/path.json`);
|
||||
if (missingRun.stdout.toLowerCase().includes('cannot read') || missingRun.stderr.toLowerCase().includes('cannot read')) {
|
||||
green(`reports cannot read manifest`);
|
||||
} else {
|
||||
red(`missing expected error message`);
|
||||
}
|
||||
if (missingRun.exitCode === 2) green(`missing manifest exits 2`); else red(`missing manifest exits ${missingRun.exitCode}`);
|
||||
|
||||
// ── T03 Tests ──────────────────────────────────────────────────────────
|
||||
|
||||
const SABOTAGED_DIR = path.join(PROJECT_DIR, 'test', 'fixtures-sabotaged');
|
||||
const SABOTAGED_DUP_DIR = path.join(PROJECT_DIR, 'test', 'fixtures-sabotaged-dup');
|
||||
const CREATE_SABOTAGED = path.join(__dirname, 'create-sabotaged-fixtures.js');
|
||||
|
||||
// ── Test 6: sabotaged fixture (function renamed) → PATCH_FAIL exact format ──
|
||||
console.log('\n[Test 6] T03: Sabotaged fixtures — PATCH_FAIL exact format');
|
||||
execSync(`node "${CREATE_SABOTAGED}"`, { cwd: PROJECT_DIR, stdio: 'pipe' });
|
||||
const sabotagedRun = run(`node "${PATCH_JS}" --repo "${SABOTAGED_DIR}" --strict`);
|
||||
if (sabotagedRun.exitCode === 1) green(`sabotaged exits 1`); else red(`sabotaged exits ${sabotagedRun.exitCode}`);
|
||||
if (sabotagedRun.combined.includes('PATCH_FAIL:P001:newIDE/app/src/Utils/GDevelopServices/Usage.js:finder')) {
|
||||
green(`exact PATCH_FAIL:P001:newIDE/app/src/Utils/GDevelopServices/Usage.js:finder`);
|
||||
} else {
|
||||
red(`missing exact PATCH_FAIL format`);
|
||||
}
|
||||
if (!sabotagedRun.combined.includes('[PASS]') || !sabotagedRun.combined.includes('P001')) {
|
||||
// P001 should NOT show [PASS]
|
||||
const p001pass = sabotagedRun.combined.match(/\[PASS\].*P001/);
|
||||
if (!p001pass) green(`P001 does NOT show [PASS]`); else red(`P001 incorrectly shows [PASS]`);
|
||||
} else {
|
||||
green(`P001 does NOT show [PASS] (verified)`);
|
||||
}
|
||||
// Other 7 patches should still pass
|
||||
const otherPasses = (sabotagedRun.combined.match(/\[PASS\]/g) || []).length;
|
||||
if (otherPasses === 7) green(`7 other patches [PASS]`); else red(`expected 7 [PASS], got ${otherPasses}`);
|
||||
|
||||
// ── Test 7: sabotaged-dup fixture (function duplicated) → PATCH_FAIL ──
|
||||
console.log('\n[Test 7] T03: Duplicate finder — PATCH_FAIL for 2+ matches');
|
||||
const dupRun = run(`node "${PATCH_JS}" --repo "${SABOTAGED_DUP_DIR}" --strict`);
|
||||
if (dupRun.exitCode === 1) green(`dup-sabotaged exits 1`); else red(`dup-sabotaged exits ${dupRun.exitCode}`);
|
||||
if (dupRun.combined.includes('PATCH_FAIL:P001:newIDE/app/src/Utils/GDevelopServices/Usage.js:finder')) {
|
||||
green(`exact PATCH_FAIL for duplicate match`);
|
||||
} else {
|
||||
red(`missing PATCH_FAIL for duplicate`);
|
||||
}
|
||||
// Verify "2 time(s)" appears in error detail
|
||||
if (dupRun.combined.includes('2 time(s)')) {
|
||||
green(`reports "2 time(s)" for duplicate`);
|
||||
} else {
|
||||
red(`missing "2 time(s)" detail`);
|
||||
}
|
||||
|
||||
// ── Test 8: --verbose with sabotaged fixtures shows step-level detail ──
|
||||
console.log('\n[Test 8] T03: Verbose sabotaged shows finder match count 0');
|
||||
const verboseSabotaged = run(`node "${PATCH_JS}" --repo "${SABOTAGED_DIR}" --strict --verbose`);
|
||||
if (verboseSabotaged.combined.includes('finder matches = 0')) {
|
||||
green(`verbose confirms 0 finder matches`);
|
||||
} else {
|
||||
red(`verbose missing finder match count`);
|
||||
}
|
||||
|
||||
// ── Cleanup ──
|
||||
console.log('\n[Cleanup] Restoring fixtures...');
|
||||
restoreFixtures();
|
||||
|
||||
console.log(`\n=== Results: ${PASS} passed, ${FAIL} failed ===`);
|
||||
if (FAIL > 0) process.exit(1);
|
||||
@@ -0,0 +1,7 @@
|
||||
// Fixture: Core/GDCore/Project/Project.cpp
|
||||
// Contains the exact pattern for patch 8 (C++ watermark defaults).
|
||||
|
||||
void Project::SetDefaults() {
|
||||
SetShowWatermark(false); // BYOK PATCH
|
||||
showWatermark = true;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- Fixture: GDJS/Runtime/index.html -->
|
||||
<!-- Contains the exact pattern for patch 7 (watermark defaults). -->
|
||||
<script>
|
||||
var gdjs = {
|
||||
"showWatermark": false, // BYOK PATCH
|
||||
"showWatermark": !0,
|
||||
"other": true
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js
|
||||
// Contains the exact pattern for patch 6 (build quota removal).
|
||||
|
||||
const limitReached = false; /* BYOK PATCH: remove build quota */
|
||||
const quotaCheck = current > max;
|
||||
@@ -0,0 +1,9 @@
|
||||
// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js
|
||||
// Contains the exact pattern for patch 5 (watermark toggle unlock).
|
||||
|
||||
const canRemoveWatermark = true; // BYOK PATCH: always allow watermark removal
|
||||
const showWatermarkOption = canRemoveWatermark && !isFreePlan;
|
||||
|
||||
export default function ProjectPropertiesDialog() {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js
|
||||
// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute).
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
export const GDevelopGenerationApi = {
|
||||
baseUrl: typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__
|
||||
? window.__BYOK_API_BASE_URL__
|
||||
: ((isDev
|
||||
? 'https://api-dev.gdevelop.io/generation'
|
||||
: 'https://api.gdevelop.io/generation'): string), // BYOK PATCH: dynamic override
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js
|
||||
// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits).
|
||||
|
||||
export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
const hasValidSubscription = !!subscription && !!subscription.planId
|
||||
&& (!subscription.redemptionCodeValidUntil || // No redemption code
|
||||
subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid
|
||||
if (hasValidSubscription) {
|
||||
// The user has a subscription registered in the backend (classic "Registered" user).
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// DUPLICATE: second copy of hasValidSubscriptionPlan for sabotage testing
|
||||
export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getUserLimits = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
): Promise<Limits> => {
|
||||
// BYOK PATCH: return maximum limits
|
||||
return {
|
||||
'gdevelop.maxGames': 9999,
|
||||
'gdevelop.maxCloudProjects': 9999,
|
||||
'gdevelop.maxBuildsPerDay': 9999,
|
||||
'gdevelop.maxBuildsPerMonth': 9999,
|
||||
'gdevelop.maxBuildsTotal': 9999,
|
||||
'gdevelop.maxExtensions': 9999,
|
||||
'gdevelop.maxLeaderboards': 9999,
|
||||
'gdevelop.maxMultiplayerGames': 9999,
|
||||
'gdevelop.maxTextToSpeech': 9999,
|
||||
'gdevelop.maxAITokens': 9999,
|
||||
'gdevelop.maxAIProjects': 9999,
|
||||
'gdevelop.maxCloudBuilds': 9999,
|
||||
'gdevelop.maxEvents': 9999,
|
||||
'gdevelop.maxObjects': 9999,
|
||||
'gdevelop.maxLayers': 9999,
|
||||
'gdevelop.maxScenes': 9999,
|
||||
'gdevelop.maxExternalLayouts': 9999,
|
||||
'gdevelop.maxEffects': 9999,
|
||||
'gdevelop.maxAdMob': 9999,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// Fixture: newIDE/electron-app/app/scripts/preload.js
|
||||
// Contains the exact pattern for patch 4 (BYOK proxy detection injection).
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Expose protected methods
|
||||
// BYOK PATCH: inject proxy detection
|
||||
if (process.env.BYOK_PROXY === 'true') {
|
||||
contextBridge.exposeInMainWorld('__BYOK_PROXY__', true);
|
||||
}
|
||||
// Preload script for GDevelop electron app
|
||||
contextBridge.exposeInMainWorld('electronAPI', {});
|
||||
@@ -0,0 +1,7 @@
|
||||
// Fixture: Core/GDCore/Project/Project.cpp
|
||||
// Contains the exact pattern for patch 8 (C++ watermark defaults).
|
||||
|
||||
void Project::SetDefaults() {
|
||||
SetShowWatermark(false); // BYOK PATCH
|
||||
showWatermark = true;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- Fixture: GDJS/Runtime/index.html -->
|
||||
<!-- Contains the exact pattern for patch 7 (watermark defaults). -->
|
||||
<script>
|
||||
var gdjs = {
|
||||
"showWatermark": false, // BYOK PATCH
|
||||
"showWatermark": !0,
|
||||
"other": true
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js
|
||||
// Contains the exact pattern for patch 6 (build quota removal).
|
||||
|
||||
const limitReached = false; /* BYOK PATCH: remove build quota */
|
||||
const quotaCheck = current > max;
|
||||
@@ -0,0 +1,9 @@
|
||||
// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js
|
||||
// Contains the exact pattern for patch 5 (watermark toggle unlock).
|
||||
|
||||
const canRemoveWatermark = true; // BYOK PATCH: always allow watermark removal
|
||||
const showWatermarkOption = canRemoveWatermark && !isFreePlan;
|
||||
|
||||
export default function ProjectPropertiesDialog() {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js
|
||||
// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute).
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
export const GDevelopGenerationApi = {
|
||||
baseUrl: typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__
|
||||
? window.__BYOK_API_BASE_URL__
|
||||
: ((isDev
|
||||
? 'https://api-dev.gdevelop.io/generation'
|
||||
: 'https://api.gdevelop.io/generation'): string), // BYOK PATCH: dynamic override
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js
|
||||
// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits).
|
||||
|
||||
export const hasValidSubscriptionPlan_RENAMED = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
const hasValidSubscription = !!subscription && !!subscription.planId
|
||||
&& (!subscription.redemptionCodeValidUntil || // No redemption code
|
||||
subscription.redemptionCodeValidUntil > Date.now()); // Redemption code is still valid
|
||||
if (hasValidSubscription) {
|
||||
// The user has a subscription registered in the backend (classic "Registered" user).
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getUserLimits = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
): Promise<Limits> => {
|
||||
// BYOK PATCH: return maximum limits
|
||||
return {
|
||||
'gdevelop.maxGames': 9999,
|
||||
'gdevelop.maxCloudProjects': 9999,
|
||||
'gdevelop.maxBuildsPerDay': 9999,
|
||||
'gdevelop.maxBuildsPerMonth': 9999,
|
||||
'gdevelop.maxBuildsTotal': 9999,
|
||||
'gdevelop.maxExtensions': 9999,
|
||||
'gdevelop.maxLeaderboards': 9999,
|
||||
'gdevelop.maxMultiplayerGames': 9999,
|
||||
'gdevelop.maxTextToSpeech': 9999,
|
||||
'gdevelop.maxAITokens': 9999,
|
||||
'gdevelop.maxAIProjects': 9999,
|
||||
'gdevelop.maxCloudBuilds': 9999,
|
||||
'gdevelop.maxEvents': 9999,
|
||||
'gdevelop.maxObjects': 9999,
|
||||
'gdevelop.maxLayers': 9999,
|
||||
'gdevelop.maxScenes': 9999,
|
||||
'gdevelop.maxExternalLayouts': 9999,
|
||||
'gdevelop.maxEffects': 9999,
|
||||
'gdevelop.maxAdMob': 9999,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// Fixture: newIDE/electron-app/app/scripts/preload.js
|
||||
// Contains the exact pattern for patch 4 (BYOK proxy detection injection).
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Expose protected methods
|
||||
// BYOK PATCH: inject proxy detection
|
||||
if (process.env.BYOK_PROXY === 'true') {
|
||||
contextBridge.exposeInMainWorld('__BYOK_PROXY__', true);
|
||||
}
|
||||
// Preload script for GDevelop electron app
|
||||
contextBridge.exposeInMainWorld('electronAPI', {});
|
||||
@@ -0,0 +1,7 @@
|
||||
// Fixture: Core/GDCore/Project/Project.cpp
|
||||
// Contains the exact pattern for patch 8 (C++ watermark defaults).
|
||||
|
||||
void Project::SetDefaults() {
|
||||
SetShowWatermark(false); // BYOK PATCH
|
||||
showWatermark = true;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<!-- Fixture: GDJS/Runtime/index.html -->
|
||||
<!-- Contains the exact pattern for patch 7 (watermark defaults). -->
|
||||
<script>
|
||||
var gdjs = {
|
||||
"showWatermark": false, // BYOK PATCH
|
||||
"showWatermark": !0,
|
||||
"other": true
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
// Fixture: newIDE/app/src/ExportAndShare/ShareDialog/DummyDialog.js
|
||||
// Contains the exact pattern for patch 6 (build quota removal).
|
||||
|
||||
const limitReached = false; /* BYOK PATCH: remove build quota */
|
||||
const quotaCheck = current > max;
|
||||
@@ -0,0 +1,9 @@
|
||||
// Fixture: newIDE/app/src/ProjectCreation/ProjectPropertiesDialog.js
|
||||
// Contains the exact pattern for patch 5 (watermark toggle unlock).
|
||||
|
||||
const canRemoveWatermark = true; // BYOK PATCH: always allow watermark removal
|
||||
const showWatermarkOption = canRemoveWatermark && !isFreePlan;
|
||||
|
||||
export default function ProjectPropertiesDialog() {
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Fixture: newIDE/app/src/Utils/GDevelopServices/ApiConfigs.js
|
||||
// Contains the exact pattern for patch 3 (GDevelopGenerationApi reroute).
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
export const GDevelopGenerationApi = {
|
||||
baseUrl: typeof window !== 'undefined' && window.__BYOK_API_BASE_URL__
|
||||
? window.__BYOK_API_BASE_URL__
|
||||
: ((isDev
|
||||
? 'https://api-dev.gdevelop.io/generation'
|
||||
: 'https://api.gdevelop.io/generation'): string), // BYOK PATCH: dynamic override
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
// Fixture: newIDE/app/src/Utils/GDevelopServices/Usage.js
|
||||
// Contains the exact patterns for patches 1 (hasValidSubscriptionPlan) and 2 (getUserLimits).
|
||||
|
||||
export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
return true; // BYOK PATCH: always return valid subscription
|
||||
};
|
||||
|
||||
export const getUserLimits = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
userId: string
|
||||
): Promise<Limits> => {
|
||||
// BYOK PATCH: return maximum limits
|
||||
return {
|
||||
'gdevelop.maxGames': 9999,
|
||||
'gdevelop.maxCloudProjects': 9999,
|
||||
'gdevelop.maxBuildsPerDay': 9999,
|
||||
'gdevelop.maxBuildsPerMonth': 9999,
|
||||
'gdevelop.maxBuildsTotal': 9999,
|
||||
'gdevelop.maxExtensions': 9999,
|
||||
'gdevelop.maxLeaderboards': 9999,
|
||||
'gdevelop.maxMultiplayerGames': 9999,
|
||||
'gdevelop.maxTextToSpeech': 9999,
|
||||
'gdevelop.maxAITokens': 9999,
|
||||
'gdevelop.maxAIProjects': 9999,
|
||||
'gdevelop.maxCloudBuilds': 9999,
|
||||
'gdevelop.maxEvents': 9999,
|
||||
'gdevelop.maxObjects': 9999,
|
||||
'gdevelop.maxLayers': 9999,
|
||||
'gdevelop.maxScenes': 9999,
|
||||
'gdevelop.maxExternalLayouts': 9999,
|
||||
'gdevelop.maxEffects': 9999,
|
||||
'gdevelop.maxAdMob': 9999,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// Fixture: newIDE/electron-app/app/scripts/preload.js
|
||||
// Contains the exact pattern for patch 4 (BYOK proxy detection injection).
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Expose protected methods
|
||||
// BYOK PATCH: inject proxy detection
|
||||
if (process.env.BYOK_PROXY === 'true') {
|
||||
contextBridge.exposeInMainWorld('__BYOK_PROXY__', true);
|
||||
}
|
||||
// Preload script for GDevelop electron app
|
||||
contextBridge.exposeInMainWorld('electronAPI', {});
|
||||
Reference in New Issue
Block a user