| title | Governed event resources |
|---|---|
| status | implemented-private-file-intake |
| last_updated | 2026-09-23 |
The permanent delivery and revocation decision is ADR-033. Operator and attendee guidance is linked from the public documentation navigation.
EventResource is an independent tenant/event-owned aggregate for governed event materials. Management authors semantic drafts; audience reads expose only currently authorized metadata. Metadata never substitutes for the separate delivery decision.
This is an organizer-management contract, not a public material catalogue. The authenticated, private/no-store routes are:
| Purpose | Route |
|---|---|
| Read one management representation | GET /api/eventresource/{id}/management |
| List an event's management representations | GET /api/event/{eventId}/resources/management?page=&pageSize= |
| Read the bounded management audit | GET /api/eventresource/{id}/audit?limit= |
| Create a draft | POST /api/event/{eventId}/resources |
| Update draft semantics | PUT /api/eventresource/{id} |
| Archive, delete, or the reserved state routes | POST /api/eventresource/{id}/archive, DELETE /api/eventresource/{id}, POST /api/eventresource/{id}/publish, POST /api/eventresource/{id}/unpublish, POST /api/eventresource/{id}/moderate |
Every management representation is private/no-store, including its HAL form and
audit result. Organizer clients follow only the server-authored HAL links; a
link is an affordance for the current decision, not a durable permission. The
management detail and collection expose self, collection, audit, create,
edit, archive, delete, unpublish, and moderation relations only when applicable
and authorized. Stored-file management additionally offers upload-file and,
when current policy and inspected ownership permit it, publish.
Management pages contain only the requested page number/size, authorized HAL items and collection actions. They expose no total count, total pages or count-derived navigation: parent management authority does not authorize counts of provider-denied resources on other pages. Each read loads at most 100 rows, including archived history, without materializing the entire event inventory.
Create bodies contain a client-retained UUIDv7 resourceId; it is the replay
identity and is not server-derived tenant or subject authority. Update and state
requests supply the representation's version as expectedVersion; stale
versions conflict, and unauthorized or missing targets are not exposed. Every mutation requires an
Idempotency-Key. RequireIdempotencyKey and RevalidateIdempotencyReplay are
both active: a replay does not reuse an old authorization result, and there is
no suppression bypass for this controller.
P5.A drafts carry only semantic metadata, audience rules, availability intent,
and a governed delivery type placeholder. They accept no storage reference,
external destination, encryption material, file input, or other delivery
payload. Delivery is configured through its separate operation. publish
rejects incomplete or disallowed files and unconfigured external-link placeholders;
it must not be treated as a way to publish a placeholder.
Archiving is terminal and never publishes content. It is available from draft
or withdrawn state; deletion remains permitted for an archived resource when
the current HAL action is granted.
PUT /api/eventresource/{id}/destination accepts a raw link only as
write-only, idempotency-revalidated input from a current resource manager. It
validates absolute HTTPS without userinfo, literal IP or recognized local
hosts, deceptive IDN authority, control/format characters or hostname suffix
matching. The authoritative origin must be in the current intersected instance
and tenant allow-list; no origin is enabled by default. Only the normalized
origin and a Data Protection envelope are stored. Draft, audit, export,
federation and ordinary audience DTOs never contain raw links or ciphertext.
The manager and an eligible reader can see the safe origin to warn before
navigation; teasers omit it completely. Arbitrarily seeded envelopes must
decrypt and revalidate against that same origin or publication fails.
EventResourceDestinationProtector uses the API's existing database-backed
Data Protection keyring and stable islamu-event application identity. Its
purpose chain binds the external-destination operation, version, tenant and
resource; another tenant, resource or version cannot decrypt a copied
envelope. Combined hosting reasserts this API database keyring after optional
BFF Redis registration when the API key context is registered; no-database
test/OpenAPI hosts keep their existing BFF key authority. The BFF must not
replace production destination-key authority.
Current authority is checked before unprotecting, and a fresh
single-use header gate precedes the controlled 302. The response carries the
destination only in Location, with Cache-Control: no-store and
Referrer-Policy: no-referrer; the API and BFF never fetch or follow it.
Unavailable keys, tampering, withdrawal and policy changes emit no Location.
Only the initial origin is constrained: neither DNS resolution nor later
browser/provider redirects are confined by server-side validation.
Keep retired key material available while any stored destination ciphertext still depends on it, including withdrawn and archived resources. Withdrawal or archive hides a link but retains its ciphertext; republishing must decrypt it. Retire a key only after every dependent destination is replaced or removed. A backup containing both encrypted rows and unwrapped Data Protection key XML is not confidential against full backup compromise; use the deployment's existing key-wrapping authority where configured.
EventResources and StudioEventResources use the scoped
IEventResourceService over generated per-tag clients, never component-side
role checks or destination readback. The attendee surface loads audience
collection and fresh item details; only a root-relative, exact download,
access or accessible-alternative HAL relation produces a delivery anchor.
Teaser cards render the public title and availability requirements, never
private description, language, accessibility notes, file details or safe
origin, even if a client detail record contains inconsistent private fields.
The file safety state remains truthful metadata when a private detail is
visible without a current download relation; only that relation creates
the action. The associated download explanation states that withdrawal can
deny the next request but cannot stop a response already streaming.
The safe origin is plain text, not a preview/fetch target. The Studio
navigation obtains manage-resources from the fresh audience collection,
including for a private event whose public parent resources link is absent.
Collection create-resource/export and each item's management relations
gate distinct controls. A mutation rereads the item/version and relation,
uses a generated client with a new idempotency key, and refreshes after a
denial without reflecting server error details. The external-destination
form clears its URL after submission and shows only safe-origin metadata.
Blazor components rely on the service and server authority independently:
HAL hides an affordance; it does not grant a write.
GET/PUT /api/settings/instance/event-resources fixes the native
EventResources category at instance scope and requires current instance
administrator authority for both reads and writes. The private/no-store GET
resolves all eight effective SettingGroupResponseDto entries with value,
source, lock, editability and reason. The PUT accepts only registered resource
keys, forces strict UpdateSettingBatchCommand mode and delegates to the
coordinated setting writer rather than directly updating rows. Invalid
private values are not reflected in API validation responses.
The existing GET/PUT /api/settings/tenant/EventResources resolves the same
category for the current tenant. Tenant GET replaces each raw merged value
with the current EventResourceGovernancePolicyReader intersection of instance,
tenant and storage ceilings; it fails closed when that policy is unavailable.
source still identifies the stored setting's provenance, which can differ
from the authority behind a tighter effective value. Its generated HAL
group emits self; an
edit relation for either scope requires at least one currently editable
setting and the corresponding scoped permission. A missing edit link makes
the entire client form read-only; CanEdit additionally gates each field.
allow_unscanned_documents is instance-only. Tenant collection controls
offer only subsets of current effective values, and numeric controls cannot
exceed the currently displayed effective ceiling. Server-side coordinated
mutation and policy intersection remain authoritative: a stale browser may
be denied and must reload the group before another write. Instance locks
remain active even in SingleTenant mode.
After either a save or denial, the form reloads current policy, restores
keyboard focus to its heading when the original control was replaced, and
announces the result through a status or alert without duplicating that text
in the focus target.
The AT Protocol event source-field manifest explicitly excludes private
stored-document inspection state and storage-provider identifiers. Its
completeness test guards future additions to StorageObject against
unreviewed federation projections.
The audit page contains only a closed action/outcome/reason, timestamp, and retained responsible-manager identity. A successful mutation writes that minimal audit entry in the same serializable transaction as the resource change. With retention zero no new entry is written and the hourly cleanup purges existing entries; otherwise it removes expired complete rows. Subject erasure clears manager attribution without deleting the shared resource. The current retention configuration and operational limits remain in the existing event-resource governance guide, rather than being duplicated here.
The public contract above intentionally omits implementation mechanics. Native
CQS uses closed ICommand/IQuery requests and handlers rather than generic
CRUD: create, update, publish, unpublish, archive, delete, moderate, detail,
management list, and audit each retain their explicit result/failure boundary.
The authority orchestrator takes serializable A and B snapshots around provider
I/O; B compares the frozen route and complete provider inputs, including the
resource version. Mutations recheck current authority inside their own
serializable transaction, including execution-strategy replay, before the state
and audit change commit.
HAL assembly is asynchronous. Management metadata receives a final version-bound authorization check after that assembly; this is a decision boundary, not a guarantee that authority remains fresh through response completion.
GET /api/event/{eventId}/resources and GET /api/eventresource/{id} use
handler-owned native queries, explicit safe projections and private/no-store HAL.
Audience DTOs never carry management notes, audience-rule objects, resource
versions, backing identities or destination material. Teasers use the public
title with closed availability/requirement values; protected description,
language, accessibility, file metadata and alternative references are omitted.
Discovery evaluates the complete governed active set, capped at 500 resources,
using category-bounded authority reads and provider batches. It returns at most
100 authorized items (20 by default), without total counts or total pages.
Continuation is emitted only after another authorized item is observed. Its
position is the last returned (SortOrder, Id), not a hidden candidate.
The narrow Application cursor port uses native Data Protection in Infrastructure, with a distinct purpose, query version, tenant/event/subject-or-anonymous/machine binding and 15-minute expiry. Inputs longer than 2,048 characters are rejected before unprotection. Invalid, expired or wrong-scope state produces bounded 400 without echoing the token. Every continuation reevaluates current authority; reordering may require a fresh first page.
The native result retains a non-wire disclosure proof for post-HAL validation. It binds resource versions, private-versus-teaser disclosure, independently authorized alternative references and the additional continuation witness. Final fresh A/provider/B checks cannot reuse an earlier private projection after an entitlement downgrade, even when no resource stamp changed. Proof state is neither serialized nor a reusable grant. As with delivery's later boundary, this does not promise freshness through response completion.
GET /api/event/{eventId}/resources/export?page=&pageSize= returns bounded
semantic JSON under the distinct native export action. The parent event and
every included resource require current export authority; management visibility
alone is not a provider export decision. The management collection advertises
export only through its scoped capability policy.
The explicit immutable projection preserves kind, publication state, authorized
public/private metadata (including management notes), audience qualifiers,
relative availability intent and same-event semantic references. It excludes
concurrency stamps, audit history, manager attribution, storage identity,
provider keys, protected envelopes, origins and destinations. A privately owned
file may contribute its display name, MIME type, byte size and safety state.
A separate exact download decision controls the optional application-route
reference; export or moderation permission never implies download authority.
Pages default to 20 and are capped at 100. There are no totals or inferred final page links. Rows are projected in a bounded serializable read, then exact resource versions and attachment generations are bound to a fresh parent-plus-row A/provider/B export decision immediately before returning JSON. A stale projection or any denied row discards the entire prepared page. Cancellation remains caller-owned; infrastructure failure carries no metadata. This is not bulk ZIP export or resource import, and the document is not an authorization grant.
POST /api/eventresource/{id}/upload-sessions accepts the expected resource
version, declared size, closed MIME type, safe display name, extension and
replay identity. The native command delegates to
EventResourceFileUploadWorkflow. Existing storage-session finalize/cancel
routes resolve resource ownership server-side and call that same workflow:
they neither require generic-storage create permission nor bypass resource
update authority. Their HTTP replay filters reauthorize committed results.
Upload mutation and private response requests select
EventResourceAuthorityRequest.RequiresUnquarantinedMaterial. Fresh
EventResourceAuthorizationFacts capture the aggregate's native quarantine
state, and the shared evaluator enforces this precondition at acquisition,
mutation rechecks and final response release. General management reads retain
their separate metadata-only quarantine status; management permission does not
authorize an upload DTO containing stored file details.
Reservation retains the resource version. Finalization spools a bounded, delete-on-close inspection snapshot, persists an inaccessible delete-requested staging identity, and writes the selected provider outside a transaction. Fresh authority, version, policy and quota checks then atomically attach the new object, record the success audit, finalize the session and retire the old attachment. A failed commit leaves durable cleanup state rather than relying on in-memory compensation. A replacement does not remove the old attachment before the new one commits.
Quarantine before finalization returns the existing structured denial without
starting a provider write. If quarantine arrives during an acknowledged write,
the final transaction denies attachment, transfers the exact staged object to
durable retirement and releases its reservation through native accounting.
The previous attachment and current quarantine causes remain unchanged;
SetStoredFile is never used as an exception-driven authorization check.
EventResourceDocumentInspection and EventResourceDocumentPolicy accept only
PDF, non-macro DOCX and non-macro PPTX declarations. PDF validation is signature
checking, not active-content or malware scanning. OOXML checks package
structure, CRCs, content types and relationships; it rejects unsupported active,
embedded, encrypted or externally related content and enforces entry, expansion
and XML bounds. This is deliberately a conservative subset, not a scanner.
The immutable object identity and SHA-256 bind an unscanned verdict; lifecycle
success never means clean. Default governance denies unscanned publication
and access. Only the instance authority can opt into this supported subset.
Generic storage access excludes resource purpose, resource owner discriminator
or any retained resource attachment independently, including malformed tuples.
The exclusion precedes list counts and applies to public-image, presign and
internal readers as well as direct content and mutation endpoints. The uploader
receives no generic access exception. Resource objects remain PrivateOwner
even when their audience is public.
GET /api/eventresource/{id}/content uses
EventResourceContentService for A/provider/private preparation/B and
EventResourceFileResult for the final clock/freshness gate immediately before
headers. The response owns the pending lease, so cancelled MVC result execution
also disposes prepared streams. File metadata disclosure binds the full
ownership, inspection and content generation after HAL assembly, not merely a
resource concurrency stamp. Audience download links require exact native
download authority; teasers never carry file descriptors.
Responses are full binary attachments with private/no-store, nosniff and
restrictive content policy. There are no resource presigns, conditional 304
responses, ETags, Last-Modified validators or range responses. Binary OpenAPI
media schemas generate FileResponse, not JSON MVC-result types. BFF uploads
bind opaque sessions to the current subject and resource; successful browser
completion returns only the resource ID, never a generic storage locator.
Split forwarding and Combined in-process transport use server-held identity;
the Combined integration fixture exercises production BFF login, SQLite and
the real local-file provider.
Combined hosting registers top-level authentication/authorization explicitly
after the cookie bridge so framework auto-insertion cannot reject the request
before trusted enrichment. The bridge validates antiforgery against the actual
cookie principal, then clears that principal for API-token authentication.
HTTP request events use normalized route templates; raw framework
request-start/finish messages are kept below the native logging threshold
because they contain URLs before routing. Warnings and errors remain enabled.
EventResourceTraceProcessor in shared ServiceDefaults removes resource and
upload-session identities from matching server and outbound HTTP span URLs
before exporters run. This also covers the separate Split BFF process; native
API middleware alone cannot sanitize a different host's client spans.
The forward BindEventResourceFiles migrations add explicit inspection binding
and upload version facts, private-owner constraints and the tenant-qualified
session/object relationship. Their PostgreSQL, SQLite, SQL Server and MySQL
histories are generated artifacts.
The generated AlignAtprotoRecordDidCollation forward histories make
AtprotoRecord.Did use the same provider-appropriate ordinal ASCII collation
as AtprotoIdentity.Did. Public event eligibility compares those persisted
identifiers when evaluating resource authority; a SQL Server database with
the mismatched old collations fails that query closed instead of granting a
resource. Apply the selected provider history before serving resource reads.
MariaDB uses the shared MySQL catalog. The SQLite generated reverse operation,
all four model snapshots and all five real-engine behavior contracts were
verified without editing migration files by hand.
EventResourceStorageLifecycleRepository serializes activation, producer
acknowledgement and retirement through a conditional source-row fence. Uploads
commit an immutable StorageProviderBinding before provider writes. Successful
receipts commit independently of attachment and required-success audit; a late
receipt may settle a matching tombstone after source removal, never recreate a
resource. Content reads use the captured binding and exact object version, which
also participate in delivery generation checks.
Provider resolution/write failures return a closed storage-upload failure without
logging or serializing untrusted provider exception text. Without a confirmed
write receipt, the staged object's producer remains unsettled and its deletion
authority stays non-executable until an exact later acknowledgement.
Retirement transfers deletion authority and settles logical quota once in the native caller transaction. Heavy moderation retires before overwriting the storage lifecycle, then saves redaction/detachment and removes transferred sources. This ordering does not depend on finalized sessions remaining after subject erasure. Erasure preserves shared live materials; only the erased subject's exact detached staging objects enter retirement. Evidence-backed objects and their upload sessions are excluded from both ordinary retirement and source handoff; parent moderation withdraws their resource affordance without deleting the independently retained evidence. Expiry conditionally fences the observed Uploading session before retiring its staged object, so a concurrent finalization invalidates the entire transaction.
StorageObjectDeletionTombstone retains identifiers, the machine key, binding
and version, state, claim stamp and scheduling times, without tenant/resource/
user foreign keys or content/attribution. AwaitingProducer is not executable:
expiry, cancellation and elapsed time cannot prove producer settlement.
Ready work needs a conditional lease before external deletion. Absence and
retry updates require the matching unexpired fence; a stale worker cannot
complete newer work. Audit-row expiry remains independent.
The existing storage reconciliation job invokes
EventResourceStorageCleanupService. Its bounded transactional passes retire
expired resource reservations and remove transferred metadata, including
unknown-producer sources. Claims and terminal purge require source rows to be
absent. Provider I/O stays outside transactions; deletion acknowledgements alone
are insufficient without confirmed absence. Generic reconciliation, image
deletion and inventory cannot take over resource-owned or tombstoned keys.
Dry-run performs no mutation. No second scheduler or outbox is introduced.
Bindings retain original non-secret target coordinates and external secret references, not credential values or a fallback to current provider settings. S3 operations address exact versions; delete markers, unavailable buckets and missing/inaccessible local roots do not establish absence. Unknown receipts or versions remain pending for operator reconciliation; see the operator recovery guidance. Only a bound local write may initialize its captured root for the first upload; read, inventory validation and cleanup cannot recreate a missing old root.
The database enforces tenant-qualified ownership instead of relying on globally unique identifiers:
- resources reference
(TenantId, EventId)events and optional(TenantId, EventId, EventSessionId)sessions; SessionScopeIdis the session ID for session-owned resources and the event ID for event-level resources, giving audience rows a non-null composite owner key;- accessible alternatives reference a different resource in the same tenant and event;
- stored objects use
(TenantId, StorageObjectId)and a unique nullable attachment index, allowing many unattached drafts but only one resource per attached object; - admission rules reference the actual
AdmissionTarget.Idtogether with tenant, event, target type and scope. Session targets must match the rule's exact session; a target from a sibling session in the same event is rejected; - ticket qualifiers carry both catalog-version and ticket-type IDs. Composite FKs prove
ticket type -> catalog -> event; same-tenant ticket identity alone is not accepted.
Audience discriminator and qualifier checks reject empty/contradictory shapes. Publication/payload checks permit incomplete drafts but require one delivery payload for published or withdrawn rows. Availability remains scalar columns on event_resources; the computed Domain Availability property is not mapped and no owned availability table exists.
Nullable boundary and envelope checks explicitly test required leaves for null:
SQL UNKNOWN must not admit a partial anchor/offset or an envelope missing its
protection version. Teaser/public titles must contain non-whitespace text, and
deleted rows retain no delivery payload.
IEventResourceRepository returns Domain entities only. Detail, candidate, parent, subject, target, ticket, and audit reads are bounded and no-tracking. Only GetByIdForUpdateAsync tracks the resource graph. Candidate ordering is deterministic by (SortOrder, Id), represented by EventResourceCursor(int SortOrder, Guid Id).
Candidate reads reject descending or alternate sorts before querying; their cursor cannot describe those orderings. Equal SortOrder values continue through the same database-ordered Id tie-breaker used by the first page.
The port has no generic unbounded CRUD/query surface. Oversized identity requests are rejected, and subject fact reads detect budget overflow rather than quietly return an incomplete authorization snapshot.
Mutation handlers call Update after the accepted Domain operation and before
the unit of work saves. It explicitly marks the aggregate modified so the existing
DbContext concurrency-stamp mechanism also runs for child-only policy edits when
the supplied audit timestamp has not changed. The Domain does not manufacture
its own persistence stamp.
The aggregate exposes AudienceRules as a read-only snapshot backed by a private EF collection. EF can rehydrate relationship rows without exposing a caller-mutable collection.
EventResourceSettingDefinitions registers the event_resources.* family in the
native setting registry and configuration-manifest catalog. Every member requires
coordinated mutation. allow_unscanned_documents is instance-only; delivery
types, audiences, MIME types, upload bytes, external origins, retention and active
resource capacity permit non-widening tenant overrides. These are native settings,
not environment variables or a separate configuration store.
EventResourceSettingsWriter validates the complete proposed instance state
before attaching any changed entities. Tenant value writes are compared against
the proposed instance ceiling before storage clamping; clamping must not disguise
an invalid widening request. Instance tightening does not require rewriting old
tenant choices. Untouched older overrides remain stored but become ineffective
where they exceed the current ceiling. The instance-only unscanned opt-in cannot
be set at tenant scope even when the instance has enabled it.
The relational setting lock expands this family into one ordered group acquired before opening a transaction. Standalone writes use a serializable unit of work; manifest application joins its caller-owned transaction and complete outer lock group. Generic repository/resolver paths cannot bypass the writer. A rejected batch saves nothing and emits no notifications. Accepted writes return deferred notifications; callers invalidate caches and dispatch only after commit. Import notifications travel through the existing durable effect outbox.
EventResourceGovernancePolicyReader reads current setting entities rather than
the cached hierarchical resolver. Native instance locks apply in both deployment
modes. It strictly parses the values, intersects tenant choices with instance
ceilings, and applies current storage upload limits and delegation. Malformed
effective policy fails closed. The immutable policy is captured in each authority
snapshot; final authority comparison includes the whole value, including limits
that do not change the provider's immediate boolean decision.
EventResourceAccessRules suppresses attendee disclosure when the resource uses
a disabled delivery type or audience. External delivery also requires an allowed
authoritative HTTPS origin. Existing resource repair, withdrawal and deletion remain
subject to their normal management authority rather than being disabled by a
tightened delivery policy. Creation requires enabled delivery/audience choices
and positive capacity. Publication additionally requires a governed resource
policy and safe payload; governance alone never establishes file safety.
The Domain policy fixes the supported MIME set to PDF, OOXML Word and OOXML PowerPoint. Defaults are 10 MiB, unscanned denied, an empty external-origin allowlist, 30-day audit retention and 500 active resources. Retention is bounded to 0–90 days and capacity to 0–500. Tenant retention/capacity can only decrease. See the operator guide for configuration effects.
EventResourceKind contains 13 stable semantic material kinds. EventResourceDeliveryType contains only STORED_FILE and EXTERNAL_LINK. LookupTableSeeder repairs missing IDs idempotently; model HasData is not used.
EventResourceAuditEntry stores only closed action/outcome/reason values, responsible manager identity when retained, and a timestamp. It does not store raw metadata, destinations, or value snapshots. Tenant/resource and tenant/time indexes support bounded history and retention.
AddEventResourceProviderActivation adds a global deployment fence with a UUIDv7
deployment identity, operation ownership, monotonic local epoch and optimistic
concurrency stamp. Its table is intentionally not tenant-owned: aliases used by
different tenants can identify the same remote policy deployment. The generated
histories live in the same four application migration assemblies; MariaDB uses
the MySQL assembly. Apply this migration before configuring remote resource
authority. It introduces no storage bytes or tenant resource governance defaults.
The native binding document and activation state commit together under the existing setting lock/unit of work. Snapshot reads are no-tracking and operator recovery starts a new operation; a failed or stale operation cannot reopen the fence. See Authorization for the complete publication and convergence protocol. Reversing the additive activation migration discards its coordination state, so withdraw resource authority first and prefer forward repair.
AddEventResources is generated independently for PostgreSQL, SQLite, SQL Server, and the shared MySQL/MariaDB migration assembly. Generated migrations and snapshots are never hand-edited. SQL Server's generated unique nullable attachment index includes its provider-specific non-null filter; the other engines use their native multiple-NULL uniqueness semantics.
The catalog lookup index remains explicit while the new ticket-type lineage key is installed; MySQL cannot drop an index still supporting the live catalog FK. Encrypted destinations use the repository's portable text mapping rather than contributing a large fixed-width declaration to MySQL's row-size limit.
Rollback is feature withdrawal and forward repair. Reversing a migration after resource content exists is not an accepted data-preservation procedure.