Update Documentation.docc

Summary:
- Sources: Documentation.docc
- Added symbols: extension StorageKey, struct AuthCatalog, struct RemoteKeyProvider, func keyMaterial

Stats:
- 7 files changed, 339 insertions(+)
This commit is contained in:
Matt Bruce 2026-01-17 10:48:31 -06:00
parent f3f42c7844
commit 6fc5ef8d08
7 changed files with 339 additions and 0 deletions

View File

@ -0,0 +1,58 @@
# Getting Started
Follow this guide to define keys, configure LocalData, and register catalogs for auditing.
## 1) Define keys
Keys are static, typed constants. Do not use dynamic key names.
```swift
import LocalData
extension StorageKey where Value == String {
static let userToken = StorageKey(
name: "user_token",
domain: .keychain(service: "com.myapp"),
owner: "Auth",
description: "Current user auth token."
)
}
```
## 2) Configure the router (optional)
Set defaults once at app launch to avoid repeating service identifiers.
```swift
let storageConfig = StorageConfiguration(
defaultKeychainService: "com.myapp.keychain",
defaultAppGroupIdentifier: "group.com.myapp"
)
await StorageRouter.shared.updateStorageConfiguration(storageConfig)
```
## 3) Register catalogs
Catalogs are mandatory if you want auditing and duplicate detection.
```swift
struct AuthCatalog: StorageKeyCatalog {
let allKeys: [AnyStorageKey] = [
.key(StorageKey.userToken)
]
}
try await StorageRouter.shared.registerCatalog(AuthCatalog())
```
## 4) Store and read values
```swift
try await StorageRouter.shared.set("token", for: .userToken)
let token = try await StorageRouter.shared.get(.userToken)
```
## Next steps
- Read <doc:KeyAndCatalogDiscipline> to avoid audit gaps.
- Add migrations in <doc:Migrations>.

View File

@ -0,0 +1,39 @@
# Key and Catalog Discipline
This package is designed for static, discoverable keys. Treat your key catalog as your storage manifest.
## Static keys only
Do not generate dynamic key names at runtime. If a value can exist, it should have a corresponding
static ``StorageKey`` definition. This keeps audits accurate and prevents invisible storage usage.
If you must represent multiple instances (for example, per-account settings), create a static key
for each supported slot or store a dictionary keyed by account ID inside a single ``StorageKey``.
## Avoid audit gaps
Catalog registration enforces correctness:
- Missing descriptions are rejected.
- Duplicate key names across catalogs are rejected.
- Unregistered keys throw ``StorageError/unregisteredKey(_:)`` at runtime.
If you skip catalog registration, LocalData cannot guarantee that all writes are auditable.
## Modular catalogs
Split catalogs by feature or module and register them incrementally:
```swift
try await StorageRouter.shared.registerCatalog(AuthCatalog())
try await StorageRouter.shared.registerCatalog(ProfileCatalog())
```
Registration is additive. A collision in key names across catalogs throws an error.
## Recommended workflow
1. Define keys in `StorageKeys/` per feature.
2. Create a catalog for each feature.
3. Register all catalogs on app launch.
4. Run ``StorageAuditReport`` to verify output.

View File

@ -0,0 +1,43 @@
# LocalData
LocalData is a typed, auditable storage layer that unifies UserDefaults, Keychain, and file storage under a single API. It favors static, discoverable keys and explicit catalogs so teams can reason about what is stored, where it lives, and how it is secured.
## Overview
LocalData revolves around three concepts:
- ``StorageKey`` defines storage metadata for a single value.
- ``StorageRouter`` coordinates serialization, security, and routing.
- ``StorageKeyCatalog`` registers keys for auditing and migration.
## Topics
### Getting Started
- <doc:GettingStarted>
- <doc:KeyAndCatalogDiscipline>
### Migrations
- <doc:Migrations>
### Sync
- <doc:WatchSync>
### Security
- <doc:Security>
### Testing
- <doc:Testing>
### Reference
- ``StorageRouter``
- ``StorageKey``
- ``StorageKeyCatalog``
- ``StorageDomain``
- ``SecurityPolicy``
- ``StorageError``

View File

@ -0,0 +1,72 @@
# Migrations
LocalData supports lazy and proactive migrations to move data from legacy keys to modern keys.
## Lazy migrations (on read)
Attach a migration to a key. If the destination is missing, `StorageRouter.get(_:)` will run it.
```swift
extension StorageKey where Value == String {
static let legacyToken = StorageKey(
name: "legacy_token",
domain: .userDefaults(suite: nil),
security: .none,
owner: "Auth",
description: "Legacy token."
)
static let userToken = StorageKey(
name: "user_token",
domain: .keychain(service: "com.myapp"),
owner: "Auth",
description: "Modern token.",
migration: { destination in
AnyStorageMigration(
SimpleLegacyMigration(
destinationKey: destination,
sourceKey: .key(StorageKey.legacyToken)
)
)
}
)
}
```
## Proactive sweeps
Run migrations at startup to drain legacy values:
```swift
try await StorageRouter.shared.registerCatalog(AuthCatalog(), migrateImmediately: true)
```
## Transforming migrations
Use `DefaultTransformingMigration` when types change.
```swift
let migration = DefaultTransformingMigration(
destinationKey: StorageKey.userAge,
sourceKey: StorageKey.legacyAgeString
) { value in
guard let intValue = Int(value) else {
throw MigrationError.transformationFailed("Invalid age")
}
return intValue
}
```
## Aggregating migrations
Combine multiple legacy values into one destination:
```swift
let migration = DefaultAggregatingMigration(
destinationKey: StorageKey.profileSummary,
sourceKeys: [.key(StorageKey.firstName), .key(StorageKey.lastName)]
) { sources in
let parts = sources.compactMap { $0.value as? String }
return parts.joined(separator: " ")
}
```

View File

@ -0,0 +1,61 @@
# Security
Security is declared per key using ``SecurityPolicy``.
## Recommended default
Use the default security policy unless you have a specific reason not to:
```swift
let key = StorageKey(
name: "secure_value",
domain: .fileSystem(directory: .documents),
owner: "Security",
description: "Sensitive value stored with recommended policy."
)
```
## Keychain security
Store data directly in Keychain with accessibility and access control:
```swift
let key = StorageKey(
name: "token",
domain: .keychain(service: "com.myapp"),
security: .keychain(
accessibility: .afterFirstUnlock,
accessControl: .biometryAny
),
owner: "Auth",
description: "Auth token."
)
```
## Encrypted file storage
Use encryption for file-based storage:
```swift
let key = StorageKey(
name: "secret_file",
domain: .encryptedFileSystem(directory: .documents),
owner: "Vault",
description: "Encrypted file data."
)
```
## External key material
Register a provider and reference it in the policy:
```swift
struct RemoteKeyProvider: KeyMaterialProviding {
func keyMaterial(for keyName: String) async throws -> Data {
Data(repeating: 1, count: 32)
}
}
let source = KeyMaterialSource(id: "remote.key")
await StorageRouter.shared.registerKeyMaterialProvider(RemoteKeyProvider(), for: source)
```

View File

@ -0,0 +1,32 @@
# Testing
Use isolated helpers and custom configurations to avoid polluting real storage.
## Use temporary file locations
```swift
let testURL = FileManager.default.temporaryDirectory
.appending(path: "LocalDataTests-\(UUID().uuidString)")
let router = StorageRouter(
keychain: MockKeychainHelper(),
encryption: EncryptionHelper(keychain: MockKeychainHelper()),
file: FileStorageHelper(configuration: FileStorageConfiguration(baseURL: testURL)),
defaults: UserDefaultsHelper(defaults: UserDefaults(suiteName: "LocalDataTests")!)
)
```
## App Group identifiers
Use a mock `StorageConfiguration` to avoid real entitlements:
```swift
await router.updateStorageConfiguration(
StorageConfiguration(defaultAppGroupIdentifier: "group.test")
)
```
## Keychain in simulator
Keychain tests in the simulator often require a host app with entitlements.
If you see `-34018`, ensure the test target has a host app and keychain sharing.

View File

@ -0,0 +1,34 @@
# Watch Sync
LocalData supports WatchConnectivity application context sync for eligible keys.
## Eligibility rules
A key syncs only when:
- `availability` is `.all` or `.phoneWithWatchSync`
- `syncPolicy` is `.manual` or `.automaticSmall`
For `.automaticSmall`, the payload must be below `SyncConfiguration.maxAutoSyncSize`.
## Bootstrapping at launch
Call this on iOS after session activation or reachability changes:
```swift
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
```
## Responding to watch requests
Build a snapshot and return it via `WCSession`:
```swift
let snapshot = await StorageRouter.shared.syncSnapshot()
```
## Best practices
- Activate `WCSession` in the app, not in LocalData.
- Use a launch-order-safe handshake (watch first or phone first).
- Avoid large payloads; prefer smaller derived values for watch display.