artisan-build / bfc-client
Client-side Built for Cloud package: stable client identity and proof-of-life for BfC client applications.
Requires
- php: ^8.3
- illuminate/cache: ^13.0
- illuminate/contracts: ^13.0
- illuminate/filesystem: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/pint: ^1.27
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
This package is auto-updated.
Last update: 2026-08-24 11:55:55 UTC
README
The client-side Built for Cloud package. A BfC client application carries this lean library so its provider can attribute an API token to a specific client installation and confirm the client is actually installed — a stable client identity and a proof-of-life signal, without the control plane ever reading customer environment variables.
Installation
composer require artisan-build/bfc-client
The service provider is auto-discovered. Optionally publish the config:
php artisan vendor:publish --tag=bfc-client-config
Client identity
ArtisanBuild\BfcClient\ClientIdentity (a container singleton) resolves a stable identifier for the installation via resolve():
- Explicit config — when
bfc-client.identity(theBFC_CLIENT_IDENTITYenv var) is a non-empty string, it is returned verbatim and never persisted. - Persisted file — otherwise the identity stored at
storage_path('app/bfc-client/identity')is returned (the value is read back trimmed). - Generated — otherwise a UUID is generated, persisted to that file, and reused on subsequent resolutions.
The identity is an identifier, never a secret. Installs on ephemeral filesystems should set BFC_CLIENT_IDENTITY explicitly — otherwise the generated identity will churn on every redeploy.
Attaching the identity to requests
The package registers an HTTP client macro, Http::withClientIdentity(), that returns a pending request carrying the identity header. It composes with every other pending-request option:
use Illuminate\Support\Facades\Http; Http::withClientIdentity() ->withToken($token) ->post('https://provider.example/api/things', [...]);
The identity is resolved lazily, at call time. The macro never truncates or mutates the identity: a resolved identity that violates the wire contract below (not valid UTF-8, over 255 bytes, or containing a CR, LF, or NUL octet) throws an InvalidArgumentException instead of being sent. The macro also replaces any pre-existing X-BfC-Client-Id header (for example one set via Http::globalOptions()), so the validated identity is the only value on the wire.
Proof of life
The package registers a read-only route, GET /bfc-client by default, that a BfC provider polls to confirm the package is installed in the client app and to read its client identity:
curl https://client-app.example/bfc-client
# {"package":"artisan-build/bfc-client","client_id":"0198c5f2-..."}
The route is unauthenticated (the identity is an identifier, never a secret), throttled to 60 requests per minute, and returns nothing beyond the package name and the resolved identity; an identity that violates the wire contract's limits fails loudly (a 500) rather than being served. Move it with BFC_CLIENT_PROOF_OF_LIFE_PATH, or disable it entirely with BFC_CLIENT_PROOF_OF_LIFE=false.
Wire contract
The wire contract between a client app and its BfC provider is documented here as each piece lands:
- Client identity header (
X-BfC-Client-Id)- Header name:
X-BfC-Client-Id. - Value: the resolved client identity — an opaque, stable string of valid UTF-8, 1–255 bytes, containing no CR (
\r), LF (\n), or NUL (\0) octets. (Literal CR/LF octets are the header-injection hazard; exotic Unicode separators such as U+2028 are permitted and treated as opaque bytes.) Providers MUST treat it as an opaque identifier: compare byte-wise and store verbatim. Providers MUST NOT treat it as a credential or grant anything based on it alone. - Exactly one header value: a request carries
X-BfC-Client-Idexactly once, with exactly one value. HTTP folds repeated field lines into one comma-joined value, and the identity is opaque — it may itself contain a comma — so a folded value cannot be unambiguously split back apart. A client MUST NOT send more than one value, and a provider that receives more than one MUST NOT pick one or join them.Http::withClientIdentity()usesreplaceHeaders()for this reason: whatever was pre-set, the validated identity is the only value on the wire. - No NUL byte: the identity MUST NOT contain a NUL (
\0) octet. A NUL is valid UTF-8 and is neither CR nor LF, so it clears every other limit — but PostgreSQL silently truncates a stored string at the first NUL rather than erroring, soclient\0oneis stored asclient. Two byte-distinct identities collide on a PostgreSQL-backed provider while staying distinct on another driver, which is why a provider cannot honour "store verbatim" for such a value and rejects it instead. This client rejects a NUL-containing identity before it is sent. - Byte-wise comparison: providers MUST compare identities as raw bytes, never under a database collation. A case- or accent-insensitive collation — MySQL's
utf8mb4_0900_ai_cidefault among them — treatsclient-aandCLIENT-Aas equal and collapses two distinct installations into one row. A stored identity column inherits the consuming application's collation, so a provider that needs to look an identity up should key on a digest of the exact bytes rather than trust the column's comparison semantics. - When sent: on requests the client app makes to its provider using
Http::withClientIdentity(), typically alongside its normal token auth. The client never sends a value outside the limits above — it fails loudly instead. - Provider behaviour (informative): a BfC provider records the identity against the API token that authenticated the request (per-token client identity), enabling token→client attribution. Attribution is best-effort and never breaks the client's request:
artisan-build/built-for-cloudv0.4.0 discards a header that is absent, repeated, or non-conforming (over 255 bytes, or containing CR, LF, or NUL) and logs it, rather than failing the request. A client therefore cannot infer from a200that its identity was accepted — which is the reason this package validates before sending.
- Header name:
- Proof-of-life route (
GET /bfc-client)- Endpoint:
GET /bfc-clienton the client app. The path is configurable viaBFC_CLIENT_PROOF_OF_LIFE_PATH; the route can be disabled entirely viaBFC_CLIENT_PROOF_OF_LIFE=false. - Response:
200withContent-Type: application/jsonand the JSON body{"package": "artisan-build/bfc-client", "client_id": "<client identity>"}— those two keys and nothing else. - Semantics: unauthenticated, read-only, throttled (60 requests per minute). The response never contains secrets;
client_idis the same opaque identifier theX-BfC-Client-Idheader carries, subject to the same limits, and grants nothing on its own. - Provider behaviour (informative): a
200with a parseable body confirms the package is installed; the returnedclient_idlets the provider key this installation to its records. A404means the package is not installed, or the route is disabled or moved.
- Endpoint:
License
MIT. See LICENSE.