'use strict'; const path = require('path'); const BEGIN_MARKER = '# BEGIN LINKFIELD MANAGED PROXY'; const END_MARKER = '# END LINKFIELD MANAGED PROXY'; function normalizePort(value) { const port = Number(value); if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid Apache bridge port: ${value}`); return port; } function renderPublicBoundary(){ return `\n`+ ` # The service source may share this directory with Apache. Expose only\n`+ ` # the curated browser bundle and deny every other direct file request.\n`+ ` RewriteRule ^$ - [L]\n`+ ` RewriteRule ^(?:index\\.html|style\\.css|favicon\\.(?:svg|ico)|build-meta\\.js|runtime-config\\.js|shared-contracts\\.js|store-catalog\\.(?:generated\\.js|json)|puzzle-patterns\\.js|puzzle-core\\.js|app-logic\\.js|archive-codec\\.js|field-persistence(?:-worker)?\\.js|puzzle-worker\\.js|app\\.js|api-bridge\\.php)$ - [L]\n`+ ` RewriteRule ^(?:assets|client)(?:/|$) - [L]\n`+ ` RewriteRule ^ - [F,L]\n`; } function renderSensitiveFileFallback(){ return `\n`+ ` Require all denied\n`+ `\n`; } function renderApacheBridge(portValue) { const port = normalizePort(portValue); return `${BEGIN_MARKER}\n` + `ServerSignature Off\n` + `\n` + ` Header always unset X-Powered-By\n` + `\n` + `\n` + ` RewriteEngine On\n` + `\n` + ` # Prefer a native Apache proxy when the host permits it.\n` + ` \n` + ` \n` + ` RewriteCond %{HTTP:Upgrade} =websocket [NC]\n` + ` RewriteRule ^api/realtime/?$ ws://127.0.0.1:${port}/api/realtime [P,L]\n` + ` \n` + ` \n` + ` RewriteRule ^api/(.*)$ http://127.0.0.1:${port}/api/$1 [P,L]\n` + ` \n` + ` \n` + `\n` + ` # Shared hosts often disable mod_proxy. Route ordinary API requests\n` + ` # through the bundled PHP bridge instead.\n` + ` RewriteCond %{REQUEST_FILENAME} !-f\n` + ` RewriteRule ^api/(.*)$ api-bridge.php?path=/api/$1 [QSA,L]\n` + renderPublicBoundary()+ `\n` + renderSensitiveFileFallback()+ `${END_MARKER}\n`; } function renderApacheBootstrap(){ return `${BEGIN_MARKER}\n`+ `ServerSignature Off\n`+ `\n`+ ` Header always unset X-Powered-By\n`+ `\n`+ `\n`+ ` RewriteEngine On\n\n`+ ` # Safe bootstrap: use the bounded PHP bridge until the Node server\n`+ ` # writes a verified current proxy port after it begins listening.\n`+ ` RewriteCond %{REQUEST_FILENAME} !-f\n`+ ` RewriteRule ^api/(.*)$ api-bridge.php?path=/api/$1 [QSA,L]\n`+ renderPublicBoundary()+ `\n`+ renderSensitiveFileFallback()+ `${END_MARKER}\n`; } function replaceManagedBlock(existingValue, managedBlock) { const existing = String(existingValue || '').replace(/\r\n?/g, '\n'); const pattern = new RegExp(`(?:^|\\n)${BEGIN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\n|$)`, 'g'); const preserved = existing.replace(pattern, '\n').replace(/^\n+|\n+$/g, ''); return preserved ? `${preserved}\n\n${managedBlock}` : managedBlock; } async function installApacheBridge({fsp, root, port, enabled = true} = {}) { if (!enabled) return {enabled:false, written:false, file:null}; if (!fsp || typeof fsp.readFile !== 'function' || typeof fsp.writeFile !== 'function') throw new Error('A promises-compatible filesystem is required'); const file = path.join(root, '.htaccess'); let existing = ''; try { existing = await fsp.readFile(file, 'utf8'); } catch (error) { if (error?.code !== 'ENOENT') throw error; } const next = replaceManagedBlock(existing, renderApacheBridge(port)); if (next === existing.replace(/\r\n?/g, '\n')) return {enabled:true, written:false, file}; const temporary = `${file}.linkfield-${process.pid}-${Date.now()}.tmp`; await fsp.writeFile(temporary, next, {encoding:'utf8', mode:0o644}); await fsp.rename(temporary, file); return {enabled:true, written:true, file}; } module.exports = Object.freeze({BEGIN_MARKER, END_MARKER, renderApacheBridge,renderApacheBootstrap,replaceManagedBlock,installApacheBridge});