Skip to content

Commit c16b972

Browse files
committed
migrate code
1 parent cfc402d commit c16b972

File tree

126 files changed

+12466
-5371
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+12466
-5371
lines changed

.claude/implementation-patterns.md

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
# Implementation History and Patterns
2+
3+
> **Note**: This is a detailed reference guide documenting implementation decisions, patterns, and lessons learned. For daily development, see the main [CLAUDE.md](../CLAUDE.md) file.
4+
5+
This document covers key implementation decisions, patterns, and lessons learned during BushelCloud development. Use this as reference when building similar CloudKit demos.
6+
7+
## Data Source Integration Pattern
8+
9+
BushelCloud integrates multiple external data sources. Here's the pattern for adding new sources:
10+
11+
**Step 1: Create Fetcher**
12+
```swift
13+
struct AppleDBFetcher: Sendable {
14+
func fetch() async throws -> [RestoreImageRecord] {
15+
// 1. Fetch data from external API
16+
// 2. Parse and map to domain model
17+
// 3. Return array of records
18+
}
19+
}
20+
```
21+
22+
**Step 2: Add to Pipeline**
23+
```swift
24+
// In DataSourcePipeline.swift
25+
private func fetchRestoreImages(options: Options) async throws -> [RestoreImageRecord] {
26+
async let ipswImages = IPSWFetcher().fetch()
27+
async let appleDBImages = AppleDBFetcher().fetch()
28+
29+
var allImages: [RestoreImageRecord] = []
30+
allImages.append(contentsOf: try await ipswImages)
31+
allImages.append(contentsOf: try await appleDBImages)
32+
33+
return deduplicateRestoreImages(allImages)
34+
}
35+
```
36+
37+
**Step 3: Add CLI Option (Optional)**
38+
```swift
39+
struct SyncCommand {
40+
@Flag(name: .long, help: "Exclude AppleDB.dev as data source")
41+
var noAppleDB: Bool = false
42+
43+
private func buildSyncOptions() -> SyncEngine.SyncOptions {
44+
var pipelineOptions = DataSourcePipeline.Options()
45+
if noAppleDB {
46+
pipelineOptions.includeAppleDB = false
47+
}
48+
return pipelineOptions
49+
}
50+
}
51+
```
52+
53+
## Deduplication Strategy
54+
55+
**Build Number as Unique Key:**
56+
57+
Multiple sources provide the same restore images. BushelCloud uses `buildNumber` as the unique key:
58+
59+
```swift
60+
private func deduplicateRestoreImages(_ images: [RestoreImageRecord]) -> [RestoreImageRecord] {
61+
var uniqueImages: [String: RestoreImageRecord] = [:]
62+
63+
for image in images {
64+
let key = image.buildNumber
65+
66+
if let existing = uniqueImages[key] {
67+
// Merge records, prefer most complete data
68+
uniqueImages[key] = mergeRestoreImages(existing, image)
69+
} else {
70+
uniqueImages[key] = image
71+
}
72+
}
73+
74+
return Array(uniqueImages.values)
75+
.sorted { $0.releaseDate > $1.releaseDate }
76+
}
77+
```
78+
79+
**Merge Priority Rules:**
80+
1. **IPSW.me** - Most complete data (has both SHA1 + SHA256)
81+
2. **AppleDB** - Device-specific signing status, comprehensive coverage
82+
3. **MESU** - Authoritative for signing status (freshness detection)
83+
4. **MrMacintosh** - Beta/RC releases, community-maintained
84+
85+
**Merge Logic:**
86+
```swift
87+
private func mergeRestoreImages(
88+
_ existing: RestoreImageRecord,
89+
_ new: RestoreImageRecord
90+
) -> RestoreImageRecord {
91+
var merged = existing
92+
93+
// Prefer more recent sourceUpdatedAt
94+
if new.sourceUpdatedAt > existing.sourceUpdatedAt {
95+
merged = new
96+
}
97+
98+
// Backfill missing SHA hashes
99+
if merged.sha256Hash.isEmpty && !new.sha256Hash.isEmpty {
100+
merged.sha256Hash = new.sha256Hash
101+
}
102+
if merged.sha1Hash.isEmpty && !new.sha1Hash.isEmpty {
103+
merged.sha1Hash = new.sha1Hash
104+
}
105+
106+
// MESU is authoritative for signing status
107+
if new.source == "MESU" {
108+
merged.isSigned = new.isSigned
109+
}
110+
111+
return merged
112+
}
113+
```
114+
115+
## AppleDB Integration
116+
117+
AppleDB was added to provide comprehensive restore image data with device-specific signing status.
118+
119+
**API Endpoint:**
120+
```swift
121+
let url = URL(string: "https://api.appledb.dev/ios/VirtualMac2,1.json")!
122+
```
123+
124+
**Key Features:**
125+
- Device filtering for VirtualMac variants
126+
- File size parsing (string → Int64)
127+
- Prerelease detection (beta/RC in version string)
128+
- Signing status per device
129+
130+
**Implementation Files:**
131+
- `AppleDB/AppleDBParser.swift` - API client
132+
- `AppleDB/AppleDBFetcher.swift` - Fetcher pattern implementation
133+
- `AppleDB/Models/AppleDBVersion.swift` - Domain model
134+
- `AppleDB/Models/AppleDBAPITypes.swift` - API response types
135+
136+
## Server-to-Server Authentication Migration
137+
138+
BushelCloud was refactored from API Tokens to S2S Keys to demonstrate enterprise authentication patterns.
139+
140+
**What Changed:**
141+
142+
| Before (API Token) | After (S2S Key) |
143+
|-------------------|-----------------|
144+
| Single token string | Key ID + Private Key (.pem) |
145+
| `APITokenManager` | `ServerToServerAuthManager` |
146+
| `CLOUDKIT_API_TOKEN` env var | `CLOUDKIT_KEY_ID` + `CLOUDKIT_KEY_FILE` |
147+
| `--api-token` flag | `--key-id` + `--key-file` flags |
148+
149+
**Migration Steps:**
150+
1. Generate ECDSA key pair with OpenSSL
151+
2. Register public key in CloudKit Dashboard
152+
3. Update `BushelCloudKitService` to use `ServerToServerAuthManager`
153+
4. Update all commands to accept new parameters
154+
5. Update environment variable handling
155+
6. Update documentation
156+
157+
## Critical Issues Solved
158+
159+
### Issue 1: CloudKit Schema Validation Errors
160+
161+
**Problem:** `cktool validate-schema` failed with parsing error.
162+
163+
**Root Cause:** Schema file missing `DEFINE SCHEMA` header and included system fields.
164+
165+
**Solution:**
166+
```text
167+
# Wrong
168+
RECORD TYPE RestoreImage (
169+
"__recordID" RECORD ID, ❌
170+
171+
# Correct
172+
DEFINE SCHEMA ✅
173+
174+
RECORD TYPE RestoreImage (
175+
"version" STRING, ✅
176+
```
177+
178+
**Lesson:** CloudKit auto-adds system fields. Never include them in schema definitions.
179+
180+
### Issue 2: ACCESS_DENIED Errors Despite Correct Permissions
181+
182+
**Problem:** Record creation failed with ACCESS_DENIED even after adding `_creator` permissions.
183+
184+
**Root Cause:** Schema needed BOTH `_creator` AND `_icloud` permissions.
185+
186+
**Solution:**
187+
```text
188+
GRANT READ, CREATE, WRITE TO "_creator",
189+
GRANT READ, CREATE, WRITE TO "_icloud", ← Both required!
190+
```
191+
192+
**Lesson:** S2S authentication with public database requires permissions for both roles.
193+
194+
### Issue 3: cktool Command Syntax Confusion
195+
196+
**Problem:** Script used invalid cktool commands and flags.
197+
198+
**Incorrect:**
199+
```bash
200+
xcrun cktool list-containers # ❌ Not a valid command
201+
xcrun cktool validate-schema schema.ckdb # ❌ Missing --file flag
202+
```
203+
204+
**Correct:**
205+
```bash
206+
xcrun cktool get-teams # ✅ Valid auth test command
207+
xcrun cktool validate-schema --file schema.ckdb # ✅ Correct syntax
208+
```
209+
210+
**Lesson:** Always check cktool syntax with `xcrun cktool --help`.
211+
212+
## Token Types Reference
213+
214+
CloudKit uses different tokens for different operations:
215+
216+
| Token Type | Purpose | Used By | How to Get |
217+
|-----------|---------|---------|------------|
218+
| **Management Token** | Schema operations (import/export) | `cktool` | Dashboard → CloudKit Web Services |
219+
| **Server-to-Server Key** | Runtime API operations (server-side) | Your application | Dashboard → Server-to-Server Keys |
220+
| **API Token** | Simpler runtime auth (legacy) | Legacy apps | Dashboard → API Tokens |
221+
| **User Token** | User-specific operations | Web apps | OAuth flow |
222+
223+
**For BushelCloud:**
224+
- Schema setup: **Management Token** (via `cktool save-token`)
225+
- Sync/export: **Server-to-Server Key** (Key ID + .pem file)
226+
227+
## Date Handling with CloudKit
228+
229+
CloudKit dates use **milliseconds since epoch**, not seconds:
230+
231+
```swift
232+
// MistKit handles conversion automatically
233+
fields["releaseDate"] = .date(Date()) // ✅ Converted to milliseconds
234+
235+
// If manually creating timestamp
236+
let milliseconds = Int64(date.timeIntervalSince1970 * 1000)
237+
fields["releaseDate"] = .int64(milliseconds)
238+
```
239+
240+
## Boolean Fields in CloudKit
241+
242+
CloudKit has no native boolean type. Use INT64 with 0/1:
243+
244+
**Schema:**
245+
```text
246+
"isSigned" INT64 QUERYABLE,
247+
"isPrerelease" INT64 QUERYABLE,
248+
```
249+
250+
**Swift code:**
251+
```swift
252+
fields["isSigned"] = .int64(record.isSigned ? 1 : 0)
253+
fields["isPrerelease"] = .int64(record.isPrerelease ? 1 : 0)
254+
```
255+
256+
**Reading back:**
257+
```swift
258+
if case .int64(let value) = fields["isSigned"] {
259+
let isSigned = value == 1
260+
}
261+
```
262+
263+
## Batch Operation Optimization
264+
265+
CloudKit limits: **200 operations per request**
266+
267+
**Efficient batching:**
268+
```swift
269+
let batchSize = 200
270+
let batches = operations.chunked(into: batchSize)
271+
272+
for (index, batch) in batches.enumerated() {
273+
print("Batch \(index + 1)/\(batches.count)...")
274+
let results = try await service.modifyRecords(batch)
275+
276+
// Process results immediately
277+
for result in results {
278+
if result.recordType == "Unknown" {
279+
// Handle error
280+
}
281+
}
282+
}
283+
```
284+
285+
**Don't batch too small:** Each request has overhead. Use full 200-operation batches when possible.
286+
287+
## Reference Field Ordering
288+
289+
Upload order matters for records with references:
290+
291+
```text
292+
SwiftVersion (no dependencies)
293+
294+
RestoreImage (no dependencies)
295+
296+
XcodeVersion (references both)
297+
```
298+
299+
**Correct upload order:**
300+
```swift
301+
// 1. Records with no dependencies first
302+
try await syncSwiftVersions()
303+
try await syncRestoreImages()
304+
305+
// 2. Records with references last
306+
try await syncXcodeVersions() // References uploaded records
307+
```
308+
309+
**Wrong order causes:** `VALIDATING_REFERENCE_ERROR`
310+
311+
## Error Handling Best Practices
312+
313+
**Check for partial failures:**
314+
```swift
315+
let results = try await service.modifyRecords(batch)
316+
let errors = results.filter { $0.recordType == "Unknown" }
317+
318+
if !errors.isEmpty {
319+
for error in errors {
320+
print("Failed: \(error.recordName ?? "unknown")")
321+
print("Reason: \(error.reason ?? "N/A")")
322+
}
323+
}
324+
```
325+
326+
**Common recoverable errors:**
327+
- `VALIDATING_REFERENCE_ERROR` - Retry after uploading referenced records
328+
- `CONFLICT` - Use `.forceReplace` instead of `.create`
329+
- `QUOTA_EXCEEDED` - Reduce batch size or wait
330+
331+
**Non-recoverable errors:**
332+
- `ACCESS_DENIED` - Fix schema permissions
333+
- `AUTHENTICATION_FAILED` - Fix key ID/PEM file
334+
335+
## Lessons for Future Demos
336+
337+
When building similar CloudKit demos (e.g., Celestra):
338+
339+
**1. Start with S2S Keys from the beginning**
340+
- More secure and production-ready
341+
- Better demonstrates enterprise patterns
342+
343+
**2. Schema setup first**
344+
- Create schema with `DEFINE SCHEMA` header
345+
- Include both `_creator` and `_icloud` permissions
346+
- Test with cktool before app development
347+
348+
**3. Use the DataSourcePipeline pattern**
349+
- Parallel fetching with `async let`
350+
- Deduplication by unique key
351+
- Merge priority rules for conflict resolution
352+
353+
**4. Reusable patterns from BushelCloud:**
354+
- `BushelCloudKitService` wrapper pattern
355+
- `CloudKitRecord` protocol for models
356+
- CLI structure with swift-argument-parser
357+
- Environment variable handling
358+
- Batch operation chunking
359+
360+
**5. Documentation structure:**
361+
- README for user-facing quick start
362+
- CLAUDE.md for development context
363+
- DocC for comprehensive tutorials
364+
- No separate Documentation/ directory
365+
366+
## Common Pitfalls to Avoid
367+
368+
**❌ Don't:**
369+
- Commit .pem files to git
370+
- Use system fields in schema
371+
- Grant permissions to only one role
372+
- Upload references before referenced records
373+
- Batch operations larger than 200
374+
- Assume boolean type exists in CloudKit
375+
- Use seconds for timestamps (use milliseconds)
376+
377+
**✅ Do:**
378+
- Use environment variables for credentials
379+
- Start schema with `DEFINE SCHEMA`
380+
- Grant to both `_creator` and `_icloud`
381+
- Upload in dependency order
382+
- Chunk operations to 200 max
383+
- Use INT64 (0/1) for booleans
384+
- Let MistKit handle date conversion

0 commit comments

Comments
 (0)