ABOUTME: Comprehensive API architecture reference covering middleware pipeline, rate limiting, caching, HATEOAS, specification pattern, and all runtime behavior. ABOUTME: Authoritative source for Explore.API patterns — middleware order, request protection, content negotiation, filtering, and error handling.
PostgresUserTenantMembershipContract requires explicit deployment enrollment and the configured Global session for cross-tenant membership discovery. Native entry and reinstall attest exact dedicated role flags, memberships and data grants; Runtime cannot inherit discovery authority through executor or login roles. Installers reject existing drift before changing authority. Active-user membership and tenant visibility predicates remain unchanged, and ordinary tenant-filtered Runtime membership access remains separate and intact.
PostgresAdmissionScannerAuthenticationContract requires explicit deployment enrollment and the configured Global session for paired credential discovery. Entry and reinstall attest exact role flags, membership options and dedicated data grants. Runtime cannot borrow executor authority directly or through the Global login; installers reject drift before changing roles or grants. Paired version/digest validation, the two-match ambiguity bound and result shape are unchanged. Ordinary Runtime data privileges are not broadened.
PostgresExternalApiKeyAuthenticationContract requires explicit deployment enrollment and the configured Global session for both exact key lookup and the fixed usage-metadata update. Native entry and reinstall attest exact role flags, membership options and effective privileges; Runtime cannot borrow executor authority directly or through the Global login. Existing drift is rejected before installer role or grant changes. Key selection and the two-column usage effect remain unchanged, and Runtime gains no direct credential-table privilege.
ResolveUserTenantRedirectionRequestHandler requires an authenticated request whose user identifier matches the server-resolved subject before accessing navigation data. IUserTenantNavigationRepository returns only a live user's identity and saved tenant preference; this path does not load UserPii through UserRepository. Membership enumeration uses the separate Global membership capability, independent of the request tenant. A saved preference is selected only when it appears in the current active-membership result. Otherwise navigation chooses the latest active membership, or returns no target when no active memberships remain. The navigation response grants no tenant access and never changes the preference.
UserTenantNavigationRepository uses the same untracked EF Core projection for
every provider: User.Id, IsDeleted and LastActiveTenantId, excluding deleted
users and disabling PII auto-includes without disabling query filters. C# owns
subject selection and membership decisions; no navigation business routine remains.
Each read uses its factory-created, sealed tenantless Global context and retains
the original connector/backend through permission admission and query completion.
PostgresUserTenantNavigationReadPermissions installs and attests exactly those
three direct nondelegable column reads for the configured Global login. Runtime
receives no User-table access; profile/audit columns, table-wide rights and writes
remain denied. Fixed catalogue/role/ACL checks are deployment primitives, not
stored business logic. The preference writer recognizes only this same compiled
read inventory, retaining its separate mutation authority. A stored preference
still grants neither membership nor tenant access.
Global is an API deployment database login purpose, not a human role. These fixed database permissions cannot enforce authenticated-current-user equality: a stolen Global credential can enumerate user/preferred-tenant associations. This accepted trust boundary depends on API authorization and credential protection, not a claim that row-level security authenticates the human. No profile access follows.
The old user_tenant_navigation_read routine and cluster-wide
event_user_navigation_owner, event_user_navigation_global and
event_user_navigation_migrator roles must be absent. Installation/readiness reject
obsolete objects or grant drift without dropping, revoking or silently repairing
them. Use a fresh isolated development cluster or a reviewed dependency-checked
operator decommissioning; a new database alone does not remove cluster roles.
No entity/schema migration or compatibility routine accompanies this cutover.
UpdateUserLastActiveTenantCommand selects an original Global owner before handler construction. HumanTenantPreferencePreparation retains the resolved self, actual request/principal references, validated credential lifetime, and immutable target. It acquires source-set, principal and support exclusions in one nonreplaying Serializable attempt. Native subject authority and the active target membership remain current through settlement; support forwarding does not replace self. Selecting a tenant imposes no tenant-status condition and grants no resource access.
HumanTenantPreferenceStore accepts only the registered preparation's current mutation. Common EF updates only the exact non-deleted user's last_active_tenant_id, updated_at and updated_by, with active membership and target existence in the update predicate. Suspended tenants remain valid preference targets; ConcurrencyStamp is neither compared nor changed. A separate EF image query checks all three resulting fields before final source/lifetime checks. The original context, sealed-null tenant lease, open connector/backend and Serializable transaction remain pinned across admission, mutation and completion; configured retries cannot replace that owner.
Global has fixed direct SELECT on User.Id, IsDeleted, LastActiveTenantId, UpdatedAt and UpdatedBy, and UPDATE on the latter three preference/audit fields. These include the navigation read subset. Runtime receives no new User authority, and this profile does not grant UserPii access or table-wide privileges. A holder of Global credentials can use those fixed column rights outside resolved-self C# admission; database grants do not authenticate a human. Other explicitly installed owner profiles remain separately attested, not inferred from live ACLs. The two old preference routines and their roles are retired. Existing installations are rejected without repair: use a fresh isolated development PostgreSQL cluster, or operator-owned dependency-checked decommissioning of obsolete roles and routines. No automatic data deletion or compatibility routine is provided. Missing membership causes whole-attempt rollback and false; cache invalidation occurs only after successful settlement. Navigation reads alone confer no write grant.
Localization governance binds the original UpdateLocalizationGovernanceCommand to its exact normalized instance effects before BEGIN. The selected Global owner retains the validated credential lifetime, committed subject and administrator sources through one Serializable attempt. Native ordinary settings writes share that owner; completeness and source/deadline checks precede commit. Settings notifications and translation-cache invalidation happen only after settlement. Signed provider identities resolve through the independent Global EF binding reader. PostgreSQL grants that configured login SELECT on the fixed nine UserExternalLogin scalar columns; the exact provider/account predicate belongs to C#. Runtime has no binding-table access. Every consumer sharing the Global login can read those columns; purpose-specific factory aliases do not create separate database identities.
LookupDataCache owns an identity-free Runtime context from IDbContextFactory for initialization and refresh. It never resolves the tenant-selected scoped context; a tenant-bound lease cannot execute the tenant-independent catalogue projection. The factory lease is disposed after materialization.
AdminContext reads persisted administrative authority through IAdministrativeAuthorityRepository on an independent Global context. PostgreSQL projects a boolean instance result or primitive tenant, organization and group scope identifiers for one exact internal user. Role claims cannot substitute for committed membership, and Runtime retains no direct membership-table access. These discovery reads do not supply transactional mutation authority.
LookupDataCache loads seventeen closed reference entity types through ordinary untracked EF queries in GlobalLookupCatalogueProviderOperations on every provider. Here "Global" names tenant-independent data, not the database login. PostgreSQL requires the enrolled Runtime session and direct SELECT on exactly the declared reference columns; no catalogue business function remains. Global, migration and email sessions cannot borrow this reader. EventType contributes only tenant-null rows to this tenantless cache, with normal EF filters and forced tenant RLS intact. Role definitions are reference data, not RolePermission, membership or administrator authority. Sharing the Runtime credential shares these reference read permissions; a factory alias is not a separate database identity.
Each read retains its original connection and Serializable transaction. These are per-read database observations, not a claim of one database snapshot across all seventeen types. The cache assembles all type maps privately, completes context and scope disposal, then atomically publishes one complete in-memory snapshot. Loads are serialized while readers remain lock-free. Initialization, admission, query and cancellation failures propagate; a failed refresh retains the last complete snapshot instead of clearing it or publishing partial success.
Localization TMS binding status uses ILocalizationSecretStatusReader rather than generic SecretBindingRepository queries. Native reads return only whether an unqualified binding exists for the selected tenant or instance fallback. They never return secret values or binding references. Native execution is a bounded data capability, not administrator authority; existing handler authorization remains mandatory.
HierarchicalSettingsReader separates hierarchy resolution and local cache invalidation from mutation entrypoints. InstanceTenantSettingsReader obtains detached native instance and selected-tenant images through ordinary settings custody. Reads retain the original context, tenant selection, connection and enclosing transaction; they do not acquire human mutation authority or grant direct access to settings tables. Ordinary native custody retains its source, completion and discovery-disclosure settlement guards.
TenantLifecycleAccessService checks public eligibility through ITenantLookupSource. The directory owns an independent Global context and exposes only active tenant identifiers, slugs and public domain reservations. PostgreSQL projects these fields through a security-definer function with column-limited owner reads and active-only RLS policies. Global receives function execution, not direct tenant table access. Unknown routine executors are rejected before reads or installer replacement.
SetupSecretProvider and DeploymentModeProvider read committed bootstrap evidence through IInstanceBootstrapStateReader rather than a Runtime mutation repository. The reader owns an independent Global context. PostgreSQL obtains the selected bootstrap image through the provider configuration contract; other providers use an untracked query ordered by generation, creation time and identifier. Native provider configuration custody covers original settings, secret bindings, bootstrap, user/login and event configuration rows. Enrollment and source images do not confer human or setup authority. Failed readiness checks do not repair drift or broaden Runtime table access.
The provider source anchor serializes changes to the original source relations until physical transaction completion. Installation and admission attest exact routine bodies, ownership, privileges, policies and triggers. Storage source custody is installed alongside provider custody so their statement triggers coexist; neither contract substitutes for request authorization.
BoundProgrammeModelCandidates records the committed recovery model in generated migrations: refund dispatch ownership, AI execution identity and effect linkage, report synchronization custody, location correction messages and configuration effect references. Five regex columns widen from 1,000 to 2,048 characters. Generated defaults on historical rows do not reconstruct authenticated recovery authority. Existing rows require the native recovery policy, not inference from empty digests or zero identifiers. Down migration removes recovery evidence and narrows regex columns; it is not a safe operational rollback after new writes.
The localization bundle import endpoint declares a 4,195,328-byte request limit (the 4 MiB bundle budget plus 1 KiB envelope allowance) and a 413 response. The strict converter rejects malformed candidates before handler execution. HTTP regressions verify 400 ProblemDetails and unchanged existing bundle bytes for duplicate keys, nested values, unknown fields, unsupported keys and excessive value length. These TestServer checks do not prove Kestrel's 413 enforcement.
ImportLocalizationBundleCommandHandler independently applies LocalizationBundlePolicy to direct command candidates before file effects. Import and TMS export handlers return fixed failure responses and log stable categories without attaching provider or filesystem exceptions. Existing administrator checks remain mandatory; these changes do not introduce a transaction-bound publication authority. LocalizationImportSchemaTransformer publishes the strict converter's object shape explicitly: required languageCode and translations, no unknown outer properties, bounded language length, entry count and translation value length. This preserves the DTO members consumed by generated clients; runtime parsing also enforces the key-family and aggregate limits that this schema does not express.
TolgeeClient and WeblateClient bound every response body to 4 MiB through TmsResponseBoundaryHandler, inside the resilience pipeline. Buffer admission precedes retry metadata readers and generated client deserialization, including error responses and responses without Content-Length. Failed admission disposes the response; cancellation flows through buffering. This transport limit does not supply translation schema validation or publication authority.
Localization provider, resolver and cache diagnostics retain stable event codes and useful failure categories without provider exception bodies, caller language strings, configuration coordinates or translation values. Translation metrics retain counters/durations while mapping labels through supported culture and closed provider/category sets; unknown values cannot become unbounded metric labels. Tests exercise real provider failure/fallback paths and an isolated meter collector. This confidentiality change does not establish TMS response-size bounds or final publication admission; those gates remain open.
BundleSchema bounds incoming file bytes before parsing and rejects the entire override on duplicate, nested or oversized translation data. Offline reads retain embedded defaults rather than publishing a partial invalid override. BundleFileWriter validates before creating output directories or replacing an existing file, serializes with safe JSON escaping, bounds output size and publishes through a temporary file plus rename. Logs retain counts and stable event codes, not paths, translation contents or raw exceptions. TMS transport bounds and final publication admission remain separate integrations.
LocalizationBundlePolicy bounds flat translation imports to 4 MiB input, 16,384 entries, 512 key characters, 16,384 value characters and 524,288 aggregate characters. The import converter validates token structure before dictionary materialization, rejecting duplicate/unknown contract members, duplicate translation names, nested values and unsupported key families. ImportLocalizationBundleDto copies caller input into a read-only ordinal dictionary, preserving the original admitted candidate. This input boundary does not provide publication authority: handler commit admission, file/TMS provider bounds and OpenAPI integration remain separate required consumers.
LocalStorageQuarantine retains physical custody independently of tenant attribution and metadata lifetime. Admission captures the local provider binding, bounded object key, canonical path hash and observed size/time evidence. Keys cannot escape the captured root or enter quarantine namespaces; non-Linux hosts conservatively collapse supported ASCII case aliases. Claims compare the original concurrency stamp and require a live lease for completion/block/retry. Reclaimed workers cannot settle newer claims. Lexical identity is not a filesystem effect check: persistence live-slot uniqueness, native admission and provider symlink/ rename custody remain separate required integrations.
RegistrationOrderCreationReceipt retains authenticated order-creation evidence independent of account, order and HTTP-response retention. Its operation fingerprint hashes the family discriminator, original tenant, account, trusted principal-scope digest and exact closed ASCII operation key. Digests normalize to lowercase; tenant attribution cannot change. The receipt contains identity digests rather than recoverable request or credential payloads. This domain primitive does not query a receipt or admit a replay: repository retention, generated persistence mappings and native creation admission remain required.
KeycloakLifecycleGrantProfile defines the closed lifecycle-client grant contract for provider version 26.1.2: a confidential service-account client with interactive grants disabled, exactly realm-management/manage-users and no realm roles. Fresh response claims must retain expected issuer, client/service-account identity and an unexpired lifetime without excess grants. The helper accepts claims only after a transport-authenticated client-credentials exchange; it does not validate arbitrary caller bearer tokens or their signatures. Provider bootstrap, response admission and lifecycle send integration remain separate consumers; this domain profile alone grants no provider I/O permission.
Password deployment settings are catalogued as public, startup-owned values: AUTHENTICATION_LOCAL_PASSWORD_BLOCKLIST_PATH and SHA256 are optional file identity inputs; AUTHENTICATION_LOCAL_PASSWORD_HASH_ITERATIONS defaults to 210000 and is validated against 100000..2000000. API normalization maps those flat keys to Authentication:Local options without overwriting explicit hierarchical values. The existing KEYCLOAK__AUDIENCE template setting is also catalogued; its authentication behavior is unchanged. Process restart captures a new immutable snapshot. The operator supplies a read-only JSON file with FormatVersion=1, Revision, Source, License and Passwords; its SHA-256 is checked before parsing. No corpus or credential value belongs in source configuration.
ConfigurePersistenceServices now attaches AddLocalPasswordPolicy after native Identity registration. The shared validator replaces composition-based password checks, and the normalized IdentityV3 hasher uses the configured work factor. Missing/unusable blocklist capture makes readiness unhealthy when the primary provider is local or not yet selected; external-provider readiness does not fail solely for absent local policy, but local credential mutation still fails closed. The startup tests invoke production composition without opening a database. Flat and hierarchical configuration are described above; lifecycle-store mutation integration remains a separate boundary.
LocalPasswordAttribute and manually instantiated request validators now use the same domain normalization policy. Login and current-password checks use the verification shape, while onboarding, replacement, password change and recovery use new-credential scalar bounds. DTO annotations no longer impose UTF-16 StringLength limits that reject valid supplementary characters. Real annotation validation and manual validation share deterministic boundary cases. These checks validate shape only: blocklist enforcement, startup attachment and lifecycle mutation guarantees remain separate backend responsibilities.
The local-password backend captures a deployment-owned JSON blocklist once, verifies its configured SHA-256 before parsing, bounds input to 16 MiB and 100,000 entries, and rejects malformed metadata or canonical duplicates. An unusable capture denies all new credentials. Registration configures IdentityV3 PBKDF2 with 100,000..2,000,000 iterations (default 210,000), replaces hashing with NFC normalization and exposes readiness without credential/path data. LocalIdentityOptions declares PasswordBlocklistPath, PasswordBlocklistSha256 and PasswordHashIterations. No corpus is bundled. Production startup, request and deployment wiring are described above; lifecycle-store mutation integration remains required.
LocalPasswordPolicy normalizes valid UTF-16 to NFC without trimming whitespace or changing case. New credentials require 15 through 128 normalized Unicode scalars; verification accepts shorter existing credential shapes while retaining the 128-scalar ceiling. A 1,024-code-unit input bound precedes normalization, and malformed UTF-16 is rejected rather than repaired. Deterministic tests use fixed ASCII and astral scalars to avoid normalization-dependent random boundaries. Request validation and production native hashing integration are described above; lifecycle mutation enforcement remains a separate boundary.
CustomPropertyPatternPolicy admits at most 2,048 characters of .NET non-backtracking syntax with a finite 100 ms match deadline. Unsupported backreferences/lookarounds and malformed patterns fail closed. Definition validators share this policy across general, event and session families, validate bounds/defaults and reject null option collections/items. Runtime validation checks input length before matching and omits rejected URL schemes from errors. All five definition storage mappings share the same ceiling; provider-model tests guard the admitted/storage boundary. Generated migration parity is still required before deployment. This intentionally rejects previously accepted unsafe syntax.
RefundAttempt retains a native operation/input digest and separates Ready, buyer handoff, buyer observation, application-fee handoff and settlement phases. BeginDispatch fences a provider invocation with a token and the original bounded mutation window; process or lease expiry alone cannot reopen dispatch. Only the current invocation token can prove no provider handoff. Buyer success keeps capacity reserved and cannot be undone by fee recovery. Nullable full-refund selection is retained for reservation-time reallocation. Persistence mappings, generated migration parity and native dispatcher integration remain required; these domain primitives alone must not be treated as provider I/O admission.
EventLocationDisclosureAudit.BindCorrection retains one RFC UUIDv7 correction message, accepting an identical repeat but rejecting replacement and unrelated audit reasons. LocationPrivacyCorrectionReceipt pairs governance effects with the exact audit message and timestamp, or erasure effects with committed intent identity, positive authority sequence and location identity. Tenant ownership is immutable. These identity-only domain receipts do not retain address payloads or foreign keys to disposable sources. Persistence mappings, generated migrations and dispatch admission remain required before deployment.
EventLocation.CaptureGovernanceState returns immutable policy identity, disclosure selection, version and concurrency facts without physical address payloads. Governance tightening creates its audit from those facts before updating the aggregate, preserves the selected fields, audience and reveal time, and advances version/audit metadata once. Detached locations remain rejected. Native governance readers and correction-publication custody are separate consumers; this domain snapshot adds no stored column or provider authority.
EventReport.RetainProviderSubmission binds one original provider submission tuple: outbox ID, case ID, historical case concurrency stamp, UTC report creation time and uppercase SHA-256 payload digest. Only an unchanged Submitted report and its unchanged Open case with matching tenant/report identity can bind. Validation precedes assignment of every field, preventing partial custody. Later case version changes do not rewrite the historical tuple. Persistence mappings, generated migrations and provider publication consumers remain required.
PaidEventPolicyVersion.RestoreActiveForPolicyEvaluation reconstructs an active evaluation aggregate with its stored policy ID, scope and positive version. It uses the same organizer, currency, refund-protection and risk-limit normalization as normal creation, and rejects missing identity or an empty tenant. The restoration factory does not load or track persistence state; repository callers must supply verified active persisted facts, and mutations retain their separate tracked aggregate. Native reader integration remains a separate prerequisite.
ConfigurationImportOperation binds one SHA-256 UTF-8 effect payload digest only after an applied or forward-rollback receipt has an outbox identity. Failed operations cannot bind it, and a second binding cannot replace the digest. ConfigurationManifestOperation binds one nonempty UUIDv7 outbox identity only in the Applied state. These are domain receipt primitives: persistence mappings, generated migration parity and publication-consumer wiring remain required before deployment; no new storage contract is claimed by this slice.
HumanCerbosPrincipalReader requires the original resolved human and authentic credential lifetime before observing persisted principal sources on the selected idle owner. It rejects machine callers and account mismatch, retains the same request/principal/tenant across the await, and validates current persisted human facts afterward. It does not retain mutation admission. The signed-source tests resolve subjects through the real identity query and exercise actual JWT lifetime and SQLite source checks; production scope-factory registration remains separate.
CerbosPrincipalBuilder.BuildPrincipalFromSources projects immutable persisted human facts into the existing policy attribute schema. Administrator membership maps are deduplicated, while event permission arrays retain their original order and multiplicity. CapturePrincipal clones the protobuf principal so later SDK mutation cannot alter retained comparison evidence. These pure operations do not validate a credential, acquire a source lease or authorize a commit. The existing reader constructor and production read path are not switched by this slice.
The managed AI moderation proposal target source reads one exact cross-tenant event on the original selected conversation owner. Discovery requires current persisted instance administration; acquisition and final validation also retain the genuine principal lease. Target discovery does not change the selected tenant or open ordinary event reads. The assignment source epoch protects target facts; administrator continuity belongs to the principal source. These primitives are not a proposal authorization decision or production preparation registration.
Managed webhook resource sources retain the original context, connection, human, selected tenant and closed command selector. Acquisition validates the source-set, principal and support leases before retaining provider locks; final reads use that same Serializable owner and see persisted writes rather than tracked changes. The pure projection resolves endpoint ownership and attempt/message/plan lineage without treating an audit identity, portal session or requested ID as authority. Immutable portal custody records retain provider evidence only. Production factory registration, provider-service release checks and final parent admission are separate consumers and remain required.
PostgresNativeQueueEnrollment binds Migrator, Global, Runtime and the existing EmailPolicyInstanceMutation/EmailPolicyTenantMutation login authorities together. All five must be distinct PostgreSQL identifiers within the unchanged 63-byte, trim/control-character boundary. The email role configuration already exists; enrollment now carries its original identities into source-owned native contracts instead of rediscovering role authority from current ACLs. No new secret source or database role configuration key is introduced by this enrollment expansion.
HumanEventAssignmentSourceFence captures the original actor, tenant and bounded event set, acquires after the actual policy-source-set lease, and revalidates persisted facts on the same connection and Serializable transaction. Its pure HumanEventAssignmentProjection preserves ordinary owner/manager roles, active permission semantics, inclusive assignment start and exclusive expiry. Resource projection respects the required creating-actor soft-delete join; organizer control remains a separate persisted membership/placement predicate, without an administrator shortcut or a new gate on ordinary policy. Unflushed tracker edits are not source facts. Production factory registration and final parent admission remain separate.
HumanSupportForwardingObservation captures validated in-process support metadata and the original canonical subject before selecting an owner. The managed support fence acquires only after policy-source-set and principal revalidation, then returns fresh persisted session/governance facts on the same Serializable connection. HumanSupportAccessProjection applies original defaults, inactivity, lifetime, read-only compatibility and target-tenant boundary denials after final reads using the caller's trusted time sample. It never grants the requested operation. Native AllowedValues arrays remain serialized metadata at the managed boundary. Production factory registration and parent final admission remain separate work. SQLite session readers preserve actor/session/tenant bounds and compare UTC offsets at full tick precision, avoiding unsupported DateTimeOffset LINQ comparisons and millisecond rounding at the exclusive expiry boundary.
HumanPrincipalSourceFence observes fixed account, binding and membership inputs on the selected connection, acquires policy-source-set exclusion first, and retains the original Serializable transaction through revalidation. Native tenant-owner proof binds the canonical human subject, connection and transaction; tracked-write validation consumes that proof only for its declared tenant foreign key. All save paths run the shared validator, preserving its existing AI, checkout and payment reconciliation branches and persisted-visibility fallback. Human audit attribution uses the original canonical subject, not a mutable current-user service. Context disposal clears retained human proofs and admission state as well as the subject. This supplies custody primitives; production scope registration remains separate.
AiToolExecution reserves a confirmed event-draft action against its exact conversation, principal, acting actor, payload digest and one event identifier. Primary linking is lifetime-bounded and cannot switch targets; already linked recovery is idempotent. Image upload/storage custody cannot replace the original source, session or object. Success requires the linked primary effect, and settled failure cannot reopen. AiProposedAction consent versions bind displayed source and owner inputs while excluding mutable status for recovery. These are domain state primitives, not authorization or durable deployment by themselves. Persistence configuration, generated migration parity and production tool callers are separate programme dependencies and remain pending.
PostgresStorageProbeSourceContract adds a dedicated generation sentinel for the actual storage probe. Whole-relation BEFORE-statement writers on SystemSetting, TenantSetting, SecretBinding and InstanceBootstrapState advance that sentinel for inserts, updates, deletes and truncation, including empty ranges and unrelated metadata keys. Acquisition holds it on the original Serializable Runtime-tenant or Global-instance session; observation returns only the generation. The capability adds no original source-table access. Exact source, role, privilege, trigger and body admission, including the optional provisioning writer footprint, rejects drift without repairing it. Managed probe sessions and production wiring remain separate work.
PostgresHumanWebhookResourceSourceContract projects fixed webhook resource lineage for a selected actor, tenant, operation, consumer kind and target. Its nonlogin owner reads declared columns only; original Runtime and Global sessions retain separate purpose/tenant rules. Serializable acquisition and compulsory contributor writers share a private epoch, including absent target ranges. Exact source, body, role, privilege, trigger and stamped tenant-policy admission rejects drift without repairing it. These are persisted resource facts, not provider receipts, delivery approval or authorization decisions. Production installation and verified managed consumers remain separate work.
PostgresHumanEventAssignmentSourceContract projects fixed assignment and team facts for a selected actor, tenant and at most 128 event identifiers. Serializable acquisition and compulsory contributor writers share a dedicated epoch, including empty assignment ranges. ApplyAsync installs the assignment capability and the mandatory AI moderation proposal target capability in the same original migration transaction; the latter reuses that epoch for one exact cross-tenant event. AI target discovery requires a current persisted platform.admin account and an original Runtime tenant, rejects selected-tenant/deleted/missing targets, and does not itself grant moderation authority. Administrator continuity through commit is owned by the preceding principal lease, not by the assignment epoch. Exact source, body, privilege, trigger and stamped tenant-policy admission rejects drift. Production registration and managed-consumer wiring remain separate work.
PostgresHumanAdministratorCommitAuthorityContract supplies fixed boolean instance-administrator, tenant-administrator and subject-continuity fences. It borrows the enrolled business owner's original Serializable transaction. The nonlogin owner locks the original account, provider binding and role/membership contributors, with validated foreign keys preserving insertion exclusion. Tenant administration requires an active tenant/member and an unrevoked same-tenant administrator grant; instance administration requires the persisted platform.admin role. Primary and email mutation logins retain distinct declared purposes. Exact body, privilege, source and stamped tenant-policy admission rejects drift. Native installation is explicit; managed consumers and production wiring remain separate.
PolicySourceSetFence and HumanAuthorizationRouteSource compose the native prerequisites on one selected physical connection and original Serializable transaction. They capture the context-pool lease and tenant/import-purpose binding, acquire source-set exclusion first, and revalidate it around route reads. SQLite starts a physical write transaction; SQL Server and MySQL use fixed-order physical source locks, with InnoDB required for MySQL. PostgreSQL attests both native contracts before and after the six-key projection. AllowedValues JSON is preserved as serialized setting metadata instead of being deserialized as a JSON string. Configuration-import ownership binds only once on a fresh instance owner and is cleared with its context lease. Production registration and mutation consumers remain separate work.
PostgresHumanSupportAccessSourceContract projects only the selected actor/session support record and the fixed support governance settings. Runtime requires its original tenant and matching resolved tenant; Global requires an instance purpose. Serializable acquisition updates a private anchor and locks matching source rows. Compulsory source writers touch the same anchor, excluding concurrent inserts even when observation found no session. Exact body, role, privilege, trigger, source column and stamped tenant-policy checks reject drift rather than repairing it. This native prerequisite does not install production wiring or grant support access.
PostgresHumanAuthorizationRouteContract exposes only the six source-owned route setting keys through a SELECT-only nonlogin owner. Runtime reads require the selected tenant; Global reads require an instance purpose and null tenant. The acquisition entry point additionally requires Serializable isolation, but does not itself exclude source membership changes: policy-source-set exclusion must be composed by the managed consumer. Exact body, privilege, source-column and stamped tenant-policy admission rejects drift without repairing it. Installation and consumer wiring remain separate from this executable native prerequisite.
PostgresHumanPrincipalSourceContract installs execute-only principal observation, Serializable acquisition and validated Local-session revocation reads on the original enrolled sessions. Fixed actor/account/tenant projections run under a nonlogin owner with column-scoped privileges. Acquisition and compulsory source writers update one private anchor, closing absent-row insertion races; existing rows are locked on acquisition. Admission compares exact routine bodies, roles, ACLs, triggers, foreign keys, uniqueness and stamped tenant RLS policies.
The provisioning shared-writer leaf retains its owning source inventory, privilege contract and exact changed-trigger body without importing provisioning execution. Whole absence does not create a reverse installation dependency; any footprint requires the complete source-owned assertion. These primitives do not wire production authentication, mutation leases or provisioning installation.
ProvisioningSharedWriterAssertions.Writer is the executable catalog verifier shared by source-owned native compositions. Expected bodies, targets, role stamps and privilege contracts come from the owning implementation, never live ACLs. It verifies exact SECURITY DEFINER routine shape, body hash/comment, compulsory trigger flags and relation targets, rejecting extra trigger attachment as drift. Assertions read catalogs and do not repair native objects. Entire protocol absence is allowed only in the declared migrator's installation window; partial footprints must satisfy the complete assertion. The inventory and owner bridges remain separate composition work, not substitutes for this verifier.
For navigation-owned tenant tables, RLS metadata follows the one mapped ownership path referenced by the named tenant filter instead of selecting the first foreign key. Intermediate joins must be required dependent-to-principal navigations with single-column keys; missing, ambiguous or cyclic paths fail closed. Predicates recursively join until the mapped tenant column, with distinct aliases per hop. An explicit target schema overrides mapped schemas at every level, so isolated native installations cannot accidentally bind parent relations in another schema.
Generated tenant RLS policies carry an event-tenant-policy:v2 comment combining the SHA-256 of the mapped model condition with PostgreSQL expression-tree digests for USING and WITH CHECK. Native admission consumers can compare both model and installed policy identity without relying on search-path-sensitive deparsed SQL. Stamp generation itself is not drift admission: consumers must enforce the match. Fingerprint generation uses the same mapped ownership predicate as the installed policy, including recursive joins and explicit target schema selection.
PostgresPolicySourceSetContract provides an explicit native source-set exclusion capability, not yet production installation wiring. ApplyAsync borrows an existing transaction or commits its own. It installs a non-PII singleton and compulsory writer triggers on RolePermission plus the six governance setting keys. Acquisition and matching writers update the same sentinel, excluding membership changes even when no relevant source row existed when observation began.
The execute-only acquisition entry point requires Serializable isolation and the enrolled login, purpose and tenant context. Its nonlogin owner can modify only the private sentinel, not the policy sources. Exact role/ACL/table/trigger/routine-body attestation rejects drift rather than repairing it. Existing migrator ownership installs the capability before traffic; production enrollment and managed lease consumers remain separate work.
HumanPrincipalSourceValidation requires one current undeleted account and its original provider binding. Local sessions additionally require ready credential state, matching validated stamp/email state and completed operation/personal actor continuity. Non-Local identities cannot borrow Local session facts.
HumanAuthorizationRouteProjector selects an immutable route from the six fixed fresh setting facts and captured deployment scalars. Tenant BYO precedes instance mode for ordinary resources; platform settings governance never routes to tenant BYO. Local decisions require co-located decision sources. Route equivalence compares decision inputs, excluding the observed admin endpoint. These pure projections provide neither source locks nor policy decisions; native acquisition and final consumer admission remain separate prerequisites.
HierarchicalSettingMerge accepts immutable SettingMergeFacts as well as entity maps. Entity reads adapt to facts and share one precedence evaluator with source observations: instance locks, tenant locks, registered scope bounds and child overrides retain their ordering. HasTenantContext remains distinct from a present tenant override; missing SMTP delegation therefore locks even an empty tenant context, while instance-only evaluation is unaffected.
HumanCommitSubject clones the authenticated principal before awaiting the existing user-resolution query, preserving provider-binding precedence and rejecting unresolved or empty identities. It captures account identity, not an authorization grant. IHumanCommitScopePreparation selects that observation before constructing consumers on their original committing database owner.
The Application contracts separate observation from transaction-bound acquisition: IPolicySourceSetFence excludes policy-source membership changes; IHumanPrincipalSourceFence and IHumanAuthorizationRouteSource expose immutable facts and validating leases. Route facts contain the six fixed governance keys and retain raw system/tenant setting values and locks for provider-native merge semantics. IHumanActionCommitFinalizer registers source and timed-assignment validation for the actual committing owner. These contracts alone install no native locks, finalization hooks, registrations or active mutation admission.
Successful persisted-key authentication stores a server feature containing the authenticated row identity, owner, tenant, status, expiry and immutable scope snapshot. PersistedApiKeyAuthenticationProof binds that snapshot to the exact API-key ClaimsIdentity. Copied claims, additional authenticated identities and human subject/session claims cannot substitute for that identity.
MachinePrincipalAccessor exposes IPersistedApiKeyProofAccessor only when the current key, owner, tenant and scopes still match. An internal execution principal or configured client has no persisted-key proof. All three accessor interfaces share one scoped instance. The proof establishes authentication provenance; mutation commit-time authority and source-row revalidation are separate consumers.
The API captures signed credential bounds only after native JWT or ATProto session validation. HumanAuthenticationLifetimeObserver returns immutable request-local evidence, not an authorization grant. It requires final successful authentication, the same selected scheme, the exact authenticated identity and the original provider account key. Replaced principals and unrelated requests cannot inherit the observation.
Storage configuration retains the original request principal and independently snapshots its identity and claim collections and the authenticated identity and claim references, selected scheme, and detached issued/expiry property values. Repeated native authentication may return new ticket or principal containers; those wrappers' object identities are not credential evidence. Changing the original request's collections, including adding an unauthenticated identity, still fails custody. The original API-issued lifetime observation remains mandatory. Request custody also freezes the presence and typed UUID of the server-resolved tenant marker, separately from the immutable selected persistence tenant. Missing, replaced or malformed captured tenant markers fail closed without selecting another persistence owner.
StorageConfigurationAuthority captures principal and support facts while the
selected owner is idle. External storage-credential resolution finishes before
the database transaction. HumanSettingsMutationPreparation then acquires the
existing source-set, principal and support leases in its one Serializable
attempt. Active checks reuse those actual transaction-bound leases and compare
fresh facts with the original snapshots; they never call idle-only observation
or cache an authorization success. The stateless source comparison grants no
authority, and StorageConfigurationWriter still requires its original physical
owner and exact complete effects.
Terminal admission runs the fixed writer's completion and data-image check first. The coordinator then performs its final request/authentication recheck, fresh held-source reads, current administrator predicate and original-facts comparison. Credential and support deadlines are sampled after the blocking work. Denial before commit rolls back its effects. The unit of work distinguishes unknown commit outcomes and cleanup failures after acknowledged commit from rollback; callers must retain those settlement distinctions when retrying. Cache activation follows acknowledged settlement. The prepared observation cannot replay, so a retry starts observation and preparation again.
Executed Release evidence comprises all 16 NativeTenantStorageHttpTests cases
and both principal-container variants of
StorageProbeOriginalActionTests.NativeStorageConfigurationAcceptsReauthenticationAndRejectsChangedOriginalSources:
18 passed with zero skips. These native SQLite controls use validated credentials
and the real coordinator, leases and finalizer, with controlled external secret
and S3 boundaries. They cover late binding/grant/support-source changes, complete
rollback and warm-cache retention, consumed-observation rejection, a fresh
transaction on retry, and original-request membership drift. They do not certify
PostgreSQL, other configuration families, live S3 or cross-server cache refresh.
Expiry and optional not-before come from the authenticated raw token payload, not mutable claims or ticket properties. Duplicate, malformed or overflowing bounds produce no evidence. IsValidAt preserves the validating scheme's inclusive clock skew anchored to signed expiry. Local evidence also retains the originally validated session authority. Mutation consumers must bind the account and recheck the evidence after their final blocking operation; consumer adoption is separate from this capture mechanism. Serialization ignores evidence fields and ToString returns only the type name.
Resolver, operator-identity and provider-configuration authorities retain the authenticated identity and claim objects, selected scheme and detached ticket issued/expiry values. Native authentication can return new result containers without changing those facts; container references do not establish authority. The original request principal remains retained throughout the operation.
Human sources still require their original API-issued credential lifetime and resolved subject. Provider human sources additionally retain persisted principal and support snapshots. Resolver and operator-identity mutation coordinators own their persisted grant and binding checks. Setup sources instead keep the actual accepted setup secret and existing trusted setup lease where applicable; no human lifetime observation is manufactured for a setup-secret identity. Credential-source checks do not certify a completed mutation or its transaction.
Provider human mutations reuse the original HumanSettingsMutationPreparation
coordinator's sole transaction-bound source-set, principal and support leases.
The three active ProviderConfigurationSettingsWriter checks use the native
source's request-only authentication and API-issued lifetime checks, without
calling idle-only database observation. The coordinator supplies freshly read
held-source facts to the stateless comparison with the original principal and
support snapshots. It does not reacquire leases or cache an authorization result.
The fixed writer completion callback is registered before coordinator acquisition. Its complete data-image check precedes the final request/authentication cycle, fresh held-source reads, current administrator predicate and terminal deadline sample. The original physical owner, connection and transaction, and complete source/effect snapshots remain mandatory. Activation follows acknowledged settlement. Denial before commit rolls back the attempt; unknown commit outcomes and cleanup failures after commit retain their existing unit-of-work distinctions. An attempted observation cannot replay, and a retry must observe current state again.
The human authorization-provider controls retain both canonical rows in the original Serializable transaction before grant removal or binding-key drift, with an affected-row witness. Denial restores the setting rows, grant identity and binding key and prevents activation. Binding drift can be rejected by the writer's original-login data-image check before the final held-source callback; the grant case verifies current authority beyond that data image.
IProviderConfigurationSetupSource.AcquireCoordinationAsync requires the exact
original child IUnitOfWork and its selected idle context and connection. It
reuses HumanSettingsMutationPreparation.SettingsCoordination to acquire the
existing cooperative Human and Discovery gates before keyed locks and BEGIN.
The same leases remain held through settlement and notification completion and
are released in reverse order. Setup retains its genuinely accepted setup secret
and trusted setup lease; it does not manufacture a human receipt or expiration.
Cleanup runs after the operation's captured failure, outside a C# finally
block. The original exception and stack are rethrown when cleanup succeeds;
cleanup evidence preserves the existing precommit, unknown-outcome and
committed-cleanup classifications. A controlled commit fault can leave observed
persisted rollback while still reporting HumanMutationOutcomeUnknownException.
That observation does not downgrade the caller's unknown outcome or allow replay.
Credential relevance is projected once from the complete original and requested provider groups and the frozen deployment selection. It limits material acquisition, while the writer still retains the complete database and binding image, including unrelated remote keys. The Application contract carries Keycloak status and public authority/client coordinates and Google status; retained references, credential values and value equality stay private to the native source.
The source captures detached actual consumer/provider coordinates and selected
credential-authority metadata before reading the original image.
ObserveCurrent distinguishes genuine revision absence from malformed configured
revisions, which fail closed. Strict maintenance ReadCurrent still requires a
usable revision. Profile metadata never establishes credential readiness. An
irrelevant provider causes no acquisition; forbidden paths remain Invalid
without capture or resolution, and relevant permitted material without a profile
revision remains Unavailable. Denied and unavailable entries remain in the late
comparison even without a retained handle. Genuine permitted Combined material
acquisition retains its native provenance and private value comparison.
Empty Local material sets still retain command, consumer, authority-coordinate and profile custody. Existing request, acquired-fact and terminal-time checks recheck those inputs synchronously, including after fresh held-source reads. No credential provider is queried between the final authority cycle and commit. The API never borrows BFF-only credential roots. This adds no Split remote-readiness authority.
Direct AuthProviderConfigurationService.ApplyConfigurationAsync settles the
observed mutation. The authentication command handler separately invalidates
mode and refreshes JWT authority after acknowledged settlement. A direct-service
test must not expect that handler callback. Actual-handler controls require
activation only after independently visible committed effects and reject a
skipped writer.
Sixteen distinct controlled-HTTP/native SQLite cases passed across two admitted API-test DLL cohorts, with zero skips. The earlier checkpoint contributes genuine setup authorization (1), human authorization-provider authority (3), and Local binding/profile/profile-installation refusal (3). Its separate unchanged Local case failed an incorrect positive handler-callback assertion; that failure is retained and is not counted as a pass.
After the one-line direct-service assertion correction, the second cohort passed the corrected unchanged Local case (1), remote credential-ownership denial (2), setup authorization-provider settlement (4), and actual authentication-handler activation (2). The unchanged case reaches independent persisted effects, warm configuration readback and replay checks. Setup controls witness pre-BEGIN gates, late setup lock/header changes and the controlled commit-fault outcome. The earlier seven passes are not relabeled as a fresh sixteen-case single-DLL run. The separate local profile-reader class passed all nineteen cases, with zero skips. These receipts do not certify PostgreSQL readiness, Combined execution, upstream remote sign-in, other startup paths or cross-server cache propagation.
Support-access request auditing preserves the completed HTTP response when
context retrieval or audit persistence fails. Its warning omits the exception
object, so provider messages and inner exceptions cannot enter diagnostic sinks.
The failure metric category remains support_access_audit_persistence_failed.
CorrelationIdMiddleware creates a UUIDv7 in compact N format per request.
It stores that server-owned value in request items and its logging context,
then returns it through X-Correlation-ID when the response starts. Neither
X-Correlation-ID nor X-Request-ID request headers confer diagnostic authority;
even printable caller values are ignored rather than copied into responses.
ApiDiagnosticTraceProcessor is registered before ServiceDefaults exporters.
On export it clears inherited baggage and trace state. HTTP client spans retain
status and a fixed dependency label; server spans retain authored route metadata
instead of caller-selected URL segments. Exception descriptions and arbitrary
HTTP tags are removed while non-HTTP business counts remain intact. Instrumentation
exception recording is disabled for ASP.NET Core and HTTP clients.
Request completion logs use the safe route identity and a closed HTTP-method
set, falling back to route-unresolved and OTHER. The active request span's
URL tags are redacted before request completion reaches downstream exporters.
The final Serilog filter excludes ASP.NET Core, IdentityModel, HTTP/gRPC client, EF Core and host lifecycle categories. Their framework-authored events can carry raw URLs, SQL, identifiers and exceptions. Native API diagnostic categories remain available; severity overrides alone do not establish confidentiality.
UseApiDiagnosticLogging decorates Serilog's actual ILoggerFactory.
Its loggers discard BeginScope before inherited properties can overwrite
native event arguments. Logging, level checks, provider forwarding and disposal
delegate to the original factory. LoggerFilterOptions.CaptureScopes alone
cannot govern Serilog's replacement factory. Tests use this same production
composition with both values of that option.
The API Serilog pipeline also omits FromLogContext in both host composition and
shipped configuration: inherited scopes can overwrite native event fields
before a sink enricher can validate them. Do not re-enable scope ingestion.
ApiDiagnosticLogEnricher removes contextual properties unless the event template
binds that field, retaining SourceContext for producer attribution. Emitters
must still use bounded arguments and omit private exception objects; enrichment
does not sanitize template arguments or exception contents.
Storage readiness reports provider_unavailable or policy_unavailable
instead of exception subtype names. Sink tests exercise scheduler and startup
worker outcomes with private canaries and colliding inherited field names.
ApiTenantResolutionMiddleware completes preliminary resolver, slug-cache and
bootstrap-directory reads in an independent service scope, disposes that scope,
then binds the request tenant. Exempt or already-resolved requests do not
instantiate those preliminary repositories. Authentication lookup and claims
enrichment use purpose-specific independent readers, so neither preliminary
routing nor authentication freezes the request's database lease with a missing
or different tenant.
The request's PostgreSQL context freezes EF and session tenant identity together. A later identity change rejects SQL rather than retargeting a pooled connection. Exact-user administrative discovery does not confer arbitrary private-row access; handlers still authorize their concrete tenant/resource. See MULTI_TENANCY.md for lease/factory ownership and SECURITY-MODEL.md for ordinary-role admission and purpose-bound Global capabilities.
AiCredentialReference.TryCreate produces a provider-specific secret key,
scope, optional tenant ID and empty qualifier; it never contains a secret value.
Tenant-owned transports cannot select instance credentials. Tenant scope requires
a nonempty admitted tenant ID. Azure OpenAI accepts instance scope only.
Credential-free selection is explicit and limited to the fake and compatible
providers; unknown providers and invalid scopes fail without a reference.
The secret registry defines ai.openai_compatible.api_key and
ai.anthropic_compatible.api_key for instance or tenant scope, and
ai.azure.api_key for instance scope only. Their default authority names are
AI_OPENAI_COMPATIBLE_API_KEY, AI_ANTHROPIC_COMPATIBLE_API_KEY and
AI_AZURE_API_KEY under /ai. This value contract does not resolve credentials
or replace transport admission and secret-consumer authorization.
AI governance and tenant DTOs carry CredentialScope, not plaintext ApiKey
or ApiKeyConfigured. Prompt payloads carry the scope with provider, endpoint
and model metadata. Infrastructure re-resolves hierarchical settings and requires
the payload selection to match before resolving the provider-specific secret.
The secret result must retain the requested key, scope and tenant identity.
Missing, unavailable or unauthorized credentials never trigger scope fallback.
Provider or endpoint source metadata establishes transport ownership, even when an override has the same value as its parent. Tenant transports use public-network clients; instance transports use separate clients constrained to the configured origin. Azure SDK requests use the same selected HTTP transport. Health reports credential capability asynchronously without exposing the value or probing a provider endpoint.
Model discovery accepts explicit instance or tenant scope. Instance discovery requires platform-administrator authority and the configured compatible-provider origin; tenant discovery cannot use instance credentials or its local-network opt-in. Controllers delegate authorization to the registered discovery query. The OpenAPI enum catalog and generated client publish the same scope contract.
Ordinary settings writes reject ai_assistant.api_key with
ai_credential_setting_retired, including changes that rename a tracked retired
key. Existing rows are not deleted or used as fallback. Deployment maps
AI_CREDENTIAL_SCOPE; AI_API_KEY no longer populates provider options or seeds
ordinary settings. Secret authority aliases remain provider-specific.
ReservedTenantSlugs.IsReserved rejects the operational roots alive,
vapid-public-key, scalar and quartz, as well as any root containing a dot.
The dotted-root rule covers generated asset fingerprints and metadata filenames
without enumerating each build output. Tenant create/update validators consume
this predicate; the finite All set is not an exhaustive list of rejected values.
This validation does not rename existing tenants or change endpoint authorization.
The moderation application root is reserved in both this catalog and the BFF's
resolver-configuration outage fallback, so /moderation and its child routes do
not select a tenant when the resolver API is unavailable.
Authenticated GET /api/operator-identity-metadata dispatches native
GetOperatorIdentityFormOptionsQuery and assembles a HAL resource through the
standard registered assembler/policy. The exact route is tenant-independent so
an unpublished or absent directory cannot block its static vocabulary; this does
not bypass authentication or grant document access. Responses are private/no-store.
The immutable DTO snapshots its collections. Operator kinds come from
TenantDirectoryOperatorKinds.All; country choices project the runtime
CultureInfo/RegionInfo data, deduplicated and ordered by alpha-2 code. These are
display choices, not a new jurisdictional compliance registry: domain validation
still owns the alpha-2 shape. Empty or invalid region data returns
countryState: Unavailable and operator_identity_countries_unavailable rather
than a partial mandatory selector. Country display names use runtime localization;
kind and field label/help identifiers are stable client localization/association
keys, not translated prose supplied by this endpoint.
Shared legal-identity field lengths reuse Domain constants. Disclosure and paid
commerce requirements are separate; saving a draft still permits missing fields.
Registration identifiers are optional, while authority choices are explicitly
empty with registrationAuthorityState: NotSupported. Instance-specific identity
and readiness rules remain on the value-bearing document contract.
No identity service, tenant repository, credential provider or logger participates
in the metadata query. HAL supplies authenticated self and refresh, omitting
unresolved links. Normal authenticated instance identity documents expose the
form-options lookup relation; the setup-secret scheme does not gain access.
Metadata never supplies an edit link. Consumers must retain the independently
authorized identity document's mutation links and revisions.
GET/PUT /api/instance-operator-identity return separate publicDisclosure and
paidCommerce assessments, each containing isReady, failureCode, and bounded
reasonCodes. The former top-level readiness fields are removed. Authorized
administrators may save syntactically valid incomplete drafts after setup as well
as during setup; saving never grants a protected capability. Disclosure does not
require commercial terms, but paid commerce does. Missing or corrupt documents
fail both assessments closed. Routes, setup/admin authority, and revision conflicts
are unchanged.
Authenticated tenant administration exposes:
| Route | Operation | Contract |
|---|---|---|
GET /api/tenant/settings/documents/directory-operator-identity |
GetTenantDirectoryOperatorIdentityDocument |
HAL document with grouped payload, capability readiness, concurrency revision, and authorized links |
PATCH /api/tenant/settings/documents/directory-operator-identity |
PatchTenantDirectoryOperatorIdentityDocument |
Presence-aware legal/contact/link groups plus expected concurrency revision |
The PATCH binds tenant scope from the authenticated context, validates the draft
in the Application handler, records actor audit, invalidates public caches, and
returns 409 for stale revisions. HAL _links.edit is the client affordance
authority.
Anonymous public settings and shell responses contain separate
directoryOperator and instanceOperator objects. If the exact tenant identity
is unavailable for PublicDisclosure, both endpoints return non-cacheable RFC
7807 503 with stable code: tenant_identity_unavailable; no partial shell or
branding fallback is cached.
Paid checkout composition contains structured directoryOperator.
PaidOrderAcceptanceDisclosureDto contains grouped organizerMerchant,
tenantDirectoryOperator, and instanceOperator evidence plus a versioned
acceptance template. Removed breaking fields include the former
branding-derived directory prose and the prior flattened operator/provider
acceptance properties. The checked-in OpenAPI document and generated NSwag
client are the only browser contract source.
Audience: Integrators | Contributors | AI agents Status: Implemented Owner: API Last Verified: 2026-08-26 Source Anchors:
Explore.API/Program.cs,Explore.API/Controllers/,Explore.API/Middleware/,Explore.API/Hateoas/,Explore.API/Authentication/,Explore.API/Extensions/,Explore.API/OpenApi/,Explore.API/Explore.API.csproj,Explore.Blazor.Client/Explore.Blazor.Client.csproj,Event.API.IntegrationTests/Features/ContractInvariantsTests.cs,Event.API.IntegrationTests/Features/OpenApiParityTests.cs
This document describes the full API behavior in Explore.API: the middleware pipeline, rate limiting, request timeouts, caching strategy, HATEOAS implementation, specification pattern, error handling, content negotiation, and client-generation flow.
Controllers using EventControllerBase.TryParseConcurrencyStamp require one
strong quoted If-Match value containing a non-empty, hyphenated GUID. Bare
GUIDs, weak tags, wildcard/list values, unmatched or repeated quotes, and empty
GUIDs return validation errors before dispatch. Surrounding header whitespace
is permitted; the parsed stamp reaches the handler for the existing stale-write
check. No client timestamp or new concurrency authority is introduced.
For task-first integration guidance, use API_COOKBOOK.md. Generated OpenAPI output remains the endpoint and DTO reference; Scalar is a development/testing UI over that contract.
The profile's internal publicUrl preserves scheme, port and path base. The
InteractiveServer setup component supplies it from the effective BFF request, with
no URL form field. Persistence still requires authorized setup. PUBLIC_BASE_URL
overrides automatic resolution and participates in the journey generation. There
is no generic address launch gate; subdomain routing requires an explicit base
domain when that capability is enabled. Response-only ownership flags are absent
from the writable profile contract.
GET /api/instanceonboarding/journey requires active setup authority or instance
administrator authority and returns a private, no-store HAL snapshot. Declarative
[Authorize] admits the exact setup-secret authentication scheme or a normal
authenticated principal; the action still requires active setup or persisted
instance-administrator authority. On this exact GET, an Authorization header
selects normal bearer validation even when a setup header is present. The BFF
forwards its protected setup authority alongside that bearer; the action validates
setup independently. A pre-administrator bearer alone remains insufficient, and a
stale setup cookie cannot shadow a persisted administrator's ordinary session.
Missing authentication returns 401; an
authenticated non-administrator without setup authority receives 403. The native
GetInstanceOnboardingJourneyQuery composes bootstrap status, selected deployment,
existing provider configuration contracts, persisted profile, operator identity,
and existing preflight checks. Two sequential projections must agree; changing,
unavailable or contradictory sources return Failed with a bounded reason and
refresh, without mutation affordances. This is a read projection, not a database
transaction or a new workflow engine.
Provider states are Ready, ActionRequired, DeploymentRestartRequired,
Unavailable, and Failed. Deployment ownership alone never implies readiness.
Checks carry requirement category, remediation authority, restart requirement,
reason code and action relation; only actual HAL links authorize UI actions.
Generation is an opaque content fingerprint, not completion authority. Both
interactive completion POSTs require an Available snapshot, the exact current
generation, and every projected blocking check to pass, including authorization
provider readiness. Failure returns 409 ProblemDetails with a journey refresh
relation before Local credential reservation or external administrator creation.
Missing HAL links are never the server-side enforcement mechanism.
Interactive completion reprojects readiness and generation inside each owning transaction under the bootstrap mutation fence. Profile save uses that same fence and rechecks durable completion, so a profile committed after HTTP admission cannot be overwritten by stale completion, and an admitted profile cannot write after completion. The fence covers an absent bootstrap row as well as an existing one. Local reservation validates before credential creation and final convergence validates again; a stale final attempt leaves the existing reservation recoverable with refreshed setup state. Serializable retries repeat admission. Reserving a Local operation does not change the journey generation or repository readiness.
The duplicate GET /api/system/onboarding-preflight and its generated method are
removed with no alias. Clients refresh the journey, and save profile through the
existing setup-only PATCH operation before refreshing readiness. Persisted profile
projection includes the stored authoritative host; it does not invent a URL scheme or
persist the non-persisted purpose/time-zone fields.
The private Keycloak operator surface is rooted at /api/instance/keycloak.
Every request evaluates active setup authority or persisted instance-administrator
authority at request time; a route attribute or a receipt ID never grants that
authority. All seven responses are private, no-store. None participates in
generic idempotency response storage, so a historical response cannot replay
current provider or administrator authority.
| Method and route | Operation | Contract |
|---|---|---|
GET /api/instance/keycloak/connection |
GetInstanceKeycloakConnection |
Returns sanitized effective binding and available HAL actions; it never returns credentials. |
POST /api/instance/keycloak/inspect |
InspectInstanceKeycloak |
Performs foreground, read-only discovery or privileged inspection. Advanced-form administrator credentials are accepted only for this request. |
POST /api/instance/keycloak/plans |
PlanInstanceKeycloak |
Stores a server-authored, credential-free approved-operation receipt after validating the current target and authority. |
GET /api/instance/keycloak/operations/{id} |
GetInstanceKeycloakOperation |
Returns a private receipt only to a currently authorized requester that satisfies its creator or setup-generation binding. |
POST /api/instance/keycloak/operations/{id}/apply |
ApplyInstanceKeycloakOperation |
Applies only the receipt's reviewed scope and requires fresh advanced-form administrator credentials when provider privilege is needed. |
POST /api/instance/keycloak/operations/{id}/reconcile |
ReconcileInstanceKeycloakOperation |
Reads provider state and updates the local outcome; reconciliation never writes to Keycloak. |
POST /api/instance/keycloak/operations/{id}/cancel |
CancelInstanceKeycloakOperation |
Prevents remaining unsent work where possible; it never rolls back a submitted provider request. |
A proposal receipt expires 15 minutes after creation. Setup-created receipts are bound to their server-derived setup generation; administrator-created receipts are bound to their verified creator. Apply, read, reconcile, and cancel recheck that binding as well as current setup-or-instance-administrator authority. The receipt contains only allowlisted nonsecret target, approval, state, and outcome data: it contains no administrator username, password, token, runtime credential, or provider response body. An uncertain or partial provider result remains visible until an operator performs inspect-only reconciliation. The service neither retries mutating provider calls automatically nor rolls them back.
The request schemas are KeycloakInspectionCredentialsDto,
KeycloakOperationPlanInputDto, and KeycloakOperationCredentialsDto. These
Application records retain write-only credential fields and sealed redacted
ToString() diagnostics, including through derived records and enclosing native
requests. Planning intent is the catalogued string enum KeycloakOperationIntent
(RepairClient, CreateClients, CreateRealm), not an integer schema. The four
mutating native ports and handlers reside in their Requests.Commands and
Handlers.Commands namespaces. Authorization inventory dispositions name only
those four commands and their existing handler-owned authority checks; they do
not add a pipeline bypass or require an already-existing administrator during setup.
Public GetEvents now returns EventDiscoveryTraversalResource.
cursor replaces public pageNumber; pageSize is 1..100.
The response carries snapshotCount, truncated, expiresAt, hasMore,
_embedded.items and _links.self/_links.next, without exhaustive totals or
random-page relations. Follow next with the same filters; changing criteria
invalidates the authenticated continuation. Invalid/expired/changed continuations
return no-store 400/410/409 ProblemDetails, and unavailable authority/capacity
returns 503. See the API changelog and
the authority ADR.
The OpenAPI document defines wire shape; repository generation policy defines the checked-in C# shape. Pinned NSwag first emits POCO syntax, then eng/tools/Explore.GeneratedContracts converts structurally eligible response/value schemas into nominal records without changing JSON names, requiredness, nullability, HAL relations, operation methods, or wire payloads. Protocol inputs, nested request graphs, HAL resources, inherited schemas, clients, exceptions, file wrappers, and explicitly mutable UI/service contracts remain classes. Generated record properties are init-only except [JsonExtensionData] AdditionalProperties, which stays settable for System.Text.Json AOT compatibility.
This is a source-level breaking change for consumers of the generated C# client: use object initializers or with copies instead of post-construction mutation. It is not a wire-format compatibility layer, and the pre-v1 repository carries no legacy generated-client variant.
Build Explore.API in Release to export the schema, then build Explore.Blazor.Client to regenerate the client. Generation alone uses dotnet msbuild src/Explore.Blazor.Client/Explore.Blazor.Client.csproj -t:GenerateApiClient -p:Configuration=Release, not a direct NSwag invocation: the target includes repository repairs, validation, and publication. API export and client preparation write private intermediates; publication replaces each primary file through a closed temporary sibling and same-filesystem rename. Failed preparation leaves the published file unchanged, and identical bytes do not change its timestamp.
Within a solution build, client builds wait for the API's normal build/export target before preparing or compiling the client. The standalone architecture-project entry point establishes the same order before resolving its references. These dependencies preserve ordinary global-property identities; verification does not request another API/client variant or propagate a schema-path override. Standalone client builds remain authoritative-schema-driven, and the generation-only command above operates on the selected schema without compiling either application. Incremental preparation compares selected schema bytes with the captured schema, so switching to an older-timestamp input cannot reuse another contract's completion stamp.
The client compiles its captured intermediate source. Architecture builds embed that completed schema, client source, and mutable policy into the test assembly. Generated-contract checks read those embedded inputs, including during --no-build runs, rather than live source-tree artifacts. Missing captures fail collection instead of falling back to primary file; normal generation restores missing outputs. When selected, the existing OpenAPI CI workflow regenerates and compares authoritative schema/client artifacts. Its pre-existing path filter does not cover changes limited to nswag.pertag.json or EventApiTagClients.g.cs, so contributors must also run the generation command and inspect the generated diff locally. Concurrent external builds sharing the same project bin/obj are not supported; ordinary consumers within one solution build share their producer identity. Test serialization is not the publication mechanism.
See RECORD_CONTRACTS.md for exact eligibility, privacy-safe diagnostics, generation steps, and focused tests.
- API:
https://localhost:7039 - Swagger UI:
https://localhost:7039/swagger - Scalar API reference: mapped by
MapScalarApiReference()in Development and Testing. - OpenAPI document:
https://localhost:7039/openapi/islamu-event.json
- API:
http://localhost:7039 - Compose runs the API with
ASPNETCORE_ENVIRONMENT=Production, so Swagger UI, Scalar, and/openapi/islamu-event.jsonare not exposed there unless the environment is intentionally changed.
/alive, /health, and /metrics are runtime operational endpoints, not generated OpenAPI controller operations. Storage local-first readiness is reported through the storage health check, and the dry-run-first reconciliation worker posture is reported through storage-reconciliation. These health payloads use bounded status/failure fields and must not expose filesystem paths, object keys, bucket names, endpoints, access keys, or secrets.
DELETE /api/user requires normal login authorization plus a UUIDv7
Idempotency-Key header. On first acceptance it returns 202 Accepted,
Location: /api/privacy-erasure/status, Retry-After: 5, and a short-lived
receipt in the body. The receipt is revealed exactly once; repeat submissions
for the same intent return the accepted status without minting a second
receipt.
GET /api/privacy-erasure/status is the receipt-authenticated status route.
Send Authorization: ErasureReceipt <receipt>; OpenAPI documents the
PrivacyErasureReceipt apiKey scheme on the Authorization header, not
bearer auth. Responses are private, no-store and only expose bounded
fenced, provider_pending, or completed status plus aggregate provider-work
counts and settlement timestamps. Missing, invalid, wrong, and expired receipts
all return 401 without revealing whether the subject or receipt exists.
The status DTO is intentionally bounded: it does not expose provider locators, payloads, or raw failure text. Provider settlement and replay are worker-owned concerns; this route only reports the current fence state.
The optional MCP adapter is not an OpenAPI controller group. It is mapped at the startup Mcp:EndpointPath only when Mcp:Enabled=true, then gated at runtime by hierarchical mcp.enabled settings after tenant/auth resolution. The endpoint is mapped anonymously so MCP SDK authorization filters can expose anonymous-safe registry discovery and public event list/detail/program/session read tools, while protected reads such as list_my_events, get_event_creation_context, get_event_publish_readiness, the event_management_context resource template, program/custom-property/registration/team/template/sync context tools, and other scoped tools/resources/prompts still require a valid bearer or API-key principal. API keys need mcp:read plus event read-equivalent scope authority for those protected event-management reads. Runtime MCP governance never changes endpoint path or stateless transport mode.
The middleware pipeline in Program.cs is ordered precisely. Changing order will break behavior:
- Forwarded Headers — the effective-host ingress boundary applies trusted
X-Forwarded-*values before host-derived tenant resolution. - Correlation ID —
UseCorrelationId(). Creates a server-owned UUIDv7 identifier, pushes it to SerilogLogContext, and returnsX-Correlation-ID; caller correlation/request headers are not echoed. - Request Logging —
UseRequestLogging(). Records bounded method, native route identity, final status, duration, presence flags and server correlation. - API Exception Handling —
UseApiExceptionHandling(). ProblemDetails-based chainedIExceptionHandler(Validation → Configuration Import → Global), inside the diagnostic scope so caught failures retain correlation and the request event records their final status. - Security Headers —
UseSecurityHeaders(). Adds defensive headers to every response. - Response Compression —
UseResponseCompression(). Brotli + Gzip atCompressionLevel.Fastest. Enabled for HTTPS. Additional MIME types:application/json,application/hal+json. - HTTPS Redirection —
UseHttpsRedirection(). - HATEOAS Prefer Header —
UseHateoas(). RFC 7240Preferheader processing (return=minimalstrips_links). - Routing —
UseRouting(). - Tenant Resolution (pre-auth) —
UseMiddleware<ApiTenantResolutionMiddleware>(). ResolvesX-Tenant-Slugand normalized host hints for/apiand/mcprequests; API-key requests may defer binding until after authentication. - Request Timeouts —
UseRequestTimeouts(). Three configurable tiers (see below). - Auth Conflict Guard —
UseMiddleware<ApiAuthenticationConflictMiddleware>(). Rejects conflicting auth inputs before standard authentication runs. - Authentication —
UseAuthentication(). JWT Bearer via Keycloak. - Tenant Resolution (post-auth) —
UseMiddleware<ApiTenantPostAuthenticationMiddleware>(). Finalizes API-key tenant binding, mismatch handling, and fail-closed auth behavior. - MCP Runtime Gate —
UseMiddleware<McpRuntimeGateMiddleware>(). Applies only to the configured MCP path after tenant/auth context exists. Returns404when startup mapping is enabled but runtimemcp.enabledresolves false. - Request Localization —
UseRequestLocalization(). - Rate Limiter —
UseRateLimiter(). Eight tiered policies (see below). - Authorization —
UseAuthorization(). - Tenant Lifecycle —
TenantLifecycleAccessMiddleware. Ordinary routes must pass current lifecycle admission before replay; private administrator-session reads have their earlier dedicated lifecycle boundary. - Idempotency —
UseMiddleware<IdempotencyMiddleware>(). Claims write identities by (Key, TenantId), binds method/target/body/principal, and replays only eligible unexpired receipts after current authorization and lifecycle admission. - Support Access Audit —
SupportAccessAuditMiddleware. For active BFF/server-forwarded support-access sessions, records bounded request evidence after authorization without changing the response if audit persistence fails. - Output Cache —
UseOutputCache(). Eight cache policies (see below). - ETag —
UseETag(). SHA256-based weak ETags, 304 Not Modified support.
GlobalExceptionHandler and ValidationExceptionHandler emit stable code,
status, bounded method, native route, server trace ID and server correlation,
not exception objects, messages, inner exceptions or request paths. This applies
in Development and Testing as well as production. ProblemDetails retains its
status/type/title/detail/instance and extension JSON shapes; instance is a
native endpoint identity/template or route-unresolved. Validation exception
errors retain the dictionary-of-arrays shape but use one generic validation
entry: arbitrary field names and validator messages are not a safe content
contract. Nonexception typed controller validation contracts are separate.
Concurrency responses retain known conflict codes and omit optional entity
identity strings; quota exceptions retain numeric limit/actual/attempted fields
with bounded quota/request key/scope categories. Retry-After and existing
no-store/no-referrer policy are unchanged.
API-local ApiDiagnosticLogEnricher retains SourceContext and properties bound
by the event's message template, discarding inherited request/caller scopes from
every event. Producers must still omit private explicit arguments; this is not
automatic redaction of message-template values. API-key
authentication retains its existing bounded outcome/reason/source fields;
post-auth tenancy and throttling use fixed codes and native route metadata,
never key IDs, tenant IDs or raw paths. Both the Serilog pipeline and forwarded
logging providers exclude framework ASP.NET Core/IdentityModel, native HTTP
client, gRPC client, EF Core and host lifecycle/error categories, which have no
confidentiality contract for URLs, SQL, startup or authentication exceptions.
OpenTelemetry logging disables scope export. Native API failure and HTTP events
replace those messages; this does not disable metrics.
Native configuration, role, moderation, fanout, admission/idempotency and HAL
producers emit bounded status/decision/counts without private identity, validity
windows, paths, provider messages or exception objects. Authorized results and
persisted audit/operation records retain their business data. JWT warmup keeps
key counts/source and retry timing, not issuer/authority URLs. Link-generation
failures omit route values. Scheduler default diagnostics keep action, target
category and refusal code; the ISchedulerAdminAuditSink record retains operator
and job references for a separately registered access-controlled audit destination.
The default logger is not evidence of durable actor-attributed scheduler auditing.
ApiDiagnosticTraceProcessor is registered before ServiceDefaults' exporters.
It keeps native HTTP status, bounded method and server route template (or the
http-dependency category for native HTTP client spans), dropping URL query/full
URL, unrelated tags and free-form HTTP status descriptions.
For every exported activity source, the processor clears baggage and trace-state
on the activity and its live ancestors: clearing only a child cannot remove
inherited context from exporter enumeration. Native AI/authorization/MCP fields
and trace/span IDs remain. This also stops subsequent spans inheriting the
removed diagnostic context; baggage is never business authority.
HTTP instrumentation exception recording is explicitly disabled and
UseExceptionHandler suppresses raw framework exception diagnostics. IdentityModel
PII and complete security-artifact export are disabled in every environment.
Span trace/span IDs still link diagnostics; outside HTTP it does not sanitize
producer tags/events. Their closed vocabulary remains a producer obligation.
BoundedResponseCaptureStream checks eligibility and remaining space before allocating
or copying response bytes. Its lazy buffer has a fixed capacity: 256 KiB for ETag,
1 MiB for generic idempotency receipts. Declared streaming/non-JSON responses bypass
ETag, and declared ineligible receipt output keeps the original response stream.
Unknown-length output crossing either cap drains the retained prefix exactly once,
disposes it, and forwards subsequent writes directly. Explicit flush/start also ends
capture, preserving partial-response delivery. An uncaptured response receives no
middleware ETag. HEAD and 204/304 suppress body delivery; compression remains outside
capture in the host pipeline, so validators describe the uncompressed representation.
Generic HTTP handling cannot prove business rollback. Oversize or otherwise
ineligible responses, downstream exceptions, serialization failures, cancellation,
connection abort and HTTP error status therefore never release the acquired claim.
An owner may complete status 0 (in progress) into a normal receipt or status -1
(ResponseUnavailableStatusCode, a persistence sentinel, never an HTTP status).
Status -1 stores no body or content type. A matching retry receives 409 with
idempotency_response_unavailable, private, no-store, and a reconciliation
instruction; a different identity receives idempotency_key_reuse. There is no
generic reexecution path for unavailable outcomes. If the bounded five-second
failure-state update itself fails, the original status-zero fence still blocks
reexecution. A completed usable receipt may safely survive a later client disconnect.
Receipt expiry removes disclosure authority, not execution history.
IdempotencyRepository.TryClaimAsync scrubs an expired receipt in place rather than
creating another owner. The cleanup contract ScrubExpiredAsync removes
payloads and converts expired active/completed receipts to unavailable fences;
its count is receipts scrubbed, not physical rows deleted. Existing schema columns
and the unique tenant/key index carry the fence, with no migration. These minimal
identities remain retained while an old request can be submitted. Their eventual
retirement requires native proof that replay cannot execute; the 24-hour receipt
TTL alone is not that proof. ID-only ReleaseAsync has been removed. A durable
HTTP claim cannot be released by rollback of a later, unrelated business
transaction. Transaction-owned AI claims, message/run writes and receipt
completion share one original transaction; acknowledged rollback removes them
together without a separate delete/release capability.
IdempotencyRequestDescriptor binds every lookup to the exact request identity.
Mismatches return no prior record or payload. Completion requires the opaque
original IIdempotencyClaimOwner, not a copied receipt or GUID. The managed
handle retains its issuer, context rental, tenant/schema, original connection
and transaction; closing/reopening the connection invalidates it. PostgreSQL
independently checks private nonce-hash/backend/transaction evidence created
only by the inserting invocation. The native receipt functions expose bounded
operations, never table grants to Runtime/Global. ATProto bootstrap consumption
uses a dedicated Boolean one-time operation rather than generic response replay.
Feature-owned anonymous-order recovery may read the exact committed order without acquiring/releasing its historical claim. Existing feature-owned revalidation of usable receipts remains intact. One-time/API-key issuance still suppresses generic storage entirely. Protected receipts keep private no-store, bind Data Protection purpose to the exact durable record ID, and reject tampered/cross-record ciphertext. Replay expiry is checked again before disclosure; anonymous-order disclosure also checks its original recovery deadline before forwarding buffered or streaming bytes.
The source regression owners are ResponseCaptureBoundaryTests,
IdempotencyRepositorySqliteTests, IdempotencyRepositoryTests, and the existing
anonymous registration deadline tests. They cover native TestServer responses,
real durable commits, subscribed response/commit gates, and positive native
transaction rollback.
Three-reader non-URL versioning — clients may use any of the following; all three are read simultaneously via ApiVersionReader.Combine:
- Media-type strategy:
Accept: application/json;v=0.1orapplication/hal+json;v=0.1. - Query-string strategy:
?api-version=0.1appended to the request URL. - Custom-header strategy:
X-Api-Version: 0.1request header. - Default API version is
0.1when unspecified (AssumeDefaultVersionWhenUnspecified = true). - Version is reported in response headers via
Asp.Versioningmiddleware (ReportApiVersions = true). - URL-segment versioning is intentionally NOT supported — every endpoint has exactly one primary path (
/api/controller). This invariant is enforced by theNoUrlSegmentVersioningcontract test so thatoperationId,RouteNames, and HAL link generation stay stable across versions.
Profile collection now has two additional authenticated POST boundaries:
/api/moderation/profile-collection-subject-observations accepts only explicit
profileId and native mode; it cannot bind a subject ID, observation time,
claimed authority or sensitive input. Manual Application validation precedes the
original native subject owner, which resolves the actual authenticated Human and
returns only the observation ID. This is not an applicability assessment or
collection permission.
/api/moderation/profile-collection-assessments/adoptions/{adoptionId}/withdraw
accepts exact retained original jurisdiction/subject/policy/publication/profile/
mode/source/adoption coordinates and generations. The route replaces the body's
adoption ID before manual validation. Withdrawal is historical correction, not
fresh parent qualification or renewal. Unavailable native results use the same
non-disclosing 404 mapping.
Both reject top-level unknown members, enforce a 64 KiB JSON limit and write limiting, disable response/output caching and suppress idempotency response storage. Their integrated API product compilation passed exit 0 in 8m00.27s; the later four-boundary native export/generated Client receipt below also covers these two operations.
The same assessment controller now has authenticated sources and adoptions
POSTs. Strict nested original intent requires every observation, jurisdiction,
policy/publication/profile/mode and original procedure revision/generation
coordinate. It does not accept a subject ID, observation timestamp or claimed
principal. Recording requires an explicit complete required set (including
positive empty); acceptance names the exact source revision/generation and
assessment-slot CAS. The copied set is bounded to the three known prerequisites.
Manual validation precedes native nominated-owner/reviewer qualification. Both
return only the committed entity ID, not a prerequisite, consent or collection
receipt. Unknown members are rejected at both outer and original-object levels.
Seventy API and seventeen related Application cases are authored UNRUN/unbuilt.
All new transport diagnostics are clean. Combined record/adopt product
compilation passed exit 0 in 7m18.68s. Current native export passed in 6.79s;
private generated Client compilation passed in 44.50s. Emitted schema has all
four named paths, complete required original coordinates and closed outer/
nested objects. Seven exact published/API/Release/Debug byte comparisons and
complete JSON syntax/schema semantic checks passed. The export
host repeated pre-existing LocalLifecycleDeliveryScanFailed; durable operation
state was retained and this unrelated worker was not changed.
Actual-self consent now has authenticated POSTs at
/api/moderation/profile-subject-consents and
/api/moderation/profile-subject-consents/{consentId}/withdraw.
Grant requires strict nested assessment and original nominations: all original
observation/policy/procedure coordinates plus exact assessment source/adoption
revisions and generations. It accepts only a new consent ID, the separate
consent-slot CAS and requested finite end alongside these nominations. Subject,
provider, grant time, required set, DOB, relationship and receipt cannot bind.
Unknown fields fail at all three levels. The native owner authenticates canonical
original self and independently accepted applicability; it denies gratuitous
consent when SubjectConsent is not demanded.
Withdrawal overwrites the body consent ID with the route before real manual validation. Historical consent generation and current consent-slot CAS remain distinct. Live policy/assessment/parent availability is not required for terminal original-self correction. Both operations retain private 404, 64 KiB JSON, write limiting, response/output no-store and suppressed replay-response storage. Scoped DI binds the real private owner; handlers return committed consent IDs only. Eighty-six native, seven Application and forty-two HTTP cases are authored UNRUN/unbuilt. Fresh new-source/test diagnostics are clean; existing registration using warnings remain. Integrated product compilation passed in 7m06.04s, native export in 6.94s and regenerated Client compilation in 46.11s. Both new operations and their closed 4/4/7-field schemas were inspected, along with actual generated methods and exact Release schema capture. Debug capture refreshed; final seven byte comparisons and complete six-operation/schema/JSON checks passed. Earlier passing four-path artifacts do not alone cover these additions. Account/guardian prerequisites and authenticated collection receipt/before-body enforcement remain unfinished.
Profile collection procedure administration has three authenticated POSTs beneath
/api/moderation/profile-collection-procedures: sources, adoptions and
adoptions/{adoptionId}/withdraw. Typed intent names the exact child-age
source/adoption revisions and writer generations, explicit preliminary/normal
field vocabulary and finite proposal validity. These bodies contain policy
criteria, never individual DOB, consent evidence or a claimed requester.
Top-level unknown members are rejected by the CQS records. Manual validation
and native qualification remain separate; the actual accepted parent nominates
the recording owner and independent reviewer. Withdrawal overwrites the body
adoption coordinate with the route coordinate before validation.
The controller follows existing RFC 7807 mapping, 64 KiB JSON limits, write
limiting, response/output no-store and suppressed idempotency response storage.
Native request-bound receipts, not middleware replay, own mutation recovery.
Twenty-five HTTP and fifteen Application contract witnesses are authored
UNRUN/unbuilt. CQS source integration is complete, with automatic scoped handler
discovery. Targeted API compilation/native export and generated Client compilation
passed after the full dependency sweep timed out. The later distinct
GuardianConsentReference vocabulary and five-field preliminary limit are
compiled/exported with current Client compilation and exact capture parity passed; relationship references
never substitute for guardian consent. Two further native/Swagger
schema witnesses are authored UNRUN; their diagnostic freshness request timed out.
Procedure adoption supplies no individual applicability assessment, consent,
guardian proof or permission to accept sensitive inputs. Guided administration
and the before-body individual collection owner remain unfinished.
Material classification intent now also uses an API-owned converter rather than
serializing the Domain record's computed Parent. The wire has only eventId,
eventStamp, nullable sessionId/sessionStamp and nullable resourceId/
resourceStamp. Event coordinates are mandatory; each child identity/stamp pair
must be coherent. Unknown or duplicate members, null material and empty GUIDs
are rejected. Native stamps remain GUIDs, not long revisions or forced UUIDv7.
The registered converter and shared native/Swagger transformer describe the same
six writable fields. Twenty wire cases and two new hosted schema-parity cases
are authored UNRUN/unbuilt; generated contracts and actual HTTP execution remain
programme-exit obligations.
The material inventory read uses a bounded authenticated POST because the
candidate mandate carries review grounds that do not belong in a query URL.
Its closed body contains Event generation, taxonomy, profile, mode and mandate,
never caller-owned completeness or server tenant/human identity. The Application
handler captures those server coordinates; the native owner qualifies them.
One terminal projection acquires the original complete native observation,
maps category meaning/version/floor and payload-free material/component
coordinates privately, then revalidates the original lease before actual body
delivery. Disposal retains no reusable observation. The HAL source-record link
requires the mapped NativeOwnerQualified marker, not roles/claims, and cannot
grant independent acceptance. The native implementation borrows the actual
tenant-authoring transaction and qualified evidence session, retaining the
selected policy, original epoch, nominated humans, owner assignment, finite
prerequisites and all complete material/component versions through final output.
Final revalidation compares those originals and the unfiltered latest Event
writer generation; it cannot acquire a replacement observation to make delivery
pass. Disposal closes the owned read transaction and restores the original
tenant/trust frame. It adds no mutation, reservation, epoch advance or commit;
existing native no-op DML fences remain. App/API source compilation passed.
Native source compilation passed. Actual HTTP/serialization/provider/race
execution remains pending; thirteen Application/twelve API/thirty native cases
remain UNRUN.
The nominated-reviewer source GET is separate from the owner's inventory POST.
GetModerationClassificationMaterialReviewQueryHandler captures server identity;
the source ID cannot claim reviewer/controller status. The native source read
retains the actual authoring transaction and evidence session, authenticates
the proposal's original record receipt, requires the actual nominated reviewer,
and rejects any retained acceptance. It reuses complete original inventory
checks, adding exact source ID/revision/writer, mandate, validity and ordered
material/category selections. Newer proposals and changed prerequisite/content/
assignment/controller facts invalidate output rather than replacing originals.
ModerationClassificationMaterialReviewHttpResult assembles HAL/JSON privately,
then revalidates the original source lease before the actual write. The internal
NativeReviewerQualified marker gates only the acceptance affordance; the POST
still performs independent native qualification. No raw resource contents, DOB
or accepted classification are emitted. Thirty-one additional native/eight
Application/three API cases remain UNRUN/unbuilt; actual body/provider/race
execution and generated-contract interoperability remain programme-exit obligations.
The current moderation OpenAPI surface has now been exported through the existing API build target and consumed by the native per-tag Client generation pipeline. NSwag writes private candidates; repository-owned record/byte normalization and exact capture publication precede Client compilation. The export and Client product build passed. These source/generation receipts do not replace hosted native/Swagger shape parity or actual generated-client HTTP execution; those remain UNRUN. No generated schema/client was hand-edited.
The material Client service now authors owner-source and nominated-review
operations from detached displayed HAL metadata, freezes the exact serialized
body and prospective identities for explicit uncertain replay, and validates
the action's origin, path and POST method before sending. It does not refresh
the displayed writer generations or infer permission from claims. The parent
review found the native HAL catalogue omitted the material, component and
selection leaf types, leaving inventory items as object; those authoritative
catalogue entries have been regenerated rather than retained behind a JSON
conversion adapter. Native export and current service integration compilation passed.
Thirty-six actual HTTP fixture cases and two native typed-HAL schema cases are
authored UNRUN; a documentation/schema build is not HTTP runtime assurance.
Moderation scope and subject intent use API-owned JSON converters rather than
public serialization constructors in the shared Domain. A scope has exactly
kind, id and optional parent; a subject has exactly kind, id and origin.
The converters call AuthorityScope ancestry factories and SubjectReference.Create,
reject unknown/duplicate members, empty or malformed IDs, unknown kinds, invalid
parentage and null required values with JsonException. Derived trust/coordinate
fields and claimed permissions are not accepted. MVC registers both converters;
persisted snapshot/digest serializers are unchanged. This is intent binding,
not native permission, provenance qualification or a new lifecycle endpoint.
The 33 authored wire-contract cases remain UNRUN/unbuilt; generated OpenAPI/client
and hosted HTTP assurance are programme-exit obligations.
Decision-proposal binding also reconstructs immutable rule and proposal intent.
Rule JSON is ownerId, code, revision; proposal JSON is caseId, rule,
capabilities, startsAt, optional endsAt/reviewAt and requiredApprovals.
API-owned converters invoke the existing factory/defensive constructor, preserving
capability snapshots, protected remedies, interval coherence and quorum. Every
member is unique, including nullable interval terms. These values do not qualify
the proposer or adopted policy. POST /api/moderation/decisions/{decisionId}/proposal
uses the route decision ID and expected open-case revision; all later decision
actions use expected decision revision. Twenty-five proposal/rule wire cases and
six real-handler/service boundary cases are authored UNRUN/unbuilt.
ModerationIntentSchemaTransformer now describes the converter-owned
AuthorityScope, SubjectReference, ModerationRuleReference and
ModerationProposal shapes on native OpenAPI and the existing Swagger baseline.
The shared mutation describes known writable wire fields only; computed ancestry
IDs and defensive CLR collection implementations are not request members.
Native reference IDs are explicit for these four types; Swagger keeps its existing
qualified schema IDs. Parent ancestry, subject origin and original rule references
resolve to their own components instead of opaque objects. Nullable optional
interval terms remain optional; capability items exclude protected remedies and
quorum is 1 or 2. Factories/handlers still own complete ancestry, coherent intervals
and actual authority, not the schema. Twelve hosted contract cases are authored
UNRUN/unbuilt; no generated-client or hosted-parity pass is claimed.
The forced native export was compiled and inspected: all four named components
contain their wire fields and the actual proposal-command component references
the restored subject/target/proposal definitions. Native traversal first retains
opaque markers; the final document phase replaces those component entries with
wire definitions. Adding recursive references during CLR-property traversal
caused an exporter stack overflow and was removed. Generation success alone was
insufficient: adding rather than replacing an existing component left an opaque
marker, which direct artifact inspection caught before the final replacement.
- Controllers are thin: receive request → invoke native CQS command/query handler → assemble HATEOAS response → return HTTP result.
- Business logic belongs in handlers/services, never controllers.
- Every endpoint has named routes (via
RouteNamesconstants) for HATEOAS link generation. - Endpoints include
[ProducesResponseType]and XML doc summaries for OpenAPI quality. - Published record collections are defensive snapshots. Controllers replace immutable extension data with
SetItem/record copies rather than mutating indexers, and convertReadOnlyMemory<byte>to an MVC byte array only at the file-result boundary. - No Generic CRUD or Lookup Base Controllers:
Controllers are concrete HTTP transport adapters, not business entities. Never introduce generic base classes (such as
CrudControllerBase<...>orLookupControllerBase<...>) to abstract endpoint actions. Generic action inheritance creates an inflexible "framework inside a framework", obscures route names and compile-time uniqueness, degrades OpenAPI metadata ([EndpointSummary],[EndpointDescription], operation IDs), and restricts customization when backlog features diverge (custom validation, lifecycle state transitions, Cerbos authorization policies, multipart uploads, sub-resources). - Approved Base Classes & Composition:
- Root Base (
EventControllerBase): Exposes request-scoped principal identity (CurrentUserId,RequiredUserId) and strong ETag/concurrency parsing (TryParseConcurrencyStamp). - Domain-Family Base Classes: Permitted only when two or more split controllers share an exact, multi-step domain protocol or security check (e.g.
RegistrationOrderControllerBasefor guest vs. authenticated checkout;InstanceSettingsControllerBasefor setup-secret vs. admin). - Composition Over Inheritance: Shared mechanics belong in
CommandFailurePolicy,IResourceAssembler, CQS commands/queries, and extension methods (ToCommandValidationProblem,ToNotFoundProblem), leaving controller actions explicit, declarative, and independent.
- Root Base (
DeleteEventCommandHandler returns BaseCommandResponse<Guid>: missing event,
denied owner/organization/instance authority, and paid-evidence conflict carry
distinct failure codes. EventLifecycleController.Delete maps those codes
through CommandFailurePolicy to 404, 403, and 409; only a committed deletion
returns 204. The native AuthorizeResource check remains in front of the
handler. Event detail's delete HAL link requires event:delete.
FallbackAuthorizationService maps registration-form delete to
event_registration:manage only for the registration-form resource kind;
an EventManager or RegistrationManager assignment cannot borrow that
permission to delete the event aggregate.
GetEventDetailsRequestHandler partitions its HybridCache projection by the
ambient ITenantContext.TenantId and event ID. The projection reads through
tenant-filtered repositories, so a missing projection from a wrong-tenant
request must not cache a 404 for the tenant that owns the event. Public
eligibility is rechecked against the current tenant after each cache read;
event, aspect, session, ticketing and registration writers invalidate
CacheTags.Event(eventId) to evict every tenant-partitioned projection. Removing
the former unqualified event:detail:{eventId} key leaves stale details behind.
CreateEventGraphSessionDto.StartTime is nullable. A Draft request may omit both
start and end for a prepared session; its metadata and validated graph
references persist without invented dates or inferred day rows. A Fixed
scheduled session requires both instants and EndTime > StartTime. OpenEnded
preserves start-only schedules; existing prayer-relative inputs keep their
scheduling/aspect rules. An end without a start, a partial Fixed schedule, or
publication containing an unscheduled session fails validation before rollups
and persistence. A scheduled child cannot make an invalid sibling disappear.
Native validation errors retain property/index paths, including
Sessions[1].StartTime, through the shared command-failure ProblemDetails
mapping. Location, room, image and temp-key references remain tenant-qualified;
a valid explicit day association on an unscheduled draft is retained, not
silently dropped. Applicable owner assignment and publication outbox remain in
the existing serializable graph transaction. Personal community reports retain
their existing submitted-by provenance without an invented owner grant;
organizer-created events retain their owner lifecycle. Managed reads retain draft programs;
anonymous/public program reads do not expose them.
Regenerate consumers through API GetOpenApiContractInput, the inventory
producer and client GenerateApiClient in that order. Standalone client
compilation alone does not export current backend inputs. No migration is
needed for this wire change. After nullable drafts exist, an older executable
that dereferences starts is not a safe downgrade; use a compatible full-outcome
rollback or forward correction without deleting retained programs. A lost
create response is an unknown outcome, not proven rollback or permission for
automatic browser replay.
EventManagementReadController.GetProgramSummary and GetManagedProgramSummary
inject separate closed IQueryHandler<..., EventProgramSummaryDto?> ports. Both
ports are implemented by GetEventProgramSummaryRequestHandler and discovered
under one scoped owner, with authorization outside performance instrumentation.
These are pure reads: no transaction, notification, outbox, or mutation is added.
The managed request retains Event:ViewManagement; public eligibility and
public session/group/agenda repositories remain distinct from managed reads.
Public location fields come only through PublicEventLocationProjection and
IEventLocationDisclosureService. Managed summaries do not project physical
location details or public location envelopes.
Null still maps to the existing 404 ProblemDetails; authorization may reject a
missing or inaccessible management target before the query runs. Managed HTTP
responses retain private/no-store. EventManagementMcpTools uses the public
native summary port after its public-event gate, then applies the AI disclosure
ceiling and existing bounded descriptor mapping (including the 100-item budget).
Its grouped management context still uses separate managed session/group/agenda
queries and the native managed-day port, not the managed summary query.
Grouping, local dates, timezone fallback and warning paths are unchanged. Tokens
flow through existing token-bearing reads and disclosure calls; inherited
GetEventWithDetails and managed GetSessionsByEvent remain tokenless. Full
in-flight cancellation of those database reads is not claimed. The focused
NativeEventProgramHttpTests exercise the real HTTP and MCP adapters, protected
ports and repositories on PostgreSQL and SQLite, including positive approved
venue fields and retained public/managed authority and suppression boundaries.
EventLocationConfiguration restores DateTimeKind.Utc when materializing only
CreatedAt and nullable RevealFullDetailsFromUtc. These columns already store
UTC instants: the domain factory normalizes creation time, and the policy audit
rejects non-UTC explicit reveal inputs. The EF converters leave writes and ticks
unchanged and preserve null reveal times; they do not reinterpret domain input.
Without this read-side restoration, SQLite materializes DateTime as
Unspecified, causing the unchanged strict disclosure evaluator to suppress even
approved venue names. The evaluator still rejects invalid UTC facts, and pending
privacy review still suppresses fields after the fix. Real SQLite round-trip and
HTTP tests cover both null and explicit reveal dates. No global/base-entity time
convention, policy relaxation, store-type change or migration is introduced.
The task-local agenda ordering prerequisite is repaired in
EventAgendaItemRepository.GetByEventAsync and GetPublicByEventAsync.
Both methods materialize their existing event-filtered, no-tracking lists with
the supplied cancellation token before sorting by SortOrder, then absolute
DateTimeOffset start instant. Tenant, soft-delete and public-eligibility filters
remain in SQL. These methods already returned the complete event-scoped list;
client ordering fetches no additional rows and adds no provider-name branch,
UTC SQL conversion, pagination or equal-key tie-breaker contract. SQLite therefore
no longer rejects the summary's agenda query with an unsupported ORDER BY.
Repository tests cover conflicting local-clock/instant order, sort priority,
unpublished days, deleted items and event/tenant isolation. The original failure
was a direct summary-path prerequisite, not unrelated suite rot.
No route, schema, generated-client, configuration or migration change is required.
The seven Features/EventAgendaItems operations use closed native command/query
ports, discovered under authorization -> performance -> handler composition.
EventAgendaItemController injects all seven; EventManagementMcpTools injects
only the managed agenda-list port alongside its existing native Day/Program
ports. The separate Features/Agenda projection now uses its own native public
query port and reads repositories directly, as described below. DTO assemblers,
routes, response types and generated contracts are unchanged.
EventAgendaItemAuthorizationContextEnricher is explicitly registered for the
three writes. It loads the agenda item's persisted source parent (or create's
target parent), rejects missing/deleted/foreign-tenant rows, and supplies
EventScopedAuthorizationFacts to the existing AgendaItem create/update/delete
capabilities. This fixes legitimate owner writes denied for missing event
context without adding grants. A reparenting update separately checks the
persisted destination's Event:update authority before attachment or mutation;
submitted destination authority never replaces source authority.
Public list/detail retain authoritative eligibility and policy-filtered location
disclosure; managed list/detail retain Event:ViewManagement, parent binding
and private/no-store HTTP responses. Managed detail alone retains exact location
IDs. MCP management descriptors omit physical location information. Nullable
detail results still map to 404. PATCH preserves required strong If-Match,
409 stale-write responses and explicit field-operation semantics.
Handlers retain manual validators, UTC rescheduling, destination timezone/day reprojection, transaction-owned location attachment/detachment and post-commit update cache invalidation. The existing unit of work rolls back placement and agenda changes together and translates optimistic conflicts. Real SQLite tests exercise seven registered ports, HTTP and MCP transport, denied/forged/foreign access, positive moves, venue review suppression, storage failure rollback and two previously authorized snapshots with one durable winner. The concurrency fixture gates actual provider decisions with signals; it never grants authority. Inherited tokenless repository methods remain tokenless; full in-flight cancellation of those reads is not claimed. No provider matrix or performance claim accompanies this slice.
GetEventAgendaProjectionRequest implements only IQuery<EventAgendaProjectionDto?>;
its handler exposes Task-based QueryAsync. EventAgendaItemController injects
that exact closed port alongside its seven native agenda-item ports, removing
its last mediator dependency. Automatic native registration retains authorization
-> performance -> handler composition. This is a public query, not a management
grant: the handler checks authoritative persisted public eligibility before reading
published days, public sessions and public agenda items. It has no nested sender.
The existing merge omits sessions missing required schedule projections, groups
entries by local date, orders entries by local start minute then sort order, and
orders day groups by day sort order then date. Published empty days remain;
entry dates without a published day receive an unlabelled group. Timezone remains
EventTimeZoneId ?? Timezone. Physical location and room IDs are redacted;
public location envelopes come only from the existing batched disclosure service,
including approved venue fields and renewed privacy-review suppression. The
portable agenda ordering and UTC venue materialization repairs remain unchanged.
Null retains the controller's existing 404 ProblemDetails mapping. Real SQLite
HTTP tests observe that error body with application/json; this migration does
not change the shared response policy. NativeAgendaProjectionHttpTests proves
the registered port, complete HTTP/native JSON parity, meaningful merged dates
and order, approved venue disclosure, private/draft/foreign/deleted/missing denial
even for an owner, fresh actor-suspension denial and privacy-review tightening.
In-memory AgendaProjectionTests covers input permutation, incomplete sessions,
published/implicit/empty day behavior and timezone fallback without extracting
the handler's existing algorithm. Tokens still flow through token-bearing reads
and disclosure; inherited parent GetById remains tokenless. The SQLite command
interceptor observes real read tokens and cancels at a read boundary without
sleeping or replacing repositories. No full in-flight cancellation, provider
matrix, performance, route, schema, client or configuration change is claimed.
GetEventSessionStatusDetailsQuery and its native handler/closed controller port
return EventSessionStatusDto?: absence is a query result, not a fabricated DTO
or a mapping exception. EventSessionStatusController.GetById maps null through
ApiNotFoundProblemDescriptor and ToNotFoundProblem, producing the existing
404 ProblemDetails convention (resource_not_found, request instance and tracing
extensions). Present rows still return the unchanged 200 DTO.
This repairs the former Ok(null) -> 204 mismatch without changing the declared
OpenAPI paths, response schemas, operation IDs, or generated client. Both reads
remain anonymous global lookups; list contents (IDs 1-10) and LookupData/DetailData
cache policies are unchanged. The supplied cancellation token continues through
the native decorators to the handler; IEventSessionStatusRepository.GetById(int)
and GetAll() have no token parameter, so database-read interruption is not
claimed or introduced by this repair.
CreateEventSessionLanguageCommand retains session authorization and manual input validation. The repository insert remains arbitrated by the existing unique (TenantId, EventSessionId, LanguageId) index, including concurrent requests; there is no check-then-insert substitute for that constraint. EventSessionLanguageRepository translates only that exact model-derived index violation through the existing provider-aware constraint classifier into EventSessionLanguageAlreadyAssignedException, detaching the rejected assignment. The command maps that exception to its established validation result, and the controller returns HTTP 400 ValidationProblemDetails (validation_failed, errors.program). Unrelated primary-key, foreign-key and other database failures are not classified as duplicate assignments.
This replaces provider-dependent duplicate-create 500 responses without changing routes, successful 201 payloads, OpenAPI/client shapes or the unique index. No migration or configuration change is required. The inherited repository create signature remains tokenless; this repair does not claim database-write cancellation support. NativeEventSessionLanguageHttpTests covers sequential duplicates, two real inserts synchronized before persistence, durable uniqueness, and unrelated constraint failures on authoritative SQLite; other provider execution remains part of the workstream matrix.
The 25 actions under api/tenants/{tenantId:guid}/events/{eventId:guid}/registration-providers
are partitioned into RegistrationProviderConnectionsController (connections and approved
origins), RegistrationProviderBindingsController (external schema import, bindings,
publication, and mappings), RegistrationProviderChannelsController (channels and launch
descriptors), and RegistrationProviderOperationsController (health, queue, and reconciliation).
External schema import retains its connections/{connectionId:guid}/external-imports route.
Each concrete controller declares the same route prefix, API version 0.1, authenticated
classification, authorization, JSON/HAL media types, and explicit
Tags("RegistrationProviderManagement"). Named routes, action contracts, rate limits,
timeouts, private/no-store behavior, and HAL assembly remain unchanged.
Each controller uses EventControllerBase and keeps its small validation descriptor and
result mapping local, using the existing ToCommandValidationProblem extension.
Controllers still dispatch through IMediator and inject only their own HAL assemblers;
this partition does not change Application request or handler execution.
User profile media at PATCH /api/user/{id} is a replacement group:
omitting profileImage preserves the image, {"profileImage":{}} clears it,
and a supplied group selects either profilePictureId (a managed storage UUID)
or externalProfilePictureUri (an absolute HTTP(S) source). Both together,
empty UUIDs, relative URLs and credential-bearing URLs are invalid. Existing
route-user authorization and If-Match concurrency checks still apply.
The native Actor update command retains OptionalUpdate<T> presence semantics
and accepts exactly one managed-ID or external-URI operation in its image group.
Actor and current-user responses distinguish profilePictureStorageObjectId
from externalProfilePictureUri; only one may be populated. profilePictureUri
(Actor) and profileImageUri (User) are display projections, not stored ownership.
The obsolete User profileImageKey is removed. Managed projections use the
stable public-image route only after current publication/tenant checks; external
sources remain external even when their paths resemble Event routes.
Tag, Tenant metadata, tenant navigation links, footer link groups, footer links, control-plane tenant-plan drafts, current-user appearance localization, user appearance profiles, UI themes, EventLocation disclosure, EventSession agenda items, EventSession groups, EventSession speaker assignments, EventTemplate, EventSessionTemplate, and shared/Event/EventSession custom-property definitions use route-ID or current-resource PATCH. Their bodies contain only nullable logical groups; omitted groups preserve persisted values, and identity comes from the route plus trusted tenant context rather than body-owned IDs. Template PATCH uses metadata and definitions groups: supplied definitions atomically replace definitions and nested options, while omission preserves the existing set. Template detail reads expose the required concurrency stamp, and sync diff/apply/history remain dedicated operations. Custom-property definition PATCH uses metadata, validation, and options groups; the shared definition additionally exposes its entity-type relation group. Supplying options atomically replaces the option set, while omitting options leaves it untouched. Template and custom-property definition updates require the observed concurrency stamp through strong If-Match; Event and EventSession projection refresh remains inside the write transaction. Session-group and speaker updates also require strong If-Match; group list/detail reads expose that stamp. Islamic and Tech aspects use separate POST create operations and grouped PATCH update operations. Appearance active-profile selection, current theme mode, profile archive, Tenant lifecycle, navigation reorder, footer reorder, and tenant-plan publish/archive/clone remain dedicated actions rather than generic property groups. UI-theme PATCH keeps the observed row version at the wrapper level and validates the merged metadata/state/palette candidate before one transactional update.
OptionalUpdate<T> preserves three distinct JSON states: an omitted member leaves the persisted value unchanged, an explicitly supplied null clears a nullable value, and a supplied non-null value replaces it. Record request bodies keep constructor/member names and types aligned for ASP.NET Core and System.Text.Json; validation metadata remains on the constructor parameter or bound member used by the public contract. Do not add compatibility readers or aliases for removed authority fields.
Current caller/tenant authority never comes from a request body. Controllers derive it from EventControllerBase, ITenantContext, an authoritative route/persisted resource, or a purpose-bound trusted adapter, then place it on the Application request only when authorization or business intent needs it. A body UserId/TenantId is valid only as an explicit target that is independently authorized and tenant-checked.
Tenant navigation and footer-link URLs accept relative paths or HTTPS URLs by default. The instance-only security.require_https_external_urls setting defaults to true; setting it to false permits HTTP only for deployments that explicitly trust an HTTP-only private network.
The guest routes under api/events/{eventId:guid}/registration-orders have five
concrete owners: GuestRegistrationOrderController for start, read and lifecycle;
GuestRegistrationOrderRequirementsController for native/provider requirements;
GuestRegistrationOrderParticipantsController for participants and ticket assignments;
GuestRegistrationOrderPromotionsController for promotions; and
GuestRegistrationOrderClaimController for authenticated account claim.
All use the existing RegistrationOrderControllerBase protocol helpers and retain
the GuestRegistrationOrder tag. Only the lifecycle owner needs TimeProvider.
Capability headers, challenge admission, idempotency/replay protections, per-action
authorization, rate limits and HAL mapping retain their original contracts.
The participant helper still constructs one concrete guest mutation command;
Application operation migration remains owned by its corresponding cohort.
Event reads expose typed provenance plus reviewed Active public actions. External destinations are stored as EventPublicAction records rather than caller-supplied redirect URLs.
- Anonymous
GET /api/events/{eventId}/public-actionsandGET /api/events/{eventId}/public-actions/{actionId}return only active reviewed actions for a published public event. - Anonymous
GET /api/events/{eventId}/public-actions/{actionId}/redirectresolves the stored action byeventIdandactionId, returns302, and isno-store. It never accepts a destination or return URL from the request. - Public-action DTOs instruct clients to open external destinations in a new tab with
rel="noopener noreferrer"; these values are fixed by the server. - Authenticated
POST /api/events/{eventId}/public-actions,PUT /api/events/{eventId}/public-actions/{actionId}, andDELETE /api/events/{eventId}/public-actions/{actionId}manage actions through event authorization. Updating a destination returns it to pending review; deletion requires the current concurrency stamp inIf-Match. - Organizer-claim list/detail, submit, withdraw, and review operations live under
/api/events/{eventId}/organizer-claims; claimant-scoped reads useGET /api/actors/{claimantActorId}/organizer-claims. All claim reads are authenticated andno-store; withdraw requiresIf-Match. - Event-bound claim requests and HAL checks authorize as
islamuevent_event_organizer_claimwhile carrying server-only parent-event and claim metadata. Withdrawal useswithdraw-organizer-claim; before provider evaluation, the server loads the persisted claim and claimant actor and supplies claimant user, organization, or group ownership as non-serialized authorization attributes. Request route/body ownership is never trusted. Public action, claim, correction, and unsafe-link affordances require a published public event; withdrawal and review candidates require a pending or evidence-required claim. Clients must use_links, not provenance, actor ids, status inference, or role claims, to decide which controls to render.
Venue disclosure is decided per event, not per venue. The same physical Location can be fully public for one event and withheld for another, so every read is a separate purpose-specific operation and the response shape differs by purpose. All of them live under /api/events/{eventId:guid}/locations and address venues by EventLocationId; the physical LocationId appears only on the management contract.
- Anonymous
GET /api/events/{eventId}/locationsreturnsEventLocationPublicDtofor a published public event. It never varies by authentication cookie and carries no exact street, postcode, or coordinates. - Authenticated
GET /api/events/{eventId}/locations/my-accessreturnsEventLocationAttendeeDtofor the caller's own registration coverage. Exact fields appear only when policy, instance/tenant governance, entitlement, and the server-side reveal time all allow it. - Authenticated
GET /api/events/{eventId}/locations/{eventLocationId}/managementreturnsHalResource<EventLocationManagementDto>afterevent:view-managementauthorization, and appends a PII-free exact-read audit record before returning. - Authenticated
GET /api/events/{eventId}/locations/managementreturns every EventLocation on the event as a HAL collection;GET /api/events/{eventId}/locations/reviewis the same read filtered toNeedsPrivacyReview. PATCH /api/events/{eventId}/locations/{eventLocationId}/disclosureupdates the seven visibility flags, the full-details audience, and the reveal instant. It requires bothExpectedPolicyVersionandExpectedConcurrencyStampin the body.POST /api/events/{eventId}/locations/{eventLocationId}/remediation/confirmclears a privacy review, and only for a usable active physical venue or an explicit to-be-announced association.- Attendee, management, review, and both write routes are
Cache-Control: private, no-store. The anonymous public read is the only cacheable one. stateis a stable snake_case string (hidden,to_be_announced,available,private_venue,unavailable,needs_privacy_review) on every purpose contract.- Clients must gate Edit and remediation controls on
_links["edit"]and_links["remediate-location"], never on local roles or claims. Each row in a collection carries its own links.
A private home is somebody's household, so ownership is claimed by the incoming owner with explicit versioned consent — never assigned to a third party.
POST /api/location/{id}/private-homeclassifies a location as a private home and records the authenticated actor as its consenting owner.POST /api/location/{id}/private-home/ownershiptransfers ownership to the authenticated actor. The domain requires the consenting user and the new owner to be the same person.- Both require the current concurrency stamp in
If-Matchand a body containingconsentAcknowledged: trueplus aconsentVersion. A missing or false acknowledgement is a refusal, not a default, and is rejected before the location is loaded. CreateLocationDto,UpdateLocationDto, and nested event creation'sCreateEventLocationDtoaccept manual address fields but no rawlatitudeorlongitude. Manual address transitions clear any previously derived coordinates; clients must remove coordinate controls and assignments rather than send aliases. Authorized reads are unchanged:LocationDtoand purpose-specific attendee/management/disclosure field contracts retain their policy-controlled coordinate values.- The generic geographic browse routes
GET /api/location/by-city/{city}andGET /api/location/by-country/{country}are removed. They enumerated exact venue addresses, including private homes, without any disclosure evaluation.
POST /api/geocoding/address-suggestionsaccepts onlysearchText,limit, and an optional organization target. Tenant, actor, provider, credentials, coordinates, source, and visibility are server-owned.- The operation is authenticated, bounded by the dedicated
AddressSuggestionsrate-limit policy, and always returnsCache-Control: private, no-store, including validation, authorization, and throttling errors. Request bodies and search text are not logged. - Provider
Nonestill returns eligible local rows. The Application handler derives tenant and user context, and Persistence enforces active tenant/user membership plus creator, organization, or tenant-approved visibility in one query. - Results are a HAL collection containing source, visibility, concurrency, and links. Clients invoke tenant approval only when
_links["approve-tenant-address"]is present; the namedPOST /api/location/{id}/address-approvaloperation requires the current concurrency stamp inIf-Match. - Browser calls use the existing
/api/*BFF proxy. Unsafe requests require antiforgery; browser-supplied bearer, API-key, and tenant headers are removed before forwarding.
- Event sync routes live under
/api/events/{eventId:guid}/template-sync/{diff|apply|history}. - Event-session sync routes live under
/api/event-sessions/{sessionId:guid}/template-sync/{diff|apply|history}. diffreturns the operator-visible template delta for a requested target version.applyaccepts an explicit sync plan plusBaseProvenanceVersionand uses theComplextimeout policy.- Stale-base and concurrent-update conflicts return
409 Conflictwith ProblemDetails types/problems/stale_sync_baseand/problems/concurrent_update.
Support-access routes live under /api/support-access and require authentication. They model support access as a persisted session for the real actor rather than as impersonation claims.
GET /api/support-access/currentreturns the authenticated actor's current active support-access session when the API can validate one for the current request or recover one for BFF status refresh.POST /api/support-access/sessionsstarts a short-lived support-access session for a target tenant. The handler enforces governance settings, mode caps, ticket/reference requirements, one-active-session constraints, and authorization through the support-access resource kind.POST /api/support-access/sessions/{sessionId}/stopstops the authenticated actor's active session.POST /api/support-access/sessions/{sessionId}/force-stopforce-stops a session for emergency revocation through the higher-privilege support-access action.GET /api/support-access/tenants/{targetTenantId}/sessionsreturns bounded session history.GET /api/support-access/tenants/{targetTenantId}/sessions/{sessionId}/audit-eventsreturns bounded support-access audit evidence.
All session and audit responses are HAL resources or HAL collections. Link policies decide start/stop/force-stop/audit affordances; clients must not recreate those decisions from roles, claims, or cached support state.
Storage object metadata and general download routes are authenticated, resource-protected contracts because metadata can include provider object keys, storage provider labels, lifecycle state, and tenant-owned file details.
GET /api/storageobjectandGET /api/storageobject/{id}require authentication plusislamuevent_storage_object:view.GET /api/storageobject/{id}/contentrequires authentication plusislamuevent_storage_object:download; safe raster content is served inline, while SVG, HTML, documents, and other general content use a sanitized attachment disposition. The content reader enforces lifecycle and visibility before opening the server-owned provider key.GET /api/storageobject/{id}/presigned-urlrequires authentication plusislamuevent_storage_object:presigned_download, returns no provider object key, forces an attachment disposition at the provider, and is marked no-store. Browser image presentation does not use presigned URLs.GET /api/storageobject/{id}/publicis the only anonymous storage content route. It serves only active safe public raster images by storage object ID and never accepts provider object keys, filesystem paths, or arbitrary URLs from the browser.- Public writes use provider-neutral upload sessions:
POST /api/storageobject/upload-sessions,PUT /api/storageobject/upload-sessions/{uploadSessionId}/content, andDELETE /api/storageobject/upload-sessions/{uploadSessionId}. Browser clients reach this flow only through the authenticated same-origin BFF session/proxy contract. - The legacy direct-upload
POST /api/storageobject/generate-upload-urloperation and caller-authoredPOST /api/storageobjectmetadata creation are removed. There is no compatibility route, DTO, generated-client method, or fallback path before v1.0. - Clients must discover
content,presigned-download, andpublic-imageaffordances from HAL_links; local role/claim checks are not authoritative.
SafeRasterContentPolicy in Application is the single metadata and container authority for storage finalization, image references, AI image ingress, public delivery, and ATProto thumbnail validation. Browser and AI inputs accept exact JPEG, PNG, GIF, and WebP; server and ATProto paths additionally accept AVIF. All five server formats require matching MIME/extension metadata and a structurally complete container through exact EOF, so truncated content and bytes appended after the active container are rejected. This policy does not claim pixel decoding, sanitization, full codec validity, content moderation, or malware scanning.
EmailDispatch admin routes live under /api/admin/email-dispatch and are authenticated operator APIs for Basic Dispatch Mode. They expose tenant-scoped delivery state and controls without exposing recipient email, subject, body, provider message ids, or raw provider errors.
GET /api/admin/email-dispatch/statusrequires a tenant id query value and authorizesislamuevent_email_dispatch:view. Itslimitdefaults to 50 and accepts 1 through 200.EmailDispatchAdminController.GetStatuspasses cancellation to the native query and awaitsIResourceAssembler.ToCollectionResourcebefore constructingOk, including asynchronous link authorization. The success body is the declaredHalCollectionResource<EmailDispatchStatusDto>: root_linksand_embedded.items, with sanitized rows and permission-filtered item links. It never serializes a Task or aresultenvelope. This repairs runtime conformance to the existing OpenAPI response; route names, schemas and generated clients are unchanged.PUT /api/admin/email-dispatch/tenants/{tenantId}/pauseandDELETE /api/admin/email-dispatch/tenants/{tenantId}/pauseauthorizeislamuevent_email_dispatch:manage_tenant.PUT /api/admin/email-dispatch/tenants/{tenantId}/outbox/{outboxId}/parkauthorizesislamuevent_email_dispatch:park.POST /api/admin/email-dispatch/tenants/{tenantId}/outbox/{outboxId}/replayauthorizesislamuevent_email_dispatch:replay.POST /api/admin/email-dispatch/tenants/{tenantId}/outbox/{outboxId}/resolve-without-replay?reason=...authorizesislamuevent_email_dispatch:resolveand transitions onlyDeadLettered,Parked, orUnknownrows to terminalSkippedstate.POST /api/admin/email-dispatch/tenants/{tenantId}/outbox/{outboxId}/reconcile?outcome=Delivered|NotDelivered&reason=...&providerMessageId=...authorizesislamuevent_email_dispatch:reconcileand atomically aligns anUnknownoutbox, latest attempt, receipt, and notification delivery.Deliveredsettles the graph as delivered;NotDeliveredqueues it safely. Generic replay does not acceptUnknown.GET /api/admin/email-dispatch/controlreturns sanitized global processor state.PUT|DELETE /api/admin/email-dispatch/control/pausepauses/resumes admission, andPUT|DELETE /api/admin/email-dispatch/control/rate-limitsets/clears the bounded global SMTP-per-minute override. These routes authorize as the instance settingemail-dispatch.processor, so tenant administrators cannot use them.- HAL item links for
replay,park,resolve-without-replay, andreconcile, plus globalpause/resumeand rate links, use the same resource/action metadata; clients must render controls only when the server includes the link. Skippedis terminal. Sent, skipped, andContentRedactedAtrows are not replayable or parkable; redacted rows permanently omit all delivery-control affordances.
Notification preference routes are authenticated private preference endpoints. They return a HAL NotificationPreferenceMatrixDto and expose mutation affordances only through _links.
GET /api/notification/preferences/me,PATCH /api/notification/preferences/me, andPUT /api/notification/preferences/me/mutemanage the current user's matrix.GET|PATCH /api/organization/{id}/notification-preferencesandPUT /api/organization/{id}/notification-preferences/mutemanage organization-scoped defaults/overrides through organization resource authorization.GET|PATCH /api/group/{id}/notification-preferencesandPUT /api/group/{id}/notification-preferences/mutemanage group-scoped defaults/overrides through group resource authorization.- Response
_links.self,_links.save, and_links.set-muteare the only UI authority for rendering save and mute controls. Clients must not infer preference editability from roles or claims. - Matrix PATCH bodies contain an optional
cellsgroup. Omitted cells preserve stored choices; an absent or empty group fails validation. Command handlers validate every supplied cell before opening the write transaction, so required or broader-locked cells reject the whole request without partial writes. PATCH /api/actor-subscriptions/actors/{targetActorId}/notification-leveltakes actor identity only from the route. Its body containsexpectedConcurrencyStampand an optionalnotificationLevelgroup; the group is required for a non-empty patch, and a missing subscription returns typed404ProblemDetails.
GET /api/notification/web-push/configis anonymous and returns only{ enabled, publicKey }; the VAPID private key is never part of the API contract.GET /vapid-public-keyis anonymous and returns only the VAPID public key astext/plain; Blazor consumes it through the generated API client and notification service.GET /api/notification/web-push/subscription?deviceIdentifier=...returns the authenticated current user's safe current-device status without endpoint,p256dh, or auth material.POST /api/notification/web-push/subscriptionscreates or refreshes the authenticated user's tenant-scoped device subscription.DELETE /api/notification/web-push/subscriptions/{subscriptionId}deactivates only a subscription owned by the authenticated tenant/user.- Current-user preference resources advertise
subscribe-web-push; active subscription resources advertiseunsubscribe. Blazor must gate both actions from those HAL links.
Localization admin routes live under /api/admin/localization and require
authentication. They expose provider configuration, secret status, static bundle
health, and no-TMS bundle operations without exposing TMS secret values or raw
provider errors.
GET /api/admin/localization/configurationreturns localization governance and metadata such asTmsApiKeyConfigured; it never returns the plaintext API key.POST /api/admin/localization/test-connectiontests the configured provider from the server side.POST /api/admin/localization/tms-api-key/rotatestores the write-only TMS API key through the shared secret-binding flow.PUT /api/admin/localization/governanceupdates non-secret localization governance settings.POST /api/admin/localization/export-from-tms?languageCode={code}pulls the configured Tolgee/Weblate language into the writable static bundle cache.GET /api/admin/localization/bundle?languageCode={code}returns the merged static bundle for offline/no-TMS operators without calling live providers.POST /api/admin/localization/bundlevalidates and writes a flat static bundle JSON payload, then invalidates translation caches for that language.GET /api/admin/localization/bundle-healthreports whether the writable bundle path is usable.
Static bundle imports accept only flat ui.* and lookup.* string dictionaries.
ProblemDetails and logs must not include raw bundle content or TMS credentials.
Listmonk settings are exposed under /api/integrations/listmonk:
| Route | Route name | Auth | Purpose |
|---|---|---|---|
GET /api/integrations/listmonk/settings |
GetListmonkIntegrationSettings |
[AllowAnonymous], public classification |
Returns sanitized ListmonkIntegrationSettingsDto for admin UI state. |
PUT /api/integrations/listmonk/settings |
UpdateListmonkIntegrationSettings |
[Authorize] |
Updates non-secret Listmonk settings. |
POST /api/integrations/listmonk/credentials/rotate |
RotateListmonkIntegrationCredentials |
[Authorize] |
Rotates the write-only API username/key secret bindings. |
POST /api/integrations/listmonk/test-connection |
TestListmonkIntegrationConnection |
[Authorize] |
Runs a server-side connectivity check with resolved settings and credentials. |
Authenticated event-scoped ticket catalog versions, draft authoring, ticket types, and capacity pool management endpoints are exposed under /api/events/{eventId:guid}/ticketing:
| Route | Route Name | Auth | Purpose |
|---|---|---|---|
GET /api/events/{eventId}/ticketing |
GetEventTicketCatalogManagement |
[Authorize] |
Returns the event ticket catalog management DTO with catalog version, ticket types, capacity pools, normalized lookup metadata, and HAL affordances. Instance monetization is not embedded. |
POST /api/events/{eventId}/ticketing/draft |
CreateEventTicketCatalogDraft |
[Authorize] |
Creates a new draft catalog version with currency code. |
POST /api/events/{eventId}/ticketing/draft:clone |
CloneEventTicketCatalogDraft |
[Authorize] |
Clones active published catalog version into a working draft version. |
POST /api/events/{eventId}/ticketing/ticket-types |
CreateEventTicketType |
[Authorize] |
Adds a new ticket type (pricing mode, price, capacity pool assignment) to draft catalog. |
PUT /api/events/{eventId}/ticketing/ticket-types/{ticketTypeId} |
UpdateEventTicketType |
[Authorize] |
Updates an existing ticket type configuration. |
DELETE /api/events/{eventId}/ticketing/ticket-types/{ticketTypeId} |
DeleteEventTicketType |
[Authorize] |
Removes a ticket type from draft catalog. |
POST /api/events/{eventId}/ticketing/capacity-pools |
CreateEventCapacityPool |
[Authorize] |
Creates a shared capacity pool with oversell policy and seat allocations. |
PUT /api/events/{eventId}/ticketing/capacity-pools/{capacityPoolId} |
UpdateEventCapacityPool |
[Authorize] |
Updates capacity pool limits and oversell policy. |
DELETE /api/events/{eventId}/ticketing/capacity-pools/{capacityPoolId} |
DeleteEventCapacityPool |
[Authorize] |
Deletes a capacity pool. |
GET /api/events/{eventId}/ticketing/publication-preflight |
GetPaidEventPublicationPreflight |
[Authorize], private, no-store |
Returns the current paid-publication readiness and blockers. |
PUT /api/events/{eventId}/ticketing/commercial-disclosures |
UpdateEventTicketCatalogCommercialDisclosures |
[Authorize] |
Updates the merchant, refund-policy, and support disclosures required for a paid catalog. |
GET /api/events/{eventId}/ticketing/payment-connection |
GetEventOrganizerPaymentConnection |
[Authorize], private, no-store |
Returns bounded readiness for the exact event organizer's payment connection. |
POST /api/events/{eventId}/ticketing/payment-connection/onboarding |
StartEventOrganizerPaymentOnboarding |
[Authorize], private, no-store |
Starts or reuses hosted organizer onboarding and returns an absolute HTTP(S) URL plus a reuse flag. |
GET /api/events/{eventId}/ticketing/payment-connection/onboarding/return |
ReturnEventOrganizerPaymentOnboarding |
[Authorize], private, no-store |
Redirects to Studio ticketing; it does not mark the connection ready. |
GET /api/events/{eventId}/ticketing/payment-connection/onboarding/refresh |
RefreshEventOrganizerPaymentOnboarding |
[Authorize], private, no-store |
Redirects to Studio ticketing for a new onboarding attempt; it does not mark the connection ready. |
POST /api/events/{eventId}/ticketing/publish |
PublishEventTicketCatalog |
[Authorize] |
Recomputes paid readiness inside the publish transaction before promoting a draft catalog. |
Ticketing money fields use integer minor units in long ...Minor properties. Percentages use integer basis points, where 10_000 = 100%. Catalog, ticket-type, and capacity-pool mutations must follow the exact relation on the returned HAL resource, including create-draft, clone-draft, create-type, create-pool, publish, edit, and delete.
Paid-event policy settings are separate from instance monetization:
| Route | Route Name | Auth | Purpose |
|---|---|---|---|
GET /api/instance/settings/paid-event-policy |
GetInstancePaidEventPolicySettings |
[Authorize], Admin, private, no-store |
Returns the active instance ceiling. |
PUT /api/instance/settings/paid-event-policy |
UpdateInstancePaidEventPolicySettings |
[Authorize], Admin, private, no-store |
Revises the instance ceiling. |
GET /api/tenants/{tenantId}/settings/paid-event-policy |
GetTenantPaidEventPolicySettings |
[Authorize], private, no-store |
Returns the instance ceiling, tenant override, and effective policy. |
PUT /api/tenants/{tenantId}/settings/paid-event-policy |
UpdateTenantPaidEventPolicySettings |
[Authorize], private, no-store |
Revises a tenant-only narrowing of that ceiling. |
The paid-management resources expose actions only through their exact HAL relations: preflight, commercial-disclosures, payment-connection, start-onboarding, and publish. A paid publish relation requires both fresh readiness and manage-paid-event-commerce for the event's persisted organizer actor. Policy responses expose policy values without policy or tenant identifiers. The connection response exposes only status, merchant country, charge-capability state, requirements state, supported currencies, and readiness timestamp. It omits provider, platform, account, tenant, actor, connection, lineage, and evidence identifiers. Checkout, capture, refunds, disputes, transfers, payouts, and legal/tax/invoice support are separate API capabilities; admission and online QR check-in are documented in Admission And Registration.
Organizer promotion management is authenticated, event-scoped, and private, no-store for reads. Every operation authorizes manage-paid-event-commerce against the persisted Event organizer; the feature is available only for a platform-managed Event and an exact scoped ticket catalog.
| Route | Route Name | Protection | Purpose |
|---|---|---|---|
GET /api/events/{eventId}/promotions?ticketCatalogVersionId={id} |
GetEventPromotions |
[Authorize], private, no-store |
Lists safe definition versions and masked active-code labels for one exact catalog. |
GET /api/events/{eventId}/promotions/{promotionDefinitionId} |
GetEventPromotion |
[Authorize], private, no-store |
Returns one safe promotion-management HAL resource. |
POST /api/events/{eventId}/promotions |
CreateEventPromotionDraft |
[Authorize], Write, Idempotency-Key |
Creates a draft and returns the submitted code once as issuedCode; no plaintext or digest is present in the management DTO. |
PUT /api/events/{eventId}/promotions/{promotionDefinitionId} |
ReviseEventPromotion |
[Authorize], Write, Idempotency-Key |
Creates the next draft version from a published definition; it does not mutate the published version. |
POST /api/events/{eventId}/promotions/{promotionDefinitionId}/publish |
PublishEventPromotion |
[Authorize], Write, Idempotency-Key |
Publishes a draft and persists only the versioned digest and masked suffix for its submitted code. |
POST /api/events/{eventId}/promotions/{promotionDefinitionId}/revoke |
RevokeEventPromotion |
[Authorize], Write, Idempotency-Key |
Immediately revokes a published definition at the server-owned decision instant; callers cannot schedule an effective time. |
POST /api/events/{eventId}/promotions/{promotionDefinitionId}/code:rotate |
RotateEventPromotionCode |
[Authorize], Write, Idempotency-Key |
Retires the active code, creates its replacement, and returns the replacement plaintext once as issuedCode. |
The management resource DTO exposes only event/catalog/version/currency, definition group/version/status, eligibility, discount parameters, UTC window, redemption limits, and a masked promotionCodeDisplayLabel. Tenant, actor, and organizer identifiers are server-only HAL authorization metadata marked [JsonIgnore]. Digests, lookup-key versions, active/retired storage flags, and reservation IDs are absent from JSON and generated clients. Plaintext appears in generated request models only where a caller must submit a code; among responses, only successful create and rotate command wrappers expose the one-time issuedCode. Collection HAL advertises create-promotion; draft items may advertise only publish; published items may advertise revise-promotion, revoke, and rotate-promotion-code. Link presence, not a role or status guess, is the client action authority.
Authenticated and capability-scoped guest orders expose matching apply/remove operations only while the order is READY_FOR_CHECKOUT:
| Route | Route Name | Protection |
|---|---|---|
POST /api/events/{eventId}/registration-orders/{orderId}/promotion |
ApplyAuthenticatedRegistrationOrderPromotion |
[Authorize], Write, Idempotency-Key |
DELETE /api/events/{eventId}/registration-orders/{orderId}/promotion |
RemoveAuthenticatedRegistrationOrderPromotion |
[Authorize], Write, Idempotency-Key |
POST /api/events/{eventId}/registration-orders/guest/{orderId}/promotion |
ApplyGuestRegistrationOrderPromotion |
[AllowAnonymous], PublicTransactional, Idempotency-Key, X-Registration-Order-Capability |
DELETE /api/events/{eventId}/registration-orders/guest/{orderId}/promotion |
RemoveGuestRegistrationOrderPromotion |
[AllowAnonymous], PublicTransactional, Idempotency-Key, X-Registration-Order-Capability |
Guest operations use the fixed 10-per-60-second effective-IP public_transactional window with queue limit zero. Browser-originated unsafe requests still cross the BFF antiforgery boundary. Both guest responses protect idempotency replay metadata. Authenticated operations use the per-user Write policy. The capability is header-only and is compared with the opaque hash on the exact tenant/Event/order row after order-expiry validation; it never enters a URL, response body, HAL link, log, or generated DTO.
Apply accepts { "code": "..." }. Lookup normalizes the code, computes candidates for only the key versions already represented by active scoped codes, then rechecks tenant, Event, catalog, currency, window, eligibility, limits, one-active-reservation, and pinned fee-policy facts inside one serializable transaction. Wrong order ownership/capability, an empty or overlong submitted code, unknown or retired code, wrong scope, exhausted limit, inactive definition, an existing promotion, and pricing mismatch all collapse to the same registration-order 404; the API does not reveal which predicate failed. Syntactically malformed JSON remains subject to the API's global safe 400 validation contract before the redemption handler runs. Success returns only the masked applied label plus promotion discount, platform fee, platform contribution, and final due totals. Order reads expose aggregate pre-discount, discount, post-discount, organizer-directed, fee, organizer-earnings, contribution, and final totals in one currency; organizerDirectedTotalMinor equals postDiscountOrganizerDirectedTotalMinor. They do not expose raw code, digests, key versions, promotion IDs, reservation IDs, or authorization booleans.
Order HAL exposes exactly one of apply-promotion or remove-promotion in READY_FOR_CHECKOUT; authenticated links pass through normal registration-order authorization and guest links remain capability-bound at execution. Promotion removal clears the applied snapshot and reprices from pinned facts. Positive totals proceed to the payment contract below; zero totals continue to use the provider-free finalize relation.
Account-owned and capability-scoped guest orders expose idempotent payment start and retry plus private authoritative status. Guest writes are PublicTransactional, require Idempotency-Key, and accept X-Registration-Order-Capability only as a header. Account writes require authentication and current-account ownership. Status and start responses are HAL resources with only safe state, timestamps, bounded failure codes, and redirect/retry availability; they exclude provider account/session/payment/request identifiers, idempotency keys, capabilities, PII, and raw provider errors.
| Route | Operation | Protection |
|---|---|---|
POST /api/events/{eventId}/registration-orders/{orderId}/payment |
StartAuthenticatedRegistrationPayment |
[Authorize], Write, idempotent, private/no-store |
GET /api/events/{eventId}/registration-orders/{orderId}/payment |
GetAuthenticatedRegistrationPayment |
[Authorize], private/no-store |
POST /api/events/{eventId}/registration-orders/{orderId}/payment/retry |
RetryAuthenticatedRegistrationPayment |
[Authorize], Write, idempotent, private/no-store |
GET /api/events/{eventId}/registration-orders/{orderId}/payment/checkout-target |
GetAuthenticatedRegistrationPaymentCheckoutTarget |
[Authorize], current-account access, private/no-store; BFF resolver use |
POST /api/events/{eventId}/registration-orders/guest/{orderId}/payment |
StartGuestRegistrationPayment |
[AllowAnonymous], PublicTransactional, capability, idempotent, private/no-store |
GET /api/events/{eventId}/registration-orders/guest/{orderId}/payment |
GetGuestRegistrationPayment |
[AllowAnonymous], capability, private/no-store |
POST /api/events/{eventId}/registration-orders/guest/{orderId}/payment/retry |
RetryGuestRegistrationPayment |
[AllowAnonymous], PublicTransactional, capability, idempotent, private/no-store |
GET /api/events/{eventId}/registration-orders/guest/{orderId}/payment/checkout-target |
GetGuestRegistrationPaymentCheckoutTarget |
[AllowAnonymous], capability, private/no-store; BFF resolver use |
GET /api/events/{eventId}/registration-orders/{orderId}/payment/studio |
GetStudioRegistrationPayment |
[Authorize], exact manage-paid-event-commerce, private/no-store |
Payment state is one of Created, Processing, RequiresAction, Unknown, Failed, Cancelled, Succeeded, or NeedsReconciliation. Start only claims or reuses the durable attempt/dispatch effect and never calls the provider synchronously. Retry is available for a parked pre-handoff dispatch with no provider session or an authoritative terminal Failed/Cancelled attempt; ambiguous, processing, succeeded, and reconciliation-required states never advertise it. Idempotency identity binds the tenant key to the resolved path/route values and a SHA-256 capability scope; raw capability values are never persisted or logged. Successful replay restores protected Cache-Control: private, no-store metadata. checkout-redirect is an antiforgery-protected, rate-limited, PathBase-aware same-origin BFF POST executed by browser fetch in every Blazor render mode. Anonymous rate partitions use only trusted effective remote IP plus resolved tenant; authenticated partitions use stable user ID, never checkout or antiforgery cookies. Issue prepares the compact cookie, rechecks RequestAborted, commits the bounded server-side target, rechecks again, and only then writes cookies; cancellation triggers compare-and-delete rollback. The constant GET requires Sec-Fetch-Site: same-origin exactly before validating origin, PathBase, tenant/order, expiry, dedicated-session digest, and current allowlist, then peeks and atomically consumes the target. Split requires healthy Redis and Combined standalone uses bounded expiry scavenging. URL paths and traces contain no bearer ticket, capability, PII, provider account, API token, or provider URL.
Checkout creation runs asynchronously from the Quartz payment-reconciliation-drain job. It uses the authoritative public HTTPS origin for success/cancel navigation, the persisted connected account and idempotency key, and the configured Checkout-host allowlist. Signed Connect callbacks only persist a normalized inbox envelope and make reconciliation due. Authoritative connected-account Checkout and PaymentIntent retrieval, exact amount/currency/application-fee matching, and fenced monotonic settlement determine payment truth; browser return navigation never does.
Terminal Failed or Cancelled retry persists release of the old active slot before creating one replacement. Capability-aware replay recognizes only safe Created + Pending or DispatchPending + Failed replacement states and returns the same attempt/effect; Unknown and Succeeded remain forbidden, and expired guest capability remains 404. A normalized PublicBaseUrl subpath is preserved in Checkout success/cancel URLs.
Admission ticket self-service recovery, credential rotation, and account-owned ticket reads live under /api/tickets. See ADMISSION_AND_REGISTRATION.md for domain concepts and zero-knowledge security architecture.
| Route | Route Name | Protection | Purpose |
|---|---|---|---|
POST /api/tickets/recovery |
RequestAdmissionTicketRecovery |
[AllowAnonymous], PublicTransactional, Idempotency-Key, public_transactional plus chained tenant recovery budget |
Initiates self-service ticket recovery. Accepts { "email": "..." } and returns indistinguishable 202 Accepted to prevent email enumeration. |
POST /api/tickets/recovery/consume |
ConsumeAdmissionTicketRecovery |
[AllowAnonymous], Public, private, no-store, AdmissionTicketRecoveryPolicy, X-Admission-Ticket-Recovery-Capability |
Consumes a single-use recovery capability token passed via header; rotates the capability and delivers the rendered QR SVG and printable ticket DTO. |
Key invariants:
- Zero-Knowledge Bearers: Plaintext QR barcodes and tokens are never persisted in the database; only versioned keyed HMAC-SHA-256 lookup digests (
LookupDigest) are stored. - Wire Payload Format: Standardized
islamu-admission:v1:<43-char-base64url-bearer>(63 characters total). - Indistinguishable Email Responses:
POST /api/tickets/recoveryreturns identical202 Acceptedpayloads regardless of whether the email address has active tickets. - Two-layer request throttling: recovery request traffic must pass both the per-IP
public_transactionalpolicy and the global tenant-scoped recovery budget. Capability consumption keeps the dedicated recovery limiter. - No consume-response replay: capability consumption is intentionally not idempotency-cached. The keyed, expiring capability is atomically single-use, and replaying a cached successful bearer response would violate that authority.
- Single-Use Capabilities: Recovery capability tokens expire and rotate automatically upon consumption or re-request.
Phase 21 check-in is online and server-authoritative. Staff routes use ordinary authenticated event
authority; scanner routes use only the opaque X-Admission-Scanner-Capability scheme bound to one
tenant, event, target, action set, and expiry. Every response is private/no-store, mutation
failures are bounded RFC 7807 responses, and UI controls come only from returned HAL relations.
| Route | Route Name | Protection | Purpose |
|---|---|---|---|
GET /api/events/{eventId}/admission/scanner-capabilities |
ListAdmissionScannerCapabilities |
Staff event_check_in:view |
Lists masked capabilities for the event. |
POST /api/events/{eventId}/admission/scanner-capabilities |
IssueAdmissionScannerCapability |
Staff event_check_in:manage, Idempotency-Key |
Issues one target-scoped capability and discloses plaintext once. |
DELETE /api/events/{eventId}/admission/scanner-capabilities/{scannerCapabilityId} |
RevokeAdmissionScannerCapability |
Staff event_check_in:manage |
Revokes one device authority without stopping other target channels. |
POST /api/events/{eventId}/admission/check-ins |
CheckInAdmission |
Staff event_check_in:manage, Idempotency-Key |
Checks in one opaque credential against one body targetId. |
POST /api/events/{eventId}/admission/check-ins/batch |
BatchCheckInAdmissions |
Staff event_check_in:manage, Idempotency-Key |
Processes 1–100 business outcomes independently in input order; a dependency outage aborts the remaining queue with 503. |
GET /api/events/{eventId}/admission/check-ins/{checkInId} |
GetAdmissionCheckIn |
Staff event_check_in:view |
Returns bounded fact detail and current undo affordance. |
POST /api/events/{eventId}/admission/check-ins/{checkInId}/undo |
UndoAdmissionCheckIn |
Staff event_check_in:manage, Idempotency-Key |
Appends a compensation linked to the exact active fact. |
GET /api/events/{eventId}/admission/check-ins/summary?targetId={targetId} |
GetAdmissionCheckInSummary |
Staff event_check_in:view |
Returns exact-target aggregate counts without roster data. |
GET /api/events/{eventId}/admission/check-ins/audit?cursor={cursor}&pageSize={pageSize} |
GetAdmissionCheckInAudit |
Staff event_check_in:view |
Traverses an export-safe immutable keyset page of 1–100 facts. |
GET /api/events/{eventId}/admission/check-ins/health?targetId={targetId} |
GetAdmissionCheckInHealth |
Staff event_check_in:view |
Returns Active, Stopped, or Unavailable plus dependency state. |
POST /api/events/{eventId}/admission/check-ins/operations/stop |
StopAdmissionCheckIn |
Staff event_check_in:manage |
Stops every admission channel for the exact target. |
POST /api/events/{eventId}/admission/check-ins/operations/restore |
RestoreAdmissionCheckIn |
Staff event_check_in:manage |
Restores the exact target after dependency recovery. |
POST /api/events/{eventId}/admission/check-ins/operations/reconcile |
ReconcileAdmissionCheckIn |
Staff event_check_in:manage |
Appends a bounded post-incident decision without rewriting facts. |
POST /api/admission/scanner/check-ins |
ScannerCheckInAdmission |
AdmissionScanner, Idempotency-Key |
Performs one check-in using the authenticated capability target. |
POST /api/admission/scanner/check-ins/batch |
ScannerBatchCheckInAdmissions |
AdmissionScanner, Idempotency-Key |
Processes a bounded target-fixed scanner batch and aborts remaining work on dependency outage. |
POST /api/admission/scanner/check-ins/{checkInId}/undo |
ScannerUndoAdmissionCheckIn |
AdmissionScanner, Idempotency-Key |
Compensates the exact active fact when the capability permits undo. |
Undo request bodies carry reasonCode, restricted to OperatorCorrection, DuplicateScan,
WrongTarget, or ExceptionalReconciliation; free-form reason text is not accepted or persisted.
Invalid credential, capability, tenant, event, and target states remain indistinguishable. A
dependency failure returns bounded 503; scanner clients stop intake and retain no offline queue.
Authenticated registration-form authoring is rooted at /api/events/{eventId:guid}/registration-workflows. The event manage-registration-workflow relation is emitted only after server-side authorization confirms a verified organizer controller or an explicit event registration-manager assignment. Community contributors, listing submitters, tenant-only curators, machine principals, ambiguous organizers, and unrelated tenants receive neither the relation nor authoring data.
The surface provides event-purpose workflow read/create/update; requirement create/update/delete; form create/read; version create-or-clone/read; draft section, field, option, and bounded-rule mutations; publication preflight; and publication. Reads are authenticated and private, no-store. Writes use the WritePolicy, require a strong quoted If-Match containing the observed parent/root concurrency stamp, and return 409 Conflict for stale observations. Preflight rejects empty forms, incomplete choice options, unresolved or forward condition references, and explicit-consent fields without both a purpose code and text version. Publication always invokes the Application publication facade and atomically pins the exact generated data, UI, logic, and mapping artifacts plus their lowercase SHA-256 hash; callers never supply schema JSON or hashes.
The authoritative preflight operation is POST /api/events/{eventId:guid}/registration-forms/{formId:guid}/versions/{versionId:guid}/preflight (GetRegistrationFormPublishPreflight); the former publish:preflight spelling is not part of the contract. The generated OpenAPI document, NSwag client, and API_CONTRACT_INVENTORY.md are regenerated from this route and checked for byte-level determinism.
Draft authoring resources expose only state-valid, permission-checked HAL relations such as edit, add-section, add-field, add-option, add-rule, preflight, and publish. Published versions expose read, preflight, and new-version navigation only; child mutation relations are omitted. DTOs expose normalized lookup IDs/codes/names, lifecycle status, provenance, schema hash, and concurrency stamps, with no provider question IDs, claims, roles, or local capability booleans.
Participation requirements attach through authenticated POST /api/events/{eventId}/participation/requirements/{requirementId} and detach idempotently through DELETE on the same route. Both operations require the current participation-configuration stamp in a strong quoted If-Match header and are authorized as registration-form attach/detach actions. The workflow management projection exposes persisted isAttached state, while each requirement resource emits exactly one matching, permission-checked attach or detach HAL relation as the action authority. A valid walk-in standalone questionnaire is discovered anonymously at GET /api/events/{eventId}/participation/optional-questionnaire; the HAL optional-questionnaire relation is the only client affordance and the endpoint returns non-leaking 404 when the active published descriptor is unavailable.
Platform fees and optional instance contributions are a separate versioned settings resource:
| Route | Route Name | Auth | Purpose |
|---|---|---|---|
GET /api/instance/settings/platform-monetization |
GetInstancePlatformMonetizationSettings |
[Authorize], Admin classification |
Returns the active fee and contribution revisions as a HAL resource. |
PUT /api/instance/settings/platform-monetization |
UpdateInstancePlatformMonetizationSettings |
[Authorize], Admin classification, Write rate limit |
Creates replacement fee and contribution revisions after expected-version checks. |
Both Application handlers recheck instance-administrator authority before repository access. Fresh defaults are fee disabled with zero basis points and no fixed charges, plus contribution disabled with a zero default option. The response emits edit only when the caller may update the platform-monetization instance setting. Tenant administrators, organizers, and curators have no monetization management path.
Configured in RateLimitingExtensions.cs. All settings are configurable via appsettings.json under RateLimiting section.
- Policy:
global— applied to all endpoints by default. - Mechanism: Token bucket per successfully authenticated API key ID when present, otherwise per remote IP address. Empty, malformed, invalid, revoked, or inactive API keys do not create key partitions and remain in the anonymous/IP partition.
- Defaults: 200 tokens, replenish 40 tokens per 10 seconds.
- IP Resolution: uses
HttpContext.Connection.RemoteIpAddress; trusted forwarded-header middleware updates the effective remote/host values earlier in the pipeline. - Exemption: Localhost (
127.0.0.1,::1) is exempt.
- Policy:
authenticated— for authenticated user endpoints. - Mechanism: Sliding window per API key ID when present, otherwise per
User.Identity.Name. - Defaults: 200 requests per 60-second window, 4 segments.
- Policy:
write— for mutation endpoints (POST,PUT,DELETE). - Mechanism: Fixed window per API key ID when present, otherwise per
User.Identity.Name. - Defaults: 30 requests per 60-second window.
- Policy:
PublicIngestion— for anonymous signed machine callback endpoints. - Mechanism: Fixed window per IP address.
- Defaults: 60 requests per 60-second window.
- Policy:
setup_secret— for instance bootstrap endpoints. - Mechanism: Fixed window per IP address. GET requests include the normalized route in the partition so status, journey, and provider-configuration reads cannot exhaust one another; setup mutations retain one shared IP partition.
- Defaults: 5 requests per partition per 60-second window.
- Keycloak operator routes: the seven
/api/instance/keycloakroutes use the setup or authenticated authority partition selected by the current request; setup authority does not bypass the receipt's setup-generation binding.
- Policy:
AnalyticsRelay— for anonymous browser analytics relay traffic. - Mechanism: Fixed window per IP address.
- Defaults: 120 requests per 60-second window.
- Policy:
AiAssistant— for AI assistant send/model/action endpoints. - Mechanism: Fixed window per API key ID when present, otherwise per authenticated user ID.
- Defaults: 12 requests per 60-second window.
- Policy:
EventOpenGraphImage— for public event Open Graph image rendering. - Mechanism: Concurrency limiter with one fixed
EventOpenGraphImagepartition shared by all requests in the API process. - Defaults: 2 concurrent renders, queue limit 0.
- Returns
429 Too Many Requestswith RFC 6585ProblemDetails. - Includes
Retry-Afterwhen available plusX-RateLimit-LimitandX-RateLimit-Remaining.
In Testing environment, all rate limiters, including EventOpenGraphImage, are replaced with NoLimiter (disabled). Integration factories can opt back into rate limiting to verify 429, Retry-After, and partition behavior.
Configured in RequestTimeoutExtensions.cs. All settings configurable via RequestTimeouts section.
| Policy | Default | Use Case |
|---|---|---|
Default |
30 seconds | Standard operations |
Lookup |
10 seconds | Fast lookup queries |
Complex |
60 seconds | Complex queries, exports |
Timeout expiry returns 504 Gateway Timeout.
Applied via [OutputCache(PolicyName = "...")] on controller endpoints.
| Policy | Duration | Vary By | Use Case |
|---|---|---|---|
LookupData |
1 hour | X-Tenant-Slug, Host |
Lookup tables (event types, languages, etc.) |
PublicData |
1 hour | X-Tenant-Slug, Host |
Anonymous lookup endpoints |
ListData |
30 seconds | X-Tenant-Slug, Host, Authorization, query: pageNumber, pageSize |
Collection listings |
DetailData |
60 seconds | X-Tenant-Slug, Host, Authorization, route: id |
Single-entity detail views |
TenantNav |
5 minutes | X-Tenant-Slug, Host |
Tenant navigation/config endpoints |
PublicExperienceShell |
30 seconds | X-Tenant-Slug, Host |
Public shell and experience settings |
SystemConfig |
10 seconds | Host |
System configuration checks |
SitemapData |
30 minutes | X-Tenant-Slug, Host |
Sitemap output |
The default ASP.NET Core output-cache store is process-local. HybridCache or
IDistributedCache does not distribute output-cache entries or tag eviction:
immediate tag eviction is guaranteed only on the replica handling the request,
while other replicas may serve stale output until the policy TTL expires.
Cross-replica output-cache invalidation is deferred without a dedicated
distributed output-cache dependency.
Injected into native CQS handlers, not controllers. Provides in-memory L1 + distributed L2 caching with stampede protection.
| Setting | Value |
|---|---|
| Default expiration | 30 minutes |
| Local cache expiration | 5 minutes |
| Max payload size | 10 MB |
| Max key length | 512 characters |
Read-through pattern (query handlers): _cache.GetOrCreateAsync(key, factory, options).
Invalidation (command handlers): _cache.RemoveAsync(key).
- Computes SHA256-based weak ETags on
application/jsonandapplication/hal+jsonresponses. - Returns
304 Not Modifiedwhen client sendsIf-None-Matchheader matching current ETag. - Applied globally after output cache in the pipeline.
- Uses bounded response capture with a 256 KiB retention budget checked before allocation. Crossing the budget drains the retained prefix once and permanently forwards subsequent writes without computing an ETag. Flushes and ineligible output also switch to forwarding; event streams and declared non-JSON output bypass capture. Private-no-store endpoints remain excluded.
- Only completely captured successful JSON receives a validator. Matching requests return bodyless 304 responses; HEAD suppresses response bytes while retaining the representation length when available. The original response stream is restored even when downstream execution throws.
File content actions declare trusted SandboxedFileResponseAttribute metadata.
The central middleware composes their CSP at response start as
default-src 'none'; frame-ancestors 'none'; sandbox. This preserves
the sandbox directive instead of overwriting a controller's earlier file policy.
Storage content, public storage images and event-resource content use this
metadata; ordinary API endpoints retain the base policy below.
Added by SecurityHeadersMiddleware to every response:
| Header | Value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Referrer-Policy |
strict-origin-when-cross-origin |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), payment=() |
Content-Security-Policy |
default-src 'none'; frame-ancestors 'none' |
Non-GET responses additionally receive:
Cache-Control: no-storePragma: no-cache
- Authority: Keycloak OIDC metadata endpoint.
- One resource audience: Keycloak:Audience defaults to islamu-event-api; an explicit deployment value replaces that default. The signed aud must contain the resource; azp never substitutes for it. Retired ValidAudiences arrays are not consulted.
- KeycloakTokenContextPolicy runs after cryptographic JWT validation for both supported token representations. JOSE typ must be JWT and signed payload typ must be Bearer. Duplicate/malformed audience, purpose or client fields fail closed. Audience arrays must contain distinct nonempty strings.
- Keycloak:PermittedClients optionally restricts azp independently. Without a restriction azp may be absent, but a present value must still be one nonempty string. Configured client entries must be nonempty and distinct.
- Clock skew tolerance: 5 minutes.
- Dev mode: accepts self-signed certificates.
- JWT event logging uses fixed
authentication_failed(Warning) andauthentication_challenge(Debug) categories with native route/trace/correlation.IncludeErrorDetails=false; challenge error descriptions/URIs are cleared and the only failure error isinvalid_token, without provider exception content.
GET: usually[AllowAnonymous]POST/PUT/DELETE:[Authorize]- Privileged operations: role/policy constrained
- User ID extraction fallback order:
sub→nameidentifier→sid.
KeycloakOpenApiSecurityTransformer always defines HTTP Bearer (JWT) and
header ApiKey (X-API-Key) authentication for native documentation generation.
Operations using default [Authorize] metadata advertise these as separate
alternatives, matching MultiAuth provider selection for Local Identity,
AT Protocol sessions, external JWTs, and API keys. A valid configured Keycloak
authorization URL or authority additionally exposes the existing Keycloak OAuth
flow and alternative; absent configuration never invents an OAuth endpoint.
There is no document-wide security requirement. Anonymous operations, explicit
specialized authentication schemes, and pre-existing operation requirements
retain their own contracts. These descriptions do not change runtime token
validation, tenant binding, scope checks, or resource authorization.
ApiTenantResolutionMiddleware exempts POST /api/auth/local/login,
authenticated GET /api/user for Local-session validation, and authenticated
GET /api/user/admin-authority from tenant resolution. Native Local credentials,
the current user's identity, and persisted administrator authority are
instance-scoped; the BFF can therefore sign in and validate an administrator
at the dedicated admin host without naming an ordinary tenant. Credential
verification retains its rate limiter and no-store response; both identity
reads still require a valid bearer token. The current-user exception is
method-specific: DELETE /api/user, event reads, and other tenant-scoped
API actions still fail closed without a resolved tenant. The persona HTTP
regression verifies these boundaries.
AuthorizationCommandHandlerDecorator and AuthorizationQueryHandlerDecorator check:
IAuthorizedRequestinterface — commands/queries declare required permissions.[AuthorizeResource]attribute — declarative resource-level authorization.ISecureRequest— provides dynamic resource context for permission evaluation.
Denied requests throw AuthorizationException → mapped to 403 Forbidden by exception handler.
Non-interactive callers authenticate with long-lived X-API-Key credentials in the form {keyId}.{secret}. The endpoint contract is otherwise identical to JWT callers — only the credential presentation and principal shape differ.
Owner Types are normalized lookup rows. Write contracts use externalApiKeyOwnerTypeId; read contracts expose externalApiKeyOwnerTypeId, externalApiKeyOwnerTypeCode, and externalApiKeyOwnerTypeName so clients do not depend on domain enum serialization.
| ID | Code | Owner | Tenant Binding | Effective Authority |
|---|---|---|---|---|
1 |
USER |
User | Required | Key inherits the owner user's memberships (tenant/org/group admin claims) |
2 |
ORGANIZATION |
Organization | Required | Acts as organization admin for the owning org within the bound tenant |
3 |
GROUP |
Group | Required | Acts as group admin for the owning group within the bound tenant |
4 |
TENANT |
Tenant | Required | Acts as tenant admin for the bound tenant |
5 |
INSTANCE_ADMIN |
Instance Admin | Nullable credential | Platform operator; tenant-scoped API/MCP execution requires an explicit tenant hint, while explicit host-administration routes may run without tenant context |
Scope Model (ExternalApiKeyScopes): events:read, events:write, organizations:read, organizations:write, groups:read, groups:write, users:read, users:write, lookups:read, mcp:read, registrations:write, mcp:propose, api-keys:manage, admin:tenant, admin:instance. Effective permissions are the intersection of (a) the scopes on the key and (b) the owner's authority ceiling (ExternalApiKeyScopeCeiling). Keys cannot hold scopes above their owner type. Anonymous-safe MCP event tools expose only published public event data; program/session tools first require the event detail query to resolve a Published + Public event before returning lower-level program or session data. Protected MCP event-management reads such as list_my_events, get_event_creation_context, get_event_publish_readiness, event_management_context, and the Phase 5 program/custom-property/registration/team/template/sync contexts require a user bearer session or, for API keys, both mcp:read and the existing event read scope gate (events:read, events:write, or tenant/admin equivalent accepted by MachineScopeMapping). mcp:read alone does not grant private event-management discovery. These reads derive user and tenant context from the authenticated request and existing Application services rather than accepting caller-supplied user, tenant, role, or claim data. The management-context resource derives edit/delete/publish/publish-readiness/add-session/session-create-context availability from REST HAL _links, not from MCP-side role checks. The standalone get_event_publish_readiness tool also requires the REST HAL publish-readiness affordance before it calls GetEventPublishReadinessRequest, so MCP cannot broaden publish readiness beyond the current management action surface. MCP-specific scopes are least-privilege AI-conversation and adapter scopes: mcp:read allows scoped MCP read discovery, mcp:read plus event read scope authority allows protected event-management reads, and mcp:propose is required to discover/call MCP proposal tools and prompts without granting event writes or confirmation authority.
Authentication Flow:
ApiKeyAuthenticationHandlerparses theX-API-Keyheader, splits{keyId}.{secret}.- Repository lookup via
IgnoreTenantFilter(auth path only) returns the stored key. - Secret is SHA256-hashed and verified with fixed-time comparison against
SecretHash. Failed authentication attempts are recorded with boundedoutcome,tenant_id, andowner_typetags only; raw API keys, secrets, and request paths are never metric tags. ApiTenantPostAuthenticationMiddlewareasserts the API-keyTenantIdmatches the resolved request tenant. Tenant-bound keys with mismatched hints return404 Tenant mismatch.InstanceAdminkeys remain nullable credentials, but tenant-scoped API and MCP requests must resolve an execution tenant through the normal tenant hint/host pipeline. With an explicit tenant hint, the middleware binds that tenant for the request; without one, tenant-scoped API/MCP calls fail closed with404andcode=tenant_required. Only explicit host-administration API routes continue without tenant context.- Principal is materialized with claims
explore:api-key:id,explore:tenant:id(absent for the persisted InstanceAdmin credential),explore:api-key:owner:type,explore:api-key:owner:id, and repeatedexplore:api-key:scopeclaims. TouchUsageMetadataupdatesLastUsedAt/LastUsedIp(5-minute throttle per key).
Machine Principal Authorization: IMachinePrincipalAccessor exposes the current ApiKeyPrincipalContext to both authorization providers. CerbosPrincipalBuilder.BuildMachinePrincipalAsync emits a Cerbos principal with is_machine=true, api_key_id, owner_type, owner_id, scopes, and synthesized isInstanceAdmin/tenantMemberships/orgMemberships attributes derived from owner type. FallbackAuthorizationService applies MachineScopeMapping.ScopesPermit as a fast-reject gate before dispatching to owner-type-specific authority checks — so machine principals evaluate consistently against both local and Cerbos-backed authorization.
Management Endpoints (/api/ExternalApiKey):
| Verb | Route | Purpose | Response |
|---|---|---|---|
GET |
/api/ExternalApiKey |
List keys visible to the caller | HAL collection |
GET |
/api/ExternalApiKey/{id} |
Key detail (metadata only, no secret) | HAL resource |
POST |
/api/ExternalApiKey |
Issue once or recover metadata with a required operation key | 200 success-only ExternalApiKeyIssuanceDto |
PATCH |
/api/ExternalApiKey/{id} |
Update policy (scopes, expiry, quotas) | Command result |
DELETE |
/api/ExternalApiKey/{id} |
Revoke key (soft delete, status=Revoked) | 204 No Content |
GET |
/api/ExternalApiKey/usage-report |
Tenant admins see their tenant; instance admins see platform-wide | Aggregated report |
Create/revoke/update emit business metrics (created, revoked, policy_updated) tagged with tenant_id and owner_type.
POST /api/ExternalApiKey requires exactly one Idempotency-Key header:
1..128 ASCII characters matching [A-Za-z0-9._:-]. Missing, duplicate,
comma-combined, whitespace, Unicode, or otherwise invalid values fail 400.
The key is case-sensitive. The native CreateExternalApiKeyCommand also
requires OperationKey and validates the same Domain grammar, so non-HTTP
callers cannot bypass this requirement. OpenAPI marks the header as required.
For example, send Idempotency-Key: key-create-20261005-01 on a single
creation intent. The generated C# client call is
CreateExternalApiKeyAsync(operationKey, dto); keep both arguments stable
after a lost response. The body chooses requested policy and legitimate
organization/group targets, not the current actor or tenant. The handler
resolves the platform user through IAdminContext.ResolveUserIdAsync and
takes scope from the trusted tenant context; InstanceAdmin issuance is
explicitly global (TenantId = null).
Unresolved trusted tenancy is a validation failure for User, Tenant,
Organization and Group issuance: HTTP returns 400 before the clean write-scope
guard or transaction, without a key or receipt. Global InstanceAdmin issuance
continues to work without selecting a tenant.
| Outcome | HTTP | Disclosure |
|---|---|---|
| First acknowledged creation | 200 |
disclosureStatus = "Issued"; apiKey contains the raw credential |
| Authorized retry with the same normalized policy | 200 |
disclosureStatus = "PreviouslyIssued"; apiKey = null; stable id and keyId |
| Same operation identity, different normalized policy | 409 |
Conflict; no new credential |
| Missing or empty authenticated platform-user binding | 401 |
No recovery metadata |
| Tenant-owned issuance without a resolved trusted tenant | 400 |
Validation; no credential or receipt |
| Current owner authority revoked or unavailable | 403 |
No recovery metadata |
| Issued key removed, revoked, expired, or otherwise unusable | 404 |
No recovered credential |
The public success payload requires id, keyId, disclosureStatus, and
apiKey. Its disclosure enum contains exactly Issued and PreviouslyIssued;
it cannot be null or represent failure. Recovery explicitly carries null
apiKey. Native commands retain failure metadata in
CreateExternalApiKeyCommandResponse with no Issue; the controller publishes
only the validated success payload or ProblemDetails. The browser validates
generated success data before accepting it and keeps failure guidance in a
local result, not a fabricated generated success contract.
The input digest covers every accepted field: trimmed name, normalized optional description, owner-type and target IDs, trimmed/lowercased/deduplicated/sorted scopes, UTC expiry, normalized credit period, credit limit, and rollover limit. It excludes current actor and tenant identity; those bind the separate operation fingerprint. Recovery of a committed operation does not generate entropy or create another credential; an uncommitted attempt can still complete issuance.
The creation service distinguishes definitive 400/401/403/404/409
rejections from transport errors and 5xx uncertainty. It renders fixed,
status-specific cancellation, sign-in, access-restoration, or list-review guidance
without reflecting the remote body or exception. The dialog keeps the submitted
policy and operation key frozen; a new intent remains deliberate rather than an
automatic replacement for a rejected request.
[SuppressIdempotencyResponseStorage] bypasses generic replay response capture
for this action; [ResponseCache(NoStore = true)] remains. Recovery comes from
a digest-only issuance receipt, never a saved response body. See
receipt retention,
commit authority,
and the operator recovery guide.
The exact GET-list and POST-creation route, and DELETE with one aggregate ID, may proceed without a tenant only after normal tenant resolution has been attempted. This supports global InstanceAdmin issuance and the existing revoke/reissue remedy on the administrator host. It grants no owner authority: native handlers retain their current ownership checks, and issuance rejects tenant-bound owners without a resolved scope. List, revoke, and issuance resolve the current provider-to-platform-user binding instead of parsing a provider's GUID subject as the platform user ID.
Trusted ERP, CRM, and managed-hosting operators provision customers through the managed-provider composition endpoint:
| Verb | Route | Route Name | Purpose | Response |
|---|---|---|---|---|
POST |
/api/managed-provider-provisioning/clients:ensure |
EnsureManagedProviderClientProvisioned |
Create or rehydrate a provider-customer tenant, external admin identity, tenant-local user state, tenant-admin grant, and optional approved organizer actor. | BaseCommandResponse<ManagedProviderClientProvisioningResultDto> |
Security and tenancy rules:
- The endpoint is
[Authorize], classified asEndpointClass.Admin, and explicitly checksIAdminContext.IsInstanceAdminAsync; ERP customer/admin identities are never treated as instance administrators by this path. - Provider automation identifies the customer with stable
providerKey,externalSystem, andexternalCustomerId; OIDC user linkage uses stableidentityProvider+subject, not mutable email or display names. - The Application command writes provider-neutral
ExternalBindingrecords for the customer tenant, tenant-local user state, user actor, external login, and optional organizer records. Existing bindings rehydrate the original IDs for retry-safe provisioning. - Tenant-local user status/profile/moderation state is stored in
TenantUserandTenantUserProfile; the globalUserremains the auth/account anchor. - Do not send arbitrary tenant headers as provisioning authority. The command creates or resolves the tenant from trusted bindings and returns the resulting internal IDs.
- Send
Idempotency-Keyon HTTP retries. The durable source of truth is still theExternalBindinguniqueness model, so a replay with the same provider/customer IDs returns the existing provisioning result.
Tenant role authority is exposed through explicit role-grant endpoints, not the former tenant-member contract:
| Verb | Route | Route Name | Purpose | Response |
|---|---|---|---|---|
GET |
/api/tenant-user-role-grants |
GetTenantUserRoleGrants |
List active/revoked tenant-local role grants for tenant admins in the resolved tenant or instance admins. | HAL collection of TenantUserRoleGrantListDto |
GET |
/api/tenant-user-role-grants/{id} |
GetTenantUserRoleGrantById |
Retrieve one tenant-local role grant for an authorized tenant or instance admin. | HAL resource of TenantUserRoleGrantDto |
POST |
/api/tenant-user-role-grants |
CreateTenantUserRoleGrant |
Grant a tenant-scoped role to an active TenantUser. |
BaseCommandResponse<Guid> |
DELETE |
/api/tenant-user-role-grants/{id} |
RevokeTenantUserRoleGrant |
Revoke a grant and populate revoke audit fields. | 204 No Content |
Contract rules:
- Create accepts
TenantUserIdand tenant-scopedRoleId; tenant identity is derived fromITenantContext, not request bodyTenantId. - Read DTOs are identity-bearing administrative projections.
GETroutes require authentication plusislamuevent_tenant_user_role_grantresource authorization for actionview; regular authenticated users cannot enumerate or inspect tenant role grants. - Handlers validate that the
TenantUserbelongs to the resolved tenant, is active, is not soft-deleted, and that the role has tenant scope. - Grant changes are create/revoke, not update-in-place. HAL detail resources may expose
revoke; collection resources may exposecreate; clients must render actions from_links. - Cerbos/local authorization uses resource kind
islamuevent_tenant_user_role_grantwithview,create, anddeleteactions.
Organization membership endpoints expose identity, role, and position data for organization administration:
| Verb | Route | Route Name | Purpose | Response |
|---|---|---|---|---|
GET |
/api/organizationmember/{organizationId} |
GetOrganizationMembersByOrganization |
List members of one organization for authorized organization or tenant administrators. | HAL collection of OrganizationMemberDto |
GET |
/api/organizationmember/member/{id} |
GetOrganizationMemberById |
Retrieve one organization member for authorized organization or tenant administrators. | HAL resource of OrganizationMemberDto |
POST |
/api/organizationmember |
AddOrganizationMember |
Add or invite an organization member inside the resolved tenant. | BaseCommandResponse<Guid> |
PUT |
/api/organizationmember/role |
UpdateOrganizationMemberRole |
Change a member role or position. | BaseCommandResponse<Guid> |
DELETE |
/api/organizationmember/{id} |
DeleteOrganizationMember |
Remove a member. | 204 No Content |
Contract rules:
OrganizationMemberDtois an identity-bearing administrative projection. It includestenantId,organizationId,userId,userFullName,userEmail, role, and position fields; it is not a public organization profile DTO.- Member list/detail reads require authentication plus operation resource authorization for resource kind
islamuevent_organization_memberand actionview. Regular authenticated users without tenant-admin or organization-admin authority receive403. - List reads authorize with the resolved tenant id and route organization id. Detail reads authorize by member id, and the authorization decorator enriches the resource attributes from the repository before evaluating Cerbos/local fallback policy.
- Create and HAL collection affordances carry the resolved tenant id and organization id so tenant-admin and organization-admin checks use the same resource/action context as the API path.
- Clients must gate member-management UI from HAL
_linkssuch as collectioncreateand item edit/delete links, not from local role or claim inspection.
Outgoing product webhooks are managed under /api/webhooks. These routes configure ISLAMU Event sending webhook events to external systems through the selected outgoing provider (Disabled, Local, Svix, Composite, or DryRun). Incoming provider callbacks such as Coop, Osprey, payment, email, or future Svix operational callbacks remain separate integration endpoints under /api/integrations/*; they do not require the outgoing provider to be enabled.
| Verb | Route | Route Name | Purpose | Response |
|---|---|---|---|---|
GET |
/api/webhooks/event-types |
GetWebhookEventTypes |
Public primary event catalog with schema/example metadata. | IReadOnlyList<WebhookEventTypeDto> |
GET |
/api/webhooks/consumers |
GetWebhookConsumers |
Tenant-scoped webhook consumers/integration owners visible to the caller. | HAL collection of WebhookConsumerDto |
GET |
/api/webhooks/consumers/{consumerId} |
GetWebhookConsumerById |
One tenant-scoped webhook consumer. | HAL resource of WebhookConsumerDto |
POST |
/api/webhooks/consumers |
CreateWebhookConsumer |
Create a tenant-scoped consumer for Local/Svix/Composite/DryRun/Disabled mode. | BaseCommandResponse<Guid> |
GET |
/api/webhooks/endpoints |
GetWebhookEndpoints |
Tenant-scoped webhook endpoints, optionally filtered by consumer. | HAL collection of WebhookEndpointDto |
GET |
/api/webhooks/endpoints/{endpointId} |
GetWebhookEndpointById |
One tenant-scoped webhook endpoint with enabled event subscriptions. | HAL resource of WebhookEndpointDto |
POST |
/api/webhooks/endpoints |
CreateWebhookEndpoint |
Create a Local/Svix-mirrored endpoint and subscribe it to enabled event types. | BaseCommandResponse<Guid> |
PUT |
/api/webhooks/endpoints/{endpointId} |
UpdateWebhookEndpoint |
Update a tenant-scoped webhook endpoint URL, delivery controls, and event type subscriptions. | BaseCommandResponse<Guid> |
DELETE |
/api/webhooks/endpoints/{endpointId} |
DeleteWebhookEndpoint |
Archive a tenant-scoped webhook endpoint while preserving delivery history. | 204 No Content |
POST |
/api/webhooks/endpoints/{endpointId}/rotate-secret |
RotateWebhookEndpointSecret |
Rotate the endpoint signing secret reference while preserving a bounded previous-secret overlap window. | BaseCommandResponse<Guid> |
POST |
/api/webhooks/endpoints/{endpointId}/test |
TestWebhookEndpoint |
Schedule a signed LocalProvider test delivery for one active tenant-scoped endpoint. | BaseCommandResponse<Guid> |
GET |
/api/webhooks/messages |
GetWebhookMessages |
Tenant-scoped authoritative webhook messages and provider handoff state. | HAL collection of WebhookMessageDto |
GET |
/api/webhooks/messages/{messageId} |
GetWebhookMessageById |
One tenant-scoped webhook message without raw payload material. | HAL resource of WebhookMessageDto |
GET |
/api/webhooks/messages/{messageId}/payload |
GetWebhookMessagePayload |
Separately authorized exact payload bytes while the tenant-scoped retention window remains open. | WebhookMessagePayloadDto |
GET |
/api/webhooks/delivery-attempts |
GetWebhookDeliveryAttempts |
Tenant-scoped LocalProvider delivery attempts, optionally filtered by message or endpoint. | HAL collection of WebhookDeliveryAttemptDto |
GET |
/api/webhooks/delivery-attempts/{attemptId} |
GetWebhookDeliveryAttemptById |
One tenant-scoped delivery attempt with safe HTTP outcome metadata. | HAL resource of WebhookDeliveryAttemptDto |
POST |
/api/webhooks/delivery-attempts/{attemptId}/retry |
RetryWebhookDeliveryAttempt |
Schedule a manual retry from a failed or abandoned LocalProvider delivery attempt. | BaseCommandResponse<Guid> |
GET |
/api/webhooks/bulk-replays/preview |
PreviewWebhookBulkReplay |
Preview bounded eligible and excluded counts for an explicit UTC/consumer/endpoint/event filter. | WebhookBulkReplayPreviewDto |
GET |
/api/webhooks/bulk-replays |
GetWebhookBulkReplays |
List recent tenant-scoped durable replay operations. | HAL collection of WebhookBulkReplayOperationDto |
GET |
/api/webhooks/bulk-replays/{operationId} |
GetWebhookBulkReplayById |
Poll one durable replay operation and its normalized lifecycle evidence. | HAL resource of WebhookBulkReplayOperationDto |
POST |
/api/webhooks/bulk-replays |
ScheduleWebhookBulkReplay |
Queue an idempotent bounded Local replay operation after server-side eligibility re-evaluation. | 202 Accepted with BaseCommandResponse<Guid> |
POST |
/api/webhooks/bulk-replays/{operationId}/cancel |
CancelWebhookBulkReplay |
Cancel a still-queued operation using its observed concurrency version. | BaseCommandResponse<Guid> |
POST |
/api/webhooks/svix/app-portal |
OpenSvixAppPortal |
Generate short-lived backend-only Svix App Portal access. | WebhookProviderPortalAccessDto |
Contract rules:
GET /event-typesis anonymous and cacheable lookup data. It exposes registry-driven schema/example metadata and includes persisted event type IDs after startup catalog synchronization so management clients can create endpoint subscriptions.- Consumer DTOs expose normalized
consumerKindId/name,statusId/name, andproviderModeId/name; they never expose endpoint secrets. - Consumer create derives
tenantIdfromITenantContext, validates domain enum IDs in the Application handler, sets status toActive, and returns conflict ProblemDetails for duplicate tenant-local names. - Endpoint DTOs expose normalized status fields, provider endpoint ids, bounded timeout/retry/rate-limit settings, last success/failure timestamps, and enabled subscription event types. They never expose
secretRefor secret material. - Endpoint create derives
tenantIdfromITenantContext, requires an active tenant-local consumer, validates an absolute HTTP(S) URL, stores only the supplied secret reference, rejects duplicate tenant/consumer URLs, and fails closed when requested event type IDs are missing, duplicated, disabled, or unknown. - Endpoint update replaces URL, delivery controls, and the enabled event-type subscription set after validating all requested event types. It does not rotate signing secrets; secret rotation remains a separate route.
- Endpoint delete is a soft archive operation. Archived endpoints leave active lists and lose mutation HAL affordances while preserving authoritative delivery history.
- Endpoint secret rotation accepts
newSecretRefand optionalpreviousSecretValidForSecondsonly. It never accepts or returns raw signing secret material, rejects unchanged secret references, incrementssecretVersion, stores the old reference aspreviousSecretRef, and sets a boundedpreviousSecretValidUntiltransition window. Repeated calls without anIdempotency-Keycreate distinct rotations. - Endpoint test scheduling creates an authoritative
webhook.testmessage plus one LocalProvider delivery attempt for the target endpoint. It requires an active Local or Composite consumer endpoint; Svix-managed endpoint tests belong in the Svix App Portal because Svix owns provider-side endpoint delivery/replay semantics. - Message DTOs expose tenant, event type, event id, aggregate reference, consumer/provider state, payload hash, and retention timestamps. They intentionally do not expose
payloadJsonor raw sensitive event data. - Payload reads require the distinct
webhook:view-payloadaction. The dedicated response base64-encodes the normalized bytes and includes only content type/encoding, hash, byte length, retention cutoff, and retrieval time. The action writes a mandatoryPAYLOAD_VIEWEDaudit before returning data and fails closed if audit persistence fails. - Payload responses set
Cache-Control: no-store,no-cacheandPragma: no-cache. A missing or cross-tenant message returns the same generic404; a known tenant-local message whose bytes are expired or cleared returns410. HAL emitspayloadonly while the bytes are retained and the caller passes the separate permission check. - Delivery attempt DTOs expose endpoint/message references, attempt number, status, bounded HTTP status/failure/duration metadata, next retry time, and a safe response-body preview only. They do not expose endpoint secrets, request payloads, authorization headers, or full endpoint responses.
- Manual retry is attempt-based. Only failed or abandoned attempt detail resources may expose
retry, and the command delegates scheduling to the LocalProvider delivery drain service. - Bulk replay requires
webhook:bulk-replay. Preview and execution use the messageMaterializedAthalf-open interval[fromUtc,toUtc), with optional exact consumer, endpoint, and event-type filters. Only terminal Local targets (DEAD_LETTEREDorABANDONED) can become eligible. Active holds, expired/cleared payloads, inactive endpoints, nonterminal/succeeded Local work, and every provider publication are excluded and counted; provider conflict, unknown, and manual-reconciliation states have distinct exclusion counts and are never guessed or blindly republished. - Scheduling requires an operator reason and stable
operationKey. Reusing the key with identical authoritative filters returns the existing operation; changing any parameter returns409. Configured operation and per-tenant reserved-item ceilings are checked under a tenant advisory lock. The worker rechecks eligibility in its transaction and only changes eligible Local targets toRETRY_DUE; ordinary Local claim workers continue to enforce tenant/endpoint fairness, in-flight limits, rate limits, signing, and retry policy. Cancellation is available only inQUEUEDand requiresexpectedConcurrencyVersion; worker start and cancellation therefore resolve without an ABA race. - HAL collection resources may expose
create; active endpoint detail resources may exposeupdate,rotate-secret,test, anddelete; archived endpoint detail resources expose no mutation affordances. Message resources may exposedelivery-attempts,provider-publications, and the separately authorized retainedpayloadrelation; retryable attempt detail resources may exposeretry; Svix or Composite consumer detail resources may exposeopen-provider-portal. Clients must render webhook actions from_links, not client-side role checks. - The Svix App Portal route returns only short-lived URL/token data. The Svix API token is resolved server-side through the configured secret provider and is never sent to Blazor.
Managed reporting routing APIs expose tenant-owned provider configuration, readiness, and dashboards without returning provider secrets or report payloads.
| Verb | Route | Route Name | Purpose | Response |
|---|---|---|---|---|
GET |
/api/tenant/settings/moderation-reporting/routing-state |
GetModerationReportingRoutingState |
Current-tenant routing state, lock flags, provider target configured flags, and HAL affordances. | HAL resource of ReportingRoutingStateDto |
PATCH |
/api/tenant/settings/moderation-reporting/routing-state |
UpdateModerationReportingRoutingSettings |
Update supplied policy, Osprey, or Coop groups when their instance delegation locks allow it. Nested credential input is write-only; omitted groups and secret leaves preserve existing values. | BaseCommandResponse<Guid> |
POST |
/api/tenant/settings/moderation-reporting/routing-state/test/{provider} |
TestModerationReportingProvider |
Readiness-check a tenant Osprey or Coop target without external HTTP dispatch or secret output. | BaseCommandResponse<Guid> |
PATCH |
/api/instance/settings/moderation-reporting/locks |
UpdateInstanceModerationReportingProviderLocks |
Independently update general, Osprey, or Coop reporting-provider delegation lock groups. | BaseCommandResponse<Guid> |
GET |
/api/tenant/settings/moderation-reporting/dashboard |
GetTenantModerationReportingDashboard |
Current-tenant aggregate queue and provider-sync health. | HAL resource of TenantModerationReportingDashboardDto |
GET |
/api/admin/control-plane/operations |
GetControlPlaneOperations |
Instance control-plane operations status now includes aggregate moderation-reporting provider-sync and tenant lock-impact metrics. |
HAL resource of ControlPlaneOperationsDto |
Contract rules:
- Routing-state and dashboard reads are tenant-scoped and redacted. They may expose provider target identifiers, configured flags, aggregate counts, and HAL links, but never raw endpoint URLs, API keys, webhook secrets, provider payloads, correlation IDs, report evidence, or raw provider errors.
- Tenant routing PATCH bodies contain optional
policy,osprey, andcoopgroups. Omitted groups preserve persisted values. The general provider lock blocks every routing patch, while provider-specific locks block only a supplied matching provider group. - Tenant update commands accept endpoint URLs and secret values only as request input. Provider credentials are nested explicitly under the matching provider group. Response DTOs, HAL resources, generated response models, logs, metrics, traces, screenshots, and ProblemDetails must not echo those values.
- Provider test actions are readiness checks over effective routing state. They validate lock state, provider enablement, tenant target presence, and configured endpoint/API-key flags; they do not call external provider endpoints.
- HAL rels
routing-state,edit,test-osprey-provider, andtest-coop-providerare the client action source of truth. Clients must not recreate hidden actions from local roles or claims. - Reporter-owned communication consent uses
PATCH /api/event-reports/my/{reportId}/communication-consentwith one requiredconsentgroup containing both purpose-specific choices. Ownership, privacy-erasure fencing, audit-neutral unchanged requests, transactional persistence, and user-scoped cache invalidation remain enforced by the existing command handler.
Incoming webhooks are provider callbacks received by ISLAMU Event. They are separate from outgoing product webhooks and continue to work when the outgoing provider is Disabled, Local, Svix, Composite, or DryRun.
| Verb | Route | Route Name | Auth | Purpose |
|---|---|---|---|---|
POST |
/api/integrations/moderation/osprey/callback |
ModerationIntegrationOspreyCallback |
API-key policy ModerationIntegration.OspreyCallback |
Records bounded Osprey-compatible moderation signals on the local report without executing moderation actions. |
POST |
/api/integrations/moderation/coop/callback |
ModerationIntegrationCoopCallback |
API-key policy ModerationIntegration.CoopCallback plus signed raw-body HMAC verification |
Atomically retains the verified callback and its unique Coop decision-effect pointer. A fenced background worker dispatches the existing decision command and completes the pointer only after command success. |
POST |
/api/integrations/registration/{provider}/{bindingId}/callback |
RegistrationProviderCallback |
[AllowAnonymous] plus provider verifier as authentication |
Reads up to 256 KiB, verifies binding/proof through provider-neutral callback services, captures one registration.provider_submission incoming-webhook effect, and returns 202 Accepted for accepted, duplicate, malformed, unknown, stale, or parked evidence except oversize payloads (413). |
GET |
/api/admin/incoming-webhook-effects/status?tenantId={tenantId}&limit={limit} |
GetIncomingWebhookEffectStatus |
Authenticated plus Webhooks.ViewDelivery authorization |
Returns tenant-scoped safe effect lifecycle rows and HAL item affordances; limit range is 1..200. |
POST |
/api/admin/incoming-webhook-effects/tenants/{tenantId}/{effectOutboxId}/redrive |
RedriveIncomingWebhookEffect |
Authenticated plus Webhooks.RedriveIncoming authorization |
Redrives an eligible dead-lettered effect when expectedProcessingGeneration still matches and the retained callback remains replayable. |
POST |
/api/integrations/svix/operational |
IntegrationSvixOperationalCallback |
[AllowAnonymous] with Svix-compatible signature verification as authentication |
Accepts Svix operational callbacks without requiring the outgoing provider mode to be Svix. Tenant-addressed payloads are captured in the incoming webhook ledger; instance-level operational payloads are verified and acknowledged without side effects. |
Incoming callback rules:
- Raw request bodies are read before JSON parsing and verified against provider signatures where the provider supplies a signature.
- Signed callbacks enforce bounded body sizes, timestamp tolerance, and constant-time signature comparison.
- Verified tenant-scoped callbacks are stored in
incoming_webhook_messagesbefore any Application side effect. Coop decision callbacks create a specialized pointer in the intake transaction; command dispatch occurs later outside that transaction. - Duplicate provider message IDs are treated idempotently and do not re-run side effects.
- The incoming webhook ledger stores the tenant and provider message identifiers needed for idempotency, plus payload hashes, bounded status/failure metadata, and redacted headers only. Logs, metrics, and ProblemDetails use bounded provider/outcome/failure categories and must not include raw payloads, signature headers, secrets, tokens, authorization headers, tenant/user identifiers, provider message IDs, or raw provider errors.
- Coop effect status exposes only internal lifecycle identifiers/state, bounded failure category/detail, attempts, generation/fence, and timestamps. It excludes callback bytes, callback hash, signed provider decision ID, headers, and raw exceptions. HAL emits
redriveonly for a dead-lettered row and remains the client action authority.
Provider-neutral registration integration management is authenticated, event-scoped, private/no-store, and available only when the parent Event HAL exposes manage-registration-channels or view-registration-provider-health. It does not include a concrete Phase 10 provider adapter.
| Verb | Route | Route Name | Authority | Purpose |
|---|---|---|---|---|
GET/POST |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/connections |
GetRegistrationProviderConnections / CreateRegistrationProviderConnection |
Tenant update for connection writes | List or create provider connections with secret-binding IDs only. |
GET/PUT/DELETE |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/connections/{connectionId} |
GetRegistrationProviderConnection / UpdateRegistrationProviderConnection / DeleteRegistrationProviderConnection |
Tenant update | Read, update, or soft-delete a connection; delete fails while bindings reference it. |
PUT |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/connections/{connectionId}/approved-origins |
ReplaceRegistrationProviderApprovedOrigins |
Tenant update | Replace the SSRF-safe browser/embed origin allowlist. |
GET/POST |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/bindings |
GetRegistrationProviderBindings / CreateRegistrationProviderBinding |
manage-registration-channels |
List or create draft bindings for exact form/version and mode/trust lookup IDs. |
GET/PUT/DELETE |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/bindings/{bindingId} |
GetRegistrationProviderBinding / UpdateRegistrationProviderBinding / DeleteRegistrationProviderBinding |
manage-registration-channels |
Manage draft bindings; published mappings are immutable. |
PUT |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/bindings/{bindingId}/mappings |
ReplaceRegistrationProviderMappings |
manage-registration-channels |
Replace draft field/option mappings. |
POST |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/bindings/{bindingId}/publish |
PublishRegistrationProviderBinding |
manage-registration-channels |
Publish when drift is nonblocking. |
GET/POST/PUT/DELETE |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/workflows/{workflowId}/requirements/{requirementId}/channels[/{channelId}] |
GetRegistrationChannels / CreateRegistrationChannel / UpdateRegistrationChannel / DeleteRegistrationChannel |
manage-registration-channels |
Manage native/provider channel order and fallback. |
GET |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/workflows/{workflowId}/requirements/{requirementId}/channels/{channelId}/bindings/{bindingId}/launch-descriptor |
GetRegistrationProviderLaunchDescriptor |
manage-registration-channels |
Return one server-generated redirect/embed/manual descriptor from approved connection origins. |
GET |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/health |
GetRegistrationProviderHealth |
view-registration-provider-health |
Return bounded connection validity, callback age, drift, reconciliation lag, queue depth, and capability codes. |
GET/POST/POST/POST |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/queue, /manual-imports, /queue/retry, /queue/resolve |
queue/manual/retry/resolve routes | manage-registration-channels |
List parked issue codes and manage bounded reconciliation without answers or raw provider payloads. |
POST |
/api/tenants/{tenantId}/events/{eventId}/registration-providers/{bindingId}/reconcile |
PollRegistrationProviderReconciliation |
manage-registration-channels |
Request bounded provider reconciliation when the exact tuple has RECONCILIATION. |
Returned HAL relations include manage-registration-channels, view-registration-provider-health, provider-create, origins, mappings, publish, manual-import, poll, retry, resolve, and launch-descriptor. Clients must use those links as authority and must not infer access from roles, provider names, drift state, or capability booleans.
Reporter communication contracts use two required, independently selected booleans: ReportCaseUpdatesConsent covers acknowledgements, status updates, and final outcomes, while ReportFollowUpContactConsent covers requests for clarification or additional evidence. POST /api/event-reports, reporter-owned reads, and moderation reads expose both values; anonymous submissions force both to false. The pre-1.0 ReporterContactConsent field was removed without a compatibility alias, so clients must regenerate from OpenAPI. PUT /api/event-reports/my/{reportId}/communication-consent updates both purposes for the authenticated reporter's own report and returns the refreshed HAL resource. My Reports detail and collection items expose update-communication-consent only after the current-user User/Update authorization-provider check succeeds, and the write handler repeats that exact provider decision before opening its transaction. Tenant and ReporterUserId ownership checks remain defense in depth; missing identity, provider denial, non-owner, tenant mismatch, and indeterminate authorization fail closed. Clients must render withdrawal controls only when that relation exists.
The HATEOAS system uses a layered architecture to ensure "Plug-and-Play" compatibility for all consumers:
ResourceAssemblerBase<TDto, TListDto>— Base class for assembling HAL responses. Implements the high-performance 4-Phase Capability Planning Pipeline. Most families need no assembly behavior of their own and use the concrete genericHalResourceAssembler<TDto, TListDto>, registered byAddHalResource<...>; declare a subclass only when a family genuinely assembles differently.ILinkPolicy<TDto>/ICollectionLinkPolicy<TDto>— Per-entity link definitions using theyield returnpattern.LinkDefinition— Metadata for a link, including relation, route, and authorization requirements.HateoasAuthorizationEvaluator— The engine that batches and deduplicated permission checks.HateoasLinkGenerator— Resolves named routes to absolute URLs.RouteNames— 100+ named route constants ensuring type-safe link generation.
To prevent
- Candidate Selection: Link policies yield all possible link definitions for the resource(s).
- Normalization: The evaluator extracts
AuthorizationCheckobjects from permission-bearing links. - Batch Decisioning: Deduplicated checks are sent to the
IAuthorizationProviderin a single batch call. - Materialization: Authorized links are resolved to URLs and embedded into the
_linksobject.
Collection endpoints use BuildListResourcesWithBatch to ensure scalability:
- All link definitions for all items in a paginated result are collected first.
- These are flattened into one massive batch (potentially hundreds of checks).
- The evaluator deduplicates identical checks (e.g., if multiple items share the same parent).
- One single gRPC call (Cerbos) or one single profile resolution (Local) authorizes the entire list.
- Default format:
application/hal+jsonwith_linksand_embeddedsections. Prefer: return=minimal(RFC 7240) strips all_linksto save bandwidth for non-UI consumers.PreferHeaderMiddlewarereads thePreferheader and sets a flag consumed by assemblers.
Collection responses include standard pagination links: self, first, prev, next, last.
Link policies use ResourceDescriptors to extract resource metadata from DTOs, ensuring authorization is context-aware.
If the batch authorization call fails (e.g., network error to Cerbos), all permission-bound links are denied by default. Non-permission links (e.g., self) remain unaffected.
Ordinary fixed Event and EventSession lifecycle links are emitted only when the matching Domain lifecycle predicate accepts the DTO's current facts. Event actions use the current EventStatusId; restoration additionally requires the server-projected reversible-moderation-record eligibility. Session detail and collection policies use EventSessionStatusId, ParentEventStatusId, StartTime, EndTime, and EndTimeType, so publish and complete require a Published parent Event and publish requires a valid schedule. A Published Event never advertises archive. Heavy redaction remains a distinct Application/API-owned irreversible safety override rather than an ordinary Domain transition affordance.
HAL omits same-target ordinary fixed lifecycle actions because they do not change resource state, although sending such a command directly is a successful idempotent no-op. A true no-op may emit one structured idempotent outcome log after the unit of work completes for observability, but it does not change state or timestamps and produces no durable write, cache invalidation, or metric. Light moderation is the exception: applying it to an already-Moderated Event may repair missing child-session moderation, producing only the effects required by that repair and no duplicate moderation record, outbox, or federation work. Every advertised lifecycle mutation remains authenticated and permission-bound. Clients must follow _links rather than reconstructing these Domain decisions from status fields, parent status, schedule fields, roles, or claims.
The API's _links payload is the single source of truth for action affordances in the Blazor UI. The server already evaluated every authorization check and only emitted the links the caller is allowed to follow — the client must trust that contract and render UI affordances directly from it.
Blazor components gate mutation buttons (Edit, Delete, Create, etc.) with extension helpers defined in Explore.Blazor.Client/Helpers/HalResourceExtensions.cs:
private void CheckEditPermissions()
{
canEdit = organization?.HasHalLink("edit") ?? false;
}
// In markup:
@if (canEdit) { <AppButton StartIcon="@Icons.Material.Filled.Edit">Edit</AppButton> }The helpers (HasHalLink(this OrganizationDto, string), HasHalLink(this EventListDto, string), etc.) read _links from the generated DTO extension data or from typed client models that explicitly preserve HAL links. Collection-level affordances such as create must come from the HAL collection _links carried through PaginatedResult<T>.Links; they must not be inferred from the first row or from an empty-list fallback.
Never gate mutation UI through client-side role checks (RoleHelper.CanManage, user.IsInRole("OrgAdmin"), ClaimsPrincipal inspection). These duplicate server-side policy, drift over time, and leak authorization logic into the client. If the server didn't emit an edit link, the user is not allowed to edit — period.
Role/claim inspection is acceptable only outside action-gating contexts:
- Navigation menu filtering (
NavMenu.razor— determines which top-level sections are visible). - Eligibility previews for empty-state CTAs (
EventCreationEligibilityService— does the user belong to any org that could create an event?). - Client-side route guards that short-circuit before an API call (e.g. redirecting anonymous users away from
/my/*).
All three cases guard entire pages or menus, not per-resource actions, and none substitute for the authorization decisions the API already encoded in _links.
Every client DTO consumed for affordance gating must:
- Preserve item
_linkseither through[JsonExtensionData] IDictionary<string, object>? AdditionalPropertiesor an explicit[JsonPropertyName("_links")]Linksproperty when mapping to a UI model. - Preserve collection
_linkson paginated wrappers when a page-level action such ascreateis rendered independently of rows. - Have a matching
HasHalLink(this TDto, string linkRel)extension or wrapper method. - Never have a corresponding standalone permission flag (e.g.
CanEdit: bool) on the DTO — permission state must flow exclusively through_links.
Notification preference matrices follow this contract: NotificationPreferenceMatrixDto is served as a HAL resource, and Blazor gates save and set-mute exclusively from _links.
HAL link consumption is protected by three test layers:
Event.API.IntegrationTests/Features/Hateoas/HateoasLinkDeserializationTests.cs— wire-level regression guard that_linkssurvive NSwag round-trip.Event.API.IntegrationTests/Features/Hateoas/OrganizationHateoasAuthTests.cs— verifies authenticated vs. anonymous requests receive different link sets on embedded items.Explore.Blazor.Client.Tests/Pages/Organizations/OrganizationDetailsHateoasTests.cs— bUnit component test confirms Edit button renders iff_links.editis present and the page never callsIOrganizationMemberService.GetMembersAsyncon load.Explore.Blazor.Client.Tests/Helpers/EventTemplateHalResourceExtensionsTests.csandExplore.Blazor.Client.Tests/Pages/Admin/EventTemplateListPageTests.cs— prove event-template collectioncreatelinks survive empty collections and that page-level create is not inferred from row links.
The application uses a custom Specification Pattern for complex filtering, especially on the Event entity.
IQuerySpecification<T>— ComposesIFilterSpecification<T>+ISortSpecification<T>. Immutable builder pattern.IFilterSpecification<T>— Individual filter producingExpression<Func<T, bool>>.ISortSpecification<T>— Sort directives with field name and direction.
EventQuerySpecification is an immutable fluent builder that composes filters via AND logic:
spec = spec
.And(new EventFilter(...))
.And(new EventSubqueryFilter(...))
.And(new IslamicAspectFilter(...))
.And(new EventCustomPropertyProjectionFilter(...))
.SortByDescending(EventSort.StartUtc);
| Filter Class | What It Handles | Mechanism |
|---|---|---|
EventFilter |
Core fields (search, date, status, type, format) | Direct Expression<Func<Event, bool>> |
EventSubqueryFilter |
Junction tables (categories, tags, locations, languages, registration modes) + JSONB metadata | Subquery with Any() / All() |
IslamicAspectFilter |
Islamic module fields (madhab, gender mode) | Module-conditional — silently ignored when module disabled |
TechAspectFilter |
Tech module fields (skill level, stack) | Module-conditional — silently ignored when module disabled |
AspectPresenceFilter |
HasIslamicAspect / HasTechAspect flags | Navigation property null check |
EventCustomPropertyProjectionFilter |
Projection-backed custom property discovery/filtering | Projection query composed alongside typed filters |
Tags and categories support tri-state AND/OR filtering:
- Include AND: all specified tags must be present.
- Include OR: any specified tag matches.
- Exclude AND: exclude only if ALL specified tags present.
- Exclude OR: exclude if ANY specified tag present.
Implemented as separate EventSubqueryFilterType enum values.
Event metadata stored as JSONB supports two filter types:
JsonContains— PostgreSQL@>operator for value matching.JsonKeyExists— PostgreSQL?operator for key existence check.
EventQuerySpecification.ToCacheKeySuffix() deterministically serializes all active filters and sorts into a cache key suffix for HybridCache integration.
Standard pagination via PaginatedResult<T>:
| Parameter | Default | Max | Description |
|---|---|---|---|
pageNumber |
1 | — | Current page (1-based) |
pageSize |
20 | 100 | Items per page |
PaginatedResult.NormalizeParameters() clamps values to valid ranges. Response includes TotalCount, PageNumber, PageSize, TotalPages, HasPrevious, HasNext.
- Create/update flows return
BaseCommandResponse<Guid>withSuccess,Message,Errors,Id. - Many delete flows return
booland map to204 NoContentor404 NotFound. - Explicit purge flows return
BaseCommandResponse<CustomPropertyPurgeResultDto>and are admin-only operations that hard-delete only dependency-free custom-property definitions. - Query flows return DTOs or
PaginatedResult<TDto>wrappers. - All responses wrapped in HAL format by default.
BaseCommandResponse<T>is a success body only. A failed command never serializes as the command response; it becomes RFC 7807 ProblemDetails. Do not declare[ProducesResponseType(typeof(BaseCommandResponse<T>), …)]for a 4xx or 5xx status.
Not every failure is an exception. A command handler that returns Success = false with a FailureCode
produces a ProblemDetails response too, through one of two API-layer authorities:
| Authority | Use |
|---|---|
CommandFailurePolicy |
A capability with its own failure vocabulary. Declare the table once as a static readonly field — ValidatedBy(validation).NotFound(descriptor, codes…).Conflict(title, detail, codes…) — then call Policy.Map(this, response). Rules match in declaration order; policies are immutable and compose, so a variant is the base policy plus one rule. |
MapCommandResponse |
A capability using the shared FailureCodes vocabulary: not_found → 404, admin_required → 403, authentication_required → 401, concurrency_conflict → 409, anything else → 400 ValidationProblemDetails. |
An unmatched failure code falls through to a validation problem rather than collapsing into an untyped 400,
so distinct failures stay distinguishable to clients. Writing a private switch over FailureCode in a
controller is forbidden — that is how two endpoints in the same product came to fail in two different formats.
Exception handling uses .NET 8+ IExceptionHandler chain (not middleware):
ValidationExceptionHandler— CatchesFluentValidation.ValidationExceptionandApplication.Exceptions.ValidationException. Returns400 Bad Requestwith structured errors dictionary.GlobalExceptionHandler— Catches everything else:BadRequestException→400NotFoundException→404AuthorizationException→403QuotaExceededException→422with type/problems/quota_exceededConcurrencyConflictException→409with type/problems/concurrent_updateor/problems/stale_sync_base- Unhandled →
500(detail hidden in production)
All responses use RFC 7807 ProblemDetails with extensions:
traceId— fromHttpContext.TraceIdentifiertimestamp— UTC ISO 8601correlationId— fromX-Correlation-ID/X-Request-IDheader or generated UUID
Validation payloads normalize serializer/model-binding paths before returning
them to callers. JSON-path style keys such as $, $._links, or other
serializer internals are reported as body with a generic invalid-body message
so API responses do not leak parser implementation details or unsupported-field
paths. Unsupported media type responses use 415 ProblemDetails with a stable
title and detail.
Custom-property quota failures use stable extensions code, quotaKey, limit,
scope, and optional actual/attempted. The generic API mapper intentionally
does not emit tenantId; tenant identifiers are only safe on explicitly
authorized/admin surfaces.
Template-sync conflicts keep business stale-base conflicts distinct from technical optimistic concurrency:
| Code | HTTP | Problem type | Meaning |
|---|---|---|---|
concurrent_update |
409 | /problems/concurrent_update |
A mutable row changed since the client loaded it. Reload and retry. |
stale_sync_base |
409 | /problems/stale_sync_base |
The template sync base version changed. Recompute the diff before applying. |
The type field uses IANA RFC 9110 standard URIs (e.g., https://tools.ietf.org/html/rfc9110#section-15.5.5 for 404) instead of httpstatuses.com.
Current implementation detail: ExceptionHandlingExtensions writes traceId from HttpContext.TraceIdentifier.
.NET 10 note: handled exceptions can suppress diagnostics by default once an IExceptionHandler returns true. UseApiExceptionHandling() currently calls plain app.UseExceptionHandler() with no SuppressDiagnosticsCallback override, so treat handled-exception logging/metrics behavior as an explicit runtime decision.
Five policies configured in Program.cs:
| Policy | Origins | Methods | Credentials | Use Case |
|---|---|---|---|---|
InternalAppPolicy |
Configurable | All | Yes | Internal app communication |
ExternalAppPolicy |
Configurable | Specific set | No | External API consumers |
InternalWebsitePolicy |
Configurable (loaded from CorsSettings:AllowedOrigins) |
All | Yes | Internal website |
ExternalWebsitePolicy |
Configurable | GET, OPTIONS only |
No | External read-only |
DevPolicy |
All origins | All | Yes | Development only |
Returns 404 Not Found in single-tenant mode with hiding enabled. Conceals multi-tenant endpoints from discovery.
Returns 403 Forbidden with error payload when endpoint requires multi-tenant mode.
Gates onboarding endpoints behind the setup secret:
- If setup mode is inactive: returns RFC 7807
410 Gonewith codesetup_already_completed. - If
X-Setup-Secretheader is missing/invalid: returns RFC 7807403 Forbiddenwith codeforbidden. - Setup-secret-gated onboarding endpoints use the named
SetupSecretrate-limit policy and advertise429 Too Many RequestsasProblemDetailsin OpenAPI. - Uses
TypeFilterAttributepattern for DI-aware filtering withISetupSecretProvider.
| Decorator | Purpose |
|---|---|
PerformanceCommandHandlerDecorator / PerformanceQueryHandlerDecorator |
Logs requests taking >500ms as warnings |
AuthorizationCommandHandlerDecorator / AuthorizationQueryHandlerDecorator |
Checks IAuthorizedRequest / [AuthorizeResource] attribute; throws AuthorizationException on deny. Reflection results cached via ConcurrentDictionary. Emits OpenTelemetry activity spans on Explore.Authorization source with resource.kind, resource.action, and request.type tags. |
| Endpoint | Contract |
|---|---|
GET /api/event |
Anonymous HAL collection of EventDiscoveryItemDto. Each item is either the existing local EventListDto projection or a bounded FederatedEventDto; the federated projection does not return raw provider payloads, credentials, DIDs, record keys, or external source URLs. |
GET /api/event/federated/{atprotoRecordId}/source |
Anonymous, globally rate-limited 302 to the current tenant-visible normalized HTTPS source. Disabled capability, missing/tombstoned/cross-tenant records, and unsafe targets all return 404. |
GET /api/settings/instance/atproto-federation; keyed /api/settings/instance/atproto-federation/{key} and /api/settings/instance/atproto-federation/{key}/lock mutations |
Instance-admin HAL surface for the exact capability and validation-profile keys. Update and lock affordances are server-produced. |
GET /api/actor, GET /api/actor/{id}, GET /api/actor/by-did/{did} |
Anonymous authoritative global Actor reads. Responses omit tenant participation IDs, private User ownership, and tenant storage-object identifiers. |
GET /api/actor/by-tenant/{tenantId} |
Anonymous tenant-contextual Actor collection containing only locally discoverable Actors, with safe approved participation overrides and tenant-local subscription affordances. |
GET /api/actor/by-tenant/{tenantId}/{id} |
Anonymous exact tenant-contextual Actor detail. Hidden or cross-tenant targets return 404; safe approved participation overrides and tenant-local HAL affordances are composed server-side. |
POST /api/actor/{actorId}/moderation/suspend |
Suspend the global Actor. The body contains only reasonCode; the route selects Suspend. |
POST /api/actor/{actorId}/moderation/reinstate |
Reinstate the global Actor. The body contains only reasonCode; the route selects Reinstate. |
POST /api/actor/atproto-identities/{identityId}/moderation/suspend |
Suspend one exact global ATProto identity credential. The body contains only reasonCode; the route selects Suspend. |
POST /api/actor/atproto-identities/{identityId}/moderation/reinstate |
Reinstate one exact global ATProto identity credential without changing IsActive. The body contains only reasonCode; the route selects Reinstate. |
GET /api/organizations/{organizationId}/legitimacy-evidence |
Authenticated HAL collection of safe retained evidence metadata for the current tenant participation. |
POST /api/organizations/{organizationId}/legitimacy-evidence/upload-session |
Organization-admin creation of a server-bound, private PDF Document upload session owned by the pending participation. |
POST /api/organizations/{organizationId}/legitimacy-evidence |
Attach the finalized eligible Document as retained participation evidence. |
POST /api/organizations/{organizationId}/legitimacy-evidence/{evidenceId}/review |
Tenant-admin approve/reject decision for one pending evidence row; this does not approve the participation. |
federation.atproto_events_enabled is the single capability for tenant presentation of inbound community events and eligible outbound event/RSVP enqueue. federation.atproto_event_validation_profile=community_lexicon relaxes only the required local business fields for publication; it does not relax supplied-value validation, authorization, privacy, projection completeness, or record validation. Outbound publication additionally requires the owner's self-scoped federation.atproto_publish_my_events consent and one exact linked encrypted ATProto session.
The four moderation POST routes are authenticated instance operations. Their commands authorize update of the instance setting resource global-actor-moderation, then handlers recheck instance-admin authority before target lookup. A tenant administrator cannot mutate either global state. Same-state retries return success without another aggregate update or moderation record. Every accepted request invalidates HybridCache Event tags and output-cache discovery, detail, home, and sitemap tags.
Event publication is database-first: the committed local lifecycle mutation and immutable PdsSyncOutbox intent share one transaction, and CarpaNet PDS I/O occurs later under a fenced worker claim. Every eligible public event/session/aspect/resolved-lookup/EAV value must be mapped natively or rendered into the one community event description; coverage, privacy, JSON/DAG-CBOR size, or validation failure prevents enqueue, with no truncation or silent omission. RSVP egress represents only a committed active registration as community.lexicon.calendar.rsvp#going, ignores organizer approval state, and remains blocked until the event's settled URI/CID can form the exact strongRef.
Ingress uses one globally leased Jetstream consumer for exactly community.lexicon.calendar.event and community.lexicon.calendar.rsvp. Authoritative DID/collection/record-key state, current source version, typed event projection, tenant presentation, quarantine/tombstone effects, and cursor advancement are persisted atomically. Public clients must treat HAL links as action authority: federated items have no write affordances, and source exists only when the server can safely resolve the internal redirect route.
Public Event reads require a published, public, non-deleted Event and active Actor. Local User Events additionally require an active TenantUser; local Organization and Group Events require approved, visible, unsuspended participation, without rechecking organizer eligibility. Inbound federated Events instead require the current visible tenant presentation, non-tombstoned record, and exact active DID identity owned by the Actor. Anonymous child reads inherit the same parent gate. Authorized management detail remains available through view-management when public eligibility fails, and HAL omits public affordances from that management representation.
Inbound projection discovery keeps public Draft, Cancelled, and Completed projections, deduplicates Published projections to the local Event branch, and hides Moderated, Archived, deleted, non-public, tombstoned, stale-presentation, or identity-ineligible projections. The exact source redirect applies the same base gate. Outbound planning skips an ungrounded ineligible Create, converts a grounded ineligible Update to a fenced Delete, and rechecks identity, session, source version, ownership, record key, and CID fences at delivery. RSVP behavior is unchanged.
The removed raw /api/atprotorecord, /api/indexeddid, /api/userexternallogin, /api/actorkeystore, and /api/syncstate surfaces have no compatibility aliases. Clients cannot assert provider, DID, PDS, key, tenant, user, encrypted signing material, or ingestion cursor state through generic CRUD. Authenticated session-metadata reads and idempotent local session deletion remain credential-free; verified authentication, fenced Jetstream ingestion, and other dedicated federation internals are the only authorities over linked identity and provider-owned state. The checked-in OpenAPI contract and API Contract Inventory are the route/schema authority.
Generic Actor POST, PATCH, and DELETE routes are also absent. Actor creation, promotion, identity linking, and retirement are dedicated verified onboarding, federation, moderation, or consolidation workflows rather than browser-authored identity CRUD.
Every event has a typed EventParticipationConfiguration read projection with normalized handling-mode, advance-registration-obligation, and optional identity-access lookup facts plus guest-recovery policy and its own concurrency stamp. The former isRegistrationRequired and externalRegistrationUrl fields do not exist.
Organizers update this isolated resource through authenticated PATCH /api/events/{eventId}/participation with the configuration concurrency stamp as one required strong quoted GUID entity tag in If-Match. Authorization uses the organizer/assignment-only manage-registrations event action; listing contributors, tenant administrators, and instance administrators receive no automatic authority. Clients discover the capability through configure-participation rather than inspecting roles or claims.
Public participation is HAL-authored and fail-closed for published public events only. INFORMATION_ONLY and WALK_IN emit no participation CTA. EXTERNAL_MANAGED emits at most one external-registration relation selected from reviewed active stored public actions and routed through the stored-ID redirect. PLATFORM_MANAGED emits permission-bound start-registration for authenticated callers and sign-in-to-register for anonymous callers; both target the existing protected native registration operation. Clients must not render a CTA from mode fields or raw URLs. External labels distinguish an unverified source (View original event page) from organizer authority (Register on organizer website).
The anonymous redirect records only the bounded explore.event_public_actions.engagements metric dimensions action_kind, surface, and outcome=redirect_issued. It creates no engagement row, captures no identity or event/action identifier in labels, and never claims that a registration completed.
Generic event-registration reads are authenticated self-service contracts. /api/eventregistration, /api/eventregistration/by-session/{eventSessionId}, /api/eventregistration/by-user/{userId}, and /api/eventregistration/{id} return only registrations owned by the authenticated user; cross-user route IDs fail with 403 Forbidden. EventRegistrationDto and EventRegistrationListDto intentionally omit serialized user identity fields (userId, userFullName, userEmail). Organizer attendee-management views require a separate resource-authorized projection instead of these generic routes.
Meter name: Explore.Business. Tags vary by counter and include dimensions such as tenant_id, event_type, resource, action, result, and owner_type where those dimensions apply.
| Counter | Description |
|---|---|
explore.events.created |
Events created |
explore.events.published |
Events published |
explore.registrations.created |
Event registrations |
explore.event_public_actions.engagements |
Stored public-action redirects issued, using only bounded action_kind, surface, and outcome tags |
explore.organizations.created |
Organizations created |
explore.authorization.decisions |
Authorization check outcomes |
explore.external_api_keys.created |
External API keys created |
explore.external_api_keys.revoked |
External API keys revoked |
explore.external_api_keys.authentication_attempts |
External API-key authentication attempts with bounded outcome/tenant_id/owner_type tags only |
explore.external_api_keys.throttled |
External API-key throttling events |
explore.external_api_keys.policy_updated |
External API-key policy updates |
explore.external_api_keys.rotated |
External API-key rotations |
explore.idempotency.cleanup_runs |
Expired idempotency cleanup attempts by bounded mode and outcome tags |
explore.idempotency.cleanup_rows |
Expired idempotency cleanup eligible/deleted row counts by bounded mode and outcome tags |
explore.notifications.fanout_runs |
Notification fanout run outcomes by bounded tenant_id, fanout_kind, and outcome tags |
explore.notifications.fanout_subscribers |
Aggregate notification fanout subscriber decisions by bounded tenant_id, fanout_kind, and outcome tags |
explore.event_reports.submissions |
Event-report intake outcomes by bounded tenant_id, outcome, and failure_category tags |
explore.event_reports.workflow_actions |
Moderation report triage/assign/decide/execute outcomes by bounded tenant_id, action, outcome, and failure_category tags |
explore.event_reports.provider_syncs |
Osprey/Coop provider sync outcomes by bounded tenant_id, provider, outcome, and failure_category tags |
explore.event_reports.provider_callbacks |
Moderation provider callback outcomes by bounded tenant_id, provider, outcome, and failure_category tags |
event_role_assignment.changed |
Event role assignment changes |
Authorization decisions are also traced via ActivitySource named Explore.Authorization with resource.kind, resource.action, and request.type tags.
- Polls
outbox_messagestable for pending events at configurable interval (default 5s). - Processes in batches (default 100) with optimistic locking (
TryMarkAsProcessing). - Dispatches via
IOutboxMessageDispatcher; current routing is handled byCompositeOutboxMessageDispatcher, which sendsEventPublishedNotificationFanoutRequestedto internal notification fanout and fails closed for retired external broker event types. - Exponential backoff retry:
InitialRetryDelaySeconds × 2^retryCount, capped atMaxRetryDelaySeconds. - Dead-letters messages after
MaxRetryCountexhausted. - Configuration section:
OutboxProcessor(Enabled, PollingIntervalSeconds, BatchSize, MaxRetryCount, InitialRetryDelaySeconds, MaxRetryDelaySeconds, VerboseLogging).
GET /api/notification/streamis an authenticatedtext/event-streamendpoint for one-way notification refresh hints.- The stream emits
notification-refreshevents with minimal unread-count state only; notification bodies, entity IDs, user IDs, deduplication keys, and PII are not sent through SSE. - The endpoint disables request timeout, sends no-store/no-cache headers, and sets
X-Accel-Buffering: no. Do not addtext/event-streamto response compression or proxy buffering rules. - Existing notification list/detail/unread APIs remain the source of truth. Blazor keeps polling as fallback if the SSE stream disconnects or is unavailable.
- AI assistant run progress uses authenticated polling as the supported transport.
POST /api/ai/assistant/conversations/{conversationId}/messagesreturns202 Acceptedwith aLocationroute toGET /api/ai/assistant/conversations/{conversationId}/runs/{runId}. - Send-message requests accept
mode: "ask" | "build".askis text-only and disables action schemas/tool proposals;buildpermits proposal-only actions such as event-draft creation, still requiring HAL affordance checks and explicit user confirmation before any write side effect runs. - The run-status response is a HAL resource. It includes
selfand conversationuplinks, pluscancel-runonly while the run is queued or in progress. - Streaming is intentionally not part of the current AI assistant contract.
ai_assistant.streaming_enabledremains disabled until a separate hardening slice covers transport buffering, cancellation, timeout behavior, authentication, logs, and non-streaming fallback. - Polling responses must remain safe metadata only: status, provider label, model label, timestamps, bounded failure code/message, and HAL links. Do not return prompt content, provider response bodies, tool payloads, provider request IDs tied to content, endpoint URLs, API keys, or raw provider exceptions.
- Polls
PdsSyncOutboxfor committed, due AT Protocol event/RSVP delivery intents and reconciles missing active RSVP intents in bounded pages. - Claims rows with owner, token, monotonic fence, and expiring lease so crashed workers are reclaimable and stale workers cannot settle or fail a successor claim.
- Rechecks effective capability, the owner's current self-consent, exact linked DID/session, source version, public-location privacy, and immutable payload immediately before CarpaNet PDS I/O.
- Retries the same stable record key with bounded exponential backoff; permanent or exhausted failures are dead-lettered with a stable failure code, never a provider response body.
- Settles URI/CID, primary record ownership/presentation, outbox completion, and the local Event's
AtprotoRecordIdtransactionally. RSVP claims remain blocked until the event URI/CID strong reference exists.
- Grace period: 25 seconds on
SIGTERM. - Health checks return
503during shutdown for load balancer draining. - Uses cooperative cancellation via
app.Lifetime.StopApplication().Console.CancelKeyPresssetsisShuttingDownflag and triggers graceful stop. Kestrel.KeepAliveTimeout: 30 seconds.Host.ShutdownTimeout: 30 seconds.
Write operations support the Idempotency-Key HTTP header for safe retries:
- Client sends
Idempotency-Key: <UUID>on POST/PUT/PATCH/DELETE requests. - Every endpoint marked
RequireIdempotencyKeypublishes a required, non-nullable OpenAPI header parameter; generated clients therefore require the argument rather than relying on an implicit hook. - Server atomically persists an in-progress claim by
(Key, TenantId)in PostgreSQL before dispatching the write. - Duplicate requests within 24 hours replay the cached response with original status code when the original response was persisted.
- Ordinary fixed Event and EventSession lifecycle commands also treat a request for the current target status as a successful no-op: no lifecycle state, audit timestamp, or downstream side effect is changed. HAL does not advertise these same-target actions. Already-Moderated light moderation may instead repair missing child-session moderation; only actual repair effects run, without duplicating the moderation record, outbox, or federation work.
- Reusing the same key for a different write request is rejected with
409 Conflict. The request identity includes method, normalized target, content type, request-body hash, and a principal fingerprint. - Capability-scoped payment writes hash the capability into that fingerprint. A matching completed key re-executes the current access guard instead of blindly replaying, so capability revocation or order expiry returns the same uniform
404; durable payment claim/retry state keeps the repeated valid operation idempotent. - A matching request while the original claim is in progress receives
409 Conflictwith codeidempotency_request_in_progress; it must retry for the completed replay. - Required claim or result persistence failures return
503 Service Unavailablewith codeidempotency_unavailable, never a successful write response. - Persisted responses must have status
200through499, body size at or below 1 MB, and blank,application/json, orapplication/problem+jsoncontent type. 5xx, large, or non-JSON responses are not persisted for replay.- Keys expire after 24 hours for replay eligibility. Expired rows are ignored by reads; the
idempotency-cleanupQuartz job physically deletes expired rows after the configuredIdempotencyCleanup:ExpirationGraceHourssafety buffer. - Entity:
IdempotencyRecordwithKey,TenantId, request fingerprint fields,StatusCode,ResponseBody,CreatedAt, andExpiresAt.
The seven /api/instance/keycloak operation routes are explicitly excluded from
this mechanism. In particular, inspection and apply can carry ephemeral
administrator credentials, and receipt state rather than a replayed HTTP response
is the only supported repeat-operation record.
- Algorithms: Brotli + Gzip at
CompressionLevel.Fastest. - Enabled for HTTPS.
- Additional MIME types:
application/json,application/hal+json.
- Tenant context is resolved per request.
- Resolution behavior:
SingleTenant: default tenant is bound immediately.MultiTenant:ApiTenantResolutionMiddlewareresolves trustedX-Tenant-Slugfirst, then normalizedRequest.Host.Hostafter forwarded-header processing; unresolved non-API-key requests fail closed with404.- API-key requests may carry a requested tenant hint through pre-auth middleware and are finalized by
ApiTenantPostAuthenticationMiddleware, which can return404 Tenant mismatch,404 tenant_required, or401 API key authentication failed.
- EF query filters enforce tenant scoping in persistence.
- Hierarchical Settings: Governance settings follow a 5-tier resolution cascade: User → Group → Organization → Tenant → Instance. Resolution is performed in batch via
HierarchicalSettingsResolverwith support for instance-level locks and single-tenant bypass.
- Core events:
GET /api/event— list with full specification pattern filteringGET /api/event/{id}— detail with HATEOAS linksGET /api/event/public/{slugCode}/og-image— anonymous same-origin 1200x630 PNG for eligible published/public events; rechecks eligibility on every request, returns a strong quoted ETag, varies byHostandX-Tenant-Slug, and returns304 Not Modifiedfor a matchingIf-None-Matchwithout shared output cachingGET /api/event/{id}/management-detail— authenticated management detail, including draft/internal/moderated events visible to the principal through eventview-managementGET /api/event/{id}/moderation/history— authenticated safe moderation audit history for authorized management viewsGET /api/event/management/by-actor/{actorId}— authenticated actor-owned management list, including hidden rows authorized by per-eventview-managementGET /api/event/{id}/publish-readiness— authenticated/resource-authorized publish readiness diagnosticsPOST /api/event— createPOST /api/event/import— authenticated import/backfill create with provenancePOST /api/event/with-sessions— create with sessions in one requestPOST /api/event/{id}/publish— publish after readiness and concurrency validationPOST /api/event/{id}/archive— archive after concurrency validationPOST /api/event/{id}/cancel— cancel after concurrency validationPOST /api/event/{id}/moderation/light— light moderation; exposed by HAL relationmoderate-lightwhen the caller has moderation authorityPOST /api/event/{id}/moderation/heavy— irreversible heavy redaction; exposed by HAL relationmoderate-heavyonly after backend redaction, storage-deletion retry, and generic notification safety are availablePOST /api/event/{id}/moderation/unmoderate— restore a reversibly light-moderated event toPublished; exposed by HAL relationunmoderateonly when the latest moderation record allows unmoderation
- Event sessions and program items:
GET /api/eventsession/GET /api/eventsession/{id}/GET /api/eventsession/by-event/{eventId}— anonymous public session reads; only scheduled, published sessions under public published events are returnedGET /api/eventsession/management/by-event/{eventId}— authenticated management read that can return draft/internal sessions when authorizedPOST /api/eventsession/drafts— create an unscheduled draft session under an eventPOST /api/eventsession/{id}/schedule— assign a real schedule and local projections after concurrency validationPOST /api/eventsession/{id}/publish— publish a scheduled session after parent-event/readiness checksPOST /api/eventsession/{id}/cancel— cancel a draft/submitted/review/approved/published session after concurrency and parent-event lifecycle validationPOST /api/eventsession/{id}/complete— complete a published session after confirming the parent event is still publishedPOST /api/eventsession/{id}/archive— archive a draft, cancelled, or completed session after concurrency and parent-event lifecycle validationEventSessionDtoandEventSessionListDtoexposeparentEventStatusId, nullablestartTime,endTime, and local projection fields. Lifecycle_linksare computed from the current session status, parent Event status, and full schedule shape; clients must not infer actions from those fields. UseconcurrencyStampwhen following advertised writes.
- Aspect endpoints:
- Islamic:
GET /api/event/{id}/aspects/islamic(GetEventIslamicAspect),POST(CreateEventIslamicAspect), groupedPATCH(UpdateEventIslamicAspect), andDELETE(DeleteEventIslamicAspect). - Tech:
GET /api/event/{id}/aspects/tech(GetEventTechAspect),POST(CreateEventTechAspect), groupedPATCH(UpdateEventTechAspect), andDELETE(DeleteEventTechAspect).
- Islamic:
- Module governance:
/api/module/*(available,enabled,enable,disable,schema)
- Public experience:
GET /api/publicexperience/settingsPOST /api/a/t— anonymous-safe analytics relay for relay transport mode
- Federation:
GET /api/event— anonymous typed local/federated event discovery; federated items appear only for an effectively enabled tenant and are de-duplicated against local ATProto ownership.- Accepted inbound community events are imported internally through
ImportAtprotoFederatedEventCommandinto tenant-localEventandEventSessionrows. There is no public ATProto-import endpoint; the normal event/session read APIs expose the mapped aggregates, while the primary record retains the complete accepted source JSON. GET /api/event/federated/{atprotoRecordId}/source— anonymous, rate-limited redirect to the currently tenant-visible bounded HTTPS source. Clients render this action only from the item-levelsourceHAL relation.GET /api/event/my— authenticated local event list with optionalatprotoDeliveryStatusand stableatprotoDeliveryFailureCode; no provider body is returned.GET|PUT|DELETE /api/settings/instance/atproto-federation...— instance administrator read/update/reset and lock/unlock operations for the two administrator ATProto federation settings, with HAL as action authority./api/indexeddid/*— DID indexing metadata under existing authorization./api/auth/atproto/*— server-private bootstrap/current/refresh/revoke bridge, excluded from public OpenAPI and generated browser clients.- No
/api/atprotorecordCRUD/read contract exists. Lifecycle-owned outbox delivery and authoritative Jetstream ingestion are the onlyAtprotoRecordwrite authorities.
- Notifications (all
[Authorize]): -GET /api/notification— paginated list with?isRead=and?notificationTypeId=filtersGET /api/notification/{id}— detailGET /api/notification/unread-count— unread count (partial index optimized)GET /api/notification/stream— SSE unread-count refresh hints (text/event-stream)PATCH /api/notification/{id}/read— mark single as read (idempotent)POST /api/notification/read-all— bulk mark all as read (YouTube-style, timestamp cutoff)DELETE /api/notification/{id}— soft deleteGET /api/notification/preferences/me— current user's HAL notification preference matrixPATCH /api/notification/preferences/me— patch supplied current-user preference cellsPUT /api/notification/preferences/me/mute— set current-user non-essential notification mute stateGET|PATCH /api/organization/{id}/notification-preferencesplusPUT .../mute— organization-scoped notification preferencesGET|PATCH /api/group/{id}/notification-preferencesplusPUT .../mute— group-scoped notification preferencesGET /api/notification/web-push/config— public enabled flag and VAPID public key onlyGET /api/notification/web-push/subscription?deviceIdentifier=...— safe current-device subscription statusPOST /api/notification/web-push/subscriptions— enroll or refresh the current browserDELETE /api/notification/web-push/subscriptions/{subscriptionId}— deactivate an owned browser subscription
- Actor subscriptions (all
[Authorize]):GET /api/actor-subscriptions— current user's paged actor subscriptionsGET /api/actor-subscriptions/actors/{targetActorId}— current user's subscription state for a target actorPOST /api/actor-subscriptions— subscribe to an organization/group actorPATCH /api/actor-subscriptions/actors/{targetActorId}/notification-level— patch the route-owned subscription notification-level group with a concurrency stampDELETE /api/actor-subscriptions/actors/{targetActorId}— unsubscribe with concurrency stamp
- Footer management:
GET /api/footer/config: public footer config (AllowAnonymous)GET /api/footer/settings: authenticated scalar settings, typed social links, governance locks, and HALedit/manage-link-groupscapabilitiesPATCH /api/footer/settings: presence-awaregeneral,template,description,socialLinks, andcopyrightgroups; omitted leaves and instance-locked scalar leaves are preservedGET /api/footer/link-groups: list link groups (Authorize)GET /api/footer/link-groups/{id}: link group detail (Authorize)POST /api/footer/link-groups: create link group; requires authenticated tenant update authorizationPATCH /api/footer/link-groups/{id}: update supplied link-group fields; requires authenticated tenant update authorizationDELETE /api/footer/link-groups/{id}: delete link group; requires authenticated tenant update authorizationPOST /api/footer/link-groups/reorder: reorder link groups; requires authenticated tenant update authorizationPOST /api/footer/link-groups/{groupId}/links: create link in group; requires authenticated tenant update authorizationPATCH /api/footer/links/{id}: update supplied link fields; requires authenticated tenant update authorizationDELETE /api/footer/links/{id}: delete link; requires authenticated tenant update authorization- Link mutations remain explicit operations and repeat the effective link-group governance check server-side. Clients render link management only when the settings resource includes
manage-link-groups.
- Actor appearance:
- Actor entities include appearance fields (BackgroundColor, BackgroundEffect, BannerColor, BannerPictureId, BackgroundImageId) managed via actor update endpoints.
- Instance MCP governance:
GET /api/instance/settings/mcp— instance MCP runtime enablement and tenant override lock state.PUT /api/instance/settings/mcp— updatemcp.enabled,mcp.enable_legacy_sse,governance.lock_tenant_mcp, andgovernance.lock_tenant_mcp_legacy_sse. - Endpoint path and stateless mode remain startup-only and are not exposed as runtime-editable fields.
- Authenticated UI shell context:
GET /api/ui-shell/contextreturns the current tenant's server-authoritative workspace availability, organization/group publisher actors, explicitly authorized settings scopes, deployment mode, organization-centric pinned actor, and resolved navigation defaults.- The response is a plain DTO, requires authentication, sends
Cache-Control: private, no-store, and is never shared with the anonymous public-experience shell. Instance administration alone does not grant Studio or Tenant settings access.
- Building
Explore.API/Explore.API.csprojinReleaseruns ASP.NET Core build-time OpenAPI generation and refreshes the checked-inschemas/openapi_islamu-event.jsoncontract. - Contract invariant and parity tests assert the runtime
/openapi/islamu-event.jsonshape without writing generated files. Explore.ApiContractInventorywrites the committed endpoint inventory to API_CONTRACT_INVENTORY.md.- HAL schema transformers shape OpenAPI schemas so generated clients preserve HAL extension data.
Explore.Blazor.Client/Explore.Blazor.Client.csprojusesschemas/openapi_islamu-event.jsonas NSwag input and regeneratesExplore.Blazor.Client/Clients/EventApiClient.g.csbeforeCoreCompile.- DTO changes should follow API-first regeneration workflow (see
docs/CONTRIBUTING.md).
Public HAL detail wrappers must be registered in Explore.API/OpenApi/HalOpenApiSchemaCatalog.cs. If a new HalResourceOf*Dto wrapper is omitted, OpenAPI can emit an empty wrapper schema and generated clients lose the DTO fields even though runtime HAL responses are correct.
For contract changes, regenerate from server DTOs in this order:
dotnet build src/Explore.API/Explore.API.csproj --configuration Release --no-restore --verbosity minimal -maxcpucount:1
dotnet run --project eng/tools/Explore.ApiContractInventory/Explore.ApiContractInventory.csproj --configuration Release
dotnet msbuild src/Explore.Blazor.Client/Explore.Blazor.Client.csproj /t:GenerateApiClient /p:Configuration=Release /p:Restore=false /m:1 /v:minimalThe API build is the provenance for schemas/openapi_islamu-event.json; the named integration test generates docs/API_CONTRACT_INVENTORY.md; the NSwag target consumes that schema and generates Clients/EventApiClient.g.cs. These artifacts are never hand-edited.
The generated contract now includes ImportEvent, CreateDraftEventSession, ScheduleEventSession, PublishEventSession, CancelEventSession, CompleteEventSession, and ArchiveEventSession operations. NSwag emits nullable client properties for draft-capable session schedule fields, so callers must handle DateTimeOffset? and TimeSpan? for session schedule/local projections.
Before v1.0, intentional breaking API contract changes may be accepted when they make the API, HAL affordances, or generated OpenAPI contract cleaner. They still require an entry in API_CHANGELOG.md, regenerated OpenAPI/inventory/generated-client artifacts through the documented workflow when applicable, and retained contract-governance evidence. Do not hand-edit schemas/openapi_islamu-event.json, docs/API_CONTRACT_INVENTORY.md, or generated NSwag client output. At v1.0, breaking schema diffs become blocking per governance.
- Account buyers read the private payment projection at
GET /api/events/{eventId}/registration-orders/{orderId}/payment.refundedAmountMinorfollows exact provider-proven buyer-refund evidence even if platform-fee settlement still needs operator action; an individual operation is namedRefundedonly after both legs are exact. Requested, pending, action-required, unknown, failed, and cancelled outcomes remain distinct. POST .../{orderId}/payment/refundsis an authenticated, account-owner-authorized, rate-limited, idempotent event-cancellation refund request. Therequest-refundHAL relation is its UI authority.POST .../{orderId}/payment/material-change-choicerecords the account owner's closed-setaccept_new_termsorrequest_refundresponse. Therespond-material-changerelation appears only while a pending durable choice exists.- Studio reads
GET .../{orderId}/payment/studioand may create a bounded refund withPOST .../{orderId}/payment/studio/refundsonly throughmanage_paid_event_commerceand thecreate-refundrelation. A definitively provider-blocked settlement exposesretry-refund;POST .../{orderId}/payment/studio/refunds/{refundAttemptId}/retryrequeues that same attempt for authoritative reconciliation without creating another buyer refund. - Event-authorized campaign operations are
GET /api/events/{eventId}/refund-campaigns,GET /api/events/{eventId}/refund-campaigns/{campaignId}, and idempotentPOST .../{campaignId}/resume. Campaign resources expose only bounded progress counters, omit free-text decision reasons, and emitresume-refund-campaignonly for resumable state. Resume explicitly requeues provider-blocked attempts; automatic reconciliation never repeats a definitive provider rejection. - Every response is private/no-store. Provider account, payment/refund IDs, idempotency material, request IDs, raw provider errors, and purchaser PII are not part of the contract.
Purchase authority is exposed as two private, idempotent HAL operations:
| Caller | Route | Authority |
|---|---|---|
| Authenticated account | POST /api/events/{eventId}/registration-orders/{orderId}/purchase-authority |
Current account plus a server-verified personal/group/organization actor |
| Guest capability | POST /api/events/{eventId}/registration-orders/guest/{orderId}/purchase-authority |
Opaque order capability plus persisted verified-contact or order-scoped name-only mode |
The JSON request can select access mode and actor context where applicable. It cannot supply tenant, account, normalized-contact hash, enforcement key, quantity, policy version, or idempotency authority. Quantity comes from persisted order lines, current policy lineage is selected inside Application, and the BFF creates the durable operation key.
Both routes use write/public-transactional rate limits, require the HTTP idempotency header, return private/no-store responses, and expose stable 400/403/404/409/503 shapes. A successful HAL resource publishes supportsHardCrossOrderCeiling and enforcementScopeCode; name-only mode reports order scope rather than claiming cross-order person identity.
RegistrationOrder HAL resources publish reserve-purchase-authority only while the order is currently payable. OpenAPI and EventApiClient.g.cs are generated artifacts; the purchase request, result, enum, and flattened HalResourceOfTicketPurchaseGovernanceResource schemas are covered by generated-contract tests.
The private exact resource is GET|POST|DELETE /api/events/{eventId}/registration-orders/{registrationOrderId}/lines/{registrationOrderLineId}/waitlist. Offer acceptance and supply withdrawal are subordinate pointer routes. Reads accept the opaque registration-order capability only in X-Registration-Order-Capability; writes require authentication, Idempotency-Key, rate limiting, replay protection, and private no-store responses.
FairReturnWaitlistDto publishes only an opaque resource UUID, bounded position, bounded status/reason codes, and optional offer expiry. A position of zero means unavailable and values above 999 are capped. Server-only Can* and stop-control facts are JSON-ignored and drive HAL relations; clients must not infer actions from roles, local claims, status strings, or payment state. Unknown identities, seller conflicts, stale offers, and authority failures use the same private not-found envelope.
The Blazor BFF mirrors these routes under /bff, forwards through the generated IEventApiClient, retains cookie authority, validates antiforgery before write-rate limiting, and never accepts capabilities in query strings or response bodies.
docs/SECURITY-MODEL.md— auth, JWT, CORS, security headersdocs/ARCHITECTURE.md— Clean Architecture layers, request flowdocs/OPERATIONS.md— rate limiting config, timeouts, shutdowndocs/CODEBASE_INSIGHTS.md— non-obvious patternsdocs/MULTI_TENANCY.md— tenant resolution and isolationdocs/OUTBOX_PATTERN.md— outbox pattern implementation detailsdocs/FOOTER_MANAGEMENT.md— footer management systemdocs/CONTRIBUTING.md— development workflow