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 accessNotifier.Events:Write— Event ingestion only
Tenant Isolation
X-StellaOps-Tenantheader is required on all requests- Server validates token tenant claim matches header
- Cross-tenant access returns
403 Forbidden
Idempotency
Idempotency-Keyheader 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,keypatterns
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 correlationtenantId— Tenant identifierpackId— Package identifiereventId— 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"
}'