create-prisma-php-app 5.0.0-alpha.8 → 5.0.1

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.
Files changed (60) hide show
  1. package/README.md +23 -2
  2. package/dist/.github/copilot-instructions.md +191 -0
  3. package/dist/AGENTS.md +588 -193
  4. package/dist/bootstrap.php +426 -213
  5. package/dist/index.js +2 -2
  6. package/dist/phpunit.xml +25 -0
  7. package/dist/postcss.config.js +4 -2
  8. package/dist/prisma-php.js +2 -2
  9. package/dist/public/.htaccess +1 -1
  10. package/dist/public/js/main.js +13 -1
  11. package/dist/public/js/pp-reactive-v2.min.js +1 -0
  12. package/dist/settings/auto-swagger-docs.ts +10 -10
  13. package/dist/settings/bs-config.ts +47 -27
  14. package/dist/settings/build.ts +2 -32
  15. package/dist/settings/component-map.ts +476 -0
  16. package/dist/settings/files-list.ts +2 -2
  17. package/dist/settings/project-name.ts +3 -6
  18. package/dist/settings/restart-mcp.ts +2 -2
  19. package/dist/settings/restart-websocket.ts +2 -2
  20. package/dist/settings/run-postcss.ts +205 -0
  21. package/dist/settings/run-tests.ts +35 -0
  22. package/dist/settings/swagger-config.ts +3 -3
  23. package/dist/settings/utils.ts +3 -3
  24. package/dist/src/Lib/Auth/Auth.php +62 -34
  25. package/dist/src/Lib/MCP/mcp-server.php +2 -3
  26. package/dist/src/Lib/Middleware/CorsMiddleware.php +72 -0
  27. package/dist/src/Lib/Websocket/ConnectionManager.php +460 -7
  28. package/dist/src/Lib/Websocket/Socket.php +170 -0
  29. package/dist/src/Lib/Websocket/SocketPool.php +50 -0
  30. package/dist/src/Lib/Websocket/SocketRegistry.php +88 -0
  31. package/dist/src/Lib/Websocket/sockets.php +50 -0
  32. package/dist/src/Lib/Websocket/websocket-server.php +10 -3
  33. package/dist/src/app/globals.css +3 -1
  34. package/dist/src/app/layout.php +1 -1
  35. package/dist/tests/AuthTest.php +59 -0
  36. package/dist/tests/ConnectionManagerTest.php +277 -0
  37. package/dist/tests/CsrfTest.php +119 -0
  38. package/dist/tests/DeferComponentRootsTest.php +147 -0
  39. package/dist/tests/FeaturesTest.php +41 -0
  40. package/dist/tests/README.md +119 -0
  41. package/dist/tests/RpcWireContractTest.php +101 -0
  42. package/dist/tests/SocketPoolTest.php +69 -0
  43. package/dist/tests/SocketRegistryTest.php +77 -0
  44. package/dist/tests/SocketTest.php +124 -0
  45. package/dist/tests/SocketsRegistrationTest.php +40 -0
  46. package/dist/tests/Support/FakeConnection.php +62 -0
  47. package/dist/tests/Support/Features.php +38 -0
  48. package/dist/tests/Support/RequiresFeature.php +26 -0
  49. package/dist/tests/bootstrap.php +44 -0
  50. package/dist/ts/main.ts +21 -3
  51. package/dist/ts/tailwind-merge.ts +13 -0
  52. package/package.json +4 -4
  53. package/dist/README.md +0 -213
  54. package/dist/public/js/pp-reactive-v2.js +0 -1
  55. package/dist/settings/bs-config.json +0 -6
  56. package/dist/settings/class-imports.ts +0 -165
  57. package/dist/settings/class-log.ts +0 -244
  58. package/dist/settings/component-import-checker.ts +0 -90
  59. package/dist/settings/files-list.json +0 -1
  60. package/dist/settings/prisma-schema-config.json +0 -16
@@ -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);
@@ -11,7 +11,7 @@ const { __dirname } = getFileMeta();
11
11
  export async function swaggerConfig(): Promise<void> {
12
12
  const outputPath = join(
13
13
  __dirname,
14
- "../src/app/swagger-docs/apis/pp-swagger.json"
14
+ "../src/app/swagger-docs/apis/pp-swagger.json",
15
15
  );
16
16
 
17
17
  const options = {
@@ -56,8 +56,8 @@ export async function swaggerConfig(): Promise<void> {
56
56
  writeFileSync(outputPath, swaggerSpec, "utf-8");
57
57
  console.log(
58
58
  `Swagger JSON has been generated and saved to ${chalk.blue(
59
- "src/app/swagger-docs/apis/pp-swagger.json"
60
- )}`
59
+ "src/app/swagger-docs/apis/pp-swagger.json",
60
+ )}`,
61
61
  );
62
62
  } catch (error) {
63
63
  console.error("Error saving Swagger JSON:", error);
@@ -40,7 +40,7 @@ export function createSrcWatcher(
40
40
  logPrefix?: string;
41
41
  usePolling?: boolean;
42
42
  interval?: number;
43
- }
43
+ },
44
44
  ): FSWatcher {
45
45
  const {
46
46
  exts,
@@ -87,7 +87,7 @@ export class DebouncedWorker {
87
87
  constructor(
88
88
  private work: () => Promise<void> | void,
89
89
  private debounceMs = 350,
90
- private name = "worker"
90
+ private name = "worker",
91
91
  ) {}
92
92
 
93
93
  schedule(reason?: string) {
@@ -172,7 +172,7 @@ export function createRestartableProcess(spec: {
172
172
  function killOnWindows(pid: number): Promise<void> {
173
173
  return new Promise((resolve) => {
174
174
  const cp = execFile("taskkill", ["/F", "/T", "/PID", String(pid)], () =>
175
- resolve()
175
+ resolve(),
176
176
  );
177
177
  cp.on("error", () => resolve());
178
178
  });
@@ -16,12 +16,14 @@ 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
  {
22
23
  public const PAYLOAD_NAME = 'payload_name_8639D';
23
24
  public const ROLE_NAME = 'role';
24
25
  public const PAYLOAD_SESSION_KEY = 'payload_session_key_2183A';
26
+ private const OAUTH_STATE_SESSION_KEY = 'oauth_state_61e4a';
25
27
 
26
28
  public static string $cookieName = '';
27
29
 
@@ -32,7 +34,10 @@ class Auth
32
34
 
33
35
  private function __construct()
34
36
  {
35
- $this->secretKey = Env::string('AUTH_SECRET', 'CD24eEv4qbsC5LOzqeaWbcr58mBMSvA4Mkii8GjRiHkt');
37
+ $this->secretKey = Env::string('AUTH_SECRET', '');
38
+ if ($this->secretKey === '') {
39
+ throw new InvalidArgumentException('AUTH_SECRET is required for authentication.');
40
+ }
36
41
  self::$cookieName = self::getCookieName();
37
42
  }
38
43
 
@@ -162,11 +167,15 @@ class Auth
162
167
  /**
163
168
  * Verifies the JWT token and returns the decoded payload if the token is valid.
164
169
  * If the token is invalid or expired, null is returned.
165
- *
166
- * @param string $jwt The JWT token to verify.
167
- * @return object|null Returns the decoded payload if the token is valid, or null if invalid or expired.
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.
168
177
  */
169
- public function verifyToken(?string $jwt): ?object
178
+ public function verifyToken(?string $jwt): mixed
170
179
  {
171
180
  try {
172
181
  if (!$jwt) return null;
@@ -256,27 +265,9 @@ class Auth
256
265
 
257
266
  public function rotateCsrfToken(): void
258
267
  {
259
- $secret = Env::string('FUNCTION_CALL_SECRET', '');
260
-
261
- if (empty($secret)) {
262
- return;
263
- }
264
-
265
- $nonce = bin2hex(random_bytes(16));
266
- $signature = hash_hmac('sha256', $nonce, $secret);
267
- $token = $nonce . '.' . $signature;
268
-
269
- if (!headers_sent()) {
270
- setcookie('prisma_php_csrf', $token, [
271
- 'expires' => time() + 3600, // 1 hour validity
272
- 'path' => '/',
273
- 'secure' => $this->isHttpsRequest(),
274
- 'httponly' => false, // Must be FALSE so client JS can read it
275
- 'samesite' => 'Lax',
276
- ]);
277
- }
278
-
279
- $_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();
280
271
  }
281
272
 
282
273
  /**
@@ -386,26 +377,34 @@ class Auth
386
377
  {
387
378
  $dynamicRouteParams = Request::$dynamicParams[self::PPAUTH] ?? [];
388
379
 
389
- if (Request::$isGet && in_array('signin', $dynamicRouteParams)) {
380
+ if (Request::$isGet && in_array('signin', $dynamicRouteParams, true)) {
390
381
  foreach ($providers as $provider) {
391
- if ($provider instanceof GithubProvider && in_array('github', $dynamicRouteParams)) {
392
- $githubAuthUrl = "https://github.com/login/oauth/authorize?scope=user:email%20read:user&client_id={$provider->clientId}";
382
+ if ($provider instanceof GithubProvider && in_array('github', $dynamicRouteParams, true)) {
383
+ $state = $this->createOAuthState('github');
384
+ $githubAuthUrl = "https://github.com/login/oauth/authorize?scope=user:email%20read:user&client_id={$provider->clientId}&state=" . urlencode($state);
393
385
  Request::redirect($githubAuthUrl);
394
- } elseif ($provider instanceof GoogleProvider && in_array('google', $dynamicRouteParams)) {
386
+ } elseif ($provider instanceof GoogleProvider && in_array('google', $dynamicRouteParams, true)) {
387
+ $state = $this->createOAuthState('google');
395
388
  $googleAuthUrl = "https://accounts.google.com/o/oauth2/v2/auth?"
396
389
  . "scope=" . urlencode('email profile') . "&"
397
390
  . "response_type=code&"
398
391
  . "client_id=" . urlencode($provider->clientId) . "&"
399
- . "redirect_uri=" . urlencode($provider->redirectUri);
392
+ . "redirect_uri=" . urlencode($provider->redirectUri) . "&"
393
+ . "state=" . urlencode($state);
400
394
  Request::redirect($googleAuthUrl);
401
395
  }
402
396
  }
403
397
  }
404
398
 
405
399
  $authCode = Validator::string($_GET['code'] ?? '');
400
+ $authState = Validator::string($_GET['state'] ?? '', false);
401
+
402
+ if (Request::$isGet && in_array('callback', $dynamicRouteParams, true) && $authCode !== '') {
403
+ if (in_array('github', $dynamicRouteParams, true)) {
404
+ if (!$this->consumeOAuthState('github', $authState)) {
405
+ exit("Error occurred. Please try again.");
406
+ }
406
407
 
407
- if (Request::$isGet && in_array('callback', $dynamicRouteParams) && isset($authCode)) {
408
- if (in_array('github', $dynamicRouteParams)) {
409
408
  $provider = $this->findProvider($providers, GithubProvider::class);
410
409
 
411
410
  if (!$provider) {
@@ -413,7 +412,11 @@ class Auth
413
412
  }
414
413
 
415
414
  return $this->githubProvider($provider, $authCode);
416
- } elseif (in_array('google', $dynamicRouteParams)) {
415
+ } elseif (in_array('google', $dynamicRouteParams, true)) {
416
+ if (!$this->consumeOAuthState('google', $authState)) {
417
+ exit("Error occurred. Please try again.");
418
+ }
419
+
417
420
  $provider = $this->findProvider($providers, GoogleProvider::class);
418
421
 
419
422
  if (!$provider) {
@@ -554,6 +557,31 @@ class Auth
554
557
  }
555
558
  }
556
559
 
560
+ private function createOAuthState(string $provider): string
561
+ {
562
+ $state = bin2hex(random_bytes(16));
563
+ $_SESSION[self::OAUTH_STATE_SESSION_KEY][$provider] = $state;
564
+
565
+ return $state;
566
+ }
567
+
568
+ private function consumeOAuthState(string $provider, string $state): bool
569
+ {
570
+ $expectedState = $_SESSION[self::OAUTH_STATE_SESSION_KEY][$provider] ?? '';
571
+
572
+ if (!is_string($expectedState) || $expectedState === '' || $state === '' || !hash_equals($expectedState, $state)) {
573
+ return false;
574
+ }
575
+
576
+ unset($_SESSION[self::OAUTH_STATE_SESSION_KEY][$provider]);
577
+
578
+ if (empty($_SESSION[self::OAUTH_STATE_SESSION_KEY])) {
579
+ unset($_SESSION[self::OAUTH_STATE_SESSION_KEY]);
580
+ }
581
+
582
+ return true;
583
+ }
584
+
557
585
  private static function getCookieName(): string
558
586
  {
559
587
  $authCookieName = Env::string('AUTH_COOKIE_NAME', 'auth_cookie_name_d36e5');
@@ -68,9 +68,8 @@ try {
68
68
  $port,
69
69
  $prefix,
70
70
  null, // sslContext
71
- true, // logger
72
- $enableJson // enableJsonResponse
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);
@@ -16,6 +16,11 @@ final class CorsMiddleware
16
16
  }
17
17
 
18
18
  $cfg = self::buildConfig($overrides);
19
+ $cfg = self::allowImplicitLoopbackOrigin($origin, $cfg);
20
+
21
+ if ($cfg['allowCredentials'] && self::listHasWildcard($cfg['allowedOrigins'])) {
22
+ return;
23
+ }
19
24
 
20
25
  if (!self::isAllowedOrigin($origin, $cfg['allowedOrigins'])) {
21
26
  return;
@@ -82,6 +87,20 @@ final class CorsMiddleware
82
87
  return $cfg;
83
88
  }
84
89
 
90
+ private static function allowImplicitLoopbackOrigin(string $origin, array $cfg): array
91
+ {
92
+ if ($cfg['allowedOrigins'] !== []) {
93
+ return $cfg;
94
+ }
95
+
96
+ if (!self::isImplicitLoopbackOrigin($origin)) {
97
+ return $cfg;
98
+ }
99
+
100
+ $cfg['allowedOrigins'] = [self::normalize($origin)];
101
+ return $cfg;
102
+ }
103
+
85
104
  private static function parseList(string $raw): array
86
105
  {
87
106
  $raw = trim($raw);
@@ -101,6 +120,59 @@ final class CorsMiddleware
101
120
  return rtrim($origin, '/');
102
121
  }
103
122
 
123
+ private static function isImplicitLoopbackOrigin(string $origin): bool
124
+ {
125
+ $originParts = parse_url(self::normalize($origin));
126
+ if (!is_array($originParts)) {
127
+ return false;
128
+ }
129
+
130
+ $originHost = strtolower((string) ($originParts['host'] ?? ''));
131
+ $originScheme = strtolower((string) ($originParts['scheme'] ?? ''));
132
+ $requestHost = self::getRequestHost();
133
+ $requestScheme = self::isHttpsRequest() ? 'https' : 'http';
134
+
135
+ if (
136
+ $originHost === '' ||
137
+ $requestHost === '' ||
138
+ $originHost !== $requestHost ||
139
+ $originScheme !== $requestScheme
140
+ ) {
141
+ return false;
142
+ }
143
+
144
+ return self::isLoopbackHost($originHost);
145
+ }
146
+
147
+ private static function getRequestHost(): string
148
+ {
149
+ $host = (string) ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '');
150
+ if ($host === '') {
151
+ return '';
152
+ }
153
+
154
+ $parts = parse_url('http://' . $host);
155
+ if (is_array($parts) && isset($parts['host'])) {
156
+ return strtolower((string) $parts['host']);
157
+ }
158
+
159
+ return strtolower(explode(':', $host, 2)[0]);
160
+ }
161
+
162
+ private static function isLoopbackHost(string $host): bool
163
+ {
164
+ return in_array(trim(strtolower($host), '[]'), ['localhost', '127.0.0.1', '::1'], true);
165
+ }
166
+
167
+ private static function isHttpsRequest(): bool
168
+ {
169
+ return (
170
+ (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
171
+ (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ||
172
+ (isset($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443)
173
+ );
174
+ }
175
+
104
176
  private static function isAllowedOrigin(string $origin, array $list): bool
105
177
  {
106
178
  $o = self::normalize($origin);