@mozole/cli 1.4.2 → 2.0.0-b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -4
- package/README.md +85 -89
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +204 -157
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +5 -8
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +49 -812
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -7
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +69 -377
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/new.d.ts +8 -10
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +32 -149
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/phase.d.ts +11 -0
- package/dist/commands/phase.d.ts.map +1 -0
- package/dist/commands/phase.js +106 -0
- package/dist/commands/phase.js.map +1 -0
- package/dist/commands/prototype.d.ts +6 -0
- package/dist/commands/prototype.d.ts.map +1 -0
- package/dist/commands/prototype.js +28 -0
- package/dist/commands/prototype.js.map +1 -0
- package/dist/commands/repomap.d.ts +6 -0
- package/dist/commands/repomap.d.ts.map +1 -0
- package/dist/commands/repomap.js +71 -0
- package/dist/commands/repomap.js.map +1 -0
- package/dist/commands/test.d.ts +6 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +115 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/ui.d.ts +1 -37
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +75 -317
- package/dist/commands/ui.js.map +1 -1
- package/dist/commands/verify.d.ts +12 -7
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +148 -65
- package/dist/commands/verify.js.map +1 -1
- package/dist/qa/a11y.d.ts +24 -0
- package/dist/qa/a11y.d.ts.map +1 -0
- package/dist/qa/a11y.js +124 -0
- package/dist/qa/a11y.js.map +1 -0
- package/dist/qa/breakpoints.d.ts +16 -0
- package/dist/qa/breakpoints.d.ts.map +1 -0
- package/dist/qa/breakpoints.js +45 -0
- package/dist/qa/breakpoints.js.map +1 -0
- package/dist/qa/contract.d.ts +21 -0
- package/dist/qa/contract.d.ts.map +1 -0
- package/dist/qa/contract.js +116 -0
- package/dist/qa/contract.js.map +1 -0
- package/dist/qa/index.d.ts +6 -0
- package/dist/qa/index.d.ts.map +1 -0
- package/dist/qa/index.js +6 -0
- package/dist/qa/index.js.map +1 -0
- package/dist/qa/probe.d.ts +48 -0
- package/dist/qa/probe.d.ts.map +1 -0
- package/dist/qa/probe.js +206 -0
- package/dist/qa/probe.js.map +1 -0
- package/dist/qa/runner.d.ts +27 -32
- package/dist/qa/runner.d.ts.map +1 -1
- package/dist/qa/runner.js +286 -336
- package/dist/qa/runner.js.map +1 -1
- package/dist/scaffold/agents.d.ts +5 -0
- package/dist/scaffold/agents.d.ts.map +1 -0
- package/dist/scaffold/agents.js +84 -0
- package/dist/scaffold/agents.js.map +1 -0
- package/dist/scaffold/backend.d.ts +6 -8
- package/dist/scaffold/backend.d.ts.map +1 -1
- package/dist/scaffold/backend.js +318 -199
- package/dist/scaffold/backend.js.map +1 -1
- package/dist/scaffold/design.d.ts +4 -3
- package/dist/scaffold/design.d.ts.map +1 -1
- package/dist/scaffold/design.js +38 -47
- package/dist/scaffold/design.js.map +1 -1
- package/dist/scaffold/flagship.d.ts +8 -0
- package/dist/scaffold/flagship.d.ts.map +1 -0
- package/dist/scaffold/flagship.js +337 -0
- package/dist/scaffold/flagship.js.map +1 -0
- package/dist/scaffold/index.d.ts +9 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +48 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/phases.d.ts +10 -0
- package/dist/scaffold/phases.d.ts.map +1 -0
- package/dist/scaffold/phases.js +165 -0
- package/dist/scaffold/phases.js.map +1 -0
- package/dist/scaffold/repomap.d.ts +6 -0
- package/dist/scaffold/repomap.d.ts.map +1 -0
- package/dist/scaffold/repomap.js +89 -0
- package/dist/scaffold/repomap.js.map +1 -0
- package/dist/scaffold/standard.d.ts +13 -0
- package/dist/scaffold/standard.d.ts.map +1 -0
- package/dist/scaffold/standard.js +371 -0
- package/dist/scaffold/standard.js.map +1 -0
- package/dist/scaffold/tokens.d.ts +3 -0
- package/dist/scaffold/tokens.d.ts.map +1 -0
- package/dist/scaffold/tokens.js +126 -0
- package/dist/scaffold/tokens.js.map +1 -0
- package/dist/server/manager.d.ts +23 -0
- package/dist/server/manager.d.ts.map +1 -0
- package/dist/server/manager.js +96 -0
- package/dist/server/manager.js.map +1 -0
- package/dist/utils/ai-trace.d.ts +8 -0
- package/dist/utils/ai-trace.d.ts.map +1 -0
- package/dist/utils/ai-trace.js +69 -0
- package/dist/utils/ai-trace.js.map +1 -0
- package/dist/utils/browser.d.ts +10 -0
- package/dist/utils/browser.d.ts.map +1 -0
- package/dist/utils/browser.js +127 -0
- package/dist/utils/browser.js.map +1 -0
- package/dist/utils/fs.d.ts +11 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +166 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +69 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/process.d.ts +21 -0
- package/dist/utils/process.d.ts.map +1 -0
- package/dist/utils/process.js +120 -0
- package/dist/utils/process.js.map +1 -0
- package/dist/version.d.ts +21 -36
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +21 -44
- package/dist/version.js.map +1 -1
- package/package.json +19 -38
- package/CHANGELOG.md +0 -30
- package/dist/commands/cce.d.ts +0 -7
- package/dist/commands/cce.d.ts.map +0 -1
- package/dist/commands/cce.js +0 -37
- package/dist/commands/cce.js.map +0 -1
- package/dist/commands/logs.d.ts +0 -3
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/logs.js +0 -39
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/qa.d.ts +0 -12
- package/dist/commands/qa.d.ts.map +0 -1
- package/dist/commands/qa.js +0 -143
- package/dist/commands/qa.js.map +0 -1
- package/dist/commands/stats.d.ts +0 -37
- package/dist/commands/stats.d.ts.map +0 -1
- package/dist/commands/stats.js +0 -169
- package/dist/commands/stats.js.map +0 -1
- package/dist/config/project.d.ts +0 -51
- package/dist/config/project.d.ts.map +0 -1
- package/dist/config/project.js +0 -81
- package/dist/config/project.js.map +0 -1
- package/dist/context/cce.d.ts +0 -39
- package/dist/context/cce.d.ts.map +0 -1
- package/dist/context/cce.js +0 -455
- package/dist/context/cce.js.map +0 -1
- package/dist/governor/fingerprint.d.ts +0 -7
- package/dist/governor/fingerprint.d.ts.map +0 -1
- package/dist/governor/fingerprint.js +0 -129
- package/dist/governor/fingerprint.js.map +0 -1
- package/dist/governor/hook.d.ts +0 -13
- package/dist/governor/hook.d.ts.map +0 -1
- package/dist/governor/hook.js +0 -389
- package/dist/governor/hook.js.map +0 -1
- package/dist/governor/state.d.ts +0 -45
- package/dist/governor/state.d.ts.map +0 -1
- package/dist/governor/state.js +0 -60
- package/dist/governor/state.js.map +0 -1
- package/dist/integrations/antigravity.d.ts +0 -20
- package/dist/integrations/antigravity.d.ts.map +0 -1
- package/dist/integrations/antigravity.js +0 -153
- package/dist/integrations/antigravity.js.map +0 -1
- package/dist/output.d.ts +0 -5
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -2
- package/dist/output.js.map +0 -1
- package/dist/platform/executable.d.ts +0 -11
- package/dist/platform/executable.d.ts.map +0 -1
- package/dist/platform/executable.js +0 -69
- package/dist/platform/executable.js.map +0 -1
- package/dist/platform/process.d.ts +0 -25
- package/dist/platform/process.d.ts.map +0 -1
- package/dist/platform/process.js +0 -179
- package/dist/platform/process.js.map +0 -1
- package/dist/progress.d.ts +0 -44
- package/dist/progress.d.ts.map +0 -1
- package/dist/progress.js +0 -123
- package/dist/progress.js.map +0 -1
- package/dist/qa/diff.d.ts +0 -10
- package/dist/qa/diff.d.ts.map +0 -1
- package/dist/qa/diff.js +0 -91
- package/dist/qa/diff.js.map +0 -1
- package/dist/qa/server.d.ts +0 -10
- package/dist/qa/server.d.ts.map +0 -1
- package/dist/qa/server.js +0 -127
- package/dist/qa/server.js.map +0 -1
- package/dist/scaffold/adoption.d.ts +0 -17
- package/dist/scaffold/adoption.d.ts.map +0 -1
- package/dist/scaffold/adoption.js +0 -279
- package/dist/scaffold/adoption.js.map +0 -1
- package/dist/scaffold/legacy.d.ts +0 -4
- package/dist/scaffold/legacy.d.ts.map +0 -1
- package/dist/scaffold/legacy.js +0 -139
- package/dist/scaffold/legacy.js.map +0 -1
- package/dist/scaffold/routes.d.ts +0 -3
- package/dist/scaffold/routes.d.ts.map +0 -1
- package/dist/scaffold/routes.js +0 -37
- package/dist/scaffold/routes.js.map +0 -1
- package/dist/scaffold/standalone.d.ts +0 -7
- package/dist/scaffold/standalone.d.ts.map +0 -1
- package/dist/scaffold/standalone.js +0 -294
- package/dist/scaffold/standalone.js.map +0 -1
- package/dist/scaffold/templates.d.ts +0 -8
- package/dist/scaffold/templates.d.ts.map +0 -1
- package/dist/scaffold/templates.js +0 -312
- package/dist/scaffold/templates.js.map +0 -1
- package/dist/scaffold/write.d.ts +0 -2
- package/dist/scaffold/write.d.ts.map +0 -1
- package/dist/scaffold/write.js +0 -38
- package/dist/scaffold/write.js.map +0 -1
- package/dist/telemetry/store.d.ts +0 -33
- package/dist/telemetry/store.d.ts.map +0 -1
- package/dist/telemetry/store.js +0 -75
- package/dist/telemetry/store.js.map +0 -1
- package/dist/verify/runner.d.ts +0 -15
- package/dist/verify/runner.d.ts.map +0 -1
- package/dist/verify/runner.js +0 -112
- package/dist/verify/runner.js.map +0 -1
- package/docs/agent-audit.md +0 -24
package/dist/scaffold/backend.js
CHANGED
|
@@ -1,224 +1,343 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
timeoutMs: 10_000,
|
|
36
|
-
});
|
|
37
|
-
if (version.exitCode !== 0 ||
|
|
38
|
-
!/^\d+$/.test(version.stdout.trim()) ||
|
|
39
|
-
Number(version.stdout.trim()) < 80000) {
|
|
40
|
-
return {
|
|
41
|
-
enabled: true,
|
|
42
|
-
healthy: false,
|
|
43
|
-
detail: "PHP 8.0 or newer is required; runtime probe failed.",
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
const php = phpExecutable;
|
|
47
|
-
async function lint(directory) {
|
|
48
|
-
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
49
|
-
const file = path.join(directory, entry.name);
|
|
50
|
-
if (entry.isDirectory()) {
|
|
51
|
-
const failure = await lint(file);
|
|
52
|
-
if (failure)
|
|
53
|
-
return failure;
|
|
54
|
-
}
|
|
55
|
-
else if (entry.isFile() && entry.name.endsWith(".php")) {
|
|
56
|
-
const result = await run(php, ["-l", file], {
|
|
57
|
-
cwd: root,
|
|
58
|
-
timeoutMs: 10_000,
|
|
59
|
-
});
|
|
60
|
-
if (result.exitCode !== 0)
|
|
61
|
-
return `${path.relative(root, file)}: ${(result.stderr || result.stdout).trim().slice(0, 2000)}`;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
const failure = await lint(path.join(root, "api"));
|
|
67
|
-
if (failure)
|
|
68
|
-
return { enabled: true, healthy: false, detail: failure };
|
|
69
|
-
return {
|
|
70
|
-
enabled: true,
|
|
71
|
-
healthy: true,
|
|
72
|
-
detail: `PHP runtime detected (${path.basename(phpExecutable)}); API routes ready at ${config.backend.apiPrefix}`,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
return {
|
|
77
|
-
enabled: true,
|
|
78
|
-
healthy: false,
|
|
79
|
-
detail: `Backend inspection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
2
|
+
import { atomicWrite } from "../utils/fs.js";
|
|
3
|
+
export function generatePhpConfig() {
|
|
4
|
+
return `<?php
|
|
5
|
+
/**
|
|
6
|
+
* Mozole Studio - Production Backend Configuration
|
|
7
|
+
* Pure PHP 8.1+ Zero-Dependency Runtime
|
|
8
|
+
*/
|
|
9
|
+
defined('MOZOLE_SECURE') or die(json_encode(['error' => 'Direct access prohibited']));
|
|
10
|
+
|
|
11
|
+
return [
|
|
12
|
+
// Recipients
|
|
13
|
+
'mail_to' => 'info@mozole.studio',
|
|
14
|
+
'mail_subject_prefix' => '[Mozole Contact] ',
|
|
15
|
+
|
|
16
|
+
// Allowed frontend origins for CORS and Referer verification
|
|
17
|
+
'allowed_origins' => [
|
|
18
|
+
'http://localhost:5173',
|
|
19
|
+
'http://localhost:3000',
|
|
20
|
+
'https://mozole.studio',
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
// Security & Rate Limiting
|
|
24
|
+
'rate_limit' => [
|
|
25
|
+
'max_requests' => 5, // Max requests
|
|
26
|
+
'window_seconds' => 300, // Per 5 minutes
|
|
27
|
+
'storage_dir' => sys_get_temp_dir() . '/mozole_rate_limits',
|
|
28
|
+
],
|
|
29
|
+
|
|
30
|
+
// Honeypot field name (must remain empty on legitimate submissions)
|
|
31
|
+
'honeypot_field' => '_mozole_website_url',
|
|
32
|
+
];
|
|
33
|
+
`;
|
|
82
34
|
}
|
|
83
|
-
export function
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
declare(strict_types=1);
|
|
35
|
+
export function generatePhpMailer() {
|
|
36
|
+
return `<?php
|
|
37
|
+
defined('MOZOLE_SECURE') or die(json_encode(['error' => 'Direct access prohibited']));
|
|
87
38
|
|
|
88
|
-
|
|
39
|
+
function mozole_send_mail(array $config, string $fromEmail, string $name, string $subject, string $body): bool {
|
|
40
|
+
$to = filter_var($config['mail_to'], FILTER_VALIDATE_EMAIL);
|
|
41
|
+
if (!$to) return false;
|
|
42
|
+
$cleanSubject = $config['mail_subject_prefix'] . str_replace(["\\r", "\\n", "\\0"], '', $subject);
|
|
43
|
+
|
|
44
|
+
// Prevent email header injection
|
|
45
|
+
$cleanFromEmail = filter_var($fromEmail, FILTER_VALIDATE_EMAIL);
|
|
46
|
+
if (!$cleanFromEmail) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
$cleanName = str_replace(["\\r", "\\n", "\\0"], '', $name);
|
|
50
|
+
|
|
51
|
+
$headers = [];
|
|
52
|
+
$headers[] = 'MIME-Version: 1.0';
|
|
53
|
+
$headers[] = 'Content-Type: text/html; charset=UTF-8';
|
|
54
|
+
$headers[] = 'From: ' . sprintf('=?UTF-8?B?%s?= <%s>', base64_encode($cleanName), $cleanFromEmail);
|
|
55
|
+
$headers[] = 'Reply-To: ' . $cleanFromEmail;
|
|
56
|
+
$headers[] = 'X-Mailer: MozoleStudioPurePHP/2.0';
|
|
57
|
+
|
|
58
|
+
$htmlContent = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family: sans-serif; line-height: 1.6; color: #111;">'
|
|
59
|
+
. '<h2>New Message via Website Contact Form</h2>'
|
|
60
|
+
. '<p><strong>Name:</strong> ' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</p>'
|
|
61
|
+
. '<p><strong>Email:</strong> ' . htmlspecialchars($cleanFromEmail, ENT_QUOTES, 'UTF-8') . '</p>'
|
|
62
|
+
. '<p><strong>Subject:</strong> ' . htmlspecialchars($subject, ENT_QUOTES, 'UTF-8') . '</p>'
|
|
63
|
+
. '<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 20px 0;" />'
|
|
64
|
+
. '<p style="white-space: pre-wrap;">' . nl2br(htmlspecialchars($body, ENT_QUOTES, 'UTF-8')) . '</p>'
|
|
65
|
+
. '</body></html>';
|
|
89
66
|
|
|
90
|
-
|
|
67
|
+
return mail($to, '=?UTF-8?B?' . base64_encode($cleanSubject) . '?=', $htmlContent, implode("\\r\\n", $headers));
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
export function generatePhpIndex() {
|
|
72
|
+
return `<?php
|
|
73
|
+
/**
|
|
74
|
+
* Mozole Studio - API Front Controller
|
|
75
|
+
* Pure PHP 8.1+ Zero-Dependency Runtime
|
|
76
|
+
*/
|
|
77
|
+
define('MOZOLE_SECURE', true);
|
|
78
|
+
header('Content-Type: application/json; charset=UTF-8');
|
|
91
79
|
header('X-Content-Type-Options: nosniff');
|
|
92
80
|
header('X-Frame-Options: DENY');
|
|
81
|
+
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
93
82
|
|
|
94
|
-
$
|
|
95
|
-
|
|
83
|
+
$config = require __DIR__ . '/config.php';
|
|
84
|
+
require_once __DIR__ . '/mailer.php';
|
|
96
85
|
|
|
97
|
-
//
|
|
98
|
-
$
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
86
|
+
// 1. CORS Verification
|
|
87
|
+
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
88
|
+
$isAllowedOrigin = in_array($origin, $config['allowed_origins'], true);
|
|
89
|
+
|
|
90
|
+
if ($isAllowedOrigin) {
|
|
91
|
+
header('Access-Control-Allow-Origin: ' . $origin);
|
|
92
|
+
header('Access-Control-Allow-Methods: POST, OPTIONS, GET');
|
|
93
|
+
header('Access-Control-Allow-Headers: Content-Type, Accept');
|
|
103
94
|
}
|
|
104
95
|
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
// Handle CORS preflight
|
|
97
|
+
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
98
|
+
http_response_code(204);
|
|
99
|
+
exit;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Health check endpoint
|
|
103
|
+
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
|
104
|
+
http_response_code(200);
|
|
105
|
+
echo json_encode([
|
|
106
|
+
'status' => 'operational',
|
|
107
|
+
'service' => 'Mozole Studio API',
|
|
108
|
+
'php_version' => PHP_VERSION,
|
|
109
|
+
'timestamp' => time(),
|
|
110
|
+
]);
|
|
111
|
+
exit;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
107
115
|
http_response_code(405);
|
|
108
116
|
echo json_encode(['error' => 'Method not allowed']);
|
|
109
117
|
exit;
|
|
110
118
|
}
|
|
111
|
-
|
|
119
|
+
|
|
120
|
+
// 2. Require a trusted Origin, or a full-origin Referer when Origin is absent.
|
|
121
|
+
function mozole_origin(string $url): ?string {
|
|
122
|
+
$parts = parse_url($url);
|
|
123
|
+
if (!$parts || !isset($parts['scheme'], $parts['host'])) return null;
|
|
124
|
+
$scheme = strtolower($parts['scheme']);
|
|
125
|
+
if (!in_array($scheme, ['http', 'https'], true)) return null;
|
|
126
|
+
$port = $parts['port'] ?? ($scheme === 'https' ? 443 : 80);
|
|
127
|
+
return $scheme . '://' . strtolower($parts['host']) . ':' . $port;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$source = $origin !== '' ? $origin : ($_SERVER['HTTP_REFERER'] ?? '');
|
|
131
|
+
$sourceOrigin = mozole_origin($source);
|
|
132
|
+
$allowedOrigins = array_map('mozole_origin', $config['allowed_origins']);
|
|
133
|
+
if ($sourceOrigin === null || !in_array($sourceOrigin, $allowedOrigins, true)) {
|
|
134
|
+
http_response_code(403);
|
|
135
|
+
echo json_encode(['error' => 'Forbidden: Untrusted request origin']);
|
|
136
|
+
exit;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 3. IP Rate Limiting
|
|
140
|
+
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
|
141
|
+
$rateDir = $config['rate_limit']['storage_dir'];
|
|
142
|
+
if (!is_dir($rateDir)) {
|
|
143
|
+
@mkdir($rateDir, 0750, true);
|
|
144
|
+
}
|
|
145
|
+
$ipHash = md5($clientIp);
|
|
146
|
+
$rateFile = $rateDir . '/rl_' . $ipHash . '.json';
|
|
147
|
+
|
|
148
|
+
$now = time();
|
|
149
|
+
$window = $config['rate_limit']['window_seconds'];
|
|
150
|
+
$maxRequests = $config['rate_limit']['max_requests'];
|
|
151
|
+
|
|
152
|
+
// Keep the read, quota check and write under the same exclusive lock.
|
|
153
|
+
$rateHandle = @fopen($rateFile, 'c+');
|
|
154
|
+
if ($rateHandle === false || !flock($rateHandle, LOCK_EX)) {
|
|
155
|
+
if (is_resource($rateHandle)) fclose($rateHandle);
|
|
156
|
+
http_response_code(503);
|
|
157
|
+
echo json_encode(['error' => 'Rate limiter unavailable. Please try again later.']);
|
|
112
158
|
exit;
|
|
113
159
|
}
|
|
114
160
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
|
134
|
-
break;
|
|
135
|
-
|
|
136
|
-
default:
|
|
137
|
-
http_response_code(404);
|
|
138
|
-
echo json_encode([
|
|
139
|
-
'error' => 'Endpoint not found',
|
|
140
|
-
'path' => $uri,
|
|
141
|
-
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
`,
|
|
145
|
-
"api/config.php": `<?php
|
|
146
|
-
declare(strict_types=1);
|
|
147
|
-
|
|
148
|
-
function env(string $key, ?string $default = null): ?string {
|
|
149
|
-
$value = getenv($key);
|
|
150
|
-
return $value !== false ? $value : $default;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Environment configuration
|
|
154
|
-
define('APP_ENV', env('APP_ENV', 'production'));
|
|
155
|
-
define('APP_DEBUG', env('APP_DEBUG', 'false') === 'true');
|
|
156
|
-
|
|
157
|
-
// Optional database connection helper
|
|
158
|
-
function get_db_connection(): ?PDO {
|
|
159
|
-
static $pdo = null;
|
|
160
|
-
if ($pdo !== null) {
|
|
161
|
-
return $pdo;
|
|
161
|
+
try {
|
|
162
|
+
$raw = stream_get_contents($rateHandle);
|
|
163
|
+
$stored = json_decode($raw === false ? '' : $raw, true);
|
|
164
|
+
$timestamps = is_array($stored)
|
|
165
|
+
? array_values(array_filter($stored, fn($t) => is_int($t) && $t > $now - $window))
|
|
166
|
+
: [];
|
|
167
|
+
|
|
168
|
+
if (count($timestamps) >= $maxRequests) {
|
|
169
|
+
http_response_code(429);
|
|
170
|
+
echo json_encode(['error' => 'Too many requests. Please wait a few minutes before trying again.']);
|
|
171
|
+
} else {
|
|
172
|
+
$timestamps[] = $now;
|
|
173
|
+
$encoded = json_encode($timestamps);
|
|
174
|
+
rewind($rateHandle);
|
|
175
|
+
if (!ftruncate($rateHandle, 0) || fwrite($rateHandle, $encoded) !== strlen($encoded) || !fflush($rateHandle)) {
|
|
176
|
+
http_response_code(503);
|
|
177
|
+
echo json_encode(['error' => 'Rate limiter unavailable. Please try again later.']);
|
|
178
|
+
}
|
|
162
179
|
}
|
|
180
|
+
} finally {
|
|
181
|
+
flock($rateHandle, LOCK_UN);
|
|
182
|
+
fclose($rateHandle);
|
|
183
|
+
}
|
|
184
|
+
if (http_response_code() >= 400) exit;
|
|
185
|
+
|
|
186
|
+
// 4. Payload Parsing
|
|
187
|
+
$rawInput = file_get_contents('php://input');
|
|
188
|
+
$data = json_decode($rawInput, true);
|
|
189
|
+
|
|
190
|
+
if (!is_array($data)) {
|
|
191
|
+
// Fallback to application/x-www-form-urlencoded
|
|
192
|
+
$data = $_POST;
|
|
193
|
+
}
|
|
163
194
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
195
|
+
// 5. Honeypot Verification (strict check against empty string or numeric zero)
|
|
196
|
+
$honeyField = $config['honeypot_field'];
|
|
197
|
+
if (isset($data[$honeyField]) && trim((string)$data[$honeyField]) !== '') {
|
|
198
|
+
// Silently succeed for bots without sending email
|
|
199
|
+
http_response_code(200);
|
|
200
|
+
echo json_encode(['success' => true, 'message' => 'Message delivered.']);
|
|
201
|
+
exit;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 6. Input Sanitization & Validation
|
|
205
|
+
foreach (['name', 'email', 'subject', 'message'] as $field) {
|
|
206
|
+
if (isset($data[$field]) && !is_string($data[$field])) {
|
|
207
|
+
http_response_code(400);
|
|
208
|
+
echo json_encode(['error' => 'Contact fields must be strings.']);
|
|
209
|
+
exit;
|
|
167
210
|
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// PCRE Unicode counting works without the optional mbstring extension.
|
|
214
|
+
function mozole_length(string $value): int {
|
|
215
|
+
$count = preg_match_all('/./us', $value);
|
|
216
|
+
return $count === false ? PHP_INT_MAX : $count;
|
|
217
|
+
}
|
|
218
|
+
$name = trim($data['name'] ?? '');
|
|
219
|
+
$email = trim($data['email'] ?? '');
|
|
220
|
+
$subject = trim($data['subject'] ?? 'Website Inquiry');
|
|
221
|
+
$message = trim($data['message'] ?? '');
|
|
222
|
+
|
|
223
|
+
if (empty($name) || mozole_length($name) > 120) {
|
|
224
|
+
http_response_code(400);
|
|
225
|
+
echo json_encode(['error' => 'A valid name is required (max 120 characters).']);
|
|
226
|
+
exit;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (mozole_length($subject) > 200) {
|
|
230
|
+
http_response_code(400);
|
|
231
|
+
echo json_encode(['error' => 'Subject line is too long (max 200 characters).']);
|
|
232
|
+
exit;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
236
|
+
http_response_code(400);
|
|
237
|
+
echo json_encode(['error' => 'A valid email address is required.']);
|
|
238
|
+
exit;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (empty($message) || mozole_length($message) > 5000) {
|
|
242
|
+
http_response_code(400);
|
|
243
|
+
echo json_encode(['error' => 'Message body is required (max 5000 characters).']);
|
|
244
|
+
exit;
|
|
245
|
+
}
|
|
168
246
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
247
|
+
// 7. Dispatch Mail
|
|
248
|
+
$sent = mozole_send_mail($config, $email, $name, $subject, $message);
|
|
249
|
+
|
|
250
|
+
if ($sent) {
|
|
251
|
+
http_response_code(200);
|
|
252
|
+
echo json_encode(['success' => true, 'message' => 'Your message has been sent successfully.']);
|
|
253
|
+
} else {
|
|
254
|
+
http_response_code(502);
|
|
255
|
+
echo json_encode([
|
|
256
|
+
'success' => false,
|
|
257
|
+
'error' => 'Message could not be sent. Please try again later.'
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
export function generatePhpSecurityHtaccess() {
|
|
263
|
+
return `# Mozole Studio - Shared Hosting Security
|
|
264
|
+
# Blocks direct access to sensitive PHP files and enforces strict methods
|
|
265
|
+
|
|
266
|
+
<FilesMatch "^config\\.php$">
|
|
267
|
+
Require all denied
|
|
268
|
+
</FilesMatch>
|
|
269
|
+
|
|
270
|
+
Options -Indexes -MultiViews
|
|
271
|
+
|
|
272
|
+
<LimitExcept GET POST OPTIONS>
|
|
273
|
+
Require all denied
|
|
274
|
+
</LimitExcept>
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
export function generateNodeBackend() {
|
|
278
|
+
return `import { createServer } from "node:http";
|
|
279
|
+
|
|
280
|
+
const PORT = Number(process.env.PORT || 3001);
|
|
281
|
+
|
|
282
|
+
const server = createServer(async (req, res) => {
|
|
283
|
+
res.setHeader("Content-Type", "application/json");
|
|
284
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
285
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
|
286
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
287
|
+
|
|
288
|
+
if (req.method === "OPTIONS") {
|
|
289
|
+
res.writeHead(204);
|
|
290
|
+
res.end();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (req.method === "GET" && req.url === "/api/health") {
|
|
295
|
+
res.writeHead(200);
|
|
296
|
+
res.end(JSON.stringify({ status: "operational", runtime: "node" }));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (req.method === "POST" && req.url === "/api/contact") {
|
|
301
|
+
let body = "";
|
|
302
|
+
req.on("data", (chunk) => (body += chunk));
|
|
303
|
+
req.on("end", () => {
|
|
304
|
+
try {
|
|
305
|
+
const data = JSON.parse(body);
|
|
306
|
+
if (data._honey) {
|
|
307
|
+
res.writeHead(200);
|
|
308
|
+
res.end(JSON.stringify({ success: true }));
|
|
309
|
+
return;
|
|
182
310
|
}
|
|
183
|
-
|
|
184
|
-
|
|
311
|
+
res.writeHead(200);
|
|
312
|
+
res.end(JSON.stringify({ success: true, message: "Message received." }));
|
|
313
|
+
} catch {
|
|
314
|
+
res.writeHead(400);
|
|
315
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
res.writeHead(404);
|
|
322
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
server.listen(PORT, () => {
|
|
326
|
+
console.log(\`Backend API running on http://localhost:\${PORT}\`);
|
|
327
|
+
});
|
|
328
|
+
`;
|
|
185
329
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# Fallback to prerendered / client index for SPA routes
|
|
200
|
-
RewriteRule ^index\\.html$ - [L]
|
|
201
|
-
RewriteRule . /__spa-fallback.html [L]
|
|
202
|
-
</IfModule>
|
|
203
|
-
`,
|
|
204
|
-
"docs/backend-php.md": `# PHP Backend Architecture
|
|
205
|
-
|
|
206
|
-
This project combines a **static-first React + Vite frontend** with a lightweight, native **PHP backend API**.
|
|
207
|
-
|
|
208
|
-
## How It Works
|
|
209
|
-
|
|
210
|
-
### Local Development
|
|
211
|
-
- **Frontend (React + Vite)**: Runs via \`npm run dev\` (typically at \`http://127.0.0.1:5173\`).
|
|
212
|
-
- **Backend (PHP API)**: Runs via \`npm run dev:api\` (\`php -S 127.0.0.1:8000 -t api api/index.php\`).
|
|
213
|
-
- **Proxy**: Vite's \`server.proxy\` automatically forwards all \`/api/*\` requests from the browser directly to the PHP dev server. Your React code calls \`fetch("/api/health")\` without dealing with CORS or port mismatches.
|
|
214
|
-
|
|
215
|
-
### Production Deployment (Apache / LiteSpeed / Nginx)
|
|
216
|
-
- Requires PHP 8.0 or newer. Run \`npm run build:deploy\`, then upload the **contents** of \`deploy/\`, including the hidden \`.htaccess\` file. The deployment contains static HTML, assets, and \`api/\` together; no Node.js runtime is required.
|
|
217
|
-
- **Apache / LiteSpeed**: The provided \`.htaccess\` handles routing:
|
|
218
|
-
- All \`/api/*\` calls are routed directly to \`api/index.php\`.
|
|
219
|
-
- All static HTML, CSS, JS, and image assets are served directly by the web server at maximum speed.
|
|
220
|
-
- **Nginx**: Configure your PHP-FPM socket and route \`/api/\` to \`api/index.php\` with \`SCRIPT_FILENAME\` set explicitly. Use \`try_files $uri $uri/ /__spa-fallback.html\` for the frontend. Nginx ignores \`.htaccess\`; server configuration is required.\n- The supplied rewrite rules assume deployment at the domain root. Subdirectory hosting needs matching frontend basename and server rewrite configuration.\n- Environment values use actual process environment variables (\`getenv\`); no .env loader is installed. Production and debug-off are the defaults.
|
|
221
|
-
`,
|
|
222
|
-
};
|
|
330
|
+
export async function scaffoldBackend(projectRoot, type = "php") {
|
|
331
|
+
if (type === "php") {
|
|
332
|
+
const apiDir = path.join(projectRoot, "api");
|
|
333
|
+
await atomicWrite(path.join(apiDir, "config.php"), generatePhpConfig());
|
|
334
|
+
await atomicWrite(path.join(apiDir, "mailer.php"), generatePhpMailer());
|
|
335
|
+
await atomicWrite(path.join(apiDir, "index.php"), generatePhpIndex());
|
|
336
|
+
await atomicWrite(path.join(apiDir, ".htaccess"), generatePhpSecurityHtaccess());
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const serverDir = path.join(projectRoot, "server");
|
|
340
|
+
await atomicWrite(path.join(serverDir, "index.mjs"), generateNodeBackend());
|
|
341
|
+
}
|
|
223
342
|
}
|
|
224
343
|
//# sourceMappingURL=backend.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/scaffold/backend.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/scaffold/backend.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,UAAU,iBAAiB;IAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4LR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,2BAA2B;IACzC,OAAO;;;;;;;;;;;;CAYR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDR,CAAC;AACF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,WAAmB,EACnB,OAAuB,KAAK;IAE5B,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QAC7C,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;QACtE,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,2BAA2B,EAAE,CAAC,CAAC;IACnF,CAAC;SAAM,CAAC;QACN,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QACnD,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,mBAAmB,EAAE,CAAC,CAAC;IAC9E,CAAC;AACH,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export declare
|
|
3
|
-
export declare
|
|
1
|
+
export declare function generateDesignReadme(): string;
|
|
2
|
+
export declare function generateDesignMd(projectName: string): string;
|
|
3
|
+
export declare function generateBriefMd(projectName: string): string;
|
|
4
|
+
export declare function scaffoldDesign(projectRoot: string, projectName: string): Promise<void>;
|
|
4
5
|
//# sourceMappingURL=design.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"design.d.ts","sourceRoot":"","sources":["../../src/scaffold/design.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"design.d.ts","sourceRoot":"","sources":["../../src/scaffold/design.ts"],"names":[],"mappings":"AAGA,wBAAgB,oBAAoB,IAAI,MAAM,CAU7C;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAS5D;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAQ3D;AAED,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ5F"}
|