import Foundation
/// The gate's state: its rules and the registration id → domain index used to
/// resolve mutations that address a domain by registration id.
public actor DomainGate {
public nonisolated let rules: GateRules
private var index: [String: String]
init(rules: GateRules, index: [String: String]) {
self.rules = rules
self.index = index
}
public func cachedDomain(for registrationId: String) -> String? { index[registrationId] }
public var registrationIndex: [String: String] { index }
func remember(id: String, domain: String) { index[id] = domain }
func replace(_ newIndex: [String: String]) { index = newIndex }
}
extension IrdClient {
/// Checks `target` against the gate; a no-op on ungated clients. Public so hand-written
/// code can gate custom raw calls the same way.
public func checkGate(op: String, target: GateTarget) async throws {
guard let gate else { return }
let domain: String?
switch target {
case .domain(let d): domain = d
case .registrationId(nil): domain = nil
case .registrationId(let id?): domain = await resolveRegistration(id, gate: gate)
}
if let denial = gate.rules.decide(domain) {
throw IrdError.domainDenied(domain: denial.domain, pattern: denial.pattern, op: op)
}
}
/// Cache hit, else one `GET /domains/registrations/{id}` (failures resolve to nil).
func resolveRegistration(_ id: String, gate: DomainGate) async -> String? {
if let cached = await gate.cachedDomain(for: id) { return cached }
let op = IrdOperation(method: .get, pathTemplate: "/domains/registrations/{id}", module: "registrations", name: "get", security: .keyOrJwt)
guard let (_, json) = try? await perform(op, params: ["id": id], query: [], body: nil, gated: false),
let domain = normalizeDomain(json.at(["registration", "domain"])?.stringValue) else { return nil }
await gate.remember(id: id, domain: domain)
return domain
}
/// Every registration's id → domain, walked with `limit=100` cursor pages.
func fetchRegistrationIndex() async throws -> [String: String] {
let op = IrdOperation(method: .get, pathTemplate: "/domains/registrations", module: "registrations", name: "list", security: .keyOrJwt)
let rows: [JSONValue] = try await Paginator.cursor(
fetch: { (cursor: String?) in
var query = [QueryItem("limit", "100")]
if let cursor { query.append(QueryItem("cursor", cursor)) }
return try await perform(op, params: [:], query: query, body: nil, gated: false).json
},
items: { $0["registrations"]?.arrayValue ?? [] },
next: { $0["next_cursor"]?.stringValue })
var index: [String: String] = [:]
for row in rows {
if let id = row["id"]?.stringValue, let domain = normalizeDomain(row["domain"]?.stringValue) { index[id] = domain }
}
return index
}
}