Files
inter-hub/workplans/IHUB-WP-0010-ihf-phase9-external-api.md
tegwick 26708ba799
Some checks failed
Build and Deploy / build-push-deploy (push) Has been cancelled
chore: close IHUB-WP-0010 status
2026-06-07 17:42:26 +02:00

28 KiB
Raw Permalink Blame History

id, type, title, domain, repo, status, owner, topic_slug, created, updated, completed, state_hub_sync, state_hub_workstream_id
id type title domain repo status owner topic_slug created updated completed state_hub_sync state_hub_workstream_id
IHUB-WP-0010 workplan IHF Phase 9 — External API Surface and Consumer SDKs inter_hub inter-hub done custodian inter_hub 2026-04-01 2026-06-07 2026-06-07 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 18 and IHUB-WP-0009 (GAAF Compliance Foundation) are complete. All Phase 9 entry gates are satisfied:

  • Four type registries seeded and validated in controllers ✓
  • HubCapabilityManifest table and activation workflow operational ✓
  • /contracts/ directory with Core and Functional contract artifacts ✓
  • ARCHITECTURE-LAYERS.md scorecard at ≥3.3 ✓ (actual: 3.34)
  • Architectural fitness functions in CI ✓
  • docs/domain-hub-extension-guide.md published ✓

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:

  1. No bare TEXT type discriminatorsapi_consumers.consumer_type (if added) must reference a registry or carry a CHECK constraint.
  2. ApiConsumer must declare capability scope — if a consumer is a domain hub, its hub_capability_manifest_id FK must be set. Non-hub consumers leave it NULL; that is not an exception.
  3. Append-only invariant is permanent — no migration in this workplan may add UPDATE or DELETE capability to interaction_events or outcome_signals.
  4. Core tables are frozenwidgets, interaction_events, annotations, hubs, and Phase 14 dependents must not gain columns without a corresponding /contracts/core/ update.

Data Artifacts Introduced

ApiConsumer, ApiKey, WebhookSubscription, WebhookDelivery

Schema additions:

  • api_consumers table with hub_capability_manifest_id FK (nullable)
  • api_keys table linked to api_consumers
  • webhook_subscriptions table
  • webhook_deliveries table
  • api_request_log table (for usage dashboard and rate limiting)

Close-out Correction - 2026-06-07

State Hub showed IHUB-WP-0010 as active even though all eleven task rows were already done. The workplan frontmatter and final exit checklist were corrected to reflect the completed Phase 9 state.


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.

  1. 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);
    
  2. Write migration: Application/Migration/1743000001-api-phase9-schema.sql containing the DDL above. Run via migrate.

  3. Register new types in Web/Types.hs:

    • data ApiConsumer, data ApiKey, data WebhookSubscription, data WebhookDelivery, data ApiRequestLog
    • Include HasField instances following IHP conventions.
  4. Add routes in Web/Routes.hs for:

    • 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.

  1. 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
    
  2. 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)
    
  3. 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)
  4. JSON serialisation: each *ToJson function must produce camelCase field names. Type discriminator fields (widget_type, event_type, category) emit the registry name string as-is.

  5. Pagination: ?page=N&per_page=M query params; default 50 per page; max 200. Response envelope: { "data": [...], "meta": { "page": N, "per_page": M, "total": T } }.

  6. 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.

  1. Extend Api.V2.InteractionEventsController with CreateInteractionEventAction:

    POST /api/v2/interaction-events
    Content-Type: application/json
    Authorization: Bearer <token>
    
    {
      "widgetId": "<uuid>",
      "eventType": "clicked",
      "viewContext": "dashboard",
      "metadata": {}
    }
    
  2. Validation pipeline for event_type:

    • Check event_type exists in event_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 in declared_event_types on the active manifest. If not → HTTP 422 with "code": "event_type_not_in_manifest".
    • Framework-level types (owned by no hub) are always allowed.
  3. Extend Api.V2.AnnotationsController with CreateAnnotationAction:

    POST /api/v2/annotations
    Body: { "widgetId": "<uuid>", "category": "ux-friction", "body": "..." }
    

    Validate category against annotation_category_registry with the same 422 pattern.

  4. On success, return HTTP 201 with the created record JSON.

  5. Add GET /api/v2/event-types and GET /api/v2/widget-types and GET /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.

  1. 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).

  2. 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>]
    
  3. 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.

  4. Add x-ihf-contract: /contracts/functional/interaction-reporting-v1.md as a top-level extension field.

  5. Serve at GET /api/v2/openapi.json (unauthenticated — it is a public contract). Also serve GET /api/v2/openapi.yaml as a convenience.

  6. Add a Swagger UI at GET /api/v2/docs that 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.

  1. POST /api/v2/token endpoint:

    Content-Type: application/x-www-form-urlencoded
    Body: grant_type=client_credentials&client_id=<id>&client_secret=<secret>&scope=hub:dev-hub:read
    

    Response:

    { "access_token": "<opaque>", "token_type": "Bearer", "expires_in": 3600 }
    
  2. Implementation approach: opaque tokens stored in api_keys (reuse the existing table; add token_type TEXT DEFAULT 'bearer' and expires_at). JWTs are acceptable but opaque tokens are simpler for v1 and avoid key management complexity.

  3. Scope validation:

    • hub:{slug}:read — allowed for all active consumers
    • hub:{slug}:write — requires the consumer's hub_capability_manifest_id to reference an active manifest for that hub slug
    • framework:read — allowed for all
    • Unknown scopes → 400 invalid_scope
  4. Token lifetime: 3600 seconds (1 hour). Refresh tokens are out of scope for Phase 9.

  5. Standard OAuth error responses: 400 with error field (invalid_client, invalid_grant, invalid_scope, unsupported_grant_type).

  6. 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.

  1. Web/Controller/ApiConsumersController.hs:

    • CRUD: index, show, new, create, edit, update, delete
    • Delete: soft-delete by setting is_active = FALSE; do not destroy records
  2. 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) and key_hash (SHA-256). Display the full key once on creation — never again. Show a "copy this now" alert banner.
  3. 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
  4. Link "API Consumers" from the existing admin navigation.

  5. Domain hub consumer display: if hub_capability_manifest_id is 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.

  1. Create Web/Controller/Api/V2/SdkController.hs with a GenerateTsSdkAction that:

    • Queries widget_type_registry, event_type_registry, annotation_category_registry
    • Renders Web/View/Api/V2/TsSdk.hs which produces a .ts file
  2. 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 }) { ... }
    }
    
  3. Serve the raw .ts file at GET /api/v2/sdk/ihf-client.ts (MIME type application/typescript). This is always regenerated from live registries.

  4. Also serve a static SDK download page at GET /api/v2/sdk listing 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.

  1. Extend Web/Controller/Api/V2/SdkController.hs with a GeneratePySdkAction.

  2. 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: ...
    
  3. The enum identifier names follow Python conventions (UPPER_SNAKE_CASE). The values are the registry strings. Use str, Enum so instances compare equal to their string value.

  4. Serve at GET /api/v2/sdk/ihf-client.py (MIME text/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.

  1. Web/Controller/WebhookSubscriptionsController.hs:

    • CRUD scoped to an ApiConsumer
    • event_type validated against event_type_registry
  2. Delivery trigger: after inserting an InteractionEvent row, dispatch delivery for all active subscriptions matching event_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
    
  3. HMAC signing: add X-IHF-Signature: sha256=<hex> header to each delivery request. Consumers verify this header to authenticate the webhook.

  4. 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.

  5. Add the same delivery trigger for RequirementCandidate creation and DecisionRecord creation.

  6. 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.

  1. Request logging middleware: for all /api/v2/ requests, insert a row into api_request_log after the response is sent (non-blocking). Capture: api_consumer_id, endpoint, method, status_code, latency_ms, requested_at.

  2. Web/Controller/ApiDashboardController.hs with ShowApiDashboardAction:

    • Aggregate query: per-consumer request count (last 24h), error rate (4xx+5xx / total), last-seen timestamp, top 5 endpoints
    • Uses IHP AutoRefresh for push updates on new log rows
  3. 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
  4. 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:

  1. Middleware (applied to all /api/v2/ routes) before action dispatch:

    • Count requests in the last 60 seconds from api_request_log for the consumer. If > rate_limit_per_minute → HTTP 429 with Retry-After: <seconds> header.
    • Count requests today (since quota_resets_at - 1 day) for the consumer. If > quota_per_day → HTTP 429 with X-Quota-Resets-At: <iso8601> header.
    • Both return JSON { "error": "Rate limit exceeded", "code": "rate_limited" } or "code": "quota_exceeded".
  2. Quota reset: add a daily job Web/Job/QuotaResetJob.hs that runs at 00:00 UTC and updates quota_resets_at to the next midnight for all consumers.

Close-out:

  1. Update CLAUDE.md:

    • Change Active Workplan section 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.
  2. Update SCOPE.md to reflect Phase 9 completion.

  3. Update ARCHITECTURE-LAYERS.md:

    • Add ApiConsumer, ApiKey, WebhookSubscription, WebhookDelivery, ApiRequestLog to the Extensions layer.
    • Update scorecard (Phase 9 adds external API surface — likely improves Cross-layer and Configuration scores).
    • Set next review date.
  4. Update /contracts/functional/interaction-reporting-v1.md to reference the live OpenAPI spec URL (/api/v2/openapi.json) and confirm the Phase 9 endpoint list.

  5. Mark all exit criteria verified in this workplan file.

  6. 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, category carry enum arrays from live registries
  • TypeScript SDK at /api/v2/sdk/ihf-client.ts exports correct enums
  • Python SDK at /api/v2/sdk/ihf-client.py exports correct enums
  • Webhook delivery confirmed for interaction_event.created and requirement_candidate.created
  • API usage dashboard renders correctly with AutoRefresh
  • OAuth client credentials flow works end-to-end
  • Submission of an unregistered event_type returns HTTP 422 with registry-referenced error
  • Rate limiting returns 429 with Retry-After
  • CLAUDE.md updated; IHUB-WP-0010 listed as complete