api-swift / Sources / IrdAPI / Gate / GateRules.swift
338 words
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import Foundation

/// A client-side safety net for agents and automation: domain-touching mutations
/// (POST/PUT/PATCH/DELETE) are refused before any request when the domain is on the
/// deny-list or missing from the allow-list. Reads are never gated.
public struct GateRules: Sendable, Equatable {
    /// Exact names or `*.suffix` patterns that may never be touched. Deny wins.
    public var deniedDomains: [String]?
    /// When set, only matching domains may be touched; `[]` allows nothing, nil means no allow-list.
    public var allowedDomains: [String]?

    public init(deniedDomains: [String]? = nil, allowedDomains: [String]? = nil) {
        self.deniedDomains = deniedDomains
        self.allowedDomains = allowedDomains
    }

    /// The denial for `domain`, or nil when the mutation may proceed. An unresolvable
    /// domain passes under a pure deny-list and fails closed under an allow-list.
    public func decide(_ domain: String?) -> GateDenial? {
        guard let name = normalizeDomain(domain) else {
            return allowedDomains != nil ? GateDenial(domain: nil, pattern: nil) : nil
        }
        if let denied = deniedDomains?.first(where: { patternMatches($0, name) }) {
            return GateDenial(domain: name, pattern: denied)
        }
        if let allowed = allowedDomains, !allowed.contains(where: { patternMatches($0, name) }) {
            return GateDenial(domain: name, pattern: nil)
        }
        return nil
    }
}

/// What a mutation touches: a domain name, or a registration whose domain the gate resolves.
public enum GateTarget: Sendable, Equatable {
    case domain(String?)
    case registrationId(String?)
}

public struct GateDenial: Sendable, Equatable {
    public let domain: String?
    /// The deny-list pattern that matched; nil for an allow-list miss.
    public let pattern: String?
}

/// Lower-case, trimmed, without trailing dots; nil for blank input.
public func normalizeDomain(_ domain: String?) -> String? {
    guard let domain else { return nil }
    var name = domain.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
    while name.hasSuffix(".") { name.removeLast() }
    return name.isEmpty ? nil : name
}

/// `*.example.ro` matches `example.ro` and every name under it; anything else is exact.
func patternMatches(_ pattern: String, _ name: String) -> Bool {
    guard let normalized = normalizeDomain(pattern) else { return false }
    if normalized.hasPrefix("*.") {
        let suffix = String(normalized.dropFirst(2))
        return name == suffix || name.hasSuffix("." + suffix)
    }
    return normalized == name
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/api-swift.git