up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StellaOps.Notifier.WebService.Contracts;
|
||||
|
||||
public sealed class PackApprovalAckRequest
|
||||
{
|
||||
[Required]
|
||||
public string AckToken { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -118,6 +118,50 @@ app.MapPost("/api/v1/notify/pack-approvals", async (
|
||||
return Results.Accepted();
|
||||
});
|
||||
|
||||
app.MapPost("/api/v1/notify/pack-approvals/{packId}/ack", async (
|
||||
HttpContext context,
|
||||
string packId,
|
||||
PackApprovalAckRequest request,
|
||||
INotifyLockRepository locks,
|
||||
INotifyAuditRepository audit,
|
||||
TimeProvider timeProvider) =>
|
||||
{
|
||||
var tenantId = context.Request.Headers["X-StellaOps-Tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return Results.BadRequest(Error("tenant_missing", "X-StellaOps-Tenant header is required.", context));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.AckToken))
|
||||
{
|
||||
return Results.BadRequest(Error("ack_token_missing", "AckToken is required.", context));
|
||||
}
|
||||
|
||||
var lockKey = $"pack-approvals-ack|{tenantId}|{packId}|{request.AckToken}";
|
||||
var reserved = await locks.TryAcquireAsync(tenantId, lockKey, "pack-approvals-ack", TimeSpan.FromMinutes(10), context.RequestAborted)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!reserved)
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status200OK);
|
||||
}
|
||||
|
||||
var auditEntry = new NotifyAuditEntryDocument
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Actor = "pack-approvals-ack",
|
||||
Action = "pack.approval.acknowledged",
|
||||
EntityId = packId,
|
||||
EntityType = "pack-approval",
|
||||
Timestamp = timeProvider.GetUtcNow(),
|
||||
Payload = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<MongoDB.Bson.BsonDocument>(JsonSerializer.Serialize(request))
|
||||
};
|
||||
|
||||
await audit.AppendAsync(auditEntry, context.RequestAborted).ConfigureAwait(false);
|
||||
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
app.MapGet("/.well-known/openapi", (HttpContext context, OpenApiDocumentCache cache) =>
|
||||
{
|
||||
context.Response.Headers.CacheControl = "public, max-age=300";
|
||||
|
||||
@@ -12,7 +12,9 @@ public sealed class OpenApiDocumentCache
|
||||
var path = Path.Combine(environment.ContentRootPath, "openapi", "notify-openapi.yaml");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException("OpenAPI document not found.", path);
|
||||
_document = string.Empty;
|
||||
_hash = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
_document = File.ReadAllText(path, Encoding.UTF8);
|
||||
|
||||
@@ -317,6 +317,75 @@ paths:
|
||||
default:
|
||||
$ref: '#/components/responses/Error'
|
||||
|
||||
/api/v1/notify/pack-approvals:
|
||||
post:
|
||||
summary: Ingest pack approval decision
|
||||
tags: [PackApprovals]
|
||||
operationId: ingestPackApproval
|
||||
security:
|
||||
- oauth2: [notify.operator]
|
||||
- hmac: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Tenant'
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/PackApprovalEvent' }
|
||||
examples:
|
||||
approval-granted:
|
||||
value:
|
||||
eventId: "20e4e5fe-3d4a-4f57-9f9b-b1a1c1111111"
|
||||
issuedAt: "2025-11-17T16:00:00Z"
|
||||
kind: "pack.approval.granted"
|
||||
packId: "offline-kit-2025-11"
|
||||
policy:
|
||||
id: "policy-123"
|
||||
version: "v5"
|
||||
decision: "approved"
|
||||
actor: "task-runner"
|
||||
resumeToken: "rt-abc123"
|
||||
summary: "All required attestations verified."
|
||||
labels:
|
||||
environment: "prod"
|
||||
approver: "ops"
|
||||
responses:
|
||||
'202':
|
||||
description: Accepted; durable write queued for processing.
|
||||
headers:
|
||||
X-Resume-After:
|
||||
description: Resume token echo or replacement
|
||||
schema: { type: string }
|
||||
default:
|
||||
$ref: '#/components/responses/Error'
|
||||
|
||||
/api/v1/notify/pack-approvals/{packId}/ack:
|
||||
post:
|
||||
summary: Acknowledge a pack approval notification
|
||||
tags: [PackApprovals]
|
||||
operationId: ackPackApproval
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Tenant'
|
||||
- name: packId
|
||||
in: path
|
||||
required: true
|
||||
schema: { type: string }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
ackToken: { type: string }
|
||||
required: [ackToken]
|
||||
responses:
|
||||
'204':
|
||||
description: Acknowledged
|
||||
default:
|
||||
$ref: '#/components/responses/Error'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
oauth2:
|
||||
@@ -328,6 +397,10 @@ components:
|
||||
notify.viewer: Read-only Notifier access
|
||||
notify.operator: Manage rules/templates/incidents within tenant
|
||||
notify.admin: Tenant-scoped administration
|
||||
hmac:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: Pre-shared HMAC token (air-gap friendly) referenced by secretRef.
|
||||
parameters:
|
||||
Tenant:
|
||||
name: X-StellaOps-Tenant
|
||||
@@ -335,6 +408,12 @@ components:
|
||||
required: true
|
||||
description: Tenant slug
|
||||
schema: { type: string }
|
||||
IdempotencyKey:
|
||||
name: Idempotency-Key
|
||||
in: header
|
||||
required: true
|
||||
description: Stable UUID to dedupe retries.
|
||||
schema: { type: string, format: uuid }
|
||||
PageSize:
|
||||
name: pageSize
|
||||
in: query
|
||||
@@ -468,6 +547,39 @@ components:
|
||||
type: object
|
||||
additionalProperties: { type: string }
|
||||
|
||||
PackApprovalEvent:
|
||||
type: object
|
||||
required:
|
||||
- eventId
|
||||
- issuedAt
|
||||
- kind
|
||||
- packId
|
||||
- decision
|
||||
- actor
|
||||
properties:
|
||||
eventId: { type: string, format: uuid }
|
||||
issuedAt: { type: string, format: date-time }
|
||||
kind:
|
||||
type: string
|
||||
enum: [pack.approval.granted, pack.approval.denied, pack.policy.override]
|
||||
packId: { type: string }
|
||||
policy:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
version: { type: string }
|
||||
decision:
|
||||
type: string
|
||||
enum: [approved, denied, overridden]
|
||||
actor: { type: string }
|
||||
resumeToken:
|
||||
type: string
|
||||
description: Opaque token for at-least-once resume.
|
||||
summary: { type: string }
|
||||
labels:
|
||||
type: object
|
||||
additionalProperties: { type: string }
|
||||
|
||||
QuietHours:
|
||||
type: object
|
||||
required: [quietHoursId, windows]
|
||||
|
||||
Reference in New Issue
Block a user