Download the PHP package hygo/laravel-api-waypoint without Composer
On this page you can find all versions of the php package hygo/laravel-api-waypoint. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download hygo/laravel-api-waypoint
More information about hygo/laravel-api-waypoint
Files in hygo/laravel-api-waypoint
Package laravel-api-waypoint
Short Description Dev-only Laravel package that publishes a machine-readable description of every API endpoint: input schema, query contract, auth requirements and payload-generation hints.
License MIT
Informations about the package laravel-api-waypoint
hygo/laravel-api-waypoint
A dev-only Laravel package that introspects your application and publishes a machine-readable description of every API endpoint: its input schema, its query-string contract, its auth requirements, and hints for generating realistic test payloads.
A local companion app (the Central App) pulls that document and builds editable, ready-to-send requests. The point is to stop hand-maintaining Bruno and Postman collections that go stale the moment someone adds a field.
Security, read this first
This package must never be enabled in production.
It exposes database reads, Sanctum token minting and state seeding behind a single shared secret. That is an acceptable trade for a local development tool and completely unacceptable anywhere near customer data.
Three independent conditions must all hold before a single route is registered, and the service provider throws at boot if
productionappears in the permitted environment list at all. See Safety model.
Requirements
| Dependency | Version | |
|---|---|---|
| PHP | ^8.3 | |
| Laravel | ^12.0 || ^13.0 | |
spatie/laravel-data |
^4.0 | required |
lorisleiva/laravel-actions |
^2.7 | optional |
nwidart/laravel-modules |
^11.0 || ^12.0 | optional, module attribution |
spatie/laravel-query-builder |
^6.0 || ^7.0 | optional, query contracts |
spatie/laravel-fractal |
^6.0 | optional, transformer includes |
laravel/sanctum |
^4.0 | optional, token minting |
Every optional dependency is detected at runtime. The package installs and works without any of them; it just degrades to "no module attribution / no query config / no transformer info".
Laravel 11 is not supported. Its security-fix window closed in March 2026, every 11.x release now carries an unpatched advisory, and Composer refuses to install advisory-affected packages by default. Supporting a version that cannot be installed without switching that protection off would be a claim rather than a fact.
Install
waypoint:install publishes the config, works out which URI prefix your application actually registers its API under and writes routes.include to match, then adds the two environment keys with a generated secret. It refuses to run in production, never overwrites a secret that is already set, and reports a customised routes.include rather than clobbering it. --include=, --secret=, --skip-env and --force override the individual steps.
By hand instead:
Then set two environment variables, in your local .env only:
And set routes.include to the prefix your endpoints are actually registered under. The default is api/*, which is right for an application routing through routes/api.php and matches nothing in one that registers v1/orders from a per-module route file. The symptom is a document with zero endpoints, which reads as a broken package rather than as one wrong config line.
Check it:
You should get a document whose schema_format_version is 1.0. If you get a 404, one of the three registration conditions is not met: see below.
Safety model
Registration is conditional, not protected
Routes are registered only when all three hold:
| Condition | Default |
|---|---|
api-waypoint.enabled === true |
false |
the current environment is in api-waypoint.environments |
['local'] |
api-waypoint.secret is non-empty |
no default |
Fail any one and the routes are absent from the route table entirely, so a probe gets Laravel's own 404. There is no "registered but forbidden" state to discover.
production in api-waypoint.environments is a hard boot failure, not a silent decline. A silent decline hides the mistake until somebody "fixes" it by making the config worse.
The secret
Sent as X-Api-Waypoint-Secret and compared with hash_equals() over hashes of both sides, so the comparison is constant-time for any presented length. A mismatch is a 404, never a 403, with Laravel's standard {"message": "Not Found."}.
Reference lookups are whitelisted by the compiled schema
GET /references/{table}/{column} reads a (table, column) pair only if it appears in an exists: or unique: rule somewhere in the compiled document, in a route-model binding, or in references.extra. A table that plainly exists in your database but is named nowhere in the schema is a 404.
On top of that: where[] keys are checked with Schema::hasColumn(), values are always bound, limit is clamped to 50, and columns in references.redact can be neither read, labelled by, nor filtered on.
Token minting is whitelisted by role
Only role names in tokens.roles are accepted. Each role's resolver is handed a waypoint email derived from tokens.email_pattern (default waypoint+{role}@{host}), and the controller re-checks the returned user's email against it. A resolver that goes looking for a real customer account cannot get a token issued for it.
Scenarios accept a name, not code
POST /scenarios takes a name from api-waypoint.scenarios and that scenario's own declared, validated parameters. There is deliberately no code path that accepts a class name, factory name or attribute array.
Audit log
Every waypoint request writes one line to api-waypoint.log_channel: route, method, status, an 8-character fingerprint of the presented secret, and the resolved scenario or role where relevant. It makes "who seeded 400 orders" answerable.
What it produces
Field schemas are JSON Schema draft 2020-12 plus two extension namespaces:
x-laravel— the Laravel facts JSON Schema cannot express:exists:,unique:, conditional rules, the enum class, the PHP property name.x-faker— an abstract generation strategy. This package never names a generator library method; the Central App maps a strategy to whatever it generates with. That is what lets the two codebases be built independently.
The wire format is normative in resources/schema/api-waypoint-1.0.json, a meta-schema both this package and the Central App test against. One authority, and it is machine-checkable: a prose copy alongside it drifts silently, because nothing fails when it is wrong.
Adopting the query contract
Spatie Query Builder assembles its allowed lists inside a runtime method chain, so nothing can read them by reflection. Declare them once as a QueryConfig and build the query from the same object, and the description cannot drift from what the endpoint enforces.
Before — the allowed lists exist only at runtime, and the Postman collection is a guess:
After — one declaration, used by both the endpoint and the schema:
That is fewer lines than before, the per_page ceiling is enforced rather than repeated, and the enum's cases become the filter's allowed_values in the document automatically.
The four unmapped reasons, and how to fix each
An endpoint the compiler cannot fully describe is still emitted in endpoints[] with "input": null, so the Central App can list it as read-only, and is simultaneously listed in diagnostics.unmapped_routes. Nothing is silently dropped.
Every entry carries exactly one reason:
| Reason | What it means | How to fix it |
|---|---|---|
no_data_class |
The action takes no Spatie Data parameter and does not implement ProvidesWaypointInput. Usually an inline $request->validate(), which v1 does not introspect. |
Type-hint a Data class on handle() / asController(), or implement ProvidesWaypointInput::waypointInput() and return the Data class FQCN. |
multipart |
The endpoint accepts file uploads, detected from an UploadedFile-typed property or a file / image / mimes rule. Out of scope for v1. |
Split the upload into its own endpoint, or exclude the route in routes.exclude. The endpoint still appears, just without a body schema. |
closure_action |
The route action is a closure, so there is nothing to reflect. | Move the closure into an Action or controller class. |
unsupported_action |
The action class exists but could not be reflected, or a declared Data class could not be compiled. | Check the class is autoloadable and not abstract. If a Data class failed, look for an uncompilable_data_class warning naming the reason. |
A GET or DELETE with no Data class is not reported: there is no body to describe, so it is not a gap. That is what makes --fail-on-unmapped adoptable rather than something you switch off on day two.
Escape hatches
When reflection cannot see the body, or sees the wrong thing:
Returning null is a positive statement, not a gap, and is not reported.
Other declarations the compiler will not guess at:
And per-property generation overrides:
Artisan commands
| Command | Behaviour |
|---|---|
waypoint:install |
Publish the config, detect the API route prefix, write the local env keys. --include=, --secret=, --skip-env, --force. |
waypoint:handshake |
Print the connection details a local companion app needs: URL, header, secret, paths. --json. Exits non-zero when the surface is not registered, naming which condition is unmet. |
waypoint:schema |
Compile and write the document. --output=path (default stdout), --pretty, --clear to bust the cache. |
waypoint:check |
Compile and report gaps and warnings. --fail-on-unmapped, --fail-on-warning, --baseline=path. |
waypoint:snapshot |
--list shows stored response snapshots and their age, --prune deletes them. |
waypoint:check works with the package disabled: only route registration is gated on enabled, never the compiler. CI never needs the HTTP surface switched on.
The second is the "collection cannot go stale" enforcement: a PR that changes an endpoint must regenerate the committed baseline, and the diff shows exactly which endpoints and DTOs moved.
Connecting a local companion app
Every route needs the shared secret, including the two read-only ones, and that is not going to be relaxed. The document is a precise map of the application - table and column names from every exists: rule, action classes, roles and abilities, and the URL of the token-minting endpoint - and the same secret is what keeps POST /tokens and POST /scenarios out of reach of any web page your browser happens to load. A custom request header cannot be sent cross-origin without clearing a CORS preflight, so requiring one is also what stops a hostile site from reaching your-app.test while you are logged into it.
None of that requires you to copy a secret by hand, because a local companion app is local: it can read the secret out of the project directory instead.
Being able to run that command in a checkout is a stronger claim to be the local dev tool than any credential presented over HTTP. Paths are published rather than assumed, so a companion app hardcodes none of them and keeps working when prefix changes.
It also answers the question the HTTP surface deliberately cannot. A 404 there is identical for an unregistered surface and a wrong secret; this reports registered: false with unregistered_reason of disabled, environment_not_permitted, no_secret or not_loaded, and exits non-zero, so a companion app can say which of the three conditions to fix rather than "connection failed".
It refuses to run in production, and never prints a secret there.
AI agents, via Laravel Boost
Install Laravel Boost alongside this package and boost:install picks up a guideline and a skill shipped here, composing them into whichever agent files your project uses (Claude Code, Cursor, Copilot, Codex, and the rest). Authored once, rather than per agent.
The guideline is short and always in context: the document is the source of truth for endpoint shape, run waypoint:check after touching a route or a Data class, and never enable this outside local. The api-waypoint skill carries the detail an agent otherwise gets wrong: that routes.include has to match the real prefix, that the document is served at the prefix root and {prefix}/schema is a 404, that a 404 cannot distinguish an unregistered surface from a bad secret and route:list is how you tell, and what to do about each of the four unmapped reasons.
Both live under resources/boost/, which is where Boost globs a third-party package for them. You can exclude either from the composed output with boost.guidelines.exclude or boost.skills.exclude.
MCP tools
With Boost installed, three read-only tools are added to its MCP server:
| Tool | Returns |
|---|---|
waypoint-check |
Unmapped routes with a remedy each, warnings grouped by code, the document hash. Optional reason filter, warnings: false. |
waypoint-endpoints |
One line per endpoint: id, method, URI, module, auth, input component, whether it has a query contract. Filter by module, search, unmapped_only. |
waypoint-endpoint |
One endpoint in full, with every referenced Data class resolved transitively, its error responses, and any warning about it. Takes id. |
They read the compiler, not the HTTP surface, so they need neither the shared secret nor enabled and work in a checkout where waypoint was never switched on. Each recompiles on call rather than reusing the memoised document: an agent asking is usually an agent that just changed something, and answering from before its edit would be confidently wrong.
Registration appends to boost.mcp.tools.include, which Boost merges into both the tool list it serves and the allow-list its executor checks. It is guarded on laravel/mcp being installed, so an application without Boost is unaffected.
Response snapshots
The compiler will not derive a response body schema from a Fractal transformer: transform() is arbitrary PHP, and a guessed shape is worse than an honest "shape": "opaque". Instead, record a real one.
Snapshots are sanitised with a recursive deny-list (snapshots.redact), truncated to 3 array elements and 500 characters, and rewritten at most once per TTL window. They never affect a hash, so recording one does not make the Central App report drift.
Configuration
Every key is documented inline in config/api-waypoint.php. The ones worth knowing about:
| Key | Default | |
|---|---|---|
routes.include / routes.exclude |
['api/*'] / waypoint, Sanctum, Horizon, Telescope |
which routes are candidates |
routes.required_middleware |
[] |
when non-empty, a route must carry one of these |
query.probe |
false |
run query() methods to discover config. Executes host code |
faker.overrides |
[] |
keyed "Module.DataClass.property" or "*.property" |
faker.default_include_probability |
0.5 |
chance an optional field is included in a generated payload |
references.redact |
password, remember_token, two_factor_secret, api_token | never read, labelled by or filtered on |
tokens.max_ttl_minutes |
240 |
requested TTLs are clamped to this |
scenarios |
[] |
name => class implementing WaypointScenario |
Development
workbench/ is a miniature host application with two modules and seven endpoints, covering every branch the pipeline has: a create with a nested collection, an index with a full query contract, a show with a route binding, a refund with an unresolvable field, a multipart upload, an inline-validate() action, and an unnamed closure route. The golden-file test asserts the whole compiled document in one assertion, and its fixture is what the Central App is built against.
Releasing
Never tag locally. Releases are cut by the Release workflow, so that a version cannot exist without the full check suite having passed on it.
- Move the shipping entries from
## [Unreleased]into a new## [x.y.z] - YYYY-MM-DDsection inCHANGELOG.md, and merge that tomaster. - Actions → Release → Run workflow, entering
x.y.z(no leadingv). Tick dry-run first if you want the checks without the tag.
The workflow refuses to continue unless the version is valid semver, has a matching CHANGELOG.md section, and has no existing tag. It then validates the manifest, runs Pint, PHPStan and the test suite, confirms the package autoloads with --no-dev, creates the annotated vx.y.z tag and opens a GitHub Release using that section as the notes.
CHANGELOG.md is the single place a version number lives. It is deliberately not in composer.json: Composer derives a package's version from its git tags, and composer validate --strict warns when a package published to Packagist carries a version field. That validation runs in CI, so adding one would fail the build.
Licence
MIT.
All versions of laravel-api-waypoint with dependencies
illuminate/console Version ^12.0 || ^13.0
illuminate/contracts Version ^12.0 || ^13.0
illuminate/database Version ^12.0 || ^13.0
illuminate/http Version ^12.0 || ^13.0
illuminate/routing Version ^12.0 || ^13.0
illuminate/support Version ^12.0 || ^13.0
spatie/laravel-data Version ^4.0