Files
git.stella-ops.org/docs/notifications/pack-approvals-contract.md
master e950474a77
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
up
2025-11-27 15:16:31 +02:00

9.0 KiB

Pack Approvals Notification Contract

Status: Implemented (NOTIFY-SVC-37-001) Last Updated: 2025-11-27 OpenAPI Spec: src/Notifier/StellaOps.Notifier/StellaOps.Notifier.WebService/openapi/pack-approvals.yaml

Overview

This document defines the canonical contract for pack approval notifications between Task Runner and the Notifier service. It covers event payloads, resume token mechanics, error handling, and security requirements.

Event Kinds

Kind Description Trigger
pack.approval.requested Approval required for pack deployment Task Runner initiates deployment requiring approval
pack.approval.updated Approval state changed Decision recorded or timeout
pack.policy.hold Policy gate blocked deployment Policy Engine rejects deployment
pack.policy.released Policy hold lifted Policy conditions satisfied

Canonical Event Schema

{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "issuedAt": "2025-11-27T10:30:00Z",
  "kind": "pack.approval.requested",
  "packId": "pkg:oci/stellaops/scanner@v2.1.0",
  "policy": {
    "id": "policy-prod-deploy",
    "version": "1.2.3"
  },
  "decision": "pending",
  "actor": "ci-pipeline@stellaops.example.com",
  "resumeToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "summary": "Deployment approval required for production scanner update",
  "labels": {
    "environment": "production",
    "team": "security"
  }
}

Required Fields

Field Type Description
eventId UUID Unique event identifier; used for deduplication
issuedAt ISO 8601 Event timestamp in UTC
kind string Event type (see Event Kinds table)
packId string Package identifier in PURL format
decision string Current state: pending, approved, rejected, hold, expired
actor string Identity that triggered the event

Optional Fields

Field Type Description
policy object Policy metadata (id, version)
resumeToken string Opaque token for Task Runner resume flow
summary string Human-readable summary for notifications
labels object Custom key-value metadata

Resume Token Mechanics

Token Flow

┌─────────────┐     POST /pack-approvals      ┌──────────────┐
│ Task Runner │ ──────────────────────────────►│   Notifier   │
│             │   { resumeToken: "abc123" }   │              │
│             │◄──────────────────────────────│              │
│             │   X-Resume-After: "abc123"    │              │
└─────────────┘                               └──────────────┘
       │                                              │
       │                                              │
       │  User acknowledges approval                  │
       │                                              ▼
       │                              ┌──────────────────────────────┐
       │                              │  POST /pack-approvals/{id}/ack
       │                              │  { ackToken: "..." }         │
       │                              └──────────────────────────────┘
       │                                              │
       │◄─────────────────────────────────────────────┤
       │  Resume callback (webhook or message bus)    │

Token Properties

  • Format: Opaque string; clients must not parse or modify
  • TTL: 24 hours from issuedAt
  • Uniqueness: Scoped to tenant + packId + eventId
  • Expiry Handling: Expired tokens return 410 Gone

X-Resume-After Header

When resumeToken is present in the request, the server echoes it in the X-Resume-After response header. This enables cursor-based processing for Task Runner polling.

Error Handling

HTTP Status Codes

Code Meaning Client Action
200 Duplicate request (idempotent) Treat as success
202 Accepted for processing Continue normal flow
204 Acknowledgement recorded Continue normal flow
400 Validation error Fix request and retry
401 Authentication required Refresh token and retry
403 Insufficient permissions Check scope; contact admin
404 Resource not found Verify packId; may have expired
410 Token expired Re-initiate approval flow
429 Rate limited Retry after Retry-After seconds
5xx Server error Retry with exponential backoff

Error Response Format

{
  "error": {
    "code": "invalid_request",
    "message": "eventId, packId, kind, decision, actor are required.",
    "traceId": "00-abc123-def456-00"
  }
}

Retry Strategy

  • Transient errors (5xx, 429): Exponential backoff starting at 1s, max 60s, max 5 retries
  • Validation errors (4xx except 429): Do not retry; fix request
  • Idempotency: Safe to retry any request with the same Idempotency-Key

Security Requirements

Authentication

All endpoints require a valid OAuth2 bearer token with one of these scopes:

  • packs.approve — Full approval flow access
  • Notifier.Events:Write — Event ingestion only

Tenant Isolation

  • X-StellaOps-Tenant header is required on all requests
  • Server validates token tenant claim matches header
  • Cross-tenant access returns 403 Forbidden

Idempotency

  • Idempotency-Key header is required for POST endpoints
  • Keys are scoped to tenant and expire after 15 minutes
  • Duplicate requests within the window return 200 OK

HMAC Signature (Webhooks)

For webhook callbacks from Notifier to Task Runner:

X-StellaOps-Signature: sha256=<hex-encoded-signature>
X-StellaOps-Timestamp: <unix-timestamp>

Signature computed as:

HMAC-SHA256(secret, timestamp + "." + body)

Verification requirements:

  • Reject if timestamp is >5 minutes old
  • Reject if signature does not match
  • Reject if body has been modified

IP Allowlist

Configurable per environment in notifier:security:ipAllowlist:

notifier:
  security:
    ipAllowlist:
      - "10.0.0.0/8"
      - "192.168.1.100"

Sensitive Data Handling

  • Resume tokens: Encrypted at rest; never logged in full
  • Ack tokens: Signed with KMS; validated on acknowledgement
  • Labels: Redacted if keys match secret, password, token, key patterns

Audit Trail

All operations emit structured audit events:

Event Fields Retention
pack.approval.ingested packId, kind, decision, actor, eventId 90 days
pack.approval.acknowledged packId, ackToken, decision, actor 90 days
pack.policy.hold packId, policyId, reason 90 days

Observability

Metrics

Metric Type Labels
notifier_pack_approvals_total Counter kind, decision, tenant
notifier_pack_approvals_outstanding Gauge tenant
notifier_pack_approval_ack_latency_seconds Histogram decision
notifier_pack_approval_errors_total Counter code, tenant

Structured Logs

All operations include:

  • traceId — Distributed trace correlation
  • tenantId — Tenant identifier
  • packId — Package identifier
  • eventId — Event identifier

Integration Examples

Task Runner → Notifier (Ingestion)

curl -X POST https://notifier.stellaops.example.com/api/v1/notify/pack-approvals \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-StellaOps-Tenant: tenant-acme-corp" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "550e8400-e29b-41d4-a716-446655440000",
    "issuedAt": "2025-11-27T10:30:00Z",
    "kind": "pack.approval.requested",
    "packId": "pkg:oci/stellaops/scanner@v2.1.0",
    "decision": "pending",
    "actor": "ci-pipeline@stellaops.example.com",
    "resumeToken": "abc123",
    "summary": "Approval required for production deployment"
  }'

Console → Notifier (Acknowledgement)

curl -X POST https://notifier.stellaops.example.com/api/v1/notify/pack-approvals/pkg%3Aoci%2Fstellaops%2Fscanner%40v2.1.0/ack \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-StellaOps-Tenant: tenant-acme-corp" \
  -H "Content-Type: application/json" \
  -d '{
    "ackToken": "ack-token-xyz789",
    "decision": "approved",
    "comment": "Reviewed and approved"
  }'