Module 20 — Phases 15-17: API Stability, SDK/CLI, Mobile/Desktop Final Review
Phase 15: API stability classification
No dedicated "stable vs. beta/experimental" endpoint-tagging scheme
exists, and none is needed to be added: docs/architecture/api-gateway.md's
"Versioning policy" section documents a deliberate no-URI-versioning
decision (no /v1/ prefix, no stability tags in Swagger) in favor of
strict additive-only evolution — new endpoints/fields are always safe to
add; breaking changes require a new path segment and a docs/releases/
entry. This is a considered, pre-existing policy, not a gap.
Correction to this module's own prior framing: earlier in this
module I described POST /public-api/keys/:id/rotate as "new public API
surface added this module." That was inaccurate — the route has existed
since Module 15 (task #336, commit fc90106); what Module 20 actually
added was the API_KEY_ENDPOINTS.rotate shared constant (a frontend
convenience) and, this phase, its first SDK/CLI wrappers. Corrected here
for the final report's accuracy.
Phase 16: SDK/CLI parity — real gap found and fixed
An audit found that the public API keys resource (GET/POST /public-api/keys,
DELETE /public-api/keys/:id, POST /public-api/keys/:id/rotate — live
since Module 15) had zero coverage in any of the three SDKs
(packages/sdk-typescript, sdks/python, sdks/go) or the CLI
(packages/cli). This predates Module 20 (it's a Module 15-era gap that
was simply never closed), but it's real and worth closing before a v1.0
cut, and additive-only per the versioning policy above — safe, low-risk
work.
Fixed by mirroring each codebase's own existing per-resource pattern exactly (no new abstractions introduced):
packages/sdk-typescript/src/resources/api-keys.resource.ts(new) —ApiKeysResource.{list,create,revoke,rotate}, wired intoclient.tsand re-exported fromindex.ts.sdks/python/pentesthub_ai/resources/api_keys.py(new) —ApiKeysResourcewith the same four methods, snake_case params/ camelCase JSON body matching every other Python resource; wired intoresources/__init__.pyandclient.py; README's "Scope (v1)" resource list updated to 15 entries per the0021-post-m18-sdk-reconciliation.mdconvention.sdks/go/api_keys.go(new) —ApiKeysResourceplusApiKey/CreateApiKeyRequest/CreateApiKeyResult/RotateApiKeyResultstructs mirroring the shared-package DTOs field-for-field; wired intoclient.go; README updated identically to the Python SDK's.packages/cli/src/commands/api-keys.ts(new) —api-keys list/create/ revoke/rotatesubcommands over the CLI's ownHttpClient, wired intobin/pentesthub.ts's dispatch switch and help text.
No SDK-side error handling addition was needed for QUOTA_EXCEEDED
(the code Phase 8's recon/vuln quota enforcement can now return): it was
already a documented closed error code in packages/shared/src/errors.ts
from the pre-existing AI budget enforcer, and every SDK's HttpClient
already retries generically on any 429 — no special-casing exists or is
needed anywhere.
Separately noted, not fixed: the audit also surfaced that
QUOTA_EXCEEDED is thrown as HTTP 403 by the older, Module-10-era
apps/api/src/modules/enterprise/quota-enforcement.util.ts (entity-count
limits — max workspaces/members/teams/etc.) but as HTTP 429 by the
newer, Module-15-era QuotaService.enforce() (periodic usage-metering
limits — the mechanism Phase 8's recon/vuln fix uses). These are two
different subsystems for two different kinds of limit (entity-count caps
vs. rate/usage caps) that happen to share the same error-code string.
429 is the more semantically correct HTTP status for a usage/rate
quota per the HTTP spec; 403 is defensible for "you've hit a hard
account-size limit." Reconciling the two is a real but low-priority,
pre-existing (not introduced by this module) inconsistency — changing
either now risks an unrelated-module behavior change this late in the
release cycle for a cosmetic-severity issue, so it's documented here as
a known limitation for a future module rather than fixed in this pass.
Phase 17: Mobile/Desktop final review
No changes needed. apps/desktop's recon/scanner tooling
(src-tauri/src/recon/, .../scanner/) runs security tools locally
via its own ToolRegistry and never calls the cloud recon/vuln
job-creation endpoints Phase 8 touched — confirmed by grep (zero
references to RECON_JOB_ENDPOINTS/vuln-job endpoints or any API-keys
endpoint in apps/desktop or apps/mobile). docs/reviews/0016-m18-cli-desktop-mobile-sdk-reliability-audit.md
already verified both apps' reliability layers with no open action
items; this narrow follow-up confirms none of this module's four
concrete changes (IDOR fix, webhook ordering, quota enforcement, API
keys SDK/CLI parity) touch either app's code paths.
Verification
- TypeScript SDK/CLI:
tsc --noEmit --noResolve --experimentalDecoratorsstructural check on all new/edited files — clean beyond the standard disclosed--noResolvenoise (module-not-found, and oneinstanceof-narrowing-lost-under---noResolveartifact inbin/pentesthub.ts's pre-existing top-level error handler, confirmed unrelated to the new commands by inspection). - Python SDK:
python3 -m compileallclean; live-instantiatedPentestHubClientand confirmedclient.api_keysexposes all four methods; existingtests/test_client.pysuite still passes (5/5). - Go SDK:
go/gofmtunavailable in this sandbox (same disclosed, longstanding toolchain limitation as every prior module) — NOT compiler-verified. Manually cross-checkedapi_keys.go'sHttpClient.Get/Post/Deletecall signatures againsthttp_client.go's actual method signatures (Get(ctx, path, query url.Values, out),Post(ctx, path, body, out),Delete(ctx, path, out)) and confirmed exact match, and confirmedQuery()already skips empty-string map values (so passing an emptyworkspaceIDthroughList()correctly omits the query param rather than sending?workspaceId=) — the same patternplugins.go'sListInstallationsalready uses for an identical optional-workspace-filter case.