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
260 lines
9.0 KiB
Markdown
260 lines
9.0 KiB
Markdown
# 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=<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`:
|
|
```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)
|