restore: patch manifest, test fixtures, and fixture tooling from orphaned commits

This commit is contained in:
John Doe
2026-05-16 07:46:11 -04:00
parent d423dd95ab
commit 92e7711009
30 changed files with 1182 additions and 269 deletions
+1
View File
@@ -0,0 +1 @@
[]
+178
View File
@@ -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,
};
+83
View File
@@ -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'\\);"
}
]
+4
View File
@@ -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"
+149
View File
@@ -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
View File
@@ -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();
+105
View File
@@ -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.`);
+7
View File
@@ -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" "$@"
+160
View File
@@ -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', {});
+7
View File
@@ -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
View File
@@ -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', {});