7.7 KiB
API Contracts: 109 — Tenant Review Pack Export v1
Date: 2026-02-23
Branch: 109-review-pack-export
Overview
This feature introduces three interaction surfaces:
- Filament Resource — ReviewPackResource (list, view pages + modal generate action)
- Filament Widget — Tenant Dashboard card
- HTTP Route — Signed download endpoint
All Filament interactions are handled through Livewire/Filament's built-in request model (no custom REST endpoints). The only non-Filament HTTP endpoint is the signed file download route.
1. Signed Download Route
GET /admin/review-packs/{reviewPack}/download
Route Name: admin.review-packs.download
Authentication: Signed URL (via URL::signedRoute()). No active session required.
Authorization: URL is generated only if requesting user has REVIEW_PACK_VIEW capability in the pack's workspace+tenant scope. Non-members cannot obtain a valid URL. Signature validation is done by middleware.
Parameters:
| Parameter | Location | Type | Required | Notes |
|---|---|---|---|---|
reviewPack |
path | integer | yes | ReviewPack record ID |
signature |
query | string | yes | Auto-injected by URL::signedRoute() |
expires |
query | integer | yes | Auto-injected by URL::signedRoute() |
Response (Success — 200):
Content-Type: application/zip
Content-Disposition: attachment; filename="review-pack-{tenant_external_id}-{YYYY-MM-DD}.zip"
Content-Length: {file_size}
X-Review-Pack-SHA256: {sha256}
{binary ZIP content streamed from exports disk}
Response (Expired Signature — 403):
{ "message": "Invalid signature." }
Response (Pack Expired or Not Ready — 404):
{ "message": "Not Found" }
Response (Pack Not Found / Wrong Workspace — 404):
{ "message": "Not Found" }
Implementation Notes:
- Route middleware:
signed(Laravel built-in) - Controller validates
reviewPack->status === 'ready'before streaming - File streamed via
Storage::disk($pack->file_disk)->download($pack->file_path, ...) - Response includes
X-Review-Pack-SHA256header for client-side integrity verification
2. Signed URL Generation (Internal)
Not an HTTP endpoint. URL is generated server-side when user clicks "Download" in Filament.
// Service method
public function generateDownloadUrl(ReviewPack $pack): string
{
return URL::signedRoute(
'admin.review-packs.download',
['reviewPack' => $pack->id],
now()->addMinutes(config('tenantpilot.review_pack.download_url_ttl_minutes', 60))
);
}
Authorization check happens before URL generation (Gate/Policy), not at download time (signature-only at download).
3. Filament Actions Contract
ReviewPackResource — Header Action: "Generate Pack"
Trigger: Modal action button on List page header
Capability Required: REVIEW_PACK_MANAGE
Modal Fields:
| Field | Type | Default | Notes |
|---|---|---|---|
include_pii |
Toggle | true |
Label: "Include display names (PII)" |
include_operations |
Toggle | true |
Label: "Include operations log" |
Behavior:
- Check for active
OperationRunof typetenant.review_pack.generatefor this tenant → if exists, show error notification "Generation already in progress" and abort. - Compute input fingerprint → check for existing
readyunexpired pack with same fingerprint → if exists, show info notification "Identical pack already exists" with download link and abort. - Create
OperationRun(type:tenant.review_pack.generate, status: queued). - Create
ReviewPack(status: queued, linked to OperationRun). - Dispatch
GenerateReviewPackJob. - Show success notification: "Review pack generation started."
ReviewPackResource — Row Action: "Download"
Visibility: status === 'ready'
Capability Required: REVIEW_PACK_VIEW
Behavior: Generate signed URL → open in new tab (->openUrlInNewTab())
ReviewPackResource — Row Action: "Expire"
Visibility: status === 'ready'
Capability Required: REVIEW_PACK_MANAGE
Destructive: Yes → ->requiresConfirmation() + ->color('danger')
Behavior:
- Set
status = expired. - Delete file from
exportsdisk. - Show success notification: "Review pack expired."
ReviewPack View Page — Header Action: "Regenerate"
Capability Required: REVIEW_PACK_MANAGE
Destructive: Yes (if a ready pack exists) → ->requiresConfirmation()
Behavior: Same as "Generate Pack" but with the current pack's options pre-filled and previous_fingerprint set to the current pack's fingerprint.
4. Tenant Dashboard Widget Contract
TenantReviewPackCard Widget
Location: Tenant dashboard
Data: Latest ReviewPack for current tenant (eager-loaded)
Display States:
| Pack State | Card Content | Actions |
|---|---|---|
| No pack exists | "No review pack yet" | "Generate first pack" (REVIEW_PACK_MANAGE) |
| queued / generating | Status badge + "Generation in progress" | — |
| ready | Status badge + generated_at + expires_at + file_size | "Download" (REVIEW_PACK_VIEW) + "Generate new" (REVIEW_PACK_MANAGE) |
| failed | Status badge + failure reason (sanitized) | "Retry" = Generate (REVIEW_PACK_MANAGE) |
| expired | Status badge + "Expired on {date}" | "Generate new" (REVIEW_PACK_MANAGE) |
5. Job Contract
GenerateReviewPackJob
Queue: default
Implements: ShouldQueue
Unique: Via OperationRun active-run dedupe (not Laravel's ShouldBeUnique)
Input:
public function __construct(
public int $reviewPackId,
public int $operationRunId,
) {}
Steps:
- Load ReviewPack + OperationRun; abort if either missing.
- Mark OperationRun as
running, ReviewPack asgenerating. - Collect data: StoredReports, Findings, Tenant hardening, recent OperationRuns.
- Compute
data_freshnesstimestamps per source. - Build in-memory file map (filenames → content).
- Apply PII redaction if
options.include_pii === false. - Assemble ZIP to temp file (alphabetical insertion order).
- Compute SHA-256 of ZIP.
- Store ZIP on
exportsdisk. - Update ReviewPack: status=ready, fingerprint, sha256, file_size, file_path, file_disk, generated_at, summary.
- Mark OperationRun as
completed, outcome=success. - Send
ReviewPackStatusNotification(ready) to initiator.
On Failure:
- Update ReviewPack: status=failed.
- Mark OperationRun as
completed, outcome=failed, withreason_codein context. - Send
ReviewPackStatusNotification(failed) to initiator. - Re-throw exception for queue worker visibility.
6. Artisan Command Contract
tenantpilot:review-pack:prune
Signature: tenantpilot:review-pack:prune {--hard-delete}
Behavior:
- Query ReviewPacks where
status = 'ready'ANDexpires_at < now(). - For each: set
status = expired, delete file from disk. - If
--hard-delete: query ReviewPacks wherestatus = 'expired'ANDupdated_at < now() - grace_days. Hard-delete these rows. - Output summary:
{n} packs expired, {m} packs hard-deleted.
Schedule: daily() + withoutOverlapping()
7. Notification Contract
ReviewPackStatusNotification
Channel: Database (Filament notification system)
Recipients: Initiator user (via initiated_by_user_id)
Payload (ready):
title: "Review pack ready"
body: "Review pack for {tenant_name} is ready for download."
actions: [View → ViewReviewPack page URL]
Payload (failed):
title: "Review pack generation failed"
body: "Review pack for {tenant_name} could not be generated: {sanitized_reason}."
actions: [View → ViewReviewPack page URL]