OctetVerdict
The outcome of a predicate call. Returned by isWithin, isOutside, contains on OctetSdk.loc.
Shape
public struct OctetVerdict: Sendable {
public let result: Result
public let reason: ReasonCode
public let message: String
public let proof: LocationProof?
public let validity: Interval?
public let queriedAt: Date
public let confidence: ConfidenceSummary
public let achievableLevel: ProofLevel? // non-nil only when reason == .insufficientPrecision
public enum Result: Sendable, Hashable {
case yes, no, indeterminate
}
public enum ReasonCode: Sendable, Hashable {
case ok
case noFix
case futureTime
case staleFix
case noProofAtResolution
case insufficientPrecision
case attestationFailed
case mockLocationDetected
case sdkNotRunning
case notYetReleased
case invalidSessionNonce
case invalidDecisionRef
case spoofingDetected
case tampering
case regionUnresolved
}
}
data class OctetVerdict(
val result: Result,
val reason: ReasonCode,
val message: String,
val proof: LocationProof?,
val validity: Interval?,
val queriedAt: Instant,
val confidence: ConfidenceSummary,
val achievableLevel: ProofLevel? = null, // non-null only when reason == INSUFFICIENT_PRECISION
) {
enum class Result { YES, NO, INDETERMINATE }
enum class ReasonCode {
OK, NO_FIX, FUTURE_TIME, STALE_FIX, NO_PROOF_AT_RESOLUTION,
INSUFFICIENT_PRECISION, ATTESTATION_FAILED, MOCK_LOCATION_DETECTED,
SDK_NOT_RUNNING, NOT_YET_RELEASED, INVALID_SESSION_NONCE,
INVALID_DECISION_REF, SPOOFING_DETECTED, TAMPERING, REGION_UNRESOLVED,
}
}
Field invariants
proofisnil/nulliffresult == .indeterminate/INDETERMINATE. Enforced as a precondition /requireat construction. AYESorNOalways carries a proof. AnINDETERMINATEnever does.validityis non-nil exactly whenproofis non-nil. Both move together.queriedAtis theatTimethat was passed in. Echoed back so callers do not have to thread it themselves.confidenceis always populated, even forINDETERMINATE(with an empty summary in that case).achievableLevelis non-nil only whenreason == .insufficientPrecision. It names the best proof level the SDK could reach for this query right now. New in 1.1. The SDK never silently down-levels to it on your behalf.
result. The trichotomy.
| Value | Meaning |
|---|---|
.yes / YES |
Predicate holds. Proof attached. |
.no / NO |
Predicate provably does NOT hold. Proof attached. |
.indeterminate / INDETERMINATE |
SDK cannot answer. See reason. |
Never collapse .indeterminate to .no silently. See Verdicts for the rationale.
reason. The ReasonCode.
| Code | Result | Meaning |
|---|---|---|
ok / OK |
YES or NO |
Proof covered atTime. Predicate evaluated cleanly. |
noFix / NO_FIX |
INDETERMINATE |
SDK started but no fix yet (also: running on simulator / emulator). |
futureTime / FUTURE_TIME |
INDETERMINATE |
atTime is in the future beyond ±2 s clock-skew tolerance. |
staleFix / STALE_FIX |
INDETERMINATE |
atTime falls outside the validity window of any cached proof. |
noProofAtResolution / NO_PROOF_AT_RESOLUTION |
INDETERMINATE |
Cached proof is too coarse for the query (country proof, city query). |
insufficientPrecision / INSUFFICIENT_PRECISION |
INDETERMINATE |
Conditions cannot support the requested precision. Read achievableLevel. |
attestationFailed / ATTESTATION_FAILED |
INDETERMINATE |
Device attestation did not pass. |
mockLocationDetected / MOCK_LOCATION_DETECTED |
INDETERMINATE |
OS flagged mocked location. |
sdkNotRunning / SDK_NOT_RUNNING |
INDETERMINATE |
Octet.start(...) was never called or session ended. |
notYetReleased / NOT_YET_RELEASED |
INDETERMINATE |
Predicate path declared by the API but not available in this SDK build. |
invalidSessionNonce / INVALID_SESSION_NONCE |
INDETERMINATE |
Supplied sessionNonce is empty or exceeds 512 bytes. No proof is produced. Added in 1.2. |
invalidDecisionRef / INVALID_DECISION_REF |
INDETERMINATE |
Supplied decisionRef exceeds 256 characters. No proof, no network call. Added in 2.0. |
spoofingDetected / SPOOFING_DETECTED |
INDETERMINATE |
The spoof pipeline flagged the fix. Adversarial. Added in 2.0. |
tampering / TAMPERING |
INDETERMINATE |
Device-integrity tampering (root / hook). Adversarial. Added in 2.0. |
regionUnresolved / REGION_UNRESOLVED |
INDETERMINATE |
A country or subdivision predicate could not resolve the region at the required confidence. A benign measurement gap, not a detected spoof. Added in 2.0. |
Adversarial and benign reasons
Added in 2.0, the INDETERMINATE reasons split into two classes so a policy layer can treat them differently:
- Adversarial (a detected forgery or compromise):
spoofingDetected,mockLocationDetected,tampering,attestationFailed. Deny. - Benign (the SDK could not measure):
regionUnresolved,insufficientPrecision,noFix,staleFix,futureTime. A policy layer can permit these with a step-up.
Both classes carry no proof. The distinction is in the reason, not the result.
message. For humans only.
message is a free-form string useful for log lines and debug UI. Do not parse message for control flow. Parse reason instead. The text is intentionally informal. The SDK reserves the right to refine it across versions.
proof. The cryptographic witness.
LocationProof is the signed protobuf that a verifier checks against Octet's public keys. Its full shape is part of the protocol, not the SDK's public surface. See Serialization for the v1 alpha caveat about how LocationProof is currently rendered through .toJson().
For most integrators the proof is opaque. Forward it to your verifier as-is.
validity. The proof's temporal window.
The interval over which the underlying proof answers the predicate as stated. Not "how long the verdict will stay true". See Time Semantics.
public struct Interval: Sendable, Hashable {
public let from: Date
public let to: Date
}
data class Interval(val from: Instant, val to: Instant)
Inclusive-inclusive, from <= to.
confidence. The score and flags.
ConfidenceSummary surfaces the SDK's overall confidence score and a list of flags (mock-location detected, VPN active, GNSS anomaly, and so on). A strict caller can treat a low-confidence YES as effectively INDETERMINATE:
let strictYes = verdict.result == .yes && verdict.confidence.overallScore > 0.8
val strictYes = verdict.result == OctetVerdict.Result.YES &&
verdict.confidence.overallScore > 0.8
The SDK never applies this collapse for you. The policy lives on the caller side.
See also
- Verdicts. Why the trichotomy matters.
- Time Semantics. How
validityandqueriedAtinteract. - Serialization.
.toStr()/.toJson()/.toJsonl()on verdicts.