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,170 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\Websocket;
|
|
6
|
+
|
|
7
|
+
use Ratchet\ConnectionInterface;
|
|
8
|
+
use InvalidArgumentException;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* One open connection, as a socket handler holds it.
|
|
12
|
+
*
|
|
13
|
+
* The server half of `pp.socket(name, args, handlers)`. A handler receives
|
|
14
|
+
* this and the argument payload once the first frame arrives, wires its
|
|
15
|
+
* callbacks, and talks JSON frames both ways:
|
|
16
|
+
*
|
|
17
|
+
* ```php
|
|
18
|
+
* SocketRegistry::register('echo', function (Socket $socket, array $args): void {
|
|
19
|
+
* $socket->onMessage(fn (mixed $value) => $socket->send($value));
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The frame shape `{"error": "..."}` (that key alone) is reserved by the
|
|
24
|
+
* wire: it is how the server reports a failure inside an open connection,
|
|
25
|
+
* and the client runtime routes it to `onError` followed by the close.
|
|
26
|
+
* `send()` therefore refuses to emit it as an ordinary message.
|
|
27
|
+
*/
|
|
28
|
+
final class Socket
|
|
29
|
+
{
|
|
30
|
+
private bool $closed = false;
|
|
31
|
+
|
|
32
|
+
/** @var callable|null fn(mixed $value): void */
|
|
33
|
+
private $messageListener = null;
|
|
34
|
+
|
|
35
|
+
/** @var callable|null fn(): void */
|
|
36
|
+
private $closeListener = null;
|
|
37
|
+
|
|
38
|
+
public function __construct(
|
|
39
|
+
private readonly ConnectionInterface $conn,
|
|
40
|
+
private readonly string $name,
|
|
41
|
+
private readonly mixed $authPayload = null,
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
public function name(): string
|
|
45
|
+
{
|
|
46
|
+
return $this->name;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The verified auth payload of the connection, or null for a guest.
|
|
51
|
+
*/
|
|
52
|
+
public function payload(): mixed
|
|
53
|
+
{
|
|
54
|
+
return $this->authPayload;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public function isOpen(): bool
|
|
58
|
+
{
|
|
59
|
+
return !$this->closed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Send one JSON value. False means nobody is listening any more — the
|
|
64
|
+
* browser navigated away or closed the tab. That is the signal to stop,
|
|
65
|
+
* not an error to report.
|
|
66
|
+
*/
|
|
67
|
+
public function send(mixed $value): bool
|
|
68
|
+
{
|
|
69
|
+
if ($this->closed) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (self::isReservedErrorShape($value)) {
|
|
74
|
+
throw new InvalidArgumentException(
|
|
75
|
+
'The frame shape {"error": "..."} is reserved for failures. '
|
|
76
|
+
. 'Wrap the value or rename the key.'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$frame = json_encode($value, JSON_UNESCAPED_UNICODE);
|
|
81
|
+
if ($frame === false) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
$this->conn->send($frame);
|
|
86
|
+
|
|
87
|
+
return !$this->closed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Called with each decoded JSON value the browser sends.
|
|
92
|
+
*/
|
|
93
|
+
public function onMessage(callable $listener): void
|
|
94
|
+
{
|
|
95
|
+
$this->messageListener = $listener;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Called once, when the connection closes for any reason.
|
|
100
|
+
*/
|
|
101
|
+
public function onClose(callable $listener): void
|
|
102
|
+
{
|
|
103
|
+
$this->closeListener = $listener;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Say goodbye mid-conversation. Close code 1000, a normal closure.
|
|
108
|
+
*/
|
|
109
|
+
public function close(): void
|
|
110
|
+
{
|
|
111
|
+
if ($this->closed) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$this->conn->close(1000);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The error frame, then the close. The conversation is over.
|
|
120
|
+
* Framework/handler use for failures meant to reach `onError`.
|
|
121
|
+
*/
|
|
122
|
+
public function error(string $message): void
|
|
123
|
+
{
|
|
124
|
+
if ($this->closed) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
$this->conn->send(json_encode(['error' => $message], JSON_UNESCAPED_UNICODE));
|
|
129
|
+
$this->conn->close(1008);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @internal Called by the connection manager on each inbound frame. */
|
|
133
|
+
public function handleMessage(mixed $value): void
|
|
134
|
+
{
|
|
135
|
+
if ($this->messageListener !== null) {
|
|
136
|
+
($this->messageListener)($value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @internal Called by the connection manager when the wire closes. */
|
|
141
|
+
public function handleClose(): void
|
|
142
|
+
{
|
|
143
|
+
if ($this->closed) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$this->closed = true;
|
|
148
|
+
|
|
149
|
+
if ($this->closeListener !== null) {
|
|
150
|
+
($this->closeListener)();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** @internal Close with a specific code without emitting a frame. */
|
|
155
|
+
public function closeWithCode(int $code): void
|
|
156
|
+
{
|
|
157
|
+
if ($this->closed) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
$this->conn->close($code);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public static function isReservedErrorShape(mixed $value): bool
|
|
165
|
+
{
|
|
166
|
+
return is_array($value)
|
|
167
|
+
&& array_keys($value) === ['error']
|
|
168
|
+
&& is_string($value['error']);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\Websocket;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A broadcast pool: one `Socket` per open connection.
|
|
9
|
+
*
|
|
10
|
+
* How a handler shares its connection with the room it belongs to: keep one
|
|
11
|
+
* pool in shared state, `add` each socket when its conversation starts, and
|
|
12
|
+
* `broadcast` fans one value out to everyone. Connections whose browser is
|
|
13
|
+
* gone are pruned on the way. Keep authenticated and guest traffic in
|
|
14
|
+
* separate pools so a private broadcast can never reach a guest connection.
|
|
15
|
+
*/
|
|
16
|
+
final class SocketPool
|
|
17
|
+
{
|
|
18
|
+
/** @var Socket[] */
|
|
19
|
+
private array $sockets = [];
|
|
20
|
+
|
|
21
|
+
public function count(): int
|
|
22
|
+
{
|
|
23
|
+
return count($this->sockets);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public function add(Socket $socket): void
|
|
27
|
+
{
|
|
28
|
+
$this->sockets[] = $socket;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public function discard(Socket $socket): void
|
|
32
|
+
{
|
|
33
|
+
$this->sockets = array_values(array_filter(
|
|
34
|
+
$this->sockets,
|
|
35
|
+
static fn (Socket $candidate): bool => $candidate !== $socket,
|
|
36
|
+
));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Send one value to everyone, pruning closed connections.
|
|
41
|
+
*/
|
|
42
|
+
public function broadcast(mixed $value): void
|
|
43
|
+
{
|
|
44
|
+
foreach ($this->sockets as $socket) {
|
|
45
|
+
if (!$socket->send($value)) {
|
|
46
|
+
$this->discard($socket);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\Websocket;
|
|
6
|
+
|
|
7
|
+
use InvalidArgumentException;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The named-socket registry: what `pp.socket("name", ...)` connects to.
|
|
11
|
+
*
|
|
12
|
+
* The client connects with a name and nothing else, so socket names are
|
|
13
|
+
* unique application-wide and a duplicate is refused at registration time.
|
|
14
|
+
* Registrations live in `src/Lib/Websocket/sockets.php`, which the socket
|
|
15
|
+
* server loads on startup.
|
|
16
|
+
*/
|
|
17
|
+
final class SocketRegistry
|
|
18
|
+
{
|
|
19
|
+
/**
|
|
20
|
+
* @var array<string, array{
|
|
21
|
+
* name: string,
|
|
22
|
+
* handler: callable,
|
|
23
|
+
* requireAuth: bool,
|
|
24
|
+
* allowedRoles: string[],
|
|
25
|
+
* }>
|
|
26
|
+
*/
|
|
27
|
+
private static array $sockets = [];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register a function as a named socket.
|
|
31
|
+
*
|
|
32
|
+
* The handler receives `(Socket $socket, array $args)`: the open
|
|
33
|
+
* connection and the argument payload from the connection's first frame.
|
|
34
|
+
*
|
|
35
|
+
* - `requireAuth: true` refuses the connection unless the handshake
|
|
36
|
+
* carries a valid auth cookie, before the handler runs.
|
|
37
|
+
* - `allowedRoles: [...]` adds RBAC on the verified payload, the same
|
|
38
|
+
* rule as `#[Exposed(allowedRoles: [...])]`.
|
|
39
|
+
*
|
|
40
|
+
* @param string[] $allowedRoles
|
|
41
|
+
*/
|
|
42
|
+
public static function register(
|
|
43
|
+
string $name,
|
|
44
|
+
callable $handler,
|
|
45
|
+
bool $requireAuth = false,
|
|
46
|
+
array $allowedRoles = [],
|
|
47
|
+
): void {
|
|
48
|
+
if ($name === '') {
|
|
49
|
+
throw new InvalidArgumentException('A socket needs a non-empty name.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (isset(self::$sockets[$name])) {
|
|
53
|
+
throw new InvalidArgumentException(
|
|
54
|
+
"Two sockets are named `$name`. The client connects with a "
|
|
55
|
+
. 'name and nothing else, so socket names must be unique '
|
|
56
|
+
. 'application-wide.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
self::$sockets[$name] = [
|
|
61
|
+
'name' => $name,
|
|
62
|
+
'handler' => $handler,
|
|
63
|
+
'requireAuth' => $requireAuth,
|
|
64
|
+
'allowedRoles' => array_values($allowedRoles),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @return array{
|
|
70
|
+
* name: string,
|
|
71
|
+
* handler: callable,
|
|
72
|
+
* requireAuth: bool,
|
|
73
|
+
* allowedRoles: string[],
|
|
74
|
+
* }|null
|
|
75
|
+
*/
|
|
76
|
+
public static function get(string $name): ?array
|
|
77
|
+
{
|
|
78
|
+
return self::$sockets[$name] ?? null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @return string[]
|
|
83
|
+
*/
|
|
84
|
+
public static function names(): array
|
|
85
|
+
{
|
|
86
|
+
return array_keys(self::$sockets);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Named-socket registrations: the server half of `pp.socket(...)`.
|
|
7
|
+
*
|
|
8
|
+
* This file is loaded once by `websocket-server.php` on startup. Register
|
|
9
|
+
* each socket the app serves here; the name the client connects with is the
|
|
10
|
+
* registered one, and names are unique application-wide.
|
|
11
|
+
*
|
|
12
|
+
* From the browser:
|
|
13
|
+
*
|
|
14
|
+
* ```js
|
|
15
|
+
* const sock = pp.socket("echo", { label: "you" }, {
|
|
16
|
+
* onMessage: (value) => append(value),
|
|
17
|
+
* });
|
|
18
|
+
* sock.send("hello");
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
use Lib\Websocket\Socket;
|
|
23
|
+
use Lib\Websocket\SocketPool;
|
|
24
|
+
use Lib\Websocket\SocketRegistry;
|
|
25
|
+
|
|
26
|
+
// A minimal example: echo every frame back, prefixed with the label the
|
|
27
|
+
// page opened the socket with.
|
|
28
|
+
SocketRegistry::register('echo', function (Socket $socket, array $args): void {
|
|
29
|
+
$label = is_string($args['label'] ?? null) ? $args['label'] : 'echo';
|
|
30
|
+
|
|
31
|
+
$socket->onMessage(function (mixed $value) use ($socket, $label): void {
|
|
32
|
+
$text = is_string($value) ? $value : json_encode($value, JSON_UNESCAPED_UNICODE);
|
|
33
|
+
$socket->send("$label: $text");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// A broadcast example: everyone connected to `chat` hears everyone else.
|
|
38
|
+
$chatRoom = new SocketPool();
|
|
39
|
+
|
|
40
|
+
SocketRegistry::register('chat', function (Socket $socket, array $args) use ($chatRoom): void {
|
|
41
|
+
$chatRoom->add($socket);
|
|
42
|
+
|
|
43
|
+
$socket->onMessage(function (mixed $value) use ($chatRoom): void {
|
|
44
|
+
$chatRoom->broadcast($value);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
$socket->onClose(function () use ($chatRoom, $socket): void {
|
|
48
|
+
$chatRoom->discard($socket);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -54,7 +54,10 @@ if ($probe === false) {
|
|
|
54
54
|
fclose($probe);
|
|
55
55
|
|
|
56
56
|
// ── Build app ────────────────────────────────────────────────────────────────
|
|
57
|
-
|
|
57
|
+
// Named-socket registrations: the functions `pp.socket("name", ...)` reaches.
|
|
58
|
+
require __DIR__ . '/sockets.php';
|
|
59
|
+
|
|
60
|
+
$manager = new ConnectionManager(); // The PulsePoint named-socket wire.
|
|
58
61
|
$server = IoServer::factory(
|
|
59
62
|
new HttpServer(new WsServer($manager)),
|
|
60
63
|
$port,
|
|
@@ -73,14 +76,18 @@ echo " Port: {$port}" . PHP_EOL;
|
|
|
73
76
|
echo " URL: {$url}" . PHP_EOL;
|
|
74
77
|
echo " PID: {$pid}" . PHP_EOL;
|
|
75
78
|
echo " Started: {$ts}" . PHP_EOL;
|
|
79
|
+
echo " Sockets: " . (implode(', ', \Lib\Websocket\SocketRegistry::names()) ?: '(none registered)') . PHP_EOL;
|
|
76
80
|
|
|
77
81
|
// ── Graceful shutdown & periodic logs (if loop available) ────────────────────
|
|
78
82
|
$loop = property_exists($server, 'loop') ? $server->loop : null;
|
|
79
83
|
if ($loop instanceof LoopInterface) {
|
|
84
|
+
// First-frame timeout and idle sweep need the loop's timers.
|
|
85
|
+
$manager->attachLoop($loop);
|
|
86
|
+
|
|
80
87
|
// Periodic stats every 60s
|
|
81
|
-
$loop->addPeriodicTimer(60, function () use ($ok) {
|
|
88
|
+
$loop->addPeriodicTimer(60, function () use ($ok, $manager) {
|
|
82
89
|
$mem = function_exists('memory_get_usage') ? number_format(memory_get_usage(true) / 1048576, 2) . ' MB' : 'n/a';
|
|
83
|
-
$msg = "✓ Heartbeat — memory: {$mem}";
|
|
90
|
+
$msg = "✓ Heartbeat — connections: {$manager->openConnectionCount()}, memory: {$mem}";
|
|
84
91
|
echo $ok($msg) . PHP_EOL;
|
|
85
92
|
});
|
|
86
93
|
|
package/dist/src/app/globals.css
CHANGED
package/dist/src/app/layout.php
CHANGED
|
@@ -15,7 +15,7 @@ MainLayout::$description = !empty(MainLayout::$description) ? MainLayout::$descr
|
|
|
15
15
|
<!-- Dynamic Header Scripts -->
|
|
16
16
|
</head>
|
|
17
17
|
|
|
18
|
-
<body
|
|
18
|
+
<body style="opacity:0;pointer-events:none;user-select:none;transition:opacity .18s ease-out;">
|
|
19
19
|
<?= MainLayout::$children; ?>
|
|
20
20
|
<!-- Dynamic Footer Scripts -->
|
|
21
21
|
</body>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Tests;
|
|
6
|
+
|
|
7
|
+
use Firebase\JWT\JWT;
|
|
8
|
+
use Lib\Auth\Auth;
|
|
9
|
+
use PHPUnit\Framework\TestCase;
|
|
10
|
+
use PP\Env;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The JWT session tokens both wires trust: RPC auth reads them through the
|
|
14
|
+
* request session, and the socket server verifies the handshake cookie with
|
|
15
|
+
* `verifyToken(...)` alone.
|
|
16
|
+
*/
|
|
17
|
+
final class AuthTest extends TestCase
|
|
18
|
+
{
|
|
19
|
+
public function testSignInReturnsATokenThatVerifiesBackToThePayload(): void
|
|
20
|
+
{
|
|
21
|
+
$auth = Auth::getInstance();
|
|
22
|
+
|
|
23
|
+
$jwt = $auth->signIn('admin');
|
|
24
|
+
|
|
25
|
+
self::assertSame('admin', $auth->verifyToken($jwt));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public function testATamperedTokenVerifiesToNull(): void
|
|
29
|
+
{
|
|
30
|
+
$auth = Auth::getInstance();
|
|
31
|
+
$jwt = $auth->signIn('admin');
|
|
32
|
+
|
|
33
|
+
self::assertNull($auth->verifyToken($jwt . 'x'));
|
|
34
|
+
self::assertNull($auth->verifyToken('not-a-jwt'));
|
|
35
|
+
self::assertNull($auth->verifyToken(null));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public function testAnExpiredTokenVerifiesToNull(): void
|
|
39
|
+
{
|
|
40
|
+
$expired = JWT::encode(
|
|
41
|
+
[Auth::PAYLOAD_NAME => 'admin', 'exp' => time() - 60],
|
|
42
|
+
Env::string('AUTH_SECRET', ''),
|
|
43
|
+
'HS256'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
self::assertNull(Auth::getInstance()->verifyToken($expired));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public function testATokenSignedWithADifferentSecretVerifiesToNull(): void
|
|
50
|
+
{
|
|
51
|
+
$forged = JWT::encode(
|
|
52
|
+
[Auth::PAYLOAD_NAME => 'admin', 'exp' => time() + 3600],
|
|
53
|
+
'a-different-32-byte-signing-secret-key',
|
|
54
|
+
'HS256'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
self::assertNull(Auth::getInstance()->verifyToken($forged));
|
|
58
|
+
}
|
|
59
|
+
}
|