# 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 ```json { "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 ```json { "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= X-StellaOps-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`: ```yaml 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) ```bash 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) ```bash 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" }' ``` ## Related Documents - [Pack Approvals Integration Requirements](pack-approvals-integration.md) - [Notifications Architecture](architecture.md) - [Notifications API Reference](api.md) - [Notification Templates](templates.md)