2026-08-29 19:22:12 -03:00
|
|
|
# LCEssentials — Extensions
|
|
|
|
|
|
|
|
|
|
Foundation, value-type, string, collection, numeric, date, and crypto helpers, plus
|
|
|
|
|
the `LCEssentials` namespace itself. UIKit extensions live in
|
|
|
|
|
[UIKit.md](UIKit.md); SwiftUI helpers in [SwiftUI.md](SwiftUI.md).
|
|
|
|
|
|
|
|
|
|
Every section is a collapsible block — click a heading to expand it.
|
|
|
|
|
|
|
|
|
|
## Contents
|
|
|
|
|
|
|
|
|
|
- [API & Networking](#api--networking)
|
|
|
|
|
- [Strings & Text](#strings--text)
|
|
|
|
|
- [Collections & Sequences](#collections--sequences)
|
|
|
|
|
- [Numbers & Geometry](#numbers--geometry)
|
|
|
|
|
- [Date, Data & Files](#date-data--files)
|
|
|
|
|
- [Encoding & Errors](#encoding--errors)
|
|
|
|
|
- [Crypto](#crypto)
|
|
|
|
|
- [Core — the `LCEssentials` namespace](#core--the-lcessentials-namespace)
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## API & Networking
|
|
|
|
|
|
2026-08-29 19:23:23 -03:00
|
|
|
`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`).
|
|
|
|
|
|
|
|
|
|
<details>
|
|
|
|
|
<summary><b>API</b> — typed async requests & uploads</summary>
|
|
|
|
|
|
|
|
|
|
### `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")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</details>
|
2026-08-29 19:22:12 -03:00
|
|
|
|
|
|
|
|
## Strings & Text
|
|
|
|
|
|
|
|
|
|
<!-- batch 3 -->
|
|
|
|
|
|
|
|
|
|
## Collections & Sequences
|
|
|
|
|
|
|
|
|
|
<!-- batch 4 -->
|
|
|
|
|
|
|
|
|
|
## Numbers & Geometry
|
|
|
|
|
|
|
|
|
|
<!-- batch 5 -->
|
|
|
|
|
|
|
|
|
|
## Date, Data & Files
|
|
|
|
|
|
|
|
|
|
<!-- batch 6 -->
|
|
|
|
|
|
|
|
|
|
## Encoding & Errors
|
|
|
|
|
|
|
|
|
|
<!-- batch 7 -->
|
|
|
|
|
|
|
|
|
|
## Crypto
|
|
|
|
|
|
2026-08-29 19:23:23 -03:00
|
|
|
<details>
|
|
|
|
|
<summary><b>RIPEMD_160</b> — RIPEMD-160 digest</summary>
|
|
|
|
|
|
|
|
|
|
### `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"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</details>
|
|
|
|
|
|
|
|
|
|
<details>
|
|
|
|
|
<summary><b>LCECryptoKitManager</b> — OTP / peppered-login bridge (needs the <code>LCECryptoKit</code> binary)</summary>
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
</details>
|
2026-08-29 19:22:12 -03:00
|
|
|
|
|
|
|
|
## Core — the `LCEssentials` namespace
|
|
|
|
|
|
|
|
|
|
<!-- batch 7 -->
|