Predicates
The three predicate methods on OctetSdk.loc. All three are async (Swift) / suspend (Kotlin) and do not throw. A runtime problem produces an INDETERMINATE verdict.
isWithin
The primary predicate. Tests whether the device's provable location at atTime (default now) is inside region.
public func isWithin(
region: OctetRegion,
atTime: Date = Date(),
sessionNonce: Data? = nil,
decisionRef: String? = nil,
forceFresh: Bool = false,
content: [ContentItem] = []
) async -> OctetVerdict
suspend fun isWithin(
region: OctetRegion,
atTime: Instant = Instant.now(),
sessionNonce: ByteArray? = null,
decisionRef: String? = null,
forceFresh: Boolean = false,
content: List<ContentItem> = emptyList(),
): OctetVerdict
Example
let v = await sdk.loc.isWithin(
region: .country(isoCode: "US"),
atTime: Date()
)
if v.result == .yes { ship(v.proof!) }
val v = sdk.loc.isWithin(
region = OctetRegion.country("US"),
atTime = Instant.now()
)
if (v.result == OctetVerdict.Result.YES) ship(v.proof!!)
isOutside
Tests whether the device's provable location at atTime (default now) is outside region.
public func isOutside(
region: OctetRegion,
atTime: Date = Date(),
sessionNonce: Data? = nil,
decisionRef: String? = nil,
forceFresh: Bool = false,
content: [ContentItem] = []
) async -> OctetVerdict
suspend fun isOutside(
region: OctetRegion,
atTime: Instant = Instant.now(),
sessionNonce: ByteArray? = null,
decisionRef: String? = null,
forceFresh: Boolean = false,
content: List<ContentItem> = emptyList(),
): OctetVerdict
isOutside is a separate query, not the negation of isWithin. A YES from isOutside carries a proof that the device is outside region, and a verifier checks that proof the same way as any other. An INDETERMINATE from isWithin does not mean the device is outside. Negating it would merge two outcomes: the device is not inside region, and the SDK could not tell.
contains
Tests whether the point center is within tol meters of the device's provable location.
public func contains(
center: LatLon,
tol: Meters,
atTime: Date = Date(),
sessionNonce: Data? = nil,
decisionRef: String? = nil,
forceFresh: Bool = false,
content: [ContentItem] = []
) async -> OctetVerdict
suspend fun contains(
center: LatLon,
tol: Meters,
atTime: Instant = Instant.now(),
sessionNonce: ByteArray? = null,
decisionRef: String? = null,
forceFresh: Boolean = false,
content: List<ContentItem> = emptyList(),
): OctetVerdict
Mathematically equivalent to isWithin(region: .disc(center: center, radiusMeters: tol), atTime:). Kept as a distinct verb because the device-centric reading is the natural one for proximity use cases ("am I near the delivery dropoff", "did I arrive at the geofence").
PLAUSIBLE containment
A disc answer, from contains(...) or a disc isWithin(...), carries an assurance tier on its proof. By default a disc query answers YES or NO only from a VERIFIED proof, and otherwise returns INDETERMINATE. New in 3.0: setting OctetConfig.advanced.acceptPlausibleContainment = true lets it answer from a PLAUSIBLE proof as well. Tight tolerances pass only with a tight location fix.
let config = OctetConfig(
advanced: AdvancedConfig(acceptPlausibleContainment: true))
val config = OctetConfig(
advanced = AdvancedConfig(acceptPlausibleContainment = true))
Custom predicates
isWithin is the load-bearing primitive, and contains is a one-line wrapper over it. Custom predicates can be built the same way:
extension OctetLoc {
func isNear(_ center: LatLon, _ tol: Meters) async -> OctetVerdict {
await isWithin(region: .disc(center: center, radiusMeters: tol))
}
func isInsideAny(_ regions: [OctetRegion]) async -> OctetVerdict? {
for r in regions {
let v = await isWithin(region: r)
if v.result == .yes { return v }
}
return nil
}
}
suspend fun OctetLoc.isNear(center: LatLon, tol: Meters): OctetVerdict =
isWithin(OctetRegion.disc(center, tol))
suspend fun OctetLoc.isInsideAny(regions: List<OctetRegion>): OctetVerdict? {
for (r in regions) {
val v = isWithin(r)
if (v.result == OctetVerdict.Result.YES) return v
}
return null
}
Time semantics
All three predicates take an optional atTime. See Time Semantics for the live / historical / future regimes and the per-resolution validity windows. Default is "now". Future times beyond ±2 s come back INDETERMINATE / FUTURE_TIME.
Proof binding and freshness
All three predicates take three optional controls over which proof answers the call:
sessionNonce(opaque bytes, 1 to 512) binds the proof to one login. The SDK commits a hash of the nonce inside the signed proof, so a verifier can confirm the proof was made for that login. An empty or over-512-byte nonce returnsINDETERMINATE / INVALID_SESSION_NONCE.decisionRef(1 to 256 characters) binds the uploaded proof to one authorization decision, tagging the upload envelope withdecision_refandregion_ref. An over-256-character ref returnsINDETERMINATE / INVALID_DECISION_REF, with no network call. Passnil/null, not an empty string, when there is no decision.forceFreshbypasses the freshness buffer and mints a proof for this call, with none of the binding semantics above.
Each of sessionNonce, decisionRef, and content forces a fresh proof. Setting forceFresh as well has no additional effect.
let nonce = try await loginBackend.issueNonce()
let v = await sdk.loc.isWithin(region: .country(isoCode: "US"), sessionNonce: nonce)
if v.result == .yes { send(v.proof!, to: loginBackend) }
Proof Binding explains when to use each, and how a bound proof is verified.
Content
New in 3.1. All three predicates take an optional content list. The device signs a commitment to each item into the proof: its key, content type, size, and SHA256(salt ‖ value). Unless you mark an item for upload, the content itself stays on the device. An empty list (the default) adds nothing.
public struct ContentItem: Sendable, Equatable {
public let key: String
public let contentType: String
public let locator: String? // where you will keep the content; not signed
public var salt: Data { get } // the salt the item is committed with
public static func bytes(key: String, contentType: String, _ data: Data, salt: Data? = nil,
upload: Bool = false, locator: String? = nil) -> ContentItem
public static func file(key: String, contentType: String, _ url: URL, salt: Data? = nil,
upload: Bool = false, locator: String? = nil) -> ContentItem
public static func digest(key: String, contentType: String, _ digest: ContentDigest,
uploadFrom: URL? = nil, uploadSalt: Data? = nil,
locator: String? = nil) -> ContentItem
public static func generateSalt() -> Data
}
public struct ContentDigest: Sendable, Equatable {
public let entryDigest: Data
public let size: UInt64
public let salt: Data
}
public struct ContentError: Error, Sendable, Equatable { public let message: String }
extension Octet {
public static func hashContent(file: URL, salt: Data? = nil) async throws -> ContentDigest
}
class ContentItem {
val key: String
val contentType: String
val locator: String? // where you will keep the content; not signed
val salt: ByteArray // the salt the item is committed with
companion object {
fun bytes(key: String, contentType: String, data: ByteArray, salt: ByteArray? = null,
upload: Boolean = false, locator: String? = null): ContentItem
fun file(key: String, contentType: String, file: File, salt: ByteArray? = null,
upload: Boolean = false, locator: String? = null): ContentItem
fun digest(key: String, contentType: String, digest: ContentDigest,
uploadFrom: File? = null, uploadSalt: ByteArray? = null,
locator: String? = null): ContentItem
fun generateSalt(): ByteArray
}
}
class ContentDigest(entryDigest: ByteArray, val size: Long, val salt: ByteArray)
class ContentError(message: String) : Exception(message)
// on Octet:
suspend fun hashContent(file: File, salt: ByteArray? = null): ContentDigest
byteshashes a value in memory during the call.filestreams a file during the call, up to 64 MiB.digesttakes a digest fromOctet.hashContent, which hashes a file of any size before the call.hashContentthrowsContentErrorfor a bad salt or an unreadable file.saltdefaults to a random 32-byte salt that the SDK generates. Read it back fromContentItem.salt(orContentDigest.salt) and keep it with the content. Pass an empty value to commit unsalted, or your own salt of 16 or 32 bytes.generateSalt()returns 32 random bytes.upload(oruploadFromon adigestitem) has the SDK upload the content to your own storage after the proof is made. It needs caller content uploads enabled for your license. See Uploading content.locatoris an identifier you choose for where the content will be kept, up to 256 bytes. It travels with the proof unsigned and is passed to your upload provider. Never put a credential in it.
Content that breaks a rule (1 to 16 items, unique keys of 1 to 64 bytes, a type/subtype content type with no parameters, a valid salt, a readable file of at most 64 MiB) returns INDETERMINATE / INVALID_CALLER_CONTENT with no proof. If caller content isn't enabled for your license, a query with content returns INDETERMINATE / CALLER_CONTENT_NOT_ENABLED with no proof. Without location authorization, a query with content returns INDETERMINATE / PERMISSION_REQUIRED.
Content Commitments explains what a commitment proves and how a verifier checks it.
See also
OctetRegion. The shapes you can pass.OctetVerdict. What you get back.- Proof Binding. Binding a proof to a login or a decision.
- Content Commitments. Binding content to a proof.
- Verdicts. The trichotomy and the
ReasonCodetaxonomy.