diff --git a/Documentation/Extensions.md b/Documentation/Extensions.md index 38b23c5..ff75537 100644 --- a/Documentation/Extensions.md +++ b/Documentation/Extensions.md @@ -21,7 +21,101 @@ Every section is a collapsible block — click a heading to expand it. ## API & Networking - +`API` is an `actor` wrapping `URLSession` for typed JSON requests and multipart +uploads. This is a summary — the **[full guide is in API.md](API.md)** (all +parameters, error model, client certificates, custom body types, and the +rationale vs. a hand-rolled `URLSession`). + +
+API — typed async requests & uploads + +### `API.shared` + +The shared `actor` instance. Every call is `await`; nothing runs on the main +thread unless you hop there yourself. + +```swift +struct User: Decodable, Sendable { let id: Int; let name: String } + +let user: User = try await API.shared.request( + url: "https://api.example.com/users/{id}", + method: .get, + pathParams: ["id": "42"] +) +``` + +### `request(url:method:body:pathParams:headers:debug:timeoutInterval:networkServiceType:persistConnection:)` + +Sends a request and decodes the JSON response into `T: Decodable & Sendable`. +Non-2xx responses throw an `NSError` whose `code` is the HTTP status and whose +`localizedFailureReason` is the response body. When `T == String` the raw body +is returned without JSON decoding. + +```swift +struct CreateUser: Encodable, Sendable { let name: String } + +// JSON body +let created: User = try await API.shared.request( + url: "https://api.example.com/users", + method: .post, + body: jsonBody(CreateUser(name: "Ana")) +) + +// form-url-encoded body +let token: Token = try await API.shared.request( + url: "https://api.example.com/oauth/token", + method: .post, + body: .form(["grant_type": "password", "username": "ana"]) +) +``` + +### `upload(url:method:form:pathParams:headers:debug:timeoutInterval:networkServiceType:)` and its `onProgress:` overload + +Uploads a `multipart/form-data` body serialised to a temp file (removed +afterwards) and streamed from disk, so large files never fully load into memory. + +```swift +var form = MultipartForm() +form.field("caption", "Sunset") +form.file("photo", data: jpegData, filename: "p.jpg") +form.file("video", url: localVideoURL) // streamed from disk + +let result: UploadResult = try await API.shared.upload( + url: "https://api.example.com/media", + form: form, + onProgress: { fraction in + Task { @MainActor in progressView.progress = Float(fraction) } + } +) +``` + +### Body types — `HTTPBody` + +`jsonBody(_:)` wraps any `Encodable & Sendable`; `FormURLEncodedBody` (a.k.a. +`.form([:])`) percent-escapes every value and never drops one; `RawBody` lets you +supply the bytes and `Content-Type` yourself. Conform your own type to `HTTPBody` +and `API` accepts it with no change. + +```swift +struct CSVBody: HTTPBody { + let rows: [[String]] + func encoded() throws -> (data: Data, contentType: String) { + let text = rows.map { $0.joined(separator: ",") }.joined(separator: "\n") + return (Data(text.utf8), "text/csv; charset=UTF-8") + } +} +``` + +### `await API.shared.setupCertification(certData:password:)` + +Registers a client certificate (`.p12`) for mutual-TLS on subsequent requests. + +```swift +let p12 = try Data(contentsOf: certURL) +await API.shared.setupCertification(certData: p12, password: "cert-pw") +``` + +
## Strings & Text @@ -45,7 +139,106 @@ Every section is a collapsible block — click a heading to expand it. ## Crypto - +
+RIPEMD_160 — RIPEMD-160 digest + +### `static func hash(_ message: Data) -> Data` + +Returns the 20-byte RIPEMD-160 digest of `message`. Pure Swift, no system +dependency. Mainly useful for Bitcoin-style address hashing (`RIPEMD160(SHA256(x))`). + +```swift +let digest = RIPEMD_160.hash(Data("abc".utf8)) +digest.count // 20 +digest.map { String(format: "%02x", $0) }.joined() +// "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc" +``` + +
+ +
+LCECryptoKitManager — OTP / peppered-login bridge (needs the LCECryptoKit binary) + +A thin facade over the optional `LCECryptoKit` binary product (enabled by the +`LCE_ENABLE_CRYPTO_BINARY` build flag). **When the binary is not linked every +method is a no-op** returning `nil` / `""` / `false`, so calling code still +compiles and runs. + +### `init()` / `init(privateKey:)` + +Create a manager. The `privateKey` (a.k.a. "hash key") is only needed by the +`*WithKey` methods. + +```swift +let crypto = LCECryptoKitManager() +let keyed = LCECryptoKitManager(privateKey: serverHashKey) +``` + +### `static func generateKey() -> String` + +Generates a random AES key string. + +```swift +let key = LCECryptoKitManager.generateKey() +``` + +### `func encodeTP(email:password:) -> String?` / `func decodeOTP(_:) -> String?` + +Encode an email+password pair into an OTP seed hash, and decode it back. + +```swift +let hash = crypto.encodeTP(email: "ana@x.com", password: "s3cr3t") +let back = crypto.decodeOTP(hash ?? "") +``` + +### `func encodeOTPWithKey(email:password:) -> String?` / `func decodeOTPWithKey(_:) -> Bool` + +Same as above but bound to the instance's `privateKey`; `decodeOTPWithKey` +returns whether the hash validates against that key rather than the decoded value. + +```swift +let keyed = LCECryptoKitManager(privateKey: serverHashKey) +let hash = keyed.encodeOTPWithKey(email: "ana@x.com", password: "s3cr3t") +let ok = keyed.decodeOTPWithKey(hash ?? "") // Bool +``` + +### `static func generateSalt() -> String` + +Random salt for the salted/iterated/peppered login flow. + +```swift +let salt = LCECryptoKitManager.generateSalt() +``` + +### `static func computeClientHash(email:password:salt:) -> String` + +Client-side hash of the credentials with the given salt — sent to the server +instead of the raw password. + +```swift +let clientHash = LCECryptoKitManager.computeClientHash( + email: "ana@x.com", password: "s3cr3t", salt: salt +) +``` + +### `static func computeLoginBearerToken(userId:clientHash:) -> String?` + +Derives the login bearer token from a user id and the client hash. + +```swift +let token = LCECryptoKitManager.computeLoginBearerToken(userId: "42", clientHash: clientHash) +``` + +### `static func otpEncode(_:) -> String?` / `static func otpDecode(_:) -> String?` + +One-time-pad encode/decode of an arbitrary string. + +```swift +let enc = LCECryptoKitManager.otpEncode("secret-value") +let dec = LCECryptoKitManager.otpDecode(enc ?? "") // "secret-value" +``` + +
## Core — the `LCEssentials` namespace