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

KindDescriptionTrigger
pack.approval.requestedApproval required for pack deploymentTask Runner initiates deployment requiring approval
pack.approval.updatedApproval state changedDecision recorded or timeout
pack.policy.holdPolicy gate blocked deploymentPolicy Engine rejects deployment
pack.policy.releasedPolicy hold liftedPolicy 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

FieldTypeDescription
eventIdUUIDUnique event identifier; used for deduplication
issuedAtISO 8601Event timestamp in UTC
kindstringEvent type (see Event Kinds table)
packIdstringPackage identifier in PURL format
decisionstringCurrent state: pending, approved, rejected, hold, expired
actorstringIdentity that triggered the event

Optional Fields

FieldTypeDescription
policyobjectPolicy metadata (id, version)
resumeTokenstringOpaque token for Task Runner resume flow
summarystringHuman-readable summary for notifications
labelsobjectCustom 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

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

CodeMeaningClient Action
200Duplicate request (idempotent)Treat as success
202Accepted for processingContinue normal flow
204Acknowledgement recordedContinue normal flow
400Validation errorFix request and retry
401Authentication requiredRefresh token and retry
403Insufficient permissionsCheck scope; contact admin
404Resource not foundVerify packId; may have expired
410Token expiredRe-initiate approval flow
429Rate limitedRetry after Retry-After seconds
5xxServer errorRetry 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

Security Requirements

Authentication

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

Tenant Isolation

Idempotency

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:

IP Allowlist

Configurable per environment in notifier:security:ipAllowlist:

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

Sensitive Data Handling

Audit Trail

All operations emit structured audit events:

EventFieldsRetention
pack.approval.ingestedpackId, kind, decision, actor, eventId90 days
pack.approval.acknowledgedpackId, ackToken, decision, actor90 days
pack.policy.holdpackId, policyId, reason90 days

Observability

Metrics

MetricTypeLabels
notifier_pack_approvals_totalCounterkind, decision, tenant
notifier_pack_approvals_outstandingGaugetenant
notifier_pack_approval_ack_latency_secondsHistogramdecision
notifier_pack_approval_errors_totalCountercode, tenant

Structured Logs

All operations include:

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-TenantId: 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-TenantId: tenant-acme-corp" \
  -H "Content-Type: application/json" \
  -d '{
    "ackToken": "ack-token-xyz789",
    "decision": "approved",
    "comment": "Reviewed and approved"
  }'