Access decisions
Every call the tenant’s active policies decide returns a Decided<T> value, never an exception. A
Decided<T> is always one of three decisions: allow, deny or challenge.
Outcome and Decided<T>
Outcome is 'allow', 'deny' or 'challenge'. Decided<T> is the union of Allowed<T>,
Denied and Challenged<T>, tagged by outcome.
type Outcome = 'allow' | 'deny' | 'challenge';type Decided<T = {}> = Allowed<T> | Denied | Challenged<T>;enum Decided<T> { case allow(T, eventId: EventId) case deny(Denied) case challenge(Challenged<T>)}sealed interface Decided<out T> { data class Allow<T>(val value: T, val eventId: EventId) : Decided<T> data class Deny(val eventId: EventId, val reason: DenyReason, val text: String) : Decided<Nothing> class Challenge<T>(val eventId: EventId) : Decided<T> { suspend fun stepUp(): Decided<T> }}Every Decided<T> carries an EventId, the access event the Seald Healthcare Cloud assigned. The Seald Healthcare Cloud
records every decision: allow, deny and challenge. The record of the request and its decision
is complete whatever the decision.
Allowed<T>
On allow, the value is what you asked for, with the outcome and the event id.
type Allowed<T> = { outcome: 'allow'; eventId: EventId } & T;// Swift has no Allowed type. The allow case carries the value and the event id.case allow(T, eventId: EventId)data class Allow<T>(val value: T, val eventId: EventId) : Decided<T>Denied
On deny, you get a fixed reason code and the words a person should see. You never get a policy
or a rule.
interface Denied { outcome: 'deny'; eventId: EventId; reason: DenyReason; text: string;}struct Denied { let eventId: EventId let reason: DenyReason let text: String}data class Deny(val eventId: EventId, val reason: DenyReason, val text: String) : Decided<Nothing>The fixed list of DenyReason values is on Deny reasons.
Challenged<T>
On challenge, the tenant wants a fresh multi-factor sign-in before it decides. Call stepUp(). It
runs the sign-in adapter and returns a second Decided<T> for the same request, with its own event
id.
interface Challenged<T> { outcome: 'challenge'; eventId: EventId; stepUp(): Promise<Decided<T>>;}struct Challenged<T> { let eventId: EventId func stepUp() async throws -> Decided<T>}class Challenge<T>(val eventId: EventId) : Decided<T> { suspend fun stepUp(): Decided<T>}A stepUp() can itself resolve to another challenge. Handle it recursively, as the allowed
helper below does. This covers every case without special handling for a second round.
SealdHealthcareError
A decision is never an error. An error is a fault or a state your application must handle:
- the device is locked
- no session is open
- the device was revoked
- the release is below the version floor
- the Seald Healthcare Cloud is unreachable
- a stored object or a device certificate (card) failed its check
SealdHealthcareError carries one fixed code and a retryable flag. retryable says whether the same call
may succeed later with no change on your side.
interface SealdHealthcareError extends Error { code: ErrorCode; retryable: boolean;}enum SealdHealthcareError: Error { case locked case noSession // one case per code var retryable: Bool { get }}class SealdHealthcareException(val code: ErrorCode, val retryable: Boolean) : Exception()The full list of codes is on Errors.
A decision, not an error
A Decided<T> and a SealdHealthcareError answer different questions. A Decided<T> says whether the
tenant’s active policies let this person do this thing, on this device, now. It always gives a
meaningful answer. A SealdHealthcareError says that the SDK could not ask. For example, the device is
locked, the network is gone or the release is too old. Handle a Decided<T> by outcome. Handle a
SealdHealthcareError by code.
The allowed helper
Most application code needs only the value on allow. The allowed helper reduces a Decided<T>
to that value. It follows a challenge until it resolves and shows the reason on a deny.
async function allowed<T>(decided: Decided<T>): Promise<Allowed<T> | undefined> { if (decided.outcome === 'challenge') return allowed(await decided.stepUp()); if (decided.outcome === 'deny') { showReason(decided.reason, decided.text); return undefined; } return decided;}func allowed<T>(_ decided: Decided<T>) async throws -> T? { switch decided { case .allow(let value, _): return value case .deny(let denied): showReason(denied.reason, denied.text); return nil case .challenge(let challenge): return try await allowed(challenge.stepUp()) }}suspend fun <T> allowed(decided: Decided<T>): T? = when (decided) { is Decided.Allow -> decided.value is Decided.Deny -> { showReason(decided.reason, decided.text); null } is Decided.Challenge -> allowed(decided.stepUp())}The guides use allowed to keep examples short. You can handle the three decisions by hand
instead. Quickstart does this once, to show what allowed does.
Next
- Deny reasons for the fixed list
DenyReasondraws from. - Errors for the fixed list
ErrorCodedraws from. - Plaintext lifetime for what happens to the value an
allowhands you. - Open records and files to see
Decided<Opened>in a full flow.