create-prisma-php-app 5.1.0-alpha.3 → 5.1.0-alpha.30
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/README.md +23 -2
- package/dist/.github/copilot-instructions.md +80 -33
- package/dist/AGENTS.md +59 -25
- package/dist/bootstrap.php +207 -187
- package/dist/index.js +2 -2
- package/dist/phpunit.xml +25 -0
- package/dist/postcss.config.js +4 -2
- package/dist/public/.htaccess +1 -1
- package/dist/public/js/pp-reactive-v2.min.js +1 -0
- package/dist/settings/bs-config.ts +44 -1
- package/dist/settings/run-postcss.ts +205 -0
- package/dist/settings/run-tests.ts +35 -0
- package/dist/src/Lib/Auth/Auth.php +12 -25
- package/dist/src/Lib/MCP/mcp-server.php +2 -3
- package/dist/src/Lib/Websocket/ConnectionManager.php +500 -47
- package/dist/src/Lib/Websocket/Socket.php +170 -0
- package/dist/src/Lib/Websocket/SocketPool.php +50 -0
- package/dist/src/Lib/Websocket/SocketRegistry.php +88 -0
- package/dist/src/Lib/Websocket/sockets.php +50 -0
- package/dist/src/Lib/Websocket/websocket-server.php +10 -3
- package/dist/src/app/globals.css +3 -1
- package/dist/src/app/layout.php +1 -1
- package/dist/tests/AuthTest.php +59 -0
- package/dist/tests/ConnectionManagerTest.php +277 -0
- package/dist/tests/CsrfTest.php +119 -0
- package/dist/tests/DeferComponentRootsTest.php +147 -0
- package/dist/tests/FeaturesTest.php +41 -0
- package/dist/tests/README.md +119 -0
- package/dist/tests/RpcWireContractTest.php +101 -0
- package/dist/tests/SocketPoolTest.php +69 -0
- package/dist/tests/SocketRegistryTest.php +77 -0
- package/dist/tests/SocketTest.php +124 -0
- package/dist/tests/SocketsRegistrationTest.php +40 -0
- package/dist/tests/Support/FakeConnection.php +62 -0
- package/dist/tests/Support/Features.php +38 -0
- package/dist/tests/Support/RequiresFeature.php +26 -0
- package/dist/tests/bootstrap.php +44 -0
- package/dist/ts/main.ts +5 -8
- package/dist/ts/tailwind-merge.ts +13 -0
- package/package.json +4 -4
- package/dist/public/js/pp-reactive-v2.js +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, readFileSync, rm, rmSync, writeFile } from "node:fs";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
const mode: "watch" | "build" = process.argv[2] === "watch" ? "watch" : "build";
|
|
8
|
+
const watcherPidFile = join(process.cwd(), ".pp", "postcss-watch.pid");
|
|
9
|
+
const mkdirAsync = promisify(mkdir);
|
|
10
|
+
const readFileAsync = promisify(readFile);
|
|
11
|
+
const rmAsync = promisify(rm);
|
|
12
|
+
const writeFileAsync = promisify(writeFile);
|
|
13
|
+
|
|
14
|
+
const args: string[] = [
|
|
15
|
+
"--max-old-space-size=6144",
|
|
16
|
+
"./node_modules/postcss-cli/index.js",
|
|
17
|
+
"src/app/globals.css",
|
|
18
|
+
"-o",
|
|
19
|
+
"public/css/styles.css",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
if (mode === "watch") {
|
|
23
|
+
args.push("--watch");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isValidPid(value: string): boolean {
|
|
27
|
+
return /^\d+$/.test(value.trim());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isProcessRunning(pid: number): boolean {
|
|
31
|
+
try {
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function killWindowsProcessTree(pid: number): Promise<void> {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const killer = execFile("taskkill", ["/F", "/T", "/PID", String(pid)], () =>
|
|
42
|
+
resolve(),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
killer.on("error", () => resolve());
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function killProcessTree(pid: number): Promise<void> {
|
|
50
|
+
if (process.platform === "win32") {
|
|
51
|
+
await killWindowsProcessTree(pid);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
process.kill(pid, "SIGTERM");
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function ensurePidDirectory(): Promise<void> {
|
|
63
|
+
await mkdirAsync(dirname(watcherPidFile), { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function clearPidFile(): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
const storedPid = (await readFileAsync(watcherPidFile, "utf8")).trim();
|
|
69
|
+
if (storedPid === String(process.pid)) {
|
|
70
|
+
await rmAsync(watcherPidFile, { force: true });
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clearPidFileSync(): void {
|
|
76
|
+
try {
|
|
77
|
+
const storedPid = readFileSync(watcherPidFile, "utf8").trim();
|
|
78
|
+
if (storedPid === String(process.pid)) {
|
|
79
|
+
rmSync(watcherPidFile, { force: true });
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function cleanupStaleWatcher(): Promise<void> {
|
|
85
|
+
if (mode !== "watch") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await ensurePidDirectory();
|
|
90
|
+
|
|
91
|
+
let storedPid = "";
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
storedPid = (await readFileAsync(watcherPidFile, "utf8")).trim();
|
|
95
|
+
} catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isValidPid(storedPid)) {
|
|
100
|
+
await rmAsync(watcherPidFile, { force: true });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const pid = Number(storedPid);
|
|
105
|
+
|
|
106
|
+
if (pid === process.pid) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!isProcessRunning(pid)) {
|
|
111
|
+
await rmAsync(watcherPidFile, { force: true });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.warn(
|
|
116
|
+
`[tailwind] Found stale PostCSS watcher (PID ${pid}), stopping it before restart.`,
|
|
117
|
+
);
|
|
118
|
+
await killProcessTree(pid);
|
|
119
|
+
await rmAsync(watcherPidFile, { force: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function writePidFile(): Promise<void> {
|
|
123
|
+
if (mode !== "watch") {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await ensurePidDirectory();
|
|
128
|
+
await writeFileAsync(watcherPidFile, `${process.pid}\n`, "utf8");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await cleanupStaleWatcher();
|
|
132
|
+
await writePidFile();
|
|
133
|
+
|
|
134
|
+
const child = spawn(process.execPath, args, {
|
|
135
|
+
stdio: "inherit",
|
|
136
|
+
windowsHide: true,
|
|
137
|
+
env: {
|
|
138
|
+
...process.env,
|
|
139
|
+
PP_POSTCSS_MODE: mode,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let shuttingDown = false;
|
|
144
|
+
|
|
145
|
+
child.on("error", (error) => {
|
|
146
|
+
console.error(error);
|
|
147
|
+
void shutdown(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
async function shutdown(exitCode: number): Promise<void> {
|
|
151
|
+
if (shuttingDown) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
shuttingDown = true;
|
|
156
|
+
|
|
157
|
+
if (child.pid && child.exitCode === null && !child.killed) {
|
|
158
|
+
await killProcessTree(child.pid);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await clearPidFile();
|
|
162
|
+
process.exit(exitCode);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
child.on("exit", async (code, signal) => {
|
|
166
|
+
await clearPidFile();
|
|
167
|
+
|
|
168
|
+
if (shuttingDown) {
|
|
169
|
+
process.exit(code ?? 0);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (signal) {
|
|
174
|
+
try {
|
|
175
|
+
process.kill(process.pid, signal);
|
|
176
|
+
} catch {
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
process.exit(code ?? 0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
process.once("SIGINT", () => {
|
|
186
|
+
void shutdown(0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
process.once("SIGTERM", () => {
|
|
190
|
+
void shutdown(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
process.once("uncaughtException", (error) => {
|
|
194
|
+
console.error(error);
|
|
195
|
+
void shutdown(1);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
process.once("unhandledRejection", (reason) => {
|
|
199
|
+
console.error(reason);
|
|
200
|
+
void shutdown(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
process.once("exit", () => {
|
|
204
|
+
clearPidFileSync();
|
|
205
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import prismaPhpConfigJson from "../prisma-php.json";
|
|
5
|
+
import { getFileMeta } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
const { __dirname } = getFileMeta();
|
|
8
|
+
const projectRoot = join(__dirname, "..");
|
|
9
|
+
|
|
10
|
+
// The PHP binary the project is configured for (prisma-php.json), falling
|
|
11
|
+
// back to whatever `php` resolves to on PATH.
|
|
12
|
+
const phpExe =
|
|
13
|
+
prismaPhpConfigJson.phpRootPathExe && existsSync(prismaPhpConfigJson.phpRootPathExe)
|
|
14
|
+
? prismaPhpConfigJson.phpRootPathExe
|
|
15
|
+
: "php";
|
|
16
|
+
|
|
17
|
+
const phpunit = join(projectRoot, "vendor", "phpunit", "phpunit", "phpunit");
|
|
18
|
+
|
|
19
|
+
if (!existsSync(phpunit)) {
|
|
20
|
+
console.error(
|
|
21
|
+
"PHPUnit is not installed. Run: composer install (phpunit/phpunit is a dev dependency).",
|
|
22
|
+
);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Everything after `npm run test --` is handed to PHPUnit, so
|
|
27
|
+
// `npm run test -- --filter CsrfTest` narrows the run.
|
|
28
|
+
const args = [phpunit, "--configuration", join(projectRoot, "phpunit.xml"), ...process.argv.slice(2)];
|
|
29
|
+
|
|
30
|
+
const result = spawnSync(phpExe, args, {
|
|
31
|
+
cwd: projectRoot,
|
|
32
|
+
stdio: "inherit",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
process.exit(result.status ?? 1);
|
|
@@ -16,6 +16,7 @@ use Exception;
|
|
|
16
16
|
use InvalidArgumentException;
|
|
17
17
|
use ArrayObject;
|
|
18
18
|
use PP\Env;
|
|
19
|
+
use PP\Security\Csrf;
|
|
19
20
|
|
|
20
21
|
class Auth
|
|
21
22
|
{
|
|
@@ -166,11 +167,15 @@ class Auth
|
|
|
166
167
|
/**
|
|
167
168
|
* Verifies the JWT token and returns the decoded payload if the token is valid.
|
|
168
169
|
* If the token is invalid or expired, null is returned.
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
170
|
+
*
|
|
171
|
+
* The payload is whatever `signIn(...)` stored: a scalar such as a role
|
|
172
|
+
* string, or an object for structured user data — so the return type is
|
|
173
|
+
* `mixed`, with `null` reserved for an invalid or expired token.
|
|
174
|
+
*
|
|
175
|
+
* @param string|null $jwt The JWT token to verify.
|
|
176
|
+
* @return mixed The decoded payload, or null if invalid or expired.
|
|
172
177
|
*/
|
|
173
|
-
public function verifyToken(?string $jwt):
|
|
178
|
+
public function verifyToken(?string $jwt): mixed
|
|
174
179
|
{
|
|
175
180
|
try {
|
|
176
181
|
if (!$jwt) return null;
|
|
@@ -260,27 +265,9 @@ class Auth
|
|
|
260
265
|
|
|
261
266
|
public function rotateCsrfToken(): void
|
|
262
267
|
{
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
throw new InvalidArgumentException('FUNCTION_CALL_SECRET is required for CSRF protection.');
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
$nonce = bin2hex(random_bytes(16));
|
|
270
|
-
$signature = hash_hmac('sha256', $nonce, $secret);
|
|
271
|
-
$token = $nonce . '.' . $signature;
|
|
272
|
-
|
|
273
|
-
if (!headers_sent()) {
|
|
274
|
-
setcookie('prisma_php_csrf', $token, [
|
|
275
|
-
'expires' => time() + 3600, // 1 hour validity
|
|
276
|
-
'path' => '/',
|
|
277
|
-
'secure' => $this->isHttpsRequest(),
|
|
278
|
-
'httponly' => false, // Must be FALSE so client JS can read it
|
|
279
|
-
'samesite' => 'Lax',
|
|
280
|
-
]);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
$_COOKIE['prisma_php_csrf'] = $token;
|
|
268
|
+
// The PulsePoint runtime reads the `pp_csrf` cookie family; issuing
|
|
269
|
+
// and naming live in one place so every writer stays aligned.
|
|
270
|
+
Csrf::rotate();
|
|
284
271
|
}
|
|
285
272
|
|
|
286
273
|
/**
|
|
@@ -68,9 +68,8 @@ try {
|
|
|
68
68
|
$port,
|
|
69
69
|
$prefix,
|
|
70
70
|
null, // sslContext
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// , false // (optional) stateless
|
|
71
|
+
$enableJson, // enableJsonResponse
|
|
72
|
+
false // stateless
|
|
74
73
|
);
|
|
75
74
|
echo $color("✓ Listening on {$base}", '32') . PHP_EOL;
|
|
76
75
|
$server->listen($transport);
|