Move SSD remote config predicates to typed Kotlin

ober

f9166cd134e28c23ad0dfa0782b6cf38ece85513

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 79d7133..d35dda7 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1535,10 +1535,15 @@
                 TruthIndexEntry-sourceName TruthIndexEntry-canonicalSourceName
                 TruthIndexEntry-page TruthIndexEntry-dpi TruthIndexEntry-groupCount
                 TruthIndexEntry-truthTime betterTruthIndex
-                oldDestinationBytes urlEffectivePort)
+                fileExists fileIsRegular destinationNewFileDelta
+                oldDestinationBytes urlEffectivePort
+                remoteBearerTokenPresent remoteBearerTokenValid
+                remotePinPresent remoteRelativePathSafe)
         (type File)
         (type URL)
         (type Int32)
+        (extern (fileExistsRaw (file : File)) : Bool
+          (kotlin-member-call exists))
         (extern (fileIsFile (file : File)) : Bool
           (kotlin-member-get isFile))
         (extern (fileLength (file : File)) : Int
@@ -1572,11 +1577,30 @@
                            (TruthIndexEntry-groupCount current))
                    (> (TruthIndexEntry-truthTime candidate)
                       (TruthIndexEntry-truthTime current)))))
+        (def (fileExists (file : File)) : Bool
+          (fileExistsRaw file))
+        (def (fileIsRegular (file : File)) : Bool
+          (and (fileExists file) (fileIsFile file)))
+        (def (destinationNewFileDelta (destination : File)) : Int32
+          (if (fileExists destination) (int32 0) (int32 1)))
         (def (oldDestinationBytes (destination : File)) : Int
           (if (fileIsFile destination) (fileLength destination) (int 0)))
         (def (urlEffectivePort (url : URL)) : Int32
           (let ((port (urlPort url)))
-            (if (>= port (int32 0)) port (urlDefaultPort url))))))
+            (if (>= port (int32 0)) port (urlDefaultPort url))))
+        (def (remoteBearerTokenPresent (token : String)) : Bool
+          (not (string-blank? token)))
+        (def (remoteBearerTokenValid (token : String)) : Bool
+          (or (string-blank? token)
+              (and (<= (string-length token) 4096)
+                   (and (not (string-contains? token "\n"))
+                        (not (string-contains? token "\r"))))))
+        (def (remotePinPresent (pinText : String)) : Bool
+          (not (string-blank? pinText)))
+        (def (remoteRelativePathSafe (relative : String)) : Bool
+          (and (not (string-blank? relative))
+               (and (not (string-starts-with? relative "/"))
+                    (not (string-contains? relative "..")))))))
 
     (typed-kotlin-file "com/sfb/ssdreview/JsonGeometry.kt"
       (kotlin-imports (org json JSONArray))
@@ -7790,7 +7814,7 @@
        "        require(bytes in 0..MAX_ZIP_ENTRY_BYTES) { \"Per-file storage limit exceeded\" }"
        "        val stats = storageStats()"
        "        val oldBytes = oldDestinationBytes(destination)"
-       "        val newFiles = stats.files + if (destination.exists()) 0 else 1"
+       "        val newFiles = stats.files + destinationNewFileDelta(destination)"
        "        val newBytes = stats.bytes - oldBytes + bytes"
        "        require(newFiles <= MAX_STORAGE_FILES && newBytes <= MAX_STORAGE_BYTES) {"
        "            \"Storage quota exceeded\""
@@ -7832,12 +7856,12 @@
        "        val url = URL(endpoint.trimEnd('/'))"
        "        require(url.protocol == \"https\" && url.host.isNotBlank()) { \"HTTPS endpoint required\" }"
        "        require(url.userInfo == null && url.query == null && url.ref == null) { \"Unsafe endpoint URL\" }"
-       "        if (token.isNotBlank()) {"
-       "            require(token.length <= MAX_AUTH_TOKEN_CHARS && !token.contains('\\n') && !token.contains('\\r')) {"
+       "        if (remoteBearerTokenPresent(token)) {"
+       "            require(remoteBearerTokenValid(token)) {"
        "                \"Invalid remote authentication token\""
        "            }"
        "        }"
-       "        val pin = if (pinText.isNotBlank()) {"
+       "        val pin = if (remotePinPresent(pinText)) {"
        "            val encodedPin = pinText.removePrefix(\"sha256/\")"
        "            Base64.decode(encodedPin, Base64.NO_WRAP).also {"
        "                require(it.size == 32) { \"SPKI SHA-256 pin must decode to 32 bytes\" }"
@@ -7850,7 +7874,7 @@
        "    }"
        ""
        "    private fun remoteUrl(relative: String): URL {"
-       "        require(relative.isNotBlank() && !relative.startsWith('/') && !relative.contains(\"..\"))"
+       "        require(remoteRelativePathSafe(relative))"
        "        val config = remoteConfig ?: throw IllegalStateException(\"Remote SSD service is not locally configured\")"
        "        val candidate = URL(config.apiUrl.toString().trimEnd('/') + \"/\" + relative)"
        "        val basePort = urlEffectivePort(config.apiUrl)"