Install
$ agentstack add skill-lonsdale201-wp-agent-skills-br-openapi ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
better-route: OpenAPI documentation
For developers exporting OpenAPI 3.1.0 documentation for a better-route API. Two paths — generate the document on demand (OpenApiExporter::export(...)) or publish it as a live REST endpoint (OpenApiRouteRegistrar::register(...)).
Misconception this skill corrects
> "I added OpenApiRouteRegistrar::register(...) and now my staging environment's /openapi.json returns 401 to anonymous users — must be a permission cache bug."
It's not a cache bug — that's the v0.3.0 default. Verified at [src/OpenApi/OpenApiRouteRegistrar.php:39-41](OpenApiRouteRegistrar.php):
$permissionCallback = $options['permissionCallback'] ?? static function (): bool {
return function_exists('current_user_can') && current_user_can('manage_options');
};
The default permission is current_user_can('manage_options'). Pre-v0.3.0 the doc was public; the v0.3.0 release switched the default to admin-only because OpenAPI docs reveal the entire API surface — useful for internal tooling, dangerous for public exposure.
To restore public access:
OpenApiRouteRegistrar::register(
restNamespace: 'myapp/v1',
contractsProvider: fn () => $router->contracts(openApiOnly: true),
options: [
'permissionCallback' => static fn (): bool => true, // public
],
);
Other AI-prone misconceptions:
- "
strictSchemas: trueis the safest default — turn it on everywhere." Half-true. It throwsInvalidArgumentExceptionon$refto unknown components ([OpenApiExporter.php:33-41](OpenApiExporter.php)). For an internal API where you control every schema reference, that's correct. For a multi-tenant scenario where consumers may pass partial schemas, strict mode breaks generation. Defaultfalsesubstitutes{type: 'object', additionalProperties: true}which is the forgiving v0.2.0 behavior. - "I can publish OpenAPI from a
Resource::makechain without going through the router." Wrong — pass eitherRouter::contracts(),Resourceinstance, or a contract array to the registrar. Mixing types causesInvalidArgumentExceptionat [OpenApiRouteRegistrar.php:99](OpenApiRouteRegistrar.php). - "OpenAPI security schemes are derived from middleware automatically." Wrong — declare them explicitly via the
securitySchemesandglobalSecurityoptions. Better-route doesn't introspect your middleware to figure out auth schemes.
When to use this skill
Trigger when ANY of the following is true:
- The diff calls
BetterRoute::openApiExporter()orOpenApiRouteRegistrar::register(...). - The user asks "how do I generate OpenAPI for my better-route API".
- After v0.3.0 upgrade, the OpenAPI endpoint suddenly returns 401.
- Composing WooCommerce / better-data DTO schemas into the export.
Workflow
1. Generate on demand (one-off export)
add_action('rest_api_init', function () {
$router = \BetterRoute\BetterRoute::router('myapp', 'v1');
// ... routes ...
$router->register();
if (defined('WP_CLI') && WP_CLI) {
return;
}
if (!isset($_GET['__export_openapi'])) {
return;
}
$exporter = \BetterRoute\BetterRoute::openApiExporter();
$document = $exporter->export($router->contracts(), [
'title' => 'My API',
'version' => 'v1.0.0',
]);
header('Content-Type: application/json');
echo json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
});
This is a hand-rolled "export on demand" via a query-string trigger. For production, use the registrar (next step).
2. Publish as a live endpoint
use \BetterRoute\OpenApi\OpenApiRouteRegistrar;
add_action('rest_api_init', function () {
$router = \BetterRoute\BetterRoute::router('myapp', 'v1');
// ... routes ...
$router->register();
OpenApiRouteRegistrar::register(
restNamespace: 'myapp/v1',
contractsProvider: static fn () => $router->contracts(openApiOnly: true),
options: [
'title' => 'My API',
'version' => 'v1.0.0',
]
);
});
Mounts at GET /wp-json/myapp/v1/openapi.json. Default permission: current_user_can('manage_options').
To make it public:
options: [
'title' => 'My API',
'version' => 'v1.0.0',
'permissionCallback' => static fn (): bool => true,
]
To restrict to a custom capability:
'permissionCallback' => static fn (): bool => current_user_can('view_api_docs'),
The contractsProvider is a callable so contracts are computed lazily — only when a client actually requests openapi.json. Saves on every page load.
openApiOnly: true ([Router::contracts](Router.php) parameter) excludes routes that opted out of OpenAPI inclusion (via ->meta(['openApiOnly' => false])). Useful for internal-only routes that shouldn't appear in public docs.
3. Security schemes
$document = $exporter->export($contracts, [
'title' => 'My API',
'version' => 'v1.0.0',
'securitySchemes' => [
'bearerAuth' => [
'type' => 'http',
'scheme' => 'bearer',
'bearerFormat' => 'JWT',
],
'apiKey' => [
'type' => 'apiKey',
'in' => 'header',
'name' => 'X-API-Key',
],
],
'globalSecurity' => [
['bearerAuth' => []], // bearer required globally; per-route override possible
],
]);
securitySchemes declares the auth methods. globalSecurity applies to every operation by default. Per-route overrides via ->protectedByMiddleware([['bearerAuth' => ['scope']]]) or ->publicRoute() (clears security to []).
4. strictSchemas
$exporter->export($contracts, [
'strictSchemas' => true, // throws on unknown $ref
]);
When true and a route references '#/components/schemas/UnknownThing' that isn't defined in components, the exporter throws:
InvalidArgumentException: 'OpenAPI $ref points to unknown component schema "#/components/schemas/UnknownThing"'
When false (default — backwards-compat with v0.2.0), the unknown ref is replaced with {type: 'object', additionalProperties: true} — a valid OpenAPI document, but missing the actual shape.
Pick strict mode for internal APIs where you've cataloged every schema. Pick forgiving mode when consumers / extensions pass partial schemas you don't control.
5. Compose with WooCommerce + better-data DTO schemas
$exporter = \BetterRoute\BetterRoute::openApiExporter();
$contracts = $router->contracts();
$wooComponents = \BetterRoute\BetterRoute::wooOpenApiComponents();
$dtoComponents = \BetterData\Route\BetterRouteBridge::openApiComponents([
\MyPlugin\Dto\PostDto::class,
\MyPlugin\Dto\CreatePostDto::class,
]);
$document = $exporter->export($contracts, [
'title' => 'My API',
'version' => 'v1.0.0',
'strictSchemas' => true,
'components' => array_merge($wooComponents, $dtoComponents),
'securitySchemes' => [/* ... */],
'globalSecurity' => [/* ... */],
]);
wooOpenApiComponents() returns the WooCommerce-specific schemas (Order, Product, Customer, Coupon shapes) so routes registered via wooRouteRegistrar() resolve their $refs. BetterRouteBridge::openApiComponents([...]) walks each DTO class and emits its REST schema — feed both into the components option.
6. Multiple sources in one document
OpenApiRouteRegistrar::register(
restNamespace: 'myapp/v1',
contractsProvider: function () use ($router1, $router2, $resource) {
return [
...$router1->contracts(openApiOnly: true),
...$router2->contracts(openApiOnly: true),
...$resource->contracts(),
];
},
options: [/* ... */]
);
Combine routes from multiple routers and resources into one OpenAPI document. The registrar's source detector ([OpenApiRouteRegistrar.php:99](OpenApiRouteRegistrar.php)) accepts arrays of contract objects, Router instances, or Resource instances. Mixing types of those sources throws InvalidArgumentException.
Critical rules
- Default permission for
OpenApiRouteRegistrarismanage_options([OpenApiRouteRegistrar.php:39-41](OpenApiRouteRegistrar.php)). Public docs require'permissionCallback' => static fn (): bool => true. strictSchemas: truethrows on unknown$ref. Use for internal APIs; defaultfalsesubstitutes{type: 'object', additionalProperties: true}.securitySchemesandglobalSecurityare explicit. Better-route doesn't infer auth schemes from middleware.->publicRoute()clears OpenAPI security to[]for that operation;->protectedByMiddleware($security)sets per-operation security.contractsProvideris a callable — contracts compute lazily on request. Don't pass a pre-computed array; you'd snapshot at registration time.openApiOnly: truefilters out internal routes marked via->meta(['openApiOnly' => false]).permissionCallbackmust be callable. ThrowsInvalidArgumentExceptionotherwise ([OpenApiRouteRegistrar.php:42-44](OpenApiRouteRegistrar.php)).restNamespacemust matchvendor/versionformat — verified at [OpenApiRouteRegistrar.php:121](OpenApiRouteRegistrar.php).- OpenAPI 3.1.0 output — most tooling (Swagger UI, Redoc, Postman) supports it; legacy 3.0.x consumers may need conversion.
Common mistakes
// WRONG — assuming OpenAPI is public by default (post-v0.3.0)
OpenApiRouteRegistrar::register(
restNamespace: 'myapp/v1',
contractsProvider: fn () => $router->contracts(),
);
// → /openapi.json returns 401 for anonymous users
// RIGHT — explicit public for documentation portals
options: ['permissionCallback' => static fn (): bool => true]
// RIGHT — explicit admin-only for internal docs
// (this is the default, no override needed)
// WRONG — passing pre-computed contracts (frozen at registration time)
$contracts = $router->contracts(); // computed at rest_api_init time
OpenApiRouteRegistrar::register(
restNamespace: 'myapp/v1',
contractsProvider: fn () => $contracts,
);
// → if routes are added after this point, doc misses them
// RIGHT — lazy callable
contractsProvider: fn () => $router->contracts(openApiOnly: true),
// WRONG — strictSchemas: true with extension-supplied DTOs
$exporter->export($contracts, ['strictSchemas' => true]);
// Some extension's $ref to '#/components/schemas/PluginXThing' isn't in components → throws.
// RIGHT — for extensible APIs, leave strictSchemas: false (default)
// OR explicitly merge in extension components
// WRONG — securitySchemes without globalSecurity
options: [
'securitySchemes' => ['bearerAuth' => [...]],
// no globalSecurity
]
// → security schemes defined but no operation references them; tooling shows "auth: none"
// RIGHT — declare both
options: [
'securitySchemes' => ['bearerAuth' => [...]],
'globalSecurity' => [['bearerAuth' => []]],
]
// WRONG — invalid restNamespace format
OpenApiRouteRegistrar::register(
restNamespace: 'myapp', // 🔴 missing version
);
// → InvalidArgumentException
// RIGHT — vendor/version format
restNamespace: 'myapp/v1'
// WRONG — permissionCallback as a string (function name as string IS callable in PHP, so this works,
// but the registrar's signature expects callable; pass the closure form for clarity)
options: ['permissionCallback' => 'is_user_logged_in']
// RIGHT — closure
options: ['permissionCallback' => static fn (): bool => is_user_logged_in()]
// WRONG — exposing internal routes in public OpenAPI
OpenApiRouteRegistrar::register(
restNamespace: 'myapp/v1',
contractsProvider: fn () => $router->contracts(), // includes internal routes
options: ['permissionCallback' => fn () => true],
);
// Public consumers see /api/internal/admin-tools-export.
// RIGHT — filter via openApiOnly + per-route opt-out
$router->get('/internal/admin', $h)->meta(['openApiOnly' => false]);
contractsProvider: fn () => $router->contracts(openApiOnly: true),
Cross-references
- Run
br-routesfor->protectedByMiddleware($security)and->publicRoute()— they affect per-operation OpenAPI security. - Run
br-resource-cpt/br-resource-tablefor Resource-generated route contracts that auto-feed the exporter. - Run
br-woo-routesforwooOpenApiComponents()— WC schema bundle. - See
bd-better-route-bridgein the better-data folder forBetterRouteBridge::openApiComponents([...])— DTO schemas.
What this skill does NOT cover
- OpenAPI 3.0.x export. Library emits 3.1.0; downgrading requires external conversion.
- AsyncAPI / GraphQL export. Out of scope.
- Postman collection import. Most tooling can import OpenAPI 3.1.0; collections specific to Postman would need a separate exporter.
- Live UI rendering (Swagger UI, Redoc, Stoplight). Library emits the document; mounting a UI is the consumer's choice.
- API versioning across multiple
restNamespace(e.g./v1vs/v2docs). Run two registrars or merge contracts manually. - Schema diffing / breaking-change detection. External tooling (oasdiff, openapi-diff).
References
- OpenApiExporter: [libraries/better-route/src/OpenApi/OpenApiExporter.php:33](OpenApiExporter.php) —
export(array $contracts, array $options): array. Options docblock at lines 22-30. DefaultstrictSchemas: falseat line 41. - OpenApiRouteRegistrar: [libraries/better-route/src/OpenApi/OpenApiRouteRegistrar.php:30-44](OpenApiRouteRegistrar.php) —
register(restNamespace, contractsProvider, options). Default permission at line 39-41 (current_user_can('manage_options')). - restNamespace format guard: [OpenApiRouteRegistrar.php:121](OpenApiRouteRegistrar.php) — must be
'vendor/version'. - Source-type detection: [OpenApiRouteRegistrar.php:99](OpenApiRouteRegistrar.php) — accepts Router / Resource / contract list, throws on others.
- WooCommerce components: [libraries/better-route/src/Integration/Woo/WooOpenApiComponents.php](WooOpenApiComponents.php) — registered via
BetterRoute::wooOpenApiComponents(). - BetterRouteBridge::openApiComponents (better-data): [libraries/better-data/src/Route/BetterRouteBridge.php:349](BetterRouteBridge.php).
- OpenAPI 3.1 spec: https://spec.openapis.org/oas/v3.1.0.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Lonsdale201
- Source: Lonsdale201/wp-agent-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.