Delivers the full Phase 9 external API layer: - Versioned REST API (/api/v2/) with OpenAPI 3.1 spec; enum arrays for widget_type, event_type, annotation category drawn live from registry tables - OAuth 2.0 client credentials flow (/api/v2/token); hub:*:write scopes gated on active HubCapabilityManifest FK - API key management: SHA256-hashed tokens, key_prefix for display, one-time reveal on creation, revocation support - TypeScript and Python consumer SDKs generated from registry tables (/api/v2/sdk/ihf-client.ts, /api/v2/sdk/ihf-client.py) - Webhook delivery: HMAC-SHA256 signing, append-only webhook_deliveries, fire-and-forget dispatch via forkIO, 3-retry logic - Admin API dashboard with 24h stats (request count, error rate, last seen) - Rate limiting (per-minute) and daily quota enforcement via api_request_log - Schema migration: api_consumers, api_keys, webhook_subscriptions (CHECK constraint on 6 framework lifecycle topics), webhook_deliveries (append-only trigger), api_request_log - ARCHITECTURE-LAYERS.md scorecard: 3.34 → 3.41 (approaching Strong) - contracts/functional/interaction-reporting-v1.md extended with Phase 9 endpoint catalogue and 422 validation error format GAAF: no bare TEXT discriminators; webhook event_type uses CHECK constraint over 6 allowed framework lifecycle topic strings (not widget event types). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
27 KiB
id, type, title, domain, repo, status, owner, topic_slug, created, updated, state_hub_sync, state_hub_workstream_id
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | state_hub_sync | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|---|
| IHUB-WP-0010 | workplan | IHF Phase 9 — External API Surface and Consumer SDKs | inter_hub | inter-hub | active | custodian | inter_hub | 2026-04-01 | 2026-04-01 | done | c6c6e87f-e145-4bc4-9881-61f92b14d4de |
IHF Phase 9 — External API Surface and Consumer SDKs
Goal
Make the IHF consumable by systems outside the reference IHP implementation. Phase 8 established federated governance within a single deployment. Phase 9 exposes that governance state as a stable, versioned, authenticated REST API and ships consumer SDKs that make integration a day's work rather than a project.
Background
Phases 1–8 and IHUB-WP-0009 (GAAF Compliance Foundation) are complete. All Phase 9 entry gates are satisfied:
- Four type registries seeded and validated in controllers ✓
HubCapabilityManifesttable and activation workflow operational ✓/contracts/directory with Core and Functional contract artifacts ✓ARCHITECTURE-LAYERS.mdscorecard at ≥3.3 ✓ (actual: 3.34)- Architectural fitness functions in CI ✓
docs/domain-hub-extension-guide.mdpublished ✓
The type registries established in WP-0009 are the critical dependency: the
OpenAPI 3.1 spec must enumerate widget_type, event_type, and category
as finite enum arrays derived from those registries — not unconstrained
strings. Building the API without this would produce an incorrect contract.
Reference: specs/InteractionHubFrameworkSpecification_v0.2.md §Phase 9.
GAAF Architectural Constraints
All new code in this workplan must comply with:
- No bare TEXT type discriminators —
api_consumers.consumer_type(if added) must reference a registry or carry a CHECK constraint. - ApiConsumer must declare capability scope — if a consumer is a domain
hub, its
hub_capability_manifest_idFK must be set. Non-hub consumers leave it NULL; that is not an exception. - Append-only invariant is permanent — no migration in this workplan may
add UPDATE or DELETE capability to
interaction_eventsoroutcome_signals. - Core tables are frozen —
widgets,interaction_events,annotations,hubs, and Phase 1–4 dependents must not gain columns without a corresponding/contracts/core/update.
Data Artifacts Introduced
ApiConsumer, ApiKey, WebhookSubscription, WebhookDelivery
Schema additions:
api_consumerstable withhub_capability_manifest_idFK (nullable)api_keystable linked toapi_consumerswebhook_subscriptionstablewebhook_deliveriestableapi_request_logtable (for usage dashboard and rate limiting)
Tasks
T01 — Schema: ApiConsumer, ApiKey, WebhookSubscription, WebhookDelivery
id: IHUB-WP-0010-T01
status: done
priority: high
state_hub_task_id: "4db51957-5aaf-4834-88f8-14c972b32136"
Add all new tables required by Phase 9. This is the only task that touches
Application/Schema.sql and migrations.
-
Schema additions in
Application/Schema.sql:-- api_consumers: external systems that authenticate against /api/v2/ -- hub_capability_manifest_id: set when the consumer is a domain hub; -- NULL for third-party tools that authenticate without a manifest. CREATE TABLE api_consumers ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, name TEXT NOT NULL, description TEXT, hub_capability_manifest_id UUID REFERENCES hub_capability_manifests(id), rate_limit_per_minute INTEGER NOT NULL DEFAULT 60, quota_per_day INTEGER NOT NULL DEFAULT 10000, quota_resets_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (date_trunc('day', NOW() AT TIME ZONE 'UTC') + interval '1 day'), is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); -- api_keys: bearer tokens for consumer authentication CREATE TABLE api_keys ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, api_consumer_id UUID NOT NULL REFERENCES api_consumers(id) ON DELETE CASCADE, key_prefix TEXT NOT NULL, -- first 8 chars of the key, for display key_hash TEXT NOT NULL, -- bcrypt/sha256 hash; never store plain scopes TEXT NOT NULL DEFAULT '', -- space-separated OAuth-style scopes expires_at TIMESTAMP WITH TIME ZONE, revoked_at TIMESTAMP WITH TIME ZONE, last_used_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE UNIQUE INDEX api_keys_prefix_idx ON api_keys (key_prefix); CREATE INDEX api_keys_consumer_idx ON api_keys (api_consumer_id); -- webhook_subscriptions: consumer event subscriptions CREATE TABLE webhook_subscriptions ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, api_consumer_id UUID NOT NULL REFERENCES api_consumers(id) ON DELETE CASCADE, event_type TEXT NOT NULL REFERENCES event_type_registry(name), target_url TEXT NOT NULL, secret TEXT NOT NULL, -- HMAC signing secret; never expose in responses is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE INDEX webhook_subs_consumer_idx ON webhook_subscriptions (api_consumer_id); CREATE INDEX webhook_subs_event_type_idx ON webhook_subscriptions (event_type); -- webhook_deliveries: delivery attempt log (append-only) CREATE TABLE webhook_deliveries ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, webhook_subscription_id UUID NOT NULL REFERENCES webhook_subscriptions(id), payload JSONB NOT NULL, attempted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, status TEXT NOT NULL CHECK (status IN ('pending','delivered','failed')), response_code INTEGER, latency_ms INTEGER, error_message TEXT ); -- Append-only trigger for webhook_deliveries CREATE OR REPLACE FUNCTION webhook_deliveries_no_update() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'webhook_deliveries is append-only'; END; $$; CREATE TRIGGER webhook_deliveries_no_update BEFORE UPDATE ON webhook_deliveries FOR EACH ROW EXECUTE FUNCTION webhook_deliveries_no_update(); -- api_request_log: usage tracking for dashboard and rate limiting CREATE TABLE api_request_log ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, api_consumer_id UUID REFERENCES api_consumers(id), endpoint TEXT NOT NULL, method TEXT NOT NULL, status_code INTEGER NOT NULL, latency_ms INTEGER, requested_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE INDEX api_request_log_consumer_idx ON api_request_log (api_consumer_id, requested_at DESC); -
Write migration:
Application/Migration/1743000001-api-phase9-schema.sqlcontaining the DDL above. Run viamigrate. -
Register new types in
Web/Types.hs:data ApiConsumer,data ApiKey,data WebhookSubscription,data WebhookDelivery,data ApiRequestLog- Include
HasFieldinstances following IHP conventions.
-
Add routes in
Web/Routes.hsfor:ApiConsumersController(admin CRUD)ApiKeysController(admin CRUD)WebhookSubscriptionsController(admin CRUD)Api.V2.*controllers (added in T02/T03)
Exit criteria: migrate runs cleanly; Types.hs compiles; no existing
tests broken.
T02 — REST read API /api/v2/ for all core IHF artifact types
id: IHUB-WP-0010-T02
status: done
priority: high
state_hub_task_id: "5a4fc7d1-f930-40f9-bbcf-323aaa717191"
Expose all core IHF artifact types as read-only JSON endpoints under
/api/v2/. All endpoints require a valid ApiKey Bearer token.
-
Create
Web/Controller/Api/V2/directory with a shared auth helper:-- Web/Controller/Api/V2/Auth.hs module Web.Controller.Api.V2.Auth where import IHP.Prelude import Web.Types import qualified Crypto.Hash.SHA256 as SHA256 import qualified Data.ByteString.Base16 as Base16 -- | Extract and validate Bearer token from Authorization header. -- Returns the ApiConsumer on success or halts with 401. requireApiConsumer :: (?context :: ControllerContext) => IO ApiConsumer -
Implement read controllers for each artifact type. Pattern:
-- Web/Controller/Api/V2/WidgetsController.hs instance Controller Api.V2.WidgetsController where action Api.V2.IndexWidgetsAction = do consumer <- requireApiConsumer widgets <- query @Widget |> orderByDesc #createdAt |> paginate |> fetch renderJson (map widgetToJson widgets) action Api.V2.ShowWidgetAction { widgetId } = do consumer <- requireApiConsumer widget <- fetch widgetId renderJson (widgetToJson widget) -
Implement controllers for:
Api.V2.WidgetsController(index, show)Api.V2.InteractionEventsController(index, show; filter by widget_id, hub_id, event_type)Api.V2.AnnotationsController(index, show; filter by widget_id, category)Api.V2.RequirementCandidatesController(index, show)Api.V2.DecisionRecordsController(index, show)Api.V2.DeploymentRecordsController(index, show)Api.V2.OutcomeSignalsController(index, show)
-
JSON serialisation: each
*ToJsonfunction must produce camelCase field names. Type discriminator fields (widget_type,event_type,category) emit the registry name string as-is. -
Pagination:
?page=N&per_page=Mquery params; default 50 per page; max 200. Response envelope:{ "data": [...], "meta": { "page": N, "per_page": M, "total": T } }. -
Authentication failure: 401 JSON
{ "error": "Unauthorized", "code": "invalid_api_key" }.
Exit criteria: All seven read endpoints return 200 JSON with a valid Bearer token; return 401 without one; pagination meta renders in all responses.
T03 — REST write API: submit interaction events and annotations
id: IHUB-WP-0010-T03
status: done
priority: high
state_hub_task_id: "ade7f62b-b27d-4edf-b91c-b785057d269d"
Allow external consumers to submit interaction events and annotations via
POST /api/v2/interaction-events and POST /api/v2/annotations.
-
Extend
Api.V2.InteractionEventsControllerwithCreateInteractionEventAction:POST /api/v2/interaction-events Content-Type: application/json Authorization: Bearer <token> { "widgetId": "<uuid>", "eventType": "clicked", "viewContext": "dashboard", "metadata": {} } -
Validation pipeline for
event_type:- Check
event_typeexists inevent_type_registry. If not → HTTP 422:{ "error": "Unregistered event type", "code": "unregistered_event_type", "value": "<submitted_value>", "registry": "/api/v2/event-types" } - If the consumer has a
hub_capability_manifest_id, additionally check the type is indeclared_event_typeson the active manifest. If not → HTTP 422 with"code": "event_type_not_in_manifest". - Framework-level types (owned by no hub) are always allowed.
- Check
-
Extend
Api.V2.AnnotationsControllerwithCreateAnnotationAction:POST /api/v2/annotations Body: { "widgetId": "<uuid>", "category": "ux-friction", "body": "..." }Validate
categoryagainstannotation_category_registrywith the same 422 pattern. -
On success, return HTTP 201 with the created record JSON.
-
Add
GET /api/v2/event-typesandGET /api/v2/widget-typesandGET /api/v2/annotation-categories— read the respective registry tables and return the list of registered names with description and owner_hub_id. These endpoints are unauthenticated (public enumeration).
Exit criteria: Valid POST creates a record and returns 201; unregistered
event_type returns 422 with registry URL in response; unauthenticated POST
returns 401; registry listing endpoints return 200 without auth.
T04 — OpenAPI 3.1 spec generation with type registry enumerations
id: IHUB-WP-0010-T04
status: done
priority: high
state_hub_task_id: "f5ba93af-6772-4b87-b1b5-0c79b46486c8"
Generate a valid OpenAPI 3.1 JSON document at GET /api/v2/openapi.json that
accurately describes all Phase 9 endpoints. The critical constraint: type
discriminator fields must carry enum arrays, not unconstrained string.
-
Create
Web/Controller/Api/V2/OpenApiController.hs. This controller builds and renders the OpenAPI document in-process (no code-generation tool needed for v1; a Haskell value → JSON approach is fine). -
The spec structure:
openapi: "3.1.0" info: title: Interaction Hub Framework API version: "2.0" description: | IHF external API. See /contracts/functional/interaction-reporting-v1.md for the human-readable contract companion. servers: - url: /api/v2 paths: /widgets: ... /interaction-events: ... ... components: schemas: WidgetType: type: string enum: [<values from widget_type_registry>] EventType: type: string enum: [<values from event_type_registry>] AnnotationCategory: type: string enum: [<values from annotation_category_registry>] -
The enum arrays must be queried live from the registry tables at request time. The spec is generated data, not a static file. This ensures the spec stays in sync with the registries automatically.
-
Add
x-ihf-contract: /contracts/functional/interaction-reporting-v1.mdas a top-level extension field. -
Serve at
GET /api/v2/openapi.json(unauthenticated — it is a public contract). Also serveGET /api/v2/openapi.yamlas a convenience. -
Add a Swagger UI at
GET /api/v2/docsthat loads the generated spec. Use the CDN-served Swagger UI (static HTML that loads swagger-ui from unpkg; no npm build step required).
Exit criteria: /api/v2/openapi.json is valid OpenAPI 3.1; widget_type,
event_type, and category fields reference $ref schemas with enum
arrays populated from live registries; a new type added to any registry
appears in the spec on the next request.
T05 — OAuth 2.0 client credentials flow
id: IHUB-WP-0010-T05
status: done
priority: high
state_hub_task_id: "b8c5c9d5-7d9c-42ca-b679-0f7514b61910"
Implement the OAuth 2.0 client credentials grant so that external consumers can obtain short-lived access tokens by presenting their client ID and secret.
-
POST /api/v2/tokenendpoint:Content-Type: application/x-www-form-urlencoded Body: grant_type=client_credentials&client_id=<id>&client_secret=<secret>&scope=hub:dev-hub:readResponse:
{ "access_token": "<opaque>", "token_type": "Bearer", "expires_in": 3600 } -
Implementation approach: opaque tokens stored in
api_keys(reuse the existing table; addtoken_type TEXT DEFAULT 'bearer'andexpires_at). JWTs are acceptable but opaque tokens are simpler for v1 and avoid key management complexity. -
Scope validation:
hub:{slug}:read— allowed for all active consumershub:{slug}:write— requires the consumer'shub_capability_manifest_idto reference an active manifest for that hub slugframework:read— allowed for all- Unknown scopes → 400
invalid_scope
-
Token lifetime: 3600 seconds (1 hour). Refresh tokens are out of scope for Phase 9.
-
Standard OAuth error responses: 400 with
errorfield (invalid_client,invalid_grant,invalid_scope,unsupported_grant_type). -
The existing ApiKey Bearer auth in T02/T03 should accept both pre-created static keys AND tokens issued by this endpoint. Both are rows in
api_keys.
Exit criteria: Full client credentials flow works end-to-end; hub-scoped write is rejected without a manifest; token expiry is enforced.
T06 — API consumer and API key management UI
id: IHUB-WP-0010-T06
status: done
priority: medium
state_hub_task_id: "64f48e86-522a-4566-a91a-ce840d3f2633"
Admin-only web UI for managing API consumers and their keys. Extends the existing admin section pattern established in Phase 8.
-
Web/Controller/ApiConsumersController.hs:- CRUD: index, show, new, create, edit, update, delete
- Delete: soft-delete by setting
is_active = FALSE; do not destroy records
-
Web/Controller/ApiKeysController.hs:- Index (scoped to a consumer), new, create, revoke (sets
revoked_at) - Key generation: create a random 32-byte key, store
key_prefix(first 8 hex chars) andkey_hash(SHA-256). Display the full key once on creation — never again. Show a "copy this now" alert banner.
- Index (scoped to a consumer), new, create, revoke (sets
-
Views (
Web/View/ApiConsumers/,Web/View/ApiKeys/):- Consumer index: name, linked manifest (if set), active/inactive badge, rate limit, quota, key count
- Consumer show: details + key list + webhook subscriptions
- Consumer form: name, description, manifest selector (optional), rate limit, quota fields
- Key create success: one-time display of full key value
-
Link "API Consumers" from the existing admin navigation.
-
Domain hub consumer display: if
hub_capability_manifest_idis set, show the manifest name, status badge, and a link to its show page.
Exit criteria: Full CRUD works; key is shown once on creation; revoked keys are rejected by auth middleware; consumer linked to manifest shows manifest details.
T07 — TypeScript SDK generation from type registries
id: IHUB-WP-0010-T07
status: done
priority: medium
state_hub_task_id: "8b7ecc7b-ffbe-449e-a33a-8ac2f029fc9f"
Generate a TypeScript client SDK that exports typed enums derived from the
live type registries, plus typed fetch wrappers for /api/v2/ endpoints.
-
Create
Web/Controller/Api/V2/SdkController.hswith aGenerateTsSdkActionthat:- Queries
widget_type_registry,event_type_registry,annotation_category_registry - Renders
Web/View/Api/V2/TsSdk.hswhich produces a.tsfile
- Queries
-
The generated TypeScript file (
GET /api/v2/sdk/ihf-client.ts):// Auto-generated by IHF. Do not edit manually. // Regenerate by fetching /api/v2/sdk/ihf-client.ts export enum WidgetType { Button = "button", DataTable = "data-table", // ... all entries from widget_type_registry } export enum EventType { Clicked = "clicked", Viewed = "viewed", // ... all entries from event_type_registry } export enum AnnotationCategory { UxFriction = "ux-friction", // ... all entries from annotation_category_registry } export interface IhfApiOptions { baseUrl: string; bearerToken: string; } export class IhfClient { constructor(private opts: IhfApiOptions) {} async getWidgets(params?: { page?: number; perPage?: number }) { ... } async getInteractionEvents(params?: { widgetId?: string; eventType?: EventType }) { ... } async submitInteractionEvent(body: { widgetId: string; eventType: EventType; viewContext: string; metadata?: object }) { ... } async submitAnnotation(body: { widgetId: string; category: AnnotationCategory; body: string }) { ... } } -
Serve the raw
.tsfile atGET /api/v2/sdk/ihf-client.ts(MIME typeapplication/typescript). This is always regenerated from live registries. -
Also serve a static SDK download page at
GET /api/v2/sdklisting available SDKs with download links and usage instructions.
Exit criteria: /api/v2/sdk/ihf-client.ts is valid TypeScript; enum
values match the live registries; a new registry entry appears in the SDK on
next request.
T08 — Python SDK generation from type registries
id: IHUB-WP-0010-T08
status: done
priority: medium
state_hub_task_id: "a943d34b-9898-4aa1-a505-9184a6a476b0"
Generate a Python client SDK (GET /api/v2/sdk/ihf-client.py) following the
same live-generation approach as the TypeScript SDK.
-
Extend
Web/Controller/Api/V2/SdkController.hswith aGeneratePySdkAction. -
The generated Python file:
# Auto-generated by IHF. Do not edit manually. # Regenerate: curl <base>/api/v2/sdk/ihf-client.py > ihf_client.py from enum import Enum from typing import Optional import urllib.request, json class WidgetType(str, Enum): BUTTON = "button" DATA_TABLE = "data-table" # ... all entries from widget_type_registry class EventType(str, Enum): CLICKED = "clicked" VIEWED = "viewed" # ... all entries from event_type_registry class AnnotationCategory(str, Enum): UX_FRICTION = "ux-friction" # ... all entries from annotation_category_registry class IhfClient: def __init__(self, base_url: str, bearer_token: str): ... def get_widgets(self, page: int = 1, per_page: int = 50) -> dict: ... def get_interaction_events(self, widget_id: Optional[str] = None, event_type: Optional[EventType] = None) -> dict: ... def submit_interaction_event(self, widget_id: str, event_type: EventType, view_context: str, metadata: Optional[dict] = None) -> dict: ... def submit_annotation(self, widget_id: str, category: AnnotationCategory, body: str) -> dict: ... -
The enum identifier names follow Python conventions (UPPER_SNAKE_CASE). The values are the registry strings. Use
str, Enumso instances compare equal to their string value. -
Serve at
GET /api/v2/sdk/ihf-client.py(MIMEtext/x-python).
Exit criteria: /api/v2/sdk/ihf-client.py is syntactically valid Python;
enum values match live registries; Python SDK page linked from /api/v2/sdk.
T09 — Webhook delivery for interaction events, candidate creation, and decision records
id: IHUB-WP-0010-T09
status: done
priority: medium
state_hub_task_id: "249e4de7-a0fb-4325-809b-a896bb807c17"
Allow consumers to subscribe to push notifications when governance events occur.
At minimum: interaction_event.created and requirement_candidate.created.
-
Web/Controller/WebhookSubscriptionsController.hs:- CRUD scoped to an
ApiConsumer event_typevalidated againstevent_type_registry
- CRUD scoped to an
-
Delivery trigger: after inserting an
InteractionEventrow, dispatch delivery for all active subscriptions matchingevent_type. Use IHP's background job mechanism (IHP.Job):-- Web/Job/WebhookDeliveryJob.hs instance Job WebhookDeliveryJob where perform WebhookDeliveryJob { .. } = do -- Build payload, sign with subscription.secret (HMAC-SHA256) -- POST to target_url -- Insert WebhookDelivery record with result -
HMAC signing: add
X-IHF-Signature: sha256=<hex>header to each delivery request. Consumers verify this header to authenticate the webhook. -
Retry policy: on HTTP 4xx/5xx or network error, retry up to 3 times with exponential backoff (5s, 25s, 125s). Record each attempt separately in
webhook_deliveries. -
Add the same delivery trigger for
RequirementCandidatecreation andDecisionRecordcreation. -
Webhook subscriptions management linked from the consumer show page.
Exit criteria: A subscription to interaction_event.created receives a
POST with HMAC signature when an event is inserted via the API; delivery is
recorded in webhook_deliveries; failed deliveries are retried up to 3 times.
T10 — API usage dashboard
id: IHUB-WP-0010-T10
status: done
priority: medium
state_hub_task_id: "fedae9a8-4cdc-41e2-b692-3cc848b9dd6f"
Admin view that surfaces per-consumer API usage metrics in near real-time.
-
Request logging middleware: for all
/api/v2/requests, insert a row intoapi_request_logafter the response is sent (non-blocking). Capture:api_consumer_id,endpoint,method,status_code,latency_ms,requested_at. -
Web/Controller/ApiDashboardController.hswithShowApiDashboardAction:- Aggregate query: per-consumer request count (last 24h), error rate (4xx+5xx / total), last-seen timestamp, top 5 endpoints
- Uses IHP
AutoRefreshfor push updates on new log rows
-
View (
Web/View/ApiDashboard/Show.hs):- Summary table: consumer name, requests (24h), error rate %, last seen, manifest linked (✓/–)
- Time-series sparkline (optional; use a simple count-per-hour query if rendering inline)
- Error breakdown panel: group by status code
-
Link "API Dashboard" from the admin nav.
Exit criteria: Dashboard renders for all consumers; request counts and error rates update in near-real-time via AutoRefresh; last-seen shows correct timestamp.
T11 — Rate limiting, quota management, and close-out
id: IHUB-WP-0010-T11
status: done
priority: medium
state_hub_task_id: "32befd50-af80-4bfd-affd-cbcf5e6bfac5"
Enforce per-consumer limits and close out the workplan.
Rate limiting and quota:
-
Middleware (applied to all
/api/v2/routes) before action dispatch:- Count requests in the last 60 seconds from
api_request_logfor the consumer. If> rate_limit_per_minute→ HTTP 429 withRetry-After: <seconds>header. - Count requests today (since
quota_resets_at - 1 day) for the consumer. If> quota_per_day→ HTTP 429 withX-Quota-Resets-At: <iso8601>header. - Both return JSON
{ "error": "Rate limit exceeded", "code": "rate_limited" }or"code": "quota_exceeded".
- Count requests in the last 60 seconds from
-
Quota reset: add a daily job
Web/Job/QuotaResetJob.hsthat runs at 00:00 UTC and updatesquota_resets_atto the next midnight for all consumers.
Close-out:
-
Update
CLAUDE.md:- Change
Active Workplansection to point to the next workplan (Phase 10: IHUB-WP-0011 when created) or mark Phase 9 as complete. - Add IHUB-WP-0010 to the "Completed workplans" list.
- Change
-
Update
SCOPE.mdto reflect Phase 9 completion. -
Update
ARCHITECTURE-LAYERS.md:- Add
ApiConsumer,ApiKey,WebhookSubscription,WebhookDelivery,ApiRequestLogto the Extensions layer. - Update scorecard (Phase 9 adds external API surface — likely improves Cross-layer and Configuration scores).
- Set next review date.
- Add
-
Update
/contracts/functional/interaction-reporting-v1.mdto reference the live OpenAPI spec URL (/api/v2/openapi.json) and confirm the Phase 9 endpoint list. -
Mark all exit criteria verified in this workplan file.
-
Sync task completions to state hub.
Exit criteria (Phase 9 complete when all of these are true):
- All core IHF artifact types are readable via
/api/v2/ - Interaction events and annotations are writable via
/api/v2/ - OpenAPI spec generated;
widget_type,event_type,categorycarryenumarrays from live registries - TypeScript SDK at
/api/v2/sdk/ihf-client.tsexports correct enums - Python SDK at
/api/v2/sdk/ihf-client.pyexports correct enums - Webhook delivery confirmed for
interaction_event.createdandrequirement_candidate.created - API usage dashboard renders correctly with AutoRefresh
- OAuth client credentials flow works end-to-end
- Submission of an unregistered
event_typereturns HTTP 422 with registry-referenced error - Rate limiting returns 429 with
Retry-After - CLAUDE.md updated; IHUB-WP-0010 listed as complete