Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- `$fleetbase->socket->token()` mints a short-lived realtime socket token (`POST socket/token`) for server-side exchange; the browser presents it with `socket.authenticate(token)`.

## [1.4.1] - 2026-09-14

### Changed
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,24 @@ $history = $fleetbase->vehicles->listVehicleInspections($vehicleId, ['limit' =>

Read the form's `grouped_fields` to obtain field IDs and required answer types; a field's `id` is what an answer names, and it is the same id the submission answers with. `answers` was called `custom_field_values`, and each answer's `field` was called `custom_field`; both older spellings are still accepted on submit. Reuse the same caller-generated idempotency key when replaying one submission; generate a new key for a new inspection. The SDK passes the key through to the API and does not implement its own deduplication. Forms are published in the console, not created through this public API. Form authoring, submission updates/deletion, and public inspection-link management are not supported public endpoints.

### Realtime socket tokens

Realtime channel subscriptions are authorized with a short-lived socket token. Mint it on your server with your secret key and send only the token to the browser or device; never expose the API key in client code.

```php
$minted = $fleetbase->socket->token(); // POST /v1/socket/token

// Return this to your authenticated front end:
// { "token": "...", "expires_in": 900, "expires_at": "2026-01-01T00:15:00+00:00" }
echo json_encode([
'token' => $minted->token,
'expires_in' => $minted->expires_in,
'expires_at' => $minted->expires_at,
]);
```

The browser connects with `socketcluster-client`, calls `socket.authenticate(token)` (or supplies the token through an in-memory `authEngine`), and asks your server for a new token about 60 seconds before `expires_in` elapses. A token minted with an API key may subscribe to its company channel (`company.{company uuid}`), its own key channel (`api.{key id}`), and channels of resources in the same company. A server without realtime authentication configured answers `404`, raised as `NotFoundException`.

## Configuration

The second constructor argument accepts client configuration. The third legacy argument retains the debug flag without printing requests or credentials.
Expand Down
10 changes: 10 additions & 0 deletions src/Fleetbase.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use Fleetbase\Sdk\Services\ServiceAreaService;
use Fleetbase\Sdk\Services\ServiceQuoteService;
use Fleetbase\Sdk\Services\ServiceRateService;
use Fleetbase\Sdk\Services\SocketService;
use Fleetbase\Sdk\Services\TrackingNumberService;
use Fleetbase\Sdk\Services\TrackingStatusService;
use Fleetbase\Sdk\Services\TrailerService;
Expand Down Expand Up @@ -175,8 +176,11 @@
/** @var FileService */
public $files;

/** @var SocketService */
public $socket;

/** @param array<string, mixed> $config */
public function __construct(string $publicKey, array $config = [], bool $debug = false)

Check warning on line 183 in src/Fleetbase.php

View workflow job for this annotation

GitHub Actions / Mutation baseline

Escaped Mutant for Mutator "FalseValue": @@ @@ public $socket; /** @PARAM array<string, mixed> $config */ - public function __construct(string $publicKey, array $config = [], bool $debug = false) + public function __construct(string $publicKey, array $config = [], bool $debug = true) { $configuration = new Configuration($publicKey, $config, $debug); $this->version = $configuration->getVersion();
{
$configuration = new Configuration($publicKey, $config, $debug);
$this->version = $configuration->getVersion();
Expand Down Expand Up @@ -221,6 +225,7 @@
$this->chatChannels = new ChatChannelService($this->client);
$this->comments = new CommentService($this->client);
$this->files = new FileService($this->client);
$this->socket = new SocketService($this->client);
}

public function setApiKey(string $publicKey): Fleetbase
Expand All @@ -236,7 +241,7 @@
$args = func_get_args();
$publicKey = $args[0] ?? null;
$config = $args[1] ?? [];
$debug = $args[2] ?? false;

Check warning on line 244 in src/Fleetbase.php

View workflow job for this annotation

GitHub Actions / Mutation baseline

Escaped Mutant for Mutator "FalseValue": @@ @@ $args = func_get_args(); $publicKey = $args[0] ?? null; $config = $args[1] ?? []; - $debug = $args[2] ?? false; + $debug = $args[2] ?? true; if (!is_string($publicKey) || !is_array($config) || !is_bool($debug)) { throw new \InvalidArgumentException('Fleetbase::newInstance() expects an API key, configuration array, and debug boolean.'); }
if (!is_string($publicKey) || !is_array($config) || !is_bool($debug)) {
throw new \InvalidArgumentException('Fleetbase::newInstance() expects an API key, configuration array, and debug boolean.');
}
Expand Down Expand Up @@ -460,4 +465,9 @@
{
return $this->workOrders;
}

public function socket(): SocketService
{
return $this->socket;
}
}
52 changes: 52 additions & 0 deletions src/Services/SocketService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/**
* This file is part of the fleetbase/fleetbase-php library.
*
* @copyright Copyright (c) Fleetbase Pte Ltd. <ron@fleetbase.io>
* @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace Fleetbase\Sdk\Services;

use Fleetbase\Sdk\HttpClient;
use Fleetbase\Sdk\Service;

/**
* Realtime (SocketCluster) helpers.
*
* Hand-written: this service is not generated from the Postman contract, so
* tools/generate-endpoint-services.php leaves it untouched.
*/
class SocketService extends Service
{
/** @param array<string, mixed> $options */
public function __construct(HttpClient $client, array $options = [])
{
parent::__construct('Socket', $client, array_merge(['namespace' => 'socket'], $options));
}

/**
* Mint a short-lived realtime socket token: `POST socket/token`.
*
* Call this on your server with your secret API key and hand only the
* returned token to the browser or device, which presents it with
* `socket.authenticate(token)`. An API-key token may subscribe to the
* key's company channel (`company.{company uuid}`), its own key channel
* (`api.{key id}`), and channels of resources in the same company.
* Refresh it about 60 seconds before `expires_in` elapses.
*
* The decoded response has `token` (string), `expires_in` (seconds) and
* `expires_at` (ISO 8601). A server without realtime authentication
* configured responds 404, raised as a NotFoundException.
*
* @param array<string, mixed> $options Request options.
* @return mixed
*/
public function token(array $options = [])
{
return $this->client->post($this->uri('token'), [], $options);
}
}
1 change: 1 addition & 0 deletions tests/Fleetbase/FleetbaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ private static function services(): array
'chatChannels' => \Fleetbase\Sdk\Services\ChatChannelService::class,
'comments' => \Fleetbase\Sdk\Services\CommentService::class,
'files' => \Fleetbase\Sdk\Services\FileService::class,
'socket' => \Fleetbase\Sdk\Services\SocketService::class,
];
}
}
47 changes: 47 additions & 0 deletions tests/Fleetbase/SocketTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Fleetbase\Sdk\Test\Fleetbase;

use Fleetbase\Sdk\Exception\NotFoundException;
use Fleetbase\Sdk\Services\SocketService;
use Fleetbase\Sdk\Test\TestCase;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;

final class SocketTest extends TestCase
{
public function testMintsSocketTokenWithPostToSocketToken(): void
{
$service = new SocketService($this->mockHttpClient([
new Response(200, ['Content-Type' => 'application/json'], '{"token":"header.payload.signature","expires_in":900,"expires_at":"2026-01-01T00:15:00+00:00"}'),
]));

$minted = $service->token();

self::assertIsObject($minted);
$attributes = get_object_vars($minted);
self::assertSame('header.payload.signature', $attributes['token'] ?? null);
self::assertSame(900, $attributes['expires_in'] ?? null);
self::assertSame('2026-01-01T00:15:00+00:00', $attributes['expires_at'] ?? null);

self::assertCount(1, $this->history);
$transaction = $this->history[0];
self::assertIsArray($transaction);
$request = $transaction['request'] ?? null;
self::assertInstanceOf(RequestInterface::class, $request);
self::assertSame('POST', $request->getMethod());
self::assertSame('/v1/socket/token', $request->getUri()->getPath());
}

public function testServerWithoutSocketAuthRaisesNotFound(): void
{
$service = new SocketService($this->mockHttpClient([
new Response(404, ['Content-Type' => 'application/json'], '{"error":"Not found"}'),
]));

$this->expectException(NotFoundException::class);
$service->token();
}
}
Loading